mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-27 07:26:51 +00:00
429 lines
18 KiB
C++
429 lines
18 KiB
C++
#include <xrpl/ledger/entries/SLEBase.h>
|
|
|
|
#include <xrpl/beast/utility/Journal.h>
|
|
#include <xrpl/ledger/ApplyView.h>
|
|
#include <xrpl/ledger/ApplyViewImpl.h>
|
|
#include <xrpl/ledger/OpenView.h>
|
|
#include <xrpl/ledger/ReadView.h>
|
|
#include <xrpl/ledger/entries/AMMEntry.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
|
#include <xrpl/ledger/entries/AmendmentsEntry.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/entries/BridgeEntry.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/entries/CheckEntry.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/entries/CredentialEntry.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/entries/DIDEntry.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/entries/DelegateEntry.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/entries/DepositPreauthEntry.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/entries/DirectoryNodeEntry.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/entries/EscrowEntry.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/entries/FeeSettingsEntry.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/entries/LedgerHashesEntry.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/entries/LoanBrokerEntry.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/entries/LoanEntry.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/entries/MPTokenEntry.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/entries/MPTokenIssuanceEntry.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/entries/NFTokenOfferEntry.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/entries/NFTokenPageEntry.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/entries/NegativeUNLEntry.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/entries/OfferEntry.h>
|
|
#include <xrpl/ledger/entries/OracleEntry.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/entries/PayChannelEntry.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/entries/PermissionedDomainEntry.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/entries/RippleStateEntry.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/entries/SignerListEntry.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/entries/SponsorshipEntry.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/entries/TicketEntry.h>
|
|
#include <xrpl/ledger/entries/VaultEntry.h> // IWYU pragma: keep
|
|
#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/LedgerFormats.h>
|
|
#include <xrpl/protocol/SField.h>
|
|
#include <xrpl/protocol/STLedgerEntry.h>
|
|
#include <xrpl/protocol/SeqProxy.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 <stdexcept>
|
|
#include <tuple>
|
|
#include <type_traits>
|
|
|
|
namespace xrpl {
|
|
|
|
// The entry classes have no consumers yet, and an un-instantiated class
|
|
// template is barely type-checked. Instantiate every one explicitly so the
|
|
// compiler actually checks them. Keep this block even once real call sites
|
|
// exist: it is what catches a new ledger entry type being added without its
|
|
// wrapper class, or the wrapper class existing but never actually being used.
|
|
//
|
|
// Driving this off ledger_entries.macro keeps it exhaustive by construction:
|
|
// adding a ledger entry type without adding its entry class stops compiling
|
|
// here, and the static_assert pins each one to the right LedgerEntryType.
|
|
//
|
|
// Keep this loop in one file rather than splitting it across the per-entry
|
|
// *Entry_test.cpp suites. Those are hand-written, so a new ledger entry type
|
|
// would simply have no file there and nothing would complain; this is the only
|
|
// thing making the coverage exhaustive rather than merely extensive.
|
|
|
|
template class SLEBase<ReadView>;
|
|
template class SLEBase<ApplyView>;
|
|
|
|
#pragma push_macro("LEDGER_ENTRY")
|
|
#undef LEDGER_ENTRY
|
|
|
|
#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);
|
|
|
|
#include <xrpl/protocol/detail/ledger_entries.macro>
|
|
|
|
#undef LEDGER_ENTRY
|
|
#pragma pop_macro("LEDGER_ENTRY")
|
|
|
|
// --- Entry-type safety, checked at compile time. ---
|
|
//
|
|
// The writable -> read-only converting constructor is inherited into every
|
|
// per-type entry, so without the entry-type constraint it will bind any
|
|
// writable entry that slices to SLEBase. These assertions pin down which
|
|
// conversions are legal.
|
|
|
|
// An entry class for one entry type must never be constructible from another.
|
|
static_assert(
|
|
!std::is_convertible_v<OfferEntryW, AccountRootEntryR>,
|
|
"cross-entry-type conversion must not compile");
|
|
static_assert(
|
|
!std::is_constructible_v<AccountRootEntryR, OfferEntryW>,
|
|
"cross-entry-type construction must not compile, even explicitly");
|
|
static_assert(
|
|
!std::is_convertible_v<OfferEntryR, AccountRootEntryR>,
|
|
"read-only cross-entry-type conversion must not compile");
|
|
|
|
// Nor from a type-erased writable entry, which carries no static type.
|
|
static_assert(
|
|
!std::is_convertible_v<WritableSLE, AccountRootEntryR>,
|
|
"generic -> typed conversion must not compile");
|
|
|
|
// The intended conversions must keep working: same type writable -> read-only,
|
|
// and typed -> generic widening.
|
|
static_assert(
|
|
std::is_convertible_v<AccountRootEntryW, AccountRootEntryR>,
|
|
"same-type writable -> read-only conversion must keep working");
|
|
static_assert(
|
|
std::is_convertible_v<AccountRootEntryW, ReadOnlySLE>,
|
|
"typed -> generic widening must keep working");
|
|
|
|
// Detection idioms for the writable interface. These have to go through a
|
|
// template parameter: a requires-expression over a concrete type is checked
|
|
// eagerly, so spelling the calls out inline would be a hard error rather than
|
|
// the `false` the assertions below want.
|
|
template <typename T>
|
|
concept HasMutableRawSle = requires(T& t) { t.mutableRawSle(); };
|
|
|
|
template <typename T>
|
|
concept HasApplyView = requires(T& t) { t.applyView(); };
|
|
|
|
namespace test {
|
|
|
|
/**
|
|
* Scaffolding shared by the test cases below: a funded alice, an unfunded bob
|
|
* (for the entries that need to resolve to nothing), and the TxTest ledger
|
|
* they live in.
|
|
*/
|
|
class SLEBaseTests : public ::testing::Test
|
|
{
|
|
protected:
|
|
TxTest env_;
|
|
Account const alice_{"alice"};
|
|
Account const bob_{"bob"};
|
|
|
|
SLEBaseTests()
|
|
{
|
|
env_.createAccount(alice_, XRP(10'000));
|
|
}
|
|
};
|
|
|
|
TEST_F(SLEBaseTests, ReadOnly)
|
|
{
|
|
AccountRootEntryR const absent(bob_.id(), env_.getClosedLedger());
|
|
EXPECT_FALSE(absent.exists());
|
|
EXPECT_FALSE(static_cast<bool>(absent));
|
|
// A typed entry knows its entry type even with nothing to read.
|
|
EXPECT_EQ(absent.type(), ltACCOUNT_ROOT);
|
|
|
|
AccountRootEntryR const present(alice_.id(), env_.getClosedLedger());
|
|
EXPECT_TRUE(present.exists());
|
|
EXPECT_TRUE(static_cast<bool>(present));
|
|
EXPECT_EQ(present.key(), keylet::account(alice_.id()).key);
|
|
EXPECT_EQ(present.type(), ltACCOUNT_ROOT);
|
|
EXPECT_EQ(present.keylet().type, ltACCOUNT_ROOT);
|
|
EXPECT_EQ(present->getType(), ltACCOUNT_ROOT);
|
|
EXPECT_EQ((*present).getType(), ltACCOUNT_ROOT);
|
|
EXPECT_EQ(&present.readView(), &env_.getClosedLedger());
|
|
}
|
|
|
|
TEST_F(SLEBaseTests, AdoptSLE)
|
|
{
|
|
auto const sle = env_.getClosedLedger().read(keylet::account(alice_.id()));
|
|
ASSERT_NE(sle, nullptr);
|
|
|
|
AccountRootEntryR const adopted(sle, env_.getClosedLedger());
|
|
EXPECT_TRUE(adopted.exists());
|
|
EXPECT_EQ(adopted.rawSle(), sle);
|
|
EXPECT_EQ(adopted.key(), keylet::account(alice_.id()).key);
|
|
EXPECT_EQ(adopted.type(), ltACCOUNT_ROOT);
|
|
// keylet() reports the SLE's own type, not the entry's static binding, so
|
|
// it stays truthful in a Release build where the constructor's
|
|
// entry-type assert is compiled out.
|
|
EXPECT_EQ(adopted.keylet().type, ltACCOUNT_ROOT);
|
|
|
|
// Adopting a null SLE is allowed: the assert only fires on a
|
|
// type mismatch, and a null pointer has no type to mismatch.
|
|
AccountRootEntryR const empty(SLE::const_pointer{}, env_.getClosedLedger());
|
|
EXPECT_FALSE(empty.exists());
|
|
EXPECT_EQ(empty.type(), ltACCOUNT_ROOT);
|
|
|
|
// A generic entry adopting the same SLE has to read the type back.
|
|
ReadOnlySLE const generic(sle, env_.getClosedLedger());
|
|
EXPECT_TRUE(generic.exists());
|
|
EXPECT_EQ(generic.type(), ltACCOUNT_ROOT);
|
|
EXPECT_EQ(generic.keylet().type, ltACCOUNT_ROOT);
|
|
|
|
// There is deliberately no writable equivalent.
|
|
static_assert(
|
|
!std::is_constructible_v<AccountRootEntryW, SLE::pointer, ApplyView&>,
|
|
"writable entries must not be constructible from a bare SLE");
|
|
}
|
|
|
|
TEST_F(SLEBaseTests, WritableAccessors)
|
|
{
|
|
ApplyViewImpl av(&env_.getClosedLedger(), TapNone);
|
|
beast::Journal const j{beast::Journal::getNullSink()};
|
|
|
|
AccountRootEntryW account(alice_.id(), av, j);
|
|
EXPECT_TRUE(account.exists());
|
|
EXPECT_EQ(account.mutableRawSle(), account.rawSle());
|
|
EXPECT_EQ(&account.applyView(), &av);
|
|
EXPECT_EQ(&account.readView(), static_cast<ReadView const*>(&av));
|
|
EXPECT_EQ(&account.journal().sink(), &j.sink());
|
|
|
|
// The mutable dereference operators reach the same entry.
|
|
EXPECT_EQ(account.operator->(), account.rawSle().get());
|
|
EXPECT_EQ(&*account, account.rawSle().get());
|
|
|
|
// Everything handing out mutable access is non-const, so a const
|
|
// writable entry is as inert as a read-only one.
|
|
static_assert(HasMutableRawSle<AccountRootEntryW>);
|
|
static_assert(HasApplyView<AccountRootEntryW>);
|
|
static_assert(
|
|
!HasMutableRawSle<AccountRootEntryW const>,
|
|
"mutableRawSle() must not be callable on a const writable entry");
|
|
static_assert(
|
|
!HasApplyView<AccountRootEntryW const>,
|
|
"applyView() must not be callable on a const writable entry");
|
|
|
|
// Read-only entries do not have the writable interface at all.
|
|
static_assert(
|
|
!HasMutableRawSle<AccountRootEntryR>,
|
|
"mutableRawSle() must not exist on a read-only entry");
|
|
static_assert(
|
|
!HasApplyView<AccountRootEntryR>, "applyView() must not exist on a read-only entry");
|
|
}
|
|
|
|
TEST_F(SLEBaseTests, ApplyViewContextCtor)
|
|
{
|
|
ApplyViewImpl av(&env_.getClosedLedger(), TapNone);
|
|
beast::Journal const j{beast::Journal::getNullSink()};
|
|
|
|
transactions::AccountSetBuilder builder{alice_.id()};
|
|
builder.setSequence(env_.getAccountRoot(alice_.id()).getSequence());
|
|
builder.setFee(XRPAmount(10));
|
|
auto const tx = builder.build(alice_.pk(), alice_.sk()).getSTTx();
|
|
ASSERT_NE(tx, nullptr);
|
|
ApplyViewContext const ctx{.view = av, .tx = *tx};
|
|
|
|
// Delegates to the (Keylet, ApplyView&) constructor; ctx.tx is not
|
|
// retained, so this must be indistinguishable from building from
|
|
// ctx.view directly.
|
|
AccountRootEntryW fromCtx(keylet::account(alice_.id()), ctx, j);
|
|
EXPECT_TRUE(fromCtx.exists());
|
|
EXPECT_EQ(&fromCtx.applyView(), &av);
|
|
EXPECT_EQ(fromCtx.key(), keylet::account(alice_.id()).key);
|
|
|
|
AccountRootEntryW const fromView(keylet::account(alice_.id()), av, j);
|
|
EXPECT_EQ(fromCtx.rawSle(), fromView.rawSle());
|
|
}
|
|
|
|
TEST_F(SLEBaseTests, WritableLifecycle)
|
|
{
|
|
// A view we never apply, so nothing here reaches the ledger.
|
|
ApplyViewImpl av(&env_.getClosedLedger(), TapNone);
|
|
|
|
// Entry that does not exist yet: newSLE() -> insert().
|
|
{
|
|
TicketEntryW ticket(keylet::ticket(alice_.id(), SeqProxy::rawTicket(1)), av);
|
|
EXPECT_FALSE(ticket.exists());
|
|
EXPECT_EQ(ticket.key(), keylet::ticket(alice_.id(), SeqProxy::rawTicket(1)).key);
|
|
EXPECT_EQ(ticket.type(), ltTICKET);
|
|
EXPECT_EQ(ticket.keylet().type, ltTICKET);
|
|
|
|
ticket.newSLE();
|
|
EXPECT_TRUE(ticket.exists());
|
|
ticket.insert();
|
|
ticket.update();
|
|
|
|
// Erasing an entry inserted in this same view drops it outright.
|
|
ticket.erase();
|
|
EXPECT_FALSE(ticket.exists());
|
|
}
|
|
|
|
// Entry that already exists: update() is what promotes it from a bare
|
|
// peek to a real change. ApplyViewImpl::size() counts Insert, Modify and
|
|
// Erase but not Cache, so it shows the difference: building the entry
|
|
// only peeks, and the write is invisible to the view until update().
|
|
{
|
|
ApplyViewImpl fresh(&env_.getClosedLedger(), TapNone);
|
|
|
|
AccountRootEntryW account(alice_.id(), fresh);
|
|
EXPECT_TRUE(account.exists());
|
|
EXPECT_EQ(fresh.size(), 0);
|
|
|
|
account->setFieldU32(sfSequence, account->getFieldU32(sfSequence) + 1);
|
|
EXPECT_EQ(fresh.size(), 0);
|
|
|
|
account.update();
|
|
EXPECT_EQ(fresh.size(), 1);
|
|
|
|
// update() is idempotent: the entry is already a Modify.
|
|
account.update();
|
|
EXPECT_EQ(fresh.size(), 1);
|
|
}
|
|
|
|
// Entry that already exists. ApplyStateTable::erase() keeps holding
|
|
// this exact SLE and builds the DeletedNode's FinalFields from it, so
|
|
// the entry must drop its pointer or a later write would silently
|
|
// land in transaction metadata.
|
|
{
|
|
AccountRootEntryW account(alice_.id(), av);
|
|
EXPECT_TRUE(account.exists());
|
|
|
|
account.erase();
|
|
EXPECT_FALSE(account.exists());
|
|
}
|
|
}
|
|
|
|
TEST_F(SLEBaseTests, Conversion)
|
|
{
|
|
ApplyViewImpl av(&env_.getClosedLedger(), TapNone);
|
|
|
|
AccountRootEntryW const writable(alice_.id(), av);
|
|
EXPECT_TRUE(writable.exists());
|
|
|
|
AccountRootEntryR const readOnly = writable;
|
|
EXPECT_TRUE(readOnly.exists());
|
|
EXPECT_EQ(readOnly.rawSle(), writable.rawSle());
|
|
|
|
ReadOnlySLE const generic = writable;
|
|
EXPECT_TRUE(generic.exists());
|
|
EXPECT_EQ(generic.rawSle(), writable.rawSle());
|
|
// A generic entry has to read the type back out of the SLE.
|
|
EXPECT_EQ(generic.type(), ltACCOUNT_ROOT);
|
|
}
|
|
|
|
TEST_F(SLEBaseTests, ResolveEntryPeeks)
|
|
{
|
|
// getOpenLedger() is an OpenView, which derives from ReadView but not
|
|
// from ApplyView, so resolveEntry's dynamic_cast fails and this takes
|
|
// the plain ReadView::read() path.
|
|
OpenView const& ledger = env_.getOpenLedger();
|
|
AccountRootEntryR const overLedger(alice_.id(), ledger);
|
|
EXPECT_TRUE(overLedger.exists());
|
|
|
|
ApplyViewImpl av(&ledger, TapNone);
|
|
|
|
// ReadView const& binds an ApplyViewImpl just as happily, and there the
|
|
// dynamic_cast succeeds, so this one resolves through ApplyView::peek().
|
|
AccountRootEntryR const readOnly(alice_.id(), av);
|
|
EXPECT_TRUE(readOnly.exists());
|
|
|
|
AccountRootEntryW writable(alice_.id(), av);
|
|
EXPECT_TRUE(writable.exists());
|
|
|
|
// The invariant resolveEntry() exists to hold: one SLE per key per
|
|
// view. read() would have handed back the base ledger's entry instead,
|
|
// which is a different object.
|
|
EXPECT_EQ(readOnly.rawSle(), writable.rawSle());
|
|
EXPECT_NE(readOnly.rawSle(), overLedger.rawSle());
|
|
|
|
// Which is what keeps a read-only entry from going stale: a write
|
|
// through any other entry over the same view is visible through it.
|
|
auto const bumped = writable->getFieldU32(sfSequence) + 1;
|
|
writable->setFieldU32(sfSequence, bumped);
|
|
EXPECT_EQ(readOnly->getFieldU32(sfSequence), bumped);
|
|
}
|
|
|
|
TEST_F(SLEBaseTests, ThrowsOnMissingEntry)
|
|
{
|
|
// A generic read-only entry has no static type to fall back on, so
|
|
// type() must read it off the (absent) SLE and throw.
|
|
ReadOnlySLE const absent(keylet::account(bob_.id()), env_.getClosedLedger());
|
|
EXPECT_FALSE(absent.exists());
|
|
EXPECT_THROW(std::ignore = absent.type(), std::logic_error);
|
|
|
|
// A per-type read-only entry always knows its type, but keylet() and
|
|
// key() still have to derive the ledger key from the SLE.
|
|
AccountRootEntryR const missing(bob_.id(), env_.getClosedLedger());
|
|
EXPECT_FALSE(missing.exists());
|
|
EXPECT_THROW(std::ignore = missing.key(), std::logic_error);
|
|
EXPECT_THROW(std::ignore = missing.keylet(), std::logic_error);
|
|
|
|
// Dereferencing an absent entry throws rather than handing back a null
|
|
// pointer for the caller to walk into.
|
|
EXPECT_THROW(std::ignore = missing.operator->(), std::logic_error);
|
|
EXPECT_THROW(std::ignore = (*missing).getType(), std::logic_error);
|
|
}
|
|
|
|
TEST_F(SLEBaseTests, ThrowsOnMissingWritableEntry)
|
|
{
|
|
// A view we never apply, so nothing here reaches the ledger.
|
|
ApplyViewImpl av(&env_.getClosedLedger(), TapNone);
|
|
|
|
// bob is unfunded, so this resolves to nothing and every operation that
|
|
// needs an SLE has to throw instead of dereferencing null. These are the
|
|
// cases a Release build used to walk straight past, back when they were
|
|
// XRPL_ASSERTs.
|
|
AccountRootEntryW missing(bob_.id(), av);
|
|
EXPECT_FALSE(missing.exists());
|
|
|
|
EXPECT_THROW(std::ignore = missing.operator->(), std::logic_error);
|
|
EXPECT_THROW(std::ignore = (*missing).getType(), std::logic_error);
|
|
EXPECT_THROW(missing.insert(), std::logic_error);
|
|
EXPECT_THROW(missing.update(), std::logic_error);
|
|
EXPECT_THROW(missing.erase(), std::logic_error);
|
|
|
|
// keylet() and key() are the exception: a writable entry keeps the keylet
|
|
// it was built from, so they stay valid before newSLE().
|
|
EXPECT_EQ(missing.key(), keylet::account(bob_.id()).key);
|
|
|
|
// newSLE() is the inverse -- it throws when the entry *does* exist,
|
|
// rather than silently dropping the SLE already held.
|
|
missing.newSLE();
|
|
EXPECT_TRUE(missing.exists());
|
|
EXPECT_THROW(missing.newSLE(), std::logic_error);
|
|
|
|
// And once erased, the entry is empty again and throws as before.
|
|
missing.insert();
|
|
missing.erase();
|
|
EXPECT_FALSE(missing.exists());
|
|
EXPECT_THROW(missing.update(), std::logic_error);
|
|
}
|
|
|
|
} // namespace test
|
|
} // namespace xrpl
|