Skip to main content

surfpool_core/runloops/
mod.rs

1#![allow(unused_imports, dead_code, unused_mut, unused_variables)]
2
3use std::{
4    collections::{HashMap, HashSet},
5    net::SocketAddr,
6    path::PathBuf,
7    sync::{Arc, RwLock},
8    thread::{JoinHandle, sleep},
9    time::{Duration, Instant},
10};
11
12use agave_geyser_plugin_interface::geyser_plugin_interface::{
13    GeyserPlugin, ReplicaBlockInfoV4, ReplicaBlockInfoVersions, ReplicaEntryInfoV2,
14    ReplicaEntryInfoVersions, ReplicaTransactionInfoV3, ReplicaTransactionInfoVersions, SlotStatus,
15};
16use chrono::{Local, Utc};
17use crossbeam::select;
18use crossbeam_channel::{Receiver, Sender, unbounded};
19use itertools::Itertools;
20use jsonrpc_core::MetaIoHandler;
21use jsonrpc_http_server::{DomainsValidation, ServerBuilder};
22use jsonrpc_pubsub::{PubSubHandler, Session};
23use jsonrpc_ws_server::{RequestContext, ServerBuilder as WsServerBuilder};
24use libloading::{Library, Symbol};
25use serde::Serialize;
26use solana_commitment_config::CommitmentConfig;
27use solana_message::SimpleAddressLoader;
28use solana_transaction::sanitized::{MessageHash, SanitizedTransaction};
29use solana_transaction_status::RewardsAndNumPartitions;
30use surfpool_types::{
31    BlockProductionMode, ClockCommand, ClockEvent, DEFAULT_MAINNET_RPC_URL, DataIndexingCommand,
32    SimnetCommand, SimnetConfig, SimnetEvent, SurfpoolConfig,
33};
34type PluginConstructor = unsafe fn() -> *mut dyn GeyserPlugin;
35use txtx_addon_kit::helpers::fs::FileLocation;
36
37use crate::{
38    PluginInfo,
39    rpc::{
40        self, RunloopContext, SurfpoolMiddleware, SurfpoolWebsocketMeta,
41        SurfpoolWebsocketMiddleware, accounts_data::AccountsData, accounts_scan::AccountsScan,
42        admin::AdminRpc, bank_data::BankData, full::Full, jito::Jito, minimal::Minimal,
43        surfnet_cheatcodes::SurfnetCheatcodes, ws::Rpc,
44    },
45    surfnet::{GeyserEvent, PluginCommand, locker::SurfnetSvmLocker, remote::SurfnetRemoteClient},
46};
47
48const BLOCKHASH_SLOT_TTL: u64 = 75;
49
50/// A loaded geyser plugin with all metadata needed for lifecycle management.
51/// Field order matters: `plugin` must be declared before `_library` so it drops first,
52/// since the plugin's vtable points into the loaded dynamic library.
53pub(crate) struct ManagedPlugin {
54    pub(crate) uuid: crate::Uuid,
55    pub(crate) name: String,
56    pub(crate) plugin: Box<dyn GeyserPlugin>,
57    pub(crate) _library: Library,
58    pub(crate) config_path: String,
59}
60
61pub(crate) fn load_plugin_from_config(
62    config_file: &str,
63    is_reload: bool,
64) -> Result<ManagedPlugin, String> {
65    let plugin_manifest_location = FileLocation::from_path(PathBuf::from(config_file));
66    let config_content = plugin_manifest_location
67        .read_content_as_utf8()
68        .map_err(|e| format!("Unable to read plugin config '{}': {}", config_file, e))?;
69    let result: serde_json::Value = serde_json::from_str(&config_content)
70        .map_err(|e| format!("Unable to parse plugin manifest '{}': {}", config_file, e))?;
71
72    let plugin_dylib_path = result
73        .get("libpath")
74        .and_then(|p| p.as_str())
75        .ok_or_else(|| {
76            format!(
77                "Plugin config file should include a 'libpath' field: {}",
78                plugin_manifest_location
79            )
80        })?;
81
82    let mut plugin_dylib_location = plugin_manifest_location
83        .get_parent_location()
84        .expect("path invalid");
85    plugin_dylib_location
86        .append_path(plugin_dylib_path)
87        .expect("path invalid");
88
89    let (plugin, library) = unsafe {
90        let lib = Library::new(&plugin_dylib_location.to_string())
91            .map_err(|e| format!("Unable to load plugin {}: {}", plugin_dylib_location, e))?;
92        let constructor: Symbol<PluginConstructor> = lib
93            .get(b"_create_plugin")
94            .map_err(|e| format!("Plugin missing _create_plugin symbol: {}", e))?;
95        let plugin_raw = constructor();
96        (Box::from_raw(plugin_raw), lib)
97    };
98
99    let mut plugin = plugin;
100
101    let name = plugin.name().to_string();
102    plugin
103        .on_load(config_file, is_reload)
104        .map_err(|e| format!("Failed to call on_load for plugin '{}': {}", name, e))?;
105
106    let uuid = crate::Uuid::new_v4();
107
108    Ok(ManagedPlugin {
109        uuid,
110        name,
111        plugin,
112        _library: library,
113        config_path: config_file.to_string(),
114    })
115}
116
117pub(crate) fn unload_plugin_by_name(
118    name: &str,
119    managed_plugins: &mut Vec<ManagedPlugin>,
120) -> Result<(), String> {
121    let idx = managed_plugins
122        .iter()
123        .position(|p| p.name == name)
124        .ok_or_else(|| format!("Plugin '{}' not found", name))?;
125
126    let mut plugin = managed_plugins.remove(idx);
127    plugin.plugin.on_unload();
128    // plugin and _library are dropped here in correct order
129    Ok(())
130}
131
132/// Checks if a port is available for binding.
133fn check_port_availability(addr: SocketAddr, server_type: &str) -> Result<(), String> {
134    match std::net::TcpListener::bind(addr) {
135        Ok(_listener) => Ok(()),
136        Err(e) if e.kind() == std::io::ErrorKind::AddrInUse => {
137            let msg = format!(
138                "{} port {} is already in use. Try --port or --ws-port to use a different port.",
139                server_type,
140                addr.port()
141            );
142            eprintln!("Error: {}", msg);
143            Err(msg)
144        }
145        Err(e) => {
146            let msg = format!("Failed to bind {} server to {}: {}", server_type, addr, e);
147            eprintln!("Error: {}", msg);
148            Err(msg)
149        }
150    }
151}
152
153pub async fn start_local_surfnet_runloop(
154    svm_locker: SurfnetSvmLocker,
155    config: SurfpoolConfig,
156    simnet_commands_tx: Sender<SimnetCommand>,
157    simnet_commands_rx: Receiver<SimnetCommand>,
158    geyser_events_rx: Receiver<GeyserEvent>,
159) -> Result<(), Box<dyn std::error::Error>> {
160    let Some(simnet) = config.simnets.first() else {
161        return Ok(());
162    };
163    let block_production_mode = simnet.block_production_mode.clone();
164
165    let remote_rpc_client = match simnet.offline_mode {
166        true => None,
167        false => SurfnetRemoteClient::new_unsafe(
168            simnet
169                .remote_rpc_url
170                .as_ref()
171                .unwrap_or(&DEFAULT_MAINNET_RPC_URL.to_string()),
172        ),
173    };
174
175    svm_locker.initialize(&remote_rpc_client).await?;
176
177    if let Some(remote_client) = &remote_rpc_client {
178        let _ = svm_locker
179            .simnet_events_tx()
180            .send(SimnetEvent::Connected(remote_client.client.url()));
181    }
182    let _ = svm_locker
183        .simnet_events_tx()
184        .send(SimnetEvent::EpochInfoUpdate(svm_locker.get_epoch_info()));
185
186    svm_locker.airdrop_pubkeys(simnet.airdrop_token_amount, &simnet.airdrop_addresses);
187
188    // Load snapshot accounts if provided
189    if !simnet.snapshot.is_empty() {
190        match svm_locker
191            .load_snapshot(
192                &simnet.snapshot,
193                remote_rpc_client.as_ref(),
194                CommitmentConfig::confirmed(),
195            )
196            .await
197        {
198            Ok(loaded_count) => {
199                let _ = svm_locker.with_svm_reader(|svm| {
200                    svm.simnet_events_tx.send(SimnetEvent::info(format!(
201                        "Preloaded {} accounts from snapshot(s) into SVM",
202                        loaded_count
203                    )))
204                });
205            }
206            Err(e) => {
207                let _ = svm_locker.with_svm_reader(|svm| {
208                    svm.simnet_events_tx.send(SimnetEvent::warn(format!(
209                        "Error loading snapshot accounts: {}",
210                        e
211                    )))
212                });
213            }
214        }
215    }
216
217    let simnet_events_tx_cc = svm_locker.simnet_events_tx();
218
219    let (plugin_commands_tx, plugin_commands_rx) = unbounded::<PluginCommand>();
220
221    let (_rpc_handle, _ws_handle, shutdown_rpc_servers) = start_rpc_servers_runloop(
222        &config,
223        &simnet_commands_tx,
224        svm_locker.clone(),
225        &remote_rpc_client,
226        plugin_commands_tx,
227    )
228    .await?;
229
230    let simnet_config = simnet.clone();
231
232    match start_geyser_runloop(
233        config.plugin_config_path.clone(),
234        simnet_events_tx_cc.clone(),
235        geyser_events_rx,
236        plugin_commands_rx,
237    ) {
238        Ok(_) => {}
239        Err(e) => {
240            let _ =
241                simnet_events_tx_cc.send(SimnetEvent::error(format!("Geyser plugin failed: {e}")));
242        }
243    };
244
245    let (clock_event_rx, clock_command_tx) =
246        start_clock_runloop(simnet_config.slot_time, Some(simnet_events_tx_cc.clone()));
247
248    // Emit TransactionProcessed events for each stored transaction before Ready
249    let initial_transaction_count = svm_locker.with_svm_reader(|svm| {
250        let iter_result = svm.transactions.into_iter();
251        let mut count: u64 = 0;
252
253        if let Ok(iter) = iter_result {
254            let mut events = vec![];
255            for (_, status) in iter {
256                if let Some((tx_meta, _updated_accounts)) = status.as_processed() {
257                    let signature = tx_meta.transaction.signatures[0];
258                    let err = tx_meta.meta.status.clone().err();
259
260                    // Build TransactionMetadata from stored data
261                    let meta = surfpool_types::TransactionMetadata {
262                        signature,
263                        logs: tx_meta.meta.log_messages.clone().unwrap_or_default(),
264                        inner_instructions: tx_meta
265                            .meta
266                            .inner_instructions
267                            .clone()
268                            .unwrap_or_default()
269                            .into_iter()
270                            .map(|inner_ixs| {
271                                inner_ixs
272                                    .instructions
273                                    .into_iter()
274                                    .map(|ix| solana_message::inner_instruction::InnerInstruction {
275                                        instruction: ix.instruction,
276                                        stack_height: ix.stack_height.unwrap_or(1) as u8,
277                                    })
278                                    .collect()
279                            })
280                            .collect(),
281                        compute_units_consumed: tx_meta.meta.compute_units_consumed.unwrap_or(0),
282                        return_data: tx_meta.meta.return_data.clone().unwrap_or_default(),
283                        fee: tx_meta.meta.fee,
284                    };
285
286                    events.push((
287                        tx_meta.slot,
288                        SimnetEvent::TransactionProcessed(Local::now(), meta, err.clone()),
289                    ));
290
291                    count += 1;
292                }
293            }
294            for (_, event) in events
295                .into_iter()
296                .sorted_by(|(a_slot, _), (b_slot, _)| a_slot.cmp(b_slot))
297            {
298                let _ = svm.simnet_events_tx.send(event);
299            }
300        }
301
302        count
303    });
304    let _ = simnet_events_tx_cc.send(SimnetEvent::Ready(initial_transaction_count));
305
306    // Notify geyser plugins that startup is complete
307    let _ = svm_locker.with_svm_reader(|svm| svm.geyser_events_tx.send(GeyserEvent::EndOfStartup));
308
309    start_block_production_runloop(
310        clock_event_rx,
311        clock_command_tx,
312        simnet_commands_rx,
313        simnet_commands_tx.clone(),
314        svm_locker,
315        block_production_mode,
316        &remote_rpc_client,
317        simnet_config.expiry.map(|e| e * 1000),
318        &simnet_config,
319        Some(shutdown_rpc_servers),
320    )
321    .await
322}
323
324#[allow(clippy::too_many_arguments)]
325pub async fn start_block_production_runloop(
326    clock_event_rx: Receiver<ClockEvent>,
327    clock_command_tx: Sender<ClockCommand>,
328    simnet_commands_rx: Receiver<SimnetCommand>,
329    simnet_commands_tx: Sender<SimnetCommand>,
330    svm_locker: SurfnetSvmLocker,
331    mut block_production_mode: BlockProductionMode,
332    remote_rpc_client: &Option<SurfnetRemoteClient>,
333    expiry_duration_ms: Option<u64>,
334    simnet_config: &SimnetConfig,
335    mut shutdown_rpc_servers: Option<Box<dyn FnOnce() + Send>>,
336) -> Result<(), Box<dyn std::error::Error>> {
337    let remote_client_with_commitment = remote_rpc_client.as_ref().map(|c| {
338        (
339            c.clone(),
340            solana_commitment_config::CommitmentConfig::confirmed(),
341        )
342    });
343    let mut next_scheduled_expiry_check: Option<u64> =
344        expiry_duration_ms.map(|expiry_val| Utc::now().timestamp_millis() as u64 + expiry_val);
345    let global_skip_sig_verify = simnet_config.skip_signature_verification;
346    let ix_profiling_initially_enabled = simnet_config.instruction_profiling_enabled;
347    loop {
348        let mut do_produce_block = false;
349
350        select! {
351            recv(clock_event_rx) -> msg => if let Ok(event) = msg {
352                match event {
353                    ClockEvent::Tick => {
354                        if block_production_mode.eq(&BlockProductionMode::Clock) {
355                            do_produce_block = true;
356                        }
357
358                        if let Some(expiry_ms) = expiry_duration_ms {
359                            if let Some(scheduled_time_ref) = &mut next_scheduled_expiry_check {
360                                let now_ms = Utc::now().timestamp_millis() as u64;
361                                if now_ms >= *scheduled_time_ref {
362                                    let svm = svm_locker.0.read().await;
363                                    if svm.updated_at + expiry_ms < now_ms {
364                                        let _ = simnet_commands_tx.send(SimnetCommand::Terminate(None));
365                                    } else {
366                                        *scheduled_time_ref = svm.updated_at + expiry_ms;
367                                    }
368                                }
369                            }
370                        }
371                    }
372                    ClockEvent::ExpireBlockHash => {
373                        do_produce_block = true;
374                    }
375                }
376            },
377            recv(simnet_commands_rx) -> msg => if let Ok(event) = msg {
378                match event {
379                    SimnetCommand::SlotForward(_key) => {
380                        block_production_mode = BlockProductionMode::Manual;
381                        do_produce_block = true;
382                    }
383                    SimnetCommand::SlotBackward(_key) => {
384
385                    }
386                    SimnetCommand::CommandClock(_, update) => {
387                        if let ClockCommand::UpdateSlotInterval(updated_slot_time) = update {
388                            svm_locker.with_svm_writer(|svm_writer| {
389                                svm_writer.slot_time = updated_slot_time;
390                            });
391                        }
392
393                        // Handle PauseWithConfirmation specially
394                        if let ClockCommand::PauseWithConfirmation(response_tx) = update {
395                            // Get current slot and slot_time before pausing
396                            let (current_slot, slot_time) = svm_locker.with_svm_reader(|svm_reader| {
397                                (svm_reader.latest_epoch_info.absolute_slot, svm_reader.slot_time)
398                            });
399
400                            // Send Pause to clock runloop
401                            let _ = clock_command_tx.send(ClockCommand::Pause);
402
403                            // Give the clock time to process the pause command
404                            tokio::time::sleep(tokio::time::Duration::from_millis(slot_time / 2)).await;
405
406                            // Loop and check if the slot has stopped advancing
407                            let max_attempts = 10;
408                            let mut attempts = 0;
409                            loop {
410                                tokio::time::sleep(tokio::time::Duration::from_millis(slot_time)).await;
411
412                                let new_slot = svm_locker.with_svm_reader(|svm_reader| {
413                                    svm_reader.latest_epoch_info.absolute_slot
414                                });
415
416                                // If slot hasn't changed, clock has stopped
417                                if new_slot == current_slot || attempts >= max_attempts {
418                                    break;
419                                }
420
421                                attempts += 1;
422                            }
423
424                            // Read epoch info after clock has stopped
425                            let epoch_info = svm_locker.with_svm_reader(|svm_reader| {
426                                svm_reader.latest_epoch_info.clone()
427                            });
428                            // Send response
429                            let _ = response_tx.send(epoch_info);
430                        } else {
431                            let _ = clock_command_tx.send(update);
432                        }
433                        continue
434                    }
435                    SimnetCommand::UpdateInternalClock(_, clock) => {
436                        // Confirm the current block to materialize any scheduled overrides for this slot
437                        if let Err(e) = svm_locker.confirm_current_block(&remote_client_with_commitment).await {
438                            let _ = svm_locker.simnet_events_tx().send(SimnetEvent::error(format!(
439                                "Failed to confirm block after time travel: {}", e
440                            )));
441                        }
442
443                        svm_locker.with_svm_writer(|svm_writer| {
444                            svm_writer.inner.set_sysvar(&clock);
445                            svm_writer.updated_at = clock.unix_timestamp as u64 * 1_000;
446                            svm_writer.latest_epoch_info.absolute_slot = clock.slot;
447                            svm_writer.latest_epoch_info.epoch = clock.epoch;
448                            svm_writer.latest_epoch_info.slot_index = clock.slot;
449                            svm_writer.latest_epoch_info.epoch = clock.epoch;
450                            svm_writer.latest_epoch_info.absolute_slot = clock.slot + clock.epoch * svm_writer.latest_epoch_info.slots_in_epoch;
451                            let _ = svm_writer.simnet_events_tx.send(SimnetEvent::SystemClockUpdated(clock));
452                        });
453                    }
454                    SimnetCommand::UpdateInternalClockWithConfirmation(_, clock, response_tx) => {
455                        // Confirm the current block to materialize any scheduled overrides for this slot
456                        if let Err(e) = svm_locker.confirm_current_block(&remote_client_with_commitment).await {
457                            let _ = svm_locker.simnet_events_tx().send(SimnetEvent::error(format!(
458                                "Failed to confirm block after time travel: {}", e
459                            )));
460                        }
461
462                        let epoch_info = svm_locker.with_svm_writer(|svm_writer| {
463                            svm_writer.inner.set_sysvar(&clock);
464                            svm_writer.updated_at = clock.unix_timestamp as u64 * 1_000;
465                            svm_writer.latest_epoch_info.absolute_slot = clock.slot;
466                            svm_writer.latest_epoch_info.epoch = clock.epoch;
467                            svm_writer.latest_epoch_info.slot_index = clock.slot;
468                            svm_writer.latest_epoch_info.epoch = clock.epoch;
469                            svm_writer.latest_epoch_info.absolute_slot = clock.slot + clock.epoch * svm_writer.latest_epoch_info.slots_in_epoch;
470                            let _ = svm_writer.simnet_events_tx.send(SimnetEvent::SystemClockUpdated(clock));
471                            svm_writer.latest_epoch_info.clone()
472                        });
473
474                        // Send confirmation back
475                        let _ = response_tx.send(epoch_info);
476                    }
477                    SimnetCommand::UpdateBlockProductionMode(update) => {
478                        block_production_mode = update;
479                        continue
480                    }
481                    SimnetCommand::ProcessTransaction(_key, transaction, status_tx, skip_preflight, skip_sig_verify_override) => {
482                       let skip_sig_verify = skip_sig_verify_override.unwrap_or(global_skip_sig_verify);
483                       let sigverify = !skip_sig_verify;
484                       if let Err(e) = svm_locker.process_transaction(&remote_client_with_commitment, transaction, status_tx, skip_preflight, sigverify).await {
485                            let _ = svm_locker.simnet_events_tx().send(SimnetEvent::error(format!("Failed to process transaction: {}", e)));
486                       }
487                       if block_production_mode.eq(&BlockProductionMode::Transaction) {
488                           do_produce_block = true;
489                       }
490                    }
491                    SimnetCommand::Terminate(_) => {
492                        // Explicitly shutdown storage to trigger WAL checkpoint before exiting
493                        svm_locker.shutdown();
494                        // Close RPC servers on a separate thread to avoid dropping
495                        // Tokio runtimes from within an async context
496                        if let Some(shutdown_fn) = shutdown_rpc_servers.take() {
497                            std::thread::spawn(shutdown_fn);
498                        }
499                        break;
500                    }
501                    SimnetCommand::StartRunbookExecution(runbook_id) => {
502                        svm_locker.start_runbook_execution(runbook_id);
503                    }
504
505                    SimnetCommand::CompleteRunbookExecution(runbook_id, error) => {
506                        svm_locker.complete_runbook_execution(runbook_id, error, ix_profiling_initially_enabled);
507                    }
508                    SimnetCommand::FetchRemoteAccounts(pubkeys, remote_url) => {
509                        let remote_client = SurfnetRemoteClient::new_unsafe(&remote_url);
510                        if let Some(remote_client) = remote_client {
511                              match svm_locker.get_multiple_accounts_with_remote_fallback(&remote_client, &pubkeys, CommitmentConfig::confirmed()).await {
512                                 Ok(account_updates) => {
513                                     svm_locker.write_multiple_account_updates(&account_updates.inner);
514                                 }
515                                 Err(e) => {
516                                     svm_locker.simnet_events_tx().try_send(SimnetEvent::error(format!("Failed to fetch remote accounts {:?}: {}", pubkeys, e))).ok();
517                                 }
518                             };
519                        }
520                    }
521                    SimnetCommand::AirdropProcessed => {
522                       if block_production_mode.eq(&BlockProductionMode::Transaction) {
523                           do_produce_block = true;
524                       }
525                    }
526                }
527            },
528        }
529
530        {
531            if do_produce_block {
532                svm_locker
533                    .confirm_current_block(&remote_client_with_commitment)
534                    .await?;
535            }
536        }
537    }
538    Ok(())
539}
540
541pub fn start_clock_runloop(
542    mut slot_time: u64,
543    simnet_events_tx: Option<Sender<SimnetEvent>>,
544) -> (Receiver<ClockEvent>, Sender<ClockCommand>) {
545    let (clock_event_tx, clock_event_rx) = unbounded::<ClockEvent>();
546    let (clock_command_tx, clock_command_rx) = unbounded::<ClockCommand>();
547
548    let _handle = hiro_system_kit::thread_named("clock").spawn(move || {
549        let mut enabled = true;
550        let mut block_hash_timeout = Instant::now();
551
552        loop {
553            match clock_command_rx.try_recv() {
554                Ok(ClockCommand::Pause) => {
555                    enabled = false;
556                    if let Some(ref simnet_events_tx) = simnet_events_tx {
557                        let _ =
558                            simnet_events_tx.send(SimnetEvent::ClockUpdate(ClockCommand::Pause));
559                    }
560                }
561                Ok(ClockCommand::Resume) => {
562                    enabled = true;
563                    if let Some(ref simnet_events_tx) = simnet_events_tx {
564                        let _ =
565                            simnet_events_tx.send(SimnetEvent::ClockUpdate(ClockCommand::Resume));
566                    }
567                }
568                Ok(ClockCommand::Toggle) => {
569                    enabled = !enabled;
570                    if let Some(ref simnet_events_tx) = simnet_events_tx {
571                        let _ =
572                            simnet_events_tx.send(SimnetEvent::ClockUpdate(ClockCommand::Toggle));
573                    }
574                }
575                Ok(ClockCommand::UpdateSlotInterval(updated_slot_time)) => {
576                    slot_time = updated_slot_time;
577                }
578                Ok(ClockCommand::PauseWithConfirmation(_)) => {
579                    // This should be handled in the block production runloop, not here
580                    // If it reaches here, just treat it as a regular Pause
581                    enabled = false;
582                    if let Some(ref simnet_events_tx) = simnet_events_tx {
583                        let _ =
584                            simnet_events_tx.send(SimnetEvent::ClockUpdate(ClockCommand::Pause));
585                    }
586                }
587                Err(_e) => {}
588            }
589            sleep(Duration::from_millis(slot_time));
590            if enabled {
591                let _ = clock_event_tx.send(ClockEvent::Tick);
592                // Todo: the block expiration is not completely accurate.
593                if block_hash_timeout.elapsed()
594                    > Duration::from_millis(BLOCKHASH_SLOT_TTL * slot_time)
595                {
596                    let _ = clock_event_tx.send(ClockEvent::ExpireBlockHash);
597                    block_hash_timeout = Instant::now();
598                }
599            }
600        }
601    });
602
603    (clock_event_rx, clock_command_tx)
604}
605
606fn start_geyser_runloop(
607    plugin_config_paths: Vec<PathBuf>,
608    simnet_events_tx: Sender<SimnetEvent>,
609    geyser_events_rx: Receiver<GeyserEvent>,
610    plugin_commands_rx: Receiver<PluginCommand>,
611) -> Result<JoinHandle<Result<(), String>>, String> {
612    let handle: JoinHandle<Result<(), String>> = hiro_system_kit::thread_named("Geyser Plugins Handler").spawn(move || {
613        let mut indexing_enabled = false;
614
615        let mut managed_plugins: Vec<ManagedPlugin> = vec![];
616
617        // helper to log errors that can't be propagated
618        let log_error = |msg:String|{
619            let _ = simnet_events_tx.send(SimnetEvent::error(msg));
620        };
621
622        let log_warn = |msg:String|{
623            let _ = simnet_events_tx.send(SimnetEvent::warn(msg));
624        };
625
626        let log_info = |msg:String|{
627            let _ = simnet_events_tx.send(SimnetEvent::info(msg));
628        };
629
630        // Load plugins from config paths at startup
631        for plugin_config_path in plugin_config_paths.iter() {
632            let config_file = plugin_config_path.to_string_lossy().to_string();
633            match load_plugin_from_config(&config_file, false) {
634                Ok(managed) => {
635                    indexing_enabled = true;
636                    log_info(format!("Loaded geyser plugin '{}'", managed.name));
637                    managed_plugins.push(managed);
638                }
639                Err(e) => {
640                    log_error(format!("Failed to load geyser plugin from '{}': {}", config_file, e));
641                }
642            }
643        }
644
645        let err = loop {
646            use agave_geyser_plugin_interface::geyser_plugin_interface::{ReplicaAccountInfoV3, ReplicaAccountInfoVersions};
647
648            use crate::types::GeyserAccountUpdate;
649
650            select! {
651                recv(geyser_events_rx) -> msg => match msg {
652                    Err(e) => {
653                        break format!("Failed to read new transaction to send to Geyser plugin: {e}");
654                    },
655                    Ok(GeyserEvent::NotifyTransaction(transaction_with_status_meta, versioned_transaction)) => {
656
657                        if !indexing_enabled {
658                            continue;
659                        }
660
661                        let transaction = match versioned_transaction {
662                            Some(tx) => tx,
663                            None => {
664                                log_warn("Unable to index sanitized transaction".to_string());
665                                continue;
666                            }
667                        };
668
669                        let transaction_replica = ReplicaTransactionInfoV3 {
670                            signature: &transaction.signatures[0],
671                            is_vote: false,
672                            transaction: &transaction,
673                            transaction_status_meta: &transaction_with_status_meta.meta,
674                            index: 0,
675                            message_hash: &transaction.message.hash(),
676                        };
677
678                        for plugin in managed_plugins.iter().map(|p| &*p.plugin) {
679                            if let Err(e) = plugin.notify_transaction(ReplicaTransactionInfoVersions::V0_0_3(&transaction_replica), transaction_with_status_meta.slot) {
680                                log_error(format!("Failed to notify Geyser plugin of new transaction: {:?}", e))
681                            };
682                        }
683                    }
684                    Ok(GeyserEvent::UpdateAccount(account_update)) => {
685                        let GeyserAccountUpdate {
686                            pubkey,
687                            account,
688                            slot,
689                            sanitized_transaction,
690                            write_version,
691                        } = account_update;
692
693                        let account_replica = ReplicaAccountInfoV3 {
694                            pubkey: pubkey.as_ref(),
695                            lamports: account.lamports,
696                            owner: account.owner.as_ref(),
697                            executable: account.executable,
698                            rent_epoch: account.rent_epoch,
699                            data: account.data.as_ref(),
700                            write_version,
701                            txn: sanitized_transaction.as_ref(),
702                        };
703
704                        for plugin in managed_plugins.iter().map(|p| &*p.plugin) {
705                            if let Err(e) = plugin.update_account(ReplicaAccountInfoVersions::V0_0_3(&account_replica), slot, false) {
706                                log_error(format!("Failed to update account in Geyser plugin: {:?}", e));
707                            }
708                        }
709                    }
710                    Ok(GeyserEvent::StartupAccountUpdate(account_update)) => {
711                        let GeyserAccountUpdate {
712                            pubkey,
713                            account,
714                            slot,
715                            sanitized_transaction,
716                            write_version,
717                        } = account_update;
718
719                        let account_replica = ReplicaAccountInfoV3 {
720                            pubkey: pubkey.as_ref(),
721                            lamports: account.lamports,
722                            owner: account.owner.as_ref(),
723                            executable: account.executable,
724                            rent_epoch: account.rent_epoch,
725                            data: account.data.as_ref(),
726                            write_version,
727                            txn: sanitized_transaction.as_ref(),
728                        };
729
730                        // Send startup account updates with is_startup=true
731                        for plugin in managed_plugins.iter().map(|p| &*p.plugin) {
732                            if let Err(e) = plugin.update_account(ReplicaAccountInfoVersions::V0_0_3(&account_replica), slot, true) {
733                                log_error(format!("Failed to send startup account update to Geyser plugin: {:?}", e));
734                            }
735                        }
736                    }
737                    Ok(GeyserEvent::EndOfStartup) => {
738                        for plugin in managed_plugins.iter().map(|p| &*p.plugin) {
739                            if let Err(e) = plugin.notify_end_of_startup() {
740                                let _ = simnet_events_tx.send(SimnetEvent::error(format!("Failed to notify end of startup to Geyser plugin: {:?}", e)));
741                            }
742                        }
743                    }
744                    Ok(GeyserEvent::UpdateSlotStatus { slot, parent, status }) => {
745                        let slot_status = match status {
746                            crate::surfnet::GeyserSlotStatus::Processed => SlotStatus::Processed,
747                            crate::surfnet::GeyserSlotStatus::Confirmed => SlotStatus::Confirmed,
748                            crate::surfnet::GeyserSlotStatus::Rooted => SlotStatus::Rooted,
749                        };
750
751                        for plugin in managed_plugins.iter().map(|p| &*p.plugin) {
752                            if let Err(e) = plugin.update_slot_status(slot, parent, &slot_status) {
753                                let _ = simnet_events_tx.send(SimnetEvent::error(format!("Failed to update slot status in Geyser plugin: {:?}", e)));
754                            }
755                        }
756                    }
757                    Ok(GeyserEvent::NotifyBlockMetadata(block_metadata)) => {
758                        let rewards = RewardsAndNumPartitions {
759                            rewards: vec![],
760                            num_partitions: None,
761                        };
762
763                        let block_info = ReplicaBlockInfoV4 {
764                            slot: block_metadata.slot,
765                            blockhash: &block_metadata.blockhash,
766                            rewards: &rewards,
767                            block_time: block_metadata.block_time,
768                            block_height: block_metadata.block_height,
769                            parent_slot: block_metadata.parent_slot,
770                            parent_blockhash: &block_metadata.parent_blockhash,
771                            executed_transaction_count: block_metadata.executed_transaction_count,
772                            entry_count: block_metadata.entry_count,
773                        };
774
775                        for plugin in managed_plugins.iter().map(|p| &*p.plugin) {
776                            if let Err(e) = plugin.notify_block_metadata(ReplicaBlockInfoVersions::V0_0_4(&block_info)) {
777                                let _ = simnet_events_tx.send(SimnetEvent::error(format!("Failed to notify block metadata to Geyser plugin: {:?}", e)));
778                            }
779                        }
780                    }
781                    Ok(GeyserEvent::NotifyEntry(entry_info)) => {
782                        let entry_replica = ReplicaEntryInfoV2 {
783                            slot: entry_info.slot,
784                            index: entry_info.index,
785                            num_hashes: entry_info.num_hashes,
786                            hash: &entry_info.hash,
787                            executed_transaction_count: entry_info.executed_transaction_count,
788                            starting_transaction_index: entry_info.starting_transaction_index,
789                        };
790
791                        for plugin in managed_plugins.iter().map(|p| &*p.plugin) {
792                            if let Err(e) = plugin.notify_entry(ReplicaEntryInfoVersions::V0_0_2(&entry_replica)) {
793                                let _ = simnet_events_tx.send(SimnetEvent::error(format!("Failed to notify entry to Geyser plugin: {:?}", e)));
794                            }
795                        }
796                    }
797                },
798                recv(plugin_commands_rx) -> cmd => match cmd {
799                    Err(_) => {
800                        break "Plugin command channel closed".to_string();
801                    }
802                    Ok(PluginCommand::Load { config_file, response_tx }) => {
803                        let result = match load_plugin_from_config(&config_file, false) {
804                            Ok(managed) => {
805                                indexing_enabled = true;
806                                let info = PluginInfo {
807                                    plugin_name: managed.name.clone(),
808                                    uuid: managed.uuid.to_string(),
809                                };
810                                log_info(format!("Loaded geyser plugin '{}'", managed.name));
811                                managed_plugins.push(managed);
812                                Ok(info)
813                            }
814                            Err(e) => {
815                                log_error(format!("Failed to load plugin: {}", e));
816                                Err(e)
817                            }
818                        };
819                        let _ = response_tx.send(result);
820                    }
821                    Ok(PluginCommand::Unload { name, response_tx }) => {
822                        let result = unload_plugin_by_name(&name, &mut managed_plugins);
823                        if result.is_ok() {
824                            log_info(format!("Unloaded geyser plugin '{}'", name));
825                        }
826                        let _ = response_tx.send(result);
827                    }
828                    Ok(PluginCommand::Reload { name, config_file, response_tx }) => {
829                        let result = unload_plugin_by_name(&name, &mut managed_plugins)
830                            .and_then(|_| {
831                                let managed = load_plugin_from_config(&config_file, true)?;
832                                log_info(format!("Reloaded geyser plugin '{}'", managed.name));
833                                managed_plugins.push(managed);
834                                Ok(())
835                            });
836                        if let Err(ref e) = result {
837                            log_error(format!("Failed to reload plugin '{}': {}", name, e));
838                        }
839                        let _ = response_tx.send(result);
840                    }
841                    Ok(PluginCommand::List { response_tx }) => {
842                        let infos = managed_plugins.iter().map(|p| PluginInfo {
843                            plugin_name: p.name.clone(),
844                            uuid: p.uuid.to_string(),
845                        }).collect();
846                        let _ = response_tx.send(infos);
847                    }
848                }
849            }
850        };
851        Err(err)
852    }).map_err(|e| format!("Failed to spawn Geyser Plugins Handler thread: {:?}", e))?;
853    Ok(handle)
854}
855
856async fn start_rpc_servers_runloop(
857    config: &SurfpoolConfig,
858    simnet_commands_tx: &Sender<SimnetCommand>,
859    svm_locker: SurfnetSvmLocker,
860    remote_rpc_client: &Option<SurfnetRemoteClient>,
861    plugin_commands_tx: Sender<PluginCommand>,
862) -> Result<(JoinHandle<()>, JoinHandle<()>, Box<dyn FnOnce() + Send>), String> {
863    let rpc_addr: SocketAddr = config
864        .rpc
865        .get_rpc_base_url()
866        .parse()
867        .map_err(|e: std::net::AddrParseError| e.to_string())?;
868    let ws_addr: SocketAddr = config
869        .rpc
870        .get_ws_base_url()
871        .parse()
872        .map_err(|e: std::net::AddrParseError| e.to_string())?;
873
874    check_port_availability(rpc_addr, "RPC")?;
875    check_port_availability(ws_addr, "WebSocket")?;
876
877    let simnet_events_tx = svm_locker.simnet_events_tx();
878
879    let middleware = SurfpoolMiddleware::new(
880        svm_locker,
881        simnet_commands_tx,
882        &config.rpc,
883        remote_rpc_client,
884        plugin_commands_tx,
885    );
886
887    let (rpc_handle, rpc_close_handle) =
888        start_http_rpc_server_runloop(config, middleware.clone(), simnet_events_tx.clone()).await?;
889    let (ws_handle, ws_close_handle) =
890        start_ws_rpc_server_runloop(config, middleware, simnet_events_tx).await?;
891
892    let shutdown_rpc_servers: Box<dyn FnOnce() + Send> = Box::new(move || {
893        rpc_close_handle.close();
894        ws_close_handle.close();
895    });
896
897    Ok((rpc_handle, ws_handle, shutdown_rpc_servers))
898}
899
900async fn start_http_rpc_server_runloop(
901    config: &SurfpoolConfig,
902    middleware: SurfpoolMiddleware,
903    simnet_events_tx: Sender<SimnetEvent>,
904) -> Result<(JoinHandle<()>, jsonrpc_http_server::CloseHandle), String> {
905    let server_bind: SocketAddr = config
906        .rpc
907        .get_rpc_base_url()
908        .parse::<SocketAddr>()
909        .map_err(|e| e.to_string())?;
910
911    let mut io = MetaIoHandler::with_middleware(middleware);
912
913    // Cheatcodes should be added first and are a special case. One of the cheatcode methods needs access
914    // to the list of all cheatcode methods. The IoHandler allows us to iterate over them, so we're
915    // initializing and storing that list here.
916    {
917        let cheatcode_methods: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(vec![]));
918        let mut cheatcodes_impl = rpc::surfnet_cheatcodes::SurfnetCheatcodesRpc {
919            registered_methods: Arc::clone(&cheatcode_methods),
920        };
921        io.extend_with(cheatcodes_impl.to_delegate());
922
923        cheatcode_methods
924            .write()
925            .unwrap()
926            .extend(io.iter().map(|(n, _)| n.clone()).collect::<Vec<String>>());
927    }
928
929    io.extend_with(rpc::minimal::SurfpoolMinimalRpc.to_delegate());
930    io.extend_with(rpc::full::SurfpoolFullRpc.to_delegate());
931    io.extend_with(rpc::jito::SurfpoolJitoRpc.to_delegate());
932    io.extend_with(rpc::accounts_data::SurfpoolAccountsDataRpc.to_delegate());
933    io.extend_with(rpc::accounts_scan::SurfpoolAccountsScanRpc.to_delegate());
934    io.extend_with(rpc::bank_data::SurfpoolBankDataRpc.to_delegate());
935    io.extend_with(rpc::admin::SurfpoolAdminRpc.to_delegate());
936
937    let (close_handle_tx, close_handle_rx) =
938        crossbeam_channel::bounded::<Result<jsonrpc_http_server::CloseHandle, String>>(1);
939
940    let _handle = hiro_system_kit::thread_named("RPC Handler")
941        .spawn(move || {
942            let server = match ServerBuilder::new(io)
943                .cors(DomainsValidation::Disabled)
944                .threads(6)
945                .max_request_body_size(15 * 1024 * 1024)
946                .start_http(&server_bind)
947            {
948                Ok(server) => server,
949                Err(e) => {
950                    let error = format!("Failed to start RPC server: {:?}", e);
951                    let _ = close_handle_tx.send(Err(error.clone()));
952                    let _ = simnet_events_tx.send(SimnetEvent::Aborted(error));
953                    return;
954                }
955            };
956
957            let _ = close_handle_tx.send(Ok(server.close_handle()));
958            server.wait();
959            let _ = simnet_events_tx.send(SimnetEvent::Shutdown);
960        })
961        .map_err(|e| format!("Failed to spawn RPC Handler thread: {:?}", e))?;
962
963    let close_handle = close_handle_rx
964        .recv()
965        .map_err(|_| "Failed to receive HTTP RPC server startup result".to_string())??;
966
967    Ok((_handle, close_handle))
968}
969async fn start_ws_rpc_server_runloop(
970    config: &SurfpoolConfig,
971    middleware: SurfpoolMiddleware,
972    simnet_events_tx: Sender<SimnetEvent>,
973) -> Result<(JoinHandle<()>, jsonrpc_ws_server::CloseHandle), String> {
974    let ws_server_bind: SocketAddr = config
975        .rpc
976        .get_ws_base_url()
977        .parse::<SocketAddr>()
978        .map_err(|e| e.to_string())?;
979
980    let uid = std::sync::atomic::AtomicUsize::new(0);
981    let ws_middleware = SurfpoolWebsocketMiddleware::new(middleware.clone(), None);
982
983    let mut rpc_io = PubSubHandler::new(MetaIoHandler::with_middleware(ws_middleware));
984
985    let (close_handle_tx, close_handle_rx) =
986        crossbeam_channel::bounded::<Result<jsonrpc_ws_server::CloseHandle, String>>(1);
987
988    let _ws_handle = hiro_system_kit::thread_named("WebSocket RPC Handler")
989        .spawn(move || {
990            // The pubsub handler needs to be able to run async tasks, so we create a Tokio runtime here
991            let runtime = tokio::runtime::Builder::new_multi_thread()
992                .enable_all()
993                .build()
994                .expect("Failed to build Tokio runtime");
995
996            let tokio_handle = runtime.handle();
997            rpc_io.extend_with(
998                rpc::ws::SurfpoolWsRpc {
999                    uid,
1000                    signature_subscription_map: Arc::new(RwLock::new(HashMap::new())),
1001                    account_subscription_map: Arc::new(RwLock::new(HashMap::new())),
1002                    program_subscription_map: Arc::new(RwLock::new(HashMap::new())),
1003                    slot_subscription_map: Arc::new(RwLock::new(HashMap::new())),
1004                    logs_subscription_map: Arc::new(RwLock::new(HashMap::new())),
1005                    snapshot_subscription_map: Arc::new(RwLock::new(HashMap::new())),
1006                    tokio_handle: tokio_handle.clone(),
1007                }
1008                .to_delegate(),
1009            );
1010            runtime.block_on(async move {
1011                let server = match WsServerBuilder::new(rpc_io)
1012                    .session_meta_extractor(move |ctx: &RequestContext| {
1013                        // Create meta from context + session
1014                        let runloop_context = RunloopContext {
1015                            id: None,
1016                            svm_locker: middleware.surfnet_svm.clone(),
1017                            simnet_commands_tx: middleware.simnet_commands_tx.clone(),
1018                            remote_rpc_client: middleware.remote_rpc_client.clone(),
1019                            rpc_config: middleware.config.clone(),
1020                            cheatcode_config: middleware.cheatcode_config.clone(),
1021                            plugin_commands_tx: middleware.plugin_commands_tx.clone(),
1022                        };
1023                        Some(SurfpoolWebsocketMeta::new(
1024                            runloop_context,
1025                            Some(Arc::new(Session::new(ctx.sender()))),
1026                        ))
1027                    })
1028                    .start(&ws_server_bind)
1029                {
1030                    Ok(server) => server,
1031                    Err(e) => {
1032                        let error = format!("Failed to start WebSocket RPC server: {:?}", e);
1033                        let _ = close_handle_tx.send(Err(error.clone()));
1034                        let _ = simnet_events_tx.send(SimnetEvent::Aborted(error));
1035                        return;
1036                    }
1037                };
1038                let _ = close_handle_tx.send(Ok(server.close_handle()));
1039                // The server itself is blocking, so spawn it in a separate thread if needed
1040                tokio::task::spawn_blocking(move || {
1041                    server.wait().unwrap();
1042                })
1043                .await
1044                .ok();
1045
1046                let _ = simnet_events_tx.send(SimnetEvent::Shutdown);
1047            });
1048        })
1049        .map_err(|e| format!("Failed to spawn WebSocket RPC Handler thread: {:?}", e))?;
1050
1051    let close_handle = close_handle_rx
1052        .recv()
1053        .map_err(|_| "Failed to receive WebSocket RPC server startup result".to_string())??;
1054
1055    Ok((_ws_handle, close_handle))
1056}