fix: Update pDEX invariant firing under a valid offer deletion (#7118)

Co-authored-by: Peter Chen <ychen@ripple.com>
This commit is contained in:
Shawn Xie
2026-05-20 17:10:04 -04:00
committed by GitHub
parent 9cb0740673
commit 8c0080020f
3 changed files with 66 additions and 6 deletions

View File

@@ -10,9 +10,10 @@ namespace xrpl {
class ValidPermissionedDEX
{
bool regularOffers_ = false;
bool badHybridsOld_ = false; // pre-fixCleanup3_1_3: missing field/domain or size > 1
bool badHybrids_ = false; // post-fixCleanup3_1_3: also catches size == 0 (size != 1)
bool regularOffersOld_ = false; // pre-fixCleanup3_2_0: also flags deleted offers
bool regularOffers_ = false; // post-fixCleanup3_2_0: excludes deleted offers
bool badHybridsOld_ = false; // pre-fixCleanup3_1_3: missing field/domain or size > 1
bool badHybrids_ = false; // post-fixCleanup3_1_3: also catches size == 0 (size != 1)
hash_set<uint256> domains_;
public:

View File

@@ -20,7 +20,7 @@ namespace xrpl {
void
ValidPermissionedDEX::visitEntry(
bool,
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
@@ -38,7 +38,9 @@ ValidPermissionedDEX::visitEntry(
}
else
{
regularOffers_ = true;
regularOffersOld_ = true;
if (!isDelete)
regularOffers_ = true;
}
// pre-fixCleanup3_1_3: hybrid offer missing domain, missing
@@ -100,7 +102,9 @@ ValidPermissionedDEX::finalize(
}
}
if (regularOffers_)
bool const hasRegularOffers =
view.rules().enabled(fixCleanup3_2_0) ? regularOffers_ : regularOffersOld_;
if (hasRegularOffers)
{
JLOG(j.fatal()) << "Invariant failed: domain transaction"
" affected regular offers";

View File

@@ -7,6 +7,7 @@
#include <test/jtx/balance.h>
#include <test/jtx/credentials.h>
#include <test/jtx/domain.h>
#include <test/jtx/jtx_json.h>
#include <test/jtx/offer.h>
#include <test/jtx/owners.h> // IWYU pragma: keep
#include <test/jtx/paths.h>
@@ -34,6 +35,7 @@
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
#include <chrono>
#include <cstddef>
@@ -1455,6 +1457,54 @@ class PermissionedDEX_test : public beast::unit_test::Suite
}
}
void
testCancelRegularOfferWithDomainCreate(FeatureBitset features)
{
bool const fixEnabled = features[fixCleanup3_2_0];
testcase << "Cancel regular offer via domain OfferCreate"
<< (fixEnabled ? " (fixCleanup3_2_0 enabled)" : " (fixCleanup3_2_0 disabled)");
// An OfferCreate with sfDomainID and sfOfferSequence pointing to
// the user's own non-domain offer should atomically cancel the
// regular offer and place the new domain offer.
//
// Pre-fixCleanup3_2_0: ValidPermissionedDEX flagged the deleted
// regular offer, so the transaction failed with tecINVARIANT_FAILED.
// Post-fixCleanup3_2_0: the invariant ignores deletions and the
// transaction succeeds.
Env env(*this, features);
auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
PermissionedDEX(env);
auto const regularSeq = env.seq(bob);
env(offer(bob, XRP(10), USD(10)));
env.close();
BEAST_EXPECT(checkOffer(env, bob, regularSeq, XRP(10), USD(10), 0, false));
auto const domainSeq = env.seq(bob);
if (fixEnabled)
{
env(offer(bob, XRP(20), USD(20)),
Domain(domainID),
Json(jss::OfferSequence, regularSeq));
env.close();
BEAST_EXPECT(!offerExists(env, bob, regularSeq));
BEAST_EXPECT(checkOffer(env, bob, domainSeq, XRP(20), USD(20), 0, true));
}
else
{
env(offer(bob, XRP(20), USD(20)),
Domain(domainID),
Json(jss::OfferSequence, regularSeq),
Ter(tecINVARIANT_FAILED));
env.close();
BEAST_EXPECT(offerExists(env, bob, regularSeq));
BEAST_EXPECT(!offerExists(env, bob, domainSeq));
}
}
public:
void
run() override
@@ -1478,6 +1528,11 @@ public:
testHybridOfferDirectories(all);
testHybridMalformedOffer(all);
testHybridMalformedOffer(all - fixCleanup3_1_3);
// Cancelling a regular offer in a domain OfferCreate is allowed
// only after fixCleanup3_2_0.
testCancelRegularOfferWithDomainCreate(all);
testCancelRegularOfferWithDomainCreate(all - fixCleanup3_2_0);
}
};