Skip to content

Commit c8c7a75

Browse files
authored
Lumen snapshot invariant (#5059)
# Description Resolves #5028 <!--- Describe what this pull request does, which issue it's resolving (usually applicable for code changes). ---> # Checklist - [ ] Reviewed the [contributing](https://github.com/stellar/stellar-core/blob/master/CONTRIBUTING.md#submitting-changes) document - [ ] Rebased on top of master (no merge commits) - [ ] Ran `clang-format` v8.0.0 (via `make format` or the Visual Studio extension) - [ ] Compiles - [ ] Ran all tests - [ ] If change impacts performance, include supporting evidence per the [performance document](https://github.com/stellar/stellar-core/blob/master/performance-eval/performance-eval.md)
2 parents d7290d7 + dcc1a0d commit c8c7a75

14 files changed

+589
-38
lines changed

src/invariant/ArchivedStateConsistency.cpp

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ ArchivedStateConsistency::ArchivedStateConsistency() : Invariant(true)
3030
std::string
3131
ArchivedStateConsistency::checkSnapshot(
3232
CompleteConstLedgerStatePtr ledgerState,
33-
InMemorySorobanState const& inMemorySnapshot)
33+
InMemorySorobanState const& inMemorySnapshot,
34+
std::function<bool()> isStopping)
3435
{
3536
LogSlowExecution logSlow("ArchivedStateConsistency::stateSnapshotInvariant",
3637
LogSlowExecution::Mode::AUTOMATIC_RAII, "took",
@@ -54,33 +55,39 @@ ArchivedStateConsistency::checkSnapshot(
5455
// For each entry in the Live BucketList, check if we have seen it in a
5556
// previous level. If not, this entry is the newest version, so check if it
5657
// exists in the Hot Archive.
57-
auto checkIfLiveEntryInArchive =
58-
[&seenKeys, &errorMsg, &hotArchiveSnapshot](BucketEntry const& be) {
59-
if (be.type() == LIVEENTRY || be.type() == INITENTRY)
60-
{
61-
auto lk = LedgerEntryKey(be.liveEntry());
62-
auto [_, wasInserted] = seenKeys.emplace(lk);
63-
64-
// If this BucketEntry is not shadowed, and the key exists in
65-
// the Hot Archive, we have an error.
66-
if (wasInserted && hotArchiveSnapshot->load(lk))
67-
{
68-
errorMsg = fmt::format(
69-
FMT_STRING("ArchivedStateConsistency invariant failed: "
70-
"Live entry is present in both live and "
71-
"archived state: {}"),
72-
xdrToCerealString(lk, "entry_key"));
73-
return Loop::COMPLETE;
74-
}
75-
}
76-
// Mark DEADENTRY as seen, but the key does not exist wrt ledger
77-
// state, so we don't need to check the Hot Archive.
78-
else if (be.type() == DEADENTRY)
58+
auto checkIfLiveEntryInArchive = [&seenKeys, &errorMsg, &hotArchiveSnapshot,
59+
&isStopping](BucketEntry const& be) {
60+
// Allow early termination if application is stopping
61+
if (isStopping())
62+
{
63+
return Loop::COMPLETE;
64+
}
65+
66+
if (be.type() == LIVEENTRY || be.type() == INITENTRY)
67+
{
68+
auto lk = LedgerEntryKey(be.liveEntry());
69+
auto [_, wasInserted] = seenKeys.emplace(lk);
70+
71+
// If this BucketEntry is not shadowed, and the key exists in
72+
// the Hot Archive, we have an error.
73+
if (wasInserted && hotArchiveSnapshot->load(lk))
7974
{
80-
seenKeys.emplace(be.deadEntry());
75+
errorMsg = fmt::format(
76+
FMT_STRING("ArchivedStateConsistency invariant failed: "
77+
"Live entry is present in both live and "
78+
"archived state: {}"),
79+
xdrToCerealString(lk, "entry_key"));
80+
return Loop::COMPLETE;
8181
}
82-
return Loop::INCOMPLETE;
83-
};
82+
}
83+
// Mark DEADENTRY as seen, but the key does not exist wrt ledger
84+
// state, so we don't need to check the Hot Archive.
85+
else if (be.type() == DEADENTRY)
86+
{
87+
seenKeys.emplace(be.deadEntry());
88+
}
89+
return Loop::INCOMPLETE;
90+
};
8491

8592
// We just need to check for Soroban types that are stored in the Hot
8693
// Archive

src/invariant/ArchivedStateConsistency.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class ArchivedStateConsistency : public Invariant
4444

4545
virtual std::string
4646
checkSnapshot(CompleteConstLedgerStatePtr ledgerState,
47-
InMemorySorobanState const& inMemorySnapshot) override;
47+
InMemorySorobanState const& inMemorySnapshot,
48+
std::function<bool()> isStopping) override;
4849
};
4950
}

