1use crate::llm::actions::{
9 client_trait::{Client, ClientActionResult},
10 protocol_trait::Protocol,
11 ActionDefinition, ParameterDefinition, StartupExamples,
12};
13use crate::protocol::log_template::LogTemplate;
14use crate::protocol::{ConnectContext, EventType};
15use crate::server::devp2p::actions as shared;
16use crate::server::devp2p::actions::p;
17use crate::server::devp2p::session;
18use crate::state::app_state::AppState;
19use anyhow::Result;
20use serde_json::{json, Value};
21use std::sync::LazyLock;
22
23#[derive(Default)]
24pub struct Devp2pClientProtocol;
25
26impl Devp2pClientProtocol {
27 pub fn new() -> Self {
28 Self
29 }
30}
31
32pub static STATUS_EVENT: LazyLock<EventType> = LazyLock::new(|| {
33 EventType::new(
34 "eth_status",
35 "The RLPx handshake and Hello are done and this node must send its eth Status. \
36 peer_status is the peer's own Status, or null when the peer is waiting for ours. Send \
37 eth_send_status (requests in the same answer go out once both Statuses are \
38 exchanged) or devp2p_disconnect",
39 json!({"type": session::SEND_STATUS, "mirror_peer": true}),
40 )
41 .with_parameters(vec![
42 p("remote", "string", "The peer's address", true),
43 p("client_id", "string", "The client id from its Hello", true),
44 p(
45 "node_id",
46 "string",
47 "Its node id (128 hex characters)",
48 true,
49 ),
50 p("capabilities", "array", "Capabilities from its Hello", true),
51 p(
52 "eth_version",
53 "number",
54 "The eth version both sides speak",
55 true,
56 ),
57 p(
58 "peer_status",
59 "object",
60 "Its Status, or null if it has not sent one yet",
61 true,
62 ),
63 p(
64 "configured",
65 "object",
66 "The Status values configured at startup",
67 true,
68 ),
69 ])
70 .with_actions(shared::all_actions())
71 .with_alternative_example(json!({"type": session::DISCONNECT, "reason": "useless_peer"}))
72 .with_log_template(
73 LogTemplate::new()
74 .with_info("devp2p connected to {client_id} at {remote}, eth/{eth_version}")
75 .with_trace("devp2p status: {json_pretty(.)}"),
76 )
77});
78
79pub static HEADERS_REQUESTED_EVENT: LazyLock<EventType> =
80 LazyLock::new(|| shared::HEADERS_REQUESTED_EVENT.clone());
81pub static BODIES_REQUESTED_EVENT: LazyLock<EventType> =
82 LazyLock::new(|| shared::BODIES_REQUESTED_EVENT.clone());
83pub static POOLED_REQUESTED_EVENT: LazyLock<EventType> =
84 LazyLock::new(|| shared::POOLED_REQUESTED_EVENT.clone());
85pub static ANNOUNCED_EVENT: LazyLock<EventType> = LazyLock::new(|| shared::ANNOUNCED_EVENT.clone());
86pub static HEADERS_RECEIVED_EVENT: LazyLock<EventType> =
87 LazyLock::new(|| shared::HEADERS_RECEIVED_EVENT.clone());
88pub static BODIES_RECEIVED_EVENT: LazyLock<EventType> =
89 LazyLock::new(|| shared::BODIES_RECEIVED_EVENT.clone());
90pub static POOLED_RECEIVED_EVENT: LazyLock<EventType> =
91 LazyLock::new(|| shared::POOLED_RECEIVED_EVENT.clone());
92pub static REQUEST_TIMEOUT_EVENT: LazyLock<EventType> =
93 LazyLock::new(|| shared::REQUEST_TIMEOUT_EVENT.clone());
94
95pub static ERROR_EVENT: LazyLock<EventType> = LazyLock::new(|| {
96 EventType::new(
97 "devp2p_error",
98 "An action could not be carried out; nothing was sent for it",
99 json!({"type": session::REQUEST_HEADERS, "origin": 0}),
100 )
101 .with_parameters(vec![
102 p("action", "string", "The action's type", true),
103 p("message", "string", "Why it was refused", true),
104 ])
105 .with_actions(shared::all_actions())
106 .with_log_template(LogTemplate::new().with_info("devp2p {action} refused: {message}"))
107});
108
109pub static DISCONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
110 EventType::new(
111 "devp2p_disconnected",
112 "The session ended. Nothing more can be sent on it",
113 json!({"type": "show_message", "message": "devp2p session ended"}),
114 )
115 .with_parameters(vec![p(
116 "reason",
117 "string",
118 "Why the session ended, and which side ended it",
119 true,
120 )])
121 .with_no_actions()
122 .with_log_template(LogTemplate::new().with_info("devp2p session ended: {reason}"))
123});
124
125impl Protocol for Devp2pClientProtocol {
126 fn protocol_name(&self) -> &'static str {
127 "devp2p"
128 }
129 fn stack_name(&self) -> &'static str {
130 "ETH>IP>TCP>RLPx>devp2p"
131 }
132 fn keywords(&self) -> Vec<&'static str> {
133 vec![
134 "devp2p",
135 "ethereum",
136 "eth/68",
137 "eth/69",
138 "rlpx",
139 "enode",
140 "geth",
141 "ethereum client",
142 ]
143 }
144 fn description(&self) -> &'static str {
145 "Ethereum devp2p dialler: connects to a node by enode URL over RLPx, exchanges Hello and an eth Status the LLM chooses, and fetches block headers, bodies and pooled transactions with hashes computed locally"
146 }
147 fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
148 shared::all_actions()
149 }
150 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
151 vec![]
152 }
153 fn get_event_types(&self) -> Vec<EventType> {
154 vec![
155 STATUS_EVENT.clone(),
156 HEADERS_REQUESTED_EVENT.clone(),
157 BODIES_REQUESTED_EVENT.clone(),
158 POOLED_REQUESTED_EVENT.clone(),
159 ANNOUNCED_EVENT.clone(),
160 HEADERS_RECEIVED_EVENT.clone(),
161 BODIES_RECEIVED_EVENT.clone(),
162 POOLED_RECEIVED_EVENT.clone(),
163 REQUEST_TIMEOUT_EVENT.clone(),
164 ERROR_EVENT.clone(),
165 DISCONNECTED_EVENT.clone(),
166 ]
167 }
168 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
169 shared::common_startup_parameters()
170 }
171 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
172 use crate::protocol::metadata::*;
173 ProtocolMetadataV2::builder()
174 .state(DevelopmentState::Experimental)
175 .privilege_requirement(PrivilegeRequirement::None)
176 .implementation("The server's hand-written RLPx (EIP-8 handshake as the initiator, AES-256-CTR frames, keccak MACs, Snappy), RLP codec, Hello and eth/68 and eth/69, dialled to an enode URL. Every header received is decoded and hashed here; a response is checked against the request it answers")
177 .llm_control("This node's Status (eth_status, with the peer's Status when it has sent one), the header, body and pooled-transaction requests to send, answers to the peer's requests, and when to disconnect")
178 .e2e_testing("tests/client/devp2p: go-ethereum 1.17 and reth 2.7, each on a private dev chain with blocks, accept NetGet's handshake and eth/69 Status and serve headers whose computed hashes match their own eth_getBlockByNumber")
179 .notes("Experimental: two independent servers (go-ethereum and reth) drive it in tests that fail rather than skip, but nobody has reviewed it and its pair job had not run in CI when it landed. No discovery: the peer is named by its enode URL. No snap protocol. Received bodies are decoded (transaction hashes, uncles, withdrawals) but not checked against the header's transactionsRoot.")
180 .build()
181 }
182 fn example_prompt(&self) -> &'static str {
183 "Connect to enode://<node id>@127.0.0.1:30303, mirror its Status, and fetch the first four block headers"
184 }
185 fn get_startup_examples(&self) -> StartupExamples {
186 let enode = format!("enode://{}@127.0.0.1:30303", "8a".repeat(64));
187 let llm = json!({"type": "open_client", "protocol": "devp2p", "remote_addr": enode,
188 "instruction": "Mirror the peer's Status, then fetch headers 0 to 3 and report their hashes"});
189 let mut static_example = llm.clone();
190 static_example["event_handlers"] = json!([
191 {"event_pattern": "eth_status", "handler": {"type": "static", "actions": [
192 {"type": session::SEND_STATUS, "mirror_peer": true},
193 {"type": session::REQUEST_HEADERS, "origin": 0, "amount": 4}
194 ]}},
195 {"event_pattern": "*", "handler": {"type": "static", "actions": []}}
196 ]);
197 let mut scripted = static_example.clone();
198 scripted["event_handlers"][0] = json!({"event_pattern": "eth_status", "handler": {"type": "script", "language": "python",
199 "code": "import json,sys\ne=json.load(sys.stdin)['event']\nlatest=(e.get('peer_status') or {}).get('latest_block',0)\nprint(json.dumps({'actions':[{'type':'eth_send_status','mirror_peer':e.get('peer_status') is not None},{'type':'eth_request_block_headers','origin':latest,'amount':1}]}))"}});
200 StartupExamples::new(llm, scripted, static_example)
201 }
202 fn group_name(&self) -> &'static str {
203 "Blockchain"
204 }
205}
206
207impl Client for Devp2pClientProtocol {
208 fn connect(
209 &self,
210 ctx: ConnectContext,
211 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
212 {
213 Box::pin(super::connect(ctx))
214 }
215
216 fn execute_action(&self, v: Value) -> Result<ClientActionResult> {
217 session::parse_command(&v)?;
218 Ok(ClientActionResult::Custom {
219 name: v["type"].as_str().unwrap_or_default().to_string(),
220 data: v,
221 })
222 }
223}