diff --git a/program/Cargo.toml b/program/Cargo.toml new file mode 100644 index 0000000..669a295 --- /dev/null +++ b/program/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "spl-record" +version = "0.3.0" +description = "Solana Program Library Record Program" +authors = ["Solana Labs Maintainers "] +repository = "https://github.com/solana-labs/solana-program-library" +license = "Apache-2.0" +edition = "2021" + +[features] +no-entrypoint = [] +test-sbf = [] + +[dependencies] +bytemuck = { version = "1.20.0", features = ["derive"] } +num-derive = "0.4" +num-traits = "0.2" +solana-account-info = "2.1.0" +solana-decode-error = "2.1.0" +solana-instruction = { version = "2.1.0", features = ["std"] } +solana-msg = "2.1.0" +solana-program-entrypoint = "2.1.0" +solana-program-error = "2.1.0" +solana-program-pack = "2.1.0" +solana-pubkey = { version = "2.1.0", features = ["bytemuck"] } +solana-rent = "2.1.0" +thiserror = "2.0" + +[dev-dependencies] +solana-program-test = "2.1.0" +solana-sdk = "2.1.0" + +[lib] +crate-type = ["cdylib", "lib"] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[lints] +workspace = true diff --git a/program/README.md b/program/README.md new file mode 100644 index 0000000..1886149 --- /dev/null +++ b/program/README.md @@ -0,0 +1,9 @@ +# Record + +On-chain program for writing arbitrary data to an account, authorized by an +owner of the account. + +## Audit + +The repository [README](https://github.com/solana-labs/solana-program-library#audits) +contains information about program audits. diff --git a/program/src/entrypoint.rs b/program/src/entrypoint.rs new file mode 100644 index 0000000..f52b183 --- /dev/null +++ b/program/src/entrypoint.rs @@ -0,0 +1,16 @@ +//! Program entrypoint + +#![cfg(all(target_os = "solana", not(feature = "no-entrypoint")))] + +use { + solana_account_info::AccountInfo, solana_program_error::ProgramResult, solana_pubkey::Pubkey, +}; + +solana_program_entrypoint::entrypoint!(process_instruction); +fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + crate::processor::process_instruction(program_id, accounts, instruction_data) +} diff --git a/program/src/error.rs b/program/src/error.rs new file mode 100644 index 0000000..f878043 --- /dev/null +++ b/program/src/error.rs @@ -0,0 +1,28 @@ +//! Error types + +use { + num_derive::FromPrimitive, solana_decode_error::DecodeError, + solana_program_error::ProgramError, thiserror::Error, +}; + +/// Errors that may be returned by the program. +#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] +pub enum RecordError { + /// Incorrect authority provided on update or delete + #[error("Incorrect authority provided on update or delete")] + IncorrectAuthority, + + /// Calculation overflow + #[error("Calculation overflow")] + Overflow, +} +impl From for ProgramError { + fn from(e: RecordError) -> Self { + ProgramError::Custom(e as u32) + } +} +impl DecodeError for RecordError { + fn type_of() -> &'static str { + "Record Error" + } +} diff --git a/program/src/instruction.rs b/program/src/instruction.rs new file mode 100644 index 0000000..a8de8c8 --- /dev/null +++ b/program/src/instruction.rs @@ -0,0 +1,260 @@ +//! Program instructions + +use { + crate::id, + solana_instruction::{AccountMeta, Instruction}, + solana_program_error::ProgramError, + solana_pubkey::Pubkey, + std::mem::size_of, +}; + +/// Instructions supported by the program +#[derive(Clone, Debug, PartialEq)] +pub enum RecordInstruction<'a> { + /// Create a new record + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Record account, must be uninitialized + /// 1. `[]` Record authority + Initialize, + + /// Write to the provided record account + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Record account, must be previously initialized + /// 1. `[signer]` Current record authority + Write { + /// Offset to start writing record, expressed as `u64`. + offset: u64, + /// Data to replace the existing record data + data: &'a [u8], + }, + + /// Update the authority of the provided record account + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Record account, must be previously initialized + /// 1. `[signer]` Current record authority + /// 2. `[]` New record authority + SetAuthority, + + /// Close the provided record account, draining lamports to recipient + /// account + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Record account, must be previously initialized + /// 1. `[signer]` Record authority + /// 2. `[]` Receiver of account lamports + CloseAccount, + + /// Reallocate additional space in a record account + /// + /// If the record account already has enough space to hold the specified + /// data length, then the instruction does nothing. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The record account to reallocate + /// 1. `[signer]` The account's owner + Reallocate { + /// The length of the data to hold in the record account excluding meta + /// data + data_length: u64, + }, +} + +impl<'a> RecordInstruction<'a> { + /// Unpacks a byte buffer into a [RecordInstruction]. + pub fn unpack(input: &'a [u8]) -> Result { + const U32_BYTES: usize = 4; + const U64_BYTES: usize = 8; + + let (&tag, rest) = input + .split_first() + .ok_or(ProgramError::InvalidInstructionData)?; + Ok(match tag { + 0 => Self::Initialize, + 1 => { + let offset = rest + .get(..U64_BYTES) + .and_then(|slice| slice.try_into().ok()) + .map(u64::from_le_bytes) + .ok_or(ProgramError::InvalidInstructionData)?; + let (length, data) = rest[U64_BYTES..].split_at(U32_BYTES); + let length = u32::from_le_bytes( + length + .try_into() + .map_err(|_| ProgramError::InvalidInstructionData)?, + ) as usize; + + Self::Write { + offset, + data: &data[..length], + } + } + 2 => Self::SetAuthority, + 3 => Self::CloseAccount, + 4 => { + let data_length = rest + .get(..U64_BYTES) + .and_then(|slice| slice.try_into().ok()) + .map(u64::from_le_bytes) + .ok_or(ProgramError::InvalidInstructionData)?; + + Self::Reallocate { data_length } + } + _ => return Err(ProgramError::InvalidInstructionData), + }) + } + + /// Packs a [RecordInstruction] into a byte buffer. + pub fn pack(&self) -> Vec { + let mut buf = Vec::with_capacity(size_of::()); + match self { + Self::Initialize => buf.push(0), + Self::Write { offset, data } => { + buf.push(1); + buf.extend_from_slice(&offset.to_le_bytes()); + buf.extend_from_slice(&(data.len() as u32).to_le_bytes()); + buf.extend_from_slice(data); + } + Self::SetAuthority => buf.push(2), + Self::CloseAccount => buf.push(3), + Self::Reallocate { data_length } => { + buf.push(4); + buf.extend_from_slice(&data_length.to_le_bytes()); + } + }; + buf + } +} + +/// Create a `RecordInstruction::Initialize` instruction +pub fn initialize(record_account: &Pubkey, authority: &Pubkey) -> Instruction { + Instruction { + program_id: id(), + accounts: vec![ + AccountMeta::new(*record_account, false), + AccountMeta::new_readonly(*authority, false), + ], + data: RecordInstruction::Initialize.pack(), + } +} + +/// Create a `RecordInstruction::Write` instruction +pub fn write(record_account: &Pubkey, signer: &Pubkey, offset: u64, data: &[u8]) -> Instruction { + Instruction { + program_id: id(), + accounts: vec![ + AccountMeta::new(*record_account, false), + AccountMeta::new_readonly(*signer, true), + ], + data: RecordInstruction::Write { offset, data }.pack(), + } +} + +/// Create a `RecordInstruction::SetAuthority` instruction +pub fn set_authority( + record_account: &Pubkey, + signer: &Pubkey, + new_authority: &Pubkey, +) -> Instruction { + Instruction { + program_id: id(), + accounts: vec![ + AccountMeta::new(*record_account, false), + AccountMeta::new_readonly(*signer, true), + AccountMeta::new_readonly(*new_authority, false), + ], + data: RecordInstruction::SetAuthority.pack(), + } +} + +/// Create a `RecordInstruction::CloseAccount` instruction +pub fn close_account(record_account: &Pubkey, signer: &Pubkey, receiver: &Pubkey) -> Instruction { + Instruction { + program_id: id(), + accounts: vec![ + AccountMeta::new(*record_account, false), + AccountMeta::new_readonly(*signer, true), + AccountMeta::new(*receiver, false), + ], + data: RecordInstruction::CloseAccount.pack(), + } +} + +/// Create a `RecordInstruction::Reallocate` instruction +pub fn reallocate(record_account: &Pubkey, signer: &Pubkey, data_length: u64) -> Instruction { + Instruction { + program_id: id(), + accounts: vec![ + AccountMeta::new(*record_account, false), + AccountMeta::new_readonly(*signer, true), + ], + data: RecordInstruction::Reallocate { data_length }.pack(), + } +} + +#[cfg(test)] +mod tests { + use {super::*, crate::state::tests::TEST_BYTES, solana_program_error::ProgramError}; + + #[test] + fn serialize_initialize() { + let instruction = RecordInstruction::Initialize; + let expected = vec![0]; + assert_eq!(instruction.pack(), expected); + assert_eq!(RecordInstruction::unpack(&expected).unwrap(), instruction); + } + + #[test] + fn serialize_write() { + let data = &TEST_BYTES; + let offset = 0u64; + let instruction = RecordInstruction::Write { offset: 0, data }; + let mut expected = vec![1]; + expected.extend_from_slice(&offset.to_le_bytes()); + expected.extend_from_slice(&(data.len() as u32).to_le_bytes()); + expected.extend_from_slice(data); + assert_eq!(instruction.pack(), expected); + assert_eq!(RecordInstruction::unpack(&expected).unwrap(), instruction); + } + + #[test] + fn serialize_set_authority() { + let instruction = RecordInstruction::SetAuthority; + let expected = vec![2]; + assert_eq!(instruction.pack(), expected); + assert_eq!(RecordInstruction::unpack(&expected).unwrap(), instruction); + } + + #[test] + fn serialize_close_account() { + let instruction = RecordInstruction::CloseAccount; + let expected = vec![3]; + assert_eq!(instruction.pack(), expected); + assert_eq!(RecordInstruction::unpack(&expected).unwrap(), instruction); + } + + #[test] + fn serialize_reallocate() { + let data_length = 16u64; + let instruction = RecordInstruction::Reallocate { data_length }; + let mut expected = vec![4]; + expected.extend_from_slice(&data_length.to_le_bytes()); + assert_eq!(instruction.pack(), expected); + assert_eq!(RecordInstruction::unpack(&expected).unwrap(), instruction); + } + + #[test] + fn deserialize_invalid_instruction() { + let mut expected = vec![12]; + expected.extend_from_slice(&TEST_BYTES); + let err: ProgramError = RecordInstruction::unpack(&expected).unwrap_err(); + assert_eq!(err, ProgramError::InvalidInstructionData); + } +} diff --git a/program/src/lib.rs b/program/src/lib.rs new file mode 100644 index 0000000..7646e07 --- /dev/null +++ b/program/src/lib.rs @@ -0,0 +1,17 @@ +//! Record program +#![deny(missing_docs)] + +mod entrypoint; +pub mod error; +pub mod instruction; +pub mod processor; +pub mod state; + +// Export current SDK types for downstream users building with a different SDK +// version +pub use { + solana_account_info, solana_decode_error, solana_instruction, solana_msg, + solana_program_entrypoint, solana_program_error, solana_program_pack, solana_pubkey, +}; + +solana_pubkey::declare_id!("recr1L3PCGKLbckBqMNcJhuuyU1zgo8nBhfLVsJNwr5"); diff --git a/program/src/processor.rs b/program/src/processor.rs new file mode 100644 index 0000000..2def5c1 --- /dev/null +++ b/program/src/processor.rs @@ -0,0 +1,181 @@ +//! Program state processor + +use { + crate::{error::RecordError, instruction::RecordInstruction, state::RecordData}, + solana_account_info::{next_account_info, AccountInfo}, + solana_msg::msg, + solana_program_error::{ProgramError, ProgramResult}, + solana_program_pack::IsInitialized, + solana_pubkey::Pubkey, +}; + +fn check_authority(authority_info: &AccountInfo, expected_authority: &Pubkey) -> ProgramResult { + if expected_authority != authority_info.key { + msg!("Incorrect record authority provided"); + return Err(RecordError::IncorrectAuthority.into()); + } + if !authority_info.is_signer { + msg!("Record authority signature missing"); + return Err(ProgramError::MissingRequiredSignature); + } + Ok(()) +} + +/// Instruction processor +pub fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + input: &[u8], +) -> ProgramResult { + let instruction = RecordInstruction::unpack(input)?; + let account_info_iter = &mut accounts.iter(); + + match instruction { + RecordInstruction::Initialize => { + msg!("RecordInstruction::Initialize"); + + let data_info = next_account_info(account_info_iter)?; + let authority_info = next_account_info(account_info_iter)?; + + let raw_data = &mut data_info.data.borrow_mut(); + if raw_data.len() < RecordData::WRITABLE_START_INDEX { + return Err(ProgramError::InvalidAccountData); + } + + let account_data = bytemuck::try_from_bytes_mut::( + &mut raw_data[..RecordData::WRITABLE_START_INDEX], + ) + .map_err(|_| ProgramError::InvalidArgument)?; + if account_data.is_initialized() { + msg!("Record account already initialized"); + return Err(ProgramError::AccountAlreadyInitialized); + } + + account_data.authority = *authority_info.key; + account_data.version = RecordData::CURRENT_VERSION; + Ok(()) + } + + RecordInstruction::Write { offset, data } => { + msg!("RecordInstruction::Write"); + let data_info = next_account_info(account_info_iter)?; + let authority_info = next_account_info(account_info_iter)?; + { + let raw_data = &data_info.data.borrow(); + if raw_data.len() < RecordData::WRITABLE_START_INDEX { + return Err(ProgramError::InvalidAccountData); + } + let account_data = bytemuck::try_from_bytes::( + &raw_data[..RecordData::WRITABLE_START_INDEX], + ) + .map_err(|_| ProgramError::InvalidArgument)?; + if !account_data.is_initialized() { + msg!("Record account not initialized"); + return Err(ProgramError::UninitializedAccount); + } + check_authority(authority_info, &account_data.authority)?; + } + let start = RecordData::WRITABLE_START_INDEX.saturating_add(offset as usize); + let end = start.saturating_add(data.len()); + if end > data_info.data.borrow().len() { + Err(ProgramError::AccountDataTooSmall) + } else { + data_info.data.borrow_mut()[start..end].copy_from_slice(data); + Ok(()) + } + } + + RecordInstruction::SetAuthority => { + msg!("RecordInstruction::SetAuthority"); + let data_info = next_account_info(account_info_iter)?; + let authority_info = next_account_info(account_info_iter)?; + let new_authority_info = next_account_info(account_info_iter)?; + let raw_data = &mut data_info.data.borrow_mut(); + if raw_data.len() < RecordData::WRITABLE_START_INDEX { + return Err(ProgramError::InvalidAccountData); + } + let account_data = bytemuck::try_from_bytes_mut::( + &mut raw_data[..RecordData::WRITABLE_START_INDEX], + ) + .map_err(|_| ProgramError::InvalidArgument)?; + if !account_data.is_initialized() { + msg!("Record account not initialized"); + return Err(ProgramError::UninitializedAccount); + } + check_authority(authority_info, &account_data.authority)?; + account_data.authority = *new_authority_info.key; + Ok(()) + } + + RecordInstruction::CloseAccount => { + msg!("RecordInstruction::CloseAccount"); + let data_info = next_account_info(account_info_iter)?; + let authority_info = next_account_info(account_info_iter)?; + let destination_info = next_account_info(account_info_iter)?; + let raw_data = &mut data_info.data.borrow_mut(); + if raw_data.len() < RecordData::WRITABLE_START_INDEX { + return Err(ProgramError::InvalidAccountData); + } + let account_data = bytemuck::try_from_bytes_mut::( + &mut raw_data[..RecordData::WRITABLE_START_INDEX], + ) + .map_err(|_| ProgramError::InvalidArgument)?; + if !account_data.is_initialized() { + msg!("Record not initialized"); + return Err(ProgramError::UninitializedAccount); + } + check_authority(authority_info, &account_data.authority)?; + let destination_starting_lamports = destination_info.lamports(); + let data_lamports = data_info.lamports(); + **data_info.lamports.borrow_mut() = 0; + **destination_info.lamports.borrow_mut() = destination_starting_lamports + .checked_add(data_lamports) + .ok_or(RecordError::Overflow)?; + Ok(()) + } + + RecordInstruction::Reallocate { data_length } => { + msg!("RecordInstruction::Reallocate"); + let data_info = next_account_info(account_info_iter)?; + let authority_info = next_account_info(account_info_iter)?; + + { + let raw_data = &mut data_info.data.borrow_mut(); + if raw_data.len() < RecordData::WRITABLE_START_INDEX { + return Err(ProgramError::InvalidAccountData); + } + let account_data = bytemuck::try_from_bytes_mut::( + &mut raw_data[..RecordData::WRITABLE_START_INDEX], + ) + .map_err(|_| ProgramError::InvalidArgument)?; + if !account_data.is_initialized() { + msg!("Record not initialized"); + return Err(ProgramError::UninitializedAccount); + } + check_authority(authority_info, &account_data.authority)?; + } + + // needed account length is the sum of the meta data length and the specified + // data length + let needed_account_length = std::mem::size_of::() + .checked_add( + usize::try_from(data_length).map_err(|_| ProgramError::InvalidArgument)?, + ) + .unwrap(); + + // reallocate + if data_info.data_len() >= needed_account_length { + msg!("no additional reallocation needed"); + return Ok(()); + } + msg!( + "reallocating +{:?} bytes", + needed_account_length + .checked_sub(data_info.data_len()) + .unwrap(), + ); + data_info.realloc(needed_account_length, false)?; + Ok(()) + } + } +} diff --git a/program/src/state.rs b/program/src/state.rs new file mode 100644 index 0000000..0f8e788 --- /dev/null +++ b/program/src/state.rs @@ -0,0 +1,71 @@ +//! Program state +use { + bytemuck::{Pod, Zeroable}, + solana_program_pack::IsInitialized, + solana_pubkey::Pubkey, +}; + +/// Header type for recorded account data +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +pub struct RecordData { + /// Struct version, allows for upgrades to the program + pub version: u8, + + /// The account allowed to update the data + pub authority: Pubkey, +} + +impl RecordData { + /// Version to fill in on new created accounts + pub const CURRENT_VERSION: u8 = 1; + + /// Start of writable account data, after version and authority + pub const WRITABLE_START_INDEX: usize = 33; +} + +impl IsInitialized for RecordData { + /// Is initialized + fn is_initialized(&self) -> bool { + self.version == Self::CURRENT_VERSION + } +} + +#[cfg(test)] +pub mod tests { + use {super::*, solana_program_error::ProgramError}; + + /// Version for tests + pub const TEST_VERSION: u8 = 1; + /// Pubkey for tests + pub const TEST_PUBKEY: Pubkey = Pubkey::new_from_array([100; 32]); + /// Bytes for tests + pub const TEST_BYTES: [u8; 8] = [42; 8]; + /// RecordData for tests + pub const TEST_RECORD_DATA: RecordData = RecordData { + version: TEST_VERSION, + authority: TEST_PUBKEY, + }; + + #[test] + fn serialize_data() { + let mut expected = vec![TEST_VERSION]; + expected.extend_from_slice(&TEST_PUBKEY.to_bytes()); + assert_eq!(bytemuck::bytes_of(&TEST_RECORD_DATA), expected); + assert_eq!( + *bytemuck::try_from_bytes::(&expected).unwrap(), + TEST_RECORD_DATA, + ); + } + + #[test] + fn deserialize_invalid_slice() { + let mut expected = vec![TEST_VERSION]; + expected.extend_from_slice(&TEST_PUBKEY.to_bytes()); + expected.extend_from_slice(&TEST_BYTES); + let err = bytemuck::try_from_bytes::(&expected) + .map_err(|_| ProgramError::InvalidArgument) + .unwrap_err(); + assert_eq!(err, ProgramError::InvalidArgument); + } +} diff --git a/program/tests/functional.rs b/program/tests/functional.rs new file mode 100644 index 0000000..3fe0ef9 --- /dev/null +++ b/program/tests/functional.rs @@ -0,0 +1,698 @@ +#![cfg(feature = "test-sbf")] + +use { + solana_instruction::{error::InstructionError, AccountMeta, Instruction}, + solana_program_test::*, + solana_pubkey::Pubkey, + solana_rent::Rent, + solana_sdk::{ + signature::{Keypair, Signer}, + system_instruction, + transaction::{Transaction, TransactionError}, + }, + spl_record::{ + error::RecordError, id, instruction, processor::process_instruction, state::RecordData, + }, +}; + +fn program_test() -> ProgramTest { + ProgramTest::new("spl_record", id(), processor!(process_instruction)) +} + +async fn initialize_storage_account( + context: &mut ProgramTestContext, + authority: &Keypair, + account: &Keypair, + data: &[u8], +) { + let account_length = std::mem::size_of::() + .checked_add(data.len()) + .unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::create_account( + &context.payer.pubkey(), + &account.pubkey(), + 1.max(Rent::default().minimum_balance(account_length)), + account_length as u64, + &id(), + ), + instruction::initialize(&account.pubkey(), &authority.pubkey()), + instruction::write(&account.pubkey(), &authority.pubkey(), 0, data), + ], + Some(&context.payer.pubkey()), + &[&context.payer, account, authority], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); +} + +#[tokio::test] +async fn initialize_success() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = &[111u8; 8]; + initialize_storage_account(&mut context, &authority, &account, data).await; + + let account = context + .banks_client + .get_account(account.pubkey()) + .await + .unwrap() + .unwrap(); + let account_data = + bytemuck::try_from_bytes::(&account.data[..RecordData::WRITABLE_START_INDEX]) + .unwrap(); + assert_eq!(account_data.authority, authority.pubkey()); + assert_eq!(account_data.version, RecordData::CURRENT_VERSION); + assert_eq!(&account.data[RecordData::WRITABLE_START_INDEX..], data); +} + +#[tokio::test] +async fn initialize_with_seed_success() { + let context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let seed = "storage"; + let account = Pubkey::create_with_seed(&authority.pubkey(), seed, &id()).unwrap(); + let data = &[111u8; 8]; + let account_length = std::mem::size_of::() + .checked_add(data.len()) + .unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::create_account_with_seed( + &context.payer.pubkey(), + &account, + &authority.pubkey(), + seed, + 1.max(Rent::default().minimum_balance(account_length)), + account_length as u64, + &id(), + ), + instruction::initialize(&account, &authority.pubkey()), + instruction::write(&account, &authority.pubkey(), 0, data), + ], + Some(&context.payer.pubkey()), + &[&context.payer, &authority], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + let account = context + .banks_client + .get_account(account) + .await + .unwrap() + .unwrap(); + let account_data = + bytemuck::try_from_bytes::(&account.data[..RecordData::WRITABLE_START_INDEX]) + .unwrap(); + assert_eq!(account_data.authority, authority.pubkey()); + assert_eq!(account_data.version, RecordData::CURRENT_VERSION); + assert_eq!(&account.data[RecordData::WRITABLE_START_INDEX..], data); +} + +#[tokio::test] +async fn initialize_twice_fail() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = &[111u8; 8]; + initialize_storage_account(&mut context, &authority, &account, data).await; + let transaction = Transaction::new_signed_with_payer( + &[instruction::initialize( + &account.pubkey(), + &authority.pubkey(), + )], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(0, InstructionError::AccountAlreadyInitialized) + ); +} + +#[tokio::test] +async fn write_success() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = &[222u8; 8]; + initialize_storage_account(&mut context, &authority, &account, data).await; + + let new_data = &[200u8; 8]; + let transaction = Transaction::new_signed_with_payer( + &[instruction::write( + &account.pubkey(), + &authority.pubkey(), + 0, + new_data, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &authority], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + let account = context + .banks_client + .get_account(account.pubkey()) + .await + .unwrap() + .unwrap(); + let account_data = + bytemuck::try_from_bytes::(&account.data[..RecordData::WRITABLE_START_INDEX]) + .unwrap(); + assert_eq!(account_data.authority, authority.pubkey()); + assert_eq!(account_data.version, RecordData::CURRENT_VERSION); + assert_eq!(&account.data[RecordData::WRITABLE_START_INDEX..], new_data); +} + +#[tokio::test] +async fn write_fail_wrong_authority() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = &[222u8; 8]; + initialize_storage_account(&mut context, &authority, &account, data).await; + + let new_data = &[200u8; 8]; + let wrong_authority = Keypair::new(); + let transaction = Transaction::new_signed_with_payer( + &[instruction::write( + &account.pubkey(), + &wrong_authority.pubkey(), + 0, + new_data, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &wrong_authority], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError( + 0, + InstructionError::Custom(RecordError::IncorrectAuthority as u32) + ) + ); +} + +#[tokio::test] +async fn write_fail_unsigned() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = &[222u8; 8]; + initialize_storage_account(&mut context, &authority, &account, data).await; + + let data = &[200u8; 8]; + + let transaction = Transaction::new_signed_with_payer( + &[Instruction { + program_id: id(), + accounts: vec![ + AccountMeta::new(account.pubkey(), false), + AccountMeta::new_readonly(authority.pubkey(), false), + ], + data: instruction::RecordInstruction::Write { offset: 0, data }.pack(), + }], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) + ); +} + +#[tokio::test] +async fn close_account_success() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = &[222u8; 8]; + let account_length = std::mem::size_of::() + .checked_add(data.len()) + .unwrap(); + initialize_storage_account(&mut context, &authority, &account, data).await; + let recipient = Pubkey::new_unique(); + + let transaction = Transaction::new_signed_with_payer( + &[instruction::close_account( + &account.pubkey(), + &authority.pubkey(), + &recipient, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &authority], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + let account = context + .banks_client + .get_account(recipient) + .await + .unwrap() + .unwrap(); + assert_eq!( + account.lamports, + 1.max(Rent::default().minimum_balance(account_length)) + ); +} + +#[tokio::test] +async fn close_account_fail_wrong_authority() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = &[222u8; 8]; + initialize_storage_account(&mut context, &authority, &account, data).await; + + let wrong_authority = Keypair::new(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction { + program_id: id(), + accounts: vec![ + AccountMeta::new(account.pubkey(), false), + AccountMeta::new_readonly(wrong_authority.pubkey(), true), + AccountMeta::new(Pubkey::new_unique(), false), + ], + data: instruction::RecordInstruction::CloseAccount.pack(), + }], + Some(&context.payer.pubkey()), + &[&context.payer, &wrong_authority], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError( + 0, + InstructionError::Custom(RecordError::IncorrectAuthority as u32) + ) + ); +} + +#[tokio::test] +async fn close_account_fail_unsigned() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = &[222u8, 8]; + initialize_storage_account(&mut context, &authority, &account, data).await; + + let transaction = Transaction::new_signed_with_payer( + &[Instruction { + program_id: id(), + accounts: vec![ + AccountMeta::new(account.pubkey(), false), + AccountMeta::new_readonly(authority.pubkey(), false), + AccountMeta::new(Pubkey::new_unique(), false), + ], + data: instruction::RecordInstruction::CloseAccount.pack(), + }], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) + ); +} + +#[tokio::test] +async fn set_authority_success() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = &[222u8; 8]; + initialize_storage_account(&mut context, &authority, &account, data).await; + let new_authority = Keypair::new(); + + let transaction = Transaction::new_signed_with_payer( + &[instruction::set_authority( + &account.pubkey(), + &authority.pubkey(), + &new_authority.pubkey(), + )], + Some(&context.payer.pubkey()), + &[&context.payer, &authority], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + let account_handle = context + .banks_client + .get_account(account.pubkey()) + .await + .unwrap() + .unwrap(); + let account_data = bytemuck::try_from_bytes::( + &account_handle.data[..RecordData::WRITABLE_START_INDEX], + ) + .unwrap(); + assert_eq!(account_data.authority, new_authority.pubkey()); + + let new_data = &[200u8; 8]; + let transaction = Transaction::new_signed_with_payer( + &[instruction::write( + &account.pubkey(), + &new_authority.pubkey(), + 0, + new_data, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &new_authority], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + let account_handle = context + .banks_client + .get_account(account.pubkey()) + .await + .unwrap() + .unwrap(); + let account_data = bytemuck::try_from_bytes::( + &account_handle.data[..RecordData::WRITABLE_START_INDEX], + ) + .unwrap(); + assert_eq!(account_data.authority, new_authority.pubkey()); + assert_eq!(account_data.version, RecordData::CURRENT_VERSION); + assert_eq!( + &account_handle.data[RecordData::WRITABLE_START_INDEX..], + new_data, + ); +} + +#[tokio::test] +async fn set_authority_fail_wrong_authority() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = &[222u8; 8]; + initialize_storage_account(&mut context, &authority, &account, data).await; + + let wrong_authority = Keypair::new(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction { + program_id: id(), + accounts: vec![ + AccountMeta::new(account.pubkey(), false), + AccountMeta::new_readonly(wrong_authority.pubkey(), true), + AccountMeta::new(Pubkey::new_unique(), false), + ], + data: instruction::RecordInstruction::SetAuthority.pack(), + }], + Some(&context.payer.pubkey()), + &[&context.payer, &wrong_authority], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError( + 0, + InstructionError::Custom(RecordError::IncorrectAuthority as u32) + ) + ); +} + +#[tokio::test] +async fn set_authority_fail_unsigned() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = &[222u8; 8]; + initialize_storage_account(&mut context, &authority, &account, data).await; + + let transaction = Transaction::new_signed_with_payer( + &[Instruction { + program_id: id(), + accounts: vec![ + AccountMeta::new(account.pubkey(), false), + AccountMeta::new_readonly(authority.pubkey(), false), + AccountMeta::new(Pubkey::new_unique(), false), + ], + data: instruction::RecordInstruction::SetAuthority.pack(), + }], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) + ); +} + +#[tokio::test] +async fn reallocate_success() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = &[222u8; 8]; + initialize_storage_account(&mut context, &authority, &account, data).await; + + let new_data_length = 16u64; + let expected_account_data_length = RecordData::WRITABLE_START_INDEX + .checked_add(new_data_length as usize) + .unwrap(); + + let delta_account_data_length = new_data_length.saturating_sub(data.len() as u64); + let additional_lamports_needed = + Rent::default().minimum_balance(delta_account_data_length as usize); + + let transaction = Transaction::new_signed_with_payer( + &[ + instruction::reallocate(&account.pubkey(), &authority.pubkey(), new_data_length), + system_instruction::transfer( + &context.payer.pubkey(), + &account.pubkey(), + additional_lamports_needed, + ), + ], + Some(&context.payer.pubkey()), + &[&context.payer, &authority], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + let account_handle = context + .banks_client + .get_account(account.pubkey()) + .await + .unwrap() + .unwrap(); + + assert_eq!(account_handle.data.len(), expected_account_data_length); + + // reallocate to a smaller length + let old_data_length = 8u64; + let transaction = Transaction::new_signed_with_payer( + &[instruction::reallocate( + &account.pubkey(), + &authority.pubkey(), + old_data_length, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &authority], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + let account = context + .banks_client + .get_account(account.pubkey()) + .await + .unwrap() + .unwrap(); + + assert_eq!(account.data.len(), expected_account_data_length); +} + +#[tokio::test] +async fn reallocate_fail_wrong_authority() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = &[222u8; 8]; + initialize_storage_account(&mut context, &authority, &account, data).await; + + let new_data_length = 16u64; + let delta_account_data_length = new_data_length.saturating_sub(data.len() as u64); + let additional_lamports_needed = + Rent::default().minimum_balance(delta_account_data_length as usize); + + let wrong_authority = Keypair::new(); + let transaction = Transaction::new_signed_with_payer( + &[ + Instruction { + program_id: id(), + accounts: vec![ + AccountMeta::new(account.pubkey(), false), + AccountMeta::new(wrong_authority.pubkey(), true), + ], + data: instruction::RecordInstruction::Reallocate { + data_length: new_data_length, + } + .pack(), + }, + system_instruction::transfer( + &context.payer.pubkey(), + &account.pubkey(), + additional_lamports_needed, + ), + ], + Some(&context.payer.pubkey()), + &[&context.payer, &wrong_authority], + context.last_blockhash, + ); + + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError( + 0, + InstructionError::Custom(RecordError::IncorrectAuthority as u32) + ) + ); +} + +#[tokio::test] +async fn reallocate_fail_unsigned() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = &[222u8; 8]; + initialize_storage_account(&mut context, &authority, &account, data).await; + + let new_data_length = 16u64; + let delta_account_data_length = new_data_length.saturating_sub(data.len() as u64); + let additional_lamports_needed = + Rent::default().minimum_balance(delta_account_data_length as usize); + + let transaction = Transaction::new_signed_with_payer( + &[ + Instruction { + program_id: id(), + accounts: vec![ + AccountMeta::new(account.pubkey(), false), + AccountMeta::new(authority.pubkey(), false), + ], + data: instruction::RecordInstruction::Reallocate { + data_length: new_data_length, + } + .pack(), + }, + system_instruction::transfer( + &context.payer.pubkey(), + &account.pubkey(), + additional_lamports_needed, + ), + ], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) + ); +}