src/invariant/ConservationOfLumens.cpp

Lines changed: 220 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@
33
// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0
44

55
#include "invariant/ConservationOfLumens.h"
6-
#include "crypto/SHA.h"
76
#include "invariant/InvariantManager.h"
7+
#include "ledger/LedgerManager.h"
88
#include "ledger/LedgerTxn.h"
99
#include "main/Application.h"
1010
#include "transactions/TransactionUtils.h"
11-
#include "util/GlobalChecks.h"
12-
#include <fmt/format.h>
11+
#include "util/LogSlowExecution.h"
1312
#include <numeric>
1413

1514
namespace stellar
@@ -175,4 +174,222 @@ ConservationOfLumens::checkOnOperationApply(
175174
}
176175
return {};
177176
}
177+
178+
// Helper function that processes an entry if it hasn't been seen before.
179+
// Returns true on success, false on error (with error set in errorMsg).
180+
static bool
181+
processEntryIfNew(LedgerEntry const& entry, LedgerKey const& key,
182+
std::unordered_set<LedgerKey>& countedKeys,
183+
Asset const& asset,
184+
AssetContractInfo const& assetContractInfo,
185+
int64_t& sumBalance, std::string& errorMsg)
186+
{
187+
if (countedKeys.count(key) != 0)
188+
{
189+
return true;
190+
}
191+
192+
auto result = getAssetBalance(entry, asset, assetContractInfo);
193+
194+
if (result.overflowed)
195+
{
196+
errorMsg = fmt::format(
197+
FMT_STRING(
198+
"ConservationOfLumens: getAssetBalance overflow for key: {}"),
199+
xdrToCerealString(key, "ledger_key"));
200+
return false;
201+
}
202+
203+
if (!result.assetMatched)
204+
{
205+
return true; // Asset doesn't match, skip
206+
}
207+
208+
if (!result.balance || !addBalance(sumBalance, *result.balance))
209+
{
210+
errorMsg = fmt::format(
211+
FMT_STRING(
212+
"ConservationOfLumens: Overflow adding balance for key: {}"),
213+
xdrToCerealString(key, "ledger_key"));
214+
return false;
215+
}
216+
217+
countedKeys.emplace(key);
218+
219+
return true;
220+
}
221+
222+
// Scan live bucket list for entries that can hold the native asset
223+
static void
224+
scanLiveBuckets(
225+
std::shared_ptr<SearchableLiveBucketListSnapshot const> const& liveSnapshot,
226+
Asset const& asset, AssetContractInfo const& assetContractInfo,
227+
int64_t& sumBalance, std::string& errorMsg,
228+
std::function<bool()> const& isStopping)
229+
{
230+
// Scan all entry types that can hold the native asset
231+
for (auto let : xdr::xdr_traits<LedgerEntryType>::enum_values())
232+
{
233+
LedgerEntryType type = static_cast<LedgerEntryType>(let);
234+
if (!canHoldAsset(type, asset))
235+
{
236+
continue;
237+
}
238+
239+
std::unordered_set<LedgerKey> countedKeys;
240+
241+
liveSnapshot->scanForEntriesOfType(
242+
type, [&](BucketEntry const& be) -> Loop {
243+
if (isStopping())
244+
{
245+
return Loop::COMPLETE;
246+
}
247+
248+
if (be.type() == LIVEENTRY || be.type() == INITENTRY)
249+
{
250+
if (!processEntryIfNew(
251+
be.liveEntry(), LedgerEntryKey(be.liveEntry()),
252+
countedKeys, asset, assetContractInfo, sumBalance,
253+
errorMsg))
254+
{
255+
return Loop::COMPLETE;
256+
}
257+
}
258+
else if (be.type() == DEADENTRY)
259+
{
260+
countedKeys.emplace(be.deadEntry());
261+
}
262+
return Loop::INCOMPLETE;
263+
});
264+
265+
if (!errorMsg.empty())
266+
{
267+
return;
268+
}
269+
}
270+
}
271+
272+
static Loop
273+
scanHotArchiveBucket(HotArchiveBucketSnapshot const& bucket,
274+
std::unordered_set<LedgerKey>& countedKeys,
275+
Asset const& asset,
276+
AssetContractInfo const& assetContractInfo,
277+
int64_t& sumBalance, std::string& errorMsg,
278+
std::function<bool()> const& isStopping)
279+
{
280+
for (HotArchiveBucketInputIterator iter(bucket.getRawBucket()); iter;
281+
++iter)
282+
{
283+
// Allow early termination if application is stopping
284+
if (isStopping())
285+
{
286+
return Loop::COMPLETE;
287+
}
288+
289+
auto const& be = *iter;
290+
if (be.type() == HOT_ARCHIVE_ARCHIVED)
291+
{
292+
if (!canHoldAsset(be.archivedEntry().data.type(), asset))
293+
{
294+
continue;
295+
}
296+
if (!processEntryIfNew(be.archivedEntry(),
297+
LedgerEntryKey(be.archivedEntry()),
298+
countedKeys, asset, assetContractInfo,
299+
sumBalance, errorMsg))
300+
{
301+
return Loop::COMPLETE;
302+
}
303+
}
304+
else if (be.type() == HOT_ARCHIVE_LIVE &&
305+
canHoldAsset(be.key().type(), asset))
306+
{
307+
// HOT_ARCHIVE_LIVE means entry was restored from archive,
308+
// so mark it as seen (shadowing any archived versions)
309+
countedKeys.emplace(be.key());
310+
}
311+
}
312+
return Loop::INCOMPLETE;
313+
}
314+
315+
std::string
316+
ConservationOfLumens::checkSnapshot(
317+
CompleteConstLedgerStatePtr ledgerState,
318+
InMemorySorobanState const& inMemorySnapshot,
319+
std::function<bool()> isStopping)
320+
{
321+
LogSlowExecution logSlow("ConservationOfLumens::checkSnapshot",
322+
LogSlowExecution::Mode::AUTOMATIC_RAII, "took",
323+
std::chrono::seconds(90));
324+
325+
auto liveSnapshot = ledgerState->getBucketSnapshot();
326+
auto hotArchiveSnapshot = ledgerState->getHotArchiveSnapshot();
327+
auto const& header = liveSnapshot->getLedgerHeader();
328+
329+
// This invariant can fail prior to v24 due to bugs
330+
if (protocolVersionIsBefore(header.ledgerVersion, ProtocolVersion::V_24))
331+
{
332+
return std::string{};
333+
}
334+
335+
Asset nativeAsset(ASSET_TYPE_NATIVE);
336+
337+
int64_t sumBalance = 0;
338+
std::string errorMsg;
339+
340+
// Start with the fee pool from the ledger header
341+
if (!addBalance(sumBalance, header.feePool))
342+
{
343+
return fmt::format(
344+
FMT_STRING("ConservationOfLumens invariant failed: "
345+
"Fee pool balance overflowed when added to total. "
346+
"Current sum: {}, Fee pool: {}"),
347+
sumBalance, header.feePool);
348+
}
349+
350+
// Scan the Live BucketList for native balances using loopAllBuckets
351+
352+
scanLiveBuckets(liveSnapshot, nativeAsset, mLumenContractInfo, sumBalance,
353+
errorMsg, isStopping);
354+
355+
if (!errorMsg.empty())
356+
{
357+
return errorMsg;
358+
}
359+
360+
// Scan the Hot Archive for native balances using loopAllBuckets
361+
{
362+
std::unordered_set<LedgerKey> countedKeys;
363+
hotArchiveSnapshot->loopAllBuckets(
364+
[&countedKeys, &nativeAsset, &sumBalance, &errorMsg, &isStopping,
365+
this](HotArchiveBucketSnapshot const& bucket) {
366+
return scanHotArchiveBucket(bucket, countedKeys, nativeAsset,
367+
mLumenContractInfo, sumBalance,
368+
errorMsg, isStopping);
369+
});
370+
371+
if (!errorMsg.empty())
372+
{
373+
return errorMsg;
374+
}
375+
}
376+
377+
// We stopped early, so it's likely we didn't finish scanning everything
378+
if (isStopping())
379+
{
380+
return std::string{};
381+
}
382+
383+
// Compare the calculated total with totalCoins from the ledger header
384+
if (sumBalance != header.totalCoins)
385+
{
386+
return fmt::format(
387+
FMT_STRING("ConservationOfLumens invariant failed: "
388+
"Total native asset supply mismatch. "
389+
"Calculated from buckets: {}, Expected (totalCoins): "
390+
"{}, Difference: {}"),
391+
sumBalance, header.totalCoins, header.totalCoins - sumBalance);
392+
}
393+
return std::string{};
394+
}
178395
}

