mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-22 03:27:08 +00:00
Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00e6407514 | ||
|
|
19c94c73f4 | ||
|
|
cd005ff60d | ||
|
|
578224f2e6 | ||
|
|
4a4fded2eb | ||
|
|
c8e767afa4 | ||
|
|
00eeb0a005 | ||
|
|
a18839d92d | ||
|
|
8c594c7ed9 | ||
|
|
ebd810b184 | ||
|
|
3e4e56d6bb | ||
|
|
76da5d4475 | ||
|
|
c0d0fd0d97 | ||
|
|
9aebb5ebea | ||
|
|
796f2f8f1e | ||
|
|
b190f2b14f | ||
|
|
6099940c2c | ||
|
|
0db7b766e6 | ||
|
|
ea6226b8b9 | ||
|
|
eae0a35415 | ||
|
|
2ad4def35f | ||
|
|
f7f50caa6e | ||
|
|
3e54e7d00b | ||
|
|
827b50f169 | ||
|
|
58a59c37ed | ||
|
|
986065c16f | ||
|
|
37cb4cdbe3 | ||
|
|
2ddf6ee148 | ||
|
|
8ce4f71427 | ||
|
|
6e1eb88e6e | ||
|
|
49cdc105de | ||
|
|
636d2d4851 | ||
|
|
7d7275847d |
No files matched your search
@@ -141,6 +141,7 @@ words:
|
||||
- hwrap
|
||||
- ifndef
|
||||
- inequation
|
||||
- Injectivity
|
||||
- insuf
|
||||
- insuff
|
||||
- invasively
|
||||
|
||||
@@ -40,10 +40,11 @@ runs:
|
||||
# Unlike the Linux nix images, macOS needs no SSL_CERT_FILE: it has its
|
||||
# own trust store, and pinning would break TLS to hosts relying on it.
|
||||
|
||||
# Workspace-local, so `cleanup-workspace` clears it, but not the
|
||||
# `.conan2` prepare-runner hands the system toolchain: that Conan is a
|
||||
# different version, and the two would migrate each other's cache.
|
||||
echo "CONAN_HOME=${{ github.workspace }}/.conan2-nix" >>"${GITHUB_ENV}"
|
||||
# In RUNNER_TEMP, which the runner empties per job, like the `.conan2`
|
||||
# prepare-runner hands the system toolchain - but under its own name:
|
||||
# that Conan is a different version, and the two would migrate each
|
||||
# other's cache.
|
||||
echo "CONAN_HOME=${RUNNER_TEMP}/.conan2-nix" >>"${GITHUB_ENV}"
|
||||
|
||||
# Config, profiles and remote, exactly as the dev shell sets them up on
|
||||
# entry; the `setup-conan` action is skipped for this toolchain.
|
||||
|
||||
@@ -18,7 +18,7 @@ If too broad, please consider splitting into multiple PRs.
|
||||
If there is a relevant task or issue, please link it here.
|
||||
-->
|
||||
|
||||
### Context of Change
|
||||
## Context of Change
|
||||
|
||||
<!--
|
||||
Please include the context of a change.
|
||||
@@ -29,7 +29,7 @@ If a refactor, how is this better than the previous implementation?
|
||||
If there is a spec or design document for this feature, please link it here.
|
||||
-->
|
||||
|
||||
### API Impact
|
||||
## API Impact
|
||||
|
||||
<!--
|
||||
Please check [x] relevant options, delete irrelevant ones.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -74,7 +74,20 @@
|
||||
"extra_cmake_args": "-Dvalidator_keys=ON",
|
||||
"package": {
|
||||
"type": "deb",
|
||||
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-b6a8995"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -88,7 +101,7 @@
|
||||
"extra_cmake_args": "-Dvalidator_keys=ON",
|
||||
"package": {
|
||||
"type": "rpm",
|
||||
"image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-b6a8995"
|
||||
"image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-49cdc10"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
base_image: debian:bookworm
|
||||
- name: rhel
|
||||
base_image: registry.access.redhat.com/ubi9/ubi:latest
|
||||
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@65d5a0bd72be4ecea95cff0673a6e0672ab5243a
|
||||
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@034e87065fcd0100214cf0672923bd38d193cf78
|
||||
with:
|
||||
image_name: xrpld/nix-${{ matrix.distro.name }}
|
||||
dockerfile: nix/docker/Dockerfile
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
# AlmaLinux rather than UBI, which does not ship rpm-sign.
|
||||
- name: rhel
|
||||
base_image: almalinux:10
|
||||
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@65d5a0bd72be4ecea95cff0673a6e0672ab5243a
|
||||
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@034e87065fcd0100214cf0672923bd38d193cf78
|
||||
with:
|
||||
image_name: xrpld/packaging-${{ matrix.distro.name }}
|
||||
dockerfile: package/docker/Dockerfile
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@65d5a0bd72be4ecea95cff0673a6e0672ab5243a
|
||||
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@034e87065fcd0100214cf0672923bd38d193cf78
|
||||
with:
|
||||
image_name: xrpld/pre-commit
|
||||
dockerfile: bin/pre-commit/Dockerfile
|
||||
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
|
||||
uses: XRPLF/actions/prepare-runner@b3e255d74d785d053e4903da8ac90983cd7d9e82
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -14,7 +14,7 @@ on:
|
||||
jobs:
|
||||
# Call the workflow in the XRPLF/actions repo that runs the pre-commit hooks.
|
||||
run-hooks:
|
||||
uses: XRPLF/actions/.github/workflows/pre-commit.yml@f1952595d212e86169935135efc66294b4574131
|
||||
uses: XRPLF/actions/.github/workflows/pre-commit.yml@279ec358f4a1be4088be3e024b07916fa97c75b6
|
||||
with:
|
||||
runs_on: ubuntu-latest
|
||||
container: '{ "image": "ghcr.io/xrplf/xrpld/pre-commit:sha-473fe44" }'
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
|
||||
uses: XRPLF/actions/prepare-runner@b3e255d74d785d053e4903da8ac90983cd7d9e82
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
|
||||
uses: XRPLF/actions/prepare-runner@b3e255d74d785d053e4903da8ac90983cd7d9e82
|
||||
with:
|
||||
enable_ccache: ${{ inputs.ccache_enabled }}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
|
||||
uses: XRPLF/actions/prepare-runner@b3e255d74d785d053e4903da8ac90983cd7d9e82
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -1,11 +1,16 @@
|
||||
# Build Linux packages from the pre-built xrpld and validator-keys artifacts:
|
||||
# Build, verify and publish Linux packages from the pre-built xrpld and
|
||||
# validator-keys artifacts, in three stages:
|
||||
#
|
||||
# - one job per config that carries a "package" map in linux.json
|
||||
# - that map names the container image and the format it builds there
|
||||
# - every job ends with the image's publish_pkg.py, uploading what it built
|
||||
# with 'publish: true' and doing a --dry-run otherwise
|
||||
# - '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-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'
|
||||
#
|
||||
# Only linux/amd64 is supported; the runner is hardcoded in the job below.
|
||||
# Only linux/amd64 is supported; the runner is hardcoded in the jobs below.
|
||||
name: Package
|
||||
|
||||
on:
|
||||
@@ -20,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:
|
||||
@@ -39,12 +44,15 @@ defaults:
|
||||
|
||||
env:
|
||||
BUILD_DIR: build
|
||||
PACKAGE_DIR: packages
|
||||
|
||||
jobs:
|
||||
generate-matrix:
|
||||
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
|
||||
@@ -70,14 +78,14 @@ jobs:
|
||||
contents: read
|
||||
runs-on: ["self-hosted", "Linux", "X64", "heavy"]
|
||||
container: ${{ matrix.image }}
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
|
||||
uses: XRPLF/actions/prepare-runner@b3e255d74d785d053e4903da8ac90983cd7d9e82
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
@@ -103,33 +111,121 @@ 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 and the published package are the
|
||||
# same bytes. DEBs are not signed, so the key is never set on that job.
|
||||
# Before the upload, so the artifact, the tested package and the published
|
||||
# package are the same bytes.
|
||||
- name: Sign RPM
|
||||
if: ${{ inputs.publish && matrix.package_type == 'rpm' }}
|
||||
env:
|
||||
PKG_SIGNING_KEY: ${{ secrets.signing_key }}
|
||||
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. 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/*.deb
|
||||
${{ env.BUILD_DIR }}/debbuild/*.ddeb
|
||||
${{ env.BUILD_DIR }}/rpmbuild/RPMS/**/*.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
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ matrix.xrpld_artifact_name }}-pkg-debug
|
||||
path: |
|
||||
${{ 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
|
||||
|
||||
# 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"
|
||||
]
|
||||
|
||||
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-deb, test-install-rpm]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
|
||||
name: "publish ${{ matrix.xrpld_artifact_name }}"
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ["self-hosted", "Linux", "X64", "heavy"]
|
||||
container: ${{ matrix.image }}
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@b3e255d74d785d053e4903da8ac90983cd7d9e82
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
# Both artifacts, so the debug symbols are published alongside the package.
|
||||
- name: Download package artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: ${{ matrix.xrpld_artifact_name }}-pkg*
|
||||
merge-multiple: true
|
||||
path: ${{ env.PACKAGE_DIR }}
|
||||
|
||||
- name: Determine release info
|
||||
id: release_info
|
||||
uses: ./.github/actions/release-info
|
||||
|
||||
- name: Publish package
|
||||
env:
|
||||
CHANNEL: ${{ steps.release_info.outputs.channel }}
|
||||
@@ -140,6 +236,6 @@ jobs:
|
||||
run: |
|
||||
publish_pkg.py \
|
||||
--channel "${CHANNEL}" \
|
||||
--package-dir "${BUILD_DIR}" \
|
||||
--package-dir "${PACKAGE_DIR}" \
|
||||
--nexus-url "${NEXUS_URL}" \
|
||||
${DRY_RUN_OPTION}
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
|
||||
uses: XRPLF/actions/prepare-runner@b3e255d74d785d053e4903da8ac90983cd7d9e82
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
|
||||
uses: XRPLF/actions/prepare-runner@b3e255d74d785d053e4903da8ac90983cd7d9e82
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
|
||||
+56
-14
@@ -22,14 +22,46 @@ API version 2 is available in `xrpld` version 2.0.0 and later. See [API-VERSION-
|
||||
|
||||
This version is supported by all `xrpld` versions. For WebSocket and HTTP JSON-RPC requests, it is currently the default API version used when no `api_version` is specified.
|
||||
|
||||
## Unreleased
|
||||
## XRP Ledger server version 3.4.0
|
||||
|
||||
This section contains changes targeting a future version.
|
||||
Version 3.4.0 is not yet released. These changes are available in the 3.4.0 beta releases.
|
||||
|
||||
### Additions
|
||||
### Additions in 3.4.0
|
||||
|
||||
- `ledger`: `nftoken_id`, `nftoken_ids`, and `offer_id` are now included in transaction metadata when transactions are expanded (`expand`, or admin-only `full`), matching the `tx`, `account_tx`, and `subscribe` (`transactions` stream) responses. ([#5706](https://github.com/XRPLF/rippled/pull/5706))
|
||||
|
||||
### Bugfixes in 3.4.0
|
||||
|
||||
- `sign`, `sign_for`, `submit`: `signature_target` now returns `invalidParams` unless it names `CounterpartySignature` or `SponsorSignature`. It previously accepted any inner object field, such as `Book` or `NFToken`, and signed into it.
|
||||
- `sign`, `sign_for`, `submit`, `submit_multisigned`: With `fixCleanup3_4_0` enabled, a signature in `CounterpartySignature` or `SponsorSignature` covers a different prefix than the transaction's own signature, so a signature can no longer be moved from one of those roles into another. Clients that build these signatures themselves must use the new prefixes: `CPT` and `CPM` (single- and multi-signing) for `CounterpartySignature`, and `SPN` and `SPM` for `SponsorSignature`.
|
||||
- `get_aggregate_price`: Duplicate entries in the `oracles` request array are now ignored. [#6586](https://github.com/XRPLF/rippled/pull/6586)
|
||||
- `vault_info`: Errors now identify what the request got wrong instead of reporting every failure as the unregistered token `malformedRequest`, and the `error`, `error_code` and `error_message` fields now agree with each other. An invalid `vault_id` or `seq` returns `invalidParams`, an invalid `owner` returns `actMalformed`, and a request that mixes `vault_id` with `owner`/`seq` or supplies neither returns `invalidParams` with a message naming the accepted combinations. [#8015](https://github.com/XRPLF/rippled/pull/8015)
|
||||
- `vault_info`: A well-formed all-zero `vault_id` now returns `entryNotFound` instead of being rejected as malformed, and `entryNotFound` responses now include `error_code` and `error_message`. Clients that request `ripplerpc` 3.0 or above therefore receive HTTP 400 with that error rather than HTTP 200. [#8015](https://github.com/XRPLF/rippled/pull/8015)
|
||||
- `vault_info`: `vault_id` and `owner` must now be strings, matching how `ledger_entry` reads the same fields. An object or an array in either field previously produced an internal error, and a number was silently converted to its decimal text; `vault_id` now returns `invalidParams` and `owner` returns `actMalformed`. [#8015](https://github.com/XRPLF/rippled/pull/8015)
|
||||
- `gateway_balances`: The `account` and `ident` fields now return an `invalidParams` error if the value is not a string, instead of an `internal` error. [#7655](https://github.com/XRPLF/rippled/pull/7655)
|
||||
- `account_lines`: The `peer` field now returns an error if the value is not a string. [#7728](https://github.com/XRPLF/rippled/pull/7728)
|
||||
- `ledger`: `delivered_amount` is now included in the metadata of successful `AccountDelete` transactions when transactions are expanded (`expand`, or admin-only `full`). Previously it was only added for `Payment` and `CheckCash`, which made `ledger` inconsistent with `tx` and `account_tx`. [#5706](https://github.com/XRPLF/rippled/pull/5706)
|
||||
|
||||
## XRP Ledger server version 3.3.0
|
||||
|
||||
[Version 3.3.0](https://github.com/XRPLF/rippled/releases/tag/3.3.0) was released on Aug 6, 2026.
|
||||
|
||||
### Additions in 3.3.0
|
||||
|
||||
- `account_tx`: Added an optional `delegate` request object to filter delegated transactions. The object requires `delegate_filter`, which must be either `actor` for transactions owned by the requested account but signed by another account, or `authorizer` for transactions signed by the requested account on behalf of another account. The optional `counter_party` account narrows the results to a specific signer/delegate for `actor` or a specific owner/delegator for `authorizer`. Malformed `delegate`, `delegate_filter`, and `counter_party` values return standard invalid field errors, and invalid account IDs return `actMalformed`. When paginating delegate-filtered queries, a marker from a delegate-filtered query includes a `delegate` flag and is only valid for follow-up requests that also supply `delegate` (mixing marker conventions returns `invalidParams`). Because filtering is applied after the ledger scan, a page may contain fewer results than `limit` (possibly zero) while still returning a marker, so callers must continue until no marker is present. ([#6126](https://github.com/XRPLF/rippled/pull/6126))
|
||||
|
||||
## XRP Ledger server version 3.2.1
|
||||
|
||||
[Version 3.2.1](https://github.com/XRPLF/rippled/releases/tag/3.2.1) was released on Aug 1, 2026.
|
||||
|
||||
This release contains bug fixes only and no API changes.
|
||||
|
||||
## XRP Ledger server version 3.2.0
|
||||
|
||||
[Version 3.2.0](https://github.com/XRPLF/rippled/releases/tag/3.2.0) was released on Jun 16, 2026.
|
||||
|
||||
### Additions in 3.2.0
|
||||
|
||||
- `account_tx`: Added an optional `delegate` request object to filter delegated transactions. The object requires `delegate_filter`, which must be either `actor` for transactions owned by the requested account but signed by another account, or `authorizer` for transactions signed by the requested account on behalf of another account. The optional `counter_party` account narrows the results to a specific signer/delegate for `actor` or a specific owner/delegator for `authorizer`. Malformed `delegate`, `delegate_filter`, and `counter_party` values return standard invalid field errors, and invalid account IDs return `actMalformed`.
|
||||
When paginating delegate-filtered queries, a marker from a delegate-filtered query includes a `delegate` flag and is only valid for follow-up requests that also supply `delegate` (mixing marker conventions returns `invalidParams`). Because filtering is applied after the ledger scan, a page may contain fewer results than `limit` (possibly zero) while still returning a marker, so callers must continue until no marker is present.
|
||||
- `ledger_entry`, `account_objects`: The `Delegate` ledger entry now includes an optional `DestinationNode` field, which stores the index into the authorized account's owner directory. This field is present on entries created after bidirectional directory tracking was introduced and may appear in RPC responses for those entries. ([#6681](https://github.com/XRPLF/rippled/pull/6681))
|
||||
- `server_definitions`: Added the following new sections to the response ([#6321](https://github.com/XRPLF/rippled/pull/6321)):
|
||||
- `TRANSACTION_FORMATS`: Describes the fields and their optionality for each transaction type, including common fields shared across all transactions.
|
||||
@@ -37,11 +69,9 @@ This section contains changes targeting a future version.
|
||||
- `TRANSACTION_FLAGS`: Maps transaction type names to their supported flags and flag values.
|
||||
- `LEDGER_ENTRY_FLAGS`: Maps ledger entry type names to their flags and flag values.
|
||||
- `ACCOUNT_SET_FLAGS`: Maps AccountSet flag names (asf flags) to their numeric values.
|
||||
- `ledger`: `nftoken_id`, `nftoken_ids`, and `offer_id` are now included in transaction metadata when transactions are expanded (`expand`, or admin-only `full`), matching the `tx`, `account_tx`, and `subscribe` (`transactions` stream) responses. ([#5706](https://github.com/XRPLF/rippled/pull/5706))
|
||||
|
||||
### Bugfixes
|
||||
### Bugfixes in 3.2.0
|
||||
|
||||
- `get_aggregate_price`: Duplicate entries in the `oracles` request array are now ignored. [#6586](https://github.com/XRPLF/rippled/pull/6586)
|
||||
- Peer Crawler: The `port` field in `overlay.active[]` now consistently returns an integer instead of a string for outbound peers. [#6318](https://github.com/XRPLF/rippled/pull/6318)
|
||||
- `ping`: The `ip` field is no longer returned as an empty string for proxied connections without a forwarded-for header. It is now omitted, consistent with the behavior for identified connections. [#6730](https://github.com/XRPLF/rippled/pull/6730)
|
||||
- gRPC `GetLedgerDiff`: Fixed error message that incorrectly said "base ledger not validated" when the desired ledger was not validated. [#6730](https://github.com/XRPLF/rippled/pull/6730)
|
||||
@@ -53,12 +83,24 @@ This section contains changes targeting a future version.
|
||||
- `submit`: The `fail_hard` field now returns an error if the value is not a boolean. [#6529](https://github.com/XRPLF/rippled/pull/6529)
|
||||
- `subscribe`: The `taker` field in the `books` array now returns `actMalformed` instead of `badIssuer` if the value is not a valid account. [#6529](https://github.com/XRPLF/rippled/pull/6529)
|
||||
- Fixed a bug in `Forwarded` HTTP header parsing where the extracted IP address could be incorrect when no comma or semicolon delimiter follows the address. This could cause the server to misidentify a client's IP address when operating behind a reverse proxy. [#6529](https://github.com/XRPLF/rippled/pull/6529)
|
||||
- `vault_info`: Errors now identify what the request got wrong instead of reporting every failure as the unregistered token `malformedRequest`, and the `error`, `error_code` and `error_message` fields now agree with each other. An invalid `vault_id` or `seq` returns `invalidParams`, an invalid `owner` returns `actMalformed`, and a request that mixes `vault_id` with `owner`/`seq` or supplies neither returns `invalidParams` with a message naming the accepted combinations. [#8015](https://github.com/XRPLF/rippled/pull/8015)
|
||||
- `vault_info`: A well-formed all-zero `vault_id` now returns `entryNotFound` instead of being rejected as malformed, and `entryNotFound` responses now include `error_code` and `error_message`. Clients that request `ripplerpc` 3.0 or above therefore receive HTTP 400 with that error rather than HTTP 200. [#8015](https://github.com/XRPLF/rippled/pull/8015)
|
||||
- `vault_info`: `vault_id` and `owner` must now be strings, matching how `ledger_entry` reads the same fields. An object or an array in either field previously produced an internal error, and a number was silently converted to its decimal text; `vault_id` now returns `invalidParams` and `owner` returns `actMalformed`. [#8015](https://github.com/XRPLF/rippled/pull/8015)
|
||||
- `gateway_balances`: The `account` and `ident` fields now return an `invalidParams` error if the value is not a string, instead of an `internal` error. [#7655](https://github.com/XRPLF/rippled/pull/7655)
|
||||
- `account_lines`: The `peer` field now returns an error if the value is not a string. [#7728](https://github.com/XRPLF/rippled/pull/7728)
|
||||
- `ledger`: `delivered_amount` is now included in the metadata of successful `AccountDelete` transactions when transactions are expanded (`expand`, or admin-only `full`). Previously it was only added for `Payment` and `CheckCash`, which made `ledger` inconsistent with `tx` and `account_tx`. [#5706](https://github.com/XRPLF/rippled/pull/5706)
|
||||
|
||||
## XRP Ledger server version 3.1.3
|
||||
|
||||
[Version 3.1.3](https://github.com/XRPLF/rippled/releases/tag/3.1.3) was released on May 8, 2026.
|
||||
|
||||
This release contains bug fixes only and no API changes.
|
||||
|
||||
## XRP Ledger server version 3.1.2
|
||||
|
||||
[Version 3.1.2](https://github.com/XRPLF/rippled/releases/tag/3.1.2) was released on Mar 12, 2026.
|
||||
|
||||
This release contains bug fixes only and no API changes.
|
||||
|
||||
## XRP Ledger server version 3.1.1
|
||||
|
||||
[Version 3.1.1](https://github.com/XRPLF/rippled/releases/tag/3.1.1) was released on Feb 23, 2026.
|
||||
|
||||
This release contains bug fixes only and no API changes.
|
||||
|
||||
## XRP Ledger server version 3.1.0
|
||||
|
||||
|
||||
@@ -25,7 +25,9 @@ esac
|
||||
# Packaging runs in a vanilla distro image, so the tooling comes from the distro's
|
||||
# archive rather than from nixpkgs:
|
||||
#
|
||||
# - debhelper and dpkg-dev build the DEB
|
||||
# - debhelper and dpkg-dev build the DEB, and lintian checks it
|
||||
# - binutils gives debian/rules the readelf its glibc-floor check runs; it
|
||||
# already arrives via dpkg-dev, but that tool is called directly
|
||||
# - rpm-build builds the RPM, with systemd-rpm-macros and redhat-rpm-config
|
||||
# supplying the systemd and find-debuginfo macros the spec uses
|
||||
# - rpm-sign and gnupg2 sign the built RPM
|
||||
@@ -37,11 +39,13 @@ function install() {
|
||||
debian | ubuntu)
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends \
|
||||
binutils \
|
||||
ca-certificates \
|
||||
debhelper \
|
||||
debhelper-compat \
|
||||
dpkg-dev \
|
||||
git \
|
||||
lintian \
|
||||
python3
|
||||
;;
|
||||
|
||||
|
||||
@@ -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/*",
|
||||
|
||||
+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]
|
||||
|
||||
@@ -34,7 +34,10 @@ enum class HashRouterFlags : std::uint16_t {
|
||||
PRIVATE4 = 0x0800,
|
||||
// Used in EscrowFinish.cpp
|
||||
PRIVATE5 = 0x1000,
|
||||
PRIVATE6 = 0x2000
|
||||
PRIVATE6 = 0x2000,
|
||||
// Used in apply.cpp
|
||||
PRIVATE7 = 0x4000,
|
||||
PRIVATE8 = 0x8000
|
||||
};
|
||||
|
||||
constexpr HashRouterFlags
|
||||
|
||||
@@ -38,6 +38,12 @@ enum class FreezeHandling { IgnoreFreeze, ZeroIfFrozen };
|
||||
*/
|
||||
enum class AuthHandling { IgnoreAuth, ZeroIfUnauthorized };
|
||||
|
||||
/**
|
||||
* Controls whether the recipient owner-reserve check is enforced when
|
||||
* auto-creating a trustline or MPToken during AMMWithdraw or AMMClawback.
|
||||
*/
|
||||
enum class ReserveHandling : bool { EnforceReserve, IgnoreReserve };
|
||||
|
||||
/**
|
||||
* Controls whether to include the account's full spendable balance
|
||||
*/
|
||||
|
||||
@@ -92,7 +92,7 @@ public:
|
||||
void
|
||||
importDatabase(Database& source) override
|
||||
{
|
||||
importInternal(*backend_.get(), source);
|
||||
importInternal(*backend_, source);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -92,6 +92,26 @@ enum class HashPrefix : std::uint32_t {
|
||||
* Batch
|
||||
*/
|
||||
Batch = detail::makeHashPrefix('B', 'C', 'H'),
|
||||
|
||||
/**
|
||||
* inner transaction to sign as the counterparty
|
||||
*/
|
||||
CounterpartyTxSign = detail::makeHashPrefix('C', 'P', 'T'),
|
||||
|
||||
/**
|
||||
* inner transaction to multi-sign as the counterparty
|
||||
*/
|
||||
CounterpartyTxMultiSign = detail::makeHashPrefix('C', 'P', 'M'),
|
||||
|
||||
/**
|
||||
* inner transaction to sign as the sponsor
|
||||
*/
|
||||
SponsorTxSign = detail::makeHashPrefix('S', 'P', 'N'),
|
||||
|
||||
/**
|
||||
* inner transaction to multi-sign as the sponsor
|
||||
*/
|
||||
SponsorTxMultiSign = detail::makeHashPrefix('S', 'P', 'M'),
|
||||
};
|
||||
|
||||
template <class Hasher>
|
||||
|
||||
@@ -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,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
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -13,6 +14,7 @@
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
|
||||
#include <boost/container/flat_set.hpp>
|
||||
@@ -105,14 +107,36 @@ public:
|
||||
[[nodiscard]] json::Value
|
||||
getJson(JsonOptions options, bool binary) const;
|
||||
|
||||
/**
|
||||
* Sign the transaction as its account.
|
||||
*
|
||||
* @param publicKey The public key for signing.
|
||||
* @param secretKey The secret key for signing.
|
||||
*/
|
||||
void
|
||||
sign(PublicKey const& publicKey, SecretKey const& secretKey);
|
||||
|
||||
/**
|
||||
* Sign the transaction in one of its signature fields.
|
||||
*
|
||||
* The signature is bound to the role that made it, so it cannot be moved
|
||||
* into another role.
|
||||
*
|
||||
* @param publicKey The public key for signing.
|
||||
* @param secretKey The secret key for signing.
|
||||
* @param role The role signing the transaction.
|
||||
* @param rules The current ledger rules.
|
||||
*/
|
||||
void
|
||||
sign(
|
||||
PublicKey const& publicKey,
|
||||
SecretKey const& secretKey,
|
||||
std::optional<std::reference_wrapper<SField const>> signatureTarget = {});
|
||||
SignatureRole role,
|
||||
Rules const& rules);
|
||||
|
||||
/**
|
||||
* Check the signature.
|
||||
*
|
||||
* @param rules The current ledger rules.
|
||||
* @return `true` if valid signature. If invalid, the error message string.
|
||||
*/
|
||||
@@ -120,7 +144,7 @@ public:
|
||||
checkSign(Rules const& rules) const;
|
||||
|
||||
[[nodiscard]] std::expected<void, std::string>
|
||||
checkBatchSign(Rules const& rules) const;
|
||||
checkBatchSign() const;
|
||||
|
||||
// SQL Functions with metadata.
|
||||
static std::string const&
|
||||
@@ -162,28 +186,28 @@ public:
|
||||
private:
|
||||
/**
|
||||
* Check the signature.
|
||||
*
|
||||
* @param rules The current ledger rules.
|
||||
* @param sigObject Reference to object that contains the signature fields.
|
||||
* Will be *this more often than not.
|
||||
* @param role The role that made the signature in sigObject. Determines
|
||||
* the signing prefix, which binds the signature to that role.
|
||||
* @return `true` if valid signature. If invalid, the error message string.
|
||||
*/
|
||||
[[nodiscard]] std::expected<void, std::string>
|
||||
checkSign(Rules const& rules, STObject const& sigObject) const;
|
||||
checkSign(Rules const& rules, STObject const& sigObject, SignatureRole role) const;
|
||||
|
||||
[[nodiscard]] std::expected<void, std::string>
|
||||
checkSingleSign(STObject const& sigObject) const;
|
||||
checkSingleSign(STObject const& sigObject, HashPrefix prefix) const;
|
||||
|
||||
[[nodiscard]] std::expected<void, std::string>
|
||||
checkMultiSign(Rules const& rules, STObject const& sigObject) const;
|
||||
checkMultiSign(STObject const& sigObject, HashPrefix prefix) const;
|
||||
|
||||
[[nodiscard]] std::expected<void, std::string>
|
||||
checkBatchSingleSign(STObject const& batchSigner, std::vector<uint256> const& txIds) const;
|
||||
|
||||
[[nodiscard]] std::expected<void, std::string>
|
||||
checkBatchMultiSign(
|
||||
STObject const& batchSigner,
|
||||
Rules const& rules,
|
||||
std::vector<uint256> const& txIds) const;
|
||||
checkBatchMultiSign(STObject const& batchSigner, std::vector<uint256> const& txIds) const;
|
||||
|
||||
void
|
||||
buildBatchTxns();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -4,13 +4,65 @@
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* The signature slots on a transaction.
|
||||
*
|
||||
* Each role signs different bytes, so a signature cannot be moved from the
|
||||
* role that made it into another role. See signingPrefix.
|
||||
*/
|
||||
enum class SignatureRole {
|
||||
/**
|
||||
* The transaction's own signature, in sfTxnSignature or sfSigners.
|
||||
*/
|
||||
Transaction,
|
||||
/**
|
||||
* The counterparty's signature, in sfCounterpartySignature.
|
||||
*/
|
||||
Counterparty,
|
||||
/**
|
||||
* The sponsor's signature, in sfSponsorSignature.
|
||||
*/
|
||||
Sponsor
|
||||
};
|
||||
|
||||
/**
|
||||
* The field that holds this role's signature.
|
||||
*
|
||||
* @return The signature field, or nullptr for SignatureRole::Transaction,
|
||||
* whose signature lives at the top level of the transaction.
|
||||
*/
|
||||
[[nodiscard]] SField const*
|
||||
signatureField(SignatureRole role);
|
||||
|
||||
/**
|
||||
* The role that signs into the given field.
|
||||
*
|
||||
* @return The role, or an unseated optional if the field does not hold a
|
||||
* transaction signature.
|
||||
*/
|
||||
[[nodiscard]] std::optional<SignatureRole>
|
||||
signatureRole(SField const& sigField);
|
||||
|
||||
/**
|
||||
* The hash prefix that binds a transaction signature to the role that made it.
|
||||
*
|
||||
* @param role The role making the signature.
|
||||
* @param multiSigning Whether the signature is a multi-signature.
|
||||
* @param rules The current ledger rules.
|
||||
*/
|
||||
[[nodiscard]] HashPrefix
|
||||
signingPrefix(SignatureRole role, bool multiSigning, Rules const& rules);
|
||||
|
||||
/**
|
||||
* Sign an STObject
|
||||
*
|
||||
@@ -49,9 +101,12 @@ verify(
|
||||
|
||||
/**
|
||||
* Return a Serializer suitable for computing a multisigning TxnSignature.
|
||||
*
|
||||
* @param prefix Prefix to insert before the serialized object. Get it from
|
||||
* signingPrefix, so that the signature is bound to the role making it.
|
||||
*/
|
||||
Serializer
|
||||
buildMultiSigningData(STObject const& obj, AccountID const& signingID);
|
||||
buildMultiSigningData(STObject const& obj, AccountID const& signingID, HashPrefix prefix);
|
||||
|
||||
/**
|
||||
* Break the multi-signing hash computation into 2 parts for optimization.
|
||||
@@ -67,7 +122,7 @@ buildMultiSigningData(STObject const& obj, AccountID const& signingID);
|
||||
* signer's unique data.
|
||||
*/
|
||||
Serializer
|
||||
startMultiSigningData(STObject const& obj);
|
||||
startMultiSigningData(STObject const& obj, HashPrefix prefix);
|
||||
|
||||
inline void
|
||||
finishMultiSigningData(AccountID const& signingID, Serializer& s)
|
||||
|
||||
@@ -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,6 +15,7 @@
|
||||
// Add new amendments to the top of this list.
|
||||
// Keep it sorted in reverse chronological order.
|
||||
|
||||
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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -109,6 +109,11 @@ public:
|
||||
* @param lpTokens current LPT balance
|
||||
* @param lpTokensWithdraw amount of tokens to withdraw
|
||||
* @param tfee trading fee in basis points
|
||||
* @param freezeHandling whether a frozen balance is reported as zero
|
||||
* @param authHandling whether an unauthorized MPT balance is reported as
|
||||
* zero
|
||||
* @param reserveHandling whether the recipient owner-reserve check is
|
||||
* enforced when a trustline or MPToken has to be auto-created
|
||||
* @param withdrawAll if withdrawing all lptokens
|
||||
* @param priorBalance balance before fees
|
||||
* @return
|
||||
@@ -128,6 +133,7 @@ public:
|
||||
std::uint16_t tfee,
|
||||
FreezeHandling freezeHandling,
|
||||
AuthHandling authHandling,
|
||||
ReserveHandling reserveHandling,
|
||||
WithdrawAll withdrawAll,
|
||||
XRPAmount const& priorBalance,
|
||||
beast::Journal const& journal);
|
||||
@@ -150,6 +156,11 @@ public:
|
||||
* @param lpTokensAMMBalance current AMM LPT balance
|
||||
* @param lpTokensWithdraw amount of lptokens to withdraw
|
||||
* @param tfee trading fee in basis points
|
||||
* @param freezeHandling whether a frozen balance is reported as zero
|
||||
* @param authHandling whether an unauthorized MPT balance is reported as
|
||||
* zero
|
||||
* @param reserveHandling whether the recipient owner-reserve check is
|
||||
* enforced when a trustline or MPToken has to be auto-created
|
||||
* @param withdrawAll if withdraw all lptokens
|
||||
* @param priorBalance balance before fees
|
||||
* @return
|
||||
@@ -169,6 +180,7 @@ public:
|
||||
std::uint16_t tfee,
|
||||
FreezeHandling freezeHandling,
|
||||
AuthHandling authHandling,
|
||||
ReserveHandling reserveHandling,
|
||||
WithdrawAll withdrawAll,
|
||||
XRPAmount const& priorBalance,
|
||||
beast::Journal const& journal);
|
||||
|
||||
+139
-39
@@ -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, 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,32 +33,94 @@ 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 |
|
||||
| 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
|
||||
|
||||
Caller workflows (`on-pr.yml`, `on-tag.yml`, `on-trigger.yml`) call
|
||||
`reusable-package.yml`. That workflow generates its own packaging matrix from
|
||||
the configs that carry a `package` map (via `generate.py --packaging`) and fans
|
||||
out one job per distro. Each job downloads the pre-built `xrpld` and
|
||||
`validator-keys` binary artifacts and runs in that distro's container, building
|
||||
the format `package.type` declares. The packaging script derives the package
|
||||
version from the downloaded binary's `xrpld --version` output; no CMake
|
||||
configure or build step is needed inside the packaging job.
|
||||
`reusable-package.yml`, which runs in three stages:
|
||||
|
||||
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-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
|
||||
`xrpld --version` output; no CMake configure or build step is needed inside the
|
||||
packaging job.
|
||||
|
||||
The binaries come from the `debian` and `rhel` build configs themselves — the
|
||||
ones carrying the `package` map — which pass `-Dvalidator_keys=ON` so that the
|
||||
@@ -94,11 +157,13 @@ docker run --rm \
|
||||
--pkg-release "${PKG_RELEASE}" \
|
||||
--channel UNRELEASED
|
||||
|
||||
# Output:
|
||||
# build/debbuild/*.deb (DEB + dbgsym; Debian names both .deb)
|
||||
# Output (the deb image writes build/debbuild/*.deb instead):
|
||||
# build/rpmbuild/RPMS/x86_64/*.rpm
|
||||
```
|
||||
|
||||
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`
|
||||
@@ -128,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
|
||||
@@ -142,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`.
|
||||
@@ -155,9 +226,9 @@ the last, and the date and hash say which commit a package on
|
||||
`packages.xrplf.org` came from. Both reach the packaging scripts as arguments,
|
||||
so neither script derives anything itself.
|
||||
|
||||
Publishing is the last step of each packaging job, uploading from the container
|
||||
that built the packages with the `publish_pkg.py` shipped in the image — the
|
||||
same copy other repositories run. Without `publish: true` the step is a
|
||||
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
|
||||
any run that builds packages also exercises the upload routing. `on-trigger.yml`
|
||||
passes `publish: true` for develop pushes in `XRPLF/rippled` and `on-tag.yml`
|
||||
@@ -170,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
|
||||
@@ -202,6 +273,9 @@ the final release. If that normalized package version still contains `-`,
|
||||
packaging fails because RPM forbids `-` in `Version`, and Debian uses `-` as
|
||||
the upstream/revision separator.
|
||||
|
||||
> [!NOTE]
|
||||
> Debug and sanitizer builds are not packaged yet.
|
||||
|
||||
`pkg_version` is the normalized package metadata version derived inside
|
||||
`build_pkg.py` from the binary-reported `xrpld` version (`-` pre-release
|
||||
separator converted to `~`). It is not a separate user input.
|
||||
@@ -236,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.
|
||||
@@ -278,38 +365,51 @@ 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 into `debian/` where `dh_installsystemd`, `dh_installsysusers`, and `dh_installtmpfiles` 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.
|
||||
|
||||
It also rewrites the `libc6` bound to `LIBC_MIN` in `debian/rules`, the glibc
|
||||
the Nix toolchain builds against. `dpkg-shlibdeps` would otherwise derive it
|
||||
from the build host's symbols file — on trixie that yields `libc6 (>= 2.34)`
|
||||
because of `sysconf`, locking out distros the binaries run on. A check fails
|
||||
the build if either binary outgrows `LIBC_MIN`.
|
||||
|
||||
7. Output: `debbuild/*.deb`, the binary package and the `-dbgsym` package.
|
||||
Debian gives dbgsym packages a `.deb` extension; only Ubuntu uses `.ddeb`.
|
||||
|
||||
## Post-build verification
|
||||
|
||||
```bash
|
||||
# DEB
|
||||
dpkg-deb -c debbuild/*.deb | grep -E 'systemd|sysusers|tmpfiles'
|
||||
# DEB (one invocation per package: the dbgsym package is a .deb too)
|
||||
for deb in debbuild/*.deb; do dpkg-deb -c "${deb}"; done | grep -E 'systemd|sysusers|tmpfiles'
|
||||
lintian -I debbuild/*.deb
|
||||
|
||||
# RPM
|
||||
rpm -qlp rpmbuild/RPMS/x86_64/*.rpm
|
||||
|
||||
# Optional, and not in the packaging image: apt-get install -y lintian
|
||||
lintian -I debbuild/*.deb
|
||||
```
|
||||
|
||||
`lintian` still reports `embedded-library zlib`, `no-manual-page` and
|
||||
`initial-upload-closes-no-bugs`; only the `/usr/local` tags are overridden.
|
||||
|
||||
## Reproducibility
|
||||
|
||||
`build_pkg.py` sets `SOURCE_DATE_EPOCH` from the latest git commit time and
|
||||
exports it; the RPM spec clamps file modification times to it via
|
||||
`%build_mtime_policy`. The remaining variables
|
||||
below further improve reproducibility but are _not_ set by the script — export
|
||||
them yourself if needed:
|
||||
Both formats build reproducibly as they are: the same binaries at the same
|
||||
commit give byte-identical packages on a rebuild, and nothing has to be
|
||||
exported by hand.
|
||||
|
||||
```bash
|
||||
export TZ=UTC
|
||||
export LC_ALL=C.UTF-8
|
||||
export GZIP=-n
|
||||
export DEB_BUILD_OPTIONS="noautodbgsym reproducible=+fixfilepath"
|
||||
```
|
||||
`build_pkg.py` sets `SOURCE_DATE_EPOCH` from the latest git commit time.
|
||||
`dpkg-buildpackage` honours it on its own; the RPM spec sets three macros:
|
||||
|
||||
- `%clamp_mtime_to_source_date_epoch` — file modification times, from
|
||||
`SOURCE_DATE_EPOCH`.
|
||||
- `%use_source_date_epoch_as_buildtime` — the `BUILDTIME` header, from the
|
||||
same.
|
||||
- `%_buildhost` — pinned, so the builder's hostname stays out of the header.
|
||||
+97
-11
@@ -19,7 +19,15 @@ from pathlib import Path
|
||||
# This script lives in the repository it packages.
|
||||
SRC_DIR = Path(__file__).resolve().parents[1]
|
||||
|
||||
PRE_RELEASE = re.compile(r"^(b0|b[1-9][0-9]*|rc[0-9]+)(\+.*)?$")
|
||||
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")
|
||||
@@ -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()
|
||||
@@ -133,11 +175,20 @@ def stage_common(build_dir: Path, dest: Path) -> None:
|
||||
shutil.copy2(build_dir / name, dest / name)
|
||||
for source, name in STAGED_FROM_SRC.items():
|
||||
shutil.copy2(SRC_DIR / source, dest / name)
|
||||
|
||||
|
||||
def stage_units(dest: Path, *, prefix: str = "") -> None:
|
||||
"""Copy the systemd, sysusers, tmpfiles and logrotate files into dest.
|
||||
|
||||
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"):
|
||||
@@ -146,6 +197,10 @@ def build_rpm(build_dir: Path, *, version: str, pkg_release: str) -> None:
|
||||
spec = topdir / "SPECS" / "xrpld.spec"
|
||||
shutil.copy2(SRC_DIR / "package" / "rpm" / "xrpld.spec", spec)
|
||||
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",
|
||||
@@ -159,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,
|
||||
*,
|
||||
@@ -171,30 +245,29 @@ 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.
|
||||
for name in STAGED_UNITS:
|
||||
shutil.copy2(staging / name, staging / "debian" / name)
|
||||
# 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}
|
||||
""")
|
||||
(staging / "debian" / "changelog").write_text(changelog)
|
||||
|
||||
(staging / "debian" / "rules").chmod(0o755)
|
||||
|
||||
run("dpkg-buildpackage", "-b", "--no-sign", "-d", cwd=staging)
|
||||
|
||||
|
||||
@@ -217,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,
|
||||
@@ -228,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 "
|
||||
@@ -247,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,
|
||||
@@ -255,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,18 +1,17 @@
|
||||
Source: xrpld
|
||||
Source: @PKG@
|
||||
Section: net
|
||||
Priority: optional
|
||||
Maintainer: XRPL Foundation <[email protected]>
|
||||
Rules-Requires-Root: no
|
||||
Build-Depends:
|
||||
binutils,
|
||||
debhelper-compat (= 13)
|
||||
Standards-Version: 4.7.0
|
||||
Homepage: https://github.com/XRPLF/rippled
|
||||
Vcs-Git: https://github.com/XRPLF/rippled.git
|
||||
Vcs-Browser: https://github.com/XRPLF/rippled
|
||||
|
||||
Package: xrpld
|
||||
Section: net
|
||||
Priority: optional
|
||||
Package: @PKG@
|
||||
Architecture: any
|
||||
Depends:
|
||||
${shlibs:Depends},
|
||||
@@ -23,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@
|
||||
@@ -1,5 +1,5 @@
|
||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: rippled
|
||||
Upstream-Name: xrpld
|
||||
Source: https://github.com/XRPLF/rippled
|
||||
|
||||
Files: *
|
||||
@@ -15,7 +15,7 @@ Copyright: 2016, Ripple Labs Inc.
|
||||
2009-2010, Satoshi Nakamoto
|
||||
2011, The Bitcoin developers
|
||||
2003-2005, Tom Wu
|
||||
License: ISC
|
||||
License: ISC and BSL-1.0 and MIT and Tom-Wu
|
||||
Comment: Built from https://github.com/ripple/validator-keys-tool at the commit
|
||||
pinned in cmake/XrplValidatorKeys.cmake. Besides ISC-licensed code it
|
||||
incorporates work under the Boost Software License 1.0 (ASIO), the MIT/X11
|
||||
@@ -35,3 +35,74 @@ License: ISC
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
License: BSL-1.0
|
||||
Boost Software License - Version 1.0 - August 17th, 2003
|
||||
.
|
||||
Permission is hereby granted, free of charge, to any person or organization
|
||||
obtaining a copy of the software and accompanying documentation covered by
|
||||
this license (the "Software") to use, reproduce, display, distribute,
|
||||
execute, and transmit the Software, and to prepare derivative works of the
|
||||
Software, and to permit third-parties to whom the Software is furnished to
|
||||
do so, all subject to the following:
|
||||
.
|
||||
The copyright notices in the Software and this entire statement, including
|
||||
the above license grant, this restriction and the following disclaimer,
|
||||
must be included in all copies of the Software, in whole or in part, and
|
||||
all derivative works of the Software, unless such copies or derivative
|
||||
works are solely in the form of machine-executable object code generated by
|
||||
a source language processor.
|
||||
.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
License: MIT
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
.
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
License: Tom-Wu
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
.
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
.
|
||||
THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY
|
||||
WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
|
||||
.
|
||||
IN NO EVENT SHALL TOM WU BE LIABLE FOR ANY SPECIAL, INCIDENTAL,
|
||||
INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER
|
||||
RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER OR NOT ADVISED OF
|
||||
THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF LIABILITY, ARISING OUT
|
||||
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
.
|
||||
In addition, the following condition applies:
|
||||
.
|
||||
All redistributions must retain an intact copy of this copyright notice
|
||||
and disclaimer.
|
||||
@@ -1,2 +1,3 @@
|
||||
README.md
|
||||
LICENSE.md
|
||||
validator-keys-LICENSE
|
||||
@@ -0,0 +1,3 @@
|
||||
# Legacy compatibility for pre-FHS package layouts.
|
||||
# TODO: remove after rippled fully deprecated.
|
||||
usr/bin/xrpld usr/local/bin/rippled
|
||||
@@ -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]
|
||||
Regular → Executable
+57
-6
@@ -2,25 +2,76 @@
|
||||
|
||||
export DH_VERBOSE = 1
|
||||
|
||||
# The glibc the Nix toolchain builds against, and so the real floor for the
|
||||
# binaries. dpkg-shlibdeps would instead derive libc6 (>= 2.34) from the build
|
||||
# host's symbols file, where sysconf carries that minver, locking out distros
|
||||
# the binaries actually run on.
|
||||
LIBC_MIN = 2.31
|
||||
|
||||
# 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 $(BINARIES); do \
|
||||
needed=$$(readelf --dyn-syms --wide $$binary \
|
||||
| grep -o 'GLIBC_[0-9.]*' | sed 's/GLIBC_//' | sort -uV | tail -1); \
|
||||
if [ -z "$$needed" ]; then \
|
||||
echo "$$binary: no GLIBC_ symbol versions read, cannot check LIBC_MIN" >&2; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
if dpkg --compare-versions "$$needed" gt "$(LIBC_MIN)"; then \
|
||||
echo "$$binary needs glibc $$needed, above LIBC_MIN $(LIBC_MIN)" >&2; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
done
|
||||
sed -i 's/libc6 (>= [0-9.]*)/libc6 (>= $(LIBC_MIN))/' debian/$(PKG).substvars
|
||||
|
||||
override_dh_dwz:
|
||||
@:
|
||||
@@ -1,2 +0,0 @@
|
||||
# Legacy compat symlinks (remove next major release)
|
||||
usr/bin/xrpld usr/local/bin/rippled
|
||||
@@ -2,9 +2,9 @@ ARG BASE_IMAGE=debian:trixie
|
||||
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
COPY bin/install-packaging-tools.sh /tmp/install-packaging-tools.sh
|
||||
|
||||
RUN /tmp/install-packaging-tools.sh
|
||||
# Bind-mounted rather than copied in, so the installer never lands in a layer.
|
||||
RUN --mount=type=bind,source=bin/install-packaging-tools.sh,target=/install-packaging-tools.sh \
|
||||
/install-packaging-tools.sh
|
||||
|
||||
# See ../README.md, "Publishing from other repositories".
|
||||
COPY package/docker/publish_pkg.py /usr/local/bin/publish_pkg.py
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Publish built DEB and RPM packages to the XRPLF repositories on Nexus.
|
||||
|
||||
Takes packages and a channel, and nothing else, so it publishes whatever built
|
||||
them; see package/README.md, "Publishing from other repositories".
|
||||
Knows nothing about what it uploads beyond the channel, so it publishes whatever
|
||||
built the packages; see package/README.md, "Publishing from other repositories".
|
||||
|
||||
RPMs are uploaded to the hosted repository, but yum clients install from the
|
||||
'rpm-<channel>' group repository in front of it, which serves signed metadata.
|
||||
@@ -29,6 +29,9 @@ STALL_TIMEOUT = 300
|
||||
ATTEMPTS = 4
|
||||
RETRY_DELAY = 5
|
||||
|
||||
# 429 is Nexus asking to slow down, not a rejection, so it retries like a 5xx.
|
||||
RETRYABLE_STATUSES = (429,)
|
||||
|
||||
|
||||
def build_opener() -> urllib.request.OpenerDirector:
|
||||
"""An opener with no redirect handler, so a 3xx raises instead of being followed.
|
||||
@@ -47,9 +50,9 @@ def build_opener() -> urllib.request.OpenerDirector:
|
||||
def upload(url: str, method: str, headers: dict[str, str], package: Path) -> None:
|
||||
"""Send one package, retrying only what is worth retrying.
|
||||
|
||||
A 4xx is a deterministic rejection, so it is reported at once rather than
|
||||
re-sending the whole body three more times. Nexus explains what it rejected
|
||||
in the response body, so that body is always surfaced.
|
||||
A 4xx other than 429 is a deterministic rejection, so it is reported at once
|
||||
rather than re-sending the whole body three more times. Nexus explains what
|
||||
it rejected in the response body, so that body is always surfaced.
|
||||
"""
|
||||
opener = build_opener()
|
||||
|
||||
@@ -67,7 +70,7 @@ def upload(url: str, method: str, headers: dict[str, str], package: Path) -> Non
|
||||
except urllib.error.HTTPError as error:
|
||||
detail = error.read().decode(errors="replace").strip()
|
||||
reason = f"HTTP {error.code}: {detail}"
|
||||
retryable = error.code >= 500
|
||||
retryable = error.code >= 500 or error.code in RETRYABLE_STATUSES
|
||||
except (urllib.error.URLError, OSError) as error:
|
||||
reason = str(error)
|
||||
retryable = True
|
||||
@@ -121,6 +124,8 @@ def main() -> None:
|
||||
token = base64.b64encode(f"{username}:{password}".encode()).decode()
|
||||
auth = {"Authorization": f"Basic {token}"}
|
||||
|
||||
# Deliberately not shared with sign_rpm.py: this script ships standalone in
|
||||
# the packaging image for other repositories to run.
|
||||
packages = sorted(
|
||||
path
|
||||
for path in package_dir.rglob("*")
|
||||
|
||||
+46
-20
@@ -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,17 @@ 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}
|
||||
%{?sysusers_requires_compat}
|
||||
|
||||
%undefine _debugsource_packages
|
||||
%debug_package
|
||||
# Level 3 rather than the el9 default of 19: it shrinks the multi-gigabyte
|
||||
@@ -25,10 +40,13 @@ BuildRequires: systemd-rpm-macros
|
||||
%global _binary_payload w3.zstdio
|
||||
%global _find_debuginfo_dwz_opts %{nil}
|
||||
|
||||
%build_mtime_policy clamp_to_source_date_epoch
|
||||
# Reproducibility: the first two take their value from the SOURCE_DATE_EPOCH
|
||||
# build_pkg.py exports. Without these the header records the wall clock and the
|
||||
# build container's hostname, so two builds of the same commit differ.
|
||||
%global clamp_mtime_to_source_date_epoch 1
|
||||
%global use_source_date_epoch_as_buildtime 1
|
||||
%global _buildhost xrplf.org
|
||||
|
||||
%{?systemd_requires}
|
||||
%{?sysusers_requires_compat}
|
||||
|
||||
%description
|
||||
xrpld is the reference implementation of the XRP Ledger protocol. It
|
||||
@@ -44,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 -Dm0644 /dev/null %{buildroot}%{_presetdir}/50-xrpld.preset
|
||||
cat >%{buildroot}%{_presetdir}/50-xrpld.preset <<'EOF'
|
||||
install -d %{buildroot}%{_presetdir}
|
||||
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
|
||||
@@ -70,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
|
||||
systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || :
|
||||
%tmpfiles_create_package %{base_name} %{_sourcedir}/xrpld.tmpfiles
|
||||
%systemd_post xrpld.service
|
||||
|
||||
%preun
|
||||
@@ -84,24 +102,32 @@ systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || :
|
||||
|
||||
%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}
|
||||
%license %{_docdir}/%{name}/LICENSE.md
|
||||
%license %{_docdir}/%{name}/validator-keys-LICENSE
|
||||
%doc %{_docdir}/%{name}/README.md
|
||||
|
||||
%dir %{_sysconfdir}/%{name}
|
||||
%attr(0755,root,root) %dir %{_sysconfdir}/%{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
|
||||
%{_presetdir}/50-xrpld.preset
|
||||
%attr(0644,root,root) %{_presetdir}/50-%{base_name}.preset
|
||||
%{_sysusersdir}/xrpld.conf
|
||||
%{_tmpfilesdir}/xrpld.conf
|
||||
%ghost %dir /var/lib/xrpld
|
||||
|
||||
@@ -17,6 +17,8 @@ ProtectHome=true
|
||||
PrivateTmp=true
|
||||
User=xrpld
|
||||
Group=xrpld
|
||||
# xrpld.tmpfiles creates these at install and boot; these recreate them on
|
||||
# every start, so a removed directory does not stop the service.
|
||||
StateDirectory=xrpld
|
||||
StateDirectoryMode=0750
|
||||
LogsDirectory=xrpld
|
||||
|
||||
@@ -107,6 +107,8 @@ def main() -> None:
|
||||
args = parser.parse_args()
|
||||
package_dir: Path = args.package_dir
|
||||
|
||||
# Deliberately not shared with publish_pkg.py, which ships standalone in the
|
||||
# packaging image.
|
||||
rpms = sorted(path for path in package_dir.rglob("*.rpm") if path.is_file())
|
||||
# Signing nothing would otherwise look like a successful signing.
|
||||
assert rpms, f"no RPMs found in {package_dir}"
|
||||
|
||||
@@ -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-b3"
|
||||
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
|
||||
|
||||
@@ -168,10 +168,10 @@ STTx::getMentionedAccounts() const
|
||||
}
|
||||
|
||||
static Blob
|
||||
getSigningData(STTx const& that)
|
||||
getSigningData(STTx const& that, HashPrefix prefix)
|
||||
{
|
||||
Serializer s;
|
||||
s.add32(HashPrefix::TxSign);
|
||||
s.add32(prefix);
|
||||
that.addWithoutSigningFields(s);
|
||||
return s.getData();
|
||||
}
|
||||
@@ -212,30 +212,42 @@ STTx::getSeqProxy() const
|
||||
return SeqProxy::rawTicket(*ticketSeq);
|
||||
}
|
||||
|
||||
void
|
||||
STTx::sign(PublicKey const& publicKey, SecretKey const& secretKey)
|
||||
{
|
||||
// The account's own signature always covers the plain transaction prefix;
|
||||
// see signingPrefix for the role signatures that do not.
|
||||
auto const data = getSigningData(*this, HashPrefix::TxSign);
|
||||
|
||||
setFieldVL(sfTxnSignature, xrpl::sign(publicKey, secretKey, makeSlice(data)));
|
||||
tid_ = getHash(HashPrefix::TransactionId);
|
||||
}
|
||||
|
||||
void
|
||||
STTx::sign(
|
||||
PublicKey const& publicKey,
|
||||
SecretKey const& secretKey,
|
||||
std::optional<std::reference_wrapper<SField const>> signatureTarget)
|
||||
SignatureRole role,
|
||||
Rules const& rules)
|
||||
{
|
||||
auto const data = getSigningData(*this);
|
||||
auto const data = getSigningData(*this, signingPrefix(role, false, rules));
|
||||
|
||||
auto const sig = xrpl::sign(publicKey, secretKey, makeSlice(data));
|
||||
|
||||
if (signatureTarget)
|
||||
if (auto const target = signatureField(role))
|
||||
{
|
||||
auto& target = peekFieldObject(*signatureTarget);
|
||||
target.setFieldVL(sfTxnSignature, sig);
|
||||
peekFieldObject(*target).setFieldVL(sfTxnSignature, sig);
|
||||
}
|
||||
else
|
||||
{
|
||||
setFieldVL(sfTxnSignature, sig);
|
||||
}
|
||||
|
||||
tid_ = getHash(HashPrefix::TransactionId);
|
||||
}
|
||||
|
||||
std::expected<void, std::string>
|
||||
STTx::checkSign(Rules const& rules, STObject const& sigObject) const
|
||||
STTx::checkSign(Rules const& rules, STObject const& sigObject, SignatureRole role) const
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -244,8 +256,10 @@ STTx::checkSign(Rules const& rules, STObject const& sigObject) const
|
||||
// multi-signing. Otherwise we're single-signing.
|
||||
|
||||
Blob const& signingPubKey = sigObject.getFieldVL(sfSigningPubKey);
|
||||
return signingPubKey.empty() ? checkMultiSign(rules, sigObject)
|
||||
: checkSingleSign(sigObject);
|
||||
bool const multiSigning = signingPubKey.empty();
|
||||
auto const prefix = signingPrefix(role, multiSigning, rules);
|
||||
return multiSigning ? checkMultiSign(sigObject, prefix)
|
||||
: checkSingleSign(sigObject, prefix);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
@@ -256,20 +270,20 @@ STTx::checkSign(Rules const& rules, STObject const& sigObject) const
|
||||
std::expected<void, std::string>
|
||||
STTx::checkSign(Rules const& rules) const
|
||||
{
|
||||
if (auto const ret = checkSign(rules, *this); !ret)
|
||||
if (auto const ret = checkSign(rules, *this, SignatureRole::Transaction); !ret)
|
||||
return ret;
|
||||
|
||||
if (isFieldPresent(sfCounterpartySignature))
|
||||
{
|
||||
auto const counterSig = getFieldObject(sfCounterpartySignature);
|
||||
if (auto const ret = checkSign(rules, counterSig); !ret)
|
||||
if (auto const ret = checkSign(rules, counterSig, SignatureRole::Counterparty); !ret)
|
||||
return std::unexpected("Counterparty: " + ret.error());
|
||||
}
|
||||
|
||||
if (isFieldPresent(sfSponsorSignature))
|
||||
{
|
||||
auto const sponsorSignatureObj = getFieldObject(sfSponsorSignature);
|
||||
if (auto const ret = checkSign(rules, sponsorSignatureObj); !ret)
|
||||
if (auto const ret = checkSign(rules, sponsorSignatureObj, SignatureRole::Sponsor); !ret)
|
||||
return std::unexpected("Sponsor: " + ret.error());
|
||||
}
|
||||
|
||||
@@ -277,14 +291,14 @@ STTx::checkSign(Rules const& rules) const
|
||||
// of signature checking.
|
||||
if (isFieldPresent(sfBatchSigners))
|
||||
{
|
||||
if (auto const ret = checkBatchSign(rules); !ret)
|
||||
if (auto const ret = checkBatchSign(); !ret)
|
||||
return ret;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::expected<void, std::string>
|
||||
STTx::checkBatchSign(Rules const& rules) const
|
||||
STTx::checkBatchSign() const
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -318,7 +332,7 @@ STTx::checkBatchSign(Rules const& rules) const
|
||||
for (auto const& signer : signers)
|
||||
{
|
||||
Blob const& signingPubKey = signer.getFieldVL(sfSigningPubKey);
|
||||
auto const result = signingPubKey.empty() ? checkBatchMultiSign(signer, rules, txIds)
|
||||
auto const result = signingPubKey.empty() ? checkBatchMultiSign(signer, txIds)
|
||||
: checkBatchSingleSign(signer, txIds);
|
||||
|
||||
if (!result)
|
||||
@@ -447,9 +461,9 @@ singleSignHelper(STObject const& sigObject, Slice const& data)
|
||||
}
|
||||
|
||||
std::expected<void, std::string>
|
||||
STTx::checkSingleSign(STObject const& sigObject) const
|
||||
STTx::checkSingleSign(STObject const& sigObject, HashPrefix prefix) const
|
||||
{
|
||||
auto const data = getSigningData(*this);
|
||||
auto const data = getSigningData(*this, prefix);
|
||||
return singleSignHelper(sigObject, makeSlice(data));
|
||||
}
|
||||
|
||||
@@ -467,8 +481,7 @@ std::expected<void, std::string>
|
||||
multiSignHelper(
|
||||
STObject const& sigObject,
|
||||
std::optional<AccountID> txnAccountID,
|
||||
std::function<Serializer(AccountID const&)> makeMsg,
|
||||
Rules const& rules)
|
||||
std::function<Serializer(AccountID const&)> makeMsg)
|
||||
{
|
||||
// Make sure the MultiSigners are present. Otherwise they are not
|
||||
// attempting multi-signing and we just have a bad SigningPubKey.
|
||||
@@ -541,10 +554,7 @@ multiSignHelper(
|
||||
}
|
||||
|
||||
std::expected<void, std::string>
|
||||
STTx::checkBatchMultiSign(
|
||||
STObject const& batchSigner,
|
||||
Rules const& rules,
|
||||
std::vector<uint256> const& txIds) const
|
||||
STTx::checkBatchMultiSign(STObject const& batchSigner, std::vector<uint256> const& txIds) const
|
||||
{
|
||||
XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::checkBatchMultiSign : batch transaction");
|
||||
// We can ease the computational load inside the loop a bit by
|
||||
@@ -555,18 +565,15 @@ STTx::checkBatchMultiSign(
|
||||
serializeBatch(dataStart, getAccountID(sfAccount), getSeqProxy().value(), getFlags(), txIds);
|
||||
dataStart.addBitString(batchSignerAccount);
|
||||
return multiSignHelper(
|
||||
batchSigner,
|
||||
batchSignerAccount,
|
||||
[&dataStart](AccountID const& accountID) -> Serializer {
|
||||
batchSigner, batchSignerAccount, [&dataStart](AccountID const& accountID) -> Serializer {
|
||||
Serializer s = dataStart;
|
||||
finishMultiSigningData(accountID, s);
|
||||
return s;
|
||||
},
|
||||
rules);
|
||||
});
|
||||
}
|
||||
|
||||
std::expected<void, std::string>
|
||||
STTx::checkMultiSign(Rules const& rules, STObject const& sigObject) const
|
||||
STTx::checkMultiSign(STObject const& sigObject, HashPrefix prefix) const
|
||||
{
|
||||
// Used inside the loop in multiSignHelper to enforce that
|
||||
// the account owner may not multisign for themselves.
|
||||
@@ -578,16 +585,13 @@ STTx::checkMultiSign(Rules const& rules, STObject const& sigObject) const
|
||||
// We can ease the computational load inside the loop a bit by
|
||||
// pre-constructing part of the data that we hash. Fill a Serializer
|
||||
// with the stuff that stays constant from signature to signature.
|
||||
Serializer dataStart = startMultiSigningData(*this);
|
||||
Serializer dataStart = startMultiSigningData(*this, prefix);
|
||||
return multiSignHelper(
|
||||
sigObject,
|
||||
txnAccountID,
|
||||
[&dataStart](AccountID const& accountID) -> Serializer {
|
||||
sigObject, txnAccountID, [&dataStart](AccountID const& accountID) -> Serializer {
|
||||
Serializer s = dataStart;
|
||||
finishMultiSigningData(accountID, s);
|
||||
return s;
|
||||
},
|
||||
rules);
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -1,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 {
|
||||
@@ -104,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;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,77 @@
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STExchange.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
SField const*
|
||||
signatureField(SignatureRole role)
|
||||
{
|
||||
switch (role)
|
||||
{
|
||||
case SignatureRole::Transaction:
|
||||
return nullptr;
|
||||
case SignatureRole::Counterparty:
|
||||
return &sfCounterpartySignature;
|
||||
case SignatureRole::Sponsor:
|
||||
return &sfSponsorSignature;
|
||||
}
|
||||
UNREACHABLE("xrpl::signatureField : unknown SignatureRole");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::optional<SignatureRole>
|
||||
signatureRole(SField const& sigField)
|
||||
{
|
||||
if (sigField == sfCounterpartySignature)
|
||||
return SignatureRole::Counterparty;
|
||||
if (sigField == sfSponsorSignature)
|
||||
return SignatureRole::Sponsor;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Signature validity depends on fixCleanup3_4_0: a role signature covers
|
||||
// different bytes before and after the amendment activates. checkValidity
|
||||
// caches its verdict per transaction ID, so it keeps two separate cache slots
|
||||
// for role-signature transactions (kSfSiggoodOldPrefix / kSfSigbadOldPrefix in
|
||||
// tx/apply.cpp) to keep a pre-fix verdict from being reused in the post-fix
|
||||
// era, and vice versa. See the block comment in tx/apply.cpp for the details
|
||||
// and the reason both directions matter.
|
||||
HashPrefix
|
||||
signingPrefix(SignatureRole role, bool multiSigning, Rules const& rules)
|
||||
{
|
||||
// Before fixCleanup3_4_0 every signature on a transaction covered the same
|
||||
// bytes, so a signature could be moved from one role to another.
|
||||
if (!rules.enabled(fixCleanup3_4_0))
|
||||
return multiSigning ? HashPrefix::TxMultiSign : HashPrefix::TxSign;
|
||||
|
||||
switch (role)
|
||||
{
|
||||
case SignatureRole::Transaction:
|
||||
return multiSigning ? HashPrefix::TxMultiSign : HashPrefix::TxSign;
|
||||
case SignatureRole::Counterparty:
|
||||
return multiSigning ? HashPrefix::CounterpartyTxMultiSign
|
||||
: HashPrefix::CounterpartyTxSign;
|
||||
case SignatureRole::Sponsor:
|
||||
return multiSigning ? HashPrefix::SponsorTxMultiSign : HashPrefix::SponsorTxSign;
|
||||
}
|
||||
UNREACHABLE("xrpl::signingPrefix : unknown SignatureRole");
|
||||
return multiSigning ? HashPrefix::TxMultiSign : HashPrefix::TxSign;
|
||||
}
|
||||
|
||||
void
|
||||
sign(
|
||||
STObject& st,
|
||||
@@ -70,18 +130,18 @@ verify(STObject const& st, HashPrefix const& prefix, PublicKey const& pk, SF_VL
|
||||
// So, if we support multiple levels of signing, then we'll need to
|
||||
// incorporate the "signing for" accounts into the signing data as well.
|
||||
Serializer
|
||||
buildMultiSigningData(STObject const& obj, AccountID const& signingID)
|
||||
buildMultiSigningData(STObject const& obj, AccountID const& signingID, HashPrefix prefix)
|
||||
{
|
||||
Serializer s{startMultiSigningData(obj)};
|
||||
Serializer s{startMultiSigningData(obj, prefix)};
|
||||
finishMultiSigningData(signingID, s);
|
||||
return s;
|
||||
}
|
||||
|
||||
Serializer
|
||||
startMultiSigningData(STObject const& obj)
|
||||
startMultiSigningData(STObject const& obj, HashPrefix prefix)
|
||||
{
|
||||
Serializer s;
|
||||
s.add32(HashPrefix::TxMultiSign);
|
||||
s.add32(prefix);
|
||||
obj.addWithoutSigningFields(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,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()
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/OpenView.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
@@ -23,13 +24,38 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// These are the same flags defined as HashRouterFlags::PRIVATE1-4 in
|
||||
// HashRouter.h
|
||||
// This file owns HashRouterFlags::PRIVATE1-4 and PRIVATE7-8 in HashRouter.h.
|
||||
// These are the first four; the other two are below.
|
||||
constexpr HashRouterFlags kSfSigbad = HashRouterFlags::PRIVATE1; // Signature is bad
|
||||
constexpr HashRouterFlags kSfSiggood = HashRouterFlags::PRIVATE2; // Signature is good
|
||||
constexpr HashRouterFlags kSfLocalbad = HashRouterFlags::PRIVATE3; // Local checks failed
|
||||
constexpr HashRouterFlags kSfLocalgood = HashRouterFlags::PRIVATE4; // Local checks passed
|
||||
|
||||
// Before fixCleanup3_4_0, a signature in an alternate role field, such as
|
||||
// sfSponsorSignature, covered the same bytes as the top level signature. Which
|
||||
// bytes a role signature must cover therefore depends on whether the fix is
|
||||
// enabled, but the four flags above record only the verdict, not the rules that
|
||||
// produced it. A verdict reached under one prefix would otherwise be reused
|
||||
// under the other.
|
||||
//
|
||||
// The two flags below hold the verdict for the pre-fix prefixes, so the pre-fix
|
||||
// and post-fix verdicts occupy separate slots and neither is ever read in the
|
||||
// other's era. Nothing is cleared when the amendment activates: setFlags only
|
||||
// sets bits, so a stale pre-fix verdict simply stops being read and ages out
|
||||
// with the rest of the routing table.
|
||||
//
|
||||
// This is not one switchover at a single instant. The era is chosen per call
|
||||
// from the rules passed in, and callers do not agree on the rules: relay and
|
||||
// submit verify against the validated rules, which lag the open ledger rules
|
||||
// that preflight2 verifies against. At the amendment's flag ledger the same
|
||||
// transaction can therefore be checked under both prefixes, on the same node,
|
||||
// at the same time.
|
||||
//
|
||||
// Remove these two flags, and oldPrefixSig below, when Cleanup3_4_0 is retired
|
||||
// in features.macro.
|
||||
constexpr HashRouterFlags kSfSigbadOldPrefix = HashRouterFlags::PRIVATE7;
|
||||
constexpr HashRouterFlags kSfSiggoodOldPrefix = HashRouterFlags::PRIVATE8;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
std::pair<Validity, std::string>
|
||||
@@ -48,21 +74,41 @@ checkValidity(HashRouter& router, STTx const& tx, Rules const& rules)
|
||||
return {Validity::SigBad, "Batch inner transactions are never considered validly signed."};
|
||||
}
|
||||
|
||||
if (any(flags & kSfSigbad))
|
||||
// Pick the cache slot for this call's era; see kSfSiggoodOldPrefix above.
|
||||
// Only a transaction that carries a role signature, and only while the fix
|
||||
// is disabled, uses the separate slot. Every other transaction, and every
|
||||
// transaction once the fix is enabled, uses the ordinary flags and verifies
|
||||
// exactly once, so there is no steady state cost.
|
||||
//
|
||||
// Both directions matter. A good verdict from before the fix must not let a
|
||||
// signature moved between roles survive the amendment, and a bad verdict
|
||||
// from before the fix must not condemn a transaction that the new prefixes
|
||||
// accept.
|
||||
//
|
||||
// Whether a transaction carries a role signature is fixed for its ID: the
|
||||
// fields are kNotSigning, so they are excluded from the signed bytes, but
|
||||
// they are still covered by the transaction ID. Repeat calls for one ID
|
||||
// therefore always agree on which slot pair to use.
|
||||
bool const oldPrefixSig = !rules.enabled(fixCleanup3_4_0) &&
|
||||
(tx.isFieldPresent(sfSponsorSignature) || tx.isFieldPresent(sfCounterpartySignature));
|
||||
auto const sigbadFlag = oldPrefixSig ? kSfSigbadOldPrefix : kSfSigbad;
|
||||
auto const siggoodFlag = oldPrefixSig ? kSfSiggoodOldPrefix : kSfSiggood;
|
||||
|
||||
if (any(flags & sigbadFlag))
|
||||
{
|
||||
// Signature is known bad
|
||||
return {Validity::SigBad, "Transaction has bad signature."};
|
||||
}
|
||||
|
||||
if (!any(flags & kSfSiggood))
|
||||
if (!any(flags & siggoodFlag))
|
||||
{
|
||||
auto const sigVerify = tx.checkSign(rules);
|
||||
if (!sigVerify)
|
||||
{
|
||||
router.setFlags(id, kSfSigbad);
|
||||
router.setFlags(id, sigbadFlag);
|
||||
return {Validity::SigBad, sigVerify.error()};
|
||||
}
|
||||
router.setFlags(id, kSfSiggood);
|
||||
router.setFlags(id, siggoodFlag);
|
||||
}
|
||||
|
||||
// Signature is now known good
|
||||
@@ -94,6 +140,19 @@ checkValidity(HashRouter& router, STTx const& tx, Rules const& rules)
|
||||
void
|
||||
forceValidity(HashRouter& router, uint256 const& txid, Validity validity)
|
||||
{
|
||||
// Callers reach here when they deliberately skip signature verification,
|
||||
// such as a cluster peer that trusts its neighbor's checks, or a
|
||||
// configuration that turns signature checks off. Nothing was verified, so
|
||||
// there is no prefix era to record. Mark both of checkValidity's signature
|
||||
// slots good: otherwise the forced verdict is ignored for a role-signature
|
||||
// transaction until fixCleanup3_4_0 is enabled, and the signature the
|
||||
// caller meant to skip gets verified after all. Marking both cannot leak a
|
||||
// verdict across eras, because no verdict was reached, and this is the only
|
||||
// place the distinction can be recorded: kSfSiggood alone does not say
|
||||
// whether checkValidity verified a post-fix signature or a caller forced
|
||||
// the result. An already cached bad verdict still wins, since checkValidity
|
||||
// tests its bad flag first. Drop kSfSiggoodOldPrefix when Cleanup3_4_0 is
|
||||
// retired.
|
||||
HashRouterFlags flags = HashRouterFlags::UNDEFINED;
|
||||
switch (validity)
|
||||
{
|
||||
@@ -101,7 +160,7 @@ forceValidity(HashRouter& router, uint256 const& txid, Validity validity)
|
||||
flags |= kSfLocalgood;
|
||||
[[fallthrough]];
|
||||
case Validity::SigGoodOnly:
|
||||
flags |= kSfSiggood;
|
||||
flags |= kSfSiggood | kSfSiggoodOldPrefix;
|
||||
[[fallthrough]];
|
||||
case Validity::SigBad:
|
||||
// would be silly to call directly
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -237,6 +237,7 @@ AMMClawback::applyGuts(Sandbox& sb)
|
||||
0,
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
ReserveHandling::IgnoreReserve,
|
||||
WithdrawAll::Yes,
|
||||
preFeeBalance_,
|
||||
ctx_.journal);
|
||||
@@ -345,6 +346,7 @@ AMMClawback::equalWithdrawMatchingOneAmount(
|
||||
0,
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
ReserveHandling::IgnoreReserve,
|
||||
WithdrawAll::Yes,
|
||||
preFeeBalance_,
|
||||
ctx_.journal);
|
||||
@@ -385,6 +387,7 @@ AMMClawback::equalWithdrawMatchingOneAmount(
|
||||
0,
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
ReserveHandling::IgnoreReserve,
|
||||
WithdrawAll::No,
|
||||
preFeeBalance_,
|
||||
ctx_.journal);
|
||||
@@ -406,6 +409,7 @@ AMMClawback::equalWithdrawMatchingOneAmount(
|
||||
0,
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
ReserveHandling::IgnoreReserve,
|
||||
WithdrawAll::No,
|
||||
preFeeBalance_,
|
||||
ctx_.journal);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -527,6 +527,7 @@ AMMWithdraw::withdraw(
|
||||
tfee,
|
||||
issuerFreezeHandling(),
|
||||
AuthHandling::ZeroIfUnauthorized,
|
||||
ReserveHandling::EnforceReserve,
|
||||
isWithdrawAll(ctx_.tx),
|
||||
preFeeBalance_,
|
||||
j_);
|
||||
@@ -548,6 +549,7 @@ AMMWithdraw::withdraw(
|
||||
std::uint16_t tfee,
|
||||
FreezeHandling freezeHandling,
|
||||
AuthHandling authHandling,
|
||||
ReserveHandling reserveHandling,
|
||||
WithdrawAll withdrawAll,
|
||||
XRPAmount const& priorBalance,
|
||||
beast::Journal const& journal)
|
||||
@@ -681,6 +683,14 @@ AMMWithdraw::withdraw(
|
||||
});
|
||||
if (assetNotExists)
|
||||
{
|
||||
// Intentionally ignore the reserve check for AMMClawback, so the
|
||||
// holder can not avoid clawback by deleting the trustline/MPToken
|
||||
// and keeping a low spendable balance. AMMClawback has a higher
|
||||
// priority than the reserve check.
|
||||
if (view.rules().enabled(fixCleanup3_4_0) &&
|
||||
reserveHandling == ReserveHandling::IgnoreReserve)
|
||||
return tesSUCCESS;
|
||||
|
||||
auto sleAccount = view.peek(keylet::account(account));
|
||||
if (!sleAccount)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
@@ -850,6 +860,7 @@ AMMWithdraw::equalWithdrawTokens(
|
||||
tfee,
|
||||
issuerFreezeHandling(),
|
||||
AuthHandling::ZeroIfUnauthorized,
|
||||
ReserveHandling::EnforceReserve,
|
||||
isWithdrawAll(ctx_.tx),
|
||||
preFeeBalance_,
|
||||
ctx_.journal);
|
||||
@@ -903,6 +914,7 @@ AMMWithdraw::equalWithdrawTokens(
|
||||
std::uint16_t tfee,
|
||||
FreezeHandling freezeHandling,
|
||||
AuthHandling authHandling,
|
||||
ReserveHandling reserveHandling,
|
||||
WithdrawAll withdrawAll,
|
||||
XRPAmount const& priorBalance,
|
||||
beast::Journal const& journal)
|
||||
@@ -926,6 +938,7 @@ AMMWithdraw::equalWithdrawTokens(
|
||||
tfee,
|
||||
freezeHandling,
|
||||
authHandling,
|
||||
reserveHandling,
|
||||
WithdrawAll::Yes,
|
||||
priorBalance,
|
||||
journal);
|
||||
@@ -962,6 +975,7 @@ AMMWithdraw::equalWithdrawTokens(
|
||||
tfee,
|
||||
freezeHandling,
|
||||
authHandling,
|
||||
reserveHandling,
|
||||
withdrawAll,
|
||||
priorBalance,
|
||||
journal);
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
#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>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/OfferHelpers.h>
|
||||
@@ -242,8 +242,31 @@ OfferCreate::preclaim(PreclaimContext const& ctx)
|
||||
// is part of the domain
|
||||
if (ctx.tx.isFieldPresent(sfDomainID))
|
||||
{
|
||||
if (!permissioned_dex::accountInDomain(ctx.view, id, ctx.tx[sfDomainID]))
|
||||
return tecNO_PERMISSION;
|
||||
if (ctx.view.rules().enabled(fixCleanup3_4_0))
|
||||
{
|
||||
auto const domainID = ctx.tx[sfDomainID];
|
||||
auto const sleDomain = ctx.view.read(keylet::permissionedDomain(domainID));
|
||||
if (!sleDomain)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// Domain owner is always considered in the domain, no credential check
|
||||
// needed. For all other accounts, use validDomain which detects expired
|
||||
// credentials. Suppress tecEXPIRED here so doApply can run and delete
|
||||
// the expired credential SLEs from the ledger.
|
||||
if (sleDomain->getAccountID(sfOwner) != id)
|
||||
{
|
||||
// validDomain returns tecNO_AUTH when no matching credential is
|
||||
// found. Map it to tecNO_PERMISSION to preserve existing behavior.
|
||||
if (auto const err = credentials::validDomain(ctx.view, domainID, id);
|
||||
!isTesSuccess(err) && err != tecEXPIRED)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!permissioned_dex::accountInDomain(ctx.view, id, ctx.tx[sfDomainID]))
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
}
|
||||
|
||||
if (auto const ter = canTrade(ctx.view, saTakerPays.asset()); !isTesSuccess(ter))
|
||||
@@ -610,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;
|
||||
@@ -990,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";
|
||||
|
||||
@@ -1000,6 +1023,27 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel)
|
||||
TER
|
||||
OfferCreate::doApply()
|
||||
{
|
||||
// If a DomainID is present, verify the account is still in the domain and
|
||||
// delete any expired credential SLEs. This must happen before the Sandboxes
|
||||
// are created: if we return a tec error, the engine applies sbCancel (not
|
||||
// sb) to rawView, so deletions made inside sb would be lost. Deletions made
|
||||
// directly to ctx_.view() here are preserved regardless of which branch
|
||||
// applyGuts takes.
|
||||
if (ctx_.tx.isFieldPresent(sfDomainID) && ctx_.view().rules().enabled(fixCleanup3_4_0))
|
||||
{
|
||||
auto const domainID = ctx_.tx[sfDomainID];
|
||||
auto const sleDomain = ctx_.view().read(keylet::permissionedDomain(domainID));
|
||||
if (!sleDomain)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (sleDomain->getAccountID(sfOwner) != accountID_)
|
||||
{
|
||||
if (auto const err = verifyValidDomain(ctx_.view(), accountID_, domainID, j_);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
// This is the ledger view that we work against. Transactions are applied
|
||||
// as we go on processing transactions.
|
||||
Sandbox sb(&ctx_.view());
|
||||
|
||||
@@ -169,6 +169,12 @@ EscrowCancel::doApply()
|
||||
auto const sle = ctx_.view().peek(keylet::account(account));
|
||||
STAmount const amount = slep->getFieldAmount(sfAmount);
|
||||
|
||||
// The return can re-create a holding the owner deleted while the escrow
|
||||
// was pending; the removed escrow must not be counted against its reserve.
|
||||
bool const recycleReserve = ctx_.view().rules().enabled(fixCleanup3_4_0);
|
||||
if (recycleReserve)
|
||||
decreaseOwnerCountForObject(ctx_.view(), sle, slep, 1, ctx_.journal);
|
||||
|
||||
// Transfer amount back to the owner
|
||||
if (isXRP(amount))
|
||||
{
|
||||
@@ -212,7 +218,8 @@ EscrowCancel::doApply()
|
||||
}
|
||||
}
|
||||
|
||||
decreaseOwnerCountForObject(ctx_.view(), sle, slep, 1, ctx_.journal);
|
||||
if (!recycleReserve)
|
||||
decreaseOwnerCountForObject(ctx_.view(), sle, slep, 1, ctx_.journal);
|
||||
|
||||
// Remove escrow from ledger
|
||||
ctx_.view().erase(slep);
|
||||
|
||||
@@ -343,14 +343,12 @@ EscrowFinish::doApply()
|
||||
}
|
||||
}
|
||||
|
||||
// With the Sponsor amendment, release the escrow reserve before delivery.
|
||||
// Token delivery can auto-create a destination holding, and the same
|
||||
// sponsor (or the same account, for a self-escrow) may cover both the
|
||||
// escrow being removed and the holding being created. Without the
|
||||
// amendment, keep the legacy order: releasing early changes the reserve
|
||||
// arithmetic for self-escrows and would break consensus if not gated.
|
||||
bool const sponsorEnabled = ctx_.view().rules().enabled(featureSponsor);
|
||||
if (sponsorEnabled)
|
||||
// Delivery can auto-create the destination's holding; the removed escrow
|
||||
// must not be counted against its reserve. The two share a reserve payer
|
||||
// for a self-escrow, or when one sponsor covers both.
|
||||
bool const recycleReserve =
|
||||
ctx_.view().rules().enabled(featureSponsor) || ctx_.view().rules().enabled(fixCleanup3_4_0);
|
||||
if (recycleReserve)
|
||||
decreaseOwnerCountForObject(ctx_.view(), account, slep, 1, ctx_.journal);
|
||||
|
||||
STAmount const amount = slep->getFieldAmount(sfAmount);
|
||||
@@ -402,8 +400,7 @@ EscrowFinish::doApply()
|
||||
|
||||
ctx_.view().update(sled);
|
||||
|
||||
// Adjust source owner count (legacy position, pre-Sponsor)
|
||||
if (!sponsorEnabled)
|
||||
if (!recycleReserve)
|
||||
decreaseOwnerCountForObject(ctx_.view(), account, slep, 1, ctx_.journal);
|
||||
|
||||
// Remove escrow from ledger
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
@@ -458,11 +477,41 @@ Payment::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfDomainID))
|
||||
{
|
||||
if (!permissioned_dex::accountInDomain(ctx.view, ctx.tx[sfAccount], ctx.tx[sfDomainID]))
|
||||
return tecNO_PERMISSION;
|
||||
if (ctx.view.rules().enabled(fixCleanup3_4_0))
|
||||
{
|
||||
auto const domainID = ctx.tx[sfDomainID];
|
||||
auto const sleDomain = ctx.view.read(keylet::permissionedDomain(domainID));
|
||||
if (!sleDomain)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
if (!permissioned_dex::accountInDomain(ctx.view, ctx.tx[sfDestination], ctx.tx[sfDomainID]))
|
||||
return tecNO_PERMISSION;
|
||||
// Domain owner is always considered in the domain. For other accounts,
|
||||
// suppress tecEXPIRED so doApply can run and delete expired credential
|
||||
// SLEs from the ledger.
|
||||
auto const checkAccount = [&](AccountID const& acct) -> TER {
|
||||
if (sleDomain->getAccountID(sfOwner) == acct)
|
||||
return tesSUCCESS;
|
||||
// validDomain returns tecNO_AUTH when no matching credential is
|
||||
// found. Map it to tecNO_PERMISSION to preserve existing behavior.
|
||||
if (auto const err = credentials::validDomain(ctx.view, domainID, acct);
|
||||
!isTesSuccess(err) && err != tecEXPIRED)
|
||||
return tecNO_PERMISSION;
|
||||
return tesSUCCESS;
|
||||
};
|
||||
|
||||
if (auto const err = checkAccount(ctx.tx[sfAccount]); !isTesSuccess(err))
|
||||
return err;
|
||||
if (auto const err = checkAccount(ctx.tx[sfDestination]); !isTesSuccess(err))
|
||||
return err;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!permissioned_dex::accountInDomain(ctx.view, ctx.tx[sfAccount], ctx.tx[sfDomainID]))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
if (!permissioned_dex::accountInDomain(
|
||||
ctx.view, ctx.tx[sfDestination], ctx.tx[sfDomainID]))
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
@@ -471,6 +520,31 @@ Payment::preclaim(PreclaimContext const& ctx)
|
||||
TER
|
||||
Payment::doApply()
|
||||
{
|
||||
// If a DomainID is present, verify both sender and destination are still in
|
||||
// the domain and delete any expired credential SLEs from the ledger.
|
||||
if (ctx_.tx.isFieldPresent(sfDomainID) && ctx_.view().rules().enabled(fixCleanup3_4_0))
|
||||
{
|
||||
auto const domainID = ctx_.tx[sfDomainID];
|
||||
auto const sleDomain = ctx_.view().read(keylet::permissionedDomain(domainID));
|
||||
if (!sleDomain)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const cleanupFor = [&](AccountID const& acct) -> TER {
|
||||
if (sleDomain->getAccountID(sfOwner) == acct)
|
||||
return tesSUCCESS;
|
||||
return verifyValidDomain(ctx_.view(), acct, domainID, j_);
|
||||
};
|
||||
|
||||
auto const destination = ctx_.tx[sfDestination];
|
||||
auto const senderErr = cleanupFor(accountID_);
|
||||
auto const destinationErr = accountID_ == destination ? senderErr : cleanupFor(destination);
|
||||
|
||||
if (!isTesSuccess(senderErr))
|
||||
return senderErr;
|
||||
if (!isTesSuccess(destinationErr))
|
||||
return destinationErr;
|
||||
}
|
||||
|
||||
auto const deliverMin = ctx_.tx[~sfDeliverMin];
|
||||
|
||||
// Ripple if source or destination is non-native or if there are paths.
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -2198,6 +2198,89 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
|
||||
BEAST_EXPECT(amm.ammExists());
|
||||
}
|
||||
|
||||
void
|
||||
testClawbackBypassesReserve(FeatureBitset features)
|
||||
{
|
||||
// Same as the IOU case, but the paired asset is an MPT alice does not
|
||||
// hold yet. The reserve check is skipped on the clawback path while
|
||||
// createMPToken() still runs, so alice's MPToken is created even though
|
||||
// neither she nor the low-XRP issuer can cover the owner reserve.
|
||||
testcase("test clawback bypasses recipient reserve (MPT)");
|
||||
using namespace jtx;
|
||||
|
||||
Env env(*this, features);
|
||||
Account const gw{"gateway"}; // IOU issuer + claw authority, low XRP
|
||||
Account const gw2{"gateway2"}; // MPT issuer of the paired asset
|
||||
Account const carol{"carol"};
|
||||
Account const alice{"alice"};
|
||||
|
||||
auto const usd = gw["USD"];
|
||||
auto const baseFee = env.current()->fees().base;
|
||||
|
||||
env.fund(XRP(1'000'000), gw2, carol);
|
||||
// Low XRP so the legacy issuer-balance check cannot pass.
|
||||
env.fund(env.current()->fees().accountReserve(0, 1) + baseFee * 10, gw);
|
||||
// Reserve for the USD trustline and LP token trustline.
|
||||
env.fund(env.current()->fees().accountReserve(2, 1) + baseFee * 5, alice);
|
||||
env.close();
|
||||
|
||||
env(fset(gw, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
|
||||
// The paired MPT: transferable so an AMM can hold it, and no
|
||||
// RequireAuth so createMPToken()'s WeakAuth check passes.
|
||||
MPT const btc = MPTTester(
|
||||
{.env = env,
|
||||
.issuer = gw2,
|
||||
.holders = {carol},
|
||||
.pay = 1'000'000,
|
||||
.flags = kMptDexFlags});
|
||||
|
||||
env.trust(usd(1'000'000), carol);
|
||||
env(pay(gw, carol, usd(100'000)));
|
||||
env.close();
|
||||
AMM amm(env, carol, usd(1'000), btc(1'000), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// alice holds a USD trustline and LP tokens, but no BTC MPToken.
|
||||
env.trust(usd(100'000), alice);
|
||||
env(pay(gw, alice, usd(1'000)));
|
||||
env.close();
|
||||
amm.deposit(alice, usd(100));
|
||||
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 2);
|
||||
BEAST_EXPECT(!env.le(keylet::mptoken(btc.issuanceID, alice.id())));
|
||||
|
||||
// AMMWithdraw still enforces the reserve check.
|
||||
amm.withdrawAll(alice, std::nullopt, Ter(tecINSUFFICIENT_RESERVE));
|
||||
BEAST_EXPECT(!env.le(keylet::mptoken(btc.issuanceID, alice.id())));
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 2);
|
||||
// alice cannot afford a third owner object.
|
||||
BEAST_EXPECT(env.balance(alice) < STAmount(env.current()->fees().accountReserve(3, 1)));
|
||||
|
||||
if (features[fixCleanup3_4_0])
|
||||
{
|
||||
// Reserve check skipped; the paired BTC returns to alice on a
|
||||
// newly created MPToken.
|
||||
env(amm::ammClawback(gw, alice, usd, btc, usd(10)), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.le(keylet::mptoken(btc.issuanceID, alice.id())));
|
||||
BEAST_EXPECT(env.balance(alice, btc) > btc(0));
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 3);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Legacy path: the check runs against max(issuer, holder) XRP,
|
||||
// neither of which covers a third owner object.
|
||||
env(amm::ammClawback(gw, alice, usd, btc, usd(10)), Ter(tecINSUFFICIENT_RESERVE));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!env.le(keylet::mptoken(btc.issuanceID, alice.id())));
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 2);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
@@ -2225,6 +2308,8 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
|
||||
featureLendingProtocol);
|
||||
testLastHolderLPTokenBalance(all - fixAMMClawbackRounding);
|
||||
testClawAssetCheck(all);
|
||||
testClawbackBypassesReserve(all);
|
||||
testClawbackBypassesReserve(all - fixCleanup3_4_0);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <xrpl/ledger/helpers/AMMHelpers.h>
|
||||
#include <xrpl/protocol/AmountConversions.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
@@ -2715,6 +2716,81 @@ class AMMClawback_test : public beast::unit_test::Suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testClawbackBypassesReserve(FeatureBitset features)
|
||||
{
|
||||
// Clawback must not fail the holder-side reserve check: a holder could
|
||||
// otherwise veto it by omitting the paired trustline. AMMWithdraw still
|
||||
// enforces the check. Pre-fixCleanup3_4_0 the holder's reserve was
|
||||
// compared against max(issuer pre-fee, holder current) XRP, so the
|
||||
// clawback was blocked when neither balance covered it.
|
||||
testcase("test clawback bypasses recipient reserve");
|
||||
using namespace jtx;
|
||||
|
||||
Env env(*this, features);
|
||||
Account const gw{"gateway"};
|
||||
Account const carol{"carol"};
|
||||
Account const alice{"alice"};
|
||||
|
||||
auto const usd = gw["USD"];
|
||||
auto const eur = gw["EUR"];
|
||||
auto const baseFee = env.current()->fees().base;
|
||||
|
||||
env.fund(XRP(1'000'000), carol);
|
||||
// Low XRP so the legacy issuer-balance check cannot pass.
|
||||
env.fund(env.current()->fees().accountReserve(0, 1) + baseFee * 10, gw);
|
||||
// Reserve for the USD trustline and LP token trustline.
|
||||
env.fund(env.current()->fees().accountReserve(2, 1) + baseFee * 5, alice);
|
||||
env.close();
|
||||
|
||||
env(fset(gw, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
|
||||
env.trust(usd(1'000'000), carol);
|
||||
env.trust(eur(1'000'000), carol);
|
||||
env(pay(gw, carol, usd(100'000)));
|
||||
env(pay(gw, carol, eur(100'000)));
|
||||
env.close();
|
||||
AMM amm(env, carol, usd(1'000), eur(1'000), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// alice holds a USD trustline and LP tokens, but no EUR trustline.
|
||||
env.trust(usd(100'000), alice);
|
||||
env(pay(gw, alice, usd(1'000)));
|
||||
env.close();
|
||||
amm.deposit(alice, usd(100));
|
||||
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 2);
|
||||
// alice cannot afford a third owner object.
|
||||
BEAST_EXPECT(env.balance(alice) < STAmount(env.current()->fees().accountReserve(3, 1)));
|
||||
|
||||
// AMMWithdraw still enforces the reserve check.
|
||||
amm.withdraw(
|
||||
WithdrawArg{
|
||||
.account = alice, .asset1Out = eur(1), .err = Ter(tecINSUFFICIENT_RESERVE)});
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 2);
|
||||
|
||||
if (features[fixCleanup3_4_0])
|
||||
{
|
||||
// Reserve check skipped; the paired EUR returns to alice on a
|
||||
// newly created EUR trustline.
|
||||
env(amm::ammClawback(gw, alice, usd, eur, usd(10)), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.le(keylet::trustLine(alice.id(), eur.issue())));
|
||||
BEAST_EXPECT(env.balance(alice, eur) > eur(0));
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 3);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Legacy path: the check runs against max(issuer, holder) XRP,
|
||||
// neither of which covers a third owner object.
|
||||
env(amm::ammClawback(gw, alice, usd, eur, usd(10)), Ter(tecINSUFFICIENT_RESERVE));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 2);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testExactLPTokenEquality(FeatureBitset features)
|
||||
{
|
||||
@@ -2809,6 +2885,7 @@ class AMMClawback_test : public beast::unit_test::Suite
|
||||
testAssetFrozen(features);
|
||||
testSingleDepositAndClawback(features);
|
||||
testLastHolderLPTokenBalance(features);
|
||||
testClawbackBypassesReserve(features);
|
||||
testExactLPTokenEquality(features);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -951,6 +951,99 @@ struct EscrowToken_test : public beast::unit_test::Suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testIOUCancelReserveRecycle(FeatureBitset features)
|
||||
{
|
||||
testcase("IOU Cancel Reserve Recycle");
|
||||
using namespace jtx;
|
||||
using namespace std::literals;
|
||||
|
||||
// Escrowing the whole IOU balance lets the owner delete the now-zero
|
||||
// trust line, so cancelling has to re-create it: one object destroyed,
|
||||
// one created, and the reserve requirement unchanged.
|
||||
Env env{*this, features};
|
||||
bool const fixEnabled = env.current()->rules().enabled(fixCleanup3_4_0);
|
||||
|
||||
auto const baseFee = env.current()->fees().base;
|
||||
auto const alice = Account("alice");
|
||||
auto const bob = Account("bob");
|
||||
auto const gw = Account("gw");
|
||||
auto const usd = gw["USD"];
|
||||
|
||||
env.fund(XRP(10'000), alice, bob, gw);
|
||||
env.close();
|
||||
|
||||
env(fset(gw, asfAllowTrustLineLocking));
|
||||
env.close();
|
||||
|
||||
env.trust(usd(10'000), alice);
|
||||
env.close();
|
||||
|
||||
env(pay(gw, alice, usd(10'000)));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 1);
|
||||
|
||||
auto const cancelAfter = env.now() + 100s;
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, bob, usd(10'000)),
|
||||
escrow::kFinishTime(env.now() + 1s),
|
||||
escrow::kCancelTime(cancelAfter),
|
||||
Fee(baseFee));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 2);
|
||||
|
||||
auto const trustLineKey = keylet::trustLine(alice.id(), gw.id(), usd.currency);
|
||||
env(trust(alice, usd(0)));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.current()->exists(trustLineKey));
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 1);
|
||||
|
||||
// Leave alice holding the reserve for exactly one owned object. That
|
||||
// is the escrow now and the re-created trust line after the cancel.
|
||||
auto const oneObject = env.current()->fees().accountReserve(1, 1);
|
||||
auto const twoObjects = env.current()->fees().accountReserve(2, 1);
|
||||
auto const balance = env.balance(alice).value().xrp();
|
||||
auto const feeCushion = baseFee.drops() * 20;
|
||||
env(pay(alice, bob, drops(balance.drops() - oneObject.drops() - feeCushion)));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(alice).value().xrp() >= oneObject);
|
||||
BEAST_EXPECT(env.balance(alice).value().xrp() < twoObjects);
|
||||
|
||||
for (; env.now() < cancelAfter; env.close())
|
||||
{
|
||||
}
|
||||
env.close();
|
||||
env.close();
|
||||
|
||||
auto const expectedResult = fixEnabled ? Ter(tesSUCCESS) : Ter(tecNO_LINE_INSUF_RESERVE);
|
||||
env(escrow::cancel(alice, alice, seq), Fee(baseFee), expectedResult);
|
||||
env.close();
|
||||
|
||||
auto const escrowKey = keylet::escrow(alice.id(), SeqProxy::rawSequence(seq));
|
||||
if (fixEnabled)
|
||||
{
|
||||
BEAST_EXPECT(!env.le(escrowKey));
|
||||
BEAST_EXPECT(env.current()->exists(trustLineKey));
|
||||
BEAST_EXPECT(env.balance(alice, usd) == usd(10'000));
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
// The tec keeps the escrow, so one more owner reserve lets the
|
||||
// retry through.
|
||||
BEAST_EXPECT(env.le(escrowKey) != nullptr);
|
||||
BEAST_EXPECT(!env.current()->exists(trustLineKey));
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 1);
|
||||
|
||||
env(pay(bob, alice, drops(twoObjects.drops() - oneObject.drops())));
|
||||
env.close();
|
||||
env(escrow::cancel(alice, alice, seq), Fee(baseFee), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(escrowKey));
|
||||
BEAST_EXPECT(env.balance(alice, usd) == usd(10'000));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testIOUBalances(FeatureBitset features)
|
||||
{
|
||||
@@ -4250,6 +4343,8 @@ public:
|
||||
}
|
||||
testMPTSplitEscrowTransferFee(all - fixCleanup3_4_0);
|
||||
testMPTSplitEscrowTransferFee(all);
|
||||
testIOUCancelReserveRecycle(all - fixCleanup3_4_0);
|
||||
testIOUCancelReserveRecycle(all);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -11,14 +11,18 @@
|
||||
#include <xrpl/basics/ToString.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/core/JobQueue.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <ostream>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
@@ -115,6 +119,96 @@ class LedgerMaster_test : public beast::unit_test::Suite
|
||||
}
|
||||
}
|
||||
|
||||
// Wait until the SHAMapStore has finished processing the ledger that the
|
||||
// preceding env.close() produced.
|
||||
//
|
||||
// env.close() returns as soon as the ledger_accept RPC returns, but the
|
||||
// validated ledger path -- LedgerMaster::setValidLedger() ->
|
||||
// SHAMapStore::onLedgerClosed() -- runs on a job queue thread. Without
|
||||
// draining the job queue first, the store may not have been handed the
|
||||
// ledger at all, in which case rendezvous() observes working_ == false and
|
||||
// returns immediately, before any work has been done.
|
||||
[[nodiscard]] static bool
|
||||
syncStore(jtx::Env& env)
|
||||
{
|
||||
// Drain the job queue first, so that onLedgerClosed() has run and
|
||||
// working_ is set. Then use the store's timeout overload, so a store
|
||||
// that never finishes fails this test instead of blocking on it.
|
||||
//
|
||||
// Only the second wait is bounded: JobQueue::rendezvous() has no
|
||||
// timeout overload, so a job that never completes hangs here. That is
|
||||
// pre-existing -- ~AppBundle waits on it the same way for every jtx
|
||||
// test -- but it does mean this helper is not hang-proof end to end.
|
||||
env.app().getJobQueue().rendezvous();
|
||||
return env.app().getSHAMapStore().rendezvous(std::chrono::seconds{60});
|
||||
}
|
||||
|
||||
// Bring the SHAMapStore to the point where it has been handed a validated
|
||||
// ledger and initialized lastRotated, and report how many extra ledgers had
|
||||
// to be closed to get it there (normally none). Returns std::nullopt if
|
||||
// syncStore() itself failed.
|
||||
//
|
||||
// syncStore() alone does not guarantee that, because
|
||||
// SHAMapStoreImp::run()'s loop does not use the notification and the
|
||||
// working_ flag safely:
|
||||
//
|
||||
// * onLedgerClosed() notifies cond_ whether or not run()'s thread is
|
||||
// parked on it, and run() waits on cond_ without a predicate, so a
|
||||
// notification that lands while the thread is still starting up --
|
||||
// before it first reaches that wait -- is lost.
|
||||
// * run() clears working_ at the top of its loop without checking
|
||||
// whether newLedger_ is still set, so rendezvous() can report the
|
||||
// store idle with a validated ledger queued.
|
||||
//
|
||||
// Either way the store ends up parked with work pending, and only another
|
||||
// notification gets it moving again. In a standalone test nothing else
|
||||
// closes ledgers, so that has to come from here: this closes a ledger
|
||||
// rather than polling getLastRotated(), because polling would just time
|
||||
// out. onLedgerClosed() keeps only the most recent ledger in newLedger_,
|
||||
// so the ledger the store picks up -- and therefore lastRotated -- is a
|
||||
// timing detail, which is why the caller derives its expectations from the
|
||||
// value it observes instead of assuming one.
|
||||
//
|
||||
// run() is deliberately left as it is. In production the only effect is
|
||||
// latency: the trigger is validatedSeq >= lastRotated + deleteInterval, so
|
||||
// a lost notification delays rotation to the next validated ledger and
|
||||
// nothing is skipped or accumulated -- starting at 513 instead of 512 does
|
||||
// not matter. Two consequences do follow from leaving it in place, and both
|
||||
// hold today: nothing in production decides anything from working_ or
|
||||
// rendezvous() (rendezvous() has no production callers at all), and a node
|
||||
// whose ledgers only advance on demand -- standalone, driven by
|
||||
// ledger_accept -- can sit on a queued ledger until something closes the
|
||||
// next one, which is exactly the situation this helper is working around.
|
||||
//
|
||||
// So this helper is permanent rather than a stopgap. Working around the
|
||||
// race must not make it invisible, so every extra close is logged. That
|
||||
// keeps how often it is actually hit observable in the unit test output --
|
||||
// which is the only signal left once this testcase stops flaking on it.
|
||||
[[nodiscard]] std::optional<int>
|
||||
initializeStore(jtx::Env& env, int const maxExtraCloses = 3)
|
||||
{
|
||||
auto& store = env.app().getSHAMapStore();
|
||||
|
||||
for (int extraCloses = 0;; ++extraCloses)
|
||||
{
|
||||
if (!syncStore(env))
|
||||
return std::nullopt;
|
||||
if (store.getLastRotated() != 0 || extraCloses == maxExtraCloses)
|
||||
{
|
||||
if (extraCloses != 0)
|
||||
{
|
||||
log << "initializeStore: the store needed " << extraCloses
|
||||
<< " extra ledger close(s) to pick up a validated ledger. "
|
||||
"SHAMapStoreImp::run() dropped the notification for the "
|
||||
"first one; see the comment on initializeStore()."
|
||||
<< std::endl;
|
||||
}
|
||||
return extraCloses;
|
||||
}
|
||||
env.close();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testCompleteLedgerRange(FeatureBitset features)
|
||||
{
|
||||
@@ -136,15 +230,42 @@ class LedgerMaster_test : public beast::unit_test::Suite
|
||||
|
||||
auto& lm = env.app().getLedgerMaster();
|
||||
LedgerIndex minSeq = 2;
|
||||
LedgerIndex maxSeq = env.closed()->header().seq;
|
||||
auto& store = env.app().getSHAMapStore();
|
||||
BEAST_EXPECT(store.rendezvous());
|
||||
// Which of the existing complete ledgers the store initializes
|
||||
// lastRotated from is a timing detail; all this test needs is that it is
|
||||
// one of them. Everything below derives from the observed value rather
|
||||
// than assuming a particular one.
|
||||
//
|
||||
// The range check and the initializeStore() one both end the testcase
|
||||
// rather than merely reporting, because lastRotated is the only value
|
||||
// from the store that enters minSeq. A lastRotated of 0 -- the value
|
||||
// getLastRotated() reports until the store has been handed a
|
||||
// validated ledger -- makes minSeq 0 below, and the minSeq - 1 and
|
||||
// minSeq - 2 ranges then underflow to first > last, which aborts a
|
||||
// Debug build inside missingFromCompleteLedgerRange().
|
||||
auto const extraCloses = initializeStore(env);
|
||||
if (!BEAST_EXPECT(extraCloses.has_value()))
|
||||
return;
|
||||
LedgerIndex maxSeq = env.closed()->header().seq;
|
||||
LedgerIndex lastRotated = store.getLastRotated();
|
||||
BEAST_EXPECTS(maxSeq == 3, to_string(maxSeq));
|
||||
BEAST_EXPECTS(lm.getCompleteLedgers() == "2-3", lm.getCompleteLedgers());
|
||||
BEAST_EXPECTS(lastRotated == 3, to_string(lastRotated));
|
||||
if (!BEAST_EXPECTS(lastRotated >= minSeq && lastRotated <= maxSeq, to_string(lastRotated)))
|
||||
return;
|
||||
// The BEAST_EXPECT above already returned if this is nullopt, but that
|
||||
// is invisible to clang-tidy's optional model.
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
BEAST_EXPECTS(maxSeq == 3 + *extraCloses, to_string(maxSeq));
|
||||
std::stringstream initialRange;
|
||||
initialRange << minSeq << "-" << maxSeq;
|
||||
BEAST_EXPECTS(lm.getCompleteLedgers() == initialRange.str(), lm.getCompleteLedgers());
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq, maxSeq) == 0);
|
||||
BEAST_EXPECT(minSeq + 1 > maxSeq - 1);
|
||||
// The inner range is empty unless initializeStore() had to close extra
|
||||
// ledgers, and missingFromCompleteLedgerRange() treats first > last as a
|
||||
// precondition violation that aborts a Debug build via UNREACHABLE, so
|
||||
// only check it when it is well formed.
|
||||
if (minSeq + 1 <= maxSeq - 1)
|
||||
{
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 1, maxSeq - 1) == 0);
|
||||
}
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 1, maxSeq + 1) == 2);
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 2, maxSeq - 2) == 2);
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 2, maxSeq + 2) == 2);
|
||||
@@ -157,7 +278,7 @@ class LedgerMaster_test : public beast::unit_test::Suite
|
||||
env(noop(alice));
|
||||
}
|
||||
env.close();
|
||||
BEAST_EXPECT(store.rendezvous());
|
||||
BEAST_EXPECT(syncStore(env));
|
||||
|
||||
++maxSeq;
|
||||
|
||||
@@ -173,7 +294,19 @@ class LedgerMaster_test : public beast::unit_test::Suite
|
||||
expectedRange << minSeq << "-" << maxSeq;
|
||||
BEAST_EXPECTS(lm.getCompleteLedgers() == expectedRange.str(), lm.getCompleteLedgers());
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq, maxSeq) == 0);
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 1, maxSeq - 1) == 0);
|
||||
// missingFromCompleteLedgerRange() treats first > last as a
|
||||
// precondition violation and aborts a Debug build via UNREACHABLE.
|
||||
// The range can only collapse if this test's model of minSeq /
|
||||
// maxSeq has desynced from the store, so report that as a failure
|
||||
// instead of taking down the whole unit test job.
|
||||
if (minSeq + 1 <= maxSeq - 1)
|
||||
{
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 1, maxSeq - 1) == 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECTS(false, to_string(minSeq) + "-" + to_string(maxSeq));
|
||||
}
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 1, maxSeq + 1) == 2);
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 2, maxSeq - 2) == 2);
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 2, maxSeq + 2) == 2);
|
||||
|
||||
+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();
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <test/jtx/trust.h>
|
||||
#include <test/jtx/txflags.h>
|
||||
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
@@ -179,7 +180,10 @@ class PermissionedDEX_test : public beast::unit_test::Suite
|
||||
void
|
||||
testOfferCreate(FeatureBitset features)
|
||||
{
|
||||
testcase("OfferCreate");
|
||||
bool const fixEnabled = features[fixCleanup3_4_0];
|
||||
|
||||
testcase << "OfferCreate"
|
||||
<< (fixEnabled ? " (Cleanup3_4_0 enabled)" : " (Cleanup3_4_0 disabled)");
|
||||
|
||||
// test preflight
|
||||
{
|
||||
@@ -273,8 +277,10 @@ class PermissionedDEX_test : public beast::unit_test::Suite
|
||||
// time advance
|
||||
env.close(std::chrono::seconds(20));
|
||||
|
||||
// devin cannot create offer with expired cred
|
||||
env(offer(devin, XRP(10), USD(10)), Domain(domainID), Ter(tecNO_PERMISSION));
|
||||
// Devin cannot create offer with expired cred. After fixCleanup3_4_0,
|
||||
// doApply deletes the expired credential SLE and returns tecEXPIRED.
|
||||
TER const expectedExpiredCredTer = fixEnabled ? tecEXPIRED : tecNO_PERMISSION;
|
||||
env(offer(devin, XRP(10), USD(10)), Domain(domainID), Ter(expectedExpiredCredTer));
|
||||
env.close();
|
||||
}
|
||||
|
||||
@@ -1510,7 +1516,9 @@ class PermissionedDEX_test : public beast::unit_test::Suite
|
||||
env.close(std::chrono::seconds(100));
|
||||
|
||||
// Confirm devin can no longer create domain offers.
|
||||
env(offer(devin, XRP(1), USD(1)), Domain(domainID), Ter(tecNO_PERMISSION));
|
||||
// After fixCleanup3_4_0, OfferCreate deletes the expired credential and
|
||||
// returns tecEXPIRED (covered in depth by testExpiredCredentialCleanup).
|
||||
env(offer(devin, XRP(1), USD(1)), Domain(domainID), Ter(tecEXPIRED));
|
||||
env.close();
|
||||
|
||||
// The hybrid offer must still exist in the open book after expiry.
|
||||
@@ -1635,6 +1643,202 @@ class PermissionedDEX_test : public beast::unit_test::Suite
|
||||
BEAST_EXPECT(!offerExists(env, bob, carolOfferSeq));
|
||||
}
|
||||
|
||||
void
|
||||
testExpiredCredentialCleanup(FeatureBitset features)
|
||||
{
|
||||
bool const fixEnabled = features[fixCleanup3_4_0];
|
||||
|
||||
testcase << "Expired credential cleanup"
|
||||
<< (fixEnabled ? " (Cleanup3_4_0 enabled)" : " (Cleanup3_4_0 disabled)");
|
||||
|
||||
TER const expectedExpiredCredTer = fixEnabled ? tecEXPIRED : tecNO_PERMISSION;
|
||||
|
||||
auto const fundAccount =
|
||||
[](Env& env, Account const& account, Account const& gw, IOU const& usd) {
|
||||
env.fund(XRP(1000), account);
|
||||
env.close();
|
||||
env.trust(usd(1000), account);
|
||||
env.close();
|
||||
env(pay(gw, account, usd(100)));
|
||||
env.close();
|
||||
};
|
||||
|
||||
auto const fundDevin = [&](Env& env, Account const& gw, IOU const& usd) {
|
||||
Account const devin("devin");
|
||||
fundAccount(env, devin, gw, usd);
|
||||
return devin;
|
||||
};
|
||||
|
||||
auto const createExpiringCredential = [](Env& env,
|
||||
Account const& subject,
|
||||
Account const& issuer,
|
||||
std::string const& credType) {
|
||||
auto jv = credentials::create(subject, issuer, credType);
|
||||
uint32_t const t = env.current()->header().parentCloseTime.time_since_epoch().count();
|
||||
jv[sfExpiration.jsonName] = t + 20;
|
||||
env(jv);
|
||||
env(credentials::accept(subject, issuer, credType));
|
||||
env.close();
|
||||
|
||||
return keylet::credential(subject.id(), issuer.id(), makeSlice(credType));
|
||||
};
|
||||
|
||||
auto const expectExpiredCredentialState = [&](Env const& env, Keylet const& credKey) {
|
||||
if (fixEnabled)
|
||||
{
|
||||
BEAST_EXPECT(!env.le(credKey));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(env.le(credKey));
|
||||
}
|
||||
};
|
||||
|
||||
// A payment referencing a non-existent domain is rejected in preclaim.
|
||||
{
|
||||
Env env(*this, features);
|
||||
auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
|
||||
PermissionedDEX(env);
|
||||
|
||||
uint256 const badDomain{
|
||||
"F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E3370F3649CE134"
|
||||
"E5"};
|
||||
|
||||
env(offer(bob, XRP(10), USD(10)), Domain(domainID));
|
||||
env.close();
|
||||
|
||||
env(pay(alice, bob, USD(10)),
|
||||
Path(~USD),
|
||||
Sendmax(XRP(10)),
|
||||
Domain(badDomain),
|
||||
Ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// OfferCreate with an expired credential.
|
||||
{
|
||||
Env env(*this, features);
|
||||
auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
|
||||
PermissionedDEX(env);
|
||||
|
||||
Account const devin = fundDevin(env, gw, USD);
|
||||
auto const credKey = createExpiringCredential(env, devin, domainOwner, credType);
|
||||
BEAST_EXPECT(env.le(credKey)); // credential exists before expiry
|
||||
|
||||
env.close(std::chrono::seconds(20));
|
||||
|
||||
env(offer(devin, XRP(10), USD(10)), Domain(domainID), Ter(expectedExpiredCredTer));
|
||||
env.close();
|
||||
|
||||
expectExpiredCredentialState(env, credKey);
|
||||
}
|
||||
|
||||
// Payment where the sender's credential is expired.
|
||||
{
|
||||
Env env(*this, features);
|
||||
auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
|
||||
PermissionedDEX(env);
|
||||
|
||||
Account const devin = fundDevin(env, gw, USD);
|
||||
auto const credKey = createExpiringCredential(env, devin, domainOwner, credType);
|
||||
|
||||
auto const bobOfferSeq{env.seq(bob)};
|
||||
auto const bobCredKey =
|
||||
keylet::credential(bob.id(), domainOwner.id(), makeSlice(credType));
|
||||
env(offer(bob, XRP(10), USD(10)), Domain(domainID));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.le(credKey));
|
||||
BEAST_EXPECT(env.le(bobCredKey));
|
||||
BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
|
||||
|
||||
env.close(std::chrono::seconds(20));
|
||||
|
||||
env(pay(devin, alice, USD(10)),
|
||||
Path(~USD),
|
||||
Sendmax(XRP(10)),
|
||||
Domain(domainID),
|
||||
Ter(expectedExpiredCredTer));
|
||||
env.close();
|
||||
|
||||
expectExpiredCredentialState(env, credKey);
|
||||
BEAST_EXPECT(env.le(bobCredKey));
|
||||
BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
|
||||
}
|
||||
|
||||
// Payment where the destination's credential is expired.
|
||||
{
|
||||
Env env(*this, features);
|
||||
auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
|
||||
PermissionedDEX(env);
|
||||
|
||||
Account const devin = fundDevin(env, gw, USD);
|
||||
auto const credKey = createExpiringCredential(env, devin, domainOwner, credType);
|
||||
|
||||
auto const bobOfferSeq{env.seq(bob)};
|
||||
auto const bobCredKey =
|
||||
keylet::credential(bob.id(), domainOwner.id(), makeSlice(credType));
|
||||
env(offer(bob, XRP(10), USD(10)), Domain(domainID));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.le(credKey));
|
||||
BEAST_EXPECT(env.le(bobCredKey));
|
||||
BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
|
||||
|
||||
env.close(std::chrono::seconds(20));
|
||||
|
||||
env(pay(alice, devin, USD(10)),
|
||||
Path(~USD),
|
||||
Sendmax(XRP(10)),
|
||||
Domain(domainID),
|
||||
Ter(expectedExpiredCredTer));
|
||||
env.close();
|
||||
|
||||
expectExpiredCredentialState(env, credKey);
|
||||
BEAST_EXPECT(env.le(bobCredKey));
|
||||
BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
|
||||
}
|
||||
|
||||
// Payment where both sender and destination credentials are expired.
|
||||
{
|
||||
Env env(*this, features);
|
||||
auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
|
||||
PermissionedDEX(env);
|
||||
|
||||
Account const devin = fundDevin(env, gw, USD);
|
||||
Account const erin("erin");
|
||||
fundAccount(env, erin, gw, USD);
|
||||
|
||||
auto const devinCredKey = createExpiringCredential(env, devin, domainOwner, credType);
|
||||
auto const erinCredKey = createExpiringCredential(env, erin, domainOwner, credType);
|
||||
|
||||
auto const bobOfferSeq{env.seq(bob)};
|
||||
auto const bobCredKey =
|
||||
keylet::credential(bob.id(), domainOwner.id(), makeSlice(credType));
|
||||
env(offer(bob, XRP(10), USD(10)), Domain(domainID));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.le(devinCredKey));
|
||||
BEAST_EXPECT(env.le(erinCredKey));
|
||||
BEAST_EXPECT(env.le(bobCredKey));
|
||||
BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
|
||||
|
||||
env.close(std::chrono::seconds(20));
|
||||
|
||||
env(pay(devin, erin, USD(10)),
|
||||
Path(~USD),
|
||||
Sendmax(XRP(10)),
|
||||
Domain(domainID),
|
||||
Ter(expectedExpiredCredTer));
|
||||
env.close();
|
||||
|
||||
expectExpiredCredentialState(env, devinCredKey);
|
||||
expectExpiredCredentialState(env, erinCredKey);
|
||||
BEAST_EXPECT(env.le(bobCredKey));
|
||||
BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testHybridMalformedOffer(FeatureBitset features)
|
||||
{
|
||||
@@ -2209,6 +2413,7 @@ public:
|
||||
// Test domain offer (w/o hybrid)
|
||||
testOfferCreate(all);
|
||||
testOfferCreate(all - fixCleanup3_2_0);
|
||||
testOfferCreate(all - fixCleanup3_4_0);
|
||||
testPayment(all);
|
||||
testPayment(all - fixCleanup3_2_0);
|
||||
testBookStep(all);
|
||||
@@ -2219,6 +2424,8 @@ public:
|
||||
testAmmQualityNotLeaked(all);
|
||||
testAmmQualityNotLeaked(all - fixCleanup3_3_0);
|
||||
testAutoBridge(all);
|
||||
testExpiredCredentialCleanup(all);
|
||||
testExpiredCredentialCleanup(all - fixCleanup3_4_0);
|
||||
|
||||
// Test hybrid offers
|
||||
testHybridOfferCreate(all);
|
||||
|
||||
+1053
-32
File diff suppressed because it is too large.
Load diff
@@ -376,11 +376,11 @@ public:
|
||||
}
|
||||
|
||||
void
|
||||
testSingleSigning()
|
||||
testSingleSigning(FeatureBitset features)
|
||||
{
|
||||
testcase("Single signing");
|
||||
using namespace test::jtx;
|
||||
Env env{*this, testableAmendments()};
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const sponsor("sponsor");
|
||||
Account const invalid("invalid");
|
||||
@@ -415,11 +415,11 @@ public:
|
||||
}
|
||||
|
||||
void
|
||||
testMultiSigning()
|
||||
testMultiSigning(FeatureBitset features)
|
||||
{
|
||||
testcase("Multi signing");
|
||||
using namespace test::jtx;
|
||||
Env env{*this, testableAmendments()};
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const sponsor("sponsor");
|
||||
@@ -5467,13 +5467,12 @@ public:
|
||||
using namespace test::jtx;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
// Finishing a self-escrow (source == destination) whose trust line
|
||||
// was deleted while the escrow was outstanding auto-creates the line,
|
||||
// and the outcome of that reserve check depends on whether the escrow
|
||||
// reserve is released before delivery (Sponsor) or after (legacy).
|
||||
// With the source's balance in the one-increment window
|
||||
// [reserve(1), reserve(2)), the legacy order requires reserve(2) and
|
||||
// fails, while the Sponsor order requires reserve(1) and succeeds.
|
||||
// Finishing a self-escrow (source == destination) whose trust line was
|
||||
// deleted while the escrow was outstanding auto-creates the line. With
|
||||
// the source's balance in the one-increment window
|
||||
// [reserve(1), reserve(2)), the finish succeeds only when the escrow
|
||||
// reserve is released before delivery, which either featureSponsor or
|
||||
// fixCleanup3_4_0 does.
|
||||
auto runTest = [&](FeatureBitset features, TER expected) {
|
||||
Account const alice("alice");
|
||||
Account const gw("gw");
|
||||
@@ -5538,11 +5537,9 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
// Pre-amendment: legacy order — the escrow still counts against the
|
||||
// reserve while the auto-created line is checked.
|
||||
runTest(testableAmendments() - featureSponsor, tecNO_LINE_INSUF_RESERVE);
|
||||
|
||||
// Post-amendment: the escrow reserve is recycled into the new line.
|
||||
runTest(testableAmendments() - featureSponsor - fixCleanup3_4_0, tecNO_LINE_INSUF_RESERVE);
|
||||
runTest(testableAmendments() - featureSponsor, tesSUCCESS);
|
||||
runTest(testableAmendments() - fixCleanup3_4_0, tesSUCCESS);
|
||||
runTest(testableAmendments(), tesSUCCESS);
|
||||
}
|
||||
|
||||
@@ -5678,8 +5675,12 @@ protected:
|
||||
testInvalidSponsorshipSet();
|
||||
testPseudoAccountSponsorship();
|
||||
|
||||
testSingleSigning();
|
||||
testMultiSigning();
|
||||
// The signing prefix of an alternate signature field changes with
|
||||
// fixCleanup3_4_0, so sign and verify under both rule sets.
|
||||
testSingleSigning(jtx::testableAmendments());
|
||||
testSingleSigning(jtx::testableAmendments() - fixCleanup3_4_0);
|
||||
testMultiSigning(jtx::testableAmendments());
|
||||
testMultiSigning(jtx::testableAmendments() - fixCleanup3_4_0);
|
||||
|
||||
testInvalidSponsorField();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -205,8 +205,14 @@ private:
|
||||
if (!BEAST_EXPECT(!createJson.isMember(jss::Signers)))
|
||||
counterpartyJson[sfSigners] = createJson[sfSigners];
|
||||
|
||||
// The duplicated signature works
|
||||
createJson = env.json(createJson, Json(sfCounterpartySignature, counterpartyJson));
|
||||
// The duplicated signature does not work: the counterparty signs a
|
||||
// different prefix than the account.
|
||||
env(env.json(createJson, Json(sfCounterpartySignature, counterpartyJson)),
|
||||
Ter(telENV_RPC_FAILED));
|
||||
|
||||
// Signing the counterparty field itself works, even though the lender
|
||||
// is both the borrower and the counterparty.
|
||||
createJson = env.json(createJson, Sig(sfCounterpartySignature, lender));
|
||||
env(createJson);
|
||||
|
||||
env.close();
|
||||
|
||||
@@ -113,21 +113,26 @@ private:
|
||||
txJson[sfTransactionType] = "AccountSet";
|
||||
txJson[sfAccount] = borrower.human();
|
||||
|
||||
auto const borrowerSignParams = [&]() {
|
||||
json::Value params{json::ValueType::Object};
|
||||
params[jss::passphrase] = borrowerPass;
|
||||
params[jss::key_type] = "ed25519";
|
||||
params[jss::signature_target] = "Destination";
|
||||
params[jss::tx_json] = txJson;
|
||||
return params;
|
||||
}();
|
||||
auto const jSignBorrower = env.rpc("json", "sign", to_string(borrowerSignParams));
|
||||
BEAST_EXPECT(
|
||||
jSignBorrower.isMember(jss::result) &&
|
||||
jSignBorrower[jss::result].isMember(jss::error) &&
|
||||
jSignBorrower[jss::result][jss::error] == "invalidParams" &&
|
||||
jSignBorrower[jss::result].isMember(jss::error_message) &&
|
||||
jSignBorrower[jss::result][jss::error_message] == "Destination");
|
||||
// "Destination" is not an inner object at all. "Book" is one, but
|
||||
// it holds no transaction signature, so it is not a target either.
|
||||
for (char const* target : {"Destination", "Book", "Signer"})
|
||||
{
|
||||
auto const borrowerSignParams = [&]() {
|
||||
json::Value params{json::ValueType::Object};
|
||||
params[jss::passphrase] = borrowerPass;
|
||||
params[jss::key_type] = "ed25519";
|
||||
params[jss::signature_target] = target;
|
||||
params[jss::tx_json] = txJson;
|
||||
return params;
|
||||
}();
|
||||
auto const jSignBorrower = env.rpc("json", "sign", to_string(borrowerSignParams));
|
||||
BEAST_EXPECT(
|
||||
jSignBorrower.isMember(jss::result) &&
|
||||
jSignBorrower[jss::result].isMember(jss::error) &&
|
||||
jSignBorrower[jss::result][jss::error] == "invalidParams" &&
|
||||
jSignBorrower[jss::result].isMember(jss::error_message) &&
|
||||
jSignBorrower[jss::result][jss::error_message] == target);
|
||||
}
|
||||
}
|
||||
{
|
||||
testcase("RPC LoanSet - sign and submit borrower initiated");
|
||||
|
||||
@@ -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().
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/fee.h>
|
||||
#include <test/jtx/noop.h>
|
||||
#include <test/jtx/sponsor.h>
|
||||
#include <test/jtx/ter.h>
|
||||
#include <test/jtx/txflags.h>
|
||||
#include <test/jtx/vault.h>
|
||||
@@ -1090,9 +1091,74 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
// Every signature on a transaction covered the same bytes before
|
||||
// fixCleanup3_4_0, so a signature could be moved from the role that made
|
||||
// it into another role. Here the lender signs a LoanSet as the
|
||||
// counterparty, and the borrower copies that signature into the
|
||||
// SponsorSignature, making the lender pay the fee without the lender ever
|
||||
// agreeing to sponsor it.
|
||||
void
|
||||
testSignatureCopiedBetweenRoles(bool fixEnabled)
|
||||
{
|
||||
testcase(
|
||||
std::string("Counterparty signature copied into the sponsor slot") +
|
||||
(fixEnabled ? "" : " (pre-amendment)"));
|
||||
|
||||
using namespace jtx;
|
||||
using namespace loan;
|
||||
|
||||
Env env(*this, fixEnabled ? all_ : all_ - fixCleanup3_4_0);
|
||||
BEAST_EXPECT(env.enabled(fixCleanup3_4_0) == fixEnabled);
|
||||
|
||||
Account const lender{"lender"};
|
||||
Account const borrower{"borrower"};
|
||||
|
||||
env.fund(XRP(100'000'000), lender, borrower);
|
||||
env.close();
|
||||
|
||||
PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
|
||||
auto const broker = createVaultAndBroker(env, xrpAsset, lender);
|
||||
|
||||
auto const feeAmt = XRP(1);
|
||||
|
||||
// The lender agrees to the loan by signing the Counterparty slot of a
|
||||
// LoanSet that names the lender as the fee sponsor. The lender signs
|
||||
// nothing else.
|
||||
auto loanSet = env.json(
|
||||
set(borrower, broker.brokerID, broker.asset(Number{1, 3}).value()),
|
||||
sponsor::As(lender, spfSponsorFee),
|
||||
Sig(sfCounterpartySignature, lender),
|
||||
Fee(feeAmt));
|
||||
|
||||
// The borrower copies the lender's signature into the sponsor slot.
|
||||
loanSet[sfSponsorSignature.jsonName] = loanSet[sfCounterpartySignature.jsonName];
|
||||
|
||||
auto const lenderBalance = env.balance(lender);
|
||||
auto const borrowerBalance = env.balance(borrower);
|
||||
|
||||
env(loanSet, Ter(fixEnabled ? TER{telENV_RPC_FAILED} : TER{tesSUCCESS}));
|
||||
env.close();
|
||||
|
||||
if (fixEnabled)
|
||||
{
|
||||
// The copied signature does not verify in the sponsor slot, so
|
||||
// nothing happens at all.
|
||||
BEAST_EXPECT(env.balance(lender) == lenderBalance);
|
||||
BEAST_EXPECT(env.balance(borrower) == borrowerBalance);
|
||||
}
|
||||
else
|
||||
{
|
||||
// The lender paid the fee, and the borrower got the loan.
|
||||
BEAST_EXPECT(env.balance(lender) == lenderBalance - feeAmt);
|
||||
BEAST_EXPECT(env.balance(borrower).value() > borrowerBalance.value());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
runAmendmentIndependent()
|
||||
{
|
||||
testSignatureCopiedBetweenRoles(true);
|
||||
testSignatureCopiedBetweenRoles(false);
|
||||
testRIPD3901();
|
||||
testImpairmentPaymentDateUnchanged();
|
||||
testImpairmentPaymentDatePreAmendment();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <test/jtx/envconfig.h>
|
||||
#include <test/jtx/fee.h>
|
||||
#include <test/jtx/flags.h>
|
||||
#include <test/jtx/jtx_json.h>
|
||||
#include <test/jtx/mpt.h>
|
||||
#include <test/jtx/pay.h>
|
||||
#include <test/jtx/sponsor.h>
|
||||
@@ -522,15 +521,13 @@ private:
|
||||
auto const loanSetFee = Fee(env.current()->fees().base * 2);
|
||||
Number const principalRequest{1, 3};
|
||||
|
||||
auto createJson = env.json(set(lender, broker.brokerID, principalRequest), Fee(loanSetFee));
|
||||
|
||||
json::Value counterpartyJson{json::ValueType::Object};
|
||||
counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature];
|
||||
counterpartyJson[sfSigningPubKey] = createJson[sfSigningPubKey];
|
||||
if (!BEAST_EXPECT(!createJson.isMember(jss::Signers)))
|
||||
counterpartyJson[sfSigners] = createJson[sfSigners];
|
||||
|
||||
createJson = env.json(createJson, Json(sfCounterpartySignature, counterpartyJson));
|
||||
// The lender is both the borrower and the counterparty here, but the
|
||||
// two roles sign different bytes, so each signature must be made for
|
||||
// the field it goes into.
|
||||
auto const createJson = env.json(
|
||||
set(lender, broker.brokerID, principalRequest),
|
||||
Sig(sfCounterpartySignature, lender),
|
||||
Fee(loanSetFee));
|
||||
env(createJson);
|
||||
|
||||
env.close();
|
||||
|
||||
Loaded 100 of 124 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user