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
14pub const LOOKUP_TABLE_MAX_ADDRESSES: usize = 256;
16
17pub const LOOKUP_TABLE_META_SIZE: usize = 56;
19
20#[derive(Debug, PartialEq, Eq, Clone)]
22pub enum LookupTableStatus {
23 Activated,
24 Deactivating { remaining_blocks: usize },
25 Deactivated,
26}
27
28#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, AbiExample)]
30pub struct LookupTableMeta {
31 pub deactivation_slot: Slot,
34 pub last_extended_slot: Slot,
38 pub last_extended_slot_start_index: u8,
41 pub authority: Option<Pubkey>,
43 pub _padding: u16,
45 }
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 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 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 LookupTableStatus::Deactivating {
97 remaining_blocks: MAX_ENTRIES.saturating_sub(slot_hash_position),
98 }
99 } else {
100 LookupTableStatus::Deactivated
101 }
102 }
103}
104
105#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, AbiExample, AbiEnumVisitor)]
107#[allow(clippy::large_enum_variant)]
108pub enum ProgramState {
109 Uninitialized,
111 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 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 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 return Err(AddressLookupError::LookupTableAccountNotFound);
148 }
149
150 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 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 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 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 InstructionError::InvalidAccountData
205 })?;
206 let addresses: &[Pubkey] = bytemuck::try_cast_slice(raw_addresses_data).map_err(|_| {
207 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 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}