use { crate::invoke_context::InvokeContext, solana_rbpf::{ elf::Executable, error::EbpfError, verifier::RequisiteVerifier, vm::{BuiltInProgram, VerifiedExecutable}, }, solana_sdk::{ bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, clock::Slot, pubkey::Pubkey, }, std::{ collections::HashMap, fmt::{Debug, Formatter}, sync::{atomic::AtomicU64, Arc}, }, }; /// Relationship between two fork IDs #[derive(Copy, Clone, PartialEq)] pub enum BlockRelation { /// The slot is on the same fork and is an ancestor of the other slot Ancestor, /// The two slots are equal and are on the same fork Equal, /// The slot is on the same fork and is a descendant of the other slot Descendant, /// The slots are on two different forks and may have had a common ancestor at some point Unrelated, /// Either one or both of the slots are either older than the latest root, or are in future Unknown, } /// Maps relationship between two slots. pub trait ForkGraph { /// Returns the BlockRelation of A to B fn relationship(&self, a: Slot, b: Slot) -> BlockRelation; } /// Provides information about current working slot, and its ancestors pub trait WorkingSlot { /// Returns the current slot value fn current_slot(&self) -> Slot; /// Returns true if the `other` slot is an ancestor of self, false otherwise fn is_ancestor(&self, other: Slot) -> bool; } pub enum LoadedProgramType { /// Tombstone for undeployed, closed or unloadable programs Invalid, LegacyV0(VerifiedExecutable>), LegacyV1(VerifiedExecutable>), // Typed(TypedProgram>), BuiltIn(BuiltInProgram>), } impl Debug for LoadedProgramType { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { LoadedProgramType::Invalid => write!(f, "LoadedProgramType::Invalid"), LoadedProgramType::LegacyV0(_) => write!(f, "LoadedProgramType::LegacyV0"), LoadedProgramType::LegacyV1(_) => write!(f, "LoadedProgramType::LegacyV1"), LoadedProgramType::BuiltIn(_) => write!(f, "LoadedProgramType::BuiltIn"), } } } #[derive(Debug)] pub struct LoadedProgram { /// The program of this entry pub program: LoadedProgramType, /// Size of account that stores the program and program data pub account_size: usize, /// Slot in which the program was (re)deployed pub deployment_slot: Slot, /// Slot in which this entry will become active (can be in the future) pub effective_slot: Slot, /// How often this entry was used pub usage_counter: AtomicU64, } impl LoadedProgram { /// Creates a new user program pub fn new( loader_key: &Pubkey, loader: Arc>>, deployment_slot: Slot, elf_bytes: &[u8], account_size: usize, ) -> Result { let program = if bpf_loader_deprecated::check_id(loader_key) { let executable = Executable::load(elf_bytes, loader.clone())?; LoadedProgramType::LegacyV0(VerifiedExecutable::from_executable(executable)?) } else if bpf_loader::check_id(loader_key) || bpf_loader_upgradeable::check_id(loader_key) { let executable = Executable::load(elf_bytes, loader.clone())?; LoadedProgramType::LegacyV1(VerifiedExecutable::from_executable(executable)?) } else { panic!(); }; Ok(Self { deployment_slot, account_size, effective_slot: deployment_slot.saturating_add(1), usage_counter: AtomicU64::new(0), program, }) } /// Creates a new built-in program pub fn new_built_in( deployment_slot: Slot, program: BuiltInProgram>, ) -> Self { Self { deployment_slot, account_size: 0, effective_slot: deployment_slot.saturating_add(1), usage_counter: AtomicU64::new(0), program: LoadedProgramType::BuiltIn(program), } } } #[derive(Debug, Default)] pub struct LoadedPrograms { /// A two level index: /// /// Pubkey is the address of a program, multiple versions can coexists simultaneously under the same address (in different slots). entries: HashMap>>, } #[cfg(RUSTC_WITH_SPECIALIZATION)] impl solana_frozen_abi::abi_example::AbiExample for LoadedPrograms { fn example() -> Self { // Delegate AbiExample impl to Default before going deep and stuck with // not easily impl-able Arc due to rust's coherence issue // This is safe because LoadedPrograms isn't serializable by definition. Self::default() } } impl LoadedPrograms { /// Inserts a single entry pub fn insert_entry(&mut self, key: Pubkey, entry: LoadedProgram) -> bool { let second_level = self.entries.entry(key).or_insert_with(Vec::new); let index = second_level .iter() .position(|at| at.effective_slot >= entry.effective_slot); if let Some(index) = index { let existing = second_level .get(index) .expect("Missing entry, even though position was found"); if existing.deployment_slot == entry.deployment_slot && existing.effective_slot == entry.effective_slot { return false; } } second_level.insert(index.unwrap_or(second_level.len()), Arc::new(entry)); true } /// Before rerooting the blockstore this removes all programs of orphan forks pub fn prune(&mut self, fork_graph: &F, new_root: Slot) { self.entries.retain(|_key, second_level| { let mut first_ancestor = true; *second_level = second_level .iter() .rev() .filter(|entry| { let relation = fork_graph.relationship(entry.deployment_slot, new_root); if entry.deployment_slot >= new_root { matches!(relation, BlockRelation::Equal | BlockRelation::Descendant) } else if first_ancestor { first_ancestor = false; matches!(relation, BlockRelation::Ancestor) } else { false } }) .cloned() .collect(); second_level.reverse(); !second_level.is_empty() }); } /// Extracts a subset of the programs relevant to a transaction batch /// and returns which program accounts the accounts DB needs to load. pub fn extract( &self, working_slot: &S, keys: impl Iterator, ) -> (HashMap>, Vec) { let mut missing = Vec::new(); let found = keys .filter_map(|key| { if let Some(second_level) = self.entries.get(&key) { for entry in second_level.iter().rev() { if working_slot.current_slot() >= entry.effective_slot && working_slot.is_ancestor(entry.deployment_slot) { return Some((key, entry.clone())); } } } missing.push(key); None }) .collect(); (found, missing) } /// Evicts programs which were used infrequently pub fn sort_and_evict(&mut self) { // TODO: Sort programs by their usage_counter // TODO: Truncate the end of the list } /// Removes the entries at the given keys, if they exist pub fn remove_entries(&mut self, _key: impl Iterator) { // TODO: Remove at primary index level } } #[cfg(test)] mod tests { use { crate::loaded_programs::{ BlockRelation, ForkGraph, LoadedProgram, LoadedProgramType, LoadedPrograms, WorkingSlot, }, solana_sdk::{clock::Slot, pubkey::Pubkey}, std::{ collections::HashMap, ops::ControlFlow, sync::{atomic::AtomicU64, Arc}, }, }; struct TestForkGraph { relation: BlockRelation, } impl ForkGraph for TestForkGraph { fn relationship(&self, _a: Slot, _b: Slot) -> BlockRelation { self.relation } } #[test] fn test_prune_empty() { let mut cache = LoadedPrograms::default(); let fork_graph = TestForkGraph { relation: BlockRelation::Unrelated, }; cache.prune(&fork_graph, 0); assert!(cache.entries.is_empty()); cache.prune(&fork_graph, 10); assert!(cache.entries.is_empty()); let fork_graph = TestForkGraph { relation: BlockRelation::Ancestor, }; cache.prune(&fork_graph, 0); assert!(cache.entries.is_empty()); cache.prune(&fork_graph, 10); assert!(cache.entries.is_empty()); let fork_graph = TestForkGraph { relation: BlockRelation::Descendant, }; cache.prune(&fork_graph, 0); assert!(cache.entries.is_empty()); cache.prune(&fork_graph, 10); assert!(cache.entries.is_empty()); let fork_graph = TestForkGraph { relation: BlockRelation::Unknown, }; cache.prune(&fork_graph, 0); assert!(cache.entries.is_empty()); cache.prune(&fork_graph, 10); assert!(cache.entries.is_empty()); } #[derive(Default)] struct TestForkGraphSpecific { forks: Vec>, } impl TestForkGraphSpecific { fn insert_fork(&mut self, fork: &[Slot]) { let mut fork = fork.to_vec(); fork.sort(); self.forks.push(fork) } } impl ForkGraph for TestForkGraphSpecific { fn relationship(&self, a: Slot, b: Slot) -> BlockRelation { match self.forks.iter().try_for_each(|fork| { let relation = fork .iter() .position(|x| *x == a) .and_then(|a_pos| { fork.iter().position(|x| *x == b).and_then(|b_pos| { (a_pos == b_pos) .then_some(BlockRelation::Equal) .or_else(|| (a_pos < b_pos).then_some(BlockRelation::Ancestor)) .or(Some(BlockRelation::Descendant)) }) }) .unwrap_or(BlockRelation::Unrelated); if relation != BlockRelation::Unrelated { return ControlFlow::Break(relation); } ControlFlow::Continue(()) }) { ControlFlow::Break(relation) => relation, _ => BlockRelation::Unrelated, } } } struct TestWorkingSlot { slot: Slot, fork: Vec, slot_pos: usize, } impl TestWorkingSlot { fn new(slot: Slot, fork: &[Slot]) -> Self { let mut fork = fork.to_vec(); fork.sort(); let slot_pos = fork .iter() .position(|current| *current == slot) .expect("The fork didn't have the slot in it"); TestWorkingSlot { slot, fork, slot_pos, } } fn update_slot(&mut self, slot: Slot) { self.slot = slot; self.slot_pos = self .fork .iter() .position(|current| *current == slot) .expect("The fork didn't have the slot in it"); } } impl WorkingSlot for TestWorkingSlot { fn current_slot(&self) -> Slot { self.slot } fn is_ancestor(&self, other: Slot) -> bool { self.fork .iter() .position(|current| *current == other) .map(|other_pos| other_pos < self.slot_pos) .unwrap_or(false) } } fn new_test_loaded_program(deployment_slot: Slot, effective_slot: Slot) -> Arc { Arc::new(LoadedProgram { program: LoadedProgramType::Invalid, account_size: 0, deployment_slot, effective_slot, usage_counter: AtomicU64::default(), }) } fn match_slot( table: &HashMap>, program: &Pubkey, deployment_slot: Slot, ) -> bool { table .get(program) .map(|entry| entry.deployment_slot == deployment_slot) .unwrap_or(false) } #[test] fn test_fork_extract_and_prune() { let mut cache = LoadedPrograms::default(); // Fork graph created for the test // 0 // / \ // 10 5 // | | // 20 11 // | | \ // 22 15 25 // | | // 16 27 // | // 19 // | // 23 let mut fork_graph = TestForkGraphSpecific::default(); fork_graph.insert_fork(&[0, 10, 20, 22]); fork_graph.insert_fork(&[0, 5, 11, 15, 16]); fork_graph.insert_fork(&[0, 5, 11, 25, 27]); let program1 = Pubkey::new_unique(); cache.entries.insert( program1, vec![ new_test_loaded_program(0, 1), new_test_loaded_program(10, 11), new_test_loaded_program(20, 21), ], ); let program2 = Pubkey::new_unique(); cache.entries.insert( program2, vec![ new_test_loaded_program(5, 6), new_test_loaded_program(11, 12), ], ); let program3 = Pubkey::new_unique(); cache .entries .insert(program3, vec![new_test_loaded_program(25, 26)]); let program4 = Pubkey::new_unique(); cache.entries.insert( program4, vec![ new_test_loaded_program(0, 1), new_test_loaded_program(5, 6), // The following is a special case, where effective slot is 4 slots in the future new_test_loaded_program(15, 19), ], ); // Current fork graph // 0 // / \ // 10 5 // | | // 20 11 // | | \ // 22 15 25 // | | // 16 27 // | // 19 // | // 23 // Testing fork 0 - 10 - 12 - 22 with current slot at 22 let working_slot = TestWorkingSlot::new(22, &[0, 10, 20, 22]); let (found, missing) = cache.extract( &working_slot, vec![program1, program2, program3, program4].into_iter(), ); assert!(match_slot(&found, &program1, 20)); assert!(match_slot(&found, &program4, 0)); assert!(missing.contains(&program2)); assert!(missing.contains(&program3)); // Testing fork 0 - 5 - 11 - 15 - 16 with current slot at 16 let mut working_slot = TestWorkingSlot::new(16, &[0, 5, 11, 15, 16, 19, 23]); let (found, missing) = cache.extract( &working_slot, vec![program1, program2, program3, program4].into_iter(), ); assert!(match_slot(&found, &program1, 0)); assert!(match_slot(&found, &program2, 11)); // The effective slot of program4 deployed in slot 15 is 19. So it should not be usable in slot 16. assert!(match_slot(&found, &program4, 5)); assert!(missing.contains(&program3)); // Testing the same fork above, but current slot is now 19 (equal to effective slot of program4). working_slot.update_slot(19); let (found, missing) = cache.extract( &working_slot, vec![program1, program2, program3, program4].into_iter(), ); assert!(match_slot(&found, &program1, 0)); assert!(match_slot(&found, &program2, 11)); // The effective slot of program4 deployed in slot 15 is 19. So it should be usable in slot 19. assert!(match_slot(&found, &program4, 15)); assert!(missing.contains(&program3)); // Testing the same fork above, but current slot is now 23 (future slot than effective slot of program4). working_slot.update_slot(23); let (found, missing) = cache.extract( &working_slot, vec![program1, program2, program3, program4].into_iter(), ); assert!(match_slot(&found, &program1, 0)); assert!(match_slot(&found, &program2, 11)); // The effective slot of program4 deployed in slot 15 is 19. So it should be usable in slot 23. assert!(match_slot(&found, &program4, 15)); assert!(missing.contains(&program3)); // Testing fork 0 - 5 - 11 - 15 - 16 with current slot at 11 let working_slot = TestWorkingSlot::new(11, &[0, 5, 11, 15, 16]); let (found, missing) = cache.extract( &working_slot, vec![program1, program2, program3, program4].into_iter(), ); assert!(match_slot(&found, &program1, 0)); assert!(match_slot(&found, &program2, 5)); assert!(match_slot(&found, &program4, 5)); assert!(missing.contains(&program3)); cache.prune(&fork_graph, 5); // Fork graph after pruning // 0 // | // 5 // | // 11 // | \ // 15 25 // | | // 16 27 // | // 19 // | // 23 // Testing fork 0 - 10 - 12 - 22 (which was pruned) with current slot at 22 let working_slot = TestWorkingSlot::new(22, &[0, 10, 20, 22]); let (found, missing) = cache.extract( &working_slot, vec![program1, program2, program3, program4].into_iter(), ); // Since the fork was pruned, we should not find the entry deployed at slot 20. assert!(match_slot(&found, &program1, 0)); assert!(match_slot(&found, &program4, 0)); assert!(missing.contains(&program2)); assert!(missing.contains(&program3)); // Testing fork 0 - 5 - 11 - 25 - 27 with current slot at 27 let working_slot = TestWorkingSlot::new(27, &[0, 5, 11, 25, 27]); let (found, _missing) = cache.extract( &working_slot, vec![program1, program2, program3, program4].into_iter(), ); assert!(match_slot(&found, &program1, 0)); assert!(match_slot(&found, &program2, 11)); assert!(match_slot(&found, &program3, 25)); assert!(match_slot(&found, &program4, 5)); cache.prune(&fork_graph, 15); // Fork graph after pruning // 0 // | // 5 // | // 11 // | // 15 // | // 16 // | // 19 // | // 23 // Testing fork 0 - 5 - 11 - 25 - 27 (with root at 15, slot 25, 27 are pruned) with current slot at 27 let working_slot = TestWorkingSlot::new(27, &[0, 5, 11, 25, 27]); let (found, missing) = cache.extract( &working_slot, vec![program1, program2, program3, program4].into_iter(), ); assert!(match_slot(&found, &program1, 0)); assert!(match_slot(&found, &program2, 11)); assert!(match_slot(&found, &program4, 5)); // program3 was deployed on slot 25, which has been pruned assert!(missing.contains(&program3)); } }