Focus mode
Time to make sure no one can mess with our Solana Movie Database (SMDB) program. We'll add some basic security measures, do some input validation, and add a update_movie_review
instruction.
I'll get you started in a single click, check out this Playground setup.
The complete file structure is as follows:
Note changes in starter code compared to ending of “State Management”In processor.rs
:
account_len
in the add_movie_review
function is changed to a fixed size of 1000
// from this
let account_len: usize = 1 + 1 + (4 + title.len()) + (4 + description.len());
// to this
let account_len: usize = 1000;
In state.rs
is_initialized
function that checks the is_initialized
field on the MovieAccountState
struct.Sealed
for MovieAccountState
, which specifies that MovieAccountState
has a known size and provides for some compiler optimizations.// inside state.rs
impl Sealed for MovieAccountState {}
impl IsInitialized for MovieAccountState {
fn is_initialized(&self) -> bool {
self.is_initialized
}
}
Let's start with some custom errors!
We'll need errors that we can use in the following situations:
In error.rs
:
ReviewError
enumProgramError
// inside error.rs
use solana_program::{program_error::ProgramError};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ReviewError{
// Error 0
#[error("Account not initialized yet")]
UninitializedAccount,
// Error 1
#[error("PDA derived does not equal PDA passed in")]
InvalidPDA,
// Error 2
#[error("Input data exceeds max length")]
InvalidDataLength,
// Error 3
#[error("Rating greater than 5 or less than 1")]
InvalidRating,
}
impl From<ReviewError> for ProgramError {
fn from(e: ReviewError) -> Self {
ProgramError::Custom(e as u32)
}
}
Head over to processor.rs
and bring ReviewError
into scope.
// inside processor.rs
use crate::error::ReviewError;
Next we'll add security checks to the add_movie_review
function.
Signer check
initializer
of a review is also a signer on the transaction.let account_info_iter = &mut accounts.iter();
let initializer = next_account_info(account_info_iter)?;
let pda_account = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;
// add check here
if !initializer.is_signer {
msg!("Missing required signature");
return Err(ProgramError::MissingRequiredSignature)
}
Account validation
pda_account
passed in by the user is the pda
we expectlet (pda, bump_seed) = Pubkey::find_program_address(&[initializer.key.as_ref(), title.as_bytes().as_ref(),], program_id);
if pda != *pda_account.key {
msg!("Invalid seeds for PDA");
return Err(ProgramError::InvalidArgument)
}
Data validation
rating
falls within the 1 to 5 scale. We don't want 0 or 69 star reviews lolif rating > 5 || rating < 1 {
msg!("Rating cannot be higher than 5");
return Err(ReviewError::InvalidRating.into())
}
let total_len: usize = 1 + 1 + (4 + title.len()) + (4 + description.len());
if total_len > 1000 {
msg!("Data length is larger than 1000 bytes");
return Err(ReviewError::InvalidDataLength.into())
}
And now for the fun part! Let's add the update_movie_review
instruction.
We'll start by updating the MovieInstruction
enum in the instruction.rs
file:
// inside instruction.rs
pub enum MovieInstruction {
AddMovieReview {
title: String,
rating: u8,
description: String
},
UpdateMovieReview {
title: String,
rating: u8,
description: String
}
}
The Payload struct can stay the same since aside from the variant type, the instruction data is the same as what we used for AddMovieReview
.
We'll also need to add this new variant to the unpack
function in the same file:
// inside instruction.rs
impl MovieInstruction {
pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
let (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;
let payload = MovieReviewPayload::try_from_slice(rest).unwrap();
Ok(match variant {
0 => Self::AddMovieReview {
title: payload.title,
rating: payload.rating,
description: payload.description },
1 => Self::UpdateMovieReview {
title: payload.title,
rating: payload.rating,
description: payload.description },
_ => return Err(ProgramError::InvalidInstructionData)
})
}
}
Finally, let's add the update_movie_review
to the match statement in the process_instruction
function:
// inside processor.rs
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8]
) -> ProgramResult {
// unpack instruction data
let instruction = MovieInstruction::unpack(instruction_data)?;
match instruction {
MovieInstruction::AddMovieReview { title, rating, description } => {
add_movie_review(program_id, accounts, title, rating, description)
},
// add UpdateMovieReview to match against our new data structure
MovieInstruction::UpdateMovieReview { title, rating, description } => {
// make call to update function that we'll define next
update_movie_review(program_id, accounts, title, rating, description)
}
}
}
Quick recap of all the places we had to update to add a new instruction:
And now we're ready to write the actual update_movie_review
function!
We'll start by iterating through the accounts:
pub fn update_movie_review(
program_id: &Pubkey,
accounts: &[AccountInfo],
_title: String,
rating: u8,
description: String
) -> ProgramResult {
msg!("Updating movie review...");
// Get Account iterator
let account_info_iter = &mut accounts.iter();
// Get accounts
let initializer = next_account_info(account_info_iter)?;
let pda_account = next_account_info(account_info_iter)?;
Ok(())
}
This is a good time to check that the pda_account.owner
is the same as the program_id
:
if pda_account.owner != program_id {
return Err(ProgramError::IllegalOwner)
}
Next we'll check that the signer is the same as the initializer:
if !initializer.is_signer {
msg!("Missing required signature");
return Err(ProgramError::MissingRequiredSignature)
}
Now we can unpack the data from the pda_account
:
msg!("unpacking state account");
let mut account_data = try_from_slice_unchecked::<MovieAccountState>(&pda_account.data.borrow()).unwrap();
msg!("borrowed account data");
Final bit of validation on this fresh new data:
// Derive PDA and check that it matches client
let (pda, _bump_seed) = Pubkey::find_program_address(&[initializer.key.as_ref(), account_data.title.as_bytes().as_ref(),], program_id);
if pda != *pda_account.key {
msg!("Invalid seeds for PDA");
return Err(ReviewError::InvalidPDA.into())
}
if !account_data.is_initialized() {
msg!("Account is not initialized");
return Err(ReviewError::UninitializedAccount.into());
}
if rating > 5 || rating < 1 {
msg!("Rating cannot be higher than 5");
return Err(ReviewError::InvalidRating.into())
}
let total_len: usize = 1 + 1 + (4 + account_data.title.len()) + (4 + description.len());
if total_len > 1000 {
msg!("Data length is larger than 1000 bytes");
return Err(ReviewError::InvalidDataLength.into())
}
Phew, that's a whole lotta checks. Got me feeling like a bank teller lol
The last bit is to update the account info and serialize it to account:
account_data.rating = rating;
account_data.description = description;
account_data.serialize(&mut &mut pda_account.data.borrow_mut()[..])?;
WE DID IT! We've added a new instruction to our program and made it hella secure. Let's test it out!
BUILD -> UPGRADE -> COPY ADDRESS -> PASTE INTO FRONT-END
git clone https://github.com/buildspace/solana-movie-frontend/
cd solana-movie-frontend
git checkout solution-update-reviews
npm install
Your front-end should now be displaying reviews! You can add a review and update your old ones!
Now it’s your turn to build something independently by building on top of the Student Intro program that you've used in previous lessons.
The Student Intro program is a Solana Program that lets students introduce themselves. The program takes a user's name and a short message as the instruction_data
and creates an account to store the data on-chain.
Using what you've learned in this lesson, try applying what you've learned to the Student Intro Program. The program should:
Feel free to use this starter code.
Try to do this independently if you can! But if you get stuck, feel free to reference the solution code. Note that your code may look slightly different than the solution code depending on the checks you implement and the errors you write.
Programs to Accelerate Your Progress in a Software Career
Join our 4-8 month intensive Patika+ bootcamps, start with the fundamentals and gain comprehensive knowledge to kickstart your software career!
You need to enroll in the course to be able to comment!