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#[inline]
25pub fn estimate_last_valid_slot(deactivation_slot: Slot) -> Slot {
26 deactivation_slot.saturating_add(get_entries() as Slot)
27}
28
29pub const LOOKUP_TABLE_MAX_ADDRESSES: usize = 256;
31
32pub const LOOKUP_TABLE_META_SIZE: usize = 56;
34
35#[derive(Debug, PartialEq, Eq, Clone)]
37pub enum LookupTableStatus {
38 Activated,
39 Deactivating { remaining_blocks: usize },
40 Deactivated,
41}
42
43#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
45#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
46pub struct LookupTableMeta {
47 pub deactivation_slot: Slot,
50 pub last_extended_slot: Slot,
54 pub last_extended_slot_start_index: u8,
57 pub authority: Option<Pubkey>,
59 pub _padding: u16,
61 }
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 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 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 LookupTableStatus::Deactivating {
113 remaining_blocks: MAX_ENTRIES.saturating_sub(slot_hash_position),
114 }
115 } else {
116 LookupTableStatus::Deactivated
117 }
118 }
119}
120
121#[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 Uninitialized,
128 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 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 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 return Err(AddressLookupError::LookupTableAccountNotFound);
166 }
167
168 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 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 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 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 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 InstructionError::InvalidAccountData
237 })?;
238 let addresses: &[Pubkey] = bytemuck::try_cast_slice(raw_addresses_data).map_err(|_| {
239 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 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}