Compare commits

...

5 Commits

Author SHA1 Message Date
Vito
65724aeac1 feat: Move VaultSet invariants from ValidVault to VaultSet 2026-06-09 15:39:12 +02:00
Vito
d0a54d1159 feat: Move VaultCreate invariants from ValidVault to VaultCreate
Introduce VaultInvariantData to collect vault and MPTokenIssuance
snapshots during visitInvariantEntry, then implement the six
ttVAULT_CREATE-specific post-conditions in VaultCreate::finalizeInvariants
(empty vault, pseudo-account linkage). ValidVault now passes through
ttVAULT_CREATE in its switch, keeping only the universal vault checks and
the cross-transaction sentinel that guards against other tx types creating
vaults.
2026-06-09 14:33:52 +02:00
Ayaz Salikhov
577d7457f1 ci: Use XRPLF/actions build-multiarch-image workflow (#7428) 2026-06-08 17:10:05 +00:00
Ayaz Salikhov
a389f922dd ci: Use new packaging images and don't cancel develop builds (#7417)
Co-authored-by: Bart <bthomee@users.noreply.github.com>
2026-06-08 13:41:08 +00:00
dependabot[bot]
79f4ddc4a6 ci: [DEPENDABOT] bump codecov/codecov-action from 6.0.1 to 7.0.0 (#7426)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-08 09:37:50 +00:00
22 changed files with 614 additions and 351 deletions

View File

@@ -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"
}
]
}

View File

@@ -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' }}

View File

@@ -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' }}

View File

@@ -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" }'

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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}"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -15,6 +15,7 @@ gcc --version
gcov --version
gcovr --version
git --version
git-cliff --version
gpg --version
less --version
make --version

View File

@@ -15,6 +15,7 @@ in
doxygen
gcovr
git
git-cliff
gnumake
gnupg # needed for signing commits & codecov/codecov-action
llvmPackages_22.clang-tools

View File

@@ -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");

View 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

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -53,4 +53,4 @@ foreach(module IN LISTS test_modules)
)
endforeach()
gtest_discover_tests(xrpl_tests)
gtest_discover_tests(xrpl_tests DISCOVERY_TIMEOUT 60)