Skip to content

Commit

Permalink
onchain-development anchor-pdas updated (#391)
Browse files Browse the repository at this point in the history
* onchain anchor-pdas updated

* onchain anchor-pdas updated

* minor refactor

* discriminator type u8 -> usize

* BORSH_LEN variable renamed to STRING_SIZE_SPACE
  • Loading branch information
SAMAD101 authored Sep 4, 2024
1 parent 71d7730 commit 14155b1
Showing 1 changed file with 79 additions and 83 deletions.
162 changes: 79 additions & 83 deletions content/courses/onchain-development/anchor-pdas.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,14 @@ In this lesson you'll learn how to work with PDAs, reallocate accounts, and
close accounts in Anchor.

Recall that Anchor programs separate instruction logic from account validation.
Account validation primarily happens within structs that represent the list of
accounts needed for a given instruction. Each field of the struct represents a
different account, and you can customize the validation performed on the account
using the `#[account(...)]` attribute macro.
Account validation happens in structs that list the accounts needed for an
instruction. Each field in the struct represents an account, and you can
customize the validation using the `#[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.
In addition to validating accounts, some constraints can automate tasks that
would otherwise require repetitive code in our instructions. This lesson will
cover the `seeds`, `bump`, `realloc`, and `close` constraints to help you easily
handle PDAs, reallocate space, and close accounts.

### PDAs with Anchor

Expand All @@ -54,31 +52,38 @@ struct ExampleAccounts {
}
```

During account validation, Anchor will derive a PDA using the seeds specified in
the `seeds` constraint and verify that the account passed into the instruction
matches the PDA found using the specified `seeds`.
During account validation, Anchor will use the specified seeds to derive a PDA
and check if the provided account matches the derived PDA.

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.
Anchor will use the canonical bump (the first bump that results in a valid PDA,
with a value of 255). Typically, 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 use other fields from within the struct as seeds, such as 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`.
For example, the following example shows a list of accounts that include:

- `pda_account`
- `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`.

```rust
#[derive(Accounts)]
#[instruction(instruction_data: String)]
pub struct Example<'info> {
#[account(
seeds = [b"example_seed", user.key().as_ref(), instruction_data.as_ref()],
seeds = [
b"example_seed",
user.key().as_ref(),
instruction_data.as_ref()
],
bump
)]
pub pda_account: Account<'info, AccountType>,
Expand All @@ -96,11 +101,12 @@ validation will fail.
You can combine the `seeds` 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.
Recall that the `init` constraint must be used with the `payer` and `space`
constraints to specify who pays for the account initialization and how much
space to allocate.

Additionally, you need to include `system_program` to handle the creation and
funding of the new account.

```rust
#[derive(Accounts)]
Expand All @@ -110,7 +116,7 @@ pub struct InitializePda<'info> {
seeds = [b"example_seed", user.key().as_ref()],
bump,
payer = user,
space = 8 + 8
space = DISCRIMINATOR + Accountype::INIT_SPACE
)]
pub pda_account: Account<'info, AccountType>,
#[account(mut)]
Expand All @@ -119,9 +125,12 @@ pub struct InitializePda<'info> {
}

#[account]
#[derive(InitSpace)]
pub struct AccountType {
pub data: u64,
}

const DISCRIMINATOR: usize = 8;
```

When using `init` for non-PDA accounts, Anchor defaults to setting the owner of
Expand All @@ -134,18 +143,11 @@ 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](https://www.anchor-lang.com/docs/space) to calculate how much space
you should allocate for an account.

#### Seed inference

The account list for an instruction can get really long for some programs. To
simplify the client-side experience when invoking an Anchor program instruction,
we can turn on seed inference.
we can turn on **seed inference**.

Seed inference adds information about PDA seeds to the IDL so that Anchor can
infer PDA seeds from existing call-site information. In the previous example,
Expand Down Expand Up @@ -299,7 +301,7 @@ pub struct ReallocExample<'info> {
mut,
seeds = [b"example_seed", user.key().as_ref()],
bump,
realloc = 8 + 4 + instruction_data.len(),
realloc = DISCRIMINATOR + STRING_SIZE_SPACE + instruction_data.len(),
realloc::payer = user,
realloc::zero = false,
)]
Expand All @@ -310,42 +312,46 @@ pub struct ReallocExample<'info> {
}

#[account]
#[derive(InitSpace)]
pub struct AccountType {
pub data: String,
}

const DISCRIMINATOR: usize = 8;
const STRING_SIZE_SPACE: usize = 4;
```

Notice that `realloc` is set to `8 + 4 + instruction_data.len()`. This breaks
down as follows:
The `realloc` constraint from the above example can be broken down as follows:

- `8` is for the account discriminator
- `4` is for the 4 bytes of space that BORSH uses to store the length of the
string
- the `DISCRIMINATOR` is `8`
- the `STRING_SIZE_SPACE` is `4` for the space required to store the length of
the string. As required by BORSH serialization
- `instruction_data.len()` is the length of the string itself

> [BORSH](https://solanacookbook.com/guides/serialization.html) stands for
> _Binary Object Representation Serializer for Hashing_ and is used to
> efficiently and compactly serialize and deserialize data structures.
If the change in account data length is additive, lamports will be transferred
from the `realloc::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.
The `realloc::zero` constraint ensures that any new memory allocated during
reallocation is set to zero. This should be set to true if you expect the memory
of an account to change size frequently. This way, you clear out any old data
that might otherwise remain.

