Compare commits

..
Author SHA1 Message Date
Pratik Mankawde 60d3b0082f style: Compare connection_ to nullptr explicitly 2026-10-02 17:15:09 +01:00
Pratik Mankawde f4e5897e7c Merge branch 'develop'
Resolve the PeerReservationTable.h conflict with develop's naming and
member-init changes. Use insertOrAssign in the new error text, and add the
direct includes that misc-include-cleaner needs for the new Throw, JLOG
and std::runtime_error uses.
2026-10-02 16:34:21 +01:00
Pratik Mankawde e2ee835952 minor naming change
Signed-off-by: Pratik Mankawde <[email protected]>
2026-02-24 15:24:15 +00:00
Pratik Mankawde f1723d8647 more checks
Signed-off-by: Pratik Mankawde <[email protected]>
2026-02-24 15:17:13 +00:00
Pratik Mankawde 320f9c8866 Merge branch 'develop' into pratik/Add_checks_db_objects
Signed-off-by: Pratik Mankawde <[email protected]>
2026-02-24 14:39:24 +00:00
Pratik Mankawde 229df04edd code review comments
Signed-off-by: Pratik Mankawde <[email protected]>
2026-02-24 14:33:22 +00:00
Pratik Mankawde 640428a1d4 Merge branch 'develop' into pratik/Add_checks_db_objects 2025-12-11 16:31:16 +00:00
Pratik Mankawde 0363c12b23 added checks in SQLiteDatabase.cpp 2025-12-11 16:29:59 +00:00
115 changed files with 1297 additions and 4944 deletions

No files matched your search

