Skip to main content

arcium_anchor/
lib.rs

1use anchor_lang::prelude::*;
2use arcium_client::{
3    idl::{
4        arcium::{
5            accounts::Cluster,
6            cpi::{accounts::InitComputationDefinition, init_computation_definition},
7            types::{
8                AccountArgument,
9                ArgumentList,
10                ArgumentRef,
11                CallbackInstruction,
12                CircuitSource,
13                ComputationDefinitionMeta,
14                ComputationSignature,
15                Parameter,
16                SetUnset,
17            },
18        },
19        BLSDomainSeparator,
20    },
21    pda::{CLOCK_PDA, FEE_POOL_PDA},
22};
23// Re-export for use in derive_mxe_lut_pda macro
24pub use solana_address_lookup_table_interface;
25use solana_alt_bn128_bls::Sha256Normalized;
26use traits::{InitCompDefAccs, QueueCompAccs};
27
28pub mod traits;
29
30pub mod prelude {
31    pub use super::*;
32    pub use arcium_client::idl::arcium::{
33        accounts::{ClockAccount, Cluster, ComputationDefinitionAccount, FeePool, MXEAccount},
34        program::Arcium,
35        types::{AccountArgument, ArgumentList, ArgumentRef},
36        ID_CONST as ARCIUM_PROG_ID,
37    };
38    pub use arcium_macros::{
39        arcium_callback,
40        arcium_program,
41        callback_accounts,
42        check_args,
43        init_computation_definition_accounts,
44        queue_computation_accounts,
45    };
46    pub use traits::CallbackCompAccs;
47    pub use ArgBuilder;
48    pub use LUT_PROGRAM_ID;
49}
50
51#[derive(AnchorSerialize, AnchorDeserialize)]
52pub struct SharedEncryptedStruct<const LEN: usize> {
53    pub encryption_key: [u8; 32],
54    pub nonce: u128,
55    pub ciphertexts: [[u8; 32]; LEN],
56}
57
58#[derive(AnchorSerialize, AnchorDeserialize)]
59pub struct MXEEncryptedStruct<const LEN: usize> {
60    pub nonce: u128,
61    pub ciphertexts: [[u8; 32]; LEN],
62}
63
64#[derive(AnchorSerialize, AnchorDeserialize)]
65pub struct EncDataStruct<const LEN: usize> {
66    pub ciphertexts: [[u8; 32]; LEN],
67}
68
69#[error_code]
70pub enum ArciumError {
71    AbortedComputation,
72    BLSSignatureVerificationFailed,
73    InvalidClusterBLSPublicKey,
74    InvalidComputationAccount,
75    MarkerForIdlBuildUsageNotAllowed,
76    OutputTooLarge,
77    TrustedDealer,
78    #[msg("Multi-transaction callbacks disabled; enable 'multi-tx-callbacks' feature")]
79    MultiTxCallbacksDisabled,
80    Serialization,
81    Router,
82    Circuit,
83    Inputs,
84    ProtocolInit,
85    ProtocolRun,
86    InvalidMAC,
87    ExpectedSentShare,
88    ExpectedFieldElement,
89    ExpectedAbort,
90    MalformedData,
91    ComputationFailed,
92    InternalError,
93    PreprocessingStreamError,
94    DivisionByZero,
95    NoSignature,
96    InvalidSignature,
97    PrimitiveError,
98    InvalidBatchLength,
99    QuadraticNonResidue,
100    BitConversionError,
101    ChannelClosed,
102    TimeoutElapsed,
103    KeysDigestMismatch,
104    X25519PubkeyMismatch,
105    Ed25519VerifyingKeyMismatch,
106    ElGamalPubkeyMismatch,
107    TooManyCorruptPeers,
108    NoPubkeysSet,
109    KeyRecoverySerialization,
110}
111
112#[derive(Debug, AnchorSerialize, AnchorDeserialize)]
113pub enum RawComputationOutputs<O: AnchorDeserialize + AnchorSerialize> {
114    Success(O),
115    Failure,
116}
117
118/// Output types with a compile-time serialized size.
119pub trait HasSize {
120    const SIZE: usize;
121}
122
123/// Signed computation result encoded as raw bytes plus a BLS signature.
124#[derive(Debug)]
125pub enum SignedComputationOutputs<O: HasSize + AnchorDeserialize + AnchorSerialize> {
126    Success(Vec<u8>, [u8; 64]),
127    Failure([u8; 64]),
128    MarkerForIdlBuildDoNotUseThis(O),
129}
130
131impl<O: HasSize + AnchorDeserialize + AnchorSerialize> AnchorDeserialize
132    for SignedComputationOutputs<O>
133{
134    fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
135        let variant = u8::deserialize_reader(reader)?;
136        match variant {
137            variant if variant == (BLSDomainSeparator::Success as u8) => {
138                let mut bytes = vec![0u8; O::SIZE];
139                reader.read_exact(&mut bytes)?;
140                let mut sig = [0u8; 64];
141                reader.read_exact(&mut sig)?;
142                Ok(SignedComputationOutputs::Success(bytes, sig))
143            }
144            variant if variant == (BLSDomainSeparator::Failure as u8) => {
145                let mut sig = [0u8; 64];
146                reader.read_exact(&mut sig)?;
147                Ok(SignedComputationOutputs::Failure(sig))
148            }
149            _ => Err(std::io::Error::new(
150                std::io::ErrorKind::InvalidData,
151                "Invalid SignedComputationOutputs variant",
152            )),
153        }
154    }
155}
156
157impl<O: HasSize + AnchorDeserialize + AnchorSerialize> AnchorSerialize
158    for SignedComputationOutputs<O>
159{
160    fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
161        match self {
162            SignedComputationOutputs::Success(bytes, sig) => {
163                if bytes.len() != O::SIZE {
164                    return Err(std::io::Error::new(
165                        std::io::ErrorKind::InvalidInput,
166                        "SignedComputationOutputs payload must match O::SIZE",
167                    ));
168                }
169                (BLSDomainSeparator::Success as u8).serialize(writer)?;
170                writer.write_all(bytes)?;
171                writer.write_all(sig)?;
172            }
173            SignedComputationOutputs::Failure(sig) => {
174                (BLSDomainSeparator::Failure as u8).serialize(writer)?;
175                writer.write_all(sig)?;
176            }
177            SignedComputationOutputs::MarkerForIdlBuildDoNotUseThis(o) => {
178                255u8.serialize(writer)?;
179                o.serialize(writer)?;
180            }
181        }
182        Ok(())
183    }
184}
185
186impl<O: HasSize + AnchorDeserialize + AnchorSerialize> SignedComputationOutputs<O> {
187    pub fn verify_output_raw(
188        self,
189        arcium_cluster_acc: &Cluster,
190        computation_account: &UncheckedAccount,
191    ) -> Result<Vec<u8>> {
192        let bls_pubkey = match arcium_cluster_acc.bls_public_key {
193            SetUnset::Set(bls_pubkey) => bls_pubkey,
194            SetUnset::Unset(..) => return Err(ArciumError::InvalidClusterBLSPublicKey.into()),
195        };
196
197        let (slot, slot_counter) = get_slot_and_slot_counter_bytes(computation_account)?;
198
199        match self {
200            SignedComputationOutputs::Success(o_bytes, bls_sig_bytes) => {
201                let message = &[
202                    &[BLSDomainSeparator::Success as u8],
203                    o_bytes.as_slice(),
204                    // We hash slot and slot counter into the message as well to prevent replay
205                    // attacks
206                    slot.as_ref(),
207                    slot_counter.as_ref(),
208                ]
209                .concat();
210
211                // Convert bytes to BLS types
212                let bls_pubkey = solana_alt_bn128_bls::G2CompressedPoint(bls_pubkey.0);
213                let bls_sig = solana_alt_bn128_bls::G1Point(bls_sig_bytes);
214
215                // Verify the BLS signature
216                bls_pubkey
217                    .verify_signature::<Sha256Normalized, &[u8], solana_alt_bn128_bls::G1Point>(
218                        bls_sig, message,
219                    )
220                    .map_err(|_| ArciumError::BLSSignatureVerificationFailed)?;
221
222                Ok(o_bytes)
223            }
224            SignedComputationOutputs::Failure(_) => Err(ArciumError::AbortedComputation.into()),
225            SignedComputationOutputs::MarkerForIdlBuildDoNotUseThis(_) => {
226                Err(ArciumError::MarkerForIdlBuildUsageNotAllowed.into())
227            }
228        }
229    }
230
231    pub fn verify_output(
232        self,
233        arcium_cluster_acc: &Cluster,
234        computation_account: &UncheckedAccount,
235    ) -> Result<O> {
236        let raw = self.verify_output_raw(arcium_cluster_acc, computation_account)?;
237        Ok(O::try_from_slice(&raw)?)
238    }
239}
240
241/// Offset of the bump field in SignerAccount data (after 8-byte discriminator).
242const SIGNER_ACCOUNT_BUMP_OFFSET: usize = 8;
243
244pub fn queue_computation<'info, T>(
245    accs: &T,
246    computation_offset: u64,
247    args: ArgumentList,
248    callback_instructions: Vec<CallbackInstruction>,
249    num_callback_txs: u8,
250    cu_price_micro: u64,
251) -> Result<()>
252where
253    T: QueueCompAccs<'info>,
254{
255    #[cfg(not(feature = "multi-tx-callbacks"))]
256    if num_callback_txs != 1 {
257        return Err(error!(ArciumError::MultiTxCallbacksDisabled));
258    }
259
260    let bump = accs.signer_pda_bump();
261    let signer_seeds: &[&[&[u8]]] = &[&[SIGN_PDA_SEED, &[bump]]];
262    let queue_comp_accounts = accs.queue_comp_accs();
263    queue_comp_accounts.sign_seed.try_borrow_mut_data()?[SIGNER_ACCOUNT_BUMP_OFFSET] = bump;
264
265    let cpi_context =
266        CpiContext::new_with_signer(accs.arcium_program(), queue_comp_accounts, signer_seeds);
267    arcium_client::idl::arcium::cpi::queue_computation(
268        cpi_context,
269        computation_offset,
270        accs.comp_def_offset(),
271        args,
272        accs.mxe_program(),
273        callback_instructions,
274        num_callback_txs,
275        0,
276        cu_price_micro,
277    )
278}
279
280pub fn init_comp_def<'info, T>(
281    accs: &T,
282    circuit_source_override: Option<CircuitSource>,
283    finalize_authority: Option<Pubkey>,
284) -> Result<()>
285where
286    T: InitCompDefAccs<'info>,
287{
288    let cpi_context = CpiContext::new(
289        accs.arcium_program(),
290        InitComputationDefinition {
291            signer: accs.signer(),
292            system_program: accs.system_program(),
293            mxe: accs.mxe_acc(),
294            comp_def_acc: accs.comp_def_acc(),
295            address_lookup_table: accs.address_lookup_table(),
296            lut_program: accs.lut_program(),
297        },
298    );
299
300    let signature = ComputationSignature {
301        parameters: accs.params(),
302        outputs: accs.outputs(),
303    };
304    let computation_definition = ComputationDefinitionMeta {
305        circuit_len: accs.compiled_circuit_len(),
306        signature,
307    };
308    init_computation_definition(
309        cpi_context,
310        accs.comp_def_offset(),
311        accs.mxe_program(),
312        computation_definition,
313        circuit_source_override,
314        accs.weight(),
315        finalize_authority,
316    )?;
317
318    Ok(())
319}
320
321pub struct ShortVec<T: AnchorSerialize + AnchorDeserialize> {
322    pub data: Vec<T>,
323}
324
325impl<T: AnchorSerialize + AnchorDeserialize> AnchorSerialize for ShortVec<T> {
326    fn serialize<W: std::io::Write>(
327        &self,
328        writer: &mut W,
329    ) -> std::result::Result<(), std::io::Error> {
330        let len: u16 = self.data.len().try_into().map_err(|_| {
331            std::io::Error::new(
332                std::io::ErrorKind::InvalidInput,
333                "Length too large, must fit in u16",
334            )
335        })?;
336        len.serialize(writer)?;
337        for item in &self.data {
338            item.serialize(writer)?;
339        }
340        Ok(())
341    }
342}
343
344impl<T: AnchorSerialize + AnchorDeserialize> AnchorDeserialize for ShortVec<T> {
345    fn deserialize_reader<R: std::io::Read>(
346        reader: &mut R,
347    ) -> std::result::Result<Self, std::io::Error> {
348        let len: u16 = u16::deserialize_reader(reader)?;
349        let mut data = Vec::with_capacity(len as usize);
350        for _ in 0..len {
351            data.push(T::deserialize_reader(reader)?);
352        }
353        Ok(Self { data })
354    }
355}
356
357// Returns the slot and slot counter of the computation account as little endian u64 and u16
358fn get_slot_and_slot_counter_bytes(
359    computation_account: &UncheckedAccount,
360) -> Result<([u8; 8], [u8; 2])> {
361    const SLOT_OFFSET: usize = 100;
362    const SLOT_COUNTER_OFFSET: usize = 108;
363
364    let data = computation_account.try_borrow_data()?;
365
366    let slot_bytes: [u8; 8] = data
367        .get(SLOT_OFFSET..SLOT_OFFSET + 8)
368        .ok_or(ArciumError::InvalidComputationAccount)?
369        .try_into()
370        .map_err(|_| ArciumError::InvalidComputationAccount)?;
371
372    let slot_counter_bytes: [u8; 2] = data
373        .get(SLOT_COUNTER_OFFSET..SLOT_COUNTER_OFFSET + 2)
374        .ok_or(ArciumError::InvalidComputationAccount)?
375        .try_into()
376        .map_err(|_| ArciumError::InvalidComputationAccount)?;
377
378    Ok((slot_bytes, slot_counter_bytes))
379}
380
381#[cfg(feature = "idl-build")]
382impl<T: AnchorSerialize + AnchorDeserialize> anchor_lang::idl::build::IdlBuild for ShortVec<T> {
383    fn create_type() -> Option<anchor_lang::idl::types::IdlTypeDef> {
384        Some(anchor_lang::idl::types::IdlTypeDef {
385            name: Self::get_full_path(),
386            docs: vec![],
387            serialization: anchor_lang::idl::types::IdlSerialization::default(),
388            repr: None,
389            generics: <[_]>::into_vec(Box::new([
390                anchor_lang::idl::types::IdlTypeDefGeneric::Type { name: "T".into() }.into(),
391            ])),
392            ty: anchor_lang::idl::types::IdlTypeDefTy::Struct {
393                fields: Some(anchor_lang::idl::types::IdlDefinedFields::Named(
394                    <[_]>::into_vec(Box::new([anchor_lang::idl::types::IdlField {
395                        name: "data".into(),
396                        docs: vec![],
397                        ty: anchor_lang::idl::types::IdlType::Vec(Box::new(
398                            anchor_lang::idl::types::IdlType::Generic("T".into()),
399                        )),
400                    }])),
401                )),
402            },
403        })
404    }
405
406    fn insert_types(
407        types: &mut std::collections::BTreeMap<String, anchor_lang::idl::types::IdlTypeDef>,
408    ) {
409    }
410
411    fn get_full_path() -> String {
412        std::fmt::format(format_args!("{0}", "ShortVec"))
413    }
414}
415
416#[cfg(feature = "idl-build")]
417impl<O: HasSize + AnchorSerialize + AnchorDeserialize> anchor_lang::idl::build::IdlBuild
418    for SignedComputationOutputs<O>
419{
420    fn create_type() -> Option<anchor_lang::idl::types::IdlTypeDef> {
421        Some(anchor_lang::idl::types::IdlTypeDef {
422            name: Self::get_full_path(),
423            docs: vec![],
424            serialization: anchor_lang::idl::types::IdlSerialization::default(),
425            repr: None,
426            generics: <[_]>::into_vec(Box::new([
427                anchor_lang::idl::types::IdlTypeDefGeneric::Type { name: "O".into() }.into(),
428            ])),
429            ty: anchor_lang::idl::types::IdlTypeDefTy::Enum {
430                variants: vec![
431                    anchor_lang::idl::types::IdlEnumVariant {
432                        name: "Success".into(),
433                        fields: Some(anchor_lang::idl::types::IdlDefinedFields::Tuple(vec![
434                            anchor_lang::idl::types::IdlType::Generic("O".into()),
435                            anchor_lang::idl::types::IdlType::Array(
436                                Box::new(anchor_lang::idl::types::IdlType::U8),
437                                anchor_lang::idl::types::IdlArrayLen::Value(64),
438                            ),
439                        ])),
440                    },
441                    anchor_lang::idl::types::IdlEnumVariant {
442                        name: "Failure".into(),
443                        fields: None,
444                    },
445                    anchor_lang::idl::types::IdlEnumVariant {
446                        name: "MarkerForIdlBuildDoNotUseThis".into(),
447                        fields: Some(anchor_lang::idl::types::IdlDefinedFields::Tuple(vec![
448                            anchor_lang::idl::types::IdlType::Generic("O".into()),
449                        ])),
450                    },
451                ],
452            },
453        })
454    }
455
456    fn insert_types(
457        types: &mut std::collections::BTreeMap<String, anchor_lang::idl::types::IdlTypeDef>,
458    ) {
459    }
460
461    fn get_full_path() -> String {
462        "SignedComputationOutputs".to_string()
463    }
464}
465
466#[macro_export]
467macro_rules! derive_seed {
468    ($name:ident) => {
469        stringify!($name).as_bytes()
470    };
471}
472
473pub const fn comp_def_offset(conf_ix_name: &str) -> u32 {
474    let hasher = ::sha2_const_stable::Sha256::new();
475    let result = hasher.update(conf_ix_name.as_bytes()).finalize();
476    u32::from_le_bytes([result[0], result[1], result[2], result[3]])
477}
478
479pub const MXE_PDA_SEED: &[u8] = derive_seed!(MXEAccount);
480pub const MEMPOOL_PDA_SEED: &[u8] = b"Mempool";
481pub const EXECPOOL_PDA_SEED: &[u8] = b"Execpool";
482pub const COMP_PDA_SEED: &[u8] = derive_seed!(ComputationAccount);
483pub const COMP_DEF_PDA_SEED: &[u8] = derive_seed!(ComputationDefinitionAccount);
484pub const CLUSTER_PDA_SEED: &[u8] = derive_seed!(Cluster);
485pub const POOL_PDA_SEED: &[u8] = derive_seed!(FeePool);
486pub const CLOCK_PDA_SEED: &[u8] = derive_seed!(ClockAccount);
487pub const SIGN_PDA_SEED: &[u8] = derive_seed!(ArciumSignerAccount);
488
489pub const ARCIUM_CLOCK_ACCOUNT_ADDRESS: Pubkey = CLOCK_PDA.0;
490pub const ARCIUM_FEE_POOL_ACCOUNT_ADDRESS: Pubkey = FEE_POOL_PDA.0;
491pub const LUT_PROGRAM_ID: Pubkey = solana_address_lookup_table_interface::program::ID;
492
493#[macro_export]
494macro_rules! derive_mxe_pda {
495    () => {
496        Pubkey::find_program_address(&[MXE_PDA_SEED, ID.to_bytes().as_ref()], &ARCIUM_PROG_ID).0
497    };
498}
499
500#[macro_export]
501macro_rules! derive_mempool_pda {
502    ($mxe_account:expr, $error_path:expr) => {
503        Pubkey::find_program_address(
504            &[
505                MEMPOOL_PDA_SEED,
506                &$mxe_account.cluster.ok_or($error_path)?.to_le_bytes(),
507            ],
508            &ARCIUM_PROG_ID,
509        )
510        .0
511    };
512}
513
514#[macro_export]
515macro_rules! derive_execpool_pda {
516    ($mxe_account:expr, $error_path:expr) => {
517        Pubkey::find_program_address(
518            &[
519                EXECPOOL_PDA_SEED,
520                &$mxe_account.cluster.ok_or($error_path)?.to_le_bytes(),
521            ],
522            &ARCIUM_PROG_ID,
523        )
524        .0
525    };
526}
527
528#[macro_export]
529macro_rules! derive_comp_pda {
530    ($computation_offset:expr, $mxe_account:expr, $error_path:expr) => {
531        Pubkey::find_program_address(
532            &[
533                COMP_PDA_SEED,
534                &$mxe_account.cluster.ok_or($error_path)?.to_le_bytes(),
535                &$computation_offset.to_le_bytes(),
536            ],
537            &ARCIUM_PROG_ID,
538        )
539        .0
540    };
541}
542
543#[macro_export]
544macro_rules! derive_comp_def_pda {
545    ($conf_ix_name:expr) => {
546        Pubkey::find_program_address(
547            &[
548                COMP_DEF_PDA_SEED,
549                &ID_CONST.to_bytes(),
550                &$conf_ix_name.to_le_bytes(),
551            ],
552            &ARCIUM_PROG_ID,
553        )
554        .0
555    };
556}
557
558#[macro_export]
559macro_rules! derive_cluster_pda {
560    ($mxe_account:expr, $error_path:expr) => {
561        Pubkey::find_program_address(
562            &[
563                CLUSTER_PDA_SEED,
564                &$mxe_account.cluster.ok_or($error_path)?.to_le_bytes(),
565            ],
566            &ARCIUM_PROG_ID,
567        )
568        .0
569    };
570}
571
572#[macro_export]
573macro_rules! derive_sign_pda {
574    () => {
575        Pubkey::find_program_address(&[SIGN_PDA_SEED], &ID_CONST).0
576    };
577}
578
579#[macro_export]
580macro_rules! derive_mxe_lut_pda {
581    ($lut_offset:expr) => {{
582        let mxe_pda = derive_mxe_pda!();
583        ::arcium_anchor::solana_address_lookup_table_interface::instruction::derive_lookup_table_address(&mxe_pda, $lut_offset).0
584    }};
585}
586include!("arg_builder.rs");
587include!("arg_match_param.rs");
588pub const fn const_match_computation(
589    arguments: &[ArgumentRef],
590    accounts: &[AccountArgument],
591    parameters: &[Parameter],
592) {
593    if let Err(err) = args_match_params(arguments, accounts, parameters) {
594        err.const_panic();
595    }
596}
597
598#[cfg(test)]
599mod tests {
600    use super::*;
601    use arcium_client::idl::arcium::{
602        accounts::ComputationAccount,
603        types::{ComputationStatus, ExecutionFee},
604        ID_CONST as ARCIUM_PROG_ID,
605    };
606    use std::{cell::RefCell, rc::Rc};
607
608    fn derive_arcium_pda(seeds: &[&[u8]]) -> Pubkey {
609        Pubkey::find_program_address(seeds, &ARCIUM_PROG_ID).0
610    }
611
612    #[test]
613    fn test_comp_def_offset() {
614        let conf_ix_name = "add_together";
615        let offset = comp_def_offset(conf_ix_name);
616        assert_eq!(offset, 4005749700);
617    }
618
619    #[test]
620    fn test_clock_account_address() {
621        let address = derive_arcium_pda(&[CLOCK_PDA_SEED]);
622        assert_eq!(address, ARCIUM_CLOCK_ACCOUNT_ADDRESS);
623    }
624
625    #[test]
626    fn test_fee_pool_account_address() {
627        let address = derive_arcium_pda(&[POOL_PDA_SEED]);
628        assert_eq!(address, ARCIUM_FEE_POOL_ACCOUNT_ADDRESS);
629    }
630
631    #[test]
632    fn test_get_slot_and_slot_counter_bytes() {
633        let computation_account = ComputationAccount {
634            payer: Pubkey::default(),
635            mxe_program_id: Pubkey::default(),
636            computation_definition_offset: 0,
637            execution_fee: ExecutionFee {
638                base_fee: 0,
639                priority_fee: 0,
640                output_delivery_fee: 0,
641            },
642            slot: 12345,
643            slot_counter: 5678,
644            status: ComputationStatus::Queued,
645            arguments: ArgumentList {
646                args: vec![],
647                byte_arrays: vec![],
648                plaintext_numbers: vec![],
649                values_128_bit: vec![],
650                accounts: vec![],
651            },
652            custom_callback_instructions: Vec::new(),
653            callback_transactions_required: 0,
654            callback_transactions_submitted_bm: 0,
655            bump: 0,
656        };
657
658        let mut key = Pubkey::default();
659        let mut lamports = 0;
660        let mut data = vec![];
661        let mut owner = Pubkey::default();
662
663        computation_account.try_serialize(&mut data).unwrap();
664
665        let account_info = AccountInfo {
666            key: &mut key,
667            lamports: Rc::new(RefCell::new(&mut lamports)),
668            data: Rc::new(RefCell::new(&mut data)),
669            owner: &mut owner,
670            rent_epoch: 0,
671            is_signer: false,
672            is_writable: false,
673            executable: false,
674        };
675
676        let computation_account = UncheckedAccount::try_from(&account_info);
677        let (slot_bytes, slot_counter_bytes) =
678            get_slot_and_slot_counter_bytes(&computation_account).unwrap();
679
680        let slot = u64::from_le_bytes(slot_bytes);
681        let slot_counter = u16::from_le_bytes(slot_counter_bytes);
682
683        assert_eq!(slot, 12345);
684        assert_eq!(slot_counter, 5678);
685    }
686
687    #[derive(Debug, PartialEq, AnchorSerialize, AnchorDeserialize)]
688    struct TestOutput {
689        value: u64,
690    }
691
692    impl HasSize for TestOutput {
693        const SIZE: usize = 8;
694    }
695
696    #[test]
697    fn test_signed_computation_outputs_roundtrip_success() {
698        let original = SignedComputationOutputs::<TestOutput>::Success(
699            vec![1, 2, 3, 4, 5, 6, 7, 8],
700            [42u8; 64],
701        );
702        let mut buf = Vec::new();
703        original.serialize(&mut buf).unwrap();
704
705        assert_eq!(buf.len(), 1 + 8 + 64);
706        assert_eq!(buf[0], 0);
707
708        let deserialized =
709            SignedComputationOutputs::<TestOutput>::deserialize(&mut &buf[..]).unwrap();
710        match deserialized {
711            SignedComputationOutputs::Success(bytes, sig) => {
712                assert_eq!(bytes, vec![1, 2, 3, 4, 5, 6, 7, 8]);
713                assert_eq!(sig, [42u8; 64]);
714            }
715            _ => panic!("Expected Success variant"),
716        }
717    }
718
719    #[test]
720    fn test_signed_computation_outputs_no_length_prefix() {
721        let output = SignedComputationOutputs::<TestOutput>::Success(vec![0xAA; 8], [0xBB; 64]);
722        let mut buf = Vec::new();
723        output.serialize(&mut buf).unwrap();
724
725        assert_eq!(&buf[1..9], &[0xAA; 8]);
726        assert_eq!(&buf[9..73], &[0xBB; 64]);
727    }
728
729    #[test]
730    fn test_signed_computation_outputs_roundtrip_failure() {
731        let original = SignedComputationOutputs::<TestOutput>::Failure([0; 64]);
732        let mut buf = Vec::new();
733        original.serialize(&mut buf).unwrap();
734
735        // 1 byte domain separator + 64 byte sig
736        assert_eq!(buf.len(), 65);
737        assert_eq!(buf[0], BLSDomainSeparator::Failure as u8);
738
739        let deserialized =
740            SignedComputationOutputs::<TestOutput>::deserialize(&mut &buf[..]).unwrap();
741        assert!(matches!(deserialized, SignedComputationOutputs::Failure(_)));
742    }
743
744    #[test]
745    fn test_signed_computation_outputs_serialize_validates_size() {
746        let invalid = SignedComputationOutputs::<TestOutput>::Success(vec![1, 2, 3], [0u8; 64]);
747        let mut buf = Vec::new();
748        let result = invalid.serialize(&mut buf);
749        assert!(result.is_err());
750    }
751}