mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-22 19:47:17 +00:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00e6407514 | ||
|
|
19c94c73f4 | ||
|
|
cd005ff60d | ||
|
|
578224f2e6 | ||
|
|
4a4fded2eb | ||
|
|
c8e767afa4 | ||
|
|
00eeb0a005 | ||
|
|
a18839d92d | ||
|
|
8c594c7ed9 | ||
|
|
ebd810b184 | ||
|
|
3e4e56d6bb | ||
|
|
76da5d4475 | ||
|
|
c0d0fd0d97 | ||
|
|
9aebb5ebea | ||
|
|
796f2f8f1e | ||
|
|
b190f2b14f | ||
|
|
6099940c2c | ||
|
|
0db7b766e6 | ||
|
|
ea6226b8b9 | ||
|
|
eae0a35415 |
No files matched your search
+1
-1
@@ -141,6 +141,7 @@ words:
|
||||
- hwrap
|
||||
- ifndef
|
||||
- inequation
|
||||
- Injectivity
|
||||
- insuf
|
||||
- insuff
|
||||
- invasively
|
||||
@@ -372,7 +373,6 @@ words:
|
||||
- writeme
|
||||
- wsrch
|
||||
- wthread
|
||||
- Xahau
|
||||
- xbridge
|
||||
- xchain
|
||||
- xcrun
|
||||
|
||||
@@ -15,6 +15,14 @@ _BASE_CMAKE_ARGS = [
|
||||
"-Drust=ON",
|
||||
]
|
||||
|
||||
# The package formats a config can be packaged as, each with its own
|
||||
# install-test job in reusable-package.yml.
|
||||
PACKAGE_TYPES = ("deb", "rpm")
|
||||
|
||||
# The package name a variant suffixes, as build_pkg.py's BASE_NAME spells it:
|
||||
# the two have to agree, or the artifact globs miss what was built.
|
||||
BASE_NAME = "xrpld"
|
||||
|
||||
# Maps sanitizer names (as used in cmake) to short config-name suffixes.
|
||||
_SANITIZER_SUFFIX: dict[str, str] = {
|
||||
"address": "asan",
|
||||
@@ -62,10 +70,20 @@ def get_cmake_args(build_type: str, extra_args: str) -> str:
|
||||
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
|
||||
type: str # 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
|
||||
# A flavour of the package, named xrpld-<variant>, for a config whose
|
||||
# binaries are not the plain release build. A variant needs no counterpart
|
||||
# in the other format.
|
||||
variant: str = ""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
assert self.type in PACKAGE_TYPES, (
|
||||
f"unsupported package type {self.type!r}: "
|
||||
f"use one of {', '.join(PACKAGE_TYPES)}."
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
@@ -178,6 +196,8 @@ class PackagingEntry:
|
||||
validator_keys_artifact_name: str
|
||||
image: str
|
||||
package_type: str # "deb" or "rpm"; drives the format-specific steps
|
||||
package_variant: str # passed to build_pkg.py --variant; empty for xrpld
|
||||
package_name: str # the name it builds under, which the artifact globs use
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -267,12 +287,32 @@ def expand_linux_packaging(linux: LinuxFile) -> list[PackagingEntry]:
|
||||
validator_keys_artifact_name=f"validator-keys-{name}",
|
||||
image=cfg.package.image,
|
||||
package_type=cfg.package.type,
|
||||
package_variant=cfg.package.variant,
|
||||
package_name=(
|
||||
f"{BASE_NAME}-{cfg.package.variant}"
|
||||
if cfg.package.variant
|
||||
else BASE_NAME
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def package_names_by_type(entries: list[PackagingEntry]) -> dict[str, list[str]]:
|
||||
"""The names of the packages in 'entries', keyed by format.
|
||||
|
||||
Derived from the packaging matrix rather than listed again, so the packages
|
||||
the install-test jobs look for are the packages that were built.
|
||||
"""
|
||||
return {
|
||||
package_type: sorted(
|
||||
{e.package_name for e in entries if e.package_type == package_type}
|
||||
)
|
||||
for package_type in PACKAGE_TYPES
|
||||
}
|
||||
|
||||
|
||||
def expand_platform_matrix(pf: PlatformFile, minimal: bool) -> list[MatrixEntry]:
|
||||
"""Expand a PlatformFile (macOS or Windows) into matrix entries.
|
||||
|
||||
@@ -341,6 +381,10 @@ if __name__ == "__main__":
|
||||
|
||||
if args.packaging:
|
||||
matrix = expand_linux_packaging(LinuxFile.load(THIS_DIR / "linux.json"))
|
||||
# One list per format, so each install-test job installs the packages its
|
||||
# own format produced.
|
||||
for package_type, names in package_names_by_type(matrix).items():
|
||||
print(f"{package_type}_package_names={json.dumps(names)}")
|
||||
else:
|
||||
if args.config in ("linux", None):
|
||||
matrix += expand_linux_matrix(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"image_tag": "sha-060957e",
|
||||
"image_tag": "sha-473fe44",
|
||||
"configs": {
|
||||
"ubuntu": [
|
||||
{
|
||||
@@ -76,6 +76,19 @@
|
||||
"type": "deb",
|
||||
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-49cdc10"
|
||||
}
|
||||
},
|
||||
{
|
||||
"compiler": ["gcc"],
|
||||
"build_type": ["Release"],
|
||||
"arch": ["amd64"],
|
||||
"minimal": false,
|
||||
"suffix": "assert",
|
||||
"extra_cmake_args": "-Dvalidator_keys=ON -Dassert=ON",
|
||||
"package": {
|
||||
"type": "deb",
|
||||
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-49cdc10",
|
||||
"variant": "assert"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
|
||||
@@ -5,13 +5,15 @@ on:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- ".github/workflows/build-nix-images.yml"
|
||||
- "flake.nix"
|
||||
- "flake.lock"
|
||||
- "rust-toolchain.toml"
|
||||
- "nix/**"
|
||||
- "!nix/docker/README.md"
|
||||
- "!nix/devshell.nix"
|
||||
- "!nix/check-tools/**"
|
||||
- "!nix/check-tools/*.txt"
|
||||
- "bin/check-tools.sh"
|
||||
- "bin/default-loader-path.sh"
|
||||
- "bin/install-sanitizer-libs.sh"
|
||||
pull_request:
|
||||
@@ -23,7 +25,7 @@ on:
|
||||
- "nix/**"
|
||||
- "!nix/docker/README.md"
|
||||
- "!nix/devshell.nix"
|
||||
- "!nix/check-tools/**"
|
||||
- "!nix/check-tools/*.txt"
|
||||
- "bin/check-tools.sh"
|
||||
- "bin/default-loader-path.sh"
|
||||
- "bin/install-sanitizer-libs.sh"
|
||||
|
||||
@@ -34,7 +34,7 @@ permissions:
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44
|
||||
permissions:
|
||||
contents: read
|
||||
# Needed to open an issue on scheduled failures.
|
||||
|
||||
@@ -85,6 +85,7 @@ jobs:
|
||||
.github/workflows/reusable-build-test.yml
|
||||
.github/workflows/reusable-check-autogen.yml
|
||||
.github/workflows/reusable-clang-tidy.yml
|
||||
.github/workflows/reusable-package-test-install.yml
|
||||
.github/workflows/reusable-package.yml
|
||||
.github/workflows/reusable-rust.yml
|
||||
.github/workflows/reusable-strategy-matrix.yml
|
||||
|
||||
@@ -23,6 +23,7 @@ on:
|
||||
- ".github/workflows/reusable-build-test.yml"
|
||||
- ".github/workflows/reusable-check-autogen.yml"
|
||||
- ".github/workflows/reusable-clang-tidy.yml"
|
||||
- ".github/workflows/reusable-package-test-install.yml"
|
||||
- ".github/workflows/reusable-package.yml"
|
||||
- ".github/workflows/reusable-rust.yml"
|
||||
- ".github/workflows/reusable-strategy-matrix.yml"
|
||||
|
||||
@@ -41,7 +41,7 @@ env:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -91,4 +91,4 @@ jobs:
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deploy
|
||||
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
|
||||
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
|
||||
@@ -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-060957e"
|
||||
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-473fe44"
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# Install one package format on every distro family it targets, one job per
|
||||
# package name and image, and run the binaries there. Called once per format by
|
||||
# reusable-package.yml, which owns the names and the image lists.
|
||||
name: Install packages
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
package_type:
|
||||
description: 'The package format to install ("deb" or "rpm").'
|
||||
required: true
|
||||
type: string
|
||||
package_names:
|
||||
description: "JSON array of package names built for this format."
|
||||
required: true
|
||||
type: string
|
||||
images:
|
||||
description: "JSON array of container images to install in."
|
||||
required: true
|
||||
type: string
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
PACKAGE_DIR: packages
|
||||
|
||||
jobs:
|
||||
install:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
package_name: ${{ fromJson(inputs.package_names) }}
|
||||
image: ${{ fromJson(inputs.images) }}
|
||||
name: "${{ matrix.package_name }} on ${{ matrix.image }}"
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
container: ${{ matrix.image }}
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
# Every package lands in one directory; the step below picks its own,
|
||||
# which keeps this 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_NAME: ${{ matrix.package_name }}
|
||||
PACKAGE_TYPE: ${{ inputs.package_type }}
|
||||
run: |
|
||||
# The version follows the name, separated by '_' in a DEB and '-' in an
|
||||
# RPM. Requiring a digit after it is what keeps 'xrpld' from picking up
|
||||
# another package, such as 'xrpld-assert'.
|
||||
pattern="${PACKAGE_NAME}[_-][0-9]*.${PACKAGE_TYPE}"
|
||||
package="$(find "${PACKAGE_DIR}" -type f -name "${pattern}" -print -quit)"
|
||||
test -n "${package}" || {
|
||||
echo "no ${pattern} found in ${PACKAGE_DIR}" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "package=${package}" >>"${GITHUB_OUTPUT}"
|
||||
|
||||
# Debian 11 went end-of-life on 2026-08-31
|
||||
# (https://www.debian.org/News/2026/20260831) and its packages are
|
||||
# already partly gone from deb.debian.org, so switch to the
|
||||
# snapshot.debian.org entries the image ships commented out in its
|
||||
# sources.list: they are pinned to the snapshot the image was built
|
||||
# from, so they serve every version it needs and never go away.
|
||||
# Snapshots keep their original, long-passed Valid-Until, hence the
|
||||
# disabled check; the retries absorb snapshot.debian.org's throttling.
|
||||
- name: Switch Debian 11 to snapshot.debian.org
|
||||
if: ${{ matrix.image == 'debian:11' }}
|
||||
run: |
|
||||
sed -i 's|^deb |# deb |; s|^# deb http://snapshot|deb http://snapshot|' /etc/apt/sources.list
|
||||
printf '%s\n' \
|
||||
'Acquire::Check-Valid-Until "false";' \
|
||||
'Acquire::Retries "3";' \
|
||||
>/etc/apt/apt.conf.d/99snapshot
|
||||
|
||||
- name: Install the DEB
|
||||
if: ${{ inputs.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: ${{ inputs.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
|
||||
@@ -3,8 +3,10 @@
|
||||
#
|
||||
# - '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
|
||||
# - 'test-install-deb' and 'test-install-rpm' call
|
||||
# reusable-package-test-install.yml to install what was built on a range of
|
||||
# distros and run 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'
|
||||
#
|
||||
@@ -23,7 +25,7 @@ on:
|
||||
description: "The base URL of the Nexus instance hosting the deb and rpm repositories."
|
||||
required: false
|
||||
type: string
|
||||
default: https://packages.xrplf.org
|
||||
default: https://packages-upload.xrplf.org
|
||||
|
||||
secrets:
|
||||
remote_username:
|
||||
@@ -49,6 +51,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.generate.outputs.matrix }}
|
||||
deb_package_names: ${{ steps.generate.outputs.deb_package_names }}
|
||||
rpm_package_names: ${{ steps.generate.outputs.rpm_package_names }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -107,13 +111,15 @@ jobs:
|
||||
- name: Build package
|
||||
env:
|
||||
PACKAGE_TYPE: ${{ matrix.package_type }}
|
||||
PKG_RELEASE: ${{ steps.release_info.outputs.pkg_release }}
|
||||
PACKAGE_VARIANT: ${{ matrix.package_variant }}
|
||||
PKG_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') && '2' || steps.release_info.outputs.pkg_release }}
|
||||
CHANNEL: ${{ steps.release_info.outputs.channel }}
|
||||
run: |
|
||||
./package/build_pkg.py \
|
||||
--package-type "${PACKAGE_TYPE}" \
|
||||
--build-dir "${BUILD_DIR}" \
|
||||
--pkg-release "${PKG_RELEASE}" \
|
||||
--variant "${PACKAGE_VARIANT}" \
|
||||
--channel "${CHANNEL}"
|
||||
|
||||
# Before the upload, so the artifact, the tested package and the published
|
||||
@@ -125,14 +131,17 @@ jobs:
|
||||
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.
|
||||
# that test-install downloads only what it installs. In the globs below the
|
||||
# version follows the name, separated by '_' in a DEB and '-' in an RPM. A
|
||||
# version starts with a digit and a longer name does not, so that one digit
|
||||
# is what tells 'xrpld-3.4.1-...' from 'xrpld-assert-3.4.1-...'.
|
||||
- name: Upload package artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ matrix.xrpld_artifact_name }}-pkg
|
||||
path: |
|
||||
${{ env.BUILD_DIR }}/debbuild/xrpld_*.deb
|
||||
${{ env.BUILD_DIR }}/rpmbuild/RPMS/**/xrpld-[0-9]*.rpm
|
||||
${{ env.BUILD_DIR }}/debbuild/${{ matrix.package_name }}_[0-9]*.deb
|
||||
${{ env.BUILD_DIR }}/rpmbuild/RPMS/**/${{ matrix.package_name }}-[0-9]*.rpm
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload debug symbol artifact
|
||||
@@ -140,129 +149,52 @@ jobs:
|
||||
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
|
||||
${{ env.BUILD_DIR }}/debbuild/${{ matrix.package_name }}-dbgsym_[0-9]*.deb
|
||||
${{ env.BUILD_DIR }}/debbuild/${{ matrix.package_name }}-dbgsym_[0-9]*.ddeb
|
||||
${{ env.BUILD_DIR }}/rpmbuild/RPMS/**/${{ matrix.package_name }}-debuginfo-[0-9]*.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
|
||||
# One call per format, so a variant packaged for one format is installed for
|
||||
# that format alone. The images are every distro family that format targets,
|
||||
# oldest release first, so both ends of the dependency range the packages
|
||||
# declare are exercised.
|
||||
test-install-deb:
|
||||
needs: [generate-matrix, package]
|
||||
name: install deb
|
||||
uses: ./.github/workflows/reusable-package-test-install.yml
|
||||
with:
|
||||
package_type: deb
|
||||
package_names: ${{ needs.generate-matrix.outputs.deb_package_names }}
|
||||
images: |
|
||||
[
|
||||
"debian:11",
|
||||
"debian:12",
|
||||
"debian:13",
|
||||
"ubuntu:20.04",
|
||||
"ubuntu:22.04",
|
||||
"ubuntu:24.04",
|
||||
"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}"
|
||||
|
||||
# Debian 11 went end-of-life on 2026-08-31
|
||||
# (https://www.debian.org/News/2026/20260831) and its packages are
|
||||
# already partly gone from deb.debian.org, so switch to the
|
||||
# snapshot.debian.org entries the image ships commented out in its
|
||||
# sources.list: they are pinned to the snapshot the image was built
|
||||
# from, so they serve every version it needs and never go away.
|
||||
# Snapshots keep their original, long-passed Valid-Until, hence the
|
||||
# disabled check; the retries absorb snapshot.debian.org's throttling.
|
||||
- name: Switch Debian 11 to snapshot.debian.org
|
||||
if: ${{ matrix.image == 'debian:11' }}
|
||||
run: |
|
||||
sed -i 's|^deb |# deb |; s|^# deb http://snapshot|deb http://snapshot|' /etc/apt/sources.list
|
||||
printf '%s\n' \
|
||||
'Acquire::Check-Valid-Until "false";' \
|
||||
'Acquire::Retries "3";' \
|
||||
>/etc/apt/apt.conf.d/99snapshot
|
||||
|
||||
- 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
|
||||
test-install-rpm:
|
||||
needs: [generate-matrix, package]
|
||||
name: install rpm
|
||||
uses: ./.github/workflows/reusable-package-test-install.yml
|
||||
with:
|
||||
package_type: rpm
|
||||
package_names: ${{ needs.generate-matrix.outputs.rpm_package_names }}
|
||||
images: |
|
||||
[
|
||||
"almalinux:9",
|
||||
"almalinux:10",
|
||||
"rockylinux/rockylinux:9",
|
||||
"rockylinux/rockylinux:10",
|
||||
"registry.access.redhat.com/ubi9/ubi",
|
||||
"registry.access.redhat.com/ubi10/ubi"
|
||||
]
|
||||
|
||||
publish:
|
||||
needs: [generate-matrix, package, test-install]
|
||||
needs: [generate-matrix, package, test-install-deb, test-install-rpm]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# Clippy, coverage and documentation for the Rust crates in crates/. Each runs
|
||||
# as an independent job on a GitHub-hosted runner, but inside the same container
|
||||
# image used to build the crates in the C++/Corrosion path, so the toolchain
|
||||
# (and therefore the lints and the cargo cache) matches what production builds
|
||||
# use. Coverage is the exception: it needs the nightly rustc that honours
|
||||
# #[coverage(off)], which the image carries alongside the pinned stable.
|
||||
# (and therefore the lints, coverage instrumentation and the cargo cache) matches
|
||||
# what production builds use.
|
||||
#
|
||||
# Rust unit tests are deliberately NOT run here. They run as part of the C++
|
||||
# build (reusable-build-test-config.yml), which already compiles the crates on a
|
||||
@@ -28,7 +27,7 @@ permissions:
|
||||
jobs:
|
||||
clippy:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -41,14 +40,11 @@ jobs:
|
||||
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Use the nightly Rust toolchain
|
||||
run: rust-nightly path >>"${GITHUB_PATH}"
|
||||
|
||||
- name: Use cargo artifacts cache
|
||||
uses: ./.github/actions/cargo-cache
|
||||
|
||||
@@ -70,7 +66,7 @@ jobs:
|
||||
|
||||
doc:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
@@ -40,7 +40,7 @@ defaults:
|
||||
jobs:
|
||||
upload:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44
|
||||
env:
|
||||
REMOTE_NAME: ${{ inputs.remote_name }}
|
||||
CONAN_LOGIN_USERNAME_XRPLF: ${{ secrets.remote_username }}
|
||||
|
||||
@@ -70,11 +70,6 @@ repos:
|
||||
language: system
|
||||
types: [rust]
|
||||
pass_filenames: false # rustfmt formats the whole workspace
|
||||
- id: check-coverage-attrs
|
||||
name: check Rust coverage attributes
|
||||
entry: ./bin/pre-commit/check_rust_coverage_attrs.py
|
||||
language: python
|
||||
files: ^crates/.*\.rs$
|
||||
|
||||
- repo: https://github.com/BlankSpruce/gersemi-pre-commit
|
||||
rev: e98930bdc210d3387007f9252d8c1694ea7e410f # frozen: 0.27.7
|
||||
|
||||
@@ -158,7 +158,6 @@ if [ "${os}" = "linux" ] || [ "${os}" = "macos" ]; then
|
||||
check cargo-nextest cargo nextest --version
|
||||
check clippy-driver
|
||||
check rust-analyzer
|
||||
check rust-nightly rust-nightly run rustc --version
|
||||
check rustc
|
||||
check rustfmt
|
||||
fi
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Check that Rust unit tests stay out of the coverage report.
|
||||
|
||||
cargo-llvm-cov instruments the test code along with everything else, so a test
|
||||
module that is not excluded counts its own body as covered and inflates the
|
||||
reported number. Excluding it takes two attributes:
|
||||
|
||||
* every `#[cfg(test)]` module carries
|
||||
`#[cfg_attr(coverage_nightly, coverage(off))]`;
|
||||
* every crate root (lib.rs, main.rs) carries
|
||||
`#![cfg_attr(coverage_nightly, feature(coverage_attribute))]`, which the
|
||||
attribute above needs in order to compile.
|
||||
|
||||
Both are inert outside the coverage job: cargo-llvm-cov defines
|
||||
`coverage_nightly` only when it runs on a nightly toolchain.
|
||||
|
||||
The crate-root gate is checked even in a crate that has no tests yet, because
|
||||
that is what lets the first test module added later carry the attribute without
|
||||
a build failure. Missing it is a hard error, so it cannot go unnoticed; a
|
||||
missing `coverage(off)` fails open, which is why this check exists.
|
||||
|
||||
Matching is on exact attribute text, which works because `cargo fmt` runs over
|
||||
the whole workspace in the hook ahead of this one: rustfmt puts every attribute
|
||||
on its own line and normalizes what is inside it, turning `#[cfg( test )]`
|
||||
and `#[cfg(test,)]` alike into `#[cfg(test)]`. So there is nothing here that
|
||||
parses Rust. The price is that a cfg this file does not spell out literally --
|
||||
`all(test, ...)`, `any(test, ...)`, `not(test)` -- is reported rather than
|
||||
classified, on the grounds that guessing at coverage semantics is how a check
|
||||
like this ends up quietly wrong.
|
||||
|
||||
Usage: ./bin/pre-commit/check_rust_coverage_attrs.py <file1> <file2> ...
|
||||
|
||||
Exit status is non-zero if any violation is found.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
CRATE_ROOTS = {"lib.rs", "main.rs"}
|
||||
|
||||
FEATURE_ATTR = "#![cfg_attr(coverage_nightly, feature(coverage_attribute))]"
|
||||
COVERAGE_OFF_ATTR = "#[cfg_attr(coverage_nightly, coverage(off))]"
|
||||
CFG_TEST_ATTR = "#[cfg(test)]"
|
||||
|
||||
# Any other cfg that mentions `test`. String literals are blanked before this
|
||||
# runs, so `feature = "test"` does not read as the `test` cfg.
|
||||
RE_CFG_MENTIONS_TEST = re.compile(r"^#\[cfg\(.*\btest\b.*\)\]$")
|
||||
RE_STRING = re.compile(r'"(?:[^"\\]|\\.)*"')
|
||||
RE_MOD = re.compile(r"^(?:pub(?:\([^)]*\))?\s+)?mod\s+([A-Za-z_]\w*)")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Finding:
|
||||
line: int
|
||||
label: str
|
||||
message: str
|
||||
|
||||
|
||||
def _check_module(attrs: list[str], line: int, name: str) -> list[Finding]:
|
||||
"""Findings for one module, given the attributes attached to it."""
|
||||
if COVERAGE_OFF_ATTR in attrs:
|
||||
return [] # excluded from coverage; which cfg gates it does not matter
|
||||
if CFG_TEST_ATTR in attrs:
|
||||
return [
|
||||
Finding(
|
||||
line,
|
||||
"missing-coverage-off",
|
||||
f"`mod {name}` is #[cfg(test)] but not excluded from coverage; "
|
||||
f"add {COVERAGE_OFF_ATTR}",
|
||||
)
|
||||
]
|
||||
unclassified = [
|
||||
attr for attr in attrs if RE_CFG_MENTIONS_TEST.match(RE_STRING.sub('""', attr))
|
||||
]
|
||||
if unclassified:
|
||||
return [
|
||||
Finding(
|
||||
line,
|
||||
"unclassified-cfg",
|
||||
f"`mod {name}` is gated on {unclassified[0]}, which this check "
|
||||
f"cannot tell apart from a module that ships in the library; "
|
||||
f"add {COVERAGE_OFF_ATTR} if it is test-only, or teach this "
|
||||
f"check the cfg if it is not",
|
||||
)
|
||||
]
|
||||
return []
|
||||
|
||||
|
||||
def _check_test_modules(lines: list[str]) -> list[Finding]:
|
||||
"""Findings for every test module that is not excluded from coverage."""
|
||||
findings: list[Finding] = []
|
||||
attrs: list[str] = []
|
||||
attrs_line = 0
|
||||
for number, raw in enumerate(lines, start=1):
|
||||
stripped = raw.strip()
|
||||
# Blank lines and comments are allowed between an attribute and its item.
|
||||
if not stripped or stripped.startswith("//"):
|
||||
continue
|
||||
if stripped.startswith("#["):
|
||||
if not attrs:
|
||||
attrs_line = number
|
||||
attrs.append(stripped)
|
||||
continue
|
||||
module = RE_MOD.match(stripped)
|
||||
if module is not None and attrs:
|
||||
findings += _check_module(attrs, attrs_line, module.group(1))
|
||||
attrs = []
|
||||
return findings
|
||||
|
||||
|
||||
def _check_crate_root(name: str, lines: list[str]) -> list[Finding]:
|
||||
"""A finding if a crate root is missing the coverage_attribute feature gate."""
|
||||
if name not in CRATE_ROOTS:
|
||||
return []
|
||||
if any(line.strip() == FEATURE_ATTR for line in lines):
|
||||
return []
|
||||
return [
|
||||
Finding(
|
||||
1,
|
||||
"missing-feature-gate",
|
||||
f"crate root is missing {FEATURE_ATTR}",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def check_source(name: str, text: str) -> list[Finding]:
|
||||
"""Findings for one file's contents; `name` is its base name (lib.rs, ...)."""
|
||||
lines = text.splitlines()
|
||||
return _check_crate_root(name, lines) + _check_test_modules(lines)
|
||||
|
||||
|
||||
def check_file(path: Path) -> list[Finding]:
|
||||
return check_source(path.name, path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
total = 0
|
||||
for path in (Path(name) for name in sys.argv[1:]):
|
||||
for finding in check_file(path):
|
||||
total += 1
|
||||
print(f"{path}:{finding.line}: {finding.label}: {finding.message}")
|
||||
return 1 if total else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -44,12 +44,18 @@ else()
|
||||
set(pkg_type rpm)
|
||||
endif()
|
||||
|
||||
# Unquoted below, so an empty value adds no argument at all.
|
||||
set(pkg_variant_option "")
|
||||
if(assert)
|
||||
set(pkg_variant_option --variant=assert)
|
||||
endif()
|
||||
|
||||
add_custom_target(
|
||||
package
|
||||
COMMAND
|
||||
${CMAKE_SOURCE_DIR}/package/build_pkg.py --package-type=${pkg_type}
|
||||
--build-dir=${CMAKE_BINARY_DIR} --pkg-release=${pkg_release}
|
||||
--channel=UNRELEASED
|
||||
${pkg_variant_option} --channel=UNRELEASED
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
DEPENDS xrpld validator-keys
|
||||
COMMENT "Building Linux ${pkg_type} package"
|
||||
|
||||
@@ -149,6 +149,7 @@ class Xrpl(ConanFile):
|
||||
self.requires("xxhash/0.8.3", transitive_headers=True)
|
||||
|
||||
exports_sources = (
|
||||
"bin/default-loader-path.sh",
|
||||
"CMakeLists.txt",
|
||||
"cfg/*",
|
||||
"cmake/*",
|
||||
|
||||
@@ -8,9 +8,6 @@ cxx = { version = "1.0.198", features = ["c++20"] }
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
|
||||
[workspace.lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(coverage)', 'cfg(coverage_nightly)' ] }
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
overflow-checks = true
|
||||
|
||||
@@ -8,6 +8,3 @@ crate-type = ["staticlib"]
|
||||
|
||||
[dependencies]
|
||||
cxx.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,5 +1,3 @@
|
||||
#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
|
||||
|
||||
#[cxx::bridge(namespace = "rs::hello_world")]
|
||||
mod ffi {
|
||||
extern "Rust" {
|
||||
@@ -10,14 +8,3 @@ mod ffi {
|
||||
pub fn hello_world() -> String {
|
||||
"hello_world".to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn hello_world_returns_hello_world() {
|
||||
assert_eq!(hello_world(), "hello_world")
|
||||
}
|
||||
}
|
||||
+19
-4
@@ -6,7 +6,8 @@
|
||||
|
||||
`xrpld` is published as DEB and RPM packages for 64-bit x86 Linux.
|
||||
Use APT on Debian-based distributions such as Debian and Ubuntu,
|
||||
and YUM on Red Hat-based distributions such as RHEL, AlmaLinux, and Rocky Linux.
|
||||
and DNF on Red Hat-based distributions such as RHEL, AlmaLinux, and Rocky Linux,
|
||||
where `yum` is a symlink to `dnf`.
|
||||
To build from source instead, see [BUILD.md](../BUILD.md).
|
||||
|
||||
## Release channels
|
||||
@@ -81,7 +82,7 @@ wherever it appears in the repository configuration.
|
||||
sudo apt -y install xrpld
|
||||
```
|
||||
|
||||
### With the YUM package manager
|
||||
### With the DNF package manager
|
||||
|
||||
1. Add the XRPL Foundation package-signing key:
|
||||
|
||||
@@ -109,9 +110,23 @@ wherever it appears in the repository configuration.
|
||||
3. Install the `xrpld` package:
|
||||
|
||||
```bash
|
||||
sudo yum install -y xrpld
|
||||
sudo dnf install -y xrpld
|
||||
```
|
||||
|
||||
### Optional: the assert-enabled build
|
||||
|
||||
Every channel also carries `xrpld-assert` as a DEB, the same build with assertions
|
||||
enabled, for diagnosing a problem on a non-production server.
|
||||
It installs the same files as `xrpld` and replaces it, so install one or the other:
|
||||
|
||||
```bash
|
||||
sudo apt -y install xrpld-assert # APT removes xrpld itself
|
||||
```
|
||||
|
||||
Switching stops the service, since it is a removal and an installation rather than an upgrade,
|
||||
and APT starts it again.
|
||||
Install `xrpld` the same way to switch back.
|
||||
|
||||
## The xrpld service
|
||||
|
||||
Both package managers install a systemd unit and enable it, so `xrpld` starts on boot.
|
||||
@@ -121,7 +136,7 @@ Check whether it is already running:
|
||||
systemctl status xrpld.service
|
||||
```
|
||||
|
||||
The APT packages start it immediately as well; the YUM packages do not, so start it yourself:
|
||||
The DEB packages start it immediately as well; the RPM packages do not, so start it yourself:
|
||||
|
||||
```bash
|
||||
sudo systemctl start xrpld.service
|
||||
|
||||
@@ -3,9 +3,84 @@
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* Add two signed 64-bit integers, returning std::nullopt when the exact
|
||||
* mathematical sum is not representable in std::int64_t.
|
||||
*/
|
||||
[[nodiscard]] constexpr std::optional<std::int64_t>
|
||||
checkedAdd(std::int64_t a, std::int64_t b) noexcept
|
||||
{
|
||||
using L = std::numeric_limits<std::int64_t>;
|
||||
|
||||
if ((b > 0 && a > L::max() - b) || (b < 0 && a < L::min() - b))
|
||||
return std::nullopt;
|
||||
|
||||
return a + b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtract two signed 64-bit integers, returning std::nullopt when the exact
|
||||
* mathematical difference is not representable in std::int64_t.
|
||||
*/
|
||||
[[nodiscard]] constexpr std::optional<std::int64_t>
|
||||
checkedSub(std::int64_t a, std::int64_t b) noexcept
|
||||
{
|
||||
using L = std::numeric_limits<std::int64_t>;
|
||||
|
||||
if ((b > 0 && a < L::min() + b) || (b < 0 && a > L::max() + b))
|
||||
return std::nullopt;
|
||||
|
||||
return a - b;
|
||||
}
|
||||
|
||||
static_assert(checkedAdd(0, 0) == 0);
|
||||
static_assert(checkedAdd(1, -1) == 0);
|
||||
static_assert(checkedAdd(-5, 2) == -3);
|
||||
static_assert(!checkedAdd(std::numeric_limits<std::int64_t>::max(), 1).has_value());
|
||||
static_assert(!checkedAdd(std::numeric_limits<std::int64_t>::min(), -1).has_value());
|
||||
static_assert(
|
||||
checkedAdd(std::numeric_limits<std::int64_t>::max() - 1, 1) ==
|
||||
std::numeric_limits<std::int64_t>::max());
|
||||
static_assert(
|
||||
checkedAdd(
|
||||
std::numeric_limits<std::int64_t>::min(),
|
||||
std::numeric_limits<std::int64_t>::max()) == -1);
|
||||
static_assert(
|
||||
checkedAdd(
|
||||
std::numeric_limits<std::int64_t>::max(),
|
||||
std::numeric_limits<std::int64_t>::min()) == -1);
|
||||
static_assert(
|
||||
!checkedAdd(std::numeric_limits<std::int64_t>::max(), std::numeric_limits<std::int64_t>::max())
|
||||
.has_value());
|
||||
static_assert(
|
||||
!checkedAdd(std::numeric_limits<std::int64_t>::min(), std::numeric_limits<std::int64_t>::min())
|
||||
.has_value());
|
||||
|
||||
static_assert(checkedSub(0, 0) == 0);
|
||||
static_assert(checkedSub(1, 1) == 0);
|
||||
static_assert(checkedSub(-5, 2) == -7);
|
||||
static_assert(checkedSub(-5, -2) == -3);
|
||||
static_assert(!checkedSub(std::numeric_limits<std::int64_t>::min(), 1).has_value());
|
||||
static_assert(!checkedSub(std::numeric_limits<std::int64_t>::max(), -1).has_value());
|
||||
static_assert(
|
||||
checkedSub(std::numeric_limits<std::int64_t>::min() + 1, 1) ==
|
||||
std::numeric_limits<std::int64_t>::min());
|
||||
static_assert(
|
||||
checkedSub(-1, std::numeric_limits<std::int64_t>::max()) ==
|
||||
std::numeric_limits<std::int64_t>::min());
|
||||
static_assert(
|
||||
!checkedSub(std::numeric_limits<std::int64_t>::max(), std::numeric_limits<std::int64_t>::min())
|
||||
.has_value());
|
||||
static_assert(
|
||||
!checkedSub(std::numeric_limits<std::int64_t>::min(), std::numeric_limits<std::int64_t>::max())
|
||||
.has_value());
|
||||
|
||||
/**
|
||||
* Calculate one number divided by another number in percentage.
|
||||
* The result is rounded up to the next integer, and capped in the range [0,100]
|
||||
|
||||
@@ -11,7 +11,6 @@ struct Sections
|
||||
static constexpr auto kCompression = "compression";
|
||||
static constexpr auto kCrawl = "crawl";
|
||||
static constexpr auto kDatabasePath = "database_path";
|
||||
static constexpr auto kDatagramMonitor = "datagram_monitor";
|
||||
static constexpr auto kDebugLogfile = "debug_logfile";
|
||||
static constexpr auto kElbSupport = "elb_support";
|
||||
static constexpr auto kFeatures = "features";
|
||||
|
||||
@@ -92,7 +92,7 @@ public:
|
||||
void
|
||||
importDatabase(Database& source) override
|
||||
{
|
||||
importInternal(*backend_.get(), source);
|
||||
importInternal(*backend_, source);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <ostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <variant>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -121,9 +123,32 @@ operator==(PathAsset const& lhs, PathAsset const& rhs)
|
||||
|
||||
template <typename Hasher>
|
||||
void
|
||||
hash_append(Hasher& h, PathAsset const& pathAsset)
|
||||
hash_append(Hasher& h, PathAsset const& pathAsset) noexcept
|
||||
{
|
||||
std::visit([&]<ValidPathAsset T>(T const& e) { hash_append(h, e); }, pathAsset.value());
|
||||
using beast::hash_append;
|
||||
using Variant = std::remove_cvref_t<decltype(pathAsset.value())>;
|
||||
|
||||
static_assert(
|
||||
std::variant_size_v<Variant> < 0xFFu,
|
||||
"PathAsset's discriminant must fit in a byte, leaving 0xFF reserved.");
|
||||
|
||||
// std::visit is not noexcept: it throws bad_variant_access when the variant
|
||||
// is valueless_by_exception.
|
||||
if (pathAsset.value().valueless_by_exception()) [[unlikely]]
|
||||
{
|
||||
hash_append(h, static_cast<std::uint8_t>(0xFFu));
|
||||
return;
|
||||
}
|
||||
|
||||
hash_append(h, static_cast<std::uint8_t>(pathAsset.value().index()));
|
||||
std::visit(
|
||||
[&]<ValidPathAsset T>(T const& e) noexcept {
|
||||
static_assert(
|
||||
noexcept(hash_append(h, e)),
|
||||
"Every PathAsset alternative must be nothrow-hashable.");
|
||||
hash_append(h, e);
|
||||
},
|
||||
pathAsset.value());
|
||||
}
|
||||
|
||||
inline bool
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -545,11 +544,6 @@ constexpr std::size_t kEcClawbackProofLength = SECP256K1_COMPACT_CLAWBACK_PROOF_
|
||||
*/
|
||||
constexpr std::uint32_t kConfidentialFeeMultiplier = 9;
|
||||
|
||||
/**
|
||||
* Maximum value a confidential MPT key epoch may reach.
|
||||
*/
|
||||
constexpr std::uint32_t kMaxKeyEpoch = std::numeric_limits<std::uint32_t>::max();
|
||||
|
||||
/**
|
||||
* Compressed EC point prefix for even y-coordinate
|
||||
*/
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -65,7 +67,7 @@ public:
|
||||
PathAsset const& asset,
|
||||
AccountID const& issuer);
|
||||
|
||||
[[nodiscard]] auto
|
||||
[[nodiscard]] std::uint32_t
|
||||
getNodeType() const;
|
||||
|
||||
[[nodiscard]] bool
|
||||
@@ -109,9 +111,6 @@ public:
|
||||
[[nodiscard]] bool
|
||||
isType(Type const& pe) const;
|
||||
|
||||
[[nodiscard]] size_t
|
||||
getHash() const;
|
||||
|
||||
bool
|
||||
operator==(STPathElement const& t) const;
|
||||
|
||||
@@ -120,6 +119,17 @@ private:
|
||||
getHash(STPathElement const& element);
|
||||
};
|
||||
|
||||
template <class Hasher>
|
||||
void
|
||||
hash_append(Hasher& h, STPathElement const& e) noexcept
|
||||
{
|
||||
using beast::hash_append;
|
||||
hash_append(h, (e.getNodeType() & STPathElement::TypeAccount) != 0u);
|
||||
hash_append(h, e.getAccountID());
|
||||
hash_append(h, e.getPathAsset());
|
||||
hash_append(h, e.getIssuerID());
|
||||
}
|
||||
|
||||
class STPath final : public CountedObject<STPath>
|
||||
{
|
||||
std::vector<STPathElement> path_;
|
||||
@@ -176,9 +186,10 @@ template <class Hasher>
|
||||
void
|
||||
hash_append(Hasher& h, STPath const& p) noexcept
|
||||
{
|
||||
using beast::hash_append;
|
||||
for (auto const& e : p)
|
||||
{
|
||||
beast::hash_append(h, e.getHash());
|
||||
hash_append(h, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,13 +199,39 @@ hash_append(Hasher& h, STPath const& p) noexcept
|
||||
class STPathSet final : public STBase, public CountedObject<STPathSet>
|
||||
{
|
||||
std::vector<STPath> value_;
|
||||
xrpl::hardened_hash_set<STPath> seenHashes_;
|
||||
|
||||
/**
|
||||
* Deduplication index over `value_`, for pathfinding.
|
||||
* The use of a std::unique_ptr is intentional as it
|
||||
* only requires 8 additional bytes of storage for the pointer
|
||||
* as opposed to 64 bytes with an optional. This keeps the size
|
||||
* of the STPathSet to within the `STVar::kMaxSize` limit of 72 bytes.
|
||||
*/
|
||||
std::unique_ptr<hardened_hash_set<STPath>> seen_;
|
||||
|
||||
public:
|
||||
struct DeduplicationTag
|
||||
{
|
||||
};
|
||||
|
||||
STPathSet() = default;
|
||||
/**
|
||||
* Deduplication tagged constructor.
|
||||
* Use when you want to ensure that the STPathSet does not contain duplicate paths.
|
||||
*/
|
||||
explicit STPathSet(DeduplicationTag);
|
||||
|
||||
STPathSet(SField const& n);
|
||||
STPathSet(SerialIter& sit, SField const& name);
|
||||
STPathSet(STPathSet const& other);
|
||||
STPathSet(STPathSet&&) = default;
|
||||
|
||||
STPathSet&
|
||||
operator=(STPathSet const& other);
|
||||
STPathSet&
|
||||
operator=(STPathSet&&) = default;
|
||||
|
||||
~STPathSet() override = default;
|
||||
|
||||
void
|
||||
add(Serializer& s) const override;
|
||||
@@ -204,6 +241,16 @@ public:
|
||||
[[nodiscard]] SerializedTypeID
|
||||
getSType() const override;
|
||||
|
||||
/**
|
||||
* @brief assembleAdd adds a path to the set by combining a base path and a tail element.
|
||||
*
|
||||
* @param base The base path.
|
||||
* @param tail The tail element.
|
||||
* @return true if the path was added, false if it was a duplicate and not added.
|
||||
* @remarks Requires the STPathSet to be constructed with the DeduplicationTag. The return value
|
||||
* indicates whether the combined path was inserted (true) or rejected as a duplicate (false).
|
||||
* It is fine for callers to ignore the return value.
|
||||
*/
|
||||
bool
|
||||
assembleAdd(STPath const& base, STPathElement const& tail);
|
||||
|
||||
@@ -229,22 +276,61 @@ public:
|
||||
[[nodiscard]] bool
|
||||
empty() const;
|
||||
|
||||
void
|
||||
/**
|
||||
* @brief pushBack adds a path to the set.
|
||||
*
|
||||
* @param e The path to add.
|
||||
* @return true if the path was added, false if it was a duplicate and not added.
|
||||
* @remarks If the STPathSet was constructed with the DeduplicationTag, then this method will
|
||||
* check for duplicates and only add the path if it is not already present in the
|
||||
* set. If the STPathSet was constructed without the DeduplicationTag,
|
||||
* then this method will always add the path to the set, regardless of duplicates.
|
||||
* It is fine for callers to ignore the return value.
|
||||
*/
|
||||
bool
|
||||
pushBack(STPath const& e);
|
||||
|
||||
/**
|
||||
* @brief emplaceBack adds a path to the set.
|
||||
*
|
||||
* @param args The arguments to construct the path with.
|
||||
* @return true if the path was added, false if it was a duplicate and not added.
|
||||
* @remarks If the STPathSet was constructed with the DeduplicationTag, then this method will
|
||||
* check for duplicates and only add the path if it is not already present in the
|
||||
* set. If the STPathSet was constructed without the DeduplicationTag,
|
||||
* then this method will always add the path to the set, regardless of duplicates.
|
||||
* It is fine for callers to ignore the return value.
|
||||
* @note The path is constructed before the duplicate check, so on a false
|
||||
* return the constructed path is discarded and any argument
|
||||
* forwarded as an rvalue is left in a moved-from state. Use
|
||||
* pushBack when the caller needs to keep its path on rejection.
|
||||
*/
|
||||
template <typename... Args>
|
||||
void
|
||||
bool
|
||||
emplaceBack(Args&&... args);
|
||||
|
||||
[[nodiscard]] bool
|
||||
contains(STPath const& path) const;
|
||||
|
||||
private:
|
||||
STBase*
|
||||
copy(std::size_t n, void* buf) const override;
|
||||
STBase*
|
||||
move(std::size_t n, void* buf) override;
|
||||
|
||||
/**
|
||||
* @brief Append a path via `append`, then register it in the deduplication index.
|
||||
*
|
||||
* @param append Invoked with `value_`; must append exactly one path to it.
|
||||
* @return true if the path was kept, false if it was a duplicate and was rolled back.
|
||||
* @remarks Appends to the vector before touching the index, so that a failed allocation
|
||||
* there leaves both containers untouched rather than leaving the index holding
|
||||
* a path the vector does not. If the index insert reports a duplicate, or
|
||||
* throws, the append is rolled back so the two containers stay consistent; in
|
||||
* the throwing case the exception propagates. With no index (constructed
|
||||
* without the DeduplicationTag) the append is unconditional.
|
||||
*/
|
||||
template <typename Append>
|
||||
bool
|
||||
appendUnique(Append&& append);
|
||||
|
||||
friend class detail::STVar;
|
||||
};
|
||||
|
||||
@@ -336,7 +422,7 @@ inline STPathElement::STPathElement(
|
||||
hashValue_ = getHash(*this);
|
||||
}
|
||||
|
||||
inline auto
|
||||
inline std::uint32_t
|
||||
STPathElement::getNodeType() const
|
||||
{
|
||||
return type_;
|
||||
@@ -545,25 +631,50 @@ STPathSet::empty() const
|
||||
return value_.empty();
|
||||
}
|
||||
|
||||
inline void
|
||||
STPathSet::pushBack(STPath const& e)
|
||||
template <typename Append>
|
||||
inline bool
|
||||
STPathSet::appendUnique(Append&& append)
|
||||
{
|
||||
value_.push_back(e);
|
||||
seenHashes_.emplace(value_.back());
|
||||
}
|
||||
// Append to the vector first, so that a failed allocation there leaves both
|
||||
// containers untouched rather than leaving the index holding a path the
|
||||
// vector does not.
|
||||
append(value_);
|
||||
|
||||
template <typename... Args>
|
||||
inline void
|
||||
STPathSet::emplaceBack(Args&&... args)
|
||||
{
|
||||
value_.emplace_back(std::forward<Args>(args)...);
|
||||
seenHashes_.emplace(value_.back());
|
||||
if (seen_ == nullptr)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!seen_->insert(value_.back()).second)
|
||||
{
|
||||
// Already present: roll back the append.
|
||||
value_.pop_back();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// The index insert failed, so roll back the append to keep the vector
|
||||
// and the index consistent.
|
||||
value_.pop_back();
|
||||
throw;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool
|
||||
STPathSet::contains(STPath const& path) const
|
||||
STPathSet::pushBack(STPath const& e)
|
||||
{
|
||||
return seenHashes_.contains(path);
|
||||
return appendUnique([&](auto& value) { value.push_back(e); });
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
inline bool
|
||||
STPathSet::emplaceBack(Args&&... args)
|
||||
{
|
||||
return appendUnique([&](auto& value) { value.emplace_back(std::forward<Args>(args)...); });
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -124,6 +124,13 @@ public:
|
||||
[[nodiscard]] NodeID const&
|
||||
getNodeID() const noexcept;
|
||||
|
||||
/**
|
||||
* Whether this validation carries a good signature.
|
||||
*
|
||||
* Reports false if the signature cannot be checked at all, so a caller
|
||||
* cannot tell that apart from a bad signature. Either way the validation is
|
||||
* unusable, and the reason is logged. Only a computed answer is remembered.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isValid() const noexcept;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
@@ -25,6 +26,101 @@ private:
|
||||
Blob data_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* A header is never longer than this. The encoder fills a buffer of this
|
||||
* size and writes only the bytes it used.
|
||||
*/
|
||||
static constexpr int kMaxNumberOfBytesInHeader = 3;
|
||||
|
||||
// A field whose size varies is stored as a header holding its length, then
|
||||
// the field data. The header is 1, 2 or 3 bytes long. Nothing outside it says
|
||||
// which, so the decoder reads the first byte and its value says how long the
|
||||
// header is:
|
||||
//
|
||||
// 0 ... 192 kMin/kMaxValueOfFirstByteFor1ByteHeader
|
||||
// 193 ... 240 kMin/kMaxValueOfFirstByteFor2ByteHeader
|
||||
// 241 ... 254 kMin/kMaxValueOfFirstByteFor3ByteHeader
|
||||
// 255 belongs to no header
|
||||
//
|
||||
// Each range starts one past the end of the range before it.
|
||||
|
||||
static constexpr int kMinValueOfFirstByteFor1ByteHeader = 0;
|
||||
static constexpr int kMaxValueOfFirstByteFor1ByteHeader = 192;
|
||||
|
||||
static constexpr int kMinValueOfFirstByteFor2ByteHeader =
|
||||
kMaxValueOfFirstByteFor1ByteHeader + 1;
|
||||
static constexpr int kMaxValueOfFirstByteFor2ByteHeader = 240;
|
||||
|
||||
static constexpr int kMinValueOfFirstByteFor3ByteHeader =
|
||||
kMaxValueOfFirstByteFor2ByteHeader + 1;
|
||||
|
||||
static constexpr int kMaxValueOfFirstByteFor3ByteHeader = 254;
|
||||
|
||||
// A length x too big for one byte is split across the header. For 2 bytes:
|
||||
//
|
||||
// first byte = 193 + (x - 193) / 256
|
||||
// second byte = (x - 193) % 256
|
||||
//
|
||||
// so 300 is stored as 193, 107. For 3 bytes it is the same, from 241, with
|
||||
// the remainder split across two bytes: 20,000 is stored as 241, 29, 95.
|
||||
|
||||
static constexpr int kNumberOfValuesInOneByte = 256;
|
||||
static constexpr int kNumberOfValuesInTwoBytes =
|
||||
kNumberOfValuesInOneByte * kNumberOfValuesInOneByte;
|
||||
|
||||
// Each header length therefore covers a range of field lengths:
|
||||
//
|
||||
// 0 ... 192 kMin/kMaxValueOfLengthFor1ByteHeader
|
||||
// 193 ... 12,480 kMin/kMaxValueOfLengthFor2ByteHeader
|
||||
// 12,481 ... 918,744 kMin/kMaxValueOfLengthFor3ByteHeader
|
||||
//
|
||||
// The encoder always uses the shortest header that fits.
|
||||
|
||||
/**
|
||||
* A 1 byte header holds the length in the byte itself, so both ends of
|
||||
* this range are the same numbers as the first byte's own range.
|
||||
*/
|
||||
static constexpr int kMinValueOfLengthFor1ByteHeader = kMinValueOfFirstByteFor1ByteHeader;
|
||||
static constexpr int kMaxValueOfLengthFor1ByteHeader = kMaxValueOfFirstByteFor1ByteHeader;
|
||||
|
||||
static constexpr int kMinValueOfLengthFor2ByteHeader = kMaxValueOfLengthFor1ByteHeader + 1;
|
||||
|
||||
/**
|
||||
* 48 values of the first byte mean a 2 byte header, and each of them covers
|
||||
* 256 lengths. The 48 is worked out from the two range ends above, so it
|
||||
* stays right if either of them changes.
|
||||
*/
|
||||
static constexpr int kMaxValueOfLengthFor2ByteHeader = kMinValueOfLengthFor2ByteHeader +
|
||||
((kMaxValueOfFirstByteFor2ByteHeader - kMaxValueOfFirstByteFor1ByteHeader) *
|
||||
kNumberOfValuesInOneByte) -
|
||||
1;
|
||||
|
||||
static constexpr int kMinValueOfLengthFor3ByteHeader = kMaxValueOfLengthFor2ByteHeader + 1;
|
||||
|
||||
/**
|
||||
* 14 values of the first byte mean a 3 byte header, and each of them covers
|
||||
* 65,536 lengths. Counted the same way, that gives the largest length any
|
||||
* header can state.
|
||||
*
|
||||
* Nothing is accepted or rejected against this. The assertion below uses it
|
||||
* to check that every length the encoder writes is one a header can state.
|
||||
*/
|
||||
static constexpr int kMaxRepresentableLength = kMinValueOfLengthFor3ByteHeader +
|
||||
((kMaxValueOfFirstByteFor3ByteHeader - kMaxValueOfFirstByteFor2ByteHeader) *
|
||||
kNumberOfValuesInTwoBytes) -
|
||||
1;
|
||||
|
||||
/**
|
||||
* The largest length the encoder will write. This is the one number here
|
||||
* that is picked rather than worked out. The decoder accepts nothing above
|
||||
* it, so both sides agree on the same set of lengths.
|
||||
*/
|
||||
static constexpr int kMaxValueOfLengthFor3ByteHeader = 918744;
|
||||
|
||||
static_assert(
|
||||
kMaxValueOfLengthFor3ByteHeader <= kMaxRepresentableLength,
|
||||
"a length the encoder writes must be one a header can state");
|
||||
|
||||
explicit Serializer(int n = 256)
|
||||
{
|
||||
data_.reserve(n);
|
||||
@@ -61,7 +157,7 @@ public:
|
||||
|
||||
// assemble functions
|
||||
int
|
||||
add8(unsigned char i);
|
||||
add8(unsigned char byteValue);
|
||||
int
|
||||
add16(std::uint16_t i);
|
||||
|
||||
@@ -270,18 +366,90 @@ public:
|
||||
return v.data_ == data_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Works out how long a header is, from its first byte.
|
||||
*
|
||||
* Each overload of decodeVLLength below reads one header length, so call
|
||||
* this first to learn which of them to call.
|
||||
*
|
||||
* @param firstByte First byte of the header, as read from the stream.
|
||||
* @return How many bytes the whole header takes, counting firstByte: 1, 2
|
||||
* or 3.
|
||||
* @throws std::overflow_error if firstByte is the one value that starts no
|
||||
* header.
|
||||
*/
|
||||
static int
|
||||
decodeLengthLength(int b1);
|
||||
decodeLengthLength(std::byte firstByte);
|
||||
|
||||
/**
|
||||
* Reads the field length out of a 1 byte header.
|
||||
*
|
||||
* @param firstByte The single header byte, which is the length itself.
|
||||
* @return Field length in bytes, from kMinValueOfLengthFor1ByteHeader to
|
||||
* kMaxValueOfLengthFor1ByteHeader.
|
||||
* @throws std::overflow_error if firstByte is big enough to mean a longer
|
||||
* header, in which case it is not a length by itself.
|
||||
*/
|
||||
static int
|
||||
decodeVLLength(int b1);
|
||||
decodeVLLength(std::byte firstByte);
|
||||
|
||||
/**
|
||||
* Reads the field length out of a 2 byte header.
|
||||
*
|
||||
* @param firstByte First header byte. Its value means a 2 byte header, and
|
||||
* how far it sits into that range gives the top part of the length.
|
||||
* @param secondByte Second header byte, holding the rest of the length.
|
||||
* @return Field length in bytes, from kMinValueOfLengthFor2ByteHeader to
|
||||
* kMaxValueOfLengthFor2ByteHeader.
|
||||
* @throws std::overflow_error if firstByte is outside the range that means
|
||||
* a 2 byte header.
|
||||
*/
|
||||
static int
|
||||
decodeVLLength(int b1, int b2);
|
||||
decodeVLLength(std::byte firstByte, std::byte secondByte);
|
||||
|
||||
/**
|
||||
* Reads the field length out of a 3 byte header.
|
||||
*
|
||||
* @param firstByte First header byte. Its value means a 3 byte header, and
|
||||
* how far it sits into that range gives the top part of the length.
|
||||
* @param secondByte Second header byte, holding the middle part of the
|
||||
* length.
|
||||
* @param thirdByte Third header byte, holding the low part.
|
||||
* @return Field length in bytes, from kMinValueOfLengthFor3ByteHeader to
|
||||
* kMaxValueOfLengthFor3ByteHeader.
|
||||
* @throws std::overflow_error if firstByte is outside the range that means
|
||||
* a 3 byte header, or if the three bytes together state a length above
|
||||
* kMaxValueOfLengthFor3ByteHeader, which the encoder would not write back.
|
||||
*/
|
||||
static int
|
||||
decodeVLLength(int b1, int b2, int b3);
|
||||
decodeVLLength(std::byte firstByte, std::byte secondByte, std::byte thirdByte);
|
||||
|
||||
private:
|
||||
/**
|
||||
* Works out how many bytes the header needs for the given length.
|
||||
*
|
||||
* This deliberately repeats the width choice addEncoded makes, so that
|
||||
* addVL's assertion can compare the two. It has no other caller; do not
|
||||
* reach for it as a utility.
|
||||
*
|
||||
* @param length Field length in bytes.
|
||||
* @return How many header bytes it needs: 1, 2 or 3.
|
||||
* @throws std::overflow_error if length is negative, or above
|
||||
* kMaxValueOfLengthFor3ByteHeader.
|
||||
*/
|
||||
static int
|
||||
encodeLengthLength(int length); // length to encode length
|
||||
encodeLengthLength(int length);
|
||||
|
||||
/**
|
||||
* Appends the length header for a field of the given length.
|
||||
*
|
||||
* The field's own data is not written; the caller appends it next.
|
||||
*
|
||||
* @param length Field length in bytes.
|
||||
* @return Offset within this Serializer at which the header was written.
|
||||
* @throws std::overflow_error if length is negative, or above
|
||||
* kMaxValueOfLengthFor3ByteHeader.
|
||||
*/
|
||||
int
|
||||
addEncoded(int length);
|
||||
};
|
||||
@@ -390,9 +558,15 @@ public:
|
||||
void
|
||||
getFieldID(int& type, int& name);
|
||||
|
||||
// Returns the size of the VL if the
|
||||
// next object is a VL. Advances the iterator
|
||||
// to the beginning of the VL.
|
||||
/**
|
||||
* Reads the length header at the read position and steps past it.
|
||||
*
|
||||
* @return Field length in bytes. The iterator is left on the first byte of
|
||||
* the field data.
|
||||
* @throws std::overflow_error if the header states a length the encoder could
|
||||
* not have written.
|
||||
* @throws std::runtime_error if the data runs out before the header does.
|
||||
*/
|
||||
int
|
||||
getVLDataLength();
|
||||
|
||||
|
||||
@@ -129,11 +129,8 @@ enum TEMcodes : TERUnderlyingType {
|
||||
temARRAY_TOO_LARGE,
|
||||
temBAD_TRANSFER_FEE,
|
||||
temINVALID_INNER_BATCH,
|
||||
|
||||
temBAD_MPT,
|
||||
temBAD_CIPHERTEXT,
|
||||
temINVALID_BYTECODE,
|
||||
temTEMP_DISABLED,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -182,8 +179,6 @@ enum TEFcodes : TERUnderlyingType {
|
||||
tefINVALID_LEDGER_FIX_TYPE,
|
||||
tefNO_DST_PARTIAL,
|
||||
tefBAD_PATH_COUNT,
|
||||
tefNO_BYTECODE,
|
||||
tefBYTECODE_NOT_INCLUDED,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -375,8 +370,6 @@ enum TECcodes : TERUnderlyingType {
|
||||
tecNO_DELEGATE_PERMISSION = 198,
|
||||
tecBAD_PROOF = 199,
|
||||
tecNO_SPONSOR_PERMISSION = 200,
|
||||
tecOUT_OF_GAS = 201,
|
||||
tecBYTECODE_REJECTED = 202,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -34,10 +34,11 @@ concept ValidConstructSTArgs =
|
||||
// and includes a small-object allocation optimization.
|
||||
class STVar
|
||||
{
|
||||
private:
|
||||
public:
|
||||
// The largest "small object" we can accommodate
|
||||
static constexpr std::size_t kMaxSize = 72;
|
||||
|
||||
private:
|
||||
alignas(std::max_align_t) std::byte d_[kMaxSize] = {};
|
||||
STBase* p_ = nullptr;
|
||||
|
||||
|
||||
@@ -15,10 +15,7 @@
|
||||
// Add new amendments to the top of this list.
|
||||
// Keep it sorted in reverse chronological order.
|
||||
|
||||
XRPL_FEATURE(SmartEscrow, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(LendingProtocolV1_2, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (Cleanup3_5_0, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(ConfidentialMPTKeyRotation, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (BatchV1_2, Supported::Yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FIX (Cleanup3_4_0, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(Sponsor, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(BatchV1_1, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
|
||||
@@ -309,11 +309,6 @@ LEDGER_ENTRY(ltFEE_SETTINGS, 0x0073, FeeSettings, fee, ({
|
||||
{sfBaseFeeDrops, SoeOptional},
|
||||
{sfReserveBaseDrops, SoeOptional},
|
||||
{sfReserveIncrementDrops, SoeOptional},
|
||||
// Smart Escrow fields
|
||||
{sfGasLimit, SoeOptional},
|
||||
{sfBytecodeSizeLimit, SoeOptional},
|
||||
{sfGasPrice, SoeOptional},
|
||||
|
||||
{sfPreviousTxnID, SoeOptional},
|
||||
{sfPreviousTxnLgrSeq, SoeOptional},
|
||||
}))
|
||||
@@ -344,8 +339,6 @@ LEDGER_ENTRY(ltESCROW, 0x0075, Escrow, escrow, ({
|
||||
{sfCondition, SoeOptional},
|
||||
{sfCancelAfter, SoeOptional},
|
||||
{sfFinishAfter, SoeOptional},
|
||||
{sfBytecode, SoeOptional},
|
||||
{sfData, SoeOptional},
|
||||
{sfSourceTag, SoeOptional},
|
||||
{sfDestinationTag, SoeOptional},
|
||||
{sfOwnerNode, SoeRequired},
|
||||
@@ -415,8 +408,6 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({
|
||||
{sfReferenceHolding, SoeOptional},
|
||||
{sfIssuerEncryptionKey, SoeOptional},
|
||||
{sfAuditorEncryptionKey, SoeOptional},
|
||||
{sfIssuerKeyEpoch, SoeOptional},
|
||||
{sfAuditorKeyEpoch, SoeOptional},
|
||||
{sfConfidentialOutstandingAmount, SoeDefault},
|
||||
}))
|
||||
|
||||
|
||||
@@ -119,15 +119,6 @@ TYPED_SFIELD(sfRemainingOwnerCount, UINT32, 73)
|
||||
TYPED_SFIELD(sfSponsorFlags, UINT32, 74)
|
||||
TYPED_SFIELD(sfSubscriptionDate, UINT32, 75)
|
||||
TYPED_SFIELD(sfRedemptionDate, UINT32, 76)
|
||||
TYPED_SFIELD(sfIssuerKeyEpoch, UINT32, 77)
|
||||
TYPED_SFIELD(sfAuditorKeyEpoch, UINT32, 78)
|
||||
TYPED_SFIELD(sfIssuerKeyMirrorEpoch, UINT32, 79)
|
||||
TYPED_SFIELD(sfAuditorKeyMirrorEpoch, UINT32, 80)
|
||||
TYPED_SFIELD(sfGasLimit, UINT32, 81)
|
||||
TYPED_SFIELD(sfBytecodeSizeLimit, UINT32, 82)
|
||||
TYPED_SFIELD(sfGasPrice, UINT32, 83)
|
||||
TYPED_SFIELD(sfGas, UINT32, 84)
|
||||
TYPED_SFIELD(sfGasUsed, UINT32, 85)
|
||||
|
||||
// 64-bit integers (common)
|
||||
TYPED_SFIELD(sfIndexNext, UINT64, 1)
|
||||
@@ -243,7 +234,6 @@ TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17, SField::kSmdNeedsAsset
|
||||
// 32-bit signed (common)
|
||||
TYPED_SFIELD(sfLoanScale, INT32, 1)
|
||||
TYPED_SFIELD(sfRemainingOwnerCountDelta, INT32, 2)
|
||||
TYPED_SFIELD(sfVMReturnCode, INT32, 3)
|
||||
|
||||
// currency amount (common)
|
||||
TYPED_SFIELD(sfAmount, AMOUNT, 1)
|
||||
@@ -328,7 +318,6 @@ TYPED_SFIELD(sfAuditorEncryptedAmount, VL, 43)
|
||||
TYPED_SFIELD(sfAuditorEncryptionKey, VL, 44)
|
||||
TYPED_SFIELD(sfAmountCommitment, VL, 45)
|
||||
TYPED_SFIELD(sfBalanceCommitment, VL, 46)
|
||||
TYPED_SFIELD(sfBytecode, VL, 47)
|
||||
|
||||
// account (common)
|
||||
TYPED_SFIELD(sfAccount, ACCOUNT, 1)
|
||||
|
||||
@@ -66,13 +66,11 @@ TRANSACTION(ttPAYMENT, 0, Payment,
|
||||
#endif
|
||||
TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, ({.delegable = Delegation::Delegable}), ({
|
||||
{sfDestination, SoeRequired},
|
||||
{sfDestinationTag, SoeOptional},
|
||||
{sfAmount, SoeRequired, SoeMptSupported},
|
||||
{sfCondition, SoeOptional},
|
||||
{sfCancelAfter, SoeOptional},
|
||||
{sfFinishAfter, SoeOptional},
|
||||
{sfBytecode, SoeOptional},
|
||||
{sfData, SoeOptional},
|
||||
{sfDestinationTag, SoeOptional},
|
||||
}))
|
||||
|
||||
/** This transaction type completes an existing escrow. */
|
||||
@@ -85,7 +83,6 @@ TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, ({.delegable = Delegation::Delegab
|
||||
{sfFulfillment, SoeOptional},
|
||||
{sfCondition, SoeOptional},
|
||||
{sfCredentialIDs, SoeOptional},
|
||||
{sfGas, SoeOptional},
|
||||
}))
|
||||
|
||||
|
||||
@@ -1164,10 +1161,6 @@ TRANSACTION(ttFEE, 101, SetFee,
|
||||
{sfBaseFeeDrops, SoeOptional},
|
||||
{sfReserveBaseDrops, SoeOptional},
|
||||
{sfReserveIncrementDrops, SoeOptional},
|
||||
// Smart Escrow fields
|
||||
{sfGasLimit, SoeOptional},
|
||||
{sfBytecodeSizeLimit, SoeOptional},
|
||||
{sfGasPrice, SoeOptional},
|
||||
}))
|
||||
|
||||
/** This system-generated transaction type is used to update the network's negative UNL
|
||||
|
||||
@@ -174,54 +174,6 @@ public:
|
||||
return this->sle_->isFieldPresent(sfFinishAfter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfBytecode (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getBytecode() const
|
||||
{
|
||||
if (hasBytecode())
|
||||
return this->sle_->at(sfBytecode);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfBytecode is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasBytecode() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfBytecode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfData (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getData() const
|
||||
{
|
||||
if (hasData())
|
||||
return this->sle_->at(sfData);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfData is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasData() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfSourceTag (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
@@ -501,28 +453,6 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfBytecode (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
EscrowBuilder&
|
||||
setBytecode(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfBytecode] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfData (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
EscrowBuilder&
|
||||
setData(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfData] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfSourceTag (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
|
||||
@@ -213,78 +213,6 @@ public:
|
||||
return this->sle_->isFieldPresent(sfReserveIncrementDrops);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfGasLimit (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getGasLimit() const
|
||||
{
|
||||
if (hasGasLimit())
|
||||
return this->sle_->at(sfGasLimit);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfGasLimit is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasGasLimit() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfGasLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfBytecodeSizeLimit (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getBytecodeSizeLimit() const
|
||||
{
|
||||
if (hasBytecodeSizeLimit())
|
||||
return this->sle_->at(sfBytecodeSizeLimit);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfBytecodeSizeLimit is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasBytecodeSizeLimit() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfBytecodeSizeLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfGasPrice (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getGasPrice() const
|
||||
{
|
||||
if (hasGasPrice())
|
||||
return this->sle_->at(sfGasPrice);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfGasPrice is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasGasPrice() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfGasPrice);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfPreviousTxnID (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
@@ -447,39 +375,6 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfGasLimit (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
FeeSettingsBuilder&
|
||||
setGasLimit(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfGasLimit] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfBytecodeSizeLimit (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
FeeSettingsBuilder&
|
||||
setBytecodeSizeLimit(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfBytecodeSizeLimit] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfGasPrice (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
FeeSettingsBuilder&
|
||||
setGasPrice(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfGasPrice] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfPreviousTxnID (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
|
||||
@@ -351,54 +351,6 @@ public:
|
||||
return this->sle_->isFieldPresent(sfAuditorEncryptionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfIssuerKeyEpoch (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getIssuerKeyEpoch() const
|
||||
{
|
||||
if (hasIssuerKeyEpoch())
|
||||
return this->sle_->at(sfIssuerKeyEpoch);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfIssuerKeyEpoch is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasIssuerKeyEpoch() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfIssuerKeyEpoch);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfAuditorKeyEpoch (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getAuditorKeyEpoch() const
|
||||
{
|
||||
if (hasAuditorKeyEpoch())
|
||||
return this->sle_->at(sfAuditorKeyEpoch);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfAuditorKeyEpoch is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasAuditorKeyEpoch() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfAuditorKeyEpoch);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfConfidentialOutstandingAmount (SoeDefault)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
@@ -648,28 +600,6 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfIssuerKeyEpoch (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
MPTokenIssuanceBuilder&
|
||||
setIssuerKeyEpoch(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfIssuerKeyEpoch] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfAuditorKeyEpoch (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
MPTokenIssuanceBuilder&
|
||||
setAuditorKeyEpoch(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfAuditorKeyEpoch] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfConfidentialOutstandingAmount (SoeDefault)
|
||||
* @return Reference to this builder for method chaining.
|
||||
|
||||
@@ -58,32 +58,6 @@ public:
|
||||
return this->tx_->at(sfDestination);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfDestinationTag (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getDestinationTag() const
|
||||
{
|
||||
if (hasDestinationTag())
|
||||
{
|
||||
return this->tx_->at(sfDestinationTag);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfDestinationTag is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasDestinationTag() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfDestinationTag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfAmount (SoeRequired)
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
@@ -175,55 +149,29 @@ public:
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfBytecode (SoeOptional)
|
||||
* @brief Get sfDestinationTag (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getBytecode() const
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getDestinationTag() const
|
||||
{
|
||||
if (hasBytecode())
|
||||
if (hasDestinationTag())
|
||||
{
|
||||
return this->tx_->at(sfBytecode);
|
||||
return this->tx_->at(sfDestinationTag);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfBytecode is present.
|
||||
* @brief Check if sfDestinationTag is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasBytecode() const
|
||||
hasDestinationTag() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfBytecode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfData (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getData() const
|
||||
{
|
||||
if (hasData())
|
||||
{
|
||||
return this->tx_->at(sfData);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfData is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasData() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfData);
|
||||
return this->tx_->isFieldPresent(sfDestinationTag);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -284,17 +232,6 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfDestinationTag (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
EscrowCreateBuilder&
|
||||
setDestinationTag(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfDestinationTag] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfAmount (SoeRequired)
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
@@ -341,24 +278,13 @@ public:
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfBytecode (SoeOptional)
|
||||
* @brief Set sfDestinationTag (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
EscrowCreateBuilder&
|
||||
setBytecode(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
setDestinationTag(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfBytecode] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfData (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
EscrowCreateBuilder&
|
||||
setData(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfData] = value;
|
||||
object_[sfDestinationTag] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
@@ -146,32 +146,6 @@ public:
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfCredentialIDs);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfGas (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getGas() const
|
||||
{
|
||||
if (hasGas())
|
||||
{
|
||||
return this->tx_->at(sfGas);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfGas is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasGas() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfGas);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -275,17 +249,6 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfGas (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
EscrowFinishBuilder&
|
||||
setGas(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfGas] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the EscrowFinish wrapper.
|
||||
* @param publicKey The public key for signing.
|
||||
|
||||
@@ -254,84 +254,6 @@ public:
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfReserveIncrementDrops);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfGasLimit (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getGasLimit() const
|
||||
{
|
||||
if (hasGasLimit())
|
||||
{
|
||||
return this->tx_->at(sfGasLimit);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfGasLimit is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasGasLimit() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfGasLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfBytecodeSizeLimit (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getBytecodeSizeLimit() const
|
||||
{
|
||||
if (hasBytecodeSizeLimit())
|
||||
{
|
||||
return this->tx_->at(sfBytecodeSizeLimit);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfBytecodeSizeLimit is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasBytecodeSizeLimit() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfBytecodeSizeLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfGasPrice (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getGasPrice() const
|
||||
{
|
||||
if (hasGasPrice())
|
||||
{
|
||||
return this->tx_->at(sfGasPrice);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfGasPrice is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasGasPrice() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfGasPrice);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -464,39 +386,6 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfGasLimit (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
SetFeeBuilder&
|
||||
setGasLimit(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfGasLimit] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfBytecodeSizeLimit (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
SetFeeBuilder&
|
||||
setBytecodeSizeLimit(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfBytecodeSizeLimit] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfGasPrice (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
SetFeeBuilder&
|
||||
setGasPrice(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfGasPrice] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the SetFee wrapper.
|
||||
* @param publicKey The public key for signing.
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -20,7 +19,6 @@
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -85,18 +83,6 @@ class NetworkOPs : public InfoSub::Source
|
||||
public:
|
||||
using clock_type = beast::AbstractClock<std::chrono::steady_clock>;
|
||||
|
||||
// Snapshot of per-operating-mode accounting, exposed for the datagram monitor.
|
||||
struct AccountingCounter
|
||||
{
|
||||
std::uint64_t transitions{0};
|
||||
std::chrono::microseconds dur{std::chrono::microseconds(0)};
|
||||
};
|
||||
using StateAccountingData = std::tuple<
|
||||
std::array<AccountingCounter, 5>,
|
||||
OperatingMode,
|
||||
std::chrono::steady_clock::time_point,
|
||||
std::uint64_t>;
|
||||
|
||||
enum class FailHard : unsigned char { No, Yes };
|
||||
static FailHard
|
||||
doFailHard(bool noMeansDont)
|
||||
@@ -117,8 +103,6 @@ public:
|
||||
|
||||
[[nodiscard]] virtual OperatingMode
|
||||
getOperatingMode() const = 0;
|
||||
[[nodiscard]] virtual StateAccountingData
|
||||
getStateAccountingData() = 0;
|
||||
[[nodiscard]] virtual std::string
|
||||
strOperatingMode(OperatingMode const mode, bool const admin = false) const = 0;
|
||||
[[nodiscard]] virtual std::string
|
||||
@@ -236,12 +220,6 @@ public:
|
||||
|
||||
virtual json::Value
|
||||
getConsensusInfo() = 0;
|
||||
// Proposers and round time of the last consensus round, for out-of-band
|
||||
// telemetry (DatagramMonitor) that cannot reach the private consensus object.
|
||||
[[nodiscard]] virtual std::size_t
|
||||
getPrevProposers() const = 0;
|
||||
[[nodiscard]] virtual std::chrono::milliseconds
|
||||
getPrevRoundTime() const = 0;
|
||||
virtual json::Value
|
||||
getServerInfo(bool human, bool admin, bool counters) = 0;
|
||||
virtual void
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <xrpl/ledger/ApplyViewImpl.h>
|
||||
#include <xrpl/ledger/OpenView.h>
|
||||
#include <xrpl/ledger/RawView.h>
|
||||
#include <xrpl/protocol/Book.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
@@ -129,6 +130,14 @@ public:
|
||||
view_->rawDestroyXRP(fee);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a newly-created order book directory with the shared,
|
||||
* process-wide OrderBookDB, unless this transaction is being applied
|
||||
* under TapDryRun.
|
||||
*/
|
||||
void
|
||||
addOrderBook(Book const& book);
|
||||
|
||||
ApplyViewContext
|
||||
getApplyViewContext()
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
@@ -393,16 +394,21 @@ preclaim(PreflightResult const& preflightResult, ServiceRegistry& registry, Open
|
||||
*
|
||||
* No validation is done or implied by this function.
|
||||
*
|
||||
* Caller is responsible for handling any exceptions.
|
||||
* Since none should be thrown, that will usually
|
||||
* mean terminating.
|
||||
*
|
||||
* Callers do not expect this function to throw; exceptions from a transactor's
|
||||
* `calculateBaseFee` are caught and reported as an error instead.
|
||||
* @param view The current open ledger.
|
||||
* @param tx The transaction to be checked.
|
||||
*
|
||||
* @return The base fee.
|
||||
* @return The base fee on success. Returns `std::unexpected(temUNKNOWN)` if the transaction
|
||||
* type is not recognized, and `std::unexpected(tefEXCEPTION)` if the transactor's
|
||||
* `calculateBaseFee` threw.
|
||||
*
|
||||
* @note Failure is reported as an error rather than a fee of zero because a
|
||||
* zero (or default) fee would pass checkFee and let the transaction be
|
||||
* applied for less than it owes. Callers that only need a fee hint may fall
|
||||
* back to a default; callers deciding whether to apply should reject.
|
||||
*/
|
||||
XRPAmount
|
||||
[[nodiscard]] std::expected<XRPAmount, TER>
|
||||
calculateBaseFee(ReadView const& view, STTx const& tx);
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
#include <xrpl/tx/invariants/SponsorshipInvariant.h>
|
||||
#include <xrpl/tx/invariants/VaultInvariant.h>
|
||||
|
||||
#include <boost/multiprecision/cpp_int.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <set>
|
||||
#include <string>
|
||||
@@ -139,7 +141,7 @@ public:
|
||||
*/
|
||||
class XRPNotCreated
|
||||
{
|
||||
std::int64_t drops_ = 0;
|
||||
boost::multiprecision::int128_t drops_ = 0;
|
||||
|
||||
public:
|
||||
void
|
||||
|
||||
@@ -38,9 +38,11 @@ namespace xrpl {
|
||||
* 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: neither `PrincipalOutstanding` nor
|
||||
* `TotalValueOutstanding` increases, and at least one of them
|
||||
* strictly decreases;
|
||||
* `PaymentRemaining` strictly decreases;
|
||||
* `NextPaymentDueDate` advances by N * `PaymentInterval`, N > 0.
|
||||
* `PaymentRemaining == 0` after: pinned by checks 1 and 5b.
|
||||
*
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/MathUtilities.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -23,6 +25,7 @@
|
||||
#include <ostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -502,6 +505,46 @@ public:
|
||||
};
|
||||
/** @endcond */
|
||||
|
||||
/** @cond INTERNAL */
|
||||
template <class T>
|
||||
[[nodiscard]] std::optional<T>
|
||||
checkedStepAddOpt(T const& lhs, T const& rhs)
|
||||
{
|
||||
if constexpr (std::is_same_v<T, XRPAmount>)
|
||||
{
|
||||
if (auto const r = checkedAdd(lhs.drops(), rhs.drops()))
|
||||
return XRPAmount{*r};
|
||||
return std::nullopt;
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, MPTAmount>)
|
||||
{
|
||||
if (auto const r = checkedAdd(lhs.value(), rhs.value()))
|
||||
return MPTAmount{*r};
|
||||
return std::nullopt;
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, IOUAmount>)
|
||||
{
|
||||
// IOUAmount is Number-backed and throws on overflow.
|
||||
return lhs + rhs;
|
||||
}
|
||||
else
|
||||
{
|
||||
// A new amount type must decide explicitly how to add; do not fall back
|
||||
// to an unchecked add.
|
||||
static_assert(sizeof(T) == 0, "checkedStepAddOpt: unsupported amount type");
|
||||
}
|
||||
}
|
||||
|
||||
template <class T>
|
||||
[[nodiscard]] T
|
||||
checkedStepAdd(T const& lhs, T const& rhs)
|
||||
{
|
||||
if (auto const r = checkedStepAddOpt(lhs, rhs))
|
||||
return *r;
|
||||
Throw<FlowException>(tecPATH_DRY);
|
||||
}
|
||||
/** @endcond */
|
||||
|
||||
/** @cond INTERNAL */
|
||||
// Check equal with tolerance
|
||||
bool
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
#include <cstdint>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <numeric>
|
||||
#include <optional>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
@@ -646,11 +645,21 @@ flow(
|
||||
boost::container::flat_multiset<TOutAmt> savedOuts;
|
||||
savedOuts.reserve(maxTries);
|
||||
|
||||
auto sum = [](auto const& col) {
|
||||
// Returns std::nullopt if the aggregate overflows; callers treat that as a
|
||||
// dry path.
|
||||
auto sum = [](auto const& col) -> std::optional<std::decay_t<decltype(*col.begin())>> {
|
||||
using TResult = std::decay_t<decltype(*col.begin())>;
|
||||
if (col.empty())
|
||||
return TResult{beast::kZero};
|
||||
return std::accumulate(col.begin() + 1, col.end(), *col.begin());
|
||||
TResult total = *col.begin();
|
||||
for (auto it = col.begin() + 1; it != col.end(); ++it)
|
||||
{
|
||||
auto const next = checkedStepAddOpt(total, *it);
|
||||
if (!next)
|
||||
return std::nullopt;
|
||||
total = *next;
|
||||
}
|
||||
return total;
|
||||
};
|
||||
|
||||
// These offers only need to be removed if the payment is not
|
||||
@@ -749,9 +758,17 @@ flow(
|
||||
{
|
||||
savedIns.insert(best->in);
|
||||
savedOuts.insert(best->out);
|
||||
remainingOut = outReq - sum(savedOuts);
|
||||
auto const sumOut = sum(savedOuts);
|
||||
if (!sumOut)
|
||||
return {tecPATH_DRY, std::move(ofrsToRmOnFail)};
|
||||
remainingOut = outReq - *sumOut;
|
||||
if (sendMax)
|
||||
remainingIn = *sendMax - sum(savedIns);
|
||||
{
|
||||
auto const sumIn = sum(savedIns);
|
||||
if (!sumIn)
|
||||
return {tecPATH_DRY, std::move(ofrsToRmOnFail)};
|
||||
remainingIn = *sendMax - *sumIn;
|
||||
}
|
||||
|
||||
if (flowDebugInfo)
|
||||
{
|
||||
@@ -786,8 +803,12 @@ flow(
|
||||
break;
|
||||
}
|
||||
|
||||
auto const actualOut = sum(savedOuts);
|
||||
auto const actualIn = sum(savedIns);
|
||||
auto const actualOutOpt = sum(savedOuts);
|
||||
auto const actualInOpt = sum(savedIns);
|
||||
if (!actualOutOpt || !actualInOpt)
|
||||
return {tecPATH_DRY, std::move(ofrsToRmOnFail)};
|
||||
auto const actualOut = *actualOutOpt;
|
||||
auto const actualIn = *actualInOpt;
|
||||
|
||||
JLOG(j.trace()) << "Total flow: in: " << to_string(actualIn)
|
||||
<< " out: " << to_string(actualOut);
|
||||
|
||||
@@ -131,9 +131,6 @@ Rust toolchain:
|
||||
✅ 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
|
||||
✅ rust-nightly
|
||||
rustc 1.99.0-nightly (87e5904f5 2026-07-20)
|
||||
/nix/store/fqpjz4l0nsnji8b2pz57mnj0akbp6hcl-rust-nightly/bin/rust-nightly
|
||||
✅ rustc
|
||||
rustc 1.97.1 (8bab26f4f 2026-07-14)
|
||||
/nix/store/bnfk1sl4s9angb0vj1cj9a5y5zvqinwy-rust-minimal-1.97.1/bin/rustc
|
||||
@@ -143,4 +140,4 @@ Rust toolchain:
|
||||
|
||||
Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set).
|
||||
|
||||
✅ All 45 checked tools are present and runnable.
|
||||
✅ All 44 checked tools are present and runnable.
|
||||
@@ -131,9 +131,6 @@ Rust toolchain:
|
||||
✅ 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
|
||||
✅ rust-nightly
|
||||
rustc 1.99.0-nightly (87e5904f5 2026-07-20)
|
||||
/nix/store/j7kf7a5h4xypzp6x1skg4dsdx2k4fwb3-rust-nightly/bin/rust-nightly
|
||||
✅ rustc
|
||||
rustc 1.97.1 (8bab26f4f 2026-07-14)
|
||||
/nix/store/40d3mzka7r1ps71l0yv2fs6616nbw85m-rust-minimal-1.97.1/bin/rustc
|
||||
@@ -171,4 +168,4 @@ Mold:
|
||||
|
||||
Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set).
|
||||
|
||||
✅ All 53 checked tools are present and runnable.
|
||||
✅ All 52 checked tools are present and runnable.
|
||||
@@ -131,9 +131,6 @@ Rust toolchain:
|
||||
✅ 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
|
||||
✅ rust-nightly
|
||||
rustc 1.99.0-nightly (87e5904f5 2026-07-20)
|
||||
/nix/store/c59pxk1yikdlf129qwyg4fplmxcrha0k-rust-nightly/bin/rust-nightly
|
||||
✅ rustc
|
||||
rustc 1.97.1 (8bab26f4f 2026-07-14)
|
||||
/nix/store/a6p27cg6b8szfixfyvkssx6l0c345zw8-rust-minimal-1.97.1/bin/rustc
|
||||
@@ -171,4 +168,4 @@ Mold:
|
||||
|
||||
Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set).
|
||||
|
||||
✅ All 53 checked tools are present and runnable.
|
||||
✅ All 52 checked tools are present and runnable.
|
||||
@@ -10,8 +10,6 @@ RUN mkdir -p ~/.config/nix && \
|
||||
COPY nix/ci-env.nix /tmp/build/nix/ci-env.nix
|
||||
COPY nix/linux.nix /tmp/build/nix/linux.nix
|
||||
COPY nix/packages.nix /tmp/build/nix/packages.nix
|
||||
COPY nix/rust-nightly.sh /tmp/build/nix/rust-nightly.sh
|
||||
COPY nix/rust.nix /tmp/build/nix/rust.nix
|
||||
COPY nix/utils.nix /tmp/build/nix/utils.nix
|
||||
COPY flake.nix /tmp/build/
|
||||
COPY flake.lock /tmp/build/
|
||||
|
||||
+54
-35
@@ -16,7 +16,23 @@ let
|
||||
exec ${pkgs.python3}/bin/python3 ${llvmPackages.clang-unwrapped}/bin/run-clang-tidy "$@"
|
||||
'';
|
||||
|
||||
rust = import ./rust.nix { inherit pkgs; };
|
||||
# rust-overlay's toolchain propagates the *default* stdenv.cc onto the PATH (so
|
||||
# cargo has a linker). That default may be different from the clang we pin here,
|
||||
# so it shadows our clang and the build can silently use a different compiler
|
||||
# version. Drop that cc from every propagation channel instead of pinning a
|
||||
# replacement: the toolchain then carries no compiler and cargo just uses the
|
||||
# active shell's stdenv cc. Must cover all channels — rust-overlay uses both
|
||||
# propagatedBuildInputs and depsHostHostPropagated.
|
||||
rustToolchainBase = pkgs.rust-bin.fromRustupToolchainFile ../rust-toolchain.toml;
|
||||
rustToolchain =
|
||||
let
|
||||
defaultCc = pkgs.stdenv.cc; # default compiler from nixpkgs stdenv
|
||||
withoutDefaultCc = builtins.filter (dep: (dep.outPath or "") != defaultCc.outPath);
|
||||
in
|
||||
rustToolchainBase.overrideAttrs (old: {
|
||||
propagatedBuildInputs = withoutDefaultCc (old.propagatedBuildInputs or [ ]);
|
||||
depsHostHostPropagated = withoutDefaultCc (old.depsHostHostPropagated or [ ]);
|
||||
});
|
||||
|
||||
# Nix wraps its toolchain so that binaries are exposed only under unsuffixed
|
||||
# names (gcc, g++, clang-tidy, ...). Several tools probe for a
|
||||
@@ -92,38 +108,41 @@ in
|
||||
mkGcov
|
||||
;
|
||||
|
||||
commonPackages =
|
||||
(with pkgs; [
|
||||
clangToolLinks
|
||||
runClangTidyLink
|
||||
ccache
|
||||
clangbuildanalyzer
|
||||
clangTools
|
||||
cmake
|
||||
conan
|
||||
curlMinimal # needed for codecov/codecov-action
|
||||
doxygen
|
||||
file # needed for cpack in Clio
|
||||
gcovr
|
||||
gh
|
||||
git
|
||||
git-cliff
|
||||
git-lfs
|
||||
gnumake
|
||||
gnupg # needed for signing commits & codecov/codecov-action
|
||||
graphviz
|
||||
less # needed for git diff
|
||||
mold
|
||||
nettools # provides netstat, used to debug failures in CI
|
||||
ninja
|
||||
patchelf
|
||||
perl # needed for openssl
|
||||
pkg-config
|
||||
pre-commit
|
||||
python3
|
||||
runClangTidy
|
||||
vim
|
||||
zip
|
||||
])
|
||||
++ rust.packages;
|
||||
commonPackages = with pkgs; [
|
||||
clangToolLinks
|
||||
runClangTidyLink
|
||||
ccache
|
||||
clangbuildanalyzer
|
||||
clangTools
|
||||
cmake
|
||||
conan
|
||||
curlMinimal # needed for codecov/codecov-action
|
||||
doxygen
|
||||
file # needed for cpack in Clio
|
||||
gcovr
|
||||
gh
|
||||
git
|
||||
git-cliff
|
||||
git-lfs
|
||||
gnumake
|
||||
gnupg # needed for signing commits & codecov/codecov-action
|
||||
graphviz
|
||||
less # needed for git diff
|
||||
mold
|
||||
nettools # provides netstat, used to debug failures in CI
|
||||
ninja
|
||||
patchelf
|
||||
perl # needed for openssl
|
||||
pkg-config
|
||||
pre-commit
|
||||
python3
|
||||
runClangTidy
|
||||
vim
|
||||
zip
|
||||
# Rust packages
|
||||
cargo-audit
|
||||
cargo-llvm-cov
|
||||
cargo-nextest
|
||||
rustToolchain
|
||||
];
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
#!@runtimeShell@
|
||||
# Reaches the nightly Rust toolchain, which is deliberately kept off PATH.
|
||||
# Packaged by nix/rust.nix, which explains why.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "usage: rust-nightly (path | run <command>...)" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
case "${1-}" in
|
||||
path) printf '%s\n' "@rustNightlyBin@" ;;
|
||||
run)
|
||||
shift
|
||||
if [[ $# -eq 0 ]]; then
|
||||
usage
|
||||
fi
|
||||
export PATH="@rustNightlyBin@:${PATH}"
|
||||
exec "$@"
|
||||
;;
|
||||
*) usage ;;
|
||||
esac
|
||||
@@ -1,84 +0,0 @@
|
||||
# The Rust half of the tool set shared by the CI environment and the dev shell:
|
||||
# the stable toolchain pinned by rust-toolchain.toml, the nightly the Rust
|
||||
# coverage job needs, and the cargo plugins. Consumed by packages.nix.
|
||||
{ pkgs }:
|
||||
let
|
||||
# rust-overlay's toolchain propagates the *default* stdenv.cc onto the PATH (so
|
||||
# cargo has a linker). That default may be different from the clang we pin
|
||||
# elsewhere, so it shadows our clang and the build can silently use a different
|
||||
# compiler version. Drop that cc from every propagation channel instead of
|
||||
# pinning a replacement: the toolchain then carries no compiler and cargo just
|
||||
# uses the active shell's stdenv cc.
|
||||
#
|
||||
# The channel list is every list mkDerivation propagates to a dependent's
|
||||
# environment (including the two legacy aliases). rust-overlay currently only
|
||||
# uses propagatedBuildInputs and depsHostHostPropagated, but covering all of
|
||||
# them means an upstream switch to another channel cannot quietly put the
|
||||
# compiler back on PATH.
|
||||
dropDefaultCc =
|
||||
toolchain:
|
||||
let
|
||||
defaultCc = pkgs.stdenv.cc; # default compiler from nixpkgs stdenv
|
||||
withoutDefaultCc = builtins.filter (dep: (dep.outPath or "") != defaultCc.outPath);
|
||||
in
|
||||
toolchain.overrideAttrs (
|
||||
old:
|
||||
pkgs.lib.genAttrs [
|
||||
"depsBuildBuildPropagated"
|
||||
"propagatedNativeBuildInputs" # alias of depsBuildHostPropagated
|
||||
"depsBuildTargetPropagated"
|
||||
"depsHostHostPropagated"
|
||||
"propagatedBuildInputs" # alias of depsHostTargetPropagated
|
||||
"depsTargetTargetPropagated"
|
||||
] (channel: withoutDefaultCc (old.${channel} or [ ]))
|
||||
);
|
||||
|
||||
rustToolchain = dropDefaultCc (pkgs.rust-bin.fromRustupToolchainFile ../rust-toolchain.toml);
|
||||
|
||||
# cargo-llvm-cov honours the #[coverage(off)] that keeps unit tests out of the
|
||||
# coverage report only under a nightly rustc, and looks for llvm-profdata and
|
||||
# llvm-cov in that same toolchain's sysroot — hence llvm-tools-preview.
|
||||
#
|
||||
# Not every nightly ships every component, so `nightly.latest` breaks on the
|
||||
# days llvm-tools-preview is absent; selectLatestNightlyWith walks back to the
|
||||
# newest one that has it. The result is the newest such nightly *known to the
|
||||
# locked rust-overlay*, which means updating flake.lock moves the compiler that
|
||||
# produces the coverage numbers — and with it the rustc version recorded in
|
||||
# nix/check-tools/*.txt, so those snapshots need regenerating alongside.
|
||||
rustNightly = dropDefaultCc (
|
||||
pkgs.rust-bin.selectLatestNightlyWith (
|
||||
toolchain: toolchain.minimal.override { extensions = [ "llvm-tools-preview" ]; }
|
||||
)
|
||||
);
|
||||
|
||||
# A second toolchain cannot go on PATH: its cargo and rustc would collide with
|
||||
# the pinned stable's in the ci-env buildEnv, which resolves collisions by
|
||||
# picking one silently. Reaching the nightly only through this wrapper keeps it
|
||||
# in the image closure (the Docker build copies the whole closure, not just
|
||||
# what is linked into /bin) while leaving it inactive everywhere that does not
|
||||
# ask for it.
|
||||
#
|
||||
# The script's `path` subcommand exists for scopes wider than one command — a
|
||||
# CI job appending to $GITHUB_PATH, so that the cargo cache action's own
|
||||
# `rustc -vV` probe, which runs in a step of its own, agrees with the toolchain
|
||||
# the build will use.
|
||||
rustNightlyScript = pkgs.replaceVarsWith {
|
||||
name = "rust-nightly";
|
||||
src = ./rust-nightly.sh;
|
||||
dir = "bin";
|
||||
isExecutable = true;
|
||||
replacements = {
|
||||
inherit (pkgs) runtimeShell;
|
||||
rustNightlyBin = "${rustNightly}/bin";
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
packages = [
|
||||
pkgs.cargo-audit
|
||||
pkgs.cargo-llvm-cov
|
||||
pkgs.cargo-nextest
|
||||
rustNightlyScript
|
||||
rustToolchain
|
||||
];
|
||||
}
|
||||
+95
-11
@@ -15,7 +15,8 @@ package/
|
||||
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, xrpld.lintian-overrides, source/format)
|
||||
debian/ Debian control files (control.in, lintian-overrides.in, rules, copyright, docs, links, source/format).
|
||||
The `.in` files are templates rendered by `build_pkg.py`; `docs` and `links` are staged under the package name
|
||||
shared/
|
||||
xrpld.service systemd unit file (used by both RPM and DEB)
|
||||
xrpld.sysusers sysusers.d config (used by both RPM and DEB)
|
||||
@@ -32,20 +33,74 @@ 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.
|
||||
have to stay in step. An optional `variant` names a flavour of the package (see
|
||||
[Package variants](#package-variants)), and CI passes it as `--variant`.
|
||||
|
||||
| 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`:
|
||||
To print the full packaging matrix (artifact names, images and package names)
|
||||
for the current `linux.json`:
|
||||
|
||||
```bash
|
||||
./.github/scripts/strategy-matrix/generate.py --packaging
|
||||
```
|
||||
|
||||
## Package variants
|
||||
|
||||
A config whose binaries are not the plain release build cannot be packaged as
|
||||
`xrpld`: both would carry the same name and version, so whichever published last
|
||||
would win. It is packaged as a **variant** instead — `variant: "assert"` in its
|
||||
`package` map, which CI passes to `build_pkg.py` as `--variant assert`,
|
||||
producing `xrpld-assert`. What the build option itself does is a build concern,
|
||||
not a packaging one; see the options table in [`BUILD.md`](../BUILD.md).
|
||||
|
||||
A variant ships the same paths as `xrpld` — `/usr/bin/xrpld`, `/etc/xrpld`,
|
||||
`xrpld.service`, `/etc/logrotate.d/xrpld` — differing only in the per-package
|
||||
documentation directory, so it declares itself a stand-in for the plain package
|
||||
rather than something installable next to it: `Conflicts`, `Replaces` and a
|
||||
versioned `Provides: xrpld` on Debian, `Conflicts` and `Provides` on RPM.
|
||||
Neither format declares `Obsoletes`, so `apt upgrade` and `dnf upgrade` keep an
|
||||
installed flavour on its own flavour, and switching is always explicit:
|
||||
|
||||
```bash
|
||||
apt-get install xrpld-assert # apt removes the plain package itself
|
||||
dnf swap xrpld xrpld-VARIANT # 'dnf install' alone stops at the conflict
|
||||
```
|
||||
|
||||
Only the DEB packages carry a variant today — `xrpld-assert` comes from the
|
||||
`debian` config alone, there being no call for an assert build on RHEL-based
|
||||
distributions — but the RPM side works the same way if one is added.
|
||||
|
||||
A switch is a removal plus an installation rather than an upgrade, so unlike a
|
||||
version upgrade it stops the service: Debian's scriptlets start it again, while
|
||||
on RPM the operator runs `systemctl start xrpld`. Configuration survives either
|
||||
way, being conffiles on Debian and `%config(noreplace)` on RPM.
|
||||
|
||||
`dnf` installs the replacement before erasing the old flavour, whose `%preun`
|
||||
would leave `xrpld.service` disabled, so `%postun` re-applies the preset when
|
||||
the unit file outlives the erase — which, since rpm keeps a file another
|
||||
installed package owns, happens only during a swap. The cost is that a
|
||||
deliberate `systemctl disable` is not carried across an RPM switch.
|
||||
|
||||
The alternative is an `xrpld-common` package owning the unit, the sysusers and
|
||||
tmpfiles snippets and the configuration, required by both flavours at an exact
|
||||
version: nothing is erased mid-swap, so no scriptlet has to detect one. It is
|
||||
not worth it for a single variant — it moves files out of the production
|
||||
package, and a sanitizer flavour would likely need its own unit anyway, putting
|
||||
the lifecycle back where it is now.
|
||||
|
||||
Adding a variant is the flavour in `VARIANTS` in `build_pkg.py`, which is the
|
||||
list `--variant` accepts, plus a config in `linux.json` with the CMake arguments
|
||||
and a `package` map naming it, for one format or for both: `generate.py
|
||||
--packaging` emits the package names per format, and the `test-install-deb` and
|
||||
`test-install-rpm` jobs install what their own format produced.
|
||||
|
||||
Operators switch between the flavours as described in
|
||||
[`docs/install.md`](../docs/install.md#optional-the-assert-enabled-build).
|
||||
|
||||
## Building packages
|
||||
|
||||
### Via CI
|
||||
@@ -56,9 +111,11 @@ Caller workflows (`on-pr.yml`, `on-tag.yml`, `on-trigger.yml`) call
|
||||
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.
|
||||
2. `test-install-deb` and `test-install-rpm` call
|
||||
[`reusable-package-test-install.yml`](../.github/workflows/reusable-package-test-install.yml)
|
||||
with their format's package names and distro images, installing each package
|
||||
in the container of every distro that format targets and running 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
|
||||
@@ -104,6 +161,9 @@ docker run --rm \
|
||||
# build/rpmbuild/RPMS/x86_64/*.rpm
|
||||
```
|
||||
|
||||
Add `--variant assert` to package binaries built with `-Dassert=ON`; the package
|
||||
is then named `xrpld-assert`.
|
||||
|
||||
### Via CMake (host-side target)
|
||||
|
||||
If you run CMake configure on a host that has `rpmbuild` or `dpkg-buildpackage`
|
||||
@@ -133,6 +193,9 @@ 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`.
|
||||
|
||||
`-Dassert=ON` passes `--variant assert`, so such a build packages as
|
||||
`xrpld-assert` without anything else being asked for.
|
||||
|
||||
## Publishing packages
|
||||
|
||||
Packages are published to the XRPLF repositories on Sonatype Nexus at
|
||||
@@ -147,6 +210,9 @@ the event, and `publish_pkg.py` maps that channel to its repositories:
|
||||
| push to `develop` | `xrpld --version` | `develop` | `deb-develop` | `rpm-develop-hosted` |
|
||||
| tag, non-public codebase | _any_ | `private` | `deb-private` | `rpm-private-hosted` |
|
||||
|
||||
A variant is published to the same channel under its own name, so
|
||||
`xrpld-assert` never overwrites `xrpld`.
|
||||
|
||||
Only a tag names a channel — do not extend that to `develop`, where
|
||||
`BuildInfo.cpp`'s `versionString` moves through `-bN`, `-rcN` and even the final
|
||||
version during a release cycle, which would send develop builds into `stable`.
|
||||
@@ -160,7 +226,7 @@ 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 its own job, gated behind `test-install`, uploading from the same
|
||||
Publishing is its own job, gated behind the install tests, 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
|
||||
@@ -175,7 +241,7 @@ Nexus owns the repository metadata; nothing here indexes anything. Worth knowing
|
||||
- Each apt-hosted repository needs a distribution (ours use `any`) and a PGP
|
||||
signing keypair configured in Nexus, which rejects one created without a
|
||||
keypair. Nexus signs the apt metadata with it, never the packages.
|
||||
- Hosted yum repositories cannot be signed by Nexus, so each `rpm-<channel>-hosted`
|
||||
- yum-hosted repositories cannot be signed by Nexus, so each `rpm-<channel>-hosted`
|
||||
repository sits behind a `rpm-<channel>` yum group repository whose metadata
|
||||
Nexus signs. Uploads go to the hosted repository; clients point at the group
|
||||
and verify the metadata with `repo_gpgcheck=1`. Nexus never signs the RPMs
|
||||
@@ -244,6 +310,19 @@ 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.
|
||||
|
||||
`--variant` is the flavour of the package, empty by default and accepting only
|
||||
the flavours in `VARIANTS`; see [Package variants](#package-variants). The RPM
|
||||
path passes it to the spec as the `pkg_variant` macro, which suffixes `Name` and
|
||||
adds the `Conflicts`/`Provides` pair. Debian control files have no conditionals, so the DEB path renders
|
||||
`debian/control.in` and `debian/lintian-overrides.in` instead, substituting
|
||||
`@PKG@` with the package name and `@VARIANT_FIELDS@` with the
|
||||
`Conflicts`/`Replaces`/`Provides` block, empty for the plain package; a token
|
||||
with no value fails the build rather than reaching dpkg. The files debhelper
|
||||
keys by package name (`docs`, `links`, and the units) are staged under that same
|
||||
name. The paths inside the package are unchanged either way, so `debian/rules`
|
||||
reads its package name from `dh_listpackages` and names the unit, sysusers,
|
||||
tmpfiles and logrotate files with `--name xrpld`.
|
||||
|
||||
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.
|
||||
@@ -286,8 +365,13 @@ service restart.
|
||||
1. Creates a staging source tree at `debbuild/source/` inside the build directory.
|
||||
2. Stages the binaries, configs, `README.md`, `LICENSE.md`, and
|
||||
`validator-keys-LICENSE`.
|
||||
3. Copies `package/debian/` control files into `debbuild/source/debian/`.
|
||||
4. Copies shared service/sysusers/tmpfiles/logrotate into `debian/` where `dh_installsystemd`, `dh_installsysusers`, `dh_installtmpfiles` and `dh_installlogrotate` pick them up automatically.
|
||||
3. Stages `package/debian/` into `debbuild/source/debian/`: the `.in` templates
|
||||
are rendered, and the files debhelper keys by package name (`docs`, `links`,
|
||||
`lintian-overrides`) are staged under the name being built.
|
||||
4. Copies shared service/sysusers/tmpfiles/logrotate into `debian/` as
|
||||
`<package>.xrpld.*`, which `dh_installsystemd`, `dh_installsysusers`,
|
||||
`dh_installtmpfiles` and `dh_installlogrotate` read because `debian/rules`
|
||||
passes them `--name xrpld`.
|
||||
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.
|
||||
|
||||
+90
-10
@@ -21,6 +21,14 @@ SRC_DIR = Path(__file__).resolve().parents[1]
|
||||
|
||||
PRE_RELEASE = re.compile(r"^(b|rc)(0|[1-9][0-9]*)(\+.*)?$")
|
||||
|
||||
# The package name a variant suffixes, and the name every variant keeps for its
|
||||
# on-disk paths (/usr/bin/xrpld, /etc/xrpld, xrpld.service).
|
||||
BASE_NAME = "xrpld"
|
||||
|
||||
# The flavours that can be built, '' being the plain xrpld package. A variant
|
||||
# needs a config in linux.json to be built by CI; see package/README.md.
|
||||
VARIANTS = ("", "assert")
|
||||
|
||||
# Files both packaging systems consume, staged under the same names.
|
||||
STAGED_FROM_BUILD = ("xrpld", "validator-keys", "validator-keys-LICENSE")
|
||||
STAGED_FROM_SRC = {
|
||||
@@ -31,6 +39,18 @@ STAGED_FROM_SRC = {
|
||||
}
|
||||
STAGED_UNITS = ("xrpld.service", "xrpld.sysusers", "xrpld.tmpfiles", "xrpld.logrotate")
|
||||
|
||||
# debian/ files debhelper keys by package name, staged as '<package>.<name>'.
|
||||
DEBIAN_PKG_FILES = ("docs", "links")
|
||||
|
||||
# Debian control files have no conditionals, so what makes a variant replace the
|
||||
# plain package is rendered into control.in rather than written there.
|
||||
DEB_VARIANT_FIELDS = """\
|
||||
Conflicts: xrpld
|
||||
Replaces: xrpld
|
||||
Provides: xrpld (= ${binary:Version})"""
|
||||
|
||||
TOKEN = re.compile(r"@[A-Z_]+@")
|
||||
|
||||
|
||||
def run(*command: object, cwd: Path | None = None) -> None:
|
||||
"""Echo a command and run it."""
|
||||
@@ -75,6 +95,28 @@ def package_version(reported: str) -> str:
|
||||
return version
|
||||
|
||||
|
||||
def render(template: Path, dest: Path, values: dict[str, str]) -> None:
|
||||
"""Write template to dest with its @TOKEN@ placeholders substituted.
|
||||
|
||||
A token left without a value fails the build rather than reaching dpkg.
|
||||
"""
|
||||
text = template.read_text()
|
||||
for token, value in values.items():
|
||||
text = text.replace(f"@{token}@", value)
|
||||
|
||||
missing = sorted(set(TOKEN.findall(text)))
|
||||
assert not missing, f"{template}: no value for {', '.join(missing)}"
|
||||
|
||||
# An empty value at the end of a stanza would otherwise leave a blank line,
|
||||
# which is what ends a stanza.
|
||||
dest.write_text(text.rstrip("\n") + "\n")
|
||||
|
||||
|
||||
def package_name(variant: str) -> str:
|
||||
"""The binary package name for a variant: '' -> xrpld, 'assert' -> xrpld-assert."""
|
||||
return f"{BASE_NAME}-{variant}" if variant else BASE_NAME
|
||||
|
||||
|
||||
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()
|
||||
@@ -135,17 +177,18 @@ def stage_common(build_dir: Path, dest: Path) -> None:
|
||||
shutil.copy2(SRC_DIR / source, dest / name)
|
||||
|
||||
|
||||
def stage_units(dest: Path) -> None:
|
||||
def stage_units(dest: Path, *, prefix: str = "") -> 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/.
|
||||
Each format wants them somewhere else: rpmbuild reads them from SOURCES by
|
||||
path, debhelper from debian/ by package name -- hence 'prefix', which makes
|
||||
the copies 'xrpld-assert.xrpld.service' and so on.
|
||||
"""
|
||||
for name in STAGED_UNITS:
|
||||
shutil.copy2(SRC_DIR / "package" / "shared" / name, dest / name)
|
||||
shutil.copy2(SRC_DIR / "package" / "shared" / name, dest / f"{prefix}{name}")
|
||||
|
||||
|
||||
def build_rpm(build_dir: Path, *, version: str, pkg_release: str) -> None:
|
||||
def build_rpm(build_dir: Path, *, version: str, pkg_release: str, variant: 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"):
|
||||
@@ -156,6 +199,9 @@ def build_rpm(build_dir: Path, *, version: str, pkg_release: str) -> None:
|
||||
stage_common(build_dir, topdir / "SOURCES")
|
||||
stage_units(topdir / "SOURCES")
|
||||
|
||||
# The spec defaults it to nothing, so a plain build is unchanged.
|
||||
variant_defines = ["--define", f"pkg_variant {variant}"] if variant else []
|
||||
|
||||
run(
|
||||
"rpmbuild",
|
||||
"-bb",
|
||||
@@ -168,10 +214,29 @@ def build_rpm(build_dir: Path, *, version: str, pkg_release: str) -> None:
|
||||
# The image tracks the newest distro, but the packages target el9.
|
||||
"--define",
|
||||
"dist .el9",
|
||||
*variant_defines,
|
||||
spec,
|
||||
)
|
||||
|
||||
|
||||
def stage_debian(dest: Path, name: str) -> None:
|
||||
"""Stage the debian directory for the package name being built."""
|
||||
source = SRC_DIR / "package" / "debian"
|
||||
shutil.copytree(
|
||||
source, dest, ignore=shutil.ignore_patterns("*.in", *DEBIAN_PKG_FILES)
|
||||
)
|
||||
|
||||
values = {
|
||||
"PKG": name,
|
||||
"VARIANT_FIELDS": "" if name == BASE_NAME else DEB_VARIANT_FIELDS,
|
||||
}
|
||||
render(source / "control.in", dest / "control", values)
|
||||
render(source / "lintian-overrides.in", dest / f"{name}.lintian-overrides", values)
|
||||
|
||||
for suffix in DEBIAN_PKG_FILES:
|
||||
shutil.copy2(source / suffix, dest / f"{name}.{suffix}")
|
||||
|
||||
|
||||
def build_deb(
|
||||
build_dir: Path,
|
||||
*,
|
||||
@@ -180,21 +245,23 @@ def build_deb(
|
||||
pkg_release: str,
|
||||
channel: str,
|
||||
epoch: int,
|
||||
name: str,
|
||||
) -> 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")
|
||||
stage_debian(staging / "debian", name)
|
||||
|
||||
# debhelper picks these up from debian/ automatically.
|
||||
stage_units(staging / "debian")
|
||||
# Prefixed whether it is a variant's name or not: debian/rules names them
|
||||
# explicitly either way.
|
||||
stage_units(staging / "debian", prefix=f"{name}.")
|
||||
|
||||
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
|
||||
{name} ({version}-{pkg_release}) {channel}; urgency=medium
|
||||
* Release {reported}.
|
||||
|
||||
-- XRPL Foundation <contact@xrplf.org> {date}
|
||||
@@ -223,6 +290,14 @@ def main() -> None:
|
||||
default="1",
|
||||
help="package release iteration (default: %(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--variant",
|
||||
default="",
|
||||
choices=VARIANTS,
|
||||
help="the flavour of the package to build: 'assert' produces "
|
||||
"xrpld-assert, which ships the same paths as xrpld and replaces it "
|
||||
"(default: the plain xrpld package)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--channel",
|
||||
required=True,
|
||||
@@ -234,6 +309,8 @@ def main() -> None:
|
||||
build_dir: Path = args.build_dir.resolve()
|
||||
pkg_release: str = args.pkg_release
|
||||
channel: str = args.channel
|
||||
variant: str = args.variant
|
||||
name = package_name(variant)
|
||||
|
||||
assert build_dir.is_dir(), (
|
||||
f"build directory not found: {build_dir}. Build the binaries before "
|
||||
@@ -253,6 +330,8 @@ def main() -> None:
|
||||
for tree in ("debbuild", "rpmbuild"):
|
||||
shutil.rmtree(build_dir / tree, ignore_errors=True)
|
||||
|
||||
print(f"Building {package_type} {name} {version}-{pkg_release}", flush=True)
|
||||
|
||||
if package_type == "deb":
|
||||
build_deb(
|
||||
build_dir,
|
||||
@@ -261,9 +340,10 @@ def main() -> None:
|
||||
pkg_release=pkg_release,
|
||||
channel=channel,
|
||||
epoch=epoch,
|
||||
name=name,
|
||||
)
|
||||
else:
|
||||
build_rpm(build_dir, version=version, pkg_release=pkg_release)
|
||||
build_rpm(build_dir, version=version, pkg_release=pkg_release, variant=variant)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Source: xrpld
|
||||
Source: @PKG@
|
||||
Section: net
|
||||
Priority: optional
|
||||
Maintainer: XRPL Foundation <[email protected]>
|
||||
@@ -11,7 +11,7 @@ Homepage: https://github.com/XRPLF/rippled
|
||||
Vcs-Git: https://github.com/XRPLF/rippled.git
|
||||
Vcs-Browser: https://github.com/XRPLF/rippled
|
||||
|
||||
Package: xrpld
|
||||
Package: @PKG@
|
||||
Architecture: any
|
||||
Depends:
|
||||
${shlibs:Depends},
|
||||
@@ -22,3 +22,4 @@ Description: XRP Ledger daemon
|
||||
transactions, and maintains the ledger database.
|
||||
This package also includes the validator-keys tool for validator key
|
||||
management.
|
||||
@VARIANT_FIELDS@
|
||||
File renamed without changes.
File renamed without changes.
@@ -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/links after rippled fully deprecated.
|
||||
@PKG@: dir-in-usr-local [usr/local/bin/]
|
||||
@PKG@: file-in-usr-local [usr/local/bin/rippled]
|
||||
@PKG@: file-in-unusual-dir [usr/local/bin/rippled]
|
||||
+33
-8
@@ -8,33 +8,58 @@ export DH_VERBOSE = 1
|
||||
# the binaries actually run on.
|
||||
LIBC_MIN = 2.31
|
||||
|
||||
# The binary package's name, which a variant build changes to e.g. xrpld-assert,
|
||||
# and the directory debhelper expects its files staged in.
|
||||
PKG := $(firstword $(shell dh_listpackages))
|
||||
PKG_DIR = debian/$(PKG)
|
||||
|
||||
# The base name, which every package ships under whatever it is called itself.
|
||||
BASE_NAME = xrpld
|
||||
|
||||
# What build_pkg.py stages beside this directory, each installed under its own
|
||||
# name. The binaries are also the ones checked against LIBC_MIN below.
|
||||
BINARIES = $(BASE_NAME) validator-keys
|
||||
CONFIGS = $(BASE_NAME).cfg validators.txt
|
||||
|
||||
%:
|
||||
dh $@
|
||||
|
||||
override_dh_auto_configure override_dh_auto_build override_dh_auto_test:
|
||||
@:
|
||||
|
||||
# The unit, sysusers, tmpfiles and logrotate files are named after the daemon
|
||||
# rather than after the package, so a variant still ships xrpld.service and
|
||||
# /etc/logrotate.d/xrpld. debhelper only reads debian/$(PKG).$(BASE_NAME).* when told
|
||||
# the name.
|
||||
override_dh_installsystemd:
|
||||
dh_installsystemd --no-stop-on-upgrade xrpld.service
|
||||
dh_installsystemd --no-stop-on-upgrade --name $(BASE_NAME)
|
||||
|
||||
# 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
|
||||
dh_installsysusers --name $(BASE_NAME)
|
||||
|
||||
override_dh_installsysusers:
|
||||
|
||||
override_dh_installtmpfiles:
|
||||
dh_installtmpfiles --name $(BASE_NAME)
|
||||
|
||||
override_dh_installlogrotate:
|
||||
dh_installlogrotate --name $(BASE_NAME)
|
||||
|
||||
override_dh_install:
|
||||
install -D -m 0755 xrpld debian/xrpld/usr/bin/xrpld
|
||||
install -D -m 0755 validator-keys debian/xrpld/usr/bin/validator-keys
|
||||
install -D -m 0644 xrpld.cfg debian/xrpld/etc/xrpld/xrpld.cfg
|
||||
install -D -m 0644 validators.txt debian/xrpld/etc/xrpld/validators.txt
|
||||
for binary in $(BINARIES); do \
|
||||
install -D -m 0755 "$$binary" "$(PKG_DIR)/usr/bin/$$binary"; \
|
||||
done
|
||||
for config in $(CONFIGS); do \
|
||||
install -D -m 0644 "$$config" "$(PKG_DIR)/etc/$(BASE_NAME)/$$config"; \
|
||||
done
|
||||
|
||||
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 \
|
||||
for binary in $(BINARIES); do \
|
||||
needed=$$(readelf --dyn-syms --wide $$binary \
|
||||
| grep -o 'GLIBC_[0-9.]*' | sed 's/GLIBC_//' | sort -uV | tail -1); \
|
||||
if [ -z "$$needed" ]; then \
|
||||
@@ -46,7 +71,7 @@ override_dh_shlibdeps:
|
||||
exit 1; \
|
||||
fi; \
|
||||
done
|
||||
sed -i 's/libc6 (>= [0-9.]*)/libc6 (>= $(LIBC_MIN))/' debian/xrpld.substvars
|
||||
sed -i 's/libc6 (>= [0-9.]*)/libc6 (>= $(LIBC_MIN))/' debian/$(PKG).substvars
|
||||
|
||||
override_dh_dwz:
|
||||
@:
|
||||
@@ -1,6 +0,0 @@
|
||||
# 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]
|
||||
+33
-16
@@ -6,10 +6,14 @@
|
||||
%{error:pkg_release must be defined}
|
||||
%endif
|
||||
|
||||
Name: xrpld
|
||||
# The base name, which every package ships under. A variant build
|
||||
# (build_pkg.py --variant) only suffixes the package name, e.g. xrpld-assert.
|
||||
%global base_name xrpld
|
||||
|
||||
Name: %{base_name}%{?pkg_variant:-%{pkg_variant}}
|
||||
Version: %{pkg_version}
|
||||
Release: %{pkg_release}%{?dist}
|
||||
Summary: XRP Ledger daemon
|
||||
Summary: XRP Ledger daemon%{?pkg_variant: (%{pkg_variant} build)}
|
||||
|
||||
License: ISC
|
||||
URL: https://github.com/XRPLF/rippled
|
||||
@@ -17,6 +21,12 @@ URL: https://github.com/XRPLF/rippled
|
||||
ExclusiveArch: x86_64 aarch64
|
||||
BuildRequires: systemd-rpm-macros
|
||||
|
||||
# A variant owns the same paths, so it stands in for the plain package.
|
||||
%if "%{?pkg_variant}" != ""
|
||||
Conflicts: %{base_name}
|
||||
Provides: %{base_name} = %{version}-%{release}
|
||||
%endif
|
||||
|
||||
# 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}
|
||||
@@ -52,22 +62,22 @@ management.
|
||||
:
|
||||
|
||||
%install
|
||||
install -Dm0755 %{_sourcedir}/xrpld %{buildroot}%{_bindir}/%{name}
|
||||
install -Dm0755 %{_sourcedir}/xrpld %{buildroot}%{_bindir}/%{base_name}
|
||||
install -Dm0755 %{_sourcedir}/validator-keys %{buildroot}%{_bindir}/validator-keys
|
||||
install -Dm0644 %{_sourcedir}/xrpld.cfg %{buildroot}%{_sysconfdir}/%{name}/xrpld.cfg
|
||||
install -Dm0644 %{_sourcedir}/validators.txt %{buildroot}%{_sysconfdir}/%{name}/validators.txt
|
||||
install -Dm0644 %{_sourcedir}/xrpld.cfg %{buildroot}%{_sysconfdir}/%{base_name}/xrpld.cfg
|
||||
install -Dm0644 %{_sourcedir}/validators.txt %{buildroot}%{_sysconfdir}/%{base_name}/validators.txt
|
||||
|
||||
# systemd units, sysusers, tmpfiles, preset
|
||||
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 -d %{buildroot}%{_presetdir}
|
||||
cat >%{buildroot}%{_presetdir}/50-xrpld.preset <<'EOF'
|
||||
cat >%{buildroot}%{_presetdir}/50-%{base_name}.preset <<'EOF'
|
||||
enable xrpld.service
|
||||
EOF
|
||||
|
||||
# Logrotate config
|
||||
install -Dm0644 %{_sourcedir}/xrpld.logrotate %{buildroot}%{_sysconfdir}/logrotate.d/%{name}
|
||||
install -Dm0644 %{_sourcedir}/xrpld.logrotate %{buildroot}%{_sysconfdir}/logrotate.d/%{base_name}
|
||||
|
||||
# Docs
|
||||
install -Dm0644 %{_sourcedir}/LICENSE.md %{buildroot}%{_docdir}/%{name}/LICENSE.md
|
||||
@@ -78,13 +88,13 @@ install -Dm0644 %{_sourcedir}/validator-keys-LICENSE %{buildroot}%{_docdir}/%{na
|
||||
# Legacy compatibility for pre-FHS package layouts.
|
||||
# TODO: remove after rippled fully deprecated.
|
||||
install -d %{buildroot}/usr/local/bin
|
||||
ln -s %{_bindir}/%{name} %{buildroot}/usr/local/bin/rippled
|
||||
ln -s %{_bindir}/%{base_name} %{buildroot}/usr/local/bin/rippled
|
||||
|
||||
%pre
|
||||
%sysusers_create_package %{name} %{_sourcedir}/xrpld.sysusers
|
||||
%sysusers_create_package %{base_name} %{_sourcedir}/xrpld.sysusers
|
||||
|
||||
%post
|
||||
%tmpfiles_create_package %{name} %{_sourcedir}/xrpld.tmpfiles
|
||||
%tmpfiles_create_package %{base_name} %{_sourcedir}/xrpld.tmpfiles
|
||||
%systemd_post xrpld.service
|
||||
|
||||
%preun
|
||||
@@ -92,6 +102,13 @@ ln -s %{_bindir}/%{name} %{buildroot}/usr/local/bin/rippled
|
||||
|
||||
%postun
|
||||
%systemd_postun xrpld.service
|
||||
# A flavour swap installs the replacement before erasing this package, so the
|
||||
# %%preun above has just disabled a unit the replacement still owns. rpm keeps a
|
||||
# file that another installed package owns, so the unit outliving our own erase
|
||||
# means exactly that; a plain erase takes it with us and re-presets nothing.
|
||||
if [ $1 -eq 0 ] && [ -f %{_unitdir}/xrpld.service ]; then
|
||||
systemctl preset xrpld.service >/dev/null 2>&1 || :
|
||||
fi
|
||||
|
||||
%files
|
||||
%attr(0755,root,root) %dir %{_docdir}/%{name}
|
||||
@@ -99,18 +116,18 @@ ln -s %{_bindir}/%{name} %{buildroot}/usr/local/bin/rippled
|
||||
%license %{_docdir}/%{name}/validator-keys-LICENSE
|
||||
%doc %{_docdir}/%{name}/README.md
|
||||
|
||||
%attr(0755,root,root) %dir %{_sysconfdir}/%{name}
|
||||
%attr(0755,root,root) %dir %{_sysconfdir}/%{base_name}
|
||||
|
||||
%{_bindir}/%{name}
|
||||
%{_bindir}/%{base_name}
|
||||
%{_bindir}/validator-keys
|
||||
|
||||
%config(noreplace) %{_sysconfdir}/%{name}/xrpld.cfg
|
||||
%config(noreplace) %{_sysconfdir}/%{name}/validators.txt
|
||||
%config(noreplace) %{_sysconfdir}/logrotate.d/%{name}
|
||||
%config(noreplace) %{_sysconfdir}/%{base_name}/xrpld.cfg
|
||||
%config(noreplace) %{_sysconfdir}/%{base_name}/validators.txt
|
||||
%config(noreplace) %{_sysconfdir}/logrotate.d/%{base_name}
|
||||
|
||||
|
||||
%{_unitdir}/xrpld.service
|
||||
%attr(0644,root,root) %{_presetdir}/50-xrpld.preset
|
||||
%attr(0644,root,root) %{_presetdir}/50-%{base_name}.preset
|
||||
%{_sysusersdir}/xrpld.conf
|
||||
%{_tmpfilesdir}/xrpld.conf
|
||||
%ghost %dir /var/lib/xrpld
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/MathUtilities.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
@@ -1155,6 +1156,10 @@ accountSendMultiIOU(
|
||||
|
||||
if (receiver)
|
||||
{
|
||||
// Confirm the running debit will not overflow before crediting.
|
||||
if (!checkedAdd(takeFromSender.xrp().drops(), amount.xrp().drops()))
|
||||
return tecINTERNAL;
|
||||
|
||||
// Increment XRP balance.
|
||||
auto const rcvBal = receiver->getFieldAmount(sfBalance);
|
||||
receiver->setFieldAmount(sfBalance, rcvBal + amount);
|
||||
@@ -1162,7 +1167,7 @@ accountSendMultiIOU(
|
||||
|
||||
view.update(receiver);
|
||||
|
||||
// Take what is actually sent
|
||||
// Take what is actually sent.
|
||||
takeFromSender += amount;
|
||||
}
|
||||
|
||||
@@ -1438,6 +1443,8 @@ directSendNoLimitMultiMPT(
|
||||
}
|
||||
|
||||
// Direct send: redeeming MPTs and/or sending own MPTs.
|
||||
if (!checkedAdd(actual.mpt().value(), amount.mpt().value()))
|
||||
return tecINTERNAL;
|
||||
if (auto const ter = directSendNoFeeMPT(view, senderID, receiverID, amount, j);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
@@ -1451,6 +1458,10 @@ directSendNoLimitMultiMPT(
|
||||
STAmount const actualSend = (waiveFee == WaiveTransferFee::Yes)
|
||||
? amount
|
||||
: multiply(amount, transferRate(view, amount.get<MPTIssue>().getMptID()));
|
||||
// actual is a superset of takeFromSender, so checking it before both add
|
||||
// sites also protects the debit accumulator.
|
||||
if (!checkedAdd(actual.mpt().value(), actualSend.mpt().value()))
|
||||
return tecINTERNAL;
|
||||
actual += actualSend;
|
||||
takeFromSender += actualSend;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace {
|
||||
//------------------------------------------------------------------------------
|
||||
// clang-format off
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
char const* const versionString = "3.4.0-rc1"
|
||||
char const* const versionString = "3.4.1"
|
||||
// clang-format on
|
||||
;
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include <xrpl/protocol/STPathSet.h>
|
||||
|
||||
#include <xrpl/basics/CountedObject.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/beast/hash/uhash.h>
|
||||
@@ -11,10 +13,12 @@
|
||||
#include <xrpl/protocol/STBase.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/detail/STVar.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -31,6 +35,11 @@ STPathElement::getHash(STPathElement const& element)
|
||||
// NIKB NOTE: This doesn't have to be a secure hash as speed is more
|
||||
// important. We don't even really need to fully hash the whole
|
||||
// base_uint here, as a few bytes would do for our use.
|
||||
//
|
||||
// The note above is only true because the result of this function reaches
|
||||
// nothing but STPathElement::operator==, where it is a fast-reject
|
||||
// prefilter ahead of the field comparisons that decide the answer. Do not
|
||||
// use it to key a container.
|
||||
|
||||
for (auto const x : element.getAccountID())
|
||||
hashAccount += (hashAccount * 257) ^ x;
|
||||
@@ -51,10 +60,49 @@ STPathElement::getHash(STPathElement const& element)
|
||||
return (hashAccount ^ hashCurrency ^ hashIssuer);
|
||||
}
|
||||
|
||||
[[nodiscard]] size_t
|
||||
STPathElement::getHash() const
|
||||
// For guidance on deciding which option to pursue:
|
||||
// 1. Try to decrease the size of the STPathSet first. For instance, if a std::optional was
|
||||
// injected into the type, could you get the same functionality using a std::unique_ptr instead?
|
||||
// 2. If the size of the STPathSet is already as small as it can be, then consider what the cost
|
||||
// of increasing STVar::kMaxSize would be on all the other STVar types. Each of those types
|
||||
// will carry the additional cost of accommodating the larger STPathSet in their SBO.
|
||||
// 3. If the cost of increasing STVar::kMaxSize is too high, then heap allocate the STPathSet and
|
||||
// remove this static_assert.
|
||||
static_assert(
|
||||
sizeof(STPathSet) <= detail::STVar::kMaxSize,
|
||||
"STPathSet is too large to fit in STVar's small object optimization. Please verify if it "
|
||||
"should, if the kMaxSize should be increased, or if STPathSet should be stored on the heap "
|
||||
"instead of in STVar.");
|
||||
|
||||
STPathSet::STPathSet(DeduplicationTag) : seen_{std::make_unique<hardened_hash_set<STPath>>()}
|
||||
{
|
||||
return STPathElement::getHash(*this);
|
||||
}
|
||||
|
||||
STPathSet::STPathSet(STPathSet const& other)
|
||||
: STBase{other}
|
||||
, CountedObject<STPathSet>{other}
|
||||
, value_{other.value_}
|
||||
, seen_{
|
||||
other.seen_ != nullptr ? std::make_unique<hardened_hash_set<STPath>>(*other.seen_)
|
||||
: nullptr}
|
||||
{
|
||||
}
|
||||
|
||||
STPathSet&
|
||||
STPathSet::operator=(STPathSet const& other)
|
||||
{
|
||||
if (this == &other)
|
||||
{
|
||||
return *this;
|
||||
}
|
||||
auto newSeen = other.seen_ != nullptr
|
||||
? std::make_unique<hardened_hash_set<STPath>>(*other.seen_)
|
||||
: nullptr;
|
||||
STBase::operator=(other);
|
||||
CountedObject<STPathSet>::operator=(other);
|
||||
value_ = other.value_;
|
||||
seen_ = std::move(newSeen);
|
||||
return *this;
|
||||
}
|
||||
|
||||
STPathSet::STPathSet(SerialIter& sit, SField const& name) : STBase(name)
|
||||
@@ -72,7 +120,8 @@ STPathSet::STPathSet(SerialIter& sit, SField const& name) : STBase(name)
|
||||
Throw<std::runtime_error>("empty path");
|
||||
}
|
||||
|
||||
pushBack(path);
|
||||
// Move rather than converting the vector to an STPath by copy.
|
||||
value_.emplace_back(std::move(path));
|
||||
path.clear();
|
||||
|
||||
if (iType == STPathElement::TypeNone)
|
||||
@@ -132,16 +181,10 @@ STPathSet::move(std::size_t n, void* buf)
|
||||
bool
|
||||
STPathSet::assembleAdd(STPath const& base, STPathElement const& tail)
|
||||
{ // assemble base+tail and add it to the set if it's not a duplicate
|
||||
XRPL_ASSERT(seen_ != nullptr, "xrpl::STPathSet::assembleAdd : DeduplicationTag");
|
||||
STPath combined = base;
|
||||
combined.pushBack(tail);
|
||||
|
||||
if (!seenHashes_.insert(combined).second)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value_.push_back(std::move(combined));
|
||||
return true;
|
||||
return appendUnique([&](auto& value) { value.push_back(std::move(combined)); });
|
||||
}
|
||||
|
||||
bool
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <xrpl/protocol/STValidation.h>
|
||||
|
||||
#include <xrpl/basics/Blob.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
@@ -15,6 +16,7 @@
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <exception>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -59,10 +61,6 @@ STValidation::validationFormat()
|
||||
{sfBaseFeeDrops, SoeOptional},
|
||||
{sfReserveBaseDrops, SoeOptional},
|
||||
{sfReserveIncrementDrops, SoeOptional},
|
||||
// featureSmartEscrow
|
||||
{sfGasLimit, SoeOptional},
|
||||
{sfBytecodeSizeLimit, SoeOptional},
|
||||
{sfGasPrice, SoeOptional},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
@@ -108,11 +106,42 @@ STValidation::isValid() const noexcept
|
||||
publicKeyType(getSignerPublic()) == KeyType::Secp256k1,
|
||||
"xrpl::STValidation::isValid : valid key type");
|
||||
|
||||
valid_ = verifyDigest(
|
||||
getSignerPublic(),
|
||||
getSigningHash(),
|
||||
makeSlice(getFieldVL(sfSignature)),
|
||||
(getFlags() & kVfFullyCanonicalSig) != 0u);
|
||||
// Log that the signature was never checked, so an operator does not
|
||||
// read this as a bad key. The log is guarded because it can throw too.
|
||||
auto reportUncheckable = [this](char const* reason) noexcept {
|
||||
try
|
||||
{
|
||||
JLOG(debugLog().error())
|
||||
<< "Cannot check the signature of the validation for ledger " << getLedgerHash()
|
||||
<< ": " << reason;
|
||||
}
|
||||
catch (...) // NOLINT(bugprone-empty-catch)
|
||||
{
|
||||
// Nothing can be reported when reporting is what failed.
|
||||
}
|
||||
};
|
||||
|
||||
// The signing hash re-serializes the fields, which can fail. This
|
||||
// function is noexcept, so report the validation as invalid instead of
|
||||
// throwing. valid_ stays unset, so a later call checks again.
|
||||
try
|
||||
{
|
||||
valid_ = verifyDigest(
|
||||
getSignerPublic(),
|
||||
getSigningHash(),
|
||||
makeSlice(getFieldVL(sfSignature)),
|
||||
(getFlags() & kVfFullyCanonicalSig) != 0u);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
reportUncheckable(e.what());
|
||||
return false;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
reportUncheckable("unknown exception");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return valid_.value();
|
||||
|
||||
@@ -143,10 +143,10 @@ Serializer::addFieldID(int type, int name)
|
||||
}
|
||||
|
||||
int
|
||||
Serializer::add8(unsigned char byte)
|
||||
Serializer::add8(unsigned char byteValue)
|
||||
{
|
||||
int const ret = data_.size();
|
||||
data_.push_back(byte);
|
||||
data_.push_back(byteValue);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -210,109 +210,138 @@ Serializer::addVL(void const* ptr, int len)
|
||||
int
|
||||
Serializer::addEncoded(int length)
|
||||
{
|
||||
std::array<std::uint8_t, 4> bytes{};
|
||||
// Without this, a negative length would fall into the 1 byte case below and
|
||||
// be cast to a first byte no header uses. A size too big for int arrives
|
||||
// here negative as well, since callers pass sizes through this parameter.
|
||||
if (length < kMinValueOfLengthFor1ByteHeader)
|
||||
Throw<std::overflow_error>("addEncoded: length is negative or did not fit in an int");
|
||||
|
||||
std::array<std::byte, kMaxNumberOfBytesInHeader> bytes{};
|
||||
int numBytes = 0;
|
||||
|
||||
if (length <= 192)
|
||||
if (length <= kMaxValueOfLengthFor1ByteHeader)
|
||||
{
|
||||
bytes[0] = static_cast<unsigned char>(length);
|
||||
bytes[0] = static_cast<std::byte>(length);
|
||||
numBytes = 1;
|
||||
}
|
||||
else if (length <= 12480)
|
||||
else if (length <= kMaxValueOfLengthFor2ByteHeader)
|
||||
{
|
||||
length -= 193;
|
||||
bytes[0] = 193 + static_cast<unsigned char>(length >> 8);
|
||||
bytes[1] = static_cast<unsigned char>(length & 0xff);
|
||||
// Count from the smallest length a 2 byte header covers.
|
||||
int const offset = length - kMinValueOfLengthFor2ByteHeader;
|
||||
bytes[0] = static_cast<std::byte>(
|
||||
kMinValueOfFirstByteFor2ByteHeader + (offset / kNumberOfValuesInOneByte));
|
||||
bytes[1] = static_cast<std::byte>(offset % kNumberOfValuesInOneByte);
|
||||
numBytes = 2;
|
||||
}
|
||||
else if (length <= 918744)
|
||||
else if (length <= kMaxValueOfLengthFor3ByteHeader)
|
||||
{
|
||||
length -= 12481;
|
||||
bytes[0] = 241 + static_cast<unsigned char>(length >> 16);
|
||||
bytes[1] = static_cast<unsigned char>((length >> 8) & 0xff);
|
||||
bytes[2] = static_cast<unsigned char>(length & 0xff);
|
||||
int const offset = length - kMinValueOfLengthFor3ByteHeader;
|
||||
bytes[0] = static_cast<std::byte>(
|
||||
kMinValueOfFirstByteFor3ByteHeader + (offset / kNumberOfValuesInTwoBytes));
|
||||
bytes[1] =
|
||||
static_cast<std::byte>((offset / kNumberOfValuesInOneByte) % kNumberOfValuesInOneByte);
|
||||
bytes[2] = static_cast<std::byte>(offset % kNumberOfValuesInOneByte);
|
||||
numBytes = 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
Throw<std::overflow_error>("lenlen");
|
||||
Throw<std::overflow_error>("addEncoded: length is too large to encode");
|
||||
}
|
||||
|
||||
return addRaw(&bytes[0], numBytes);
|
||||
return addRaw(bytes.data(), numBytes);
|
||||
}
|
||||
|
||||
int
|
||||
Serializer::encodeLengthLength(int length)
|
||||
{
|
||||
if (length < 0)
|
||||
Throw<std::overflow_error>("len<0");
|
||||
if (length < kMinValueOfLengthFor1ByteHeader)
|
||||
{
|
||||
Throw<std::overflow_error>(
|
||||
"encodeLengthLength: length is negative or did not fit in an int");
|
||||
}
|
||||
|
||||
if (length <= 192)
|
||||
if (length <= kMaxValueOfLengthFor1ByteHeader)
|
||||
return 1;
|
||||
|
||||
if (length <= 12480)
|
||||
if (length <= kMaxValueOfLengthFor2ByteHeader)
|
||||
return 2;
|
||||
|
||||
if (length <= 918744)
|
||||
if (length <= kMaxValueOfLengthFor3ByteHeader)
|
||||
return 3;
|
||||
|
||||
Throw<std::overflow_error>("len>918744");
|
||||
return 0; // Silence compiler warning.
|
||||
Throw<std::overflow_error>("encodeLengthLength: length is too large to encode");
|
||||
}
|
||||
|
||||
int
|
||||
Serializer::decodeLengthLength(int b1)
|
||||
Serializer::decodeLengthLength(std::byte firstByte)
|
||||
{
|
||||
if (b1 < 0)
|
||||
Throw<std::overflow_error>("b1<0");
|
||||
int const firstByteValue = std::to_integer<int>(firstByte);
|
||||
|
||||
if (b1 <= 192)
|
||||
if (firstByteValue <= kMaxValueOfFirstByteFor1ByteHeader)
|
||||
return 1;
|
||||
|
||||
if (b1 <= 240)
|
||||
if (firstByteValue <= kMaxValueOfFirstByteFor2ByteHeader)
|
||||
return 2;
|
||||
|
||||
if (b1 <= 254)
|
||||
if (firstByteValue <= kMaxValueOfFirstByteFor3ByteHeader)
|
||||
return 3;
|
||||
|
||||
Throw<std::overflow_error>("b1>254");
|
||||
return 0; // Silence compiler warning.
|
||||
Throw<std::overflow_error>("decodeLengthLength: first byte does not start any header");
|
||||
}
|
||||
|
||||
int
|
||||
Serializer::decodeVLLength(int b1)
|
||||
Serializer::decodeVLLength(std::byte firstByte)
|
||||
{
|
||||
if (b1 < 0)
|
||||
Throw<std::overflow_error>("b1<0");
|
||||
int const length = std::to_integer<int>(firstByte);
|
||||
|
||||
if (b1 > 254)
|
||||
Throw<std::overflow_error>("b1>254");
|
||||
// A bigger value means a longer header, so it is not a length by itself.
|
||||
if (length > kMaxValueOfLengthFor1ByteHeader)
|
||||
Throw<std::overflow_error>("decodeVLLength 1 byte: first byte is not a length");
|
||||
|
||||
return b1;
|
||||
return length;
|
||||
}
|
||||
|
||||
int
|
||||
Serializer::decodeVLLength(int b1, int b2)
|
||||
Serializer::decodeVLLength(std::byte firstByte, std::byte secondByte)
|
||||
{
|
||||
if (b1 < 193)
|
||||
Throw<std::overflow_error>("b1<193");
|
||||
int const firstByteValue = std::to_integer<int>(firstByte);
|
||||
|
||||
if (b1 > 240)
|
||||
Throw<std::overflow_error>("b1>240");
|
||||
if (firstByteValue < kMinValueOfFirstByteFor2ByteHeader)
|
||||
Throw<std::overflow_error>("decodeVLLength 2 byte: first byte is below the range");
|
||||
|
||||
return 193 + ((b1 - 193) * 256) + b2;
|
||||
if (firstByteValue > kMaxValueOfFirstByteFor2ByteHeader)
|
||||
Throw<std::overflow_error>("decodeVLLength 2 byte: first byte is above the range");
|
||||
|
||||
// Both bytes are bounded by their own type, and the first one is bounded to
|
||||
// the 2 byte range above, so this cannot leave the range the header covers.
|
||||
return kMinValueOfLengthFor2ByteHeader +
|
||||
((firstByteValue - kMinValueOfFirstByteFor2ByteHeader) * kNumberOfValuesInOneByte) +
|
||||
std::to_integer<int>(secondByte);
|
||||
}
|
||||
|
||||
int
|
||||
Serializer::decodeVLLength(int b1, int b2, int b3)
|
||||
Serializer::decodeVLLength(std::byte firstByte, std::byte secondByte, std::byte thirdByte)
|
||||
{
|
||||
if (b1 < 241)
|
||||
Throw<std::overflow_error>("b1<241");
|
||||
int const firstByteValue = std::to_integer<int>(firstByte);
|
||||
|
||||
if (b1 > 254)
|
||||
Throw<std::overflow_error>("b1>254");
|
||||
if (firstByteValue < kMinValueOfFirstByteFor3ByteHeader)
|
||||
Throw<std::overflow_error>("decodeVLLength 3 byte: first byte is below the range");
|
||||
|
||||
return 12481 + ((b1 - 241) * 65536) + (b2 * 256) + b3;
|
||||
if (firstByteValue > kMaxValueOfFirstByteFor3ByteHeader)
|
||||
Throw<std::overflow_error>("decodeVLLength 3 byte: first byte is above the range");
|
||||
|
||||
int const length = kMinValueOfLengthFor3ByteHeader +
|
||||
((firstByteValue - kMinValueOfFirstByteFor3ByteHeader) * kNumberOfValuesInTwoBytes) +
|
||||
(std::to_integer<int>(secondByte) * kNumberOfValuesInOneByte) +
|
||||
std::to_integer<int>(thirdByte);
|
||||
|
||||
// A 3 byte header reaches further than kMaxValueOfLengthFor3ByteHeader, which
|
||||
// is as far as the encoder goes. Refuse the rest, so every length accepted
|
||||
// here is one that can be written back.
|
||||
if (length > kMaxValueOfLengthFor3ByteHeader)
|
||||
Throw<std::overflow_error>("decodeVLLength 3 byte: length is too large to re-encode");
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -471,24 +500,24 @@ SerialIter::getRaw(int size)
|
||||
int
|
||||
SerialIter::getVLDataLength()
|
||||
{
|
||||
int const b1 = get8();
|
||||
std::byte const firstByte{get8()};
|
||||
int datLen = 0;
|
||||
int const lenLen = Serializer::decodeLengthLength(b1);
|
||||
int const lenLen = Serializer::decodeLengthLength(firstByte);
|
||||
if (lenLen == 1)
|
||||
{
|
||||
datLen = Serializer::decodeVLLength(b1);
|
||||
datLen = Serializer::decodeVLLength(firstByte);
|
||||
}
|
||||
else if (lenLen == 2)
|
||||
{
|
||||
int const b2 = get8();
|
||||
datLen = Serializer::decodeVLLength(b1, b2);
|
||||
std::byte const secondByte{get8()};
|
||||
datLen = Serializer::decodeVLLength(firstByte, secondByte);
|
||||
}
|
||||
else
|
||||
{
|
||||
XRPL_ASSERT(lenLen == 3, "xrpl::SerialIter::getVLDataLength : lenLen is 3");
|
||||
int const b2 = get8();
|
||||
int const b3 = get8();
|
||||
datLen = Serializer::decodeVLLength(b1, b2, b3);
|
||||
std::byte const secondByte{get8()};
|
||||
std::byte const thirdByte{get8()};
|
||||
datLen = Serializer::decodeVLLength(firstByte, secondByte, thirdByte);
|
||||
}
|
||||
return datLen;
|
||||
}
|
||||
|
||||
@@ -108,8 +108,6 @@ transResults()
|
||||
MAKE_ERROR(tecPRECISION_LOSS, "The amounts used by the transaction cannot interact."),
|
||||
MAKE_ERROR(tecBAD_PROOF, "Proof cannot be verified"),
|
||||
MAKE_ERROR(tecNO_SPONSOR_PERMISSION, "Sponsor has not authorized this transaction."),
|
||||
MAKE_ERROR(tecOUT_OF_GAS, "The WASM code ran out of gas during execution."),
|
||||
MAKE_ERROR(tecBYTECODE_REJECTED, "The custom WASM code that was run rejected your transaction."),
|
||||
|
||||
MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."),
|
||||
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),
|
||||
@@ -135,8 +133,6 @@ transResults()
|
||||
MAKE_ERROR(tefINVALID_LEDGER_FIX_TYPE, "The LedgerFixType field has an invalid value."),
|
||||
MAKE_ERROR(tefNO_DST_PARTIAL, "Partial payment to create account not allowed."),
|
||||
MAKE_ERROR(tefBAD_PATH_COUNT, "Malformed: Too many paths."),
|
||||
MAKE_ERROR(tefNO_BYTECODE, "There is no WASM code to run, but a WASM-specific field was included."),
|
||||
MAKE_ERROR(tefBYTECODE_NOT_INCLUDED, "WASM code requires a field that was not included."),
|
||||
|
||||
MAKE_ERROR(telLOCAL_ERROR, "Local failure."),
|
||||
MAKE_ERROR(telBAD_DOMAIN, "Domain too long."),
|
||||
@@ -208,8 +204,6 @@ transResults()
|
||||
MAKE_ERROR(temBAD_TRANSFER_FEE, "Malformed: Transfer fee is outside valid range."),
|
||||
MAKE_ERROR(temINVALID_INNER_BATCH, "Malformed: Invalid inner batch transaction."),
|
||||
MAKE_ERROR(temBAD_CIPHERTEXT, "Malformed: Invalid ciphertext."),
|
||||
MAKE_ERROR(temINVALID_BYTECODE, "Malformed: Provided byte code is invalid."),
|
||||
MAKE_ERROR(temTEMP_DISABLED, "The transaction requires logic that is currently temporarily disabled."),
|
||||
|
||||
MAKE_ERROR(terRETRY, "Retry transaction."),
|
||||
MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."),
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/OpenView.h>
|
||||
#include <xrpl/ledger/OrderBookDB.h>
|
||||
#include <xrpl/protocol/Book.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxMeta.h>
|
||||
@@ -54,6 +56,13 @@ ApplyContext::apply(TER ter)
|
||||
return view_->apply(base_, tx, ter, parentBatchId_, (flags_ & TapDryRun) != 0u, journal);
|
||||
}
|
||||
|
||||
void
|
||||
ApplyContext::addOrderBook(Book const& book)
|
||||
{
|
||||
if ((flags_ & TapDryRun) == TapNone)
|
||||
registry.get().getOrderBookDB().addOrderBook(book);
|
||||
}
|
||||
|
||||
std::size_t
|
||||
ApplyContext::size()
|
||||
{
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <expected>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
@@ -195,7 +196,12 @@ invokePreclaim(PreclaimContext const& ctx)
|
||||
}())
|
||||
return preSigResult;
|
||||
|
||||
if (TER const result = T::checkFee(ctx, calculateBaseFee(ctx.view, ctx.tx)))
|
||||
// We can't check the fee if we can't compute it, so reject.
|
||||
auto const baseFee = calculateBaseFee(ctx.view, ctx.tx);
|
||||
if (!baseFee)
|
||||
return baseFee.error();
|
||||
|
||||
if (TER const result = T::checkFee(ctx, *baseFee))
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -223,13 +229,12 @@ invokePreclaim(PreclaimContext const& ctx)
|
||||
*
|
||||
* @param view The ledger view to use for fee calculation.
|
||||
* @param tx The transaction for which the base fee is to be calculated.
|
||||
* @return The calculated base fee as an XRPAmount.
|
||||
* @return The calculated base fee. Returns `std::unexpected(temUNKNOWN)` if the transaction
|
||||
* type is not recognized, and `std::unexpected(tefEXCEPTION)` if the transactor's
|
||||
* `calculateBaseFee` threw.
|
||||
*
|
||||
* @throws std::exception If an error occurs during fee calculation, including
|
||||
* but not limited to unknown transaction types or internal errors, the function
|
||||
* logs an error and returns an XRPAmount of zero.
|
||||
*/
|
||||
static XRPAmount
|
||||
static std::expected<XRPAmount, TER>
|
||||
invokeCalculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
try
|
||||
@@ -238,13 +243,25 @@ invokeCalculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
return T::calculateBaseFee(view, tx);
|
||||
});
|
||||
}
|
||||
catch (UnknownTxnType const& e)
|
||||
catch (UnknownTxnType const&)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::invoke_calculateBaseFee : unknown transaction type");
|
||||
return XRPAmount{0};
|
||||
return std::unexpected(temUNKNOWN);
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(debugLog().error()) << "calculateBaseFee: " << tx.getTransactionID()
|
||||
<< " threw an exception: " << e.what();
|
||||
return std::unexpected(tefEXCEPTION);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
JLOG(debugLog().error()) << "calculateBaseFee: " << tx.getTransactionID()
|
||||
<< " threw an unknown exception";
|
||||
return std::unexpected(tefEXCEPTION);
|
||||
}
|
||||
}
|
||||
|
||||
TxConsequences::TxConsequences(NotTEC pfResult)
|
||||
@@ -416,7 +433,7 @@ preclaim(PreflightResult const& preflightResult, ServiceRegistry& registry, Open
|
||||
}
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
std::expected<XRPAmount, TER>
|
||||
calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
return invokeCalculateBaseFee(view, tx);
|
||||
@@ -441,13 +458,26 @@ doApply(PreclaimResult const& preclaimResult, ServiceRegistry& registry, OpenVie
|
||||
{
|
||||
if (!preclaimResult.likelyToClaimFee)
|
||||
return {preclaimResult.ter, false};
|
||||
|
||||
// For any tx with a real account, preclaim already computed this fee
|
||||
// successfully against this same view.
|
||||
auto const baseFee = calculateBaseFee(view, preclaimResult.tx);
|
||||
if (!baseFee)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(preclaimResult.j.error())
|
||||
<< "apply: could not compute base fee: " << transToken(baseFee.error());
|
||||
return {tefINTERNAL, false};
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
ApplyContext ctx(
|
||||
registry,
|
||||
view,
|
||||
preclaimResult.parentBatchId,
|
||||
preclaimResult.tx,
|
||||
preclaimResult.ter,
|
||||
calculateBaseFee(view, preclaimResult.tx),
|
||||
*baseFee,
|
||||
preclaimResult.flags,
|
||||
preclaimResult.j);
|
||||
return invokeApply(ctx);
|
||||
|
||||
@@ -210,6 +210,9 @@ XRPNotCreated::finalize(
|
||||
ReadView const&,
|
||||
beast::Journal const& j) const
|
||||
{
|
||||
// drops_ is a full-width running total, so a value that would have wrapped a
|
||||
// 64-bit accumulator is caught here.
|
||||
|
||||
// The net change should never be positive, as this would mean that the
|
||||
// transaction created XRP out of thin air. That's not possible.
|
||||
if (drops_ > 0)
|
||||
|
||||
@@ -231,17 +231,38 @@ ValidLoan::finalize(
|
||||
// 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.
|
||||
//
|
||||
// PrincipalOutstanding may stay put on a non-final pay: at integer
|
||||
// scale, fixCleanup3_2_0 rounds principal up so a fractional
|
||||
// amortization step does not reduce it. Interest (TVO) still falls.
|
||||
// Neither balance may grow: a payment never adds to what is owed,
|
||||
// since late-payment penalties are charged in the same transaction
|
||||
// rather than tracked in TotalValueOutstanding.
|
||||
if (isTesSuccess(result) && txType == ttLOAN_PAY)
|
||||
{
|
||||
if (before && after->at(sfPaymentRemaining) != 0)
|
||||
{
|
||||
if (!(after->at(sfPrincipalOutstanding) < before->at(sfPrincipalOutstanding)))
|
||||
if (after->at(sfPrincipalOutstanding) > before->at(sfPrincipalOutstanding))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: loan pay must strictly decrease "
|
||||
JLOG(j.fatal()) << "Invariant failed: loan pay must not increase "
|
||||
"PrincipalOutstanding on a non-full-repayment";
|
||||
return false;
|
||||
}
|
||||
if (!(after->at(sfPaymentRemaining) < before->at(sfPaymentRemaining)))
|
||||
if (after->at(sfTotalValueOutstanding) > before->at(sfTotalValueOutstanding))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: loan pay must not increase "
|
||||
"TotalValueOutstanding on a non-full-repayment";
|
||||
return false;
|
||||
}
|
||||
if (after->at(sfPrincipalOutstanding) == before->at(sfPrincipalOutstanding) &&
|
||||
after->at(sfTotalValueOutstanding) == before->at(sfTotalValueOutstanding))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: loan pay must decrease "
|
||||
"PrincipalOutstanding or TotalValueOutstanding "
|
||||
"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";
|
||||
|
||||
@@ -299,27 +299,46 @@ ValidMPTIssuance::finalize(
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (lendingProtocolEnabled && (mptokensCreated_ + mptokensDeleted_) > 1)
|
||||
else
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: MPT authorize succeeded "
|
||||
"but created/deleted bad number mptokens";
|
||||
return false;
|
||||
}
|
||||
else if (submittedByIssuer && (mptokensCreated_ > 0 || mptokensDeleted_ > 0))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: MPT authorize submitted by issuer "
|
||||
"succeeded but created/deleted mptokens";
|
||||
return false;
|
||||
}
|
||||
else if (
|
||||
!submittedByIssuer && hasPrivilege(tx, Privilege::MustAuthorizeMpt) &&
|
||||
(mptokensCreated_ + mptokensDeleted_ != 1))
|
||||
{
|
||||
// if the holder submitted this tx, then a mptoken must be
|
||||
// either created or deleted.
|
||||
JLOG(j.fatal()) << "Invariant failed: MPT authorize submitted by holder "
|
||||
"succeeded but created/deleted bad number of mptokens";
|
||||
return false;
|
||||
// Cap on MPToken creates and deletes while featureLendingProtocol is enabled.
|
||||
// - LoanSet: at most two creates and no deletes.
|
||||
// - VaultWithdraw: at most one create and one delete.
|
||||
// - Other MayAuthorizeMpt types: created + deleted <= 1.
|
||||
// - MustAuthorizeMpt still requires exactly one create or delete below.
|
||||
auto const mptokensExceedAuthorizeCap = [&] {
|
||||
if (!lendingProtocolEnabled)
|
||||
return false;
|
||||
if (rules.enabled(fixCleanup3_4_0))
|
||||
{
|
||||
if (txnType == ttLOAN_SET)
|
||||
return mptokensDeleted_ != 0 || mptokensCreated_ > 2;
|
||||
if (txnType == ttVAULT_WITHDRAW)
|
||||
return mptokensCreated_ > 1 || mptokensDeleted_ > 1;
|
||||
}
|
||||
return (mptokensCreated_ + mptokensDeleted_) > 1;
|
||||
};
|
||||
if (mptokensExceedAuthorizeCap())
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: MPT authorize succeeded "
|
||||
"but created/deleted bad number mptokens";
|
||||
return false;
|
||||
}
|
||||
if (submittedByIssuer && (mptokensCreated_ > 0 || mptokensDeleted_ > 0))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: MPT authorize submitted by issuer "
|
||||
"succeeded but created/deleted mptokens";
|
||||
return false;
|
||||
}
|
||||
if (!submittedByIssuer && hasPrivilege(tx, Privilege::MustAuthorizeMpt) &&
|
||||
(mptokensCreated_ + mptokensDeleted_ != 1))
|
||||
{
|
||||
// if the holder submitted this tx, then a mptoken must be
|
||||
// either created or deleted.
|
||||
JLOG(j.fatal()) << "Invariant failed: MPT authorize submitted by holder "
|
||||
"succeeded but created/deleted bad number of mptokens";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1060,7 +1060,10 @@ sum(TCollection const& col)
|
||||
using TResult = std::decay_t<decltype(*col.begin())>;
|
||||
if (col.empty())
|
||||
return TResult{beast::kZero};
|
||||
return std::accumulate(col.begin() + 1, col.end(), *col.begin());
|
||||
return std::accumulate(
|
||||
col.begin() + 1, col.end(), *col.begin(), [](TResult const& a, TResult const& b) {
|
||||
return checkedStepAdd(a, b);
|
||||
});
|
||||
};
|
||||
|
||||
template <class TIn, class TOut, class TDerived>
|
||||
|
||||
@@ -439,6 +439,12 @@ CheckCash::doApply()
|
||||
AccountID const& deliverIssuer = flowDeliver.getIssuer();
|
||||
auto const err = flowDeliver.asset().visit(
|
||||
[&](Issue const& issue) -> std::optional<TER> {
|
||||
// An issuer needs no holder-limit waiver to receive its own currency.
|
||||
if (deliverIssuer == accountID_ && ctx_.view().rules().enabled(fixCleanup3_4_0))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// If a trust line does not exist yet create one.
|
||||
Issue const& trustLineIssue = issue;
|
||||
AccountID const truster = deliverIssuer == accountID_ ? srcId : accountID_;
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/OrderBookDB.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/Sandbox.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
@@ -397,7 +395,7 @@ applyCreate(ApplyContext& ctx, Sandbox& sb, AccountID const& account, beast::Jou
|
||||
Book const book{assetIn, assetOut, std::nullopt};
|
||||
auto const dir = keylet::quality(keylet::book(book), uRate);
|
||||
if (auto const bookExisted = static_cast<bool>(sb.read(dir)); !bookExisted)
|
||||
ctx.registry.get().getOrderBookDB().addOrderBook(book);
|
||||
ctx.addOrderBook(book);
|
||||
};
|
||||
addOrderBook(amount.asset(), amount2.asset(), getRate(amount2, amount));
|
||||
addOrderBook(amount2.asset(), amount.asset(), getRate(amount, amount2));
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/OrderBookDB.h>
|
||||
#include <xrpl/ledger/PaymentSandbox.h>
|
||||
#include <xrpl/ledger/Sandbox.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
@@ -634,7 +633,7 @@ OfferCreate::applyHybrid(
|
||||
bookArr.pushBack(std::move(bookInfo));
|
||||
|
||||
if (!bookExists)
|
||||
ctx_.registry.get().getOrderBookDB().addOrderBook(book);
|
||||
ctx_.addOrderBook(book);
|
||||
|
||||
sleOffer->setFieldArray(sfAdditionalBooks, bookArr);
|
||||
return tesSUCCESS;
|
||||
@@ -1014,7 +1013,7 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel)
|
||||
sb.insert(sleOffer);
|
||||
|
||||
if (!bookExisted)
|
||||
ctx_.registry.get().getOrderBookDB().addOrderBook(book);
|
||||
ctx_.addOrderBook(book);
|
||||
|
||||
JLOG(j_.debug()) << "final result: success";
|
||||
|
||||
|
||||
@@ -88,13 +88,8 @@ EscrowCreate::checkExtraFeatures(PreflightContext const& ctx)
|
||||
// Only require featureMPTokensV1 when the escrow amount is an MPT and
|
||||
// fixCleanup3_2_0 is active; XRP/IOU escrows are unaffected by this gate.
|
||||
if (ctx.rules.enabled(fixCleanup3_2_0) && ctx.tx[sfAmount].holds<MPTIssue>())
|
||||
{
|
||||
if (!ctx.rules.enabled(featureMPTokensV1))
|
||||
return false;
|
||||
}
|
||||
|
||||
return (!ctx.tx.isFieldPresent(sfBytecode) && !ctx.tx.isFieldPresent(sfData)) ||
|
||||
ctx.rules.enabled(featureSmartEscrow);
|
||||
return ctx.rules.enabled(featureMPTokensV1);
|
||||
return true;
|
||||
}
|
||||
|
||||
template <ValidIssueType T>
|
||||
|
||||
@@ -65,13 +65,7 @@ checkCondition(Slice f, Slice c)
|
||||
bool
|
||||
EscrowFinish::checkExtraFeatures(PreflightContext const& ctx)
|
||||
{
|
||||
if (ctx.tx.isFieldPresent(sfCredentialIDs) && !ctx.rules.enabled(featureCredentials))
|
||||
return false;
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfGas) && !ctx.rules.enabled(featureSmartEscrow))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
return !ctx.tx.isFieldPresent(sfCredentialIDs) || ctx.rules.enabled(featureCredentials);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
|
||||
@@ -36,6 +36,15 @@
|
||||
namespace xrpl {
|
||||
|
||||
namespace {
|
||||
// Returns true if the transaction's payment amount is malformed. A loan
|
||||
// payment must be strictly positive: zero would move nothing, and a negative
|
||||
// amount is not a payment at all.
|
||||
bool
|
||||
isPaymentAmountInvalid(STAmount const& amount)
|
||||
{
|
||||
return amount <= beast::kZero;
|
||||
}
|
||||
|
||||
// Returns the account's true, unclamped balance in `asset`, for use only in
|
||||
// fund-conservation checks. accountHolds(..., SpendableHandling::FullBalance)
|
||||
// cannot be used for this: for XRP it always defers to xrpLiquid, which
|
||||
@@ -81,7 +90,7 @@ LoanPay::preflight(PreflightContext const& ctx)
|
||||
if (ctx.tx[sfLoanID] == beast::kZero)
|
||||
return temINVALID;
|
||||
|
||||
if (ctx.tx[sfAmount] <= beast::kZero)
|
||||
if (isPaymentAmountInvalid(ctx.tx[sfAmount]))
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
// The loan payment flags are all mutually exclusive. If more than one is
|
||||
@@ -103,10 +112,19 @@ LoanPay::preflight(PreflightContext const& ctx)
|
||||
XRPAmount
|
||||
LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
auto fixEnabled313 = view.rules().enabled(fixCleanup3_1_3);
|
||||
auto fixEnabled340 = view.rules().enabled(fixCleanup3_4_0);
|
||||
|
||||
using namespace lending;
|
||||
|
||||
auto const normalCost = Transactor::calculateBaseFee(view, tx);
|
||||
|
||||
if (fixEnabled340 && isPaymentAmountInvalid(tx[sfAmount]))
|
||||
{
|
||||
// Let preflight worry about the error for this
|
||||
return normalCost;
|
||||
}
|
||||
|
||||
if (tx.isFlag(tfLoanFullPayment) || tx.isFlag(tfLoanLatePayment))
|
||||
{
|
||||
// The loan will be making one set of calculations for one full or late
|
||||
@@ -179,8 +197,7 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
static constexpr std::int64_t kMaxFeeIncrements =
|
||||
kLoanMaximumPaymentsPerTransaction / kLoanPaymentsPerFeeIncrement;
|
||||
|
||||
if (view.rules().enabled(fixCleanup3_1_3) &&
|
||||
amount >= regularPayment * kLoanMaximumPaymentsPerTransaction)
|
||||
if (fixEnabled313 && amount >= regularPayment * kLoanMaximumPaymentsPerTransaction)
|
||||
{
|
||||
// The payment handler will never process more than
|
||||
// loanMaximumPaymentsPerTransaction payments (including overpayments),
|
||||
|
||||
@@ -339,17 +339,36 @@ Payment::checkGranularSemantics(
|
||||
bool const accountIsHolder =
|
||||
accountIsLow ? rawBalance > beast::kZero : rawBalance < beast::kZero;
|
||||
|
||||
bool const mayIssue =
|
||||
heldGranularPermissions.contains(PaymentMint) && destLimit > beast::kZero;
|
||||
|
||||
// PaymentMint requires the destination to be the holder and the account to be the
|
||||
// issuer. destLimit > 0: destination is willing to hold account's IOUs (account is the
|
||||
// issuer). !accountIsHolder: DirectStepI will issue, not redeem.
|
||||
if (heldGranularPermissions.contains(PaymentMint) && destLimit > beast::kZero &&
|
||||
!accountIsHolder)
|
||||
if (mayIssue && !accountIsHolder)
|
||||
return tesSUCCESS;
|
||||
|
||||
// PaymentBurn requires the source account to be the holder and the destination to be
|
||||
// the issuer. accountIsHolder: DirectStepI will redeem, not issue.
|
||||
if (heldGranularPermissions.contains(PaymentBurn) && accountIsHolder)
|
||||
return tesSUCCESS;
|
||||
{
|
||||
if (view.rules().enabled(fixCleanup3_4_0))
|
||||
{
|
||||
// Redeeming stops at the balance held; beyond that the payment engine
|
||||
// crosses zero and issues the account's own IOUs, which is a mint. So with
|
||||
// only PaymentBurn we must check the amount against the balance held. The
|
||||
// granular template forbids sfPaths, tfPartialPayment and a cross-asset
|
||||
// sfSendMax, so this is a single direct step, sfAmount is what the
|
||||
// trustline is debited.
|
||||
STAmount const held = accountIsLow ? rawBalance : -rawBalance;
|
||||
if (dstAmount <= held || mayIssue)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
else
|
||||
{
|
||||
return tesSUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
return terNO_DELEGATE_PERMISSION;
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAccount.h> // IWYU pragma: keep
|
||||
@@ -73,14 +74,23 @@ Batch::calculateBaseFeeImpl(ReadView const& view, STTx const& tx)
|
||||
for (auto const& stx : tx.getBatchTransactions())
|
||||
{
|
||||
auto const fee = xrpl::calculateBaseFee(view, *stx);
|
||||
// LCOV_EXCL_START
|
||||
if (txnFees > maxAmount - fee)
|
||||
if (!fee)
|
||||
{
|
||||
JLOG(debugLog().error())
|
||||
<< "BatchTrace: base fee of inner transaction " << stx->getTransactionID()
|
||||
<< " could not be computed: " << transToken(fee.error());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// LCOV_EXCL_START
|
||||
if (txnFees > maxAmount - *fee)
|
||||
{
|
||||
UNREACHABLE("XRPAmount overflow in txnFees calculation");
|
||||
JLOG(debugLog().error()) << "BatchTrace: XRPAmount overflow in txnFees calculation.";
|
||||
return std::nullopt;
|
||||
}
|
||||
// LCOV_EXCL_STOP
|
||||
txnFees += fee;
|
||||
txnFees += *fee;
|
||||
}
|
||||
|
||||
// Calculate the Signers/BatchSigners Fees
|
||||
@@ -185,6 +195,7 @@ Batch::getFlagsMask(PreflightContext const& ctx)
|
||||
* - The batch must contain at least two and no more than the maximum allowed
|
||||
* inner transactions.
|
||||
* - Each inner transaction must:
|
||||
* - Be wrapped in a RawTransaction object (with fixBatchV1_2).
|
||||
* - Be unique within the batch.
|
||||
* - Not itself be a Batch transaction.
|
||||
* - Have the tfInnerBatchTxn flag set.
|
||||
@@ -278,6 +289,14 @@ Batch::preflight(PreflightContext const& ctx)
|
||||
for (auto const& stxPtr : ctx.tx.getBatchTransactions())
|
||||
{
|
||||
STTx const& stx = *stxPtr;
|
||||
|
||||
if (ctx.rules.enabled(fixBatchV1_2) && stx.getFName() != sfRawTransaction)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:"
|
||||
<< "txns array may contain only RawTransaction objects.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
auto const hash = stx.getTransactionID();
|
||||
if (!uniqueHashes.emplace(hash).second)
|
||||
{
|
||||
|
||||
@@ -123,12 +123,6 @@ Change::preclaim(PreclaimContext const& ctx)
|
||||
ctx.tx.isFieldPresent(sfReserveIncrementDrops))
|
||||
return temDISABLED;
|
||||
}
|
||||
// The ttFEE transaction format defines these fields as optional,
|
||||
// but they are unconditionally forbidden until FeeVoteImpl is
|
||||
// updated to populate them (SmartEscrow behavioral port).
|
||||
if (ctx.tx.isFieldPresent(sfGasLimit) || ctx.tx.isFieldPresent(sfBytecodeSizeLimit) ||
|
||||
ctx.tx.isFieldPresent(sfGasPrice))
|
||||
return temDISABLED;
|
||||
return tesSUCCESS;
|
||||
case ttAMENDMENT:
|
||||
case ttUNL_MODIFY:
|
||||
|
||||
@@ -119,15 +119,7 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
|
||||
if (hasHolder && (hasIssuerElGamalKey || hasAuditorElGamalKey))
|
||||
return temMALFORMED;
|
||||
|
||||
// Pre-ConfidentialMPTKeyRotation amendment, the auditor key could not be
|
||||
// registered independently of the issuer key. The issuer could either:
|
||||
// - Register only the issuer key (in which case an auditor key could not be added later), or
|
||||
// - Register both the issuer and auditor keys simultaneously.
|
||||
//
|
||||
// Post-ConfidentialMPTKeyRotation amendment, the auditor key can be
|
||||
// registered after the issuer key has already been registered.
|
||||
if (hasAuditorElGamalKey && !hasIssuerElGamalKey &&
|
||||
!ctx.rules.enabled(featureConfidentialMPTKeyRotation))
|
||||
if (hasAuditorElGamalKey && !hasIssuerElGamalKey)
|
||||
return temMALFORMED;
|
||||
|
||||
if (hasIssuerElGamalKey && !isValidCompressedECPoint(ctx.tx[sfIssuerEncryptionKey]))
|
||||
@@ -227,57 +219,18 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// Updating an existing encryption key requires the
|
||||
// ConfidentialMPTKeyRotation amendment.
|
||||
bool const canRotateKey = ctx.view.rules().enabled(featureConfidentialMPTKeyRotation);
|
||||
|
||||
bool const txHasIssuerKey = ctx.tx.isFieldPresent(sfIssuerEncryptionKey);
|
||||
bool const txHasAuditorKey = ctx.tx.isFieldPresent(sfAuditorEncryptionKey);
|
||||
bool const sleHasIssuerKey = sleMptIssuance->isFieldPresent(sfIssuerEncryptionKey);
|
||||
bool const sleHasAuditorKey = sleMptIssuance->isFieldPresent(sfAuditorEncryptionKey);
|
||||
|
||||
if (canRotateKey)
|
||||
// cannot update issuer public key
|
||||
if (ctx.tx.isFieldPresent(sfIssuerEncryptionKey) &&
|
||||
sleMptIssuance->isFieldPresent(sfIssuerEncryptionKey))
|
||||
{
|
||||
// Post-ConfidentialMPTKeyRotation amendment, the encryption keys can be updated.
|
||||
// A first-time auditor key registration requires an issuer key,
|
||||
// either already on the issuance or set by the same transaction.
|
||||
bool const registersAuditorKey = txHasAuditorKey && !sleHasAuditorKey;
|
||||
bool const issuerKeyExists = sleHasIssuerKey || txHasIssuerKey;
|
||||
if (registersAuditorKey && !issuerKeyExists)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// Rotating a key to its current value is not permitted: a key epoch
|
||||
// increment must always correspond to an actual key change.
|
||||
if (txHasIssuerKey && sleHasIssuerKey &&
|
||||
ctx.tx[sfIssuerEncryptionKey] == (*sleMptIssuance)[sfIssuerEncryptionKey])
|
||||
return tecDUPLICATE;
|
||||
|
||||
if (txHasAuditorKey && sleHasAuditorKey &&
|
||||
ctx.tx[sfAuditorEncryptionKey] == (*sleMptIssuance)[sfAuditorEncryptionKey])
|
||||
return tecDUPLICATE;
|
||||
|
||||
// Key epochs must never wrap. Epoch 0 serves as the sentinel for "never
|
||||
// rotated." Holders' mirror epochs are checked against it for equality,
|
||||
// so a wrap would cause stale mirror ciphertexts to appear valid instead
|
||||
// of failing loudly.
|
||||
if (txHasIssuerKey && sleHasIssuerKey &&
|
||||
(*sleMptIssuance)[~sfIssuerKeyEpoch].value_or(0) == kMaxKeyEpoch)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
if (txHasAuditorKey && sleHasAuditorKey &&
|
||||
(*sleMptIssuance)[~sfAuditorKeyEpoch].value_or(0) == kMaxKeyEpoch)
|
||||
return tecNO_PERMISSION;
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Pre-ConfidentialMPTKeyRotation amendment, the encryption keys can not be updated.
|
||||
// cannot update issuer public key
|
||||
if (txHasIssuerKey && sleHasIssuerKey)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// cannot update auditor public key
|
||||
if (txHasAuditorKey && sleHasAuditorKey)
|
||||
return tecNO_PERMISSION; // LCOV_EXCL_LINE
|
||||
// cannot update auditor public key
|
||||
if (ctx.tx.isFieldPresent(sfAuditorEncryptionKey) &&
|
||||
sleMptIssuance->isFieldPresent(sfAuditorEncryptionKey))
|
||||
{
|
||||
return tecNO_PERMISSION; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
auto const enablesConfidentialBalance =
|
||||
@@ -288,30 +241,25 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
// Encryption keys can only be set if confidential amounts are already
|
||||
// enabled on the issuance OR if the transaction is enabling it
|
||||
if (txHasIssuerKey && !sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) &&
|
||||
!enablesConfidentialBalance)
|
||||
if (ctx.tx.isFieldPresent(sfIssuerEncryptionKey) &&
|
||||
!sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) && !enablesConfidentialBalance)
|
||||
{
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
if (txHasAuditorKey && !sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) &&
|
||||
!enablesConfidentialBalance)
|
||||
if (ctx.tx.isFieldPresent(sfAuditorEncryptionKey) &&
|
||||
!sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) && !enablesConfidentialBalance)
|
||||
{
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
bool const hasConfidentialOA =
|
||||
(*sleMptIssuance)[~sfConfidentialOutstandingAmount].value_or(0) > 0;
|
||||
|
||||
// Pre-ConfidentialMPTKeyRotation amendment, keys cannot be uploaded while
|
||||
// COA > 0. Post-amendment they can be uploaded even if COA > 0.
|
||||
if (!canRotateKey && (txHasIssuerKey || txHasAuditorKey) && hasConfidentialOA)
|
||||
// cannot upload key if there's circulating supply of COA
|
||||
if ((ctx.tx.isFieldPresent(sfIssuerEncryptionKey) ||
|
||||
ctx.tx.isFieldPresent(sfAuditorEncryptionKey) || enablesConfidentialBalance) &&
|
||||
(*sleMptIssuance)[~sfConfidentialOutstandingAmount].value_or(0) > 0)
|
||||
{
|
||||
return tecNO_PERMISSION; // LCOV_EXCL_LINE
|
||||
|
||||
// Enabling confidential balances when COA > 0 is not permitted, regardless of
|
||||
// ConfidentialMPTKeyRotation.
|
||||
if (enablesConfidentialBalance && hasConfidentialOA)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
@@ -429,69 +377,25 @@ MPTokenIssuanceSet::doApply()
|
||||
}
|
||||
}
|
||||
|
||||
// Sets an encryption key on the issuance. Overwriting an existing key
|
||||
// (a rotation) increments the corresponding key epoch; a first-time
|
||||
// registration leaves the epoch absent (epoch 0), matching issuances
|
||||
// whose keys were registered before the ConfidentialMPTKeyRotation
|
||||
// amendment.
|
||||
bool const canRotateKey = view().rules().enabled(featureConfidentialMPTKeyRotation);
|
||||
auto const setEncryptionKey = [&](SF_VL const& keyField, SF_UINT32 const& epochField) -> TER {
|
||||
auto const pubKey = ctx_.tx[~keyField];
|
||||
if (!pubKey)
|
||||
return tesSUCCESS;
|
||||
|
||||
// This is enforced in preflight, which rejects a transaction carrying
|
||||
// both sfHolder and an encryption key.
|
||||
if (auto const pubKey = ctx_.tx[~sfIssuerEncryptionKey])
|
||||
{
|
||||
// This is enforced in preflight.
|
||||
XRPL_ASSERT(
|
||||
sle->getType() == ltMPTOKEN_ISSUANCE,
|
||||
"MPTokenIssuanceSet::doApply : modifying MPTokenIssuance");
|
||||
|
||||
// Add sanity check under the amendment ConfidentialMPTKeyRotation.
|
||||
// Pre-confidentialMPTKeyRotation did not return tecINTERNAL so
|
||||
// this should be under the amendment guard.
|
||||
if (canRotateKey && sle->getType() != ltMPTOKEN_ISSUANCE)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
sle->setFieldVL(sfIssuerEncryptionKey, *pubKey);
|
||||
}
|
||||
|
||||
// NOTE: presence must be checked before the key is overwritten below.
|
||||
bool const isRotation = sle->isFieldPresent(keyField);
|
||||
sle->setFieldVL(keyField, *pubKey);
|
||||
if (auto const pubKey = ctx_.tx[~sfAuditorEncryptionKey])
|
||||
{
|
||||
// This is enforced in preflight.
|
||||
XRPL_ASSERT(
|
||||
sle->getType() == ltMPTOKEN_ISSUANCE,
|
||||
"MPTokenIssuanceSet::doApply : modifying MPTokenIssuance");
|
||||
|
||||
if (isRotation)
|
||||
{
|
||||
// Preclaim rejects overwriting an existing key unless the amendment is
|
||||
// enabled.
|
||||
if (!canRotateKey)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::MPTokenIssuanceSet::doApply : rotation without amendment");
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
auto const epoch = (*sle)[~epochField].valueOr(0);
|
||||
|
||||
// Preclaim rejects a rotation that would wrap the epoch. So this should never happen.
|
||||
if (epoch >= kMaxKeyEpoch)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::MPTokenIssuanceSet::doApply : key epoch overflow");
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
(*sle)[epochField] = epoch + 1;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
};
|
||||
|
||||
if (auto const ter = setEncryptionKey(sfIssuerEncryptionKey, sfIssuerKeyEpoch);
|
||||
!isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
|
||||
if (auto const ter = setEncryptionKey(sfAuditorEncryptionKey, sfAuditorKeyEpoch);
|
||||
!isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
sle->setFieldVL(sfAuditorEncryptionKey, *pubKey);
|
||||
}
|
||||
|
||||
view().update(sle);
|
||||
|
||||
|
||||
@@ -5896,6 +5896,81 @@ class Batch_test : public beast::unit_test::Suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWrappedInnerSubmission(FeatureBitset features)
|
||||
{
|
||||
testcase("wrapper field submission");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
// Object fields with no InnerObjectFormats template, used in place of
|
||||
// sfRawTransaction as the wrapper of each inner transaction.
|
||||
for (SField const* wrapper :
|
||||
{&sfRawTransaction,
|
||||
&sfCreatedNode,
|
||||
&sfModifiedNode,
|
||||
&sfDeletedNode,
|
||||
&sfTemplateEntry,
|
||||
&sfEmitDetails,
|
||||
&sfMemo,
|
||||
&sfFinalFields,
|
||||
&sfNewFields,
|
||||
&sfPreviousFields,
|
||||
&sfTransactionMetaData})
|
||||
{
|
||||
bool const poisoned = (wrapper != &sfRawTransaction);
|
||||
|
||||
auto const alice = Account("alice");
|
||||
auto const bob = Account("bob");
|
||||
|
||||
auto wrap = [&](std::uint32_t s) {
|
||||
json::Value inner = pay(alice, bob, XRP(1));
|
||||
inner[jss::SigningPubKey] = "";
|
||||
inner[jss::Sequence] = s;
|
||||
inner[jss::Fee] = "0";
|
||||
inner[jss::Flags] = tfInnerBatchTxn;
|
||||
|
||||
json::Value wrapped;
|
||||
wrapped[wrapper->jsonName] = inner;
|
||||
return wrapped;
|
||||
};
|
||||
|
||||
auto submit = [&](Env& env, TER expected) {
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
auto const preBob = env.balance(bob);
|
||||
auto const batchFee = batch::calcBatchFee(env, 0, 2);
|
||||
auto const seq = env.seq(alice);
|
||||
|
||||
auto jv = batch::outer(alice, seq, batchFee, tfAllOrNothing);
|
||||
jv[jss::RawTransactions][0u] = wrap(seq + 1);
|
||||
jv[jss::RawTransactions][1u] = wrap(seq + 2);
|
||||
|
||||
env(jv, Ter(expected));
|
||||
env.close();
|
||||
|
||||
return env.balance(bob) == preBob + XRP(2);
|
||||
};
|
||||
|
||||
// Without fixBatchV1_2 every wrapper executes.
|
||||
{
|
||||
Env env{*this, features - fixBatchV1_2};
|
||||
BEAST_EXPECTS(
|
||||
submit(env, tesSUCCESS),
|
||||
wrapper->getName() + " did not execute without fixBatchV1_2");
|
||||
}
|
||||
|
||||
// With fixBatchV1_2 only sfRawTransaction is accepted.
|
||||
{
|
||||
Env env{*this, features | fixBatchV1_2};
|
||||
bool const delivered = submit(env, poisoned ? TER{temMALFORMED} : TER{tesSUCCESS});
|
||||
BEAST_EXPECTS(
|
||||
delivered == !poisoned, wrapper->getName() + " wrong result with fixBatchV1_2");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWithFeats(FeatureBitset features)
|
||||
{
|
||||
@@ -5935,6 +6010,7 @@ class Batch_test : public beast::unit_test::Suite
|
||||
testOuterBinding(features);
|
||||
testUnsortedBatchSigners(features);
|
||||
testBatchSigCache(features);
|
||||
testWrappedInnerSubmission(features);
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
@@ -1,634 +0,0 @@
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/ConfidentialTransfer.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/mpt.h>
|
||||
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/OpenView.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class ConfidentialMPTKeyRotation_test : public ConfidentialTransferTestBase
|
||||
{
|
||||
void
|
||||
testMPTokenIssuanceSetRotateIssuerKey(FeatureBitset features)
|
||||
{
|
||||
testcase("MPTokenIssuanceSet rotate issuer key");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
|
||||
});
|
||||
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(bob);
|
||||
|
||||
// First-time registration.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
});
|
||||
|
||||
// Verify that no epochs are set when registering for the first time.
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
|
||||
|
||||
// Rotating the issuer key requires the key rotation amendment
|
||||
bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(bob),
|
||||
.err = rotationEnabled ? TER(tesSUCCESS) : TER(tecNO_PERMISSION),
|
||||
});
|
||||
|
||||
// A rotation replaces the issuer key and bumps its epoch. The auditor
|
||||
// key was never registered, so it and its epoch stay absent.
|
||||
if (rotationEnabled)
|
||||
{
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(bob, std::nullopt));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, std::nullopt));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, std::nullopt));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
|
||||
}
|
||||
|
||||
if (rotationEnabled)
|
||||
{
|
||||
// A second rotation increments the epoch again
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
});
|
||||
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(2u, std::nullopt));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testMPTokenIssuanceSetRotateBothKeys(FeatureBitset features)
|
||||
{
|
||||
testcase("MPTokenIssuanceSet rotate both issuer and auditor keys");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const auditor("auditor");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
|
||||
});
|
||||
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(bob);
|
||||
mptAlice.generateKeyPair(auditor);
|
||||
|
||||
// Register both keys together.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
.auditorPubKey = mptAlice.getPubKey(auditor),
|
||||
});
|
||||
|
||||
// Verify that no epochs are set when registering for the first time.
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
|
||||
|
||||
// Rotating both keys requires the amendment
|
||||
bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(bob),
|
||||
.auditorPubKey = mptAlice.getPubKey(alice),
|
||||
.err = rotationEnabled ? TER(tesSUCCESS) : TER(tecNO_PERMISSION),
|
||||
});
|
||||
|
||||
if (rotationEnabled)
|
||||
{
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(bob, alice));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, 1u));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
|
||||
}
|
||||
|
||||
if (rotationEnabled)
|
||||
{
|
||||
// Rotating the issuer key to its current value fails.
|
||||
// Current issuer key is bob, duplicate.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(bob),
|
||||
.err = tecDUPLICATE,
|
||||
});
|
||||
|
||||
// Rotating the auditor key to its current value fails.
|
||||
// Current auditor key is alice, duplicate.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.auditorPubKey = mptAlice.getPubKey(alice),
|
||||
.err = tecDUPLICATE,
|
||||
});
|
||||
|
||||
// The whole transaction fails when one key is unchanged, even if
|
||||
// the other key is rotated to a new value.
|
||||
// Current issuer key is bob, duplicate.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(bob),
|
||||
.auditorPubKey = mptAlice.getPubKey(auditor),
|
||||
.err = tecDUPLICATE,
|
||||
});
|
||||
|
||||
// Current auditor key is alice, duplicate.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(auditor),
|
||||
.auditorPubKey = mptAlice.getPubKey(alice),
|
||||
.err = tecDUPLICATE,
|
||||
});
|
||||
|
||||
// Nothing changed: keys and epochs are untouched
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, 1u));
|
||||
|
||||
// A second rotation increments both epochs again
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
.auditorPubKey = mptAlice.getPubKey(auditor),
|
||||
});
|
||||
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(2u, 2u));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testMPTokenIssuanceSetRotateAuditorKeyOnly(FeatureBitset features)
|
||||
{
|
||||
testcase("MPTokenIssuanceSet rotate auditor key only");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const auditor("auditor");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
|
||||
});
|
||||
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(bob);
|
||||
mptAlice.generateKeyPair(auditor);
|
||||
|
||||
// Register both keys together.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
.auditorPubKey = mptAlice.getPubKey(auditor),
|
||||
});
|
||||
|
||||
// A transaction carrying only the auditor key fails preflight
|
||||
// pre-ConfidentialMPTKeyRotation; post-ConfidentialMPTKeyRotation it rotates the auditor
|
||||
// key
|
||||
bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.auditorPubKey = mptAlice.getPubKey(bob),
|
||||
.err = rotationEnabled ? TER(tesSUCCESS) : TER(temMALFORMED),
|
||||
});
|
||||
|
||||
// The issuer key keeps unchanged, and rotating only the auditor key
|
||||
// bumps only its epoch.
|
||||
if (rotationEnabled)
|
||||
{
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, bob));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, 1u));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
|
||||
}
|
||||
|
||||
if (rotationEnabled)
|
||||
{
|
||||
// A second rotation increments the epoch again
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.auditorPubKey = mptAlice.getPubKey(auditor),
|
||||
});
|
||||
|
||||
// The issuer key epoch is still untouched.
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, 2u));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testMPTokenIssuanceSetRegisterAuditorKeyLater(FeatureBitset features)
|
||||
{
|
||||
testcase("MPTokenIssuanceSet register auditor key after issuer key");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const auditor("auditor");
|
||||
MPTTester mptAlice(env, alice);
|
||||
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
|
||||
});
|
||||
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(auditor);
|
||||
|
||||
// Register the issuer key first. We'll register the auditor key in a separate transaction.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
});
|
||||
|
||||
// Register the auditor key separately.
|
||||
// pre-ConfidentialMPTKeyRotation it fails preflight; post-ConfidentialMPTKeyRotation it
|
||||
// succeeds without touching any epoch because it's a first-time registration.
|
||||
bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.auditorPubKey = mptAlice.getPubKey(auditor),
|
||||
.err = rotationEnabled ? TER(tesSUCCESS) : TER(temMALFORMED),
|
||||
});
|
||||
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(
|
||||
alice, rotationEnabled ? std::optional<Account>(auditor) : std::nullopt));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
|
||||
}
|
||||
|
||||
void
|
||||
testMPTokenIssuanceSetRegisterAuditorKeyLaterWithCOA(FeatureBitset features)
|
||||
{
|
||||
testcase("MPTokenIssuanceSet register auditor key later with circulating supply");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const auditor("auditor");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
|
||||
});
|
||||
|
||||
mptAlice.authorize({.account = bob});
|
||||
mptAlice.pay(alice, bob, 100);
|
||||
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(bob);
|
||||
mptAlice.generateKeyPair(auditor);
|
||||
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
});
|
||||
|
||||
// Convert some of bob's balance so that COA > 0
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = 50,
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
});
|
||||
|
||||
auto const sleIssuanceBefore = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
|
||||
if (!BEAST_EXPECT(sleIssuanceBefore))
|
||||
return;
|
||||
auto const coaBefore = (*sleIssuanceBefore)[~sfConfidentialOutstandingAmount].value_or(0);
|
||||
BEAST_EXPECT(coaBefore > 0);
|
||||
|
||||
// Registering the auditor key for the first time while confidential
|
||||
// supply is circulating: pre-ConfidentialMPTKeyRotation an auditor-only
|
||||
// transaction fails preflight; post-ConfidentialMPTKeyRotation it
|
||||
// succeeds as a first-time late-registration even COA > 0.
|
||||
bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.auditorPubKey = mptAlice.getPubKey(auditor),
|
||||
.err = rotationEnabled ? TER(tesSUCCESS) : TER(temMALFORMED),
|
||||
});
|
||||
|
||||
auto const sleIssuance = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
|
||||
if (!BEAST_EXPECT(sleIssuance))
|
||||
return;
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(
|
||||
alice, rotationEnabled ? std::optional<Account>(auditor) : std::nullopt));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
|
||||
|
||||
// The circulating supply itself is not affected.
|
||||
BEAST_EXPECT((*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0) == coaBefore);
|
||||
}
|
||||
|
||||
void
|
||||
testMPTokenIssuanceSetAuditorKeyWithoutIssuerKey(FeatureBitset features)
|
||||
{
|
||||
testcase("MPTokenIssuanceSet auditor key requires issuer key");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const auditor("auditor");
|
||||
MPTTester mptAlice(env, alice);
|
||||
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
|
||||
});
|
||||
|
||||
mptAlice.generateKeyPair(auditor);
|
||||
// The issuer key was never registered. pre-ConfidentialMPTKeyRotation an auditor-only
|
||||
// transaction fails preflight; post-ConfidentialMPTKeyRotation it passes preflight
|
||||
// but preclaim rejects registering an auditor key on an issuance
|
||||
// without an issuer key.
|
||||
bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.auditorPubKey = mptAlice.getPubKey(auditor),
|
||||
.err = rotationEnabled ? TER(tecNO_PERMISSION) : TER(temMALFORMED),
|
||||
});
|
||||
|
||||
// The rejected transaction leaves the issuance without either key.
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(std::nullopt, std::nullopt));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
|
||||
}
|
||||
|
||||
void
|
||||
testMPTokenIssuanceSetRotateWithCOA(FeatureBitset features)
|
||||
{
|
||||
testcase("MPTokenIssuanceSet rotate with circulating confidential supply");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const carol("carol");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
|
||||
});
|
||||
|
||||
mptAlice.authorize({.account = bob});
|
||||
mptAlice.pay(alice, bob, 100);
|
||||
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(bob);
|
||||
mptAlice.generateKeyPair(carol);
|
||||
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
});
|
||||
|
||||
// Convert some of bob's balance to confidential spending, so that the
|
||||
// issuance has confidential supply. COA > 0.
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = 50,
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
});
|
||||
|
||||
auto const sleIssuanceBeforeRotation =
|
||||
env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
|
||||
if (!BEAST_EXPECT(sleIssuanceBeforeRotation))
|
||||
return;
|
||||
auto const coaBeforeRotation =
|
||||
(*sleIssuanceBeforeRotation)[~sfConfidentialOutstandingAmount].value_or(0);
|
||||
BEAST_EXPECT(coaBeforeRotation > 0);
|
||||
|
||||
// Rotating key requires the
|
||||
// amendment.
|
||||
bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(carol),
|
||||
.err = rotationEnabled ? TER(tesSUCCESS) : TER(tecNO_PERMISSION),
|
||||
});
|
||||
|
||||
auto const sleIssuance = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
|
||||
if (!BEAST_EXPECT(sleIssuance))
|
||||
return;
|
||||
if (rotationEnabled)
|
||||
{
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(carol, std::nullopt));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, std::nullopt));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, std::nullopt));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
|
||||
}
|
||||
|
||||
// The confidential outstanding amount is not affected by the rotation
|
||||
BEAST_EXPECT(
|
||||
(*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0) == coaBeforeRotation);
|
||||
|
||||
// Re-enabling confidential balances while supply is circulating is
|
||||
// rejected regardless of the ConfidentialMPTKeyRotation amendment.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.flags = tfMPTSetCanHoldConfidentialBalance,
|
||||
.err = tecNO_PERMISSION,
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
testMPTokenIssuanceSetKeyEpochAtMax(FeatureBitset features)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
if (!features[featureConfidentialMPTKeyRotation])
|
||||
return;
|
||||
|
||||
testcase("MPTokenIssuanceSet key epoch cannot wrap");
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const carol("carol");
|
||||
Account const auditor("auditor");
|
||||
|
||||
// Keep the ledger open so that we can write the key epochs directly into it.
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}, .close = false});
|
||||
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
|
||||
});
|
||||
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(bob);
|
||||
mptAlice.generateKeyPair(carol);
|
||||
mptAlice.generateKeyPair(auditor);
|
||||
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
.auditorPubKey = mptAlice.getPubKey(auditor),
|
||||
});
|
||||
|
||||
auto const issuanceKeylet = keylet::mptokenIssuance(mptAlice.issuanceID());
|
||||
|
||||
// Writes the supplied key epochs straight into the open ledger so that
|
||||
// the maximum epoch is reachable without submitting four billion
|
||||
// rotations.
|
||||
auto setEpochs = [&](std::optional<std::uint32_t> const& issuerKeyEpoch,
|
||||
std::optional<std::uint32_t> const& auditorKeyEpoch) {
|
||||
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) {
|
||||
auto const sle = view.read(issuanceKeylet);
|
||||
if (!sle)
|
||||
return false; // LCOV_EXCL_LINE
|
||||
|
||||
auto replacement = std::make_shared<SLE>(*sle);
|
||||
if (issuerKeyEpoch)
|
||||
(*replacement)[sfIssuerKeyEpoch] = *issuerKeyEpoch;
|
||||
if (auditorKeyEpoch)
|
||||
(*replacement)[sfAuditorKeyEpoch] = *auditorKeyEpoch;
|
||||
view.rawReplace(replacement);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
|
||||
|
||||
// Increment the auditor epoch to kMaxKeyEpoch - 1, leaving the issuer epoch absent.
|
||||
setEpochs(std::nullopt, kMaxKeyEpoch - 1);
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, kMaxKeyEpoch - 1));
|
||||
|
||||
// Rotating the auditor key to kMaxKeyEpoch succeeds.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.auditorPubKey = mptAlice.getPubKey(carol),
|
||||
});
|
||||
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, carol));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, kMaxKeyEpoch));
|
||||
|
||||
// A further auditor rotation is rejected because the epoch is exhausted.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.auditorPubKey = mptAlice.getPubKey(bob),
|
||||
.err = tecNO_PERMISSION,
|
||||
});
|
||||
|
||||
// Rotating both keys at once is rejected as a whole because the auditor
|
||||
// epoch is exhausted.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(auditor),
|
||||
.auditorPubKey = mptAlice.getPubKey(bob),
|
||||
.err = tecNO_PERMISSION,
|
||||
});
|
||||
|
||||
// Both rejections leave every key and epoch as it was.
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, carol));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, kMaxKeyEpoch));
|
||||
|
||||
// The issuer key is unaffected by the exhausted auditor epoch.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(bob),
|
||||
});
|
||||
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(bob, carol));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, kMaxKeyEpoch));
|
||||
|
||||
// Increment the issuer epoch to kMaxKeyEpoch - 1.
|
||||
setEpochs(kMaxKeyEpoch - 1, std::nullopt);
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(bob, carol));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(kMaxKeyEpoch - 1, kMaxKeyEpoch));
|
||||
|
||||
// Rotating the issuer key to kMaxKeyEpoch succeeds.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(auditor),
|
||||
});
|
||||
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(auditor, carol));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(kMaxKeyEpoch, kMaxKeyEpoch));
|
||||
|
||||
// With both epochs exhausted neither key can be rotated again.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
.err = tecNO_PERMISSION,
|
||||
});
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.auditorPubKey = mptAlice.getPubKey(bob),
|
||||
.err = tecNO_PERMISSION,
|
||||
});
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
.auditorPubKey = mptAlice.getPubKey(bob),
|
||||
.err = tecNO_PERMISSION,
|
||||
});
|
||||
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(auditor, carol));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(kMaxKeyEpoch, kMaxKeyEpoch));
|
||||
}
|
||||
|
||||
void
|
||||
testMPTokenIssuanceSetWithFeats(FeatureBitset features)
|
||||
{
|
||||
testMPTokenIssuanceSetRotateIssuerKey(features);
|
||||
testMPTokenIssuanceSetRotateBothKeys(features);
|
||||
testMPTokenIssuanceSetRotateAuditorKeyOnly(features);
|
||||
testMPTokenIssuanceSetRegisterAuditorKeyLater(features);
|
||||
testMPTokenIssuanceSetRegisterAuditorKeyLaterWithCOA(features);
|
||||
testMPTokenIssuanceSetAuditorKeyWithoutIssuerKey(features);
|
||||
testMPTokenIssuanceSetRotateWithCOA(features);
|
||||
testMPTokenIssuanceSetKeyEpochAtMax(features);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace test::jtx;
|
||||
FeatureBitset const all{testableAmendments()};
|
||||
|
||||
testMPTokenIssuanceSetWithFeats(all);
|
||||
testMPTokenIssuanceSetWithFeats(all - featureConfidentialMPTKeyRotation);
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(ConfidentialMPTKeyRotation, app, xrpl);
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -736,8 +736,12 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
|
||||
.err = temMALFORMED,
|
||||
});
|
||||
|
||||
// Note: "auditor key without issuer key" (temMALFORMED before
|
||||
// ConfidentialMPTKeyRotation) is covered in ConfidentialMPTKeyRotation_test
|
||||
// Cannot set auditor key without issuer key
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.auditorPubKey = mptAlice.getPubKey(alice),
|
||||
.err = temMALFORMED,
|
||||
});
|
||||
|
||||
// Cannot set Holder and issuer Keys in the same transaction
|
||||
mptAlice.set({
|
||||
@@ -783,9 +787,9 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
|
||||
});
|
||||
}
|
||||
|
||||
// Cannot update issuer public key once set (pre-ConfidentialMPTKeyRotation behavior)
|
||||
// Cannot update issuer public key once set
|
||||
{
|
||||
Env env{*this, features - featureConfidentialMPTKeyRotation};
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
@@ -815,9 +819,8 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
|
||||
// Cannot update issuer and auditor public keys once set
|
||||
// Note: trying to set only auditor key fails in preflight (temMALFORMED)
|
||||
// so we must provide both keys, which fails on issuer key check first
|
||||
// (pre-ConfidentialMPTKeyRotation behavior)
|
||||
{
|
||||
Env env{*this, features - featureConfidentialMPTKeyRotation};
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const auditor("auditor");
|
||||
@@ -897,9 +900,8 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
|
||||
}
|
||||
|
||||
// Set issuer key first, then auditor key in a separate tx
|
||||
// (pre-ConfidentialMPTKeyRotation behavior)
|
||||
{
|
||||
Env env{*this, features - featureConfidentialMPTKeyRotation};
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const auditor("auditor");
|
||||
MPTTester mptAlice(env, alice, {.holders = {}, .auditor = auditor});
|
||||
|
||||
@@ -1144,6 +1144,88 @@ class Delegate_test : public beast::unit_test::Suite
|
||||
env.require(Balance(gw, aliceUSD(-20)));
|
||||
}
|
||||
|
||||
// PaymentBurn must not exceed the balance the account holds. Redeeming past
|
||||
// zero makes the payment engine issue the account's own IOUs, which is a mint.
|
||||
{
|
||||
Env env(*this, features);
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
Account const gw{"gateway"};
|
||||
auto const gwUSD = gw["USD"];
|
||||
auto const aliceUSD = alice["USD"];
|
||||
|
||||
env.fund(XRP(10000), alice, bob, gw);
|
||||
env.trust(gwUSD(200), alice);
|
||||
env.close();
|
||||
|
||||
env(pay(gw, alice, gwUSD(50)));
|
||||
env.close();
|
||||
env.require(Balance(alice, gwUSD(50)));
|
||||
|
||||
// gw accepts alice-issued USD, so the engine has issuing liquidity
|
||||
// available once the trustline reaches zero.
|
||||
env(trust(gw, aliceUSD(200)));
|
||||
env.close();
|
||||
|
||||
env(delegate::set(alice, bob, {"PaymentBurn"}));
|
||||
env.close();
|
||||
|
||||
if (!features[fixCleanup3_4_0])
|
||||
{
|
||||
// Pre-fixCleanup3_4_0: the balance direction alone authorizes the payment, so it
|
||||
// redeems alice's 50 and then mints 50 alice-issued USD.
|
||||
env(pay(alice, gw, gwUSD(100)), delegate::As(bob));
|
||||
env.require(Balance(alice, gwUSD(-50)));
|
||||
env.require(Balance(gw, aliceUSD(50)));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Post-fixCleanup3_4_0: Rejected because it exceeds what alice holds.
|
||||
env(pay(alice, gw, gwUSD(100)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
|
||||
env.require(Balance(alice, gwUSD(50)));
|
||||
env.require(Balance(gw, aliceUSD(-50)));
|
||||
|
||||
// Allowed because it is less than what alice holds.
|
||||
env(pay(alice, gw, gwUSD(20)), delegate::As(bob));
|
||||
env.require(Balance(alice, gwUSD(30)));
|
||||
env.close();
|
||||
|
||||
// Exactly what alice holds: allowed, and settles at zero.
|
||||
env(pay(alice, gw, gwUSD(30)), delegate::As(bob));
|
||||
env.require(Balance(alice, gwUSD(0)));
|
||||
env.close();
|
||||
|
||||
// Nothing left to burn: rejected.
|
||||
env(pay(alice, gw, gwUSD(1)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
|
||||
env.require(Balance(gw, aliceUSD(0)));
|
||||
}
|
||||
}
|
||||
|
||||
// A delegate holding both PaymentMint and PaymentBurn may cross zero.
|
||||
{
|
||||
Env env(*this, features);
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
Account const gw{"gateway"};
|
||||
auto const gwUSD = gw["USD"];
|
||||
auto const aliceUSD = alice["USD"];
|
||||
|
||||
env.fund(XRP(10000), alice, bob, gw);
|
||||
env.trust(gwUSD(200), alice);
|
||||
env.close();
|
||||
|
||||
env(pay(gw, alice, gwUSD(50)));
|
||||
env(trust(gw, aliceUSD(200)));
|
||||
env.close();
|
||||
|
||||
env(delegate::set(alice, bob, {"PaymentBurn", "PaymentMint"}));
|
||||
env.close();
|
||||
|
||||
env(pay(alice, gw, gwUSD(100)), delegate::As(bob));
|
||||
env.require(Balance(alice, gwUSD(-50)));
|
||||
env.require(Balance(gw, aliceUSD(50)));
|
||||
}
|
||||
|
||||
// Test invalid fields or flags not allowed in granular permission template
|
||||
{
|
||||
Env env(*this, features);
|
||||
@@ -2916,6 +2998,7 @@ class Delegate_test : public beast::unit_test::Suite
|
||||
testAccountDelete();
|
||||
testDelegateTransaction();
|
||||
testPaymentGranular(all);
|
||||
testPaymentGranular(all - fixCleanup3_4_0);
|
||||
testTrustSetGranular();
|
||||
testAccountSetGranular();
|
||||
testMPTokenIssuanceSetGranular();
|
||||
|
||||
+218
-1
@@ -25,6 +25,7 @@
|
||||
#include <xrpld/rpc/detail/Tuning.h>
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/hardened_hash.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/core/Job.h>
|
||||
#include <xrpl/core/JobQueue.h>
|
||||
@@ -46,16 +47,20 @@
|
||||
#include <xrpl/resource/Consumer.h>
|
||||
#include <xrpl/resource/Fees.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
@@ -1943,7 +1948,7 @@ public:
|
||||
static constexpr AccountID kAccountID7{kAccount7};
|
||||
static constexpr AccountID kAccountID8{kAccount8};
|
||||
|
||||
auto ps = STPathSet{};
|
||||
auto ps = STPathSet{STPathSet::DeduplicationTag{}};
|
||||
|
||||
auto createPathElements = [](auto const& account1, auto const& account2) {
|
||||
auto base = STPath{};
|
||||
@@ -2017,6 +2022,215 @@ public:
|
||||
BEAST_EXPECT(ps.size() == 6);
|
||||
}
|
||||
|
||||
void
|
||||
testPushBackDeduplication()
|
||||
{
|
||||
testcase("STPathSet::pushBack/emplaceBack deduplication");
|
||||
|
||||
// pushBack and emplaceBack reject duplicates on a set built with the
|
||||
// DeduplicationTag, and append unconditionally without it. Both
|
||||
// report which happened. The unconditional case is the one the wire
|
||||
// and JSON paths rely on: collapsing duplicates there would change the
|
||||
// signed content of a transaction.
|
||||
|
||||
static constexpr AccountID kAccountID1{"A3F19C7B2E5D08146FB93A7C0E2D5184BC6F3A09"};
|
||||
static constexpr AccountID kAccountID2{"1D7E4B90C2A6F3851E0B9D47A2C5F8136E0A4B7D"};
|
||||
static constexpr AccountID kAccountID3{"F08C36A1D95E27B40CA1F63E8D204B7950E1C3A6"};
|
||||
|
||||
auto makePath = [](AccountID const& account) {
|
||||
auto p = STPath{};
|
||||
p.pushBack(STPathElement{STPathElement::TypeAccount, account, xrpCurrency(), account});
|
||||
return p;
|
||||
};
|
||||
|
||||
auto const first = makePath(kAccountID1);
|
||||
auto const second = makePath(kAccountID2);
|
||||
auto const third = makePath(kAccountID3);
|
||||
|
||||
// Deduplicating set: the second insert of a path is rejected, and the
|
||||
// rejection is reported rather than silently swallowed.
|
||||
{
|
||||
auto ps = STPathSet{STPathSet::DeduplicationTag{}};
|
||||
|
||||
BEAST_EXPECT(ps.pushBack(first));
|
||||
BEAST_EXPECT(ps.size() == 1);
|
||||
|
||||
BEAST_EXPECT(!ps.pushBack(first));
|
||||
BEAST_EXPECT(ps.size() == 1);
|
||||
|
||||
// emplaceBack sees paths registered by pushBack...
|
||||
BEAST_EXPECT(!ps.emplaceBack(first));
|
||||
BEAST_EXPECT(ps.size() == 1);
|
||||
|
||||
BEAST_EXPECT(ps.emplaceBack(second));
|
||||
BEAST_EXPECT(ps.size() == 2);
|
||||
|
||||
// ...and pushBack sees paths registered by emplaceBack.
|
||||
BEAST_EXPECT(!ps.pushBack(second));
|
||||
BEAST_EXPECT(ps.size() == 2);
|
||||
|
||||
// emplaceBack's forwarding form registers the same way.
|
||||
BEAST_EXPECT(ps.emplaceBack(std::vector<STPathElement>{third.front()}));
|
||||
BEAST_EXPECT(ps.size() == 3);
|
||||
BEAST_EXPECT(!ps.pushBack(third));
|
||||
BEAST_EXPECT(ps.size() == 3);
|
||||
|
||||
// A rejected duplicate must not disturb what is already stored.
|
||||
BEAST_EXPECT(ps[0] == first);
|
||||
BEAST_EXPECT(ps[1] == second);
|
||||
BEAST_EXPECT(ps[2] == third);
|
||||
}
|
||||
|
||||
// Without the tag there is no index, so duplicates are appended and
|
||||
// both methods report success every time.
|
||||
{
|
||||
auto plain = STPathSet{};
|
||||
BEAST_EXPECT(plain.pushBack(first));
|
||||
BEAST_EXPECT(plain.pushBack(first));
|
||||
BEAST_EXPECT(plain.emplaceBack(first));
|
||||
BEAST_EXPECT(plain.size() == 3);
|
||||
|
||||
auto named = STPathSet{sfPaths};
|
||||
BEAST_EXPECT(named.pushBack(first));
|
||||
BEAST_EXPECT(named.pushBack(first));
|
||||
BEAST_EXPECT(named.size() == 2);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testPathHashInjectivity()
|
||||
{
|
||||
testcase("STPathElement hash injectivity");
|
||||
|
||||
auto const zeroCurrency =
|
||||
STPathElement{AccountID{}, PathAsset{Currency{}}, AccountID{}, true};
|
||||
auto const zeroMPT = STPathElement{AccountID{}, PathAsset{MPTID{}}, AccountID{}, true};
|
||||
|
||||
BEAST_EXPECT(!(zeroCurrency == zeroMPT));
|
||||
|
||||
auto path = [](std::vector<STPathElement> const& elements) {
|
||||
auto p = STPath{};
|
||||
for (auto const& element : elements)
|
||||
p.pushBack(element);
|
||||
return p;
|
||||
};
|
||||
|
||||
auto const currencyFirst = path({zeroCurrency, zeroMPT});
|
||||
auto const mptFirst = path({zeroMPT, zeroCurrency});
|
||||
|
||||
BEAST_EXPECT(!(currencyFirst == mptFirst));
|
||||
|
||||
auto const hasher = HardenedHash<>{};
|
||||
BEAST_EXPECT(hasher(currencyFirst) != hasher(mptFirst));
|
||||
|
||||
auto mask = std::vector<int>{0, 0, 1, 1};
|
||||
auto hashes = std::set<std::size_t>{};
|
||||
auto orderings = 0uz;
|
||||
do
|
||||
{
|
||||
auto elements = std::vector<STPathElement>{};
|
||||
for (auto const isMPT : mask)
|
||||
{
|
||||
elements.push_back(isMPT != 0 ? zeroMPT : zeroCurrency);
|
||||
}
|
||||
hashes.insert(hasher(path(elements)));
|
||||
++orderings;
|
||||
} while (std::ranges::next_permutation(mask).found);
|
||||
|
||||
BEAST_EXPECT(orderings == 6);
|
||||
BEAST_EXPECT(hashes.size() == orderings);
|
||||
|
||||
auto seen = hardened_hash_set<STPath>{};
|
||||
for (auto const& p : {currencyFirst, mptFirst})
|
||||
{
|
||||
seen.emplace(p);
|
||||
}
|
||||
BEAST_EXPECT(seen.size() == 2);
|
||||
|
||||
// The other half of the invariant: equal elements must hash equally.
|
||||
// STPathElement::operator== masks type_ down to the TypeAccount bit, so
|
||||
// elements whose remaining type bits differ still compare equal --
|
||||
// hashing the full type_ would give them distinct hashes and silently
|
||||
// defeat deduplication.
|
||||
static constexpr AccountID kAccount{"A3F19C7B2E5D08146FB93A7C0E2D5184BC6F3A09"};
|
||||
static constexpr AccountID kIssuer{"1D7E4B90C2A6F3851E0B9D47A2C5F8136E0A4B7D"};
|
||||
|
||||
auto const equivalent = std::vector<std::pair<STPathElement, STPathElement>>{
|
||||
// forceAsset toggles TypeCurrency on an XRP asset.
|
||||
{STPathElement{kAccount, PathAsset{xrpCurrency()}, kIssuer, true},
|
||||
STPathElement{kAccount, PathAsset{xrpCurrency()}, kIssuer, false}},
|
||||
// An explicit type mask vs. one derived from the populated fields.
|
||||
{STPathElement{STPathElement::TypeAccount, kAccount, xrpCurrency(), kIssuer},
|
||||
STPathElement{kAccount, PathAsset{xrpCurrency()}, kIssuer, false}},
|
||||
};
|
||||
|
||||
for (auto const& [lhs, rhs] : equivalent)
|
||||
{
|
||||
BEAST_EXPECT(lhs.getNodeType() != rhs.getNodeType());
|
||||
BEAST_EXPECT(lhs == rhs);
|
||||
|
||||
auto const lhsPath = path({lhs});
|
||||
auto const rhsPath = path({rhs});
|
||||
BEAST_EXPECT(hasher(lhsPath) == hasher(rhsPath));
|
||||
|
||||
auto equal = hardened_hash_set<STPath>{};
|
||||
equal.emplace(lhsPath);
|
||||
equal.emplace(rhsPath);
|
||||
BEAST_EXPECT(equal.size() == 1);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testDeserializationPreservesDuplicates()
|
||||
{
|
||||
testcase("STPathSet deserialization preserves duplicate paths");
|
||||
|
||||
// The `Paths` field of a signed transaction must round-trip byte for
|
||||
// byte. The deduplication index exists solely for pathfinding, so the
|
||||
// deserializing constructor must never engage it: collapsing duplicates
|
||||
// on parse would silently change the signed content of a transaction.
|
||||
|
||||
static constexpr AccountID kAccountID1{"A3F19C7B2E5D08146FB93A7C0E2D5184BC6F3A09"};
|
||||
static constexpr AccountID kAccountID2{"1D7E4B90C2A6F3851E0B9D47A2C5F8136E0A4B7D"};
|
||||
|
||||
auto const element =
|
||||
STPathElement{kAccountID1, PathAsset{xrpCurrency()}, kAccountID2, true};
|
||||
|
||||
auto path = STPath{};
|
||||
path.pushBack(element);
|
||||
|
||||
static constexpr auto kDuplicates = 64uz;
|
||||
|
||||
auto original = STPathSet{sfPaths};
|
||||
for (auto i = 0uz; i < kDuplicates; ++i)
|
||||
{
|
||||
original.pushBack(path);
|
||||
}
|
||||
|
||||
// No index was requested, so nothing is deduplicated on the way in.
|
||||
BEAST_EXPECT(original.size() == kDuplicates);
|
||||
|
||||
auto s = Serializer{};
|
||||
original.add(s);
|
||||
|
||||
auto sit = SerialIter{s.slice()};
|
||||
auto const parsed = STPathSet{sit, sfPaths};
|
||||
|
||||
// The duplicates survive the round trip...
|
||||
BEAST_EXPECT(parsed.size() == kDuplicates);
|
||||
BEAST_EXPECT(parsed.isEquivalent(original));
|
||||
|
||||
// ...and re-serializing reproduces the original bytes exactly.
|
||||
auto serialized = Serializer{};
|
||||
parsed.add(serialized);
|
||||
BEAST_EXPECT(serialized.getData() == s.getData());
|
||||
|
||||
// A parsed set holds no index, so appending to it stays append-only.
|
||||
auto appended = parsed;
|
||||
appended.pushBack(path);
|
||||
BEAST_EXPECT(appended.size() == kDuplicates + 1);
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
@@ -2031,6 +2245,9 @@ public:
|
||||
issuesPathNegativeRippleClientIssue23Larger();
|
||||
qualityPathsQualitySetAndTest();
|
||||
testAssembleAddDeduplication();
|
||||
testPushBackDeduplication();
|
||||
testPathHashInjectivity();
|
||||
testDeserializationPreservesDuplicates();
|
||||
trustAutoClearTrustNormalClear();
|
||||
trustAutoClearTrustAutoClear();
|
||||
norippleCombinations();
|
||||
|
||||
@@ -967,6 +967,75 @@ class InvariantsMPT_test : public InvariantsBase
|
||||
});
|
||||
}
|
||||
|
||||
// LoanSet / VaultWithdraw MayAuthorizeMpt caps (fixCleanup3_4_0):
|
||||
// LoanSet allows at most two creates and no deletes; VaultWithdraw
|
||||
// allows at most one of each. Fabricate one extra mutation so a
|
||||
// too-loose cap would miss these.
|
||||
{
|
||||
auto const insertHolderTokens =
|
||||
[](Account const& issuer, Account const& holder, ApplyContext& ac, int n) {
|
||||
auto const sle = ac.view().peek(keylet::account(issuer.id()));
|
||||
if (!sle)
|
||||
return false;
|
||||
auto seq = sle->getFieldU32(sfSequence);
|
||||
for (int i = 0; i < n; ++i)
|
||||
{
|
||||
MPTIssue const mpt{makeMptID(seq + i, issuer)};
|
||||
auto sleNew =
|
||||
std::make_shared<SLE>(keylet::mptoken(mpt.getMptID(), holder));
|
||||
(*sleNew)[sfAccount] = holder.id();
|
||||
(*sleNew)[sfMPTokenIssuanceID] = mpt.getMptID();
|
||||
ac.view().insert(sleNew);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
std::array<std::pair<xrpl::TxType, std::uint8_t>, 2> const createOverCap{
|
||||
{{ttLOAN_SET, 3}, {ttVAULT_WITHDRAW, 2}}};
|
||||
for (auto const& [txnType, nTokens] : createOverCap)
|
||||
{
|
||||
doInvariantCheck(
|
||||
{{"MPT authorize succeeded but created/deleted bad number mptokens"}},
|
||||
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
|
||||
return insertHolderTokens(a1, a2, ac, nTokens);
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{txnType, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED});
|
||||
}
|
||||
|
||||
MPTID id;
|
||||
auto const precloseTwoHolders = [&id](Account const& a1, Account const& a2, Env& env) {
|
||||
Account const gw("gw");
|
||||
env.fund(XRP(1'000), gw);
|
||||
MPTTester const mpt({.env = env, .issuer = gw, .holders = {a1, a2}});
|
||||
id = mpt.issuanceID();
|
||||
return true;
|
||||
};
|
||||
std::array<std::pair<xrpl::TxType, std::uint8_t>, 2> const deleteOverCap{
|
||||
{{ttLOAN_SET, 1}, {ttVAULT_WITHDRAW, 2}}};
|
||||
for (auto const& [txnType, nTokens] : deleteOverCap)
|
||||
{
|
||||
doInvariantCheck(
|
||||
{{"MPT authorize succeeded but created/deleted bad number mptokens"}},
|
||||
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
|
||||
std::array const holders{a1, a2};
|
||||
for (int i = 0; i < nTokens; ++i)
|
||||
{
|
||||
auto sle = ac.view().peek(keylet::mptoken(id, holders[i]));
|
||||
if (!sle)
|
||||
return false;
|
||||
ac.view().erase(sle);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{txnType, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseTwoHolders);
|
||||
}
|
||||
}
|
||||
|
||||
// sfReferenceHolding can only be set on creation by VaultCreate. A
|
||||
// non-VaultCreate transaction that creates an MPTokenIssuance with
|
||||
// sfReferenceHolding present must trip the invariant.
|
||||
|
||||
@@ -1224,33 +1224,51 @@ class InvariantsVault_test : public InvariantsBase
|
||||
|
||||
// ttLOAN_PAY success post-conditions. A loan left with payments still
|
||||
// remaining after a successful payment must show that payment in its
|
||||
// balance and schedule: PrincipalOutstanding and PaymentRemaining both
|
||||
// strictly decrease, and NextPaymentDueDate advances by a positive
|
||||
// multiple of PaymentInterval. Each case seeds the same loan, then applies
|
||||
// balance and schedule: neither PrincipalOutstanding nor
|
||||
// TotalValueOutstanding may increase, at least one of them must
|
||||
// strictly decrease, PaymentRemaining must strictly decrease, and
|
||||
// NextPaymentDueDate must advance by a positive multiple of
|
||||
// PaymentInterval. Each failing case seeds the same loan, then applies
|
||||
// an after-image that breaks exactly one of those conditions.
|
||||
{
|
||||
struct Case
|
||||
{
|
||||
Number principal;
|
||||
Number totalValue;
|
||||
std::uint32_t remaining;
|
||||
std::uint32_t dueDate;
|
||||
std::string expected;
|
||||
};
|
||||
auto const cases = std::to_array<Case>({
|
||||
{.principal = Number(100),
|
||||
.totalValue = Number(150),
|
||||
.remaining = 1,
|
||||
.dueDate = 110,
|
||||
.expected = "loan pay must strictly decrease PrincipalOutstanding"},
|
||||
.expected = "loan pay must decrease PrincipalOutstanding or "
|
||||
"TotalValueOutstanding"},
|
||||
{.principal = Number(110),
|
||||
.totalValue = Number(150),
|
||||
.remaining = 1,
|
||||
.dueDate = 110,
|
||||
.expected = "loan pay must not increase PrincipalOutstanding"},
|
||||
{.principal = Number(50),
|
||||
.totalValue = Number(160),
|
||||
.remaining = 1,
|
||||
.dueDate = 110,
|
||||
.expected = "loan pay must not increase TotalValueOutstanding"},
|
||||
{.principal = Number(50),
|
||||
.totalValue = Number(150),
|
||||
.remaining = 2,
|
||||
.dueDate = 110,
|
||||
.expected = "loan pay must decrease PaymentRemaining"},
|
||||
{.principal = Number(50),
|
||||
.totalValue = Number(150),
|
||||
.remaining = 1,
|
||||
.dueDate = 100,
|
||||
.expected = "loan pay must advance NextPaymentDueDate"},
|
||||
// Advanced, but not by a whole number of payment intervals.
|
||||
{.principal = Number(50),
|
||||
.totalValue = Number(150),
|
||||
.remaining = 1,
|
||||
.dueDate = 105,
|
||||
.expected = "loan pay must advance NextPaymentDueDate"},
|
||||
@@ -1291,6 +1309,7 @@ class InvariantsVault_test : public InvariantsBase
|
||||
if (!BEAST_EXPECT(sleLoan))
|
||||
continue;
|
||||
sleLoan->at(sfPrincipalOutstanding) = c.principal;
|
||||
sleLoan->at(sfTotalValueOutstanding) = c.totalValue;
|
||||
sleLoan->setFieldU32(sfPaymentRemaining, c.remaining);
|
||||
sleLoan->setFieldU32(sfNextPaymentDueDate, c.dueDate);
|
||||
ac.view().update(sleLoan);
|
||||
@@ -1303,6 +1322,65 @@ class InvariantsVault_test : public InvariantsBase
|
||||
BEAST_EXPECT(result == tecINVARIANT_FAILED);
|
||||
BEAST_EXPECT(sink.messages().str().contains(c.expected));
|
||||
}
|
||||
|
||||
// Principal may stick while TotalValueOutstanding falls. This
|
||||
// after-image is only a Loan mutation, so other (vault) invariants
|
||||
// still fail under Full scope; ValidLoan itself must not.
|
||||
{
|
||||
Env env{*this, all_};
|
||||
Account const a1{"A1"};
|
||||
Account const a2{"A2"};
|
||||
env.fund(XRP(1000), a1, a2);
|
||||
auto const keys = createClosedXrpBroker(a1, env);
|
||||
if (!keys)
|
||||
{
|
||||
fail();
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const& brokerKeylet = keys->second;
|
||||
OpenView ov{*env.current()};
|
||||
auto const loanKeylet =
|
||||
keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
|
||||
{
|
||||
auto sleLoan = makeLoanSle(brokerKeylet.key, 1, a2.id());
|
||||
sleLoan->at(sfPrincipalOutstanding) = Number(100);
|
||||
sleLoan->at(sfTotalValueOutstanding) = Number(150);
|
||||
sleLoan->at(sfPaymentInterval) = 10u;
|
||||
sleLoan->setFieldU32(sfPaymentRemaining, 2);
|
||||
sleLoan->setFieldU32(sfNextPaymentDueDate, 100);
|
||||
ov.rawInsert(sleLoan);
|
||||
}
|
||||
|
||||
STTx const tx{
|
||||
ttLOAN_PAY, [](STObject& t) { t.setFieldAmount(sfAmount, XRPAmount(50)); }};
|
||||
test::StreamSink sink{beast::Severity::Warning};
|
||||
beast::Journal const jlog{sink};
|
||||
ApplyContext ac{
|
||||
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
|
||||
CurrentTransactionRulesGuard const rulesGuard(ov.rules());
|
||||
|
||||
auto sleLoan = ac.view().peek(loanKeylet);
|
||||
if (BEAST_EXPECT(sleLoan))
|
||||
{
|
||||
sleLoan->at(sfPrincipalOutstanding) = Number(100);
|
||||
sleLoan->at(sfTotalValueOutstanding) = Number(140);
|
||||
sleLoan->setFieldU32(sfPaymentRemaining, 1);
|
||||
sleLoan->setFieldU32(sfNextPaymentDueDate, 110);
|
||||
ac.view().update(sleLoan);
|
||||
|
||||
auto transactor = makeTransactor(ac);
|
||||
if (BEAST_EXPECT(transactor))
|
||||
{
|
||||
std::ignore = transactor->checkInvariants(
|
||||
tesSUCCESS, XRPAmount{}, Transactor::InvariantScope::Full);
|
||||
auto const logs = sink.messages().str();
|
||||
BEAST_EXPECT(!logs.contains("Invariant failed: Loan"));
|
||||
BEAST_EXPECT(!logs.contains("loan pay"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ttLOAN_MANAGE (default): the write-off is rounded downward at the
|
||||
|
||||
@@ -415,6 +415,9 @@ private:
|
||||
// The test pays one period at a time across three LoanPay
|
||||
// transactions and verifies the loan completes (paymentRemaining=0)
|
||||
// with totals matching the loan's economics (1 principal + 2 interest).
|
||||
// Also run under featureLendingProtocolV1_1: ValidLoan must allow the
|
||||
// two sticking pays (TVO falls, PO does not) and the final clear
|
||||
// (PaymentRemaining 0, NextPaymentDueDate 0).
|
||||
void
|
||||
testIntegerScalePrincipalSticks(FeatureBitset features)
|
||||
{
|
||||
@@ -446,28 +449,19 @@ private:
|
||||
env(pay(issuer, borrower, asset(10'000)));
|
||||
env.close();
|
||||
|
||||
Vault const vault{env};
|
||||
auto [vaultTx, vaultKeylet] = vault.create({.owner = lender, .asset = asset});
|
||||
env(vaultTx);
|
||||
env.close();
|
||||
// createVaultAndBroker promotes the vault to ClosedEnded under
|
||||
// featureLendingProtocolV1_1 (LoanBrokerSet rejects open-ended).
|
||||
BrokerParameters const params{
|
||||
.vaultDeposit = Number{5'000},
|
||||
.debtMax = Number{100},
|
||||
.coverRateMin = TenthBips32{0},
|
||||
.coverDeposit = 0,
|
||||
.managementFeeRate = TenthBips16{0},
|
||||
.coverRateLiquidation = TenthBips32{0}};
|
||||
BrokerInfo const broker = createVaultAndBroker(env, asset, lender, params);
|
||||
|
||||
env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = asset(5'000)}));
|
||||
env.close();
|
||||
|
||||
auto const brokerKeylet =
|
||||
keylet::loanBroker(lender.id(), SeqProxy::rawSequence(env.seq(lender)));
|
||||
env(loan_broker::set(lender, vaultKeylet.key),
|
||||
loan_broker::kDebtMaximum(Number{100}),
|
||||
Fee(env.current()->fees().base * 2));
|
||||
env.close();
|
||||
|
||||
auto const brokerStateBefore = env.le(brokerKeylet);
|
||||
if (!BEAST_EXPECT(brokerStateBefore))
|
||||
return;
|
||||
auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
|
||||
auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(loanSequence));
|
||||
|
||||
env(loan::set(borrower, brokerKeylet.key, Number{1}),
|
||||
auto const loanKeylet = nextLoanKeylet(env, broker);
|
||||
env(loan::set(borrower, broker.brokerID, Number{1}),
|
||||
Sig(sfCounterpartySignature, lender),
|
||||
loan::kInterestRate(TenthBips32{50'000}),
|
||||
loan::kPaymentTotal(3),
|
||||
@@ -499,6 +493,8 @@ private:
|
||||
BEAST_EXPECT(sle->at(sfPrincipalOutstanding) == expectedPO[i]);
|
||||
BEAST_EXPECT(sle->at(sfTotalValueOutstanding) == expectedTVO[i]);
|
||||
BEAST_EXPECT(sle->at(sfPaymentRemaining) == expectedRemaining[i]);
|
||||
if (expectedRemaining[i] == 0)
|
||||
BEAST_EXPECT(sle->at(~sfNextPaymentDueDate).value_or(0) == 0);
|
||||
}
|
||||
|
||||
// Borrower paid 3 total regardless of fee split (1 principal + 2
|
||||
@@ -1226,6 +1222,9 @@ private:
|
||||
testBugVaultWithdrawDustVsAssetsTotal(all_ - fixCleanup3_4_0);
|
||||
testBugVaultWithdrawDustVsAssetsTotal(all_);
|
||||
testBugInterestDueDeltaCrash();
|
||||
// all_ excludes V1.1; amendmentCombinations never pairs it with the
|
||||
// sticking schedule. Run that combination explicitly.
|
||||
testIntegerScalePrincipalSticks(all_ | featureLendingProtocolV1_1);
|
||||
}
|
||||
|
||||
// Tests run under each entry in amendmentCombinations().
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/Units.h>
|
||||
@@ -597,6 +598,105 @@ private:
|
||||
nullptr);
|
||||
}
|
||||
|
||||
void
|
||||
testLoanSetOriginationFeeTwoMptCreates(FeatureBitset features)
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace loan;
|
||||
|
||||
bool const fix340Enabled = features[fixCleanup3_4_0];
|
||||
testcase << "LoanSet: borrower and broker owner missing MPToken"
|
||||
<< (fix340Enabled ? "" : " pre-fixCleanup3_4_0");
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
Account const lender{"lender"};
|
||||
Account const borrower{"borrower"};
|
||||
|
||||
Env env(*this, features);
|
||||
env.fund(XRP(1'000'000), issuer, lender, borrower);
|
||||
env.close();
|
||||
|
||||
MPTTester mptt{env, issuer, kMptInitNoFund};
|
||||
mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
|
||||
env.close();
|
||||
PrettyAsset const asset = mptt.issuanceID();
|
||||
mptt.authorize({.account = lender});
|
||||
mptt.authorize({.account = borrower});
|
||||
env.close();
|
||||
|
||||
env(pay(issuer, lender, asset(10'000'000)));
|
||||
env.close();
|
||||
|
||||
auto const broker = createVaultAndBroker(env, asset, lender);
|
||||
|
||||
// Delete borrower's asset MPToken.
|
||||
mptt.authorize({.account = borrower, .flags = tfMPTUnauthorize});
|
||||
env.close();
|
||||
|
||||
// Pay out and delete the broker owner's asset MPToken.
|
||||
auto const lenderMPToken = keylet::mptoken(mptt.issuanceID(), lender);
|
||||
auto const sleLenderMPT = env.le(lenderMPToken);
|
||||
if (!BEAST_EXPECT(sleLenderMPT))
|
||||
return;
|
||||
env(pay(lender, issuer, asset(sleLenderMPT->at(sfMPTAmount))));
|
||||
env.close();
|
||||
mptt.authorize({.account = lender, .flags = tfMPTUnauthorize});
|
||||
env.close();
|
||||
|
||||
auto const borrowerMPToken = keylet::mptoken(mptt.issuanceID(), borrower);
|
||||
auto const brokerKeylet = keylet::loanBroker(broker.brokerID);
|
||||
auto const sleBrokerBefore = env.le(brokerKeylet);
|
||||
if (!BEAST_EXPECT(sleBrokerBefore))
|
||||
return;
|
||||
auto const loanSequence = sleBrokerBefore->at(sfLoanSequence);
|
||||
auto const debtTotalBefore = sleBrokerBefore->at(sfDebtTotal);
|
||||
auto const loanKeylet = keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence));
|
||||
|
||||
auto const sleVaultBefore = env.le(keylet::vault(broker.vaultID));
|
||||
if (!BEAST_EXPECT(sleVaultBefore))
|
||||
return;
|
||||
auto const assetsAvailableBefore = sleVaultBefore->at(sfAssetsAvailable);
|
||||
|
||||
env(set(borrower, broker.brokerID, asset(1'000).value()),
|
||||
kLoanOriginationFee(asset(1).value()),
|
||||
kCounterparty(lender),
|
||||
Sig(sfCounterpartySignature, lender),
|
||||
Fee(env.current()->fees().base * 5),
|
||||
Ter{fix340Enabled ? TER{tesSUCCESS} : TER{tecINVARIANT_FAILED}});
|
||||
env.close();
|
||||
|
||||
auto const sleBorrowerAfter = env.le(borrowerMPToken);
|
||||
auto const sleLenderAfter = env.le(lenderMPToken);
|
||||
auto const sleLoanAfter = env.le(loanKeylet);
|
||||
auto const sleBrokerAfter = env.le(brokerKeylet);
|
||||
auto const sleVaultAfter = env.le(keylet::vault(broker.vaultID));
|
||||
if (!BEAST_EXPECT(sleVaultAfter))
|
||||
return;
|
||||
if (fix340Enabled)
|
||||
{
|
||||
if (!BEAST_EXPECT(sleBorrowerAfter && sleLenderAfter && sleLoanAfter && sleBrokerAfter))
|
||||
return;
|
||||
BEAST_EXPECT(sleBorrowerAfter->at(sfMPTAmount) == 999);
|
||||
BEAST_EXPECT(sleLenderAfter->at(sfMPTAmount) == 1);
|
||||
BEAST_EXPECT(sleLoanAfter->at(sfPrincipalOutstanding) == Number{1'000});
|
||||
BEAST_EXPECT(sleBrokerAfter->at(sfLoanSequence) == loanSequence + 1);
|
||||
BEAST_EXPECT(
|
||||
sleVaultAfter->at(sfAssetsAvailable) == assetsAvailableBefore - Number{1'000});
|
||||
}
|
||||
else
|
||||
{
|
||||
// The whole transaction must roll back.
|
||||
BEAST_EXPECT(!sleBorrowerAfter);
|
||||
BEAST_EXPECT(!sleLenderAfter);
|
||||
BEAST_EXPECT(!sleLoanAfter);
|
||||
if (!BEAST_EXPECT(sleBrokerAfter))
|
||||
return;
|
||||
BEAST_EXPECT(sleBrokerAfter->at(sfLoanSequence) == loanSequence);
|
||||
BEAST_EXPECT(sleBrokerAfter->at(sfDebtTotal) == debtTotalBefore);
|
||||
BEAST_EXPECT(sleVaultAfter->at(sfAssetsAvailable) == assetsAvailableBefore);
|
||||
}
|
||||
}
|
||||
|
||||
// LoanSet in a closed-ended vault — phase gating and maturity bound.
|
||||
void
|
||||
testLoanSetClosedEnded()
|
||||
@@ -838,6 +938,8 @@ public:
|
||||
|
||||
testLoanSetClosedEnded();
|
||||
testLoanSetExistingLineAfterIssuerClearsDefaultRipple();
|
||||
testLoanSetOriginationFeeTwoMptCreates(all_);
|
||||
testLoanSetOriginationFeeTwoMptCreates(all_ - fixCleanup3_4_0);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1586,14 +1586,10 @@ private:
|
||||
// which for an integral MPT asset the destination check would reject if
|
||||
// it were reached.
|
||||
//
|
||||
// ValidMPTIssuance is a separate checker and still runs. It only trips on
|
||||
// the one arm that both creates and deletes an MPToken: Alice's last
|
||||
// share with the asset MPToken missing, where addEmptyHolding creates the
|
||||
// asset token while her share token is deleted (created + deleted > 1).
|
||||
// Leftover shares with the token missing is create-only, and a last share
|
||||
// with the token present is delete-only; neither exceeds one. Bob still
|
||||
// owns shares throughout, so this is never the vault's final outstanding
|
||||
// share.
|
||||
// ValidMPTIssuance: pre-fixCleanup3_4_0, a VaultWithdraw that both
|
||||
// creates and deletes an MPToken fails. Post-fixCleanup3_4_0 that is
|
||||
// allowed.
|
||||
|
||||
//
|
||||
// Post-fixCleanup3_4_0, doWithdraw skips addEmptyHolding on a zero
|
||||
// payout and zeroDeltaIsLegitimate lets the vault-delta and
|
||||
|
||||
@@ -795,6 +795,86 @@ private:
|
||||
},
|
||||
{.requireAuth = false});
|
||||
|
||||
auto const redeemAllNoAssetMpt = [this](TER expected) {
|
||||
return [this, expected](
|
||||
Env& env,
|
||||
Account const&,
|
||||
Account const& owner,
|
||||
Account const& depositor,
|
||||
Asset const& asset,
|
||||
Vault& vault,
|
||||
MPTTester& mptt) {
|
||||
testcase << "MPT non-owner redeems all shares with no asset MPToken"
|
||||
<< (isTesSuccess(expected) ? "" : " pre-fixCleanup3_4_0");
|
||||
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
tx = vault.deposit(
|
||||
{.depositor = depositor,
|
||||
.id = keylet.key,
|
||||
.amount = asset(1000)}); // all assets held by depositor
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
auto const vaultSle = env.le(keylet);
|
||||
if (!BEAST_EXPECT(vaultSle))
|
||||
return;
|
||||
auto const shareMPTID = vaultSle->at(sfShareMPTID);
|
||||
|
||||
// Depositor's asset MPToken balance is now zero; delete it.
|
||||
mptt.authorize({.account = depositor, .flags = tfMPTUnauthorize});
|
||||
env.close();
|
||||
|
||||
auto const mptoken = keylet::mptoken(mptt.issuanceID(), depositor);
|
||||
|
||||
auto const shareKeylet = keylet::mptoken(shareMPTID, depositor.id());
|
||||
auto const sleShareBefore = env.le(shareKeylet);
|
||||
if (!BEAST_EXPECT(sleShareBefore))
|
||||
return;
|
||||
auto const shareAmountBefore = sleShareBefore->at(sfMPTAmount);
|
||||
auto const assetsTotalBefore = vaultSle->at(sfAssetsTotal);
|
||||
auto const assetsAvailableBefore = vaultSle->at(sfAssetsAvailable);
|
||||
|
||||
// Redeeming ALL shares in one transaction both erases the
|
||||
// now-empty share MPToken and re-creates the asset MPToken.
|
||||
tx = vault.withdraw(
|
||||
{.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
|
||||
env(tx, Ter{expected});
|
||||
env.close();
|
||||
|
||||
auto const sleAsset = env.le(mptoken);
|
||||
auto const sleShare = env.le(shareKeylet);
|
||||
auto const vaultAfter = env.le(keylet);
|
||||
if (!BEAST_EXPECT(vaultAfter))
|
||||
return;
|
||||
if (isTesSuccess(expected))
|
||||
{
|
||||
if (!BEAST_EXPECT(sleAsset))
|
||||
return;
|
||||
BEAST_EXPECT(sleAsset->at(sfMPTAmount) == 1000);
|
||||
BEAST_EXPECT(!sleShare);
|
||||
BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == beast::kZero);
|
||||
BEAST_EXPECT(vaultAfter->at(sfAssetsAvailable) == beast::kZero);
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!sleAsset);
|
||||
if (!BEAST_EXPECT(sleShare))
|
||||
return;
|
||||
BEAST_EXPECT(sleShare->at(sfMPTAmount) == shareAmountBefore);
|
||||
BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == assetsTotalBefore);
|
||||
BEAST_EXPECT(vaultAfter->at(sfAssetsAvailable) == assetsAvailableBefore);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
testCase(redeemAllNoAssetMpt(tesSUCCESS), {.requireAuth = false});
|
||||
testCase(
|
||||
redeemAllNoAssetMpt(tecINVARIANT_FAILED),
|
||||
{.requireAuth = false, .features = testableAmendments() - fixCleanup3_4_0});
|
||||
|
||||
auto const [acctReserve, incReserve] = [this]() -> std::pair<int, int> {
|
||||
Env const env{*this, testableAmendments()};
|
||||
return {
|
||||
|
||||
@@ -648,38 +648,6 @@ MPTTester::checkImmutableFlags(std::uint32_t expectedFlags) const
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
MPTTester::checkKeyEpochs(
|
||||
std::optional<std::uint32_t> issuerKeyEpoch,
|
||||
std::optional<std::uint32_t> auditorKeyEpoch) const
|
||||
{
|
||||
return forObject([&](SLEP const& sle) -> bool {
|
||||
return (*sle)[~sfIssuerKeyEpoch] == issuerKeyEpoch &&
|
||||
(*sle)[~sfAuditorKeyEpoch] == auditorKeyEpoch;
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
MPTTester::checkEncryptionKeys(
|
||||
std::optional<Account> const& issuerKeyOwner,
|
||||
std::optional<Account> const& auditorKeyOwner) const
|
||||
{
|
||||
auto const matches =
|
||||
[this](SLEP const& sle, SF_VL const& field, std::optional<Account> const& owner) {
|
||||
if (!owner)
|
||||
return !sle->isFieldPresent(field);
|
||||
|
||||
auto const expected = getPubKey(*owner);
|
||||
return expected && sle->isFieldPresent(field) &&
|
||||
strHex((*sle)[field]) == strHex(*expected);
|
||||
};
|
||||
|
||||
return forObject([&](SLEP const& sle) -> bool {
|
||||
return matches(sle, sfIssuerEncryptionKey, issuerKeyOwner) &&
|
||||
matches(sle, sfAuditorEncryptionKey, auditorKeyOwner);
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
MPTTester::pay(
|
||||
Account const& src,
|
||||
|
||||
@@ -612,21 +612,6 @@ public:
|
||||
[[nodiscard]] bool
|
||||
checkImmutableFlags(std::uint32_t expectedFlags) const;
|
||||
|
||||
// Checks both key epochs on the issuance. Pass std::nullopt for an epoch
|
||||
// that is expected to be absent, which means the key is never rotated.
|
||||
[[nodiscard]] bool
|
||||
checkKeyEpochs(
|
||||
std::optional<std::uint32_t> issuerKeyEpoch,
|
||||
std::optional<std::uint32_t> auditorKeyEpoch) const;
|
||||
|
||||
// Checks that the issuance carries the encryption keys of the given
|
||||
// accounts. Pass std::nullopt for a key that is expected to be absent,
|
||||
// which means the key is never registered.
|
||||
[[nodiscard]] bool
|
||||
checkEncryptionKeys(
|
||||
std::optional<Account> const& issuerKeyOwner,
|
||||
std::optional<Account> const& auditorKeyOwner) const;
|
||||
|
||||
[[nodiscard]] Account const&
|
||||
issuer() const
|
||||
{
|
||||
|
||||
Loaded 100 of 126 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user