src/invariant/ConservationOfLumens.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ class ConservationOfLumens : public Invariant
3333
LedgerTxnDelta const& ltxDelta,
3434
std::vector<ContractEvent> const& events, AppConnector& app) override;
3535

36+
virtual std::string
37+
checkSnapshot(CompleteConstLedgerStatePtr ledgerState,
38+
InMemorySorobanState const& inMemorySnapshot,
39+
std::function<bool()> isStopping) override;
40+
3641
private:
3742
AssetContractInfo const mLumenContractInfo;
3843
};

src/invariant/Invariant.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ class Invariant
8686

8787
virtual std::string
8888
checkSnapshot(CompleteConstLedgerStatePtr ledgerState,
89-
InMemorySorobanState const& inMemorySnapshot)
89+
InMemorySorobanState const& inMemorySnapshot,
90+
std::function<bool()> isStopping)
9091
{
9192
return std::string{};
9293
}

src/invariant/InvariantManager.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ class InvariantManager
6868
// INVARIANT_EXTRA_CHECKS is enabled.
6969
virtual void
7070
runStateSnapshotInvariant(CompleteConstLedgerStatePtr ledgerState,
71-
InMemorySorobanState const& inMemorySnapshot) = 0;
71+
InMemorySorobanState const& inMemorySnapshot,
72+
std::function<bool()> isStopping) = 0;
7273

