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
209            .addresses
210            .get(0..active_addresses_len)
211            .ok_or(AddressLookupError::InvalidAccountData)?;
212        Ok(indexes
213            .iter()
214            .map(|idx| active_addresses.get(*idx as usize).cloned()))
215    }
216
217    /// Serialize an address table including its addresses
218    #[cfg(feature = "bincode")]
219    pub fn serialize_for_tests(self) -> Result<Vec<u8>, InstructionError> {
220        let mut data = vec![0; LOOKUP_TABLE_META_SIZE];
221        Self::overwrite_meta_data(&mut data, self.meta)?;
222        self.addresses.iter().for_each(|address| {
223            data.extend_from_slice(address.as_ref());
224        });
225        Ok(data)
226    }
227
228    /// Efficiently deserialize an address table without allocating
229    /// for stored addresses.
230    #[cfg(all(feature = "bincode", feature = "bytemuck"))]
231    pub fn deserialize(data: &'a [u8]) -> Result<AddressLookupTable<'a>, InstructionError> {
232        let program_state: ProgramState =
233            bincode::deserialize(data).map_err(|_| InstructionError::InvalidAccountData)?;
234
235        let meta = match program_state {
236            ProgramState::LookupTable(meta) => Ok(meta),
237            ProgramState::Uninitialized => Err(InstructionError::UninitializedAccount),
238        }?;
239
240        let raw_addresses_data = data.get(LOOKUP_TABLE_META_SIZE..).ok_or({
241            // Should be impossible because table accounts must
242            // always be LOOKUP_TABLE_META_SIZE in length
243            InstructionError::InvalidAccountData
244        })?;
245        let addresses: &[Pubkey] = bytemuck::try_cast_slice(raw_addresses_data).map_err(|_| {
246            // Should be impossible because raw address data
247            // should be aligned and sized in multiples of 32 bytes
248            InstructionError::InvalidAccountData
249        })?;
250
251        Ok(Self {
252            meta,
253            addresses: Cow::Borrowed(addresses),
254        })
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use {super::*, solana_hash::Hash};
261
262    impl AddressLookupTable<'_> {
263        fn new_for_tests(meta: LookupTableMeta, num_addresses: usize) -> Self {
264            let mut addresses = Vec::with_capacity(num_addresses);
265            addresses.resize_with(num_addresses, Pubkey::new_unique);
266            AddressLookupTable {
267                meta,
268                addresses: Cow::Owned(addresses),
269            }
270        }
271    }
272
273    impl LookupTableMeta {
274        fn new_for_tests() -> Self {
275            Self {
276                authority: Some(Pubkey::new_unique()),
277                ..LookupTableMeta::default()
278            }
279        }
280    }
281
282    #[test]
283    fn test_lookup_table_meta_size() {
284        let lookup_table = ProgramState::LookupTable(LookupTableMeta::new_for_tests());
285        let meta_size = bincode::serialized_size(&lookup_table).unwrap();
286        assert!(meta_size as usize <= LOOKUP_TABLE_META_SIZE);
287        assert_eq!(meta_size as usize, 56);
288
289        let lookup_table = ProgramState::LookupTable(LookupTableMeta::default());
290        let meta_size = bincode::serialized_size(&lookup_table).unwrap();
291        assert!(meta_size as usize <= LOOKUP_TABLE_META_SIZE);
292        assert_eq!(meta_size as usize, 24);
293    }
294
295    #[test]
296    fn test_lookup_table_meta_status() {
297        let mut slot_hashes = SlotHashes::default();
298        for slot in 1..=MAX_ENTRIES as Slot {
299            slot_hashes.add(slot, Hash::new_unique());
300        }
301
302        let most_recent_slot = slot_hashes.first().unwrap().0;
303        let least_recent_slot = slot_hashes.last().unwrap().0;
304        assert!(least_recent_slot < most_recent_slot);
305
306        // 10 was chosen because the current slot isn't necessarily the next
307        // slot after the most recent block
308        let current_slot = most_recent_slot + 10;
309
310        let active_table = LookupTableMeta {
311            deactivation_slot: Slot::MAX,
312            ..LookupTableMeta::default()
313        };
314
315        let just_started_deactivating_table = LookupTableMeta {
316            deactivation_slot: current_slot,
317            ..LookupTableMeta::default()
318        };
319
320        let recently_started_deactivating_table = LookupTableMeta {
321            deactivation_slot: most_recent_slot,
322            ..LookupTableMeta::default()
323        };
324
325        let almost_deactivated_table = LookupTableMeta {
326            deactivation_slot: least_recent_slot,
327            ..LookupTableMeta::default()
328        };
329
330        let deactivated_table = LookupTableMeta {
331            deactivation_slot: least_recent_slot - 1,
332            ..LookupTableMeta::default()
333        };
334
335        assert_eq!(
336            active_table.status(current_slot, &slot_hashes),
337            LookupTableStatus::Activated
338        );
339        assert_eq!(
340            just_started_deactivating_table.status(current_slot, &slot_hashes),
341            LookupTableStatus::Deactivating {
342                remaining_blocks: MAX_ENTRIES.saturating_add(1),
343            }
344        );
345        assert_eq!(
346            recently_started_deactivating_table.status(current_slot, &slot_hashes),
347            LookupTableStatus::Deactivating {
348                remaining_blocks: MAX_ENTRIES,
349            }
350        );
351        assert_eq!(
352            almost_deactivated_table.status(current_slot, &slot_hashes),
353            LookupTableStatus::Deactivating {
354                remaining_blocks: 1,
355            }
356        );
357        assert_eq!(
358            deactivated_table.status(current_slot, &slot_hashes),
359            LookupTableStatus::Deactivated
360        );
361    }
362
363    #[test]
364    fn test_overwrite_meta_data() {
365        let meta = LookupTableMeta::new_for_tests();
366        let empty_table = ProgramState::LookupTable(meta.clone());
367        let mut serialized_table_1 = bincode::serialize(&empty_table).unwrap();
368        serialized_table_1.resize(LOOKUP_TABLE_META_SIZE, 0);
369
370        let address_table = AddressLookupTable::new_for_tests(meta, 0);
371        let mut serialized_table_2 = vec![0; LOOKUP_TABLE_META_SIZE];
372        AddressLookupTable::overwrite_meta_data(&mut serialized_table_2, address_table.meta)
373            .unwrap();
374
375        assert_eq!(serialized_table_1, serialized_table_2);
376    }
377
378    #[test]
379    fn test_deserialize() {
380        assert_eq!(
381            AddressLookupTable::deserialize(&[]).err(),
382            Some(InstructionError::InvalidAccountData),
383        );
384
385        assert_eq!(
386            AddressLookupTable::deserialize(&[0u8; LOOKUP_TABLE_META_SIZE]).err(),
387            Some(InstructionError::UninitializedAccount),
388        );
389
390        fn test_case(num_addresses: usize) {
391            let lookup_table_meta = LookupTableMeta::new_for_tests();
392            let address_table = AddressLookupTable::new_for_tests(lookup_table_meta, num_addresses);
393            let address_table_data =
394                AddressLookupTable::serialize_for_tests(address_table.clone()).unwrap();
395            assert_eq!(
396                AddressLookupTable::deserialize(&address_table_data).unwrap(),
397                address_table,
398            );
399        }
400
401        for case in [0, 1, 10, 255, 256] {
402            test_case(case);
403        }
404    }
405
406    #[test]
407    fn test_lookup_from_empty_table() {
408        let lookup_table = AddressLookupTable {
409            meta: LookupTableMeta::default(),
410            addresses: Cow::Owned(vec![]),
411        };
412
413        assert_eq!(
414            lookup_table.lookup(0, &[], &SlotHashes::default()),
415            Ok(vec![])
416        );
417        assert_eq!(
418            lookup_table.lookup(0, &[0], &SlotHashes::default()),
419            Err(AddressLookupError::InvalidLookupIndex)
420        );
421    }
422
423    #[test]
424    fn test_lookup_from_deactivating_table() {
425        let current_slot = 1;
426        let slot_hashes = SlotHashes::default();
427        let addresses = vec![Pubkey::new_unique()];
428        let lookup_table = AddressLookupTable {
429            meta: LookupTableMeta {
430                deactivation_slot: current_slot,
431                last_extended_slot: current_slot - 1,
432                ..LookupTableMeta::default()
433            },
434            addresses: Cow::Owned(addresses.clone()),
435        };
436
437        assert_eq!(
438            lookup_table.meta.status(current_slot, &slot_hashes),
439            LookupTableStatus::Deactivating {
440                remaining_blocks: MAX_ENTRIES + 1
441            }
442        );
443
444        assert_eq!(
445            lookup_table.lookup(current_slot, &[0], &slot_hashes),
446            Ok(vec![addresses[0]]),
447        );
448    }
449
450    #[test]
451    fn test_lookup_from_deactivated_table() {
452        let current_slot = 1;
453        let slot_hashes = SlotHashes::default();
454        let lookup_table = AddressLookupTable {
455            meta: LookupTableMeta {
456                deactivation_slot: current_slot - 1,
457                last_extended_slot: current_slot - 1,
458                ..LookupTableMeta::default()
459            },
460            addresses: Cow::Owned(vec![]),
461        };
462
463        assert_eq!(
464            lookup_table.meta.status(current_slot, &slot_hashes),
465            LookupTableStatus::Deactivated
466        );
467        assert_eq!(
468            lookup_table.lookup(current_slot, &[0], &slot_hashes),
469            Err(AddressLookupError::LookupTableAccountNotFound)
470        );
471    }
472
473    #[test]
474    fn test_lookup_from_table_extended_in_current_slot() {
475        let current_slot = 0;
476        let addresses: Vec<_> = (0..2).map(|_| Pubkey::new_unique()).collect();
477        let lookup_table = AddressLookupTable {
478            meta: LookupTableMeta {
479                last_extended_slot: current_slot,
480                last_extended_slot_start_index: 1,
481                ..LookupTableMeta::default()
482            },
483            addresses: Cow::Owned(addresses.clone()),
484        };
485
486        assert_eq!(
487            lookup_table.lookup(current_slot, &[0], &SlotHashes::default()),
488            Ok(vec![addresses[0]])
489        );
490        assert_eq!(
491            lookup_table.lookup(current_slot, &[1], &SlotHashes::default()),
492            Err(AddressLookupError::InvalidLookupIndex),
493        );
494    }
495
496    #[test]
497    fn test_lookup_from_table_extended_in_previous_slot() {
498        let current_slot = 1;
499        let addresses: Vec<_> = (0..10).map(|_| Pubkey::new_unique()).collect();
500        let lookup_table = AddressLookupTable {
501            meta: LookupTableMeta {
502                last_extended_slot: current_slot - 1,
503                last_extended_slot_start_index: 1,
504                ..LookupTableMeta::default()
505            },
506            addresses: Cow::Owned(addresses.clone()),
507        };
508
509        assert_eq!(
510            lookup_table.lookup(current_slot, &[0, 3, 1, 5], &SlotHashes::default()),
511            Ok(vec![addresses[0], addresses[3], addresses[1], addresses[5]])
512        );
513        assert_eq!(
514            lookup_table.lookup(current_slot, &[10], &SlotHashes::default()),
515            Err(AddressLookupError::InvalidLookupIndex),
516        );
517    }
518
519    #[test]
520    fn test_lookup_from_table_with_invalid_meta() {
521        let current_slot = 10;
522        let addresses: Vec<_> = (0..5).map(|_| Pubkey::new_unique()).collect();
523        let lookup_table = AddressLookupTable {
524            meta: LookupTableMeta {
525                last_extended_slot: current_slot,
526                last_extended_slot_start_index: 10, // larger than 5 = impossible
527                ..LookupTableMeta::default()
528            },
529            addresses: Cow::Owned(addresses.clone()),
530        };
531
532        assert_eq!(
533            lookup_table.lookup(current_slot, &[0], &SlotHashes::default()),
534            Err(AddressLookupError::InvalidAccountData),
535        );
536    }
537}