solana_program/address_lookup_table/
state.rs

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