Learn how programs store data, using Nexis Native Chain’s inbuilt -key-value store.
create_account
instruction on the System Programdata
field that holds a byte array. This makes
accounts as flexible as files on a computer. You can store literally anything in
an account (so long as the account has the storage space for it).
Just as files in a traditional filesystem conform to specific data formats like
PDF or MP3, the data stored in a Nexis Native Chain account needs to follow some kind of
pattern so that the data can be retrieved and deserialized into something
usable.
borsh
crate to get access to the BorshSerialize
and
BorshDeserialize
traits. We can then apply those traits using the derive
attribute macro.
NoteState
that we can use to serialize
and deserialize the data as needed.
NoteState
struct specifies three fields
that need to be stored in an account: title
, body
, and id
. To calculate
the size the account needs to be, you would simply add up the size required to
store the data in each field.
For dynamic data, like strings, Borsh adds an additional 4 bytes at the
beginning to store the length of that particular field. That means title
and
body
are each 4 bytes plus their respective sizes. The id
field is a 64-bit
integer, or 8 bytes.
You can add up those lengths and then calculate the rent required for that
amount of space using the minimum_balance
function from the rent
module of
the solana_program
crate.
find_program_address
function.
As the name implies, PDAs are derived using the program ID (address of the
program creating the account) and an optional list of “seeds”. Optional seeds
are additional inputs used in the find_program_address
function to derive the
PDA. The function used to derive PDAs will return the same address every time
when given the same inputs. This gives us the ability to create any number of
PDA accounts and a deterministic way to find each account.
In addition to the seeds you provide for deriving a PDA, the
find_program_address
function will provide one additional “bump seed.” What
makes PDAs unique from other Nexis Native Chain account addresses is that they do not have a
corresponding secret key. This ensures that only the program that owns the
address can sign on behalf of the PDA. When the find_program_address
function
attempts to derive a PDA using the provided seeds, it passes in the number 255
as the “bump seed.” If the resulting address is invalid (i.e. has a
corresponding secret key), then the function decreases the bump seed by 1 and
derives a new PDA with that bump seed. Once a valid PDA is found, the function
returns both the PDA and the bump that was used to derive the PDA.
For our note-taking program, we will use the note creator’s public key and the
ID as the optional seeds to derive the PDA. Deriving the PDA this way allows us
to deterministically find the account for each note.
create_account
instruction on the system program.
CPIs can be done using either invoke
or invoke_signed
.
invoke_signed
. Unlike a regular signature where a
secret key is used to sign, invoke_signed
uses the optional seeds, bump seed,
and program ID to derive a PDA and sign an instruction. This is done by
comparing the derived PDA against all accounts passed into the instruction. If
any of the accounts match the PDA, then the signer field for that account is set
to true.
A program can securely sign transactions this way because invoke_signed
generates the PDA used for signing with the program ID of the program invoking
the instruction. Therefore, it is not possible for one program to generate a
matching PDA to sign for an account with a PDA derived using another program ID.
data
byte
array into its Rust type. You can do this by first borrowing the data field on
the account. This allows you to access the data without taking ownership.
You can then use the try_from_slice_unchecked
function to deserialize the data
field of the borrowed account using the format of the type you created to
represent the data. This gives you an instance of your Rust type so you can
easily update fields using dot notation. If we were to do this with the
note-taking app example we’ve been using, it would look like this:
serialize
function on the instance of the Rust type you
created. You’ll need to pass in a mutable reference to the account data. The
syntax here is tricky, so don’t worry if you don’t understand it completely.
Borrowing and references are two of the toughest concepts in Rust.
account_data
object to a byte array and sets it
to the data
property on note_pda_account
. This saves the updated
account_data
variable to the data field of the new account. Now when a user
fetches the note_pda_account
and deserializes the data, it will display the
updated data we’ve serialized into the account.
note_creator
and didn’t show where that came from.
To get access to this and other accounts, we use an
Iterator. An iterator
is a Rust trait used to give sequential access to each element in a collection
of values. Iterators are used in Nexis Native Chain programs to safely iterate over the list
of accounts passed into the program entry point through the accounts
argument.
iter()
method creates an iterator object that references a collection. An
iterator is responsible for the logic of iterating over each item and
determining when the sequence has finished. In Rust, iterators are lazy, meaning
they have no effect until you call methods that consume the iterator to use it
up. Once you’ve created an iterator, you must call the next()
function on it
to get the next item.
AccountInfo
for all accounts required by an instruction are
passing through a single accounts
argument. To parse through the accounts and
use them within our instruction, we will need to create an iterator with a
mutable reference to the accounts
.
At that point, instead of using the iterator directly, we pass it to the
next_account_info
function from the account_info
module provided by the
solana_program
crate.
For example, the instruction to create a new note in a note-taking program would
at minimum require the accounts for the user creating the note, a PDA to store
the note, and the system_program
to initialize a new account. All three
accounts would be passed into the program entry point through the accounts
argument. An iterator of accounts
is then used to separate out the
AccountInfo
associated with each account to process the instruction.
Note that &mut
means a mutable reference to the accounts
argument. You can
read more about
references in Rust
and the mut
keyword.
instruction.rs
file we use to deserialize
the instruction_data
passed into the program entry point. We have also
completed lib.rs
file to the point where we can print our deserialized
instruction data to the program log using the msg!
macro.
state.rs
.
This file will:
BorshSerialize
and BorshDeserialize
traits to this structborsh
crate.
MovieAccountState
struct. This struct will define the
parameters that each new movie review account will store in its data field. Our
MovieAccountState
struct will require the following parameters:
is_initialized
- shows whether or not the account has been initializedrating
- user’s rating of the moviedescription
- user’s description of the movietitle
- title of the movie the user is reviewinglib.rs
lib.rs
file. First, we’ll bring into scope everything
we will need to complete our Movie Review program. You can read more about the
details each item we are using from
the solana_program
crate.
accounts
add_movie_review
function. Recall that
an array of accounts is passed into the add_movie_review
function through a
single accounts
argument. To process our instruction, we will need to iterate
through accounts
and assign the AccountInfo
for each account to its own
variable.
add_movie_review
function, let’s independently derive the PDA
we expect the user to have passed in. We’ll need to provide the bump seed for
the derivation later, so even though pda_account
should reference the same
account, we still need to call find_program_address
.
Note that we derive the PDA for each new account using the initializer’s public
key and the movie title as optional seeds. Setting up the PDA this way restricts
each user to only one review for any one movie title. However, it still allows
the same user to review movies with different titles and different users to
review movies with the same title.
MovieAccountState
struct has four fields. We will allocate 1 byte each for
rating
and is_initialized
. For both title
and description
we will
allocate space equal to 4 bytes plus the length of the string.
create_account
instruction from the system program. We do this with a Cross Program Invocation
(CPI) using the invoke_signed
function. We use invoke_signed
because we are
creating the account using a PDA and need the Movie Review program to “sign” the
instruction.
MovieAccountState
struct from our
state.rs
file. We first deserialize the account data from pda_account
using
try_from_slice_unchecked
, then set the values of each field.
account_data
into the data field of our
pda_account
.
MOVIE_REVIEW_PROGRAM_ID
in both
the MovieList.tsx
and Form.tsx
components with the address of the program
you’ve deployed. Then run the frontend, submit a view, and refresh the browser
to see the review.
If you need more time with this project to feel comfortable with these concepts,
have a look at the
solution code before
continuing.
instruction_data
and creates an account to store the data onchain.
Using what you’ve learned in this lesson, build out this program. In addition to
taking a name a short message as instruction data, the program should:
is_initialized
as a boolean, name
as a string, and msg
as a
string in each account