We pace all responses to their requests
Found some open issues in the P2P implementation here, so ended up fully generalizing the design pattern across all channel types. Client-server interfaces have a pretty clear request-response vs. event-notify pattern, and r/r is guarded by not reading the next off the socket until the previous is fully sent. The e/n patter requires cost accounting, as otherwise a slow counterparty can force buffer overflow.
On p2p there wasn't a clear distinction between r/r and e/n, so I created one, which is controlled by the protocol invoking notify(message) (vs send(message)) in the cases where the message originates from an event, not a request. P2P is more complex than client/server given that multiple protocols may subscribe to and handle the same request on a given channel. This is because p2p mixes inventory types (e.g. tx and block) in one message, and similar issues, which makes it harder to isolate protocol behavior. So the request is only complete after all subscribers have released the message.
So both the notification distinction and the concurrent handling are now generalized to p2p, which closes the DoS gap there. This also coincides with the low level network code subsuming the monitor() function into this framework. A monitor is created on a channel when the message handler leaves the strand (e.g. to do potentially parallel and long-running tasks). This arms the socket to monitor for a counterparty drop, which then propagates cancellation. So this is now automatic in the r/r cycle. When a method departs the strand it explicitly requests a handle, which arms the monitor, and is automatically disarmed when the handle destructs.
So overall less code, simpler to get right, even less to manage in the protocol, correct/consistent across interfaces, and flexible enough to handle complex scenarios.