Skip to content

Commit

Permalink
[spl-record] Add Reallocate instruction (solana-labs#6063)
Browse files Browse the repository at this point in the history
* add `Reallocate` instruction

* add tests for `Reallocate` instruction

* cargo fmt

* clippy

* remove lamport transfer logic
  • Loading branch information
samkim-crypto authored Jan 6, 2024
1 parent baf1bc3 commit e08f30b
Show file tree
Hide file tree
Showing 3 changed files with 273 additions and 3 deletions.
55 changes: 53 additions & 2 deletions record/program/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,35 @@ pub enum RecordInstruction<'a> {
/// 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<Self, ProgramError> {
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 => {
const U32_BYTES: usize = 4;
const U64_BYTES: usize = 8;
let offset = rest
.get(..U64_BYTES)
.and_then(|slice| slice.try_into().ok())
Expand All @@ -84,6 +100,15 @@ impl<'a> RecordInstruction<'a> {
}
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),
})
}
Expand All @@ -101,6 +126,10 @@ impl<'a> RecordInstruction<'a> {
}
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
}
Expand Down Expand Up @@ -160,6 +189,18 @@ pub fn close_account(record_account: &Pubkey, signer: &Pubkey, receiver: &Pubkey
}
}

/// 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::program_error::ProgramError};
Expand Down Expand Up @@ -201,6 +242,16 @@ mod tests {
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];
Expand Down
45 changes: 44 additions & 1 deletion record/program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use {
program_pack::IsInitialized,
pubkey::Pubkey,
},
spl_pod::bytemuck::{pod_from_bytes, pod_from_bytes_mut},
spl_pod::bytemuck::{pod_from_bytes, pod_from_bytes_mut, pod_get_packed_len},
};

fn check_authority(authority_info: &AccountInfo, expected_authority: &Pubkey) -> ProgramResult {
Expand Down Expand Up @@ -132,5 +132,48 @@ pub fn process_instruction(
.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 = pod_from_bytes_mut::<RecordData>(
&mut raw_data[..RecordData::WRITABLE_START_INDEX],
)?;
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 = pod_get_packed_len::<RecordData>()
.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(())
}
}
}
176 changes: 176 additions & 0 deletions record/program/tests/functional.rs
Original file line number Diff line number Diff line change
Expand Up @@ -518,3 +518,179 @@ async fn set_authority_fail_unsigned() {
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)
);
}

0 comments on commit e08f30b

Please sign in to comment.