mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-12 14:48:10 +00:00
Compare commits
1 Commits
nb-counted
...
tapanito/n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae9cb05521 |
@@ -23,6 +23,8 @@ ignoreRegExpList:
|
||||
- /\b(XRPL|BEAST)_[A-Z_0-9]+_H+/g # include guards
|
||||
- /::[a-z:_]+/g # things from other namespaces
|
||||
- /\blib[a-z]+/g # libraries
|
||||
- /\b(mpd|MPD)_[A-Za-z_0-9]+/g # libmpdec (mpdecimal) API
|
||||
- /\bbid(64|128)_[a-z_0-9]+/g # Intel decimal library API
|
||||
- /\b[0-9]{4}-[0-9]{2}-[0-9]{2}[,:][A-Za-zÀ-ÖØ-öø-ÿ.\s]+/g # copyright dates
|
||||
- /\b[0-9]{4}[,:]?\s*[A-Za-zÀ-ÖØ-öø-ÿ.\s]+/g # copyright years
|
||||
- /\[[A-Za-z0-9-]+\]\(https:\/\/github.com\/[A-Za-z0-9-]+\)/g # Github usernames
|
||||
@@ -44,6 +46,7 @@ suggestWords:
|
||||
- synch->sync
|
||||
words:
|
||||
- abempty
|
||||
- allcr
|
||||
- AMMID
|
||||
- AMMMPT
|
||||
- AMMMPToken
|
||||
@@ -184,6 +187,7 @@ words:
|
||||
- Merkle
|
||||
- misprediction
|
||||
- missingok
|
||||
- mpdecimal
|
||||
- mptbalance
|
||||
- MPTDEX
|
||||
- mptflags
|
||||
@@ -255,7 +259,6 @@ words:
|
||||
- queuable
|
||||
- Raphson
|
||||
- rcflags
|
||||
- reencrypted
|
||||
- replayer
|
||||
- repodata
|
||||
- repomd
|
||||
|
||||
4
.envrc
4
.envrc
@@ -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)
|
||||
|
||||
17
.github/actions/release-info/action.yml
vendored
17
.github/actions/release-info/action.yml
vendored
@@ -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
|
||||
38
.github/scripts/releasing/check-no-merge-back.sh
vendored
38
.github/scripts/releasing/check-no-merge-back.sh
vendored
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Exit the script as soon as an error occurs.
|
||||
set -euo pipefail
|
||||
|
||||
# This script fails if <head> contains release or staging commits that <base> does not,
|
||||
# i.e. if <head> merges a release back into <base>.
|
||||
# Used in the merge queue, which squashes PRs and would drop the merge commit, see RELEASING.md.
|
||||
# Usage: .github/scripts/releasing/check-no-merge-back.sh <base> <head>
|
||||
|
||||
if [ "$#" -ne 2 ]; then
|
||||
echo "Usage: $0 <base> <head>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BASE=$1
|
||||
HEAD=$2
|
||||
|
||||
SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}")
|
||||
# shellcheck source=.github/scripts/releasing/common.sh
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
load_release_branches
|
||||
|
||||
RELEASE_COMMITS=$(git rev-list "${BRANCHES[@]}" --not "${BASE}")
|
||||
HEAD_COMMITS=$(git rev-list "${BASE}..${HEAD}")
|
||||
# The release commits in <head>, newest first.
|
||||
MERGED=$(grep -xF -f <(echo "${RELEASE_COMMITS}") <<<"${HEAD_COMMITS}" || true)
|
||||
if [ -z "${MERGED}" ]; then
|
||||
echo "No release commits are merged back."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "This PR merges $(wc -l <<<"${MERGED}" | tr -d ' ') release commits back, e.g.:"
|
||||
head -5 <<<"${MERGED}" | xargs git log --no-walk --format=' %h %s'
|
||||
echo
|
||||
echo "Merge-backs must not go through the merge queue, which squashes them."
|
||||
echo "Fast-forward develop to the PR branch instead, see RELEASING.md."
|
||||
exit 1
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Exit the script as soon as an error occurs.
|
||||
set -euo pipefail
|
||||
|
||||
# This script fails if a final release (a tag like 3.4.0) on a release branch
|
||||
# is not merged back into <develop> within a few days, see RELEASING.md.
|
||||
# Usage: .github/scripts/releasing/check-releases-merged.sh <develop>
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "Usage: $0 <develop>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DEVELOP=$1
|
||||
GRACE_DAYS=3
|
||||
|
||||
mapfile -t MERGED_ARGS < <(git for-each-ref --format='--merged=%(refname)' 'refs/remotes/*/release/*')
|
||||
if [ "${#MERGED_ARGS[@]}" -eq 0 ]; then
|
||||
echo "Error: No release branches found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Tags on a release branch that <develop> does not contain.
|
||||
TAGS=$(git for-each-ref --format='%(refname:short) %(creatordate:unix)' \
|
||||
"${MERGED_ARGS[@]}" --no-merged="${DEVELOP}" 'refs/tags/[0-9]*')
|
||||
|
||||
MISSING=0
|
||||
while read -r TAG CREATED; do
|
||||
if ! [[ "${TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
continue
|
||||
fi
|
||||
if (($(date +%s) - CREATED < GRACE_DAYS * 86400)); then
|
||||
echo "${TAG}: not merged yet, still within the ${GRACE_DAYS}-day grace period."
|
||||
else
|
||||
echo "${TAG}: not merged into develop."
|
||||
MISSING=1
|
||||
fi
|
||||
done <<<"${TAGS}"
|
||||
|
||||
if [ "${MISSING}" -ne 0 ]; then
|
||||
echo
|
||||
echo "Merge the missing releases back into develop, see RELEASING.md."
|
||||
exit 1
|
||||
fi
|
||||
echo "No releases past the grace period are missing from develop."
|
||||
12
.github/scripts/releasing/common.sh
vendored
12
.github/scripts/releasing/common.sh
vendored
@@ -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
|
||||
}
|
||||
6
.github/scripts/strategy-matrix/linux.json
vendored
6
.github/scripts/strategy-matrix/linux.json
vendored
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
6
.github/workflows/build-packaging-images.yml
vendored
6
.github/workflows/build-packaging-images.yml
vendored
@@ -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' }}
|
||||
|
||||
1
.github/workflows/check-pr-description.yml
vendored
1
.github/workflows/check-pr-description.yml
vendored
@@ -13,6 +13,7 @@ on:
|
||||
- ready_for_review
|
||||
branches:
|
||||
- develop
|
||||
- "release-*"
|
||||
- "release/*"
|
||||
- "staging/*"
|
||||
|
||||
|
||||
1
.github/workflows/check-pr-title.yml
vendored
1
.github/workflows/check-pr-title.yml
vendored
@@ -13,6 +13,7 @@ on:
|
||||
- ready_for_review
|
||||
branches:
|
||||
- develop
|
||||
- "release-*"
|
||||
- "release/*"
|
||||
- "staging/*"
|
||||
|
||||
|
||||
60
.github/workflows/check-releases-merged.yml
vendored
60
.github/workflows/check-releases-merged.yml
vendored
@@ -1,60 +0,0 @@
|
||||
# This workflow checks that every final release on a release branch
|
||||
# has been merged back into develop, see RELEASING.md.
|
||||
name: Check releases merged
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 06:47 UTC every Monday.
|
||||
- cron: "47 6 * * 1"
|
||||
push:
|
||||
branches:
|
||||
- "develop"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
OUTPUT_FILE: /tmp/releases-merged.txt
|
||||
ISSUE_FILE: /tmp/releases-merged-issue.md
|
||||
|
||||
jobs:
|
||||
releases-merged:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
# Needed to open an issue on scheduled failures.
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
# The check needs the full history of develop, the release branches and tags.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check releases are merged into develop
|
||||
run: |
|
||||
set -o pipefail
|
||||
.github/scripts/releasing/check-releases-merged.sh origin/develop | tee "${OUTPUT_FILE}"
|
||||
|
||||
- name: Prepare issue body
|
||||
if: ${{ failure() && github.event_name == 'schedule' && github.repository == 'XRPLF/rippled' }}
|
||||
run: |
|
||||
{
|
||||
echo '```'
|
||||
cat "${OUTPUT_FILE}"
|
||||
echo '```'
|
||||
} >"${ISSUE_FILE}"
|
||||
|
||||
- name: Create issue
|
||||
if: ${{ failure() && github.event_name == 'schedule' && github.repository == 'XRPLF/rippled' }}
|
||||
uses: XRPLF/actions/create-issue@2b8bc36af85b88bca0dd7bfac2e2dc05f94ad712
|
||||
with:
|
||||
title: "Releases not merged back into develop"
|
||||
body_file: ${{ env.ISSUE_FILE }}
|
||||
labels: "Bug"
|
||||
43
.github/workflows/on-pr.yml
vendored
43
.github/workflows/on-pr.yml
vendored
@@ -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
|
||||
|
||||
9
.github/workflows/on-tag.yml
vendored
9
.github/workflows/on-tag.yml
vendored
@@ -1,12 +1,9 @@
|
||||
# When a versioned tag is pushed, this workflow:
|
||||
#
|
||||
# - builds and tests the release binaries
|
||||
# - uploads the libxrpl recipe to the Conan remote
|
||||
# - builds and tests the release binaries
|
||||
# - builds the DEB and RPM packages
|
||||
# - publishes those packages to the XRPLF package repositories
|
||||
#
|
||||
# Nothing is published unless the build passes, which is also where CMake
|
||||
# rejects a tag that is not a valid version, e.g. 3.2.01.
|
||||
name: Tag
|
||||
|
||||
on:
|
||||
@@ -25,7 +22,6 @@ defaults:
|
||||
jobs:
|
||||
upload-recipe:
|
||||
if: ${{ github.repository == 'XRPLF/rippled' }}
|
||||
needs: build-test
|
||||
uses: ./.github/workflows/reusable-upload-recipe.yml
|
||||
secrets:
|
||||
remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
|
||||
@@ -55,6 +51,3 @@ jobs:
|
||||
remote_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
|
||||
signing_key: ${{ secrets.NEXUS_PACKAGES_PRIVATE_KEY }}
|
||||
dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
antithesis_docker_host: ${{ secrets.ANTITHESIS_DOCKER_HOST }}
|
||||
antithesis_docker_path: ${{ secrets.ANTITHESIS_DOCKER_PATH }}
|
||||
antithesis_docker_credentials: ${{ secrets.ANTITHESIS_DOCKER_CREDENTIALS }}
|
||||
|
||||
3
.github/workflows/on-trigger.yml
vendored
3
.github/workflows/on-trigger.yml
vendored
@@ -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 }}
|
||||
|
||||
13
.github/workflows/reusable-build-test-config.yml
vendored
13
.github/workflows/reusable-build-test-config.yml
vendored
@@ -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' }}' \
|
||||
|
||||
71
.github/workflows/reusable-package.yml
vendored
71
.github/workflows/reusable-package.yml
vendored
@@ -9,9 +9,8 @@
|
||||
# never reaches Nexus
|
||||
# - 'publish' uploads with the image's publish_pkg.py, doing a --dry-run
|
||||
# unless 'publish: true'
|
||||
# - 'docker' builds an Ubuntu image from the tested DEB, and one with the
|
||||
# voidstar binary for Antithesis, pushing them to Docker Hub and to the
|
||||
# Antithesis registry only with 'publish: true'
|
||||
# - 'docker' builds an Ubuntu image from the tested DEB, pushing it to Docker
|
||||
# Hub only with 'publish: true'
|
||||
#
|
||||
# Only linux/amd64 is supported; the runner is hardcoded in the jobs below.
|
||||
name: Package
|
||||
@@ -38,19 +37,10 @@ on:
|
||||
description: "The password or token for that Nexus account."
|
||||
required: false
|
||||
signing_key:
|
||||
description: "Armoured PGP private key used to sign the RPMs."
|
||||
description: "Armoured PGP private key used to sign the RPMs. Required when publishing."
|
||||
required: false
|
||||
dockerhub_token:
|
||||
description: "A Docker Hub organization access token for xrplf, with push access to xrplf/xrpld."
|
||||
required: false
|
||||
antithesis_docker_host:
|
||||
description: "The host of the Antithesis container registry, e.g. us-central1-docker.pkg.dev."
|
||||
required: false
|
||||
antithesis_docker_path:
|
||||
description: "The repository path in that registry, the image name excluded."
|
||||
required: false
|
||||
antithesis_docker_credentials:
|
||||
description: "The JSON key of a service account with push access to that repository."
|
||||
description: "A Docker Hub organization access token for xrplf, with push access to xrplf/xrpld. Required when publishing."
|
||||
required: false
|
||||
|
||||
defaults:
|
||||
@@ -257,35 +247,18 @@ jobs:
|
||||
|
||||
docker:
|
||||
needs: [test-install-deb, test-install-rpm]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target: [xrpld, voidstar]
|
||||
name: "docker ${{ matrix.target }}${{ !inputs.publish && ' (dry run)' || '' }}"
|
||||
name: "docker${{ !inputs.publish && ' (dry run)' || '' }}"
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
CONTEXT: image-context
|
||||
IMAGE: ${{ matrix.target == 'voidstar' && 'xrpld-voidstar' || 'xrplf/xrpld' }}:${{ github.ref_type == 'tag' && github.ref_name || 'develop' }}
|
||||
IMAGE: xrplf/xrpld:${{ github.ref_type == 'tag' && github.ref_name || 'develop' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Determine release info
|
||||
id: release_info
|
||||
uses: ./.github/actions/release-info
|
||||
|
||||
# Docker Hub is public, so it only gets builds whose packages are public:
|
||||
# those of a public codebase, and stable releases.
|
||||
# The Antithesis registry is private, so it gets every build.
|
||||
- name: Decide whether to push
|
||||
env:
|
||||
PUSH: ${{ inputs.publish && (matrix.target == 'voidstar' || github.event.repository.visibility == 'public' || steps.release_info.outputs.channel == 'stable') }}
|
||||
run: echo "PUSH=${PUSH}" | tee -a "${GITHUB_ENV}"
|
||||
|
||||
- name: Download package artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
@@ -293,20 +266,13 @@ jobs:
|
||||
merge-multiple: true
|
||||
path: ${{ env.PACKAGE_DIR }}
|
||||
|
||||
- name: Download voidstar binary
|
||||
if: ${{ matrix.target == 'voidstar' }}
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: xrpld-ubuntu-clang-debug-amd64-voidstar
|
||||
path: ${{ env.CONTEXT }}
|
||||
|
||||
- name: Build image
|
||||
env:
|
||||
TARGET: ${{ matrix.target }}
|
||||
CONTEXT: image-context
|
||||
run: |
|
||||
mkdir -p "${CONTEXT}"
|
||||
find "${PACKAGE_DIR}" -type f -name 'xrpld_[0-9]*.deb' -exec cp {} "${CONTEXT}/" \;
|
||||
docker build --pull --file package/images/xrpld/Dockerfile --target "${TARGET}" --tag "${IMAGE}" "${CONTEXT}"
|
||||
docker build --pull --file package/image/Dockerfile --tag "${IMAGE}" "${CONTEXT}"
|
||||
|
||||
- name: Start the server
|
||||
run: |
|
||||
@@ -324,25 +290,14 @@ jobs:
|
||||
docker logs "${container}"
|
||||
exit 1
|
||||
|
||||
# Docker Hub is public, so a private build never reaches it.
|
||||
- name: Log in to Docker Hub
|
||||
if: ${{ env.PUSH == 'true' && matrix.target == 'xrpld' }}
|
||||
if: ${{ inputs.publish && github.event.repository.visibility == 'public' }}
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
username: xrplf
|
||||
password: ${{ secrets.dockerhub_token }}
|
||||
|
||||
- name: Log in to the Antithesis registry
|
||||
if: ${{ env.PUSH == 'true' && matrix.target == 'voidstar' }}
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: ${{ secrets.antithesis_docker_host }}
|
||||
username: _json_key
|
||||
password: ${{ secrets.antithesis_docker_credentials }}
|
||||
|
||||
- name: Push image
|
||||
if: ${{ env.PUSH == 'true' }}
|
||||
env:
|
||||
REGISTRY: ${{ matrix.target == 'voidstar' && format('{0}/{1}/', secrets.antithesis_docker_host, secrets.antithesis_docker_path) || '' }}
|
||||
run: |
|
||||
docker tag "${IMAGE}" "${REGISTRY}${IMAGE}"
|
||||
docker push "${REGISTRY}${IMAGE}"
|
||||
if: ${{ inputs.publish && github.event.repository.visibility == 'public' }}
|
||||
run: docker push "${IMAGE}"
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -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,19 +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))
|
||||
|
||||
### Bugfixes in 3.5.0
|
||||
|
||||
- `channel_authorize`: The `channel_id` field now returns an `invalidParams` error if the value is not a string. [#7582](https://github.com/XRPLF/rippled/pull/7582)
|
||||
- `channel_verify`: The `channel_id` and `signature` fields now return an `invalidParams` error if the value is not a string. [#7582](https://github.com/XRPLF/rippled/pull/7582)
|
||||
|
||||
## 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.
|
||||
|
||||
14
BUILD.md
14
BUILD.md
@@ -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)
|
||||
|
||||
606
CONTRIBUTING.md
606
CONTRIBUTING.md
@@ -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
RELEASING.md
152
RELEASING.md
@@ -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
|
||||
|
||||
56
bin/git/update-version.sh
Executable file
56
bin/git/update-version.sh
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ $# -ne 3 || "$1" == "--help" || "$1" = "-h" ]]; then
|
||||
name=$(basename $0)
|
||||
cat <<-USAGE
|
||||
Usage: $name workbranch base/branch version
|
||||
|
||||
* workbranch will be created locally from base/branch. If it exists,
|
||||
it will be reused, so make sure you don't overwrite any work.
|
||||
* base/branch may be specified as user:branch to allow easy copying
|
||||
from Github PRs.
|
||||
USAGE
|
||||
exit 0
|
||||
fi
|
||||
|
||||
work="$1"
|
||||
shift
|
||||
|
||||
base=$(echo "$1" | sed "s/:/\//")
|
||||
shift
|
||||
|
||||
version=$1
|
||||
shift
|
||||
|
||||
set -e
|
||||
|
||||
git fetch upstreams
|
||||
|
||||
git checkout -B "${work}" --no-track "${base}"
|
||||
|
||||
push=$(git rev-parse --abbrev-ref --symbolic-full-name '@{push}' \
|
||||
2>/dev/null) || true
|
||||
if [[ "${push}" != "" ]]; then
|
||||
echo "Warning: ${push} may already exist."
|
||||
fi
|
||||
|
||||
build=$(find -name BuildInfo.cpp)
|
||||
sed 's/\(^.*versionString =\).*$/\1 "'${version}'"/' ${build} >version.cpp &&
|
||||
diff "${build}" version.cpp && exit 1 ||
|
||||
mv -vi version.cpp ${build}
|
||||
|
||||
git diff
|
||||
|
||||
git add ${build}
|
||||
|
||||
git commit -S -m "Set version to ${version}"
|
||||
|
||||
git log --oneline --first-parent ${base}^..
|
||||
|
||||
cat <<PUSH
|
||||
|
||||
-------------------------------------------------------------------
|
||||
This script will not push. Verify everything is correct, then push
|
||||
to your repo, and create a PR as described in CONTRIBUTING.md.
|
||||
-------------------------------------------------------------------
|
||||
PUSH
|
||||
28
cmake/GitInfo.cmake
Normal file
28
cmake/GitInfo.cmake
Normal file
@@ -0,0 +1,28 @@
|
||||
include_guard()
|
||||
|
||||
set(GIT_BUILD_BRANCH "")
|
||||
set(GIT_COMMIT_HASH "")
|
||||
|
||||
find_package(Git)
|
||||
if(NOT Git_FOUND)
|
||||
message(WARNING "Git not found. Git branch and commit hash will be empty.")
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(GIT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/.git)
|
||||
|
||||
execute_process(
|
||||
COMMAND
|
||||
${GIT_EXECUTABLE} --git-dir=${GIT_DIRECTORY} rev-parse --abbrev-ref HEAD
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
OUTPUT_VARIABLE GIT_BUILD_BRANCH
|
||||
)
|
||||
|
||||
execute_process(
|
||||
COMMAND ${GIT_EXECUTABLE} --git-dir=${GIT_DIRECTORY} rev-parse HEAD
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
OUTPUT_VARIABLE GIT_COMMIT_HASH
|
||||
)
|
||||
|
||||
message(STATUS "Git branch: ${GIT_BUILD_BRANCH}")
|
||||
message(STATUS "Git commit hash: ${GIT_COMMIT_HASH}")
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
29
conanfile.py
29
conanfile.py
@@ -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
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
the `xrpld` DEB package installed on Ubuntu 26.04, running as the `xrpld` user.
|
||||
Each release is tagged with its version, `xrplf/xrpld:<version>`, and
|
||||
`xrplf/xrpld:develop` follows the `develop` branch.
|
||||
See [`package/README.md`](../package/README.md#docker-images) for how it is built
|
||||
See [`package/README.md`](../package/README.md#docker-image) for how it is built
|
||||
and tagged.
|
||||
|
||||
```bash
|
||||
|
||||
409
docs/NumberDecimalBenchmark.md
Normal file
409
docs/NumberDecimalBenchmark.md
Normal file
@@ -0,0 +1,409 @@
|
||||
# Number vs. decimal floating-point libraries: benchmark plan
|
||||
|
||||
Status: complete (phases 1-5). Goal: before extending `xrpl::Number` to a 128-bit
|
||||
mantissa, (1) measure how `Number` performs against established base-10
|
||||
floating-point libraries, and (2) quantify where `Number` diverges from
|
||||
IEEE 754-2008/2019 decimal arithmetic.
|
||||
|
||||
Target precisions for a future 128-bit `Number`: **both 34 digits** (matches
|
||||
IEEE decimal128) **and 38 digits** (largest `10^k - 1` that fits in a
|
||||
`uint128`, the direct analogue of today's 19-digit-in-`uint64` design).
|
||||
|
||||
## Summary
|
||||
|
||||
Measured on one x86-64 machine with GCC 15.2; see "Findings so far" for
|
||||
method, caveats, and every number.
|
||||
|
||||
**Correctness.** Today's `Number` (`Large330`) is correctly rounded for
|
||||
`+ - * /` and conversion to int64 in all four rounding modes: 9.2 million of
|
||||
9.2 million samples, including the int64-cap cases older scales got wrong.
|
||||
Its `root2` (up to ~5 ULP) and `power` (up to ~2800 ULP for n = 360) are not
|
||||
correctly rounded. No IEEE library tested has an accurate integer `power`
|
||||
either, and loan payments lose only ~4 digits to it at every precision.
|
||||
Boost.Decimal 1.91 has several rounding defects (see "Validating the
|
||||
oracle"); Intel's library had none in `+ - * /`, `sqrt`, or int64
|
||||
conversion.
|
||||
|
||||
**Divergence from IEEE 754.** `Number` is always normalized (no cohorts),
|
||||
has no infinities, NaNs, signed zero, subnormals, or status flags, throws on
|
||||
overflow and division by zero, flushes underflow to zero, has a wider
|
||||
exponent range, and only ~18.96 digits of uniform precision because the
|
||||
external mantissa is capped at int64. See "How Number diverges from IEEE 754
|
||||
decimal".
|
||||
|
||||
**Speed.** Today's `Number` is 6-25x slower than 64-bit IEEE types at `+`
|
||||
and `*`, and slower than Intel's 34-digit BID128 at everything except
|
||||
comparison: 87 vs 12 ns for `+`, 204 vs 38 ns for `*`, parity for `/`. Most
|
||||
of the cost is normalization one digit at a time (52% of instructions in
|
||||
`Guard::doDropDigit<unsigned __int128>`) plus a heap-allocated error string
|
||||
on every `+` and `*`, not the 64-bit width.
|
||||
|
||||

|
||||
|
||||
**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
|
||||
```
|
||||
105
docs/images/number/number-amm-swap-in.svg
Normal file
105
docs/images/number/number-amm-swap-in.svg
Normal file
@@ -0,0 +1,105 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 834 276" width="834" height="276" role="img" aria-label="AMM swap-in: correct digits (higher is better) and cost (lower is faster)">
|
||||
<style>
|
||||
text{font-family:system-ui, -apple-system, sans-serif;font-size:12px}
|
||||
.bg{fill:#fcfcfb}.t1{fill:#0b0b0b}.t2{fill:#52514e}.grid{stroke:#e4e3df;stroke-width:1}.base{stroke:#b9b8b2;stroke-width:1}.f-number{fill:#2a78d6}.f-ieee64{fill:#eb6834}.f-wide{fill:#1baf7a}
|
||||
@media (prefers-color-scheme: dark){.bg{fill:#1a1a19}.t1{fill:#ffffff}.t2{fill:#c3c2b7}.grid{stroke:#383835}.base{stroke:#5e5d58}.f-number{fill:#3987e5}.f-ieee64{fill:#d95926}.f-wide{fill:#199e70}}
|
||||
</style>
|
||||
<rect class="bg" width="834" height="276"/>
|
||||
<text class="t1" x="0" y="18" font-size="15" font-weight="600">AMM swap-in: correct digits (higher is better) and cost (lower is faster)</text>
|
||||
<rect class="f-number" x="0.0" y="32" width="12" height="12" rx="3"/>
|
||||
<text class="t2" x="18.0" y="42">Number today (19 digits)</text>
|
||||
<rect class="f-ieee64" x="214.8" y="32" width="12" height="12" rx="3"/>
|
||||
<text class="t2" x="232.8" y="42">16-digit types</text>
|
||||
<rect class="f-wide" x="357.6" y="32" width="12" height="12" rx="3"/>
|
||||
<text class="t2" x="375.6" y="42">34- and 38-digit types</text>
|
||||
<text class="t1" x="0" y="94.0">Number (Large330)</text>
|
||||
<text class="t1" x="0" y="118.0">Boost decimal64</text>
|
||||
<text class="t1" x="0" y="142.0">Intel BID64</text>
|
||||
<text class="t1" x="0" y="166.0">Boost decimal128</text>
|
||||
<text class="t1" x="0" y="190.0">Intel BID128</text>
|
||||
<text class="t1" x="0" y="214.0">mpdecimal 34</text>
|
||||
<text class="t1" x="0" y="238.0">mpdecimal 38</text>
|
||||
<text class="t1" x="150" y="64" font-weight="600">Correct digits, worst case</text>
|
||||
<line class="grid" x1="150.0" y1="78" x2="150.0" y2="246"/>
|
||||
<text class="t2" x="150.0" y="262" text-anchor="middle" font-size="11">0</text>
|
||||
<line class="grid" x1="190.0" y1="78" x2="190.0" y2="246"/>
|
||||
<text class="t2" x="190.0" y="262" text-anchor="middle" font-size="11">10</text>
|
||||
<line class="grid" x1="230.0" y1="78" x2="230.0" y2="246"/>
|
||||
<text class="t2" x="230.0" y="262" text-anchor="middle" font-size="11">20</text>
|
||||
<line class="grid" x1="270.0" y1="78" x2="270.0" y2="246"/>
|
||||
<text class="t2" x="270.0" y="262" text-anchor="middle" font-size="11">30</text>
|
||||
<line class="grid" x1="310.0" y1="78" x2="310.0" y2="246"/>
|
||||
<text class="t2" x="310.0" y="262" text-anchor="middle" font-size="11">40</text>
|
||||
<text class="t2" x="310.0" y="274" text-anchor="end" font-size="11">significant digits</text>
|
||||
<path class="f-number" d="M150.0,84.0 H172.0 Q176.0,84.0 176.0,88.0 V92.0 Q176.0,96.0 172.0,96.0 H150.0 Z"><title>Number (Large330): 6.5 significant digits</title></path>
|
||||
<text class="t2" x="181.0" y="94.0" font-size="11">6.5</text>
|
||||
<path class="f-ieee64" d="M150.0,108.0 H159.2 Q163.2,108.0 163.2,112.0 V116.0 Q163.2,120.0 159.2,120.0 H150.0 Z"><title>Boost decimal64: 3.3 significant digits</title></path>
|
||||
<text class="t2" x="168.2" y="118.0" font-size="11">3.3</text>
|
||||
<path class="f-ieee64" d="M150.0,132.0 H159.2 Q163.2,132.0 163.2,136.0 V140.0 Q163.2,144.0 159.2,144.0 H150.0 Z"><title>Intel BID64: 3.3 significant digits</title></path>
|
||||
<text class="t2" x="168.2" y="142.0" font-size="11">3.3</text>
|
||||
<path class="f-wide" d="M150.0,156.0 H233.7 Q237.7,156.0 237.7,160.0 V164.0 Q237.7,168.0 233.7,168.0 H150.0 Z"><title>Boost decimal128: 21.9 significant digits</title></path>
|
||||
<text class="t2" x="242.7" y="166.0" font-size="11">21.9</text>
|
||||
<path class="f-wide" d="M150.0,180.0 H233.7 Q237.7,180.0 237.7,184.0 V188.0 Q237.7,192.0 233.7,192.0 H150.0 Z"><title>Intel BID128: 21.9 significant digits</title></path>
|
||||
<text class="t2" x="242.7" y="190.0" font-size="11">21.9</text>
|
||||
<path class="f-wide" d="M150.0,204.0 H233.7 Q237.7,204.0 237.7,208.0 V212.0 Q237.7,216.0 233.7,216.0 H150.0 Z"><title>mpdecimal 34: 21.9 significant digits</title></path>
|
||||
<text class="t2" x="242.7" y="214.0" font-size="11">21.9</text>
|
||||
<path class="f-wide" d="M150.0,228.0 H251.2 Q255.2,228.0 255.2,232.0 V236.0 Q255.2,240.0 251.2,240.0 H150.0 Z"><title>mpdecimal 38: 26.3 significant digits</title></path>
|
||||
<text class="t2" x="260.2" y="238.0" font-size="11">26.3</text>
|
||||
<line class="base" x1="150" y1="78" x2="150" y2="246"/>
|
||||
<text class="t1" x="378" y="64" font-weight="600">Correct digits, median</text>
|
||||
<line class="grid" x1="378.0" y1="78" x2="378.0" y2="246"/>
|
||||
<text class="t2" x="378.0" y="262" text-anchor="middle" font-size="11">0</text>
|
||||
<line class="grid" x1="418.0" y1="78" x2="418.0" y2="246"/>
|
||||
<text class="t2" x="418.0" y="262" text-anchor="middle" font-size="11">10</text>
|
||||
<line class="grid" x1="458.0" y1="78" x2="458.0" y2="246"/>
|
||||
<text class="t2" x="458.0" y="262" text-anchor="middle" font-size="11">20</text>
|
||||
<line class="grid" x1="498.0" y1="78" x2="498.0" y2="246"/>
|
||||
<text class="t2" x="498.0" y="262" text-anchor="middle" font-size="11">30</text>
|
||||
<line class="grid" x1="538.0" y1="78" x2="538.0" y2="246"/>
|
||||
<text class="t2" x="538.0" y="262" text-anchor="middle" font-size="11">40</text>
|
||||
<text class="t2" x="538.0" y="274" text-anchor="end" font-size="11">significant digits</text>
|
||||
<path class="f-number" d="M378.0,84.0 H429.6 Q433.6,84.0 433.6,88.0 V92.0 Q433.6,96.0 429.6,96.0 H378.0 Z"><title>Number (Large330): 13.9 significant digits</title></path>
|
||||
<text class="t2" x="438.6" y="94.0" font-size="11">13.9</text>
|
||||
<path class="f-ieee64" d="M378.0,108.0 H417.3 Q421.3,108.0 421.3,112.0 V116.0 Q421.3,120.0 417.3,120.0 H378.0 Z"><title>Boost decimal64: 10.8 significant digits</title></path>
|
||||
<text class="t2" x="426.3" y="118.0" font-size="11">10.8</text>
|
||||
<path class="f-ieee64" d="M378.0,132.0 H417.3 Q421.3,132.0 421.3,136.0 V140.0 Q421.3,144.0 417.3,144.0 H378.0 Z"><title>Intel BID64: 10.8 significant digits</title></path>
|
||||
<text class="t2" x="426.3" y="142.0" font-size="11">10.8</text>
|
||||
<path class="f-wide" d="M378.0,156.0 H492.6 Q496.6,156.0 496.6,160.0 V164.0 Q496.6,168.0 492.6,168.0 H378.0 Z"><title>Boost decimal128: 29.7 significant digits</title></path>
|
||||
<text class="t2" x="501.6" y="166.0" font-size="11">29.7</text>
|
||||
<path class="f-wide" d="M378.0,180.0 H493.3 Q497.3,180.0 497.3,184.0 V188.0 Q497.3,192.0 493.3,192.0 H378.0 Z"><title>Intel BID128: 29.8 significant digits</title></path>
|
||||
<text class="t2" x="502.3" y="190.0" font-size="11">29.8</text>
|
||||
<path class="f-wide" d="M378.0,204.0 H493.3 Q497.3,204.0 497.3,208.0 V212.0 Q497.3,216.0 493.3,216.0 H378.0 Z"><title>mpdecimal 34: 29.8 significant digits</title></path>
|
||||
<text class="t2" x="502.3" y="214.0" font-size="11">29.8</text>
|
||||
<path class="f-wide" d="M378.0,228.0 H508.7 Q512.7,228.0 512.7,232.0 V236.0 Q512.7,240.0 508.7,240.0 H378.0 Z"><title>mpdecimal 38: 33.7 significant digits</title></path>
|
||||
<text class="t2" x="517.7" y="238.0" font-size="11">33.7</text>
|
||||
<line class="base" x1="378" y1="78" x2="378" y2="246"/>
|
||||
<text class="t1" x="606" y="64" font-weight="600">Cost</text>
|
||||
<line class="grid" x1="606.0" y1="78" x2="606.0" y2="246"/>
|
||||
<text class="t2" x="606.0" y="262" text-anchor="middle" font-size="11">0</text>
|
||||
<line class="grid" x1="638.0" y1="78" x2="638.0" y2="246"/>
|
||||
<text class="t2" x="638.0" y="262" text-anchor="middle" font-size="11">200</text>
|
||||
<line class="grid" x1="670.0" y1="78" x2="670.0" y2="246"/>
|
||||
<text class="t2" x="670.0" y="262" text-anchor="middle" font-size="11">400</text>
|
||||
<line class="grid" x1="702.0" y1="78" x2="702.0" y2="246"/>
|
||||
<text class="t2" x="702.0" y="262" text-anchor="middle" font-size="11">600</text>
|
||||
<line class="grid" x1="734.0" y1="78" x2="734.0" y2="246"/>
|
||||
<text class="t2" x="734.0" y="262" text-anchor="middle" font-size="11">800</text>
|
||||
<line class="grid" x1="766.0" y1="78" x2="766.0" y2="246"/>
|
||||
<text class="t2" x="766.0" y="262" text-anchor="middle" font-size="11">1000</text>
|
||||
<text class="t2" x="766.0" y="274" text-anchor="end" font-size="11">ns per call</text>
|
||||
<path class="f-number" d="M606.0,84.0 H725.2 Q729.2,84.0 729.2,88.0 V92.0 Q729.2,96.0 725.2,96.0 H606.0 Z"><title>Number (Large330): 770 ns per call</title></path>
|
||||
<text class="t2" x="734.2" y="94.0" font-size="11">770</text>
|
||||
<path class="f-ieee64" d="M606.0,108.0 H625.7 Q629.7,108.0 629.7,112.0 V116.0 Q629.7,120.0 625.7,120.0 H606.0 Z"><title>Boost decimal64: 148 ns per call</title></path>
|
||||
<text class="t2" x="634.7" y="118.0" font-size="11">148</text>
|
||||
<path class="f-ieee64" d="M606.0,132.0 H622.4 Q626.4,132.0 626.4,136.0 V140.0 Q626.4,144.0 622.4,144.0 H606.0 Z"><title>Intel BID64: 128 ns per call</title></path>
|
||||
<text class="t2" x="631.4" y="142.0" font-size="11">128</text>
|
||||
<path class="f-wide" d="M606.0,156.0 H705.3 Q709.3,156.0 709.3,160.0 V164.0 Q709.3,168.0 705.3,168.0 H606.0 Z"><title>Boost decimal128: 646 ns per call</title></path>
|
||||
<text class="t2" x="714.3" y="166.0" font-size="11">646</text>
|
||||
<path class="f-wide" d="M606.0,180.0 H644.6 Q648.6,180.0 648.6,184.0 V188.0 Q648.6,192.0 644.6,192.0 H606.0 Z"><title>Intel BID128: 266 ns per call</title></path>
|
||||
<text class="t2" x="653.6" y="190.0" font-size="11">266</text>
|
||||
<path class="f-wide" d="M606.0,204.0 H664.4 Q668.4,204.0 668.4,208.0 V212.0 Q668.4,216.0 664.4,216.0 H606.0 Z"><title>mpdecimal 34: 390 ns per call</title></path>
|
||||
<text class="t2" x="673.4" y="214.0" font-size="11">390</text>
|
||||
<path class="f-wide" d="M606.0,228.0 H666.7 Q670.7,228.0 670.7,232.0 V236.0 Q670.7,240.0 666.7,240.0 H606.0 Z"><title>mpdecimal 38: 404 ns per call</title></path>
|
||||
<text class="t2" x="675.7" y="238.0" font-size="11">404</text>
|
||||
<line class="base" x1="606" y1="78" x2="606" y2="246"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.2 KiB |
117
docs/images/number/number-core-ops.svg
Normal file
117
docs/images/number/number-core-ops.svg
Normal file
@@ -0,0 +1,117 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 834 324" width="834" height="324" role="img" aria-label="Cost of one operation (full-width operands, lower is faster)">
|
||||
<style>
|
||||
text{font-family:system-ui, -apple-system, sans-serif;font-size:12px}
|
||||
.bg{fill:#fcfcfb}.t1{fill:#0b0b0b}.t2{fill:#52514e}.grid{stroke:#e4e3df;stroke-width:1}.base{stroke:#b9b8b2;stroke-width:1}.f-number{fill:#2a78d6}.f-ieee64{fill:#eb6834}.f-wide{fill:#1baf7a}
|
||||
@media (prefers-color-scheme: dark){.bg{fill:#1a1a19}.t1{fill:#ffffff}.t2{fill:#c3c2b7}.grid{stroke:#383835}.base{stroke:#5e5d58}.f-number{fill:#3987e5}.f-ieee64{fill:#d95926}.f-wide{fill:#199e70}}
|
||||
</style>
|
||||
<rect class="bg" width="834" height="324"/>
|
||||
<text class="t1" x="0" y="18" font-size="15" font-weight="600">Cost of one operation (full-width operands, lower is faster)</text>
|
||||
<rect class="f-number" x="0.0" y="32" width="12" height="12" rx="3"/>
|
||||
<text class="t2" x="18.0" y="42">Number today (19 digits)</text>
|
||||
<rect class="f-ieee64" x="214.8" y="32" width="12" height="12" rx="3"/>
|
||||
<text class="t2" x="232.8" y="42">16-digit types</text>
|
||||
<rect class="f-wide" x="357.6" y="32" width="12" height="12" rx="3"/>
|
||||
<text class="t2" x="375.6" y="42">34- and 38-digit types</text>
|
||||
<text class="t1" x="0" y="94.0">Number (Large330)</text>
|
||||
<text class="t1" x="0" y="118.0">Boost decimal64</text>
|
||||
<text class="t1" x="0" y="142.0">Boost decimal_fast64</text>
|
||||
<text class="t1" x="0" y="166.0">Intel BID64</text>
|
||||
<text class="t1" x="0" y="190.0">Boost decimal128</text>
|
||||
<text class="t1" x="0" y="214.0">Boost decimal_fast128</text>
|
||||
<text class="t1" x="0" y="238.0">Intel BID128</text>
|
||||
<text class="t1" x="0" y="262.0">mpdecimal 34</text>
|
||||
<text class="t1" x="0" y="286.0">mpdecimal 38</text>
|
||||
<text class="t1" x="150" y="64" font-weight="600">Add</text>
|
||||
<line class="grid" x1="150.0" y1="78" x2="150.0" y2="294"/>
|
||||
<text class="t2" x="150.0" y="310" text-anchor="middle" font-size="11">0</text>
|
||||
<line class="grid" x1="190.0" y1="78" x2="190.0" y2="294"/>
|
||||
<text class="t2" x="190.0" y="310" text-anchor="middle" font-size="11">100</text>
|
||||
<line class="grid" x1="230.0" y1="78" x2="230.0" y2="294"/>
|
||||
<text class="t2" x="230.0" y="310" text-anchor="middle" font-size="11">200</text>
|
||||
<line class="grid" x1="270.0" y1="78" x2="270.0" y2="294"/>
|
||||
<text class="t2" x="270.0" y="310" text-anchor="middle" font-size="11">300</text>
|
||||
<line class="grid" x1="310.0" y1="78" x2="310.0" y2="294"/>
|
||||
<text class="t2" x="310.0" y="310" text-anchor="middle" font-size="11">400</text>
|
||||
<text class="t2" x="310.0" y="322" text-anchor="end" font-size="11">ns per operation</text>
|
||||
<path class="f-number" d="M150.0,84.0 H180.9 Q184.9,84.0 184.9,88.0 V92.0 Q184.9,96.0 180.9,96.0 H150.0 Z"><title>Number (Large330): 87 ns per operation</title></path>
|
||||
<text class="t2" x="189.9" y="94.0" font-size="11">87</text>
|
||||
<path class="f-ieee64" d="M150.0,108.0 H152.3 Q156.3,108.0 156.3,112.0 V116.0 Q156.3,120.0 152.3,120.0 H150.0 Z"><title>Boost decimal64: 16 ns per operation</title></path>
|
||||
<text class="t2" x="161.3" y="118.0" font-size="11">16</text>
|
||||
<path class="f-ieee64" d="M150.0,132.0 H154.2 Q158.2,132.0 158.2,136.0 V140.0 Q158.2,144.0 154.2,144.0 H150.0 Z"><title>Boost decimal_fast64: 21 ns per operation</title></path>
|
||||
<text class="t2" x="163.2" y="142.0" font-size="11">21</text>
|
||||
<path class="f-ieee64" d="M150.0,156.0 H150.6 Q154.6,156.0 154.6,160.0 V164.0 Q154.6,168.0 150.6,168.0 H150.0 Z"><title>Intel BID64: 11 ns per operation</title></path>
|
||||
<text class="t2" x="159.6" y="166.0" font-size="11">11</text>
|
||||
<path class="f-wide" d="M150.0,180.0 H190.4 Q194.4,180.0 194.4,184.0 V188.0 Q194.4,192.0 190.4,192.0 H150.0 Z"><title>Boost decimal128: 111 ns per operation</title></path>
|
||||
<text class="t2" x="199.4" y="190.0" font-size="11">111</text>
|
||||
<path class="f-wide" d="M150.0,204.0 H187.2 Q191.2,204.0 191.2,208.0 V212.0 Q191.2,216.0 187.2,216.0 H150.0 Z"><title>Boost decimal_fast128: 103 ns per operation</title></path>
|
||||
<text class="t2" x="196.2" y="214.0" font-size="11">103</text>
|
||||
<path class="f-wide" d="M150.0,228.0 H150.7 Q154.7,228.0 154.7,232.0 V236.0 Q154.7,240.0 150.7,240.0 H150.0 Z"><title>Intel BID128: 12 ns per operation</title></path>
|
||||
<text class="t2" x="159.7" y="238.0" font-size="11">12</text>
|
||||
<path class="f-wide" d="M150.0,252.0 H156.2 Q160.2,252.0 160.2,256.0 V260.0 Q160.2,264.0 156.2,264.0 H150.0 Z"><title>mpdecimal 34: 26 ns per operation</title></path>
|
||||
<text class="t2" x="165.2" y="262.0" font-size="11">26</text>
|
||||
<path class="f-wide" d="M150.0,276.0 H156.0 Q160.0,276.0 160.0,280.0 V284.0 Q160.0,288.0 156.0,288.0 H150.0 Z"><title>mpdecimal 38: 25 ns per operation</title></path>
|
||||
<text class="t2" x="165.0" y="286.0" font-size="11">25</text>
|
||||
<line class="base" x1="150" y1="78" x2="150" y2="294"/>
|
||||
<text class="t1" x="378" y="64" font-weight="600">Multiply</text>
|
||||
<line class="grid" x1="378.0" y1="78" x2="378.0" y2="294"/>
|
||||
<text class="t2" x="378.0" y="310" text-anchor="middle" font-size="11">0</text>
|
||||
<line class="grid" x1="418.0" y1="78" x2="418.0" y2="294"/>
|
||||
<text class="t2" x="418.0" y="310" text-anchor="middle" font-size="11">100</text>
|
||||
<line class="grid" x1="458.0" y1="78" x2="458.0" y2="294"/>
|
||||
<text class="t2" x="458.0" y="310" text-anchor="middle" font-size="11">200</text>
|
||||
<line class="grid" x1="498.0" y1="78" x2="498.0" y2="294"/>
|
||||
<text class="t2" x="498.0" y="310" text-anchor="middle" font-size="11">300</text>
|
||||
<line class="grid" x1="538.0" y1="78" x2="538.0" y2="294"/>
|
||||
<text class="t2" x="538.0" y="310" text-anchor="middle" font-size="11">400</text>
|
||||
<text class="t2" x="538.0" y="322" text-anchor="end" font-size="11">ns per operation</text>
|
||||
<path class="f-number" d="M378.0,84.0 H455.6 Q459.6,84.0 459.6,88.0 V92.0 Q459.6,96.0 455.6,96.0 H378.0 Z"><title>Number (Large330): 204 ns per operation</title></path>
|
||||
<text class="t2" x="464.6" y="94.0" font-size="11">204</text>
|
||||
<path class="f-ieee64" d="M378.0,108.0 H378.0 Q381.4,108.0 381.4,111.4 V116.6 Q381.4,120.0 378.0,120.0 H378.0 Z"><title>Boost decimal64: 8.5 ns per operation</title></path>
|
||||
<text class="t2" x="386.4" y="118.0" font-size="11">8.5</text>
|
||||
<path class="f-ieee64" d="M378.0,132.0 H380.6 Q384.6,132.0 384.6,136.0 V140.0 Q384.6,144.0 380.6,144.0 H378.0 Z"><title>Boost decimal_fast64: 17 ns per operation</title></path>
|
||||
<text class="t2" x="389.6" y="142.0" font-size="11">17</text>
|
||||
<path class="f-ieee64" d="M378.0,156.0 H381.3 Q385.3,156.0 385.3,160.0 V164.0 Q385.3,168.0 381.3,168.0 H378.0 Z"><title>Intel BID64: 18 ns per operation</title></path>
|
||||
<text class="t2" x="390.3" y="166.0" font-size="11">18</text>
|
||||
<path class="f-wide" d="M378.0,180.0 H403.1 Q407.1,180.0 407.1,184.0 V188.0 Q407.1,192.0 403.1,192.0 H378.0 Z"><title>Boost decimal128: 73 ns per operation</title></path>
|
||||
<text class="t2" x="412.1" y="190.0" font-size="11">73</text>
|
||||
<path class="f-wide" d="M378.0,204.0 H421.7 Q425.7,204.0 425.7,208.0 V212.0 Q425.7,216.0 421.7,216.0 H378.0 Z"><title>Boost decimal_fast128: 119 ns per operation</title></path>
|
||||
<text class="t2" x="430.7" y="214.0" font-size="11">119</text>
|
||||
<path class="f-wide" d="M378.0,228.0 H389.3 Q393.3,228.0 393.3,232.0 V236.0 Q393.3,240.0 389.3,240.0 H378.0 Z"><title>Intel BID128: 38 ns per operation</title></path>
|
||||
<text class="t2" x="398.3" y="238.0" font-size="11">38</text>
|
||||
<path class="f-wide" d="M378.0,252.0 H386.4 Q390.4,252.0 390.4,256.0 V260.0 Q390.4,264.0 386.4,264.0 H378.0 Z"><title>mpdecimal 34: 31 ns per operation</title></path>
|
||||
<text class="t2" x="395.4" y="262.0" font-size="11">31</text>
|
||||
<path class="f-wide" d="M378.0,276.0 H380.1 Q384.1,276.0 384.1,280.0 V284.0 Q384.1,288.0 380.1,288.0 H378.0 Z"><title>mpdecimal 38: 15 ns per operation</title></path>
|
||||
<text class="t2" x="389.1" y="286.0" font-size="11">15</text>
|
||||
<line class="base" x1="378" y1="78" x2="378" y2="294"/>
|
||||
<text class="t1" x="606" y="64" font-weight="600">Divide</text>
|
||||
<line class="grid" x1="606.0" y1="78" x2="606.0" y2="294"/>
|
||||
<text class="t2" x="606.0" y="310" text-anchor="middle" font-size="11">0</text>
|
||||
<line class="grid" x1="646.0" y1="78" x2="646.0" y2="294"/>
|
||||
<text class="t2" x="646.0" y="310" text-anchor="middle" font-size="11">100</text>
|
||||
<line class="grid" x1="686.0" y1="78" x2="686.0" y2="294"/>
|
||||
<text class="t2" x="686.0" y="310" text-anchor="middle" font-size="11">200</text>
|
||||
<line class="grid" x1="726.0" y1="78" x2="726.0" y2="294"/>
|
||||
<text class="t2" x="726.0" y="310" text-anchor="middle" font-size="11">300</text>
|
||||
<line class="grid" x1="766.0" y1="78" x2="766.0" y2="294"/>
|
||||
<text class="t2" x="766.0" y="310" text-anchor="middle" font-size="11">400</text>
|
||||
<text class="t2" x="766.0" y="322" text-anchor="end" font-size="11">ns per operation</text>
|
||||
<path class="f-number" d="M606.0,84.0 H627.7 Q631.7,84.0 631.7,88.0 V92.0 Q631.7,96.0 627.7,96.0 H606.0 Z"><title>Number (Large330): 64 ns per operation</title></path>
|
||||
<text class="t2" x="636.7" y="94.0" font-size="11">64</text>
|
||||
<path class="f-ieee64" d="M606.0,108.0 H615.7 Q619.7,108.0 619.7,112.0 V116.0 Q619.7,120.0 615.7,120.0 H606.0 Z"><title>Boost decimal64: 34 ns per operation</title></path>
|
||||
<text class="t2" x="624.7" y="118.0" font-size="11">34</text>
|
||||
<path class="f-ieee64" d="M606.0,132.0 H616.8 Q620.8,132.0 620.8,136.0 V140.0 Q620.8,144.0 616.8,144.0 H606.0 Z"><title>Boost decimal_fast64: 37 ns per operation</title></path>
|
||||
<text class="t2" x="625.8" y="142.0" font-size="11">37</text>
|
||||
<path class="f-ieee64" d="M606.0,156.0 H610.0 Q614.0,156.0 614.0,160.0 V164.0 Q614.0,168.0 610.0,168.0 H606.0 Z"><title>Intel BID64: 20 ns per operation</title></path>
|
||||
<text class="t2" x="619.0" y="166.0" font-size="11">20</text>
|
||||
<path class="f-wide" d="M606.0,180.0 H726.2 Q730.2,180.0 730.2,184.0 V188.0 Q730.2,192.0 726.2,192.0 H606.0 Z"><title>Boost decimal128: 310 ns per operation</title></path>
|
||||
<text class="t2" x="735.2" y="190.0" font-size="11">310</text>
|
||||
<path class="f-wide" d="M606.0,204.0 H648.7 Q652.7,204.0 652.7,208.0 V212.0 Q652.7,216.0 648.7,216.0 H606.0 Z"><title>Boost decimal_fast128: 117 ns per operation</title></path>
|
||||
<text class="t2" x="657.7" y="214.0" font-size="11">117</text>
|
||||
<path class="f-wide" d="M606.0,228.0 H627.4 Q631.4,228.0 631.4,232.0 V236.0 Q631.4,240.0 627.4,240.0 H606.0 Z"><title>Intel BID128: 63 ns per operation</title></path>
|
||||
<text class="t2" x="636.4" y="238.0" font-size="11">63</text>
|
||||
<path class="f-wide" d="M606.0,252.0 H623.2 Q627.2,252.0 627.2,256.0 V260.0 Q627.2,264.0 623.2,264.0 H606.0 Z"><title>mpdecimal 34: 53 ns per operation</title></path>
|
||||
<text class="t2" x="632.2" y="262.0" font-size="11">53</text>
|
||||
<path class="f-wide" d="M606.0,276.0 H624.5 Q628.5,276.0 628.5,280.0 V284.0 Q628.5,288.0 624.5,288.0 H606.0 Z"><title>mpdecimal 38: 56 ns per operation</title></path>
|
||||
<text class="t2" x="633.5" y="286.0" font-size="11">56</text>
|
||||
<line class="base" x1="606" y1="78" x2="606" y2="294"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
105
docs/images/number/number-loan-amortize360.svg
Normal file
105
docs/images/number/number-loan-amortize360.svg
Normal file
@@ -0,0 +1,105 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 834 276" width="834" height="276" role="img" aria-label="360-payment amortization: correct digits (higher is better) and cost (lower is faster)">
|
||||
<style>
|
||||
text{font-family:system-ui, -apple-system, sans-serif;font-size:12px}
|
||||
.bg{fill:#fcfcfb}.t1{fill:#0b0b0b}.t2{fill:#52514e}.grid{stroke:#e4e3df;stroke-width:1}.base{stroke:#b9b8b2;stroke-width:1}.f-number{fill:#2a78d6}.f-ieee64{fill:#eb6834}.f-wide{fill:#1baf7a}
|
||||
@media (prefers-color-scheme: dark){.bg{fill:#1a1a19}.t1{fill:#ffffff}.t2{fill:#c3c2b7}.grid{stroke:#383835}.base{stroke:#5e5d58}.f-number{fill:#3987e5}.f-ieee64{fill:#d95926}.f-wide{fill:#199e70}}
|
||||
</style>
|
||||
<rect class="bg" width="834" height="276"/>
|
||||
<text class="t1" x="0" y="18" font-size="15" font-weight="600">360-payment amortization: correct digits (higher is better) and cost (lower is faster)</text>
|
||||
<rect class="f-number" x="0.0" y="32" width="12" height="12" rx="3"/>
|
||||
<text class="t2" x="18.0" y="42">Number today (19 digits)</text>
|
||||
<rect class="f-ieee64" x="214.8" y="32" width="12" height="12" rx="3"/>
|
||||
<text class="t2" x="232.8" y="42">16-digit types</text>
|
||||
<rect class="f-wide" x="357.6" y="32" width="12" height="12" rx="3"/>
|
||||
<text class="t2" x="375.6" y="42">34- and 38-digit types</text>
|
||||
<text class="t1" x="0" y="94.0">Number (Large330)</text>
|
||||
<text class="t1" x="0" y="118.0">Boost decimal64</text>
|
||||
<text class="t1" x="0" y="142.0">Intel BID64</text>
|
||||
<text class="t1" x="0" y="166.0">Boost decimal128</text>
|
||||
<text class="t1" x="0" y="190.0">Intel BID128</text>
|
||||
<text class="t1" x="0" y="214.0">mpdecimal 34</text>
|
||||
<text class="t1" x="0" y="238.0">mpdecimal 38</text>
|
||||
<text class="t1" x="150" y="64" font-weight="600">Correct digits, worst case</text>
|
||||
<line class="grid" x1="150.0" y1="78" x2="150.0" y2="246"/>
|
||||
<text class="t2" x="150.0" y="262" text-anchor="middle" font-size="11">0</text>
|
||||
<line class="grid" x1="190.0" y1="78" x2="190.0" y2="246"/>
|
||||
<text class="t2" x="190.0" y="262" text-anchor="middle" font-size="11">10</text>
|
||||
<line class="grid" x1="230.0" y1="78" x2="230.0" y2="246"/>
|
||||
<text class="t2" x="230.0" y="262" text-anchor="middle" font-size="11">20</text>
|
||||
<line class="grid" x1="270.0" y1="78" x2="270.0" y2="246"/>
|
||||
<text class="t2" x="270.0" y="262" text-anchor="middle" font-size="11">30</text>
|
||||
<line class="grid" x1="310.0" y1="78" x2="310.0" y2="246"/>
|
||||
<text class="t2" x="310.0" y="262" text-anchor="middle" font-size="11">40</text>
|
||||
<text class="t2" x="310.0" y="274" text-anchor="end" font-size="11">significant digits</text>
|
||||
<path class="f-number" d="M150.0,84.0 H202.3 Q206.3,84.0 206.3,88.0 V92.0 Q206.3,96.0 202.3,96.0 H150.0 Z"><title>Number (Large330): 14.1 significant digits</title></path>
|
||||
<text class="t2" x="211.3" y="94.0" font-size="11">14.1</text>
|
||||
<path class="f-ieee64" d="M150.0,108.0 H190.8 Q194.8,108.0 194.8,112.0 V116.0 Q194.8,120.0 190.8,120.0 H150.0 Z"><title>Boost decimal64: 11.2 significant digits</title></path>
|
||||
<text class="t2" x="199.8" y="118.0" font-size="11">11.2</text>
|
||||
<path class="f-ieee64" d="M150.0,132.0 H190.8 Q194.8,132.0 194.8,136.0 V140.0 Q194.8,144.0 190.8,144.0 H150.0 Z"><title>Intel BID64: 11.2 significant digits</title></path>
|
||||
<text class="t2" x="199.8" y="142.0" font-size="11">11.2</text>
|
||||
<path class="f-wide" d="M150.0,156.0 H262.5 Q266.5,156.0 266.5,160.0 V164.0 Q266.5,168.0 262.5,168.0 H150.0 Z"><title>Boost decimal128: 29.1 significant digits</title></path>
|
||||
<text class="t2" x="271.5" y="166.0" font-size="11">29.1</text>
|
||||
<path class="f-wide" d="M150.0,180.0 H262.5 Q266.5,180.0 266.5,184.0 V188.0 Q266.5,192.0 262.5,192.0 H150.0 Z"><title>Intel BID128: 29.1 significant digits</title></path>
|
||||
<text class="t2" x="271.5" y="190.0" font-size="11">29.1</text>
|
||||
<path class="f-wide" d="M150.0,204.0 H262.6 Q266.6,204.0 266.6,208.0 V212.0 Q266.6,216.0 262.6,216.0 H150.0 Z"><title>mpdecimal 34: 29.1 significant digits</title></path>
|
||||
<text class="t2" x="271.6" y="214.0" font-size="11">29.1</text>
|
||||
<path class="f-wide" d="M150.0,228.0 H277.9 Q281.9,228.0 281.9,232.0 V236.0 Q281.9,240.0 277.9,240.0 H150.0 Z"><title>mpdecimal 38: 33.0 significant digits</title></path>
|
||||
<text class="t2" x="286.9" y="238.0" font-size="11">33.0</text>
|
||||
<line class="base" x1="150" y1="78" x2="150" y2="246"/>
|
||||
<text class="t1" x="378" y="64" font-weight="600">Correct digits, median</text>
|
||||
<line class="grid" x1="378.0" y1="78" x2="378.0" y2="246"/>
|
||||
<text class="t2" x="378.0" y="262" text-anchor="middle" font-size="11">0</text>
|
||||
<line class="grid" x1="418.0" y1="78" x2="418.0" y2="246"/>
|
||||
<text class="t2" x="418.0" y="262" text-anchor="middle" font-size="11">10</text>
|
||||
<line class="grid" x1="458.0" y1="78" x2="458.0" y2="246"/>
|
||||
<text class="t2" x="458.0" y="262" text-anchor="middle" font-size="11">20</text>
|
||||
<line class="grid" x1="498.0" y1="78" x2="498.0" y2="246"/>
|
||||
<text class="t2" x="498.0" y="262" text-anchor="middle" font-size="11">30</text>
|
||||
<line class="grid" x1="538.0" y1="78" x2="538.0" y2="246"/>
|
||||
<text class="t2" x="538.0" y="262" text-anchor="middle" font-size="11">40</text>
|
||||
<text class="t2" x="538.0" y="274" text-anchor="end" font-size="11">significant digits</text>
|
||||
<path class="f-number" d="M378.0,84.0 H437.1 Q441.1,84.0 441.1,88.0 V92.0 Q441.1,96.0 437.1,96.0 H378.0 Z"><title>Number (Large330): 15.8 significant digits</title></path>
|
||||
<text class="t2" x="446.1" y="94.0" font-size="11">15.8</text>
|
||||
<path class="f-ieee64" d="M378.0,108.0 H425.4 Q429.4,108.0 429.4,112.0 V116.0 Q429.4,120.0 425.4,120.0 H378.0 Z"><title>Boost decimal64: 12.8 significant digits</title></path>
|
||||
<text class="t2" x="434.4" y="118.0" font-size="11">12.8</text>
|
||||
<path class="f-ieee64" d="M378.0,132.0 H425.4 Q429.4,132.0 429.4,136.0 V140.0 Q429.4,144.0 425.4,144.0 H378.0 Z"><title>Intel BID64: 12.8 significant digits</title></path>
|
||||
<text class="t2" x="434.4" y="142.0" font-size="11">12.8</text>
|
||||
<path class="f-wide" d="M378.0,156.0 H497.2 Q501.2,156.0 501.2,160.0 V164.0 Q501.2,168.0 497.2,168.0 H378.0 Z"><title>Boost decimal128: 30.8 significant digits</title></path>
|
||||
<text class="t2" x="506.2" y="166.0" font-size="11">30.8</text>
|
||||
<path class="f-wide" d="M378.0,180.0 H497.2 Q501.2,180.0 501.2,184.0 V188.0 Q501.2,192.0 497.2,192.0 H378.0 Z"><title>Intel BID128: 30.8 significant digits</title></path>
|
||||
<text class="t2" x="506.2" y="190.0" font-size="11">30.8</text>
|
||||
<path class="f-wide" d="M378.0,204.0 H497.2 Q501.2,204.0 501.2,208.0 V212.0 Q501.2,216.0 497.2,216.0 H378.0 Z"><title>mpdecimal 34: 30.8 significant digits</title></path>
|
||||
<text class="t2" x="506.2" y="214.0" font-size="11">30.8</text>
|
||||
<path class="f-wide" d="M378.0,228.0 H513.3 Q517.3,228.0 517.3,232.0 V236.0 Q517.3,240.0 513.3,240.0 H378.0 Z"><title>mpdecimal 38: 34.8 significant digits</title></path>
|
||||
<text class="t2" x="522.3" y="238.0" font-size="11">34.8</text>
|
||||
<line class="base" x1="378" y1="78" x2="378" y2="246"/>
|
||||
<text class="t1" x="606" y="64" font-weight="600">Cost</text>
|
||||
<line class="grid" x1="606.0" y1="78" x2="606.0" y2="246"/>
|
||||
<text class="t2" x="606.0" y="262" text-anchor="middle" font-size="11">0</text>
|
||||
<line class="grid" x1="638.0" y1="78" x2="638.0" y2="246"/>
|
||||
<text class="t2" x="638.0" y="262" text-anchor="middle" font-size="11">25</text>
|
||||
<line class="grid" x1="670.0" y1="78" x2="670.0" y2="246"/>
|
||||
<text class="t2" x="670.0" y="262" text-anchor="middle" font-size="11">50</text>
|
||||
<line class="grid" x1="702.0" y1="78" x2="702.0" y2="246"/>
|
||||
<text class="t2" x="702.0" y="262" text-anchor="middle" font-size="11">75</text>
|
||||
<line class="grid" x1="734.0" y1="78" x2="734.0" y2="246"/>
|
||||
<text class="t2" x="734.0" y="262" text-anchor="middle" font-size="11">100</text>
|
||||
<line class="grid" x1="766.0" y1="78" x2="766.0" y2="246"/>
|
||||
<text class="t2" x="766.0" y="262" text-anchor="middle" font-size="11">125</text>
|
||||
<text class="t2" x="766.0" y="274" text-anchor="end" font-size="11">µs per schedule</text>
|
||||
<path class="f-number" d="M606.0,84.0 H748.1 Q752.1,84.0 752.1,88.0 V92.0 Q752.1,96.0 748.1,96.0 H606.0 Z"><title>Number (Large330): 114 µs per schedule</title></path>
|
||||
<text class="t2" x="757.1" y="94.0" font-size="11">114</text>
|
||||
<path class="f-ieee64" d="M606.0,108.0 H627.0 Q631.0,108.0 631.0,112.0 V116.0 Q631.0,120.0 627.0,120.0 H606.0 Z"><title>Boost decimal64: 20 µs per schedule</title></path>
|
||||
<text class="t2" x="636.0" y="118.0" font-size="11">20</text>
|
||||
<path class="f-ieee64" d="M606.0,132.0 H625.5 Q629.5,132.0 629.5,136.0 V140.0 Q629.5,144.0 625.5,144.0 H606.0 Z"><title>Intel BID64: 18 µs per schedule</title></path>
|
||||
<text class="t2" x="634.5" y="142.0" font-size="11">18</text>
|
||||
<path class="f-wide" d="M606.0,156.0 H726.1 Q730.1,156.0 730.1,160.0 V164.0 Q730.1,168.0 726.1,168.0 H606.0 Z"><title>Boost decimal128: 97 µs per schedule</title></path>
|
||||
<text class="t2" x="735.1" y="166.0" font-size="11">97</text>
|
||||
<path class="f-wide" d="M606.0,180.0 H676.9 Q680.9,180.0 680.9,184.0 V188.0 Q680.9,192.0 676.9,192.0 H606.0 Z"><title>Intel BID128: 58 µs per schedule</title></path>
|
||||
<text class="t2" x="685.9" y="190.0" font-size="11">58</text>
|
||||
<path class="f-wide" d="M606.0,204.0 H672.7 Q676.7,204.0 676.7,208.0 V212.0 Q676.7,216.0 672.7,216.0 H606.0 Z"><title>mpdecimal 34: 55 µs per schedule</title></path>
|
||||
<text class="t2" x="681.7" y="214.0" font-size="11">55</text>
|
||||
<path class="f-wide" d="M606.0,228.0 H659.8 Q663.8,228.0 663.8,232.0 V236.0 Q663.8,240.0 659.8,240.0 H606.0 Z"><title>mpdecimal 38: 45 µs per schedule</title></path>
|
||||
<text class="t2" x="668.8" y="238.0" font-size="11">45</text>
|
||||
<line class="base" x1="606" y1="78" x2="606" y2="246"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.3 KiB |
12
flake.lock
generated
12
flake.lock
generated
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1791130267,
|
||||
"narHash": "sha256-1sjwcQMcgAbmiip/VivBSiK8p2bTkQGOvCuO7vnwfx4=",
|
||||
"lastModified": 1781173989,
|
||||
"narHash": "sha256-fnzKKPvS+oieI/pTzotA5tkoM47EB1NpaBcgk4R97hE=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9013764fcc0ea99fa16cf7aa7decf8a5c3889dd2",
|
||||
"rev": "8c91a71d13451abc40eb9dae8910f972f979852f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -47,11 +47,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1791189933,
|
||||
"narHash": "sha256-Y8eN7ki0Urx4Ojf4w1xSpWg8uk/3lJqK+E0Oth77rH0=",
|
||||
"lastModified": 1784611586,
|
||||
"narHash": "sha256-OfqgY+0hp/zseZB7uyH0U8kIDPS4scZZCyAurEplvG0=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "e60029353d0c48d216bc4b065168ccd4079c166f",
|
||||
"rev": "14f58845249f3552a89b07772626b8d3c632fa86",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -1,31 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/beast/type_name.h>
|
||||
|
||||
#include <boost/core/type_name.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* Manages all counted object types.
|
||||
*
|
||||
* Counters register themselves on a lock-free intrusive list maintained
|
||||
* by this object when constructed. Because counters are never destroyed
|
||||
* or removed, the ABA problem does not apply.
|
||||
*
|
||||
* The registry is iterable as a forward range.
|
||||
*/
|
||||
class CountedObjects
|
||||
{
|
||||
public:
|
||||
static CountedObjects&
|
||||
getInstance() noexcept;
|
||||
|
||||
using Entry = std::pair<std::string, int>;
|
||||
using List = std::vector<Entry>;
|
||||
|
||||
[[nodiscard]] List
|
||||
getCounts(int minimumThreshold) const;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Implementation for @ref CountedObject.
|
||||
@@ -35,147 +33,68 @@ public:
|
||||
class Counter
|
||||
{
|
||||
public:
|
||||
Counter(std::string name) noexcept;
|
||||
Counter(std::string name) noexcept : name_(std::move(name)), count_(0)
|
||||
{
|
||||
// Insert ourselves at the front of the lock-free linked list
|
||||
CountedObjects& instance = CountedObjects::getInstance();
|
||||
Counter* head = nullptr;
|
||||
|
||||
// Counters are intrusive list nodes whose addresses are published
|
||||
// in the registry; they must never be copied or moved. The atomic
|
||||
// members already force this, but we make it explicit.
|
||||
Counter(Counter const&) = delete;
|
||||
Counter&
|
||||
operator=(Counter const&) = delete;
|
||||
Counter(Counter&&) = delete;
|
||||
Counter&
|
||||
operator=(Counter&&) = delete;
|
||||
do
|
||||
{
|
||||
head = instance.head_.load();
|
||||
next_ = head;
|
||||
} while (instance.head_.exchange(this) != head);
|
||||
|
||||
std::uint32_t
|
||||
++instance.count_;
|
||||
}
|
||||
|
||||
~Counter() noexcept = default;
|
||||
|
||||
int
|
||||
increment() noexcept
|
||||
{
|
||||
auto const newCount = count_.fetch_add(1, std::memory_order::relaxed) + 1;
|
||||
XRPL_ASSERT(newCount != 0, "xrpl::CountedObjects::Counter::increment : no overflow");
|
||||
|
||||
auto maxCount = maxCount_.load(std::memory_order::relaxed);
|
||||
|
||||
while (newCount > maxCount &&
|
||||
!maxCount_.compare_exchange_weak(maxCount, newCount, std::memory_order::relaxed))
|
||||
{
|
||||
}
|
||||
|
||||
return newCount;
|
||||
return ++count_;
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
int
|
||||
decrement() noexcept
|
||||
{
|
||||
auto const prev = count_.fetch_sub(1, std::memory_order::relaxed);
|
||||
XRPL_ASSERT(prev != 0, "xrpl::CountedObjects::Counter::decrement : no underflow");
|
||||
return prev - 1;
|
||||
return --count_;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::uint32_t
|
||||
count() const noexcept
|
||||
[[nodiscard]] int
|
||||
getCount() const noexcept
|
||||
{
|
||||
return count_.load(std::memory_order::relaxed);
|
||||
return count_.load();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::uint32_t
|
||||
max() const noexcept
|
||||
[[nodiscard]] Counter*
|
||||
getNext() const noexcept
|
||||
{
|
||||
return std::max(
|
||||
count_.load(std::memory_order::relaxed),
|
||||
maxCount_.load(std::memory_order::relaxed));
|
||||
return next_;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string const&
|
||||
name() const noexcept
|
||||
getName() const noexcept
|
||||
{
|
||||
return name_;
|
||||
}
|
||||
|
||||
private:
|
||||
friend class CountedObjects;
|
||||
|
||||
Counter* next_ = nullptr;
|
||||
std::atomic<std::uint32_t> count_ = 0;
|
||||
std::atomic<std::uint32_t> maxCount_ = 0;
|
||||
std::string const name_;
|
||||
std::atomic<int> count_;
|
||||
Counter* next_;
|
||||
};
|
||||
|
||||
class Iterator
|
||||
{
|
||||
public:
|
||||
using value_type = Counter const;
|
||||
using reference = value_type&;
|
||||
using pointer = value_type*;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
using iterator_category = std::forward_iterator_tag;
|
||||
|
||||
explicit Iterator(Counter* c = nullptr) noexcept : current_(c)
|
||||
{
|
||||
}
|
||||
|
||||
reference
|
||||
operator*() const noexcept
|
||||
{
|
||||
return *current_;
|
||||
}
|
||||
|
||||
pointer
|
||||
operator->() const noexcept
|
||||
{
|
||||
return current_;
|
||||
}
|
||||
|
||||
Iterator&
|
||||
operator++() noexcept
|
||||
{
|
||||
current_ = current_->next_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Iterator
|
||||
operator++(int) noexcept
|
||||
{
|
||||
auto tmp = *this;
|
||||
++*this;
|
||||
return tmp;
|
||||
}
|
||||
|
||||
bool
|
||||
operator==(Iterator const&) const noexcept = default;
|
||||
|
||||
private:
|
||||
Counter* current_;
|
||||
};
|
||||
|
||||
constexpr CountedObjects() noexcept = default;
|
||||
|
||||
[[nodiscard]] auto
|
||||
begin() const noexcept
|
||||
{
|
||||
return Iterator{head_.load(std::memory_order::acquire)};
|
||||
}
|
||||
|
||||
[[nodiscard]] auto
|
||||
end() const noexcept
|
||||
{
|
||||
return Iterator{};
|
||||
}
|
||||
|
||||
private:
|
||||
std::atomic<Counter*> head_ = nullptr;
|
||||
CountedObjects() noexcept;
|
||||
~CountedObjects() noexcept = default;
|
||||
|
||||
private:
|
||||
std::atomic<int> count_;
|
||||
std::atomic<Counter*> head_;
|
||||
};
|
||||
|
||||
/** The global counted object registry. */
|
||||
inline constinit CountedObjects gCountedObjects;
|
||||
|
||||
inline CountedObjects::Counter::Counter(std::string name) noexcept
|
||||
: next_(gCountedObjects.head_.load(std::memory_order::relaxed)), name_(std::move(name))
|
||||
{
|
||||
while (!gCountedObjects.head_.compare_exchange_weak(
|
||||
next_, this, std::memory_order::release, std::memory_order::relaxed))
|
||||
;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -184,37 +103,27 @@ inline CountedObjects::Counter::Counter(std::string name) noexcept
|
||||
* Derived classes have their instances counted automatically. This is used
|
||||
* for reporting purposes.
|
||||
*
|
||||
* The constructors are private and `Object` is befriended so that the
|
||||
* CRTP parameter must be the deriving class itself: a copy-paste error
|
||||
* like `class B : public CountedObject<A>` fails to compile instead of
|
||||
* silently polluting A's count.
|
||||
*
|
||||
* @note This class has no move operations by design: a derived class's
|
||||
* move constructor falls back to the copy constructor for this
|
||||
* base, so the newly created instance is counted. This keeps the
|
||||
* invariant that count is the number of outstanding subobjects.
|
||||
*
|
||||
* @warning Counted objects constructed during dynamic initialization of
|
||||
* other translation units may have their increments discarded when
|
||||
* counter itself is dynamically initialized. Do not create counted
|
||||
* objects before main() begins.
|
||||
*
|
||||
* @ingroup basics
|
||||
*/
|
||||
template <class Object>
|
||||
requires std::is_class_v<Object>
|
||||
class CountedObject
|
||||
{
|
||||
static inline CountedObjects::Counter counter{boost::core::type_name<Object>()};
|
||||
private:
|
||||
static auto&
|
||||
getCounter() noexcept
|
||||
{
|
||||
static CountedObjects::Counter kC{beast::typeName<Object>()};
|
||||
return kC;
|
||||
}
|
||||
|
||||
CountedObject() noexcept
|
||||
{
|
||||
counter.increment();
|
||||
getCounter().increment();
|
||||
}
|
||||
|
||||
CountedObject(CountedObject const&) noexcept
|
||||
{
|
||||
counter.increment();
|
||||
getCounter().increment();
|
||||
}
|
||||
|
||||
CountedObject&
|
||||
@@ -223,7 +132,7 @@ class CountedObject
|
||||
public:
|
||||
~CountedObject() noexcept
|
||||
{
|
||||
counter.decrement();
|
||||
getCounter().decrement();
|
||||
}
|
||||
|
||||
friend Object;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/ByteUtilities.h>
|
||||
#include <xrpl/beast/type_name.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
|
||||
#include <boost/align.hpp>
|
||||
#include <boost/container/static_vector.hpp>
|
||||
#include <boost/core/type_name.hpp>
|
||||
#include <boost/predef.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -16,7 +16,6 @@
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <typeinfo>
|
||||
#include <vector>
|
||||
|
||||
#if BOOST_OS_LINUX
|
||||
@@ -336,7 +335,7 @@ public:
|
||||
}) != cfg.end())
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"SlabAllocatorSet<" + boost::core::type_name<Type>() + ">: duplicate slab size");
|
||||
"SlabAllocatorSet<" + beast::typeName<Type>() + ">: duplicate slab size");
|
||||
}
|
||||
|
||||
for (auto const& c : cfg)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/sanitizers.h>
|
||||
|
||||
#include <boost/core/type_name.hpp>
|
||||
#include <xrpl/beast/type_name.h>
|
||||
|
||||
#include <exception>
|
||||
#include <string>
|
||||
@@ -56,7 +55,7 @@ Throw(Args&&... args)
|
||||
std::is_convertible_v<E*, std::exception*>, "Exception must derive from std::exception.");
|
||||
|
||||
E e(std::forward<Args>(args)...);
|
||||
logThrow("Throwing exception of type " + boost::core::type_name<E>() + ": " + e.what());
|
||||
logThrow(std::string("Throwing exception of type " + beast::typeName<E>() + ": ") + e.what());
|
||||
throw std::move(e);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <concepts>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* Opt-in bitwise operators for scoped enumerations.
|
||||
*
|
||||
* Provides `&`, `|`, `^`, `~` and the corresponding compound assignment forms
|
||||
* for any scoped enumeration with an unsigned underlying type that opts in by
|
||||
* specializing @ref OptIn:
|
||||
*
|
||||
* @code
|
||||
* namespace xrpl {
|
||||
*
|
||||
* enum class MyFlags : std::uint32_t { a = 1, b = 2, c = 4 };
|
||||
*
|
||||
* template <>
|
||||
* struct enum_bitops::OptIn<MyFlags> : std::true_type {};
|
||||
*
|
||||
* } // namespace xrpl
|
||||
* @endcode
|
||||
*
|
||||
* The operators are constexpr and noexcept, and return the enumeration type.
|
||||
*
|
||||
* @par Where the specialization may be declared
|
||||
* In namespace xrpl, or at global scope with full qualification, and after
|
||||
* the enumeration is defined but before the operators are first used. This
|
||||
* cannot be done in a nested namespace or at class scope; for enumerations
|
||||
* nested in a class this means after the class definition.
|
||||
*
|
||||
* @par Where the operators are found
|
||||
* The operators are declared in namespace xrpl, so argument-dependent lookup
|
||||
* will find them only for enumerations declared in xrpl or nested in a class
|
||||
* declared directly in xrpl. Enumerations in a nested namespace will only be
|
||||
* found from code inside xrpl, and only if no enclosing scope declares an
|
||||
* operator of the same name; elsewhere they require using-declarations.
|
||||
*/
|
||||
namespace enum_bitops {
|
||||
|
||||
/**
|
||||
* Types that may be opted in to the bitwise operators.
|
||||
*
|
||||
* Satisfied by a scoped enumeration whose underlying type is an unsigned
|
||||
* integer type.
|
||||
*
|
||||
* @tparam T The type to test.
|
||||
*/
|
||||
template <typename T>
|
||||
concept Eligible = std::is_scoped_enum_v<T> && std::unsigned_integral<std::underlying_type_t<T>>;
|
||||
|
||||
/**
|
||||
* Opt-in switch for the bitwise operators.
|
||||
*
|
||||
* The primary template derives from std::false_type. Specialize it to derive
|
||||
* from std::true_type to enable the operators for an @ref Eligible enumeration.
|
||||
*
|
||||
* @tparam T The scoped enumeration to opt in.
|
||||
*/
|
||||
template <Eligible T>
|
||||
struct OptIn : std::false_type
|
||||
{
|
||||
};
|
||||
|
||||
/**
|
||||
* Enumerations for which the bitwise operators are enabled.
|
||||
*
|
||||
* Satisfied when @p T satisfies @ref Eligible and @ref OptIn has been
|
||||
* specialized for it to derive from std::true_type.
|
||||
*
|
||||
* @tparam T The type to test.
|
||||
*/
|
||||
template <typename T>
|
||||
concept Candidate = Eligible<T> && OptIn<T>::value;
|
||||
|
||||
} // namespace enum_bitops
|
||||
|
||||
// These are declared in xrpl, not in enum_bitops, so that argument-dependent
|
||||
// lookup finds them for enumerations whose associated namespace is xrpl.
|
||||
|
||||
/**
|
||||
* @name Bitwise operators for opted-in scoped enumerations
|
||||
*
|
||||
* Each operator applies the corresponding built-in operator to the
|
||||
* underlying values and converts the result back to the enumeration type.
|
||||
* Both operands must have the same enumeration type; there is no implicit
|
||||
* conversion to or from the underlying type.
|
||||
*/
|
||||
/** @{ */
|
||||
|
||||
/**
|
||||
* Bitwise AND.
|
||||
*
|
||||
* @param lhs The left operand.
|
||||
* @param rhs The right operand.
|
||||
* @return The bits set in both @p lhs and @p rhs.
|
||||
*/
|
||||
template <enum_bitops::Candidate T>
|
||||
constexpr T
|
||||
operator&(T lhs, T rhs) noexcept
|
||||
{
|
||||
return static_cast<T>(std::to_underlying(lhs) & std::to_underlying(rhs));
|
||||
}
|
||||
|
||||
/**
|
||||
* Bitwise OR.
|
||||
*
|
||||
* @param lhs The left operand.
|
||||
* @param rhs The right operand.
|
||||
* @return The bits set in @p lhs, in @p rhs, or in both.
|
||||
*/
|
||||
template <enum_bitops::Candidate T>
|
||||
constexpr T
|
||||
operator|(T lhs, T rhs) noexcept
|
||||
{
|
||||
return static_cast<T>(std::to_underlying(lhs) | std::to_underlying(rhs));
|
||||
}
|
||||
|
||||
/**
|
||||
* Bitwise exclusive OR.
|
||||
*
|
||||
* @param lhs The left operand.
|
||||
* @param rhs The right operand.
|
||||
* @return The bits set in exactly one of @p lhs and @p rhs.
|
||||
*/
|
||||
template <enum_bitops::Candidate T>
|
||||
constexpr T
|
||||
operator^(T lhs, T rhs) noexcept
|
||||
{
|
||||
return static_cast<T>(std::to_underlying(lhs) ^ std::to_underlying(rhs));
|
||||
}
|
||||
|
||||
/**
|
||||
* Bitwise complement.
|
||||
*
|
||||
* The result has every bit of the underlying type that is clear in
|
||||
* @p val, including bits that no enumerator names. It is intended for
|
||||
* clearing flags, as in `flags & ~flag`.
|
||||
*
|
||||
* @param val The operand.
|
||||
* @return The complement of @p val.
|
||||
*/
|
||||
template <enum_bitops::Candidate T>
|
||||
constexpr T
|
||||
operator~(T val) noexcept
|
||||
{
|
||||
return static_cast<T>(~std::to_underlying(val));
|
||||
}
|
||||
|
||||
/**
|
||||
* Bitwise AND assignment.
|
||||
*
|
||||
* @param lhs The value to modify.
|
||||
* @param rhs The right operand.
|
||||
* @return A reference to @p lhs.
|
||||
*/
|
||||
template <enum_bitops::Candidate T>
|
||||
constexpr T&
|
||||
operator&=(T& lhs, T rhs) noexcept
|
||||
{
|
||||
lhs = lhs & rhs;
|
||||
return lhs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bitwise OR assignment.
|
||||
*
|
||||
* @param lhs The value to modify.
|
||||
* @param rhs The right operand.
|
||||
* @return A reference to @p lhs.
|
||||
*/
|
||||
template <enum_bitops::Candidate T>
|
||||
constexpr T&
|
||||
operator|=(T& lhs, T rhs) noexcept
|
||||
{
|
||||
lhs = lhs | rhs;
|
||||
return lhs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bitwise exclusive OR assignment.
|
||||
*
|
||||
* @param lhs The value to modify.
|
||||
* @param rhs The right operand.
|
||||
* @return A reference to @p lhs.
|
||||
*/
|
||||
template <enum_bitops::Candidate T>
|
||||
constexpr T&
|
||||
operator^=(T& lhs, T rhs) noexcept
|
||||
{
|
||||
lhs = lhs ^ rhs;
|
||||
return lhs;
|
||||
}
|
||||
|
||||
/** @} */
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -2,212 +2,107 @@
|
||||
|
||||
#include <xrpl/beast/utility/instrumentation.h> // IWYU pragma: keep
|
||||
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Every value of @p Src can be represented by @p Dest.
|
||||
// safe_cast adds compile-time checks to a static_cast to ensure that
|
||||
// the destination can hold all values of the source. This is particularly
|
||||
// handy when the source or destination is an enumeration type.
|
||||
|
||||
Given two integral types, the cast is safe when the destination can
|
||||
hold every possible value of the source.
|
||||
template <class Src, class Dest>
|
||||
concept SafeToCast = (std::is_integral_v<Src> && std::is_integral_v<Dest>) &&
|
||||
(std::is_signed_v<Src> || std::is_unsigned_v<Dest>) &&
|
||||
(std::is_signed_v<Src> != std::is_signed_v<Dest> ? sizeof(Dest) > sizeof(Src)
|
||||
: sizeof(Dest) >= sizeof(Src));
|
||||
|
||||
Comparing the bounds requires care: we use @c std::cmp_less_equal and
|
||||
@c std::cmp_greater_equal; the plain relational operators would apply
|
||||
arithmetic conversions, resulting in incorrect results when comparing
|
||||
across signedness.
|
||||
|
||||
Because @c std::cmp_* requires standard signed or unsigned integer
|
||||
arguments, which excludes character types and bool, we first widen
|
||||
all type bounds to the maximum-width integer type while preserving
|
||||
signedness.
|
||||
|
||||
@note Extended integer types, like __int128 on gcc, cannot be safely
|
||||
widened to a standard integer type, so the concept will reject
|
||||
them.
|
||||
*/
|
||||
template <typename Src, typename Dest>
|
||||
concept SafeToCast = std::is_integral_v<Src> && std::is_integral_v<Dest> && []() consteval {
|
||||
using WideSrc = std::conditional_t<std::is_signed_v<Src>, std::intmax_t, std::uintmax_t>;
|
||||
using WideDest = std::conditional_t<std::is_signed_v<Dest>, std::intmax_t, std::uintmax_t>;
|
||||
|
||||
// Note: this guard must be an evaluated branch, and not a
|
||||
// static_assert. The lambda body is outside the immediate
|
||||
// context, so a substitution-time failure here would be a
|
||||
// hard error rather than leaving the concept unsatisfied.
|
||||
if constexpr (sizeof(Src) > sizeof(WideSrc) || sizeof(Dest) > sizeof(WideDest))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
return std::cmp_less_equal(
|
||||
static_cast<WideDest>(std::numeric_limits<Dest>::min()),
|
||||
static_cast<WideSrc>(std::numeric_limits<Src>::min())) &&
|
||||
std::cmp_greater_equal(
|
||||
static_cast<WideDest>(std::numeric_limits<Dest>::max()),
|
||||
static_cast<WideSrc>(std::numeric_limits<Src>::max()));
|
||||
}
|
||||
}();
|
||||
|
||||
/** Compile-time-checked static_cast that rejects non-value preserving casts.
|
||||
|
||||
@note There is deliberately no enum-to-enum overload, and no overload
|
||||
returning the underlying type of an enum. For the latter, use
|
||||
@c std::to_underlying.
|
||||
*/
|
||||
/** @{ */
|
||||
template <typename Dest, typename Src>
|
||||
requires(std::is_integral_v<Dest> && std::is_integral_v<Src>)
|
||||
template <class Dest, class Src>
|
||||
constexpr Dest
|
||||
safeCast(Src s) noexcept
|
||||
requires(std::is_integral_v<Dest> && std::is_integral_v<Src>)
|
||||
{
|
||||
static_assert(
|
||||
SafeToCast<Src, Dest>, "This cast is not value-preserving. Please use unsafeCast instead.");
|
||||
std::is_signed_v<Dest> || std::is_unsigned_v<Src>, "Cannot cast signed to unsigned");
|
||||
constexpr unsigned kNotSame = std::is_signed_v<Dest> != std::is_signed_v<Src>;
|
||||
static_assert(
|
||||
sizeof(Dest) >= sizeof(Src) + kNotSame,
|
||||
"Destination is too small to hold all values of source");
|
||||
return static_cast<Dest>(s);
|
||||
}
|
||||
|
||||
template <typename Dest, typename Src>
|
||||
requires(std::is_enum_v<Dest> && std::is_integral_v<Src>)
|
||||
template <class Dest, class Src>
|
||||
constexpr Dest
|
||||
safeCast(Src s) noexcept
|
||||
requires(std::is_enum_v<Dest> && std::is_integral_v<Src>)
|
||||
{
|
||||
return static_cast<Dest>(safeCast<std::underlying_type_t<Dest>>(s));
|
||||
}
|
||||
|
||||
template <typename Dest, typename Src>
|
||||
requires(std::is_integral_v<Dest> && std::is_enum_v<Src>)
|
||||
template <class Dest, class Src>
|
||||
constexpr Dest
|
||||
safeCast(Src s) noexcept
|
||||
requires(std::is_integral_v<Dest> && std::is_enum_v<Src>)
|
||||
{
|
||||
return safeCast<Dest>(std::to_underlying(s));
|
||||
return safeCast<Dest>(static_cast<std::underlying_type_t<Src>>(s));
|
||||
}
|
||||
/** @} */
|
||||
|
||||
/** Integral-to-integral cast that is known to be narrowing or sign-erasing.
|
||||
// unsafe_cast explicitly flags a static_cast as not necessarily able to hold
|
||||
// all values of the source. It includes a compile-time check so that if
|
||||
// underlying types become safe, it can be converted to a safe_cast.
|
||||
|
||||
This explicitly flags a conversion that can lose information for some
|
||||
values of the source type, where the call site accepts that loss (or
|
||||
truncation is the intended behavior).
|
||||
|
||||
The compile-time check ensures the cast remains "unsafe": if the types
|
||||
involved later change such that the conversion becomes inherently
|
||||
value-preserving, the static assertion fires with instructions to
|
||||
migrate the call site to @ref safeCast.
|
||||
|
||||
If the conversion's safety depends on a runtime precondition rather
|
||||
than on the types, or varies across instantiations of generic code,
|
||||
use @ref checkedCast instead.
|
||||
*/
|
||||
/** @{ */
|
||||
template <typename Dest, typename Src>
|
||||
requires(std::is_integral_v<Dest> && std::is_integral_v<Src>)
|
||||
template <class Dest, class Src>
|
||||
constexpr Dest
|
||||
unsafeCast(Src s) noexcept
|
||||
requires(std::is_integral_v<Dest> && std::is_integral_v<Src>)
|
||||
{
|
||||
static_assert(
|
||||
!SafeToCast<Src, Dest>, "This cast is value-preserving. Please use safeCast instead.");
|
||||
!SafeToCast<Src, Dest>,
|
||||
"Only unsafe if casting signed to unsigned or "
|
||||
"destination is too small");
|
||||
return static_cast<Dest>(s);
|
||||
}
|
||||
|
||||
template <typename Dest, typename Src>
|
||||
requires(std::is_enum_v<Dest> && std::is_integral_v<Src>)
|
||||
template <class Dest, class Src>
|
||||
constexpr Dest
|
||||
unsafeCast(Src s) noexcept
|
||||
requires(std::is_enum_v<Dest> && std::is_integral_v<Src>)
|
||||
{
|
||||
return static_cast<Dest>(unsafeCast<std::underlying_type_t<Dest>>(s));
|
||||
}
|
||||
|
||||
template <typename Dest, typename Src>
|
||||
requires(std::is_integral_v<Dest> && std::is_enum_v<Src>)
|
||||
template <class Dest, class Src>
|
||||
constexpr Dest
|
||||
unsafeCast(Src s) noexcept
|
||||
{
|
||||
return unsafeCast<Dest>(std::to_underlying(s));
|
||||
}
|
||||
/** @} */
|
||||
|
||||
/** Integral-to-integral cast when the caller has performed a bounds check.
|
||||
|
||||
This documents that a runtime precondition or external invariant, which
|
||||
is not necessarily visible to the compiler, guarantees that the requested
|
||||
conversion is value-preserving for the values that can actually occur.
|
||||
|
||||
This is primarily meant for generic code, where the same expression may
|
||||
be value-preserving for one instantiation but not for another, making
|
||||
both @ref safeCast and @ref unsafeCast unusable.
|
||||
|
||||
Unlike @ref safeCast and @ref unsafeCast, this imposes no static check
|
||||
on the type relationship: the caller's claim is about runtime values,
|
||||
not about types.
|
||||
*/
|
||||
/** @{ */
|
||||
template <typename Dest, typename Src>
|
||||
requires(std::is_integral_v<Dest> && std::is_integral_v<Src>)
|
||||
constexpr Dest
|
||||
checkedCast(Src s) noexcept
|
||||
{
|
||||
return static_cast<Dest>(s);
|
||||
}
|
||||
|
||||
template <typename Dest, typename Src>
|
||||
requires(std::is_enum_v<Dest> && std::is_integral_v<Src>)
|
||||
constexpr Dest
|
||||
checkedCast(Src s) noexcept
|
||||
{
|
||||
return static_cast<Dest>(checkedCast<std::underlying_type_t<Dest>>(s));
|
||||
}
|
||||
|
||||
template <typename Dest, typename Src>
|
||||
requires(std::is_integral_v<Dest> && std::is_enum_v<Src>)
|
||||
constexpr Dest
|
||||
checkedCast(Src s) noexcept
|
||||
{
|
||||
return checkedCast<Dest>(std::to_underlying(s));
|
||||
return unsafeCast<Dest>(static_cast<std::underlying_type_t<Src>>(s));
|
||||
}
|
||||
/** @} */
|
||||
|
||||
/** Downcast within a class hierarchy, verified in debug builds.
|
||||
|
||||
Performs a static_cast down a hierarchy, but in debug builds verifies
|
||||
via dynamic_cast that the object's dynamic type actually permits the
|
||||
downcast. Both build modes execute the same conversion; debug builds
|
||||
merely add the check.
|
||||
|
||||
The pointer form passes null through unchanged, as dynamic_cast does.
|
||||
|
||||
@note The check requires @p Src to be polymorphic; in release builds
|
||||
an invalid downcast is undefined behavior on use, exactly as
|
||||
with a bare static_cast.
|
||||
*/
|
||||
/** @{ */
|
||||
template <class Dest, class Src>
|
||||
requires(
|
||||
std::is_pointer_v<Dest> && std::is_polymorphic_v<Src> &&
|
||||
std::derived_from<std::remove_pointer_t<Dest>, Src> &&
|
||||
(std::is_const_v<std::remove_pointer_t<Dest>> || !std::is_const_v<Src>) &&
|
||||
(std::is_volatile_v<std::remove_pointer_t<Dest>> || !std::is_volatile_v<Src>))
|
||||
requires std::is_pointer_v<Dest>
|
||||
inline Dest
|
||||
safeDowncast(Src* s) noexcept
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
s == nullptr || dynamic_cast<Dest>(s) != nullptr, "xrpl::safeDowncast : valid downcast");
|
||||
#ifdef NDEBUG
|
||||
return static_cast<Dest>(s); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
|
||||
#else
|
||||
auto* result = dynamic_cast<Dest>(s);
|
||||
XRPL_ASSERT(result != nullptr, "xrpl::safeDowncast : pointer downcast is valid");
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
template <class Dest, class Src>
|
||||
requires(
|
||||
std::is_lvalue_reference_v<Dest> && std::is_polymorphic_v<Src> &&
|
||||
std::derived_from<std::remove_reference_t<Dest>, Src>)
|
||||
requires std::is_lvalue_reference_v<Dest>
|
||||
inline Dest
|
||||
safeDowncast(Src& s) noexcept
|
||||
{
|
||||
return *safeDowncast<std::add_pointer_t<std::remove_reference_t<Dest>>>(std::addressof(s));
|
||||
#ifndef NDEBUG
|
||||
XRPL_ASSERT(
|
||||
dynamic_cast<std::add_pointer_t<std::remove_reference_t<Dest>>>(&s) != nullptr,
|
||||
"xrpl::safeDowncast : reference downcast is valid");
|
||||
#endif
|
||||
return static_cast<Dest>(s); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
|
||||
}
|
||||
/** @} */
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
47
include/xrpl/beast/type_name.h
Normal file
47
include/xrpl/beast/type_name.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <typeinfo>
|
||||
|
||||
#ifndef _MSC_VER
|
||||
#include <cxxabi.h>
|
||||
#endif
|
||||
|
||||
namespace beast {
|
||||
|
||||
template <typename T>
|
||||
std::string
|
||||
typeName()
|
||||
{
|
||||
using TR = std::remove_reference_t<T>;
|
||||
|
||||
std::string name = typeid(TR).name();
|
||||
|
||||
#ifndef _MSC_VER
|
||||
if (auto s = abi::__cxa_demangle(name.c_str(), nullptr, nullptr, nullptr))
|
||||
{
|
||||
name = s;
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-no-malloc)
|
||||
std::free(s);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (std::is_const_v<TR>)
|
||||
name += " const";
|
||||
if (std::is_volatile_v<TR>)
|
||||
name += " volatile";
|
||||
if (std::is_lvalue_reference_v<T>)
|
||||
{
|
||||
name += "&";
|
||||
}
|
||||
else if (std::is_rvalue_reference_v<T>)
|
||||
{
|
||||
name += "&&";
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
} // namespace beast
|
||||
@@ -2,24 +2,26 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <compare>
|
||||
#include <concepts>
|
||||
|
||||
namespace beast {
|
||||
|
||||
/**
|
||||
* Zero allows classes to offer efficient comparisons to zero.
|
||||
*
|
||||
* Zero is a struct to allow classes to efficiently compare with zero without
|
||||
* requiring an rvalue construction.
|
||||
*
|
||||
* It's often the case that we have classes which combine a number and a unit.
|
||||
* In such cases, comparisons like t > 0 or t != 0 make sense, but comparisons
|
||||
* like t > 1 or t != 1 do not. Comparing against kZero expresses exactly that,
|
||||
* without constructing a T.
|
||||
* like t > 1 or t != 1 do not.
|
||||
*
|
||||
* A type T participates if either `t.signum()` or an unqualified `signum(t)`
|
||||
* found by argument-dependent lookup returns an integer that is negative,
|
||||
* zero, or positive according to the sign of t. Both `t == kZero` and
|
||||
* `kZero == t` work, as do all six relational operators in either order.
|
||||
* The class Zero allows such comparisons to be easily made.
|
||||
*
|
||||
* The comparing class T either needs to have a method called signum() which
|
||||
* returns a positive number, 0, or a negative; or there needs to be a signum
|
||||
* function which resolves in the namespace which takes an instance of T and
|
||||
* returns a positive, zero or negative number.
|
||||
*/
|
||||
|
||||
struct Zero
|
||||
{
|
||||
explicit Zero() = default;
|
||||
@@ -28,44 +30,115 @@ struct Zero
|
||||
inline constexpr Zero kZero{};
|
||||
|
||||
/**
|
||||
* Default implementation of signum: call the member function.
|
||||
* Default implementation of signum calls the method on the class.
|
||||
*/
|
||||
template <class T>
|
||||
requires requires(T const& t) {
|
||||
{ t.signum() } -> std::integral;
|
||||
}
|
||||
[[nodiscard]] constexpr auto
|
||||
signum(T const& t) noexcept(noexcept(t.signum()))
|
||||
template <typename T>
|
||||
auto
|
||||
signum(T const& t)
|
||||
{
|
||||
return t.signum();
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
namespace detail::zero_helper {
|
||||
|
||||
/**
|
||||
* A type with a usable signum: either the member-based default above, or a
|
||||
* `signum(t)` overload in T's own namespace, found by ADL. A user overload
|
||||
* that is a better match than the template wins, as usual.
|
||||
*/
|
||||
// For argument dependent lookup to function properly, calls to signum must
|
||||
// be made from a namespace that does not include overloads of the function..
|
||||
template <class T>
|
||||
concept HasSignum = requires(T const& t) {
|
||||
{ signum(t) } -> std::integral;
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
template <detail::HasSignum T>
|
||||
[[nodiscard]] constexpr bool
|
||||
operator==(T const& t, Zero) noexcept(noexcept(signum(t)))
|
||||
auto
|
||||
callSignum(T const& t)
|
||||
{
|
||||
return signum(t) == 0;
|
||||
return signum(t);
|
||||
}
|
||||
|
||||
template <detail::HasSignum T>
|
||||
[[nodiscard]] constexpr std::strong_ordering
|
||||
operator<=>(T const& t, Zero) noexcept(noexcept(signum(t)))
|
||||
} // namespace detail::zero_helper
|
||||
|
||||
// Handle operators where T is on the left side using signum.
|
||||
|
||||
template <typename T>
|
||||
bool
|
||||
operator==(T const& t, Zero)
|
||||
{
|
||||
return signum(t) <=> 0;
|
||||
return detail::zero_helper::callSignum(t) == 0;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool
|
||||
operator!=(T const& t, Zero)
|
||||
{
|
||||
return detail::zero_helper::callSignum(t) != 0;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool
|
||||
operator<(T const& t, Zero)
|
||||
{
|
||||
return detail::zero_helper::callSignum(t) < 0;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool
|
||||
operator>(T const& t, Zero)
|
||||
{
|
||||
return detail::zero_helper::callSignum(t) > 0;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool
|
||||
operator>=(T const& t, Zero)
|
||||
{
|
||||
return detail::zero_helper::callSignum(t) >= 0;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool
|
||||
operator<=(T const& t, Zero)
|
||||
{
|
||||
return detail::zero_helper::callSignum(t) <= 0;
|
||||
}
|
||||
|
||||
// Handle operators where T is on the right side by
|
||||
// reversing the operation, so that T is on the left side.
|
||||
|
||||
template <typename T>
|
||||
bool
|
||||
operator==(Zero, T const& t)
|
||||
{
|
||||
return t == kZero;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool
|
||||
operator!=(Zero, T const& t)
|
||||
{
|
||||
return t != kZero;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool
|
||||
operator<(Zero, T const& t)
|
||||
{
|
||||
return t > kZero;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool
|
||||
operator>(Zero, T const& t)
|
||||
{
|
||||
return t < kZero;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool
|
||||
operator>=(Zero, T const& t)
|
||||
{
|
||||
return t <= kZero;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool
|
||||
operator<=(Zero, T const& t)
|
||||
{
|
||||
return t >= kZero;
|
||||
}
|
||||
|
||||
} // namespace beast
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/enum_bitops.h>
|
||||
#include <xrpl/basics/safe_cast.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/OwnerCounts.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
@@ -22,39 +22,80 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
enum class ApplyFlags : std::uint32_t {
|
||||
None = 0x00,
|
||||
// Bitwise flag enum with existing operator overloads
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
|
||||
enum ApplyFlags : std::uint32_t {
|
||||
TapNone = 0x00,
|
||||
|
||||
// This is a local transaction with the
|
||||
// fail_hard flag set.
|
||||
FailHard = 0x10,
|
||||
TapFailHard = 0x10,
|
||||
|
||||
// This is not the transaction's last pass
|
||||
// Transaction can be retried, soft failures allowed
|
||||
Retry = 0x20,
|
||||
TapRetry = 0x20,
|
||||
|
||||
// Transaction came from a privileged source
|
||||
Unlimited = 0x400,
|
||||
TapUnlimited = 0x400,
|
||||
|
||||
// Transaction is executing as part of a batch
|
||||
Batch = 0x800,
|
||||
TapBatch = 0x800,
|
||||
|
||||
// Transaction shouldn't be applied
|
||||
// Signatures shouldn't be checked
|
||||
DryRun = 0x1000,
|
||||
TapDryRun = 0x1000,
|
||||
|
||||
// Transaction is being preflighted as the payload of a
|
||||
// TransactionProposalCreate. Its signatures are collected on-ledger
|
||||
// afterward, so signature-presence checks (e.g. Batch signer matching)
|
||||
// are skipped at proposal-creation time (On-Chain Cosigner spec
|
||||
// §5.3.1.2).
|
||||
Proposal = 0x2000
|
||||
TapProposal = 0x2000
|
||||
};
|
||||
|
||||
template <>
|
||||
struct enum_bitops::OptIn<ApplyFlags> : std::true_type
|
||||
constexpr ApplyFlags
|
||||
operator|(ApplyFlags const& lhs, ApplyFlags const& rhs)
|
||||
{
|
||||
};
|
||||
return safeCast<ApplyFlags>(
|
||||
safeCast<std::underlying_type_t<ApplyFlags>>(lhs) |
|
||||
safeCast<std::underlying_type_t<ApplyFlags>>(rhs));
|
||||
}
|
||||
|
||||
static_assert((TapFailHard | TapRetry) == safeCast<ApplyFlags>(0x30u), "ApplyFlags operator |");
|
||||
static_assert((TapRetry | TapFailHard) == safeCast<ApplyFlags>(0x30u), "ApplyFlags operator |");
|
||||
|
||||
constexpr ApplyFlags
|
||||
operator&(ApplyFlags const& lhs, ApplyFlags const& rhs)
|
||||
{
|
||||
return safeCast<ApplyFlags>(
|
||||
safeCast<std::underlying_type_t<ApplyFlags>>(lhs) &
|
||||
safeCast<std::underlying_type_t<ApplyFlags>>(rhs));
|
||||
}
|
||||
|
||||
static_assert((TapFailHard & TapRetry) == TapNone, "ApplyFlags operator &");
|
||||
static_assert((TapRetry & TapFailHard) == TapNone, "ApplyFlags operator &");
|
||||
|
||||
constexpr ApplyFlags
|
||||
operator~(ApplyFlags const& flags)
|
||||
{
|
||||
return safeCast<ApplyFlags>(~safeCast<std::underlying_type_t<ApplyFlags>>(flags));
|
||||
}
|
||||
|
||||
static_assert(~TapRetry == safeCast<ApplyFlags>(0xFFFFFFDFu), "ApplyFlags operator ~");
|
||||
|
||||
inline ApplyFlags
|
||||
operator|=(ApplyFlags& lhs, ApplyFlags const& rhs)
|
||||
{
|
||||
lhs = lhs | rhs;
|
||||
return lhs;
|
||||
}
|
||||
|
||||
inline ApplyFlags
|
||||
operator&=(ApplyFlags& lhs, ApplyFlags const& rhs)
|
||||
{
|
||||
lhs = lhs & rhs;
|
||||
return lhs;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/SLEBase.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STVector256.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -31,31 +25,6 @@ public:
|
||||
: Base(keylet::skip(), view, j)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up a hash `diff` slots back from the most recent entry in the
|
||||
* sfHashes vector (diff == 0 is the most recent entry).
|
||||
*
|
||||
* Callers decide which skip list entry to read and how to translate a
|
||||
* target ledger sequence into `diff`; this only does the bounds check
|
||||
* and vector indexing shared by both the recent (stride 1) and distant
|
||||
* (stride 256) skip lists.
|
||||
*
|
||||
* @param diff how many slots back from the most recent hash to look up;
|
||||
* 0 is the most recent hash.
|
||||
* @return the hash at that slot, or std::nullopt if the entry does not
|
||||
* exist or `diff` is out of range.
|
||||
*/
|
||||
[[nodiscard]] std::optional<UInt256>
|
||||
hashAt(std::size_t diff) const
|
||||
{
|
||||
if (!this->exists())
|
||||
return std::nullopt;
|
||||
STVector256 const vec = (*this)->getFieldV256(sfHashes);
|
||||
if (vec.size() > diff)
|
||||
return vec[vec.size() - diff - 1];
|
||||
return std::nullopt;
|
||||
}
|
||||
};
|
||||
|
||||
using LedgerHashesEntryR = LedgerHashesEntry<ReadView>;
|
||||
|
||||
@@ -101,6 +101,22 @@ operator<<(std::ostream& os, AccountID const& x)
|
||||
return os;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the global cache used to map AccountID to base58 conversions.
|
||||
*
|
||||
* The cache is optional and need not be initialized. But because conversion
|
||||
* is expensive (it requires a SHA-256 operation) in most cases the overhead
|
||||
* of the cache is worth the benefit.
|
||||
*
|
||||
* @param count The number of entries the cache should accommodate. Zero will
|
||||
* disable the cache, releasing any memory associated with it.
|
||||
*
|
||||
* @note The function will only initialize the cache the first time it is
|
||||
* invoked. Subsequent invocations do nothing.
|
||||
*/
|
||||
void
|
||||
initAccountIdCache(std::size_t count);
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/beast/type_name.h>
|
||||
#include <xrpl/protocol/SOTemplate.h>
|
||||
|
||||
#include <boost/container/flat_map.hpp>
|
||||
#include <boost/core/type_name.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
@@ -84,7 +84,7 @@ public:
|
||||
* Derived classes will load the object with all the known formats.
|
||||
*/
|
||||
private:
|
||||
KnownFormats() : name_(boost::core::type_name<Derived>())
|
||||
KnownFormats() : name_(beast::typeName<Derived>())
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <xrpl/basics/CountedObject.h>
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/basics/enum_bitops.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -16,7 +15,6 @@
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -24,8 +22,18 @@ namespace xrpl {
|
||||
|
||||
class STPathElement final : public CountedObject<STPathElement>
|
||||
{
|
||||
unsigned int type_;
|
||||
AccountID accountID_;
|
||||
PathAsset assetID_;
|
||||
AccountID issuerID_;
|
||||
|
||||
bool isOffer_;
|
||||
std::size_t hashValue_;
|
||||
|
||||
public:
|
||||
enum class Type : std::uint8_t {
|
||||
// Bitwise values (typeCurrency | typeMPT)
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
|
||||
enum Type {
|
||||
TypeNone = 0x00,
|
||||
TypeAccount = 0x01, // Rippling through an account (vs taking an offer).
|
||||
TypeCurrency = 0x10, // Currency follows.
|
||||
@@ -37,8 +45,6 @@ public:
|
||||
// Combination of all types.
|
||||
};
|
||||
|
||||
using enum Type;
|
||||
|
||||
STPathElement();
|
||||
STPathElement(STPathElement const&) = default;
|
||||
STPathElement&
|
||||
@@ -56,42 +62,42 @@ public:
|
||||
bool forceAsset = false);
|
||||
|
||||
STPathElement(
|
||||
Type type,
|
||||
unsigned int uType,
|
||||
AccountID const& account,
|
||||
PathAsset const& asset,
|
||||
AccountID const& issuer);
|
||||
|
||||
[[nodiscard]] Type
|
||||
getNodeType() const noexcept;
|
||||
[[nodiscard]] std::uint32_t
|
||||
getNodeType() const;
|
||||
|
||||
[[nodiscard]] bool
|
||||
isOffer() const noexcept;
|
||||
isOffer() const;
|
||||
|
||||
[[nodiscard]] bool
|
||||
isAccount() const noexcept;
|
||||
isAccount() const;
|
||||
|
||||
[[nodiscard]] bool
|
||||
hasIssuer() const noexcept;
|
||||
hasIssuer() const;
|
||||
|
||||
[[nodiscard]] bool
|
||||
hasCurrency() const noexcept;
|
||||
hasCurrency() const;
|
||||
|
||||
[[nodiscard]] bool
|
||||
hasMPT() const noexcept;
|
||||
hasMPT() const;
|
||||
|
||||
[[nodiscard]] bool
|
||||
hasAsset() const noexcept;
|
||||
hasAsset() const;
|
||||
|
||||
[[nodiscard]] bool
|
||||
isNone() const noexcept;
|
||||
isNone() const;
|
||||
|
||||
// Nodes are either an account ID or a offer prefix. Offer prefixs denote a
|
||||
// class of offers.
|
||||
[[nodiscard]] AccountID const&
|
||||
getAccountID() const noexcept;
|
||||
getAccountID() const;
|
||||
|
||||
[[nodiscard]] PathAsset const&
|
||||
getPathAsset() const noexcept;
|
||||
getPathAsset() const;
|
||||
|
||||
[[nodiscard]] Currency const&
|
||||
getCurrency() const;
|
||||
@@ -100,35 +106,17 @@ public:
|
||||
getMPTID() const;
|
||||
|
||||
[[nodiscard]] AccountID const&
|
||||
getIssuerID() const noexcept;
|
||||
getIssuerID() const;
|
||||
|
||||
[[nodiscard]] bool
|
||||
isType(Type pe) const noexcept;
|
||||
isType(Type const& pe) const;
|
||||
|
||||
friend bool
|
||||
operator==(STPathElement const& lhs, STPathElement const& rhs) noexcept
|
||||
{
|
||||
return lhs.isType(TypeAccount) == rhs.isType(TypeAccount) &&
|
||||
lhs.hashValue_ == rhs.hashValue_ && lhs.accountID_ == rhs.accountID_ &&
|
||||
lhs.assetID_ == rhs.assetID_ && lhs.issuerID_ == rhs.issuerID_;
|
||||
}
|
||||
bool
|
||||
operator==(STPathElement const& t) const;
|
||||
|
||||
private:
|
||||
static std::size_t
|
||||
getHash(STPathElement const& element);
|
||||
|
||||
Type type_;
|
||||
AccountID accountID_;
|
||||
PathAsset assetID_;
|
||||
AccountID issuerID_;
|
||||
|
||||
bool isOffer_;
|
||||
std::size_t hashValue_;
|
||||
};
|
||||
|
||||
template <>
|
||||
struct enum_bitops::OptIn<STPathElement::Type> : std::true_type
|
||||
{
|
||||
};
|
||||
|
||||
template <class Hasher>
|
||||
@@ -136,7 +124,7 @@ void
|
||||
hash_append(Hasher& h, STPathElement const& e) noexcept
|
||||
{
|
||||
using beast::hash_append;
|
||||
hash_append(h, e.isType(STPathElement::TypeAccount));
|
||||
hash_append(h, (e.getNodeType() & STPathElement::TypeAccount) != 0u);
|
||||
hash_append(h, e.getAccountID());
|
||||
hash_append(h, e.getPathAsset());
|
||||
hash_append(h, e.getIssuerID());
|
||||
@@ -416,11 +404,11 @@ inline STPathElement::STPathElement(
|
||||
}
|
||||
|
||||
inline STPathElement::STPathElement(
|
||||
Type type,
|
||||
unsigned int uType,
|
||||
AccountID const& account,
|
||||
PathAsset const& asset,
|
||||
AccountID const& issuer)
|
||||
: type_(type)
|
||||
: type_(uType)
|
||||
, accountID_(account)
|
||||
, assetID_(asset)
|
||||
, issuerID_(issuer)
|
||||
@@ -435,56 +423,56 @@ inline STPathElement::STPathElement(
|
||||
hashValue_ = getHash(*this);
|
||||
}
|
||||
|
||||
inline STPathElement::Type
|
||||
STPathElement::getNodeType() const noexcept
|
||||
inline std::uint32_t
|
||||
STPathElement::getNodeType() const
|
||||
{
|
||||
return type_;
|
||||
}
|
||||
|
||||
inline bool
|
||||
STPathElement::isOffer() const noexcept
|
||||
STPathElement::isOffer() const
|
||||
{
|
||||
return isOffer_;
|
||||
}
|
||||
|
||||
inline bool
|
||||
STPathElement::isAccount() const noexcept
|
||||
STPathElement::isAccount() const
|
||||
{
|
||||
return !isOffer();
|
||||
}
|
||||
|
||||
inline bool
|
||||
STPathElement::isType(Type pe) const noexcept
|
||||
STPathElement::isType(Type const& pe) const
|
||||
{
|
||||
return (type_ & pe) != STPathElement::TypeNone;
|
||||
return (type_ & pe) != 0u;
|
||||
}
|
||||
|
||||
inline bool
|
||||
STPathElement::hasIssuer() const noexcept
|
||||
STPathElement::hasIssuer() const
|
||||
{
|
||||
return isType(STPathElement::TypeIssuer);
|
||||
}
|
||||
|
||||
inline bool
|
||||
STPathElement::hasCurrency() const noexcept
|
||||
STPathElement::hasCurrency() const
|
||||
{
|
||||
return isType(STPathElement::TypeCurrency);
|
||||
}
|
||||
|
||||
inline bool
|
||||
STPathElement::hasMPT() const noexcept
|
||||
STPathElement::hasMPT() const
|
||||
{
|
||||
return isType(STPathElement::TypeMpt);
|
||||
}
|
||||
|
||||
inline bool
|
||||
STPathElement::hasAsset() const noexcept
|
||||
STPathElement::hasAsset() const
|
||||
{
|
||||
return isType(STPathElement::TypeAsset);
|
||||
}
|
||||
|
||||
inline bool
|
||||
STPathElement::isNone() const noexcept
|
||||
STPathElement::isNone() const
|
||||
{
|
||||
return getNodeType() == STPathElement::TypeNone;
|
||||
}
|
||||
@@ -492,13 +480,13 @@ STPathElement::isNone() const noexcept
|
||||
// Nodes are either an account ID or a offer prefix. Offer prefixs denote a
|
||||
// class of offers.
|
||||
inline AccountID const&
|
||||
STPathElement::getAccountID() const noexcept
|
||||
STPathElement::getAccountID() const
|
||||
{
|
||||
return accountID_;
|
||||
}
|
||||
|
||||
inline PathAsset const&
|
||||
STPathElement::getPathAsset() const noexcept
|
||||
STPathElement::getPathAsset() const
|
||||
{
|
||||
return assetID_;
|
||||
}
|
||||
@@ -516,11 +504,18 @@ STPathElement::getMPTID() const
|
||||
}
|
||||
|
||||
inline AccountID const&
|
||||
STPathElement::getIssuerID() const noexcept
|
||||
STPathElement::getIssuerID() const
|
||||
{
|
||||
return issuerID_;
|
||||
}
|
||||
|
||||
inline bool
|
||||
STPathElement::operator==(STPathElement const& t) const
|
||||
{
|
||||
return (type_ & TypeAccount) == (t.type_ & TypeAccount) && hashValue_ == t.hashValue_ &&
|
||||
accountID_ == t.accountID_ && assetID_ == t.assetID_ && issuerID_ == t.issuerID_;
|
||||
}
|
||||
|
||||
// ------------ STPath ------------
|
||||
|
||||
inline STPath::STPath(std::vector<STPathElement> p) : path_(std::move(p))
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -392,7 +392,6 @@ JSS(max_ledger); // in/out: LedgerCleaner
|
||||
JSS(max_queue_size); // out: TxQ
|
||||
JSS(max_spend_drops); // out: AccountInfo
|
||||
JSS(max_spend_drops_total); // out: AccountInfo
|
||||
JSS(maximum);
|
||||
JSS(mean); // out: get_aggregate_price
|
||||
JSS(median); // out: get_aggregate_price
|
||||
JSS(median_fee); // out: TxQ
|
||||
@@ -412,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
|
||||
|
||||
@@ -51,8 +51,7 @@ public:
|
||||
beast::Journal journal = beast::Journal{beast::Journal::getNullSink()})
|
||||
: ApplyContext(registry, base, std::nullopt, tx, preclaimResult, baseFee, flags, journal)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
(flags & ApplyFlags::Batch) == ApplyFlags::None, "Batch apply flag should not be set");
|
||||
XRPL_ASSERT((flags & TapBatch) == 0, "Batch apply flag should not be set");
|
||||
}
|
||||
|
||||
std::reference_wrapper<ServiceRegistry> registry;
|
||||
|
||||
@@ -59,11 +59,10 @@ public:
|
||||
, parentBatchId(parentBatchId)
|
||||
, j(j)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
(flags & ApplyFlags::Batch) == ApplyFlags::Batch, "Batch apply flag should be set");
|
||||
XRPL_ASSERT((flags & TapBatch) == TapBatch, "Batch apply flag should be set");
|
||||
XRPL_ASSERT_IF(
|
||||
(flags & ApplyFlags::Proposal) != ApplyFlags::None,
|
||||
(flags & ApplyFlags::DryRun) != ApplyFlags::None,
|
||||
(flags & TapProposal) != TapNone,
|
||||
(flags & TapDryRun) != TapNone,
|
||||
"xrpl::PreflightContext : proposal preflight implies dry run");
|
||||
}
|
||||
|
||||
@@ -75,11 +74,10 @@ public:
|
||||
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
|
||||
: registry(registry), tx(tx), rules(std::move(rules)), flags(flags), j(j)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
(flags & ApplyFlags::Batch) == ApplyFlags::None, "Batch apply flag should not be set");
|
||||
XRPL_ASSERT((flags & TapBatch) == 0, "Batch apply flag should not be set");
|
||||
XRPL_ASSERT_IF(
|
||||
(flags & ApplyFlags::Proposal) != ApplyFlags::None,
|
||||
(flags & ApplyFlags::DryRun) != ApplyFlags::None,
|
||||
(flags & TapProposal) != TapNone,
|
||||
(flags & TapDryRun) != TapNone,
|
||||
"xrpl::PreflightContext : proposal preflight implies dry run");
|
||||
}
|
||||
|
||||
@@ -118,7 +116,7 @@ public:
|
||||
, j(j)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
parentBatchId.has_value() == ((flags & ApplyFlags::Batch) == ApplyFlags::Batch),
|
||||
parentBatchId.has_value() == ((flags & TapBatch) == TapBatch),
|
||||
"Parent Batch ID should be set if batch apply flag is set");
|
||||
}
|
||||
|
||||
@@ -131,8 +129,7 @@ public:
|
||||
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
|
||||
: PreclaimContext(registry, view, preflightResult, tx, flags, std::nullopt, j)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
(flags & ApplyFlags::Batch) == ApplyFlags::None, "Batch apply flag should not be set");
|
||||
XRPL_ASSERT((flags & TapBatch) == 0, "Batch apply flag should not be set");
|
||||
}
|
||||
|
||||
PreclaimContext&
|
||||
|
||||
@@ -41,7 +41,7 @@ struct ApplyResult
|
||||
inline bool
|
||||
isTecClaimHardFail(TER ter, ApplyFlags flags)
|
||||
{
|
||||
return isTecClaim(ter) && ((flags & ApplyFlags::Retry) == ApplyFlags::None);
|
||||
return isTecClaim(ter) && ((flags & TapRetry) == 0u);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,144 +2,144 @@ Detected OS: macos (Darwin arm64)
|
||||
|
||||
Core build tools:
|
||||
✅ cmake
|
||||
cmake version 4.4.3
|
||||
/nix/store/q85csxf4s4shx89zif097h1ql4ax1rrz-cmake-4.4.3/bin/cmake
|
||||
cmake version 4.1.2
|
||||
/nix/store/gvabsb4yqb5xsqzqph54rijnn4zpihnp-cmake-4.1.2/bin/cmake
|
||||
✅ conan
|
||||
Conan version 2.32.0
|
||||
/nix/store/921jqsgbilixmr3xchj95si0jl8pi13z-conan-2.32.0/bin/conan
|
||||
Conan version 2.28.1
|
||||
/nix/store/9jiyxmkpwmn6dcqs0765s83riw3l5ail-conan-2.28.1/bin/conan
|
||||
✅ git
|
||||
git version 2.55.0
|
||||
/nix/store/gw7c7m0dwca5lg4152x9lpmxpj171bzw-git-2.55.0/bin/git
|
||||
git version 2.54.0
|
||||
/nix/store/a14yxcqvv9x2l9mllgpirzhvz93pgprg-git-2.54.0/bin/git
|
||||
✅ python3
|
||||
Python 3.14.7
|
||||
/nix/store/0kcws5blwbdyxqxjd1w8hjipz61xny3k-python3-3.14.7/bin/python3.14
|
||||
Python 3.13.13
|
||||
/nix/store/ygxqin6ydzjfawywqpp5pal8wv6sf5bh-python3-3.13.13/bin/python3.13
|
||||
|
||||
Development tooling:
|
||||
✅ ccache
|
||||
ccache version 4.13.6
|
||||
/nix/store/ydr5nlzxp1djb5256y0zzqa87vz55gsk-ccache-4.13.6/bin/ccache
|
||||
/nix/store/57davyvs6p6dkrl3svzwg1ph18wsy4cz-ccache-4.13.6/bin/ccache
|
||||
✅ clang
|
||||
clang version 22.1.8
|
||||
/nix/store/dakcxgwz4ixkk6ps50b7vn3dvblyzsnf-clang-wrapper-22.1.8/bin/clang
|
||||
clang version 22.1.7
|
||||
/nix/store/192glrb2cldvziyf3378mzjqbzx3ih4g-clang-wrapper-22.1.7/bin/clang
|
||||
✅ clang-22
|
||||
clang version 22.1.8
|
||||
/nix/store/w826sqb98nnym6kzackg829664iqdlxj-clang-22/bin/clang-22
|
||||
clang version 22.1.7
|
||||
/nix/store/rbap7zqq7mw00fyqa02p5rj7gqjp4w5i-clang-22/bin/clang-22
|
||||
✅ clang++
|
||||
clang version 22.1.8
|
||||
/nix/store/dakcxgwz4ixkk6ps50b7vn3dvblyzsnf-clang-wrapper-22.1.8/bin/clang++
|
||||
clang version 22.1.7
|
||||
/nix/store/192glrb2cldvziyf3378mzjqbzx3ih4g-clang-wrapper-22.1.7/bin/clang++
|
||||
✅ clang++-22
|
||||
clang version 22.1.8
|
||||
/nix/store/wnwyw3yfqrhpxij65bl3pa1x65q2zg3m-clang++-22/bin/clang++-22
|
||||
clang version 22.1.7
|
||||
/nix/store/v9haf787f7bcz0mq1sad4bpyx21pj6li-clang++-22/bin/clang++-22
|
||||
✅ ClangBuildAnalyzer
|
||||
ClangBuildAnalyzer 1.6.0
|
||||
/nix/store/c7jjnw29ra78jdlzjww2m374izhy05n8-clangbuildanalyzer-1.6.0/bin/ClangBuildAnalyzer
|
||||
/nix/store/4l50ds9fa2mkvh7wg8qzrlbmjs12sb8l-clangbuildanalyzer-1.6.0/bin/ClangBuildAnalyzer
|
||||
✅ curl
|
||||
curl 8.22.0 (aarch64-apple-darwin25.6.0) libcurl/8.22.0 OpenSSL/3.5.8 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.70.0 mit-krb5/1.22.2
|
||||
/nix/store/pq1h17abqy6q5y6k5dn9qj0fwb89flba-curl-8.22.0-bin/bin/curl
|
||||
curl 8.20.0 (aarch64-apple-darwin25.3.0) libcurl/8.20.0 OpenSSL/3.6.2 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.69.0 mit-krb5/1.22.1
|
||||
/nix/store/kclq0czaxvsgh4ym9ld7b6iwy50l1snk-curl-8.20.0-bin/bin/curl
|
||||
✅ file
|
||||
file-5.48
|
||||
/nix/store/y44vpziwbzfm8020dzlqqbybyyy7lb6g-file-5.48/bin/file
|
||||
file-5.47
|
||||
/nix/store/dax63li7wwcbqxxkkgzc4g2rx7d4w86x-file-5.47/bin/file
|
||||
✅ less
|
||||
less 710 (PCRE2 regular expressions)
|
||||
/nix/store/3yn3dx5fwg5i0inqqzwnxq7f6lwhpjqa-less-710/bin/less
|
||||
less 692 (PCRE2 regular expressions)
|
||||
/nix/store/lvr16y75r1pdxpdv0aph5ak2yd0hkvqm-less-692/bin/less
|
||||
✅ make
|
||||
GNU Make 4.4.1
|
||||
/nix/store/8l4wgxmhdxfvc9g18y76bnxl1ghgcp7n-gnumake-4.4.1/bin/make
|
||||
/nix/store/8wwiw8pwyhrkzyq28hqzxfl4z84lks81-gnumake-4.4.1/bin/make
|
||||
✅ netstat
|
||||
present
|
||||
/nix/store/57fqvgdav8lrmzndkwm7qrgksl01yj23-network_cmds-741.100.2/bin/netstat
|
||||
/nix/store/qsd1kzqb0ahrk433vmyl245gp623j19s-network_cmds-730.80.3/bin/netstat
|
||||
✅ ninja
|
||||
1.13.2
|
||||
/nix/store/infm4vlx71q83bqz0k559w1gn4b0rq4i-ninja-1.13.2/bin/ninja
|
||||
/nix/store/bqykhrblarkj4fl0hz2mf8ngwfv6x6bz-ninja-1.13.2/bin/ninja
|
||||
✅ perl
|
||||
v5.42.3
|
||||
/nix/store/7rvr6qjfzk2ybzn1imjw6c6qq1n1a9cr-perl-5.42.3/bin/perl
|
||||
v5.42.0
|
||||
/nix/store/js13ri9fvm0ajk1fpd3acigys2a9whdv-perl-5.42.0/bin/perl
|
||||
✅ pkg-config
|
||||
0.29.2
|
||||
/nix/store/nhgylvj98klqggy54h7gbc3b2ia4a00y-pkg-config-wrapper-0.29.2/bin/pkg-config
|
||||
/nix/store/lzrwr375jqhhbca116kja96xf1md83l8-pkg-config-wrapper-0.29.2/bin/pkg-config
|
||||
✅ vim
|
||||
VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Jan 01 1980 00:00:00)
|
||||
/nix/store/akaa9kqm3b8rlkv61kvsqr2l2j70rvxb-vim-9.2.1001/bin/vim
|
||||
/nix/store/6vbkykg92w603c0sw3mkk7p7mfaawbns-vim-9.2.0389/bin/vim
|
||||
✅ zip
|
||||
Zip 3.0
|
||||
/nix/store/dvawr36npnad4xa6dxi2ss0fw44aa82r-zip-3.0/bin/zip
|
||||
/nix/store/z6ph729vcakbvz3wh8ln1wk6mi06w487-zip-3.0/bin/zip
|
||||
✅ clang-apply-replacements
|
||||
clang-apply-replacements version 22.1.8
|
||||
/nix/store/5bij4zn161lagwyndmrr21vmxjg054nv-clang-tools-22.1.8/bin/clang-apply-replacements
|
||||
clang-apply-replacements version 22.1.7
|
||||
/nix/store/vzyyjf3cm1hbj9wcr2qcb66x6j98zpy7-clang-tools-22.1.7/bin/clang-apply-replacements
|
||||
✅ clang-apply-replacements-22
|
||||
clang-apply-replacements version 22.1.8
|
||||
/nix/store/w4bz034k4l0w719rrmbwnqcgyjbswy81-clang-apply-replacements-22/bin/clang-apply-replacements-22
|
||||
clang-apply-replacements version 22.1.7
|
||||
/nix/store/m3ii69rca4077lf4wlk7m3jcag1fs577-clang-apply-replacements-22/bin/clang-apply-replacements-22
|
||||
✅ clang-format
|
||||
clang-format version 22.1.8
|
||||
/nix/store/5bij4zn161lagwyndmrr21vmxjg054nv-clang-tools-22.1.8/bin/clang-format
|
||||
clang-format version 22.1.7
|
||||
/nix/store/vzyyjf3cm1hbj9wcr2qcb66x6j98zpy7-clang-tools-22.1.7/bin/clang-format
|
||||
✅ clang-format-22
|
||||
clang-format version 22.1.8
|
||||
/nix/store/w7z346l0r8y36b6lc5i7jzj3w0581l2y-clang-format-22/bin/clang-format-22
|
||||
clang-format version 22.1.7
|
||||
/nix/store/4fawqy6ngqcsqd2ygyyzm93q0xy3f5gs-clang-format-22/bin/clang-format-22
|
||||
✅ clang-tidy
|
||||
LLVM version 22.1.8
|
||||
/nix/store/5bij4zn161lagwyndmrr21vmxjg054nv-clang-tools-22.1.8/bin/clang-tidy
|
||||
LLVM version 22.1.7
|
||||
/nix/store/vzyyjf3cm1hbj9wcr2qcb66x6j98zpy7-clang-tools-22.1.7/bin/clang-tidy
|
||||
✅ clang-tidy-22
|
||||
LLVM version 22.1.8
|
||||
/nix/store/0lv25qd3ddgjm3ndn70q61jfr6krlkl7-clang-tidy-22/bin/clang-tidy-22
|
||||
LLVM version 22.1.7
|
||||
/nix/store/jqw4280saixaxxihwdba9ldm2fsm6dr3-clang-tidy-22/bin/clang-tidy-22
|
||||
✅ dot
|
||||
dot - graphviz version 15.1.1 (0)
|
||||
/nix/store/mc99a6bpbk2waym2cnw6ifmxli4ndb4x-graphviz-15.1.1/bin/dot
|
||||
dot - graphviz version 12.2.1 (0)
|
||||
/nix/store/ijb4fbnqa6wzlpqnhb6q9knqpf7qqn5z-graphviz-12.2.1/bin/dot
|
||||
✅ doxygen
|
||||
1.17.0
|
||||
/nix/store/kz43yaj0wg6fx6cbrcmi2442z1ramy0c-doxygen-1.17.0/bin/doxygen
|
||||
1.16.1
|
||||
/nix/store/kbryjdpq9jizjb0ws0nzbf2h2ymbdiwm-doxygen-1.16.1/bin/doxygen
|
||||
✅ gcovr
|
||||
gcovr 8.4
|
||||
/nix/store/czj1vxsizrbba280gg22cys40qxkqjgr-python3.14-gcovr-8.4/bin/gcovr
|
||||
/nix/store/wn8jiyh9p0bybs96s4163qp3k8vfmczx-python3.13-gcovr-8.4/bin/gcovr
|
||||
✅ gh
|
||||
gh version 2.102.0 (2026-09-30)
|
||||
/nix/store/s8bahl6zssvry93ihivfmk1l6c58byrr-gh-2.102.0/bin/gh
|
||||
gh version 2.94.0 (nixpkgs)
|
||||
/nix/store/fhnpw0hs0gjms1ha6ap02jq7rx13gkbp-gh-2.94.0/bin/gh
|
||||
✅ git-cliff
|
||||
git-cliff 2.14.2
|
||||
/nix/store/k4m5yk0qb7hfslwsq1zjajz1d94j12bj-git-cliff-2.14.2/bin/git-cliff
|
||||
git-cliff 2.13.1
|
||||
/nix/store/cy0wwhgxa7yvrz97zydbq6sqmixc90fq-git-cliff-2.13.1/bin/git-cliff
|
||||
✅ git-lfs
|
||||
git-lfs/3.8.0 (3.8.0; darwin arm64; go 1.26.8)
|
||||
/nix/store/wzixjr3yb40a13m2q654hm3bdjn6sal3-git-lfs-3.8.0/bin/git-lfs
|
||||
git-lfs/3.7.1 (3.7.1; darwin arm64; go 1.26.3)
|
||||
/nix/store/k9r7zjfjplqa4d5s71cqvf2iv73jd9mc-git-lfs-3.7.1/bin/git-lfs
|
||||
✅ gpg
|
||||
gpg (GnuPG) 2.4.9
|
||||
/nix/store/0ldfwnwx385i5kh7cyvxx4mkvgjhs5rh-gnupg-2.4.9/bin/gpg
|
||||
/nix/store/cgh6iwzz5jgx9z5whka4vgj210i6npc6-gnupg-2.4.9/bin/gpg
|
||||
✅ pre-commit
|
||||
pre-commit 4.6.2
|
||||
/nix/store/ivh54xypg9qvd3ha9lis0i8p4nkf8n7a-pre-commit-4.6.2/bin/pre-commit
|
||||
pre-commit 4.5.1
|
||||
/nix/store/z3cca68620w0w10f090szgzdnmh1waf2-pre-commit-4.5.1/bin/pre-commit
|
||||
✅ run-clang-tidy
|
||||
usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
|
||||
/nix/store/3xss1mm3mh1hzcmg182na6ki1p5rnxf1-run-clang-tidy/bin/run-clang-tidy
|
||||
/nix/store/4x28x911z2f9y7adqlh3qspp4a16dig7-run-clang-tidy/bin/run-clang-tidy
|
||||
✅ run-clang-tidy-22
|
||||
usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
|
||||
/nix/store/w68r9z07hcq4fwbfyq3yvc7qxm8aqfbl-run-clang-tidy-22/bin/run-clang-tidy-22
|
||||
/nix/store/x8iymrh76sk5q91ryg5pa7i32s6gfh34-run-clang-tidy-22/bin/run-clang-tidy-22
|
||||
|
||||
Rust toolchain:
|
||||
✅ cargo
|
||||
cargo 1.97.1 (c980f4866 2026-06-30)
|
||||
/nix/store/gklji8yq8yrx1k039c10kwz60vqf2dxj-rust-minimal-1.97.1/bin/cargo
|
||||
/nix/store/bnfk1sl4s9angb0vj1cj9a5y5zvqinwy-rust-minimal-1.97.1/bin/cargo
|
||||
✅ cargo-audit
|
||||
cargo-audit-audit 0.22.2
|
||||
/nix/store/7jgrwj557l91940aj0ag4i924ima1cqn-cargo-audit-0.22.2/bin/cargo-audit
|
||||
cargo-audit-audit 0.22.1
|
||||
/nix/store/snwkga2f5gyf404h7mmp9wriwxb8v65f-cargo-audit-0.22.1/bin/cargo-audit
|
||||
✅ cargo-llvm-cov
|
||||
cargo-llvm-cov 0.9.0
|
||||
/nix/store/5q1h285z8zzkw2qxig9a0b1avcsk1cf0-cargo-llvm-cov-0.9.0/bin/cargo-llvm-cov
|
||||
cargo-llvm-cov 0.8.5
|
||||
/nix/store/fpiqdh91gwyxalqp409ynm0s0g086w7w-cargo-llvm-cov-0.8.5/bin/cargo-llvm-cov
|
||||
✅ cargo-nextest
|
||||
cargo-nextest 0.9.146
|
||||
/nix/store/69v3hx3k597hcxd2zy7yad7fg0aw5yxd-cargo-nextest-0.9.146/bin/cargo-nextest
|
||||
cargo-nextest 0.9.137
|
||||
/nix/store/ylz7m947mhkgsp6i7611id3s3gcd58nq-cargo-nextest-0.9.137/bin/cargo-nextest
|
||||
✅ clippy-driver
|
||||
clippy 0.1.97 (8bab26f4f6 2026-07-14)
|
||||
/nix/store/gklji8yq8yrx1k039c10kwz60vqf2dxj-rust-minimal-1.97.1/bin/clippy-driver
|
||||
/nix/store/bnfk1sl4s9angb0vj1cj9a5y5zvqinwy-rust-minimal-1.97.1/bin/clippy-driver
|
||||
✅ rust-analyzer
|
||||
rust-analyzer 1.97.1 (8bab26f4 2026-07-14)
|
||||
/nix/store/30rbyk8gx7v53fxqhs4pp0fvxfjycspr-rust-analyzer-preview-1.97.1-aarch64-apple-darwin/bin/rust-analyzer
|
||||
/nix/store/j6apc5pmd0giy15da9p650r8zklslmvi-rust-analyzer-preview-1.97.1-aarch64-apple-darwin/bin/rust-analyzer
|
||||
✅ rust-nightly
|
||||
rustc 1.101.0-nightly (282215592 2026-10-04)
|
||||
/nix/store/a9552rf3pylcnkijc1z0hf1lcnv50djq-rust-nightly/bin/rust-nightly
|
||||
rustc 1.99.0-nightly (87e5904f5 2026-07-20)
|
||||
/nix/store/fqpjz4l0nsnji8b2pz57mnj0akbp6hcl-rust-nightly/bin/rust-nightly
|
||||
✅ rustc
|
||||
rustc 1.97.1 (8bab26f4f 2026-07-14)
|
||||
/nix/store/gklji8yq8yrx1k039c10kwz60vqf2dxj-rust-minimal-1.97.1/bin/rustc
|
||||
/nix/store/bnfk1sl4s9angb0vj1cj9a5y5zvqinwy-rust-minimal-1.97.1/bin/rustc
|
||||
✅ rustfmt
|
||||
rustfmt 1.9.0-stable (8bab26f4f6 2026-07-14)
|
||||
/nix/store/0jmdipyn8266j9kqx2xxm2qkxlrlrjfc-rustfmt-preview-1.97.1-aarch64-apple-darwin/bin/rustfmt
|
||||
/nix/store/5ymwgr9jqjz7zzbmj0j5vqbwcd3kp0vm-rustfmt-preview-1.97.1-aarch64-apple-darwin/bin/rustfmt
|
||||
|
||||
Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set).
|
||||
|
||||
|
||||
@@ -21,8 +21,7 @@ let
|
||||
)
|
||||
);
|
||||
|
||||
toolchain =
|
||||
if pkgs.stdenv.hostPlatform.isLinux then linux.toolchain else (darwin.toolchain ++ [ darwinEnv ]);
|
||||
toolchain = if pkgs.stdenv.isLinux then linux.toolchain else (darwin.toolchain ++ [ darwinEnv ]);
|
||||
in
|
||||
{
|
||||
default = pkgs.buildEnv {
|
||||
|
||||
@@ -20,16 +20,15 @@ let
|
||||
|
||||
# Custom-glibc stdenvs, matching the CI environment. darwin has no custom
|
||||
# glibc, so there they fall back to the plain nixpkgs stdenvs.
|
||||
customGccStdenv = if pkgs.stdenv.hostPlatform.isLinux then linux.gccStdenv else plainGccStdenv;
|
||||
customClangStdenv =
|
||||
if pkgs.stdenv.hostPlatform.isLinux then linux.clangStdenv else plainClangStdenv;
|
||||
customGccStdenv = if pkgs.stdenv.isLinux then linux.gccStdenv else plainGccStdenv;
|
||||
customClangStdenv = if pkgs.stdenv.isLinux then linux.clangStdenv else plainClangStdenv;
|
||||
|
||||
# gcov matching each gcc shell, so `-Dcoverage=ON` builds work in the shell.
|
||||
plainGcov = mkGcov {
|
||||
name = "plain";
|
||||
cc = gccPackage.cc;
|
||||
};
|
||||
customGccGcov = if pkgs.stdenv.hostPlatform.isLinux then linux.gcov else plainGcov;
|
||||
customGccGcov = if pkgs.stdenv.isLinux then linux.gcov else plainGcov;
|
||||
|
||||
# Whole directory: init.sh locates the profiles relative to itself.
|
||||
conanDir = ../conan;
|
||||
@@ -52,7 +51,7 @@ let
|
||||
|
||||
# Not sdkEnv: a shell's stdenv already sets that up. Prepended so the stub
|
||||
# beats the nixpkgs libresolv this shell's tooling drags in.
|
||||
darwinLibresolvHook = pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isDarwin (
|
||||
darwinLibresolvHook = pkgs.lib.optionalString pkgs.stdenv.isDarwin (
|
||||
pkgs.lib.concatLines (
|
||||
pkgs.lib.mapAttrsToList (
|
||||
name: value: ''export ${name}="${value} ''${${name}:-}"''
|
||||
@@ -127,7 +126,7 @@ let
|
||||
in
|
||||
rec {
|
||||
# macOS: Nix Clang. Linux: Nix GCC.
|
||||
default = if pkgs.stdenv.hostPlatform.isDarwin then clang else gcc;
|
||||
default = if pkgs.stdenv.isDarwin then clang else gcc;
|
||||
|
||||
# gcc/clang use the custom-glibc toolchain, matching CI. On darwin there is no
|
||||
# custom glibc, so they fall back to the plain nixpkgs toolchain.
|
||||
@@ -148,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";
|
||||
@@ -172,7 +158,7 @@ rec {
|
||||
# The *-plain shells (stock nixpkgs toolchain) exist only on Linux: on darwin
|
||||
# gcc/clang are already plain, so these would be redundant and are omitted, which
|
||||
# makes `nix develop .#gcc-plain` fail there rather than silently aliasing gcc.
|
||||
// pkgs.lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux {
|
||||
// pkgs.lib.optionalAttrs pkgs.stdenv.isLinux {
|
||||
gcc-plain = makeShell {
|
||||
shellName = "gcc-plain";
|
||||
stdenv = plainGccStdenv;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -24,12 +24,6 @@ PRE_RELEASE = re.compile(r"^(b|rc)(0|[1-9][0-9]*)$")
|
||||
# Channels a tag of any version is published to, rather than only bN/rcN.
|
||||
ANY_VERSION_CHANNELS = ("custom", "private")
|
||||
|
||||
# The version of any untagged build, which is all the develop channel publishes.
|
||||
DEV_VERSION = "0.0.0-dev"
|
||||
|
||||
# Channels an untagged build is packaged for: develop, or none for a local build.
|
||||
DEV_VERSION_CHANNELS = ("develop", "UNRELEASED")
|
||||
|
||||
# The package name a variant suffixes, and the name every variant keeps for its
|
||||
# on-disk paths (/usr/bin/xrpld, /etc/xrpld, xrpld.service).
|
||||
BASE_NAME = "xrpld"
|
||||
@@ -80,23 +74,13 @@ def capture(*command: object) -> str:
|
||||
def package_version(reported: str, channel: str) -> str:
|
||||
"""Normalise a reported version into one the package formats accept.
|
||||
|
||||
- Release: unchanged (3.2.0 -> 3.2.0).
|
||||
- bN/rcN pre-release: '-' becomes '~', to sort before the release
|
||||
(3.2.0-b1 -> 3.2.0~b1).
|
||||
- custom, private: any version, '-' inside the pre-release and build
|
||||
metadata becomes '.' (3.4.0-custom-1 -> 3.4.0~custom.1).
|
||||
- develop, UNRELEASED: 0.0.0-dev without build metadata, so builds sort by
|
||||
package release, not commit hash (0.0.0-dev+abc1234 -> 0.0.0~dev).
|
||||
develop accepts nothing else.
|
||||
A pre-release switches to '~' (3.2.0-b1 -> 3.2.0~b1), which also sorts before
|
||||
the final 3.2.0; a no-op for a final release. The custom and private
|
||||
channels accept any pre-release and build metadata, with any '-' inside
|
||||
either switched to '.' (3.4.0-custom-1 -> 3.4.0~custom.1).
|
||||
"""
|
||||
# Metadata first, as it may contain '-' too.
|
||||
release, plus, metadata = reported.partition("+")
|
||||
if release == DEV_VERSION and channel in DEV_VERSION_CHANNELS:
|
||||
return DEV_VERSION.replace("-", "~")
|
||||
assert channel != "develop", (
|
||||
f"unsupported version {reported!r}: "
|
||||
f"the develop channel only accepts {DEV_VERSION}."
|
||||
)
|
||||
base, _, pre_release = release.partition("-")
|
||||
if channel in ANY_VERSION_CHANNELS:
|
||||
pre_release = pre_release.replace("-", ".")
|
||||
|
||||
10
package/docker/Dockerfile
Normal file
10
package/docker/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
ARG BASE_IMAGE=debian:trixie
|
||||
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
# Bind-mounted rather than copied in, so the installer never lands in a layer.
|
||||
RUN --mount=type=bind,source=bin/install-packaging-tools.sh,target=/install-packaging-tools.sh \
|
||||
/install-packaging-tools.sh
|
||||
|
||||
# See ../README.md, "Publishing from other repositories".
|
||||
COPY package/docker/publish_pkg.py /usr/local/bin/publish_pkg.py
|
||||
15
package/image/Dockerfile
Normal file
15
package/image/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM ubuntu:26.04
|
||||
|
||||
RUN --mount=type=bind,target=/tmp/package \
|
||||
apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends /tmp/package/*.deb \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& ln -sf /dev/stdout /var/log/xrpld/debug.log
|
||||
|
||||
USER xrpld
|
||||
WORKDIR /var/lib/xrpld
|
||||
|
||||
EXPOSE 2459 5005 6006 50051
|
||||
|
||||
ENTRYPOINT ["/usr/bin/xrpld"]
|
||||
CMD ["--net", "--silent", "--conf", "/etc/xrpld/xrpld.cfg"]
|
||||
@@ -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()
|
||||
|
||||
263
src/benchmarks/libxrpl/number/Arithmetic.cpp
Normal file
263
src/benchmarks/libxrpl/number/Arithmetic.cpp
Normal file
@@ -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
|
||||
269
src/benchmarks/libxrpl/number/BoostDecimal.h
Normal file
269
src/benchmarks/libxrpl/number/BoostDecimal.h
Normal file
@@ -0,0 +1,269 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
|
||||
#include <boost/decimal.hpp>
|
||||
|
||||
#include <benchmarks/libxrpl/number/NumberLike.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::number_bench {
|
||||
|
||||
namespace detail {
|
||||
|
||||
constexpr boost::decimal::rounding_mode
|
||||
toBoost(Number::RoundingMode mode)
|
||||
{
|
||||
using enum Number::RoundingMode;
|
||||
using boost::decimal::rounding_mode;
|
||||
switch (mode)
|
||||
{
|
||||
case ToNearest:
|
||||
return rounding_mode::fe_dec_to_nearest;
|
||||
case TowardsZero:
|
||||
return rounding_mode::fe_dec_toward_zero;
|
||||
case Downward:
|
||||
return rounding_mode::fe_dec_downward;
|
||||
case Upward:
|
||||
return rounding_mode::fe_dec_upward;
|
||||
}
|
||||
return rounding_mode::fe_dec_to_nearest;
|
||||
}
|
||||
|
||||
constexpr Number::RoundingMode
|
||||
fromBoost(boost::decimal::rounding_mode mode)
|
||||
{
|
||||
using enum Number::RoundingMode;
|
||||
using boost::decimal::rounding_mode;
|
||||
switch (mode)
|
||||
{
|
||||
case rounding_mode::fe_dec_toward_zero:
|
||||
return TowardsZero;
|
||||
case rounding_mode::fe_dec_downward:
|
||||
return Downward;
|
||||
case rounding_mode::fe_dec_upward:
|
||||
return Upward;
|
||||
default:
|
||||
return ToNearest;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/**
|
||||
* Wraps a Boost.Decimal type behind Number's interface.
|
||||
*
|
||||
* Every member is a one-line inline forward, so at -O2 and above the wrapper
|
||||
* adds no cost over using the Boost type directly.
|
||||
*
|
||||
* Semantic differences from Number that the wrapper deliberately does NOT
|
||||
* hide (they are part of what we are measuring):
|
||||
* - Overflow produces infinity and division by zero produces inf/NaN
|
||||
* instead of throwing.
|
||||
* - The rounding mode is a process-wide global in Boost.Decimal, not
|
||||
* thread-local like Number's.
|
||||
* - decimal64_t / decimal_fast64_t carry 16 digits, so constructing from a
|
||||
* 19-digit int64 mantissa rounds.
|
||||
*/
|
||||
template <class D>
|
||||
class BoostDecimal final
|
||||
{
|
||||
D value_{};
|
||||
|
||||
explicit constexpr BoostDecimal(D value) noexcept : value_{value}
|
||||
{
|
||||
}
|
||||
|
||||
public:
|
||||
using rep = std::int64_t;
|
||||
using value_type = D;
|
||||
|
||||
constexpr BoostDecimal() = default;
|
||||
|
||||
// Implicit, like Number(rep).
|
||||
constexpr BoostDecimal(rep mantissa) noexcept : value_{mantissa, 0} // NOLINT
|
||||
{
|
||||
}
|
||||
|
||||
explicit constexpr BoostDecimal(rep mantissa, int exponent) noexcept
|
||||
: value_{mantissa, exponent}
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr D
|
||||
value() const noexcept
|
||||
{
|
||||
return value_;
|
||||
}
|
||||
|
||||
constexpr BoostDecimal
|
||||
operator-() const noexcept
|
||||
{
|
||||
return BoostDecimal{-value_};
|
||||
}
|
||||
|
||||
constexpr BoostDecimal&
|
||||
operator+=(BoostDecimal const& x) noexcept
|
||||
{
|
||||
value_ += x.value_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
constexpr BoostDecimal&
|
||||
operator-=(BoostDecimal const& x) noexcept
|
||||
{
|
||||
value_ -= x.value_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
constexpr BoostDecimal&
|
||||
operator*=(BoostDecimal const& x) noexcept
|
||||
{
|
||||
value_ *= x.value_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
constexpr BoostDecimal&
|
||||
operator/=(BoostDecimal const& x) noexcept
|
||||
{
|
||||
value_ /= x.value_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
friend constexpr BoostDecimal
|
||||
operator+(BoostDecimal const& x, BoostDecimal const& y) noexcept
|
||||
{
|
||||
return BoostDecimal{x.value_ + y.value_};
|
||||
}
|
||||
|
||||
friend constexpr BoostDecimal
|
||||
operator-(BoostDecimal const& x, BoostDecimal const& y) noexcept
|
||||
{
|
||||
return BoostDecimal{x.value_ - y.value_};
|
||||
}
|
||||
|
||||
friend constexpr BoostDecimal
|
||||
operator*(BoostDecimal const& x, BoostDecimal const& y) noexcept
|
||||
{
|
||||
return BoostDecimal{x.value_ * y.value_};
|
||||
}
|
||||
|
||||
friend constexpr BoostDecimal
|
||||
operator/(BoostDecimal const& x, BoostDecimal const& y) noexcept
|
||||
{
|
||||
return BoostDecimal{x.value_ / y.value_};
|
||||
}
|
||||
|
||||
friend constexpr bool
|
||||
operator==(BoostDecimal const& x, BoostDecimal const& y) noexcept
|
||||
{
|
||||
return x.value_ == y.value_;
|
||||
}
|
||||
|
||||
friend constexpr bool
|
||||
operator<(BoostDecimal const& x, BoostDecimal const& y) noexcept
|
||||
{
|
||||
return x.value_ < y.value_;
|
||||
}
|
||||
|
||||
friend constexpr bool
|
||||
operator>(BoostDecimal const& x, BoostDecimal const& y) noexcept
|
||||
{
|
||||
return y < x;
|
||||
}
|
||||
|
||||
friend constexpr bool
|
||||
operator<=(BoostDecimal const& x, BoostDecimal const& y) noexcept
|
||||
{
|
||||
return !(y < x);
|
||||
}
|
||||
|
||||
friend constexpr bool
|
||||
operator>=(BoostDecimal const& x, BoostDecimal const& y) noexcept
|
||||
{
|
||||
return !(x < y);
|
||||
}
|
||||
|
||||
// Round to an integer using the current rounding mode, like Number.
|
||||
explicit
|
||||
operator rep() const noexcept
|
||||
{
|
||||
// Boost 1.91's llrint mishandles values that are already integers at full
|
||||
// precision (significand exponent >= 0): it returns a wrong result, or
|
||||
// divides by zero when the exponent is exactly 0. Fixed upstream (the
|
||||
// same early return as here); remove this once Boost includes the fix.
|
||||
int exponent = 0;
|
||||
boost::decimal::frexp10(value_, &exponent);
|
||||
if (exponent >= 0)
|
||||
return static_cast<rep>(value_);
|
||||
return static_cast<rep>(boost::decimal::llrint(value_));
|
||||
}
|
||||
|
||||
static Number::RoundingMode
|
||||
getround() noexcept
|
||||
{
|
||||
return detail::fromBoost(boost::decimal::fegetround());
|
||||
}
|
||||
|
||||
// Returns the previous mode, like Number::setround.
|
||||
static Number::RoundingMode
|
||||
setround(Number::RoundingMode mode) noexcept
|
||||
{
|
||||
auto const old = getround();
|
||||
boost::decimal::fesetround(detail::toBoost(mode));
|
||||
return old;
|
||||
}
|
||||
|
||||
friend std::string
|
||||
to_string(BoostDecimal const& x)
|
||||
{
|
||||
std::array<char, 64> buffer{};
|
||||
auto const result =
|
||||
boost::decimal::to_chars(buffer.data(), buffer.data() + buffer.size(), x.value_);
|
||||
return {buffer.data(), result.ptr};
|
||||
}
|
||||
|
||||
friend constexpr BoostDecimal
|
||||
abs(BoostDecimal const& x) noexcept
|
||||
{
|
||||
return BoostDecimal{boost::decimal::abs(x.value_)};
|
||||
}
|
||||
|
||||
friend constexpr BoostDecimal
|
||||
power(BoostDecimal const& x, unsigned n) noexcept
|
||||
{
|
||||
return BoostDecimal{boost::decimal::pow(x.value_, n)};
|
||||
}
|
||||
|
||||
friend constexpr BoostDecimal
|
||||
root2(BoostDecimal const& x) noexcept
|
||||
{
|
||||
return BoostDecimal{boost::decimal::sqrt(x.value_)};
|
||||
}
|
||||
|
||||
friend constexpr Decomposed
|
||||
decompose(BoostDecimal const& x) noexcept
|
||||
{
|
||||
int exponent = 0;
|
||||
auto const coefficient = boost::decimal::frexp10(x.value_, &exponent);
|
||||
return {
|
||||
.negative = boost::decimal::signbit(x.value_),
|
||||
.coefficient = static_cast<unsigned __int128>(coefficient),
|
||||
.exponent = exponent};
|
||||
}
|
||||
};
|
||||
|
||||
using BoostDecimal64 = BoostDecimal<boost::decimal::decimal64_t>;
|
||||
using BoostDecimal128 = BoostDecimal<boost::decimal::decimal128_t>;
|
||||
using BoostDecimalFast64 = BoostDecimal<boost::decimal::decimal_fast64_t>;
|
||||
using BoostDecimalFast128 = BoostDecimal<boost::decimal::decimal_fast128_t>;
|
||||
|
||||
static_assert(DecomposableNumber<BoostDecimal64>);
|
||||
static_assert(DecomposableNumber<BoostDecimal128>);
|
||||
static_assert(DecomposableNumber<BoostDecimalFast64>);
|
||||
static_assert(DecomposableNumber<BoostDecimalFast128>);
|
||||
|
||||
} // namespace xrpl::number_bench
|
||||
200
src/benchmarks/libxrpl/number/Double.h
Normal file
200
src/benchmarks/libxrpl/number/Double.h
Normal file
@@ -0,0 +1,200 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
|
||||
#include <benchmarks/libxrpl/number/NumberLike.h>
|
||||
|
||||
#include <cfenv>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::number_bench {
|
||||
|
||||
/**
|
||||
* Binary `double` behind Number's interface: a hardware speed-of-light
|
||||
* baseline, not a candidate (base 2, 53-bit significand).
|
||||
*
|
||||
* It is not DecomposableNumber because a binary value has no exact short
|
||||
* decimal coefficient.
|
||||
*/
|
||||
class Double final
|
||||
{
|
||||
double value_ = 0.0;
|
||||
|
||||
explicit constexpr Double(double value) noexcept : value_{value}
|
||||
{
|
||||
}
|
||||
|
||||
public:
|
||||
using rep = std::int64_t;
|
||||
|
||||
constexpr Double() = default;
|
||||
|
||||
// Implicit, like Number(rep).
|
||||
Double(rep mantissa) noexcept : value_{static_cast<double>(mantissa)} // NOLINT
|
||||
{
|
||||
}
|
||||
|
||||
explicit Double(rep mantissa, int exponent) noexcept
|
||||
: value_{static_cast<double>(mantissa) * std::pow(10.0, exponent)}
|
||||
{
|
||||
}
|
||||
|
||||
constexpr Double
|
||||
operator-() const noexcept
|
||||
{
|
||||
return Double{-value_};
|
||||
}
|
||||
|
||||
constexpr Double&
|
||||
operator+=(Double const& x) noexcept
|
||||
{
|
||||
value_ += x.value_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
constexpr Double&
|
||||
operator-=(Double const& x) noexcept
|
||||
{
|
||||
value_ -= x.value_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
constexpr Double&
|
||||
operator*=(Double const& x) noexcept
|
||||
{
|
||||
value_ *= x.value_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
constexpr Double&
|
||||
operator/=(Double const& x) noexcept
|
||||
{
|
||||
value_ /= x.value_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
friend constexpr Double
|
||||
operator+(Double const& x, Double const& y) noexcept
|
||||
{
|
||||
return Double{x.value_ + y.value_};
|
||||
}
|
||||
|
||||
friend constexpr Double
|
||||
operator-(Double const& x, Double const& y) noexcept
|
||||
{
|
||||
return Double{x.value_ - y.value_};
|
||||
}
|
||||
|
||||
friend constexpr Double
|
||||
operator*(Double const& x, Double const& y) noexcept
|
||||
{
|
||||
return Double{x.value_ * y.value_};
|
||||
}
|
||||
|
||||
friend constexpr Double
|
||||
operator/(Double const& x, Double const& y) noexcept
|
||||
{
|
||||
return Double{x.value_ / y.value_};
|
||||
}
|
||||
|
||||
friend constexpr bool
|
||||
operator==(Double const& x, Double const& y) noexcept
|
||||
{
|
||||
return x.value_ == y.value_;
|
||||
}
|
||||
|
||||
friend constexpr auto
|
||||
operator<=>(Double const& x, Double const& y) noexcept
|
||||
{
|
||||
return x.value_ <=> y.value_;
|
||||
}
|
||||
|
||||
explicit
|
||||
operator rep() const noexcept
|
||||
{
|
||||
return static_cast<rep>(std::llrint(value_));
|
||||
}
|
||||
|
||||
static Number::RoundingMode
|
||||
getround() noexcept
|
||||
{
|
||||
using enum Number::RoundingMode;
|
||||
switch (std::fegetround())
|
||||
{
|
||||
case FE_TOWARDZERO:
|
||||
return TowardsZero;
|
||||
case FE_DOWNWARD:
|
||||
return Downward;
|
||||
case FE_UPWARD:
|
||||
return Upward;
|
||||
default:
|
||||
return ToNearest;
|
||||
}
|
||||
}
|
||||
|
||||
static Number::RoundingMode
|
||||
setround(Number::RoundingMode mode) noexcept
|
||||
{
|
||||
using enum Number::RoundingMode;
|
||||
auto const old = getround();
|
||||
switch (mode)
|
||||
{
|
||||
case ToNearest:
|
||||
std::fesetround(FE_TONEAREST);
|
||||
break;
|
||||
case TowardsZero:
|
||||
std::fesetround(FE_TOWARDZERO);
|
||||
break;
|
||||
case Downward:
|
||||
std::fesetround(FE_DOWNWARD);
|
||||
break;
|
||||
case Upward:
|
||||
std::fesetround(FE_UPWARD);
|
||||
break;
|
||||
}
|
||||
return old;
|
||||
}
|
||||
|
||||
friend std::string
|
||||
to_string(Double const& x)
|
||||
{
|
||||
std::ostringstream os;
|
||||
os.precision(17);
|
||||
os << x.value_;
|
||||
return os.str();
|
||||
}
|
||||
|
||||
friend Double
|
||||
abs(Double const& x) noexcept
|
||||
{
|
||||
return Double{std::fabs(x.value_)};
|
||||
}
|
||||
|
||||
// Repeated squaring, the same algorithm as xrpl::power(Number, unsigned).
|
||||
friend constexpr Double
|
||||
power(Double const& x, unsigned n) noexcept
|
||||
{
|
||||
double result = 1.0;
|
||||
double base = x.value_;
|
||||
for (; n != 0; n >>= 1)
|
||||
{
|
||||
if ((n & 1u) != 0)
|
||||
result *= base;
|
||||
base *= base;
|
||||
}
|
||||
return Double{result};
|
||||
}
|
||||
|
||||
friend Double
|
||||
root2(Double const& x) noexcept
|
||||
{
|
||||
return Double{std::sqrt(x.value_)};
|
||||
}
|
||||
};
|
||||
|
||||
static_assert(NumberLike<Double>);
|
||||
|
||||
} // namespace xrpl::number_bench
|
||||
377
src/benchmarks/libxrpl/number/IntelBid.h
Normal file
377
src/benchmarks/libxrpl/number/IntelBid.h
Normal file
@@ -0,0 +1,377 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
|
||||
#include <benchmarks/libxrpl/number/NumberLike.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
// The library must be built with the same three settings (see the
|
||||
// bench_intel_dfp block in src/benchmarks/libxrpl/CMakeLists.txt): arguments
|
||||
// by value, rounding mode passed to each call, status flags passed to each
|
||||
// call. That is the configuration closest to Number, whose rounding mode is a
|
||||
// per-thread setting rather than process-global state.
|
||||
#define DECIMAL_CALL_BY_REFERENCE 0 // NOLINT(cppcoreguidelines-macro-usage)
|
||||
#define DECIMAL_GLOBAL_ROUNDING 0 // NOLINT(cppcoreguidelines-macro-usage)
|
||||
#define DECIMAL_GLOBAL_EXCEPTION_FLAGS 0 // NOLINT(cppcoreguidelines-macro-usage)
|
||||
#include <bid_conf.h>
|
||||
#include <bid_functions.h>
|
||||
|
||||
namespace xrpl::number_bench {
|
||||
|
||||
namespace detail {
|
||||
|
||||
/**
|
||||
* One function table per interchange width, so IntelBid is written once.
|
||||
*/
|
||||
struct Bid64Ops
|
||||
{
|
||||
using Value = BID_UINT64;
|
||||
static constexpr unsigned kDigits = 16;
|
||||
static constexpr int kBias = 398;
|
||||
|
||||
// clang-format off
|
||||
static Value add(Value x, Value y, _IDEC_round r, _IDEC_flags* f) { return bid64_add(x, y, r, f); }
|
||||
static Value sub(Value x, Value y, _IDEC_round r, _IDEC_flags* f) { return bid64_sub(x, y, r, f); }
|
||||
static Value mul(Value x, Value y, _IDEC_round r, _IDEC_flags* f) { return bid64_mul(x, y, r, f); }
|
||||
static Value div(Value x, Value y, _IDEC_round r, _IDEC_flags* f) { return bid64_div(x, y, r, f); }
|
||||
static Value sqrt(Value x, _IDEC_round r, _IDEC_flags* f) { return bid64_sqrt(x, r, f); }
|
||||
static Value pow(Value x, Value y, _IDEC_round r, _IDEC_flags* f) { return bid64_pow(x, y, r, f); }
|
||||
static Value fromInt64(std::int64_t x, _IDEC_round r, _IDEC_flags* f) { return bid64_from_int64(x, r, f); }
|
||||
static Value scalbn(Value x, int n, _IDEC_round r, _IDEC_flags* f) { return bid64_scalbn(x, n, r, f); }
|
||||
static Value negate(Value x) { return bid64_negate(x); }
|
||||
static Value abs(Value x) { return bid64_abs(x); }
|
||||
static bool less(Value x, Value y, _IDEC_flags* f) { return bid64_quiet_less(x, y, f) != 0; }
|
||||
static bool equal(Value x, Value y, _IDEC_flags* f) { return bid64_quiet_equal(x, y, f) != 0; }
|
||||
static std::int64_t toInt64Nearest(Value x, _IDEC_flags* f) { return bid64_to_int64_rnint(x, f); }
|
||||
static std::int64_t toInt64TowardZero(Value x, _IDEC_flags* f) { return bid64_to_int64_int(x, f); }
|
||||
static std::int64_t toInt64Floor(Value x, _IDEC_flags* f) { return bid64_to_int64_floor(x, f); }
|
||||
static std::int64_t toInt64Ceil(Value x, _IDEC_flags* f) { return bid64_to_int64_ceil(x, f); }
|
||||
static void toString(char* s, Value x, _IDEC_flags* f) { bid64_to_string(s, x, f); }
|
||||
// clang-format on
|
||||
|
||||
/**
|
||||
* Decodes the BID64 layout. Precondition: finite and canonical.
|
||||
*/
|
||||
static Decomposed
|
||||
decompose(Value x) noexcept
|
||||
{
|
||||
bool const negative = (x >> 63) != 0;
|
||||
bool const largeForm = ((x >> 61) & 3) == 3;
|
||||
auto const exponent = static_cast<int>(largeForm ? (x >> 51) & 0x3ff : (x >> 53) & 0x3ff);
|
||||
auto const coefficient = largeForm
|
||||
? (x & 0x0007'ffff'ffff'ffffULL) | 0x0020'0000'0000'0000ULL
|
||||
: x & 0x001f'ffff'ffff'ffffULL;
|
||||
return {.negative = negative, .coefficient = coefficient, .exponent = exponent - kBias};
|
||||
}
|
||||
};
|
||||
|
||||
struct Bid128Ops
|
||||
{
|
||||
using Value = BID_UINT128;
|
||||
static constexpr unsigned kDigits = 34;
|
||||
static constexpr int kBias = 6176;
|
||||
|
||||
// clang-format off
|
||||
static Value add(Value x, Value y, _IDEC_round r, _IDEC_flags* f) { return bid128_add(x, y, r, f); }
|
||||
static Value sub(Value x, Value y, _IDEC_round r, _IDEC_flags* f) { return bid128_sub(x, y, r, f); }
|
||||
static Value mul(Value x, Value y, _IDEC_round r, _IDEC_flags* f) { return bid128_mul(x, y, r, f); }
|
||||
static Value div(Value x, Value y, _IDEC_round r, _IDEC_flags* f) { return bid128_div(x, y, r, f); }
|
||||
static Value sqrt(Value x, _IDEC_round r, _IDEC_flags* f) { return bid128_sqrt(x, r, f); }
|
||||
static Value pow(Value x, Value y, _IDEC_round r, _IDEC_flags* f) { return bid128_pow(x, y, r, f); }
|
||||
static Value fromInt64(std::int64_t x, _IDEC_round, _IDEC_flags*) { return bid128_from_int64(x); }
|
||||
static Value scalbn(Value x, int n, _IDEC_round r, _IDEC_flags* f) { return bid128_scalbn(x, n, r, f); }
|
||||
static Value negate(Value x) { return bid128_negate(x); }
|
||||
static Value abs(Value x) { return bid128_abs(x); }
|
||||
static bool less(Value x, Value y, _IDEC_flags* f) { return bid128_quiet_less(x, y, f) != 0; }
|
||||
static bool equal(Value x, Value y, _IDEC_flags* f) { return bid128_quiet_equal(x, y, f) != 0; }
|
||||
static std::int64_t toInt64Nearest(Value x, _IDEC_flags* f) { return bid128_to_int64_rnint(x, f); }
|
||||
static std::int64_t toInt64TowardZero(Value x, _IDEC_flags* f) { return bid128_to_int64_int(x, f); }
|
||||
static std::int64_t toInt64Floor(Value x, _IDEC_flags* f) { return bid128_to_int64_floor(x, f); }
|
||||
static std::int64_t toInt64Ceil(Value x, _IDEC_flags* f) { return bid128_to_int64_ceil(x, f); }
|
||||
static void toString(char* s, Value x, _IDEC_flags* f) { bid128_to_string(s, x, f); }
|
||||
// clang-format on
|
||||
|
||||
/**
|
||||
* Decodes the BID128 layout. Precondition: finite and canonical.
|
||||
*/
|
||||
static Decomposed
|
||||
decompose(Value x) noexcept
|
||||
{
|
||||
auto const hi = x.w[BID_HIGH_128W];
|
||||
auto const lo = x.w[BID_LOW_128W];
|
||||
bool const negative = (hi >> 63) != 0;
|
||||
auto const exponent = static_cast<int>((hi >> 49) & 0x3fff);
|
||||
auto const coefficient =
|
||||
(static_cast<unsigned __int128>(hi & 0x0001'ffff'ffff'ffffULL) << 64) | lo;
|
||||
return {.negative = negative, .coefficient = coefficient, .exponent = exponent - kBias};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/**
|
||||
* Intel's Decimal Floating-Point Math Library (BID encoding) behind Number's
|
||||
* interface.
|
||||
*
|
||||
* The rounding mode is thread-local, like Number's, and passed to each call.
|
||||
* Status flags from every call accumulate in a thread-local; read and clear
|
||||
* them with `takeStatus()`.
|
||||
*
|
||||
* Like the other IEEE types, overflow produces infinity and division by zero
|
||||
* produces inf/NaN instead of throwing; decimal64 rounds 19-digit int64
|
||||
* mantissas to 16 digits on construction.
|
||||
*/
|
||||
template <class Ops>
|
||||
class IntelBid final
|
||||
{
|
||||
using Value = typename Ops::Value;
|
||||
|
||||
Value value_{};
|
||||
|
||||
struct State
|
||||
{
|
||||
_IDEC_round round = BID_ROUNDING_TO_NEAREST;
|
||||
_IDEC_flags flags = 0;
|
||||
};
|
||||
|
||||
static State&
|
||||
state() noexcept
|
||||
{
|
||||
static thread_local State s;
|
||||
return s;
|
||||
}
|
||||
|
||||
static IntelBid
|
||||
wrap(Value v) noexcept
|
||||
{
|
||||
IntelBid result;
|
||||
result.value_ = v;
|
||||
return result;
|
||||
}
|
||||
|
||||
public:
|
||||
using rep = std::int64_t;
|
||||
|
||||
IntelBid() : value_{Ops::fromInt64(0, BID_ROUNDING_TO_NEAREST, &state().flags)}
|
||||
{
|
||||
}
|
||||
|
||||
// Implicit, like Number(rep).
|
||||
IntelBid(rep mantissa) : IntelBid(mantissa, 0) // NOLINT
|
||||
{
|
||||
}
|
||||
|
||||
explicit IntelBid(rep mantissa, int exponent)
|
||||
{
|
||||
auto& s = state();
|
||||
value_ = Ops::fromInt64(mantissa, s.round, &s.flags);
|
||||
if (exponent != 0)
|
||||
value_ = Ops::scalbn(value_, exponent, s.round, &s.flags);
|
||||
}
|
||||
|
||||
IntelBid
|
||||
operator-() const noexcept
|
||||
{
|
||||
return wrap(Ops::negate(value_));
|
||||
}
|
||||
|
||||
friend IntelBid
|
||||
operator+(IntelBid const& x, IntelBid const& y) noexcept
|
||||
{
|
||||
auto& s = state();
|
||||
return wrap(Ops::add(x.value_, y.value_, s.round, &s.flags));
|
||||
}
|
||||
|
||||
friend IntelBid
|
||||
operator-(IntelBid const& x, IntelBid const& y) noexcept
|
||||
{
|
||||
auto& s = state();
|
||||
return wrap(Ops::sub(x.value_, y.value_, s.round, &s.flags));
|
||||
}
|
||||
|
||||
friend IntelBid
|
||||
operator*(IntelBid const& x, IntelBid const& y) noexcept
|
||||
{
|
||||
auto& s = state();
|
||||
return wrap(Ops::mul(x.value_, y.value_, s.round, &s.flags));
|
||||
}
|
||||
|
||||
friend IntelBid
|
||||
operator/(IntelBid const& x, IntelBid const& y) noexcept
|
||||
{
|
||||
auto& s = state();
|
||||
return wrap(Ops::div(x.value_, y.value_, s.round, &s.flags));
|
||||
}
|
||||
|
||||
IntelBid&
|
||||
operator+=(IntelBid const& x) noexcept
|
||||
{
|
||||
return *this = *this + x;
|
||||
}
|
||||
|
||||
IntelBid&
|
||||
operator-=(IntelBid const& x) noexcept
|
||||
{
|
||||
return *this = *this - x;
|
||||
}
|
||||
|
||||
IntelBid&
|
||||
operator*=(IntelBid const& x) noexcept
|
||||
{
|
||||
return *this = *this * x;
|
||||
}
|
||||
|
||||
IntelBid&
|
||||
operator/=(IntelBid const& x) noexcept
|
||||
{
|
||||
return *this = *this / x;
|
||||
}
|
||||
|
||||
friend bool
|
||||
operator==(IntelBid const& x, IntelBid const& y) noexcept
|
||||
{
|
||||
return Ops::equal(x.value_, y.value_, &state().flags);
|
||||
}
|
||||
|
||||
friend bool
|
||||
operator<(IntelBid const& x, IntelBid const& y) noexcept
|
||||
{
|
||||
return Ops::less(x.value_, y.value_, &state().flags);
|
||||
}
|
||||
|
||||
friend bool
|
||||
operator>(IntelBid const& x, IntelBid const& y) noexcept
|
||||
{
|
||||
return y < x;
|
||||
}
|
||||
|
||||
friend bool
|
||||
operator<=(IntelBid const& x, IntelBid const& y) noexcept
|
||||
{
|
||||
return !(y < x);
|
||||
}
|
||||
|
||||
friend bool
|
||||
operator>=(IntelBid const& x, IntelBid const& y) noexcept
|
||||
{
|
||||
return !(x < y);
|
||||
}
|
||||
|
||||
// Round to an integer using the current rounding mode, like Number.
|
||||
explicit
|
||||
operator rep() const noexcept
|
||||
{
|
||||
auto& s = state();
|
||||
switch (s.round)
|
||||
{
|
||||
case BID_ROUNDING_TO_ZERO:
|
||||
return Ops::toInt64TowardZero(value_, &s.flags);
|
||||
case BID_ROUNDING_DOWN:
|
||||
return Ops::toInt64Floor(value_, &s.flags);
|
||||
case BID_ROUNDING_UP:
|
||||
return Ops::toInt64Ceil(value_, &s.flags);
|
||||
default:
|
||||
return Ops::toInt64Nearest(value_, &s.flags);
|
||||
}
|
||||
}
|
||||
|
||||
static Number::RoundingMode
|
||||
getround() noexcept
|
||||
{
|
||||
using enum Number::RoundingMode;
|
||||
switch (state().round)
|
||||
{
|
||||
case BID_ROUNDING_TO_ZERO:
|
||||
return TowardsZero;
|
||||
case BID_ROUNDING_DOWN:
|
||||
return Downward;
|
||||
case BID_ROUNDING_UP:
|
||||
return Upward;
|
||||
default:
|
||||
return ToNearest;
|
||||
}
|
||||
}
|
||||
|
||||
static Number::RoundingMode
|
||||
setround(Number::RoundingMode mode) noexcept
|
||||
{
|
||||
using enum Number::RoundingMode;
|
||||
auto const old = getround();
|
||||
auto& round = state().round;
|
||||
switch (mode)
|
||||
{
|
||||
case ToNearest:
|
||||
round = BID_ROUNDING_TO_NEAREST;
|
||||
break;
|
||||
case TowardsZero:
|
||||
round = BID_ROUNDING_TO_ZERO;
|
||||
break;
|
||||
case Downward:
|
||||
round = BID_ROUNDING_DOWN;
|
||||
break;
|
||||
case Upward:
|
||||
round = BID_ROUNDING_UP;
|
||||
break;
|
||||
}
|
||||
return old;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the status flags raised since the last call, and clears them.
|
||||
*/
|
||||
static _IDEC_flags
|
||||
takeStatus() noexcept
|
||||
{
|
||||
return std::exchange(state().flags, 0);
|
||||
}
|
||||
|
||||
friend std::string
|
||||
to_string(IntelBid const& x)
|
||||
{
|
||||
std::array<char, 64> buffer{};
|
||||
Ops::toString(buffer.data(), x.value_, &state().flags);
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
friend IntelBid
|
||||
abs(IntelBid const& x) noexcept
|
||||
{
|
||||
return wrap(Ops::abs(x.value_));
|
||||
}
|
||||
|
||||
friend IntelBid
|
||||
power(IntelBid const& x, unsigned n)
|
||||
{
|
||||
auto& s = state();
|
||||
IntelBid const exponent{static_cast<rep>(n)};
|
||||
return wrap(Ops::pow(x.value_, exponent.value_, s.round, &s.flags));
|
||||
}
|
||||
|
||||
friend IntelBid
|
||||
root2(IntelBid const& x) noexcept
|
||||
{
|
||||
auto& s = state();
|
||||
return wrap(Ops::sqrt(x.value_, s.round, &s.flags));
|
||||
}
|
||||
|
||||
/**
|
||||
* Precondition: x is finite.
|
||||
*/
|
||||
friend Decomposed
|
||||
decompose(IntelBid const& x) noexcept
|
||||
{
|
||||
return Ops::decompose(x.value_);
|
||||
}
|
||||
};
|
||||
|
||||
using IntelBid64 = IntelBid<detail::Bid64Ops>;
|
||||
using IntelBid128 = IntelBid<detail::Bid128Ops>;
|
||||
|
||||
static_assert(DecomposableNumber<IntelBid64>);
|
||||
static_assert(DecomposableNumber<IntelBid128>);
|
||||
|
||||
} // namespace xrpl::number_bench
|
||||
355
src/benchmarks/libxrpl/number/Kernels.cpp
Normal file
355
src/benchmarks/libxrpl/number/Kernels.cpp
Normal file
@@ -0,0 +1,355 @@
|
||||
// Ledger-formula benchmarks: AMM swaps, vault share conversion, and loan
|
||||
// amortization (see Kernels.h), run identically on every NumberLike type.
|
||||
// See docs/NumberDecimalBenchmark.md.
|
||||
//
|
||||
// Benchmark names are "kernel/<kernel>/<type>". Each reports throughput and,
|
||||
// when built with mpdecimal (bench_mpdecimal), accuracy counters comparing the
|
||||
// type's result with the same formula evaluated at 96 digits from the type's
|
||||
// own inputs, so construction rounding is excluded:
|
||||
//
|
||||
// digits_min / digits_median correct significant digits, -log10(relative
|
||||
// error), worst case and median over the
|
||||
// cases; 40 means exact. For loan_amortize the
|
||||
// error is the final balance relative to the
|
||||
// principal.
|
||||
// mismatches for integer results: cases that differ from
|
||||
// the exact truncated result.
|
||||
|
||||
#include <benchmarks/libxrpl/number/Kernels.h>
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
|
||||
#include <benchmark/benchmark.h>
|
||||
#include <benchmarks/libxrpl/number/BoostDecimal.h>
|
||||
#include <benchmarks/libxrpl/number/Double.h>
|
||||
#include <benchmarks/libxrpl/number/Number128.h>
|
||||
#include <benchmarks/libxrpl/number/NumberLike.h>
|
||||
#include <benchmarks/libxrpl/number/Operands.h>
|
||||
|
||||
#ifdef XRPL_BENCH_MPDECIMAL
|
||||
#include <benchmarks/libxrpl/number/MpDecimal.h>
|
||||
#endif
|
||||
#ifdef XRPL_BENCH_INTEL_DFP
|
||||
#include <benchmarks/libxrpl/number/IntelBid.h>
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <format>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::number_bench {
|
||||
namespace {
|
||||
|
||||
constexpr std::size_t kCases = 256;
|
||||
constexpr std::uint64_t kSeed = 0x6b65'726e;
|
||||
|
||||
/**
|
||||
* Inputs for one kernel evaluation: up to three decimal values and two
|
||||
* integer parameters (fee, rate, interval, payment count).
|
||||
*/
|
||||
struct KernelCase
|
||||
{
|
||||
std::array<RawOperand, 3> values;
|
||||
std::array<std::uint32_t, 3> integers;
|
||||
};
|
||||
|
||||
template <class T>
|
||||
using Values = std::array<T, 3>;
|
||||
|
||||
// ---- Case generation ----
|
||||
|
||||
/**
|
||||
* A positive value with a 16-digit mantissa and magnitude 10^lo to 10^hi.
|
||||
*/
|
||||
RawOperand
|
||||
amount(std::mt19937_64& rng, int lo, int hi)
|
||||
{
|
||||
constexpr auto kE15 = static_cast<std::int64_t>(kPowerOfTen[15]);
|
||||
auto const mantissa = std::uniform_int_distribution<std::int64_t>{kE15, (kE15 * 10) - 1}(rng);
|
||||
return {mantissa, std::uniform_int_distribution<int>{lo, hi}(rng)-15};
|
||||
}
|
||||
|
||||
/**
|
||||
* `base` scaled by a factor 10^lo to 10^hi, with a fresh mantissa.
|
||||
*/
|
||||
RawOperand
|
||||
fractionOf(std::mt19937_64& rng, RawOperand base, int lo, int hi)
|
||||
{
|
||||
auto result = amount(rng, 0, 0);
|
||||
result.exponent = base.exponent + std::uniform_int_distribution<int>{lo, hi}(rng);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pools of 10^6 to 10^12 per side, trades of 10^-6 to 10^-1 of the pool,
|
||||
* fees of 0 to 1%.
|
||||
*/
|
||||
std::vector<KernelCase>
|
||||
ammCases()
|
||||
{
|
||||
std::mt19937_64 rng{kSeed};
|
||||
std::vector<KernelCase> cases;
|
||||
for (std::size_t i = 0; i < kCases; ++i)
|
||||
{
|
||||
auto const poolIn = amount(rng, 6, 12);
|
||||
auto const poolOut = amount(rng, 6, 12);
|
||||
// Trades are a fraction of the out pool for swap-out to stay solvent.
|
||||
auto const trade = fractionOf(rng, poolOut, -6, -2);
|
||||
auto const fee = std::uniform_int_distribution<std::uint32_t>{0, 1000}(rng);
|
||||
cases.push_back({{poolIn, poolOut, trade}, {fee, 0, 0}});
|
||||
}
|
||||
return cases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vaults holding 10^9 to 10^17 drops-like integer assets, share supply 10^6
|
||||
* times larger (vault scale 6) within a factor of 2, but at most 10^18 since
|
||||
* MPT supply is capped at 2^63-1; deposits of 10^-6 to 10^-1 of the assets.
|
||||
*/
|
||||
std::vector<KernelCase>
|
||||
vaultCases()
|
||||
{
|
||||
std::mt19937_64 rng{kSeed + 1};
|
||||
std::vector<KernelCase> cases;
|
||||
for (std::size_t i = 0; i < kCases; ++i)
|
||||
{
|
||||
auto const assetDigits = std::uniform_int_distribution<int>{10, 17}(rng);
|
||||
auto const assetTotal = std::uniform_int_distribution<std::int64_t>{
|
||||
static_cast<std::int64_t>(kPowerOfTen[assetDigits - 1]),
|
||||
static_cast<std::int64_t>(kPowerOfTen[assetDigits]) - 1}(rng);
|
||||
auto const ratio = std::uniform_real_distribution<double>{0.5, 2.0}(rng);
|
||||
auto const shareMantissa =
|
||||
static_cast<std::int64_t>(static_cast<double>(assetTotal) * ratio) | 1;
|
||||
auto const shareExponent = std::min(6, 18 - assetDigits);
|
||||
auto const deposit = std::uniform_int_distribution<std::int64_t>{
|
||||
std::max<std::int64_t>(1, assetTotal / 1'000'000), assetTotal / 10}(rng);
|
||||
cases.push_back(
|
||||
{{RawOperand{shareMantissa, shareExponent},
|
||||
RawOperand{assetTotal, 0},
|
||||
RawOperand{deposit, 0}},
|
||||
{}});
|
||||
}
|
||||
return cases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Principal of 10^3 to 10^9, annual rates of 0.1% to 30% (in 1/10 bips),
|
||||
* monthly payments.
|
||||
*/
|
||||
std::vector<KernelCase>
|
||||
loanCases(std::uint32_t payments)
|
||||
{
|
||||
constexpr std::uint32_t kMonth = 30 * 24 * 60 * 60;
|
||||
std::mt19937_64 rng{kSeed + 2 + payments};
|
||||
std::vector<KernelCase> cases;
|
||||
for (std::size_t i = 0; i < kCases; ++i)
|
||||
{
|
||||
auto const principal = amount(rng, 3, 9);
|
||||
auto const rate = std::uniform_int_distribution<std::uint32_t>{100, 30'000}(rng);
|
||||
cases.push_back(
|
||||
{{principal, RawOperand{0, 0}, RawOperand{0, 0}}, {rate, kMonth, payments}});
|
||||
}
|
||||
return cases;
|
||||
}
|
||||
|
||||
// ---- Kernels, adapted to the common case shape ----
|
||||
|
||||
auto const kAmmSwapIn = [](auto const& v, auto const& i) {
|
||||
return ammSwapIn(v[0], v[1], v[2], static_cast<std::uint16_t>(i[0]));
|
||||
};
|
||||
auto const kAmmSwapOut = [](auto const& v, auto const& i) {
|
||||
return ammSwapOut(v[0], v[1], v[2], static_cast<std::uint16_t>(i[0]));
|
||||
};
|
||||
auto const kVaultAssetsToShares = [](auto const& v, auto const&) {
|
||||
return vaultAssetsToShares(v[0], v[1], v[2]);
|
||||
};
|
||||
auto const kVaultSharesToAssets = [](auto const& v, auto const&) {
|
||||
// Redeem the same magnitude of shares as the deposit's asset amount.
|
||||
return vaultSharesToAssets(v[1], v[0], v[2]);
|
||||
};
|
||||
auto const kLoanPayment = [](auto const& v, auto const& i) {
|
||||
using T = std::decay_t<decltype(v[0])>;
|
||||
return loanPeriodicPayment(v[0], loanPeriodicRate<T>(i[0], i[1]), i[2]);
|
||||
};
|
||||
auto const kLoanAmortize = [](auto const& v, auto const& i) {
|
||||
using T = std::decay_t<decltype(v[0])>;
|
||||
return loanAmortize(v[0], loanPeriodicRate<T>(i[0], i[1]), i[2]);
|
||||
};
|
||||
|
||||
// ---- Accuracy against a 96-digit evaluation ----
|
||||
|
||||
#ifdef XRPL_BENCH_MPDECIMAL
|
||||
using Exact = MpDecimal<96>;
|
||||
|
||||
template <NumberLike T>
|
||||
Exact
|
||||
toExact(T const& x)
|
||||
{
|
||||
if constexpr (DecomposableNumber<T>)
|
||||
{
|
||||
return Exact::fromDecomposed(decompose(x));
|
||||
}
|
||||
else
|
||||
{
|
||||
Exact result;
|
||||
mpd_context_t ctx;
|
||||
mpd_maxcontext(&ctx);
|
||||
std::uint32_t status = 0;
|
||||
mpd_qset_string(result.get(), to_string(x).c_str(), &ctx, &status);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
double
|
||||
correctDigits(Exact const& error, Exact const& scale)
|
||||
{
|
||||
constexpr double kExact = 40;
|
||||
if (mpd_iszero(error.get()) != 0)
|
||||
return kExact;
|
||||
auto const relative = std::stod(to_string(abs(error) / abs(scale)));
|
||||
return std::min(kExact, -std::log10(relative));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets digits_min/digits_median (or mismatches) on `state`.
|
||||
*/
|
||||
template <NumberLike T, class Fn>
|
||||
void
|
||||
measureAccuracy(
|
||||
benchmark::State& state,
|
||||
std::vector<Values<T>> const& inputs,
|
||||
std::vector<KernelCase> const& cases,
|
||||
Fn fn,
|
||||
bool relativeToFirstInput)
|
||||
{
|
||||
std::vector<double> digits;
|
||||
std::size_t mismatches = 0;
|
||||
for (std::size_t c = 0; c < inputs.size(); ++c)
|
||||
{
|
||||
Values<Exact> const exactInputs{
|
||||
toExact(inputs[c][0]), toExact(inputs[c][1]), toExact(inputs[c][2])};
|
||||
auto const result = fn(inputs[c], cases[c].integers);
|
||||
auto const exact = fn(exactInputs, cases[c].integers);
|
||||
if constexpr (std::is_integral_v<std::decay_t<decltype(result)>>)
|
||||
{
|
||||
mismatches += result == exact ? 0 : 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const& scale = relativeToFirstInput ? exactInputs[0] : exact;
|
||||
digits.push_back(correctDigits(toExact(result) - exact, scale));
|
||||
}
|
||||
}
|
||||
if (digits.empty())
|
||||
{
|
||||
state.counters["mismatches"] = static_cast<double>(mismatches);
|
||||
return;
|
||||
}
|
||||
std::ranges::sort(digits);
|
||||
state.counters["digits_min"] = digits.front();
|
||||
state.counters["digits_median"] = digits[digits.size() / 2];
|
||||
}
|
||||
#endif
|
||||
|
||||
// ---- Benchmarks ----
|
||||
|
||||
struct NoEnv
|
||||
{
|
||||
};
|
||||
|
||||
template <MantissaRange::MantissaScale Scale>
|
||||
struct NumberScaleEnv
|
||||
{
|
||||
NumberMantissaScaleGuard guard{Scale};
|
||||
};
|
||||
|
||||
template <NumberLike T, class Env, class Fn>
|
||||
void
|
||||
registerKernel(
|
||||
std::string_view kernel,
|
||||
std::string_view subject,
|
||||
std::vector<KernelCase> const& cases,
|
||||
Fn fn,
|
||||
bool relativeToFirstInput = false)
|
||||
{
|
||||
benchmark::RegisterBenchmark(
|
||||
std::format("kernel/{}/{}", kernel, subject),
|
||||
[cases, fn, relativeToFirstInput](benchmark::State& state) {
|
||||
[[maybe_unused]] Env const env{};
|
||||
std::vector<Values<T>> inputs;
|
||||
inputs.reserve(cases.size());
|
||||
for (auto const& c : cases)
|
||||
{
|
||||
inputs.push_back(
|
||||
{T{c.values[0].mantissa, c.values[0].exponent},
|
||||
T{c.values[1].mantissa, c.values[1].exponent},
|
||||
T{c.values[2].mantissa, c.values[2].exponent}});
|
||||
}
|
||||
for (auto _ : state)
|
||||
{
|
||||
for (std::size_t c = 0; c < inputs.size(); ++c)
|
||||
{
|
||||
auto r = fn(inputs[c], cases[c].integers);
|
||||
benchmark::DoNotOptimize(r);
|
||||
}
|
||||
}
|
||||
state.SetItemsProcessed(static_cast<std::int64_t>(state.iterations() * inputs.size()));
|
||||
#ifdef XRPL_BENCH_MPDECIMAL
|
||||
measureAccuracy<T>(state, inputs, cases, fn, relativeToFirstInput);
|
||||
#else
|
||||
(void)relativeToFirstInput;
|
||||
#endif
|
||||
});
|
||||
}
|
||||
|
||||
template <NumberLike T, class Env>
|
||||
void
|
||||
registerSubject(std::string_view subject)
|
||||
{
|
||||
static auto const kAmm = ammCases();
|
||||
static auto const kVault = vaultCases();
|
||||
static auto const kLoan12 = loanCases(12);
|
||||
static auto const kLoan360 = loanCases(360);
|
||||
|
||||
registerKernel<T, Env>("amm_swap_in", subject, kAmm, kAmmSwapIn);
|
||||
registerKernel<T, Env>("amm_swap_out", subject, kAmm, kAmmSwapOut);
|
||||
registerKernel<T, Env>("vault_assets_to_shares", subject, kVault, kVaultAssetsToShares);
|
||||
registerKernel<T, Env>("vault_shares_to_assets", subject, kVault, kVaultSharesToAssets);
|
||||
registerKernel<T, Env>("loan_payment12", subject, kLoan12, kLoanPayment);
|
||||
registerKernel<T, Env>("loan_payment360", subject, kLoan360, kLoanPayment);
|
||||
registerKernel<T, Env>("loan_amortize12", subject, kLoan12, kLoanAmortize, true);
|
||||
registerKernel<T, Env>("loan_amortize360", subject, kLoan360, kLoanAmortize, true);
|
||||
}
|
||||
|
||||
[[maybe_unused]] bool const kRegistered = [] {
|
||||
using enum MantissaRange::MantissaScale;
|
||||
registerSubject<Number, NumberScaleEnv<Small>>("Number.Small");
|
||||
registerSubject<Number, NumberScaleEnv<Large330>>("Number.Large330");
|
||||
registerSubject<BoostDecimal64, NoEnv>("BoostDecimal64");
|
||||
registerSubject<BoostDecimal128, NoEnv>("BoostDecimal128");
|
||||
registerSubject<Double, NoEnv>("Double");
|
||||
#ifdef XRPL_BENCH_MPDECIMAL
|
||||
registerSubject<MpDecimal34, NoEnv>("MpDecimal34");
|
||||
registerSubject<MpDecimal38, NoEnv>("MpDecimal38");
|
||||
#endif
|
||||
#ifdef XRPL_BENCH_INTEL_DFP
|
||||
registerSubject<IntelBid64, NoEnv>("IntelBid64");
|
||||
registerSubject<IntelBid128, NoEnv>("IntelBid128");
|
||||
#endif
|
||||
#ifdef XRPL_BENCH_HAS_NUMBER128
|
||||
registerSubject<Number128, NoEnv>("Number128");
|
||||
#endif
|
||||
return true;
|
||||
}();
|
||||
|
||||
} // namespace
|
||||
} // namespace xrpl::number_bench
|
||||
220
src/benchmarks/libxrpl/number/Kernels.h
Normal file
220
src/benchmarks/libxrpl/number/Kernels.h
Normal file
@@ -0,0 +1,220 @@
|
||||
#pragma once
|
||||
|
||||
// Ledger formulas, written once against NumberLike so every library runs the
|
||||
// identical computation. Each kernel mirrors a production function; keep them
|
||||
// in sync if those change:
|
||||
//
|
||||
// ammSwapIn / ammSwapOut swapAssetIn / swapAssetOut, fixAMMv1_1 branch
|
||||
// (include/xrpl/ledger/helpers/AMMHelpers.h)
|
||||
// vaultAssetsToShares assetsToSharesDeposit
|
||||
// vaultSharesToAssets sharesToAssetsDeposit
|
||||
// (src/libxrpl/ledger/helpers/VaultHelpers.cpp)
|
||||
// loanPeriodicRate loanPeriodicRate
|
||||
// loanPowerMinusOne computePowerMinusOneHybrid
|
||||
// loanPeriodicPayment loanPeriodicPayment, fixCleanup3_2_0 branch
|
||||
// (src/libxrpl/ledger/helpers/LendingHelpers.cpp)
|
||||
//
|
||||
// loanAmortize is not a production function: it runs a full payment schedule
|
||||
// (interest = balance * rate; balance -= payment - interest) so error
|
||||
// accumulates the way it does over a loan's life. The exact remaining balance
|
||||
// after the last payment is 0.
|
||||
//
|
||||
// The kernels operate on bare values, not STAmount or ledger entries, and
|
||||
// omit the final rounding to the asset's scale, so they measure arithmetic
|
||||
// alone.
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
|
||||
#include <benchmarks/libxrpl/number/NumberLike.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl::number_bench {
|
||||
|
||||
namespace detail {
|
||||
|
||||
template <NumberLike T>
|
||||
T
|
||||
integer(std::int64_t value)
|
||||
{
|
||||
return T{value};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a rounding mode for its lifetime, then restores the previous one.
|
||||
*/
|
||||
template <NumberLike T>
|
||||
class RoundGuard
|
||||
{
|
||||
Number::RoundingMode saved_;
|
||||
|
||||
public:
|
||||
explicit RoundGuard(Number::RoundingMode mode) : saved_{T::setround(mode)}
|
||||
{
|
||||
}
|
||||
|
||||
~RoundGuard()
|
||||
{
|
||||
T::setround(saved_);
|
||||
}
|
||||
|
||||
RoundGuard(RoundGuard const&) = delete;
|
||||
RoundGuard&
|
||||
operator=(RoundGuard const&) = delete;
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/**
|
||||
* Amount of the out asset received for swapping `assetIn` into the pool.
|
||||
* Rounding at each step favors the AMM.
|
||||
*/
|
||||
template <NumberLike T>
|
||||
T
|
||||
ammSwapIn(T const& poolIn, T const& poolOut, T const& assetIn, std::uint16_t tfee)
|
||||
{
|
||||
using enum Number::RoundingMode;
|
||||
auto const saved = T::getround();
|
||||
|
||||
T::setround(Upward);
|
||||
auto const numerator = poolIn * poolOut;
|
||||
auto const fee = detail::integer<T>(tfee) / detail::integer<T>(100'000);
|
||||
|
||||
T::setround(Downward);
|
||||
auto const denom = poolIn + (assetIn * (detail::integer<T>(1) - fee));
|
||||
|
||||
T::setround(Upward);
|
||||
auto const ratio = numerator / denom;
|
||||
|
||||
T::setround(Downward);
|
||||
auto const swapOut = poolOut - ratio;
|
||||
|
||||
T::setround(saved);
|
||||
return swapOut;
|
||||
}
|
||||
|
||||
/**
|
||||
* Amount of the in asset required to take `assetOut` out of the pool.
|
||||
* Rounding at each step favors the AMM.
|
||||
*/
|
||||
template <NumberLike T>
|
||||
T
|
||||
ammSwapOut(T const& poolIn, T const& poolOut, T const& assetOut, std::uint16_t tfee)
|
||||
{
|
||||
using enum Number::RoundingMode;
|
||||
auto const saved = T::getround();
|
||||
|
||||
T::setround(Upward);
|
||||
auto const numerator = poolIn * poolOut;
|
||||
|
||||
T::setround(Downward);
|
||||
auto const denom = poolOut - assetOut;
|
||||
|
||||
T::setround(Upward);
|
||||
auto const ratio = numerator / denom;
|
||||
auto const numerator2 = ratio - poolIn;
|
||||
auto const fee = detail::integer<T>(tfee) / detail::integer<T>(100'000);
|
||||
|
||||
T::setround(Downward);
|
||||
auto const feeMult = detail::integer<T>(1) - fee;
|
||||
|
||||
T::setround(Upward);
|
||||
auto const swapIn = numerator2 / feeMult;
|
||||
|
||||
T::setround(saved);
|
||||
return swapIn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shares minted for depositing `assets` into a non-empty vault, truncated to
|
||||
* an integer (vault shares are MPTs).
|
||||
*/
|
||||
template <NumberLike T>
|
||||
std::int64_t
|
||||
vaultAssetsToShares(T const& shareTotal, T const& assetTotal, T const& assets)
|
||||
{
|
||||
auto const shares = (shareTotal * assets) / assetTotal;
|
||||
detail::RoundGuard<T> const guard{Number::RoundingMode::TowardsZero};
|
||||
return static_cast<std::int64_t>(shares);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assets corresponding to `shares` of a non-empty vault.
|
||||
*/
|
||||
template <NumberLike T>
|
||||
T
|
||||
vaultSharesToAssets(T const& assetTotal, T const& shareTotal, T const& shares)
|
||||
{
|
||||
return (assetTotal * shares) / shareTotal;
|
||||
}
|
||||
|
||||
constexpr std::uint32_t kSecondsInYear = 365 * 24 * 60 * 60;
|
||||
|
||||
/**
|
||||
* Interest rate per payment interval, from an annual rate in 1/10 bips.
|
||||
*/
|
||||
template <NumberLike T>
|
||||
T
|
||||
loanPeriodicRate(std::uint32_t interestRateTenthBips, std::uint32_t paymentInterval)
|
||||
{
|
||||
auto const interval = detail::integer<T>(paymentInterval);
|
||||
return interval * detail::integer<T>(interestRateTenthBips) / detail::integer<T>(100'000) /
|
||||
detail::integer<T>(kSecondsInYear);
|
||||
}
|
||||
|
||||
/**
|
||||
* (1 + r)^n - 1: closed form, or a binomial series when r * n is tiny and the
|
||||
* closed form would cancel catastrophically.
|
||||
*/
|
||||
template <NumberLike T>
|
||||
T
|
||||
loanPowerMinusOne(T const& rate, std::uint32_t payments)
|
||||
{
|
||||
auto const n = detail::integer<T>(payments);
|
||||
if (n * rate >= T{1, -9})
|
||||
return power(detail::integer<T>(1) + rate, payments) - detail::integer<T>(1);
|
||||
|
||||
T term = n * rate;
|
||||
T sum = term;
|
||||
for (std::uint32_t k = 1; k < payments; ++k)
|
||||
{
|
||||
term = term * rate * detail::integer<T>(payments - k) / detail::integer<T>(k + 1);
|
||||
T const next = sum + term;
|
||||
if (next == sum)
|
||||
break;
|
||||
sum = next;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Level payment that amortizes `principal` over `payments` periods.
|
||||
*/
|
||||
template <NumberLike T>
|
||||
T
|
||||
loanPeriodicPayment(T const& principal, T const& rate, std::uint32_t payments)
|
||||
{
|
||||
auto const raisedMinusOne = loanPowerMinusOne(rate, payments);
|
||||
auto const raised = detail::integer<T>(1) + raisedMinusOne;
|
||||
return principal * ((rate * raised) / raisedMinusOne);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remaining balance after making every scheduled payment. Exactly 0 in exact
|
||||
* arithmetic; anything else is accumulated rounding error.
|
||||
*/
|
||||
template <NumberLike T>
|
||||
T
|
||||
loanAmortize(T const& principal, T const& rate, std::uint32_t payments)
|
||||
{
|
||||
auto const payment = loanPeriodicPayment(principal, rate, payments);
|
||||
T balance = principal;
|
||||
for (std::uint32_t i = 0; i < payments; ++i)
|
||||
{
|
||||
auto const interest = balance * rate;
|
||||
balance -= payment - interest;
|
||||
}
|
||||
return balance;
|
||||
}
|
||||
|
||||
} // namespace xrpl::number_bench
|
||||
395
src/benchmarks/libxrpl/number/MpDecimal.h
Normal file
395
src/benchmarks/libxrpl/number/MpDecimal.h
Normal file
@@ -0,0 +1,395 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
|
||||
#include <benchmarks/libxrpl/number/NumberLike.h>
|
||||
|
||||
#include <mpdecimal.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl::number_bench {
|
||||
|
||||
/**
|
||||
* mpdecimal (libmpdec) at a fixed precision, behind Number's interface.
|
||||
*
|
||||
* This is the correctly-rounded reference for the divergence harness: with
|
||||
* `allcr` set, every operation, including `power`, is correctly rounded in
|
||||
* the active rounding mode.
|
||||
*
|
||||
* `Digits` is the coefficient precision: 19 to mirror today's Number, 34 for
|
||||
* IEEE decimal128, 38 for a full-uint128 Number. The exponent range follows
|
||||
* Number's convention: value = coefficient * 10^e with e in
|
||||
* [Number::kMinExponent, Number::kMaxExponent] for a `Digits`-digit
|
||||
* coefficient. mpdecimal uses adjusted exponents (the exponent of the leading
|
||||
* digit), hence the `Digits - 1` offset.
|
||||
*
|
||||
* Coefficients live in inline storage sized for `Digits`, so copying never
|
||||
* allocates. Library temporaries may still allocate; that is part of
|
||||
* mpdecimal's cost. Precisions above 38 digits exist for the divergence
|
||||
* harness, which needs a near-exact result to round from; `decompose` is only
|
||||
* available up to 38 digits.
|
||||
*
|
||||
* Unlike Number, results that overflow become infinity and underflow becomes
|
||||
* subnormal or zero. Each operation ORs its mpdecimal status into a
|
||||
* thread-local accumulator; read and clear it with `takeStatus()`.
|
||||
*/
|
||||
template <unsigned Digits>
|
||||
class MpDecimal final
|
||||
{
|
||||
static_assert(Digits >= 16 && Digits <= 114);
|
||||
|
||||
// Enough 19-digit limbs for a Digits-digit coefficient, and never fewer
|
||||
// than MPD_MINALLOC_MIN.
|
||||
static constexpr mpd_ssize_t kLimbs = (Digits / MPD_RDIGITS) + 2;
|
||||
|
||||
mpd_uint_t data_[kLimbs]{};
|
||||
// Zero: one limb holding 0.
|
||||
mpd_t dec_{MPD_STATIC | MPD_STATIC_DATA, 0, 1, 1, kLimbs, data_};
|
||||
|
||||
struct Context
|
||||
{
|
||||
mpd_context_t ctx{};
|
||||
uint32_t status = 0;
|
||||
|
||||
Context()
|
||||
{
|
||||
mpd_defaultcontext(&ctx);
|
||||
ctx.prec = Digits;
|
||||
ctx.emax = Number::kMaxExponent + static_cast<int>(Digits) - 1;
|
||||
ctx.emin = Number::kMinExponent + static_cast<int>(Digits) - 1;
|
||||
ctx.round = MPD_ROUND_HALF_EVEN;
|
||||
ctx.traps = 0;
|
||||
ctx.clamp = 0;
|
||||
ctx.allcr = 1;
|
||||
}
|
||||
};
|
||||
|
||||
static Context&
|
||||
context() noexcept
|
||||
{
|
||||
static thread_local Context c;
|
||||
return c;
|
||||
}
|
||||
|
||||
template <class F>
|
||||
static MpDecimal
|
||||
compute(F f)
|
||||
{
|
||||
MpDecimal result;
|
||||
auto& c = context();
|
||||
f(&result.dec_, &c.ctx, &c.status);
|
||||
return result;
|
||||
}
|
||||
|
||||
public:
|
||||
using rep = std::int64_t;
|
||||
|
||||
static constexpr unsigned kDigits = Digits;
|
||||
|
||||
MpDecimal() noexcept = default;
|
||||
|
||||
~MpDecimal()
|
||||
{
|
||||
// Frees the coefficient only if an operation outgrew inline storage.
|
||||
mpd_del(&dec_);
|
||||
}
|
||||
|
||||
MpDecimal(MpDecimal const& other) noexcept
|
||||
{
|
||||
mpd_qcopy(&dec_, &other.dec_, &context().status);
|
||||
}
|
||||
|
||||
MpDecimal&
|
||||
operator=(MpDecimal const& other) noexcept
|
||||
{
|
||||
if (this != &other)
|
||||
mpd_qcopy(&dec_, &other.dec_, &context().status);
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Implicit, like Number(rep).
|
||||
MpDecimal(rep mantissa) : MpDecimal(mantissa, 0) // NOLINT
|
||||
{
|
||||
}
|
||||
|
||||
explicit MpDecimal(rep mantissa, int exponent)
|
||||
{
|
||||
auto& c = context();
|
||||
mpd_qset_i64_exact(&dec_, mantissa, &c.status);
|
||||
dec_.exp += exponent;
|
||||
mpd_qfinalize(&dec_, &c.ctx, &c.status);
|
||||
}
|
||||
|
||||
MpDecimal
|
||||
operator-() const
|
||||
{
|
||||
return compute(
|
||||
[this](mpd_t* r, mpd_context_t* ctx, uint32_t* s) { mpd_qminus(r, &dec_, ctx, s); });
|
||||
}
|
||||
|
||||
friend MpDecimal
|
||||
operator+(MpDecimal const& x, MpDecimal const& y)
|
||||
{
|
||||
return compute([&](mpd_t* r, mpd_context_t* ctx, uint32_t* s) {
|
||||
mpd_qadd(r, &x.dec_, &y.dec_, ctx, s);
|
||||
});
|
||||
}
|
||||
|
||||
friend MpDecimal
|
||||
operator-(MpDecimal const& x, MpDecimal const& y)
|
||||
{
|
||||
return compute([&](mpd_t* r, mpd_context_t* ctx, uint32_t* s) {
|
||||
mpd_qsub(r, &x.dec_, &y.dec_, ctx, s);
|
||||
});
|
||||
}
|
||||
|
||||
friend MpDecimal
|
||||
operator*(MpDecimal const& x, MpDecimal const& y)
|
||||
{
|
||||
return compute([&](mpd_t* r, mpd_context_t* ctx, uint32_t* s) {
|
||||
mpd_qmul(r, &x.dec_, &y.dec_, ctx, s);
|
||||
});
|
||||
}
|
||||
|
||||
friend MpDecimal
|
||||
operator/(MpDecimal const& x, MpDecimal const& y)
|
||||
{
|
||||
return compute([&](mpd_t* r, mpd_context_t* ctx, uint32_t* s) {
|
||||
mpd_qdiv(r, &x.dec_, &y.dec_, ctx, s);
|
||||
});
|
||||
}
|
||||
|
||||
MpDecimal&
|
||||
operator+=(MpDecimal const& x)
|
||||
{
|
||||
return *this = *this + x;
|
||||
}
|
||||
|
||||
MpDecimal&
|
||||
operator-=(MpDecimal const& x)
|
||||
{
|
||||
return *this = *this - x;
|
||||
}
|
||||
|
||||
MpDecimal&
|
||||
operator*=(MpDecimal const& x)
|
||||
{
|
||||
return *this = *this * x;
|
||||
}
|
||||
|
||||
MpDecimal&
|
||||
operator/=(MpDecimal const& x)
|
||||
{
|
||||
return *this = *this / x;
|
||||
}
|
||||
|
||||
friend bool
|
||||
operator==(MpDecimal const& x, MpDecimal const& y) noexcept
|
||||
{
|
||||
return mpd_qcmp(&x.dec_, &y.dec_, &context().status) == 0;
|
||||
}
|
||||
|
||||
friend bool
|
||||
operator<(MpDecimal const& x, MpDecimal const& y) noexcept
|
||||
{
|
||||
return mpd_qcmp(&x.dec_, &y.dec_, &context().status) == -1;
|
||||
}
|
||||
|
||||
friend bool
|
||||
operator>(MpDecimal const& x, MpDecimal const& y) noexcept
|
||||
{
|
||||
return y < x;
|
||||
}
|
||||
|
||||
friend bool
|
||||
operator<=(MpDecimal const& x, MpDecimal const& y) noexcept
|
||||
{
|
||||
return x < y || x == y;
|
||||
}
|
||||
|
||||
friend bool
|
||||
operator>=(MpDecimal const& x, MpDecimal const& y) noexcept
|
||||
{
|
||||
return y <= x;
|
||||
}
|
||||
|
||||
// Round to an integer using the current rounding mode, like Number.
|
||||
explicit
|
||||
operator rep() const
|
||||
{
|
||||
auto const rounded = compute([this](mpd_t* r, mpd_context_t* ctx, uint32_t* s) {
|
||||
mpd_qround_to_int(r, &dec_, ctx, s);
|
||||
});
|
||||
return mpd_qget_i64(&rounded.dec_, &context().status);
|
||||
}
|
||||
|
||||
static Number::RoundingMode
|
||||
getround() noexcept
|
||||
{
|
||||
using enum Number::RoundingMode;
|
||||
switch (context().ctx.round)
|
||||
{
|
||||
case MPD_ROUND_DOWN:
|
||||
return TowardsZero;
|
||||
case MPD_ROUND_FLOOR:
|
||||
return Downward;
|
||||
case MPD_ROUND_CEILING:
|
||||
return Upward;
|
||||
default:
|
||||
return ToNearest;
|
||||
}
|
||||
}
|
||||
|
||||
static Number::RoundingMode
|
||||
setround(Number::RoundingMode mode) noexcept
|
||||
{
|
||||
using enum Number::RoundingMode;
|
||||
auto const old = getround();
|
||||
auto& round = context().ctx.round;
|
||||
switch (mode)
|
||||
{
|
||||
case ToNearest:
|
||||
round = MPD_ROUND_HALF_EVEN;
|
||||
break;
|
||||
case TowardsZero:
|
||||
round = MPD_ROUND_DOWN;
|
||||
break;
|
||||
case Downward:
|
||||
round = MPD_ROUND_FLOOR;
|
||||
break;
|
||||
case Upward:
|
||||
round = MPD_ROUND_CEILING;
|
||||
break;
|
||||
}
|
||||
return old;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mpdecimal status flags raised since the last call, and clears them.
|
||||
*/
|
||||
static uint32_t
|
||||
takeStatus() noexcept
|
||||
{
|
||||
return std::exchange(context().status, 0);
|
||||
}
|
||||
|
||||
friend std::string
|
||||
to_string(MpDecimal const& x)
|
||||
{
|
||||
char* raw = nullptr;
|
||||
auto const size = mpd_to_sci_size(&raw, &x.dec_, 0);
|
||||
std::unique_ptr<char, void (*)(void*)> const owned{raw, mpd_free};
|
||||
return size < 0 ? std::string{} : std::string{raw, static_cast<std::size_t>(size)};
|
||||
}
|
||||
|
||||
friend MpDecimal
|
||||
abs(MpDecimal const& x)
|
||||
{
|
||||
return compute(
|
||||
[&](mpd_t* r, mpd_context_t* ctx, uint32_t* s) { mpd_qabs(r, &x.dec_, ctx, s); });
|
||||
}
|
||||
|
||||
friend MpDecimal
|
||||
power(MpDecimal const& x, unsigned n)
|
||||
{
|
||||
MpDecimal const exponent{static_cast<rep>(n)};
|
||||
return compute([&](mpd_t* r, mpd_context_t* ctx, uint32_t* s) {
|
||||
mpd_qpow(r, &x.dec_, &exponent.dec_, ctx, s);
|
||||
});
|
||||
}
|
||||
|
||||
friend MpDecimal
|
||||
root2(MpDecimal const& x)
|
||||
{
|
||||
return compute(
|
||||
[&](mpd_t* r, mpd_context_t* ctx, uint32_t* s) { mpd_qsqrt(r, &x.dec_, ctx, s); });
|
||||
}
|
||||
|
||||
/**
|
||||
* The underlying libmpdec value, for the divergence harness.
|
||||
*/
|
||||
[[nodiscard]] mpd_t const*
|
||||
get() const noexcept
|
||||
{
|
||||
return &dec_;
|
||||
}
|
||||
|
||||
[[nodiscard]] mpd_t*
|
||||
get() noexcept
|
||||
{
|
||||
return &dec_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds `x` to this type's precision in the current rounding mode.
|
||||
*/
|
||||
template <unsigned OtherDigits>
|
||||
static MpDecimal
|
||||
roundFrom(MpDecimal<OtherDigits> const& x)
|
||||
{
|
||||
MpDecimal result;
|
||||
auto& c = context();
|
||||
mpd_qcopy(&result.dec_, x.get(), &c.status);
|
||||
mpd_qfinalize(&result.dec_, &c.ctx, &c.status);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exact conversion; rounds only if `d` has more than Digits digits.
|
||||
*/
|
||||
static MpDecimal
|
||||
fromDecomposed(Decomposed const& d)
|
||||
{
|
||||
auto& c = context();
|
||||
mpd_context_t exact;
|
||||
mpd_maxcontext(&exact);
|
||||
exact.traps = 0;
|
||||
|
||||
// coefficient = hi * 2^64 + lo, assembled at unlimited precision.
|
||||
MpDecimal result;
|
||||
MpDecimal lo;
|
||||
MpDecimal shift;
|
||||
mpd_qset_u64_exact(
|
||||
&result.dec_, static_cast<std::uint64_t>(d.coefficient >> 64), &c.status);
|
||||
mpd_qset_u64_exact(&lo.dec_, static_cast<std::uint64_t>(d.coefficient), &c.status);
|
||||
mpd_qset_u64_exact(&shift.dec_, std::uint64_t{1} << 32, &c.status);
|
||||
mpd_qmul(&shift.dec_, &shift.dec_, &shift.dec_, &exact, &c.status);
|
||||
mpd_qmul(&result.dec_, &result.dec_, &shift.dec_, &exact, &c.status);
|
||||
mpd_qadd(&result.dec_, &result.dec_, &lo.dec_, &exact, &c.status);
|
||||
if (d.negative)
|
||||
mpd_set_negative(&result.dec_);
|
||||
result.dec_.exp += d.exponent;
|
||||
mpd_qfinalize(&result.dec_, &c.ctx, &c.status);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Precondition: x is finite (check takeStatus() for overflow/invalid first).
|
||||
*/
|
||||
friend Decomposed
|
||||
decompose(MpDecimal const& x) noexcept
|
||||
requires(Digits <= 38)
|
||||
{
|
||||
unsigned __int128 coefficient = 0;
|
||||
for (auto i = x.dec_.len; i-- > 0;)
|
||||
coefficient = (coefficient * MPD_RADIX) + x.dec_.data[i];
|
||||
return {
|
||||
.negative = (x.dec_.flags & MPD_NEG) != 0,
|
||||
.coefficient = coefficient,
|
||||
.exponent = static_cast<int>(x.dec_.exp)};
|
||||
}
|
||||
};
|
||||
|
||||
using MpDecimal19 = MpDecimal<19>;
|
||||
using MpDecimal34 = MpDecimal<34>;
|
||||
using MpDecimal38 = MpDecimal<38>;
|
||||
|
||||
static_assert(DecomposableNumber<MpDecimal19>);
|
||||
static_assert(DecomposableNumber<MpDecimal34>);
|
||||
static_assert(DecomposableNumber<MpDecimal38>);
|
||||
|
||||
} // namespace xrpl::number_bench
|
||||
37
src/benchmarks/libxrpl/number/Number128.h
Normal file
37
src/benchmarks/libxrpl/number/Number128.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
// Slot for a 128-bit Number prototype.
|
||||
//
|
||||
// When <xrpl/basics/Number128.h> exists, CMake defines XRPL_BENCH_NUMBER128
|
||||
// (see src/benchmarks/libxrpl/CMakeLists.txt) and the type is benchmarked and
|
||||
// checked by every tool here with no further changes: xrpl.bench.number (single
|
||||
// operations and ledger formulas) and xrpl.bench.number_diff (rounding
|
||||
// correctness in all four modes). The header must provide `xrpl::Number128`
|
||||
// satisfying DecomposableNumber (see NumberLike.h), plus:
|
||||
//
|
||||
// static constexpr unsigned kDigits; // mantissa precision, e.g. 34 or 38
|
||||
//
|
||||
// number_diff rounds exact results onto a plain kDigits-digit grid. Up to 38
|
||||
// digits that is exact: 10^38 - 1 is below the signed 128-bit maximum, so
|
||||
// there is no capped region like today's Number has above 2^63 - 1. If the
|
||||
// prototype's grid differs, extend `Grid` in number_diff/main.cpp.
|
||||
|
||||
#ifdef XRPL_BENCH_NUMBER128
|
||||
|
||||
#include <xrpl/basics/Number128.h>
|
||||
|
||||
#include <benchmarks/libxrpl/number/NumberLike.h>
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
||||
#define XRPL_BENCH_HAS_NUMBER128 1
|
||||
|
||||
namespace xrpl::number_bench {
|
||||
|
||||
static_assert(
|
||||
DecomposableNumber<Number128>,
|
||||
"Number128 must provide Number's interface; see NumberLike.h");
|
||||
static_assert(Number128::kDigits <= 38, "Decomposed holds at most 38 digits");
|
||||
|
||||
} // namespace xrpl::number_bench
|
||||
|
||||
#endif
|
||||
91
src/benchmarks/libxrpl/number/NumberLike.h
Normal file
91
src/benchmarks/libxrpl/number/NumberLike.h
Normal file
@@ -0,0 +1,91 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::number_bench {
|
||||
|
||||
/**
|
||||
* Sign, coefficient, and exponent of a finite decimal value:
|
||||
* value = (negative ? -1 : 1) * coefficient * 10^exponent.
|
||||
*
|
||||
* The coefficient is 128 bits wide so the same struct can describe 34- and
|
||||
* 38-digit types. Different libraries normalize to different precisions, so
|
||||
* compare decompositions with `canonical()`, which strips trailing zeros.
|
||||
*/
|
||||
struct Decomposed
|
||||
{
|
||||
bool negative = false;
|
||||
unsigned __int128 coefficient = 0;
|
||||
int exponent = 0;
|
||||
|
||||
[[nodiscard]] constexpr Decomposed
|
||||
canonical() const noexcept
|
||||
{
|
||||
if (coefficient == 0)
|
||||
return {};
|
||||
Decomposed result = *this;
|
||||
while (result.coefficient % 10 == 0)
|
||||
{
|
||||
result.coefficient /= 10;
|
||||
++result.exponent;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
friend constexpr bool
|
||||
operator==(Decomposed const&, Decomposed const&) = default;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline Decomposed
|
||||
decompose(Number const& x) noexcept
|
||||
{
|
||||
auto const m = x.mantissa();
|
||||
// Negating INT64_MIN is UB, but Number never produces it (see Number.h).
|
||||
auto const magnitude = static_cast<std::uint64_t>(m < 0 ? -m : m);
|
||||
return {.negative = m < 0, .coefficient = magnitude, .exponent = x.exponent()};
|
||||
}
|
||||
|
||||
/**
|
||||
* The subset of Number's interface that ledger code relies on. Benchmarks and
|
||||
* the divergence harness are written against this concept, so any type
|
||||
* satisfying it, including a future 128-bit Number, can be dropped in.
|
||||
*/
|
||||
template <class T>
|
||||
concept NumberLike = std::copyable<T> && std::totally_ordered<T> &&
|
||||
requires(T a, T b, std::int64_t mantissa, int exponent, unsigned n, Number::RoundingMode mode) {
|
||||
T{mantissa};
|
||||
T{mantissa, exponent};
|
||||
{ a + b } -> std::same_as<T>;
|
||||
{ a - b } -> std::same_as<T>;
|
||||
{ a * b } -> std::same_as<T>;
|
||||
{ a / b } -> std::same_as<T>;
|
||||
{ -a } -> std::same_as<T>;
|
||||
{ a += b } -> std::same_as<T&>;
|
||||
{ a -= b } -> std::same_as<T&>;
|
||||
{ a *= b } -> std::same_as<T&>;
|
||||
{ a /= b } -> std::same_as<T&>;
|
||||
static_cast<std::int64_t>(a);
|
||||
{ to_string(a) } -> std::same_as<std::string>;
|
||||
{ abs(a) } -> std::same_as<T>;
|
||||
{ power(a, n) } -> std::same_as<T>;
|
||||
{ root2(a) } -> std::same_as<T>;
|
||||
{ T::setround(mode) } -> std::same_as<Number::RoundingMode>;
|
||||
{ T::getround() } -> std::same_as<Number::RoundingMode>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A NumberLike type whose exact value can be inspected digit by digit.
|
||||
*/
|
||||
template <class T>
|
||||
concept DecomposableNumber = NumberLike<T> && requires(T a) {
|
||||
{ decompose(a) } -> std::same_as<Decomposed>;
|
||||
};
|
||||
|
||||
static_assert(DecomposableNumber<Number>);
|
||||
|
||||
} // namespace xrpl::number_bench
|
||||
250
src/benchmarks/libxrpl/number/Operands.h
Normal file
250
src/benchmarks/libxrpl/number/Operands.h
Normal file
@@ -0,0 +1,250 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
|
||||
#include <benchmarks/libxrpl/number/NumberLike.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <random>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::number_bench {
|
||||
|
||||
/**
|
||||
* Operand count per batch: small enough to stay in L1 for 32-byte types.
|
||||
*/
|
||||
constexpr std::size_t kBatchSize = 1024;
|
||||
|
||||
/**
|
||||
* Raw operand: value = mantissa * 10^exponent.
|
||||
*/
|
||||
struct RawOperand
|
||||
{
|
||||
std::int64_t mantissa;
|
||||
int exponent;
|
||||
};
|
||||
|
||||
/**
|
||||
* Operand distributions. Each one models a class of values the ledger works
|
||||
* with, because cost depends on digit count and exponent alignment.
|
||||
*/
|
||||
enum class Dataset {
|
||||
/**
|
||||
* Full-width 19-digit mantissas in [10^18, 2^63-1], exponents in [-10, 10].
|
||||
*/
|
||||
Full,
|
||||
/**
|
||||
* IOU-like 16-digit mantissas in [10^15, 10^16-1], exponents in [-25, 5].
|
||||
*/
|
||||
Iou,
|
||||
/**
|
||||
* XRP drop amounts: integers with 1 to 17 digits (log-uniform), exponent 0.
|
||||
*/
|
||||
Drops,
|
||||
/**
|
||||
* MPT amounts: integers with 1 to 19 digits up to 2^63-1 (log-uniform), exponent 0.
|
||||
*/
|
||||
Mpt,
|
||||
/**
|
||||
* Rates in [1, 1.01) with full-width mantissas: the operand of interest and amortization
|
||||
* math, and of long multiply/divide chains that must not overflow.
|
||||
*/
|
||||
Rate,
|
||||
/**
|
||||
* Non-integers below 10^18 (19-digit mantissas, exponents in [-18, -1]): rounding to int64.
|
||||
*/
|
||||
Fraction,
|
||||
/**
|
||||
* Exact halves, m + 0.5 with m < 10^17: the tie cases of rounding to int64.
|
||||
*/
|
||||
Half,
|
||||
};
|
||||
|
||||
constexpr std::string_view
|
||||
toString(Dataset d)
|
||||
{
|
||||
switch (d)
|
||||
{
|
||||
case Dataset::Full:
|
||||
return "full";
|
||||
case Dataset::Iou:
|
||||
return "iou";
|
||||
case Dataset::Drops:
|
||||
return "drops";
|
||||
case Dataset::Mpt:
|
||||
return "mpt";
|
||||
case Dataset::Rate:
|
||||
return "rate";
|
||||
case Dataset::Fraction:
|
||||
return "fraction";
|
||||
case Dataset::Half:
|
||||
return "half";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
|
||||
/**
|
||||
* Integer with a log-uniform digit count in [1, maxDigits], capped at `cap`.
|
||||
*/
|
||||
inline std::int64_t
|
||||
logUniformInteger(std::mt19937_64& rng, int maxDigits, std::int64_t cap)
|
||||
{
|
||||
auto const digits = std::uniform_int_distribution<int>{1, maxDigits}(rng);
|
||||
auto const lo = static_cast<std::int64_t>(kPowerOfTen[digits - 1]);
|
||||
auto const hi =
|
||||
digits >= 19 ? cap : std::min(cap, static_cast<std::int64_t>(kPowerOfTen[digits]) - 1);
|
||||
return std::uniform_int_distribution<std::int64_t>{lo, hi}(rng);
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/**
|
||||
* Deterministic: the same dataset and seed always produce the same operands.
|
||||
*/
|
||||
inline std::vector<RawOperand>
|
||||
makeRaw(Dataset d, std::uint64_t seed, std::size_t count = kBatchSize)
|
||||
{
|
||||
constexpr auto kMaxInt64 = std::numeric_limits<std::int64_t>::max();
|
||||
constexpr auto kE18 = static_cast<std::int64_t>(kPowerOfTen[18]);
|
||||
constexpr auto kE16 = static_cast<std::int64_t>(kPowerOfTen[16]);
|
||||
constexpr auto kE15 = static_cast<std::int64_t>(kPowerOfTen[15]);
|
||||
|
||||
std::mt19937_64 rng{seed};
|
||||
std::vector<RawOperand> result;
|
||||
result.reserve(count);
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
switch (d)
|
||||
{
|
||||
case Dataset::Full:
|
||||
result.push_back(
|
||||
{std::uniform_int_distribution<std::int64_t>{kE18, kMaxInt64}(rng),
|
||||
std::uniform_int_distribution<int>{-10, 10}(rng)});
|
||||
break;
|
||||
case Dataset::Iou:
|
||||
result.push_back(
|
||||
{std::uniform_int_distribution<std::int64_t>{kE15, kE16 - 1}(rng),
|
||||
std::uniform_int_distribution<int>{-25, 5}(rng)});
|
||||
break;
|
||||
case Dataset::Drops:
|
||||
result.push_back({detail::logUniformInteger(rng, 17, kMaxInt64), 0});
|
||||
break;
|
||||
case Dataset::Mpt:
|
||||
result.push_back({detail::logUniformInteger(rng, 19, kMaxInt64), 0});
|
||||
break;
|
||||
case Dataset::Rate:
|
||||
// [10^18, 1.01 * 10^18) * 10^-18 = [1, 1.01)
|
||||
result.push_back(
|
||||
{std::uniform_int_distribution<std::int64_t>{
|
||||
kE18, kE18 + (kE18 / 100) - 1}(rng),
|
||||
-18});
|
||||
break;
|
||||
case Dataset::Fraction:
|
||||
result.push_back(
|
||||
{std::uniform_int_distribution<std::int64_t>{kE18, kMaxInt64}(rng),
|
||||
std::uniform_int_distribution<int>{-18, -1}(rng)});
|
||||
break;
|
||||
case Dataset::Half:
|
||||
result.push_back(
|
||||
{(std::uniform_int_distribution<std::int64_t>{0, (kE18 / 10) - 1}(rng) * 10) +
|
||||
5,
|
||||
-1});
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
template <NumberLike T>
|
||||
std::vector<T>
|
||||
materialize(std::vector<RawOperand> const& raw)
|
||||
{
|
||||
std::vector<T> result;
|
||||
result.reserve(raw.size());
|
||||
for (auto const& r : raw)
|
||||
result.push_back(T{r.mantissa, r.exponent});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pairs of full-width operands whose exponents differ by exactly `gap`, so
|
||||
* addition must shift (and round away) `gap` digits of the smaller operand.
|
||||
*/
|
||||
inline std::pair<std::vector<RawOperand>, std::vector<RawOperand>>
|
||||
makeExponentGapPairs(int gap, std::uint64_t seed, std::size_t count = kBatchSize)
|
||||
{
|
||||
auto lhs = makeRaw(Dataset::Full, seed, count);
|
||||
auto rhs = makeRaw(Dataset::Full, seed + 1, count);
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
rhs[i].exponent = lhs[i].exponent - gap;
|
||||
return {std::move(lhs), std::move(rhs)};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pairs of nearly equal full-width operands, so subtraction cancels most
|
||||
* leading digits and the result must be renormalized by many places.
|
||||
*/
|
||||
inline std::pair<std::vector<RawOperand>, std::vector<RawOperand>>
|
||||
makeCancellationPairs(std::uint64_t seed, std::size_t count = kBatchSize)
|
||||
{
|
||||
auto lhs = makeRaw(Dataset::Full, seed, count);
|
||||
std::vector<RawOperand> rhs;
|
||||
rhs.reserve(count);
|
||||
std::mt19937_64 rng{seed + 1};
|
||||
std::uniform_int_distribution<std::int64_t> delta{1, 1000};
|
||||
for (auto const& l : lhs)
|
||||
rhs.push_back({l.mantissa - delta(rng), l.exponent});
|
||||
return {std::move(lhs), std::move(rhs)};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pairs whose exact sum is exactly halfway between two adjacent 19-digit
|
||||
* values: a = m * 10^e, b = 5 * 10^(e-1). Exercises round-half-even.
|
||||
*/
|
||||
inline std::pair<std::vector<RawOperand>, std::vector<RawOperand>>
|
||||
makeTiePairs(std::uint64_t seed, std::size_t count = kBatchSize)
|
||||
{
|
||||
auto lhs = makeRaw(Dataset::Full, seed, count);
|
||||
std::vector<RawOperand> rhs;
|
||||
rhs.reserve(count);
|
||||
for (auto const& l : lhs)
|
||||
rhs.push_back({5, l.exponent - 1});
|
||||
return {std::move(lhs), std::move(rhs)};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pairs whose sum straddles 2^63-1 (Number::kMaxRep), where Number's
|
||||
* representable values switch from a step of 1 to a step of 10. The addend has
|
||||
* one more decimal place than the base, so most sums fall between
|
||||
* representable values and must be rounded; sums are within ±200 of the cap,
|
||||
* so a fair share land in (2^63-1, 2^63), the interval where LargeLegacy's
|
||||
* known cusp-rounding error shows.
|
||||
*/
|
||||
inline std::pair<std::vector<RawOperand>, std::vector<RawOperand>>
|
||||
makeCuspPairs(std::uint64_t seed, std::size_t count = kBatchSize)
|
||||
{
|
||||
constexpr auto kMaxInt64 = std::numeric_limits<std::int64_t>::max();
|
||||
std::mt19937_64 rng{seed};
|
||||
std::uniform_int_distribution<std::int64_t> base{kMaxInt64 - 100, kMaxInt64};
|
||||
std::uniform_int_distribution<std::int64_t> addend{1, 2'000};
|
||||
std::uniform_int_distribution<int> exponent{-10, 10};
|
||||
std::pair<std::vector<RawOperand>, std::vector<RawOperand>> result;
|
||||
result.first.reserve(count);
|
||||
result.second.reserve(count);
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
auto const e = exponent(rng);
|
||||
result.first.push_back({base(rng), e});
|
||||
result.second.push_back({addend(rng), e - 1});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace xrpl::number_bench
|
||||
309
src/benchmarks/libxrpl/number/plot_results.py
Executable file
309
src/benchmarks/libxrpl/number/plot_results.py
Executable file
@@ -0,0 +1,309 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render the figures in docs/NumberDecimalBenchmark.md from benchmark JSON.
|
||||
|
||||
Usage:
|
||||
plot_results.py CORE_JSON KERNELS_JSON OUT_DIR
|
||||
|
||||
CORE_JSON is Google Benchmark JSON from xrpl.bench.number filtered to
|
||||
'^(add|mul|div)/full/'; KERNELS_JSON from '^kernel/' with mpdecimal enabled
|
||||
(for the accuracy counters). Both need --benchmark_repetitions so medians are
|
||||
available. Writes self-contained SVG files with light and dark variants
|
||||
(prefers-color-scheme) and no dependencies beyond the standard library.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
from xml.sax.saxutils import escape
|
||||
|
||||
# Categorical slots 1-3 of the reference palette (validated all-pairs, light
|
||||
# and dark). Color encodes the type family; every bar is also labeled.
|
||||
FAMILIES = {
|
||||
"number": ("Number today (19 digits)", "#2a78d6", "#3987e5"),
|
||||
"ieee64": ("16-digit types", "#eb6834", "#d95926"),
|
||||
"wide": ("34- and 38-digit types", "#1baf7a", "#199e70"),
|
||||
}
|
||||
|
||||
# (benchmark subject, row label, family)
|
||||
CORE_ROWS = [
|
||||
("Number.Large330", "Number (Large330)", "number"),
|
||||
("BoostDecimal64", "Boost decimal64", "ieee64"),
|
||||
("BoostDecimalFast64", "Boost decimal_fast64", "ieee64"),
|
||||
("IntelBid64", "Intel BID64", "ieee64"),
|
||||
("BoostDecimal128", "Boost decimal128", "wide"),
|
||||
("BoostDecimalFast128", "Boost decimal_fast128", "wide"),
|
||||
("IntelBid128", "Intel BID128", "wide"),
|
||||
("MpDecimal34", "mpdecimal 34", "wide"),
|
||||
("MpDecimal38", "mpdecimal 38", "wide"),
|
||||
]
|
||||
|
||||
KERNEL_ROWS = [
|
||||
("Number.Large330", "Number (Large330)", "number"),
|
||||
("BoostDecimal64", "Boost decimal64", "ieee64"),
|
||||
("IntelBid64", "Intel BID64", "ieee64"),
|
||||
("BoostDecimal128", "Boost decimal128", "wide"),
|
||||
("IntelBid128", "Intel BID128", "wide"),
|
||||
("MpDecimal34", "mpdecimal 34", "wide"),
|
||||
("MpDecimal38", "mpdecimal 38", "wide"),
|
||||
]
|
||||
|
||||
FONT = "system-ui, -apple-system, sans-serif"
|
||||
ROW_H = 24
|
||||
BAR_H = 12
|
||||
LABEL_W = 150
|
||||
PANEL_W = 200
|
||||
PANEL_GAP = 28
|
||||
TOP = 78
|
||||
BOTTOM = 30
|
||||
|
||||
|
||||
@dataclass
|
||||
class Panel:
|
||||
title: str
|
||||
unit: str
|
||||
values: dict[str, float]
|
||||
fmt: Callable[[float], str]
|
||||
# Panels with the same group share an x scale; use it only for panels
|
||||
# with the same unit, so bar lengths are comparable.
|
||||
group: str = ""
|
||||
|
||||
|
||||
def medians(path: str) -> dict[str, dict]:
|
||||
"""Map run_name to its median aggregate."""
|
||||
data = json.loads(Path(path).read_text())
|
||||
return {
|
||||
b["run_name"]: b
|
||||
for b in data["benchmarks"]
|
||||
if b.get("aggregate_name") == "median"
|
||||
}
|
||||
|
||||
|
||||
def ns_per_item(b: dict) -> float:
|
||||
return 1e9 / b["items_per_second"]
|
||||
|
||||
|
||||
def nice_max(v: float) -> tuple[float, list[float]]:
|
||||
"""Axis maximum and ticks: 4-5 round steps covering v."""
|
||||
for step in [
|
||||
1,
|
||||
2,
|
||||
2.5,
|
||||
5,
|
||||
10,
|
||||
20,
|
||||
25,
|
||||
50,
|
||||
100,
|
||||
200,
|
||||
250,
|
||||
500,
|
||||
1000,
|
||||
2000,
|
||||
2500,
|
||||
5000,
|
||||
10000,
|
||||
20000,
|
||||
25000,
|
||||
50000,
|
||||
]:
|
||||
if v / step <= 5:
|
||||
top = step * -(-v // step)
|
||||
return top, [step * i for i in range(int(top / step) + 1)]
|
||||
return v, [0, v]
|
||||
|
||||
|
||||
def bar_path(x: float, y: float, w: float, h: float, r: float = 4) -> str:
|
||||
"""Bar anchored at the baseline (left) with a rounded data end (right)."""
|
||||
r = min(r, w, h / 2)
|
||||
return (
|
||||
f"M{x:.1f},{y:.1f} H{x + w - r:.1f} "
|
||||
f"Q{x + w:.1f},{y:.1f} {x + w:.1f},{y + r:.1f} "
|
||||
f"V{y + h - r:.1f} Q{x + w:.1f},{y + h:.1f} {x + w - r:.1f},{y + h:.1f} "
|
||||
f"H{x:.1f} Z"
|
||||
)
|
||||
|
||||
|
||||
def render(
|
||||
title: str, rows: list[tuple[str, str, str]], panels: list[Panel], out: Path
|
||||
) -> None:
|
||||
"""Horizontal bar panels sharing one column of row labels."""
|
||||
width = LABEL_W + len(panels) * (PANEL_W + PANEL_GAP)
|
||||
height = TOP + len(rows) * ROW_H + BOTTOM
|
||||
css_light = "".join(
|
||||
f".f-{k}{{fill:{light}}}" for k, (_, light, _) in FAMILIES.items()
|
||||
)
|
||||
css_dark = "".join(f".f-{k}{{fill:{dark}}}" for k, (_, _, dark) in FAMILIES.items())
|
||||
out_lines = [
|
||||
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {width} {height}" '
|
||||
f'width="{width}" height="{height}" role="img" '
|
||||
f'aria-label="{escape(title)}">',
|
||||
"<style>",
|
||||
f"text{{font-family:{FONT};font-size:12px}}",
|
||||
".bg{fill:#fcfcfb}.t1{fill:#0b0b0b}.t2{fill:#52514e}"
|
||||
".grid{stroke:#e4e3df;stroke-width:1}.base{stroke:#b9b8b2;stroke-width:1}"
|
||||
+ css_light,
|
||||
"@media (prefers-color-scheme: dark){"
|
||||
".bg{fill:#1a1a19}.t1{fill:#ffffff}.t2{fill:#c3c2b7}"
|
||||
".grid{stroke:#383835}.base{stroke:#5e5d58}" + css_dark + "}",
|
||||
"</style>",
|
||||
f'<rect class="bg" width="{width}" height="{height}"/>',
|
||||
f'<text class="t1" x="0" y="18" font-size="15" font-weight="600">'
|
||||
f"{escape(title)}</text>",
|
||||
]
|
||||
# Legend: always present for more than one family.
|
||||
lx = 0.0
|
||||
for key, (label, _, _) in FAMILIES.items():
|
||||
out_lines.append(
|
||||
f'<rect class="f-{key}" x="{lx}" y="32" width="12" height="12" rx="3"/>'
|
||||
)
|
||||
out_lines.append(
|
||||
f'<text class="t2" x="{lx + 18}" y="42">{escape(label)}</text>'
|
||||
)
|
||||
lx += 18 + 7.2 * len(label) + 24
|
||||
|
||||
for i, (_, label, _) in enumerate(rows):
|
||||
y = TOP + i * ROW_H + ROW_H / 2 + 4
|
||||
out_lines.append(f'<text class="t1" x="0" y="{y:.1f}">{escape(label)}</text>')
|
||||
|
||||
for p_index, panel in enumerate(panels):
|
||||
x0 = LABEL_W + p_index * (PANEL_W + PANEL_GAP)
|
||||
vmax = max(
|
||||
p.values.get(s, 0)
|
||||
for p in panels
|
||||
if p is panel or (panel.group and p.group == panel.group)
|
||||
for s, _, _ in rows
|
||||
)
|
||||
top, ticks = nice_max(vmax * 1.08)
|
||||
scale = (PANEL_W - 40) / top
|
||||
out_lines.append(
|
||||
f'<text class="t1" x="{x0}" y="{TOP - 14}" font-weight="600">'
|
||||
f"{escape(panel.title)}</text>"
|
||||
)
|
||||
y_end = TOP + len(rows) * ROW_H
|
||||
for t in ticks:
|
||||
gx = x0 + t * scale
|
||||
out_lines.append(
|
||||
f'<line class="grid" x1="{gx:.1f}" y1="{TOP}" x2="{gx:.1f}" y2="{y_end}"/>'
|
||||
)
|
||||
out_lines.append(
|
||||
f'<text class="t2" x="{gx:.1f}" y="{y_end + 16}" text-anchor="middle" '
|
||||
f'font-size="11">{t:g}</text>'
|
||||
)
|
||||
out_lines.append(
|
||||
f'<text class="t2" x="{x0 + PANEL_W - 40:.1f}" y="{y_end + 28}" '
|
||||
f'text-anchor="end" font-size="11">{escape(panel.unit)}</text>'
|
||||
)
|
||||
for i, (subject, label, family) in enumerate(rows):
|
||||
if subject not in panel.values:
|
||||
continue
|
||||
v = panel.values[subject]
|
||||
y = TOP + i * ROW_H + (ROW_H - BAR_H) / 2
|
||||
w = max(v * scale, 1.0)
|
||||
out_lines.append(
|
||||
f'<path class="f-{family}" d="{bar_path(x0, y, w, BAR_H)}">'
|
||||
f"<title>{escape(label)}: {panel.fmt(v)} {escape(panel.unit)}</title></path>"
|
||||
)
|
||||
out_lines.append(
|
||||
f'<text class="t2" x="{x0 + w + 5:.1f}" y="{y + BAR_H - 2:.1f}" '
|
||||
f'font-size="11">{panel.fmt(v)}</text>'
|
||||
)
|
||||
out_lines.append(
|
||||
f'<line class="base" x1="{x0}" y1="{TOP}" x2="{x0}" y2="{y_end}"/>'
|
||||
)
|
||||
out_lines.append("</svg>")
|
||||
out.write_text("\n".join(out_lines) + "\n")
|
||||
|
||||
|
||||
def fmt_ns(v: float) -> str:
|
||||
return f"{v:.0f}" if v >= 10 else f"{v:.1f}"
|
||||
|
||||
|
||||
def fmt_us(v: float) -> str:
|
||||
return f"{v:.0f}" if v >= 10 else f"{v:.1f}"
|
||||
|
||||
|
||||
def fmt_digits(v: float) -> str:
|
||||
return f"{v:.1f}"
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
if len(argv) != 4:
|
||||
print(__doc__, file=sys.stderr)
|
||||
return 2
|
||||
core = medians(argv[1])
|
||||
kernels = medians(argv[2])
|
||||
out_dir = Path(argv[3])
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
core_panels = [
|
||||
Panel(
|
||||
op_title,
|
||||
"ns per operation",
|
||||
{
|
||||
s: ns_per_item(core[f"{op}/full/{s}"])
|
||||
for s, _, _ in CORE_ROWS
|
||||
if f"{op}/full/{s}" in core
|
||||
},
|
||||
fmt_ns,
|
||||
group="ns",
|
||||
)
|
||||
for op, op_title in [("add", "Add"), ("mul", "Multiply"), ("div", "Divide")]
|
||||
]
|
||||
render(
|
||||
"Cost of one operation (full-width operands, lower is faster)",
|
||||
CORE_ROWS,
|
||||
core_panels,
|
||||
out_dir / "number-core-ops.svg",
|
||||
)
|
||||
|
||||
def kernel(name: str) -> dict[str, dict]:
|
||||
prefix = f"kernel/{name}/"
|
||||
return {k[len(prefix) :]: v for k, v in kernels.items() if k.startswith(prefix)}
|
||||
|
||||
for name, title, unit, divisor, fmt in [
|
||||
("amm_swap_in", "AMM swap-in", "ns per call", 1.0, fmt_ns),
|
||||
(
|
||||
"loan_amortize360",
|
||||
"360-payment amortization",
|
||||
"µs per schedule",
|
||||
1e3,
|
||||
fmt_us,
|
||||
),
|
||||
]:
|
||||
k = kernel(name)
|
||||
render(
|
||||
f"{title}: correct digits (higher is better) and cost (lower is faster)",
|
||||
KERNEL_ROWS,
|
||||
[
|
||||
Panel(
|
||||
"Correct digits, worst case",
|
||||
"significant digits",
|
||||
{s: b["digits_min"] for s, b in k.items()},
|
||||
fmt_digits,
|
||||
group="digits",
|
||||
),
|
||||
Panel(
|
||||
"Correct digits, median",
|
||||
"significant digits",
|
||||
{s: b["digits_median"] for s, b in k.items()},
|
||||
fmt_digits,
|
||||
group="digits",
|
||||
),
|
||||
Panel(
|
||||
"Cost",
|
||||
unit,
|
||||
{s: ns_per_item(b) / divisor for s, b in k.items()},
|
||||
fmt,
|
||||
),
|
||||
],
|
||||
out_dir / f"number-{name.replace('_', '-')}.svg",
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
555
src/benchmarks/libxrpl/number_diff/main.cpp
Normal file
555
src/benchmarks/libxrpl/number_diff/main.cpp
Normal file
@@ -0,0 +1,555 @@
|
||||
// number_diff: measures how far Number's results are from correctly rounded
|
||||
// results. See docs/NumberDecimalBenchmark.md, "Divergence harness".
|
||||
//
|
||||
// For every operation, the operands are converted exactly into a 96-digit
|
||||
// mpdecimal value and the operation is evaluated there (correctly rounded at
|
||||
// 96 digits). That near-exact result is then rounded onto the subject type's
|
||||
// own grid of representable values in the active rounding mode, and compared
|
||||
// with what the subject actually returned.
|
||||
//
|
||||
// Rounding a 96-digit result again creates a false tie only if digits 20-96
|
||||
// of a non-terminating result happen to round to exactly 5000...0, which
|
||||
// will not occur at these sample sizes.
|
||||
//
|
||||
// Usage: xrpl.bench.number_diff [--samples N] [--csv]
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
|
||||
#include <benchmarks/libxrpl/number/BoostDecimal.h>
|
||||
#include <benchmarks/libxrpl/number/MpDecimal.h>
|
||||
#include <benchmarks/libxrpl/number/Number128.h>
|
||||
#include <benchmarks/libxrpl/number/NumberLike.h>
|
||||
#include <benchmarks/libxrpl/number/Operands.h>
|
||||
|
||||
#ifdef XRPL_BENCH_INTEL_DFP
|
||||
#include <benchmarks/libxrpl/number/IntelBid.h>
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <format>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::number_bench {
|
||||
namespace {
|
||||
|
||||
using Exact = MpDecimal<96>;
|
||||
|
||||
constexpr std::uint64_t kSeedLhs = 0xd1ff'0001;
|
||||
constexpr std::uint64_t kSeedRhs = 0xd1ff'0002;
|
||||
|
||||
/**
|
||||
* The set of values a type can represent, as far as rounding is concerned.
|
||||
* `int64Cap` models Number's large scales, whose mantissas above 2^63-1 must
|
||||
* end in 0 (see Number.h, "External Interface").
|
||||
*/
|
||||
struct Grid
|
||||
{
|
||||
unsigned digits;
|
||||
bool int64Cap = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Integers: the grid of conversions to int64.
|
||||
*/
|
||||
struct IntegerGrid
|
||||
{
|
||||
};
|
||||
|
||||
// ---- Exact arithmetic helpers ----
|
||||
|
||||
Exact
|
||||
fromSubject(DecomposableNumber auto const& x)
|
||||
{
|
||||
return Exact::fromDecomposed(decompose(x));
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds a non-negative value to `digits` significant digits, toward (down) or away from (up)
|
||||
* zero.
|
||||
*/
|
||||
Exact
|
||||
roundMagnitude(Exact x, unsigned digits, bool up)
|
||||
{
|
||||
mpd_context_t ctx;
|
||||
mpd_maxcontext(&ctx);
|
||||
ctx.prec = digits;
|
||||
ctx.round = up ? MPD_ROUND_UP : MPD_ROUND_DOWN;
|
||||
ctx.traps = 0;
|
||||
std::uint32_t status = 0;
|
||||
mpd_qfinalize(x.get(), &ctx, &status);
|
||||
return x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds a non-negative value to an integer, toward (down) or away from (up) zero.
|
||||
*/
|
||||
Exact
|
||||
roundMagnitudeToInteger(Exact const& x, bool up)
|
||||
{
|
||||
mpd_context_t ctx;
|
||||
mpd_maxcontext(&ctx);
|
||||
ctx.round = up ? MPD_ROUND_UP : MPD_ROUND_DOWN;
|
||||
ctx.traps = 0;
|
||||
std::uint32_t status = 0;
|
||||
Exact result;
|
||||
mpd_qround_to_int(result.get(), x.get(), &ctx, &status);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool
|
||||
isZero(Exact const& x)
|
||||
{
|
||||
return mpd_iszero(x.get()) != 0;
|
||||
}
|
||||
|
||||
bool
|
||||
isNegative(Exact const& x)
|
||||
{
|
||||
return mpd_isnegative(x.get()) != 0 && !isZero(x);
|
||||
}
|
||||
|
||||
/**
|
||||
* True if q (a non-negative integer) is even.
|
||||
*/
|
||||
bool
|
||||
isEvenInteger(Exact const& q)
|
||||
{
|
||||
auto const s = to_string(q);
|
||||
auto const pos = s.find_first_of("Ee");
|
||||
// Positive exponent in scientific notation means trailing zeros.
|
||||
if (pos != std::string::npos && s[pos + 1] != '-')
|
||||
return true;
|
||||
auto const digits = s.substr(0, pos);
|
||||
return ((digits.back() - '0') % 2) == 0;
|
||||
}
|
||||
|
||||
// ---- Candidate representable values around an exact result ----
|
||||
|
||||
/**
|
||||
* The representable magnitudes immediately at or below and at or above |x|.
|
||||
*/
|
||||
struct Bracket
|
||||
{
|
||||
Exact lo;
|
||||
Exact hi;
|
||||
/**
|
||||
* Grid spacing at lo: lo / spacing is lo's integer coefficient.
|
||||
*/
|
||||
Exact loSpacing;
|
||||
/**
|
||||
* lo is 2^63-1 and hi is its successor 2^63+3: the two candidates straddle Number's cap.
|
||||
*/
|
||||
bool straddlesCap = false;
|
||||
};
|
||||
|
||||
Bracket
|
||||
bracket(Exact const& magnitude, Grid grid)
|
||||
{
|
||||
auto const adjusted = static_cast<int>(mpd_adjexp(magnitude.get()));
|
||||
auto lo = roundMagnitude(magnitude, grid.digits, false);
|
||||
auto hi = roundMagnitude(magnitude, grid.digits, true);
|
||||
Exact loSpacing{1, adjusted - static_cast<int>(grid.digits) + 1};
|
||||
bool straddlesCap = false;
|
||||
if (grid.int64Cap)
|
||||
{
|
||||
// Above cap, only every tenth 19-digit value is representable.
|
||||
Exact const cap{static_cast<std::int64_t>(Number::kMaxRep), adjusted - 18};
|
||||
if (hi > cap)
|
||||
hi = roundMagnitude(magnitude, 18, true);
|
||||
if (lo > cap)
|
||||
{
|
||||
auto const lo18 = roundMagnitude(magnitude, 18, false);
|
||||
if (lo18 < cap)
|
||||
{
|
||||
lo = cap;
|
||||
straddlesCap = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
lo = lo18;
|
||||
loSpacing = Exact{1, adjusted - 17};
|
||||
}
|
||||
}
|
||||
}
|
||||
return {std::move(lo), std::move(hi), std::move(loSpacing), straddlesCap};
|
||||
}
|
||||
|
||||
/**
|
||||
* The correctly rounded magnitude, or nullopt if two candidates are equally valid (a cusp tie).
|
||||
*/
|
||||
std::optional<Exact>
|
||||
choose(Exact const& magnitude, Bracket const& b, bool negative, Number::RoundingMode mode)
|
||||
{
|
||||
using enum Number::RoundingMode;
|
||||
if (b.lo == b.hi)
|
||||
return b.lo;
|
||||
switch (mode)
|
||||
{
|
||||
case TowardsZero:
|
||||
return b.lo;
|
||||
case Downward:
|
||||
return negative ? b.hi : b.lo;
|
||||
case Upward:
|
||||
return negative ? b.lo : b.hi;
|
||||
case ToNearest:
|
||||
break;
|
||||
}
|
||||
auto const below = magnitude - b.lo;
|
||||
auto const above = b.hi - magnitude;
|
||||
if (below < above)
|
||||
return b.lo;
|
||||
if (above < below)
|
||||
return b.hi;
|
||||
// Tie at the cap: 2^63-1 and 2^63+3 are both odd, so "even" is undefined.
|
||||
if (b.straddlesCap)
|
||||
return std::nullopt;
|
||||
return isEvenInteger(b.lo / b.loSpacing) ? b.lo : b.hi;
|
||||
}
|
||||
|
||||
// ---- Tallies ----
|
||||
|
||||
struct Tally
|
||||
{
|
||||
std::size_t samples = 0;
|
||||
/**
|
||||
* Exactly the correctly rounded result.
|
||||
*/
|
||||
std::size_t correct = 0;
|
||||
/**
|
||||
* The other neighbor of the exact result: rounded in the wrong direction.
|
||||
*/
|
||||
std::size_t wrongNeighbor = 0;
|
||||
/**
|
||||
* Neither neighbor: off by more than one step.
|
||||
*/
|
||||
std::size_t worse = 0;
|
||||
/**
|
||||
* Returned zero for a non-zero result.
|
||||
*/
|
||||
std::size_t flushed = 0;
|
||||
/**
|
||||
* Threw instead of returning.
|
||||
*/
|
||||
std::size_t threw = 0;
|
||||
/**
|
||||
* Largest |result - exact| / local grid spacing.
|
||||
*/
|
||||
double maxError = 0;
|
||||
};
|
||||
|
||||
void
|
||||
classify(Tally& t, Exact const& result, Exact const& exact, Grid grid, Number::RoundingMode mode)
|
||||
{
|
||||
if (isZero(exact))
|
||||
{
|
||||
++(isZero(result) ? t.correct : t.worse);
|
||||
return;
|
||||
}
|
||||
if (isZero(result))
|
||||
{
|
||||
++t.flushed;
|
||||
return;
|
||||
}
|
||||
bool const negative = isNegative(exact);
|
||||
auto const magnitude = abs(exact);
|
||||
auto const b = bracket(magnitude, grid);
|
||||
auto const resultMagnitude = abs(result);
|
||||
auto const chosen = choose(magnitude, b, negative, mode);
|
||||
|
||||
bool const sameSign = isNegative(result) == negative;
|
||||
bool const isLo = sameSign && resultMagnitude == b.lo;
|
||||
bool const isHi = sameSign && resultMagnitude == b.hi;
|
||||
if (sameSign && (chosen ? resultMagnitude == *chosen : (isLo || isHi)))
|
||||
++t.correct;
|
||||
else if (isLo || isHi)
|
||||
++t.wrongNeighbor;
|
||||
else
|
||||
++t.worse;
|
||||
|
||||
if (b.lo != b.hi)
|
||||
{
|
||||
auto const error = abs(result - exact) / (b.hi - b.lo);
|
||||
t.maxError = std::max(t.maxError, std::stod(to_string(error)));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
classifyInteger(Tally& t, std::int64_t result, Exact const& exact, Number::RoundingMode mode)
|
||||
{
|
||||
bool const negative = isNegative(exact);
|
||||
auto const magnitude = abs(exact);
|
||||
auto const lo = roundMagnitudeToInteger(magnitude, false);
|
||||
auto const hi = roundMagnitudeToInteger(magnitude, true);
|
||||
Bracket const b{lo, hi, Exact{1}};
|
||||
auto const chosen = *choose(magnitude, b, negative, mode);
|
||||
Exact const r{result};
|
||||
auto const resultMagnitude = abs(r);
|
||||
bool const sameSign = result == 0 || (result < 0) == negative;
|
||||
if (sameSign && resultMagnitude == chosen)
|
||||
++t.correct;
|
||||
else if (sameSign && (resultMagnitude == lo || resultMagnitude == hi))
|
||||
++t.wrongNeighbor;
|
||||
else
|
||||
++t.worse;
|
||||
t.maxError = std::max(t.maxError, std::stod(to_string(abs(r - exact))));
|
||||
}
|
||||
|
||||
// ---- Running cells ----
|
||||
|
||||
struct Row
|
||||
{
|
||||
std::string subject;
|
||||
Number::RoundingMode mode;
|
||||
std::string op;
|
||||
std::string data;
|
||||
Tally tally;
|
||||
};
|
||||
|
||||
template <DecomposableNumber T, class BinaryOp>
|
||||
Tally
|
||||
runBinary(
|
||||
std::vector<RawOperand> const& lhs,
|
||||
std::vector<RawOperand> const& rhs,
|
||||
BinaryOp op,
|
||||
Grid grid,
|
||||
Number::RoundingMode mode)
|
||||
{
|
||||
Tally t;
|
||||
for (std::size_t i = 0; i < lhs.size(); ++i)
|
||||
{
|
||||
T const a{lhs[i].mantissa, lhs[i].exponent};
|
||||
T const b{rhs[i].mantissa, rhs[i].exponent};
|
||||
auto const exact = op(fromSubject(a), fromSubject(b));
|
||||
++t.samples;
|
||||
try
|
||||
{
|
||||
classify(t, fromSubject(T{op(a, b)}), exact, grid, mode);
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
++t.threw;
|
||||
}
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
template <DecomposableNumber T, class UnaryOp>
|
||||
Tally
|
||||
runUnary(std::vector<RawOperand> const& operands, UnaryOp op, Grid grid, Number::RoundingMode mode)
|
||||
{
|
||||
Tally t;
|
||||
for (auto const& raw : operands)
|
||||
{
|
||||
T const a{raw.mantissa, raw.exponent};
|
||||
auto const exact = op(fromSubject(a));
|
||||
++t.samples;
|
||||
try
|
||||
{
|
||||
classify(t, fromSubject(T{op(a)}), exact, grid, mode);
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
++t.threw;
|
||||
}
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
template <DecomposableNumber T>
|
||||
Tally
|
||||
runToInt64(std::vector<RawOperand> const& operands, Number::RoundingMode mode)
|
||||
{
|
||||
Tally t;
|
||||
for (auto const& raw : operands)
|
||||
{
|
||||
T const a{raw.mantissa, raw.exponent};
|
||||
auto const exact = fromSubject(a);
|
||||
++t.samples;
|
||||
try
|
||||
{
|
||||
classifyInteger(t, static_cast<std::int64_t>(a), exact, mode);
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
++t.threw;
|
||||
}
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs every operation and dataset for one subject type and rounding mode.
|
||||
*/
|
||||
template <DecomposableNumber T>
|
||||
void
|
||||
runSubject(
|
||||
std::string_view subject,
|
||||
Grid grid,
|
||||
Number::RoundingMode mode,
|
||||
std::size_t samples,
|
||||
std::vector<Row>& rows)
|
||||
{
|
||||
auto const saved = T::setround(mode);
|
||||
auto const record = [&](std::string op, Dataset d, Tally t) {
|
||||
rows.push_back({std::string{subject}, mode, std::move(op), std::string{toString(d)}, t});
|
||||
};
|
||||
auto const recordPairs = [&](std::string op, std::string data, Tally t) {
|
||||
rows.push_back({std::string{subject}, mode, std::move(op), std::move(data), t});
|
||||
};
|
||||
|
||||
auto const plus = [](auto const& x, auto const& y) { return x + y; };
|
||||
auto const minus = [](auto const& x, auto const& y) { return x - y; };
|
||||
auto const times = [](auto const& x, auto const& y) { return x * y; };
|
||||
auto const divide = [](auto const& x, auto const& y) { return x / y; };
|
||||
|
||||
for (auto const d : {Dataset::Full, Dataset::Iou, Dataset::Mpt})
|
||||
{
|
||||
auto const lhs = makeRaw(d, kSeedLhs, samples);
|
||||
auto const rhs = makeRaw(d, kSeedRhs, samples);
|
||||
record("add", d, runBinary<T>(lhs, rhs, plus, grid, mode));
|
||||
record("sub", d, runBinary<T>(lhs, rhs, minus, grid, mode));
|
||||
record("mul", d, runBinary<T>(lhs, rhs, times, grid, mode));
|
||||
record("div", d, runBinary<T>(lhs, rhs, divide, grid, mode));
|
||||
record("root2", d, runUnary<T>(lhs, [](auto const& x) { return root2(x); }, grid, mode));
|
||||
}
|
||||
|
||||
for (int const gap : {1, 5, 18})
|
||||
{
|
||||
auto const [lhs, rhs] = makeExponentGapPairs(gap, kSeedLhs, samples);
|
||||
recordPairs("add", std::format("gap{}", gap), runBinary<T>(lhs, rhs, plus, grid, mode));
|
||||
recordPairs("sub", std::format("gap{}", gap), runBinary<T>(lhs, rhs, minus, grid, mode));
|
||||
}
|
||||
{
|
||||
auto const [lhs, rhs] = makeCancellationPairs(kSeedLhs, samples);
|
||||
recordPairs("sub", "cancel", runBinary<T>(lhs, rhs, minus, grid, mode));
|
||||
}
|
||||
{
|
||||
auto const [lhs, rhs] = makeTiePairs(kSeedLhs, samples);
|
||||
recordPairs("add", "tie", runBinary<T>(lhs, rhs, plus, grid, mode));
|
||||
}
|
||||
{
|
||||
auto const [lhs, rhs] = makeCuspPairs(kSeedLhs, samples);
|
||||
recordPairs("add", "cusp", runBinary<T>(lhs, rhs, plus, grid, mode));
|
||||
}
|
||||
|
||||
auto const rates = makeRaw(Dataset::Rate, kSeedLhs, samples);
|
||||
for (unsigned const n : {12u, 360u})
|
||||
{
|
||||
record(
|
||||
std::format("power{}", n),
|
||||
Dataset::Rate,
|
||||
runUnary<T>(rates, [n](auto const& x) { return power(x, n); }, grid, mode));
|
||||
}
|
||||
|
||||
for (auto const d : {Dataset::Fraction, Dataset::Half})
|
||||
record("to_int64", d, runToInt64<T>(makeRaw(d, kSeedLhs, samples), mode));
|
||||
|
||||
T::setround(saved);
|
||||
}
|
||||
|
||||
void
|
||||
print(std::vector<Row> const& rows, bool csv)
|
||||
{
|
||||
if (csv)
|
||||
std::cout << "subject,mode,op,data,samples,correct,wrong_neighbor,worse,flushed,threw,max_"
|
||||
"error\n";
|
||||
else
|
||||
std::cout << "| subject | mode | op | data | samples | correct | wrong neighbor | worse | "
|
||||
"flushed | threw | max error |\n"
|
||||
"|---|---|---|---|---:|---:|---:|---:|---:|---:|---:|\n";
|
||||
for (auto const& r : rows)
|
||||
{
|
||||
auto const& t = r.tally;
|
||||
auto const mode = to_string(r.mode);
|
||||
auto const fmt = csv ? "{},{},{},{},{},{},{},{},{},{},{:.4g}\n"
|
||||
: "| {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {:.4g} |\n";
|
||||
std::cout << std::vformat(
|
||||
fmt,
|
||||
std::make_format_args(
|
||||
r.subject,
|
||||
mode,
|
||||
r.op,
|
||||
r.data,
|
||||
t.samples,
|
||||
t.correct,
|
||||
t.wrongNeighbor,
|
||||
t.worse,
|
||||
t.flushed,
|
||||
t.threw,
|
||||
t.maxError));
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
run(int argc, char** argv)
|
||||
{
|
||||
std::size_t samples = 10'000;
|
||||
bool csv = false;
|
||||
for (int i = 1; i < argc; ++i)
|
||||
{
|
||||
std::string_view const arg = argv[i];
|
||||
if (arg == "--samples" && i + 1 < argc)
|
||||
samples = std::strtoull(argv[++i], nullptr, 10);
|
||||
else if (arg == "--csv")
|
||||
csv = true;
|
||||
else
|
||||
{
|
||||
std::cerr << "usage: " << argv[0] << " [--samples N] [--csv]\n";
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
using enum Number::RoundingMode;
|
||||
using enum MantissaRange::MantissaScale;
|
||||
constexpr auto kModes = std::to_array({ToNearest, TowardsZero, Downward, Upward});
|
||||
|
||||
std::vector<Row> rows;
|
||||
for (auto const mode : kModes)
|
||||
{
|
||||
for (auto const scale : {Small, LargeLegacy, Large320, Large330})
|
||||
{
|
||||
NumberMantissaScaleGuard const guard{scale};
|
||||
auto const grid =
|
||||
scale == Small ? Grid{.digits = 16} : Grid{.digits = 19, .int64Cap = true};
|
||||
runSubject<Number>("Number." + to_string(scale), grid, mode, samples, rows);
|
||||
}
|
||||
// Validate the oracle and the grid logic against IEEE implementations,
|
||||
// which must be correctly rounded for + - * / and sqrt.
|
||||
runSubject<BoostDecimal64>("BoostDecimal64", Grid{.digits = 16}, mode, samples, rows);
|
||||
runSubject<BoostDecimalFast64>(
|
||||
"BoostDecimalFast64", Grid{.digits = 16}, mode, samples, rows);
|
||||
runSubject<BoostDecimal128>("BoostDecimal128", Grid{.digits = 34}, mode, samples, rows);
|
||||
runSubject<BoostDecimalFast128>(
|
||||
"BoostDecimalFast128", Grid{.digits = 34}, mode, samples, rows);
|
||||
#ifdef XRPL_BENCH_INTEL_DFP
|
||||
runSubject<IntelBid64>("IntelBid64", Grid{.digits = 16}, mode, samples, rows);
|
||||
runSubject<IntelBid128>("IntelBid128", Grid{.digits = 34}, mode, samples, rows);
|
||||
#endif
|
||||
#ifdef XRPL_BENCH_HAS_NUMBER128
|
||||
runSubject<Number128>("Number128", Grid{.digits = Number128::kDigits}, mode, samples, rows);
|
||||
#endif
|
||||
}
|
||||
print(rows, csv);
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace xrpl::number_bench
|
||||
|
||||
int
|
||||
main(int argc, char** argv)
|
||||
{
|
||||
return xrpl::number_bench::run(argc, argv);
|
||||
}
|
||||
39
src/libxrpl/basics/CountedObject.cpp
Normal file
39
src/libxrpl/basics/CountedObject.cpp
Normal file
@@ -0,0 +1,39 @@
|
||||
#include <xrpl/basics/CountedObject.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
CountedObjects&
|
||||
CountedObjects::getInstance() noexcept
|
||||
{
|
||||
static CountedObjects kInstance;
|
||||
|
||||
return kInstance;
|
||||
}
|
||||
|
||||
CountedObjects::CountedObjects() noexcept : count_(0), head_(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
CountedObjects::List
|
||||
CountedObjects::getCounts(int minimumThreshold) const
|
||||
{
|
||||
List counts;
|
||||
|
||||
// When other operations are concurrent, the count
|
||||
// might be temporarily less than the actual count.
|
||||
counts.reserve(count_.load());
|
||||
|
||||
for (auto* ctr = head_.load(); ctr != nullptr; ctr = ctr->getNext())
|
||||
{
|
||||
if (ctr->getCount() >= minimumThreshold)
|
||||
counts.emplace_back(ctr->getName(), ctr->getCount());
|
||||
}
|
||||
|
||||
std::ranges::sort(counts);
|
||||
|
||||
return counts;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -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()))
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/LedgerTiming.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/FeeSettingsEntry.h>
|
||||
#include <xrpl/nodestore/NodeObject.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Fees.h>
|
||||
@@ -561,7 +560,7 @@ Ledger::setup()
|
||||
|
||||
try
|
||||
{
|
||||
if (auto const sle = FeeSettingsEntryR(*this))
|
||||
if (auto const sle = read(keylet::feeSettings()))
|
||||
{
|
||||
bool oldFees = false;
|
||||
bool newFees = false;
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user