Get a deeper understanding of PDAs.
find_program_address
function. This function takes an optional list of “seeds” and a program ID as
inputs, and then returns the PDA and a bump seed.
find_program_address
function to
derive a PDA. For example, seeds can be any combination of public keys, inputs
provided by a user, or hardcoded values. A PDA can also be derived using only
the program ID and no additional seeds. Using seeds to find our PDAs, however,
allows us to create an arbitrary number of accounts that our program can own.
While you, the developer, determine the seeds to pass into the
find_program_address
function, the function itself provides an additional seed
called a “bump seed.” The cryptographic function for deriving a PDA results in a
key that lies on the Ed25519 curve about 50% of the time. To ensure that the
result is not on the Ed25519 curve and therefore does not have a secret key,
the find_program_address
function adds a numeric seed called a bump seed.
The function starts by using the value 255
as the bump seed, then checks to
see if the output is a valid PDA. If the result is not a valid PDA, the function
decreases the bump seed by 1 and tries again (255
, 254
, 253
, et cetera).
Once a valid PDA is found, the function returns both the PDA and the bump that
was used to derive the PDA.
find_program_address
find_program_address
.
find_program_address
function passes the input seeds
and
program_id
to the try_find_program_address
function.
The try_find_program_address
function then introduces the bump_seed
. The
bump_seed
is a u8
variable with a value ranging between 0 to 255. Iterating
over a descending range starting from 255, a bump_seed
is appended to the
optional input seeds which are then passed to the create_program_address
function. If the output of create_program_address
is not a valid PDA, then the
bump_seed
is decreased by 1 and the loop continues until a valid PDA is found.
create_program_address
function performs a set of hash operations over the
seeds and program_id
. These operations compute a key, then verify if the
computed key lies on the Ed25519 elliptic curve or not. If a valid PDA is found
(i.e. an address that is off the curve), then the PDA is returned. Otherwise,
an error is returned.
find_program_address
function passes our input seeds and
program_id
to the try_find_program_address
function. The
try_find_program_address
function adds a bump_seed
(starting from 255) to
our input seeds, then calls the create_program_address
function until a valid
PDA is found. Once found, both the PDA and the bump_seed
are returned.
Note that for the same input seeds, different valid bumps will generate
different valid PDAs. The bump_seed
returned by find_program_address
will
always be the first valid PDA found. Because the function starts with a
bump_seed
value of 255 and iterates downwards to zero, the bump_seed
that
ultimately gets returned will always be the largest valid 8-bit value possible.
This bump_seed
is commonly referred to as the “canonical bump”. To avoid
confusion, it’s recommended to only use the canonical bump, and to always
validate every PDA passed into your program.
One point to emphasize is that the find_program_address
function only returns
a Program Derived Address and the bump seed used to derive it. The
find_program_address
function does not initialize a new account, nor is any
PDA returned by the function necessarily associated with an account that stores
data.
get_associated_token_address
function which takes a wallet_address
and token_mint_address
as inputs.
wallet_address
, token_program_id
, and token_mint_address
as seeds. This
provides a deterministic way to find a token account associated with any wallet
address for a specific token mint.
starter
branch.
If you’ve been following along with the Movie Review labs, you’ll notice that
this is the program we’ve built out so far. Previously, we
used Nexis Native Chain Playground to write, build, and deploy our
code. In this lesson, we’ll build and deploy the program locally.
Open the folder, then run cargo-build-bpf
to build the program. The
cargo-build-bpf
command will output instruction to deploy the program.
cargo-build-bpf
and running the
solana program deploy
command.
solution-update-reviews
branch.
n
is the total number of comments on the review, loop n
times. Each
iteration of the loop will derive a PDA using the review address and the
current number as seeds. The result is n
number of PDAs, each of which is
the address of an account that stores a comment.n
PDAs and read the data stored in each.MovieAccountState
to contain a discriminator (more on
this later)add_comment
instructionadd_movie_review
instruction processing function to
include creating the comment counter accountadd_comment
instruction processing functionMovieCommentCounter
and MovieComment
structsstate.rs
file defines the structs our program uses to populate
the data field of a new account.
We’ll need to define two new structs to enable commenting.
MovieCommentCounter
- to store a counter for the number of comments
associated with a reviewMovieComment
- to store data associated with each commentdiscriminator
field to each struct, including the existing
MovieAccountState
. Since we now have multiple account types, we need a way to
only fetch the account type we need from the client. This discriminator is a
string that can be used to filter through accounts when we fetch our program
accounts.
discriminator
field to our existing struct, the
account size calculation needs to change. Let’s use this as an opportunity to
clean up some of our code a bit. We’ll add an implementation for each of the
three structs above that adds a constant DISCRIMINATOR
and either a constant
SIZE
or function get_account_size
so we can quickly get the size needed when
initializing an account.
AddComment
instructioninstruction.rs
file defines the instructions our program will
accept and how to deserialize the data for each. We need to add a new
instruction variant for adding comments. Let’s start by adding a new variant
AddComment
to the MovieInstruction
enum.
CommentPayload
struct to represent the instruction data
associated with this new instruction. Most of the data we’ll include in the
account are public keys associated with accounts passed into the program, so the
only thing we actually need here is a single field to represent the comment
text.
process_instruction
function in processor.rs
to use
the new instruction variant we’ve created.
In processor.rs
, bring into scope the new structs from state.rs
.
process_instruction
let’s match our deserialized AddComment
instruction data to the add_comment
function we’ll be implementing shortly.
add_movie_review
to create comment counter accountadd_comment
function, we need to update the
add_movie_review
function to create the review’s comment counter account.
Remember that this account will keep track of the total number of comments that
exist for an associated review. It’s address will be a PDA derived using the
movie review address and the word “comment” as seeds. Note that how we store the
counter is simply a design choice. We could also add a “counter” field to the
original movie review account.
Within the add_movie_review
function, let’s add a pda_counter
to represent
the new counter account we’ll be initializing along with the movie review
account. This means we now expect four accounts to be passed into
the add_movie_review
function through the accounts
argument.
total_len
is less than 1000 bytes, but
total_len
is no longer accurate since we added the discriminator. Let’s
replace total_len
with a call to MovieAccountState::get_account_size
:
update_movie_review
function
for that instruction to work properly.
Once we’ve initialized the review account, we’ll also need to update the
account_data
with the new fields we specified in the MovieAccountState
struct.
add_movie_review
function. This means:
add_movie_review
function before
the Ok(())
.
add_comment
add_comment
function to create new comment
accounts.
When a new comment is created for a review, we will increment the count on the
comment counter PDA account and derive the PDA for the comment account using the
review address and current count.
Like in other instruction processing functions, we’ll start by iterating through
accounts passed into the program. Then before we do anything else we need to
deserialize the counter account so we have access to the current comment count:
cargo-build-bpf
. Then deploy the program
by running the solana program deploy
command printed to the console.
You can test your program by submitting a transaction with the right instruction
data. You can create your own script or feel free to use
this frontend.
Be sure to use the solution-add-comments
branch and replace the
MOVIE_REVIEW_PROGRAM_ID
in utils/constants.ts
with your program’s ID or the
frontend won’t work with your program.
Keep in mind that we made breaking changes to the review accounts (i.e. adding a
discriminator). If you were to use the same program ID that you’ve used
previously when deploying this program, none of the reviews you created
previously will show on this frontend due to a data mismatch.
If you need more time with this project to feel comfortable with these concepts,
have a look at
the solution code
before continuing. Note that the solution code is on the solution-add-comments
branch of the linked repository.
instruction_data
and creates an
account to store the data onchain. For this challenge you should:
starter
branch of
this repository.
Try to do this independently if you can! If you get stuck though, feel free to
reference the
solution code.
Note that the solution code is on the solution-add-replies
branch and that
your code may look slightly different.