P2P controller hardening#60
Open
kalabukdima wants to merge 9 commits into
Open
Conversation
kalabukdima
force-pushed
the
net-stream-server
branch
from
July 9, 2026 06:52
6f6fe83 to
e85cfa3
Compare
kalabukdima
marked this pull request as ready for review
July 9, 2026 06:52
kalabukdima
force-pushed
the
net-stream-server
branch
from
July 9, 2026 14:12
e85cfa3 to
0ed08cd
Compare
Extract the compute-unit accounting (`process_query`) and log building (`generate_log`) into free functions generic over two dependency traits, `QueryRunner` and `CuChecker`, and add mock implementations. This lets the query pipeline be unit-tested without standing up the transport, storage, or a real query engine. Tests cover the current behavior: an overloaded engine is fully refunded, malformed params are rejected before the CU spend, partial chunks refund the unused fraction, and no-allocation queries are not logged.
Bump sqd-network to the stream_server server behaviour (libp2p-stream instead of request_response): responses are written to the stream directly, so the send_* handle methods are now async. On a full processing queue the worker now returns a signed too_many_requests error instead of dropping the stream, which had left the client with an opaque transport error. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The transport now hands over raw request bytes and takes raw response bytes. Decode Query/LogsRequest in the event loop (off the swarm loop) and encode responses via ResponseSender::send directly, replacing the removed WorkerTransportHandle::send_* methods. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The log entry a worker stores (and gets rewarded on) was built independently of the response the client received, so an oversized or unsignable result was sent as a `server_error` while still being logged as a successful query. Make the compute-unit spend the single boundary: a query is logged iff it consumed a CU, and the log is a projection of the response that was actually sent. - Move admission (verify, reserve slot, spend one CU) into the event loop; reject pre-admission queries with signed typed errors and a backoff hint instead of silently dropping the stream - Build the wire response and its log from one outcome in `build_delivery`, so an oversized/unsignable result downgrades to `server_error` in both - Charge and log malformed params and engine overload; only unprovable or unbudgeted rejects (bad signature/timestamp, no allocation, rate limit) stay out of the log store, which bounds a log-flood DoS - Bound concurrent reject responses with a semaphore
- Replace the run_all! macro and hand-wired cancellation token tree with a subsystem tree that drains gracefully and cancels stragglers after a 5s timeout instead of hanging forever - Leak the controller for &'static access, removing the Arc clone per spawned task - Report a loop that dies on its own as a named subsystem error instead of a silent all-Ok shutdown - Pin the dependency to an exact version; the audited tarball hash is recorded in Cargo.lock Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
kalabukdima
force-pushed
the
net-stream-server
branch
from
July 15, 2026 15:34
bd6ae0f to
6d29ad0
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #25
Transport layer
Replace
request-responsebehaviour with a much simpler and more flexiblestreambehaviour. Benefits:See subsquid/sqd-network#211
Handling backpressure
Now there is a synchronous event loop pulling events (stream requests) from the transport layer. It quickly checks if the client is allowed to send the request and spawns a processing tokio task or sends the rejection to the client. If this loop can't keep up with the incoming requests, extra requests will get dropped before being parsed.
This is not perfect because it doesn't propagate the backpressure to cut the early handshake. The full incoming stream is still read to memory. But making it perfect requires a significant reorg of the libp2p stack and isn't directly supported by existing libp2p behaviours.
Graceful rejections
In many cases the stream was dropped without sending any bytes. The client just saw a "connection reset" which was difficult to diagnose. Try to return a meaningful rejection now. Only drop the stream if there are too many in-flight rejections or the response couldn't be written.
query_idto sign)BadRequest("invalid query signature")BadRequest("timestamp out of allowed range")ServerOverloaded+ backoffTooManyRequestsTooManyRequests+ retryblock_range)BadRequestBadRequestOkServerError("failed to sign query result")ServerErrorServerError("query result too large")ServerErrorOk(delivered) ¹NotFoundNotFoundServiceOverloaded)ServerOverloaded+ backoffServerOverloadedOther)ServerErrorServerError¹ The old transport had an internal send queue that could reject a perfectly good result.
stream_server'sResponseSenderwrites to the stream directly, so that queue no longer exists; the result is delivered (a reset now only happens if the client itself is gone or the write times out).²
chip= the query's overlap fraction with the chunk (1 for a full-chunk query); the flat1.0is claimed at admission and the unused part refunded, so a served query's charge ischip.