How to implement account checks and validate instruction data.
program_id
solana_program
crate provides a ProgramError
enum with a list of
generic errors we can use, it will often be useful to create your own. Your
custom errors will be able to provide more context and detail while you’re
debugging your code.
We can define our own errors by creating an enum type listing the errors we want
to use. For example, the NoteError
contains variants Forbidden
and
InvalidLength
. The enum is made into a Rust Error
type by using the derive
attribute macro to implement the Error
trait from the thiserror
library.
Each error type also has its own #[error("...")]
notation. This lets you
provide an error message for each particular error type.
ProgramError
from the solana_program
crate. That means we won’t be able to return our
custom error unless we have a way to convert it into this type. The following
implementation handles conversion between our custom error and the
ProgramError
type.
into()
method to
convert the error into an instance of ProgramError
.
update
instruction, they also provide a pda_account
.
We presume the provided pda_account
is for the particular note they want to
update, but the user can input any instruction data they want. They could even
potentially send data which matches the data format of a note account but was
not also created by the note-taking program. This security vulnerability is one
potential way to introduce malicious code.
The simplest way to avoid this problem is to always check that the owner of an
account is the public key you expect it to be. In this case, we expect the note
account to be a PDA account owned by the program itself. When this is not the
case, we can report it as an error accordingly.
update
instruction.
Otherwise, anyone can update another user’s notes by simply passing in the
user’s public key as the initializer.
u8
only supports numbers 0-255, so the result of
addition that would be 256 would actually be 0, 257 would be 1, etc.
This is always important to keep in mind, but especially so when dealing with
any code that represents true value, such as depositing and withdrawing tokens.
To avoid integer overflow and underflow, either:
checked_add
instead of +
lib.rs
was getting rather large and unwieldy, we’ve separated its code into 3
files: lib.rs
, entrypoint.rs
, and processor.rs
. lib.rs
now only
registers the code’s modules, entrypoint.rs
only defines and sets the
program’s entrypoint, and processor.rs
handles the program logic for
processing instructions. We’ve also added an error.rs
file where we’ll be
defining custom errors. The complete file structure is as follows:
account_len
in the add_movie_review
function (now in processor.rs
). Instead of
calculating the size of the review and setting the account length to only as
large as it needs to be, we’re simply going to allocate 1000 bytes to each
review account. This way, we don’t have to worry about reallocating size or
re-calculating rent when a user updates their movie review.
We went from this:
MovieAccountState
struct in state.rs
using the impl
keyword.
For our movie reviews, we want the ability to check whether an account has
already been initialized. To do this, we create an is_initialized
function
that checks the is_initialized
field on the MovieAccountState
struct.
Sealed
is Nexis Native Chain’s version of Rust’s Sized
trait. This simply specifies that
MovieAccountState
has a known size and provides for some compiler
optimizations.
error.rs
file. Open that file and add
errors for each of the above cases.
ProgramError
type as
needed.
Before moving on, let’s bring ReviewError
into scope in the processor.rs
. We
will be using these errors shortly when we add our security checks.
add_movie_review
add_movie_review
function.
initializer
of a review is
also a signer on the transaction. This ensures that you can’t submit movie
reviews impersonating somebody else. We’ll put this check right after iterating
through the accounts.
pda_account
passed in by the user is the pda
we
expect. Recall we derived the pda
for a movie review using the initializer
and title
as seeds. Within our instruction we’ll derive the pda
again and
then check if it matches the pda_account
. If the addresses do not match, we’ll
return our custom InvalidPDA
error.
rating
falls within the 1 to 5 scale. If the rating
provided by the user outside of this range, we’ll return our custom
InvalidRating
error.
InvalidDataLength
error.
is_initialized
function we implemented for our MovieAccountState
. If the
account already exists, then we will return an error.
add_movie_review
function should look something like this:
MovieInstruction
add_movie_review
is more secure, let’s turn our attention to
supporting the ability to update a movie review.
Let’s begin by updating instruction.rs
. We’ll start by adding an
UpdateMovieReview
variant to MovieInstruction
that includes embedded data
for the new title, rating, and description.
AddMovieReview
.
Lastly, in the unpack
function we need to add UpdateMovieReview
to the match
statement.
update_movie_review
functioninstruction_data
and determine which instruction of
the program to run, we can add UpdateMovieReview
to the match statement in
the process_instruction
function in the processor.rs
file.
update_movie_review
function. The definition
should have the same parameters as the definition of add_movie_review
.
update_movie_review
functionadd_movie_review
function, let’s start by iterating through the
accounts. The only accounts we’ll need are the first two: initializer
and
pda_account
.
pda_account
to verify that it is owned by our
program. If it isn’t, we’ll return an InvalidOwner
error.
initializer
of the
update instruction has also signed the transaction. Since we are updating the
data for a movie review, we want to ensure that the original initializer
of
the review has approved the changes by signing the transaction. If the
initializer
did not sign the transaction, we’ll return an error.
pda_account
passed in by the user is the PDA we
expect by deriving the PDA using initializer
and title
as seeds. If the
addresses do not match, we’ll return our custom InvalidPDA
error. We’ll
implement this the same way we did in the add_movie_review
function.
pda_account
and perform data validationpda_account
and perform some data validation. We’ll start by unpacking
pda_account
and assigning it to a mutable variable account_data
.
UninitializedAccount
error.
rating
, title
, and description
data just
like in the add_movie_review
function. We want to limit the rating
to a
scale of 1 to 5 and limit the overall size of the review to be fewer than 1000
bytes. If the rating provided by the user outside of this range, then we’ll
return our custom InvalidRating
error. If the review is too long, then we’ll
return our custom InvalidDataLength
error.
account_data
and re-serializing it. At that
point, we can return Ok
from our program.
update_movie_review
function should look something like the
code snippet below. We’ve included some additional logging for clarity in
debugging.
MOVIE_REVIEW_PROGRAM_ID
with your program ID in Form.tsx
and
MovieCoordinator.ts
.
If you need more time with this project to feel comfortable with these concepts,
have a look at the
solution code before
continuing.