Skip to content

Commit

Permalink
[spl-record] Remove borsh dependency from spl-record program (#6054)
Browse files Browse the repository at this point in the history
  • Loading branch information
samkim-crypto authored Jan 4, 2024
1 parent ac239e5 commit f36c2fb
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 138 deletions.
3 changes: 2 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion record/program/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ no-entrypoint = []
test-sbf = []

[dependencies]
borsh = "0.10"
bytemuck = { version = "1.14.0", features = ["derive"] }
num-derive = "0.4"
num-traits = "0.2"
solana-program = "1.17.6"
thiserror = "1.0"
spl-pod = { version = "0.1", path = "../../libraries/pod" }

[dev-dependencies]
solana-program-test = "1.17.6"
Expand Down
154 changes: 97 additions & 57 deletions record/program/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@
use {
crate::id,
borsh::{BorshDeserialize, BorshSerialize},
solana_program::{
instruction::{AccountMeta, Instruction},
program_error::ProgramError,
pubkey::Pubkey,
},
std::mem::size_of,
};

/// Instructions supported by the program
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, PartialEq)]
pub enum RecordInstruction {
#[derive(Clone, Debug, PartialEq)]
pub enum RecordInstruction<'a> {
/// Create a new record
///
/// Accounts expected by this instruction:
Expand All @@ -30,7 +31,7 @@ pub enum RecordInstruction {
/// Offset to start writing record, expressed as `u64`.
offset: u64,
/// Data to replace the existing record data
data: Vec<u8>,
data: &'a [u8],
},

/// Update the authority of the provided record account
Expand All @@ -53,28 +54,80 @@ pub enum RecordInstruction {
CloseAccount,
}

impl<'a> RecordInstruction<'a> {
/// Unpacks a byte buffer into a [RecordInstruction].
pub fn unpack(input: &'a [u8]) -> Result<Self, ProgramError> {
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())
.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,
_ => return Err(ProgramError::InvalidInstructionData),
})
}

/// Packs a [RecordInstruction] into a byte buffer.
pub fn pack(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(size_of::<Self>());
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),
};
buf
}
}

