1use crate::llm::actions::{
14 protocol_trait::{ActionResult, Protocol, Server},
15 ActionDefinition, Parameter, ParameterDefinition, StartupExamples,
16};
17use crate::protocol::log_template::LogTemplate;
18use crate::protocol::EventType;
19use crate::server::devp2p::session::{self, StatusDefaults};
20use crate::server::devp2p::{crypto::NodeKey, eth};
21use crate::state::app_state::AppState;
22use anyhow::{anyhow, Context, Result};
23use serde_json::{json, Value};
24use std::sync::LazyLock;
25
26pub fn default_client_id() -> String {
28 format!("NetGet/v{}", env!("CARGO_PKG_VERSION"))
29}
30
31#[derive(Clone, Default)]
32pub struct Devp2pProtocol;
33
34impl Devp2pProtocol {
35 pub fn new() -> Self {
36 Self
37 }
38}
39
40pub fn p(name: &str, type_hint: &str, description: &str, required: bool) -> Parameter {
41 Parameter {
42 name: name.into(),
43 type_hint: type_hint.into(),
44 description: description.into(),
45 required,
46 }
47}
48
49pub fn example_header() -> Value {
51 let z32 = format!("0x{}", "00".repeat(32));
52 json!({
53 "parentHash": format!("0x{}", "11".repeat(32)),
54 "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
55 "miner": format!("0x{}", "00".repeat(20)),
56 "stateRoot": format!("0x{}", "22".repeat(32)),
57 "transactionsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
58 "receiptsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
59 "logsBloom": format!("0x{}", "00".repeat(256)),
60 "difficulty": "0x0",
61 "number": "0x1",
62 "gasLimit": "0x1c9c380",
63 "gasUsed": "0x0",
64 "timestamp": "0x6553f100",
65 "extraData": "0x",
66 "mixHash": z32,
67 "nonce": "0x0000000000000000",
68 "baseFeePerGas": "0x3b9aca00"
69 })
70}
71
72fn action(
73 name: &str,
74 description: &str,
75 parameters: Vec<Parameter>,
76 example: Value,
77) -> ActionDefinition {
78 ActionDefinition {
79 name: name.into(),
80 description: description.into(),
81 parameters,
82 example,
83 log_template: Some(
84 LogTemplate::new()
85 .with_info(format!("devp2p {name}"))
86 .with_debug(format!("devp2p {name}: {{json(.)}}")),
87 ),
88 }
89}
90
91pub fn send_status_action() -> ActionDefinition {
92 action(
93 session::SEND_STATUS,
94 "Send this node's eth Status, which admits the peer: the eth handshake completes when \
95 both Statuses are exchanged. Fields left out come from the peer's Status when \
96 mirror_peer is true, else from the startup configuration. The fork hash, when no one \
97 gives it, is computed from genesis_hash and forks (EIP-2124). A peer on another \
98 network or genesis will drop the session itself.",
99 vec![
100 p("mirror_peer", "boolean", "Copy every field not given here from the peer's own Status (claim its chain and head)", false),
101 p("network_id", "number", "Network id (1 is mainnet)", false),
102 p("genesis_hash", "string", "0x-prefixed 32-byte genesis block hash", false),
103 p("fork_hash", "string", "0x-prefixed 4-byte EIP-2124 fork hash", false),
104 p("fork_next", "number", "Next fork block or timestamp, 0 for none", false),
105 p("forks", "array", "Fork block numbers or timestamps this chain has passed, to compute fork_hash from", false),
106 p("earliest_block", "number", "eth/69: oldest block served", false),
107 p("latest_block", "number", "eth/69: newest block number", false),
108 p("latest_block_hash", "string", "eth/69: newest block hash", false),
109 p("total_difficulty", "number", "eth/68: total difficulty", false),
110 p("head_hash", "string", "eth/68: head block hash", false),
111 ],
112 json!({"type": session::SEND_STATUS, "mirror_peer": true}),
113 )
114}
115
116pub fn send_headers_action() -> ActionDefinition {
117 action(
118 session::SEND_HEADERS,
119 "Answer a GetBlockHeaders with block headers, each an object of the fields \
120 eth_getBlockByNumber returns (parentHash, sha3Uncles, miner, stateRoot, \
121 transactionsRoot, receiptsRoot, logsBloom, difficulty, number, gasLimit, gasUsed, \
122 timestamp, extraData, mixHash, nonce, then baseFeePerGas, withdrawalsRoot, \
123 blobGasUsed, excessBlobGas, parentBeaconBlockRoot, requestsHash when the chain has \
124 them). NetGet encodes and hashes them; the first must be the requested block, the \
125 numbers must follow the request's skip and direction, each must name the previous as \
126 its parent, and a 'hash' field must equal the computed hash. An empty list says \
127 'I do not have them'.",
128 vec![
129 p(
130 "headers",
131 "array",
132 "Header objects, at most the amount requested",
133 true,
134 ),
135 p(
136 "request_id",
137 "number",
138 "The request answered; defaults to the one in the event",
139 false,
140 ),
141 ],
142 json!({"type": session::SEND_HEADERS, "headers": [example_header()]}),
143 )
144}
145
146pub fn send_bodies_action() -> ActionDefinition {
147 action(
148 session::SEND_BODIES,
149 "Answer a GetBlockBodies: one body per requested hash, in order (fewer is allowed). A \
150 body is {transactions: [signed transactions as 0x-hex, as eth_getRawTransactionByHash \
151 returns them], uncles: [header objects], withdrawals: [{index, validatorIndex, \
152 address, amount}] for post-Shanghai blocks}. An empty list says 'I do not have them'.",
153 vec![
154 p(
155 "bodies",
156 "array",
157 "One block body per requested hash, in the order requested",
158 true,
159 ),
160 p(
161 "request_id",
162 "number",
163 "The request answered; defaults to the one in the event",
164 false,
165 ),
166 ],
167 json!({"type": session::SEND_BODIES, "bodies": [{"transactions": [], "uncles": [], "withdrawals": []}]}),
168 )
169}
170
171pub fn send_pooled_action() -> ActionDefinition {
172 action(
173 session::SEND_POOLED,
174 "Answer a GetPooledTransactions with signed transactions (0x-hex canonical encodings). \
175 NetGet hashes each one and refuses any the peer did not ask for. An empty list says \
176 'I do not have them'.",
177 vec![
178 p("transactions", "array", "Signed transactions, 0x-hex", true),
179 p(
180 "request_id",
181 "number",
182 "The request answered; defaults to the one in the event",
183 false,
184 ),
185 ],
186 json!({"type": session::SEND_POOLED, "transactions": []}),
187 )
188}
189
190pub fn request_headers_action() -> ActionDefinition {
191 action(
192 session::REQUEST_HEADERS,
193 "Ask the peer for block headers (GetBlockHeaders). The answer arrives as \
194 eth_block_headers_received with every header decoded and its hash computed.",
195 vec![
196 p(
197 "origin",
198 "number",
199 "First block: a number, or a 0x-prefixed 32-byte block hash",
200 true,
201 ),
202 p(
203 "amount",
204 "number",
205 "How many headers, 1 to 64 (default 1)",
206 false,
207 ),
208 p(
209 "skip",
210 "number",
211 "Blocks skipped between consecutive headers (default 0)",
212 false,
213 ),
214 p(
215 "reverse",
216 "boolean",
217 "Walk towards genesis instead of the head (default false)",
218 false,
219 ),
220 ],
221 json!({"type": session::REQUEST_HEADERS, "origin": 0, "amount": 4}),
222 )
223}
224
225pub fn request_bodies_action() -> ActionDefinition {
226 action(
227 session::REQUEST_BODIES,
228 "Ask the peer for block bodies by block hash (GetBlockBodies). The answer arrives as \
229 eth_block_bodies_received.",
230 vec![p("hashes", "array", "Block hashes, 0x-hex, 1 to 256", true)],
231 json!({"type": session::REQUEST_BODIES, "hashes": [format!("0x{}", "ab".repeat(32))]}),
232 )
233}
234
235pub fn request_pooled_action() -> ActionDefinition {
236 action(
237 session::REQUEST_POOLED,
238 "Ask the peer for transactions from its pool by hash (GetPooledTransactions), \
239 typically ones it announced. The answer arrives as eth_pooled_transactions_received.",
240 vec![p(
241 "hashes",
242 "array",
243 "Transaction hashes, 0x-hex, 1 to 256",
244 true,
245 )],
246 json!({"type": session::REQUEST_POOLED, "hashes": [format!("0x{}", "cd".repeat(32))]}),
247 )
248}
249
250pub fn disconnect_action() -> ActionDefinition {
251 action(
252 session::DISCONNECT,
253 "Send Disconnect with a reason and close the session. Reasons: requested, \
254 useless_peer, too_many_peers, already_connected, incompatible_version, \
255 breach_of_protocol, subprotocol_error, client_quitting.",
256 vec![p(
257 "reason",
258 "string",
259 "The reason (default requested)",
260 false,
261 )],
262 json!({"type": session::DISCONNECT, "reason": "useless_peer"}),
263 )
264}
265
266pub fn all_actions() -> Vec<ActionDefinition> {
268 vec![
269 send_status_action(),
270 send_headers_action(),
271 send_bodies_action(),
272 send_pooled_action(),
273 request_headers_action(),
274 request_bodies_action(),
275 request_pooled_action(),
276 disconnect_action(),
277 ]
278}
279
280fn requests() -> Vec<ActionDefinition> {
281 vec![
282 request_headers_action(),
283 request_bodies_action(),
284 request_pooled_action(),
285 disconnect_action(),
286 ]
287}
288
289fn with(first: Vec<ActionDefinition>) -> Vec<ActionDefinition> {
290 let mut v = first;
291 v.extend(requests());
292 v
293}
294
295pub static STATUS_EVENT: LazyLock<EventType> = LazyLock::new(|| {
296 EventType::new(
297 "eth_status",
298 "A peer completed the RLPx handshake and Hello and sent its eth Status. Admit it with \
299 eth_send_status (follow-up requests may come in the same answer) or refuse it with \
300 devp2p_disconnect",
301 json!({"type": session::SEND_STATUS, "mirror_peer": true}),
302 )
303 .with_parameters(vec![
304 p("remote", "string", "The peer's address", true),
305 p("client_id", "string", "The client id from its Hello", true),
306 p("node_id", "string", "Its node id (128 hex characters)", true),
307 p("capabilities", "array", "Capabilities from its Hello, as name/version", true),
308 p("eth_version", "number", "The eth version both sides speak", true),
309 p("peer_status", "object", "Its Status: network_id, genesis_hash, fork_hash, fork_next, and the eth/69 block range or eth/68 total difficulty and head", true),
310 p("configured", "object", "The Status values configured at startup", true),
311 ])
312 .with_actions(with(vec![send_status_action()]))
313 .with_alternative_example(json!({"type": session::DISCONNECT, "reason": "useless_peer"}))
314 .with_log_template(
315 LogTemplate::new()
316 .with_info("devp2p peer {client_id} at {remote} sent its Status")
317 .with_trace("devp2p status: {json_pretty(.)}"),
318 )
319});
320
321pub static HEADERS_REQUESTED_EVENT: LazyLock<EventType> =
322 LazyLock::new(|| {
323 EventType::new(
324 "eth_block_headers_requested",
325 "The peer asked for block headers. Answer with eth_send_block_headers (an empty list if \
326 you do not have them); a request left unanswered closes the session",
327 json!({"type": session::SEND_HEADERS, "headers": []}),
328 )
329 .with_parameters(vec![
330 p("request_id", "number", "The request's id", true),
331 p("origin", "number", "First block: a number, or a 0x block hash", true),
332 p("amount", "number", "How many headers at most", true),
333 p("skip", "number", "Blocks between consecutive headers", true),
334 p("reverse", "boolean", "Towards genesis instead of the head", true),
335 ])
336 .with_actions(with(vec![send_headers_action()]))
337 .with_log_template(
338 LogTemplate::new()
339 .with_info("devp2p peer asks for {amount} headers from {origin}")
340 .with_debug("devp2p GetBlockHeaders: {json(.)}"),
341 )
342 });
343
344pub static BODIES_REQUESTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
345 EventType::new(
346 "eth_block_bodies_requested",
347 "The peer asked for block bodies by hash. Answer with eth_send_block_bodies (an empty \
348 list if you do not have them)",
349 json!({"type": session::SEND_BODIES, "bodies": []}),
350 )
351 .with_parameters(vec![
352 p("request_id", "number", "The request's id", true),
353 p(
354 "hashes",
355 "array",
356 "The hashes of the blocks whose bodies are wanted",
357 true,
358 ),
359 ])
360 .with_actions(with(vec![send_bodies_action()]))
361 .with_log_template(LogTemplate::new().with_info("devp2p peer asks for block bodies"))
362});
363
364pub static POOLED_REQUESTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
365 EventType::new(
366 "eth_pooled_transactions_requested",
367 "The peer asked for transactions from this node's pool. Answer with \
368 eth_send_pooled_transactions (an empty list if you do not have them)",
369 json!({"type": session::SEND_POOLED, "transactions": []}),
370 )
371 .with_parameters(vec![
372 p("request_id", "number", "The request's id", true),
373 p("hashes", "array", "Transaction hashes", true),
374 ])
375 .with_actions(with(vec![send_pooled_action()]))
376 .with_log_template(LogTemplate::new().with_info("devp2p peer asks for pooled transactions"))
377});
378
379pub static ANNOUNCED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
380 EventType::new(
381 "eth_transactions_announced",
382 "The peer announced transactions in its pool (NewPooledTransactionHashes). Request \
383 them with eth_request_pooled_transactions, or do nothing",
384 json!({"type": session::REQUEST_POOLED, "hashes": [format!("0x{}", "cd".repeat(32))]}),
385 )
386 .with_parameters(vec![p(
387 "transactions",
388 "array",
389 "Announced transactions: hash, type and size",
390 true,
391 )])
392 .with_actions(requests())
393 .with_log_template(LogTemplate::new().with_info("devp2p peer announced transactions"))
394});
395
396pub static HEADERS_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
397 EventType::new(
398 "eth_block_headers_received",
399 "The peer answered a header request. Every header is decoded with its computed hash; \
400 matches_request says whether they are the blocks asked for, linked whether each \
401 names the previous as its parent",
402 json!({"type": session::REQUEST_HEADERS, "origin": 4, "amount": 4}),
403 )
404 .with_parameters(vec![
405 p("request_id", "number", "The request answered", true),
406 p(
407 "request",
408 "object",
409 "The request this answers: its kind and its query or hashes",
410 true,
411 ),
412 p("count", "number", "How many headers came back", true),
413 p(
414 "headers",
415 "array",
416 "Header fields and hash, by eth_getBlockByNumber names",
417 true,
418 ),
419 p(
420 "matches_request",
421 "boolean",
422 "The numbers (or first hash) are the ones asked for",
423 true,
424 ),
425 p(
426 "linked",
427 "boolean",
428 "Each header's parentHash is the previous header's hash",
429 true,
430 ),
431 ])
432 .with_actions(requests())
433 .with_log_template(
434 LogTemplate::new()
435 .with_info("devp2p received {count} headers")
436 .with_trace("devp2p headers: {json_pretty(.)}"),
437 )
438});
439
440pub static BODIES_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
441 EventType::new(
442 "eth_block_bodies_received",
443 "The peer answered a body request: per block, its transactions (hash, type, size), \
444 uncles and withdrawals",
445 json!({"type": session::REQUEST_HEADERS, "origin": 0}),
446 )
447 .with_parameters(vec![
448 p("request_id", "number", "The request answered", true),
449 p(
450 "request",
451 "object",
452 "The request this answers: its kind and its query or hashes",
453 true,
454 ),
455 p("count", "number", "How many bodies came back", true),
456 p(
457 "bodies",
458 "array",
459 "Per block: its hash, transactions (hash, type, size), uncles and withdrawals",
460 true,
461 ),
462 ])
463 .with_actions(requests())
464 .with_log_template(LogTemplate::new().with_info("devp2p received {count} block bodies"))
465});
466
467pub static POOLED_RECEIVED_EVENT: LazyLock<EventType> =
468 LazyLock::new(|| {
469 EventType::new(
470 "eth_pooled_transactions_received",
471 "The peer answered a pooled-transaction request: each transaction's hash, type and size",
472 json!({"type": session::REQUEST_POOLED, "hashes": [format!("0x{}", "cd".repeat(32))]}),
473 )
474 .with_parameters(vec![
475 p("request_id", "number", "The request answered", true),
476 p("request", "object", "The request this answers: its kind and its query or hashes", true),
477 p("count", "number", "How many came back", true),
478 p("transactions", "array", "hash, type and size of each", true),
479 p("unrequested", "number", "How many were not asked for", true),
480 ])
481 .with_actions(requests())
482 .with_log_template(LogTemplate::new().with_info("devp2p received {count} pooled transactions"))
483 });
484
485pub static REQUEST_TIMEOUT_EVENT: LazyLock<EventType> = LazyLock::new(|| {
486 EventType::new(
487 "eth_request_timed_out",
488 "A request this node sent got no answer in time; it is forgotten",
489 json!({"type": session::REQUEST_HEADERS, "origin": 0}),
490 )
491 .with_parameters(vec![
492 p(
493 "request_id",
494 "number",
495 "The id of the request that went unanswered",
496 true,
497 ),
498 p(
499 "request",
500 "object",
501 "The request this answers: its kind and its query or hashes",
502 true,
503 ),
504 p("timeout_secs", "number", "How long NetGet waited", true),
505 ])
506 .with_actions(requests())
507 .with_log_template(LogTemplate::new().with_info("devp2p request {request_id} timed out"))
508});
509
510pub fn event_types() -> Vec<EventType> {
511 vec![
512 STATUS_EVENT.clone(),
513 HEADERS_REQUESTED_EVENT.clone(),
514 BODIES_REQUESTED_EVENT.clone(),
515 POOLED_REQUESTED_EVENT.clone(),
516 ANNOUNCED_EVENT.clone(),
517 HEADERS_RECEIVED_EVENT.clone(),
518 BODIES_RECEIVED_EVENT.clone(),
519 POOLED_RECEIVED_EVENT.clone(),
520 REQUEST_TIMEOUT_EVENT.clone(),
521 ]
522}
523
524pub fn common_startup_parameters() -> Vec<ParameterDefinition> {
526 let param =
527 |name: &str, type_hint: &str, description: &str, example: Value| ParameterDefinition {
528 name: name.into(),
529 type_hint: type_hint.into(),
530 description: description.into(),
531 required: false,
532 example,
533 default: None,
534 };
535 vec![
536 param(
537 "node_private_key",
538 "string",
539 "The node's secp256k1 private key, 64 hex characters; its public key is the node id \
540 in the enode URL. Without it a fresh key is made at start",
541 json!("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291"),
542 ),
543 ParameterDefinition {
544 name: "client_id".into(),
545 type_hint: "string".into(),
546 description: "The client id sent in Hello (peers show it as the node's name)".into(),
547 required: false,
548 example: json!("NetGet/v1.0/linux"),
549 default: Some(json!(default_client_id())),
550 },
551 param(
552 "network_id",
553 "number",
554 "Status default: network id (1 is mainnet)",
555 json!(1337),
556 ),
557 param(
558 "genesis_hash",
559 "string",
560 "Status default: 0x-prefixed genesis block hash",
561 json!(format!("0x{}", "00".repeat(32))),
562 ),
563 param(
564 "forks",
565 "array",
566 "Status default: fork block numbers or timestamps the chain has passed, for the \
567 EIP-2124 fork hash (forks at genesis are left out)",
568 json!([1150000, 1920000]),
569 ),
570 param(
571 "fork_next",
572 "number",
573 "Status default: the next fork, 0 for none",
574 json!(0),
575 ),
576 param(
577 "latest_block",
578 "number",
579 "Status default: newest block number",
580 json!(0),
581 ),
582 param(
583 "latest_block_hash",
584 "string",
585 "Status default: newest block hash (the genesis hash when not given)",
586 json!(format!("0x{}", "00".repeat(32))),
587 ),
588 ]
589}
590
591pub struct CommonConfig {
593 pub key: NodeKey,
594 pub client_id: String,
595 pub defaults: StatusDefaults,
596}
597
598pub fn common_config(params: Option<&crate::protocol::StartupParams>) -> Result<CommonConfig> {
599 let s = |k: &str| -> Result<Option<String>> {
600 Ok(params
601 .map(|p| p.get_optional_string(k))
602 .transpose()?
603 .flatten())
604 };
605 let u = |k: &str| -> Result<Option<u64>> {
606 Ok(params.map(|p| p.get_optional_u64(k)).transpose()?.flatten())
607 };
608 let key = match s("node_private_key")? {
609 Some(k) => NodeKey::from_hex(&k).context("node_private_key")?,
610 None => NodeKey::random(),
611 };
612 let client_id = s("client_id")?.unwrap_or_else(default_client_id);
613 anyhow::ensure!(
614 !client_id.is_empty()
615 && client_id.len() <= eth::MAX_CLIENT_ID_CHARS
616 && !crate::utils::sanitize::has_controls(&client_id),
617 "client_id is 1 to {} printable characters",
618 eth::MAX_CLIENT_ID_CHARS
619 );
620 let hash = |k: &str| -> Result<Option<[u8; 32]>> {
621 s(k)?
622 .map(|h| eth::parse_fixed::<32>(&json!(h), k))
623 .transpose()
624 };
625 let forks = match params
626 .map(|p| p.get_optional_array("forks"))
627 .transpose()?
628 .flatten()
629 {
630 Some(a) => {
631 anyhow::ensure!(a.len() <= 64, "at most 64 forks");
632 a.iter()
633 .map(|f| eth::parse_u64(f, "forks entry"))
634 .collect::<Result<Vec<_>>>()?
635 }
636 None => Vec::new(),
637 };
638 Ok(CommonConfig {
639 key,
640 client_id,
641 defaults: StatusDefaults {
642 network_id: u("network_id")?,
643 genesis: hash("genesis_hash")?,
644 forks,
645 fork_next: u("fork_next")?,
646 latest_block: u("latest_block")?,
647 latest_block_hash: hash("latest_block_hash")?,
648 },
649 })
650}
651
652impl Protocol for Devp2pProtocol {
653 fn protocol_name(&self) -> &'static str {
654 "devp2p"
655 }
656 fn stack_name(&self) -> &'static str {
657 "ETH>IP>TCP>RLPx>devp2p"
658 }
659 fn description(&self) -> &'static str {
660 "Ethereum devp2p node (RLPx, eth/68 and eth/69) whose admission, Status and block-header answers the LLM decides"
661 }
662 fn example_prompt(&self) -> &'static str {
663 "Start an Ethereum devp2p node on port 30303; admit every peer by mirroring its Status and answer header requests from these blocks"
664 }
665 fn keywords(&self) -> Vec<&'static str> {
666 vec![
667 "devp2p",
668 "ethereum",
669 "eth/68",
670 "eth/69",
671 "rlpx",
672 "enode",
673 "geth",
674 "ethereum node",
675 "execution client",
676 ]
677 }
678 fn group_name(&self) -> &'static str {
679 "Blockchain"
680 }
681
682 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
683 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
684 ProtocolMetadataV2::builder()
685 .state(DevelopmentState::Experimental)
686 .well_known_port(30303)
687 .answers_on_failure()
688 .max_inbound_bytes(session::MAX_MESSAGE_BYTES)
689 .implementation(
690 "Hand-written RLPx (no Ethereum crate): the EIP-8 auth/ack handshake (secp256k1 \
691 ECDH and recoverable signatures from the secp256k1 crate, ECIES with \
692 AES-128-CTR and HMAC-SHA256 over a SHA-256 concat KDF), AES-256-CTR frames \
693 with go-ethereum's keccak MAC construction, Snappy after Hello v5, a bounded \
694 canonical RLP codec, the base protocol (Hello, Disconnect, Ping/Pong) and \
695 eth/68 and eth/69 (Status, GetBlockHeaders/BlockHeaders, \
696 GetBlockBodies/BlockBodies, NewPooledTransactionHashes, \
697 GetPooledTransactions/PooledTransactions, BlockRangeUpdate; GetReceipts is \
698 answered empty). Block hashes are keccak-256 of the RLP header, computed here",
699 )
700 .llm_control(
701 "Admission and this node's Status (eth_status), every answer to the peer's \
702 header, body and pooled-transaction requests, and the requests sent to it; \
703 headers are JSON fields, never bytes",
704 )
705 .e2e_testing(
706 "tests/server/devp2p: go-ethereum 1.17 dials NetGet after admin_addPeer, \
707 completes the eth/69 handshake, lists NetGet in admin_peers with the block \
708 range NetGet sent, and verifies with --eth.requiredblocks that the header \
709 NetGet served hashes to its own block; go-ethereum's devp2p tool reads \
710 NetGet's Hello; reth 2.7 dials NetGet and lists it in admin_peers. \
711 tshark has no RLPx dissector (every frame is encrypted), so no pcap oracle",
712 )
713 .notes(
714 "Experimental: two independent implementations (go-ethereum, with its devp2p \
715 tool, and reth) complete sessions with it in tests that fail rather than skip, \
716 but no fuzz target exists, nobody has reviewed it against the devp2p specs \
717 line by line, and its pair job had not run in CI when it landed. No discovery \
718 (discv4/discv5): peers are added statically (admin_addPeer, --bootnodes, \
719 static nodes). No snap protocol. Bodies and pooled transactions are checked \
720 for shape and, for pooled transactions, against the hashes asked for; a body \
721 is not checked against its header's transactionsRoot (that needs a Merkle \
722 Patricia trie). Nothing is stored: every header the node serves is one the \
723 model wrote.",
724 )
725 .build()
726 }
727
728 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
729 common_startup_parameters()
730 }
731
732 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
733 Vec::new()
734 }
735
736 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
737 all_actions()
738 }
739
740 fn get_event_types(&self) -> Vec<EventType> {
741 event_types()
742 }
743
744 fn get_startup_examples(&self) -> StartupExamples {
745 StartupExamples::new(
746 json!({
747 "type": "open_server",
748 "port": 30303,
749 "base_stack": "devp2p",
750 "startup_params": {"network_id": 1337},
751 "instruction": "Admit every peer on network 1337 by mirroring its Status. Answer \
752 header requests with an empty list."
753 }),
754 json!({
755 "type": "open_server",
756 "port": 30303,
757 "base_stack": "devp2p",
758 "event_handlers": [{
759 "event_pattern": "*",
760 "handler": {
761 "type": "script",
762 "language": "python",
763 "code": "import json,sys\ni=json.load(sys.stdin); t=i['event_type_id']\na=[]\nif t=='eth_status': a=[{'type':'eth_send_status','mirror_peer':True}]\nelif t=='eth_block_headers_requested': a=[{'type':'eth_send_block_headers','headers':[]}]\nprint(json.dumps({'actions':a}))"
764 }
765 }]
766 }),
767 json!({
768 "type": "open_server",
769 "port": 30303,
770 "base_stack": "devp2p",
771 "event_handlers": [
772 {"event_pattern": "eth_status", "handler": {"type": "static", "actions": [
773 {"type": session::SEND_STATUS, "mirror_peer": true}
774 ]}},
775 {"event_pattern": "eth_block_headers_requested", "handler": {"type": "static", "actions": [
776 {"type": session::SEND_HEADERS, "headers": []}
777 ]}},
778 {"event_pattern": "*", "handler": {"type": "static", "actions": []}}
779 ]
780 }),
781 )
782 }
783}
784
785impl Server for Devp2pProtocol {
786 fn spawn(
787 &self,
788 ctx: crate::protocol::SpawnContext,
789 ) -> std::pin::Pin<
790 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
791 > {
792 Box::pin(async move {
793 let config = common_config(ctx.startup_params.as_ref())?;
794 crate::server::devp2p::Devp2pServer::spawn(
795 ctx.legacy_listen_addr(),
796 ctx.llm_client,
797 ctx.state,
798 ctx.status_tx,
799 ctx.server_id,
800 config,
801 )
802 .await
803 })
804 }
805
806 fn execute_action(&self, action: Value) -> Result<ActionResult> {
807 let kind = action
808 .get("type")
809 .and_then(|v| v.as_str())
810 .ok_or_else(|| anyhow!("Missing 'type' field in action"))?
811 .to_string();
812 session::parse_command(&action)?;
813 Ok(ActionResult::Custom {
814 name: kind,
815 data: action,
816 })
817 }
818}