mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-16 20:38:35 +00:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ad4def35f | ||
|
|
f7f50caa6e | ||
|
|
3e54e7d00b | ||
|
|
827b50f169 | ||
|
|
58a59c37ed | ||
|
|
986065c16f | ||
|
|
37cb4cdbe3 | ||
|
|
2ddf6ee148 | ||
|
|
8ce4f71427 | ||
|
|
6e1eb88e6e | ||
|
|
49cdc105de | ||
|
|
636d2d4851 | ||
|
|
7d7275847d | ||
|
|
f0fd6ad85e | ||
|
|
5d8fd9824e | ||
|
|
346ea40f69 | ||
|
|
8809bdf3f0 | ||
|
|
deaf596494 | ||
|
|
7863ac8cf6 | ||
|
|
b3b38e4416 | ||
|
|
ccd5dc5e06 | ||
|
|
b2453b626e | ||
|
|
de6e5d3a94 | ||
|
|
fac20a06f3 | ||
|
|
9a7c5ea593 | ||
|
|
7281e0606a | ||
|
|
71f5555873 | ||
|
|
3967ed6d54 | ||
|
|
e0151229b6 | ||
|
|
b6a899583b | ||
|
|
dc3bd9cf00 | ||
|
|
1e8b136bfb | ||
|
|
3c47af779c | ||
|
|
c28d389e0e | ||
|
|
d83a84510e | ||
|
|
f8fba079fe | ||
|
|
42502e4263 | ||
|
|
36c165f74d | ||
|
|
f7ea645bf4 | ||
|
|
50527485d3 | ||
|
|
421af6db79 | ||
|
|
fee4bfc22e | ||
|
|
5e3d20b3ed | ||
|
|
9e2aaf6f60 | ||
|
|
ec042fefee | ||
|
|
45e4b8899d | ||
|
|
c5dc408596 | ||
|
|
473fe44a85 |
@@ -366,7 +366,6 @@ words:
|
||||
- venv
|
||||
- vfalco
|
||||
- vinnie
|
||||
- vkeylet
|
||||
- wasmi
|
||||
- wextra
|
||||
- wptr
|
||||
@@ -387,3 +386,4 @@ words:
|
||||
- xxhasher
|
||||
- zstdio
|
||||
- CGNAT
|
||||
- ungated
|
||||
|
||||
3
.envrc
3
.envrc
@@ -1,5 +1,8 @@
|
||||
watch_file nix/*.nix
|
||||
|
||||
# Pinned Rust toolchain, read by nix/packages.nix via fromRustupToolchainFile.
|
||||
watch_file rust-toolchain.toml
|
||||
|
||||
# The dev shell derivation includes all of conan/ (see nix/devshell.nix), so any
|
||||
# change in there has to invalidate direnv's cached environment.
|
||||
watch_dir conan
|
||||
|
||||
58
.github/actions/release-info/action.yml
vendored
58
.github/actions/release-info/action.yml
vendored
@@ -7,10 +7,10 @@ outputs:
|
||||
value: ${{ steps.version.outputs.version }}
|
||||
channel:
|
||||
description: "The release channel this build belongs to."
|
||||
value: ${{ steps.channel.outputs.channel }}
|
||||
value: ${{ steps.release_info.outputs.channel }}
|
||||
pkg_release:
|
||||
description: "The package release number: 1 for a tag, the run number otherwise."
|
||||
value: ${{ steps.pkg_release.outputs.pkg_release }}
|
||||
description: "The package release number: 1 for a tag, <run number>.<commit date>git<short commit hash> otherwise."
|
||||
value: ${{ steps.release_info.outputs.pkg_release }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
@@ -39,52 +39,6 @@ runs:
|
||||
|
||||
echo "version=${version}" | tee -a "${GITHUB_OUTPUT}"
|
||||
|
||||
# Only a tag says how mature a build is: a push is a develop build whatever
|
||||
# its version, and a non-public codebase keeps its packages to itself.
|
||||
- name: Determine release channel
|
||||
id: channel
|
||||
shell: bash
|
||||
env:
|
||||
IS_TAG: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
VISIBILITY: ${{ github.event.repository.visibility }}
|
||||
run: |
|
||||
pre_release=""
|
||||
if [[ "${REF_NAME}" == *-* ]]; then
|
||||
pre_release="${REF_NAME#*-}"
|
||||
fi
|
||||
|
||||
if [[ "${VISIBILITY}" != "public" ]]; then
|
||||
channel=private
|
||||
elif [[ "${IS_TAG}" != "true" ]]; then
|
||||
channel=develop
|
||||
elif [[ -z "${pre_release}" ]]; then
|
||||
channel=stable
|
||||
elif [[ "${pre_release}" =~ ^rc[0-9]+(\+.*)?$ ]]; then
|
||||
channel=unstable
|
||||
elif [[ "${pre_release}" =~ ^b(0|[1-9][0-9]*)(\+.*)?$ ]]; then
|
||||
channel=experimental
|
||||
else
|
||||
echo "Unsupported pre-release in tag '${REF_NAME}'. Use bN or rcN." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "channel=${channel}" | tee -a "${GITHUB_OUTPUT}"
|
||||
|
||||
# A tag is packaged once, so its release number is fixed at 1. Develop builds
|
||||
# repeat the same version, so the run number is what makes each push an
|
||||
# upgrade rather than a reinstall.
|
||||
- name: Determine package release
|
||||
id: pkg_release
|
||||
shell: bash
|
||||
env:
|
||||
IS_TAG: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
RUN_NUMBER: ${{ github.run_number }}
|
||||
run: |
|
||||
if [[ "${IS_TAG}" == "true" ]]; then
|
||||
pkg_release=1
|
||||
else
|
||||
pkg_release="${RUN_NUMBER}"
|
||||
fi
|
||||
|
||||
echo "pkg_release=${pkg_release}" | tee -a "${GITHUB_OUTPUT}"
|
||||
- name: Determine release channel and package release
|
||||
id: release_info
|
||||
uses: XRPLF/actions/release-info@7cc0e4a8d9d0b838f92c48d312856b190341bbba
|
||||
|
||||
9
.github/actions/setup-nix-env/action.yml
vendored
9
.github/actions/setup-nix-env/action.yml
vendored
@@ -40,10 +40,11 @@ runs:
|
||||
# Unlike the Linux nix images, macOS needs no SSL_CERT_FILE: it has its
|
||||
# own trust store, and pinning would break TLS to hosts relying on it.
|
||||
|
||||
# Workspace-local, so `cleanup-workspace` clears it, but not the
|
||||
# `.conan2` prepare-runner hands the system toolchain: that Conan is a
|
||||
# different version, and the two would migrate each other's cache.
|
||||
echo "CONAN_HOME=${{ github.workspace }}/.conan2-nix" >>"${GITHUB_ENV}"
|
||||
# In RUNNER_TEMP, which the runner empties per job, like the `.conan2`
|
||||
# prepare-runner hands the system toolchain - but under its own name:
|
||||
# that Conan is a different version, and the two would migrate each
|
||||
# other's cache.
|
||||
echo "CONAN_HOME=${RUNNER_TEMP}/.conan2-nix" >>"${GITHUB_ENV}"
|
||||
|
||||
# Config, profiles and remote, exactly as the dev shell sets them up on
|
||||
# entry; the `setup-conan` action is skipped for this toolchain.
|
||||
|
||||
4
.github/pull_request_template.md
vendored
4
.github/pull_request_template.md
vendored
@@ -18,7 +18,7 @@ If too broad, please consider splitting into multiple PRs.
|
||||
If there is a relevant task or issue, please link it here.
|
||||
-->
|
||||
|
||||
### Context of Change
|
||||
## Context of Change
|
||||
|
||||
<!--
|
||||
Please include the context of a change.
|
||||
@@ -29,7 +29,7 @@ If a refactor, how is this better than the previous implementation?
|
||||
If there is a spec or design document for this feature, please link it here.
|
||||
-->
|
||||
|
||||
### API Impact
|
||||
## API Impact
|
||||
|
||||
<!--
|
||||
Please check [x] relevant options, delete irrelevant ones.
|
||||
|
||||
101
.github/scripts/strategy-matrix/generate.py
vendored
101
.github/scripts/strategy-matrix/generate.py
vendored
@@ -23,6 +23,19 @@ _SANITIZER_SUFFIX: dict[str, str] = {
|
||||
}
|
||||
|
||||
|
||||
def config_name(
|
||||
distro: str,
|
||||
compiler: str,
|
||||
build_type: str,
|
||||
arch: str,
|
||||
suffix: str = "",
|
||||
sanitizer: str = "",
|
||||
) -> str:
|
||||
"""Name a config. Its artifacts are named after it, so packaging reuses this."""
|
||||
parts = [s for s in [suffix, _SANITIZER_SUFFIX.get(sanitizer, "")] if s]
|
||||
return "-".join([f"{distro}-{compiler}-{build_type.lower()}-{arch}", *parts])
|
||||
|
||||
|
||||
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()
|
||||
@@ -37,17 +50,27 @@ def get_cmake_args(build_type: str, extra_args: str) -> str:
|
||||
|
||||
|
||||
# Every config must declare 'minimal'. Minimal configs form the reduced matrix
|
||||
# built for pull requests by default; the full matrix adds the rest. Packaging
|
||||
# configs declare it too, but packaging is gated in the workflow, not by it.
|
||||
# built for pull requests by default; the full matrix adds the rest.
|
||||
#
|
||||
# Configs may also opt into 'benchmark' to smoke-run the benchmarks. Note that
|
||||
# the flag applies to every entry a config expands into, so only set it on
|
||||
# configs that expand to a single combination.
|
||||
# Configs may also opt into 'benchmark' to smoke-run the benchmarks, or carry a
|
||||
# 'package' map to be packaged as well. Note that either applies to every entry
|
||||
# a config expands into, so only set them on configs that expand to a single
|
||||
# combination.
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class PackageConfig:
|
||||
"""The 'package' map of a config whose binaries are also packaged."""
|
||||
|
||||
type: str # "deb" or "rpm"; has to match what the image provides
|
||||
# The packaging container image: a vanilla distro image, not the nix image
|
||||
# the config itself builds in.
|
||||
image: str
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class LinuxConfig:
|
||||
"""One entry in linux.json's 'configs' or 'package_configs' arrays."""
|
||||
"""One entry in a linux.json 'configs' array."""
|
||||
|
||||
compiler: list[str]
|
||||
build_type: list[str]
|
||||
@@ -57,7 +80,11 @@ class LinuxConfig:
|
||||
sanitizers: list[str] = dataclasses.field(default_factory=list)
|
||||
suffix: str = ""
|
||||
extra_cmake_args: str = ""
|
||||
image: str = "" # only used by package_configs entries
|
||||
package: PackageConfig | None = None # set to also package this config
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if isinstance(self.package, dict):
|
||||
self.package = PackageConfig(**self.package)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
@@ -66,22 +93,16 @@ class LinuxFile:
|
||||
|
||||
image_tag: str
|
||||
configs: dict[str, list[LinuxConfig]] # distro → configs
|
||||
package_configs: dict[str, list[LinuxConfig]] # distro → packaging configs
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: Path) -> "LinuxFile":
|
||||
data = json.loads(path.read_text())
|
||||
|
||||
def parse(section: dict) -> dict[str, list[LinuxConfig]]:
|
||||
return {
|
||||
distro: [LinuxConfig(**c) for c in cfgs]
|
||||
for distro, cfgs in section.items()
|
||||
}
|
||||
|
||||
return cls(
|
||||
image_tag=data["image_tag"],
|
||||
configs=parse(data["configs"]),
|
||||
package_configs=parse(data.get("package_configs", {})),
|
||||
configs={
|
||||
distro: [LinuxConfig(**c) for c in cfgs]
|
||||
for distro, cfgs in data["configs"].items()
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -156,7 +177,7 @@ class PackagingEntry:
|
||||
xrpld_artifact_name: str
|
||||
validator_keys_artifact_name: str
|
||||
image: str
|
||||
distro: str # e.g. "debian" or "rhel"; drives package-format-specific steps
|
||||
package_type: str # "deb" or "rpm"; drives the format-specific steps
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -197,13 +218,9 @@ def expand_linux_matrix(linux: LinuxFile, minimal: bool) -> list[MatrixEntry]:
|
||||
effective_sanitizers,
|
||||
effective_archs.items(),
|
||||
):
|
||||
name = f"{distro}-{compiler}-{build_type.lower()}-{arch}"
|
||||
suffix_parts = [
|
||||
s for s in [cfg.suffix, _SANITIZER_SUFFIX.get(sanitizer, "")] if s
|
||||
]
|
||||
if suffix_parts:
|
||||
name += "-" + "-".join(suffix_parts)
|
||||
|
||||
name = config_name(
|
||||
distro, compiler, build_type, arch, cfg.suffix, sanitizer
|
||||
)
|
||||
entries.append(
|
||||
MatrixEntry(
|
||||
config_name=name,
|
||||
@@ -223,27 +240,33 @@ def expand_linux_matrix(linux: LinuxFile, minimal: bool) -> list[MatrixEntry]:
|
||||
|
||||
|
||||
def expand_linux_packaging(linux: LinuxFile) -> list[PackagingEntry]:
|
||||
"""Generate the packaging matrix from a LinuxFile's package_configs section.
|
||||
"""Generate the packaging matrix from the configs that carry a 'package' map.
|
||||
|
||||
Packaging uses vanilla distro images (debian:bookworm, almalinux:9) instead of
|
||||
the nix-based build images, because deb/rpm tooling (debhelper, rpm-build)
|
||||
is taken from the distro's archive rather than from nixpkgs. Each config
|
||||
entry carries its own 'image'.
|
||||
Packaging consumes the binaries that config's build job uploaded, so the
|
||||
artifact names come from the same config name, and a packaged config is one
|
||||
that passes -Dvalidator_keys=ON.
|
||||
|
||||
The artifact names must match what the build job uploads: one artifact per
|
||||
binary, each named after the build config.
|
||||
Packaging itself runs in vanilla distro images (debian:trixie, almalinux:10)
|
||||
instead of the nix-based build images, because deb/rpm tooling (debhelper,
|
||||
rpm-build) is taken from the distro's archive rather than from nixpkgs.
|
||||
"""
|
||||
entries = []
|
||||
for distro, configs in linux.package_configs.items():
|
||||
for distro, configs in linux.configs.items():
|
||||
for cfg in configs:
|
||||
for compiler, build_type in itertools.product(cfg.compiler, cfg.build_type):
|
||||
config_name = f"{distro}-{compiler}-{build_type.lower()}-amd64"
|
||||
if cfg.package is None:
|
||||
continue
|
||||
for compiler, build_type, arch in itertools.product(
|
||||
cfg.compiler, cfg.build_type, cfg.arch
|
||||
):
|
||||
# The packaging workflow hardcodes an amd64 runner.
|
||||
assert arch == "amd64", f"cannot package {distro} on {arch}"
|
||||
name = config_name(distro, compiler, build_type, arch, cfg.suffix)
|
||||
entries.append(
|
||||
PackagingEntry(
|
||||
xrpld_artifact_name=f"xrpld-{config_name}",
|
||||
validator_keys_artifact_name=f"validator-keys-{config_name}",
|
||||
image=cfg.image,
|
||||
distro=distro,
|
||||
xrpld_artifact_name=f"xrpld-{name}",
|
||||
validator_keys_artifact_name=f"validator-keys-{name}",
|
||||
image=cfg.package.image,
|
||||
package_type=cfg.package.type,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
35
.github/scripts/strategy-matrix/linux.json
vendored
35
.github/scripts/strategy-matrix/linux.json
vendored
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"image_tag": "sha-a0074f8",
|
||||
"image_tag": "sha-473fe44",
|
||||
"configs": {
|
||||
"ubuntu": [
|
||||
{
|
||||
@@ -71,7 +71,11 @@
|
||||
"build_type": ["Release"],
|
||||
"arch": ["amd64"],
|
||||
"minimal": false,
|
||||
"extra_cmake_args": "-Dvalidator_keys=ON"
|
||||
"extra_cmake_args": "-Dvalidator_keys=ON",
|
||||
"package": {
|
||||
"type": "deb",
|
||||
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-49cdc10"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -81,28 +85,11 @@
|
||||
"build_type": ["Release"],
|
||||
"arch": ["amd64"],
|
||||
"minimal": false,
|
||||
"extra_cmake_args": "-Dvalidator_keys=ON"
|
||||
}
|
||||
]
|
||||
},
|
||||
"package_configs": {
|
||||
"debian": [
|
||||
{
|
||||
"compiler": ["gcc"],
|
||||
"build_type": ["Release"],
|
||||
"arch": ["amd64"],
|
||||
"minimal": false,
|
||||
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-a6983f8"
|
||||
}
|
||||
],
|
||||
|
||||
"rhel": [
|
||||
{
|
||||
"compiler": ["gcc"],
|
||||
"build_type": ["Release"],
|
||||
"arch": ["amd64"],
|
||||
"minimal": false,
|
||||
"image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-a6983f8"
|
||||
"extra_cmake_args": "-Dvalidator_keys=ON",
|
||||
"package": {
|
||||
"type": "rpm",
|
||||
"image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-49cdc10"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
4
.github/workflows/build-nix-images.yml
vendored
4
.github/workflows/build-nix-images.yml
vendored
@@ -12,6 +12,7 @@ on:
|
||||
- "nix/**"
|
||||
- "!nix/docker/README.md"
|
||||
- "!nix/devshell.nix"
|
||||
- "!nix/check-tools/*.txt"
|
||||
- "bin/check-tools.sh"
|
||||
- "bin/default-loader-path.sh"
|
||||
- "bin/install-sanitizer-libs.sh"
|
||||
@@ -24,6 +25,7 @@ on:
|
||||
- "nix/**"
|
||||
- "!nix/docker/README.md"
|
||||
- "!nix/devshell.nix"
|
||||
- "!nix/check-tools/*.txt"
|
||||
- "bin/check-tools.sh"
|
||||
- "bin/default-loader-path.sh"
|
||||
- "bin/install-sanitizer-libs.sh"
|
||||
@@ -58,7 +60,7 @@ 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@65d5a0bd72be4ecea95cff0673a6e0672ab5243a
|
||||
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@034e87065fcd0100214cf0672923bd38d193cf78
|
||||
with:
|
||||
image_name: xrpld/nix-${{ matrix.distro.name }}
|
||||
dockerfile: nix/docker/Dockerfile
|
||||
|
||||
20
.github/workflows/build-packaging-images.yml
vendored
20
.github/workflows/build-packaging-images.yml
vendored
@@ -6,13 +6,13 @@ on:
|
||||
- develop
|
||||
paths:
|
||||
- ".github/workflows/build-packaging-images.yml"
|
||||
- "package/Dockerfile"
|
||||
- "package/install-packaging-tools.sh"
|
||||
- "bin/install-packaging-tools.sh"
|
||||
- "package/docker/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/build-packaging-images.yml"
|
||||
- "package/Dockerfile"
|
||||
- "package/install-packaging-tools.sh"
|
||||
- "bin/install-packaging-tools.sh"
|
||||
- "package/docker/**"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
@@ -33,15 +33,17 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Newest of each distro: these images only wrap pre-built binaries, so
|
||||
# they set no floor for consumers. build_pkg.py pins the RPM dist tag.
|
||||
distro:
|
||||
- name: debian
|
||||
base_image: debian:bookworm
|
||||
# AlmaLinux rather than UBI9, which does not ship rpm-sign.
|
||||
base_image: debian:trixie
|
||||
# AlmaLinux rather than UBI, which does not ship rpm-sign.
|
||||
- name: rhel
|
||||
base_image: almalinux:9
|
||||
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@65d5a0bd72be4ecea95cff0673a6e0672ab5243a
|
||||
base_image: almalinux:10
|
||||
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@034e87065fcd0100214cf0672923bd38d193cf78
|
||||
with:
|
||||
image_name: xrpld/packaging-${{ matrix.distro.name }}
|
||||
dockerfile: package/Dockerfile
|
||||
dockerfile: package/docker/Dockerfile
|
||||
base_image: ${{ matrix.distro.base_image }}
|
||||
push: ${{ github.event_name == 'push' }}
|
||||
|
||||
2
.github/workflows/build-pre-commit-image.yml
vendored
2
.github/workflows/build-pre-commit-image.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@65d5a0bd72be4ecea95cff0673a6e0672ab5243a
|
||||
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@034e87065fcd0100214cf0672923bd38d193cf78
|
||||
with:
|
||||
image_name: xrpld/pre-commit
|
||||
dockerfile: bin/pre-commit/Dockerfile
|
||||
|
||||
2
.github/workflows/cargo-audit.yml
vendored
2
.github/workflows/cargo-audit.yml
vendored
@@ -34,7 +34,7 @@ permissions:
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-a0074f8
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44
|
||||
permissions:
|
||||
contents: read
|
||||
# Needed to open an issue on scheduled failures.
|
||||
|
||||
2
.github/workflows/check-tools.yml
vendored
2
.github/workflows/check-tools.yml
vendored
@@ -79,7 +79,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
|
||||
uses: XRPLF/actions/prepare-runner@b3e255d74d785d053e4903da8ac90983cd7d9e82
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
|
||||
4
.github/workflows/pre-commit.yml
vendored
4
.github/workflows/pre-commit.yml
vendored
@@ -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@f1952595d212e86169935135efc66294b4574131
|
||||
uses: XRPLF/actions/.github/workflows/pre-commit.yml@279ec358f4a1be4088be3e024b07916fa97c75b6
|
||||
with:
|
||||
runs_on: ubuntu-latest
|
||||
container: '{ "image": "ghcr.io/xrplf/xrpld/pre-commit:sha-f56b79f" }'
|
||||
container: '{ "image": "ghcr.io/xrplf/xrpld/pre-commit:sha-473fe44" }'
|
||||
|
||||
4
.github/workflows/publish-docs.yml
vendored
4
.github/workflows/publish-docs.yml
vendored
@@ -41,13 +41,13 @@ env:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-a0074f8
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
|
||||
uses: XRPLF/actions/prepare-runner@b3e255d74d785d053e4903da8ac90983cd7d9e82
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
|
||||
uses: XRPLF/actions/prepare-runner@b3e255d74d785d053e4903da8ac90983cd7d9e82
|
||||
with:
|
||||
enable_ccache: ${{ inputs.ccache_enabled }}
|
||||
|
||||
|
||||
4
.github/workflows/reusable-clang-tidy.yml
vendored
4
.github/workflows/reusable-clang-tidy.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
needs: [determine-files]
|
||||
if: ${{ needs.determine-files.outputs.cpp_changed_files != '' || needs.determine-files.outputs.need_full_run == 'true' }}
|
||||
runs-on: ["self-hosted", "Linux", "X64", "heavy"]
|
||||
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-a0074f8"
|
||||
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-473fe44"
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
|
||||
uses: XRPLF/actions/prepare-runner@b3e255d74d785d053e4903da8ac90983cd7d9e82
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
|
||||
203
.github/workflows/reusable-package.yml
vendored
203
.github/workflows/reusable-package.yml
vendored
@@ -1,11 +1,14 @@
|
||||
# Build Linux packages from the pre-built xrpld and validator-keys artifacts:
|
||||
# Build, verify and publish Linux packages from the pre-built xrpld and
|
||||
# validator-keys artifacts, in three stages:
|
||||
#
|
||||
# - one job per distro, taken from "package_configs" in linux.json
|
||||
# - each job runs in that distro's container, which is what decides DEB or RPM
|
||||
# - with 'publish: true' a job also uploads what it built
|
||||
# (see package/publish_pkg.sh)
|
||||
# - 'package' builds and signs one format per config that carries a "package"
|
||||
# map in linux.json; that map names the container image and the format
|
||||
# - 'test-install' installs what was built on a range of distros and runs the
|
||||
# binaries there, so a package that cannot be installed never reaches Nexus
|
||||
# - 'publish' uploads with the image's publish_pkg.py, doing a --dry-run
|
||||
# unless 'publish: true'
|
||||
#
|
||||
# Only linux/amd64 is supported; the runner is hardcoded in the job below.
|
||||
# Only linux/amd64 is supported; the runner is hardcoded in the jobs below.
|
||||
name: Package
|
||||
|
||||
on:
|
||||
@@ -39,6 +42,7 @@ defaults:
|
||||
|
||||
env:
|
||||
BUILD_DIR: build
|
||||
PACKAGE_DIR: packages
|
||||
|
||||
jobs:
|
||||
generate-matrix:
|
||||
@@ -70,12 +74,17 @@ jobs:
|
||||
contents: read
|
||||
runs-on: ["self-hosted", "Linux", "X64", "heavy"]
|
||||
container: ${{ matrix.image }}
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@b3e255d74d785d053e4903da8ac90983cd7d9e82
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
- name: Download pre-built xrpld binary
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
@@ -97,33 +106,187 @@ jobs:
|
||||
|
||||
- name: Build package
|
||||
env:
|
||||
PACKAGE_TYPE: ${{ matrix.package_type }}
|
||||
PKG_RELEASE: ${{ steps.release_info.outputs.pkg_release }}
|
||||
PKG_CHANNEL: ${{ steps.release_info.outputs.channel }}
|
||||
run: ./package/build_pkg.sh
|
||||
CHANNEL: ${{ steps.release_info.outputs.channel }}
|
||||
run: |
|
||||
./package/build_pkg.py \
|
||||
--package-type "${PACKAGE_TYPE}" \
|
||||
--build-dir "${BUILD_DIR}" \
|
||||
--pkg-release "${PKG_RELEASE}" \
|
||||
--channel "${CHANNEL}"
|
||||
|
||||
# Before the upload, so the artifact and the published package are the
|
||||
# same bytes. DEBs are not signed, so the key is never set on that job.
|
||||
# Before the upload, so the artifact, the tested package and the published
|
||||
# package are the same bytes.
|
||||
- name: Sign RPM
|
||||
if: ${{ inputs.publish && matrix.distro == 'rhel' }}
|
||||
if: ${{ inputs.publish && matrix.package_type == 'rpm' }}
|
||||
env:
|
||||
PKG_SIGNING_KEY: ${{ secrets.signing_key }}
|
||||
run: ./package/sign_rpm.sh "${BUILD_DIR}"
|
||||
run: ./package/sign_rpm.py --package-dir "${BUILD_DIR}"
|
||||
|
||||
# Split from the debug symbols, which are an order of magnitude larger, so
|
||||
# that test-install downloads only what it installs.
|
||||
- name: Upload package artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ matrix.xrpld_artifact_name }}-pkg
|
||||
path: |
|
||||
${{ env.BUILD_DIR }}/debbuild/*.deb
|
||||
${{ env.BUILD_DIR }}/debbuild/*.ddeb
|
||||
${{ env.BUILD_DIR }}/rpmbuild/RPMS/**/*.rpm
|
||||
${{ env.BUILD_DIR }}/debbuild/xrpld_*.deb
|
||||
${{ env.BUILD_DIR }}/rpmbuild/RPMS/**/xrpld-[0-9]*.rpm
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload debug symbol artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ matrix.xrpld_artifact_name }}-pkg-debug
|
||||
path: |
|
||||
${{ env.BUILD_DIR }}/debbuild/xrpld-dbgsym_*.deb
|
||||
${{ env.BUILD_DIR }}/debbuild/xrpld-dbgsym_*.ddeb
|
||||
${{ env.BUILD_DIR }}/rpmbuild/RPMS/**/xrpld-debuginfo-*.rpm
|
||||
if-no-files-found: error
|
||||
|
||||
# Every distro family the packages target, oldest release first, so both ends
|
||||
# of the dependency range they declare are exercised.
|
||||
test-install:
|
||||
needs: [package]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- package_type: deb
|
||||
image: debian:11
|
||||
- package_type: deb
|
||||
image: debian:12
|
||||
- package_type: deb
|
||||
image: debian:13
|
||||
- package_type: deb
|
||||
image: ubuntu:20.04
|
||||
- package_type: deb
|
||||
image: ubuntu:22.04
|
||||
- package_type: deb
|
||||
image: ubuntu:24.04
|
||||
- package_type: deb
|
||||
image: ubuntu:26.04
|
||||
|
||||
- package_type: rpm
|
||||
image: almalinux:9
|
||||
- package_type: rpm
|
||||
image: almalinux:10
|
||||
- package_type: rpm
|
||||
image: rockylinux/rockylinux:9
|
||||
- package_type: rpm
|
||||
image: rockylinux/rockylinux:10
|
||||
- package_type: rpm
|
||||
image: registry.access.redhat.com/ubi9/ubi
|
||||
- package_type: rpm
|
||||
image: registry.access.redhat.com/ubi10/ubi
|
||||
name: "install ${{ matrix.package_type }} on ${{ matrix.image }}"
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
container: ${{ matrix.image }}
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
# Both formats land in one directory; the step below picks its own by
|
||||
# extension, so this stays independent of the artifact names.
|
||||
- name: Download package artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: "*-pkg"
|
||||
merge-multiple: true
|
||||
path: ${{ env.PACKAGE_DIR }}
|
||||
|
||||
- name: Find the package
|
||||
id: find
|
||||
env:
|
||||
PACKAGE_TYPE: ${{ matrix.package_type }}
|
||||
run: |
|
||||
package="$(find "${PACKAGE_DIR}" -type f -name "*.${PACKAGE_TYPE}" -print -quit)"
|
||||
test -n "${package}" || {
|
||||
echo "no .${PACKAGE_TYPE} found in ${PACKAGE_DIR}" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "package=${package}" >>"${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Install the DEB
|
||||
if: ${{ matrix.package_type == 'deb' }}
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
PACKAGE: ${{ steps.find.outputs.package }}
|
||||
run: |
|
||||
# Stock Debian and Ubuntu images carry no package lists, so apt has
|
||||
# nothing to resolve the systemd dependency from until it fetches them.
|
||||
apt-get update -qq
|
||||
apt-get install -y "./${PACKAGE}"
|
||||
|
||||
- name: Install the RPM
|
||||
if: ${{ matrix.package_type == 'rpm' }}
|
||||
env:
|
||||
PACKAGE: ${{ steps.find.outputs.package }}
|
||||
run: dnf install -y "./${PACKAGE}"
|
||||
|
||||
- name: Run xrpld
|
||||
run: xrpld --version
|
||||
|
||||
- name: Run validator-keys
|
||||
run: validator-keys --version
|
||||
|
||||
- name: Run rippled, the legacy compatibility symlink
|
||||
run: rippled --version
|
||||
|
||||
- name: Check the service account
|
||||
run: id xrpld
|
||||
|
||||
- name: Check the state directory
|
||||
run: test -d /var/lib/xrpld
|
||||
|
||||
- name: Check the log directory
|
||||
run: test -d /var/log/xrpld
|
||||
|
||||
publish:
|
||||
needs: [generate-matrix, package, test-install]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
|
||||
name: "publish ${{ matrix.xrpld_artifact_name }}"
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ["self-hosted", "Linux", "X64", "heavy"]
|
||||
container: ${{ matrix.image }}
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@b3e255d74d785d053e4903da8ac90983cd7d9e82
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
# Both artifacts, so the debug symbols are published alongside the package.
|
||||
- name: Download package artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: ${{ matrix.xrpld_artifact_name }}-pkg*
|
||||
merge-multiple: true
|
||||
path: ${{ env.PACKAGE_DIR }}
|
||||
|
||||
- name: Determine release info
|
||||
id: release_info
|
||||
uses: ./.github/actions/release-info
|
||||
|
||||
- name: Publish package
|
||||
if: ${{ inputs.publish }}
|
||||
env:
|
||||
CHANNEL: ${{ steps.release_info.outputs.channel }}
|
||||
DRY_RUN_OPTION: ${{ !inputs.publish && '--dry-run' || '' }}
|
||||
NEXUS_URL: ${{ inputs.nexus_url }}
|
||||
NEXUS_USERNAME: ${{ secrets.remote_username }}
|
||||
NEXUS_PASSWORD: ${{ secrets.remote_password }}
|
||||
run: ./package/publish_pkg.sh "${CHANNEL}" "${BUILD_DIR}"
|
||||
NEXUS_USERNAME: ${{ inputs.publish && secrets.remote_username || '' }}
|
||||
NEXUS_PASSWORD: ${{ inputs.publish && secrets.remote_password || '' }}
|
||||
run: |
|
||||
publish_pkg.py \
|
||||
--channel "${CHANNEL}" \
|
||||
--package-dir "${PACKAGE_DIR}" \
|
||||
--nexus-url "${NEXUS_URL}" \
|
||||
${DRY_RUN_OPTION}
|
||||
|
||||
6
.github/workflows/reusable-rust.yml
vendored
6
.github/workflows/reusable-rust.yml
vendored
@@ -27,7 +27,7 @@ permissions:
|
||||
jobs:
|
||||
clippy:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-a0074f8
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-a0074f8
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
|
||||
doc:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-a0074f8
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
7
.github/workflows/reusable-upload-recipe.yml
vendored
7
.github/workflows/reusable-upload-recipe.yml
vendored
@@ -40,7 +40,7 @@ defaults:
|
||||
jobs:
|
||||
upload:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-a0074f8
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44
|
||||
env:
|
||||
REMOTE_NAME: ${{ inputs.remote_name }}
|
||||
CONAN_LOGIN_USERNAME_XRPLF: ${{ secrets.remote_username }}
|
||||
@@ -49,6 +49,11 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@b3e255d74d785d053e4903da8ac90983cd7d9e82
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
- name: Determine release info
|
||||
id: release_info
|
||||
uses: ./.github/actions/release-info
|
||||
|
||||
2
.github/workflows/upload-conan-deps.yml
vendored
2
.github/workflows/upload-conan-deps.yml
vendored
@@ -68,7 +68,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
|
||||
uses: XRPLF/actions/prepare-runner@b3e255d74d785d053e4903da8ac90983cd7d9e82
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -72,11 +72,16 @@ DerivedData
|
||||
/.zed/
|
||||
|
||||
# AI tools.
|
||||
# Shared/committable AI agent config (AGENTS.md, CLAUDE.md, GEMINI.md, .claude/settings.json,
|
||||
# tool-specific rules files, etc.) should be checked in — see CONTRIBUTING.md. Only the
|
||||
# personal/local variants below are ignored.
|
||||
/.agent
|
||||
/.agents
|
||||
/.augment
|
||||
/.claude
|
||||
/CLAUDE.md
|
||||
/.claude/settings.local.json
|
||||
AGENTS.override.md
|
||||
CLAUDE.local.md
|
||||
GEMINI.local.md
|
||||
|
||||
# Python
|
||||
__pycache__
|
||||
|
||||
@@ -82,11 +82,27 @@ repos:
|
||||
- id: prettier
|
||||
args: [--end-of-line=auto]
|
||||
|
||||
# Scoped to package/: the rest of the repo's Python has pre-existing findings,
|
||||
# so widening these is its own change.
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: 7c55798a78262d14b2074abf623d8a992ebb70d4 # frozen: v0.16.2
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args: [--fix]
|
||||
files: ^package/.*\.py$
|
||||
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 4160603246a6b365d4a2af661c6d71b0a0f50478 # frozen: 26.5.1
|
||||
hooks:
|
||||
- id: black
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: 41e691678310dfd3833f7ab4e180ddb014310356 # frozen: v2.3.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
args: [--strict]
|
||||
files: ^package/.*\.py$
|
||||
|
||||
- repo: https://github.com/scop/pre-commit-shfmt
|
||||
rev: 05c1426671b9237fb5e1444dd63aa5731bec0dfb # frozen: v3.13.1-1
|
||||
hooks:
|
||||
|
||||
42
AGENTS.md
Normal file
42
AGENTS.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file provides guidance to AI coding agents (Claude Code, and other AGENTS.md-compatible tools) when working with code in this repository.
|
||||
|
||||
## Build
|
||||
|
||||
Required on Linux/macOS: use the Nix devshell, which sets up the compiler, Conan, ccache, and (optionally) Rust automatically.
|
||||
|
||||
```bash
|
||||
nix develop
|
||||
```
|
||||
|
||||
For alternate devshell variants (specific compiler, no-compiler, coverage), see [docs/build/nix.md](./docs/build/nix.md). For the manual build steps, CMake options, and protocol codegen commands, see [BUILD.md](./BUILD.md) (`## Steps`, `## Options`, `## Code generation`).
|
||||
|
||||
Rust crate tests (independent of the CMake build): `cargo test --manifest-path crates/Cargo.toml --workspace` (CI uses `cargo nextest`).
|
||||
|
||||
## Testing
|
||||
|
||||
Unit tests are a custom framework built into the `xrpld` binary itself (not Boost.Test/GTest/Catch); see [CONTRIBUTING.md](./CONTRIBUTING.md#unit-tests) for the basic invocation. Notes not covered there:
|
||||
|
||||
- A suite's `--unittest` name is built from the arguments to its `BEAST_DEFINE_TESTSUITE`/`BEAST_DEFINE_TESTSUITE_PRIO` macro (usually at the bottom of the test file), in reverse order and joined with `.`: `BEAST_DEFINE_TESTSUITE(Credentials, app, xrpl)` → `xrpl.app.Credentials`.
|
||||
- `--unittest-arg` does nothing — don't use it.
|
||||
- Tests that run offline in under a minute should be automatic `--unittest` suites; anything else is a manual/integration test.
|
||||
- New tests should be written using `gtest` under `src/tests/` unless that isn't possible, in which case fall back to the legacy Beast framework under `src/test/`. `tests/` (top-level) holds integration tests exercised against `libxrpl`/`xrpld`.
|
||||
|
||||
## Lint/Format
|
||||
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md#pre-commit-hooks) for `pre-commit` setup and [CONTRIBUTING.md](./CONTRIBUTING.md#clang-tidy) for `clang-tidy` (opt-in, needs local `clang-tidy` and generated headers).
|
||||
|
||||
## Code Style
|
||||
|
||||
New file placement and header levelization: see [CONTRIBUTING.md](./CONTRIBUTING.md#before-making-a-pull-request). Braces, whitespace, member order, and other conventions: see [docs/CodingStyle.md](./docs/CodingStyle.md). `XRPL_ASSERT`/`UNREACHABLE` contracts: see [CONTRIBUTING.md](./CONTRIBUTING.md#contracts-and-instrumentation). Commit messages: see [CONTRIBUTING.md](./CONTRIBUTING.md#good-commit-messages).
|
||||
|
||||
## Architecture
|
||||
|
||||
Paths below reflect the current layout; update this section if modularization moves a subsystem to a different directory.
|
||||
|
||||
- `include/xrpl/` + `src/libxrpl/` — the core protocol library: ledger, shamap, consensus, crypto, json, resource, nodestore, rdb, peerfinder, and `tx/` (transaction application: `Transactor.cpp`, `applySteps.cpp`, invariants, payment paths). `tx/transactors/` has one file per transaction type, grouped by subsystem: `escrow/`, `vault/`, `lending/`, `sponsor/`, `nft/`, `token/` (MPT), `payment_channel/`, `permissioned_domain/`, `dex/`, `oracle/`, `did/`, `credentials/`, `bridge/`, `check/`, `delegate/`, `account/`, `system/`. Any change to transaction-processing behavior must be gated behind an Amendment.
|
||||
- `src/xrpld/` — the server application built on top of `libxrpl`: `app`, `core`, `overlay` (P2P networking), `peerfinder`, `perflog`, `rpc`, `shamap`. `main` builds an `ApplicationImp` implementing `Application`; most components hold a reference to it (`app_`), giving broad cross-component access — expect to trace call chains through `Application&`.
|
||||
- `src/test/` — unit tests mirroring the subsystems above, plus `jtx/` (the transaction-building test DSL — e.g. `jtx/escrow.h`, `jtx/vault.h`, `jtx/sponsor.h`, `jtx/permissioned_dex.h`) and `unit_test/` (the custom test framework itself, derived from Beast).
|
||||
- `src/tests/` — unit tests for `libxrpl` written in `gtest`, gradually replacing the `src/test` equivalents.
|
||||
- `crates/` — a Rust workspace (only built with `-Dxrpld -Drust=ON`) bridged into C++ via `cxxbridge`/the `cxx` crate; currently just a `hello_world` interop scaffold. Requires the Rust toolchain pinned in `rust-toolchain.toml` (the Nix devshell provides it automatically).
|
||||
@@ -22,17 +22,47 @@ API version 2 is available in `xrpld` version 2.0.0 and later. See [API-VERSION-
|
||||
|
||||
This version is supported by all `xrpld` versions. For WebSocket and HTTP JSON-RPC requests, it is currently the default API version used when no `api_version` is specified.
|
||||
|
||||
## Unreleased
|
||||
## XRP Ledger server version 3.4.0
|
||||
|
||||
This section contains changes targeting a future version.
|
||||
Version 3.4.0 is not yet released. These changes are available in the 3.4.0 beta releases.
|
||||
|
||||
### Additions
|
||||
### Additions in 3.4.0
|
||||
|
||||
- `account_tx`: Added an optional `delegate` request object to filter delegated transactions. The object requires `delegate_filter`, which must be either `actor` for transactions owned by the requested account but signed by another account, or `authorizer` for transactions signed by the requested account on behalf of another account. The optional `counter_party` account narrows the results to a specific signer/delegate for `actor` or a specific owner/delegator for `authorizer`. Malformed `delegate`, `delegate_filter`, and `counter_party` values return standard invalid field errors, and invalid account IDs return `actMalformed`.
|
||||
When paginating delegate-filtered queries, a marker from a delegate-filtered query includes a `delegate` flag and is only valid for follow-up requests that also supply `delegate` (mixing marker conventions returns `invalidParams`). Because filtering is applied after the ledger scan, a page may contain fewer results than `limit` (possibly zero) while still returning a marker, so callers must continue until no marker is present.
|
||||
- `ledger`: `nftoken_id`, `nftoken_ids`, and `offer_id` are now included in transaction metadata when transactions are expanded (`expand`, or admin-only `full`), matching the `tx`, `account_tx`, and `subscribe` (`transactions` stream) responses. ([#5706](https://github.com/XRPLF/rippled/pull/5706))
|
||||
|
||||
### Bugfixes in 3.4.0
|
||||
|
||||
- `sign`, `sign_for`, `submit`: `signature_target` now returns `invalidParams` unless it names `CounterpartySignature` or `SponsorSignature`. It previously accepted any inner object field, such as `Book` or `NFToken`, and signed into it.
|
||||
- `sign`, `sign_for`, `submit`, `submit_multisigned`: With `fixCleanup3_4_0` enabled, a signature in `CounterpartySignature` or `SponsorSignature` covers a different prefix than the transaction's own signature, so a signature can no longer be moved from one of those roles into another. Clients that build these signatures themselves must use the new prefixes: `CPT` and `CPM` (single- and multi-signing) for `CounterpartySignature`, and `SPN` and `SPM` for `SponsorSignature`.
|
||||
- `get_aggregate_price`: Duplicate entries in the `oracles` request array are now ignored. [#6586](https://github.com/XRPLF/rippled/pull/6586)
|
||||
- `vault_info`: Errors now identify what the request got wrong instead of reporting every failure as the unregistered token `malformedRequest`, and the `error`, `error_code` and `error_message` fields now agree with each other. An invalid `vault_id` or `seq` returns `invalidParams`, an invalid `owner` returns `actMalformed`, and a request that mixes `vault_id` with `owner`/`seq` or supplies neither returns `invalidParams` with a message naming the accepted combinations. [#8015](https://github.com/XRPLF/rippled/pull/8015)
|
||||
- `vault_info`: A well-formed all-zero `vault_id` now returns `entryNotFound` instead of being rejected as malformed, and `entryNotFound` responses now include `error_code` and `error_message`. Clients that request `ripplerpc` 3.0 or above therefore receive HTTP 400 with that error rather than HTTP 200. [#8015](https://github.com/XRPLF/rippled/pull/8015)
|
||||
- `vault_info`: `vault_id` and `owner` must now be strings, matching how `ledger_entry` reads the same fields. An object or an array in either field previously produced an internal error, and a number was silently converted to its decimal text; `vault_id` now returns `invalidParams` and `owner` returns `actMalformed`. [#8015](https://github.com/XRPLF/rippled/pull/8015)
|
||||
- `gateway_balances`: The `account` and `ident` fields now return an `invalidParams` error if the value is not a string, instead of an `internal` error. [#7655](https://github.com/XRPLF/rippled/pull/7655)
|
||||
- `account_lines`: The `peer` field now returns an error if the value is not a string. [#7728](https://github.com/XRPLF/rippled/pull/7728)
|
||||
- `ledger`: `delivered_amount` is now included in the metadata of successful `AccountDelete` transactions when transactions are expanded (`expand`, or admin-only `full`). Previously it was only added for `Payment` and `CheckCash`, which made `ledger` inconsistent with `tx` and `account_tx`. [#5706](https://github.com/XRPLF/rippled/pull/5706)
|
||||
|
||||
## XRP Ledger server version 3.3.0
|
||||
|
||||
[Version 3.3.0](https://github.com/XRPLF/rippled/releases/tag/3.3.0) was released on Aug 6, 2026.
|
||||
|
||||
### Additions in 3.3.0
|
||||
|
||||
- `account_tx`: Added an optional `delegate` request object to filter delegated transactions. The object requires `delegate_filter`, which must be either `actor` for transactions owned by the requested account but signed by another account, or `authorizer` for transactions signed by the requested account on behalf of another account. The optional `counter_party` account narrows the results to a specific signer/delegate for `actor` or a specific owner/delegator for `authorizer`. Malformed `delegate`, `delegate_filter`, and `counter_party` values return standard invalid field errors, and invalid account IDs return `actMalformed`. When paginating delegate-filtered queries, a marker from a delegate-filtered query includes a `delegate` flag and is only valid for follow-up requests that also supply `delegate` (mixing marker conventions returns `invalidParams`). Because filtering is applied after the ledger scan, a page may contain fewer results than `limit` (possibly zero) while still returning a marker, so callers must continue until no marker is present. ([#6126](https://github.com/XRPLF/rippled/pull/6126))
|
||||
|
||||
## XRP Ledger server version 3.2.1
|
||||
|
||||
[Version 3.2.1](https://github.com/XRPLF/rippled/releases/tag/3.2.1) was released on Aug 1, 2026.
|
||||
|
||||
This release contains bug fixes only and no API changes.
|
||||
|
||||
## XRP Ledger server version 3.2.0
|
||||
|
||||
[Version 3.2.0](https://github.com/XRPLF/rippled/releases/tag/3.2.0) was released on Jun 16, 2026.
|
||||
|
||||
### Additions in 3.2.0
|
||||
|
||||
- `ledger_entry`, `account_objects`: The `Delegate` ledger entry now includes an optional `DestinationNode` field, which stores the index into the authorized account's owner directory. This field is present on entries created after bidirectional directory tracking was introduced and may appear in RPC responses for those entries. ([#6681](https://github.com/XRPLF/rippled/pull/6681))
|
||||
|
||||
- `server_definitions`: Added the following new sections to the response ([#6321](https://github.com/XRPLF/rippled/pull/6321)):
|
||||
- `TRANSACTION_FORMATS`: Describes the fields and their optionality for each transaction type, including common fields shared across all transactions.
|
||||
- `LEDGER_ENTRY_FORMATS`: Describes the fields and their optionality for each ledger entry type, including common fields shared across all ledger entries.
|
||||
@@ -40,9 +70,8 @@ This section contains changes targeting a future version.
|
||||
- `LEDGER_ENTRY_FLAGS`: Maps ledger entry type names to their flags and flag values.
|
||||
- `ACCOUNT_SET_FLAGS`: Maps AccountSet flag names (asf flags) to their numeric values.
|
||||
|
||||
### Bugfixes
|
||||
### Bugfixes in 3.2.0
|
||||
|
||||
- `get_aggregate_price`: Duplicate entries in the `oracles` request array are now ignored. [#6586](https://github.com/XRPLF/rippled/pull/6586)
|
||||
- Peer Crawler: The `port` field in `overlay.active[]` now consistently returns an integer instead of a string for outbound peers. [#6318](https://github.com/XRPLF/rippled/pull/6318)
|
||||
- `ping`: The `ip` field is no longer returned as an empty string for proxied connections without a forwarded-for header. It is now omitted, consistent with the behavior for identified connections. [#6730](https://github.com/XRPLF/rippled/pull/6730)
|
||||
- gRPC `GetLedgerDiff`: Fixed error message that incorrectly said "base ledger not validated" when the desired ledger was not validated. [#6730](https://github.com/XRPLF/rippled/pull/6730)
|
||||
@@ -54,11 +83,24 @@ This section contains changes targeting a future version.
|
||||
- `submit`: The `fail_hard` field now returns an error if the value is not a boolean. [#6529](https://github.com/XRPLF/rippled/pull/6529)
|
||||
- `subscribe`: The `taker` field in the `books` array now returns `actMalformed` instead of `badIssuer` if the value is not a valid account. [#6529](https://github.com/XRPLF/rippled/pull/6529)
|
||||
- Fixed a bug in `Forwarded` HTTP header parsing where the extracted IP address could be incorrect when no comma or semicolon delimiter follows the address. This could cause the server to misidentify a client's IP address when operating behind a reverse proxy. [#6529](https://github.com/XRPLF/rippled/pull/6529)
|
||||
- `vault_info`: Errors now identify what the request got wrong instead of reporting every failure as the unregistered token `malformedRequest`, and the `error`, `error_code` and `error_message` fields now agree with each other. An invalid `vault_id` or `seq` returns `invalidParams`, an invalid `owner` returns `actMalformed`, and a request that mixes `vault_id` with `owner`/`seq` or supplies neither returns `invalidParams` with a message naming the accepted combinations. [#8015](https://github.com/XRPLF/rippled/pull/8015)
|
||||
- `vault_info`: A well-formed all-zero `vault_id` now returns `entryNotFound` instead of being rejected as malformed, and `entryNotFound` responses now include `error_code` and `error_message`. Clients that request `ripplerpc` 3.0 or above therefore receive HTTP 400 with that error rather than HTTP 200. [#8015](https://github.com/XRPLF/rippled/pull/8015)
|
||||
- `vault_info`: `vault_id` and `owner` must now be strings, matching how `ledger_entry` reads the same fields. An object or an array in either field previously produced an internal error, and a number was silently converted to its decimal text; `vault_id` now returns `invalidParams` and `owner` returns `actMalformed`. [#8015](https://github.com/XRPLF/rippled/pull/8015)
|
||||
- `gateway_balances`: The `account` and `ident` fields now return an `invalidParams` error if the value is not a string, instead of an `internal` error. [#7655](https://github.com/XRPLF/rippled/pull/7655)
|
||||
- `account_lines`: The `peer` field now returns an error if the value is not a string. [#7728](https://github.com/XRPLF/rippled/pull/7728)
|
||||
|
||||
## XRP Ledger server version 3.1.3
|
||||
|
||||
[Version 3.1.3](https://github.com/XRPLF/rippled/releases/tag/3.1.3) was released on May 8, 2026.
|
||||
|
||||
This release contains bug fixes only and no API changes.
|
||||
|
||||
## XRP Ledger server version 3.1.2
|
||||
|
||||
[Version 3.1.2](https://github.com/XRPLF/rippled/releases/tag/3.1.2) was released on Mar 12, 2026.
|
||||
|
||||
This release contains bug fixes only and no API changes.
|
||||
|
||||
## XRP Ledger server version 3.1.1
|
||||
|
||||
[Version 3.1.1](https://github.com/XRPLF/rippled/releases/tag/3.1.1) was released on Feb 23, 2026.
|
||||
|
||||
This release contains bug fixes only and no API changes.
|
||||
|
||||
## XRP Ledger server version 3.1.0
|
||||
|
||||
|
||||
@@ -59,6 +59,17 @@ to an existing XLS. Neither change will be released (in an amendment's
|
||||
case, marked as `Supported::yes`) until the corresponding XLS's status
|
||||
is `Final`.
|
||||
|
||||
## AI coding agents
|
||||
|
||||
[`AGENTS.md`](./AGENTS.md) (and its `CLAUDE.md` symlink, for Claude Code) holds shared, checked-in guidance for AI coding agents working in this repository — build/test/lint commands and architecture notes. Additional `AGENTS.md` files may exist in subdirectories to give agents context specific to that part of the codebase; whenever you add one, also add a `CLAUDE.md` symlink pointing to it (`ln -s AGENTS.md CLAUDE.md`) so Claude Code picks it up too.
|
||||
|
||||
If you want to give an agent personal instructions that shouldn't be shared with other contributors (e.g. your own workflow preferences), those are gitignored, not checked in:
|
||||
|
||||
- `CLAUDE.local.md` — read by Claude Code alongside `CLAUDE.md`.
|
||||
- `AGENTS.override.md` — read by AGENTS.md-compatible tools that support a personal override file layered on top of `AGENTS.md`.
|
||||
|
||||
Likewise, `.claude/settings.local.json` is for personal, untracked Claude Code settings, while `.claude/settings.json` is shared.
|
||||
|
||||
## Before making a pull request
|
||||
|
||||
(Or marking a draft pull request as ready.)
|
||||
@@ -82,7 +93,7 @@ If you create new source files, they must be organized as follows:
|
||||
under `include/xrpl`, and source (`.cpp`) files must go under
|
||||
`src/libxrpl`.
|
||||
- All other non-test files must go under `src/xrpld`.
|
||||
- All test source files must go under `src/test`.
|
||||
- New test source files should use `gtest` and go under `src/tests`, unless that isn't possible, in which case they should use our legacy test framework and go under `src/test`.
|
||||
- All benchmark source files must go under `src/benchmarks`.
|
||||
|
||||
The source must be formatted according to the style guide below. The easiest
|
||||
|
||||
@@ -25,34 +25,38 @@ esac
|
||||
# Packaging runs in a vanilla distro image, so the tooling comes from the distro's
|
||||
# archive rather than from nixpkgs:
|
||||
#
|
||||
# - debhelper and dpkg-dev build the DEB
|
||||
# - debhelper and dpkg-dev build the DEB, and lintian checks it
|
||||
# - binutils gives debian/rules the readelf its glibc-floor check runs; it
|
||||
# already arrives via dpkg-dev, but that tool is called directly
|
||||
# - rpm-build builds the RPM, with systemd-rpm-macros and redhat-rpm-config
|
||||
# supplying the systemd and find-debuginfo macros the spec uses
|
||||
# - rpm-sign signs the built RPM
|
||||
# - git gives build_pkg.sh a real history to read SOURCE_DATE_EPOCH from;
|
||||
# without one the timestamp falls back to the wall clock
|
||||
# - curl uploads the finished packages in publish_pkg.sh
|
||||
# - ca-certificates lets curl and git verify TLS
|
||||
# - rpm-sign and gnupg2 sign the built RPM
|
||||
# - python3 runs the packaging scripts
|
||||
# - git gives build_pkg.py the commit timestamp it stamps files with
|
||||
# - ca-certificates lets git and the packaging scripts verify TLS
|
||||
function install() {
|
||||
case "${ID}" in
|
||||
debian | ubuntu)
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends \
|
||||
binutils \
|
||||
ca-certificates \
|
||||
curl \
|
||||
debhelper \
|
||||
debhelper-compat \
|
||||
dpkg-dev \
|
||||
git
|
||||
git \
|
||||
lintian \
|
||||
python3
|
||||
;;
|
||||
|
||||
rhel | centos | rocky | almalinux)
|
||||
dnf install -y --setopt=install_weak_deps=False \
|
||||
curl-minimal \
|
||||
git \
|
||||
gnupg2 \
|
||||
python3 \
|
||||
redhat-rpm-config \
|
||||
rpm-build \
|
||||
rpm-sign \
|
||||
redhat-rpm-config \
|
||||
systemd-rpm-macros
|
||||
;;
|
||||
esac
|
||||
@@ -1,7 +1,7 @@
|
||||
#[===================================================================[
|
||||
Linux packaging support: 'package' target.
|
||||
|
||||
The packaging script (package/build_pkg.sh) installs to FHS-standard
|
||||
The packaging script (package/build_pkg.py) installs to FHS-standard
|
||||
paths (/usr/bin, /etc/xrpld, etc.) regardless of CMAKE_INSTALL_PREFIX,
|
||||
so no prefix guard is needed here.
|
||||
#]===================================================================]
|
||||
@@ -38,19 +38,20 @@ if(NOT TARGET validator-keys)
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(package_env
|
||||
SRC_DIR=${CMAKE_SOURCE_DIR}
|
||||
BUILD_DIR=${CMAKE_BINARY_DIR}
|
||||
PKG_RELEASE=${pkg_release}
|
||||
)
|
||||
if(DPKG_BUILDPACKAGE_EXECUTABLE)
|
||||
set(pkg_type deb)
|
||||
else()
|
||||
set(pkg_type rpm)
|
||||
endif()
|
||||
|
||||
add_custom_target(
|
||||
package
|
||||
COMMAND
|
||||
${CMAKE_COMMAND} -E env ${package_env}
|
||||
${CMAKE_SOURCE_DIR}/package/build_pkg.sh
|
||||
${CMAKE_SOURCE_DIR}/package/build_pkg.py --package-type=${pkg_type}
|
||||
--build-dir=${CMAKE_BINARY_DIR} --pkg-release=${pkg_release}
|
||||
--channel=UNRELEASED
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
DEPENDS xrpld validator-keys
|
||||
COMMENT "Building Linux package (deb/rpm inferred from host tooling)"
|
||||
COMMENT "Building Linux ${pkg_type} package"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
@@ -13,9 +13,9 @@ To build from source instead, see [BUILD.md](../BUILD.md).
|
||||
|
||||
Packages are published to four channels:
|
||||
|
||||
- `stable` - the latest production release
|
||||
- `unstable` - release candidates
|
||||
- `experimental` - beta builds
|
||||
- `stable` - production releases
|
||||
- `rc` - release candidates
|
||||
- `beta` - beta builds
|
||||
- `develop` - every push to the [`develop` branch](https://github.com/XRPLF/rippled/tree/develop)
|
||||
|
||||
See [Publishing packages](../package/README.md#publishing-packages) for how channels are produced.
|
||||
|
||||
@@ -103,7 +103,7 @@ namespace boost {
|
||||
template <>
|
||||
struct hash<::beast::ip::Address>
|
||||
{
|
||||
explicit hash() = default;
|
||||
hash() = default;
|
||||
|
||||
std::size_t
|
||||
operator()(::beast::ip::Address const& addr) const
|
||||
|
||||
@@ -34,7 +34,10 @@ enum class HashRouterFlags : std::uint16_t {
|
||||
PRIVATE4 = 0x0800,
|
||||
// Used in EscrowFinish.cpp
|
||||
PRIVATE5 = 0x1000,
|
||||
PRIVATE6 = 0x2000
|
||||
PRIVATE6 = 0x2000,
|
||||
// Used in apply.cpp
|
||||
PRIVATE7 = 0x4000,
|
||||
PRIVATE8 = 0x8000
|
||||
};
|
||||
|
||||
constexpr HashRouterFlags
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -353,14 +352,14 @@ pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey);
|
||||
*
|
||||
* 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.
|
||||
* SField::kSmdPseudoAccount 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 true if and only if sleAcct is a pseudo-account of any kind
|
||||
* (i.e. carries at least one field flagged with SField::kSmdPseudoAccount).
|
||||
*
|
||||
* Returns false if sleAcct is:
|
||||
* - NOT a pseudo-account OR
|
||||
@@ -368,18 +367,15 @@ getPseudoAccountFields();
|
||||
* - null pointer
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isPseudoAccount(SLE::const_pointer sleAcct, std::set<SField const*> const& pseudoFieldFilter = {});
|
||||
isPseudoAccount(SLE::const_pointer sleAcct);
|
||||
|
||||
/**
|
||||
* Convenience overload that reads the account from the view.
|
||||
*/
|
||||
[[nodiscard]] inline bool
|
||||
isPseudoAccount(
|
||||
ReadView const& view,
|
||||
AccountID const& accountId,
|
||||
std::set<SField const*> const& pseudoFieldFilter = {})
|
||||
isPseudoAccount(ReadView const& view, AccountID const& accountId)
|
||||
{
|
||||
return isPseudoAccount(view.read(keylet::account(accountId)), pseudoFieldFilter);
|
||||
return isPseudoAccount(view.read(keylet::account(accountId)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
#include <xrpl/protocol/STVector256.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
@@ -34,32 +33,6 @@ checkExpired(SLE const& sleCredential, NetClock::time_point const& closed);
|
||||
[[nodiscard]] TER
|
||||
deleteSLE(ApplyView& view, SLE::ref sleCredential, beast::Journal j);
|
||||
|
||||
/**
|
||||
* @brief Remove credentials pinned to a pseudo-account's owner directory.
|
||||
*
|
||||
* Cleans up credentials that were linked to a pseudo-account (Vault, LoanBroker,
|
||||
* AMM), which such an account can neither accept nor delete. Only credentials
|
||||
* are removed; every other object is left in place. The walk visits at most
|
||||
* @p maxNodesToDelete directory entries and charges the ones it leaves alone
|
||||
* against that budget too, so a directory holding other objects yields fewer
|
||||
* than @p maxNodesToDelete deletions. On reaching the bound the result is
|
||||
* `tecINCOMPLETE` and the caller must propagate it so a later transaction
|
||||
* resumes.
|
||||
*
|
||||
* @param view Mutable ledger view.
|
||||
* @param pseudoAcct The pseudo-account whose directory is cleaned.
|
||||
* @param maxNodesToDelete Upper bound on directory entries processed in one call.
|
||||
* @param j Journal for diagnostics.
|
||||
* @return tesSUCCESS once no credentials remain, tecINCOMPLETE if the bound was
|
||||
* reached, or a deletion error.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
deletePseudoAccountCredentials(
|
||||
ApplyView& view,
|
||||
AccountID const& pseudoAcct,
|
||||
std::uint16_t maxNodesToDelete,
|
||||
beast::Journal j);
|
||||
|
||||
// Amendment and parameters checks for sfCredentialIDs field
|
||||
NotTEC
|
||||
checkFields(STTx const& tx, Rules const& rules, beast::Journal j);
|
||||
|
||||
@@ -324,6 +324,12 @@ computeFullPaymentInterest(
|
||||
std::uint32_t startDate,
|
||||
TenthBips32 closeInterestRate);
|
||||
|
||||
// Returns true if the loan's next payment is late per protocol rules. The
|
||||
// boundary is amendment-gated: with fixCleanup3_4_0 the due date must be
|
||||
// strictly in the past, otherwise the exact due-date instant counts as late.
|
||||
[[nodiscard]] bool
|
||||
isPaymentLate(ReadView const& view, SLE::const_ref loanSle);
|
||||
|
||||
// Deltas applied to Vault.AssetsTotal and LoanBroker.DebtTotal at a single
|
||||
// accounting touch point (origination, payment, impair/unimpair/default).
|
||||
struct AccountingDeltas
|
||||
|
||||
@@ -239,8 +239,13 @@ canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, Acc
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Any transactors that call addEmptyHolding() in doApply must call
|
||||
* canAddHolding() in preflight with the same View and Asset
|
||||
* XRP and the issuer itself are always tesSUCCESS. Otherwise, after
|
||||
* fixCleanup3_4_0, an existing trust line returns tecDUPLICATE without
|
||||
* consulting issuer freeze or DefaultRipple; both still apply on the create
|
||||
* path (DefaultRipple off is terNO_RIPPLE). canAddHolding() ignores existing
|
||||
* holdings, so transactors that may create a holding in doApply should gate
|
||||
* their preclaim call on it: after the amendment only when no holding
|
||||
* exists, before it always.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
addEmptyHolding(
|
||||
|
||||
@@ -38,6 +38,12 @@ enum class FreezeHandling { IgnoreFreeze, ZeroIfFrozen };
|
||||
*/
|
||||
enum class AuthHandling { IgnoreAuth, ZeroIfUnauthorized };
|
||||
|
||||
/**
|
||||
* Controls whether the recipient owner-reserve check is enforced when
|
||||
* auto-creating a trustline or MPToken during AMMWithdraw or AMMClawback.
|
||||
*/
|
||||
enum class ReserveHandling : bool { EnforceReserve, IgnoreReserve };
|
||||
|
||||
/**
|
||||
* Controls whether to include the account's full spendable balance
|
||||
*/
|
||||
@@ -294,6 +300,14 @@ accountFunds(
|
||||
AuthHandling authHandling,
|
||||
beast::Journal j);
|
||||
|
||||
/**
|
||||
* Returns the transfer fee as Rate based on the type of token
|
||||
* @param view The ledger view
|
||||
* @param asset The asset being transferred
|
||||
*/
|
||||
[[nodiscard]] Rate
|
||||
transferRate(ReadView const& view, Asset const& asset);
|
||||
|
||||
/**
|
||||
* Returns the transfer fee as Rate based on the type of token
|
||||
* @param view The ledger view
|
||||
@@ -311,6 +325,12 @@ transferRate(ReadView const& view, STAmount const& amount);
|
||||
[[nodiscard]] TER
|
||||
canAddHolding(ReadView const& view, Asset const& asset);
|
||||
|
||||
/**
|
||||
* True if the account already holds this asset (or is the issuer / XRP).
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
holdingExists(ReadView const& view, AccountID const& account, Asset const& asset);
|
||||
|
||||
[[nodiscard]] TER
|
||||
addEmptyHolding(
|
||||
ApplyViewContext ctx,
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -44,6 +45,32 @@ assetsToSharesDeposit(SLE::const_ref vault, SLE::const_ref issuance, STAmount co
|
||||
[[nodiscard]] std::optional<STAmount>
|
||||
sharesToAssetsDeposit(SLE::const_ref vault, SLE::const_ref issuance, STAmount const& shares);
|
||||
|
||||
/**
|
||||
* Adjusts a requested asset change (`delta`) to match the decimal scale of the
|
||||
* updated total vault assets. This ensures `sfAssetsTotal`, `sfAssetsAvailable`,
|
||||
* and the actual asset transfer change by the exact same representable amount.
|
||||
*
|
||||
* Rounding strategy:
|
||||
* - Debits (withdrawals): Rounds down `|delta|` on the new scale to prevent
|
||||
* paying out more than requested.
|
||||
* - Credits (deposits): Floors the resulting total asset balance and returns the
|
||||
* difference from the current total. This prevents crediting the vault with
|
||||
* more assets than the user deposited.
|
||||
*
|
||||
* Key rules:
|
||||
* - The returned magnitude never exceeds `|delta|`.
|
||||
* - Returns `tecPRECISION_LOSS` if the change is smaller than 1 ULP of the target scale
|
||||
* (prevents share operations when totals cannot change).
|
||||
* - For integer assets (XRP, MPT), rounding is a no-op.
|
||||
*
|
||||
* @param vault The vault ledger entry.
|
||||
* @param delta The requested signed change to sfAssetsTotal.
|
||||
* @return The rounded, positive magnitude, or `tecPRECISION_LOSS` if the
|
||||
* change is below representable precision.
|
||||
*/
|
||||
[[nodiscard]] std::expected<STAmount, TER>
|
||||
clampToAssetsTotalScale(SLE::const_ref vault, STAmount const& delta);
|
||||
|
||||
/**
|
||||
* Controls whether to truncate shares instead of rounding.
|
||||
*/
|
||||
@@ -59,33 +86,30 @@ enum class TruncateShares : bool { No = false, Yes = true };
|
||||
enum class WaiveUnrealizedLoss : bool { No = false, Yes = true };
|
||||
|
||||
/**
|
||||
* Returns the effective total of assets backing outstanding shares for the
|
||||
* purposes of a withdrawal, i.e. sfAssetsTotal, discounted by sfLossUnrealized
|
||||
* unless waived. This is the numerator used by both withdraw conversion
|
||||
* helpers (assetsToSharesWithdraw and sharesToAssetsWithdraw) to compute the
|
||||
* share/asset exchange rate.
|
||||
* Returns the assets backing outstanding shares for a withdrawal:
|
||||
* sfAssetsTotal minus sfLossUnrealized, or sfAssetsTotal alone when the
|
||||
* unrealized loss is waived. Used by assetsToSharesWithdraw and
|
||||
* sharesToAssetsWithdraw as the numerator of the share/asset exchange rate.
|
||||
*
|
||||
* @param vault The vault SLE.
|
||||
* @param waive Whether to waive (i.e. not subtract) the vault's unrealized
|
||||
* loss.
|
||||
* @param waive Whether to skip subtracting the unrealized loss.
|
||||
*/
|
||||
[[nodiscard]] Number
|
||||
assetsTotalForWithdrawal(SLE::const_ref vault, WaiveUnrealizedLoss waive);
|
||||
|
||||
/**
|
||||
* Returns whether debiting `amount` from `total` — the current value of a
|
||||
* vault's sfAssetsTotal or sfAssetsAvailable field — would canonicalize back
|
||||
* to the exact same STAmount value it started at. This happens when a
|
||||
* genuinely non-zero debit is dust relative to a `total` large enough to
|
||||
* exceed STAmount's significant-digit precision: the shares still move, but
|
||||
* the stored total doesn't change, which otherwise trips the ValidVault
|
||||
* invariant after the fact instead of failing cleanly upfront.
|
||||
* Returns true if debiting `amount` from `total` (the current value of a
|
||||
* vault's sfAssetsTotal or sfAssetsAvailable) would canonicalize to the
|
||||
* same STAmount value. This happens when `amount` is non-zero but too small
|
||||
* to change the stored total at STAmount's precision. Shares would still
|
||||
* move, so the ValidVault invariant would fail after apply; callers use
|
||||
* this to reject the transaction upfront instead.
|
||||
*
|
||||
* @param asset The vault's underlying asset, used to canonicalize both sides
|
||||
* the same way the ledger will when the field is stored.
|
||||
* @param asset The vault's underlying asset, used to canonicalize both
|
||||
* sides the same way the ledger will when the field is stored.
|
||||
* @param total The field's current value.
|
||||
* @param amount The amount to debit. A value of zero always returns false;
|
||||
* that case is rejected separately and unconditionally.
|
||||
* @param amount The amount to debit. Zero always returns false; that case
|
||||
* is rejected separately.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
debitIsNonZeroDust(Asset const& asset, Number const& total, Number const& amount);
|
||||
|
||||
@@ -133,7 +133,7 @@ private:
|
||||
using id_hash_type = boost::base_from_member<std::hash<xrpl::MPTID>, 0>;
|
||||
|
||||
public:
|
||||
explicit hash() = default;
|
||||
hash() = default;
|
||||
|
||||
using value_type = std::size_t;
|
||||
using argument_type = xrpl::MPTIssue;
|
||||
@@ -160,7 +160,7 @@ private:
|
||||
mptissue_hasher mMptissueHasher_;
|
||||
|
||||
public:
|
||||
explicit hash() = default;
|
||||
hash() = default;
|
||||
|
||||
value_type
|
||||
operator()(argument_type const& asset) const
|
||||
@@ -227,7 +227,7 @@ struct hash<xrpl::Issue> : std::hash<xrpl::Issue>
|
||||
template <>
|
||||
struct hash<xrpl::MPTIssue> : std::hash<xrpl::MPTIssue>
|
||||
{
|
||||
explicit hash() = default;
|
||||
hash() = default;
|
||||
|
||||
using Base = std::hash<xrpl::MPTIssue>;
|
||||
};
|
||||
@@ -235,7 +235,7 @@ struct hash<xrpl::MPTIssue> : std::hash<xrpl::MPTIssue>
|
||||
template <>
|
||||
struct hash<xrpl::Asset> : std::hash<xrpl::Asset>
|
||||
{
|
||||
explicit hash() = default;
|
||||
hash() = default;
|
||||
|
||||
using Base = std::hash<xrpl::Asset>;
|
||||
};
|
||||
|
||||
@@ -92,6 +92,26 @@ enum class HashPrefix : std::uint32_t {
|
||||
* Batch
|
||||
*/
|
||||
Batch = detail::makeHashPrefix('B', 'C', 'H'),
|
||||
|
||||
/**
|
||||
* inner transaction to sign as the counterparty
|
||||
*/
|
||||
CounterpartyTxSign = detail::makeHashPrefix('C', 'P', 'T'),
|
||||
|
||||
/**
|
||||
* inner transaction to multi-sign as the counterparty
|
||||
*/
|
||||
CounterpartyTxMultiSign = detail::makeHashPrefix('C', 'P', 'M'),
|
||||
|
||||
/**
|
||||
* inner transaction to sign as the sponsor
|
||||
*/
|
||||
SponsorTxSign = detail::makeHashPrefix('S', 'P', 'N'),
|
||||
|
||||
/**
|
||||
* inner transaction to multi-sign as the sponsor
|
||||
*/
|
||||
SponsorTxMultiSign = detail::makeHashPrefix('S', 'P', 'M'),
|
||||
};
|
||||
|
||||
template <class Hasher>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <ostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
@@ -174,4 +175,17 @@ mulRatio(MPTAmount const& amt, std::uint32_t num, std::uint32_t den, bool roundU
|
||||
return MPTAmount(r.convert_to<MPTAmount::value_type>());
|
||||
}
|
||||
|
||||
inline std::optional<MPTAmount>
|
||||
tryMulRatio(MPTAmount const& amt, std::uint32_t num, std::uint32_t den, bool roundUp)
|
||||
{
|
||||
try
|
||||
{
|
||||
return mulRatio(amt, num, den, roundUp);
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -151,7 +151,7 @@ namespace std {
|
||||
template <>
|
||||
struct hash<xrpl::MPTID> : xrpl::MPTID::hasher
|
||||
{
|
||||
explicit hash() = default;
|
||||
hash() = default;
|
||||
};
|
||||
|
||||
} // namespace std
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/json/json_forwards.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TxMeta.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace xrpl::rpc {
|
||||
|
||||
/**
|
||||
* Adds common synthetic fields to transaction-related JSON responses
|
||||
*/
|
||||
/** @{ */
|
||||
void
|
||||
insertNFTSyntheticInJson(json::Value&, std::shared_ptr<STTx const> const&, TxMeta const&);
|
||||
/** @} */
|
||||
|
||||
} // namespace xrpl::rpc
|
||||
@@ -348,13 +348,24 @@ enum class VaultPhase : std::uint8_t {
|
||||
Redemption,
|
||||
};
|
||||
|
||||
/**
|
||||
* Minimum gap between a closed-ended loan's final scheduled payment and the
|
||||
* vault's RedemptionDate. LoanSet rejects a schedule whose final payment is
|
||||
* fewer than this many seconds before RedemptionDate.
|
||||
*/
|
||||
constexpr std::uint32_t kLoanRedemptionBuffer = std::chrono::seconds{60}.count();
|
||||
|
||||
/**
|
||||
* Bounds on the length of a closed-ended vault's Investment phase
|
||||
* (RedemptionDate - SubscriptionDate). At vault creation the gap must satisfy
|
||||
* kMinInvestmentPeriod <= gap < kMaxInvestmentPeriod.
|
||||
*
|
||||
* 180s is enough to originate a loan that uses the minimum payment interval
|
||||
* and kLoanRedemptionBuffer after StartDate, which is strictly after
|
||||
* SubscriptionDate. The interval and buffer need not be equal; only their
|
||||
* sum plus one second must fit in this floor.
|
||||
*/
|
||||
constexpr std::uint32_t kMinInvestmentPeriod =
|
||||
std::chrono::seconds{std::chrono::minutes{1}}.count();
|
||||
constexpr std::uint32_t kMinInvestmentPeriod = std::chrono::seconds{180}.count();
|
||||
// This is 946708560 seconds which 30 x 365.2425 days (the average length of a Gregorian year).
|
||||
constexpr std::uint32_t kMaxInvestmentPeriod = std::chrono::seconds{std::chrono::years{30}}.count();
|
||||
|
||||
@@ -396,16 +407,6 @@ using TxID = uint256;
|
||||
*/
|
||||
constexpr std::uint16_t kMaxDeletableAmmTrustLines = 512;
|
||||
|
||||
/**
|
||||
* The maximum number of owner-directory entries to walk when clearing
|
||||
* credentials pinned to a pseudo-account, in a single transaction.
|
||||
*
|
||||
* The walk stops after this many entries whether or not each one turns out to
|
||||
* be a credential, so a directory that also holds other objects yields fewer
|
||||
* deletions per transaction.
|
||||
*/
|
||||
constexpr std::uint16_t kMaxDeletablePseudoAccountCredentials = 512;
|
||||
|
||||
/**
|
||||
* The maximum length of a URI inside an Oracle
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -13,6 +14,7 @@
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
|
||||
#include <boost/container/flat_set.hpp>
|
||||
@@ -105,14 +107,36 @@ public:
|
||||
[[nodiscard]] json::Value
|
||||
getJson(JsonOptions options, bool binary) const;
|
||||
|
||||
/**
|
||||
* Sign the transaction as its account.
|
||||
*
|
||||
* @param publicKey The public key for signing.
|
||||
* @param secretKey The secret key for signing.
|
||||
*/
|
||||
void
|
||||
sign(PublicKey const& publicKey, SecretKey const& secretKey);
|
||||
|
||||
/**
|
||||
* Sign the transaction in one of its signature fields.
|
||||
*
|
||||
* The signature is bound to the role that made it, so it cannot be moved
|
||||
* into another role.
|
||||
*
|
||||
* @param publicKey The public key for signing.
|
||||
* @param secretKey The secret key for signing.
|
||||
* @param role The role signing the transaction.
|
||||
* @param rules The current ledger rules.
|
||||
*/
|
||||
void
|
||||
sign(
|
||||
PublicKey const& publicKey,
|
||||
SecretKey const& secretKey,
|
||||
std::optional<std::reference_wrapper<SField const>> signatureTarget = {});
|
||||
SignatureRole role,
|
||||
Rules const& rules);
|
||||
|
||||
/**
|
||||
* Check the signature.
|
||||
*
|
||||
* @param rules The current ledger rules.
|
||||
* @return `true` if valid signature. If invalid, the error message string.
|
||||
*/
|
||||
@@ -120,7 +144,7 @@ public:
|
||||
checkSign(Rules const& rules) const;
|
||||
|
||||
[[nodiscard]] std::expected<void, std::string>
|
||||
checkBatchSign(Rules const& rules) const;
|
||||
checkBatchSign() const;
|
||||
|
||||
// SQL Functions with metadata.
|
||||
static std::string const&
|
||||
@@ -162,28 +186,28 @@ public:
|
||||
private:
|
||||
/**
|
||||
* Check the signature.
|
||||
*
|
||||
* @param rules The current ledger rules.
|
||||
* @param sigObject Reference to object that contains the signature fields.
|
||||
* Will be *this more often than not.
|
||||
* @param role The role that made the signature in sigObject. Determines
|
||||
* the signing prefix, which binds the signature to that role.
|
||||
* @return `true` if valid signature. If invalid, the error message string.
|
||||
*/
|
||||
[[nodiscard]] std::expected<void, std::string>
|
||||
checkSign(Rules const& rules, STObject const& sigObject) const;
|
||||
checkSign(Rules const& rules, STObject const& sigObject, SignatureRole role) const;
|
||||
|
||||
[[nodiscard]] std::expected<void, std::string>
|
||||
checkSingleSign(STObject const& sigObject) const;
|
||||
checkSingleSign(STObject const& sigObject, HashPrefix prefix) const;
|
||||
|
||||
[[nodiscard]] std::expected<void, std::string>
|
||||
checkMultiSign(Rules const& rules, STObject const& sigObject) const;
|
||||
checkMultiSign(STObject const& sigObject, HashPrefix prefix) const;
|
||||
|
||||
[[nodiscard]] std::expected<void, std::string>
|
||||
checkBatchSingleSign(STObject const& batchSigner, std::vector<uint256> const& txIds) const;
|
||||
|
||||
[[nodiscard]] std::expected<void, std::string>
|
||||
checkBatchMultiSign(
|
||||
STObject const& batchSigner,
|
||||
Rules const& rules,
|
||||
std::vector<uint256> const& txIds) const;
|
||||
checkBatchMultiSign(STObject const& batchSigner, std::vector<uint256> const& txIds) const;
|
||||
|
||||
void
|
||||
buildBatchTxns();
|
||||
|
||||
@@ -4,13 +4,65 @@
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* The signature slots on a transaction.
|
||||
*
|
||||
* Each role signs different bytes, so a signature cannot be moved from the
|
||||
* role that made it into another role. See signingPrefix.
|
||||
*/
|
||||
enum class SignatureRole {
|
||||
/**
|
||||
* The transaction's own signature, in sfTxnSignature or sfSigners.
|
||||
*/
|
||||
Transaction,
|
||||
/**
|
||||
* The counterparty's signature, in sfCounterpartySignature.
|
||||
*/
|
||||
Counterparty,
|
||||
/**
|
||||
* The sponsor's signature, in sfSponsorSignature.
|
||||
*/
|
||||
Sponsor
|
||||
};
|
||||
|
||||
/**
|
||||
* The field that holds this role's signature.
|
||||
*
|
||||
* @return The signature field, or nullptr for SignatureRole::Transaction,
|
||||
* whose signature lives at the top level of the transaction.
|
||||
*/
|
||||
[[nodiscard]] SField const*
|
||||
signatureField(SignatureRole role);
|
||||
|
||||
/**
|
||||
* The role that signs into the given field.
|
||||
*
|
||||
* @return The role, or an unseated optional if the field does not hold a
|
||||
* transaction signature.
|
||||
*/
|
||||
[[nodiscard]] std::optional<SignatureRole>
|
||||
signatureRole(SField const& sigField);
|
||||
|
||||
/**
|
||||
* The hash prefix that binds a transaction signature to the role that made it.
|
||||
*
|
||||
* @param role The role making the signature.
|
||||
* @param multiSigning Whether the signature is a multi-signature.
|
||||
* @param rules The current ledger rules.
|
||||
*/
|
||||
[[nodiscard]] HashPrefix
|
||||
signingPrefix(SignatureRole role, bool multiSigning, Rules const& rules);
|
||||
|
||||
/**
|
||||
* Sign an STObject
|
||||
*
|
||||
@@ -49,9 +101,12 @@ verify(
|
||||
|
||||
/**
|
||||
* Return a Serializer suitable for computing a multisigning TxnSignature.
|
||||
*
|
||||
* @param prefix Prefix to insert before the serialized object. Get it from
|
||||
* signingPrefix, so that the signature is bound to the role making it.
|
||||
*/
|
||||
Serializer
|
||||
buildMultiSigningData(STObject const& obj, AccountID const& signingID);
|
||||
buildMultiSigningData(STObject const& obj, AccountID const& signingID, HashPrefix prefix);
|
||||
|
||||
/**
|
||||
* Break the multi-signing hash computation into 2 parts for optimization.
|
||||
@@ -67,7 +122,7 @@ buildMultiSigningData(STObject const& obj, AccountID const& signingID);
|
||||
* signer's unique data.
|
||||
*/
|
||||
Serializer
|
||||
startMultiSigningData(STObject const& obj);
|
||||
startMultiSigningData(STObject const& obj, HashPrefix prefix);
|
||||
|
||||
inline void
|
||||
finishMultiSigningData(AccountID const& signingID, Serializer& s)
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
XRPL_FIX (Cleanup3_4_0, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(Sponsor, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(BatchV1_1, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(LendingProtocolV1_1, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(LendingProtocolV1_1, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(ConfidentialTransfer, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (Cleanup3_3_0, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (Cleanup3_2_0, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
|
||||
@@ -259,6 +259,13 @@ public:
|
||||
static XRPAmount
|
||||
calculateBaseFee(ReadView const& view, STTx const& tx, std::uint32_t extraBaseFeeMultiplier);
|
||||
|
||||
// Exposed for invariant checks (e.g. ValidVault) that need to know which
|
||||
// ledger entry actually pays a transaction's fee, distinguishing an
|
||||
// ordinary sender, a delegate, and pre-funded vs. co-signed fee
|
||||
// sponsorship.
|
||||
static FeePayer
|
||||
getFeePayer(ReadView const& view, STTx const& tx);
|
||||
|
||||
/* Do NOT define an invokePreflight function in a derived class.
|
||||
Instead, define:
|
||||
|
||||
@@ -525,9 +532,6 @@ private:
|
||||
std::pair<TER, XRPAmount>
|
||||
reset(XRPAmount fee);
|
||||
|
||||
static FeePayer
|
||||
getFeePayer(ReadView const& view, STTx const& tx);
|
||||
|
||||
TER
|
||||
consumeSeqProxy(SLE::pointer const& sleAccount);
|
||||
TER
|
||||
|
||||
@@ -19,6 +19,11 @@ namespace xrpl {
|
||||
* 1. If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most one
|
||||
* node (the root), which will only hold entries for `RippleState` or
|
||||
* `MPToken` objects.
|
||||
* 2. Under featureLendingProtocolV1_1, an `ltLOAN_BROKER` may only be deleted
|
||||
* by a `ttLOAN_BROKER_DELETE` transaction, and only when its pre-state
|
||||
* `OwnerCount` is zero and its pre-state `DebtTotal` rounds to zero at the
|
||||
* vault's `AssetsTotal` scale, as `LoanBrokerDelete::preclaim` requires.
|
||||
* 3. At most one `ltLOAN_BROKER` may be deleted in a single transaction.
|
||||
*
|
||||
*/
|
||||
class ValidLoanBroker
|
||||
@@ -36,6 +41,15 @@ class ValidLoanBroker
|
||||
// pseudo-accounts. Key is the brokerID / index. It will be used to find the
|
||||
// LoanBroker object if brokerBefore and brokerAfter are nullptr
|
||||
std::map<uint256, BrokerInfo> brokers_;
|
||||
// The broker whose ledger entry was deleted by this transaction, if any.
|
||||
// Only ttLOAN_BROKER_DELETE removes a broker, and it removes exactly one.
|
||||
// This is the pre-transaction state, which is what LoanBrokerDelete::preclaim
|
||||
// reads when it decides whether the broker may be deleted, so the deletion invariants inspect
|
||||
// the same DebtTotal and OwnerCount that the transactor did.
|
||||
SLE::const_pointer deletedBroker_ = nullptr;
|
||||
// Set if visitEntry observes more than one ltLOAN_BROKER deletion in the
|
||||
// same transaction. Enforced as its own invariant in finalize.
|
||||
bool multipleBrokerDeletions_ = false;
|
||||
// Collect all the modified trust lines. Their high and low accounts will be
|
||||
// loaded to look for LoanBroker pseudo-accounts.
|
||||
std::vector<SLE::const_pointer> lines_;
|
||||
|
||||
@@ -15,9 +15,33 @@ namespace xrpl {
|
||||
/**
|
||||
* @brief Invariants: Loans are internally consistent
|
||||
*
|
||||
* 1. If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0`
|
||||
* 1. If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0`.
|
||||
* 2. A newly-created Loan against a closed-ended vault must satisfy
|
||||
* `StartDate + PaymentInterval * PaymentRemaining < Vault.RedemptionDate`.
|
||||
* 3. An `ltLOAN` may only be created by a `ttLOAN_SET` transaction.
|
||||
* 4. Prior to `featureLendingProtocolV1_1`, the `lsfLoanOverpayment` flag on a
|
||||
* Loan must not change. From `featureLendingProtocolV1_1` onward the same
|
||||
* rule is enforced by `NoModifiedUnmodifiableFields`.
|
||||
* 5. Under `featureLendingProtocolV1_1`:
|
||||
* a. An `ltLOAN` may only be deleted by a `ttLOAN_DELETE` transaction.
|
||||
* b. If `Loan.PaymentRemaining = 0` then `Loan.NextPaymentDueDate = 0`.
|
||||
* c. The `lsfLoanImpaired` flag may only change through a `ttLOAN_MANAGE`
|
||||
* or `ttLOAN_PAY` transaction.
|
||||
* d. The `lsfLoanDefault` flag may only change through a `ttLOAN_MANAGE`
|
||||
* transaction. Combined with `NoModifiedUnmodifiableFields`, which
|
||||
* rejects any clearing of `lsfLoanDefault`, this makes the flag
|
||||
* write-once: `ttLOAN_MANAGE` may set it, and no transaction may
|
||||
* clear it.
|
||||
* e. Interest due, computed as `TotalValueOutstanding -
|
||||
* PrincipalOutstanding - ManagementFeeOutstanding`, must not be
|
||||
* negative.
|
||||
* f. A Loan must reference a live `ltLOAN_BROKER`, and that broker must
|
||||
* reference a live `ltVAULT`.
|
||||
* g. Post-conditions for the Loan paid down by a successful `ttLOAN_PAY`:
|
||||
* `PaymentRemaining > 0` after: `PrincipalOutstanding` and
|
||||
* `PaymentRemaining` strictly decrease; `NextPaymentDueDate`
|
||||
* advances by N * `PaymentInterval`, N > 0.
|
||||
* `PaymentRemaining == 0` after: pinned by checks 1 and 5b.
|
||||
*
|
||||
*/
|
||||
class ValidLoan
|
||||
@@ -25,6 +49,9 @@ class ValidLoan
|
||||
// Pair is <before, after>. After is used for most of the checks, except
|
||||
// those that check changed values.
|
||||
std::vector<std::pair<SLE::const_pointer, SLE::const_pointer>> loans_;
|
||||
// Loans removed from the ledger, in the same <before, after> form as loans_.
|
||||
// Note that `after` holds the erased entry, so it is not null.
|
||||
std::vector<std::pair<SLE::const_pointer, SLE::const_pointer>> deletedLoans_;
|
||||
|
||||
public:
|
||||
void
|
||||
|
||||
@@ -215,6 +215,13 @@ class ValidMPTTransfer
|
||||
// Deleted MPToken
|
||||
// MPToken key: true if MPTAuthorized is set
|
||||
hash_map<uint256, bool> deletedAuthorized_;
|
||||
// Every touched AccountRoot (not only pseudos):
|
||||
// AccountID -> whether it was a pseudo-account BEFORE this transaction
|
||||
// applied. Needed because a transaction may erase a pseudo-account and
|
||||
// move MPT out of it in the same transaction; by finalize() time the
|
||||
// view no longer shows it as a pseudo-account (or as existing at all).
|
||||
// False entries freeze the pre-tx classification for touched non-pseudos.
|
||||
hash_map<AccountID, bool> pseudoAccountsBefore_;
|
||||
|
||||
public:
|
||||
/**
|
||||
|
||||
@@ -48,7 +48,10 @@ namespace xrpl {
|
||||
* vault phase is Investment
|
||||
*
|
||||
* Immutability of VaultKind, SubscriptionDate and RedemptionDate is enforced
|
||||
* by NoModifiedUnmodifiableFields (see InvariantCheck.cpp).
|
||||
* by NoModifiedUnmodifiableFields (see InvariantCheck.cpp). From
|
||||
* featureLendingProtocolV1_1 onwards, immutability of the vault's Asset,
|
||||
* pseudo-account and ShareMPTID is likewise enforced by
|
||||
* NoModifiedUnmodifiableFields; prior to that amendment it is checked here.
|
||||
*/
|
||||
class ValidVault
|
||||
{
|
||||
@@ -128,20 +131,57 @@ private:
|
||||
deltaAssets(AccountID const& id) const;
|
||||
|
||||
/**
|
||||
* @brief Return the vault-asset delta for the transaction's sending
|
||||
* account, adjusted for the fee.
|
||||
* @brief Return the AccountRoot whose XRP balance actually absorbed a
|
||||
* transaction's fee, if any.
|
||||
*
|
||||
* Calls @c deltaAssets for @c tx[sfAccount] and, for non-delegated XRP
|
||||
* transactions, adds the consumed fee back so the invariant sees the net
|
||||
* asset movement rather than the fee-reduced balance change.
|
||||
* Mirrors @c Transactor::getFeePayer, but resolves to @c std::nullopt for
|
||||
* a pre-funded sponsorship: that fee is drawn from the @c ltSponsorship
|
||||
* object's @c sfFeeAmount, never from the sponsor's own AccountRoot, so
|
||||
* there is no balance to add back there.
|
||||
*
|
||||
* @param tx The transaction being applied.
|
||||
* @param fee Fee charged by this transaction.
|
||||
* @param view Read-only view of the ledger after the transaction.
|
||||
* @param tx The transaction being applied.
|
||||
* @return The fee-paying AccountRoot's id, or @c std::nullopt when the
|
||||
* fee was not drawn from any AccountRoot balance.
|
||||
*/
|
||||
[[nodiscard]] static std::optional<AccountID>
|
||||
feePayerAccountRoot(ReadView const& view, STTx const& tx);
|
||||
|
||||
/**
|
||||
* @brief Return the vault-asset delta for a party inspected as a
|
||||
* withdrawal/deposit counterparty, adjusted for the fee.
|
||||
*
|
||||
* Calls @c deltaAssets for @p id and, for XRP transactions, adds the
|
||||
* consumed fee back only when @p id is the AccountRoot that actually
|
||||
* paid it (per @c feePayerAccountRoot) -- so the invariant sees the net
|
||||
* asset movement rather than a fee-reduced balance change, regardless of
|
||||
* whether @p id is the sender, a distinct destination, a delegate, or a
|
||||
* co-signed fee sponsor. Post-@c fixCleanup3_4_0, any resulting
|
||||
* economically-zero delta is always normalized to absence.
|
||||
*
|
||||
* Pre-@c fixCleanup3_4_0 this replicates the legacy behaviour exactly:
|
||||
* only @c tx[sfAccount] could ever receive a fee correction (and only
|
||||
* when it was itself, per @c STTx::getFeePayerID, the fee payer). After
|
||||
* that sender-only correction a zero delta is collapsed to absence; if
|
||||
* the correction does not apply, a present-zero delta is kept as-is.
|
||||
*
|
||||
* @param view Read-only view of the ledger after the transaction.
|
||||
* @param id Account being inspected as sender or destination.
|
||||
* @param tx The transaction being applied.
|
||||
* @param fee Fee charged by this transaction.
|
||||
* @param fix340Enabled Whether @c fixCleanup3_4_0 is enabled, as already
|
||||
* determined once by @c finalize.
|
||||
* @return The fee-adjusted delta, or @c std::nullopt if the net delta is
|
||||
* zero or the account entry was not touched.
|
||||
* zero (always post-amendment; pre-amendment only after the
|
||||
* sender-only fee correction) or the entry was not touched.
|
||||
*/
|
||||
[[nodiscard]] std::optional<DeltaInfo>
|
||||
deltaAssetsTxAccount(STTx const& tx, XRPAmount fee) const;
|
||||
deltaAssetsForParty(
|
||||
ReadView const& view,
|
||||
AccountID const& id,
|
||||
STTx const& tx,
|
||||
XRPAmount fee,
|
||||
bool fix340Enabled) const;
|
||||
|
||||
/**
|
||||
* @brief Return the vault-share balance-change delta for an account.
|
||||
@@ -171,8 +211,8 @@ private:
|
||||
*
|
||||
* For a closed-ended vault, a loan may only be originated while the vault is in the Investment
|
||||
* phase (strictly past @c SubscriptionDate and before @c RedemptionDate). Open-ended vaults (@c
|
||||
* NoPhase) are unaffected. The complementary maturity bound (final payment strictly precedes @c
|
||||
* RedemptionDate) is enforced by @c ValidLoan.
|
||||
* NoPhase) are unaffected. The complementary maturity bound (final payment precedes @c
|
||||
* RedemptionDate by at least @c kLoanRedemptionBuffer) is enforced by @c ValidLoan.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
finalizeLoanSet(ReadView const& view, beast::Journal const& j) const;
|
||||
|
||||
@@ -109,6 +109,11 @@ public:
|
||||
* @param lpTokens current LPT balance
|
||||
* @param lpTokensWithdraw amount of tokens to withdraw
|
||||
* @param tfee trading fee in basis points
|
||||
* @param freezeHandling whether a frozen balance is reported as zero
|
||||
* @param authHandling whether an unauthorized MPT balance is reported as
|
||||
* zero
|
||||
* @param reserveHandling whether the recipient owner-reserve check is
|
||||
* enforced when a trustline or MPToken has to be auto-created
|
||||
* @param withdrawAll if withdrawing all lptokens
|
||||
* @param priorBalance balance before fees
|
||||
* @return
|
||||
@@ -128,6 +133,7 @@ public:
|
||||
std::uint16_t tfee,
|
||||
FreezeHandling freezeHandling,
|
||||
AuthHandling authHandling,
|
||||
ReserveHandling reserveHandling,
|
||||
WithdrawAll withdrawAll,
|
||||
XRPAmount const& priorBalance,
|
||||
beast::Journal const& journal);
|
||||
@@ -150,6 +156,11 @@ public:
|
||||
* @param lpTokensAMMBalance current AMM LPT balance
|
||||
* @param lpTokensWithdraw amount of lptokens to withdraw
|
||||
* @param tfee trading fee in basis points
|
||||
* @param freezeHandling whether a frozen balance is reported as zero
|
||||
* @param authHandling whether an unauthorized MPT balance is reported as
|
||||
* zero
|
||||
* @param reserveHandling whether the recipient owner-reserve check is
|
||||
* enforced when a trustline or MPToken has to be auto-created
|
||||
* @param withdrawAll if withdraw all lptokens
|
||||
* @param priorBalance balance before fees
|
||||
* @return
|
||||
@@ -169,6 +180,7 @@ public:
|
||||
std::uint16_t tfee,
|
||||
FreezeHandling freezeHandling,
|
||||
AuthHandling authHandling,
|
||||
ReserveHandling reserveHandling,
|
||||
WithdrawAll withdrawAll,
|
||||
XRPAmount const& priorBalance,
|
||||
beast::Journal const& journal);
|
||||
|
||||
@@ -114,8 +114,8 @@ Development tooling:
|
||||
|
||||
Rust toolchain:
|
||||
✅ cargo
|
||||
cargo 1.95.0 (f2d3ce0bd 2026-03-21)
|
||||
/nix/store/92vz1f4kislnj58j1pr1788l688py6f0-rust-minimal-1.95.0/bin/cargo
|
||||
cargo 1.97.1 (c980f4866 2026-06-30)
|
||||
/nix/store/bnfk1sl4s9angb0vj1cj9a5y5zvqinwy-rust-minimal-1.97.1/bin/cargo
|
||||
✅ cargo-audit
|
||||
cargo-audit-audit 0.22.1
|
||||
/nix/store/snwkga2f5gyf404h7mmp9wriwxb8v65f-cargo-audit-0.22.1/bin/cargo-audit
|
||||
@@ -126,17 +126,17 @@ Rust toolchain:
|
||||
cargo-nextest 0.9.137
|
||||
/nix/store/ylz7m947mhkgsp6i7611id3s3gcd58nq-cargo-nextest-0.9.137/bin/cargo-nextest
|
||||
✅ clippy-driver
|
||||
clippy 0.1.95 (59807616e1 2026-04-14)
|
||||
/nix/store/92vz1f4kislnj58j1pr1788l688py6f0-rust-minimal-1.95.0/bin/clippy-driver
|
||||
clippy 0.1.97 (8bab26f4f6 2026-07-14)
|
||||
/nix/store/bnfk1sl4s9angb0vj1cj9a5y5zvqinwy-rust-minimal-1.97.1/bin/clippy-driver
|
||||
✅ rust-analyzer
|
||||
rust-analyzer 1.95.0 (59807616 2026-04-14)
|
||||
/nix/store/jqvjap2727r9cjpr25fkw5glv2kbxrdx-rust-analyzer-preview-1.95.0-aarch64-apple-darwin/bin/rust-analyzer
|
||||
rust-analyzer 1.97.1 (8bab26f4 2026-07-14)
|
||||
/nix/store/j6apc5pmd0giy15da9p650r8zklslmvi-rust-analyzer-preview-1.97.1-aarch64-apple-darwin/bin/rust-analyzer
|
||||
✅ rustc
|
||||
rustc 1.95.0 (59807616e 2026-04-14)
|
||||
/nix/store/92vz1f4kislnj58j1pr1788l688py6f0-rust-minimal-1.95.0/bin/rustc
|
||||
rustc 1.97.1 (8bab26f4f 2026-07-14)
|
||||
/nix/store/bnfk1sl4s9angb0vj1cj9a5y5zvqinwy-rust-minimal-1.97.1/bin/rustc
|
||||
✅ rustfmt
|
||||
rustfmt 1.9.0-stable (59807616e1 2026-04-14)
|
||||
/nix/store/03x750yj6fakl7shbhicpnkxiwqxjrrs-rustfmt-preview-1.95.0-aarch64-apple-darwin/bin/rustfmt
|
||||
rustfmt 1.9.0-stable (8bab26f4f6 2026-07-14)
|
||||
/nix/store/5ymwgr9jqjz7zzbmj0j5vqbwcd3kp0vm-rustfmt-preview-1.97.1-aarch64-apple-darwin/bin/rustfmt
|
||||
|
||||
Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set).
|
||||
|
||||
|
||||
@@ -114,8 +114,8 @@ Development tooling:
|
||||
|
||||
Rust toolchain:
|
||||
✅ cargo
|
||||
cargo 1.95.0 (f2d3ce0bd 2026-03-21)
|
||||
/nix/store/85qbwr3vzfs58m7ywnjblz105p8ahbrv-cargo-1.95.0-x86_64-unknown-linux-gnu/bin/cargo
|
||||
cargo 1.97.1 (c980f4866 2026-06-30)
|
||||
/nix/store/88abzp43ywyzql1rhf8jh5aj5n5j7xzr-cargo-1.97.1-x86_64-unknown-linux-gnu/bin/cargo
|
||||
✅ cargo-audit
|
||||
cargo-audit-audit 0.22.1
|
||||
/nix/store/2w9if868piw98xz057sz97jnjvf7hnvf-cargo-audit-0.22.1/bin/cargo-audit
|
||||
@@ -126,17 +126,17 @@ Rust toolchain:
|
||||
cargo-nextest 0.9.137
|
||||
/nix/store/jhkr7gwyrchkml33gyns9cy0yn7b57qc-cargo-nextest-0.9.137/bin/cargo-nextest
|
||||
✅ clippy-driver
|
||||
clippy 0.1.95 (59807616e1 2026-04-14)
|
||||
/nix/store/bnvg9nmdq4g98dd9v3r6nvjg5h2rr8i7-rust-minimal-1.95.0/bin/clippy-driver
|
||||
clippy 0.1.97 (8bab26f4f6 2026-07-14)
|
||||
/nix/store/40d3mzka7r1ps71l0yv2fs6616nbw85m-rust-minimal-1.97.1/bin/clippy-driver
|
||||
✅ rust-analyzer
|
||||
rust-analyzer 1.95.0 (5980761 2026-04-14)
|
||||
/nix/store/i3cnpngfwa3k4jn431pl6ji1r4qmxky9-rust-analyzer-preview-1.95.0-x86_64-unknown-linux-gnu/bin/rust-analyzer
|
||||
rust-analyzer 1.97.1 (8bab26f 2026-07-14)
|
||||
/nix/store/lr3m97p3hx1k22a7c44pb0wa7rbayhfi-rust-analyzer-preview-1.97.1-x86_64-unknown-linux-gnu/bin/rust-analyzer
|
||||
✅ rustc
|
||||
rustc 1.95.0 (59807616e 2026-04-14)
|
||||
/nix/store/bnvg9nmdq4g98dd9v3r6nvjg5h2rr8i7-rust-minimal-1.95.0/bin/rustc
|
||||
rustc 1.97.1 (8bab26f4f 2026-07-14)
|
||||
/nix/store/40d3mzka7r1ps71l0yv2fs6616nbw85m-rust-minimal-1.97.1/bin/rustc
|
||||
✅ rustfmt
|
||||
rustfmt 1.9.0-stable (59807616e1 2026-04-14)
|
||||
/nix/store/366hhk2dgwxmnf4hgrj4b8llhjr3hf0i-rustfmt-preview-1.95.0-x86_64-unknown-linux-gnu/bin/rustfmt
|
||||
rustfmt 1.9.0-stable (8bab26f4f6 2026-07-14)
|
||||
/nix/store/6f1icmb2za20kxn30pgmbv5jq9fnbf4z-rustfmt-preview-1.97.1-x86_64-unknown-linux-gnu/bin/rustfmt
|
||||
|
||||
GCC toolchain:
|
||||
✅ gcc
|
||||
|
||||
@@ -114,8 +114,8 @@ Development tooling:
|
||||
|
||||
Rust toolchain:
|
||||
✅ cargo
|
||||
cargo 1.95.0 (f2d3ce0bd 2026-03-21)
|
||||
/nix/store/yw1rs50s6qpsw0zyl7j3dpm18swbl0ag-cargo-1.95.0-aarch64-unknown-linux-gnu/bin/cargo
|
||||
cargo 1.97.1 (c980f4866 2026-06-30)
|
||||
/nix/store/6hch2qrr86n2sa2m90lrpxrfxxwbkayl-cargo-1.97.1-aarch64-unknown-linux-gnu/bin/cargo
|
||||
✅ cargo-audit
|
||||
cargo-audit-audit 0.22.1
|
||||
/nix/store/9rxbrn9aa2r1z96186s69pc7vzizyfch-cargo-audit-0.22.1/bin/cargo-audit
|
||||
@@ -126,17 +126,17 @@ Rust toolchain:
|
||||
cargo-nextest 0.9.137
|
||||
/nix/store/qb6bcg2fjvm3r9s9j98nmffmf9xwh45s-cargo-nextest-0.9.137/bin/cargo-nextest
|
||||
✅ clippy-driver
|
||||
clippy 0.1.95 (59807616e1 2026-04-14)
|
||||
/nix/store/nz4qv12pf16c092qr9hh4dsn0fzf47da-rust-minimal-1.95.0/bin/clippy-driver
|
||||
clippy 0.1.97 (8bab26f4f6 2026-07-14)
|
||||
/nix/store/a6p27cg6b8szfixfyvkssx6l0c345zw8-rust-minimal-1.97.1/bin/clippy-driver
|
||||
✅ rust-analyzer
|
||||
rust-analyzer 1.95.0 (5980761 2026-04-14)
|
||||
/nix/store/m1rn67sqfz8s44idcxqallg680ifk71r-rust-analyzer-preview-1.95.0-aarch64-unknown-linux-gnu/bin/rust-analyzer
|
||||
rust-analyzer 1.97.1 (8bab26f 2026-07-14)
|
||||
/nix/store/262830dlw2517lnagfx7i7agqgl4fmsd-rust-analyzer-preview-1.97.1-aarch64-unknown-linux-gnu/bin/rust-analyzer
|
||||
✅ rustc
|
||||
rustc 1.95.0 (59807616e 2026-04-14)
|
||||
/nix/store/nz4qv12pf16c092qr9hh4dsn0fzf47da-rust-minimal-1.95.0/bin/rustc
|
||||
rustc 1.97.1 (8bab26f4f 2026-07-14)
|
||||
/nix/store/a6p27cg6b8szfixfyvkssx6l0c345zw8-rust-minimal-1.97.1/bin/rustc
|
||||
✅ rustfmt
|
||||
rustfmt 1.9.0-stable (59807616e1 2026-04-14)
|
||||
/nix/store/jidfsprj2820glyzjn54ldn3j1fmz8c5-rustfmt-preview-1.95.0-aarch64-unknown-linux-gnu/bin/rustfmt
|
||||
rustfmt 1.9.0-stable (8bab26f4f6 2026-07-14)
|
||||
/nix/store/nd8g81wv1smnvdpy4whpcyv2siwjmaan-rustfmt-preview-1.97.1-aarch64-unknown-linux-gnu/bin/rustfmt
|
||||
|
||||
GCC toolchain:
|
||||
✅ gcc
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
ARG BASE_IMAGE=debian:bookworm
|
||||
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
COPY package/install-packaging-tools.sh /tmp/install-packaging-tools.sh
|
||||
|
||||
RUN /tmp/install-packaging-tools.sh
|
||||
@@ -8,12 +8,14 @@ a build configured with `-Dvalidator_keys=ON`.
|
||||
|
||||
```
|
||||
package/
|
||||
build_pkg.sh Staging and build script (called by the CMake `package` target and CI)
|
||||
sign_rpm.sh Signs the built RPMs (called by CI when publishing)
|
||||
publish_pkg.sh Uploads built packages to the XRPLF Nexus repositories (called by CI)
|
||||
build_pkg.py Staging and build script (called by the CMake `package` target and CI)
|
||||
sign_rpm.py Signs the built RPMs (called by CI when publishing)
|
||||
docker/
|
||||
Dockerfile Packaging image, built by `build-packaging-images.yml`; installs its tooling with `bin/install-packaging-tools.sh`
|
||||
publish_pkg.py Uploads built packages to the XRPLF Nexus repositories (called by CI, and shipped in that image)
|
||||
rpm/
|
||||
xrpld.spec RPM spec
|
||||
debian/ Debian control files (control, rules, copyright, xrpld.docs, xrpld.links, source/format)
|
||||
debian/ Debian control files (control, rules, copyright, xrpld.docs, xrpld.links, xrpld.lintian-overrides, source/format)
|
||||
shared/
|
||||
xrpld.service systemd unit file (used by both RPM and DEB)
|
||||
xrpld.sysusers sysusers.d config (used by both RPM and DEB)
|
||||
@@ -23,18 +25,19 @@ package/
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Packaging targets and their container images are declared in
|
||||
[`.github/scripts/strategy-matrix/linux.json`](../.github/scripts/strategy-matrix/linux.json)
|
||||
under `package_configs`, one entry per distro. Today only `linux/amd64` is
|
||||
emitted. Each entry pins its full container image in an `image` field; to move
|
||||
to a new image, edit that field and both CI and local builds pick it up. The
|
||||
package format (deb or rpm) is inferred at build time from the container's
|
||||
package manager (`apt-get` -> deb, `dnf`/`yum` -> rpm).
|
||||
Packaging is declared on the build configs themselves, in
|
||||
[`.github/scripts/strategy-matrix/linux.json`](../.github/scripts/strategy-matrix/linux.json):
|
||||
a config that is also packaged carries a `package` map, so its binaries and its
|
||||
packaging job cannot drift apart. Today only `linux/amd64` is emitted. The map
|
||||
pins the full container image in `image` — edit that field to move to a new
|
||||
image and both CI and local builds pick it up — and names the format that image
|
||||
builds in `type`, which CI passes to `build_pkg.py` as `--package-type`; the two
|
||||
have to stay in step.
|
||||
|
||||
| Package type | Image (`package_configs.<distro>[].image` in `linux.json`) | Tools required |
|
||||
| ------------ | ---------------------------------------------------------- | --------------------------------------------------- |
|
||||
| RPM | `ghcr.io/xrplf/xrpld/packaging-rhel:sha-<sha>` | `rpmbuild`, `rpmsign` |
|
||||
| DEB | `ghcr.io/xrplf/xrpld/packaging-debian:sha-<sha>` | `dpkg-buildpackage`, debhelper with compat level 13 |
|
||||
| Package type | Image (`configs.<distro>[].package.image` in `linux.json`) | Tools required |
|
||||
| ------------ | ---------------------------------------------------------- | -------------------------------------------------------------- |
|
||||
| RPM | `ghcr.io/xrplf/xrpld/packaging-rhel:sha-<sha>` | `rpmbuild`, `rpmsign` |
|
||||
| DEB | `ghcr.io/xrplf/xrpld/packaging-debian:sha-<sha>` | `dpkg-buildpackage`, debhelper with compat level 13, `lintian` |
|
||||
|
||||
To print the full packaging matrix (artifact names and images) for the current
|
||||
`linux.json`:
|
||||
@@ -48,20 +51,27 @@ To print the full packaging matrix (artifact names and images) for the current
|
||||
### Via CI
|
||||
|
||||
Caller workflows (`on-pr.yml`, `on-tag.yml`, `on-trigger.yml`) call
|
||||
`reusable-package.yml`. That workflow generates its own packaging matrix from
|
||||
`package_configs` in `linux.json` (via `generate.py --packaging`) and fans out
|
||||
one job per distro. Each job downloads the pre-built `xrpld` and `validator-keys`
|
||||
binary artifacts and runs in that distro's container, so the package format
|
||||
follows from the container's package manager. The packaging script derives the
|
||||
package version from the downloaded binary's `xrpld --version` output; no CMake
|
||||
configure or build step is needed inside the packaging job.
|
||||
`reusable-package.yml`, which runs in three stages:
|
||||
|
||||
The binaries come from the `debian` and `rhel` build configurations in
|
||||
`linux.json`'s `configs` section, which pass `-Dvalidator_keys=ON` so that the
|
||||
1. `package` fans out one job per config carrying a `package` map, building and
|
||||
signing in that config's container, and uploading `<config>-pkg` alongside
|
||||
`<config>-pkg-debug` for the much larger debug symbols.
|
||||
2. `test-install` installs `<config>-pkg` in the container of every distro the
|
||||
packages target and runs the binaries there, so one that cannot be installed
|
||||
never reaches Nexus.
|
||||
3. `publish` uploads both artifacts, or lists what it would upload.
|
||||
|
||||
The packaging script derives the package version from the downloaded binary's
|
||||
`xrpld --version` output; no CMake configure or build step is needed inside the
|
||||
packaging job.
|
||||
|
||||
The binaries come from the `debian` and `rhel` build configs themselves — the
|
||||
ones carrying the `package` map — which pass `-Dvalidator_keys=ON` so that the
|
||||
build job produces `validator-keys` next to `xrpld` and uploads it as the
|
||||
`validator-keys-<config name>` artifact. The packaging entry for a distro names
|
||||
both artifacts (`xrpld_artifact_name` and `validator_keys_artifact_name`), so a
|
||||
packaged configuration must keep `-Dvalidator_keys=ON`.
|
||||
`validator-keys-<config name>` artifact. The packaging matrix names both
|
||||
artifacts (`xrpld_artifact_name` and `validator_keys_artifact_name`) after that
|
||||
same config, so a packaged config must keep `-Dvalidator_keys=ON`. Those configs
|
||||
are not `minimal`, so `on-pr.yml` only packages once a PR runs the full matrix.
|
||||
|
||||
`validator-keys` is fetched from an exact commit pinned in
|
||||
[`cmake/XrplValidatorKeys.cmake`](../cmake/XrplValidatorKeys.cmake), so a given
|
||||
@@ -74,11 +84,10 @@ With `xrpld` and `validator-keys` binaries already built at `build/xrpld` and
|
||||
The image tag is derived from `linux.json` so you don't need to hardcode a SHA.
|
||||
|
||||
```bash
|
||||
# From the repo root. Each distro's container image is the `image` field of its
|
||||
# package_configs entry in linux.json; the package format is inferred from the
|
||||
# container's package manager. Example for the rpm-producing image (use
|
||||
# .package_configs.debian[0].image for the deb image):
|
||||
IMAGE=$(jq -r '.package_configs.rhel[0].image' .github/scripts/strategy-matrix/linux.json)
|
||||
# From the repo root. Each distro's container image is the `package.image` field
|
||||
# of its config in linux.json. Example for the rpm-producing image (use
|
||||
# .configs.debian[0].package.image and --package-type deb for the other one):
|
||||
IMAGE=$(jq -r '.configs.rhel[0].package.image' .github/scripts/strategy-matrix/linux.json)
|
||||
|
||||
PKG_RELEASE=1
|
||||
|
||||
@@ -86,10 +95,12 @@ docker run --rm \
|
||||
-v "$(pwd):/src" \
|
||||
-w /src \
|
||||
"${IMAGE}" \
|
||||
./package/build_pkg.sh --pkg-release "${PKG_RELEASE}"
|
||||
./package/build_pkg.py \
|
||||
--package-type rpm \
|
||||
--pkg-release "${PKG_RELEASE}" \
|
||||
--channel UNRELEASED
|
||||
|
||||
# Output:
|
||||
# build/debbuild/*.deb (DEB + dbgsym; Debian names both .deb)
|
||||
# Output (the deb image writes build/debbuild/*.deb instead):
|
||||
# build/rpmbuild/RPMS/x86_64/*.rpm
|
||||
```
|
||||
|
||||
@@ -113,12 +124,12 @@ cmake --build . --target package # deb on Debian/Ubuntu, rpm on RHEL
|
||||
The `cmake/XrplPackaging.cmake` module defines the `package` target only if at
|
||||
least one of `rpmbuild` / `dpkg-buildpackage` is present and both the `xrpld` and
|
||||
`validator-keys` targets exist (`-Dxrpld=ON -Dvalidator_keys=ON`); the target
|
||||
builds both binaries before packaging. `build_pkg.sh` then infers the package
|
||||
format from the host's package manager. The packaging script installs to
|
||||
FHS-standard paths (`/usr/bin`, `/etc/xrpld`, etc.) regardless of
|
||||
`CMAKE_INSTALL_PREFIX`.
|
||||
builds both binaries before packaging, passing `--package-type deb` when
|
||||
`dpkg-buildpackage` is present and `rpm` otherwise, and `--channel UNRELEASED`.
|
||||
The packaging script installs to FHS-standard paths (`/usr/bin`, `/etc/xrpld`,
|
||||
etc.) regardless of `CMAKE_INSTALL_PREFIX`.
|
||||
|
||||
The package version is not a CMake input on this path: `build_pkg.sh` derives it
|
||||
The package version is not a CMake input on this path: `build_pkg.py` derives it
|
||||
from the just-built `xrpld` binary's `xrpld --version` output. The package
|
||||
release defaults to 1 and is overridable with `-Dpkg_release=N`.
|
||||
|
||||
@@ -126,15 +137,15 @@ release defaults to 1 and is overridable with `-Dpkg_release=N`.
|
||||
|
||||
Packages are published to the XRPLF repositories on Sonatype Nexus at
|
||||
`https://packages.xrplf.org`. The `release-info` action decides the channel from
|
||||
the event, and `publish_pkg.sh` maps that channel to its repositories:
|
||||
the event, and `publish_pkg.py` maps that channel to its repositories:
|
||||
|
||||
| Event | Version | Channel | DEB repository | RPM upload repository |
|
||||
| ------------------------ | ----------------- | -------------- | ------------------ | ------------------------- |
|
||||
| tag | `X.Y.Z` | `stable` | `deb-stable` | `rpm-stable-hosted` |
|
||||
| tag | `X.Y.Z-rcN` | `unstable` | `deb-unstable` | `rpm-unstable-hosted` |
|
||||
| tag | `X.Y.Z-bN` | `experimental` | `deb-experimental` | `rpm-experimental-hosted` |
|
||||
| push to `develop` | `xrpld --version` | `develop` | `deb-develop` | `rpm-develop-hosted` |
|
||||
| tag, non-public codebase | _any_ | `private` | `deb-private` | `rpm-private-hosted` |
|
||||
| Event | Version | Channel | DEB repository | RPM upload repository |
|
||||
| ------------------------ | ----------------- | --------- | -------------- | --------------------- |
|
||||
| tag | `X.Y.Z` | `stable` | `deb-stable` | `rpm-stable-hosted` |
|
||||
| tag | `X.Y.Z-rcN` | `rc` | `deb-rc` | `rpm-rc-hosted` |
|
||||
| tag | `X.Y.Z-bN` | `beta` | `deb-beta` | `rpm-beta-hosted` |
|
||||
| push to `develop` | `xrpld --version` | `develop` | `deb-develop` | `rpm-develop-hosted` |
|
||||
| tag, non-public codebase | _any_ | `private` | `deb-private` | `rpm-private-hosted` |
|
||||
|
||||
Only a tag names a channel — do not extend that to `develop`, where
|
||||
`BuildInfo.cpp`'s `versionString` moves through `-bN`, `-rcN` and even the final
|
||||
@@ -143,15 +154,21 @@ Versions sort in row order, so moving to a more mature channel never downgrades.
|
||||
|
||||
The action decides the package release number on the same split: a tag's version
|
||||
is unique, so its packages are release 1, while develop repeats the same version
|
||||
and takes `github.run_number` so each push supersedes the last. Both reach the
|
||||
packaging scripts as arguments, so neither script derives anything itself.
|
||||
and takes `<run number>.<commit date>git<commit hash>`, e.g.
|
||||
`857.20260826gitb6a8995` — the leading run number keeps each push superseding
|
||||
the last, and the date and hash say which commit a package on
|
||||
`packages.xrplf.org` came from. Both reach the packaging scripts as arguments,
|
||||
so neither script derives anything itself.
|
||||
|
||||
Publishing is the last step of each packaging job, uploading from the container
|
||||
that built the packages. It runs when the caller passes `publish: true`:
|
||||
`on-trigger.yml` for develop pushes in `XRPLF/rippled`, `on-tag.yml` for tags in
|
||||
any `XRPLF` repository, `on-pr.yml` never. Both authenticate with the
|
||||
Publishing is its own job, gated behind `test-install`, uploading from the same
|
||||
image that built the packages with the `publish_pkg.py` shipped in it — the
|
||||
same copy other repositories run. Without `publish: true` the job is a
|
||||
`--dry-run`, listing the uploads it would make without needing credentials, so
|
||||
any run that builds packages also exercises the upload routing. `on-trigger.yml`
|
||||
passes `publish: true` for develop pushes in `XRPLF/rippled` and `on-tag.yml`
|
||||
for tags in any `XRPLF` repository, both authenticating with the
|
||||
`NEXUS_REMOTE_USERNAME` / `NEXUS_REMOTE_PASSWORD` secrets already used for the
|
||||
Conan remote.
|
||||
Conan remote; `on-pr.yml` never publishes.
|
||||
|
||||
Nexus owns the repository metadata; nothing here indexes anything. Worth knowing:
|
||||
|
||||
@@ -162,7 +179,7 @@ Nexus owns the repository metadata; nothing here indexes anything. Worth knowing
|
||||
repository sits behind a `rpm-<channel>` yum group repository whose metadata
|
||||
Nexus signs. Uploads go to the hosted repository; clients point at the group
|
||||
and verify the metadata with `repo_gpgcheck=1`. Nexus never signs the RPMs
|
||||
themselves, so `sign_rpm.sh` signs them before they are uploaded, and clients
|
||||
themselves, so `sign_rpm.py` signs them before they are uploaded, and clients
|
||||
verify them with `gpgcheck=1`.
|
||||
- yum metadata is rebuilt asynchronously, so a successful publish is not
|
||||
immediately installable.
|
||||
@@ -172,20 +189,29 @@ Nexus owns the repository metadata; nothing here indexes anything. Worth knowing
|
||||
- The `develop` repositories gain a package per push, so they need a cleanup
|
||||
policy to stay bounded; tagged channels publish each version once.
|
||||
|
||||
## How `build_pkg.sh` works
|
||||
### Publishing from other repositories
|
||||
|
||||
`build_pkg.sh` derives the `xrpld` software version from
|
||||
`publish_pkg.py` knows nothing about `xrpld`, so the packaging image
|
||||
installs it at `/usr/local/bin/publish_pkg.py` for other XRPLF repositories that
|
||||
build their packages elsewhere.
|
||||
|
||||
## How `build_pkg.py` works
|
||||
|
||||
`build_pkg.py` derives the `xrpld` software version from
|
||||
`${BUILD_DIR}/xrpld --version` in both package formats.
|
||||
|
||||
The binary's version is already SemVer-validated by `BuildInfo`.
|
||||
`build_pkg.sh` converts pre-release versions such as `3.2.0-b1` or
|
||||
`build_pkg.py` converts pre-release versions such as `3.2.0-b1` or
|
||||
`3.2.0-rc1` from `-` to `~` for package metadata so pre-releases sort before
|
||||
the final release. If that normalized package version still contains `-`,
|
||||
packaging fails because RPM forbids `-` in `Version`, and Debian uses `-` as
|
||||
the upstream/revision separator.
|
||||
|
||||
> [!NOTE]
|
||||
> Debug and sanitizer builds are not packaged yet.
|
||||
|
||||
`pkg_version` is the normalized package metadata version derived inside
|
||||
`build_pkg.sh` from the binary-reported `xrpld` version (`-` pre-release
|
||||
`build_pkg.py` from the binary-reported `xrpld` version (`-` pre-release
|
||||
separator converted to `~`). It is not a separate user input.
|
||||
|
||||
`PKG_RELEASE` is a different value: the package release iteration for that
|
||||
@@ -203,35 +229,41 @@ With `PKG_RELEASE=1`, the package metadata becomes:
|
||||
| `3.2.0-b1` | `3.2.0~b1-1%{?dist}` | `3.2.0~b1-1` |
|
||||
| `3.2.0-rc1` | `3.2.0~rc1-1%{?dist}` | `3.2.0~rc1-1` |
|
||||
|
||||
The Debian changelog entry carries the channel passed as `--channel`
|
||||
(`PKG_CHANNEL`), defaulting to `unstable`. An unsupported pre-release, and build
|
||||
metadata on a final release such as `3.2.0+abc123`, are both rejected.
|
||||
`build_pkg.py` defines `dist` as `.el9` rather than letting rpmbuild take it
|
||||
from the build host, so the RHEL image can track a newer release without
|
||||
changing what the packages claim to target.
|
||||
|
||||
The Debian changelog entry carries the channel passed as `--channel`, which
|
||||
only accepts the channels in the table above plus `UNRELEASED`, the Debian
|
||||
convention for a build that targets no channel at all — what local and CMake
|
||||
builds pass, since nothing publishes them. An unsupported pre-release, and
|
||||
build metadata on a final release such as `3.2.0+abc123`, are both rejected.
|
||||
|
||||
The RPM path intentionally uses `~` in `Version`, matching the Debian
|
||||
pre-release ordering convention, so RPM filenames/NVRs begin with forms like
|
||||
`xrpld-3.2.0~b1-...` and `xrpld-3.2.0~rc1-...` instead of encoding
|
||||
pre-releases with an older `0.<release>.<suffix>` RPM `Release` value.
|
||||
|
||||
The package format (`deb` or `rpm`) is inferred from the host's package
|
||||
manager (`apt-get` -> deb, `dnf`/`yum` -> rpm). Hosts without one of those
|
||||
fail early.
|
||||
The package format is `--package-type`, either `deb` or `rpm`. It is required,
|
||||
so a job never silently builds the wrong format for the image it runs in; the
|
||||
matching build tool still has to be on PATH.
|
||||
|
||||
Flags are for explicit invocation; environment variables are intended for
|
||||
CMake/CI integration. The CI workflow and the CMake `package` target both invoke
|
||||
`build_pkg.sh` with no flags; CMake supplies `SRC_DIR`, `BUILD_DIR`, and
|
||||
`PKG_RELEASE` via env, while CI supplies `BUILD_DIR`, `PKG_RELEASE` and
|
||||
`PKG_CHANNEL` via env and lets the script use defaults for the rest.
|
||||
Every input is a named argument, and every argument but `--build-dir` and
|
||||
`--pkg-release` is required. The repository root is not an argument
|
||||
at all: the script reads it from its own location. Only secrets stay in the
|
||||
environment, so they never reach the process list -- `PKG_SIGNING_KEY` for
|
||||
`sign_rpm.py`, and `NEXUS_USERNAME` / `NEXUS_PASSWORD` for `publish_pkg.py`.
|
||||
|
||||
Signing is not part of this script. `sign_rpm.sh` does it in a separate CI step
|
||||
Signing is not part of this script. `sign_rpm.py` does it in a separate CI step
|
||||
that only runs when publishing, so a published RPM is always signed and a local
|
||||
build never needs a key.
|
||||
|
||||
It resolves `SRC_DIR` and `BUILD_DIR` to absolute paths, then calls
|
||||
It resolves the build directory to an absolute path, then calls
|
||||
`stage_common()` to copy the `xrpld` and `validator-keys` binaries, config files,
|
||||
and shared support files into the staging area, and invokes the platform build
|
||||
tool. Both binaries must be present in `BUILD_DIR` and must run in the packaging
|
||||
environment; a missing or non-runnable one fails early. That runtime check is
|
||||
what catches a binary still linked against the Nix store's ELF loader (see
|
||||
tool. Both binaries must be present in the build directory and must run in the
|
||||
packaging environment; a missing or non-runnable one fails early. That runtime
|
||||
check is what catches a binary still linked against the Nix store's ELF loader (see
|
||||
`patch_nix_binary` in `cmake/PatchNixBinary.cmake`).
|
||||
|
||||
### RPM
|
||||
@@ -255,38 +287,45 @@ service restart.
|
||||
2. Stages the binaries, configs, `README.md`, `LICENSE.md`, and
|
||||
`validator-keys-LICENSE`.
|
||||
3. Copies `package/debian/` control files into `debbuild/source/debian/`.
|
||||
4. Copies shared service/sysusers/tmpfiles into `debian/` where `dh_installsystemd`, `dh_installsysusers`, and `dh_installtmpfiles` pick them up automatically.
|
||||
4. Copies shared service/sysusers/tmpfiles/logrotate into `debian/` where `dh_installsystemd`, `dh_installsysusers`, `dh_installtmpfiles` and `dh_installlogrotate` pick them up automatically.
|
||||
5. Generates a minimal `debian/changelog` using `${pkg_version}-${PKG_RELEASE}`,
|
||||
where `pkg_version` is derived from the binary-reported `xrpld` version.
|
||||
6. Runs `dpkg-buildpackage -b --no-sign -d` (`-d` skips the build-dependency check, since the binary is already built). `debian/rules` uses manual `install` commands.
|
||||
|
||||
It also rewrites the `libc6` bound to `LIBC_MIN` in `debian/rules`, the glibc
|
||||
the Nix toolchain builds against. `dpkg-shlibdeps` would otherwise derive it
|
||||
from the build host's symbols file — on trixie that yields `libc6 (>= 2.34)`
|
||||
because of `sysconf`, locking out distros the binaries run on. A check fails
|
||||
the build if either binary outgrows `LIBC_MIN`.
|
||||
|
||||
7. Output: `debbuild/*.deb`, the binary package and the `-dbgsym` package.
|
||||
Debian gives dbgsym packages a `.deb` extension; only Ubuntu uses `.ddeb`.
|
||||
|
||||
## Post-build verification
|
||||
|
||||
```bash
|
||||
# DEB
|
||||
dpkg-deb -c debbuild/*.deb | grep -E 'systemd|sysusers|tmpfiles'
|
||||
# DEB (one invocation per package: the dbgsym package is a .deb too)
|
||||
for deb in debbuild/*.deb; do dpkg-deb -c "${deb}"; done | grep -E 'systemd|sysusers|tmpfiles'
|
||||
lintian -I debbuild/*.deb
|
||||
|
||||
# RPM
|
||||
rpm -qlp rpmbuild/RPMS/x86_64/*.rpm
|
||||
|
||||
# Optional, and not in the packaging image: apt-get install -y lintian
|
||||
lintian -I debbuild/*.deb
|
||||
```
|
||||
|
||||
`lintian` still reports `embedded-library zlib`, `no-manual-page` and
|
||||
`initial-upload-closes-no-bugs`; only the `/usr/local` tags are overridden.
|
||||
|
||||
## Reproducibility
|
||||
|
||||
`build_pkg.sh` already defaults `SOURCE_DATE_EPOCH` to the latest git commit
|
||||
time, or the current time outside a git tree, and exports it (override with
|
||||
`--source-date-epoch` / `SOURCE_DATE_EPOCH`); the RPM spec clamps file
|
||||
modification times to it via `%build_mtime_policy`. The remaining variables
|
||||
below further improve reproducibility but are _not_ set by the script — export
|
||||
them yourself if needed:
|
||||
Both formats build reproducibly as they are: the same binaries at the same
|
||||
commit give byte-identical packages on a rebuild, and nothing has to be
|
||||
exported by hand.
|
||||
|
||||
```bash
|
||||
export TZ=UTC
|
||||
export LC_ALL=C.UTF-8
|
||||
export GZIP=-n
|
||||
export DEB_BUILD_OPTIONS="noautodbgsym reproducible=+fixfilepath"
|
||||
```
|
||||
`build_pkg.py` sets `SOURCE_DATE_EPOCH` from the latest git commit time.
|
||||
`dpkg-buildpackage` honours it on its own; the RPM spec sets three macros:
|
||||
|
||||
- `%clamp_mtime_to_source_date_epoch` — file modification times, from
|
||||
`SOURCE_DATE_EPOCH`.
|
||||
- `%use_source_date_epoch_as_buildtime` — the `BUILDTIME` header, from the
|
||||
same.
|
||||
- `%_buildhost` — pinned, so the builder's hostname stays out of the header.
|
||||
|
||||
270
package/build_pkg.py
Executable file
270
package/build_pkg.py
Executable file
@@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build an RPM or Debian package from the pre-built xrpld and validator-keys binaries.
|
||||
|
||||
The build tool for the chosen format has to be on PATH, so this runs in the
|
||||
vanilla distro image that matches it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import textwrap
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
# This script lives in the repository it packages.
|
||||
SRC_DIR = Path(__file__).resolve().parents[1]
|
||||
|
||||
PRE_RELEASE = re.compile(r"^(b|rc)(0|[1-9][0-9]*)(\+.*)?$")
|
||||
|
||||
# Files both packaging systems consume, staged under the same names.
|
||||
STAGED_FROM_BUILD = ("xrpld", "validator-keys", "validator-keys-LICENSE")
|
||||
STAGED_FROM_SRC = {
|
||||
"cfg/xrpld-example.cfg": "xrpld.cfg",
|
||||
"cfg/validators-example.txt": "validators.txt",
|
||||
"LICENSE.md": "LICENSE.md",
|
||||
"README.md": "README.md",
|
||||
}
|
||||
STAGED_UNITS = ("xrpld.service", "xrpld.sysusers", "xrpld.tmpfiles", "xrpld.logrotate")
|
||||
|
||||
|
||||
def run(*command: object, cwd: Path | None = None) -> None:
|
||||
"""Echo a command and run it."""
|
||||
argv = [str(part) for part in command]
|
||||
print("+ " + " ".join(argv), flush=True)
|
||||
subprocess.run(argv, check=True, cwd=cwd)
|
||||
|
||||
|
||||
def capture(*command: object) -> str:
|
||||
"""Run a command and return its stdout, stripped."""
|
||||
argv = [str(part) for part in command]
|
||||
# stderr is left alone so a failing command explains itself.
|
||||
return subprocess.run(
|
||||
argv, stdout=subprocess.PIPE, text=True, check=True
|
||||
).stdout.strip()
|
||||
|
||||
|
||||
def package_version(reported: str) -> str:
|
||||
"""Normalise a reported version into one the package formats accept.
|
||||
|
||||
A pre-release switches to '~' (3.2.0-b1 -> 3.2.0~b1), which also sorts before
|
||||
the final 3.2.0; a no-op for a final release.
|
||||
"""
|
||||
base, _, pre_release = reported.partition("-")
|
||||
version = f"{base}~{pre_release}" if pre_release else base
|
||||
|
||||
# BuildInfo already SemVer-validates the version. Packaging adds one narrower
|
||||
# constraint: after normalisation the version must not contain '-', because
|
||||
# RPM forbids it in Version and Debian reads it as the revision separator.
|
||||
assert "-" not in version, (
|
||||
f"unsupported version {reported!r}: {version!r} cannot contain '-'. "
|
||||
"Use a single-token pre-release like 3.2.0-b1 or 3.2.0-rc2."
|
||||
)
|
||||
assert pre_release or "+" not in reported, (
|
||||
f"unsupported version {reported!r}: "
|
||||
"build metadata is only supported on bN/rcN pre-releases."
|
||||
)
|
||||
assert not pre_release or PRE_RELEASE.match(pre_release), (
|
||||
f"unsupported pre-release {pre_release!r}: use bN or rcN, "
|
||||
"e.g. 3.2.0-b1 or 3.2.0-rc2."
|
||||
)
|
||||
return version
|
||||
|
||||
|
||||
def read_version(xrpld: Path) -> str:
|
||||
"""Read the version from the binary that is about to be packaged."""
|
||||
fields = capture(xrpld, "--version").partition("\n")[0].split()
|
||||
assert len(fields) >= 3, f"cannot read a version from {xrpld} --version"
|
||||
return fields[2]
|
||||
|
||||
|
||||
def check_binaries(build_dir: Path) -> None:
|
||||
"""Fail unless the binaries and their notices are present and runnable."""
|
||||
missing = [
|
||||
name
|
||||
for name in ("xrpld", "validator-keys")
|
||||
if not os.access(build_dir / name, os.X_OK)
|
||||
]
|
||||
assert not missing, (
|
||||
f"missing or not executable in {build_dir}: {' '.join(missing)}. "
|
||||
"Both binaries come from a single CMake build directory configured with "
|
||||
"-Dxrpld=ON -Dvalidator_keys=ON."
|
||||
)
|
||||
|
||||
# No package goes out without the attribution.
|
||||
notice = build_dir / "validator-keys-LICENSE"
|
||||
assert notice.is_file(), (
|
||||
f"missing {notice}. cmake/XrplValidatorKeys.cmake copies it out of the "
|
||||
"fetched validator-keys-tool source, so reconfigure with -Dvalidator_keys=ON."
|
||||
)
|
||||
|
||||
# Catches a binary still pointing at the Nix store's ELF loader, since
|
||||
# packaging runs in a vanilla distro container.
|
||||
capture(build_dir / "validator-keys", "--version")
|
||||
|
||||
|
||||
def source_date_epoch() -> int:
|
||||
"""The last commit's timestamp."""
|
||||
# git refuses to read a checkout owned by another user, which is what a CI
|
||||
# container or a bind mount hands it.
|
||||
return int(
|
||||
capture(
|
||||
"git",
|
||||
"-c",
|
||||
f"safe.directory={SRC_DIR}",
|
||||
"-C",
|
||||
SRC_DIR,
|
||||
"log",
|
||||
"-1",
|
||||
"--format=%ct",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def stage_common(build_dir: Path, dest: Path) -> None:
|
||||
"""Copy everything both packaging systems consume into dest."""
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for name in STAGED_FROM_BUILD:
|
||||
shutil.copy2(build_dir / name, dest / name)
|
||||
for source, name in STAGED_FROM_SRC.items():
|
||||
shutil.copy2(SRC_DIR / source, dest / name)
|
||||
|
||||
|
||||
def stage_units(dest: Path) -> None:
|
||||
"""Copy the systemd, sysusers, tmpfiles and logrotate files into dest.
|
||||
|
||||
Each format wants them somewhere else: rpmbuild reads them from SOURCES,
|
||||
debhelper from debian/.
|
||||
"""
|
||||
for name in STAGED_UNITS:
|
||||
shutil.copy2(SRC_DIR / "package" / "shared" / name, dest / name)
|
||||
|
||||
|
||||
def build_rpm(build_dir: Path, *, version: str, pkg_release: str) -> None:
|
||||
"""Stage the spec and its sources, then build the binary RPMs."""
|
||||
topdir = build_dir / "rpmbuild"
|
||||
for name in ("BUILD", "BUILDROOT", "RPMS", "SOURCES", "SPECS", "SRPMS"):
|
||||
(topdir / name).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
spec = topdir / "SPECS" / "xrpld.spec"
|
||||
shutil.copy2(SRC_DIR / "package" / "rpm" / "xrpld.spec", spec)
|
||||
stage_common(build_dir, topdir / "SOURCES")
|
||||
stage_units(topdir / "SOURCES")
|
||||
|
||||
run(
|
||||
"rpmbuild",
|
||||
"-bb",
|
||||
"--define",
|
||||
f"_topdir {topdir}",
|
||||
"--define",
|
||||
f"pkg_version {version}",
|
||||
"--define",
|
||||
f"pkg_release {pkg_release}",
|
||||
# The image tracks the newest distro, but the packages target el9.
|
||||
"--define",
|
||||
"dist .el9",
|
||||
spec,
|
||||
)
|
||||
|
||||
|
||||
def build_deb(
|
||||
build_dir: Path,
|
||||
*,
|
||||
version: str,
|
||||
reported: str,
|
||||
pkg_release: str,
|
||||
channel: str,
|
||||
epoch: int,
|
||||
) -> None:
|
||||
"""Stage the debian directory and its sources, then build the binary DEBs."""
|
||||
staging = build_dir / "debbuild" / "source"
|
||||
stage_common(build_dir, staging)
|
||||
shutil.copytree(SRC_DIR / "package" / "debian", staging / "debian")
|
||||
|
||||
# debhelper picks these up from debian/ automatically.
|
||||
stage_units(staging / "debian")
|
||||
|
||||
date = datetime.fromtimestamp(epoch, timezone.utc).strftime(
|
||||
"%a, %d %b %Y %H:%M:%S %z"
|
||||
)
|
||||
# The leading spaces are significant to dpkg.
|
||||
changelog = textwrap.dedent(f"""\
|
||||
xrpld ({version}-{pkg_release}) {channel}; urgency=medium
|
||||
* Release {reported}.
|
||||
|
||||
-- XRPL Foundation <contact@xrplf.org> {date}
|
||||
""")
|
||||
(staging / "debian" / "changelog").write_text(changelog)
|
||||
|
||||
run("dpkg-buildpackage", "-b", "--no-sign", "-d", cwd=staging)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--package-type",
|
||||
required=True,
|
||||
choices=("deb", "rpm"),
|
||||
help="the package format to build",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--build-dir",
|
||||
type=Path,
|
||||
default=Path("build"),
|
||||
help="directory holding the xrpld and validator-keys binaries (default: %(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pkg-release",
|
||||
default="1",
|
||||
help="package release iteration (default: %(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--channel",
|
||||
required=True,
|
||||
choices=("stable", "rc", "beta", "develop", "private", "UNRELEASED"),
|
||||
help="release channel, written to debian/changelog",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
package_type: str = args.package_type
|
||||
build_dir: Path = args.build_dir.resolve()
|
||||
pkg_release: str = args.pkg_release
|
||||
channel: str = args.channel
|
||||
|
||||
assert build_dir.is_dir(), (
|
||||
f"build directory not found: {build_dir}. Build the binaries before "
|
||||
"packaging, or point --build-dir at the directory holding them."
|
||||
)
|
||||
|
||||
check_binaries(build_dir)
|
||||
reported = read_version(build_dir / "xrpld")
|
||||
version = package_version(reported)
|
||||
epoch = source_date_epoch()
|
||||
|
||||
# rpmbuild and dpkg-buildpackage both honour this for file timestamps.
|
||||
os.environ["SOURCE_DATE_EPOCH"] = str(epoch)
|
||||
|
||||
# Remove both build trees, because a package left from an earlier build would
|
||||
# otherwise be picked up and published alongside this one.
|
||||
for tree in ("debbuild", "rpmbuild"):
|
||||
shutil.rmtree(build_dir / tree, ignore_errors=True)
|
||||
|
||||
if package_type == "deb":
|
||||
build_deb(
|
||||
build_dir,
|
||||
version=version,
|
||||
reported=reported,
|
||||
pkg_release=pkg_release,
|
||||
channel=channel,
|
||||
epoch=epoch,
|
||||
)
|
||||
else:
|
||||
build_rpm(build_dir, version=version, pkg_release=pkg_release)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,252 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Build an RPM or Debian package from the pre-built xrpld and validator-keys
|
||||
# binaries.
|
||||
#
|
||||
# Flags override env vars; env vars override defaults.
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: build_pkg.sh [options]
|
||||
|
||||
Options (each can also be set via the env var shown):
|
||||
--src-dir DIR repo root [SRC_DIR; default: ${PWD}]
|
||||
--build-dir DIR directory holding the
|
||||
xrpld and validator-keys
|
||||
binaries [BUILD_DIR; default: ${PWD}/build]
|
||||
--pkg-release N package release iteration [PKG_RELEASE; default: 1]
|
||||
--channel NAME release channel, written
|
||||
to debian/changelog [PKG_CHANNEL; default: unstable]
|
||||
--source-date-epoch SECS reproducibility timestamp [SOURCE_DATE_EPOCH; latest git ctime; fallback: current time]
|
||||
-h, --help show this help and exit
|
||||
EOF
|
||||
}
|
||||
|
||||
need_arg() {
|
||||
if [[ $# -lt 2 || "$2" == --* ]]; then
|
||||
echo "Missing value for $1" >&2
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
# Seed from env. CLI parsing below overrides these directly.
|
||||
SRC_DIR="${SRC_DIR:-}"
|
||||
BUILD_DIR="${BUILD_DIR:-}"
|
||||
PKG_RELEASE="${PKG_RELEASE:-1}"
|
||||
PKG_CHANNEL="${PKG_CHANNEL:-unstable}"
|
||||
SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--src-dir)
|
||||
need_arg "$@"
|
||||
SRC_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--build-dir)
|
||||
need_arg "$@"
|
||||
BUILD_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--pkg-release)
|
||||
need_arg "$@"
|
||||
PKG_RELEASE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--channel)
|
||||
need_arg "$@"
|
||||
PKG_CHANNEL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--source-date-epoch)
|
||||
need_arg "$@"
|
||||
SOURCE_DATE_EPOCH="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
SRC_DIR="$(cd "${SRC_DIR:-${PWD}}" && pwd)"
|
||||
BUILD_DIR="${BUILD_DIR:-${PWD}/build}"
|
||||
if [[ ! -d "${BUILD_DIR}" ]]; then
|
||||
echo "build_pkg.sh: build directory not found: ${BUILD_DIR}" >&2
|
||||
echo "Build the binaries before packaging, or set BUILD_DIR to the directory containing them." >&2
|
||||
exit 1
|
||||
fi
|
||||
BUILD_DIR="$(cd "${BUILD_DIR}" && pwd)"
|
||||
|
||||
xrpld_binary="${BUILD_DIR}/xrpld"
|
||||
validator_keys_binary="${BUILD_DIR}/validator-keys"
|
||||
|
||||
# Report both binaries at once: they share a single BUILD_DIR, so telling the
|
||||
# reader to point it at one of them in isolation is advice they cannot follow.
|
||||
missing=()
|
||||
[[ -x "${xrpld_binary}" ]] || missing+=(xrpld)
|
||||
[[ -x "${validator_keys_binary}" ]] || missing+=(validator-keys)
|
||||
|
||||
if [[ ${#missing[@]} -gt 0 ]]; then
|
||||
echo "build_pkg.sh: missing or not executable in ${BUILD_DIR}: ${missing[*]}" >&2
|
||||
echo "Both binaries come from a single CMake build directory configured with" >&2
|
||||
echo "-Dxrpld=ON -Dvalidator_keys=ON. Build them, then point BUILD_DIR at that" >&2
|
||||
echo "directory." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Shipping validator-keys means shipping its notice, so treat it as required
|
||||
# rather than letting a package go out without the attribution.
|
||||
validator_keys_license="${BUILD_DIR}/validator-keys-LICENSE"
|
||||
if [[ ! -f "${validator_keys_license}" ]]; then
|
||||
echo "build_pkg.sh: missing ${validator_keys_license}." >&2
|
||||
echo "cmake/XrplValidatorKeys.cmake copies it out of the fetched" >&2
|
||||
echo "validator-keys-tool source, so reconfigure with -Dvalidator_keys=ON." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The binary must also *run* here. Packaging happens in a vanilla distro
|
||||
# container, so this is what catches a binary still pointing at the Nix store's
|
||||
# ELF loader (see patch_nix_binary in cmake/PatchNixBinary.cmake); xrpld is
|
||||
# covered implicitly by the version query below.
|
||||
if ! "${validator_keys_binary}" --version >/dev/null; then
|
||||
echo "build_pkg.sh: ${validator_keys_binary} exists but does not run here." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
xrpld_version="$("${xrpld_binary}" --version | awk 'NR == 1 { print $3 }')"
|
||||
|
||||
if [[ -z "${xrpld_version}" ]]; then
|
||||
echo "build_pkg.sh: unable to derive xrpld version from ${xrpld_binary} --version." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The version as the package formats consume it: identical to xrpld_version
|
||||
# except a pre-release uses '~' (3.2.0-b1 -> 3.2.0~b1), which also sorts before
|
||||
# the final 3.2.0; a no-op for a final release. Lowercase = derived internally,
|
||||
# not an input (cf. pkg_type).
|
||||
pkg_version="${xrpld_version}"
|
||||
pre_release=""
|
||||
if [[ "${xrpld_version}" == *-* ]]; then
|
||||
pre_release="${xrpld_version#*-}"
|
||||
pkg_version="${xrpld_version%%-*}~${pre_release}"
|
||||
fi
|
||||
|
||||
# BuildInfo already SemVer-validates the binary's version. Packaging adds one
|
||||
# narrower constraint: after pre-release normalization, the package version must
|
||||
# not contain '-' because RPM forbids it in Version and Debian uses it as the
|
||||
# upstream/revision separator.
|
||||
if [[ "${pkg_version}" == *-* ]]; then
|
||||
echo "build_pkg.sh: unsupported xrpld version '${xrpld_version}'." >&2
|
||||
echo "Package version '${pkg_version}' cannot contain '-'." >&2
|
||||
echo "Use a single-token pre-release like 3.2.0-b1 or 3.2.0-rc2." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${pre_release}" && "${xrpld_version}" == *+* ]]; then
|
||||
echo "build_pkg.sh: unsupported xrpld version '${xrpld_version}'." >&2
|
||||
echo "Build metadata is only supported on bN/rcN pre-releases." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${pre_release}" && ! "${pre_release}" =~ ^(b0|b[1-9][0-9]*|rc[0-9]+)(\+.*)?$ ]]; then
|
||||
echo "build_pkg.sh: unsupported xrpld pre-release '${pre_release}'." >&2
|
||||
echo "Use bN or rcN, e.g. 3.2.0-b1 or 3.2.0-rc2." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
pkg_type=deb
|
||||
elif command -v dnf >/dev/null 2>&1 || command -v yum >/dev/null 2>&1; then
|
||||
pkg_type=rpm
|
||||
else
|
||||
echo "Cannot infer pkg_type: no apt-get, dnf, or yum on PATH." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${SOURCE_DATE_EPOCH}" ]]; then
|
||||
if git -C "${SRC_DIR}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
SOURCE_DATE_EPOCH="$(git -C "${SRC_DIR}" log -1 --format=%ct)"
|
||||
else
|
||||
SOURCE_DATE_EPOCH="$(date +%s)"
|
||||
fi
|
||||
fi
|
||||
|
||||
export SOURCE_DATE_EPOCH
|
||||
CHANGELOG_DATE="$(date -u -R -d "@${SOURCE_DATE_EPOCH}")"
|
||||
|
||||
SHARED="${SRC_DIR}/package/shared"
|
||||
DEBIAN_DIR="${SRC_DIR}/package/debian"
|
||||
|
||||
# Stage files that both packaging systems consume using the same filenames.
|
||||
stage_common() {
|
||||
local dest="$1"
|
||||
mkdir -p "${dest}"
|
||||
|
||||
cp "${xrpld_binary}" "${dest}/xrpld"
|
||||
cp "${validator_keys_binary}" "${dest}/validator-keys"
|
||||
cp "${validator_keys_license}" "${dest}/validator-keys-LICENSE"
|
||||
cp "${SRC_DIR}/cfg/xrpld-example.cfg" "${dest}/xrpld.cfg"
|
||||
cp "${SRC_DIR}/cfg/validators-example.txt" "${dest}/validators.txt"
|
||||
cp "${SRC_DIR}/LICENSE.md" "${dest}/LICENSE.md"
|
||||
cp "${SRC_DIR}/README.md" "${dest}/README.md"
|
||||
|
||||
cp "${SHARED}/xrpld.service" "${dest}/xrpld.service"
|
||||
cp "${SHARED}/xrpld.sysusers" "${dest}/xrpld.sysusers"
|
||||
cp "${SHARED}/xrpld.tmpfiles" "${dest}/xrpld.tmpfiles"
|
||||
cp "${SHARED}/xrpld.logrotate" "${dest}/xrpld.logrotate"
|
||||
}
|
||||
|
||||
build_rpm() {
|
||||
local topdir="${BUILD_DIR}/rpmbuild"
|
||||
mkdir -p "${topdir}"/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
||||
|
||||
cp "${SRC_DIR}/package/rpm/xrpld.spec" "${topdir}/SPECS/xrpld.spec"
|
||||
stage_common "${topdir}/SOURCES"
|
||||
|
||||
set -x
|
||||
rpmbuild -bb \
|
||||
--define "_topdir ${topdir}" \
|
||||
--define "pkg_version ${pkg_version}" \
|
||||
--define "pkg_release ${PKG_RELEASE}" \
|
||||
"${topdir}/SPECS/xrpld.spec"
|
||||
}
|
||||
|
||||
build_deb() {
|
||||
local staging="${BUILD_DIR}/debbuild/source"
|
||||
mkdir -p "${staging}"
|
||||
|
||||
stage_common "${staging}"
|
||||
cp -r "${DEBIAN_DIR}" "${staging}/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"
|
||||
|
||||
# Debian version is <upstream>[~<pre>]-<pkg release>.
|
||||
cat >"${staging}/debian/changelog" <<EOF
|
||||
xrpld (${pkg_version}-${PKG_RELEASE}) ${PKG_CHANNEL}; urgency=medium
|
||||
* Release ${xrpld_version}.
|
||||
|
||||
-- XRPL Foundation <contact@xrplf.org> ${CHANGELOG_DATE}
|
||||
EOF
|
||||
|
||||
chmod +x "${staging}/debian/rules"
|
||||
|
||||
set -x
|
||||
(cd "${staging}" && dpkg-buildpackage -b --no-sign -d)
|
||||
}
|
||||
|
||||
# Remove both build directories, because a package left from an earlier build
|
||||
# would otherwise be picked up and published alongside this one.
|
||||
rm -rf "${BUILD_DIR}/debbuild" "${BUILD_DIR}/rpmbuild"
|
||||
|
||||
"build_${pkg_type}"
|
||||
@@ -4,6 +4,7 @@ Priority: optional
|
||||
Maintainer: XRPL Foundation <contact@xrplf.org>
|
||||
Rules-Requires-Root: no
|
||||
Build-Depends:
|
||||
binutils,
|
||||
debhelper-compat (= 13)
|
||||
Standards-Version: 4.7.0
|
||||
Homepage: https://github.com/XRPLF/rippled
|
||||
@@ -11,8 +12,6 @@ Vcs-Git: https://github.com/XRPLF/rippled.git
|
||||
Vcs-Browser: https://github.com/XRPLF/rippled
|
||||
|
||||
Package: xrpld
|
||||
Section: net
|
||||
Priority: optional
|
||||
Architecture: any
|
||||
Depends:
|
||||
${shlibs:Depends},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: rippled
|
||||
Upstream-Name: xrpld
|
||||
Source: https://github.com/XRPLF/rippled
|
||||
|
||||
Files: *
|
||||
@@ -15,7 +15,7 @@ Copyright: 2016, Ripple Labs Inc.
|
||||
2009-2010, Satoshi Nakamoto
|
||||
2011, The Bitcoin developers
|
||||
2003-2005, Tom Wu
|
||||
License: ISC
|
||||
License: ISC and BSL-1.0 and MIT and Tom-Wu
|
||||
Comment: Built from https://github.com/ripple/validator-keys-tool at the commit
|
||||
pinned in cmake/XrplValidatorKeys.cmake. Besides ISC-licensed code it
|
||||
incorporates work under the Boost Software License 1.0 (ASIO), the MIT/X11
|
||||
@@ -35,3 +35,74 @@ License: ISC
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
License: BSL-1.0
|
||||
Boost Software License - Version 1.0 - August 17th, 2003
|
||||
.
|
||||
Permission is hereby granted, free of charge, to any person or organization
|
||||
obtaining a copy of the software and accompanying documentation covered by
|
||||
this license (the "Software") to use, reproduce, display, distribute,
|
||||
execute, and transmit the Software, and to prepare derivative works of the
|
||||
Software, and to permit third-parties to whom the Software is furnished to
|
||||
do so, all subject to the following:
|
||||
.
|
||||
The copyright notices in the Software and this entire statement, including
|
||||
the above license grant, this restriction and the following disclaimer,
|
||||
must be included in all copies of the Software, in whole or in part, and
|
||||
all derivative works of the Software, unless such copies or derivative
|
||||
works are solely in the form of machine-executable object code generated by
|
||||
a source language processor.
|
||||
.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
License: MIT
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
.
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
License: Tom-Wu
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
.
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
.
|
||||
THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY
|
||||
WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
|
||||
.
|
||||
IN NO EVENT SHALL TOM WU BE LIABLE FOR ANY SPECIAL, INCIDENTAL,
|
||||
INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER
|
||||
RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER OR NOT ADVISED OF
|
||||
THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF LIABILITY, ARISING OUT
|
||||
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
.
|
||||
In addition, the following condition applies:
|
||||
.
|
||||
All redistributions must retain an intact copy of this copyright notice
|
||||
and disclaimer.
|
||||
|
||||
26
package/debian/rules
Normal file → Executable file
26
package/debian/rules
Normal file → Executable file
@@ -2,6 +2,12 @@
|
||||
|
||||
export DH_VERBOSE = 1
|
||||
|
||||
# The glibc the Nix toolchain builds against, and so the real floor for the
|
||||
# binaries. dpkg-shlibdeps would instead derive libc6 (>= 2.34) from the build
|
||||
# host's symbols file, where sysconf carries that minver, locking out distros
|
||||
# the binaries actually run on.
|
||||
LIBC_MIN = 2.31
|
||||
|
||||
%:
|
||||
dh $@
|
||||
|
||||
@@ -11,6 +17,8 @@ override_dh_auto_configure override_dh_auto_build override_dh_auto_test:
|
||||
override_dh_installsystemd:
|
||||
dh_installsystemd --no-stop-on-upgrade xrpld.service
|
||||
|
||||
# The tmpfiles snippet sets ownership to the xrpld user, so the sysusers snippet
|
||||
# has to be emitted first: run it early and make its own sequence slot a no-op.
|
||||
execute_before_dh_installtmpfiles:
|
||||
dh_installsysusers
|
||||
|
||||
@@ -22,5 +30,23 @@ override_dh_install:
|
||||
install -D -m 0644 xrpld.cfg debian/xrpld/etc/xrpld/xrpld.cfg
|
||||
install -D -m 0644 validators.txt debian/xrpld/etc/xrpld/validators.txt
|
||||
|
||||
override_dh_shlibdeps:
|
||||
dh_shlibdeps
|
||||
# Guards against the toolchain moving past LIBC_MIN and the packages then
|
||||
# claiming a floor they do not meet.
|
||||
for binary in xrpld validator-keys; do \
|
||||
needed=$$(readelf --dyn-syms --wide $$binary \
|
||||
| grep -o 'GLIBC_[0-9.]*' | sed 's/GLIBC_//' | sort -uV | tail -1); \
|
||||
if [ -z "$$needed" ]; then \
|
||||
echo "$$binary: no GLIBC_ symbol versions read, cannot check LIBC_MIN" >&2; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
if dpkg --compare-versions "$$needed" gt "$(LIBC_MIN)"; then \
|
||||
echo "$$binary needs glibc $$needed, above LIBC_MIN $(LIBC_MIN)" >&2; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
done
|
||||
sed -i 's/libc6 (>= [0-9.]*)/libc6 (>= $(LIBC_MIN))/' debian/xrpld.substvars
|
||||
|
||||
override_dh_dwz:
|
||||
@:
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
README.md
|
||||
LICENSE.md
|
||||
validator-keys-LICENSE
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
# Legacy compat symlinks (remove next major release)
|
||||
# Legacy compatibility for pre-FHS package layouts.
|
||||
# TODO: remove after rippled fully deprecated.
|
||||
usr/bin/xrpld usr/local/bin/rippled
|
||||
|
||||
6
package/debian/xrpld.lintian-overrides
Normal file
6
package/debian/xrpld.lintian-overrides
Normal file
@@ -0,0 +1,6 @@
|
||||
# The /usr/local/bin/rippled symlink is deliberate compatibility for pre-FHS
|
||||
# layouts, so the Policy 9.1.2 tags it raises are expected.
|
||||
# TODO: remove alongside debian/xrpld.links after rippled fully deprecated.
|
||||
xrpld: dir-in-usr-local [usr/local/bin/]
|
||||
xrpld: file-in-usr-local [usr/local/bin/rippled]
|
||||
xrpld: file-in-unusual-dir [usr/local/bin/rippled]
|
||||
10
package/docker/Dockerfile
Normal file
10
package/docker/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
ARG BASE_IMAGE=debian:trixie
|
||||
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
# Bind-mounted rather than copied in, so the installer never lands in a layer.
|
||||
RUN --mount=type=bind,source=bin/install-packaging-tools.sh,target=/install-packaging-tools.sh \
|
||||
/install-packaging-tools.sh
|
||||
|
||||
# See ../README.md, "Publishing from other repositories".
|
||||
COPY package/docker/publish_pkg.py /usr/local/bin/publish_pkg.py
|
||||
162
package/docker/publish_pkg.py
Executable file
162
package/docker/publish_pkg.py
Executable file
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Publish built DEB and RPM packages to the XRPLF repositories on Nexus.
|
||||
|
||||
Knows nothing about what it uploads beyond the channel, so it publishes whatever
|
||||
built the packages; see package/README.md, "Publishing from other repositories".
|
||||
|
||||
RPMs are uploaded to the hosted repository, but yum clients install from the
|
||||
'rpm-<channel>' group repository in front of it, which serves signed metadata.
|
||||
|
||||
NEXUS_USERNAME and NEXUS_PASSWORD are read from the environment, so the
|
||||
credentials never reach the process list.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import os
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
SUFFIXES = (".deb", ".ddeb", ".rpm")
|
||||
|
||||
# No progress for this long ends an attempt. urlopen applies the timeout per
|
||||
# socket operation, so a stalled transfer fails while a merely slow one carries
|
||||
# on -- the debuginfo package is large enough for that distinction to matter.
|
||||
STALL_TIMEOUT = 300
|
||||
|
||||
ATTEMPTS = 4
|
||||
RETRY_DELAY = 5
|
||||
|
||||
# 429 is Nexus asking to slow down, not a rejection, so it retries like a 5xx.
|
||||
RETRYABLE_STATUSES = (429,)
|
||||
|
||||
|
||||
def build_opener() -> urllib.request.OpenerDirector:
|
||||
"""An opener with no redirect handler, so a 3xx raises instead of being followed.
|
||||
|
||||
A redirected upload is silently downgraded to a GET, turning it into a no-op
|
||||
that still answers 200.
|
||||
"""
|
||||
opener = urllib.request.OpenerDirector()
|
||||
opener.add_handler(urllib.request.HTTPHandler())
|
||||
opener.add_handler(urllib.request.HTTPSHandler())
|
||||
opener.add_handler(urllib.request.HTTPErrorProcessor())
|
||||
opener.add_handler(urllib.request.HTTPDefaultErrorHandler())
|
||||
return opener
|
||||
|
||||
|
||||
def upload(url: str, method: str, headers: dict[str, str], package: Path) -> None:
|
||||
"""Send one package, retrying only what is worth retrying.
|
||||
|
||||
A 4xx other than 429 is a deterministic rejection, so it is reported at once
|
||||
rather than re-sending the whole body three more times. Nexus explains what
|
||||
it rejected in the response body, so that body is always surfaced.
|
||||
"""
|
||||
opener = build_opener()
|
||||
|
||||
for attempt in range(1, ATTEMPTS + 1):
|
||||
try:
|
||||
with package.open("rb") as body:
|
||||
request = urllib.request.Request(
|
||||
url,
|
||||
data=body,
|
||||
method=method,
|
||||
headers={**headers, "Content-Length": str(package.stat().st_size)},
|
||||
)
|
||||
opener.open(request, timeout=STALL_TIMEOUT)
|
||||
return
|
||||
except urllib.error.HTTPError as error:
|
||||
detail = error.read().decode(errors="replace").strip()
|
||||
reason = f"HTTP {error.code}: {detail}"
|
||||
retryable = error.code >= 500 or error.code in RETRYABLE_STATUSES
|
||||
except (urllib.error.URLError, OSError) as error:
|
||||
reason = str(error)
|
||||
retryable = True
|
||||
|
||||
assert (
|
||||
retryable and attempt < ATTEMPTS
|
||||
), f"upload of {package.name} failed: {reason}"
|
||||
print(f" attempt {attempt} failed ({reason}), retrying")
|
||||
time.sleep(RETRY_DELAY)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--channel",
|
||||
required=True,
|
||||
choices=("stable", "rc", "beta", "develop", "private"),
|
||||
help="release channel, selecting the deb-<channel> and rpm-<channel>-hosted repositories",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--package-dir",
|
||||
type=Path,
|
||||
default=Path("build"),
|
||||
help=f"searched recursively for {', '.join(SUFFIXES)} (default: %(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--nexus-url",
|
||||
default="https://packages.xrplf.org",
|
||||
help="the Nexus instance to publish to (default: %(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="list the uploads without performing them",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
channel: str = args.channel
|
||||
package_dir: Path = args.package_dir
|
||||
nexus_url: str = args.nexus_url
|
||||
dry_run: bool = args.dry_run
|
||||
|
||||
nexus = nexus_url.rstrip("/")
|
||||
deb_repo = f"deb-{channel}"
|
||||
rpm_repo = f"rpm-{channel}-hosted"
|
||||
|
||||
auth: dict[str, str] = {}
|
||||
if not dry_run:
|
||||
username = os.environ.get("NEXUS_USERNAME")
|
||||
password = os.environ.get("NEXUS_PASSWORD")
|
||||
assert username and password, "NEXUS_USERNAME and NEXUS_PASSWORD are required"
|
||||
token = base64.b64encode(f"{username}:{password}".encode()).decode()
|
||||
auth = {"Authorization": f"Basic {token}"}
|
||||
|
||||
# Deliberately not shared with sign_rpm.py: this script ships standalone in
|
||||
# the packaging image for other repositories to run.
|
||||
packages = sorted(
|
||||
path
|
||||
for path in package_dir.rglob("*")
|
||||
if path.is_file() and path.suffix in SUFFIXES
|
||||
)
|
||||
# Uploading nothing would otherwise look like a successful publish.
|
||||
assert packages, f"no packages found in {package_dir}"
|
||||
|
||||
print(f"Publishing {package_dir} to {deb_repo} and {rpm_repo} on {nexus}:")
|
||||
for package in packages:
|
||||
if package.suffix == ".rpm":
|
||||
# yum repositories are addressed by path, and the arch comes from
|
||||
# the name, e.g. xrpld-3.4.0-1.el9.x86_64.rpm.
|
||||
destination = f"{rpm_repo}/{package.stem.rsplit('.', 1)[-1]}"
|
||||
url = f"{nexus}/repository/{destination}/{package.name}"
|
||||
method, content_type = "PUT", "application/octet-stream"
|
||||
else:
|
||||
# A raw body with a multipart Content-Type, POSTed to the repository
|
||||
# root, is the documented upload for a hosted apt repository:
|
||||
# https://help.sonatype.com/en/apt-repositories.html#deploying-packages-to-hosted-apt-repositories
|
||||
destination = deb_repo
|
||||
url = f"{nexus}/repository/{destination}/"
|
||||
method, content_type = "POST", "multipart/form-data"
|
||||
|
||||
print(f" {package.name} -> {destination}")
|
||||
if not dry_run:
|
||||
upload(url, method, {"Content-Type": content_type, **auth}, package)
|
||||
|
||||
verb = "would be published" if dry_run else "published"
|
||||
print(f"{len(packages)} package(s) {verb}.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,109 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Publish the DEB and RPM packages built by build_pkg.sh to the XRPLF package
|
||||
# repositories on Sonatype Nexus.
|
||||
#
|
||||
# Usage: publish_pkg.sh <channel> [package-dir]
|
||||
#
|
||||
# channel release channel, selecting the 'deb-<channel>' and
|
||||
# 'rpm-<channel>-hosted' repositories
|
||||
# package-dir searched recursively for *.deb, *.ddeb and *.rpm ('build' by
|
||||
# default)
|
||||
#
|
||||
# RPMs are uploaded to the hosted repository, but yum clients install from the
|
||||
# 'rpm-<channel>' group repository in front of it, which serves signed metadata.
|
||||
#
|
||||
# NEXUS_USERNAME and NEXUS_PASSWORD are required. NEXUS_URL overrides the target
|
||||
# instance, and DRY_RUN=1 lists the uploads without performing them.
|
||||
|
||||
channel="${1:-}"
|
||||
pkg_dir="${2:-build}"
|
||||
nexus_url="${NEXUS_URL:-https://packages.xrplf.org}"
|
||||
|
||||
if [[ -z "${channel}" ]]; then
|
||||
echo "usage: publish_pkg.sh <channel> [package-dir]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
deb_repo="deb-${channel}"
|
||||
rpm_repo="rpm-${channel}-hosted"
|
||||
|
||||
if [[ -z "${DRY_RUN:-}" ]]; then
|
||||
: "${NEXUS_USERNAME:?is required}" "${NEXUS_PASSWORD:?is required}"
|
||||
fi
|
||||
|
||||
# Deliberate curl choices:
|
||||
#
|
||||
# - no --fail, which would hide the response body where Nexus explains what it
|
||||
# rejected
|
||||
# - no --location, since curl downgrades a redirected POST to GET and turns an
|
||||
# upload into a no-op that still answers 200
|
||||
# - credentials on stdin, to keep them out of the process list
|
||||
upload() {
|
||||
local url="$1"
|
||||
shift
|
||||
[[ -z "${DRY_RUN:-}" ]] || return 0
|
||||
|
||||
local body code status=0
|
||||
body="$(mktemp)"
|
||||
code="$(
|
||||
printf 'user = %s:%s\n' "${NEXUS_USERNAME}" "${NEXUS_PASSWORD}" |
|
||||
curl \
|
||||
--config - \
|
||||
--silent \
|
||||
--show-error \
|
||||
--retry 3 \
|
||||
--retry-delay 5 \
|
||||
--retry-all-errors \
|
||||
--output "${body}" \
|
||||
--write-out '%{http_code}' \
|
||||
"$@" \
|
||||
"${url}"
|
||||
)" || status=$?
|
||||
|
||||
if [[ ${status} -ne 0 || ! "${code}" =~ ^2[0-9][0-9]$ ]]; then
|
||||
echo "publish_pkg.sh: upload failed (curl ${status}, HTTP ${code}): ${url}" >&2
|
||||
cat "${body}" >&2
|
||||
echo >&2
|
||||
rm -f "${body}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -f "${body}"
|
||||
}
|
||||
|
||||
echo "Publishing ${pkg_dir} to ${deb_repo} and ${rpm_repo} on ${nexus_url}:"
|
||||
|
||||
count=0
|
||||
while IFS= read -r -d '' file; do
|
||||
name="${file##*/}"
|
||||
case "${name}" in
|
||||
# A raw body with a multipart Content-Type, POSTed to the repository root,
|
||||
# is the documented upload for a hosted apt repository:
|
||||
# https://help.sonatype.com/en/apt-repositories.html#deploying-packages-to-hosted-apt-repositories
|
||||
*.deb | *.ddeb)
|
||||
echo " ${name} -> ${deb_repo}"
|
||||
upload "${nexus_url}/repository/${deb_repo}/" \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--data-binary "@${file}"
|
||||
;;
|
||||
# yum repositories are addressed by path; the arch comes from the name.
|
||||
*.rpm)
|
||||
arch="${name%.rpm}"
|
||||
arch="${arch##*.}"
|
||||
echo " ${name} -> ${rpm_repo}/${arch}"
|
||||
upload "${nexus_url}/repository/${rpm_repo}/${arch}/${name}" \
|
||||
--upload-file "${file}"
|
||||
;;
|
||||
esac
|
||||
count=$((count + 1))
|
||||
done < <(find "${pkg_dir}" -type f \( -name '*.deb' -o -name '*.ddeb' -o -name '*.rpm' \) -print0)
|
||||
|
||||
# Uploading nothing would otherwise look like a successful publish.
|
||||
if [[ ${count} -eq 0 ]]; then
|
||||
echo "publish_pkg.sh: no packages found in ${pkg_dir}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "${count} package(s) ${DRY_RUN:+would be }published."
|
||||
@@ -17,6 +17,11 @@ URL: https://github.com/XRPLF/rippled
|
||||
ExclusiveArch: x86_64 aarch64
|
||||
BuildRequires: systemd-rpm-macros
|
||||
|
||||
# These have to precede %%debug_package: it opens the debuginfo subpackage, and
|
||||
# any tag after it is silently dropped from the main package.
|
||||
%{?systemd_requires}
|
||||
%{?sysusers_requires_compat}
|
||||
|
||||
%undefine _debugsource_packages
|
||||
%debug_package
|
||||
# Level 3 rather than the el9 default of 19: it shrinks the multi-gigabyte
|
||||
@@ -25,10 +30,13 @@ BuildRequires: systemd-rpm-macros
|
||||
%global _binary_payload w3.zstdio
|
||||
%global _find_debuginfo_dwz_opts %{nil}
|
||||
|
||||
%build_mtime_policy clamp_to_source_date_epoch
|
||||
# Reproducibility: the first two take their value from the SOURCE_DATE_EPOCH
|
||||
# build_pkg.py exports. Without these the header records the wall clock and the
|
||||
# build container's hostname, so two builds of the same commit differ.
|
||||
%global clamp_mtime_to_source_date_epoch 1
|
||||
%global use_source_date_epoch_as_buildtime 1
|
||||
%global _buildhost xrplf.org
|
||||
|
||||
%{?systemd_requires}
|
||||
%{?sysusers_requires_compat}
|
||||
|
||||
%description
|
||||
xrpld is the reference implementation of the XRP Ledger protocol. It
|
||||
@@ -53,7 +61,7 @@ install -Dm0644 %{_sourcedir}/validators.txt %{buildroot}%{_sysconfdir}/%{
|
||||
install -Dm0644 %{_sourcedir}/xrpld.service %{buildroot}%{_unitdir}/xrpld.service
|
||||
install -Dm0644 %{_sourcedir}/xrpld.sysusers %{buildroot}%{_sysusersdir}/xrpld.conf
|
||||
install -Dm0644 %{_sourcedir}/xrpld.tmpfiles %{buildroot}%{_tmpfilesdir}/xrpld.conf
|
||||
install -Dm0644 /dev/null %{buildroot}%{_presetdir}/50-xrpld.preset
|
||||
install -d %{buildroot}%{_presetdir}
|
||||
cat >%{buildroot}%{_presetdir}/50-xrpld.preset <<'EOF'
|
||||
enable xrpld.service
|
||||
EOF
|
||||
@@ -76,7 +84,7 @@ ln -s %{_bindir}/%{name} %{buildroot}/usr/local/bin/rippled
|
||||
%sysusers_create_package %{name} %{_sourcedir}/xrpld.sysusers
|
||||
|
||||
%post
|
||||
systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || :
|
||||
%tmpfiles_create_package %{name} %{_sourcedir}/xrpld.tmpfiles
|
||||
%systemd_post xrpld.service
|
||||
|
||||
%preun
|
||||
@@ -86,11 +94,12 @@ systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || :
|
||||
%systemd_postun xrpld.service
|
||||
|
||||
%files
|
||||
%attr(0755,root,root) %dir %{_docdir}/%{name}
|
||||
%license %{_docdir}/%{name}/LICENSE.md
|
||||
%license %{_docdir}/%{name}/validator-keys-LICENSE
|
||||
%doc %{_docdir}/%{name}/README.md
|
||||
|
||||
%dir %{_sysconfdir}/%{name}
|
||||
%attr(0755,root,root) %dir %{_sysconfdir}/%{name}
|
||||
|
||||
%{_bindir}/%{name}
|
||||
%{_bindir}/validator-keys
|
||||
@@ -101,7 +110,7 @@ systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || :
|
||||
|
||||
|
||||
%{_unitdir}/xrpld.service
|
||||
%{_presetdir}/50-xrpld.preset
|
||||
%attr(0644,root,root) %{_presetdir}/50-xrpld.preset
|
||||
%{_sysusersdir}/xrpld.conf
|
||||
%{_tmpfilesdir}/xrpld.conf
|
||||
%ghost %dir /var/lib/xrpld
|
||||
|
||||
@@ -17,6 +17,8 @@ ProtectHome=true
|
||||
PrivateTmp=true
|
||||
User=xrpld
|
||||
Group=xrpld
|
||||
# xrpld.tmpfiles creates these at install and boot; these recreate them on
|
||||
# every start, so a removed directory does not stop the service.
|
||||
StateDirectory=xrpld
|
||||
StateDirectoryMode=0750
|
||||
LogsDirectory=xrpld
|
||||
|
||||
130
package/sign_rpm.py
Executable file
130
package/sign_rpm.py
Executable file
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Sign the RPMs built by build_pkg.py.
|
||||
|
||||
Nexus signs the yum repository metadata (via the 'rpm-<channel>' group
|
||||
repository), but never the packages themselves, so they carry their own
|
||||
signature. Clients verify the packages with gpgcheck=1 and the metadata with
|
||||
repo_gpgcheck=1.
|
||||
|
||||
The DEBs are deliberately not signed: embedded DEB signatures exist (debsigs),
|
||||
but apt does not verify them by default and trusts the repository metadata,
|
||||
which Nexus signs, instead.
|
||||
|
||||
PKG_SIGNING_KEY is read from the environment, so the key never reaches the
|
||||
process list.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
# An RSA signature lands in the RSAHEADER tag, a DSA or EdDSA one in DSAHEADER,
|
||||
# so both are queried; checking only the first would reject a signed package.
|
||||
SIGNATURE_QUERY = "%{RSAHEADER:pgpsig}%{DSAHEADER:pgpsig}"
|
||||
UNSIGNED = "(none)(none)"
|
||||
|
||||
|
||||
def gpg(gnupghome: Path, *args: str, stdin: str | None = None) -> str:
|
||||
"""Run gpg against a throwaway keyring and return its stdout."""
|
||||
return subprocess.run(
|
||||
["gpg", "--batch", "--quiet", *args],
|
||||
input=stdin,
|
||||
# stderr is left alone so a failing gpg explains itself.
|
||||
stdout=subprocess.PIPE,
|
||||
text=True,
|
||||
check=True,
|
||||
env={**os.environ, "GNUPGHOME": str(gnupghome)},
|
||||
).stdout
|
||||
|
||||
|
||||
def import_key(gnupghome: Path, key: str) -> str:
|
||||
"""Import the armoured private key and return its fingerprint."""
|
||||
gpg(gnupghome, "--import", stdin=key)
|
||||
|
||||
records = [
|
||||
line.split(":")
|
||||
for line in gpg(gnupghome, "--list-secret-keys", "--with-colons").splitlines()
|
||||
]
|
||||
# Exactly one, so the fingerprint picked below is not a guess.
|
||||
secrets = [record for record in records if record[0] == "sec"]
|
||||
assert (
|
||||
len(secrets) == 1
|
||||
), f"PKG_SIGNING_KEY must hold exactly one secret key, found {len(secrets)}"
|
||||
|
||||
# The first fingerprint belongs to the primary key; subkeys follow.
|
||||
fingerprints = [record[9] for record in records if record[0] == "fpr"]
|
||||
assert fingerprints, "PKG_SIGNING_KEY holds a secret key with no fingerprint"
|
||||
return fingerprints[0]
|
||||
|
||||
|
||||
def sign(gnupghome: Path, rpms: list[Path], fingerprint: str) -> None:
|
||||
"""Attach a signature to every RPM in one rpmsign invocation."""
|
||||
subprocess.run(
|
||||
[
|
||||
"rpmsign",
|
||||
"--define",
|
||||
f"_gpg_name {fingerprint}",
|
||||
# Loopback pinentry: the key is unattended, so there is no tty to
|
||||
# prompt on.
|
||||
"--define",
|
||||
"_gpg_sign_cmd_extra_args --pinentry-mode loopback --batch --yes",
|
||||
"--addsign",
|
||||
*(str(rpm) for rpm in rpms),
|
||||
],
|
||||
check=True,
|
||||
env={**os.environ, "GNUPGHOME": str(gnupghome)},
|
||||
)
|
||||
|
||||
|
||||
def verify(rpms: list[Path]) -> None:
|
||||
"""Fail unless every RPM now carries a signature.
|
||||
|
||||
rpmsign can exit 0 having attached nothing, and an unsigned package is only
|
||||
rejected later, on the installing machine.
|
||||
"""
|
||||
for rpm in rpms:
|
||||
signature = subprocess.run(
|
||||
["rpm", "--query", "--queryformat", SIGNATURE_QUERY, "--package", str(rpm)],
|
||||
stdout=subprocess.PIPE,
|
||||
text=True,
|
||||
check=True,
|
||||
).stdout.strip()
|
||||
assert signature != UNSIGNED, f"{rpm} is unsigned after rpmsign"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--package-dir",
|
||||
type=Path,
|
||||
default=Path("build"),
|
||||
help="searched recursively for *.rpm (default: %(default)s)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
package_dir: Path = args.package_dir
|
||||
|
||||
# Deliberately not shared with publish_pkg.py, which ships standalone in the
|
||||
# packaging image.
|
||||
rpms = sorted(path for path in package_dir.rglob("*.rpm") if path.is_file())
|
||||
# Signing nothing would otherwise look like a successful signing.
|
||||
assert rpms, f"no RPMs found in {package_dir}"
|
||||
|
||||
key = os.environ.get("PKG_SIGNING_KEY")
|
||||
assert key, "PKG_SIGNING_KEY is required"
|
||||
|
||||
# The keyring holds an unencrypted private key, so it goes even if signing
|
||||
# fails.
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
gnupghome = Path(tmp)
|
||||
fingerprint = import_key(gnupghome, key)
|
||||
print(f"Signing {len(rpms)} RPM(s) with {fingerprint}.")
|
||||
sign(gnupghome, rpms, fingerprint)
|
||||
verify(rpms)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,67 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Sign the RPMs built by build_pkg.sh. Nexus signs the yum repository metadata
|
||||
# (via the 'rpm-<channel>' group repository), but never the packages themselves,
|
||||
# so they carry their own signature. Clients verify the packages with gpgcheck=1
|
||||
# and the metadata with repo_gpgcheck=1.
|
||||
#
|
||||
# Usage: sign_rpm.sh [package-dir]
|
||||
#
|
||||
# package-dir searched recursively for *.rpm ('build' by default)
|
||||
#
|
||||
# PKG_SIGNING_KEY must hold an armoured PGP private key. It has no flag, to keep
|
||||
# the key out of the process list.
|
||||
#
|
||||
# The DEBs are deliberately not signed: embedded DEB signatures exist (debsigs),
|
||||
# but apt does not verify them by default and trusts the repository metadata,
|
||||
# which Nexus signs, instead.
|
||||
|
||||
pkg_dir="${1:-build}"
|
||||
|
||||
mapfile -d '' rpms < <(find "${pkg_dir}" -type f -name '*.rpm' -print0)
|
||||
|
||||
# Signing nothing would otherwise look like a successful signing.
|
||||
if [[ ${#rpms[@]} -eq 0 ]]; then
|
||||
echo "sign_rpm.sh: no RPMs found in ${pkg_dir}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
: "${PKG_SIGNING_KEY:?is required}"
|
||||
|
||||
# Global, and expanded by the trap when it fires: the keyring holds an
|
||||
# unencrypted private key, so it must go even if signing fails.
|
||||
signing_home="$(mktemp -d)"
|
||||
trap 'rm -rf "${signing_home}"' EXIT
|
||||
export GNUPGHOME="${signing_home}"
|
||||
|
||||
printf '%s' "${PKG_SIGNING_KEY}" | gpg --batch --quiet --import
|
||||
|
||||
# Exactly one secret key, so that picking the first below is not a guess between
|
||||
# several.
|
||||
secrets="$(gpg --list-secret-keys --with-colons | grep -c '^sec:' || true)"
|
||||
if [[ "${secrets}" -ne 1 ]]; then
|
||||
echo "sign_rpm.sh: PKG_SIGNING_KEY must hold exactly one secret key, found ${secrets}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
key="$(gpg --list-secret-keys --with-colons | awk -F: '/^fpr:/ { print $10; exit }')"
|
||||
echo "Signing ${#rpms[@]} RPM(s) with ${key}."
|
||||
|
||||
# Loopback pinentry: the key is unattended, so there is no tty to prompt on.
|
||||
rpmsign \
|
||||
--define "_gpg_name ${key}" \
|
||||
--define "_gpg_sign_cmd_extra_args --pinentry-mode loopback --batch --yes" \
|
||||
--addsign "${rpms[@]}"
|
||||
|
||||
# rpmsign can exit 0 having attached nothing, and an unsigned package is only
|
||||
# rejected later, on the installing machine. Both header tags are checked
|
||||
# because an RSA signature lands in RSAHEADER and a DSA or EdDSA one in
|
||||
# DSAHEADER.
|
||||
for pkg in "${rpms[@]}"; do
|
||||
signature="$(rpm --query --queryformat '%{RSAHEADER:pgpsig}%{DSAHEADER:pgpsig}' --package "${pkg}")"
|
||||
if [[ "${signature}" == "(none)(none)" ]]; then
|
||||
echo "sign_rpm.sh: ${pkg} is unsigned after rpmsign." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
@@ -1,4 +1,4 @@
|
||||
[toolchain]
|
||||
channel = "1.95"
|
||||
components = ["rustfmt", "clippy", "rust-analyzer", "llvm-tools-preview"]
|
||||
channel = "1.97.1"
|
||||
components = ["rustfmt", "clippy", "rust-analyzer", "llvm-tools-preview", "rust-src"]
|
||||
profile = "minimal"
|
||||
|
||||
@@ -543,12 +543,19 @@ doWithdraw(
|
||||
{
|
||||
auto const dstSle = ctx.view.read(keylet::account(dstAcct));
|
||||
|
||||
// Create trust line or MPToken for the receiving account
|
||||
// Create a trust line or MPToken for a self-destination only when there
|
||||
// is a payout to credit. Post-fixCleanup3_4_0, a zero-value withdraw
|
||||
// (e.g. share redemption from a fully impaired vault) must not insert
|
||||
// an empty holding: that records a one-sided zero delta and can also
|
||||
// create+delete MPTokens in the same transaction.
|
||||
if (dstAcct == senderAcct)
|
||||
{
|
||||
if (auto const ter = addEmptyHolding(ctx, senderAcct, priorBalance, amount.asset(), j);
|
||||
!isTesSuccess(ter) && ter != tecDUPLICATE)
|
||||
return ter;
|
||||
if (amount > beast::kZero || !ctx.view.rules().enabled(fixCleanup3_4_0))
|
||||
{
|
||||
if (auto const ter = addEmptyHolding(ctx, senderAcct, priorBalance, amount.asset(), j);
|
||||
!isTesSuccess(ter) && ter != tecDUPLICATE)
|
||||
return ter;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/Sandbox.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
@@ -691,12 +690,6 @@ deleteAMMTrustLines(
|
||||
|
||||
return {deleteAMMTrustLine(sb, sleItem, ammAccountID, j), SkipEntry::No};
|
||||
}
|
||||
// A credential naming the pseudo-account as subject can't be
|
||||
// accepted or deleted by it and would otherwise permanently pin the
|
||||
// AMM. Clean it up here, inside the same bounded walk, so the
|
||||
// pinned AMM can still be deleted.
|
||||
if (sb.rules().enabled(fixCleanup3_4_0) && nodeType == ltCREDENTIAL)
|
||||
return {credentials::deleteSLE(sb, sleItem, j), SkipEntry::No};
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.error()) << "deleteAMMObjects: deleting non-trustline or non-MPT " << nodeType;
|
||||
return {tecINTERNAL, SkipEntry::No};
|
||||
@@ -774,8 +767,6 @@ deleteAMMAccount(Sandbox& sb, Asset const& asset, Asset const& asset2, beast::Jo
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// deleteAMMTrustLines also removes any credentials pinned to the AMM
|
||||
// pseudo-account, within its bounded walk.
|
||||
if (auto const ter = deleteAMMTrustLines(sb, ammAccountID, kMaxDeletableAmmTrustLines, j);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
@@ -917,11 +908,6 @@ isOnlyLiquidityProvider(ReadView const& view, Issue const& ammIssue, AccountID c
|
||||
++nMPT;
|
||||
continue;
|
||||
}
|
||||
// A credential naming the pseudo-account as subject can be pinned
|
||||
// to its owner directory. Ignore it here; deleteAMMTrustLines
|
||||
// removes it when the AMM is deleted.
|
||||
if (view.rules().enabled(fixCleanup3_4_0) && entryType == ltCREDENTIAL)
|
||||
continue;
|
||||
if (entryType != ltRIPPLE_STATE)
|
||||
return std::unexpected<TER>(tecINTERNAL); // LCOV_EXCL_LINE
|
||||
auto const lowLimit = sle->getFieldAmount(sfLowLimit);
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
@@ -515,8 +514,8 @@ pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey)
|
||||
}
|
||||
|
||||
// Pseudo-account designator fields MUST be maintained by including the
|
||||
// SField::sMD_PseudoAccount flag in the SField definition. (Don't forget to
|
||||
// "| SField::sMD_Default"!) The fields do NOT need to be amendment-gated,
|
||||
// SField::kSmdPseudoAccount flag in the SField definition. (Don't forget to
|
||||
// "| SField::kSmdDefault"!) The fields do NOT need to be amendment-gated,
|
||||
// since a non-active amendment will not set any field, by definition.
|
||||
// Specific properties of a pseudo-account are NOT checked here, that's what
|
||||
// InvariantCheck is for.
|
||||
@@ -547,18 +546,14 @@ getPseudoAccountFields()
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
isPseudoAccount(SLE::const_pointer sleAcct, std::set<SField const*> const& pseudoFieldFilter)
|
||||
isPseudoAccount(SLE::const_pointer sleAcct)
|
||||
{
|
||||
auto const& fields = getPseudoAccountFields();
|
||||
|
||||
// Intentionally use defensive coding here because it's cheap and makes the
|
||||
// semantics of true return value clean.
|
||||
return sleAcct && sleAcct->getType() == ltACCOUNT_ROOT &&
|
||||
std::count_if(
|
||||
fields.begin(), fields.end(), [&sleAcct, &pseudoFieldFilter](SField const* sf) -> bool {
|
||||
return sleAcct->isFieldPresent(*sf) &&
|
||||
(pseudoFieldFilter.empty() || pseudoFieldFilter.contains(sf));
|
||||
}) > 0;
|
||||
std::ranges::any_of(getPseudoAccountFields(), [&sleAcct](SField const* sf) {
|
||||
return sleAcct->isFieldPresent(*sf);
|
||||
});
|
||||
}
|
||||
|
||||
std::expected<SLE::pointer, TER>
|
||||
|
||||
@@ -5,10 +5,8 @@
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/chrono.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/View.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
@@ -129,36 +127,6 @@ deleteSLE(ApplyView& view, SLE::ref sleCredential, beast::Journal j)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
deletePseudoAccountCredentials(
|
||||
ApplyView& view,
|
||||
AccountID const& pseudoAcct,
|
||||
std::uint16_t maxNodesToDelete,
|
||||
beast::Journal j)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
isPseudoAccount(view.read(keylet::account(pseudoAcct))),
|
||||
"xrpl::credentials::deletePseudoAccountCredentials : is a pseudo-account");
|
||||
|
||||
// Delete the credentials linked into the pseudo-account's owner directory,
|
||||
// visiting at most maxNodesToDelete entries. Any other object is left in
|
||||
// place; the caller's own checks decide whether the remaining directory
|
||||
// blocks deletion. If the bound is reached, cleanupOnAccountDelete returns
|
||||
// tecINCOMPLETE and the caller propagates it so a later transaction resumes.
|
||||
return cleanupOnAccountDelete(
|
||||
view,
|
||||
keylet::ownerDir(pseudoAcct),
|
||||
[&view, &j](LedgerEntryType nodeType, uint256 const&, SLE::pointer& sleItem)
|
||||
-> std::pair<TER, SkipEntry> {
|
||||
if (nodeType == ltCREDENTIAL)
|
||||
return {deleteSLE(view, sleItem, j), SkipEntry::No};
|
||||
|
||||
return {tesSUCCESS, SkipEntry::Yes};
|
||||
},
|
||||
j,
|
||||
maxNodesToDelete);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
checkFields(STTx const& tx, Rules const& rules, beast::Journal j)
|
||||
{
|
||||
|
||||
@@ -169,6 +169,16 @@ isRounded(Asset const& asset, Number const& value, std::int32_t scale)
|
||||
roundToAsset(asset, value, scale, Number::RoundingMode::Upward);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
isPaymentLate(ReadView const& view, SLE::const_ref loanSle)
|
||||
{
|
||||
return hasExpired(
|
||||
view,
|
||||
loanSle->at(sfNextPaymentDueDate),
|
||||
view.rules().enabled(fixCleanup3_4_0) ? ExpiryComparison::Exclusive
|
||||
: ExpiryComparison::Inclusive);
|
||||
}
|
||||
|
||||
namespace accrual {
|
||||
|
||||
AccountingDeltas
|
||||
@@ -514,7 +524,7 @@ loanLatePaymentInterest(
|
||||
// If the payment is not late by any amount of time, then there's no late
|
||||
// interest
|
||||
if (now <= nextPaymentDueDate)
|
||||
return 0;
|
||||
return kNumZero;
|
||||
|
||||
// Equation (3) from XLS-66 spec, Section A-2 Equation Glossary
|
||||
auto const secondsOverdue = now - nextPaymentDueDate;
|
||||
@@ -1035,7 +1045,7 @@ doOverpayment(
|
||||
std::expected<ExtendedPaymentComponents, TER>
|
||||
computeLatePayment(
|
||||
Asset const& asset,
|
||||
ApplyView const& view,
|
||||
ReadView const& view,
|
||||
SLE::const_ref loan,
|
||||
ExtendedPaymentComponents const& periodic,
|
||||
STAmount const& amount,
|
||||
@@ -1046,8 +1056,11 @@ computeLatePayment(
|
||||
std::int32_t const loanScale = loan->at(sfLoanScale);
|
||||
|
||||
// Check if the due date has passed. If not, reject the payment as
|
||||
// being too soon
|
||||
if (!hasExpired(view, nextDueDate))
|
||||
// being too soon. Uses isPaymentLate() so this agrees with the
|
||||
// regular payment path on whether the loan is actually late at the
|
||||
// exact due date boundary (amendment-gated: Exclusive once
|
||||
// fixCleanup3_4_0 is enabled, Inclusive otherwise).
|
||||
if (!isPaymentLate(view, loan))
|
||||
return std::unexpected(tecTOO_SOON);
|
||||
|
||||
// Calculate the penalty interest based on how long the payment is overdue.
|
||||
@@ -1128,7 +1141,7 @@ computeLatePayment(
|
||||
std::expected<ExtendedPaymentComponents, TER>
|
||||
computeFullPayment(
|
||||
Asset const& asset,
|
||||
ApplyView& view,
|
||||
ReadView const& view,
|
||||
SLE::const_ref loan,
|
||||
Number const& periodicRate,
|
||||
STAmount const& amount,
|
||||
@@ -2270,7 +2283,7 @@ loanMakePayment(
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// A late payment not flagged as late overrides all other options.
|
||||
if (paymentType != LoanPaymentType::Late && hasExpired(view, nextDueDateProxy))
|
||||
if (paymentType != LoanPaymentType::Late && isPaymentLate(view, loan))
|
||||
{
|
||||
// If the payment is late, and the late flag was not set, it's not
|
||||
// valid
|
||||
|
||||
@@ -184,6 +184,8 @@ addEmptyHolding(
|
||||
auto const mpt = ctx.view.peek(keylet::mptokenIssuance(mptID));
|
||||
if (!mpt)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
// Unlike IOU addEmptyHolding (post-fixCleanup3_4_0), a locked issuance is
|
||||
// still rejected before the "MPToken already exists" short circuit.
|
||||
if (mpt->isFlag(lsfMPTLocked))
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
if (ctx.view.peek(keylet::mptoken(mptID, accountID)))
|
||||
@@ -384,8 +386,7 @@ requireAuth(
|
||||
// They are implicitly authorized for any MPT they hold, including vault shares whose
|
||||
// underlying asset would otherwise require auth.
|
||||
auto const isPseudoAccountExempt = [&] {
|
||||
return (featureSAVEnabled || featureMPTV2Enabled) &&
|
||||
isPseudoAccount(view, account, {&sfVaultID, &sfLoanBrokerID, &sfAMMID});
|
||||
return (featureSAVEnabled || featureMPTV2Enabled) && isPseudoAccount(view, account);
|
||||
};
|
||||
|
||||
auto const mptID = keylet::mptokenIssuance(mptIssue.getMptID());
|
||||
|
||||
@@ -652,21 +652,32 @@ addEmptyHolding(
|
||||
|
||||
auto const& issuerId = issue.getIssuer();
|
||||
auto const& currency = issue.currency;
|
||||
if (isGlobalFrozen(ctx.view, issuerId))
|
||||
return tecFROZEN; // LCOV_EXCL_LINE
|
||||
|
||||
auto const& srcId = issuerId;
|
||||
auto const& dstId = accountID;
|
||||
auto const high = srcId > dstId;
|
||||
auto const index = keylet::trustLine(srcId, dstId, currency);
|
||||
// Post-fixCleanup3_4_0: an existing line is a no-op. Issuer freeze and
|
||||
// DefaultRipple only matter when this function has to create a line.
|
||||
bool const fix340Enabled = ctx.view.rules().enabled(fixCleanup3_4_0);
|
||||
if (fix340Enabled && ctx.view.exists(index))
|
||||
return tecDUPLICATE;
|
||||
|
||||
if (isGlobalFrozen(ctx.view, issuerId))
|
||||
return tecFROZEN; // LCOV_EXCL_LINE
|
||||
|
||||
auto const sleSrc = ctx.view.peek(keylet::account(srcId));
|
||||
auto const sleDst = ctx.view.peek(keylet::account(dstId));
|
||||
if (!sleDst || !sleSrc)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
// Create path: DefaultRipple is still required. terNO_RIPPLE is
|
||||
// intentional so VaultWithdraw / CoverWithdraw fail in preclaim via
|
||||
// canAddHolding (retryable, no fee) rather than claiming a tec* fee
|
||||
// in doApply. Transactor::operator() will not apply and will not
|
||||
// convert it to tefINTERNAL.
|
||||
if (!sleSrc->isFlag(lsfDefaultRipple))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
return fix340Enabled ? TER{terNO_RIPPLE} : tecINTERNAL;
|
||||
// If the line already exists, don't create it again.
|
||||
if (ctx.view.read(index))
|
||||
if (!fix340Enabled && ctx.view.exists(index))
|
||||
return tecDUPLICATE;
|
||||
|
||||
// A reserve sponsor only covers tx.Account's own objects.
|
||||
|
||||
@@ -535,13 +535,19 @@ accountFunds(
|
||||
}
|
||||
|
||||
Rate
|
||||
transferRate(ReadView const& view, STAmount const& amount)
|
||||
transferRate(ReadView const& view, Asset const& asset)
|
||||
{
|
||||
return amount.asset().visit(
|
||||
return asset.visit(
|
||||
[&](Issue const& issue) { return transferRate(view, issue.getIssuer()); },
|
||||
[&](MPTIssue const& issue) { return transferRate(view, issue.getMptID()); });
|
||||
}
|
||||
|
||||
Rate
|
||||
transferRate(ReadView const& view, STAmount const& amount)
|
||||
{
|
||||
return transferRate(view, amount.asset());
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
//
|
||||
// Holding operations
|
||||
@@ -577,6 +583,32 @@ canAddHolding(ReadView const& view, Asset const& asset)
|
||||
asset.value());
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
holdingExists(ReadView const& view, AccountID const& account, Issue const& issue)
|
||||
{
|
||||
if (issue.native() || account == issue.getIssuer())
|
||||
return true;
|
||||
return view.exists(keylet::trustLine(account, issue));
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
holdingExists(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue)
|
||||
{
|
||||
if (account == mptIssue.getIssuer())
|
||||
return true;
|
||||
return view.exists(keylet::mptoken(mptIssue.getMptID(), account));
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
holdingExists(ReadView const& view, AccountID const& account, Asset const& asset)
|
||||
{
|
||||
return std::visit(
|
||||
[&]<ValidIssueType TIss>(TIss const& issue) -> bool {
|
||||
return holdingExists(view, account, issue);
|
||||
},
|
||||
asset.value());
|
||||
}
|
||||
|
||||
TER
|
||||
addEmptyHolding(
|
||||
ApplyViewContext ctx,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
@@ -17,6 +18,7 @@
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
@@ -69,6 +71,65 @@ sharesToAssetsDeposit(SLE::const_ref vault, SLE::const_ref issuance, STAmount co
|
||||
return assets;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<STAmount, TER>
|
||||
clampToAssetsTotalScale(SLE::const_ref vault, STAmount const& delta)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
delta.asset() == vault->at(sfAsset),
|
||||
"xrpl::clampToAssetsTotalScale : delta and vault asset match");
|
||||
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
|
||||
STAmount magnitude = delta.negative() ? -delta : delta;
|
||||
if (asset.integral())
|
||||
{
|
||||
return magnitude;
|
||||
}
|
||||
Number const assetsTotal = vault->at(sfAssetsTotal);
|
||||
|
||||
// Calculate the scale after applying the delta using ToNearest rounding.
|
||||
// This aligns the delta with scale checks used by vault invariants.
|
||||
int const postScale = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
|
||||
return scale(assetsTotal + delta, asset);
|
||||
}();
|
||||
|
||||
STAmount actualDelta;
|
||||
if (delta.negative())
|
||||
{
|
||||
// For withdrawals (debits), floor the magnitude to the target scale
|
||||
// to ensure exact grid alignment without paying out extra assets.
|
||||
actualDelta = roundToScale(magnitude, postScale, Number::RoundingMode::Downward);
|
||||
}
|
||||
else
|
||||
{
|
||||
// For deposits (credits), derive actualDelta from the floored posterior total.
|
||||
// This prevents grid alignment issues from crediting the vault more than deposited.
|
||||
//
|
||||
// Sum using Downward rounding so intermediate precision doesn't round up
|
||||
// and exceed the original requested amount.
|
||||
Number const posterior = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return assetsTotal + magnitude;
|
||||
}();
|
||||
|
||||
Number const roundedPosterior =
|
||||
roundToAsset(asset, posterior, postScale, Number::RoundingMode::Downward);
|
||||
actualDelta = STAmount{asset, roundedPosterior - assetsTotal};
|
||||
}
|
||||
|
||||
XRPL_ASSERT(
|
||||
abs(actualDelta) <= abs(delta),
|
||||
"xrpl::clampToAssetsTotalScale : actual delta smaller or equal to calculated delta");
|
||||
|
||||
// Reject changes below scale precision (1 ULP) to prevent share balance changes
|
||||
// without corresponding asset movements.
|
||||
if (actualDelta <= beast::kZero)
|
||||
return std::unexpected(tecPRECISION_LOSS);
|
||||
|
||||
return actualDelta;
|
||||
}
|
||||
|
||||
[[nodiscard]] Number
|
||||
assetsTotalForWithdrawal(SLE::const_ref vault, WaiveUnrealizedLoss waive)
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace {
|
||||
//------------------------------------------------------------------------------
|
||||
// clang-format off
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
char const* const versionString = "3.4.0-b1"
|
||||
char const* const versionString = "3.4.0-rc1"
|
||||
// clang-format on
|
||||
;
|
||||
|
||||
|
||||
@@ -123,6 +123,10 @@ getBookBase(Book const& book)
|
||||
{
|
||||
XRPL_ASSERT(isConsistent(book), "xrpl::getBookBase : input is consistent");
|
||||
|
||||
constexpr std::uint8_t kIssueToMPTTag = 0x01;
|
||||
constexpr std::uint8_t kMPTToIssueTag = 0x02;
|
||||
constexpr std::uint8_t kMPTToMPTTag = 0x03;
|
||||
|
||||
auto getIndexHash = [&book]<typename... Args>(Args... args) {
|
||||
if (book.domain)
|
||||
return indexHash(std::forward<Args>(args)..., *book.domain);
|
||||
@@ -136,19 +140,36 @@ getBookBase(Book const& book)
|
||||
return getIndexHash(
|
||||
LedgerNameSpace::BookDir, in.currency, out.currency, in.account, out.account);
|
||||
}
|
||||
// The three MPT-involving branches are new under MPTokensV2 and
|
||||
// each gets a 1-byte discriminator to prevent preimage collisions
|
||||
// between branches: the (Issue,MPT) and (MPT,Issue) preimages
|
||||
// are both 64 bytes of raw concatenation, so without a
|
||||
// per-branch tag chosen Currency / MPTID / AccountID values can
|
||||
// align byte-for-byte and produce the same BookDir keylet for
|
||||
// two distinct markets. (Issue,Issue) is left untagged to
|
||||
// preserve existing mainnet order-book keylets.
|
||||
else if constexpr (std::is_same_v<TIn, Issue> && std::is_same_v<TOut, MPTIssue>)
|
||||
{
|
||||
return getIndexHash(
|
||||
LedgerNameSpace::BookDir, in.currency, out.getMptID(), in.account);
|
||||
LedgerNameSpace::BookDir,
|
||||
kIssueToMPTTag,
|
||||
in.currency,
|
||||
out.getMptID(),
|
||||
in.account);
|
||||
}
|
||||
else if constexpr (std::is_same_v<TIn, MPTIssue> && std::is_same_v<TOut, Issue>)
|
||||
{
|
||||
return getIndexHash(
|
||||
LedgerNameSpace::BookDir, in.getMptID(), out.currency, out.account);
|
||||
LedgerNameSpace::BookDir,
|
||||
kMPTToIssueTag,
|
||||
in.getMptID(),
|
||||
out.currency,
|
||||
out.account);
|
||||
}
|
||||
else
|
||||
{
|
||||
return getIndexHash(LedgerNameSpace::BookDir, in.getMptID(), out.getMptID());
|
||||
return getIndexHash(
|
||||
LedgerNameSpace::BookDir, kMPTToMPTTag, in.getMptID(), out.getMptID());
|
||||
}
|
||||
},
|
||||
book.in.value(),
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
#include <xrpl/protocol/NFTSyntheticSerializer.h>
|
||||
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/NFTokenID.h>
|
||||
#include <xrpl/protocol/NFTokenOfferID.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TxMeta.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace xrpl::rpc {
|
||||
|
||||
void
|
||||
insertNFTSyntheticInJson(
|
||||
json::Value& response,
|
||||
std::shared_ptr<STTx const> const& transaction,
|
||||
TxMeta const& transactionMeta)
|
||||
{
|
||||
insertNFTokenID(response[jss::meta], transaction, transactionMeta);
|
||||
insertNFTokenOfferID(response[jss::meta], transaction, transactionMeta);
|
||||
}
|
||||
|
||||
} // namespace xrpl::rpc
|
||||
@@ -168,10 +168,10 @@ STTx::getMentionedAccounts() const
|
||||
}
|
||||
|
||||
static Blob
|
||||
getSigningData(STTx const& that)
|
||||
getSigningData(STTx const& that, HashPrefix prefix)
|
||||
{
|
||||
Serializer s;
|
||||
s.add32(HashPrefix::TxSign);
|
||||
s.add32(prefix);
|
||||
that.addWithoutSigningFields(s);
|
||||
return s.getData();
|
||||
}
|
||||
@@ -212,30 +212,42 @@ STTx::getSeqProxy() const
|
||||
return SeqProxy::rawTicket(*ticketSeq);
|
||||
}
|
||||
|
||||
void
|
||||
STTx::sign(PublicKey const& publicKey, SecretKey const& secretKey)
|
||||
{
|
||||
// The account's own signature always covers the plain transaction prefix;
|
||||
// see signingPrefix for the role signatures that do not.
|
||||
auto const data = getSigningData(*this, HashPrefix::TxSign);
|
||||
|
||||
setFieldVL(sfTxnSignature, xrpl::sign(publicKey, secretKey, makeSlice(data)));
|
||||
tid_ = getHash(HashPrefix::TransactionId);
|
||||
}
|
||||
|
||||
void
|
||||
STTx::sign(
|
||||
PublicKey const& publicKey,
|
||||
SecretKey const& secretKey,
|
||||
std::optional<std::reference_wrapper<SField const>> signatureTarget)
|
||||
SignatureRole role,
|
||||
Rules const& rules)
|
||||
{
|
||||
auto const data = getSigningData(*this);
|
||||
auto const data = getSigningData(*this, signingPrefix(role, false, rules));
|
||||
|
||||
auto const sig = xrpl::sign(publicKey, secretKey, makeSlice(data));
|
||||
|
||||
if (signatureTarget)
|
||||
if (auto const target = signatureField(role))
|
||||
{
|
||||
auto& target = peekFieldObject(*signatureTarget);
|
||||
target.setFieldVL(sfTxnSignature, sig);
|
||||
peekFieldObject(*target).setFieldVL(sfTxnSignature, sig);
|
||||
}
|
||||
else
|
||||
{
|
||||
setFieldVL(sfTxnSignature, sig);
|
||||
}
|
||||
|
||||
tid_ = getHash(HashPrefix::TransactionId);
|
||||
}
|
||||
|
||||
std::expected<void, std::string>
|
||||
STTx::checkSign(Rules const& rules, STObject const& sigObject) const
|
||||
STTx::checkSign(Rules const& rules, STObject const& sigObject, SignatureRole role) const
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -244,8 +256,10 @@ STTx::checkSign(Rules const& rules, STObject const& sigObject) const
|
||||
// multi-signing. Otherwise we're single-signing.
|
||||
|
||||
Blob const& signingPubKey = sigObject.getFieldVL(sfSigningPubKey);
|
||||
return signingPubKey.empty() ? checkMultiSign(rules, sigObject)
|
||||
: checkSingleSign(sigObject);
|
||||
bool const multiSigning = signingPubKey.empty();
|
||||
auto const prefix = signingPrefix(role, multiSigning, rules);
|
||||
return multiSigning ? checkMultiSign(sigObject, prefix)
|
||||
: checkSingleSign(sigObject, prefix);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
@@ -256,20 +270,20 @@ STTx::checkSign(Rules const& rules, STObject const& sigObject) const
|
||||
std::expected<void, std::string>
|
||||
STTx::checkSign(Rules const& rules) const
|
||||
{
|
||||
if (auto const ret = checkSign(rules, *this); !ret)
|
||||
if (auto const ret = checkSign(rules, *this, SignatureRole::Transaction); !ret)
|
||||
return ret;
|
||||
|
||||
if (isFieldPresent(sfCounterpartySignature))
|
||||
{
|
||||
auto const counterSig = getFieldObject(sfCounterpartySignature);
|
||||
if (auto const ret = checkSign(rules, counterSig); !ret)
|
||||
if (auto const ret = checkSign(rules, counterSig, SignatureRole::Counterparty); !ret)
|
||||
return std::unexpected("Counterparty: " + ret.error());
|
||||
}
|
||||
|
||||
if (isFieldPresent(sfSponsorSignature))
|
||||
{
|
||||
auto const sponsorSignatureObj = getFieldObject(sfSponsorSignature);
|
||||
if (auto const ret = checkSign(rules, sponsorSignatureObj); !ret)
|
||||
if (auto const ret = checkSign(rules, sponsorSignatureObj, SignatureRole::Sponsor); !ret)
|
||||
return std::unexpected("Sponsor: " + ret.error());
|
||||
}
|
||||
|
||||
@@ -277,14 +291,14 @@ STTx::checkSign(Rules const& rules) const
|
||||
// of signature checking.
|
||||
if (isFieldPresent(sfBatchSigners))
|
||||
{
|
||||
if (auto const ret = checkBatchSign(rules); !ret)
|
||||
if (auto const ret = checkBatchSign(); !ret)
|
||||
return ret;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::expected<void, std::string>
|
||||
STTx::checkBatchSign(Rules const& rules) const
|
||||
STTx::checkBatchSign() const
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -318,7 +332,7 @@ STTx::checkBatchSign(Rules const& rules) const
|
||||
for (auto const& signer : signers)
|
||||
{
|
||||
Blob const& signingPubKey = signer.getFieldVL(sfSigningPubKey);
|
||||
auto const result = signingPubKey.empty() ? checkBatchMultiSign(signer, rules, txIds)
|
||||
auto const result = signingPubKey.empty() ? checkBatchMultiSign(signer, txIds)
|
||||
: checkBatchSingleSign(signer, txIds);
|
||||
|
||||
if (!result)
|
||||
@@ -447,9 +461,9 @@ singleSignHelper(STObject const& sigObject, Slice const& data)
|
||||
}
|
||||
|
||||
std::expected<void, std::string>
|
||||
STTx::checkSingleSign(STObject const& sigObject) const
|
||||
STTx::checkSingleSign(STObject const& sigObject, HashPrefix prefix) const
|
||||
{
|
||||
auto const data = getSigningData(*this);
|
||||
auto const data = getSigningData(*this, prefix);
|
||||
return singleSignHelper(sigObject, makeSlice(data));
|
||||
}
|
||||
|
||||
@@ -467,8 +481,7 @@ std::expected<void, std::string>
|
||||
multiSignHelper(
|
||||
STObject const& sigObject,
|
||||
std::optional<AccountID> txnAccountID,
|
||||
std::function<Serializer(AccountID const&)> makeMsg,
|
||||
Rules const& rules)
|
||||
std::function<Serializer(AccountID const&)> makeMsg)
|
||||
{
|
||||
// Make sure the MultiSigners are present. Otherwise they are not
|
||||
// attempting multi-signing and we just have a bad SigningPubKey.
|
||||
@@ -541,10 +554,7 @@ multiSignHelper(
|
||||
}
|
||||
|
||||
std::expected<void, std::string>
|
||||
STTx::checkBatchMultiSign(
|
||||
STObject const& batchSigner,
|
||||
Rules const& rules,
|
||||
std::vector<uint256> const& txIds) const
|
||||
STTx::checkBatchMultiSign(STObject const& batchSigner, std::vector<uint256> const& txIds) const
|
||||
{
|
||||
XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::checkBatchMultiSign : batch transaction");
|
||||
// We can ease the computational load inside the loop a bit by
|
||||
@@ -555,18 +565,15 @@ STTx::checkBatchMultiSign(
|
||||
serializeBatch(dataStart, getAccountID(sfAccount), getSeqProxy().value(), getFlags(), txIds);
|
||||
dataStart.addBitString(batchSignerAccount);
|
||||
return multiSignHelper(
|
||||
batchSigner,
|
||||
batchSignerAccount,
|
||||
[&dataStart](AccountID const& accountID) -> Serializer {
|
||||
batchSigner, batchSignerAccount, [&dataStart](AccountID const& accountID) -> Serializer {
|
||||
Serializer s = dataStart;
|
||||
finishMultiSigningData(accountID, s);
|
||||
return s;
|
||||
},
|
||||
rules);
|
||||
});
|
||||
}
|
||||
|
||||
std::expected<void, std::string>
|
||||
STTx::checkMultiSign(Rules const& rules, STObject const& sigObject) const
|
||||
STTx::checkMultiSign(STObject const& sigObject, HashPrefix prefix) const
|
||||
{
|
||||
// Used inside the loop in multiSignHelper to enforce that
|
||||
// the account owner may not multisign for themselves.
|
||||
@@ -578,16 +585,13 @@ STTx::checkMultiSign(Rules const& rules, STObject const& sigObject) const
|
||||
// We can ease the computational load inside the loop a bit by
|
||||
// pre-constructing part of the data that we hash. Fill a Serializer
|
||||
// with the stuff that stays constant from signature to signature.
|
||||
Serializer dataStart = startMultiSigningData(*this);
|
||||
Serializer dataStart = startMultiSigningData(*this, prefix);
|
||||
return multiSignHelper(
|
||||
sigObject,
|
||||
txnAccountID,
|
||||
[&dataStart](AccountID const& accountID) -> Serializer {
|
||||
sigObject, txnAccountID, [&dataStart](AccountID const& accountID) -> Serializer {
|
||||
Serializer s = dataStart;
|
||||
finishMultiSigningData(accountID, s);
|
||||
return s;
|
||||
},
|
||||
rules);
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -1,17 +1,77 @@
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STExchange.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
SField const*
|
||||
signatureField(SignatureRole role)
|
||||
{
|
||||
switch (role)
|
||||
{
|
||||
case SignatureRole::Transaction:
|
||||
return nullptr;
|
||||
case SignatureRole::Counterparty:
|
||||
return &sfCounterpartySignature;
|
||||
case SignatureRole::Sponsor:
|
||||
return &sfSponsorSignature;
|
||||
}
|
||||
UNREACHABLE("xrpl::signatureField : unknown SignatureRole");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::optional<SignatureRole>
|
||||
signatureRole(SField const& sigField)
|
||||
{
|
||||
if (sigField == sfCounterpartySignature)
|
||||
return SignatureRole::Counterparty;
|
||||
if (sigField == sfSponsorSignature)
|
||||
return SignatureRole::Sponsor;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Signature validity depends on fixCleanup3_4_0: a role signature covers
|
||||
// different bytes before and after the amendment activates. checkValidity
|
||||
// caches its verdict per transaction ID, so it keeps two separate cache slots
|
||||
// for role-signature transactions (kSfSiggoodOldPrefix / kSfSigbadOldPrefix in
|
||||
// tx/apply.cpp) to keep a pre-fix verdict from being reused in the post-fix
|
||||
// era, and vice versa. See the block comment in tx/apply.cpp for the details
|
||||
// and the reason both directions matter.
|
||||
HashPrefix
|
||||
signingPrefix(SignatureRole role, bool multiSigning, Rules const& rules)
|
||||
{
|
||||
// Before fixCleanup3_4_0 every signature on a transaction covered the same
|
||||
// bytes, so a signature could be moved from one role to another.
|
||||
if (!rules.enabled(fixCleanup3_4_0))
|
||||
return multiSigning ? HashPrefix::TxMultiSign : HashPrefix::TxSign;
|
||||
|
||||
switch (role)
|
||||
{
|
||||
case SignatureRole::Transaction:
|
||||
return multiSigning ? HashPrefix::TxMultiSign : HashPrefix::TxSign;
|
||||
case SignatureRole::Counterparty:
|
||||
return multiSigning ? HashPrefix::CounterpartyTxMultiSign
|
||||
: HashPrefix::CounterpartyTxSign;
|
||||
case SignatureRole::Sponsor:
|
||||
return multiSigning ? HashPrefix::SponsorTxMultiSign : HashPrefix::SponsorTxSign;
|
||||
}
|
||||
UNREACHABLE("xrpl::signingPrefix : unknown SignatureRole");
|
||||
return multiSigning ? HashPrefix::TxMultiSign : HashPrefix::TxSign;
|
||||
}
|
||||
|
||||
void
|
||||
sign(
|
||||
STObject& st,
|
||||
@@ -70,18 +130,18 @@ verify(STObject const& st, HashPrefix const& prefix, PublicKey const& pk, SF_VL
|
||||
// So, if we support multiple levels of signing, then we'll need to
|
||||
// incorporate the "signing for" accounts into the signing data as well.
|
||||
Serializer
|
||||
buildMultiSigningData(STObject const& obj, AccountID const& signingID)
|
||||
buildMultiSigningData(STObject const& obj, AccountID const& signingID, HashPrefix prefix)
|
||||
{
|
||||
Serializer s{startMultiSigningData(obj)};
|
||||
Serializer s{startMultiSigningData(obj, prefix)};
|
||||
finishMultiSigningData(signingID, s);
|
||||
return s;
|
||||
}
|
||||
|
||||
Serializer
|
||||
startMultiSigningData(STObject const& obj)
|
||||
startMultiSigningData(STObject const& obj, HashPrefix prefix)
|
||||
{
|
||||
Serializer s;
|
||||
s.add32(HashPrefix::TxMultiSign);
|
||||
s.add32(prefix);
|
||||
obj.addWithoutSigningFields(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/shamap/SHAMap.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
@@ -40,11 +41,41 @@ depthMask(unsigned int depth)
|
||||
return kMasks.entry[depth];
|
||||
}
|
||||
|
||||
// The prefix of `key` at `depth`: the leading nibbles naming the subtree a node at that depth
|
||||
// identifies, with the remainder of the key masked off.
|
||||
static uint256
|
||||
maskedToDepth(uint256 const& key, unsigned int depth)
|
||||
{
|
||||
return key & depthMask(depth);
|
||||
}
|
||||
|
||||
// Whether `id` at `depth` is what `key` looks like once masked down to that depth, i.e.
|
||||
// whether an ID with this depth and id names a subtree that `key` falls under.
|
||||
static bool
|
||||
isPrefixOfAtDepth(uint256 const& id, unsigned int depth, uint256 const& key)
|
||||
{
|
||||
return maskedToDepth(key, depth) == id;
|
||||
}
|
||||
|
||||
// canonicalize the hash to a node ID for this depth
|
||||
SHAMapNodeID::SHAMapNodeID(unsigned int depth, uint256 const& hash) : id_(hash), depth_(depth)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
depth <= SHAMap::kLeafDepth, "xrpl::SHAMapNodeID::SHAMapNodeID : maximum depth input");
|
||||
// Every SHAMapNodeID's depth is stored here, so this is the one place that can stop an
|
||||
// out-of-range one from being kept: a depth past kLeafDepth would go on to index depthMask
|
||||
// out of bounds, and getRawString would narrow it to a byte, silently renaming the node.
|
||||
// Clamp rather than throw, since node IDs are built from peer-supplied depths on the ledger
|
||||
// data path, where no caller catches an exception before it reaches a thread boundary.
|
||||
if (depth_ > SHAMap::kLeafDepth)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMapNodeID::SHAMapNodeID : depth within tree");
|
||||
depth_ = SHAMap::kLeafDepth;
|
||||
id_ = maskedToDepth(id_, depth_);
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// Reads the clamped member rather than the depth argument, so it cannot index depthMask past
|
||||
// its last entry even once the clamp above has reported the bad input and carried on.
|
||||
XRPL_ASSERT(
|
||||
isPrefixOf(id_), "xrpl::SHAMapNodeID::SHAMapNodeID : hash and depth inputs do match");
|
||||
}
|
||||
@@ -89,7 +120,7 @@ SHAMapNodeID::getChildNodeID(unsigned int branch) const
|
||||
bool
|
||||
SHAMapNodeID::isPrefixOf(uint256 const& key) const
|
||||
{
|
||||
return (key & depthMask(depth_)) == id_;
|
||||
return isPrefixOfAtDepth(id_, depth_, key);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<SHAMapNodeID>
|
||||
@@ -102,9 +133,9 @@ deserializeSHAMapNodeID(void const* data, std::size_t size)
|
||||
unsigned int const depth = *(static_cast<unsigned char const*>(data) + 32);
|
||||
if (depth <= SHAMap::kLeafDepth)
|
||||
{
|
||||
auto const id = uint256::fromVoid(data);
|
||||
|
||||
if (id == (id & depthMask(depth)))
|
||||
// Reject a serialized ID carrying bits below its own depth. Checked before
|
||||
// constructing, since the constructor asserts that same property.
|
||||
if (auto const id = uint256::fromVoid(data); isPrefixOfAtDepth(id, depth, id))
|
||||
ret.emplace(depth, id);
|
||||
}
|
||||
}
|
||||
@@ -115,7 +146,11 @@ deserializeSHAMapNodeID(void const* data, std::size_t size)
|
||||
[[nodiscard]] unsigned int
|
||||
selectBranch(SHAMapNodeID const& id, uint256 const& hash)
|
||||
{
|
||||
auto const depth = id.getDepth();
|
||||
XRPL_ASSERT(id.getDepth() < SHAMap::kLeafDepth, "xrpl::selectBranch : depth below leaf depth");
|
||||
|
||||
// A depth-64 ID has no nibble left to select. Callers must not ask, but clamp anyway to keep
|
||||
// the read below the end of the 32-byte key.
|
||||
auto const depth = std::min(id.getDepth(), SHAMap::kLeafDepth - 1u);
|
||||
auto branch = static_cast<unsigned int>(*(hash.begin() + (depth / 2)));
|
||||
|
||||
if ((depth & 1) != 0u)
|
||||
@@ -134,8 +169,18 @@ selectBranch(SHAMapNodeID const& id, uint256 const& hash)
|
||||
SHAMapNodeID
|
||||
SHAMapNodeID::createID(unsigned int depth, uint256 const& key)
|
||||
{
|
||||
XRPL_ASSERT(depth <= SHAMap::kLeafDepth, "xrpl::SHAMapNodeID::createID : valid depth");
|
||||
return SHAMapNodeID(depth, key & depthMask(depth));
|
||||
// The mask is chosen here, before the constructor runs, so the clamp there cannot cover this
|
||||
// call: an out-of-range depth would index depthMask's table while still evaluating this
|
||||
// argument. A public factory has to hold its own bound.
|
||||
if (depth > SHAMap::kLeafDepth)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMapNodeID::createID : depth within tree");
|
||||
depth = SHAMap::kLeafDepth;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
return SHAMapNodeID(depth, maskedToDepth(key, depth));
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
5
src/libxrpl/tx/AGENTS.md
Normal file
5
src/libxrpl/tx/AGENTS.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# AGENTS.md — tx
|
||||
|
||||
See the repo-level [AGENTS.md](../../../AGENTS.md) for general build/test/style guidance.
|
||||
|
||||
Any change to transaction-processing behavior must be gated behind an amendment. New amendments (and fixes, i.e. `fix*` amendments) are added to [`include/xrpl/protocol/detail/features.macro`](../../../include/xrpl/protocol/detail/features.macro), as an `XRPL_FEATURE(...)` or `XRPL_FIX(...)` entry added to the top of the list (the list is kept in reverse chronological order). Once the pre-amendment code path for a retired amendment is removed, move its entry to `XRPL_RETIRE_FEATURE(...)`/`XRPL_RETIRE_FIX(...)` instead of deleting it.
|
||||
1
src/libxrpl/tx/CLAUDE.md
Symbolic link
1
src/libxrpl/tx/CLAUDE.md
Symbolic link
@@ -0,0 +1 @@
|
||||
AGENTS.md
|
||||
@@ -1246,7 +1246,7 @@ removeExpiredNFTokenOffers(
|
||||
}
|
||||
|
||||
static void
|
||||
removeDeletedCredentials(ApplyView& view, std::vector<uint256> const& creds, beast::Journal viewJ)
|
||||
removeExpiredCredentials(ApplyView& view, std::vector<uint256> const& creds, beast::Journal viewJ)
|
||||
{
|
||||
for (auto const& index : creds)
|
||||
{
|
||||
@@ -1255,7 +1255,7 @@ removeDeletedCredentials(ApplyView& view, std::vector<uint256> const& creds, bea
|
||||
if (auto const ter = credentials::deleteSLE(view, sle, viewJ); !isTesSuccess(ter))
|
||||
{
|
||||
JLOG(viewJ.error())
|
||||
<< "removeDeletedCredentials: failed to delete credential. Err: "
|
||||
<< "removeExpiredCredentials: failed to delete expired credential. Err: "
|
||||
<< transToken(ter);
|
||||
}
|
||||
}
|
||||
@@ -1437,8 +1437,7 @@ Transactor::processPersistentChanges(TER result, XRPAmount fee)
|
||||
// should be used, making it possible to do more useful work
|
||||
// when transactions fail with a `tec` code.
|
||||
|
||||
auto typesForResult = [credentialCleanup =
|
||||
view().rules().enabled(fixCleanup3_4_0)](TER const ter) {
|
||||
auto typesForResult = [](TER const ter) {
|
||||
std::unordered_set<LedgerEntryType> types;
|
||||
if ((ter == tecOVERSIZE) || (ter == tecKILLED))
|
||||
{
|
||||
@@ -1447,11 +1446,6 @@ Transactor::processPersistentChanges(TER result, XRPAmount fee)
|
||||
else if (ter == tecINCOMPLETE)
|
||||
{
|
||||
types.insert(ltRIPPLE_STATE);
|
||||
// A bounded pseudo-account credential cleanup (VaultDelete /
|
||||
// LoanBrokerDelete) persists its partial credential deletions so a
|
||||
// later transaction can resume.
|
||||
if (credentialCleanup)
|
||||
types.insert(ltCREDENTIAL);
|
||||
}
|
||||
else if (ter == tecEXPIRED)
|
||||
{
|
||||
@@ -1529,7 +1523,7 @@ Transactor::processPersistentChanges(TER result, XRPAmount fee)
|
||||
removeDeletedTrustLines(view(), ids, viewJ);
|
||||
break;
|
||||
case ltCREDENTIAL:
|
||||
removeDeletedCredentials(view(), ids, viewJ);
|
||||
removeExpiredCredentials(view(), ids, viewJ);
|
||||
break;
|
||||
// LCOV_EXCL_START
|
||||
default:
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/OpenView.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
@@ -23,13 +24,38 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// These are the same flags defined as HashRouterFlags::PRIVATE1-4 in
|
||||
// HashRouter.h
|
||||
// This file owns HashRouterFlags::PRIVATE1-4 and PRIVATE7-8 in HashRouter.h.
|
||||
// These are the first four; the other two are below.
|
||||
constexpr HashRouterFlags kSfSigbad = HashRouterFlags::PRIVATE1; // Signature is bad
|
||||
constexpr HashRouterFlags kSfSiggood = HashRouterFlags::PRIVATE2; // Signature is good
|
||||
constexpr HashRouterFlags kSfLocalbad = HashRouterFlags::PRIVATE3; // Local checks failed
|
||||
constexpr HashRouterFlags kSfLocalgood = HashRouterFlags::PRIVATE4; // Local checks passed
|
||||
|
||||
// Before fixCleanup3_4_0, a signature in an alternate role field, such as
|
||||
// sfSponsorSignature, covered the same bytes as the top level signature. Which
|
||||
// bytes a role signature must cover therefore depends on whether the fix is
|
||||
// enabled, but the four flags above record only the verdict, not the rules that
|
||||
// produced it. A verdict reached under one prefix would otherwise be reused
|
||||
// under the other.
|
||||
//
|
||||
// The two flags below hold the verdict for the pre-fix prefixes, so the pre-fix
|
||||
// and post-fix verdicts occupy separate slots and neither is ever read in the
|
||||
// other's era. Nothing is cleared when the amendment activates: setFlags only
|
||||
// sets bits, so a stale pre-fix verdict simply stops being read and ages out
|
||||
// with the rest of the routing table.
|
||||
//
|
||||
// This is not one switchover at a single instant. The era is chosen per call
|
||||
// from the rules passed in, and callers do not agree on the rules: relay and
|
||||
// submit verify against the validated rules, which lag the open ledger rules
|
||||
// that preflight2 verifies against. At the amendment's flag ledger the same
|
||||
// transaction can therefore be checked under both prefixes, on the same node,
|
||||
// at the same time.
|
||||
//
|
||||
// Remove these two flags, and oldPrefixSig below, when Cleanup3_4_0 is retired
|
||||
// in features.macro.
|
||||
constexpr HashRouterFlags kSfSigbadOldPrefix = HashRouterFlags::PRIVATE7;
|
||||
constexpr HashRouterFlags kSfSiggoodOldPrefix = HashRouterFlags::PRIVATE8;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
std::pair<Validity, std::string>
|
||||
@@ -48,21 +74,41 @@ checkValidity(HashRouter& router, STTx const& tx, Rules const& rules)
|
||||
return {Validity::SigBad, "Batch inner transactions are never considered validly signed."};
|
||||
}
|
||||
|
||||
if (any(flags & kSfSigbad))
|
||||
// Pick the cache slot for this call's era; see kSfSiggoodOldPrefix above.
|
||||
// Only a transaction that carries a role signature, and only while the fix
|
||||
// is disabled, uses the separate slot. Every other transaction, and every
|
||||
// transaction once the fix is enabled, uses the ordinary flags and verifies
|
||||
// exactly once, so there is no steady state cost.
|
||||
//
|
||||
// Both directions matter. A good verdict from before the fix must not let a
|
||||
// signature moved between roles survive the amendment, and a bad verdict
|
||||
// from before the fix must not condemn a transaction that the new prefixes
|
||||
// accept.
|
||||
//
|
||||
// Whether a transaction carries a role signature is fixed for its ID: the
|
||||
// fields are kNotSigning, so they are excluded from the signed bytes, but
|
||||
// they are still covered by the transaction ID. Repeat calls for one ID
|
||||
// therefore always agree on which slot pair to use.
|
||||
bool const oldPrefixSig = !rules.enabled(fixCleanup3_4_0) &&
|
||||
(tx.isFieldPresent(sfSponsorSignature) || tx.isFieldPresent(sfCounterpartySignature));
|
||||
auto const sigbadFlag = oldPrefixSig ? kSfSigbadOldPrefix : kSfSigbad;
|
||||
auto const siggoodFlag = oldPrefixSig ? kSfSiggoodOldPrefix : kSfSiggood;
|
||||
|
||||
if (any(flags & sigbadFlag))
|
||||
{
|
||||
// Signature is known bad
|
||||
return {Validity::SigBad, "Transaction has bad signature."};
|
||||
}
|
||||
|
||||
if (!any(flags & kSfSiggood))
|
||||
if (!any(flags & siggoodFlag))
|
||||
{
|
||||
auto const sigVerify = tx.checkSign(rules);
|
||||
if (!sigVerify)
|
||||
{
|
||||
router.setFlags(id, kSfSigbad);
|
||||
router.setFlags(id, sigbadFlag);
|
||||
return {Validity::SigBad, sigVerify.error()};
|
||||
}
|
||||
router.setFlags(id, kSfSiggood);
|
||||
router.setFlags(id, siggoodFlag);
|
||||
}
|
||||
|
||||
// Signature is now known good
|
||||
@@ -94,6 +140,19 @@ checkValidity(HashRouter& router, STTx const& tx, Rules const& rules)
|
||||
void
|
||||
forceValidity(HashRouter& router, uint256 const& txid, Validity validity)
|
||||
{
|
||||
// Callers reach here when they deliberately skip signature verification,
|
||||
// such as a cluster peer that trusts its neighbor's checks, or a
|
||||
// configuration that turns signature checks off. Nothing was verified, so
|
||||
// there is no prefix era to record. Mark both of checkValidity's signature
|
||||
// slots good: otherwise the forced verdict is ignored for a role-signature
|
||||
// transaction until fixCleanup3_4_0 is enabled, and the signature the
|
||||
// caller meant to skip gets verified after all. Marking both cannot leak a
|
||||
// verdict across eras, because no verdict was reached, and this is the only
|
||||
// place the distinction can be recorded: kSfSiggood alone does not say
|
||||
// whether checkValidity verified a post-fix signature or a caller forced
|
||||
// the result. An already cached bad verdict still wins, since checkValidity
|
||||
// tests its bad flag first. Drop kSfSiggoodOldPrefix when Cleanup3_4_0 is
|
||||
// retired.
|
||||
HashRouterFlags flags = HashRouterFlags::UNDEFINED;
|
||||
switch (validity)
|
||||
{
|
||||
@@ -101,7 +160,7 @@ forceValidity(HashRouter& router, uint256 const& txid, Validity validity)
|
||||
flags |= kSfLocalgood;
|
||||
[[fallthrough]];
|
||||
case Validity::SigGoodOnly:
|
||||
flags |= kSfSiggood;
|
||||
flags |= kSfSiggood | kSfSiggoodOldPrefix;
|
||||
[[fallthrough]];
|
||||
case Validity::SigBad:
|
||||
// would be silly to call directly
|
||||
|
||||
@@ -1123,10 +1123,17 @@ NoModifiedUnmodifiableFields::finalize(
|
||||
ReadView const& view,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
static auto const kFieldChanged = [](auto const& before, auto const& after, auto const& field) {
|
||||
auto const kFieldChanged = [&j, &tx](auto const& before, auto const& after, auto const& field) {
|
||||
bool const beforeField = before->isFieldPresent(field);
|
||||
bool const afterField = after->isFieldPresent(field);
|
||||
return beforeField != afterField || (afterField && before->at(field) != after->at(field));
|
||||
bool const changed =
|
||||
beforeField != afterField || (afterField && before->at(field) != after->at(field));
|
||||
if (changed)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: " << field.getName()
|
||||
<< " changed on immutable ledger entry in " << tx.getTransactionID();
|
||||
}
|
||||
return changed;
|
||||
};
|
||||
for (auto const& slePair : changedEntries_)
|
||||
{
|
||||
@@ -1172,13 +1179,40 @@ NoModifiedUnmodifiableFields::finalize(
|
||||
kFieldChanged(before, after, sfPaymentInterval) ||
|
||||
kFieldChanged(before, after, sfGracePeriod) ||
|
||||
kFieldChanged(before, after, sfLoanScale);
|
||||
|
||||
// lsfLoanOverpayment must never toggle. lsfLoanDefault may only
|
||||
// transition from unset to set, which combined with ValidLoan's rule that
|
||||
// only LoanManage may change it makes the flag write-once.
|
||||
if (view.rules().enabled(featureLendingProtocolV1_1))
|
||||
{
|
||||
std::uint32_t const beforeFlags = before->getFlags();
|
||||
std::uint32_t const afterFlags = after->getFlags();
|
||||
bool const overpaymentChanged =
|
||||
(beforeFlags & lsfLoanOverpayment) != (afterFlags & lsfLoanOverpayment);
|
||||
if (overpaymentChanged)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: lsfLoanOverpayment flag "
|
||||
"toggled on immutable ledger entry in "
|
||||
<< tx.getTransactionID();
|
||||
}
|
||||
bad = bad || overpaymentChanged;
|
||||
bool const defaultCleared =
|
||||
(beforeFlags & lsfLoanDefault) != 0 && (afterFlags & lsfLoanDefault) == 0;
|
||||
if (defaultCleared)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: lsfLoanDefault flag "
|
||||
"cleared on immutable ledger entry in "
|
||||
<< tx.getTransactionID();
|
||||
}
|
||||
bad = bad || defaultCleared;
|
||||
}
|
||||
break;
|
||||
case ltVAULT:
|
||||
/*
|
||||
* sfAccount, sfAsset and sfShareMPTID are already
|
||||
* captured by VaultInvariant. The additional fields
|
||||
* below are introduced by featureLendingProtocolV1_1
|
||||
* and only exist on V1_1 vaults.
|
||||
* All the fields below are only immutable from
|
||||
* featureLendingProtocolV1_1 onwards; some of them only exist on
|
||||
* V1_1 vaults. Before that amendment, sfAsset, sfAccount and
|
||||
* sfShareMPTID are checked by VaultInvariant instead.
|
||||
*/
|
||||
if (view.rules().enabled(featureLendingProtocolV1_1))
|
||||
{
|
||||
@@ -1190,7 +1224,10 @@ NoModifiedUnmodifiableFields::finalize(
|
||||
kFieldChanged(before, after, sfOwner) ||
|
||||
kFieldChanged(before, after, sfWithdrawalPolicy) ||
|
||||
kFieldChanged(before, after, sfScale) ||
|
||||
kFieldChanged(before, after, sfLEVersion);
|
||||
kFieldChanged(before, after, sfLEVersion) ||
|
||||
kFieldChanged(before, after, sfAsset) ||
|
||||
kFieldChanged(before, after, sfAccount) ||
|
||||
kFieldChanged(before, after, sfShareMPTID);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
#include <xrpl/tx/invariants/LoanBrokerInvariant.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
@@ -22,6 +27,24 @@ namespace xrpl {
|
||||
void
|
||||
ValidLoanBroker::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
|
||||
{
|
||||
// Track LoanBroker deletions so finalize() can enforce:
|
||||
// (a) only ttLOAN_BROKER_DELETE removes a broker
|
||||
// (b) at most one broker is removed per transaction
|
||||
// (c) DebtTotal and OwnerCount were zero before deletion
|
||||
// `before` is the pre-transaction state, which is what
|
||||
// LoanBrokerDelete::preclaim reads. Erased trust lines and MPTokens need no
|
||||
// special handling here: the `if (after)` branch below already records them.
|
||||
if (isDelete && before && before->getType() == ltLOAN_BROKER)
|
||||
{
|
||||
if (deletedBroker_)
|
||||
{
|
||||
multipleBrokerDeletions_ = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
deletedBroker_ = before;
|
||||
}
|
||||
}
|
||||
if (after)
|
||||
{
|
||||
if (after->getType() == ltLOAN_BROKER)
|
||||
@@ -99,6 +122,64 @@ ValidLoanBroker::finalize(
|
||||
// Loan Brokers will not exist on ledger if the Lending Protocol amendment
|
||||
// is not enabled, so there's no need to check it.
|
||||
|
||||
// Deletion invariants (featureLendingProtocolV1_1). At most one
|
||||
// LoanBroker may be removed per transaction, and only by
|
||||
// ttLOAN_BROKER_DELETE, and only when its pre-state OwnerCount is zero and
|
||||
// its pre-state DebtTotal is zero to the precision of the vault asset. The
|
||||
// DebtTotal check complements ValidLoan's
|
||||
// LoanBrokerDelete-must-not-touch-any-loan rule: even a broker that has
|
||||
// finished paying off every loan may still hold non-zero exposure until
|
||||
// its LoanBrokerCoverWithdraw settles, and neither state is safe to
|
||||
// delete.
|
||||
if (view.rules().enabled(featureLendingProtocolV1_1))
|
||||
{
|
||||
if (multipleBrokerDeletions_)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: more than one Loan Broker deleted in a single transaction";
|
||||
return false;
|
||||
}
|
||||
if (deletedBroker_)
|
||||
{
|
||||
if (tx.getTxnType() != ttLOAN_BROKER_DELETE)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: " << //
|
||||
"Loan Broker deleted by a transaction other than LoanBrokerDelete";
|
||||
return false;
|
||||
}
|
||||
// Mirror LoanBrokerDelete::preclaim, which accepts a DebtTotal
|
||||
// that rounds to zero at the vault's AssetsTotal scale rather than
|
||||
// requiring an exact zero. Requiring more here would turn a
|
||||
// transaction the transactor deliberately permits into an
|
||||
// invariant failure.
|
||||
if (auto const debtTotal = deletedBroker_->at(sfDebtTotal); debtTotal != beast::kZero)
|
||||
{
|
||||
// The erased broker is also collected in brokers_, and that
|
||||
// loop reports a missing vault, so no separate diagnostic is
|
||||
// needed here. Without a vault there is no scale to round at,
|
||||
// so the residue cannot be excused as dust.
|
||||
auto const vault = view.read(keylet::vault(deletedBroker_->at(sfVaultID)));
|
||||
if (!vault ||
|
||||
roundToAsset(
|
||||
Asset{vault->at(sfAsset)},
|
||||
debtTotal,
|
||||
getAssetsTotalScale(vault),
|
||||
Number::RoundingMode::TowardsZero) != beast::kZero)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: Loan Broker deleted with non-zero debt total";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (deletedBroker_->at(sfOwnerCount) != 0)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: Loan Broker deleted with non-zero owner count";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (auto const& line : lines_)
|
||||
{
|
||||
for (auto const& field : {&sfLowLimit, &sfHighLimit})
|
||||
@@ -142,7 +223,6 @@ ValidLoanBroker::finalize(
|
||||
|
||||
auto const& before = broker.brokerBefore;
|
||||
|
||||
// https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3123-invariants
|
||||
// If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most
|
||||
// one node (the root), which will only hold entries for `RippleState`
|
||||
// or `MPToken` objects.
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#include <xrpl/tx/invariants/LoanInvariant.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
@@ -13,6 +16,7 @@
|
||||
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <cstdint>
|
||||
@@ -22,7 +26,14 @@ namespace xrpl {
|
||||
void
|
||||
ValidLoan::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
|
||||
{
|
||||
if (after && after->getType() == ltLOAN)
|
||||
// Classify here, but leave the decision about which checks apply to
|
||||
// finalize(), which is the only place that can see the Rules.
|
||||
if (isDelete)
|
||||
{
|
||||
if (before && before->getType() == ltLOAN)
|
||||
deletedLoans_.emplace_back(before, after);
|
||||
}
|
||||
else if (after && after->getType() == ltLOAN)
|
||||
{
|
||||
loans_.emplace_back(before, after);
|
||||
}
|
||||
@@ -39,12 +50,23 @@ ValidLoan::finalize(
|
||||
// Loans will not exist on ledger if the Lending Protocol amendment
|
||||
// is not enabled, so there's no need to check it.
|
||||
|
||||
auto const txType = tx.getTxnType();
|
||||
bool const lpV11Enabled = view.rules().enabled(featureLendingProtocolV1_1);
|
||||
|
||||
// Without featureLendingProtocolV1_1 an erased Loan is subject to the same
|
||||
// per-entry checks as any modified Loan. From V1_1 onward it is only subject
|
||||
// to the ttLOAN_DELETE check below.
|
||||
if (!lpV11Enabled)
|
||||
loans_.insert(loans_.end(), deletedLoans_.begin(), deletedLoans_.end());
|
||||
|
||||
// Ledger entry validation checks.
|
||||
for (auto const& [before, after] : loans_)
|
||||
{
|
||||
// A closed-ended vault must not accept a loan whose final scheduled payment falls on or
|
||||
// after the vault's RedemptionDate. This mirrors the LoanSet::preclaim gate and only fires
|
||||
// on loan creation; once the loan exists, its StartDate / PaymentInterval are immutable and
|
||||
// PaymentRemaining only decreases, so the bound is preserved.
|
||||
// A closed-ended vault must not accept a loan whose final scheduled payment falls fewer
|
||||
// than kLoanRedemptionBuffer seconds before the vault's RedemptionDate. This mirrors the
|
||||
// LoanSet::preclaim gate and only fires on loan creation; once the loan exists, its
|
||||
// StartDate / PaymentInterval are immutable and PaymentRemaining only decreases, so the
|
||||
// bound is preserved.
|
||||
if (!before && isTesSuccess(result))
|
||||
{
|
||||
auto const broker = view.read(keylet::loanBroker(after->at(sfLoanBrokerID)));
|
||||
@@ -59,11 +81,13 @@ ValidLoan::finalize(
|
||||
std::uint32_t const interval = after->at(sfPaymentInterval);
|
||||
std::uint32_t const remaining = after->at(sfPaymentRemaining);
|
||||
std::uint32_t const redemption = vault->at(sfRedemptionDate);
|
||||
if (std::uint64_t{startDate} + (std::uint64_t{interval} * remaining) >=
|
||||
if (std::uint64_t{startDate} + (std::uint64_t{interval} * remaining) +
|
||||
kLoanRedemptionBuffer >
|
||||
redemption)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: closed-ended loan final payment "
|
||||
"must precede RedemptionDate";
|
||||
"must precede RedemptionDate by at least "
|
||||
"kLoanRedemptionBuffer";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -91,7 +115,11 @@ ValidLoan::finalize(
|
||||
JLOG(j.fatal()) << "Invariant failed: Fully paid off Loan still has payments remaining";
|
||||
return false;
|
||||
}
|
||||
if (before && (before->isFlag(lsfLoanOverpayment) != after->isFlag(lsfLoanOverpayment)))
|
||||
|
||||
// From featureLendingProtocolV1_1 onwards this flag is immutable by way of
|
||||
// NoModifiedUnmodifiableFields.
|
||||
if (!lpV11Enabled && before &&
|
||||
(before->isFlag(lsfLoanOverpayment) != after->isFlag(lsfLoanOverpayment)))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: Loan Overpayment flag changed";
|
||||
return false;
|
||||
@@ -123,6 +151,125 @@ ValidLoan::finalize(
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (lpV11Enabled)
|
||||
{
|
||||
// Only LoanSet may create a loan.
|
||||
if (!before && txType != ttLOAN_SET)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: Loan created by a transaction "
|
||||
"other than LoanSet";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (after->at(sfPaymentRemaining) == 0 &&
|
||||
after->at(~sfNextPaymentDueDate).value_or(0) != 0)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: Loan with zero payments must have zero next "
|
||||
"payment due date";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (before)
|
||||
{
|
||||
bool const wasImpaired = before->isFlag(lsfLoanImpaired);
|
||||
bool const isImpaired = after->isFlag(lsfLoanImpaired);
|
||||
bool const wasDefaulted = before->isFlag(lsfLoanDefault);
|
||||
bool const isDefaulted = after->isFlag(lsfLoanDefault);
|
||||
|
||||
if (wasImpaired != isImpaired && txType != ttLOAN_MANAGE && txType != ttLOAN_PAY)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: lsfLoanImpaired changed "
|
||||
"outside LoanManage or LoanPay";
|
||||
return false;
|
||||
}
|
||||
if (wasDefaulted != isDefaulted && txType != ttLOAN_MANAGE)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: lsfLoanDefault changed "
|
||||
"outside LoanManage";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// A loan must reference a live loan broker, and that broker must
|
||||
// reference a live vault; otherwise the loan is orphaned and its
|
||||
// balances have no counterparty on the ledger.
|
||||
auto const brokerSle = view.read(keylet::loanBroker(after->at(sfLoanBrokerID)));
|
||||
if (!brokerSle)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: Loan broker does not exist";
|
||||
return false;
|
||||
}
|
||||
auto const vaultSle = view.read(keylet::vault(brokerSle->at(sfVaultID)));
|
||||
if (!vaultSle)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: Loan broker vault does not exist";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Interest due (the total value owed less principal and management fee)
|
||||
// must never be negative. TotalValueOutstanding, PrincipalOutstanding and
|
||||
// ManagementFeeOutstanding are each independently rounded to sfLoanScale
|
||||
// by the accounting code, so their difference can carry one unit of
|
||||
// quantization noise even when the underlying flow is correct. Absorb
|
||||
// one unit at that scale, matching the pattern used in ValidVault.
|
||||
auto const interestDue = after->at(sfTotalValueOutstanding) -
|
||||
after->at(sfPrincipalOutstanding) - after->at(sfManagementFeeOutstanding);
|
||||
|
||||
// Only IOU amounts can accumulate STAmount quantization noise. For integral-domain
|
||||
// assets (XRP/MPT) enforce the boundary strictly.
|
||||
bool const integral = Asset{vaultSle->at(sfAsset)}.integral();
|
||||
|
||||
Number const tolerance = integral ? Number{} : Number{-1, after->at(sfLoanScale)};
|
||||
if (interestDue < tolerance)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: Loan interest due is negative";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Transaction success post-conditions. A successful loan pay makes at least
|
||||
// one scheduled payment, so a loan left with payments still outstanding
|
||||
// must show that payment in its balance and schedule. A payment that clears
|
||||
// the loan outright instead drives PaymentRemaining to zero, which the
|
||||
// fully-paid-off and zero due-date checks above pin.
|
||||
if (isTesSuccess(result) && txType == ttLOAN_PAY)
|
||||
{
|
||||
if (before && after->at(sfPaymentRemaining) != 0)
|
||||
{
|
||||
if (!(after->at(sfPrincipalOutstanding) < before->at(sfPrincipalOutstanding)))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: loan pay must strictly decrease "
|
||||
"PrincipalOutstanding on a non-full-repayment";
|
||||
return false;
|
||||
}
|
||||
if (!(after->at(sfPaymentRemaining) < before->at(sfPaymentRemaining)))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: loan pay must decrease "
|
||||
"PaymentRemaining on a non-full-repayment";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::uint32_t const beforeDue = before->at(~sfNextPaymentDueDate).value_or(0);
|
||||
std::uint32_t const afterDue = after->at(~sfNextPaymentDueDate).value_or(0);
|
||||
std::uint32_t const interval = after->at(sfPaymentInterval);
|
||||
if (afterDue <= beforeDue || interval == 0 ||
|
||||
(afterDue - beforeDue) % interval != 0)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: loan pay must advance "
|
||||
"NextPaymentDueDate by a positive multiple of "
|
||||
"PaymentInterval on a non-full-repayment";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only LoanDelete may delete a loan.
|
||||
if (lpV11Enabled && txType != ttLOAN_DELETE && !deletedLoans_.empty())
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: Loan deleted by a transaction "
|
||||
"other than LoanDelete";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -234,14 +234,6 @@ ValidMPTIssuance::finalize(
|
||||
|
||||
if (hasPrivilege(tx, Privilege::DestroyMptIssuance))
|
||||
{
|
||||
// A VaultDelete that is still cleaning up credentials pinned to its
|
||||
// pseudo-account returns tecINCOMPLETE and has not yet reached the
|
||||
// share issuance. Don't require the issuance to be removed until
|
||||
// the deletion completes (a later transaction).
|
||||
if (rules.enabled(fixCleanup3_4_0) && txnType == ttVAULT_DELETE &&
|
||||
result == tecINCOMPLETE)
|
||||
return mptIssuancesDeleted_ == 0 && mptIssuancesCreated_ == 0;
|
||||
|
||||
if (mptIssuancesDeleted_ == 0)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion "
|
||||
@@ -832,6 +824,14 @@ ValidMPTTransfer::visitEntry(
|
||||
|
||||
if (after)
|
||||
update(*after, false);
|
||||
|
||||
// Record whether every touched AccountRoot was a pseudo-account BEFORE
|
||||
// the transaction applied (true and false). A transaction that erases a
|
||||
// pseudo-account (and moves MPT out of it) in the same transaction leaves
|
||||
// no trace of its pseudo-account status in the post-transaction view
|
||||
// isAuthorized() sees at finalize() time.
|
||||
if (before && before->getType() == ltACCOUNT_ROOT)
|
||||
pseudoAccountsBefore_[before->at(sfAccount)] = isPseudoAccount(before);
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -844,10 +844,19 @@ ValidMPTTransfer::isAuthorized(
|
||||
// Pseudo-accounts (Vault, LoanBroker, AMM) hold assets on behalf of their
|
||||
// participants and are implicitly authorized for any MPT they hold,
|
||||
// including vault shares whose underlying asset would otherwise require
|
||||
// auth. Exempt them here rather than relying on requireAuth: the recursive
|
||||
// auth. Exempt them here rather than relying on requireAuth: the recursive
|
||||
// share -> underlying descent in requireAuth fails for a pseudo-account
|
||||
// that holds the share but not the underlying.
|
||||
if (isPseudoAccount(view, holder, {&sfVaultID, &sfLoanBrokerID, &sfAMMID}))
|
||||
//
|
||||
// Use the pre-transaction classification for any account this
|
||||
// transaction touched (pseudoAccountsBefore_): the post-transaction view
|
||||
// is wrong for an account this same transaction erased. Untouched
|
||||
// accounts aren't in the map, so fall back to the current view, which is
|
||||
// still accurate for them since nothing changed.
|
||||
auto const pseudoIt = pseudoAccountsBefore_.find(holder);
|
||||
bool const isPseudo =
|
||||
pseudoIt != pseudoAccountsBefore_.end() ? pseudoIt->second : isPseudoAccount(view, holder);
|
||||
if (isPseudo)
|
||||
return true;
|
||||
|
||||
auto const key = keylet::mptoken(mptid, holder);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
@@ -20,6 +21,7 @@
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -234,21 +236,57 @@ ValidVault::deltaAssets(AccountID const& id) const
|
||||
vaultAsset.value());
|
||||
}
|
||||
|
||||
std::optional<AccountID>
|
||||
ValidVault::feePayerAccountRoot(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
auto const feePayer = Transactor::getFeePayer(view, tx);
|
||||
if (feePayer.type == FeePayerType::SponsorPreFunded)
|
||||
return std::nullopt;
|
||||
return feePayer.id;
|
||||
}
|
||||
|
||||
std::optional<ValidVault::DeltaInfo>
|
||||
ValidVault::deltaAssetsTxAccount(STTx const& tx, XRPAmount fee) const
|
||||
ValidVault::deltaAssetsForParty(
|
||||
ReadView const& view,
|
||||
AccountID const& id,
|
||||
STTx const& tx,
|
||||
XRPAmount fee,
|
||||
bool fix340Enabled) const
|
||||
{
|
||||
auto const& vaultAsset = afterVault_[0].asset;
|
||||
auto ret = deltaAssets(tx[sfAccount]);
|
||||
auto ret = deltaAssets(id);
|
||||
if (!ret.has_value() || !vaultAsset.native())
|
||||
return ret;
|
||||
|
||||
// Only add the fee back if tx[sfAccount] actually paid it. When the fee is
|
||||
// paid by someone else (a delegate or a fee sponsor), the
|
||||
// account's XRP balance moved only by the vault amount.
|
||||
if (tx.getFeePayerID() != tx[sfAccount])
|
||||
return ret;
|
||||
if (!fix340Enabled)
|
||||
{
|
||||
// Legacy behaviour: only tx[sfAccount] was ever considered for a fee
|
||||
// correction, and only when STTx::getFeePayerID identified it as the
|
||||
// fee payer (which is never true for a sponsor, since
|
||||
// self-sponsorship is disallowed). After that sender-only correction
|
||||
// a zero delta is collapsed to absence; if the correction does not
|
||||
// apply, a present-zero is returned as-is.
|
||||
if (id != tx[sfAccount] || tx.getFeePayerID() != id)
|
||||
return ret;
|
||||
|
||||
ret->delta += fee.drops();
|
||||
ret->delta += fee.drops();
|
||||
if (ret->delta == kZero)
|
||||
return std::nullopt;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Add the fee back only onto the AccountRoot that actually paid it: an
|
||||
// ordinary sender, a delegate, or a co-signed fee sponsor -- but never a
|
||||
// pre-funded sponsorship, whose fee is drawn from the ltSponsorship
|
||||
// object rather than the sponsor's own XRP balance.
|
||||
if (auto const payer = feePayerAccountRoot(view, tx); payer && *payer == id)
|
||||
ret->delta += fee.drops();
|
||||
|
||||
// Normalize an economically zero delta to absence regardless of who (if
|
||||
// anyone) paid the fee, so a touched-but-unchanged AccountRoot (e.g. the
|
||||
// sender in a third-party withdrawal, touched only for sequence/ticket
|
||||
// processing) is never misread as a second payout recipient.
|
||||
if (ret->delta == kZero)
|
||||
return std::nullopt;
|
||||
|
||||
@@ -305,6 +343,45 @@ ValidVault::finalizeLoanSet(ReadView const& view, beast::Journal const& j) const
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// sfAssetsTotal, sfAssetsAvailable and sfLossUnrealized are STNumber fields
|
||||
// with kSmdNeedsAsset, so IOU writes go through associateAsset -> roundToAsset
|
||||
// -> STAmount quantization. Since assetsTotal is the largest number, it lands
|
||||
// on the coarsest decimal grid, and strict equality on the deltas can fire on
|
||||
// a single unit of quantization noise even when the underlying flow is
|
||||
// correct. Absorb one unit at the coarsest scale.
|
||||
//
|
||||
// XRP and MPT are integer-domain assets (Asset::integral() is true) with no
|
||||
// sub-ULP quantization; treating a whole drop / MPT unit as "noise" would
|
||||
// hide real accounting bugs. Keep the strict comparison there. Note that
|
||||
// gating on the sign of `scale` would be wrong: IOU amounts >= 1e15 have a
|
||||
// non-negative STAmount exponent but still quantize.
|
||||
[[nodiscard]] bool
|
||||
agreesWithinOneUnit(Number const& lhs, Number const& rhs, Asset const& asset, std::int32_t scale)
|
||||
{
|
||||
if (asset.integral())
|
||||
return lhs == rhs;
|
||||
auto const diff = lhs - rhs;
|
||||
Number const tolerance{1, scale};
|
||||
return (diff < beast::kZero ? -diff : diff) <= tolerance;
|
||||
}
|
||||
|
||||
// L, T and A are each independently quantized; the strict L <= T - A check
|
||||
// can fire on residual noise even when the true relationship holds. Tolerate
|
||||
// one unit at scale(assetsTotal) - the coarsest of the three grids. As with
|
||||
// the delta check above, the tolerance is meaningful only for IOU
|
||||
// (Asset::integral() is false); XRP and MPT keep the strict comparison.
|
||||
[[nodiscard]] bool
|
||||
lessOrEqualPlusOneUnit(Number const& lhs, Number const& rhs, Asset const& asset, std::int32_t scale)
|
||||
{
|
||||
if (asset.integral())
|
||||
return lhs <= rhs;
|
||||
return lhs <= rhs + Number{1, scale};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::int32_t
|
||||
ValidVault::computeVaultMinScale(DeltaInfo const& vaultDelta, Rules const& rules) const
|
||||
{
|
||||
@@ -340,6 +417,7 @@ ValidVault::finalize(
|
||||
beast::Journal const& j)
|
||||
{
|
||||
bool const enforce = view.rules().enabled(featureSingleAssetVault);
|
||||
bool const fix340Enabled = view.rules().enabled(fixCleanup3_4_0);
|
||||
|
||||
if (!isTesSuccess(ret))
|
||||
return true; // Do not perform checks
|
||||
@@ -474,7 +552,8 @@ ValidVault::finalize(
|
||||
bool result = true;
|
||||
|
||||
// Universal transaction checks
|
||||
if (!beforeVault_.empty())
|
||||
// From LendingProtocolV1_1 onwards, vault immutability check is moved to InvariantCheck.cpp
|
||||
if (!beforeVault_.empty() && !view.rules().enabled(featureLendingProtocolV1_1))
|
||||
{
|
||||
auto const& beforeVault = beforeVault_[0];
|
||||
if (afterVault.asset != beforeVault.asset || afterVault.pseudoId != beforeVault.pseudoId ||
|
||||
@@ -527,15 +606,32 @@ ValidVault::finalize(
|
||||
"not be greater than assets outstanding";
|
||||
result = false;
|
||||
}
|
||||
else if (afterVault.lossUnrealized > afterVault.assetsTotal - afterVault.assetsAvailable)
|
||||
else
|
||||
{
|
||||
JLOG(j.fatal()) //
|
||||
<< "Invariant failed: loss unrealized must not exceed "
|
||||
"the difference between assets outstanding and available";
|
||||
result = false;
|
||||
bool const gapExceeded = [&] {
|
||||
if (!fix340Enabled)
|
||||
{
|
||||
return afterVault.lossUnrealized >
|
||||
afterVault.assetsTotal - afterVault.assetsAvailable;
|
||||
}
|
||||
|
||||
auto const s = scale(afterVault.assetsTotal, afterVault.asset);
|
||||
return !lessOrEqualPlusOneUnit(
|
||||
afterVault.lossUnrealized,
|
||||
afterVault.assetsTotal - afterVault.assetsAvailable,
|
||||
afterVault.asset,
|
||||
s);
|
||||
}();
|
||||
if (gapExceeded)
|
||||
{
|
||||
JLOG(j.fatal()) //
|
||||
<< "Invariant failed: loss unrealized must not exceed "
|
||||
"the difference between assets outstanding and available";
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (view.rules().enabled(fixCleanup3_4_0) && afterVault.lossUnrealized < kZero)
|
||||
if (fix340Enabled && afterVault.lossUnrealized < kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: loss unrealized must not be negative";
|
||||
result = false;
|
||||
@@ -706,8 +802,13 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
// AssetsTotal may exceed AssetsMaximum when the excess is interest. After
|
||||
// fixCleanup3_4_0, only reject a VaultSet that supplies sfAssetsMaximum or
|
||||
// otherwise changes the cap to a nonzero value still below AssetsTotal.
|
||||
if (afterVault.assetsMaximum > kZero &&
|
||||
afterVault.assetsTotal > afterVault.assetsMaximum)
|
||||
afterVault.assetsTotal > afterVault.assetsMaximum &&
|
||||
(!fix340Enabled || tx.isFieldPresent(sfAssetsMaximum) ||
|
||||
beforeVault.assetsMaximum != afterVault.assetsMaximum))
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: set assets outstanding must not "
|
||||
@@ -796,7 +897,8 @@ ValidVault::finalize(
|
||||
|
||||
if (!issuerDeposit)
|
||||
{
|
||||
auto const maybeAccDeltaAssets = deltaAssetsTxAccount(tx, fee);
|
||||
auto const maybeAccDeltaAssets =
|
||||
deltaAssetsForParty(view, tx[sfAccount], tx, fee, fix340Enabled);
|
||||
if (!maybeAccDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
@@ -821,7 +923,14 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (localVaultDeltaAssets * -1 != accountDeltaAssets)
|
||||
bool const acctVaultAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
localVaultDeltaAssets * -1,
|
||||
accountDeltaAssets,
|
||||
vaultAsset,
|
||||
localMinScale)
|
||||
: localVaultDeltaAssets * -1 == accountDeltaAssets;
|
||||
if (!acctVaultAddsUp)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: " << //
|
||||
"deposit must change vault and depositor balance by equal amount";
|
||||
@@ -869,7 +978,10 @@ ValidVault::finalize(
|
||||
|
||||
auto const assetTotalDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
|
||||
if (assetTotalDelta != vaultDeltaAssets)
|
||||
bool const totalAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(assetTotalDelta, vaultDeltaAssets, vaultAsset, minScale)
|
||||
: assetTotalDelta == vaultDeltaAssets;
|
||||
if (!totalAddsUp)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: deposit and assets outstanding must add up";
|
||||
@@ -878,7 +990,11 @@ ValidVault::finalize(
|
||||
|
||||
auto const assetAvailableDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
|
||||
if (assetAvailableDelta != vaultDeltaAssets)
|
||||
bool const availableAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
assetAvailableDelta, vaultDeltaAssets, vaultAsset, minScale)
|
||||
: assetAvailableDelta == vaultDeltaAssets;
|
||||
if (!availableAddsUp)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: deposit and assets available must add up";
|
||||
result = false;
|
||||
@@ -920,8 +1036,8 @@ ValidVault::finalize(
|
||||
// value merely rounds down to zero, so a missing delta while
|
||||
// the pool still held positive effective value indicates a
|
||||
// real accounting bug, not this exception.
|
||||
bool const zeroDeltaIsLegitimate = view.rules().enabled(fixCleanup3_4_0) &&
|
||||
!maybeVaultDeltaAssets && beforeVault.assetsTotal == beforeVault.lossUnrealized;
|
||||
bool const zeroDeltaIsLegitimate = fix340Enabled && !maybeVaultDeltaAssets &&
|
||||
beforeVault.assetsTotal == beforeVault.lossUnrealized;
|
||||
|
||||
if (!maybeVaultDeltaAssets && !zeroDeltaIsLegitimate)
|
||||
{
|
||||
@@ -955,21 +1071,39 @@ ValidVault::finalize(
|
||||
|
||||
if (!issuerWithdrawal)
|
||||
{
|
||||
auto const maybeAccDelta = deltaAssetsTxAccount(tx, fee);
|
||||
auto const maybeOtherAccDelta = [&]() -> std::optional<DeltaInfo> {
|
||||
if (auto const destination = tx[~sfDestination];
|
||||
destination && *destination != tx[sfAccount])
|
||||
return deltaAssets(*destination);
|
||||
return std::nullopt;
|
||||
}();
|
||||
// Identify the intended recipient explicitly from
|
||||
// sfDestination (falling back to sfAccount for a
|
||||
// self-withdrawal), rather than inferring it from which
|
||||
// side happens to show a delta. When a distinct
|
||||
// destination is named, the sending account must not
|
||||
// also show a real economic delta -- that would mean two
|
||||
// accounts were paid, which is always a bug, regardless
|
||||
// of what (if anything) the named destination received.
|
||||
auto const destinationField = tx[~sfDestination];
|
||||
AccountID const recipient = destinationField.value_or(tx[sfAccount]);
|
||||
bool const distinctDestination =
|
||||
destinationField.has_value() && *destinationField != tx[sfAccount];
|
||||
|
||||
if (maybeAccDelta.has_value() == maybeOtherAccDelta.has_value())
|
||||
// Intentionally ungated: `fix340Enabled &&` here would let the
|
||||
// pre-amendment sponsored case succeed and change consensus.
|
||||
if (distinctDestination &&
|
||||
deltaAssetsForParty(view, tx[sfAccount], tx, fee, fix340Enabled)
|
||||
.has_value())
|
||||
{
|
||||
// Both changed is always a bug. Neither changed is
|
||||
// consistent only with a legitimate zero-value
|
||||
// withdrawal, which moves nothing on either side —
|
||||
// there is nothing left to cross-check.
|
||||
if (!zeroDeltaIsLegitimate || maybeAccDelta.has_value())
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: withdrawal must change one destination balance";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto const maybeRecipientDelta =
|
||||
deltaAssetsForParty(view, recipient, tx, fee, fix340Enabled);
|
||||
|
||||
if (!maybeRecipientDelta.has_value())
|
||||
{
|
||||
// A legitimate zero-value withdrawal moves nothing to
|
||||
// the recipient either; there is nothing left to
|
||||
// cross-check.
|
||||
if (!zeroDeltaIsLegitimate)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: withdrawal must change one destination balance";
|
||||
@@ -981,8 +1115,7 @@ ValidVault::finalize(
|
||||
// A one-sided change is cross-checked even for a
|
||||
// legitimate zero vault delta: the destination must
|
||||
// then have moved by (rounded) zero as well.
|
||||
auto const destinationDelta =
|
||||
*maybeAccDelta.or_else([&] { return maybeOtherAccDelta; });
|
||||
auto const destinationDelta = *maybeRecipientDelta;
|
||||
|
||||
// the scale of destinationDelta can be coarser than
|
||||
// minScale, so we take that into account when rounding
|
||||
@@ -996,9 +1129,7 @@ ValidVault::finalize(
|
||||
// only. If the receiver's trust line sits at a coarser scale, the inflow
|
||||
// may safely round down to zero.
|
||||
//
|
||||
// XRP and MPT remain strict. Because they are integer-exact, a zero
|
||||
// destination delta indicates a true accounting bug, not a rounding
|
||||
// artifact.
|
||||
// XRP and MPT remain strict for rounding artifacts.
|
||||
bool const tolerateZeroDelta =
|
||||
view.rules().enabled(fixCleanup3_2_0) && !vaultAsset.integral();
|
||||
auto const invalidBalanceChange = tolerateZeroDelta
|
||||
@@ -1027,8 +1158,14 @@ ValidVault::finalize(
|
||||
vaultDeltaAssets.delta * -1 - destinationDelta.delta,
|
||||
destinationScale,
|
||||
Number::RoundingMode::Downward) == kZero;
|
||||
if (!destroyedIsSubUlp &&
|
||||
localPseudoDeltaAssets * -1 != roundedDestinationDelta)
|
||||
bool const withdrawAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
localPseudoDeltaAssets * -1,
|
||||
roundedDestinationDelta,
|
||||
vaultAsset,
|
||||
localMinScale)
|
||||
: localPseudoDeltaAssets * -1 == roundedDestinationDelta;
|
||||
if (!destroyedIsSubUlp && !withdrawAddsUp)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: " << //
|
||||
"withdrawal must change vault and destination balance by equal "
|
||||
@@ -1071,7 +1208,11 @@ ValidVault::finalize(
|
||||
auto const assetTotalDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
|
||||
// Note, vaultBalance is negative (see check above)
|
||||
if (assetTotalDelta != vaultPseudoDeltaAssets)
|
||||
bool const totalAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
assetTotalDelta, vaultPseudoDeltaAssets, vaultAsset, minScale)
|
||||
: assetTotalDelta == vaultPseudoDeltaAssets;
|
||||
if (!totalAddsUp)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: withdrawal and assets outstanding must add up";
|
||||
@@ -1081,7 +1222,11 @@ ValidVault::finalize(
|
||||
auto const assetAvailableDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
|
||||
|
||||
if (assetAvailableDelta != vaultPseudoDeltaAssets)
|
||||
bool const availableAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
assetAvailableDelta, vaultPseudoDeltaAssets, vaultAsset, minScale)
|
||||
: assetAvailableDelta == vaultPseudoDeltaAssets;
|
||||
if (!availableAddsUp)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: withdrawal and assets available must add up";
|
||||
@@ -1126,7 +1271,11 @@ ValidVault::finalize(
|
||||
|
||||
auto const assetsTotalDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
|
||||
if (assetsTotalDelta != vaultDeltaAssets)
|
||||
bool const totalAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
assetsTotalDelta, vaultDeltaAssets, vaultAsset, minScale)
|
||||
: assetsTotalDelta == vaultDeltaAssets;
|
||||
if (!totalAddsUp)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback and assets outstanding must add up";
|
||||
@@ -1137,7 +1286,11 @@ ValidVault::finalize(
|
||||
vaultAsset,
|
||||
afterVault.assetsAvailable - beforeVault.assetsAvailable,
|
||||
minScale);
|
||||
if (assetAvailableDelta != vaultDeltaAssets)
|
||||
bool const availableAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
assetAvailableDelta, vaultDeltaAssets, vaultAsset, minScale)
|
||||
: assetAvailableDelta == vaultDeltaAssets;
|
||||
if (!availableAddsUp)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback and assets available must add up";
|
||||
|
||||
@@ -1500,6 +1500,13 @@ template <class TIn, class TOut, class TDerived>
|
||||
bool
|
||||
BookStep<TIn, TOut, TDerived>::checkMPTDEX(ReadView const& view, AccountID const& owner) const
|
||||
{
|
||||
// Offer-owner locks on book_.in and book_.out are handled by the
|
||||
// liquidity sources before an offer reaches this point. OfferStream
|
||||
// filters CLOB offers through the assetIn deep-freeze check and the
|
||||
// assetOut owner-funds check using FreezeHandling::ZeroIfFrozen, while
|
||||
// AMMLiquidity gets pool balances through ammAccountHolds(), which zeroes
|
||||
// locked holdings. This method only enforces MPT trade and transfer
|
||||
// permissions.
|
||||
if (!isTesSuccess(canTrade(view, book_.in)) || !isTesSuccess(canTrade(view, book_.out)))
|
||||
return false;
|
||||
|
||||
@@ -1513,14 +1520,8 @@ BookStep<TIn, TOut, TDerived>::checkMPTDEX(ReadView const& view, AccountID const
|
||||
// Offer's owner is an issuer
|
||||
if (asset.getIssuer() == owner)
|
||||
return true;
|
||||
// The previous step could be MPTEndpointStep with non issuer account or
|
||||
// BookStep. Fail both if in asset is locked. In the former case it is holder
|
||||
// to locked holder transfer. In the latter case it is not possible to tell if
|
||||
// it is issuer to holder or holder to holder transfer.
|
||||
if (isFrozen(view, owner, book_.in.get<MPTIssue>()))
|
||||
return false;
|
||||
// Previous step is BookStep. BookStep only sends if CanTransfer is
|
||||
// set and not locked or the offer is owned by an issuer
|
||||
// Previous BookStep already enforced transferability for the asset
|
||||
// it sends to this offer.
|
||||
if (prevStep_->bookStepBook())
|
||||
return true;
|
||||
// Previous step is MPTEndpointStep and offer's owner is not an
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/MPTAmount.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/Quality.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
@@ -89,6 +90,13 @@ protected:
|
||||
void
|
||||
resetCache(DebtDirection dir);
|
||||
|
||||
[[nodiscard]] TER
|
||||
sendWithMPTCreate(
|
||||
ApplyView& view,
|
||||
AccountID const& src,
|
||||
AccountID const& dst,
|
||||
MPTAmount const& amount);
|
||||
|
||||
private:
|
||||
MPTEndpointStep(
|
||||
StrandContext const& ctx,
|
||||
@@ -274,7 +282,7 @@ public:
|
||||
|
||||
// Not applicable for payment
|
||||
static TER
|
||||
checkCreateMPT(ApplyView&, DebtDirection)
|
||||
checkCreateMPT(ApplyView&)
|
||||
{
|
||||
return tesSUCCESS;
|
||||
}
|
||||
@@ -322,7 +330,7 @@ public:
|
||||
|
||||
// Can be created in rev or fwd (if limiting step) direction.
|
||||
TER
|
||||
checkCreateMPT(ApplyView& view, DebtDirection srcDebtDir);
|
||||
checkCreateMPT(ApplyView& view);
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -401,7 +409,7 @@ MPTEndpointOfferCrossingStep::check(StrandContext const& ctx, SLE::const_ref)
|
||||
}
|
||||
|
||||
TER
|
||||
MPTEndpointOfferCrossingStep::checkCreateMPT(ApplyView& view, xrpl::DebtDirection srcDebtDir)
|
||||
MPTEndpointOfferCrossingStep::checkCreateMPT(ApplyView& view)
|
||||
{
|
||||
// TakerPays is the last step if offer crossing
|
||||
if (isLast_)
|
||||
@@ -412,9 +420,14 @@ MPTEndpointOfferCrossingStep::checkCreateMPT(ApplyView& view, xrpl::DebtDirectio
|
||||
// crossed. See CreateOffer::applyGuts() for reserve check.
|
||||
if (auto const err = xrpl::checkCreateMPT(view, mptIssue_, dst_, j_); !isTesSuccess(err))
|
||||
{
|
||||
// Unreachable: offer-crossing checks reject an offer whose owner
|
||||
// could fail to create the MPToken.
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE(
|
||||
"xrpl::MPTEndpointOfferCrossingStep::checkCreateMPT : create MPToken failed");
|
||||
JLOG(j_.trace()) << "MPTEndpointStep::checkCreateMPT: failed create MPT";
|
||||
resetCache(srcDebtDir);
|
||||
return err;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
}
|
||||
return tesSUCCESS;
|
||||
@@ -422,6 +435,30 @@ MPTEndpointOfferCrossingStep::checkCreateMPT(ApplyView& view, xrpl::DebtDirectio
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
template <class TDerived>
|
||||
TER
|
||||
MPTEndpointStep<TDerived>::sendWithMPTCreate(
|
||||
ApplyView& view,
|
||||
AccountID const& src,
|
||||
AccountID const& dst,
|
||||
MPTAmount const& amount)
|
||||
{
|
||||
// Only offer crossing can fail here (payment checkCreateMPT is a no-op),
|
||||
// via the unreachable path excluded in checkCreateMPT() above.
|
||||
if (auto const err = static_cast<TDerived*>(this)->checkCreateMPT(view); !isTesSuccess(err))
|
||||
return err; // LCOV_EXCL_LINE
|
||||
|
||||
return directSendNoFee(
|
||||
view,
|
||||
src,
|
||||
dst,
|
||||
toSTAmount(amount, mptIssue_),
|
||||
/*checkIssuer*/ false,
|
||||
j_);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
template <class TDerived>
|
||||
std::pair<MPTAmount, DebtDirection>
|
||||
MPTEndpointStep<TDerived>::maxPaymentFlow(ReadView const& sb) const
|
||||
@@ -478,8 +515,6 @@ MPTEndpointStep<TDerived>::revImp(
|
||||
auto const [srcQOut, dstQIn] = qualities(sb, srcDebtDir, StrandDirection::Reverse);
|
||||
(void)dstQIn;
|
||||
|
||||
MPTIssue const srcToDstIss(mptIssue_);
|
||||
|
||||
JLOG(j_.trace()) << "MPTEndpointStep::rev"
|
||||
<< " srcRedeems: " << redeems(srcDebtDir) << " outReq: " << to_string(out)
|
||||
<< " maxSrcToDst: " << to_string(maxSrcToDst) << " srcQOut: " << srcQOut
|
||||
@@ -492,59 +527,41 @@ MPTEndpointStep<TDerived>::revImp(
|
||||
return {beast::kZero, beast::kZero};
|
||||
}
|
||||
|
||||
if (auto const err = static_cast<TDerived*>(this)->checkCreateMPT(sb, srcDebtDir);
|
||||
!isTesSuccess(err))
|
||||
return {beast::kZero, beast::kZero};
|
||||
// When a previous step feeds this issuing step, srcQOut is the issuer's
|
||||
// transfer rate and maxPaymentFlow() returns the issuance maximum rather
|
||||
// than a real limit, so srcToDst * srcQOut need not be representable. Cap
|
||||
// srcToDst at the largest amount whose input is; the previous step then
|
||||
// limits the flow to what the source actually holds.
|
||||
MPTAmount const maxRepresentable =
|
||||
mulRatio(MPTAmount(kMaxMpTokenAmount), QUALITY_ONE, srcQOut, /*roundUp*/ false);
|
||||
|
||||
// Don't have to factor in dstQIn since it is always QUALITY_ONE
|
||||
MPTAmount const srcToDst = out;
|
||||
MPTAmount const srcToDst = std::min({out, maxSrcToDst, maxRepresentable});
|
||||
|
||||
if (srcToDst <= maxSrcToDst)
|
||||
{
|
||||
MPTAmount const in = mulRatio(srcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true);
|
||||
cache_.emplace(in, srcToDst, srcToDst, srcDebtDir);
|
||||
auto const ter = directSendNoFee(
|
||||
sb,
|
||||
src_,
|
||||
dst_,
|
||||
toSTAmount(srcToDst, srcToDstIss),
|
||||
/*checkIssuer*/ false,
|
||||
j_);
|
||||
if (!isTesSuccess(ter))
|
||||
{
|
||||
JLOG(j_.trace()) << "MPTEndpointStep::rev: error " << ter;
|
||||
resetCache(srcDebtDir);
|
||||
return {beast::kZero, beast::kZero};
|
||||
}
|
||||
JLOG(j_.trace()) << "MPTEndpointStep::rev: Non-limiting"
|
||||
<< " srcRedeems: " << redeems(srcDebtDir) << " in: " << to_string(in)
|
||||
<< " srcToDst: " << to_string(srcToDst) << " out: " << to_string(out);
|
||||
return {in, out};
|
||||
}
|
||||
// Can't overflow: srcToDst <= kMaxMpTokenAmount * QUALITY_ONE / srcQOut,
|
||||
// so the rounded up product is at most kMaxMpTokenAmount.
|
||||
MPTAmount const in = mulRatio(srcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true);
|
||||
|
||||
// limiting node
|
||||
MPTAmount const in = mulRatio(maxSrcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true);
|
||||
// Don't have to factor in dsqQIn since it's always QUALITY_ONE
|
||||
MPTAmount const actualOut = maxSrcToDst;
|
||||
cache_.emplace(in, maxSrcToDst, actualOut, srcDebtDir);
|
||||
cache_.emplace(in, srcToDst, srcToDst, srcDebtDir);
|
||||
|
||||
auto const ter = directSendNoFee(
|
||||
sb,
|
||||
src_,
|
||||
dst_,
|
||||
toSTAmount(maxSrcToDst, srcToDstIss),
|
||||
/*checkIssuer*/ false,
|
||||
j_);
|
||||
auto const ter = sendWithMPTCreate(sb, src_, dst_, srcToDst);
|
||||
if (!isTesSuccess(ter))
|
||||
{
|
||||
// Unreachable: send fails only on funds/auth/overflow, precluded by
|
||||
// maxPaymentFlow, check() requireAuth, and 2*kMaxMpTokenAmount < 2^64.
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::MPTEndpointStep::revImp : send failed");
|
||||
JLOG(j_.trace()) << "MPTEndpointStep::rev: error " << ter;
|
||||
resetCache(srcDebtDir);
|
||||
return {beast::kZero, beast::kZero};
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
JLOG(j_.trace()) << "MPTEndpointStep::rev: Limiting"
|
||||
|
||||
JLOG(j_.trace()) << "MPTEndpointStep::rev: " << (srcToDst < out ? "Limiting" : "Non-limiting")
|
||||
<< " srcRedeems: " << redeems(srcDebtDir) << " in: " << to_string(in)
|
||||
<< " srcToDst: " << to_string(maxSrcToDst) << " out: " << to_string(out);
|
||||
return {in, actualOut};
|
||||
<< " srcToDst: " << to_string(srcToDst) << " out: " << to_string(out);
|
||||
|
||||
return {in, srcToDst};
|
||||
}
|
||||
|
||||
// The forward pass should never have more liquidity than the reverse
|
||||
@@ -609,8 +626,6 @@ MPTEndpointStep<TDerived>::fwdImp(
|
||||
auto const [srcQOut, dstQIn] = qualities(sb, srcDebtDir, StrandDirection::Forward);
|
||||
(void)dstQIn;
|
||||
|
||||
MPTIssue const srcToDstIss(mptIssue_);
|
||||
|
||||
JLOG(j_.trace()) << "MPTEndpointStep::fwd"
|
||||
<< " srcRedeems: " << redeems(srcDebtDir) << " inReq: " << to_string(in)
|
||||
<< " maxSrcToDst: " << to_string(maxSrcToDst) << " srcQOut: " << srcQOut
|
||||
@@ -618,63 +633,81 @@ MPTEndpointStep<TDerived>::fwdImp(
|
||||
|
||||
if (maxSrcToDst.signum() <= 0)
|
||||
{
|
||||
// Unreachable: the reverse pass owns dry detection; every path that
|
||||
// reaches fwdImp (see StrandFlow::flow) has a funded source.
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::MPTEndpointStep::fwdImp : dry source");
|
||||
JLOG(j_.trace()) << "MPTEndpointStep::fwd: dry";
|
||||
resetCache(srcDebtDir);
|
||||
return {beast::kZero, beast::kZero};
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
if (auto const err = static_cast<TDerived*>(this)->checkCreateMPT(sb, srcDebtDir);
|
||||
!isTesSuccess(err))
|
||||
auto const maybeSrcToDst = tryMulRatio(in, QUALITY_ONE, srcQOut, /*roundUp*/ false);
|
||||
if (!maybeSrcToDst)
|
||||
{
|
||||
// Unreachable: divides by srcQOut >= QUALITY_ONE, so result <= in <=
|
||||
// maxMPTAmount and can never overflow int64.
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::MPTEndpointStep::fwdImp : source to destination overflow");
|
||||
JLOG(j_.trace()) << "MPTEndpointStep::fwd: overflow";
|
||||
resetCache(srcDebtDir);
|
||||
return {beast::kZero, beast::kZero};
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
MPTAmount const srcToDst = mulRatio(in, QUALITY_ONE, srcQOut, /*roundUp*/ false);
|
||||
MPTAmount const srcToDst = *maybeSrcToDst;
|
||||
|
||||
if (srcToDst <= maxSrcToDst)
|
||||
{
|
||||
// Don't have to factor in dstQIn since it's always QUALITY_ONE
|
||||
MPTAmount const out = srcToDst;
|
||||
setCacheLimiting(in, srcToDst, out, srcDebtDir);
|
||||
auto const ter = directSendNoFee(
|
||||
sb,
|
||||
src_,
|
||||
dst_,
|
||||
toSTAmount(cache_->srcToDst, srcToDstIss),
|
||||
/*checkIssuer*/ false,
|
||||
j_);
|
||||
if (!isTesSuccess(ter))
|
||||
{
|
||||
JLOG(j_.trace()) << "MPTEndpointStep::fwd: error " << ter;
|
||||
resetCache(srcDebtDir);
|
||||
return {beast::kZero, beast::kZero};
|
||||
}
|
||||
|
||||
JLOG(j_.trace()) << "MPTEndpointStep::fwd: Non-limiting"
|
||||
<< " srcRedeems: " << redeems(srcDebtDir) << " in: " << to_string(in)
|
||||
<< " srcToDst: " << to_string(srcToDst) << " out: " << to_string(out);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unreachable: the reverse pass owns all limiting; the forward driver
|
||||
// (StrandFlow::flow) never re-finds a limit, so srcToDst <= maxSrcToDst.
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::MPTEndpointStep::fwdImp : forward pass limiting");
|
||||
// limiting node
|
||||
MPTAmount const actualIn = mulRatio(maxSrcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true);
|
||||
// Don't have to factor in dstQIn since it's always QUALITY_ONE
|
||||
MPTAmount const out = maxSrcToDst;
|
||||
setCacheLimiting(actualIn, maxSrcToDst, out, srcDebtDir);
|
||||
auto const ter = directSendNoFee(
|
||||
sb,
|
||||
src_,
|
||||
dst_,
|
||||
toSTAmount(cache_->srcToDst, srcToDstIss),
|
||||
/*checkIssuer*/ false,
|
||||
j_);
|
||||
if (!isTesSuccess(ter))
|
||||
auto const maybeActualIn = tryMulRatio(maxSrcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true);
|
||||
if (!maybeActualIn)
|
||||
{
|
||||
JLOG(j_.trace()) << "MPTEndpointStep::fwd: error " << ter;
|
||||
JLOG(j_.trace()) << "MPTEndpointStep::fwd: overflow";
|
||||
resetCache(srcDebtDir);
|
||||
return {beast::kZero, beast::kZero};
|
||||
}
|
||||
|
||||
MPTAmount const actualIn = *maybeActualIn;
|
||||
|
||||
// Don't have to factor in dstQIn since it's always QUALITY_ONE
|
||||
MPTAmount const out = maxSrcToDst;
|
||||
setCacheLimiting(actualIn, maxSrcToDst, out, srcDebtDir);
|
||||
|
||||
JLOG(j_.trace()) << "MPTEndpointStep::fwd: Limiting"
|
||||
<< " srcRedeems: " << redeems(srcDebtDir) << " in: " << to_string(actualIn)
|
||||
<< " srcToDst: " << to_string(srcToDst) << " out: " << to_string(out);
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
auto const ter = sendWithMPTCreate(sb, src_, dst_, cache_->srcToDst);
|
||||
if (!isTesSuccess(ter))
|
||||
{
|
||||
// Unreachable: send fails only on funds/auth/overflow, precluded by
|
||||
// maxPaymentFlow, check() requireAuth, and 2*kMaxMpTokenAmount < 2^64.
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::MPTEndpointStep::fwdImp : send failed");
|
||||
JLOG(j_.trace()) << "MPTEndpointStep::fwd: error " << ter;
|
||||
resetCache(srcDebtDir);
|
||||
return {beast::kZero, beast::kZero};
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
return {cache_->in, cache_->out};
|
||||
// NOLINTEND(bugprone-unchecked-optional-access)
|
||||
}
|
||||
|
||||
@@ -237,6 +237,7 @@ AMMClawback::applyGuts(Sandbox& sb)
|
||||
0,
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
ReserveHandling::IgnoreReserve,
|
||||
WithdrawAll::Yes,
|
||||
preFeeBalance_,
|
||||
ctx_.journal);
|
||||
@@ -324,11 +325,13 @@ AMMClawback::equalWithdrawMatchingOneAmount(
|
||||
auto amount2Withdraw = amount2Balance * frac;
|
||||
|
||||
auto const lpTokensWithdraw = toSTAmount(lptAMMBalance.asset(), lptAMMBalance * frac);
|
||||
if (lpTokensWithdraw > holdLPtokens)
|
||||
auto const& rules = sb.rules();
|
||||
// Pre-fixCleanup3_4_0 only a strictly greater computed LP amount takes
|
||||
// the withdraw-all path. Equality left the last holder unable to be
|
||||
// fully clawed. The amendment treats equality as withdraw-all.
|
||||
if (rules.enabled(fixCleanup3_4_0) ? lpTokensWithdraw >= holdLPtokens
|
||||
: lpTokensWithdraw > holdLPtokens)
|
||||
{
|
||||
// if lptoken balance less than what the issuer intended to clawback,
|
||||
// clawback all the tokens. Because we are doing a two-asset withdrawal,
|
||||
// tfee is actually not used, so pass tfee as 0.
|
||||
return AMMWithdraw::equalWithdrawTokens(
|
||||
sb,
|
||||
ammSle,
|
||||
@@ -343,12 +346,12 @@ AMMClawback::equalWithdrawMatchingOneAmount(
|
||||
0,
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
ReserveHandling::IgnoreReserve,
|
||||
WithdrawAll::Yes,
|
||||
preFeeBalance_,
|
||||
ctx_.journal);
|
||||
}
|
||||
|
||||
auto const& rules = sb.rules();
|
||||
if (rules.enabled(fixAMMClawbackRounding))
|
||||
{
|
||||
auto tokensAdj = getRoundedLPTokens(rules, lptAMMBalance, frac, IsDeposit::No);
|
||||
@@ -384,6 +387,7 @@ AMMClawback::equalWithdrawMatchingOneAmount(
|
||||
0,
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
ReserveHandling::IgnoreReserve,
|
||||
WithdrawAll::No,
|
||||
preFeeBalance_,
|
||||
ctx_.journal);
|
||||
@@ -405,6 +409,7 @@ AMMClawback::equalWithdrawMatchingOneAmount(
|
||||
0,
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
ReserveHandling::IgnoreReserve,
|
||||
WithdrawAll::No,
|
||||
preFeeBalance_,
|
||||
ctx_.journal);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user