mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-09 19:56:46 +00:00
Compare commits
5 Commits
bthomee/no
...
tapanito/v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65724aeac1 | ||
|
|
d0a54d1159 | ||
|
|
577d7457f1 | ||
|
|
a389f922dd | ||
|
|
79f4ddc4a6 |
6
.github/scripts/strategy-matrix/linux.json
vendored
6
.github/scripts/strategy-matrix/linux.json
vendored
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"image_tag": "sha-8abe82e",
|
||||
"image_tag": "sha-63ffdc3",
|
||||
"configs": {
|
||||
"ubuntu": [
|
||||
{
|
||||
@@ -67,7 +67,7 @@
|
||||
"compiler": ["gcc"],
|
||||
"build_type": ["Release"],
|
||||
"arch": ["amd64"],
|
||||
"image": "debian:bookworm"
|
||||
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-63ffdc3"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
"compiler": ["gcc"],
|
||||
"build_type": ["Release"],
|
||||
"arch": ["amd64"],
|
||||
"image": "registry.access.redhat.com/ubi9/ubi:latest"
|
||||
"image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-63ffdc3"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
10
.github/workflows/build-nix-images.yml
vendored
10
.github/workflows/build-nix-images.yml
vendored
@@ -6,23 +6,20 @@ on:
|
||||
- develop
|
||||
paths:
|
||||
- ".github/workflows/build-nix-images.yml"
|
||||
- ".github/workflows/reusable-build-docker-image.yml"
|
||||
- ".github/workflows/reusable-build-merge-docker-images.yml"
|
||||
- "flake.nix"
|
||||
- "flake.lock"
|
||||
- "nix/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/build-nix-images.yml"
|
||||
- ".github/workflows/reusable-build-docker-image.yml"
|
||||
- ".github/workflows/reusable-build-merge-docker-images.yml"
|
||||
- "flake.nix"
|
||||
- "flake.lock"
|
||||
- "nix/**"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
# Read `on-trigger.yml` for the rationale behind this concurrency group name.
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' && github.sha || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
@@ -49,8 +46,9 @@ jobs:
|
||||
base_image: debian:bookworm
|
||||
- name: rhel
|
||||
base_image: registry.access.redhat.com/ubi9/ubi:latest
|
||||
uses: ./.github/workflows/reusable-build-merge-docker-images.yml
|
||||
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@c1b480188519e0cad040e6aa70db1cbc5a797e07
|
||||
with:
|
||||
image_name: ghcr.io/xrplf/xrpld/nix-${{ matrix.distro.name }}
|
||||
dockerfile: nix/docker/Dockerfile
|
||||
base_image: ${{ matrix.distro.base_image }}
|
||||
push: ${{ github.repository == 'XRPLF/rippled' && github.event_name == 'push' }}
|
||||
|
||||
10
.github/workflows/build-packaging-images.yml
vendored
10
.github/workflows/build-packaging-images.yml
vendored
@@ -6,21 +6,18 @@ on:
|
||||
- develop
|
||||
paths:
|
||||
- ".github/workflows/build-packaging-images.yml"
|
||||
- ".github/workflows/reusable-build-docker-image.yml"
|
||||
- ".github/workflows/reusable-build-merge-docker-images.yml"
|
||||
- "package/Dockerfile"
|
||||
- "package/install-packaging-tools.sh"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/build-packaging-images.yml"
|
||||
- ".github/workflows/reusable-build-docker-image.yml"
|
||||
- ".github/workflows/reusable-build-merge-docker-images.yml"
|
||||
- "package/Dockerfile"
|
||||
- "package/install-packaging-tools.sh"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
# Read `on-trigger.yml` for the rationale behind this concurrency group name.
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' && github.sha || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
@@ -41,8 +38,9 @@ jobs:
|
||||
base_image: debian:bookworm
|
||||
- name: rhel
|
||||
base_image: registry.access.redhat.com/ubi9/ubi:latest
|
||||
uses: ./.github/workflows/reusable-build-merge-docker-images.yml
|
||||
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@c1b480188519e0cad040e6aa70db1cbc5a797e07
|
||||
with:
|
||||
image_name: ghcr.io/xrplf/xrpld/packaging-${{ matrix.distro.name }}
|
||||
dockerfile: package/Dockerfile
|
||||
base_image: ${{ matrix.distro.base_image }}
|
||||
push: ${{ github.repository == 'XRPLF/rippled' && github.event_name == 'push' }}
|
||||
|
||||
2
.github/workflows/pre-commit.yml
vendored
2
.github/workflows/pre-commit.yml
vendored
@@ -14,7 +14,7 @@ on:
|
||||
jobs:
|
||||
# Call the workflow in the XRPLF/actions repo that runs the pre-commit hooks.
|
||||
run-hooks:
|
||||
uses: XRPLF/actions/.github/workflows/pre-commit.yml@cba1f0891650baf1a9c88624dc2d72573be2eb81
|
||||
uses: XRPLF/actions/.github/workflows/pre-commit.yml@312aaab296060ff89d7f798dcab59f019bea6e02
|
||||
with:
|
||||
runs_on: ubuntu-latest
|
||||
container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-41ec7c1" }'
|
||||
|
||||
4
.github/workflows/publish-docs.yml
vendored
4
.github/workflows/publish-docs.yml
vendored
@@ -41,13 +41,13 @@ env:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-8abe82e
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-63ffdc3
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@90f11ee655d1687824fb8793db770477d52afbab
|
||||
uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
# Build a single-platform Docker image. On push, the image is pushed to
|
||||
# GHCR with arch-suffixed tags (e.g. `:latest-amd64`, `:sha-abc-amd64`)
|
||||
# so the calling workflow can stitch per-arch builds into a multi-arch
|
||||
# manifest without needing to pass digests around.
|
||||
name: Reusable build Docker image (single platform)
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
image_name:
|
||||
description: "Full image name without tag (e.g. 'ghcr.io/xrplf/xrpld/nix-ubuntu')"
|
||||
required: true
|
||||
type: string
|
||||
dockerfile:
|
||||
description: "Path to the Dockerfile, relative to the repository root"
|
||||
required: true
|
||||
type: string
|
||||
base_image:
|
||||
description: "Value passed to the Dockerfile as the BASE_IMAGE build arg"
|
||||
required: true
|
||||
type: string
|
||||
platform:
|
||||
description: "Docker platform string, e.g. linux/amd64"
|
||||
required: true
|
||||
type: string
|
||||
runner:
|
||||
description: "GitHub Actions runner label to build on"
|
||||
required: true
|
||||
type: string
|
||||
push:
|
||||
description: "Whether to push the image to GHCR"
|
||||
required: true
|
||||
type: boolean
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build ${{ inputs.platform }}
|
||||
runs-on: ${{ inputs.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Determine arch
|
||||
id: vars
|
||||
env:
|
||||
PLATFORM: ${{ inputs.platform }}
|
||||
run: |
|
||||
echo "arch=${PLATFORM##*/}" >>$GITHUB_OUTPUT
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: inputs.push
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
with:
|
||||
images: ${{ inputs.image_name }}
|
||||
tags: |
|
||||
type=sha,prefix=sha-,format=short
|
||||
type=raw,value=latest
|
||||
flavor: |
|
||||
suffix=-${{ steps.vars.outputs.arch }},onlatest=true
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||
with:
|
||||
context: .
|
||||
file: ${{ inputs.dockerfile }}
|
||||
platforms: ${{ inputs.platform }}
|
||||
push: ${{ inputs.push }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: BASE_IMAGE=${{ inputs.base_image }}
|
||||
@@ -1,89 +0,0 @@
|
||||
name: Reusable build and merge Docker image (multi-arch)
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
image_name:
|
||||
description: "Full image name without tag (e.g. 'ghcr.io/xrplf/xrpld/nix-ubuntu')"
|
||||
required: true
|
||||
type: string
|
||||
dockerfile:
|
||||
description: "Path to the Dockerfile, relative to the repository root"
|
||||
required: true
|
||||
type: string
|
||||
base_image:
|
||||
description: "Value passed to the Dockerfile as the BASE_IMAGE build arg"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build ${{ inputs.image_name }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
|
||||
uses: ./.github/workflows/reusable-build-docker-image.yml
|
||||
with:
|
||||
image_name: ${{ inputs.image_name }}
|
||||
dockerfile: ${{ inputs.dockerfile }}
|
||||
base_image: ${{ inputs.base_image }}
|
||||
platform: ${{ matrix.target.platform }}
|
||||
runner: ${{ matrix.target.runner }}
|
||||
push: ${{ github.repository == 'XRPLF/rippled' && github.event_name == 'push' }}
|
||||
|
||||
merge:
|
||||
name: Merge ${{ inputs.image_name }}
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
|
||||
- name: Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
with:
|
||||
images: ${{ inputs.image_name }}
|
||||
tags: |
|
||||
type=sha,prefix=sha-,format=short
|
||||
type=raw,value=latest
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create multi-arch manifests
|
||||
if: ${{ github.repository == 'XRPLF/rippled' && github.event_name == 'push' }}
|
||||
run: |
|
||||
for tag in $(jq -cr '.tags[]' <<<"$DOCKER_METADATA_OUTPUT_JSON"); do
|
||||
docker buildx imagetools create -t "$tag" "${tag}-amd64" "${tag}-arm64"
|
||||
done
|
||||
|
||||
- name: Inspect image
|
||||
if: ${{ github.repository == 'XRPLF/rippled' && github.event_name == 'push' }}
|
||||
env:
|
||||
IMAGE_NAME: ${{ inputs.image_name }}
|
||||
IMAGE_VERSION: ${{ steps.meta.outputs.version }}
|
||||
run: |
|
||||
docker buildx imagetools inspect "${IMAGE_NAME}:${IMAGE_VERSION}"
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@90f11ee655d1687824fb8793db770477d52afbab
|
||||
uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8
|
||||
with:
|
||||
enable_ccache: ${{ inputs.ccache_enabled }}
|
||||
|
||||
@@ -370,7 +370,7 @@ jobs:
|
||||
|
||||
- name: Upload coverage report
|
||||
if: ${{ github.repository == 'XRPLF/rippled' && !inputs.build_only && env.COVERAGE_ENABLED == 'true' }}
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
with:
|
||||
disable_search: true
|
||||
disable_telem: true
|
||||
|
||||
6
.github/workflows/reusable-clang-tidy.yml
vendored
6
.github/workflows/reusable-clang-tidy.yml
vendored
@@ -29,14 +29,14 @@ jobs:
|
||||
if: ${{ inputs.check_only_changed }}
|
||||
permissions:
|
||||
contents: read
|
||||
uses: XRPLF/actions/.github/workflows/determine-tidy-files.yml@224f3c48d3014d082a1129237b8291ff0b0a331f
|
||||
uses: XRPLF/actions/.github/workflows/determine-tidy-files.yml@312aaab296060ff89d7f798dcab59f019bea6e02
|
||||
|
||||
run-clang-tidy:
|
||||
name: Run clang tidy
|
||||
needs: [determine-files]
|
||||
if: ${{ always() && !cancelled() && (!inputs.check_only_changed || needs.determine-files.outputs.cpp_changed_files != '' || needs.determine-files.outputs.clang_tidy_config_changed == 'true') }}
|
||||
runs-on: ["self-hosted", "Linux", "X64", "heavy"]
|
||||
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-8abe82e"
|
||||
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-63ffdc3"
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@90f11ee655d1687824fb8793db770477d52afbab
|
||||
uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
|
||||
25
.github/workflows/reusable-package.yml
vendored
25
.github/workflows/reusable-package.yml
vendored
@@ -68,31 +68,6 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
# Packaging runs in a vanilla distro image, so the tooling has to come
|
||||
# from the distro's archive: debhelper for deb, rpm-build (and the
|
||||
# systemd / find-debuginfo macros it depends on) for rpm. Run this
|
||||
# before actions/checkout so the latter can use git (real history) for
|
||||
# build_pkg.sh's SOURCE_DATE_EPOCH; otherwise it falls back to a tarball
|
||||
# download and the timestamp comes from wall-clock time.
|
||||
- name: Install packaging tooling (deb)
|
||||
if: ${{ matrix.distro == 'debian' }}
|
||||
run: |
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
debhelper \
|
||||
git
|
||||
|
||||
- name: Install packaging tooling (rpm)
|
||||
if: ${{ matrix.distro == 'rhel' }}
|
||||
run: |
|
||||
dnf install -y --setopt=install_weak_deps=False \
|
||||
git \
|
||||
rpm-build \
|
||||
redhat-rpm-config \
|
||||
systemd-rpm-macros
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
|
||||
2
.github/workflows/reusable-upload-recipe.yml
vendored
2
.github/workflows/reusable-upload-recipe.yml
vendored
@@ -40,7 +40,7 @@ defaults:
|
||||
jobs:
|
||||
upload:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-8abe82e
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-63ffdc3
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
2
.github/workflows/upload-conan-deps.yml
vendored
2
.github/workflows/upload-conan-deps.yml
vendored
@@ -67,7 +67,7 @@ jobs:
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@90f11ee655d1687824fb8793db770477d52afbab
|
||||
uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
|
||||
132
include/xrpl/tx/invariants/VaultInvariantData.h
Normal file
132
include/xrpl/tx/invariants/VaultInvariantData.h
Normal file
@@ -0,0 +1,132 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* @brief Collects vault and share-issuance snapshots from ledger entry visits.
|
||||
*
|
||||
* Used by per-transaction invariant checks (e.g. VaultCreate, VaultSet) that
|
||||
* need vault and MPTokenIssuance state, optionally with balance-delta tracking.
|
||||
*/
|
||||
class VaultInvariantData
|
||||
{
|
||||
public:
|
||||
struct Vault
|
||||
{
|
||||
uint256 key = beast::kZero;
|
||||
Asset asset;
|
||||
AccountID pseudoId;
|
||||
AccountID owner;
|
||||
uint192 shareMPTID = beast::kZero;
|
||||
Number assetsTotal = 0;
|
||||
Number assetsAvailable = 0;
|
||||
Number assetsMaximum = 0;
|
||||
Number lossUnrealized = 0;
|
||||
|
||||
static Vault
|
||||
make(SLE const&);
|
||||
};
|
||||
|
||||
struct Shares
|
||||
{
|
||||
MPTIssue share;
|
||||
std::uint64_t sharesTotal = 0;
|
||||
std::uint64_t sharesMaximum = 0;
|
||||
|
||||
static Shares
|
||||
make(SLE const&);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Balance-change delta for a single ledger entry.
|
||||
*
|
||||
* Mirrors ValidVault::DeltaInfo. @c scale carries the STAmount exponent
|
||||
* so that callers can round to the coarsest representable precision.
|
||||
*/
|
||||
struct DeltaInfo final
|
||||
{
|
||||
Number delta = kNumZero;
|
||||
std::optional<int> scale;
|
||||
|
||||
/**
|
||||
* @brief Compute the delta between two Numbers at the coarsest scale.
|
||||
*/
|
||||
[[nodiscard]] static DeltaInfo
|
||||
makeDelta(Number const& before, Number const& after, Asset const& asset);
|
||||
};
|
||||
|
||||
void
|
||||
visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after);
|
||||
|
||||
[[nodiscard]] std::vector<Vault> const&
|
||||
afterVaults() const
|
||||
{
|
||||
return afterVault_;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<Vault> const&
|
||||
beforeVaults() const
|
||||
{
|
||||
return beforeVault_;
|
||||
}
|
||||
|
||||
/** Find shares in afterMPTs_ whose mptID matches. */
|
||||
[[nodiscard]] std::optional<Shares>
|
||||
findShares(uint192 const& mptID) const;
|
||||
|
||||
/**
|
||||
* @brief Find shares in beforeMPTs_ whose mptID matches (deleted entries).
|
||||
*/
|
||||
[[nodiscard]] std::optional<Shares>
|
||||
findDeletedShares(uint192 const& mptID) const;
|
||||
|
||||
/**
|
||||
* @brief Access the raw vector of before-state MPTokenIssuance snapshots.
|
||||
*/
|
||||
[[nodiscard]] std::vector<Shares> const&
|
||||
beforeMPTIssuances() const
|
||||
{
|
||||
return beforeMPTs_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Return the vault-asset balance-change delta for an account.
|
||||
*
|
||||
* Looks up the ledger-entry delta recorded during visitEntry for the
|
||||
* account entry (XRP), trust line (IOU), or MPToken (MPT) that corresponds
|
||||
* to the vault asset held by @p id.
|
||||
*
|
||||
* @param vaultAsset The asset held by the vault.
|
||||
* @param id Account whose asset delta is requested.
|
||||
* @returns The delta, or std::nullopt if the entry was not touched.
|
||||
*/
|
||||
[[nodiscard]] std::optional<DeltaInfo>
|
||||
deltaAssets(Asset const& vaultAsset, AccountID const& id) const;
|
||||
|
||||
/**
|
||||
* @brief Compute the coarsest scale required to represent all numbers.
|
||||
*/
|
||||
[[nodiscard]] static std::int32_t
|
||||
computeCoarsestScale(std::vector<DeltaInfo> const& numbers);
|
||||
|
||||
private:
|
||||
std::vector<Vault> afterVault_;
|
||||
std::vector<Vault> beforeVault_;
|
||||
std::vector<Shares> afterMPTs_;
|
||||
std::vector<Shares> beforeMPTs_;
|
||||
std::unordered_map<uint256, DeltaInfo> deltas_;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
#include <xrpl/tx/invariants/VaultInvariantData.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -38,6 +39,9 @@ public:
|
||||
XRPAmount fee,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j) override;
|
||||
|
||||
private:
|
||||
VaultInvariantData data_;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
#include <xrpl/tx/invariants/VaultInvariantData.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -35,6 +36,9 @@ public:
|
||||
XRPAmount fee,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j) override;
|
||||
|
||||
private:
|
||||
VaultInvariantData data_;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -15,6 +15,7 @@ gcc --version
|
||||
gcov --version
|
||||
gcovr --version
|
||||
git --version
|
||||
git-cliff --version
|
||||
gpg --version
|
||||
less --version
|
||||
make --version
|
||||
|
||||
@@ -15,6 +15,7 @@ in
|
||||
doxygen
|
||||
gcovr
|
||||
git
|
||||
git-cliff
|
||||
gnumake
|
||||
gnupg # needed for signing commits & codecov/codecov-action
|
||||
llvmPackages_22.clang-tools
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
@@ -544,112 +543,15 @@ ValidVault::finalize(
|
||||
result &= [&]() {
|
||||
switch (txnType)
|
||||
{
|
||||
case ttVAULT_CREATE: {
|
||||
bool result = true;
|
||||
|
||||
if (!beforeVault_.empty())
|
||||
{
|
||||
JLOG(j.fatal()) //
|
||||
<< "Invariant failed: create operation must not have "
|
||||
"updated a vault";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (afterVault.assetsAvailable != kZero || afterVault.assetsTotal != kZero ||
|
||||
afterVault.lossUnrealized != kZero || updatedShares->sharesTotal != 0)
|
||||
{
|
||||
JLOG(j.fatal()) //
|
||||
<< "Invariant failed: created vault must be empty";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (afterVault.pseudoId != updatedShares->share.getIssuer())
|
||||
{
|
||||
JLOG(j.fatal()) //
|
||||
<< "Invariant failed: shares issuer and vault "
|
||||
"pseudo-account must be the same";
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const sleSharesIssuer =
|
||||
view.read(keylet::account(updatedShares->share.getIssuer()));
|
||||
if (!sleSharesIssuer)
|
||||
{
|
||||
JLOG(j.fatal()) //
|
||||
<< "Invariant failed: shares issuer must exist";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isPseudoAccount(sleSharesIssuer))
|
||||
{
|
||||
JLOG(j.fatal()) //
|
||||
<< "Invariant failed: shares issuer must be a "
|
||||
"pseudo-account";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (auto const vaultId = (*sleSharesIssuer)[~sfVaultID];
|
||||
!vaultId || *vaultId != afterVault.key)
|
||||
{
|
||||
JLOG(j.fatal()) //
|
||||
<< "Invariant failed: shares issuer pseudo-account "
|
||||
"must point back to the vault";
|
||||
result = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
case ttVAULT_SET: {
|
||||
bool result = true;
|
||||
|
||||
XRPL_ASSERT(
|
||||
!beforeVault_.empty(), "xrpl::ValidVault::finalize : set updated a vault");
|
||||
auto const& beforeVault = beforeVault_[0];
|
||||
|
||||
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
|
||||
if (vaultDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: set must not change vault balance";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (beforeVault.assetsTotal != afterVault.assetsTotal)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: set must not change assets "
|
||||
"outstanding";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (afterVault.assetsMaximum > kZero &&
|
||||
afterVault.assetsTotal > afterVault.assetsMaximum)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: set assets outstanding must not "
|
||||
"exceed assets maximum";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (beforeVault.assetsAvailable != afterVault.assetsAvailable)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: set must not change assets "
|
||||
"available";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (beforeShares && updatedShares &&
|
||||
beforeShares->sharesTotal != updatedShares->sharesTotal)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: set must not change shares "
|
||||
"outstanding";
|
||||
result = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
case ttVAULT_CREATE:
|
||||
case ttVAULT_SET:
|
||||
case ttLOAN_SET:
|
||||
case ttLOAN_MANAGE:
|
||||
case ttLOAN_PAY:
|
||||
// Create-specific checks live in VaultCreate::finalizeInvariants.
|
||||
// Set-specific checks live in VaultSet::finalizeInvariants.
|
||||
// Loan checks are TBD.
|
||||
return true;
|
||||
case ttVAULT_DEPOSIT: {
|
||||
bool result = true;
|
||||
|
||||
@@ -1042,13 +944,6 @@ ValidVault::finalize(
|
||||
return result;
|
||||
}
|
||||
|
||||
case ttLOAN_SET:
|
||||
case ttLOAN_MANAGE:
|
||||
case ttLOAN_PAY: {
|
||||
// TBD
|
||||
return true;
|
||||
}
|
||||
|
||||
default:
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::ValidVault::finalize : unknown transaction type");
|
||||
|
||||
253
src/libxrpl/tx/invariants/VaultInvariantData.cpp
Normal file
253
src/libxrpl/tx/invariants/VaultInvariantData.cpp
Normal file
@@ -0,0 +1,253 @@
|
||||
#include <xrpl/tx/invariants/VaultInvariantData.h>
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
VaultInvariantData::Vault
|
||||
VaultInvariantData::Vault::make(SLE const& from)
|
||||
{
|
||||
XRPL_ASSERT(from.getType() == ltVAULT, "VaultInvariantData::Vault::make : from Vault object");
|
||||
|
||||
Vault self;
|
||||
self.key = from.key();
|
||||
self.asset = from.at(sfAsset);
|
||||
self.pseudoId = from.getAccountID(sfAccount);
|
||||
self.owner = from.at(sfOwner);
|
||||
self.shareMPTID = from.getFieldH192(sfShareMPTID);
|
||||
self.assetsTotal = from.at(sfAssetsTotal);
|
||||
self.assetsAvailable = from.at(sfAssetsAvailable);
|
||||
self.assetsMaximum = from.at(sfAssetsMaximum);
|
||||
self.lossUnrealized = from.at(sfLossUnrealized);
|
||||
return self;
|
||||
}
|
||||
|
||||
VaultInvariantData::Shares
|
||||
VaultInvariantData::Shares::make(SLE const& from)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
from.getType() == ltMPTOKEN_ISSUANCE,
|
||||
"VaultInvariantData::Shares::make : from MPTokenIssuance object");
|
||||
|
||||
Shares self;
|
||||
self.share = MPTIssue(makeMptID(from.getFieldU32(sfSequence), from.getAccountID(sfIssuer)));
|
||||
self.sharesTotal = from.getFieldU64(sfOutstandingAmount);
|
||||
self.sharesMaximum = from[~sfMaximumAmount].value_or(kMaxMpTokenAmount);
|
||||
return self;
|
||||
}
|
||||
|
||||
void
|
||||
VaultInvariantData::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
after != nullptr && (before != nullptr || !isDelete),
|
||||
"xrpl::VaultInvariantData::visitEntry : some object is available");
|
||||
|
||||
// Number balanceDelta will capture the difference (delta) between "before"
|
||||
// state (zero if created) and "after" state (zero if destroyed), and
|
||||
// preserves value scale (exponent) to round values to the same scale during
|
||||
// validation. It is used to validate that the change in account
|
||||
// balances matches the change in vault balances, stored to deltas_ at the
|
||||
// end of this function.
|
||||
DeltaInfo balanceDelta{.delta = kNumZero, .scale = std::nullopt};
|
||||
|
||||
std::int8_t sign = 0;
|
||||
if (before)
|
||||
{
|
||||
switch (before->getType())
|
||||
{
|
||||
case ltVAULT:
|
||||
beforeVault_.push_back(Vault::make(*before));
|
||||
break;
|
||||
case ltMPTOKEN_ISSUANCE:
|
||||
// At this moment we have no way of telling if this object holds
|
||||
// vault shares or something else. Save it for finalize.
|
||||
beforeMPTs_.push_back(Shares::make(*before));
|
||||
balanceDelta.delta =
|
||||
static_cast<std::int64_t>(before->getFieldU64(sfOutstandingAmount));
|
||||
// MPTs are ints, so the scale is always 0.
|
||||
balanceDelta.scale = 0;
|
||||
sign = 1;
|
||||
break;
|
||||
case ltMPTOKEN:
|
||||
balanceDelta.delta = static_cast<std::int64_t>(before->getFieldU64(sfMPTAmount));
|
||||
// MPTs are ints, so the scale is always 0.
|
||||
balanceDelta.scale = 0;
|
||||
sign = -1;
|
||||
break;
|
||||
case ltACCOUNT_ROOT:
|
||||
balanceDelta.delta = before->getFieldAmount(sfBalance);
|
||||
// Account balance is XRP, which is an int, so the scale is
|
||||
// always 0.
|
||||
balanceDelta.scale = 0;
|
||||
sign = -1;
|
||||
break;
|
||||
case ltRIPPLE_STATE: {
|
||||
auto const amount = before->getFieldAmount(sfBalance);
|
||||
balanceDelta.delta = amount;
|
||||
// Trust Line balances are STAmounts, so we can use the exponent
|
||||
// directly to get the scale.
|
||||
balanceDelta.scale = amount.exponent();
|
||||
sign = -1;
|
||||
break;
|
||||
}
|
||||
default:;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isDelete && after)
|
||||
{
|
||||
switch (after->getType())
|
||||
{
|
||||
case ltVAULT:
|
||||
afterVault_.push_back(Vault::make(*after));
|
||||
break;
|
||||
case ltMPTOKEN_ISSUANCE:
|
||||
// At this moment we have no way of telling if this object holds
|
||||
// vault shares or something else. Save it for finalize.
|
||||
afterMPTs_.push_back(Shares::make(*after));
|
||||
balanceDelta.delta -=
|
||||
Number(static_cast<std::int64_t>(after->getFieldU64(sfOutstandingAmount)));
|
||||
// MPTs are ints, so the scale is always 0.
|
||||
balanceDelta.scale = 0;
|
||||
sign = 1;
|
||||
break;
|
||||
case ltMPTOKEN:
|
||||
balanceDelta.delta -=
|
||||
Number(static_cast<std::int64_t>(after->getFieldU64(sfMPTAmount)));
|
||||
// MPTs are ints, so the scale is always 0.
|
||||
balanceDelta.scale = 0;
|
||||
sign = -1;
|
||||
break;
|
||||
case ltACCOUNT_ROOT:
|
||||
balanceDelta.delta -= Number(after->getFieldAmount(sfBalance));
|
||||
// Account balance is XRP, which is an int, so the scale is
|
||||
// always 0.
|
||||
balanceDelta.scale = 0;
|
||||
sign = -1;
|
||||
break;
|
||||
case ltRIPPLE_STATE: {
|
||||
auto const amount = after->getFieldAmount(sfBalance);
|
||||
balanceDelta.delta -= Number(amount);
|
||||
// Trust Line balances are STAmounts, so we can use the exponent
|
||||
// directly to get the scale.
|
||||
if (amount.exponent() > balanceDelta.scale)
|
||||
balanceDelta.scale = amount.exponent();
|
||||
sign = -1;
|
||||
break;
|
||||
}
|
||||
default:;
|
||||
}
|
||||
}
|
||||
|
||||
uint256 const key = (before ? before->key() : after->key());
|
||||
// Append to deltas if sign is non-zero, i.e. an object of an interesting
|
||||
// type has been updated. A transaction may update an object even when
|
||||
// its balance has not changed, e.g. transaction fee equals the amount
|
||||
// transferred to the account. We intentionally do not compare balanceDelta
|
||||
// against zero, to avoid missing such updates.
|
||||
if (sign != 0)
|
||||
{
|
||||
XRPL_ASSERT_PARTS(
|
||||
balanceDelta.scale, "xrpl::VaultInvariantData::visitEntry", "scale initialized");
|
||||
balanceDelta.delta *= sign;
|
||||
deltas_[key] = balanceDelta;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<VaultInvariantData::Shares>
|
||||
VaultInvariantData::findShares(uint192 const& mptID) const
|
||||
{
|
||||
for (auto const& s : afterMPTs_)
|
||||
{
|
||||
if (s.share.getMptID() == mptID)
|
||||
return s;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<VaultInvariantData::Shares>
|
||||
VaultInvariantData::findDeletedShares(uint192 const& mptID) const
|
||||
{
|
||||
for (auto const& s : beforeMPTs_)
|
||||
{
|
||||
if (s.share.getMptID() == mptID)
|
||||
return s;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<VaultInvariantData::DeltaInfo>
|
||||
VaultInvariantData::deltaAssets(Asset const& vaultAsset, AccountID const& id) const
|
||||
{
|
||||
auto const lookup = [&](uint256 const& key) -> std::optional<DeltaInfo> {
|
||||
auto const it = deltas_.find(key);
|
||||
if (it == deltas_.end())
|
||||
return std::nullopt;
|
||||
return it->second;
|
||||
};
|
||||
|
||||
return std::visit(
|
||||
[&]<typename TIss>(TIss const& issue) -> std::optional<DeltaInfo> {
|
||||
if constexpr (std::is_same_v<TIss, Issue>)
|
||||
{
|
||||
if (isXRP(issue))
|
||||
return lookup(keylet::account(id).key);
|
||||
auto result = lookup(keylet::line(id, issue).key);
|
||||
// Trust-line balance is stored from the low-account's
|
||||
// perspective; negate if id is the high account so the delta is
|
||||
// in id's terms.
|
||||
if (result && id > issue.getIssuer())
|
||||
result->delta = -result->delta;
|
||||
return result;
|
||||
}
|
||||
else if constexpr (std::is_same_v<TIss, MPTIssue>)
|
||||
{
|
||||
return lookup(keylet::mptoken(issue.getMptID(), id).key);
|
||||
}
|
||||
},
|
||||
vaultAsset.value());
|
||||
}
|
||||
|
||||
[[nodiscard]] VaultInvariantData::DeltaInfo
|
||||
VaultInvariantData::DeltaInfo::makeDelta(
|
||||
Number const& before,
|
||||
Number const& after,
|
||||
Asset const& asset)
|
||||
{
|
||||
return {
|
||||
.delta = after - before,
|
||||
.scale = std::max(xrpl::scale(after, asset), xrpl::scale(before, asset))};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::int32_t
|
||||
VaultInvariantData::computeCoarsestScale(std::vector<DeltaInfo> const& numbers)
|
||||
{
|
||||
if (numbers.empty())
|
||||
return 0;
|
||||
|
||||
auto const max = std::ranges::max_element(
|
||||
numbers, [](auto const& a, auto const& b) -> bool { return a.scale < b.scale; });
|
||||
XRPL_ASSERT_PARTS(
|
||||
max->scale,
|
||||
"xrpl::VaultInvariantData::computeCoarsestScale",
|
||||
"scale set for destinationDelta");
|
||||
return max->scale.value_or(STAmount::kMaxOffset);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -1,8 +1,10 @@
|
||||
#include <xrpl/tx/transactors/vault/VaultCreate.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
@@ -263,15 +265,107 @@ VaultCreate::doApply()
|
||||
}
|
||||
|
||||
void
|
||||
VaultCreate::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref)
|
||||
VaultCreate::visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
|
||||
{
|
||||
// No transaction-specific invariants yet (future work).
|
||||
data_.visitEntry(isDelete, before, after);
|
||||
}
|
||||
|
||||
bool
|
||||
VaultCreate::finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&)
|
||||
VaultCreate::finalizeInvariants(
|
||||
STTx const&,
|
||||
TER result,
|
||||
XRPAmount,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
// No transaction-specific invariants yet (future work).
|
||||
bool const enforce = view.rules().enabled(featureSingleAssetVault);
|
||||
|
||||
if (!isTesSuccess(result))
|
||||
return true;
|
||||
|
||||
auto const& afterVaults = data_.afterVaults();
|
||||
if (afterVaults.empty())
|
||||
return true;
|
||||
|
||||
auto const& afterVault = afterVaults[0];
|
||||
bool checkResult = true;
|
||||
|
||||
if (!data_.beforeVaults().empty())
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: create operation must not have updated a vault";
|
||||
checkResult = false;
|
||||
}
|
||||
|
||||
// The MPTokenIssuance may not be in the modified set (e.g. only the vault
|
||||
// was touched in the test), so fall back to a view read if needed.
|
||||
auto const updatedShares = [&]() -> std::optional<VaultInvariantData::Shares> {
|
||||
if (auto found = data_.findShares(afterVault.shareMPTID))
|
||||
return found;
|
||||
auto const sleShares = view.read(keylet::mptIssuance(afterVault.shareMPTID));
|
||||
return sleShares ? std::optional<VaultInvariantData::Shares>(
|
||||
VaultInvariantData::Shares::make(*sleShares))
|
||||
: std::nullopt;
|
||||
}();
|
||||
|
||||
static constexpr Number kZero{};
|
||||
|
||||
if (afterVault.assetsAvailable != kZero || afterVault.assetsTotal != kZero ||
|
||||
afterVault.lossUnrealized != kZero || (updatedShares && updatedShares->sharesTotal != 0))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: created vault must be empty";
|
||||
checkResult = false;
|
||||
}
|
||||
|
||||
if (!updatedShares)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: updated vault must have shares";
|
||||
XRPL_ASSERT(enforce, "xrpl::VaultCreate::finalizeInvariants : vault has shares invariant");
|
||||
if (!checkResult)
|
||||
{
|
||||
XRPL_ASSERT(enforce, "xrpl::VaultCreate::finalizeInvariants : vault create invariants");
|
||||
}
|
||||
return !enforce;
|
||||
}
|
||||
|
||||
if (afterVault.pseudoId != updatedShares->share.getIssuer())
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: shares issuer and vault pseudo-account must be the same";
|
||||
checkResult = false;
|
||||
}
|
||||
|
||||
auto const sleSharesIssuer = view.read(keylet::account(updatedShares->share.getIssuer()));
|
||||
if (!sleSharesIssuer)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: shares issuer must exist";
|
||||
XRPL_ASSERT(
|
||||
enforce, "xrpl::VaultCreate::finalizeInvariants : shares issuer exists invariant");
|
||||
if (!checkResult)
|
||||
{
|
||||
XRPL_ASSERT(enforce, "xrpl::VaultCreate::finalizeInvariants : vault create invariants");
|
||||
}
|
||||
return !enforce;
|
||||
}
|
||||
|
||||
if (!isPseudoAccount(sleSharesIssuer))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: shares issuer must be a pseudo-account";
|
||||
checkResult = false;
|
||||
}
|
||||
|
||||
if (auto const vaultId = (*sleSharesIssuer)[~sfVaultID]; !vaultId || *vaultId != afterVault.key)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: shares issuer pseudo-account must point back to the vault";
|
||||
checkResult = false;
|
||||
}
|
||||
|
||||
if (!checkResult)
|
||||
{
|
||||
XRPL_ASSERT(enforce, "xrpl::VaultCreate::finalizeInvariants : vault create invariants");
|
||||
return !enforce;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <xrpl/tx/transactors/vault/VaultSet.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -14,6 +15,7 @@
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
#include <xrpl/tx/invariants/VaultInvariantData.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -178,15 +180,99 @@ VaultSet::doApply()
|
||||
}
|
||||
|
||||
void
|
||||
VaultSet::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref)
|
||||
VaultSet::visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
|
||||
{
|
||||
// No transaction-specific invariants yet (future work).
|
||||
data_.visitEntry(isDelete, before, after);
|
||||
}
|
||||
|
||||
bool
|
||||
VaultSet::finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&)
|
||||
VaultSet::finalizeInvariants(
|
||||
STTx const&,
|
||||
TER result,
|
||||
XRPAmount,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
// No transaction-specific invariants yet (future work).
|
||||
bool const enforce = view.rules().enabled(featureSingleAssetVault);
|
||||
|
||||
if (!isTesSuccess(result))
|
||||
return true;
|
||||
|
||||
auto const& afterVaults = data_.afterVaults();
|
||||
if (afterVaults.empty())
|
||||
return true; // Let ValidVault catch missing vault
|
||||
|
||||
auto const& afterVault = afterVaults[0];
|
||||
auto const& beforeVaults = data_.beforeVaults();
|
||||
// VaultSet always modifies an existing vault; beforeVaults must be non-empty
|
||||
if (beforeVaults.empty())
|
||||
return true; // Let ValidVault catch this
|
||||
|
||||
auto const& beforeVault = beforeVaults[0];
|
||||
|
||||
// The MPTokenIssuance may not be in the modified set (e.g. only the vault
|
||||
// was touched), so fall back to a view read if needed.
|
||||
auto const updatedShares = [&]() -> std::optional<VaultInvariantData::Shares> {
|
||||
if (auto found = data_.findShares(afterVault.shareMPTID))
|
||||
return found;
|
||||
auto const sleShares = view.read(keylet::mptIssuance(afterVault.shareMPTID));
|
||||
return sleShares ? std::optional<VaultInvariantData::Shares>(
|
||||
VaultInvariantData::Shares::make(*sleShares))
|
||||
: std::nullopt;
|
||||
}();
|
||||
|
||||
auto const beforeShares = data_.findDeletedShares(afterVault.shareMPTID);
|
||||
|
||||
static constexpr Number kZero{};
|
||||
bool checkResult = true;
|
||||
|
||||
// 1. VaultSet must not change the vault's asset balance
|
||||
auto const vaultDeltaAssets = data_.deltaAssets(afterVault.asset, afterVault.pseudoId);
|
||||
if (vaultDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: set must not change vault balance";
|
||||
checkResult = false;
|
||||
}
|
||||
|
||||
// 2. VaultSet must not change assets outstanding
|
||||
if (beforeVault.assetsTotal != afterVault.assetsTotal)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: set must not change assets outstanding";
|
||||
checkResult = false;
|
||||
}
|
||||
|
||||
// 3. After set, assets outstanding must not exceed the new maximum
|
||||
if (afterVault.assetsMaximum > kZero && afterVault.assetsTotal > afterVault.assetsMaximum)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: set assets outstanding must not exceed assets maximum";
|
||||
checkResult = false;
|
||||
}
|
||||
|
||||
// 4. VaultSet must not change assets available
|
||||
if (beforeVault.assetsAvailable != afterVault.assetsAvailable)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: set must not change assets available";
|
||||
checkResult = false;
|
||||
}
|
||||
|
||||
// 5. VaultSet must not change shares outstanding
|
||||
if (beforeShares && updatedShares && beforeShares->sharesTotal != updatedShares->sharesTotal)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: set must not change shares outstanding";
|
||||
checkResult = false;
|
||||
}
|
||||
|
||||
if (!checkResult)
|
||||
{
|
||||
XRPL_ASSERT(enforce, "xrpl::VaultSet::finalizeInvariants : vault set invariants");
|
||||
return !enforce;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,4 +53,4 @@ foreach(module IN LISTS test_modules)
|
||||
)
|
||||
endforeach()
|
||||
|
||||
gtest_discover_tests(xrpl_tests)
|
||||
gtest_discover_tests(xrpl_tests DISCOVERY_TIMEOUT 60)
|
||||
|
||||
Reference in New Issue
Block a user