1pub mod actions;
18
19use crate::client::llm_budget::call_llm_for_client;
20use crate::logging::emit::Log;
21use crate::protocol::{ConnectContext, Event, EventType};
22use crate::server::devp2p::actions::{common_config, CommonConfig};
23use crate::server::devp2p::session::{
24 self, Applied, Command, Inbound, PeerEvent, PeerEventKind, Session,
25};
26use crate::server::devp2p::{crypto, eth};
27use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
28use crate::state::{AccessLogOwner, ClientStatus};
29pub use actions::Devp2pClientProtocol;
30use anyhow::{Context, Result};
31use serde_json::json;
32use std::net::SocketAddr;
33use std::time::Duration;
34use tokio::sync::mpsc;
35
36pub const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
38pub const STATUS_WAIT: Duration = Duration::from_millis(1500);
40pub const MAX_FOLLOWUP_DEPTH: u32 = 8;
42const INBOUND_QUEUE: usize = 32;
43
44const USELESS_PEER: u64 = 0x03;
45const BREACH: u64 = 0x02;
46const SUBPROTOCOL_ERROR: u64 = 0x10;
47
48fn event_type_for(kind: PeerEventKind) -> &'static EventType {
49 match kind {
50 PeerEventKind::HeadersRequested => &actions::HEADERS_REQUESTED_EVENT,
51 PeerEventKind::BodiesRequested => &actions::BODIES_REQUESTED_EVENT,
52 PeerEventKind::PooledRequested => &actions::POOLED_REQUESTED_EVENT,
53 PeerEventKind::Announced => &actions::ANNOUNCED_EVENT,
54 PeerEventKind::HeadersReceived => &actions::HEADERS_RECEIVED_EVENT,
55 PeerEventKind::BodiesReceived => &actions::BODIES_RECEIVED_EVENT,
56 PeerEventKind::PooledReceived => &actions::POOLED_RECEIVED_EVENT,
57 PeerEventKind::RequestTimedOut => &actions::REQUEST_TIMEOUT_EVENT,
58 }
59}
60
61pub async fn connect(ctx: ConnectContext) -> Result<SocketAddr> {
62 let config = common_config(ctx.startup_params.as_ref())?;
63 let (node_id, target) = eth::parse_enode(&ctx.remote_addr)?;
64 let remote_key = crypto::public_from_id(&node_id)?;
65 let server = tokio::net::lookup_host(&target)
66 .await
67 .with_context(|| format!("cannot resolve {target}"))?
68 .next()
69 .with_context(|| format!("{target} resolved to no address"))?;
70 let stream = tokio::time::timeout(CONNECT_TIMEOUT, tokio::net::TcpStream::connect(server))
71 .await
72 .with_context(|| format!("connecting to {server} timed out"))?
73 .with_context(|| format!("cannot reach {server}"))?;
74 let _ = stream.set_nodelay(true);
75 let (r, w) = stream.into_split();
76 let r = crate::server::accept_bounded::IdleTimeoutReader::new(r, session::IDLE_TIMEOUT);
77 let est = session::establish(
78 Box::new(r),
79 Box::new(w),
80 &config.key,
81 session::Role::Dial(remote_key),
82 &config.client_id,
83 0,
84 )
85 .await
86 .map_err(|e| anyhow::anyhow!("devp2p session with {server}: {e}"))?;
87 ctx.state
88 .update_client_status(ctx.client_id, ClientStatus::Connected)
89 .await;
90 Log::new(Some(&ctx.status_tx)).info(format!(
91 "devp2p client {} connected to {} ({}) at {server}, eth/{}, as {}",
92 ctx.client_id,
93 &hex::encode(est.remote_id)[..16],
94 est.hello.client_id,
95 est.eth_version,
96 eth::format_enode(&config.key.id(), server)
97 .split('@')
98 .next()
99 .unwrap_or_default()
100 ));
101 let external =
102 crate::client::command_support::register_command_channel(&ctx.state, ctx.client_id).await;
103 let (tx, rx) = mpsc::channel(INBOUND_QUEUE);
104 let reader = tokio::spawn(session::read_loop(
105 est.reader,
106 est.read_half,
107 est.writer.clone(),
108 tx,
109 ));
110 let reader_abort = reader.abort_handle();
111 ctx.state.register_client_task(ctx.client_id, reader).await;
112 let session_ctx = ctx.clone();
113 let task = tokio::spawn(async move {
114 let mut client = Devp2pClient {
115 ctx: session_ctx.clone(),
116 config,
117 session: Session::new(est.writer, est.eth_version),
118 server,
119 };
120 let reason = client.run(&est.hello, est.remote_id, rx, external).await;
121 reader_abort.abort();
122 session::close(&client.session.writer).await;
123 Log::new(Some(&session_ctx.status_tx)).info(format!(
124 "devp2p client {} disconnected: {reason}",
125 session_ctx.client_id
126 ));
127 client
128 .ask(
129 &Event::new(&actions::DISCONNECTED_EVENT, json!({"reason": reason})),
130 0,
131 )
132 .await;
133 session_ctx
134 .state
135 .update_client_status(session_ctx.client_id, ClientStatus::Disconnected)
136 .await;
137 session_ctx
138 .state
139 .remove_client_handle(session_ctx.client_id)
140 .await;
141 let _ = session_ctx.status_tx.send("__UPDATE_UI__".into());
142 });
143 ctx.state.register_client_task(ctx.client_id, task).await;
144 Ok(server)
145}
146
147struct Devp2pClient {
148 ctx: ConnectContext,
149 config: CommonConfig,
150 session: Session,
151 server: SocketAddr,
152}
153
154type Answer = Option<Vec<(Command, String)>>;
156
157impl Devp2pClient {
158 fn log(&self) -> Log<'_> {
159 Log::new(Some(&self.ctx.status_tx))
160 }
161
162 async fn ask_raw(
166 &mut self,
167 event: &Event,
168 ) -> Option<(Vec<(Command, String)>, Vec<(String, String)>)> {
169 let instruction = self
170 .ctx
171 .state
172 .get_instruction_for_client(self.ctx.client_id)
173 .await
174 .unwrap_or_default();
175 let memory = self
176 .ctx
177 .state
178 .get_memory_for_client(self.ctx.client_id)
179 .await
180 .unwrap_or_default();
181 let name = event.id().to_string();
182 let result = match call_llm_for_client(
183 &self.ctx.llm_client,
184 &self.ctx.state,
185 self.ctx.client_id.to_string(),
186 &instruction,
187 &memory,
188 Some(event),
189 &Devp2pClientProtocol,
190 &self.ctx.status_tx,
191 )
192 .await
193 {
194 Ok(r) => r,
195 Err(e) => {
196 let tag = match crate::utils::wire_failure::WireFailure::classify(&e) {
197 crate::utils::wire_failure::WireFailure::Overloaded => {
198 "fail_closed_llm_overloaded"
199 }
200 crate::utils::wire_failure::WireFailure::Unavailable => "fail_closed_llm_error",
201 };
202 self.log().error(format!(
203 "devp2p client {}: {name} not answered decision={tag}: {e}",
204 self.ctx.client_id
205 ));
206 return None;
207 }
208 };
209 if let Some(memory) = result.memory_updates {
210 self.ctx
211 .state
212 .set_memory_for_client(self.ctx.client_id, memory)
213 .await;
214 }
215 let mut commands = Vec::new();
216 let mut refused = Vec::new();
217 for action in result.actions {
218 let kind = action["type"].as_str().unwrap_or_default().to_string();
219 match session::parse_command(&action) {
220 Ok(c) => commands.push((c, kind)),
221 Err(e) => {
222 self.log().error(format!(
223 "devp2p client {}: {kind} refused decision=fail_closed_bad_action: {e:#}",
224 self.ctx.client_id
225 ));
226 refused.push((kind, format!("{e:#}")));
227 }
228 }
229 }
230 Some((commands, refused))
231 }
232
233 async fn ask(&mut self, event: &Event, depth: u32) -> Answer {
235 let (commands, refused) = self.ask_raw(event).await?;
236 for (kind, message) in refused {
237 self.error_event(&kind, &message, depth).await;
238 }
239 Some(commands)
240 }
241
242 async fn error_event(&mut self, action: &str, message: &str, depth: u32) {
245 if depth + 1 >= MAX_FOLLOWUP_DEPTH {
246 return;
247 }
248 let event = Event::new(
249 &actions::ERROR_EVENT,
250 json!({"action": action, "message": message}),
251 );
252 let Some((commands, _)) = self.ask_raw(&event).await else {
253 return;
254 };
255 for (c, kind) in commands {
256 if let Err(e) = self.apply(c, None, depth + 1).await {
257 self.log().warn(format!(
258 "devp2p client {}: {kind} after an error also failed: {e:#}",
259 self.ctx.client_id
260 ));
261 }
262 }
263 }
264
265 async fn apply(
266 &mut self,
267 command: Command,
268 answering: Option<u64>,
269 depth: u32,
270 ) -> Result<Applied> {
271 let applied = self
272 .session
273 .apply(command, answering, depth, &self.config.defaults)
274 .await?;
275 match &applied {
276 Applied::Sent(what) => self
277 .log()
278 .info(format!("devp2p client {}: {what}", self.ctx.client_id)),
279 Applied::StatusSent(st) => self.log().info(format!(
280 "devp2p client {}: Status sent: {}",
281 self.ctx.client_id,
282 st.to_json()
283 )),
284 Applied::Disconnected(_) => {}
285 }
286 Ok(applied)
287 }
288
289 async fn carry_out(
292 &mut self,
293 commands: Vec<(Command, String)>,
294 answering: Option<u64>,
295 depth: u32,
296 ) -> std::result::Result<Vec<(Command, String)>, String> {
297 let mut deferred = Vec::new();
298 for (command, kind) in commands {
299 let waits = !self.session.ready()
300 && !matches!(command, Command::SendStatus(_) | Command::Disconnect(_));
301 if waits {
302 deferred.push((command, kind));
303 continue;
304 }
305 match self.apply(command, answering, depth).await {
306 Ok(Applied::Disconnected(r)) => {
307 return Err(format!("disconnected ({})", eth::reason_name(r)))
308 }
309 Ok(_) => {}
310 Err(e) => {
311 self.log().error(format!(
312 "devp2p client {}: {kind} refused decision=fail_closed_bad_action: {e:#}",
313 self.ctx.client_id
314 ));
315 self.error_event(&kind, &format!("{e:#}"), depth).await;
316 }
317 }
318 }
319 Ok(deferred)
320 }
321
322 async fn read_status(
324 &mut self,
325 rx: &mut mpsc::Receiver<Inbound>,
326 wait: Duration,
327 ) -> std::result::Result<Option<eth::Status>, String> {
328 let m = match tokio::time::timeout(wait, rx.recv()).await {
329 Err(_) => return Ok(None),
330 Ok(m) => m,
331 };
332 match m {
333 None => Err("the reader stopped".into()),
334 Some(Inbound::Ended(end)) => Err(self.ended(end)),
335 Some(Inbound::Message { code, payload }) => {
336 if code == eth::code::DISCONNECT {
337 return Err(format!(
338 "the peer disconnected: {}",
339 eth::reason_name(eth::decode_disconnect(&payload))
340 ));
341 }
342 match self.session.take_status(code, &payload) {
343 Ok(Some(s)) => Ok(Some(s)),
344 Ok(None) => {
345 self.log().warn(format!(
346 "devp2p client {}: message {code:#04x} before Status decision=protocol_error",
347 self.ctx.client_id
348 ));
349 session::disconnect(&self.session.writer, BREACH).await;
350 Err("the peer sent a message before its Status".into())
351 }
352 Err(e) => {
353 self.log().warn(format!(
354 "devp2p client {}: bad Status decision=protocol_error: {e:#}",
355 self.ctx.client_id
356 ));
357 session::disconnect(&self.session.writer, BREACH).await;
358 Err(format!("bad Status: {e:#}"))
359 }
360 }
361 }
362 }
363 }
364
365 fn ended(&self, end: session::End) -> String {
366 if let Some(tag) = end.decision {
367 self.log().warn(format!(
368 "devp2p client {}: {} decision={tag}",
369 self.ctx.client_id, end.reason
370 ));
371 }
372 end.reason
373 }
374
375 async fn run(
376 &mut self,
377 hello: ð::Hello,
378 remote_id: [u8; 64],
379 mut rx: mpsc::Receiver<Inbound>,
380 mut external: mpsc::Receiver<ClientCommand>,
381 ) -> String {
382 let id = self.ctx.client_id;
383 let peer_status = match self.read_status(&mut rx, STATUS_WAIT).await {
384 Ok(s) => s,
385 Err(reason) => return reason,
386 };
387 let event = Event::new(
388 &actions::STATUS_EVENT,
389 json!({
390 "remote": self.server.to_string(),
391 "client_id": hello.client_id,
392 "node_id": hex::encode(remote_id),
393 "capabilities": hello.caps_strings(),
394 "eth_version": self.session.eth_version,
395 "peer_status": peer_status.as_ref().map(eth::Status::to_json),
396 "configured": self.config.defaults.to_json(),
397 }),
398 );
399 let Some(commands) = self.ask(&event, 0).await else {
400 session::disconnect(&self.session.writer, SUBPROTOCOL_ERROR).await;
401 return "no usable answer to eth_status".into();
402 };
403 let admits = commands
404 .iter()
405 .any(|(c, _)| matches!(c, Command::SendStatus(_)));
406 let refuses = commands
407 .iter()
408 .any(|(c, _)| matches!(c, Command::Disconnect(_)));
409 if commands.is_empty() {
410 self.log().warn(format!(
411 "devp2p client {id}: eth_status produced no action decision=model_silent"
412 ));
413 session::disconnect(&self.session.writer, SUBPROTOCOL_ERROR).await;
414 return "no answer to eth_status".into();
415 }
416 if !admits && !refuses {
417 self.log().info(format!(
418 "devp2p client {id}: eth_status answered without eth_send_status decision=model_reject"
419 ));
420 session::disconnect(&self.session.writer, USELESS_PEER).await;
421 return "the model did not send a Status".into();
422 }
423 self.log().info(format!(
424 "devp2p client {id}: eth_status decision={}",
425 if refuses {
426 "model_reject"
427 } else {
428 "model_answer"
429 }
430 ));
431 let deferred = match self.carry_out(commands, None, 0).await {
432 Ok(d) => d,
433 Err(reason) => return reason,
434 };
435 if self.session.our_status.is_none() {
436 session::disconnect(&self.session.writer, SUBPROTOCOL_ERROR).await;
437 return "the Status could not be sent".into();
438 }
439 if self.session.peer_status.is_none() {
440 match self
441 .read_status(&mut rx, crate::server::devp2p::STATUS_TIMEOUT)
442 .await
443 {
444 Ok(Some(_)) => {}
445 Ok(None) => {
446 self.log().warn(format!(
447 "devp2p client {id}: the peer sent no Status decision=fail_closed_status_timeout"
448 ));
449 session::disconnect(&self.session.writer, 0x0b).await;
450 return "the peer sent no Status".into();
451 }
452 Err(reason) => return reason,
453 }
454 }
455 if let (Some(ours), Some(theirs)) = (&self.session.our_status, &self.session.peer_status) {
456 match session::compatible(ours, theirs) {
457 Ok(()) => self.log().info(format!(
458 "devp2p client {id}: eth handshake done with {}",
459 theirs.to_json()
460 )),
461 Err(why) => self.log().warn(format!(
462 "devp2p client {id}: this node's Status is not the peer's chain ({why}); the peer will likely disconnect"
463 )),
464 }
465 }
466 if let Err(reason) = self.carry_out(deferred, None, 0).await {
467 return reason;
468 }
469
470 let mut ping = tokio::time::interval(session::PING_INTERVAL);
471 ping.tick().await;
472 loop {
473 tokio::select! {
474 m = rx.recv() => match m {
475 None => return "the reader stopped".into(),
476 Some(Inbound::Ended(end)) => return self.ended(end),
477 Some(Inbound::Message { code, payload }) => {
478 if code == eth::code::DISCONNECT {
479 return format!("the peer disconnected: {}", eth::reason_name(eth::decode_disconnect(&payload)));
480 }
481 match self.session.on_message(code, &payload).await {
482 Ok(None) => {}
483 Ok(Some(ev)) => {
484 if let Some(reason) = self.handle(ev).await {
485 return reason;
486 }
487 }
488 Err(e) => {
489 self.log().warn(format!("devp2p client {id}: decision=protocol_error: {e:#}"));
490 session::disconnect(&self.session.writer, BREACH).await;
491 return format!("protocol error: {e:#}");
492 }
493 }
494 }
495 },
496 _ = ping.tick() => {
497 if session::send(&self.session.writer, eth::code::PING, ð::empty_payload()).await.is_err() {
498 return "the connection could not be written".into();
499 }
500 for ev in self.session.expire() {
501 if let Some(reason) = self.handle(ev).await {
502 return reason;
503 }
504 }
505 }
506 c = external.recv() => match c {
507 None => {
508 session::disconnect(&self.session.writer, 0x08).await;
509 return "the client was stopped".into();
510 }
511 Some(c) => {
512 if let Some(reason) = self.inject(c).await {
513 return reason;
514 }
515 }
516 },
517 }
518 }
519 }
520
521 async fn handle(&mut self, ev: PeerEvent) -> Option<String> {
523 let event_type = event_type_for(ev.kind);
524 if ev.depth >= MAX_FOLLOWUP_DEPTH {
525 self.log().warn(format!(
526 "devp2p client {}: not asking the model about {}: {} turns deep decision=followup_depth",
527 self.ctx.client_id, event_type.id, ev.depth
528 ));
529 } else {
530 let event = Event::new(event_type, ev.data.clone());
531 if let Some(commands) = self.ask(&event, ev.depth).await {
532 if commands.is_empty() {
533 self.log().info(format!(
534 "devp2p client {}: {} decision=model_silent",
535 self.ctx.client_id, event_type.id
536 ));
537 }
538 if let Err(reason) = self.carry_out(commands, ev.request_id, ev.depth).await {
539 return Some(reason);
540 }
541 }
542 }
543 if let Some(request_id) = ev.request_id {
544 if self.session.owes(request_id) {
545 self.log().warn(format!(
546 "devp2p client {}: {} {request_id} left unanswered decision=fail_closed_unanswered",
547 self.ctx.client_id, event_type.id
548 ));
549 session::disconnect(&self.session.writer, SUBPROTOCOL_ERROR).await;
550 return Some(format!("{} {request_id} unanswered", event_type.id));
551 }
552 }
553 None
554 }
555
556 async fn inject(&mut self, command: ClientCommand) -> Option<String> {
558 self.ctx
559 .state
560 .record_access_log(
561 AccessLogOwner::Client(self.ctx.client_id.as_u32()),
562 "devp2p",
563 None,
564 "injected_action",
565 command.action.clone(),
566 vec![],
567 )
568 .await;
569 let (outcome, end) = match session::parse_command(&command.action) {
570 Err(e) => (
571 ClientSendOutcome::Rejected {
572 error: format!("{e:#}"),
573 },
574 None,
575 ),
576 Ok(cmd) => match self.apply(cmd, None, 0).await {
577 Ok(Applied::Disconnected(r)) => (
578 ClientSendOutcome::Disconnected,
579 Some(format!("disconnected ({})", eth::reason_name(r))),
580 ),
581 Ok(Applied::StatusSent(st)) => (
582 ClientSendOutcome::Executed {
583 detail: st.to_json().to_string(),
584 },
585 None,
586 ),
587 Ok(Applied::Sent(what)) => (ClientSendOutcome::Executed { detail: what }, None),
588 Err(e) => (
589 ClientSendOutcome::Rejected {
590 error: format!("{e:#}"),
591 },
592 None,
593 ),
594 },
595 };
596 crate::client::command_support::reply(command, Ok(outcome));
597 end
598 }
599}