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.addresses[0..active_addresses_len];
209 Ok(indexes
210 .iter()
211 .map(|idx| active_addresses.get(*idx as usize).cloned()))
212 }
213
214 #[cfg(feature = "bincode")]
216 pub fn serialize_for_tests(self) -> Result<Vec<u8>, InstructionError> {
217 let mut data = vec![0; LOOKUP_TABLE_META_SIZE];
218 Self::overwrite_meta_data(&mut data, self.meta)?;
219 self.addresses.iter().for_each(|address| {
220 data.extend_from_slice(address.as_ref());
221 });
222 Ok(data)
223 }
224
225 #[cfg(all(feature = "bincode", feature = "bytemuck"))]
228 pub fn deserialize(data: &'a [u8]) -> Result<AddressLookupTable<'a>, InstructionError> {
229 let program_state: ProgramState =
230 bincode::deserialize(data).map_err(|_| InstructionError::InvalidAccountData)?;
231
232 let meta = match program_state {
233 ProgramState::LookupTable(meta) => Ok(meta),
234 ProgramState::Uninitialized => Err(InstructionError::UninitializedAccount),
235 }?;
236
237 let raw_addresses_data = data.get(LOOKUP_TABLE_META_SIZE..).ok_or({
238 InstructionError::InvalidAccountData
241 })?;
242 let addresses: &[Pubkey] = bytemuck::try_cast_slice(raw_addresses_data).map_err(|_| {
243 InstructionError::InvalidAccountData
246 })?;
247
248 Ok(Self {
249 meta,
250 addresses: Cow::Borrowed(addresses),
251 })
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use {super::*, solana_hash::Hash};
258
259 impl AddressLookupTable<'_> {
260 fn new_for_tests(meta: LookupTableMeta, num_addresses: usize) -> Self {
261 let mut addresses = Vec::with_capacity(num_addresses);
262 addresses.resize_with(num_addresses, Pubkey::new_unique);
263 AddressLookupTable {
264 meta,
265 addresses: Cow::Owned(addresses),
266 }
267 }
268 }
269
270 impl LookupTableMeta {
271 fn new_for_tests() -> Self {
272 Self {
273 authority: Some(Pubkey::new_unique()),
274 ..LookupTableMeta::default()
275 }
276 }
277 }
278
279 #[test]
280 fn test_lookup_table_meta_size() {
281 let lookup_table = ProgramState::LookupTable(LookupTableMeta::new_for_tests());
282 let meta_size = bincode::serialized_size(&lookup_table).unwrap();
283 assert!(meta_size as usize <= LOOKUP_TABLE_META_SIZE);
284 assert_eq!(meta_size as usize, 56);
285
286 let lookup_table = ProgramState::LookupTable(LookupTableMeta::default());
287 let meta_size = bincode::serialized_size(&lookup_table).unwrap();
288 assert!(meta_size as usize <= LOOKUP_TABLE_META_SIZE);
289 assert_eq!(meta_size as usize, 24);
290 }
291
292 #[test]
293 fn test_lookup_table_meta_status() {
294 let mut slot_hashes = SlotHashes::default();
295 for slot in 1..=MAX_ENTRIES as Slot {
296 slot_hashes.add(slot, Hash::new_unique());
297 }
298
299 let most_recent_slot = slot_hashes.first().unwrap().0;
300 let least_recent_slot = slot_hashes.last().unwrap().0;
301 assert!(least_recent_slot < most_recent_slot);
302
303 let current_slot = most_recent_slot + 10;
306
307 let active_table = LookupTableMeta {
308 deactivation_slot: Slot::MAX,
309 ..LookupTableMeta::default()
310 };
311
312 let just_started_deactivating_table = LookupTableMeta {
313 deactivation_slot: current_slot,
314 ..LookupTableMeta::default()
315 };
316
317 let recently_started_deactivating_table = LookupTableMeta {
318 deactivation_slot: most_recent_slot,
319 ..LookupTableMeta::default()
320 };
321
322 let almost_deactivated_table = LookupTableMeta {
323 deactivation_slot: least_recent_slot,
324 ..LookupTableMeta::default()
325 };
326
327 let deactivated_table = LookupTableMeta {
328 deactivation_slot: least_recent_slot - 1,
329 ..LookupTableMeta::default()
330 };
331
332 assert_eq!(
333 active_table.status(current_slot, &slot_hashes),
334 LookupTableStatus::Activated
335 );
336 assert_eq!(
337 just_started_deactivating_table.status(current_slot, &slot_hashes),
338 LookupTableStatus::Deactivating {
339 remaining_blocks: MAX_ENTRIES.saturating_add(1),
340 }
341 );
342 assert_eq!(
343 recently_started_deactivating_table.status(current_slot, &slot_hashes),
344 LookupTableStatus::Deactivating {
345 remaining_blocks: MAX_ENTRIES,
346 }
347 );
348 assert_eq!(
349 almost_deactivated_table.status(current_slot, &slot_hashes),
350 LookupTableStatus::Deactivating {
351 remaining_blocks: 1,
352 }
353 );
354 assert_eq!(
355 deactivated_table.status(current_slot, &slot_hashes),
356 LookupTableStatus::Deactivated
357 );
358 }
359
360 #[test]
361 fn test_overwrite_meta_data() {
362 let meta = LookupTableMeta::new_for_tests();
363 let empty_table = ProgramState::LookupTable(meta.clone());
364 let mut serialized_table_1 = bincode::serialize(&empty_table).unwrap();
365 serialized_table_1.resize(LOOKUP_TABLE_META_SIZE, 0);
366
367 let address_table = AddressLookupTable::new_for_tests(meta, 0);
368 let mut serialized_table_2 = vec![0; LOOKUP_TABLE_META_SIZE];
369 AddressLookupTable::overwrite_meta_data(&mut serialized_table_2, address_table.meta)
370 .unwrap();
371
372 assert_eq!(serialized_table_1, serialized_table_2);
373 }
374
375 #[test]
376 fn test_deserialize() {
377 assert_eq!(
378 AddressLookupTable::deserialize(&[]).err(),
379 Some(InstructionError::InvalidAccountData),
380 );
381
382 assert_eq!(
383 AddressLookupTable::deserialize(&[0u8; LOOKUP_TABLE_META_SIZE]).err(),
384 Some(InstructionError::UninitializedAccount),
385 );
386
387 fn test_case(num_addresses: usize) {
388 let lookup_table_meta = LookupTableMeta::new_for_tests();
389 let address_table = AddressLookupTable::new_for_tests(lookup_table_meta, num_addresses);
390 let address_table_data =
391 AddressLookupTable::serialize_for_tests(address_table.clone()).unwrap();
392 assert_eq!(
393 AddressLookupTable::deserialize(&address_table_data).unwrap(),
394 address_table,
395 );
396 }
397
398 for case in [0, 1, 10, 255, 256] {
399 test_case(case);
400 }
401 }
402
403 #[test]
404 fn test_lookup_from_empty_table() {
405 let lookup_table = AddressLookupTable {
406 meta: LookupTableMeta::default(),
407 addresses: Cow::Owned(vec![]),
408 };
409
410 assert_eq!(
411 lookup_table.lookup(0, &[], &SlotHashes::default()),
412 Ok(vec![])
413 );
414 assert_eq!(
415 lookup_table.lookup(0, &[0], &SlotHashes::default()),
416 Err(AddressLookupError::InvalidLookupIndex)
417 );
418 }
419
420 #[test]
421 fn test_lookup_from_deactivating_table() {
422 let current_slot = 1;
423 let slot_hashes = SlotHashes::default();
424 let addresses = vec![Pubkey::new_unique()];
425 let lookup_table = AddressLookupTable {
426 meta: LookupTableMeta {
427 deactivation_slot: current_slot,
428 last_extended_slot: current_slot - 1,
429 ..LookupTableMeta::default()
430 },
431 addresses: Cow::Owned(addresses.clone()),
432 };
433
434 assert_eq!(
435 lookup_table.meta.status(current_slot, &slot_hashes),
436 LookupTableStatus::Deactivating {
437 remaining_blocks: MAX_ENTRIES + 1
438 }
439 );
440
441 assert_eq!(
442 lookup_table.lookup(current_slot, &[0], &slot_hashes),
443 Ok(vec![addresses[0]]),
444 );
445 }
446
447 #[test]
448 fn test_lookup_from_deactivated_table() {
449 let current_slot = 1;
450 let slot_hashes = SlotHashes::default();
451 let lookup_table = AddressLookupTable {
452 meta: LookupTableMeta {
453 deactivation_slot: current_slot - 1,
454 last_extended_slot: current_slot - 1,
455 ..LookupTableMeta::default()
456 },
457 addresses: Cow::Owned(vec![]),
458 };
459
460 assert_eq!(
461 lookup_table.meta.status(current_slot, &slot_hashes),
462 LookupTableStatus::Deactivated
463 );
464 assert_eq!(
465 lookup_table.lookup(current_slot, &[0], &slot_hashes),
466 Err(AddressLookupError::LookupTableAccountNotFound)
467 );
468 }
469
470 #[test]
471 fn test_lookup_from_table_extended_in_current_slot() {
472 let current_slot = 0;
473 let addresses: Vec<_> = (0..2).map(|_| Pubkey::new_unique()).collect();
474 let lookup_table = AddressLookupTable {
475 meta: LookupTableMeta {
476 last_extended_slot: current_slot,
477 last_extended_slot_start_index: 1,
478 ..LookupTableMeta::default()
479 },
480 addresses: Cow::Owned(addresses.clone()),
481 };
482
483 assert_eq!(
484 lookup_table.lookup(current_slot, &[0], &SlotHashes::default()),
485 Ok(vec![addresses[0]])
486 );
487 assert_eq!(
488 lookup_table.lookup(current_slot, &[1], &SlotHashes::default()),
489 Err(AddressLookupError::InvalidLookupIndex),
490 );
491 }
492
493 #[test]
494 fn test_lookup_from_table_extended_in_previous_slot() {
495 let current_slot = 1;
496 let addresses: Vec<_> = (0..10).map(|_| Pubkey::new_unique()).collect();
497 let lookup_table = AddressLookupTable {
498 meta: LookupTableMeta {
499 last_extended_slot: current_slot - 1,
500 last_extended_slot_start_index: 1,
501 ..LookupTableMeta::default()
502 },
503 addresses: Cow::Owned(addresses.clone()),
504 };
505
506 assert_eq!(
507 lookup_table.lookup(current_slot, &[0, 3, 1, 5], &SlotHashes::default()),
508 Ok(vec![addresses[0], addresses[3], addresses[1], addresses[5]])
509 );
510 assert_eq!(
511 lookup_table.lookup(current_slot, &[10], &SlotHashes::default()),
512 Err(AddressLookupError::InvalidLookupIndex),
513 );
514 }
515}