1use crate::llm::actions::{
4 protocol_trait::{ActionResult, Protocol, Server},
5 ActionDefinition, Parameter, ParameterDefinition,
6};
7use crate::protocol::log_template::LogTemplate;
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use bitcoin::consensus::Encodable;
12use bitcoin::p2p::address::Address;
13use bitcoin::p2p::message::{NetworkMessage, RawNetworkMessage};
14use bitcoin::p2p::message_network::VersionMessage;
15use bitcoin::p2p::Magic;
16use bitcoin::p2p::ServiceFlags;
17use serde_json::json;
18use std::sync::LazyLock;
19
20fn magic_for_network(network: &str) -> Result<Magic> {
33 match network {
34 "mainnet" | "main" => Ok(Magic::BITCOIN),
35 "testnet" | "test" => Ok(Magic::TESTNET3),
36 "signet" => Ok(Magic::SIGNET),
37 "regtest" => Ok(Magic::REGTEST),
38 other => Err(anyhow::anyhow!(
39 "unknown Bitcoin network '{}': expected one of mainnet (or main), testnet (or \
40 test), signet, regtest",
41 other
42 )),
43 }
44}
45
46pub struct BitcoinProtocol;
47
48impl BitcoinProtocol {
49 pub fn new() -> Self {
50 Self
51 }
52}
53
54impl Protocol for BitcoinProtocol {
56 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
57 vec![ParameterDefinition {
58 name: "network".to_string(),
59 type_hint: "string".to_string(),
60 description:
61 "Bitcoin network: 'mainnet', 'testnet', 'signet', or 'regtest' (default: mainnet)"
62 .to_string(),
63 required: false,
64 example: json!("mainnet"),
65 default: None,
66 }]
67 }
68 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
69 vec![]
70 }
71 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
72 vec![
73 send_bitcoin_message_action(),
74 send_version_action(),
75 send_verack_action(),
76 send_ping_action(),
77 send_pong_action(),
78 send_getaddr_action(),
79 close_this_connection_action(),
80 ]
81 }
82 fn protocol_name(&self) -> &'static str {
83 "Bitcoin P2P"
84 }
85 fn get_event_types(&self) -> Vec<EventType> {
86 get_bitcoin_event_types()
87 }
88 fn stack_name(&self) -> &'static str {
89 "ETH>IP>TCP>Bitcoin"
90 }
91 fn keywords(&self) -> Vec<&'static str> {
92 vec!["bitcoin", "btc", "p2p", "blockchain"]
93 }
94 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
95 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
96
97 ProtocolMetadataV2::builder()
98 .state(DevelopmentState::Experimental)
99 .well_known_port(8333)
100 .implementation("Bitcoin P2P protocol using rust-bitcoin crate for message parsing")
101 .llm_control("LLM decides how to respond to all P2P messages (version, getdata, ping, etc.)")
102 .e2e_testing("tests/server/bitcoin/{e2e_test,peer_inject_test}.rs, 17 LLM calls, none #[ignore]d. The peer is a raw TcpStream; the `bitcoin` crate encodes and decodes the messages on the test's side, which makes it a **codec, not a peer completing a session** - the same situation as dhcp's in-test RFC 2131 decoder, and why this is not Beta. No bitcoind, no third-party node. This field read 'Bitcoin P2P client (TBD)'. Covered: version/verack handshake, ping/pong, getaddr, a testnet magic check, and the dashboard's injected message and disconnect. Not tested: a real Bitcoin node, block or transaction relay, anything past the handshake.")
103 .notes("Not a real full node - LLM controls all responses. Supports mainnet/testnet/signet/regtest; an unrecognised network name is now an error rather than silently answering on mainnet. Stores nothing: no chain, no mempool, no peer database. Message framing is bounded (24-byte header validated before the length field is trusted, 4 MB body cap matching Bitcoin Core), and the `bitcoin` crate's consensus decoding caps its own var-int-counted allocations.")
104 .max_inbound_bytes(crate::server::bitcoin::MAX_MESSAGE_BYTES)
105 .build()
106 }
107 fn description(&self) -> &'static str {
108 "Bitcoin P2P protocol server (LLM-controlled, not a real full node)"
109 }
110 fn example_prompt(&self) -> &'static str {
111 "Run Bitcoin P2P server on port 8333; respond to version with our own version, handle ping/pong"
112 }
113 fn group_name(&self) -> &'static str {
114 "Blockchain"
115 }
116
117 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
118 use crate::llm::actions::StartupExamples;
119 use serde_json::json;
120
121 let script = r#"import json, sys
124data = json.load(sys.stdin)
125event = data["event"]
126if data["event_type_id"] == "bitcoin_connection_opened":
127 actions = [{"type": "send_version", "user_agent": "/NetGet:0.1/"},
128 {"type": "send_verack"}]
129else:
130 actions = []
131print(json.dumps({"actions": actions}))"#;
132
133 StartupExamples::new(
134 json!({
136 "type": "open_server",
137 "port": 8333,
138 "base_stack": "bitcoin",
139 "instruction": "Act as Bitcoin P2P node. Respond to version with our version, handle ping/pong",
140 "startup_params": {
141 "network": "mainnet"
142 }
143 }),
144 json!({
146 "type": "open_server",
147 "port": 8333,
148 "base_stack": "bitcoin",
149 "startup_params": {
150 "network": "mainnet"
151 },
152 "event_handlers": [{
153 "event_pattern": "bitcoin_connection_opened",
154 "handler": {
155 "type": "script",
156 "language": "python",
157 "code": script
158 }
159 }]
160 }),
161 json!({
163 "type": "open_server",
164 "port": 8333,
165 "base_stack": "bitcoin",
166 "startup_params": {
167 "network": "mainnet"
168 },
169 "event_handlers": [
170 {
171 "event_pattern": "bitcoin_message_received",
172 "handler": {
173 "type": "static",
174 "actions": [{
175 "type": "send_verack",
176 "network": "mainnet"
177 }]
178 }
179 }
180 ]
181 }),
182 )
183 }
184}
185
186impl Server for BitcoinProtocol {
188 fn spawn(
189 &self,
190 ctx: crate::protocol::SpawnContext,
191 ) -> std::pin::Pin<
192 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
193 > {
194 Box::pin(async move {
195 let network = ctx
197 .startup_params
198 .as_ref()
199 .map(|p| p.get_optional_string("network"))
200 .transpose()?
201 .flatten()
202 .unwrap_or_else(|| "mainnet".to_string());
203
204 use crate::server::bitcoin::BitcoinServer;
205 BitcoinServer::spawn_with_llm_actions(
206 ctx.legacy_listen_addr(),
207 ctx.llm_client,
208 ctx.state,
209 ctx.status_tx,
210 ctx.server_id,
211 network,
212 )
213 .await
214 })
215 }
216 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
217 let action_type = action
218 .get("type")
219 .and_then(|v| v.as_str())
220 .context("Missing 'type' field in action")?;
221
222 match action_type {
223 "send_bitcoin_message" => self.execute_send_bitcoin_message(action),
224 "send_version" => self.execute_send_version(action),
225 "send_verack" => self.execute_send_verack(action),
226 "send_ping" => self.execute_send_ping(action),
227 "send_pong" => self.execute_send_pong(action),
228 "send_getaddr" => self.execute_send_getaddr(action),
229 "close_this_connection" => Ok(ActionResult::CloseConnection),
230 "close_connection" => Ok(ActionResult::CloseConnection),
234 _ => Err(anyhow::anyhow!("Unknown Bitcoin action: {}", action_type)),
235 }
236 }
237}
238
239impl BitcoinProtocol {
240 fn execute_send_bitcoin_message(&self, action: serde_json::Value) -> Result<ActionResult> {
242 let hex_data = action
243 .get("hex_data")
244 .and_then(|v| v.as_str())
245 .context("Missing 'hex_data' parameter")?;
246
247 let bytes = hex::decode(hex_data).context("Invalid hex data")?;
248
249 Ok(ActionResult::Output(bytes))
250 }
251
252 fn execute_send_version(&self, action: serde_json::Value) -> Result<ActionResult> {
254 let network = action
255 .get("network")
256 .and_then(|v| v.as_str())
257 .unwrap_or("mainnet");
258
259 let magic = magic_for_network(network)?;
260
261 let version = action
263 .get("version")
264 .and_then(|v| v.as_u64())
265 .unwrap_or(70015) as u32;
266
267 let services = action.get("services").and_then(|v| v.as_u64()).unwrap_or(0);
268
269 let timestamp = action
270 .get("timestamp")
271 .and_then(|v| v.as_i64())
272 .unwrap_or_else(|| {
273 crate::utils::clock::SystemTime::now()
274 .duration_since(crate::utils::clock::UNIX_EPOCH)
275 .unwrap()
276 .as_secs() as i64
277 });
278
279 let user_agent = action
280 .get("user_agent")
281 .and_then(|v| v.as_str())
282 .unwrap_or("/NetGet:0.1.0/");
283
284 let start_height = action
285 .get("start_height")
286 .and_then(|v| v.as_i64())
287 .unwrap_or(0) as i32;
288
289 let relay = action
290 .get("relay")
291 .and_then(|v| v.as_bool())
292 .unwrap_or(false);
293
294 let receiver = Address::new(
296 &std::net::SocketAddr::from(([0, 0, 0, 0], 0)),
297 ServiceFlags::NONE,
298 );
299 let sender = Address::new(
300 &std::net::SocketAddr::from(([0, 0, 0, 0], 0)),
301 ServiceFlags::from(services),
302 );
303
304 let version_msg = NetworkMessage::Version(VersionMessage {
305 version,
306 services: ServiceFlags::from(services),
307 timestamp,
308 receiver,
309 sender,
310 nonce: rand::random(),
311 user_agent: user_agent.to_string(),
312 start_height,
313 relay,
314 });
315
316 let raw_msg = RawNetworkMessage::new(magic, version_msg);
318 let mut bytes = Vec::new();
319 raw_msg
320 .consensus_encode(&mut bytes)
321 .context("Failed to encode version message")?;
322
323 Ok(ActionResult::Output(bytes))
324 }
325
326 fn execute_send_verack(&self, action: serde_json::Value) -> Result<ActionResult> {
328 let network = action
329 .get("network")
330 .and_then(|v| v.as_str())
331 .unwrap_or("mainnet");
332
333 let magic = magic_for_network(network)?;
334
335 let raw_msg = RawNetworkMessage::new(magic, NetworkMessage::Verack);
336 let mut bytes = Vec::new();
337 raw_msg
338 .consensus_encode(&mut bytes)
339 .context("Failed to encode verack message")?;
340
341 Ok(ActionResult::Output(bytes))
342 }
343
344 fn execute_send_ping(&self, action: serde_json::Value) -> Result<ActionResult> {
346 let network = action
347 .get("network")
348 .and_then(|v| v.as_str())
349 .unwrap_or("mainnet");
350
351 let magic = magic_for_network(network)?;
352
353 let nonce = action
354 .get("nonce")
355 .and_then(|v| v.as_u64())
356 .unwrap_or_else(|| rand::random());
357
358 let raw_msg = RawNetworkMessage::new(magic, NetworkMessage::Ping(nonce));
359 let mut bytes = Vec::new();
360 raw_msg
361 .consensus_encode(&mut bytes)
362 .context("Failed to encode ping message")?;
363
364 Ok(ActionResult::Output(bytes))
365 }
366
367 fn execute_send_pong(&self, action: serde_json::Value) -> Result<ActionResult> {
369 let network = action
370 .get("network")
371 .and_then(|v| v.as_str())
372 .unwrap_or("mainnet");
373
374 let magic = magic_for_network(network)?;
375
376 let nonce = action
377 .get("nonce")
378 .and_then(|v| v.as_u64())
379 .context("Missing 'nonce' parameter for pong")?;
380
381 let raw_msg = RawNetworkMessage::new(magic, NetworkMessage::Pong(nonce));
382 let mut bytes = Vec::new();
383 raw_msg
384 .consensus_encode(&mut bytes)
385 .context("Failed to encode pong message")?;
386
387 Ok(ActionResult::Output(bytes))
388 }
389
390 fn execute_send_getaddr(&self, action: serde_json::Value) -> Result<ActionResult> {
392 let network = action
393 .get("network")
394 .and_then(|v| v.as_str())
395 .unwrap_or("mainnet");
396
397 let magic = magic_for_network(network)?;
398
399 let raw_msg = RawNetworkMessage::new(magic, NetworkMessage::GetAddr);
400 let mut bytes = Vec::new();
401 raw_msg
402 .consensus_encode(&mut bytes)
403 .context("Failed to encode getaddr message")?;
404
405 Ok(ActionResult::Output(bytes))
406 }
407}
408
409fn send_bitcoin_message_action() -> ActionDefinition {
411 ActionDefinition {
412 name: "send_bitcoin_message".to_string(),
413 description: "ESCAPE HATCH - prefer send_version/send_verack/send_ping/send_pong/\
414 send_getaddr. Sends bytes you supply verbatim, including the 24-byte \
415 header, magic and payload checksum, none of which are computed for you. \
416 Only use this for a message type with no dedicated action."
417 .to_string(),
418 parameters: vec![Parameter {
419 name: "hex_data".to_string(),
420 type_hint: "string".to_string(),
421 description: "Complete hex-encoded Bitcoin message: magic(4) + command(12) + \
422 length(4) + checksum(4) + payload. Written to the socket as-is."
423 .to_string(),
424 required: true,
425 }],
426 example: json!({
427 "type": "send_bitcoin_message",
428 "hex_data": "f9beb4d976657261636b000000000000000000005df6e0e2"
429 }),
430 log_template: Some(
431 LogTemplate::new()
432 .with_info("-> BTC raw message")
433 .with_debug("BTC send_bitcoin_message"),
434 ),
435 }
436}
437
438fn send_version_action() -> ActionDefinition {
440 ActionDefinition {
441 name: "send_version".to_string(),
442 description: "Send a Bitcoin version message (handshake)".to_string(),
443 parameters: vec![
444 Parameter {
445 name: "network".to_string(),
446 type_hint: "string".to_string(),
447 description: "Network: 'mainnet', 'testnet', 'signet', 'regtest'".to_string(),
448 required: false,
449 },
450 Parameter {
451 name: "version".to_string(),
452 type_hint: "number".to_string(),
453 description: "Protocol version (default: 70015)".to_string(),
454 required: false,
455 },
456 Parameter {
457 name: "services".to_string(),
458 type_hint: "number".to_string(),
459 description: "Service flags (default: 0)".to_string(),
460 required: false,
461 },
462 Parameter {
465 name: "timestamp".to_string(),
466 type_hint: "number".to_string(),
467 description: "Unix timestamp for the version message (default: now). Peers use \
468 it to estimate network-adjusted time."
469 .to_string(),
470 required: false,
471 },
472 Parameter {
473 name: "user_agent".to_string(),
474 type_hint: "string".to_string(),
475 description: "User agent string (default: '/NetGet:0.1.0/')".to_string(),
476 required: false,
477 },
478 Parameter {
479 name: "start_height".to_string(),
480 type_hint: "number".to_string(),
481 description: "Blockchain height (default: 0)".to_string(),
482 required: false,
483 },
484 Parameter {
485 name: "relay".to_string(),
486 type_hint: "boolean".to_string(),
487 description: "Whether to relay transactions (default: false)".to_string(),
488 required: false,
489 },
490 ],
491 example: json!({
492 "type": "send_version",
493 "network": "mainnet",
494 "version": 70015,
495 "user_agent": "/NetGet:0.1.0/",
496 "start_height": 0,
497 "relay": false
498 }),
499 log_template: Some(
500 LogTemplate::new()
501 .with_info("-> BTC version v{version} {user_agent}")
502 .with_debug("BTC send_version: version={version} user_agent={user_agent} height={start_height}"),
503 ),
504 }
505}
506
507fn send_verack_action() -> ActionDefinition {
509 ActionDefinition {
510 name: "send_verack".to_string(),
511 description: "Send a Bitcoin verack message (acknowledge version)".to_string(),
512 parameters: vec![Parameter {
513 name: "network".to_string(),
514 type_hint: "string".to_string(),
515 description: "Network: 'mainnet', 'testnet', 'signet', 'regtest'".to_string(),
516 required: false,
517 }],
518 example: json!({
519 "type": "send_verack",
520 "network": "mainnet"
521 }),
522 log_template: Some(
523 LogTemplate::new()
524 .with_info("-> BTC verack")
525 .with_debug("BTC send_verack: network={network}"),
526 ),
527 }
528}
529
530fn send_ping_action() -> ActionDefinition {
532 ActionDefinition {
533 name: "send_ping".to_string(),
534 description: "Send a Bitcoin ping message".to_string(),
535 parameters: vec![
536 Parameter {
537 name: "network".to_string(),
538 type_hint: "string".to_string(),
539 description: "Network: 'mainnet', 'testnet', 'signet', 'regtest'".to_string(),
540 required: false,
541 },
542 Parameter {
543 name: "nonce".to_string(),
544 type_hint: "number".to_string(),
545 description: "Ping nonce (default: random)".to_string(),
546 required: false,
547 },
548 ],
549 example: json!({
550 "type": "send_ping",
551 "network": "mainnet",
552 "nonce": 123456789
553 }),
554 log_template: Some(
555 LogTemplate::new()
556 .with_info("-> BTC ping nonce={nonce}")
557 .with_debug("BTC send_ping: nonce={nonce} network={network}"),
558 ),
559 }
560}
561
562fn send_pong_action() -> ActionDefinition {
564 ActionDefinition {
565 name: "send_pong".to_string(),
566 description: "Send a Bitcoin pong message (response to ping)".to_string(),
567 parameters: vec![
568 Parameter {
569 name: "network".to_string(),
570 type_hint: "string".to_string(),
571 description: "Network: 'mainnet', 'testnet', 'signet', 'regtest'".to_string(),
572 required: false,
573 },
574 Parameter {
575 name: "nonce".to_string(),
576 type_hint: "number".to_string(),
577 description: "Nonce from ping message".to_string(),
578 required: true,
579 },
580 ],
581 example: json!({
582 "type": "send_pong",
583 "network": "mainnet",
584 "nonce": 123456789
585 }),
586 log_template: Some(
587 LogTemplate::new()
588 .with_info("-> BTC pong nonce={nonce}")
589 .with_debug("BTC send_pong: nonce={nonce} network={network}"),
590 ),
591 }
592}
593
594fn send_getaddr_action() -> ActionDefinition {
596 ActionDefinition {
597 name: "send_getaddr".to_string(),
598 description: "Send a Bitcoin getaddr message (request peer addresses)".to_string(),
599 parameters: vec![Parameter {
600 name: "network".to_string(),
601 type_hint: "string".to_string(),
602 description: "Network: 'mainnet', 'testnet', 'signet', 'regtest'".to_string(),
603 required: false,
604 }],
605 example: json!({
606 "type": "send_getaddr",
607 "network": "mainnet"
608 }),
609 log_template: Some(
610 LogTemplate::new()
611 .with_info("-> BTC getaddr")
612 .with_debug("BTC send_getaddr: network={network}"),
613 ),
614 }
615}
616
617fn close_this_connection_action() -> ActionDefinition {
619 ActionDefinition {
620 name: "close_this_connection".to_string(),
621 description: "Close the current Bitcoin P2P connection".to_string(),
622 parameters: vec![],
623 example: json!({
624 "type": "close_this_connection"
625 }),
626 log_template: Some(
627 LogTemplate::new()
628 .with_info("-> BTC close")
629 .with_debug("BTC close_this_connection"),
630 ),
631 }
632}
633
634pub static BITCOIN_CONNECTION_OPENED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
640 EventType::new(
641 "bitcoin_connection_opened",
642 "New Bitcoin P2P connection established (decide whether to send version or wait)",
643 json!({
644 "type": "send_version",
645 "network": "mainnet",
646 "version": 70015
647 }),
648 )
649 .with_actions(vec![send_version_action(), close_this_connection_action()])
650 .with_log_template(
651 LogTemplate::new()
652 .with_info("Bitcoin P2P connection opened from {client_ip}")
653 .with_debug("Bitcoin P2P connection from {client_ip}:{client_port}")
654 .with_trace("Bitcoin connection: {json_pretty(.)}"),
655 )
656});
657
658pub static BITCOIN_MESSAGE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
660 EventType::new(
661 "bitcoin_message_received",
662 "Bitcoin P2P message received. Reply with the message this peer expects: verack \
663 after their version, pong echoing a ping's nonce, and so on.",
664 json!({
665 "type": "send_verack",
666 "network": "mainnet"
667 }),
668 )
669 .with_alternative_example(json!({
670 "type": "send_pong",
671 "network": "mainnet",
672 "nonce": 123456789
673 }))
674 .with_parameters(vec![
675 Parameter {
676 name: "message_type".to_string(),
677 type_hint: "string".to_string(),
678 description: "Type of message (version, verack, ping, pong, getdata, etc.)".to_string(),
679 required: true,
680 },
681 Parameter {
682 name: "message".to_string(),
683 type_hint: "object".to_string(),
684 description: "Parsed message data (structure depends on message type)".to_string(),
685 required: true,
686 },
687 ])
688 .with_actions(vec![
689 send_bitcoin_message_action(),
690 send_version_action(),
691 send_verack_action(),
692 send_ping_action(),
693 send_pong_action(),
694 send_getaddr_action(),
695 close_this_connection_action(),
696 ])
697 .with_log_template(
698 LogTemplate::new()
699 .with_info("{client_ip} BTC {message_type}")
700 .with_debug("Bitcoin {message_type} from {client_ip}:{client_port}")
701 .with_trace("Bitcoin message: {json_pretty(.)}"),
702 )
703});
704
705pub fn get_bitcoin_event_types() -> Vec<EventType> {
707 vec![
708 BITCOIN_CONNECTION_OPENED_EVENT.clone(),
709 BITCOIN_MESSAGE_RECEIVED_EVENT.clone(),
710 ]
711}