1use super::{
2 Config,
3 ingress::{Mailbox, Message},
4};
5use crate::p2p::{Handler, Monitor};
6use commonware_actor::mailbox;
7use commonware_codec::Codec;
8use commonware_cryptography::{Committable, Digestible, PublicKey};
9use commonware_macros::select_loop;
10use commonware_p2p::{Blocker, Receiver, Recipients, Sender, utils::codec::wrap};
11use commonware_runtime::{
12 BufferPooler, Clock, ContextCell, Handle, Metrics, Spawner, spawn_cell,
13 telemetry::metrics::{Counter, Gauge, GaugeExt, MetricsExt as _},
14};
15use commonware_utils::{channel::oneshot, futures::Pool};
16use std::collections::{HashMap, HashSet};
17use tracing::{debug, error};
18
19pub struct Engine<E, B, Rq, Rs, P, M, H>
21where
22 E: BufferPooler + Clock + Spawner,
23 P: PublicKey,
24 B: Blocker<PublicKey = P>,
25 Rq: Committable + Digestible + Codec,
26 Rs: Committable<Commitment = Rq::Commitment> + Digestible<Digest = Rq::Digest> + Codec,
27 M: Monitor<Response = Rs, PublicKey = P>,
28 H: Handler<Request = Rq, Response = Rs, PublicKey = P>,
29{
30 context: ContextCell<E>,
32 blocker: B,
33 priority_request: bool,
34 request_codec: Rq::Cfg,
35 priority_response: bool,
36 response_codec: Rs::Cfg,
37
38 monitor: M,
40 handler: H,
41 mailbox: mailbox::Receiver<Message<P, Rq>>,
42
43 tracked: HashMap<Rq::Commitment, (HashSet<P>, HashSet<P>)>,
45
46 outstanding: Gauge,
48 requests: Counter,
49 responses: Counter,
50}
51
52impl<E, B, Rq, Rs, P, M, H> Engine<E, B, Rq, Rs, P, M, H>
53where
54 E: BufferPooler + Clock + Spawner + Metrics,
55 P: PublicKey,
56 B: Blocker<PublicKey = P>,
57 Rq: Committable + Digestible + Codec,
58 Rs: Committable<Commitment = Rq::Commitment> + Digestible<Digest = Rq::Digest> + Codec,
59 M: Monitor<Response = Rs, PublicKey = P>,
60 H: Handler<Request = Rq, Response = Rs, PublicKey = P>,
61{
62 pub fn new(context: E, cfg: Config<B, M, H, Rq::Cfg, Rs::Cfg>) -> (Self, Mailbox<P, Rq>) {
66 let (tx, rx) = mailbox::new(context.child("mailbox"), cfg.mailbox_size);
68 let mailbox: Mailbox<P, Rq> = Mailbox::new(tx);
69
70 let outstanding = context.gauge("outstanding", "outstanding commitments");
72 let requests = context.counter("requests", "processed requests");
73 let responses = context.counter("responses", "sent responses");
74
75 (
76 Self {
77 context: ContextCell::new(context),
78 blocker: cfg.blocker,
79 priority_request: cfg.priority_request,
80 request_codec: cfg.request_codec,
81 priority_response: cfg.priority_response,
82 response_codec: cfg.response_codec,
83 monitor: cfg.monitor,
84 handler: cfg.handler,
85 mailbox: rx,
86 tracked: HashMap::new(),
87 outstanding,
88 requests,
89 responses,
90 },
91 mailbox,
92 )
93 }
94
95 pub fn start(
99 mut self,
100 requests: (impl Sender<PublicKey = P>, impl Receiver<PublicKey = P>),
101 responses: (impl Sender<PublicKey = P>, impl Receiver<PublicKey = P>),
102 ) -> Handle<()> {
103 spawn_cell!(self.context, self.run(requests, responses))
104 }
105
106 async fn run(
107 mut self,
108 requests: (impl Sender<PublicKey = P>, impl Receiver<PublicKey = P>),
109 responses: (impl Sender<PublicKey = P>, impl Receiver<PublicKey = P>),
110 ) {
111 let (mut req_tx, mut req_rx) = wrap(
113 self.request_codec,
114 self.context.network_buffer_pool().clone(),
115 requests.0,
116 requests.1,
117 );
118 let (mut res_tx, mut res_rx) = wrap(
119 self.response_codec,
120 self.context.network_buffer_pool().clone(),
121 responses.0,
122 responses.1,
123 );
124
125 let mut processed: Pool<'_, Result<(P, Rs), oneshot::error::RecvError>> = Pool::default();
127 select_loop! {
128 self.context,
129 on_stopped => {
130 debug!("context shutdown, stopping engine");
131 },
132 Some(command) = self.mailbox.recv() else continue => match command {
134 Message::Send {
135 request,
136 recipients,
137 } => {
138 let commitment = request.commitment();
139 let recipients = req_tx.send(recipients, request, self.priority_request);
140 if !recipients.is_empty() {
141 let entry = self.tracked.entry(commitment).or_insert_with(|| {
142 self.outstanding.inc();
143 (HashSet::new(), HashSet::new())
144 });
145 entry.0.extend(recipients);
146 }
147 }
148 Message::Cancel { commitment } => {
149 if self.tracked.remove(&commitment).is_none() {
150 debug!(?commitment, "ignoring removal of unknown commitment");
151 }
152 let _ = self.outstanding.try_set(self.tracked.len());
153 }
154 },
155
156 Ok((peer, reply)) = processed.next_completed() else continue => {
158 self.responses.inc();
159
160 let _ = res_tx.send(Recipients::One(peer), reply, self.priority_response);
162 },
163
164 message = req_rx.recv() => {
166 self.requests.inc();
167
168 let (peer, msg) = match message {
170 Ok(r) => r,
171 Err(err) => {
172 error!(?err, "request receiver failed");
173 break;
174 }
175 };
176 let msg = match msg {
177 Ok(msg) => msg,
178 Err(err) => {
179 commonware_p2p::block!(self.blocker, peer, ?err, "invalid request");
180 continue;
181 }
182 };
183
184 let (tx, rx) = oneshot::channel();
186 self.handler.process(peer.clone(), msg, tx);
187 processed.push(async move { Ok((peer, rx.await?)) });
188 },
189
190 response = res_rx.recv() => {
192 let (peer, msg) = match response {
194 Ok(r) => r,
195 Err(err) => {
196 error!(?err, "response receiver failed");
197 break;
198 }
199 };
200 let msg = match msg {
201 Ok(msg) => msg,
202 Err(err) => {
203 commonware_p2p::block!(self.blocker, peer, ?err, "invalid response");
204 continue;
205 }
206 };
207
208 let commitment = msg.commitment();
210 let Some(responses) = self.tracked.get_mut(&commitment) else {
211 debug!(?commitment, ?peer, "response for unknown commitment");
212 continue;
213 };
214 if !responses.0.contains(&peer) {
215 debug!(?commitment, ?peer, "never sent request");
216 continue;
217 }
218 if !responses.1.insert(peer.clone()) {
219 debug!(?commitment, ?peer, "duplicate response");
220 continue;
221 }
222
223 self.monitor.collected(peer, msg, responses.1.len());
225 },
226 }
227 }
228}