1pub mod subscribe_config;
2pub mod usage;
3
4use std::{
5 collections::{BTreeMap, BTreeSet, HashSet},
6 fmt,
7};
8
9use base64::{
10 DecodeError as Base64DecodeError, Engine as _, engine::general_purpose::STANDARD as BASE64,
11};
12use serde::{Deserialize, Serialize};
13use solana_address::Address;
14
15pub mod ws_compression;
16
17#[derive(Debug, Serialize, Deserialize)]
19#[serde(tag = "method", content = "params", rename_all = "camelCase")]
20pub enum BacktestRequest {
21 CreateBacktestSession(CreateBacktestSessionRequest),
22 Continue(ContinueParams),
23 ContinueTo(ContinueToParams),
24 ContinueSessionV1(ContinueSessionRequestV1),
25 ContinueToSessionV1(ContinueToSessionRequestV1),
26 CloseBacktestSession,
27 CloseSessionV1(CloseSessionRequestV1),
28 AttachBacktestSession {
29 session_id: String,
30 last_sequence: Option<u64>,
33 },
34 ResumeAttachedSession,
37 AttachParallelControlSessionV2 {
38 control_session_id: String,
39 #[serde(default)]
43 last_sequences: BTreeMap<String, u64>,
44 },
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
52#[serde(untagged)]
53pub enum CreateBacktestSessionRequest {
54 V1(CreateBacktestSessionRequestV1),
55 V0(CreateSessionParams),
56}
57
58impl CreateBacktestSessionRequest {
59 pub fn into_request_options(self) -> CreateBacktestSessionRequestOptions {
60 match self {
61 Self::V0(request) => CreateBacktestSessionRequestOptions {
62 request,
63 parallel: false,
64 },
65 Self::V1(CreateBacktestSessionRequestV1 { request, parallel }) => {
66 CreateBacktestSessionRequestOptions { request, parallel }
67 }
68 }
69 }
70
71 pub fn into_request_and_parallel(self) -> (CreateSessionParams, bool) {
72 let options = self.into_request_options();
73 (options.request, options.parallel)
74 }
75}
76
77impl From<CreateSessionParams> for CreateBacktestSessionRequest {
78 fn from(value: CreateSessionParams) -> Self {
79 Self::V0(value)
80 }
81}
82
83impl From<CreateBacktestSessionRequestV1> for CreateBacktestSessionRequest {
84 fn from(value: CreateBacktestSessionRequestV1) -> Self {
85 Self::V1(value)
86 }
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
90#[serde(rename_all = "camelCase")]
91pub struct CreateBacktestSessionRequestV1 {
92 #[serde(flatten)]
93 pub request: CreateSessionParams,
94 pub parallel: bool,
95}
96
97#[derive(Debug, Clone)]
98pub struct CreateBacktestSessionRequestOptions {
99 pub request: CreateSessionParams,
100 pub parallel: bool,
101}
102
103#[derive(Debug, Serialize, Deserialize)]
104#[serde(rename_all = "camelCase")]
105pub struct ContinueSessionRequestV1 {
106 pub session_id: String,
107 pub request: ContinueParams,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct ContinueToSessionRequestV1 {
113 pub session_id: String,
114 pub request: ContinueToParams,
115}
116
117#[derive(Debug, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct CloseSessionRequestV1 {
120 pub session_id: String,
121}
122
123#[serde_with::serde_as]
130#[derive(Debug, Clone, Serialize, Deserialize)]
131#[serde(tag = "kind", content = "value", rename_all = "camelCase")]
132pub enum DiscoveryFilter {
133 ProgramExecuted(#[serde_as(as = "serde_with::DisplayFromStr")] Address),
135}
136
137pub struct TxMatchContext<'a> {
142 pub invoked_programs: &'a HashSet<Address>,
144}
145
146impl DiscoveryFilter {
147 pub fn matches(&self, ctx: &TxMatchContext<'_>) -> bool {
150 match self {
151 Self::ProgramExecuted(target) => ctx.invoked_programs.contains(target),
152 }
153 }
154}
155
156#[serde_with::serde_as]
158#[derive(Debug, Clone, Serialize, Deserialize)]
159#[serde(rename_all = "camelCase")]
160pub struct CreateSessionParams {
161 pub start_slot: u64,
163 pub end_slot: u64,
165 #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
166 #[serde(default)]
167 pub signer_filter: BTreeSet<Address>,
169 #[serde(default)]
172 pub send_summary: bool,
173 #[serde(default)]
176 pub capacity_wait_timeout_secs: Option<u16>,
177 #[serde(default)]
181 pub disconnect_timeout_secs: Option<u16>,
182 #[serde(default)]
187 pub extra_compute_units: Option<u32>,
188 #[serde(default)]
190 pub agents: Vec<AgentParams>,
191 #[serde(default, skip_serializing_if = "Vec::is_empty")]
198 pub discoveries: Vec<DiscoveryFilter>,
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
204#[serde(rename_all = "kebab-case")]
205pub enum FailFastDivergenceKind {
206 #[default]
209 AnyNonBenign,
210 Tracked,
213}
214
215impl FailFastDivergenceKind {
216 pub fn as_str(self) -> &'static str {
219 match self {
220 Self::AnyNonBenign => "any-non-benign",
221 Self::Tracked => "tracked",
222 }
223 }
224
225 pub fn from_str_opt(value: &str) -> Option<Self> {
227 match value {
228 "any-non-benign" => Some(Self::AnyNonBenign),
229 "tracked" => Some(Self::Tracked),
230 _ => None,
231 }
232 }
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
237#[serde(rename_all = "camelCase")]
238pub enum AgentType {
239 Arb,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244#[serde(rename_all = "camelCase")]
245pub struct ArbRouteParams {
246 pub base_mint: String,
247 pub temp_mint: String,
248 #[serde(default)]
249 pub buy_dexes: Vec<String>,
250 #[serde(default)]
251 pub sell_dexes: Vec<String>,
252 pub min_input: u64,
253 pub max_input: u64,
254 #[serde(default)]
255 pub min_profit: u64,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
260#[serde(rename_all = "camelCase")]
261pub struct AgentParams {
262 pub agent_type: AgentType,
263 pub wallet: Option<String>,
264 pub keypair: Option<String>,
266 pub seed_sol_lamports: Option<u64>,
267 #[serde(default)]
268 pub seed_token_accounts: BTreeMap<String, u64>,
269 #[serde(default)]
270 pub arb_routes: Vec<ArbRouteParams>,
271}
272
273#[serde_with::serde_as]
275#[derive(Debug, Serialize, Deserialize, Default)]
276pub struct AccountModifications(
277 #[serde_as(as = "BTreeMap<serde_with::DisplayFromStr, _>")]
278 #[serde(default)]
279 pub BTreeMap<Address, AccountData>,
280);
281
282#[serde_with::serde_as]
284#[derive(Debug, Serialize, Deserialize)]
285#[serde(rename_all = "camelCase")]
286pub struct ContinueParams {
287 #[serde(default = "ContinueParams::default_advance_count")]
288 pub advance_count: u64,
290 #[serde(default)]
291 pub transactions: Vec<String>,
293 #[serde(default)]
294 pub modify_account_states: AccountModifications,
296}
297
298impl Default for ContinueParams {
299 fn default() -> Self {
300 Self {
301 advance_count: Self::default_advance_count(),
302 transactions: Vec::new(),
303 modify_account_states: AccountModifications(BTreeMap::new()),
304 }
305 }
306}
307
308impl ContinueParams {
309 pub fn default_advance_count() -> u64 {
310 1
311 }
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
320#[serde(rename_all = "camelCase")]
321pub struct PausedEvent {
322 pub slot: u64,
323 #[serde(default, skip_serializing_if = "Option::is_none")]
324 pub batch_index: Option<u32>,
325}
326
327#[serde_with::serde_as]
336#[derive(Debug, Clone, Serialize, Deserialize)]
337#[serde(rename_all = "camelCase")]
338pub struct DiscoveryBatchEvent {
339 pub slot: u64,
340 pub batch_index: u32,
341 pub matched: Vec<DiscoveryFilter>,
343 pub transactions: Vec<EncodedBinary>,
347}
348
349#[derive(Debug, Clone, Serialize, Deserialize)]
351#[serde(rename_all = "camelCase")]
352pub struct ContinueToParams {
353 pub slot: u64,
355 #[serde(default)]
361 pub batch_index: Option<u32>,
362}
363
364#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
366#[serde(rename_all = "lowercase")]
367pub enum BinaryEncoding {
368 Base64,
369}
370
371impl BinaryEncoding {
372 pub fn encode(self, bytes: &[u8]) -> String {
373 match self {
374 Self::Base64 => BASE64.encode(bytes),
375 }
376 }
377
378 pub fn decode(self, data: &str) -> Result<Vec<u8>, Base64DecodeError> {
379 match self {
380 Self::Base64 => BASE64.decode(data),
381 }
382 }
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize)]
387#[serde(rename_all = "camelCase")]
388pub struct EncodedBinary {
389 pub data: String,
391 pub encoding: BinaryEncoding,
393}
394
395impl EncodedBinary {
396 pub fn new(data: String, encoding: BinaryEncoding) -> Self {
397 Self { data, encoding }
398 }
399
400 pub fn from_bytes(bytes: &[u8], encoding: BinaryEncoding) -> Self {
401 Self {
402 data: encoding.encode(bytes),
403 encoding,
404 }
405 }
406
407 pub fn decode(&self) -> Result<Vec<u8>, Base64DecodeError> {
408 self.encoding.decode(&self.data)
409 }
410}
411
412#[serde_with::serde_as]
414#[derive(Debug, Serialize, Deserialize)]
415#[serde(rename_all = "camelCase")]
416pub struct AccountData {
417 pub data: EncodedBinary,
419 pub executable: bool,
421 pub lamports: u64,
423 #[serde_as(as = "serde_with::DisplayFromStr")]
424 pub owner: Address,
426 pub space: u64,
428}
429
430#[derive(Debug, Clone, Serialize, Deserialize)]
432#[serde(tag = "method", content = "params", rename_all = "camelCase")]
433pub enum BacktestResponse {
434 SessionCreated {
435 session_id: String,
436 rpc_endpoint: String,
437 #[serde(default, skip_serializing_if = "Option::is_none")]
438 task_id: Option<String>,
439 },
440 SessionAttached {
441 session_id: String,
442 rpc_endpoint: String,
443 #[serde(default, skip_serializing_if = "Option::is_none")]
444 task_id: Option<String>,
445 },
446 SessionsCreated {
447 session_ids: Vec<String>,
448 },
449 SessionsCreatedV2 {
450 control_session_id: String,
451 session_ids: Vec<String>,
452 #[serde(default)]
453 task_ids: Vec<Option<String>>,
454 #[serde(default)]
459 start_slots: Vec<u64>,
460 #[serde(default)]
461 end_slots: Vec<u64>,
462 },
463 ParallelSessionAttachedV2 {
464 control_session_id: String,
465 session_ids: Vec<String>,
466 #[serde(default)]
467 task_ids: Vec<Option<String>>,
468 },
469 ReadyForContinue,
470 SlotNotification(u64),
471 Paused(PausedEvent),
472 DiscoveryBatch(DiscoveryBatchEvent),
473 Error(BacktestError),
474 Success,
475 Completed {
476 #[serde(skip_serializing_if = "Option::is_none")]
480 summary: Option<SessionSummary>,
481 #[serde(default, skip_serializing_if = "Option::is_none")]
482 agent_stats: Option<Vec<AgentStatsReport>>,
483 },
484 Status {
485 status: BacktestStatus,
486 },
487 SessionEventV1 {
488 session_id: String,
489 event: SessionEventV1,
490 },
491 SessionEventV2 {
492 session_id: String,
493 seq_id: u64,
494 event: SessionEventKind,
495 },
496}
497
498impl BacktestResponse {
499 pub fn is_completed(&self) -> bool {
500 matches!(self, BacktestResponse::Completed { .. })
501 }
502
503 pub fn is_terminal(&self) -> bool {
504 match self {
505 BacktestResponse::Completed { .. } => true,
506 BacktestResponse::Error(e) => matches!(
507 e,
508 BacktestError::NoMoreBlocks
509 | BacktestError::AdvanceSlotFailed { .. }
510 | BacktestError::FinalizeSlotFailed { .. }
511 | BacktestError::Internal { .. }
512 ),
513 _ => false,
514 }
515 }
516}
517
518impl From<BacktestStatus> for BacktestResponse {
519 fn from(status: BacktestStatus) -> Self {
520 Self::Status { status }
521 }
522}
523
524impl From<String> for BacktestResponse {
525 fn from(message: String) -> Self {
526 BacktestError::Internal { error: message }.into()
527 }
528}
529
530impl From<&str> for BacktestResponse {
531 fn from(message: &str) -> Self {
532 BacktestError::Internal {
533 error: message.to_string(),
534 }
535 .into()
536 }
537}
538
539#[derive(Debug, Clone, Serialize, Deserialize)]
540#[serde(tag = "method", content = "params", rename_all = "camelCase")]
541pub enum SessionEventV1 {
542 ReadyForContinue,
543 SlotNotification(u64),
544 Paused(PausedEvent),
545 DiscoveryBatch(DiscoveryBatchEvent),
546 Error(BacktestError),
547 Success,
548 Completed {
549 #[serde(skip_serializing_if = "Option::is_none")]
550 summary: Option<SessionSummary>,
551 #[serde(default, skip_serializing_if = "Option::is_none")]
552 agent_stats: Option<Vec<AgentStatsReport>>,
553 },
554 Status {
555 status: BacktestStatus,
556 },
557}
558
559#[derive(Debug, Clone, Serialize, Deserialize)]
560#[serde(tag = "method", content = "params", rename_all = "camelCase")]
561pub enum SessionEventKind {
562 ReadyForContinue,
563 SlotNotification(u64),
564 Paused(PausedEvent),
565 DiscoveryBatch(DiscoveryBatchEvent),
566 Error(BacktestError),
567 Success,
568 Completed {
569 #[serde(skip_serializing_if = "Option::is_none")]
570 summary: Option<SessionSummary>,
571 },
572 Status {
573 status: BacktestStatus,
574 },
575}
576
577impl SessionEventKind {
578 pub fn is_terminal(&self) -> bool {
579 match self {
580 Self::Completed { .. } => true,
581 Self::Error(e) => matches!(
582 e,
583 BacktestError::NoMoreBlocks
584 | BacktestError::AdvanceSlotFailed { .. }
585 | BacktestError::FinalizeSlotFailed { .. }
586 | BacktestError::Internal { .. }
587 ),
588 _ => false,
589 }
590 }
591}
592
593#[derive(Debug, Clone, Serialize, Deserialize)]
596#[serde(rename_all = "camelCase")]
597pub struct SequencedResponse {
598 pub seq_id: u64,
599 #[serde(flatten)]
600 pub response: BacktestResponse,
601}
602
603#[derive(Debug, Clone, Serialize, Deserialize)]
605#[serde(rename_all = "camelCase")]
606pub enum BacktestStatus {
607 StartingRuntime,
609 DecodedTransactions,
610 AppliedAccountModifications,
611 ReadyToExecuteUserTransactions,
612 ExecutedUserTransactions,
613 ExecutingBlockTransactions,
614 ExecutedBlockTransactions,
615 ProgramAccountsLoaded,
616}
617
618impl std::fmt::Display for BacktestStatus {
619 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
620 let s = match self {
621 Self::StartingRuntime => "starting runtime",
622 Self::DecodedTransactions => "decoded transactions",
623 Self::AppliedAccountModifications => "applied account modifications",
624 Self::ReadyToExecuteUserTransactions => "ready to execute user transactions",
625 Self::ExecutedUserTransactions => "executed user transactions",
626 Self::ExecutingBlockTransactions => "executing block transactions",
627 Self::ExecutedBlockTransactions => "executed block transactions",
628 Self::ProgramAccountsLoaded => "program accounts loaded",
629 };
630 f.write_str(s)
631 }
632}
633
634#[derive(Debug, Clone, Default, Serialize, Deserialize)]
636#[serde(rename_all = "camelCase")]
637pub struct AgentStatsReport {
638 pub name: String,
639 pub slots_processed: u64,
640 pub opportunities_found: u64,
641 pub opportunities_skipped: u64,
642 pub no_routes: u64,
643 pub txs_produced: u64,
644 pub expected_gain_by_mint: BTreeMap<String, i64>,
646 #[serde(default)]
648 pub txs_submitted: u64,
649 #[serde(default)]
651 pub txs_failed: u64,
652 #[serde(default)]
654 pub txs_simulation_rejected: u64,
655 #[serde(default)]
657 pub txs_simulation_failed: u64,
658}
659
660#[derive(Debug, Clone, Default, Serialize, Deserialize)]
662#[serde(rename_all = "camelCase")]
663pub struct SessionSummary {
664 pub correct_simulation: usize,
667 pub incorrect_simulation: usize,
670 pub execution_errors: usize,
672 pub balance_diff: usize,
674 pub log_diff: usize,
676}
677
678impl SessionSummary {
679 pub fn has_deviations(&self) -> bool {
681 self.incorrect_simulation > 0 || self.execution_errors > 0 || self.balance_diff > 0
682 }
683
684 pub fn total_transactions(&self) -> usize {
686 self.correct_simulation
687 + self.incorrect_simulation
688 + self.execution_errors
689 + self.balance_diff
690 + self.log_diff
691 }
692}
693
694impl std::fmt::Display for SessionSummary {
695 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
696 let total = self.total_transactions();
697 write!(
698 f,
699 "Session summary: {total} transactions\n\
700 \x20 - {} correct simulation\n\
701 \x20 - {} incorrect simulation\n\
702 \x20 - {} execution errors\n\
703 \x20 - {} balance diffs\n\
704 \x20 - {} log diffs",
705 self.correct_simulation,
706 self.incorrect_simulation,
707 self.execution_errors,
708 self.balance_diff,
709 self.log_diff,
710 )
711 }
712}
713
714#[derive(Debug, Clone, Serialize, Deserialize)]
716#[serde(rename_all = "camelCase")]
717pub enum BacktestError {
718 InvalidTransactionEncoding {
719 index: usize,
720 error: String,
721 },
722 InvalidTransactionFormat {
723 index: usize,
724 error: String,
725 },
726 InvalidAccountEncoding {
727 address: String,
728 encoding: BinaryEncoding,
729 error: String,
730 },
731 InvalidAccountOwner {
732 address: String,
733 error: String,
734 },
735 InvalidAccountPubkey {
736 address: String,
737 error: String,
738 },
739 NoMoreBlocks,
740 AdvanceSlotFailed {
741 slot: u64,
742 error: String,
743 },
744 FinalizeSlotFailed {
745 slot: u64,
746 error: String,
747 },
748 InvalidRequest {
749 error: String,
750 },
751 Internal {
752 error: String,
753 },
754 InvalidBlockhashFormat {
755 slot: u64,
756 error: String,
757 },
758 InitializingSysvarsFailed {
759 slot: u64,
760 error: String,
761 },
762 ClerkError {
763 error: String,
764 },
765 SimulationError {
766 error: String,
767 },
768 SessionNotFound {
769 session_id: String,
770 },
771 SessionOwnerMismatch,
772 SessionOwnershipBusy {
777 reason: String,
778 },
779}
780
781#[derive(Debug, Clone, Serialize, Deserialize)]
783pub struct AvailableRange {
784 pub bundle_start_slot: u64,
785 pub bundle_start_slot_utc: Option<String>,
786 pub max_bundle_end_slot: Option<u64>,
787 pub max_bundle_end_slot_utc: Option<String>,
788 pub max_bundle_size: Option<u64>,
789}
790
791pub fn split_range(
809 ranges: &[AvailableRange],
810 requested_start: u64,
811 requested_end: u64,
812) -> Result<Vec<(u64, u64)>, String> {
813 if requested_end < requested_start {
814 return Err(format!(
815 "invalid range: start_slot {requested_start} > end_slot {requested_end}"
816 ));
817 }
818
819 let mut ends_by_start: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
824 for r in ranges {
825 if let Some(end) = r.max_bundle_end_slot
826 && end > r.bundle_start_slot
827 {
828 ends_by_start
829 .entry(r.bundle_start_slot)
830 .or_default()
831 .insert(end);
832 }
833 }
834
835 let Some((&anchor_start, _)) = ends_by_start.range(..=requested_start).rfind(|(_, ends)| {
842 ends.iter()
843 .next_back()
844 .is_some_and(|&end| end >= requested_start)
845 }) else {
846 return Err(format!(
847 "start_slot {requested_start} is not covered by any available bundle range"
848 ));
849 };
850
851 let mut best_from: BTreeMap<u64, Vec<(u64, u64)>> = BTreeMap::new();
857 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end).rev() {
858 let mut best: Option<Vec<(u64, u64)>> = None;
859 for &end in ends {
860 let candidate = if end >= requested_end {
861 Some(vec![(start, requested_end)])
862 } else {
863 best_from.get(&(end + 1)).map(|rest| {
864 std::iter::once((start, end))
865 .chain(rest.iter().copied())
866 .collect()
867 })
868 };
869 if let Some(candidate) = candidate
870 && best.as_ref().is_none_or(|b| candidate.len() > b.len())
871 {
872 best = Some(candidate);
873 }
874 }
875 if let Some(best) = best {
876 best_from.insert(start, best);
877 }
878 }
879
880 best_from.remove(&anchor_start).ok_or_else(|| {
881 let mut covered_to = anchor_start.saturating_sub(1);
885 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end) {
886 if start > covered_to.saturating_add(1) {
887 break;
888 }
889 if let Some(&end) = ends.iter().next_back() {
890 covered_to = covered_to.max(end);
891 }
892 }
893 if covered_to < requested_end {
894 format!("gap in coverage at slot {}", covered_to + 1)
895 } else {
896 format!(
897 "no gap-free split of [{requested_start}, {requested_end}] aligns with the available bundle ranges"
898 )
899 }
900 })
901}
902
903impl From<BacktestError> for BacktestResponse {
904 fn from(error: BacktestError) -> Self {
905 Self::Error(error)
906 }
907}
908
909impl std::error::Error for BacktestError {}
910
911impl fmt::Display for BacktestError {
912 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
913 match self {
914 BacktestError::InvalidTransactionEncoding { index, error } => {
915 write!(f, "invalid transaction encoding at index {index}: {error}")
916 }
917 BacktestError::InvalidTransactionFormat { index, error } => {
918 write!(f, "invalid transaction format at index {index}: {error}")
919 }
920 BacktestError::InvalidAccountEncoding {
921 address,
922 encoding,
923 error,
924 } => write!(
925 f,
926 "invalid encoding for account {address} ({encoding:?}): {error}"
927 ),
928 BacktestError::InvalidAccountOwner { address, error } => {
929 write!(f, "invalid owner for account {address}: {error}")
930 }
931 BacktestError::InvalidAccountPubkey { address, error } => {
932 write!(f, "invalid account pubkey {address}: {error}")
933 }
934 BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
935 BacktestError::AdvanceSlotFailed { slot, error } => {
936 write!(f, "failed to advance to slot {slot}: {error}")
937 }
938 BacktestError::FinalizeSlotFailed { slot, error } => {
939 write!(f, "failed to finalize slot {slot}: {error}")
940 }
941 BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
942 BacktestError::Internal { error } => write!(f, "internal error: {error}"),
943 BacktestError::InvalidBlockhashFormat { slot, error } => {
944 write!(f, "invalid blockhash at slot {slot}: {error}")
945 }
946 BacktestError::InitializingSysvarsFailed { slot, error } => {
947 write!(f, "failed to initialize sysvars at slot {slot}: {error}")
948 }
949 BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
950 BacktestError::SimulationError { error } => {
951 write!(f, "simulation error: {error}")
952 }
953 BacktestError::SessionNotFound { session_id } => {
954 write!(f, "session not found: {session_id}")
955 }
956 BacktestError::SessionOwnerMismatch => {
957 write!(f, "session owner mismatch")
958 }
959 BacktestError::SessionOwnershipBusy { reason } => {
960 write!(f, "session ownership busy: {reason}")
961 }
962 }
963 }
964}
965
966#[cfg(test)]
967mod tests {
968 use super::*;
969
970 #[test]
971 fn fail_fast_divergence_kind_str_round_trips() {
972 for kind in [
973 FailFastDivergenceKind::AnyNonBenign,
974 FailFastDivergenceKind::Tracked,
975 ] {
976 assert_eq!(
977 FailFastDivergenceKind::from_str_opt(kind.as_str()),
978 Some(kind)
979 );
980 }
981 assert_eq!(FailFastDivergenceKind::from_str_opt("nonsense"), None);
982 assert_eq!(
983 FailFastDivergenceKind::default(),
984 FailFastDivergenceKind::AnyNonBenign
985 );
986 }
987
988 fn range(start: u64, end: u64) -> AvailableRange {
989 AvailableRange {
990 bundle_start_slot: start,
991 bundle_start_slot_utc: None,
992 max_bundle_end_slot: Some(end),
993 max_bundle_end_slot_utc: None,
994 max_bundle_size: None,
995 }
996 }
997
998 #[rstest::rstest]
1002 #[case::single(vec![range(100, 300)], 100, 300, Some(vec![(100, 300)]))]
1003 #[case::multi(
1004 vec![range(100, 200), range(201, 300), range(301, 400)],
1005 100, 300, Some(vec![(100, 200), (201, 300)])
1006 )]
1007 #[case::nested(
1010 vec![range(100, 500), range(110, 150), range(150, 190), range(501, 900)],
1011 100, 900, Some(vec![(100, 500), (501, 900)])
1012 )]
1013 #[case::prefers_finer_grid(
1017 vec![range(1_000, 1_999), range(1_500, 3_400), range(2_000, 2_999), range(3_000, 3_999)],
1018 1_000, 3_999, Some(vec![(1_000, 1_999), (2_000, 2_999), (3_000, 3_999)])
1019 )]
1020 #[case::shared_start_prefers_finer(
1024 vec![range(100, 150), range(100, 120), range(121, 140), range(141, 160)],
1025 100, 160, Some(vec![(100, 120), (121, 140), (141, 160)])
1026 )]
1027 #[case::falls_back_to_coarse(
1031 vec![range(100, 160), range(100, 120), range(121, 140)],
1032 100, 160, Some(vec![(100, 160)])
1033 )]
1034 #[case::clamps_final_bundle(vec![range(100, 199), range(200, 999)], 100, 450, Some(vec![(100, 199), (200, 450)]))]
1036 #[case::anchors_mid_bundle(vec![range(150, 350)], 200, 300, Some(vec![(150, 300)]))]
1039 #[case::anchors_then_continues(
1040 vec![range(150, 350), range(351, 600)],
1041 200, 600, Some(vec![(150, 350), (351, 600)])
1042 )]
1043 #[case::start_inside_bundle_anchors(vec![range(200, 400)], 300, 400, Some(vec![(200, 400)]))]
1044 #[case::start_before_first_bundle(vec![range(200, 400)], 100, 400, None)]
1046 #[case::end_not_covered(vec![range(100, 200)], 100, 300, None)]
1048 #[case::gap_in_coverage(vec![range(100, 200), range(210, 300)], 100, 300, None)]
1049 #[case::inverted_range(vec![range(100, 300)], 300, 100, None)]
1051 fn split_range_cases(
1052 #[case] ranges: Vec<AvailableRange>,
1053 #[case] start: u64,
1054 #[case] end: u64,
1055 #[case] expected: Option<Vec<(u64, u64)>>,
1056 ) {
1057 match expected {
1058 Some(expected) => assert_eq!(split_range(&ranges, start, end).unwrap(), expected),
1059 None => assert!(split_range(&ranges, start, end).is_err()),
1060 }
1061 }
1062
1063 fn ends_by_start(ranges: &[AvailableRange]) -> BTreeMap<u64, BTreeSet<u64>> {
1066 let mut ends: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
1067 for r in ranges {
1068 if let Some(end) = r.max_bundle_end_slot
1069 && end > r.bundle_start_slot
1070 {
1071 ends.entry(r.bundle_start_slot).or_default().insert(end);
1072 }
1073 }
1074 ends
1075 }
1076
1077 fn reference_max_split(
1081 ends: &BTreeMap<u64, BTreeSet<u64>>,
1082 cursor: u64,
1083 end: u64,
1084 ) -> Option<Vec<(u64, u64)>> {
1085 ends.get(&cursor)?
1086 .iter()
1087 .filter_map(|&bundle_end| {
1088 if bundle_end >= end {
1089 Some(vec![(cursor, end)])
1090 } else {
1091 reference_max_split(ends, bundle_end + 1, end).map(|mut rest| {
1092 rest.insert(0, (cursor, bundle_end));
1093 rest
1094 })
1095 }
1096 })
1097 .max_by_key(Vec::len)
1098 }
1099
1100 fn is_valid_split(
1104 split: &[(u64, u64)],
1105 ends: &BTreeMap<u64, BTreeSet<u64>>,
1106 start: u64,
1107 end: u64,
1108 ) -> bool {
1109 split.first().is_some_and(|&(s, _)| s == start)
1110 && split.last().is_some_and(|&(_, e)| e == end)
1111 && split.windows(2).all(|w| w[1].0 == w[0].1 + 1)
1112 && split.iter().all(|&(s, e)| {
1113 e >= s
1114 && ends
1115 .get(&s)
1116 .and_then(|bundle_ends| bundle_ends.iter().next_back())
1117 .is_some_and(|&max_end| e <= max_end)
1118 })
1119 }
1120
1121 #[test]
1126 fn split_range_matches_reference() {
1127 let mut seed: u64 = 0x9E3779B97F4A7C15;
1128 let mut next = || {
1129 seed = seed
1130 .wrapping_mul(6364136223846793005)
1131 .wrapping_add(1442695040888963407);
1132 seed >> 33
1133 };
1134
1135 for _ in 0..50_000 {
1136 let ranges: Vec<AvailableRange> = (0..next() % 6)
1139 .map(|_| {
1140 let start = next() % 12;
1141 range(start, start + next() % 6) })
1143 .collect();
1144 let start = next() % 12;
1145 let end = start + next() % 6; let got = split_range(&ranges, start, end);
1148 let ends = ends_by_start(&ranges);
1149 let anchor = ends
1152 .range(..=start)
1153 .rfind(|(_, e)| e.iter().next_back().is_some_and(|&x| x >= start))
1154 .map(|(&s, _)| s);
1155 let reference = anchor.and_then(|a| reference_max_split(&ends, a, end));
1156
1157 let layout: Vec<_> = ranges
1158 .iter()
1159 .map(|r| (r.bundle_start_slot, r.max_bundle_end_slot))
1160 .collect();
1161 match (&got, &reference) {
1162 (Ok(split), Some(best)) => {
1163 assert!(
1164 is_valid_split(split, &ends, anchor.unwrap(), end),
1165 "invalid split {split:?} for {layout:?} [{start},{end}]"
1166 );
1167 assert_eq!(
1168 split.len(),
1169 best.len(),
1170 "suboptimal split {split:?} vs {best:?} for {layout:?} [{start},{end}]"
1171 );
1172 }
1173 (Err(_), None) => {}
1174 _ => panic!(
1175 "disagreement: split_range={got:?}, reference={reference:?} for {layout:?} [{start},{end}]"
1176 ),
1177 }
1178 }
1179 }
1180}