light_compressed_account/instruction_data/
with_readonly.rs

1use core::ops::Deref;
2
3use light_zero_copy::{
4    errors::ZeroCopyError, slice::ZeroCopySliceBorsh, traits::ZeroCopyAt, ZeroCopyMut,
5};
6use zerocopy::{
7    little_endian::{U16, U32, U64},
8    FromBytes, Immutable, IntoBytes, KnownLayout, Ref, Unaligned,
9};
10
11use super::{
12    compressed_proof::CompressedProof,
13    cpi_context::CompressedCpiContext,
14    data::{
15        NewAddressParamsAssignedPacked, OutputCompressedAccountWithPackedContext,
16        PackedReadOnlyAddress,
17    },
18    traits::{AccountOptions, InputAccount, InstructionData, NewAddress},
19    zero_copy::{
20        ZCompressedCpiContext, ZNewAddressParamsAssignedPacked,
21        ZOutputCompressedAccountWithPackedContext, ZPackedMerkleContext, ZPackedReadOnlyAddress,
22        ZPackedReadOnlyCompressedAccount,
23    },
24};
25use crate::{
26    compressed_account::{
27        hash_with_hashed_values, CompressedAccount, CompressedAccountData,
28        PackedCompressedAccountWithMerkleContext, PackedMerkleContext,
29        PackedReadOnlyCompressedAccount,
30    },
31    discriminators::DISCRIMINATOR_INVOKE_CPI_WITH_READ_ONLY,
32    instruction_data::traits::LightInstructionData,
33    pubkey::Pubkey,
34    CompressedAccountError, InstructionDiscriminator, Vec,
35};
36
37#[repr(C)]
38#[cfg_attr(
39    all(feature = "std", feature = "anchor"),
40    derive(anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize)
41)]
42#[cfg_attr(
43    not(feature = "anchor"),
44    derive(borsh::BorshDeserialize, borsh::BorshSerialize)
45)]
46#[derive(Debug, Default, PartialEq, Clone, ZeroCopyMut)]
47pub struct InAccount {
48    pub discriminator: [u8; 8],
49    /// Data hash
50    pub data_hash: [u8; 32],
51    /// Merkle tree context.
52    pub merkle_context: PackedMerkleContext,
53    /// Root index.
54    pub root_index: u16,
55    /// Lamports.
56    pub lamports: u64,
57    /// Optional address.
58    pub address: Option<[u8; 32]>,
59}
60
61impl From<PackedCompressedAccountWithMerkleContext> for InAccount {
62    fn from(value: PackedCompressedAccountWithMerkleContext) -> Self {
63        Self {
64            discriminator: value
65                .compressed_account
66                .data
67                .as_ref()
68                .expect("Into InAccount expected data to exist.")
69                .discriminator,
70            merkle_context: value.merkle_context,
71            data_hash: value
72                .compressed_account
73                .data
74                .as_ref()
75                .expect("Into InAccount expected data to exist.")
76                .data_hash,
77            root_index: value.root_index,
78            lamports: value.compressed_account.lamports,
79            address: value.compressed_account.address,
80        }
81    }
82}
83
84impl From<InAccount> for PackedCompressedAccountWithMerkleContext {
85    fn from(value: InAccount) -> Self {
86        Self {
87            read_only: false,
88            merkle_context: value.merkle_context,
89            root_index: value.root_index,
90            compressed_account: CompressedAccount {
91                owner: Pubkey::default(), // Placeholder, as owner is not part of InAccount
92                address: value.address,
93                lamports: value.lamports,
94                data: Some(CompressedAccountData {
95                    discriminator: value.discriminator,
96                    data: Vec::new(),
97                    data_hash: value.data_hash,
98                }),
99            },
100        }
101    }
102}
103
104impl<'a> InputAccount<'a> for ZInAccount<'a> {
105    fn skip(&self) -> bool {
106        false
107    }
108    fn owner(&self) -> &Pubkey {
109        &self.owner
110    }
111    fn lamports(&self) -> u64 {
112        self.lamports.into()
113    }
114    fn address(&self) -> Option<[u8; 32]> {
115        self.address.map(|x| *x)
116    }
117    fn merkle_context(&self) -> ZPackedMerkleContext {
118        self.merkle_context
119    }
120
121    fn root_index(&self) -> u16 {
122        self.root_index.into()
123    }
124
125    fn has_data(&self) -> bool {
126        true
127    }
128
129    fn data(&self) -> Option<CompressedAccountData> {
130        Some(CompressedAccountData {
131            discriminator: self.discriminator,
132            data: Vec::new(),
133            data_hash: self.data_hash,
134        })
135    }
136
137    fn hash_with_hashed_values(
138        &self,
139        owner_hashed: &[u8; 32],
140        merkle_tree_hashed: &[u8; 32],
141        leaf_index: &u32,
142        is_batched: bool,
143    ) -> Result<[u8; 32], CompressedAccountError> {
144        hash_with_hashed_values(
145            &(self.lamports.into()),
146            self.address.as_ref().map(|x| x.as_slice()),
147            Some((self.discriminator.as_slice(), self.data_hash.as_slice())),
148            owner_hashed,
149            merkle_tree_hashed,
150            leaf_index,
151            is_batched,
152        )
153    }
154}
155
156impl InAccount {
157    pub fn into_packed_compressed_account_with_merkle_context(
158        &self,
159        owner: Pubkey,
160    ) -> PackedCompressedAccountWithMerkleContext {
161        PackedCompressedAccountWithMerkleContext {
162            read_only: false,
163            merkle_context: self.merkle_context,
164            root_index: self.root_index,
165            compressed_account: CompressedAccount {
166                owner,
167                address: self.address,
168                lamports: self.lamports,
169                data: Some(CompressedAccountData {
170                    data: Vec::new(),
171                    discriminator: self.discriminator,
172                    data_hash: self.data_hash,
173                }),
174            },
175        }
176    }
177}
178
179#[repr(C)]
180#[derive(
181    Debug, Default, PartialEq, Clone, Copy, FromBytes, IntoBytes, Unaligned, Immutable, KnownLayout,
182)]
183pub struct ZInAccountMeta {
184    pub discriminator: [u8; 8],
185    /// Data hash
186    pub data_hash: [u8; 32],
187    /// Merkle tree context.
188    pub merkle_context: ZPackedMerkleContext,
189    /// Root index.
190    pub root_index: U16,
191    /// Lamports.
192    pub lamports: U64,
193}
194
195#[repr(C)]
196#[derive(Debug, PartialEq)]
197pub struct ZInAccount<'a> {
198    pub owner: Pubkey,
199    meta: Ref<&'a [u8], ZInAccountMeta>,
200    pub address: Option<Ref<&'a [u8], [u8; 32]>>,
201}
202
203impl<'a> InAccount {
204    fn zero_copy_at_with_owner(
205        bytes: &'a [u8],
206        owner: Pubkey,
207    ) -> Result<(ZInAccount<'a>, &'a [u8]), ZeroCopyError> {
208        let (meta, bytes) = Ref::<&[u8], ZInAccountMeta>::from_prefix(bytes)?;
209        let (address, bytes) = Option::<Ref<&[u8], [u8; 32]>>::zero_copy_at(bytes)?;
210        Ok((
211            ZInAccount {
212                owner,
213                meta,
214                address,
215            },
216            bytes,
217        ))
218    }
219}
220
221impl<'a> Deref for ZInAccount<'a> {
222    type Target = Ref<&'a [u8], ZInAccountMeta>;
223
224    fn deref(&self) -> &Self::Target {
225        &self.meta
226    }
227}
228
229#[repr(C)]
230#[cfg_attr(
231    all(feature = "std", feature = "anchor"),
232    derive(anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize)
233)]
234#[cfg_attr(
235    not(feature = "anchor"),
236    derive(borsh::BorshDeserialize, borsh::BorshSerialize)
237)]
238#[derive(Debug, PartialEq, Default, Clone, ZeroCopyMut)]
239pub struct InstructionDataInvokeCpiWithReadOnly {
240    /// 0 With program ids
241    /// 1 without program ids
242    pub mode: u8,
243    pub bump: u8,
244    pub invoking_program_id: Pubkey,
245    /// If compress_or_decompress_lamports > 0 -> expect sol_pool_pda
246    pub compress_or_decompress_lamports: u64,
247    /// -> expect account decompression_recipient
248    pub is_compress: bool,
249    pub with_cpi_context: bool,
250    pub with_transaction_hash: bool,
251    pub cpi_context: CompressedCpiContext,
252    pub proof: Option<CompressedProof>,
253    pub new_address_params: Vec<NewAddressParamsAssignedPacked>,
254    pub input_compressed_accounts: Vec<InAccount>,
255    pub output_compressed_accounts: Vec<OutputCompressedAccountWithPackedContext>,
256    pub read_only_addresses: Vec<PackedReadOnlyAddress>,
257    pub read_only_accounts: Vec<PackedReadOnlyCompressedAccount>,
258}
259
260impl InstructionDataInvokeCpiWithReadOnly {
261    pub fn new(invoking_program_id: Pubkey, bump: u8, proof: Option<CompressedProof>) -> Self {
262        Self {
263            invoking_program_id,
264            bump,
265            proof,
266            mode: 1,
267            ..Default::default()
268        }
269    }
270
271    #[must_use = "mode_v1 returns a new value"]
272    pub fn mode_v1(mut self) -> Self {
273        self.mode = 0;
274        self
275    }
276
277    #[must_use = "write_to_cpi_context_set returns a new value"]
278    pub fn write_to_cpi_context_set(mut self) -> Self {
279        self.with_cpi_context = true;
280        self.cpi_context = CompressedCpiContext::set();
281        self
282    }
283
284    #[must_use = "write_to_cpi_context_first returns a new value"]
285    pub fn write_to_cpi_context_first(mut self) -> Self {
286        self.with_cpi_context = true;
287        self.cpi_context = CompressedCpiContext::first();
288        self
289    }
290
291    #[must_use = "execute_with_cpi_context returns a new value"]
292    pub fn execute_with_cpi_context(mut self) -> Self {
293        self.with_cpi_context = true;
294        self
295    }
296
297    #[must_use = "with_with_transaction_hash returns a new value"]
298    pub fn with_with_transaction_hash(mut self, with_transaction_hash: bool) -> Self {
299        self.with_transaction_hash = with_transaction_hash;
300        self
301    }
302
303    #[must_use = "with_cpi_context returns a new value"]
304    pub fn with_cpi_context(mut self, cpi_context: CompressedCpiContext) -> Self {
305        self.cpi_context = cpi_context;
306        self
307    }
308
309    #[must_use = "with_proof returns a new value"]
310    pub fn with_proof(mut self, proof: Option<CompressedProof>) -> Self {
311        self.proof = proof;
312        self
313    }
314
315    #[must_use = "with_new_addresses returns a new value"]
316    pub fn with_new_addresses(
317        mut self,
318        new_address_params: &[NewAddressParamsAssignedPacked],
319    ) -> Self {
320        if !new_address_params.is_empty() {
321            self.new_address_params
322                .extend_from_slice(new_address_params);
323        }
324        self
325    }
326
327    #[must_use = "with_input_compressed_accounts returns a new value"]
328    pub fn with_input_compressed_accounts(
329        mut self,
330        input_compressed_accounts: &[InAccount],
331    ) -> Self {
332        if !input_compressed_accounts.is_empty() {
333            self.input_compressed_accounts
334                .extend_from_slice(input_compressed_accounts);
335        }
336        self
337    }
338
339    #[must_use = "with_output_compressed_accounts returns a new value"]
340    pub fn with_output_compressed_accounts(
341        mut self,
342        output_compressed_accounts: &[OutputCompressedAccountWithPackedContext],
343    ) -> Self {
344        if !output_compressed_accounts.is_empty() {
345            self.output_compressed_accounts
346                .extend_from_slice(output_compressed_accounts);
347        }
348        self
349    }
350
351    #[must_use = "with_read_only_addresses returns a new value"]
352    pub fn with_read_only_addresses(
353        mut self,
354        read_only_addresses: &[PackedReadOnlyAddress],
355    ) -> Self {
356        if !read_only_addresses.is_empty() {
357            self.read_only_addresses
358                .extend_from_slice(read_only_addresses);
359        }
360        self
361    }
362
363    #[must_use = "with_read_only_accounts returns a new value"]
364    pub fn with_read_only_accounts(
365        mut self,
366        read_only_accounts: &[PackedReadOnlyCompressedAccount],
367    ) -> Self {
368        if !read_only_accounts.is_empty() {
369            self.read_only_accounts
370                .extend_from_slice(read_only_accounts);
371        }
372        self
373    }
374}
375
376impl InstructionDiscriminator for InstructionDataInvokeCpiWithReadOnly {
377    fn discriminator(&self) -> &'static [u8] {
378        &DISCRIMINATOR_INVOKE_CPI_WITH_READ_ONLY
379    }
380}
381
382impl LightInstructionData for InstructionDataInvokeCpiWithReadOnly {}
383
384#[repr(C)]
385#[derive(
386    Debug, Default, PartialEq, Clone, Copy, FromBytes, IntoBytes, Unaligned, Immutable, KnownLayout,
387)]
388pub struct ZInstructionDataInvokeCpiWithReadOnlyMeta {
389    /// 0 With program ids
390    /// 1 without program ids
391    pub mode: u8,
392    pub bump: u8,
393    pub invoking_program_id: Pubkey,
394    /// If compress_or_decompress_lamports > 0 -> expect sol_pool_pda
395    pub compress_or_decompress_lamports: U64,
396    /// -> expect account decompression_recipient
397    is_compress: u8,
398    with_cpi_context: u8,
399    with_transaction_hash: u8,
400    pub cpi_context: ZCompressedCpiContext,
401}
402
403impl ZInstructionDataInvokeCpiWithReadOnlyMeta {
404    pub fn is_compress(&self) -> bool {
405        self.is_compress > 0
406    }
407    pub fn with_cpi_context(&self) -> bool {
408        self.with_cpi_context > 0
409    }
410    pub fn with_transaction_hash(&self) -> bool {
411        self.with_transaction_hash > 0
412    }
413}
414
415#[derive(Debug, PartialEq)]
416pub struct ZInstructionDataInvokeCpiWithReadOnly<'a> {
417    meta: Ref<&'a [u8], ZInstructionDataInvokeCpiWithReadOnlyMeta>,
418    pub proof: Option<Ref<&'a [u8], CompressedProof>>,
419    pub new_address_params: ZeroCopySliceBorsh<'a, ZNewAddressParamsAssignedPacked>,
420    pub input_compressed_accounts: Vec<ZInAccount<'a>>,
421    pub output_compressed_accounts: Vec<ZOutputCompressedAccountWithPackedContext<'a>>,
422    pub read_only_addresses: ZeroCopySliceBorsh<'a, ZPackedReadOnlyAddress>,
423    pub read_only_accounts: ZeroCopySliceBorsh<'a, ZPackedReadOnlyCompressedAccount>,
424}
425
426impl<'a> InstructionData<'a> for ZInstructionDataInvokeCpiWithReadOnly<'a> {
427    fn account_option_config(&self) -> Result<AccountOptions, CompressedAccountError> {
428        let sol_pool_pda = self.compress_or_decompress_lamports().is_some();
429        let decompression_recipient = sol_pool_pda && !self.is_compress();
430        let cpi_context_account = self.cpi_context().is_some();
431        let write_to_cpi_context =
432            self.cpi_context.first_set_context() || self.cpi_context.set_context();
433
434        // Validate: if we want to write to CPI context, we must have a CPI context
435        if write_to_cpi_context && !cpi_context_account {
436            return Err(CompressedAccountError::InvalidCpiContext);
437        }
438
439        Ok(AccountOptions {
440            sol_pool_pda,
441            decompression_recipient,
442            cpi_context_account,
443            write_to_cpi_context,
444        })
445    }
446
447    fn with_transaction_hash(&self) -> bool {
448        self.meta.with_transaction_hash()
449    }
450
451    fn bump(&self) -> Option<u8> {
452        Some(self.bump)
453    }
454    fn read_only_accounts(&self) -> Option<&[ZPackedReadOnlyCompressedAccount]> {
455        Some(self.read_only_accounts.as_slice())
456    }
457
458    fn read_only_addresses(&self) -> Option<&[ZPackedReadOnlyAddress]> {
459        Some(self.read_only_addresses.as_slice())
460    }
461
462    fn owner(&self) -> Pubkey {
463        self.meta.invoking_program_id
464    }
465
466    fn new_addresses(&self) -> &[impl NewAddress<'a>] {
467        self.new_address_params.as_slice()
468    }
469
470    fn proof(&self) -> Option<Ref<&'a [u8], CompressedProof>> {
471        self.proof
472    }
473
474    fn cpi_context(&self) -> Option<CompressedCpiContext> {
475        if self.meta.with_cpi_context() {
476            Some(CompressedCpiContext {
477                set_context: self.cpi_context.set_context(),
478                first_set_context: self.cpi_context.first_set_context(),
479                cpi_context_account_index: self.cpi_context.cpi_context_account_index,
480            })
481        } else {
482            None
483        }
484    }
485
486    fn is_compress(&self) -> bool {
487        self.meta.is_compress() && self.compress_or_decompress_lamports().is_some()
488    }
489
490    fn input_accounts(&self) -> &[impl InputAccount<'a>] {
491        self.input_compressed_accounts.as_slice()
492    }
493
494    fn output_accounts(&self) -> &[impl super::traits::OutputAccount<'a>] {
495        self.output_compressed_accounts.as_slice()
496    }
497
498    fn compress_or_decompress_lamports(&self) -> Option<u64> {
499        let lamports: u64 = self.meta.compress_or_decompress_lamports.into();
500        if lamports != 0 {
501            Some(lamports)
502        } else {
503            None
504        }
505    }
506}
507
508impl<'a> Deref for ZInstructionDataInvokeCpiWithReadOnly<'a> {
509    type Target = Ref<&'a [u8], ZInstructionDataInvokeCpiWithReadOnlyMeta>;
510
511    fn deref(&self) -> &Self::Target {
512        &self.meta
513    }
514}
515
516impl<'a> ZeroCopyAt<'a> for InstructionDataInvokeCpiWithReadOnly {
517    type ZeroCopyAt = ZInstructionDataInvokeCpiWithReadOnly<'a>;
518    fn zero_copy_at(bytes: &'a [u8]) -> Result<(Self::ZeroCopyAt, &'a [u8]), ZeroCopyError> {
519        let (meta, bytes) =
520            Ref::<&[u8], ZInstructionDataInvokeCpiWithReadOnlyMeta>::from_prefix(bytes)?;
521        let (proof, bytes) = Option::<Ref<&[u8], CompressedProof>>::zero_copy_at(bytes)?;
522        let (new_address_params, bytes) =
523            ZeroCopySliceBorsh::<'a, ZNewAddressParamsAssignedPacked>::from_bytes_at(bytes)?;
524        let (input_compressed_accounts, bytes) = {
525            let (num_slices, mut bytes) = Ref::<&[u8], U32>::from_prefix(bytes)?;
526            let num_slices = u32::from(*num_slices) as usize;
527            // Prevent heap exhaustion attacks by checking if num_slices is reasonable
528            // Each element needs at least 1 byte when serialized
529            if bytes.len() < num_slices {
530                return Err(ZeroCopyError::InsufficientMemoryAllocated(
531                    bytes.len(),
532                    num_slices,
533                ));
534            }
535            let mut slices = Vec::with_capacity(num_slices);
536            for _ in 0..num_slices {
537                let (slice, _bytes) =
538                    InAccount::zero_copy_at_with_owner(bytes, meta.invoking_program_id)?;
539                bytes = _bytes;
540                slices.push(slice);
541            }
542            (slices, bytes)
543        };
544
545        let (output_compressed_accounts, bytes) = <Vec<
546            ZOutputCompressedAccountWithPackedContext<'a>,
547        > as ZeroCopyAt<'a>>::zero_copy_at(bytes)?;
548
549        let (read_only_addresses, bytes) =
550            ZeroCopySliceBorsh::<'a, ZPackedReadOnlyAddress>::from_bytes_at(bytes)?;
551
552        let (read_only_accounts, bytes) =
553            ZeroCopySliceBorsh::<'a, ZPackedReadOnlyCompressedAccount>::from_bytes_at(bytes)?;
554
555        Ok((
556            ZInstructionDataInvokeCpiWithReadOnly {
557                meta,
558                proof,
559                new_address_params,
560                input_compressed_accounts,
561                output_compressed_accounts,
562                read_only_addresses,
563                read_only_accounts,
564            },
565            bytes,
566        ))
567    }
568}
569
570impl PartialEq<InstructionDataInvokeCpiWithReadOnly> for ZInstructionDataInvokeCpiWithReadOnly<'_> {
571    fn eq(&self, other: &InstructionDataInvokeCpiWithReadOnly) -> bool {
572        // Compare basic fields
573        if self.mode != other.mode
574            || self.bump != other.bump
575            || self.invoking_program_id != other.invoking_program_id
576            || u64::from(self.compress_or_decompress_lamports)
577                != other.compress_or_decompress_lamports
578            || self.is_compress() != other.is_compress
579            || self.with_cpi_context() != other.with_cpi_context
580        {
581            return false;
582        }
583
584        // Compare complex fields
585        if self.proof.is_some() != other.proof.is_some() {
586            return false;
587        }
588        // We'd need a more complex comparison for proofs, but we know they match
589        // when testing with empty objects
590
591        // Compare cpi_context
592        if self.cpi_context.set_context() != other.cpi_context.set_context
593            || self.cpi_context.first_set_context() != other.cpi_context.first_set_context
594            || self.cpi_context.cpi_context_account_index
595                != other.cpi_context.cpi_context_account_index
596        {
597            return false;
598        }
599
600        if self.new_address_params.len() != other.new_address_params.len()
601            || self.input_compressed_accounts.len() != other.input_compressed_accounts.len()
602            || self.output_compressed_accounts.len() != other.output_compressed_accounts.len()
603            || self.read_only_addresses.len() != other.read_only_addresses.len()
604            || self.read_only_accounts.len() != other.read_only_accounts.len()
605        {
606            return false;
607        }
608
609        true
610    }
611}
612
613#[cfg(all(not(feature = "pinocchio"), feature = "new-unique"))]
614#[cfg(test)]
615mod test {
616    use borsh::BorshSerialize;
617    use rand::{
618        rngs::{StdRng, ThreadRng},
619        Rng, SeedableRng,
620    };
621
622    use super::*;
623    use crate::CompressedAccountError;
624    // TODO: add randomized tests.
625    #[test]
626    fn test_read_only_zero_copy() {
627        let borsh_struct = InstructionDataInvokeCpiWithReadOnly {
628            mode: 0,
629            bump: 0,
630            invoking_program_id: Pubkey::default(),
631            compress_or_decompress_lamports: 0,
632            is_compress: false,
633            with_cpi_context: false,
634            with_transaction_hash: true,
635            cpi_context: CompressedCpiContext {
636                set_context: false,
637                first_set_context: false,
638                cpi_context_account_index: 0,
639            },
640            proof: None,
641            new_address_params: vec![NewAddressParamsAssignedPacked {
642                seed: [1; 32],
643                address_merkle_tree_account_index: 1,
644                address_queue_account_index: 2,
645                address_merkle_tree_root_index: 3,
646                assigned_to_account: true,
647                assigned_account_index: 2,
648            }],
649            input_compressed_accounts: vec![InAccount {
650                discriminator: [1, 2, 3, 4, 5, 6, 7, 8],
651                data_hash: [10; 32],
652                merkle_context: PackedMerkleContext {
653                    merkle_tree_pubkey_index: 1,
654                    queue_pubkey_index: 2,
655                    leaf_index: 3,
656                    prove_by_index: false,
657                },
658                root_index: 3,
659                lamports: 1000,
660                address: Some([30; 32]),
661            }],
662            output_compressed_accounts: vec![OutputCompressedAccountWithPackedContext {
663                compressed_account: CompressedAccount {
664                    owner: Pubkey::default(),
665                    lamports: 2000,
666                    address: Some([40; 32]),
667                    data: Some(CompressedAccountData {
668                        discriminator: [3, 4, 5, 6, 7, 8, 9, 10],
669                        data: vec![],
670                        data_hash: [50; 32],
671                    }),
672                },
673                merkle_tree_index: 3,
674            }],
675            read_only_addresses: vec![PackedReadOnlyAddress {
676                address: [70; 32],
677                address_merkle_tree_account_index: 4,
678                address_merkle_tree_root_index: 5,
679            }],
680            read_only_accounts: vec![PackedReadOnlyCompressedAccount {
681                account_hash: [80; 32],
682                merkle_context: PackedMerkleContext {
683                    merkle_tree_pubkey_index: 5,
684                    queue_pubkey_index: 6,
685                    leaf_index: 7,
686                    prove_by_index: false,
687                },
688                root_index: 8,
689            }],
690        };
691        let bytes = borsh_struct.try_to_vec().unwrap();
692
693        let (zero_copy, _) = InstructionDataInvokeCpiWithReadOnly::zero_copy_at(&bytes).unwrap();
694
695        assert_eq!(zero_copy, borsh_struct);
696    }
697
698    /// Compare the original struct with its zero-copy counterpart
699    fn compare_invoke_cpi_with_readonly(
700        reference: &InstructionDataInvokeCpiWithReadOnly,
701        z_copy: &ZInstructionDataInvokeCpiWithReadOnly,
702    ) -> Result<(), CompressedAccountError> {
703        // Basic field comparisons
704        if reference.mode != z_copy.meta.mode {
705            return Err(CompressedAccountError::InvalidArgument);
706        }
707        if reference.bump != z_copy.meta.bump {
708            return Err(CompressedAccountError::InvalidArgument);
709        }
710        if reference.invoking_program_id != z_copy.meta.invoking_program_id {
711            return Err(CompressedAccountError::InvalidArgument);
712        }
713        if reference.compress_or_decompress_lamports
714            != u64::from(z_copy.meta.compress_or_decompress_lamports)
715        {
716            return Err(CompressedAccountError::InvalidArgument);
717        }
718        if reference.is_compress != z_copy.meta.is_compress() {
719            return Err(CompressedAccountError::InvalidArgument);
720        }
721        if reference.with_cpi_context != z_copy.meta.with_cpi_context() {
722            return Err(CompressedAccountError::InvalidArgument);
723        }
724        if reference.with_transaction_hash != z_copy.meta.with_transaction_hash() {
725            return Err(CompressedAccountError::InvalidArgument);
726        }
727
728        // CPI context comparisons
729        if reference.cpi_context.first_set_context != z_copy.meta.cpi_context.first_set_context() {
730            return Err(CompressedAccountError::InvalidArgument);
731        }
732        if reference.cpi_context.set_context != z_copy.meta.cpi_context.set_context() {
733            return Err(CompressedAccountError::InvalidArgument);
734        }
735        if reference.cpi_context.cpi_context_account_index
736            != z_copy.meta.cpi_context.cpi_context_account_index
737        {
738            return Err(CompressedAccountError::InvalidArgument);
739        }
740
741        // Proof comparisons
742        if reference.proof.is_some() && z_copy.proof.is_none() {
743            return Err(CompressedAccountError::InvalidArgument);
744        }
745        if reference.proof.is_none() && z_copy.proof.is_some() {
746            return Err(CompressedAccountError::InvalidArgument);
747        }
748        if reference.proof.is_some() && z_copy.proof.is_some() {
749            let ref_proof = reference.proof.as_ref().unwrap();
750            let z_proof = *z_copy.proof.as_ref().unwrap();
751            if ref_proof.a != z_proof.a || ref_proof.b != z_proof.b || ref_proof.c != z_proof.c {
752                return Err(CompressedAccountError::InvalidArgument);
753            }
754        }
755
756        // Collection length comparisons
757        if reference.new_address_params.len() != z_copy.new_address_params.len() {
758            return Err(CompressedAccountError::InvalidArgument);
759        }
760        if reference.input_compressed_accounts.len() != z_copy.input_compressed_accounts.len() {
761            return Err(CompressedAccountError::InvalidArgument);
762        }
763        if reference.output_compressed_accounts.len() != z_copy.output_compressed_accounts.len() {
764            return Err(CompressedAccountError::InvalidArgument);
765        }
766        if reference.read_only_addresses.len() != z_copy.read_only_addresses.len() {
767            return Err(CompressedAccountError::InvalidArgument);
768        }
769        if reference.read_only_accounts.len() != z_copy.read_only_accounts.len() {
770            return Err(CompressedAccountError::InvalidArgument);
771        }
772
773        // If we're testing the traits, let's also check that the relevant trait methods work
774        assert_eq!(
775            z_copy.with_transaction_hash(),
776            reference.with_transaction_hash
777        );
778        assert_eq!(z_copy.bump(), Some(reference.bump));
779        assert_eq!(z_copy.owner(), reference.invoking_program_id);
780
781        // The compress or decompress logic is a bit complex, let's test it
782        if reference.compress_or_decompress_lamports > 0 {
783            assert_eq!(
784                z_copy.compress_or_decompress_lamports(),
785                Some(reference.compress_or_decompress_lamports)
786            );
787        } else {
788            assert_eq!(z_copy.compress_or_decompress_lamports(), None);
789        }
790
791        assert_eq!(
792            z_copy.is_compress(),
793            reference.is_compress && reference.compress_or_decompress_lamports > 0
794        );
795
796        // For cpi_context, the trait adds a layer of conditional return
797        if reference.with_cpi_context {
798            assert!(z_copy.cpi_context().is_some());
799        } else {
800            assert!(z_copy.cpi_context().is_none());
801        }
802
803        Ok(())
804    }
805
806    /// Generate a random InstructionDataInvokeCpiWithReadOnly
807    fn get_rnd_instruction_data_invoke_cpi_with_readonly(
808        rng: &mut StdRng,
809    ) -> InstructionDataInvokeCpiWithReadOnly {
810        InstructionDataInvokeCpiWithReadOnly {
811            mode: rng.gen_range(0..2),
812            bump: rng.gen(),
813            invoking_program_id: Pubkey::new_unique(),
814            compress_or_decompress_lamports: rng.gen(),
815            is_compress: rng.gen(),
816            with_cpi_context: rng.gen(),
817            with_transaction_hash: rng.gen(),
818            cpi_context: CompressedCpiContext {
819                first_set_context: rng.gen(),
820                set_context: rng.gen(),
821                cpi_context_account_index: rng.gen(),
822            },
823            proof: if rng.gen() {
824                Some(CompressedProof {
825                    a: rng.gen(),
826                    b: (0..64)
827                        .map(|_| rng.gen())
828                        .collect::<Vec<u8>>()
829                        .try_into()
830                        .unwrap(),
831                    c: rng.gen(),
832                })
833            } else {
834                None
835            },
836            // Keep collections small to minimize complex serialization issues
837            new_address_params: (0..rng.gen_range(1..5))
838                .map(|_| NewAddressParamsAssignedPacked {
839                    seed: rng.gen(),
840                    address_queue_account_index: rng.gen(),
841                    address_merkle_tree_account_index: rng.gen(),
842                    address_merkle_tree_root_index: rng.gen(),
843                    assigned_to_account: rng.gen(),
844                    assigned_account_index: rng.gen(),
845                })
846                .collect::<Vec<_>>(),
847            input_compressed_accounts: (0..rng.gen_range(1..5))
848                .map(|_| InAccount {
849                    discriminator: rng.gen(),
850                    data_hash: rng.gen(),
851                    merkle_context: PackedMerkleContext {
852                        merkle_tree_pubkey_index: rng.gen(),
853                        queue_pubkey_index: rng.gen(),
854                        leaf_index: rng.gen(),
855                        prove_by_index: rng.gen(),
856                    },
857                    root_index: rng.gen(),
858                    lamports: rng.gen(),
859                    address: if rng.gen() { Some(rng.gen()) } else { None },
860                })
861                .collect(),
862            output_compressed_accounts: (0..rng.gen_range(1..5))
863                .map(|_| {
864                    OutputCompressedAccountWithPackedContext {
865                        compressed_account: CompressedAccount {
866                            owner: Pubkey::new_unique(),
867                            lamports: rng.gen(),
868                            address: if rng.gen() { Some(rng.gen()) } else { None },
869                            data: if rng.gen() {
870                                Some(CompressedAccountData {
871                                    discriminator: rng.gen(),
872                                    data: vec![], // Keep data empty for simpler testing
873                                    data_hash: rng.gen(),
874                                })
875                            } else {
876                                None
877                            },
878                        },
879                        merkle_tree_index: rng.gen(),
880                    }
881                })
882                .collect::<Vec<_>>(),
883            read_only_addresses: (0..rng.gen_range(1..5))
884                .map(|_| PackedReadOnlyAddress {
885                    address: rng.gen(),
886                    address_merkle_tree_account_index: rng.gen(),
887                    address_merkle_tree_root_index: rng.gen(),
888                })
889                .collect::<Vec<_>>(),
890            read_only_accounts: (0..rng.gen_range(1..5))
891                .map(|_| PackedReadOnlyCompressedAccount {
892                    account_hash: rng.gen(),
893                    merkle_context: PackedMerkleContext {
894                        merkle_tree_pubkey_index: rng.gen(),
895                        queue_pubkey_index: rng.gen(),
896                        leaf_index: rng.gen(),
897                        prove_by_index: rng.gen(),
898                    },
899                    root_index: rng.gen(),
900                })
901                .collect::<Vec<_>>(),
902        }
903    }
904
905    #[test]
906    fn test_instruction_data_invoke_cpi_with_readonly_rnd() {
907        let mut thread_rng = ThreadRng::default();
908        let seed = thread_rng.gen();
909        println!("\n\ne2e test seed {}\n\n", seed);
910        let mut rng = StdRng::seed_from_u64(seed);
911
912        let num_iters = 1000;
913        for _ in 0..num_iters {
914            let value = get_rnd_instruction_data_invoke_cpi_with_readonly(&mut rng);
915
916            let mut vec = Vec::new();
917            value.serialize(&mut vec).unwrap();
918            let (zero_copy, _) = InstructionDataInvokeCpiWithReadOnly::zero_copy_at(&vec).unwrap();
919
920            // Use the PartialEq implementation first
921            assert_eq!(zero_copy, value);
922
923            // Then use our detailed comparison that also tests trait methods
924            compare_invoke_cpi_with_readonly(&value, &zero_copy).unwrap();
925        }
926    }
927}