Compare commits

...

4 Commits

Author SHA1 Message Date
Mayukha Vadari
beae9832c7 Include Keylet.h in the SLEBase test
(cherry picked from commit 3cca9d3266)
2026-10-05 14:37:07 +01:00
Mayukha Vadari
cc7aab0398 test: Drop extern template declarations from SLEBase test
An explicit instantiation declaration stops clang from emitting the
inline constructors of the header-only entries at -O0, so the debug
builds fail to link xrpl_tests (undefined AccountRootEntry constructors).
Naming each class in a static_assert already instantiates its definition,
which is all the exhaustiveness check needs.

(cherry picked from commit 611415e1f9)
2026-10-05 14:37:07 +01:00
Mayukha Vadari
6c3647c68c test: Declare, not define, entry instantiations in SLEBase test
The exhaustive per-entry-type check in the SLEBase gtest used explicit
instantiation definitions. Once an entry class gets out-of-line member
bodies in its own .cpp (with the explicit instantiation definition
there), the test would instantiate the class a second time, and MSVC
rejects it with C4661 because those member bodies are not visible in
the test translation unit. Explicit instantiation declarations still
instantiate the class definition, which is all the check needs.

(cherry picked from commit 8b07abc3cd)
2026-10-05 13:49:40 +01:00
Mayukha Vadari
d87e49bf3b refactor: Use PayChannelEntry everywhere
Migrate every read/peek/insert/update/erase of a PayChannel ledger
entry in PaymentChannelCreate, PaymentChannelFund, and
PaymentChannelClaim onto PayChannelEntryR/PayChannelEntryW, part of
the wrapped-SLE migration.

Move the closeChannel() and isChannelExpired() free helpers from
PaymentChannelHelpers onto the wrapper as removeFromLedger() and
isExpired(), keeping the exact operation order (owner-directory
removal, balance refund, owner-count decrement, erase).
saturatingAdd() is a plain Rules-based utility and stays free.
2026-10-05 13:49:40 +01:00
8 changed files with 146 additions and 128 deletions

View File

@@ -3,11 +3,17 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/TER.h>
#include <cstdint>
#include <optional>
namespace xrpl {
@@ -30,6 +36,40 @@ public:
: Base(keylet::payChannel(src, dst, seq), view, j)
{
}
/**
* Determine whether a payment channel time field represents an expired
* time.
*
* @param timeField The optional expiry timestamp (seconds since the XRP
* Ledger epoch). If empty, returns false.
* @return @c true if @p timeField is set and the indicated time
* is in the past relative to the view's parent close
* time; @c false otherwise.
*/
[[nodiscard]] bool
isExpired(std::optional<std::uint32_t> timeField) const
{
if (!timeField)
return false;
auto const& view = this->readView();
if (view.rules().enabled(fixCleanup3_2_0))
return after(view.header().parentCloseTime, *timeField);
return view.header().parentCloseTime.time_since_epoch().count() >= *timeField;
}
/**
* Closes this payment channel: returns its remaining funds to the
* channel's source account, removes it from the source's and (if
* present) the destination's owner directories, decrements the source's
* owner count, and erases the channel from the ledger.
*
* @return tesSUCCESS on success; tefBAD_LEDGER if a directory removal
* fails; tefINTERNAL if the source account SLE cannot be found.
*/
[[nodiscard]] TER
removeFromLedger()
requires Base::kIsWritable;
};
using PayChannelEntryR = PayChannelEntry<ReadView>;

View File

