Compare commits

...

33 Commits

Author SHA1 Message Date
Mayukha Vadari
a25f372a2a fix clang-tidy 2026-06-11 17:45:42 -04:00
Mayukha Vadari
efc2dc7864 remove bad test 2026-06-11 16:38:55 -04:00
Mayukha Vadari
84e77cc2de Merge branch 'develop' of https://github.com/XRPLF/rippled into mvadari/refactor-tec-deletions 2026-06-11 14:52:24 -04:00
Mayukha Vadari
4c58579166 add more tests, fix bug 2026-06-08 17:13:17 -04:00
Mayukha Vadari
9f490c93ec Merge branch 'develop' of https://github.com/XRPLF/rippled into mvadari/refactor-tec-deletions 2026-06-08 16:22:14 -04:00
Mayukha Vadari
70a95fbf32 Merge branch 'develop' into mvadari/refactor-tec-deletions 2026-05-28 12:22:19 -04:00
Mayukha Vadari
6dfdb0eba2 fix test 2026-05-28 12:22:02 -04:00
Mayukha Vadari
7e82565162 more codecov (hopefully) 2026-05-27 17:26:53 -04:00
Mayukha Vadari
ff0ddad2bb fix regression 2026-05-27 17:18:13 -04:00
Mayukha Vadari
2ff05bfa36 Merge branch 'develop' into mvadari/refactor-tec-deletions 2026-05-27 16:54:31 -04:00
Mayukha Vadari
1569102ac4 Merge branch 'develop' into mvadari/refactor-tec-deletions 2026-05-26 12:32:04 -04:00
Mayukha Vadari
45e5b2cf9f fix clang-tidy 2026-05-21 14:53:09 -04:00
Mayukha Vadari
981099d152 Merge branch 'develop' into mvadari/refactor-tec-deletions 2026-05-20 17:16:15 -04:00
Mayukha Vadari
855f9141a3 use tuple instead of & 2026-04-28 14:30:14 -04:00
Mayukha Vadari
712416e597 Merge branch 'develop' into mvadari/refactor-tec-deletions 2026-04-28 11:14:30 -04:00
Mayukha Vadari
6edcc3547f Merge branch 'develop' into mvadari/refactor-tec-deletions 2026-04-27 10:24:56 -04:00
Mayukha Vadari
a15bac0d34 Merge branch 'develop' into mvadari/refactor-tec-deletions 2026-04-22 16:38:12 -04:00
Mayukha Vadari
ac68086bca Update src/libxrpl/tx/Transactor.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-22 16:38:05 -04:00
Mayukha Vadari
ec0b68437b Merge branch 'develop' into mvadari/refactor-tec-deletions 2026-04-22 14:58:17 -04:00
Mayukha Vadari
4906bd24a1 respond to copilot comments 2026-04-22 14:57:25 -04:00
Mayukha Vadari
721d7b01a0 Merge branch 'develop' into mvadari/refactor-tec-deletions 2026-04-22 14:23:37 -04:00
Mayukha Vadari
fb667a79b8 Merge branch 'develop' into mvadari/refactor-tec-deletions 2026-04-08 14:23:49 -04:00
Mayukha Vadari
abba516f04 fix signature 2026-04-03 10:49:00 -04:00
Mayukha Vadari
58e50d308f Update src/libxrpl/tx/Transactor.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-03 10:40:50 -04:00
Mayukha Vadari
311b618068 Update include/xrpl/tx/Transactor.h
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-03 10:40:38 -04:00
Mayukha Vadari
3d11d3e10d use std::unordered_set 2026-04-03 10:33:07 -04:00
Mayukha Vadari
02455f9bfc move to helper function 2026-04-03 10:30:01 -04:00
Mayukha Vadari
b2fb7382a8 Merge branch 'develop' into mvadari/refactor-tec-deletions 2026-04-03 10:25:14 -04:00
Mayukha Vadari
64b53d6890 add unreachable 2026-03-19 13:50:05 -04:00
Mayukha Vadari
d8f11a9c17 Merge branch 'develop' into mvadari/refactor-tec-deletions 2026-03-19 13:48:33 -04:00
Mayukha Vadari
ceeff478f4 clean up code 2026-03-19 13:38:39 -04:00
Mayukha Vadari
a149cc944a Merge branch 'develop' into mvadari/refactor-tec-deletions 2026-03-19 09:47:28 -04:00
Mayukha Vadari
c4c76e2aaf refactor: Clean up tec object deletion logic 2026-03-19 00:45:45 -04:00
5 changed files with 176 additions and 103 deletions

