Skip to content

Commit e3937cc

Browse files
committed
Cleanup herder out of sync flow and add test
1 parent 930fa02 commit e3937cc

2 files changed

Lines changed: 156 additions & 1 deletion

File tree

src/herder/test/HerderTests.cpp

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

src/ledger/LedgerManagerImpl.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1373,7 +1373,7 @@ LedgerManagerImpl::ledgerCloseComplete(uint32_t lcl, bool calledViaExternalize,
13731373
// "synced"
13741374
bool appliedLatest = false;
13751375

1376-
if (latestHeardFromNetwork == lcl)
1376+
if (latestHeardFromNetwork == lcl && mApp.getHerder().isTracking())
13771377
{
13781378
mApp.getLedgerManager().moveToSynced();
13791379
appliedLatest = true;

0 commit comments

Comments
 (0)