Skip to main content

arcium_client/
state.rs

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
34/// Minimum context slot for filtering cluster accounts.
35/// This helps skip stale accounts from old program versions.
36/// Set to last known program upgrade slot
37pub const MIN_CLUSTER_CONTEXT_SLOT: u64 = 0;
38
39/// Errors that can occur during cluster offset discovery
40#[derive(Error, Debug, Clone, PartialEq)]
41pub enum ClusterOffsetError {
42    /// Failed to fetch node accounts from RPC
43    #[error("Failed to fetch node accounts from RPC: {0}")]
44    AccountFetchFailed(String),
45    /// Failed to deserialize node account data
46    #[error("Failed to deserialize node account data: {0}")]
47    DeserializationFailed(String),
48    /// Found an inactive node in the cluster
49    #[error("Found inactive node in cluster at offset {0}")]
50    InactiveNode(u32),
51    /// Cluster has no nodes
52    #[error("Cluster has no nodes")]
53    EmptyCluster,
54    /// Node has no cluster membership matching the target cluster
55    #[error("No cluster membership found for target cluster")]
56    NoClusterMembership,
57}
58
59/// Represents the state of a cluster offset lookup.
60#[derive(Debug, Clone, PartialEq)]
61pub enum ClusterOffsetState {
62    /// Offset successfully looked up, cluster is healthy with all nodes active
63    Available(u32),
64    /// Cluster exists but offset has not yet been looked up (or empty cluster)
65    NotLookedUp,
66    /// Cluster is unhealthy or unreachable
67    Unavailable(ClusterOffsetError),
68}
69
70impl ClusterOffsetState {
71    /// Returns true if the cluster is available with a known offset
72    pub fn is_available(&self) -> bool {
73        matches!(self, ClusterOffsetState::Available(_))
74    }
75
76    /// Returns the offset if available
77    pub fn get(&self) -> Option<u32> {
78        match self {
79            ClusterOffsetState::Available(offset) => Some(*offset),
80            _ => None,
81        }
82    }
83
84    /// Returns the error if unavailable
85    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    // Proposals are all set to the current price by default, so we filter out duplicates
131    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
147/// Fetches all Cluster accounts from the Arcium program using discriminator filtering.
148///
149/// # Arguments
150/// * `rpc_client` - The RPC client to use for fetching accounts
151/// * `min_context_slot` - Optional minimum context slot to filter out stale accounts from old
152///   program versions
153pub 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        // Discriminator check is done server-side, so we can directly deserialize
192        match Cluster::try_deserialize(&mut data.as_slice()) {
193            Ok(cluster) => clusters.push((pubkey, cluster)),
194            Err(_) => continue, // Silently skip malformed accounts
195        }
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    // We want to search for MXE accounts that have us as their cluster. MXE accounts store the
209    // cluster they're assigned to in the first field which is an Option<u32> serialized to
210    // borsh. This means the first 13 bytes for valid mxes are:
211    // - 8 bytes of discriminator to specify mxe
212    // - 1 bytes set to 1 to specify Some(..)
213    // - 4 bytes matching the cluster offset
214    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    // Fetch 0 length of data, as we only care about the count of accounts but not their content
223    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/// Enriched cluster information for display and decision-making.
248/// Wraps the IDL-generated Cluster account with additional metadata.
249#[derive(Debug, Clone)]
250pub struct ClusterInfo {
251    pub pubkey: Pubkey,
252    /// Cluster offset lookup state
253    pub offset: ClusterOffsetState,
254    pub cluster: Cluster,
255    /// Number of MXEs in the cluster, optional because we don't know it yet
256    pub mxe_count: usize,
257}
258
259impl ClusterInfo {
260    /// Get the number of nodes currently in the cluster
261    pub fn node_count(&self) -> usize {
262        self.cluster.nodes.len()
263    }
264
265    /// Get the maximum number of nodes allowed
266    pub fn max_nodes(&self) -> u16 {
267        self.cluster.cluster_size
268    }
269
270    /// Get the number of pending nodes
271    pub fn pending_node_count(&self) -> usize {
272        self.cluster.pending_nodes.len()
273    }
274
275    /// Calculate node utilization as a percentage
276    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
286/// Fetches all clusters and enriches with computed metrics.
287///
288/// Cluster offsets are resolved by querying node cluster memberships.
289/// Empty clusters (no nodes) will have `offset: None`.
290///
291/// # Arguments
292/// * `rpc_client` - The RPC client to use for fetching accounts
293/// * `_current_epoch` - Current epoch (reserved for future use)
294/// * `min_context_slot` - Optional minimum context slot to filter out stale accounts from old
295///   program versions
296///
297/// # Performance
298/// O(N) RPC calls instead of O(N × 1000) PDA derivations.
299pub 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        // Look up cluster offset and validate all nodes are active
309        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
328/// Efficiently finds cluster offset by validating all nodes are active.
329/// Returns error if ANY node is inactive - we only recommend fully healthy clusters.
330/// Also returns error for empty clusters, fetch failures, or missing cluster membership.
331async 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    // Safety: Ensure returned accounts match requested nodes
355    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
403/// Gets the current epoch from the on-chain clock account
404pub 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
425// This is a zero-copy account with different size variants, so we leave the deserialization to the
426// caller.
427pub 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
435/// Maps a mempool account discriminator to its cluster tier, if recognized.
436pub 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
446/// Fetches the cluster tier for each cluster offset using batched, discriminator-only
447/// (8-byte) account fetches, avoiding multi-MB mempool downloads. Returns one entry per
448/// input offset; `None` when the account is missing or has an unrecognized discriminator.
449/// `min_context_slot` applies the same freshness guard as the other fetchers in this module.
450pub 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    // getMultipleAccounts is capped at 100 accounts per request.
456    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
500// This is a zero-copy account with different size variants, so we leave the deserialization to the
501// caller.
502pub 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
517// TODO: Check support for medium and large mempools (current problem is they're too big and
518// cause a stack overflow) We should do an optimization where we only fetch/receive the
519// heap at current_slot (since that's the only thing that changes), and not the entire mempool
520pub 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                // This is a circular buffer, so we need to normalize the index
554                let normalized_i = if *i >= start_index {
555                    *i - start_index
556                } else {
557                    buffer_size - start_index + *i
558                };
559                // valid_bits is indexed by physical position on-chain.
560                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                // Normalize circular buffer index
581                let normalized_i = if i >= start_index {
582                    i - start_index
583                } else {
584                    buffer_size - start_index + i
585                };
586
587                // valid_bits is indexed by physical position on-chain.
588                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                // valid_bits is indexed by physical position on-chain.
609                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        // Reject malformed start_index; downstream readers rely on the invariant.
636        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    /// Last slot at which the on-chain buffer actually advanced, or 0 if never.
650    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    /// The cluster tier this mempool was initialized at.
660    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    /// Maximum computations that can be queued within a single Solana slot (the
670    /// capacity of each per-slot heap in the mempool's circular buffer). Queueing
671    /// beyond this within one slot fails with `HeapFull`.
672    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                // Iterate by reference through the Box; dereffing would move the full
685                // (multi-MB) mempool onto the stack and overflow debug stacks.
686                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    // Returns Err if the mempool data is too short or has an unrecognized discriminator
807    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    // Returns true if the heap at index idx is valid (i.e. not stale)
823    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    /// The cluster tier this execpool was initialized at.
865    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    /// Maximum computations the cluster executes in parallel.
875    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    // Valid discriminator but truncated body: the outer `len < 8` check passes, but
1047    // the inner deserialize_mempool!/ExecPool bounds check (`offset + size_of::<T>()
1048    // > raw.len()`) must still reject to avoid out-of-bounds pod reads.
1049    #[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); // far shorter than size_of::<TinyMempool>()
1055        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        // valid_bits only covers 8 bits; anything at or past that index must be
1062        // false, not panic, regardless of content.
1063        let bits = [0xFF_u8];
1064        assert!(!MempoolWrapper::is_valid(&bits, 8));
1065        assert!(!MempoolWrapper::is_valid(&bits, usize::MAX));
1066    }
1067
1068    // The 4th tuple field of a computations_raw row is the source index `i`.
1069    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    // Mirrors what from_raw consumes: discriminator ++ bytemuck(inner).
1078    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    // Small/Medium/Large share the deserialize_mempool! expansion with Tiny. A parallel
1118    // from_raw test on those variants overflows the debug thread stack via
1119    // pod_read_unaligned's by-value return; hardening that is a separate fix.
1120
1121    #[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    // Pairs with from_raw_rejects_start_index_equal_to_capacity_tiny to pin both sides
1141    // of the boundary; catches an off-by-one in either direction.
1142    #[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        // valid_bits is indexed by physical position (matches on-chain).
1224        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    // Regression test for a latent bug where `extract_computations!` looked up
1241    // `valid_bits` by logical offset, while on-chain the buffer stores them by
1242    // physical position. With `start_index != 0` the wrong bit was read, so
1243    // valid entries could be dropped or stale ones included.
1244    #[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    // Wrappers are constructed directly: parsing non-Tiny tiers through from_raw
1269    // overflows the debug-build test stack (see the MempoolWrapper TODO about
1270    // medium/large mempools), and from_raw's tier dispatch is already covered above.
1271    #[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}