Skip to content

Commit 1d7e5d7

Browse files
Merge pull request #451 from stellar/get-scp-state-fix
Rate limit `GET_SCP_STATE` messages
2 parents 90fa6e4 + f85b82f commit 1d7e5d7

3 files changed

Lines changed: 111 additions & 4 deletions

File tree

src/overlay/Peer.cpp

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ using namespace soci;
5656

5757
namespace
5858
{
59+
// Maximum number of GET_SCP_STATE requests per window per peer to respond to. A
60+
// window defaults to roughly 1 minute.
61+
constexpr uint32_t GET_SCP_STATE_MAX_RATE = 10;
62+
5963
// Check the signature(s) in `tx`, adding the result to the signature cache in
6064
// the process. This function requires that background signature verification
6165
// is enabled and the current thread is the overlay thread.
@@ -1416,15 +1420,15 @@ Peer::recvDontHave(StellarMessage const& msg)
14161420
}
14171421

14181422
bool
1419-
Peer::process(QueryInfo& queryInfo)
1423+
Peer::process(QueryInfo& queryInfo, std::optional<uint32_t> maxQueriesPerWindow)
14201424
{
14211425
auto const& cfg = mAppConnector.getConfig();
14221426
std::chrono::seconds const QUERY_WINDOW =
14231427
std::chrono::duration_cast<std::chrono::seconds>(
14241428
mAppConnector.getLedgerManager().getExpectedLedgerCloseTime() *
14251429
cfg.MAX_SLOTS_TO_REMEMBER);
1426-
uint32_t const QUERIES_PER_WINDOW =
1427-
QUERY_WINDOW.count() * QUERY_RESPONSE_MULTIPLIER;
1430+
uint32_t const QUERIES_PER_WINDOW = maxQueriesPerWindow.value_or(
1431+
QUERY_WINDOW.count() * QUERY_RESPONSE_MULTIPLIER);
14281432
if (mAppConnector.now() - queryInfo.mLastTimeStamp >= QUERY_WINDOW)
14291433
{
14301434
queryInfo.mLastTimeStamp = mAppConnector.now();
@@ -1679,6 +1683,13 @@ Peer::recvGetSCPState(StellarMessage const& msg)
16791683
{
16801684
ZoneScoped;
16811685
releaseAssert(threadIsMain());
1686+
if (!process(mSCPStateQueryInfo, GET_SCP_STATE_MAX_RATE))
1687+
{
1688+
CLOG_DEBUG(Overlay, "Dropping GET_SCP_STATE request from {}",
1689+
KeyUtils::toShortString(mPeerID));
1690+
return;
1691+
}
1692+
mSCPStateQueryInfo.mNumQueries++;
16821693
uint32 seq = msg.getSCPLedgerSeq();
16831694
mAppConnector.getHerder().sendSCPStateToPeer(seq, shared_from_this());
16841695
}

src/overlay/Peer.h

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ class Peer : public std::enable_shared_from_this<Peer>,
276276
std::shared_ptr<TxAdverts> mTxAdverts;
277277
QueryInfo mQSetQueryInfo;
278278
QueryInfo mTxSetQueryInfo;
279+
QueryInfo mSCPStateQueryInfo;
279280
bool mPeersReceived{false};
280281

281282
static Hash pingIDfromTimePoint(VirtualClock::time_point const& tp);
@@ -319,7 +320,17 @@ class Peer : public std::enable_shared_from_this<Peer>,
319320
void sendDontHave(MessageType type, uint256 const& itemID);
320321
void sendPeers();
321322
void sendError(ErrorCode error, std::string const& message);
322-
bool process(QueryInfo& queryInfo);
323+
324+
// Returns `true` if a query is within the configured limits and should be
325+
// processed, `false` if it exceeds limits and should be dropped. This
326+
// function may reset the per-window state in `queryInfo` when a new
327+
// window begins, but it does not increment any query counters; callers
328+
// are responsible for updating `queryInfo` (for example, incrementing the
329+
// number of processed queries) after a query is accepted. Callers may
330+
// optionally set `maxQueriesPerWindow` to override the default per-window
331+
// query limit.
332+
bool process(QueryInfo& queryInfo,
333+
std::optional<uint32_t> maxQueriesPerWindow = std::nullopt);
323334

324335
void recvMessage(std::shared_ptr<CapacityTrackedMessage> msgTracker);
325336

@@ -492,6 +503,13 @@ class Peer : public std::enable_shared_from_this<Peer>,
492503
static void
493504
populateSignatureCacheForTesting(AppConnector& app,
494505
TransactionFrameBaseConstPtr tx);
506+
507+
uint32_t
508+
getSCPStateQueryCountForTesting() const
509+
{
510+
releaseAssert(threadIsMain());
511+
return mSCPStateQueryInfo.mNumQueries;
512+
}
495513
#endif
496514

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

src/overlay/test/OverlayTests.cpp

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1843,6 +1843,84 @@ TEST_CASE("drop peers who straggle", "[overlay][connections][straggler]")
18431843
}
18441844
}
18451845

