miraland_program/address_lookup_table/
state.rs

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