Skip to content

Commit 1a9b367

Browse files
committed
Fix more sub-second ledger edge cases
1 parent 1067bb6 commit 1a9b367

9 files changed

Lines changed: 154 additions & 12 deletions

File tree

src/herder/LedgerCloseData.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ stellarValueToString(Config const& c, StellarValue const& sv)
8181
}
8282
res << " txH: " << hexAbbrev(sv.txSetHash) << ", ct: " << sv.closeTime;
8383
#ifdef MS_CLOSE_TIME
84-
if (getCloseTimeMs(sv) != 0 || isMsCloseTimeStellarValue(sv))
84+
if (isMsCloseTimeStellarValue(sv))
8585
{
8686
res << ", ctMs: " << getCloseTimeMs(sv);
8787
}

src/overlay/Peer.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1422,8 +1422,10 @@ bool
14221422
Peer::process(QueryInfo& queryInfo, std::optional<uint32_t> maxQueriesPerWindow)
14231423
{
14241424
auto const& cfg = mAppConnector.getConfig();
1425+
// Round up so sub-second close times can't produce a zero-length window
1426+
// (which would zero out QUERIES_PER_WINDOW and reject every query).
14251427
std::chrono::seconds const QUERY_WINDOW =
1426-
std::chrono::duration_cast<std::chrono::seconds>(
1428+
std::chrono::ceil<std::chrono::seconds>(
14271429
mAppConnector.getLedgerManager().getExpectedLedgerCloseTime() *
14281430
cfg.MAX_SLOTS_TO_REMEMBER);
14291431
uint32_t const QUERIES_PER_WINDOW = maxQueriesPerWindow.value_or(

src/overlay/Peer.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,13 @@ class Peer : public std::enable_shared_from_this<Peer>,
508508
releaseAssert(threadIsMain());
509509
return mSCPStateQueryInfo.mNumQueries;
510510
}
511+
512+
// Testing only function to expose the query rate-limiting check
513+
bool
514+
processQueryForTesting(QueryInfo& queryInfo)
515+
{
516+
return process(queryInfo);
517+
}
511518
#endif
512519

513520
// Public thread-safe methods that access Peer's state

src/overlay/test/OverlayTests.cpp

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2097,6 +2097,52 @@ TEST_CASE("GET_SCP_STATE rate limiting", "[overlay]")
20972097
testutil::shutdownWorkScheduler(*app1);
20982098
}
20992099

2100+
TEST_CASE("query rate limit window under sub-second close times", "[overlay]")
2101+
{
2102+
VirtualClock clock;
2103+
Config cfg1 = getTestConfig(0);
2104+
Config cfg2 = getTestConfig(1);
2105+
2106+
// Make the raw query window (close time * slots to remember) shorter than
2107+
// one second. It must round up to a one-second window; truncating instead
2108+
// would produce a zero-length window with a zero query allowance,
2109+
// rejecting every query.
2110+
for (auto* cfg : {&cfg1, &cfg2})
2111+
{
2112+
cfg->ARTIFICIALLY_SET_CLOSE_TIME_FOR_TESTING = 100;
2113+
cfg->MAX_SLOTS_TO_REMEMBER = 4;
2114+
}
2115+
2116+
auto app1 = createTestApplication(clock, cfg1);
2117+
auto app2 = createTestApplication(clock, cfg2);
2118+
2119+
LoopbackPeerConnection conn(*app1, *app2);
2120+
testutil::crankSome(clock);
2121+
auto peer = conn.getAcceptor();
2122+
REQUIRE(peer->isAuthenticatedForTesting());
2123+
2124+
// The rounded-up 1s window admits QUERY_RESPONSE_MULTIPLIER (see Peer.cpp)
2125+
// queries per window
2126+
uint32_t constexpr EXPECTED_QUERIES_PER_WINDOW = 5;
2127+
2128+
Peer::QueryInfo queryInfo;
2129+
for (uint32_t i = 0; i < EXPECTED_QUERIES_PER_WINDOW; i++)
2130+
{
2131+
REQUIRE(peer->processQueryForTesting(queryInfo));
2132+
queryInfo.mNumQueries++;
2133+
}
2134+
// The next query in the same window exceeds the allowance
2135+
REQUIRE(!peer->processQueryForTesting(queryInfo));
2136+
2137+
// Advancing past the rounded-up window resets the allowance
2138+
testutil::crankFor(clock, std::chrono::seconds(2));
2139+
REQUIRE(peer->processQueryForTesting(queryInfo));
2140+
REQUIRE(queryInfo.mNumQueries == 0);
2141+
2142+
testutil::shutdownWorkScheduler(*app2);
2143+
testutil::shutdownWorkScheduler(*app1);
2144+
}
2145+
21002146
TEST_CASE("reject peers with the same nodeid", "[overlay][connections]")
21012147
{
21022148
VirtualClock clock;

src/simulation/LoadGenerator.cpp

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -502,12 +502,15 @@ LoadGenerator::scheduleLoadGeneration(GeneratedLoadConfig cfg)
502502
}
503503

504504
// During load submission, we must have enough unique source accounts (with
505-
// a buffer) to accommodate the desired tx rate.
506-
auto closeTimeSeconds = std::chrono::duration_cast<std::chrono::seconds>(
507-
mApp.getLedgerManager().getExpectedLedgerCloseTime());
508-
if (cfg.nTxs > cfg.nAccounts && (cfg.txRate * closeTimeSeconds.count()) *
509-
MIN_UNIQUE_ACCOUNT_MULTIPLIER >
510-
cfg.nAccounts)
505+
// a buffer) to accommodate the desired tx rate. txRate is per second, so
506+
// (txRate * closeTime) is the per-ledger tx count; round it up so
507+
// sub-second close times don't truncate it to 0 and disable the check.
508+
auto const closeTime = mApp.getLedgerManager().getExpectedLedgerCloseTime();
509+
auto const txsPerLedger = std::chrono::ceil<std::chrono::seconds>(
510+
closeTime * static_cast<int64_t>(cfg.txRate))
511+
.count();
512+
if (cfg.nTxs > cfg.nAccounts &&
513+
txsPerLedger * MIN_UNIQUE_ACCOUNT_MULTIPLIER > cfg.nAccounts)
511514
{
512515
errorMsg = fmt::format(
513516
"Tx rate is too high, there are not enough unique accounts. Make "

src/transactions/TransactionUtils.cpp

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1387,10 +1387,12 @@ getUpperBoundCloseTimeOffset(Application& app, uint64_t lastCloseTime)
13871387
uint64_t closeTimeDrift =
13881388
currentTime <= lastCloseTime ? 0 : currentTime - lastCloseTime;
13891389

1390-
return std::chrono::duration_cast<std::chrono::seconds>(
1391-
app.getLedgerManager().getExpectedLedgerCloseTime())
1392-
.count() *
1393-
EXPECTED_CLOSE_TIME_MULT +
1390+
// Round up so sub-second expected close times still leave at least a one
1391+
// second buffer (time bounds are whole-second quantities).
1392+
return std::chrono::ceil<std::chrono::seconds>(
1393+
app.getLedgerManager().getExpectedLedgerCloseTime() *
1394+
EXPECTED_CLOSE_TIME_MULT)
1395+
.count() +
13941396
closeTimeDrift;
13951397
}
13961398

src/transactions/test/TxEnvelopeTests.cpp

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3241,3 +3241,79 @@ TEST_CASE("transaction time bounds under sub-second ledgers", "[tx][envelope]")
32413241
}
32423242
}
32433243
#endif // MS_CLOSE_TIME
3244+
3245+
TEST_CASE("getUpperBoundCloseTimeOffset under sub-second ledgers",
3246+
"[tx][envelope]")
3247+
{
3248+
// The offset is consumed at transaction queue admission: a tx whose
3249+
// maxTime falls inside [lclCloseTime, lclCloseTime + offset) is rejected
3250+
// as txTOO_LATE by Herder::recvTransaction, since it would likely be
3251+
// expired by the time it lands in a ledger. Exercise that path end to end
3252+
// and assert exactly where the admission boundary sits.
3253+
auto testWithCloseTimeMs = [](uint32_t closeTimeMs,
3254+
uint64_t expectedBuffer) {
3255+
VirtualClock clock;
3256+
auto cfg = getTestConfig();
3257+
cfg.ARTIFICIALLY_SET_CLOSE_TIME_FOR_TESTING = closeTimeMs;
3258+
auto app = createTestApplication(clock, cfg);
3259+
auto& lm = app->getLedgerManager();
3260+
auto root = app->getRoot();
3261+
3262+
// Close a ledger at a known whole second T, then align the clock with
3263+
// it so the offset's drift term starts at zero
3264+
TimePoint const T =
3265+
VirtualClock::to_time_t(app->getClock().system_now()) + 1000;
3266+
closeLedgerOn(*app, lm.getLastClosedLedgerNum() + 1, T);
3267+
3268+
// The queue admits one pending tx per source account, so give every
3269+
// submission its own account
3270+
int accountIndex = 0;
3271+
auto submitWithMaxTime = [&](TimePoint maxTime) {
3272+
auto acc =
3273+
root->create("sub-second-" + std::to_string(accountIndex++),
3274+
lm.getLastMinBalance(0) + 10000);
3275+
auto tx = acc.tx({payment(*root, 1)});
3276+
setMaxTime(tx, maxTime);
3277+
getSignatures(tx).clear();
3278+
tx->addSignature(acc.getSecretKey());
3279+
return app->getHerder().recvTransaction(tx, true);
3280+
};
3281+
auto expectTooLate = [&](TimePoint maxTime) {
3282+
auto r = submitWithMaxTime(maxTime);
3283+
REQUIRE(r.code ==
3284+
TransactionQueue::AddResultCode::ADD_STATUS_ERROR);
3285+
REQUIRE(r.txResult->getResultCode() == txTOO_LATE);
3286+
};
3287+
auto expectAdmitted = [&](TimePoint maxTime) {
3288+
REQUIRE(submitWithMaxTime(maxTime).code ==
3289+
TransactionQueue::AddResultCode::ADD_STATUS_PENDING);
3290+
};
3291+
3292+
clock.setCurrentVirtualTime(VirtualClock::from_time_t(T));
3293+
3294+
// With no drift, the first admissible maxTime is T + buffer
3295+
expectTooLate(T + expectedBuffer - 1);
3296+
expectAdmitted(T + expectedBuffer);
3297+
3298+
// Wall-clock time elapsed since the last close is added on top
3299+
clock.setCurrentVirtualTime(VirtualClock::from_time_t(T + 7));
3300+
expectTooLate(T + expectedBuffer + 7 - 1);
3301+
expectAdmitted(T + expectedBuffer + 7);
3302+
};
3303+
3304+
SECTION("whole-second close times keep the legacy 2x buffer")
3305+
{
3306+
testWithCloseTimeMs(5000, 10);
3307+
}
3308+
SECTION("sub-second close times round the buffer up to a whole second")
3309+
{
3310+
// 2 * 400ms rounds up to 1s rather than truncating to 0, which would
3311+
// erase the admission-time expiry buffer entirely
3312+
testWithCloseTimeMs(400, 1);
3313+
}
3314+
SECTION("fractional-second close times round up, not down")
3315+
{
3316+
// 2 * 1300ms = 2600ms rounds up to 3s
3317+
testWithCloseTimeMs(1300, 3);
3318+
}
3319+
}