1846+
TEST_CASE("GET_SCP_STATE rate limiting", "[overlay]")
1847+
{
1848+
VirtualClock clock;
1849+
Config cfg1 = getTestConfig(0);
1850+
Config cfg2 = getTestConfig(1);
1851+
1852+
// Bump up close time and max slots to remember to production levels. These
1853+
// must be large enough that crankSome calls between requests don't cause a
1854+
// window reset.
1855+
cfg1.ARTIFICIALLY_SET_CLOSE_TIME_FOR_TESTING = 5;
1856+
cfg2.ARTIFICIALLY_SET_CLOSE_TIME_FOR_TESTING = 5;
1857+
cfg1.MAX_SLOTS_TO_REMEMBER = 12;
1858+
cfg2.MAX_SLOTS_TO_REMEMBER = 12;
1859+
1860+
// The window size + 1 second. Minimum time required to ensure the rate
1861+
// limit window resets.
1862+
std::chrono::seconds const WINDOW_CLEAR_DURATION(
1863+
cfg1.ARTIFICIALLY_SET_CLOSE_TIME_FOR_TESTING *
1864+
cfg1.MAX_SLOTS_TO_REMEMBER +
1865+
1);
1866+
1867+
// Should be no more than 10 processed GET_SCP_STATE messages per window
1868+
uint32_t constexpr MAX_PER_WINDOW = 10;
1869+
1870+
auto app1 = createTestApplication(clock, cfg1);
1871+
auto app2 = createTestApplication(clock, cfg2);
1872+
1873+
LoopbackPeerConnection conn(*app1, *app2);
1874+
auto sender = conn.getInitiator();
1875+
auto receiver = conn.getAcceptor();
1876+
testutil::crankSome(clock);
1877+
REQUIRE(conn.getInitiator()->isAuthenticatedForTesting());
1878+
REQUIRE(conn.getAcceptor()->isAuthenticatedForTesting());
1879+
1880+
// Advance past QUERY_WINDOW so the first test request triggers a window
1881+
// reset
1882+
testutil::crankFor(clock, WINDOW_CLEAR_DURATION);
1883+
1884+
// Send requests up to the limit.
1885+
for (int i = 0; i < MAX_PER_WINDOW; i++)
1886+
{
1887+
sender->sendGetScpState(0);
1888+
testutil::crankSome(clock);
1889+
}
1890+
1891+
// Should have logged 10 queries, and the peers should remain connected
1892+
REQUIRE(receiver->getSCPStateQueryCountForTesting() == MAX_PER_WINDOW);
1893+
1894+
// Send 5 more -- all should be dropped
1895+
for (int i = 0; i < 5; i++)
1896+
{
1897+
sender->sendGetScpState(0);
1898+
testutil::crankSome(clock);
1899+
}
1900+
// Should still be at the maximum query count, as the additional messages
1901+
// should have been dropped
1902+
REQUIRE(receiver->getSCPStateQueryCountForTesting() == MAX_PER_WINDOW);
1903+
1904+
// Advance past the window duration so the next request triggers a window
1905+
// reset
1906+
testutil::crankFor(clock, WINDOW_CLEAR_DURATION);
1907+
1908+
// Send a GET_SCP_STATE message to trigger the window reset
1909+
sender->sendGetScpState(0);
1910+
testutil::crankSome(clock);
1911+
1912+
// Should just have processed the one message sent after the window clear
1913+
// duration
1914+
REQUIRE(receiver->getSCPStateQueryCountForTesting() == 1);
1915+
1916+
// Peers should still be connected
1917+
REQUIRE(sender->isConnectedForTesting());
1918+
REQUIRE(receiver->isConnectedForTesting());
1919+
1920+
testutil::shutdownWorkScheduler(*app2);
1921+
testutil::shutdownWorkScheduler(*app1);
1922+
}
1923+
18461924
TEST_CASE("reject peers with the same nodeid", "[overlay][connections]")
18471925
{
18481926
VirtualClock clock;

0 commit comments

Comments
 (0)