Skip to main content

sof_tx/submit/
types.rs

1//! Shared submission types, errors, and transport traits.
2
3use std::time::Duration;
4
5use async_trait::async_trait;
6use solana_signature::Signature;
7use thiserror::Error;
8
9use crate::{builder::BuilderError, providers::LeaderTarget, routing::RoutingPolicy};
10
11/// Runtime submit mode.
12#[derive(Debug, Clone, Copy, Eq, PartialEq)]
13pub enum SubmitMode {
14    /// Submit only through JSON-RPC.
15    RpcOnly,
16    /// Submit only through direct leader/validator targets.
17    DirectOnly,
18    /// Submit direct first, then RPC fallback on failure.
19    Hybrid,
20}
21
22/// Reliability profile for direct and hybrid submission behavior.
23#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
24pub enum SubmitReliability {
25    /// Fastest path with minimal retrying.
26    LowLatency,
27    /// Balanced latency and retry behavior.
28    #[default]
29    Balanced,
30    /// Aggressive retrying before giving up.
31    HighReliability,
32}
33
34/// Signed transaction payload variants accepted by submit APIs.
35#[derive(Debug, Clone, Eq, PartialEq)]
36pub enum SignedTx {
37    /// Bincode-serialized `VersionedTransaction` bytes.
38    VersionedTransactionBytes(Vec<u8>),
39    /// Wire-format transaction bytes.
40    WireTransactionBytes(Vec<u8>),
41}
42
43/// RPC submit tuning.
44#[derive(Debug, Clone, Eq, PartialEq)]
45pub struct RpcSubmitConfig {
46    /// Skip preflight simulation when true.
47    pub skip_preflight: bool,
48    /// Optional preflight commitment string.
49    pub preflight_commitment: Option<String>,
50}
51
52impl Default for RpcSubmitConfig {
53    fn default() -> Self {
54        Self {
55            skip_preflight: true,
56            preflight_commitment: None,
57        }
58    }
59}
60
61/// Direct submit tuning.
62#[derive(Debug, Clone, Eq, PartialEq)]
63pub struct DirectSubmitConfig {
64    /// Per-target send timeout.
65    pub per_target_timeout: Duration,
66    /// Global send budget for one submission.
67    pub global_timeout: Duration,
68    /// Number of rounds to iterate across selected direct targets.
69    pub direct_target_rounds: usize,
70    /// Number of direct-only submit attempts (target selection can refresh per attempt).
71    pub direct_submit_attempts: usize,
72    /// Number of direct submit attempts in `Hybrid` mode before RPC fallback.
73    pub hybrid_direct_attempts: usize,
74    /// Delay between direct rebroadcast attempts/rounds (Agave-like pacing).
75    pub rebroadcast_interval: Duration,
76    /// Enables Agave-style post-ack rebroadcast persistence for direct submits.
77    pub agave_rebroadcast_enabled: bool,
78    /// Maximum time budget for background rebroadcast persistence.
79    pub agave_rebroadcast_window: Duration,
80    /// Delay between background rebroadcast cycles.
81    pub agave_rebroadcast_interval: Duration,
82    /// When true, `Hybrid` mode broadcasts to RPC even after direct send succeeds.
83    pub hybrid_rpc_broadcast: bool,
84    /// Enables latency-aware ordering of direct targets before submit.
85    pub latency_aware_targeting: bool,
86    /// Timeout used for per-target TCP latency probes.
87    pub latency_probe_timeout: Duration,
88    /// Optional extra TCP port to probe (in addition to target TPU port).
89    pub latency_probe_port: Option<u16>,
90    /// Max number of targets to probe per submission.
91    pub latency_probe_max_targets: usize,
92}
93
94impl DirectSubmitConfig {
95    /// Builds a direct-submit config from a reliability profile.
96    #[must_use]
97    pub const fn from_reliability(reliability: SubmitReliability) -> Self {
98        match reliability {
99            SubmitReliability::LowLatency => Self {
100                per_target_timeout: Duration::from_millis(200),
101                global_timeout: Duration::from_millis(1_200),
102                direct_target_rounds: 3,
103                direct_submit_attempts: 3,
104                hybrid_direct_attempts: 2,
105                rebroadcast_interval: Duration::from_millis(90),
106                agave_rebroadcast_enabled: true,
107                agave_rebroadcast_window: Duration::from_secs(30),
108                agave_rebroadcast_interval: Duration::from_millis(700),
109                hybrid_rpc_broadcast: false,
110                latency_aware_targeting: true,
111                latency_probe_timeout: Duration::from_millis(80),
112                latency_probe_port: Some(8899),
113                latency_probe_max_targets: 128,
114            },
115            SubmitReliability::Balanced => Self {
116                per_target_timeout: Duration::from_millis(300),
117                global_timeout: Duration::from_millis(1_800),
118                direct_target_rounds: 4,
119                direct_submit_attempts: 4,
120                hybrid_direct_attempts: 3,
121                rebroadcast_interval: Duration::from_millis(110),
122                agave_rebroadcast_enabled: true,
123                agave_rebroadcast_window: Duration::from_secs(45),
124                agave_rebroadcast_interval: Duration::from_millis(800),
125                hybrid_rpc_broadcast: true,
126                latency_aware_targeting: true,
127                latency_probe_timeout: Duration::from_millis(120),
128                latency_probe_port: Some(8899),
129                latency_probe_max_targets: 128,
130            },
131            SubmitReliability::HighReliability => Self {
132                per_target_timeout: Duration::from_millis(450),
133                global_timeout: Duration::from_millis(3_200),
134                direct_target_rounds: 6,
135                direct_submit_attempts: 5,
136                hybrid_direct_attempts: 4,
137                rebroadcast_interval: Duration::from_millis(140),
138                agave_rebroadcast_enabled: true,
139                agave_rebroadcast_window: Duration::from_secs(70),
140                agave_rebroadcast_interval: Duration::from_millis(900),
141                hybrid_rpc_broadcast: true,
142                latency_aware_targeting: true,
143                latency_probe_timeout: Duration::from_millis(160),
144                latency_probe_port: Some(8899),
145                latency_probe_max_targets: 128,
146            },
147        }
148    }
149
150    /// Returns this config with minimum valid retry counters.
151    #[must_use]
152    pub const fn normalized(self) -> Self {
153        let direct_target_rounds = if self.direct_target_rounds == 0 {
154            1
155        } else {
156            self.direct_target_rounds
157        };
158        let direct_submit_attempts = if self.direct_submit_attempts == 0 {
159            1
160        } else {
161            self.direct_submit_attempts
162        };
163        let hybrid_direct_attempts = if self.hybrid_direct_attempts == 0 {
164            1
165        } else {
166            self.hybrid_direct_attempts
167        };
168        let latency_probe_max_targets = if self.latency_probe_max_targets == 0 {
169            1
170        } else {
171            self.latency_probe_max_targets
172        };
173        let rebroadcast_interval = if self.rebroadcast_interval.is_zero() {
174            Duration::from_millis(1)
175        } else {
176            self.rebroadcast_interval
177        };
178        let agave_rebroadcast_interval = if self.agave_rebroadcast_interval.is_zero() {
179            Duration::from_millis(1)
180        } else {
181            self.agave_rebroadcast_interval
182        };
183        Self {
184            per_target_timeout: self.per_target_timeout,
185            global_timeout: self.global_timeout,
186            direct_target_rounds,
187            direct_submit_attempts,
188            hybrid_direct_attempts,
189            rebroadcast_interval,
190            agave_rebroadcast_enabled: self.agave_rebroadcast_enabled,
191            agave_rebroadcast_window: self.agave_rebroadcast_window,
192            agave_rebroadcast_interval,
193            hybrid_rpc_broadcast: self.hybrid_rpc_broadcast,
194            latency_aware_targeting: self.latency_aware_targeting,
195            latency_probe_timeout: self.latency_probe_timeout,
196            latency_probe_port: self.latency_probe_port,
197            latency_probe_max_targets,
198        }
199    }
200}
201
202impl Default for DirectSubmitConfig {
203    fn default() -> Self {
204        Self::from_reliability(SubmitReliability::default())
205    }
206}
207
208/// Low-level transport errors surfaced by submit backends.
209#[derive(Debug, Error, Clone, Eq, PartialEq)]
210pub enum SubmitTransportError {
211    /// Invalid transport configuration.
212    #[error("transport configuration invalid: {message}")]
213    Config {
214        /// Human-readable description.
215        message: String,
216    },
217    /// Transport operation failed.
218    #[error("transport failure: {message}")]
219    Failure {
220        /// Human-readable description.
221        message: String,
222    },
223}
224
225/// Submission-level errors.
226#[derive(Debug, Error)]
227pub enum SubmitError {
228    /// Could not build/sign transaction for builder submit path.
229    #[error("failed to build/sign transaction: {source}")]
230    Build {
231        /// Builder-layer failure.
232        source: BuilderError,
233    },
234    /// No blockhash available for builder submit path.
235    #[error("blockhash provider returned no recent blockhash")]
236    MissingRecentBlockhash,
237    /// Signed bytes could not be decoded into a transaction.
238    #[error("failed to decode signed transaction bytes: {source}")]
239    DecodeSignedBytes {
240        /// Bincode decode error.
241        source: Box<bincode::ErrorKind>,
242    },
243    /// Duplicate signature was suppressed by dedupe window.
244    #[error("duplicate signature suppressed by dedupe window")]
245    DuplicateSignature,
246    /// RPC mode requested but no RPC transport was configured.
247    #[error("rpc transport is not configured")]
248    MissingRpcTransport,
249    /// Direct mode requested but no direct transport was configured.
250    #[error("direct transport is not configured")]
251    MissingDirectTransport,
252    /// No direct targets resolved from routing inputs.
253    #[error("no direct targets resolved from leader/backups")]
254    NoDirectTargets,
255    /// Direct transport failure.
256    #[error("direct submit failed: {source}")]
257    Direct {
258        /// Direct transport error.
259        source: SubmitTransportError,
260    },
261    /// RPC transport failure.
262    #[error("rpc submit failed: {source}")]
263    Rpc {
264        /// RPC transport error.
265        source: SubmitTransportError,
266    },
267    /// Internal synchronization failure.
268    #[error("internal synchronization failure: {message}")]
269    InternalSync {
270        /// Synchronization error details.
271        message: String,
272    },
273}
274
275/// Summary of a successful submission.
276#[derive(Debug, Clone, Eq, PartialEq)]
277pub struct SubmitResult {
278    /// Signature parsed from submitted transaction bytes.
279    pub signature: Option<Signature>,
280    /// Mode selected by caller.
281    pub mode: SubmitMode,
282    /// Target chosen by direct path when applicable.
283    pub direct_target: Option<LeaderTarget>,
284    /// RPC-returned signature string when RPC path succeeded.
285    pub rpc_signature: Option<String>,
286    /// True when RPC fallback was used from hybrid mode.
287    pub used_rpc_fallback: bool,
288    /// Number of direct targets selected for submit attempt that succeeded.
289    pub selected_target_count: usize,
290    /// Number of unique validator identities in selected direct targets.
291    pub selected_identity_count: usize,
292}
293
294/// RPC transport interface.
295#[async_trait]
296pub trait RpcSubmitTransport: Send + Sync {
297    /// Submits transaction bytes to RPC and returns signature string.
298    async fn submit_rpc(
299        &self,
300        tx_bytes: &[u8],
301        config: &RpcSubmitConfig,
302    ) -> Result<String, SubmitTransportError>;
303}
304
305/// Direct transport interface.
306#[async_trait]
307pub trait DirectSubmitTransport: Send + Sync {
308    /// Submits transaction bytes to direct targets and returns the first successful target.
309    async fn submit_direct(
310        &self,
311        tx_bytes: &[u8],
312        targets: &[LeaderTarget],
313        policy: RoutingPolicy,
314        config: &DirectSubmitConfig,
315    ) -> Result<LeaderTarget, SubmitTransportError>;
316}