You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Context: Building an application-level authorization gate (HMAC-based pairing) where both sides refuse to act on incoming RPCs until a challenge/response handshake completes. The spec called for a 4-message handshake (Challenge → ResponseAndChallenge → Response → Ack) driven from the event loop on incoming RPC events, with each side reacting to the sender's PeerId.
Problem: Two stacked gaps in v0.2.1 force a redesign:
NetworkEvent::RpcIncomingMessageHandled { data: RpcData } is declared in prelude.rs:486 but nothing in core/mod.rs ever pushes it onto the event queue (confirmed by full-source grep — only GossipsubIncomingMessageHandled has an emit site). Apps cannot react to incoming RPC via the normal node.next_event() loop at all.
The only path the library gives you for incoming RPC is the rpc_handler_fn: fn(RpcData) -> RpcData registered via CoreBuilder::with_rpc. It is a bare fn pointer (so no captured app state — same issue as (example PR closing issues) Dev to main #8 above), it is synchronous (cannot .await, cannot take a tokio::sync::Mutex), and it receives no PeerId for the caller. The response is whatever bytes the fn returns, sent back on the same RPC channel; there is no other hook.
Consequence for the app: The 4-message event-driven handshake is not expressible on SwarmNL v0.2.1. The protocol was simplified to a single round-trip Challenge(nonce) → Response(hmac) per direction, with each side independently initiating against the other on ConnectionEstablished. Mutual authentication still holds in aggregate (each side proves knowledge of S to the other via its own challenge), at the cost of losing the combined ResponseAndChallenge + Ack flow.
Consequence for the data-plane gate: The app's three is_trusted(&peer) checks become "two enforceable + one best-effort". The two initiator-side checks (before sending DataPing, after receiving DataPong as the RPC response) are checkable. The handler-side check on incoming DataPing cannot see the caller, so it falls back to a "single connected peer" assumption cached from ConnectionEstablished and consulted via a static OnceLock<Arc<HandlerCtx>> — clean for a two-node demo, but does not generalise. Any production app that wants a per-peer receive-side gate is blocked on this.
Suggestion: Either (a) actually emit RpcIncomingMessageHandled { data, peer } from the request-response branch in core/mod.rs so apps can handle RPC in the normal async event loop, or (b) change with_rpc to accept Arc<dyn Fn(PeerId, RpcData) -> BoxFuture<'static, RpcData> + Send + Sync> so handlers can take state and see the caller and do async work. A narrower composable version is a per-peer delivery-filter hook: one closure (PeerId, AppData) -> Deliver | Drop | Disconnect registered on the Node, usable for pairing, rate limiting, allowlists, mute lists, and paid-tier gating.
Workaround: static HANDLER_CTX: OnceLock<Arc<HandlerCtx>> holding the shared secret, a std::sync::Mutex-backed PairingBook (not tokio::sync::Mutex, so the sync handler can lock it without blocking the runtime), and an Option<PeerId> cache of the one currently-connected peer, set from ConnectionEstablished. Document clearly that the handler-side gate only works because n=2.
Severity: blocking (for the 4-message protocol) → important (after the protocol simplification)
Context: Building an application-level authorization gate (HMAC-based pairing) where both sides refuse to act on incoming RPCs until a challenge/response handshake completes. The spec called for a 4-message handshake (
Challenge → ResponseAndChallenge → Response → Ack) driven from the event loop on incoming RPC events, with each side reacting to the sender'sPeerId.Problem: Two stacked gaps in v0.2.1 force a redesign:
NetworkEvent::RpcIncomingMessageHandled { data: RpcData }is declared inprelude.rs:486but nothing incore/mod.rsever pushes it onto the event queue (confirmed by full-source grep — onlyGossipsubIncomingMessageHandledhas an emit site). Apps cannot react to incoming RPC via the normalnode.next_event()loop at all.rpc_handler_fn: fn(RpcData) -> RpcDataregistered viaCoreBuilder::with_rpc. It is a bare fn pointer (so no captured app state — same issue as (example PR closing issues) Dev to main #8 above), it is synchronous (cannot.await, cannot take atokio::sync::Mutex), and it receives noPeerIdfor the caller. The response is whatever bytes the fn returns, sent back on the same RPC channel; there is no other hook.Consequence for the app: The 4-message event-driven handshake is not expressible on SwarmNL v0.2.1. The protocol was simplified to a single round-trip
Challenge(nonce) → Response(hmac)per direction, with each side independently initiating against the other onConnectionEstablished. Mutual authentication still holds in aggregate (each side proves knowledge ofSto the other via its own challenge), at the cost of losing the combinedResponseAndChallenge + Ackflow.Consequence for the data-plane gate: The app's three
is_trusted(&peer)checks become "two enforceable + one best-effort". The two initiator-side checks (before sendingDataPing, after receivingDataPongas the RPC response) are checkable. The handler-side check on incomingDataPingcannot see the caller, so it falls back to a "single connected peer" assumption cached fromConnectionEstablishedand consulted via astatic OnceLock<Arc<HandlerCtx>>— clean for a two-node demo, but does not generalise. Any production app that wants a per-peer receive-side gate is blocked on this.Suggestion: Either (a) actually emit
RpcIncomingMessageHandled { data, peer }from the request-response branch incore/mod.rsso apps can handle RPC in the normal async event loop, or (b) changewith_rpcto acceptArc<dyn Fn(PeerId, RpcData) -> BoxFuture<'static, RpcData> + Send + Sync>so handlers can take state and see the caller and do async work. A narrower composable version is a per-peer delivery-filter hook: one closure(PeerId, AppData) -> Deliver | Drop | Disconnectregistered on theNode, usable for pairing, rate limiting, allowlists, mute lists, and paid-tier gating.Workaround:
static HANDLER_CTX: OnceLock<Arc<HandlerCtx>>holding the shared secret, astd::sync::Mutex-backedPairingBook(nottokio::sync::Mutex, so the sync handler can lock it without blocking the runtime), and anOption<PeerId>cache of the one currently-connected peer, set fromConnectionEstablished. Document clearly that the handler-side gate only works because n=2.Severity: blocking (for the 4-message protocol) → important (after the protocol simplification)
Relevant API:
NetworkEvent::RpcIncomingMessageHandled,CoreBuilder::with_rpc,RpcConfig,AppData::SendRpcOriginally logged while building
apps/paired-exchangeinforge-p2p. Seelibrary-feedback.mdfor the full index of findings.See also: #53, #54 — related RPC handler / event gaps.