+1 -1
View File
@@ -166,6 +166,7 @@ words:
- legleux
- levelization
- levelized
- lgrdb
- libpb
- libxrpl
- llection
@@ -255,7 +256,6 @@ words:
- queuable
- Raphson
- rcflags
- reencrypted
- replayer
- repodata
- repomd
-4
View File
@@ -8,7 +8,3 @@ watch_file rust-toolchain.toml
watch_dir conan
use flake
# Optional, untracked local overrides. To use a different shell, put e.g.
# `use flake .#formal-verification` in .envrc.local.
source_env_if_exists .envrc.local
-12
View File
@@ -5,18 +5,6 @@
# This file is sorted in reverse chronological order, with the most recent commits at the top.
# The commits listed here are ignored by git blame, which is useful for formatting-only commits that would otherwise obscure the history of changes to a file.
# chore: CamelCase for typedef/using in `clang-tidy` (#8177)
97a1824537d20d82d25bf151a37a7a4532ebf085
# chore: Rename CamelCase namespaces to snake_case (#7933)
06488c1318d96f56d0536251bee08ac85fa7fdd3
# style: Unify style for all Doxygen comments (#7776)
73b6852a122854140336e6e6bc30a3a4b41aa5fd
# style: More clang-tidy identifier renaming (#7290)
a830ab10efed8d3e59ef2fc15d66efdf9c6bb0d8
# refactor: Rename static constants (#7120)
5b6e8b6f93b19c1e3f6a3467a25639031d9d9a53
# chore: More fixes for bad renames (#7092)
7afdd71a54d562b32a50b29a5aa00bb997dc9053
# refactor: Enable clang-tidy `readability-identifier-naming` check (#6571)
8995564ed6b9e453e144bb663303072a3c1ba305
# refactor: Enable remaining clang-tidy `cppcoreguidelines` checks (#6538)
+1 -1
View File
@@ -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
-12
View File
@@ -1,12 +0,0 @@
# shellcheck shell=bash
# Helpers shared by the release checks in this directory, see RELEASING.md.
# Sets BRANCHES to all release and staging branches, and fails if there are none.
load_release_branches() {
mapfile -t BRANCHES < <(git for-each-ref --format='%(refname)' 'refs/remotes/*/release/*' 'refs/remotes/*/staging/*')
if [ "${#BRANCHES[@]}" -eq 0 ]; then
echo "Error: No release or staging branches found."
exit 1
fi
}
+2 -23
View File
@@ -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
+3 -12
View File
@@ -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:
-3
View File
@@ -92,9 +92,6 @@ target/
# Direnv's directory
/.direnv
# Direnv's local, per-developer overrides
/.envrc.local
# clangd cache
/.cache
-8
View File
@@ -22,14 +22,6 @@ API version 2 is available in `xrpld` version 2.0.0 and later. See [API-VERSION-
This version is supported by all `xrpld` versions. For WebSocket and HTTP JSON-RPC requests, it is currently the default API version used when no `api_version` is specified.
## XRP Ledger server version 3.5.0
Version 3.5.0 is not yet released.
### Additions in 3.5.0
- `subscribe`, `unsubscribe`: Added an optional `mpt_issuances` request field, an array of MPT issuance IDs (hex strings). Subscribers receive a message with `type` `mptTransaction` for each validated transaction whose metadata affects a subscribed issuance; the message has the same fields as the `transactions` stream. MPT issuance subscriptions count toward the per-connection subscription limit. An empty array, a non-array value, or an invalid ID returns `invalidParams`. ([#5671](https://github.com/XRPLF/rippled/pull/5671))
## XRP Ledger server version 3.4.0
Version 3.4.0 is not yet released. These changes are available in the 3.4.0 beta releases.
+5 -3
View File
@@ -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
View File
@@ -14,13 +14,9 @@ The following branches exist in the main project repository:
- `develop`: The latest set of unreleased features, and the most common
starting point for contributions.
- `staging/*` (e.g. `staging/3.4.x`): Staging branches, one per release line,
where fixes for that line are developed.
- `release/*` (e.g. `release/3.4.x`): Release branches, one per release line,
holding the latest public release candidate or release for that line.
Releases are published as [tagged releases](https://github.com/XRPLF/rippled/releases).
See [RELEASING.md](./RELEASING.md) for how these branches are used.
- `release/*` (e.g. `release/3.2.x`): Release branches, one per release line,
holding the latest release candidate, or stable release for that line.
Stable releases are published as [tagged releases](https://github.com/XRPLF/rippled/releases).
The tip of each branch must be signed. In order for GitHub to sign a
squashed commit that it builds from your pull request, GitHub must know
@@ -149,8 +145,8 @@ tl;dr
In general, pull requests use `develop` as the base branch.
The exceptions are fixes for an existing release line,
which use that line's staging branch (e.g. `staging/3.4.x`) as the base.
The exceptions are fixes, improvements, and hotfixes for an existing release,
which use that release's branch (e.g. `release/3.2.x`) as the base.
If your changes are not quite ready, but you want to make it easily available
for preliminary examination or review, you can create a "Draft" pull request.
@@ -595,16 +591,15 @@ the suggested commit message, or modify it as needed.
#### Slightly more complicated pull requests
Some pull requests need to be pushed to their base branch (usually `develop`)
as more than one commit.
A PR author may _request_ to merge as separate commits. They
Some pull requests need to be pushed to `develop` as more than one
commit. A PR author may _request_ to merge as separate commits. They
must _justify_ why separate commits are needed, and _specify_ how they
would like the commits to be merged. If you disagree with the author,
discuss it with them directly.
If the process is reasonable, follow it. The simplest option is to do a
fast forward only merge (`--ff-only`) on the command line
and push to the base branch.
fast forward only merge (`--ff-only`) on the command line and push to
`develop`.
Some examples of when separate commits are worthwhile are:
@@ -617,10 +612,9 @@ Some examples of when separate commits are worthwhile are:
Either way, check that:
- The commits are based on the current tip of the base branch.
- The commits are clean:
No merge commits (except when merging a release, see [RELEASING.md](./RELEASING.md)),
no "[FOLD]" or "fixup!" messages.
- The commits are based on the current tip of `develop`.
- The commits are clean: No merge commits (except when reverse
merging), no "[FOLD]" or "fixup!" messages.
- All commits are signed. If the commits are not signed by the author, use
`git commit --amend -S` to sign them yourself.
- At least one (but preferably all) of the commits has the PR number
@@ -632,8 +626,578 @@ use them!**
### Releases
Releases, release branches, and merging releases back into `develop`
are described in [RELEASING.md](./RELEASING.md).
All releases, including release candidates and betas, are handled
differently from typical PRs. Most importantly, never use
the Github UI to merge a release.
Xrpld uses a linear workflow model that can be summarized as:
1. In between releases, developers work against the `develop` branch.
2. Periodically, a maintainer will build and tag a beta version from
`develop`, which is pushed to `release`.
- Betas are usually released every two to three weeks, though that
schedule can vary depending on progress, availability, and other
factors.
3. When the changes in `develop` are considered stable and mature enough
to be ready to release, a release candidate (RC) is built and tagged
from `develop`, and merged to `release`.
- Further development for that release (primarily fixes) then
continues against `release`, while other development continues on
`develop`. Effectively, `release` is forked from `develop`. Changes
to `release` must be reverse merged to `develop`.
4. When the candidate has passed testing and is ready for release, the
final release is merged to `master`.
5. If any issues are found post-release, a hotfix / point release may be
created, which is merged to `master`, and then reverse merged to
`develop`.
#### Betas, and the first release candidate
##### Preparing the `develop` branch
1. Optimally, the `develop` branch will be ready to go, with all
relevant PRs already merged.
2. If there are any PRs pending, merge them **BEFORE** preparing the beta.
1. If only one or two PRs need to be merged, merge those PRs [as
normal](#when-and-how-to-merge-pull-requests), updating the second
one, and waiting for CI to finish in between.
2. If there are several pending PRs, do not use the Github UI,
because the delays waiting for CI in between each merge will be
unnecessarily onerous. (Incidentally, this process can also be
used to merge if the Github UI has issues.) Merge each PR branch
directly to a `release-next` on your local machine and create a single
PR, then push your branch to `develop`.
1. Squash the changes from each PR, one commit each (unless more
are needed), being sure to sign each commit and update the
commit message to include the PR number. You may be able to use
a fast-forward merge for the first PR.
2. Push your branch.
3. Continue to [Making the release](#making-the-release) to update
the version number, etc.
The workflow may look something like:
```
git fetch --multiple upstreams user1 user2 user3 [...]
git checkout -B release-next --no-track upstream/develop
# Only do an ff-only merge if pr-branch1 is either already
# squashed, or needs to be merged with separate commits,
# and has no merge commits.
# Use -S on the ff-only merge if pr-branch1 isn't signed.
git merge [-S] --ff-only user1/pr-branch1
git merge --squash user2/pr-branch2
git commit -S # Use the commit message provided on the PR
git merge --squash user3/pr-branch3
git commit -S # Use the commit message provided on the PR
[...]
# Make sure the commits look right
git log --show-signature "upstream/develop..HEAD"
git push --set-upstream origin
# Continue to "Making the release" to update the version number, so
# everything can be done in one PR.
```
You can also use the [squash-branches] script.
You may also need to manually close the open PRs after the changes are
merged to `develop`. Be sure to include the commit ID.
##### Making the release
This includes, betas, and the first release candidate (RC).
1. If you didn't create one [preparing the `develop`
branch](#preparing-the-develop-branch), Ensure there is no old
`release-next` branch hanging around. Then make a `release-next`
branch that only changes the version number. e.g.
```
git fetch upstreams
git checkout --no-track -B release-next upstream/develop
v="A.B.C-bD"
build=$( find -name BuildInfo.cpp )
sed 's/\(^.*versionString =\).*$/\1 "'${v}'"/' ${build} > version.cpp && mv -vi version.cpp ${build}
git diff
git add ${build}
git commit -S -m "Set version to ${v}"
# You could use your "origin" repo, but some CI tests work better on upstream.
git push upstream-push
git fetch upstreams
git branch --set-upstream-to=upstream/release-next
```
You can also use the [update-version] script. 2. Create a Pull Request for `release-next` with **`develop`** as
the base branch.
1. Use the title "[TRIVIAL] Set version to X.X.X-bX".
2. Instead of the default description template, use the following:
```
## High Level Overview of Change
This PR only changes the version number. It will be merged as
soon as Github CI actions successfully complete.
```
3. Wait for CI to successfully complete, and get someone to approve
the PR. (It is safe to ignore known CI issues.)
4. Push the updated `develop` branch using your `release-next`
branch. **Do not use the Github UI. It's important to preserve
commit IDs.**
```
git push upstream-push release-next:develop
```
5. In the unlikely event that the push fails because someone has merged
something else in the meantime, rebase your branch onto the updated
`develop` branch, push again, and go back to step 3.
6. Ensure that your PR against `develop` is closed. Github should do it
automatically.
7. Once this is done, forward progress on `develop` can continue
(other PRs may be merged).
8. Now create a Pull Request for `release-next` with **`release`** as
the base branch. Instead of the default template, reuse and update
the message from the previous release. Include the following verbiage
somewhere in the description:
```
The base branch is `release`. [All releases (including
betas)](https://github.com/XRPLF/rippled/blob/develop/CONTRIBUTING.md#before-you-start)
go in `release`. This PR branch will be pushed directly to `release` (not
squashed or rebased, and not using the GitHub UI).
```
7. Sign-offs for the three platforms (Linux, Mac, Windows) usually occur
offline, but at least one approval will be needed on the PR.
- If issues are discovered during testing, simply abandon the
release. It's easy to start a new release, it should be easy to
abandon one. **DO NOT REUSE THE VERSION NUMBER.** e.g. If you
abandon 2.4.0-b1, the next attempt will be 2.4.0-b2.
8. Once everything is ready to go, push to `release`.
```
git fetch upstreams
# Just to be safe, do a dry run first:
git push --dry-run upstream-push release-next:release
# If everything looks right, push the branch
git push upstream-push release-next:release
# Check that all of the branches are updated
git fetch upstreams
git log -1 --oneline
# The output should look like:
# 0123456789 (HEAD -> upstream/release-next, upstream/release,
# upstream/develop) Set version to 2.4.0-b1
# Note that upstream/develop may not be on this commit, but
# upstream/release must be.
# Other branches, including some from upstream-push, may also be
# present.
```
9. Tag the release, too.
```
git tag <version number>
git push upstream-push <version number>
```
10. Delete the `release-next` branch on the repo. Use the Github UI or:
```
git push --delete upstream-push release-next
```
11. Finally [create a new release on
Github](https://github.com/XRPLF/rippled/releases).
#### Release candidates after the first
Once the first release candidate is [merged into
release](#making-the-release), then `release` and `develop` _are allowed
to diverge_.
If a bug or issue is discovered in a version that has a release
candidate being tested, any fix and new version will need to be applied
against `release`, then reverse-merged to `develop`. This helps keep git
history as linear as possible.
A `release-next` branch will be created from `release`, and any further
work for that release must be based on `release-next`. Specifically,
PRs must use `release-next` as the base, and those PRs will be merged
directly to `release-next` when approved. Changes should be restricted
to bug fixes, but other changes may be necessary from time to time.
1. Open any PRs for the pending release using `release-next` as the base,
so they can be merged directly in to it. Unlike `develop`, though,
`release-next` can be thrown away and recreated if necessary.
2. Once a new release candidate is ready, create a version commit as in
step 1 [above](#making-the-release) on `release-next`. You can use
the [update-version] script for this, too.
3. Jump to step 8 ("Now create a Pull Request for `release-next` with
**`release`** as the base") from the process
[above](#making-the-release) to merge `release-next` into `release`.
##### Follow up: reverse merge
Once the RC is merged and tagged, it needs to be reverse merged into
`develop` as soon as possible.
1. Create a branch, based on `upstream/develop`.
The branch name is not important, but could include "mergeNNNrcN".
E.g. For release A.B.C-rcD, use `mergeABCrcD`.
```
git fetch upstreams
git checkout --no-track -b mergeABCrcD upstream/develop
```
2. Merge `release` into your branch.
```
# I like the "--edit --log --verbose" parameters, but they are
# not required.
git merge upstream/release
```
3. `BuildInfo.cpp` will have a conflict with the version number.
Resolve it with the version from `develop` - the higher version.
4. Push your branch to your repo (or `upstream` if you have permission),
and open a normal PR against `develop`. The "High level overview" can
simply indicate that this is a merge of the RC. The "Context" should
summarize the changes from the RC. Include the following text
prominently:
```
This PR must be merged manually using a push. Do not use the Github UI.
```
5. Depending on the complexity of the changes, and/or merge conflicts,
the PR may need a thorough review, or just a sign-off that the
merge was done correctly.
6. If `develop` is updated before this PR is merged, do not merge
`develop` back into your branch. Instead rebase preserving merges,
or do the merge again. (See also the `rerere` git config setting.)
```
git rebase --rebase-merges upstream/develop
# OR
git reset --hard upstream/develop
git merge upstream/release
```
7. When the PR is ready, push it to `develop`.
```
git fetch upstreams
# Make sure the commits look right
git log --show-signature "upstream/develop^..HEAD"
git push upstream-push mergeABCrcD:develop
git fetch upstreams
```
Development on `develop` can proceed as normal.
#### Final releases
A final release is any release that is not a beta or RC, such as 2.2.0.
Only code that has already been tested and vetted across all three
platforms should be included in a final release. Most of the time, that
means that the commit immediately preceding the commit setting the
version number will be an RC. Occasionally, there may be last-minute bug
fixes included as well. If so, those bug fixes must have been tested
internally as if they were RCs (at minimum, ensuring unit tests pass,
and the app starts, syncs, and stops cleanly across all three
platforms.)
_If in doubt, make an RC first._
The process for building a final release is very similar to [the process
for building a beta](#making-the-release), except the code will be
moving from `release` to `master` instead of from `develop` to
`release`, and both branches will be pushed at the same time.
1. Ensure there is no old `master-next` branch hanging around.
Then make a `master-next` branch that only changes the version
number. As above, or using the
[update-version] script.
2. Create a Pull Request for `master-next` with **`master`** as
the base branch. Instead of the default template, reuse and update
the message from the previous final release. Include the following verbiage
somewhere in the description:
```
The base branch is `master`. This PR branch will be pushed directly to
`release` and `master` (not squashed or rebased, and not using the
GitHub UI).
```
7. Sign-offs for the three platforms (Linux, Mac, Windows) usually occur
offline, but at least one approval will be needed on the PR.
- If issues are discovered during testing, close the PR, delete
`master-next`, and move development back to `release`, [issuing
more RCs as necessary](#release-candidates-after-the-first)
8. Once everything is ready to go, push to `release` and `master`.
```
git fetch upstreams
# Just to be safe, do dry runs first:
git push --dry-run upstream-push master-next:release
git push --dry-run upstream-push master-next:master
# If everything looks right, push the branch
git push upstream-push master-next:release
git push upstream-push master-next:master
# Check that all of the branches are updated
git fetch upstreams
git log -1 --oneline
# The output should look like:
# 0123456789 (HEAD -> upstream/master-next, upstream/master,
# upstream/release) Set version to A.B.0
# Note that both upstream/release and upstream/master must be on this
# commit.
# Other branches, including some from upstream-push, may also be
# present.
```
9. Tag the release, too.
```
git tag <version number>
git push upstream-push <version number>
```
10. Delete the `master-next` branch on the repo. Use the Github UI or:
```
git push --delete upstream-push master-next
```
11. [Create a new release on
Github](https://github.com/XRPLF/rippled/releases). Be sure that
"Set as the latest release" is checked.
12. Open a PR to update the [API-CHANGELOG](API-CHANGELOG.md) and `API-VERSION-[n].md` with the changes for this release (if any are missing).
13. Finally, [reverse merge the release into `develop`](#follow-up-reverse-merge).
#### Special cases: point releases, hotfixes, etc.
On occasion, a bug or issue is discovered in a version that already
had a final release. Most of the time, development will have started
on the next version, and will usually have changes in `develop`
and often in `release`.
Because git history is kept as linear as possible, any fix and new
version will need to be applied against `master`.
The process for building a hotfix release is very similar to [the
process for building release candidates after the
first](#release-candidates-after-the-first) and [for building a final
release](#final-releases), except the changes will be done against
`master` instead of `release`.
If there is only a single issue for the hotfix, the work can be done in
any branch. When it's ready to merge, jump to step 3 using your branch
instead of `master-next`.
1. Create a `master-next` branch from `master`.
```
git checkout --no-track -b master-next upstream/master
git push upstream-push
git fetch upstreams
```
2. Open any PRs for the pending hotfix using `master-next` as the base,
so they can be merged directly in to it. Unlike `develop`, though,
`master-next` can be thrown away and recreated if necessary.
3. Once the hotfix is ready, create a version commit using the same
steps as above, or use the
[update-version] script.
4. Create a Pull Request for `master-next` with **`master`** as
the base branch. Instead of the default template, reuse and update
the message from the previous final release. Include the following verbiage
somewhere in the description:
```
The base branch is `master`. This PR branch will be pushed directly to
`master` (not squashed or rebased, and not using the GitHub UI).
```
7. Sign-offs for the three platforms (Linux, Mac, Windows) usually occur
offline, but at least one approval will be needed on the PR.
- If issues are discovered during testing, update `master-next` as
needed, but ensure that the changes are properly squashed, and the
version setting commit remains last
8. Once everything is ready to go, push to `master` **only**.
```
git fetch upstreams
# Just to be safe, do a dry run first:
git push --dry-run upstream-push master-next:master
# If everything looks right, push the branch
git push upstream-push master-next:master
# Check that all of the branches are updated
git fetch upstreams
git log -1 --oneline
# The output should look like:
# 0123456789 (HEAD -> upstream/master-next, upstream/master) Set version
# to 2.4.1
# Note that upstream/master must be on this commit. upstream/release and
# upstream/develop should not.
# Other branches, including some from upstream-push, may also be
# present.
```
9. Tag the release, too.
```
git tag <version number>
git push upstream-push <version number>
```
9. Delete the `master-next` branch on the repo.
```
git push --delete upstream-push master-next
```
10. [Create a new release on
Github](https://github.com/XRPLF/rippled/releases). Be sure that
"Set as the latest release" is checked.
Once the hotfix is released, it needs to be reverse merged into
`develop` as soon as possible. It may also need to be merged into
`release` if a release candidate is under development.
1. Create a branch in your own repo, based on `upstream/develop`.
The branch name is not important, but could include "mergeNNN".
E.g. For release 2.2.3, use `merge223`.
```
git fetch upstreams
git checkout --no-track -b merge223 upstream/develop
```
2. Merge master into your branch.
```
# I like the "--edit --log --verbose" parameters, but they are
# not required.
git merge upstream/master
```
3. `BuildInfo.cpp` will have a conflict with the version number.
Resolve it with the version from `develop` - the higher version.
4. Push your branch to your repo, and open a normal PR against
`develop`. The "High level overview" can simply indicate that this
is a merge of the hotfix version. The "Context" should summarize
the changes from the hotfix. Include the following text
prominently:
```
This PR must be merged manually using a --ff-only merge. Do not use the Github UI.
```
5. Depending on the complexity of the hotfix, and/or merge conflicts,
the PR may need a thorough review, or just a sign-off that the
merge was done correctly.
6. If `develop` is updated before this PR is merged, do not merge
`develop` back into your branch. Instead rebase preserving merges,
or do the merge again. (See also the `rerere` git config setting.)
```
git rebase --rebase-merges upstream/develop
# OR
git reset --hard upstream/develop
git merge upstream/master
```
7. When the PR is ready, push it to `develop`.
```
git fetch upstreams
# Make sure the commits look right
git log --show-signature "upstream/develop..HEAD"
git push upstream-push HEAD:develop
```
Development on `develop` can proceed as normal. It is recommended to
create a beta (or RC) immediately to ensure that everything worked as
expected.
##### An even rarer scenario: A hotfix on an old release
Historically, once a final release is tagged and packages are released,
versions older than the latest final release are no longer supported.
However, there is a possibility that a very high severity bug may occur
in a non-amendment blocked version that is still being run by
a significant fraction of users, which would necessitate a hotfix / point
release to that version as well as any later versions.
This scenario would follow the same basic procedure as above,
except that _none_ of `develop`, `release`, or `master`
would be touched during the release process.
In this example, consider if version 2.1.1 needed to be patched.
1. Create two branches in the main (`upstream`) repo.
```
git fetch upstreams
# Create a base branch off the tag
git checkout --no-track -b master-2.1.2 2.1.1
git push upstream-push
# Create a working branch
git checkout --no-track -b master212-next master-2.1.2
git push upstream-push
git fetch upstreams
```
2. Work continues as above, except using `master-2.1.2`as
the base branch for any merging, packaging, etc.
3. After the release is tagged and packages are built, you could
potentially delete both branches, e.g. `master-2.1.2` and
`master212-next`. However, it may be useful to keep `master-2.1.2`
around indefinitely for reference.
4. Assuming that a hotfix is also released for the latest
version in parallel with this one, or if the issue is
already fixed in the latest version, do no do any
reverse merges. However, if it is not, it probably makes
sense to reverse merge `master-2.1.2` into `master`,
release a hotfix for _that_ version, then reverse merge
from `master` to `develop`. (Please don't do this unless absolutely
necessary.)
[contrib]: https://docs.github.com/en/get-started/quickstart/contributing-to-projects
[squash]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-commits
@@ -641,3 +1205,5 @@ are described in [RELEASING.md](./RELEASING.md).
[xrpld]: https://github.com/XRPLF/rippled
[signing]: https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification
[setup-upstreams]: ./bin/git/setup-upstreams.sh
[squash-branches]: ./bin/git/squash-branches.sh
[update-version]: ./bin/git/update-version.sh
-152
View File
@@ -1,152 +0,0 @@
# Branching and Release Management
This document describes how we branch, release, and merge releases back into `develop`.
It does not define version naming
(e.g., what constitutes a major-minor, patch, or beta release).
Examples use `X.Y` for a release line:
`X` and `Y` are placeholders, while the trailing `x` is literal,
e.g., `release/X.Y.x` is `release/3.4.x` for the `3.4` line.
## Branches
| Branch | Purpose |
| :-------------- | :---------------------------------------------------------------------------------------- |
| `develop` | Main development branch. Betas and the first RC of a major-minor release are tagged here. |
| `staging/X.Y.x` | Where fixes for the `X.Y` line are developed and RCs are prepared. |
| `release/X.Y.x` | The last public RC or release of the `X.Y` line. |
A release line is named `X.Y.x`
because one branch serves every patch release of that line (`X.Y.0`, `X.Y.1`, `X.Y.2`, ...).
The `/` groups branches hierarchically,
so tools can filter them and protection rules can target `release/*` and `staging/*`.
## Principles
- **Releases are merged back into `develop`, never cherry-picked or rebased onto it.**
Cherry-picked and rebased commits get new hashes,
so Git can't tell that `develop` already has them.
Future merges then replay them and produce artificial conflicts,
and it's hard to verify that every fix actually reached `develop`.
Merging keeps a single history:
Git knows exactly which release commits `develop` contains,
and no fix is left behind.
- **Branches only move forward.**
`develop`, `staging/X.Y.x`, and `release/X.Y.x` are never rewritten.
- **Cherry-picking only goes from `develop` to a staging branch**,
for fixes that must get into a release after the code freeze
(see [Emergency Fixes From `develop`](#emergency-fixes-from-develop)).
- **Security fixes are prepared privately and published with the release that contains them**,
so vulnerabilities are not disclosed prematurely.
## Release Lifecycle
This diagram shows a major-minor release and its first patch release.
The staging and release branches are drawn as one line.
```text
develop staging/X.Y.x & release/X.Y.x
│
├── Tag: X.Y.0-b1
├── Tag: X.Y.0-bN
├── Tag: X.Y.0-rc1 ───────────────┐ (branches created)
│ │
│ (development continues) ├── Fixes
│ ├── Tag: X.Y.0-rcN
│ ├── Tag: X.Y.0 (final)
◀──── (merge) ────────────────────┤
│ ├── Fixes
│ ├── Tag: X.Y.1-rcN
│ ├── Tag: X.Y.1 (final)
◀──── (merge) ────────────────────┤
│
▼
```
### Betas, First RC & Branching
> [!NOTE]
> This phase applies only to a new major-minor release (e.g., `X.Y.0`).
> Patch releases work on the existing branches of the line.
1. **Betas:** All beta versions (e.g., `X.Y.0-b1`) are built and tagged directly on `develop`.
2. **First RC:** We release the first RC (`X.Y.0-rc1`)
once everything that should be included in the release has been merged.
3. **Branches:** `staging/X.Y.x` and `release/X.Y.x` are created from `develop`
at the commit tagged `X.Y.0-rc1`.
The first RC also kicks off the QE process.
4. **Code freeze:** No new features or unrelated changes are pulled from `develop`
into `staging/X.Y.x` or `release/X.Y.x`.
Only critical stabilization fixes go into the line.
5. **No large changes on `develop`:** Until `X.Y.0` is [merged back](#merging-back-into-develop),
large changes (e.g., big refactors, moving or renaming many files, mass reformatting)
are not merged into `develop`,
so that fixes on the line and the merge back don't run into conflicts.
### Release Candidates
1. Fixes are developed against `staging/X.Y.x`.
2. When ready, a new RC is created on `staging/X.Y.x`,
and `release/X.Y.x` is fast-forwarded to it.
RCs that contain unpublished security fixes are not published,
and don't touch the public branches.
RCs are not merged back into `develop`:
`X.Y.0-rc1` is tagged on `develop` itself,
and all later changes reach `develop` with the [final release](#final-release).
### Final Release
Security fixes become public as soon as they reach the public repo,
so these steps happen only once the release is ready to be published,
one right after the other.
1. Unpublished security fixes, if any, are merged into `staging/X.Y.x`.
2. `release/X.Y.x` is fast-forwarded to `staging/X.Y.x`,
and the release is tagged on it.
3. The release is immediately [merged back into `develop`](#merging-back-into-develop).
For a major-minor release, this lifts the freeze on large changes in `develop`.
### Merging Back Into `develop`
The merge back is a regular PR into `develop`
whose branch contains a real merge commit of the release tag:
1. Create a branch from `develop`, run `git merge --no-ff <tag>`, and resolve any conflicts.
2. Once the PR is approved, `develop` is fast-forwarded to the PR branch,
so the merge commit lands as it is.
Never squash or rebase it.
Never add it to the merge queue either: the queue squashes PRs, which would drop the merge commit.
3. If `develop` moves while the PR is open, redo the merge on top of the new `develop`.
After the merge, `git log develop..<tag>` must be empty.
## Special Cases
### Emergency Fixes From `develop`
If a commit was merged to `develop`
and needs to be included in a release after the code freeze:
1. Create a PR that cherry-picks the commit onto `staging/X.Y.x`.
2. Leave `develop` as it is, with no reverts.
3. Follow the [release candidates](#release-candidates) process as usual.
When the release is later merged back into `develop`,
both sides already contain the same change,
so the merge usually resolves it cleanly or with a trivial conflict.
From Git's perspective, the cherry-picked commit then becomes part of `develop` too.
### Several Supported Lines
When a fix must ship in more than one supported line (e.g., `X.Y` and `X.(Y+1)`),
the lines are merged upwards rather than cherry-picked between:
1. The fix goes into the oldest line first.
2. After that line's release,
its `release/X.Y.x` is merged into the staging branch of the next newer line,
and so on up to the newest line.
3. The newest line is merged back into `develop` as usual.
This way every newer line, and eventually `develop`, contains the history of the older lines.
-12
View File
@@ -24,13 +24,9 @@
# `g++-15`, ...) are probed under both names: a suffixed name can break while
# the plain one still works (see mkVersionedToolLinks in nix/packages.nix).
#
# Tools scoped to a single dev shell rather than to commonPackages are checked
# only in that shell, keyed off XRPL_DEVSHELL.
#
# Environment variables:
# CI if set, skip the tools above when on macOS.
# CHECK_TOOLS_SKIP_CLONE if set, skip the git-over-HTTPS connectivity check.
# XRPL_DEVSHELL active dev shell; selects shell-specific tools.
set -uo pipefail
@@ -167,14 +163,6 @@ if [ "${os}" = "linux" ] || [ "${os}" = "macos" ]; then
check rustfmt
fi
# Lean4 is in the formal-verification shell only, not in commonPackages.
if [ "${XRPL_DEVSHELL:-}" = "formal-verification" ]; then
echo
echo "Formal verification toolchain:"
check lean
check lake
fi
# GCC is the default compiler on Linux. macOS uses the system Apple Clang
# instead, so GCC/g++/gcov are not expected there.
if [ "${os}" = "linux" ]; then
+12 -12
View File
@@ -110,10 +110,10 @@ static_assert(
*
* However, it does not have sufficient precision to represent the full integer
* range of int64_t values (-2^63 to 2^63-1), which are needed for XRP and MPT
* values. The implementation of SingleAssetVault, LendingProtocol, and
* MPTokensV2 need to represent those integer values accurately and precisely,
* both for the STNumber field type, and for internal calculations. That
* necessitated the "large" scale.
* values. The implementation of SingleAssetVault, and LendingProtocol need to
* represent those integer values accurately and precisely, both for the
* STNumber field type, and for internal calculations. That necessitated the
* "large" scale.
*
* The "Large" scales are intended to represent all values that can be represented
* by an STAmount - IOUs, XRP, and MPTs. It has a min value of 10^18, and a max
@@ -134,8 +134,8 @@ struct MantissaRange final
// NOLINTBEGIN(readability-enum-initial-value)
// The values don't matter, except for Large
enum class MantissaScale {
// Small can be removed when any of featureSingleAssetVault, featureLendingProtocol, or
// featureMPTokensV2 are retired
// Small can be removed when either featureSingleAssetVault or featureLendingProtocol are
// retired
Small,
// LargeLegacy can be removed when fixCleanup3_2_0 is retired
LargeLegacy,
@@ -311,10 +311,10 @@ concept Integral64 = std::is_same_v<T, std::int64_t> || std::is_same_v<T, std::u
*
* The mantissa range may be changed at runtime via setMantissaScale(). The
* default mantissa range is "large". The range is updated whenever transaction
* processing begins, based on whether SingleAssetVault, LendingProtocol, or
* MPTokensV2 are enabled. If any is enabled, the mantissa range is set to
* "large". If not, it is set to "small", preserving backward compatibility and
* correct "amendment-gating".
* processing begins, based on whether SingleAssetVault or LendingProtocol are
* enabled. If either is enabled, the mantissa range is set to "large". If not,
* it is set to "small", preserving backward compatibility and correct
* "amendment-gating".
*
* It is extremely unlikely that any more calls to setMantissaScale() will be
* needed outside of unit tests.
@@ -344,8 +344,8 @@ concept Integral64 = std::is_same_v<T, std::int64_t> || std::is_same_v<T, std::u
* set/getMantissaScale() functions may be most appropriate. However, if the
* test has anything to do with transaction processing, it should enable or
* disable the amendments that control the mantissa range choice
* (SingleAssetVault, LendingProtocol, and MPTokensV2), and/or check if any of
* those amendments are enabled to determine which result to expect.
* (SingleAssetVault and LendingProtocol), and/or check if either of those
* amendments are enabled to determine which result to expect.
*/
class Number final
{
+4
View File
@@ -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>;
+1 -2
View File
@@ -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);
-6
View File
@@ -238,12 +238,6 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal;
TF_FLAG(tfSponsorshipEnd, 0x00010000) \
TF_FLAG(tfSponsorshipCreate, 0x00020000) \
TF_FLAG(tfSponsorshipReassign, 0x00040000), \
MASK_ADJ(0)) \
\
TRANSACTION(ConfidentialMPTHolderKeyUpdate, \
TF_FLAG(tfHolderKeyRotation, 0x00010000) \
TF_FLAG(tfHolderKeyRecovery, 0x00020000) \
TF_FLAG(tfCancelRecovery, 0x00040000), \
MASK_ADJ(0))
// clang-format on
-5
View File
@@ -12,12 +12,10 @@
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/UintTypes.h>
#include <boost/container/flat_set.hpp>
#include <cstdint>
#include <flat_set>
#include <optional>
namespace xrpl {
@@ -117,9 +115,6 @@ public:
parentBatchID_ = id;
}
[[nodiscard]] std::flat_set<MPTID>
getAffectedMPTs() const;
private:
UInt256 transactionID_;
std::uint32_t ledgerSeq_;
@@ -439,7 +439,6 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
{sfIssuerKeyMirrorEpoch, SoeOptional},
{sfAuditorKeyMirrorEpoch, SoeOptional},
{sfHolderEncryptionKey, SoeOptional},
{sfRecoveryKey, SoeOptional},
}))
/** A ledger object which tracks Oracle
@@ -330,7 +330,6 @@ TYPED_SFIELD(sfAuditorEncryptionKey, VL, 44)
TYPED_SFIELD(sfAmountCommitment, VL, 45)
TYPED_SFIELD(sfBalanceCommitment, VL, 46)
TYPED_SFIELD(sfBytecode, VL, 47)
TYPED_SFIELD(sfRecoveryKey, VL, 48)
// account (common)
TYPED_SFIELD(sfAccount, ACCOUNT, 1)
@@ -1147,27 +1147,12 @@ TRANSACTION(ttCONFIDENTIAL_MPT_MIRROR_UPDATE, 92, ConfidentialMPTMirrorUpdate,
{sfZKProof, SoeRequired},
}))
/** This transaction rotates or recovers a confidential MPT holder's ElGamal encryption key,
or cancels a recovery. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/token/ConfidentialMPTHolderKeyUpdate.h>
#endif
TRANSACTION(ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE, 93, ConfidentialMPTHolderKeyUpdate,
({.amendment = featureConfidentialMPTKeyRotation}),
({
{sfMPTokenIssuanceID, SoeRequired},
{sfHolderEncryptionKey, SoeOptional},
{sfConfidentialBalanceSpending, SoeOptional},
{sfConfidentialBalanceInbox, SoeOptional},
{sfZKProof, SoeOptional},
}))
/** This transaction posts an unsigned transaction on-ledger as a
TransactionProposal, pending multi-signature collection. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/proposal/TransactionProposalCreate.h>
#endif
TRANSACTION(ttTRANSACTION_PROPOSAL_CREATE, 95, TransactionProposalCreate,
TRANSACTION(ttTRANSACTION_PROPOSAL_CREATE, 93, TransactionProposalCreate,
({.amendment = featureCosign}),
({
{sfProposedTransaction, SoeRequired},
-1
View File
@@ -411,7 +411,6 @@ JSS(minimum_level); // out: TxQ
JSS(missingCommand); // error
JSS(mpt_amount); // out: mpt_holders
JSS(mpt_issuance_id); // in: Payment, mpt_holders
JSS(mpt_issuances); // in: Subscribe, Unsubscribe
JSS(mptoken_index); // out: mpt_holders
JSS(mpt_issuance_id_a); // out: BookChanges
JSS(mpt_issuance_id_b); // out: BookChanges
@@ -339,30 +339,6 @@ public:
{
return this->sle_->isFieldPresent(sfHolderEncryptionKey);
}
/**
* @brief Get sfRecoveryKey (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getRecoveryKey() const
{
if (hasRecoveryKey())
return this->sle_->at(sfRecoveryKey);
return std::nullopt;
}
/**
* @brief Check if sfRecoveryKey is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasRecoveryKey() const
{
return this->sle_->isFieldPresent(sfRecoveryKey);
}
};
/**
@@ -576,17 +552,6 @@ public:
return *this;
}
/**
* @brief Set sfRecoveryKey (SoeOptional)
* @return Reference to this builder for method chaining.
*/
MPTokenBuilder&
setRecoveryKey(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfRecoveryKey] = value;
return *this;
}
/**
* @brief Build and return the completed MPToken wrapper.
* @param index The ledger entry index.
@@ -1,279 +0,0 @@
// This file is auto-generated. Do not edit.
#pragma once
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/TransactionBase.h>
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
#include <xrpl/json/json_value.h>
#include <stdexcept>
#include <optional>
namespace xrpl::transactions {
class ConfidentialMPTHolderKeyUpdateBuilder;
/**
* @brief Transaction: ConfidentialMPTHolderKeyUpdate
*
* Type: ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE (93)
* Delegable: Delegation::NotDelegable
* Amendment: featureConfidentialMPTKeyRotation
* Privileges: Privilege::NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use ConfidentialMPTHolderKeyUpdateBuilder to construct new transactions.
*/
class ConfidentialMPTHolderKeyUpdate : public TransactionBase
{
public:
static constexpr xrpl::TxType txType = ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE;
/**
* @brief Construct a ConfidentialMPTHolderKeyUpdate transaction wrapper from an existing STTx object.
* @throws std::runtime_error if the transaction type doesn't match.
*/
explicit ConfidentialMPTHolderKeyUpdate(std::shared_ptr<STTx const> tx)
: TransactionBase(std::move(tx))
{
// Verify transaction type
if (tx_->getTxnType() != txType)
{
throw std::runtime_error("Invalid transaction type for ConfidentialMPTHolderKeyUpdate");
}
}
// Transaction-specific field getters
/**
* @brief Get sfMPTokenIssuanceID (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT192::type::value_type
getMPTokenIssuanceID() const
{
return this->tx_->at(sfMPTokenIssuanceID);
}
/**
* @brief Get sfHolderEncryptionKey (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getHolderEncryptionKey() const
{
if (hasHolderEncryptionKey())
{
return this->tx_->at(sfHolderEncryptionKey);
}
return std::nullopt;
}
/**
* @brief Check if sfHolderEncryptionKey is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasHolderEncryptionKey() const
{
return this->tx_->isFieldPresent(sfHolderEncryptionKey);
}
/**
* @brief Get sfConfidentialBalanceSpending (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getConfidentialBalanceSpending() const
{
if (hasConfidentialBalanceSpending())
{
return this->tx_->at(sfConfidentialBalanceSpending);
}
return std::nullopt;
}
/**
* @brief Check if sfConfidentialBalanceSpending is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasConfidentialBalanceSpending() const
{
return this->tx_->isFieldPresent(sfConfidentialBalanceSpending);
}
/**
* @brief Get sfConfidentialBalanceInbox (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getConfidentialBalanceInbox() const
{
if (hasConfidentialBalanceInbox())
{
return this->tx_->at(sfConfidentialBalanceInbox);
}
return std::nullopt;
}
/**
* @brief Check if sfConfidentialBalanceInbox is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasConfidentialBalanceInbox() const
{
return this->tx_->isFieldPresent(sfConfidentialBalanceInbox);
}
/**
* @brief Get sfZKProof (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getZKProof() const
{
if (hasZKProof())
{
return this->tx_->at(sfZKProof);
}
return std::nullopt;
}
/**
* @brief Check if sfZKProof is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasZKProof() const
{
return this->tx_->isFieldPresent(sfZKProof);
}
};
/**
* @brief Builder for ConfidentialMPTHolderKeyUpdate transactions.
*
* Provides a fluent interface for constructing transactions with method chaining.
* Uses STObject internally for flexible transaction construction.
* Inherits common field setters from TransactionBuilderBase.
*/
class ConfidentialMPTHolderKeyUpdateBuilder : public TransactionBuilderBase<ConfidentialMPTHolderKeyUpdateBuilder>
{
public:
/**
* @brief Construct a new ConfidentialMPTHolderKeyUpdateBuilder with required fields.
* @param account The account initiating the transaction.
* @param mPTokenIssuanceID The sfMPTokenIssuanceID field value.
* @param sequence Optional sequence number for the transaction.
* @param fee Optional fee for the transaction.
*/
ConfidentialMPTHolderKeyUpdateBuilder(SF_ACCOUNT::type::value_type account,
std::decay_t<typename SF_UINT192::type::value_type> const& mPTokenIssuanceID, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
)
: TransactionBuilderBase<ConfidentialMPTHolderKeyUpdateBuilder>(ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE, account, sequence, fee)
{
setMPTokenIssuanceID(mPTokenIssuanceID);
}
/**
* @brief Construct a ConfidentialMPTHolderKeyUpdateBuilder from an existing STTx object.
* @param tx The existing transaction to copy from.
* @throws std::runtime_error if the transaction type doesn't match.
*/
ConfidentialMPTHolderKeyUpdateBuilder(std::shared_ptr<STTx const> tx)
{
if (tx->getTxnType() != ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE)
{
throw std::runtime_error("Invalid transaction type for ConfidentialMPTHolderKeyUpdateBuilder");
}
object_ = *tx;
}
/**
* @brief Transaction-specific field setters
*/
/**
* @brief Set sfMPTokenIssuanceID (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTHolderKeyUpdateBuilder&
setMPTokenIssuanceID(std::decay_t<typename SF_UINT192::type::value_type> const& value)
{
object_[sfMPTokenIssuanceID] = value;
return *this;
}
/**
* @brief Set sfHolderEncryptionKey (SoeOptional)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTHolderKeyUpdateBuilder&
setHolderEncryptionKey(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfHolderEncryptionKey] = value;
return *this;
}
/**
* @brief Set sfConfidentialBalanceSpending (SoeOptional)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTHolderKeyUpdateBuilder&
setConfidentialBalanceSpending(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfConfidentialBalanceSpending] = value;
return *this;
}
/**
* @brief Set sfConfidentialBalanceInbox (SoeOptional)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTHolderKeyUpdateBuilder&
setConfidentialBalanceInbox(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfConfidentialBalanceInbox] = value;
return *this;
}
/**
* @brief Set sfZKProof (SoeOptional)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTHolderKeyUpdateBuilder&
setZKProof(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfZKProof] = value;
return *this;
}
/**
* @brief Build and return the ConfidentialMPTHolderKeyUpdate wrapper.
* @param publicKey The public key for signing.
* @param secretKey The secret key for signing.
* @return The constructed transaction wrapper.
*/
ConfidentialMPTHolderKeyUpdate
build(PublicKey const& publicKey, SecretKey const& secretKey)
{
sign(publicKey, secretKey);
return ConfidentialMPTHolderKeyUpdate{std::make_shared<STTx>(std::move(object_))};
}
};
} // namespace xrpl::transactions
@@ -18,7 +18,7 @@ class TransactionProposalCreateBuilder;
/**
* @brief Transaction: TransactionProposalCreate
*
* Type: ttTRANSACTION_PROPOSAL_CREATE (95)
* Type: ttTRANSACTION_PROPOSAL_CREATE (93)
* Delegable: Delegation::NotDelegable
* Amendment: featureCosign
* Privileges: Privilege::NoPriv
+8 -66
View File
@@ -7,7 +7,6 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Book.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/resource/Consumer.h>
#include <xrpl/server/Manifest.h>
@@ -27,11 +26,11 @@ namespace xrpl {
/**
* Maximum number of subscriptions a single client connection may hold at once.
*
* Applies to the account, real-time account, account-history, and MPT issuance
* subscriptions tracked on one InfoSub (the sets counted by
* totalSubscriptionCount), bounding the disconnect-time cleanup of those sets.
* Book subscriptions are tracked separately (OrderBookDB) and are not counted
* here. Generous enough for legitimate power users such as block explorers.
* Applies to the account, real-time account, and account-history subscriptions
* tracked on one InfoSub (the sets counted by totalSubscriptionCount), bounding
* the disconnect-time cleanup of those sets. Book subscriptions are tracked
* separately (OrderBookDB) and are not counted here. Generous enough for
* legitimate power users such as block explorers.
*/
constexpr std::size_t kMaxSubscriptionsPerConnection = 100'000;
@@ -152,21 +151,6 @@ public:
AccountID const& account,
bool historyOnly) = 0;
/**
* Remove an MPT issuance subscription during InfoSub teardown.
*
* Removes only the server-side entry from subMPT_. Does NOT touch
* InfoSub::mptSubscriptions_ because the InfoSub is being destroyed.
* Called by ~InfoSub() for each issuance in mptSubscriptions_.
*
* @param uListener The sequence number of the subscriber being torn down.
* @param mptID The MPT issuance to remove.
*
* @note Thread-safety: acquires mptLock_ internally.
*/
virtual void
unsubMPTInternal(std::uint64_t uListener, MPTID const& mptID) = 0;
/**
* Schedule the server-side teardown of a disconnecting connection's
* account subscriptions off the destructor thread.
@@ -286,11 +270,6 @@ public:
virtual bool
unsubConsensus(std::uint64_t uListener) = 0;
virtual void
subMPT(InfoSub::Ref ispListener, HashSet<MPTID> const& mptIDs) = 0;
virtual void
unsubMPT(InfoSub::Ref ispListener, HashSet<MPTID> const& mptIDs) = 0;
// VFALCO TODO Remove
// This was added for one particular partner, it
// "pushes" subscription data to a particular URL.
@@ -330,9 +309,9 @@ public:
* Return the number of subscriptions currently tracked on this
* connection.
*
* The combined size of the per-connection account, real-time account,
* account-history, and MPT issuance subscription sets. `doSubscribe` reads
* this to enforce the per-connection subscription cap before admitting more.
* The combined size of the per-connection account, real-time account, and
* account-history subscription sets. `doSubscribe` reads this to enforce
* the per-connection subscription cap before admitting more.
*
* @return The total tracked subscription count for this connection.
*
@@ -363,25 +342,6 @@ public:
HashSet<AccountID> const& normalAccounts,
std::size_t cap);
/**
* Enforce the cap and reserve a request's net-new MPT issuances, atomically.
*
* The MPT analogue of tryReserveAccountSubscriptions: under one hold of
* `lock_`, count the net-new issuances, check the total against @p cap, and
* insert them only if it fits. All-or-nothing, so a rejected request records
* nothing. Doing check and insert together stops two concurrent requests
* sharing an InfoSub (the admin subscribe-by-url path) from both passing the
* check before either records its issuances. The server-side map is
* populated afterwards by subMPT, whose re-insert is a no-op.
*
* @param mptIDs The MPT issuance ids to reserve.
* @param cap The effective per-connection cap.
* @return true if reserved; false if the request must be rejected.
* @note Thread-safe: takes `lock_`.
*/
[[nodiscard]] bool
tryReserveMPTSubscriptions(HashSet<MPTID> const& mptIDs, std::size_t cap);
/**
* Whether this connection already tracks an account-history for @p account.
*
@@ -453,29 +413,12 @@ public:
[[nodiscard]] unsigned int
getApiVersion() const noexcept;
void
insertSubMPTInfo(MPTID const& mptID);
void
deleteSubMPTInfo(MPTID const& mptID);
protected:
// Mutable so the read-only totalSubscriptionCount() accessor can lock it
// from a const method; locking semantics are otherwise unchanged.
mutable std::mutex lock_;
private:
// The lock type guarding this instance's subscription sets.
using ScopedLock = std::scoped_lock<decltype(lock_)>;
/**
* The combined tally the per-connection cap is enforced against.
*
* @param lock Proof that `lock_` is held; unused otherwise.
*/
[[nodiscard]] std::size_t
subscriptionCount(ScopedLock const& lock) const;
Consumer consumer_;
Source& source_;
HashSet<AccountID> realTimeSubscriptions_;
@@ -484,7 +427,6 @@ private:
std::uint64_t seq_;
HashSet<AccountID> accountHistorySubscriptions_;
HashSet<Book> bookSubscriptions_;
HashSet<MPTID> mptSubscriptions_;
unsigned int apiVersion_ = 0;
static int
+2 -8
View File
@@ -46,9 +46,6 @@ class ValidMPTIssuance
* MPTokens and RippleStates deleted during apply. finalize() checks each
* holder's AccountRoot to detect vault pseudo-account holdings deleted
* outside VaultDelete. All these checks are gated on fixCleanup3_2_0.
*
* Under fixCleanup3_5_0, finalize() also rejects any MPToken erased with
* a non-zero sfMPTAmount.
*/
std::vector<std::shared_ptr<SLE const>> deletedHoldings_;
@@ -141,9 +138,8 @@ public:
* - Convert/ConvertBack symmetry:
* Regular MPToken balance change (±X) == COA (Confidential Outstanding Amount) change (∓X)
* - Cannot delete MPToken with non-zero confidential state:
* Cannot delete if any of sfConfidentialBalanceSpending, sfConfidentialBalanceInbox,
* sfIssuerEncryptedBalance or sfAuditorEncryptedBalance is present, and the issuance's
* sfConfidentialOutstandingAmount is non-zero. Mirrors MPTokenAuthorize::preclaim.
* Cannot delete if sfIssuerEncryptedBalance exists
* Cannot delete if sfConfidentialBalanceInbox and sfConfidentialBalanceSpending exist
* - Privacy flag consistency:
* MPToken confidential balance fields can only be created or changed if
* lsfMPTCanHoldConfidentialBalance is set on the issuance.
@@ -166,8 +162,6 @@ class ValidConfidentialMPToken
std::int64_t outstandingDelta = 0;
SLE::const_pointer issuance;
bool deletedWithEncrypted = false;
// True when an erased MPToken had a non-zero pre-tx public balance.
bool deletedWithBalanceBefore = false;
bool badConsistency = false;
bool badCOA = false;
bool changesConfidentialFields = false;
+3 -4
View File
@@ -5,7 +5,6 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/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
-4
View File
@@ -26,10 +26,6 @@ The store paths carry their derivation hash, so they change whenever a tool is
rebuilt — a `flake.lock` update generally rewrites most of them even when no
version moves. That is deliberate: it makes tooling changes visible in review.
Tools scoped to a single dev shell rather than to `commonPackages` do not appear
here: `check-tools.sh` keys those off `XRPL_DEVSHELL`, and none of the three
environments above is such a shell. Adding to one needs no snapshot update.
## Regenerating
The two Linux snapshots come from the `nix-ubuntu` image (Docker or a compatible
-13
View File
@@ -147,19 +147,6 @@ rec {
versionedTools = clangVersionedTools;
};
# The gcc shell plus the Lean4 formal verification toolchain
formal-verification = makeShell {
shellName = "formal-verification";
stdenv = customGccStdenv;
compilerName = "gcc";
version = gccVersion;
versionedTools = gccVersionedTools;
extraPackages = [
customGccGcov
pkgs.lean4
];
};
# Nix provides no compiler; use the one from your system (e.g. Apple Clang).
no-compiler = makeShell {
shellName = "no-compiler";
+4 -6
View File
@@ -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
+2 -4
View File
@@ -5,7 +5,6 @@
#include <cerrno>
#include <cstddef>
#include <filesystem>
#include <format>
#include <fstream>
#include <iomanip>
#include <ios>
@@ -100,13 +99,12 @@ uniqueRandomPath(
if (ec)
{
Throw<std::runtime_error>(
std::format("Unable to check path '{}': {}", candidate.string(), ec.message()));
"Unable to check path '" + candidate.string() + "': " + ec.message());
}
if (!exists)
return candidate;
}
Throw<std::runtime_error>(
std::format("Unable to generate a unique path under '{}'", base.string()));
Throw<std::runtime_error>("Unable to generate a unique path under '" + base.string() + "'");
}
TempDir::TempDir() : path_(uniqueRandomPath(std::filesystem::temp_directory_path()))
+1 -2
View File
@@ -16,7 +16,6 @@
#include <boost/container/pmr/monotonic_buffer_resource.hpp>
#include <cstddef>
#include <format>
#include <memory>
#include <optional>
#include <stdexcept>
@@ -265,7 +264,7 @@ OpenView::rawTxInsert(
auto const result = txs_.emplace(
std::piecewise_construct, std::forward_as_tuple(key), std::forward_as_tuple(txn, metaData));
if (!result.second)
Throw<std::logic_error>(std::format("rawTxInsert: duplicate TX id: {}", to_string(key)));
Throw<std::logic_error>("rawTxInsert: duplicate TX id: " + to_string(key));
}
} // namespace xrpl
+3 -6
View File
@@ -11,7 +11,6 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/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;
+10 -14
View File
@@ -7,7 +7,6 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/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);
+15 -17
View File
@@ -7,7 +7,6 @@
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/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
+2 -3
View File
@@ -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
}
}
+3 -5
View File
@@ -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
+5 -4
View File
@@ -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);
+3 -2
View File
@@ -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());
+1 -2
View File
@@ -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
+3 -2
View File
@@ -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;
+2 -2
View File
@@ -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();
+1 -2
View File
@@ -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;
+3 -3
View File
@@ -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));
}
}
-50
View File
@@ -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)
{
+4 -5
View File
@@ -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);
}
}
+20 -67
View File
@@ -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
+1 -2
View File
@@ -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) + ")";
+2 -3
View File
@@ -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);
+24 -21
View File
@@ -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
+1 -2
View File
@@ -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";
+14 -56
View File
@@ -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 "
+1 -2
View File
@@ -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))
+2 -3
View File
@@ -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))
{
+4 -4
View File
@@ -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
+14 -13
View File
@@ -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";
}
+10 -15
View File
@@ -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());
+2 -2
View File
@@ -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);
+1 -1
View File
@@ -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)));
+35 -16
View File
@@ -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_);
+23 -26
View File
@@ -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
{
+69 -8
View File
@@ -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);
+5 -5
View File
@@ -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);
+1 -4
View File
@@ -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;
-116
View File
@@ -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
View File
@@ -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);
-12
View File
@@ -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.
+7 -8
View File
@@ -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()
{
-21
View File
@@ -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
+1 -2
View File
@@ -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));
}
-23
View File
@@ -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;
+2 -70
View File
@@ -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();
-34
View File
@@ -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();
}
};
-493
View File
@@ -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());
}
}
@@ -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