Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit e08f30b

Browse files
[spl-record] Add Reallocate instruction (#6063)
* add `Reallocate` instruction * add tests for `Reallocate` instruction * cargo fmt * clippy * remove lamport transfer logic
1 parent baf1bc3 commit e08f30b

File tree

3 files changed

+273
-3
lines changed

3 files changed

+273
-3
lines changed

record/program/src/instruction.rs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,19 +52,35 @@ pub enum RecordInstruction<'a> {
5252
/// 1. `[signer]` Record authority
5353
/// 2. `[]` Receiver of account lamports
5454
CloseAccount,
55+
56+
/// Reallocate additional space in a record account
57+
///
58+
/// If the record account already has enough space to hold the specified
59+
/// data length, then the instruction does nothing.
60+
///
61+
/// Accounts expected by this instruction:
62+
///
63+
/// 0. `[writable]` The record account to reallocate
64+
/// 1. `[signer]` The account's owner
65+
Reallocate {
66+
/// The length of the data to hold in the record account excluding meta
67+
/// data
68+
data_length: u64,
69+
},
5570
}
5671

5772
impl<'a> RecordInstruction<'a> {
5873
/// Unpacks a byte buffer into a [RecordInstruction].
5974
pub fn unpack(input: &'a [u8]) -> Result<Self, ProgramError> {
75+
const U32_BYTES: usize = 4;
76+
const U64_BYTES: usize = 8;
77+
6078
let (&tag, rest) = input
6179
.split_first()
6280
.ok_or(ProgramError::InvalidInstructionData)?;
6381
Ok(match tag {
6482
0 => Self::Initialize,
6583
1 => {
66-
const U32_BYTES: usize = 4;
67-
const U64_BYTES: usize = 8;
6884
let offset = rest
6985
.get(..U64_BYTES)
7086
.and_then(|slice| slice.try_into().ok())
@@ -84,6 +100,15 @@ impl<'a> RecordInstruction<'a> {
84100
}
85101
2 => Self::SetAuthority,
86102
3 => Self::CloseAccount,
103+
4 => {
104+
let data_length = rest
105+
.get(..U64_BYTES)
106+
.and_then(|slice| slice.try_into().ok())
107+
.map(u64::from_le_bytes)
108+
.ok_or(ProgramError::InvalidInstructionData)?;
109+
110+
Self::Reallocate { data_length }
111+
}
87112
_ => return Err(ProgramError::InvalidInstructionData),
88113
})
89114
}
@@ -101,6 +126,10 @@ impl<'a> RecordInstruction<'a> {
101126
}
102127
Self::SetAuthority => buf.push(2),
103128
Self::CloseAccount => buf.push(3),
129+
Self::Reallocate { data_length } => {
130+
buf.push(4);
131+
buf.extend_from_slice(&data_length.to_le_bytes());
132+
}
104133
};
105134
buf
106135
}
@@ -160,6 +189,18 @@ pub fn close_account(record_account: &Pubkey, signer: &Pubkey, receiver: &Pubkey
160189
}
161190
}
162191