View File

@@ -7,6 +7,7 @@
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/applySteps.h>
#include <tuple>
#include <utility>
namespace xrpl {
@@ -358,8 +359,13 @@ private:
TER
consumeSeqProxy(SLE::pointer const& sleAccount);
TER
payFee();
std::tuple<TER, XRPAmount, bool>
processPersistentChanges(TER result, XRPAmount fee);
static NotTEC
checkSingleSign(
ReadView const& view,
@@ -367,6 +373,7 @@ private:
AccountID const& idAccount,
SLE::const_pointer sleAccount,
beast::Journal const j);
static NotTEC
checkMultiSign(
ReadView const& view,

View File

@@ -44,8 +44,11 @@
#include <cstddef>
#include <cstdint>
#include <exception>
#include <map>
#include <optional>
#include <stdexcept>
#include <tuple>
#include <unordered_set>
#include <utility>
#include <vector>
@@ -1065,7 +1068,7 @@ removeDeletedMPTs(ApplyView& view, std::vector<uint256> const& mpts, beast::Jour
for (auto const& index : mpts)
{
if (auto const sleState = view.peek({ltMPTOKEN, index}); sleState &&
deleteAMMMPToken(view, sleState, (*sleState)[sfIssuer], viewJ) != tesSUCCESS)
deleteAMMMPToken(view, sleState, (*sleState)[sfAccount], viewJ) != tesSUCCESS)
{
JLOG(viewJ.error()) << "removeDeletedMPTs: failed to delete AMM MPT";
}
@@ -1134,6 +1137,120 @@ Transactor::trapTransaction(uint256 txHash) const
JLOG(j_.debug()) << "Transaction trapped: " << txHash;
}
std::tuple<TER, XRPAmount, bool>
Transactor::processPersistentChanges(TER result, XRPAmount fee)
{
JLOG(j_.trace()) << "reapplying because of " << transToken(result);
// FIXME: This mechanism for doing work while returning a `tec` is
// awkward and very limiting. A more general purpose approach
// should be used, making it possible to do more useful work
// when transactions fail with a `tec` code.
auto typesForResult = [](TER const ter) {
std::unordered_set<LedgerEntryType> types;
if ((ter == tecOVERSIZE) || (ter == tecKILLED))
types.insert(ltOFFER);
if (ter == tecINCOMPLETE)
{
types.insert(ltRIPPLE_STATE);
types.insert(ltMPTOKEN);
}
if (ter == tecEXPIRED)
{
types.insert(ltNFTOKEN_OFFER);
types.insert(ltCREDENTIAL);
}
return types;
};
// Build a list of ledger entry types to collect, based on the
// result code. Only deleted objects of these types will be
// re-applied after the context is reset.
auto const typesToCollect = typesForResult(result);
std::map<LedgerEntryType, std::vector<uint256>> deletedObjects;
if (!typesToCollect.empty())
{
ctx_.visit(
[&typesToCollect, &deletedObjects](
uint256 const& index, bool isDelete, SLE::const_ref before, SLE::const_ref after) {
if (isDelete)
{
XRPL_ASSERT(
before && after,
"xrpl::Transactor::processPersistentChanges : non-null "
"SLE inputs");
if (before && after)
{
auto const type = before->getType();
if (typesToCollect.contains(type))
{
// For offers, only collect unfunded removals
// (where TakerPays is unchanged)
if (type == ltOFFER &&
before->getFieldAmount(sfTakerPays) !=
after->getFieldAmount(sfTakerPays))
return;
deletedObjects[type].push_back(index);
}
}
}
});
}
// Reset the context, potentially adjusting the fee.
{
auto const resetResult = reset(fee);
if (!isTesSuccess(resetResult.first))
result = resetResult.first;
fee = resetResult.second;
}
// Re-apply the collected deletions, but only if the reset succeeded
// and the post-reset result still allows the same deletion type.
auto const typesToApply = typesForResult(result);
if (isTecClaim(result) && !typesToApply.empty())
{
auto const viewJ = ctx_.registry.get().getJournal("View");
for (auto const& [type, ids] : deletedObjects)
{
if (ids.empty() || !typesToApply.contains(type))
continue;
switch (type)
{
case ltOFFER:
removeUnfundedOffers(view(), ids, viewJ);
break;
case ltNFTOKEN_OFFER:
removeExpiredNFTokenOffers(view(), ids, viewJ);
break;
case ltRIPPLE_STATE:
removeDeletedTrustLines(view(), ids, viewJ);
break;
case ltMPTOKEN:
removeDeletedMPTs(view(), ids, viewJ);
break;
case ltCREDENTIAL:
removeExpiredCredentials(view(), ids, viewJ);
break;
// LCOV_EXCL_START
default:
UNREACHABLE(
"xrpl::Transactor::processPersistentChanges() : "
"unexpected type");
break;
// LCOV_EXCL_STOP
}
}
}
return {result, fee, isTecClaim(result)};
}
[[nodiscard]] TER
Transactor::checkTransactionInvariants(TER result, XRPAmount fee)
{
@@ -1183,6 +1300,7 @@ Transactor::checkInvariants(TER result, XRPAmount fee)
*/
return ctx_.checkInvariants(result, fee);
}
//------------------------------------------------------------------------------
ApplyResult
Transactor::operator()()
@@ -1249,108 +1367,7 @@ Transactor::operator()()
(result == tecOVERSIZE) || (result == tecKILLED) || (result == tecINCOMPLETE) ||
(result == tecEXPIRED) || (isTecClaimHardFail(result, view().flags())))
{
JLOG(j_.trace()) << "reapplying because of " << transToken(result);
// FIXME: This mechanism for doing work while returning a `tec` is
// awkward and very limiting. A more general purpose approach
// should be used, making it possible to do more useful work
// when transactions fail with a `tec` code.
std::vector<uint256> removedOffers;
std::vector<uint256> removedTrustLines;
std::vector<uint256> removedMPTs;
std::vector<uint256> expiredNFTokenOffers;
std::vector<uint256> expiredCredentials;
bool const doOffers = ((result == tecOVERSIZE) || (result == tecKILLED));
bool const doLinesOrMPTs = (result == tecINCOMPLETE);
bool const doNFTokenOffers = (result == tecEXPIRED);
bool const doCredentials = (result == tecEXPIRED);
if (doOffers || doLinesOrMPTs || doNFTokenOffers || doCredentials)
{
ctx_.visit([doOffers,
&removedOffers,
doLinesOrMPTs,
&removedTrustLines,
&removedMPTs,
doNFTokenOffers,
&expiredNFTokenOffers,
doCredentials,
&expiredCredentials](
uint256 const& index,
bool isDelete,
SLE::const_ref before,
SLE::const_ref after) {
if (isDelete)
{
XRPL_ASSERT(
before && after,
"xrpl::Transactor::operator()::visit : non-null SLE "
"inputs");
if (doOffers && before && after && (before->getType() == ltOFFER) &&
(before->getFieldAmount(sfTakerPays) == after->getFieldAmount(sfTakerPays)))
{
// Removal of offer found or made unfunded
removedOffers.push_back(index);
}
if (doLinesOrMPTs && before && after)
{
// Removal of obsolete AMM trust line
if (before->getType() == ltRIPPLE_STATE)
{
removedTrustLines.push_back(index);
}
else if (before->getType() == ltMPTOKEN)
{
removedMPTs.push_back(index);
}
}
if (doNFTokenOffers && before && after &&
(before->getType() == ltNFTOKEN_OFFER))
expiredNFTokenOffers.push_back(index);
if (doCredentials && before && after && (before->getType() == ltCREDENTIAL))
expiredCredentials.push_back(index);
}
});
}
// Reset the context, potentially adjusting the fee.
{
auto const resetResult = reset(fee);
if (!isTesSuccess(resetResult.first))
result = resetResult.first;
fee = resetResult.second;
}
// If necessary, remove any offers found unfunded during processing
if ((result == tecOVERSIZE) || (result == tecKILLED))
{
removeUnfundedOffers(view(), removedOffers, ctx_.registry.get().getJournal("View"));
}
if (result == tecEXPIRED)
{
removeExpiredNFTokenOffers(
view(), expiredNFTokenOffers, ctx_.registry.get().getJournal("View"));
}
if (result == tecINCOMPLETE)
{
removeDeletedTrustLines(
view(), removedTrustLines, ctx_.registry.get().getJournal("View"));
removeDeletedMPTs(view(), removedMPTs, ctx_.registry.get().getJournal("View"));
}
if (result == tecEXPIRED)
{
removeExpiredCredentials(
view(), expiredCredentials, ctx_.registry.get().getJournal("View"));
}
applied = isTecClaim(result);
std::tie(result, fee, applied) = processPersistentChanges(result, fee);
}
if (applied)

View File

@@ -1120,6 +1120,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
if (features[fixCleanup3_1_3])
{
buyerCount--;
BEAST_EXPECT(!env.closed()->exists(keylet::nftoffer(buyerExpOfferIndex)));
}
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
@@ -1143,6 +1144,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
if (features[fixCleanup3_1_3])
{
aliceCount--;
BEAST_EXPECT(!env.closed()->exists(keylet::nftoffer(aliceExpOfferIndex)));
}
BEAST_EXPECT(ownerCount(env, alice) == aliceCount);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);