test-tx-meta-baseline-current/TxEnvelopeTests.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
27,
3636
28
3737
],
38+
"getUpperBoundCloseTimeOffset under sub-second ledgers|fractional-second close times round up, not down" : [ "EbIPrt4owbA=", "kAwIMT7FxME=", "+8eXWHGepSo=", "Pk+9/t2Sw1Q=" ],
39+
"getUpperBoundCloseTimeOffset under sub-second ledgers|sub-second close times round the buffer up to a whole second" : [ "EbIPrt4owbA=", "kAwIMT7FxME=", "+8eXWHGepSo=", "Pk+9/t2Sw1Q=" ],
40+
"getUpperBoundCloseTimeOffset under sub-second ledgers|whole-second close times keep the legacy 2x buffer" : [ "EbIPrt4owbA=", "kAwIMT7FxME=", "+8eXWHGepSo=", "Pk+9/t2Sw1Q=" ],
3841
"overlay validation handles ed25519 signed payload signers|protocol version 19" :
3942
[
4043
"bXrfFM/EOrA=",

test-tx-meta-baseline-next/TxEnvelopeTests.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
28,
3737
29
3838
],
39+
"getUpperBoundCloseTimeOffset under sub-second ledgers|fractional-second close times round up, not down" : [ "EbIPrt4owbA=", "kAwIMT7FxME=", "+8eXWHGepSo=", "Pk+9/t2Sw1Q=" ],
40+
"getUpperBoundCloseTimeOffset under sub-second ledgers|sub-second close times round the buffer up to a whole second" : [ "EbIPrt4owbA=", "kAwIMT7FxME=", "+8eXWHGepSo=", "Pk+9/t2Sw1Q=" ],
41+
"getUpperBoundCloseTimeOffset under sub-second ledgers|whole-second close times keep the legacy 2x buffer" : [ "EbIPrt4owbA=", "kAwIMT7FxME=", "+8eXWHGepSo=", "Pk+9/t2Sw1Q=" ],
3942
"overlay validation handles ed25519 signed payload signers|protocol version 19" :
4043
[
4144
"bXrfFM/EOrA=",

0 commit comments

Comments
 (0)