Compare commits

...

18 Commits

Author SHA1 Message Date
Vito Tumas
58dcb0ba9c Merge branch 'develop' into tapanito/pseudo-invariants 2026-06-26 09:56:48 +02:00
Ayaz Salikhov
0711a7b493 build: Switch to a new conan XRPLF remote, again (#7638) 2026-06-25 22:06:04 +00:00
Vito Tumas
dd6f95af6c Update src/libxrpl/tx/invariants/InvariantCheck.cpp
Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com>
2026-06-25 18:27:22 +02:00
Vito
81149c762d address nits from @ximinez 2026-06-25 18:19:08 +02:00
Ayaz Salikhov
07c64f07f0 chore: Revert "build: Switch to a new conan XRPLF remote (#7622)" (#7623) 2026-06-25 14:47:55 +00:00
Vito Tumas
db92907a31 Merge branch 'develop' into tapanito/pseudo-invariants 2026-06-25 14:51:55 +02:00
Ayaz Salikhov
3097c157b6 build: Switch to a new conan XRPLF remote (#7622) 2026-06-25 08:40:06 -04:00
Vito Tumas
f68eaeb148 Merge branch 'develop' into tapanito/pseudo-invariants 2026-06-24 12:23:42 +02:00
Vito Tumas
38195c6d73 Merge branch 'develop' into tapanito/pseudo-invariants 2026-06-22 15:49:41 +02:00
Vito
fc05d9a02f fix: address review comments 2026-06-22 15:31:33 +02:00
Vito Tumas
0eca5a7d29 Merge branch 'develop' into tapanito/pseudo-invariants 2026-06-17 10:38:46 +02:00
Vito
78483ccdc9 clang-tidy 2026-06-12 14:39:37 +02:00
Vito Tumas
fcb5e9ed6d Merge branch 'develop' into tapanito/pseudo-invariants 2026-06-12 12:39:08 +02:00
Vito
0a8b12cd67 refactor: Invert ObjectHasPseudoAccount to check deletion
Change ObjectHasPseudoAccount from checking "object exists →
pseudo-account exists" to "object deleted → pseudo-account deleted".

Together with AccountRootsDeletedClean (which enforces the reverse:
pseudo-account deleted → object deleted), this pair of invariants
guarantees that an object and its pseudo-account are always deleted
as a unit.
2026-06-12 12:38:16 +02:00
Vito
00425ee6e9 Merge remote-tracking branch 'origin/develop' into tapanito/pseudo-invariants 2026-06-11 11:01:43 +02:00
Vito
66693fb7a4 fix: address PR reviewer comments 2026-06-10 17:16:11 +02:00
Vito Tumas
cc4e2b412e Merge branch 'develop' into tapanito/pseudo-invariants 2026-06-10 15:20:02 +02:00
Vito
51e888785d feat: adds an invariant to ensure a pseudo-account is not deleted 2026-06-10 15:18:24 +02:00
14 changed files with 296 additions and 81 deletions

View File

@@ -9,7 +9,7 @@ inputs:
remote_url:
description: "The URL of the Conan endpoint to use."
required: false
default: https://conan.ripplex.io
default: https://conan.xrplf.org/repository/conan/
runs:
using: composite

View File

@@ -154,8 +154,8 @@ jobs:
if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release') }}
uses: ./.github/workflows/reusable-upload-recipe.yml
secrets:
remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}
remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
remote_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
notify-clio:
needs: upload-recipe

View File

@@ -20,8 +20,8 @@ jobs:
if: ${{ github.repository == 'XRPLF/rippled' }}
uses: ./.github/workflows/reusable-upload-recipe.yml
secrets:
remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}
remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
remote_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
build-test:
if: ${{ github.repository == 'XRPLF/rippled' }}

View File

@@ -98,8 +98,8 @@ jobs:
if: ${{ github.repository == 'XRPLF/rippled' && github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
uses: ./.github/workflows/reusable-upload-recipe.yml
secrets:
remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}
remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
remote_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
package:
needs: build-test

View File

@@ -14,7 +14,7 @@ on:
description: "The URL of the Conan endpoint to use."
required: false
type: string
default: https://conan.ripplex.io
default: https://conan.xrplf.org/repository/conan/
secrets:
remote_username:
@@ -41,6 +41,10 @@ jobs:
upload:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-e29b523
env:
REMOTE_NAME: ${{ inputs.remote_name }}
CONAN_LOGIN_USERNAME_XRPLF: ${{ secrets.remote_username }}
CONAN_PASSWORD_XRPLF: ${{ secrets.remote_password }}
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
@@ -56,15 +60,9 @@ jobs:
remote_url: ${{ inputs.remote_url }}
- name: Log into Conan remote
env:
REMOTE_NAME: ${{ inputs.remote_name }}
REMOTE_USERNAME: ${{ secrets.remote_username }}
REMOTE_PASSWORD: ${{ secrets.remote_password }}
run: conan remote login "${REMOTE_NAME}" "${REMOTE_USERNAME}" --password "${REMOTE_PASSWORD}"
run: conan remote login "${REMOTE_NAME}" "${CONAN_LOGIN_USERNAME_XRPLF}" --password "${CONAN_PASSWORD_XRPLF}"
- name: Upload Conan recipe (version)
env:
REMOTE_NAME: ${{ inputs.remote_name }}
run: |
conan export . --version=${{ steps.version.outputs.version }}
conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/${{ steps.version.outputs.version }}
@@ -73,8 +71,6 @@ jobs:
# 'develop' branch, see on-trigger.yml.
- name: Upload Conan recipe (develop)
if: ${{ github.event_name == 'push' }}
env:
REMOTE_NAME: ${{ inputs.remote_name }}
run: |
conan export . --version=develop
conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/develop
@@ -83,8 +79,6 @@ jobs:
# one of the 'release' branches, see on-pr.yml.
- name: Upload Conan recipe (rc)
if: ${{ github.event_name == 'pull_request' }}
env:
REMOTE_NAME: ${{ inputs.remote_name }}
run: |
conan export . --version=rc
conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/rc
@@ -93,8 +87,6 @@ jobs:
# release, see on-tag.yml.
- name: Upload Conan recipe (release)
if: ${{ startsWith(github.ref, 'refs/tags/') }}
env:
REMOTE_NAME: ${{ inputs.remote_name }}
run: |
conan export . --version=release
conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/release

View File

@@ -34,7 +34,7 @@ on:
env:
CONAN_REMOTE_NAME: xrplf
CONAN_REMOTE_URL: https://conan.ripplex.io
CONAN_REMOTE_URL: https://conan.xrplf.org/repository/conan/
NPROC_SUBTRACT: 2
concurrency:
@@ -108,10 +108,12 @@ jobs:
- name: Log into Conan remote
if: ${{ github.repository == 'XRPLF/rippled' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
run: conan remote login "${CONAN_REMOTE_NAME}" "${{ secrets.CONAN_REMOTE_USERNAME }}" --password "${{ secrets.CONAN_REMOTE_PASSWORD }}"
run: conan remote login "${CONAN_REMOTE_NAME}" "${{ secrets.NEXUS_REMOTE_USERNAME }}" --password "${{ secrets.NEXUS_REMOTE_PASSWORD }}"
- name: Upload Conan packages
if: ${{ github.repository == 'XRPLF/rippled' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
env:
FORCE_OPTION: ${{ github.event.inputs.force_upload == 'true' && '--force' || '' }}
CONAN_LOGIN_USERNAME_XRPLF: ${{ secrets.NEXUS_REMOTE_USERNAME }}
CONAN_PASSWORD_XRPLF: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
run: conan upload "*" --remote="${CONAN_REMOTE_NAME}" --confirm ${FORCE_OPTION}

View File

@@ -101,7 +101,7 @@ More information on customizing Conan can be found in the [Advanced Conan config
Run the following command to add the `xrplf` remote, which hosts some of our dependencies:
```bash
conan remote add --index 0 --force xrplf https://conan.ripplex.io
conan remote add --index 0 --force xrplf https://conan.xrplf.org/repository/conan/
```
### Set Up Ccache

View File

@@ -1,43 +1,43 @@
{
"version": "0.5",
"requires": [
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1778091116.056",
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1765850149.987",
"sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1778091117.311",
"soci/4.0.3#fe32b9ad5eb47e79ab9e45a68f363945%1774450067.231",
"snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1765850147.878",
"secp256k1/0.7.1#481881709eb0bdd0185a12b912bbe8ad%1770910500.329",
"rocksdb/10.5.1#4a197eca381a3e5ae8adf8cffa5aacd0%1765850186.86",
"re2/20251105#8579cfd0bda4daf0683f9e3898f964b4%1774398111.888",
"protobuf/6.33.5#d96d52ba5baaaa532f47bda866ad87a5%1774467363.12",
"openssl/3.6.2#4789bbf131b77d0515d15e094c8f697f%1778071755.506",
"nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1775040983.408",
"lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1765850143.914",
"libiconv/1.17#1e65319e945f2d31941a9d28cc13c058%1765842973.492",
"libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1765842973.03",
"libarchive/3.8.7#c446109bd1f1d8ba7936c94189bc50e6%1778091117.848",
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1782392402.420688",
"sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1782392403.185447",
"soci/4.0.3#e726491a03468795453f7c83fc924a96%1782392402.679521",
"snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1782307151.633168",
"secp256k1/0.7.1#b1f450b7f78a36fff75bb6934a356f3a%1782338841.3729",
"rocksdb/10.5.1#4a197eca381a3e5ae8adf8cffa5aacd0%1782392413.075713",
"re2/20251105#8579cfd0bda4daf0683f9e3898f964b4%1782392402.431897",
"protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1782392410.233933",
"openssl/3.6.3#1163d4ddc603907084d08a6a0c6e580f%1782307150.583886",
"nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1782392402.297166",
"lz4/1.10.0#982d9b673900f665a1da109e09c17cab%1782392402.164188",
"libiconv/1.17#9923bc6dc6f106646d6967e0039a5ada%1782392792.775744",
"libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1782392402.420732",
"libarchive/3.8.7#c446109bd1f1d8ba7936c94189bc50e6%1782392403.066892",
"jemalloc/5.3.1#1fc58d55316041f10fbc1e8a2eae632a%1776700028.228",
"gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1768312129.152",
"grpc/1.81.0#2fb144aeb47e7f35c6ebb0e5f35bed31%1781620605.685",
"ed25519/2015.03#ae761bdc52730a843f0809bdf6c1b1f6%1765850143.772",
"date/3.0.4#862e11e80030356b53c2c38599ceb32b%1765850143.772",
"c-ares/1.34.6#545240bb1c40e2cacd4362d6b8967650%1774439234.681",
"bzip2/1.0.8#c470882369c2d95c5c77e970c0c7e321%1765850143.837",
"boost/1.91.0#ea540ca2133d831b560036aa24dece3c%1778091165.282",
"abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1774365460.196"
"gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1782392402.791979",
"grpc/1.81.1#5217e6ef0544c42b46f4af35d5e7f649%1782307148.845616",
"ed25519/2015.03#ae761bdc52730a843f0809bdf6c1b1f6%1782307148.15562",
"date/3.0.4#862e11e80030356b53c2c38599ceb32b%1782392402.538492",
"c-ares/1.34.6#545240bb1c40e2cacd4362d6b8967650%1782392402.681654",
"bzip2/1.0.8#c470882369c2d95c5c77e970c0c7e321%1782392402.296732",
"boost/1.91.0#ea540ca2133d831b560036aa24dece3c%1782392419.475605",
"abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1782307147.395833"
],
"build_requires": [
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1778091116.056",
"strawberryperl/5.32.1.1#8d114504d172cfea8ea1662d09b6333e%1774447376.964",
"protobuf/6.33.5#d96d52ba5baaaa532f47bda866ad87a5%1774467363.12",
"nasm/2.16.01#31e26f2ee3c4346ecd347911bd126904%1765850144.707",
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
"strawberryperl/5.32.1.1#8d114504d172cfea8ea1662d09b6333e%1782395692.540639",
"protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1782392410.233933",
"nasm/2.16.01#31e26f2ee3c4346ecd347911bd126904%1782395690.33162",
"msys2/cci.latest#d22fe7b2808f5fd34d0a7923ace9c54f%1770657326.649",
"m4/1.4.19#4523e4347b55cd26ae918bd5770cab9a%1778062762.471",
"cmake/4.3.0#b939a42e98f593fb34d3a8c5cc860359%1774439249.183",
"b2/5.4.2#ffd6084a119587e70f11cd45d1a386e2%1774439233.447",
"m4/1.4.19#34c4bbc3eeebe98ca6edf2f52d602e7d%1777282960.259",
"cmake/4.3.3#840cf00ea09777e05c2050a50a82c722%1782392418.696091",
"b2/5.4.2#ffd6084a119587e70f11cd45d1a386e2%1782392402.624226",
"automake/1.16.5#b91b7c384c3deaa9d535be02da14d04f%1755524470.56",
"autoconf/2.71#51077f068e61700d65bb05541ea1e4b0%1731054366.86",
"abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1774365460.196"
"abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1782307147.395833"
],
"python_requires": [],
"overrides": {
@@ -57,7 +57,7 @@
"boost/1.91.0"
],
"lz4/[>=1.9.4 <2]": [
"lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504"
"lz4/1.10.0#982d9b673900f665a1da109e09c17cab"
]
},
"config_requires": []

View File

@@ -14,7 +14,7 @@ export CONAN_HOME="$TEMP_DIR"
# Ensure that the xrplf remote is the first to be consulted, so any recipes we
# patched are used. We also add it there to not created huge diff when the
# official Conan Center Index is updated.
conan remote add --force --index 0 xrplf https://conan.ripplex.io
conan remote add --force --index 0 xrplf https://conan.xrplf.org/repository/conan/
# Delete any existing lockfile.
rm -f conan.lock

View File

@@ -28,10 +28,10 @@ class Xrpl(ConanFile):
requires = [
"ed25519/2015.03",
"grpc/1.81.0",
"grpc/1.81.1",
"libarchive/3.8.7",
"nudb/2.0.9",
"openssl/3.6.2",
"openssl/3.6.3",
"secp256k1/0.7.1",
"soci/4.0.3",
"zlib/1.3.2",

View File

@@ -34,7 +34,7 @@ higher index than the default Conan Center remote, so it is consulted first. You
can do this by running:
```bash
conan remote add --index 0 --force xrplf https://conan.ripplex.io
conan remote add --index 0 --force xrplf https://conan.xrplf.org/repository/conan/
```
Alternatively, you can pull our recipes from the repository and export them locally:

View File

@@ -375,16 +375,35 @@ public:
*/
class ValidAmounts
{
std::vector<std::shared_ptr<SLE const>> afterEntries_;
std::vector<SLE::const_pointer> afterEntries_;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(bool, SLE::const_ref, SLE::const_ref);
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
};
/*
* Verify that when an object with an associated pseudo-account is deleted,
* its pseudo-account is also deleted.
*
* The reverse (pseudo-account deleted → object deleted) is enforced by
* AccountRootsDeletedClean via getPseudoAccountFields().
*/
class ObjectHasPseudoAccount
{
public:
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
private:
std::vector<SLE::const_pointer> deletedObjSles_;
};
// additional invariant checks can be declared above and then added to this
// tuple
using InvariantChecks = std::tuple<
@@ -415,7 +434,8 @@ using InvariantChecks = std::tuple<
ValidVault,
ValidMPTPayment,
ValidAmounts,
ValidMPTTransfer>;
ValidMPTTransfer,
ObjectHasPseudoAccount>;
/**
* @brief get a tuple of all invariant checks

View File

@@ -35,6 +35,7 @@
#include <memory>
#include <optional>
#include <sstream>
#include <string>
#include <vector>
namespace xrpl {
@@ -63,6 +64,23 @@ hasPrivilege(STTx const& tx, Privilege priv)
#undef TRANSACTION
#pragma pop_macro("TRANSACTION")
// Returns the human-readable name of a ledger entry's type, falling back to
// the numeric type if the format is somehow unknown.
static std::string
ledgerEntryTypeName(SLE const& sle)
{
auto const item = LedgerFormats::getInstance().findByType(sle.getType());
if (item == nullptr)
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::ledgerEntryTypeName : ledger entry has no known ledger format");
return std::to_string(sle.getType());
// LCOV_EXCL_STOP
}
return item->getName();
}
void
TransactionFeeCheck::visitEntry(bool, SLE::const_ref, SLE::const_ref)
{
@@ -457,16 +475,8 @@ AccountRootsDeletedClean::finalize(
if (auto const sle = view.read(keylet))
{
// Finding the object is bad
auto const typeName = [&sle]() {
auto item = LedgerFormats::getInstance().findByType(sle->getType());
if (item != nullptr)
return item->getName();
return std::to_string(sle->getType());
}();
JLOG(j.fatal()) << "Invariant failed: account deletion left behind a " << typeName
<< " object";
JLOG(j.fatal()) << "Invariant failed: account deletion left behind a "
<< ledgerEntryTypeName(*sle) << " object";
// The comment above starting with "assert(enforce)" explains this
// assert.
XRPL_ASSERT(
@@ -1071,4 +1081,69 @@ ValidAmounts::finalize(
return true;
}
void
ObjectHasPseudoAccount::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
{
if (!isDelete)
return;
// Before should never be null when isDelete = true
if (!before)
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ObjectHasPseudoAccount::visitEntry : deleted ledger entry missing before state");
return;
// LCOV_EXCL_STOP
}
switch (before->getType())
{
case ltAMM:
case ltVAULT:
case ltLOAN_BROKER:
deletedObjSles_.push_back(before);
break;
default:
return;
}
}
[[nodiscard]] bool
ObjectHasPseudoAccount::finalize(
STTx const&,
TER const,
XRPAmount const,
ReadView const& view,
beast::Journal const& j) const
{
if (!view.rules().enabled(fixCleanup3_3_0))
return true;
if (deletedObjSles_.empty())
return true;
bool failed = false;
for (auto const& sle : deletedObjSles_)
{
if (!sle->isFieldPresent(sfAccount))
{
JLOG(j.fatal()) << "Invariant failed: deleted " << ledgerEntryTypeName(*sle)
<< " is missing pseudo-account field";
failed = true;
continue;
}
// The pseudo-account must NOT exist on the ledger after the object is deleted.
if (view.exists(keylet::account(sle->getAccountID(sfAccount))))
{
JLOG(j.fatal()) << "Invariant failed: deleted " << ledgerEntryTypeName(*sle)
<< " without deleting its pseudo-account";
failed = true;
}
}
return !failed;
}
} // namespace xrpl

View File

@@ -2823,7 +2823,8 @@ class Invariants_test : public beast::unit_test::Suite
});
doInvariantCheck(
{"vault updated by a wrong transaction type"},
{"vault updated by a wrong transaction type",
"deleted Vault without deleting its pseudo-account"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
auto sleVault = ac.view().peek(keylet);
@@ -2834,7 +2835,7 @@ class Invariants_test : public beast::unit_test::Suite
},
XRPAmount{},
STTx{ttPAYMENT, [](STObject&) {}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
[&](Account const& a1, Account const& a2, Env& env) {
Vault const vault{env};
auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
@@ -2871,6 +2872,7 @@ class Invariants_test : public beast::unit_test::Suite
auto const vaultPage = ac.view().dirInsert(
keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id()));
sleVault->setFieldU64(sfOwnerNode, *vaultPage);
sleVault->setAccountID(sfAccount, a1.id());
ac.view().insert(sleVault);
return true;
},
@@ -2879,7 +2881,8 @@ class Invariants_test : public beast::unit_test::Suite
{tecINVARIANT_FAILED, tecINVARIANT_FAILED});
doInvariantCheck(
{"vault deleted by a wrong transaction type"},
{"vault deleted by a wrong transaction type",
"deleted Vault without deleting its pseudo-account"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
auto sleVault = ac.view().peek(keylet);
@@ -2890,7 +2893,7 @@ class Invariants_test : public beast::unit_test::Suite
},
XRPAmount{},
STTx{ttVAULT_SET, [](STObject&) {}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
[&](Account const& a1, Account const& a2, Env& env) {
Vault const vault{env};
auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
@@ -2899,7 +2902,8 @@ class Invariants_test : public beast::unit_test::Suite
});
doInvariantCheck(
{"vault operation updated more than single vault"},
{"vault operation updated more than single vault",
"deleted Vault without deleting its pseudo-account"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
{
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
@@ -2919,7 +2923,7 @@ class Invariants_test : public beast::unit_test::Suite
},
XRPAmount{},
STTx{ttVAULT_DELETE, [](STObject&) {}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
[&](Account const& a1, Account const& a2, Env& env) {
Vault const vault{env};
{
@@ -2943,6 +2947,7 @@ class Invariants_test : public beast::unit_test::Suite
auto const vaultPage = ac.view().dirInsert(
keylet::ownerDir(a.id()), sleVault->key(), describeOwnerDir(a.id()));
sleVault->setFieldU64(sfOwnerNode, *vaultPage);
sleVault->setAccountID(sfAccount, a.id());
ac.view().insert(sleVault);
};
insertVault(a1);
@@ -2954,7 +2959,8 @@ class Invariants_test : public beast::unit_test::Suite
{tecINVARIANT_FAILED, tecINVARIANT_FAILED});
doInvariantCheck(
{"deleted vault must also delete shares"},
{"deleted vault must also delete shares",
"deleted Vault without deleting its pseudo-account"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
auto sleVault = ac.view().peek(keylet);
@@ -2965,7 +2971,7 @@ class Invariants_test : public beast::unit_test::Suite
},
XRPAmount{},
STTx{ttVAULT_DELETE, [](STObject&) {}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
[&](Account const& a1, Account const& a2, Env& env) {
Vault const vault{env};
auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
@@ -4965,6 +4971,125 @@ class Invariants_test : public beast::unit_test::Suite
}
}
void
testObjectHasPseudoAccount()
{
testcase << "object has pseudo-account";
using namespace jtx;
auto const amendments = defaultAmendments() | fixCleanup3_3_0;
// Vault: object deleted without its pseudo-account
{
Keylet vaultKeylet = keylet::amendments();
doInvariantCheck(
Env{*this, amendments},
{{"deleted Vault without deleting its pseudo-account"}},
[&vaultKeylet](Account const&, Account const&, ApplyContext& ac) {
auto sle = ac.view().peek(vaultKeylet);
if (!sle)
return false;
ac.view().erase(sle);
return true;
},
XRPAmount{},
STTx{ttVAULT_DELETE, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
[&vaultKeylet](Account const& a1, Account const&, Env& env) {
Vault const vault{env};
auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
env(tx);
vaultKeylet = keylet;
return true;
});
}
// AMM: object deleted without its pseudo-account
{
uint256 ammID{};
Account const gw{"gw"};
doInvariantCheck(
Env{*this, amendments},
{{"deleted AMM without deleting its pseudo-account"}},
[&ammID](Account const&, Account const&, ApplyContext& ac) {
auto sle = ac.view().peek(keylet::amm(ammID));
if (!sle)
return false;
ac.view().erase(sle);
return true;
},
XRPAmount{},
STTx{ttAMM_DELETE, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
[&ammID, &gw](Account const&, Account const&, Env& env) {
env.fund(XRP(1'000), gw);
AMM const amm(env, gw, XRP(100), gw["USD"](100));
ammID = amm.ammID();
return true;
});
}
// LoanBroker: object deleted without its pseudo-account
{
Keylet loanBrokerKeylet = keylet::amendments();
doInvariantCheck(
Env{*this, amendments},
{{"deleted LoanBroker without deleting its pseudo-account"}},
[&loanBrokerKeylet](Account const&, Account const&, ApplyContext& ac) {
auto sle = ac.view().peek(loanBrokerKeylet);
if (!sle)
return false;
ac.view().erase(sle);
return true;
},
XRPAmount{},
STTx{ttLOAN_BROKER_DELETE, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
[&loanBrokerKeylet, this](Account const& a1, Account const&, Env& env) {
PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
loanBrokerKeylet = this->createLoanBroker(a1, env, xrpAsset);
return BEAST_EXPECT(env.le(loanBrokerKeylet));
});
}
// Deleted object missing sfAccount field (defensive check).
// Manually construct the view to place a vault SLE without
// sfAccount into the base ledger, then erase it.
{
Env env{*this, amendments};
Account const a1{"A1"};
Account const a2{"A2"};
env.fund(XRP(1000), a1, a2);
env.close();
OpenView ov{*env.current()};
auto const vaultKeylet = keylet::vault(a1.id(), ov.seq());
auto sleVault = std::make_shared<SLE>(vaultKeylet);
sleVault->makeFieldAbsent(sfAccount);
ov.rawInsert(sleVault);
STTx const tx{ttVAULT_DELETE, [](STObject&) {}};
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
ApplyContext ac{
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
CurrentTransactionRulesGuard const rulesGuard(ov.rules());
auto sle = ac.view().peek(vaultKeylet);
if (!BEAST_EXPECT(sle))
return;
ac.view().erase(sle);
auto transactor = makeTransactor(ac);
if (!BEAST_EXPECT(transactor))
return;
TER const result = transactor->checkInvariants(tesSUCCESS, XRPAmount{});
BEAST_EXPECT(result == tecINVARIANT_FAILED);
BEAST_EXPECT(sink.messages().str().contains("is missing pseudo-account field"));
}
}
public:
void
run() override
@@ -4998,6 +5123,7 @@ public:
testInvariantOverwrite(defaultAmendments() - fixCleanup3_1_3);
testVaultComputeCoarsestScale();
testAMM();
testObjectHasPseudoAccount();
}
};