diff --git a/Cargo.lock b/Cargo.lock index 4001a181317579..76c2fbd220e60d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4386,6 +4386,33 @@ dependencies = [ "tokio-postgres", ] +[[package]] +name = "solana-address-map-program" +version = "1.9.0" +dependencies = [ + "bincode", + "log 0.4.14", + "num-derive", + "num-traits", + "rustc_version 0.4.0", + "serde", + "solana-frozen-abi 1.9.0", + "solana-frozen-abi-macro 1.9.0", + "solana-program-runtime", + "solana-sdk", + "thiserror", +] + +[[package]] +name = "solana-address-map-program-tests" +version = "1.9.0" +dependencies = [ + "bincode", + "solana-address-map-program", + "solana-program-test", + "solana-sdk", +] + [[package]] name = "solana-banking-bench" version = "1.9.0" @@ -5657,6 +5684,7 @@ dependencies = [ "rustc_version 0.4.0", "serde", "serde_derive", + "solana-address-map-program", "solana-bucket-map", "solana-compute-budget-program", "solana-config-program", diff --git a/Cargo.toml b/Cargo.toml index 7f6df6b6941efb..ce4bc4ae24c8df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,8 @@ members = [ "poh", "poh-bench", "program-test", + "programs/address-map", + "programs/address-map-tests", "programs/bpf_loader", "programs/compute-budget", "programs/config", diff --git a/programs/address-map-tests/Cargo.toml b/programs/address-map-tests/Cargo.toml new file mode 100644 index 00000000000000..d3524cbb5d7f2c --- /dev/null +++ b/programs/address-map-tests/Cargo.toml @@ -0,0 +1,21 @@ +# This package only exists to avoid circular dependencies during cargo publish: +# solana-runtime -> solana-address-program-runtime -> solana-program-test -> solana-runtime + +[package] +name = "solana-address-map-program-tests" +version = "1.9.0" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana" +license = "Apache-2.0" +homepage = "https://solana.com/" +edition = "2018" +publish = false + +[dev-dependencies] +bincode = "1.3.3" +solana-address-map-program = { path = "../address-map", version = "=1.9.0" } +solana-program-test = { path = "../../program-test", version = "=1.9.0" } +solana-sdk = { path = "../../sdk", version = "=1.9.0" } + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/programs/address-map-tests/tests/processor.rs b/programs/address-map-tests/tests/processor.rs new file mode 100644 index 00000000000000..edf01da915f4a4 --- /dev/null +++ b/programs/address-map-tests/tests/processor.rs @@ -0,0 +1,246 @@ +use { + solana_address_map_program::{ + id, + instruction::{ + activate, close_account, deactivate, initialize_account, insert_entries, set_authority, + }, + processor::process_instruction, + state::{AddressMap, AddressMapState}, + }, + solana_program_test::*, + solana_sdk::{ + clock::{Epoch, Slot}, + epoch_schedule::EpochSchedule, + instruction::InstructionError, + native_token::LAMPORTS_PER_SOL, + pubkey::Pubkey, + signature::{Keypair, Signer}, + system_instruction::transfer, + transaction::{Transaction, TransactionError}, + }, +}; + +const WARP_EPOCH: Epoch = 1; + +async fn setup_warped_context() -> ProgramTestContext { + let program_test = ProgramTest::new("", id(), Some(process_instruction)); + let mut context = program_test.start_with_context().await; + let epoch_schedule = EpochSchedule::default(); + let warp_slot = epoch_schedule.get_first_slot_in_epoch(WARP_EPOCH); + context.warp_to_slot(warp_slot).unwrap(); + context +} + +#[tokio::test] +async fn test_initialize_account() { + let mut context = setup_warped_context().await; + let client = &mut context.banks_client; + let payer = &context.payer; + let recent_blockhash = context.last_blockhash; + + // First initialization should succeed + { + let authority_address = Pubkey::new_unique(); + let (init_account_instruction, map_address) = + initialize_account(payer.pubkey(), authority_address, WARP_EPOCH, 0); + let transaction = Transaction::new_signed_with_payer( + &[init_account_instruction], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + + assert!(client.process_transaction(transaction).await.is_ok()); + let map_account = client.get_account(map_address).await.unwrap().unwrap(); + let map = AddressMap::deserialize(&map_account.data).unwrap(); + assert_eq!(map.authority, Some(authority_address)); + assert_eq!(map.activation_slot, Slot::MAX); + assert_eq!(map.deactivation_slot, Slot::MAX); + assert_eq!(map.num_entries, 0u8); + } + + // Second initialization should fail + { + let authority_address = Pubkey::new_unique(); + let transaction = Transaction::new_signed_with_payer( + &[initialize_account(payer.pubkey(), authority_address, WARP_EPOCH, 0).0], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + + assert_eq!( + client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(0, InstructionError::AccountAlreadyInitialized), + ); + } +} + +#[tokio::test] +async fn test_initialize_account_wrong_epoch() { + let mut context = setup_warped_context().await; + let client = &mut context.banks_client; + let payer = &context.payer; + let recent_blockhash = context.last_blockhash; + let transaction = Transaction::new_signed_with_payer( + &[initialize_account(payer.pubkey(), Pubkey::new_unique(), WARP_EPOCH + 1, 0).0], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + + assert_eq!( + client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); +} + +#[tokio::test] +async fn test_set_authority() { + let mut context = setup_warped_context().await; + let client = &mut context.banks_client; + let payer = &context.payer; + let recent_blockhash = context.last_blockhash; + let authority = Keypair::new(); + let new_authority = Pubkey::new_unique(); + + let (init_account_instruction, map_address) = + initialize_account(payer.pubkey(), authority.pubkey(), WARP_EPOCH, 0); + let transaction = Transaction::new_signed_with_payer( + &[ + init_account_instruction, + set_authority(map_address, authority.pubkey(), Some(new_authority)), + ], + Some(&payer.pubkey()), + &[payer, &authority], + recent_blockhash, + ); + + assert!(client.process_transaction(transaction).await.is_ok()); + let map_account = client.get_account(map_address).await.unwrap().unwrap(); + let map = AddressMap::deserialize(&map_account.data).unwrap(); + assert_eq!(map.authority, Some(new_authority)); +} + +#[tokio::test] +async fn test_insert_entries() { + let mut context = setup_warped_context().await; + let client = &mut context.banks_client; + let payer = &context.payer; + let recent_blockhash = context.last_blockhash; + let authority = Keypair::new(); + + let (init_account_instruction, map_address) = + initialize_account(payer.pubkey(), authority.pubkey(), WARP_EPOCH, 10); + let mut entries = Vec::with_capacity(10); + entries.resize_with(10, Pubkey::new_unique); + let transaction = Transaction::new_signed_with_payer( + &[ + init_account_instruction, + insert_entries(map_address, authority.pubkey(), 0, entries.clone()), + ], + Some(&payer.pubkey()), + &[payer, &authority], + recent_blockhash, + ); + + assert!(client.process_transaction(transaction).await.is_ok()); + let map_account = client.get_account(map_address).await.unwrap().unwrap(); + let map = AddressMap::deserialize(&map_account.data).unwrap(); + let map_entries = map.deserialize_entries(&map_account.data).unwrap(); + assert_eq!(map_entries, entries); +} + +#[tokio::test] +async fn test_activate() { + let mut context = setup_warped_context().await; + let client = &mut context.banks_client; + let payer = &context.payer; + let recent_blockhash = context.last_blockhash; + let authority = Keypair::new(); + + let (init_account_instruction, map_address) = + initialize_account(payer.pubkey(), authority.pubkey(), WARP_EPOCH, 0); + let transaction = Transaction::new_signed_with_payer( + &[ + init_account_instruction, + activate(map_address, authority.pubkey()), + ], + Some(&payer.pubkey()), + &[payer, &authority], + recent_blockhash, + ); + + assert!(client.process_transaction(transaction).await.is_ok()); + let map_account = client.get_account(map_address).await.unwrap().unwrap(); + let map = AddressMap::deserialize(&map_account.data).unwrap(); + let current_slot = client.get_root_slot().await.unwrap(); + assert_eq!(map.activation_slot, current_slot); +} + +#[tokio::test] +async fn test_deactivate() { + let mut context = setup_warped_context().await; + let client = &mut context.banks_client; + let payer = &context.payer; + let recent_blockhash = context.last_blockhash; + let authority = Keypair::new(); + + let (init_account_instruction, map_address) = + initialize_account(payer.pubkey(), authority.pubkey(), WARP_EPOCH, 0); + let transaction = Transaction::new_signed_with_payer( + &[ + init_account_instruction, + activate(map_address, authority.pubkey()), + deactivate(map_address, authority.pubkey()), + ], + Some(&payer.pubkey()), + &[payer, &authority], + recent_blockhash, + ); + + assert!(client.process_transaction(transaction).await.is_ok()); + let map_account = client.get_account(map_address).await.unwrap().unwrap(); + let map = AddressMap::deserialize(&map_account.data).unwrap(); + let current_slot = client.get_root_slot().await.unwrap(); + assert_eq!(map.deactivation_slot, current_slot); +} + +#[tokio::test] +async fn test_close_account() { + let mut context = setup_warped_context().await; + let client = &mut context.banks_client; + let payer = &context.payer; + let recent_blockhash = context.last_blockhash; + let authority = Keypair::new(); + + let (init_account_instruction, map_address) = + initialize_account(payer.pubkey(), authority.pubkey(), WARP_EPOCH, 0); + let transaction = Transaction::new_signed_with_payer( + &[ + init_account_instruction, + close_account(map_address, payer.pubkey(), authority.pubkey()), + transfer(&payer.pubkey(), &map_address, LAMPORTS_PER_SOL), + ], + Some(&payer.pubkey()), + &[payer, &authority], + recent_blockhash, + ); + + assert!(client.process_transaction(transaction).await.is_ok()); + let closed_account = client.get_account(map_address).await.unwrap().unwrap(); + let uninitialized_data = bincode::serialize(&AddressMapState::Uninitialized).unwrap(); + assert_eq!( + closed_account.data[0..uninitialized_data.len()], + uninitialized_data[..] + ); + assert_eq!(closed_account.lamports, LAMPORTS_PER_SOL); +} diff --git a/programs/address-map/Cargo.toml b/programs/address-map/Cargo.toml new file mode 100644 index 00000000000000..f209cc065bb57c --- /dev/null +++ b/programs/address-map/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "solana-address-map-program" +version = "1.9.0" +description = "Solana address map program" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana" +license = "Apache-2.0" +homepage = "https://solana.com/" +documentation = "https://docs.rs/solana-address-map-program" +edition = "2018" + +[dependencies] +bincode = "1.3.3" +log = "0.4.14" +num-derive = "0.3" +num-traits = "0.2" +serde = { version = "1.0.127", features = ["derive"] } +solana-frozen-abi = { path = "../../frozen-abi", version = "=1.9.0" } +solana-frozen-abi-macro = { path = "../../frozen-abi/macro", version = "=1.9.0" } +solana-program-runtime = { path = "../../program-runtime", version = "=1.9.0" } +solana-sdk = { path = "../../sdk", version = "=1.9.0" } +thiserror = "1.0" + +[build-dependencies] +rustc_version = "0.4" + +[lib] +crate-type = ["lib"] +name = "solana_address_map_program" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/programs/address-map/build.rs b/programs/address-map/build.rs new file mode 120000 index 00000000000000..84539eddaa6ded --- /dev/null +++ b/programs/address-map/build.rs @@ -0,0 +1 @@ +../../frozen-abi/build.rs \ No newline at end of file diff --git a/programs/address-map/src/instruction.rs b/programs/address-map/src/instruction.rs new file mode 100644 index 00000000000000..8513c408a934f1 --- /dev/null +++ b/programs/address-map/src/instruction.rs @@ -0,0 +1,177 @@ +use { + crate::id, + serde::{Deserialize, Serialize}, + solana_sdk::{ + clock::Epoch, + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + system_program, + }, +}; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub enum AddressMapInstruction { + /// Initialize an address map account + /// + /// # Account references + /// 0. `[WRITE]` Uninitialized address map account + /// 1. `[SIGNER, WRITE]` Account that will fund the new address map. + /// 2. `[]` System program for CPI. + InitializeAccount { + bump_seed: u8, + num_entries: u8, + authority: Pubkey, + }, + + /// Set a new authority for an address map account + /// + /// # Account references + /// 0. `[WRITE]` Address map account to set authority for + /// 1. `[SIGNER]` Current authority + SetAuthority { new_authority: Option }, + + /// Insert entries into an address map account + /// + /// # Account references + /// 0. `[WRITE]` Address map account to insert entries into + /// 1. `[SIGNER]` Current authority + InsertEntries { offset: u8, entries: Vec }, + + /// Activate an address map account + /// + /// # Account references + /// 0. `[WRITE]` Address map account to activate + /// 1. `[SIGNER]` Current authority + Activate, + + /// Deactivate an address map account + /// + /// # Account references + /// 0. `[WRITE]` Address map account to deactivate + /// 1. `[SIGNER]` Current authority + Deactivate, + + /// Close an address map account + /// + /// # Account references + /// 0. `[WRITE]` Address map account to close + /// 1. `[WRITE]` Recipient of closed account lamports + /// 2. `[SIGNER]` Current authority, if required. + CloseAccount, +} + +/// Derive an address map address from a wallet address and the current epoch. +pub fn derive_address_map_address(payer_address: &Pubkey, current_epoch: Epoch) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[payer_address.as_ref(), ¤t_epoch.to_le_bytes()], + &id(), + ) +} + +/// Returns an instruction to initialize a map account as well as the address of +/// the map account that will be created when the instruction is processed. +pub fn initialize_account( + payer_address: Pubkey, + authority_address: Pubkey, + current_epoch: Epoch, + num_entries: u8, +) -> (Instruction, Pubkey) { + let (map_address, bump_seed) = derive_address_map_address(&payer_address, current_epoch); + let instruction = Instruction::new_with_bincode( + id(), + &AddressMapInstruction::InitializeAccount { + bump_seed, + num_entries, + authority: authority_address, + }, + vec![ + AccountMeta::new(map_address, false), + AccountMeta::new(payer_address, true), + AccountMeta::new_readonly(system_program::id(), false), + ], + ); + + (instruction, map_address) +} + +/// Returns an instruction that updates the authority of an address map account. +/// If the new authority is `None`, the address map account will be immutable. +/// Inactive address maps cannot be made immutable. +pub fn set_authority( + map_address: Pubkey, + current_authority_address: Pubkey, + new_authority_address: Option, +) -> Instruction { + Instruction::new_with_bincode( + id(), + &AddressMapInstruction::SetAuthority { + new_authority: new_authority_address, + }, + vec![ + AccountMeta::new(map_address, false), + AccountMeta::new_readonly(current_authority_address, true), + ], + ) +} + +/// Returns an instruction which inserts address entries into an address map +/// account at the specified offset. +pub fn insert_entries( + map_address: Pubkey, + authority_address: Pubkey, + offset: u8, + entries: Vec, +) -> Instruction { + Instruction::new_with_bincode( + id(), + &AddressMapInstruction::InsertEntries { offset, entries }, + vec![ + AccountMeta::new(map_address, false), + AccountMeta::new_readonly(authority_address, true), + ], + ) +} + +/// Returns an instruction that starts the activation phase for an address map +/// account. +pub fn activate(map_address: Pubkey, authority_address: Pubkey) -> Instruction { + Instruction::new_with_bincode( + id(), + &AddressMapInstruction::Activate, + vec![ + AccountMeta::new(map_address, false), + AccountMeta::new_readonly(authority_address, true), + ], + ) +} + +/// Returns an instruction that starts the deactivation phase for an address map +/// account. +pub fn deactivate(map_address: Pubkey, authority_address: Pubkey) -> Instruction { + Instruction::new_with_bincode( + id(), + &AddressMapInstruction::Deactivate, + vec![ + AccountMeta::new(map_address, false), + AccountMeta::new_readonly(authority_address, true), + ], + ) +} + +/// Returns an instruction that closes an inactive address map account and +/// transfers its lamport balance to the recipient address. +pub fn close_account( + map_address: Pubkey, + recipient_address: Pubkey, + authority_address: Pubkey, +) -> Instruction { + Instruction::new_with_bincode( + id(), + &AddressMapInstruction::CloseAccount, + vec![ + AccountMeta::new(map_address, false), + AccountMeta::new(recipient_address, false), + AccountMeta::new_readonly(authority_address, true), + ], + ) +} diff --git a/programs/address-map/src/lib.rs b/programs/address-map/src/lib.rs new file mode 100644 index 00000000000000..e49c47074552a7 --- /dev/null +++ b/programs/address-map/src/lib.rs @@ -0,0 +1,22 @@ +#![allow(incomplete_features)] +#![cfg_attr(RUSTC_WITH_SPECIALIZATION, feature(specialization))] +#![cfg_attr(RUSTC_NEEDS_PROC_MACRO_HYGIENE, feature(proc_macro_hygiene))] + +use solana_sdk::{clock::Epoch, declare_id}; + +pub mod instruction; +pub mod processor; +pub mod state; + +declare_id!("AddressMap111111111111111111111111111111111"); + +/// The number of epochs required to deactivate an address map. If an address +/// map is deactivated in some slot in epoch X, it can no longer be used to map +/// addresses from the start of epoch X + 1. +/// +/// # Notes +/// +/// The deactivation cooldown time must be at least one epoch to ensure that +/// address map accounts cannot be activated more than once. Address maps +/// are derived from the authority and the current epoch when created. +pub const DEACTIVATION_COOLDOWN: Epoch = 1; diff --git a/programs/address-map/src/processor.rs b/programs/address-map/src/processor.rs new file mode 100644 index 00000000000000..d8273a6e09b385 --- /dev/null +++ b/programs/address-map/src/processor.rs @@ -0,0 +1,292 @@ +use { + crate::{ + instruction::AddressMapInstruction, + state::{AddressMap, AddressMapState, ADDRESS_MAP_ENTRIES_START}, + DEACTIVATION_COOLDOWN, + }, + solana_program_runtime::{ + instruction_processor::InstructionProcessor, + invoke_context::{get_sysvar, InvokeContext}, + }, + solana_sdk::{ + account::WritableAccount, + account_utils::State, + clock::Slot, + instruction::InstructionError, + keyed_account::keyed_account_at_index, + program_utils::limited_deserialize, + pubkey::{Pubkey, PUBKEY_BYTES}, + system_instruction, + sysvar::{ + clock::{self, Clock}, + epoch_schedule::{self, EpochSchedule}, + rent::{self, Rent}, + }, + }, +}; + +pub fn process_instruction( + _first_instruction_account: usize, + instruction_data: &[u8], + invoke_context: &mut dyn InvokeContext, +) -> Result<(), InstructionError> { + let keyed_accounts = invoke_context.get_keyed_accounts()?; + + match limited_deserialize(instruction_data)? { + AddressMapInstruction::InitializeAccount { + bump_seed, + num_entries, + authority, + } => { + const MAP_INDEX: usize = 0; + const PAYER_INDEX: usize = 1; + + let map_account = keyed_account_at_index(keyed_accounts, MAP_INDEX)?; + if map_account.data_len()? > 0 { + return Err(InstructionError::AccountAlreadyInitialized); + } + + let payer_account = keyed_account_at_index(keyed_accounts, PAYER_INDEX)?; + let payer_key = if let Some(payer_key) = payer_account.signer_key() { + *payer_key + } else { + return Err(InstructionError::MissingRequiredSignature); + }; + + let clock: Clock = get_sysvar(invoke_context, &clock::id())?; + let current_epoch = clock.epoch; + + // Use a derived address to ensure that an address map can never be + // initialized more than once at the same address. + let map_key = *map_account.unsigned_key(); + if Ok(map_key) + != Pubkey::create_program_address( + &[ + payer_key.as_ref(), + ¤t_epoch.to_le_bytes(), + &[bump_seed], + ], + &crate::id(), + ) + { + return Err(InstructionError::InvalidArgument); + } + + let signers = &[map_key, payer_key]; + let entries_size = usize::from(num_entries).saturating_mul(PUBKEY_BYTES); + let map_account_len = ADDRESS_MAP_ENTRIES_START.saturating_add(entries_size); + let rent: Rent = get_sysvar(invoke_context, &rent::id())?; + let required_lamports = rent + .minimum_balance(map_account_len) + .max(1) + .saturating_sub(map_account.lamports()?); + + if required_lamports > 0 { + InstructionProcessor::native_invoke( + invoke_context, + system_instruction::transfer(&payer_key, &map_key, required_lamports), + signers, + )?; + } + + InstructionProcessor::native_invoke( + invoke_context, + system_instruction::allocate(&map_key, map_account_len as u64), + signers, + )?; + + InstructionProcessor::native_invoke( + invoke_context, + system_instruction::assign(&map_key, &crate::id()), + signers, + )?; + + let keyed_accounts = invoke_context.get_keyed_accounts()?; + let map_account = keyed_account_at_index(keyed_accounts, MAP_INDEX)?; + map_account.set_state(&AddressMapState::Initialized(AddressMap { + authority: Some(authority), + activation_slot: Slot::MAX, + deactivation_slot: Slot::MAX, + num_entries, + }))?; + } + AddressMapInstruction::SetAuthority { new_authority } => { + let map_account = keyed_account_at_index(keyed_accounts, 0)?; + if map_account.owner()? != crate::id() { + return Err(InstructionError::InvalidAccountOwner); + } + + let authority_account = keyed_account_at_index(keyed_accounts, 1)?; + let mut map = if let AddressMapState::Initialized(map) = map_account.state()? { + if map.authority.is_none() { + return Err(InstructionError::Immutable); + } + if map.authority != Some(*authority_account.unsigned_key()) { + return Err(InstructionError::IncorrectAuthority); + } + if authority_account.signer_key().is_none() { + return Err(InstructionError::MissingRequiredSignature); + } + // Cannot make an unactivated account immutable + if new_authority.is_none() && map.activation_slot == Slot::MAX { + return Err(InstructionError::InvalidInstructionData); + } + map + } else { + return Err(InstructionError::UninitializedAccount); + }; + + map.authority = new_authority; + map_account.set_state(&AddressMapState::Initialized(map))?; + } + AddressMapInstruction::InsertEntries { offset, entries } => { + let map_account = keyed_account_at_index(keyed_accounts, 0)?; + if map_account.owner()? != crate::id() { + return Err(InstructionError::InvalidAccountOwner); + } + + let authority_account = keyed_account_at_index(keyed_accounts, 1)?; + if let AddressMapState::Initialized(map) = map_account.state()? { + if map.authority.is_none() { + return Err(InstructionError::Immutable); + } + if map.authority != Some(*authority_account.unsigned_key()) { + return Err(InstructionError::IncorrectAuthority); + } + if authority_account.signer_key().is_none() { + return Err(InstructionError::MissingRequiredSignature); + } + // cannot modify activated map accounts + if map.activation_slot != Slot::MAX { + return Err(InstructionError::InvalidInstructionData); + } + } else { + return Err(InstructionError::UninitializedAccount); + } + + let insert_start = ADDRESS_MAP_ENTRIES_START.saturating_add(usize::from(offset)); + let insert_end = + insert_start.saturating_add(PUBKEY_BYTES.saturating_mul(entries.len())); + let mut map_account_ref = map_account.try_account_ref_mut()?; + let serialized_map: &mut [u8] = &mut map_account_ref.data_as_mut_slice(); + if serialized_map.len() < insert_end { + return Err(InstructionError::InvalidInstructionData); + } + + let mut start = insert_start; + for entry in entries { + let end = start.saturating_add(PUBKEY_BYTES); + serialized_map[start..end].copy_from_slice(entry.as_ref()); + start = end; + } + } + AddressMapInstruction::Activate => { + let map_account = keyed_account_at_index(keyed_accounts, 0)?; + if map_account.owner()? != crate::id() { + return Err(InstructionError::InvalidAccountOwner); + } + + let authority_account = keyed_account_at_index(keyed_accounts, 1)?; + let mut map = if let AddressMapState::Initialized(map) = map_account.state()? { + if map.authority.is_none() { + return Err(InstructionError::Immutable); + } + if map.authority != Some(*authority_account.unsigned_key()) { + return Err(InstructionError::IncorrectAuthority); + } + if authority_account.signer_key().is_none() { + return Err(InstructionError::MissingRequiredSignature); + } + // Cannot activate an already activated account + if map.activation_slot != Slot::MAX { + return Err(InstructionError::InvalidInstructionData); + } + map + } else { + return Err(InstructionError::UninitializedAccount); + }; + + let clock: Clock = get_sysvar(invoke_context, &clock::id())?; + map.activation_slot = clock.slot; + map_account.set_state(&AddressMapState::Initialized(map))?; + } + AddressMapInstruction::Deactivate => { + let map_account = keyed_account_at_index(keyed_accounts, 0)?; + if map_account.owner()? != crate::id() { + return Err(InstructionError::InvalidAccountOwner); + } + + let authority_account = keyed_account_at_index(keyed_accounts, 1)?; + let mut map = if let AddressMapState::Initialized(map) = map_account.state()? { + if map.authority.is_none() { + return Err(InstructionError::Immutable); + } + if map.authority != Some(*authority_account.unsigned_key()) { + return Err(InstructionError::IncorrectAuthority); + } + if authority_account.signer_key().is_none() { + return Err(InstructionError::MissingRequiredSignature); + } + // Cannot deactivate an unactivated account + if map.activation_slot == Slot::MAX { + return Err(InstructionError::InvalidInstructionData); + } + // Cannot deactivate an already deactivated account + if map.deactivation_slot != Slot::MAX { + return Err(InstructionError::InvalidInstructionData); + } + map + } else { + return Err(InstructionError::UninitializedAccount); + }; + + let clock: Clock = get_sysvar(invoke_context, &clock::id())?; + map.deactivation_slot = clock.slot; + map_account.set_state(&AddressMapState::Initialized(map))?; + } + AddressMapInstruction::CloseAccount => { + let map_account = keyed_account_at_index(keyed_accounts, 0)?; + if map_account.owner()? != crate::id() { + return Err(InstructionError::InvalidAccountOwner); + } + + let recipient_account = keyed_account_at_index(keyed_accounts, 1)?; + if let Ok(AddressMapState::Initialized(map)) = map_account.state() { + let authority_account = keyed_account_at_index(keyed_accounts, 2)?; + if map.authority.is_none() { + return Err(InstructionError::Immutable); + } + if map.authority != Some(*authority_account.unsigned_key()) { + return Err(InstructionError::IncorrectAuthority); + } + if authority_account.signer_key().is_none() { + return Err(InstructionError::MissingRequiredSignature); + } + if map.activation_slot != Slot::MAX { + if map.deactivation_slot == Slot::MAX { + return Err(InstructionError::InvalidInstructionData); + } + + let clock: Clock = get_sysvar(invoke_context, &clock::id())?; + let epoch_schedule: EpochSchedule = + get_sysvar(invoke_context, &epoch_schedule::id())?; + let current_epoch = clock.epoch; + let deactivation_epoch = epoch_schedule.get_epoch(map.deactivation_slot); + let first_inactive_epoch = + deactivation_epoch.saturating_add(DEACTIVATION_COOLDOWN); + if current_epoch < first_inactive_epoch { + return Err(InstructionError::InvalidInstructionData); + } + } + map_account.set_state(&AddressMapState::Uninitialized)?; + } + + recipient_account + .try_account_ref_mut()? + .checked_add_lamports(map_account.lamports()?)?; + map_account.try_account_ref_mut()?.set_lamports(0); + } + } + + Ok(()) +} diff --git a/programs/address-map/src/state.rs b/programs/address-map/src/state.rs new file mode 100644 index 00000000000000..acb47306facd94 --- /dev/null +++ b/programs/address-map/src/state.rs @@ -0,0 +1,190 @@ +use { + serde::{Deserialize, Serialize}, + solana_frozen_abi_macro::{AbiEnumVisitor, AbiExample}, + solana_sdk::{ + clock::Slot, + pubkey::{Pubkey, PUBKEY_BYTES}, + }, + thiserror::Error, +}; + +/// The byte offset where entry encoding begins +pub const ADDRESS_MAP_ENTRIES_START: usize = 54; + +/// Address map program account states +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, AbiExample, AbiEnumVisitor)] +#[allow(clippy::large_enum_variant)] +pub enum AddressMapState { + /// Account is not initialized. + Uninitialized, + /// Initialized `AddressMap` account. + Initialized(AddressMap), +} + +/// Data structure of address map +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, AbiExample)] +pub struct AddressMap { + /// Activation slot. Address map entries may not be modified once activated. + pub activation_slot: Slot, + /// Deactivation slot. Address map accounts cannot be closed unless they have been deactivated. + pub deactivation_slot: Slot, + /// Number of stored address entries. Limited by `MAX_ADDRESS_MAP_ENTRIES`. + pub num_entries: u8, + /// Authority address which must sign for each modification. + pub authority: Option, + // Raw list of `num_entries` addresses follows this serialized structure in + // the account's data, starting from `ADDRESS_MAP_ENTRIES_START`. +} + +#[derive(Error, Debug, PartialEq, Eq)] +pub enum SerializationError { + /// Bincode serialization failed + #[error("Bincode serialization error")] + BincodeError, + + /// Address map is uninitialized + #[error("Address map must be initialized")] + Uninitialized, + + /// Address map data must have room for `num_entries` addresses. + #[error("Address map data too small for {0} address entries")] + InvalidNumEntries(u8), +} + +impl From for SerializationError { + fn from(_: bincode::Error) -> Self { + Self::BincodeError + } +} + +impl AddressMap { + /// Attempt to deserialize an initialized address map account. + pub fn deserialize(serialized_map: &[u8]) -> Result { + let state = bincode::deserialize::(serialized_map)?; + match state { + AddressMapState::Initialized(address_map) => Ok(address_map), + AddressMapState::Uninitialized => Err(SerializationError::Uninitialized), + } + } + + /// Attempt to deserialize the address entries stored in an address map + /// account. Address map data must have room for `num_entries` addresses. + pub fn deserialize_entries( + &self, + serialized_map: &[u8], + ) -> Result, SerializationError> { + let entries_size = PUBKEY_BYTES.saturating_mul(usize::from(self.num_entries)); + let entries_end = ADDRESS_MAP_ENTRIES_START.saturating_add(entries_size); + if serialized_map.len() >= entries_end { + Ok(serialized_map[ADDRESS_MAP_ENTRIES_START..entries_end] + .chunks(PUBKEY_BYTES) + .map(|entry_bytes| Pubkey::new(entry_bytes)) + .collect()) + } else { + Err(SerializationError::InvalidNumEntries(self.num_entries)) + } + } + + /// Serialize the address map along with its entries. + pub fn serialize_with_entries(&self, entries: &[Pubkey]) -> Vec { + let mut data = bincode::serialize(&(1u32, &self)).unwrap(); + data.resize(ADDRESS_MAP_ENTRIES_START, 0); + for entry in entries { + data.extend_from_slice(entry.as_ref()); + } + data + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn create_test_address_map(num_entries: u8) -> AddressMap { + AddressMap { + authority: Some(Pubkey::new_unique()), + activation_slot: Slot::MAX, + deactivation_slot: Slot::MAX, + num_entries, + } + } + + #[test] + fn test_map_entries_start() { + let address_map = AddressMapState::Initialized(create_test_address_map(0)); + let map_meta_size = bincode::serialized_size(&address_map).unwrap(); + + assert_eq!(map_meta_size as usize, ADDRESS_MAP_ENTRIES_START); + } + + #[test] + fn test_deserialize() { + assert_eq!( + AddressMap::deserialize(&[]).err(), + Some(SerializationError::BincodeError), + ); + + assert_eq!( + AddressMap::deserialize(&[0u8; ADDRESS_MAP_ENTRIES_START]).err(), + Some(SerializationError::Uninitialized), + ); + + let address_map = create_test_address_map(1); + let address_map_data = address_map.serialize_with_entries(&[]); + assert_eq!(AddressMap::deserialize(&address_map_data), Ok(address_map),); + } + + #[test] + fn test_deserialize_entries() { + for num_entries in [0, 1, 10, u8::MAX] { + let address_map = create_test_address_map(num_entries); + let entries = { + let mut vec = Vec::with_capacity(usize::from(num_entries)); + vec.resize_with(usize::from(num_entries), Pubkey::new_unique); + vec + }; + + let serialized_map = address_map.serialize_with_entries(&entries); + assert_eq!( + address_map.deserialize_entries(&serialized_map), + Ok(entries), + ); + } + } + + #[test] + fn test_deserialize_entries_trailing_bytes() { + let address_map = create_test_address_map(0); + let mut serialized_map = address_map.serialize_with_entries(&[]); + serialized_map.resize(serialized_map.len() + 1, 0u8); + + assert!(address_map.deserialize_entries(&serialized_map).is_ok()); + } + + #[test] + fn test_deserialize_entries_too_small() { + let address_map = create_test_address_map(1); + let serialized_map = address_map.serialize_with_entries(&[]); + + assert_eq!( + address_map.deserialize_entries(&serialized_map).err(), + Some(SerializationError::InvalidNumEntries(1)), + ); + } + + #[test] + fn test_serialize_with_entries() { + let address_map = create_test_address_map(1); + let entries = vec![Pubkey::new_unique()]; + let serialized_map = address_map.serialize_with_entries(&entries); + + assert_eq!( + address_map.deserialize_entries(&serialized_map), + Ok(entries) + ); + assert_eq!( + bincode::deserialize::(&serialized_map).ok(), + Some(AddressMapState::Initialized(address_map)) + ); + } +} diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index a32368a002f46f..9e9ecc46c7ed96 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -32,6 +32,7 @@ rayon = "1.5.1" regex = "1.5.4" serde = { version = "1.0.130", features = ["rc"] } serde_derive = "1.0.103" +solana-address-map-program = { path = "../programs/address-map", version = "=1.9.0" } solana-config-program = { path = "../programs/config", version = "=1.9.0" } solana-compute-budget-program = { path = "../programs/compute-budget", version = "=1.9.0" } solana-frozen-abi = { path = "../frozen-abi", version = "=1.9.0" } diff --git a/runtime/src/builtins.rs b/runtime/src/builtins.rs index b5a38edb863223..3d3832990634f2 100644 --- a/runtime/src/builtins.rs +++ b/runtime/src/builtins.rs @@ -171,6 +171,15 @@ fn feature_builtins() -> Vec<(Builtin, Pubkey, ActivationType)> { feature_set::prevent_calling_precompiles_as_programs::id(), ActivationType::RemoveProgram, ), + ( + Builtin::new( + "address_map_program", + solana_address_map_program::id(), + solana_address_map_program::processor::process_instruction, + ), + feature_set::versioned_tx_message_enabled::id(), + ActivationType::NewProgram, + ), ] }