|
3 | 3 | // of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 |
4 | 4 |
|
5 | 5 | #include "invariant/ConservationOfLumens.h" |
6 | | -#include "crypto/SHA.h" |
7 | 6 | #include "invariant/InvariantManager.h" |
| 7 | +#include "ledger/LedgerManager.h" |
8 | 8 | #include "ledger/LedgerTxn.h" |
9 | 9 | #include "main/Application.h" |
10 | 10 | #include "transactions/TransactionUtils.h" |
11 | | -#include "util/GlobalChecks.h" |
12 | | -#include <fmt/format.h> |
| 11 | +#include "util/LogSlowExecution.h" |
13 | 12 | #include <numeric> |
14 | 13 |
|
15 | 14 | namespace stellar |
@@ -175,4 +174,222 @@ ConservationOfLumens::checkOnOperationApply( |
175 | 174 | } |
176 | 175 | return {}; |
177 | 176 | } |
| 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 | +} |
178 | 395 | } |
0 commit comments