agave_transaction_view/
resolved_transaction_view.rs

1use {
2    crate::{
3        result::{Result, TransactionViewError},
4        transaction_data::TransactionData,
5        transaction_version::TransactionVersion,
6        transaction_view::TransactionView,
7    },
8    core::{
9        fmt::{Debug, Formatter},
10        ops::Deref,
11    },
12    solana_hash::Hash,
13    solana_message::{v0::LoadedAddresses, AccountKeys},
14    solana_pubkey::Pubkey,
15    solana_sdk_ids::bpf_loader_upgradeable,
16    solana_signature::Signature,
17    solana_svm_transaction::{
18        instruction::SVMInstruction, message_address_table_lookup::SVMMessageAddressTableLookup,
19        svm_message::SVMMessage, svm_transaction::SVMTransaction,
20    },
21    std::collections::HashSet,
22};
23
24/// A parsed and sanitized transaction view that has had all address lookups
25/// resolved.
26pub struct ResolvedTransactionView<D: TransactionData> {
27    /// The parsed and sanitized transction view.
28    view: TransactionView<true, D>,
29    /// The resolved address lookups.
30    resolved_addresses: Option<LoadedAddresses>,
31    /// A cache for whether an address is writable.
32    // Sanitized transactions are guaranteed to have a maximum of 256 keys,
33    // because account indexing is done with a u8.
34    writable_cache: [bool; 256],
35}
36
37impl<D: TransactionData> Deref for ResolvedTransactionView<D> {
38    type Target = TransactionView<true, D>;
39
40    fn deref(&self) -> &Self::Target {
41        &self.view
42    }
43}
44
45impl<D: TransactionData> ResolvedTransactionView<D> {
46    /// Given a parsed and sanitized transaction view, and a set of resolved
47    /// addresses, create a resolved transaction view.
48    pub fn try_new(
49        view: TransactionView<true, D>,
50        resolved_addresses: Option<LoadedAddresses>,
51        reserved_account_keys: &HashSet<Pubkey>,
52    ) -> Result<Self> {
53        let resolved_addresses_ref = resolved_addresses.as_ref();
54
55        // verify that the number of readable and writable match up.
56        // This is a basic sanity check to make sure we're not passing a totally
57        // invalid set of resolved addresses.
58        // Additionally if it is a v0 transaction it *must* have resolved
59        // addresses, even if they are empty.
60        if matches!(view.version(), TransactionVersion::V0) && resolved_addresses_ref.is_none() {
61            return Err(TransactionViewError::AddressLookupMismatch);
62        }
63        if let Some(loaded_addresses) = resolved_addresses_ref {
64            if loaded_addresses.writable.len() != usize::from(view.total_writable_lookup_accounts())
65                || loaded_addresses.readonly.len()
66                    != usize::from(view.total_readonly_lookup_accounts())
67            {
68                return Err(TransactionViewError::AddressLookupMismatch);
69            }
70        } else if view.total_writable_lookup_accounts() != 0
71            || view.total_readonly_lookup_accounts() != 0
72        {
73            return Err(TransactionViewError::AddressLookupMismatch);
74        }
75
76        let writable_cache =
77            Self::cache_is_writable(&view, resolved_addresses_ref, reserved_account_keys);
78        Ok(Self {
79            view,
80            resolved_addresses,
81            writable_cache,
82        })
83    }
84
85    /// Helper function to check if an address is writable,
86    /// and cache the result.
87    /// This is done so we avoid recomputing the expensive checks each time we call
88    /// `is_writable` - since there is more to it than just checking index.
89    fn cache_is_writable(
90        view: &TransactionView<true, D>,
91        resolved_addresses: Option<&LoadedAddresses>,
92        reserved_account_keys: &HashSet<Pubkey>,
93    ) -> [bool; 256] {
94        // Build account keys so that we can iterate over and check if
95        // an address is writable.
96        let account_keys = AccountKeys::new(view.static_account_keys(), resolved_addresses);
97
98        let mut is_writable_cache = [false; 256];
99        let num_static_account_keys = usize::from(view.num_static_account_keys());
100        let num_writable_lookup_accounts = usize::from(view.total_writable_lookup_accounts());
101        let num_signed_accounts = usize::from(view.num_required_signatures());
102        let num_writable_unsigned_static_accounts =
103            usize::from(view.num_writable_unsigned_static_accounts());
104        let num_writable_signed_static_accounts =
105            usize::from(view.num_writable_signed_static_accounts());
106
107        for (index, key) in account_keys.iter().enumerate() {
108            let is_requested_write = {
109                // If the account is a resolved address, check if it is writable.
110                if index >= num_static_account_keys {
111                    let loaded_address_index = index.wrapping_sub(num_static_account_keys);
112                    loaded_address_index < num_writable_lookup_accounts
113                } else if index >= num_signed_accounts {
114                    let unsigned_account_index = index.wrapping_sub(num_signed_accounts);
115                    unsigned_account_index < num_writable_unsigned_static_accounts
116                } else {
117                    index < num_writable_signed_static_accounts
118                }
119            };
120
121            // If the key is reserved it cannot be writable.
122            is_writable_cache[index] = is_requested_write && !reserved_account_keys.contains(key);
123        }
124
125        // If a program account is locked, it cannot be writable unless the
126        // upgradable loader is present.
127        // However, checking for the upgradable loader is somewhat expensive, so
128        // we only do it if we find a writable program id.
129        let mut is_upgradable_loader_present = None;
130        for ix in view.instructions_iter() {
131            let program_id_index = usize::from(ix.program_id_index);
132            if is_writable_cache[program_id_index]
133                && !*is_upgradable_loader_present.get_or_insert_with(|| {
134                    for key in account_keys.iter() {
135                        if key == &bpf_loader_upgradeable::ID {
136                            return true;
137                        }
138                    }
139                    false
140                })
141            {
142                is_writable_cache[program_id_index] = false;
143            }
144        }
145
146        is_writable_cache
147    }
148
149    pub fn loaded_addresses(&self) -> Option<&LoadedAddresses> {
150        self.resolved_addresses.as_ref()
151    }
152}
153
154impl<D: TransactionData> SVMMessage for ResolvedTransactionView<D> {
155    fn num_transaction_signatures(&self) -> u64 {
156        u64::from(self.view.num_required_signatures())
157    }
158
159    fn num_write_locks(&self) -> u64 {
160        self.view.num_requested_write_locks()
161    }
162
163    fn recent_blockhash(&self) -> &Hash {
164        self.view.recent_blockhash()
165    }
166
167    fn num_instructions(&self) -> usize {
168        usize::from(self.view.num_instructions())
169    }
170
171    fn instructions_iter(&self) -> impl Iterator<Item = SVMInstruction> {
172        self.view.instructions_iter()
173    }
174
175    fn program_instructions_iter(
176        &self,
177    ) -> impl Iterator<
178        Item = (
179            &solana_pubkey::Pubkey,
180            solana_svm_transaction::instruction::SVMInstruction,
181        ),
182    > + Clone {
183        self.view.program_instructions_iter()
184    }
185
186    fn account_keys(&self) -> AccountKeys {
187        AccountKeys::new(
188            self.view.static_account_keys(),
189            self.resolved_addresses.as_ref(),
190        )
191    }
192
193    fn fee_payer(&self) -> &Pubkey {
194        &self.view.static_account_keys()[0]
195    }
196
197    fn is_writable(&self, index: usize) -> bool {
198        self.writable_cache.get(index).copied().unwrap_or(false)
199    }
200
201    fn is_signer(&self, index: usize) -> bool {
202        index < usize::from(self.view.num_required_signatures())
203    }
204
205    fn is_invoked(&self, key_index: usize) -> bool {
206        let Ok(index) = u8::try_from(key_index) else {
207            return false;
208        };
209        self.view
210            .instructions_iter()
211            .any(|ix| ix.program_id_index == index)
212    }
213
214    fn num_lookup_tables(&self) -> usize {
215        usize::from(self.view.num_address_table_lookups())
216    }
217
218    fn message_address_table_lookups(&self) -> impl Iterator<Item = SVMMessageAddressTableLookup> {
219        self.view.address_table_lookup_iter()
220    }
221}
222
223impl<D: TransactionData> SVMTransaction for ResolvedTransactionView<D> {
224    fn signature(&self) -> &Signature {
225        &self.view.signatures()[0]
226    }
227
228    fn signatures(&self) -> &[Signature] {
229        self.view.signatures()
230    }
231}
232
233impl<D: TransactionData> Debug for ResolvedTransactionView<D> {
234    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
235        f.debug_struct("ResolvedTransactionView")
236            .field("view", &self.view)
237            .finish()
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use {
244        super::*,
245        crate::transaction_view::SanitizedTransactionView,
246        solana_message::{
247            compiled_instruction::CompiledInstruction,
248            v0::{self, MessageAddressTableLookup},
249            MessageHeader, VersionedMessage,
250        },
251        solana_sdk_ids::{system_program, sysvar},
252        solana_signature::Signature,
253        solana_transaction::versioned::VersionedTransaction,
254    };
255
256    #[test]
257    fn test_expected_loaded_addresses() {
258        // Expected addresses passed in, but `None` was passed.
259        let static_keys = vec![Pubkey::new_unique(), Pubkey::new_unique()];
260        let transaction = VersionedTransaction {
261            signatures: vec![Signature::default()],
262            message: VersionedMessage::V0(v0::Message {
263                header: MessageHeader {
264                    num_required_signatures: 1,
265                    num_readonly_signed_accounts: 0,
266                    num_readonly_unsigned_accounts: 0,
267                },
268                instructions: vec![],
269                account_keys: static_keys,
270                address_table_lookups: vec![MessageAddressTableLookup {
271                    account_key: Pubkey::new_unique(),
272                    writable_indexes: vec![0],
273                    readonly_indexes: vec![1],
274                }],
275                recent_blockhash: Hash::default(),
276            }),
277        };
278        let bytes = bincode::serialize(&transaction).unwrap();
279        let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref()).unwrap();
280        let result = ResolvedTransactionView::try_new(view, None, &HashSet::default());
281        assert!(matches!(
282            result,
283            Err(TransactionViewError::AddressLookupMismatch)
284        ));
285    }
286
287    #[test]
288    fn test_unexpected_loaded_addresses() {
289        // Expected no addresses passed in, but `Some` was passed.
290        let static_keys = vec![Pubkey::new_unique(), Pubkey::new_unique()];
291        let loaded_addresses = LoadedAddresses {
292            writable: vec![Pubkey::new_unique()],
293            readonly: vec![],
294        };
295        let transaction = VersionedTransaction {
296            signatures: vec![Signature::default()],
297            message: VersionedMessage::V0(v0::Message {
298                header: MessageHeader {
299                    num_required_signatures: 1,
300                    num_readonly_signed_accounts: 0,
301                    num_readonly_unsigned_accounts: 0,
302                },
303                instructions: vec![],
304                account_keys: static_keys,
305                address_table_lookups: vec![],
306                recent_blockhash: Hash::default(),
307            }),
308        };
309        let bytes = bincode::serialize(&transaction).unwrap();
310        let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref()).unwrap();
311        let result =
312            ResolvedTransactionView::try_new(view, Some(loaded_addresses), &HashSet::default());
313        assert!(matches!(
314            result,
315            Err(TransactionViewError::AddressLookupMismatch)
316        ));
317    }
318
319    #[test]
320    fn test_mismatched_loaded_address_lengths() {
321        // Loaded addresses only has 1 writable address, no readonly.
322        // The message ATL has 1 writable and 1 readonly.
323        let static_keys = vec![Pubkey::new_unique(), Pubkey::new_unique()];
324        let loaded_addresses = LoadedAddresses {
325            writable: vec![Pubkey::new_unique()],
326            readonly: vec![],
327        };
328        let transaction = VersionedTransaction {
329            signatures: vec![Signature::default()],
330            message: VersionedMessage::V0(v0::Message {
331                header: MessageHeader {
332                    num_required_signatures: 1,
333                    num_readonly_signed_accounts: 0,
334                    num_readonly_unsigned_accounts: 0,
335                },
336                instructions: vec![],
337                account_keys: static_keys,
338                address_table_lookups: vec![MessageAddressTableLookup {
339                    account_key: Pubkey::new_unique(),
340                    writable_indexes: vec![0],
341                    readonly_indexes: vec![1],
342                }],
343                recent_blockhash: Hash::default(),
344            }),
345        };
346        let bytes = bincode::serialize(&transaction).unwrap();
347        let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref()).unwrap();
348        let result =
349            ResolvedTransactionView::try_new(view, Some(loaded_addresses), &HashSet::default());
350        assert!(matches!(
351            result,
352            Err(TransactionViewError::AddressLookupMismatch)
353        ));
354    }
355
356    #[test]
357    fn test_is_writable() {
358        let reserved_account_keys = HashSet::from_iter([sysvar::clock::id(), system_program::id()]);
359        // Create a versioned transaction.
360        let create_transaction_with_keys =
361            |static_keys: Vec<Pubkey>, loaded_addresses: &LoadedAddresses| VersionedTransaction {
362                signatures: vec![Signature::default()],
363                message: VersionedMessage::V0(v0::Message {
364                    header: MessageHeader {
365                        num_required_signatures: 1,
366                        num_readonly_signed_accounts: 0,
367                        num_readonly_unsigned_accounts: 1,
368                    },
369                    account_keys: static_keys[..2].to_vec(),
370                    recent_blockhash: Hash::default(),
371                    instructions: vec![],
372                    address_table_lookups: vec![MessageAddressTableLookup {
373                        account_key: Pubkey::new_unique(),
374                        writable_indexes: (0..loaded_addresses.writable.len())
375                            .map(|x| (static_keys.len() + x) as u8)
376                            .collect(),
377                        readonly_indexes: (0..loaded_addresses.readonly.len())
378                            .map(|x| {
379                                (static_keys.len() + loaded_addresses.writable.len() + x) as u8
380                            })
381                            .collect(),
382                    }],
383                }),
384            };
385
386        let key0 = Pubkey::new_unique();
387        let key1 = Pubkey::new_unique();
388        let key2 = Pubkey::new_unique();
389        {
390            let static_keys = vec![sysvar::clock::id(), key0];
391            let loaded_addresses = LoadedAddresses {
392                writable: vec![key1],
393                readonly: vec![key2],
394            };
395            let transaction = create_transaction_with_keys(static_keys, &loaded_addresses);
396            let bytes = bincode::serialize(&transaction).unwrap();
397            let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref()).unwrap();
398            let resolved_view = ResolvedTransactionView::try_new(
399                view,
400                Some(loaded_addresses),
401                &reserved_account_keys,
402            )
403            .unwrap();
404
405            // demote reserved static key to readonly
406            let expected = vec![false, false, true, false];
407            for (index, expected) in expected.into_iter().enumerate() {
408                assert_eq!(resolved_view.is_writable(index), expected);
409            }
410        }
411
412        {
413            let static_keys = vec![system_program::id(), key0];
414            let loaded_addresses = LoadedAddresses {
415                writable: vec![key1],
416                readonly: vec![key2],
417            };
418            let transaction = create_transaction_with_keys(static_keys, &loaded_addresses);
419            let bytes = bincode::serialize(&transaction).unwrap();
420            let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref()).unwrap();
421            let resolved_view = ResolvedTransactionView::try_new(
422                view,
423                Some(loaded_addresses),
424                &reserved_account_keys,
425            )
426            .unwrap();
427
428            // demote reserved static key to readonly
429            let expected = vec![false, false, true, false];
430            for (index, expected) in expected.into_iter().enumerate() {
431                assert_eq!(resolved_view.is_writable(index), expected);
432            }
433        }
434
435        {
436            let static_keys = vec![key0, key1];
437            let loaded_addresses = LoadedAddresses {
438                writable: vec![system_program::id()],
439                readonly: vec![key2],
440            };
441            let transaction = create_transaction_with_keys(static_keys, &loaded_addresses);
442            let bytes = bincode::serialize(&transaction).unwrap();
443            let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref()).unwrap();
444            let resolved_view = ResolvedTransactionView::try_new(
445                view,
446                Some(loaded_addresses),
447                &reserved_account_keys,
448            )
449            .unwrap();
450
451            // demote loaded key to readonly
452            let expected = vec![true, false, false, false];
453            for (index, expected) in expected.into_iter().enumerate() {
454                assert_eq!(resolved_view.is_writable(index), expected);
455            }
456        }
457    }
458
459    #[test]
460    fn test_demote_writable_program() {
461        let reserved_account_keys = HashSet::default();
462        let key0 = Pubkey::new_unique();
463        let key1 = Pubkey::new_unique();
464        let key2 = Pubkey::new_unique();
465        let key3 = Pubkey::new_unique();
466        let key4 = Pubkey::new_unique();
467        let loaded_addresses = LoadedAddresses {
468            writable: vec![key3, key4],
469            readonly: vec![],
470        };
471        let create_transaction_with_static_keys =
472            |static_keys: Vec<Pubkey>, loaded_addresses: &LoadedAddresses| VersionedTransaction {
473                signatures: vec![Signature::default()],
474                message: VersionedMessage::V0(v0::Message {
475                    header: MessageHeader {
476                        num_required_signatures: 1,
477                        num_readonly_signed_accounts: 0,
478                        num_readonly_unsigned_accounts: 0,
479                    },
480                    instructions: vec![CompiledInstruction {
481                        program_id_index: 1,
482                        accounts: vec![0],
483                        data: vec![],
484                    }],
485                    account_keys: static_keys,
486                    address_table_lookups: vec![MessageAddressTableLookup {
487                        account_key: Pubkey::new_unique(),
488                        writable_indexes: (0..loaded_addresses.writable.len())
489                            .map(|x| x as u8)
490                            .collect(),
491                        readonly_indexes: (0..loaded_addresses.readonly.len())
492                            .map(|x| (loaded_addresses.writable.len() + x) as u8)
493                            .collect(),
494                    }],
495                    recent_blockhash: Hash::default(),
496                }),
497            };
498
499        // Demote writable program - static
500        {
501            let static_keys = vec![key0, key1, key2];
502            let transaction = create_transaction_with_static_keys(static_keys, &loaded_addresses);
503            let bytes = bincode::serialize(&transaction).unwrap();
504            let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref()).unwrap();
505            let resolved_view = ResolvedTransactionView::try_new(
506                view,
507                Some(loaded_addresses.clone()),
508                &reserved_account_keys,
509            )
510            .unwrap();
511
512            let expected = vec![true, false, true, true, true];
513            for (index, expected) in expected.into_iter().enumerate() {
514                assert_eq!(resolved_view.is_writable(index), expected);
515            }
516        }
517
518        // Do not demote writable program - static address: upgradable loader
519        {
520            let static_keys = vec![key0, key1, bpf_loader_upgradeable::ID];
521            let transaction = create_transaction_with_static_keys(static_keys, &loaded_addresses);
522            let bytes = bincode::serialize(&transaction).unwrap();
523            let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref()).unwrap();
524            let resolved_view = ResolvedTransactionView::try_new(
525                view,
526                Some(loaded_addresses.clone()),
527                &reserved_account_keys,
528            )
529            .unwrap();
530
531            let expected = vec![true, true, true, true, true];
532            for (index, expected) in expected.into_iter().enumerate() {
533                assert_eq!(resolved_view.is_writable(index), expected);
534            }
535        }
536
537        // Do not demote writable program - loaded address: upgradable loader
538        {
539            let static_keys = vec![key0, key1, key2];
540            let loaded_addresses = LoadedAddresses {
541                writable: vec![key3],
542                readonly: vec![bpf_loader_upgradeable::ID],
543            };
544            let transaction = create_transaction_with_static_keys(static_keys, &loaded_addresses);
545            let bytes = bincode::serialize(&transaction).unwrap();
546            let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref()).unwrap();
547
548            let resolved_view = ResolvedTransactionView::try_new(
549                view,
550                Some(loaded_addresses.clone()),
551                &reserved_account_keys,
552            )
553            .unwrap();
554
555            let expected = vec![true, true, true, true, false];
556            for (index, expected) in expected.into_iter().enumerate() {
557                assert_eq!(resolved_view.is_writable(index), expected);
558            }
559        }
560    }
561}