Skip to main content

netget/server/bitcoin/
actions.rs

1//! Bitcoin P2P protocol actions implementation
2
3use 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
20/// Bitcoin protocol action handler
21/// The network magic for a name the model supplied, or an error naming the accepted values.
22///
23/// Five executors each carried their own copy of this match, and every copy ended
24/// `_ => Magic::BITCOIN`. So `"testnet4"`, `"Testnet"` or any typo produced **mainnet**
25/// magic silently: the reply went out with the wrong four leading bytes, the peer dropped it
26/// as a foreign network, and nothing anywhere said why. Refusing names the mistake and gives
27/// the model something to correct — and refusing is the safe direction here, because the
28/// failure it replaces is "answer on mainnet when asked for anything else".
29///
30/// A *missing* `network` still defaults to mainnet; the parameter is `required: false` and
31/// its description says so. It is only an unrecognised value that is an error.
32fn 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
54// Implement Protocol trait (common functionality)
55impl 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        // Deterministic: complete the version/verack handshake on connect, no
122        // LLM call.
123        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            // LLM mode: LLM handles Bitcoin P2P protocol
135            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            // Script mode: Code-based Bitcoin P2P handling
145            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            // Static mode: Fixed Bitcoin P2P responses
162            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
186// Implement Server trait (server-specific functionality)
187impl 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            // Extract network from startup_params (default: mainnet)
196            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            // Not offered to the model (it has `close_this_connection`), but the dashboard's
231            // "disconnect this peer" injects this generic name through the peer command task,
232            // which half-closes the write side on this result.
233            "close_connection" => Ok(ActionResult::CloseConnection),
234            _ => Err(anyhow::anyhow!("Unknown Bitcoin action: {}", action_type)),
235        }
236    }
237}
238
239impl BitcoinProtocol {
240    /// Execute send_bitcoin_message action (send raw hex-encoded message)
241    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    /// Execute send_version action
253    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        // Get optional parameters with defaults
262        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        // Create version message
295        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        // Encode to bytes
317        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    /// Execute send_verack action
327    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    /// Execute send_ping action
345    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    /// Execute send_pong action
368    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    /// Execute send_getaddr action
391    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
409/// Action definition for send_bitcoin_message
410fn 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
438/// Action definition for send_version
439fn 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            // Read by the executor but declared nowhere, so the model could not control it and
463            // every version message claimed the current wall-clock time.
464            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
507/// Action definition for send_verack
508fn 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
530/// Action definition for send_ping
531fn 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
562/// Action definition for send_pong
563fn 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
594/// Action definition for send_getaddr
595fn 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
617/// Action definition for close_this_connection
618fn 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
634// ============================================================================
635// Bitcoin Event Type Constants
636// ============================================================================
637
638/// Bitcoin connection opened event
639pub 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
658/// Bitcoin message received event
659pub 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
705/// Get Bitcoin event types
706pub fn get_bitcoin_event_types() -> Vec<EventType> {
707    vec![
708        BITCOIN_CONNECTION_OPENED_EVENT.clone(),
709        BITCOIN_MESSAGE_RECEIVED_EVENT.clone(),
710    ]
711}