light_system_program/sdk/
event.rs

1use std::io::Write;
2
3use anchor_lang::{
4    prelude::borsh, solana_program::pubkey::Pubkey, AnchorDeserialize, AnchorSerialize,
5};
6
7use crate::OutputCompressedAccountWithPackedContext;
8
9#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, Default, PartialEq)]
10pub struct MerkleTreeSequenceNumber {
11    pub pubkey: Pubkey,
12    pub seq: u64,
13}
14
15#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, Default, PartialEq)]
16pub struct PublicTransactionEvent {
17    pub input_compressed_account_hashes: Vec<[u8; 32]>,
18    pub output_compressed_account_hashes: Vec<[u8; 32]>,
19    pub output_compressed_accounts: Vec<OutputCompressedAccountWithPackedContext>,
20    pub output_leaf_indices: Vec<u32>,
21    pub sequence_numbers: Vec<MerkleTreeSequenceNumber>,
22    pub relay_fee: Option<u64>,
23    pub is_compress: bool,
24    pub compress_or_decompress_lamports: Option<u64>,
25    pub pubkey_array: Vec<Pubkey>,
26    pub message: Option<Vec<u8>>,
27}
28
29impl PublicTransactionEvent {
30    pub fn man_serialize<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
31        writer.write_all(&(self.input_compressed_account_hashes.len() as u32).to_le_bytes())?;
32        for hash in self.input_compressed_account_hashes.iter() {
33            writer.write_all(hash)?;
34        }
35
36        writer.write_all(&(self.output_compressed_account_hashes.len() as u32).to_le_bytes())?;
37        for hash in self.output_compressed_account_hashes.iter() {
38            writer.write_all(hash)?;
39        }
40
41        #[cfg(target_os = "solana")]
42        let pos = light_heap::GLOBAL_ALLOCATOR.get_heap_pos();
43        writer.write_all(&(self.output_compressed_accounts.len() as u32).to_le_bytes())?;
44        for i in 0..self.output_compressed_accounts.len() {
45            let account = self.output_compressed_accounts[i].clone();
46            account.serialize(writer)?;
47        }
48        #[cfg(target_os = "solana")]
49        light_heap::GLOBAL_ALLOCATOR.free_heap(pos).unwrap();
50
51        writer.write_all(&(self.output_leaf_indices.len() as u32).to_le_bytes())?;
52        for index in self.output_leaf_indices.iter() {
53            writer.write_all(&index.to_le_bytes())?;
54        }
55
56        writer.write_all(&(self.sequence_numbers.len() as u32).to_le_bytes())?;
57        for element in self.sequence_numbers.iter() {
58            writer.write_all(&element.pubkey.to_bytes())?;
59            writer.write_all(&element.seq.to_le_bytes())?;
60        }
61        match self.relay_fee {
62            Some(relay_fee) => {
63                writer.write_all(&[1])?;
64                writer.write_all(&relay_fee.to_le_bytes())
65            }
66            None => writer.write_all(&[0]),
67        }?;
68
69        writer.write_all(&[self.is_compress as u8])?;
70
71        match self.compress_or_decompress_lamports {
72            Some(compress_or_decompress_lamports) => {
73                writer.write_all(&[1])?;
74                writer.write_all(&compress_or_decompress_lamports.to_le_bytes())
75            }
76            None => writer.write_all(&[0]),
77        }?;
78
79        writer.write_all(&(self.pubkey_array.len() as u32).to_le_bytes())?;
80        for pubkey in self.pubkey_array.iter() {
81            writer.write_all(&pubkey.to_bytes())?;
82        }
83
84        match &self.message {
85            Some(message) => {
86                writer.write_all(&[1])?;
87                writer.write_all(&(message.len() as u32).to_le_bytes())?;
88                writer.write_all(message)
89            }
90            None => writer.write_all(&[0]),
91        }?;
92
93        Ok(())
94    }
95}
96
97#[cfg(test)]
98pub mod test {
99    use rand::Rng;
100    use solana_sdk::{signature::Keypair, signer::Signer};
101
102    use super::*;
103    use crate::sdk::compressed_account::{CompressedAccount, CompressedAccountData};
104
105    #[test]
106    fn test_manual_vs_borsh_serialization() {
107        // Create a sample `PublicTransactionEvent` instance
108        let event = PublicTransactionEvent {
109            input_compressed_account_hashes: vec![[0u8; 32], [1u8; 32]],
110            output_compressed_account_hashes: vec![[2u8; 32], [3u8; 32]],
111            output_compressed_accounts: vec![OutputCompressedAccountWithPackedContext {
112                compressed_account: CompressedAccount {
113                    owner: Keypair::new().pubkey(),
114                    lamports: 100,
115                    address: Some([5u8; 32]),
116                    data: Some(CompressedAccountData {
117                        discriminator: [6u8; 8],
118                        data: vec![7u8; 32],
119                        data_hash: [8u8; 32],
120                    }),
121                },
122                merkle_tree_index: 1,
123            }],
124            sequence_numbers: vec![
125                MerkleTreeSequenceNumber {
126                    pubkey: Keypair::new().pubkey(),
127                    seq: 10,
128                },
129                MerkleTreeSequenceNumber {
130                    pubkey: Keypair::new().pubkey(),
131                    seq: 2,
132                },
133            ],
134            output_leaf_indices: vec![4, 5, 6],
135            relay_fee: Some(1000),
136            is_compress: true,
137            compress_or_decompress_lamports: Some(5000),
138            pubkey_array: vec![Keypair::new().pubkey(), Keypair::new().pubkey()],
139            message: Some(vec![8, 9, 10]),
140        };
141
142        // Serialize using Borsh
143        let borsh_serialized = event.try_to_vec().unwrap();
144
145        // Serialize manually
146        let mut manual_serialized = Vec::new();
147        event.man_serialize(&mut manual_serialized).unwrap();
148
149        // Compare the two byte arrays
150        assert_eq!(
151            borsh_serialized, manual_serialized,
152            "Borsh and manual serialization results should match"
153        );
154    }
155
156    #[test]
157    fn test_serialization_consistency() {
158        let mut rng = rand::thread_rng();
159
160        for _ in 0..10_000 {
161            let input_hashes: Vec<[u8; 32]> =
162                (0..rng.gen_range(1..10)).map(|_| rng.gen()).collect();
163            let output_hashes: Vec<[u8; 32]> =
164                (0..rng.gen_range(1..10)).map(|_| rng.gen()).collect();
165            let output_accounts: Vec<OutputCompressedAccountWithPackedContext> = (0..rng
166                .gen_range(1..10))
167                .map(|_| OutputCompressedAccountWithPackedContext {
168                    compressed_account: CompressedAccount {
169                        owner: Keypair::new().pubkey(),
170                        lamports: rng.gen(),
171                        address: Some(rng.gen()),
172                        data: None,
173                    },
174                    merkle_tree_index: rng.gen(),
175                })
176                .collect();
177            let leaf_indices: Vec<u32> = (0..rng.gen_range(1..10)).map(|_| rng.gen()).collect();
178            let pubkeys: Vec<Pubkey> = (0..rng.gen_range(1..10))
179                .map(|_| Keypair::new().pubkey())
180                .collect();
181            let message: Option<Vec<u8>> = if rng.gen() {
182                Some((0..rng.gen_range(1..100)).map(|_| rng.gen()).collect())
183            } else {
184                None
185            };
186
187            let event = PublicTransactionEvent {
188                input_compressed_account_hashes: input_hashes,
189                output_compressed_account_hashes: output_hashes,
190                output_compressed_accounts: output_accounts,
191                output_leaf_indices: leaf_indices,
192                sequence_numbers: (0..rng.gen_range(1..10))
193                    .map(|_| MerkleTreeSequenceNumber {
194                        pubkey: Keypair::new().pubkey(),
195                        seq: rng.gen(),
196                    })
197                    .collect(),
198                relay_fee: if rng.gen() { Some(rng.gen()) } else { None },
199                is_compress: rng.gen(),
200                compress_or_decompress_lamports: if rng.gen() { Some(rng.gen()) } else { None },
201                pubkey_array: pubkeys,
202                message,
203            };
204
205            let borsh_serialized = event.try_to_vec().unwrap();
206            let mut manual_serialized = Vec::new();
207            event.man_serialize(&mut manual_serialized).unwrap();
208
209            assert_eq!(
210                borsh_serialized, manual_serialized,
211                "Borsh and manual serialization results should match"
212            );
213        }
214    }
215}