Compare commits

..
Author SHA1 Message Date
Bart 5a016a5742 test: Drop an include TxTest.h no longer uses directly
getClosedLedger() returns Ledger const& since the previous commit, so
TxTest.h no longer names ReadView and CI's clang-tidy include-cleaner
rejects the include. Ledger.h still provides it transitively.
2026-10-23 20:06:38 -04:00
Bart 035caef50e test: Check that a discarded XRP burn does not reach the target
The ApplyContext discard test buffered a 10-drop burn before discard() but
never checked that it was dropped, so it would also have passed had the
burn reached the target view. OpenView buffers the burn without touching
its header, so flush the target into a real Ledger after apply() and check
that its drops fall by exactly the 3 burned after the discard. Without the
discard they fall by 13.

TxTest::getClosedLedger() now returns Ledger const& instead of ReadView
const&, so a test can build the next Ledger from it. Every existing caller
only needs a ReadView and is unaffected.
2026-10-23 18:22:08 -04:00
Bart 089885286d refactor: Keep the optional and reference the engaged view instead
Per review, ApplyViewImpl::reset() and ApplyStateTable::clear() were a
hand-written mirror of the view's state that anyone adding state would have
to remember to update. Drop them, along with the ApplyViewImpl gtests that
exercised them, and keep the view in its std::optional.

ApplyContext now also holds a std::reference_wrapper to the engaged view,
seated from the return value of emplace() in the constructor and reseated
in discard(). The accessors go through the wrapper, which clang-tidy does
not treat as an optional access, so the suppressions and the assertion
still go away. The copy and move constructors are deleted because the
wrapper points into the object itself. The ApplyContext::discard() gtest
stays.
2026-10-23 17:25:31 -04:00
Bart 23806111df refactor: Make ApplyContext hold ApplyViewImpl directly
ApplyContext kept its view in a std::optional only so that discard() could
re-emplace it, since ApplyViewImpl is not assignable. Every access to the
view therefore needed a bugprone-unchecked-optional-access suppression and
getApplyViewContext() asserted that the optional was engaged.

ApplyViewImpl gains reset(), which clears the buffered state table (entries
and destroyed drops) through a new ApplyStateTable::clear() and disengages
the delivered amount, leaving the view as if freshly constructed over the
same base and flags. ApplyContext now holds a plain ApplyViewImpl member
initialized in the constructor and discard() calls reset(). The apply()
docstring now states that reset() is valid afterwards, since apply() does
not consume the buffered changes.

Gtests cover reset() for inserted, modified and erased entries, destroyed
drops and the delivered amount, that the view is reusable afterwards, that
reset() after apply() leaves nothing for a second apply(), and that
ApplyContext::discard() has the same effect through the context.
2026-10-23 15:05:54 -04:00
6 changed files with 144 additions and 43 deletions

No files matched your search

