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#[inline]
24pub fn estimate_last_valid_slot(deactivation_slot: Slot) -> Slot {
25 deactivation_slot.saturating_add(get_entries() as Slot)
26}
27
28pub const LOOKUP_TABLE_MAX_ADDRESSES: usize = 256;
30
31pub const LOOKUP_TABLE_META_SIZE: usize = 56;
33
34#[derive(Debug, PartialEq, Eq, Clone)]
36pub enum LookupTableStatus {
37 Activated,
38 Deactivating { remaining_blocks: usize },
39 Deactivated,
40}
41
42#[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 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#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
124#[derive(Debug, PartialEq, Eq, Clone)]
125#[allow(clippy::large_enum_variant)]
126pub enum ProgramState {
127 Uninitialized,
129 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 #[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 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 return Err(AddressLookupError::LookupTableAccountNotFound);
168 }
169
170 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 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 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 #[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 #[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 InstructionError::InvalidAccountData
244 })?;
245 let addresses: &[Pubkey] = bytemuck::try_cast_slice(raw_addresses_data).map_err(|_| {
246 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 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, ..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}