192+
/// Create a `RecordInstruction::Reallocate` instruction
193+
pub fn reallocate(record_account: &Pubkey, signer: &Pubkey, data_length: u64) -> Instruction {
194+
Instruction {
195+
program_id: id(),
196+
accounts: vec![
197+
AccountMeta::new(*record_account, false),
198+
AccountMeta::new_readonly(*signer, true),
199+
],
200+
data: RecordInstruction::Reallocate { data_length }.pack(),
201+
}
202+
}
203+
163204
#[cfg(test)]
164205
mod tests {
165206
use {super::*, crate::state::tests::TEST_BYTES, solana_program::program_error::ProgramError};
@@ -201,6 +242,16 @@ mod tests {
201242
assert_eq!(RecordInstruction::unpack(&expected).unwrap(), instruction);
202243
}
203244

245+
#[test]
246+
fn serialize_reallocate() {
247+
let data_length = 16u64;
248+
let instruction = RecordInstruction::Reallocate { data_length };
249+
let mut expected = vec![4];
250+
expected.extend_from_slice(&data_length.to_le_bytes());
251+
assert_eq!(instruction.pack(), expected);
252+
assert_eq!(RecordInstruction::unpack(&expected).unwrap(), instruction);
253+
}
254+
204255
#[test]
205256
fn deserialize_invalid_instruction() {
206257
let mut expected = vec![12];

record/program/src/processor.rs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use {
1010
program_pack::IsInitialized,
1111
pubkey::Pubkey,
1212
},
13-
spl_pod::bytemuck::{pod_from_bytes, pod_from_bytes_mut},
13+
spl_pod::bytemuck::{pod_from_bytes, pod_from_bytes_mut, pod_get_packed_len},
1414
};
1515

1616
fn check_authority(authority_info: &AccountInfo, expected_authority: &Pubkey) -> ProgramResult {
@@ -132,5 +132,48 @@ pub fn process_instruction(
132132
.ok_or(RecordError::Overflow)?;
133133
Ok(())
134134
}
135+
136+
RecordInstruction::Reallocate { data_length } => {
137+
msg!("RecordInstruction::Reallocate");
138+
let data_info = next_account_info(account_info_iter)?;
139+
let authority_info = next_account_info(account_info_iter)?;
140+
141+
{
142+
let raw_data = &mut data_info.data.borrow_mut();
143+
if raw_data.len() < RecordData::WRITABLE_START_INDEX {
144+
return Err(ProgramError::InvalidAccountData);
145+
}
146+
let account_data = pod_from_bytes_mut::<RecordData>(
147+
&mut raw_data[..RecordData::WRITABLE_START_INDEX],
148+
)?;
149+
if !account_data.is_initialized() {
150+
msg!("Record not initialized");
151+
return Err(ProgramError::UninitializedAccount);
152+
}
153+
check_authority(authority_info, &account_data.authority)?;
154+
}
155+
156+
// needed account length is the sum of the meta data length and the specified
157+
// data length
158+
let needed_account_length = pod_get_packed_len::<RecordData>()
159+
.checked_add(
160+
usize::try_from(data_length).map_err(|_| ProgramError::InvalidArgument)?,
161+
)
162+
.unwrap();
163+
164+
// reallocate
165+
if data_info.data_len() >= needed_account_length {
166+
msg!("no additional reallocation needed");
167+
return Ok(());
168+
}
169+
msg!(
170+
"reallocating +{:?} bytes",
171+
needed_account_length
172+
.checked_sub(data_info.data_len())
173+
.unwrap(),
174+
);
175+
data_info.realloc(needed_account_length, false)?;
176+
Ok(())
177+
}
135178
}
136179
}

