@@ -6535,6 +6535,13 @@ namespace stellar
65356535class EarlyNominationTestAccess
65366536{
65376537 public:
6538+ static VirtualClock::time_point
6539+ triggerAnchor (HerderImpl& herder, uint64_t slot)
6540+ {
6541+ return herder.triggerAnchorFromConsensusCloseTime (
6542+ slot, herder.mApp .getClock ().now (), std::chrono::seconds (2 ));
6543+ }
6544+
65386545 static TxSetXDRFrameConstPtr
65396546 prepared (HerderImpl& herder)
65406547 {
@@ -6561,6 +6568,195 @@ class EarlyNominationTestAccess
65616568};
65626569}
65636570
6571+ TEST_CASE (" trigger fallback credits elapsed time through nomination" ,
6572+ " [herder][trigger-construction]" )
6573+ {
6574+ using namespace std ::chrono;
6575+ struct Scenario
6576+ {
6577+ int buildMs;
6578+ int ballotMs;
6579+ int closeMs;
6580+ int clockOffsetMs;
6581+ bool recordTrigger;
6582+ int expectedWaitMs;
6583+ int expectedFallbacks;
6584+ };
6585+ auto scenario =
6586+ GENERATE (Scenario{350 , 1900 , 2100 , 0 , true , 0 , 0 },
6587+ Scenario{350 , 1900 , 2100 , 4000 , true , 1800 , 1 },
6588+ Scenario{350 , 1900 , 2100 , -4000 , true , 1800 , 1 },
6589+ Scenario{350 , 1900 , 4100 , 0 , true , 0 , 1 },
6590+ Scenario{350 , 1900 , 2100 , 0 , false , 1800 , 1 },
6591+ Scenario{350 , 1500 , 1700 , 0 , true , 300 , 0 },
6592+ // A slow first round need not execute a timeout callback
6593+ // before entering ballot. Its elapsed time is still real.
6594+ Scenario{100 , 2000 , 2250 , 0 , true , 0 , 0 },
6595+ // Followers need no construction, but still wait on peers.
6596+ Scenario{0 , 2250 , 2450 , 0 , true , 0 , 0 },
6597+ // Delayed timer callbacks must not hide elapsed time.
6598+ Scenario{100 , 6500 , 6700 , 0 , true , 0 , 0 },
6599+ // Long nomination does not excuse arbitrary clock drift.
6600+ Scenario{100 , 6500 , 6700 , 10000 , true , 1800 , 1 },
6601+ Scenario{100 , 6500 , 6700 , -10000 , true , 1800 , 1 });
6602+ CAPTURE (scenario.buildMs , scenario.closeMs , scenario.clockOffsetMs ,
6603+ scenario.recordTrigger );
6604+ VirtualClock clock;
6605+ auto cfg = getTestConfig ();
6606+ cfg.HTTP_PORT = 0 ;
6607+ cfg.MANUAL_CLOSE = true ;
6608+ auto app = createTestApplication (clock, cfg);
6609+ auto & herder = static_cast <HerderImpl&>(app->getHerder ());
6610+ auto & driver = herder.getHerderSCPDriver ();
6611+ auto const slot = app->getLedgerManager ().getLastClosedLedgerNum ();
6612+ auto const ct = VirtualClock::to_time_t (clock.system_now ()) + 1000 ;
6613+ auto const origin = VirtualClock::from_time_t (ct);
6614+ clock.setCurrentVirtualTime (origin);
6615+ StellarValue value;
6616+ value.closeTime = ct;
6617+ herder.setTrackingSCPState (slot, value, true );
6618+ if (scenario.recordTrigger )
6619+ {
6620+ driver.recordNominationTrigger (slot);
6621+ }
6622+ clock.setCurrentVirtualTime (origin + milliseconds (scenario.buildMs ));
6623+ driver.recordSCPEvent (slot, true );
6624+ clock.setCurrentVirtualTime (origin + milliseconds (scenario.ballotMs ));
6625+ driver.recordSCPEvent (slot, false );
6626+ clock.setCurrentVirtualTime (origin + milliseconds (scenario.closeMs ));
6627+ clock.setSystemTimeOffset (milliseconds (scenario.clockOffsetMs ));
6628+ REQUIRE (driver.getNominationTimeouts (slot).value () == 0 );
6629+ REQUIRE (driver.getTriggerToBallotDuration (slot) ==
6630+ milliseconds (scenario.recordTrigger ? scenario.ballotMs : 0 ));
6631+ REQUIRE (driver.getTriggerToBallotDuration (slot + 1 ) ==
6632+ milliseconds::zero ());
6633+ auto metrics = app->getMetrics ().GetAllMetrics ();
6634+ auto const & fallback = dynamic_cast <medida::Meter const &>(
6635+ *metrics.at ({" scp" , " trigger" , " prepare-start-fallback" }));
6636+ auto const before = fallback.count ();
6637+ auto deadline = std::max (
6638+ clock.now (),
6639+ EarlyNominationTestAccess::triggerAnchor (herder, slot) + seconds (2 ));
6640+ REQUIRE (duration_cast<milliseconds>(deadline - clock.now ()).count () ==
6641+ scenario.expectedWaitMs );
6642+ REQUIRE (fallback.count () - before == scenario.expectedFallbacks );
6643+ }
6644+
6645+ TEST_CASE (" trigger work is recorded by the proposal path" ,
6646+ " [herder][trigger-construction]" )
6647+ {
6648+ using namespace std ::chrono;
6649+ VirtualClock clock;
6650+ auto cfg = getTestConfig ();
6651+ cfg.HTTP_PORT = 0 ;
6652+ cfg.MANUAL_CLOSE = true ;
6653+ cfg.QUORUM_SET .threshold = 2 ;
6654+ cfg.QUORUM_SET .validators = {
6655+ cfg.NODE_SEED .getPublicKey (),
6656+ SecretKey::pseudoRandomForTesting ().getPublicKey ()};
6657+ auto app = createTestApplication (clock, cfg);
6658+ auto & herder = static_cast <HerderImpl&>(app->getHerder ());
6659+ auto & driver = herder.getHerderSCPDriver ();
6660+ auto const slot = app->getLedgerManager ().getLastClosedLedgerNum () + 1 ;
6661+ herder.mGetTopTransactionsForTesting = [&](size_t ) {
6662+ // A wall-clock adjustment must not change the measured construction
6663+ // duration or replace it with a negative interval.
6664+ clock.setCurrentVirtualTime (clock.now () + milliseconds (400 ));
6665+ clock.setSystemTimeOffset (seconds (-1 ));
6666+ return std::vector<TransactionEnvelope>{};
6667+ };
6668+ herder.triggerNextLedger (slot, true );
6669+ // Until ballot starts there is no completed interval to credit.
6670+ REQUIRE (driver.getTriggerToBallotDuration (slot) == milliseconds::zero ());
6671+ clock.setCurrentVirtualTime (clock.now () + milliseconds (100 ));
6672+ driver.recordSCPEvent (slot, false );
6673+ REQUIRE (driver.getTriggerToBallotDuration (slot) == milliseconds (500 ));
6674+ clock.setCurrentVirtualTime (clock.now () + seconds (1 ));
6675+ driver.recordNominationTrigger (slot);
6676+ driver.recordSCPEvent (slot, true );
6677+ REQUIRE (driver.getTriggerToBallotDuration (slot) == milliseconds (500 ));
6678+ herder.mGetTopTransactionsForTesting = nullptr ;
6679+ }
6680+
6681+ TEST_CASE (" trigger work excludes time after entering ballot" ,
6682+ " [herder][trigger-construction]" )
6683+ {
6684+ using namespace std ::chrono;
6685+ VirtualClock clock;
6686+ auto cfg = getTestConfig ();
6687+ cfg.HTTP_PORT = 0 ;
6688+ cfg.MANUAL_CLOSE = true ;
6689+ auto app = createTestApplication (clock, cfg);
6690+ auto & driver =
6691+ static_cast <HerderImpl&>(app->getHerder ()).getHerderSCPDriver ();
6692+ uint64_t const slot = 100 ;
6693+ driver.recordNominationTrigger (slot);
6694+ clock.setCurrentVirtualTime (clock.now () + milliseconds (100 ));
6695+ driver.recordSCPEvent (slot, false );
6696+ clock.setCurrentVirtualTime (clock.now () + milliseconds (300 ));
6697+ driver.recordSCPEvent (slot, true );
6698+ REQUIRE (driver.getTriggerToBallotDuration (slot) == milliseconds (100 ));
6699+ driver.recordSCPEvent (slot + 1 , false );
6700+ clock.setCurrentVirtualTime (clock.now () + milliseconds (100 ));
6701+ driver.recordNominationTrigger (slot + 1 );
6702+ driver.recordSCPEvent (slot + 1 , true );
6703+ REQUIRE (driver.getTriggerToBallotDuration (slot + 1 ) ==
6704+ milliseconds::zero ());
6705+ }
6706+
6707+ TEST_CASE (" trigger allowance is independent of timeout callback ordering" ,
6708+ " [herder][trigger-construction]" )
6709+ {
6710+ using namespace std ::chrono;
6711+ auto const ballotBeforeTimeout = GENERATE (false , true );
6712+ VirtualClock clock;
6713+ auto cfg = getTestConfig ();
6714+ cfg.HTTP_PORT = 0 ;
6715+ cfg.MANUAL_CLOSE = true ;
6716+ auto app = createTestApplication (clock, cfg);
6717+ auto & herder = static_cast <HerderImpl&>(app->getHerder ());
6718+ auto & driver = herder.getHerderSCPDriver ();
6719+ auto const slot = herder.nextConsensusLedgerIndex ();
6720+ auto const ct = VirtualClock::to_time_t (clock.system_now ()) + 1 ;
6721+ auto const origin = VirtualClock::from_time_t (ct);
6722+ clock.setCurrentVirtualTime (origin);
6723+ driver.recordNominationTrigger (slot);
6724+ clock.setCurrentVirtualTime (origin + milliseconds (100 ));
6725+ driver.recordSCPEvent (slot, true );
6726+ bool timerFired = false ;
6727+ driver.setupTimer (slot, Slot::NOMINATION_TIMER , seconds (2 ),
6728+ [&] { timerFired = true ; });
6729+
6730+ // Both events become runnable while the main thread is occupied. Entering
6731+ // ballot before dispatching the expired timer suppresses its timeout count.
6732+ clock.setCurrentVirtualTime (origin + milliseconds (2500 ));
6733+ if (ballotBeforeTimeout)
6734+ {
6735+ driver.recordSCPEvent (slot, false );
6736+ }
6737+ for (int i = 0 ; i < 100 && !timerFired; ++i)
6738+ {
6739+ clock.crank (false );
6740+ }
6741+ REQUIRE (timerFired);
6742+ REQUIRE (clock.system_now () == origin + milliseconds (2500 ));
6743+ if (!ballotBeforeTimeout)
6744+ {
6745+ driver.recordSCPEvent (slot, false );
6746+ }
6747+ REQUIRE (driver.getNominationTimeouts (slot) ==
6748+ (ballotBeforeTimeout ? 0 : 1 ));
6749+ REQUIRE (driver.getTriggerToBallotDuration (slot) == milliseconds (2500 ));
6750+
6751+ StellarValue value;
6752+ value.closeTime = ct;
6753+ herder.setTrackingSCPState (slot, value, true );
6754+ clock.setCurrentVirtualTime (origin + milliseconds (2700 ));
6755+ REQUIRE (EarlyNominationTestAccess::triggerAnchor (herder, slot) +
6756+ seconds (2 ) <=
6757+ clock.now ());
6758+ }
6759+
65646760TEST_CASE (" prepare nomination before trigger" , " [herder][early-nomination]" )
65656761{
65666762 auto const soroban = GENERATE (false , true );
0 commit comments