From a7635c2ef8644398b3979e554b1b09a1a3c7aebd Mon Sep 17 00:00:00 2001 From: Garand Tyson Date: Fri, 7 Mar 2025 15:31:26 -0800 Subject: [PATCH 1/2] Initialize Soroban live state cache on startup --- src/bucket/BucketSnapshot.cpp | 38 +++++ src/bucket/BucketSnapshot.h | 3 + src/bucket/InMemoryIndex.cpp | 32 +++++ src/bucket/InMemoryIndex.h | 9 ++ src/bucket/LiveBucket.cpp | 6 + src/bucket/LiveBucket.h | 6 + src/bucket/LiveBucketIndex.cpp | 24 ++++ src/bucket/LiveBucketIndex.h | 3 + src/bucket/SearchableBucketList.cpp | 10 ++ src/bucket/SearchableBucketList.h | 5 + src/bucket/test/BucketIndexTests.cpp | 171 +++++++++++++++++++++-- src/catchup/AssumeStateWork.cpp | 4 + src/ledger/LedgerManager.h | 8 ++ src/ledger/LedgerManagerImpl.cpp | 66 +++++++++ src/ledger/LedgerManagerImpl.h | 13 ++ src/ledger/LedgerStateCache.cpp | 70 ++++++++++ src/ledger/LedgerStateCache.h | 198 +++++++++++++++++++++++++++ 17 files changed, 652 insertions(+), 14 deletions(-) create mode 100644 src/ledger/LedgerStateCache.cpp create mode 100644 src/ledger/LedgerStateCache.h diff --git a/src/bucket/BucketSnapshot.cpp b/src/bucket/BucketSnapshot.cpp index b819c0959e..db2e110a59 100644 --- a/src/bucket/BucketSnapshot.cpp +++ b/src/bucket/BucketSnapshot.cpp @@ -317,6 +317,44 @@ LiveBucketSnapshot::scanForEviction( return Loop::INCOMPLETE; } +Loop +LiveBucketSnapshot::scanForSorobanEntries( + std::function callback) const +{ + ZoneScoped; + if (isEmpty()) + { + return Loop::INCOMPLETE; + } + + auto range = mBucket->getSorobanRange(); + if (!range) + { + return Loop::INCOMPLETE; + } + + // Open new stream to not interfere with TTL lookups during the scan + XDRInputFileStream stream{}; + stream.open(mBucket->getFilename()); + stream.seek(range->first); + + BucketEntry be; + while (stream.pos() < range->second && stream.readOne(be)) + { + if (((be.type() == LIVEENTRY || be.type() == INITENTRY) && + isSorobanEntry(be.liveEntry().data)) || + (be.type() == DEADENTRY && isSorobanEntry(be.deadEntry()))) + { + if (callback(be) == Loop::COMPLETE) + { + return Loop::COMPLETE; + } + } + } + + return Loop::INCOMPLETE; +} + template XDRInputFileStream& BucketSnapshotBase::getStream() const diff --git a/src/bucket/BucketSnapshot.h b/src/bucket/BucketSnapshot.h index 640b3ca228..2ed4779a48 100644 --- a/src/bucket/BucketSnapshot.h +++ b/src/bucket/BucketSnapshot.h @@ -87,6 +87,9 @@ class LiveBucketSnapshot : public BucketSnapshotBase std::list& evictableKeys, SearchableLiveBucketListSnapshot const& bl, uint32_t ledgerVers) const; + + Loop scanForSorobanEntries( + std::function callback) const; }; class HotArchiveBucketSnapshot : public BucketSnapshotBase diff --git a/src/bucket/InMemoryIndex.cpp b/src/bucket/InMemoryIndex.cpp index 28a014e0a2..217d6154de 100644 --- a/src/bucket/InMemoryIndex.cpp +++ b/src/bucket/InMemoryIndex.cpp @@ -6,6 +6,7 @@ #include "bucket/BucketManager.h" #include "bucket/LedgerCmp.h" #include "bucket/LiveBucket.h" +#include "ledger/LedgerTypeUtils.h" #include "util/XDRStream.h" #include "util/types.h" #include "xdr/Stellar-ledger-entries.h" @@ -60,6 +61,8 @@ InMemoryIndex::InMemoryIndex(BucketManager const& bm, std::streamoff lastOffset = 0; std::optional firstOffer; std::optional lastOffer; + std::optional firstSoroban; + std::optional lastSoroban; while (in && in.readOne(be, hasher)) { @@ -109,6 +112,16 @@ InMemoryIndex::InMemoryIndex(BucketManager const& bm, lastOffer = lastOffset; } + // populate sorobanRange + if (!firstSoroban && isSorobanEntry(lk)) + { + firstSoroban = lastOffset; + } + if (!lastSoroban && lk.type() > CONTRACT_CODE) + { + lastSoroban = lastOffset; + } + lastOffset = in.pos(); } @@ -130,5 +143,24 @@ InMemoryIndex::InMemoryIndex(BucketManager const& bm, { mOfferRange = std::nullopt; } + + if (firstSoroban) + { + if (lastSoroban) + { + mSorobanRange = {*firstSoroban, *lastSoroban}; + } + // If we didn't see any entries after our last data/code entry, then the + // upper bound is EOF + else + { + mSorobanRange = {*firstSoroban, + std::numeric_limits::max()}; + } + } + else + { + mSorobanRange = std::nullopt; + } } } \ No newline at end of file diff --git a/src/bucket/InMemoryIndex.h b/src/bucket/InMemoryIndex.h index bf5efa58de..d7f1b15f17 100644 --- a/src/bucket/InMemoryIndex.h +++ b/src/bucket/InMemoryIndex.h @@ -64,6 +64,9 @@ class InMemoryIndex BucketEntryCounters mCounters{}; std::optional> mOfferRange; + // Range of data/code entries in the Bucket + std::optional> mSorobanRange; + public: using IterT = InMemoryBucketState::IterT; @@ -105,6 +108,12 @@ class InMemoryIndex return mOfferRange; } + std::optional> + getSorobanRange() const + { + return mSorobanRange; + } + #ifdef BUILD_TESTS bool operator==(InMemoryIndex const& in) const diff --git a/src/bucket/LiveBucket.cpp b/src/bucket/LiveBucket.cpp index feef5698b1..3504eda6fd 100644 --- a/src/bucket/LiveBucket.cpp +++ b/src/bucket/LiveBucket.cpp @@ -327,6 +327,12 @@ LiveBucket::getOfferRange() const return getIndex().getOfferRange(); } +std::optional> +LiveBucket::getSorobanRange() const +{ + return getIndex().getSorobanRange(); +} + std::vector LiveBucket::convertToBucketEntry(bool useInit, std::vector const& initEntries, diff --git a/src/bucket/LiveBucket.h b/src/bucket/LiveBucket.h index dfca865028..925b374e98 100644 --- a/src/bucket/LiveBucket.h +++ b/src/bucket/LiveBucket.h @@ -91,6 +91,12 @@ class LiveBucket : public BucketBase, std::optional> getOfferRange() const; + // Returns [lowerBound, upperBound) of file offsets for all soroban entries + // (CONTRACT_DATA and CONTRACT_CODE) in the bucket, or std::nullopt if no + // soroban entries exist + std::optional> + getSorobanRange() const; + // Create a fresh bucket from given vectors of init (created) and live // (updated) LedgerEntries, and dead LedgerEntryKeys. The bucket will // be sorted, hashed, and adopted in the provided BucketManager. diff --git a/src/bucket/LiveBucketIndex.cpp b/src/bucket/LiveBucketIndex.cpp index 90756808df..8a8c41f96a 100644 --- a/src/bucket/LiveBucketIndex.cpp +++ b/src/bucket/LiveBucketIndex.cpp @@ -295,6 +295,30 @@ LiveBucketIndex::getOfferRange() const return mInMemoryIndex->getOfferRange(); } +std::optional> +LiveBucketIndex::getSorobanRange() const +{ + if (mDiskIndex) + { + // LedgerKey ordering in the bucket is CONTRACT_DATA, CONTRACT_CODE, + // so for the range we need the smallest CONTRACT_DATA entry and the + // largest CONTRACT_CODE entry. + LedgerKey lowerBound(CONTRACT_DATA); + lowerBound.contractData().contract.type(SC_ADDRESS_TYPE_ACCOUNT); + lowerBound.contractData().contract.accountId().ed25519().fill( + std::numeric_limits::min()); + + LedgerKey upperBound(CONTRACT_CODE); + upperBound.contractCode().hash.fill( + std::numeric_limits::max()); + + return mDiskIndex->getOffsetBounds(lowerBound, upperBound); + } + + releaseAssertOrThrow(mInMemoryIndex); + return mInMemoryIndex->getSorobanRange(); +} + uint32_t LiveBucketIndex::getPageSize() const { diff --git a/src/bucket/LiveBucketIndex.h b/src/bucket/LiveBucketIndex.h index abef94058f..e10589abdd 100644 --- a/src/bucket/LiveBucketIndex.h +++ b/src/bucket/LiveBucketIndex.h @@ -136,6 +136,9 @@ class LiveBucketIndex : public NonMovableOrCopyable std::optional> getOfferRange() const; + std::optional> + getSorobanRange() const; + void maybeAddToCache(std::shared_ptr const& entry) const; BucketEntryCounters const& getBucketEntryCounters() const; diff --git a/src/bucket/SearchableBucketList.cpp b/src/bucket/SearchableBucketList.cpp index 31631ba3b6..7e0e5ed4a9 100644 --- a/src/bucket/SearchableBucketList.cpp +++ b/src/bucket/SearchableBucketList.cpp @@ -64,6 +64,16 @@ SearchableLiveBucketListSnapshot::scanForEviction( return result; } +void +SearchableLiveBucketListSnapshot::scanForSorobanEntries( + std::function callback) const +{ + releaseAssert(mSnapshot); + auto f = [&](auto const& b) { return b.scanForSorobanEntries(callback); }; + + loopAllBuckets(f, *mSnapshot); +} + // This query has two steps: // 1. For each bucket, determine what PoolIDs contain the target asset via the // assetToPoolID index diff --git a/src/bucket/SearchableBucketList.h b/src/bucket/SearchableBucketList.h index a033402a60..3e75aff3da 100644 --- a/src/bucket/SearchableBucketList.h +++ b/src/bucket/SearchableBucketList.h @@ -35,6 +35,11 @@ class SearchableLiveBucketListSnapshot std::shared_ptr stats, StateArchivalSettings const& sas, uint32_t ledgerVers) const; + // Calls callback on each CONTRACT_CODE and CONTRACT_DATA entry in + // BucketList + void scanForSorobanEntries( + std::function callback) const; + friend SearchableSnapshotConstPtr BucketSnapshotManager::copySearchableLiveBucketListSnapshot() const; }; diff --git a/src/bucket/test/BucketIndexTests.cpp b/src/bucket/test/BucketIndexTests.cpp index fdc559006c..07b9e7ce05 100644 --- a/src/bucket/test/BucketIndexTests.cpp +++ b/src/bucket/test/BucketIndexTests.cpp @@ -11,12 +11,14 @@ #include "bucket/LiveBucket.h" #include "bucket/LiveBucketList.h" #include "bucket/test/BucketTestUtils.h" +#include "ledger/LedgerTypeUtils.h" #include "ledger/test/LedgerTestUtils.h" #include "lib/catch.hpp" #include "main/Application.h" #include "main/Config.h" #include "test/test.h" +#include "util/GlobalChecks.h" #include "util/UnorderedMap.h" #include "util/UnorderedSet.h" #include "util/XDRCereal.h" @@ -38,6 +40,9 @@ class BucketIndexTest UnorderedMap mTestEntries; UnorderedSet mGeneratedKeys; + UnorderedMap mContractCodeEntries; + UnorderedMap mContractDataEntries; + // Set of keys to query BucketList for LedgerKeySet mKeysToSearch; stellar::uniform_int_distribution mDist; @@ -66,19 +71,57 @@ class BucketIndexTest void buildBucketList(std::function&)> f, - bool isCacheTest = false) + bool isCacheTest = false, bool sorobanOnly = false) { + releaseAssertOrThrow(!(isCacheTest && sorobanOnly)); + uint32_t ledger = 0; do { ++ledger; - std::vector entries = - isCacheTest - ? LedgerTestUtils:: - generateValidUniqueLedgerEntriesWithTypes( - {ACCOUNT}, 10, mGeneratedKeys) - : LedgerTestUtils::generateValidLedgerEntriesWithExclusions( - {CONFIG_SETTING}, 10); + std::vector entries; + if (!isCacheTest && !sorobanOnly) + { + entries = + LedgerTestUtils::generateValidLedgerEntriesWithExclusions( + {CONFIG_SETTING}, 10); + } + else if (isCacheTest) + { + entries = + LedgerTestUtils::generateValidUniqueLedgerEntriesWithTypes( + {ACCOUNT}, 10, mGeneratedKeys); + } + else if (sorobanOnly) + { + + auto sorobanEntries = + LedgerTestUtils::generateValidUniqueLedgerEntriesWithTypes( + {CONTRACT_DATA, CONTRACT_CODE}, 10, mGeneratedKeys); + + // Insert TTL for each entry + for (auto& e : sorobanEntries) + { + LedgerEntry ttl; + ttl.data.type(TTL); + ttl.data.ttl().keyHash = getTTLKey(e).ttl().keyHash; + // Make sure entries don't expire + ttl.data.ttl().liveUntilLedgerSeq = ledger + 10'000; + entries.emplace_back(ttl); + + if (e.data.type() == CONTRACT_CODE) + { + mContractCodeEntries.emplace(LedgerEntryKey(e), e); + } + else if (e.data.type() == CONTRACT_DATA) + { + mContractDataEntries.emplace(LedgerEntryKey(e), e); + } + } + entries.insert(entries.end(), sorobanEntries.begin(), + sorobanEntries.end()); + } + f(entries); closeLedger(*mApp); } while (!LiveBucketList::levelShouldSpill(ledger, mLevelsToBuild - 1)); @@ -98,6 +141,24 @@ class BucketIndexTest return mApp->getBucketManager(); } + Application& + getApp() const + { + return *mApp; + } + + UnorderedMap const& + getContractCodeEntries() const + { + return mContractCodeEntries; + } + + UnorderedMap const& + getContractDataEntries() const + { + return mContractDataEntries; + } + virtual void buildGeneralTest(bool isCacheTest = false) { @@ -174,7 +235,7 @@ class BucketIndexTest } virtual void - buildMultiVersionTest() + buildMultiVersionTest(bool sorobanOnly = false) { std::vector toDestroy; std::vector toUpdate; @@ -184,16 +245,46 @@ class BucketIndexTest { for (auto& e : toUpdate) { - e.data.account().balance += 1; + e.lastModifiedLedgerSeq++; auto iter = mTestEntries.find(LedgerEntryKey(e)); iter->second = e; + + if (sorobanOnly) + { + if (e.data.type() == CONTRACT_CODE) + { + mContractCodeEntries.emplace(LedgerEntryKey(e), e); + } + else if (e.data.type() == CONTRACT_DATA) + { + mContractDataEntries.emplace(LedgerEntryKey(e), e); + } + } } for (auto const& k : toDestroy) { mTestEntries.erase(k); + if (sorobanOnly) + { + if (k.type() == CONTRACT_CODE) + { + mContractCodeEntries.erase(k); + } + else if (k.type() == CONTRACT_DATA) + { + mContractDataEntries.erase(k); + } + } } + // Other test types don't guarantee unique key generation, so a + // key in entries may also be in toUpdate + if (sorobanOnly) + { + toUpdate.insert(toUpdate.end(), entries.begin(), + entries.end()); + } mApp->getLedgerManager() .setNextLedgerEntryBatchForBucketTesting({}, toUpdate, toDestroy); @@ -209,11 +300,12 @@ class BucketIndexTest { mTestEntries.emplace(LedgerEntryKey(e), e); mKeysToSearch.emplace(LedgerEntryKey(e)); - if (e.data.type() == ACCOUNT) + if (rand_flip()) { toUpdate.emplace_back(e); } - else + // Never destroy TTL keys to preserve invariant + else if (e.data.type() != TTL) { toDestroy.emplace_back(LedgerEntryKey(e)); } @@ -225,7 +317,7 @@ class BucketIndexTest } }; - buildBucketList(f); + buildBucketList(f, /*isCacheTest=*/false, sorobanOnly); } void @@ -578,7 +670,7 @@ class BucketIndexPoolShareTest : public BucketIndexTest } virtual void - buildMultiVersionTest() override + buildMultiVersionTest(bool sorobanOnly = false) override { buildTest(true); } @@ -734,6 +826,57 @@ TEST_CASE("bl cache", "[bucket][bucketindex]") totalAccountCount * (expectedCachedRatio + 0.15)); } +// Note: this test checks that the soroban cache is initialized correctly, but +// does not check that the cache is maintained as ledger state changes. +TEST_CASE("soroban cache initialization", "[soroban]") +{ + auto f = [&](Config& cfg) { + auto test = BucketIndexTest(cfg); + test.buildMultiVersionTest(/*sorobanOnly=*/true); + test.run(); + + auto& lm = test.getApp().getLedgerManager(); + lm.clearLedgerStateCacheForTesting(); + lm.populateApplyStateCacheFromBucketList(); + auto& cache = lm.getLedgerStateCacheForTesting(); + auto codeEntries = test.getContractCodeEntries(); + auto dataEntries = test.getContractDataEntries(); + + auto snapshot = test.getBM() + .getBucketSnapshotManager() + .copySearchableLiveBucketListSnapshot(); + + REQUIRE(codeEntries.size() == cache.mContractCodeTTLs.size()); + for (auto const& [k, v] : codeEntries) + { + auto ttl = cache.getContractCodeTTL(k); + REQUIRE(ttl); + + auto ttlEntry = snapshot->load(getTTLKey(k)); + REQUIRE(ttlEntry); + REQUIRE(ttlEntry->data.ttl().liveUntilLedgerSeq == ttl); + } + + REQUIRE(dataEntries.size() == cache.mEntries.size()); + for (auto const& [k, v] : dataEntries) + { + auto cacheEntry = cache.getContractDataEntry(k); + REQUIRE(cacheEntry); + + auto ttlEntry = snapshot->load(getTTLKey(k)); + REQUIRE(ttlEntry); + REQUIRE(ttlEntry->data.ttl().liveUntilLedgerSeq == + cacheEntry->liveUntilLedgerSeq); + + auto liveEntry = snapshot->load(k); + REQUIRE(liveEntry); + REQUIRE(*liveEntry == *cacheEntry->ledgerEntry); + } + }; + + testAllIndexTypes(f); +} + TEST_CASE("do not load outdated values", "[bucket][bucketindex]") { auto f = [&](Config& cfg) { diff --git a/src/catchup/AssumeStateWork.cpp b/src/catchup/AssumeStateWork.cpp index 9460d0fb03..1f0698c3b6 100644 --- a/src/catchup/AssumeStateWork.cpp +++ b/src/catchup/AssumeStateWork.cpp @@ -9,6 +9,7 @@ #include "crypto/Hex.h" #include "history/HistoryArchive.h" #include "invariant/InvariantManager.h" +#include "ledger/LedgerManager.h" #include "work/WorkSequence.h" #include "work/WorkWithCallback.h" @@ -81,6 +82,9 @@ AssumeStateWork::doWork() // Check invariants after state has been assumed app.getInvariantManager().checkAfterAssumeState(has.currentLedger); + // Populate the ledger apply cache with assumed BucketList state + app.getLedgerManager().populateApplyStateCacheFromBucketList(); + return true; }; auto work = std::make_shared(mApp, "assume-state", diff --git a/src/ledger/LedgerManager.h b/src/ledger/LedgerManager.h index 5094196de8..e349d6b6f9 100644 --- a/src/ledger/LedgerManager.h +++ b/src/ledger/LedgerManager.h @@ -15,6 +15,7 @@ namespace stellar class LedgerCloseData; class Database; class SorobanMetrics; +class LedgerStateCache; // This diagram provides a schematic of the flow of (logical) ledgers coming in // from the SCP-and-Herder consensus complex, passing through the @@ -302,11 +303,18 @@ class LedgerManager { applyLedger(ledgerData, /* externalize */ false); } + + virtual LedgerStateCache const& getLedgerStateCacheForTesting() const = 0; + virtual void clearLedgerStateCacheForTesting() = 0; #endif virtual void setLastClosedLedger(LedgerHeaderHistoryEntry const& lastClosed) = 0; + // Populates the live Soroban state cache based on the current live + // BucketList. + virtual void populateApplyStateCacheFromBucketList() = 0; + virtual void manuallyAdvanceLedgerHeader(LedgerHeader const& header) = 0; virtual SorobanMetrics& getSorobanMetrics() = 0; diff --git a/src/ledger/LedgerManagerImpl.cpp b/src/ledger/LedgerManagerImpl.cpp index 6e01e72abc..44c7e40ec7 100644 --- a/src/ledger/LedgerManagerImpl.cpp +++ b/src/ledger/LedgerManagerImpl.cpp @@ -23,6 +23,7 @@ #include "ledger/LedgerTxn.h" #include "ledger/LedgerTxnEntry.h" #include "ledger/LedgerTxnHeader.h" +#include "ledger/LedgerTypeUtils.h" #include "main/Application.h" #include "main/Config.h" #include "main/ErrorMessages.h" @@ -40,6 +41,7 @@ #include "util/ProtocolVersion.h" #include "util/XDRCereal.h" #include "util/XDRStream.h" +#include "util/types.h" #include "work/WorkScheduler.h" #include "xdrpp/printer.h" @@ -546,6 +548,14 @@ LedgerManagerImpl::storeCurrentLedgerForTest(LedgerHeader const& header) { storePersistentStateAndLedgerHeaderInDB(header, true); } + +LedgerStateCache const& +LedgerManagerImpl::getLedgerStateCacheForTesting() const +{ + releaseAssertOrThrow(mApplyState.mLedgerStateCache); + return *mApplyState.mLedgerStateCache; +} + #endif SorobanMetrics& @@ -1133,6 +1143,62 @@ LedgerManagerImpl::setLastClosedLedger( } } +void +LedgerManagerImpl::populateApplyStateCacheFromBucketList() +{ + releaseAssertOrThrow(!mApplyState.mLedgerStateCache); + mApplyState.mLedgerStateCache = std::make_unique(); + + std::unordered_set deletedKeys; + auto bl = getLastClosedSnaphot(); + auto f = [&cache = *mApplyState.mLedgerStateCache, &deletedKeys, + &bl](BucketEntry const& be) { + if (be.type() == DEADENTRY) + { + deletedKeys.insert(be.deadEntry()); + return Loop::INCOMPLETE; + } + + releaseAssertOrThrow(be.type() == LIVEENTRY || be.type() == INITENTRY); + + auto lk = LedgerEntryKey(be.liveEntry()); + releaseAssertOrThrow(lk.type() == CONTRACT_CODE || + lk.type() == CONTRACT_DATA); + + // Skip key if we've already cached it, or if it has been deleted by an + // earlier BucketEntry + if (deletedKeys.find(lk) != deletedKeys.end()) + { + return Loop::INCOMPLETE; + } + if (lk.type() == CONTRACT_CODE && cache.getContractCodeTTL(lk)) + { + return Loop::INCOMPLETE; + } + else if (lk.type() == CONTRACT_DATA && cache.getContractDataEntry(lk)) + { + return Loop::INCOMPLETE; + } + + auto ttlEntry = bl->load(getTTLKey(lk)); + releaseAssertOrThrow(ttlEntry); + if (lk.type() == CONTRACT_CODE) + { + cache.addContractCodeTTL(lk, + ttlEntry->data.ttl().liveUntilLedgerSeq); + } + else + { + cache.addContractDataEntry(be.liveEntry(), + ttlEntry->data.ttl().liveUntilLedgerSeq); + } + + return Loop::INCOMPLETE; + }; + + bl->scanForSorobanEntries(f); +} + void LedgerManagerImpl::manuallyAdvanceLedgerHeader(LedgerHeader const& header) { diff --git a/src/ledger/LedgerManagerImpl.h b/src/ledger/LedgerManagerImpl.h index 61abb7152c..01704a89e6 100644 --- a/src/ledger/LedgerManagerImpl.h +++ b/src/ledger/LedgerManagerImpl.h @@ -8,6 +8,7 @@ #include "history/HistoryManager.h" #include "ledger/LedgerCloseMetaFrame.h" #include "ledger/LedgerManager.h" +#include "ledger/LedgerStateCache.h" #include "ledger/NetworkConfig.h" #include "ledger/SorobanMetrics.h" #include "main/PersistentState.h" @@ -68,6 +69,9 @@ class LedgerManagerImpl : public LedgerManager // this variable is not synchronized, since it should only be used by // one thread (main or ledger close). std::shared_ptr mSorobanNetworkConfig; + + // Cache of live Soroban state for the current ledger. + std::unique_ptr mLedgerStateCache; }; struct LedgerApplyMetrics @@ -217,6 +221,13 @@ class LedgerManagerImpl : public LedgerManager getLastClosedLedgerTxMeta() override; TransactionResultSet mLatestTxResultSet{}; void storeCurrentLedgerForTest(LedgerHeader const& header) override; + + LedgerStateCache const& getLedgerStateCacheForTesting() const override; + void + clearLedgerStateCacheForTesting() override + { + mApplyState.mLedgerStateCache = nullptr; + } #endif uint64_t secondsSinceLastLedgerClose() const override; @@ -244,6 +255,8 @@ class LedgerManagerImpl : public LedgerManager void setLastClosedLedger(LedgerHeaderHistoryEntry const& lastClosed) override; + void populateApplyStateCacheFromBucketList() override; + void manuallyAdvanceLedgerHeader(LedgerHeader const& header) override; void setupLedgerCloseMetaStream(); diff --git a/src/ledger/LedgerStateCache.cpp b/src/ledger/LedgerStateCache.cpp new file mode 100644 index 0000000000..790e2b5d02 --- /dev/null +++ b/src/ledger/LedgerStateCache.cpp @@ -0,0 +1,70 @@ +// Copyright 2025 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +#include "ledger/LedgerStateCache.h" +#include "util/GlobalChecks.h" + +namespace stellar +{ + +void +LedgerStateCache::addContractDataEntry(LedgerEntry const& ledgerEntry, + uint32_t liveUntilLedgerSeq) +{ + mEntries.emplace( + InternalContractDataCacheEntry(ledgerEntry, liveUntilLedgerSeq)); +} + +void +LedgerStateCache::addContractCodeTTL(LedgerKey const& ledgerKey, + uint32_t liveUntilLedgerSeq) +{ + mContractCodeTTLs.emplace(ledgerKey, liveUntilLedgerSeq); +} + +void +LedgerStateCache::evictKey(LedgerKey const& ledgerKey) +{ + if (ledgerKey.type() == LedgerEntryType::CONTRACT_DATA) + { + mEntries.erase(InternalContractDataCacheEntry(ledgerKey)); + } + else if (ledgerKey.type() == LedgerEntryType::CONTRACT_CODE) + { + mContractCodeTTLs.erase(ledgerKey); + } + else + { + throw std::runtime_error("LedgerStateCache: Invalid ledger key type"); + } +} + +std::optional +LedgerStateCache::getContractDataEntry(LedgerKey const& ledgerKey) const +{ + releaseAssertOrThrow(ledgerKey.type() == LedgerEntryType::CONTRACT_DATA); + + auto it = mEntries.find(InternalContractDataCacheEntry(ledgerKey)); + if (it == mEntries.end()) + { + return std::nullopt; + } + + return it->get(); +} + +std::optional +LedgerStateCache::getContractCodeTTL(LedgerKey const& ledgerKey) const +{ + releaseAssertOrThrow(ledgerKey.type() == LedgerEntryType::CONTRACT_CODE); + + auto it = mContractCodeTTLs.find(ledgerKey); + if (it == mContractCodeTTLs.end()) + { + return std::nullopt; + } + + return it->second; +} +} diff --git a/src/ledger/LedgerStateCache.h b/src/ledger/LedgerStateCache.h new file mode 100644 index 0000000000..d2736294c7 --- /dev/null +++ b/src/ledger/LedgerStateCache.h @@ -0,0 +1,198 @@ +#pragma once + +// Copyright 2025 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +#include +#include +#include +#include + +#include "ledger/LedgerHashUtils.h" +#include "util/NonCopyable.h" +#include "util/types.h" + +namespace stellar +{ + +struct ContractDataCacheT +{ + std::shared_ptr ledgerEntry; + uint32_t liveUntilLedgerSeq; + + explicit ContractDataCacheT(LedgerEntry const& ledgerEntry, + uint32_t liveUntilLedgerSeq) + : ledgerEntry(std::make_shared(ledgerEntry)) + , liveUntilLedgerSeq(liveUntilLedgerSeq) + { + } +}; + +// Soroban keys sizes usually dominate LedgerEntry size, so we don't want to +// store a key-value map to be memory efficient. Instead, we store a set of +// InternalContractDataCacheEntry objects, which is a wrapper around either a +// LedgerKey or cache entry. This allows us to use std::unordered_set to +// efficiently store cache entries, but allows lookup by key only. +// Note that C++20 allows heterogeneous lookup in unordered_set, so we can +// simplify this class once we upgrade. +class InternalContractDataCacheEntry +{ + private: + struct AbstractEntry + { + virtual ~AbstractEntry() = default; + virtual LedgerKey copyKey() const = 0; + virtual size_t hash() const = 0; + virtual ContractDataCacheT const& get() const = 0; + + virtual bool + operator==(const AbstractEntry& other) const + { + return copyKey() == other.copyKey(); + } + }; + + // "Value" entry type used for storing ContractData entries in cache + struct ValueEntry : public AbstractEntry + { + private: + ContractDataCacheT entry; + + public: + ValueEntry(LedgerEntry const& ledgerEntry, uint32_t liveUntilLedgerSeq) + : entry(ledgerEntry, liveUntilLedgerSeq) + { + } + + LedgerKey + copyKey() const override + { + return LedgerEntryKey(*entry.ledgerEntry); + } + + size_t + hash() const override + { + return std::hash{}(LedgerEntryKey(*entry.ledgerEntry)); + } + + ContractDataCacheT const& + get() const override + { + return entry; + } + }; + + // "Key" entry type only used for querying the cache + // Warning: We take a reference to the LedgerKey here to avoid extra copies + // for lookups, so the caller must ensure that the LedgerKey remains valid + // for the lifetime of the QueryKey object. + struct QueryKey : public AbstractEntry + { + private: + LedgerKey const& ledgerKey; + + public: + QueryKey(LedgerKey const& ledgerKey) : ledgerKey(ledgerKey) + { + } + + LedgerKey + copyKey() const override + { + return ledgerKey; + } + + size_t + hash() const override + { + return std::hash{}(ledgerKey); + } + + ContractDataCacheT const& + get() const override + { + throw std::runtime_error("Called get() on QueryKey"); + } + }; + + std::unique_ptr impl; + + public: + InternalContractDataCacheEntry(LedgerEntry const& ledgerEntry, + uint32_t liveUntilLedgerSeq) + : impl(std::make_unique(ledgerEntry, liveUntilLedgerSeq)) + { + } + + InternalContractDataCacheEntry(LedgerKey const& ledgerKey) + : impl(std::make_unique(ledgerKey)) + { + } + + size_t + hash() const + { + return impl->hash(); + } + + bool + operator==(InternalContractDataCacheEntry const& other) const + { + return impl->operator==(*other.impl); + } + + ContractDataCacheT const& + get() const + { + return impl->get(); + } +}; + +struct InternalContractDataEntryHash +{ + size_t + operator()(InternalContractDataCacheEntry const& entry) const + { + return entry.hash(); + } +}; + +// This class caches all Soroban state required for Soroban tx application +// (except for the module cache, which is managed separately). Specifically, +// it caches these LedgerEntry types in the following way: +// +// ContractData: +// ContractCode: +// +// We don't need to store explicit TTL entries, nor do we need to store WASM, we +// just need to keep track of TTLs. +class LedgerStateCache : public NonMovableOrCopyable +{ +#ifdef BUILD_TESTS + public: +#endif + + std::unordered_set + mEntries; + std::unordered_map mContractCodeTTLs; + + public: + void addContractDataEntry(LedgerEntry const& ledgerEntry, + uint32_t liveUntilLedgerSeq); + void addContractCodeTTL(LedgerKey const& ledgerKey, + uint32_t liveUntilLedgerSeq); + + void evictKey(LedgerKey const& ledgerKey); + + // Returns nullopt if the entry is not in the cache + std::optional + getContractDataEntry(LedgerKey const& ledgerKey) const; + + // Returns nullopt if the entry is not in the cache + std::optional + getContractCodeTTL(LedgerKey const& ledgerKey) const; +}; +} From 141fe4f6dcaa0a2f1bf126b54bb1ae7499be0b35 Mon Sep 17 00:00:00 2001 From: Garand Tyson Date: Mon, 10 Mar 2025 19:13:34 -0700 Subject: [PATCH 2/2] Read contract data entries from cache --- src/bucket/test/BucketIndexTests.cpp | 49 ++++++++++++++++++++----- src/bucket/test/BucketTestUtils.cpp | 32 ++++++++++++++++ src/ledger/LedgerManager.h | 3 +- src/ledger/LedgerManagerImpl.cpp | 43 ++++++++++++++++++---- src/ledger/LedgerManagerImpl.h | 5 +-- src/ledger/LedgerStateCache.cpp | 55 ++++++++++++++++++++++++---- src/ledger/LedgerStateCache.h | 41 ++++++++++++++++++--- src/ledger/LedgerTxn.cpp | 18 +++++++++ 8 files changed, 213 insertions(+), 33 deletions(-) diff --git a/src/bucket/test/BucketIndexTests.cpp b/src/bucket/test/BucketIndexTests.cpp index 07b9e7ce05..128d01c641 100644 --- a/src/bucket/test/BucketIndexTests.cpp +++ b/src/bucket/test/BucketIndexTests.cpp @@ -282,12 +282,16 @@ class BucketIndexTest // key in entries may also be in toUpdate if (sorobanOnly) { - toUpdate.insert(toUpdate.end(), entries.begin(), - entries.end()); + mApp->getLedgerManager() + .setNextLedgerEntryBatchForBucketTesting( + entries, toUpdate, toDestroy); + } + else + { + mApp->getLedgerManager() + .setNextLedgerEntryBatchForBucketTesting({}, toUpdate, + toDestroy); } - mApp->getLedgerManager() - .setNextLedgerEntryBatchForBucketTesting({}, toUpdate, - toDestroy); toDestroy.clear(); toUpdate.clear(); } @@ -828,7 +832,7 @@ TEST_CASE("bl cache", "[bucket][bucketindex]") // Note: this test checks that the soroban cache is initialized correctly, but // does not check that the cache is maintained as ledger state changes. -TEST_CASE("soroban cache initialization", "[soroban]") +TEST_CASE("soroban cache", "[soroban]") { auto f = [&](Config& cfg) { auto test = BucketIndexTest(cfg); @@ -836,9 +840,7 @@ TEST_CASE("soroban cache initialization", "[soroban]") test.run(); auto& lm = test.getApp().getLedgerManager(); - lm.clearLedgerStateCacheForTesting(); - lm.populateApplyStateCacheFromBucketList(); - auto& cache = lm.getLedgerStateCacheForTesting(); + auto& cache = lm.getLedgerStateCache(); auto codeEntries = test.getContractCodeEntries(); auto dataEntries = test.getContractDataEntries(); @@ -846,6 +848,35 @@ TEST_CASE("soroban cache initialization", "[soroban]") .getBucketSnapshotManager() .copySearchableLiveBucketListSnapshot(); + // First, test that the cache is maintained correctly via `addBatch` + REQUIRE(codeEntries.size() == cache.mContractCodeTTLs.size()); + for (auto const& [k, v] : codeEntries) + { + auto ttl = cache.getContractCodeTTL(k); + REQUIRE(ttl); + + // TODO: maintain TTL values correctly + REQUIRE(*ttl == 0); + } + + REQUIRE(dataEntries.size() == cache.mEntries.size()); + for (auto const& [k, v] : dataEntries) + { + auto cacheEntry = cache.getContractDataEntry(k); + REQUIRE(cacheEntry); + + // TODO: maintain TTL values correctly + REQUIRE(cacheEntry->liveUntilLedgerSeq == 0); + + auto liveEntry = snapshot->load(k); + REQUIRE(liveEntry); + REQUIRE(*liveEntry == *cacheEntry->ledgerEntry); + } + + // Now wipe cache and repopulate from scratch to test initialization + lm.clearLedgerStateCacheForTesting(); + lm.populateApplyStateCacheFromBucketList(); + REQUIRE(codeEntries.size() == cache.mContractCodeTTLs.size()); for (auto const& [k, v] : codeEntries) { diff --git a/src/bucket/test/BucketTestUtils.cpp b/src/bucket/test/BucketTestUtils.cpp index 057065655f..3f808212ab 100644 --- a/src/bucket/test/BucketTestUtils.cpp +++ b/src/bucket/test/BucketTestUtils.cpp @@ -10,6 +10,7 @@ #include "crypto/Hex.h" #include "herder/Herder.h" #include "ledger/LedgerTxn.h" +#include "ledger/LedgerTypeUtils.h" #include "main/Application.h" #include "test/test.h" #include "util/ProtocolVersion.h" @@ -290,6 +291,37 @@ LedgerManagerForBucketTests::sealLedgerTxnAndTransferEntriesToBucketList( mApp.getBucketManager().addLiveBatch( mApp, lh, mTestInitEntries, mTestLiveEntries, mTestDeadEntries); mUseTestEntries = false; + if (protocolVersionStartsFrom(initialLedgerVers, + SOROBAN_PROTOCOL_VERSION)) + { + auto insertLiveEntries = [&](auto const& entries) { + for (auto const& entry : entries) + { + // TODO: Properly maintain TTL + if (entry.data.type() == CONTRACT_CODE) + { + mApplyState.mLedgerStateCache->updateContractCodeTTL( + LedgerEntryKey(entry), 0); + } + else if (entry.data.type() == CONTRACT_DATA) + { + mApplyState.mLedgerStateCache->updateContractDataEntry( + entry, std::nullopt); + } + } + }; + + insertLiveEntries(mTestInitEntries); + insertLiveEntries(mTestLiveEntries); + + for (auto const& key : mTestDeadEntries) + { + if (isSorobanEntry(key)) + { + mApplyState.mLedgerStateCache->evictKey(key); + } + } + } } else { diff --git a/src/ledger/LedgerManager.h b/src/ledger/LedgerManager.h index e349d6b6f9..e8be54a20b 100644 --- a/src/ledger/LedgerManager.h +++ b/src/ledger/LedgerManager.h @@ -304,10 +304,11 @@ class LedgerManager applyLedger(ledgerData, /* externalize */ false); } - virtual LedgerStateCache const& getLedgerStateCacheForTesting() const = 0; virtual void clearLedgerStateCacheForTesting() = 0; #endif + virtual LedgerStateCache const& getLedgerStateCache() const = 0; + virtual void setLastClosedLedger(LedgerHeaderHistoryEntry const& lastClosed) = 0; diff --git a/src/ledger/LedgerManagerImpl.cpp b/src/ledger/LedgerManagerImpl.cpp index 44c7e40ec7..63e988bc2c 100644 --- a/src/ledger/LedgerManagerImpl.cpp +++ b/src/ledger/LedgerManagerImpl.cpp @@ -549,15 +549,15 @@ LedgerManagerImpl::storeCurrentLedgerForTest(LedgerHeader const& header) storePersistentStateAndLedgerHeaderInDB(header, true); } +#endif + LedgerStateCache const& -LedgerManagerImpl::getLedgerStateCacheForTesting() const +LedgerManagerImpl::getLedgerStateCache() const { releaseAssertOrThrow(mApplyState.mLedgerStateCache); return *mApplyState.mLedgerStateCache; } -#endif - SorobanMetrics& LedgerManagerImpl::getSorobanMetrics() { @@ -1184,13 +1184,13 @@ LedgerManagerImpl::populateApplyStateCacheFromBucketList() releaseAssertOrThrow(ttlEntry); if (lk.type() == CONTRACT_CODE) { - cache.addContractCodeTTL(lk, - ttlEntry->data.ttl().liveUntilLedgerSeq); + cache.updateContractCodeTTL( + lk, ttlEntry->data.ttl().liveUntilLedgerSeq); } else { - cache.addContractDataEntry(be.liveEntry(), - ttlEntry->data.ttl().liveUntilLedgerSeq); + cache.updateContractDataEntry( + be.liveEntry(), ttlEntry->data.ttl().liveUntilLedgerSeq); } return Loop::INCOMPLETE; @@ -1855,6 +1855,35 @@ LedgerManagerImpl::sealLedgerTxnAndTransferEntriesToBucketList( { mApp.getBucketManager().addLiveBatch(mApp, lh, initEntries, liveEntries, deadEntries); + + if (protocolVersionStartsFrom(initialLedgerVers, + SOROBAN_PROTOCOL_VERSION)) + { + auto insertLiveEntries = [&](auto const& entries) { + for (auto const& entry : entries) + { + // TODO: Properly maintain TTL + if (entry.data.type() == CONTRACT_CODE) + { + mApplyState.mLedgerStateCache->updateContractCodeTTL( + LedgerEntryKey(entry), 0); + } + else if (entry.data.type() == CONTRACT_DATA) + { + mApplyState.mLedgerStateCache->updateContractDataEntry( + entry, std::nullopt); + } + } + }; + + insertLiveEntries(initEntries); + insertLiveEntries(liveEntries); + + for (auto const& key : deadEntries) + { + mApplyState.mLedgerStateCache->evictKey(key); + } + } } } diff --git a/src/ledger/LedgerManagerImpl.h b/src/ledger/LedgerManagerImpl.h index 01704a89e6..eb11cd2d00 100644 --- a/src/ledger/LedgerManagerImpl.h +++ b/src/ledger/LedgerManagerImpl.h @@ -49,7 +49,6 @@ class LedgerManagerImpl : public LedgerManager std::weak_ptr mFlushAndRotateMetaDebugWork; std::filesystem::path mMetaDebugPath; - private: // Output of the apply process, also what gets held as "LCL". struct LedgerState { @@ -164,7 +163,6 @@ class LedgerManagerImpl : public LedgerManager // Update cached last closed ledger state values managed by this class. void advanceLastClosedLedgerState(LedgerState const& output); - protected: // initialLedgerVers must be the ledger version at the start of the ledger // and currLedgerVers is the ledger version in the current ltx header. These // values are the same except on the ledger in which a protocol upgrade from @@ -222,7 +220,6 @@ class LedgerManagerImpl : public LedgerManager TransactionResultSet mLatestTxResultSet{}; void storeCurrentLedgerForTest(LedgerHeader const& header) override; - LedgerStateCache const& getLedgerStateCacheForTesting() const override; void clearLedgerStateCacheForTesting() override { @@ -230,6 +227,8 @@ class LedgerManagerImpl : public LedgerManager } #endif + LedgerStateCache const& getLedgerStateCache() const override; + uint64_t secondsSinceLastLedgerClose() const override; void syncMetrics() override; diff --git a/src/ledger/LedgerStateCache.cpp b/src/ledger/LedgerStateCache.cpp index 790e2b5d02..1fda71ef6f 100644 --- a/src/ledger/LedgerStateCache.cpp +++ b/src/ledger/LedgerStateCache.cpp @@ -9,18 +9,59 @@ namespace stellar { void -LedgerStateCache::addContractDataEntry(LedgerEntry const& ledgerEntry, - uint32_t liveUntilLedgerSeq) +LedgerStateCache::updateContractDataTTL(LedgerKey const& ledgerKey, + uint32_t liveUntilLedgerSeq) { - mEntries.emplace( - InternalContractDataCacheEntry(ledgerEntry, liveUntilLedgerSeq)); + releaseAssertOrThrow(ledgerKey.type() == LedgerEntryType::CONTRACT_DATA); + auto it = mEntries.find(InternalContractDataCacheEntry(ledgerKey)); + releaseAssertOrThrow(it != mEntries.end()); + it->get().updateTTL(liveUntilLedgerSeq); } void -LedgerStateCache::addContractCodeTTL(LedgerKey const& ledgerKey, - uint32_t liveUntilLedgerSeq) +LedgerStateCache::updateContractDataEntry( + LedgerEntry const& ledgerEntry, + std::optional liveUntilLedgerSeqOp) { - mContractCodeTTLs.emplace(ledgerKey, liveUntilLedgerSeq); + releaseAssertOrThrow(ledgerEntry.data.type() == CONTRACT_DATA); + auto it = mEntries.find( + InternalContractDataCacheEntry(LedgerEntryKey(ledgerEntry))); + if (it == mEntries.end()) + { + mEntries.emplace(InternalContractDataCacheEntry( + ledgerEntry, liveUntilLedgerSeqOp ? *liveUntilLedgerSeqOp : 0)); + } + else + { + if (liveUntilLedgerSeqOp) + { + mEntries.erase(it); + mEntries.emplace(InternalContractDataCacheEntry( + ledgerEntry, *liveUntilLedgerSeqOp)); + } + else + { + auto liveUntilLedgerSeq = it->get().liveUntilLedgerSeq; + mEntries.erase(it); + mEntries.emplace(InternalContractDataCacheEntry( + ledgerEntry, liveUntilLedgerSeq)); + } + } +} + +void +LedgerStateCache::updateContractCodeTTL(LedgerKey const& ledgerKey, + uint32_t liveUntilLedgerSeq) +{ + auto it = mContractCodeTTLs.find(ledgerKey); + if (it == mContractCodeTTLs.end()) + { + mContractCodeTTLs.emplace(ledgerKey, liveUntilLedgerSeq); + } + else + { + it->second = liveUntilLedgerSeq; + } } void diff --git a/src/ledger/LedgerStateCache.h b/src/ledger/LedgerStateCache.h index d2736294c7..f792b0c043 100644 --- a/src/ledger/LedgerStateCache.h +++ b/src/ledger/LedgerStateCache.h @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -16,10 +17,12 @@ namespace stellar { +class LedgerStateCache; + struct ContractDataCacheT { std::shared_ptr ledgerEntry; - uint32_t liveUntilLedgerSeq; + mutable uint32_t liveUntilLedgerSeq; explicit ContractDataCacheT(LedgerEntry const& ledgerEntry, uint32_t liveUntilLedgerSeq) @@ -27,6 +30,19 @@ struct ContractDataCacheT , liveUntilLedgerSeq(liveUntilLedgerSeq) { } + + private: + // This is a little hacky, but we want to be able to just update the TTL + // without having to re-insert the ledgerEntry as well. This entry will be + // stored in a set and always be returned const, but we know the ttl value + // is not used by the hashing or comparison operations, so this is safe. + void + updateTTL(uint32_t newLiveUntilLedgerSeq) const + { + liveUntilLedgerSeq = newLiveUntilLedgerSeq; + } + + friend class LedgerStateCache; }; // Soroban keys sizes usually dominate LedgerEntry size, so we don't want to @@ -180,11 +196,24 @@ class LedgerStateCache : public NonMovableOrCopyable std::unordered_map mContractCodeTTLs; public: - void addContractDataEntry(LedgerEntry const& ledgerEntry, - uint32_t liveUntilLedgerSeq); - void addContractCodeTTL(LedgerKey const& ledgerKey, - uint32_t liveUntilLedgerSeq); - + // Update the TTL of an existing entry in the cache, or inserts a new entry + // if no key exists. + void updateContractCodeTTL(LedgerKey const& ledgerKey, + uint32_t liveUntilLedgerSeq); + + // Update ContractData entry, or create a new entry if it doesn't exist. + // If liveUntilLedgerSeq is not nullopt, update the TTL of the entry. + // Otherwise, the TTL will remained unchanged if the entry already exists, + // or will default to 0 if the entry is new. + void updateContractDataEntry(LedgerEntry const& ledgerEntry, + std::optional liveUntilLedgerSeq); + + // Update the TTL of an existing ContractData entry. Throws if the entry + // does not exist. + void updateContractDataTTL(LedgerKey const& ledgerKey, + uint32_t liveUntilLedgerSeq); + + // Evict a key from the cache. void evictKey(LedgerKey const& ledgerKey); // Returns nullopt if the entry is not in the cache diff --git a/src/ledger/LedgerTxn.cpp b/src/ledger/LedgerTxn.cpp index 19228103df..9cac59fc02 100644 --- a/src/ledger/LedgerTxn.cpp +++ b/src/ledger/LedgerTxn.cpp @@ -7,7 +7,9 @@ #include "bucket/SearchableBucketList.h" #include "crypto/KeyUtils.h" #include "database/Database.h" +#include "ledger/LedgerManager.h" #include "ledger/LedgerRange.h" +#include "ledger/LedgerStateCache.h" #include "ledger/LedgerTxnEntry.h" #include "ledger/LedgerTxnHeader.h" #include "ledger/LedgerTxnImpl.h" @@ -3454,6 +3456,21 @@ LedgerTxnRoot::Impl::getNewestVersion(InternalLedgerKey const& gkey) const return nullptr; } auto const& key = gkey.ledgerKey(); + // TODO: Support reading TTL from cache + if (key.type() == CONTRACT_DATA) + { + auto entry = + mApp.getLedgerManager().getLedgerStateCache().getContractDataEntry( + key); + if (entry) + { + // TODO: Remove entry copy here + return std::make_shared( + *entry->ledgerEntry); + } + + return nullptr; + } if (mEntryCache.exists(key)) { @@ -3499,6 +3516,7 @@ LedgerTxnRoot::Impl::getNewestVersion(InternalLedgerKey const& gkey) const putInEntryCache(key, entry, LoadType::IMMEDIATE); if (entry) { + // TODO: Remove entry copy here return std::make_shared(*entry); } else