+13 -11
View File
@@ -54,6 +54,10 @@ public:
XRPL_ASSERT((flags & TapBatch) == 0, "Batch apply flag should not be set");
}
// viewRef_ points into view_, so a copied or moved context would alias the original.
ApplyContext(ApplyContext const&) = delete;
ApplyContext(ApplyContext&&) = delete;
std::reference_wrapper<ServiceRegistry> registry;
STTx const& tx;
TER const preclaimResult;
@@ -63,20 +67,20 @@ public:
ApplyView&
view()
{
return *view_; // NOLINT(bugprone-unchecked-optional-access) view_ emplaced in constructor
return viewRef_.get();
}
[[nodiscard]] ApplyView const&
view() const
{
return *view_; // NOLINT(bugprone-unchecked-optional-access) view_ emplaced in constructor
return viewRef_.get();
}
// VFALCO Unfortunately this is necessary
RawView&
rawView()
{
return *view_; // NOLINT(bugprone-unchecked-optional-access) view_ emplaced in constructor
return viewRef_.get();
}
[[nodiscard]] ApplyFlags const&
@@ -91,8 +95,7 @@ public:
void
deliver(STAmount const& amount)
{
// NOLINTNEXTLINE(bugprone-unchecked-optional-access) view_ emplaced in constructor
view_->deliver(amount);
viewRef_.get().deliver(amount);
}
/**
@@ -126,8 +129,7 @@ public:
void
destroyXRP(XRPAmount const& fee)
{
// NOLINTNEXTLINE(bugprone-unchecked-optional-access) view_ emplaced in constructor
view_->rawDestroyXRP(fee);
viewRef_.get().rawDestroyXRP(fee);
}
/**
@@ -141,16 +143,16 @@ public:
ApplyViewContext
getApplyViewContext()
{
XRPL_ASSERT(
view_.has_value(),
"xrpl::ApplyContext::getApplyViewContext : view_ emplaced in constructor");
return {.view = *view_, .tx = tx};
return {.view = viewRef_.get(), .tx = tx};
}
private:
OpenView& base_;
ApplyFlags flags_;
std::optional<ApplyViewImpl> view_;
// Always refers to the engaged view_. Reseated whenever view_ is re-emplaced, so the
// accessors need no optional check.
std::reference_wrapper<ApplyViewImpl> viewRef_;
// The ID of the batch transaction we are executing under, if set.
std::optional<UInt256 const> parentBatchId_;
+6 -6
View File
@@ -35,25 +35,25 @@ ApplyContext::ApplyContext(
, journal(journal)
, base_(base)
, flags_(flags)
, viewRef_(view_.emplace(&base_, flags_))
, parentBatchId_(parentBatchId)
{
XRPL_ASSERT(
parentBatchId.has_value() == ((flags_ & TapBatch) == TapBatch),
"Parent Batch ID should be set if batch apply flag is set");
view_.emplace(&base_, flags_);
}
void
ApplyContext::discard()
{
view_.emplace(&base_, flags_);
viewRef_ = view_.emplace(&base_, flags_);
}
std::optional<TxMeta>
ApplyContext::apply(TER ter)
{
// NOLINTNEXTLINE(bugprone-unchecked-optional-access) view_ emplaced in constructor
return view_->apply(base_, tx, ter, parentBatchId_, (flags_ & TapDryRun) != 0u, journal);
return viewRef_.get().apply(
base_, tx, ter, parentBatchId_, (flags_ & TapDryRun) != 0u, journal);
}
void
@@ -66,14 +66,14 @@ ApplyContext::addOrderBook(Book const& book)
std::size_t
ApplyContext::size()
{
return view_->size(); // NOLINT(bugprone-unchecked-optional-access)
return viewRef_.get().size();
}
void
ApplyContext::visit(
std::function<void(UInt256 const&, bool, SLE::ConstRef, SLE::ConstRef)> const& func)
{
view_->visit(base_, func); // NOLINT(bugprone-unchecked-optional-access)
viewRef_.get().visit(base_, func);
}
} // namespace xrpl
+8 -22
View File
@@ -132,21 +132,8 @@ private:
std::chrono::seconds effectiveOverlap = detail::kDefaultEffectiveOverlap;
int expectedRefreshMin = 0;
};
/**
* Serve each configured list from its own publisher server, fetch them all
* through one ValidatorSite and check what it reports for each site.
*
* @param good Directory the cache files are written to.
* @param paths One server per entry, with what to serve and what to expect.
* @param requestTimeout How long the ValidatorSite waits for a response
* before it reports that the request took too long.
*/
void
testFetchList(
detail::DirGuard const& good,
std::vector<FetchListConfig> const& paths,
std::chrono::seconds requestTimeout = std::chrono::seconds{12})
testFetchList(detail::DirGuard const& good, std::vector<FetchListConfig> const& paths)
{
testcase << "Fetch list - "
<< boost::algorithm::join(
@@ -224,10 +211,10 @@ private:
BEAST_EXPECT(trustedKeys.load({}, emptyCfgKeys, cfgPublishers));
// Normally, tests will only need a fraction of the default timeout,
// but sometimes DNS resolution takes an inordinate amount of time, so
// the test will just wait.
auto sites = std::make_unique<ValidatorSite>(env.app(), journal, requestTimeout);
// Normally, tests will only need a fraction of this time,
// but sometimes DNS resolution takes an inordinate amount
// of time, so the test will just wait.
auto sites = std::make_unique<ValidatorSite>(env.app(), journal, 12s);
std::vector<std::string> uris;
for (auto const& u : servers)
@@ -599,19 +586,18 @@ public:
.failApply = true,
},
});
// timeout: the server answers 1s after the request times out
// timeout
testFetchList(
good,
{
{
.path = "/sleep/2",
.path = "/sleep/13",
.msg = "took too long",
.ssl = ssl,
.failFetch = true,
.failApply = true,
},
},
std::chrono::seconds{1});
});
// bad manifest format using known versions
// * Retrieves a v1 formatted list claiming version 2
testFetchList(
+1 -2
View File
@@ -7,7 +7,6 @@
#include <xrpl/ledger/CanonicalTXSet.h>
#include <xrpl/ledger/Ledger.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Fees.h>
@@ -174,7 +173,7 @@ TxTest::getOpenLedger() const
return *openLedger_;
}
ReadView const&
Ledger const&
TxTest::getClosedLedger() const
{
return *closedLedger_;
+1 -2
View File
@@ -7,7 +7,6 @@
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/Ledger.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/LedgerFormats.h>
@@ -299,7 +298,7 @@ public:
* @brief Get the closed (base) ledger view.
* @return A const reference to the closed ledger.
*/
[[nodiscard]] ReadView const&
[[nodiscard]] Ledger const&
getClosedLedger() const;
/**
+115
View File
@@ -0,0 +1,115 @@
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/Ledger.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxMeta.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <helpers/TxTest.h>
#include <cstdint>
#include <memory>
namespace xrpl::test {
/**
* A funded alice over an open ledger, plus a signed transaction to build an
* ApplyContext around.
*/
class ApplyContextTests : public ::testing::Test
{
protected:
TxTest env_;
Account const alice_{"alice"};
std::shared_ptr<STTx const> tx_;
ApplyContextTests()
{
env_.createAccount(alice_, XRP(10'000));
transactions::AccountSetBuilder builder{alice_.id()};
builder.setSequence(env_.getAccountRoot(alice_.id()).getSequence());
builder.setFee(XRPAmount(10));
tx_ = builder.build(alice_.pk(), alice_.sk()).getSTTx();
}
/**
* Build a ticket entry that is not in the ledger.
*
* @param seq The raw ticket sequence.
* @return The new entry.
*/
SLE::pointer
makeTicket(std::uint32_t seq) const
{
auto sle = std::make_shared<SLE>(keylet::ticket(alice_.id(), SeqProxy::rawTicket(seq)));
sle->setAccountID(sfAccount, alice_.id());
sle->setFieldU32(sfTicketSequence, seq);
sle->setFieldU64(sfOwnerNode, 0);
return sle;
}
};
TEST_F(ApplyContextTests, discard_drops_pending_state_and_keeps_the_context_usable)
{
ASSERT_NE(tx_, nullptr);
// A closed base makes apply() produce metadata, which an open ledger
// does not.
OpenView open(&env_.getClosedLedger());
ApplyContext ctx(env_.getServiceRegistry(), open, *tx_, tesSUCCESS, XRPAmount{10}, TapRetry);
// Buffer an inserted entry, a modified entry, destroyed drops and a
// delivered amount through the context's own surface.
auto const stale = makeTicket(1);
ctx.view().insert(stale);
auto const account = ctx.view().peek(keylet::account(alice_.id()));
ASSERT_NE(account, nullptr);
auto const baseSeq = account->getFieldU32(sfSequence);
account->setFieldU32(sfSequence, baseSeq + 1);
ctx.view().update(account);
ctx.destroyXRP(XRPAmount{10});
ctx.deliver(STAmount{XRPAmount{5}});
ASSERT_EQ(ctx.size(), 2);
ctx.discard();
// Reads fall through to the open ledger again and the flags survive.
EXPECT_EQ(ctx.size(), 0);
EXPECT_FALSE(ctx.view().exists(keylet::unchecked(stale->key())));
auto const reread = ctx.view().read(keylet::account(alice_.id()));
ASSERT_NE(reread, nullptr);
EXPECT_EQ(reread->getFieldU32(sfSequence), baseSeq);
EXPECT_EQ(ctx.view().flags(), TapRetry);
// Only changes made after the discard reach the target view.
auto const fresh = makeTicket(2);
ctx.view().insert(fresh);
ctx.destroyXRP(XRPAmount{3});
auto const meta = ctx.apply(tesSUCCESS);
ASSERT_TRUE(meta.has_value());
EXPECT_FALSE(meta->getDeliveredAmount().has_value());
EXPECT_FALSE(open.exists(keylet::unchecked(stale->key())));
EXPECT_TRUE(open.exists(keylet::unchecked(fresh->key())));
auto const applied = open.read(keylet::account(alice_.id()));
ASSERT_NE(applied, nullptr);
EXPECT_EQ(applied->getFieldU32(sfSequence), baseSeq);
// OpenView buffers the burn without touching its header, so flush into a
// real Ledger to see it: 3 drops are gone, not 13.
Ledger const& prev = env_.getClosedLedger();
Ledger next(prev, prev.header().closeTime + prev.header().closeTimeResolution);
open.apply(next);
EXPECT_EQ(next.header().drops, prev.header().drops - XRPAmount{3});
}
} // namespace xrpl::test