Skip to main content

dlp_api/compact/
mod.rs

1mod account_meta;
2mod instruction;
3
4pub use account_meta::*;
5pub use instruction::*;
6use pinocchio::Address;
7
8use crate::args::{
9    EncryptedBuffer, MaybeEncryptedInstruction, MaybeEncryptedIxData,
10    MaybeEncryptedPubkey, PostDelegationActions,
11};
12
13pub trait ClearText: Sized {
14    type Output;
15
16    fn cleartext(self) -> Self::Output;
17}
18
19pub trait ClearTextWithInsertable: Sized {
20    type Output;
21
22    fn cleartext_with_insertable(
23        self,
24        insertable: PostDelegationActions,
25        insert_before_index: usize,
26    ) -> Self::Output;
27}
28
29impl ClearText for Vec<u8> {
30    type Output = MaybeEncryptedIxData;
31
32    fn cleartext(self) -> Self::Output {
33        MaybeEncryptedIxData {
34            prefix: self,
35            suffix: EncryptedBuffer::default(),
36        }
37    }
38}
39
40impl ClearText for Vec<solana_instruction::Instruction> {
41    type Output = PostDelegationActions;
42
43    fn cleartext(self) -> Self::Output {
44        let mut signers: Vec<solana_instruction::AccountMeta> = Vec::new();
45        let mut non_signers: Vec<solana_instruction::AccountMeta> = Vec::new();
46
47        let mut add_to_signers = |meta: &solana_instruction::AccountMeta| {
48            assert!(meta.is_signer, "AccountMeta must be a signer");
49            let Some(found) =
50                signers.iter_mut().find(|m| m.pubkey == meta.pubkey)
51            else {
52                signers.push(meta.clone());
53                return;
54            };
55
56            found.is_signer |= meta.is_signer;
57            found.is_writable |= meta.is_writable;
58        };
59
60        let mut add_to_non_signers =
61            |meta: &solana_instruction::AccountMeta| {
62                assert!(!meta.is_signer, "AccountMeta must not be a signer");
63                let Some(found) =
64                    non_signers.iter_mut().find(|m| m.pubkey == meta.pubkey)
65                else {
66                    non_signers.push(meta.clone());
67                    return;
68                };
69
70                found.is_writable |= meta.is_writable;
71            };
72
73        for meta in self
74            .iter()
75            .flat_map(|ix| ix.accounts.iter())
76            .filter(|meta| meta.is_signer)
77        {
78            add_to_signers(meta);
79        }
80
81        for ix in self.iter() {
82            add_to_non_signers(&solana_instruction::AccountMeta::new_readonly(
83                ix.program_id,
84                false,
85            ));
86            for meta in ix.accounts.iter().filter(|meta| !meta.is_signer) {
87                let Some(found) =
88                    signers.iter_mut().find(|m| m.pubkey == meta.pubkey)
89                else {
90                    add_to_non_signers(meta);
91                    continue;
92                };
93
94                found.is_writable |= meta.is_writable;
95            }
96        }
97
98        if signers.len() + non_signers.len()
99            > crate::compact::MAX_PUBKEYS as usize
100        {
101            panic!(
102                "delegate_with_actions supports at most {} unique pubkeys",
103                crate::compact::MAX_PUBKEYS
104            );
105        }
106
107        let index_of = |pk: &solana_address::Address| -> u8 {
108            if let Some(index) = signers.iter().position(|s| &s.pubkey == pk) {
109                return index as u8;
110            }
111            signers.len() as u8
112                + non_signers
113                    .iter()
114                    .position(|ns| &ns.pubkey == pk)
115                    .expect("pubkey must exist in signers or non_signers")
116                    as u8
117        };
118
119        let compact_instructions: Vec<MaybeEncryptedInstruction> = self
120            .into_iter()
121            .map(|ix| MaybeEncryptedInstruction {
122                program_id: index_of(&ix.program_id),
123
124                accounts: ix
125                    .accounts
126                    .into_iter()
127                    .map(|meta| {
128                        let index = index_of(&meta.pubkey);
129                        crate::compact::AccountMeta::try_new(
130                            index,
131                            meta.is_signer,
132                            meta.is_writable,
133                        )
134                        .expect("compact account index must fit in 6 bits")
135                        .cleartext()
136                    })
137                    .collect(),
138
139                data: ix.data.cleartext(),
140            })
141            .collect();
142
143        PostDelegationActions {
144            inserted_signers: 0,
145            inserted_non_signers: 0,
146
147            signers: signers.iter().map(|s| s.pubkey.to_bytes()).collect(),
148
149            non_signers: non_signers
150                .into_iter()
151                .map(|ns| MaybeEncryptedPubkey::ClearText(ns.pubkey.to_bytes()))
152                .collect(),
153
154            instructions: compact_instructions,
155        }
156    }
157}
158
159impl ClearTextWithInsertable for Vec<solana_instruction::Instruction> {
160    type Output = PostDelegationActions;
161    fn cleartext_with_insertable(
162        self,
163        insertable: PostDelegationActions,
164        insert_before_index: usize,
165    ) -> Self::Output {
166        assert!(
167            insertable.inserted_signers == 0,
168            "PostDelegationActions does not support multiple merge/insert"
169        );
170        assert!(
171            insertable.inserted_non_signers == 0,
172            "PostDelegationActions does not support multiple merge/insert"
173        );
174
175        // add keys from actions (pre-encrypted instructions)
176        let mut skipable_pubkeys: Vec<Option<Address>> = vec![];
177        {
178            for signer in insertable.signers.iter() {
179                skipable_pubkeys.push(Some((*signer).into()));
180            }
181            for non_signer in insertable.non_signers.iter() {
182                if let MaybeEncryptedPubkey::ClearText(non_signer) = non_signer
183                {
184                    skipable_pubkeys.push(Some((*non_signer).into()));
185                } else {
186                    // Note that None is added to the list, to mark that this slot is encrypted but
187                    // the index is already taken so that the index in referred by insertable.instructions
188                    // is maintained/calculatable.
189                    skipable_pubkeys.push(None);
190                }
191            }
192        }
193
194        let mut signers: Vec<solana_instruction::AccountMeta> = Vec::new();
195        let mut non_signers: Vec<solana_instruction::AccountMeta> = Vec::new();
196
197        let mut add_to_signers = |meta: &solana_instruction::AccountMeta| {
198            if skipable_pubkeys.contains(&Some(meta.pubkey)) {
199                return;
200            }
201
202            assert!(meta.is_signer, "AccountMeta must be a signer");
203            let Some(found) =
204                signers.iter_mut().find(|m| m.pubkey == meta.pubkey)
205            else {
206                signers.push(meta.clone());
207                return;
208            };
209
210            found.is_writable |= meta.is_writable;
211        };
212
213        let mut add_to_non_signers =
214            |meta: &solana_instruction::AccountMeta| {
215                if skipable_pubkeys.contains(&Some(meta.pubkey)) {
216                    return;
217                }
218
219                assert!(!meta.is_signer, "AccountMeta must not be a signer");
220                let Some(found) =
221                    non_signers.iter_mut().find(|m| m.pubkey == meta.pubkey)
222                else {
223                    non_signers.push(meta.clone());
224                    return;
225                };
226
227                found.is_writable |= meta.is_writable;
228            };
229
230        for meta in self
231            .iter()
232            .flat_map(|ix| ix.accounts.iter())
233            .filter(|meta| meta.is_signer)
234        {
235            add_to_signers(meta);
236        }
237
238        for ix in self.iter() {
239            add_to_non_signers(&solana_instruction::AccountMeta::new_readonly(
240                ix.program_id,
241                false,
242            ));
243            for meta in ix.accounts.iter().filter(|meta| !meta.is_signer) {
244                let Some(found) =
245                    signers.iter_mut().find(|m| m.pubkey == meta.pubkey)
246                else {
247                    add_to_non_signers(meta);
248                    continue;
249                };
250
251                found.is_writable |= meta.is_writable;
252            }
253        }
254
255        if signers.len() + non_signers.len()
256            > crate::compact::MAX_PUBKEYS as usize
257        {
258            panic!(
259                "delegate_with_actions supports at most {} unique pubkeys",
260                crate::compact::MAX_PUBKEYS
261            );
262        }
263
264        let old_signers_len = insertable.signers.len();
265        let old_non_signers_len = insertable.non_signers.len();
266        let old_total = old_signers_len + old_non_signers_len;
267
268        let index_of = |pk: &solana_address::Address| -> u8 {
269            // The final list will be this as per PostDelegationActions:
270            //
271            //  [insertable.signers..., new.signers..., insertable.non_signers..., new.non_signers...]
272            //
273            // However, the final list will invalidate the indices (of non-signers) referred to
274            // by insertable.instructions, though indices of signers will continue to be correct.
275            //
276            // To deal with that, we need to compute the indices of newly added pubkeys
277            // differently, accordingly to this imagined list:
278            //
279            //  [insertable.signers..., insertable.non_signers..., new.signers..., new.non_signers...]
280            //
281            // That means, if a key is found in skipable_pubkeys, its index will be returned as it
282            // is. Else, we'll add `old_total` to the index computed for the following list:
283            //
284            //  [new.signers..., new.non_signers...]
285            //
286            if let Some(index) = skipable_pubkeys
287                .iter()
288                .position(|pubkey| pubkey == &Some(*pk))
289            {
290                return index as u8;
291            }
292
293            if let Some(index) = signers.iter().position(|s| &s.pubkey == pk) {
294                return (old_total + index) as u8;
295            }
296            (old_total
297                + signers.len()
298                + non_signers.iter().position(|ns| &ns.pubkey == pk).unwrap())
299                as u8
300        };
301
302        let mut compact_instructions: Vec<MaybeEncryptedInstruction> = self
303            .into_iter()
304            .map(|ix| MaybeEncryptedInstruction {
305                program_id: index_of(&ix.program_id),
306
307                accounts: ix
308                    .accounts
309                    .into_iter()
310                    .map(|meta| {
311                        let index = index_of(&meta.pubkey);
312                        crate::compact::AccountMeta::try_new(
313                            index,
314                            meta.is_signer,
315                            meta.is_writable,
316                        )
317                        .expect("compact account index must fit in 6 bits")
318                        .cleartext()
319                    })
320                    .collect(),
321
322                data: ix.data.cleartext(),
323            })
324            .collect();
325
326        // Merge all parts now
327        let mut rv = insertable;
328        rv.inserted_signers = old_signers_len as u8;
329        rv.inserted_non_signers = old_non_signers_len as u8;
330
331        rv.signers.extend_from_slice(
332            &signers
333                .iter()
334                .map(|s| s.pubkey.to_bytes())
335                .collect::<Vec<_>>(),
336        );
337        rv.non_signers.extend_from_slice(
338            &non_signers
339                .iter()
340                .map(|ns| MaybeEncryptedPubkey::ClearText(ns.pubkey.to_bytes()))
341                .collect::<Vec<_>>(),
342        );
343
344        if insert_before_index <= compact_instructions.len() {
345            compact_instructions.splice(
346                insert_before_index..insert_before_index,
347                rv.instructions,
348            );
349        } else {
350            compact_instructions.extend_from_slice(&rv.instructions);
351        }
352
353        rv.instructions = compact_instructions;
354
355        rv
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use solana_instruction::{AccountMeta, Instruction};
362    use solana_pubkey::Pubkey;
363
364    use super::*;
365    use crate::args::MaybeEncryptedAccountMeta;
366
367    fn pk(byte: u8) -> Pubkey {
368        Pubkey::new_from_array([byte; 32])
369    }
370
371    fn assert_cleartext_meta(
372        meta: &MaybeEncryptedAccountMeta,
373        expected_index: u8,
374        expected_signer: bool,
375    ) {
376        let MaybeEncryptedAccountMeta::ClearText(meta) = meta else {
377            panic!("expected cleartext account meta");
378        };
379        assert_eq!(meta.key(), expected_index);
380        assert_eq!(meta.is_signer(), expected_signer);
381    }
382
383    #[test]
384    fn test_cleartext_with_insertable_indices() {
385        let s1 = pk(1);
386        let s2 = pk(2);
387        let n1 = pk(3);
388        let n2 = pk(4);
389        let n3 = pk(5);
390
391        let insertable = PostDelegationActions {
392            inserted_signers: 0,
393            inserted_non_signers: 0,
394            signers: vec![s1.to_bytes(), s2.to_bytes()],
395            non_signers: vec![
396                MaybeEncryptedPubkey::ClearText(n1.to_bytes()),
397                MaybeEncryptedPubkey::ClearText(n2.to_bytes()),
398                MaybeEncryptedPubkey::Encrypted(EncryptedBuffer::new(
399                    n3.to_bytes().into(),
400                )),
401            ],
402            instructions: vec![MaybeEncryptedInstruction {
403                program_id: 0,
404                accounts: vec![],
405                data: MaybeEncryptedIxData {
406                    prefix: vec![],
407                    suffix: EncryptedBuffer::new(vec![]),
408                },
409            }],
410        };
411
412        let ns1 = pk(6);
413        let nn1 = pk(7);
414        let program_id = pk(8);
415
416        let ix = Instruction {
417            program_id,
418            accounts: vec![
419                AccountMeta::new_readonly(s1, true), // reuse old key
420                AccountMeta::new_readonly(ns1, true),
421                AccountMeta::new_readonly(nn1, false),
422                AccountMeta::new_readonly(n3, false), // reuse old key but encrypted
423            ],
424            data: vec![1, 2, 3],
425        };
426
427        let actions = vec![ix].cleartext_with_insertable(insertable, 1);
428
429        assert_eq!(actions.inserted_signers, 2);
430        assert_eq!(actions.inserted_non_signers, 3); // even though 1 is encrypted
431
432        assert_eq!(actions.signers.len(), 3);
433        assert_eq!(actions.non_signers.len(), 5 + 1); // n3 is inserted again
434
435        assert_eq!(
436            actions.signers,
437            vec![s1.to_bytes(), s2.to_bytes(), ns1.to_bytes()]
438        );
439        assert_eq!(
440            actions.non_signers,
441            vec![
442                MaybeEncryptedPubkey::ClearText(n1.to_bytes()),
443                MaybeEncryptedPubkey::ClearText(n2.to_bytes()),
444                MaybeEncryptedPubkey::Encrypted(EncryptedBuffer::new(
445                    n3.to_bytes().into(),
446                )),
447                MaybeEncryptedPubkey::ClearText(program_id.to_bytes()),
448                MaybeEncryptedPubkey::ClearText(nn1.to_bytes()),
449                MaybeEncryptedPubkey::ClearText(n3.to_bytes()),
450            ]
451        );
452
453        assert_eq!(actions.instructions.len(), 2);
454        let new_ix = &actions.instructions[0];
455        assert_eq!(new_ix.program_id, 6);
456        assert_eq!(new_ix.accounts.len(), 4);
457
458        assert_cleartext_meta(&new_ix.accounts[0], 0, true);
459        assert_cleartext_meta(&new_ix.accounts[1], 5, true);
460        assert_cleartext_meta(&new_ix.accounts[2], 7, false);
461        assert_cleartext_meta(&new_ix.accounts[3], 8, false);
462    }
463}