@@ -1,30 +1,11 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h>
#include <cstdint>
#include <optional>
namespace xrpl {
/**
* Close a payment channel and return its remaining funds to the channel owner.
*
* @param slep The SLE for the PayChannel object to close.
* @param view The apply view in which ledger state modifications are made.
* @param key The ledger key identifying the PayChannel entry.
* @param j Journal used for fatal-level diagnostic messages.
* @return tesSUCCESS on success; tefBAD_LEDGER if a directory removal
* fails; tefINTERNAL if the source account SLE cannot be found.
*/
TER
closeChannel(SLE::Ref slep, ApplyView& view, UInt256 const& key, beast::Journal j);
/**
* Add two uint32_t values with saturation at UINT32_MAX.
*
@@ -37,17 +18,4 @@ closeChannel(SLE::Ref slep, ApplyView& view, UInt256 const& key, beast::Journal
uint32_t
saturatingAdd(Rules const& rules, uint32_t const lhs, uint32_t const rhs);
/**
* Determine whether a payment channel time field represents an expired time.
*
* @param view The apply view providing the parent close time and rules.
* @param timeField The optional expiry timestamp (seconds since the XRP
* Ledger epoch). If empty, the function returns false.
* @return @c true if @p timeField is set and the indicated time is
* in the past relative to the view's parent close time;
* @c false otherwise.
*/
bool
isChannelExpired(ApplyView const& view, std::optional<std::uint32_t> timeField);
} // namespace xrpl

View File

