Skip to main content

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