From 961b1f425b09ee62f2f2904abb48d0e47d057260 Mon Sep 17 00:00:00 2001 From: 0xCipherCoder Date: Sun, 15 Sep 2024 10:56:22 +0530 Subject: [PATCH 1/3] Added formatting --- .../program-derived-addresses.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/content/courses/native-onchain-development/program-derived-addresses.md b/content/courses/native-onchain-development/program-derived-addresses.md index ed5dd66de..1668277c3 100644 --- a/content/courses/native-onchain-development/program-derived-addresses.md +++ b/content/courses/native-onchain-development/program-derived-addresses.md @@ -81,7 +81,7 @@ 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. -#### Under the hood of `find_program_address` +#### Under the hood of find_program_address Let's take a look at the source code for `find_program_address`. @@ -182,7 +182,7 @@ System Program to create non-PDA accounts and use those to store data as well, PDAs tend to be the way to go. If you need a refresher on how to store data in PDAs, have a look at the -[State Management lesson](/content/courses/native-onchain-development/program-state-management). +[State Management lesson](/content/courses/native-onchain-development/program-state-management.md). ### Map to data stored in PDA accounts @@ -319,7 +319,7 @@ secure manner. In this lab, we'll add the ability for users to comment on a movie review. We'll use building this feature as an opportunity to work through how to structure the comment storage using PDA accounts. -#### 1. Get the starter code +### 1. Get the starter code To begin, you can find [the movie program starter code](https://github.com/Unboxed-Software/solana-movie-program/tree/starter) @@ -349,7 +349,7 @@ You can test the program by using the movie review and updating the program ID with the one you’ve just deployed. Make sure you use the `solution-update-reviews` branch. -#### 2. Plan out the account structure +### 2. Plan out the account structure Adding comments means we need to make a few decisions about how to store the data associated with each comment. The criteria for a good structure here are: @@ -394,7 +394,7 @@ To implement these changes, we'll need to do the following: include creating the comment counter account - Create a new `add_comment` instruction processing function -#### 3. Define `MovieCommentCounter` and `MovieComment` structs +### 3. Define MovieCommentCounter and MovieComment structs Recall that the `state.rs` file defines the structs our program uses to populate the data field of a new account. @@ -498,7 +498,7 @@ impl MovieComment { Now everywhere we need the discriminator or account size we can use this implementation and not risk unintentional typos. -#### 4. Create `AddComment` instruction +### 4. Create AddComment instruction Recall that the `instruction.rs` file defines the instructions our program will accept and how to deserialize the data for each. We need to add a new @@ -606,7 +606,7 @@ pub fn process_instruction( } ``` -#### 5. Update `add_movie_review` to create comment counter account +### 5. Update add_movie_review to create comment counter account Before we implement the `add_comment` function, we need to update the `add_movie_review` function to create the review's comment counter account. @@ -724,7 +724,7 @@ Now when a new review is created, two accounts are initialized: is unchanged from the version of the program we started with. 2. The second account stores the counter for comments -#### 6. Implement `add_comment` +### 6. Implement add_comment Finally, let’s implement our `add_comment` function to create new comment accounts. @@ -837,7 +837,7 @@ pub fn add_comment( } ``` -#### 7. Build and deploy +### 7. Build and deploy We're ready to build and deploy our program! From 84c11a1fe6bc05a512b609e0169f08e76929e8b0 Mon Sep 17 00:00:00 2001 From: 0xCipherCoder Date: Mon, 16 Sep 2024 07:11:52 +0530 Subject: [PATCH 2/3] Updated content and snippet as per guidelines --- .../native-onchain-development/deleteme.rs.rs | 7 + .../program-derived-addresses.md | 732 ++++++++++-------- 2 files changed, 416 insertions(+), 323 deletions(-) create mode 100644 content/courses/native-onchain-development/deleteme.rs.rs diff --git a/content/courses/native-onchain-development/deleteme.rs.rs b/content/courses/native-onchain-development/deleteme.rs.rs new file mode 100644 index 000000000..90c3511bf --- /dev/null +++ b/content/courses/native-onchain-development/deleteme.rs.rs @@ -0,0 +1,7 @@ +{ + let (pda, bump_seed) = Pubkey::find_program_address(&[ + initializer.key.as_ref(), + title.as_bytes().as_ref() + ], + program_id); +} \ No newline at end of file diff --git a/content/courses/native-onchain-development/program-derived-addresses.md b/content/courses/native-onchain-development/program-derived-addresses.md index 1668277c3..bf5b56ab4 100644 --- a/content/courses/native-onchain-development/program-derived-addresses.md +++ b/content/courses/native-onchain-development/program-derived-addresses.md @@ -2,59 +2,61 @@ title: Program Derived Addresses (PDAs) objectives: - Explain Program Derived Addresses (PDAs) - - Explain various use cases of PDAs + - Identify various use cases for PDAs - Describe how PDAs are derived - - Use PDA derivations to locate and retrieve data -description: "Get a deeper understanding of PDAs." + - Utilize PDA derivations to locate and retrieve data +description: "Gain a deeper understanding of PDAs." --- ## Summary - A **Program Derived Address** (PDA) is derived from a **program ID** and an - optional list of **seeds** -- PDAs are owned and controlled by the program they are derived from -- PDA derivation provides a deterministic way to find data based on the seeds - used for the derivation -- Seeds can be used to map to the data stored in a separate PDA account -- A program can sign instructions on behalf of the PDAs derived from its ID + optional list of **seeds**. +- PDAs are owned and controlled exclusively by the program from which they are + derived. +- PDA derivation provides a deterministic method to locate data based on the + specific seeds used for the derivation. +- Seeds can map to data stored in a separate PDA account. +- A program can sign instructions on behalf of the PDAs derived from its ID. ## Lesson ### What is a Program Derived Address? -Program Derived Addresses (PDAs) are account addresses designed to be signed for -by a program rather than a secret key. As the name suggests, PDAs are derived -using a program ID. Optionally, these derived accounts can also be found using -the ID along with a set of "seeds." More on this later, but these seeds will -play an important role in how we use PDAs for data storage and retrieval. +Program Derived Addresses (PDAs) are special account addresses that a program, +not a secret key, signs for. PDAs are derived from a program ID, and optionally, +a set of **seeds**. These seeds will be essential in using PDAs to store and +retrieve data, as we’ll explore in more detail later. -PDAs serve two main functions: +PDAs serve two key purposes: -1. Provide a deterministic way to find a given item of data for a program -2. Authorize the program from which a PDA was derived to sign on its behalf in - the same way a user may sign with their secret key +1. They provide a **deterministic way** for a program to locate a specific piece + of data. +2. They allow the program to sign for the PDA like a user signs with their + secret key. -In this lesson we'll focus on using PDAs to find and store data. We'll discuss -signing with a PDA more thoroughly in a future lesson where we cover Cross -Program Invocations (CPIs). +In this lesson, we’ll focus on using PDAs to store and retrieve data. We will +discuss how PDAs sign on behalf of programs in the next lesson that covers +[**Cross Program Invocations (CPIs)**](/content/courses/native-onchain-development/cross-program-invocations.md). ### Finding PDAs -PDAs are not technically created. Rather, they are _found_ or _derived_ based on -a program ID and one or more input seeds. +Program Derived Addresses (PDAs) are not technically created; instead, they are +**found** or **derived** based on a program ID and one or more input seeds. -Solana keypairs can be found on what is called the Ed25519 Elliptic Curve -(Ed25519). Ed25519 is a deterministic signature scheme that Solana uses to -generate corresponding public and secret keys. Together, we call these keypairs. +Solana keypairs are based on the Ed25519 Elliptic Curve (Ed25519), a +deterministic signature scheme used to generate corresponding public and secret +keys—together known as keypairs. -Alternatively, PDAs are addresses that lie _off_ the Ed25519 curve. This means -PDAs are not public keys, and don't have private keys. This property of PDAs is -essential for programs to be able to sign on their behalf, but we'll cover that -in a future lesson. +In contrast, PDAs are addresses that exist **off** the Ed25519 curve. This means +PDAs are not public keys and do not have secret keys. This characteristic is +crucial, as it enables programs to sign on behalf of PDAs, a concept we’ll +explore more in a future lesson. -To find a PDA within a Solana program, we'll use the `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. +To find a PDA in a Solana program, we use the +[`find_program_address()`](https://docs.rs/solana-program/latest/solana_program/pubkey/struct.Pubkey.html#method.find_program_address) +function. This function accepts an optional list of "seeds" and a program ID as +inputs, returning both the PDA and a bump seed: ```rust let (pda, bump_seed) = Pubkey::find_program_address(&[user.key.as_ref(), user_input.as_bytes().as_ref(), "SEED".as_bytes()], program_id) @@ -62,51 +64,54 @@ let (pda, bump_seed) = Pubkey::find_program_address(&[user.key.as_ref(), user_in #### Seeds -“Seeds” are optional inputs used in the `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. +"Seeds" are optional inputs used in the `find_program_address()` function to +derive a PDA. These seeds can be a combination of public keys, user-provided +inputs, or hardcoded values. A PDA can even be derived using only the program ID +without any additional seeds. By using seeds, however, you can create multiple +distinct accounts that your program can control. + +As the developer, you define the seeds to pass into the `find_program_address()` +function. The function itself adds a special numeric seed called a **bump +seed**. This bump seed ensures that the derived key is not on the Ed25519 curve, +as about 50% of the results lie on the curve, which would mean the PDA has a +secret key. Since PDAs should not have secret keys, the bump seed is used to +prevent this. + +The function starts by testing with the value `255` as the bump seed. If the +resulting address is not valid, the function decreases the bump seed value +(`255`, `254`, `253`, etc.) until a valid PDA is found. Once a valid PDA is +derived, the function returns both the PDA and the bump seed used to generate +it. #### Under the hood of find_program_address -Let's take a look at the source code for `find_program_address`. +Let's take a closer look at the source code behind the `find_program_address()` +function: ```rust - pub fn find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> (Pubkey, u8) { +pub fn find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> (Pubkey, u8) { Self::try_find_program_address(seeds, program_id) .unwrap_or_else(|| panic!("Unable to find a viable program address bump seed")) } ``` -Under the hood, the `find_program_address` function passes the input `seeds` and -`program_id` to the `try_find_program_address` function. +The `find_program_address()` function internally calls +[`try_find_program_address()`](https://docs.rs/solana-program/latest/solana_program/pubkey/struct.Pubkey.html#method.try_find_program_address), +passing the input `seeds` and `program_id`. -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. +The `try_find_program_address()` function introduces the `bump_seed`, a `u8` +variable that ranges from 0 to 255. The process begins by iterating over a +descending range starting at 255. A `bump_seed` is appended to the optional +input seeds and passed to the +[`create_program_address()`](https://docs.rs/solana-program/latest/solana_program/pubkey/struct.Pubkey.html#method.create_program_address) +function. If the resulting address is not a valid PDA, the `bump_seed` is +decreased by 1, and the function continues to try again until it finds a valid +PDA. ```rust pub fn try_find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> Option<(Pubkey, u8)> { - - let mut bump_seed = [std::u8::MAX]; - for _ in 0..std::u8::MAX { + let mut bump_seed = [u8::MAX]; + for _ in 0..u8::MAX { { let mut seeds_with_bump = seeds.to_vec(); seeds_with_bump.push(&bump_seed); @@ -119,22 +124,26 @@ pub fn try_find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> Option< bump_seed[0] -= 1; } None - } ``` -The `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. +The `create_program_address()` function performs a series of hash operations on +the seeds and `program_id`. These operations compute a key, and the function +checks whether the computed key lies on the Ed25519 elliptic curve. If the +result is a valid **Program Derived Address** (PDA) (i.e., an address that lies +**off** the curve), the PDA is returned. If the computed key lies **on** the +curve, indicating it's not a valid PDA, an error is returned. ```rust -pub fn create_program_address( - seeds: &[&[u8]], - program_id: &Pubkey, -) -> Result { - +pub fn create_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> Result { + if seeds.len() > MAX_SEEDS { + return Err(PubkeyError::MaxSeedLengthExceeded); + } + for seed in seeds.iter() { + if seed.len() > MAX_SEED_LEN { + return Err(PubkeyError::MaxSeedLengthExceeded); + } + } let mut hasher = crate::hash::Hasher::default(); for seed in seeds.iter() { hasher.hash(seed); @@ -146,110 +155,98 @@ pub fn create_program_address( return Err(PubkeyError::InvalidSeeds); } - Ok(Pubkey::new(hash.as_ref())) - + Ok(Pubkey::from(hash.to_bytes())) } ``` -In summary, the `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. +In summary, the `find_program_address()` function passes the input seeds and +`program_id` to the `try_find_program_address()` function. The +`try_find_program_address()` function appends a `bump_seed` (starting from 255) +to the input seeds, then calls the `create_program_address()` function until it +finds a valid PDA. Once a valid PDA is found, both the PDA and the `bump_seed` +are returned. + +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. Since the function starts with a `bump_seed` value of 255 +and iterates downward to zero, the `bump_seed` that gets returned is always the +largest valid 8-bit value possible. This `bump_seed` is referred to as the +**canonical bump**. To avoid confusion, it’s recommended to _always use the +canonical bump and validate every PDA passed into your program._ + + + +It's important to note that the `find_program_address()` function only returns a +Program Derived Address and the bump seed used to derive it. The function does +not initialize a new account, nor does it ensure the PDA is associated with an +account that stores data. ### Use PDA accounts to store data -Since programs themselves are stateless, program state is managed through -external accounts. Given that you can use seeds for mapping and that programs -can sign on their behalf, using PDA accounts to store data related to the -program is an extremely common design choice. While programs can invoke the -System Program to create non-PDA accounts and use those to store data as well, -PDAs tend to be the way to go. +Since programs are stateless, the program state is managed through external +accounts. Given that seeds can be used for mapping and that programs can sign on +behalf of PDAs, using PDA accounts to store program-related data is a common +design choice. While programs can invoke the System Program to create non-PDA +accounts for data storage, PDAs are generally preferred. -If you need a refresher on how to store data in PDAs, have a look at the +If you need a refresher on how to store data in PDAs, check out the [State Management lesson](/content/courses/native-onchain-development/program-state-management.md). ### Map to data stored in PDA accounts -Storing data in PDA accounts is only half of the equation. You also need a way -to retrieve that data. We'll talk about two approaches: - -1. Creating a PDA "map" account that stores the addresses of various accounts - where data is stored -2. Strategically using seeds to locate the appropriate PDA accounts and retrieve - the necessary data +Storing data in PDA accounts is only part of the process. You also need a way to +retrieve that data. There are two common approaches: -#### Map to data using PDA "map" accounts +1. Creating a PDA "map" account that stores the addresses of accounts where data + is stored. +2. Strategically using seeds to locate and retrieve data from the appropriate + PDA accounts. -One approach to organizing data storage is to store clusters of relevant data in -their own PDAs and then to have a separate PDA account that stores a mapping of -where all of the data is. +#### Map to data using PDA map accounts -For example, you might have a note-taking app whose backing program uses random -seeds to generate PDA accounts and stores one note in each account. The program -would also have a single global PDA "map" account that stores a mapping of -users' public keys to the list of PDAs where their notes are stored. This map -account would be derived using a static seed, e.g. "GLOBAL_MAPPING". +One approach is to store clusters of related data in separate PDAs and maintain +a separate PDA account that acts as a "map" for where all of the data is stored. -When it comes time to retrieve a user's notes, you could then look at the map -account, see the list of addresses associated with a user's public key, then -retrieve the account for each of those addresses. +For example, consider a note-taking app that uses random seeds to generate PDAs +for storing notes, where each note is in its own PDA. The program could also +have a global PDA "map" account that stores a mapping of users' public keys to +the list of PDAs holding their notes. This map account could be derived using a +static seed, such as "GLOBAL_MAPPING." -While such a solution is perhaps more approachable for traditional web -developers, it does come with some drawbacks that are particular to web3 -development. Since the size of the mapping stored in the map account will grow -over time, you'll either need to allocate more size than necessary to the -account when you first create it, or you'll need to reallocate space for it -every time a new note is created. On top of that, you'll eventually reach the -account size limit of 10 megabytes. +When retrieving a user's notes, the program would look at the map account to see +which addresses are associated with the user's public key and then fetch the +data from each of those addresses. -You could mitigate this issue to some degree by creating a separate map account -for each user. For example, rather than having a single PDA map account for the -entire program, you would construct a PDA map account per user. Each of these -map accounts could be derived with the user's public key. The addresses for each -note could then be stored inside the corresponding user's map account. +While this method is intuitive for developers familiar with traditional web +development, it presents some challenges unique to web3 development. Since the +size of the map account grows over time, you would either need to allocate more +space upfront or reallocate space as needed. However, there’s a 10 MB account +size limit, which could eventually be an issue. -This approach reduces the size required for each map account, but ultimately -still adds an unnecessary requirement to the process: having to read the -information on the map account _before_ being able to find the accounts with the -relevant note data. +One potential solution is to create a separate map account for each user, +derived using their public key. Each user’s map account would then store the +addresses for their notes. This reduces the size needed for each map account, +but it adds a step: having to read the map account _before_ finding the accounts +with the actual note data. -There may be times where using this approach makes sense for your application, -but we don't recommend it as your "go to" strategy. +While this method might be suitable for some applications, it's not generally +recommended as your primary strategy. #### Map to data using PDA derivation -If you're strategic about the seeds you use to derive PDAs, you can embed the -required mappings into the seeds themselves. This is the natural evolution of -the note-taking app example we just discussed. If you start to use the note -creator's public key as a seed to create one map account per user, then why not -use both the creator's public key and some other known piece of information to -derive a PDA for the note itself? - -Now, without talking about it explicitly, we’ve been mapping seeds to accounts -this entire course. Think about the Movie Review program we've been built in -previous lessons. This program uses a review creator's public key and the title -of the movie they're reviewing to find the address that _should_ be used to -store the review. This approach lets the program create a unique address for -every new review while also making it easy to locate a review when needed. When -you want to find a user's review of "Spiderman," you know that it is stored at -the PDA account whose address can be derived using the user's public key and the -text "Spiderman" as seeds. +A more efficient method is to strategically use seeds to embed the required +mappings directly in the PDAs. Returning to the note-taking app example, if you +use the creator's public key as a seed to create one map account per user, you +could also use both the creator's public key and some additional known +information to derive a PDA for the note itself. + +In essence, this is the approach we've been using throughout this course. +Consider the Movie Review program from previous lessons: it uses a review +creator's public key and the movie title as seeds to find the address where the +review should be stored. This method generates a unique address for each review +and makes it easy to locate the review later. For instance, to find a user’s +review of "Spiderman," the program derives the PDA account using the user's +public key and "Spiderman" as seeds. ```rust let (pda, bump_seed) = Pubkey::find_program_address(&[ @@ -261,23 +258,25 @@ let (pda, bump_seed) = Pubkey::find_program_address(&[ #### Associated token account addresses -Another practical example of this type of mapping is how associated token -account (ATA) addresses are determined. Tokens are often held in an ATA whose -address was derived using a wallet address and the mint address of a specific -token. The address for an ATA is found using the `get_associated_token_address` -function which takes a `wallet_address` and `token_mint_address` as inputs. +A practical example of mapping using seeds is how associated token account (ATA) +addresses are derived. Tokens are often held in an ATA, which is created based +on a wallet address and the mint address of a specific token. The ATA's address +can be found using the +[`get_associated_token_address()`](https://docs.rs/spl-associated-token-account/latest/spl_associated_token_account/fn.get_associated_token_address.html) +function, which takes both the `wallet_address` and `token_mint_address` as +inputs. ```rust let associated_token_address = get_associated_token_address(&wallet_address, &token_mint_address); ``` -Under the hood, the associated token address is a PDA found using the +Under the hood, the associated token address is a PDA derived using the `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. +provides a deterministic way to locate a token account associated with any +wallet address for a specific token mint. ```rust -fn get_associated_token_address_and_bump_seed_internal( +pub fn get_associated_token_address_and_bump_seed_internal( wallet_address: &Pubkey, token_mint_address: &Pubkey, program_id: &Pubkey, @@ -294,30 +293,29 @@ fn get_associated_token_address_and_bump_seed_internal( } ``` -The mappings between seeds and PDA accounts that you use will be highly -dependent on your specific program. While this isn't a lesson on system design -or architecture, it's worth calling out a few guidelines: +The mappings between seeds and PDA accounts that you create will depend heavily +on your specific program's design. While this isn't a lesson on system +architecture, it's important to follow a few guidelines: -- Use seeds that will be known at the time of PDA derivation -- Be thoughtful about what data is grouped together into a single account -- Be thoughtful about the data structure used within each account -- Simpler is usually better +- Use seeds that will be known at the time of PDA derivation. +- Be deliberate in how you group data into a single account. +- Choose your data structure wisely for storing information within each account. +- Simplicity is usually best. ## Lab -Let’s practice together with the Movie Review program we've worked on in -previous lessons. No worries if you’re just jumping into this lesson without -having done the previous lesson - it should be possible to follow along either -way. +Let's continue with the Movie Review program we've worked on in previous +lessons. If you're just joining this lesson, don't worry—you'll be able to +follow along. -As a refresher, the Movie Review program lets users create movie reviews. These -reviews are stored in an account using a PDA derived with the initializer’s -public key and the title of the movie they are reviewing. +As a quick refresher, the Movie Review program allows users to create movie +reviews, which are stored in an account using a PDA. This PDA is derived from +the review creator's public key and the movie title. -Previously, we finished implementing the ability to update a movie review in a -secure manner. In this lab, we'll add the ability for users to comment on a -movie review. We'll use building this feature as an opportunity to work through -how to structure the comment storage using PDA accounts. +Previously, we implemented the ability to securely update a movie review. Now, +in this lab, we'll add a new feature allowing users to comment on a movie +review. We'll use this opportunity to explore how to structure comment storage +using PDA accounts. ### 1. Get the starter code @@ -326,27 +324,47 @@ To begin, you can find on the `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 +this is the program we've built out so far. Previously, we used [Solana Playground](https://beta.solpg.io/) to write, build, and deploy our -code. In this lesson, we’ll build and deploy the program locally. +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. +Navigate to the program folder and build the program using the Solana Binary +Format (SBF) build tool: ```sh -cargo-build-bpf +cargo build-sbf ``` -Deploy the program by copying the output of `cargo-build-bpf` and running the -`solana program deploy` command. +Deploy your program to the Solana blockchain using the Solana CLI: ```sh -solana program deploy +solana program deploy target/deploy/.so ``` +Upon successful deployment, you'll receive a Program ID. For example: + +```sh +Program Id: 8aK2C2xyYDTiBTEYu7Q2ShSmQozNQSMoqFpUmsJLVDW9 +``` + +If you encounter an "insufficient funds" error during deployment, you may need +to add SOL to your deployment wallet. Use the Solana CLI to request an airdrop: + +```sh +solana airdrop 2 +``` + +After receiving the airdrop, attempt the deployment again. + + + +Ensure your Solana CLI is configured for the correct network (`Localnet`, +`devnet`, `testnet`, or `mainnet-beta`) before deploying or requesting airdrops. + + You can test the program by using the movie review [frontend](https://github.com/Unboxed-Software/solana-movie-frontend/tree/solution-update-reviews) -and updating the program ID with the one you’ve just deployed. Make sure you use +and updating the program ID with the one you've just deployed. Make sure you use the `solution-update-reviews` branch. ### 2. Plan out the account structure @@ -389,23 +407,23 @@ To implement these changes, we'll need to do the following: - Define structs to represent the comment counter and comment accounts - Update the existing `MovieAccountState` to contain a discriminator (more on this later) -- Add an instruction variant to represent the `add_comment` instruction -- Update the existing `add_movie_review` instruction processing function to - include creating the comment counter account -- Create a new `add_comment` instruction processing function +- Add an instruction variant to represent the `add_comment` instruction handler +- Update the existing `add_movie_review` instruction handler to include creating + the comment counter account +- Create a new `add_comment` instruction handler ### 3. Define MovieCommentCounter and MovieComment structs Recall that the `state.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. +We'll need to define two new structs to enable commenting. 1. `MovieCommentCounter` - to store a counter for the number of comments associated with a review 2. `MovieComment` - to store data associated with each comment -To start, let’s define the structs we’ll be using for our program. Note that we +To start, let's define the structs we'll be using for our program. Note that we are adding a `discriminator` 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 @@ -498,11 +516,11 @@ impl MovieComment { Now everywhere we need the discriminator or account size we can use this implementation and not risk unintentional typos. -### 4. Create AddComment instruction +### 4. Create AddComment Instruction Handler Recall that the `instruction.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 +instruction variant for adding comments. Let's start by adding a new variant `AddComment` to the `MovieInstruction` enum. ```rust @@ -536,38 +554,46 @@ struct CommentPayload { } ``` -Now let’s update how we unpack the instruction data. Notice that we’ve moved the +Now let's update how we unpack the instruction data. Notice that we've moved the deserialization of instruction data into each matching case using the associated payload struct for each instruction. ```rust impl MovieInstruction { + pub fn unpack(input: &[u8]) -> Result { - let (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?; - Ok(match variant { + let (&discriminator, rest) = input + .split_first() + .ok_or(ProgramError::InvalidInstructionData)?; + + match discriminator { 0 => { - let payload = MovieReviewPayload::try_from_slice(rest).unwrap(); - Self::AddMovieReview { - title: payload.title, - rating: payload.rating, - description: payload.description } - }, + let payload = MovieReviewPayload::try_from_slice(rest) + .map_err(|_| ProgramError::InvalidInstructionData)?; + Ok(Self::AddMovieReview { + title: payload.title, + rating: payload.rating, + description: payload.description, + }) + } 1 => { - let payload = MovieReviewPayload::try_from_slice(rest).unwrap(); - Self::UpdateMovieReview { + let payload = MovieReviewPayload::try_from_slice(rest) + .map_err(|_| ProgramError::InvalidInstructionData)?; + Ok(Self::UpdateMovieReview { title: payload.title, rating: payload.rating, - description: payload.description - } - }, + description: payload.description, + }) + } 2 => { - let payload = CommentPayload::try_from_slice(rest).unwrap(); - Self::AddComment { - comment: payload.comment - } + let payload = CommentPayload::try_from_slice(rest) + .map_err(|_| ProgramError::InvalidInstructionData)?; + Ok(Self::AddComment { + comment: payload.comment, + }) } - _ => return Err(ProgramError::InvalidInstructionData) - }) + _ => Err(ProgramError::InvalidInstructionData), + } } } ``` @@ -578,30 +604,31 @@ the new instruction variant we've created. In `processor.rs`, bring into scope the new structs from `state.rs`. ```rust -use crate::state::{MovieAccountState, MovieCommentCounter, MovieComment}; +use crate::state::{MovieAccountState, MovieComment, MovieCommentCounter}; ``` -Then in `process_instruction` let’s match our deserialized `AddComment` -instruction data to the `add_comment` function we’ll be implementing shortly. +Then in `process_instruction` let's match our deserialized `AddComment` +instruction data to the `add_comment` function we'll be implementing shortly. ```rust pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], - instruction_data: &[u8] + instruction_data: &[u8], ) -> ProgramResult { let instruction = MovieInstruction::unpack(instruction_data)?; match instruction { - MovieInstruction::AddMovieReview { title, rating, description } => { - add_movie_review(program_id, accounts, title, rating, description) - }, - MovieInstruction::UpdateMovieReview { title, rating, description } => { - update_movie_review(program_id, accounts, title, rating, description) - }, - - MovieInstruction::AddComment { comment } => { - add_comment(program_id, accounts, comment) - } + MovieInstruction::AddMovieReview { + title, + rating, + description, + } => add_movie_review(program_id, accounts, title, rating, description), + MovieInstruction::UpdateMovieReview { + title, + rating, + description, + } => update_movie_review(program_id, accounts, title, rating, description), + MovieInstruction::AddComment { comment } => add_comment(program_id, accounts, comment), } } ``` @@ -617,8 +644,8 @@ movie review address and the word “comment” as seeds. Note that how we store 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 +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. @@ -645,9 +672,9 @@ if MovieAccountState::get_account_size(title.clone(), description.clone()) > acc ``` Note that this also needs to be updated in the `update_movie_review` function -for that instruction to work properly. +for that instruction handler to work properly. -Once we’ve initialized the review account, we’ll also need to update the +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. @@ -660,7 +687,7 @@ account_data.description = description; account_data.is_initialized = true; ``` -Finally, let’s add the logic to initialize the counter account within the +Finally, let's add the logic to initialize the counter account within the `add_movie_review` function. This means: 1. Calculating the rent exemption amount for the counter account @@ -670,52 +697,61 @@ Finally, let’s add the logic to initialize the counter account within the 4. Set the starting counter value 5. Serialize the account data and return from the function -All of this should be added to the end of the `add_movie_review` function before -the `Ok(())`. +All of this should be encapsulated in the `create_comment_counter()` function, +which should be called at the end of the `add_movie_review()` function, just +before the `Ok(())`. ```rust -msg!("create comment counter"); -let rent = Rent::get()?; -let counter_rent_lamports = rent.minimum_balance(MovieCommentCounter::SIZE); - -let (counter, counter_bump) = - Pubkey::find_program_address(&[pda.as_ref(), "comment".as_ref()], program_id); -if counter != *pda_counter.key { - msg!("Invalid seeds for PDA"); - return Err(ProgramError::InvalidArgument); -} +fn create_comment_counter( + program_id: &Pubkey, + accounts: &[AccountInfo], + pda: Pubkey, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let initializer = next_account_info(account_info_iter)?; + let pda_counter = next_account_info(account_info_iter)?; + let system_program = next_account_info(account_info_iter)?; -invoke_signed( - &system_instruction::create_account( - initializer.key, - pda_counter.key, - counter_rent_lamports, - MovieCommentCounter::SIZE.try_into().unwrap(), - program_id, - ), - &[ - initializer.clone(), - pda_counter.clone(), - system_program.clone(), - ], - &[&[pda.as_ref(), "comment".as_ref(), &[counter_bump]]], -)?; -msg!("comment counter created"); + let rent = Rent::get()?; + let counter_rent_lamports = rent.minimum_balance(MovieCommentCounter::SIZE); -let mut counter_data = - try_from_slice_unchecked::(&pda_counter.data.borrow()).unwrap(); + let (counter, counter_bump) = + Pubkey::find_program_address(&[pda.as_ref(), "comment".as_ref()], program_id); + if counter != *pda_counter.key { + msg!("Invalid seeds for PDA"); + return Err(ProgramError::InvalidArgument); + } -msg!("checking if counter account is already initialized"); -if counter_data.is_initialized() { - msg!("Account already initialized"); - return Err(ProgramError::AccountAlreadyInitialized); -} + invoke_signed( + &system_instruction::create_account( + initializer.key, + pda_counter.key, + counter_rent_lamports, + MovieCommentCounter::SIZE.try_into().unwrap(), + program_id, + ), + &[ + initializer.clone(), + pda_counter.clone(), + system_program.clone(), + ], + &[&[pda.as_ref(), "comment".as_ref(), &[counter_bump]]], + )?; + msg!("Comment counter created"); -counter_data.discriminator = MovieCommentCounter::DISCRIMINATOR.to_string(); -counter_data.counter = 0; -counter_data.is_initialized = true; -msg!("comment count: {}", counter_data.counter); -counter_data.serialize(&mut &mut pda_counter.data.borrow_mut()[..])?; + let mut counter_data = MovieCommentCounter::try_from_slice(&pda_counter.data.borrow())?; + if counter_data.is_initialized() { + msg!("Account already initialized"); + return Err(ProgramError::AccountAlreadyInitialized); + } + + counter_data.discriminator = MovieCommentCounter::DISCRIMINATOR.to_string(); + counter_data.counter = 0; + counter_data.is_initialized = true; + counter_data.serialize(&mut &mut pda_counter.data.borrow_mut()[..])?; + + Ok(()) +} ``` Now when a new review is created, two accounts are initialized: @@ -726,35 +762,33 @@ Now when a new review is created, two accounts are initialized: ### 6. Implement add_comment -Finally, let’s implement our `add_comment` function to create new comment +Finally, let's implement our `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: +Like in other instruction handlers, 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: ```rust pub fn add_comment( program_id: &Pubkey, accounts: &[AccountInfo], - comment: String + comment: String, ) -> ProgramResult { - msg!("Adding Comment..."); - msg!("Comment: {}", comment); + msg!("Adding Comment: {}", comment); let account_info_iter = &mut accounts.iter(); - let commenter = next_account_info(account_info_iter)?; let pda_review = next_account_info(account_info_iter)?; let pda_counter = next_account_info(account_info_iter)?; let pda_comment = next_account_info(account_info_iter)?; let system_program = next_account_info(account_info_iter)?; - let mut counter_data = try_from_slice_unchecked::(&pda_counter.data.borrow()).unwrap(); + let mut counter_data = MovieCommentCounter::try_from_slice(&pda_counter.data.borrow())?; Ok(()) } @@ -774,49 +808,57 @@ steps: pub fn add_comment( program_id: &Pubkey, accounts: &[AccountInfo], - comment: String + comment: String, ) -> ProgramResult { - msg!("Adding Comment..."); - msg!("Comment: {}", comment); + msg!("Adding Comment: {}", comment); let account_info_iter = &mut accounts.iter(); - let commenter = next_account_info(account_info_iter)?; let pda_review = next_account_info(account_info_iter)?; let pda_counter = next_account_info(account_info_iter)?; let pda_comment = next_account_info(account_info_iter)?; let system_program = next_account_info(account_info_iter)?; - let mut counter_data = try_from_slice_unchecked::(&pda_counter.data.borrow()).unwrap(); - + let mut counter_data = MovieCommentCounter::try_from_slice(&pda_counter.data.borrow())?; let account_len = MovieComment::get_account_size(comment.clone()); let rent = Rent::get()?; let rent_lamports = rent.minimum_balance(account_len); - let (pda, bump_seed) = Pubkey::find_program_address(&[pda_review.key.as_ref(), counter_data.counter.to_be_bytes().as_ref(),], program_id); + let (pda, bump_seed) = Pubkey::find_program_address( + &[ + pda_review.key.as_ref(), + counter_data.counter.to_be_bytes().as_ref(), + ], + program_id, + ); if pda != *pda_comment.key { msg!("Invalid seeds for PDA"); - return Err(ReviewError::InvalidPDA.into()) + return Err(ReviewError::InvalidPDA.into()); } invoke_signed( &system_instruction::create_account( - commenter.key, - pda_comment.key, - rent_lamports, - account_len.try_into().unwrap(), - program_id, + commenter.key, + pda_comment.key, + rent_lamports, + account_len.try_into().unwrap(), + program_id, ), - &[commenter.clone(), pda_comment.clone(), system_program.clone()], - &[&[pda_review.key.as_ref(), counter_data.counter.to_be_bytes().as_ref(), &[bump_seed]]], + &[ + commenter.clone(), + pda_comment.clone(), + system_program.clone(), + ], + &[&[ + pda_review.key.as_ref(), + counter_data.counter.to_be_bytes().as_ref(), + &[bump_seed], + ]], )?; - msg!("Created Comment Account"); - let mut comment_data = try_from_slice_unchecked::(&pda_comment.data.borrow()).unwrap(); - - msg!("checking if comment account is already initialized"); + let mut comment_data = MovieComment::try_from_slice(&pda_comment.data.borrow())?; if comment_data.is_initialized() { msg!("Account already initialized"); return Err(ProgramError::AccountAlreadyInitialized); @@ -841,8 +883,51 @@ pub fn add_comment( We're ready to build and deploy our program! -Build the updated program by running `cargo-build-bpf`. Then deploy the program -by running the `solana program deploy` command printed to the console. +Build the updated program by running `cargo build-sbf`. + +```sh +cargo build-sbf +``` + +Then deploy the program by running the `solana program deploy` command. + +```sh +solana program deploy target/deploy/.so +``` + +For additional deployment information, refer to the deployment details outlined +in [step 1](#1-get-the-starter-code). + +If you encounter the following error during program deployment, it indicates +that your program size needs to be extended: + +```sh +Error: Deploying program failed: RPC response error -32002: Transaction simulation failed: Error processing Instruction 0: account data too small for instruction [3 log messages ] +``` + +To resolve this, if you're using Solana CLI version 1.18 or later, run the +following command: + +```sh +solana program extend PROGRAM_ID 20000 -u d -k KEYPAIR_FILE_PATH +``` + +Replace `PROGRAM_ID` and `KEYPAIR_FILE_PATH` with your own values. For example: + +```sh + solana program extend HMDRWmYvL2A9xVKZG8iA1ozxi4gMKiHQz9mFkURKrG4 20000 -u d -k ~/.config/solana/id.json +``` + + + +Ensure you are passing the correct Solana's JSON RPC or moniker URL parameter in +the command. + +```bash +-u, --url URL for Solana's JSON RPC or moniker (or their first letter): [mainnet-beta, testnet, devnet, localhost] +``` + + 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 @@ -851,39 +936,40 @@ 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. +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](https://github.com/Unboxed-Software/solana-movie-program/tree/solution-add-comments) -before continuing. Note that the solution code is on the `solution-add-comments` -branch of the linked repository. +the [`solution-add-comments` branch of the movie program repository](https://github.com/Unboxed-Software/solana-movie-program/tree/solution-add-comments) +before continuing. ## Challenge -Now it’s your turn to build something independently! Go ahead and work with the +Now it's your turn to build something independently! Go ahead and work with the Student Intro program that we've used in past lessons. The Student Intro program is a Solana program that lets students introduce themselves. This program takes a user's name and a short message as the `instruction_data` and creates an -account to store the data onchain. For this challenge you should: +account to store the data onchain. For this challenge, you should: -1. Add an instruction allowing other users to reply to an intro +1. Add an instruction handler allowing other users to reply to an intro 2. Build and deploy the program locally If you haven't been following along with past lessons or haven't saved your work -from before, feel free to use the starter code on the `starter` branch of -[this repository](https://github.com/Unboxed-Software/solana-student-intro-program/tree/starter). +from before, feel free to use the starter code on the +[`starter` branch of the student intro program repository](https://github.com/Unboxed-Software/solana-student-intro-program/tree/starter). Try to do this independently if you can! If you get stuck though, feel free to -reference the -[solution code](https://github.com/Unboxed-Software/solana-student-intro-program/tree/solution-add-replies). -Note that the solution code is on the `solution-add-replies` branch and that -your code may look slightly different. +reference the solution code in the +[`solution-add-replies` branch of the same repository](https://github.com/Unboxed-Software/solana-student-intro-program/tree/solution-add-replies). +Note that your code may look slightly different. + Push your code to GitHub and [tell us what you thought of this lesson](https://form.typeform.com/to/IPH0UGz7#answers-lesson=89d367b4-5102-4237-a7f4-4f96050fe57e)! From dc7ef9a26e83865b211c118b54f3cadd43d35e25 Mon Sep 17 00:00:00 2001 From: 0xCipherCoder Date: Mon, 16 Sep 2024 07:13:03 +0530 Subject: [PATCH 3/3] Updated content and snippet as per guidelines --- content/courses/native-onchain-development/deleteme.rs.rs | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 content/courses/native-onchain-development/deleteme.rs.rs diff --git a/content/courses/native-onchain-development/deleteme.rs.rs b/content/courses/native-onchain-development/deleteme.rs.rs deleted file mode 100644 index 90c3511bf..000000000 --- a/content/courses/native-onchain-development/deleteme.rs.rs +++ /dev/null @@ -1,7 +0,0 @@ -{ - let (pda, bump_seed) = Pubkey::find_program_address(&[ - initializer.key.as_ref(), - title.as_bytes().as_ref() - ], - program_id); -} \ No newline at end of file