Store arbitrary data on Nexis Native Chain, using PDAs, an inbuilt key-value store.
seeds
and bump
constraints are used to initialize and validate PDA
accounts in Anchorinit_if_needed
constraint is used to conditionally initialize a new
accountrealloc
constraint is used to reallocate space on an existing accountclose
constraint is used to close an account and refund its rent#[account(...)]
attribute macro.
In addition to using constraints for account validation, some constraints can
handle repeatable tasks that would otherwise require a lot of boilerplate inside
our instruction logic. This lesson will introduce the seeds
, bump
,
realloc
, and close
constraints to help you initialize and validate PDAs,
reallocate accounts, and close accounts.
seeds
and bump
constraints.
seeds
constraint and verify that the account passed into the instruction
matches the PDA found using the specified seeds
.
When the bump
constraint is included without specifying a specific bump,
Anchor will default to using the canonical bump (the first bump that results in
a valid PDA). In most cases you should use the canonical bump.
You can access other fields from within the struct from constraints, so you can
specify seeds that are dependent on other accounts like the signer’s public key.
You can also reference the deserialized instruction data if you add the
#[instruction(...)]
attribute macro to the struct.
For example, the following example shows a list of accounts that include
pda_account
and user
. The pda_account
is constrained such that the seeds
must be the string “example_seed,” the public key of user
, and the string
passed into the instruction as instruction_data
.
pda_account
address provided by the client doesn’t match the PDA
derived using the specified seeds and the canonical bump, then the account
validation will fail.
init
constraintseeds
and bump
constraints with the init
constraint to
initialize an account using a PDA.
Recall that the init
constraint must be used in combination with the payer
and space
constraints to specify the account that will pay for account
initialization and the space to allocate on the new account. Additionally, you
must include system_program
as one of the fields of the account validation
struct.
init
for non-PDA accounts, Anchor defaults to setting the owner of
the initialized account to be the program currently executing the instruction.
However, when using init
in combination with seeds
and bump
, the owner
must be the executing program. This is because initializing an account for the
PDA requires a signature that only the executing program can provide. In other
words, the signature verification for the initialization of the PDA account
would fail if the program ID used to derive the PDA did not match the program ID
of the executing program.
When determining the value of space
for an account initialized and owned by
the executing Anchor program, remember that the first 8 bytes are reserved for
the account discriminator. This is an 8-byte value that Anchor calculates and
uses to identify the program account types. You can use this
reference to calculate how much space
you should allocate for an account.
b"example_seed"
and user.key()
. The first is static and
therefore known, and the second is known because user
is the transaction
signer.
If you use seed inference when building your program, then as long as you’re
calling the program using Anchor, you don’t need to explicitly derive and pass
in the PDA. Instead, the Anchor library will do it for you.
You can turn on seed inference in the Anchor.toml
file with seeds = true
under [features]
.
#[instruction(...)]
attribute macro#[instruction(...)]
attribute macro before moving
on. When using #[instruction(...)]
, the instruction data you provide in the
list of arguments must match and be in the same order as the instruction
arguments. You can omit unused arguments at the end of the list, but you must
include all arguments up until the last one you will be using.
For example, imagine an instruction has arguments input_one
, input_two
, and
input_three
. If your account constraints need to reference input_one
and
input_three
, you need to list all three arguments in the #[instruction(...)]
attribute macro.
However, if your constraints only reference input_one
and input_two
, you can
omit input_three
.
init_if_needed
constraint that can be used to initialize an
account if the account has not already been initialized.
This feature is gated behind a feature flag to make sure you are intentional
about using it. For security reasons, it’s smart to avoid having one instruction
branch into multiple logic paths. And as the name suggests, init_if_needed
executes one of two possible code paths depending on the state of the account in
question.
When using init_if_needed
, you need to make sure to properly protect your
program against re-initialization attacks. You need to include checks in your
code that check that the initialized account cannot be reset to its initial
settings after the first time it was initialized.
To use init_if_needed
, you must first enable the feature in Cargo.toml
.
#[account(…)]
attribute macro. The example below demonstrates using the
init_if_needed
constraint to initialize a new associated token account if one
does not already exist.
initialize
instruction is invoked in the previous example, Anchor
will check if the token_account
exists and initialize it if it does not. If it
already exists, then the instruction will continue without initializing the
account. Just as with the init
constraint, you can use init_if_needed
in
conjunction with seeds
and bump
if the account is a PDA.
realloc
constraint provides a simple way to reallocate space for existing
accounts.
The realloc
constraint must be used in combination with the following
constraints:
mut
- the account must be set as mutablerealloc::payer
- the account to subtract or add lamports to depending on
whether the reallocation is decreasing or increasing account spacerealloc::zero
- boolean to specify if new memory should be zero initializedinit
, you must include system_program
as one of the accounts in the
account validation struct when using realloc
.
Below is an example of reallocating space for an account that stores a data
field of type String
.
realloc
is set to 8 + 4 + instruction_data.len()
. This breaks
down as follows:
8
is for the account discriminator4
is for the 4 bytes of space that BORSH uses to store the length of the
stringinstruction_data.len()
is the length of the string itselfrealloc::payer
to the account to maintain rent exemption. Likewise,
if the change is subtractive, lamports will be transferred from the account back
to the realloc::payer
.
The realloc::zero
constraint is required to determine whether the new memory
should be zero initialized after reallocation. This constraint should be set to
true in cases where you expect the memory of an account to shrink and expand
multiple times. That way you zero out space that would otherwise show as stale
data.
close
constraint provides a simple and secure way to close an existing
account.
The close
constraint marks the account as closed at the end of the
instruction’s execution by setting its discriminator to
the CLOSED_ACCOUNT_DISCRIMINATOR
and sends its lamports to a specified
account. Setting the discriminator to a special variant makes account revival
attacks (where a subsequent instruction adds the rent exemption lamports again)
impossible. If someone tries to reinitialize the account, the reinitialization
will fail the discriminator check and be considered invalid by the program.
The example below uses the close
constraint to close the data_account
and
sends the lamports allocated for rent to the receiver
account.
anchor init
.
lib.rs
file within the programs
folder and you should
see the following starter code.
initialize
instruction and Initialize
type.
MovieAccountState
#[account]
attribute macro to define the
MovieAccountState
that will represent the data structure of the movie review
accounts. As a reminder, the #[account]
attribute macro implements various
traits that help with serialization and deserialization of the account, set the
discriminator for the account, and set the owner of a new account as the program
ID defined in the declare_id!
macro.
Within each movie review account, we’ll store the:
reviewer
- user creating the reviewrating
- rating for the movietitle
- title of the moviedescription
- content of the reviewadd_movie_review
instruction. The add_movie_review
instruction will require a Context
of type AddMovieReview
that we’ll
implement shortly.
The instruction will require three additional arguments as instruction data
provided by a reviewer:
title
- title of the movie as a String
description
- details of the review as a String
rating
- rating for the movie as a u8
movie_review
account with the instruction data. We’ll also set the reviewer
field as the
initializer
account from the instruction context.
AddMovieReview
struct that we used as the generic in
the instruction’s context. This struct will list the accounts the
add_movie_review
instruction requires.
Remember, you’ll need the following macros:
#[derive(Accounts)]
macro is used to deserialize and validate the list
of accounts specified within the struct#[instruction(...)]
attribute macro is used to access the instruction
data passed into the instruction#[account(...)]
attribute macro then specifies additional constraints on
the accountsmovie_review
account is a PDA that needs to be initialized, so we’ll add
the seeds
and bump
constraints as well as the init
constraint with its
required payer
and space
constraints.
For the PDA seeds, we’ll use the movie title and the reviewer’s public key. The
payer for the initialization should be the reviewer, and the space allocated on
the account should be enough for the account discriminator, the reviewer’s
public key, and the movie review’s rating, title, and description.
update_movie_review
instruction with a context whose
generic type is UpdateMovieReview
.
Just as before, the instruction will require three additional arguments as
instruction data provided by a reviewer:
title
- title of the moviedescription
- details of the reviewrating
- rating for the movierating
and description
stored
on the movie_review
account.
While the title
doesn’t get used in the instruction function itself, we’ll
need it for account validation of movie_review
in the next step.
UpdateMovieReview
struct to define the accounts that
the update_movie_review
instruction needs.
Since the movie_review
account will have already been initialized by this
point, we no longer need the init
constraint. However, since the value of
description
may now be different, we need to use the realloc
constraint to
reallocate the space on the account. Accompanying this, we need the mut
,
realloc::payer
, and realloc::zero
constraints.
We’ll also still need the seeds
and bump
constraints as we had them in
AddMovieReview
.
realloc
constraint is set to the new space required by the
movie_review
account based on the updated value of description
.
Additionally, the realloc::payer
constraint specifies that any additional
lamports required or refunded will come from or be send to the initializer
account.
Finally, we set the realloc::zero
constraint to true
because the
movie_review
account may be updated multiple times either shrinking or
expanding the space allocated to the account.
delete_movie_review
instruction to close an
existing movie_review
account.
We’ll use a context whose generic type is DeleteMovieReview
and won’t include
any additional instruction data. Since we are only closing an account, we
actually don’t need any instruction logic inside the body of the function. The
closing itself will be handled by the Anchor constraint in the
DeleteMovieReview
type.
DeleteMovieReview
struct.
close
constraint to specify we are closing the movie_review
account and that the rent should be refunded to the initializer
account. We
also include the seeds
and bump
constraints for the movie_review
account
for validation. Anchor then handles the additional logic required to securely
close the account.
anchor-movie-review-program.ts
and replace the default test code with the
following.
Here we:
addMovieReview
instruction. Note
that we don’t explicitly add .accounts
. This is because the Wallet
from
AnchorProvider
is automatically included as a signer, Anchor can infer certain
accounts like SystemProgram
, and Anchor can also infer the movieReview
PDA
from the title
instruction argument and the signer’s public key.
Once the instruction runs, we then fetch the movieReview
account and check
that the data stored on the account match the expected values.
updateMovieReview
instruction following
the same process as before.
deleteMovieReview
instruction
anchor test
and you should see the following output in the
console.