From a1c5ea4e3ce102c4876a6c7c5a956915ecce8ff0 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Tue, 15 Sep 2026 23:07:51 +0100 Subject: [PATCH] test(app): assert online-delete keeps retained ledgers resolvable Adds SHAMapStore_test coverage for the freshen guarantee: after two online-delete rotations delete the first archive, a retained validated ledger must still resolve from the writable backend. The test mutates account state across ledgers, drives two rotations, clears the tree-node and ledger caches so the check reads the backend, then reloads a retained ledger and asserts walkLedger finds no missing node. Catches a regression where the rotation's copy-forward/freshen fails to rewrite an archive-only node into the writable backend: the reload would return null or the walk would report a missing node. --- src/test/app/SHAMapStore_test.cpp | 73 +++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/test/app/SHAMapStore_test.cpp b/src/test/app/SHAMapStore_test.cpp index 4a69ac17f9..23f47d84f6 100644 --- a/src/test/app/SHAMapStore_test.cpp +++ b/src/test/app/SHAMapStore_test.cpp @@ -1035,6 +1035,78 @@ public: BEAST_EXPECT(lastRotated != store.getLastRotated()); } + void + testFreshenKeepsLedgersResolvable() + { + // After a rotation deletes the archived shard, every node of a + // retained validated ledger must still be reachable from the writable + // backend. run() copies the validated state map forward and then + // freshens the caches so a node whose only on-disk copy was in the + // doomed archive is rewritten into the writable backend first. Were + // that copy-forward dropped, reloading a retained ledger from disk once + // the archive is gone would hit a missing node. + // + // Drive two rotations (so the first archive is actually deleted), + // clear the in-memory caches so the check must read the backend, then + // reload a retained ledger and assert it resolves with no missing node. + testcase("freshen keeps retained ledgers resolvable"); + using namespace jtx; + using namespace std::chrono_literals; + + Env env(*this, envconfig(onlineDelete)); + auto& store = env.app().getSHAMapStore(); + auto& lm = env.app().getLedgerMaster(); + + // Real account state, so the state map carries inner nodes that the + // rotation must copy forward rather than an almost-empty tree. + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + auto ledgerSeq = waitForReady(env); + auto lastRotated = store.getLastRotated(); + + // Two rotations. A payment each ledger mutates the state map so + // successive rotations copy forward genuinely different nodes, while + // alice's and bob's account roots persist unchanged across the archive + // boundary -- exactly the nodes the copy-forward must preserve. + for (int rotation = 0; rotation < 2; ++rotation) + { + for (auto const target = store.getLastRotated() + kDeleteInterval + 1; + ledgerSeq < target; + ++ledgerSeq) + { + env(pay(alice, bob, XRP(1))); + env(pay(bob, alice, XRP(1))); + env.close(); + + auto ledger = env.rpc("ledger", "validated"); + BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq), true)); + } + + BEAST_EXPECT(syncStore(env)); + BEAST_EXPECT(store.getLastRotated() != lastRotated); + lastRotated = store.getLastRotated(); + } + + // A validated ledger the store still retains after both rotations. + auto const retainedSeq = lastRotated; + + // Force the walk onto the backend: drop the tree-node cache and the + // cached ledger objects, so getLedgerBySeq must rebuild the ledger from + // the store. A node that was not copied forward is now only in the + // deleted archive, so the rebuild or the walk would fail. + env.app().getNodeFamily().getTreeNodeCache()->clear(); + lm.clearLedgerCachePrior(retainedSeq + 1); + + auto const retained = lm.getLedgerBySeq(retainedSeq); + if (BEAST_EXPECT(retained != nullptr)) + { + BEAST_EXPECT(retained->walkLedger(env.app().getJournal("SHAMapStoreTest"), false)); + } + } + void testCanDelete() { @@ -1925,6 +1997,7 @@ public: testConfig(); testClear(); testAutomatic(); + testFreshenKeepsLedgersResolvable(); testCanDelete(); testRotate(); testLedgerGaps();