View File

@@ -797,11 +797,13 @@ public:
// The offer expires (it's not removed yet).
env.close();
env.require(Owners(bob, 1), offers(bob, 1));
auto const expiredBobOffer = keylet::offer(bob, env.seq(bob) - 1);
// bob creates the offer that will be crossed.
env(offer(bob, usd(500), XRP(500)), Ter(tesSUCCESS));
env.close();
env.require(Owners(bob, 2), offers(bob, 2));
auto const crossedBobOffer = keylet::offer(bob, env.seq(bob) - 1);
env(trust(alice, usd(1000)), Ter(tesSUCCESS));
env(pay(gw, alice, usd(1000)), Ter(tesSUCCESS));
@@ -820,6 +822,8 @@ public:
Balance(bob, usd(kNone)),
Owners(bob, 1),
offers(bob, 1));
BEAST_EXPECT(!env.current()->exists(expiredBobOffer));
BEAST_EXPECT(env.current()->exists(crossedBobOffer));
// Order that can be filled
env(offer(alice, XRP(500), usd(500)), Txflags(tfFillOrKill), Ter(tesSUCCESS));
@@ -835,6 +839,27 @@ public:
offers(bob, 0));
}
// A failed Fill-or-Kill may tentatively consume a funded offer before
// the transaction is reset. That offer must not be treated as an
// unfunded offer cleanup.
{
Env env{*this, features};
env.fund(startBalance, gw, alice, bob);
env.close();
env(offer(bob, usd(500), XRP(500)), Ter(tesSUCCESS));
env.close();
auto const bobOffer = keylet::offer(bob, env.seq(bob) - 1);
env(trust(alice, usd(1000)), Ter(tesSUCCESS));
env(pay(gw, alice, usd(1000)), Ter(tesSUCCESS));
env(offer(alice, XRP(1000), usd(1000)), Txflags(tfFillOrKill), Ter(tecKILLED));
env.require(offers(alice, 0), offers(bob, 1), Balance(alice, usd(1000)));
BEAST_EXPECT(env.current()->exists(bobOffer));
}
// Immediate or Cancel - cross as much as possible
// and add nothing on the books:
{

View File

@@ -72,6 +72,27 @@ public:
env(regkey(alice, alice), Ter(temBAD_REGKEY));
}
void
testNoAlternativeKey()
{
using namespace test::jtx;
testcase("Cannot remove last signing method");
Env env{*this, testableAmendments()};
Account const alice("alice");
Account const bob("bob");
env.fund(XRP(10000), alice);
env(regkey(alice, bob));
env(fset(alice, asfDisableMaster), Sig(alice));
env(regkey(alice, kDisabled), Sig(bob), Ter(tecNO_ALTERNATIVE_KEY));
auto const sle = env.le(alice);
BEAST_EXPECT(
sle && sle->isFlag(lsfDisableMaster) && sle->getAccountID(sfRegularKey) == bob.id());
}
void
testPasswordSpent()
{
@@ -169,6 +190,7 @@ public:
{
testDisabledMasterKey();
testDisabledRegularKey();
testNoAlternativeKey();
testPasswordSpent();
testUniversalMask();
testTicketRegularKey();