1use crate::{
2 idl::arcium::{
3 accounts::{
4 ArxNode,
5 ClockAccount,
6 Cluster,
7 LargeExecPool,
8 LargeMempool,
9 MXEAccount,
10 MediumExecPool,
11 MediumMempool,
12 SmallExecPool,
13 SmallMempool,
14 TinyExecPool,
15 TinyMempool,
16 },
17 types::{ClusterMembership, ComputationReference, LeaderSelector, MempoolSize},
18 },
19 pda::{arx_acc, clock_acc, cluster_acc, mempool_acc},
20};
21use anchor_client::anchor_lang::{AccountDeserialize, Discriminator};
22use anchor_lang::prelude::Pubkey;
23use bytemuck::Zeroable;
24use solana_account_decoder_client_types::{UiAccountData, UiAccountEncoding, UiDataSliceConfig};
25use solana_rpc_client::nonblocking::rpc_client::RpcClient as AsyncRpcClient;
26use solana_rpc_client_api::{
27 client_error::Error as SolanaClientError,
28 config::{RpcAccountInfoConfig, RpcProgramAccountsConfig},
29 filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType},
30};
31use std::{collections::HashSet, hash::Hash};
32use thiserror::Error;
33
34pub const MIN_CLUSTER_CONTEXT_SLOT: u64 = 0;
38
39#[derive(Error, Debug, Clone, PartialEq)]
41pub enum ClusterOffsetError {
42 #[error("Failed to fetch node accounts from RPC: {0}")]
44 AccountFetchFailed(String),
45 #[error("Failed to deserialize node account data: {0}")]
47 DeserializationFailed(String),
48 #[error("Found inactive node in cluster at offset {0}")]
50 InactiveNode(u32),
51 #[error("Cluster has no nodes")]
53 EmptyCluster,
54 #[error("No cluster membership found for target cluster")]
56 NoClusterMembership,
57}
58
59#[derive(Debug, Clone, PartialEq)]
61pub enum ClusterOffsetState {
62 Available(u32),
64 NotLookedUp,
66 Unavailable(ClusterOffsetError),
68}
69
70impl ClusterOffsetState {
71 pub fn is_available(&self) -> bool {
73 matches!(self, ClusterOffsetState::Available(_))
74 }
75
76 pub fn get(&self) -> Option<u32> {
78 match self {
79 ClusterOffsetState::Available(offset) => Some(*offset),
80 _ => None,
81 }
82 }
83
84 pub fn error(&self) -> Option<&ClusterOffsetError> {
86 match self {
87 ClusterOffsetState::Unavailable(err) => Some(err),
88 _ => None,
89 }
90 }
91}
92
93impl std::fmt::Display for ClusterOffsetState {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 match self {
96 ClusterOffsetState::Available(offset) => write!(f, "Available (offset: {})", offset),
97 ClusterOffsetState::NotLookedUp => write!(f, "Not looked up"),
98 ClusterOffsetState::Unavailable(err) => write!(f, "Unavailable: {}", err),
99 }
100 }
101}
102
103pub async fn arx_acc_active(
104 rpc_client: &AsyncRpcClient,
105 node_offset: u32,
106) -> Result<bool, Box<dyn std::error::Error>> {
107 let arx_acc = arx_acc(node_offset);
108 let bytes = rpc_client
109 .get_account(&arx_acc)
110 .await
111 .map_err(|e| format!("Failed to get account data: {}", e))?
112 .data;
113 let arx_data = ArxNode::try_deserialize(&mut bytes.as_slice())
114 .map_err(|e| format!("Failed to deserialize account data: {}", e))?;
115 Ok(arx_data.is_active)
116}
117
118pub async fn active_proposals(
119 rpc_client: &AsyncRpcClient,
120 cluster_offset: u32,
121) -> Result<Vec<u64>, Box<dyn std::error::Error>> {
122 let cluster_acc = cluster_acc(cluster_offset);
123 let bytes = rpc_client
124 .get_account(&cluster_acc)
125 .await
126 .map_err(|e| format!("Failed to get account data: {}", e))?
127 .data;
128 let cluster_data = Cluster::try_deserialize(&mut bytes.as_slice())
129 .map_err(|e| format!("Failed to deserialize account data: {}", e))?;
130 Ok(dedupe(cluster_data.cu_price_proposals.to_vec()))
132}
133
134fn dedupe<T: PartialEq + Eq + Hash + Copy>(arr: Vec<T>) -> Vec<T> {
135 let mut seen = HashSet::new();
136 let mut result = Vec::new();
137
138 for &item in arr.iter() {
139 if seen.insert(item) {
140 result.push(item);
141 }
142 }
143
144 result
145}
146
147pub async fn get_all_cluster_accounts(
154 rpc_client: &AsyncRpcClient,
155 min_context_slot: Option<u64>,
156) -> Result<Vec<(Pubkey, Cluster)>, Box<dyn std::error::Error>> {
157 let program_id = crate::idl::arcium::ID;
158 let discriminator = Cluster::DISCRIMINATOR;
159
160 let memcmp_filter = RpcFilterType::Memcmp(Memcmp::new(
161 0,
162 MemcmpEncodedBytes::Bytes(discriminator.to_vec()),
163 ));
164
165 let config = RpcProgramAccountsConfig {
166 filters: Some(vec![memcmp_filter]),
167 account_config: RpcAccountInfoConfig {
168 encoding: Some(UiAccountEncoding::Base64),
169 commitment: None,
170 data_slice: None,
171 min_context_slot,
172 },
173 with_context: None,
174 sort_results: None,
175 };
176
177 let ui_accounts = rpc_client
178 .get_program_ui_accounts_with_config(&program_id, config)
179 .await?;
180
181 let mut clusters = Vec::new();
182 for (pubkey, ui_account) in ui_accounts {
183 let data = ui_account.data.decode().ok_or_else(|| {
184 let variant = match &ui_account.data {
185 UiAccountData::Json(_) => "JsonParsed".to_string(),
186 UiAccountData::LegacyBinary(_) => "LegacyBinary".to_string(),
187 UiAccountData::Binary(_, enc) => format!("Binary({enc:?})"),
188 };
189 format!("Failed to decode account data for {pubkey}: got {variant}, expected Base64")
190 })?;
191 match Cluster::try_deserialize(&mut data.as_slice()) {
193 Ok(cluster) => clusters.push((pubkey, cluster)),
194 Err(_) => continue, }
196 }
197
198 Ok(clusters)
199}
200
201async fn get_mxe_count(
202 rpc_client: &AsyncRpcClient,
203 min_context_slot: Option<u64>,
204 cluster_offset: u32,
205) -> Result<usize, Box<dyn std::error::Error>> {
206 let program_id = crate::idl::arcium::ID;
207 let discriminator = MXEAccount::DISCRIMINATOR;
208 let mut comparative_bytes = Vec::with_capacity(13);
215 comparative_bytes.extend_from_slice(discriminator);
216 comparative_bytes.push(1);
217 comparative_bytes.extend_from_slice(&cluster_offset.to_le_bytes());
218
219 let memcmp_filter =
220 RpcFilterType::Memcmp(Memcmp::new(0, MemcmpEncodedBytes::Bytes(comparative_bytes)));
221
222 let data_slice_config = UiDataSliceConfig {
224 offset: 0,
225 length: 0,
226 };
227
228 let config = RpcProgramAccountsConfig {
229 filters: Some(vec![memcmp_filter]),
230 account_config: RpcAccountInfoConfig {
231 encoding: Some(UiAccountEncoding::Base64),
232 commitment: None,
233 data_slice: Some(data_slice_config),
234 min_context_slot,
235 },
236 with_context: None,
237 sort_results: None,
238 };
239
240 let ui_accounts = rpc_client
241 .get_program_ui_accounts_with_config(&program_id, config)
242 .await?;
243
244 Ok(ui_accounts.len())
245}
246
247#[derive(Debug, Clone)]
250pub struct ClusterInfo {
251 pub pubkey: Pubkey,
252 pub offset: ClusterOffsetState,
254 pub cluster: Cluster,
255 pub mxe_count: usize,
257}
258
259impl ClusterInfo {
260 pub fn node_count(&self) -> usize {
262 self.cluster.nodes.len()
263 }
264
265 pub fn max_nodes(&self) -> u16 {
267 self.cluster.cluster_size
268 }
269
270 pub fn pending_node_count(&self) -> usize {
272 self.cluster.pending_nodes.len()
273 }
274
275 pub fn node_utilization_percent(&self) -> f32 {
277 let max = self.max_nodes();
278 if max > 0 {
279 (self.node_count() as f32 / max as f32) * 100.0
280 } else {
281 0.0
282 }
283 }
284}
285
286pub async fn get_cluster_discovery_info(
300 rpc_client: &AsyncRpcClient,
301 _current_epoch: u64,
302 min_context_slot: Option<u64>,
303) -> Result<Vec<ClusterInfo>, Box<dyn std::error::Error>> {
304 let clusters = get_all_cluster_accounts(rpc_client, min_context_slot).await?;
305
306 let mut infos = Vec::with_capacity(clusters.len());
307 for (pubkey, cluster) in clusters {
308 let offset = find_cluster_offset_via_node(rpc_client, &cluster).await;
310 let mxe_count = if let ClusterOffsetState::Available(offset) = offset {
311 let mxe_count = get_mxe_count(rpc_client, min_context_slot, offset).await?;
312 mxe_count
313 } else {
314 0
315 };
316
317 infos.push(ClusterInfo {
318 pubkey,
319 offset,
320 cluster,
321 mxe_count,
322 });
323 }
324
325 Ok(infos)
326}
327
328async fn find_cluster_offset_via_node(
332 rpc_client: &AsyncRpcClient,
333 cluster: &Cluster,
334) -> ClusterOffsetState {
335 if cluster.nodes.is_empty() {
336 return ClusterOffsetState::Unavailable(ClusterOffsetError::EmptyCluster);
337 }
338
339 let node_pubkeys: Vec<Pubkey> = cluster
340 .nodes
341 .iter()
342 .map(|node_ref| arx_acc(node_ref.offset))
343 .collect();
344
345 let accounts = match rpc_client.get_multiple_accounts(&node_pubkeys).await {
346 Ok(accounts) => accounts,
347 Err(e) => {
348 return ClusterOffsetState::Unavailable(ClusterOffsetError::AccountFetchFailed(
349 e.to_string(),
350 ))
351 }
352 };
353
354 debug_assert_eq!(
356 accounts.len(),
357 cluster.nodes.len(),
358 "RPC returned {} accounts but requested {} nodes",
359 accounts.len(),
360 cluster.nodes.len()
361 );
362
363 let mut found_offset = None;
364
365 for (i, maybe_account) in accounts.iter().enumerate() {
366 let node_offset = cluster.nodes[i].offset;
367
368 let account = match maybe_account.as_ref() {
369 Some(acc) => acc,
370 None => {
371 return ClusterOffsetState::Unavailable(ClusterOffsetError::AccountFetchFailed(
372 format!("Node account not found: {}", node_offset),
373 ))
374 }
375 };
376
377 let node = match ArxNode::try_deserialize(&mut account.data.as_slice()) {
378 Ok(node) => node,
379 Err(e) => {
380 return ClusterOffsetState::Unavailable(ClusterOffsetError::DeserializationFailed(
381 e.to_string(),
382 ))
383 }
384 };
385
386 if !node.is_active {
387 return ClusterOffsetState::Unavailable(ClusterOffsetError::InactiveNode(node_offset));
388 }
389
390 if found_offset.is_none() {
391 if let ClusterMembership::Active(cluster_offset) = &node.cluster_membership {
392 found_offset = Some(*cluster_offset);
393 }
394 }
395 }
396
397 match found_offset {
398 Some(offset) => ClusterOffsetState::Available(offset),
399 None => ClusterOffsetState::Unavailable(ClusterOffsetError::NoClusterMembership),
400 }
401}
402
403pub async fn get_current_epoch(
405 rpc_client: &AsyncRpcClient,
406) -> Result<u64, Box<dyn std::error::Error>> {
407 let clock_pubkey = clock_acc();
408 let account = rpc_client.get_account(&clock_pubkey).await?;
409 let clock_data = ClockAccount::try_deserialize(&mut account.data.as_slice())?;
410
411 Ok(clock_data.current_epoch.0)
412}
413
414pub async fn get_mempool_acc_data(
415 rpc: &AsyncRpcClient,
416 mempool_acc: &Pubkey,
417) -> Result<MempoolWrapper, ComputationPoolError> {
418 let mempool_data = rpc
419 .get_account_data(mempool_acc)
420 .await
421 .map_err(ComputationPoolError::new_solana_error)?;
422 MempoolWrapper::from_raw(&mempool_data)
423}
424
425pub async fn get_mempool_acc_data_raw(
428 rpc: &AsyncRpcClient,
429 mempool_acc: &Pubkey,
430) -> Result<Vec<u8>, SolanaClientError> {
431 let mempool_data = rpc.get_account_data(mempool_acc).await?;
432 Ok(mempool_data)
433}
434
435pub fn mempool_tier_from_discriminator(disc: &[u8]) -> Option<MempoolSize> {
437 match disc {
438 TinyMempool::DISCRIMINATOR => Some(MempoolSize::Tiny),
439 SmallMempool::DISCRIMINATOR => Some(MempoolSize::Small),
440 MediumMempool::DISCRIMINATOR => Some(MempoolSize::Medium),
441 LargeMempool::DISCRIMINATOR => Some(MempoolSize::Large),
442 _ => None,
443 }
444}
445
446pub async fn get_mempool_tiers(
451 rpc: &AsyncRpcClient,
452 cluster_offsets: &[u32],
453 min_context_slot: Option<u64>,
454) -> Result<Vec<Option<MempoolSize>>, SolanaClientError> {
455 const MAX_MULTIPLE_ACCOUNTS: usize = 100;
457
458 let pdas: Vec<Pubkey> = cluster_offsets
459 .iter()
460 .map(|offset| mempool_acc(*offset))
461 .collect();
462 let config = RpcAccountInfoConfig {
463 encoding: Some(UiAccountEncoding::Base64),
464 data_slice: Some(UiDataSliceConfig {
465 offset: 0,
466 length: 8,
467 }),
468 commitment: None,
469 min_context_slot,
470 };
471
472 let mut tiers = Vec::with_capacity(pdas.len());
473 for chunk in pdas.chunks(MAX_MULTIPLE_ACCOUNTS) {
474 let accounts = rpc
475 .get_multiple_ui_accounts_with_config(chunk, config.clone())
476 .await?
477 .value;
478 for account in accounts {
479 tiers.push(
480 account
481 .and_then(|acc| acc.data.decode())
482 .and_then(|data| mempool_tier_from_discriminator(&data)),
483 );
484 }
485 }
486 Ok(tiers)
487}
488
489pub async fn get_execpool_acc_data(
490 rpc: &AsyncRpcClient,
491 execpool_acc: &Pubkey,
492) -> Result<ExecpoolWrapper, ComputationPoolError> {
493 let execpool_data = rpc
494 .get_account_data(execpool_acc)
495 .await
496 .map_err(ComputationPoolError::new_solana_error)?;
497 ExecpoolWrapper::from_raw(&execpool_data)
498}
499
500pub async fn get_execpool_acc_data_raw(
503 rpc: &AsyncRpcClient,
504 execpool_acc: &Pubkey,
505) -> Result<Vec<u8>, SolanaClientError> {
506 let execpool_data = rpc.get_account_data(execpool_acc).await?;
507 Ok(execpool_data)
508}
509
510#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
511pub struct MempoolInfo {
512 pub cluster: Pubkey,
513 pub mxe: Pubkey,
514 pub mempool: Pubkey,
515}
516
517pub enum MempoolWrapper {
521 Tiny(Box<TinyMempool>),
522 Small(Box<SmallMempool>),
523 Medium(Box<MediumMempool>),
524 Large(Box<LargeMempool>),
525}
526
527#[derive(Debug)]
528pub enum ComputationPoolError {
529 InvalidDiscriminator,
530 InvalidSize,
531 InvalidStartIndex { start_index: usize, capacity: usize },
532 ClientError(Box<SolanaClientError>),
533}
534
535impl ComputationPoolError {
536 pub fn new_solana_error(err: SolanaClientError) -> Self {
537 ComputationPoolError::ClientError(Box::new(err))
538 }
539}
540
541macro_rules! extract_computations {
542 ($inner:expr) => {{
543 let start_index = $inner.computations.start_index as usize;
544 let buffer_size = $inner.computations.elems.len();
545 let length = $inner.computations.length as usize;
546
547 $inner
548 .computations
549 .elems
550 .iter()
551 .enumerate()
552 .filter(|(i, _)| {
553 let normalized_i = if *i >= start_index {
555 *i - start_index
556 } else {
557 buffer_size - start_index + *i
558 };
559 Self::is_valid(&$inner.computations.valid_bits, *i) && normalized_i < length
561 })
562 .flat_map(|(_, h)| h.entries.iter().copied())
563 .filter(|computation| !is_empty_computation_ref(computation))
564 .collect()
565 }};
566}
567
568macro_rules! extract_computations_highest_prio {
569 ($inner:expr) => {{
570 let start_index = $inner.computations.start_index as usize;
571 let buffer_size = $inner.computations.elems.len();
572 let length = $inner.computations.length as usize;
573
574 $inner
575 .computations
576 .elems
577 .iter()
578 .enumerate()
579 .filter_map(|(i, h)| {
580 let normalized_i = if i >= start_index {
582 i - start_index
583 } else {
584 buffer_size - start_index + i
585 };
586
587 if Self::is_valid(&$inner.computations.valid_bits, i) && normalized_i < length {
589 let first = h.entries.first().copied()?;
590 if !is_empty_computation_ref(&first) {
591 return Some(first);
592 }
593 }
594 None
595 })
596 .collect()
597 }};
598}
599macro_rules! extract_computations_with_offset {
600 ($inner:expr) => {{
601 let start_index = $inner.computations.start_index as usize;
602 let buffer_size = $inner.computations.elems.len();
603 let length = $inner.computations.length as usize;
604
605 (0..length)
606 .filter_map(|offset| {
607 let physical_index = (start_index + offset) % buffer_size;
608 if Self::is_valid(&$inner.computations.valid_bits, physical_index) {
610 Some((offset, physical_index))
611 } else {
612 None
613 }
614 })
615 .flat_map(|(offset, physical_index)| {
616 $inner.computations.elems[physical_index]
617 .entries
618 .iter()
619 .copied()
620 .filter(|c| !is_empty_computation_ref(c))
621 .map(move |c| (offset, c))
622 })
623 .collect()
624 }};
625}
626macro_rules! deserialize_mempool {
627 ($raw:expr, $mempool:ty, $variant:ident) => {{
628 let offset = <$mempool as Discriminator>::DISCRIMINATOR.len();
629 if offset + std::mem::size_of::<$mempool>() > $raw.len() {
630 return Err(ComputationPoolError::InvalidSize);
631 }
632 let data = bytemuck::pod_read_unaligned::<$mempool>(
633 &$raw[offset..offset + std::mem::size_of::<$mempool>()],
634 );
635 let capacity = data.inner.computations.elems.len();
637 let start_index = data.inner.computations.start_index as usize;
638 if start_index >= capacity {
639 return Err(ComputationPoolError::InvalidStartIndex {
640 start_index,
641 capacity,
642 });
643 }
644 Ok(MempoolWrapper::$variant(Box::new(data)))
645 }};
646}
647
648impl MempoolWrapper {
649 pub fn last_updated_slot(&self) -> u64 {
651 match self {
652 MempoolWrapper::Tiny(m) => m.inner.last_updated_slot,
653 MempoolWrapper::Small(m) => m.inner.last_updated_slot,
654 MempoolWrapper::Medium(m) => m.inner.last_updated_slot,
655 MempoolWrapper::Large(m) => m.inner.last_updated_slot,
656 }
657 }
658
659 pub fn tier(&self) -> MempoolSize {
661 match self {
662 MempoolWrapper::Tiny(_) => MempoolSize::Tiny,
663 MempoolWrapper::Small(_) => MempoolSize::Small,
664 MempoolWrapper::Medium(_) => MempoolSize::Medium,
665 MempoolWrapper::Large(_) => MempoolSize::Large,
666 }
667 }
668
669 pub fn slot_capacity(&self) -> usize {
673 match self {
674 MempoolWrapper::Tiny(m) => m.inner.computations.elems[0].entries.len(),
675 MempoolWrapper::Small(m) => m.inner.computations.elems[0].entries.len(),
676 MempoolWrapper::Medium(m) => m.inner.computations.elems[0].entries.len(),
677 MempoolWrapper::Large(m) => m.inner.computations.elems[0].entries.len(),
678 }
679 }
680
681 pub fn computations_raw(self) -> Vec<(bool, Vec<ComputationReference>, usize, usize)> {
682 match self {
683 MempoolWrapper::Tiny(tm) => {
684 let len = tm.inner.computations.elems.len();
687 let start_index = tm.inner.computations.start_index as usize;
688 let mut res = vec![Default::default(); len];
689 for (i, h) in tm.inner.computations.elems.iter().enumerate() {
690 let normalized_i = if i >= start_index {
691 i - start_index
692 } else {
693 len - start_index + i
694 };
695 res[normalized_i] = (
696 Self::is_valid(&tm.inner.computations.valid_bits, i),
697 h.entries
698 .iter()
699 .copied()
700 .filter(|c| !is_empty_computation_ref(c))
701 .collect(),
702 normalized_i,
703 i,
704 );
705 }
706 res
707 }
708 MempoolWrapper::Small(sm) => {
709 let len = sm.inner.computations.elems.len();
710 let start_index = sm.inner.computations.start_index as usize;
711 let mut res = vec![Default::default(); len];
712 for (i, h) in sm.inner.computations.elems.iter().enumerate() {
713 let normalized_i = if i >= start_index {
714 i - start_index
715 } else {
716 len - start_index + i
717 };
718 res[normalized_i] = (
719 Self::is_valid(&sm.inner.computations.valid_bits, i),
720 h.entries
721 .iter()
722 .copied()
723 .filter(|c| !is_empty_computation_ref(c))
724 .collect(),
725 normalized_i,
726 i,
727 );
728 }
729 res
730 }
731 MempoolWrapper::Medium(mm) => {
732 let len = mm.inner.computations.elems.len();
733 let start_index = mm.inner.computations.start_index as usize;
734 let mut res = vec![Default::default(); len];
735 for (i, h) in mm.inner.computations.elems.iter().enumerate() {
736 let normalized_i = if i >= start_index {
737 i - start_index
738 } else {
739 len - start_index + i
740 };
741 res[normalized_i] = (
742 Self::is_valid(&mm.inner.computations.valid_bits, i),
743 h.entries
744 .iter()
745 .copied()
746 .filter(|c| !is_empty_computation_ref(c))
747 .collect(),
748 normalized_i,
749 i,
750 );
751 }
752 res
753 }
754 MempoolWrapper::Large(lm) => {
755 let len = lm.inner.computations.elems.len();
756 let start_index = lm.inner.computations.start_index as usize;
757 let mut res = vec![Default::default(); len];
758 for (i, h) in lm.inner.computations.elems.iter().enumerate() {
759 let normalized_i = if i >= start_index {
760 i - start_index
761 } else {
762 len - start_index + i
763 };
764 res[normalized_i] = (
765 Self::is_valid(&lm.inner.computations.valid_bits, i),
766 h.entries
767 .iter()
768 .copied()
769 .filter(|c| !is_empty_computation_ref(c))
770 .collect(),
771 normalized_i,
772 i,
773 );
774 }
775 res
776 }
777 }
778 }
779
780 pub fn computations(self) -> Vec<ComputationReference> {
781 match self {
782 MempoolWrapper::Tiny(tm) => extract_computations!(tm.inner),
783 MempoolWrapper::Small(sm) => extract_computations!(sm.inner),
784 MempoolWrapper::Medium(mm) => extract_computations!(mm.inner),
785 MempoolWrapper::Large(lm) => extract_computations!(lm.inner),
786 }
787 }
788
789 pub fn computations_with_offset(self) -> Vec<(usize, ComputationReference)> {
790 match self {
791 MempoolWrapper::Tiny(tm) => extract_computations_with_offset!(tm.inner),
792 MempoolWrapper::Small(sm) => extract_computations_with_offset!(sm.inner),
793 MempoolWrapper::Medium(mm) => extract_computations_with_offset!(mm.inner),
794 MempoolWrapper::Large(lm) => extract_computations_with_offset!(lm.inner),
795 }
796 }
797
798 pub fn computations_highest_prio(self) -> Vec<ComputationReference> {
799 match self {
800 MempoolWrapper::Tiny(tm) => extract_computations_highest_prio!(tm.inner),
801 MempoolWrapper::Small(sm) => extract_computations_highest_prio!(sm.inner),
802 MempoolWrapper::Medium(mm) => extract_computations_highest_prio!(mm.inner),
803 MempoolWrapper::Large(lm) => extract_computations_highest_prio!(lm.inner),
804 }
805 }
806 pub fn from_raw(raw_mempool: &[u8]) -> Result<Self, ComputationPoolError> {
808 if raw_mempool.len() < 8 {
809 return Err(ComputationPoolError::InvalidSize);
810 }
811 match &raw_mempool[0..8] {
812 TinyMempool::DISCRIMINATOR => deserialize_mempool!(raw_mempool, TinyMempool, Tiny),
813 SmallMempool::DISCRIMINATOR => deserialize_mempool!(raw_mempool, SmallMempool, Small),
814 MediumMempool::DISCRIMINATOR => {
815 deserialize_mempool!(raw_mempool, MediumMempool, Medium)
816 }
817 LargeMempool::DISCRIMINATOR => deserialize_mempool!(raw_mempool, LargeMempool, Large),
818 _ => Err(ComputationPoolError::InvalidDiscriminator),
819 }
820 }
821
822 fn is_valid(valid_bits: &[u8], idx: usize) -> bool {
824 let byte = idx / 8;
825 let bit = idx - (byte * 8);
826
827 if byte >= valid_bits.len() {
828 return false;
829 }
830
831 (valid_bits[byte] & (1 << bit)) != 0
832 }
833}
834
835pub fn is_empty_computation_ref(c: &ComputationReference) -> bool {
836 *c == ComputationReference::zeroed()
837}
838
839impl std::fmt::Display for ComputationReference {
840 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
841 write!(
842 f,
843 "Computation offset: {}, priority fee: {}",
844 self.computation_offset, self.priority_fee
845 )
846 }
847}
848
849#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
850pub struct ExecpoolInfo {
851 pub cluster: Pubkey,
852 pub mxe: Pubkey,
853 pub execpool: Pubkey,
854}
855
856pub enum ExecpoolWrapper {
857 Tiny(Box<TinyExecPool>),
858 Small(Box<SmallExecPool>),
859 Medium(Box<MediumExecPool>),
860 Large(Box<LargeExecPool>),
861}
862
863impl ExecpoolWrapper {
864 pub fn tier(&self) -> MempoolSize {
866 match self {
867 ExecpoolWrapper::Tiny(_) => MempoolSize::Tiny,
868 ExecpoolWrapper::Small(_) => MempoolSize::Small,
869 ExecpoolWrapper::Medium(_) => MempoolSize::Medium,
870 ExecpoolWrapper::Large(_) => MempoolSize::Large,
871 }
872 }
873
874 pub fn max_parallel(&self) -> usize {
876 match self {
877 ExecpoolWrapper::Tiny(p) => p.inner.currently_executing.len(),
878 ExecpoolWrapper::Small(p) => p.inner.currently_executing.len(),
879 ExecpoolWrapper::Medium(p) => p.inner.currently_executing.len(),
880 ExecpoolWrapper::Large(p) => p.inner.currently_executing.len(),
881 }
882 }
883
884 pub fn computations_unfiltered(self) -> Vec<ComputationReferenceWIndex> {
885 match self {
886 ExecpoolWrapper::Tiny(tm) => tm
887 .inner
888 .currently_executing
889 .into_iter()
890 .enumerate()
891 .map(|(i, reference)| ComputationReferenceWIndex {
892 reference,
893 index: tm.inner.meta[i].index,
894 })
895 .collect(),
896 ExecpoolWrapper::Small(sm) => sm
897 .inner
898 .currently_executing
899 .into_iter()
900 .enumerate()
901 .map(|(i, reference)| ComputationReferenceWIndex {
902 reference,
903 index: sm.inner.meta[i].index,
904 })
905 .collect(),
906 ExecpoolWrapper::Medium(mm) => mm
907 .inner
908 .currently_executing
909 .into_iter()
910 .enumerate()
911 .map(|(i, reference)| ComputationReferenceWIndex {
912 reference,
913 index: mm.inner.meta[i].index,
914 })
915 .collect(),
916 ExecpoolWrapper::Large(lm) => lm
917 .inner
918 .currently_executing
919 .into_iter()
920 .enumerate()
921 .map(|(i, reference)| ComputationReferenceWIndex {
922 reference,
923 index: lm.inner.meta[i].index,
924 })
925 .collect(),
926 }
927 }
928
929 pub fn computations(self) -> Vec<ComputationReferenceWIndex> {
930 self.computations_unfiltered()
931 .into_iter()
932 .filter(|computation| !is_empty_computation_ref(&computation.reference))
933 .collect()
934 }
935
936 pub fn from_raw(raw_mempool: &[u8]) -> Result<Self, ComputationPoolError> {
937 if raw_mempool.len() < 8 {
938 return Err(ComputationPoolError::InvalidSize);
939 }
940 match &raw_mempool[0..8] {
941 TinyExecPool::DISCRIMINATOR => {
942 let offset = TinyExecPool::DISCRIMINATOR.len();
943 if offset + std::mem::size_of::<TinyExecPool>() > raw_mempool.len() {
944 return Err(ComputationPoolError::InvalidSize);
945 }
946 let te = bytemuck::pod_read_unaligned::<TinyExecPool>(
947 &raw_mempool[offset..offset + std::mem::size_of::<TinyExecPool>()],
948 );
949 Ok(ExecpoolWrapper::Tiny(Box::new(te)))
950 }
951 SmallExecPool::DISCRIMINATOR => {
952 let offset = SmallExecPool::DISCRIMINATOR.len();
953 if offset + std::mem::size_of::<SmallExecPool>() > raw_mempool.len() {
954 return Err(ComputationPoolError::InvalidSize);
955 }
956 let se = bytemuck::pod_read_unaligned::<SmallExecPool>(
957 &raw_mempool[offset..offset + std::mem::size_of::<SmallExecPool>()],
958 );
959 Ok(ExecpoolWrapper::Small(Box::new(se)))
960 }
961 MediumExecPool::DISCRIMINATOR => {
962 let offset = MediumExecPool::DISCRIMINATOR.len();
963 if offset + std::mem::size_of::<MediumExecPool>() > raw_mempool.len() {
964 return Err(ComputationPoolError::InvalidSize);
965 }
966 let me = bytemuck::pod_read_unaligned::<MediumExecPool>(
967 &raw_mempool[offset..offset + std::mem::size_of::<MediumExecPool>()],
968 );
969 Ok(ExecpoolWrapper::Medium(Box::new(me)))
970 }
971 LargeExecPool::DISCRIMINATOR => {
972 let offset = LargeExecPool::DISCRIMINATOR.len();
973 if offset + std::mem::size_of::<LargeExecPool>() > raw_mempool.len() {
974 return Err(ComputationPoolError::InvalidSize);
975 }
976 let le = bytemuck::pod_read_unaligned::<LargeExecPool>(
977 &raw_mempool[offset..offset + std::mem::size_of::<LargeExecPool>()],
978 );
979 Ok(ExecpoolWrapper::Large(Box::new(le)))
980 }
981 _ => Err(ComputationPoolError::InvalidDiscriminator),
982 }
983 }
984}
985
986#[derive(PartialEq, Eq, Debug, Clone)]
987pub struct ComputationReferenceWIndex {
988 pub reference: ComputationReference,
989 pub index: u64,
990}
991
992impl LeaderSelector {
993 pub fn new_with_size(size: usize) -> Self {
994 let mut selector = Self::default();
995 selector.info.resize(size, Default::default());
996 selector
997 }
998}
999
1000#[cfg(test)]
1001mod tests {
1002 use super::*;
1003
1004 #[test]
1005 fn mempool_from_raw_empty_returns_invalid_size() {
1006 let result = MempoolWrapper::from_raw(&[]);
1007 assert!(matches!(result, Err(ComputationPoolError::InvalidSize)));
1008 }
1009
1010 #[test]
1011 fn mempool_from_raw_short_returns_invalid_size() {
1012 let result = MempoolWrapper::from_raw(&[0u8; 7]);
1013 assert!(matches!(result, Err(ComputationPoolError::InvalidSize)));
1014 }
1015
1016 #[test]
1017 fn mempool_from_raw_unknown_discriminator() {
1018 let result = MempoolWrapper::from_raw(&[0xFF; 8]);
1019 assert!(matches!(
1020 result,
1021 Err(ComputationPoolError::InvalidDiscriminator)
1022 ));
1023 }
1024
1025 #[test]
1026 fn execpool_from_raw_empty_returns_invalid_size() {
1027 let result = ExecpoolWrapper::from_raw(&[]);
1028 assert!(matches!(result, Err(ComputationPoolError::InvalidSize)));
1029 }
1030
1031 #[test]
1032 fn execpool_from_raw_short_returns_invalid_size() {
1033 let result = ExecpoolWrapper::from_raw(&[0u8; 7]);
1034 assert!(matches!(result, Err(ComputationPoolError::InvalidSize)));
1035 }
1036
1037 #[test]
1038 fn execpool_from_raw_unknown_discriminator() {
1039 let result = ExecpoolWrapper::from_raw(&[0xFF; 8]);
1040 assert!(matches!(
1041 result,
1042 Err(ComputationPoolError::InvalidDiscriminator)
1043 ));
1044 }
1045
1046 #[test]
1050 fn mempool_from_raw_valid_discriminator_short_body_returns_invalid_size() {
1051 use anchor_lang::Discriminator;
1052 let mut raw = Vec::with_capacity(32);
1053 raw.extend_from_slice(TinyMempool::DISCRIMINATOR);
1054 raw.resize(32, 0u8); let result = MempoolWrapper::from_raw(&raw);
1056 assert!(matches!(result, Err(ComputationPoolError::InvalidSize)));
1057 }
1058
1059 #[test]
1060 fn is_valid_returns_false_for_index_past_valid_bits() {
1061 let bits = [0xFF_u8];
1064 assert!(!MempoolWrapper::is_valid(&bits, 8));
1065 assert!(!MempoolWrapper::is_valid(&bits, usize::MAX));
1066 }
1067
1068 fn expected_normalized(i: usize, start_index: usize, len: usize) -> usize {
1070 if i >= start_index {
1071 i - start_index
1072 } else {
1073 len - start_index + i
1074 }
1075 }
1076
1077 fn encoded_mempool_bytes<M>(mp: &M) -> Vec<u8>
1079 where
1080 M: bytemuck::Pod + anchor_lang::Discriminator,
1081 {
1082 let mut raw = Vec::with_capacity(M::DISCRIMINATOR.len() + std::mem::size_of::<M>());
1083 raw.extend_from_slice(M::DISCRIMINATOR);
1084 raw.extend_from_slice(bytemuck::bytes_of(mp));
1085 raw
1086 }
1087
1088 #[test]
1089 fn from_raw_rejects_out_of_range_start_index_tiny() {
1090 let mut mp: Box<TinyMempool> = bytemuck::zeroed_box();
1091 let capacity = mp.inner.computations.elems.len();
1092 assert!(capacity > 0 && capacity < u8::MAX as usize);
1093 mp.inner.computations.start_index = u8::MAX;
1094
1095 let raw = encoded_mempool_bytes(&*mp);
1096 assert!(matches!(
1097 MempoolWrapper::from_raw(&raw),
1098 Err(ComputationPoolError::InvalidStartIndex { start_index, capacity: cap })
1099 if start_index == u8::MAX as usize && cap == capacity,
1100 ));
1101 }
1102
1103 #[test]
1104 fn from_raw_rejects_start_index_equal_to_capacity_tiny() {
1105 let mut mp: Box<TinyMempool> = bytemuck::zeroed_box();
1106 let capacity = mp.inner.computations.elems.len();
1107 assert!(capacity > 0 && capacity <= u8::MAX as usize);
1108 mp.inner.computations.start_index = capacity as u8;
1109
1110 let raw = encoded_mempool_bytes(&*mp);
1111 assert!(matches!(
1112 MempoolWrapper::from_raw(&raw),
1113 Err(ComputationPoolError::InvalidStartIndex { .. }),
1114 ));
1115 }
1116
1117 #[test]
1122 fn from_raw_accepts_in_range_start_index_and_rotates_tiny() {
1123 let mut mp: Box<TinyMempool> = bytemuck::zeroed_box();
1124 let len = mp.inner.computations.elems.len();
1125 assert!(len > 1);
1126 let start = len / 2;
1127 mp.inner.computations.start_index = start as u8;
1128
1129 let raw = encoded_mempool_bytes(&*mp);
1130 let wrapper = MempoolWrapper::from_raw(&raw).expect("in-range start_index must parse");
1131
1132 let out = wrapper.computations_raw();
1133 assert_eq!(out.len(), len);
1134 for i in 0..len {
1135 let expected = expected_normalized(i, start, len);
1136 assert_eq!(out[expected].3, i);
1137 }
1138 }
1139
1140 #[test]
1143 fn from_raw_accepts_start_index_at_upper_bound_tiny() {
1144 let mut mp: Box<TinyMempool> = bytemuck::zeroed_box();
1145 let capacity = mp.inner.computations.elems.len();
1146 assert!(capacity > 0 && capacity <= u8::MAX as usize);
1147 mp.inner.computations.start_index = (capacity - 1) as u8;
1148
1149 let raw = encoded_mempool_bytes(&*mp);
1150 let wrapper = MempoolWrapper::from_raw(&raw)
1151 .expect("start_index == capacity - 1 is in-range and must parse");
1152
1153 let out = wrapper.computations_raw();
1154 assert_eq!(out.len(), capacity);
1155 for i in 0..capacity {
1156 let expected = expected_normalized(i, capacity - 1, capacity);
1157 assert_eq!(out[expected].3, i);
1158 }
1159 }
1160
1161 #[test]
1162 fn computations_raw_tiny_rotates_mid_buffer_correctly() {
1163 let mut mp: Box<TinyMempool> = bytemuck::zeroed_box();
1164 let len = mp.inner.computations.elems.len();
1165 assert!(len > 1);
1166 let start = len / 2;
1167 mp.inner.computations.start_index = start as u8;
1168
1169 let out = MempoolWrapper::Tiny(mp).computations_raw();
1170 assert_eq!(out.len(), len);
1171 for i in 0..len {
1172 let expected = expected_normalized(i, start, len);
1173 assert_eq!(out[expected].3, i);
1174 }
1175 }
1176
1177 #[test]
1178 fn last_updated_slot_roundtrips_through_deserialization() {
1179 let mut mp: Box<TinyMempool> = bytemuck::zeroed_box();
1180 mp.inner.last_updated_slot = 4_242;
1181
1182 let raw = encoded_mempool_bytes(&*mp);
1183 let wrapper = MempoolWrapper::from_raw(&raw).expect("must parse");
1184
1185 assert_eq!(wrapper.last_updated_slot(), 4_242);
1186 }
1187
1188 fn fabricate_comp(offset_id: u64) -> ComputationReference {
1189 let mut comp: ComputationReference = bytemuck::Zeroable::zeroed();
1190 comp.computation_offset = offset_id;
1191 comp
1192 }
1193
1194 #[test]
1195 fn computations_with_offset_yields_logical_offsets_unrotated() {
1196 let mut mp: Box<TinyMempool> = bytemuck::zeroed_box();
1197 mp.inner.last_updated_slot = 100;
1198 mp.inner.computations.start_index = 0;
1199 mp.inner.computations.length = 3;
1200 mp.inner.computations.valid_bits[0] = 0b0000_0111;
1201 mp.inner.computations.elems[0].entries[0] = fabricate_comp(10);
1202 mp.inner.computations.elems[1].entries[0] = fabricate_comp(20);
1203 mp.inner.computations.elems[2].entries[0] = fabricate_comp(30);
1204
1205 let raw = encoded_mempool_bytes(&*mp);
1206 let wrapper = MempoolWrapper::from_raw(&raw).expect("must parse");
1207
1208 let out = wrapper.computations_with_offset();
1209 assert_eq!(out.len(), 3);
1210 assert_eq!(out[0], (0, fabricate_comp(10)));
1211 assert_eq!(out[1], (1, fabricate_comp(20)));
1212 assert_eq!(out[2], (2, fabricate_comp(30)));
1213 }
1214
1215 #[test]
1216 fn computations_with_offset_preserves_logical_order_when_rotated() {
1217 let mut mp: Box<TinyMempool> = bytemuck::zeroed_box();
1218 let buf_len = mp.inner.computations.elems.len();
1219 let start = buf_len - 1;
1220 mp.inner.computations.start_index =
1221 u8::try_from(start).expect("fits in u8 for TinyMempool");
1222 mp.inner.computations.length = 2;
1223 let phys_0 = start % buf_len;
1225 let phys_1 = (start + 1) % buf_len;
1226 mp.inner.computations.valid_bits[phys_0 / 8] |= 1 << (phys_0 % 8);
1227 mp.inner.computations.valid_bits[phys_1 / 8] |= 1 << (phys_1 % 8);
1228 mp.inner.computations.elems[phys_0].entries[0] = fabricate_comp(10);
1229 mp.inner.computations.elems[phys_1].entries[0] = fabricate_comp(20);
1230
1231 let raw = encoded_mempool_bytes(&*mp);
1232 let wrapper = MempoolWrapper::from_raw(&raw).expect("must parse");
1233
1234 let out = wrapper.computations_with_offset();
1235 assert_eq!(out.len(), 2);
1236 assert_eq!(out[0], (0, fabricate_comp(10)));
1237 assert_eq!(out[1], (1, fabricate_comp(20)));
1238 }
1239
1240 #[test]
1245 fn computations_filters_by_physical_valid_bits_when_rotated() {
1246 let mut mp: Box<TinyMempool> = bytemuck::zeroed_box();
1247 let buf_len = mp.inner.computations.elems.len();
1248 let start = buf_len / 2;
1249 mp.inner.computations.start_index =
1250 u8::try_from(start).expect("fits in u8 for TinyMempool");
1251 mp.inner.computations.length = 2;
1252 let phys_0 = start % buf_len;
1253 let phys_1 = (start + 1) % buf_len;
1254 mp.inner.computations.valid_bits[phys_0 / 8] |= 1 << (phys_0 % 8);
1255 mp.inner.computations.valid_bits[phys_1 / 8] |= 1 << (phys_1 % 8);
1256 mp.inner.computations.elems[phys_0].entries[0] = fabricate_comp(11);
1257 mp.inner.computations.elems[phys_1].entries[0] = fabricate_comp(22);
1258
1259 let raw = encoded_mempool_bytes(&*mp);
1260 let wrapper = MempoolWrapper::from_raw(&raw).expect("must parse");
1261
1262 let out = wrapper.computations();
1263 assert_eq!(out.len(), 2);
1264 assert!(out.iter().any(|c| c == &fabricate_comp(11)));
1265 assert!(out.iter().any(|c| c == &fabricate_comp(22)));
1266 }
1267
1268 #[test]
1272 fn mempool_wrapper_reports_tier_and_slot_capacity() {
1273 let tiny = MempoolWrapper::Tiny(bytemuck::zeroed_box());
1274 assert!(matches!(tiny.tier(), MempoolSize::Tiny));
1275 assert_eq!(tiny.slot_capacity(), 1);
1276
1277 let small = MempoolWrapper::Small(bytemuck::zeroed_box());
1278 assert!(matches!(small.tier(), MempoolSize::Small));
1279 assert_eq!(small.slot_capacity(), 3);
1280
1281 let medium = MempoolWrapper::Medium(bytemuck::zeroed_box());
1282 assert!(matches!(medium.tier(), MempoolSize::Medium));
1283 assert_eq!(medium.slot_capacity(), 10);
1284
1285 let large = MempoolWrapper::Large(bytemuck::zeroed_box());
1286 assert!(matches!(large.tier(), MempoolSize::Large));
1287 assert_eq!(large.slot_capacity(), 100);
1288 }
1289
1290 #[test]
1291 fn execpool_wrapper_reports_tier_and_max_parallel() {
1292 let tiny = ExecpoolWrapper::Tiny(bytemuck::zeroed_box());
1293 assert!(matches!(tiny.tier(), MempoolSize::Tiny));
1294 assert_eq!(tiny.max_parallel(), 1);
1295
1296 let small = ExecpoolWrapper::Small(bytemuck::zeroed_box());
1297 assert!(matches!(small.tier(), MempoolSize::Small));
1298 assert_eq!(small.max_parallel(), 3);
1299
1300 let medium = ExecpoolWrapper::Medium(bytemuck::zeroed_box());
1301 assert!(matches!(medium.tier(), MempoolSize::Medium));
1302 assert_eq!(medium.max_parallel(), 10);
1303
1304 let large = ExecpoolWrapper::Large(bytemuck::zeroed_box());
1305 assert!(matches!(large.tier(), MempoolSize::Large));
1306 assert_eq!(large.max_parallel(), 100);
1307 }
1308
1309 #[test]
1310 fn mempool_tier_from_discriminator_maps_all_tiers() {
1311 assert!(matches!(
1312 mempool_tier_from_discriminator(TinyMempool::DISCRIMINATOR),
1313 Some(MempoolSize::Tiny)
1314 ));
1315 assert!(matches!(
1316 mempool_tier_from_discriminator(SmallMempool::DISCRIMINATOR),
1317 Some(MempoolSize::Small)
1318 ));
1319 assert!(matches!(
1320 mempool_tier_from_discriminator(MediumMempool::DISCRIMINATOR),
1321 Some(MempoolSize::Medium)
1322 ));
1323 assert!(matches!(
1324 mempool_tier_from_discriminator(LargeMempool::DISCRIMINATOR),
1325 Some(MempoolSize::Large)
1326 ));
1327 assert!(mempool_tier_from_discriminator(&[0xFF; 8]).is_none());
1328 }
1329}