A validator builds its proposal on time. The proposal is valid. Most peers are healthy.
One outbound connection has exhausted its rate allowance.
Under the old snarkOS broadcast path, that single connection could delay every peer listed after it. The network was asynchronous, yet proposal delivery was serialized by one innocent-looking await inside a loop. Worse, sends from an obsolete consensus round could remain alive after the validator had moved on, competing with messages that still mattered.
snarkOS PR #4487 changes both behaviors. Proposal delivery now fans out across peers concurrently, and round-scoped sends are canceled when consensus advances. A slow peer keeps its own delay instead of donating it to the rest of the committee.
That is a small code change with a large effect on how latency propagates.
The queue nobody intended
Consider a validator broadcasting a BatchPropose message to four peers in this iteration order:
| Peer | Transport delay | Sequential receipt | Concurrent receipt |
|---|---|---|---|
| V1 | 2 ms | 2 ms | about 2 ms |
| V2 | 430 ms | 432 ms | about 430 ms |
| V3 | 2 ms | 434 ms | about 2 ms |
| V4 | 2 ms | 436 ms | about 2 ms |
The old shape was conceptually simple: take the first peer, await Transport::send, then continue to the next peer. V2's delay therefore became part of V3 and V4's delivery time even though their own connections were ready.
Async Rust does not make a sequential loop concurrent. Awaiting the send lets Tokio run other tasks on the worker, which is good for the runtime, but the suspended broadcast future still has its loop index parked at V2. Calls for V3 and V4 have not started.
PR #4487 identified a particularly unpleasant wait inside the transport. A send could park in the rate limiter's 10 ms polling loop without a deadline. Each poll yielded cooperatively, so no OS thread was blocked. Consensus delivery was still blocked at the application level because the broadcast loop could not move forward.
That distinction matters. Tokio starvation and proposal head-of-line blocking are different failures. Starvation means a worker cannot schedule ready work. Here the runtime may be scheduling perfectly while the broadcast algorithm refuses to create the later sends.
The title needs one qualification. A delayed peer did not literally hold every Aleo validator. It delayed every recipient behind that peer in the sending validator's local iteration order. Several proposers encountering the same pattern could spread the cost across the committee, making the effect visible as network-wide proposal latency.
Fanout changes the dependency graph
Concurrent fanout removes an accidental dependency between independent connections.
Instead of making V3 wait for V2's send future to finish, the gateway starts peer sends independently. V2 can remain inside its rate-limit wait while V1, V3, and V4 progress. The limiter still protects V2's connection. What disappears is the rule that V2 must become writable before unrelated peers may receive the proposal.
Head-of-line blocking is often discussed as a transport problem. TCP can hold later bytes behind a missing segment within one ordered stream. HTTP/2 can multiplex requests while still inheriting TCP's packet-level ordering. QUIC avoids some cross-stream coupling.
The snarkOS bug sat one layer higher. Separate peer connections were available, but the application imposed a single ordered queue across them. Changing transports would not fix a loop that awaited one destination at a time.
Concurrency also changes which latency statistic matters. Under sequential broadcast, receipt time for peer $i$ was roughly the sum of send delays through peer $i$:
$ T_i \approx \sum_{j=1}^{i} d_j $
With independent fanout, each peer's receipt time is closer to its own path delay plus scheduling overhead:
$ T_i \approx d_i + \epsilon_i $
The maximum can remain high because V2 is still slow. Median receipt and early quorum receipt can improve sharply. BFT usually cares more about enough timely participants than about forcing the slowest connection to finish first.
A naive join_all style rewrite would only solve part of the problem. Starting every future together allows early deliveries, but a caller awaiting the whole set can still remain alive until the slowest send completes. That may be acceptable for ordinary notifications. Round-bound consensus work needs a second property: an expiry condition.
A round is a lifetime boundary
Proposal work belongs to a specific round.
Before PR #4487, the initial BatchPropose broadcast and the per-non-signer resends created by propose_batch ran as detached tasks. Consensus could advance from round R to round R+1 while those tasks continued trying to deliver R's proposal. No owner told them that their useful lifetime had ended.
Detached work is attractive because the consensus path does not wait for network completion. The ownership cost arrives later. Once a task is detached, the system needs another way to answer basic questions: which round owns it, when should it stop, and how is shutdown observed?
Round-scoped cancellation supplies that ownership.
When a proposal send starts for round R, it joins work associated with R. Once consensus advances, snarkOS cancels sends from the old scope. Initial broadcasts and non-signer resends follow the same lifetime rule rather than becoming unrelated background jobs.
Cancellation matters even after fanout. Imagine V2 remains rate-limited across several round changes. Concurrent fanout protects healthy peers, but every round could leave another waiting V2 task behind. The pile grows with consensus progress. Old tasks retain futures and transport state, wake on limiter polls, and compete for queue capacity. None can help the current round.
The correct policy is blunt: proposal delivery for R loses its value when the local BFT state has advanced beyond R. Keeping the send alive spends resources on a message whose decision window has closed.
Cancellation does have tradeoffs. Dropping an in-flight future must leave transport state usable. Logs need to distinguish an expected round cancellation from a network failure. A peer that repeatedly misses proposals may require later synchronization rather than endless proposal retries. The fix does not make an unhealthy connection healthy; it limits how much damage that connection can cause elsewhere.
There is also a race at the boundary. A send may complete while cancellation fires. Correctness cannot depend on which side wins because a peer may receive the old proposal either way. Consensus already has to validate the proposal's round and reject or ignore obsolete input. Cancellation is resource control, not a new consensus rule.
Certificates had a cousin
snarkOS PR #4527 addresses the same latency shape later in the BFT flow.
After receiving and storing certificates, the gateway called back into local consensus and awaited that work before broadcasting the certificates to peers. Local commit processing therefore sat in front of network propagation. A validator could have a certificate ready for relay while every peer waited for unrelated local work to finish.
PR #4527 moves the broadcast before the awaited local BFT callback. The certificate is already stored, so propagation can begin without waiting for the local consensus path to return. No wire format or consensus rule changes.
The supplied benchmark injected a 500 ms callback delay. Median peer receipt fell from 503.6 ms to 0.15 ms after reordering the operations. Those numbers demonstrate isolation under the test setup. The PR explicitly leaves production inclusion gains unmeasured, which is the right level of caution.
PRs #4487 and #4527 share one architectural opinion: locally slow work should not sit in front of independent consensus communication. In the first case, one peer's transport wait blocked other peers. In the second, a local callback blocked every outbound certificate recipient.
The same concern appeared in the earlier analysis of telemetry in the BFT path. Metrics readers, peer rate limits, and local callbacks all have valid jobs. Consensus propagation should avoid inheriting their latency when no correctness dependency exists.
Thread count does not rescue a false dependency either. The validator thread-pool analysis covered how excess Tokio and Rayon workers can add contention. Here extra workers would leave the sequential send loop sequential. Architecture comes first; scheduler tuning follows.
What operators should test
Validator operators should treat these changes as latency isolation, not higher bandwidth. Concurrent fanout can produce a sharper burst of serialization and socket work. A committee-sized peer set is bounded, but the transport still needs per-peer backpressure and sensible queue limits.
A useful staging test can be concrete:
- Add a controlled delay or exhausted rate allowance to one validator connection.
- Broadcast a proposal to that peer plus several healthy peers.
- Confirm healthy receipt times stay near their normal baseline regardless of peer iteration order.
- Advance the consensus round while the delayed send is waiting, then confirm the old task exits and does not keep polling.
Instrumentation should expose the mechanism rather than one aggregate network average. I would measure per-peer proposal send latency, the spread between first and last healthy receipt, canceled sends labeled by round age, and the number of live proposal-send tasks. Those are suggested signals, not claims about metrics already shipped by snarkOS.
Agents managing validator fleets can use the same data to separate local faults. A high delay isolated to one peer points toward that connection's limiter, socket, or remote node. Rising send latency across healthy peers suggests local runtime pressure. A growing count of work from old rounds indicates cancellation or task ownership has regressed.
Deployment status deserves care. The supplied activity shows PR #4487 merged into staging, followed by PR #4531 to repair staging workflow failures involving unused dependencies and a lockfile version mismatch. A staging merge does not prove that a particular mainnet validator release contains the behavior. Operators should check the deployed snarkOS commit or release tag before expecting the new fanout semantics.
Concurrent delivery also changes debugging assumptions. Peer iteration order should stop predicting receipt order. Error handling becomes peer-local. Tests that expected deterministic send ordering need to assert membership and round correctness instead of sequence.
I like the design because it gives slow work an explicit boundary. Peer V2 may remain slow, and round R may still fail to collect enough support if too many validators are unhealthy. Healthy peers no longer wait merely because V2 appeared earlier in a vector, while round R's abandoned sends stop consuming time after R+1 has begun.
That is the behavior a validator needs: independent paths progress independently, and obsolete consensus work ends when its round does.