mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-22 11:37:07 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae9cb05521 |
No files matched your search
+4
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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."
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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/*"
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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,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 }}
|
||||
@@ -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' }}' \
|
||||
|
||||
@@ -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}"
|
||||
@@ -92,9 +92,6 @@ target/
|
||||
# Direnv's directory
|
||||
/.direnv
|
||||
|
||||
# Direnv's local, per-developer overrides
|
||||
/.envrc.local
|
||||
|
||||
# clangd cache
|
||||
/.cache
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
@@ -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
|
||||
|
||||
Executable
+56
@@ -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
|
||||
@@ -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}")
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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}")
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
**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.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
**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
|
||||
```
|
||||
@@ -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 |
@@ -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 |
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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("-", ".")
|
||||
|
||||
@@ -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.
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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))
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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()))
|
||||
|
||||
@@ -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,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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Reference in new issue
Block a user