Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[spl-record] Add Reallocate instruction #6063

Merged
merged 5 commits into from
Jan 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
);
}