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

[spl-record] Remove borsh dependency from spl-record program #6054

Merged
merged 6 commits into from
Jan 4, 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
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the bytes are just written directly, this doesn't adhere to the borsh encoding scheme, which prefaces a variable-length type with its size as a u32. I think it should be fine (and it gives us 4 extra bytes!), but it might confuse some people.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good point. I added the vector length in the serialization.

}
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