#include #include #include #include #include #include #include // IWYU pragma: keep #include #include // IWYU pragma: keep #include // IWYU pragma: keep #include // IWYU pragma: keep #include // IWYU pragma: keep #include // IWYU pragma: keep #include // IWYU pragma: keep #include // IWYU pragma: keep #include // IWYU pragma: keep #include // IWYU pragma: keep #include // IWYU pragma: keep #include // IWYU pragma: keep #include // IWYU pragma: keep #include // IWYU pragma: keep #include // IWYU pragma: keep #include // IWYU pragma: keep #include // IWYU pragma: keep #include // IWYU pragma: keep #include // IWYU pragma: keep #include #include // IWYU pragma: keep #include // IWYU pragma: keep #include // IWYU pragma: keep #include // IWYU pragma: keep #include // IWYU pragma: keep #include // IWYU pragma: keep #include #include // IWYU pragma: keep #include // IWYU pragma: keep #include // IWYU pragma: keep #include #include #include #include #include #include #include #include #include #include #include #include #include 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; template class SLEBase; #pragma push_macro("LEDGER_ENTRY") #undef LEDGER_ENTRY #define LEDGER_ENTRY(tag, value, name, ...) \ template class name##Entry; \ template class name##Entry; \ static_assert( \ name##Entry::kEntryType == tag && name##Entry::kEntryType == tag, \ #name "Entry must be bound to " #tag); #include #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, "cross-entry-type conversion must not compile"); static_assert( !std::is_constructible_v, "cross-entry-type construction must not compile, even explicitly"); static_assert( !std::is_convertible_v, "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, "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, "same-type writable -> read-only conversion must keep working"); static_assert( std::is_convertible_v, "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 concept HasMutableRawSle = requires(T& t) { t.mutableRawSle(); }; template 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(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(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, "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(&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); static_assert(HasApplyView); static_assert( !HasMutableRawSle, "mutableRawSle() must not be callable on a const writable entry"); static_assert( !HasApplyView, "applyView() must not be callable on a const writable entry"); // Read-only entries do not have the writable interface at all. static_assert( !HasMutableRawSle, "mutableRawSle() must not exist on a read-only entry"); static_assert( !HasApplyView, "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