record/program/tests/functional.rs

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,3 +518,179 @@ async fn set_authority_fail_unsigned() {
518518
TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature)
519519
);
520520
}
521+
522+
#[tokio::test]
523+
async fn reallocate_success() {
524+
let mut context = program_test().start_with_context().await;
525+
526+
let authority = Keypair::new();
527+
let account = Keypair::new();
528+
let data = &[222u8; 8];
529+
initialize_storage_account(&mut context, &authority, &account, data).await;
530+
531+
let new_data_length = 16u64;
532+
let expected_account_data_length = RecordData::WRITABLE_START_INDEX
533+
.checked_add(new_data_length as usize)
534+
.unwrap();
535+
536+
let delta_account_data_length = new_data_length.saturating_sub(data.len() as u64);
537+
let additional_lamports_needed =
538+
Rent::default().minimum_balance(delta_account_data_length as usize);
539+
540+
let transaction = Transaction::new_signed_with_payer(
541+
&[
542+
instruction::reallocate(&account.pubkey(), &authority.pubkey(), new_data_length),
543+
system_instruction::transfer(
544+
&context.payer.pubkey(),
545+
&account.pubkey(),
546+
additional_lamports_needed,
547+
),
548+
],
549+
Some(&context.payer.pubkey()),
550+
&[&context.payer, &authority],
551+
context.last_blockhash,
552+
);
553+
context
554+
.banks_client
555+
.process_transaction(transaction)
556+
.await
557+
.unwrap();
558+
559+
let account_handle = context
560+
.banks_client
561+
.get_account(account.pubkey())
562+
.await
563+
.unwrap()
564+
.unwrap();
565+
566+
assert_eq!(account_handle.data.len(), expected_account_data_length);
567+
568+
// reallocate to a smaller length
569+
let old_data_length = 8u64;
570+
let transaction = Transaction::new_signed_with_payer(
571+
&[instruction::reallocate(
572+
&account.pubkey(),
573+
&authority.pubkey(),
574+
old_data_length,
575+
)],
576+
Some(&context.payer.pubkey()),
577+
&[&context.payer, &authority],
578+
context.last_blockhash,
579+
);
580+
context
581+
.banks_client
582+
.process_transaction(transaction)
583+
.await
584+
.unwrap();
585+
586+
let account = context
587+
.banks_client
588+
.get_account(account.pubkey())
589+
.await
590+
.unwrap()
591+
.unwrap();
592+
593+
assert_eq!(account.data.len(), expected_account_data_length);
594+
}
595+
596+
#[tokio::test]
597+
async fn reallocate_fail_wrong_authority() {
598+
let mut context = program_test().start_with_context().await;
599+
600+
let authority = Keypair::new();
601+
let account = Keypair::new();
602+
let data = &[222u8; 8];
603+
initialize_storage_account(&mut context, &authority, &account, data).await;
604+
605+
let new_data_length = 16u64;
606+
let delta_account_data_length = new_data_length.saturating_sub(data.len() as u64);
607+
let additional_lamports_needed =
608+
Rent::default().minimum_balance(delta_account_data_length as usize);
609+
610+
let wrong_authority = Keypair::new();
611+
let transaction = Transaction::new_signed_with_payer(
612+
&[
613+
Instruction {
614+
program_id: id(),
615+
accounts: vec![
616+
AccountMeta::new(account.pubkey(), false),
617+
AccountMeta::new(wrong_authority.pubkey(), true),
618+
],
619+
data: instruction::RecordInstruction::Reallocate {
620+
data_length: new_data_length,
621+
}
622+
.pack(),
623+
},
624+
system_instruction::transfer(
625+
&context.payer.pubkey(),
626+
&account.pubkey(),
627+
additional_lamports_needed,
628+
),
629+
],
630+
Some(&context.payer.pubkey()),
631+
&[&context.payer, &wrong_authority],
632+
context.last_blockhash,
633+
);
634+
635+
assert_eq!(
636+
context
637+
.banks_client
638+
.process_transaction(transaction)
639+
.await
640+
.unwrap_err()
641+
.unwrap(),
642+
TransactionError::InstructionError(
643+
0,
644+
InstructionError::Custom(RecordError::IncorrectAuthority as u32)
645+
)
646+
);
647+
}
648+
649+
#[tokio::test]
650+
async fn reallocate_fail_unsigned() {
651+
let mut context = program_test().start_with_context().await;
652+
653+
let authority = Keypair::new();
654+
let account = Keypair::new();
655+
let data = &[222u8; 8];
656+
initialize_storage_account(&mut context, &authority, &account, data).await;
657+
658+
let new_data_length = 16u64;
659+
let delta_account_data_length = new_data_length.saturating_sub(data.len() as u64);
660+
let additional_lamports_needed =
661+
Rent::default().minimum_balance(delta_account_data_length as usize);
662+
663+
let transaction = Transaction::new_signed_with_payer(
664+
&[
665+
Instruction {
666+
program_id: id(),
667+
accounts: vec![
668+
AccountMeta::new(account.pubkey(), false),
669+
AccountMeta::new(authority.pubkey(), false),
670+
],
671+
data: instruction::RecordInstruction::Reallocate {
672+
data_length: new_data_length,
673+
}
674+
.pack(),
675+
},
676+
system_instruction::transfer(
677+
&context.payer.pubkey(),
678+
&account.pubkey(),
679+
additional_lamports_needed,
680+
),
681+
],
682+
Some(&context.payer.pubkey()),
683+
&[&context.payer],
684+
context.last_blockhash,
685+
);
686+
687+
assert_eq!(
688+
context
689+
.banks_client
690+
.process_transaction(transaction)
691+
.await
692+
.unwrap_err()
693+
.unwrap(),
694+
TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature)
695+
);
696+
}

0 commit comments

Comments
 (0)