light_compressed_account/instruction_data/
with_readonly.rs

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