mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-21 19:18:22 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60d3b0082f | ||
|
|
f4e5897e7c | ||
|
|
e2ee835952 | ||
|
|
f1723d8647 | ||
|
|
320f9c8866 | ||
|
|
229df04edd | ||
|
|
640428a1d4 | ||
|
|
0363c12b23 |
No files matched your search
+1
-1
@@ -166,6 +166,7 @@ words:
|
||||
- legleux
|
||||
- levelization
|
||||
- levelized
|
||||
- lgrdb
|
||||
- libpb
|
||||
- libxrpl
|
||||
- llection
|
||||
@@ -255,7 +256,6 @@ words:
|
||||
- queuable
|
||||
- Raphson
|
||||
- rcflags
|
||||
- reencrypted
|
||||
- replayer
|
||||
- repodata
|
||||
- repomd
|
||||
|
||||
@@ -8,7 +8,3 @@ watch_file rust-toolchain.toml
|
||||
watch_dir conan
|
||||
|
||||
use flake
|
||||
|
||||
# Optional, untracked local overrides. To use a different shell, put e.g.
|
||||
# `use flake .#formal-verification` in .envrc.local.
|
||||
source_env_if_exists .envrc.local
|
||||
@@ -5,18 +5,6 @@
|
||||
# This file is sorted in reverse chronological order, with the most recent commits at the top.
|
||||
# The commits listed here are ignored by git blame, which is useful for formatting-only commits that would otherwise obscure the history of changes to a file.
|
||||
|
||||
# chore: CamelCase for typedef/using in `clang-tidy` (#8177)
|
||||
97a1824537d20d82d25bf151a37a7a4532ebf085
|
||||
# chore: Rename CamelCase namespaces to snake_case (#7933)
|
||||
06488c1318d96f56d0536251bee08ac85fa7fdd3
|
||||
# style: Unify style for all Doxygen comments (#7776)
|
||||
73b6852a122854140336e6e6bc30a3a4b41aa5fd
|
||||
# style: More clang-tidy identifier renaming (#7290)
|
||||
a830ab10efed8d3e59ef2fc15d66efdf9c6bb0d8
|
||||
# refactor: Rename static constants (#7120)
|
||||
5b6e8b6f93b19c1e3f6a3467a25639031d9d9a53
|
||||
# chore: More fixes for bad renames (#7092)
|
||||
7afdd71a54d562b32a50b29a5aa00bb997dc9053
|
||||
# refactor: Enable clang-tidy `readability-identifier-naming` check (#6571)
|
||||
8995564ed6b9e453e144bb663303072a3c1ba305
|
||||
# refactor: Enable remaining clang-tidy `cppcoreguidelines` checks (#6538)
|
||||
|
||||
@@ -36,4 +36,4 @@ runs:
|
||||
|
||||
- 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,4 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/bash
|
||||
|
||||
# Exit the script as soon as an error occurs.
|
||||
set -euo pipefail
|
||||
@@ -21,10 +21,11 @@ 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
|
||||
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
|
||||
|
||||
# Each line is "<patch-id> <commit>".
|
||||
RELEASE_PATCHES=$(patch_ids "${BRANCHES[@]}" --not "${HEAD}")
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/bash
|
||||
|
||||
# Exit the script as soon as an error occurs.
|
||||
set -euo pipefail
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -166,31 +166,10 @@ jobs:
|
||||
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 }}
|
||||
BASE: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
|
||||
HEAD: ${{ github.event.pull_request.head.sha || github.event.merge_group.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
|
||||
|
||||
@@ -269,23 +269,14 @@ jobs:
|
||||
env:
|
||||
CONTEXT: image-context
|
||||
IMAGE: ${{ matrix.target == 'voidstar' && 'xrpld-voidstar' || 'xrplf/xrpld' }}:${{ github.ref_type == 'tag' && github.ref_name || 'develop' }}
|
||||
# Docker Hub is public, so a private build never reaches it;
|
||||
# the Antithesis registry is not.
|
||||
PUSH: ${{ inputs.publish && (matrix.target == 'voidstar' || github.event.repository.visibility == 'public') }}
|
||||
|
||||
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:
|
||||
|
||||
@@ -92,9 +92,6 @@ target/
|
||||
# Direnv's directory
|
||||
/.direnv
|
||||
|
||||
# Direnv's local, per-developer overrides
|
||||
/.envrc.local
|
||||
|
||||
# clangd cache
|
||||
/.cache
|
||||
|
||||
|
||||
@@ -22,14 +22,6 @@ API version 2 is available in `xrpld` version 2.0.0 and later. See [API-VERSION-
|
||||
|
||||
This version is supported by all `xrpld` versions. For WebSocket and HTTP JSON-RPC requests, it is currently the default API version used when no `api_version` is specified.
|
||||
|
||||
## XRP Ledger server version 3.5.0
|
||||
|
||||
Version 3.5.0 is not yet released.
|
||||
|
||||
### Additions in 3.5.0
|
||||
|
||||
- `subscribe`, `unsubscribe`: Added an optional `mpt_issuances` request field, an array of MPT issuance IDs (hex strings). Subscribers receive a message with `type` `mptTransaction` for each validated transaction whose metadata affects a subscribed issuance; the message has the same fields as the `transactions` stream. MPT issuance subscriptions count toward the per-connection subscription limit. An empty array, a non-array value, or an invalid ID returns `invalidParams`. ([#5671](https://github.com/XRPLF/rippled/pull/5671))
|
||||
|
||||
## XRP Ledger server version 3.4.0
|
||||
|
||||
Version 3.4.0 is not yet released. These changes are available in the 3.4.0 beta releases.
|
||||
|
||||
@@ -41,13 +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.
|
||||
For a stable release, choose one of the [tagged
|
||||
releases](https://github.com/XRPLF/rippled/releases).
|
||||
|
||||
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
|
||||
|
||||
+586
-20
@@ -14,13 +14,9 @@ The following branches exist in the main project repository:
|
||||
|
||||
- `develop`: The latest set of unreleased features, and the most common
|
||||
starting point for contributions.
|
||||
- `staging/*` (e.g. `staging/3.4.x`): Staging branches, one per release line,
|
||||
where fixes for that line are developed.
|
||||
- `release/*` (e.g. `release/3.4.x`): Release branches, one per release line,
|
||||
holding the latest public release candidate or release for that line.
|
||||
Releases are published as [tagged releases](https://github.com/XRPLF/rippled/releases).
|
||||
|
||||
See [RELEASING.md](./RELEASING.md) for how these branches are used.
|
||||
- `release/*` (e.g. `release/3.2.x`): Release branches, one per release line,
|
||||
holding the latest release candidate, or stable release for that line.
|
||||
Stable releases are published as [tagged releases](https://github.com/XRPLF/rippled/releases).
|
||||
|
||||
The tip of each branch must be signed. In order for GitHub to sign a
|
||||
squashed commit that it builds from your pull request, GitHub must know
|
||||
@@ -149,8 +145,8 @@ tl;dr
|
||||
|
||||
In general, pull requests use `develop` as the base branch.
|
||||
|
||||
The exceptions are fixes for an existing release line,
|
||||
which use that line's staging branch (e.g. `staging/3.4.x`) as the base.
|
||||
The exceptions are fixes, improvements, and hotfixes for an existing release,
|
||||
which use that release's branch (e.g. `release/3.2.x`) as the base.
|
||||
|
||||
If your changes are not quite ready, but you want to make it easily available
|
||||
for preliminary examination or review, you can create a "Draft" pull request.
|
||||
@@ -595,16 +591,15 @@ the suggested commit message, or modify it as needed.
|
||||
|
||||
#### Slightly more complicated pull requests
|
||||
|
||||
Some pull requests need to be pushed to their base branch (usually `develop`)
|
||||
as more than one commit.
|
||||
A PR author may _request_ to merge as separate commits. They
|
||||
Some pull requests need to be pushed to `develop` as more than one
|
||||
commit. A PR author may _request_ to merge as separate commits. They
|
||||
must _justify_ why separate commits are needed, and _specify_ how they
|
||||
would like the commits to be merged. If you disagree with the author,
|
||||
discuss it with them directly.
|
||||
|
||||
If the process is reasonable, follow it. The simplest option is to do a
|
||||
fast forward only merge (`--ff-only`) on the command line
|
||||
and push to the base branch.
|
||||
fast forward only merge (`--ff-only`) on the command line and push to
|
||||
`develop`.
|
||||
|
||||
Some examples of when separate commits are worthwhile are:
|
||||
|
||||
@@ -617,10 +612,9 @@ Some examples of when separate commits are worthwhile are:
|
||||
|
||||
Either way, check that:
|
||||
|
||||
- The commits are based on the current tip of the base branch.
|
||||
- The commits are clean:
|
||||
No merge commits (except when merging a release, see [RELEASING.md](./RELEASING.md)),
|
||||
no "[FOLD]" or "fixup!" messages.
|
||||
- The commits are based on the current tip of `develop`.
|
||||
- The commits are clean: No merge commits (except when reverse
|
||||
merging), no "[FOLD]" or "fixup!" messages.
|
||||
- All commits are signed. If the commits are not signed by the author, use
|
||||
`git commit --amend -S` to sign them yourself.
|
||||
- At least one (but preferably all) of the commits has the PR number
|
||||
@@ -632,8 +626,578 @@ use them!**
|
||||
|
||||
### Releases
|
||||
|
||||
Releases, release branches, and merging releases back into `develop`
|
||||
are described in [RELEASING.md](./RELEASING.md).
|
||||
All releases, including release candidates and betas, are handled
|
||||
differently from typical PRs. Most importantly, never use
|
||||
the Github UI to merge a release.
|
||||
|
||||
Xrpld uses a linear workflow model that can be summarized as:
|
||||
|
||||
1. In between releases, developers work against the `develop` branch.
|
||||
2. Periodically, a maintainer will build and tag a beta version from
|
||||
`develop`, which is pushed to `release`.
|
||||
- Betas are usually released every two to three weeks, though that
|
||||
schedule can vary depending on progress, availability, and other
|
||||
factors.
|
||||
3. When the changes in `develop` are considered stable and mature enough
|
||||
to be ready to release, a release candidate (RC) is built and tagged
|
||||
from `develop`, and merged to `release`.
|
||||
- Further development for that release (primarily fixes) then
|
||||
continues against `release`, while other development continues on
|
||||
`develop`. Effectively, `release` is forked from `develop`. Changes
|
||||
to `release` must be reverse merged to `develop`.
|
||||
4. When the candidate has passed testing and is ready for release, the
|
||||
final release is merged to `master`.
|
||||
5. If any issues are found post-release, a hotfix / point release may be
|
||||
created, which is merged to `master`, and then reverse merged to
|
||||
`develop`.
|
||||
|
||||
#### Betas, and the first release candidate
|
||||
|
||||
##### Preparing the `develop` branch
|
||||
|
||||
1. Optimally, the `develop` branch will be ready to go, with all
|
||||
relevant PRs already merged.
|
||||
2. If there are any PRs pending, merge them **BEFORE** preparing the beta.
|
||||
1. If only one or two PRs need to be merged, merge those PRs [as
|
||||
normal](#when-and-how-to-merge-pull-requests), updating the second
|
||||
one, and waiting for CI to finish in between.
|
||||
2. If there are several pending PRs, do not use the Github UI,
|
||||
because the delays waiting for CI in between each merge will be
|
||||
unnecessarily onerous. (Incidentally, this process can also be
|
||||
used to merge if the Github UI has issues.) Merge each PR branch
|
||||
directly to a `release-next` on your local machine and create a single
|
||||
PR, then push your branch to `develop`.
|
||||
1. Squash the changes from each PR, one commit each (unless more
|
||||
are needed), being sure to sign each commit and update the
|
||||
commit message to include the PR number. You may be able to use
|
||||
a fast-forward merge for the first PR.
|
||||
2. Push your branch.
|
||||
3. Continue to [Making the release](#making-the-release) to update
|
||||
the version number, etc.
|
||||
|
||||
The workflow may look something like:
|
||||
|
||||
```
|
||||
git fetch --multiple upstreams user1 user2 user3 [...]
|
||||
git checkout -B release-next --no-track upstream/develop
|
||||
|
||||
# Only do an ff-only merge if pr-branch1 is either already
|
||||
# squashed, or needs to be merged with separate commits,
|
||||
# and has no merge commits.
|
||||
# Use -S on the ff-only merge if pr-branch1 isn't signed.
|
||||
git merge [-S] --ff-only user1/pr-branch1
|
||||
|
||||
git merge --squash user2/pr-branch2
|
||||
git commit -S # Use the commit message provided on the PR
|
||||
|
||||
git merge --squash user3/pr-branch3
|
||||
git commit -S # Use the commit message provided on the PR
|
||||
|
||||
[...]
|
||||
|
||||
# Make sure the commits look right
|
||||
git log --show-signature "upstream/develop..HEAD"
|
||||
|
||||
git push --set-upstream origin
|
||||
|
||||
# Continue to "Making the release" to update the version number, so
|
||||
# everything can be done in one PR.
|
||||
```
|
||||
|
||||
You can also use the [squash-branches] script.
|
||||
|
||||
You may also need to manually close the open PRs after the changes are
|
||||
merged to `develop`. Be sure to include the commit ID.
|
||||
|
||||
##### Making the release
|
||||
|
||||
This includes, betas, and the first release candidate (RC).
|
||||
|
||||
1. If you didn't create one [preparing the `develop`
|
||||
branch](#preparing-the-develop-branch), Ensure there is no old
|
||||
`release-next` branch hanging around. Then make a `release-next`
|
||||
branch that only changes the version number. e.g.
|
||||
|
||||
```
|
||||
git fetch upstreams
|
||||
|
||||
git checkout --no-track -B release-next upstream/develop
|
||||
|
||||
v="A.B.C-bD"
|
||||
build=$( find -name BuildInfo.cpp )
|
||||
sed 's/\(^.*versionString =\).*$/\1 "'${v}'"/' ${build} > version.cpp && mv -vi version.cpp ${build}
|
||||
|
||||
git diff
|
||||
|
||||
git add ${build}
|
||||
|
||||
git commit -S -m "Set version to ${v}"
|
||||
|
||||
# You could use your "origin" repo, but some CI tests work better on upstream.
|
||||
git push upstream-push
|
||||
git fetch upstreams
|
||||
git branch --set-upstream-to=upstream/release-next
|
||||
```
|
||||
|
||||
You can also use the [update-version] script. 2. Create a Pull Request for `release-next` with **`develop`** as
|
||||
the base branch.
|
||||
|
||||
1. Use the title "[TRIVIAL] Set version to X.X.X-bX".
|
||||
2. Instead of the default description template, use the following:
|
||||
|
||||
```
|
||||
## High Level Overview of Change
|
||||
|
||||
This PR only changes the version number. It will be merged as
|
||||
soon as Github CI actions successfully complete.
|
||||
```
|
||||
|
||||
3. Wait for CI to successfully complete, and get someone to approve
|
||||
the PR. (It is safe to ignore known CI issues.)
|
||||
4. Push the updated `develop` branch using your `release-next`
|
||||
branch. **Do not use the Github UI. It's important to preserve
|
||||
commit IDs.**
|
||||
|
||||
```
|
||||
git push upstream-push release-next:develop
|
||||
```
|
||||
|
||||
5. In the unlikely event that the push fails because someone has merged
|
||||
something else in the meantime, rebase your branch onto the updated
|
||||
`develop` branch, push again, and go back to step 3.
|
||||
6. Ensure that your PR against `develop` is closed. Github should do it
|
||||
automatically.
|
||||
7. Once this is done, forward progress on `develop` can continue
|
||||
(other PRs may be merged).
|
||||
8. Now create a Pull Request for `release-next` with **`release`** as
|
||||
the base branch. Instead of the default template, reuse and update
|
||||
the message from the previous release. Include the following verbiage
|
||||
somewhere in the description:
|
||||
|
||||
```
|
||||
The base branch is `release`. [All releases (including
|
||||
betas)](https://github.com/XRPLF/rippled/blob/develop/CONTRIBUTING.md#before-you-start)
|
||||
go in `release`. This PR branch will be pushed directly to `release` (not
|
||||
squashed or rebased, and not using the GitHub UI).
|
||||
```
|
||||
|
||||
7. Sign-offs for the three platforms (Linux, Mac, Windows) usually occur
|
||||
offline, but at least one approval will be needed on the PR.
|
||||
- If issues are discovered during testing, simply abandon the
|
||||
release. It's easy to start a new release, it should be easy to
|
||||
abandon one. **DO NOT REUSE THE VERSION NUMBER.** e.g. If you
|
||||
abandon 2.4.0-b1, the next attempt will be 2.4.0-b2.
|
||||
8. Once everything is ready to go, push to `release`.
|
||||
|
||||
```
|
||||
git fetch upstreams
|
||||
|
||||
# Just to be safe, do a dry run first:
|
||||
git push --dry-run upstream-push release-next:release
|
||||
|
||||
# If everything looks right, push the branch
|
||||
git push upstream-push release-next:release
|
||||
|
||||
# Check that all of the branches are updated
|
||||
git fetch upstreams
|
||||
git log -1 --oneline
|
||||
# The output should look like:
|
||||
# 0123456789 (HEAD -> upstream/release-next, upstream/release,
|
||||
# upstream/develop) Set version to 2.4.0-b1
|
||||
# Note that upstream/develop may not be on this commit, but
|
||||
# upstream/release must be.
|
||||
# Other branches, including some from upstream-push, may also be
|
||||
# present.
|
||||
```
|
||||
|
||||
9. Tag the release, too.
|
||||
|
||||
```
|
||||
git tag <version number>
|
||||
git push upstream-push <version number>
|
||||
```
|
||||
|
||||
10. Delete the `release-next` branch on the repo. Use the Github UI or:
|
||||
|
||||
```
|
||||
git push --delete upstream-push release-next
|
||||
```
|
||||
|
||||
11. Finally [create a new release on
|
||||
Github](https://github.com/XRPLF/rippled/releases).
|
||||
|
||||
#### Release candidates after the first
|
||||
|
||||
Once the first release candidate is [merged into
|
||||
release](#making-the-release), then `release` and `develop` _are allowed
|
||||
to diverge_.
|
||||
|
||||
If a bug or issue is discovered in a version that has a release
|
||||
candidate being tested, any fix and new version will need to be applied
|
||||
against `release`, then reverse-merged to `develop`. This helps keep git
|
||||
history as linear as possible.
|
||||
|
||||
A `release-next` branch will be created from `release`, and any further
|
||||
work for that release must be based on `release-next`. Specifically,
|
||||
PRs must use `release-next` as the base, and those PRs will be merged
|
||||
directly to `release-next` when approved. Changes should be restricted
|
||||
to bug fixes, but other changes may be necessary from time to time.
|
||||
|
||||
1. Open any PRs for the pending release using `release-next` as the base,
|
||||
so they can be merged directly in to it. Unlike `develop`, though,
|
||||
`release-next` can be thrown away and recreated if necessary.
|
||||
2. Once a new release candidate is ready, create a version commit as in
|
||||
step 1 [above](#making-the-release) on `release-next`. You can use
|
||||
the [update-version] script for this, too.
|
||||
3. Jump to step 8 ("Now create a Pull Request for `release-next` with
|
||||
**`release`** as the base") from the process
|
||||
[above](#making-the-release) to merge `release-next` into `release`.
|
||||
|
||||
##### Follow up: reverse merge
|
||||
|
||||
Once the RC is merged and tagged, it needs to be reverse merged into
|
||||
`develop` as soon as possible.
|
||||
|
||||
1. Create a branch, based on `upstream/develop`.
|
||||
The branch name is not important, but could include "mergeNNNrcN".
|
||||
E.g. For release A.B.C-rcD, use `mergeABCrcD`.
|
||||
|
||||
```
|
||||
git fetch upstreams
|
||||
|
||||
git checkout --no-track -b mergeABCrcD upstream/develop
|
||||
```
|
||||
|
||||
2. Merge `release` into your branch.
|
||||
|
||||
```
|
||||
# I like the "--edit --log --verbose" parameters, but they are
|
||||
# not required.
|
||||
git merge upstream/release
|
||||
```
|
||||
|
||||
3. `BuildInfo.cpp` will have a conflict with the version number.
|
||||
Resolve it with the version from `develop` - the higher version.
|
||||
4. Push your branch to your repo (or `upstream` if you have permission),
|
||||
and open a normal PR against `develop`. The "High level overview" can
|
||||
simply indicate that this is a merge of the RC. The "Context" should
|
||||
summarize the changes from the RC. Include the following text
|
||||
prominently:
|
||||
|
||||
```
|
||||
This PR must be merged manually using a push. Do not use the Github UI.
|
||||
```
|
||||
|
||||
5. Depending on the complexity of the changes, and/or merge conflicts,
|
||||
the PR may need a thorough review, or just a sign-off that the
|
||||
merge was done correctly.
|
||||
6. If `develop` is updated before this PR is merged, do not merge
|
||||
`develop` back into your branch. Instead rebase preserving merges,
|
||||
or do the merge again. (See also the `rerere` git config setting.)
|
||||
|
||||
```
|
||||
git rebase --rebase-merges upstream/develop
|
||||
# OR
|
||||
git reset --hard upstream/develop
|
||||
git merge upstream/release
|
||||
```
|
||||
|
||||
7. When the PR is ready, push it to `develop`.
|
||||
|
||||
```
|
||||
git fetch upstreams
|
||||
|
||||
# Make sure the commits look right
|
||||
git log --show-signature "upstream/develop^..HEAD"
|
||||
|
||||
git push upstream-push mergeABCrcD:develop
|
||||
|
||||
git fetch upstreams
|
||||
```
|
||||
|
||||
Development on `develop` can proceed as normal.
|
||||
|
||||
#### Final releases
|
||||
|
||||
A final release is any release that is not a beta or RC, such as 2.2.0.
|
||||
|
||||
Only code that has already been tested and vetted across all three
|
||||
platforms should be included in a final release. Most of the time, that
|
||||
means that the commit immediately preceding the commit setting the
|
||||
version number will be an RC. Occasionally, there may be last-minute bug
|
||||
fixes included as well. If so, those bug fixes must have been tested
|
||||
internally as if they were RCs (at minimum, ensuring unit tests pass,
|
||||
and the app starts, syncs, and stops cleanly across all three
|
||||
platforms.)
|
||||
|
||||
_If in doubt, make an RC first._
|
||||
|
||||
The process for building a final release is very similar to [the process
|
||||
for building a beta](#making-the-release), except the code will be
|
||||
moving from `release` to `master` instead of from `develop` to
|
||||
`release`, and both branches will be pushed at the same time.
|
||||
|
||||
1. Ensure there is no old `master-next` branch hanging around.
|
||||
Then make a `master-next` branch that only changes the version
|
||||
number. As above, or using the
|
||||
[update-version] script.
|
||||
2. Create a Pull Request for `master-next` with **`master`** as
|
||||
the base branch. Instead of the default template, reuse and update
|
||||
the message from the previous final release. Include the following verbiage
|
||||
somewhere in the description:
|
||||
|
||||
```
|
||||
The base branch is `master`. This PR branch will be pushed directly to
|
||||
`release` and `master` (not squashed or rebased, and not using the
|
||||
GitHub UI).
|
||||
```
|
||||
|
||||
7. Sign-offs for the three platforms (Linux, Mac, Windows) usually occur
|
||||
offline, but at least one approval will be needed on the PR.
|
||||
- If issues are discovered during testing, close the PR, delete
|
||||
`master-next`, and move development back to `release`, [issuing
|
||||
more RCs as necessary](#release-candidates-after-the-first)
|
||||
8. Once everything is ready to go, push to `release` and `master`.
|
||||
|
||||
```
|
||||
git fetch upstreams
|
||||
|
||||
# Just to be safe, do dry runs first:
|
||||
git push --dry-run upstream-push master-next:release
|
||||
git push --dry-run upstream-push master-next:master
|
||||
|
||||
# If everything looks right, push the branch
|
||||
git push upstream-push master-next:release
|
||||
git push upstream-push master-next:master
|
||||
|
||||
# Check that all of the branches are updated
|
||||
git fetch upstreams
|
||||
git log -1 --oneline
|
||||
# The output should look like:
|
||||
# 0123456789 (HEAD -> upstream/master-next, upstream/master,
|
||||
# upstream/release) Set version to A.B.0
|
||||
# Note that both upstream/release and upstream/master must be on this
|
||||
# commit.
|
||||
# Other branches, including some from upstream-push, may also be
|
||||
# present.
|
||||
```
|
||||
|
||||
9. Tag the release, too.
|
||||
|
||||
```
|
||||
git tag <version number>
|
||||
git push upstream-push <version number>
|
||||
```
|
||||
|
||||
10. Delete the `master-next` branch on the repo. Use the Github UI or:
|
||||
|
||||
```
|
||||
git push --delete upstream-push master-next
|
||||
```
|
||||
|
||||
11. [Create a new release on
|
||||
Github](https://github.com/XRPLF/rippled/releases). Be sure that
|
||||
"Set as the latest release" is checked.
|
||||
12. Open a PR to update the [API-CHANGELOG](API-CHANGELOG.md) and `API-VERSION-[n].md` with the changes for this release (if any are missing).
|
||||
13. Finally, [reverse merge the release into `develop`](#follow-up-reverse-merge).
|
||||
|
||||
#### Special cases: point releases, hotfixes, etc.
|
||||
|
||||
On occasion, a bug or issue is discovered in a version that already
|
||||
had a final release. Most of the time, development will have started
|
||||
on the next version, and will usually have changes in `develop`
|
||||
and often in `release`.
|
||||
|
||||
Because git history is kept as linear as possible, any fix and new
|
||||
version will need to be applied against `master`.
|
||||
|
||||
The process for building a hotfix release is very similar to [the
|
||||
process for building release candidates after the
|
||||
first](#release-candidates-after-the-first) and [for building a final
|
||||
release](#final-releases), except the changes will be done against
|
||||
`master` instead of `release`.
|
||||
|
||||
If there is only a single issue for the hotfix, the work can be done in
|
||||
any branch. When it's ready to merge, jump to step 3 using your branch
|
||||
instead of `master-next`.
|
||||
|
||||
1. Create a `master-next` branch from `master`.
|
||||
|
||||
```
|
||||
git checkout --no-track -b master-next upstream/master
|
||||
git push upstream-push
|
||||
git fetch upstreams
|
||||
```
|
||||
|
||||
2. Open any PRs for the pending hotfix using `master-next` as the base,
|
||||
so they can be merged directly in to it. Unlike `develop`, though,
|
||||
`master-next` can be thrown away and recreated if necessary.
|
||||
3. Once the hotfix is ready, create a version commit using the same
|
||||
steps as above, or use the
|
||||
[update-version] script.
|
||||
4. Create a Pull Request for `master-next` with **`master`** as
|
||||
the base branch. Instead of the default template, reuse and update
|
||||
the message from the previous final release. Include the following verbiage
|
||||
somewhere in the description:
|
||||
|
||||
```
|
||||
The base branch is `master`. This PR branch will be pushed directly to
|
||||
`master` (not squashed or rebased, and not using the GitHub UI).
|
||||
```
|
||||
|
||||
7. Sign-offs for the three platforms (Linux, Mac, Windows) usually occur
|
||||
offline, but at least one approval will be needed on the PR.
|
||||
- If issues are discovered during testing, update `master-next` as
|
||||
needed, but ensure that the changes are properly squashed, and the
|
||||
version setting commit remains last
|
||||
8. Once everything is ready to go, push to `master` **only**.
|
||||
|
||||
```
|
||||
git fetch upstreams
|
||||
|
||||
# Just to be safe, do a dry run first:
|
||||
git push --dry-run upstream-push master-next:master
|
||||
|
||||
# If everything looks right, push the branch
|
||||
git push upstream-push master-next:master
|
||||
|
||||
# Check that all of the branches are updated
|
||||
git fetch upstreams
|
||||
git log -1 --oneline
|
||||
# The output should look like:
|
||||
# 0123456789 (HEAD -> upstream/master-next, upstream/master) Set version
|
||||
# to 2.4.1
|
||||
# Note that upstream/master must be on this commit. upstream/release and
|
||||
# upstream/develop should not.
|
||||
# Other branches, including some from upstream-push, may also be
|
||||
# present.
|
||||
```
|
||||
|
||||
9. Tag the release, too.
|
||||
|
||||
```
|
||||
git tag <version number>
|
||||
git push upstream-push <version number>
|
||||
```
|
||||
|
||||
9. Delete the `master-next` branch on the repo.
|
||||
|
||||
```
|
||||
git push --delete upstream-push master-next
|
||||
```
|
||||
|
||||
10. [Create a new release on
|
||||
Github](https://github.com/XRPLF/rippled/releases). Be sure that
|
||||
"Set as the latest release" is checked.
|
||||
|
||||
Once the hotfix is released, it needs to be reverse merged into
|
||||
`develop` as soon as possible. It may also need to be merged into
|
||||
`release` if a release candidate is under development.
|
||||
|
||||
1. Create a branch in your own repo, based on `upstream/develop`.
|
||||
The branch name is not important, but could include "mergeNNN".
|
||||
E.g. For release 2.2.3, use `merge223`.
|
||||
|
||||
```
|
||||
git fetch upstreams
|
||||
|
||||
git checkout --no-track -b merge223 upstream/develop
|
||||
```
|
||||
|
||||
2. Merge master into your branch.
|
||||
|
||||
```
|
||||
# I like the "--edit --log --verbose" parameters, but they are
|
||||
# not required.
|
||||
git merge upstream/master
|
||||
```
|
||||
|
||||
3. `BuildInfo.cpp` will have a conflict with the version number.
|
||||
Resolve it with the version from `develop` - the higher version.
|
||||
4. Push your branch to your repo, and open a normal PR against
|
||||
`develop`. The "High level overview" can simply indicate that this
|
||||
is a merge of the hotfix version. The "Context" should summarize
|
||||
the changes from the hotfix. Include the following text
|
||||
prominently:
|
||||
|
||||
```
|
||||
This PR must be merged manually using a --ff-only merge. Do not use the Github UI.
|
||||
```
|
||||
|
||||
5. Depending on the complexity of the hotfix, and/or merge conflicts,
|
||||
the PR may need a thorough review, or just a sign-off that the
|
||||
merge was done correctly.
|
||||
6. If `develop` is updated before this PR is merged, do not merge
|
||||
`develop` back into your branch. Instead rebase preserving merges,
|
||||
or do the merge again. (See also the `rerere` git config setting.)
|
||||
|
||||
```
|
||||
git rebase --rebase-merges upstream/develop
|
||||
# OR
|
||||
git reset --hard upstream/develop
|
||||
git merge upstream/master
|
||||
```
|
||||
|
||||
7. When the PR is ready, push it to `develop`.
|
||||
|
||||
```
|
||||
git fetch upstreams
|
||||
|
||||
# Make sure the commits look right
|
||||
git log --show-signature "upstream/develop..HEAD"
|
||||
|
||||
git push upstream-push HEAD:develop
|
||||
```
|
||||
|
||||
Development on `develop` can proceed as normal. It is recommended to
|
||||
create a beta (or RC) immediately to ensure that everything worked as
|
||||
expected.
|
||||
|
||||
##### An even rarer scenario: A hotfix on an old release
|
||||
|
||||
Historically, once a final release is tagged and packages are released,
|
||||
versions older than the latest final release are no longer supported.
|
||||
However, there is a possibility that a very high severity bug may occur
|
||||
in a non-amendment blocked version that is still being run by
|
||||
a significant fraction of users, which would necessitate a hotfix / point
|
||||
release to that version as well as any later versions.
|
||||
|
||||
This scenario would follow the same basic procedure as above,
|
||||
except that _none_ of `develop`, `release`, or `master`
|
||||
would be touched during the release process.
|
||||
|
||||
In this example, consider if version 2.1.1 needed to be patched.
|
||||
|
||||
1. Create two branches in the main (`upstream`) repo.
|
||||
|
||||
```
|
||||
git fetch upstreams
|
||||
|
||||
# Create a base branch off the tag
|
||||
git checkout --no-track -b master-2.1.2 2.1.1
|
||||
git push upstream-push
|
||||
|
||||
# Create a working branch
|
||||
git checkout --no-track -b master212-next master-2.1.2
|
||||
git push upstream-push
|
||||
|
||||
git fetch upstreams
|
||||
```
|
||||
|
||||
2. Work continues as above, except using `master-2.1.2`as
|
||||
the base branch for any merging, packaging, etc.
|
||||
3. After the release is tagged and packages are built, you could
|
||||
potentially delete both branches, e.g. `master-2.1.2` and
|
||||
`master212-next`. However, it may be useful to keep `master-2.1.2`
|
||||
around indefinitely for reference.
|
||||
4. Assuming that a hotfix is also released for the latest
|
||||
version in parallel with this one, or if the issue is
|
||||
already fixed in the latest version, do no do any
|
||||
reverse merges. However, if it is not, it probably makes
|
||||
sense to reverse merge `master-2.1.2` into `master`,
|
||||
release a hotfix for _that_ version, then reverse merge
|
||||
from `master` to `develop`. (Please don't do this unless absolutely
|
||||
necessary.)
|
||||
|
||||
[contrib]: https://docs.github.com/en/get-started/quickstart/contributing-to-projects
|
||||
[squash]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-commits
|
||||
@@ -641,3 +1205,5 @@ are described in [RELEASING.md](./RELEASING.md).
|
||||
[xrpld]: https://github.com/XRPLF/rippled
|
||||
[signing]: https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification
|
||||
[setup-upstreams]: ./bin/git/setup-upstreams.sh
|
||||
[squash-branches]: ./bin/git/squash-branches.sh
|
||||
[update-version]: ./bin/git/update-version.sh
|
||||
-152
@@ -1,152 +0,0 @@
|
||||
# Branching and Release Management
|
||||
|
||||
This document describes how we branch, release, and merge releases back into `develop`.
|
||||
It does not define version naming
|
||||
(e.g., what constitutes a major-minor, patch, or beta release).
|
||||
|
||||
Examples use `X.Y` for a release line:
|
||||
`X` and `Y` are placeholders, while the trailing `x` is literal,
|
||||
e.g., `release/X.Y.x` is `release/3.4.x` for the `3.4` line.
|
||||
|
||||
## Branches
|
||||
|
||||
| Branch | Purpose |
|
||||
| :-------------- | :---------------------------------------------------------------------------------------- |
|
||||
| `develop` | Main development branch. Betas and the first RC of a major-minor release are tagged here. |
|
||||
| `staging/X.Y.x` | Where fixes for the `X.Y` line are developed and RCs are prepared. |
|
||||
| `release/X.Y.x` | The last public RC or release of the `X.Y` line. |
|
||||
|
||||
A release line is named `X.Y.x`
|
||||
because one branch serves every patch release of that line (`X.Y.0`, `X.Y.1`, `X.Y.2`, ...).
|
||||
The `/` groups branches hierarchically,
|
||||
so tools can filter them and protection rules can target `release/*` and `staging/*`.
|
||||
|
||||
## Principles
|
||||
|
||||
- **Releases are merged back into `develop`, never cherry-picked or rebased onto it.**
|
||||
Cherry-picked and rebased commits get new hashes,
|
||||
so Git can't tell that `develop` already has them.
|
||||
Future merges then replay them and produce artificial conflicts,
|
||||
and it's hard to verify that every fix actually reached `develop`.
|
||||
Merging keeps a single history:
|
||||
Git knows exactly which release commits `develop` contains,
|
||||
and no fix is left behind.
|
||||
- **Branches only move forward.**
|
||||
`develop`, `staging/X.Y.x`, and `release/X.Y.x` are never rewritten.
|
||||
- **Cherry-picking only goes from `develop` to a staging branch**,
|
||||
for fixes that must get into a release after the code freeze
|
||||
(see [Emergency Fixes From `develop`](#emergency-fixes-from-develop)).
|
||||
- **Security fixes are prepared privately and published with the release that contains them**,
|
||||
so vulnerabilities are not disclosed prematurely.
|
||||
|
||||
## Release Lifecycle
|
||||
|
||||
This diagram shows a major-minor release and its first patch release.
|
||||
The staging and release branches are drawn as one line.
|
||||
|
||||
```text
|
||||
develop staging/X.Y.x & release/X.Y.x
|
||||
│
|
||||
├── Tag: X.Y.0-b1
|
||||
├── Tag: X.Y.0-bN
|
||||
├── Tag: X.Y.0-rc1 ───────────────┐ (branches created)
|
||||
│ │
|
||||
│ (development continues) ├── Fixes
|
||||
│ ├── Tag: X.Y.0-rcN
|
||||
│ ├── Tag: X.Y.0 (final)
|
||||
◀──── (merge) ────────────────────┤
|
||||
│ ├── Fixes
|
||||
│ ├── Tag: X.Y.1-rcN
|
||||
│ ├── Tag: X.Y.1 (final)
|
||||
◀──── (merge) ────────────────────┤
|
||||
│
|
||||
▼
|
||||
```
|
||||
|
||||
### Betas, First RC & Branching
|
||||
|
||||
> [!NOTE]
|
||||
> This phase applies only to a new major-minor release (e.g., `X.Y.0`).
|
||||
> Patch releases work on the existing branches of the line.
|
||||
|
||||
1. **Betas:** All beta versions (e.g., `X.Y.0-b1`) are built and tagged directly on `develop`.
|
||||
2. **First RC:** We release the first RC (`X.Y.0-rc1`)
|
||||
once everything that should be included in the release has been merged.
|
||||
3. **Branches:** `staging/X.Y.x` and `release/X.Y.x` are created from `develop`
|
||||
at the commit tagged `X.Y.0-rc1`.
|
||||
The first RC also kicks off the QE process.
|
||||
4. **Code freeze:** No new features or unrelated changes are pulled from `develop`
|
||||
into `staging/X.Y.x` or `release/X.Y.x`.
|
||||
Only critical stabilization fixes go into the line.
|
||||
5. **No large changes on `develop`:** Until `X.Y.0` is [merged back](#merging-back-into-develop),
|
||||
large changes (e.g., big refactors, moving or renaming many files, mass reformatting)
|
||||
are not merged into `develop`,
|
||||
so that fixes on the line and the merge back don't run into conflicts.
|
||||
|
||||
### Release Candidates
|
||||
|
||||
1. Fixes are developed against `staging/X.Y.x`.
|
||||
2. When ready, a new RC is created on `staging/X.Y.x`,
|
||||
and `release/X.Y.x` is fast-forwarded to it.
|
||||
|
||||
RCs that contain unpublished security fixes are not published,
|
||||
and don't touch the public branches.
|
||||
|
||||
RCs are not merged back into `develop`:
|
||||
`X.Y.0-rc1` is tagged on `develop` itself,
|
||||
and all later changes reach `develop` with the [final release](#final-release).
|
||||
|
||||
### Final Release
|
||||
|
||||
Security fixes become public as soon as they reach the public repo,
|
||||
so these steps happen only once the release is ready to be published,
|
||||
one right after the other.
|
||||
|
||||
1. Unpublished security fixes, if any, are merged into `staging/X.Y.x`.
|
||||
2. `release/X.Y.x` is fast-forwarded to `staging/X.Y.x`,
|
||||
and the release is tagged on it.
|
||||
3. The release is immediately [merged back into `develop`](#merging-back-into-develop).
|
||||
For a major-minor release, this lifts the freeze on large changes in `develop`.
|
||||
|
||||
### Merging Back Into `develop`
|
||||
|
||||
The merge back is a regular PR into `develop`
|
||||
whose branch contains a real merge commit of the release tag:
|
||||
|
||||
1. Create a branch from `develop`, run `git merge --no-ff <tag>`, and resolve any conflicts.
|
||||
2. Once the PR is approved, `develop` is fast-forwarded to the PR branch,
|
||||
so the merge commit lands as it is.
|
||||
Never squash or rebase it.
|
||||
Never add it to the merge queue either: the queue squashes PRs, which would drop the merge commit.
|
||||
3. If `develop` moves while the PR is open, redo the merge on top of the new `develop`.
|
||||
|
||||
After the merge, `git log develop..<tag>` must be empty.
|
||||
|
||||
## Special Cases
|
||||
|
||||
### Emergency Fixes From `develop`
|
||||
|
||||
If a commit was merged to `develop`
|
||||
and needs to be included in a release after the code freeze:
|
||||
|
||||
1. Create a PR that cherry-picks the commit onto `staging/X.Y.x`.
|
||||
2. Leave `develop` as it is, with no reverts.
|
||||
3. Follow the [release candidates](#release-candidates) process as usual.
|
||||
|
||||
When the release is later merged back into `develop`,
|
||||
both sides already contain the same change,
|
||||
so the merge usually resolves it cleanly or with a trivial conflict.
|
||||
From Git's perspective, the cherry-picked commit then becomes part of `develop` too.
|
||||
|
||||
### Several Supported Lines
|
||||
|
||||
When a fix must ship in more than one supported line (e.g., `X.Y` and `X.(Y+1)`),
|
||||
the lines are merged upwards rather than cherry-picked between:
|
||||
|
||||
1. The fix goes into the oldest line first.
|
||||
2. After that line's release,
|
||||
its `release/X.Y.x` is merged into the staging branch of the next newer line,
|
||||
and so on up to the newest line.
|
||||
3. The newest line is merged back into `develop` as usual.
|
||||
|
||||
This way every newer line, and eventually `develop`, contains the history of the older lines.
|
||||
@@ -24,13 +24,9 @@
|
||||
# `g++-15`, ...) are probed under both names: a suffixed name can break while
|
||||
# the plain one still works (see mkVersionedToolLinks in nix/packages.nix).
|
||||
#
|
||||
# Tools scoped to a single dev shell rather than to commonPackages are checked
|
||||
# only in that shell, keyed off XRPL_DEVSHELL.
|
||||
#
|
||||
# Environment variables:
|
||||
# CI if set, skip the tools above when on macOS.
|
||||
# CHECK_TOOLS_SKIP_CLONE if set, skip the git-over-HTTPS connectivity check.
|
||||
# XRPL_DEVSHELL active dev shell; selects shell-specific tools.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
@@ -167,14 +163,6 @@ if [ "${os}" = "linux" ] || [ "${os}" = "macos" ]; then
|
||||
check rustfmt
|
||||
fi
|
||||
|
||||
# Lean4 is in the formal-verification shell only, not in commonPackages.
|
||||
if [ "${XRPL_DEVSHELL:-}" = "formal-verification" ]; then
|
||||
echo
|
||||
echo "Formal verification toolchain:"
|
||||
check lean
|
||||
check lake
|
||||
fi
|
||||
|
||||
# GCC is the default compiler on Linux. macOS uses the system Apple Clang
|
||||
# instead, so GCC/g++/gcov are not expected there.
|
||||
if [ "${os}" = "linux" ]; then
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -95,6 +95,10 @@ public:
|
||||
private:
|
||||
beast::Journal mutable journal_;
|
||||
std::mutex mutable mutex_;
|
||||
// Initialized to nullptr for safety. Set by load() during the second
|
||||
// phase of ApplicationImp initialization. Methods that dereference
|
||||
// this pointer must validate it first, since two-phase init means
|
||||
// load() may not have been called yet.
|
||||
DatabaseCon* connection_{};
|
||||
std::unordered_set<PeerReservation, beast::Uhash<>, KeyEqual> table_;
|
||||
};
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -42,81 +40,6 @@ public:
|
||||
: Base(keylet::trustLine(id, issue), view, j)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the limit that @p account sets on this line for IOUs issued
|
||||
* by @p issuer. The issuer of the result is @p account.
|
||||
*
|
||||
* @param account the account whose limit is returned.
|
||||
* @param issuer the account on the other side of the line.
|
||||
* @return the limit @p account sets on this line.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
creditLimit(AccountID const& account, AccountID const& issuer) const
|
||||
{
|
||||
STAmount result = (*this)->getFieldAmount(account < issuer ? sfLowLimit : sfHighLimit);
|
||||
result.get<Issue>().account = account;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of IOUs issued by @p issuer that @p account holds
|
||||
* on this line. The issuer of the result is @p account.
|
||||
*
|
||||
* @param account the account whose balance is returned.
|
||||
* @param issuer the account on the other side of the line.
|
||||
* @return the balance @p account holds on this line.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
creditBalance(AccountID const& account, AccountID const& issuer) const
|
||||
{
|
||||
STAmount result = (*this)->getFieldAmount(sfBalance);
|
||||
if (account < issuer)
|
||||
result.negate();
|
||||
result.get<Issue>().account = account;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if @p issuer froze this line for @p account.
|
||||
*
|
||||
* @param account the account whose side of the line is checked.
|
||||
* @param issuer the account on the other side of the line.
|
||||
* @return true if @p issuer froze this line for @p account.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isIndividualFrozen(AccountID const& account, AccountID const& issuer) const
|
||||
{
|
||||
return (*this)->isFlag((issuer > account) ? lsfHighFreeze : lsfLowFreeze);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if either side deep-froze this line.
|
||||
*
|
||||
* @return true if either side deep-froze this line.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isDeepFrozen() const
|
||||
{
|
||||
return (*this)->isFlag(lsfHighDeepFreeze) || (*this)->isFlag(lsfLowDeepFreeze);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if @p issuer authorized @p account on this line.
|
||||
*
|
||||
* @note This checks only the line's auth flag. Whether the issuer
|
||||
* requires authorization (lsfRequireAuth) is on the issuer AccountRoot;
|
||||
* see requireAuth().
|
||||
*
|
||||
* @param account the account whose authorization is checked.
|
||||
* @param issuer the account on the other side of the line.
|
||||
* @return true if @p issuer authorized @p account on this line.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isAuthorized(AccountID const& account, AccountID const& issuer) const
|
||||
{
|
||||
return (*this)->isFlag((account > issuer) ? lsfLowAuth : lsfHighAuth);
|
||||
}
|
||||
};
|
||||
|
||||
using RippleStateEntryR = RippleStateEntry<ReadView>;
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/entries/RippleStateEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
@@ -150,7 +149,7 @@ escrowUnlockApplyHelper<Issue>(
|
||||
// of the funds
|
||||
if (!createAsset)
|
||||
{
|
||||
RippleStateEntryW const sleRippleState(trustLineKey, ctx.view, journal);
|
||||
auto const sleRippleState = ctx.view.peek(trustLineKey);
|
||||
if (!sleRippleState)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/RippleStateEntry.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/IOUAmount.h>
|
||||
@@ -166,7 +165,7 @@ trustCreate(
|
||||
[[nodiscard]] TER
|
||||
trustDelete(
|
||||
ApplyView& view,
|
||||
RippleStateEntryW& sleRippleState,
|
||||
SLE::Ref sleRippleState,
|
||||
AccountID const& uLowAccountID,
|
||||
AccountID const& uHighAccountID,
|
||||
beast::Journal j);
|
||||
@@ -271,7 +270,7 @@ removeEmptyHolding(
|
||||
[[nodiscard]] TER
|
||||
deleteAMMTrustLine(
|
||||
ApplyView& view,
|
||||
RippleStateEntryW& sleState,
|
||||
SLE::pointer sleState,
|
||||
std::optional<AccountID> const& ammAccountID,
|
||||
beast::Journal j);
|
||||
|
||||
|
||||
@@ -238,12 +238,6 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal;
|
||||
TF_FLAG(tfSponsorshipEnd, 0x00010000) \
|
||||
TF_FLAG(tfSponsorshipCreate, 0x00020000) \
|
||||
TF_FLAG(tfSponsorshipReassign, 0x00040000), \
|
||||
MASK_ADJ(0)) \
|
||||
\
|
||||
TRANSACTION(ConfidentialMPTHolderKeyUpdate, \
|
||||
TF_FLAG(tfHolderKeyRotation, 0x00010000) \
|
||||
TF_FLAG(tfHolderKeyRecovery, 0x00020000) \
|
||||
TF_FLAG(tfCancelRecovery, 0x00040000), \
|
||||
MASK_ADJ(0))
|
||||
|
||||
// clang-format on
|
||||
|
||||
@@ -12,12 +12,10 @@
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <boost/container/flat_set.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <flat_set>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -117,9 +115,6 @@ public:
|
||||
parentBatchID_ = id;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::flat_set<MPTID>
|
||||
getAffectedMPTs() const;
|
||||
|
||||
private:
|
||||
UInt256 transactionID_;
|
||||
std::uint32_t ledgerSeq_;
|
||||
|
||||
@@ -439,7 +439,6 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
|
||||
{sfIssuerKeyMirrorEpoch, SoeOptional},
|
||||
{sfAuditorKeyMirrorEpoch, SoeOptional},
|
||||
{sfHolderEncryptionKey, SoeOptional},
|
||||
{sfRecoveryKey, SoeOptional},
|
||||
}))
|
||||
|
||||
/** A ledger object which tracks Oracle
|
||||
|
||||
@@ -330,7 +330,6 @@ TYPED_SFIELD(sfAuditorEncryptionKey, VL, 44)
|
||||
TYPED_SFIELD(sfAmountCommitment, VL, 45)
|
||||
TYPED_SFIELD(sfBalanceCommitment, VL, 46)
|
||||
TYPED_SFIELD(sfBytecode, VL, 47)
|
||||
TYPED_SFIELD(sfRecoveryKey, VL, 48)
|
||||
|
||||
// account (common)
|
||||
TYPED_SFIELD(sfAccount, ACCOUNT, 1)
|
||||
|
||||
@@ -1147,27 +1147,12 @@ TRANSACTION(ttCONFIDENTIAL_MPT_MIRROR_UPDATE, 92, ConfidentialMPTMirrorUpdate,
|
||||
{sfZKProof, SoeRequired},
|
||||
}))
|
||||
|
||||
/** This transaction rotates or recovers a confidential MPT holder's ElGamal encryption key,
|
||||
or cancels a recovery. */
|
||||
#if TRANSACTION_INCLUDE
|
||||
# include <xrpl/tx/transactors/token/ConfidentialMPTHolderKeyUpdate.h>
|
||||
#endif
|
||||
TRANSACTION(ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE, 93, ConfidentialMPTHolderKeyUpdate,
|
||||
({.amendment = featureConfidentialMPTKeyRotation}),
|
||||
({
|
||||
{sfMPTokenIssuanceID, SoeRequired},
|
||||
{sfHolderEncryptionKey, SoeOptional},
|
||||
{sfConfidentialBalanceSpending, SoeOptional},
|
||||
{sfConfidentialBalanceInbox, SoeOptional},
|
||||
{sfZKProof, SoeOptional},
|
||||
}))
|
||||
|
||||
/** This transaction posts an unsigned transaction on-ledger as a
|
||||
TransactionProposal, pending multi-signature collection. */
|
||||
#if TRANSACTION_INCLUDE
|
||||
# include <xrpl/tx/transactors/proposal/TransactionProposalCreate.h>
|
||||
#endif
|
||||
TRANSACTION(ttTRANSACTION_PROPOSAL_CREATE, 95, TransactionProposalCreate,
|
||||
TRANSACTION(ttTRANSACTION_PROPOSAL_CREATE, 93, TransactionProposalCreate,
|
||||
({.amendment = featureCosign}),
|
||||
({
|
||||
{sfProposedTransaction, SoeRequired},
|
||||
|
||||
@@ -411,7 +411,6 @@ JSS(minimum_level); // out: TxQ
|
||||
JSS(missingCommand); // error
|
||||
JSS(mpt_amount); // out: mpt_holders
|
||||
JSS(mpt_issuance_id); // in: Payment, mpt_holders
|
||||
JSS(mpt_issuances); // in: Subscribe, Unsubscribe
|
||||
JSS(mptoken_index); // out: mpt_holders
|
||||
JSS(mpt_issuance_id_a); // out: BookChanges
|
||||
JSS(mpt_issuance_id_b); // out: BookChanges
|
||||
|
||||
@@ -339,30 +339,6 @@ public:
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfHolderEncryptionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfRecoveryKey (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getRecoveryKey() const
|
||||
{
|
||||
if (hasRecoveryKey())
|
||||
return this->sle_->at(sfRecoveryKey);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfRecoveryKey is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasRecoveryKey() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfRecoveryKey);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -576,17 +552,6 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfRecoveryKey (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
MPTokenBuilder&
|
||||
setRecoveryKey(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfRecoveryKey] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the completed MPToken wrapper.
|
||||
* @param index The ledger entry index.
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
// This file is auto-generated. Do not edit.
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol_autogen/TransactionBase.h>
|
||||
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::transactions {
|
||||
|
||||
class ConfidentialMPTHolderKeyUpdateBuilder;
|
||||
|
||||
/**
|
||||
* @brief Transaction: ConfidentialMPTHolderKeyUpdate
|
||||
*
|
||||
* Type: ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE (93)
|
||||
* Delegable: Delegation::NotDelegable
|
||||
* Amendment: featureConfidentialMPTKeyRotation
|
||||
* Privileges: Privilege::NoPriv
|
||||
*
|
||||
* Immutable wrapper around STTx providing type-safe field access.
|
||||
* Use ConfidentialMPTHolderKeyUpdateBuilder to construct new transactions.
|
||||
*/
|
||||
class ConfidentialMPTHolderKeyUpdate : public TransactionBase
|
||||
{
|
||||
public:
|
||||
static constexpr xrpl::TxType txType = ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE;
|
||||
|
||||
/**
|
||||
* @brief Construct a ConfidentialMPTHolderKeyUpdate transaction wrapper from an existing STTx object.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
explicit ConfidentialMPTHolderKeyUpdate(std::shared_ptr<STTx const> tx)
|
||||
: TransactionBase(std::move(tx))
|
||||
{
|
||||
// Verify transaction type
|
||||
if (tx_->getTxnType() != txType)
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for ConfidentialMPTHolderKeyUpdate");
|
||||
}
|
||||
}
|
||||
|
||||
// Transaction-specific field getters
|
||||
|
||||
/**
|
||||
* @brief Get sfMPTokenIssuanceID (SoeRequired)
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
SF_UINT192::type::value_type
|
||||
getMPTokenIssuanceID() const
|
||||
{
|
||||
return this->tx_->at(sfMPTokenIssuanceID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfHolderEncryptionKey (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getHolderEncryptionKey() const
|
||||
{
|
||||
if (hasHolderEncryptionKey())
|
||||
{
|
||||
return this->tx_->at(sfHolderEncryptionKey);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfHolderEncryptionKey is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasHolderEncryptionKey() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfHolderEncryptionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfConfidentialBalanceSpending (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getConfidentialBalanceSpending() const
|
||||
{
|
||||
if (hasConfidentialBalanceSpending())
|
||||
{
|
||||
return this->tx_->at(sfConfidentialBalanceSpending);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfConfidentialBalanceSpending is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasConfidentialBalanceSpending() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfConfidentialBalanceSpending);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfConfidentialBalanceInbox (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getConfidentialBalanceInbox() const
|
||||
{
|
||||
if (hasConfidentialBalanceInbox())
|
||||
{
|
||||
return this->tx_->at(sfConfidentialBalanceInbox);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfConfidentialBalanceInbox is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasConfidentialBalanceInbox() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfConfidentialBalanceInbox);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfZKProof (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getZKProof() const
|
||||
{
|
||||
if (hasZKProof())
|
||||
{
|
||||
return this->tx_->at(sfZKProof);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfZKProof is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasZKProof() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfZKProof);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Builder for ConfidentialMPTHolderKeyUpdate transactions.
|
||||
*
|
||||
* Provides a fluent interface for constructing transactions with method chaining.
|
||||
* Uses STObject internally for flexible transaction construction.
|
||||
* Inherits common field setters from TransactionBuilderBase.
|
||||
*/
|
||||
class ConfidentialMPTHolderKeyUpdateBuilder : public TransactionBuilderBase<ConfidentialMPTHolderKeyUpdateBuilder>
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new ConfidentialMPTHolderKeyUpdateBuilder with required fields.
|
||||
* @param account The account initiating the transaction.
|
||||
* @param mPTokenIssuanceID The sfMPTokenIssuanceID field value.
|
||||
* @param sequence Optional sequence number for the transaction.
|
||||
* @param fee Optional fee for the transaction.
|
||||
*/
|
||||
ConfidentialMPTHolderKeyUpdateBuilder(SF_ACCOUNT::type::value_type account,
|
||||
std::decay_t<typename SF_UINT192::type::value_type> const& mPTokenIssuanceID, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
|
||||
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
|
||||
)
|
||||
: TransactionBuilderBase<ConfidentialMPTHolderKeyUpdateBuilder>(ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE, account, sequence, fee)
|
||||
{
|
||||
setMPTokenIssuanceID(mPTokenIssuanceID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct a ConfidentialMPTHolderKeyUpdateBuilder from an existing STTx object.
|
||||
* @param tx The existing transaction to copy from.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
ConfidentialMPTHolderKeyUpdateBuilder(std::shared_ptr<STTx const> tx)
|
||||
{
|
||||
if (tx->getTxnType() != ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE)
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for ConfidentialMPTHolderKeyUpdateBuilder");
|
||||
}
|
||||
object_ = *tx;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Transaction-specific field setters
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Set sfMPTokenIssuanceID (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
ConfidentialMPTHolderKeyUpdateBuilder&
|
||||
setMPTokenIssuanceID(std::decay_t<typename SF_UINT192::type::value_type> const& value)
|
||||
{
|
||||
object_[sfMPTokenIssuanceID] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfHolderEncryptionKey (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
ConfidentialMPTHolderKeyUpdateBuilder&
|
||||
setHolderEncryptionKey(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfHolderEncryptionKey] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfConfidentialBalanceSpending (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
ConfidentialMPTHolderKeyUpdateBuilder&
|
||||
setConfidentialBalanceSpending(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfConfidentialBalanceSpending] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfConfidentialBalanceInbox (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
ConfidentialMPTHolderKeyUpdateBuilder&
|
||||
setConfidentialBalanceInbox(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfConfidentialBalanceInbox] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfZKProof (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
ConfidentialMPTHolderKeyUpdateBuilder&
|
||||
setZKProof(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfZKProof] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the ConfidentialMPTHolderKeyUpdate wrapper.
|
||||
* @param publicKey The public key for signing.
|
||||
* @param secretKey The secret key for signing.
|
||||
* @return The constructed transaction wrapper.
|
||||
*/
|
||||
ConfidentialMPTHolderKeyUpdate
|
||||
build(PublicKey const& publicKey, SecretKey const& secretKey)
|
||||
{
|
||||
sign(publicKey, secretKey);
|
||||
return ConfidentialMPTHolderKeyUpdate{std::make_shared<STTx>(std::move(object_))};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::transactions
|
||||
@@ -18,7 +18,7 @@ class TransactionProposalCreateBuilder;
|
||||
/**
|
||||
* @brief Transaction: TransactionProposalCreate
|
||||
*
|
||||
* Type: ttTRANSACTION_PROPOSAL_CREATE (95)
|
||||
* Type: ttTRANSACTION_PROPOSAL_CREATE (93)
|
||||
* Delegable: Delegation::NotDelegable
|
||||
* Amendment: featureCosign
|
||||
* Privileges: Privilege::NoPriv
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Book.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/resource/Consumer.h>
|
||||
#include <xrpl/server/Manifest.h>
|
||||
|
||||
@@ -27,11 +26,11 @@ namespace xrpl {
|
||||
/**
|
||||
* Maximum number of subscriptions a single client connection may hold at once.
|
||||
*
|
||||
* Applies to the account, real-time account, account-history, and MPT issuance
|
||||
* subscriptions tracked on one InfoSub (the sets counted by
|
||||
* totalSubscriptionCount), bounding the disconnect-time cleanup of those sets.
|
||||
* Book subscriptions are tracked separately (OrderBookDB) and are not counted
|
||||
* here. Generous enough for legitimate power users such as block explorers.
|
||||
* Applies to the account, real-time account, and account-history subscriptions
|
||||
* tracked on one InfoSub (the sets counted by totalSubscriptionCount), bounding
|
||||
* the disconnect-time cleanup of those sets. Book subscriptions are tracked
|
||||
* separately (OrderBookDB) and are not counted here. Generous enough for
|
||||
* legitimate power users such as block explorers.
|
||||
*/
|
||||
constexpr std::size_t kMaxSubscriptionsPerConnection = 100'000;
|
||||
|
||||
@@ -152,21 +151,6 @@ public:
|
||||
AccountID const& account,
|
||||
bool historyOnly) = 0;
|
||||
|
||||
/**
|
||||
* Remove an MPT issuance subscription during InfoSub teardown.
|
||||
*
|
||||
* Removes only the server-side entry from subMPT_. Does NOT touch
|
||||
* InfoSub::mptSubscriptions_ because the InfoSub is being destroyed.
|
||||
* Called by ~InfoSub() for each issuance in mptSubscriptions_.
|
||||
*
|
||||
* @param uListener The sequence number of the subscriber being torn down.
|
||||
* @param mptID The MPT issuance to remove.
|
||||
*
|
||||
* @note Thread-safety: acquires mptLock_ internally.
|
||||
*/
|
||||
virtual void
|
||||
unsubMPTInternal(std::uint64_t uListener, MPTID const& mptID) = 0;
|
||||
|
||||
/**
|
||||
* Schedule the server-side teardown of a disconnecting connection's
|
||||
* account subscriptions off the destructor thread.
|
||||
@@ -286,11 +270,6 @@ public:
|
||||
virtual bool
|
||||
unsubConsensus(std::uint64_t uListener) = 0;
|
||||
|
||||
virtual void
|
||||
subMPT(InfoSub::Ref ispListener, HashSet<MPTID> const& mptIDs) = 0;
|
||||
virtual void
|
||||
unsubMPT(InfoSub::Ref ispListener, HashSet<MPTID> const& mptIDs) = 0;
|
||||
|
||||
// VFALCO TODO Remove
|
||||
// This was added for one particular partner, it
|
||||
// "pushes" subscription data to a particular URL.
|
||||
@@ -330,9 +309,9 @@ public:
|
||||
* Return the number of subscriptions currently tracked on this
|
||||
* connection.
|
||||
*
|
||||
* The combined size of the per-connection account, real-time account,
|
||||
* account-history, and MPT issuance subscription sets. `doSubscribe` reads
|
||||
* this to enforce the per-connection subscription cap before admitting more.
|
||||
* The combined size of the per-connection account, real-time account, and
|
||||
* account-history subscription sets. `doSubscribe` reads this to enforce
|
||||
* the per-connection subscription cap before admitting more.
|
||||
*
|
||||
* @return The total tracked subscription count for this connection.
|
||||
*
|
||||
@@ -363,25 +342,6 @@ public:
|
||||
HashSet<AccountID> const& normalAccounts,
|
||||
std::size_t cap);
|
||||
|
||||
/**
|
||||
* Enforce the cap and reserve a request's net-new MPT issuances, atomically.
|
||||
*
|
||||
* The MPT analogue of tryReserveAccountSubscriptions: under one hold of
|
||||
* `lock_`, count the net-new issuances, check the total against @p cap, and
|
||||
* insert them only if it fits. All-or-nothing, so a rejected request records
|
||||
* nothing. Doing check and insert together stops two concurrent requests
|
||||
* sharing an InfoSub (the admin subscribe-by-url path) from both passing the
|
||||
* check before either records its issuances. The server-side map is
|
||||
* populated afterwards by subMPT, whose re-insert is a no-op.
|
||||
*
|
||||
* @param mptIDs The MPT issuance ids to reserve.
|
||||
* @param cap The effective per-connection cap.
|
||||
* @return true if reserved; false if the request must be rejected.
|
||||
* @note Thread-safe: takes `lock_`.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
tryReserveMPTSubscriptions(HashSet<MPTID> const& mptIDs, std::size_t cap);
|
||||
|
||||
/**
|
||||
* Whether this connection already tracks an account-history for @p account.
|
||||
*
|
||||
@@ -453,29 +413,12 @@ public:
|
||||
[[nodiscard]] unsigned int
|
||||
getApiVersion() const noexcept;
|
||||
|
||||
void
|
||||
insertSubMPTInfo(MPTID const& mptID);
|
||||
|
||||
void
|
||||
deleteSubMPTInfo(MPTID const& mptID);
|
||||
|
||||
protected:
|
||||
// Mutable so the read-only totalSubscriptionCount() accessor can lock it
|
||||
// from a const method; locking semantics are otherwise unchanged.
|
||||
mutable std::mutex lock_;
|
||||
|
||||
private:
|
||||
// The lock type guarding this instance's subscription sets.
|
||||
using ScopedLock = std::scoped_lock<decltype(lock_)>;
|
||||
|
||||
/**
|
||||
* The combined tally the per-connection cap is enforced against.
|
||||
*
|
||||
* @param lock Proof that `lock_` is held; unused otherwise.
|
||||
*/
|
||||
[[nodiscard]] std::size_t
|
||||
subscriptionCount(ScopedLock const& lock) const;
|
||||
|
||||
Consumer consumer_;
|
||||
Source& source_;
|
||||
HashSet<AccountID> realTimeSubscriptions_;
|
||||
@@ -484,7 +427,6 @@ private:
|
||||
std::uint64_t seq_;
|
||||
HashSet<AccountID> accountHistorySubscriptions_;
|
||||
HashSet<Book> bookSubscriptions_;
|
||||
HashSet<MPTID> mptSubscriptions_;
|
||||
unsigned int apiVersion_ = 0;
|
||||
|
||||
static int
|
||||
|
||||
@@ -46,9 +46,6 @@ class ValidMPTIssuance
|
||||
* MPTokens and RippleStates deleted during apply. finalize() checks each
|
||||
* holder's AccountRoot to detect vault pseudo-account holdings deleted
|
||||
* outside VaultDelete. All these checks are gated on fixCleanup3_2_0.
|
||||
*
|
||||
* Under fixCleanup3_5_0, finalize() also rejects any MPToken erased with
|
||||
* a non-zero sfMPTAmount.
|
||||
*/
|
||||
std::vector<std::shared_ptr<SLE const>> deletedHoldings_;
|
||||
|
||||
@@ -141,9 +138,8 @@ public:
|
||||
* - Convert/ConvertBack symmetry:
|
||||
* Regular MPToken balance change (±X) == COA (Confidential Outstanding Amount) change (∓X)
|
||||
* - Cannot delete MPToken with non-zero confidential state:
|
||||
* Cannot delete if any of sfConfidentialBalanceSpending, sfConfidentialBalanceInbox,
|
||||
* sfIssuerEncryptedBalance or sfAuditorEncryptedBalance is present, and the issuance's
|
||||
* sfConfidentialOutstandingAmount is non-zero. Mirrors MPTokenAuthorize::preclaim.
|
||||
* Cannot delete if sfIssuerEncryptedBalance exists
|
||||
* Cannot delete if sfConfidentialBalanceInbox and sfConfidentialBalanceSpending exist
|
||||
* - Privacy flag consistency:
|
||||
* MPToken confidential balance fields can only be created or changed if
|
||||
* lsfMPTCanHoldConfidentialBalance is set on the issuance.
|
||||
@@ -166,8 +162,6 @@ class ValidConfidentialMPToken
|
||||
std::int64_t outstandingDelta = 0;
|
||||
SLE::const_pointer issuance;
|
||||
bool deletedWithEncrypted = false;
|
||||
// True when an erased MPToken had a non-zero pre-tx public balance.
|
||||
bool deletedWithBalanceBefore = false;
|
||||
bool badConsistency = false;
|
||||
bool badCOA = false;
|
||||
bool changesConfidentialFields = false;
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/RippleStateEntry.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -34,7 +33,7 @@ checkFreeze(
|
||||
}
|
||||
}
|
||||
|
||||
if (RippleStateEntryR const sle(src, dst, currency, view); sle)
|
||||
if (auto sle = view.read(keylet::trustLine(src, dst, currency)))
|
||||
{
|
||||
if (sle->isFlag((dst > src) ? lsfHighFreeze : lsfLowFreeze))
|
||||
{
|
||||
@@ -78,8 +77,8 @@ checkNoRipple(
|
||||
beast::Journal j)
|
||||
{
|
||||
// fetch the ripple lines into and out of this node
|
||||
RippleStateEntryR const sleIn(prev, cur, currency, view);
|
||||
RippleStateEntryR const sleOut(cur, next, currency, view);
|
||||
auto sleIn = view.read(keylet::trustLine(prev, cur, currency));
|
||||
auto sleOut = view.read(keylet::trustLine(cur, next, currency));
|
||||
|
||||
if (!sleIn || !sleOut)
|
||||
return terNO_LINE;
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/ApplyContext.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* @brief Allows a confidential MPT holder to rotate or recover their ElGamal
|
||||
* encryption key, or to cancel a pending recovery.
|
||||
*
|
||||
* Submitted by the holder. Exactly one of three modes must be selected via
|
||||
* the transaction flags:
|
||||
*
|
||||
* - Rotation (tfHolderKeyRotation): the holder still has their current
|
||||
* ElGamal private key. They provide a new public key along with their
|
||||
* current spending/inbox balances re-encrypted under that new key. The
|
||||
* holder's encryption key and confidential balances are updated
|
||||
* immediately.
|
||||
*
|
||||
* - Recovery (tfHolderKeyRecovery): the holder has lost their current
|
||||
* private key. They provide a new public key but cannot provide
|
||||
* re-encrypted balances. The new key is recorded as a pending
|
||||
* sfRecoveryKey; the confidential balances are left untouched. Completing
|
||||
* the recovery (rewriting the balances) is done separately by the issuer
|
||||
* via ConfidentialMPTRecoverBalance.
|
||||
*
|
||||
* - Cancel (tfCancelRecovery): the holder revokes a pending recovery
|
||||
* authorization created by a prior Recovery-mode transaction. No key,
|
||||
* balances, or proof are carried; authorization is via the holder's
|
||||
* ordinary XRPL signature. sfRecoveryKey is removed and everything else
|
||||
* is left untouched. Fails if no recovery is pending.
|
||||
*
|
||||
* @note Zero-knowledge proof verification for Rotation and Recovery is
|
||||
* deferred to a follow-up once the mpt-crypto constructions are finalized.
|
||||
*/
|
||||
class ConfidentialMPTHolderKeyUpdate : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
|
||||
|
||||
explicit ConfidentialMPTHolderKeyUpdate(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static bool
|
||||
checkExtraFeatures(PreflightContext const& ctx);
|
||||
|
||||
static std::uint32_t
|
||||
getFlagsMask(PreflightContext const& ctx);
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static XRPAmount
|
||||
calculateBaseFee(ReadView const& view, STTx const& tx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
|
||||
void
|
||||
visitInvariantEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after) override;
|
||||
|
||||
[[nodiscard]] bool
|
||||
finalizeInvariants(
|
||||
STTx const& tx,
|
||||
TER result,
|
||||
XRPAmount fee,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j) override;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -26,10 +26,6 @@ The store paths carry their derivation hash, so they change whenever a tool is
|
||||
rebuilt — a `flake.lock` update generally rewrites most of them even when no
|
||||
version moves. That is deliberate: it makes tooling changes visible in review.
|
||||
|
||||
Tools scoped to a single dev shell rather than to `commonPackages` do not appear
|
||||
here: `check-tools.sh` keys those off `XRPL_DEVSHELL`, and none of the three
|
||||
environments above is such a shell. Adding to one needs no snapshot update.
|
||||
|
||||
## Regenerating
|
||||
|
||||
The two Linux snapshots come from the `nix-ubuntu` image (Docker or a compatible
|
||||
|
||||
@@ -147,19 +147,6 @@ rec {
|
||||
versionedTools = clangVersionedTools;
|
||||
};
|
||||
|
||||
# The gcc shell plus the Lean4 formal verification toolchain
|
||||
formal-verification = makeShell {
|
||||
shellName = "formal-verification";
|
||||
stdenv = customGccStdenv;
|
||||
compilerName = "gcc";
|
||||
version = gccVersion;
|
||||
versionedTools = gccVersionedTools;
|
||||
extraPackages = [
|
||||
customGccGcov
|
||||
pkgs.lean4
|
||||
];
|
||||
};
|
||||
|
||||
# Nix provides no compiler; use the one from your system (e.g. Apple Clang).
|
||||
no-compiler = makeShell {
|
||||
shellName = "no-compiler";
|
||||
|
||||
+4
-6
@@ -217,13 +217,12 @@ The `release-info` action decides the channel from the event, and
|
||||
| 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` |
|
||||
| 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
|
||||
Only a tag picks a release channel. 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.
|
||||
@@ -285,9 +284,8 @@ 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.
|
||||
secret, an organization access token for `xrplf`. Private builds are never
|
||||
pushed there.
|
||||
- `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
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include <cerrno>
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <ios>
|
||||
@@ -100,13 +99,12 @@ uniqueRandomPath(
|
||||
if (ec)
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
std::format("Unable to check path '{}': {}", candidate.string(), ec.message()));
|
||||
"Unable to check path '" + candidate.string() + "': " + ec.message());
|
||||
}
|
||||
if (!exists)
|
||||
return candidate;
|
||||
}
|
||||
Throw<std::runtime_error>(
|
||||
std::format("Unable to generate a unique path under '{}'", base.string()));
|
||||
Throw<std::runtime_error>("Unable to generate a unique path under '" + base.string() + "'");
|
||||
}
|
||||
|
||||
TempDir::TempDir() : path_(uniqueRandomPath(std::filesystem::temp_directory_path()))
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
#include <boost/container/pmr/monotonic_buffer_resource.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <format>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
@@ -265,7 +264,7 @@ OpenView::rawTxInsert(
|
||||
auto const result = txs_.emplace(
|
||||
std::piecewise_construct, std::forward_as_tuple(key), std::forward_as_tuple(txn, metaData));
|
||||
if (!result.second)
|
||||
Throw<std::logic_error>(std::format("rawTxInsert: duplicate TX id: {}", to_string(key)));
|
||||
Throw<std::logic_error>("rawTxInsert: duplicate TX id: " + to_string(key));
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -11,7 +11,6 @@
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/Sandbox.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/RippleStateEntry.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
@@ -22,7 +21,6 @@
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
@@ -558,7 +556,7 @@ ammLPHolds(
|
||||
auto const currency = ammLPTCurrency(asset1, asset2);
|
||||
STAmount amount;
|
||||
|
||||
RippleStateEntryR const sle(lpAccount, ammAccount, currency, view);
|
||||
auto const sle = view.read(keylet::trustLine(lpAccount, ammAccount, currency));
|
||||
if (!sle)
|
||||
{
|
||||
amount.clear(Issue{currency, ammAccount});
|
||||
@@ -648,7 +646,7 @@ ammAccountHolds(ReadView const& view, AccountID const& ammAccountID, Asset const
|
||||
}
|
||||
else if (
|
||||
auto const sle =
|
||||
RippleStateEntryR(ammAccountID, issue.account, issue.currency, view);
|
||||
view.read(keylet::trustLine(ammAccountID, issue.account, issue.currency));
|
||||
sle && !isFrozen(view, ammAccountID, issue.currency, issue.account))
|
||||
{
|
||||
STAmount amount = (*sle)[sfBalance];
|
||||
@@ -690,8 +688,7 @@ deleteAMMTrustLines(
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
RippleStateEntryW line(Keylet(ltRIPPLE_STATE, sleItem->key()), sb, j);
|
||||
return {deleteAMMTrustLine(sb, line, ammAccountID, j), SkipEntry::No};
|
||||
return {deleteAMMTrustLine(sb, sleItem, ammAccountID, j), SkipEntry::No};
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.error()) << "deleteAMMObjects: deleting non-trustline or non-MPT " << nodeType;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/RippleStateEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
@@ -34,7 +33,6 @@
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <format>
|
||||
#include <functional>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
@@ -396,11 +394,9 @@ removeToken(ApplyView& view, AccountID const& owner, UInt256 const& nftokenID, S
|
||||
|
||||
if (!page2)
|
||||
{
|
||||
Throw<std::runtime_error>(std::format(
|
||||
"page {} has a broken {} field pointing to {}",
|
||||
to_string(page1->key()),
|
||||
field.getName(),
|
||||
to_string(*id)));
|
||||
Throw<std::runtime_error>(
|
||||
"page " + to_string(page1->key()) + " has a broken " + field.getName() +
|
||||
" field pointing to " + to_string(*id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -601,7 +597,7 @@ removeTokenOffersWithLimit(ApplyView& view, Keylet const& directory, std::size_t
|
||||
else
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
std::format("Offer {} cannot be deleted!", to_string(offerIndexes[i])));
|
||||
"Offer " + to_string(offerIndexes[i]) + " cannot be deleted!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -738,9 +734,9 @@ repairNFTokenDirectoryLinks(ApplyView& view, AccountID const& owner)
|
||||
if (!newPrev)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
Throw<std::runtime_error>(std::format(
|
||||
"NFTokenPage directory for {} cannot be repaired. Unexpected link problem.",
|
||||
to_string(owner)));
|
||||
Throw<std::runtime_error>(
|
||||
"NFTokenPage directory for " + to_string(owner) +
|
||||
" cannot be repaired. Unexpected link problem.");
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
newPrev->at(sfNextPageMin) = nextPage->key();
|
||||
@@ -842,7 +838,7 @@ tokenOfferCreatePreclaim(
|
||||
if (view.rules().enabled(featureNFTokenMintOffer))
|
||||
{
|
||||
if (nftIssuer != amount.getIssuer() &&
|
||||
!RippleStateEntryR(nftIssuer, amount.get<Issue>(), view))
|
||||
!view.read(keylet::trustLine(nftIssuer, amount.get<Issue>())))
|
||||
return tecNO_LINE;
|
||||
}
|
||||
else if (!view.exists(keylet::trustLine(nftIssuer, amount.get<Issue>())))
|
||||
@@ -1029,7 +1025,7 @@ checkTrustlineAuthorized(
|
||||
|
||||
if (issuerAccount->isFlag(lsfRequireAuth))
|
||||
{
|
||||
RippleStateEntryR const trustLine(id, issue.account, issue.currency, view);
|
||||
auto const trustLine = view.read(keylet::trustLine(id, issue.account, issue.currency));
|
||||
|
||||
if (!trustLine)
|
||||
{
|
||||
@@ -1079,7 +1075,7 @@ checkTrustlineDeepFrozen(
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
RippleStateEntryR const trustLine(id, issue.account, issue.currency, view);
|
||||
auto const trustLine = view.read(keylet::trustLine(id, issue.account, issue.currency));
|
||||
|
||||
if (!trustLine)
|
||||
{
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/RippleStateEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
@@ -49,10 +48,13 @@ creditLimit(
|
||||
{
|
||||
STAmount result(Issue{currency, account});
|
||||
|
||||
RippleStateEntryR const sleRippleState(account, issuer, currency, view);
|
||||
auto sleRippleState = view.read(keylet::trustLine(account, issuer, currency));
|
||||
|
||||
if (sleRippleState)
|
||||
result = sleRippleState.creditLimit(account, issuer);
|
||||
{
|
||||
result = sleRippleState->getFieldAmount(account < issuer ? sfLowLimit : sfHighLimit);
|
||||
result.get<Issue>().account = account;
|
||||
}
|
||||
|
||||
XRPL_ASSERT(result.getIssuer() == account, "xrpl::creditLimit : result issuer match");
|
||||
XRPL_ASSERT(
|
||||
@@ -77,10 +79,15 @@ creditBalance(
|
||||
{
|
||||
STAmount result(Issue{currency, account});
|
||||
|
||||
RippleStateEntryR const sleRippleState(account, issuer, currency, view);
|
||||
auto sleRippleState = view.read(keylet::trustLine(account, issuer, currency));
|
||||
|
||||
if (sleRippleState)
|
||||
result = sleRippleState.creditBalance(account, issuer);
|
||||
{
|
||||
result = sleRippleState->getFieldAmount(sfBalance);
|
||||
if (account < issuer)
|
||||
result.negate();
|
||||
result.get<Issue>().account = account;
|
||||
}
|
||||
|
||||
XRPL_ASSERT(result.getIssuer() == account, "xrpl::creditBalance : result issuer match");
|
||||
XRPL_ASSERT(
|
||||
@@ -108,8 +115,8 @@ isIndividualFrozen(
|
||||
if (issuer != account)
|
||||
{
|
||||
// Check if the issuer froze the line
|
||||
RippleStateEntryR const sle(account, issuer, currency, view);
|
||||
if (sle && sle.isIndividualFrozen(account, issuer))
|
||||
auto const sle = view.read(keylet::trustLine(account, issuer, currency));
|
||||
if (sle && sle->isFlag((issuer > account) ? lsfHighFreeze : lsfLowFreeze))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -132,8 +139,8 @@ isFrozen(
|
||||
if (issuer != account)
|
||||
{
|
||||
// Check if the issuer froze the line
|
||||
RippleStateEntryR const sleLine(account, issuer, currency, view);
|
||||
if (sleLine && sleLine.isIndividualFrozen(account, issuer))
|
||||
sle = view.read(keylet::trustLine(account, issuer, currency));
|
||||
if (sle && sle->isFlag((issuer > account) ? lsfHighFreeze : lsfLowFreeze))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -156,13 +163,13 @@ isDeepFrozen(
|
||||
return false;
|
||||
}
|
||||
|
||||
RippleStateEntryR const sle(account, issuer, currency, view);
|
||||
auto const sle = view.read(keylet::trustLine(account, issuer, currency));
|
||||
if (!sle)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return sle.isDeepFrozen();
|
||||
return sle->isFlag(lsfHighDeepFreeze) || sle->isFlag(lsfLowDeepFreeze);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -206,9 +213,8 @@ trustCreate(
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
RippleStateEntryW sleRippleState(Keylet(ltRIPPLE_STATE, uIndex), view, j);
|
||||
sleRippleState.newSLE();
|
||||
sleRippleState.insert();
|
||||
auto const sleRippleState = std::make_shared<SLE>(ltRIPPLE_STATE, uIndex);
|
||||
view.insert(sleRippleState);
|
||||
|
||||
auto lowNode = view.dirInsert(
|
||||
keylet::ownerDir(uLowAccountID), sleRippleState->key(), describeOwnerDir(uLowAccountID));
|
||||
@@ -279,8 +285,7 @@ trustCreate(
|
||||
sleRippleState->setFieldU32(sfFlags, uFlags);
|
||||
increaseOwnerCount(view, sleAccount, sponsorSle, 1, j);
|
||||
|
||||
addSponsorToLedgerEntry(
|
||||
sleRippleState.mutableRawSle(), sponsorSle, bSetHigh ? sfHighSponsor : sfLowSponsor);
|
||||
addSponsorToLedgerEntry(sleRippleState, sponsorSle, bSetHigh ? sfHighSponsor : sfLowSponsor);
|
||||
|
||||
// ONLY: Create ripple balance.
|
||||
sleRippleState->setFieldAmount(sfBalance, bSetHigh ? -saBalance : saBalance);
|
||||
@@ -293,7 +298,7 @@ trustCreate(
|
||||
TER
|
||||
trustDelete(
|
||||
ApplyView& view,
|
||||
RippleStateEntryW& sleRippleState,
|
||||
SLE::Ref sleRippleState,
|
||||
AccountID const& uLowAccountID,
|
||||
AccountID const& uHighAccountID,
|
||||
beast::Journal j)
|
||||
@@ -316,11 +321,11 @@ trustDelete(
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
removeSponsorFromLedgerEntry(sleRippleState.mutableRawSle(), sfHighSponsor);
|
||||
removeSponsorFromLedgerEntry(sleRippleState.mutableRawSle(), sfLowSponsor);
|
||||
removeSponsorFromLedgerEntry(sleRippleState, sfHighSponsor);
|
||||
removeSponsorFromLedgerEntry(sleRippleState, sfLowSponsor);
|
||||
|
||||
JLOG(j.trace()) << "trustDelete: Deleting ripple line: state";
|
||||
sleRippleState.erase();
|
||||
view.erase(sleRippleState);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
@@ -334,7 +339,7 @@ trustDelete(
|
||||
static bool
|
||||
updateTrustLine(
|
||||
ApplyView& view,
|
||||
RippleStateEntryW& state,
|
||||
SLE::pointer state,
|
||||
bool bSenderHigh,
|
||||
AccountID const& sender,
|
||||
STAmount const& before,
|
||||
@@ -371,15 +376,14 @@ updateTrustLine(
|
||||
{
|
||||
// VFALCO Where is the line being deleted?
|
||||
// Clear the reserve of the sender, possibly delete the line!
|
||||
auto const currentSponsor = getLedgerEntryReserveSponsor(
|
||||
view, state.rawSle(), bSenderHigh ? sfHighSponsor : sfLowSponsor);
|
||||
auto const currentSponsor =
|
||||
getLedgerEntryReserveSponsor(view, state, bSenderHigh ? sfHighSponsor : sfLowSponsor);
|
||||
decreaseOwnerCount(view, sle, currentSponsor, 1, j);
|
||||
|
||||
// Clear reserve flag.
|
||||
state->clearFlag(senderReserveFlag);
|
||||
|
||||
removeSponsorFromLedgerEntry(
|
||||
state.mutableRawSle(), !bSenderHigh ? sfLowSponsor : sfHighSponsor);
|
||||
removeSponsorFromLedgerEntry(state, !bSenderHigh ? sfLowSponsor : sfHighSponsor);
|
||||
|
||||
// Balance is zero, receiver reserve is clear.
|
||||
if (!after && !state->isFlag(receiverReserveFlag))
|
||||
@@ -414,7 +418,7 @@ issueIOU(
|
||||
|
||||
auto const index = keylet::trustLine(issue.account, account, issue.currency);
|
||||
|
||||
if (RippleStateEntryW state(index, view, j); state)
|
||||
if (auto state = view.peek(index))
|
||||
{
|
||||
STAmount finalBalance = state->getFieldAmount(sfBalance);
|
||||
|
||||
@@ -447,7 +451,7 @@ issueIOU(
|
||||
j);
|
||||
}
|
||||
|
||||
state.update();
|
||||
view.update(state);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
@@ -507,7 +511,7 @@ redeemIOU(
|
||||
|
||||
bool const bSenderHigh = account > issue.account;
|
||||
|
||||
if (RippleStateEntryW state(account, issue.account, issue.currency, view, j); state)
|
||||
if (auto state = view.peek(keylet::trustLine(account, issue.account, issue.currency)))
|
||||
{
|
||||
STAmount finalBalance = state->getFieldAmount(sfBalance);
|
||||
|
||||
@@ -541,7 +545,7 @@ redeemIOU(
|
||||
j);
|
||||
}
|
||||
|
||||
state.update();
|
||||
view.update(state);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -568,7 +572,7 @@ requireAuth(ReadView const& view, Issue const& issue, AccountID const& account,
|
||||
if (isXRP(issue) || issue.account == account)
|
||||
return tesSUCCESS;
|
||||
|
||||
RippleStateEntryR const trustLine(account, issue.account, issue.currency, view);
|
||||
auto const trustLine = view.read(keylet::trustLine(account, issue.account, issue.currency));
|
||||
// If account has no line, and this is a strong check, fail
|
||||
if (!trustLine && authType == AuthType::StrongAuth)
|
||||
return tecNO_LINE;
|
||||
@@ -580,7 +584,7 @@ requireAuth(ReadView const& view, Issue const& issue, AccountID const& account,
|
||||
{
|
||||
if (trustLine)
|
||||
{
|
||||
if (trustLine.isAuthorized(account, issue.account))
|
||||
if (trustLine->isFlag((account > issue.account) ? lsfLowAuth : lsfHighAuth))
|
||||
return tesSUCCESS;
|
||||
|
||||
// A pseudo-account cannot submit transactions and only stores assets for the object
|
||||
@@ -612,7 +616,7 @@ canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, Acc
|
||||
auto const isRippleDisabled = [&](AccountID account) -> bool {
|
||||
// Line might not exist, but some transfers can create it. If this
|
||||
// is the case, just check the default ripple on the issuer account.
|
||||
RippleStateEntryR const line(account, issue, view);
|
||||
auto const line = view.read(keylet::trustLine(account, issue));
|
||||
if (line)
|
||||
{
|
||||
bool const issuerHigh = issuerId > account;
|
||||
@@ -739,7 +743,7 @@ removeEmptyHolding(
|
||||
// If the account is the issuer, then no line should exist. Check anyway.
|
||||
// If a line does exist, it will get deleted. If not, return success.
|
||||
bool const accountIsIssuer = accountID == issue.account;
|
||||
RippleStateEntryW line(accountID, issue, ctx.view, journal);
|
||||
auto const line = ctx.view.peek(keylet::trustLine(accountID, issue));
|
||||
if (!line)
|
||||
return accountIsIssuer ? (TER)tesSUCCESS : (TER)tecOBJECT_NOT_FOUND;
|
||||
if (!accountIsIssuer && line->at(sfBalance)->iou() != beast::kZero)
|
||||
@@ -753,15 +757,14 @@ removeEmptyHolding(
|
||||
if (!sleLowAccount)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const currentLowSponsor =
|
||||
getLedgerEntryReserveSponsor(ctx.view, line.rawSle(), sfLowSponsor);
|
||||
auto const currentLowSponsor = getLedgerEntryReserveSponsor(ctx.view, line, sfLowSponsor);
|
||||
|
||||
decreaseOwnerCount(ctx.view, sleLowAccount, currentLowSponsor, 1, journal);
|
||||
// It's not really necessary to clear the reserve flag, since the line
|
||||
// is about to be deleted, but this will make the metadata reflect an
|
||||
// accurate state at the time of deletion.
|
||||
line->clearFlag(lsfLowReserve);
|
||||
removeSponsorFromLedgerEntry(line.mutableRawSle(), sfLowSponsor);
|
||||
removeSponsorFromLedgerEntry(line, sfLowSponsor);
|
||||
}
|
||||
|
||||
if (line->isFlag(lsfHighReserve))
|
||||
@@ -771,15 +774,14 @@ removeEmptyHolding(
|
||||
if (!sleHighAccount)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const currentHighSponsor =
|
||||
getLedgerEntryReserveSponsor(ctx.view, line.rawSle(), sfHighSponsor);
|
||||
auto const currentHighSponsor = getLedgerEntryReserveSponsor(ctx.view, line, sfHighSponsor);
|
||||
|
||||
decreaseOwnerCount(ctx.view, sleHighAccount, currentHighSponsor, 1, journal);
|
||||
// It's not really necessary to clear the reserve flag, since the line
|
||||
// is about to be deleted, but this will make the metadata reflect an
|
||||
// accurate state at the time of deletion.
|
||||
line->clearFlag(lsfHighReserve);
|
||||
removeSponsorFromLedgerEntry(line.mutableRawSle(), sfHighSponsor);
|
||||
removeSponsorFromLedgerEntry(line, sfHighSponsor);
|
||||
}
|
||||
|
||||
return trustDelete(
|
||||
@@ -793,7 +795,7 @@ removeEmptyHolding(
|
||||
TER
|
||||
deleteAMMTrustLine(
|
||||
ApplyView& view,
|
||||
RippleStateEntryW& sleState,
|
||||
SLE::pointer sleState,
|
||||
std::optional<AccountID> const& ammAccountID,
|
||||
beast::Journal j)
|
||||
{
|
||||
@@ -823,13 +825,8 @@ deleteAMMTrustLine(
|
||||
if (ammAccountID && (low != *ammAccountID && high != *ammAccountID))
|
||||
return terNO_AMM;
|
||||
|
||||
auto const sponsorSle = getLedgerEntryReserveSponsor(
|
||||
view, sleState.rawSle(), !ammLow ? sfLowSponsor : sfHighSponsor);
|
||||
|
||||
// trustDelete() drops the entry's SLE, so read the reserve flag first.
|
||||
// trustDelete() does not change the flags.
|
||||
auto const uFlags = !ammLow ? lsfLowReserve : lsfHighReserve;
|
||||
bool const hasReserve = sleState->isFlag(uFlags);
|
||||
auto const sponsorSle =
|
||||
getLedgerEntryReserveSponsor(view, sleState, !ammLow ? sfLowSponsor : sfHighSponsor);
|
||||
|
||||
if (auto const ter = trustDelete(view, sleState, low, high, j); !isTesSuccess(ter))
|
||||
{
|
||||
@@ -837,7 +834,8 @@ deleteAMMTrustLine(
|
||||
return ter;
|
||||
}
|
||||
|
||||
if (!hasReserve)
|
||||
auto const uFlags = !ammLow ? lsfLowReserve : lsfHighReserve;
|
||||
if (!sleState->isFlag(uFlags))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
decreaseOwnerCount(view, !ammLow ? sleLow : sleHigh, sponsorSle, 1, j);
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/RippleStateEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
@@ -33,7 +32,6 @@
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
|
||||
@@ -267,7 +265,7 @@ checkDepositFreeze(
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
static std::optional<RippleStateEntryR>
|
||||
static SLE::const_pointer
|
||||
getLineIfUsable(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
@@ -276,11 +274,11 @@ getLineIfUsable(
|
||||
FreezeHandling zeroIfFrozen,
|
||||
beast::Journal j)
|
||||
{
|
||||
RippleStateEntryR sle(account, issuer, currency, view);
|
||||
auto sle = view.read(keylet::trustLine(account, issuer, currency));
|
||||
|
||||
if (!sle)
|
||||
{
|
||||
return std::nullopt;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (zeroIfFrozen == FreezeHandling::ZeroIfFrozen)
|
||||
@@ -288,7 +286,7 @@ getLineIfUsable(
|
||||
if (isFrozen(view, account, currency, issuer) ||
|
||||
isDeepFrozen(view, account, currency, issuer))
|
||||
{
|
||||
return std::nullopt;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// when fixFrozenLPTokenTransfer is enabled, if currency is lptoken,
|
||||
@@ -298,7 +296,7 @@ getLineIfUsable(
|
||||
auto const sleIssuer = view.read(keylet::account(issuer));
|
||||
if (!sleIssuer)
|
||||
{
|
||||
return std::nullopt; // LCOV_EXCL_LINE
|
||||
return nullptr; // LCOV_EXCL_LINE
|
||||
}
|
||||
if (sleIssuer->isFieldPresent(sfAMMID))
|
||||
{
|
||||
@@ -307,7 +305,7 @@ getLineIfUsable(
|
||||
if (!sleAmm ||
|
||||
isLPTokenFrozen(view, account, (*sleAmm)[sfAsset], (*sleAmm)[sfAsset2]))
|
||||
{
|
||||
return std::nullopt;
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -318,7 +316,7 @@ getLineIfUsable(
|
||||
// gated by featureMPTokensV2.
|
||||
if (!isTesSuccess(canTransferLPToken(view, account, account, issuer)))
|
||||
{
|
||||
return std::nullopt;
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,7 +326,7 @@ getLineIfUsable(
|
||||
static STAmount
|
||||
getTrustLineBalance(
|
||||
ReadView const& view,
|
||||
std::optional<RippleStateEntryR> const& sle,
|
||||
SLE::ConstRef sle,
|
||||
AccountID const& account,
|
||||
Currency const& currency,
|
||||
AccountID const& issuer,
|
||||
@@ -338,7 +336,7 @@ getTrustLineBalance(
|
||||
STAmount amount;
|
||||
if (sle)
|
||||
{
|
||||
amount = (*sle)->getFieldAmount(sfBalance);
|
||||
amount = sle->getFieldAmount(sfBalance);
|
||||
bool const accountHigh = account > issuer;
|
||||
auto const& oppositeField = accountHigh ? sfLowLimit : sfHighLimit;
|
||||
if (accountHigh)
|
||||
@@ -348,7 +346,7 @@ getTrustLineBalance(
|
||||
}
|
||||
if (includeOppositeLimit)
|
||||
{
|
||||
amount += (*sle)->getFieldAmount(oppositeField);
|
||||
amount += sle->getFieldAmount(oppositeField);
|
||||
}
|
||||
amount.get<Issue>().account = issuer;
|
||||
}
|
||||
@@ -388,7 +386,7 @@ accountHolds(
|
||||
}
|
||||
|
||||
// IOU: Return balance on trust line modulo freeze
|
||||
std::optional<RippleStateEntryR> const sle =
|
||||
SLE::const_pointer const sle =
|
||||
getLineIfUsable(view, account, currency, issuer, zeroIfFrozen, j);
|
||||
|
||||
return getTrustLineBalance(view, sle, account, currency, issuer, returnSpendable, j);
|
||||
@@ -715,7 +713,7 @@ directSendNoFeeIOU(
|
||||
"xrpl::directSendNoFeeIOU : receiver is not XRP");
|
||||
|
||||
// If the line exists, modify it accordingly.
|
||||
if (RippleStateEntryW sleRippleState(index, view, j); sleRippleState)
|
||||
if (auto const sleRippleState = view.peek(index))
|
||||
{
|
||||
STAmount saBalance = sleRippleState->getFieldAmount(sfBalance);
|
||||
|
||||
@@ -761,11 +759,11 @@ directSendNoFeeIOU(
|
||||
{
|
||||
// Clear the reserve of the sender, possibly delete the line!
|
||||
auto const currentSponsor = getLedgerEntryReserveSponsor(
|
||||
view, sleRippleState.rawSle(), !bSenderHigh ? sfLowSponsor : sfHighSponsor);
|
||||
view, sleRippleState, !bSenderHigh ? sfLowSponsor : sfHighSponsor);
|
||||
decreaseOwnerCount(view, view.peek(keylet::account(uSenderID)), currentSponsor, 1, j);
|
||||
|
||||
removeSponsorFromLedgerEntry(
|
||||
sleRippleState.mutableRawSle(), !bSenderHigh ? sfLowSponsor : sfHighSponsor);
|
||||
sleRippleState, !bSenderHigh ? sfLowSponsor : sfHighSponsor);
|
||||
|
||||
// Clear reserve flag.
|
||||
sleRippleState->clearFlag(senderReserveFlag);
|
||||
@@ -793,7 +791,7 @@ directSendNoFeeIOU(
|
||||
j);
|
||||
}
|
||||
|
||||
sleRippleState.update();
|
||||
view.update(sleRippleState);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
@@ -194,7 +193,7 @@ public:
|
||||
if (!s.ok())
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
std::format("Unable to set RocksDB bbt_options: {}", s.ToString()));
|
||||
std::string("Unable to set RocksDB bbt_options: ") + s.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +206,7 @@ public:
|
||||
if (!s.ok())
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
std::format("Unable to set RocksDB options: {}", s.ToString()));
|
||||
std::string("Unable to set RocksDB options: ") + s.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,7 +241,7 @@ public:
|
||||
if (!status.ok() || (localDb == nullptr))
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
std::format("Unable to open/create RocksDB: {}", status.ToString()));
|
||||
std::string("Unable to open/create RocksDB: ") + status.ToString());
|
||||
}
|
||||
db.reset(localDb);
|
||||
}
|
||||
@@ -357,7 +356,7 @@ public:
|
||||
auto ret = db->Write(options, &wb);
|
||||
|
||||
if (!ret.ok())
|
||||
Throw<std::runtime_error>(std::format("storeBatch failed: {}", ret.ToString()));
|
||||
Throw<std::runtime_error>("storeBatch failed: " + ret.ToString());
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <format>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
@@ -106,8 +105,8 @@ Permission::Permission()
|
||||
if (type <= UINT16_MAX)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
Throw<std::logic_error>(std::format(
|
||||
"Granular permission value must exceed the maximum uint16_t value: {}", name));
|
||||
Throw<std::logic_error>(
|
||||
"Granular permission value must exceed the maximum uint16_t value: " + name);
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,9 +43,8 @@ setCurrentTransactionRules(std::optional<Rules> r)
|
||||
auto const range = [&r]() {
|
||||
// If any new conditions with new amendments are added to "enableLargeNumbers", those
|
||||
// amendments must also be added to useRulesGuards.
|
||||
bool const enableLargeNumbers = !r ||
|
||||
(r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol) ||
|
||||
r->enabled(featureMPTokensV2));
|
||||
bool const enableLargeNumbers =
|
||||
!r || (r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol));
|
||||
// If enableLargeNumbers is true, then useRulesGuards must also return true.
|
||||
// However, the reverse is not true. Other amendments can cause the rules guard to be used,
|
||||
// even though large numbers are _not_ used.
|
||||
@@ -85,8 +84,7 @@ useRulesGuards(Rules const& rules)
|
||||
// with createGuards, and any other callers, and the first set of guards can be created directly
|
||||
// at the call site, without using optional.
|
||||
return rules.enabled(featureSingleAssetVault) || rules.enabled(featureLendingProtocol) ||
|
||||
rules.enabled(fixCleanup3_2_0) || rules.enabled(fixCleanup3_3_0) ||
|
||||
rules.enabled(featureMPTokensV2);
|
||||
rules.enabled(fixCleanup3_2_0) || rules.enabled(fixCleanup3_3_0);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <format>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
@@ -1194,7 +1193,8 @@ muldiv(std::uint64_t multiplier, std::uint64_t multiplicand, std::uint64_t divis
|
||||
if (ret > std::numeric_limits<std::uint64_t>::max())
|
||||
{
|
||||
Throw<std::overflow_error>(
|
||||
std::format("overflow: ({} * {}) / {}", multiplier, multiplicand, divisor));
|
||||
"overflow: (" + std::to_string(multiplier) + " * " + std::to_string(multiplicand) +
|
||||
") / " + std::to_string(divisor));
|
||||
}
|
||||
|
||||
return static_cast<uint64_t>(ret);
|
||||
@@ -1215,8 +1215,9 @@ muldivRound(
|
||||
|
||||
if (ret > std::numeric_limits<std::uint64_t>::max())
|
||||
{
|
||||
Throw<std::overflow_error>(std::format(
|
||||
"overflow: (({} * {}) + {}) / {}", multiplier, multiplicand, rounding, divisor));
|
||||
Throw<std::overflow_error>(
|
||||
"overflow: ((" + std::to_string(multiplier) + " * " + std::to_string(multiplicand) +
|
||||
") + " + std::to_string(rounding) + ") / " + std::to_string(divisor));
|
||||
}
|
||||
|
||||
return static_cast<uint64_t>(ret);
|
||||
|
||||
@@ -35,8 +35,9 @@ STLedgerEntry::STLedgerEntry(Keylet const& k) : STObject(sfLedgerEntry), key_(k.
|
||||
|
||||
if (format == nullptr)
|
||||
{
|
||||
Throw<std::runtime_error>(std::format(
|
||||
"Attempt to create a SLE of unknown type {}", safeCast<std::uint16_t>(k.type)));
|
||||
Throw<std::runtime_error>(
|
||||
"Attempt to create a SLE of unknown type " +
|
||||
std::to_string(safeCast<std::uint16_t>(k.type)));
|
||||
}
|
||||
|
||||
set(format->getSOTemplate());
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <format>
|
||||
#include <limits>
|
||||
#include <ostream>
|
||||
#include <stdexcept>
|
||||
@@ -171,7 +170,7 @@ partsFromString(std::string const& number)
|
||||
boost::smatch match;
|
||||
|
||||
if (!boost::regex_match(number, match, kReNumber))
|
||||
Throw<std::runtime_error>(std::format("'{}' is not a number", number));
|
||||
Throw<std::runtime_error>("'" + number + "' is not a number");
|
||||
|
||||
// Match fields:
|
||||
// 0 = whole input
|
||||
|
||||
@@ -59,8 +59,9 @@ getTxFormat(TxType type)
|
||||
|
||||
if (format == nullptr)
|
||||
{
|
||||
Throw<std::runtime_error>(std::format(
|
||||
"Invalid transaction type {}", safeCast<std::underlying_type_t<TxType>>(type)));
|
||||
Throw<std::runtime_error>(
|
||||
"Invalid transaction type " +
|
||||
std::to_string(safeCast<std::underlying_type_t<TxType>>(type)));
|
||||
}
|
||||
|
||||
return format;
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <format>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -22,7 +22,7 @@ STVector256::STVector256(SerialIter& sit, SField const& name) : STBase(name)
|
||||
if (slice.size() % UInt256::size() != 0)
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
std::format("Bad serialization for STVector256: {}", slice.size()));
|
||||
"Bad serialization for STVector256: " + std::to_string(slice.size()));
|
||||
}
|
||||
|
||||
auto const cnt = slice.size() / UInt256::size();
|
||||
|
||||
@@ -70,8 +70,7 @@ STXChainBridge::STXChainBridge(SField const& name, json::Value const& v) : STBas
|
||||
std::string const name = it.memberName();
|
||||
if (!kBridgeJson.isMember(name))
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
std::format("STXChainBridge extra field detected: {}", name));
|
||||
Throw<std::runtime_error>("STXChainBridge extra field detected: " + name);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -456,7 +456,7 @@ SerialIter::getFieldID(int& type, int& name)
|
||||
// uncommon type
|
||||
type = get8();
|
||||
if (type < 16)
|
||||
Throw<std::runtime_error>(std::format("gFID: uncommon type out of range {}", type));
|
||||
Throw<std::runtime_error>("gFID: uncommon type out of range " + std::to_string(type));
|
||||
}
|
||||
|
||||
if (name == 0)
|
||||
@@ -464,7 +464,7 @@ SerialIter::getFieldID(int& type, int& name)
|
||||
// uncommon name
|
||||
name = get8();
|
||||
if (name < 16)
|
||||
Throw<std::runtime_error>(std::format("gFID: uncommon name out of range {}", name));
|
||||
Throw<std::runtime_error>("gFID: uncommon name out of range " + std::to_string(name));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAccount.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
@@ -15,12 +13,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 <limits>
|
||||
#include <stdexcept>
|
||||
|
||||
@@ -149,52 +145,6 @@ TxMeta::getAffectedAccounts() const
|
||||
return list;
|
||||
}
|
||||
|
||||
std::flat_set<MPTID>
|
||||
TxMeta::getAffectedMPTs() const
|
||||
{
|
||||
std::flat_set<MPTID> list;
|
||||
|
||||
for (auto const& it : nodes_)
|
||||
{
|
||||
int const index =
|
||||
it.getFieldIndex((it.getFName() == sfCreatedNode) ? sfNewFields : sfFinalFields);
|
||||
|
||||
if (index != -1)
|
||||
{
|
||||
auto inner = dynamic_cast<STObject const*>(&it.peekAtIndex(index));
|
||||
XRPL_ASSERT(inner, "xrpl::getAffectedMPTs : STObject type cast succeeded");
|
||||
if (inner != nullptr)
|
||||
{
|
||||
// An MPTokenIssuance entry does not store its own issuance id;
|
||||
// the id is derived from the issuer and the sequence that
|
||||
// created the issuance.
|
||||
if (it.getFieldU16(sfLedgerEntryType) == ltMPTOKEN_ISSUANCE)
|
||||
{
|
||||
list.insert(
|
||||
makeMptID(inner->getFieldU32(sfSequence), inner->getAccountID(sfIssuer)));
|
||||
}
|
||||
|
||||
for (auto const& field : *inner)
|
||||
{
|
||||
if (auto mptID = dynamic_cast<STBitString<192> const*>(&field);
|
||||
field.getFName() == sfMPTokenIssuanceID && (mptID != nullptr))
|
||||
{
|
||||
list.insert(mptID->value());
|
||||
}
|
||||
else if (
|
||||
auto amount = dynamic_cast<STAmount const*>(&field);
|
||||
(amount != nullptr) && amount->holds<MPTIssue>())
|
||||
{
|
||||
list.insert(amount->get<MPTIssue>().getMptID());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
STObject&
|
||||
TxMeta::getAffectedNode(SLE::Ref node, SField const& type)
|
||||
{
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
|
||||
#include <soci/sqlite3/soci-sqlite3.h> // IWYU pragma: keep
|
||||
|
||||
#include <format>
|
||||
#include <memory>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -41,8 +40,8 @@ getSociSqliteInit(std::string const& name, std::string const& dir, std::string c
|
||||
{
|
||||
if (name.empty())
|
||||
{
|
||||
Throw<std::runtime_error>(std::format(
|
||||
"Sqlite databases must specify a dir and a name. Name: {} Dir: {}", name, dir));
|
||||
Throw<std::runtime_error>(
|
||||
"Sqlite databases must specify a dir and a name. Name: " + name + " Dir: " + dir);
|
||||
}
|
||||
std::filesystem::path file(dir);
|
||||
if (std::filesystem::is_directory(file))
|
||||
@@ -57,7 +56,7 @@ getSociInit(BasicConfig const& config, std::string const& dbName)
|
||||
auto const backendName = get(section, Keys::kBackend, "sqlite");
|
||||
|
||||
if (backendName != "sqlite")
|
||||
Throw<std::runtime_error>(std::format("Unsupported soci backend: {}", backendName));
|
||||
Throw<std::runtime_error>("Unsupported soci backend: " + backendName);
|
||||
|
||||
auto const path = config.legacy(Sections::kDatabasePath);
|
||||
auto const ext = dbName == "validators" || dbName == "peerfinder" ? ".sqlite" : ".db";
|
||||
@@ -102,7 +101,7 @@ open(soci::session& s, std::string const& beName, std::string const& connectionS
|
||||
}
|
||||
else
|
||||
{
|
||||
Throw<std::runtime_error>(std::format("Unsupported soci backend: {}", beName));
|
||||
Throw<std::runtime_error>("Unsupported soci backend: " + beName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
#include <xrpl/server/InfoSub.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Book.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/resource/Consumer.h>
|
||||
|
||||
#include <cstddef>
|
||||
@@ -44,22 +42,6 @@ safeUnsub(std::uint64_t seq, F&& f, beast::Journal j) noexcept
|
||||
}
|
||||
}
|
||||
|
||||
// Number of requested entries not already tracked in `existing`. Only these are
|
||||
// charged against the cap, so re-subscribing entries a connection already holds
|
||||
// is free.
|
||||
template <typename T>
|
||||
[[nodiscard]] std::size_t
|
||||
countNew(HashSet<T> const& requested, HashSet<T> const& existing)
|
||||
{
|
||||
std::size_t fresh = 0;
|
||||
for (auto const& entry : requested)
|
||||
{
|
||||
if (!existing.contains(entry))
|
||||
++fresh;
|
||||
}
|
||||
return fresh;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// This is the primary interface into the "client" portion of the program.
|
||||
@@ -102,16 +84,6 @@ InfoSub::~InfoSub()
|
||||
safeUnsub(seq_, [&] { source_.unsubPeerStatus(seq_); }, j);
|
||||
safeUnsub(seq_, [&] { source_.unsubConsensus(seq_); }, j);
|
||||
|
||||
// MPT subscriptions are torn down inline here, keyed on seq_, like books
|
||||
// below. The set is capped; each unsubMPTInternal takes mptLock_ for a
|
||||
// single O(1) erase and releases it, so a competing MPT publish can
|
||||
// interleave between erases. Use the internal variant so it does not write
|
||||
// back to mptSubscriptions_ on this partially-destroyed object.
|
||||
for (auto const& mptID : mptSubscriptions_)
|
||||
{
|
||||
safeUnsub(seq_, [&] { source_.unsubMPTInternal(seq_, mptID); }, j);
|
||||
}
|
||||
|
||||
// Book subscriptions are torn down inline here, keyed on seq_, rather than
|
||||
// through the chunked account cleanup below. The book set is not capped, so
|
||||
// it can be large; but each unsubBookInternal takes bookLock_ for a single
|
||||
@@ -173,21 +145,16 @@ InfoSub::onSendEmpty()
|
||||
{
|
||||
}
|
||||
|
||||
std::size_t
|
||||
InfoSub::subscriptionCount(ScopedLock const&) const
|
||||
{
|
||||
return normalSubscriptions_.size() + realTimeSubscriptions_.size() +
|
||||
accountHistorySubscriptions_.size() + mptSubscriptions_.size();
|
||||
}
|
||||
|
||||
std::size_t
|
||||
InfoSub::totalSubscriptionCount() const
|
||||
{
|
||||
// Hold lock_ for the whole read so the counted sets cannot be mutated
|
||||
// Hold lock_ for the whole read so the three sets cannot be mutated
|
||||
// mid-count by a concurrent (un)subscribe on this connection.
|
||||
std::scoped_lock const sl(lock_);
|
||||
|
||||
return subscriptionCount(sl);
|
||||
// Combined tally the per-connection cap is enforced against.
|
||||
return normalSubscriptions_.size() + realTimeSubscriptions_.size() +
|
||||
accountHistorySubscriptions_.size();
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -199,10 +166,25 @@ InfoSub::tryReserveAccountSubscriptions(
|
||||
// One lock hold covers the count, the check and the insert.
|
||||
std::scoped_lock const sl(lock_);
|
||||
|
||||
// Entries not already tracked; re-subscribing held accounts is not charged.
|
||||
auto const countNew = [](HashSet<AccountID> const& requested,
|
||||
HashSet<AccountID> const& existing) {
|
||||
std::size_t fresh = 0;
|
||||
for (auto const& account : requested)
|
||||
{
|
||||
if (!existing.contains(account))
|
||||
++fresh;
|
||||
}
|
||||
return fresh;
|
||||
};
|
||||
|
||||
std::size_t const additional = countNew(proposedAccounts, realTimeSubscriptions_) +
|
||||
countNew(normalAccounts, normalSubscriptions_);
|
||||
|
||||
if (exceedsSubscriptionCap(subscriptionCount(sl), additional, cap))
|
||||
std::size_t const current = normalSubscriptions_.size() + realTimeSubscriptions_.size() +
|
||||
accountHistorySubscriptions_.size();
|
||||
|
||||
if (exceedsSubscriptionCap(current, additional, cap))
|
||||
return false;
|
||||
|
||||
realTimeSubscriptions_.insert(proposedAccounts.begin(), proposedAccounts.end());
|
||||
@@ -210,19 +192,6 @@ InfoSub::tryReserveAccountSubscriptions(
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
InfoSub::tryReserveMPTSubscriptions(HashSet<MPTID> const& mptIDs, std::size_t cap)
|
||||
{
|
||||
// One lock hold covers the count, the check and the insert.
|
||||
std::scoped_lock const sl(lock_);
|
||||
|
||||
if (exceedsSubscriptionCap(subscriptionCount(sl), countNew(mptIDs, mptSubscriptions_), cap))
|
||||
return false;
|
||||
|
||||
mptSubscriptions_.insert(mptIDs.begin(), mptIDs.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
InfoSub::insertSubAccountInfo(AccountID const& account, bool rt)
|
||||
{
|
||||
@@ -319,20 +288,4 @@ InfoSub::getApiVersion() const noexcept
|
||||
return apiVersion_;
|
||||
}
|
||||
|
||||
void
|
||||
InfoSub::insertSubMPTInfo(MPTID const& mptID)
|
||||
{
|
||||
std::scoped_lock const sl(lock_);
|
||||
|
||||
mptSubscriptions_.insert(mptID);
|
||||
}
|
||||
|
||||
void
|
||||
InfoSub::deleteSubMPTInfo(MPTID const& mptID)
|
||||
{
|
||||
std::scoped_lock const sl(lock_);
|
||||
|
||||
mptSubscriptions_.erase(mptID);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -26,7 +26,6 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <format>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
@@ -49,7 +48,7 @@ to_string(Manifest const& m)
|
||||
return "Revocation Manifest " + mk;
|
||||
|
||||
if (!m.signingKey)
|
||||
Throw<std::runtime_error>(std::format("No SigningKey in manifest {}", mk));
|
||||
Throw<std::runtime_error>("No SigningKey in manifest " + mk);
|
||||
|
||||
return "Manifest " + mk + " (" + std::to_string(m.sequence) + ": " +
|
||||
toBase58(TokenType::NodePublic, *m.signingKey) + ")";
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <format>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
@@ -108,10 +107,10 @@ SHAMapNodeID::getChildNodeID(unsigned int branch) const
|
||||
depth_ <= SHAMap::kLeafDepth, "xrpl::SHAMapNodeID::getChildNodeID : maximum leaf depth");
|
||||
|
||||
if (depth_ >= SHAMap::kLeafDepth)
|
||||
Throw<std::logic_error>(std::format("Request for child node ID of {}", to_string(*this)));
|
||||
Throw<std::logic_error>("Request for child node ID of " + to_string(*this));
|
||||
|
||||
if (!isPrefixOf(id_))
|
||||
Throw<std::logic_error>(std::format("Incorrect mask for {}", to_string(*this)));
|
||||
Throw<std::logic_error>("Incorrect mask for " + to_string(*this));
|
||||
|
||||
SHAMapNodeID node{depth_ + 1, id_};
|
||||
node.id_.begin()[depth_ / 2] |= ((depth_ & 1) != 0u) ? branch : (branch << 4);
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include <xrpl/shamap/SHAMapTxPlusMetaLeafNode.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <format>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
@@ -31,8 +30,9 @@ SHAMapTreeNode::makeTransaction(Slice data, SHAMapHash const& hash, bool hashVal
|
||||
{
|
||||
if (data.size() < kMinShaMapItemBytes)
|
||||
{
|
||||
Throw<std::runtime_error>(std::format(
|
||||
"Short TXN node: {} bytes (minimum {} required)", data.size(), kMinShaMapItemBytes));
|
||||
Throw<std::runtime_error>(
|
||||
"Short TXN node: " + std::to_string(data.size()) + " bytes (minimum " +
|
||||
std::to_string(kMinShaMapItemBytes) + " required)");
|
||||
}
|
||||
|
||||
auto item = makeShamapitem(sha512Half(HashPrefix::TransactionId, data), data);
|
||||
@@ -52,25 +52,27 @@ SHAMapTreeNode::makeTransactionWithMeta(Slice data, SHAMapHash const& hash, bool
|
||||
|
||||
if (s.size() < tag.kBytes)
|
||||
{
|
||||
Throw<std::runtime_error>(std::format(
|
||||
"Short TXN+MD node: {} bytes (minimum {} required for tag)", s.size(), tag.kBytes));
|
||||
Throw<std::runtime_error>(
|
||||
"Short TXN+MD node: " + std::to_string(s.size()) + " bytes (minimum " +
|
||||
std::to_string(tag.kBytes) + " required for tag)");
|
||||
}
|
||||
|
||||
// FIXME: improve this interface so that the above check isn't needed
|
||||
if (!s.getBitString(tag, s.size() - tag.kBytes))
|
||||
{
|
||||
Throw<std::out_of_range>(std::format(
|
||||
"Short TXN+MD node: failed to read tag at offset {}", s.size() - tag.kBytes));
|
||||
Throw<std::out_of_range>(
|
||||
"Short TXN+MD node: failed to read tag at offset " +
|
||||
std::to_string(s.size() - tag.kBytes));
|
||||
}
|
||||
|
||||
s.chop(tag.kBytes);
|
||||
|
||||
if (s.size() < kMinShaMapItemBytes)
|
||||
{
|
||||
Throw<std::runtime_error>(std::format(
|
||||
"Short TXN+MD node: {} bytes after tag removal (minimum {} required)",
|
||||
s.size(),
|
||||
kMinShaMapItemBytes));
|
||||
Throw<std::runtime_error>(
|
||||
"Short TXN+MD node: " + std::to_string(s.size()) +
|
||||
" bytes after tag removal (minimum " + std::to_string(kMinShaMapItemBytes) +
|
||||
" required)");
|
||||
}
|
||||
|
||||
auto item = makeShamapitem(tag, s.slice());
|
||||
@@ -90,15 +92,16 @@ SHAMapTreeNode::makeAccountState(Slice data, SHAMapHash const& hash, bool hashVa
|
||||
|
||||
if (s.size() < tag.kBytes)
|
||||
{
|
||||
Throw<std::runtime_error>(std::format(
|
||||
"Short AS node: {} bytes (minimum {} required for tag)", s.size(), tag.kBytes));
|
||||
Throw<std::runtime_error>(
|
||||
"Short AS node: " + std::to_string(s.size()) + " bytes (minimum " +
|
||||
std::to_string(tag.kBytes) + " required for tag)");
|
||||
}
|
||||
|
||||
// FIXME: improve this interface so that the above check isn't needed
|
||||
if (!s.getBitString(tag, s.size() - tag.kBytes))
|
||||
{
|
||||
Throw<std::out_of_range>(
|
||||
std::format("Short AS node: failed to read tag at offset {}", s.size() - tag.kBytes));
|
||||
"Short AS node: failed to read tag at offset " + std::to_string(s.size() - tag.kBytes));
|
||||
}
|
||||
|
||||
s.chop(tag.kBytes);
|
||||
@@ -108,10 +111,9 @@ SHAMapTreeNode::makeAccountState(Slice data, SHAMapHash const& hash, bool hashVa
|
||||
|
||||
if (s.size() < kMinShaMapItemBytes)
|
||||
{
|
||||
Throw<std::runtime_error>(std::format(
|
||||
"Short AS node: {} bytes after tag removal (minimum {} required)",
|
||||
s.size(),
|
||||
kMinShaMapItemBytes));
|
||||
Throw<std::runtime_error>(
|
||||
"Short AS node: " + std::to_string(s.size()) + " bytes after tag removal (minimum " +
|
||||
std::to_string(kMinShaMapItemBytes) + " required)");
|
||||
}
|
||||
|
||||
auto item = makeShamapitem(tag, s.slice());
|
||||
@@ -150,7 +152,7 @@ SHAMapTreeNode::makeFromWire(Slice rawNode)
|
||||
if (type == kWireTypeTransactionWithMeta)
|
||||
return makeTransactionWithMeta(rawNode, hash, hashValid);
|
||||
|
||||
Throw<std::runtime_error>(std::format("wire: Unknown type ({})", type));
|
||||
Throw<std::runtime_error>("wire: Unknown type (" + std::to_string(type) + ")");
|
||||
}
|
||||
|
||||
SHAMapTreeNodePtr
|
||||
@@ -181,8 +183,9 @@ SHAMapTreeNode::makeFromPrefix(Slice rawNode, SHAMapHash const& hash)
|
||||
if (type == HashPrefix::TxNode)
|
||||
return makeTransactionWithMeta(rawNode, hash, hashValid);
|
||||
|
||||
Throw<std::runtime_error>(std::format(
|
||||
"prefix: unknown type ({})", safeCast<std::underlying_type_t<HashPrefix>>(type)));
|
||||
Throw<std::runtime_error>(
|
||||
"prefix: unknown type (" +
|
||||
std::to_string(safeCast<std::underlying_type_t<HashPrefix>>(type)) + ")");
|
||||
}
|
||||
|
||||
std::string
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include <xrpl/json/to_string.h> // IWYU pragma: keep
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/RippleStateEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DelegateHelpers.h>
|
||||
@@ -1278,7 +1277,7 @@ removeDeletedTrustLines(
|
||||
|
||||
for (auto const& index : trustLines)
|
||||
{
|
||||
if (RippleStateEntryW sleState(Keylet(ltRIPPLE_STATE, index), view, viewJ);
|
||||
if (auto const sleState = view.peek({ltRIPPLE_STATE, index});
|
||||
!isTesSuccess(deleteAMMTrustLine(view, sleState, std::nullopt, viewJ)))
|
||||
{
|
||||
JLOG(viewJ.error()) << "removeDeletedTrustLines: failed to delete AMM trustline";
|
||||
|
||||
@@ -41,7 +41,6 @@ constexpr auto kConfidentialMptTxTypes = std::to_array<TxType>({
|
||||
ttCONFIDENTIAL_MPT_MERGE_INBOX,
|
||||
ttCONFIDENTIAL_MPT_CLAWBACK,
|
||||
ttCONFIDENTIAL_MPT_MIRROR_UPDATE,
|
||||
ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE,
|
||||
});
|
||||
|
||||
// Clamp to the cap (== INT64_MAX) before the signed conversion. Invariant
|
||||
@@ -74,7 +73,7 @@ ValidMPTIssuance::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef
|
||||
// only meaningful post-fixCleanup3_2_0 (the field is never set
|
||||
// pre-amendment, and the holding-deletion rule does not apply).
|
||||
// Skip both blocks when the amendment is off so we avoid wasted work
|
||||
// on the hot path, except where noted for fixCleanup3_5_0 below.
|
||||
// on the hot path.
|
||||
bool const fix320Enabled = isFeatureEnabled(fixCleanup3_2_0);
|
||||
|
||||
if (after && after->getType() == ltMPTOKEN_ISSUANCE)
|
||||
@@ -109,10 +108,7 @@ ValidMPTIssuance::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef
|
||||
if (isDelete)
|
||||
{
|
||||
mptokensDeleted_++;
|
||||
// deletedHoldings_ also feeds finalize()'s erase-time public
|
||||
// balance check, gated on fixCleanup3_5_0 independently of
|
||||
// fixCleanup3_2_0.
|
||||
if (fix320Enabled || isFeatureEnabled(fixCleanup3_5_0))
|
||||
if (fix320Enabled)
|
||||
deletedHoldings_.push_back(after);
|
||||
}
|
||||
else if (!before)
|
||||
@@ -199,19 +195,6 @@ ValidMPTIssuance::finalize(
|
||||
return false;
|
||||
}
|
||||
|
||||
// Deleting an MPToken with a non-zero MPTAmount is rejected.
|
||||
if (rules.enabled(fixCleanup3_5_0))
|
||||
{
|
||||
for (auto const& sleHolding : deletedHoldings_)
|
||||
{
|
||||
if (sleHolding->getType() == ltMPTOKEN && sleHolding->getFieldU64(sfMPTAmount) > 0)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: MPToken deleted with non-zero balance";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isTesSuccess(result) || (mptV2Enabled && result == tecINCOMPLETE))
|
||||
{
|
||||
[[maybe_unused]]
|
||||
@@ -596,27 +579,16 @@ ValidConfidentialMPToken::visitEntry(
|
||||
change.mptAmountDelta =
|
||||
subtractMPTAmountDelta(change.mptAmountDelta, before->getFieldU64(sfMPTAmount));
|
||||
|
||||
// Cannot delete MPToken with non-zero confidential state.
|
||||
// Cannot delete MPToken with non-zero confidential state or non-zero public amount
|
||||
if (isDelete)
|
||||
{
|
||||
// changes_ is keyed by issuance, so sibling holders erased by the
|
||||
// same transaction share this entry. Only ever set these flags,
|
||||
// never clear them, or an empty sibling visited later would mask
|
||||
// a funded MPToken.
|
||||
|
||||
// Retired by fixCleanup3_5_0, which moved the public balance
|
||||
// check to ValidMPTIssuance::finalize. Kept pre-amendment for
|
||||
// consensus safety: a non-zero public balance used to feed the
|
||||
// confidential gate below, rejecting the erase whenever the
|
||||
// issuance's COA was non-zero, and already-validated ledgers
|
||||
// depend on that.
|
||||
if (!isFeatureEnabled(fixCleanup3_5_0) && before->getFieldU64(sfMPTAmount) > 0)
|
||||
changes_[id].deletedWithBalanceBefore = true;
|
||||
|
||||
if (before->isFieldPresent(sfConfidentialBalanceSpending) ||
|
||||
bool const hasPublicBalance = before->getFieldU64(sfMPTAmount) > 0;
|
||||
bool const hasEncryptedFields = before->isFieldPresent(sfConfidentialBalanceSpending) ||
|
||||
before->isFieldPresent(sfConfidentialBalanceInbox) ||
|
||||
before->isFieldPresent(sfIssuerEncryptedBalance) ||
|
||||
before->isFieldPresent(sfAuditorEncryptedBalance))
|
||||
before->isFieldPresent(sfAuditorEncryptedBalance);
|
||||
|
||||
if (hasPublicBalance || hasEncryptedFields)
|
||||
changes_[id].deletedWithEncrypted = true;
|
||||
}
|
||||
}
|
||||
@@ -722,8 +694,6 @@ ValidConfidentialMPToken::finalize(
|
||||
if (result != tesSUCCESS)
|
||||
return true;
|
||||
|
||||
bool const fix350Enabled = view.rules().enabled(fixCleanup3_5_0);
|
||||
|
||||
for (auto const& [id, checks] : changes_)
|
||||
{
|
||||
// Find the MPTokenIssuance
|
||||
@@ -738,20 +708,8 @@ ValidConfidentialMPToken::finalize(
|
||||
if (!issuance)
|
||||
continue;
|
||||
|
||||
// Cannot delete MPToken with non-zero confidential state.
|
||||
//
|
||||
// Before fixCleanup3_5_0 this gate also absorbed the pre-transaction
|
||||
// public balance, so any drain-then-erase of an MPToken -- an
|
||||
// AMMWithdraw of the whole pool, a LoanBrokerDelete returning cover --
|
||||
// was rejected whenever some unrelated holder of the same issuance
|
||||
// held a confidential balance. The COA gate itself is correct for
|
||||
// ciphertext and mirrors MPTokenAuthorize::preclaim; only the public
|
||||
// balance leg was misplaced.
|
||||
bool const deletedWithEncrypted = fix350Enabled
|
||||
? checks.deletedWithEncrypted
|
||||
: (checks.deletedWithEncrypted || checks.deletedWithBalanceBefore);
|
||||
|
||||
if (deletedWithEncrypted)
|
||||
// Cannot delete MPToken with non-zero confidential state
|
||||
if (checks.deletedWithEncrypted)
|
||||
{
|
||||
if ((*issuance)[~sfConfidentialOutstandingAmount].value_or(0) > 0)
|
||||
{
|
||||
@@ -826,10 +784,10 @@ ValidConfidentialMPToken::finalize(
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reaching here means this confidential MPT transaction left coaDelta
|
||||
// unmodified (e.g. ConfidentialMPTSend, ConfidentialMPTMergeInbox, or
|
||||
// ConfidentialMPTHolderKeyUpdate/ConfidentialMPTMirrorUpdate, none of which touch
|
||||
// sfConfidentialOutstandingAmount), so it must not modify sfOutstandingAmount either.
|
||||
// Among confidential MPT transactions, only ConfidentialMPTSend and
|
||||
// ConfidentialMPTMergeInbox leave coaDelta unmodified. Therefore, if a confidential MPT
|
||||
// transaction reaches here, it must be one of these two types, neither of which will
|
||||
// modify sfOutstandingAmount
|
||||
if (checks.outstandingDelta != 0)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: OutstandingAmount changed "
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/PaymentSandbox.h>
|
||||
#include <xrpl/ledger/Sandbox.h>
|
||||
#include <xrpl/ledger/entries/RippleStateEntry.h>
|
||||
#include <xrpl/ledger/helpers/AMMHelpers.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
@@ -1450,7 +1449,7 @@ BookStep<TIn, TOut, TDerived>::check(StrandContext const& ctx) const
|
||||
|
||||
auto const err = book_.in.visit(
|
||||
[&](Issue const& issue) -> std::optional<TER> {
|
||||
RippleStateEntryR const sle(*prev, cur, issue.currency, view);
|
||||
auto sle = view.read(keylet::trustLine(*prev, cur, issue.currency));
|
||||
if (!sle)
|
||||
return terNO_LINE;
|
||||
if (sle->isFlag((cur > *prev) ? lsfHighNoRipple : lsfLowNoRipple))
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/PaymentSandbox.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/RippleStateEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
@@ -346,7 +345,7 @@ DirectIPaymentStep::quality(ReadView const& sb, QualityDirection qDir) const
|
||||
if (src_ == dst_)
|
||||
return QUALITY_ONE;
|
||||
|
||||
RippleStateEntryR const sle(dst_, src_, currency_, sb);
|
||||
auto const sle = sb.read(keylet::trustLine(dst_, src_, currency_));
|
||||
|
||||
if (!sle)
|
||||
return QUALITY_ONE;
|
||||
@@ -422,7 +421,7 @@ DirectIPaymentStep::check(StrandContext const& ctx, SLE::ConstRef sleSrc) const
|
||||
// Since this is a payment a trust line must be present. Perform all
|
||||
// trust line related checks.
|
||||
{
|
||||
RippleStateEntryR const sleLine(src_, dst_, currency_, ctx.view);
|
||||
auto const sleLine = ctx.view.read(keylet::trustLine(src_, dst_, currency_));
|
||||
if (!sleLine)
|
||||
{
|
||||
JLOG(j_.trace()) << "DirectStepI: No credit line. " << *this;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/PaymentSandbox.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/RippleStateEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
@@ -199,7 +198,8 @@ CheckCash::preclaim(PreclaimContext const& ctx)
|
||||
return value.asset().visit(
|
||||
[&](Issue const& issue) -> TER {
|
||||
Currency const currency{issue.currency};
|
||||
RippleStateEntryR const sleTrustLine(dstId, issuerId, currency, ctx.view);
|
||||
auto const sleTrustLine =
|
||||
ctx.view.read(keylet::trustLine(dstId, issuerId, currency));
|
||||
|
||||
auto const sleIssuer = ctx.view.read(keylet::account(issuerId));
|
||||
if (!sleIssuer)
|
||||
@@ -506,7 +506,7 @@ CheckCash::doApply()
|
||||
// would exceed the limit on their trust line. So we
|
||||
// tweak the trust line limits before calling flow and
|
||||
// then restore the trust line limits afterwards.
|
||||
RippleStateEntryW sleTrustLine(*trustLineKey, psb, j_);
|
||||
auto const sleTrustLine = psb.peek(*trustLineKey);
|
||||
if (!sleTrustLine)
|
||||
return tecNO_LINE;
|
||||
|
||||
@@ -552,7 +552,7 @@ CheckCash::doApply()
|
||||
if (trustLineKey)
|
||||
{
|
||||
SF_AMOUNT const& tweakedLimit = destLow ? sfLowLimit : sfHighLimit;
|
||||
if (RippleStateEntryW sleTrustLine(*trustLineKey, psb); sleTrustLine)
|
||||
if (auto const sleTrustLine = psb.peek(*trustLineKey))
|
||||
sleTrustLine->at(tweakedLimit) = savedLimit;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/RippleStateEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
@@ -127,7 +126,8 @@ CheckCreate::preclaim(PreclaimContext const& ctx)
|
||||
if (issuerId != srcId)
|
||||
{
|
||||
// Check if the issuer froze the line
|
||||
RippleStateEntryR const sleTrust(srcId, issuerId, issue.currency, ctx.view);
|
||||
auto const sleTrust =
|
||||
ctx.view.read(keylet::trustLine(srcId, issuerId, issue.currency));
|
||||
if (sleTrust &&
|
||||
sleTrust->isFlag((issuerId > srcId) ? lsfHighFreeze : lsfLowFreeze))
|
||||
{
|
||||
@@ -138,7 +138,8 @@ CheckCreate::preclaim(PreclaimContext const& ctx)
|
||||
if (issuerId != dstId)
|
||||
{
|
||||
// Check if dst froze the line.
|
||||
RippleStateEntryR const sleTrust(issuerId, dstId, issue.currency, ctx.view);
|
||||
auto const sleTrust =
|
||||
ctx.view.read(keylet::trustLine(issuerId, dstId, issue.currency));
|
||||
if (sleTrust &&
|
||||
sleTrust->isFlag((dstId > issuerId) ? lsfHighFreeze : lsfLowFreeze))
|
||||
{
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/Sandbox.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/RippleStateEntry.h>
|
||||
#include <xrpl/ledger/helpers/AMMHelpers.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
@@ -276,7 +275,7 @@ applyCreate(ApplyContext& ctx, Sandbox& sb, AccountID const& account, beast::Jou
|
||||
|
||||
// LP Token already exists. (should not happen)
|
||||
auto const lptIss = ammLPTIssue(amount.asset(), amount2.asset(), accountId);
|
||||
if (RippleStateEntryR(accountId, lptIss, sb))
|
||||
if (sb.read(keylet::trustLine(accountId, lptIss)))
|
||||
{
|
||||
JLOG(j.error()) << "AMM Instance: LP Token already exists.";
|
||||
return {tecDUPLICATE, false};
|
||||
@@ -359,7 +358,8 @@ applyCreate(ApplyContext& ctx, Sandbox& sb, AccountID const& account, beast::Jou
|
||||
// Set AMM flag on AMM trustline
|
||||
if (!isXRP(amount))
|
||||
{
|
||||
RippleStateEntryW sleRippleState(accountId, issue, sb, j);
|
||||
SLE::pointer const sleRippleState =
|
||||
sb.peek(keylet::trustLine(accountId, issue));
|
||||
if (!sleRippleState)
|
||||
{
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
@@ -367,7 +367,7 @@ applyCreate(ApplyContext& ctx, Sandbox& sb, AccountID const& account, beast::Jou
|
||||
|
||||
auto const flags = sleRippleState->getFlags();
|
||||
sleRippleState->setFieldU32(sfFlags, flags | lsfAMMNode);
|
||||
sleRippleState.update();
|
||||
sb.update(sleRippleState);
|
||||
}
|
||||
return tesSUCCESS;
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/Sandbox.h>
|
||||
#include <xrpl/ledger/entries/RippleStateEntry.h>
|
||||
#include <xrpl/ledger/helpers/AMMHelpers.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
@@ -234,7 +233,8 @@ AMMDeposit::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
auto const lpIssue = (*ammSle)[sfLPTokenBalance].get<Issue>();
|
||||
// Adjust the reserve if LP doesn't have LPToken trustline
|
||||
RippleStateEntryR const sle(accountID, lpIssue.account, lpIssue.currency, ctx.view);
|
||||
auto const sle =
|
||||
ctx.view.read(keylet::trustLine(accountID, lpIssue.account, lpIssue.currency));
|
||||
if (xrpLiquid(ctx.view, accountID, !sle, ctx.j) >= deposit)
|
||||
return TER(tesSUCCESS);
|
||||
if (sle)
|
||||
@@ -584,7 +584,8 @@ AMMDeposit::deposit(
|
||||
{
|
||||
auto const& lpIssue = lpTokensDeposit.get<Issue>();
|
||||
// Adjust the reserve if LP doesn't have LPToken trustline
|
||||
RippleStateEntryR const sle(accountID_, lpIssue.account, lpIssue.currency, view);
|
||||
auto const sle =
|
||||
view.read(keylet::trustLine(accountID_, lpIssue.account, lpIssue.currency));
|
||||
if (xrpLiquid(view, accountID_, !sle, j_) >= depositAmount)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include <xrpl/ledger/PaymentSandbox.h>
|
||||
#include <xrpl/ledger/Sandbox.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/RippleStateEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
@@ -308,7 +307,7 @@ OfferCreate::checkAcceptAsset(
|
||||
return asset.visit(
|
||||
[&](Issue const& issue) -> TER {
|
||||
auto const& issuer = issue.getIssuer();
|
||||
RippleStateEntryR const trustLine(id, issuer, issue.currency, view);
|
||||
auto const trustLine = view.read(keylet::trustLine(id, issuer, issue.currency));
|
||||
|
||||
// Check if the issuer has lsfDisallowIncomingTrustline set.
|
||||
// If so, the account must already have a trustline to receive tokens.
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/RippleStateEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
@@ -215,7 +214,7 @@ escrowCreatePreclaimHelper<Issue>(
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// If the account does not have a trustline to the issuer, return tecNO_LINE
|
||||
RippleStateEntryR const sleRippleState(account, issuer, issue.currency, ctx.view);
|
||||
auto const sleRippleState = ctx.view.read(keylet::trustLine(account, issuer, issue.currency));
|
||||
if (!sleRippleState)
|
||||
return tecNO_LINE;
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/RippleStateEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/NFTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
@@ -318,7 +317,7 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx)
|
||||
if (ctx.view.rules().enabled(fixEnforceNFTokenTrustline) &&
|
||||
(nft::getFlags(tokenID) & nft::kFlagCreateTrustLines) == 0 &&
|
||||
nftMinter != amount.getIssuer() &&
|
||||
!RippleStateEntryR(nftMinter, amount.get<Issue>(), ctx.view))
|
||||
!ctx.view.read(keylet::trustLine(nftMinter, amount.get<Issue>())))
|
||||
return tecNO_LINE;
|
||||
|
||||
// Check that the issuer is allowed to receive IOUs.
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/PaymentSandbox.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/RippleStateEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
@@ -330,7 +329,7 @@ Payment::checkGranularSemantics(
|
||||
if (issue.getIssuer() != account && issue.getIssuer() != destination)
|
||||
return terNO_DELEGATE_PERMISSION;
|
||||
|
||||
RippleStateEntryR const sle(account, destination, issue.currency, view);
|
||||
auto const sle = view.read(keylet::trustLine(account, destination, issue.currency));
|
||||
if (!sle)
|
||||
return terNO_DELEGATE_PERMISSION;
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/entries/RippleStateEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -109,8 +108,8 @@ preclaimHelper<Issue>(
|
||||
if (!sleIssuer.isFlag(lsfAllowTrustLineClawback) || sleIssuer.isFlag(lsfNoFreeze))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
RippleStateEntryR const sleRippleState(
|
||||
holder, issuer, clawAmount.get<Issue>().currency, ctx.view);
|
||||
auto const sleRippleState =
|
||||
ctx.view.read(keylet::trustLine(holder, issuer, clawAmount.get<Issue>().currency));
|
||||
if (!sleRippleState)
|
||||
return tecNO_LINE;
|
||||
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
#include <xrpl/tx/transactors/token/ConfidentialMPTHolderKeyUpdate.h>
|
||||
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/ConfidentialTransfer.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
#include <bit>
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
bool
|
||||
ConfidentialMPTHolderKeyUpdate::checkExtraFeatures(PreflightContext const& ctx)
|
||||
{
|
||||
// Holder key update is only meaningful when confidential transfers are enabled.
|
||||
return ctx.rules.enabled(featureConfidentialTransfer);
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
ConfidentialMPTHolderKeyUpdate::getFlagsMask(PreflightContext const& ctx)
|
||||
{
|
||||
return tfConfidentialMPTHolderKeyUpdateMask;
|
||||
}
|
||||
|
||||
NotTEC
|
||||
ConfidentialMPTHolderKeyUpdate::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
bool const rotation = ctx.tx.isFlag(tfHolderKeyRotation);
|
||||
bool const recovery = ctx.tx.isFlag(tfHolderKeyRecovery);
|
||||
bool const cancel = ctx.tx.isFlag(tfCancelRecovery);
|
||||
|
||||
// Exactly one of the three mode flags must be set.
|
||||
static constexpr auto modeFlags = tfHolderKeyRotation | tfHolderKeyRecovery | tfCancelRecovery;
|
||||
if (std::popcount(ctx.tx.getFlags() & modeFlags) != 1)
|
||||
return temINVALID_FLAG;
|
||||
|
||||
// The issuer cannot hold confidential balances.
|
||||
if (ctx.tx[sfAccount] == MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer())
|
||||
return temMALFORMED;
|
||||
|
||||
bool const hasHolderKey = ctx.tx.isFieldPresent(sfHolderEncryptionKey);
|
||||
bool const hasSpending = ctx.tx.isFieldPresent(sfConfidentialBalanceSpending);
|
||||
bool const hasInbox = ctx.tx.isFieldPresent(sfConfidentialBalanceInbox);
|
||||
bool const hasProof = ctx.tx.isFieldPresent(sfZKProof);
|
||||
|
||||
if (cancel)
|
||||
{
|
||||
// Cancel mode only revokes a pending recovery authorization; it
|
||||
// carries no key material, balances, or proof.
|
||||
if (hasHolderKey || hasSpending || hasInbox || hasProof)
|
||||
return temMALFORMED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
// Rotation and Recovery both require a holder key and a proof.
|
||||
if (!hasHolderKey || !hasProof)
|
||||
return temMALFORMED;
|
||||
|
||||
// Rotation mode requires re-encrypted balances; Recovery mode must not
|
||||
// provide them since the holder cannot decrypt the current ones.
|
||||
if (rotation && (!hasSpending || !hasInbox))
|
||||
return temMALFORMED;
|
||||
|
||||
if (recovery && (hasSpending || hasInbox))
|
||||
return temMALFORMED;
|
||||
|
||||
if (hasSpending &&
|
||||
ctx.tx[sfConfidentialBalanceSpending].length() != kEcGamalEncryptedTotalLength)
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
if (hasInbox && ctx.tx[sfConfidentialBalanceInbox].length() != kEcGamalEncryptedTotalLength)
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
if (!isValidCompressedECPoint(ctx.tx[sfHolderEncryptionKey]))
|
||||
return temMALFORMED;
|
||||
|
||||
if (hasSpending && !isValidCiphertext(ctx.tx[sfConfidentialBalanceSpending]))
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
if (hasInbox && !isValidCiphertext(ctx.tx[sfConfidentialBalanceInbox]))
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
ConfidentialMPTHolderKeyUpdate::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
return Transactor::calculateBaseFee(view, tx, kConfidentialFeeMultiplier);
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialMPTHolderKeyUpdate::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
auto const account = ctx.tx[sfAccount];
|
||||
auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
|
||||
|
||||
auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID));
|
||||
if (!sleIssuance)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
if (!sleIssuance->isFlag(lsfMPTCanHoldConfidentialBalance))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
auto const sleMptoken = ctx.view.read(keylet::mptoken(mptIssuanceID, account));
|
||||
if (!sleMptoken)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
if (!sleMptoken->isFieldPresent(sfHolderEncryptionKey) ||
|
||||
!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) ||
|
||||
!sleMptoken->isFieldPresent(sfConfidentialBalanceInbox))
|
||||
{
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
if (ctx.tx.isFlag(tfCancelRecovery))
|
||||
{
|
||||
// Nothing to cancel if no recovery is pending.
|
||||
if (!sleMptoken->isFieldPresent(sfRecoveryKey))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
if (ctx.tx[sfHolderEncryptionKey] == (*sleMptoken)[sfHolderEncryptionKey])
|
||||
return tecDUPLICATE;
|
||||
|
||||
// Recovery mode: reject if a recovery is already pending
|
||||
if (ctx.tx.isFlag(tfHolderKeyRecovery) && sleMptoken->isFieldPresent(sfRecoveryKey))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialMPTHolderKeyUpdate::doApply()
|
||||
{
|
||||
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
|
||||
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_));
|
||||
if (!sleMptoken)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE(
|
||||
"xrpl::ConfidentialMPTHolderKeyUpdate::doApply : preclaim already validated the "
|
||||
"MPToken exists");
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
if (ctx_.tx.isFlag(tfCancelRecovery))
|
||||
{
|
||||
// The holder revokes their pending recovery authorization; the
|
||||
// current key and balances are left untouched.
|
||||
sleMptoken->makeFieldAbsent(sfRecoveryKey);
|
||||
view().update(sleMptoken);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
auto const newPubKey = ctx_.tx[sfHolderEncryptionKey];
|
||||
|
||||
if (ctx_.tx.isFlag(tfHolderKeyRotation))
|
||||
{
|
||||
// Replace the key and balances in rotation mode. Rotation proves the
|
||||
// holder still has the old key, so any pending recovery is cancelled.
|
||||
(*sleMptoken)[sfHolderEncryptionKey] = newPubKey;
|
||||
(*sleMptoken)[sfConfidentialBalanceSpending] = ctx_.tx[sfConfidentialBalanceSpending];
|
||||
(*sleMptoken)[sfConfidentialBalanceInbox] = ctx_.tx[sfConfidentialBalanceInbox];
|
||||
sleMptoken->makeFieldAbsent(sfRecoveryKey);
|
||||
incrementConfidentialVersion(*sleMptoken);
|
||||
}
|
||||
else if (ctx_.tx.isFlag(tfHolderKeyRecovery))
|
||||
{
|
||||
// Recovery mode: the holder cannot decrypt their current balances,
|
||||
// so only the pending recovery key is recorded. The balances are
|
||||
// rewritten separately by the issuer via ConfidentialMPTRecoverBalance.
|
||||
(*sleMptoken)[sfRecoveryKey] = newPubKey;
|
||||
}
|
||||
else
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::ConfidentialMPTHolderKeyUpdate::doApply : invalid mode");
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
view().update(sleMptoken);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
void
|
||||
ConfidentialMPTHolderKeyUpdate::visitInvariantEntry(bool, SLE::ConstRef, SLE::ConstRef)
|
||||
{
|
||||
}
|
||||
|
||||
bool
|
||||
ConfidentialMPTHolderKeyUpdate::finalizeInvariants(
|
||||
STTx const&,
|
||||
TER,
|
||||
XRPAmount,
|
||||
ReadView const&,
|
||||
beast::Journal const&)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -5,7 +5,6 @@
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/RippleStateEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
@@ -130,8 +129,9 @@ TrustSet::checkGranularSemantics(
|
||||
std::unordered_set<GranularPermissionType> const& heldGranularPermissions)
|
||||
{
|
||||
auto const saLimitAmount = tx.getFieldAmount(sfLimitAmount);
|
||||
RippleStateEntryR const sleRippleState(
|
||||
tx[sfAccount], saLimitAmount.getIssuer(), saLimitAmount.get<Issue>().currency, view);
|
||||
auto const sleRippleState = view.read(
|
||||
keylet::trustLine(
|
||||
tx[sfAccount], saLimitAmount.getIssuer(), saLimitAmount.get<Issue>().currency));
|
||||
|
||||
// granular permissions are not allowed to create a trustline
|
||||
if (!sleRippleState)
|
||||
@@ -258,7 +258,7 @@ TrustSet::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
bool const bHigh = id > uDstAccountID;
|
||||
// Fetching current state of trust line
|
||||
RippleStateEntryR const sleRippleState(id, uDstAccountID, currency, ctx.view);
|
||||
auto const sleRippleState = ctx.view.read(keylet::trustLine(id, uDstAccountID, currency));
|
||||
std::uint32_t uFlags = sleRippleState ? sleRippleState->getFieldU32(sfFlags) : 0u;
|
||||
// Computing expected trust line state
|
||||
uFlags = computeFreezeFlags(
|
||||
@@ -361,7 +361,8 @@ TrustSet::doApply()
|
||||
STAmount saLimitAllow = saLimitAmount;
|
||||
saLimitAllow.get<Issue>().account = accountID_;
|
||||
|
||||
RippleStateEntryW sleRippleState(accountID_, uDstAccountID, currency, view(), j_);
|
||||
SLE::pointer const sleRippleState =
|
||||
view().peek(keylet::trustLine(accountID_, uDstAccountID, currency));
|
||||
|
||||
if (sleRippleState)
|
||||
{
|
||||
@@ -518,9 +519,9 @@ TrustSet::doApply()
|
||||
bool bReserveIncrease = false;
|
||||
|
||||
auto const currentHighSponsor =
|
||||
getLedgerEntryReserveSponsor(view(), sleRippleState.rawSle(), sfHighSponsor);
|
||||
getLedgerEntryReserveSponsor(view(), sleRippleState, sfHighSponsor);
|
||||
auto const currentLowSponsor =
|
||||
getLedgerEntryReserveSponsor(view(), sleRippleState.rawSle(), sfLowSponsor);
|
||||
getLedgerEntryReserveSponsor(view(), sleRippleState, sfLowSponsor);
|
||||
|
||||
if (bSetAuth)
|
||||
{
|
||||
@@ -551,7 +552,7 @@ TrustSet::doApply()
|
||||
increaseOwnerCount(view(), sleLowAccount, lowSponsor, 1, viewJ);
|
||||
uFlagsOut |= lsfLowReserve;
|
||||
|
||||
addSponsorToLedgerEntry(sleRippleState.mutableRawSle(), lowSponsor, sfLowSponsor);
|
||||
addSponsorToLedgerEntry(sleRippleState, lowSponsor, sfLowSponsor);
|
||||
|
||||
if (!bHigh)
|
||||
bReserveIncrease = true;
|
||||
@@ -563,7 +564,7 @@ TrustSet::doApply()
|
||||
decreaseOwnerCount(view(), sleLowAccount, currentLowSponsor, 1, viewJ);
|
||||
uFlagsOut &= ~lsfLowReserve;
|
||||
|
||||
removeSponsorFromLedgerEntry(sleRippleState.mutableRawSle(), sfLowSponsor);
|
||||
removeSponsorFromLedgerEntry(sleRippleState, sfLowSponsor);
|
||||
}
|
||||
|
||||
if (bHighReserveSet && !bHighReserved)
|
||||
@@ -593,7 +594,7 @@ TrustSet::doApply()
|
||||
increaseOwnerCount(view(), sleHighAccount, highSponsor, 1, viewJ);
|
||||
uFlagsOut |= lsfHighReserve;
|
||||
|
||||
addSponsorToLedgerEntry(sleRippleState.mutableRawSle(), highSponsor, sfHighSponsor);
|
||||
addSponsorToLedgerEntry(sleRippleState, highSponsor, sfHighSponsor);
|
||||
|
||||
if (bHigh)
|
||||
bReserveIncrease = true;
|
||||
@@ -605,7 +606,7 @@ TrustSet::doApply()
|
||||
decreaseOwnerCount(view(), sleHighAccount, currentHighSponsor, 1, viewJ);
|
||||
uFlagsOut &= ~lsfHighReserve;
|
||||
|
||||
removeSponsorFromLedgerEntry(sleRippleState.mutableRawSle(), sfHighSponsor);
|
||||
removeSponsorFromLedgerEntry(sleRippleState, sfHighSponsor);
|
||||
}
|
||||
|
||||
if (uFlagsIn != uFlagsOut)
|
||||
@@ -641,7 +642,7 @@ TrustSet::doApply()
|
||||
}
|
||||
else
|
||||
{
|
||||
sleRippleState.update();
|
||||
view().update(sleRippleState);
|
||||
|
||||
JLOG(j_.trace()) << "Modify ripple line";
|
||||
}
|
||||
@@ -667,7 +668,7 @@ TrustSet::doApply()
|
||||
}
|
||||
else
|
||||
{
|
||||
sleRippleState.update();
|
||||
view().update(sleRippleState);
|
||||
|
||||
JLOG(j_.trace()) << "Modify ripple line";
|
||||
}
|
||||
|
||||
@@ -31,15 +31,6 @@
|
||||
namespace xrpl::test {
|
||||
class AMMClawbackMPT_test : public beast::unit_test::Suite
|
||||
{
|
||||
// SingleAssetVault, LendingProtocol and MPTokensV2 enable the large
|
||||
// Number mantissa.
|
||||
static bool
|
||||
largeMantissa(FeatureBitset const& features)
|
||||
{
|
||||
return features[featureSingleAssetVault] || features[featureLendingProtocol] ||
|
||||
features[featureMPTokensV2];
|
||||
}
|
||||
|
||||
void
|
||||
testInvalidRequest(FeatureBitset features)
|
||||
{
|
||||
@@ -1733,7 +1724,7 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
|
||||
.asString();
|
||||
auto const lpTokenBalance =
|
||||
amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value].asString();
|
||||
if (largeMantissa(features))
|
||||
if (features[featureSingleAssetVault] || features[featureLendingProtocol])
|
||||
{
|
||||
BEAST_EXPECT(lpToken == "1.414213562374011" && lpTokenBalance == "1.4142135623741");
|
||||
}
|
||||
@@ -1750,14 +1741,16 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
|
||||
env(amm::ammClawback(gw, alice, usd, eur, std::nullopt));
|
||||
BEAST_EXPECT(!amm.ammExists());
|
||||
}
|
||||
else if (features[fixAMMv1_3] && largeMantissa(features))
|
||||
else if (
|
||||
features[fixAMMv1_3] &&
|
||||
(features[featureSingleAssetVault] || features[featureLendingProtocol]))
|
||||
{
|
||||
env(amm::ammClawback(gw, alice, usd, eur, std::nullopt));
|
||||
// Without the Rounding feature and with new Number a dust pool
|
||||
// amount remains
|
||||
BEAST_EXPECT(amm.ammExists());
|
||||
}
|
||||
else if (!largeMantissa(features))
|
||||
else if (!features[featureSingleAssetVault] && !features[featureLendingProtocol])
|
||||
{
|
||||
env(amm::ammClawback(gw, alice, usd, eur, std::nullopt), Ter(tecINTERNAL));
|
||||
BEAST_EXPECT(amm.ammExists());
|
||||
@@ -1801,7 +1794,7 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
|
||||
.asString();
|
||||
auto const lpTokenBalance =
|
||||
amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value].asString();
|
||||
if (!largeMantissa(features))
|
||||
if (!features[featureSingleAssetVault] && !features[featureLendingProtocol])
|
||||
{
|
||||
BEAST_EXPECT(lpToken == "1.414213562374011" && lpTokenBalance == "1.414213562374");
|
||||
}
|
||||
@@ -1818,14 +1811,16 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
|
||||
env(amm::ammClawback(gw, alice, usd, eur, std::nullopt));
|
||||
BEAST_EXPECT(!amm.ammExists());
|
||||
}
|
||||
else if (features[fixAMMv1_3] && largeMantissa(features))
|
||||
else if (
|
||||
features[fixAMMv1_3] &&
|
||||
(features[featureSingleAssetVault] || features[featureLendingProtocol]))
|
||||
{
|
||||
// Without the Rounding feature and with new Number a dust pool
|
||||
// amount remains
|
||||
env(amm::ammClawback(gw, alice, usd, eur, std::nullopt));
|
||||
BEAST_EXPECT(amm.ammExists());
|
||||
}
|
||||
else if (!largeMantissa(features))
|
||||
else if (!features[featureSingleAssetVault] && !features[featureLendingProtocol])
|
||||
{
|
||||
env(amm::ammClawback(gw, alice, usd, eur, std::nullopt), Ter(tecINTERNAL));
|
||||
BEAST_EXPECT(amm.ammExists());
|
||||
|
||||
@@ -2857,8 +2857,8 @@ class AMMClawback_test : public beast::unit_test::Suite
|
||||
{
|
||||
// For now, just disable SAV entirely, which locks in the small Number
|
||||
// mantissas
|
||||
FeatureBitset const all = jtx::testableAmendments() - featureSingleAssetVault -
|
||||
featureLendingProtocol - featureMPTokensV2;
|
||||
FeatureBitset const all =
|
||||
jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol;
|
||||
|
||||
testInvalidRequest(all);
|
||||
testInvalidRequest(all - featureMPTokensV2);
|
||||
|
||||
@@ -116,7 +116,7 @@ private:
|
||||
Txflags(tfPartialPayment));
|
||||
|
||||
BEAST_EXPECT(ammCarol.expectBalances(
|
||||
btc(1'001'000'000'374'814), eth(100'000'000'000'000'000), ammCarol.tokens()));
|
||||
btc(1'001'000'000'374'816), eth(100'000'000'000'000'000), ammCarol.tokens()));
|
||||
|
||||
env.require(Balance(bob_, eth(200'100'000'000'000'000)));
|
||||
BEAST_EXPECT(isOffer(env, carol_, btc(49'000'000'000'000), XRP(49)));
|
||||
|
||||
@@ -73,8 +73,7 @@ class AMMExtended_test : public jtx::AMMTest
|
||||
// For now, just disable SAV entirely, which locks in the small Number
|
||||
// mantissas
|
||||
FeatureBitset const all_{
|
||||
testableAmendments() - featureSingleAssetVault - featureLendingProtocol -
|
||||
featureMPTokensV2};
|
||||
testableAmendments() - featureSingleAssetVault - featureLendingProtocol};
|
||||
|
||||
private:
|
||||
void
|
||||
@@ -268,20 +267,39 @@ private:
|
||||
{features});
|
||||
|
||||
// tfPassive -- place the offer without crossing it.
|
||||
testAMM(
|
||||
[&](AMM& ammAlice, Env& env) {
|
||||
// Carol creates a passive offer that could cross AMM.
|
||||
// Carol's offer should stay in the ledger.
|
||||
env(offer(carol_, XRP(100), USD(100), tfPassive));
|
||||
env.close();
|
||||
BEAST_EXPECT(
|
||||
ammAlice.expectBalances(XRP(10'100), STAmount{USD, 10'000}, ammAlice.tokens()));
|
||||
BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100), STAmount{USD, 100}}}}));
|
||||
},
|
||||
{{XRP(10'100), USD(10'000)}},
|
||||
0,
|
||||
std::nullopt,
|
||||
{features});
|
||||
if (features[featureMPTokensV2])
|
||||
{
|
||||
Env env{*this, features};
|
||||
fund(env, gw_, {alice_, carol_}, XRP(30'000'000), {USD(30'000'000)});
|
||||
|
||||
AMM const ammAlice(env, alice_, XRP(10'100'000), USD(10'000'000));
|
||||
|
||||
// Scale the exact-quality fixture up so the visual relationship
|
||||
// stays clear: the passive CLOB offer has the same 1:1 quality as
|
||||
// the generated AMM offer, so it should not cross.
|
||||
env(offer(carol_, XRP(100'000), USD(100'000), tfPassive));
|
||||
env.close();
|
||||
BEAST_EXPECT(
|
||||
ammAlice.expectBalances(XRP(10'100'000), USD(10'000'000), ammAlice.tokens()));
|
||||
BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100'000), USD(100'000)}}}));
|
||||
}
|
||||
else
|
||||
{
|
||||
testAMM(
|
||||
[&](AMM& ammAlice, Env& env) {
|
||||
// Carol creates a passive offer that could cross AMM.
|
||||
// Carol's offer should stay in the ledger.
|
||||
env(offer(carol_, XRP(100), USD(100), tfPassive));
|
||||
env.close();
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
XRP(10'100), STAmount{USD, 10'000}, ammAlice.tokens()));
|
||||
BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100), STAmount{USD, 100}}}}));
|
||||
},
|
||||
{{XRP(10'100), USD(10'000)}},
|
||||
0,
|
||||
std::nullopt,
|
||||
{features});
|
||||
}
|
||||
|
||||
// tfPassive -- cross only offers of better quality.
|
||||
testAMM(
|
||||
@@ -1432,6 +1450,7 @@ private:
|
||||
testRmFundedOffer(all_ - fixAMMv1_1 - fixAMMv1_3);
|
||||
testEnforceNoRipple(all_);
|
||||
testFillModes(all_);
|
||||
testFillModes(all_ - featureMPTokensV2);
|
||||
testOfferCrossWithXRP(all_);
|
||||
testOfferCrossWithLimitOverride(all_);
|
||||
testCurrencyConversionEntire(all_);
|
||||
|
||||
@@ -1451,14 +1451,12 @@ private:
|
||||
auto carolXRP = env.balance(carol_, XRP);
|
||||
auto carolMPT = env.balance(carol_, MPT(ammAlice[1]));
|
||||
|
||||
// The exact deposit is 201; rounding in the pool's favor at
|
||||
// the large mantissa takes one more unit.
|
||||
ammAlice.deposit(carol_, 100'000, MPT(ammAlice[1])(205));
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
XRP(10'000), MPT(ammAlice[1])(10'202), IOUAmount{10'100'000, 0}));
|
||||
XRP(10'000), MPT(ammAlice[1])(10'201), IOUAmount{10'100'000, 0}));
|
||||
|
||||
env.require(Balance(carol_, carolXRP - drops(baseFee)));
|
||||
env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(202)));
|
||||
env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(201)));
|
||||
},
|
||||
{{XRP(10'000), gAmmmpt(10'000)}});
|
||||
|
||||
@@ -1469,13 +1467,11 @@ private:
|
||||
auto carolXRP = env.balance(carol_, XRP);
|
||||
auto carolMPT = env.balance(carol_, MPT(ammAlice[1]));
|
||||
|
||||
// The exact deposit is 201 XRP; rounding in the pool's favor
|
||||
// at the large mantissa takes one more drop.
|
||||
ammAlice.deposit(carol_, 100'000, XRP(205));
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
XRPAmount{10'201'000'001}, MPT(ammAlice[1])(10'000), IOUAmount{10'100'000, 0}));
|
||||
XRP(10'201), MPT(ammAlice[1])(10'000), IOUAmount{10'100'000, 0}));
|
||||
|
||||
env.require(Balance(carol_, carolXRP - XRPAmount{201'000'001} - drops(baseFee)));
|
||||
env.require(Balance(carol_, carolXRP - XRP(201) - drops(baseFee)));
|
||||
env.require(Balance(carol_, carolMPT));
|
||||
},
|
||||
{{XRP(10'000), gAmmmpt(10'000)}});
|
||||
@@ -1555,10 +1551,10 @@ private:
|
||||
STAmount{ammAlice.lptIssue(), 2004, -6});
|
||||
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
XRP(10'000), MPT(ammAlice[1])(10'080), IOUAmount{10'039'920'3184089, -7}));
|
||||
XRP(10'000), MPT(ammAlice[1])(10'081), IOUAmount{10'039'920'31840891, -8}));
|
||||
|
||||
env.require(Balance(carol_, carolXRP - drops(baseFee)));
|
||||
env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(80)));
|
||||
env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(81)));
|
||||
},
|
||||
{{XRP(10'000), gAmmmpt(10'000)}});
|
||||
|
||||
@@ -1577,10 +1573,10 @@ private:
|
||||
STAmount{ammAlice.lptIssue(), 2004, -6});
|
||||
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
XRP(10'000), MPT(ammAlice[1])(10'080), IOUAmount{10'039'920'3184089, -7}));
|
||||
XRP(10'000), MPT(ammAlice[1])(10'081), IOUAmount{10'039'920'31840891, -8}));
|
||||
|
||||
env.require(Balance(carol_, carolXRP - drops(baseFee)));
|
||||
env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(80)));
|
||||
env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(81)));
|
||||
},
|
||||
{{XRP(10'000), gAmmmpt(10'000)}});
|
||||
|
||||
@@ -1801,9 +1797,9 @@ private:
|
||||
|
||||
ammAlice.deposit(gw_, MPT(ammAlice[0])(1000));
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
MPT(ammAlice[0])(1'011'000),
|
||||
MPT(ammAlice[0])(1'010'999),
|
||||
MPT(ammAlice[1])(1'010'000),
|
||||
IOUAmount{1'010'499'876298854, -9}));
|
||||
IOUAmount{1'010'499'376546071, -9}));
|
||||
},
|
||||
{{gAmmmpt(10'000), gAmmmpt(10'000)}});
|
||||
|
||||
@@ -2216,7 +2212,7 @@ private:
|
||||
if (env.enabled(fixAMMv1_3))
|
||||
{
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
MPT(ammAlice[0])(1), STAmount{USD, 1, -12}, IOUAmount{1, -9}));
|
||||
MPT(ammAlice[0])(1), STAmount{USD, 1, -11}, IOUAmount{1, -8}));
|
||||
}
|
||||
},
|
||||
{{gAmmmpt(10'000'000'000), USD(10'000)}},
|
||||
@@ -2545,12 +2541,11 @@ private:
|
||||
// XRP amount to withdraw is 0
|
||||
ammAlice.withdraw(
|
||||
alice_, IOUAmount{1, -5}, std::nullopt, std::nullopt, Ter(tecAMM_FAILED));
|
||||
// Calculated tokens to withdraw are 0: 1e-16 of 10,000 USD is
|
||||
// below the large Number mantissa's precision
|
||||
// Calculated tokens to withdraw are 0
|
||||
ammAlice.withdraw(
|
||||
alice_,
|
||||
std::nullopt,
|
||||
STAmount{USD, 1, -16},
|
||||
STAmount{USD, 1, -11},
|
||||
std::nullopt,
|
||||
Ter(tecAMM_INVALID_TOKENS));
|
||||
ammAlice.deposit(carol_, STAmount{USD, 1, -10});
|
||||
@@ -2712,7 +2707,7 @@ private:
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
XRPAmount(9000'000001),
|
||||
MPT(ammAlice[1])(10'000),
|
||||
IOUAmount{9'486'832'980505138, -9}));
|
||||
IOUAmount{9'486'832'98050514, -8}));
|
||||
},
|
||||
{{XRP(10'000), gAmmmpt(10'000)}});
|
||||
testAMM(
|
||||
@@ -2720,7 +2715,7 @@ private:
|
||||
// single withdraw MPT from XRP/MPT
|
||||
ammAlice.withdraw(alice_, MPT(ammAlice[1])(1'000));
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
XRP(10000), MPT(ammAlice[1])(9001), IOUAmount{9'486'832'980505138, -9}));
|
||||
XRP(10000), MPT(ammAlice[1])(9001), IOUAmount{9'486'832'98050514, -8}));
|
||||
},
|
||||
{{XRP(10'000), gAmmmpt(10'000)}});
|
||||
testAMM(
|
||||
@@ -2728,7 +2723,9 @@ private:
|
||||
// single withdraw IOU from IOU/MPT
|
||||
ammAlice.withdraw(alice_, USD(1'000));
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
USD(9'000), MPT(ammAlice[1])(10'000), IOUAmount{9486'832980505138, -12}));
|
||||
STAmount{USD, UINT64_C(9000'000000000004), -12},
|
||||
MPT(ammAlice[1])(10'000),
|
||||
IOUAmount{9486'83298050514, -11}));
|
||||
},
|
||||
{{USD(10'000), gAmmmpt(10'000)}});
|
||||
testAMM(
|
||||
@@ -2736,7 +2733,7 @@ private:
|
||||
// single withdraw MPT from IOU/MPT
|
||||
ammAlice.withdraw(alice_, MPT(ammAlice[1])(1'000));
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
USD(10'000), MPT(ammAlice[1])(9001), IOUAmount{9486'832980505138, -12}));
|
||||
USD(10'000), MPT(ammAlice[1])(9001), IOUAmount{9486'83298050514, -11}));
|
||||
},
|
||||
{{USD(10'000), gAmmmpt(10'000)}});
|
||||
testAMM(
|
||||
@@ -2746,7 +2743,7 @@ private:
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
MPT(ammAlice[0])(9001),
|
||||
MPT(ammAlice[1])(10'000),
|
||||
IOUAmount{9486'832980505138, -12}));
|
||||
IOUAmount{9486'83298050514, -11}));
|
||||
},
|
||||
{{gAmmmpt(10'000), gAmmmpt(10'000)}});
|
||||
|
||||
@@ -3027,7 +3024,7 @@ private:
|
||||
ammAlice.deposit(carol_, 1'000);
|
||||
ammAlice.withdrawAll(carol_, USD(0));
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
STAmount{USD, UINT64_C(9'090'909090909091), -12},
|
||||
STAmount{USD, UINT64_C(9'090'909090909092), -12},
|
||||
MPT(ammAlice[1])(11'000),
|
||||
IOUAmount{10'000}));
|
||||
},
|
||||
@@ -3252,7 +3249,7 @@ private:
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
MPT(ammAlice[0])(10'000'000'000),
|
||||
STAmount{USD, 1, -11},
|
||||
IOUAmount{316227766, -9}));
|
||||
IOUAmount{316227765, -9}));
|
||||
},
|
||||
{{gAmmmpt(10'000'000'000), USD(10'000)}});
|
||||
// XRP
|
||||
@@ -5963,7 +5960,7 @@ private:
|
||||
if (!features[fixAMMv1_3])
|
||||
{
|
||||
BEAST_EXPECT(tokensFee == IOUAmount(98'019'80198019, -8));
|
||||
BEAST_EXPECT(tokensNoFee == IOUAmount(98'495'13933557, -8));
|
||||
BEAST_EXPECT(tokensNoFee == IOUAmount(98'495'13933556, -8));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -84,8 +84,7 @@ private:
|
||||
{
|
||||
// For now, just disable SAV entirely, which locks in the small Number
|
||||
// mantissas
|
||||
return jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol -
|
||||
featureMPTokensV2;
|
||||
return jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol;
|
||||
}
|
||||
|
||||
// Seed from the local testableAmendments() which strips SAV and Lending.
|
||||
@@ -3811,7 +3810,7 @@ private:
|
||||
BEAST_EXPECT(amm.expectBalances(XRP(1'000), USD(500), amm.tokens()));
|
||||
BEAST_EXPECT(expectOffers(env, carol_, 1, {{Amounts{XRP(100), USD(55)}}}));
|
||||
}
|
||||
else
|
||||
else if (!features[featureMPTokensV2])
|
||||
{
|
||||
BEAST_EXPECT(amm.expectBalances(
|
||||
XRPAmount(909'090'909),
|
||||
@@ -3826,6 +3825,30 @@ private:
|
||||
env.balance(carol_, USD) ==
|
||||
STAmount(USD, UINT64_C(29'949'94999999494), -11));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Post-amendment the transfer fee is taken into account
|
||||
// when calculating the limit out based on limitQuality.
|
||||
// This increases the limitQuality and decreases
|
||||
// the limit out. Consequently, AMM offer size is decreased,
|
||||
// and the quality is increased, matching the overall
|
||||
// quality.
|
||||
// AMM offer ~50USD/91XRP
|
||||
BEAST_EXPECT(amm.expectBalances(
|
||||
XRPAmount(909'090'910),
|
||||
STAmount{USD, UINT64_C(549'99999945), -8},
|
||||
amm.tokens()));
|
||||
// Offer ~91XRP/50USD
|
||||
BEAST_EXPECT(expectOffers(
|
||||
env,
|
||||
carol_,
|
||||
1,
|
||||
{{Amounts{XRPAmount{9'090'910}, STAmount{USD, 5'0000005, -7}}}}));
|
||||
// Carol pays 0.1% fee on ~50USD =~ 0.05USD
|
||||
BEAST_EXPECT(
|
||||
env.balance(carol_, USD) ==
|
||||
STAmount(USD, UINT64_C(29'949'95000060055), -11));
|
||||
}
|
||||
},
|
||||
{{XRP(1'000), USD(500)}},
|
||||
0,
|
||||
@@ -6475,7 +6498,7 @@ private:
|
||||
BEAST_EXPECT(expectOffers(env, bob_, 1, {{Amounts{USD(1), XRPAmount(500)}}}));
|
||||
BEAST_EXPECT(expectOffers(env, carol_, 1, {{Amounts{XRP(100), USD(55)}}}));
|
||||
}
|
||||
else
|
||||
else if (!features[featureMPTokensV2])
|
||||
{
|
||||
BEAST_EXPECT(amm.expectBalances(
|
||||
XRPAmount(909'090'909),
|
||||
@@ -6488,6 +6511,19 @@ private:
|
||||
{{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}}));
|
||||
BEAST_EXPECT(expectOffers(env, bob_, 1, {{Amounts{USD(1), XRPAmount(500)}}}));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(amm.expectBalances(
|
||||
XRPAmount(909'090'910),
|
||||
STAmount{USD, UINT64_C(549'99999945), -8},
|
||||
amm.tokens()));
|
||||
BEAST_EXPECT(expectOffers(
|
||||
env,
|
||||
carol_,
|
||||
1,
|
||||
{{Amounts{XRPAmount{9'090'910}, STAmount{USD, 5'0000005, -7}}}}));
|
||||
BEAST_EXPECT(expectOffers(env, bob_, 1, {{Amounts{USD(1), XRPAmount(500)}}}));
|
||||
}
|
||||
}
|
||||
|
||||
// There is no blocking offer, the same AMM liquidity is consumed
|
||||
@@ -6499,10 +6535,30 @@ private:
|
||||
AMM const amm(env, alice_, XRP(1'000), USD(500));
|
||||
env(offer(carol_, XRP(100), USD(55)));
|
||||
env.close();
|
||||
BEAST_EXPECT(amm.expectBalances(
|
||||
XRPAmount(909'090'909), STAmount{USD, UINT64_C(550'000000055), -9}, amm.tokens()));
|
||||
BEAST_EXPECT(expectOffers(
|
||||
env, carol_, 1, {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}}));
|
||||
if (!features[featureMPTokensV2])
|
||||
{
|
||||
BEAST_EXPECT(amm.expectBalances(
|
||||
XRPAmount(909'090'909),
|
||||
STAmount{USD, UINT64_C(550'000000055), -9},
|
||||
amm.tokens()));
|
||||
BEAST_EXPECT(expectOffers(
|
||||
env,
|
||||
carol_,
|
||||
1,
|
||||
{{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}}));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(amm.expectBalances(
|
||||
XRPAmount(909'090'910),
|
||||
STAmount{USD, UINT64_C(549'99999945), -8},
|
||||
amm.tokens()));
|
||||
BEAST_EXPECT(expectOffers(
|
||||
env,
|
||||
carol_,
|
||||
1,
|
||||
{{Amounts{XRPAmount{9'090'910}, STAmount{USD, 5'0000005, -7}}}}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7392,6 +7448,8 @@ private:
|
||||
.err = Ter(tecAMM_BALANCE)});
|
||||
};
|
||||
|
||||
// Bound holds regardless of the deposit-side fix amendment.
|
||||
test(all - featureMPTokensV2);
|
||||
test(all);
|
||||
}
|
||||
|
||||
@@ -7423,6 +7481,7 @@ private:
|
||||
testFlags();
|
||||
testRippling();
|
||||
testAMMAndCLOB(all);
|
||||
testAMMAndCLOB(all - featureMPTokensV2);
|
||||
testAMMAndCLOB(all - fixAMMv1_1 - fixAMMv1_3);
|
||||
testTradingFee(all);
|
||||
testTradingFee(all - fixAMMv1_3);
|
||||
@@ -7442,8 +7501,10 @@ private:
|
||||
testOverflowOffer(all - fixAMMv1_1 - fixAMMv1_3);
|
||||
testSwapRounding();
|
||||
testFixChangeSpotPriceQuality(all);
|
||||
testFixChangeSpotPriceQuality(all - featureMPTokensV2);
|
||||
testFixChangeSpotPriceQuality(all - fixAMMv1_1 - fixAMMv1_3);
|
||||
testFixAMMOfferBlockedByLOB(all);
|
||||
testFixAMMOfferBlockedByLOB(all - featureMPTokensV2);
|
||||
testFixAMMOfferBlockedByLOB(all - fixAMMv1_1 - fixAMMv1_3);
|
||||
testLPTokenBalance(all);
|
||||
testLPTokenBalance(all - fixAMMv1_3);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/ConfidentialTransfer.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/mpt.h>
|
||||
|
||||
#include <xrpl/basics/Buffer.h>
|
||||
@@ -21,7 +20,6 @@
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -2456,703 +2454,6 @@ class ConfidentialMPTKeyRotation_test : public ConfidentialTransferTestBase
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
testConfidentialMPTHolderKeyUpdatePreflight(FeatureBitset features)
|
||||
{
|
||||
testcase("ConfidentialMPTHolderKeyUpdate preflight");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
|
||||
// Both amendments are required: ConfidentialMPTKeyRotation and ConfidentialTransfer.
|
||||
if (!features[featureConfidentialMPTKeyRotation] || !features[featureConfidentialTransfer])
|
||||
{
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer});
|
||||
mptAlice.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = gMakeZeroBuffer(kEcPubKeyLength),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
.err = temDISABLED,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ConfidentialEnv ct{env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
|
||||
|
||||
Account const bobNewKey("bobNewKey");
|
||||
ct.mpt.generateKeyPair(bobNewKey);
|
||||
|
||||
// The flag contains a value outside the recognized mode bits. This is
|
||||
// rejected by the flags-mask check
|
||||
for (auto const flags : {0x00080000u, 0x00080000u | tfHolderKeyRecovery, 0x00100000u})
|
||||
{
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = flags,
|
||||
.err = temINVALID_FLAG,
|
||||
});
|
||||
}
|
||||
|
||||
// Exactly one of Rotation, Recovery, and Cancel must be set.
|
||||
for (auto const flags :
|
||||
{0u,
|
||||
tfHolderKeyRotation | tfHolderKeyRecovery,
|
||||
tfHolderKeyRotation | tfCancelRecovery,
|
||||
tfHolderKeyRecovery | tfCancelRecovery,
|
||||
tfHolderKeyRotation | tfHolderKeyRecovery | tfCancelRecovery})
|
||||
{
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = flags,
|
||||
.err = temINVALID_FLAG,
|
||||
});
|
||||
}
|
||||
|
||||
// The issuer cannot rotate or recover a confidential balance it cannot hold.
|
||||
Account const aliceNewKey("aliceNewKey");
|
||||
ct.mpt.generateKeyPair(aliceNewKey);
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = alice,
|
||||
.holderPubKey = ct.mpt.getPubKey(aliceNewKey),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
.err = temMALFORMED,
|
||||
});
|
||||
|
||||
// HolderEncryptionKey one byte short of the required length.
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = gMakeZeroBuffer(kEcPubKeyLength - 1),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
.err = temMALFORMED,
|
||||
});
|
||||
|
||||
// HolderEncryptionKey the correct length, but not a well-formed
|
||||
// compressed secp256k1 point.
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = gMakeZeroBuffer(kEcPubKeyLength),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
.err = temMALFORMED,
|
||||
});
|
||||
|
||||
// HolderEncryptionKey is entirely absent (as opposed to present with
|
||||
// the wrong length or format).
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
.err = temMALFORMED,
|
||||
});
|
||||
|
||||
std::optional<Buffer> const cipher = getTrivialCiphertext();
|
||||
std::optional<Buffer> const none;
|
||||
|
||||
// Rotation mode requires both the spending and inbox ciphertexts;
|
||||
for (auto const& [spending, inbox] :
|
||||
{std::pair{none, none}, std::pair{cipher, none}, std::pair{none, cipher}})
|
||||
{
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.spendingCiphertext = spending,
|
||||
.inboxCiphertext = inbox,
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRotation,
|
||||
.err = temMALFORMED,
|
||||
});
|
||||
}
|
||||
|
||||
// Recovery and Cancel modes must not include either.
|
||||
for (auto const& [spending, inbox] :
|
||||
{std::pair{cipher, none}, std::pair{none, cipher}, std::pair{cipher, cipher}})
|
||||
{
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.spendingCiphertext = spending,
|
||||
.inboxCiphertext = inbox,
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
.err = temMALFORMED,
|
||||
});
|
||||
}
|
||||
|
||||
for (auto const& [spending, inbox] :
|
||||
{std::pair{cipher, none}, std::pair{none, cipher}, std::pair{cipher, cipher}})
|
||||
{
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.spendingCiphertext = spending,
|
||||
.inboxCiphertext = inbox,
|
||||
.flags = tfCancelRecovery,
|
||||
.err = temMALFORMED,
|
||||
});
|
||||
}
|
||||
|
||||
// Spending ciphertext has the wrong length.
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.spendingCiphertext = gMakeZeroBuffer(kEcGamalEncryptedTotalLength - 1),
|
||||
.inboxCiphertext = getTrivialCiphertext(),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRotation,
|
||||
.err = temBAD_CIPHERTEXT,
|
||||
});
|
||||
|
||||
// Inbox ciphertext has the wrong length.
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.spendingCiphertext = getTrivialCiphertext(),
|
||||
.inboxCiphertext = gMakeZeroBuffer(kEcGamalEncryptedTotalLength - 1),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRotation,
|
||||
.err = temBAD_CIPHERTEXT,
|
||||
});
|
||||
|
||||
// Spending ciphertext has the correct length, but is not a
|
||||
// well-formed EC ElGamal ciphertext.
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.spendingCiphertext = getBadCiphertext(),
|
||||
.inboxCiphertext = getTrivialCiphertext(),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRotation,
|
||||
.err = temBAD_CIPHERTEXT,
|
||||
});
|
||||
|
||||
// Inbox ciphertext has the correct length, but is not a well-formed
|
||||
// EC ElGamal ciphertext.
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.spendingCiphertext = getTrivialCiphertext(),
|
||||
.inboxCiphertext = getBadCiphertext(),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRotation,
|
||||
.err = temBAD_CIPHERTEXT,
|
||||
});
|
||||
|
||||
// Rotation/Recovery mode requires a ZKProof.
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
.err = temMALFORMED,
|
||||
});
|
||||
|
||||
// Cancel mode must not include HolderEncryptionKey.
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.flags = tfCancelRecovery,
|
||||
.err = temMALFORMED,
|
||||
});
|
||||
|
||||
// Cancel mode must not include a ZKProof.
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfCancelRecovery,
|
||||
.err = temMALFORMED,
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
testConfidentialMPTHolderKeyUpdatePreclaim(FeatureBitset features)
|
||||
{
|
||||
testcase("ConfidentialMPTHolderKeyUpdate preclaim");
|
||||
using namespace test::jtx;
|
||||
|
||||
// The issuance does not exist.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
|
||||
});
|
||||
// Destroy the issuance to test issuance not found.
|
||||
mptAlice.destroy();
|
||||
|
||||
mptAlice.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.flags = tfCancelRecovery,
|
||||
.err = tecOBJECT_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
// The issuance has not enabled confidential balances. The holder is
|
||||
// authorized so this reaches the confidential-balance-flag check
|
||||
// rather than failing earlier on a missing MPToken.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer});
|
||||
mptAlice.authorize({.account = bob});
|
||||
|
||||
mptAlice.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.flags = tfCancelRecovery,
|
||||
.err = tecNO_PERMISSION,
|
||||
});
|
||||
}
|
||||
|
||||
// carol exists as an account but was never authorized to hold this
|
||||
// issuance, so she has no MPToken for it at all.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const carol("carol");
|
||||
MPTTester mptAlice(env, alice);
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
|
||||
});
|
||||
env.fund(XRP(1000), carol);
|
||||
env.close();
|
||||
|
||||
Account const carolNewKey("carolNewKey");
|
||||
mptAlice.generateKeyPair(carolNewKey);
|
||||
|
||||
mptAlice.holderKeyUpdate({
|
||||
.account = carol,
|
||||
.holderPubKey = mptAlice.getPubKey(carolNewKey),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
.err = tecOBJECT_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
// The holder has an MPToken but no confidential state yet - never
|
||||
// registered a key or converted anything.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
|
||||
});
|
||||
mptAlice.authorize({.account = bob});
|
||||
mptAlice.pay(alice, bob, 100);
|
||||
|
||||
Account const bobNewKey("bobNewKey");
|
||||
mptAlice.generateKeyPair(bobNewKey);
|
||||
|
||||
mptAlice.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = mptAlice.getPubKey(bobNewKey),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
.err = tecNO_PERMISSION,
|
||||
});
|
||||
}
|
||||
|
||||
// Submitting the holder's own current key as the "new" key is a no-op.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
ConfidentialEnv ct{
|
||||
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
|
||||
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bob),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
.err = tecDUPLICATE,
|
||||
});
|
||||
}
|
||||
|
||||
// A second Recovery-mode transaction must not silently overwrite an
|
||||
// already-pending RecoveryKey.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
ConfidentialEnv ct{
|
||||
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
|
||||
|
||||
Account const bobRecoveryKey("bobRecoveryKey");
|
||||
ct.mpt.generateKeyPair(bobRecoveryKey);
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobRecoveryKey),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
});
|
||||
|
||||
Account const bobRecoveryKey2("bobRecoveryKey2");
|
||||
ct.mpt.generateKeyPair(bobRecoveryKey2);
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobRecoveryKey2),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
.err = tecNO_PERMISSION,
|
||||
});
|
||||
}
|
||||
|
||||
// bob never submitted a Recovery-mode transaction, so there is no
|
||||
// sfRecoveryKey to cancel.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
ConfidentialEnv ct{
|
||||
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
|
||||
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.flags = tfCancelRecovery,
|
||||
.err = tecNO_PERMISSION,
|
||||
});
|
||||
}
|
||||
|
||||
// Runs rotation, recovery, and cancel in sequence and expects each to
|
||||
// succeed.
|
||||
auto const allModesSucceed = [&](ConfidentialEnv& ct, Account const& bob) {
|
||||
Account const bobNewKey("bobNewKey");
|
||||
ct.mpt.generateKeyPair(bobNewKey);
|
||||
|
||||
auto const reEnc = reencryptHolderBalances(ct.mpt, bob, bob, bobNewKey);
|
||||
BEAST_EXPECT(reEnc.has_value());
|
||||
if (!reEnc)
|
||||
return;
|
||||
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.spendingCiphertext = reEnc->first,
|
||||
.inboxCiphertext = reEnc->second,
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRotation,
|
||||
});
|
||||
|
||||
Account const bobRecoveryKey("bobRecoveryKey");
|
||||
ct.mpt.generateKeyPair(bobRecoveryKey);
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobRecoveryKey),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
});
|
||||
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.flags = tfCancelRecovery,
|
||||
});
|
||||
};
|
||||
|
||||
// Individual (per-holder) lock.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
ConfidentialEnv ct{
|
||||
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
|
||||
ct.mpt.set({.account = alice, .holder = bob, .flags = tfMPTLock});
|
||||
|
||||
allModesSucceed(ct, bob);
|
||||
}
|
||||
|
||||
// Global (issuance-wide) lock.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
ConfidentialEnv ct{
|
||||
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
|
||||
ct.mpt.set({.account = alice, .flags = tfMPTLock});
|
||||
|
||||
allModesSucceed(ct, bob);
|
||||
}
|
||||
|
||||
// A stale mirror epoch does not block rotation, recovery, or cancel:
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
ConfidentialEnv ct{
|
||||
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
|
||||
|
||||
// Rotate the issuer's key without refreshing bob's mirror,
|
||||
// leaving bob's mirror epoch stale relative to the issuance.
|
||||
Account const aliceNewIssuerKey("aliceNewIssuerKey");
|
||||
ct.mpt.generateKeyPair(aliceNewIssuerKey);
|
||||
ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(aliceNewIssuerKey)});
|
||||
BEAST_EXPECT(ct.mpt.checkKeyEpochs(1u, std::nullopt));
|
||||
BEAST_EXPECT(ct.mpt.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
|
||||
|
||||
allModesSucceed(ct, bob);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testConfidentialMPTHolderKeyUpdateDoApply(FeatureBitset features)
|
||||
{
|
||||
testcase("ConfidentialMPTHolderKeyUpdate doApply");
|
||||
using namespace test::jtx;
|
||||
|
||||
// Rotation mode updates the key and re-encrypted balances, bumps the
|
||||
// version, and clears any pending recovery.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
ConfidentialEnv ct{
|
||||
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
|
||||
|
||||
auto const prevVersion = ct.mpt.getMPTokenVersion(bob);
|
||||
|
||||
Account const bobNewKey("bobNewKey");
|
||||
ct.mpt.generateKeyPair(bobNewKey);
|
||||
|
||||
auto const reEnc = reencryptHolderBalances(ct.mpt, bob, bob, bobNewKey);
|
||||
BEAST_EXPECT(reEnc.has_value());
|
||||
if (!reEnc)
|
||||
return;
|
||||
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.spendingCiphertext = reEnc->first,
|
||||
.inboxCiphertext = reEnc->second,
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRotation,
|
||||
});
|
||||
|
||||
auto const sleMptoken = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
|
||||
if (!BEAST_EXPECT(sleMptoken))
|
||||
return;
|
||||
|
||||
auto const newPubKey = ct.mpt.getPubKey(bobNewKey);
|
||||
BEAST_EXPECT(
|
||||
newPubKey && strHex((*sleMptoken)[sfHolderEncryptionKey]) == strHex(*newPubKey));
|
||||
BEAST_EXPECT(!sleMptoken->isFieldPresent(sfRecoveryKey));
|
||||
BEAST_EXPECT(ct.mpt.getMPTokenVersion(bob) == prevVersion + 1);
|
||||
|
||||
// The rotated balances must still decrypt to the same amounts,
|
||||
// but now only under the NEW private key.
|
||||
auto const spendingCt =
|
||||
ct.mpt.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending);
|
||||
auto const inboxCt = ct.mpt.getEncryptedBalance(bob, MPTTester::holderEncryptedInbox);
|
||||
BEAST_EXPECT(spendingCt.has_value());
|
||||
BEAST_EXPECT(inboxCt.has_value());
|
||||
if (!spendingCt || !inboxCt)
|
||||
return;
|
||||
|
||||
auto const spendingAmt = ct.mpt.decryptAmount(bobNewKey, *spendingCt);
|
||||
auto const inboxAmt = ct.mpt.decryptAmount(bobNewKey, *inboxCt);
|
||||
BEAST_EXPECT(spendingAmt && *spendingAmt == 40);
|
||||
BEAST_EXPECT(inboxAmt && *inboxAmt == 0);
|
||||
}
|
||||
|
||||
// Rotation mode re-encrypts both balances correctly when spending and
|
||||
// inbox are both non-zero and differ from each other.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const carol("carol");
|
||||
ConfidentialEnv ct{
|
||||
env,
|
||||
alice,
|
||||
{{.account = bob, .payAmount = 100, .convertAmount = 40},
|
||||
{.account = carol, .payAmount = 100, .convertAmount = 50}}};
|
||||
|
||||
// carol sends 15 into bob's inbox, which is not merged into
|
||||
// spending, leaving bob with spending=40, inbox=15.
|
||||
ct.mpt.send({.account = carol, .dest = bob, .amt = 15});
|
||||
|
||||
Account const bobNewKey("bobNewKey");
|
||||
ct.mpt.generateKeyPair(bobNewKey);
|
||||
|
||||
auto const reEnc = reencryptHolderBalances(ct.mpt, bob, bob, bobNewKey);
|
||||
BEAST_EXPECT(reEnc.has_value());
|
||||
if (!reEnc)
|
||||
return;
|
||||
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.spendingCiphertext = reEnc->first,
|
||||
.inboxCiphertext = reEnc->second,
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRotation,
|
||||
});
|
||||
|
||||
auto const spendingCt =
|
||||
ct.mpt.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending);
|
||||
auto const inboxCt = ct.mpt.getEncryptedBalance(bob, MPTTester::holderEncryptedInbox);
|
||||
BEAST_EXPECT(spendingCt.has_value());
|
||||
BEAST_EXPECT(inboxCt.has_value());
|
||||
if (!spendingCt || !inboxCt)
|
||||
return;
|
||||
|
||||
auto const spendingAmt = ct.mpt.decryptAmount(bobNewKey, *spendingCt);
|
||||
auto const inboxAmt = ct.mpt.decryptAmount(bobNewKey, *inboxCt);
|
||||
BEAST_EXPECT(spendingAmt && *spendingAmt == 40);
|
||||
BEAST_EXPECT(inboxAmt && *inboxAmt == 15);
|
||||
}
|
||||
|
||||
// Recovery mode only records the pending recovery key; the current
|
||||
// key, balances, and version are untouched.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
ConfidentialEnv ct{
|
||||
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
|
||||
|
||||
auto const prevVersion = ct.mpt.getMPTokenVersion(bob);
|
||||
auto const prevSpending =
|
||||
ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
|
||||
auto const prevInbox = ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox);
|
||||
auto const prevKey = ct.mpt.getPubKey(bob);
|
||||
|
||||
Account const bobRecoveryKey("bobRecoveryKey");
|
||||
ct.mpt.generateKeyPair(bobRecoveryKey);
|
||||
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobRecoveryKey),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
});
|
||||
|
||||
auto const sleMptoken = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
|
||||
if (!BEAST_EXPECT(sleMptoken))
|
||||
return;
|
||||
|
||||
auto const recoveryKey = ct.mpt.getPubKey(bobRecoveryKey);
|
||||
BEAST_EXPECT(
|
||||
sleMptoken->isFieldPresent(sfRecoveryKey) && recoveryKey &&
|
||||
strHex((*sleMptoken)[sfRecoveryKey]) == strHex(*recoveryKey));
|
||||
|
||||
BEAST_EXPECT(
|
||||
prevKey && strHex((*sleMptoken)[sfHolderEncryptionKey]) == strHex(*prevKey));
|
||||
BEAST_EXPECT(ct.mpt.getMPTokenVersion(bob) == prevVersion);
|
||||
BEAST_EXPECT(
|
||||
ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) ==
|
||||
prevSpending);
|
||||
BEAST_EXPECT(
|
||||
ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox) == prevInbox);
|
||||
}
|
||||
|
||||
// Cancel mode clears the pending recovery key only; the current key,
|
||||
// balances, and version are untouched.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
ConfidentialEnv ct{
|
||||
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
|
||||
|
||||
Account const bobRecoveryKey("bobRecoveryKey");
|
||||
ct.mpt.generateKeyPair(bobRecoveryKey);
|
||||
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobRecoveryKey),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
});
|
||||
|
||||
auto const prevVersion = ct.mpt.getMPTokenVersion(bob);
|
||||
auto const prevSpending =
|
||||
ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
|
||||
auto const prevInbox = ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox);
|
||||
auto const prevKey = ct.mpt.getPubKey(bob);
|
||||
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.flags = tfCancelRecovery,
|
||||
});
|
||||
|
||||
auto const sleMptoken = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
|
||||
if (!BEAST_EXPECT(sleMptoken))
|
||||
return;
|
||||
|
||||
BEAST_EXPECT(!sleMptoken->isFieldPresent(sfRecoveryKey));
|
||||
BEAST_EXPECT(
|
||||
prevKey && strHex((*sleMptoken)[sfHolderEncryptionKey]) == strHex(*prevKey));
|
||||
BEAST_EXPECT(ct.mpt.getMPTokenVersion(bob) == prevVersion);
|
||||
BEAST_EXPECT(
|
||||
ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) ==
|
||||
prevSpending);
|
||||
BEAST_EXPECT(
|
||||
ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox) == prevInbox);
|
||||
}
|
||||
|
||||
// Rotation mode clears a pending recovery key.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
ConfidentialEnv ct{
|
||||
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
|
||||
|
||||
Account const bobRecoveryKey("bobRecoveryKey");
|
||||
ct.mpt.generateKeyPair(bobRecoveryKey);
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobRecoveryKey),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
});
|
||||
|
||||
{
|
||||
auto const sleMptoken = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
|
||||
if (!BEAST_EXPECT(sleMptoken))
|
||||
return;
|
||||
BEAST_EXPECT(sleMptoken->isFieldPresent(sfRecoveryKey));
|
||||
}
|
||||
|
||||
Account const bobNewKey("bobNewKey");
|
||||
ct.mpt.generateKeyPair(bobNewKey);
|
||||
|
||||
auto const reEnc = reencryptHolderBalances(ct.mpt, bob, bob, bobNewKey);
|
||||
BEAST_EXPECT(reEnc.has_value());
|
||||
if (!reEnc)
|
||||
return;
|
||||
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.spendingCiphertext = reEnc->first,
|
||||
.inboxCiphertext = reEnc->second,
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRotation,
|
||||
});
|
||||
|
||||
auto const sleMptoken = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
|
||||
if (!BEAST_EXPECT(sleMptoken))
|
||||
return;
|
||||
BEAST_EXPECT(!sleMptoken->isFieldPresent(sfRecoveryKey));
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
testMPTokenIssuanceSetWithFeats(FeatureBitset features)
|
||||
@@ -3189,12 +2490,6 @@ public:
|
||||
testConfidentialMPTMirrorUpdateDoApply(all);
|
||||
testConfidentialMPTMirrorUpdateMultipleRotationsIssuerMode(all);
|
||||
testConfidentialMPTMirrorUpdateMultipleRotationsHolderMode(all);
|
||||
|
||||
testConfidentialMPTHolderKeyUpdatePreflight(all);
|
||||
testConfidentialMPTHolderKeyUpdatePreflight(all - featureConfidentialMPTKeyRotation);
|
||||
testConfidentialMPTHolderKeyUpdatePreflight(all - featureConfidentialTransfer);
|
||||
testConfidentialMPTHolderKeyUpdatePreclaim(all);
|
||||
testConfidentialMPTHolderKeyUpdateDoApply(all);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -638,104 +637,6 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase
|
||||
}
|
||||
}
|
||||
|
||||
// A full AMMWithdraw must succeed even when an unrelated third party
|
||||
// holds a confidential balance. Pre-fixCleanup3_5_0 the erase of the
|
||||
// AMM pseudo-account's MPToken was rejected in this case, permanently
|
||||
// stranding the last LP's position.
|
||||
void
|
||||
testAMMWithdrawAllBlockedByUnrelatedCOA(FeatureBitset features)
|
||||
{
|
||||
testcase("Full AMMWithdraw with an unrelated holder's COA");
|
||||
using namespace test::jtx;
|
||||
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const carol("carol");
|
||||
|
||||
for (bool const withFix : {true, false})
|
||||
{
|
||||
for (bool const carolConverts : {false, true})
|
||||
{
|
||||
Env env{*this, withFix ? features | fixCleanup3_5_0 : features - fixCleanup3_5_0};
|
||||
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
|
||||
mptAlice.create({.flags = kMptDexFlags | tfMPTCanHoldConfidentialBalance});
|
||||
mptAlice.authorize({.account = bob});
|
||||
mptAlice.authorize({.account = carol});
|
||||
mptAlice.pay(alice, bob, 1'000);
|
||||
mptAlice.pay(alice, carol, 100);
|
||||
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
|
||||
|
||||
// bob is the sole LP, so withdrawing all of his LPTokens zeroes
|
||||
// LPTokenBalance and deletes the AMM in that same transaction.
|
||||
AMM amm(env, bob, XRP(100), mptAlice(100));
|
||||
Account const ammHolder("amm", amm.ammAccount());
|
||||
BEAST_EXPECT(amm.ammExists());
|
||||
BEAST_EXPECT(mptAlice.getBalance(ammHolder) == 100);
|
||||
|
||||
// The AMM pseudo-account cannot initialize confidential state:
|
||||
// confidential transactions must be signed by sfAccount and it
|
||||
// has no signing key.
|
||||
BEAST_EXPECT(
|
||||
!mptAlice.getEncryptedBalance(ammHolder, MPTTester::holderEncryptedInbox));
|
||||
BEAST_EXPECT(
|
||||
!mptAlice.getEncryptedBalance(ammHolder, MPTTester::holderEncryptedSpending));
|
||||
BEAST_EXPECT(
|
||||
!mptAlice.getEncryptedBalance(ammHolder, MPTTester::issuerEncryptedBalance));
|
||||
BEAST_EXPECT(
|
||||
!mptAlice.getEncryptedBalance(ammHolder, MPTTester::auditorEncryptedBalance));
|
||||
|
||||
if (carolConverts)
|
||||
{
|
||||
mptAlice.generateKeyPair(carol);
|
||||
mptAlice.convert(
|
||||
{.account = carol, .amt = 1, .holderPubKey = mptAlice.getPubKey(carol)});
|
||||
}
|
||||
|
||||
BEAST_EXPECT(mptAlice.getIssuanceConfidentialBalance() == (carolConverts ? 1 : 0));
|
||||
|
||||
if (carolConverts && !withFix)
|
||||
{
|
||||
amm.withdrawAll(bob, std::nullopt, Ter(tecINVARIANT_FAILED));
|
||||
env.close();
|
||||
|
||||
// bob cannot close his position: the pool, his LPTokens and
|
||||
// the AMM object all survive the failed withdrawal.
|
||||
BEAST_EXPECT(amm.ammExists());
|
||||
BEAST_EXPECT(mptAlice.getBalance(ammHolder) == 100);
|
||||
|
||||
// A partial withdrawal still succeeds, because it never
|
||||
// erases the MPToken -- bob can drain the pool down to a
|
||||
// residual amount but can never close his position. Withdraw
|
||||
// half of his actual LPToken balance; a fixed token count
|
||||
// would round the MPT side of the pool to zero and be
|
||||
// rejected as a one-sided withdrawal.
|
||||
auto const bobLPTokens = amm.getLPTokensBalance(bob.id());
|
||||
amm.withdraw(
|
||||
bob, IOUAmount{bobLPTokens.mantissa() / 2, bobLPTokens.exponent()});
|
||||
env.close();
|
||||
BEAST_EXPECT(amm.ammExists());
|
||||
BEAST_EXPECT(mptAlice.getBalance(ammHolder) > 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
// With the fix, carol's confidential balance is irrelevant to
|
||||
// an AMM she has no stake in: bob closes his position and the
|
||||
// AMM and its MPToken are erased.
|
||||
amm.withdrawAll(bob);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!amm.ammExists());
|
||||
BEAST_EXPECT(
|
||||
env.le(keylet::mptoken(mptAlice.issuanceID(), amm.ammAccount())) ==
|
||||
nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exercises every Confidential Transfer transaction type (MPTokenIssuanceSet,
|
||||
// Convert, MergeInbox, Send, ConvertBack) using tickets instead of regular account
|
||||
// sequence numbers.
|
||||
@@ -2660,7 +2561,6 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase
|
||||
|
||||
// AMM/pseudo-account interaction.
|
||||
testAMMHolderCannotHaveConfidentialStateClawback(features);
|
||||
testAMMWithdrawAllBlockedByUnrelatedCOA(features);
|
||||
|
||||
// Ticket interactions.
|
||||
testWithTickets(features);
|
||||
|
||||
@@ -573,8 +573,8 @@ struct EscrowToken_test : public beast::unit_test::Suite
|
||||
env(pay(gw, bob, usd(1)));
|
||||
env.close();
|
||||
|
||||
bool const largeMantissa = features[featureSingleAssetVault] ||
|
||||
features[featureLendingProtocol] || features[featureMPTokensV2];
|
||||
bool const largeMantissa =
|
||||
features[featureSingleAssetVault] || features[featureLendingProtocol];
|
||||
|
||||
// alice cannot create escrow for 1/10 iou - precision loss
|
||||
env(escrow::create(alice, bob, usd(1)),
|
||||
@@ -2294,8 +2294,8 @@ struct EscrowToken_test : public beast::unit_test::Suite
|
||||
env(pay(gw, bob, usd(1)));
|
||||
env.close();
|
||||
|
||||
bool const largeMantissa = features[featureSingleAssetVault] ||
|
||||
features[featureLendingProtocol] || features[featureMPTokensV2];
|
||||
bool const largeMantissa =
|
||||
features[featureSingleAssetVault] || features[featureLendingProtocol];
|
||||
|
||||
// alice cannot create escrow for 1/10 iou - precision loss
|
||||
env(escrow::create(alice, bob, usd(1)),
|
||||
@@ -4334,7 +4334,7 @@ public:
|
||||
using namespace test::jtx;
|
||||
FeatureBitset const all{testableAmendments()};
|
||||
for (FeatureBitset const& feats :
|
||||
{all - featureSingleAssetVault - featureLendingProtocol - featureMPTokensV2, all})
|
||||
{all - featureSingleAssetVault - featureLendingProtocol, all})
|
||||
{
|
||||
testIOUWithFeats(feats);
|
||||
testIOUWithFeats(feats - fixCleanup3_2_0);
|
||||
|
||||
@@ -252,10 +252,7 @@ class TheoreticalQuality_test : public beast::unit_test::Suite
|
||||
std::shared_ptr<ReadView const> closed,
|
||||
std::optional<Quality> const& expectedQ = {})
|
||||
{
|
||||
// Not const: read-only ledger entries peek() through an ApplyView, which
|
||||
// is undefined behavior on a const object (see SLEBase.h).
|
||||
// NOLINTNEXTLINE(misc-const-correctness)
|
||||
PaymentSandbox sb(closed.get(), TapNone);
|
||||
PaymentSandbox const sb(closed.get(), TapNone);
|
||||
AMMContext ammContext(rcp.srcAccount, false);
|
||||
|
||||
auto const sendMaxIssue = [&rcp]() -> std::optional<Asset> {
|
||||
|
||||
@@ -1632,121 +1632,12 @@ class InvariantsMPT_test : public InvariantsBase
|
||||
precloseOrphan);
|
||||
}
|
||||
|
||||
// deletedHoldings_ in ValidMPTIssuance captures every erased MPToken
|
||||
// per-holder, so finalize()'s fixCleanup3_5_0 balance check must reject
|
||||
// a funded MPToken erased alongside an empty sibling regardless of the
|
||||
// order the two are visited in.
|
||||
void
|
||||
testDeleteWithBalanceTwoHolders()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
testcase << "MPToken deleted with non-zero balance, two holders";
|
||||
|
||||
MPTID mptID;
|
||||
Account const carol{"carol"};
|
||||
|
||||
// Single-holder setup used by the baseline and the amendment-gate
|
||||
// cases.
|
||||
auto const setupSingle = [&](Account const& a1, Account const& a2, Env& env) -> bool {
|
||||
MPTTester mpt(env, a1, {.holders = {a2}, .fund = false});
|
||||
mpt.create({.flags = tfMPTCanTransfer});
|
||||
mptID = mpt.issuanceID();
|
||||
mpt.authorize({.account = a2});
|
||||
mpt.pay(a1, a2, 100);
|
||||
return true;
|
||||
};
|
||||
|
||||
Precheck const eraseSingle = [&](Account const&, Account const& a2, ApplyContext& ac) {
|
||||
auto sleA2 = ac.view().peek(keylet::mptoken(mptID, a2.id()));
|
||||
if (!sleA2)
|
||||
return false;
|
||||
ac.view().erase(sleA2);
|
||||
return true;
|
||||
};
|
||||
|
||||
Precheck const eraseBoth = [&](Account const&, Account const& a2, ApplyContext& ac) {
|
||||
auto sleA2 = ac.view().peek(keylet::mptoken(mptID, a2.id()));
|
||||
auto sleCarol = ac.view().peek(keylet::mptoken(mptID, carol.id()));
|
||||
if (!sleA2 || !sleCarol)
|
||||
return false;
|
||||
ac.view().erase(sleA2);
|
||||
ac.view().erase(sleCarol);
|
||||
return true;
|
||||
};
|
||||
|
||||
// Cases below expecting `tecINVARIANT_FAILED` use ttACCOUNT_SET; the
|
||||
// success case (fixCleanup3_5_0 disabled) uses ttMPTOKEN_AUTHORIZE
|
||||
// to sidestep unrelated invariants that would otherwise mask the
|
||||
// tesSUCCESS signal.
|
||||
|
||||
// Baseline: a single funded MPToken erased on its own.
|
||||
doInvariantCheck(
|
||||
{{"MPToken deleted with non-zero balance"}},
|
||||
eraseSingle,
|
||||
XRPAmount{},
|
||||
STTx{ttACCOUNT_SET, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
setupSingle);
|
||||
|
||||
// Two-holder erase: whichever key sorts last is visited last, so
|
||||
// funding each holder in turn guarantees one run where the funded
|
||||
// MPToken is visited first and an empty sibling follows it.
|
||||
for (bool const fundCarol : {false, true})
|
||||
{
|
||||
auto const setupTwo = [&, fundCarol](
|
||||
Account const& a1, Account const& a2, Env& env) -> bool {
|
||||
env.fund(XRP(1'000), carol);
|
||||
MPTTester mpt(env, a1, {.holders = {a2, carol}, .fund = false});
|
||||
mpt.create({.flags = tfMPTCanTransfer});
|
||||
mptID = mpt.issuanceID();
|
||||
mpt.authorize({.account = a2});
|
||||
mpt.authorize({.account = carol});
|
||||
mpt.pay(a1, fundCarol ? carol : a2, 100);
|
||||
return true;
|
||||
};
|
||||
|
||||
doInvariantCheck(
|
||||
{{"MPToken deleted with non-zero balance"}},
|
||||
eraseBoth,
|
||||
XRPAmount{},
|
||||
STTx{ttACCOUNT_SET, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
setupTwo);
|
||||
}
|
||||
|
||||
// fixCleanup3_5_0 enabled, fixCleanup3_2_0 disabled: the two
|
||||
// amendments are independent, so this is the only configuration in
|
||||
// which the `|| isFeatureEnabled(fixCleanup3_5_0)` half of the
|
||||
// deletedHoldings_ capture gate in ValidMPTIssuance::visitEntry is
|
||||
// load-bearing.
|
||||
doInvariantCheck(
|
||||
makeEnv(all_ - fixCleanup3_2_0),
|
||||
{{"MPToken deleted with non-zero balance"}},
|
||||
eraseSingle,
|
||||
XRPAmount{},
|
||||
STTx{ttACCOUNT_SET, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
setupSingle);
|
||||
|
||||
// fixCleanup3_5_0 disabled: erasing a funded MPToken must not trip
|
||||
// the new check. This pins the fixCleanup3_5_0 gate in finalize().
|
||||
doInvariantCheck(
|
||||
makeEnv(all_ - fixCleanup3_5_0),
|
||||
{},
|
||||
eraseSingle,
|
||||
XRPAmount{},
|
||||
STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
|
||||
{tesSUCCESS, tesSUCCESS},
|
||||
setupSingle);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testConfidentialMPTTransfer();
|
||||
testMPT();
|
||||
testDeleteWithBalanceTwoHolders();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/OpenView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/RippleStateEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -381,7 +380,7 @@ class InvariantsMisc_test : public InvariantsBase
|
||||
for (auto const& trustKeylet :
|
||||
{keylet::trustLine(ammAcctID, a1["USD"]), keylet::trustLine(a1, ammIssue)})
|
||||
{
|
||||
RippleStateEntryW line(trustKeylet, ac.view());
|
||||
auto const line = ac.view().peek(trustKeylet);
|
||||
if (!line)
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -1763,119 +1763,6 @@ class LoanBroker_test : public beast::unit_test::Suite
|
||||
}
|
||||
}
|
||||
|
||||
// LoanBrokerDelete should succeed even when an unrelated third party
|
||||
// holds a confidential balance. The broker pseudo-account carries no
|
||||
// confidential fields of its own, but pre-fixCleanup3_5_0 the erase
|
||||
// was rejected.
|
||||
void
|
||||
testLoanBrokerDeleteConfidentialCOA(FeatureBitset features)
|
||||
{
|
||||
testcase << "LoanBrokerDelete - unrelated confidential holder";
|
||||
using namespace jtx;
|
||||
using namespace loan_broker;
|
||||
|
||||
auto const withFix = features[fixCleanup3_5_0];
|
||||
|
||||
Account const issuer("issuer");
|
||||
Account const alice("alice");
|
||||
// carol holds the same MPT but has no relationship to the vault, the
|
||||
// broker, or alice.
|
||||
Account const carol("carol");
|
||||
|
||||
for (bool const carolConverts : {false, true})
|
||||
{
|
||||
Env env(*this, features);
|
||||
env.fund(XRP(100'000), issuer, alice, carol);
|
||||
env.close();
|
||||
|
||||
MPTTester mptt{env, issuer, kMptInitNoFund};
|
||||
mptt.create({.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance});
|
||||
|
||||
PrettyAsset const mpt{mptt.issuanceID()};
|
||||
|
||||
mptt.authorize({.account = alice});
|
||||
mptt.authorize({.account = carol});
|
||||
env(pay(issuer, alice, mpt(100'000)));
|
||||
env(pay(issuer, carol, mpt(100)));
|
||||
env.close();
|
||||
|
||||
// The issuer must publish an encryption key before any holder can
|
||||
// convert into confidential form.
|
||||
mptt.generateKeyPair(issuer);
|
||||
mptt.set({.account = issuer, .issuerPubKey = mptt.getPubKey(issuer)});
|
||||
|
||||
Vault const vault{env};
|
||||
auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = mpt});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = mpt(10'000)}));
|
||||
env.close();
|
||||
|
||||
auto const brokerKeylet =
|
||||
keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
|
||||
env(set(alice, vaultKeylet.key));
|
||||
env.close();
|
||||
|
||||
env(coverDeposit(alice, brokerKeylet.key, mpt(5'000).value()));
|
||||
env.close();
|
||||
|
||||
auto const broker = env.le(brokerKeylet);
|
||||
if (!BEAST_EXPECT(broker))
|
||||
return;
|
||||
BEAST_EXPECT(broker->at(sfCoverAvailable) > 0);
|
||||
|
||||
auto const brokerPseudoID = broker->at(sfAccount);
|
||||
auto const pseudoMptKey = keylet::mptoken(mptt.issuanceID(), brokerPseudoID);
|
||||
if (!BEAST_EXPECT(env.le(pseudoMptKey)))
|
||||
return;
|
||||
|
||||
// The broker pseudo-account has no signing key, so it can never
|
||||
// submit a confidential transaction and its MPToken holds none of
|
||||
// the four ciphertext fields.
|
||||
Account const brokerHolder("broker", brokerPseudoID);
|
||||
BEAST_EXPECT(!mptt.getEncryptedBalance(brokerHolder, MPTTester::holderEncryptedInbox));
|
||||
BEAST_EXPECT(
|
||||
!mptt.getEncryptedBalance(brokerHolder, MPTTester::holderEncryptedSpending));
|
||||
BEAST_EXPECT(
|
||||
!mptt.getEncryptedBalance(brokerHolder, MPTTester::issuerEncryptedBalance));
|
||||
BEAST_EXPECT(
|
||||
!mptt.getEncryptedBalance(brokerHolder, MPTTester::auditorEncryptedBalance));
|
||||
|
||||
if (carolConverts)
|
||||
{
|
||||
mptt.generateKeyPair(carol);
|
||||
mptt.convert({.account = carol, .amt = 1, .holderPubKey = mptt.getPubKey(carol)});
|
||||
}
|
||||
|
||||
BEAST_EXPECT(mptt.getIssuanceConfidentialBalance() == (carolConverts ? 1 : 0));
|
||||
|
||||
if (carolConverts && !withFix)
|
||||
{
|
||||
// The cover is non-zero at the start of the transaction, so
|
||||
// the erase is flagged and carol's COA rejects it.
|
||||
env(del(alice, brokerKeylet.key), Ter(tecINVARIANT_FAILED));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.le(brokerKeylet) != nullptr);
|
||||
BEAST_EXPECT(env.le(pseudoMptKey) != nullptr);
|
||||
|
||||
// Draining the cover in an earlier transaction leaves the
|
||||
// MPToken's pre-transaction balance at zero, which sidesteps
|
||||
// the flag entirely.
|
||||
env(coverWithdraw(alice, brokerKeylet.key, mpt(5'000).value()));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// With the fix the balance is read at erase time, so returning
|
||||
// the cover and deleting the broker in one transaction succeeds
|
||||
// regardless of carol's confidential balance.
|
||||
env(del(alice, brokerKeylet.key));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.le(brokerKeylet) == nullptr);
|
||||
BEAST_EXPECT(env.le(pseudoMptKey) == nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testLoanBrokerDeleteFrozenIOU(FeatureBitset features)
|
||||
{
|
||||
@@ -3117,9 +3004,6 @@ public:
|
||||
testLoanBrokerDeleteFrozenIOU(all_);
|
||||
testLoanBrokerDeleteFrozenIOU(all_ - fixCleanup3_2_0);
|
||||
|
||||
testLoanBrokerDeleteConfidentialCOA(all_);
|
||||
testLoanBrokerDeleteConfidentialCOA(all_ - fixCleanup3_5_0);
|
||||
|
||||
// featureMPTokensV2 independently makes ValidMPTTransfer enforcing,
|
||||
// but it's Supported::No (never enabled on real networks); exclude
|
||||
// it here so fixCleanup3_4_0 alone is the deciding amendment, as it
|
||||
|
||||
+23
-100
@@ -122,83 +122,6 @@ backend=sqlite
|
||||
return std::format(kConfigContentsTemplate, dbPathSection, valFileSection);
|
||||
}
|
||||
|
||||
// setenv and unsetenv are POSIX only, and MSVC does not have them.
|
||||
#ifndef _MSC_VER
|
||||
|
||||
/**
|
||||
* Read an environment variable into a value the caller owns.
|
||||
*
|
||||
* The value is copied, because POSIX allows a later `setenv` to release the
|
||||
* string `getenv` returned.
|
||||
*
|
||||
* @param name Name of the environment variable to read.
|
||||
* @return The value, or std::nullopt when the variable is not set.
|
||||
*/
|
||||
[[nodiscard]] std::optional<std::string>
|
||||
envVar(char const* name)
|
||||
{
|
||||
if (char const* const value = std::getenv(name); value != nullptr)
|
||||
return std::string{value};
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an environment variable and restore its previous value when done.
|
||||
*/
|
||||
class EnvVarGuard
|
||||
{
|
||||
private:
|
||||
std::string const name_;
|
||||
std::optional<std::string> const saved_;
|
||||
|
||||
/**
|
||||
* Give an environment variable a value, or remove it.
|
||||
*
|
||||
* @param name Name of the environment variable to write.
|
||||
* @param value Value to set, or std::nullopt to remove the variable.
|
||||
*/
|
||||
static void
|
||||
apply(char const* name, std::optional<std::string> const& value) noexcept
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
setenv(name, value->c_str(), 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
unsetenv(name);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
/**
|
||||
* Save the variable's current value, then write the given one.
|
||||
*
|
||||
* @param name Name of the environment variable to set.
|
||||
* @param value Value to set, or std::nullopt to remove the variable.
|
||||
*/
|
||||
[[nodiscard]] EnvVarGuard(char const* name, std::optional<std::string> const& value)
|
||||
: name_(name), saved_(envVar(name))
|
||||
{
|
||||
apply(name_.c_str(), value);
|
||||
}
|
||||
|
||||
EnvVarGuard(EnvVarGuard const&) = delete;
|
||||
EnvVarGuard&
|
||||
operator=(EnvVarGuard const&) = delete;
|
||||
|
||||
/**
|
||||
* Restore the value the variable held before construction.
|
||||
*/
|
||||
~EnvVarGuard()
|
||||
{
|
||||
apply(name_.c_str(), saved_);
|
||||
}
|
||||
};
|
||||
|
||||
#endif // _MSC_VER
|
||||
|
||||
/**
|
||||
* Write an xrpld config file and remove when done.
|
||||
*/
|
||||
@@ -412,11 +335,7 @@ port_wss_admin
|
||||
}
|
||||
|
||||
// Config file in HOME or XDG_CONFIG_HOME directory.
|
||||
#ifndef _MSC_VER
|
||||
// Save the values the guards below must put back.
|
||||
auto const home = detail::envVar("HOME");
|
||||
auto const xdgConfigHome = detail::envVar("XDG_CONFIG_HOME");
|
||||
|
||||
#if BOOST_OS_LINUX || BOOST_OS_MACOS
|
||||
for (auto const& configFile : configFiles)
|
||||
{
|
||||
// Point the current working directory to a temporary directory, so
|
||||
@@ -429,11 +348,13 @@ port_wss_admin
|
||||
{
|
||||
TempDir const tc;
|
||||
|
||||
// The HOME variable is not used when XDG_CONFIG_HOME is set,
|
||||
// but must be set. Both guards are declared after tc, so they
|
||||
// restore before it is removed.
|
||||
detail::EnvVarGuard const homeGuard("HOME", tc.path());
|
||||
detail::EnvVarGuard const xdgGuard("XDG_CONFIG_HOME", tc.path());
|
||||
// Set the HOME and XDG_CONFIG_HOME environment variables. The
|
||||
// HOME variable is not used when XDG_CONFIG_HOME is set, but
|
||||
// must be set.
|
||||
char const* h = getenv("HOME");
|
||||
setenv("HOME", tc.path().c_str(), 1);
|
||||
char const* x = getenv("XDG_CONFIG_HOME");
|
||||
setenv("XDG_CONFIG_HOME", tc.path().c_str(), 1);
|
||||
|
||||
// Create the config file in '${XDG_CONFIG_HOME}/[systemName]'.
|
||||
Path p = tc.file(systemName());
|
||||
@@ -450,21 +371,22 @@ port_wss_admin
|
||||
BEAST_EXPECT(
|
||||
c.section(Sections::kDebugLogfile).values()[0] ==
|
||||
"/Users/dummy/xrpld/config/log/debug.log");
|
||||
}
|
||||
|
||||
// Both guards are gone: both variables must hold their old values.
|
||||
BEAST_EXPECT(detail::envVar("HOME") == home);
|
||||
BEAST_EXPECT(detail::envVar("XDG_CONFIG_HOME") == xdgConfigHome);
|
||||
// Restore the environment variables.
|
||||
(h != nullptr) ? setenv("HOME", h, 1) : unsetenv("HOME");
|
||||
(x != nullptr) ? setenv("XDG_CONFIG_HOME", x, 1) : unsetenv("XDG_CONFIG_HOME");
|
||||
}
|
||||
|
||||
// The XDG config directory is not set: the config file must be in a
|
||||
// subdirectory named .config followed by the system name.
|
||||
{
|
||||
TempDir const tc;
|
||||
|
||||
// Both guards are declared after tc, so they restore before it
|
||||
// is removed.
|
||||
detail::EnvVarGuard const homeGuard("HOME", tc.path());
|
||||
detail::EnvVarGuard const xdgGuard("XDG_CONFIG_HOME", std::nullopt);
|
||||
// Set only the HOME environment variable.
|
||||
char const* h = getenv("HOME");
|
||||
setenv("HOME", tc.path().c_str(), 1);
|
||||
char const* x = getenv("XDG_CONFIG_HOME");
|
||||
unsetenv("XDG_CONFIG_HOME");
|
||||
|
||||
// Create the config file in '${HOME}/.config/[systemName]'.
|
||||
std::string s = ".config";
|
||||
@@ -485,13 +407,14 @@ port_wss_admin
|
||||
BEAST_EXPECT(
|
||||
c.section(Sections::kDebugLogfile).values()[0] ==
|
||||
"/Users/dummy/xrpld/config/log/debug.log");
|
||||
}
|
||||
|
||||
// Both guards are gone: both variables must hold their old values.
|
||||
BEAST_EXPECT(detail::envVar("HOME") == home);
|
||||
BEAST_EXPECT(detail::envVar("XDG_CONFIG_HOME") == xdgConfigHome);
|
||||
// Restore the environment variables.
|
||||
(h != nullptr) ? setenv("HOME", h, 1) : unsetenv("HOME");
|
||||
if (x != nullptr)
|
||||
setenv("XDG_CONFIG_HOME", x, 1);
|
||||
}
|
||||
}
|
||||
#endif // _MSC_VER
|
||||
#endif
|
||||
|
||||
// Restore the current working directory.
|
||||
current_path(cwd);
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -189,17 +188,6 @@ protected:
|
||||
// Proof layout: [compact_sigma | bulletproof]
|
||||
static constexpr size_t kBulletproofOffset = kEcSendProofLength - kEcDoubleBulletproofLength;
|
||||
|
||||
// Decrypts holder's current spending/inbox balances under currentKey and
|
||||
// re-encrypts them under newKey, returning {spendingCiphertext,
|
||||
// inboxCiphertext}. Returns std::nullopt if either balance is missing or
|
||||
// fails to decrypt.
|
||||
static std::optional<std::pair<Buffer, Buffer>>
|
||||
reencryptHolderBalances(
|
||||
test::jtx::MPTTester& mpt,
|
||||
test::jtx::Account const& holder,
|
||||
test::jtx::Account const& currentKey,
|
||||
test::jtx::Account const& newKey);
|
||||
|
||||
// Generate a forged aggregated bulletproof (double bulletproof) for
|
||||
// the given values and blinding factors. Used to test that splicing
|
||||
// a bulletproof claiming a different remaining balance is rejected.
|
||||
|
||||
@@ -139,15 +139,14 @@ AMMTestBase::testAMM(std::function<void(jtx::AMM&, jtx::Env&)> const& cb, TestAM
|
||||
// Use small Number mantissas for the life of this test.
|
||||
NumberMantissaScaleGuard const sg{xrpl::MantissaRange::MantissaScale::Small};
|
||||
|
||||
auto const [asset1, asset2] = arg.pool ? *arg.pool : std::make_pair(XRP(10000), USD(10000));
|
||||
|
||||
// For now, just disable SAV entirely, which locks in the small Number
|
||||
// mantissas. MPTokensV2 enables them too, so keep it for MPT pools
|
||||
// only, which require it.
|
||||
auto envFeatures = features - featureSingleAssetVault - featureLendingProtocol;
|
||||
if (!asset1.holds<MPTIssue>() && !asset2.holds<MPTIssue>())
|
||||
envFeatures = envFeatures - featureMPTokensV2;
|
||||
Env env{*this, envFeatures, arg.noLog ? std::make_unique<CaptureLogs>(&logs) : nullptr};
|
||||
// mantissas
|
||||
Env env{
|
||||
*this,
|
||||
features - featureSingleAssetVault - featureLendingProtocol,
|
||||
arg.noLog ? std::make_unique<CaptureLogs>(&logs) : nullptr};
|
||||
|
||||
auto const [asset1, asset2] = arg.pool ? *arg.pool : std::make_pair(XRP(10000), USD(10000));
|
||||
auto toFund = [&](STAmount const& a) -> STAmount {
|
||||
if (a.native())
|
||||
{
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -293,30 +292,6 @@ ConfidentialTransferTestBase::ConfidentialSendSetup::sendArgs(
|
||||
};
|
||||
}
|
||||
|
||||
std::optional<std::pair<Buffer, Buffer>>
|
||||
ConfidentialTransferTestBase::reencryptHolderBalances(
|
||||
test::jtx::MPTTester& mpt,
|
||||
test::jtx::Account const& holder,
|
||||
test::jtx::Account const& currentKey,
|
||||
test::jtx::Account const& newKey)
|
||||
{
|
||||
auto const spendingCt =
|
||||
mpt.getEncryptedBalance(holder, test::jtx::MPTTester::holderEncryptedSpending);
|
||||
auto const inboxCt =
|
||||
mpt.getEncryptedBalance(holder, test::jtx::MPTTester::holderEncryptedInbox);
|
||||
if (!spendingCt || !inboxCt)
|
||||
return std::nullopt;
|
||||
|
||||
auto const spendingAmt = mpt.decryptAmount(currentKey, *spendingCt);
|
||||
auto const inboxAmt = mpt.decryptAmount(currentKey, *inboxCt);
|
||||
if (!spendingAmt || !inboxAmt)
|
||||
return std::nullopt;
|
||||
|
||||
return std::pair{
|
||||
mpt.encryptAmount(newKey, *spendingAmt, generateBlindingFactor()),
|
||||
mpt.encryptAmount(newKey, *inboxAmt, generateBlindingFactor())};
|
||||
}
|
||||
|
||||
Buffer const&
|
||||
ConfidentialTransferTestBase::getBadCiphertext()
|
||||
{
|
||||
|
||||
@@ -2291,25 +2291,4 @@ MPTTester::mirrorUpdate(MPTMirrorUpdate const& arg, std::source_location const&
|
||||
submit(arg, {jv, loc});
|
||||
}
|
||||
|
||||
void
|
||||
MPTTester::holderKeyUpdate(MPTHolderKeyUpdate const& arg, std::source_location const& loc)
|
||||
{
|
||||
json::Value jv;
|
||||
jv[jss::TransactionType] = jss::ConfidentialMPTHolderKeyUpdate;
|
||||
|
||||
setAccountField(jv, arg.account);
|
||||
setIssuanceIdField(jv, arg.id);
|
||||
|
||||
if (arg.holderPubKey)
|
||||
jv[sfHolderEncryptionKey.jsonName] = strHex(*arg.holderPubKey);
|
||||
if (arg.spendingCiphertext)
|
||||
jv[sfConfidentialBalanceSpending.jsonName] = strHex(*arg.spendingCiphertext);
|
||||
if (arg.inboxCiphertext)
|
||||
jv[sfConfidentialBalanceInbox.jsonName] = strHex(*arg.inboxCiphertext);
|
||||
if (arg.proof)
|
||||
jv[sfZKProof.jsonName] = strHex(*arg.proof);
|
||||
|
||||
submit(arg, {jv, loc});
|
||||
}
|
||||
|
||||
} // namespace xrpl::test::jtx
|
||||
@@ -78,8 +78,7 @@ fillFee(json::Value& jv, ReadView const& view)
|
||||
auto const txType = jv[jss::TransactionType].asString();
|
||||
if (txType == jss::ConfidentialMPTConvert || txType == jss::ConfidentialMPTConvertBack ||
|
||||
txType == jss::ConfidentialMPTSend || txType == jss::ConfidentialMPTMergeInbox ||
|
||||
txType == jss::ConfidentialMPTClawback || txType == jss::ConfidentialMPTMirrorUpdate ||
|
||||
txType == jss::ConfidentialMPTHolderKeyUpdate)
|
||||
txType == jss::ConfidentialMPTClawback || txType == jss::ConfidentialMPTMirrorUpdate)
|
||||
{
|
||||
jv[jss::Fee] = to_string(base * (kConfidentialFeeMultiplier + 1));
|
||||
}
|
||||
|
||||
@@ -408,24 +408,6 @@ struct MPTMirrorUpdate
|
||||
std::optional<TER> err = std::nullopt;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Arguments for building a ConfidentialMPTHolderKeyUpdate test transaction.
|
||||
*/
|
||||
struct MPTHolderKeyUpdate
|
||||
{
|
||||
std::optional<Account> account = std::nullopt;
|
||||
std::optional<MPTID> id = std::nullopt;
|
||||
std::optional<Buffer> holderPubKey = std::nullopt;
|
||||
std::optional<Buffer> spendingCiphertext = std::nullopt;
|
||||
std::optional<Buffer> inboxCiphertext = std::nullopt;
|
||||
std::optional<Buffer> proof = std::nullopt;
|
||||
std::optional<std::uint32_t> ownerCount = std::nullopt;
|
||||
std::optional<std::uint32_t> holderCount = std::nullopt;
|
||||
std::optional<std::uint32_t> flags = std::nullopt;
|
||||
std::optional<XRPAmount> fee = std::nullopt;
|
||||
std::optional<TER> err = std::nullopt;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Stores the parameters that are exclusively used to generate a
|
||||
* Pedersen linkage proof.
|
||||
@@ -677,11 +659,6 @@ public:
|
||||
MPTMirrorUpdate const& arg = MPTMirrorUpdate{},
|
||||
std::source_location const& loc = std::source_location::current());
|
||||
|
||||
void
|
||||
holderKeyUpdate(
|
||||
MPTHolderKeyUpdate const& arg = MPTHolderKeyUpdate{},
|
||||
std::source_location const& loc = std::source_location::current());
|
||||
|
||||
[[nodiscard]] bool
|
||||
checkDomainID(std::optional<UInt256> expected) const;
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/credentials.h>
|
||||
#include <test/jtx/deposit.h>
|
||||
#include <test/jtx/escrow.h>
|
||||
#include <test/jtx/flags.h>
|
||||
@@ -33,7 +32,6 @@
|
||||
#include <cstddef>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::rpc {
|
||||
@@ -519,71 +517,6 @@ public:
|
||||
rpc::makeError(RpcInvalidParams)[jss::error_message]);
|
||||
}
|
||||
|
||||
void
|
||||
testMarkerNewObjectTypes()
|
||||
{
|
||||
// Regression: pagination across account_lines / account_offers /
|
||||
// account_channels must not reject a marker whose SLE is a
|
||||
// Credential (or any post-2023 owner-directory entry).
|
||||
testcase("Marker on new owner-directory entry types (Credential)");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
// Walk the owner directory via `rpcMethod` at limit=1 and return
|
||||
// the total number of paged calls plus whether the RPC ever
|
||||
// returned "invalidParams" during pagination.
|
||||
auto walkPages = [](Env& env, Account const& account, char const* rpcMethod) {
|
||||
std::optional<std::string> marker;
|
||||
int iterations = 0;
|
||||
bool hitInvalidParams = false;
|
||||
for (int guard = 0; guard < 20; ++guard)
|
||||
{
|
||||
json::Value params;
|
||||
params[jss::account] = account.human();
|
||||
params[jss::limit] = 1;
|
||||
if (marker)
|
||||
params[jss::marker] = *marker;
|
||||
auto const resp = env.rpc("json", rpcMethod, to_string(params))[jss::result];
|
||||
++iterations;
|
||||
if (resp.isMember(jss::error))
|
||||
{
|
||||
hitInvalidParams = resp[jss::error].asString() == "invalidParams";
|
||||
break;
|
||||
}
|
||||
if (!resp.isMember(jss::marker))
|
||||
break;
|
||||
marker = resp[jss::marker].asString();
|
||||
}
|
||||
return std::pair{iterations, hitInvalidParams};
|
||||
};
|
||||
|
||||
for (char const* rpcMethod : {"account_lines", "account_offers", "account_channels"})
|
||||
{
|
||||
Env env(*this);
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const issuer{"issuer"};
|
||||
env.fund(XRP(10000), alice, issuer);
|
||||
env.close();
|
||||
|
||||
auto const usd = issuer["USD"];
|
||||
env(trust(alice, usd(200)));
|
||||
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
env(credentials::create(alice, issuer, std::string("Cred") + std::to_string(i)));
|
||||
}
|
||||
env.close();
|
||||
|
||||
auto const [iterations, hitInvalidParams] = walkPages(env, alice, rpcMethod);
|
||||
BEAST_EXPECTS(!hitInvalidParams, rpcMethod);
|
||||
// 1 trust line + 4 credentials — pagination walks all owner-dir
|
||||
// entries regardless of which the RPC includes in its results.
|
||||
BEAST_EXPECTS(
|
||||
iterations == 5, std::string(rpcMethod) + ": " + std::to_string(iterations));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testAccountLinesWalkMarkers()
|
||||
{
|
||||
@@ -692,8 +625,8 @@ public:
|
||||
env(ticket::create(alice, 2));
|
||||
|
||||
// Add another trustline for good measure
|
||||
auto const btcBecky = becky["BTC"];
|
||||
env(trust(alice, btcBecky(200)));
|
||||
auto const btCbecky = becky["BTC"];
|
||||
env(trust(alice, btCbecky(200)));
|
||||
|
||||
env.close();
|
||||
|
||||
@@ -1392,7 +1325,6 @@ public:
|
||||
testAccountLines();
|
||||
testAccountLinesMarker();
|
||||
testAccountLineDelete();
|
||||
testMarkerNewObjectTypes();
|
||||
testAccountLinesWalkMarkers();
|
||||
testAccountLines2();
|
||||
testAccountLineDelete2();
|
||||
|
||||
@@ -5,12 +5,8 @@
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
class RPCHelpers_test : public beast::unit_test::Suite
|
||||
@@ -70,40 +66,10 @@ public:
|
||||
BEAST_EXPECT(result.second == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testOwnerDirNodeFieldsCoverage()
|
||||
{
|
||||
// Every UINT64 sf*Node field must be classified: either it is an
|
||||
// owner-directory page hint (rpc::isOwnerDirNodeField()) or it
|
||||
// belongs to a book directory (exclusion set below). Adding a new
|
||||
// sf*Node UINT64 without updating one of these lists trips this
|
||||
// test.
|
||||
testcase("Owner-directory Node fields coverage");
|
||||
|
||||
// Book-directory page hints, not owner-directory. sfBookNode: offer
|
||||
// book. sfNFTokenOfferNode: NFT bid/ask book.
|
||||
std::set<SField const*> const nonOwnerDirNodes{
|
||||
&sfBookNode,
|
||||
&sfNFTokenOfferNode,
|
||||
};
|
||||
|
||||
for (auto const& [_, sf] : SField::getKnownCodeToField())
|
||||
{
|
||||
if (sf->fieldType != STI_UINT64 || !sf->getName().ends_with("Node"))
|
||||
continue;
|
||||
BEAST_EXPECTS(
|
||||
rpc::isOwnerDirNodeField(*sf) || nonOwnerDirNodes.contains(sf),
|
||||
"sf" + sf->getName() +
|
||||
": add to kOwnerDirNodeFields (owner-dir page hint) "
|
||||
"or to nonOwnerDirNodes (book-dir).");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testChooseLedgerEntryType();
|
||||
testOwnerDirNodeFieldsCoverage();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/WSClient.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/batch.h>
|
||||
#include <test/jtx/domain.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
#include <test/jtx/escrow.h>
|
||||
#include <test/jtx/fee.h>
|
||||
#include <test/jtx/mpt.h>
|
||||
#include <test/jtx/offer.h>
|
||||
#include <test/jtx/owners.h> // IWYU pragma: keep
|
||||
#include <test/jtx/paths.h>
|
||||
@@ -35,14 +32,11 @@
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STValidation.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Seed.h>
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol/tokens.h>
|
||||
#include <xrpl/server/LoadFeeTrack.h>
|
||||
@@ -653,42 +647,6 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
for (auto const& nonArray : nonArrays)
|
||||
{
|
||||
json::Value jv;
|
||||
jv[jss::mpt_issuances] = nonArray;
|
||||
auto jr = wsc->invoke(method, jv)[jss::result];
|
||||
BEAST_EXPECT(jr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
|
||||
}
|
||||
|
||||
{
|
||||
json::Value jv;
|
||||
jv[jss::mpt_issuances] = json::ValueType::Array;
|
||||
jv[jss::mpt_issuances][0u] = 1;
|
||||
auto jr = wsc->invoke(method, jv)[jss::result];
|
||||
BEAST_EXPECT(jr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
|
||||
}
|
||||
|
||||
{
|
||||
json::Value jv;
|
||||
jv[jss::mpt_issuances] = json::ValueType::Array;
|
||||
jv[jss::mpt_issuances][0u] = "not-an-mpt-issuance-id";
|
||||
auto jr = wsc->invoke(method, jv)[jss::result];
|
||||
BEAST_EXPECT(jr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
|
||||
}
|
||||
|
||||
{
|
||||
json::Value jv;
|
||||
jv[jss::mpt_issuances] = json::ValueType::Array;
|
||||
jv[jss::mpt_issuances][0u] = "0123456789ABCDEF";
|
||||
auto jr = wsc->invoke(method, jv)[jss::result];
|
||||
BEAST_EXPECT(jr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
|
||||
}
|
||||
|
||||
for (auto const& nonArray : nonArrays)
|
||||
{
|
||||
json::Value jv;
|
||||
@@ -1658,38 +1616,6 @@ public:
|
||||
return jv;
|
||||
}
|
||||
|
||||
// Build `count` distinct, valid MPT issuance id strings the same cheap way
|
||||
// as makeAccountStrings: by incrementing an MPTID. doSubscribe only parses
|
||||
// these (it does not require the issuances to exist in the ledger), and
|
||||
// parseMPTIssuanceIDs dedups into a HashSet, so distinctness is what makes
|
||||
// the cap arithmetic exact.
|
||||
static std::vector<std::string>
|
||||
makeMPTIssuanceStrings(std::size_t count, std::uint32_t seed = 1)
|
||||
{
|
||||
std::vector<std::string> out;
|
||||
out.reserve(count);
|
||||
// Start at `seed` so separate calls produce non-overlapping ranges.
|
||||
MPTID id{static_cast<std::uint64_t>(seed)};
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
out.push_back(to_string(id));
|
||||
++id;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Append the given issuance ids as a jss::mpt_issuances array onto a fresh
|
||||
// subscribe request object.
|
||||
static json::Value
|
||||
mptIssuancesRequest(std::vector<std::string> const& mptIDs)
|
||||
{
|
||||
json::Value jv{json::ValueType::Object};
|
||||
jv[jss::mpt_issuances] = json::ValueType::Array;
|
||||
for (auto const& id : mptIDs)
|
||||
jv[jss::mpt_issuances].append(id);
|
||||
return jv;
|
||||
}
|
||||
|
||||
// A single, valid XRP/USD order book request, as one entry of a
|
||||
// jss::books array.
|
||||
static json::Value
|
||||
@@ -1916,149 +1842,6 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testMPTCapRejects()
|
||||
{
|
||||
// MPT issuance subscriptions are per-connection tracked state, so they
|
||||
// are bounded by the same cap as the account branches. A request that
|
||||
// alone exceeds the cap is rejected with the cap error, and nothing is
|
||||
// recorded. (Pre-fix the mpt_issuances branch skipped the cap check
|
||||
// entirely, so a client could hold unbounded issuance subscriptions.)
|
||||
testcase("mpt_issuances cap rejects an over-cap request");
|
||||
|
||||
using namespace jtx;
|
||||
Env env{*this, envconfig(cappedConfig(5))};
|
||||
auto wsc = makeWSClient(env.app().config());
|
||||
|
||||
// Six issuances against a cap of five: rejected.
|
||||
{
|
||||
auto const jr = wsc->invoke(
|
||||
"subscribe", mptIssuancesRequest(makeMPTIssuanceStrings(6)))[jss::result];
|
||||
BEAST_EXPECT(jr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jr[jss::error_message] == "Too many subscriptions for this connection.");
|
||||
}
|
||||
|
||||
// The rejected request reserved nothing: a five-issuance request (the
|
||||
// cap exactly) still fits. If the rejected one had leaked entries this
|
||||
// would be rejected too.
|
||||
{
|
||||
auto const r = wsc->invoke("subscribe", mptIssuancesRequest(makeMPTIssuanceStrings(5)));
|
||||
BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testMPTReSubscribeNotOvercounted()
|
||||
{
|
||||
// Like the account branches, the MPT charge counts only NET-NEW
|
||||
// issuances: re-subscribing issuances already held adds no tracked
|
||||
// state, so it must be admitted even sitting exactly at the cap.
|
||||
testcase("mpt_issuances re-subscribe at the cap is not over-counted");
|
||||
|
||||
using namespace jtx;
|
||||
Env env{*this, envconfig(cappedConfig(5))};
|
||||
auto wsc = makeWSClient(env.app().config());
|
||||
|
||||
// Fill the cap exactly with five distinct issuances.
|
||||
auto const five = makeMPTIssuanceStrings(5);
|
||||
{
|
||||
auto const r = wsc->invoke("subscribe", mptIssuancesRequest(five));
|
||||
BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
|
||||
}
|
||||
|
||||
// Re-subscribe the same five: net-new is zero, so it stays within the
|
||||
// cap and must succeed.
|
||||
{
|
||||
auto const r = wsc->invoke("subscribe", mptIssuancesRequest(five));
|
||||
BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
|
||||
}
|
||||
|
||||
// A single net-new issuance on top of a full cap IS rejected.
|
||||
{
|
||||
auto const jr = wsc->invoke(
|
||||
"subscribe", mptIssuancesRequest(makeMPTIssuanceStrings(1, 100)))[jss::result];
|
||||
BEAST_EXPECT(jr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jr[jss::error_message] == "Too many subscriptions for this connection.");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testMPTSharesCapWithAccounts()
|
||||
{
|
||||
// The cap is one budget over all tracked kinds, so MPT issuances and
|
||||
// accounts consume the same allowance in either order.
|
||||
testcase("mpt_issuances and accounts share one cap");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// Accounts first, then MPTs: 3 + 3 over a cap of 5 is rejected.
|
||||
{
|
||||
Env env{*this, envconfig(cappedConfig(5))};
|
||||
auto wsc = makeWSClient(env.app().config());
|
||||
|
||||
auto const r = wsc->invoke("subscribe", accountsRequest(makeAccountStrings(3)));
|
||||
BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
|
||||
|
||||
auto const jr = wsc->invoke(
|
||||
"subscribe", mptIssuancesRequest(makeMPTIssuanceStrings(3)))[jss::result];
|
||||
BEAST_EXPECT(jr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jr[jss::error_message] == "Too many subscriptions for this connection.");
|
||||
|
||||
// Two more issuances do fit the remaining allowance.
|
||||
auto const r2 =
|
||||
wsc->invoke("subscribe", mptIssuancesRequest(makeMPTIssuanceStrings(2)));
|
||||
BEAST_EXPECTS(r2[jss::status] == "success", to_string(r2));
|
||||
}
|
||||
|
||||
// MPTs first, then accounts: the tally MPTs contribute is what rejects
|
||||
// the accounts, proving mptSubscriptions_ is part of the count and not
|
||||
// just checked against it.
|
||||
{
|
||||
Env env{*this, envconfig(cappedConfig(5))};
|
||||
auto wsc = makeWSClient(env.app().config());
|
||||
|
||||
auto const r = wsc->invoke("subscribe", mptIssuancesRequest(makeMPTIssuanceStrings(5)));
|
||||
BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
|
||||
|
||||
auto const jr =
|
||||
wsc->invoke("subscribe", accountsRequest(makeAccountStrings(1)))[jss::result];
|
||||
BEAST_EXPECT(jr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jr[jss::error_message] == "Too many subscriptions for this connection.");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testMPTUnsubscribeFreesCap()
|
||||
{
|
||||
// Unsubscribing releases the allowance: after dropping the issuances,
|
||||
// the same connection can subscribe a fresh batch. Guards against a
|
||||
// reserve that is never given back (which would turn the cap into a
|
||||
// permanent per-connection budget).
|
||||
testcase("mpt_issuances unsubscribe frees cap allowance");
|
||||
|
||||
using namespace jtx;
|
||||
Env env{*this, envconfig(cappedConfig(5))};
|
||||
auto wsc = makeWSClient(env.app().config());
|
||||
|
||||
auto const five = makeMPTIssuanceStrings(5);
|
||||
{
|
||||
auto const r = wsc->invoke("subscribe", mptIssuancesRequest(five));
|
||||
BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
|
||||
}
|
||||
|
||||
{
|
||||
auto const r = wsc->invoke("unsubscribe", mptIssuancesRequest(five));
|
||||
BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
|
||||
}
|
||||
|
||||
// The cap is free again, so a disjoint batch of five is admitted.
|
||||
{
|
||||
auto const r =
|
||||
wsc->invoke("subscribe", mptIssuancesRequest(makeMPTIssuanceStrings(5, 100)));
|
||||
BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testAsyncTeardownDoesNotStall()
|
||||
{
|
||||
@@ -2181,275 +1964,6 @@ public:
|
||||
wscB->invoke("unsubscribe", accountsRequest({alice.human()}));
|
||||
}
|
||||
|
||||
void
|
||||
testSubMPT()
|
||||
{
|
||||
// subscribe to multiple MPTs
|
||||
testcase("SubMPT");
|
||||
using namespace jtx;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
Env env(*this);
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
Account const carol{"carol"};
|
||||
Account const dan{"dan"};
|
||||
|
||||
auto wsc = makeWSClient(env.app().config());
|
||||
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
MPTTester mptCarol(env, carol, {.holders = {dan}});
|
||||
|
||||
// Transfer fee is 10%
|
||||
mptAlice.create(
|
||||
{.transferFee = 10'000, .ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanLock});
|
||||
mptCarol.create({.ownerCount = 1, .flags = tfMPTCanTransfer});
|
||||
|
||||
json::Value stream;
|
||||
stream = json::ValueType::Object;
|
||||
stream[jss::mpt_issuances] = json::ValueType::Array;
|
||||
stream[jss::mpt_issuances].append(to_string(mptAlice.issuanceID()));
|
||||
stream[jss::mpt_issuances].append(to_string(mptCarol.issuanceID()));
|
||||
auto jv = wsc->invoke("subscribe", stream);
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
|
||||
// bob create MPToken
|
||||
mptAlice.authorize({.account = bob});
|
||||
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
|
||||
return jv[jss::engine_result] == "tesSUCCESS" &&
|
||||
jv[jss::transaction][jss::Account] == bob.human() &&
|
||||
jv[jss::transaction][jss::Flags] == 0 &&
|
||||
jv[jss::transaction][sfMPTokenIssuanceID.jsonName] ==
|
||||
to_string(mptAlice.issuanceID()) &&
|
||||
jv[jss::transaction][jss::TransactionType] == "MPTokenAuthorize" &&
|
||||
jv[jss::type] == "mptTransaction";
|
||||
}));
|
||||
|
||||
// dan create MPToken
|
||||
mptCarol.authorize({.account = dan});
|
||||
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
|
||||
return jv[jss::engine_result] == "tesSUCCESS" &&
|
||||
jv[jss::transaction][jss::Account] == dan.human() &&
|
||||
jv[jss::transaction][jss::Flags] == 0 &&
|
||||
jv[jss::transaction][sfMPTokenIssuanceID.jsonName] ==
|
||||
to_string(mptCarol.issuanceID()) &&
|
||||
jv[jss::transaction][jss::Sequence] == 5 &&
|
||||
jv[jss::transaction][jss::TransactionType] == "MPTokenAuthorize" &&
|
||||
jv[jss::type] == "mptTransaction";
|
||||
}));
|
||||
|
||||
// subscribe stream sees alice's MPT
|
||||
mptAlice.pay(alice, bob, 2000);
|
||||
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
|
||||
return jv[jss::engine_result] == "tesSUCCESS" &&
|
||||
jv[jss::transaction][jss::Account] == alice.human() &&
|
||||
jv[jss::transaction][jss::Amount][jss::mpt_issuance_id] ==
|
||||
to_string(mptAlice.issuanceID()) &&
|
||||
jv[jss::transaction][jss::Amount][jss::value] == "2000" &&
|
||||
jv[jss::transaction][jss::DeliverMax][jss::mpt_issuance_id] ==
|
||||
to_string(mptAlice.issuanceID()) &&
|
||||
jv[jss::transaction][jss::DeliverMax][jss::value] == "2000" &&
|
||||
jv[jss::transaction][jss::Destination] == bob.human() &&
|
||||
jv[jss::transaction][jss::Flags] == tfFullyCanonicalSig &&
|
||||
jv[jss::transaction][jss::TransactionType] == "Payment" &&
|
||||
jv[jss::type] == "mptTransaction";
|
||||
}));
|
||||
|
||||
// subscribe stream sees carol's MPT
|
||||
mptCarol.pay(carol, dan, 1000);
|
||||
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
|
||||
return jv[jss::engine_result] == "tesSUCCESS" &&
|
||||
jv[jss::transaction][jss::Account] == carol.human() &&
|
||||
jv[jss::transaction][jss::Amount][jss::mpt_issuance_id] ==
|
||||
to_string(mptCarol.issuanceID()) &&
|
||||
jv[jss::transaction][jss::Amount][jss::value] == "1000" &&
|
||||
jv[jss::transaction][jss::DeliverMax][jss::mpt_issuance_id] ==
|
||||
to_string(mptCarol.issuanceID()) &&
|
||||
jv[jss::transaction][jss::DeliverMax][jss::value] == "1000" &&
|
||||
jv[jss::transaction][jss::Destination] == dan.human() &&
|
||||
jv[jss::transaction][jss::Flags] == tfFullyCanonicalSig &&
|
||||
jv[jss::transaction][jss::Sequence] == 6 &&
|
||||
jv[jss::transaction][jss::TransactionType] == "Payment" &&
|
||||
jv[jss::type] == "mptTransaction";
|
||||
}));
|
||||
|
||||
// subscribe stream sees alice's MPT lock
|
||||
mptAlice.set({.account = alice, .flags = tfMPTLock});
|
||||
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
|
||||
return jv[jss::engine_result] == "tesSUCCESS" &&
|
||||
jv[jss::transaction][jss::Account] == alice.human() &&
|
||||
jv[jss::transaction][jss::Flags] == tfMPTLock &&
|
||||
jv[jss::transaction][sfMPTokenIssuanceID.jsonName] ==
|
||||
to_string(mptAlice.issuanceID()) &&
|
||||
jv[jss::transaction][jss::TransactionType] == "MPTokenIssuanceSet" &&
|
||||
jv[jss::type] == "mptTransaction";
|
||||
}));
|
||||
|
||||
// subscribe stream sees alice's MPT unlock
|
||||
mptAlice.set({.account = alice, .flags = tfMPTUnlock});
|
||||
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
|
||||
return jv[jss::engine_result] == "tesSUCCESS" &&
|
||||
jv[jss::transaction][jss::Account] == alice.human() &&
|
||||
jv[jss::transaction][jss::Flags] == tfMPTUnlock &&
|
||||
jv[jss::transaction][sfMPTokenIssuanceID.jsonName] ==
|
||||
to_string(mptAlice.issuanceID()) &&
|
||||
jv[jss::transaction][jss::TransactionType] == "MPTokenIssuanceSet" &&
|
||||
jv[jss::type] == "mptTransaction";
|
||||
}));
|
||||
|
||||
// unsub alice's MPT from the stream
|
||||
json::Value unsubStream;
|
||||
unsubStream = json::ValueType::Object;
|
||||
unsubStream[jss::mpt_issuances] = json::ValueType::Array;
|
||||
unsubStream[jss::mpt_issuances].append(to_string(mptAlice.issuanceID()));
|
||||
auto unsubJv = wsc->invoke("unsubscribe", unsubStream);
|
||||
BEAST_EXPECT(unsubJv[jss::status] == "success");
|
||||
|
||||
// bob pays alice, this txn should no longer be sent by the stream
|
||||
mptAlice.pay(bob, alice, 500);
|
||||
|
||||
// we don't expect to find alice's mpt in the stream
|
||||
BEAST_EXPECT(!wsc->findMsg(5s, [&](auto const& jv) {
|
||||
return jv[jss::transaction][jss::Amount][jss::mpt_issuance_id] ==
|
||||
to_string(mptAlice.issuanceID()) &&
|
||||
jv[jss::transaction][jss::Amount][jss::value] == "500";
|
||||
}));
|
||||
|
||||
// this txn should be seen
|
||||
mptCarol.pay(dan, carol, 100);
|
||||
|
||||
// only carol's MPT txn will be seen
|
||||
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
|
||||
return jv[jss::engine_result] == "tesSUCCESS" &&
|
||||
jv[jss::transaction][jss::Account] == dan.human() &&
|
||||
jv[jss::transaction][jss::Amount][jss::mpt_issuance_id] ==
|
||||
to_string(mptCarol.issuanceID()) &&
|
||||
jv[jss::transaction][jss::Amount][jss::value] == "100" &&
|
||||
jv[jss::transaction][jss::DeliverMax][jss::mpt_issuance_id] ==
|
||||
to_string(mptCarol.issuanceID()) &&
|
||||
jv[jss::transaction][jss::DeliverMax][jss::value] == "100" &&
|
||||
jv[jss::transaction][jss::Destination] == carol.human() &&
|
||||
jv[jss::transaction][jss::Flags] == tfFullyCanonicalSig &&
|
||||
jv[jss::transaction][jss::TransactionType] == "Payment" &&
|
||||
jv[jss::type] == "mptTransaction";
|
||||
}));
|
||||
}
|
||||
|
||||
void
|
||||
testSubMPTBatch()
|
||||
{
|
||||
// MPTs destroyed by the inner transactions of a Batch are still seen by
|
||||
// the stream. An MPTokenIssuance entry does not carry its own issuance
|
||||
// id, so this exercises deriving the id from the metadata.
|
||||
testcase("SubMPT batch");
|
||||
using namespace jtx;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
Env env(*this);
|
||||
|
||||
Account const alice{"alice"};
|
||||
|
||||
auto wsc = makeWSClient(env.app().config());
|
||||
|
||||
// A Batch requires at least two inner transactions, so destroy two
|
||||
// issuances at once.
|
||||
MPTTester mptAlice1(env, alice, MPTInit{});
|
||||
mptAlice1.create({.ownerCount = 1});
|
||||
auto const mptID1 = mptAlice1.issuanceID();
|
||||
|
||||
MPTTester mptAlice2(env, alice, MPTInit{.fund = false});
|
||||
mptAlice2.create({.ownerCount = 2});
|
||||
auto const mptID2 = mptAlice2.issuanceID();
|
||||
|
||||
json::Value stream;
|
||||
stream = json::ValueType::Object;
|
||||
stream[jss::mpt_issuances] = json::ValueType::Array;
|
||||
stream[jss::mpt_issuances].append(to_string(mptID1));
|
||||
stream[jss::mpt_issuances].append(to_string(mptID2));
|
||||
auto const jv = wsc->invoke("subscribe", stream);
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
|
||||
// destroy both issuances from inside a Batch
|
||||
auto const seq = env.seq(alice);
|
||||
auto const batchFee = batch::calcBatchFee(env, 0, 2);
|
||||
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
|
||||
batch::Inner(mptAlice1.destroyJV({.issuer = alice, .id = mptID1}), seq + 1),
|
||||
batch::Inner(mptAlice2.destroyJV({.issuer = alice, .id = mptID2}), seq + 2));
|
||||
env.close();
|
||||
|
||||
// each inner transaction is published on the stream for its own issuance
|
||||
for (auto const& mptID : {mptID1, mptID2})
|
||||
{
|
||||
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
|
||||
return jv[jss::engine_result] == "tesSUCCESS" &&
|
||||
jv[jss::transaction][jss::Account] == alice.human() &&
|
||||
jv[jss::transaction][sfMPTokenIssuanceID.jsonName] == to_string(mptID) &&
|
||||
jv[jss::transaction][jss::TransactionType] == "MPTokenIssuanceDestroy" &&
|
||||
jv[jss::type] == "mptTransaction";
|
||||
}));
|
||||
}
|
||||
|
||||
// the outer Batch transaction is not itself published on the stream;
|
||||
// only the inner transactions touch the issuances
|
||||
BEAST_EXPECT(!wsc->findMsg(1s, [](auto const& jv) {
|
||||
return jv[jss::transaction][jss::TransactionType] == "Batch";
|
||||
}));
|
||||
|
||||
json::Value unsubStream;
|
||||
unsubStream = json::ValueType::Object;
|
||||
unsubStream[jss::mpt_issuances] = json::ValueType::Array;
|
||||
unsubStream[jss::mpt_issuances].append(to_string(mptID1));
|
||||
unsubStream[jss::mpt_issuances].append(to_string(mptID2));
|
||||
BEAST_EXPECT(wsc->invoke("unsubscribe", unsubStream)[jss::status] == "success");
|
||||
}
|
||||
|
||||
void
|
||||
testSubMPTAmountFields()
|
||||
{
|
||||
testcase("SubMPT amount fields");
|
||||
using namespace jtx;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
Env env(*this);
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
MPTTester mpt(env, alice, {.holders = {bob, carol}});
|
||||
mpt.create({.pay = {{{bob}, 100}}, .flags = kMptDexFlags | tfMPTCanEscrow});
|
||||
|
||||
auto wsc = makeWSClient(env.app().config());
|
||||
auto const subscribed =
|
||||
wsc->invoke("subscribe", mptIssuancesRequest({to_string(mpt.issuanceID())}));
|
||||
BEAST_EXPECT(subscribed[jss::status] == "success");
|
||||
|
||||
auto expectTransaction = [&](std::string const& type) {
|
||||
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
|
||||
return jv[jss::engine_result] == "tesSUCCESS" &&
|
||||
jv[jss::transaction][jss::TransactionType] == type &&
|
||||
jv[jss::type] == "mptTransaction";
|
||||
}));
|
||||
};
|
||||
|
||||
// Created offers carry the issuance only inside their amount fields.
|
||||
env(offer(bob, XRP(10), mpt(10)));
|
||||
env.close();
|
||||
expectTransaction("OfferCreate");
|
||||
|
||||
env(offer(carol, mpt(5), XRP(10)));
|
||||
env.close();
|
||||
expectTransaction("OfferCreate");
|
||||
|
||||
// Escrow entries similarly carry the issuance in sfAmount.
|
||||
auto const baseFee = env.current()->fees().base;
|
||||
env(escrow::create(bob, carol, mpt(10)),
|
||||
escrow::kFinishTime(env.now() + 1s),
|
||||
Fee(baseFee * 150));
|
||||
env.close();
|
||||
expectTransaction("EscrowCreate");
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
@@ -2471,9 +1985,6 @@ public:
|
||||
testSubBookChanges();
|
||||
testNFToken(all);
|
||||
testNFToken(all - featureNFTokenMintOffer);
|
||||
testSubMPT();
|
||||
testSubMPTBatch();
|
||||
testSubMPTAmountFields();
|
||||
testAsyncTeardownDoesNotStall();
|
||||
testResubscribeAfterDisconnect();
|
||||
testSubscriptionCapRejects();
|
||||
@@ -2482,10 +1993,6 @@ public:
|
||||
testMultiFieldNoPartialSubscribe();
|
||||
testHistoryReSubscribeNotOvercounted();
|
||||
testHistoryCapRejectsNetNew();
|
||||
testMPTCapRejects();
|
||||
testMPTReSubscribeNotOvercounted();
|
||||
testMPTSharesCapWithAccounts();
|
||||
testMPTUnsubscribeFreesCap();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ TEST(MPTokenTests, BuilderSettersRoundTrip)
|
||||
auto const issuerKeyMirrorEpochValue = canonical_UINT32();
|
||||
auto const auditorKeyMirrorEpochValue = canonical_UINT32();
|
||||
auto const holderEncryptionKeyValue = canonical_VL();
|
||||
auto const recoveryKeyValue = canonical_VL();
|
||||
|
||||
MPTokenBuilder builder{
|
||||
accountValue,
|
||||
@@ -55,7 +54,6 @@ TEST(MPTokenTests, BuilderSettersRoundTrip)
|
||||
builder.setIssuerKeyMirrorEpoch(issuerKeyMirrorEpochValue);
|
||||
builder.setAuditorKeyMirrorEpoch(auditorKeyMirrorEpochValue);
|
||||
builder.setHolderEncryptionKey(holderEncryptionKeyValue);
|
||||
builder.setRecoveryKey(recoveryKeyValue);
|
||||
|
||||
builder.setLedgerIndex(index);
|
||||
builder.setFlags(0x1u);
|
||||
@@ -176,14 +174,6 @@ TEST(MPTokenTests, BuilderSettersRoundTrip)
|
||||
EXPECT_TRUE(entry.hasHolderEncryptionKey());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = recoveryKeyValue;
|
||||
auto const actualOpt = entry.getRecoveryKey();
|
||||
ASSERT_TRUE(actualOpt.has_value());
|
||||
expectEqualField(expected, *actualOpt, "sfRecoveryKey");
|
||||
EXPECT_TRUE(entry.hasRecoveryKey());
|
||||
}
|
||||
|
||||
EXPECT_TRUE(entry.hasLedgerIndex());
|
||||
auto const ledgerIndex = entry.getLedgerIndex();
|
||||
ASSERT_TRUE(ledgerIndex.has_value());
|
||||
@@ -212,7 +202,6 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip)
|
||||
auto const issuerKeyMirrorEpochValue = canonical_UINT32();
|
||||
auto const auditorKeyMirrorEpochValue = canonical_UINT32();
|
||||
auto const holderEncryptionKeyValue = canonical_VL();
|
||||
auto const recoveryKeyValue = canonical_VL();
|
||||
|
||||
auto sle = std::make_shared<SLE>(MPToken::entryType, index);
|
||||
|
||||
@@ -231,7 +220,6 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip)
|
||||
sle->at(sfIssuerKeyMirrorEpoch) = issuerKeyMirrorEpochValue;
|
||||
sle->at(sfAuditorKeyMirrorEpoch) = auditorKeyMirrorEpochValue;
|
||||
sle->at(sfHolderEncryptionKey) = holderEncryptionKeyValue;
|
||||
sle->at(sfRecoveryKey) = recoveryKeyValue;
|
||||
|
||||
MPTokenBuilder builderFromSle{sle};
|
||||
EXPECT_TRUE(builderFromSle.validate());
|
||||
@@ -422,19 +410,6 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip)
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfHolderEncryptionKey");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = recoveryKeyValue;
|
||||
|
||||
auto const fromSleOpt = entryFromSle.getRecoveryKey();
|
||||
auto const fromBuilderOpt = entryFromBuilder.getRecoveryKey();
|
||||
|
||||
ASSERT_TRUE(fromSleOpt.has_value());
|
||||
ASSERT_TRUE(fromBuilderOpt.has_value());
|
||||
|
||||
expectEqualField(expected, *fromSleOpt, "sfRecoveryKey");
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfRecoveryKey");
|
||||
}
|
||||
|
||||
EXPECT_EQ(entryFromSle.getKey(), index);
|
||||
EXPECT_EQ(entryFromBuilder.getKey(), index);
|
||||
}
|
||||
@@ -517,7 +492,5 @@ TEST(MPTokenTests, OptionalFieldsReturnNullopt)
|
||||
EXPECT_FALSE(entry.getAuditorKeyMirrorEpoch().has_value());
|
||||
EXPECT_FALSE(entry.hasHolderEncryptionKey());
|
||||
EXPECT_FALSE(entry.getHolderEncryptionKey().has_value());
|
||||
EXPECT_FALSE(entry.hasRecoveryKey());
|
||||
EXPECT_FALSE(entry.getRecoveryKey().has_value());
|
||||
}
|
||||
}
|
||||
-258
@@ -1,258 +0,0 @@
|
||||
// Auto-generated unit tests for transaction ConfidentialMPTHolderKeyUpdate
|
||||
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <protocol_autogen/TestHelpers.h>
|
||||
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Seed.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol_autogen/transactions/ConfidentialMPTHolderKeyUpdate.h>
|
||||
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::transactions {
|
||||
|
||||
// 1 & 4) Set fields via builder setters, build, then read them back via
|
||||
// wrapper getters. After build(), validate() should succeed.
|
||||
TEST(TransactionsConfidentialMPTHolderKeyUpdateTests, BuilderSettersRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTHolderKeyUpdate"));
|
||||
|
||||
// Common transaction fields
|
||||
auto const accountValue = calcAccountID(publicKey);
|
||||
std::uint32_t const sequenceValue = 1;
|
||||
auto const feeValue = canonical_AMOUNT();
|
||||
|
||||
// Transaction-specific field values
|
||||
auto const mPTokenIssuanceIDValue = canonical_UINT192();
|
||||
auto const holderEncryptionKeyValue = canonical_VL();
|
||||
auto const confidentialBalanceSpendingValue = canonical_VL();
|
||||
auto const confidentialBalanceInboxValue = canonical_VL();
|
||||
auto const zKProofValue = canonical_VL();
|
||||
|
||||
ConfidentialMPTHolderKeyUpdateBuilder builder{
|
||||
accountValue,
|
||||
mPTokenIssuanceIDValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
// Set optional fields
|
||||
builder.setHolderEncryptionKey(holderEncryptionKeyValue);
|
||||
builder.setConfidentialBalanceSpending(confidentialBalanceSpendingValue);
|
||||
builder.setConfidentialBalanceInbox(confidentialBalanceInboxValue);
|
||||
builder.setZKProof(zKProofValue);
|
||||
|
||||
auto tx = builder.build(publicKey, secretKey);
|
||||
|
||||
std::string reason;
|
||||
EXPECT_TRUE(tx.validate(reason)) << reason;
|
||||
|
||||
// Verify signing was applied
|
||||
EXPECT_FALSE(tx.getSigningPubKey().empty());
|
||||
EXPECT_TRUE(tx.hasTxnSignature());
|
||||
|
||||
// Verify common fields
|
||||
EXPECT_EQ(tx.getAccount(), accountValue);
|
||||
EXPECT_EQ(tx.getSequence(), sequenceValue);
|
||||
EXPECT_EQ(tx.getFee(), feeValue);
|
||||
|
||||
// Verify required fields
|
||||
{
|
||||
auto const& expected = mPTokenIssuanceIDValue;
|
||||
auto const actual = tx.getMPTokenIssuanceID();
|
||||
expectEqualField(expected, actual, "sfMPTokenIssuanceID");
|
||||
}
|
||||
|
||||
// Verify optional fields
|
||||
{
|
||||
auto const& expected = holderEncryptionKeyValue;
|
||||
auto const actualOpt = tx.getHolderEncryptionKey();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfHolderEncryptionKey should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfHolderEncryptionKey");
|
||||
EXPECT_TRUE(tx.hasHolderEncryptionKey());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = confidentialBalanceSpendingValue;
|
||||
auto const actualOpt = tx.getConfidentialBalanceSpending();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfConfidentialBalanceSpending should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfConfidentialBalanceSpending");
|
||||
EXPECT_TRUE(tx.hasConfidentialBalanceSpending());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = confidentialBalanceInboxValue;
|
||||
auto const actualOpt = tx.getConfidentialBalanceInbox();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfConfidentialBalanceInbox should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfConfidentialBalanceInbox");
|
||||
EXPECT_TRUE(tx.hasConfidentialBalanceInbox());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = zKProofValue;
|
||||
auto const actualOpt = tx.getZKProof();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfZKProof should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfZKProof");
|
||||
EXPECT_TRUE(tx.hasZKProof());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
|
||||
// and verify all fields match.
|
||||
TEST(TransactionsConfidentialMPTHolderKeyUpdateTests, BuilderFromStTxRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTHolderKeyUpdateFromTx"));
|
||||
|
||||
// Common transaction fields
|
||||
auto const accountValue = calcAccountID(publicKey);
|
||||
std::uint32_t const sequenceValue = 2;
|
||||
auto const feeValue = canonical_AMOUNT();
|
||||
|
||||
// Transaction-specific field values
|
||||
auto const mPTokenIssuanceIDValue = canonical_UINT192();
|
||||
auto const holderEncryptionKeyValue = canonical_VL();
|
||||
auto const confidentialBalanceSpendingValue = canonical_VL();
|
||||
auto const confidentialBalanceInboxValue = canonical_VL();
|
||||
auto const zKProofValue = canonical_VL();
|
||||
|
||||
// Build an initial transaction
|
||||
ConfidentialMPTHolderKeyUpdateBuilder initialBuilder{
|
||||
accountValue,
|
||||
mPTokenIssuanceIDValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
initialBuilder.setHolderEncryptionKey(holderEncryptionKeyValue);
|
||||
initialBuilder.setConfidentialBalanceSpending(confidentialBalanceSpendingValue);
|
||||
initialBuilder.setConfidentialBalanceInbox(confidentialBalanceInboxValue);
|
||||
initialBuilder.setZKProof(zKProofValue);
|
||||
|
||||
auto initialTx = initialBuilder.build(publicKey, secretKey);
|
||||
|
||||
// Create builder from existing STTx
|
||||
ConfidentialMPTHolderKeyUpdateBuilder builderFromTx{initialTx.getSTTx()};
|
||||
|
||||
auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
|
||||
|
||||
std::string reason;
|
||||
EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
|
||||
|
||||
// Verify common fields
|
||||
EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
|
||||
EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
|
||||
EXPECT_EQ(rebuiltTx.getFee(), feeValue);
|
||||
|
||||
// Verify required fields
|
||||
{
|
||||
auto const& expected = mPTokenIssuanceIDValue;
|
||||
auto const actual = rebuiltTx.getMPTokenIssuanceID();
|
||||
expectEqualField(expected, actual, "sfMPTokenIssuanceID");
|
||||
}
|
||||
|
||||
// Verify optional fields
|
||||
{
|
||||
auto const& expected = holderEncryptionKeyValue;
|
||||
auto const actualOpt = rebuiltTx.getHolderEncryptionKey();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfHolderEncryptionKey should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfHolderEncryptionKey");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = confidentialBalanceSpendingValue;
|
||||
auto const actualOpt = rebuiltTx.getConfidentialBalanceSpending();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfConfidentialBalanceSpending should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfConfidentialBalanceSpending");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = confidentialBalanceInboxValue;
|
||||
auto const actualOpt = rebuiltTx.getConfidentialBalanceInbox();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfConfidentialBalanceInbox should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfConfidentialBalanceInbox");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = zKProofValue;
|
||||
auto const actualOpt = rebuiltTx.getZKProof();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfZKProof should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfZKProof");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 3) Verify wrapper throws when constructed from wrong transaction type.
|
||||
TEST(TransactionsConfidentialMPTHolderKeyUpdateTests, WrapperThrowsOnWrongTxType)
|
||||
{
|
||||
// Build a valid transaction of a different type
|
||||
auto const [pk, sk] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
|
||||
auto const account = calcAccountID(pk);
|
||||
|
||||
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
|
||||
auto wrongTx = wrongBuilder.build(pk, sk);
|
||||
|
||||
EXPECT_THROW(ConfidentialMPTHolderKeyUpdate{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 4) Verify builder throws when constructed from wrong transaction type.
|
||||
TEST(TransactionsConfidentialMPTHolderKeyUpdateTests, BuilderThrowsOnWrongTxType)
|
||||
{
|
||||
// Build a valid transaction of a different type
|
||||
auto const [pk, sk] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
|
||||
auto const account = calcAccountID(pk);
|
||||
|
||||
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
|
||||
auto wrongTx = wrongBuilder.build(pk, sk);
|
||||
|
||||
EXPECT_THROW(ConfidentialMPTHolderKeyUpdateBuilder{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 5) Build with only required fields and verify optional fields return nullopt.
|
||||
TEST(TransactionsConfidentialMPTHolderKeyUpdateTests, OptionalFieldsReturnNullopt)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTHolderKeyUpdateNullopt"));
|
||||
|
||||
// Common transaction fields
|
||||
auto const accountValue = calcAccountID(publicKey);
|
||||
std::uint32_t const sequenceValue = 3;
|
||||
auto const feeValue = canonical_AMOUNT();
|
||||
|
||||
// Transaction-specific required field values
|
||||
auto const mPTokenIssuanceIDValue = canonical_UINT192();
|
||||
|
||||
ConfidentialMPTHolderKeyUpdateBuilder builder{
|
||||
accountValue,
|
||||
mPTokenIssuanceIDValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
// Do NOT set optional fields
|
||||
|
||||
auto tx = builder.build(publicKey, secretKey);
|
||||
|
||||
// Verify optional fields are not present
|
||||
EXPECT_FALSE(tx.hasHolderEncryptionKey());
|
||||
EXPECT_FALSE(tx.getHolderEncryptionKey().has_value());
|
||||
EXPECT_FALSE(tx.hasConfidentialBalanceSpending());
|
||||
EXPECT_FALSE(tx.getConfidentialBalanceSpending().has_value());
|
||||
EXPECT_FALSE(tx.hasConfidentialBalanceInbox());
|
||||
EXPECT_FALSE(tx.getConfidentialBalanceInbox().has_value());
|
||||
EXPECT_FALSE(tx.hasZKProof());
|
||||
EXPECT_FALSE(tx.getZKProof().has_value());
|
||||
}
|
||||
|
||||
}
|
||||
Loaded 100 of 115 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user