/// Create a `RecordInstruction::Initialize` instruction
pub fn initialize(record_account: &Pubkey, authority: &Pubkey) -> Instruction {
Instruction::new_with_borsh(
id(),
&RecordInstruction::Initialize,
vec![
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: Vec<u8>) -> Instruction {
Instruction::new_with_borsh(
id(),
&RecordInstruction::Write { offset, data },
vec![
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
Expand All @@ -83,92 +136,79 @@ pub fn set_authority(
signer: &Pubkey,
new_authority: &Pubkey,
) -> Instruction {
Instruction::new_with_borsh(
id(),
&RecordInstruction::SetAuthority,
vec![
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::new_with_borsh(
id(),
&RecordInstruction::CloseAccount,
vec![
Instruction {
program_id: id(),
accounts: vec![
AccountMeta::new(*record_account, false),
AccountMeta::new_readonly(*signer, true),
AccountMeta::new(*receiver, false),
],
)
data: RecordInstruction::CloseAccount.pack(),
}
}

#[cfg(test)]
mod tests {
use {super::*, crate::state::tests::TEST_DATA, solana_program::program_error::ProgramError};
use {
super::*, crate::state::tests::TEST_DATA, solana_program::program_error::ProgramError,
spl_pod::bytemuck::pod_bytes_of,
};

#[test]
fn serialize_initialize() {
let instruction = RecordInstruction::Initialize;
let expected = vec![0];
assert_eq!(instruction.try_to_vec().unwrap(), expected);
assert_eq!(
RecordInstruction::try_from_slice(&expected).unwrap(),
instruction
);
assert_eq!(instruction.pack(), expected);
assert_eq!(RecordInstruction::unpack(&expected).unwrap(), instruction);
}

#[test]
fn serialize_write() {
let data = TEST_DATA.try_to_vec().unwrap();
let data = pod_bytes_of(&TEST_DATA);
let offset = 0u64;
let instruction = RecordInstruction::Write {
offset: 0,
data: data.clone(),
};
let instruction = RecordInstruction::Write { offset: 0, data };
let mut expected = vec![1];
expected.extend_from_slice(&offset.to_le_bytes());
expected.append(&mut data.try_to_vec().unwrap());
assert_eq!(instruction.try_to_vec().unwrap(), expected);
assert_eq!(
RecordInstruction::try_from_slice(&expected).unwrap(),
instruction
);
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.try_to_vec().unwrap(), expected);
assert_eq!(
RecordInstruction::try_from_slice(&expected).unwrap(),
instruction
);
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.try_to_vec().unwrap(), expected);
assert_eq!(
RecordInstruction::try_from_slice(&expected).unwrap(),
instruction
);
assert_eq!(instruction.pack(), expected);
assert_eq!(RecordInstruction::unpack(&expected).unwrap(), instruction);
}

#[test]
fn deserialize_invalid_instruction() {
let mut expected = vec![12];
expected.append(&mut TEST_DATA.try_to_vec().unwrap());
let err: ProgramError = RecordInstruction::try_from_slice(&expected)
.unwrap_err()
.into();
assert!(matches!(err, ProgramError::BorshIoError(_)));
expected.append(&mut pod_bytes_of(&TEST_DATA).to_vec());
let err: ProgramError = RecordInstruction::unpack(&expected).unwrap_err();
assert_eq!(err, ProgramError::InvalidInstructionData);
}
}
37 changes: 20 additions & 17 deletions record/program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use {
instruction::RecordInstruction,
state::{Data, RecordData},
},
borsh::BorshDeserialize,
solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
Expand All @@ -15,6 +14,7 @@ use {
program_pack::IsInitialized,
pubkey::Pubkey,
},
spl_pod::bytemuck::{pod_from_bytes, pod_from_bytes_mut},
};

fn check_authority(authority_info: &AccountInfo, expected_authority: &Pubkey) -> ProgramResult {
Expand All @@ -35,7 +35,7 @@ pub fn process_instruction(
accounts: &[AccountInfo],
input: &[u8],
) -> ProgramResult {
let instruction = RecordInstruction::try_from_slice(input)?;
let instruction = RecordInstruction::unpack(input)?;
let account_info_iter = &mut accounts.iter();

match instruction {
Expand All @@ -45,34 +45,37 @@ pub fn process_instruction(
let data_info = next_account_info(account_info_iter)?;
let authority_info = next_account_info(account_info_iter)?;

let mut account_data = RecordData::try_from_slice(*data_info.data.borrow())?;
let raw_data = &mut data_info.data.borrow_mut();
let account_data = pod_from_bytes_mut::<RecordData>(raw_data)?;
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;
borsh::to_writer(&mut data_info.data.borrow_mut()[..], &account_data)
.map_err(|e| e.into())
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 account_data = RecordData::try_from_slice(&data_info.data.borrow())?;
if !account_data.is_initialized() {
msg!("Record account not initialized");
return Err(ProgramError::UninitializedAccount);
{
let raw_data = &data_info.data.borrow();
let account_data = pod_from_bytes::<RecordData>(raw_data)?;
if !account_data.is_initialized() {
msg!("Record account not initialized");
return Err(ProgramError::UninitializedAccount);
}
check_authority(authority_info, &account_data.authority)?;
}
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);
data_info.data.borrow_mut()[start..end].copy_from_slice(data);
Ok(())
}
}
Expand All @@ -82,23 +85,24 @@ pub fn process_instruction(
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 mut account_data = RecordData::try_from_slice(&data_info.data.borrow())?;
let raw_data = &mut data_info.data.borrow_mut();
let account_data = pod_from_bytes_mut::<RecordData>(raw_data)?;
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;
borsh::to_writer(&mut data_info.data.borrow_mut()[..], &account_data)
.map_err(|e| e.into())
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 mut account_data = RecordData::try_from_slice(&data_info.data.borrow())?;
let raw_data = &mut data_info.data.borrow_mut();
let account_data = pod_from_bytes_mut::<RecordData>(raw_data)?;
if !account_data.is_initialized() {
msg!("Record not initialized");
return Err(ProgramError::UninitializedAccount);
Expand All @@ -111,8 +115,7 @@ pub fn process_instruction(
.checked_add(data_lamports)
.ok_or(RecordError::Overflow)?;
account_data.data = Data::default();
borsh::to_writer(&mut data_info.data.borrow_mut()[..], &account_data)
.map_err(|e| e.into())
Ok(())
}
}
}
Loading

0 comments on commit f36c2fb

Please sign in to comment.