solana_address_lookup_table_interface/
state.rs

1#[cfg(feature = "serde")]
2use serde_derive::{Deserialize, Serialize};
3#[cfg(feature = "frozen-abi")]
4use solana_frozen_abi_macro::{AbiEnumVisitor, AbiExample};
5#[cfg(feature = "bincode")]
6use solana_instruction::error::InstructionError;
7use {
8    crate::error::AddressLookupError,
9    solana_clock::Slot,
10    solana_pubkey::Pubkey,
11    solana_slot_hashes::{get_entries, SlotHashes, MAX_ENTRIES},
12    std::borrow::Cow,
13};
14
15/// The lookup table may be in a deactivating state until
16/// the `deactivation_slot`` is no longer "recent".
17/// This function returns a conservative estimate for the
18/// last block that the table may be used for lookups.
19/// This estimate may be incorrect due to skipped blocks,
20/// however, if the current slot is lower than the returned
21/// value, the table is guaranteed to still be in the
22/// deactivating state.
23#[inline]
24pub fn estimate_last_valid_slot(deactivation_slot: Slot) -> Slot {
25    deactivation_slot.saturating_add(get_entries() as Slot)
26}
27
28/// The maximum number of addresses that a lookup table can hold
29pub const LOOKUP_TABLE_MAX_ADDRESSES: usize = 256;
30
31/// The serialized size of lookup table metadata
32pub const LOOKUP_TABLE_META_SIZE: usize = 56;
33
34/// Activation status of a lookup table
35#[derive(Debug, PartialEq, Eq, Clone)]
36pub enum LookupTableStatus {
37    Activated,
38    Deactivating { remaining_blocks: usize },
39    Deactivated,
40}
41
42/// Address lookup table metadata
43#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
44#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
45#[derive(Debug, PartialEq, Eq, Clone)]
46pub struct LookupTableMeta {
47    /// Lookup tables cannot be closed until the deactivation slot is
48    /// no longer "recent" (not accessible in the `SlotHashes` sysvar).
49    pub deactivation_slot: Slot,
50    /// The slot that the table was last extended. Address tables may
51    /// only be used to lookup addresses that were extended before
52    /// the current bank's slot.
53    pub last_extended_slot: Slot,
54    /// The start index where the table was last extended from during
55    /// the `last_extended_slot`.
56    pub last_extended_slot_start_index: u8,
57    /// Authority address which must sign for each modification.
58    pub authority: Option<Pubkey>,
59    // Padding to keep addresses 8-byte aligned
60    pub _padding: u16,
61    // Raw list of addresses follows this serialized structure in
62    // the account's data, starting from `LOOKUP_TABLE_META_SIZE`.
63}
64
65impl Default for LookupTableMeta {
66    fn default() -> Self {
67        Self {
68            deactivation_slot: Slot::MAX,
69            last_extended_slot: 0,
70            last_extended_slot_start_index: 0,
71            authority: None,
72            _padding: 0,
73        }
74    }
75}
76
77impl LookupTableMeta {
78    pub fn new(authority: Pubkey) -> Self {
79        LookupTableMeta {
80            authority: Some(authority),
81            ..LookupTableMeta::default()
82        }
83    }
84
85    /// Returns whether the table is considered active for address lookups
86    pub fn is_active(&self, current_slot: Slot, slot_hashes: &SlotHashes) -> bool {
87        match self.status(current_slot, slot_hashes) {
88            LookupTableStatus::Activated => true,
89            LookupTableStatus::Deactivating { .. } => true,
90            LookupTableStatus::Deactivated => false,
91        }
92    }
93
94    /// Return the current status of the lookup table
95    pub fn status(&self, current_slot: Slot, slot_hashes: &SlotHashes) -> LookupTableStatus {
96        if self.deactivation_slot == Slot::MAX {
97            LookupTableStatus::Activated
98        } else if self.deactivation_slot == current_slot {
99            LookupTableStatus::Deactivating {
100                remaining_blocks: MAX_ENTRIES.saturating_add(1),
101            }
102        } else if let Some(slot_hash_position) = slot_hashes.position(&self.deactivation_slot) {
103            // Deactivation requires a cool-down period to give in-flight transactions
104            // enough time to land and to remove indeterminism caused by transactions loading
105            // addresses in the same slot when a table is closed. The cool-down period is
106            // equivalent to the amount of time it takes for a slot to be removed from the
107            // slot hash list.
108            //
109            // By using the slot hash to enforce the cool-down, there is a side effect
110            // of not allowing lookup tables to be recreated at the same derived address
111            // because tables must be created at an address derived from a recent slot.
112            LookupTableStatus::Deactivating {
113                remaining_blocks: MAX_ENTRIES.saturating_sub(slot_hash_position),
114            }
115        } else {
116            LookupTableStatus::Deactivated
117        }
118    }
119}
120
121/// Program account states
122#[cfg_attr(feature = "frozen-abi", derive(AbiEnumVisitor, AbiExample))]
123#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
124#[derive(Debug, PartialEq, Eq, Clone)]
125#[allow(clippy::large_enum_variant)]
126pub enum ProgramState {
127    /// Account is not initialized.
128    Uninitialized,
129    /// Initialized `LookupTable` account.
130    LookupTable(LookupTableMeta),
131}
132
133#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
134#[derive(Debug, PartialEq, Eq, Clone)]
135pub struct AddressLookupTable<'a> {
136    pub meta: LookupTableMeta,
137    pub addresses: Cow<'a, [Pubkey]>,
138}
139
140impl<'a> AddressLookupTable<'a> {
141    /// Serialize an address table's updated meta data and zero
142    /// any leftover bytes.
143    #[cfg(feature = "bincode")]
144    pub fn overwrite_meta_data(
145        data: &mut [u8],
146        lookup_table_meta: LookupTableMeta,
147    ) -> Result<(), InstructionError> {
148        let meta_data = data
149            .get_mut(0..LOOKUP_TABLE_META_SIZE)
150            .ok_or(InstructionError::InvalidAccountData)?;
151        meta_data.fill(0);
152        bincode::serialize_into(meta_data, &ProgramState::LookupTable(lookup_table_meta))
153            .map_err(|_| InstructionError::GenericError)?;
154        Ok(())
155    }
156
157    /// Get the length of addresses that are active for lookups
158    pub fn get_active_addresses_len(
159        &self,
160        current_slot: Slot,
161        slot_hashes: &SlotHashes,
162    ) -> Result<usize, AddressLookupError> {
163        if !self.meta.is_active(current_slot, slot_hashes) {
164            // Once a lookup table is no longer active, it can be closed
165            // at any point, so returning a specific error for deactivated
166            // lookup tables could result in a race condition.
167            return Err(AddressLookupError::LookupTableAccountNotFound);
168        }
169
170        // If the address table was extended in the same slot in which it is used
171        // to lookup addresses for another transaction, the recently extended
172        // addresses are not considered active and won't be accessible.
173        let active_addresses_len = if current_slot > self.meta.last_extended_slot {
174            self.addresses.len()
175        } else {
176            self.meta.last_extended_slot_start_index as usize
177        };
178
179        Ok(active_addresses_len)
180    }
181
182    /// Lookup addresses for provided table indexes. Since lookups are performed on
183    /// tables which are not read-locked, this implementation needs to be careful
184    /// about resolving addresses consistently.
185    pub fn lookup(
186        &self,
187        current_slot: Slot,
188        indexes: &[u8],
189        slot_hashes: &SlotHashes,
190    ) -> Result<Vec<Pubkey>, AddressLookupError> {
191        self.lookup_iter(current_slot, indexes, slot_hashes)?
192            .collect::<Option<_>>()
193            .ok_or(AddressLookupError::InvalidLookupIndex)
194    }
195
196    /// Lookup addresses for provided table indexes. Since lookups are performed on
197    /// tables which are not read-locked, this implementation needs to be careful
198    /// about resolving addresses consistently.
199    /// If ANY of the indexes return `None`, the entire lookup should be considered
200    /// invalid.
201    pub fn lookup_iter(
202        &'a self,
203        current_slot: Slot,
204        indexes: &'a [u8],
205        slot_hashes: &SlotHashes,
206    ) -> Result<impl Iterator<Item = Option<Pubkey>> + 'a, AddressLookupError> {
207        let active_addresses_len = self.get_active_addresses_len(current_slot, slot_hashes)?;
208        let active_addresses = &self.addresses[0..active_addresses_len];
209        Ok(indexes
210            .iter()
211            .map(|idx| active_addresses.get(*idx as usize).cloned()))
212    }
213
214    /// Serialize an address table including its addresses
215    #[cfg(feature = "bincode")]
216    pub fn serialize_for_tests(self) -> Result<Vec<u8>, InstructionError> {
217        let mut data = vec![0; LOOKUP_TABLE_META_SIZE];
218        Self::overwrite_meta_data(&mut data, self.meta)?;
219        self.addresses.iter().for_each(|address| {
220            data.extend_from_slice(address.as_ref());
221        });
222        Ok(data)
223    }
224
225    /// Efficiently deserialize an address table without allocating
226    /// for stored addresses.
227    #[cfg(all(feature = "bincode", feature = "bytemuck"))]
228    pub fn deserialize(data: &'a [u8]) -> Result<AddressLookupTable<'a>, InstructionError> {
229        let program_state: ProgramState =
230            bincode::deserialize(data).map_err(|_| InstructionError::InvalidAccountData)?;
231
232        let meta = match program_state {
233            ProgramState::LookupTable(meta) => Ok(meta),
234            ProgramState::Uninitialized => Err(InstructionError::UninitializedAccount),
235        }?;
236
237        let raw_addresses_data = data.get(LOOKUP_TABLE_META_SIZE..).ok_or({
238            // Should be impossible because table accounts must
239            // always be LOOKUP_TABLE_META_SIZE in length
240            InstructionError::InvalidAccountData
241        })?;
242        let addresses: &[Pubkey] = bytemuck::try_cast_slice(raw_addresses_data).map_err(|_| {
243            // Should be impossible because raw address data
244            // should be aligned and sized in multiples of 32 bytes
245            InstructionError::InvalidAccountData
246        })?;
247
248        Ok(Self {
249            meta,
250            addresses: Cow::Borrowed(addresses),
251        })
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use {super::*, solana_hash::Hash};
258
259    impl AddressLookupTable<'_> {
260        fn new_for_tests(meta: LookupTableMeta, num_addresses: usize) -> Self {
261            let mut addresses = Vec::with_capacity(num_addresses);
262            addresses.resize_with(num_addresses, Pubkey::new_unique);
263            AddressLookupTable {
264                meta,
265                addresses: Cow::Owned(addresses),
266            }
267        }
268    }
269
270    impl LookupTableMeta {
271        fn new_for_tests() -> Self {
272            Self {
273                authority: Some(Pubkey::new_unique()),
274                ..LookupTableMeta::default()
275            }
276        }
277    }
278
279    #[test]
280    fn test_lookup_table_meta_size() {
281        let lookup_table = ProgramState::LookupTable(LookupTableMeta::new_for_tests());
282        let meta_size = bincode::serialized_size(&lookup_table).unwrap();
283        assert!(meta_size as usize <= LOOKUP_TABLE_META_SIZE);
284        assert_eq!(meta_size as usize, 56);
285
286        let lookup_table = ProgramState::LookupTable(LookupTableMeta::default());
287        let meta_size = bincode::serialized_size(&lookup_table).unwrap();
288        assert!(meta_size as usize <= LOOKUP_TABLE_META_SIZE);
289        assert_eq!(meta_size as usize, 24);
290    }
291
292    #[test]
293    fn test_lookup_table_meta_status() {
294        let mut slot_hashes = SlotHashes::default();
295        for slot in 1..=MAX_ENTRIES as Slot {
296            slot_hashes.add(slot, Hash::new_unique());
297        }
298
299        let most_recent_slot = slot_hashes.first().unwrap().0;
300        let least_recent_slot = slot_hashes.last().unwrap().0;
301        assert!(least_recent_slot < most_recent_slot);
302
303        // 10 was chosen because the current slot isn't necessarily the next
304        // slot after the most recent block
305        let current_slot = most_recent_slot + 10;
306
307        let active_table = LookupTableMeta {
308            deactivation_slot: Slot::MAX,
309            ..LookupTableMeta::default()
310        };
311
312        let just_started_deactivating_table = LookupTableMeta {
313            deactivation_slot: current_slot,
314            ..LookupTableMeta::default()
315        };
316
317        let recently_started_deactivating_table = LookupTableMeta {
318            deactivation_slot: most_recent_slot,
319            ..LookupTableMeta::default()
320        };
321
322        let almost_deactivated_table = LookupTableMeta {
323            deactivation_slot: least_recent_slot,
324            ..LookupTableMeta::default()
325        };
326
327        let deactivated_table = LookupTableMeta {
328            deactivation_slot: least_recent_slot - 1,
329            ..LookupTableMeta::default()
330        };
331
332        assert_eq!(
333            active_table.status(current_slot, &slot_hashes),
334            LookupTableStatus::Activated
335        );
336        assert_eq!(
337            just_started_deactivating_table.status(current_slot, &slot_hashes),
338            LookupTableStatus::Deactivating {
339                remaining_blocks: MAX_ENTRIES.saturating_add(1),
340            }
341        );
342        assert_eq!(
343            recently_started_deactivating_table.status(current_slot, &slot_hashes),
344            LookupTableStatus::Deactivating {
345                remaining_blocks: MAX_ENTRIES,
346            }
347        );
348        assert_eq!(
349            almost_deactivated_table.status(current_slot, &slot_hashes),
350            LookupTableStatus::Deactivating {
351                remaining_blocks: 1,
352            }
353        );
354        assert_eq!(
355            deactivated_table.status(current_slot, &slot_hashes),
356            LookupTableStatus::Deactivated
357        );
358    }
359
360    #[test]
361    fn test_overwrite_meta_data() {
362        let meta = LookupTableMeta::new_for_tests();
363        let empty_table = ProgramState::LookupTable(meta.clone());
364        let mut serialized_table_1 = bincode::serialize(&empty_table).unwrap();
365        serialized_table_1.resize(LOOKUP_TABLE_META_SIZE, 0);
366
367        let address_table = AddressLookupTable::new_for_tests(meta, 0);
368        let mut serialized_table_2 = vec![0; LOOKUP_TABLE_META_SIZE];
369        AddressLookupTable::overwrite_meta_data(&mut serialized_table_2, address_table.meta)
370            .unwrap();
371
372        assert_eq!(serialized_table_1, serialized_table_2);
373    }
374
375    #[test]
376    fn test_deserialize() {
377        assert_eq!(
378            AddressLookupTable::deserialize(&[]).err(),
379            Some(InstructionError::InvalidAccountData),
380        );
381
382        assert_eq!(
383            AddressLookupTable::deserialize(&[0u8; LOOKUP_TABLE_META_SIZE]).err(),
384            Some(InstructionError::UninitializedAccount),
385        );
386
387        fn test_case(num_addresses: usize) {
388            let lookup_table_meta = LookupTableMeta::new_for_tests();
389            let address_table = AddressLookupTable::new_for_tests(lookup_table_meta, num_addresses);
390            let address_table_data =
391                AddressLookupTable::serialize_for_tests(address_table.clone()).unwrap();
392            assert_eq!(
393                AddressLookupTable::deserialize(&address_table_data).unwrap(),
394                address_table,
395            );
396        }
397
398        for case in [0, 1, 10, 255, 256] {
399            test_case(case);
400        }
401    }
402
403    #[test]
404    fn test_lookup_from_empty_table() {
405        let lookup_table = AddressLookupTable {
406            meta: LookupTableMeta::default(),
407            addresses: Cow::Owned(vec![]),
408        };
409
410        assert_eq!(
411            lookup_table.lookup(0, &[], &SlotHashes::default()),
412            Ok(vec![])
413        );
414        assert_eq!(
415            lookup_table.lookup(0, &[0], &SlotHashes::default()),
416            Err(AddressLookupError::InvalidLookupIndex)
417        );
418    }
419
420    #[test]
421    fn test_lookup_from_deactivating_table() {
422        let current_slot = 1;
423        let slot_hashes = SlotHashes::default();
424        let addresses = vec![Pubkey::new_unique()];
425        let lookup_table = AddressLookupTable {
426            meta: LookupTableMeta {
427                deactivation_slot: current_slot,
428                last_extended_slot: current_slot - 1,
429                ..LookupTableMeta::default()
430            },
431            addresses: Cow::Owned(addresses.clone()),
432        };
433
434        assert_eq!(
435            lookup_table.meta.status(current_slot, &slot_hashes),
436            LookupTableStatus::Deactivating {
437                remaining_blocks: MAX_ENTRIES + 1
438            }
439        );
440
441        assert_eq!(
442            lookup_table.lookup(current_slot, &[0], &slot_hashes),
443            Ok(vec![addresses[0]]),
444        );
445    }
446
447    #[test]
448    fn test_lookup_from_deactivated_table() {
449        let current_slot = 1;
450        let slot_hashes = SlotHashes::default();
451        let lookup_table = AddressLookupTable {
452            meta: LookupTableMeta {
453                deactivation_slot: current_slot - 1,
454                last_extended_slot: current_slot - 1,
455                ..LookupTableMeta::default()
456            },
457            addresses: Cow::Owned(vec![]),
458        };
459
460        assert_eq!(
461            lookup_table.meta.status(current_slot, &slot_hashes),
462            LookupTableStatus::Deactivated
463        );
464        assert_eq!(
465            lookup_table.lookup(current_slot, &[0], &slot_hashes),
466            Err(AddressLookupError::LookupTableAccountNotFound)
467        );
468    }
469
470    #[test]
471    fn test_lookup_from_table_extended_in_current_slot() {
472        let current_slot = 0;
473        let addresses: Vec<_> = (0..2).map(|_| Pubkey::new_unique()).collect();
474        let lookup_table = AddressLookupTable {
475            meta: LookupTableMeta {
476                last_extended_slot: current_slot,
477                last_extended_slot_start_index: 1,
478                ..LookupTableMeta::default()
479            },
480            addresses: Cow::Owned(addresses.clone()),
481        };
482
483        assert_eq!(
484            lookup_table.lookup(current_slot, &[0], &SlotHashes::default()),
485            Ok(vec![addresses[0]])
486        );
487        assert_eq!(
488            lookup_table.lookup(current_slot, &[1], &SlotHashes::default()),
489            Err(AddressLookupError::InvalidLookupIndex),
490        );
491    }
492
493    #[test]
494    fn test_lookup_from_table_extended_in_previous_slot() {
495        let current_slot = 1;
496        let addresses: Vec<_> = (0..10).map(|_| Pubkey::new_unique()).collect();
497        let lookup_table = AddressLookupTable {
498            meta: LookupTableMeta {
499                last_extended_slot: current_slot - 1,
500                last_extended_slot_start_index: 1,
501                ..LookupTableMeta::default()
502            },
503            addresses: Cow::Owned(addresses.clone()),
504        };
505
506        assert_eq!(
507            lookup_table.lookup(current_slot, &[0, 3, 1, 5], &SlotHashes::default()),
508            Ok(vec![addresses[0], addresses[3], addresses[1], addresses[5]])
509        );
510        assert_eq!(
511            lookup_table.lookup(current_slot, &[10], &SlotHashes::default()),
512            Err(AddressLookupError::InvalidLookupIndex),
513        );
514    }
515}