7374
virtual void registerInvariant(std::shared_ptr<Invariant> invariant) = 0;
7475

src/invariant/InvariantManagerImpl.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,8 @@ InvariantManagerImpl::handleInvariantFailure(bool isStrict,
331331
void
332332
InvariantManagerImpl::runStateSnapshotInvariant(
333333
CompleteConstLedgerStatePtr ledgerState,
334-
InMemorySorobanState const& inMemorySnapshot)
334+
InMemorySorobanState const& inMemorySnapshot,
335+
std::function<bool()> isStopping)
335336
{
336337
// Reset our trigger flag and mark the invariant as running.
337338
mStateSnapshotInvariantRunning = true;
@@ -342,7 +343,8 @@ InvariantManagerImpl::runStateSnapshotInvariant(
342343

343344
for (auto const& invariant : mEnabled)
344345
{
345-
auto result = invariant->checkSnapshot(ledgerState, inMemorySnapshot);
346+
auto result =
347+
invariant->checkSnapshot(ledgerState, inMemorySnapshot, isStopping);
346348
if (!result.empty())
347349
{
348350
auto ledgerSeq =

src/invariant/InvariantManagerImpl.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ class InvariantManagerImpl : public InvariantManager
8080

8181
bool shouldRunInvariantSnapshot() const override;
8282

83-
void runStateSnapshotInvariant(
84-
CompleteConstLedgerStatePtr ledgerState,
85-
InMemorySorobanState const& inMemorySnapshot) override;
83+
void runStateSnapshotInvariant(CompleteConstLedgerStatePtr ledgerState,
84+
InMemorySorobanState const& inMemorySnapshot,
85+
std::function<bool()> isStopping) override;
8686

8787
#ifdef BUILD_TESTS
8888
void snapshotForFuzzer() override;

0 commit comments

Comments
 (0)