@@ -5281,6 +5281,161 @@ TEST_CASE("slots purged while externalized ledgers are queued to apply",
52815281 10 * simulation->getExpectedLedgerCloseTime (), false );
52825282}
52835283
5284+ TEST_CASE (" apply buffered ledgers after repeated out-of-sync" , " [herder]" )
5285+ {
5286+ auto networkID = sha256 (getTestConfig ().NETWORK_PASSPHRASE );
5287+ auto simulation = std::make_shared<Simulation>(
5288+ Simulation::OVER_LOOPBACK , networkID, [&](int i) {
5289+ auto cfg = getTestConfig (i, Config::TESTDB_BUCKET_DB_PERSISTENT );
5290+ cfg.RUN_STANDALONE = false ;
5291+ return cfg;
5292+ });
5293+
5294+ auto validatorAKey = SecretKey::fromSeed (sha256 (" validator-A" ));
5295+ auto validatorBKey = SecretKey::fromSeed (sha256 (" validator-B" ));
5296+ auto validatorCKey = SecretKey::fromSeed (sha256 (" validator-C" ));
5297+
5298+ SCPQuorumSet qset;
5299+ qset.threshold = 2 ;
5300+ qset.validators .push_back (validatorAKey.getPublicKey ());
5301+ qset.validators .push_back (validatorBKey.getPublicKey ());
5302+ qset.validators .push_back (validatorCKey.getPublicKey ());
5303+
5304+ auto A = simulation->addNode (validatorAKey, qset);
5305+ auto B = simulation->addNode (validatorBKey, qset);
5306+ auto C = simulation->addNode (validatorCKey, qset);
5307+
5308+ simulation->addPendingConnection (validatorAKey.getPublicKey (),
5309+ validatorCKey.getPublicKey ());
5310+ simulation->addPendingConnection (validatorAKey.getPublicKey (),
5311+ validatorBKey.getPublicKey ());
5312+
5313+ simulation->startAllNodes ();
5314+ simulation->stopOverlayTick ();
5315+
5316+ HerderImpl& herderC = static_cast <HerderImpl&>(C->getHerder ());
5317+ auto & lmC = C->getLedgerManager ();
5318+
5319+ // Close a few ledgers with everyone connected
5320+ simulation->crankUntil (
5321+ [&]() {
5322+ return simulation->haveAllExternalized (
5323+ LedgerManager::GENESIS_LEDGER_SEQ + 4 , 1 );
5324+ },
5325+ 10 * simulation->getExpectedLedgerCloseTime (), false );
5326+
5327+ // Disconnect C; the network moves on without it
5328+ simulation->dropConnection (validatorAKey.getPublicKey (),
5329+ validatorCKey.getPublicKey ());
5330+ uint32_t const N = lmC.getLastClosedLedgerNum ();
5331+
5332+ // Advance A and B to N+5, then freeze the network by disconnecting them, so
5333+ // their state can be compared against C later. C will externalize N+1..N+5;
5334+ // the 5-ledger drift is below MAX_EXTERNALIZE_LEDGER_APPLY_DRIFT (12), so
5335+ // once the gap is filled the whole run can be queued to apply at once. The
5336+ // tracking index stays within MAX_SLOTS_TO_REMEMBER (12) of the gap ledger,
5337+ // so the gap envelope is never discarded.
5338+ uint32_t const last = N + 5 ;
5339+ simulation->crankUntil (
5340+ [&]() {
5341+ return A->getLedgerManager ().getLastClosedLedgerNum () >= last &&
5342+ B->getLedgerManager ().getLastClosedLedgerNum () >= last;
5343+ },
5344+ 2 * last * simulation->getExpectedLedgerCloseTime (), false );
5345+ simulation->dropConnection (validatorAKey.getPublicKey (),
5346+ validatorBKey.getPublicKey ());
5347+
5348+ auto validatorSCPMessagesA =
5349+ getValidatorExternalizeMessages (*A, N + 1 , last);
5350+ auto validatorSCPMessagesB =
5351+ getValidatorExternalizeMessages (*B, N + 1 , last);
5352+ REQUIRE (validatorSCPMessagesA.size () == 5 );
5353+ REQUIRE (validatorSCPMessagesB.size () == 5 );
5354+
5355+ auto feedLedger = [&](uint32_t ledger) {
5356+ auto newMsgA = validatorSCPMessagesA.at (ledger);
5357+ auto newMsgB = validatorSCPMessagesB.at (ledger);
5358+ REQUIRE (herderC.recvSCPEnvelope (newMsgA.first , qset, newMsgA.second ) ==
5359+ Herder::ENVELOPE_STATUS_READY );
5360+ REQUIRE (herderC.recvSCPEnvelope (newMsgB.first , qset, newMsgB.second ) ==
5361+ Herder::ENVELOPE_STATUS_READY );
5362+ };
5363+
5364+ // Step 1: go out of sync at N. Feed C the ledger after the missing one
5365+ // (N+2), leaving a gap at N+1. C is tracking N, so the future slot is only
5366+ // processed once the consensus-stuck timer fires and Herder goes out of
5367+ // sync; afterwards Herder is tracking N+2 but LM's LCL is stuck at N.
5368+ feedLedger (N + 2 );
5369+ simulation->crankUntil ([&]() { return !lmC.isSynced (); },
5370+ 2 * Herder::CONSENSUS_STUCK_TIMEOUT_SECONDS , false );
5371+ checkHerder (*C, herderC, Herder::State::HERDER_TRACKING_NETWORK_STATE ,
5372+ N + 2 );
5373+ REQUIRE (lmC.getLastClosedLedgerNum () == N);
5374+
5375+ // Step 2: buffer up to N+5. Each future slot externalizes at receive time
5376+ // (LM is buffering, so nothing is applying), advancing the tracking index
5377+ // while the LCL stays stuck at N and the gap at N+1 remains.
5378+ for (uint32_t seq = N + 3 ; seq <= last; ++seq)
5379+ {
5380+ feedLedger (seq);
5381+ simulation->crankForAtLeast (std::chrono::seconds (1 ), false );
5382+ checkHerder (*C, herderC, Herder::State::HERDER_TRACKING_NETWORK_STATE ,
5383+ seq);
5384+ REQUIRE (lmC.getLastClosedLedgerNum () == N);
5385+ }
5386+
5387+ // N+2..N+5 are buffered, waiting for N+1; nothing is applying.
5388+ auto & lamC = C->getLedgerApplyManager ();
5389+ REQUIRE (!lmC.isApplying ());
5390+ REQUIRE (!lamC.maybeGetNextBufferedLedgerToApply ());
5391+ REQUIRE (lamC.maybeGetLargestBufferedLedger ()->getLedgerSeq () == last);
5392+
5393+ // Step 3: go out of sync again. With no LCL progress and no new latest
5394+ // externalize to re-arm the heartbeat, the consensus-stuck timer fires
5395+ // again. Since nothing is applying, this drives Herder all the way to the
5396+ // out-of-sync (SYNCING) state, where it stays -- there are no unprocessed
5397+ // slots to bring it back to tracking.
5398+ simulation->crankForAtLeast (Herder::CONSENSUS_STUCK_TIMEOUT_SECONDS +
5399+ std::chrono::seconds (5 ),
5400+ false );
5401+ checkHerder (*C, herderC, Herder::State::HERDER_SYNCING_STATE , last);
5402+ REQUIRE (!lmC.isApplying ());
5403+ REQUIRE (lmC.getLastClosedLedgerNum () == N);
5404+
5405+ // Step 4: fill the gap with N+1. The run N+1..N+5 becomes contiguous, so
5406+ // LedgerApplyManager queues all of them to the apply thread at once.
5407+ feedLedger (N + 1 );
5408+ REQUIRE (lmC.isApplying ());
5409+ REQUIRE (lmC.getLastClosedLedgerNum () == N);
5410+
5411+ // The buffered ledgers all apply and C ends up synced on the same ledger
5412+ // and hash as A and B, which closed these ledgers via real consensus.
5413+ simulation->crankUntil (
5414+ [&]() { return lmC.getLastClosedLedgerNum () == last; },
5415+ 4 * last * simulation->getExpectedLedgerCloseTime (), false );
5416+ checkHerder (*C, herderC, Herder::State::HERDER_SYNCING_STATE , last);
5417+ REQUIRE (lmC.getLastClosedLedgerNum () == last);
5418+ REQUIRE (lmC.getLastClosedLedgerHeader ().hash ==
5419+ A->getLedgerManager ().getLastClosedLedgerHeader ().hash );
5420+
5421+ // Reconnect everyone: the network (including C) proceeds to close ledgers.
5422+ simulation->addConnection (validatorAKey.getPublicKey (),
5423+ validatorCKey.getPublicKey ());
5424+ simulation->addConnection (validatorAKey.getPublicKey (),
5425+ validatorBKey.getPublicKey ());
5426+ simulation->crankUntil (
5427+ [&]() { return simulation->haveAllExternalized (last + 3 , 1 ); },
5428+ 10 * simulation->getExpectedLedgerCloseTime (), false );
5429+
5430+ // C is back in sync and ready to move on to the next ledger.
5431+ checkSynced (*C);
5432+ checkHerder (*C, herderC, Herder::State::HERDER_TRACKING_NETWORK_STATE ,
5433+ last + 3 );
5434+
5435+ REQUIRE (herderC.getTriggerTimer ().seq () > 0 );
5436+ REQUIRE (herderC.mTriggerNextLedgerSeq == last + 4 );
5437+ }
5438+
52845439// The stronger variant of the test above: the node externalizes _more_ slots
52855440// than MAX_SLOTS_TO_REMEMBER while its LCL is stuck, so purging evicts the
52865441// slots of ledgers that are themselves still queued for application. Such a
0 commit comments