Compare commits

..
Author SHA1 Message Date
Vito ae9cb05521 benchmarks 2026-10-01 12:33:37 +02:00
226 changed files with 6645 additions and 6512 deletions

No files matched your search

+4 -1
View File
@@ -23,6 +23,8 @@ ignoreRegExpList:
- /\b(XRPL|BEAST)_[A-Z_0-9]+_H+/g # include guards
- /::[a-z:_]+/g # things from other namespaces
- /\blib[a-z]+/g # libraries
- /\b(mpd|MPD)_[A-Za-z_0-9]+/g # libmpdec (mpdecimal) API
- /\bbid(64|128)_[a-z_0-9]+/g # Intel decimal library API
- /\b[0-9]{4}-[0-9]{2}-[0-9]{2}[,:][A-Za-zÀ-ÖØ-öø-ÿ.\s]+/g # copyright dates
- /\b[0-9]{4}[,:]?\s*[A-Za-zÀ-ÖØ-öø-ÿ.\s]+/g # copyright years
- /\[[A-Za-z0-9-]+\]\(https:\/\/github.com\/[A-Za-z0-9-]+\)/g # Github usernames
@@ -44,6 +46,7 @@ suggestWords:
- synch->sync
words:
- abempty
- allcr
- AMMID
- AMMMPT
- AMMMPToken
@@ -184,6 +187,7 @@ words:
- Merkle
- misprediction
- missingok
- mpdecimal
- mptbalance
- MPTDEX
- mptflags
@@ -255,7 +259,6 @@ words:
- queuable
- Raphson
- rcflags
- reencrypted
- replayer
- repodata
- repomd
-4
View File
@@ -8,7 +8,3 @@ watch_file rust-toolchain.toml
watch_dir conan
use flake
# Optional, untracked local overrides. To use a different shell, put e.g.
# `use flake .#formal-verification` in .envrc.local.
source_env_if_exists .envrc.local
-12
View File
@@ -5,18 +5,6 @@
# This file is sorted in reverse chronological order, with the most recent commits at the top.
# The commits listed here are ignored by git blame, which is useful for formatting-only commits that would otherwise obscure the history of changes to a file.
# chore: CamelCase for typedef/using in `clang-tidy` (#8177)
97a1824537d20d82d25bf151a37a7a4532ebf085
# chore: Rename CamelCase namespaces to snake_case (#7933)
06488c1318d96f56d0536251bee08ac85fa7fdd3
# style: Unify style for all Doxygen comments (#7776)
73b6852a122854140336e6e6bc30a3a4b41aa5fd
# style: More clang-tidy identifier renaming (#7290)
a830ab10efed8d3e59ef2fc15d66efdf9c6bb0d8
# refactor: Rename static constants (#7120)
5b6e8b6f93b19c1e3f6a3467a25639031d9d9a53
# chore: More fixes for bad renames (#7092)
7afdd71a54d562b32a50b29a5aa00bb997dc9053
# refactor: Enable clang-tidy `readability-identifier-naming` check (#6571)
8995564ed6b9e453e144bb663303072a3c1ba305
# refactor: Enable remaining clang-tidy `cppcoreguidelines` checks (#6538)
+11 -6
View File
@@ -15,25 +15,30 @@ outputs:
runs:
using: composite
steps:
# A tag names its own version. Anything else is a development build named by
# its commit hash, matching what cmake/XrplVersion.cmake derives: the head of
# a pull request rather than the merge commit GitHub creates for it.
# A tag names its own version. Anything else takes it from BuildInfo.cpp and
# appends the commit hash as build metadata, joined with a plus sign because a
# Conan version cannot contain two hyphens.
- name: Determine version
id: version
shell: bash
env:
IS_TAG: ${{ startsWith(github.ref, 'refs/tags/') }}
REF_NAME: ${{ github.ref_name }}
SHA: ${{ github.event.pull_request.head.sha || github.sha }}
SHA: ${{ github.sha }}
run: |
if [[ "${IS_TAG}" == "true" ]]; then
version="${REF_NAME}"
else
version="0.0.0-dev+${SHA:0:7}"
version="$(awk -F'"' '/versionString =/ { print $2 }' src/libxrpl/protocol/BuildInfo.cpp)"
if [[ -z "${version}" ]]; then
echo "Unable to read versionString from BuildInfo.cpp." >&2
exit 1
fi
version="${version}+${SHA:0:7}"
fi
echo "version=${version}" | tee -a "${GITHUB_OUTPUT}"
- name: Determine release channel and package release
id: release_info
uses: XRPLF/actions/release-info@a9f2eeca6fb3980ba3a84cf68566f1c69ad30674
uses: XRPLF/actions/release-info@ebcf6cea14eee258697308a51fea55cad777581b
@@ -1,40 +0,0 @@
#!/usr/bin/env bash
# Exit the script as soon as an error occurs.
set -euo pipefail
# This script fails if <head> merges a release back into <base>,
# but also adds commits that aren't on a release or staging branch, see RELEASING.md.
# Merge commits are allowed.
# Usage: .github/scripts/releasing/check-merge-back-commits.sh <base> <head>
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <base> <head>"
exit 1
fi
BASE=$1
HEAD=$2
SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}")
# shellcheck source=.github/scripts/releasing/common.sh
source "${SCRIPT_DIR}/common.sh"
load_release_branches
# A PR is a merge-back if some of its commits are on a release or staging branch.
PR_COUNT=$(git rev-list --no-merges --count "${BASE}..${HEAD}")
NEW_COUNT=$(git rev-list --no-merges --count "${BASE}..${HEAD}" --not "${BRANCHES[@]}")
if ((NEW_COUNT == PR_COUNT)); then
echo "This PR doesn't merge a release back."
exit 0
fi
if ((NEW_COUNT == 0)); then
echo "This merge-back adds no commits of its own."
exit 0
fi
echo "This PR merges a release back, but also adds commits that aren't on a release or staging branch:"
git log --no-merges --format=' %h %s' "${BASE}..${HEAD}" --not "${BRANCHES[@]}"
echo
echo "Make these changes in a separate PR, see RELEASING.md."
exit 1
@@ -1,46 +0,0 @@
#!/usr/bin/env bash
# Exit the script as soon as an error occurs.
set -euo pipefail
# This script fails if the commits in <base>..<head> copy commits from a release or staging branch
# (e.g. by rebasing or cherry-picking them) instead of merging that branch, see RELEASING.md.
# Commits are compared by patch-id,
# and only against release and staging commits that <head> does not already contain.
# Usage: .github/scripts/releasing/check-no-copied-release-commits.sh <base> <head>
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <base> <head>"
exit 1
fi
BASE=$1
HEAD=$2
patch_ids() {
git log --no-merges --patch --no-color --no-ext-diff "$@" | git patch-id --stable | sort
}
SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}")
# shellcheck source=.github/scripts/releasing/common.sh
source "${SCRIPT_DIR}/common.sh"
load_release_branches
# Each line is "<patch-id> <commit>".
RELEASE_PATCHES=$(patch_ids "${BRANCHES[@]}" --not "${HEAD}")
PR_PATCHES=$(patch_ids "${BASE}..${HEAD}")
# Each line is "<patch-id> <release commit> <PR commit>".
COPIES=$(join <(echo "${RELEASE_PATCHES}") <(echo "${PR_PATCHES}"))
if [ -z "${COPIES}" ]; then
echo "No copied release commits found."
exit 0
fi
echo "These commits copy release commits instead of merging them:"
while read -r _ RELEASE_COMMIT PR_COMMIT; do
echo " $(git log -1 --format='%h %s' "${PR_COMMIT}") (copies ${RELEASE_COMMIT:0:10})"
done <<<"${COPIES}"
echo
echo "Merge the release tag (or branch) instead, see RELEASING.md."
exit 1
@@ -1,38 +0,0 @@
#!/usr/bin/env bash
# Exit the script as soon as an error occurs.
set -euo pipefail
# This script fails if <head> contains release or staging commits that <base> does not,
# i.e. if <head> merges a release back into <base>.
# Used in the merge queue, which squashes PRs and would drop the merge commit, see RELEASING.md.
# Usage: .github/scripts/releasing/check-no-merge-back.sh <base> <head>
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <base> <head>"
exit 1
fi
BASE=$1
HEAD=$2
SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}")
# shellcheck source=.github/scripts/releasing/common.sh
source "${SCRIPT_DIR}/common.sh"
load_release_branches
RELEASE_COMMITS=$(git rev-list "${BRANCHES[@]}" --not "${BASE}")
HEAD_COMMITS=$(git rev-list "${BASE}..${HEAD}")
# The release commits in <head>, newest first.
MERGED=$(grep -xF -f <(echo "${RELEASE_COMMITS}") <<<"${HEAD_COMMITS}" || true)
if [ -z "${MERGED}" ]; then
echo "No release commits are merged back."
exit 0
fi
echo "This PR merges $(wc -l <<<"${MERGED}" | tr -d ' ') release commits back, e.g.:"
head -5 <<<"${MERGED}" | xargs git log --no-walk --format=' %h %s'
echo
echo "Merge-backs must not go through the merge queue, which squashes them."
echo "Fast-forward develop to the PR branch instead, see RELEASING.md."
exit 1
@@ -1,46 +0,0 @@
#!/usr/bin/env bash
# Exit the script as soon as an error occurs.
set -euo pipefail
# This script fails if a final release (a tag like 3.4.0) on a release branch
# is not merged back into <develop> within a few days, see RELEASING.md.
# Usage: .github/scripts/releasing/check-releases-merged.sh <develop>
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <develop>"
exit 1
fi
DEVELOP=$1
GRACE_DAYS=3
mapfile -t MERGED_ARGS < <(git for-each-ref --format='--merged=%(refname)' 'refs/remotes/*/release/*')
if [ "${#MERGED_ARGS[@]}" -eq 0 ]; then
echo "Error: No release branches found."
exit 1
fi
# Tags on a release branch that <develop> does not contain.
TAGS=$(git for-each-ref --format='%(refname:short) %(creatordate:unix)' \
"${MERGED_ARGS[@]}" --no-merged="${DEVELOP}" 'refs/tags/[0-9]*')
MISSING=0
while read -r TAG CREATED; do
if ! [[ "${TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
continue
fi
if (($(date +%s) - CREATED < GRACE_DAYS * 86400)); then
echo "${TAG}: not merged yet, still within the ${GRACE_DAYS}-day grace period."
else
echo "${TAG}: not merged into develop."
MISSING=1
fi
done <<<"${TAGS}"
if [ "${MISSING}" -ne 0 ]; then
echo
echo "Merge the missing releases back into develop, see RELEASING.md."
exit 1
fi
echo "No releases past the grace period are missing from develop."
-12
View File
@@ -1,12 +0,0 @@
# shellcheck shell=bash
# Helpers shared by the release checks in this directory, see RELEASING.md.
# Sets BRANCHES to all release and staging branches, and fails if there are none.
load_release_branches() {
mapfile -t BRANCHES < <(git for-each-ref --format='%(refname)' 'refs/remotes/*/release/*' 'refs/remotes/*/staging/*')
if [ "${#BRANCHES[@]}" -eq 0 ]; then
echo "Error: No release or staging branches found."
exit 1
fi
}
+3 -3
View File
@@ -74,7 +74,7 @@
"extra_cmake_args": "-Dvalidator_keys=ON",
"package": {
"type": "deb",
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-3a2d19f"
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-49cdc10"
}
},
{
@@ -86,7 +86,7 @@
"extra_cmake_args": "-Dvalidator_keys=ON -Dassert=ON",
"package": {
"type": "deb",
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-3a2d19f",
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-49cdc10",
"variant": "assert"
}
}
@@ -101,7 +101,7 @@
"extra_cmake_args": "-Dvalidator_keys=ON",
"package": {
"type": "rpm",
"image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-3a2d19f"
"image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-49cdc10"
}
}
]
+3 -3
View File
@@ -7,12 +7,12 @@ on:
paths:
- ".github/workflows/build-packaging-images.yml"
- "bin/install-packaging-tools.sh"
- "package/images/packaging/**"
- "package/docker/**"
pull_request:
paths:
- ".github/workflows/build-packaging-images.yml"
- "bin/install-packaging-tools.sh"
- "package/images/packaging/**"
- "package/docker/**"
workflow_dispatch:
concurrency:
@@ -44,6 +44,6 @@ jobs:
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@696384b292577293292daed06af0306d1b83bd7d
with:
image_name: xrpld/packaging-${{ matrix.distro.name }}
dockerfile: package/images/packaging/Dockerfile
dockerfile: package/docker/Dockerfile
base_image: ${{ matrix.distro.base_image }}
push: ${{ github.event_name == 'push' }}
@@ -13,6 +13,7 @@ on:
- ready_for_review
branches:
- develop
- "release-*"
- "release/*"
- "staging/*"
+1
View File
@@ -13,6 +13,7 @@ on:
- ready_for_review
branches:
- develop
- "release-*"
- "release/*"
- "staging/*"
@@ -1,60 +0,0 @@
# This workflow checks that every final release on a release branch
# has been merged back into develop, see RELEASING.md.
name: Check releases merged
on:
schedule:
# 06:47 UTC every Monday.
- cron: "47 6 * * 1"
push:
branches:
- "develop"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: bash
env:
OUTPUT_FILE: /tmp/releases-merged.txt
ISSUE_FILE: /tmp/releases-merged-issue.md
jobs:
releases-merged:
runs-on: ubuntu-latest
permissions:
contents: read
# Needed to open an issue on scheduled failures.
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# The check needs the full history of develop, the release branches and tags.
fetch-depth: 0
- name: Check releases are merged into develop
run: |
set -o pipefail
.github/scripts/releasing/check-releases-merged.sh origin/develop | tee "${OUTPUT_FILE}"
- name: Prepare issue body
if: ${{ failure() && github.event_name == 'schedule' && github.repository == 'XRPLF/rippled' }}
run: |
{
echo '```'
cat "${OUTPUT_FILE}"
echo '```'
} >"${ISSUE_FILE}"
- name: Create issue
if: ${{ failure() && github.event_name == 'schedule' && github.repository == 'XRPLF/rippled' }}
uses: XRPLF/actions/create-issue@2b8bc36af85b88bca0dd7bfac2e2dc05f94ad712
with:
title: "Releases not merged back into develop"
body_file: ${{ env.ISSUE_FILE }}
labels: "Bug"
-43
View File
@@ -150,48 +150,6 @@ jobs:
if: ${{ needs.should-run.outputs.go == 'true' }}
uses: ./.github/workflows/reusable-check-rename.yml
# Runs regardless of the changed files.
# PRs into staging branches are skipped, since fixes may be cherry-picked between release lines.
check-release-commits:
needs: should-run
if: ${{ github.event.pull_request.base.ref == 'develop' || github.event.merge_group.base_ref == 'refs/heads/develop' }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# The check needs the full history of the release and staging branches.
fetch-depth: 0
persist-credentials: false
- name: Check for copied release commits
if: ${{ github.event_name == 'pull_request' }}
env:
BASE: ${{ github.event.pull_request.base.sha }}
HEAD: ${{ github.event.pull_request.head.sha }}
run: .github/scripts/releasing/check-no-copied-release-commits.sh "${BASE}" "${HEAD}"
# Runs even if the previous check fails, so that both problems are reported at once.
- name: Check merge-back has no new commits
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
env:
BASE: ${{ github.event.pull_request.base.sha }}
HEAD: ${{ github.event.pull_request.head.sha }}
run: .github/scripts/releasing/check-merge-back-commits.sh "${BASE}" "${HEAD}"
# The queue squashes PRs, so check the PR's own branch, named in the queue branch.
- name: Check the merge queue doesn't merge a release back
if: ${{ github.event_name == 'merge_group' }}
env:
BASE: ${{ github.event.merge_group.base_sha }}
HEAD_REF: ${{ github.event.merge_group.head_ref }}
run: |
if ! [[ "${HEAD_REF}" =~ /pr-([0-9]+)-[0-9a-f]+$ ]]; then
echo "Error: Can't find the PR number in '${HEAD_REF}'."
exit 1
fi
git fetch --no-tags origin "refs/pull/${BASH_REMATCH[1]}/head"
.github/scripts/releasing/check-no-merge-back.sh "${BASE}" FETCH_HEAD
clang-tidy:
needs: should-run
if: ${{ needs.should-run.outputs.go == 'true' }}
@@ -272,7 +230,6 @@ jobs:
- check-autogen
- check-levelization
- check-rename
- check-release-commits
- clang-tidy
- build-test
- rust
+1 -8
View File
@@ -1,12 +1,9 @@
# When a versioned tag is pushed, this workflow:
#
# - builds and tests the release binaries
# - uploads the libxrpl recipe to the Conan remote
# - builds and tests the release binaries
# - builds the DEB and RPM packages
# - publishes those packages to the XRPLF package repositories
#
# Nothing is published unless the build passes, which is also where CMake
# rejects a tag that is not a valid version, e.g. 3.2.01.
name: Tag
on:
@@ -25,7 +22,6 @@ defaults:
jobs:
upload-recipe:
if: ${{ github.repository == 'XRPLF/rippled' }}
needs: build-test
uses: ./.github/workflows/reusable-upload-recipe.yml
secrets:
remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
@@ -55,6 +51,3 @@ jobs:
remote_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
signing_key: ${{ secrets.NEXUS_PACKAGES_PRIVATE_KEY }}
dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }}
antithesis_docker_host: ${{ secrets.ANTITHESIS_DOCKER_HOST }}
antithesis_docker_path: ${{ secrets.ANTITHESIS_DOCKER_PATH }}
antithesis_docker_credentials: ${{ secrets.ANTITHESIS_DOCKER_CREDENTIALS }}
-3
View File
@@ -130,6 +130,3 @@ jobs:
remote_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
signing_key: ${{ secrets.NEXUS_PACKAGES_PRIVATE_KEY }}
dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }}
antithesis_docker_host: ${{ secrets.ANTITHESIS_DOCKER_HOST }}
antithesis_docker_path: ${{ secrets.ANTITHESIS_DOCKER_PATH }}
antithesis_docker_credentials: ${{ secrets.ANTITHESIS_DOCKER_CREDENTIALS }}
@@ -196,19 +196,6 @@ jobs:
env:
BUILD_TYPE: ${{ inputs.build_type }}
CMAKE_ARGS: ${{ inputs.cmake_args }}
# GitHub creates a merge commit for a PR
# https://www.kenmuse.com/blog/the-many-shas-of-a-github-pull-request/
#
# We:
# - explicitly provide branch name
# - use `github.event.pull_request.head.sha` to get the SHA of last commit in the PR branch
#
# This way it works both for PRs and pushes to branches.
GITHUB_BRANCH_NAME: "${{ github.head_ref || github.ref_name }}"
GITHUB_HEAD_SHA: "${{ github.event.pull_request.head.sha || github.sha }}"
#
# If tag is being pushed, we use that version.
FORCE_XRPLD_VERSION: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || '' }}
run: |
cmake \
-G '${{ runner.os == 'Windows' && 'Visual Studio 18 2026' || 'Ninja' }}' \
+13 -58
View File
@@ -9,9 +9,8 @@
# never reaches Nexus
# - 'publish' uploads with the image's publish_pkg.py, doing a --dry-run
# unless 'publish: true'
# - 'docker' builds an Ubuntu image from the tested DEB, and one with the
# voidstar binary for Antithesis, pushing them to Docker Hub and to the
# Antithesis registry only with 'publish: true'
# - 'docker' builds an Ubuntu image from the tested DEB, pushing it to Docker
# Hub only with 'publish: true'
#
# Only linux/amd64 is supported; the runner is hardcoded in the jobs below.
name: Package
@@ -38,19 +37,10 @@ on:
description: "The password or token for that Nexus account."
required: false
signing_key:
description: "Armoured PGP private key used to sign the RPMs."
description: "Armoured PGP private key used to sign the RPMs. Required when publishing."
required: false
dockerhub_token:
description: "A Docker Hub organization access token for xrplf, with push access to xrplf/xrpld."
required: false
antithesis_docker_host:
description: "The host of the Antithesis container registry, e.g. us-central1-docker.pkg.dev."
required: false
antithesis_docker_path:
description: "The repository path in that registry, the image name excluded."
required: false
antithesis_docker_credentials:
description: "The JSON key of a service account with push access to that repository."
description: "A Docker Hub organization access token for xrplf, with push access to xrplf/xrpld. Required when publishing."
required: false
defaults:
@@ -257,35 +247,18 @@ jobs:
docker:
needs: [test-install-deb, test-install-rpm]
strategy:
fail-fast: false
matrix:
target: [xrpld, voidstar]
name: "docker ${{ matrix.target }}${{ !inputs.publish && ' (dry run)' || '' }}"
name: "docker${{ !inputs.publish && ' (dry run)' || '' }}"
permissions:
contents: read
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 5
env:
CONTEXT: image-context
IMAGE: ${{ matrix.target == 'voidstar' && 'xrpld-voidstar' || 'xrplf/xrpld' }}:${{ github.ref_type == 'tag' && github.ref_name || 'develop' }}
IMAGE: xrplf/xrpld:${{ github.ref_type == 'tag' && github.ref_name || 'develop' }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Determine release info
id: release_info
uses: ./.github/actions/release-info
# Docker Hub is public, so it only gets builds whose packages are public:
# those of a public codebase, and stable releases.
# The Antithesis registry is private, so it gets every build.
- name: Decide whether to push
env:
PUSH: ${{ inputs.publish && (matrix.target == 'voidstar' || github.event.repository.visibility == 'public' || steps.release_info.outputs.channel == 'stable') }}
run: echo "PUSH=${PUSH}" | tee -a "${GITHUB_ENV}"
- name: Download package artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
@@ -293,20 +266,13 @@ jobs:
merge-multiple: true
path: ${{ env.PACKAGE_DIR }}
- name: Download voidstar binary
if: ${{ matrix.target == 'voidstar' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: xrpld-ubuntu-clang-debug-amd64-voidstar
path: ${{ env.CONTEXT }}
- name: Build image
env:
TARGET: ${{ matrix.target }}
CONTEXT: image-context
run: |
mkdir -p "${CONTEXT}"
find "${PACKAGE_DIR}" -type f -name 'xrpld_[0-9]*.deb' -exec cp {} "${CONTEXT}/" \;
docker build --pull --file package/images/xrpld/Dockerfile --target "${TARGET}" --tag "${IMAGE}" "${CONTEXT}"
docker build --pull --file package/image/Dockerfile --tag "${IMAGE}" "${CONTEXT}"
- name: Start the server
run: |
@@ -324,25 +290,14 @@ jobs:
docker logs "${container}"
exit 1
# Docker Hub is public, so a private build never reaches it.
- name: Log in to Docker Hub
if: ${{ env.PUSH == 'true' && matrix.target == 'xrpld' }}
if: ${{ inputs.publish && github.event.repository.visibility == 'public' }}
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
username: xrplf
password: ${{ secrets.dockerhub_token }}
- name: Log in to the Antithesis registry
if: ${{ env.PUSH == 'true' && matrix.target == 'voidstar' }}
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ secrets.antithesis_docker_host }}
username: _json_key
password: ${{ secrets.antithesis_docker_credentials }}
- name: Push image
if: ${{ env.PUSH == 'true' }}
env:
REGISTRY: ${{ matrix.target == 'voidstar' && format('{0}/{1}/', secrets.antithesis_docker_host, secrets.antithesis_docker_path) || '' }}
run: |
docker tag "${IMAGE}" "${REGISTRY}${IMAGE}"
docker push "${REGISTRY}${IMAGE}"
if: ${{ inputs.publish && github.event.repository.visibility == 'public' }}
run: docker push "${IMAGE}"
-3
View File
@@ -92,9 +92,6 @@ target/
# Direnv's directory
/.direnv
# Direnv's local, per-developer overrides
/.envrc.local
# clangd cache
/.cache
+7 -7
View File
@@ -60,7 +60,7 @@ repos:
types_or: [c++, c]
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: e2b496dc2bd8340c2524cb9a2d2a943cde1bb6df # frozen: v23.1.1
rev: f4d7745e17a28aad7eed2f4874ca8d1568c11c4c # frozen: v22.1.8
hooks:
- id: clang-format
args: [--style=file]
@@ -82,12 +82,12 @@ repos:
files: ^crates/.*\.rs$
- repo: https://github.com/BlankSpruce/gersemi-pre-commit
rev: f1c4833f8cf23c6d952673abc73525411a5719e8 # frozen: 0.29.1
rev: e98930bdc210d3387007f9252d8c1694ea7e410f # frozen: 0.27.7
hooks:
- id: gersemi
- repo: https://github.com/rbubley/mirrors-prettier
rev: ef4a397f916211b4a39ccf9d3d9cbb6562157251 # frozen: v3.9.9
rev: 9337a74165b178ae2c766f60bee7252a0f06f3e8 # frozen: v3.9.5
hooks:
- id: prettier
args: [--end-of-line=auto]
@@ -95,7 +95,7 @@ repos:
# Scoped to package/: the rest of the repo's Python has pre-existing findings,
# so widening these is its own change.
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: a56c0b927e6465d37cae3e97d35d4d18ab2b96cd # frozen: v0.16.9
rev: 7c55798a78262d14b2074abf623d8a992ebb70d4 # frozen: v0.16.2
hooks:
- id: ruff-check
args: [--fix]
@@ -107,14 +107,14 @@ repos:
- id: black
- repo: https://github.com/pre-commit/mirrors-mypy
rev: 7ff8d35ae36a7d2b968f2f90b4c723e292e594ee # frozen: v2.3.1
rev: 41e691678310dfd3833f7ab4e180ddb014310356 # frozen: v2.3.0
hooks:
- id: mypy
args: [--strict]
files: ^package/.*\.py$
- repo: https://github.com/scop/pre-commit-shfmt
rev: 479be5958357ba5ab65ce47172117348516860e9 # frozen: v3.14.1-1
rev: 05c1426671b9237fb5e1444dd63aa5731bec0dfb # frozen: v3.13.1-1
hooks:
- id: shfmt
args: [--write, --indent=4, --case-indent=true]
@@ -133,7 +133,7 @@ repos:
files: \.md$
- repo: https://github.com/streetsidesoftware/cspell-cli
rev: f5c5d72342f35643988a9ffa0705c05bd3ff8383 # frozen: v10.3.0
rev: ea11f9efc0bec520073405bc30552da887ba71bc # frozen: v10.0.1
hooks:
- id: cspell
name: check changed files spelling
-8
View File
@@ -22,14 +22,6 @@ 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.
## XRP Ledger server version 3.5.0
Version 3.5.0 is not yet released.
### Additions in 3.5.0
- `subscribe`, `unsubscribe`: Added an optional `mpt_issuances` request field, an array of MPT issuance IDs (hex strings). Subscribers receive a message with `type` `mptTransaction` for each validated transaction whose metadata affects a subscribed issuance; the message has the same fields as the `transactions` stream. MPT issuance subscriptions count toward the per-connection subscription limit. An empty array, a non-array value, or an invalid ID returns `invalidParams`. ([#5671](https://github.com/XRPLF/rippled/pull/5671))
## XRP Ledger server version 3.4.0
Version 3.4.0 is not yet released. These changes are available in the 3.4.0 beta releases.
+5 -9
View File
@@ -41,19 +41,15 @@ branch.
git checkout develop
```
For a release or release candidate, check out its [tag](https://github.com/XRPLF/rippled/releases), e.g.:
For a release candidate, choose the relevant release branch, e.g.
`release/3.2.x`.
```bash
git checkout 3.4.0
git checkout release/3.2.x
```
See [RELEASING.md](./RELEASING.md) for how branches and releases are organized.
A build reports `0.0.0-dev` with its short commit hash as build metadata, e.g.
`0.0.0-dev+0123abc`. Only builds of a release, from a tag in CI or a versioned
Conan reference, report a release version. To use another version, set the
`FORCE_XRPLD_VERSION` environment variable when running CMake, e.g.
`FORCE_XRPLD_VERSION=3.4.0`.
For a stable release, choose one of the [tagged
releases](https://github.com/XRPLF/rippled/releases).
### Set Up Conan
+1
View File
@@ -77,6 +77,7 @@ endif()
include(PatchNixBinary)
include(XrplSanity)
include(XrplVersion)
include(XrplSettings)
# this check has to remain in the top-level cmake because of the early return statement
if(packages_only)
+586 -20
View File
@@ -14,13 +14,9 @@ The following branches exist in the main project repository:
- `develop`: The latest set of unreleased features, and the most common
starting point for contributions.
- `staging/*` (e.g. `staging/3.4.x`): Staging branches, one per release line,
where fixes for that line are developed.
- `release/*` (e.g. `release/3.4.x`): Release branches, one per release line,
holding the latest public release candidate or release for that line.
Releases are published as [tagged releases](https://github.com/XRPLF/rippled/releases).
See [RELEASING.md](./RELEASING.md) for how these branches are used.
- `release/*` (e.g. `release/3.2.x`): Release branches, one per release line,
holding the latest release candidate, or stable release for that line.
Stable releases are published as [tagged releases](https://github.com/XRPLF/rippled/releases).
The tip of each branch must be signed. In order for GitHub to sign a
squashed commit that it builds from your pull request, GitHub must know
@@ -149,8 +145,8 @@ tl;dr
In general, pull requests use `develop` as the base branch.
The exceptions are fixes for an existing release line,
which use that line's staging branch (e.g. `staging/3.4.x`) as the base.
The exceptions are fixes, improvements, and hotfixes for an existing release,
which use that release's branch (e.g. `release/3.2.x`) as the base.
If your changes are not quite ready, but you want to make it easily available
for preliminary examination or review, you can create a "Draft" pull request.
@@ -595,16 +591,15 @@ the suggested commit message, or modify it as needed.
#### Slightly more complicated pull requests
Some pull requests need to be pushed to their base branch (usually `develop`)
as more than one commit.
A PR author may _request_ to merge as separate commits. They
Some pull requests need to be pushed to `develop` as more than one
commit. A PR author may _request_ to merge as separate commits. They
must _justify_ why separate commits are needed, and _specify_ how they
would like the commits to be merged. If you disagree with the author,
discuss it with them directly.
If the process is reasonable, follow it. The simplest option is to do a
fast forward only merge (`--ff-only`) on the command line
and push to the base branch.
fast forward only merge (`--ff-only`) on the command line and push to
`develop`.
Some examples of when separate commits are worthwhile are:
@@ -617,10 +612,9 @@ Some examples of when separate commits are worthwhile are:
Either way, check that:
- The commits are based on the current tip of the base branch.
- The commits are clean:
No merge commits (except when merging a release, see [RELEASING.md](./RELEASING.md)),
no "[FOLD]" or "fixup!" messages.
- The commits are based on the current tip of `develop`.
- The commits are clean: No merge commits (except when reverse
merging), no "[FOLD]" or "fixup!" messages.
- All commits are signed. If the commits are not signed by the author, use
`git commit --amend -S` to sign them yourself.
- At least one (but preferably all) of the commits has the PR number
@@ -632,8 +626,578 @@ use them!**
### Releases
Releases, release branches, and merging releases back into `develop`
are described in [RELEASING.md](./RELEASING.md).
All releases, including release candidates and betas, are handled
differently from typical PRs. Most importantly, never use
the Github UI to merge a release.
Xrpld uses a linear workflow model that can be summarized as:
1. In between releases, developers work against the `develop` branch.
2. Periodically, a maintainer will build and tag a beta version from
`develop`, which is pushed to `release`.
- Betas are usually released every two to three weeks, though that
schedule can vary depending on progress, availability, and other
factors.
3. When the changes in `develop` are considered stable and mature enough
to be ready to release, a release candidate (RC) is built and tagged
from `develop`, and merged to `release`.
- Further development for that release (primarily fixes) then
continues against `release`, while other development continues on
`develop`. Effectively, `release` is forked from `develop`. Changes
to `release` must be reverse merged to `develop`.
4. When the candidate has passed testing and is ready for release, the
final release is merged to `master`.
5. If any issues are found post-release, a hotfix / point release may be
created, which is merged to `master`, and then reverse merged to
`develop`.
#### Betas, and the first release candidate
##### Preparing the `develop` branch
1. Optimally, the `develop` branch will be ready to go, with all
relevant PRs already merged.
2. If there are any PRs pending, merge them **BEFORE** preparing the beta.
1. If only one or two PRs need to be merged, merge those PRs [as
normal](#when-and-how-to-merge-pull-requests), updating the second
one, and waiting for CI to finish in between.
2. If there are several pending PRs, do not use the Github UI,
because the delays waiting for CI in between each merge will be
unnecessarily onerous. (Incidentally, this process can also be
used to merge if the Github UI has issues.) Merge each PR branch
directly to a `release-next` on your local machine and create a single
PR, then push your branch to `develop`.
1. Squash the changes from each PR, one commit each (unless more
are needed), being sure to sign each commit and update the
commit message to include the PR number. You may be able to use
a fast-forward merge for the first PR.
2. Push your branch.
3. Continue to [Making the release](#making-the-release) to update
the version number, etc.
The workflow may look something like:
```
git fetch --multiple upstreams user1 user2 user3 [...]
git checkout -B release-next --no-track upstream/develop
# Only do an ff-only merge if pr-branch1 is either already
# squashed, or needs to be merged with separate commits,
# and has no merge commits.
# Use -S on the ff-only merge if pr-branch1 isn't signed.
git merge [-S] --ff-only user1/pr-branch1
git merge --squash user2/pr-branch2
git commit -S # Use the commit message provided on the PR
git merge --squash user3/pr-branch3
git commit -S # Use the commit message provided on the PR
[...]
# Make sure the commits look right
git log --show-signature "upstream/develop..HEAD"
git push --set-upstream origin
# Continue to "Making the release" to update the version number, so
# everything can be done in one PR.
```
You can also use the [squash-branches] script.
You may also need to manually close the open PRs after the changes are
merged to `develop`. Be sure to include the commit ID.
##### Making the release
This includes, betas, and the first release candidate (RC).
1. If you didn't create one [preparing the `develop`
branch](#preparing-the-develop-branch), Ensure there is no old
`release-next` branch hanging around. Then make a `release-next`
branch that only changes the version number. e.g.
```
git fetch upstreams
git checkout --no-track -B release-next upstream/develop
v="A.B.C-bD"
build=$( find -name BuildInfo.cpp )
sed 's/\(^.*versionString =\).*$/\1 "'${v}'"/' ${build} > version.cpp && mv -vi version.cpp ${build}
git diff
git add ${build}
git commit -S -m "Set version to ${v}"
# You could use your "origin" repo, but some CI tests work better on upstream.
git push upstream-push
git fetch upstreams
git branch --set-upstream-to=upstream/release-next
```
You can also use the [update-version] script. 2. Create a Pull Request for `release-next` with **`develop`** as
the base branch.
1. Use the title "[TRIVIAL] Set version to X.X.X-bX".
2. Instead of the default description template, use the following:
```
## High Level Overview of Change
This PR only changes the version number. It will be merged as
soon as Github CI actions successfully complete.
```
3. Wait for CI to successfully complete, and get someone to approve
the PR. (It is safe to ignore known CI issues.)
4. Push the updated `develop` branch using your `release-next`
branch. **Do not use the Github UI. It's important to preserve
commit IDs.**
```
git push upstream-push release-next:develop
```
5. In the unlikely event that the push fails because someone has merged
something else in the meantime, rebase your branch onto the updated
`develop` branch, push again, and go back to step 3.
6. Ensure that your PR against `develop` is closed. Github should do it
automatically.
7. Once this is done, forward progress on `develop` can continue
(other PRs may be merged).
8. Now create a Pull Request for `release-next` with **`release`** as
the base branch. Instead of the default template, reuse and update
the message from the previous release. Include the following verbiage
somewhere in the description:
```
The base branch is `release`. [All releases (including
betas)](https://github.com/XRPLF/rippled/blob/develop/CONTRIBUTING.md#before-you-start)
go in `release`. This PR branch will be pushed directly to `release` (not
squashed or rebased, and not using the GitHub UI).
```
7. Sign-offs for the three platforms (Linux, Mac, Windows) usually occur
offline, but at least one approval will be needed on the PR.
- If issues are discovered during testing, simply abandon the
release. It's easy to start a new release, it should be easy to
abandon one. **DO NOT REUSE THE VERSION NUMBER.** e.g. If you
abandon 2.4.0-b1, the next attempt will be 2.4.0-b2.
8. Once everything is ready to go, push to `release`.
```
git fetch upstreams
# Just to be safe, do a dry run first:
git push --dry-run upstream-push release-next:release
# If everything looks right, push the branch
git push upstream-push release-next:release
# Check that all of the branches are updated
git fetch upstreams
git log -1 --oneline
# The output should look like:
# 0123456789 (HEAD -> upstream/release-next, upstream/release,
# upstream/develop) Set version to 2.4.0-b1
# Note that upstream/develop may not be on this commit, but
# upstream/release must be.
# Other branches, including some from upstream-push, may also be
# present.
```
9. Tag the release, too.
```
git tag <version number>
git push upstream-push <version number>
```
10. Delete the `release-next` branch on the repo. Use the Github UI or:
```
git push --delete upstream-push release-next
```
11. Finally [create a new release on
Github](https://github.com/XRPLF/rippled/releases).
#### Release candidates after the first
Once the first release candidate is [merged into
release](#making-the-release), then `release` and `develop` _are allowed
to diverge_.
If a bug or issue is discovered in a version that has a release
candidate being tested, any fix and new version will need to be applied
against `release`, then reverse-merged to `develop`. This helps keep git
history as linear as possible.
A `release-next` branch will be created from `release`, and any further
work for that release must be based on `release-next`. Specifically,
PRs must use `release-next` as the base, and those PRs will be merged
directly to `release-next` when approved. Changes should be restricted
to bug fixes, but other changes may be necessary from time to time.
1. Open any PRs for the pending release using `release-next` as the base,
so they can be merged directly in to it. Unlike `develop`, though,
`release-next` can be thrown away and recreated if necessary.
2. Once a new release candidate is ready, create a version commit as in
step 1 [above](#making-the-release) on `release-next`. You can use
the [update-version] script for this, too.
3. Jump to step 8 ("Now create a Pull Request for `release-next` with
**`release`** as the base") from the process
[above](#making-the-release) to merge `release-next` into `release`.
##### Follow up: reverse merge
Once the RC is merged and tagged, it needs to be reverse merged into
`develop` as soon as possible.
1. Create a branch, based on `upstream/develop`.
The branch name is not important, but could include "mergeNNNrcN".
E.g. For release A.B.C-rcD, use `mergeABCrcD`.
```
git fetch upstreams
git checkout --no-track -b mergeABCrcD upstream/develop
```
2. Merge `release` into your branch.
```
# I like the "--edit --log --verbose" parameters, but they are
# not required.
git merge upstream/release
```
3. `BuildInfo.cpp` will have a conflict with the version number.
Resolve it with the version from `develop` - the higher version.
4. Push your branch to your repo (or `upstream` if you have permission),
and open a normal PR against `develop`. The "High level overview" can
simply indicate that this is a merge of the RC. The "Context" should
summarize the changes from the RC. Include the following text
prominently:
```
This PR must be merged manually using a push. Do not use the Github UI.
```
5. Depending on the complexity of the changes, and/or merge conflicts,
the PR may need a thorough review, or just a sign-off that the
merge was done correctly.
6. If `develop` is updated before this PR is merged, do not merge
`develop` back into your branch. Instead rebase preserving merges,
or do the merge again. (See also the `rerere` git config setting.)
```
git rebase --rebase-merges upstream/develop
# OR
git reset --hard upstream/develop
git merge upstream/release
```
7. When the PR is ready, push it to `develop`.
```
git fetch upstreams
# Make sure the commits look right
git log --show-signature "upstream/develop^..HEAD"
git push upstream-push mergeABCrcD:develop
git fetch upstreams
```
Development on `develop` can proceed as normal.
#### Final releases
A final release is any release that is not a beta or RC, such as 2.2.0.
Only code that has already been tested and vetted across all three
platforms should be included in a final release. Most of the time, that
means that the commit immediately preceding the commit setting the
version number will be an RC. Occasionally, there may be last-minute bug
fixes included as well. If so, those bug fixes must have been tested
internally as if they were RCs (at minimum, ensuring unit tests pass,
and the app starts, syncs, and stops cleanly across all three
platforms.)
_If in doubt, make an RC first._
The process for building a final release is very similar to [the process
for building a beta](#making-the-release), except the code will be
moving from `release` to `master` instead of from `develop` to
`release`, and both branches will be pushed at the same time.
1. Ensure there is no old `master-next` branch hanging around.
Then make a `master-next` branch that only changes the version
number. As above, or using the
[update-version] script.
2. Create a Pull Request for `master-next` with **`master`** as
the base branch. Instead of the default template, reuse and update
the message from the previous final release. Include the following verbiage
somewhere in the description:
```
The base branch is `master`. This PR branch will be pushed directly to
`release` and `master` (not squashed or rebased, and not using the
GitHub UI).
```
7. Sign-offs for the three platforms (Linux, Mac, Windows) usually occur
offline, but at least one approval will be needed on the PR.
- If issues are discovered during testing, close the PR, delete
`master-next`, and move development back to `release`, [issuing
more RCs as necessary](#release-candidates-after-the-first)
8. Once everything is ready to go, push to `release` and `master`.
```
git fetch upstreams
# Just to be safe, do dry runs first:
git push --dry-run upstream-push master-next:release
git push --dry-run upstream-push master-next:master
# If everything looks right, push the branch
git push upstream-push master-next:release
git push upstream-push master-next:master
# Check that all of the branches are updated
git fetch upstreams
git log -1 --oneline
# The output should look like:
# 0123456789 (HEAD -> upstream/master-next, upstream/master,
# upstream/release) Set version to A.B.0
# Note that both upstream/release and upstream/master must be on this
# commit.
# Other branches, including some from upstream-push, may also be
# present.
```
9. Tag the release, too.
```
git tag <version number>
git push upstream-push <version number>
```
10. Delete the `master-next` branch on the repo. Use the Github UI or:
```
git push --delete upstream-push master-next
```
11. [Create a new release on
Github](https://github.com/XRPLF/rippled/releases). Be sure that
"Set as the latest release" is checked.
12. Open a PR to update the [API-CHANGELOG](API-CHANGELOG.md) and `API-VERSION-[n].md` with the changes for this release (if any are missing).
13. Finally, [reverse merge the release into `develop`](#follow-up-reverse-merge).
#### Special cases: point releases, hotfixes, etc.
On occasion, a bug or issue is discovered in a version that already
had a final release. Most of the time, development will have started
on the next version, and will usually have changes in `develop`
and often in `release`.
Because git history is kept as linear as possible, any fix and new
version will need to be applied against `master`.
The process for building a hotfix release is very similar to [the
process for building release candidates after the
first](#release-candidates-after-the-first) and [for building a final
release](#final-releases), except the changes will be done against
`master` instead of `release`.
If there is only a single issue for the hotfix, the work can be done in
any branch. When it's ready to merge, jump to step 3 using your branch
instead of `master-next`.
1. Create a `master-next` branch from `master`.
```
git checkout --no-track -b master-next upstream/master
git push upstream-push
git fetch upstreams
```
2. Open any PRs for the pending hotfix using `master-next` as the base,
so they can be merged directly in to it. Unlike `develop`, though,
`master-next` can be thrown away and recreated if necessary.
3. Once the hotfix is ready, create a version commit using the same
steps as above, or use the
[update-version] script.
4. Create a Pull Request for `master-next` with **`master`** as
the base branch. Instead of the default template, reuse and update
the message from the previous final release. Include the following verbiage
somewhere in the description:
```
The base branch is `master`. This PR branch will be pushed directly to
`master` (not squashed or rebased, and not using the GitHub UI).
```
7. Sign-offs for the three platforms (Linux, Mac, Windows) usually occur
offline, but at least one approval will be needed on the PR.
- If issues are discovered during testing, update `master-next` as
needed, but ensure that the changes are properly squashed, and the
version setting commit remains last
8. Once everything is ready to go, push to `master` **only**.
```
git fetch upstreams
# Just to be safe, do a dry run first:
git push --dry-run upstream-push master-next:master
# If everything looks right, push the branch
git push upstream-push master-next:master
# Check that all of the branches are updated
git fetch upstreams
git log -1 --oneline
# The output should look like:
# 0123456789 (HEAD -> upstream/master-next, upstream/master) Set version
# to 2.4.1
# Note that upstream/master must be on this commit. upstream/release and
# upstream/develop should not.
# Other branches, including some from upstream-push, may also be
# present.
```
9. Tag the release, too.
```
git tag <version number>
git push upstream-push <version number>
```
9. Delete the `master-next` branch on the repo.
```
git push --delete upstream-push master-next
```
10. [Create a new release on
Github](https://github.com/XRPLF/rippled/releases). Be sure that
"Set as the latest release" is checked.
Once the hotfix is released, it needs to be reverse merged into
`develop` as soon as possible. It may also need to be merged into
`release` if a release candidate is under development.
1. Create a branch in your own repo, based on `upstream/develop`.
The branch name is not important, but could include "mergeNNN".
E.g. For release 2.2.3, use `merge223`.
```
git fetch upstreams
git checkout --no-track -b merge223 upstream/develop
```
2. Merge master into your branch.
```
# I like the "--edit --log --verbose" parameters, but they are
# not required.
git merge upstream/master
```
3. `BuildInfo.cpp` will have a conflict with the version number.
Resolve it with the version from `develop` - the higher version.
4. Push your branch to your repo, and open a normal PR against
`develop`. The "High level overview" can simply indicate that this
is a merge of the hotfix version. The "Context" should summarize
the changes from the hotfix. Include the following text
prominently:
```
This PR must be merged manually using a --ff-only merge. Do not use the Github UI.
```
5. Depending on the complexity of the hotfix, and/or merge conflicts,
the PR may need a thorough review, or just a sign-off that the
merge was done correctly.
6. If `develop` is updated before this PR is merged, do not merge
`develop` back into your branch. Instead rebase preserving merges,
or do the merge again. (See also the `rerere` git config setting.)
```
git rebase --rebase-merges upstream/develop
# OR
git reset --hard upstream/develop
git merge upstream/master
```
7. When the PR is ready, push it to `develop`.
```
git fetch upstreams
# Make sure the commits look right
git log --show-signature "upstream/develop..HEAD"
git push upstream-push HEAD:develop
```
Development on `develop` can proceed as normal. It is recommended to
create a beta (or RC) immediately to ensure that everything worked as
expected.
##### An even rarer scenario: A hotfix on an old release
Historically, once a final release is tagged and packages are released,
versions older than the latest final release are no longer supported.
However, there is a possibility that a very high severity bug may occur
in a non-amendment blocked version that is still being run by
a significant fraction of users, which would necessitate a hotfix / point
release to that version as well as any later versions.
This scenario would follow the same basic procedure as above,
except that _none_ of `develop`, `release`, or `master`
would be touched during the release process.
In this example, consider if version 2.1.1 needed to be patched.
1. Create two branches in the main (`upstream`) repo.
```
git fetch upstreams
# Create a base branch off the tag
git checkout --no-track -b master-2.1.2 2.1.1
git push upstream-push
# Create a working branch
git checkout --no-track -b master212-next master-2.1.2
git push upstream-push
git fetch upstreams
```
2. Work continues as above, except using `master-2.1.2`as
the base branch for any merging, packaging, etc.
3. After the release is tagged and packages are built, you could
potentially delete both branches, e.g. `master-2.1.2` and
`master212-next`. However, it may be useful to keep `master-2.1.2`
around indefinitely for reference.
4. Assuming that a hotfix is also released for the latest
version in parallel with this one, or if the issue is
already fixed in the latest version, do no do any
reverse merges. However, if it is not, it probably makes
sense to reverse merge `master-2.1.2` into `master`,
release a hotfix for _that_ version, then reverse merge
from `master` to `develop`. (Please don't do this unless absolutely
necessary.)
[contrib]: https://docs.github.com/en/get-started/quickstart/contributing-to-projects
[squash]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-commits
@@ -641,3 +1205,5 @@ are described in [RELEASING.md](./RELEASING.md).
[xrpld]: https://github.com/XRPLF/rippled
[signing]: https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification
[setup-upstreams]: ./bin/git/setup-upstreams.sh
[squash-branches]: ./bin/git/squash-branches.sh
[update-version]: ./bin/git/update-version.sh
-152
View File
@@ -1,152 +0,0 @@
# Branching and Release Management
This document describes how we branch, release, and merge releases back into `develop`.
It does not define version naming
(e.g., what constitutes a major-minor, patch, or beta release).
Examples use `X.Y` for a release line:
`X` and `Y` are placeholders, while the trailing `x` is literal,
e.g., `release/X.Y.x` is `release/3.4.x` for the `3.4` line.
## Branches
| Branch | Purpose |
| :-------------- | :---------------------------------------------------------------------------------------- |
| `develop` | Main development branch. Betas and the first RC of a major-minor release are tagged here. |
| `staging/X.Y.x` | Where fixes for the `X.Y` line are developed and RCs are prepared. |
| `release/X.Y.x` | The last public RC or release of the `X.Y` line. |
A release line is named `X.Y.x`
because one branch serves every patch release of that line (`X.Y.0`, `X.Y.1`, `X.Y.2`, ...).
The `/` groups branches hierarchically,
so tools can filter them and protection rules can target `release/*` and `staging/*`.
## Principles
- **Releases are merged back into `develop`, never cherry-picked or rebased onto it.**
Cherry-picked and rebased commits get new hashes,
so Git can't tell that `develop` already has them.
Future merges then replay them and produce artificial conflicts,
and it's hard to verify that every fix actually reached `develop`.
Merging keeps a single history:
Git knows exactly which release commits `develop` contains,
and no fix is left behind.
- **Branches only move forward.**
`develop`, `staging/X.Y.x`, and `release/X.Y.x` are never rewritten.
- **Cherry-picking only goes from `develop` to a staging branch**,
for fixes that must get into a release after the code freeze
(see [Emergency Fixes From `develop`](#emergency-fixes-from-develop)).
- **Security fixes are prepared privately and published with the release that contains them**,
so vulnerabilities are not disclosed prematurely.
## Release Lifecycle
This diagram shows a major-minor release and its first patch release.
The staging and release branches are drawn as one line.
```text
develop staging/X.Y.x & release/X.Y.x
│
├── Tag: X.Y.0-b1
├── Tag: X.Y.0-bN
├── Tag: X.Y.0-rc1 ───────────────┐ (branches created)
│ │
│ (development continues) ├── Fixes
│ ├── Tag: X.Y.0-rcN
│ ├── Tag: X.Y.0 (final)
◀──── (merge) ────────────────────┤
│ ├── Fixes
│ ├── Tag: X.Y.1-rcN
│ ├── Tag: X.Y.1 (final)
◀──── (merge) ────────────────────┤
│
▼
```
### Betas, First RC & Branching
> [!NOTE]
> This phase applies only to a new major-minor release (e.g., `X.Y.0`).
> Patch releases work on the existing branches of the line.
1. **Betas:** All beta versions (e.g., `X.Y.0-b1`) are built and tagged directly on `develop`.
2. **First RC:** We release the first RC (`X.Y.0-rc1`)
once everything that should be included in the release has been merged.
3. **Branches:** `staging/X.Y.x` and `release/X.Y.x` are created from `develop`
at the commit tagged `X.Y.0-rc1`.
The first RC also kicks off the QE process.
4. **Code freeze:** No new features or unrelated changes are pulled from `develop`
into `staging/X.Y.x` or `release/X.Y.x`.
Only critical stabilization fixes go into the line.
5. **No large changes on `develop`:** Until `X.Y.0` is [merged back](#merging-back-into-develop),
large changes (e.g., big refactors, moving or renaming many files, mass reformatting)
are not merged into `develop`,
so that fixes on the line and the merge back don't run into conflicts.
### Release Candidates
1. Fixes are developed against `staging/X.Y.x`.
2. When ready, a new RC is created on `staging/X.Y.x`,
and `release/X.Y.x` is fast-forwarded to it.
RCs that contain unpublished security fixes are not published,
and don't touch the public branches.
RCs are not merged back into `develop`:
`X.Y.0-rc1` is tagged on `develop` itself,
and all later changes reach `develop` with the [final release](#final-release).
### Final Release
Security fixes become public as soon as they reach the public repo,
so these steps happen only once the release is ready to be published,
one right after the other.
1. Unpublished security fixes, if any, are merged into `staging/X.Y.x`.
2. `release/X.Y.x` is fast-forwarded to `staging/X.Y.x`,
and the release is tagged on it.
3. The release is immediately [merged back into `develop`](#merging-back-into-develop).
For a major-minor release, this lifts the freeze on large changes in `develop`.
### Merging Back Into `develop`
The merge back is a regular PR into `develop`
whose branch contains a real merge commit of the release tag:
1. Create a branch from `develop`, run `git merge --no-ff <tag>`, and resolve any conflicts.
2. Once the PR is approved, `develop` is fast-forwarded to the PR branch,
so the merge commit lands as it is.
Never squash or rebase it.
Never add it to the merge queue either: the queue squashes PRs, which would drop the merge commit.
3. If `develop` moves while the PR is open, redo the merge on top of the new `develop`.
After the merge, `git log develop..<tag>` must be empty.
## Special Cases
### Emergency Fixes From `develop`
If a commit was merged to `develop`
and needs to be included in a release after the code freeze:
1. Create a PR that cherry-picks the commit onto `staging/X.Y.x`.
2. Leave `develop` as it is, with no reverts.
3. Follow the [release candidates](#release-candidates) process as usual.
When the release is later merged back into `develop`,
both sides already contain the same change,
so the merge usually resolves it cleanly or with a trivial conflict.
From Git's perspective, the cherry-picked commit then becomes part of `develop` too.
### Several Supported Lines
When a fix must ship in more than one supported line (e.g., `X.Y` and `X.(Y+1)`),
the lines are merged upwards rather than cherry-picked between:
1. The fix goes into the oldest line first.
2. After that line's release,
its `release/X.Y.x` is merged into the staging branch of the next newer line,
and so on up to the newest line.
3. The newest line is merged back into `develop` as usual.
This way every newer line, and eventually `develop`, contains the history of the older lines.
-12
View File
@@ -24,13 +24,9 @@
# `g++-15`, ...) are probed under both names: a suffixed name can break while
# the plain one still works (see mkVersionedToolLinks in nix/packages.nix).
#
# Tools scoped to a single dev shell rather than to commonPackages are checked
# only in that shell, keyed off XRPL_DEVSHELL.
#
# Environment variables:
# CI if set, skip the tools above when on macOS.
# CHECK_TOOLS_SKIP_CLONE if set, skip the git-over-HTTPS connectivity check.
# XRPL_DEVSHELL active dev shell; selects shell-specific tools.
set -uo pipefail
@@ -167,14 +163,6 @@ if [ "${os}" = "linux" ] || [ "${os}" = "macos" ]; then
check rustfmt
fi
# Lean4 is in the formal-verification shell only, not in commonPackages.
if [ "${XRPL_DEVSHELL:-}" = "formal-verification" ]; then
echo
echo "Formal verification toolchain:"
check lean
check lake
fi
# GCC is the default compiler on Linux. macOS uses the system Apple Clang
# instead, so GCC/g++/gcov are not expected there.
if [ "${os}" = "linux" ]; then
+56
View File
@@ -0,0 +1,56 @@
#!/bin/bash
if [[ $# -ne 3 || "$1" == "--help" || "$1" = "-h" ]]; then
name=$(basename $0)
cat <<-USAGE
Usage: $name workbranch base/branch version
* workbranch will be created locally from base/branch. If it exists,
it will be reused, so make sure you don't overwrite any work.
* base/branch may be specified as user:branch to allow easy copying
from Github PRs.
USAGE
exit 0
fi
work="$1"
shift
base=$(echo "$1" | sed "s/:/\//")
shift
version=$1
shift
set -e
git fetch upstreams
git checkout -B "${work}" --no-track "${base}"
push=$(git rev-parse --abbrev-ref --symbolic-full-name '@{push}' \
2>/dev/null) || true
if [[ "${push}" != "" ]]; then
echo "Warning: ${push} may already exist."
fi
build=$(find -name BuildInfo.cpp)
sed 's/\(^.*versionString =\).*$/\1 "'${version}'"/' ${build} >version.cpp &&
diff "${build}" version.cpp && exit 1 ||
mv -vi version.cpp ${build}
git diff
git add ${build}
git commit -S -m "Set version to ${version}"
git log --oneline --first-parent ${base}^..
cat <<PUSH
-------------------------------------------------------------------
This script will not push. Verify everything is correct, then push
to your repo, and create a PR as described in CONTRIBUTING.md.
-------------------------------------------------------------------
PUSH
+28
View File
@@ -0,0 +1,28 @@
include_guard()
set(GIT_BUILD_BRANCH "")
set(GIT_COMMIT_HASH "")
find_package(Git)
if(NOT Git_FOUND)
message(WARNING "Git not found. Git branch and commit hash will be empty.")
return()
endif()
set(GIT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/.git)
execute_process(
COMMAND
${GIT_EXECUTABLE} --git-dir=${GIT_DIRECTORY} rev-parse --abbrev-ref HEAD
OUTPUT_STRIP_TRAILING_WHITESPACE
OUTPUT_VARIABLE GIT_BUILD_BRANCH
)
execute_process(
COMMAND ${GIT_EXECUTABLE} --git-dir=${GIT_DIRECTORY} rev-parse HEAD
OUTPUT_STRIP_TRAILING_WHITESPACE
OUTPUT_VARIABLE GIT_COMMIT_HASH
)
message(STATUS "Git branch: ${GIT_BUILD_BRANCH}")
message(STATUS "Git commit hash: ${GIT_COMMIT_HASH}")
+1 -6
View File
@@ -81,7 +81,7 @@ include(target_link_modules)
add_module(xrpl beast)
target_link_libraries(xrpl.libxrpl.beast PUBLIC xrpl.imports.main)
include(XrplVersion)
include(GitInfo)
add_module(xrpl git)
target_compile_definitions(
xrpl.libxrpl.git
@@ -111,11 +111,6 @@ target_link_libraries(
xrpl.libxrpl.protocol
PUBLIC xrpl.libxrpl.crypto xrpl.libxrpl.git xrpl.libxrpl.json
)
# Only on BuildInfo.cpp, so a new version does not rebuild the whole module.
set_source_files_properties(
${CMAKE_CURRENT_SOURCE_DIR}/src/libxrpl/protocol/BuildInfo.cpp
PROPERTIES COMPILE_DEFINITIONS XRPLD_VERSION="${XRPLD_VERSION}"
)
# Level 05
add_module(xrpl protocol_autogen)
+6
View File
@@ -31,6 +31,12 @@ if(tests)
endif()
option(benchmark "Build benchmarks" ON)
# Opt-in: the Number divergence harness (xrpl.bench.number_diff) and the
# mpdecimal benchmark subjects. Requires the `bench_mpdecimal` Conan option.
option(bench_mpdecimal "Build benchmarks that need mpdecimal" OFF)
# Opt-in: Intel Decimal Floating-Point Math Library benchmark subjects. The
# library is downloaded and built at configure/build time (benchmarks only).
option(bench_intel_dfp "Build benchmarks that need Intel's decimal library" OFF)
# When OFF, the crates directory is not added to the build at all: no Rust
# toolchain is required, no cxxbridge bindings are generated, and the C++ tests
+11 -71
View File
@@ -1,75 +1,15 @@
find_package(Git)
#[===================================================================[
read version from source
#]===================================================================]
set(GIT_BUILD_BRANCH "")
set(GIT_COMMIT_HASH "")
if(DEFINED ENV{GITHUB_BRANCH_NAME})
set(GIT_BUILD_BRANCH $ENV{GITHUB_BRANCH_NAME})
set(GIT_COMMIT_HASH $ENV{GITHUB_HEAD_SHA})
elseif(Git_FOUND AND EXISTS "${CMAKE_CURRENT_LIST_DIR}/../.git")
execute_process(
COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/..
OUTPUT_VARIABLE GIT_BUILD_BRANCH
OUTPUT_STRIP_TRAILING_WHITESPACE
COMMAND_ERROR_IS_FATAL ANY
)
execute_process(
COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/..
OUTPUT_VARIABLE GIT_COMMIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
COMMAND_ERROR_IS_FATAL ANY
)
endif()
message(STATUS "Git branch: ${GIT_BUILD_BRANCH}")
message(STATUS "Git commit hash: ${GIT_COMMIT_HASH}")
if(
DEFINED ENV{FORCE_XRPLD_VERSION}
AND NOT "$ENV{FORCE_XRPLD_VERSION}" STREQUAL ""
)
message(
STATUS
"Using explicitly provided '$ENV{FORCE_XRPLD_VERSION}' as xrpld version"
)
set(XRPLD_VERSION "$ENV{FORCE_XRPLD_VERSION}")
# The rules beast::SemanticVersion::parse applies, so that an invalid version
# fails here rather than when xrpld starts. It reads each number as an int.
set(SEMVER_NUMBER "(0|[1-9][0-9]*)")
set(SEMVER_PRE_RELEASE "[A-Za-z1-9-][A-Za-z0-9-]*")
set(SEMVER_METADATA "[A-Za-z0-9-]+")
set(SEMVER_NUMBER_MAX 2147483647)
if(
NOT XRPLD_VERSION
MATCHES
"^${SEMVER_NUMBER}\\.${SEMVER_NUMBER}\\.${SEMVER_NUMBER}(-${SEMVER_PRE_RELEASE}(\\.${SEMVER_PRE_RELEASE})*)?(\\+${SEMVER_METADATA}(\\.${SEMVER_METADATA})*)?$"
OR CMAKE_MATCH_1 GREATER SEMVER_NUMBER_MAX
OR CMAKE_MATCH_2 GREATER SEMVER_NUMBER_MAX
OR CMAKE_MATCH_3 GREATER SEMVER_NUMBER_MAX
)
message(
FATAL_ERROR
"FORCE_XRPLD_VERSION '${XRPLD_VERSION}' is not a semantic version xrpld accepts, see https://semver.org"
)
file(STRINGS src/libxrpl/protocol/BuildInfo.cpp BUILD_INFO)
foreach(line_ ${BUILD_INFO})
if(line_ MATCHES "versionString[ ]*=[ ]*\"(.+)\"")
set(xrpld_version ${CMAKE_MATCH_1})
endif()
endforeach()
if(xrpld_version)
message(STATUS "xrpld version: ${xrpld_version}")
else()
message(STATUS "Using '0.0.0-dev+<git short rev>' as xrpld version")
if(GIT_COMMIT_HASH STREQUAL "")
message(
FATAL_ERROR
"Unable to determine xrpld version without git, set FORCE_XRPLD_VERSION"
)
endif()
string(SUBSTRING ${GIT_COMMIT_HASH} 0 7 GIT_COMMIT_HASH_SHORT)
set(XRPLD_VERSION "0.0.0-dev+${GIT_COMMIT_HASH_SHORT}")
message(FATAL_ERROR "unable to determine xrpld version")
endif()
message(STATUS "Build version: ${XRPLD_VERSION}")
+1
View File
@@ -14,6 +14,7 @@
"openssl/3.6.3#f806de8933e3bf6f01016c6a888cee2e%1783945160.863288",
"nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1782392402.297166",
"mpt-crypto/1.0.2#b313cef0c1a493eb970ad185b2e9bab7%1784285108.866483",
"mpdecimal/4.0.0#2fe1a7688d6c8b4bf209ea23e8b0c6ba%1736849696.519",
"lz4/1.10.0#982d9b673900f665a1da109e09c17cab%1782392402.164188",
"libiconv/1.17#9923bc6dc6f106646d6967e0039a5ada%1782392792.775744",
"libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1782392402.420732",
+3
View File
@@ -25,12 +25,15 @@ rm -f conan.lock
conan lock create . \
--options '&:jemalloc=True' \
--options '&:rocksdb=True' \
--options '&:bench_mpdecimal=True' \
--profile:all=conan/lockfile/linux.profile
conan lock create . \
--options '&:jemalloc=True' \
--options '&:rocksdb=True' \
--options '&:bench_mpdecimal=True' \
--profile:all=conan/lockfile/macos.profile
conan lock create . \
--options '&:jemalloc=True' \
--options '&:rocksdb=True' \
--options '&:bench_mpdecimal=True' \
--profile:all=conan/lockfile/windows.profile
+13 -16
View File
@@ -1,13 +1,9 @@
import os
import re
from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout
from conan.tools.env import Environment
from conan import ConanFile
DEV_VERSION = "0.0.0-dev"
class Xrpl(ConanFile):
name = "xrpl"
@@ -20,6 +16,7 @@ class Xrpl(ConanFile):
options = {
"assertions": [True, False],
"benchmark": [True, False],
"bench_mpdecimal": [True, False],
"coverage": [True, False],
"fPIC": [True, False],
"jemalloc": [True, False],
@@ -55,6 +52,7 @@ class Xrpl(ConanFile):
default_options = {
"assertions": False,
"benchmark": True,
"bench_mpdecimal": False,
"coverage": False,
"fPIC": True,
"jemalloc": False,
@@ -70,6 +68,7 @@ class Xrpl(ConanFile):
"boost/*:without_coroutine2": False,
"date/*:header_only": True,
"ed25519/*:shared": False,
"mpdecimal/*:cxx": False,
"grpc/*:shared": False,
"grpc/*:secure": True,
"grpc/*:codegen": True,
@@ -124,7 +123,13 @@ class Xrpl(ConanFile):
}
def set_version(self):
self.version = self.version or DEV_VERSION
if self.version is None:
path = f"{self.recipe_folder}/src/libxrpl/protocol/BuildInfo.cpp"
regex = r"versionString\s?=\s?\"(.*)\""
with open(path, encoding="utf-8") as file:
matches = (re.search(regex, line) for line in file)
match = next(m for m in matches if m)
self.version = match.group(1)
def configure(self):
if self.settings.compiler == "apple-clang":
@@ -135,6 +140,8 @@ class Xrpl(ConanFile):
def requirements(self):
if self.options.benchmark:
self.requires("benchmark/1.9.5")
if self.options.bench_mpdecimal:
self.requires("mpdecimal/4.0.0")
self.requires("boost/1.91.0", force=True, transitive_headers=True)
self.requires("date/3.0.4", transitive_headers=True)
if self.options.jemalloc:
@@ -167,20 +174,10 @@ class Xrpl(ConanFile):
generators = "CMakeDeps"
def generate(self):
# The sources in the Conan cache have no git history, so the version
# comes from the reference, unless it is not one, like 'develop'.
if not os.path.exists(os.path.join(self.source_folder, ".git")):
version = str(self.version)
env = Environment()
env.define(
"FORCE_XRPLD_VERSION",
version if re.match(r"\d+\.\d+\.\d+", version) else DEV_VERSION,
)
env.vars(self).save_script("xrpld_version")
tc = CMakeToolchain(self)
tc.variables["tests"] = self.options.tests
tc.variables["benchmark"] = self.options.benchmark
tc.variables["bench_mpdecimal"] = self.options.bench_mpdecimal
tc.variables["assert"] = self.options.assertions
tc.variables["coverage"] = self.options.coverage
tc.variables["jemalloc"] = self.options.jemalloc
+1 -1
View File
@@ -4,7 +4,7 @@
the `xrpld` DEB package installed on Ubuntu 26.04, running as the `xrpld` user.
Each release is tagged with its version, `xrplf/xrpld:<version>`, and
`xrplf/xrpld:develop` follows the `develop` branch.
See [`package/README.md`](../package/README.md#docker-images) for how it is built
See [`package/README.md`](../package/README.md#docker-image) for how it is built
and tagged.
```bash
+409
View File
@@ -0,0 +1,409 @@
# Number vs. decimal floating-point libraries: benchmark plan
Status: complete (phases 1-5). Goal: before extending `xrpl::Number` to a 128-bit
mantissa, (1) measure how `Number` performs against established base-10
floating-point libraries, and (2) quantify where `Number` diverges from
IEEE 754-2008/2019 decimal arithmetic.
Target precisions for a future 128-bit `Number`: **both 34 digits** (matches
IEEE decimal128) **and 38 digits** (largest `10^k - 1` that fits in a
`uint128`, the direct analogue of today's 19-digit-in-`uint64` design).
## Summary
Measured on one x86-64 machine with GCC 15.2; see "Findings so far" for
method, caveats, and every number.
**Correctness.** Today's `Number` (`Large330`) is correctly rounded for
`+ - * /` and conversion to int64 in all four rounding modes: 9.2 million of
9.2 million samples, including the int64-cap cases older scales got wrong.
Its `root2` (up to ~5 ULP) and `power` (up to ~2800 ULP for n = 360) are not
correctly rounded. No IEEE library tested has an accurate integer `power`
either, and loan payments lose only ~4 digits to it at every precision.
Boost.Decimal 1.91 has several rounding defects (see "Validating the
oracle"); Intel's library had none in `+ - * /`, `sqrt`, or int64
conversion.
**Divergence from IEEE 754.** `Number` is always normalized (no cohorts),
has no infinities, NaNs, signed zero, subnormals, or status flags, throws on
overflow and division by zero, flushes underflow to zero, has a wider
exponent range, and only ~18.96 digits of uniform precision because the
external mantissa is capped at int64. See "How Number diverges from IEEE 754
decimal".
**Speed.** Today's `Number` is 6-25x slower than 64-bit IEEE types at `+`
and `*`, and slower than Intel's 34-digit BID128 at everything except
comparison: 87 vs 12 ns for `+`, 204 vs 38 ns for `*`, parity for `/`. Most
of the cost is normalization one digit at a time (52% of instructions in
`Guard::doDropDigit<unsigned __int128>`) plus a heap-allocated error string
on every `+` and `*`, not the 64-bit width.
![Cost of one operation](images/number/number-core-ops.svg)
**Precision pays off in ledger formulas.** Each formula loses about the same
number of digits at any precision, so a 34-digit type keeps ~15 more correct
digits than today's `Number`, and 38 digits ~4 more again. The AMM swap-in is
the clearest case: 6.5 correct digits in the worst case today, against 22
(34 digits) or 26 (38 digits). 16-digit types are ruled out: they get 60-65%
of vault share conversions wrong.
![AMM swap-in: accuracy and cost](images/number/number-amm-swap-in.svg)
![360-payment amortization: accuracy and cost](images/number/number-loan-amortize360.svg)
**Implications for a 128-bit `Number`.**
- Speed is not a reason to stay at 64 bits. Intel BID128 runs every ledger
formula 2-3x faster than today's `Number` while keeping ~15 more digits.
Removing several digits per normalization step (one divide by 10^k, with
the remainder feeding the guard) and passing error locations as
`char const*` instead of `std::string` are likely the largest gains, at
any width.
- 34 vs 38 digits: mpdecimal runs both at the same speed, so the choice is
~4 extra digits against matching IEEE decimal128 exactly (and being able
to use Intel's library as a drop-in reference for testing).
- `power` needs a better algorithm (wider intermediates or a correctly
rounded method) regardless of width, if Lending needs it accurate to the
last digit.
- A prototype can be measured with no changes here: add
`include/xrpl/basics/Number128.h` (see "Measuring a Number128 prototype").
## Library shortlist
| Library | Types | Role | Import |
| -------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Boost.Decimal (Boost >= 1.91) | `decimal64_t`, `decimal128_t`, `decimal_fast64_t`, `decimal_fast128_t` | Primary competitor; header-only C++ with operator overloads. The `fast` types are always normalized (no cohorts), the same design as `Number`. | Already a dependency (`boost/1.91.0`). |
| Intel Decimal Floating-Point Math Library (libbid 2.0u4) | BID64, BID128 | IEEE reference and expected performance ceiling; backs GCC's `_Decimal*` on x86. Per-call rounding mode and status flags. | Fetched from netlib and built at build time (`-Dbench_intel_dfp=ON`); a Conan recipe if it is adopted beyond benchmarking. |
| mpdecimal (libmpdec 4.0) | arbitrary precision (configured to 19 / 34 / 38 digits) | Correctly-rounded oracle for the divergence harness; previews 34- and 38-digit results. | ConanCenter `mpdecimal/4.0.0`. |
| IBM decNumber (optional) | `decDouble`, `decQuad` | Second independent IEEE implementation, only if Boost and Intel disagree. | Vendored. |
Excluded: GCC `_Decimal*` built-ins (Intel's library underneath, not portable
to Clang), Rust `rust_decimal` (96-bit, non-IEEE, FFI distorts timings),
fixed-point libraries.
Baselines: `double`; `Number` under each `MantissaScale` (`Small`,
`LargeLegacy`, `Large330`).
## How Number diverges from IEEE 754 decimal
Items marked _(to verify)_ are hypotheses from reading the code; the
divergence harness below will confirm or refute them.
| Aspect | IEEE 754 decimal | `Number` |
| ----------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Precision | 16 digits (decimal64), 34 (decimal128) | Internal mantissa `[10^18, 10^19-1]`, but the external `mantissa()` is capped at `int64` (`kMaxRep`); mantissas above it are stored with a forced trailing zero, so precision is non-uniform (~18.96 digits). |
| Cohorts / quantum | Preserved (`1.0` and `1.00` are distinct representations) | Always normalized; one representation per value; `==` compares fields. |
| Exponent range | ±384 (decimal64), ±6144 (decimal128) | ±32768 (`int` exponent). |
| Underflow | Gradual (subnormals) | Flush to zero (`doNormalize`). |
| Overflow | ±∞ plus a status flag | Throws `std::overflow_error`. |
| Division by zero | ±∞ / NaN | Throws. |
| NaN / ∞ | Yes | None; comparisons are a plain total order. |
| Signed zero | Yes | `-0` collapses to `0`. |
| Rounding modes | 5 (incl. ties-away) | 4; thread-local global mode, similar to `fenv`. (Boost.Decimal's mode is a plain process-wide global, not thread-local.) |
| Status flags | Inexact, underflow, overflow, … | None. |
| Correct rounding of + − × ÷ √ | Required | `×` uses a 128-bit product with guard digits (likely correct). `÷` uses a fixed 10^17 scale plus a staged correction; the legacy `Small` path may not be correctly rounded _(to verify)_. `root`/`root2` (Newton iteration) and `power` (repeated squaring) are not guaranteed correctly rounded. |
| Amendment-gated variants | n/a | `Small`, `LargeLegacy` (keeps a known cusp-rounding error), `Large320`, `Large330`; all must stay bit-identical. |
| Representable range | Symmetric | −2^63 is not exactly representable. |
| Encoding | 8 / 16 bytes (BID or DPD) | 24 bytes in memory (`bool`, `uint64`, `int`, with padding); 12 bytes serialized as `STNumber` (`int64` + `int32`). |
| Other operations | fma, quantize, sameQuantum, … | None. |
## Integration
Nothing in `libxrpl` changes; all code lives under
`src/benchmarks/libxrpl/number/` and builds only when `benchmark=ON`.
- `xrpl_add_benchmark(number)` in `src/benchmarks/libxrpl/CMakeLists.txt`.
- `number/NumberLike.h`: a concept capturing the subset of `Number`'s
interface used by ledger code: construction from `(int64 mantissa, int
exponent)` and from `int64`; `+ - * /`; `< ==`; explicit conversion to
`int64`; `to_string`; `abs`; `power(x, n)`; `root2`; rounding-mode control
mapped onto `Number::RoundingMode`; and `decompose()`, which returns
`(sign, coefficient, exponent)` for digit-exact comparison.
- `number/adapters/*.h`: one thin wrapper per library, holding a single
backend value, all functions inline. `Number` satisfies the concept
directly.
- Optional backends are gated behind options that are off by default, so the
default benchmark build gains no dependencies. mpdecimal: Conan option
`bench_mpdecimal` (which also sets the CMake option of the same name).
Intel: CMake option `bench_intel_dfp`; the tarball is downloaded (SHA-256
pinned) and built with its own makefile via `ExternalProject`, with
`CALL_BY_REF=0 GLOBAL_RND=0 GLOBAL_FLAGS=0` and `-O3`.
## Benchmarks (each templated on the NumberLike type)
1. Single operations: add (equal exponents; exponents differing by 1, 5, and
18), subtract with heavy cancellation, multiply, divide, compare,
construct from `int64` / `(mantissa, exponent)`, convert to `int64`,
`to_string`, `power`, `root2`.
2. Operand sets, generated up front into arrays (never compile-time
constants; Boost.Decimal is `constexpr`, so constants would be folded
away): random full-width coefficients; IOU-like values (16 digits); XRP
drop amounts; MPT integers up to 2^63−1; wide exponent spread.
3. Throughput (independent operations) and latency (each result feeds the
next) variants.
4. Realistic formula kernels, as templated copies: AMM swap in/out
(`AMMHelpers`), Vault share/asset conversion, Lending interest and
amortization (`power`, `root`). Report speed and the size of
end-result differences.
5. Measurement discipline: Release, `-O3`, pinned core, fixed CPU frequency,
`--benchmark_repetitions=10`, report medians and variation, compare runs
with Google Benchmark's `compare.py`. Report the cost of `Number`'s
thread-local loads (`mode`, `kRange`) separately.
## Divergence harness
A separate executable, `xrpl.bench.number_diff`, runs millions of random and
edge-case operations against mpdecimal configured to match `Number`'s
precision and exponent range. It tallies results that are exact, off by
1 ULP, or worse, per operation, mantissa scale, and rounding mode. It also
checks Boost.Decimal and Intel against mpdecimal to validate the oracle, and
becomes the regression oracle for a 128-bit `Number` at 34 and 38 digits.
## Findings so far
Divergence results are from `xrpl.bench.number_diff --samples 100000` (all
four rounding modes, every mantissa scale). "Correctly rounded" means identical to the exact result
rounded onto the type's own grid of representable values.
### Validating the oracle
The harness was checked against IEEE implementations, and every disagreement
was cross-checked against both mpdecimal at the native precision and Python's
`decimal` module. The oracle agreed with both in every case; the
disagreements are Boost.Decimal 1.91 defects:
- `llrint` returns wrong values for inputs that are already integers at full
precision (e.g. `1.23e17` gives `12345678901235`), and divides by zero
(SIGFPE) when the significand exponent is exactly 0. Fixed upstream; the
adapter works around it.
- Directed rounding is ignored when an add/sub operand is far below the
other's last digit: `6111703207134976 - 5.857e-10` toward zero returns
`...976` instead of `...975` (`decimal64_t` and `decimal_fast64_t`, about
10% of IOU-dataset subtractions in `TowardsZero`).
- `decimal128_t` division misrounds near ties (1 in 2000 full-width
quotients).
- `sqrt` is not correctly rounded (errors up to ~4.5 ULP), although IEEE
requires it.
- The `fast` types are not correctly rounded for `*` and `/` (up to ~1 ULP),
as documented.
Intel's library is correctly rounded for `+ - * /`, `sqrt`, and conversion
to int64 in all four modes (9.2 million of 9.2 million samples for both
BID64 and BID128), including the cases Boost gets wrong. Its `pow` is not
correctly rounded (allowed by IEEE): up to ~1500 ULP for x^360 with x in
[1, 1.01). Example confirmed with Python's `decimal`: 1.002740590413819539^360
is `2.678516411804073475375264650425851`, Intel BID128 returns
`...650426258`. So no IEEE library here provides an accurate integer power;
Number's `power` errors are of the same order as Intel's.
### Number
- **`Large330` (current):** `+ - * /` and conversion to int64 are correctly
rounded in all four modes on every dataset, including the exponent-gap,
cancellation, tie, and int64-cap ("cusp") cases: 9.2 million of 9.2
million samples.
- **Older scales:** the harness reproduces what each amendment fixed.
`LargeLegacy` misrounds at the int64 cap in `Upward` (the documented
`fixCleanup3_2_0` defect); `LargeLegacy` and `Large320` misround at the cap
in the other modes and misround subtraction in `TowardsZero` (fixed in
`Large330`); `Small` misrounds 3-5% of divisions.
- **`root2`** is not correctly rounded in any scale: errors up to ~2.7 ULP
to nearest and ~5 ULP in directed modes.
- **`power`** accumulates error through repeated squaring without extra
precision: ~5 ULP for n = 12 and ~800-2800 ULP for n = 360. Relevant to
Lending amortization, and a design input for a 128-bit Number: wider
intermediates would remove most of it.
### Performance (phase 1)
Median ns per operation, throughput (independent operations), from
`xrpl.bench.number --benchmark_repetitions=3`. GCC 15.2, Release,
single pinned core of a 16-core x86-64 machine. CPU frequency scaling was on
(`powersave`), so treat differences under ~10% as noise; the median
coefficient of variation across benchmarks was 1.1%.
N = Number, B = `BoostDecimal`, BF = `BoostDecimalFast`, Mpd = mpdecimal at
that precision.
| op | data | N.Small | N.LargeLegacy | N.Large330 | B64 | BF64 | B128 | BF128 | Mpd19 | Mpd34 | Mpd38 | double |
| --------- | ---- | ------: | ------------: | ---------: | ---: | ---: | ---: | ----: | ----: | ----: | ----: | -----: |
| add | full | 105 | 105 | 109 | 17.6 | 21.9 | 118 | 118 | 42.1 | 26.1 | 25.7 | 0.6 |
| sub | full | 128 | 125 | 110 | 17.7 | 21.3 | 113 | 130 | 40.1 | 26.4 | 26.3 | 0.6 |
| mul | full | 192 | 210 | 248 | 9.2 | 18.0 | 77.4 | 139 | 30.3 | 31.9 | 15.7 | 0.6 |
| div | full | 40.8 | 66.2 | 70.2 | 38.6 | 44.3 | 336 | 136 | 55.4 | 54.3 | 57.0 | 0.9 |
| lt | full | 1.2 | 1.4 | 1.4 | 3.8 | 2.1 | 8.6 | 2.1 | 4.1 | 4.3 | 4.4 | 0.6 |
| construct | full | 24.3 | 19.1 | 18.4 | 4.7 | 4.8 | 1.0 | 6.5 | 12.7 | 12.7 | 12.7 | 11.0 |
| to_int64 | mpt | 26.4 | 30.1 | 34.8 | 11.7 | 4.4 | 86.4 | 56.7 | 9.4 | 9.5 | 9.5 | 1.5 |
| root2 | full | 1575 | 1734 | 2077 | 120 | 159 | 856 | 906 | 929 | 1212 | 1492 | 1.3 |
| add_gap0 | full | 48.5 | 52.1 | 34.7 | 14.4 | 20.4 | 63.7 | 56.0 | 24.3 | 18.3 | 18.3 | 0.6 |
| add_gap18 | full | 207 | 204 | 204 | 18.6 | 21.8 | 188 | 162 | 43.6 | 44.8 | 29.1 | 0.6 |
| power360 | rate | 1973 | 2503 | 2229 | 221 | 278 | 1421 | 1567 | 788 | 1259 | 1246 | 2.9 |
| mul_chain | rate | 180 | 204 | 284 | 13.1 | 25.0 | 135 | 127 | 32.7 | 36.0 | 36.7 | 0.8 |
| div_chain | rate | 47.2 | 69.9 | 103 | 42.3 | 38.3 | 350 | 130 | 54.4 | 58.0 | 60.7 | 2.7 |
Observations:
- Number's `+` and `*` are 6-25x slower than Boost `decimal64_t`, and 4-15x
slower than mpdecimal at 38 digits, even though mpdecimal is arbitrary
precision and heap-capable. Comparison is Number's only clear win.
- Under callgrind, 52% of the instructions in Number's multiply-and-add loop
are in `Guard::doDropDigit<unsigned __int128>`, which removes surplus digits
one at a time with a 128-bit divide each. `add_gap0` vs `add_gap18` (35 vs
204 ns) shows the same per-digit cost in addition. About another 10% is
`malloc`/`free`: `doRoundUp`/`doRound` take the error-location message as a
`std::string` by value, so every `*` builds one with `std::to_string`, and
every `+` builds `"Number::addition overflow"`, which is too long for the
small-string optimization.
- So Number's cost today is digit-at-a-time normalization, not mantissa
width. A 128-bit Number does not need to be slower than today's if it
removes several digits per step (divide by 10^k with a remainder for the
guard). mpdecimal at 38 digits (add 26 ns, mul 16 ns, div 57 ns) is a
realistic target.
- Boost `decimal128_t` is slower than mpdecimal at 34 digits for `+` and `/`
(`/` 336 vs 54 ns), so Boost.Decimal is not a performance reference at 128
bits. Intel BID128 is (next section).
- `mpdecimal` at 34 and 38 digits costs about the same; 38 is often faster
because 19-digit x 19-digit products fit without rounding. Precision alone
does not separate the 34- and 38-digit options on speed.
### Performance with Intel (phase 3)
A separate run (same build, same conditions) of the Intel types with three
anchor subjects repeated from the table above. The anchors moved by less than
5%, except Number's `add`, which was ~15% faster in this run; read the two
tables as consistent to within about that margin.
| op | data | N.Large330 | B64 | Mpd38 | Intel64 | Intel128 |
| --------- | ---- | ---------: | ---: | ----: | ------: | -------: |
| add | full | 89.8 | 16.3 | 25.5 | 12.3 | 11.8 |
| sub | full | 94.2 | 16.8 | 25.9 | 12.3 | 13.0 |
| mul | full | 206 | 9.1 | 15.7 | 18.3 | 37.6 |
| div | full | 65.5 | 36.0 | 57.7 | 19.7 | 64.1 |
| lt | full | 1.4 | 3.6 | 4.4 | 4.2 | 6.6 |
| construct | full | 16.6 | 4.2 | 12.6 | 11.1 | 4.0 |
| to_int64 | mpt | 27.8 | 9.4 | 9.6 | 4.1 | 4.8 |
| root2 | full | 1819 | 114 | 1491 | 14.8 | 108 |
| add_gap18 | full | 193 | 17.9 | 29.0 | 8.3 | 24.2 |
| power360 | rate | 2196 | 193 | 1275 | 259 | 1149 |
| mul_chain | rate | 197 | 13.1 | 38.9 | 19.2 | 49.7 |
| div_chain | rate | 70.0 | 41.8 | 60.8 | 28.2 | 69.2 |
Observations:
- Intel BID128 (34 digits, correctly rounded) adds in ~12 ns and multiplies
in 25-38 ns: 7-8x faster than today's 64-bit Number for `+` and 5-8x for
`*`, with division at parity. It is the realistic speed target for a
128-bit Number.
- Intel's `sqrt` is two orders of magnitude faster than Number's `root2`
(15 vs ~1800 ns at 16 digits; 108 ns at 34 digits) and correctly rounded.
### Ledger formulas (phase 4)
`number/Kernels.h` holds templated copies of production formulas, run
identically on every type, including the rounding-mode switches:
`swapAssetIn`/`swapAssetOut` (fixAMMv1_1 branch), `assetsToSharesDeposit`
and `sharesToAssetsDeposit`, and `loanPeriodicRate` plus
`loanPeriodicPayment` (fixCleanup3_2_0 branch). `loan_amortize` runs a full
monthly schedule (12 or 360 payments) and measures the leftover balance,
which is exactly 0 in exact arithmetic, relative to the principal. Inputs:
AMM pools of 10^6 to 10^12 with trades of 10^-6 to 10^-2 of the pool and
fees up to 1%; vaults of 10^9 to 10^17 integer assets with share supply up
to 10^18; loans of 10^3 to 10^9 at 0.1% to 30% a year.
ns per call
| kernel | N.Small | N.Large330 | B64 | Intel64 | double | B128 | Intel128 | Mpd34 | Mpd38 |
| ---------------------- | ------: | ---------: | ----: | ------: | -----: | ----: | -------: | ----: | ----: |
| amm_swap_in | 685 | 770 | 148 | 128 | 23 | 646 | 266 | 390 | 404 |
| amm_swap_out | 508 | 598 | 174 | 127 | 27 | 590 | 289 | 385 | 415 |
| vault_assets_to_shares | 237 | 291 | 66 | 45 | 10 | 432 | 95 | 91 | 90 |
| vault_shares_to_assets | 219 | 266 | 48 | 39 | 0.8 | 333 | 79 | 64 | 63 |
| loan_payment12 | 1870 | 2115 | 293 | 351 | 4.3 | 1679 | 1041 | 1158 | 1296 |
| loan_payment360 | 3599 | 3555 | 399 | 487 | 6.7 | 2616 | 1867 | 2454 | 2200 |
| loan_amortize12 | 5108 | 5803 | 1024 | 1055 | 16 | 5385 | 3308 | 3167 | 3091 |
| loan_amortize360 | 98902 | 114119 | 19552 | 18359 | 888 | 96989 | 58486 | 55230 | 45190 |
Correct significant digits against a 96-digit evaluation of the same
formula from the same inputs, worst case (median) over 256 cases; for
integer results, cases that differ from the exact truncated result:
| kernel | N.Small | N.Large330 | B64 | Intel64 | double | B128 | Intel128 | Mpd34 | Mpd38 |
| ---------------------- | ------------: | ----------: | ------------: | ------------: | ------------: | ----------: | ----------: | ----------: | ----------: |
| amm_swap_in | 3.3 (10.9) | 6.5 (13.9) | 3.3 (10.8) | 3.3 (10.8) | 4.0 (11.8) | 21.9 (29.7) | 21.9 (29.8) | 21.9 (29.8) | 26.3 (33.7) |
| amm_swap_out | 8.5 (11.3) | 11.4 (14.1) | 8.5 (11.2) | 8.5 (11.2) | 8.9 (11.4) | 26.9 (29.9) | 26.8 (30.0) | 26.8 (30.0) | 30.5 (33.9) |
| vault_assets_to_shares | 165/256 wrong | 0/256 wrong | 165/256 wrong | 165/256 wrong | 153/256 wrong | 0/256 wrong | 0/256 wrong | 0/256 wrong | 0/256 wrong |
| vault_shares_to_assets | 15.2 (16.0) | 18.1 (19.0) | 15.2 (16.0) | 15.2 (16.0) | 15.8 (16.3) | 33.4 (34.3) | 33.4 (34.3) | 33.4 (34.3) | 37.4 (38.2) |
| loan_payment12 | 11.9 (13.7) | 14.8 (16.7) | 11.9 (13.7) | 11.9 (13.7) | 12.4 (14.4) | 29.4 (31.7) | 29.4 (31.7) | 29.7 (31.8) | 33.7 (35.8) |
| loan_payment360 | 11.9 (15.1) | 15.0 (18.1) | 11.8 (15.2) | 11.8 (15.2) | 12.6 (15.7) | 30.0 (33.1) | 30.0 (33.1) | 30.1 (33.1) | 34.1 (37.1) |
| loan_amortize12 | 11.9 (13.7) | 14.8 (16.7) | 11.9 (13.7) | 11.9 (13.7) | 12.4 (14.3) | 29.4 (31.7) | 29.4 (31.7) | 29.7 (31.7) | 33.7 (35.7) |
| loan_amortize360 | 11.2 (12.8) | 14.1 (15.8) | 11.2 (12.8) | 11.2 (12.8) | 11.5 (13.3) | 29.1 (30.8) | 29.1 (30.8) | 29.1 (30.8) | 33.0 (34.8) |
Observations:
- Wider precision pays off directly in the results. For every formula the
34-digit types keep about 15 more correct digits than today's Number, and
38 digits adds about 4 more. The relative loss is roughly constant: each
formula loses the same number of digits at any precision, so the extra
digits carry through to the result.
- `amm_swap_in` is where it matters most: `poolOut - ratio` cancels for
small trades, leaving 6.5 correct digits in the worst case with today's
Number and about 3 with 16-digit types, against 22 (34 digits) and 26
(38 digits).
- 16-digit types get 60-65% of vault share conversions wrong, because share
counts above 10^16 do not fit. Today's Number and all wider types get every
case right. Any 64-bit IEEE type is ruled out for vaults.
- Intel BID128 runs every formula 2-3x faster than today's Number while
keeping ~15 more digits. A 128-bit Number does not have to trade speed for
precision.
- Loans lose about 4 digits at every precision, even though `power` is off
by up to thousands of ULP: the payment formula is not very sensitive to
that error.
- `Number.Small` and `Number.Large330` cost about the same.
## Phases
1. `Number` and Boost.Decimal adapters, plus the single-operation benchmarks
(no new dependencies). (Done.)
2. The divergence harness and the mpdecimal dependency, at 19, 34, and 38
digits. (Done.)
3. The Intel BID adapter, fetched and built at build time. (Done.)
4. The realistic formula kernels. (Done.)
5. Write-up with figures, and a slot for a `Number128` prototype. (Done.)
## Measuring a Number128 prototype
Add `include/xrpl/basics/Number128.h` defining `xrpl::Number128` with
Number's interface (the `DecomposableNumber` concept in
`src/benchmarks/libxrpl/number/NumberLike.h`) and
`static constexpr unsigned kDigits`. On the next build CMake notices the
header (a `CONFIGURE_DEPENDS` glob) and defines `XRPL_BENCH_NUMBER128`, and
the type appears in every single-operation and formula benchmark and in
`number_diff`, which checks it against a `kDigits`-digit grid. Nothing in
the benchmark sources needs to change. See
`src/benchmarks/libxrpl/number/Number128.h`.
## Running
```bash
# From the build directory. bench_mpdecimal is only needed for number_diff
# and the MpDecimal benchmark subjects; bench_intel_dfp downloads and builds
# Intel's library (needs network access and `make`).
conan install .. --output-folder . --build missing -s build_type=Release \
-o '&:benchmark=True' -o '&:bench_mpdecimal=True'
cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=build/generators/conan_toolchain.cmake \
-DCMAKE_BUILD_TYPE=Release -Dbench_intel_dfp=ON ..
cmake --build . --target xrpl.bench.number xrpl.bench.number_diff
./src/benchmarks/libxrpl/xrpl.bench.number --benchmark_repetitions=10 \
--benchmark_report_aggregates_only=true
./src/benchmarks/libxrpl/xrpl.bench.number_diff --samples 100000 >divergence.md
# Regenerate the figures (standard-library Python only).
./src/benchmarks/libxrpl/xrpl.bench.number --benchmark_filter='^(add|mul|div)/full/' \
--benchmark_repetitions=5 --benchmark_report_aggregates_only=true \
--benchmark_out=core.json --benchmark_out_format=json
./src/benchmarks/libxrpl/xrpl.bench.number --benchmark_filter='^kernel/' \
--benchmark_repetitions=3 --benchmark_report_aggregates_only=true \
--benchmark_out=kernels.json --benchmark_out_format=json
../src/benchmarks/libxrpl/number/plot_results.py core.json kernels.json \
../docs/images/number
```
+105
View File
@@ -0,0 +1,105 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 834 276" width="834" height="276" role="img" aria-label="AMM swap-in: correct digits (higher is better) and cost (lower is faster)">
<style>
text{font-family:system-ui, -apple-system, sans-serif;font-size:12px}
.bg{fill:#fcfcfb}.t1{fill:#0b0b0b}.t2{fill:#52514e}.grid{stroke:#e4e3df;stroke-width:1}.base{stroke:#b9b8b2;stroke-width:1}.f-number{fill:#2a78d6}.f-ieee64{fill:#eb6834}.f-wide{fill:#1baf7a}
@media (prefers-color-scheme: dark){.bg{fill:#1a1a19}.t1{fill:#ffffff}.t2{fill:#c3c2b7}.grid{stroke:#383835}.base{stroke:#5e5d58}.f-number{fill:#3987e5}.f-ieee64{fill:#d95926}.f-wide{fill:#199e70}}
</style>
<rect class="bg" width="834" height="276"/>
<text class="t1" x="0" y="18" font-size="15" font-weight="600">AMM swap-in: correct digits (higher is better) and cost (lower is faster)</text>
<rect class="f-number" x="0.0" y="32" width="12" height="12" rx="3"/>
<text class="t2" x="18.0" y="42">Number today (19 digits)</text>
<rect class="f-ieee64" x="214.8" y="32" width="12" height="12" rx="3"/>
<text class="t2" x="232.8" y="42">16-digit types</text>
<rect class="f-wide" x="357.6" y="32" width="12" height="12" rx="3"/>
<text class="t2" x="375.6" y="42">34- and 38-digit types</text>
<text class="t1" x="0" y="94.0">Number (Large330)</text>
<text class="t1" x="0" y="118.0">Boost decimal64</text>
<text class="t1" x="0" y="142.0">Intel BID64</text>
<text class="t1" x="0" y="166.0">Boost decimal128</text>
<text class="t1" x="0" y="190.0">Intel BID128</text>
<text class="t1" x="0" y="214.0">mpdecimal 34</text>
<text class="t1" x="0" y="238.0">mpdecimal 38</text>
<text class="t1" x="150" y="64" font-weight="600">Correct digits, worst case</text>
<line class="grid" x1="150.0" y1="78" x2="150.0" y2="246"/>
<text class="t2" x="150.0" y="262" text-anchor="middle" font-size="11">0</text>
<line class="grid" x1="190.0" y1="78" x2="190.0" y2="246"/>
<text class="t2" x="190.0" y="262" text-anchor="middle" font-size="11">10</text>
<line class="grid" x1="230.0" y1="78" x2="230.0" y2="246"/>
<text class="t2" x="230.0" y="262" text-anchor="middle" font-size="11">20</text>
<line class="grid" x1="270.0" y1="78" x2="270.0" y2="246"/>
<text class="t2" x="270.0" y="262" text-anchor="middle" font-size="11">30</text>
<line class="grid" x1="310.0" y1="78" x2="310.0" y2="246"/>
<text class="t2" x="310.0" y="262" text-anchor="middle" font-size="11">40</text>
<text class="t2" x="310.0" y="274" text-anchor="end" font-size="11">significant digits</text>
<path class="f-number" d="M150.0,84.0 H172.0 Q176.0,84.0 176.0,88.0 V92.0 Q176.0,96.0 172.0,96.0 H150.0 Z"><title>Number (Large330): 6.5 significant digits</title></path>
<text class="t2" x="181.0" y="94.0" font-size="11">6.5</text>
<path class="f-ieee64" d="M150.0,108.0 H159.2 Q163.2,108.0 163.2,112.0 V116.0 Q163.2,120.0 159.2,120.0 H150.0 Z"><title>Boost decimal64: 3.3 significant digits</title></path>
<text class="t2" x="168.2" y="118.0" font-size="11">3.3</text>
<path class="f-ieee64" d="M150.0,132.0 H159.2 Q163.2,132.0 163.2,136.0 V140.0 Q163.2,144.0 159.2,144.0 H150.0 Z"><title>Intel BID64: 3.3 significant digits</title></path>
<text class="t2" x="168.2" y="142.0" font-size="11">3.3</text>
<path class="f-wide" d="M150.0,156.0 H233.7 Q237.7,156.0 237.7,160.0 V164.0 Q237.7,168.0 233.7,168.0 H150.0 Z"><title>Boost decimal128: 21.9 significant digits</title></path>
<text class="t2" x="242.7" y="166.0" font-size="11">21.9</text>
<path class="f-wide" d="M150.0,180.0 H233.7 Q237.7,180.0 237.7,184.0 V188.0 Q237.7,192.0 233.7,192.0 H150.0 Z"><title>Intel BID128: 21.9 significant digits</title></path>
<text class="t2" x="242.7" y="190.0" font-size="11">21.9</text>
<path class="f-wide" d="M150.0,204.0 H233.7 Q237.7,204.0 237.7,208.0 V212.0 Q237.7,216.0 233.7,216.0 H150.0 Z"><title>mpdecimal 34: 21.9 significant digits</title></path>
<text class="t2" x="242.7" y="214.0" font-size="11">21.9</text>
<path class="f-wide" d="M150.0,228.0 H251.2 Q255.2,228.0 255.2,232.0 V236.0 Q255.2,240.0 251.2,240.0 H150.0 Z"><title>mpdecimal 38: 26.3 significant digits</title></path>
<text class="t2" x="260.2" y="238.0" font-size="11">26.3</text>
<line class="base" x1="150" y1="78" x2="150" y2="246"/>
<text class="t1" x="378" y="64" font-weight="600">Correct digits, median</text>
<line class="grid" x1="378.0" y1="78" x2="378.0" y2="246"/>
<text class="t2" x="378.0" y="262" text-anchor="middle" font-size="11">0</text>
<line class="grid" x1="418.0" y1="78" x2="418.0" y2="246"/>
<text class="t2" x="418.0" y="262" text-anchor="middle" font-size="11">10</text>
<line class="grid" x1="458.0" y1="78" x2="458.0" y2="246"/>
<text class="t2" x="458.0" y="262" text-anchor="middle" font-size="11">20</text>
<line class="grid" x1="498.0" y1="78" x2="498.0" y2="246"/>
<text class="t2" x="498.0" y="262" text-anchor="middle" font-size="11">30</text>
<line class="grid" x1="538.0" y1="78" x2="538.0" y2="246"/>
<text class="t2" x="538.0" y="262" text-anchor="middle" font-size="11">40</text>
<text class="t2" x="538.0" y="274" text-anchor="end" font-size="11">significant digits</text>
<path class="f-number" d="M378.0,84.0 H429.6 Q433.6,84.0 433.6,88.0 V92.0 Q433.6,96.0 429.6,96.0 H378.0 Z"><title>Number (Large330): 13.9 significant digits</title></path>
<text class="t2" x="438.6" y="94.0" font-size="11">13.9</text>
<path class="f-ieee64" d="M378.0,108.0 H417.3 Q421.3,108.0 421.3,112.0 V116.0 Q421.3,120.0 417.3,120.0 H378.0 Z"><title>Boost decimal64: 10.8 significant digits</title></path>
<text class="t2" x="426.3" y="118.0" font-size="11">10.8</text>
<path class="f-ieee64" d="M378.0,132.0 H417.3 Q421.3,132.0 421.3,136.0 V140.0 Q421.3,144.0 417.3,144.0 H378.0 Z"><title>Intel BID64: 10.8 significant digits</title></path>
<text class="t2" x="426.3" y="142.0" font-size="11">10.8</text>
<path class="f-wide" d="M378.0,156.0 H492.6 Q496.6,156.0 496.6,160.0 V164.0 Q496.6,168.0 492.6,168.0 H378.0 Z"><title>Boost decimal128: 29.7 significant digits</title></path>
<text class="t2" x="501.6" y="166.0" font-size="11">29.7</text>
<path class="f-wide" d="M378.0,180.0 H493.3 Q497.3,180.0 497.3,184.0 V188.0 Q497.3,192.0 493.3,192.0 H378.0 Z"><title>Intel BID128: 29.8 significant digits</title></path>
<text class="t2" x="502.3" y="190.0" font-size="11">29.8</text>
<path class="f-wide" d="M378.0,204.0 H493.3 Q497.3,204.0 497.3,208.0 V212.0 Q497.3,216.0 493.3,216.0 H378.0 Z"><title>mpdecimal 34: 29.8 significant digits</title></path>
<text class="t2" x="502.3" y="214.0" font-size="11">29.8</text>
<path class="f-wide" d="M378.0,228.0 H508.7 Q512.7,228.0 512.7,232.0 V236.0 Q512.7,240.0 508.7,240.0 H378.0 Z"><title>mpdecimal 38: 33.7 significant digits</title></path>
<text class="t2" x="517.7" y="238.0" font-size="11">33.7</text>
<line class="base" x1="378" y1="78" x2="378" y2="246"/>
<text class="t1" x="606" y="64" font-weight="600">Cost</text>
<line class="grid" x1="606.0" y1="78" x2="606.0" y2="246"/>
<text class="t2" x="606.0" y="262" text-anchor="middle" font-size="11">0</text>
<line class="grid" x1="638.0" y1="78" x2="638.0" y2="246"/>
<text class="t2" x="638.0" y="262" text-anchor="middle" font-size="11">200</text>
<line class="grid" x1="670.0" y1="78" x2="670.0" y2="246"/>
<text class="t2" x="670.0" y="262" text-anchor="middle" font-size="11">400</text>
<line class="grid" x1="702.0" y1="78" x2="702.0" y2="246"/>
<text class="t2" x="702.0" y="262" text-anchor="middle" font-size="11">600</text>
<line class="grid" x1="734.0" y1="78" x2="734.0" y2="246"/>
<text class="t2" x="734.0" y="262" text-anchor="middle" font-size="11">800</text>
<line class="grid" x1="766.0" y1="78" x2="766.0" y2="246"/>
<text class="t2" x="766.0" y="262" text-anchor="middle" font-size="11">1000</text>
<text class="t2" x="766.0" y="274" text-anchor="end" font-size="11">ns per call</text>
<path class="f-number" d="M606.0,84.0 H725.2 Q729.2,84.0 729.2,88.0 V92.0 Q729.2,96.0 725.2,96.0 H606.0 Z"><title>Number (Large330): 770 ns per call</title></path>
<text class="t2" x="734.2" y="94.0" font-size="11">770</text>
<path class="f-ieee64" d="M606.0,108.0 H625.7 Q629.7,108.0 629.7,112.0 V116.0 Q629.7,120.0 625.7,120.0 H606.0 Z"><title>Boost decimal64: 148 ns per call</title></path>
<text class="t2" x="634.7" y="118.0" font-size="11">148</text>
<path class="f-ieee64" d="M606.0,132.0 H622.4 Q626.4,132.0 626.4,136.0 V140.0 Q626.4,144.0 622.4,144.0 H606.0 Z"><title>Intel BID64: 128 ns per call</title></path>
<text class="t2" x="631.4" y="142.0" font-size="11">128</text>
<path class="f-wide" d="M606.0,156.0 H705.3 Q709.3,156.0 709.3,160.0 V164.0 Q709.3,168.0 705.3,168.0 H606.0 Z"><title>Boost decimal128: 646 ns per call</title></path>
<text class="t2" x="714.3" y="166.0" font-size="11">646</text>
<path class="f-wide" d="M606.0,180.0 H644.6 Q648.6,180.0 648.6,184.0 V188.0 Q648.6,192.0 644.6,192.0 H606.0 Z"><title>Intel BID128: 266 ns per call</title></path>
<text class="t2" x="653.6" y="190.0" font-size="11">266</text>
<path class="f-wide" d="M606.0,204.0 H664.4 Q668.4,204.0 668.4,208.0 V212.0 Q668.4,216.0 664.4,216.0 H606.0 Z"><title>mpdecimal 34: 390 ns per call</title></path>
<text class="t2" x="673.4" y="214.0" font-size="11">390</text>
<path class="f-wide" d="M606.0,228.0 H666.7 Q670.7,228.0 670.7,232.0 V236.0 Q670.7,240.0 666.7,240.0 H606.0 Z"><title>mpdecimal 38: 404 ns per call</title></path>
<text class="t2" x="675.7" y="238.0" font-size="11">404</text>
<line class="base" x1="606" y1="78" x2="606" y2="246"/>
</svg>

After

Width:  |  Height:  |  Size: 9.2 KiB

+117
View File
@@ -0,0 +1,117 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 834 324" width="834" height="324" role="img" aria-label="Cost of one operation (full-width operands, lower is faster)">
<style>
text{font-family:system-ui, -apple-system, sans-serif;font-size:12px}
.bg{fill:#fcfcfb}.t1{fill:#0b0b0b}.t2{fill:#52514e}.grid{stroke:#e4e3df;stroke-width:1}.base{stroke:#b9b8b2;stroke-width:1}.f-number{fill:#2a78d6}.f-ieee64{fill:#eb6834}.f-wide{fill:#1baf7a}
@media (prefers-color-scheme: dark){.bg{fill:#1a1a19}.t1{fill:#ffffff}.t2{fill:#c3c2b7}.grid{stroke:#383835}.base{stroke:#5e5d58}.f-number{fill:#3987e5}.f-ieee64{fill:#d95926}.f-wide{fill:#199e70}}
</style>
<rect class="bg" width="834" height="324"/>
<text class="t1" x="0" y="18" font-size="15" font-weight="600">Cost of one operation (full-width operands, lower is faster)</text>
<rect class="f-number" x="0.0" y="32" width="12" height="12" rx="3"/>
<text class="t2" x="18.0" y="42">Number today (19 digits)</text>
<rect class="f-ieee64" x="214.8" y="32" width="12" height="12" rx="3"/>
<text class="t2" x="232.8" y="42">16-digit types</text>
<rect class="f-wide" x="357.6" y="32" width="12" height="12" rx="3"/>
<text class="t2" x="375.6" y="42">34- and 38-digit types</text>
<text class="t1" x="0" y="94.0">Number (Large330)</text>
<text class="t1" x="0" y="118.0">Boost decimal64</text>
<text class="t1" x="0" y="142.0">Boost decimal_fast64</text>
<text class="t1" x="0" y="166.0">Intel BID64</text>
<text class="t1" x="0" y="190.0">Boost decimal128</text>
<text class="t1" x="0" y="214.0">Boost decimal_fast128</text>
<text class="t1" x="0" y="238.0">Intel BID128</text>
<text class="t1" x="0" y="262.0">mpdecimal 34</text>
<text class="t1" x="0" y="286.0">mpdecimal 38</text>
<text class="t1" x="150" y="64" font-weight="600">Add</text>
<line class="grid" x1="150.0" y1="78" x2="150.0" y2="294"/>
<text class="t2" x="150.0" y="310" text-anchor="middle" font-size="11">0</text>
<line class="grid" x1="190.0" y1="78" x2="190.0" y2="294"/>
<text class="t2" x="190.0" y="310" text-anchor="middle" font-size="11">100</text>
<line class="grid" x1="230.0" y1="78" x2="230.0" y2="294"/>
<text class="t2" x="230.0" y="310" text-anchor="middle" font-size="11">200</text>
<line class="grid" x1="270.0" y1="78" x2="270.0" y2="294"/>
<text class="t2" x="270.0" y="310" text-anchor="middle" font-size="11">300</text>
<line class="grid" x1="310.0" y1="78" x2="310.0" y2="294"/>
<text class="t2" x="310.0" y="310" text-anchor="middle" font-size="11">400</text>
<text class="t2" x="310.0" y="322" text-anchor="end" font-size="11">ns per operation</text>
<path class="f-number" d="M150.0,84.0 H180.9 Q184.9,84.0 184.9,88.0 V92.0 Q184.9,96.0 180.9,96.0 H150.0 Z"><title>Number (Large330): 87 ns per operation</title></path>
<text class="t2" x="189.9" y="94.0" font-size="11">87</text>
<path class="f-ieee64" d="M150.0,108.0 H152.3 Q156.3,108.0 156.3,112.0 V116.0 Q156.3,120.0 152.3,120.0 H150.0 Z"><title>Boost decimal64: 16 ns per operation</title></path>
<text class="t2" x="161.3" y="118.0" font-size="11">16</text>
<path class="f-ieee64" d="M150.0,132.0 H154.2 Q158.2,132.0 158.2,136.0 V140.0 Q158.2,144.0 154.2,144.0 H150.0 Z"><title>Boost decimal_fast64: 21 ns per operation</title></path>
<text class="t2" x="163.2" y="142.0" font-size="11">21</text>
<path class="f-ieee64" d="M150.0,156.0 H150.6 Q154.6,156.0 154.6,160.0 V164.0 Q154.6,168.0 150.6,168.0 H150.0 Z"><title>Intel BID64: 11 ns per operation</title></path>
<text class="t2" x="159.6" y="166.0" font-size="11">11</text>
<path class="f-wide" d="M150.0,180.0 H190.4 Q194.4,180.0 194.4,184.0 V188.0 Q194.4,192.0 190.4,192.0 H150.0 Z"><title>Boost decimal128: 111 ns per operation</title></path>
<text class="t2" x="199.4" y="190.0" font-size="11">111</text>
<path class="f-wide" d="M150.0,204.0 H187.2 Q191.2,204.0 191.2,208.0 V212.0 Q191.2,216.0 187.2,216.0 H150.0 Z"><title>Boost decimal_fast128: 103 ns per operation</title></path>
<text class="t2" x="196.2" y="214.0" font-size="11">103</text>
<path class="f-wide" d="M150.0,228.0 H150.7 Q154.7,228.0 154.7,232.0 V236.0 Q154.7,240.0 150.7,240.0 H150.0 Z"><title>Intel BID128: 12 ns per operation</title></path>
<text class="t2" x="159.7" y="238.0" font-size="11">12</text>
<path class="f-wide" d="M150.0,252.0 H156.2 Q160.2,252.0 160.2,256.0 V260.0 Q160.2,264.0 156.2,264.0 H150.0 Z"><title>mpdecimal 34: 26 ns per operation</title></path>
<text class="t2" x="165.2" y="262.0" font-size="11">26</text>
<path class="f-wide" d="M150.0,276.0 H156.0 Q160.0,276.0 160.0,280.0 V284.0 Q160.0,288.0 156.0,288.0 H150.0 Z"><title>mpdecimal 38: 25 ns per operation</title></path>
<text class="t2" x="165.0" y="286.0" font-size="11">25</text>
<line class="base" x1="150" y1="78" x2="150" y2="294"/>
<text class="t1" x="378" y="64" font-weight="600">Multiply</text>
<line class="grid" x1="378.0" y1="78" x2="378.0" y2="294"/>
<text class="t2" x="378.0" y="310" text-anchor="middle" font-size="11">0</text>
<line class="grid" x1="418.0" y1="78" x2="418.0" y2="294"/>
<text class="t2" x="418.0" y="310" text-anchor="middle" font-size="11">100</text>
<line class="grid" x1="458.0" y1="78" x2="458.0" y2="294"/>
<text class="t2" x="458.0" y="310" text-anchor="middle" font-size="11">200</text>
<line class="grid" x1="498.0" y1="78" x2="498.0" y2="294"/>
<text class="t2" x="498.0" y="310" text-anchor="middle" font-size="11">300</text>
<line class="grid" x1="538.0" y1="78" x2="538.0" y2="294"/>
<text class="t2" x="538.0" y="310" text-anchor="middle" font-size="11">400</text>
<text class="t2" x="538.0" y="322" text-anchor="end" font-size="11">ns per operation</text>
<path class="f-number" d="M378.0,84.0 H455.6 Q459.6,84.0 459.6,88.0 V92.0 Q459.6,96.0 455.6,96.0 H378.0 Z"><title>Number (Large330): 204 ns per operation</title></path>
<text class="t2" x="464.6" y="94.0" font-size="11">204</text>
<path class="f-ieee64" d="M378.0,108.0 H378.0 Q381.4,108.0 381.4,111.4 V116.6 Q381.4,120.0 378.0,120.0 H378.0 Z"><title>Boost decimal64: 8.5 ns per operation</title></path>
<text class="t2" x="386.4" y="118.0" font-size="11">8.5</text>
<path class="f-ieee64" d="M378.0,132.0 H380.6 Q384.6,132.0 384.6,136.0 V140.0 Q384.6,144.0 380.6,144.0 H378.0 Z"><title>Boost decimal_fast64: 17 ns per operation</title></path>
<text class="t2" x="389.6" y="142.0" font-size="11">17</text>
<path class="f-ieee64" d="M378.0,156.0 H381.3 Q385.3,156.0 385.3,160.0 V164.0 Q385.3,168.0 381.3,168.0 H378.0 Z"><title>Intel BID64: 18 ns per operation</title></path>
<text class="t2" x="390.3" y="166.0" font-size="11">18</text>
<path class="f-wide" d="M378.0,180.0 H403.1 Q407.1,180.0 407.1,184.0 V188.0 Q407.1,192.0 403.1,192.0 H378.0 Z"><title>Boost decimal128: 73 ns per operation</title></path>
<text class="t2" x="412.1" y="190.0" font-size="11">73</text>
<path class="f-wide" d="M378.0,204.0 H421.7 Q425.7,204.0 425.7,208.0 V212.0 Q425.7,216.0 421.7,216.0 H378.0 Z"><title>Boost decimal_fast128: 119 ns per operation</title></path>
<text class="t2" x="430.7" y="214.0" font-size="11">119</text>
<path class="f-wide" d="M378.0,228.0 H389.3 Q393.3,228.0 393.3,232.0 V236.0 Q393.3,240.0 389.3,240.0 H378.0 Z"><title>Intel BID128: 38 ns per operation</title></path>
<text class="t2" x="398.3" y="238.0" font-size="11">38</text>
<path class="f-wide" d="M378.0,252.0 H386.4 Q390.4,252.0 390.4,256.0 V260.0 Q390.4,264.0 386.4,264.0 H378.0 Z"><title>mpdecimal 34: 31 ns per operation</title></path>
<text class="t2" x="395.4" y="262.0" font-size="11">31</text>
<path class="f-wide" d="M378.0,276.0 H380.1 Q384.1,276.0 384.1,280.0 V284.0 Q384.1,288.0 380.1,288.0 H378.0 Z"><title>mpdecimal 38: 15 ns per operation</title></path>
<text class="t2" x="389.1" y="286.0" font-size="11">15</text>
<line class="base" x1="378" y1="78" x2="378" y2="294"/>
<text class="t1" x="606" y="64" font-weight="600">Divide</text>
<line class="grid" x1="606.0" y1="78" x2="606.0" y2="294"/>
<text class="t2" x="606.0" y="310" text-anchor="middle" font-size="11">0</text>
<line class="grid" x1="646.0" y1="78" x2="646.0" y2="294"/>
<text class="t2" x="646.0" y="310" text-anchor="middle" font-size="11">100</text>
<line class="grid" x1="686.0" y1="78" x2="686.0" y2="294"/>
<text class="t2" x="686.0" y="310" text-anchor="middle" font-size="11">200</text>
<line class="grid" x1="726.0" y1="78" x2="726.0" y2="294"/>
<text class="t2" x="726.0" y="310" text-anchor="middle" font-size="11">300</text>
<line class="grid" x1="766.0" y1="78" x2="766.0" y2="294"/>
<text class="t2" x="766.0" y="310" text-anchor="middle" font-size="11">400</text>
<text class="t2" x="766.0" y="322" text-anchor="end" font-size="11">ns per operation</text>
<path class="f-number" d="M606.0,84.0 H627.7 Q631.7,84.0 631.7,88.0 V92.0 Q631.7,96.0 627.7,96.0 H606.0 Z"><title>Number (Large330): 64 ns per operation</title></path>
<text class="t2" x="636.7" y="94.0" font-size="11">64</text>
<path class="f-ieee64" d="M606.0,108.0 H615.7 Q619.7,108.0 619.7,112.0 V116.0 Q619.7,120.0 615.7,120.0 H606.0 Z"><title>Boost decimal64: 34 ns per operation</title></path>
<text class="t2" x="624.7" y="118.0" font-size="11">34</text>
<path class="f-ieee64" d="M606.0,132.0 H616.8 Q620.8,132.0 620.8,136.0 V140.0 Q620.8,144.0 616.8,144.0 H606.0 Z"><title>Boost decimal_fast64: 37 ns per operation</title></path>
<text class="t2" x="625.8" y="142.0" font-size="11">37</text>
<path class="f-ieee64" d="M606.0,156.0 H610.0 Q614.0,156.0 614.0,160.0 V164.0 Q614.0,168.0 610.0,168.0 H606.0 Z"><title>Intel BID64: 20 ns per operation</title></path>
<text class="t2" x="619.0" y="166.0" font-size="11">20</text>
<path class="f-wide" d="M606.0,180.0 H726.2 Q730.2,180.0 730.2,184.0 V188.0 Q730.2,192.0 726.2,192.0 H606.0 Z"><title>Boost decimal128: 310 ns per operation</title></path>
<text class="t2" x="735.2" y="190.0" font-size="11">310</text>
<path class="f-wide" d="M606.0,204.0 H648.7 Q652.7,204.0 652.7,208.0 V212.0 Q652.7,216.0 648.7,216.0 H606.0 Z"><title>Boost decimal_fast128: 117 ns per operation</title></path>
<text class="t2" x="657.7" y="214.0" font-size="11">117</text>
<path class="f-wide" d="M606.0,228.0 H627.4 Q631.4,228.0 631.4,232.0 V236.0 Q631.4,240.0 627.4,240.0 H606.0 Z"><title>Intel BID128: 63 ns per operation</title></path>
<text class="t2" x="636.4" y="238.0" font-size="11">63</text>
<path class="f-wide" d="M606.0,252.0 H623.2 Q627.2,252.0 627.2,256.0 V260.0 Q627.2,264.0 623.2,264.0 H606.0 Z"><title>mpdecimal 34: 53 ns per operation</title></path>
<text class="t2" x="632.2" y="262.0" font-size="11">53</text>
<path class="f-wide" d="M606.0,276.0 H624.5 Q628.5,276.0 628.5,280.0 V284.0 Q628.5,288.0 624.5,288.0 H606.0 Z"><title>mpdecimal 38: 56 ns per operation</title></path>
<text class="t2" x="633.5" y="286.0" font-size="11">56</text>
<line class="base" x1="606" y1="78" x2="606" y2="294"/>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

@@ -0,0 +1,105 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 834 276" width="834" height="276" role="img" aria-label="360-payment amortization: correct digits (higher is better) and cost (lower is faster)">
<style>
text{font-family:system-ui, -apple-system, sans-serif;font-size:12px}
.bg{fill:#fcfcfb}.t1{fill:#0b0b0b}.t2{fill:#52514e}.grid{stroke:#e4e3df;stroke-width:1}.base{stroke:#b9b8b2;stroke-width:1}.f-number{fill:#2a78d6}.f-ieee64{fill:#eb6834}.f-wide{fill:#1baf7a}
@media (prefers-color-scheme: dark){.bg{fill:#1a1a19}.t1{fill:#ffffff}.t2{fill:#c3c2b7}.grid{stroke:#383835}.base{stroke:#5e5d58}.f-number{fill:#3987e5}.f-ieee64{fill:#d95926}.f-wide{fill:#199e70}}
</style>
<rect class="bg" width="834" height="276"/>
<text class="t1" x="0" y="18" font-size="15" font-weight="600">360-payment amortization: correct digits (higher is better) and cost (lower is faster)</text>
<rect class="f-number" x="0.0" y="32" width="12" height="12" rx="3"/>
<text class="t2" x="18.0" y="42">Number today (19 digits)</text>
<rect class="f-ieee64" x="214.8" y="32" width="12" height="12" rx="3"/>
<text class="t2" x="232.8" y="42">16-digit types</text>
<rect class="f-wide" x="357.6" y="32" width="12" height="12" rx="3"/>
<text class="t2" x="375.6" y="42">34- and 38-digit types</text>
<text class="t1" x="0" y="94.0">Number (Large330)</text>
<text class="t1" x="0" y="118.0">Boost decimal64</text>
<text class="t1" x="0" y="142.0">Intel BID64</text>
<text class="t1" x="0" y="166.0">Boost decimal128</text>
<text class="t1" x="0" y="190.0">Intel BID128</text>
<text class="t1" x="0" y="214.0">mpdecimal 34</text>
<text class="t1" x="0" y="238.0">mpdecimal 38</text>
<text class="t1" x="150" y="64" font-weight="600">Correct digits, worst case</text>
<line class="grid" x1="150.0" y1="78" x2="150.0" y2="246"/>
<text class="t2" x="150.0" y="262" text-anchor="middle" font-size="11">0</text>
<line class="grid" x1="190.0" y1="78" x2="190.0" y2="246"/>
<text class="t2" x="190.0" y="262" text-anchor="middle" font-size="11">10</text>
<line class="grid" x1="230.0" y1="78" x2="230.0" y2="246"/>
<text class="t2" x="230.0" y="262" text-anchor="middle" font-size="11">20</text>
<line class="grid" x1="270.0" y1="78" x2="270.0" y2="246"/>
<text class="t2" x="270.0" y="262" text-anchor="middle" font-size="11">30</text>
<line class="grid" x1="310.0" y1="78" x2="310.0" y2="246"/>
<text class="t2" x="310.0" y="262" text-anchor="middle" font-size="11">40</text>
<text class="t2" x="310.0" y="274" text-anchor="end" font-size="11">significant digits</text>
<path class="f-number" d="M150.0,84.0 H202.3 Q206.3,84.0 206.3,88.0 V92.0 Q206.3,96.0 202.3,96.0 H150.0 Z"><title>Number (Large330): 14.1 significant digits</title></path>
<text class="t2" x="211.3" y="94.0" font-size="11">14.1</text>
<path class="f-ieee64" d="M150.0,108.0 H190.8 Q194.8,108.0 194.8,112.0 V116.0 Q194.8,120.0 190.8,120.0 H150.0 Z"><title>Boost decimal64: 11.2 significant digits</title></path>
<text class="t2" x="199.8" y="118.0" font-size="11">11.2</text>
<path class="f-ieee64" d="M150.0,132.0 H190.8 Q194.8,132.0 194.8,136.0 V140.0 Q194.8,144.0 190.8,144.0 H150.0 Z"><title>Intel BID64: 11.2 significant digits</title></path>
<text class="t2" x="199.8" y="142.0" font-size="11">11.2</text>
<path class="f-wide" d="M150.0,156.0 H262.5 Q266.5,156.0 266.5,160.0 V164.0 Q266.5,168.0 262.5,168.0 H150.0 Z"><title>Boost decimal128: 29.1 significant digits</title></path>
<text class="t2" x="271.5" y="166.0" font-size="11">29.1</text>
<path class="f-wide" d="M150.0,180.0 H262.5 Q266.5,180.0 266.5,184.0 V188.0 Q266.5,192.0 262.5,192.0 H150.0 Z"><title>Intel BID128: 29.1 significant digits</title></path>
<text class="t2" x="271.5" y="190.0" font-size="11">29.1</text>
<path class="f-wide" d="M150.0,204.0 H262.6 Q266.6,204.0 266.6,208.0 V212.0 Q266.6,216.0 262.6,216.0 H150.0 Z"><title>mpdecimal 34: 29.1 significant digits</title></path>
<text class="t2" x="271.6" y="214.0" font-size="11">29.1</text>
<path class="f-wide" d="M150.0,228.0 H277.9 Q281.9,228.0 281.9,232.0 V236.0 Q281.9,240.0 277.9,240.0 H150.0 Z"><title>mpdecimal 38: 33.0 significant digits</title></path>
<text class="t2" x="286.9" y="238.0" font-size="11">33.0</text>
<line class="base" x1="150" y1="78" x2="150" y2="246"/>
<text class="t1" x="378" y="64" font-weight="600">Correct digits, median</text>
<line class="grid" x1="378.0" y1="78" x2="378.0" y2="246"/>
<text class="t2" x="378.0" y="262" text-anchor="middle" font-size="11">0</text>
<line class="grid" x1="418.0" y1="78" x2="418.0" y2="246"/>
<text class="t2" x="418.0" y="262" text-anchor="middle" font-size="11">10</text>
<line class="grid" x1="458.0" y1="78" x2="458.0" y2="246"/>
<text class="t2" x="458.0" y="262" text-anchor="middle" font-size="11">20</text>
<line class="grid" x1="498.0" y1="78" x2="498.0" y2="246"/>
<text class="t2" x="498.0" y="262" text-anchor="middle" font-size="11">30</text>
<line class="grid" x1="538.0" y1="78" x2="538.0" y2="246"/>
<text class="t2" x="538.0" y="262" text-anchor="middle" font-size="11">40</text>
<text class="t2" x="538.0" y="274" text-anchor="end" font-size="11">significant digits</text>
<path class="f-number" d="M378.0,84.0 H437.1 Q441.1,84.0 441.1,88.0 V92.0 Q441.1,96.0 437.1,96.0 H378.0 Z"><title>Number (Large330): 15.8 significant digits</title></path>
<text class="t2" x="446.1" y="94.0" font-size="11">15.8</text>
<path class="f-ieee64" d="M378.0,108.0 H425.4 Q429.4,108.0 429.4,112.0 V116.0 Q429.4,120.0 425.4,120.0 H378.0 Z"><title>Boost decimal64: 12.8 significant digits</title></path>
<text class="t2" x="434.4" y="118.0" font-size="11">12.8</text>
<path class="f-ieee64" d="M378.0,132.0 H425.4 Q429.4,132.0 429.4,136.0 V140.0 Q429.4,144.0 425.4,144.0 H378.0 Z"><title>Intel BID64: 12.8 significant digits</title></path>
<text class="t2" x="434.4" y="142.0" font-size="11">12.8</text>
<path class="f-wide" d="M378.0,156.0 H497.2 Q501.2,156.0 501.2,160.0 V164.0 Q501.2,168.0 497.2,168.0 H378.0 Z"><title>Boost decimal128: 30.8 significant digits</title></path>
<text class="t2" x="506.2" y="166.0" font-size="11">30.8</text>
<path class="f-wide" d="M378.0,180.0 H497.2 Q501.2,180.0 501.2,184.0 V188.0 Q501.2,192.0 497.2,192.0 H378.0 Z"><title>Intel BID128: 30.8 significant digits</title></path>
<text class="t2" x="506.2" y="190.0" font-size="11">30.8</text>
<path class="f-wide" d="M378.0,204.0 H497.2 Q501.2,204.0 501.2,208.0 V212.0 Q501.2,216.0 497.2,216.0 H378.0 Z"><title>mpdecimal 34: 30.8 significant digits</title></path>
<text class="t2" x="506.2" y="214.0" font-size="11">30.8</text>
<path class="f-wide" d="M378.0,228.0 H513.3 Q517.3,228.0 517.3,232.0 V236.0 Q517.3,240.0 513.3,240.0 H378.0 Z"><title>mpdecimal 38: 34.8 significant digits</title></path>
<text class="t2" x="522.3" y="238.0" font-size="11">34.8</text>
<line class="base" x1="378" y1="78" x2="378" y2="246"/>
<text class="t1" x="606" y="64" font-weight="600">Cost</text>
<line class="grid" x1="606.0" y1="78" x2="606.0" y2="246"/>
<text class="t2" x="606.0" y="262" text-anchor="middle" font-size="11">0</text>
<line class="grid" x1="638.0" y1="78" x2="638.0" y2="246"/>
<text class="t2" x="638.0" y="262" text-anchor="middle" font-size="11">25</text>
<line class="grid" x1="670.0" y1="78" x2="670.0" y2="246"/>
<text class="t2" x="670.0" y="262" text-anchor="middle" font-size="11">50</text>
<line class="grid" x1="702.0" y1="78" x2="702.0" y2="246"/>
<text class="t2" x="702.0" y="262" text-anchor="middle" font-size="11">75</text>
<line class="grid" x1="734.0" y1="78" x2="734.0" y2="246"/>
<text class="t2" x="734.0" y="262" text-anchor="middle" font-size="11">100</text>
<line class="grid" x1="766.0" y1="78" x2="766.0" y2="246"/>
<text class="t2" x="766.0" y="262" text-anchor="middle" font-size="11">125</text>
<text class="t2" x="766.0" y="274" text-anchor="end" font-size="11">µs per schedule</text>
<path class="f-number" d="M606.0,84.0 H748.1 Q752.1,84.0 752.1,88.0 V92.0 Q752.1,96.0 748.1,96.0 H606.0 Z"><title>Number (Large330): 114 µs per schedule</title></path>
<text class="t2" x="757.1" y="94.0" font-size="11">114</text>
<path class="f-ieee64" d="M606.0,108.0 H627.0 Q631.0,108.0 631.0,112.0 V116.0 Q631.0,120.0 627.0,120.0 H606.0 Z"><title>Boost decimal64: 20 µs per schedule</title></path>
<text class="t2" x="636.0" y="118.0" font-size="11">20</text>
<path class="f-ieee64" d="M606.0,132.0 H625.5 Q629.5,132.0 629.5,136.0 V140.0 Q629.5,144.0 625.5,144.0 H606.0 Z"><title>Intel BID64: 18 µs per schedule</title></path>
<text class="t2" x="634.5" y="142.0" font-size="11">18</text>
<path class="f-wide" d="M606.0,156.0 H726.1 Q730.1,156.0 730.1,160.0 V164.0 Q730.1,168.0 726.1,168.0 H606.0 Z"><title>Boost decimal128: 97 µs per schedule</title></path>
<text class="t2" x="735.1" y="166.0" font-size="11">97</text>
<path class="f-wide" d="M606.0,180.0 H676.9 Q680.9,180.0 680.9,184.0 V188.0 Q680.9,192.0 676.9,192.0 H606.0 Z"><title>Intel BID128: 58 µs per schedule</title></path>
<text class="t2" x="685.9" y="190.0" font-size="11">58</text>
<path class="f-wide" d="M606.0,204.0 H672.7 Q676.7,204.0 676.7,208.0 V212.0 Q676.7,216.0 672.7,216.0 H606.0 Z"><title>mpdecimal 34: 55 µs per schedule</title></path>
<text class="t2" x="681.7" y="214.0" font-size="11">55</text>
<path class="f-wide" d="M606.0,228.0 H659.8 Q663.8,228.0 663.8,232.0 V236.0 Q663.8,240.0 659.8,240.0 H606.0 Z"><title>mpdecimal 38: 45 µs per schedule</title></path>
<text class="t2" x="668.8" y="238.0" font-size="11">45</text>
<line class="base" x1="606" y1="78" x2="606" y2="246"/>
</svg>

After

Width:  |  Height:  |  Size: 9.3 KiB

+12 -12
View File
@@ -110,10 +110,10 @@ static_assert(
*
* However, it does not have sufficient precision to represent the full integer
* range of int64_t values (-2^63 to 2^63-1), which are needed for XRP and MPT
* values. The implementation of SingleAssetVault, LendingProtocol, and
* MPTokensV2 need to represent those integer values accurately and precisely,
* both for the STNumber field type, and for internal calculations. That
* necessitated the "large" scale.
* values. The implementation of SingleAssetVault, and LendingProtocol need to
* represent those integer values accurately and precisely, both for the
* STNumber field type, and for internal calculations. That necessitated the
* "large" scale.
*
* The "Large" scales are intended to represent all values that can be represented
* by an STAmount - IOUs, XRP, and MPTs. It has a min value of 10^18, and a max
@@ -134,8 +134,8 @@ struct MantissaRange final
// NOLINTBEGIN(readability-enum-initial-value)
// The values don't matter, except for Large
enum class MantissaScale {
// Small can be removed when any of featureSingleAssetVault, featureLendingProtocol, or
// featureMPTokensV2 are retired
// Small can be removed when either featureSingleAssetVault or featureLendingProtocol are
// retired
Small,
// LargeLegacy can be removed when fixCleanup3_2_0 is retired
LargeLegacy,
@@ -311,10 +311,10 @@ concept Integral64 = std::is_same_v<T, std::int64_t> || std::is_same_v<T, std::u
*
* The mantissa range may be changed at runtime via setMantissaScale(). The
* default mantissa range is "large". The range is updated whenever transaction
* processing begins, based on whether SingleAssetVault, LendingProtocol, or
* MPTokensV2 are enabled. If any is enabled, the mantissa range is set to
* "large". If not, it is set to "small", preserving backward compatibility and
* correct "amendment-gating".
* processing begins, based on whether SingleAssetVault or LendingProtocol are
* enabled. If either is enabled, the mantissa range is set to "large". If not,
* it is set to "small", preserving backward compatibility and correct
* "amendment-gating".
*
* It is extremely unlikely that any more calls to setMantissaScale() will be
* needed outside of unit tests.
@@ -344,8 +344,8 @@ concept Integral64 = std::is_same_v<T, std::int64_t> || std::is_same_v<T, std::u
* set/getMantissaScale() functions may be most appropriate. However, if the
* test has anything to do with transaction processing, it should enable or
* disable the amendments that control the mantissa range choice
* (SingleAssetVault, LendingProtocol, and MPTokensV2), and/or check if any of
* those amendments are enabled to determine which result to expect.
* (SingleAssetVault and LendingProtocol), and/or check if either of those
* amendments are enabled to determine which result to expect.
*/
class Number final
{
@@ -820,7 +820,7 @@ public:
template <class P, bool MaybeMap = IsMap>
std::conditional_t<IsMulti, iterator, std::pair<iterator, bool>>
insert(P&& value)
requires(MaybeMap && std::is_constructible_v<value_type, P&&>)
requires(MaybeMap && std::is_constructible_v<value_type, P &&>)
{
return emplace(std::forward<P>(value));
}
@@ -829,7 +829,7 @@ public:
template <class P, bool MaybeMap = IsMap>
std::conditional_t<IsMulti, iterator, std::pair<iterator, bool>>
insert(const_iterator hint, P&& value)
requires(MaybeMap && std::is_constructible_v<value_type, P&&>)
requires(MaybeMap && std::is_constructible_v<value_type, P &&>)
{
return emplaceHint(hint, std::forward<P>(value));
}
@@ -1028,7 +1028,7 @@ public:
template <class P, bool MaybeMap = IsMap>
std::conditional_t<IsMulti, iterator, std::pair<iterator, bool>>
insert(P&& value)
requires(MaybeMap && std::is_constructible_v<value_type, P&&>)
requires(MaybeMap && std::is_constructible_v<value_type, P &&>)
{
return emplace(std::forward<P>(value));
}
@@ -1037,7 +1037,7 @@ public:
template <class P, bool MaybeMap = IsMap>
std::conditional_t<IsMulti, iterator, std::pair<iterator, bool>>
insert(const_iterator hint, P&& value)
requires(MaybeMap && std::is_constructible_v<value_type, P&&>)
requires(MaybeMap && std::is_constructible_v<value_type, P &&>)
{
return emplaceHint(hint, std::forward<P>(value));
}
+1 -2
View File
@@ -5,7 +5,6 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/AccountRootEntry.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Keylet.h>
@@ -212,7 +211,7 @@ canWithdraw(
ReadView const& view,
AccountID const& from,
AccountID const& to,
AccountRootEntryR const& toSle,
SLE::ConstRef toSle,
STAmount const& amount,
bool hasDestinationTag,
std::optional<std::vector<UInt256>> const& credentialIDs = std::nullopt);
@@ -7,20 +7,9 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <cstdint>
#include <optional>
namespace xrpl {
struct Adjustment
{
std::int32_t ownerCountDelta = 0;
std::int32_t accountCountDelta = 0;
};
template <typename ViewT>
class AccountRootEntry : public SLEBase<ViewT, ltACCOUNT_ROOT>
{
@@ -38,123 +27,6 @@ public:
: Base(keylet::account(id), view, j)
{
}
/**
* Returns the account reserve, in drops.
*
* Actual owner count can be adjusted by delta in ownerCountAdj
* Actual reserve count can be adjusted by delta in accountCountAdj
* The reserve is calculated as:
* (ownerCount + "sponsoring object count" - "sponsored object count" + additionalOwnerCount) *
* increment + (1 if not sponsored account + sponsoringAccountCount) * "reserve base"
*
* @param adj Adjustment to the owner/account count (default: 0/0). Positive to add, negative
* to subtract.
* @return The account reserve amount in drops
*/
[[nodiscard]] XRPAmount
reserve(Adjustment adj = {}) const;
/**
* Return number of the objects which reserve is covered by the account (so called "owner
* count"). Actual owner count can be adjusted by delta in ownerCountAdj.
*
* @param ownerCountAdj Adjustment to the owner count (default: 0)
* @return The adjusted owner count
*/
[[nodiscard]] std::uint32_t
ownerCount(std::int32_t ownerCountAdj = 0) const;
/**
* Check if this account has sufficient reserve.
*
* @param ctx The apply-view context (view + tx)
* @param accBalance The account's balance
* @param sponsorSle The sponsor's ledger entry (if applicable)
* @param adj Adjustment to the owner/account count (default: 0/0). Positive to add, negative
* to subtract.
* @param insufReserveCode The transaction result code to return if the reserve is
* insufficient (default: tecINSUFFICIENT_RESERVE).
* @return Transaction result code
*/
[[nodiscard]] TER
checkReserve(
ApplyViewContext ctx,
XRPAmount accBalance,
std::optional<AccountRootEntry<ReadView>> const& sponsorSle,
Adjustment adj,
TER insufReserveCode = tecINSUFFICIENT_RESERVE) const;
/**
* Check if this account has sufficient reserve, deriving the sponsor internally.
*
* Equivalent to the overload above, but resolves the sponsor via
* getEffectiveTxReserveSponsor(ctx, *this) instead of taking it explicitly. Use this
* in the common case where the sponsor is simply the transaction's reserve
* sponsor for this account. Callers that must force the account's-own-reserve branch
* (passing a null sponsor) or supply a different sponsor should use the
* explicit overload above.
*
* @param ctx The apply-view context (view + tx)
* @param accBalance The account's balance
* @param adj Reserve adjustments (owner/account count deltas)
* @return Transaction result code
*/
[[nodiscard]] TER
checkReserve(ApplyViewContext ctx, XRPAmount accBalance, Adjustment adj) const;
/**
* Increase owner-count fields when the caller supplies the sponsor.
*
* This helper does not create a ledger object. It updates reserve accounting
* after the caller has created/updated an object.
* If sponsorSle is provided, this also adjusts the account's sponsored count
* and the sponsor's sponsoring count.
*
* @param sponsorSle The sponsor's ledger entry (if applicable)
* @param count Amount to add to the owner count
*/
void
increaseOwnerCount(std::optional<AccountRootEntry<ApplyView>>& sponsorSle, std::uint32_t count)
requires Base::kIsWritable;
/**
* Increase owner-count fields, deriving the tx reserve sponsor internally.
*
* Equivalent to the overload above, but resolves the sponsor via
* getEffectiveTxReserveSponsor(ctx, *this) instead of taking it explicitly. Use
* this when the sponsor is the transaction's reserve sponsor for this account
* (the common create path). Deletion paths, which derive the sponsor from an
* object's sfSponsor field, should keep using the explicit overload.
*
* @param ctx The apply-view context (view + tx)
* @param count Amount to add to the owner count
*/
void
increaseOwnerCount(ApplyViewContext ctx, std::uint32_t count)
requires Base::kIsWritable;
/**
* Decrease owner-count fields when the caller supplies the sponsor.
*
* This helper does not delete a ledger object. It updates reserve accounting
* after the caller has removed an owner-counted reserve, or for special
* owner-count changes whose sponsor cannot be derived from an object's
* sfSponsor field.
*
* @param sponsorSle The sponsor's ledger entry (if applicable)
* @param count Amount to remove from the owner count
*/
void
decreaseOwnerCount(std::optional<AccountRootEntry<ApplyView>>& sponsorSle, std::uint32_t count)
requires Base::kIsWritable;
private:
void
adjustOwnerCountSigned(
std::optional<AccountRootEntry<ApplyView>>& sponsorSle,
std::int32_t adjustment)
requires Base::kIsWritable;
};
using AccountRootEntryR = AccountRootEntry<ReadView>;
+164 -60
View File
@@ -4,8 +4,8 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/AccountRootEntry.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
@@ -46,55 +46,30 @@ isGlobalFrozen(ReadView const& view, AccountID const& issuer);
[[nodiscard]] XRPAmount
xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, beast::Journal j);
namespace detail {
struct Adjustment
{
std::int32_t ownerCountDelta = 0;
std::int32_t accountCountDelta = 0;
};
/**
* An owner count cannot be negative. If adjustment would cause a negative
* owner count, clamp the owner count at 0. Similarly for overflow. This
* adjustment allows the ownerCount to be adjusted up or down in multiple steps.
* If id != std::nullopt, then do error reporting.
* Returns the account reserve, in drops.
*
* Shared by AccountRootEntry::ownerCount() and the owner-count helpers.
* Actual owner count can be adjusted by delta in ownerCountAdj
* Actual reserve count can be adjusted by delta in accountCountAdj
* The reserve is calculated as:
* (ownerCount + "sponsoring object count" - "sponsored object count" + additionalOwnerCount) *
* increment + (1 if not sponsored account + sponsoringAccountCount) * "reserve base"
*
* @param currentOwnerCount The owner count before adjustment
* @param ownerCountAdj Positive to add to count, negative to reduce count
* @param id The account ID to use for error reporting, or std::nullopt to
* skip error reporting
* @param view The ledger view to read from
* @param sle The ledger entry for the account
* @param j Journal for logging
* @return The adjusted owner count
* @param adj Adjustment to the owner/account count (default: 0/0). Positive to add, negative to
* subtract.
* @return The account reserve amount in drops
*/
std::uint32_t
confineOwnerCount(
std::uint32_t currentOwnerCount,
std::int32_t ownerCountAdj,
std::optional<AccountID> const& id = std::nullopt,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()});
/**
* Clamps and writes an owner-count field of sle, then updates sle in view.
*
* Shared by AccountRootEntry and adjustLoanBrokerOwnerCount().
*
* @return The adjusted owner count
*/
std::uint32_t
adjustOwnerCountImpl(
ApplyView& view,
SLE::Ref sle,
SF_UINT32 const& sfield,
AccountID const& accID,
std::int32_t ownerCountAdj,
beast::Journal j);
/**
* Returns the number of account reserves funded by this account: 1 for itself
* (0 if sponsored by another account) plus the count of accounts it sponsors.
* Shared by AccountRootEntry::reserve() and xrpLiquid().
*/
std::uint32_t
accountCountImpl(AccountRootEntryR const& sle, std::int32_t accountCountAdj, beast::Journal j);
} // namespace detail
[[nodiscard]] XRPAmount
accountReserve(ReadView const& view, SLE::ConstRef sle, beast::Journal j, Adjustment adj = {});
/**
* Convenience overload that accepts AccountID instead of SLE.
@@ -109,9 +84,114 @@ accountCountImpl(AccountRootEntryR const& sle, std::int32_t accountCountAdj, bea
[[nodiscard]] inline XRPAmount
accountReserve(ReadView const& view, AccountID const& id, beast::Journal j, Adjustment adj = {})
{
return AccountRootEntryR(id, view, j).reserve(adj);
return accountReserve(view, view.read(keylet::account(id)), j, adj);
}
/**
* Check if an account has sufficient reserve.
*
* @param view The ledger view to read from
* @param tx The transaction being processed
* @param accSle The account's ledger entry
* @param accBalance The account's balance
* @param sponsorSle The sponsor's ledger entry (if applicable)
* @param adj Adjustment to the owner/account count (default: 0/0). Positive to add, negative to
* subtract.
* @param j Journal for logging (default: null sink)
* @param insufReserveCode The transaction result code to return if the reserve is insufficient
* (default: tecINSUFFICIENT_RESERVE).
* @return Transaction result code
*/
[[nodiscard]] TER
checkReserve(
ApplyViewContext ctx,
SLE::ConstRef accSle,
XRPAmount accBalance,
SLE::ConstRef sponsorSle,
Adjustment adj,
beast::Journal j,
TER insufReserveCode = tecINSUFFICIENT_RESERVE);
/**
* Check if an account has sufficient reserve, deriving the sponsor internally.
*
* Equivalent to the overload above, but resolves the sponsor via
* getEffectiveTxReserveSponsor(ctx, accSle) instead of taking it explicitly. Use this
* in the common case where the sponsor is simply the transaction's reserve
* sponsor for accSle. Callers that must force the account's-own-reserve branch
* (passing a null sponsor) or supply a different sponsor should use the
* explicit overload above.
*
* @param ctx The apply-view context (view + tx)
* @param accSle The account's ledger entry
* @param accBalance The account's balance
* @param adj Reserve adjustments (owner/account count deltas)
* @param j Journal for logging (default: null sink)
* @return Transaction result code
*/
[[nodiscard]] TER
checkReserve(
ApplyViewContext ctx,
SLE::ConstRef accSle,
XRPAmount accBalance,
Adjustment adj,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()});
/**
* Return number of the objects which reserve is covered by the account(sle) (so called "owner
* count"). Actual owner count can be adjusted by delta in ownerCountAdj.
*
* @param sle The account's ledger entry
* @param j Journal for logging
* @param ownerCountAdj Adjustment to the owner count (default: 0)
* @return The adjusted owner count
*/
std::uint32_t
ownerCount(SLE::ConstRef sle, beast::Journal j, std::int32_t ownerCountAdj = 0);
/**
* Increase owner-count fields when the caller supplies the sponsor.
*
* This helper does not create a ledger object. It updates reserve accounting
* after the caller has created/updated an object.
* If sponsorSle is provided, this also adjusts the account's sponsored count
* and the sponsor's sponsoring count.
*
* @param view The apply view for making changes
* @param accountSle The account's ledger entry
* @param sponsorSle The sponsor's ledger entry (if applicable)
* @param count Amount to add to the owner count
* @param j Journal for logging
*/
void
increaseOwnerCount(
ApplyView& view,
SLE::Ref accountSle,
SLE::Ref sponsorSle,
std::uint32_t count,
beast::Journal j);
/**
* Increase owner-count fields, deriving the tx reserve sponsor internally.
*
* Equivalent to the overload above, but resolves the sponsor via
* getEffectiveTxReserveSponsor(ctx, accountSle) instead of taking it explicitly. Use
* this when the sponsor is the transaction's reserve sponsor for accountSle
* (the common create path). Deletion paths, which derive the sponsor from an
* object's sfSponsor field, should keep using the explicit overload.
*
* @param ctx The apply-view context (view + tx)
* @param accountSle The account's ledger entry
* @param count Amount to add to the owner count
* @param j Journal for logging
*/
void
increaseOwnerCount(
ApplyViewContext ctx,
SLE::Ref accountSle,
std::uint32_t count,
beast::Journal j);
/**
* Convenience overload that accepts AccountID instead of SLE references.
*
@@ -129,13 +209,36 @@ increaseOwnerCount(
std::uint32_t count,
beast::Journal j)
{
AccountRootEntryW accountSle(account, view, j);
std::optional<AccountRootEntryW> sponsorSle;
if (sponsor)
sponsorSle.emplace(*sponsor, view);
accountSle.increaseOwnerCount(sponsorSle, count);
increaseOwnerCount(
view,
view.peek(keylet::account(account)),
sponsor ? view.peek(keylet::account(*sponsor)) : SLE::pointer(),
count,
j);
}
/**
* Decrease owner-count fields when the caller supplies the sponsor.
*
* This helper does not delete a ledger object. It updates reserve accounting
* after the caller has removed an owner-counted reserve, or for special
* owner-count changes whose sponsor cannot be derived from an object's
* sfSponsor field.
*
* @param view The apply view for making changes
* @param accountSle The account's ledger entry
* @param sponsorSle The sponsor's ledger entry (if applicable)
* @param count Amount to remove from the owner count
* @param j Journal for logging
*/
void
decreaseOwnerCount(
ApplyView& view,
SLE::Ref accountSle,
SLE::Ref sponsorSle,
std::uint32_t count,
beast::Journal j);
/**
* Convenience overload that accepts AccountID instead of SLE references.
*
@@ -153,11 +256,12 @@ decreaseOwnerCount(
std::uint32_t count,
beast::Journal j)
{
AccountRootEntryW accountSle(account, view, j);
std::optional<AccountRootEntryW> sponsorSle;
if (sponsor)
sponsorSle.emplace(*sponsor, view);
accountSle.decreaseOwnerCount(sponsorSle, count);
decreaseOwnerCount(
view,
view.peek(keylet::account(account)),
sponsor ? view.peek(keylet::account(*sponsor)) : SLE::pointer(),
count,
j);
}
/**
@@ -177,7 +281,7 @@ decreaseOwnerCount(
void
decreaseOwnerCountForObject(
ApplyView& view,
AccountRootEntryW& accountSle,
SLE::Ref accountSle,
SLE::Ref objectSle,
std::uint32_t count,
beast::Journal j);
@@ -199,7 +303,7 @@ decreaseOwnerCountForObject(
std::uint32_t count,
beast::Journal j)
{
AccountRootEntryW accountSle(account, view);
SLE::Ref accountSle = view.peek(keylet::account(account));
decreaseOwnerCountForObject(view, accountSle, objectSle, count, j);
}
@@ -263,7 +367,7 @@ getPseudoAccountFields();
* - null pointer
*/
[[nodiscard]] bool
isPseudoAccount(AccountRootEntryR const& sleAcct);
isPseudoAccount(SLE::const_pointer sleAcct);
/**
* Convenience overload that reads the account from the view.
@@ -271,7 +375,7 @@ isPseudoAccount(AccountRootEntryR const& sleAcct);
[[nodiscard]] inline bool
isPseudoAccount(ReadView const& view, AccountID const& accountId)
{
return isPseudoAccount(AccountRootEntryR(accountId, view));
return isPseudoAccount(view.read(keylet::account(accountId)));
}
/**
@@ -292,6 +396,6 @@ createPseudoAccount(ApplyView& view, UInt256 const& pseudoOwnerKey, SField const
* - If the SLE requires a destination tag, checks that there is a tag.
*/
[[nodiscard]] TER
checkDestinationAndTag(AccountRootEntryR const& toSle, bool hasDestinationTag);
checkDestinationAndTag(SLE::ConstRef toSle, bool hasDestinationTag);
} // namespace xrpl
+31 -62
View File
@@ -4,7 +4,7 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/entries/AccountRootEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
@@ -27,32 +27,6 @@
namespace xrpl {
namespace detail {
/**
* Before fixCleanup3_2_0, EscrowCancel passes the escrow SLE as sleDest.
* Return what the old reserve check returned for it, without adopting a
* non-AccountRoot SLE into an AccountRootEntry: the tx reserve sponsor's
* error if the escrow owner submitted the tx, else tefINTERNAL.
*
* @param ctx the apply view context for the escrow transaction.
* @param sleDest the non-AccountRoot SLE that was passed as the destination.
* @return the tx reserve sponsor's error code if the escrow owner submitted
* the tx, else tefINTERNAL.
*/
inline TER
escrowDestNotAccountResult(ApplyViewContext ctx, SLE::ConstRef sleDest)
{
if (sleDest->getAccountID(sfAccount) == ctx.tx[sfAccount])
{
if (auto const sponsor = getTxReserveSponsor(ctx); !sponsor)
return sponsor.error(); // LCOV_EXCL_LINE
}
return tefINTERNAL;
}
} // namespace detail
template <ValidIssueType T>
TER
escrowUnlockApplyHelper(
@@ -96,17 +70,18 @@ escrowUnlockApplyHelper<Issue>(
if (!ctx.view.exists(trustLineKey) && createAsset)
{
// Can the account cover the trust line's reserve?
if (sleDest->getType() != ltACCOUNT_ROOT)
return detail::escrowDestNotAccountResult(ctx, sleDest);
auto const destSle = AccountRootEntryR(sleDest, ctx.view, journal);
auto sponsorSle = getEffectiveTxReserveSponsor(ctx, destSle);
auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, sleDest);
if (!sponsorSle)
return sponsorSle.error(); // LCOV_EXCL_LINE
auto& sponsor = *sponsorSle;
if (auto const ret = destSle.checkReserve(
ctx, xrpBalance, sponsor, {.ownerCountDelta = 1}, tecNO_LINE_INSUF_RESERVE);
if (auto const ret = checkReserve(
ctx,
sleDest,
xrpBalance,
*sponsorSle,
{.ownerCountDelta = 1},
journal,
tecNO_LINE_INSUF_RESERVE);
!isTesSuccess(ret))
{
JLOG(journal.trace()) << "Trust line does not exist. "
@@ -119,22 +94,22 @@ escrowUnlockApplyHelper<Issue>(
initialBalance.get<Issue>().account = noAccount();
if (TER const ter = trustCreate(
ctx.view, // payment sandbox
recvLow, // is dest low?
issuer, // source
receiver, // destination
trustLineKey.key, // ledger index
sleDest, // Account to add to
false, // authorize account
!sleDest->isFlag(lsfDefaultRipple), //
false, // freeze trust line
false, // deep freeze trust line
initialBalance, // zero initial balance
Issue(currency, receiver), // limit of zero
0, // quality in
0, // quality out
sponsor ? sponsor->mutableRawSle() : SLE::pointer(), // sponsor
journal); // journal
ctx.view, // payment sandbox
recvLow, // is dest low?
issuer, // source
receiver, // destination
trustLineKey.key, // ledger index
sleDest, // Account to add to
false, // authorize account
!sleDest->isFlag(lsfDefaultRipple), //
false, // freeze trust line
false, // deep freeze trust line
initialBalance, // zero initial balance
Issue(currency, receiver), // limit of zero
0, // quality in
0, // quality out
*sponsorSle, // sponsor
journal); // journal
!isTesSuccess(ter))
{
return ter; // LCOV_EXCL_LINE
@@ -229,29 +204,23 @@ escrowUnlockApplyHelper<MPTIssue>(
auto const mptKeylet = keylet::mptoken(issuanceKey.key, receiver);
if (!ctx.view.exists(mptKeylet) && createAsset && !receiverIssuer)
{
if (sleDest->getType() != ltACCOUNT_ROOT)
return detail::escrowDestNotAccountResult(ctx, sleDest);
auto const destSle = AccountRootEntryR(sleDest, ctx.view, journal);
auto sponsorSle = getEffectiveTxReserveSponsor(ctx, destSle);
auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, sleDest);
if (!sponsorSle)
return sponsorSle.error(); // LCOV_EXCL_LINE
auto& sponsor = *sponsorSle;
if (auto const ret = destSle.checkReserve(ctx, xrpBalance, sponsor, {.ownerCountDelta = 1});
if (auto const ret = checkReserve(
ctx, sleDest, xrpBalance, *sponsorSle, {.ownerCountDelta = 1}, journal);
!isTesSuccess(ret))
return ret;
if (auto const ter = createMPToken(
ctx.view, mptID, receiver, sponsor ? sponsor->mutableRawSle() : SLE::pointer(), 0);
if (auto const ter = createMPToken(ctx.view, mptID, receiver, *sponsorSle, 0);
!isTesSuccess(ter))
{
return ter; // LCOV_EXCL_LINE
}
// update owner count.
AccountRootEntryW receiverSle(receiver, ctx.view, journal);
receiverSle.increaseOwnerCount(sponsor, 1);
increaseOwnerCount(ctx.view, sleDest, *sponsorSle, 1, journal);
}
if (!ctx.view.exists(mptKeylet) && !receiverIssuer)
+11 -12
View File
@@ -2,7 +2,6 @@
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/AccountRootEntry.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
@@ -55,11 +54,11 @@ getTxReserveSponsorID(STTx const& tx);
* Return a mutable SLE for the transaction's reserve sponsor account.
*
* @param ctx The apply-view context (view + tx)
* @return The sponsor account entry, nullopt if the tx is not
* @return The sponsor account SLE, a null pointer if the tx is not
* reserve-sponsored, or tecINTERNAL if the sponsor account cannot
* be loaded (an already-checked invariant).
*/
std::expected<std::optional<AccountRootEntryW>, TER>
std::expected<SLE::pointer, TER>
getTxReserveSponsor(ApplyViewContext ctx);
/**
@@ -67,29 +66,29 @@ getTxReserveSponsor(ApplyViewContext ctx);
*
* @param view The ledger read view
* @param tx The transaction to inspect
* @return The sponsor account entry, nullopt if the tx is not
* @return The sponsor account SLE, a null pointer if the tx is not
* reserve-sponsored, or tecINTERNAL if the sponsor account cannot
* be loaded (an already-checked invariant).
*/
std::expected<std::optional<AccountRootEntryR>, TER>
std::expected<SLE::const_pointer, TER>
getTxReserveSponsor(ReadView const& view, STTx const& tx);
/**
* The transaction's reserve sponsor for the given account, if applicable.
*
* A reserve sponsor only covers the transaction submitter's own objects, so
* this returns the tx reserve sponsor entry only when accountSle is the tx's
* own (non-pseudo) account; otherwise it returns nullopt. This is
* this returns the tx reserve sponsor SLE only when accountSle is the tx's own
* (non-pseudo) account; otherwise it returns a null sponsor pointer. This is
* the single source of truth for the "sponsor applies to tx.Account only" rule
* that the sponsor-deriving helper overloads in AccountRootHelpers rely on.
*
* @param ctx The apply-view context (view + tx)
* @param accountSle The account whose sponsor is being resolved
* @return The sponsor entry (nullopt if unsponsored), or tecINTERNAL if the
* @return The sponsor SLE (nullptr if unsponsored), or tecINTERNAL if the
* sponsor account cannot be loaded (an already-checked invariant)
*/
[[nodiscard]] std::expected<std::optional<AccountRootEntryW>, TER>
getEffectiveTxReserveSponsor(ApplyViewContext ctx, AccountRootEntryR const& accountSle);
[[nodiscard]] std::expected<SLE::pointer, TER>
getEffectiveTxReserveSponsor(ApplyViewContext ctx, SLE::ConstRef accountSle);
/**
* Return the AccountID stored in the given sponsor field of a ledger entry, or nullopt if absent.
@@ -106,9 +105,9 @@ getLedgerEntryReserveSponsorID(SLE::ConstRef sle, SF_ACCOUNT const& field = sfSp
* @param view The mutable apply view
* @param sle The ledger entry whose sponsor field is inspected
* @param field The field that holds the sponsor AccountID (defaults to sfSponsor)
* @return The sponsor account entry, or nullopt if the entry is unsponsored.
* @return The sponsor account SLE, or a null pointer if the entry is unsponsored.
*/
std::optional<AccountRootEntryW>
SLE::pointer
getLedgerEntryReserveSponsor(
ApplyView& view,
SLE::ConstRef sle,
+4 -4
View File
@@ -319,22 +319,22 @@ public:
chop(int num);
// vector-like functions
Blob::iterator
Blob ::iterator
begin()
{
return data_.begin();
}
Blob::iterator
Blob ::iterator
end()
{
return data_.end();
}
[[nodiscard]] Blob::const_iterator
[[nodiscard]] Blob ::const_iterator
begin() const
{
return data_.begin();
}
[[nodiscard]] Blob::const_iterator
[[nodiscard]] Blob ::const_iterator
end() const
{
return data_.end();
-6
View File
@@ -238,12 +238,6 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal;
TF_FLAG(tfSponsorshipEnd, 0x00010000) \
TF_FLAG(tfSponsorshipCreate, 0x00020000) \
TF_FLAG(tfSponsorshipReassign, 0x00040000), \
MASK_ADJ(0)) \
\
TRANSACTION(ConfidentialMPTHolderKeyUpdate, \
TF_FLAG(tfHolderKeyRotation, 0x00010000) \
TF_FLAG(tfHolderKeyRecovery, 0x00020000) \
TF_FLAG(tfCancelRecovery, 0x00040000), \
MASK_ADJ(0))
// clang-format on
-5
View File
@@ -12,12 +12,10 @@
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/UintTypes.h>
#include <boost/container/flat_set.hpp>
#include <cstdint>
#include <flat_set>
#include <optional>
namespace xrpl {
@@ -117,9 +115,6 @@ public:
parentBatchID_ = id;
}
[[nodiscard]] std::flat_set<MPTID>
getAffectedMPTs() const;
private:
UInt256 transactionID_;
std::uint32_t ledgerSeq_;
@@ -439,7 +439,6 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
{sfIssuerKeyMirrorEpoch, SoeOptional},
{sfAuditorKeyMirrorEpoch, SoeOptional},
{sfHolderEncryptionKey, SoeOptional},
{sfRecoveryKey, SoeOptional},
}))
/** A ledger object which tracks Oracle
@@ -330,7 +330,6 @@ TYPED_SFIELD(sfAuditorEncryptionKey, VL, 44)
TYPED_SFIELD(sfAmountCommitment, VL, 45)
TYPED_SFIELD(sfBalanceCommitment, VL, 46)
TYPED_SFIELD(sfBytecode, VL, 47)
TYPED_SFIELD(sfRecoveryKey, VL, 48)
// account (common)
TYPED_SFIELD(sfAccount, ACCOUNT, 1)
@@ -1147,27 +1147,12 @@ TRANSACTION(ttCONFIDENTIAL_MPT_MIRROR_UPDATE, 92, ConfidentialMPTMirrorUpdate,
{sfZKProof, SoeRequired},
}))
/** This transaction rotates or recovers a confidential MPT holder's ElGamal encryption key,
or cancels a recovery. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/token/ConfidentialMPTHolderKeyUpdate.h>
#endif
TRANSACTION(ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE, 93, ConfidentialMPTHolderKeyUpdate,
({.amendment = featureConfidentialMPTKeyRotation}),
({
{sfMPTokenIssuanceID, SoeRequired},
{sfHolderEncryptionKey, SoeOptional},
{sfConfidentialBalanceSpending, SoeOptional},
{sfConfidentialBalanceInbox, SoeOptional},
{sfZKProof, SoeOptional},
}))
/** This transaction posts an unsigned transaction on-ledger as a
TransactionProposal, pending multi-signature collection. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/proposal/TransactionProposalCreate.h>
#endif
TRANSACTION(ttTRANSACTION_PROPOSAL_CREATE, 95, TransactionProposalCreate,
TRANSACTION(ttTRANSACTION_PROPOSAL_CREATE, 93, TransactionProposalCreate,
({.amendment = featureCosign}),
({
{sfProposedTransaction, SoeRequired},
-1
View File
@@ -411,7 +411,6 @@ JSS(minimum_level); // out: TxQ
JSS(missingCommand); // error
JSS(mpt_amount); // out: mpt_holders
JSS(mpt_issuance_id); // in: Payment, mpt_holders
JSS(mpt_issuances); // in: Subscribe, Unsubscribe
JSS(mptoken_index); // out: mpt_holders
JSS(mpt_issuance_id_a); // out: BookChanges
JSS(mpt_issuance_id_b); // out: BookChanges
@@ -339,30 +339,6 @@ public:
{
return this->sle_->isFieldPresent(sfHolderEncryptionKey);
}
/**
* @brief Get sfRecoveryKey (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getRecoveryKey() const
{
if (hasRecoveryKey())
return this->sle_->at(sfRecoveryKey);
return std::nullopt;
}
/**
* @brief Check if sfRecoveryKey is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasRecoveryKey() const
{
return this->sle_->isFieldPresent(sfRecoveryKey);
}
};
/**
@@ -576,17 +552,6 @@ public:
return *this;
}
/**
* @brief Set sfRecoveryKey (SoeOptional)
* @return Reference to this builder for method chaining.
*/
MPTokenBuilder&
setRecoveryKey(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfRecoveryKey] = value;
return *this;
}
/**
* @brief Build and return the completed MPToken wrapper.
* @param index The ledger entry index.
@@ -1,279 +0,0 @@
// This file is auto-generated. Do not edit.
#pragma once
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/TransactionBase.h>
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
#include <xrpl/json/json_value.h>
#include <stdexcept>
#include <optional>
namespace xrpl::transactions {
class ConfidentialMPTHolderKeyUpdateBuilder;
/**
* @brief Transaction: ConfidentialMPTHolderKeyUpdate
*
* Type: ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE (93)
* Delegable: Delegation::NotDelegable
* Amendment: featureConfidentialMPTKeyRotation
* Privileges: Privilege::NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use ConfidentialMPTHolderKeyUpdateBuilder to construct new transactions.
*/
class ConfidentialMPTHolderKeyUpdate : public TransactionBase
{
public:
static constexpr xrpl::TxType txType = ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE;
/**
* @brief Construct a ConfidentialMPTHolderKeyUpdate transaction wrapper from an existing STTx object.
* @throws std::runtime_error if the transaction type doesn't match.
*/
explicit ConfidentialMPTHolderKeyUpdate(std::shared_ptr<STTx const> tx)
: TransactionBase(std::move(tx))
{
// Verify transaction type
if (tx_->getTxnType() != txType)
{
throw std::runtime_error("Invalid transaction type for ConfidentialMPTHolderKeyUpdate");
}
}
// Transaction-specific field getters
/**
* @brief Get sfMPTokenIssuanceID (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT192::type::value_type
getMPTokenIssuanceID() const
{
return this->tx_->at(sfMPTokenIssuanceID);
}
/**
* @brief Get sfHolderEncryptionKey (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getHolderEncryptionKey() const
{
if (hasHolderEncryptionKey())
{
return this->tx_->at(sfHolderEncryptionKey);
}
return std::nullopt;
}
/**
* @brief Check if sfHolderEncryptionKey is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasHolderEncryptionKey() const
{
return this->tx_->isFieldPresent(sfHolderEncryptionKey);
}
/**
* @brief Get sfConfidentialBalanceSpending (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getConfidentialBalanceSpending() const
{
if (hasConfidentialBalanceSpending())
{
return this->tx_->at(sfConfidentialBalanceSpending);
}
return std::nullopt;
}
/**
* @brief Check if sfConfidentialBalanceSpending is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasConfidentialBalanceSpending() const
{
return this->tx_->isFieldPresent(sfConfidentialBalanceSpending);
}
/**
* @brief Get sfConfidentialBalanceInbox (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getConfidentialBalanceInbox() const
{
if (hasConfidentialBalanceInbox())
{
return this->tx_->at(sfConfidentialBalanceInbox);
}
return std::nullopt;
}
/**
* @brief Check if sfConfidentialBalanceInbox is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasConfidentialBalanceInbox() const
{
return this->tx_->isFieldPresent(sfConfidentialBalanceInbox);
}
/**
* @brief Get sfZKProof (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getZKProof() const
{
if (hasZKProof())
{
return this->tx_->at(sfZKProof);
}
return std::nullopt;
}
/**
* @brief Check if sfZKProof is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasZKProof() const
{
return this->tx_->isFieldPresent(sfZKProof);
}
};
/**
* @brief Builder for ConfidentialMPTHolderKeyUpdate transactions.
*
* Provides a fluent interface for constructing transactions with method chaining.
* Uses STObject internally for flexible transaction construction.
* Inherits common field setters from TransactionBuilderBase.
*/
class ConfidentialMPTHolderKeyUpdateBuilder : public TransactionBuilderBase<ConfidentialMPTHolderKeyUpdateBuilder>
{
public:
/**
* @brief Construct a new ConfidentialMPTHolderKeyUpdateBuilder with required fields.
* @param account The account initiating the transaction.
* @param mPTokenIssuanceID The sfMPTokenIssuanceID field value.
* @param sequence Optional sequence number for the transaction.
* @param fee Optional fee for the transaction.
*/
ConfidentialMPTHolderKeyUpdateBuilder(SF_ACCOUNT::type::value_type account,
std::decay_t<typename SF_UINT192::type::value_type> const& mPTokenIssuanceID, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
)
: TransactionBuilderBase<ConfidentialMPTHolderKeyUpdateBuilder>(ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE, account, sequence, fee)
{
setMPTokenIssuanceID(mPTokenIssuanceID);
}
/**
* @brief Construct a ConfidentialMPTHolderKeyUpdateBuilder from an existing STTx object.
* @param tx The existing transaction to copy from.
* @throws std::runtime_error if the transaction type doesn't match.
*/
ConfidentialMPTHolderKeyUpdateBuilder(std::shared_ptr<STTx const> tx)
{
if (tx->getTxnType() != ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE)
{
throw std::runtime_error("Invalid transaction type for ConfidentialMPTHolderKeyUpdateBuilder");
}
object_ = *tx;
}
/**
* @brief Transaction-specific field setters
*/
/**
* @brief Set sfMPTokenIssuanceID (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTHolderKeyUpdateBuilder&
setMPTokenIssuanceID(std::decay_t<typename SF_UINT192::type::value_type> const& value)
{
object_[sfMPTokenIssuanceID] = value;
return *this;
}
/**
* @brief Set sfHolderEncryptionKey (SoeOptional)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTHolderKeyUpdateBuilder&
setHolderEncryptionKey(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfHolderEncryptionKey] = value;
return *this;
}
/**
* @brief Set sfConfidentialBalanceSpending (SoeOptional)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTHolderKeyUpdateBuilder&
setConfidentialBalanceSpending(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfConfidentialBalanceSpending] = value;
return *this;
}
/**
* @brief Set sfConfidentialBalanceInbox (SoeOptional)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTHolderKeyUpdateBuilder&
setConfidentialBalanceInbox(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfConfidentialBalanceInbox] = value;
return *this;
}
/**
* @brief Set sfZKProof (SoeOptional)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTHolderKeyUpdateBuilder&
setZKProof(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfZKProof] = value;
return *this;
}
/**
* @brief Build and return the ConfidentialMPTHolderKeyUpdate wrapper.
* @param publicKey The public key for signing.
* @param secretKey The secret key for signing.
* @return The constructed transaction wrapper.
*/
ConfidentialMPTHolderKeyUpdate
build(PublicKey const& publicKey, SecretKey const& secretKey)
{
sign(publicKey, secretKey);
return ConfidentialMPTHolderKeyUpdate{std::make_shared<STTx>(std::move(object_))};
}
};
} // namespace xrpl::transactions
@@ -18,7 +18,7 @@ class TransactionProposalCreateBuilder;
/**
* @brief Transaction: TransactionProposalCreate
*
* Type: ttTRANSACTION_PROPOSAL_CREATE (95)
* Type: ttTRANSACTION_PROPOSAL_CREATE (93)
* Delegable: Delegation::NotDelegable
* Amendment: featureCosign
* Privileges: Privilege::NoPriv
+8 -66
View File
@@ -7,7 +7,6 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Book.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/resource/Consumer.h>
#include <xrpl/server/Manifest.h>
@@ -27,11 +26,11 @@ namespace xrpl {
/**
* Maximum number of subscriptions a single client connection may hold at once.
*
* Applies to the account, real-time account, account-history, and MPT issuance
* subscriptions tracked on one InfoSub (the sets counted by
* totalSubscriptionCount), bounding the disconnect-time cleanup of those sets.
* Book subscriptions are tracked separately (OrderBookDB) and are not counted
* here. Generous enough for legitimate power users such as block explorers.
* Applies to the account, real-time account, and account-history subscriptions
* tracked on one InfoSub (the sets counted by totalSubscriptionCount), bounding
* the disconnect-time cleanup of those sets. Book subscriptions are tracked
* separately (OrderBookDB) and are not counted here. Generous enough for
* legitimate power users such as block explorers.
*/
constexpr std::size_t kMaxSubscriptionsPerConnection = 100'000;
@@ -152,21 +151,6 @@ public:
AccountID const& account,
bool historyOnly) = 0;
/**
* Remove an MPT issuance subscription during InfoSub teardown.
*
* Removes only the server-side entry from subMPT_. Does NOT touch
* InfoSub::mptSubscriptions_ because the InfoSub is being destroyed.
* Called by ~InfoSub() for each issuance in mptSubscriptions_.
*
* @param uListener The sequence number of the subscriber being torn down.
* @param mptID The MPT issuance to remove.
*
* @note Thread-safety: acquires mptLock_ internally.
*/
virtual void
unsubMPTInternal(std::uint64_t uListener, MPTID const& mptID) = 0;
/**
* Schedule the server-side teardown of a disconnecting connection's
* account subscriptions off the destructor thread.
@@ -286,11 +270,6 @@ public:
virtual bool
unsubConsensus(std::uint64_t uListener) = 0;
virtual void
subMPT(InfoSub::Ref ispListener, HashSet<MPTID> const& mptIDs) = 0;
virtual void
unsubMPT(InfoSub::Ref ispListener, HashSet<MPTID> const& mptIDs) = 0;
// VFALCO TODO Remove
// This was added for one particular partner, it
// "pushes" subscription data to a particular URL.
@@ -330,9 +309,9 @@ public:
* Return the number of subscriptions currently tracked on this
* connection.
*
* The combined size of the per-connection account, real-time account,
* account-history, and MPT issuance subscription sets. `doSubscribe` reads
* this to enforce the per-connection subscription cap before admitting more.
* The combined size of the per-connection account, real-time account, and
* account-history subscription sets. `doSubscribe` reads this to enforce
* the per-connection subscription cap before admitting more.
*
* @return The total tracked subscription count for this connection.
*
@@ -363,25 +342,6 @@ public:
HashSet<AccountID> const& normalAccounts,
std::size_t cap);
/**
* Enforce the cap and reserve a request's net-new MPT issuances, atomically.
*
* The MPT analogue of tryReserveAccountSubscriptions: under one hold of
* `lock_`, count the net-new issuances, check the total against @p cap, and
* insert them only if it fits. All-or-nothing, so a rejected request records
* nothing. Doing check and insert together stops two concurrent requests
* sharing an InfoSub (the admin subscribe-by-url path) from both passing the
* check before either records its issuances. The server-side map is
* populated afterwards by subMPT, whose re-insert is a no-op.
*
* @param mptIDs The MPT issuance ids to reserve.
* @param cap The effective per-connection cap.
* @return true if reserved; false if the request must be rejected.
* @note Thread-safe: takes `lock_`.
*/
[[nodiscard]] bool
tryReserveMPTSubscriptions(HashSet<MPTID> const& mptIDs, std::size_t cap);
/**
* Whether this connection already tracks an account-history for @p account.
*
@@ -453,29 +413,12 @@ public:
[[nodiscard]] unsigned int
getApiVersion() const noexcept;
void
insertSubMPTInfo(MPTID const& mptID);
void
deleteSubMPTInfo(MPTID const& mptID);
protected:
// Mutable so the read-only totalSubscriptionCount() accessor can lock it
// from a const method; locking semantics are otherwise unchanged.
mutable std::mutex lock_;
private:
// The lock type guarding this instance's subscription sets.
using ScopedLock = std::scoped_lock<decltype(lock_)>;
/**
* The combined tally the per-connection cap is enforced against.
*
* @param lock Proof that `lock_` is held; unused otherwise.
*/
[[nodiscard]] std::size_t
subscriptionCount(ScopedLock const& lock) const;
Consumer consumer_;
Source& source_;
HashSet<AccountID> realTimeSubscriptions_;
@@ -484,7 +427,6 @@ private:
std::uint64_t seq_;
HashSet<AccountID> accountHistorySubscriptions_;
HashSet<Book> bookSubscriptions_;
HashSet<MPTID> mptSubscriptions_;
unsigned int apiVersion_ = 0;
static int
@@ -27,18 +27,6 @@
namespace xrpl {
/**
* Returns true if sleAcct is an AccountRoot carrying a pseudo-account field.
*
* isPseudoAccount() takes a view-bound AccountRootEntry, which is not
* available while visiting entries, so invariant checks use this instead.
*
* @param sleAcct the SLE to check.
* @return true if sleAcct is an AccountRoot carrying a pseudo-account field.
*/
[[nodiscard]] bool
isPseudoAccountSle(SLE::ConstRef sleAcct);
#if GENERATING_DOCS
/**
* @brief Prototype for invariant check implementations.
+2 -8
View File
@@ -46,9 +46,6 @@ class ValidMPTIssuance
* MPTokens and RippleStates deleted during apply. finalize() checks each
* holder's AccountRoot to detect vault pseudo-account holdings deleted
* outside VaultDelete. All these checks are gated on fixCleanup3_2_0.
*
* Under fixCleanup3_5_0, finalize() also rejects any MPToken erased with
* a non-zero sfMPTAmount.
*/
std::vector<std::shared_ptr<SLE const>> deletedHoldings_;
@@ -141,9 +138,8 @@ public:
* - Convert/ConvertBack symmetry:
* Regular MPToken balance change (±X) == COA (Confidential Outstanding Amount) change (∓X)
* - Cannot delete MPToken with non-zero confidential state:
* Cannot delete if any of sfConfidentialBalanceSpending, sfConfidentialBalanceInbox,
* sfIssuerEncryptedBalance or sfAuditorEncryptedBalance is present, and the issuance's
* sfConfidentialOutstandingAmount is non-zero. Mirrors MPTokenAuthorize::preclaim.
* Cannot delete if sfIssuerEncryptedBalance exists
* Cannot delete if sfConfidentialBalanceInbox and sfConfidentialBalanceSpending exist
* - Privacy flag consistency:
* MPToken confidential balance fields can only be created or changed if
* lsfMPTCanHoldConfidentialBalance is set on the issuance.
@@ -166,8 +162,6 @@ class ValidConfidentialMPToken
std::int64_t outstandingDelta = 0;
SLE::const_pointer issuance;
bool deletedWithEncrypted = false;
// True when an erased MPToken had a non-zero pre-tx public balance.
bool deletedWithBalanceBefore = false;
bool badConsistency = false;
bool badCOA = false;
bool changesConfidentialFields = false;
+2 -3
View File
@@ -5,7 +5,6 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/AccountRootEntry.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -26,7 +25,7 @@ checkFreeze(
XRPL_ASSERT(src != dst, "xrpl::checkFreeze : unequal input accounts");
// check freeze
if (auto sle = AccountRootEntryR(dst, view))
if (auto sle = view.read(keylet::account(dst)))
{
if (sle->isFlag(lsfGlobalFreeze))
{
@@ -50,7 +49,7 @@ checkFreeze(
if (view.rules().enabled(fixFrozenLPTokenTransfer))
{
if (auto const sleDst = AccountRootEntryR(dst, view);
if (auto const sleDst = view.read(keylet::account(dst));
sleDst && sleDst->isFieldPresent(sfAMMID))
{
auto const sleAmm = view.read(keylet::amm((*sleDst)[sfAMMID]));
@@ -2,7 +2,6 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/AccountRootEntry.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/STLedgerEntry.h>
@@ -13,7 +12,6 @@
#include <xrpl/tx/Transactor.h>
#include <cstdint>
#include <optional>
namespace xrpl {
@@ -58,8 +56,8 @@ private:
Keylet const& sponsorshipKeylet,
AccountID const& sponsorID,
AccountID const& sponseeID,
AccountRootEntryW& sponsorAccSle,
std::optional<AccountRootEntryW>& reserveSponsorAccSle);
SLE::Ref sponsorAccSle,
SLE::Ref reserveSponsorAccSle);
};
} // namespace xrpl
@@ -1,84 +0,0 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/Transactor.h>
#include <cstdint>
namespace xrpl {
/**
* @brief Allows a confidential MPT holder to rotate or recover their ElGamal
* encryption key, or to cancel a pending recovery.
*
* Submitted by the holder. Exactly one of three modes must be selected via
* the transaction flags:
*
* - Rotation (tfHolderKeyRotation): the holder still has their current
* ElGamal private key. They provide a new public key along with their
* current spending/inbox balances re-encrypted under that new key. The
* holder's encryption key and confidential balances are updated
* immediately.
*
* - Recovery (tfHolderKeyRecovery): the holder has lost their current
* private key. They provide a new public key but cannot provide
* re-encrypted balances. The new key is recorded as a pending
* sfRecoveryKey; the confidential balances are left untouched. Completing
* the recovery (rewriting the balances) is done separately by the issuer
* via ConfidentialMPTRecoverBalance.
*
* - Cancel (tfCancelRecovery): the holder revokes a pending recovery
* authorization created by a prior Recovery-mode transaction. No key,
* balances, or proof are carried; authorization is via the holder's
* ordinary XRPL signature. sfRecoveryKey is removed and everything else
* is left untouched. Fails if no recovery is pending.
*
* @note Zero-knowledge proof verification for Rotation and Recovery is
* deferred to a follow-up once the mpt-crypto constructions are finalized.
*/
class ConfidentialMPTHolderKeyUpdate : public Transactor
{
public:
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
explicit ConfidentialMPTHolderKeyUpdate(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
void
visitInvariantEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after) override;
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
TER result,
XRPAmount fee,
ReadView const& view,
beast::Journal const& j) override;
};
} // namespace xrpl
-4
View File
@@ -26,10 +26,6 @@ The store paths carry their derivation hash, so they change whenever a tool is
rebuilt — a `flake.lock` update generally rewrites most of them even when no
version moves. That is deliberate: it makes tooling changes visible in review.
Tools scoped to a single dev shell rather than to `commonPackages` do not appear
here: `check-tools.sh` keys those off `XRPL_DEVSHELL`, and none of the three
environments above is such a shell. Adding to one needs no snapshot update.
## Regenerating
The two Linux snapshots come from the `nix-ubuntu` image (Docker or a compatible
-13
View File
@@ -147,19 +147,6 @@ rec {
versionedTools = clangVersionedTools;
};
# The gcc shell plus the Lean4 formal verification toolchain
formal-verification = makeShell {
shellName = "formal-verification";
stdenv = customGccStdenv;
compilerName = "gcc";
version = gccVersion;
versionedTools = gccVersionedTools;
extraPackages = [
customGccGcov
pkgs.lean4
];
};
# Nix provides no compiler; use the one from your system (e.g. Apple Clang).
no-compiler = makeShell {
shellName = "no-compiler";
+49 -68
View File
@@ -10,12 +10,11 @@ a build configured with `-Dvalidator_keys=ON`.
package/
build_pkg.py Staging and build script (called by the CMake `package` target and CI)
sign_rpm.py Signs the built RPMs (called by CI when publishing)
images/
packaging/
Dockerfile Packaging image, built by `build-packaging-images.yml`; installs its tooling with `bin/install-packaging-tools.sh`
publish_pkg.py Uploads built packages to the XRPLF Nexus repositories (called by CI, and shipped in that image)
xrpld/
Dockerfile The xrpld Docker images, installing the built DEB on Ubuntu (see "Docker images")
docker/
Dockerfile Packaging image, built by `build-packaging-images.yml`; installs its tooling with `bin/install-packaging-tools.sh`
publish_pkg.py Uploads built packages to the XRPLF Nexus repositories (called by CI, and shipped in that image)
image/
Dockerfile The xrpld Docker image, installing the built DEB on Ubuntu (see "Docker image")
rpm/
xrpld.spec RPM spec
debian/ Debian control files (control.in, lintian-overrides.in, rules, copyright, docs, links, source/format).
@@ -120,8 +119,8 @@ Caller workflows (`on-pr.yml`, `on-tag.yml`, `on-trigger.yml`) call
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.
4. `docker` builds the [Docker images](#docker-images) from the tested DEB, and
pushes them when publishing.
4. `docker` builds the [Docker image](#docker-image) from the tested DEB, and
pushes it when publishing.
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
@@ -210,37 +209,32 @@ rejects request bodies over 100 MB, so uploads use the DNS-only host instead.
The `release-info` action decides the channel from the event, and
`publish_pkg.py` maps that channel to its repositories:
| Event | Version | Channel | DEB repository | RPM upload repository |
| ------------------------ | ------------------ | --------- | -------------- | --------------------- |
| tag | `X.Y.Z` | `stable` | `deb-stable` | `rpm-stable-hosted` |
| tag | `X.Y.Z-rcN` | `rc` | `deb-rc` | `rpm-rc-hosted` |
| tag | `X.Y.Z-bN` | `beta` | `deb-beta` | `rpm-beta-hosted` |
| tag, any other | `xrpld --version` | `custom` | `deb-custom` | `rpm-custom-hosted` |
| push to `develop` | `0.0.0-dev+<hash>` | `develop` | `deb-develop` | `rpm-develop-hosted` |
| tag, non-public codebase | _any but `X.Y.Z`_ | `private` | `deb-private` | `rpm-private-hosted` |
| Event | Version | Channel | DEB repository | RPM upload repository |
| ------------------------ | ----------------- | --------- | -------------- | --------------------- |
| tag | `X.Y.Z` | `stable` | `deb-stable` | `rpm-stable-hosted` |
| tag | `X.Y.Z-rcN` | `rc` | `deb-rc` | `rpm-rc-hosted` |
| tag | `X.Y.Z-bN` | `beta` | `deb-beta` | `rpm-beta-hosted` |
| tag, any other | `xrpld --version` | `custom` | `deb-custom` | `rpm-custom-hosted` |
| 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 picks a release channel. A final release, `X.Y.Z`, goes to `stable`
even from a non-public codebase. Versions sort in row order, so moving to a
more mature channel never downgrades. A tag matching none of the release
patterns, such as `X.Y.Z-hotfix1`, publishes to `custom`, which sits outside
that order.
Only a tag names a channel — do not extend that to `develop`, where
`BuildInfo.cpp`'s `versionString` moves through `-bN`, `-rcN` and even the final
version during a release cycle, which would send develop builds into `stable`.
Versions sort in row order, so moving to a more mature channel never downgrades.
A tag matching none of the release patterns, such as `X.Y.Z-hotfix1`, publishes
to `custom`, which sits outside that order.
Every untagged build reports `0.0.0-dev+<hash>`, which the `develop` channel
packages as `0.0.0~dev`, below every release.
The action also picks the package release number:
- A tag: `1`, since a tag's version is never reused.
- Anything else: `<run number>.<commit date>git<commit hash>`, e.g.
`857.20260826gitb6a8995`. Develop packages all share `0.0.0~dev`, so the run
number orders them, and the date and hash name the commit a package on
`packages.xrplf.org` came from.
Both reach the packaging scripts as arguments, so neither script derives
anything itself.
The action decides the package release number on the same split: a tag's version
is unique, so its packages are release 1, while develop repeats the same version
and takes `<run number>.<commit date>git<commit hash>`, e.g.
`857.20260826gitb6a8995` — the leading run number keeps each push superseding
the last, and the date and hash say which commit a package on
`packages.xrplf.org` came from. Both reach the packaging scripts as arguments,
so neither script derives anything itself.
Publishing is 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
@@ -277,23 +271,14 @@ Nexus owns the repository metadata; nothing here indexes anything. Worth knowing
installs it at `/usr/local/bin/publish_pkg.py` for other XRPLF repositories that
build their packages elsewhere.
## Docker images
## Docker image
The `docker` job installs the tested `xrpld` DEB on `ubuntu:26.04` using
[`images/xrpld/Dockerfile`](images/xrpld/Dockerfile), once per target, and
checks that the server starts in each image. A tag's images are tagged with the
tag name, a develop image as `develop`. With `publish: true`:
- `xrpld` is pushed to `xrplf/xrpld` on Docker Hub using the `DOCKERHUB_TOKEN`
secret, an organization access token for `xrplf`. Builds of a non-public
codebase are pushed there only for `stable` releases, whose packages are
public too.
- `voidstar` replaces `/usr/bin/xrpld` with the binary of the `voidstar` build
config, adds `libvoidstar.so` and links the binary into `/symbols`, as
Antithesis expects. It is pushed as `xrpld-voidstar` to the Antithesis
registry, `${ANTITHESIS_DOCKER_HOST}/${ANTITHESIS_DOCKER_PATH}`, logging in
with the `ANTITHESIS_DOCKER_CREDENTIALS` service account key. The registry is
private, so private builds are pushed too.
[`image/Dockerfile`](image/Dockerfile), checks that the server starts, and,
with `publish: true`, pushes it to `xrplf/xrpld` on Docker Hub using the
`DOCKERHUB_TOKEN` secret, an organization access token for `xrplf`. A tag's
image is tagged with the tag name, `xrplf/xrpld:<version>`, and a develop image
as `xrplf/xrpld:develop`. Private builds are never pushed.
## How `build_pkg.py` works
@@ -322,30 +307,26 @@ values; DEB writes them as `${pkg_version}-${PKG_RELEASE}` in
With `PKG_RELEASE=1`, the package metadata becomes:
| Input version | RPM version/release | Debian version |
| ------------------- | -------------------------- | ------------------ |
| `3.2.0` | `3.2.0-1%{?dist}` | `3.2.0-1` |
| `3.2.0-b1` | `3.2.0~b1-1%{?dist}` | `3.2.0~b1-1` |
| `3.2.0-rc1` | `3.2.0~rc1-1%{?dist}` | `3.2.0~rc1-1` |
| `3.2.0-custom-1` | `3.2.0~custom.1-1%{?dist}` | `3.2.0~custom.1-1` |
| `0.0.0-dev+abc1234` | `0.0.0~dev-1%{?dist}` | `0.0.0~dev-1` |
| Input version | RPM version/release | Debian version |
| ------------------ | ---------------------------- | -------------------- |
| `3.2.0` | `3.2.0-1%{?dist}` | `3.2.0-1` |
| `3.2.0-b0+abc1234` | `3.2.0~b0+abc1234-1%{?dist}` | `3.2.0~b0+abc1234-1` |
| `3.2.0-b1` | `3.2.0~b1-1%{?dist}` | `3.2.0~b1-1` |
| `3.2.0-rc1` | `3.2.0~rc1-1%{?dist}` | `3.2.0~rc1-1` |
| `3.2.0-custom-1` | `3.2.0~custom.1-1%{?dist}` | `3.2.0~custom.1-1` |
`build_pkg.py` defines `dist` as `.el9` rather than letting rpmbuild take it
from the build host, so the RHEL image can track a newer release without
changing what the packages claim to target.
The Debian changelog entry carries the channel passed as `--channel`: one of
the channels in the table above, or `UNRELEASED`, the Debian convention for a
build that targets no channel, which local and CMake builds pass. Each channel
accepts:
- `stable`, `rc`, `beta`: `X.Y.Z`, or a `bN`/`rcN` pre-release, the only kind
that may carry build metadata. `3.2.0-b1` packages as `3.2.0~b1`.
- `custom`, `private`: any version, with each `-` inside the pre-release or
build metadata switched to `.`. `3.4.0-custom-1` packages as `3.4.0~custom.1`.
- `develop`: only `0.0.0-dev`, without its build metadata. `0.0.0-dev+abc1234`
packages as `0.0.0~dev`.
- `UNRELEASED`: `0.0.0-dev` as `develop` does, anything else as `stable` does.
The Debian changelog entry carries the channel passed as `--channel`, which
only accepts the channels in the table above plus `UNRELEASED`, the Debian
convention for a build that targets no channel at all — what local and CMake
builds pass, since nothing publishes them. An unsupported pre-release, and
build metadata on a final release such as `3.2.0+abc123`, are both rejected,
except in the `custom` and `private` channels, which accept any version and
switch each `-` inside the pre-release or build metadata to `.`, so
`3.4.0-custom-1` packages as `3.4.0~custom.1`.
The RPM path intentionally uses `~` in `Version`, matching the Debian
pre-release ordering convention, so RPM filenames/NVRs begin with forms like
+4 -20
View File
@@ -24,12 +24,6 @@ PRE_RELEASE = re.compile(r"^(b|rc)(0|[1-9][0-9]*)$")
# Channels a tag of any version is published to, rather than only bN/rcN.
ANY_VERSION_CHANNELS = ("custom", "private")
# The version of any untagged build, which is all the develop channel publishes.
DEV_VERSION = "0.0.0-dev"
# Channels an untagged build is packaged for: develop, or none for a local build.
DEV_VERSION_CHANNELS = ("develop", "UNRELEASED")
# 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"
@@ -80,23 +74,13 @@ def capture(*command: object) -> str:
def package_version(reported: str, channel: str) -> str:
"""Normalise a reported version into one the package formats accept.
- Release: unchanged (3.2.0 -> 3.2.0).
- bN/rcN pre-release: '-' becomes '~', to sort before the release
(3.2.0-b1 -> 3.2.0~b1).
- custom, private: any version, '-' inside the pre-release and build
metadata becomes '.' (3.4.0-custom-1 -> 3.4.0~custom.1).
- develop, UNRELEASED: 0.0.0-dev without build metadata, so builds sort by
package release, not commit hash (0.0.0-dev+abc1234 -> 0.0.0~dev).
develop accepts nothing else.
A pre-release switches to '~' (3.2.0-b1 -> 3.2.0~b1), which also sorts before
the final 3.2.0; a no-op for a final release. The custom and private
channels accept any pre-release and build metadata, with any '-' inside
either switched to '.' (3.4.0-custom-1 -> 3.4.0~custom.1).
"""
# Metadata first, as it may contain '-' too.
release, plus, metadata = reported.partition("+")
if release == DEV_VERSION and channel in DEV_VERSION_CHANNELS:
return DEV_VERSION.replace("-", "~")
assert channel != "develop", (
f"unsupported version {reported!r}: "
f"the develop channel only accepts {DEV_VERSION}."
)
base, _, pre_release = release.partition("-")
if channel in ANY_VERSION_CHANNELS:
pre_release = pre_release.replace("-", ".")
+10
View File
@@ -0,0 +1,10 @@
ARG BASE_IMAGE=debian:trixie
FROM ${BASE_IMAGE}
# Bind-mounted rather than copied in, so the installer never lands in a layer.
RUN --mount=type=bind,source=bin/install-packaging-tools.sh,target=/install-packaging-tools.sh \
/install-packaging-tools.sh
# See ../README.md, "Publishing from other repositories".
COPY package/docker/publish_pkg.py /usr/local/bin/publish_pkg.py
File renamed without changes.
+15
View File
@@ -0,0 +1,15 @@
FROM ubuntu:26.04
RUN --mount=type=bind,target=/tmp/package \
apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends /tmp/package/*.deb \
&& rm -rf /var/lib/apt/lists/* \
&& ln -sf /dev/stdout /var/log/xrpld/debug.log
USER xrpld
WORKDIR /var/lib/xrpld
EXPOSE 2459 5005 6006 50051
ENTRYPOINT ["/usr/bin/xrpld"]
CMD ["--net", "--silent", "--conf", "/etc/xrpld/xrpld.cfg"]
-9
View File
@@ -1,9 +0,0 @@
ARG BASE_IMAGE=debian:trixie
FROM ${BASE_IMAGE}
RUN --mount=type=bind,source=bin/install-packaging-tools.sh,target=/install-packaging-tools.sh \
/install-packaging-tools.sh
# See package/README.md, "Publishing from other repositories".
COPY package/images/packaging/publish_pkg.py /usr/local/bin/publish_pkg.py
-31
View File
@@ -1,31 +0,0 @@
FROM ubuntu:26.04 AS xrpld
RUN --mount=type=bind,target=/tmp/package \
apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends /tmp/package/*.deb \
&& rm -rf /var/lib/apt/lists/* \
&& ln -sf /dev/stdout /var/log/xrpld/debug.log
USER xrpld
WORKDIR /var/lib/xrpld
EXPOSE 2459 5005 6006 50051
ENTRYPOINT ["/usr/bin/xrpld"]
CMD ["--net", "--silent", "--conf", "/etc/xrpld/xrpld.cfg"]
# The same image with the Antithesis-instrumented binary, which loads
# libvoidstar.so; Antithesis reads the debug symbols from /symbols.
FROM xrpld AS voidstar
USER root
ADD --chmod=644 --checksum=sha256:d080cc85f1d8d96452bdaf0021a10fa3e4cc3c319840f1fadd2d33904e607095 \
https://antithesis.com/assets/instrumentation/libvoidstar.so /usr/lib/libvoidstar.so
RUN --mount=type=bind,source=xrpld,target=/tmp/xrpld \
install -m 755 /tmp/xrpld /usr/bin/xrpld \
&& mkdir /symbols \
&& ln -s /usr/bin/xrpld /symbols/xrpld
USER xrpld
+100
View File
@@ -20,3 +20,103 @@ target_link_libraries(
xrpl_add_benchmark(nodestore)
target_link_libraries(xrpl.bench.nodestore PRIVATE xrpl.imports.bench)
add_dependencies(xrpl.benchmarks xrpl.bench.nodestore)
# Number vs. decimal floating-point libraries. See docs/NumberDecimalBenchmark.md.
# Boost.Decimal is header-only and ships with Boost >= 1.91.
xrpl_add_benchmark(number)
target_link_libraries(xrpl.bench.number PRIVATE xrpl.imports.bench)
add_dependencies(xrpl.benchmarks xrpl.bench.number)
# Benchmark a 128-bit Number prototype if one exists (see number/Number128.h).
# CONFIGURE_DEPENDS re-checks the glob on every build, so adding or removing
# the header is enough to reconfigure and rebuild.
file(
GLOB number128_header
CONFIGURE_DEPENDS
"${CMAKE_SOURCE_DIR}/include/xrpl/basics/Number128.h"
)
if(number128_header)
set(XRPL_BENCH_NUMBER128 ON)
target_compile_definitions(xrpl.bench.number PRIVATE XRPL_BENCH_NUMBER128)
endif()
if(bench_intel_dfp)
# Intel Decimal Floating-Point Math Library, fetched and built with its own
# makefile for local benchmarking only. If it is ever used beyond that, it
# should become a Conan recipe instead.
#
# CALL_BY_REF=0 GLOBAL_RND=0 GLOBAL_FLAGS=0: arguments by value, rounding
# mode and status flags passed to every call. number/IntelBid.h defines the
# matching DECIMAL_* macros. The upstream makefile adds no optimization
# flags of its own, so pass -O3 to match the Release build.
include(ExternalProject)
find_program(XRPL_MAKE_EXECUTABLE NAMES gmake make REQUIRED)
set(intel_dfp_dir "${CMAKE_CURRENT_BINARY_DIR}/intel_dfp")
ExternalProject_Add(
intel_dfp
URL https://netlib.org/misc/intel/IntelRDFPMathLib20U4.tar.gz
URL_HASH
SHA256=1df86132e7a31fd74d784fee1c679b21a088f73a8ec979cfaf784c200392e125
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
SOURCE_DIR "${intel_dfp_dir}"
BUILD_IN_SOURCE TRUE
CONFIGURE_COMMAND ""
BUILD_COMMAND
${XRPL_MAKE_EXECUTABLE} -C LIBRARY CC=${CMAKE_C_COMPILER}
CALL_BY_REF=0 GLOBAL_RND=0 GLOBAL_FLAGS=0 UNCHANGED_BINARY_FLAGS=0
"CFLAGS_OPT=-O3 -fPIC"
INSTALL_COMMAND ""
BUILD_BYPRODUCTS "${intel_dfp_dir}/LIBRARY/libbid.a"
)
# Imported targets require their include directory to exist at generate time.
file(MAKE_DIRECTORY "${intel_dfp_dir}/LIBRARY/src")
add_library(xrpl.imports.intel_dfp STATIC IMPORTED)
set_target_properties(
xrpl.imports.intel_dfp
PROPERTIES
IMPORTED_LOCATION "${intel_dfp_dir}/LIBRARY/libbid.a"
INTERFACE_INCLUDE_DIRECTORIES "${intel_dfp_dir}/LIBRARY/src"
)
add_dependencies(xrpl.imports.intel_dfp intel_dfp)
target_link_libraries(xrpl.bench.number PRIVATE xrpl.imports.intel_dfp)
target_compile_definitions(xrpl.bench.number PRIVATE XRPL_BENCH_INTEL_DFP)
endif()
if(bench_mpdecimal)
find_package(mpdecimal REQUIRED)
target_link_libraries(xrpl.bench.number PRIVATE mpdecimal::libmpdecimal)
target_compile_definitions(xrpl.bench.number PRIVATE XRPL_BENCH_MPDECIMAL)
# The divergence harness is a plain executable, not a Google Benchmark, so
# it is defined here rather than with xrpl_add_benchmark. It shares the
# adapters in number/.
add_executable(xrpl.bench.number_diff number_diff/main.cpp)
isolate_headers(
xrpl.bench.number_diff
"${CMAKE_SOURCE_DIR}/src"
"${CMAKE_CURRENT_SOURCE_DIR}/number"
PRIVATE
)
target_link_libraries(
xrpl.bench.number_diff
PRIVATE xrpl.libxrpl mpdecimal::libmpdecimal
)
if(bench_intel_dfp)
target_link_libraries(
xrpl.bench.number_diff
PRIVATE xrpl.imports.intel_dfp
)
target_compile_definitions(
xrpl.bench.number_diff
PRIVATE XRPL_BENCH_INTEL_DFP
)
endif()
if(XRPL_BENCH_NUMBER128)
target_compile_definitions(
xrpl.bench.number_diff
PRIVATE XRPL_BENCH_NUMBER128
)
endif()
add_dependencies(xrpl.benchmarks xrpl.bench.number_diff)
endif()
@@ -0,0 +1,263 @@
// Single-operation benchmarks for xrpl::Number and the decimal floating-point
// types it is being compared against. See docs/NumberDecimalBenchmark.md.
//
// Benchmark names are "<operation>/<dataset>/<type>", e.g.
// "mul/full/Number.Large330", so they can be filtered with
// --benchmark_filter='^mul/.*/(Number|BoostDecimalFast128)'.
#include <xrpl/basics/Number.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/number/BoostDecimal.h>
#include <benchmarks/libxrpl/number/Double.h>
#include <benchmarks/libxrpl/number/Number128.h>
#include <benchmarks/libxrpl/number/NumberLike.h>
#include <benchmarks/libxrpl/number/Operands.h>
#ifdef XRPL_BENCH_MPDECIMAL
#include <benchmarks/libxrpl/number/MpDecimal.h>
#endif
#ifdef XRPL_BENCH_INTEL_DFP
#include <benchmarks/libxrpl/number/IntelBid.h>
#endif
#include <array>
#include <cstddef>
#include <cstdint>
#include <format>
#include <functional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace xrpl::number_bench {
namespace {
constexpr std::uint64_t kSeedLhs = 0x5eed'0001;
constexpr std::uint64_t kSeedRhs = 0x5eed'0002;
/**
* Environment for types that need no per-benchmark setup.
*/
struct NoEnv
{
};
/**
* Runs Number under a specific mantissa scale. Operands are materialized after
* the guard is in place, so they are normalized for that scale.
*/
template <MantissaRange::MantissaScale Scale>
struct NumberScaleEnv
{
NumberMantissaScaleGuard guard{Scale};
};
/**
* Independent operations over a batch: measures throughput.
*/
template <NumberLike T, class Env, class Op>
auto
binaryThroughput(std::vector<RawOperand> lhs, std::vector<RawOperand> rhs, Op op)
{
return [lhs = std::move(lhs), rhs = std::move(rhs), op](benchmark::State& state) {
[[maybe_unused]] Env const env{};
auto const a = materialize<T>(lhs);
auto const b = materialize<T>(rhs);
for (auto _ : state)
{
for (std::size_t i = 0; i < a.size(); ++i)
{
auto r = op(a[i], b[i]);
benchmark::DoNotOptimize(r);
}
}
state.SetItemsProcessed(static_cast<std::int64_t>(state.iterations() * a.size()));
};
}
template <NumberLike T, class Env, class Op>
auto
unaryThroughput(std::vector<RawOperand> operands, Op op)
{
return [operands = std::move(operands), op](benchmark::State& state) {
[[maybe_unused]] Env const env{};
auto const a = materialize<T>(operands);
for (auto _ : state)
{
for (auto const& x : a)
{
auto r = op(x);
benchmark::DoNotOptimize(r);
}
}
state.SetItemsProcessed(static_cast<std::int64_t>(state.iterations() * a.size()));
};
}
/**
* Construction from (mantissa, exponent), the path every STNumber/STAmount read takes.
*/
template <NumberLike T, class Env>
auto
constructThroughput(std::vector<RawOperand> operands)
{
return [operands = std::move(operands)](benchmark::State& state) {
[[maybe_unused]] Env const env{};
for (auto _ : state)
{
for (auto const& raw : operands)
{
T x{raw.mantissa, raw.exponent};
benchmark::DoNotOptimize(x);
}
}
state.SetItemsProcessed(static_cast<std::int64_t>(state.iterations() * operands.size()));
};
}
/**
* Each result feeds the next operation: measures latency.
*/
template <NumberLike T, class Env, class Op>
auto
chainLatency(std::vector<RawOperand> operands, Op op)
{
return [operands = std::move(operands), op](benchmark::State& state) {
[[maybe_unused]] Env const env{};
auto const a = materialize<T>(operands);
for (auto _ : state)
{
T acc{std::int64_t{1}};
for (auto const& x : a)
acc = op(acc, x);
benchmark::DoNotOptimize(acc);
}
state.SetItemsProcessed(static_cast<std::int64_t>(state.iterations() * a.size()));
};
}
template <class T, class Fn>
void
add(std::string_view op, std::string_view data, std::string_view subject, Fn fn)
{
benchmark::RegisterBenchmark(
std::format("{}/{}/{}", op, data, subject), [fn = std::move(fn)](benchmark::State& state) {
fn(state);
state.counters["sizeof"] = sizeof(T);
});
}
template <NumberLike T, class Env>
void
registerSubject(std::string_view subject)
{
constexpr auto kGeneral =
std::to_array({Dataset::Full, Dataset::Iou, Dataset::Drops, Dataset::Mpt});
for (auto const d : kGeneral)
{
auto const name = toString(d);
auto const lhs = makeRaw(d, kSeedLhs);
auto const rhs = makeRaw(d, kSeedRhs);
add<T>("add", name, subject, binaryThroughput<T, Env>(lhs, rhs, std::plus<>{}));
add<T>("sub", name, subject, binaryThroughput<T, Env>(lhs, rhs, std::minus<>{}));
add<T>("mul", name, subject, binaryThroughput<T, Env>(lhs, rhs, std::multiplies<>{}));
add<T>("div", name, subject, binaryThroughput<T, Env>(lhs, rhs, std::divides<>{}));
add<T>("lt", name, subject, binaryThroughput<T, Env>(lhs, rhs, std::less<>{}));
add<T>("construct", name, subject, constructThroughput<T, Env>(lhs));
add<T>("to_string", name, subject, unaryThroughput<T, Env>(lhs, [](T const& x) {
return to_string(x);
}));
add<T>("root2", name, subject, unaryThroughput<T, Env>(lhs, [](T const& x) {
return root2(x);
}));
}
// Only integer-valued datasets fit in int64 without overflow.
for (auto const d : {Dataset::Drops, Dataset::Mpt})
{
add<T>(
"to_int64",
toString(d),
subject,
unaryThroughput<T, Env>(
makeRaw(d, kSeedLhs), [](T const& x) { return static_cast<std::int64_t>(x); }));
}
for (int const gap : {0, 1, 5, 18})
{
auto [lhs, rhs] = makeExponentGapPairs(gap, kSeedLhs);
add<T>(
std::format("add_gap{}", gap),
toString(Dataset::Full),
subject,
binaryThroughput<T, Env>(std::move(lhs), std::move(rhs), std::plus<>{}));
}
{
auto [lhs, rhs] = makeCancellationPairs(kSeedLhs);
add<T>(
"sub_cancel",
toString(Dataset::Full),
subject,
binaryThroughput<T, Env>(std::move(lhs), std::move(rhs), std::minus<>{}));
}
// 12 and 360: monthly payments for one year and for thirty years.
for (unsigned const n : {12u, 360u})
{
add<T>(
std::format("power{}", n),
toString(Dataset::Rate),
subject,
unaryThroughput<T, Env>(
makeRaw(Dataset::Rate, kSeedLhs), [n](T const& x) { return power(x, n); }));
}
add<T>(
"add_chain",
toString(Dataset::Full),
subject,
chainLatency<T, Env>(makeRaw(Dataset::Full, kSeedLhs), std::plus<>{}));
add<T>(
"mul_chain",
toString(Dataset::Rate),
subject,
chainLatency<T, Env>(makeRaw(Dataset::Rate, kSeedLhs), std::multiplies<>{}));
add<T>(
"div_chain",
toString(Dataset::Rate),
subject,
chainLatency<T, Env>(makeRaw(Dataset::Rate, kSeedLhs), std::divides<>{}));
}
[[maybe_unused]] bool const kRegistered = [] {
using enum MantissaRange::MantissaScale;
registerSubject<Number, NumberScaleEnv<Small>>("Number.Small");
registerSubject<Number, NumberScaleEnv<LargeLegacy>>("Number.LargeLegacy");
registerSubject<Number, NumberScaleEnv<Large330>>("Number.Large330");
registerSubject<BoostDecimal64, NoEnv>("BoostDecimal64");
registerSubject<BoostDecimalFast64, NoEnv>("BoostDecimalFast64");
registerSubject<BoostDecimal128, NoEnv>("BoostDecimal128");
registerSubject<BoostDecimalFast128, NoEnv>("BoostDecimalFast128");
registerSubject<Double, NoEnv>("Double");
#ifdef XRPL_BENCH_MPDECIMAL
registerSubject<MpDecimal19, NoEnv>("MpDecimal19");
registerSubject<MpDecimal34, NoEnv>("MpDecimal34");
registerSubject<MpDecimal38, NoEnv>("MpDecimal38");
#endif
#ifdef XRPL_BENCH_INTEL_DFP
registerSubject<IntelBid64, NoEnv>("IntelBid64");
registerSubject<IntelBid128, NoEnv>("IntelBid128");
#endif
#ifdef XRPL_BENCH_HAS_NUMBER128
registerSubject<Number128, NoEnv>("Number128");
#endif
return true;
}();
} // namespace
} // namespace xrpl::number_bench
@@ -0,0 +1,269 @@
#pragma once
#include <xrpl/basics/Number.h>
#include <boost/decimal.hpp>
#include <benchmarks/libxrpl/number/NumberLike.h>
#include <array>
#include <cstdint>
#include <string>
namespace xrpl::number_bench {
namespace detail {
constexpr boost::decimal::rounding_mode
toBoost(Number::RoundingMode mode)
{
using enum Number::RoundingMode;
using boost::decimal::rounding_mode;
switch (mode)
{
case ToNearest:
return rounding_mode::fe_dec_to_nearest;
case TowardsZero:
return rounding_mode::fe_dec_toward_zero;
case Downward:
return rounding_mode::fe_dec_downward;
case Upward:
return rounding_mode::fe_dec_upward;
}
return rounding_mode::fe_dec_to_nearest;
}
constexpr Number::RoundingMode
fromBoost(boost::decimal::rounding_mode mode)
{
using enum Number::RoundingMode;
using boost::decimal::rounding_mode;
switch (mode)
{
case rounding_mode::fe_dec_toward_zero:
return TowardsZero;
case rounding_mode::fe_dec_downward:
return Downward;
case rounding_mode::fe_dec_upward:
return Upward;
default:
return ToNearest;
}
}
} // namespace detail
/**
* Wraps a Boost.Decimal type behind Number's interface.
*
* Every member is a one-line inline forward, so at -O2 and above the wrapper
* adds no cost over using the Boost type directly.
*
* Semantic differences from Number that the wrapper deliberately does NOT
* hide (they are part of what we are measuring):
* - Overflow produces infinity and division by zero produces inf/NaN
* instead of throwing.
* - The rounding mode is a process-wide global in Boost.Decimal, not
* thread-local like Number's.
* - decimal64_t / decimal_fast64_t carry 16 digits, so constructing from a
* 19-digit int64 mantissa rounds.
*/
template <class D>
class BoostDecimal final
{
D value_{};
explicit constexpr BoostDecimal(D value) noexcept : value_{value}
{
}
public:
using rep = std::int64_t;
using value_type = D;
constexpr BoostDecimal() = default;
// Implicit, like Number(rep).
constexpr BoostDecimal(rep mantissa) noexcept : value_{mantissa, 0} // NOLINT
{
}
explicit constexpr BoostDecimal(rep mantissa, int exponent) noexcept
: value_{mantissa, exponent}
{
}
[[nodiscard]] constexpr D
value() const noexcept
{
return value_;
}
constexpr BoostDecimal
operator-() const noexcept
{
return BoostDecimal{-value_};
}
constexpr BoostDecimal&
operator+=(BoostDecimal const& x) noexcept
{
value_ += x.value_;
return *this;
}
constexpr BoostDecimal&
operator-=(BoostDecimal const& x) noexcept
{
value_ -= x.value_;
return *this;
}
constexpr BoostDecimal&
operator*=(BoostDecimal const& x) noexcept
{
value_ *= x.value_;
return *this;
}
constexpr BoostDecimal&
operator/=(BoostDecimal const& x) noexcept
{
value_ /= x.value_;
return *this;
}
friend constexpr BoostDecimal
operator+(BoostDecimal const& x, BoostDecimal const& y) noexcept
{
return BoostDecimal{x.value_ + y.value_};
}
friend constexpr BoostDecimal
operator-(BoostDecimal const& x, BoostDecimal const& y) noexcept
{
return BoostDecimal{x.value_ - y.value_};
}
friend constexpr BoostDecimal
operator*(BoostDecimal const& x, BoostDecimal const& y) noexcept
{
return BoostDecimal{x.value_ * y.value_};
}
friend constexpr BoostDecimal
operator/(BoostDecimal const& x, BoostDecimal const& y) noexcept
{
return BoostDecimal{x.value_ / y.value_};
}
friend constexpr bool
operator==(BoostDecimal const& x, BoostDecimal const& y) noexcept
{
return x.value_ == y.value_;
}
friend constexpr bool
operator<(BoostDecimal const& x, BoostDecimal const& y) noexcept
{
return x.value_ < y.value_;
}
friend constexpr bool
operator>(BoostDecimal const& x, BoostDecimal const& y) noexcept
{
return y < x;
}
friend constexpr bool
operator<=(BoostDecimal const& x, BoostDecimal const& y) noexcept
{
return !(y < x);
}
friend constexpr bool
operator>=(BoostDecimal const& x, BoostDecimal const& y) noexcept
{
return !(x < y);
}
// Round to an integer using the current rounding mode, like Number.
explicit
operator rep() const noexcept
{
// Boost 1.91's llrint mishandles values that are already integers at full
// precision (significand exponent >= 0): it returns a wrong result, or
// divides by zero when the exponent is exactly 0. Fixed upstream (the
// same early return as here); remove this once Boost includes the fix.
int exponent = 0;
boost::decimal::frexp10(value_, &exponent);
if (exponent >= 0)
return static_cast<rep>(value_);
return static_cast<rep>(boost::decimal::llrint(value_));
}
static Number::RoundingMode
getround() noexcept
{
return detail::fromBoost(boost::decimal::fegetround());
}
// Returns the previous mode, like Number::setround.
static Number::RoundingMode
setround(Number::RoundingMode mode) noexcept
{
auto const old = getround();
boost::decimal::fesetround(detail::toBoost(mode));
return old;
}
friend std::string
to_string(BoostDecimal const& x)
{
std::array<char, 64> buffer{};
auto const result =
boost::decimal::to_chars(buffer.data(), buffer.data() + buffer.size(), x.value_);
return {buffer.data(), result.ptr};
}
friend constexpr BoostDecimal
abs(BoostDecimal const& x) noexcept
{
return BoostDecimal{boost::decimal::abs(x.value_)};
}
friend constexpr BoostDecimal
power(BoostDecimal const& x, unsigned n) noexcept
{
return BoostDecimal{boost::decimal::pow(x.value_, n)};
}
friend constexpr BoostDecimal
root2(BoostDecimal const& x) noexcept
{
return BoostDecimal{boost::decimal::sqrt(x.value_)};
}
friend constexpr Decomposed
decompose(BoostDecimal const& x) noexcept
{
int exponent = 0;
auto const coefficient = boost::decimal::frexp10(x.value_, &exponent);
return {
.negative = boost::decimal::signbit(x.value_),
.coefficient = static_cast<unsigned __int128>(coefficient),
.exponent = exponent};
}
};
using BoostDecimal64 = BoostDecimal<boost::decimal::decimal64_t>;
using BoostDecimal128 = BoostDecimal<boost::decimal::decimal128_t>;
using BoostDecimalFast64 = BoostDecimal<boost::decimal::decimal_fast64_t>;
using BoostDecimalFast128 = BoostDecimal<boost::decimal::decimal_fast128_t>;
static_assert(DecomposableNumber<BoostDecimal64>);
static_assert(DecomposableNumber<BoostDecimal128>);
static_assert(DecomposableNumber<BoostDecimalFast64>);
static_assert(DecomposableNumber<BoostDecimalFast128>);
} // namespace xrpl::number_bench
+200
View File
@@ -0,0 +1,200 @@
#pragma once
#include <xrpl/basics/Number.h>
#include <benchmarks/libxrpl/number/NumberLike.h>
#include <cfenv>
#include <cmath>
#include <cstdint>
#include <sstream>
#include <string>
namespace xrpl::number_bench {
/**
* Binary `double` behind Number's interface: a hardware speed-of-light
* baseline, not a candidate (base 2, 53-bit significand).
*
* It is not DecomposableNumber because a binary value has no exact short
* decimal coefficient.
*/
class Double final
{
double value_ = 0.0;
explicit constexpr Double(double value) noexcept : value_{value}
{
}
public:
using rep = std::int64_t;
constexpr Double() = default;
// Implicit, like Number(rep).
Double(rep mantissa) noexcept : value_{static_cast<double>(mantissa)} // NOLINT
{
}
explicit Double(rep mantissa, int exponent) noexcept
: value_{static_cast<double>(mantissa) * std::pow(10.0, exponent)}
{
}
constexpr Double
operator-() const noexcept
{
return Double{-value_};
}
constexpr Double&
operator+=(Double const& x) noexcept
{
value_ += x.value_;
return *this;
}
constexpr Double&
operator-=(Double const& x) noexcept
{
value_ -= x.value_;
return *this;
}
constexpr Double&
operator*=(Double const& x) noexcept
{
value_ *= x.value_;
return *this;
}
constexpr Double&
operator/=(Double const& x) noexcept
{
value_ /= x.value_;
return *this;
}
friend constexpr Double
operator+(Double const& x, Double const& y) noexcept
{
return Double{x.value_ + y.value_};
}
friend constexpr Double
operator-(Double const& x, Double const& y) noexcept
{
return Double{x.value_ - y.value_};
}
friend constexpr Double
operator*(Double const& x, Double const& y) noexcept
{
return Double{x.value_ * y.value_};
}
friend constexpr Double
operator/(Double const& x, Double const& y) noexcept
{
return Double{x.value_ / y.value_};
}
friend constexpr bool
operator==(Double const& x, Double const& y) noexcept
{
return x.value_ == y.value_;
}
friend constexpr auto
operator<=>(Double const& x, Double const& y) noexcept
{
return x.value_ <=> y.value_;
}
explicit
operator rep() const noexcept
{
return static_cast<rep>(std::llrint(value_));
}
static Number::RoundingMode
getround() noexcept
{
using enum Number::RoundingMode;
switch (std::fegetround())
{
case FE_TOWARDZERO:
return TowardsZero;
case FE_DOWNWARD:
return Downward;
case FE_UPWARD:
return Upward;
default:
return ToNearest;
}
}
static Number::RoundingMode
setround(Number::RoundingMode mode) noexcept
{
using enum Number::RoundingMode;
auto const old = getround();
switch (mode)
{
case ToNearest:
std::fesetround(FE_TONEAREST);
break;
case TowardsZero:
std::fesetround(FE_TOWARDZERO);
break;
case Downward:
std::fesetround(FE_DOWNWARD);
break;
case Upward:
std::fesetround(FE_UPWARD);
break;
}
return old;
}
friend std::string
to_string(Double const& x)
{
std::ostringstream os;
os.precision(17);
os << x.value_;
return os.str();
}
friend Double
abs(Double const& x) noexcept
{
return Double{std::fabs(x.value_)};
}
// Repeated squaring, the same algorithm as xrpl::power(Number, unsigned).
friend constexpr Double
power(Double const& x, unsigned n) noexcept
{
double result = 1.0;
double base = x.value_;
for (; n != 0; n >>= 1)
{
if ((n & 1u) != 0)
result *= base;
base *= base;
}
return Double{result};
}
friend Double
root2(Double const& x) noexcept
{
return Double{std::sqrt(x.value_)};
}
};
static_assert(NumberLike<Double>);
} // namespace xrpl::number_bench
+377
View File
@@ -0,0 +1,377 @@
#pragma once
#include <xrpl/basics/Number.h>
#include <benchmarks/libxrpl/number/NumberLike.h>
#include <array>
#include <cstdint>
#include <string>
#include <utility>
// The library must be built with the same three settings (see the
// bench_intel_dfp block in src/benchmarks/libxrpl/CMakeLists.txt): arguments
// by value, rounding mode passed to each call, status flags passed to each
// call. That is the configuration closest to Number, whose rounding mode is a
// per-thread setting rather than process-global state.
#define DECIMAL_CALL_BY_REFERENCE 0 // NOLINT(cppcoreguidelines-macro-usage)
#define DECIMAL_GLOBAL_ROUNDING 0 // NOLINT(cppcoreguidelines-macro-usage)
#define DECIMAL_GLOBAL_EXCEPTION_FLAGS 0 // NOLINT(cppcoreguidelines-macro-usage)
#include <bid_conf.h>
#include <bid_functions.h>
namespace xrpl::number_bench {
namespace detail {
/**
* One function table per interchange width, so IntelBid is written once.
*/
struct Bid64Ops
{
using Value = BID_UINT64;
static constexpr unsigned kDigits = 16;
static constexpr int kBias = 398;
// clang-format off
static Value add(Value x, Value y, _IDEC_round r, _IDEC_flags* f) { return bid64_add(x, y, r, f); }
static Value sub(Value x, Value y, _IDEC_round r, _IDEC_flags* f) { return bid64_sub(x, y, r, f); }
static Value mul(Value x, Value y, _IDEC_round r, _IDEC_flags* f) { return bid64_mul(x, y, r, f); }
static Value div(Value x, Value y, _IDEC_round r, _IDEC_flags* f) { return bid64_div(x, y, r, f); }
static Value sqrt(Value x, _IDEC_round r, _IDEC_flags* f) { return bid64_sqrt(x, r, f); }
static Value pow(Value x, Value y, _IDEC_round r, _IDEC_flags* f) { return bid64_pow(x, y, r, f); }
static Value fromInt64(std::int64_t x, _IDEC_round r, _IDEC_flags* f) { return bid64_from_int64(x, r, f); }
static Value scalbn(Value x, int n, _IDEC_round r, _IDEC_flags* f) { return bid64_scalbn(x, n, r, f); }
static Value negate(Value x) { return bid64_negate(x); }
static Value abs(Value x) { return bid64_abs(x); }
static bool less(Value x, Value y, _IDEC_flags* f) { return bid64_quiet_less(x, y, f) != 0; }
static bool equal(Value x, Value y, _IDEC_flags* f) { return bid64_quiet_equal(x, y, f) != 0; }
static std::int64_t toInt64Nearest(Value x, _IDEC_flags* f) { return bid64_to_int64_rnint(x, f); }
static std::int64_t toInt64TowardZero(Value x, _IDEC_flags* f) { return bid64_to_int64_int(x, f); }
static std::int64_t toInt64Floor(Value x, _IDEC_flags* f) { return bid64_to_int64_floor(x, f); }
static std::int64_t toInt64Ceil(Value x, _IDEC_flags* f) { return bid64_to_int64_ceil(x, f); }
static void toString(char* s, Value x, _IDEC_flags* f) { bid64_to_string(s, x, f); }
// clang-format on
/**
* Decodes the BID64 layout. Precondition: finite and canonical.
*/
static Decomposed
decompose(Value x) noexcept
{
bool const negative = (x >> 63) != 0;
bool const largeForm = ((x >> 61) & 3) == 3;
auto const exponent = static_cast<int>(largeForm ? (x >> 51) & 0x3ff : (x >> 53) & 0x3ff);
auto const coefficient = largeForm
? (x & 0x0007'ffff'ffff'ffffULL) | 0x0020'0000'0000'0000ULL
: x & 0x001f'ffff'ffff'ffffULL;
return {.negative = negative, .coefficient = coefficient, .exponent = exponent - kBias};
}
};
struct Bid128Ops
{
using Value = BID_UINT128;
static constexpr unsigned kDigits = 34;
static constexpr int kBias = 6176;
// clang-format off
static Value add(Value x, Value y, _IDEC_round r, _IDEC_flags* f) { return bid128_add(x, y, r, f); }
static Value sub(Value x, Value y, _IDEC_round r, _IDEC_flags* f) { return bid128_sub(x, y, r, f); }
static Value mul(Value x, Value y, _IDEC_round r, _IDEC_flags* f) { return bid128_mul(x, y, r, f); }
static Value div(Value x, Value y, _IDEC_round r, _IDEC_flags* f) { return bid128_div(x, y, r, f); }
static Value sqrt(Value x, _IDEC_round r, _IDEC_flags* f) { return bid128_sqrt(x, r, f); }
static Value pow(Value x, Value y, _IDEC_round r, _IDEC_flags* f) { return bid128_pow(x, y, r, f); }
static Value fromInt64(std::int64_t x, _IDEC_round, _IDEC_flags*) { return bid128_from_int64(x); }
static Value scalbn(Value x, int n, _IDEC_round r, _IDEC_flags* f) { return bid128_scalbn(x, n, r, f); }
static Value negate(Value x) { return bid128_negate(x); }
static Value abs(Value x) { return bid128_abs(x); }
static bool less(Value x, Value y, _IDEC_flags* f) { return bid128_quiet_less(x, y, f) != 0; }
static bool equal(Value x, Value y, _IDEC_flags* f) { return bid128_quiet_equal(x, y, f) != 0; }
static std::int64_t toInt64Nearest(Value x, _IDEC_flags* f) { return bid128_to_int64_rnint(x, f); }
static std::int64_t toInt64TowardZero(Value x, _IDEC_flags* f) { return bid128_to_int64_int(x, f); }
static std::int64_t toInt64Floor(Value x, _IDEC_flags* f) { return bid128_to_int64_floor(x, f); }
static std::int64_t toInt64Ceil(Value x, _IDEC_flags* f) { return bid128_to_int64_ceil(x, f); }
static void toString(char* s, Value x, _IDEC_flags* f) { bid128_to_string(s, x, f); }
// clang-format on
/**
* Decodes the BID128 layout. Precondition: finite and canonical.
*/
static Decomposed
decompose(Value x) noexcept
{
auto const hi = x.w[BID_HIGH_128W];
auto const lo = x.w[BID_LOW_128W];
bool const negative = (hi >> 63) != 0;
auto const exponent = static_cast<int>((hi >> 49) & 0x3fff);
auto const coefficient =
(static_cast<unsigned __int128>(hi & 0x0001'ffff'ffff'ffffULL) << 64) | lo;
return {.negative = negative, .coefficient = coefficient, .exponent = exponent - kBias};
}
};
} // namespace detail
/**
* Intel's Decimal Floating-Point Math Library (BID encoding) behind Number's
* interface.
*
* The rounding mode is thread-local, like Number's, and passed to each call.
* Status flags from every call accumulate in a thread-local; read and clear
* them with `takeStatus()`.
*
* Like the other IEEE types, overflow produces infinity and division by zero
* produces inf/NaN instead of throwing; decimal64 rounds 19-digit int64
* mantissas to 16 digits on construction.
*/
template <class Ops>
class IntelBid final
{
using Value = typename Ops::Value;
Value value_{};
struct State
{
_IDEC_round round = BID_ROUNDING_TO_NEAREST;
_IDEC_flags flags = 0;
};
static State&
state() noexcept
{
static thread_local State s;
return s;
}
static IntelBid
wrap(Value v) noexcept
{
IntelBid result;
result.value_ = v;
return result;
}
public:
using rep = std::int64_t;
IntelBid() : value_{Ops::fromInt64(0, BID_ROUNDING_TO_NEAREST, &state().flags)}
{
}
// Implicit, like Number(rep).
IntelBid(rep mantissa) : IntelBid(mantissa, 0) // NOLINT
{
}
explicit IntelBid(rep mantissa, int exponent)
{
auto& s = state();
value_ = Ops::fromInt64(mantissa, s.round, &s.flags);
if (exponent != 0)
value_ = Ops::scalbn(value_, exponent, s.round, &s.flags);
}
IntelBid
operator-() const noexcept
{
return wrap(Ops::negate(value_));
}
friend IntelBid
operator+(IntelBid const& x, IntelBid const& y) noexcept
{
auto& s = state();
return wrap(Ops::add(x.value_, y.value_, s.round, &s.flags));
}
friend IntelBid
operator-(IntelBid const& x, IntelBid const& y) noexcept
{
auto& s = state();
return wrap(Ops::sub(x.value_, y.value_, s.round, &s.flags));
}
friend IntelBid
operator*(IntelBid const& x, IntelBid const& y) noexcept
{
auto& s = state();
return wrap(Ops::mul(x.value_, y.value_, s.round, &s.flags));
}
friend IntelBid
operator/(IntelBid const& x, IntelBid const& y) noexcept
{
auto& s = state();
return wrap(Ops::div(x.value_, y.value_, s.round, &s.flags));
}
IntelBid&
operator+=(IntelBid const& x) noexcept
{
return *this = *this + x;
}
IntelBid&
operator-=(IntelBid const& x) noexcept
{
return *this = *this - x;
}
IntelBid&
operator*=(IntelBid const& x) noexcept
{
return *this = *this * x;
}
IntelBid&
operator/=(IntelBid const& x) noexcept
{
return *this = *this / x;
}
friend bool
operator==(IntelBid const& x, IntelBid const& y) noexcept
{
return Ops::equal(x.value_, y.value_, &state().flags);
}
friend bool
operator<(IntelBid const& x, IntelBid const& y) noexcept
{
return Ops::less(x.value_, y.value_, &state().flags);
}
friend bool
operator>(IntelBid const& x, IntelBid const& y) noexcept
{
return y < x;
}
friend bool
operator<=(IntelBid const& x, IntelBid const& y) noexcept
{
return !(y < x);
}
friend bool
operator>=(IntelBid const& x, IntelBid const& y) noexcept
{
return !(x < y);
}
// Round to an integer using the current rounding mode, like Number.
explicit
operator rep() const noexcept
{
auto& s = state();
switch (s.round)
{
case BID_ROUNDING_TO_ZERO:
return Ops::toInt64TowardZero(value_, &s.flags);
case BID_ROUNDING_DOWN:
return Ops::toInt64Floor(value_, &s.flags);
case BID_ROUNDING_UP:
return Ops::toInt64Ceil(value_, &s.flags);
default:
return Ops::toInt64Nearest(value_, &s.flags);
}
}
static Number::RoundingMode
getround() noexcept
{
using enum Number::RoundingMode;
switch (state().round)
{
case BID_ROUNDING_TO_ZERO:
return TowardsZero;
case BID_ROUNDING_DOWN:
return Downward;
case BID_ROUNDING_UP:
return Upward;
default:
return ToNearest;
}
}
static Number::RoundingMode
setround(Number::RoundingMode mode) noexcept
{
using enum Number::RoundingMode;
auto const old = getround();
auto& round = state().round;
switch (mode)
{
case ToNearest:
round = BID_ROUNDING_TO_NEAREST;
break;
case TowardsZero:
round = BID_ROUNDING_TO_ZERO;
break;
case Downward:
round = BID_ROUNDING_DOWN;
break;
case Upward:
round = BID_ROUNDING_UP;
break;
}
return old;
}
/**
* Returns the status flags raised since the last call, and clears them.
*/
static _IDEC_flags
takeStatus() noexcept
{
return std::exchange(state().flags, 0);
}
friend std::string
to_string(IntelBid const& x)
{
std::array<char, 64> buffer{};
Ops::toString(buffer.data(), x.value_, &state().flags);
return buffer.data();
}
friend IntelBid
abs(IntelBid const& x) noexcept
{
return wrap(Ops::abs(x.value_));
}
friend IntelBid
power(IntelBid const& x, unsigned n)
{
auto& s = state();
IntelBid const exponent{static_cast<rep>(n)};
return wrap(Ops::pow(x.value_, exponent.value_, s.round, &s.flags));
}
friend IntelBid
root2(IntelBid const& x) noexcept
{
auto& s = state();
return wrap(Ops::sqrt(x.value_, s.round, &s.flags));
}
/**
* Precondition: x is finite.
*/
friend Decomposed
decompose(IntelBid const& x) noexcept
{
return Ops::decompose(x.value_);
}
};
using IntelBid64 = IntelBid<detail::Bid64Ops>;
using IntelBid128 = IntelBid<detail::Bid128Ops>;
static_assert(DecomposableNumber<IntelBid64>);
static_assert(DecomposableNumber<IntelBid128>);
} // namespace xrpl::number_bench
+355
View File
@@ -0,0 +1,355 @@
// Ledger-formula benchmarks: AMM swaps, vault share conversion, and loan
// amortization (see Kernels.h), run identically on every NumberLike type.
// See docs/NumberDecimalBenchmark.md.
//
// Benchmark names are "kernel/<kernel>/<type>". Each reports throughput and,
// when built with mpdecimal (bench_mpdecimal), accuracy counters comparing the
// type's result with the same formula evaluated at 96 digits from the type's
// own inputs, so construction rounding is excluded:
//
// digits_min / digits_median correct significant digits, -log10(relative
// error), worst case and median over the
// cases; 40 means exact. For loan_amortize the
// error is the final balance relative to the
// principal.
// mismatches for integer results: cases that differ from
// the exact truncated result.
#include <benchmarks/libxrpl/number/Kernels.h>
#include <xrpl/basics/Number.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/number/BoostDecimal.h>
#include <benchmarks/libxrpl/number/Double.h>
#include <benchmarks/libxrpl/number/Number128.h>
#include <benchmarks/libxrpl/number/NumberLike.h>
#include <benchmarks/libxrpl/number/Operands.h>
#ifdef XRPL_BENCH_MPDECIMAL
#include <benchmarks/libxrpl/number/MpDecimal.h>
#endif
#ifdef XRPL_BENCH_INTEL_DFP
#include <benchmarks/libxrpl/number/IntelBid.h>
#endif
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <format>
#include <random>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <vector>
namespace xrpl::number_bench {
namespace {
constexpr std::size_t kCases = 256;
constexpr std::uint64_t kSeed = 0x6b65'726e;
/**
* Inputs for one kernel evaluation: up to three decimal values and two
* integer parameters (fee, rate, interval, payment count).
*/
struct KernelCase
{
std::array<RawOperand, 3> values;
std::array<std::uint32_t, 3> integers;
};
template <class T>
using Values = std::array<T, 3>;
// ---- Case generation ----
/**
* A positive value with a 16-digit mantissa and magnitude 10^lo to 10^hi.
*/
RawOperand
amount(std::mt19937_64& rng, int lo, int hi)
{
constexpr auto kE15 = static_cast<std::int64_t>(kPowerOfTen[15]);
auto const mantissa = std::uniform_int_distribution<std::int64_t>{kE15, (kE15 * 10) - 1}(rng);
return {mantissa, std::uniform_int_distribution<int>{lo, hi}(rng)-15};
}
/**
* `base` scaled by a factor 10^lo to 10^hi, with a fresh mantissa.
*/
RawOperand
fractionOf(std::mt19937_64& rng, RawOperand base, int lo, int hi)
{
auto result = amount(rng, 0, 0);
result.exponent = base.exponent + std::uniform_int_distribution<int>{lo, hi}(rng);
return result;
}
/**
* Pools of 10^6 to 10^12 per side, trades of 10^-6 to 10^-1 of the pool,
* fees of 0 to 1%.
*/
std::vector<KernelCase>
ammCases()
{
std::mt19937_64 rng{kSeed};
std::vector<KernelCase> cases;
for (std::size_t i = 0; i < kCases; ++i)
{
auto const poolIn = amount(rng, 6, 12);
auto const poolOut = amount(rng, 6, 12);
// Trades are a fraction of the out pool for swap-out to stay solvent.
auto const trade = fractionOf(rng, poolOut, -6, -2);
auto const fee = std::uniform_int_distribution<std::uint32_t>{0, 1000}(rng);
cases.push_back({{poolIn, poolOut, trade}, {fee, 0, 0}});
}
return cases;
}
/**
* Vaults holding 10^9 to 10^17 drops-like integer assets, share supply 10^6
* times larger (vault scale 6) within a factor of 2, but at most 10^18 since
* MPT supply is capped at 2^63-1; deposits of 10^-6 to 10^-1 of the assets.
*/
std::vector<KernelCase>
vaultCases()
{
std::mt19937_64 rng{kSeed + 1};
std::vector<KernelCase> cases;
for (std::size_t i = 0; i < kCases; ++i)
{
auto const assetDigits = std::uniform_int_distribution<int>{10, 17}(rng);
auto const assetTotal = std::uniform_int_distribution<std::int64_t>{
static_cast<std::int64_t>(kPowerOfTen[assetDigits - 1]),
static_cast<std::int64_t>(kPowerOfTen[assetDigits]) - 1}(rng);
auto const ratio = std::uniform_real_distribution<double>{0.5, 2.0}(rng);
auto const shareMantissa =
static_cast<std::int64_t>(static_cast<double>(assetTotal) * ratio) | 1;
auto const shareExponent = std::min(6, 18 - assetDigits);
auto const deposit = std::uniform_int_distribution<std::int64_t>{
std::max<std::int64_t>(1, assetTotal / 1'000'000), assetTotal / 10}(rng);
cases.push_back(
{{RawOperand{shareMantissa, shareExponent},
RawOperand{assetTotal, 0},
RawOperand{deposit, 0}},
{}});
}
return cases;
}
/**
* Principal of 10^3 to 10^9, annual rates of 0.1% to 30% (in 1/10 bips),
* monthly payments.
*/
std::vector<KernelCase>
loanCases(std::uint32_t payments)
{
constexpr std::uint32_t kMonth = 30 * 24 * 60 * 60;
std::mt19937_64 rng{kSeed + 2 + payments};
std::vector<KernelCase> cases;
for (std::size_t i = 0; i < kCases; ++i)
{
auto const principal = amount(rng, 3, 9);
auto const rate = std::uniform_int_distribution<std::uint32_t>{100, 30'000}(rng);
cases.push_back(
{{principal, RawOperand{0, 0}, RawOperand{0, 0}}, {rate, kMonth, payments}});
}
return cases;
}
// ---- Kernels, adapted to the common case shape ----
auto const kAmmSwapIn = [](auto const& v, auto const& i) {
return ammSwapIn(v[0], v[1], v[2], static_cast<std::uint16_t>(i[0]));
};
auto const kAmmSwapOut = [](auto const& v, auto const& i) {
return ammSwapOut(v[0], v[1], v[2], static_cast<std::uint16_t>(i[0]));
};
auto const kVaultAssetsToShares = [](auto const& v, auto const&) {
return vaultAssetsToShares(v[0], v[1], v[2]);
};
auto const kVaultSharesToAssets = [](auto const& v, auto const&) {
// Redeem the same magnitude of shares as the deposit's asset amount.
return vaultSharesToAssets(v[1], v[0], v[2]);
};
auto const kLoanPayment = [](auto const& v, auto const& i) {
using T = std::decay_t<decltype(v[0])>;
return loanPeriodicPayment(v[0], loanPeriodicRate<T>(i[0], i[1]), i[2]);
};
auto const kLoanAmortize = [](auto const& v, auto const& i) {
using T = std::decay_t<decltype(v[0])>;
return loanAmortize(v[0], loanPeriodicRate<T>(i[0], i[1]), i[2]);
};
// ---- Accuracy against a 96-digit evaluation ----
#ifdef XRPL_BENCH_MPDECIMAL
using Exact = MpDecimal<96>;
template <NumberLike T>
Exact
toExact(T const& x)
{
if constexpr (DecomposableNumber<T>)
{
return Exact::fromDecomposed(decompose(x));
}
else
{
Exact result;
mpd_context_t ctx;
mpd_maxcontext(&ctx);
std::uint32_t status = 0;
mpd_qset_string(result.get(), to_string(x).c_str(), &ctx, &status);
return result;
}
}
double
correctDigits(Exact const& error, Exact const& scale)
{
constexpr double kExact = 40;
if (mpd_iszero(error.get()) != 0)
return kExact;
auto const relative = std::stod(to_string(abs(error) / abs(scale)));
return std::min(kExact, -std::log10(relative));
}
/**
* Sets digits_min/digits_median (or mismatches) on `state`.
*/
template <NumberLike T, class Fn>
void
measureAccuracy(
benchmark::State& state,
std::vector<Values<T>> const& inputs,
std::vector<KernelCase> const& cases,
Fn fn,
bool relativeToFirstInput)
{
std::vector<double> digits;
std::size_t mismatches = 0;
for (std::size_t c = 0; c < inputs.size(); ++c)
{
Values<Exact> const exactInputs{
toExact(inputs[c][0]), toExact(inputs[c][1]), toExact(inputs[c][2])};
auto const result = fn(inputs[c], cases[c].integers);
auto const exact = fn(exactInputs, cases[c].integers);
if constexpr (std::is_integral_v<std::decay_t<decltype(result)>>)
{
mismatches += result == exact ? 0 : 1;
}
else
{
auto const& scale = relativeToFirstInput ? exactInputs[0] : exact;
digits.push_back(correctDigits(toExact(result) - exact, scale));
}
}
if (digits.empty())
{
state.counters["mismatches"] = static_cast<double>(mismatches);
return;
}
std::ranges::sort(digits);
state.counters["digits_min"] = digits.front();
state.counters["digits_median"] = digits[digits.size() / 2];
}
#endif
// ---- Benchmarks ----
struct NoEnv
{
};
template <MantissaRange::MantissaScale Scale>
struct NumberScaleEnv
{
NumberMantissaScaleGuard guard{Scale};
};
template <NumberLike T, class Env, class Fn>
void
registerKernel(
std::string_view kernel,
std::string_view subject,
std::vector<KernelCase> const& cases,
Fn fn,
bool relativeToFirstInput = false)
{
benchmark::RegisterBenchmark(
std::format("kernel/{}/{}", kernel, subject),
[cases, fn, relativeToFirstInput](benchmark::State& state) {
[[maybe_unused]] Env const env{};
std::vector<Values<T>> inputs;
inputs.reserve(cases.size());
for (auto const& c : cases)
{
inputs.push_back(
{T{c.values[0].mantissa, c.values[0].exponent},
T{c.values[1].mantissa, c.values[1].exponent},
T{c.values[2].mantissa, c.values[2].exponent}});
}
for (auto _ : state)
{
for (std::size_t c = 0; c < inputs.size(); ++c)
{
auto r = fn(inputs[c], cases[c].integers);
benchmark::DoNotOptimize(r);
}
}
state.SetItemsProcessed(static_cast<std::int64_t>(state.iterations() * inputs.size()));
#ifdef XRPL_BENCH_MPDECIMAL
measureAccuracy<T>(state, inputs, cases, fn, relativeToFirstInput);
#else
(void)relativeToFirstInput;
#endif
});
}
template <NumberLike T, class Env>
void
registerSubject(std::string_view subject)
{
static auto const kAmm = ammCases();
static auto const kVault = vaultCases();
static auto const kLoan12 = loanCases(12);
static auto const kLoan360 = loanCases(360);
registerKernel<T, Env>("amm_swap_in", subject, kAmm, kAmmSwapIn);
registerKernel<T, Env>("amm_swap_out", subject, kAmm, kAmmSwapOut);
registerKernel<T, Env>("vault_assets_to_shares", subject, kVault, kVaultAssetsToShares);
registerKernel<T, Env>("vault_shares_to_assets", subject, kVault, kVaultSharesToAssets);
registerKernel<T, Env>("loan_payment12", subject, kLoan12, kLoanPayment);
registerKernel<T, Env>("loan_payment360", subject, kLoan360, kLoanPayment);
registerKernel<T, Env>("loan_amortize12", subject, kLoan12, kLoanAmortize, true);
registerKernel<T, Env>("loan_amortize360", subject, kLoan360, kLoanAmortize, true);
}
[[maybe_unused]] bool const kRegistered = [] {
using enum MantissaRange::MantissaScale;
registerSubject<Number, NumberScaleEnv<Small>>("Number.Small");
registerSubject<Number, NumberScaleEnv<Large330>>("Number.Large330");
registerSubject<BoostDecimal64, NoEnv>("BoostDecimal64");
registerSubject<BoostDecimal128, NoEnv>("BoostDecimal128");
registerSubject<Double, NoEnv>("Double");
#ifdef XRPL_BENCH_MPDECIMAL
registerSubject<MpDecimal34, NoEnv>("MpDecimal34");
registerSubject<MpDecimal38, NoEnv>("MpDecimal38");
#endif
#ifdef XRPL_BENCH_INTEL_DFP
registerSubject<IntelBid64, NoEnv>("IntelBid64");
registerSubject<IntelBid128, NoEnv>("IntelBid128");
#endif
#ifdef XRPL_BENCH_HAS_NUMBER128
registerSubject<Number128, NoEnv>("Number128");
#endif
return true;
}();
} // namespace
} // namespace xrpl::number_bench
+220
View File
@@ -0,0 +1,220 @@
#pragma once
// Ledger formulas, written once against NumberLike so every library runs the
// identical computation. Each kernel mirrors a production function; keep them
// in sync if those change:
//
// ammSwapIn / ammSwapOut swapAssetIn / swapAssetOut, fixAMMv1_1 branch
// (include/xrpl/ledger/helpers/AMMHelpers.h)
// vaultAssetsToShares assetsToSharesDeposit
// vaultSharesToAssets sharesToAssetsDeposit
// (src/libxrpl/ledger/helpers/VaultHelpers.cpp)
// loanPeriodicRate loanPeriodicRate
// loanPowerMinusOne computePowerMinusOneHybrid
// loanPeriodicPayment loanPeriodicPayment, fixCleanup3_2_0 branch
// (src/libxrpl/ledger/helpers/LendingHelpers.cpp)
//
// loanAmortize is not a production function: it runs a full payment schedule
// (interest = balance * rate; balance -= payment - interest) so error
// accumulates the way it does over a loan's life. The exact remaining balance
// after the last payment is 0.
//
// The kernels operate on bare values, not STAmount or ledger entries, and
// omit the final rounding to the asset's scale, so they measure arithmetic
// alone.
#include <xrpl/basics/Number.h>
#include <benchmarks/libxrpl/number/NumberLike.h>
#include <cstdint>
namespace xrpl::number_bench {
namespace detail {
template <NumberLike T>
T
integer(std::int64_t value)
{
return T{value};
}
/**
* Sets a rounding mode for its lifetime, then restores the previous one.
*/
template <NumberLike T>
class RoundGuard
{
Number::RoundingMode saved_;
public:
explicit RoundGuard(Number::RoundingMode mode) : saved_{T::setround(mode)}
{
}
~RoundGuard()
{
T::setround(saved_);
}
RoundGuard(RoundGuard const&) = delete;
RoundGuard&
operator=(RoundGuard const&) = delete;
};
} // namespace detail
/**
* Amount of the out asset received for swapping `assetIn` into the pool.
* Rounding at each step favors the AMM.
*/
template <NumberLike T>
T
ammSwapIn(T const& poolIn, T const& poolOut, T const& assetIn, std::uint16_t tfee)
{
using enum Number::RoundingMode;
auto const saved = T::getround();
T::setround(Upward);
auto const numerator = poolIn * poolOut;
auto const fee = detail::integer<T>(tfee) / detail::integer<T>(100'000);
T::setround(Downward);
auto const denom = poolIn + (assetIn * (detail::integer<T>(1) - fee));
T::setround(Upward);
auto const ratio = numerator / denom;
T::setround(Downward);
auto const swapOut = poolOut - ratio;
T::setround(saved);
return swapOut;
}
/**
* Amount of the in asset required to take `assetOut` out of the pool.
* Rounding at each step favors the AMM.
*/
template <NumberLike T>
T
ammSwapOut(T const& poolIn, T const& poolOut, T const& assetOut, std::uint16_t tfee)
{
using enum Number::RoundingMode;
auto const saved = T::getround();
T::setround(Upward);
auto const numerator = poolIn * poolOut;
T::setround(Downward);
auto const denom = poolOut - assetOut;
T::setround(Upward);
auto const ratio = numerator / denom;
auto const numerator2 = ratio - poolIn;
auto const fee = detail::integer<T>(tfee) / detail::integer<T>(100'000);
T::setround(Downward);
auto const feeMult = detail::integer<T>(1) - fee;
T::setround(Upward);
auto const swapIn = numerator2 / feeMult;
T::setround(saved);
return swapIn;
}
/**
* Shares minted for depositing `assets` into a non-empty vault, truncated to
* an integer (vault shares are MPTs).
*/
template <NumberLike T>
std::int64_t
vaultAssetsToShares(T const& shareTotal, T const& assetTotal, T const& assets)
{
auto const shares = (shareTotal * assets) / assetTotal;
detail::RoundGuard<T> const guard{Number::RoundingMode::TowardsZero};
return static_cast<std::int64_t>(shares);
}
/**
* Assets corresponding to `shares` of a non-empty vault.
*/
template <NumberLike T>
T
vaultSharesToAssets(T const& assetTotal, T const& shareTotal, T const& shares)
{
return (assetTotal * shares) / shareTotal;
}
constexpr std::uint32_t kSecondsInYear = 365 * 24 * 60 * 60;
/**
* Interest rate per payment interval, from an annual rate in 1/10 bips.
*/
template <NumberLike T>
T
loanPeriodicRate(std::uint32_t interestRateTenthBips, std::uint32_t paymentInterval)
{
auto const interval = detail::integer<T>(paymentInterval);
return interval * detail::integer<T>(interestRateTenthBips) / detail::integer<T>(100'000) /
detail::integer<T>(kSecondsInYear);
}
/**
* (1 + r)^n - 1: closed form, or a binomial series when r * n is tiny and the
* closed form would cancel catastrophically.
*/
template <NumberLike T>
T
loanPowerMinusOne(T const& rate, std::uint32_t payments)
{
auto const n = detail::integer<T>(payments);
if (n * rate >= T{1, -9})
return power(detail::integer<T>(1) + rate, payments) - detail::integer<T>(1);
T term = n * rate;
T sum = term;
for (std::uint32_t k = 1; k < payments; ++k)
{
term = term * rate * detail::integer<T>(payments - k) / detail::integer<T>(k + 1);
T const next = sum + term;
if (next == sum)
break;
sum = next;
}
return sum;
}
/**
* Level payment that amortizes `principal` over `payments` periods.
*/
template <NumberLike T>
T
loanPeriodicPayment(T const& principal, T const& rate, std::uint32_t payments)
{
auto const raisedMinusOne = loanPowerMinusOne(rate, payments);
auto const raised = detail::integer<T>(1) + raisedMinusOne;
return principal * ((rate * raised) / raisedMinusOne);
}
/**
* Remaining balance after making every scheduled payment. Exactly 0 in exact
* arithmetic; anything else is accumulated rounding error.
*/
template <NumberLike T>
T
loanAmortize(T const& principal, T const& rate, std::uint32_t payments)
{
auto const payment = loanPeriodicPayment(principal, rate, payments);
T balance = principal;
for (std::uint32_t i = 0; i < payments; ++i)
{
auto const interest = balance * rate;
balance -= payment - interest;
}
return balance;
}
} // namespace xrpl::number_bench
+395
View File
@@ -0,0 +1,395 @@
#pragma once
#include <xrpl/basics/Number.h>
#include <benchmarks/libxrpl/number/NumberLike.h>
#include <mpdecimal.h>
#include <cstdint>
#include <memory>
#include <string>
#include <utility>
namespace xrpl::number_bench {
/**
* mpdecimal (libmpdec) at a fixed precision, behind Number's interface.
*
* This is the correctly-rounded reference for the divergence harness: with
* `allcr` set, every operation, including `power`, is correctly rounded in
* the active rounding mode.
*
* `Digits` is the coefficient precision: 19 to mirror today's Number, 34 for
* IEEE decimal128, 38 for a full-uint128 Number. The exponent range follows
* Number's convention: value = coefficient * 10^e with e in
* [Number::kMinExponent, Number::kMaxExponent] for a `Digits`-digit
* coefficient. mpdecimal uses adjusted exponents (the exponent of the leading
* digit), hence the `Digits - 1` offset.
*
* Coefficients live in inline storage sized for `Digits`, so copying never
* allocates. Library temporaries may still allocate; that is part of
* mpdecimal's cost. Precisions above 38 digits exist for the divergence
* harness, which needs a near-exact result to round from; `decompose` is only
* available up to 38 digits.
*
* Unlike Number, results that overflow become infinity and underflow becomes
* subnormal or zero. Each operation ORs its mpdecimal status into a
* thread-local accumulator; read and clear it with `takeStatus()`.
*/
template <unsigned Digits>
class MpDecimal final
{
static_assert(Digits >= 16 && Digits <= 114);
// Enough 19-digit limbs for a Digits-digit coefficient, and never fewer
// than MPD_MINALLOC_MIN.
static constexpr mpd_ssize_t kLimbs = (Digits / MPD_RDIGITS) + 2;
mpd_uint_t data_[kLimbs]{};
// Zero: one limb holding 0.
mpd_t dec_{MPD_STATIC | MPD_STATIC_DATA, 0, 1, 1, kLimbs, data_};
struct Context
{
mpd_context_t ctx{};
uint32_t status = 0;
Context()
{
mpd_defaultcontext(&ctx);
ctx.prec = Digits;
ctx.emax = Number::kMaxExponent + static_cast<int>(Digits) - 1;
ctx.emin = Number::kMinExponent + static_cast<int>(Digits) - 1;
ctx.round = MPD_ROUND_HALF_EVEN;
ctx.traps = 0;
ctx.clamp = 0;
ctx.allcr = 1;
}
};
static Context&
context() noexcept
{
static thread_local Context c;
return c;
}
template <class F>
static MpDecimal
compute(F f)
{
MpDecimal result;
auto& c = context();
f(&result.dec_, &c.ctx, &c.status);
return result;
}
public:
using rep = std::int64_t;
static constexpr unsigned kDigits = Digits;
MpDecimal() noexcept = default;
~MpDecimal()
{
// Frees the coefficient only if an operation outgrew inline storage.
mpd_del(&dec_);
}
MpDecimal(MpDecimal const& other) noexcept
{
mpd_qcopy(&dec_, &other.dec_, &context().status);
}
MpDecimal&
operator=(MpDecimal const& other) noexcept
{
if (this != &other)
mpd_qcopy(&dec_, &other.dec_, &context().status);
return *this;
}
// Implicit, like Number(rep).
MpDecimal(rep mantissa) : MpDecimal(mantissa, 0) // NOLINT
{
}
explicit MpDecimal(rep mantissa, int exponent)
{
auto& c = context();
mpd_qset_i64_exact(&dec_, mantissa, &c.status);
dec_.exp += exponent;
mpd_qfinalize(&dec_, &c.ctx, &c.status);
}
MpDecimal
operator-() const
{
return compute(
[this](mpd_t* r, mpd_context_t* ctx, uint32_t* s) { mpd_qminus(r, &dec_, ctx, s); });
}
friend MpDecimal
operator+(MpDecimal const& x, MpDecimal const& y)
{
return compute([&](mpd_t* r, mpd_context_t* ctx, uint32_t* s) {
mpd_qadd(r, &x.dec_, &y.dec_, ctx, s);
});
}
friend MpDecimal
operator-(MpDecimal const& x, MpDecimal const& y)
{
return compute([&](mpd_t* r, mpd_context_t* ctx, uint32_t* s) {
mpd_qsub(r, &x.dec_, &y.dec_, ctx, s);
});
}
friend MpDecimal
operator*(MpDecimal const& x, MpDecimal const& y)
{
return compute([&](mpd_t* r, mpd_context_t* ctx, uint32_t* s) {
mpd_qmul(r, &x.dec_, &y.dec_, ctx, s);
});
}
friend MpDecimal
operator/(MpDecimal const& x, MpDecimal const& y)
{
return compute([&](mpd_t* r, mpd_context_t* ctx, uint32_t* s) {
mpd_qdiv(r, &x.dec_, &y.dec_, ctx, s);
});
}
MpDecimal&
operator+=(MpDecimal const& x)
{
return *this = *this + x;
}
MpDecimal&
operator-=(MpDecimal const& x)
{
return *this = *this - x;
}
MpDecimal&
operator*=(MpDecimal const& x)
{
return *this = *this * x;
}
MpDecimal&
operator/=(MpDecimal const& x)
{
return *this = *this / x;
}
friend bool
operator==(MpDecimal const& x, MpDecimal const& y) noexcept
{
return mpd_qcmp(&x.dec_, &y.dec_, &context().status) == 0;
}
friend bool
operator<(MpDecimal const& x, MpDecimal const& y) noexcept
{
return mpd_qcmp(&x.dec_, &y.dec_, &context().status) == -1;
}
friend bool
operator>(MpDecimal const& x, MpDecimal const& y) noexcept
{
return y < x;
}
friend bool
operator<=(MpDecimal const& x, MpDecimal const& y) noexcept
{
return x < y || x == y;
}
friend bool
operator>=(MpDecimal const& x, MpDecimal const& y) noexcept
{
return y <= x;
}
// Round to an integer using the current rounding mode, like Number.
explicit
operator rep() const
{
auto const rounded = compute([this](mpd_t* r, mpd_context_t* ctx, uint32_t* s) {
mpd_qround_to_int(r, &dec_, ctx, s);
});
return mpd_qget_i64(&rounded.dec_, &context().status);
}
static Number::RoundingMode
getround() noexcept
{
using enum Number::RoundingMode;
switch (context().ctx.round)
{
case MPD_ROUND_DOWN:
return TowardsZero;
case MPD_ROUND_FLOOR:
return Downward;
case MPD_ROUND_CEILING:
return Upward;
default:
return ToNearest;
}
}
static Number::RoundingMode
setround(Number::RoundingMode mode) noexcept
{
using enum Number::RoundingMode;
auto const old = getround();
auto& round = context().ctx.round;
switch (mode)
{
case ToNearest:
round = MPD_ROUND_HALF_EVEN;
break;
case TowardsZero:
round = MPD_ROUND_DOWN;
break;
case Downward:
round = MPD_ROUND_FLOOR;
break;
case Upward:
round = MPD_ROUND_CEILING;
break;
}
return old;
}
/**
* Returns the mpdecimal status flags raised since the last call, and clears them.
*/
static uint32_t
takeStatus() noexcept
{
return std::exchange(context().status, 0);
}
friend std::string
to_string(MpDecimal const& x)
{
char* raw = nullptr;
auto const size = mpd_to_sci_size(&raw, &x.dec_, 0);
std::unique_ptr<char, void (*)(void*)> const owned{raw, mpd_free};
return size < 0 ? std::string{} : std::string{raw, static_cast<std::size_t>(size)};
}
friend MpDecimal
abs(MpDecimal const& x)
{
return compute(
[&](mpd_t* r, mpd_context_t* ctx, uint32_t* s) { mpd_qabs(r, &x.dec_, ctx, s); });
}
friend MpDecimal
power(MpDecimal const& x, unsigned n)
{
MpDecimal const exponent{static_cast<rep>(n)};
return compute([&](mpd_t* r, mpd_context_t* ctx, uint32_t* s) {
mpd_qpow(r, &x.dec_, &exponent.dec_, ctx, s);
});
}
friend MpDecimal
root2(MpDecimal const& x)
{
return compute(
[&](mpd_t* r, mpd_context_t* ctx, uint32_t* s) { mpd_qsqrt(r, &x.dec_, ctx, s); });
}
/**
* The underlying libmpdec value, for the divergence harness.
*/
[[nodiscard]] mpd_t const*
get() const noexcept
{
return &dec_;
}
[[nodiscard]] mpd_t*
get() noexcept
{
return &dec_;
}
/**
* Rounds `x` to this type's precision in the current rounding mode.
*/
template <unsigned OtherDigits>
static MpDecimal
roundFrom(MpDecimal<OtherDigits> const& x)
{
MpDecimal result;
auto& c = context();
mpd_qcopy(&result.dec_, x.get(), &c.status);
mpd_qfinalize(&result.dec_, &c.ctx, &c.status);
return result;
}
/**
* Exact conversion; rounds only if `d` has more than Digits digits.
*/
static MpDecimal
fromDecomposed(Decomposed const& d)
{
auto& c = context();
mpd_context_t exact;
mpd_maxcontext(&exact);
exact.traps = 0;
// coefficient = hi * 2^64 + lo, assembled at unlimited precision.
MpDecimal result;
MpDecimal lo;
MpDecimal shift;
mpd_qset_u64_exact(
&result.dec_, static_cast<std::uint64_t>(d.coefficient >> 64), &c.status);
mpd_qset_u64_exact(&lo.dec_, static_cast<std::uint64_t>(d.coefficient), &c.status);
mpd_qset_u64_exact(&shift.dec_, std::uint64_t{1} << 32, &c.status);
mpd_qmul(&shift.dec_, &shift.dec_, &shift.dec_, &exact, &c.status);
mpd_qmul(&result.dec_, &result.dec_, &shift.dec_, &exact, &c.status);
mpd_qadd(&result.dec_, &result.dec_, &lo.dec_, &exact, &c.status);
if (d.negative)
mpd_set_negative(&result.dec_);
result.dec_.exp += d.exponent;
mpd_qfinalize(&result.dec_, &c.ctx, &c.status);
return result;
}
/**
* Precondition: x is finite (check takeStatus() for overflow/invalid first).
*/
friend Decomposed
decompose(MpDecimal const& x) noexcept
requires(Digits <= 38)
{
unsigned __int128 coefficient = 0;
for (auto i = x.dec_.len; i-- > 0;)
coefficient = (coefficient * MPD_RADIX) + x.dec_.data[i];
return {
.negative = (x.dec_.flags & MPD_NEG) != 0,
.coefficient = coefficient,
.exponent = static_cast<int>(x.dec_.exp)};
}
};
using MpDecimal19 = MpDecimal<19>;
using MpDecimal34 = MpDecimal<34>;
using MpDecimal38 = MpDecimal<38>;
static_assert(DecomposableNumber<MpDecimal19>);
static_assert(DecomposableNumber<MpDecimal34>);
static_assert(DecomposableNumber<MpDecimal38>);
} // namespace xrpl::number_bench
+37
View File
@@ -0,0 +1,37 @@
#pragma once
// Slot for a 128-bit Number prototype.
//
// When <xrpl/basics/Number128.h> exists, CMake defines XRPL_BENCH_NUMBER128
// (see src/benchmarks/libxrpl/CMakeLists.txt) and the type is benchmarked and
// checked by every tool here with no further changes: xrpl.bench.number (single
// operations and ledger formulas) and xrpl.bench.number_diff (rounding
// correctness in all four modes). The header must provide `xrpl::Number128`
// satisfying DecomposableNumber (see NumberLike.h), plus:
//
// static constexpr unsigned kDigits; // mantissa precision, e.g. 34 or 38
//
// number_diff rounds exact results onto a plain kDigits-digit grid. Up to 38
// digits that is exact: 10^38 - 1 is below the signed 128-bit maximum, so
// there is no capped region like today's Number has above 2^63 - 1. If the
// prototype's grid differs, extend `Grid` in number_diff/main.cpp.
#ifdef XRPL_BENCH_NUMBER128
#include <xrpl/basics/Number128.h>
#include <benchmarks/libxrpl/number/NumberLike.h>
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define XRPL_BENCH_HAS_NUMBER128 1
namespace xrpl::number_bench {
static_assert(
DecomposableNumber<Number128>,
"Number128 must provide Number's interface; see NumberLike.h");
static_assert(Number128::kDigits <= 38, "Decomposed holds at most 38 digits");
} // namespace xrpl::number_bench
#endif
@@ -0,0 +1,91 @@
#pragma once
#include <xrpl/basics/Number.h>
#include <concepts>
#include <cstdint>
#include <string>
#include <string_view>
namespace xrpl::number_bench {
/**
* Sign, coefficient, and exponent of a finite decimal value:
* value = (negative ? -1 : 1) * coefficient * 10^exponent.
*
* The coefficient is 128 bits wide so the same struct can describe 34- and
* 38-digit types. Different libraries normalize to different precisions, so
* compare decompositions with `canonical()`, which strips trailing zeros.
*/
struct Decomposed
{
bool negative = false;
unsigned __int128 coefficient = 0;
int exponent = 0;
[[nodiscard]] constexpr Decomposed
canonical() const noexcept
{
if (coefficient == 0)
return {};
Decomposed result = *this;
while (result.coefficient % 10 == 0)
{
result.coefficient /= 10;
++result.exponent;
}
return result;
}
friend constexpr bool
operator==(Decomposed const&, Decomposed const&) = default;
};
[[nodiscard]] inline Decomposed
decompose(Number const& x) noexcept
{
auto const m = x.mantissa();
// Negating INT64_MIN is UB, but Number never produces it (see Number.h).
auto const magnitude = static_cast<std::uint64_t>(m < 0 ? -m : m);
return {.negative = m < 0, .coefficient = magnitude, .exponent = x.exponent()};
}
/**
* The subset of Number's interface that ledger code relies on. Benchmarks and
* the divergence harness are written against this concept, so any type
* satisfying it, including a future 128-bit Number, can be dropped in.
*/
template <class T>
concept NumberLike = std::copyable<T> && std::totally_ordered<T> &&
requires(T a, T b, std::int64_t mantissa, int exponent, unsigned n, Number::RoundingMode mode) {
T{mantissa};
T{mantissa, exponent};
{ a + b } -> std::same_as<T>;
{ a - b } -> std::same_as<T>;
{ a * b } -> std::same_as<T>;
{ a / b } -> std::same_as<T>;
{ -a } -> std::same_as<T>;
{ a += b } -> std::same_as<T&>;
{ a -= b } -> std::same_as<T&>;
{ a *= b } -> std::same_as<T&>;
{ a /= b } -> std::same_as<T&>;
static_cast<std::int64_t>(a);
{ to_string(a) } -> std::same_as<std::string>;
{ abs(a) } -> std::same_as<T>;
{ power(a, n) } -> std::same_as<T>;
{ root2(a) } -> std::same_as<T>;
{ T::setround(mode) } -> std::same_as<Number::RoundingMode>;
{ T::getround() } -> std::same_as<Number::RoundingMode>;
};
/**
* A NumberLike type whose exact value can be inspected digit by digit.
*/
template <class T>
concept DecomposableNumber = NumberLike<T> && requires(T a) {
{ decompose(a) } -> std::same_as<Decomposed>;
};
static_assert(DecomposableNumber<Number>);
} // namespace xrpl::number_bench
+250
View File
@@ -0,0 +1,250 @@
#pragma once
#include <xrpl/basics/Number.h>
#include <benchmarks/libxrpl/number/NumberLike.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <random>
#include <string_view>
#include <utility>
#include <vector>
namespace xrpl::number_bench {
/**
* Operand count per batch: small enough to stay in L1 for 32-byte types.
*/
constexpr std::size_t kBatchSize = 1024;
/**
* Raw operand: value = mantissa * 10^exponent.
*/
struct RawOperand
{
std::int64_t mantissa;
int exponent;
};
/**
* Operand distributions. Each one models a class of values the ledger works
* with, because cost depends on digit count and exponent alignment.
*/
enum class Dataset {
/**
* Full-width 19-digit mantissas in [10^18, 2^63-1], exponents in [-10, 10].
*/
Full,
/**
* IOU-like 16-digit mantissas in [10^15, 10^16-1], exponents in [-25, 5].
*/
Iou,
/**
* XRP drop amounts: integers with 1 to 17 digits (log-uniform), exponent 0.
*/
Drops,
/**
* MPT amounts: integers with 1 to 19 digits up to 2^63-1 (log-uniform), exponent 0.
*/
Mpt,
/**
* Rates in [1, 1.01) with full-width mantissas: the operand of interest and amortization
* math, and of long multiply/divide chains that must not overflow.
*/
Rate,
/**
* Non-integers below 10^18 (19-digit mantissas, exponents in [-18, -1]): rounding to int64.
*/
Fraction,
/**
* Exact halves, m + 0.5 with m < 10^17: the tie cases of rounding to int64.
*/
Half,
};
constexpr std::string_view
toString(Dataset d)
{
switch (d)
{
case Dataset::Full:
return "full";
case Dataset::Iou:
return "iou";
case Dataset::Drops:
return "drops";
case Dataset::Mpt:
return "mpt";
case Dataset::Rate:
return "rate";
case Dataset::Fraction:
return "fraction";
case Dataset::Half:
return "half";
}
return "unknown";
}
namespace detail {
/**
* Integer with a log-uniform digit count in [1, maxDigits], capped at `cap`.
*/
inline std::int64_t
logUniformInteger(std::mt19937_64& rng, int maxDigits, std::int64_t cap)
{
auto const digits = std::uniform_int_distribution<int>{1, maxDigits}(rng);
auto const lo = static_cast<std::int64_t>(kPowerOfTen[digits - 1]);
auto const hi =
digits >= 19 ? cap : std::min(cap, static_cast<std::int64_t>(kPowerOfTen[digits]) - 1);
return std::uniform_int_distribution<std::int64_t>{lo, hi}(rng);
}
} // namespace detail
/**
* Deterministic: the same dataset and seed always produce the same operands.
*/
inline std::vector<RawOperand>
makeRaw(Dataset d, std::uint64_t seed, std::size_t count = kBatchSize)
{
constexpr auto kMaxInt64 = std::numeric_limits<std::int64_t>::max();
constexpr auto kE18 = static_cast<std::int64_t>(kPowerOfTen[18]);
constexpr auto kE16 = static_cast<std::int64_t>(kPowerOfTen[16]);
constexpr auto kE15 = static_cast<std::int64_t>(kPowerOfTen[15]);
std::mt19937_64 rng{seed};
std::vector<RawOperand> result;
result.reserve(count);
for (std::size_t i = 0; i < count; ++i)
{
switch (d)
{
case Dataset::Full:
result.push_back(
{std::uniform_int_distribution<std::int64_t>{kE18, kMaxInt64}(rng),
std::uniform_int_distribution<int>{-10, 10}(rng)});
break;
case Dataset::Iou:
result.push_back(
{std::uniform_int_distribution<std::int64_t>{kE15, kE16 - 1}(rng),
std::uniform_int_distribution<int>{-25, 5}(rng)});
break;
case Dataset::Drops:
result.push_back({detail::logUniformInteger(rng, 17, kMaxInt64), 0});
break;
case Dataset::Mpt:
result.push_back({detail::logUniformInteger(rng, 19, kMaxInt64), 0});
break;
case Dataset::Rate:
// [10^18, 1.01 * 10^18) * 10^-18 = [1, 1.01)
result.push_back(
{std::uniform_int_distribution<std::int64_t>{
kE18, kE18 + (kE18 / 100) - 1}(rng),
-18});
break;
case Dataset::Fraction:
result.push_back(
{std::uniform_int_distribution<std::int64_t>{kE18, kMaxInt64}(rng),
std::uniform_int_distribution<int>{-18, -1}(rng)});
break;
case Dataset::Half:
result.push_back(
{(std::uniform_int_distribution<std::int64_t>{0, (kE18 / 10) - 1}(rng) * 10) +
5,
-1});
break;
}
}
return result;
}
template <NumberLike T>
std::vector<T>
materialize(std::vector<RawOperand> const& raw)
{
std::vector<T> result;
result.reserve(raw.size());
for (auto const& r : raw)
result.push_back(T{r.mantissa, r.exponent});
return result;
}
/**
* Pairs of full-width operands whose exponents differ by exactly `gap`, so
* addition must shift (and round away) `gap` digits of the smaller operand.
*/
inline std::pair<std::vector<RawOperand>, std::vector<RawOperand>>
makeExponentGapPairs(int gap, std::uint64_t seed, std::size_t count = kBatchSize)
{
auto lhs = makeRaw(Dataset::Full, seed, count);
auto rhs = makeRaw(Dataset::Full, seed + 1, count);
for (std::size_t i = 0; i < count; ++i)
rhs[i].exponent = lhs[i].exponent - gap;
return {std::move(lhs), std::move(rhs)};
}
/**
* Pairs of nearly equal full-width operands, so subtraction cancels most
* leading digits and the result must be renormalized by many places.
*/
inline std::pair<std::vector<RawOperand>, std::vector<RawOperand>>
makeCancellationPairs(std::uint64_t seed, std::size_t count = kBatchSize)
{
auto lhs = makeRaw(Dataset::Full, seed, count);
std::vector<RawOperand> rhs;
rhs.reserve(count);
std::mt19937_64 rng{seed + 1};
std::uniform_int_distribution<std::int64_t> delta{1, 1000};
for (auto const& l : lhs)
rhs.push_back({l.mantissa - delta(rng), l.exponent});
return {std::move(lhs), std::move(rhs)};
}
/**
* Pairs whose exact sum is exactly halfway between two adjacent 19-digit
* values: a = m * 10^e, b = 5 * 10^(e-1). Exercises round-half-even.
*/
inline std::pair<std::vector<RawOperand>, std::vector<RawOperand>>
makeTiePairs(std::uint64_t seed, std::size_t count = kBatchSize)
{
auto lhs = makeRaw(Dataset::Full, seed, count);
std::vector<RawOperand> rhs;
rhs.reserve(count);
for (auto const& l : lhs)
rhs.push_back({5, l.exponent - 1});
return {std::move(lhs), std::move(rhs)};
}
/**
* Pairs whose sum straddles 2^63-1 (Number::kMaxRep), where Number's
* representable values switch from a step of 1 to a step of 10. The addend has
* one more decimal place than the base, so most sums fall between
* representable values and must be rounded; sums are within ±200 of the cap,
* so a fair share land in (2^63-1, 2^63), the interval where LargeLegacy's
* known cusp-rounding error shows.
*/
inline std::pair<std::vector<RawOperand>, std::vector<RawOperand>>
makeCuspPairs(std::uint64_t seed, std::size_t count = kBatchSize)
{
constexpr auto kMaxInt64 = std::numeric_limits<std::int64_t>::max();
std::mt19937_64 rng{seed};
std::uniform_int_distribution<std::int64_t> base{kMaxInt64 - 100, kMaxInt64};
std::uniform_int_distribution<std::int64_t> addend{1, 2'000};
std::uniform_int_distribution<int> exponent{-10, 10};
std::pair<std::vector<RawOperand>, std::vector<RawOperand>> result;
result.first.reserve(count);
result.second.reserve(count);
for (std::size_t i = 0; i < count; ++i)
{
auto const e = exponent(rng);
result.first.push_back({base(rng), e});
result.second.push_back({addend(rng), e - 1});
}
return result;
}
} // namespace xrpl::number_bench
+309
View File
@@ -0,0 +1,309 @@
#!/usr/bin/env python3
"""Render the figures in docs/NumberDecimalBenchmark.md from benchmark JSON.
Usage:
plot_results.py CORE_JSON KERNELS_JSON OUT_DIR
CORE_JSON is Google Benchmark JSON from xrpl.bench.number filtered to
'^(add|mul|div)/full/'; KERNELS_JSON from '^kernel/' with mpdecimal enabled
(for the accuracy counters). Both need --benchmark_repetitions so medians are
available. Writes self-contained SVG files with light and dark variants
(prefers-color-scheme) and no dependencies beyond the standard library.
"""
from __future__ import annotations
import json
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Callable
from xml.sax.saxutils import escape
# Categorical slots 1-3 of the reference palette (validated all-pairs, light
# and dark). Color encodes the type family; every bar is also labeled.
FAMILIES = {
"number": ("Number today (19 digits)", "#2a78d6", "#3987e5"),
"ieee64": ("16-digit types", "#eb6834", "#d95926"),
"wide": ("34- and 38-digit types", "#1baf7a", "#199e70"),
}
# (benchmark subject, row label, family)
CORE_ROWS = [
("Number.Large330", "Number (Large330)", "number"),
("BoostDecimal64", "Boost decimal64", "ieee64"),
("BoostDecimalFast64", "Boost decimal_fast64", "ieee64"),
("IntelBid64", "Intel BID64", "ieee64"),
("BoostDecimal128", "Boost decimal128", "wide"),
("BoostDecimalFast128", "Boost decimal_fast128", "wide"),
("IntelBid128", "Intel BID128", "wide"),
("MpDecimal34", "mpdecimal 34", "wide"),
("MpDecimal38", "mpdecimal 38", "wide"),
]
KERNEL_ROWS = [
("Number.Large330", "Number (Large330)", "number"),
("BoostDecimal64", "Boost decimal64", "ieee64"),
("IntelBid64", "Intel BID64", "ieee64"),
("BoostDecimal128", "Boost decimal128", "wide"),
("IntelBid128", "Intel BID128", "wide"),
("MpDecimal34", "mpdecimal 34", "wide"),
("MpDecimal38", "mpdecimal 38", "wide"),
]
FONT = "system-ui, -apple-system, sans-serif"
ROW_H = 24
BAR_H = 12
LABEL_W = 150
PANEL_W = 200
PANEL_GAP = 28
TOP = 78
BOTTOM = 30
@dataclass
class Panel:
title: str
unit: str
values: dict[str, float]
fmt: Callable[[float], str]
# Panels with the same group share an x scale; use it only for panels
# with the same unit, so bar lengths are comparable.
group: str = ""
def medians(path: str) -> dict[str, dict]:
"""Map run_name to its median aggregate."""
data = json.loads(Path(path).read_text())
return {
b["run_name"]: b
for b in data["benchmarks"]
if b.get("aggregate_name") == "median"
}
def ns_per_item(b: dict) -> float:
return 1e9 / b["items_per_second"]
def nice_max(v: float) -> tuple[float, list[float]]:
"""Axis maximum and ticks: 4-5 round steps covering v."""
for step in [
1,
2,
2.5,
5,
10,
20,
25,
50,
100,
200,
250,
500,
1000,
2000,
2500,
5000,
10000,
20000,
25000,
50000,
]:
if v / step <= 5:
top = step * -(-v // step)
return top, [step * i for i in range(int(top / step) + 1)]
return v, [0, v]
def bar_path(x: float, y: float, w: float, h: float, r: float = 4) -> str:
"""Bar anchored at the baseline (left) with a rounded data end (right)."""
r = min(r, w, h / 2)
return (
f"M{x:.1f},{y:.1f} H{x + w - r:.1f} "
f"Q{x + w:.1f},{y:.1f} {x + w:.1f},{y + r:.1f} "
f"V{y + h - r:.1f} Q{x + w:.1f},{y + h:.1f} {x + w - r:.1f},{y + h:.1f} "
f"H{x:.1f} Z"
)
def render(
title: str, rows: list[tuple[str, str, str]], panels: list[Panel], out: Path
) -> None:
"""Horizontal bar panels sharing one column of row labels."""
width = LABEL_W + len(panels) * (PANEL_W + PANEL_GAP)
height = TOP + len(rows) * ROW_H + BOTTOM
css_light = "".join(
f".f-{k}{{fill:{light}}}" for k, (_, light, _) in FAMILIES.items()
)
css_dark = "".join(f".f-{k}{{fill:{dark}}}" for k, (_, _, dark) in FAMILIES.items())
out_lines = [
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {width} {height}" '
f'width="{width}" height="{height}" role="img" '
f'aria-label="{escape(title)}">',
"<style>",
f"text{{font-family:{FONT};font-size:12px}}",
".bg{fill:#fcfcfb}.t1{fill:#0b0b0b}.t2{fill:#52514e}"
".grid{stroke:#e4e3df;stroke-width:1}.base{stroke:#b9b8b2;stroke-width:1}"
+ css_light,
"@media (prefers-color-scheme: dark){"
".bg{fill:#1a1a19}.t1{fill:#ffffff}.t2{fill:#c3c2b7}"
".grid{stroke:#383835}.base{stroke:#5e5d58}" + css_dark + "}",
"</style>",
f'<rect class="bg" width="{width}" height="{height}"/>',
f'<text class="t1" x="0" y="18" font-size="15" font-weight="600">'
f"{escape(title)}</text>",
]
# Legend: always present for more than one family.
lx = 0.0
for key, (label, _, _) in FAMILIES.items():
out_lines.append(
f'<rect class="f-{key}" x="{lx}" y="32" width="12" height="12" rx="3"/>'
)
out_lines.append(
f'<text class="t2" x="{lx + 18}" y="42">{escape(label)}</text>'
)
lx += 18 + 7.2 * len(label) + 24
for i, (_, label, _) in enumerate(rows):
y = TOP + i * ROW_H + ROW_H / 2 + 4
out_lines.append(f'<text class="t1" x="0" y="{y:.1f}">{escape(label)}</text>')
for p_index, panel in enumerate(panels):
x0 = LABEL_W + p_index * (PANEL_W + PANEL_GAP)
vmax = max(
p.values.get(s, 0)
for p in panels
if p is panel or (panel.group and p.group == panel.group)
for s, _, _ in rows
)
top, ticks = nice_max(vmax * 1.08)
scale = (PANEL_W - 40) / top
out_lines.append(
f'<text class="t1" x="{x0}" y="{TOP - 14}" font-weight="600">'
f"{escape(panel.title)}</text>"
)
y_end = TOP + len(rows) * ROW_H
for t in ticks:
gx = x0 + t * scale
out_lines.append(
f'<line class="grid" x1="{gx:.1f}" y1="{TOP}" x2="{gx:.1f}" y2="{y_end}"/>'
)
out_lines.append(
f'<text class="t2" x="{gx:.1f}" y="{y_end + 16}" text-anchor="middle" '
f'font-size="11">{t:g}</text>'
)
out_lines.append(
f'<text class="t2" x="{x0 + PANEL_W - 40:.1f}" y="{y_end + 28}" '
f'text-anchor="end" font-size="11">{escape(panel.unit)}</text>'
)
for i, (subject, label, family) in enumerate(rows):
if subject not in panel.values:
continue
v = panel.values[subject]
y = TOP + i * ROW_H + (ROW_H - BAR_H) / 2
w = max(v * scale, 1.0)
out_lines.append(
f'<path class="f-{family}" d="{bar_path(x0, y, w, BAR_H)}">'
f"<title>{escape(label)}: {panel.fmt(v)} {escape(panel.unit)}</title></path>"
)
out_lines.append(
f'<text class="t2" x="{x0 + w + 5:.1f}" y="{y + BAR_H - 2:.1f}" '
f'font-size="11">{panel.fmt(v)}</text>'
)
out_lines.append(
f'<line class="base" x1="{x0}" y1="{TOP}" x2="{x0}" y2="{y_end}"/>'
)
out_lines.append("</svg>")
out.write_text("\n".join(out_lines) + "\n")
def fmt_ns(v: float) -> str:
return f"{v:.0f}" if v >= 10 else f"{v:.1f}"
def fmt_us(v: float) -> str:
return f"{v:.0f}" if v >= 10 else f"{v:.1f}"
def fmt_digits(v: float) -> str:
return f"{v:.1f}"
def main(argv: list[str]) -> int:
if len(argv) != 4:
print(__doc__, file=sys.stderr)
return 2
core = medians(argv[1])
kernels = medians(argv[2])
out_dir = Path(argv[3])
out_dir.mkdir(parents=True, exist_ok=True)
core_panels = [
Panel(
op_title,
"ns per operation",
{
s: ns_per_item(core[f"{op}/full/{s}"])
for s, _, _ in CORE_ROWS
if f"{op}/full/{s}" in core
},
fmt_ns,
group="ns",
)
for op, op_title in [("add", "Add"), ("mul", "Multiply"), ("div", "Divide")]
]
render(
"Cost of one operation (full-width operands, lower is faster)",
CORE_ROWS,
core_panels,
out_dir / "number-core-ops.svg",
)
def kernel(name: str) -> dict[str, dict]:
prefix = f"kernel/{name}/"
return {k[len(prefix) :]: v for k, v in kernels.items() if k.startswith(prefix)}
for name, title, unit, divisor, fmt in [
("amm_swap_in", "AMM swap-in", "ns per call", 1.0, fmt_ns),
(
"loan_amortize360",
"360-payment amortization",
"µs per schedule",
1e3,
fmt_us,
),
]:
k = kernel(name)
render(
f"{title}: correct digits (higher is better) and cost (lower is faster)",
KERNEL_ROWS,
[
Panel(
"Correct digits, worst case",
"significant digits",
{s: b["digits_min"] for s, b in k.items()},
fmt_digits,
group="digits",
),
Panel(
"Correct digits, median",
"significant digits",
{s: b["digits_median"] for s, b in k.items()},
fmt_digits,
group="digits",
),
Panel(
"Cost",
unit,
{s: ns_per_item(b) / divisor for s, b in k.items()},
fmt,
),
],
out_dir / f"number-{name.replace('_', '-')}.svg",
)
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
+555
View File
@@ -0,0 +1,555 @@
// number_diff: measures how far Number's results are from correctly rounded
// results. See docs/NumberDecimalBenchmark.md, "Divergence harness".
//
// For every operation, the operands are converted exactly into a 96-digit
// mpdecimal value and the operation is evaluated there (correctly rounded at
// 96 digits). That near-exact result is then rounded onto the subject type's
// own grid of representable values in the active rounding mode, and compared
// with what the subject actually returned.
//
// Rounding a 96-digit result again creates a false tie only if digits 20-96
// of a non-terminating result happen to round to exactly 5000...0, which
// will not occur at these sample sizes.
//
// Usage: xrpl.bench.number_diff [--samples N] [--csv]
#include <xrpl/basics/Number.h>
#include <benchmarks/libxrpl/number/BoostDecimal.h>
#include <benchmarks/libxrpl/number/MpDecimal.h>
#include <benchmarks/libxrpl/number/Number128.h>
#include <benchmarks/libxrpl/number/NumberLike.h>
#include <benchmarks/libxrpl/number/Operands.h>
#ifdef XRPL_BENCH_INTEL_DFP
#include <benchmarks/libxrpl/number/IntelBid.h>
#endif
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <exception>
#include <format>
#include <functional>
#include <iostream>
#include <limits>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace xrpl::number_bench {
namespace {
using Exact = MpDecimal<96>;
constexpr std::uint64_t kSeedLhs = 0xd1ff'0001;
constexpr std::uint64_t kSeedRhs = 0xd1ff'0002;
/**
* The set of values a type can represent, as far as rounding is concerned.
* `int64Cap` models Number's large scales, whose mantissas above 2^63-1 must
* end in 0 (see Number.h, "External Interface").
*/
struct Grid
{
unsigned digits;
bool int64Cap = false;
};
/**
* Integers: the grid of conversions to int64.
*/
struct IntegerGrid
{
};
// ---- Exact arithmetic helpers ----
Exact
fromSubject(DecomposableNumber auto const& x)
{
return Exact::fromDecomposed(decompose(x));
}
/**
* Rounds a non-negative value to `digits` significant digits, toward (down) or away from (up)
* zero.
*/
Exact
roundMagnitude(Exact x, unsigned digits, bool up)
{
mpd_context_t ctx;
mpd_maxcontext(&ctx);
ctx.prec = digits;
ctx.round = up ? MPD_ROUND_UP : MPD_ROUND_DOWN;
ctx.traps = 0;
std::uint32_t status = 0;
mpd_qfinalize(x.get(), &ctx, &status);
return x;
}
/**
* Rounds a non-negative value to an integer, toward (down) or away from (up) zero.
*/
Exact
roundMagnitudeToInteger(Exact const& x, bool up)
{
mpd_context_t ctx;
mpd_maxcontext(&ctx);
ctx.round = up ? MPD_ROUND_UP : MPD_ROUND_DOWN;
ctx.traps = 0;
std::uint32_t status = 0;
Exact result;
mpd_qround_to_int(result.get(), x.get(), &ctx, &status);
return result;
}
bool
isZero(Exact const& x)
{
return mpd_iszero(x.get()) != 0;
}
bool
isNegative(Exact const& x)
{
return mpd_isnegative(x.get()) != 0 && !isZero(x);
}
/**
* True if q (a non-negative integer) is even.
*/
bool
isEvenInteger(Exact const& q)
{
auto const s = to_string(q);
auto const pos = s.find_first_of("Ee");
// Positive exponent in scientific notation means trailing zeros.
if (pos != std::string::npos && s[pos + 1] != '-')
return true;
auto const digits = s.substr(0, pos);
return ((digits.back() - '0') % 2) == 0;
}
// ---- Candidate representable values around an exact result ----
/**
* The representable magnitudes immediately at or below and at or above |x|.
*/
struct Bracket
{
Exact lo;
Exact hi;
/**
* Grid spacing at lo: lo / spacing is lo's integer coefficient.
*/
Exact loSpacing;
/**
* lo is 2^63-1 and hi is its successor 2^63+3: the two candidates straddle Number's cap.
*/
bool straddlesCap = false;
};
Bracket
bracket(Exact const& magnitude, Grid grid)
{
auto const adjusted = static_cast<int>(mpd_adjexp(magnitude.get()));
auto lo = roundMagnitude(magnitude, grid.digits, false);
auto hi = roundMagnitude(magnitude, grid.digits, true);
Exact loSpacing{1, adjusted - static_cast<int>(grid.digits) + 1};
bool straddlesCap = false;
if (grid.int64Cap)
{
// Above cap, only every tenth 19-digit value is representable.
Exact const cap{static_cast<std::int64_t>(Number::kMaxRep), adjusted - 18};
if (hi > cap)
hi = roundMagnitude(magnitude, 18, true);
if (lo > cap)
{
auto const lo18 = roundMagnitude(magnitude, 18, false);
if (lo18 < cap)
{
lo = cap;
straddlesCap = true;
}
else
{
lo = lo18;
loSpacing = Exact{1, adjusted - 17};
}
}
}
return {std::move(lo), std::move(hi), std::move(loSpacing), straddlesCap};
}
/**
* The correctly rounded magnitude, or nullopt if two candidates are equally valid (a cusp tie).
*/
std::optional<Exact>
choose(Exact const& magnitude, Bracket const& b, bool negative, Number::RoundingMode mode)
{
using enum Number::RoundingMode;
if (b.lo == b.hi)
return b.lo;
switch (mode)
{
case TowardsZero:
return b.lo;
case Downward:
return negative ? b.hi : b.lo;
case Upward:
return negative ? b.lo : b.hi;
case ToNearest:
break;
}
auto const below = magnitude - b.lo;
auto const above = b.hi - magnitude;
if (below < above)
return b.lo;
if (above < below)
return b.hi;
// Tie at the cap: 2^63-1 and 2^63+3 are both odd, so "even" is undefined.
if (b.straddlesCap)
return std::nullopt;
return isEvenInteger(b.lo / b.loSpacing) ? b.lo : b.hi;
}
// ---- Tallies ----
struct Tally
{
std::size_t samples = 0;
/**
* Exactly the correctly rounded result.
*/
std::size_t correct = 0;
/**
* The other neighbor of the exact result: rounded in the wrong direction.
*/
std::size_t wrongNeighbor = 0;
/**
* Neither neighbor: off by more than one step.
*/
std::size_t worse = 0;
/**
* Returned zero for a non-zero result.
*/
std::size_t flushed = 0;
/**
* Threw instead of returning.
*/
std::size_t threw = 0;
/**
* Largest |result - exact| / local grid spacing.
*/
double maxError = 0;
};
void
classify(Tally& t, Exact const& result, Exact const& exact, Grid grid, Number::RoundingMode mode)
{
if (isZero(exact))
{
++(isZero(result) ? t.correct : t.worse);
return;
}
if (isZero(result))
{
++t.flushed;
return;
}
bool const negative = isNegative(exact);
auto const magnitude = abs(exact);
auto const b = bracket(magnitude, grid);
auto const resultMagnitude = abs(result);
auto const chosen = choose(magnitude, b, negative, mode);
bool const sameSign = isNegative(result) == negative;
bool const isLo = sameSign && resultMagnitude == b.lo;
bool const isHi = sameSign && resultMagnitude == b.hi;
if (sameSign && (chosen ? resultMagnitude == *chosen : (isLo || isHi)))
++t.correct;
else if (isLo || isHi)
++t.wrongNeighbor;
else
++t.worse;
if (b.lo != b.hi)
{
auto const error = abs(result - exact) / (b.hi - b.lo);
t.maxError = std::max(t.maxError, std::stod(to_string(error)));
}
}
void
classifyInteger(Tally& t, std::int64_t result, Exact const& exact, Number::RoundingMode mode)
{
bool const negative = isNegative(exact);
auto const magnitude = abs(exact);
auto const lo = roundMagnitudeToInteger(magnitude, false);
auto const hi = roundMagnitudeToInteger(magnitude, true);
Bracket const b{lo, hi, Exact{1}};
auto const chosen = *choose(magnitude, b, negative, mode);
Exact const r{result};
auto const resultMagnitude = abs(r);
bool const sameSign = result == 0 || (result < 0) == negative;
if (sameSign && resultMagnitude == chosen)
++t.correct;
else if (sameSign && (resultMagnitude == lo || resultMagnitude == hi))
++t.wrongNeighbor;
else
++t.worse;
t.maxError = std::max(t.maxError, std::stod(to_string(abs(r - exact))));
}
// ---- Running cells ----
struct Row
{
std::string subject;
Number::RoundingMode mode;
std::string op;
std::string data;
Tally tally;
};
template <DecomposableNumber T, class BinaryOp>
Tally
runBinary(
std::vector<RawOperand> const& lhs,
std::vector<RawOperand> const& rhs,
BinaryOp op,
Grid grid,
Number::RoundingMode mode)
{
Tally t;
for (std::size_t i = 0; i < lhs.size(); ++i)
{
T const a{lhs[i].mantissa, lhs[i].exponent};
T const b{rhs[i].mantissa, rhs[i].exponent};
auto const exact = op(fromSubject(a), fromSubject(b));
++t.samples;
try
{
classify(t, fromSubject(T{op(a, b)}), exact, grid, mode);
}
catch (std::exception const&)
{
++t.threw;
}
}
return t;
}
template <DecomposableNumber T, class UnaryOp>
Tally
runUnary(std::vector<RawOperand> const& operands, UnaryOp op, Grid grid, Number::RoundingMode mode)
{
Tally t;
for (auto const& raw : operands)
{
T const a{raw.mantissa, raw.exponent};
auto const exact = op(fromSubject(a));
++t.samples;
try
{
classify(t, fromSubject(T{op(a)}), exact, grid, mode);
}
catch (std::exception const&)
{
++t.threw;
}
}
return t;
}
template <DecomposableNumber T>
Tally
runToInt64(std::vector<RawOperand> const& operands, Number::RoundingMode mode)
{
Tally t;
for (auto const& raw : operands)
{
T const a{raw.mantissa, raw.exponent};
auto const exact = fromSubject(a);
++t.samples;
try
{
classifyInteger(t, static_cast<std::int64_t>(a), exact, mode);
}
catch (std::exception const&)
{
++t.threw;
}
}
return t;
}
/**
* Runs every operation and dataset for one subject type and rounding mode.
*/
template <DecomposableNumber T>
void
runSubject(
std::string_view subject,
Grid grid,
Number::RoundingMode mode,
std::size_t samples,
std::vector<Row>& rows)
{
auto const saved = T::setround(mode);
auto const record = [&](std::string op, Dataset d, Tally t) {
rows.push_back({std::string{subject}, mode, std::move(op), std::string{toString(d)}, t});
};
auto const recordPairs = [&](std::string op, std::string data, Tally t) {
rows.push_back({std::string{subject}, mode, std::move(op), std::move(data), t});
};
auto const plus = [](auto const& x, auto const& y) { return x + y; };
auto const minus = [](auto const& x, auto const& y) { return x - y; };
auto const times = [](auto const& x, auto const& y) { return x * y; };
auto const divide = [](auto const& x, auto const& y) { return x / y; };
for (auto const d : {Dataset::Full, Dataset::Iou, Dataset::Mpt})
{
auto const lhs = makeRaw(d, kSeedLhs, samples);
auto const rhs = makeRaw(d, kSeedRhs, samples);
record("add", d, runBinary<T>(lhs, rhs, plus, grid, mode));
record("sub", d, runBinary<T>(lhs, rhs, minus, grid, mode));
record("mul", d, runBinary<T>(lhs, rhs, times, grid, mode));
record("div", d, runBinary<T>(lhs, rhs, divide, grid, mode));
record("root2", d, runUnary<T>(lhs, [](auto const& x) { return root2(x); }, grid, mode));
}
for (int const gap : {1, 5, 18})
{
auto const [lhs, rhs] = makeExponentGapPairs(gap, kSeedLhs, samples);
recordPairs("add", std::format("gap{}", gap), runBinary<T>(lhs, rhs, plus, grid, mode));
recordPairs("sub", std::format("gap{}", gap), runBinary<T>(lhs, rhs, minus, grid, mode));
}
{
auto const [lhs, rhs] = makeCancellationPairs(kSeedLhs, samples);
recordPairs("sub", "cancel", runBinary<T>(lhs, rhs, minus, grid, mode));
}
{
auto const [lhs, rhs] = makeTiePairs(kSeedLhs, samples);
recordPairs("add", "tie", runBinary<T>(lhs, rhs, plus, grid, mode));
}
{
auto const [lhs, rhs] = makeCuspPairs(kSeedLhs, samples);
recordPairs("add", "cusp", runBinary<T>(lhs, rhs, plus, grid, mode));
}
auto const rates = makeRaw(Dataset::Rate, kSeedLhs, samples);
for (unsigned const n : {12u, 360u})
{
record(
std::format("power{}", n),
Dataset::Rate,
runUnary<T>(rates, [n](auto const& x) { return power(x, n); }, grid, mode));
}
for (auto const d : {Dataset::Fraction, Dataset::Half})
record("to_int64", d, runToInt64<T>(makeRaw(d, kSeedLhs, samples), mode));
T::setround(saved);
}
void
print(std::vector<Row> const& rows, bool csv)
{
if (csv)
std::cout << "subject,mode,op,data,samples,correct,wrong_neighbor,worse,flushed,threw,max_"
"error\n";
else
std::cout << "| subject | mode | op | data | samples | correct | wrong neighbor | worse | "
"flushed | threw | max error |\n"
"|---|---|---|---|---:|---:|---:|---:|---:|---:|---:|\n";
for (auto const& r : rows)
{
auto const& t = r.tally;
auto const mode = to_string(r.mode);
auto const fmt = csv ? "{},{},{},{},{},{},{},{},{},{},{:.4g}\n"
: "| {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {:.4g} |\n";
std::cout << std::vformat(
fmt,
std::make_format_args(
r.subject,
mode,
r.op,
r.data,
t.samples,
t.correct,
t.wrongNeighbor,
t.worse,
t.flushed,
t.threw,
t.maxError));
}
}
int
run(int argc, char** argv)
{
std::size_t samples = 10'000;
bool csv = false;
for (int i = 1; i < argc; ++i)
{
std::string_view const arg = argv[i];
if (arg == "--samples" && i + 1 < argc)
samples = std::strtoull(argv[++i], nullptr, 10);
else if (arg == "--csv")
csv = true;
else
{
std::cerr << "usage: " << argv[0] << " [--samples N] [--csv]\n";
return 2;
}
}
using enum Number::RoundingMode;
using enum MantissaRange::MantissaScale;
constexpr auto kModes = std::to_array({ToNearest, TowardsZero, Downward, Upward});
std::vector<Row> rows;
for (auto const mode : kModes)
{
for (auto const scale : {Small, LargeLegacy, Large320, Large330})
{
NumberMantissaScaleGuard const guard{scale};
auto const grid =
scale == Small ? Grid{.digits = 16} : Grid{.digits = 19, .int64Cap = true};
runSubject<Number>("Number." + to_string(scale), grid, mode, samples, rows);
}
// Validate the oracle and the grid logic against IEEE implementations,
// which must be correctly rounded for + - * / and sqrt.
runSubject<BoostDecimal64>("BoostDecimal64", Grid{.digits = 16}, mode, samples, rows);
runSubject<BoostDecimalFast64>(
"BoostDecimalFast64", Grid{.digits = 16}, mode, samples, rows);
runSubject<BoostDecimal128>("BoostDecimal128", Grid{.digits = 34}, mode, samples, rows);
runSubject<BoostDecimalFast128>(
"BoostDecimalFast128", Grid{.digits = 34}, mode, samples, rows);
#ifdef XRPL_BENCH_INTEL_DFP
runSubject<IntelBid64>("IntelBid64", Grid{.digits = 16}, mode, samples, rows);
runSubject<IntelBid128>("IntelBid128", Grid{.digits = 34}, mode, samples, rows);
#endif
#ifdef XRPL_BENCH_HAS_NUMBER128
runSubject<Number128>("Number128", Grid{.digits = Number128::kDigits}, mode, samples, rows);
#endif
}
print(rows, csv);
return 0;
}
} // namespace
} // namespace xrpl::number_bench
int
main(int argc, char** argv)
{
return xrpl::number_bench::run(argc, argv);
}
+2 -4
View File
@@ -5,7 +5,6 @@
#include <cerrno>
#include <cstddef>
#include <filesystem>
#include <format>
#include <fstream>
#include <iomanip>
#include <ios>
@@ -100,13 +99,12 @@ uniqueRandomPath(
if (ec)
{
Throw<std::runtime_error>(
std::format("Unable to check path '{}': {}", candidate.string(), ec.message()));
"Unable to check path '" + candidate.string() + "': " + ec.message());
}
if (!exists)
return candidate;
}
Throw<std::runtime_error>(
std::format("Unable to generate a unique path under '{}'", base.string()));
Throw<std::runtime_error>("Unable to generate a unique path under '" + base.string() + "'");
}
TempDir::TempDir() : path_(uniqueRandomPath(std::filesystem::temp_directory_path()))
+1 -2
View File
@@ -16,7 +16,6 @@
#include <boost/container/pmr/monotonic_buffer_resource.hpp>
#include <cstddef>
#include <format>
#include <memory>
#include <optional>
#include <stdexcept>
@@ -265,7 +264,7 @@ OpenView::rawTxInsert(
auto const result = txs_.emplace(
std::piecewise_construct, std::forward_as_tuple(key), std::forward_as_tuple(txn, metaData));
if (!result.second)
Throw<std::logic_error>(std::format("rawTxInsert: duplicate TX id: {}", to_string(key)));
Throw<std::logic_error>("rawTxInsert: duplicate TX id: " + to_string(key));
}
} // namespace xrpl
+9 -18
View File
@@ -9,7 +9,6 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/AccountRootEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
@@ -116,8 +115,8 @@ isVaultPseudoAccountFrozenForIssuance(
view, {issuer, account}, assetOfHolding(issuanceSle, *sleHolding), depth + 1);
}
auto const mptIssuer = AccountRootEntryR(issuer, view);
if (!mptIssuer.exists())
auto const mptIssuer = view.read(keylet::account(issuer));
if (mptIssuer == nullptr)
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::isVaultPseudoAccountFrozen : null MPToken issuer");
@@ -190,7 +189,7 @@ canTransferLPToken(
{
// Only AMM-issued LPTokens are subject to this check. The LPToken's issuer
// is the AMM account; if it is not an AMM, this is not an LPToken.
auto const sleIssuer = AccountRootEntryR(lpTokenIssuer, view);
auto const sleIssuer = view.read(keylet::account(lpTokenIssuer));
if (!sleIssuer || !sleIssuer->isFieldPresent(sfAMMID))
return tesSUCCESS;
@@ -467,7 +466,7 @@ canWithdraw(
ReadView const& view,
AccountID const& from,
AccountID const& to,
AccountRootEntryR const& toSle,
SLE::ConstRef toSle,
STAmount const& amount,
bool hasDestinationTag,
std::optional<std::vector<UInt256>> const& credentialIDs)
@@ -517,7 +516,7 @@ canWithdraw(
bool hasDestinationTag,
std::optional<std::vector<UInt256>> const& credentialIDs)
{
auto const toSle = AccountRootEntryR(to, view);
auto const toSle = view.read(keylet::account(to));
return canWithdraw(view, from, to, toSle, amount, hasDestinationTag, credentialIDs);
}
@@ -542,7 +541,7 @@ doWithdraw(
STAmount const& amount,
beast::Journal j)
{
auto dstSle = AccountRootEntryW(dstAcct, ctx.view);
auto const dstSle = ctx.view.read(keylet::account(dstAcct));
// Create a trust line or MPToken for a self-destination only when there
// is a payout to credit. Post-fixCleanup3_4_0, a zero-value withdraw
@@ -560,8 +559,7 @@ doWithdraw(
}
else
{
if (auto err =
verifyDepositPreauth(ctx.tx, ctx.view, senderAcct, dstAcct, dstSle.rawSle(), j))
if (auto err = verifyDepositPreauth(ctx.tx, ctx.view, senderAcct, dstAcct, dstSle, j))
return err;
}
@@ -584,21 +582,14 @@ doWithdraw(
// sponsor against the destination. accountSend can auto-create a holding
// for dstAcct; keying on the destination ensures a third-party destination's
// holding is never stamped with the tx's reserve sponsor.
auto sponsorSle = getEffectiveTxReserveSponsor(ctx, dstSle);
auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, dstSle);
if (!sponsorSle)
return sponsorSle.error(); // LCOV_EXCL_LINE
// Move the funds directly from the broker's pseudo-account to the
// dstAcct
auto& sponsor = *sponsorSle;
return accountSend(
ctx.view,
sourceAcct,
dstAcct,
amount,
j,
sponsor ? sponsor->mutableRawSle() : SLE::pointer{},
WaiveTransferFee::Yes);
ctx.view, sourceAcct, dstAcct, amount, j, *sponsorSle, WaiveTransferFee::Yes);
}
TER
@@ -1,322 +0,0 @@
#include <xrpl/ledger/entries/AccountRootEntry.h>
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/OwnerCounts.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <cstdint>
#include <limits>
#include <optional>
#include <utility>
namespace xrpl {
template <typename ViewT>
XRPAmount
AccountRootEntry<ViewT>::reserve(Adjustment adj) const
{
auto const& view = this->readView();
auto const j = this->journal();
XRPL_ASSERT(
this->exists() && (*this)->getType() == ltACCOUNT_ROOT, "xrpl::accountReserve : valid sle");
if (!view.rules().enabled(featureSponsor))
{
XRPL_ASSERT(adj.accountCountDelta == 0, "xrpl::accountReserve : no account count delta");
return view.fees().accountReserve(
(*this)->getFieldU32(sfOwnerCount) + adj.ownerCountDelta, 1);
}
std::uint32_t const currentOwnerCount = ownerCount(adj.ownerCountDelta);
std::uint32_t const currentAccountCount =
detail::accountCountImpl(*this, adj.accountCountDelta, j);
return view.fees().accountReserve(currentOwnerCount, currentAccountCount);
}
template <typename ViewT>
std::uint32_t
AccountRootEntry<ViewT>::ownerCount(std::int32_t ownerCountAdj) const
{
auto const j = this->journal();
XRPL_ASSERT(
this->exists() && (*this)->getType() == ltACCOUNT_ROOT,
"xrpl::ownerCount : sle is account root");
AccountID const id = (*this)->getAccountID(sfAccount);
std::uint32_t const currentOwnerCount = (*this)->at(sfOwnerCount);
std::uint32_t const sponsoredOwnerCount = (*this)->at(sfSponsoredOwnerCount);
std::uint32_t const sponsoringOwnerCount = (*this)->at(sfSponsoringOwnerCount);
XRPL_ASSERT(
currentOwnerCount >= sponsoredOwnerCount,
"xrpl::ownerCount : OwnerCount must be greater than or equal to SponsoredOwnerCount");
std::int64_t deltaCount =
static_cast<std::int64_t>(ownerCountAdj) - sponsoredOwnerCount + sponsoringOwnerCount;
if (deltaCount > std::numeric_limits<std::int32_t>::max())
{
// LCOV_EXCL_START
deltaCount = std::numeric_limits<std::int32_t>::max();
JLOG(j.fatal()) << "Account " << id << " delta count exceeds max, "
<< "adjustment: " << ownerCountAdj
<< ", sponsoredCount: " << sponsoredOwnerCount
<< ", sponsoringOwnerCount: " << sponsoringOwnerCount;
// LCOV_EXCL_STOP
}
else if (deltaCount < std::numeric_limits<std::int32_t>::min())
{
// LCOV_EXCL_START
deltaCount = std::numeric_limits<std::int32_t>::min();
JLOG(j.fatal()) << "Account " << id << " delta count is below min, "
<< "adjustment: " << ownerCountAdj
<< ", sponsoredCount: " << sponsoredOwnerCount
<< ", sponsoringCount: " << sponsoringOwnerCount;
// LCOV_EXCL_STOP
}
return detail::confineOwnerCount(currentOwnerCount, deltaCount);
}
template <typename ViewT>
TER
AccountRootEntry<ViewT>::checkReserve(
ApplyViewContext ctx,
XRPAmount accBalance,
std::optional<AccountRootEntry<ReadView>> const& sponsorSle,
Adjustment adj,
TER insufReserveCode) const
{
// TODO: swap to assert after fixCleanup3_2_0 is retired
if (!this->exists() || (*this)->getType() != ltACCOUNT_ROOT)
return tefINTERNAL; // LCOV_EXCL_LINE
XRPL_ASSERT(
!isTesSuccess(insufReserveCode), "xrpl::checkReserve : insufReserveCode is not tesSUCCESS");
if (ctx.view.rules().enabled(featureSponsor))
{
if (sponsorSle)
{
if ((*sponsorSle)->getType() != ltACCOUNT_ROOT)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const sle = ctx.view.read(
keylet::sponsorship(
(*sponsorSle)->getAccountID(sfAccount), (*this)->getAccountID(sfAccount)));
// A reserve-sponsored tx must carry a sponsor signature
// (cosigning path) and/or have a pre-existing sponsorship SLE
// (prefunded path). Absence of both is an internal invariant break.
if (isReserveSponsored(ctx.tx) && !sle && !ctx.tx.isFieldPresent(sfSponsorSignature))
return tecINTERNAL; // LCOV_EXCL_LINE
if (sle)
{
auto const ownerCountAllowed = sle->getFieldU32(sfRemainingOwnerCount);
if (adj.ownerCountDelta > 0 &&
ownerCountAllowed < static_cast<std::uint32_t>(adj.ownerCountDelta))
return insufReserveCode;
}
auto const sponsorBalance = (*sponsorSle)->getFieldAmount(sfBalance).xrp();
XRPAmount const sponsorReserve = sponsorSle->reserve(adj);
if (sponsorBalance < sponsorReserve)
return insufReserveCode;
}
else
{
XRPAmount const reserve = this->reserve(adj);
if (accBalance < reserve)
return insufReserveCode;
}
}
else
{
XRPL_ASSERT(
!sponsorSle,
"xrpl::checkReserve : featureSponsor disabled and sponsorSle not provided");
XRPL_ASSERT(adj.accountCountDelta == 0, "xrpl::checkReserve : accountCountDelta is 0");
auto const reserve = ctx.view.fees().accountReserve(
(*this)->getFieldU32(sfOwnerCount) + adj.ownerCountDelta, 1);
if (accBalance < reserve)
return insufReserveCode;
}
return tesSUCCESS;
}
template <typename ViewT>
TER
AccountRootEntry<ViewT>::checkReserve(ApplyViewContext ctx, XRPAmount accBalance, Adjustment adj)
const
{
auto const sponsorExp = getEffectiveTxReserveSponsor(ctx, *this);
if (!sponsorExp)
return sponsorExp.error(); // LCOV_EXCL_LINE
std::optional<AccountRootEntry<ReadView>> sponsorSle;
if (*sponsorExp)
sponsorSle.emplace(**sponsorExp);
return checkReserve(ctx, accBalance, sponsorSle, adj);
}
template <typename ViewT>
void
AccountRootEntry<ViewT>::adjustOwnerCountSigned(
std::optional<AccountRootEntry<ApplyView>>& sponsorSle,
std::int32_t adjustment)
requires Base::kIsWritable
{
auto& view = this->applyView();
auto const j = this->journal();
if (view.rules().enabled(featureSponsor))
{
XRPL_ASSERT(this->exists(), "xrpl::adjustOwnerCountSigned : valid account sle");
if (!this->exists())
return; // LCOV_EXCL_LINE
auto const accountID = (*this)->getAccountID(sfAccount);
bool const validType = (*this)->getType() == ltACCOUNT_ROOT;
XRPL_ASSERT(validType, "xrpl::adjustOwnerCountSigned : valid account sle type");
if (!validType)
return; // LCOV_EXCL_LINE
XRPL_ASSERT(adjustment, "xrpl::adjustOwnerCountSigned : nonzero adjustment input");
OwnerCounts const currentOwnerCount(this->rawSle());
OwnerCounts totalOwnerCount(currentOwnerCount);
if (sponsorSle)
{
bool const validSponsorType = (*sponsorSle)->getType() == ltACCOUNT_ROOT;
XRPL_ASSERT(validSponsorType, "xrpl::adjustOwnerCountSigned : valid sponsor sle type");
if (!validSponsorType)
return; // LCOV_EXCL_LINE
auto const sponsorID = (*sponsorSle)->getAccountID(sfAccount);
totalOwnerCount.sponsored = detail::adjustOwnerCountImpl(
view, this->mutableRawSle(), sfSponsoredOwnerCount, accountID, adjustment, j);
{
OwnerCounts const sponsorCurrent(sponsorSle->rawSle());
OwnerCounts sponsorAdjustment(sponsorCurrent);
sponsorAdjustment.sponsoring = detail::adjustOwnerCountImpl(
view,
sponsorSle->mutableRawSle(),
sfSponsoringOwnerCount,
sponsorID,
adjustment,
j);
view.adjustOwnerCountHook(sponsorID, sponsorCurrent, sponsorAdjustment);
}
auto sponsorshipSle = view.peek(keylet::sponsorship(sponsorID, accountID));
if (sponsorshipSle && adjustment > 0)
{
// Only decrease the pre-funded ReserveCount on Sponsorship if we assign new
// objects. Removing/reassigning ownership of the object doesn't increase
// RemainingOwnerCount back. Don't call hook because this counter is not something
// that requires reserve (like other sf...OwnerCounts do).
detail::adjustOwnerCountImpl(
view, sponsorshipSle, sfRemainingOwnerCount, sponsorID, -adjustment, j);
}
}
totalOwnerCount.owner = detail::adjustOwnerCountImpl(
view, this->mutableRawSle(), sfOwnerCount, accountID, adjustment, j);
view.adjustOwnerCountHook(accountID, currentOwnerCount, totalOwnerCount);
}
else
{
XRPL_ASSERT(this->exists(), "xrpl::adjustOwnerCountSigned : valid account sle");
if (!this->exists())
return;
// the remaining are only asserts to preserve existing behavior
XRPL_ASSERT(!sponsorSle, "xrpl::adjustOwnerCountSigned : sponsor not enabled");
XRPL_ASSERT(
(*this)->getType() == ltACCOUNT_ROOT,
"xrpl::adjustOwnerCountSigned : valid account sle type");
XRPL_ASSERT(adjustment, "xrpl::adjustOwnerCount : nonzero adjustment input");
std::uint32_t const current{(*this)->getFieldU32(sfOwnerCount)};
AccountID const id = (**this)[sfAccount];
std::uint32_t const adjusted = detail::confineOwnerCount(current, adjustment, id, j);
OwnerCounts const currentOwnerCount(this->rawSle());
OwnerCounts finalOwnerCount(currentOwnerCount);
finalOwnerCount.owner = adjusted;
view.adjustOwnerCountHook(id, currentOwnerCount, finalOwnerCount);
(*this)->at(sfOwnerCount) = adjusted;
this->update();
}
}
template <typename ViewT>
void
AccountRootEntry<ViewT>::increaseOwnerCount(
std::optional<AccountRootEntry<ApplyView>>& sponsorSle,
std::uint32_t count)
requires Base::kIsWritable
{
XRPL_ASSERT(
count != 0 && count <= std::numeric_limits<std::int32_t>::max(),
"xrpl::increaseOwnerCount : count in signed delta range");
if (count == 0 || count > std::numeric_limits<std::int32_t>::max())
return; // LCOV_EXCL_LINE
adjustOwnerCountSigned(sponsorSle, static_cast<std::int32_t>(count));
}
template <typename ViewT>
void
AccountRootEntry<ViewT>::increaseOwnerCount(ApplyViewContext ctx, std::uint32_t count)
requires Base::kIsWritable
{
auto sponsorExp = getEffectiveTxReserveSponsor(ctx, *this);
// The sponsor's existence is validated by checkReserve/checkSponsor before
// any owner-count mutation, so loading it here cannot fail.
XRPL_ASSERT(
sponsorExp.has_value(), "xrpl::increaseOwnerCount : sponsor validated before mutation");
std::optional<AccountRootEntry<ApplyView>> sponsorSle;
if (sponsorExp && *sponsorExp)
sponsorSle.emplace(std::move(**sponsorExp));
increaseOwnerCount(sponsorSle, count);
}
template <typename ViewT>
void
AccountRootEntry<ViewT>::decreaseOwnerCount(
std::optional<AccountRootEntry<ApplyView>>& sponsorSle,
std::uint32_t count)
requires Base::kIsWritable
{
XRPL_ASSERT(
count != 0 && count <= std::numeric_limits<std::int32_t>::max(),
"xrpl::decreaseOwnerCount : count in signed delta range");
if (count == 0 || count > std::numeric_limits<std::int32_t>::max())
return; // LCOV_EXCL_LINE
adjustOwnerCountSigned(sponsorSle, -static_cast<std::int32_t>(count));
}
template class AccountRootEntry<ReadView>;
template class AccountRootEntry<ApplyView>;
} // namespace xrpl
+3 -4
View File
@@ -11,7 +11,6 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/AccountRootEntry.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
@@ -642,7 +641,7 @@ ammAccountHolds(ReadView const& view, AccountID const& ammAccountID, Asset const
[&](Issue const& issue) {
if (isXRP(issue))
{
if (auto const sle = AccountRootEntryR(ammAccountID, view))
if (auto const sle = view.read(keylet::account(ammAccountID)))
return (*sle)[sfBalance];
}
else if (
@@ -758,7 +757,7 @@ deleteAMMAccount(Sandbox& sb, Asset const& asset, Asset const& asset2, beast::Jo
}
auto const ammAccountID = (*ammSle)[sfAccount];
auto sleAMMRoot = AccountRootEntryW(ammAccountID, sb);
auto sleAMMRoot = sb.peek(keylet::account(ammAccountID));
if (!sleAMMRoot)
{
// LCOV_EXCL_START
@@ -796,7 +795,7 @@ deleteAMMAccount(Sandbox& sb, Asset const& asset, Asset const& asset2, beast::Jo
}
sb.erase(ammSle);
sleAMMRoot.erase();
sb.erase(sleAMMRoot);
return tesSUCCESS;
}
+300 -22
View File
@@ -9,7 +9,6 @@
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/OwnerCounts.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/AccountRootEntry.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
@@ -18,6 +17,7 @@
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/digest.h>
@@ -38,19 +38,25 @@ isGlobalFrozen(ReadView const& view, AccountID const& issuer)
{
if (isXRP(issuer))
return false;
if (auto const sle = AccountRootEntryR(issuer, view))
if (auto const sle = view.read(keylet::account(issuer)))
return sle->isFlag(lsfGlobalFreeze);
return false;
}
namespace detail {
namespace {
// An owner count cannot be negative. If adjustment would cause a negative
// owner count, clamp the owner count at 0. Similarly for overflow. This
// adjustment allows the ownerCount to be adjusted up or down in multiple steps.
// If id != std::nullopt, then do error reporting.
//
// Returns adjusted owner count.
std::uint32_t
confineOwnerCount(
std::uint32_t currentOwnerCount,
std::int32_t ownerCountAdj,
std::optional<AccountID> const& id,
beast::Journal j)
std::optional<AccountID> const& id = std::nullopt,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
{
std::uint32_t totalOwnerCount{currentOwnerCount + ownerCountAdj};
if (ownerCountAdj > 0)
@@ -85,8 +91,10 @@ confineOwnerCount(
return totalOwnerCount;
}
// Returns the number of account reserves funded by this account: 1 for itself (0 if sponsored by
// another account) plus the count of accounts it sponsors.
std::uint32_t
accountCountImpl(AccountRootEntryR const& sle, std::int32_t accountCountAdj, beast::Journal j)
accountCountImpl(SLE::ConstRef sle, std::int32_t accountCountAdj, beast::Journal j)
{
bool const isSponsored = sle->isFieldPresent(sfSponsor);
std::int64_t const sponsoringAccountCount = sle->getFieldU32(sfSponsoringAccountCount);
@@ -123,25 +131,151 @@ adjustOwnerCountImpl(
{
std::uint32_t const currentOwnerCount = sle->at(sfield);
std::uint32_t const totalOwnerCount =
detail::confineOwnerCount(currentOwnerCount, ownerCountAdj, accID, j);
confineOwnerCount(currentOwnerCount, ownerCountAdj, accID, j);
sle->at(sfield) = totalOwnerCount;
view.update(sle);
return totalOwnerCount;
}
} // namespace detail
void
adjustOwnerCountSigned(
ApplyView& view,
SLE::Ref accountSle,
SLE::Ref sponsorSle,
std::int32_t adjustment,
beast::Journal j)
{
if (view.rules().enabled(featureSponsor))
{
XRPL_ASSERT(accountSle, "xrpl::adjustOwnerCountSigned : valid account sle");
if (!accountSle)
return; // LCOV_EXCL_LINE
auto const accountID = accountSle->getAccountID(sfAccount);
bool const validType = accountSle->getType() == ltACCOUNT_ROOT;
XRPL_ASSERT(validType, "xrpl::adjustOwnerCountSigned : valid account sle type");
if (!validType)
return; // LCOV_EXCL_LINE
XRPL_ASSERT(adjustment, "xrpl::adjustOwnerCountSigned : nonzero adjustment input");
OwnerCounts const currentOwnerCount(accountSle);
OwnerCounts totalOwnerCount(currentOwnerCount);
if (sponsorSle)
{
bool const validSponsorType = sponsorSle->getType() == ltACCOUNT_ROOT;
XRPL_ASSERT(validSponsorType, "xrpl::adjustOwnerCountSigned : valid sponsor sle type");
if (!validSponsorType)
return; // LCOV_EXCL_LINE
auto const sponsorID = sponsorSle->getAccountID(sfAccount);
totalOwnerCount.sponsored = adjustOwnerCountImpl(
view, accountSle, sfSponsoredOwnerCount, accountID, adjustment, j);
{
OwnerCounts const sponsorCurrent(sponsorSle);
OwnerCounts sponsorAdjustment(sponsorCurrent);
sponsorAdjustment.sponsoring = adjustOwnerCountImpl(
view, sponsorSle, sfSponsoringOwnerCount, sponsorID, adjustment, j);
view.adjustOwnerCountHook(sponsorID, sponsorCurrent, sponsorAdjustment);
}
auto sponsorshipSle = view.peek(keylet::sponsorship(sponsorID, accountID));
if (sponsorshipSle && adjustment > 0)
{
// Only decrease the pre-funded ReserveCount on Sponsorship if we assign new
// objects. Removing/reassigning ownership of the object doesn't increase
// RemainingOwnerCount back. Don't call hook because this counter is not something
// that requires reserve (like other sf...OwnerCounts do).
adjustOwnerCountImpl(
view, sponsorshipSle, sfRemainingOwnerCount, sponsorID, -adjustment, j);
}
}
totalOwnerCount.owner =
adjustOwnerCountImpl(view, accountSle, sfOwnerCount, accountID, adjustment, j);
view.adjustOwnerCountHook(accountID, currentOwnerCount, totalOwnerCount);
}
else
{
XRPL_ASSERT(accountSle, "xrpl::adjustOwnerCountSigned : valid account sle");
if (!accountSle)
return;
// the remaining are only asserts to preserve existing behavior
XRPL_ASSERT(sponsorSle == nullptr, "xrpl::adjustOwnerCountSigned : sponsor not enabled");
XRPL_ASSERT(
accountSle->getType() == ltACCOUNT_ROOT,
"xrpl::adjustOwnerCountSigned : valid account sle type");
XRPL_ASSERT(adjustment, "xrpl::adjustOwnerCount : nonzero adjustment input");
std::uint32_t const current{accountSle->getFieldU32(sfOwnerCount)};
AccountID const id = (*accountSle)[sfAccount];
std::uint32_t const adjusted = confineOwnerCount(current, adjustment, id, j);
OwnerCounts const currentOwnerCount(accountSle);
OwnerCounts finalOwnerCount(currentOwnerCount);
finalOwnerCount.owner = adjusted;
view.adjustOwnerCountHook(id, currentOwnerCount, finalOwnerCount);
accountSle->at(sfOwnerCount) = adjusted;
view.update(accountSle);
}
}
} // namespace
std::uint32_t
ownerCount(SLE::ConstRef sle, beast::Journal j, std::int32_t ownerCountAdj)
{
XRPL_ASSERT(sle && sle->getType() == ltACCOUNT_ROOT, "xrpl::ownerCount : sle is account root");
AccountID const id = sle->getAccountID(sfAccount);
std::uint32_t const currentOwnerCount = sle->at(sfOwnerCount);
std::uint32_t const sponsoredOwnerCount = sle->at(sfSponsoredOwnerCount);
std::uint32_t const sponsoringOwnerCount = sle->at(sfSponsoringOwnerCount);
XRPL_ASSERT(
currentOwnerCount >= sponsoredOwnerCount,
"xrpl::ownerCount : OwnerCount must be greater than or equal to SponsoredOwnerCount");
std::int64_t deltaCount =
static_cast<std::int64_t>(ownerCountAdj) - sponsoredOwnerCount + sponsoringOwnerCount;
if (deltaCount > std::numeric_limits<std::int32_t>::max())
{
// LCOV_EXCL_START
deltaCount = std::numeric_limits<std::int32_t>::max();
JLOG(j.fatal()) << "Account " << id << " delta count exceeds max, "
<< "adjustment: " << ownerCountAdj
<< ", sponsoredCount: " << sponsoredOwnerCount
<< ", sponsoringOwnerCount: " << sponsoringOwnerCount;
// LCOV_EXCL_STOP
}
else if (deltaCount < std::numeric_limits<std::int32_t>::min())
{
// LCOV_EXCL_START
deltaCount = std::numeric_limits<std::int32_t>::min();
JLOG(j.fatal()) << "Account " << id << " delta count is below min, "
<< "adjustment: " << ownerCountAdj
<< ", sponsoredCount: " << sponsoredOwnerCount
<< ", sponsoringCount: " << sponsoringOwnerCount;
// LCOV_EXCL_STOP
}
return confineOwnerCount(currentOwnerCount, deltaCount);
}
XRPAmount
xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, beast::Journal j)
{
auto const sle = AccountRootEntryR(id, view);
if (!sle.exists())
auto const sle = view.read(keylet::account(id));
if (sle == nullptr)
return beast::kZero;
// Return balance minus reserve
std::uint32_t const currentOwnerCount = detail::confineOwnerCount(
view.ownerCountHook(id, OwnerCounts(sle.rawSle())).count(), ownerCountAdj);
std::uint32_t const currentAccountCount = detail::accountCountImpl(sle, 0, j);
std::uint32_t const currentOwnerCount =
confineOwnerCount(view.ownerCountHook(id, OwnerCounts(sle)).count(), ownerCountAdj);
std::uint32_t const currentAccountCount = accountCountImpl(sle, 0, j);
// Pseudo-accounts have no reserve requirement
auto const reserve = isPseudoAccount(sle)
@@ -166,7 +300,7 @@ xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj,
Rate
transferRate(ReadView const& view, AccountID const& issuer)
{
auto const sle = AccountRootEntryR(issuer, view);
auto const sle = view.read(keylet::account(issuer));
if (sle && sle->isFieldPresent(sfTransferRate))
return Rate{sle->getFieldU32(sfTransferRate)};
@@ -174,10 +308,57 @@ transferRate(ReadView const& view, AccountID const& issuer)
return kParityRate;
}
void
increaseOwnerCount(
ApplyView& view,
SLE::Ref accountSle,
SLE::Ref sponsorSle,
std::uint32_t count,
beast::Journal j)
{
XRPL_ASSERT(
count != 0 && count <= std::numeric_limits<std::int32_t>::max(),
"xrpl::increaseOwnerCount : count in signed delta range");
if (count == 0 || count > std::numeric_limits<std::int32_t>::max())
return; // LCOV_EXCL_LINE
adjustOwnerCountSigned(view, accountSle, sponsorSle, static_cast<std::int32_t>(count), j);
}
void
increaseOwnerCount(ApplyViewContext ctx, SLE::Ref accountSle, std::uint32_t count, beast::Journal j)
{
auto const sponsorExp = getEffectiveTxReserveSponsor(ctx, accountSle);
// The sponsor's existence is validated by checkReserve/checkSponsor before
// any owner-count mutation, so loading it here cannot fail.
XRPL_ASSERT(
sponsorExp.has_value(), "xrpl::increaseOwnerCount : sponsor validated before mutation");
increaseOwnerCount(ctx.view, accountSle, sponsorExp ? *sponsorExp : SLE::pointer(), count, j);
}
void
decreaseOwnerCount(
ApplyView& view,
SLE::Ref accountSle,
SLE::Ref sponsorSle,
std::uint32_t count,
beast::Journal j)
{
XRPL_ASSERT(
count != 0 && count <= std::numeric_limits<std::int32_t>::max(),
"xrpl::decreaseOwnerCount : count in signed delta range");
if (count == 0 || count > std::numeric_limits<std::int32_t>::max())
return; // LCOV_EXCL_LINE
adjustOwnerCountSigned(view, accountSle, sponsorSle, -static_cast<std::int32_t>(count), j);
}
void
decreaseOwnerCountForObject(
ApplyView& view,
AccountRootEntryW& accountSle,
SLE::Ref accountSle,
SLE::Ref objectSle,
std::uint32_t count,
beast::Journal j)
@@ -191,8 +372,8 @@ decreaseOwnerCountForObject(
if (!validObjectType)
return; // LCOV_EXCL_LINE
auto sponsorSle = getLedgerEntryReserveSponsor(view, objectSle);
accountSle.decreaseOwnerCount(sponsorSle, count);
SLE::Ref sponsorSle = getLedgerEntryReserveSponsor(view, objectSle);
decreaseOwnerCount(view, accountSle, sponsorSle, count, j);
}
void
@@ -212,10 +393,107 @@ adjustLoanBrokerOwnerCount(
if (delta == 0)
return; // LCOV_EXCL_LINE
detail::adjustOwnerCountImpl(
adjustOwnerCountImpl(
view, brokerSle, sfOwnerCount, brokerSle->getAccountID(sfAccount), delta, j);
}
XRPAmount
accountReserve(ReadView const& view, SLE::ConstRef sle, beast::Journal j, Adjustment adj)
{
XRPL_ASSERT(sle && sle->getType() == ltACCOUNT_ROOT, "xrpl::accountReserve : valid sle");
if (!view.rules().enabled(featureSponsor))
{
XRPL_ASSERT(adj.accountCountDelta == 0, "xrpl::accountReserve : no account count delta");
return view.fees().accountReserve(sle->getFieldU32(sfOwnerCount) + adj.ownerCountDelta, 1);
}
std::uint32_t const currentOwnerCount = ownerCount(sle, j, adj.ownerCountDelta);
std::uint32_t const currentAccountCount = accountCountImpl(sle, adj.accountCountDelta, j);
return view.fees().accountReserve(currentOwnerCount, currentAccountCount);
}
TER
checkReserve(
ApplyViewContext ctx,
SLE::ConstRef accSle,
XRPAmount accBalance,
SLE::ConstRef sponsorSle,
Adjustment adj,
beast::Journal j,
TER insufReserveCode)
{
// TODO: swap to assert after fixCleanup3_2_0 is retired
if (!accSle || accSle->getType() != ltACCOUNT_ROOT)
return tefINTERNAL; // LCOV_EXCL_LINE
XRPL_ASSERT(
!isTesSuccess(insufReserveCode), "xrpl::checkReserve : insufReserveCode is not tesSUCCESS");
if (ctx.view.rules().enabled(featureSponsor))
{
if (sponsorSle)
{
if (sponsorSle->getType() != ltACCOUNT_ROOT)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const sle = ctx.view.read(
keylet::sponsorship(
sponsorSle->getAccountID(sfAccount), accSle->getAccountID(sfAccount)));
// A reserve-sponsored tx must carry a sponsor signature
// (cosigning path) and/or have a pre-existing sponsorship SLE
// (prefunded path). Absence of both is an internal invariant break.
if (isReserveSponsored(ctx.tx) && !sle && !ctx.tx.isFieldPresent(sfSponsorSignature))
return tecINTERNAL; // LCOV_EXCL_LINE
if (sle)
{
auto const ownerCountAllowed = sle->getFieldU32(sfRemainingOwnerCount);
if (adj.ownerCountDelta > 0 &&
ownerCountAllowed < static_cast<std::uint32_t>(adj.ownerCountDelta))
return insufReserveCode;
}
auto const sponsorBalance = sponsorSle->getFieldAmount(sfBalance).xrp();
XRPAmount const sponsorReserve = accountReserve(ctx.view, sponsorSle, j, adj);
if (sponsorBalance < sponsorReserve)
return insufReserveCode;
}
else
{
XRPAmount const reserve = accountReserve(ctx.view, accSle, j, adj);
if (accBalance < reserve)
return insufReserveCode;
}
}
else
{
XRPL_ASSERT(
!sponsorSle,
"xrpl::checkReserve : featureSponsor disabled and sponsorSle not provided");
XRPL_ASSERT(adj.accountCountDelta == 0, "xrpl::checkReserve : accountCountDelta is 0");
auto const reserve = ctx.view.fees().accountReserve(
accSle->getFieldU32(sfOwnerCount) + adj.ownerCountDelta, 1);
if (accBalance < reserve)
return insufReserveCode;
}
return tesSUCCESS;
}
TER
checkReserve(
ApplyViewContext ctx,
SLE::ConstRef accSle,
XRPAmount accBalance,
Adjustment adj,
beast::Journal j)
{
auto const sponsorExp = getEffectiveTxReserveSponsor(ctx, accSle);
if (!sponsorExp)
return sponsorExp.error(); // LCOV_EXCL_LINE
return checkReserve(ctx, accSle, accBalance, *sponsorExp, adj, j);
}
// ----------------------------------------------------
AccountID
@@ -229,7 +507,7 @@ pseudoAccountAddress(ReadView const& view, UInt256 const& pseudoOwnerKey)
auto const hash = sha512Half(i, view.header().parentHash, pseudoOwnerKey);
rsh(hash.data(), hash.size());
AccountID const ret = AccountID::fromRaw(static_cast<RipeshaHasher::result_type>(rsh));
if (!AccountRootEntryR(ret, view).exists())
if (!view.read(keylet::account(ret)))
return ret;
}
return beast::kZero;
@@ -268,7 +546,7 @@ getPseudoAccountFields()
}
[[nodiscard]] bool
isPseudoAccount(AccountRootEntryR const& sleAcct)
isPseudoAccount(SLE::const_pointer sleAcct)
{
// Intentionally use defensive coding here because it's cheap and makes the
// semantics of true return value clean.
@@ -321,9 +599,9 @@ createPseudoAccount(ApplyView& view, UInt256 const& pseudoOwnerKey, SField const
}
[[nodiscard]] TER
checkDestinationAndTag(AccountRootEntryR const& toSle, bool hasDestinationTag)
checkDestinationAndTag(SLE::ConstRef toSle, bool hasDestinationTag)
{
if (!toSle.exists())
if (toSle == nullptr)
return tecNO_DST;
// The tag is basically account-specific information we don't
@@ -7,7 +7,6 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/AccountRootEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
@@ -82,7 +81,7 @@ deleteSLE(ApplyView& view, SLE::Ref sleCredential, beast::Journal j)
auto delSLE = [&view, &sleCredential, j](
AccountID const& account, SField const& node, bool isOwner) -> TER {
auto sleAccount = AccountRootEntryW(account, view);
auto const sleAccount = view.peek(keylet::account(account));
if (!sleAccount)
{
// LCOV_EXCL_START
+11 -15
View File
@@ -7,7 +7,6 @@
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/AccountRootEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
@@ -207,7 +206,7 @@ authorizeMPToken(
std::uint32_t flags,
std::optional<AccountID> holderID)
{
auto sleAcct = AccountRootEntryW(account, ctx.view, journal);
auto const sleAcct = ctx.view.peek(keylet::account(account));
if (!sleAcct)
return tecINTERNAL; // LCOV_EXCL_LINE
@@ -243,10 +242,10 @@ authorizeMPToken(
// - create the MPToken object for the holder
// A reserve sponsor only covers tx.Account's own objects.
auto sponsorExp = getEffectiveTxReserveSponsor(ctx, sleAcct);
auto const sponsorExp = getEffectiveTxReserveSponsor(ctx, sleAcct);
if (!sponsorExp)
return sponsorExp.error(); // LCOV_EXCL_LINE
auto& sponsorSle = *sponsorExp;
auto const sponsorSle = *sponsorExp;
// The reserve that is required to create the MPToken. Note
// that although the reserve increases with every item
@@ -256,10 +255,10 @@ authorizeMPToken(
// The "free-tier" shortcut (ownerCount < 2) does not apply once a sponsor is on
// the tx — the sponsor must always cover the reserve (via balance or prefunded
// budget), so this check always runs for sponsored transactions.
if (sponsorSle || sleAcct.ownerCount() >= 2)
if (sponsorSle || ownerCount(sleAcct, journal) >= 2)
{
if (auto const ret =
sleAcct.checkReserve(ctx, priorBalance, sponsorSle, {.ownerCountDelta = 1});
if (auto const ret = checkReserve(
ctx, sleAcct, priorBalance, sponsorSle, {.ownerCountDelta = 1}, journal);
!isTesSuccess(ret))
return ret;
}
@@ -286,8 +285,8 @@ authorizeMPToken(
ctx.view.insert(mptoken);
// Update owner count.
sleAcct.increaseOwnerCount(sponsorSle, 1);
addSponsorToLedgerEntry(mptoken, sponsorSle ? sponsorSle->rawSle() : SLE::const_pointer{});
increaseOwnerCount(ctx.view, sleAcct, sponsorSle, 1, journal);
addSponsorToLedgerEntry(mptoken, sponsorSle);
return tesSUCCESS;
}
@@ -416,7 +415,7 @@ requireAuth(
}
// requireAuth is recursive if the issuer is a vault pseudo-account
auto const sleIssuer = AccountRootEntryR(mptIssuer, view);
auto const sleIssuer = view.read(keylet::account(mptIssuer));
if (!sleIssuer)
return tefINTERNAL; // LCOV_EXCL_LINE
@@ -1021,16 +1020,13 @@ checkCreateMPT(
{
return err;
}
auto sleAcct = AccountRootEntryW(holder, view, j);
auto const sleAcct = view.peek(keylet::account(holder));
if (!sleAcct)
{
return tecINTERNAL;
}
std::optional<AccountRootEntryW> sponsorAccSle;
if (sponsorSle)
sponsorAccSle.emplace(sponsorSle->getAccountID(sfAccount), view);
sleAcct.increaseOwnerCount(sponsorAccSle, 1);
increaseOwnerCount(view, sleAcct, sponsorSle, 1, j);
}
return tesSUCCESS;
}
+15 -18
View File
@@ -7,7 +7,6 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/AccountRootEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
@@ -34,7 +33,6 @@
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <format>
#include <functional>
#include <iterator>
#include <memory>
@@ -396,11 +394,9 @@ removeToken(ApplyView& view, AccountID const& owner, UInt256 const& nftokenID, S
if (!page2)
{
Throw<std::runtime_error>(std::format(
"page {} has a broken {} field pointing to {}",
to_string(page1->key()),
field.getName(),
to_string(*id)));
Throw<std::runtime_error>(
"page " + to_string(page1->key()) + " has a broken " + field.getName() +
" field pointing to " + to_string(*id));
}
}
@@ -601,7 +597,7 @@ removeTokenOffersWithLimit(ApplyView& view, Keylet const& directory, std::size_t
else
{
Throw<std::runtime_error>(
std::format("Offer {} cannot be deleted!", to_string(offerIndexes[i])));
"Offer " + to_string(offerIndexes[i]) + " cannot be deleted!");
}
}
@@ -738,9 +734,9 @@ repairNFTokenDirectoryLinks(ApplyView& view, AccountID const& owner)
if (!newPrev)
{
// LCOV_EXCL_START
Throw<std::runtime_error>(std::format(
"NFTokenPage directory for {} cannot be repaired. Unexpected link problem.",
to_string(owner)));
Throw<std::runtime_error>(
"NFTokenPage directory for " + to_string(owner) +
" cannot be repaired. Unexpected link problem.");
// LCOV_EXCL_STOP
}
newPrev->at(sfNextPageMin) = nextPage->key();
@@ -856,7 +852,7 @@ tokenOfferCreatePreclaim(
if (nftIssuer != acctID && ((nftFlags & nft::kFlagTransferable) == 0))
{
auto const root = AccountRootEntryR(nftIssuer, view);
auto const root = view.read(keylet::account(nftIssuer));
XRPL_ASSERT(root, "xrpl::nft::tokenOfferCreatePreclaim : non-null account");
if (auto minter = (*root)[~sfNFTokenMinter]; minter != acctID)
@@ -887,7 +883,7 @@ tokenOfferCreatePreclaim(
{
// If a destination is specified, the destination must already be in
// the ledger.
auto const sleDst = AccountRootEntryR(*dest, view);
auto const sleDst = view.read(keylet::account(*dest));
if (!sleDst)
return tecNO_DST;
@@ -899,7 +895,7 @@ tokenOfferCreatePreclaim(
if (owner)
{
auto const sleOwner = AccountRootEntryR(*owner, view);
auto const sleOwner = view.read(keylet::account(*owner));
// defensively check
// it should not be possible to specify owner that doesn't exist
@@ -938,8 +934,9 @@ tokenOfferCreateApply(
beast::Journal j,
std::uint32_t txFlags)
{
if (auto const acct = AccountRootEntryW(acctID, view, j);
priorBalance < acct.reserve({.ownerCountDelta = 1}))
Keylet const acctKeylet = keylet::account(acctID);
if (auto const acct = view.read(acctKeylet);
priorBalance < accountReserve(view, acct, j, {.ownerCountDelta = 1}))
return tecINSUFFICIENT_RESERVE;
auto const offerID = keylet::nftokenOffer(acctID, seqProxy);
@@ -1008,7 +1005,7 @@ checkTrustlineAuthorized(
if (view.rules().enabled(fixEnforceNFTokenTrustlineV2))
{
auto const issuerAccount = AccountRootEntryR(issue.account, view);
auto const issuerAccount = view.read(keylet::account(issue.account));
if (!issuerAccount)
{
JLOG(j.debug()) << "xrpl::nft::checkTrustlineAuthorized: can't "
@@ -1060,7 +1057,7 @@ checkTrustlineDeepFrozen(
if (view.rules().enabled(featureDeepFreeze))
{
auto const issuerAccount = AccountRootEntryR(issue.account, view);
auto const issuerAccount = view.read(keylet::account(issue.account));
if (!issuerAccount)
{
JLOG(j.debug()) << "xrpl::nft::checkTrustlineDeepFrozen: can't "
@@ -6,7 +6,6 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/AccountRootEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
@@ -52,7 +51,7 @@ closeChannel(SLE::Ref slep, ApplyView& view, UInt256 const& key, beast::Journal
}
// Transfer amount back to owner, decrement owner count
auto sle = AccountRootEntryW(src, view);
auto const sle = view.peek(keylet::account(src));
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
@@ -60,7 +59,7 @@ closeChannel(SLE::Ref slep, ApplyView& view, UInt256 const& key, beast::Journal
(*slep)[sfAmount] >= (*slep)[sfBalance], "xrpl::closeChannel : minimum channel amount");
(*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance];
decreaseOwnerCountForObject(view, sle, slep, 1, j);
sle.update();
view.update(sle);
// Remove PayChan from ledger
view.erase(slep);
@@ -7,7 +7,6 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/AccountRootEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
@@ -134,14 +133,14 @@ isFrozen(
{
if (isXRP(currency))
return false;
auto const sleIssuer = AccountRootEntryR(issuer, view);
if (sleIssuer && sleIssuer->isFlag(lsfGlobalFreeze))
auto sle = view.read(keylet::account(issuer));
if (sle && sle->isFlag(lsfGlobalFreeze))
return true;
if (issuer != account)
{
// Check if the issuer froze the line
auto const sleLine = view.read(keylet::trustLine(account, issuer, currency));
if (sleLine && sleLine->isFlag((issuer > account) ? lsfHighFreeze : lsfLowFreeze))
sle = view.read(keylet::trustLine(account, issuer, currency));
if (sle && sle->isFlag((issuer > account) ? lsfHighFreeze : lsfLowFreeze))
return true;
}
return false;
@@ -239,7 +238,7 @@ trustCreate(
XRPL_ASSERT(
sleAccount->getAccountID(sfAccount) == (bSetHigh ? uHighAccountID : uLowAccountID),
"xrpl::trustCreate : matching account ID");
auto const slePeer = AccountRootEntryW(bSetHigh ? uLowAccountID : uHighAccountID, view);
auto const slePeer = view.peek(keylet::account(bSetHigh ? uLowAccountID : uHighAccountID));
if (!slePeer)
return tecNO_TARGET;
@@ -284,11 +283,7 @@ trustCreate(
}
sleRippleState->setFieldU32(sfFlags, uFlags);
AccountRootEntryW accountSle(bSetHigh ? uHighAccountID : uLowAccountID, view, j);
std::optional<AccountRootEntryW> sponsorAccSle;
if (sponsorSle)
sponsorAccSle.emplace(sponsorSle->getAccountID(sfAccount), view);
accountSle.increaseOwnerCount(sponsorAccSle, 1);
increaseOwnerCount(view, sleAccount, sponsorSle, 1, j);
addSponsorToLedgerEntry(sleRippleState, sponsorSle, bSetHigh ? sfHighSponsor : sfLowSponsor);
@@ -354,7 +349,7 @@ updateTrustLine(
if (!state)
return false;
auto sle = AccountRootEntryW(sender, view, j);
auto sle = view.peek(keylet::account(sender));
if (!sle)
return false;
@@ -381,9 +376,9 @@ updateTrustLine(
{
// VFALCO Where is the line being deleted?
// Clear the reserve of the sender, possibly delete the line!
auto currentSponsor =
auto const currentSponsor =
getLedgerEntryReserveSponsor(view, state, bSenderHigh ? sfHighSponsor : sfLowSponsor);
sle.decreaseOwnerCount(currentSponsor, 1);
decreaseOwnerCount(view, sle, currentSponsor, 1, j);
// Clear reserve flag.
state->clearFlag(senderReserveFlag);
@@ -469,7 +464,7 @@ issueIOU(
finalBalance.get<Issue>().account = noAccount();
auto receiverAccount = AccountRootEntryW(account, view);
auto const receiverAccount = view.peek(keylet::account(account));
if (!receiverAccount)
return tefINTERNAL; // LCOV_EXCL_LINE
@@ -481,7 +476,7 @@ issueIOU(
issue.account,
account,
index.key,
receiverAccount.mutableRawSle(),
receiverAccount,
false,
noRipple,
false,
@@ -584,7 +579,7 @@ requireAuth(ReadView const& view, Issue const& issue, AccountID const& account,
// If this is a weak or legacy check, or if the account has a line, fail if
// auth is required and not set on the line
if (auto const issuerAccount = AccountRootEntryR(issue.account, view);
if (auto const issuerAccount = view.read(keylet::account(issue.account));
issuerAccount && issuerAccount->isFlag(lsfRequireAuth))
{
if (trustLine)
@@ -614,8 +609,8 @@ canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, Acc
auto const& issuerId = issue.getIssuer();
if (issuerId == from || issuerId == to)
return tesSUCCESS;
auto const sleIssuer = AccountRootEntryR(issuerId, view);
if (!sleIssuer.exists())
auto const sleIssuer = view.read(keylet::account(issuerId));
if (sleIssuer == nullptr)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const isRippleDisabled = [&](AccountID account) -> bool {
@@ -670,8 +665,8 @@ addEmptyHolding(
if (isGlobalFrozen(ctx.view, issuerId))
return tecFROZEN; // LCOV_EXCL_LINE
auto const sleSrc = AccountRootEntryW(srcId, ctx.view);
auto sleDst = AccountRootEntryW(dstId, ctx.view, journal);
auto const sleSrc = ctx.view.peek(keylet::account(srcId));
auto const sleDst = ctx.view.peek(keylet::account(dstId));
if (!sleDst || !sleSrc)
return tefINTERNAL; // LCOV_EXCL_LINE
// Create path: DefaultRipple is still required. terNO_RIPPLE is
@@ -686,14 +681,20 @@ addEmptyHolding(
return tecDUPLICATE;
// A reserve sponsor only covers tx.Account's own objects.
auto sponsorExp = getEffectiveTxReserveSponsor(ctx, sleDst);
auto const sponsorExp = getEffectiveTxReserveSponsor(ctx, sleDst);
if (!sponsorExp)
return sponsorExp.error(); // LCOV_EXCL_LINE
auto& sponsorSle = *sponsorExp;
auto const sponsorSle = *sponsorExp;
// Can the account cover the trust line reserve ?
if (auto const ret = sleDst.checkReserve(
ctx, priorBalance, sponsorSle, {.ownerCountDelta = 1}, tecNO_LINE_INSUF_RESERVE);
if (auto const ret = checkReserve(
ctx,
sleDst,
priorBalance,
sponsorSle,
{.ownerCountDelta = 1},
journal,
tecNO_LINE_INSUF_RESERVE);
!isTesSuccess(ret))
{
return ret;
@@ -705,7 +706,7 @@ addEmptyHolding(
srcId,
dstId,
index.key,
sleDst.mutableRawSle(),
sleDst,
/*bAuth=*/false,
/*bNoRipple=*/true,
/*bFreeze=*/false,
@@ -714,7 +715,7 @@ addEmptyHolding(
/*saLimit=*/STAmount{Issue{currency, dstId}},
/*uQualityIn=*/0,
/*uQualityOut=*/0,
sponsorSle ? sponsorSle->mutableRawSle() : SLE::pointer{},
sponsorSle,
journal);
}
@@ -727,7 +728,7 @@ removeEmptyHolding(
{
if (issue.native())
{
auto const sle = AccountRootEntryW(accountID, ctx.view);
auto const sle = ctx.view.read(keylet::account(accountID));
if (!sle)
return tecINTERNAL; // LCOV_EXCL_LINE
@@ -752,14 +753,13 @@ removeEmptyHolding(
if (line->isFlag(lsfLowReserve))
{
// Clear reserve for low account.
auto sleLowAccount =
AccountRootEntryW(line->at(sfLowLimit)->getIssuer(), ctx.view, journal);
auto sleLowAccount = ctx.view.peek(keylet::account(line->at(sfLowLimit)->getIssuer()));
if (!sleLowAccount)
return tecINTERNAL; // LCOV_EXCL_LINE
auto currentLowSponsor = getLedgerEntryReserveSponsor(ctx.view, line, sfLowSponsor);
auto const currentLowSponsor = getLedgerEntryReserveSponsor(ctx.view, line, sfLowSponsor);
sleLowAccount.decreaseOwnerCount(currentLowSponsor, 1);
decreaseOwnerCount(ctx.view, sleLowAccount, currentLowSponsor, 1, journal);
// It's not really necessary to clear the reserve flag, since the line
// is about to be deleted, but this will make the metadata reflect an
// accurate state at the time of deletion.
@@ -770,14 +770,13 @@ removeEmptyHolding(
if (line->isFlag(lsfHighReserve))
{
// Clear reserve for high account.
auto sleHighAccount =
AccountRootEntryW(line->at(sfHighLimit)->getIssuer(), ctx.view, journal);
auto sleHighAccount = ctx.view.peek(keylet::account(line->at(sfHighLimit)->getIssuer()));
if (!sleHighAccount)
return tecINTERNAL; // LCOV_EXCL_LINE
auto currentHighSponsor = getLedgerEntryReserveSponsor(ctx.view, line, sfHighSponsor);
auto const currentHighSponsor = getLedgerEntryReserveSponsor(ctx.view, line, sfHighSponsor);
sleHighAccount.decreaseOwnerCount(currentHighSponsor, 1);
decreaseOwnerCount(ctx.view, sleHighAccount, currentHighSponsor, 1, journal);
// It's not really necessary to clear the reserve flag, since the line
// is about to be deleted, but this will make the metadata reflect an
// accurate state at the time of deletion.
@@ -806,8 +805,8 @@ deleteAMMTrustLine(
auto const& [low, high] = std::minmax(
sleState->getFieldAmount(sfLowLimit).getIssuer(),
sleState->getFieldAmount(sfHighLimit).getIssuer());
auto sleLow = AccountRootEntryW(low, view, j);
auto sleHigh = AccountRootEntryW(high, view, j);
auto sleLow = view.peek(keylet::account(low));
auto sleHigh = view.peek(keylet::account(high));
if (!sleLow || !sleHigh)
return tecINTERNAL; // LCOV_EXCL_LINE
@@ -826,7 +825,7 @@ deleteAMMTrustLine(
if (ammAccountID && (low != *ammAccountID && high != *ammAccountID))
return terNO_AMM;
auto sponsorSle =
auto const sponsorSle =
getLedgerEntryReserveSponsor(view, sleState, !ammLow ? sfLowSponsor : sfHighSponsor);
if (auto const ter = trustDelete(view, sleState, low, high, j); !isTesSuccess(ter))
@@ -839,7 +838,7 @@ deleteAMMTrustLine(
if (!sleState->isFlag(uFlags))
return tecINTERNAL; // LCOV_EXCL_LINE
(!ammLow ? sleLow : sleHigh).decreaseOwnerCount(sponsorSle, 1);
decreaseOwnerCount(view, !ammLow ? sleLow : sleHigh, sponsorSle, 1, j);
return tesSUCCESS;
}
+18 -19
View File
@@ -3,7 +3,6 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/AccountRootEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/OracleHelpers.h>
#include <xrpl/ledger/helpers/ProposalHelpers.h>
@@ -77,7 +76,7 @@ getTxReserveSponsorID(STTx const& tx)
return {};
}
std::expected<std::optional<AccountRootEntryW>, TER>
std::expected<SLE::pointer, TER>
getTxReserveSponsor(ApplyViewContext ctx)
{
auto const sponsorID = getTxReserveSponsorID(ctx.tx);
@@ -86,17 +85,17 @@ getTxReserveSponsor(ApplyViewContext ctx)
XRPL_ASSERT(
ctx.view.rules().enabled(featureSponsor),
"xrpl::getTxReserveSponsor : sponsor exists + Sponsor enabled");
auto sle = AccountRootEntryW(*sponsorID, ctx.view);
auto sle = ctx.view.peek(keylet::account(*sponsorID));
// already checked in Transactor::checkSponsor
if (!sle.exists())
if (!sle)
return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE
return sle;
}
return std::nullopt;
return SLE::pointer();
}
std::expected<std::optional<AccountRootEntryR>, TER>
std::expected<SLE::const_pointer, TER>
getTxReserveSponsor(ReadView const& view, STTx const& tx)
{
auto const sponsorID = getTxReserveSponsorID(tx);
@@ -105,18 +104,18 @@ getTxReserveSponsor(ReadView const& view, STTx const& tx)
XRPL_ASSERT(
view.rules().enabled(featureSponsor),
"xrpl::getTxReserveSponsor : sponsor exists + Sponsor enabled");
auto sle = AccountRootEntryR(*sponsorID, view);
auto sle = view.read(keylet::account(*sponsorID));
// already checked in Transactor::checkSponsor
if (!sle.exists())
if (!sle)
return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE
return sle;
}
return std::nullopt;
return SLE::pointer();
}
std::expected<std::optional<AccountRootEntryW>, TER>
getEffectiveTxReserveSponsor(ApplyViewContext ctx, AccountRootEntryR const& accountSle)
std::expected<SLE::pointer, TER>
getEffectiveTxReserveSponsor(ApplyViewContext ctx, SLE::ConstRef accountSle)
{
// A reserve sponsor only covers tx.Account's own objects.
if (ctx.view.rules().enabled(fixCleanup3_2_0))
@@ -134,7 +133,7 @@ getEffectiveTxReserveSponsor(ApplyViewContext ctx, AccountRootEntryR const& acco
}
if (isPseudoAccount(accountSle) || accountSle->getAccountID(sfAccount) != ctx.tx[sfAccount])
return std::nullopt;
return SLE::pointer();
return getTxReserveSponsor(ctx);
}
@@ -152,7 +151,7 @@ getLedgerEntryReserveSponsorID(SLE::ConstRef sle, SF_ACCOUNT const& field)
return {};
}
std::optional<AccountRootEntryW>
SLE::pointer
getLedgerEntryReserveSponsor(ApplyView& view, SLE::ConstRef sle, SF_ACCOUNT const& field)
{
auto const sponsorID = getLedgerEntryReserveSponsorID(sle, field);
@@ -161,9 +160,9 @@ getLedgerEntryReserveSponsor(ApplyView& view, SLE::ConstRef sle, SF_ACCOUNT cons
XRPL_ASSERT(
view.rules().enabled(featureSponsor),
"xrpl::getLedgerEntryReserveSponsor : sponsor exists + Sponsor enabled");
return AccountRootEntryW(*sponsorID, view);
return view.peek(keylet::account(*sponsorID));
}
return std::nullopt;
return {};
}
void
@@ -186,16 +185,16 @@ addSponsorToLedgerEntry(SLE::Ref sle, SLE::ConstRef sponsorSle, SF_ACCOUNT const
void
addSponsorToLedgerEntry(ApplyViewContext ctx, SLE::Ref sle, SF_ACCOUNT const& field)
{
// getTxReserveSponsor yields nullopt when the tx is not reserve-sponsored,
// so addSponsorToLedgerEntry becomes a no-op then. The error case
// (tecINTERNAL) is an already-checked invariant; skip stamping.
// getTxReserveSponsor yields a null pointer when the tx is not
// reserve-sponsored, so addSponsorToLedgerEntry becomes a no-op then. The
// error case (tecINTERNAL) is an already-checked invariant; skip stamping.
auto const sponsorSle = getTxReserveSponsor(ctx);
if (sponsorSle && *sponsorSle)
{
XRPL_ASSERT(
ctx.view.rules().enabled(featureSponsor),
"xrpl::addSponsorToLedgerEntry : sponsor exists + Sponsor enabled");
addSponsorToLedgerEntry(sle, (**sponsorSle).rawSle(), field);
addSponsorToLedgerEntry(sle, *sponsorSle, field);
}
}
+18 -24
View File
@@ -7,7 +7,6 @@
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/AccountRootEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
@@ -294,7 +293,7 @@ getLineIfUsable(
// we need to check if the associated assets have been frozen
if (view.rules().enabled(fixFrozenLPTokenTransfer))
{
auto const sleIssuer = AccountRootEntryR(issuer, view);
auto const sleIssuer = view.read(keylet::account(issuer));
if (!sleIssuer)
{
return nullptr; // LCOV_EXCL_LINE
@@ -563,7 +562,7 @@ canAddHolding(ReadView const& view, Issue const& issue)
return tesSUCCESS; // No special checks for XRP
}
auto const issuer = AccountRootEntryR(issue.getIssuer(), view);
auto const issuer = view.read(keylet::account(issue.getIssuer()));
if (!issuer)
{
return terNO_ACCOUNT;
@@ -739,8 +738,6 @@ directSendNoFeeIOU(
auto const senderFreezeFlag = bSenderHigh ? lsfHighFreeze : lsfLowFreeze;
auto const receiverReserveFlag = bSenderHigh ? lsfLowReserve : lsfHighReserve;
AccountRootEntryW senderSle(uSenderID, view, j);
// FIXME This NEEDS to be cleaned up and simplified. It's impossible
// for anyone to understand.
if (saBefore > beast::kZero
@@ -749,7 +746,8 @@ directSendNoFeeIOU(
// Sender is zero or negative.
&& sleRippleState->isFlag(senderReserveFlag)
// Sender reserve is set.
&& sleRippleState->isFlag(senderNoRippleFlag) != senderSle->isFlag(lsfDefaultRipple) &&
&& sleRippleState->isFlag(senderNoRippleFlag) !=
view.read(keylet::account(uSenderID))->isFlag(lsfDefaultRipple) &&
!sleRippleState->isFlag(senderFreezeFlag) &&
!sleRippleState->getFieldAmount(bSenderHigh ? sfHighLimit : sfLowLimit)
// Sender trust limit is 0.
@@ -760,9 +758,9 @@ directSendNoFeeIOU(
// Sender quality out is 0.
{
// Clear the reserve of the sender, possibly delete the line!
auto currentSponsor = getLedgerEntryReserveSponsor(
auto const currentSponsor = getLedgerEntryReserveSponsor(
view, sleRippleState, !bSenderHigh ? sfLowSponsor : sfHighSponsor);
senderSle.decreaseOwnerCount(currentSponsor, 1);
decreaseOwnerCount(view, view.peek(keylet::account(uSenderID)), currentSponsor, 1, j);
removeSponsorFromLedgerEntry(
sleRippleState, !bSenderHigh ? sfLowSponsor : sfHighSponsor);
@@ -807,7 +805,7 @@ directSendNoFeeIOU(
<< to_string(uSenderID) << " -> " << to_string(uReceiverID) << " : "
<< saAmount.getFullText();
auto sleAccount = AccountRootEntryW(uReceiverID, view);
auto const sleAccount = view.peek(keylet::account(uReceiverID));
if (!sleAccount)
return tefINTERNAL; // LCOV_EXCL_LINE
@@ -819,7 +817,7 @@ directSendNoFeeIOU(
uSenderID,
uReceiverID,
index.key,
sleAccount.mutableRawSle(),
sleAccount,
false,
noRipple,
false,
@@ -1012,12 +1010,10 @@ accountSendIOU(
*/
TER terResult(tesSUCCESS);
SLE::pointer const sender = uSenderID != beast::kZero
? AccountRootEntryW(uSenderID, view).mutableRawSle()
: SLE::pointer();
SLE::pointer const receiver = uReceiverID != beast::kZero
? AccountRootEntryW(uReceiverID, view).mutableRawSle()
: SLE::pointer();
SLE::pointer const sender =
uSenderID != beast::kZero ? view.peek(keylet::account(uSenderID)) : SLE::pointer();
SLE::pointer const receiver =
uReceiverID != beast::kZero ? view.peek(keylet::account(uReceiverID)) : SLE::pointer();
if (auto stream = j.trace())
{
@@ -1110,9 +1106,8 @@ accountSendMultiIOU(
* ensure that transfers are balanced.
*/
SLE::pointer const sender = senderID != beast::kZero
? AccountRootEntryW(senderID, view).mutableRawSle()
: SLE::pointer();
SLE::pointer const sender =
senderID != beast::kZero ? view.peek(keylet::account(senderID)) : SLE::pointer();
if (auto stream = j.trace())
{
@@ -1143,9 +1138,8 @@ accountSendMultiIOU(
if (!amount || (senderID == receiverID))
continue;
SLE::pointer const receiver = receiverID != beast::kZero
? AccountRootEntryW(receiverID, view).mutableRawSle()
: SLE::pointer();
SLE::pointer const receiver =
receiverID != beast::kZero ? view.peek(keylet::account(receiverID)) : SLE::pointer();
if (auto stream = j.trace())
{
@@ -1597,8 +1591,8 @@ transferXRP(
XRPL_ASSERT(from != to, "xrpl::transferXRP : sender is not receiver");
XRPL_ASSERT(amount.native(), "xrpl::transferXRP : amount is XRP");
SLE::pointer const sender = AccountRootEntryW(from, view).mutableRawSle();
SLE::pointer const receiver = AccountRootEntryW(to, view).mutableRawSle();
SLE::pointer const sender = view.peek(keylet::account(from));
SLE::pointer const receiver = view.peek(keylet::account(to));
if (!sender || !receiver)
return tefINTERNAL; // LCOV_EXCL_LINE
@@ -35,7 +35,6 @@
#include <atomic>
#include <cstddef>
#include <filesystem>
#include <format>
#include <functional>
#include <memory>
#include <stdexcept>
@@ -194,7 +193,7 @@ public:
if (!s.ok())
{
Throw<std::runtime_error>(
std::format("Unable to set RocksDB bbt_options: {}", s.ToString()));
std::string("Unable to set RocksDB bbt_options: ") + s.ToString());
}
}
@@ -207,7 +206,7 @@ public:
if (!s.ok())
{
Throw<std::runtime_error>(
std::format("Unable to set RocksDB options: {}", s.ToString()));
std::string("Unable to set RocksDB options: ") + s.ToString());
}
}
@@ -242,7 +241,7 @@ public:
if (!status.ok() || (localDb == nullptr))
{
Throw<std::runtime_error>(
std::format("Unable to open/create RocksDB: {}", status.ToString()));
std::string("Unable to open/create RocksDB: ") + status.ToString());
}
db.reset(localDb);
}
@@ -357,7 +356,7 @@ public:
auto ret = db->Write(options, &wb);
if (!ret.ok())
Throw<std::runtime_error>(std::format("storeBatch failed: {}", ret.ToString()));
Throw<std::runtime_error>("storeBatch failed: " + ret.ToString());
}
void
Loaded 100 of 226 files, more files were not shown because too many files have changed in this diff. Show more