mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-11 22:28:02 +00:00
Compare commits
9 Commits
pratik/Add
...
bthomee/co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f95f7889f9 | ||
|
|
a9027bb997 | ||
|
|
0a6da4de74 | ||
|
|
6a05339c6c | ||
|
|
0c41a87604 | ||
|
|
f5938e4097 | ||
|
|
c7d10c1b60 | ||
|
|
84e2a155b9 | ||
|
|
cbade49976 |
@@ -166,7 +166,6 @@ words:
|
||||
- legleux
|
||||
- levelization
|
||||
- levelized
|
||||
- lgrdb
|
||||
- libpb
|
||||
- libxrpl
|
||||
- llection
|
||||
|
||||
4
.envrc
4
.envrc
@@ -8,3 +8,7 @@ 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
|
||||
|
||||
2
.github/actions/release-info/action.yml
vendored
2
.github/actions/release-info/action.yml
vendored
@@ -36,4 +36,4 @@ runs:
|
||||
|
||||
- name: Determine release channel and package release
|
||||
id: release_info
|
||||
uses: XRPLF/actions/release-info@ebcf6cea14eee258697308a51fea55cad777581b
|
||||
uses: XRPLF/actions/release-info@a9f2eeca6fb3980ba3a84cf68566f1c69ad30674
|
||||
|
||||
40
.github/scripts/releasing/check-merge-back-commits.sh
vendored
Executable file
40
.github/scripts/releasing/check-merge-back-commits.sh
vendored
Executable file
@@ -0,0 +1,40 @@
|
||||
#!/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 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Exit the script as soon as an error occurs.
|
||||
set -euo pipefail
|
||||
@@ -21,11 +21,10 @@ patch_ids() {
|
||||
git log --no-merges --patch --no-color --no-ext-diff "$@" | git patch-id --stable | sort
|
||||
}
|
||||
|
||||
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
|
||||
SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}")
|
||||
# shellcheck source=.github/scripts/releasing/common.sh
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
load_release_branches
|
||||
|
||||
# Each line is "<patch-id> <commit>".
|
||||
RELEASE_PATCHES=$(patch_ids "${BRANCHES[@]}" --not "${HEAD}")
|
||||
|
||||
38
.github/scripts/releasing/check-no-merge-back.sh
vendored
Executable file
38
.github/scripts/releasing/check-no-merge-back.sh
vendored
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/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 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Exit the script as soon as an error occurs.
|
||||
set -euo pipefail
|
||||
|
||||
12
.github/scripts/releasing/common.sh
vendored
Normal file
12
.github/scripts/releasing/common.sh
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# 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
|
||||
}
|
||||
25
.github/workflows/on-pr.yml
vendored
25
.github/workflows/on-pr.yml
vendored
@@ -166,10 +166,31 @@ 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 || github.event.merge_group.base_sha }}
|
||||
HEAD: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha }}
|
||||
BASE: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD: ${{ github.event.pull_request.head.sha }}
|
||||
run: .github/scripts/releasing/check-no-copied-release-commits.sh "${BASE}" "${HEAD}"
|
||||
# Runs even if the previous check fails, so that both problems are reported at once.
|
||||
- name: Check merge-back has no new commits
|
||||
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
|
||||
env:
|
||||
BASE: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD: ${{ github.event.pull_request.head.sha }}
|
||||
run: .github/scripts/releasing/check-merge-back-commits.sh "${BASE}" "${HEAD}"
|
||||
# The queue squashes PRs, so check the PR's own branch, named in the queue branch.
|
||||
- name: Check the merge queue doesn't merge a release back
|
||||
if: ${{ github.event_name == 'merge_group' }}
|
||||
env:
|
||||
BASE: ${{ github.event.merge_group.base_sha }}
|
||||
HEAD_REF: ${{ github.event.merge_group.head_ref }}
|
||||
run: |
|
||||
if ! [[ "${HEAD_REF}" =~ /pr-([0-9]+)-[0-9a-f]+$ ]]; then
|
||||
echo "Error: Can't find the PR number in '${HEAD_REF}'."
|
||||
exit 1
|
||||
fi
|
||||
git fetch --no-tags origin "refs/pull/${BASH_REMATCH[1]}/head"
|
||||
.github/scripts/releasing/check-no-merge-back.sh "${BASE}" FETCH_HEAD
|
||||
|
||||
clang-tidy:
|
||||
needs: should-run
|
||||
|
||||
15
.github/workflows/reusable-package.yml
vendored
15
.github/workflows/reusable-package.yml
vendored
@@ -269,14 +269,23 @@ 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
.gitignore
vendored
3
.gitignore
vendored
@@ -92,6 +92,9 @@ target/
|
||||
# Direnv's directory
|
||||
/.direnv
|
||||
|
||||
# Direnv's local, per-developer overrides
|
||||
/.envrc.local
|
||||
|
||||
# clangd cache
|
||||
/.cache
|
||||
|
||||
|
||||
@@ -22,6 +22,14 @@ 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.
|
||||
|
||||
8
BUILD.md
8
BUILD.md
@@ -41,15 +41,13 @@ branch.
|
||||
git checkout develop
|
||||
```
|
||||
|
||||
For a release candidate, choose the relevant release branch, e.g.
|
||||
`release/3.2.x`.
|
||||
For a release or release candidate, check out its [tag](https://github.com/XRPLF/rippled/releases), e.g.:
|
||||
|
||||
```bash
|
||||
git checkout release/3.2.x
|
||||
git checkout 3.4.0
|
||||
```
|
||||
|
||||
For a stable release, choose one of the [tagged
|
||||
releases](https://github.com/XRPLF/rippled/releases).
|
||||
See [RELEASING.md](./RELEASING.md) for how branches and releases are organized.
|
||||
|
||||
A build reports `0.0.0-dev` with its short commit hash as build metadata, e.g.
|
||||
`0.0.0-dev+0123abc`. Only builds of a release, from a tag in CI or a versioned
|
||||
|
||||
606
CONTRIBUTING.md
606
CONTRIBUTING.md
@@ -14,9 +14,13 @@ The following branches exist in the main project repository:
|
||||
|
||||
- `develop`: The latest set of unreleased features, and the most common
|
||||
starting point for contributions.
|
||||
- `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).
|
||||
- `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.
|
||||
|
||||
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
|
||||
@@ -145,8 +149,8 @@ tl;dr
|
||||
|
||||
In general, pull requests use `develop` as the base branch.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
@@ -591,15 +595,16 @@ the suggested commit message, or modify it as needed.
|
||||
|
||||
#### Slightly more complicated pull requests
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
`develop`.
|
||||
fast forward only merge (`--ff-only`) on the command line
|
||||
and push to the base branch.
|
||||
|
||||
Some examples of when separate commits are worthwhile are:
|
||||
|
||||
@@ -612,9 +617,10 @@ Some examples of when separate commits are worthwhile are:
|
||||
|
||||
Either way, check that:
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- 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
|
||||
@@ -626,578 +632,8 @@ use them!**
|
||||
|
||||
### Releases
|
||||
|
||||
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.)
|
||||
Releases, release branches, and merging releases back into `develop`
|
||||
are described in [RELEASING.md](./RELEASING.md).
|
||||
|
||||
[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
|
||||
@@ -1205,5 +641,3 @@ git fetch upstreams
|
||||
[xrpld]: https://github.com/XRPLF/rippled
|
||||
[signing]: https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification
|
||||
[setup-upstreams]: ./bin/git/setup-upstreams.sh
|
||||
[squash-branches]: ./bin/git/squash-branches.sh
|
||||
[update-version]: ./bin/git/update-version.sh
|
||||
|
||||
152
RELEASING.md
Normal file
152
RELEASING.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Branching and Release Management
|
||||
|
||||
This document describes how we branch, release, and merge releases back into `develop`.
|
||||
It does not define version naming
|
||||
(e.g., what constitutes a major-minor, patch, or beta release).
|
||||
|
||||
Examples use `X.Y` for a release line:
|
||||
`X` and `Y` are placeholders, while the trailing `x` is literal,
|
||||
e.g., `release/X.Y.x` is `release/3.4.x` for the `3.4` line.
|
||||
|
||||
## Branches
|
||||
|
||||
| Branch | Purpose |
|
||||
| :-------------- | :---------------------------------------------------------------------------------------- |
|
||||
| `develop` | Main development branch. Betas and the first RC of a major-minor release are tagged here. |
|
||||
| `staging/X.Y.x` | Where fixes for the `X.Y` line are developed and RCs are prepared. |
|
||||
| `release/X.Y.x` | The last public RC or release of the `X.Y` line. |
|
||||
|
||||
A release line is named `X.Y.x`
|
||||
because one branch serves every patch release of that line (`X.Y.0`, `X.Y.1`, `X.Y.2`, ...).
|
||||
The `/` groups branches hierarchically,
|
||||
so tools can filter them and protection rules can target `release/*` and `staging/*`.
|
||||
|
||||
## Principles
|
||||
|
||||
- **Releases are merged back into `develop`, never cherry-picked or rebased onto it.**
|
||||
Cherry-picked and rebased commits get new hashes,
|
||||
so Git can't tell that `develop` already has them.
|
||||
Future merges then replay them and produce artificial conflicts,
|
||||
and it's hard to verify that every fix actually reached `develop`.
|
||||
Merging keeps a single history:
|
||||
Git knows exactly which release commits `develop` contains,
|
||||
and no fix is left behind.
|
||||
- **Branches only move forward.**
|
||||
`develop`, `staging/X.Y.x`, and `release/X.Y.x` are never rewritten.
|
||||
- **Cherry-picking only goes from `develop` to a staging branch**,
|
||||
for fixes that must get into a release after the code freeze
|
||||
(see [Emergency Fixes From `develop`](#emergency-fixes-from-develop)).
|
||||
- **Security fixes are prepared privately and published with the release that contains them**,
|
||||
so vulnerabilities are not disclosed prematurely.
|
||||
|
||||
## Release Lifecycle
|
||||
|
||||
This diagram shows a major-minor release and its first patch release.
|
||||
The staging and release branches are drawn as one line.
|
||||
|
||||
```text
|
||||
develop staging/X.Y.x & release/X.Y.x
|
||||
│
|
||||
├── Tag: X.Y.0-b1
|
||||
├── Tag: X.Y.0-bN
|
||||
├── Tag: X.Y.0-rc1 ───────────────┐ (branches created)
|
||||
│ │
|
||||
│ (development continues) ├── Fixes
|
||||
│ ├── Tag: X.Y.0-rcN
|
||||
│ ├── Tag: X.Y.0 (final)
|
||||
◀──── (merge) ────────────────────┤
|
||||
│ ├── Fixes
|
||||
│ ├── Tag: X.Y.1-rcN
|
||||
│ ├── Tag: X.Y.1 (final)
|
||||
◀──── (merge) ────────────────────┤
|
||||
│
|
||||
▼
|
||||
```
|
||||
|
||||
### Betas, First RC & Branching
|
||||
|
||||
> [!NOTE]
|
||||
> This phase applies only to a new major-minor release (e.g., `X.Y.0`).
|
||||
> Patch releases work on the existing branches of the line.
|
||||
|
||||
1. **Betas:** All beta versions (e.g., `X.Y.0-b1`) are built and tagged directly on `develop`.
|
||||
2. **First RC:** We release the first RC (`X.Y.0-rc1`)
|
||||
once everything that should be included in the release has been merged.
|
||||
3. **Branches:** `staging/X.Y.x` and `release/X.Y.x` are created from `develop`
|
||||
at the commit tagged `X.Y.0-rc1`.
|
||||
The first RC also kicks off the QE process.
|
||||
4. **Code freeze:** No new features or unrelated changes are pulled from `develop`
|
||||
into `staging/X.Y.x` or `release/X.Y.x`.
|
||||
Only critical stabilization fixes go into the line.
|
||||
5. **No large changes on `develop`:** Until `X.Y.0` is [merged back](#merging-back-into-develop),
|
||||
large changes (e.g., big refactors, moving or renaming many files, mass reformatting)
|
||||
are not merged into `develop`,
|
||||
so that fixes on the line and the merge back don't run into conflicts.
|
||||
|
||||
### Release Candidates
|
||||
|
||||
1. Fixes are developed against `staging/X.Y.x`.
|
||||
2. When ready, a new RC is created on `staging/X.Y.x`,
|
||||
and `release/X.Y.x` is fast-forwarded to it.
|
||||
|
||||
RCs that contain unpublished security fixes are not published,
|
||||
and don't touch the public branches.
|
||||
|
||||
RCs are not merged back into `develop`:
|
||||
`X.Y.0-rc1` is tagged on `develop` itself,
|
||||
and all later changes reach `develop` with the [final release](#final-release).
|
||||
|
||||
### Final Release
|
||||
|
||||
Security fixes become public as soon as they reach the public repo,
|
||||
so these steps happen only once the release is ready to be published,
|
||||
one right after the other.
|
||||
|
||||
1. Unpublished security fixes, if any, are merged into `staging/X.Y.x`.
|
||||
2. `release/X.Y.x` is fast-forwarded to `staging/X.Y.x`,
|
||||
and the release is tagged on it.
|
||||
3. The release is immediately [merged back into `develop`](#merging-back-into-develop).
|
||||
For a major-minor release, this lifts the freeze on large changes in `develop`.
|
||||
|
||||
### Merging Back Into `develop`
|
||||
|
||||
The merge back is a regular PR into `develop`
|
||||
whose branch contains a real merge commit of the release tag:
|
||||
|
||||
1. Create a branch from `develop`, run `git merge --no-ff <tag>`, and resolve any conflicts.
|
||||
2. Once the PR is approved, `develop` is fast-forwarded to the PR branch,
|
||||
so the merge commit lands as it is.
|
||||
Never squash or rebase it.
|
||||
Never add it to the merge queue either: the queue squashes PRs, which would drop the merge commit.
|
||||
3. If `develop` moves while the PR is open, redo the merge on top of the new `develop`.
|
||||
|
||||
After the merge, `git log develop..<tag>` must be empty.
|
||||
|
||||
## Special Cases
|
||||
|
||||
### Emergency Fixes From `develop`
|
||||
|
||||
If a commit was merged to `develop`
|
||||
and needs to be included in a release after the code freeze:
|
||||
|
||||
1. Create a PR that cherry-picks the commit onto `staging/X.Y.x`.
|
||||
2. Leave `develop` as it is, with no reverts.
|
||||
3. Follow the [release candidates](#release-candidates) process as usual.
|
||||
|
||||
When the release is later merged back into `develop`,
|
||||
both sides already contain the same change,
|
||||
so the merge usually resolves it cleanly or with a trivial conflict.
|
||||
From Git's perspective, the cherry-picked commit then becomes part of `develop` too.
|
||||
|
||||
### Several Supported Lines
|
||||
|
||||
When a fix must ship in more than one supported line (e.g., `X.Y` and `X.(Y+1)`),
|
||||
the lines are merged upwards rather than cherry-picked between:
|
||||
|
||||
1. The fix goes into the oldest line first.
|
||||
2. After that line's release,
|
||||
its `release/X.Y.x` is merged into the staging branch of the next newer line,
|
||||
and so on up to the newest line.
|
||||
3. The newest line is merged back into `develop` as usual.
|
||||
|
||||
This way every newer line, and eventually `develop`, contains the history of the older lines.
|
||||
@@ -24,9 +24,13 @@
|
||||
# `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
|
||||
|
||||
@@ -163,6 +167,14 @@ 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
|
||||
|
||||
@@ -95,10 +95,6 @@ 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_;
|
||||
};
|
||||
|
||||
@@ -12,10 +12,12 @@
|
||||
#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 {
|
||||
@@ -115,6 +117,9 @@ public:
|
||||
parentBatchID_ = id;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::flat_set<MPTID>
|
||||
getAffectedMPTs() const;
|
||||
|
||||
private:
|
||||
UInt256 transactionID_;
|
||||
std::uint32_t ledgerSeq_;
|
||||
|
||||
@@ -411,6 +411,7 @@ 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
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#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>
|
||||
|
||||
@@ -26,11 +27,11 @@ namespace xrpl {
|
||||
/**
|
||||
* Maximum number of subscriptions a single client connection may hold at once.
|
||||
*
|
||||
* 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.
|
||||
* 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.
|
||||
*/
|
||||
constexpr std::size_t kMaxSubscriptionsPerConnection = 100'000;
|
||||
|
||||
@@ -151,6 +152,21 @@ 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.
|
||||
@@ -270,6 +286,11 @@ 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.
|
||||
@@ -309,9 +330,9 @@ public:
|
||||
* Return the number of subscriptions currently tracked on this
|
||||
* connection.
|
||||
*
|
||||
* 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.
|
||||
* 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.
|
||||
*
|
||||
* @return The total tracked subscription count for this connection.
|
||||
*
|
||||
@@ -342,6 +363,25 @@ 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.
|
||||
*
|
||||
@@ -413,12 +453,29 @@ 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_;
|
||||
@@ -427,6 +484,7 @@ private:
|
||||
std::uint64_t seq_;
|
||||
HashSet<AccountID> accountHistorySubscriptions_;
|
||||
HashSet<Book> bookSubscriptions_;
|
||||
HashSet<MPTID> mptSubscriptions_;
|
||||
unsigned int apiVersion_ = 0;
|
||||
|
||||
static int
|
||||
|
||||
@@ -26,6 +26,10 @@ The store paths carry their derivation hash, so they change whenever a tool is
|
||||
rebuilt — a `flake.lock` update generally rewrites most of them even when no
|
||||
version moves. That is deliberate: it makes tooling changes visible in review.
|
||||
|
||||
Tools scoped to a single dev shell rather than to `commonPackages` do not appear
|
||||
here: `check-tools.sh` keys those off `XRPL_DEVSHELL`, and none of the three
|
||||
environments above is such a shell. Adding to one needs no snapshot update.
|
||||
|
||||
## Regenerating
|
||||
|
||||
The two Linux snapshots come from the `nix-ubuntu` image (Docker or a compatible
|
||||
|
||||
@@ -147,6 +147,19 @@ 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";
|
||||
|
||||
@@ -217,12 +217,13 @@ 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_ | `private` | `deb-private` | `rpm-private-hosted` |
|
||||
| tag, non-public codebase | _any but `X.Y.Z`_ | `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. Versions sort in row order, so moving to a
|
||||
Only a tag picks a release channel. A final release, `X.Y.Z`, goes to `stable`
|
||||
even from a non-public codebase. Versions sort in row order, so moving to a
|
||||
more mature channel never downgrades. A tag matching none of the release
|
||||
patterns, such as `X.Y.Z-hotfix1`, publishes to `custom`, which sits outside
|
||||
that order.
|
||||
@@ -284,8 +285,9 @@ 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`. Private builds are never
|
||||
pushed there.
|
||||
secret, an organization access token for `xrplf`. Builds of a non-public
|
||||
codebase are pushed there only for `stable` releases, whose packages are
|
||||
public too.
|
||||
- `voidstar` replaces `/usr/bin/xrpld` with the binary of the `voidstar` build
|
||||
config, adds `libvoidstar.so` and links the binary into `/symbols`, as
|
||||
Antithesis expects. It is pushed as `xrpld-voidstar` to the Antithesis
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#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>
|
||||
@@ -13,10 +15,12 @@
|
||||
#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>
|
||||
|
||||
@@ -145,6 +149,52 @@ 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)
|
||||
{
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#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>
|
||||
@@ -42,6 +44,22 @@ 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.
|
||||
@@ -84,6 +102,16 @@ 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
|
||||
@@ -145,16 +173,21 @@ 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 three sets cannot be mutated
|
||||
// Hold lock_ for the whole read so the counted sets cannot be mutated
|
||||
// mid-count by a concurrent (un)subscribe on this connection.
|
||||
std::scoped_lock const sl(lock_);
|
||||
|
||||
// Combined tally the per-connection cap is enforced against.
|
||||
return normalSubscriptions_.size() + realTimeSubscriptions_.size() +
|
||||
accountHistorySubscriptions_.size();
|
||||
return subscriptionCount(sl);
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -166,25 +199,10 @@ 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_);
|
||||
|
||||
std::size_t const current = normalSubscriptions_.size() + realTimeSubscriptions_.size() +
|
||||
accountHistorySubscriptions_.size();
|
||||
|
||||
if (exceedsSubscriptionCap(current, additional, cap))
|
||||
if (exceedsSubscriptionCap(subscriptionCount(sl), additional, cap))
|
||||
return false;
|
||||
|
||||
realTimeSubscriptions_.insert(proposedAccounts.begin(), proposedAccounts.end());
|
||||
@@ -192,6 +210,19 @@ 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)
|
||||
{
|
||||
@@ -288,4 +319,20 @@ 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
|
||||
|
||||
@@ -122,6 +122,83 @@ 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.
|
||||
*/
|
||||
@@ -335,7 +412,11 @@ port_wss_admin
|
||||
}
|
||||
|
||||
// Config file in HOME or XDG_CONFIG_HOME directory.
|
||||
#if BOOST_OS_LINUX || BOOST_OS_MACOS
|
||||
#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");
|
||||
|
||||
for (auto const& configFile : configFiles)
|
||||
{
|
||||
// Point the current working directory to a temporary directory, so
|
||||
@@ -348,13 +429,11 @@ port_wss_admin
|
||||
{
|
||||
TempDir const tc;
|
||||
|
||||
// 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);
|
||||
// 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());
|
||||
|
||||
// Create the config file in '${XDG_CONFIG_HOME}/[systemName]'.
|
||||
Path p = tc.file(systemName());
|
||||
@@ -371,22 +450,21 @@ port_wss_admin
|
||||
BEAST_EXPECT(
|
||||
c.section(Sections::kDebugLogfile).values()[0] ==
|
||||
"/Users/dummy/xrpld/config/log/debug.log");
|
||||
|
||||
// Restore the environment variables.
|
||||
(h != nullptr) ? setenv("HOME", h, 1) : unsetenv("HOME");
|
||||
(x != nullptr) ? setenv("XDG_CONFIG_HOME", x, 1) : unsetenv("XDG_CONFIG_HOME");
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// 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;
|
||||
|
||||
// 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");
|
||||
// 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);
|
||||
|
||||
// Create the config file in '${HOME}/.config/[systemName]'.
|
||||
std::string s = ".config";
|
||||
@@ -407,14 +485,13 @@ port_wss_admin
|
||||
BEAST_EXPECT(
|
||||
c.section(Sections::kDebugLogfile).values()[0] ==
|
||||
"/Users/dummy/xrpld/config/log/debug.log");
|
||||
|
||||
// Restore the environment variables.
|
||||
(h != nullptr) ? setenv("HOME", h, 1) : unsetenv("HOME");
|
||||
if (x != nullptr)
|
||||
setenv("XDG_CONFIG_HOME", x, 1);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
#endif
|
||||
#endif // _MSC_VER
|
||||
|
||||
// Restore the current working directory.
|
||||
current_path(cwd);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#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>
|
||||
@@ -32,6 +33,7 @@
|
||||
#include <cstddef>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::rpc {
|
||||
@@ -517,6 +519,71 @@ 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()
|
||||
{
|
||||
@@ -625,8 +692,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();
|
||||
|
||||
@@ -1325,6 +1392,7 @@ public:
|
||||
testAccountLines();
|
||||
testAccountLinesMarker();
|
||||
testAccountLineDelete();
|
||||
testMarkerNewObjectTypes();
|
||||
testAccountLinesWalkMarkers();
|
||||
testAccountLines2();
|
||||
testAccountLineDelete2();
|
||||
|
||||
@@ -5,8 +5,12 @@
|
||||
#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
|
||||
@@ -66,10 +70,40 @@ public:
|
||||
BEAST_EXPECT(result.second == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testOwnerDirNodeFieldsCoverage()
|
||||
{
|
||||
// Every UINT64 sf*Node field must be classified: either it is an
|
||||
// owner-directory page hint (rpc::isOwnerDirNodeField()) or it
|
||||
// belongs to a book directory (exclusion set below). Adding a new
|
||||
// sf*Node UINT64 without updating one of these lists trips this
|
||||
// test.
|
||||
testcase("Owner-directory Node fields coverage");
|
||||
|
||||
// Book-directory page hints, not owner-directory. sfBookNode: offer
|
||||
// book. sfNFTokenOfferNode: NFT bid/ask book.
|
||||
std::set<SField const*> const nonOwnerDirNodes{
|
||||
&sfBookNode,
|
||||
&sfNFTokenOfferNode,
|
||||
};
|
||||
|
||||
for (auto const& [_, sf] : SField::getKnownCodeToField())
|
||||
{
|
||||
if (sf->fieldType != STI_UINT64 || !sf->getName().ends_with("Node"))
|
||||
continue;
|
||||
BEAST_EXPECTS(
|
||||
rpc::isOwnerDirNodeField(*sf) || nonOwnerDirNodes.contains(sf),
|
||||
"sf" + sf->getName() +
|
||||
": add to kOwnerDirNodeFields (owner-dir page hint) "
|
||||
"or to nonOwnerDirNodes (book-dir).");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testChooseLedgerEntryType();
|
||||
testOwnerDirNodeFieldsCoverage();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
#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>
|
||||
@@ -32,11 +35,14 @@
|
||||
#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>
|
||||
@@ -647,6 +653,42 @@ 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;
|
||||
@@ -1616,6 +1658,38 @@ 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
|
||||
@@ -1842,6 +1916,149 @@ 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()
|
||||
{
|
||||
@@ -1964,6 +2181,275 @@ 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
|
||||
{
|
||||
@@ -1985,6 +2471,9 @@ public:
|
||||
testSubBookChanges();
|
||||
testNFToken(all);
|
||||
testNFToken(all - featureNFTokenMintOffer);
|
||||
testSubMPT();
|
||||
testSubMPTBatch();
|
||||
testSubMPTAmountFields();
|
||||
testAsyncTeardownDoesNotStall();
|
||||
testResubscribeAfterDisconnect();
|
||||
testSubscriptionCapRejects();
|
||||
@@ -1993,6 +2482,10 @@ public:
|
||||
testMultiFieldNoPartialSubscribe();
|
||||
testHistoryReSubscribeNotOvercounted();
|
||||
testHistoryCapRejectsNetNew();
|
||||
testMPTCapRejects();
|
||||
testMPTReSubscribeNotOvercounted();
|
||||
testMPTSharesCapWithAccounts();
|
||||
testMPTUnsubscribeFreesCap();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -82,17 +82,41 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// Don't catch up more than 100 ledgers (cannot exceed 256)
|
||||
/**
|
||||
* Don't catch up more than 100 ledgers (cannot exceed 256).
|
||||
*
|
||||
* A wider gap between the published and validated ledgers is abandoned rather
|
||||
* than filled, and publication jumps straight to the validated ledger. The 256
|
||||
* ceiling is how far back hashOfSeq() can reach in one skip-list lookup.
|
||||
*/
|
||||
static constexpr int kMaxLedgerGap{100};
|
||||
|
||||
// Don't acquire history if ledger is too old
|
||||
/**
|
||||
* Don't acquire history if ledger is too old. Age of the validated ledger,
|
||||
* above which back-filling is skipped so the node can catch up first.
|
||||
*/
|
||||
static constexpr std::chrono::minutes kMaxLedgerAgeAcquire{1};
|
||||
|
||||
// Don't acquire history if write load is too high
|
||||
/**
|
||||
* Don't acquire history if write load is too high. The load is the number of
|
||||
* node objects queued or being written by the backend's batch writer; NuDB
|
||||
* always reports 0.
|
||||
*/
|
||||
static constexpr int kMaxWriteLoadAcquire{8192};
|
||||
|
||||
// Helper function for LedgerMaster::doAdvance()
|
||||
// Return true if candidateLedger should be fetched from the network.
|
||||
/**
|
||||
* Helper function for LedgerMaster::doAdvance()
|
||||
* Return true if candidateLedger should be fetched from the network.
|
||||
*
|
||||
* @param currentLedger Sequence of the newest validated ledger.
|
||||
* @param ledgerHistory How many ledgers of history the operator asked to keep.
|
||||
* @param minimumOnline Online-delete floor, or the oldest SQL ledger when
|
||||
* online delete is off; nullopt if unknown.
|
||||
* @param candidateLedger Sequence of the missing ledger being considered.
|
||||
* @param j Log sink for the decision.
|
||||
* @return true when the candidate may be the current ledger, falls inside the
|
||||
* configured history window, or is at or above the minimum to keep online.
|
||||
*/
|
||||
static bool
|
||||
shouldAcquire(
|
||||
std::uint32_t const currentLedger,
|
||||
@@ -1126,12 +1150,28 @@ LedgerMaster::consensusBuilt(
|
||||
auto validations =
|
||||
app_.getValidators().negativeUNLFilter(app_.getValidations().currentTrusted());
|
||||
|
||||
// Track validation counts with sequence numbers
|
||||
/** @cond */
|
||||
/**
|
||||
* Track validation counts with sequence numbers.
|
||||
*
|
||||
* One tally per ledger hash, built while scanning the current trusted
|
||||
* validations. Every validation carries a sequence; the first nonzero one
|
||||
* seen for a hash is kept.
|
||||
*
|
||||
* @note Not thread-safe, and not intended to be: instances live only inside
|
||||
* this function.
|
||||
*/
|
||||
class ValSeq
|
||||
{
|
||||
public:
|
||||
ValSeq() = default;
|
||||
|
||||
/**
|
||||
* Counts one more validation for this ledger.
|
||||
*
|
||||
* @param seq Sequence the validation reported. Adopted only if no
|
||||
* sequence is known yet; zero leaves the tally's sequence unknown.
|
||||
*/
|
||||
void
|
||||
mergeValidation(LedgerIndex seq)
|
||||
{
|
||||
@@ -1142,9 +1182,17 @@ LedgerMaster::consensusBuilt(
|
||||
ledgerSeq = seq;
|
||||
}
|
||||
|
||||
/**
|
||||
* How many trusted validations named this ledger.
|
||||
*/
|
||||
std::size_t valCount{0};
|
||||
|
||||
/**
|
||||
* Sequence of this ledger, or 0 while still unknown.
|
||||
*/
|
||||
LedgerIndex ledgerSeq{0};
|
||||
};
|
||||
/** @endcond */
|
||||
|
||||
// Count the number of current, trusted validations
|
||||
HashMap<UInt256, ValSeq> count;
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/Units.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
@@ -158,9 +159,10 @@ namespace xrpl {
|
||||
|
||||
/**
|
||||
* Concrete NetworkOPs: server sequencer, network tracker, and owner of all
|
||||
* client subscription state (accounts, books, streams). Subscriptions use three
|
||||
* independent non-recursive locks (accountLock_, bookLock_, streamLock_); see
|
||||
* their declarations for the locking and deferred-destruction rules.
|
||||
* client subscription state (accounts, books, MPTs, streams). Subscriptions use
|
||||
* four independent non-recursive locks (accountLock_, bookLock_, mptLock_,
|
||||
* streamLock_); see their declarations for the locking and deferred-destruction
|
||||
* rules.
|
||||
*/
|
||||
class NetworkOPsImp final : public NetworkOPs
|
||||
{
|
||||
@@ -570,6 +572,13 @@ public:
|
||||
unsubAccountInternal(std::uint64_t seq, HashSet<AccountID> const& vnaAccountIDs, bool rt)
|
||||
override;
|
||||
|
||||
void
|
||||
subMPT(InfoSub::Ref ispListener, HashSet<MPTID> const& mptIDs) override;
|
||||
void
|
||||
unsubMPT(InfoSub::Ref ispListener, HashSet<MPTID> const& mptIDs) override;
|
||||
void
|
||||
unsubMPTInternal(std::uint64_t seq, MPTID const& mptID) override;
|
||||
|
||||
ErrorCodeI
|
||||
subAccountHistory(InfoSub::Ref ispListener, AccountID const& account) override;
|
||||
void
|
||||
@@ -799,6 +808,8 @@ private:
|
||||
pubServer();
|
||||
void
|
||||
pubConsensus(ConsensusPhase phase);
|
||||
void
|
||||
pubMPTTransaction(AcceptedLedgerTx const& transaction, MultiApiJson const& jvObj);
|
||||
|
||||
std::string
|
||||
getHostId(bool forAdmin);
|
||||
@@ -807,6 +818,7 @@ private:
|
||||
using SubMapType = HashMap<std::uint64_t, InfoSub::Wptr>;
|
||||
using SubInfoMapType = HashMap<AccountID, SubMapType>;
|
||||
using SubRpcMapType = HashMap<std::string, InfoSub::pointer>;
|
||||
using SubMPTInfoMapType = HashMap<MPTID, SubMapType>;
|
||||
|
||||
/*
|
||||
* With a validated ledger to separate history and future, the node
|
||||
@@ -928,16 +940,18 @@ private:
|
||||
|
||||
// Independent lock domains so a long cleanup/publish on one does not stall
|
||||
// the others. Hold at most one at a time; if ever more, order: accountLock_,
|
||||
// bookLock_, streamLock_.
|
||||
// bookLock_, mptLock_, streamLock_.
|
||||
//
|
||||
// Deferred-destruction rule (non-recursive mutexes): under bookLock_ or
|
||||
// streamLock_, never let the last InfoSub pointer die inside the lock -
|
||||
// ~InfoSub re-acquires it via unsub* -> self-deadlock. Publishers collect the
|
||||
// locked pointers in a vector declared before the lock and destruct after
|
||||
// release (see pubServer / pubBookTransaction). accountLock_ is exempt:
|
||||
// ~InfoSub offloads account teardown to scheduleAccountCleanup.
|
||||
// Deferred-destruction rule (non-recursive mutexes): under bookLock_,
|
||||
// mptLock_, or streamLock_, never let the last InfoSub pointer die inside the
|
||||
// lock - ~InfoSub re-acquires it via unsub* -> self-deadlock. Publishers
|
||||
// collect the locked pointers in a container declared before the lock and
|
||||
// destruct after release (see pubServer / pubBookTransaction /
|
||||
// pubMPTTransaction). accountLock_ is exempt: ~InfoSub offloads account
|
||||
// teardown to scheduleAccountCleanup.
|
||||
std::mutex accountLock_; ///< Guards subAccount_, subRTAccount_, subAccountHistory_.
|
||||
std::mutex bookLock_; ///< Guards subBook_.
|
||||
std::mutex mptLock_; ///< Guards subMPT_.
|
||||
std::mutex streamLock_; ///< Guards streamMaps_[] and rpcSubMap_.
|
||||
|
||||
std::atomic<OperatingMode> mode_;
|
||||
@@ -974,7 +988,8 @@ private:
|
||||
|
||||
SubInfoMapType subAccount_;
|
||||
SubInfoMapType subRTAccount_;
|
||||
SubBookMapType subBook_; ///< Guarded by bookLock_.
|
||||
SubBookMapType subBook_; ///< Guarded by bookLock_.
|
||||
SubMPTInfoMapType subMPT_; ///< Guarded by mptLock_.
|
||||
|
||||
SubRpcMapType rpcSubMap_;
|
||||
|
||||
@@ -3633,6 +3648,7 @@ NetworkOPsImp::pubValidatedTransaction(
|
||||
pubBookTransaction(transaction, jvObj);
|
||||
|
||||
pubAccountTransaction(ledger, transaction, last);
|
||||
pubMPTTransaction(transaction, jvObj);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -4124,6 +4140,133 @@ NetworkOPsImp::scheduleAccountCleanup(
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
NetworkOPsImp::pubMPTTransaction(AcceptedLedgerTx const& alTx, MultiApiJson const& jvObj)
|
||||
{
|
||||
{
|
||||
std::scoped_lock const sl(mptLock_);
|
||||
if (subMPT_.empty())
|
||||
return;
|
||||
}
|
||||
|
||||
// getAffectedMPTs derives the issuance id for MPTokenIssuance entries from
|
||||
// the metadata, so it also covers transactions whose top-level STTx carries
|
||||
// no MPTokenIssuanceID, such as the inner transactions of a Batch. It reads
|
||||
// only the metadata, so it runs outside mptLock_.
|
||||
auto const affectedMPTs = alTx.getMeta().getAffectedMPTs();
|
||||
if (affectedMPTs.empty())
|
||||
return;
|
||||
|
||||
// Declared before the lock so a last-reference ~InfoSub runs after
|
||||
// mptLock_ is released (see the deferred-destruction rule).
|
||||
HashSet<InfoSub::pointer> notify;
|
||||
|
||||
{
|
||||
std::scoped_lock const sl(mptLock_);
|
||||
|
||||
for (auto const& affectedMPT : affectedMPTs)
|
||||
{
|
||||
if (auto simiIt = subMPT_.find(affectedMPT); simiIt != subMPT_.end())
|
||||
{
|
||||
auto it = simiIt->second.begin();
|
||||
while (it != simiIt->second.end())
|
||||
{
|
||||
InfoSub::pointer const p = it->second.lock();
|
||||
|
||||
if (p)
|
||||
{
|
||||
notify.insert(p);
|
||||
++it;
|
||||
}
|
||||
else
|
||||
{
|
||||
it = simiIt->second.erase(it);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (notify.empty())
|
||||
return;
|
||||
|
||||
// Reuse the transaction JSON built by pubValidatedTransaction; only the
|
||||
// message type differs for this stream.
|
||||
MultiApiJson jvMPT = jvObj;
|
||||
jvMPT.set(jss::type, "mptTransaction");
|
||||
|
||||
for (InfoSub::Ref isrListener : notify)
|
||||
{
|
||||
jvMPT.visit(isrListener->getApiVersion(), [&](json::Value const& jv) {
|
||||
isrListener->send(jv, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
NetworkOPsImp::subMPT(InfoSub::Ref isrListener, HashSet<MPTID> const& mptIDs)
|
||||
{
|
||||
// Insert into subMPT_ before the InfoSub (unsubMPT removes in reverse), as
|
||||
// subBook does, so a racing unsubMPT cannot leave a subMPT_ entry that the
|
||||
// InfoSub doesn't know about.
|
||||
{
|
||||
std::scoped_lock const sl(mptLock_);
|
||||
|
||||
for (auto const& mptID : mptIDs)
|
||||
{
|
||||
JLOG(journal_.trace()) << "subMPT: MPT: " << to_string(mptID);
|
||||
|
||||
auto simIterator = subMPT_.find(mptID);
|
||||
if (simIterator == subMPT_.end())
|
||||
{
|
||||
// Not found, note that the MPT issuance has a new single listener.
|
||||
SubMapType usisElement;
|
||||
usisElement[isrListener->getSeq()] = isrListener;
|
||||
subMPT_.insert(simIterator, make_pair(mptID, usisElement));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Found, note that the MPT issuance has another listener.
|
||||
simIterator->second[isrListener->getSeq()] = isrListener;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (auto const& mptID : mptIDs)
|
||||
isrListener->insertSubMPTInfo(mptID);
|
||||
}
|
||||
|
||||
void
|
||||
NetworkOPsImp::unsubMPT(InfoSub::Ref isrListener, HashSet<MPTID> const& mptIDs)
|
||||
{
|
||||
for (auto const& mptID : mptIDs)
|
||||
{
|
||||
// Remove from the InfoSub first so ~InfoSub does not re-issue an
|
||||
// unsubMPTInternal for an issuance the caller already removed, then
|
||||
// remove from the server.
|
||||
isrListener->deleteSubMPTInfo(mptID);
|
||||
unsubMPTInternal(isrListener->getSeq(), mptID);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
NetworkOPsImp::unsubMPTInternal(std::uint64_t uSeq, MPTID const& mptID)
|
||||
{
|
||||
// Only weak_ptrs are erased, so no InfoSub can be destroyed under the lock.
|
||||
std::scoped_lock const sl(mptLock_);
|
||||
|
||||
auto simIterator = subMPT_.find(mptID);
|
||||
if (simIterator == subMPT_.end())
|
||||
return;
|
||||
|
||||
simIterator->second.erase(uSeq);
|
||||
if (simIterator->second.empty())
|
||||
{
|
||||
// Don't need hash entry.
|
||||
subMPT_.erase(simIterator);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
NetworkOPsImp::addAccountHistoryJob(SubAccountHistoryInfoWeak subInfo)
|
||||
{
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/RangeSet.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/LedgerHeader.h>
|
||||
@@ -18,7 +16,6 @@
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
@@ -401,6 +398,15 @@ public:
|
||||
SQLiteDatabase&
|
||||
operator=(SQLiteDatabase&&) = delete;
|
||||
|
||||
/**
|
||||
* @brief ledgerDbHasSpace Checks if the ledger database has available
|
||||
* space.
|
||||
* @param config Config object.
|
||||
* @return True if space is available.
|
||||
*/
|
||||
bool
|
||||
ledgerDbHasSpace(Config const& config);
|
||||
|
||||
/**
|
||||
* @brief transactionDbHasSpace Checks if the transaction database has
|
||||
* available space.
|
||||
@@ -452,27 +458,13 @@ private:
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief checkoutLedger Checks out and returns node store ledger
|
||||
* @brief checkoutTransaction Checks out and returns node store ledger
|
||||
* database.
|
||||
* @return Session to the node store ledger database.
|
||||
* @throws std::runtime_error if ledger database is not available.
|
||||
*
|
||||
* @note Callers typically guard with existsLedger() before calling
|
||||
* this method. The explicit null check here provides
|
||||
* defense-in-depth so that safety does not depend solely on
|
||||
* an implicit caller contract. See PR #6029 for context on
|
||||
* the pattern of relying on config settings instead of
|
||||
* validating actual objects.
|
||||
*/
|
||||
auto
|
||||
checkoutLedger()
|
||||
{
|
||||
if (!ledgerDb_)
|
||||
{
|
||||
constexpr auto msg = "Ledger database is not available";
|
||||
JLOG(j_.fatal()) << msg;
|
||||
Throw<std::runtime_error>(msg);
|
||||
}
|
||||
return ledgerDb_->checkoutDb();
|
||||
}
|
||||
|
||||
@@ -480,23 +472,10 @@ private:
|
||||
* @brief checkoutTransaction Checks out and returns the node store
|
||||
* transaction database.
|
||||
* @return Session to the node store transaction database.
|
||||
* @throws std::runtime_error if transaction database is not available.
|
||||
*
|
||||
* @note Callers typically guard with existsTransaction() and/or
|
||||
* useTxTables_ before calling this method. The explicit null
|
||||
* check here provides defense-in-depth so that safety does
|
||||
* not depend solely on an implicit caller contract or config
|
||||
* settings. See PR #6029 for context.
|
||||
*/
|
||||
auto
|
||||
checkoutTransaction()
|
||||
{
|
||||
if (!txdb_)
|
||||
{
|
||||
constexpr auto msg = "Transaction database is not available";
|
||||
JLOG(j_.fatal()) << msg;
|
||||
Throw<std::runtime_error>(msg);
|
||||
}
|
||||
return txdb_->checkoutDb();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -559,6 +559,18 @@ SQLiteDatabase::SQLiteDatabase(SQLiteDatabase&& rhs) noexcept
|
||||
std::exchange(txdb_, std::move(rhs.txdb_));
|
||||
}
|
||||
|
||||
bool
|
||||
SQLiteDatabase::ledgerDbHasSpace(Config const& config)
|
||||
{
|
||||
if (existsLedger())
|
||||
{
|
||||
auto db = checkoutLedger();
|
||||
return detail::dbHasSpace(*db, config, j_);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
SQLiteDatabase::transactionDbHasSpace(Config const& config)
|
||||
{
|
||||
|
||||
@@ -330,8 +330,21 @@ public:
|
||||
public:
|
||||
Config();
|
||||
|
||||
/* Be very careful to make sure these bool params
|
||||
are in the right order. */
|
||||
/**
|
||||
* Set up the configuration and determine the config and data directories.
|
||||
*
|
||||
* When no config file is named, searches the working directory, then the XDG
|
||||
* config directory when HOME is set, then the system config directory, and
|
||||
* takes the first holding a config file under either name. The system config
|
||||
* directory is the default when none does.
|
||||
*
|
||||
* Be very careful to make sure these bool params are in the right order.
|
||||
*
|
||||
* @param strConf Path of the config file, or empty to search for it.
|
||||
* @param bQuiet Whether to minimize logging verbosity.
|
||||
* @param bSilent Whether to write no console output after startup.
|
||||
* @param bStandalone Whether to operate in stand-alone mode.
|
||||
*/
|
||||
void
|
||||
setup(std::string const& strConf, bool bQuiet, bool bSilent, bool bStandalone);
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
@@ -319,59 +320,66 @@ Config::setup(std::string const& strConf, bool bQuiet, bool bSilent, bool bStand
|
||||
}
|
||||
else
|
||||
{
|
||||
do
|
||||
// A config directory, paired with its data directory.
|
||||
struct Candidate
|
||||
{
|
||||
// Check if either of the config files exist in the current working
|
||||
// directory, in which case the databases will be stored in a
|
||||
// subdirectory.
|
||||
configDir = std::filesystem::current_path();
|
||||
dataDir = configDir / kDatabaseDirName;
|
||||
configFile_ = configDir / kConfigFileName;
|
||||
if (std::filesystem::exists(configFile_))
|
||||
break;
|
||||
configFile_ = configDir / kConfigLegacyName;
|
||||
if (std::filesystem::exists(configFile_))
|
||||
break;
|
||||
std::filesystem::path configDir;
|
||||
std::filesystem::path dataDir;
|
||||
};
|
||||
|
||||
// Check if the home directory is set, and optionally the XDG config
|
||||
// and/or data directories, as the config may be there. See
|
||||
// http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html.
|
||||
auto const strHome = getEnvVar("HOME");
|
||||
if (!strHome.empty())
|
||||
// The candidates to search, in order.
|
||||
std::vector<Candidate> candidates;
|
||||
candidates.reserve(3);
|
||||
|
||||
// The current working directory, with the databases in a subdirectory.
|
||||
auto const cwd = std::filesystem::current_path();
|
||||
candidates.emplace_back(cwd, cwd / kDatabaseDirName);
|
||||
|
||||
// The XDG config and data directories, which only apply when the home
|
||||
// directory is set. See
|
||||
// http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html.
|
||||
if (auto const strHome = getEnvVar("HOME"); !strHome.empty())
|
||||
{
|
||||
auto strXdgConfigHome = getEnvVar("XDG_CONFIG_HOME");
|
||||
if (strXdgConfigHome.empty())
|
||||
{
|
||||
auto strXdgConfigHome = getEnvVar("XDG_CONFIG_HOME");
|
||||
auto strXdgDataHome = getEnvVar("XDG_DATA_HOME");
|
||||
if (strXdgConfigHome.empty())
|
||||
{
|
||||
// $XDG_CONFIG_HOME was not set, use default based on $HOME.
|
||||
strXdgConfigHome = strHome + "/.config";
|
||||
}
|
||||
if (strXdgDataHome.empty())
|
||||
{
|
||||
// $XDG_DATA_HOME was not set, use default based on $HOME.
|
||||
strXdgDataHome = strHome + "/.local/share";
|
||||
}
|
||||
// $XDG_CONFIG_HOME was not set, use default based on $HOME.
|
||||
strXdgConfigHome = strHome + "/.config";
|
||||
}
|
||||
auto strXdgDataHome = getEnvVar("XDG_DATA_HOME");
|
||||
if (strXdgDataHome.empty())
|
||||
{
|
||||
// $XDG_DATA_HOME was not set, use default based on $HOME.
|
||||
strXdgDataHome = strHome + "/.local/share";
|
||||
}
|
||||
candidates.emplace_back(
|
||||
strXdgConfigHome + "/" + systemName(), strXdgDataHome + "/" + systemName());
|
||||
}
|
||||
|
||||
// Check if either of the config files exist in the XDG config
|
||||
// dir.
|
||||
dataDir = strXdgDataHome + "/" + systemName();
|
||||
configDir = strXdgConfigHome + "/" + systemName();
|
||||
configFile_ = configDir / kConfigFileName;
|
||||
if (std::filesystem::exists(configFile_))
|
||||
break;
|
||||
configFile_ = configDir / kConfigLegacyName;
|
||||
// The system config directory, as a last resort.
|
||||
candidates.emplace_back("/etc/" + systemName(), "/var/lib/" + systemName());
|
||||
|
||||
// Take the first candidate holding a config file under either name.
|
||||
// When none does, the last candidate's values stand.
|
||||
for (auto const& candidate : candidates)
|
||||
{
|
||||
configDir = candidate.configDir;
|
||||
dataDir = candidate.dataDir;
|
||||
|
||||
bool found = false;
|
||||
for (std::string_view const fileName : {kConfigFileName, kConfigLegacyName})
|
||||
{
|
||||
configFile_ = configDir / fileName;
|
||||
if (std::filesystem::exists(configFile_))
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// As a last resort, check the system config directory.
|
||||
dataDir = "/var/lib/" + systemName();
|
||||
configDir = "/etc/" + systemName();
|
||||
configFile_ = configDir / kConfigFileName;
|
||||
if (std::filesystem::exists(configFile_))
|
||||
if (found)
|
||||
break;
|
||||
configFile_ = configDir / kConfigLegacyName;
|
||||
} while (false);
|
||||
}
|
||||
}
|
||||
|
||||
// Update default values
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include <xrpl/core/PeerReservationTable.h>
|
||||
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
@@ -11,7 +10,6 @@
|
||||
#include <iterator>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -89,15 +87,6 @@ PeerReservationTable::insertOrAssign(PeerReservation const& reservation)
|
||||
}
|
||||
table_.insert(hint, reservation);
|
||||
|
||||
// connection_ is set by load() during two-phase init. Validate
|
||||
// before dereferencing to guard against use-before-load or a reset
|
||||
// connection. See PR #6029 for the general pattern discussion.
|
||||
if (connection_ == nullptr)
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
"PeerReservationTable::insertOrAssign: database connection is "
|
||||
"not available");
|
||||
}
|
||||
auto db = connection_->checkoutDb();
|
||||
insertPeerReservation(*db, reservation.nodeId, reservation.description);
|
||||
|
||||
@@ -116,14 +105,6 @@ PeerReservationTable::erase(PublicKey const& nodeId)
|
||||
{
|
||||
previous = *it;
|
||||
table_.erase(it);
|
||||
// Validate connection_ before dereferencing — see comment in
|
||||
// insertOrAssign above.
|
||||
if (connection_ == nullptr)
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
"PeerReservationTable::erase: database connection is not "
|
||||
"available");
|
||||
}
|
||||
auto db = connection_->checkoutDb();
|
||||
deletePeerReservation(*db, nodeId);
|
||||
}
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
#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/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/RPCErr.h>
|
||||
@@ -66,38 +66,47 @@ getStartHint(SLE::ConstRef sle, AccountID const& accountID)
|
||||
return sle->getFieldU64(sfOwnerNode);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// UINT64 sf*Node fields that record a page number in an owner directory.
|
||||
// Keep in sync with sfields.macro; the xrpl.rpc.RPCHelpers test enforces it.
|
||||
constexpr std::array<SField const*, 9> kOwnerDirNodeFields{
|
||||
&sfOwnerNode,
|
||||
&sfLowNode,
|
||||
&sfHighNode,
|
||||
&sfDestinationNode,
|
||||
&sfIssuerNode,
|
||||
&sfSubjectNode,
|
||||
&sfSponseeNode,
|
||||
&sfLoanBrokerNode,
|
||||
&sfVaultNode,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool
|
||||
isOwnerDirNodeField(SField const& field)
|
||||
{
|
||||
return std::ranges::contains(kOwnerDirNodeFields, &field);
|
||||
}
|
||||
|
||||
bool
|
||||
isRelatedToAccount(ReadView const& ledger, SLE::ConstRef sle, AccountID const& accountID)
|
||||
{
|
||||
if (sle->getType() == ltRIPPLE_STATE)
|
||||
{
|
||||
return (sle->getFieldAmount(sfLowLimit).getIssuer() == accountID) ||
|
||||
(sle->getFieldAmount(sfHighLimit).getIssuer() == accountID);
|
||||
}
|
||||
if (sle->isFieldPresent(sfAccount))
|
||||
{
|
||||
// If there's an sfAccount present, also test the sfDestination, if
|
||||
// present. This will match objects such as Escrows (ltESCROW), Payment
|
||||
// Channels (ltPAYCHAN), and Checks (ltCHECK) because those are added to
|
||||
// the Destination account's directory. It intentionally EXCLUDES
|
||||
// NFToken Offers (ltNFTOKEN_OFFER). NFToken Offers are NOT added to the
|
||||
// Destination account's directory.
|
||||
return sle->getAccountID(sfAccount) == accountID ||
|
||||
(sle->isFieldPresent(sfDestination) && sle->getAccountID(sfDestination) == accountID);
|
||||
}
|
||||
if (sle->getType() == ltSIGNER_LIST)
|
||||
{
|
||||
Keylet const accountSignerList = keylet::signerList(accountID);
|
||||
return sle->key() == accountSignerList.key;
|
||||
}
|
||||
if (sle->getType() == ltNFTOKEN_OFFER)
|
||||
{
|
||||
// Do not check the sfDestination field. NFToken Offers are NOT added to
|
||||
// the Destination account's directory.
|
||||
return sle->getAccountID(sfOwner) == accountID;
|
||||
}
|
||||
// Marker validator for account_lines / account_offers / account_channels
|
||||
// pagination: probes each owner-directory page-hint field on `sle` and
|
||||
// returns true iff `sle`'s key is present on that page in `accountID`'s
|
||||
// owner directory. Bounded by kOwnerDirNodeFields.size() ledger reads.
|
||||
auto const ownerDir = keylet::ownerDir(accountID);
|
||||
|
||||
return false;
|
||||
auto const pageContainsKey = [&](std::uint64_t node) {
|
||||
auto const page = ledger.read(keylet::page(ownerDir, node));
|
||||
return page && std::ranges::contains(page->getFieldV256(sfIndexes), sle->key());
|
||||
};
|
||||
|
||||
return std::ranges::any_of(kOwnerDirNodeFields, [&](SField const* field) {
|
||||
return sle->isFieldPresent(*field) && pageContainsKey(sle->getFieldU64(*field));
|
||||
});
|
||||
}
|
||||
|
||||
HashSet<AccountID>
|
||||
@@ -116,6 +125,25 @@ parseAccountIds(json::Value const& jvArray)
|
||||
return result;
|
||||
}
|
||||
|
||||
HashSet<MPTID>
|
||||
parseMPTIssuanceIDs(json::Value const& jvArray)
|
||||
{
|
||||
HashSet<MPTID> result;
|
||||
for (auto const& jv : jvArray)
|
||||
{
|
||||
if (!jv.isString())
|
||||
return HashSet<MPTID>();
|
||||
|
||||
auto const mptIssuanceIdStr = jv.asString();
|
||||
MPTID mptIssuanceID;
|
||||
if (!mptIssuanceID.parseHex(mptIssuanceIdStr))
|
||||
return HashSet<MPTID>();
|
||||
|
||||
result.insert(mptIssuanceID);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<json::Value>
|
||||
readLimitField(unsigned int& limit, tuning::LimitRange const& range, JsonContext const& context)
|
||||
{
|
||||
|
||||
@@ -14,9 +14,11 @@
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Seed.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/server/NetworkOPs.h>
|
||||
|
||||
#include <cstdint>
|
||||
@@ -60,6 +62,17 @@ getStartHint(SLE::ConstRef sle, AccountID const& accountID);
|
||||
bool
|
||||
isRelatedToAccount(ReadView const& ledger, SLE::ConstRef sle, AccountID const& accountID);
|
||||
|
||||
/**
|
||||
* @brief Checks whether an SField is a UINT64 sf*Node owner-directory
|
||||
* page-hint field probed by `isRelatedToAccount`.
|
||||
*
|
||||
* @param field The SField to test.
|
||||
* @return true if the field is one of the owner-directory page-hint
|
||||
* fields, false otherwise.
|
||||
*/
|
||||
bool
|
||||
isOwnerDirNodeField(SField const& field);
|
||||
|
||||
/**
|
||||
* @brief Parses an array of account IDs from a JSON value.
|
||||
*
|
||||
@@ -71,6 +84,17 @@ isRelatedToAccount(ReadView const& ledger, SLE::ConstRef sle, AccountID const& a
|
||||
HashSet<AccountID>
|
||||
parseAccountIds(json::Value const& jvArray);
|
||||
|
||||
/**
|
||||
* @brief Parses an array of MPT issuance IDs from a JSON value.
|
||||
*
|
||||
* Extracts and returns a set of MPTID objects from the provided JSON array.
|
||||
*
|
||||
* @param jvArray The JSON value containing an array of MPT issuance IDs.
|
||||
* @return A HashSet containing the parsed MPTID objects.
|
||||
*/
|
||||
HashSet<MPTID>
|
||||
parseMPTIssuanceIDs(json::Value const& jvArray);
|
||||
|
||||
/**
|
||||
* @brief Retrieves the limit value from a JsonContext or sets a default.
|
||||
*
|
||||
|
||||
@@ -194,12 +194,12 @@ doSubscribe(rpc::JsonContext& context)
|
||||
// Parse the proposed (real-time) and normal account sets first, then check
|
||||
// the cap against their COMBINED net-new total before subscribing either.
|
||||
// This keeps the account pair all-or-nothing: it never subscribes one set
|
||||
// and then rejects on the other. Other fields (streams and account_history)
|
||||
// are still checked and subscribed independently, as they always have been,
|
||||
// so a later field can be rejected after an earlier one subscribed. The cap
|
||||
// counts only NET-NEW accounts (those not already tracked on this
|
||||
// connection), so re-subscribing accounts already held is never wrongly
|
||||
// rejected.
|
||||
// and then rejects on the other. Other fields (streams, account_history and
|
||||
// mpt_issuances) are still checked and subscribed independently, as they
|
||||
// always have been, so a later field can be rejected after an earlier one
|
||||
// subscribed. The cap counts only NET-NEW accounts (those not already
|
||||
// tracked on this connection), so re-subscribing accounts already held is
|
||||
// never wrongly rejected.
|
||||
auto accountsProposed = context.params.isMember(jss::accounts_proposed)
|
||||
? jss::accounts_proposed
|
||||
: jss::rt_accounts; // DEPRECATED
|
||||
@@ -394,6 +394,25 @@ doSubscribe(rpc::JsonContext& context)
|
||||
}
|
||||
}
|
||||
|
||||
if (context.params.isMember(jss::mpt_issuances))
|
||||
{
|
||||
if (!context.params[jss::mpt_issuances].isArray())
|
||||
return rpcError(RpcInvalidParams);
|
||||
|
||||
auto ids = rpc::parseMPTIssuanceIDs(context.params[jss::mpt_issuances]);
|
||||
if (ids.empty())
|
||||
return rpcError(RpcInvalidParams);
|
||||
|
||||
// MPT issuance subscriptions are tracked per connection (and counted by
|
||||
// totalSubscriptionCount), so they are capped like the account branches:
|
||||
// an atomic check-and-reserve of the net-new issuances, all-or-nothing.
|
||||
if (!ispSub->tryReserveMPTSubscriptions(ids, subscriptionCap))
|
||||
return rpc::makeParamError("Too many subscriptions for this connection.");
|
||||
|
||||
context.netOps.subMPT(ispSub, ids);
|
||||
JLOG(context.j.debug()) << "doSubscribe: mpts: " << ids.size();
|
||||
}
|
||||
|
||||
return jvResult;
|
||||
}
|
||||
|
||||
|
||||
@@ -207,6 +207,18 @@ doUnsubscribe(rpc::JsonContext& context)
|
||||
}
|
||||
}
|
||||
|
||||
if (context.params.isMember(jss::mpt_issuances))
|
||||
{
|
||||
if (!context.params[jss::mpt_issuances].isArray())
|
||||
return rpcError(RpcInvalidParams);
|
||||
|
||||
auto ids = rpc::parseMPTIssuanceIDs(context.params[jss::mpt_issuances]);
|
||||
if (ids.empty())
|
||||
return rpcError(RpcInvalidParams);
|
||||
context.netOps.unsubMPT(ispSub, ids);
|
||||
JLOG(context.j.debug()) << "doUnsubscribe: mpts: " << ids.size();
|
||||
}
|
||||
|
||||
if (removeUrl)
|
||||
{
|
||||
context.netOps.tryRemoveRpcSub(context.params[jss::url].asString());
|
||||
|
||||
Reference in New Issue
Block a user