Skip to main content

surfpool_core/surfnet/
mod.rs

1use std::{collections::HashMap, fmt::Display};
2
3use crossbeam_channel::Sender;
4use jsonrpc_core::Result as RpcError;
5use locker::SurfnetSvmLocker;
6use solana_account::Account;
7use solana_account_decoder::{UiAccount, UiAccountEncoding};
8use solana_client::{
9    rpc_config::RpcTransactionLogsFilter,
10    rpc_filter::RpcFilterType,
11    rpc_response::{RpcKeyedAccount, RpcLogsResponse},
12};
13use solana_clock::Slot;
14use solana_commitment_config::CommitmentLevel;
15use solana_epoch_info::EpochInfo;
16use solana_pubkey::Pubkey;
17use solana_signature::Signature;
18use solana_transaction::versioned::VersionedTransaction;
19use solana_transaction_error::TransactionError;
20use solana_transaction_status::{EncodedConfirmedTransactionWithStatusMeta, TransactionStatus};
21use svm::SurfnetSvm;
22
23use crate::{
24    PluginInfo,
25    error::{SurfpoolError, SurfpoolResult},
26    types::{GeyserAccountUpdate, TransactionWithStatusMeta},
27};
28
29pub mod locker;
30pub mod noop_program;
31pub mod remote;
32pub mod surfnet_lite_svm;
33pub mod svm;
34
35pub const FINALIZATION_SLOT_THRESHOLD: u64 = 31;
36pub const SLOTS_PER_EPOCH: u64 = 432000;
37
38pub type AccountFactory = Box<dyn Fn(SurfnetSvmLocker) -> GetAccountResult + Send + Sync>;
39
40/// Slot status for geyser plugin notifications.
41/// Mirrors `agave_geyser_plugin_interface::geyser_plugin_interface::SlotStatus`.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum GeyserSlotStatus {
44    /// Slot is being processed
45    Processed,
46    /// Slot has been rooted (finalized)
47    Rooted,
48    /// Slot has been confirmed
49    Confirmed,
50}
51
52/// Block metadata for geyser plugin notifications.
53#[derive(Debug, Clone)]
54pub struct GeyserBlockMetadata {
55    pub slot: Slot,
56    pub blockhash: String,
57    pub parent_slot: Slot,
58    pub parent_blockhash: String,
59    pub block_time: Option<i64>,
60    pub block_height: Option<u64>,
61    pub executed_transaction_count: u64,
62    pub entry_count: u64,
63}
64
65/// Entry info for geyser plugin notifications.
66/// Surfpool emits one entry per block (simplified model).
67#[derive(Debug, Clone)]
68pub struct GeyserEntryInfo {
69    pub slot: Slot,
70    pub index: usize,
71    pub num_hashes: u64,
72    pub hash: Vec<u8>,
73    pub executed_transaction_count: u64,
74    pub starting_transaction_index: usize,
75}
76
77#[allow(clippy::large_enum_variant)]
78pub enum GeyserEvent {
79    NotifyTransaction(TransactionWithStatusMeta, Option<VersionedTransaction>),
80    UpdateAccount(GeyserAccountUpdate),
81    /// Account update sent at startup (before block production begins).
82    /// These updates should be sent to geyser plugins with is_startup=true.
83    StartupAccountUpdate(GeyserAccountUpdate),
84    /// Notify plugins that startup is complete.
85    EndOfStartup,
86    /// Update slot status (processed, confirmed, rooted/finalized).
87    UpdateSlotStatus {
88        slot: Slot,
89        parent: Option<Slot>,
90        status: GeyserSlotStatus,
91    },
92    /// Notify plugins of block metadata.
93    NotifyBlockMetadata(GeyserBlockMetadata),
94    /// Notify plugins of entry execution.
95    NotifyEntry(GeyserEntryInfo),
96}
97
98/// Commands sent from RPC to the geyser runloop for plugin management.
99pub enum PluginCommand {
100    Load {
101        config_file: String,
102        response_tx: Sender<Result<PluginInfo, String>>,
103    },
104    Unload {
105        name: String,
106        response_tx: Sender<Result<(), String>>,
107    },
108    Reload {
109        name: String,
110        config_file: String,
111        response_tx: Sender<Result<(), String>>,
112    },
113    List {
114        response_tx: Sender<Vec<PluginInfo>>,
115    },
116}
117
118#[derive(Debug, Eq, PartialEq, Hash, Clone)]
119pub struct BlockIdentifier {
120    pub index: u64,
121    pub hash: String,
122}
123
124impl BlockIdentifier {
125    pub fn zero() -> Self {
126        Self::new(
127            0,
128            "0000000000000000000000000000000000000000000000000000000000000000",
129        )
130    }
131
132    pub fn new(index: u64, hash: &str) -> Self {
133        Self {
134            index,
135            hash: hash.to_string(),
136        }
137    }
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct BlockHeader {
142    pub hash: String,
143    pub previous_blockhash: String,
144    pub parent_slot: Slot,
145    pub block_time: i64,
146    pub block_height: u64,
147    pub signatures: Vec<Signature>,
148}
149
150#[derive(PartialEq, Eq, Clone)]
151pub enum SurfnetDataConnection {
152    Offline,
153    Connected(String, EpochInfo),
154}
155
156pub type SignatureSubscriptionData = (
157    SignatureSubscriptionType,
158    Sender<(Slot, Option<TransactionError>)>,
159);
160
161pub type AccountSubscriptionData =
162    HashMap<Pubkey, Vec<(Option<UiAccountEncoding>, Sender<UiAccount>)>>;
163
164pub type ProgramSubscriptionData = HashMap<
165    Pubkey,
166    Vec<(
167        Option<UiAccountEncoding>,
168        Option<Vec<RpcFilterType>>,
169        Sender<RpcKeyedAccount>,
170    )>,
171>;
172
173pub type LogsSubscriptionData = (
174    CommitmentLevel,
175    RpcTransactionLogsFilter,
176    Sender<(Slot, RpcLogsResponse)>,
177);
178
179pub type SnapshotSubscriptionData = Sender<SnapshotImportNotification>;
180
181#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
182pub struct SnapshotImportNotification {
183    pub snapshot_id: String,
184    pub status: SnapshotImportStatus,
185    pub accounts_loaded: u64,
186    pub total_accounts: u64,
187    pub error: Option<String>,
188}
189
190#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
191pub enum SnapshotImportStatus {
192    Started,
193    InProgress,
194    Completed,
195    Failed,
196}
197
198#[derive(Debug, Clone, PartialEq)]
199pub enum SignatureSubscriptionType {
200    Received,
201    Commitment(CommitmentLevel),
202}
203
204impl Display for SignatureSubscriptionType {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        match self {
207            SignatureSubscriptionType::Received => write!(f, "received"),
208            SignatureSubscriptionType::Commitment(level) => write!(f, "{level}"),
209        }
210    }
211}
212
213type DoUpdateSvm = bool;
214
215#[derive(Clone, Debug)]
216/// Represents the result of a get_account operation.
217pub enum GetAccountResult {
218    /// Represents that the account was not found.
219    None(Pubkey),
220    /// Represents that the account was found.
221    /// The `DoUpdateSvm` flag indicates whether the SVM should be updated after this account is found.
222    /// This is useful for cases where the account was fetched from a remote source and needs to be
223    /// updated in the SVM to reflect the latest state. However, when the account is found locally,
224    /// it likely does not need to be updated in the SVM.
225    FoundAccount(Pubkey, Account, DoUpdateSvm),
226    FoundProgramAccount((Pubkey, Account), (Pubkey, Option<Account>)),
227    FoundTokenAccount((Pubkey, Account), (Pubkey, Option<Account>)),
228}
229
230impl GetAccountResult {
231    pub fn expected_data(&self) -> &Vec<u8> {
232        match &self {
233            Self::None(_) => unreachable!(),
234            Self::FoundAccount(_, account, _)
235            | Self::FoundProgramAccount((_, account), _)
236            | Self::FoundTokenAccount((_, account), _) => &account.data,
237        }
238    }
239
240    pub fn apply_update<T>(&mut self, update: T) -> RpcError<()>
241    where
242        T: Fn(&mut Account) -> RpcError<()>,
243    {
244        match self {
245            Self::None(_) => unreachable!(),
246            Self::FoundAccount(_, account, do_update_account) => {
247                update(account)?;
248                *do_update_account = true;
249            }
250            Self::FoundProgramAccount((_, account), _) => {
251                update(account)?;
252            }
253            Self::FoundTokenAccount((_, account), _) => {
254                update(account)?;
255            }
256        }
257        Ok(())
258    }
259
260    pub fn map_account(self) -> SurfpoolResult<Account> {
261        match self {
262            Self::None(pubkey) => Err(SurfpoolError::account_not_found(pubkey)),
263            Self::FoundAccount(_, account, _)
264            | Self::FoundProgramAccount((_, account), _)
265            | Self::FoundTokenAccount((_, account), _) => Ok(account),
266        }
267    }
268
269    #[allow(clippy::type_complexity)]
270    pub fn map_account_with_token_data(
271        self,
272    ) -> Option<((Pubkey, Account), Option<(Pubkey, Option<Account>)>)> {
273        match self {
274            Self::None(_) => None,
275            Self::FoundAccount(pubkey, account, _) => Some(((pubkey, account), None)),
276            Self::FoundProgramAccount((pubkey, account), _) => Some(((pubkey, account), None)),
277            Self::FoundTokenAccount((pubkey, account), token_data) => {
278                Some(((pubkey, account), Some(token_data)))
279            }
280        }
281    }
282
283    pub const fn is_none(&self) -> bool {
284        matches!(self, Self::None(_))
285    }
286
287    pub const fn requires_update(&self) -> bool {
288        match self {
289            Self::None(_) => false,
290            Self::FoundAccount(_, _, do_update) => *do_update,
291            Self::FoundProgramAccount(_, _) => true,
292            Self::FoundTokenAccount(_, _) => true,
293        }
294    }
295}
296
297impl From<GetAccountResult> for Result<Account, SurfpoolError> {
298    fn from(value: GetAccountResult) -> Self {
299        value.map_account()
300    }
301}
302
303impl SignatureSubscriptionType {
304    pub const fn received() -> Self {
305        SignatureSubscriptionType::Received
306    }
307
308    pub const fn processed() -> Self {
309        SignatureSubscriptionType::Commitment(CommitmentLevel::Processed)
310    }
311
312    pub const fn confirmed() -> Self {
313        SignatureSubscriptionType::Commitment(CommitmentLevel::Confirmed)
314    }
315
316    pub const fn finalized() -> Self {
317        SignatureSubscriptionType::Commitment(CommitmentLevel::Finalized)
318    }
319}
320
321#[allow(clippy::large_enum_variant)]
322pub enum GetTransactionResult {
323    None(Signature),
324    FoundTransaction(
325        Signature,
326        EncodedConfirmedTransactionWithStatusMeta,
327        TransactionStatus,
328    ),
329}
330
331impl GetTransactionResult {
332    pub fn found_transaction(
333        signature: Signature,
334        tx: EncodedConfirmedTransactionWithStatusMeta,
335        latest_absolute_slot: u64,
336    ) -> Self {
337        let is_finalized = latest_absolute_slot >= tx.slot + FINALIZATION_SLOT_THRESHOLD;
338        let is_confirmed = latest_absolute_slot >= tx.slot + 1;
339        let (confirmation_status, confirmations) = if is_finalized {
340            (
341                Some(solana_transaction_status::TransactionConfirmationStatus::Finalized),
342                None,
343            )
344        } else if is_confirmed {
345            (
346                Some(solana_transaction_status::TransactionConfirmationStatus::Confirmed),
347                Some((latest_absolute_slot - tx.slot) as usize),
348            )
349        } else {
350            (
351                Some(solana_transaction_status::TransactionConfirmationStatus::Processed),
352                Some((latest_absolute_slot - tx.slot) as usize),
353            )
354        };
355        let status = TransactionStatus {
356            slot: tx.slot,
357            confirmations,
358            status: tx
359                .transaction
360                .clone()
361                .meta
362                .map_or(Ok(()), |m| m.status.map_err(|e| e.into())),
363            err: tx
364                .transaction
365                .clone()
366                .meta
367                .and_then(|m| m.err.map(|e| e.into())),
368            confirmation_status,
369        };
370
371        Self::FoundTransaction(signature, tx, status)
372    }
373
374    pub const fn is_none(&self) -> bool {
375        matches!(self, Self::None(_))
376    }
377
378    pub fn map_found_transaction(&self) -> SurfpoolResult<TransactionStatus> {
379        match self {
380            Self::None(sig) => Err(SurfpoolError::transaction_not_found(sig)),
381            Self::FoundTransaction(_, _, status) => Ok(status.clone()),
382        }
383    }
384
385    pub fn map_some_transaction_status(&self) -> Option<TransactionStatus> {
386        match self {
387            Self::None(_) => None,
388            Self::FoundTransaction(_, _, status) => Some(status.clone()),
389        }
390    }
391}