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#[inline]
29pub fn estimate_last_valid_slot(deactivation_slot: Slot) -> Slot {
30 deactivation_slot.saturating_add(get_entries() as Slot)
31}
32
33pub const LOOKUP_TABLE_MAX_ADDRESSES: usize = 256;
35
36pub const LOOKUP_TABLE_META_SIZE: usize = 56;
38
39#[derive(Debug, PartialEq, Eq, Clone)]
41pub enum LookupTableStatus {
42 Activated,
43 Deactivating { remaining_blocks: usize },
44 Deactivated,
45}
46
47#[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 pub deactivation_slot: Slot,
56 pub last_extended_slot: Slot,
60 pub last_extended_slot_start_index: u8,
63 pub authority: Option<Pubkey>,
65 pub _padding: u16,
67 }
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 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 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 LookupTableStatus::Deactivating {
119 remaining_blocks: MAX_ENTRIES.saturating_sub(slot_hash_position),
120 }
121 } else {
122 LookupTableStatus::Deactivated
123 }
124 }
125}
126
127#[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 Uninitialized,
136 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 #[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 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 return Err(AddressLookupError::LookupTableAccountNotFound);
179 }
180
181 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 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 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 #[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 #[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 InstructionError::InvalidAccountData
259 })?;
260 let addresses: &[Pubkey] = bytemuck::try_cast_slice(raw_addresses_data).map_err(|_| {
261 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 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, ..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}