### Close

The `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.
instruction’s execution by setting its discriminator to a _special value_ called
`CLOSED_ACCOUNT_DISCRIMINATOR` and sends its lamports to a specified account.
This _special value_ prevents the account from being reopened because any
attempt to reinitialize the account will fail the discriminator check.

The example below uses the `close` constraint to close the `data_account` and
sends the lamports allocated for rent to the `receiver` account.
Expand Down Expand Up @@ -381,7 +387,7 @@ This program will allow users to:

To begin, let’s create a new project using `anchor init`.

```shell
```bash
anchor init anchor-movie-review-program
```

Expand Down Expand Up @@ -448,40 +454,27 @@ pub mod anchor_movie_review_program {
}

#[account]
#[derive(InitSpace)]
pub struct MovieAccountState {
pub reviewer: Pubkey, // 32
pub rating: u8, // 1
#[max_len(20)]
pub title: String, // 4 + len()
#[max_len(50)]
pub description: String, // 4 + len()
}
```

For this account struct, we will be implementing the space trait:

```rust
/*
For the MovieAccountState account, since it is dynamic, we implement the Space trait to calculate the space required for the account.
We add the STRING_LENGTH_PREFIX twice to the space to account for the title and description string prefix.
We need to add the length of the title and description to the space upon initialization.
*/
impl Space for MovieAccountState {
const INIT_SPACE: usize = ANCHOR_DISCRIMINATOR + PUBKEY_SIZE + U8_SIZE + STRING_LENGTH_PREFIX + STRING_LENGTH_PREFIX;
}
const DISCRIMINATOR: usize = 8;
```

The `Space` trait will force us to define the space of our account for
initialization, by defining the `INIT_SPACE` constant. This constant can then be
used during the account initialization.

Note that, in this case, since the account state is dynamic (`title` and
`description` are strings without a fixed size), we will add
`STRING_LENGTH_PREFIX` that represents 4 bytes (required to store their length)
but we still need to add the length of the actual context of both strings during
our account initialization (You will see that in the following steps).
Using the `#[derive(InitSpace)]` macro on the `AccountStruct` automatically
calculates the `INIT_SPACE` constant which represents the space required for the
account fields, including fixed-size fields and the length-prefixed strings.

In sum, our `INIT_SPACE` constant will be 8 bytes for the anchor discriminator +
32 bytes for the reviewer Pubkey + 1 byte for the rating + 4 bytes for the title
length storage + 4 bytes for the description length storage.
In cases of dynamic fields like strings, we can use the `#[max_len]` macro to
specify the maximum length of these fields to determining the space needed for
the account during initialization. Here, we have chosen the `title` string to be
of length 20 (max), and the `description` string to be of length 50 (max).

### Custom error codes

Expand Down Expand Up @@ -536,6 +529,11 @@ The `require!` macro will perform a check and throw a custom error in case that
check is not successful.

```rust
const MIN_RATING: u8 = 1;
const MAX_RATING: u8 = 5;
const MAX_TITLE_LENGTH: usize = 20;
const MAX_DESCRIPTION_LENGTH: usize = 50;

#[program]
pub mod anchor_movie_review_program{
use super::*;
Expand Down Expand Up @@ -585,9 +583,7 @@ Remember, you'll need the following macros:

The `movie_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. Regarding the required space, we will
be using the `INIT_SPACE` constant that we defined in the account struct, and we
will add the string length of the both the title and the description.
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
Expand All @@ -603,7 +599,7 @@ pub struct AddMovieReview<'info> {
seeds = [title.as_bytes(), initializer.key().as_ref()],
bump,
payer = initializer,
space = MovieAccountState::INIT_SPACE + title.len() + description.len() // We add the length of the title and description to the init space
space = DISCRIMINATOR + MovieAccountState::INIT_SOACE
)]
pub movie_review: Account<'info, MovieAccountState>,
#[account(mut)]
Expand Down Expand Up @@ -678,7 +674,7 @@ pub struct UpdateMovieReview<'info> {
mut,
seeds = [title.as_bytes(), initializer.key().as_ref()],
bump,
realloc = MovieAccountState::INIT_SPACE + title.len() + description.len(), // We add the length of the title and description to the init space
realloc = DISCRIMINATOR + MovieAccountState::INIT_SOACE
realloc::payer = initializer,
realloc::zero = true,
)]
Expand Down Expand Up @@ -854,7 +850,7 @@ it("Deletes a movie review", async () => {
Lastly, run `anchor test` and you should see the following output in the
console.

```shell
```bash
anchor-movie-review-program
✔ Movie review is added` (139ms)
✔ Movie review is updated` (404ms)
Expand Down

0 comments on commit 14155b1

Please sign in to comment.