1pub mod actions;
29pub mod crypto;
30pub mod eth;
31pub mod rlp;
32pub mod rlpx;
33pub mod session;
34
35use crate::llm::action_helper::call_llm;
36use crate::llm::actions::protocol_trait::ActionResult;
37use crate::llm::ollama_client::OllamaClient;
38use crate::logging::emit::Log;
39use crate::protocol::{Event, EventType};
40use crate::server::connection::ConnectionId;
41use crate::state::app_state::AppState;
42use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
43use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
44use crate::state::ServerId;
45use actions::{CommonConfig, Devp2pProtocol};
46use anyhow::Result;
47use serde_json::{json, Value};
48use session::{Applied, Command, Inbound, PeerEvent, PeerEventKind, Session};
49use std::net::SocketAddr;
50use std::sync::Arc;
51use std::time::Duration;
52use tokio::sync::mpsc;
53
54const FIRST_BYTE_TIMEOUT: Duration = Duration::from_secs(10);
56pub const STATUS_TIMEOUT: Duration = Duration::from_secs(10);
58const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
60pub const MAX_FOLLOWUP_DEPTH: u32 = 8;
62const INBOUND_QUEUE: usize = 32;
64
65const BREACH: u64 = 0x02;
67const USELESS_PEER: u64 = 0x03;
68const SUBPROTOCOL_ERROR: u64 = 0x10;
69
70pub struct Devp2pServer;
71
72struct Shared {
73 config: CommonConfig,
74 app_state: Arc<AppState>,
75 llm_client: OllamaClient,
76 status_tx: mpsc::UnboundedSender<String>,
77 server_id: ServerId,
78}
79
80pub fn event_type_for(kind: PeerEventKind) -> &'static EventType {
82 match kind {
83 PeerEventKind::HeadersRequested => &actions::HEADERS_REQUESTED_EVENT,
84 PeerEventKind::BodiesRequested => &actions::BODIES_REQUESTED_EVENT,
85 PeerEventKind::PooledRequested => &actions::POOLED_REQUESTED_EVENT,
86 PeerEventKind::Announced => &actions::ANNOUNCED_EVENT,
87 PeerEventKind::HeadersReceived => &actions::HEADERS_RECEIVED_EVENT,
88 PeerEventKind::BodiesReceived => &actions::BODIES_RECEIVED_EVENT,
89 PeerEventKind::PooledReceived => &actions::POOLED_RECEIVED_EVENT,
90 PeerEventKind::RequestTimedOut => &actions::REQUEST_TIMEOUT_EVENT,
91 }
92}
93
94impl Devp2pServer {
95 pub async fn spawn(
96 listen_addr: SocketAddr,
97 llm_client: OllamaClient,
98 app_state: Arc<AppState>,
99 status_tx: mpsc::UnboundedSender<String>,
100 server_id: ServerId,
101 config: CommonConfig,
102 ) -> Result<SocketAddr> {
103 let listener =
104 crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
105 let local_addr = listener.local_addr()?;
106 let enode = eth::format_enode(&config.key.id(), local_addr);
107 Log::new(Some(&status_tx)).info(format!("devp2p node {enode}"));
108 Log::new(Some(&status_tx)).info(format!("devp2p server listening on {local_addr}"));
110 let shared = Arc::new(Shared {
111 config,
112 app_state: app_state.clone(),
113 llm_client,
114 status_tx: status_tx.clone(),
115 server_id,
116 });
117 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
118 let registrar = app_state.clone();
119 let accept = tokio::spawn(async move {
120 loop {
121 match crate::server::accept_bounded::accept_bounded(
124 &listener,
125 &limiter,
126 b"",
127 "devp2p",
128 Some(&status_tx),
129 )
130 .await
131 {
132 Ok((stream, remote, permit)) => {
133 let shared = shared.clone();
134 let local = stream.local_addr().unwrap_or(local_addr);
135 app_state
136 .spawn_server_task(server_id, async move {
137 let _permit = permit;
138 Self::handle_connection(shared, stream, remote, local).await;
139 })
140 .await;
141 }
142 Err(e) => {
143 Log::new(Some(&status_tx))
144 .error(format!("devp2p accept failed, stopping loop: {e}"));
145 break;
146 }
147 }
148 }
149 });
150 registrar.register_server_task(server_id, accept).await;
151 Ok(local_addr)
152 }
153
154 async fn handle_connection(
155 shared: Arc<Shared>,
156 stream: tokio::net::TcpStream,
157 remote: SocketAddr,
158 local: SocketAddr,
159 ) {
160 let app_state = shared.app_state.clone();
161 let server_id = shared.server_id;
162 let log = Log::new(Some(&shared.status_tx));
163 let connection_id = ConnectionId::new(app_state.get_next_unified_id().await);
164 let _ = stream.set_nodelay(true);
165 let now = crate::utils::clock::Instant::now();
166 app_state
167 .add_connection_to_server(
168 server_id,
169 ConnectionState {
170 id: connection_id,
171 remote_addr: remote,
172 local_addr: local,
173 bytes_sent: 0,
174 bytes_received: 0,
175 packets_sent: 0,
176 packets_received: 0,
177 last_activity: now,
178 status: ConnectionStatus::Active,
179 status_changed_at: now,
180 protocol_info: ProtocolConnectionInfo::empty(),
181 },
182 )
183 .await;
184 let _ = shared.status_tx.send("__UPDATE_UI__".to_string());
185
186 let (read_half, write_half) = tokio::io::split(stream);
187 let reader = crate::server::accept_bounded::IdleTimeoutReader::with_first(
188 read_half,
189 FIRST_BYTE_TIMEOUT,
190 session::IDLE_TIMEOUT,
191 );
192 match session::establish(
193 Box::new(reader),
194 Box::new(write_half),
195 &shared.config.key,
196 session::Role::Listen,
197 &shared.config.client_id,
198 local.port(),
199 )
200 .await
201 {
202 Ok(est) => {
203 log.info(format!(
204 "devp2p {connection_id}: peer {} ({}) at {remote}, eth/{}",
205 &hex::encode(est.remote_id)[..16],
206 est.hello.client_id,
207 est.eth_version
208 ));
209 let mut peer_rx = crate::server::peer_support::register_peer_channel(
210 &app_state,
211 server_id,
212 connection_id.as_u32(),
213 )
214 .await;
215 let (tx, rx) = mpsc::channel(INBOUND_QUEUE);
216 let reader_task = app_state
217 .spawn_server_task(
218 server_id,
219 session::read_loop(est.reader, est.read_half, est.writer.clone(), tx),
220 )
221 .await;
222 let writer = est.writer.clone();
223 let reason = run(
224 &shared,
225 Session::new(est.writer, est.eth_version),
226 &est.hello,
227 est.remote_id,
228 remote,
229 connection_id,
230 rx,
231 &mut peer_rx,
232 )
233 .await;
234 reader_task.abort();
235 session::close(&writer).await;
236 log.info(format!("devp2p {connection_id}: session ended: {reason}"));
237 app_state
238 .remove_peer_handle(server_id, connection_id.as_u32())
239 .await;
240 }
241 Err(session::EstablishError::Transport(e)) => {
242 log.warn(format!(
243 "devp2p {connection_id} from {remote} refused decision=protocol_error: {e}"
244 ));
245 }
246 Err(session::EstablishError::PeerDisconnected(r)) => {
247 log.info(format!(
248 "devp2p {connection_id} from {remote}: the peer disconnected during Hello ({})",
249 eth::reason_name(r)
250 ));
251 }
252 Err(e @ session::EstablishError::Refused { .. }) => {
253 log.warn(format!(
254 "devp2p {connection_id} from {remote} decision=protocol_reject: {e}"
255 ));
256 }
257 }
258 app_state
259 .remove_connection_from_server(server_id, connection_id)
260 .await;
261 let _ = shared.status_tx.send("__UPDATE_UI__".to_string());
262 }
263}
264
265#[allow(clippy::too_many_arguments)]
267async fn run(
268 shared: &Shared,
269 mut s: Session,
270 hello: ð::Hello,
271 remote_id: [u8; 64],
272 remote: SocketAddr,
273 id: ConnectionId,
274 mut rx: mpsc::Receiver<Inbound>,
275 peer_rx: &mut mpsc::Receiver<ClientCommand>,
276) -> String {
277 let log = Log::new(Some(&shared.status_tx));
278 let status = match tokio::time::timeout(STATUS_TIMEOUT, rx.recv()).await {
280 Err(_) => {
281 log.warn(format!(
282 "devp2p {id}: no Status within {}s decision=fail_closed_status_timeout",
283 STATUS_TIMEOUT.as_secs()
284 ));
285 session::disconnect(&s.writer, 0x0b).await;
286 return "no Status".into();
287 }
288 Ok(None) => return "the reader stopped".into(),
289 Ok(Some(Inbound::Ended(end))) => return ended(&log, id, end),
290 Ok(Some(Inbound::Message { code, payload })) => {
291 if code == eth::code::DISCONNECT {
292 return format!(
293 "the peer disconnected: {}",
294 eth::reason_name(eth::decode_disconnect(&payload))
295 );
296 }
297 match s.take_status(code, &payload) {
298 Ok(Some(status)) => status,
299 Ok(None) => {
300 log.warn(format!(
301 "devp2p {id}: message {code:#04x} before Status decision=protocol_error"
302 ));
303 session::disconnect(&s.writer, BREACH).await;
304 return "no Status".into();
305 }
306 Err(e) => {
307 log.warn(format!(
308 "devp2p {id}: bad Status decision=protocol_error: {e:#}"
309 ));
310 session::disconnect(&s.writer, BREACH).await;
311 return format!("bad Status: {e:#}");
312 }
313 }
314 }
315 };
316 let event = Event::new(
317 &actions::STATUS_EVENT,
318 json!({
319 "remote": remote.to_string(),
320 "client_id": hello.client_id,
321 "node_id": hex::encode(remote_id),
322 "capabilities": hello.caps_strings(),
323 "eth_version": s.eth_version,
324 "peer_status": status.to_json(),
325 "configured": shared.config.defaults.to_json(),
326 }),
327 );
328 let decision = consult(shared, id, &event).await;
329 if decision.no_answer {
330 session::disconnect(&s.writer, SUBPROTOCOL_ERROR).await;
331 return "no usable answer to eth_status".into();
332 }
333 let admits = decision
334 .commands
335 .iter()
336 .any(|c| matches!(c, Command::SendStatus(_)));
337 let refuses = decision
338 .commands
339 .iter()
340 .any(|c| matches!(c, Command::Disconnect(_)));
341 if !admits && !refuses {
342 log.info(format!(
343 "devp2p {id}: eth_status answered without eth_send_status decision=model_reject"
344 ));
345 session::disconnect(&s.writer, USELESS_PEER).await;
346 return "not admitted".into();
347 }
348 if let Some(reason) = carry_out(shared, &mut s, id, decision.commands, None, 0).await {
349 return reason;
350 }
351 if !s.ready() {
352 session::disconnect(&s.writer, SUBPROTOCOL_ERROR).await;
353 return "the Status could not be sent".into();
354 }
355 if let (Some(ours), Some(theirs)) = (&s.our_status, &s.peer_status) {
356 if let Err(why) = session::compatible(ours, theirs) {
357 log.warn(format!(
358 "devp2p {id}: this node's Status is not the peer's chain ({why}); the peer will likely disconnect"
359 ));
360 }
361 }
362
363 let mut ping = tokio::time::interval(session::PING_INTERVAL);
364 ping.tick().await;
365 let mut peer_open = true;
366 loop {
367 tokio::select! {
368 m = rx.recv() => match m {
369 None => return "the reader stopped".into(),
370 Some(Inbound::Ended(end)) => return ended(&log, id, end),
371 Some(Inbound::Message { code, payload }) => {
372 shared.app_state
373 .update_connection_stats(shared.server_id, id, Some(payload.len() as u64), None, Some(1), None)
374 .await;
375 if code == eth::code::DISCONNECT {
376 return format!("the peer disconnected: {}", eth::reason_name(eth::decode_disconnect(&payload)));
377 }
378 match s.on_message(code, &payload).await {
379 Ok(None) => {}
380 Ok(Some(ev)) => {
381 if let Some(reason) = handle(shared, &mut s, id, ev).await {
382 return reason;
383 }
384 }
385 Err(e) => {
386 log.warn(format!("devp2p {id}: decision=protocol_error: {e:#}"));
387 session::disconnect(&s.writer, BREACH).await;
388 return format!("protocol error: {e:#}");
389 }
390 }
391 }
392 },
393 _ = ping.tick() => {
394 if session::send(&s.writer, eth::code::PING, ð::empty_payload()).await.is_err() {
395 return "the connection could not be written".into();
396 }
397 for ev in s.expire() {
398 if let Some(reason) = handle(shared, &mut s, id, ev).await {
399 return reason;
400 }
401 }
402 }
403 c = peer_rx.recv(), if peer_open => {
404 let Some(c) = c else {
406 peer_open = false;
407 continue;
408 };
409 if let Some(reason) = inject(shared, &mut s, id, c).await {
410 return reason;
411 }
412 }
413 }
414 }
415}
416
417fn ended(log: &Log, id: ConnectionId, end: session::End) -> String {
418 if let Some(tag) = end.decision {
419 log.warn(format!("devp2p {id}: {} decision={tag}", end.reason));
420 }
421 end.reason
422}
423
424async fn handle(
427 shared: &Shared,
428 s: &mut Session,
429 id: ConnectionId,
430 ev: PeerEvent,
431) -> Option<String> {
432 let log = Log::new(Some(&shared.status_tx));
433 let event_type = event_type_for(ev.kind);
434 if ev.depth >= MAX_FOLLOWUP_DEPTH {
435 log.warn(format!(
436 "devp2p {id}: not asking the model about {}: {} turns deep decision=followup_depth",
437 event_type.id, ev.depth
438 ));
439 return None;
440 }
441 let event = Event::new(event_type, ev.data.clone());
442 let decision = consult(shared, id, &event).await;
443 if !decision.no_answer {
444 if let Some(reason) =
445 carry_out(shared, s, id, decision.commands, ev.request_id, ev.depth).await
446 {
447 return Some(reason);
448 }
449 }
450 if let Some(request_id) = ev.request_id {
451 if s.owes(request_id) {
452 log.warn(format!(
454 "devp2p {id}: {} {request_id} left unanswered decision=fail_closed_unanswered",
455 event_type.id
456 ));
457 session::disconnect(&s.writer, SUBPROTOCOL_ERROR).await;
458 return Some(format!("{} {request_id} unanswered", event_type.id));
459 }
460 }
461 None
462}
463
464async fn carry_out(
466 shared: &Shared,
467 s: &mut Session,
468 id: ConnectionId,
469 commands: Vec<Command>,
470 answering: Option<u64>,
471 depth: u32,
472) -> Option<String> {
473 let log = Log::new(Some(&shared.status_tx));
474 for command in commands {
475 let before = s.bytes_sent;
476 match s
477 .apply(command, answering, depth, &shared.config.defaults)
478 .await
479 {
480 Ok(Applied::Disconnected(r)) => {
481 return Some(format!("disconnected ({})", eth::reason_name(r)))
482 }
483 Ok(Applied::StatusSent(st)) => {
484 log.info(format!("devp2p {id}: Status sent: {}", st.to_json()));
485 }
486 Ok(Applied::Sent(what)) => log.info(format!("devp2p {id}: {what}")),
487 Err(e) => {
488 log.error(format!(
489 "devp2p {id}: action refused decision=fail_closed_bad_action: {e:#}"
490 ));
491 }
492 }
493 if s.bytes_sent > before {
494 shared
495 .app_state
496 .update_connection_stats(
497 shared.server_id,
498 id,
499 None,
500 Some(s.bytes_sent - before),
501 None,
502 Some(1),
503 )
504 .await;
505 }
506 }
507 None
508}
509
510async fn inject(
512 shared: &Shared,
513 s: &mut Session,
514 id: ConnectionId,
515 command: ClientCommand,
516) -> Option<String> {
517 shared
518 .app_state
519 .record_access_log(
520 crate::state::AccessLogOwner::Server(shared.server_id.as_u32()),
521 "devp2p",
522 None,
523 "injected_action",
524 command.action.clone(),
525 vec![],
526 )
527 .await;
528 let (outcome, end) = match session::parse_command(&command.action) {
529 Err(e) => (
530 ClientSendOutcome::Rejected {
531 error: format!("{e:#}"),
532 },
533 None,
534 ),
535 Ok(cmd) => match s.apply(cmd, None, 0, &shared.config.defaults).await {
536 Ok(Applied::Disconnected(r)) => (
537 ClientSendOutcome::Disconnected,
538 Some(format!(
539 "disconnected by the operator ({})",
540 eth::reason_name(r)
541 )),
542 ),
543 Ok(Applied::StatusSent(st)) => (
544 ClientSendOutcome::Executed {
545 detail: st.to_json().to_string(),
546 },
547 None,
548 ),
549 Ok(Applied::Sent(what)) => (ClientSendOutcome::Executed { detail: what }, None),
550 Err(e) => (
551 ClientSendOutcome::Rejected {
552 error: format!("{e:#}"),
553 },
554 None,
555 ),
556 },
557 };
558 Log::new(Some(&shared.status_tx)).info(format!("devp2p {id}: injected action: {outcome:?}"));
559 let _ = command.reply_tx.send(Ok(outcome));
560 end
561}
562
563struct Decision {
565 commands: Vec<Command>,
566 no_answer: bool,
568}
569
570async fn consult(shared: &Shared, id: ConnectionId, event: &Event) -> Decision {
572 let log = Log::new(Some(&shared.status_tx));
573 let name = &event.event_type.id;
574 let execution = match call_llm(
575 &shared.llm_client,
576 &shared.app_state,
577 shared.server_id,
578 Some(id),
579 event,
580 &Devp2pProtocol::new(),
581 )
582 .await
583 {
584 Ok(x) => x,
585 Err(e) => {
586 let tag = match crate::utils::wire_failure::WireFailure::classify(&e) {
587 crate::utils::wire_failure::WireFailure::Overloaded => "fail_closed_llm_overloaded",
588 crate::utils::wire_failure::WireFailure::Unavailable => "fail_closed_llm_error",
589 };
590 log.error(format!(
591 "devp2p {id}: {name} not answered decision={tag}: {e}"
592 ));
593 return Decision {
594 commands: Vec::new(),
595 no_answer: true,
596 };
597 }
598 };
599 for m in execution.messages {
600 let _ = shared.status_tx.send(m);
601 }
602 let mut actions: Vec<Value> = Vec::new();
603 let mut stack = execution.protocol_results;
604 stack.reverse();
605 while let Some(r) = stack.pop() {
606 match r {
607 ActionResult::Custom { data, .. } => actions.push(data),
608 ActionResult::Multiple(inner) => stack.extend(inner.into_iter().rev()),
609 _ => {}
610 }
611 }
612 let commands: Vec<Command> = actions
613 .iter()
614 .filter_map(|a| session::parse_command(a).ok())
615 .collect();
616 let refused: Vec<String> = execution
617 .failures
618 .iter()
619 .map(|f| format!("{}: {}", f.action, f.error))
620 .collect();
621 let no_answer = commands.is_empty();
622 if commands.is_empty() && refused.is_empty() {
623 log.warn(format!(
624 "devp2p {id}: {name} produced no action decision=model_silent"
625 ));
626 } else if commands.is_empty() {
627 log.error(format!(
628 "devp2p {id}: {name} produced no usable action decision=fail_closed_bad_action ({})",
629 refused.join("; ")
630 ));
631 } else if commands.iter().any(|c| matches!(c, Command::Disconnect(_))) {
632 log.info(format!("devp2p {id}: {name} decision=model_reject"));
633 } else {
634 if !refused.is_empty() {
635 log.warn(format!(
636 "devp2p {id}: {name}: some actions refused: {}",
637 refused.join("; ")
638 ));
639 }
640 log.info(format!("devp2p {id}: {name} decision=model_answer"));
641 }
642 Decision {
643 commands,
644 no_answer,
645 }
646}