@@ -0,0 +1,68 @@
#include <xrpl/ledger/entries/PayChannelEntry.h>
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/TER.h>
namespace xrpl {
template <typename ViewT>
TER
PayChannelEntry<ViewT>::removeFromLedger()
requires Base::kIsWritable
{
auto& view = this->applyView();
auto const j = this->journal();
auto const& slep = this->mutableRawSle();
AccountID const src = (*slep)[sfAccount];
// Remove PayChan from owner directory
if (!view.dirRemove(keylet::ownerDir(src), (*slep)[sfOwnerNode], this->key(), true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Could not remove paychan from src owner directory";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
// Remove PayChan from recipient's owner directory, if present.
if (auto const page = (*slep)[~sfDestinationNode])
{
auto const dst = (*slep)[sfDestination];
if (!view.dirRemove(keylet::ownerDir(dst), *page, this->key(), true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Could not remove paychan from dst owner directory";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
// Transfer amount back to owner, decrement owner count
auto const sle = view.peek(keylet::account(src));
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
XRPL_ASSERT(
(*slep)[sfAmount] >= (*slep)[sfBalance],
"xrpl::PayChannelEntry::removeFromLedger : minimum channel amount");
(*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance];
decreaseOwnerCountForObject(view, sle, slep, 1, j);
view.update(sle);
// Remove PayChan from ledger
this->erase();
return tesSUCCESS;
}
template class PayChannelEntry<ReadView>;
template class PayChannelEntry<ApplyView>;
} // namespace xrpl

View File

@@ -1,71 +1,14 @@
#include <xrpl/ledger/helpers/PaymentChannelHelpers.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/Rules.h>
#include <algorithm>
#include <cstdint>
#include <limits>
#include <optional>
namespace xrpl {
TER
closeChannel(SLE::Ref slep, ApplyView& view, UInt256 const& key, beast::Journal j)
{
AccountID const src = (*slep)[sfAccount];
// Remove PayChan from owner directory
{
auto const page = (*slep)[sfOwnerNode];
if (!view.dirRemove(keylet::ownerDir(src), page, key, true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Could not remove paychan from src owner directory";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
// Remove PayChan from recipient's owner directory, if present.
if (auto const page = (*slep)[~sfDestinationNode])
{
auto const dst = (*slep)[sfDestination];
if (!view.dirRemove(keylet::ownerDir(dst), *page, key, true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Could not remove paychan from dst owner directory";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
// Transfer amount back to owner, decrement owner count
auto const sle = view.peek(keylet::account(src));
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
XRPL_ASSERT(
(*slep)[sfAmount] >= (*slep)[sfBalance], "xrpl::closeChannel : minimum channel amount");
(*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance];
decreaseOwnerCountForObject(view, sle, slep, 1, j);
view.update(sle);
// Remove PayChan from ledger
view.erase(slep);
return tesSUCCESS;
}
uint32_t
saturatingAdd(Rules const& rules, uint32_t const lhs, uint32_t const rhs)
{
@@ -80,14 +23,4 @@ saturatingAdd(Rules const& rules, uint32_t const lhs, uint32_t const rhs)
return lhs + rhs;
}
bool
isChannelExpired(ApplyView const& view, std::optional<uint32_t> timeField)
{
if (!timeField)
return false;
if (view.rules().enabled(fixCleanup3_2_0))
return after(view.header().parentCloseTime, *timeField);
return view.header().parentCloseTime.time_since_epoch().count() >= *timeField;
}
} // namespace xrpl

View File

@@ -3,6 +3,7 @@
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/entries/PayChannelEntry.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/PaymentChannelHelpers.h>
#include <xrpl/protocol/AccountID.h>
@@ -109,8 +110,8 @@ PaymentChannelClaim::preclaim(PreclaimContext const& ctx)
TER
PaymentChannelClaim::doApply()
{
Keylet const k(ltPAYCHAN, ctx_.tx[sfChannel]);
auto const slep = ctx_.view().peek(k);
PayChannelEntryW slep(
Keylet(ltPAYCHAN, ctx_.tx[sfChannel]), ctx_.view(), ctx_.registry.get().getJournal("View"));
if (!slep)
return tecNO_TARGET;
@@ -119,10 +120,9 @@ PaymentChannelClaim::doApply()
AccountID const txAccount = ctx_.tx[sfAccount];
auto const curExpiration = (*slep)[~sfExpiration];
if (isChannelExpired(ctx_.view(), (*slep)[~sfCancelAfter]) ||
isChannelExpired(ctx_.view(), curExpiration))
if (slep.isExpired((*slep)[~sfCancelAfter]) || slep.isExpired(curExpiration))
{
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View"));
return slep.removeFromLedger();
}
if (txAccount != src && txAccount != dst)
@@ -174,7 +174,7 @@ PaymentChannelClaim::doApply()
reqDelta >= beast::kZero, "xrpl::PaymentChannelClaim::doApply : minimum balance delta");
(*sled)[sfBalance] = (*sled)[sfBalance] + reqDelta;
ctx_.view().update(sled);
ctx_.view().update(slep);
slep.update();
}
if (ctx_.tx.isFlag(tfRenew))
@@ -182,14 +182,14 @@ PaymentChannelClaim::doApply()
if (src != txAccount)
return tecNO_PERMISSION;
(*slep)[~sfExpiration] = std::nullopt;
ctx_.view().update(slep);
slep.update();
}
if (ctx_.tx.isFlag(tfClose))
{
// Channel will close immediately if dry or the receiver closes
if (dst == txAccount || (*slep)[sfBalance] == (*slep)[sfAmount])
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View"));
return slep.removeFromLedger();
auto const settleExpiration = saturatingAdd(
ctx_.view().rules(),
@@ -199,7 +199,7 @@ PaymentChannelClaim::doApply()
if (!curExpiration || *curExpiration > settleExpiration)
{
(*slep)[~sfExpiration] = settleExpiration;
ctx_.view().update(slep);
slep.update();
}
}

View File

@@ -5,12 +5,12 @@
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/PayChannelEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SField.h>
@@ -22,8 +22,6 @@
#include <xrpl/tx/Transactor.h>
#include <xrpl/tx/applySteps.h>
#include <memory>
namespace xrpl {
/*
@@ -169,8 +167,8 @@ PaymentChannelCreate::doApply()
//
// Note that we use the value from the sequence or ticket as the
// payChan sequence. For more explanation see comments in SeqProxy.h.
Keylet const payChanKeylet = keylet::payChannel(account, dst, ctx_.tx.getSeqProxy());
auto const slep = std::make_shared<SLE>(payChanKeylet);
PayChannelEntryW slep(account, dst, ctx_.tx.getSeqProxy(), ctx_.view());
slep.newSLE();
// Funds held in this channel
(*slep)[sfAmount] = ctx_.tx[sfAmount];
@@ -188,12 +186,12 @@ PaymentChannelCreate::doApply()
(*slep)[sfSequence] = ctx_.tx.getSeqProxy().value();
}
ctx_.view().insert(slep);
slep.insert();
// Add PayChan to owner directory
{
auto const page = ctx_.view().dirInsert(
keylet::ownerDir(account), payChanKeylet, describeOwnerDir(account));
keylet::ownerDir(account), slep.keylet(), describeOwnerDir(account));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*slep)[sfOwnerNode] = *page;
@@ -202,7 +200,7 @@ PaymentChannelCreate::doApply()
// Add PayChan to the recipient's owner directory
{
auto const page =
ctx_.view().dirInsert(keylet::ownerDir(dst), payChanKeylet, describeOwnerDir(dst));
ctx_.view().dirInsert(keylet::ownerDir(dst), slep.keylet(), describeOwnerDir(dst));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*slep)[sfDestinationNode] = *page;
@@ -211,7 +209,7 @@ PaymentChannelCreate::doApply()
// Deduct owner's balance, increment owner count
(*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount];
increaseOwnerCount(ctx_.getApplyViewContext(), sle, 1, ctx_.journal);
addSponsorToLedgerEntry(ctx_.getApplyViewContext(), slep);
addSponsorToLedgerEntry(ctx_.getApplyViewContext(), slep.mutableRawSle());
ctx_.view().update(sle);
return tesSUCCESS;

View File

@@ -4,6 +4,7 @@
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/PayChannelEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/PaymentChannelHelpers.h>
#include <xrpl/protocol/AccountID.h>
@@ -43,8 +44,8 @@ PaymentChannelFund::preflight(PreflightContext const& ctx)
TER
PaymentChannelFund::doApply()
{
Keylet const k(ltPAYCHAN, ctx_.tx[sfChannel]);
auto const slep = ctx_.view().peek(k);
PayChannelEntryW slep(
Keylet(ltPAYCHAN, ctx_.tx[sfChannel]), ctx_.view(), ctx_.registry.get().getJournal("View"));
if (!slep)
return tecNO_ENTRY;
@@ -52,10 +53,9 @@ PaymentChannelFund::doApply()
auto const txAccount = ctx_.tx[sfAccount];
auto const curExpiration = (*slep)[~sfExpiration];
if (isChannelExpired(ctx_.view(), (*slep)[~sfCancelAfter]) ||
isChannelExpired(ctx_.view(), curExpiration))
if (slep.isExpired((*slep)[~sfCancelAfter]) || slep.isExpired(curExpiration))
{
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View"));
return slep.removeFromLedger();
}
if (src != txAccount)
@@ -79,7 +79,7 @@ PaymentChannelFund::doApply()
: TER{temBAD_EXPIRATION};
}
(*slep)[~sfExpiration] = *newExpiration;
ctx_.view().update(slep);
slep.update();
}
auto const sle = ctx_.view().peek(keylet::account(txAccount));
@@ -110,7 +110,7 @@ PaymentChannelFund::doApply()
}
(*slep)[sfAmount] = (*slep)[sfAmount] + ctx_.tx[sfAmount];
ctx_.view().update(slep);
slep.update();
(*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount];
ctx_.view().update(sle);

View File

@@ -38,6 +38,7 @@
#include <xrpl/ledger/entries/XChainOwnedClaimIDEntry.h> // IWYU pragma: keep
#include <xrpl/ledger/entries/XChainOwnedCreateAccountClaimIDEntry.h> // IWYU pragma: keep
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
@@ -76,12 +77,22 @@ template class SLEBase<ApplyView>;
#pragma push_macro("LEDGER_ENTRY")
#undef LEDGER_ENTRY
// No explicit instantiation here, neither a definition nor a declaration.
// A definition would instantiate an entry a second time once its member
// bodies live in its own .cpp (MSVC rejects that with C4661 under /WX), and a
// declaration (`extern template`) stops clang from emitting the inline
// constructors of the header-only entries at -O0, which leaves them undefined
// at link time in the debug builds. Naming the class in a static_assert is
// enough to instantiate its definition, which is all this exhaustiveness
// check needs; the per-type suites exercise the members.
#define LEDGER_ENTRY(tag, value, name, ...) \
template class name##Entry<ReadView>; \
template class name##Entry<ApplyView>; \
static_assert( \
name##Entry<ReadView>::kEntryType == tag && name##Entry<ApplyView>::kEntryType == tag, \
#name "Entry must be bound to " #tag);
#name "Entry must be bound to " #tag); \
static_assert( \
std::is_constructible_v<name##Entry<ReadView>, Keylet const&, ReadView const&> && \
std::is_constructible_v<name##Entry<ApplyView>, Keylet const&, ApplyView&>, \
#name "Entry must be constructible from a Keylet and a view");
#include <xrpl/protocol/detail/ledger_entries.macro>