diff --git a/src/bucket/BucketManager.cpp b/src/bucket/BucketManager.cpp index 9aecee8713..358745e50c 100644 --- a/src/bucket/BucketManager.cpp +++ b/src/bucket/BucketManager.cpp @@ -9,8 +9,9 @@ #include "bucket/BucketSnapshotManager.h" #include "bucket/BucketUtils.h" #include "bucket/HotArchiveBucket.h" +#include "bucket/HotArchiveBucketList.h" #include "bucket/LiveBucket.h" -#include "bucket/SearchableBucketList.h" +#include "bucket/LiveBucketList.h" #include "crypto/BLAKE2.h" #include "crypto/Hex.h" #include "history/HistoryManager.h" diff --git a/src/bucket/BucketManager.h b/src/bucket/BucketManager.h index 32043b8870..38df819a81 100644 --- a/src/bucket/BucketManager.h +++ b/src/bucket/BucketManager.h @@ -1,12 +1,11 @@ #pragma once #include "bucket/BucketMergeMap.h" -#include "bucket/HotArchiveBucketList.h" -#include "bucket/LiveBucketList.h" #include "main/Config.h" #include "util/types.h" #include "xdr/Stellar-ledger.h" +#include #include #include #include @@ -32,6 +31,9 @@ class AbstractLedgerTxn; class Application; class Bucket; class LiveBucketList; +class HotArchiveBucketList; +class BucketBase; +class BucketIndex; class BucketSnapshotManager; class SearchableLiveBucketListSnapshot; struct BucketEntryCounters; @@ -395,7 +397,6 @@ class BucketManager : NonMovableOrCopyable scheduleVerifyReferencedBucketsWork(HistoryArchiveState const& has); Config const& getConfig() const; - void reportBucketEntryCountMetrics(); }; diff --git a/src/bucket/FutureBucket.cpp b/src/bucket/FutureBucket.cpp index acbd13a758..151b46c297 100644 --- a/src/bucket/FutureBucket.cpp +++ b/src/bucket/FutureBucket.cpp @@ -7,6 +7,7 @@ // else. #include "util/asio.h" // IWYU pragma: keep +#include "bucket/BucketListBase.h" #include "bucket/BucketManager.h" #include "bucket/FutureBucket.h" #include "bucket/HotArchiveBucket.h" diff --git a/src/bucket/SearchableBucketList.cpp b/src/bucket/SearchableBucketList.cpp index d225a7c732..47fac8e742 100644 --- a/src/bucket/SearchableBucketList.cpp +++ b/src/bucket/SearchableBucketList.cpp @@ -5,6 +5,7 @@ #include "bucket/SearchableBucketList.h" #include "bucket/BucketInputIterator.h" #include "bucket/BucketListSnapshotBase.h" +#include "bucket/LiveBucketList.h" #include "util/GlobalChecks.h" #include diff --git a/src/bucket/test/BucketTestUtils.cpp b/src/bucket/test/BucketTestUtils.cpp index ea6d0f351f..c5b79d9f04 100644 --- a/src/bucket/test/BucketTestUtils.cpp +++ b/src/bucket/test/BucketTestUtils.cpp @@ -235,15 +235,27 @@ LedgerManagerForBucketTests::transferLedgerEntriesToBucketList( ltxEvictions, lh.ledgerSeq, keys, initialLedgerVers, mApp.getLedgerManager() .getSorobanNetworkConfigForApply()); - if (protocolVersionStartsFrom( initialLedgerVers, BucketBase:: FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)) { + std::vector restoredKeys; + auto restoredKeysMap = ltx.getRestoredHotArchiveKeys(); + for (auto const& key : restoredKeysMap) + { + // Hot Archive does not track TTLs + if (key.type() == CONTRACT_DATA || + key.type() == CONTRACT_CODE) + { + restoredKeys.emplace_back(key); + } + } mApp.getBucketManager().addHotArchiveBatch( - mApp, lh, evictedState.archivedEntries, {}, {}); + mApp, lh, evictedState.archivedEntries, restoredKeys, + {}); } + if (ledgerCloseMeta) { ledgerCloseMeta->populateEvictedEntries(evictedState); diff --git a/src/catchup/IndexBucketsWork.cpp b/src/catchup/IndexBucketsWork.cpp index c7887b92f4..49b7a29fe4 100644 --- a/src/catchup/IndexBucketsWork.cpp +++ b/src/catchup/IndexBucketsWork.cpp @@ -5,6 +5,7 @@ #include "IndexBucketsWork.h" #include "bucket/BucketIndex.h" #include "bucket/BucketManager.h" +#include "bucket/LiveBucket.h" #include "util/Fs.h" #include "util/Logging.h" #include "util/UnorderedSet.h" diff --git a/src/ledger/LedgerManagerImpl.cpp b/src/ledger/LedgerManagerImpl.cpp index 2bea36c459..affd40e066 100644 --- a/src/ledger/LedgerManagerImpl.cpp +++ b/src/ledger/LedgerManagerImpl.cpp @@ -4,6 +4,7 @@ #include "ledger/LedgerManagerImpl.h" #include "bucket/BucketManager.h" +#include "bucket/HotArchiveBucketList.h" #include "bucket/LiveBucketList.h" #include "catchup/AssumeStateWork.h" #include "crypto/Hex.h" @@ -44,6 +45,7 @@ #include +#include "xdr/Stellar-ledger-entries.h" #include "xdr/Stellar-ledger.h" #include "xdr/Stellar-transaction.h" #include "xdrpp/types.h" @@ -1790,8 +1792,19 @@ LedgerManagerImpl::transferLedgerEntriesToBucketList( initialLedgerVers, BucketBase::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)) { + std::vector restoredKeys; + auto const& restoredKeyMap = ltx.getRestoredHotArchiveKeys(); + for (auto const& key : restoredKeyMap) + { + // TTL keys are not recorded in the hot archive BucketList + if (key.type() == CONTRACT_DATA || + key.type() == CONTRACT_CODE) + { + restoredKeys.push_back(key); + } + } mApp.getBucketManager().addHotArchiveBatch( - mApp, lh, evictedState.archivedEntries, {}, {}); + mApp, lh, evictedState.archivedEntries, restoredKeys, {}); } if (ledgerCloseMeta) diff --git a/src/ledger/LedgerTxn.cpp b/src/ledger/LedgerTxn.cpp index c2a98ab52c..7eee5b4627 100644 --- a/src/ledger/LedgerTxn.cpp +++ b/src/ledger/LedgerTxn.cpp @@ -16,12 +16,14 @@ #include "main/Application.h" #include "transactions/TransactionUtils.h" #include "util/GlobalChecks.h" +#include "util/UnorderedSet.h" #include "util/types.h" #include "xdr/Stellar-ledger-entries.h" #include #include #include +#include namespace stellar { @@ -501,14 +503,16 @@ LedgerTxn::Impl::commit() noexcept maybeUpdateLastModifiedThenInvokeThenSeal([&](EntryMap const& entries) { // getEntryIterator has the strong exception safety guarantee // commitChild has the strong exception safety guarantee - mParent.commitChild(getEntryIterator(entries), mConsistency); + mParent.commitChild(getEntryIterator(entries), mRestoredKeys, + mConsistency); }); } void -LedgerTxn::commitChild(EntryIterator iter, LedgerTxnConsistency cons) noexcept +LedgerTxn::commitChild(EntryIterator iter, RestoredKeys const& restoredKeys, + LedgerTxnConsistency cons) noexcept { - getImpl()->commitChild(std::move(iter), cons); + getImpl()->commitChild(std::move(iter), restoredKeys, cons); } static LedgerTxnConsistency @@ -527,6 +531,7 @@ joinConsistencyLevels(LedgerTxnConsistency c1, LedgerTxnConsistency c2) void LedgerTxn::Impl::commitChild(EntryIterator iter, + RestoredKeys const& restoredKeys, LedgerTxnConsistency cons) noexcept { // Assignment of xdrpp objects does not have the strong exception safety @@ -632,6 +637,24 @@ LedgerTxn::Impl::commitChild(EntryIterator iter, printErrorAndAbort("unknown fatal error during commit to LedgerTxn"); } + for (auto const& key : restoredKeys.hotArchive) + { + auto [_, inserted] = mRestoredKeys.hotArchive.emplace(key); + if (!inserted) + { + printErrorAndAbort("restored hot archive entry already exists"); + } + } + + for (auto const& key : restoredKeys.liveBucketList) + { + auto [_, inserted] = mRestoredKeys.liveBucketList.emplace(key); + if (!inserted) + { + printErrorAndAbort("restored live BucketList entry already exists"); + } + } + // std::unique_ptr<...>::swap does not throw mHeader.swap(childHeader); mChild = nullptr; @@ -802,6 +825,91 @@ LedgerTxn::Impl::erase(InternalLedgerKey const& key) } } +void +LedgerTxn::restoreFromHotArchive(LedgerEntry const& entry, uint32_t ttl) +{ + getImpl()->restoreFromHotArchive(*this, entry, ttl); +} + +void +LedgerTxn::Impl::restoreFromHotArchive(LedgerTxn& self, + LedgerEntry const& entry, uint32_t ttl) +{ + throwIfSealed(); + throwIfChild(); + + if (!isPersistentEntry(entry.data)) + { + throw std::runtime_error("Key type not supported in Hot Archive"); + } + auto ttlKey = getTTLKey(entry); + + // Restore entry by creating it on the live BucketList + create(self, entry); + + // Also create the corresponding TTL entry + LedgerEntry ttlEntry; + ttlEntry.data.type(TTL); + ttlEntry.data.ttl().liveUntilLedgerSeq = ttl; + ttlEntry.data.ttl().keyHash = ttlKey.ttl().keyHash; + create(self, ttlEntry); + + // Mark the keys as restored + auto addKey = [this](LedgerKey const& key) { + auto [_, inserted] = mRestoredKeys.hotArchive.insert(key); + if (!inserted) + { + throw std::runtime_error("Key already removed from hot archive"); + } + }; + addKey(LedgerEntryKey(entry)); + addKey(ttlKey); +} + +void +LedgerTxn::restoreFromLiveBucketList(LedgerKey const& key, uint32_t ttl) +{ + getImpl()->restoreFromLiveBucketList(*this, key, ttl); +} + +void +LedgerTxn::Impl::restoreFromLiveBucketList(LedgerTxn& self, + LedgerKey const& key, uint32_t ttl) +{ + throwIfSealed(); + throwIfChild(); + + if (!isPersistentEntry(key)) + { + throw std::runtime_error("Key type not supported for restoration"); + } + + auto ttlKey = getTTLKey(key); + + // Note: key should have already been loaded via loadWithoutRecord by + // caller, so this read should already be in the cache. + auto ttlLtxe = load(self, ttlKey); + if (!ttlLtxe) + { + throw std::runtime_error("Entry restored from live BucketList but does " + "not exist in the live BucketList."); + } + + ttlLtxe.current().data.ttl().liveUntilLedgerSeq = ttl; + + // Mark the keys as restored + auto addKey = [this](LedgerKey const& key) { + auto [_, inserted] = mRestoredKeys.liveBucketList.insert(key); + if (!inserted) + { + throw std::runtime_error( + "Key already restored from Live BucketList"); + } + }; + addKey(key); + addKey(ttlKey); +} + void LedgerTxn::eraseWithoutLoading(InternalLedgerKey const& key) { @@ -1470,6 +1578,30 @@ LedgerTxn::Impl::getAllEntries(std::vector& initEntries, deadEntries.swap(resDead); } +UnorderedSet const& +LedgerTxn::getRestoredHotArchiveKeys() const +{ + return getImpl()->getRestoredHotArchiveKeys(); +} + +UnorderedSet const& +LedgerTxn::Impl::getRestoredHotArchiveKeys() const +{ + return mRestoredKeys.hotArchive; +} + +UnorderedSet const& +LedgerTxn::getRestoredLiveBucketListKeys() const +{ + return getImpl()->getRestoredLiveBucketListKeys(); +} + +UnorderedSet const& +LedgerTxn::Impl::getRestoredLiveBucketListKeys() const +{ + return mRestoredKeys.liveBucketList; +} + LedgerKeySet LedgerTxn::getAllTTLKeysWithoutSealing() const { @@ -1957,6 +2089,8 @@ LedgerTxn::Impl::rollback() noexcept } mEntry.clear(); + mRestoredKeys.hotArchive.clear(); + mRestoredKeys.liveBucketList.clear(); mMultiOrderBook.clear(); mActive.clear(); mActiveHeader.reset(); @@ -2559,10 +2693,10 @@ LedgerTxnRoot::Impl::throwIfChild() const } void -LedgerTxnRoot::commitChild(EntryIterator iter, +LedgerTxnRoot::commitChild(EntryIterator iter, RestoredKeys const& restoredKeys, LedgerTxnConsistency cons) noexcept { - mImpl->commitChild(std::move(iter), cons); + mImpl->commitChild(std::move(iter), restoredKeys, cons); } static void @@ -2619,6 +2753,7 @@ LedgerTxnRoot::Impl::bulkApply(BulkLedgerEntryChangeAccumulator& bleca, void LedgerTxnRoot::Impl::commitChild(EntryIterator iter, + RestoredKeys const& restoredHotArchiveKeys, LedgerTxnConsistency cons) noexcept { ZoneScoped; diff --git a/src/ledger/LedgerTxn.h b/src/ledger/LedgerTxn.h index e1ca8f9b22..e13f3ac4d3 100644 --- a/src/ledger/LedgerTxn.h +++ b/src/ledger/LedgerTxn.h @@ -314,6 +314,14 @@ struct InflationWinner int64_t votes; }; +// Tracks the set of both TTL keys and corresponding code/data keys that have +// been restored. +struct RestoredKeys +{ + UnorderedSet hotArchive; + UnorderedSet liveBucketList; +}; + class AbstractLedgerTxn; // LedgerTxnDelta represents the difference between a LedgerTxn and its @@ -421,6 +429,7 @@ class AbstractLedgerTxnParent // to trigger an atomic commit or an atomic rollback of the data stored in // the child. virtual void commitChild(EntryIterator iter, + RestoredKeys const& restoredKeys, LedgerTxnConsistency cons) noexcept = 0; virtual void rollbackChild() noexcept = 0; @@ -539,7 +548,8 @@ class AbstractLedgerTxn : public AbstractLedgerTxnParent virtual void commit() noexcept = 0; virtual void rollback() noexcept = 0; - // loadHeader, create, erase, load, and loadWithoutRecord provide the main + // loadHeader, create, erase, load, loadWithoutRecord, + // restoreFromHotArchive, and restoreFromLiveBucketList provide the main // interface to interact with data stored in the AbstractLedgerTxn. These // functions only allow one instance of a particular data to be active at a // time. @@ -566,11 +576,25 @@ class AbstractLedgerTxn : public AbstractLedgerTxnParent // then it will still be recorded after calling loadWithoutRecord. // Throws if there is an active LedgerTxnEntry associated with this // key. + // - restoreFromHotArchive: + // Indicates that an entry in the Hot Archive has been restored. This + // will create both data data/contract entry and + // corresponding TTL entry. Prior to this call, the data/contract key + // and TTL key must not exist in the live BucketList or any parent ltx, + // throws otherwise. + // - restoreFromLiveBucketlist: + // Indicates that an entry in the live BucketList is being restored and + // updates the TTL entry accordingly. TTL key must exist, throws + // otherwise. // All of these functions throw if the AbstractLedgerTxn is sealed or if // the AbstractLedgerTxn has a child. virtual LedgerTxnHeader loadHeader() = 0; virtual LedgerTxnEntry create(InternalLedgerEntry const& entry) = 0; virtual void erase(InternalLedgerKey const& key) = 0; + virtual void restoreFromHotArchive(LedgerEntry const& entry, + uint32_t ttl) = 0; + virtual void restoreFromLiveBucketList(LedgerKey const& key, + uint32_t ttl) = 0; virtual LedgerTxnEntry load(InternalLedgerKey const& key) = 0; virtual ConstLedgerTxnEntry loadWithoutRecord(InternalLedgerKey const& key) = 0; @@ -613,6 +637,12 @@ class AbstractLedgerTxn : public AbstractLedgerTxnParent virtual void getAllEntries(std::vector& initEntries, std::vector& liveEntries, std::vector& deadEntries) = 0; + // Returns set of TTL and corresponding contract/data keys that have been + // restored from the Hot Archive/Live Bucket List. + virtual UnorderedSet const& + getRestoredHotArchiveKeys() const = 0; + virtual UnorderedSet const& + getRestoredLiveBucketListKeys() const = 0; // Returns all TTL keys that have been modified (create, update, and // delete), but does not cause the AbstractLedgerTxn or update last @@ -704,12 +734,14 @@ class LedgerTxn : public AbstractLedgerTxn void commit() noexcept override; - void commitChild(EntryIterator iter, + void commitChild(EntryIterator iter, RestoredKeys const& restoredKeys, LedgerTxnConsistency cons) noexcept override; LedgerTxnEntry create(InternalLedgerEntry const& entry) override; void erase(InternalLedgerKey const& key) override; + void restoreFromHotArchive(LedgerEntry const& entry, uint32_t ttl) override; + void restoreFromLiveBucketList(LedgerKey const& key, uint32_t ttl) override; UnorderedMap getAllOffers() override; @@ -746,6 +778,10 @@ class LedgerTxn : public AbstractLedgerTxn std::vector& deadEntries) override; LedgerKeySet getAllTTLKeysWithoutSealing() const override; + UnorderedSet const& getRestoredHotArchiveKeys() const override; + UnorderedSet const& + getRestoredLiveBucketListKeys() const override; + std::shared_ptr getNewestVersion(InternalLedgerKey const& key) const override; @@ -830,7 +866,7 @@ class LedgerTxnRoot : public AbstractLedgerTxnParent void addChild(AbstractLedgerTxn& child, TransactionMode mode) override; - void commitChild(EntryIterator iter, + void commitChild(EntryIterator iter, RestoredKeys const& restoredKeys, LedgerTxnConsistency cons) noexcept override; uint64_t countOffers(LedgerRange const& ledgers) const override; diff --git a/src/ledger/LedgerTxnImpl.h b/src/ledger/LedgerTxnImpl.h index 62afa831ef..9687e2604b 100644 --- a/src/ledger/LedgerTxnImpl.h +++ b/src/ledger/LedgerTxnImpl.h @@ -8,6 +8,7 @@ #include "database/Database.h" #include "ledger/LedgerTxn.h" #include "util/RandomEvictionCache.h" +#include "util/UnorderedSet.h" #include #include #ifdef USE_POSTGRES @@ -94,6 +95,8 @@ class LedgerTxn::Impl std::unique_ptr mHeader; std::shared_ptr mActiveHeader; EntryMap mEntry; + + RestoredKeys mRestoredKeys; UnorderedMap> mActive; bool const mShouldUpdateLastModified; bool mIsSealed; @@ -335,7 +338,8 @@ class LedgerTxn::Impl void commit() noexcept; - void commitChild(EntryIterator iter, LedgerTxnConsistency cons) noexcept; + void commitChild(EntryIterator iter, RestoredKeys const& restoredKeys, + LedgerTxnConsistency cons) noexcept; // create has the basic exception safety guarantee. If it throws an // exception, then @@ -357,6 +361,20 @@ class LedgerTxn::Impl // - the entry cache may be, but is not guaranteed to be, cleared. void erase(InternalLedgerKey const& key); + // restoreFromHotArchive has the basic exception safety guarantee. If it + // throws an exception, then + // - the prepared statement cache may be, but is not guaranteed to be, + // modified + void restoreFromHotArchive(LedgerTxn& self, LedgerEntry const& entry, + uint32_t ttl); + + // restoreFromLiveBucketList has the basic exception safety guarantee. If it + // throws an exception, then + // - the prepared statement cache may be, but is not guaranteed to be, + // modified + void restoreFromLiveBucketList(LedgerTxn& self, LedgerKey const& key, + uint32_t ttl); + // getAllOffers has the basic exception safety guarantee. If it throws an // exception, then // - the prepared statement cache may be, but is not guaranteed to be, @@ -430,6 +448,10 @@ class LedgerTxn::Impl void getAllEntries(std::vector& initEntries, std::vector& liveEntries, std::vector& deadEntries); + // getRestoredHotArchiveKeys and getRestoredLiveBucketListKeys + // have the strong exception safety guarantee + UnorderedSet const& getRestoredHotArchiveKeys() const; + UnorderedSet const& getRestoredLiveBucketListKeys() const; LedgerKeySet getAllTTLKeysWithoutSealing() const; @@ -706,7 +728,8 @@ class LedgerTxnRoot::Impl // addChild has the strong exception safety guarantee. void addChild(AbstractLedgerTxn& child, TransactionMode mode); - void commitChild(EntryIterator iter, LedgerTxnConsistency cons) noexcept; + void commitChild(EntryIterator iter, RestoredKeys const& restoredKeys, + LedgerTxnConsistency cons) noexcept; // countOffers has the strong exception safety guarantee. uint64_t countOffers(LedgerRange const& ledgers) const; diff --git a/src/ledger/test/InMemoryLedgerTxn.cpp b/src/ledger/test/InMemoryLedgerTxn.cpp index 1b2dd26dfe..e3b05f81b0 100644 --- a/src/ledger/test/InMemoryLedgerTxn.cpp +++ b/src/ledger/test/InMemoryLedgerTxn.cpp @@ -5,6 +5,7 @@ #include "ledger/test/InMemoryLedgerTxn.h" #include "ledger/LedgerTxn.h" #include "transactions/TransactionUtils.h" +#include "util/UnorderedSet.h" namespace stellar { @@ -187,6 +188,7 @@ InMemoryLedgerTxn::getFilteredEntryIterator(EntryIterator const& iter) void InMemoryLedgerTxn::commitChild(EntryIterator iter, + RestoredKeys const& restoredKeys, LedgerTxnConsistency cons) noexcept { if (!mTransaction) @@ -198,7 +200,7 @@ InMemoryLedgerTxn::commitChild(EntryIterator iter, auto filteredIter = getFilteredEntryIterator(iter); updateLedgerKeyMap(filteredIter); - LedgerTxn::commitChild(filteredIter, cons); + LedgerTxn::commitChild(filteredIter, restoredKeys, cons); mTransaction->commit(); mTransaction.reset(); } @@ -272,6 +274,20 @@ InMemoryLedgerTxn::erase(InternalLedgerKey const& key) throw std::runtime_error("called erase on InMemoryLedgerTxn"); } +void +InMemoryLedgerTxn::restoreFromHotArchive(LedgerEntry const& entry, uint32_t ttl) +{ + throw std::runtime_error( + "called restoreFromHotArchive on InMemoryLedgerTxn"); +} + +void +InMemoryLedgerTxn::restoreFromLiveBucketList(LedgerKey const& key, uint32_t ttl) +{ + throw std::runtime_error( + "called restoreFromLiveBucketList on InMemoryLedgerTxn"); +} + LedgerTxnEntry InMemoryLedgerTxn::load(InternalLedgerKey const& key) { diff --git a/src/ledger/test/InMemoryLedgerTxn.h b/src/ledger/test/InMemoryLedgerTxn.h index 9b49e8a890..32498eb6a7 100644 --- a/src/ledger/test/InMemoryLedgerTxn.h +++ b/src/ledger/test/InMemoryLedgerTxn.h @@ -102,7 +102,7 @@ class InMemoryLedgerTxn : public LedgerTxn virtual ~InMemoryLedgerTxn(); void addChild(AbstractLedgerTxn& child, TransactionMode mode) override; - void commitChild(EntryIterator iter, + void commitChild(EntryIterator iter, RestoredKeys const& restoredKeys, LedgerTxnConsistency cons) noexcept override; void rollbackChild() noexcept override; @@ -112,6 +112,8 @@ class InMemoryLedgerTxn : public LedgerTxn LedgerTxnEntry create(InternalLedgerEntry const& entry) override; void erase(InternalLedgerKey const& key) override; + void restoreFromHotArchive(LedgerEntry const& entry, uint32_t ttl) override; + void restoreFromLiveBucketList(LedgerKey const& key, uint32_t ttl) override; LedgerTxnEntry load(InternalLedgerKey const& key) override; ConstLedgerTxnEntry loadWithoutRecord(InternalLedgerKey const& key) override; diff --git a/src/ledger/test/InMemoryLedgerTxnRoot.cpp b/src/ledger/test/InMemoryLedgerTxnRoot.cpp index d2d2bd4ab1..98e15aac43 100644 --- a/src/ledger/test/InMemoryLedgerTxnRoot.cpp +++ b/src/ledger/test/InMemoryLedgerTxnRoot.cpp @@ -34,6 +34,7 @@ InMemoryLedgerTxnRoot::addChild(AbstractLedgerTxn& child, TransactionMode mode) void InMemoryLedgerTxnRoot::commitChild(EntryIterator iter, + RestoredKeys const& restoredKeys, LedgerTxnConsistency cons) noexcept { printErrorAndAbort("committing to stub InMemoryLedgerTxnRoot"); diff --git a/src/ledger/test/InMemoryLedgerTxnRoot.h b/src/ledger/test/InMemoryLedgerTxnRoot.h index 16606faa6b..ebe512fe08 100644 --- a/src/ledger/test/InMemoryLedgerTxnRoot.h +++ b/src/ledger/test/InMemoryLedgerTxnRoot.h @@ -38,7 +38,7 @@ class InMemoryLedgerTxnRoot : public AbstractLedgerTxnParent #endif ); void addChild(AbstractLedgerTxn& child, TransactionMode mode) override; - void commitChild(EntryIterator iter, + void commitChild(EntryIterator iter, RestoredKeys const& restoredKeys, LedgerTxnConsistency cons) noexcept override; void rollbackChild() noexcept override; diff --git a/src/ledger/test/LedgerTxnTests.cpp b/src/ledger/test/LedgerTxnTests.cpp index 11f0a2c9fd..1d602f1f37 100644 --- a/src/ledger/test/LedgerTxnTests.cpp +++ b/src/ledger/test/LedgerTxnTests.cpp @@ -238,6 +238,124 @@ TEST_CASE("LedgerTxn commit into LedgerTxn", "[ledgertxn]") validate(ltx1, {}); } } + + SECTION("restored keys") + { + auto randomEntries = + LedgerTestUtils::generateValidUniqueLedgerEntriesWithTypes( + {CONTRACT_CODE}, 2); + std::vector randomKeys = {LedgerEntryKey(randomEntries[0]), + LedgerEntryKey(randomEntries[1])}; + LedgerTxn ltx1(app->getLedgerTxnRoot()); + + SECTION("hot archive restore key exists in live BL") + { + ltx1.create(randomEntries[0]); + REQUIRE_THROWS(ltx1.restoreFromHotArchive(randomEntries[0], 42)); + } + + SECTION("live BL restore key does not exist") + { + REQUIRE_THROWS(ltx1.restoreFromLiveBucketList(randomKeys[0], 42)); + } + + auto checkKey = [](auto const& keySet, auto const& dataKey) { + REQUIRE(keySet.find(dataKey) != keySet.end()); + REQUIRE(keySet.find(getTTLKey(dataKey)) != keySet.end()); + }; + + SECTION("commited to parent") + { + SECTION("hot archive") + { + ltx1.restoreFromHotArchive(randomEntries[0], 42); + + SECTION("rollback") + { + { + LedgerTxn ltx2(ltx1); + ltx2.restoreFromHotArchive(randomEntries[1], 42); + } + + REQUIRE(ltx1.getRestoredLiveBucketListKeys().empty()); + auto keys = ltx1.getRestoredHotArchiveKeys(); + + // Data key + TTL + REQUIRE(keys.size() == 2); + checkKey(keys, randomKeys[0]); + } + + SECTION("commit") + { + { + LedgerTxn ltx2(ltx1); + ltx2.restoreFromHotArchive(randomEntries[1], 42); + ltx2.commit(); + } + + REQUIRE(ltx1.getRestoredLiveBucketListKeys().empty()); + auto keys = ltx1.getRestoredHotArchiveKeys(); + + // (data key + TTL) * 2 + REQUIRE(keys.size() == 4); + checkKey(keys, randomKeys[0]); + checkKey(keys, randomKeys[1]); + } + } + + SECTION("live BL") + { + auto getTTLEntry = [](LedgerKey const& key) { + LedgerEntry ttl; + ttl.data.type(TTL); + ttl.data.ttl().liveUntilLedgerSeq = 42; + ttl.data.ttl().keyHash = getTTLKey(key).ttl().keyHash; + return ttl; + }; + + // Populate live BL with key, then restore it + ltx1.create(randomEntries[0]); + ltx1.create(getTTLEntry(randomKeys[0])); + ltx1.restoreFromLiveBucketList(randomKeys[0], 42); + + SECTION("rollback") + { + { + LedgerTxn ltx2(ltx1); + ltx2.create(randomEntries[1]); + ltx2.create(getTTLEntry(randomKeys[1])); + ltx2.restoreFromLiveBucketList(randomKeys[1], 42); + } + + REQUIRE(ltx1.getRestoredHotArchiveKeys().empty()); + auto keys = ltx1.getRestoredLiveBucketListKeys(); + + // Data key + TTL + REQUIRE(keys.size() == 2); + checkKey(keys, randomKeys[0]); + } + + SECTION("commit") + { + { + LedgerTxn ltx2(ltx1); + ltx2.create(randomEntries[1]); + ltx2.create(getTTLEntry(randomKeys[1])); + ltx2.restoreFromLiveBucketList(randomKeys[1], 42); + ltx2.commit(); + } + + REQUIRE(ltx1.getRestoredHotArchiveKeys().empty()); + auto keys = ltx1.getRestoredLiveBucketListKeys(); + + // (data key + TTL) * 2 + REQUIRE(keys.size() == 4); + checkKey(keys, randomKeys[0]); + checkKey(keys, randomKeys[1]); + } + } + } + } } TEST_CASE("LedgerTxn rollback into LedgerTxn", "[ledgertxn]") diff --git a/src/main/AppConnector.cpp b/src/main/AppConnector.cpp index 49b24d31c7..a387154fa6 100644 --- a/src/main/AppConnector.cpp +++ b/src/main/AppConnector.cpp @@ -139,4 +139,12 @@ AppConnector::checkScheduledAndCache( { return mApp.getOverlayManager().checkScheduledAndCache(msgTracker); } + +SearchableHotArchiveSnapshotConstPtr +AppConnector::copySearchableHotArchiveBucketListSnapshot() +{ + return mApp.getBucketManager() + .getBucketSnapshotManager() + .copySearchableHotArchiveBucketListSnapshot(); +} } \ No newline at end of file diff --git a/src/main/AppConnector.h b/src/main/AppConnector.h index 9cdeb9be30..e01a940c10 100644 --- a/src/main/AppConnector.h +++ b/src/main/AppConnector.h @@ -1,5 +1,6 @@ #pragma once +#include "bucket/BucketSnapshotManager.h" #include "main/Config.h" #include "medida/metrics_registry.h" @@ -13,6 +14,7 @@ class BanManager; struct OverlayMetrics; class SorobanNetworkConfig; class SorobanMetrics; +class SearchableHotArchiveBucketListSnapshot; struct LedgerTxnDelta; class CapacityTrackedMessage; @@ -57,5 +59,7 @@ class AppConnector SorobanNetworkConfig const& getSorobanNetworkConfigForApply() const; medida::MetricsRegistry& getMetrics() const; + SearchableHotArchiveSnapshotConstPtr + copySearchableHotArchiveBucketListSnapshot(); }; } \ No newline at end of file diff --git a/src/main/ApplicationUtils.cpp b/src/main/ApplicationUtils.cpp index 8059294394..9d3b4565c0 100644 --- a/src/main/ApplicationUtils.cpp +++ b/src/main/ApplicationUtils.cpp @@ -4,6 +4,7 @@ #include "main/ApplicationUtils.h" #include "bucket/BucketManager.h" +#include "bucket/LiveBucketList.h" #include "catchup/ApplyBucketsWork.h" #include "catchup/CatchupConfiguration.h" #include "crypto/Hex.h" diff --git a/src/main/CommandLine.cpp b/src/main/CommandLine.cpp index a061c0c14e..a2c1c110cb 100644 --- a/src/main/CommandLine.cpp +++ b/src/main/CommandLine.cpp @@ -9,6 +9,7 @@ #include "main/CommandLine.h" #include "bucket/BucketManager.h" +#include "bucket/LiveBucketList.h" #include "catchup/CatchupConfiguration.h" #include "catchup/CatchupRange.h" #include "catchup/ReplayDebugMetaWork.h" diff --git a/src/main/Config.cpp b/src/main/Config.cpp index e5782c37c5..e06ea891ae 100644 --- a/src/main/Config.cpp +++ b/src/main/Config.cpp @@ -4,6 +4,7 @@ // of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 #include "main/Config.h" +#include "bucket/LiveBucketList.h" #include "crypto/KeyUtils.h" #include "herder/Herder.h" #include "history/HistoryArchive.h" diff --git a/src/testdata/ledger-close-meta-v1-protocol-23-soroban.json b/src/testdata/ledger-close-meta-v1-protocol-23-soroban.json index 016263ca8e..c3c55557a0 100644 --- a/src/testdata/ledger-close-meta-v1-protocol-23-soroban.json +++ b/src/testdata/ledger-close-meta-v1-protocol-23-soroban.json @@ -1898,6 +1898,33 @@ "operations": [ { "changes": [ + { + "type": "LEDGER_ENTRY_RESTORED", + "restored": { + "lastModifiedLedgerSeq": 7, + "data": { + "type": "CONTRACT_DATA", + "contractData": { + "ext": { + "v": 0 + }, + "contract": "CAA3QKIP2SNVXUJTB4HKOGF55JTSSMQGED3FZYNHMNSXYV3DRRMAWA3Y", + "key": { + "type": "SCV_SYMBOL", + "sym": "archived" + }, + "durability": "PERSISTENT", + "val": { + "type": "SCV_U64", + "u64": 42 + } + } + }, + "ext": { + "v": 0 + } + } + }, { "type": "LEDGER_ENTRY_STATE", "state": { @@ -1915,8 +1942,8 @@ } }, { - "type": "LEDGER_ENTRY_UPDATED", - "updated": { + "type": "LEDGER_ENTRY_RESTORED", + "restored": { "lastModifiedLedgerSeq": 28, "data": { "type": "TTL", diff --git a/src/transactions/InvokeHostFunctionOpFrame.cpp b/src/transactions/InvokeHostFunctionOpFrame.cpp index ae76c16264..7de739ea74 100644 --- a/src/transactions/InvokeHostFunctionOpFrame.cpp +++ b/src/transactions/InvokeHostFunctionOpFrame.cpp @@ -4,15 +4,16 @@ // clang-format off // This needs to be included first +#include "rust/RustVecXdrMarshal.h" #include "TransactionUtils.h" #include "util/GlobalChecks.h" +#include "util/ProtocolVersion.h" #include "xdr/Stellar-ledger-entries.h" #include #include #include #include #include "xdr/Stellar-contract.h" -#include "rust/RustVecXdrMarshal.h" // clang-format on #include "ledger/LedgerTxnImpl.h" @@ -344,13 +345,14 @@ InvokeHostFunctionOpFrame::doApply( auto const& footprint = resources.footprint; auto footprintLength = footprint.readOnly.size() + footprint.readWrite.size(); + auto hotArchive = app.copySearchableHotArchiveBucketListSnapshot(); ledgerEntryCxxBufs.reserve(footprintLength); ttlEntryCxxBufs.reserve(footprintLength); auto addReads = [&ledgerEntryCxxBufs, &ttlEntryCxxBufs, <x, &metrics, &resources, &sorobanConfig, &appConfig, sorobanData, &res, - this](auto const& keys) -> bool { + &hotArchive, this](auto const& keys) -> bool { for (auto const& lk : keys) { uint32_t keySize = static_cast(xdr::xdr_size(lk)); @@ -404,6 +406,40 @@ InvokeHostFunctionOpFrame::doApply( } } // If ttlLtxe doesn't exist, this is a new Soroban entry + // Starting in protocol 23, we must check the Hot Archive for + // new keys. If a new key is actually archived, fail the op. + if (isPersistentEntry(lk) && + protocolVersionStartsFrom( + ltx.getHeader().ledgerVersion, + HotArchiveBucket:: + FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)) + { + auto archiveEntry = hotArchive->load(lk); + if (archiveEntry) + { + if (lk.type() == CONTRACT_CODE) + { + sorobanData->pushApplyTimeDiagnosticError( + appConfig, SCE_VALUE, SCEC_INVALID_INPUT, + "trying to access an archived contract code " + "entry", + {makeBytesSCVal(lk.contractCode().hash)}); + } + else if (lk.type() == CONTRACT_DATA) + { + sorobanData->pushApplyTimeDiagnosticError( + appConfig, SCE_VALUE, SCEC_INVALID_INPUT, + "trying to access an archived contract data " + "entry", + {makeAddressSCVal(lk.contractData().contract), + lk.contractData().key}); + } + // Cannot access an archived entry + this->innerResult(res).code( + INVOKE_HOST_FUNCTION_ENTRY_ARCHIVED); + return false; + } + } } if (!isSorobanEntry(lk) || sorobanEntryLive) diff --git a/src/transactions/OperationFrame.cpp b/src/transactions/OperationFrame.cpp index 5108b15106..00d2d46041 100644 --- a/src/transactions/OperationFrame.cpp +++ b/src/transactions/OperationFrame.cpp @@ -33,6 +33,7 @@ #include "transactions/SetTrustLineFlagsOpFrame.h" #include "transactions/TransactionFrame.h" #include "transactions/TransactionUtils.h" +#include "util/GlobalChecks.h" #include "util/Logging.h" #include "util/ProtocolVersion.h" #include "util/XDRCereal.h" @@ -329,4 +330,11 @@ OperationFrame::insertLedgerKeysToPrefetch(UnorderedSet& keys) const // Do nothing by default return; } + +SorobanResources const& +OperationFrame::getSorobanResources() const +{ + releaseAssertOrThrow(isSoroban()); + return mParentTx.sorobanResources(); +} } diff --git a/src/transactions/OperationFrame.h b/src/transactions/OperationFrame.h index c260d8f11f..a18778d54c 100644 --- a/src/transactions/OperationFrame.h +++ b/src/transactions/OperationFrame.h @@ -96,5 +96,7 @@ class OperationFrame virtual bool isDexOperation() const; virtual bool isSoroban() const; + + SorobanResources const& getSorobanResources() const; }; } diff --git a/src/transactions/RestoreFootprintOpFrame.cpp b/src/transactions/RestoreFootprintOpFrame.cpp index dc849b1c6f..e17fa9cec3 100644 --- a/src/transactions/RestoreFootprintOpFrame.cpp +++ b/src/transactions/RestoreFootprintOpFrame.cpp @@ -4,11 +4,13 @@ #include "transactions/RestoreFootprintOpFrame.h" #include "TransactionUtils.h" +#include "bucket/HotArchiveBucket.h" #include "ledger/LedgerManagerImpl.h" #include "ledger/LedgerTypeUtils.h" #include "medida/meter.h" #include "medida/timer.h" #include "transactions/MutableTransactionResult.h" +#include "util/ProtocolVersion.h" #include namespace stellar @@ -65,6 +67,7 @@ RestoreFootprintOpFrame::doApply( auto ledgerSeq = ltx.loadHeader().current().ledgerSeq; auto const& sorobanConfig = app.getSorobanNetworkConfigForApply(); auto const& appConfig = app.getConfig(); + auto hotArchive = app.copySearchableHotArchiveBucketListSnapshot(); auto const& archivalSettings = sorobanConfig.stateArchivalSettings(); rust::Vec rustEntryRentChanges; @@ -75,11 +78,34 @@ RestoreFootprintOpFrame::doApply( rustEntryRentChanges.reserve(footprint.readWrite.size()); for (auto const& lk : footprint.readWrite) { + std::shared_ptr hotArchiveEntry{nullptr}; auto ttlKey = getTTLKey(lk); { + // First check the live BucketList auto constTTLLtxe = ltx.loadWithoutRecord(ttlKey); - // Skip entry if the TTLEntry is missing or if it's already live. - if (!constTTLLtxe || isLive(constTTLLtxe.current(), ledgerSeq)) + if (!constTTLLtxe) + { + // Next check the hot archive if protocol >= 23 + if (protocolVersionStartsFrom( + ltx.getHeader().ledgerVersion, + HotArchiveBucket:: + FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)) + { + hotArchiveEntry = hotArchive->load(lk); + if (!hotArchiveEntry) + { + // Entry doesn't exist, skip + continue; + } + } + else + { + // Entry doesn't exist, skip + continue; + } + } + // Skip entry if it's already live. + else if (isLive(constTTLLtxe.current(), ledgerSeq)) { continue; } @@ -87,13 +113,23 @@ RestoreFootprintOpFrame::doApply( // We must load the ContractCode/ContractData entry for fee purposes, as // restore is considered a write - auto constEntryLtxe = ltx.loadWithoutRecord(lk); + uint32_t entrySize = 0; + if (hotArchiveEntry) + { + entrySize = static_cast( + xdr::xdr_size(hotArchiveEntry->archivedEntry())); + } + else + { + auto constEntryLtxe = ltx.loadWithoutRecord(lk); - // We checked for TTLEntry existence above - releaseAssertOrThrow(constEntryLtxe); + // We checked for TTLEntry existence above + releaseAssertOrThrow(constEntryLtxe); + + entrySize = + static_cast(xdr::xdr_size(constEntryLtxe.current())); + } - uint32_t entrySize = - static_cast(xdr::xdr_size(constEntryLtxe.current())); metrics.mLedgerReadByte += entrySize; if (resources.readBytes < metrics.mLedgerReadByte) { @@ -137,11 +173,17 @@ RestoreFootprintOpFrame::doApply( rustChange.new_size_bytes = entrySize; rustChange.new_live_until_ledger = restoredLiveUntilLedger; - // Entry exists if we get this this point due to the constTTLLtxe - // loadWithoutRecord logic above. - auto ttlLtxe = ltx.load(ttlKey); - ttlLtxe.current().data.ttl().liveUntilLedgerSeq = - restoredLiveUntilLedger; + if (hotArchiveEntry) + { + ltx.restoreFromHotArchive(hotArchiveEntry->archivedEntry(), + restoredLiveUntilLedger); + } + else + { + // Entry exists in the live BucketList if we get this this point due + // to the constTTLLtxe loadWithoutRecord logic above. + ltx.restoreFromLiveBucketList(lk, restoredLiveUntilLedger); + } } uint32_t ledgerVersion = ltx.loadHeader().current().ledgerVersion; int64_t rentFee = rust_bridge::compute_rent_fee( diff --git a/src/transactions/TransactionFrame.cpp b/src/transactions/TransactionFrame.cpp index 4d9b2d6ae1..6ea4bec5aa 100644 --- a/src/transactions/TransactionFrame.cpp +++ b/src/transactions/TransactionFrame.cpp @@ -48,6 +48,7 @@ #include #include +#include #include namespace stellar @@ -57,6 +58,114 @@ namespace // Limit to the maximum resource fee allowed for transaction, // roughly 112 million lumens. int64_t const MAX_RESOURCE_FEE = 1LL << 50; + +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION +// Starting in protocol 23, some operation meta needs to be modified +// to be consumed by downstream systems. In particular, restoration is +// (mostly) logically a new entry creation from the perspective of ltx and +// stellar-core as a whole, but this change type is reclassified to +// LEDGER_ENTRY_RESTORED for easier consumption downstream. +LedgerEntryChanges +processOpLedgerEntryChanges(std::shared_ptr op, + AbstractLedgerTxn& ltx) +{ + if (op->getOperation().body.type() != RESTORE_FOOTPRINT) + { + return ltx.getChanges(); + } + + auto const& hotArchiveRestores = ltx.getRestoredHotArchiveKeys(); + auto const& liveRestores = ltx.getRestoredLiveBucketListKeys(); + + LedgerEntryChanges changes = ltx.getChanges(); + + // Depending on whether the restored entry is still in the live + // BucketList (has not yet been evicted), or has been evicted and is in + // the hot archive, meta will be handled differently as follows: + // + // Entry restore from Hot Archive: + // Meta before changes: + // Data/Code: LEDGER_ENTRY_CREATED + // TTL: LEDGER_ENTRY_CREATED + // Meta after changes: + // Data/Code: LEDGER_ENTRY_RESTORED + // TTL: LEDGER_ENTRY_RESTORED + // + // Entry restore from Live BucketList: + // Meta before changes: + // Data/Code: no meta + // TTL: LEDGER_ENTRY_STATE(oldValue), LEDGER_ENTRY_UPDATED(newValue) + // Meta after changes: + // Data/Code: LEDGER_ENTRY_RESTORED + // TTL: LEDGER_ENTRY_STATE(oldValue), LEDGER_ENTRY_RESTORED(newValue) + // + // First, iterate through existing meta and change everything we need to + // update. + for (auto& change : changes) + { + // For entry creation meta, we only need to check for Hot Archive + // restores + if (change.type() == LEDGER_ENTRY_CREATED) + { + auto le = change.created(); + if (hotArchiveRestores.find(LedgerEntryKey(le)) != + hotArchiveRestores.end()) + { + releaseAssertOrThrow(isPersistentEntry(le.data) || + le.data.type() == TTL); + change.type(LEDGER_ENTRY_RESTORED); + change.restored() = le; + } + } + // Update meta only applies to TTL meta + else if (change.type() == LEDGER_ENTRY_UPDATED) + { + if (change.updated().data.type() == TTL) + { + auto ttlLe = change.updated(); + if (liveRestores.find(LedgerEntryKey(ttlLe)) != + liveRestores.end()) + { + // Update the TTL change from LEDGER_ENTRY_UPDATED to + // LEDGER_ENTRY_RESTORED. + change.type(LEDGER_ENTRY_RESTORED); + change.restored() = ttlLe; + } + } + } + } + + // Now we need to insert all the LEDGER_ENTRY_RESTORED changes for the + // data entries that were not created but already existed on the live + // BucketList. These data/code entries have not been modified (only the TTL + // is updated), so ltx doesn't have any meta. However this is still useful + // for downstream so we manually insert restore meta here. + for (auto const& key : liveRestores) + { + if (key.type() == TTL) + { + continue; + } + releaseAssertOrThrow(isPersistentEntry(key)); + + // Note: this is already in the cache since the RestoreOp loaded + // all data keys for size calculation during apply already + auto entry = ltx.getNewestVersion(key); + + // If TTL already exists and is just being updated, the + // data entry must also already exist + releaseAssertOrThrow(entry); + + LedgerEntryChange change; + change.type(LEDGER_ENTRY_RESTORED); + change.restored() = entry->ledgerEntry(); + changes.push_back(change); + } + + return changes; +} +#endif + } // namespace using namespace std; @@ -1648,15 +1757,30 @@ TransactionFrame::applyOperations(SignatureChecker& signatureChecker, { success = false; } + + // The operation meta will be empty if the transaction + // doesn't succeed so we may as well not do any work in that + // case if (success) { app.checkOnOperationApply(op->getOperation(), opResult, ltxOp.getDelta()); - // The operation meta will be empty if the transaction - // doesn't succeed so we may as well not do any work in that - // case - operationMetas.emplace_back(ltxOp.getChanges()); + LedgerEntryChanges changes; +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + if (protocolVersionStartsFrom( + ledgerVersion, + LiveBucket:: + FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)) + { + changes = processOpLedgerEntryChanges(op, ltxOp); + } + else +#endif + { + changes = ltxOp.getChanges(); + } + operationMetas.emplace_back(changes); } if (txRes || diff --git a/src/transactions/test/InvokeHostFunctionTests.cpp b/src/transactions/test/InvokeHostFunctionTests.cpp index 27a91a1c2f..58ed5998b8 100644 --- a/src/transactions/test/InvokeHostFunctionTests.cpp +++ b/src/transactions/test/InvokeHostFunctionTests.cpp @@ -2752,6 +2752,92 @@ TEST_CASE_VERSIONS("entry eviction", "[tx][soroban][archival]") REQUIRE(!evicted); } } + + SECTION("Restoration Meta") + { + test.invokeRestoreOp({persistentKey}, 20'048); + auto targetRestorationLedger = test.getLCLSeq(); + + XDRInputFileStream in; + in.open(metaPath); + LedgerCloseMeta lcm; + bool restoreMeta = false; + + LedgerKeySet keysToRestore = {persistentKey, + getTTLKey(persistentKey)}; + while (in.readOne(lcm)) + { + REQUIRE(lcm.v() == 1); + if (lcm.v1().ledgerHeader.header.ledgerSeq == + targetRestorationLedger) + { + REQUIRE(lcm.v1().evictedTemporaryLedgerKeys.empty()); + REQUIRE( + lcm.v1().evictedPersistentLedgerEntries.empty()); + + REQUIRE(lcm.v1().txProcessing.size() == 1); + auto txMeta = lcm.v1().txProcessing.front(); + REQUIRE( + txMeta.txApplyProcessing.v3().operations.size() == + 1); + + REQUIRE(txMeta.txApplyProcessing.v3() + .operations[0] + .changes.size() == 2); + for (auto const& change : txMeta.txApplyProcessing.v3() + .operations[0] + .changes) + { + + // Only support persistent eviction meta >= p23 + LedgerKey lk; + if (protocolVersionStartsFrom( + cfg.TESTING_UPGRADE_LEDGER_PROTOCOL_VERSION, + BucketBase:: + FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)) + { + REQUIRE(change.type() == + LedgerEntryChangeType:: + LEDGER_ENTRY_RESTORED); + lk = LedgerEntryKey(change.restored()); + REQUIRE(keysToRestore.find(lk) != + keysToRestore.end()); + keysToRestore.erase(lk); + } + else + { + if (change.type() == + LedgerEntryChangeType::LEDGER_ENTRY_STATE) + { + lk = LedgerEntryKey(change.state()); + REQUIRE(lk == getTTLKey(persistentKey)); + keysToRestore.erase(lk); + } + else + { + REQUIRE(change.type() == + LedgerEntryChangeType:: + LEDGER_ENTRY_UPDATED); + lk = LedgerEntryKey(change.updated()); + REQUIRE(lk == getTTLKey(persistentKey)); + + // While we will see the TTL key twice, + // remove the TTL key in the path above and + // the persistent key here to make the check + // easier + keysToRestore.erase(persistentKey); + } + } + } + + restoreMeta = true; + break; + } + } + + REQUIRE(restoreMeta); + REQUIRE(keysToRestore.empty()); + } } #endif @@ -2920,45 +3006,134 @@ TEST_CASE("state archival operation errors", "[tx][soroban][archival]") } #ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION -TEST_CASE("evicted persistent entries", "[tx][soroban][archival]") +TEST_CASE("persistent entry archival", "[tx][soroban][archival]") { - auto cfg = getTestConfig(); - SorobanTest test(cfg, true, [](SorobanNetworkConfig& cfg) { - cfg.stateArchivalSettings().startingEvictionScanLevel = 1; - cfg.stateArchivalSettings().minPersistentTTL = 4; - }); + auto test = [](bool evict) { + auto cfg = getTestConfig(); + SorobanTest test(cfg, true, [evict](SorobanNetworkConfig& cfg) { + cfg.stateArchivalSettings().startingEvictionScanLevel = + evict ? 1 : 5; + cfg.stateArchivalSettings().minPersistentTTL = 4; + }); - ContractStorageTestClient client(test); + ContractStorageTestClient client(test); + + // WASM and instance should not expire + test.invokeExtendOp(client.getContract().getKeys(), 10'000); + + auto writeInvocation = client.getContract().prepareInvocation( + "put_persistent", {makeSymbolSCVal("key"), makeU64SCVal(123)}, + client.writeKeySpec("key", ContractDataDurability::PERSISTENT)); + REQUIRE(writeInvocation.withExactNonRefundableResourceFee().invoke()); + auto lk = client.getContract().getDataKey( + makeSymbolSCVal("key"), ContractDataDurability::PERSISTENT); + + auto evictionLedger = 14; + + // Close ledgers until entry is evicted + for (uint32_t i = test.getLCLSeq(); i < evictionLedger; ++i) + { + closeLedgerOn(test.getApp(), i, 2, 1, 2016); + } + + auto hotArchive = test.getApp() + .getBucketManager() + .getBucketSnapshotManager() + .copySearchableHotArchiveBucketListSnapshot(); + + if (evict) + { + REQUIRE(hotArchive->load(lk)); + REQUIRE(!hotArchive->load(getTTLKey(lk))); + { + LedgerTxn ltx(test.getApp().getLedgerTxnRoot()); + REQUIRE(!ltx.load(lk)); + REQUIRE(!ltx.load(getTTLKey(lk))); + } + } + else + { + REQUIRE(!hotArchive->load(lk)); + REQUIRE(!hotArchive->load(getTTLKey(lk))); + { + LedgerTxn ltx(test.getApp().getLedgerTxnRoot()); + REQUIRE(ltx.load(lk)); + REQUIRE(ltx.load(getTTLKey(lk))); + } + } - // WASM and instance should not expire - test.invokeExtendOp(client.getContract().getKeys(), 10'000); + // Rewriting entry should fail since key is archived + REQUIRE(!writeInvocation.withExactNonRefundableResourceFee().invoke()); - auto writeInvocation = client.getContract().prepareInvocation( - "put_persistent", {makeSymbolSCVal("key"), makeU64SCVal(123)}, - client.writeKeySpec("key", ContractDataDurability::PERSISTENT)); - REQUIRE(writeInvocation.withExactNonRefundableResourceFee().invoke()); - auto lk = client.getContract().getDataKey( - makeSymbolSCVal("key"), ContractDataDurability::PERSISTENT); + // Reads should also fail + REQUIRE(client.has("key", ContractDataDurability::PERSISTENT, + std::nullopt) == + INVOKE_HOST_FUNCTION_ENTRY_ARCHIVED); - auto evictionLedger = 14; + // Rent extension is a no op + SorobanResources resources; + resources.footprint.readOnly = {lk}; - // Close ledgers until entry is evicted - for (uint32_t i = test.getLCLSeq(); i < evictionLedger; ++i) + resources.instructions = 0; + resources.readBytes = 1000; + resources.writeBytes = 1000; + auto resourceFee = 1'000'000; + + auto extendTx = + test.createExtendOpTx(resources, 100, 1'000, resourceFee); + auto result = test.invokeTx(extendTx); + REQUIRE(isSuccessResult(result)); + + REQUIRE(client.has("key", ContractDataDurability::PERSISTENT, + std::nullopt) == + INVOKE_HOST_FUNCTION_ENTRY_ARCHIVED); + + // Restore should succeed. Fee should be the same for both evicted and + // nonevicted case + SECTION("restore with nonexistent key") + { + // Get random CONTRACT_CODE key that doesn't exist in ledger + UnorderedSet excludedKeys{ + client.getContract().getKeys().begin(), + client.getContract().getKeys().end()}; + auto randomKey = + LedgerTestUtils::generateValidUniqueLedgerKeysWithTypes( + {CONTRACT_CODE}, 1, excludedKeys) + .at(0); + + // Restore should skip nonexistent key and charge same fees + test.invokeRestoreOp({lk, randomKey}, 20'048); + } + + SECTION("key accessible after restore") + { + test.invokeRestoreOp({lk}, 20'048); + auto const& stateArchivalSettings = + test.getNetworkCfg().stateArchivalSettings(); + auto newExpectedLiveUntilLedger = + test.getLCLSeq() + stateArchivalSettings.minPersistentTTL - 1; + REQUIRE(test.getTTL(lk) == newExpectedLiveUntilLedger); + + client.get("key", ContractDataDurability::PERSISTENT, 123); + + test.getApp() + .getBucketManager() + .getBucketSnapshotManager() + .maybeCopySearchableHotArchiveBucketListSnapshot(hotArchive); + + // Restored entries are deleted from Hot Archive + REQUIRE(!hotArchive->load(lk)); + } + }; + + SECTION("eviction") { - closeLedgerOn(test.getApp(), i, 2, 1, 2016); + test(true); } - auto hotArchive = test.getApp() - .getBucketManager() - .getBucketSnapshotManager() - .copySearchableHotArchiveBucketListSnapshot(); - - REQUIRE(hotArchive->load(lk)); - REQUIRE(!hotArchive->load(getTTLKey(lk))); + SECTION("expiration without eviction") { - LedgerTxn ltx(test.getApp().getLedgerTxnRoot()); - REQUIRE(!ltx.load(lk)); - REQUIRE(!ltx.load(getTTLKey(lk))); + test(false); } } diff --git a/src/util/MetaUtils.cpp b/src/util/MetaUtils.cpp index ecd87e5644..a88cd996be 100644 --- a/src/util/MetaUtils.cpp +++ b/src/util/MetaUtils.cpp @@ -8,6 +8,7 @@ #include "util/GlobalChecks.h" #include "util/XDROperators.h" #include "util/types.h" +#include "xdr/Stellar-ledger.h" #include namespace @@ -20,9 +21,14 @@ struct CmpLedgerEntryChanges { // order that we want is: // LEDGER_ENTRY_STATE, LEDGER_ENTRY_CREATED, - // LEDGER_ENTRY_UPDATED, LEDGER_ENTRY_REMOVED - static constexpr std::array reindex = {1, 2, 3, 0}; - releaseAssert(let >= 0 && let < 4); + // LEDGER_ENTRY_UPDATED, LEDGER_ENTRY_REMOVED, LEDGER_ENTRY_RESTORED + static constexpr std::array reindex = {1, 2, 3, 0 +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + , + 4 +#endif + }; + releaseAssert(let >= 0 && let < 5); return reindex[let]; } @@ -44,6 +50,11 @@ struct CmpLedgerEntryChanges case LEDGER_ENTRY_REMOVED: res = change.removed(); break; +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + case LEDGER_ENTRY_RESTORED: + res = LedgerEntryKey(change.restored()); + break; +#endif } return res; } diff --git a/test-tx-meta-baseline-next/InvokeHostFunctionTests.json b/test-tx-meta-baseline-next/InvokeHostFunctionTests.json index f43a35a8f5..2548c2d5c3 100644 --- a/test-tx-meta-baseline-next/InvokeHostFunctionTests.json +++ b/test-tx-meta-baseline-next/InvokeHostFunctionTests.json @@ -1325,11 +1325,10 @@ "bKDF6V5IzTo=" ], "contract storage|footprint|unused readWrite key" : [ "VzQb0aWq2bE=" ], - "entry eviction|protocol version 20" : [ "bKDF6V5IzTo=", "bKDF6V5IzTo=", "bKDF6V5IzTo=" ], - "entry eviction|protocol version 21" : [ "bKDF6V5IzTo=", "bKDF6V5IzTo=", "bKDF6V5IzTo=" ], - "entry eviction|protocol version 22" : [ "bKDF6V5IzTo=", "bKDF6V5IzTo=", "bKDF6V5IzTo=" ], - "entry eviction|protocol version 23" : [ "bKDF6V5IzTo=", "bKDF6V5IzTo=", "bKDF6V5IzTo=" ], - "evicted persistent entries" : [ "bKDF6V5IzTo=" ], + "entry eviction|protocol version 20" : [ "bKDF6V5IzTo=", "bKDF6V5IzTo=", "bKDF6V5IzTo=", "bKDF6V5IzTo=" ], + "entry eviction|protocol version 21" : [ "bKDF6V5IzTo=", "bKDF6V5IzTo=", "bKDF6V5IzTo=", "bKDF6V5IzTo=" ], + "entry eviction|protocol version 22" : [ "bKDF6V5IzTo=", "bKDF6V5IzTo=", "bKDF6V5IzTo=", "bKDF6V5IzTo=" ], + "entry eviction|protocol version 23" : [ "bKDF6V5IzTo=", "bKDF6V5IzTo=", "bKDF6V5IzTo=", "bKDF6V5IzTo=" ], "failure diagnostics" : [ "bKDF6V5IzTo=" ], "ledger entry size limit enforced" : [ "bKDF6V5IzTo=", "bKDF6V5IzTo=" ], "loadgen Wasm executes properly" : [ "bKDF6V5IzTo=" ], @@ -1345,6 +1344,8 @@ "bKDF6V5IzTo=", "bKDF6V5IzTo=" ], + "persistent entry archival|eviction" : [ "bKDF6V5IzTo=", "bKDF6V5IzTo=" ], + "persistent entry archival|expiration without eviction" : [ "bKDF6V5IzTo=", "bKDF6V5IzTo=" ], "refund account merged" : [ "bKDF6V5IzTo=", "398Rd+u+jdE=", "b4KxXJCxvM0=", "7/h1q7KjqKA=" ], "refund is sent to fee-bump source|protocol version 20" : [ "bKDF6V5IzTo=", "JRiUwIP1h2Q=", "LyDINL0VaLk=" ], "refund is sent to fee-bump source|protocol version 21" : [ "bKDF6V5IzTo=", "JRiUwIP1h2Q=", "LyDINL0VaLk=" ],