Summary
StatementHandler::send_statements_to_peer marks statements as known to a peer before the send is
attempted. If the send then fails, those statements are never re-offered to that peer for the
lifetime of the connection.
Where
substrate/client/network/statement/src/lib.rs:1468 — the mark happens inside the filter, before the
chunk is even queued:
let to_send: Vec<_> = statements
.iter()
.filter_map(|(hash, stmt)| {
if peer.known_statements.contains(hash) {
return None;
}
if peer.topic_affinity.as_ref().is_some_and(|a| !a.matches_statement(stmt)) {
return None;
}
peer.known_statements.insert(*hash); // <-- marked before the send is attempted
Some(stmt)
})
.collect();
Three failure paths then drop those statements silently:
| path |
location |
scope of loss |
message_sink() returns None |
lib.rs:1501 |
early return abandons all remaining chunks of the batch |
SendOutcome::NetworkError |
lib.rs:1544 |
the failed chunk |
SendOutcome::TimedOut |
lib.rs:1551 |
the failed chunk |
PendingSendResult (lib.rs:668) carries only statement_count: usize, not the hashes, so
handle_send_result has nothing to roll back with.
Why redelivery never happens
Two things compound:
propagate_statements (lib.rs:1585) uses StatementStore::take_recent_statements(), which
drains the recent index (client/statement-store/src/lib.rs:1724 →
query_index.write().take_recent(); store_api.rs:389: "Return recent statements and clear the
internal index"). A statement gets exactly one propagation pass, ever.
- Initial sync — the only path that re-walks the full store — is scheduled only on peer connect
(lib.rs:1186) and on explicit affinity change (lib.rs:1640). There is no periodic re-sync.
The statement itself is not lost: it stays in the store until expiry, and other peers are unaffected
(each peer gets its own send future). What is lost is delivery to this peer, until it reconnects or
changes affinity — at which point schedule_initial_sync_for_peer clears known_statements
(lib.rs:1608) and re-sends everything still unexpired.
Summary
StatementHandler::send_statements_to_peermarks statements as known to a peer before the send isattempted. If the send then fails, those statements are never re-offered to that peer for the
lifetime of the connection.
Where
substrate/client/network/statement/src/lib.rs:1468— the mark happens inside the filter, before thechunk is even queued:
Three failure paths then drop those statements silently:
message_sink()returnsNonelib.rs:1501returnabandons all remaining chunks of the batchSendOutcome::NetworkErrorlib.rs:1544SendOutcome::TimedOutlib.rs:1551PendingSendResult(lib.rs:668) carries onlystatement_count: usize, not the hashes, sohandle_send_resulthas nothing to roll back with.Why redelivery never happens
Two things compound:
propagate_statements(lib.rs:1585) usesStatementStore::take_recent_statements(), whichdrains the recent index (
client/statement-store/src/lib.rs:1724→query_index.write().take_recent();store_api.rs:389: "Return recent statements and clear theinternal index"). A statement gets exactly one propagation pass, ever.
(
lib.rs:1186) and on explicit affinity change (lib.rs:1640). There is no periodic re-sync.The statement itself is not lost: it stays in the store until expiry, and other peers are unaffected
(each peer gets its own send future). What is lost is delivery to this peer, until it reconnects or
changes affinity — at which point
schedule_initial_sync_for_peerclearsknown_statements(
lib.rs:1608) and re-sends everything still unexpired.