From b39860bf694a7e90410cfc651a2a37c5db97e865 Mon Sep 17 00:00:00 2001 From: 0xCipherCoder Date: Tue, 10 Sep 2024 16:28:09 +0530 Subject: [PATCH 1/6] Fixed links --- .../program-architecture.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/content/courses/program-optimization/program-architecture.md b/content/courses/program-optimization/program-architecture.md index f18289284..c6381d717 100644 --- a/content/courses/program-optimization/program-architecture.md +++ b/content/courses/program-optimization/program-architecture.md @@ -61,20 +61,21 @@ we are going to be looking at in this section: introduce you to the concept of data sizes here. 2. When operating on larger data, we run into - [Stack](https://solana.com/docs/onchain-programs/faq#stack) and - [Heap](https://solana.com/docs/onchain-programs/faq#heap-size) constraints - - to get around these, we’ll look at using Box and Zero-Copy. + [Stack](https://solana.com/docs/programs/faq#stack) and + [Heap](https://solana.com/docs/programs/faq#heap-size) constraints - to get + around these, we’ll look at using Box and Zero-Copy. #### Sizes In Solana a transaction's fee payer pays for each byte stored onchain. We call this [rent](https://solana.com/docs/core/fees). -rent is a bit of a misnomer since it never actually gets -permanently taken. Once you deposit rent into the account, that data can stay -there forever or you can get refunded the rent if you close the account. Rent -used to be an actual thing, but now there's an enforced minimum rent exemption. -You can read about it in + + +Rent is a bit of a misnomer since it never actually gets permanently taken. Once +you deposit rent into the account, that data can stay there forever or you can +get refunded the rent if you close the account. Rent used to be an actual thing, +but now there's an enforced minimum rent exemption. You can read about it in [the Solana documentation](https://solana.com/docs/intro/rent). Rent etymology aside, putting data on the blockchain can be expensive. It’s why From cdde4941813d36e67c9e9312790851a37fbb0f91 Mon Sep 17 00:00:00 2001 From: 0xCipherCoder Date: Sun, 15 Sep 2024 03:26:13 +0530 Subject: [PATCH 2/6] Updated content and code snippets --- .../program-architecture.md | 1127 ++++++++++------- 1 file changed, 673 insertions(+), 454 deletions(-) diff --git a/content/courses/program-optimization/program-architecture.md b/content/courses/program-optimization/program-architecture.md index c6381d717..581c61d47 100644 --- a/content/courses/program-optimization/program-architecture.md +++ b/content/courses/program-optimization/program-architecture.md @@ -67,26 +67,24 @@ we are going to be looking at in this section: #### Sizes -In Solana a transaction's fee payer pays for each byte stored onchain. We call -this [rent](https://solana.com/docs/core/fees). +In Solana, a transaction's fee payer pays for each byte stored onchain. This is +called [rent](https://solana.com/docs/core/fees#rent). -Rent is a bit of a misnomer since it never actually gets permanently taken. Once -you deposit rent into the account, that data can stay there forever or you can -get refunded the rent if you close the account. Rent used to be an actual thing, -but now there's an enforced minimum rent exemption. You can read about it in -[the Solana documentation](https://solana.com/docs/intro/rent). - -Rent etymology aside, putting data on the blockchain can be expensive. It’s why -NFT attributes and associated files, like the image, are stored offchain. You -ultimately want to strike a balance that leaves your program highly functional -without becoming so expensive that your users don’t want to pay to open the data -account. - -The first thing you need to know before you can start optimizing for space in -your program is the size of each of your structs. Below is a very helpful list -from the +Rent is a bit of a misnomer since it never gets permanently taken. Once you +deposit rent into the account, that data can stay there forever, or you can get +refunded the rent if you close the account. Rent used to be an actual thing, but +now there's an enforced minimum rent exemption. You can read about it in +[the Solana documentation](https://solana.com/docs/core/fees#rent-exempt). + +Putting data on the blockchain can be expensive, which is why NFT attributes and +associated files, like images, are stored offchain. The goal is to strike a +balance between keeping your program highly functional while ensuring that users +aren't discouraged by the cost of storing data onchain. + +The first step in optimizing for space in your program is understanding the size +of your structs. Below is a helpful reference from the [Anchor Book](https://book.anchor-lang.com/anchor_references/space.html). @@ -159,7 +157,9 @@ where that entire `SomeBigDataStruct` gets stored in memory and since 5000 bytes, or 5KB, is greater than the 4KB limit, it will throw a stack error. So how do we fix this? -The answer is the **`Box`** type! +The answer is the +[**`Box`**](https://docs.rs/anchor-lang/latest/anchor_lang/accounts/boxed/index.html) +type! ```rust #[account] @@ -325,13 +325,13 @@ when `flags` has four items in the vector vs eight items. If you were to call ```rust 0000: 74 e4 28 4e d9 ec 31 0a -> Account Discriminator (8) -0008: 04 00 00 00 11 22 33 44 -> Vec Size (4) | Data 4*(1) +0008: 04 00 00 00 11 22 33 44 -> Vec Size (4) | Data 4*(1) 0010: DE AD BE EF -> id (4) --- vs --- 0000: 74 e4 28 4e d9 ec 31 0a -> Account Discriminator (8) -0008: 08 00 00 00 11 22 33 44 -> Vec Size (8) | Data 4*(1) +0008: 08 00 00 00 11 22 33 44 -> Vec Size (8) | Data 4*(1) 0010: 55 66 77 88 DE AD BE EF -> Data 4*(1) | id (4) ``` @@ -374,7 +374,7 @@ queries! The simple fix is to flip the order. ```rust #[account] // Anchor hides the account disriminator pub struct GoodState { - pub id: u32 // 0xDEAD_BEEF + pub id: u32 // 0xDEAD_BEEF pub flags: Vec, // 0x11, 0x22, 0x33 ... } ``` @@ -425,7 +425,7 @@ reserves some bytes where you expect to need them most. pub struct GameState { //V1 pub health: u64, pub mana: u64, - pub for_future_use: [u8; 128], + pub for_future_use: [u8; 128], pub event_log: Vec } ``` @@ -438,8 +438,8 @@ this and both the old and new accounts are compatible. pub struct GameState { //V2 pub health: u64, pub mana: u64, - pub experience: u64, - pub for_future_use: [u8; 120], + pub experience: u64, + pub for_future_use: [u8; 120], pub event_log: Vec } ``` @@ -630,7 +630,7 @@ issues. ```rust Alice -- pays --> | - -- > Carol + -- > Carol Bob -- pays --- | ``` @@ -641,7 +641,7 @@ just Alice and Bob try to pay Carol? ```rust Alice -- pays --> | - -- > Carol + -- > Carol x1000 -- pays --- | Bob -- pays --- | ``` @@ -823,7 +823,7 @@ engine in Solana. This program will have the following features: We'll walk through the tradeoffs of various design decisions as we go to give you a sense for why we do things. Let’s get started! -#### 1. Program Setup +### 1. Program Setup We'll build this from scratch. Start by creating a new Anchor project: @@ -831,54 +831,65 @@ We'll build this from scratch. Start by creating a new Anchor project: anchor init rpg ``` -This lab was created with Anchor version `0.28.0` in mind. -If there are problems compiling, please refer to the -[solution code](https://github.com/Unboxed-Software/anchor-rpg/tree/challenge-solution) -for the environment setup. + + +This lab was created with Anchor version `0.30.1` in mind. If there are problems +compiling, please refer to the +[solution code](https://github.com/Unboxed-Software/anchor-rpg/tree/main) for +the environment setup. Next, replace the program ID in `programs/rpg/lib.rs` and `Anchor.toml` with the -program ID shown when you run `anchor keys list`. +program ID shown when you run `anchor keys list`. Alternatively, you can run +command `anchor keys sync` that will automatically sync your program ID. This +command will sync the program ids between the program files(including +`Anchor.toml`) with the actual `pubkey` from the program keypair file. -Finally, let's scaffold out the program in the `lib.rs` file. To make following -along easier, we're going to keep everything in one file. We'll augment this -with section comments for better organization and navigation. Copy the following +Finally, let's scaffold out the program in the `lib.rs` file. Copy the following into your file before we get started: -```rust +```rust filename="lib.rs" use anchor_lang::prelude::*; -use anchor_lang::system_program::{Transfer, transfer}; use anchor_lang::solana_program::log::sol_log_compute_units; declare_id!("YOUR_KEY_HERE__YOUR_KEY_HERE"); -// ----------- ACCOUNTS ---------- - -// ----------- GAME CONFIG ---------- - -// ----------- STATUS ---------- - -// ----------- INVENTORY ---------- - -// ----------- HELPER ---------- - -// ----------- CREATE GAME ---------- - -// ----------- CREATE PLAYER ---------- +#[program] +pub mod rpg { + use super::*; -// ----------- SPAWN MONSTER ---------- + pub fn create_game(ctx: Context, max_items_per_player: u8) -> Result<()> { + run_create_game(ctx, max_items_per_player)?; + sol_log_compute_units(); + Ok(()) + } -// ----------- ATTACK MONSTER ---------- + pub fn create_player(ctx: Context) -> Result<()> { + run_create_player(ctx)?; + sol_log_compute_units(); + Ok(()) + } -// ----------- REDEEM TO TREASURY ---------- + pub fn spawn_monster(ctx: Context) -> Result<()> { + run_spawn_monster(ctx)?; + sol_log_compute_units(); + Ok(()) + } -#[program] -pub mod rpg { - use super::*; + pub fn attack_monster(ctx: Context) -> Result<()> { + run_attack_monster(ctx)?; + sol_log_compute_units(); + Ok(()) + } + pub fn deposit_action_points(ctx: Context) -> Result<()> { + run_collect_action_points(ctx)?; + sol_log_compute_units(); + Ok(()) + } } ``` -#### 2. Create Account Structures +### 2. Create Account Structures Now that our initial setup is ready, let's create our accounts. We'll have 3: @@ -913,21 +924,56 @@ Now that our initial setup is ready, let's create our accounts. We'll have 3: - `game` - the game the monster is associated with - `hitpoints` - how many hit points the monster has left +This is the final project structure: + +```bash +src/ +├── constants.rs # Constants used throughout the program +├── error/ # Error module +│ ├── errors.rs # Custom error definitions +│ └── mod.rs # Module declarations for error handling +├── helpers.rs # Helper functions used across the program +├── instructions/ # Instruction handlers for different game actions +│ ├── attack_monster.rs # Handles attacking a monster +│ ├── collect_points.rs # Handles collecting points +│ ├── create_game.rs # Handles game creation +│ ├── create_player.rs # Handles player creation +│ ├── mod.rs # Module declarations for instructions +│ └── spawn_monster.rs # Handles spawning a new monster +├── lib.rs # Main entry point for the program +└── state/ # State module for game data structures + ├── game.rs # Game state representation + ├── mod.rs # Module declarations for state + ├── monster.rs # Monster state representation + └── player.rs # Player state representation +``` + When added to the program, the accounts should look like this: ```rust // ----------- ACCOUNTS ---------- -#[account] -pub struct Game { // 8 bytes - pub game_master: Pubkey, // 32 bytes - pub treasury: Pubkey, // 32 bytes - - pub action_points_collected: u64, // 8 bytes +// Inside `state/game.rs` +use anchor_lang::prelude::*; +#[account] +#[derive(InitSpace)] +pub struct Game { + pub game_master: Pubkey, + pub treasury: Pubkey, + pub action_points_collected: u64, pub game_config: GameConfig, } +#[derive(AnchorSerialize, AnchorDeserialize, Clone, InitSpace)] +pub struct GameConfig { + pub max_items_per_player: u8, + pub for_future_use: [u64; 16], // Health of Enemies? Experience per item? Action Points per Action? +} + +// Inside `state/player.rs` +use anchor_lang::prelude::*; #[account] +#[derive(InitSpace)] pub struct Player { // 8 bytes pub player: Pubkey, // 32 bytes pub game: Pubkey, // 32 bytes @@ -945,12 +991,22 @@ pub struct Player { // 8 bytes pub inventory: Vec, // Max 8 items } -#[account] -pub struct Monster { // 8 bytes - pub player: Pubkey, // 32 bytes - pub game: Pubkey, // 32 bytes +#[derive(AnchorSerialize, AnchorDeserialize, Clone, InitSpace)] +pub struct InventoryItem { + pub name: [u8; 32], // Fixed Name up to 32 bytes + pub amount: u64, + pub for_future_use: [u8; 128], // Metadata? Effects? Flags? +} + - pub hitpoints: u64, // 8 bytes +// Inside `state/monster.rs` +use anchor_lang::prelude::*; +#[account] +#[derive(InitSpace)] +pub struct Monster { + pub player: Pubkey, + pub game: Pubkey, + pub hitpoints: u64, } ``` @@ -968,7 +1024,7 @@ queries and likely couldn't query in a single call based on `inventory`. Reallocating and adding a field would move the memory position of `inventory`, leaving us to write complex logic to query accounts with various structures. -#### 3. Create ancillary types +### 3. Create Ancillary Types The next thing we need to do is add some of the types our accounts reference that we haven't created yet. @@ -982,13 +1038,13 @@ an account rather than in the middle. If you anticipate adding fields in the middle of existing date, it might make sense to add some "future use" bytes up front. -```rust +```rust filename="game.rs" // ----------- GAME CONFIG ---------- - -#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +// Inside `state/game.rs` +#[derive(AnchorSerialize, AnchorDeserialize, Clone, InitSpace)] pub struct GameConfig { pub max_items_per_player: u8, - pub for_future_use: [u64; 16], // Health of Enemies?? Experience per item?? Action Points per Action?? + pub for_future_use: [u64; 16], // Health of Enemies? Experience per item? Action Points per Action? } ``` @@ -997,35 +1053,40 @@ booleans but we save space by storing multiple flags in a single byte. Each flag takes up a different bit within the byte. We can use the `<<` operator to place `1` in the correct bit. -```rust +```rust filename="constants.rs" // ----------- STATUS ---------- -const IS_FROZEN_FLAG: u8 = 1 << 0; -const IS_POISONED_FLAG: u8 = 1 << 1; -const IS_BURNING_FLAG: u8 = 1 << 2; -const IS_BLESSED_FLAG: u8 = 1 << 3; -const IS_CURSED_FLAG: u8 = 1 << 4; -const IS_STUNNED_FLAG: u8 = 1 << 5; -const IS_SLOWED_FLAG: u8 = 1 << 6; -const IS_BLEEDING_FLAG: u8 = 1 << 7; -const NO_EFFECT_FLAG: u8 = 0b00000000; +pub const IS_FROZEN_FLAG: u8 = 1 << 0; +pub const IS_POISONED_FLAG: u8 = 1 << 1; +pub const IS_BURNING_FLAG: u8 = 1 << 2; +pub const IS_BLESSED_FLAG: u8 = 1 << 3; +pub const IS_CURSED_FLAG: u8 = 1 << 4; +pub const IS_STUNNED_FLAG: u8 = 1 << 5; +pub const IS_SLOWED_FLAG: u8 = 1 << 6; +pub const IS_BLEEDING_FLAG: u8 = 1 << 7; + +pub const NO_EFFECT_FLAG: u8 = 0b00000000; +pub const ANCHOR_DISCRIMINATOR: usize = 8; +pub const MAX_INVENTORY_ITEMS: usize = 8; ``` Finally, let's create our `InventoryItem`. This should have fields for the item's name, amount, and some bytes reserved for future use. -```rust +```rust filename="player.rs" // ----------- INVENTORY ---------- -#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +// Inside `state/player.rs` +#[derive(AnchorSerialize, AnchorDeserialize, Clone, InitSpace)] pub struct InventoryItem { pub name: [u8; 32], // Fixed Name up to 32 bytes pub amount: u64, - pub for_future_use: [u8; 128], // Metadata?? // Effects // Flags? + pub for_future_use: [u8; 128], // Metadata? Effects? Flags? } + ``` -#### 4. Create helper function for spending action points +### 4. Create helper function for spending action points The last thing we'll do before writing the program's instructions is create a helper function for spending action points. Players will send action points @@ -1041,26 +1102,40 @@ that will send the lamports from that account to the treasury in one fell swoop. This alleviates any concurrency issues since every player has their own account, but also allows the program to retrieve those lamports at any time. -```rust +```rust filename="helper.rs" // ----------- HELPER ---------- +// Inside /src/helpers.rs +use anchor_lang::{prelude::*, system_program}; + +use crate::{error::RpgError, Player}; + pub fn spend_action_points<'info>( action_points: u64, player_account: &mut Account<'info, Player>, player: &AccountInfo<'info>, system_program: &AccountInfo<'info>, ) -> Result<()> { - - player_account.action_points_spent = player_account.action_points_spent.checked_add(action_points).unwrap(); - player_account.action_points_to_be_collected = player_account.action_points_to_be_collected.checked_add(action_points).unwrap(); - - let cpi_context = CpiContext::new( - system_program.clone(), - Transfer { - from: player.clone(), - to: player_account.to_account_info().clone(), - }); - transfer(cpi_context, action_points)?; + player_account.action_points_spent = player_account + .action_points_spent + .checked_add(action_points) + .ok_or(error!(RpgError::ArithmeticOverflow))?; + + player_account.action_points_to_be_collected = player_account + .action_points_to_be_collected + .checked_add(action_points) + .ok_or(error!(RpgError::ArithmeticOverflow))?; + + system_program::transfer( + CpiContext::new( + system_program.to_account_info(), + system_program::Transfer { + from: player.to_account_info(), + to: player_account.to_account_info(), + }, + ), + action_points, + )?; msg!("Minus {} action points", action_points); @@ -1068,7 +1143,7 @@ pub fn spend_action_points<'info>( } ``` -#### 5. Create Game +### 5. Create Game Our first instruction will create the `game` account. Anyone can be a `game_master` and create their own game, but once a game has been created there @@ -1083,43 +1158,47 @@ sure whoever is creating the game has the private keys to the `treasury`. This is a design decision rather than "the right way." Ultimately, it's a security measure to ensure the game master will be able to retrieve their funds. -```rust +```rust filename="create_game.rs" // ----------- CREATE GAME ---------- +// Inside src/instructions/create_game.rs +use anchor_lang::prelude::*; + +use crate::{error::RpgError, Game, ANCHOR_DISCRIMINATOR}; + #[derive(Accounts)] pub struct CreateGame<'info> { #[account( init, - seeds=[b"GAME", treasury.key().as_ref()], + seeds = [b"GAME", treasury.key().as_ref()], bump, payer = game_master, - space = std::mem::size_of::()+ 8 + space = ANCHOR_DISCRIMINATOR + Game::INIT_SPACE )] pub game: Account<'info, Game>, - #[account(mut)] pub game_master: Signer<'info>, - - /// CHECK: Need to know they own the treasury pub treasury: Signer<'info>, pub system_program: Program<'info, System>, } pub fn run_create_game(ctx: Context, max_items_per_player: u8) -> Result<()> { + if max_items_per_player == 0 { + return Err(error!(RpgError::InvalidGameConfig)); + } - ctx.accounts.game.game_master = ctx.accounts.game_master.key().clone(); - ctx.accounts.game.treasury = ctx.accounts.treasury.key().clone(); - - ctx.accounts.game.action_points_collected = 0; - ctx.accounts.game.game_config.max_items_per_player = max_items_per_player; + let game = &mut ctx.accounts.game; + game.game_master = ctx.accounts.game_master.key(); + game.treasury = ctx.accounts.treasury.key(); + game.action_points_collected = 0; + game.game_config.max_items_per_player = max_items_per_player; msg!("Game created!"); - Ok(()) } ``` -#### 6. Create Player +### 6. Create Player Our second instruction will create the `player` account. There are three tradeoffs to note about this instruction: @@ -1134,58 +1213,63 @@ tradeoffs to note about this instruction: 100 lamports, but this could be something added to the game config in the future. -```rust +```rust filename="create_player.rs" // ----------- CREATE PLAYER ---------- + +// Inside src/instructions/create_player.rs +use anchor_lang::prelude::*; + +use crate::{ + error::RpgError, helpers::spend_action_points, Game, Player, ANCHOR_DISCRIMINATOR, + CREATE_PLAYER_ACTION_POINTS, NO_EFFECT_FLAG, +}; + #[derive(Accounts)] pub struct CreatePlayer<'info> { pub game: Box>, - #[account( init, - seeds=[ + seeds = [ b"PLAYER", game.key().as_ref(), player.key().as_ref() ], bump, payer = player, - space = std::mem::size_of::() + std::mem::size_of::() * game.game_config.max_items_per_player as usize + 8) - ] + space = ANCHOR_DISCRIMINATOR + Player::INIT_SPACE + )] pub player_account: Account<'info, Player>, - #[account(mut)] pub player: Signer<'info>, - pub system_program: Program<'info, System>, } pub fn run_create_player(ctx: Context) -> Result<()> { - - ctx.accounts.player_account.player = ctx.accounts.player.key().clone(); - ctx.accounts.player_account.game = ctx.accounts.game.key().clone(); - - ctx.accounts.player_account.status_flag = NO_EFFECT_FLAG; - ctx.accounts.player_account.experience = 0; - ctx.accounts.player_account.kills = 0; + let player_account = &mut ctx.accounts.player_account; + player_account.player = ctx.accounts.player.key(); + player_account.game = ctx.accounts.game.key(); + player_account.status_flag = NO_EFFECT_FLAG; + player_account.experience = 0; + player_account.kills = 0; msg!("Hero has entered the game!"); - { // Spend 100 lamports to create player - let action_points_to_spend = 100; + // Spend 100 lamports to create player + let action_points_to_spend = CREATE_PLAYER_ACTION_POINTS; - spend_action_points( - action_points_to_spend, - &mut ctx.accounts.player_account, - &ctx.accounts.player.to_account_info(), - &ctx.accounts.system_program.to_account_info() - )?; - } + spend_action_points( + action_points_to_spend, + player_account, + &ctx.accounts.player.to_account_info(), + &ctx.accounts.system_program.to_account_info(), + ) + .map_err(|_| error!(RpgError::InsufficientActionPoints))?; Ok(()) } ``` -#### 7. Spawn Monster +### 7. Spawn Monster Now that we have a way to create players, we need a way to spawn monsters for them to fight. This instruction will create a new `Monster` account whose @@ -1197,21 +1281,26 @@ decisions here we should talk about: 2. We wrap both the `game` and `player` accounts in `Box` to allocate them to the Heap -```rust +```rust filename="spawn_monster.rs" // ----------- SPAWN MONSTER ---------- + +// Inside src/instructions/spawn_monster.rs +use anchor_lang::prelude::*; + +use crate::{helpers::spend_action_points, Game, Monster, Player, SPAWN_MONSTER_ACTION_POINTS, ANCHOR_DISCRIMINATOR}; + #[derive(Accounts)] pub struct SpawnMonster<'info> { pub game: Box>, - - #[account(mut, + #[account( + mut, has_one = game, has_one = player, )] pub player_account: Box>, - #[account( init, - seeds=[ + seeds = [ b"MONSTER", game.key().as_ref(), player.key().as_ref(), @@ -1219,46 +1308,39 @@ pub struct SpawnMonster<'info> { ], bump, payer = player, - space = std::mem::size_of::() + 8) - ] + space = ANCHOR_DISCRIMINATOR + Monster::INIT_SPACE + )] pub monster: Account<'info, Monster>, - #[account(mut)] pub player: Signer<'info>, - pub system_program: Program<'info, System>, } pub fn run_spawn_monster(ctx: Context) -> Result<()> { + let monster = &mut ctx.accounts.monster; + monster.player = ctx.accounts.player.key(); + monster.game = ctx.accounts.game.key(); + monster.hitpoints = 100; - { - ctx.accounts.monster.player = ctx.accounts.player.key().clone(); - ctx.accounts.monster.game = ctx.accounts.game.key().clone(); - ctx.accounts.monster.hitpoints = 100; - - msg!("Monster Spawned!"); - } - - { - ctx.accounts.player_account.next_monster_index = ctx.accounts.player_account.next_monster_index.checked_add(1).unwrap(); - } + let player_account = &mut ctx.accounts.player_account; + player_account.next_monster_index = player_account.next_monster_index.checked_add(1).unwrap(); - { // Spend 5 lamports to spawn monster - let action_point_to_spend = 5; + msg!("Monster Spawned!"); - spend_action_points( - action_point_to_spend, - &mut ctx.accounts.player_account, - &ctx.accounts.player.to_account_info(), - &ctx.accounts.system_program.to_account_info() - )?; - } + // Spend 5 lamports to spawn monster + let action_point_to_spend = SPAWN_MONSTER_ACTION_POINTS; + spend_action_points( + action_point_to_spend, + player_account, + &ctx.accounts.player.to_account_info(), + &ctx.accounts.system_program.to_account_info(), + )?; Ok(()) } ``` -#### 8. Attack Monster +### 8. Attack Monster Now! Let’s attack those monsters and start gaining some exp! @@ -1280,133 +1362,227 @@ it’s about to overflow. Keep this in mind when doing math in Rust. Even though `kills` is a u64 and will never roll with it’s current programming, it’s good practice to use safe math and consider roll-overs. -```rust +```rust filename="attack_monster.rs" // ----------- ATTACK MONSTER ---------- + +// Inside src/instructions/attack_monster.rs +use anchor_lang::prelude::*; +use crate::{helpers::spend_action_points, Monster, Player, ATTACK_ACTION_POINTS, error::RpgError}; + #[derive(Accounts)] pub struct AttackMonster<'info> { - #[account( mut, has_one = player, )] pub player_account: Box>, - #[account( mut, has_one = player, - constraint = monster.game == player_account.game + constraint = monster.game == player_account.game @ RpgError::GameMismatch )] pub monster: Box>, - #[account(mut)] pub player: Signer<'info>, - pub system_program: Program<'info, System>, } pub fn run_attack_monster(ctx: Context) -> Result<()> { + let player_account = &mut ctx.accounts.player_account; + let monster = &mut ctx.accounts.monster; - let mut did_kill = false; - - { - let hp_before_attack = ctx.accounts.monster.hitpoints; - let hp_after_attack = ctx.accounts.monster.hitpoints.saturating_sub(1); - let damage_dealt = hp_before_attack - hp_after_attack; - ctx.accounts.monster.hitpoints = hp_after_attack; - - + let hp_before_attack = monster.hitpoints; + let hp_after_attack = monster.hitpoints.saturating_sub(1); + let damage_dealt = hp_before_attack.saturating_sub(hp_after_attack); + monster.hitpoints = hp_after_attack; - if hp_before_attack > 0 && hp_after_attack == 0 { - did_kill = true; - } - - if damage_dealt > 0 { - msg!("Damage Dealt: {}", damage_dealt); - } else { - msg!("Stop it's already dead!"); - } - } - - { - ctx.accounts.player_account.experience = ctx.accounts.player_account.experience.saturating_add(1); + if damage_dealt > 0 { + msg!("Damage Dealt: {}", damage_dealt); + player_account.experience = player_account.experience.saturating_add(1); msg!("+1 EXP"); - if did_kill { - ctx.accounts.player_account.kills = ctx.accounts.player_account.kills.saturating_add(1); + if hp_after_attack == 0 { + player_account.kills = player_account.kills.saturating_add(1); msg!("You killed the monster!"); } + } else { + msg!("Stop it's already dead!"); } - { // Spend 1 lamports to attack monster - let action_point_to_spend = 1; + // Spend 1 lamport to attack monster + let action_point_to_spend = ATTACK_ACTION_POINTS; - spend_action_points( - action_point_to_spend, - &mut ctx.accounts.player_account, - &ctx.accounts.player.to_account_info(), - &ctx.accounts.system_program.to_account_info() - )?; - } + spend_action_points( + action_point_to_spend, + player_account, + &ctx.accounts.player.to_account_info(), + &ctx.accounts.system_program.to_account_info() + )?; Ok(()) } ``` -#### Redeem to Treasury +### 9. Redeem to Treasury This is our last instruction. This instruction lets anyone send the spent `action_points` to the `treasury` wallet. Again, let's box the rpg accounts and use safe math. -```rust +```rust filename="collect_points.rs" // ----------- REDEEM TO TREASUREY ---------- + +// Inside src/instructions/collect_points.rs +use anchor_lang::prelude::*; +use crate::{error::RpgError, Game, Player}; + #[derive(Accounts)] pub struct CollectActionPoints<'info> { - #[account( mut, - has_one=treasury + has_one = treasury @ RpgError::InvalidTreasury )] pub game: Box>, - #[account( mut, - has_one=game + has_one = game @ RpgError::PlayerGameMismatch )] pub player: Box>, - #[account(mut)] /// CHECK: It's being checked in the game account - pub treasury: AccountInfo<'info>, - + pub treasury: UncheckedAccount<'info>, pub system_program: Program<'info, System>, } -// literally anyone who pays for the TX fee can run this command - give it to a clockwork bot +// Literally anyone who pays for the TX fee can run this command - give it to a clockwork bot pub fn run_collect_action_points(ctx: Context) -> Result<()> { - let transfer_amount: u64 = ctx.accounts.player.action_points_to_be_collected; + let transfer_amount = ctx.accounts.player.action_points_to_be_collected; - **ctx.accounts.player.to_account_info().try_borrow_mut_lamports()? -= transfer_amount; - **ctx.accounts.treasury.to_account_info().try_borrow_mut_lamports()? += transfer_amount; + // Transfer lamports from player to treasury + let player_info = ctx.accounts.player.to_account_info(); + let treasury_info = ctx.accounts.treasury.to_account_info(); + + **player_info.try_borrow_mut_lamports()? = player_info + .lamports() + .checked_sub(transfer_amount) + .ok_or(RpgError::InsufficientFunds)?; + + **treasury_info.try_borrow_mut_lamports()? = treasury_info + .lamports() + .checked_add(transfer_amount) + .ok_or(RpgError::ArithmeticOverflow)?; ctx.accounts.player.action_points_to_be_collected = 0; - ctx.accounts.game.action_points_collected = ctx.accounts.game.action_points_collected.checked_add(transfer_amount).unwrap(); + ctx.accounts.game.action_points_collected = ctx.accounts.game + .action_points_collected + .checked_add(transfer_amount) + .ok_or(RpgError::ArithmeticOverflow)?; - msg!("The treasury collected {} action points to treasury", transfer_amount); + msg!("The treasury collected {} action points", transfer_amount); Ok(()) } ``` -#### Putting it all Together +### 10. Error Handling + +Now, let's add all the errors that we have used till now in `errors.rs` file. + +```rust filename="errors.rs" +// ------------RPG ERRORS-------------- + +// Inside src/error/errors.rs + +use anchor_lang::prelude::*; + +#[error_code] +pub enum RpgError { + #[msg("Arithmetic overflow occurred")] + ArithmeticOverflow, + #[msg("Invalid game configuration")] + InvalidGameConfig, + #[msg("Player not found")] + PlayerNotFound, + #[msg("Monster not found")] + MonsterNotFound, + #[msg("Insufficient action points")] + InsufficientActionPoints, + #[msg("Invalid attack")] + InvalidAttack, + #[msg("Maximum inventory size reached")] + MaxInventoryReached, + #[msg("Invalid item operation")] + InvalidItemOperation, + #[msg("Monster and player are not in the same game")] + GameMismatch, + #[msg("Invalid treasury account")] + InvalidTreasury, + #[msg("Player does not belong to the specified game")] + PlayerGameMismatch, + #[msg("Insufficient funds for transfer")] + InsufficientFunds +} +``` + +### 11. Module Declarations + +We need to declare all the modules used in the project as follows: + +```rust + +// Inside src/error/mod.rs +pub mod errors; +pub use errors::RpgError; // Expose the custom error type + +// Inside src/instructions/mod.rs +pub mod attack_monster; +pub mod collect_points; +pub mod create_game; +pub mod create_player; +pub mod spawn_monster; + +pub use attack_monster::*; // Expose attack_monster functions +pub use collect_points::*; // Expose collect_points functions +pub use create_game::*; // Expose create_game functions +pub use create_player::*; // Expose create_player functions +pub use spawn_monster::*; // Expose spawn_monster functions + +// Inside src/state/mod.rs +pub mod game; +pub mod monster; +pub mod player; + +pub use game::*; // Expose game state +pub use monster::*; // Expose monster state +pub use player::*; // Expose player state +``` + +### 12. Putting it all Together Now that all of our instruction logic is written, let's add these functions to actual instructions in the program. It can also be helpful to log compute units for each instruction. -```rust +```rust filename="lib.rs" + +// Insider src/lib.rs +use anchor_lang::prelude::*; +use anchor_lang::solana_program::log::sol_log_compute_units; + +mod state; +mod instructions; +mod constants; +mod helpers; +mod error; + +use state::*; +use constants::*; +use instructions::*; + +declare_id!("5Sc3gJv4tvPiFzE75boYMJabbNRs44zRhtT23fLdKewz"); + #[program] pub mod rpg { use super::*; @@ -1440,7 +1616,6 @@ pub mod rpg { sol_log_compute_units(); Ok(()) } - } ``` @@ -1451,73 +1626,134 @@ successfully. anchor build ``` -#### Testing +### Testing -Now, let’s see this baby work! +Now, let's put everything together and see it in action! -Let’s set up the `tests/rpg.ts` file. We will be filling out each test in turn. -But first, we needed to set up a couple of different accounts. Mainly the -`gameMaster` and the `treasury`. +We’ll begin by setting up the `tests/rpg.ts` file. We will be writing each test +step by step. But before diving into the tests, we need to initialize a few +important accounts, specifically the `gameMaster` and the `treasury` accounts. -```typescript +```typescript filename="rpg.ts" import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; -import { Rpg, IDL } from "../target/types/rpg"; +import { Rpg } from "../target/types/rpg"; import { assert } from "chai"; +import { + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + TransactionSignature, + TransactionConfirmationStrategy, +} from "@solana/web3.js"; import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet"; -describe("RPG", () => { - // Configure the client to use the local cluster. - anchor.setProvider(anchor.AnchorProvider.env()); - - const program = anchor.workspace.Rpg as Program; - const wallet = anchor.workspace.Rpg.provider.wallet - .payer as anchor.web3.Keypair; - const gameMaster = wallet; - const player = wallet; - - const treasury = anchor.web3.Keypair.generate(); - - it("Create Game", async () => {}); - - it("Create Player", async () => {}); - - it("Spawn Monster", async () => {}); - - it("Attack Monster", async () => {}); - - it("Deposit Action Points", async () => {}); +const GAME_SEED = "GAME"; +const PLAYER_SEED = "PLAYER"; +const MONSTER_SEED = "MONSTER"; +const MAX_ITEMS_PER_PLAYER = 8; +const INITIAL_MONSTER_HITPOINTS = 100; +const AIRDROP_AMOUNT = 10 * LAMPORTS_PER_SOL; +const CREATE_PLAYER_ACTION_POINTS = 100; +const SPAWN_MONSTER_ACTION_POINTS = 5; +const ATTACK_MONSTER_ACTION_POINTS = 1; +const MONSTER_INDEX_BYTE_LENGTH = 8; + +const provider = anchor.AnchorProvider.env(); +anchor.setProvider(provider); + +const program = anchor.workspace.Rpg as Program; +const wallet = provider.wallet as NodeWallet; +const gameMaster = wallet; +const player = wallet; + +const treasury = Keypair.generate(); + +const findProgramAddress = (seeds: Buffer[]): [PublicKey, number] => + PublicKey.findProgramAddressSync(seeds, program.programId); + +const confirmTransaction = async ( + signature: TransactionSignature, + provider: anchor.Provider, +) => { + const latestBlockhash = await provider.connection.getLatestBlockhash(); + const confirmationStrategy: TransactionConfirmationStrategy = { + signature, + blockhash: latestBlockhash.blockhash, + lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, + }; + + try { + const confirmation = + await provider.connection.confirmTransaction(confirmationStrategy); + if (confirmation.value.err) { + throw new Error( + `Transaction failed: ${confirmation.value.err.toString()}`, + ); + } + } catch (error) { + throw new Error(`Transaction confirmation failed: ${error.message}`); + } +}; + +const createGameAddress = () => + findProgramAddress([Buffer.from(GAME_SEED), treasury.publicKey.toBuffer()]); + +const createPlayerAddress = (gameAddress: PublicKey) => + findProgramAddress([ + Buffer.from(PLAYER_SEED), + gameAddress.toBuffer(), + player.publicKey.toBuffer(), + ]); + +const createMonsterAddress = ( + gameAddress: PublicKey, + monsterIndex: anchor.BN, +) => + findProgramAddress([ + Buffer.from(MONSTER_SEED), + gameAddress.toBuffer(), + player.publicKey.toBuffer(), + monsterIndex.toArrayLike(Buffer, "le", MONSTER_INDEX_BYTE_LENGTH), + ]); + +describe("RPG game", () => { + it("creates a new game", async () => {}); + + it("creates a new player", async () => {}); + + it("spawns a monster", async () => {}); + + it("attacks a monster", async () => {}); + + it("deposits action points", async () => {}); }); ``` -Now lets add in the `Create Game` test. Just call `createGame` with eight items, -be sure to pass in all the accounts, and make sure the `treasury` account signs -the transaction. +Now lets add in the `creates a new game` test. Just call `createGame` with eight +items, be sure to pass in all the accounts, and make sure the `treasury` account +signs the transaction. ```typescript -it("Create Game", async () => { - const [gameKey] = anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("GAME"), treasury.publicKey.toBuffer()], - program.programId, - ); - - const txHash = await program.methods - .createGame( - 8, // 8 Items per player - ) - .accounts({ - game: gameKey, - gameMaster: gameMaster.publicKey, - treasury: treasury.publicKey, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .signers([treasury]) - .rpc(); - - await program.provider.connection.confirmTransaction(txHash); - - // Print out if you'd like - // const account = await program.account.game.fetch(gameKey); +it("creates a new game", async () => { + try { + const [gameAddress] = createGameAddress(); + + const createGameSignature = await program.methods + .createGame(MAX_ITEMS_PER_PLAYER) + .accounts({ + game: gameAddress, + gameMaster: gameMaster.publicKey, + treasury: treasury.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + }) + .signers([treasury]) + .rpc(); + + await confirmTransaction(createGameSignature, provider); + } catch (error) { + throw new Error(`Failed to create game: ${error.message}`); + } }); ``` @@ -1532,118 +1768,91 @@ anchor test some `.pnp.*` files and no `node_modules`, you may want to call `rm -rf .pnp.*` followed by `npm i` and then `yarn install`. That should work. -Now that everything is running, let’s implement the `Create Player`, -`Spawn Monster`, and `Attack Monster` tests. Run each test as you complete them -to make sure things are running smoothly. +Now that everything is running, let’s implement the `creates a new player`, +`spawns a monster`, and `attacks a monster` tests. Run each test as you complete +them to make sure things are running smoothly. ```typescript -it("Create Player", async () => { - const [gameKey] = anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("GAME"), treasury.publicKey.toBuffer()], - program.programId, - ); - - const [playerKey] = anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("PLAYER"), gameKey.toBuffer(), player.publicKey.toBuffer()], - program.programId, - ); - - const txHash = await program.methods - .createPlayer() - .accounts({ - game: gameKey, - playerAccount: playerKey, - player: player.publicKey, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .rpc(); - - await program.provider.connection.confirmTransaction(txHash); - - // Print out if you'd like - // const account = await program.account.player.fetch(playerKey); +it("creates a new player", async () => { + try { + const [gameAddress] = createGameAddress(); + const [playerAddress] = createPlayerAddress(gameAddress); + + const createPlayerSignature = await program.methods + .createPlayer() + .accounts({ + game: gameAddress, + playerAccount: playerAddress, + player: player.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + }) + .rpc(); + + await confirmTransaction(createPlayerSignature, provider); + } catch (error) { + throw new Error(`Failed to create player: ${error.message}`); + } }); -it("Spawn Monster", async () => { - const [gameKey] = anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("GAME"), treasury.publicKey.toBuffer()], - program.programId, - ); - - const [playerKey] = anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("PLAYER"), gameKey.toBuffer(), player.publicKey.toBuffer()], - program.programId, - ); +it("spawns a monster", async () => { + try { + const [gameAddress] = createGameAddress(); + const [playerAddress] = createPlayerAddress(gameAddress); - const playerAccount = await program.account.player.fetch(playerKey); + const playerAccount = await program.account.player.fetch(playerAddress); + const [monsterAddress] = createMonsterAddress( + gameAddress, + playerAccount.nextMonsterIndex, + ); - const [monsterKey] = anchor.web3.PublicKey.findProgramAddressSync( - [ - Buffer.from("MONSTER"), - gameKey.toBuffer(), - player.publicKey.toBuffer(), - playerAccount.nextMonsterIndex.toBuffer("le", 8), - ], - program.programId, - ); - - const txHash = await program.methods - .spawnMonster() - .accounts({ - game: gameKey, - playerAccount: playerKey, - monster: monsterKey, - player: player.publicKey, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .rpc(); - - await program.provider.connection.confirmTransaction(txHash); - - // Print out if you'd like - // const account = await program.account.monster.fetch(monsterKey); + const spawnMonsterSignature = await program.methods + .spawnMonster() + .accounts({ + game: gameAddress, + playerAccount: playerAddress, + monster: monsterAddress, + player: player.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + }) + .rpc(); + + await confirmTransaction(spawnMonsterSignature, provider); + } catch (error) { + throw new Error(`Failed to spawn monster: ${error.message}`); + } }); -it("Attack Monster", async () => { - const [gameKey] = anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("GAME"), treasury.publicKey.toBuffer()], - program.programId, - ); +it("attacks a monster", async () => { + try { + const [gameAddress] = createGameAddress(); + const [playerAddress] = createPlayerAddress(gameAddress); - const [playerKey] = anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("PLAYER"), gameKey.toBuffer(), player.publicKey.toBuffer()], - program.programId, - ); + const playerAccount = await program.account.player.fetch(playerAddress); + const [monsterAddress] = createMonsterAddress( + gameAddress, + playerAccount.nextMonsterIndex.subn(1), + ); - // Fetch the latest monster created - const playerAccount = await program.account.player.fetch(playerKey); - const [monsterKey] = anchor.web3.PublicKey.findProgramAddressSync( - [ - Buffer.from("MONSTER"), - gameKey.toBuffer(), - player.publicKey.toBuffer(), - playerAccount.nextMonsterIndex.subn(1).toBuffer("le", 8), - ], - program.programId, - ); - - const txHash = await program.methods - .attackMonster() - .accounts({ - playerAccount: playerKey, - monster: monsterKey, - player: player.publicKey, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .rpc(); - - await program.provider.connection.confirmTransaction(txHash); - - // Print out if you'd like - // const account = await program.account.monster.fetch(monsterKey); - - const monsterAccount = await program.account.monster.fetch(monsterKey); - assert(monsterAccount.hitpoints.eqn(99)); + const attackMonsterSignature = await program.methods + .attackMonster() + .accounts({ + playerAccount: playerAddress, + monster: monsterAddress, + player: player.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + }) + .rpc(); + + await confirmTransaction(attackMonsterSignature, provider); + + const monsterAccount = await program.account.monster.fetch(monsterAddress); + assert( + monsterAccount.hitpoints.eqn(INITIAL_MONSTER_HITPOINTS - 1), + "Monster hitpoints should decrease by 1 after attack", + ); + } catch (error) { + throw new Error(`Failed to attack monster: ${error.message}`); + } }); ``` @@ -1663,87 +1872,97 @@ game were running continuously, it probably makes sense to use something like [clockwork](https://www.clockwork.xyz/) cron jobs. ```typescript -it("Deposit Action Points", async () => { - const [gameKey] = anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("GAME"), treasury.publicKey.toBuffer()], - program.programId, - ); - - const [playerKey] = anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("PLAYER"), gameKey.toBuffer(), player.publicKey.toBuffer()], - program.programId, - ); - - // To show that anyone can deposit the action points - // Ie, give this to a clockwork bot - const clockworkWallet = anchor.web3.Keypair.generate(); - - // To give it a starting balance - const clockworkProvider = new anchor.AnchorProvider( - program.provider.connection, - new NodeWallet(clockworkWallet), - anchor.AnchorProvider.defaultOptions(), - ); - const clockworkProgram = new anchor.Program( - IDL, - program.programId, - clockworkProvider, - ); - - // Have to give the accounts some lamports else the tx will fail - const amountToInitialize = 10000000000; - - const clockworkAirdropTx = - await clockworkProgram.provider.connection.requestAirdrop( - clockworkWallet.publicKey, - amountToInitialize, +it("deposits action points", async () => { + try { + const [gameAddress] = createGameAddress(); + const [playerAddress] = createPlayerAddress(gameAddress); + + // To show that anyone can deposit the action points + // Ie, give this to a clockwork bot + const clockworkWallet = anchor.web3.Keypair.generate(); + + // To give it a starting balance + const clockworkProvider = new anchor.AnchorProvider( + program.provider.connection, + new NodeWallet(clockworkWallet), + anchor.AnchorProvider.defaultOptions(), ); - await program.provider.connection.confirmTransaction( - clockworkAirdropTx, - "confirmed", - ); - const treasuryAirdropTx = - await clockworkProgram.provider.connection.requestAirdrop( + // Have to give the accounts some lamports else the tx will fail + const amountToInitialize = 10000000000; + + const clockworkAirdropTx = + await clockworkProvider.connection.requestAirdrop( + clockworkWallet.publicKey, + amountToInitialize, + ); + + await confirmTransaction(clockworkAirdropTx, clockworkProvider); + + const treasuryAirdropTx = await clockworkProvider.connection.requestAirdrop( treasury.publicKey, amountToInitialize, ); - await program.provider.connection.confirmTransaction( - treasuryAirdropTx, - "confirmed", - ); - - const txHash = await clockworkProgram.methods - .depositActionPoints() - .accounts({ - game: gameKey, - player: playerKey, - treasury: treasury.publicKey, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .rpc(); - - await program.provider.connection.confirmTransaction(txHash); - - const expectedActionPoints = 100 + 5 + 1; // Player Create ( 100 ) + Monster Spawn ( 5 ) + Monster Attack ( 1 ) - const treasuryBalance = await program.provider.connection.getBalance( - treasury.publicKey, - ); - assert( - treasuryBalance == amountToInitialize + expectedActionPoints, // Player Create ( 100 ) + Monster Spawn ( 5 ) + Monster Attack ( 1 ) - ); - - const gameAccount = await program.account.game.fetch(gameKey); - assert(gameAccount.actionPointsCollected.eqn(expectedActionPoints)); - - const playerAccount = await program.account.player.fetch(playerKey); - assert(playerAccount.actionPointsSpent.eqn(expectedActionPoints)); - assert(playerAccount.actionPointsToBeCollected.eqn(0)); + + await confirmTransaction(treasuryAirdropTx, clockworkProvider); + + const depositActionPointsSignature = await program.methods + .depositActionPoints() + .accounts({ + game: gameAddress, + player: playerAddress, + treasury: treasury.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + }) + .rpc(); + + await confirmTransaction(depositActionPointsSignature, provider); + + const expectedActionPoints = + CREATE_PLAYER_ACTION_POINTS + + SPAWN_MONSTER_ACTION_POINTS + + ATTACK_MONSTER_ACTION_POINTS; + const treasuryBalance = await provider.connection.getBalance( + treasury.publicKey, + ); + assert( + treasuryBalance === AIRDROP_AMOUNT + expectedActionPoints, + "Treasury balance should match expected action points", + ); + + const gameAccount = await program.account.game.fetch(gameAddress); + assert( + gameAccount.actionPointsCollected.eqn(expectedActionPoints), + "Game action points collected should match expected", + ); + + const playerAccount = await program.account.player.fetch(playerAddress); + assert( + playerAccount.actionPointsSpent.eqn(expectedActionPoints), + "Player action points spent should match expected", + ); + assert( + playerAccount.actionPointsToBeCollected.eqn(0), + "Player should have no action points to be collected", + ); + } catch (error) { + throw new Error(`Failed to deposit action points: ${error.message}`); + } }); ``` Finally, run `anchor test` to see everything working. +```bash + +RPG game + ✔ creates a new game (317ms) + ✔ creates a new player (399ms) + ✔ spawns a monster (411ms) + ✔ attacks a monster (413ms) + ✔ deposits action points (1232ms) +``` + Congratulations! This was a lot to cover, but you now have a mini RPG game engine. If things aren't quite working, go back through the lab and find where you went wrong. If you need, you can refer to the @@ -1758,14 +1977,14 @@ Now it’s your turn to practice independently. Go back through the lab code looking for additional optimizations and/or expansion you can make. Think through new systems and features you would add and how you would optimize them. -You can find some example modifications on the `challenge-solution` branch of -the -[RPG repository](https://github.com/Unboxed-Software/anchor-rpg/tree/challenge-solution). +You can find some example modifications on the +[`challenge-solution` branch of the RPG repository](https://github.com/Unboxed-Software/anchor-rpg/tree/challenge-solution). Finally, go through one of your own programs and think about optimizations you can make to improve memory management, storage size, and/or concurrency. + Push your code to GitHub and [tell us what you thought of this lesson](https://form.typeform.com/to/IPH0UGz7#answers-lesson=4a628916-91f5-46a9-8eb0-6ba453aa6ca6)! From c8f06fa989320afaac17e56281cdd5021f69098a Mon Sep 17 00:00:00 2001 From: 0xCipherCoder Date: Sun, 15 Sep 2024 03:59:11 +0530 Subject: [PATCH 3/6] Fixed grammar mistake --- .../program-architecture.md | 183 +++++++++--------- 1 file changed, 92 insertions(+), 91 deletions(-) diff --git a/content/courses/program-optimization/program-architecture.md b/content/courses/program-optimization/program-architecture.md index 581c61d47..dd1fdb407 100644 --- a/content/courses/program-optimization/program-architecture.md +++ b/content/courses/program-optimization/program-architecture.md @@ -13,7 +13,7 @@ description: "Design your Solana programs efficiently." - If your data accounts are too large for the Stack, wrap them in `Box` to allocate them to the Heap - Use Zero-Copy to deal with accounts that are too large for `Box` (< 10MB) -- The size and the order of fields in an account matter; put variable length +- The size and the order of fields in an account matter; put the variable length fields at the end - Solana can process in parallel, but you can still run into bottlenecks; be mindful of "shared" accounts that all users interacting with the program have @@ -31,7 +31,7 @@ with the code. And you, as the designer, need to think about: These questions are even more important when developing for a blockchain. Not only are resources more limited than in a typical computing environment, you're -also dealing with people’s assets; code has a cost now. +also dealing with people's assets; code has a cost now. We'll leave most of the asset handling discussion to [security course lesson](/content/courses/program-security/security-intro), but @@ -41,18 +41,18 @@ there are limitations unique to blockchain and Solana development such as how much data can be stored in an account, the cost to store that data, and how many compute units are available per transaction. You, the program designer, have to be mindful of these limitations to create programs that are affordable, fast, -safe, and functional. Today we will be delving into some of the more advance +safe, and functional. Today we will be delving into some of the more advanced considerations that should be taken when creating Solana programs. ### Dealing With Large Accounts -In modern application programming, we don’t often have to think about the size -of the data structures we are using. You want to make a string? You can put a -4000 character limit on it if you want to avoid abuse, but it's probably not an -issue. Want an integer? They’re pretty much always 32-bit for convenience. +In modern application programming, we don't often have to think about the size +of the data structures we are using. Do you want to make a string? You can put a +4000-character limit on it if you want to avoid abuse, but it's probably not an +issue. Want an integer? They're pretty much always 32-bit for convenience. -In high level languages, you are in the data-land-o-plenty! Now, in Solana land, -we pay per byte stored (rent) and have limits on heap, stack and account sizes. +In high-level languages, you are in the data-land-o-plenty! Now, in Solana land, +we pay per byte stored (rent) and have limits on heap, stack, and account sizes. We have to be a little more crafty with our bytes. There are two main concerns we are going to be looking at in this section: @@ -63,7 +63,7 @@ we are going to be looking at in this section: 2. When operating on larger data, we run into [Stack](https://solana.com/docs/programs/faq#stack) and [Heap](https://solana.com/docs/programs/faq#heap-size) constraints - to get - around these, we’ll look at using Box and Zero-Copy. + around these, we'll look at using Box and Zero-Copy. #### Sizes @@ -80,7 +80,7 @@ now there's an enforced minimum rent exemption. You can read about it in Putting data on the blockchain can be expensive, which is why NFT attributes and associated files, like images, are stored offchain. The goal is to strike a -balance between keeping your program highly functional while ensuring that users +balance between keeping your program highly functional and ensuring that users aren't discouraged by the cost of storing data onchain. The first step in optimizing for space in your program is understanding the size @@ -108,8 +108,8 @@ of your structs. Below is a helpful reference from the Knowing these, start thinking about little optimizations you might take in a program. For example, if you have an integer field that will only ever reach -100, don’t use a u64/i64, use a u8. Why? Because a u64 takes up 8 bytes, with a -max value of 2^64 or 1.84 \* 10^19. Thats a waste of space since you only need +100, don't use a u64/i64, use a u8. Why? Because a u64 takes up 8 bytes, with a +max value of 2^64 or 1.84 \* 10^19. That's a waste of space since you only need to accommodate numbers up to 100. A single byte will give you a max value of 255 which, in this case, would be sufficient. Similarly, there's no reason to use i8 if you'll never have negative numbers. @@ -126,8 +126,8 @@ If you want to read more about Anchor sizes, take a look at #### Box -Now that you know a little bit about data sizes, let’s skip forward and look at -a problem you’ll run into if you want to deal with larger data accounts. Say you +Now that you know a little bit about data sizes, let's skip forward and look at +a problem you'll run into if you want to deal with larger data accounts. Say you have the following data account: ```rust @@ -143,7 +143,7 @@ pub struct SomeFunctionContext<'info> { ``` If you try to pass `SomeBigDataStruct` into the function with the -`SomeFunctionContext` context, you’ll run into the following compiler warning: +`SomeFunctionContext` context, you'll run into the following compiler warning: `// Stack offset of XXXX exceeded max offset of 4096 by XXXX bytes, please minimize large stack variables` @@ -175,7 +175,7 @@ pub struct SomeFunctionContext<'info> { In Anchor, **`Box`** is used to allocate the account to the Heap, not the Stack. Which is great since the Heap gives us 32KB to work with. The best part -is you don’t have to do anything different within the function. All you need to +is you don't have to do anything different within the function. All you need to do is add `Box<…>` around all of your big data accounts. But Box is not perfect. You can still overflow the stack with sufficiently large @@ -183,7 +183,7 @@ accounts. We'll learn how to fix this in the next section. #### Zero Copy -Okay, so now you can deal with medium sized accounts using `Box`. But what if +Okay, so now you can deal with medium-sized accounts using `Box`. But what if you need to use really big accounts like the max size of 10MB? Take the following as an example: @@ -221,13 +221,13 @@ To understand what's happening here, take a look at the [rust Anchor documentation](https://docs.rs/anchor-lang/latest/anchor_lang/attr.account.html) > Other than being more efficient, the most salient benefit [`zero_copy`] -> provides is the ability to define account types larger than the max stack or -> heap size. When using borsh, the account has to be copied and deserialized -> into a new data structure and thus is constrained by stack and heap limits -> imposed by the BPF VM. With zero copy deserialization, all bytes from the -> account’s backing `RefCell<&mut [u8]>` are simply re-interpreted as a -> reference to the data structure. No allocations or copies necessary. Hence the -> ability to get around stack and heap limitations. +> provides the ability to define account types larger than the max stack or heap +> size. When using borsh, the account has to be copied and deserialized into a +> new data structure and thus is constrained by stack and heap limits imposed by +> the BPF VM. With zero copy deserialization, all bytes from the account's +> backing `RefCell<&mut [u8]>` are simply re-interpreted as a reference to the +> data structure. No allocations or copies necessary. Hence the ability to get +> around stack and heap limitations. Basically, your program never actually loads zero-copy account data into the stack or heap. It instead gets pointer access to the raw data. The @@ -245,7 +245,7 @@ pub struct ConceptZeroCopy<'info> { } ``` -Instead, your client has to create the large account and pay for it’s rent in a +Instead, your client has to create a large account and pay for its rent in a separate instruction. ```typescript @@ -272,16 +272,16 @@ const txHash = await program.methods .rpc(); ``` -The second caveat is that your'll have to call one of the following methods from +The second caveat is that you'll have to call one of the following methods from inside your rust instruction function to load the account: - `load_init` when first initializing an account (this will ignore the missing - account discriminator that gets added only after the user’s instruction code) + account discriminator that gets added only after the user's instruction code) - `load` when the account is not mutable - `load_mut` when the account is mutable For example, if you wanted to init and manipulate the `SomeReallyBigDataStruct` -from above, you’d call the following in the function +from above, you'd call the following in the function ```rust let some_really_big_data = &mut ctx.accounts.some_really_big_data.load_init()?; @@ -290,16 +290,16 @@ let some_really_big_data = &mut ctx.accounts.some_really_big_data.load_init()?; After you do that, then you can treat the account like normal! Go ahead and experiment with this in the code yourself to see everything in action! -For a better understanding on how this all works, Solana put together a really +For a better understanding of how this all works, Solana put together a really nice [video](https://www.youtube.com/watch?v=zs_yU0IuJxc&feature=youtu.be) and [code](https://github.com/solana-developers/anchor-zero-copy-example) explaining Box and Zero-Copy in vanilla Solana. ### Dealing with Accounts -Now that you know the nuts and bolts of space consideration on Solana, let’s -look at some higher level considerations. In Solana, everything is an account, -so for the next couple sections we'll look at some account architecture +Now that you know the nuts and bolts of space consideration on Solana, let's +look at some higher-level considerations. In Solana, everything is an account, +so for the next couple sections, we'll look at some account architecture concepts. #### Data Order @@ -321,7 +321,7 @@ the location of `id` on the memory map. To make this more clear, observe what this account's data looks like onchain when `flags` has four items in the vector vs eight items. If you were to call -`solana account ACCOUNT_KEY` you’d get a data dump like the following: +`solana account ACCOUNT_KEY` you'd get a data dump like the following: ```rust 0000: 74 e4 28 4e d9 ec 31 0a -> Account Discriminator (8) @@ -345,11 +345,10 @@ the data in the `flags` field took up four more bytes. The main problem with this is lookup. When you query Solana, you use filters that look at the raw data of an account. These are called a `memcmp` filters, or memory compare filters. You give the filter an `offset` and `bytes`, and the -filter then looks directly at the memory, offsetting from the start by the -`offset` you provide, and compares the bytes in memory to the `bytes` you -provide. +filter then looks directly at the memory, offset from the start by the `offset` +you provide, and compares the bytes in memory to the `bytes` you provide. -For example, you know that the `flags` struct will always start at address +For example, you know that the `flags` struct will always start at the address 0x0008 since the first 8 bytes contain the account discriminator. Querying all accounts where the `flags` length is equal to four is possible because we _know_ that the four bytes at 0x0008 represent the length of the data in `flags`. Since @@ -368,11 +367,11 @@ const states = await program.account.badState.all([ However, if you wanted to query by the `id`, you wouldn't know what to put for the `offset` since the location of `id` is variable based on the length of -`flags`. That doesn’t seem very helpful. IDs are usually there to help with +`flags`. That doesn't seem very helpful. IDs are usually there to help with queries! The simple fix is to flip the order. ```rust -#[account] // Anchor hides the account disriminator +#[account] // Anchor hides the account discriminator pub struct GoodState { pub id: u32 // 0xDEAD_BEEF pub flags: Vec, // 0x11, 0x22, 0x33 ... @@ -386,9 +385,9 @@ structs at the end of the account. #### For Future Use -In certain cases, consider adding extra, unused bytes to you accounts. These are -held in reserve for flexibility and backward compatibility. Take the following -example: +In certain cases, consider adding extra, unused bytes to your accounts. These +are held in reserve for flexibility and backward compatibility. Take the +following example: ```rust #[account] @@ -454,13 +453,13 @@ add in some `for_future_use` bytes. #### Data Optimization The idea here is to be aware of wasted bits. For example, if you have a field -that represents the month of the year, don’t use a `u64`. There will only ever +that represents the month of the year, don't use a `u64`. There will only ever be 12 months. Use a `u8`. Better yet, use a `u8` Enum and label the months. To get even more aggressive on bit savings, be careful with booleans. Look at the below struct composed of eight boolean flags. While a boolean _can_ be represented as a single bit, borsh deserialization will allocate an entire byte -to each of these fields. that means that eight booleans winds up being eight +to each of these fields. That means that eight booleans wind up being eight bytes instead of eight bits, an eight times increase in size. ```rust @@ -537,10 +536,10 @@ Depending on the seeding you can create all sorts of relationships: program. For example, if your program needs a lookup table, you could seed it with `seeds=[b"Lookup"]`. Just be careful to provide appropriate access restrictions. -- One-Per-Owner - Say you’re creating a video game player account and you only - want one player account per wallet. Then you’d seed the account with - `seeds=[b"PLAYER", owner.key().as_ref()]`. This way, you’ll always know where - to look for a wallet’s player account **and** there can only ever be one of +- One-Per-Owner - Say you're creating a video game player account and you only + want one player account per wallet. Then you'd seed the account with + `seeds=[b"PLAYER", owner.key().as_ref()]`. This way, you'll always know where + to look for a wallet's player account **and** there can only ever be one of them. - Multiple-Per-Owner - Okay, but what if you want multiple accounts per wallet? Say you want to mint podcast episodes. Then you could seed your `Podcast` @@ -556,8 +555,8 @@ From there you can mix and match in all sorts of clever ways! But the preceding list should give you enough to get started. The big benefit of really paying attention to this aspect of design is answering -the ‘indexing’ problem. Without PDAs and seeds, all users would have to keep -track of all of the addresses of all of the accounts they’ve ever used. This +the ‘indexing' problem. Without PDAs and seeds, all users would have to keep +track of all of the addresses of all of the accounts they've ever used. This isn't feasible for users, so they'd have to depend on a centralized entity to store their addresses in a database. In many ways that defeats the purpose of a globally distributed network. PDAs are a much better solution. @@ -584,7 +583,7 @@ seeds=[b"Podcast", channel_account.key().as_ref(), episode_number.to_be_bytes(). You can always find the channel account for a particular owner. And since the channel stores the number of episodes created, you always know the upper bound -of where to search for queries. Additionally you always know what index to +of where to search for queries. Additionally, you always know what index to create a new episode at: `index = episodes_created`. ```rust @@ -600,24 +599,24 @@ Podcast X: seeds=[b"Podcast", channel_account.key().as_ref(), X.to_be_bytes().as One of the main reasons to choose Solana for your blockchain environment is its parallel transaction execution. That is, Solana can run transactions in parallel as long as those transactions aren't trying to write data to the same account. -This improves program throughput out of the box, but with some proper planning +This improves program throughput out of the box, but with some proper planning, you can avoid concurrency issues and really boost your program's performance. #### Shared Accounts -If you’ve been around crypto for a while, you may have experienced a big NFT -mint event. A new NFT project is coming out, everyone is really excited for it, -and then the candymachine goes live. It’s a mad dash to click +If you've been around crypto for a while, you may have experienced a big NFT +mint event. A new NFT project is coming out, everyone is really excited about +it, and then the candymachine goes live. It's a mad dash to click `accept transaction` as fast as you can. If you were clever, you may have -written a bot to enter in the transactions faster that the website’s UI could. -This mad rush to mint creates a lot of failed transactions. But why? Because -everyone is trying to write data to the same Candy Machine account. +written a bot to enter the transactions faster than the website's UI could. This +mad rush to mint creates a lot of failed transactions. But why? Because everyone +is trying to write data to the same Candy Machine account. Take a look at a simple example: Alice and Bob are trying to pay their friends Carol and Dean respectively. All -four accounts change, but neither depend on each other. Both transactions can -run at the same time. +four accounts change, but neither depends on other. Both transactions can run at +the same time. ```rust Alice -- pays --> Carol @@ -635,7 +634,7 @@ Bob -- pays --- | ``` Since both of these transactions write to Carol's token account, only one of -them can go through at a time. Fortunately, Solana is wicked fast, so it’ll +them can go through at a time. Fortunately, Solana is wicked fast, so it'll probably seem like they get paid at the same time. But what happens if more than just Alice and Bob try to pay Carol? @@ -659,7 +658,7 @@ trying to write data to the same account all at once. Imagine you create a super popular program and you want to take a fee on every transaction you process. For accounting reasons, you want all of those fees to go to one wallet. With that setup, on a surge of users, your protocol will -become slow and or become unreliable. Not great. So what’s the solution? +become slow and or become unreliable. Not great. So what's the solution? Separate the data transaction from the fee transaction. For example, imagine you have a data account called `DonationTally`. Its only @@ -676,7 +675,7 @@ pub struct DonationTally { } ``` -First let’s look at the suboptimal solution. +First, let's look at the suboptimal solution. ```rust pub fn run_concept_shared_account_bottleneck(ctx: Context, lamports_to_donate: u64) -> Result<()> { @@ -708,8 +707,8 @@ pub fn run_concept_shared_account_bottleneck(ctx: Context + +The `treasury` is a signer on the instruction. This is to make sure whoever is +creating the game has the private keys to the `treasury`. This is a design +decision rather than "the right way." Ultimately, it's a security measure to +ensure the game master will be able to retrieve their funds. ```rust filename="create_game.rs" // ----------- CREATE GAME ---------- @@ -1273,7 +1274,7 @@ pub fn run_create_player(ctx: Context) -> Result<()> { Now that we have a way to create players, we need a way to spawn monsters for them to fight. This instruction will create a new `Monster` account whose -address is a PDA derived with the `game` account, `player` account, and an index +address is a PDA derived from the `game` account, `player` account, and an index representing the number of monsters the player has faced. There are two design decisions here we should talk about: @@ -1342,7 +1343,7 @@ pub fn run_spawn_monster(ctx: Context) -> Result<()> { ### 8. Attack Monster -Now! Let’s attack those monsters and start gaining some exp! +Now! Let's attack those monsters and start gaining some exp! The logic here is as follows: @@ -1356,10 +1357,10 @@ incrementing experience and kill counts. The `saturating_add` function ensures the number will never overflow. Say the `kills` was a u8 and my current kill count was 255 (0xFF). If I killed another and added normally, e.g. `255 + 1 = 0 (0xFF + 0x01 = 0x00) = 0`, the kill count -would end up as 0. `saturating_add` will keep it at its max if it’s about to +would end up as 0. `saturating_add` will keep it at its max if it's about to roll over, so `255 + 1 = 255`. The `checked_add` function will throw an error if -it’s about to overflow. Keep this in mind when doing math in Rust. Even though -`kills` is a u64 and will never roll with it’s current programming, it’s good +it's about to overflow. Keep this in mind when doing math in Rust. Even though +`kills` is a u64 and will never roll with it's current programming, it's good practice to use safe math and consider roll-overs. ```rust filename="attack_monster.rs" @@ -1630,7 +1631,7 @@ anchor build Now, let's put everything together and see it in action! -We’ll begin by setting up the `tests/rpg.ts` file. We will be writing each test +We'll begin by setting up the `tests/rpg.ts` file. We will be writing each test step by step. But before diving into the tests, we need to initialize a few important accounts, specifically the `gameMaster` and the `treasury` accounts. @@ -1768,7 +1769,7 @@ anchor test some `.pnp.*` files and no `node_modules`, you may want to call `rm -rf .pnp.*` followed by `npm i` and then `yarn install`. That should work. -Now that everything is running, let’s implement the `creates a new player`, +Now that everything is running, let's implement the `creates a new player`, `spawns a monster`, and `attacks a monster` tests. Run each test as you complete them to make sure things are running smoothly. @@ -1860,7 +1861,7 @@ Notice the monster that we choose to attack is `playerAccount.nextMonsterIndex.subn(1).toBuffer('le', 8)`. This allows us to attack the most recent monster spawned. Anything below the `nextMonsterIndex` should be okay. Lastly, since seeds are just an array of bytes we have to turn -the index into the u64, which is little endian `le` at 8 bytes. +the index into the u64, which is a little endian `le` at 8 bytes. Run `anchor test` to deal some damage! @@ -1965,7 +1966,7 @@ RPG game Congratulations! This was a lot to cover, but you now have a mini RPG game engine. If things aren't quite working, go back through the lab and find where -you went wrong. If you need, you can refer to the +you went wrong. If you need to, you can refer to the [`main` branch of the solution code](https://github.com/Unboxed-Software/anchor-rpg). Be sure to put these concepts into practice in your own programs. Each little @@ -1973,8 +1974,8 @@ optimization adds up! ## Challenge -Now it’s your turn to practice independently. Go back through the lab code -looking for additional optimizations and/or expansion you can make. Think +Now it's your turn to practice independently. Go back through the lab code +looking for additional optimizations and/or expansions you can make. Think through new systems and features you would add and how you would optimize them. You can find some example modifications on the From d8256d0d1facae10e97fce3e7ede51beb7910981 Mon Sep 17 00:00:00 2001 From: 0xCipherCoder Date: Sun, 15 Sep 2024 15:24:10 +0530 Subject: [PATCH 4/6] Fixed content and formatting as per comments --- .../program-architecture.md | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/content/courses/program-optimization/program-architecture.md b/content/courses/program-optimization/program-architecture.md index dd1fdb407..3167667bc 100644 --- a/content/courses/program-optimization/program-architecture.md +++ b/content/courses/program-optimization/program-architecture.md @@ -31,18 +31,19 @@ with the code. And you, as the designer, need to think about: These questions are even more important when developing for a blockchain. Not only are resources more limited than in a typical computing environment, you're -also dealing with people's assets; code has a cost now. +also dealing with people's assets. We'll leave most of the asset handling discussion to -[security course lesson](/content/courses/program-security/security-intro), but -it's important to note the nature of resource limitations in Solana development. -There are, of course, limitations in a typical development environment, but -there are limitations unique to blockchain and Solana development such as how -much data can be stored in an account, the cost to store that data, and how many -compute units are available per transaction. You, the program designer, have to -be mindful of these limitations to create programs that are affordable, fast, -safe, and functional. Today we will be delving into some of the more advanced -considerations that should be taken when creating Solana programs. +[security course lesson](/content/courses/program-security/security-intro.md), +but it's important to note the nature of resource limitations in Solana +development. There are, of course, limitations in a typical development +environment, but there are limitations unique to blockchain and Solana +development such as how much data can be stored in an account, the cost to store +that data, and how many compute units are available per transaction. You, the +program designer, have to be mindful of these limitations to create programs +that are affordable, fast, safe, and functional. Today we will be delving into +some of the more advanced considerations that should be taken when creating +Solana programs. ### Dealing With Large Accounts @@ -74,9 +75,11 @@ called [rent](https://solana.com/docs/core/fees#rent). Rent is a bit of a misnomer since it never gets permanently taken. Once you deposit rent into the account, that data can stay there forever, or you can get -refunded the rent if you close the account. Rent used to be an actual thing, but -now there's an enforced minimum rent exemption. You can read about it in -[the Solana documentation](https://solana.com/docs/core/fees#rent-exempt). +refunded the rent if you close the account. Previously, rent was paid in +intervals, similar to traditional rent, but now there's an enforced minimum +balance for rent exemption. You can read more about it in +[the Solana documentation](https://solana.com/docs/core/fees#rent-exempt). + Putting data on the blockchain can be expensive, which is why NFT attributes and associated files, like images, are stored offchain. The goal is to strike a @@ -325,13 +328,13 @@ when `flags` has four items in the vector vs eight items. If you were to call ```rust 0000: 74 e4 28 4e d9 ec 31 0a -> Account Discriminator (8) -0008: 04 00 00 00 11 22 33 44 -> Vec Size (4) | Data 4*(1) +0008: 04 00 00 00 11 22 33 44 -> Vec Size (4) | Data 4*(1) 0010: DE AD BE EF -> id (4) --- vs --- 0000: 74 e4 28 4e d9 ec 31 0a -> Account Discriminator (8) -0008: 08 00 00 00 11 22 33 44 -> Vec Size (8) | Data 4*(1) +0008: 08 00 00 00 11 22 33 44 -> Vec Size (8) | Data 4*(1) 0010: 55 66 77 88 DE AD BE EF -> Data 4*(1) | id (4) ``` @@ -373,7 +376,7 @@ queries! The simple fix is to flip the order. ```rust #[account] // Anchor hides the account discriminator pub struct GoodState { - pub id: u32 // 0xDEAD_BEEF + pub id: u32 // 0xDEAD_BEEF pub flags: Vec, // 0x11, 0x22, 0x33 ... } ``` @@ -424,7 +427,7 @@ reserves some bytes where you expect to need them most. pub struct GameState { //V1 pub health: u64, pub mana: u64, - pub for_future_use: [u8; 128], + pub for_future_use: [u8; 128], pub event_log: Vec } ``` @@ -437,8 +440,8 @@ this and both the old and new accounts are compatible. pub struct GameState { //V2 pub health: u64, pub mana: u64, - pub experience: u64, - pub for_future_use: [u8; 120], + pub experience: u64, + pub for_future_use: [u8; 120], pub event_log: Vec } ``` @@ -629,18 +632,18 @@ issues. ```rust Alice -- pays --> | - -- > Carol + -- > Carol Bob -- pays --- | ``` Since both of these transactions write to Carol's token account, only one of -them can go through at a time. Fortunately, Solana is wicked fast, so it'll +them can go through at a time. Fortunately, Solana is very fast, so it'll probably seem like they get paid at the same time. But what happens if more than just Alice and Bob try to pay Carol? ```rust Alice -- pays --> | - -- > Carol + -- > Carol x1000 -- pays --- | Bob -- pays --- | ``` @@ -757,13 +760,11 @@ pub fn run_concept_shared_account_redeem(ctx: Context -Next, replace the program ID in `programs/rpg/lib.rs` and `Anchor.toml` with the -program ID shown when you run `anchor keys list`. Alternatively, you can run -command `anchor keys sync` that will automatically sync your program ID. This -command will sync the program ids between the program files(including -`Anchor.toml`) with the actual `pubkey` from the program keypair file. +Next, run the command `anchor keys sync` that will automatically sync your +program ID. This command updates the program IDs in your program files +(including `Anchor.toml`) with the actual `pubkey` from the program keypair +file. Finally, let's scaffold out the program in the `lib.rs` file. Copy the following into your file before we get started: @@ -1967,7 +1967,7 @@ RPG game Congratulations! This was a lot to cover, but you now have a mini RPG game engine. If things aren't quite working, go back through the lab and find where you went wrong. If you need to, you can refer to the -[`main` branch of the solution code](https://github.com/Unboxed-Software/anchor-rpg). +[`main` branch of the solution code](https://github.com/solana-developers/anchor-rpg). Be sure to put these concepts into practice in your own programs. Each little optimization adds up! @@ -1979,7 +1979,7 @@ looking for additional optimizations and/or expansions you can make. Think through new systems and features you would add and how you would optimize them. You can find some example modifications on the -[`challenge-solution` branch of the RPG repository](https://github.com/Unboxed-Software/anchor-rpg/tree/challenge-solution). +[`challenge-solution` branch of the RPG repository](https://github.com/solana-developers/anchor-rpg/tree/challenge-solution). Finally, go through one of your own programs and think about optimizations you can make to improve memory management, storage size, and/or concurrency. From d3bb4fd70bc2c510e09bd70a48adcf9cd1e4791f Mon Sep 17 00:00:00 2001 From: 0xCipherCoder Date: Sun, 15 Sep 2024 17:36:08 +0530 Subject: [PATCH 5/6] Added realloc related changes for account resizing --- .../program-architecture.md | 234 ++++++++++++------ 1 file changed, 152 insertions(+), 82 deletions(-) diff --git a/content/courses/program-optimization/program-architecture.md b/content/courses/program-optimization/program-architecture.md index 3167667bc..d59e9652e 100644 --- a/content/courses/program-optimization/program-architecture.md +++ b/content/courses/program-optimization/program-architecture.md @@ -229,8 +229,8 @@ To understand what's happening here, take a look at the > new data structure and thus is constrained by stack and heap limits imposed by > the BPF VM. With zero copy deserialization, all bytes from the account's > backing `RefCell<&mut [u8]>` are simply re-interpreted as a reference to the -> data structure. No allocations or copies necessary. Hence the ability to get -> around stack and heap limitations. +> data structure. No allocations or copies are necessary. Hence the ability to +> get around stack and heap limitations. Basically, your program never actually loads zero-copy account data into the stack or heap. It instead gets pointer access to the raw data. The @@ -386,72 +386,145 @@ accounts based on all the fields up to the first variable length field. To echo the beginning of this section: As a rule of thumb, keep all variable length structs at the end of the account. -#### For Future Use +#### Account Flexibility and Future-Proofing -In certain cases, consider adding extra, unused bytes to your accounts. These -are held in reserve for flexibility and backward compatibility. Take the -following example: +When developing Solana programs, it's crucial to design your account structures +with future upgrades and backward compatibility in mind. Solana offers powerful +features like account resizing and Anchor's `InitSpace` attribute to handle +these challenges efficiently. Let's explore a more dynamic and flexible approach +using a game state example: ```rust +use anchor_lang::prelude::*; + #[account] -pub struct GameState { +#[derive(InitSpace)] +pub struct GameState { // V1 + pub version: u8, pub health: u64, pub mana: u64, - pub event_log: Vec + pub experience: Option, + #[max_len(50)] + pub event_log: Vec } ``` -In this simple game state, a character has `health`, `mana`, and an event log. -If at some point you are making game improvements and want to add an -`experience` field, you'd hit a snag. The `experience` field should be a number -like a `u64`, which is simple enough to add. You can -[reallocate the account](/developers/courses/onchain-development/anchor-pdas) -and add space. - -However, to keep dynamic length fields, like `event_log`, at the end of the -struct, you would need to do some memory manipulation on all reallocated -accounts to move the location of `event_log`. This can be complicated and makes -querying accounts far more difficult. You'll end up in a state where -non-migrated accounts have `event_log` in one location and migrated accounts in -another. The old `GameState` without `experience` and the new `GameState` with -`experience` in it are no longer compatible. Old accounts won't serialize when -used where new accounts are expected. Queries will be far more difficult. You'll -likely need to create a migration system and ongoing logic to maintain backward -compatibility. Ultimately, it begins to seem like a bad idea. - -Fortunately, if you think ahead, you can add a `for_future_use` field that -reserves some bytes where you expect to need them most. +In this GameState, we have: -```rust -#[account] -pub struct GameState { //V1 - pub health: u64, - pub mana: u64, - pub for_future_use: [u8; 128], - pub event_log: Vec -} -``` +- A `version` field to track account structure changes +- Basic character attributes (`health`, `mana`) +- An `experience` field as `Option` for backward compatibility +- An `event_log` with a specified maximum length + +Key advantages of this approach: -That way, when you go to add `experience` or something similar, it looks like -this and both the old and new accounts are compatible. +1. **Automatic Space Calculation**: The `InitSpace` attribute automatically + calculates the required account space. +2. **Versioning**: The `version` field allows for easy identification of account + structure versions. +3. **Flexible Fields**: Using `Option` for new fields maintains compatibility + with older versions. +4. **Defined Limits**: The `max_len` attribute on `Vec` fields clearly + communicates size constraints. + +To handle upgrades and resizing, you can implement methods like this: ```rust -#[account] -pub struct GameState { //V2 - pub health: u64, - pub mana: u64, - pub experience: u64, - pub for_future_use: [u8; 120], - pub event_log: Vec +impl GameState { // V2 + pub fn upgrade_to_v2(ctx: Context) -> Result<()> { + let game_state = &mut ctx.accounts.game_state; + if game_state.version == 1 { + // Resize account to V2 size + let new_size = GameState::INIT_SPACE; + game_state.resize(new_size)?; + + // Resize the account + let account_info = &game_state.to_account_info(); + account_info.realloc(new_size, false)?; + + // Ensure the account is rent-exempt after resizing + let rent = Rent::get()?; + let new_minimum_balance = rent.minimum_balance(new_size); + let lamports_required = new_minimum_balance.saturating_sub(account_info.lamports()); + if lamports_required > 0 { + // Transfer additional lamports to maintain rent exemption + anchor_lang::system_program::transfer( + CpiContext::new( + ctx.accounts.system_program.to_account_info(), + anchor_lang::system_program::Transfer { + from: ctx.accounts.payer.to_account_info(), + to: account_info.clone(), + }, + ), + lamports_required, + )?; + } + + // Update version and initialize new field + game_state.version = 2; + game_state.experience = Some(0); + } + Ok(()) + } +} + +#[derive(Accounts)] +pub struct UpgradeAccount<'info> { + #[account(mut)] + pub game_state: Account<'info, GameState>, + #[account(mut)] + pub payer: Signer<'info>, + pub system_program: Program<'info, System>, } ``` -These extra bytes do add to the cost of using your program. However, it seems -well worth the benefit in most cases. +When using +[`realloc`](https://docs.rs/anchor-lang/latest/anchor_lang/prelude/struct.AccountInfo.html#method.realloc) +to increase the size of an account, it's crucial to ensure that the account +remains rent-exempt. This often requires transferring additional lamports to the +account. + +If you need to increase the length of `event_log` or add more fields in the +future, you can: + +- Update the `max_len` attribute: + + ```rust + #[max_len(100)] // Increased from 50 + pub event_log: Vec + ``` + +- Create a new upgrade function: + + ```rust + pub fn upgrade_to_v3(ctx: Context) -> Result<()> { + let game_state = &mut ctx.accounts.game_state; + if game_state.version == 2 { + // Resize account to new size + let new_size = GameState::INIT_SPACE; + game_state.resize(new_size)?; -So as a general rule of thumb: anytime you think your account types have the -potential to change in a way that will require some kind of complex migration, -add in some `for_future_use` bytes. + ... + + game_state.version = 3; + } + Ok(()) + } + ``` + + + +While account resizing is powerful, use it judiciously. Consider the trade-offs +between frequent resizing and initial allocation based on your specific use case +and expected growth patterns. + +- Always ensure your account remains rent-exempt after resizing. +- The payer of the transaction is responsible for providing the additional + lamports. +- Calculate the lamports required carefully to avoid overcharging or leaving the + account non-rent-exempt. +- Consider the cost implications of frequent resizing in your program design. + #### Data Optimization @@ -764,7 +837,7 @@ we keep an internal tally of how many lamports need to be redeemed, i.e. be transferred from the PDA to the community wallet at a later time. At some point in the future, the community wallet will go around and clean up all the straggling lamports. It's important to note that anyone should be able to sign -for the redeem function, since the PDA has permission over itself. +for the redeem function since the PDA has permission over itself. If you want to avoid bottlenecks at all costs, this is one way to tackle them. Ultimately this is a design decision and the simpler, less optimal solution @@ -914,7 +987,6 @@ Now that our initial setup is ready, let's create our accounts. We'll have 3: - `experience` - the player's experience - `kills` - number of monsters killed - `next_monster_index` - the index of the next monster to face - - `for_future_use` - 256 bytes reserved for future use - `inventory` - a vector of the player's inventory 3. `Monster` - A PDA account whose address is derived using the game account address, the player's wallet address, and an index (the one stored as @@ -965,8 +1037,7 @@ pub struct Game { #[derive(AnchorSerialize, AnchorDeserialize, Clone, InitSpace)] pub struct GameConfig { - pub max_items_per_player: u8, - pub for_future_use: [u64; 16], // Health of Enemies? Experience per item? Action Points per Action? + pub max_items_per_player: u8 } // Inside `state/player.rs` @@ -985,16 +1056,13 @@ pub struct Player { // 8 bytes pub kills: u64, // 8 bytes pub next_monster_index: u64, // 8 bytes - pub for_future_use: [u8; 256], // Attack/Speed/Defense/Health/Mana?? Metadata?? - pub inventory: Vec, // Max 8 items } #[derive(AnchorSerialize, AnchorDeserialize, Clone, InitSpace)] pub struct InventoryItem { pub name: [u8; 32], // Fixed Name up to 32 bytes - pub amount: u64, - pub for_future_use: [u8; 128], // Metadata? Effects? Flags? + pub amount: u64 } @@ -1010,18 +1078,8 @@ pub struct Monster { ``` There aren't a lot of complicated design decisions here, but let's talk about -the `inventory` and `for_future_use` fields on the `Player` struct. Since -`inventory` is variable in length we decided to place it at the end of the -account to make querying easier. We've also decided it's worth spending a little -extra money on rent exemption to have 256 bytes of reserved space in the -`for_future_use` field. We could exclude this and simply reallocate accounts if -we need to add fields in the future, but adding it now simplifies things for us -in the future. - -If we chose to reallocate in the future, we'd need to write more complicated -queries and likely couldn't query in a single call based on `inventory`. -Reallocating and adding a field would move the memory position of `inventory`, -leaving us to write complex logic to query accounts with various structures. +the `inventory` field on the `Player` struct. Since `inventory` is variable in +length we decided to place it at the end of the account to make querying easier. ### 3. Create Ancillary Types @@ -1030,23 +1088,36 @@ that we haven't created yet. Let's start with the game config struct. Technically, this could have gone in the `Game` account, but it's nice to have some separation and encapsulation. -This struct should store the max items allowed per player and some bytes for -future use. Again, the bytes for future use here help us avoid complexity in the -future. Reallocating accounts works best when you're adding fields at the end of -an account rather than in the middle. If you anticipate adding fields in the -middle of an existing data, it might make sense to add some "future use" bytes -up front. +This struct should store the max items allowed per player. ```rust filename="game.rs" // ----------- GAME CONFIG ---------- // Inside `state/game.rs` #[derive(AnchorSerialize, AnchorDeserialize, Clone, InitSpace)] pub struct GameConfig { - pub max_items_per_player: u8, - pub for_future_use: [u64; 16], // Health of Enemies? Experience per item? Action Points per Action? + pub max_items_per_player: u8 } ``` +Reallocating accounts in Solana programs is now more flexible thanks to Anchor's +[`InitSpace`](https://docs.rs/anchor-lang/latest/anchor_lang/derive.InitSpace.html) +attribute and Solana's account resizing capabilities. While it's still generally +easier to add fields at the end of an account structure, modern practices allow +for more adaptable designs: + +- Use Anchor's `InitSpace` attribute to automatically calculate account space. +- For variable-length fields like `Vec` or `String`, specify a `max_len` + attribute. +- When adding new fields, consider using `Option` for backward compatibility. +- Implement a versioning system in your account structure to manage different + layouts. +- Use Solana's `realloc` instruction to resize accounts when needed, ensuring + rent-exemption is maintained. + +This approach allows for easier account structure evolution, regardless of where +new fields are added, while maintaining efficient querying and +serialization/deserialization through Anchor's built-in capabilities. + Next, let's create our status flags. Remember, we _could_ store our flags as booleans but we save space by storing multiple flags in a single byte. Each flag takes up a different bit within the byte. We can use the `<<` operator to place @@ -1070,7 +1141,7 @@ pub const MAX_INVENTORY_ITEMS: usize = 8; ``` Finally, let's create our `InventoryItem`. This should have fields for the -item's name, amount, and some bytes reserved for future use. +item's name and amount. ```rust filename="player.rs" // ----------- INVENTORY ---------- @@ -1079,8 +1150,7 @@ item's name, amount, and some bytes reserved for future use. #[derive(AnchorSerialize, AnchorDeserialize, Clone, InitSpace)] pub struct InventoryItem { pub name: [u8; 32], // Fixed Name up to 32 bytes - pub amount: u64, - pub for_future_use: [u8; 128], // Metadata? Effects? Flags? + pub amount: u64 } ``` From 68c8e40115bc5bbfc2656c29e0b430f0908ca585 Mon Sep 17 00:00:00 2001 From: 0xCipherCoder Date: Sun, 15 Sep 2024 19:18:43 +0530 Subject: [PATCH 6/6] Updated content as per comment and realloc constraint --- .../program-architecture.md | 255 ++++++++++++------ 1 file changed, 173 insertions(+), 82 deletions(-) diff --git a/content/courses/program-optimization/program-architecture.md b/content/courses/program-optimization/program-architecture.md index d59e9652e..df6faa56b 100644 --- a/content/courses/program-optimization/program-architecture.md +++ b/content/courses/program-optimization/program-architecture.md @@ -427,90 +427,160 @@ Key advantages of this approach: 4. **Defined Limits**: The `max_len` attribute on `Vec` fields clearly communicates size constraints. -To handle upgrades and resizing, you can implement methods like this: +When you need to upgrade your account structure, such as increasing the length +of `event_log` or adding new fields, you can use a single upgrade instruction +with Anchor's `realloc` constraint: + +1. Update the `GameState` struct with new fields or increased `max_len` + attributes: + + ```rust + #[account] + #[derive(InitSpace)] + pub struct GameState { + pub version: u8, + pub health: u64, + pub mana: u64, + pub experience: Option, + #[max_len(100)] // Increased from 50 + pub event_log: Vec, + pub new_field: Option, // Added new field + } + ``` + +2. Use a single `UpgradeGameState` context for all upgrades with Anchor's + `realloc` constraint for `GameState`: + + ```rust + #[derive(Accounts)] + pub struct UpgradeGameState<'info> { + #[account( + mut, + realloc = GameState::INIT_SPACE, + realloc::payer = payer, + realloc::zero = false, + )] + pub game_state: Account<'info, GameState>, + #[account(mut)] + pub payer: Signer<'info>, + pub system_program: Program<'info, System>, + } + ``` + +3. Implement the upgrade logic in a single function: + + ```rust + pub fn upgrade_game_state(ctx: Context) -> Result<()> { + let game_state = &mut ctx.accounts.game_state; -```rust -impl GameState { // V2 - pub fn upgrade_to_v2(ctx: Context) -> Result<()> { - let game_state = &mut ctx.accounts.game_state; - if game_state.version == 1 { - // Resize account to V2 size - let new_size = GameState::INIT_SPACE; - game_state.resize(new_size)?; - - // Resize the account - let account_info = &game_state.to_account_info(); - account_info.realloc(new_size, false)?; - - // Ensure the account is rent-exempt after resizing - let rent = Rent::get()?; - let new_minimum_balance = rent.minimum_balance(new_size); - let lamports_required = new_minimum_balance.saturating_sub(account_info.lamports()); - if lamports_required > 0 { - // Transfer additional lamports to maintain rent exemption - anchor_lang::system_program::transfer( - CpiContext::new( - ctx.accounts.system_program.to_account_info(), - anchor_lang::system_program::Transfer { - from: ctx.accounts.payer.to_account_info(), - to: account_info.clone(), - }, - ), - lamports_required, - )?; - } - - // Update version and initialize new field + match game_state.version { + 1 => { game_state.version = 2; game_state.experience = Some(0); - } - Ok(()) + msg!("Upgraded to version 2"); + }, + 2 => { + game_state.version = 3; + game_state.new_field = Some(0); + msg!("Upgraded to version 3"); + }, + _ => return Err(ErrorCode::AlreadyUpgraded.into()), } + + Ok(()) + } + ``` + +The example to demonstrate this approach: + +```rust +use anchor_lang::prelude::*; + +#[account] +#[derive(InitSpace)] +pub struct GameState { + pub version: u8, + pub health: u64, + pub mana: u64, + pub experience: Option, + #[max_len(100)] // Increased from 50 + pub event_log: Vec, + pub new_field: Option, } #[derive(Accounts)] -pub struct UpgradeAccount<'info> { - #[account(mut)] +pub struct UpgradeGameState<'info> { + #[account( + mut, + realloc = GameState::INIT_SPACE, + realloc::payer = payer, + realloc::zero = false, + )] pub game_state: Account<'info, GameState>, #[account(mut)] pub payer: Signer<'info>, pub system_program: Program<'info, System>, } -``` -When using -[`realloc`](https://docs.rs/anchor-lang/latest/anchor_lang/prelude/struct.AccountInfo.html#method.realloc) -to increase the size of an account, it's crucial to ensure that the account -remains rent-exempt. This often requires transferring additional lamports to the -account. +#[program] +pub mod your_program { + use super::*; + + // ... other instructions ... -If you need to increase the length of `event_log` or add more fields in the -future, you can: + pub fn upgrade_game_state(ctx: Context) -> Result<()> { + let game_state = &mut ctx.accounts.game_state; -- Update the `max_len` attribute: + match game_state.version { + 1 => { + game_state.version = 2; + game_state.experience = Some(0); + msg!("Upgraded to version 2"); + }, + 2 => { + game_state.version = 3; + game_state.new_field = Some(0); + msg!("Upgraded to version 3"); + }, + _ => return Err(ErrorCode::AlreadyUpgraded.into()), + } + + Ok(()) + } +} - ```rust - #[max_len(100)] // Increased from 50 - pub event_log: Vec - ``` +#[error_code] +pub enum ErrorCode { + #[msg("Account is already at the latest version")] + AlreadyUpgraded, +} +``` -- Create a new upgrade function: +This approach: + +- Uses the Anchor's + [`realloc`](https://docs.rs/anchor-lang/latest/anchor_lang/derive.Accounts.html#normal-constraints) + constraint to automatically handle account resizing. +- The + [`InitSpace`](https://docs.rs/anchor-lang/latest/anchor_lang/derive.InitSpace.html) + derive macro automatically implements the `Space` trait for the `GameState` + struct. This trait includes the + [`INIT_SPACE`](https://docs.rs/anchor-lang/latest/anchor_lang/trait.Space.html#associatedconstant.INIT_SPACE) + associated constant , which calculates the total space required for the + account. +- Designates a payer for any additional rent with `realloc::payer = payer`. +- Keeps existing data with `realloc::zero = false`. - ```rust - pub fn upgrade_to_v3(ctx: Context) -> Result<()> { - let game_state = &mut ctx.accounts.game_state; - if game_state.version == 2 { - // Resize account to new size - let new_size = GameState::INIT_SPACE; - game_state.resize(new_size)?; + - ... +Account data can be increased within a single call by up to +`solana_program::entrypoint::MAX_PERMITTED_DATA_INCREASE` bytes. - game_state.version = 3; - } - Ok(()) - } - ``` +Memory used to grow is already zero-initialized upon program entrypoint and +re-zeroing it wastes compute units. If within the same call a program reallocs +from larger to smaller and back to larger again the new space could contain +stale data. Pass `true` for `zero_init` in this case, otherwise compute units +will be wasted re-zero-initializing. @@ -518,14 +588,16 @@ While account resizing is powerful, use it judiciously. Consider the trade-offs between frequent resizing and initial allocation based on your specific use case and expected growth patterns. -- Always ensure your account remains rent-exempt after resizing. +- Always ensure your account remains rent-exempt before resizing. - The payer of the transaction is responsible for providing the additional lamports. -- Calculate the lamports required carefully to avoid overcharging or leaving the - account non-rent-exempt. - Consider the cost implications of frequent resizing in your program design. +In native Rust, you can resize accounts using the `realloc()` method. For more +details, refer to the +[account resizing program](/content/cookbook/programs/change-account-size.md). + #### Data Optimization The idea here is to be aware of wasted bits. For example, if you have a field @@ -1099,24 +1171,43 @@ pub struct GameConfig { } ``` -Reallocating accounts in Solana programs is now more flexible thanks to Anchor's -[`InitSpace`](https://docs.rs/anchor-lang/latest/anchor_lang/derive.InitSpace.html) -attribute and Solana's account resizing capabilities. While it's still generally -easier to add fields at the end of an account structure, modern practices allow -for more adaptable designs: - -- Use Anchor's `InitSpace` attribute to automatically calculate account space. -- For variable-length fields like `Vec` or `String`, specify a `max_len` - attribute. -- When adding new fields, consider using `Option` for backward compatibility. -- Implement a versioning system in your account structure to manage different - layouts. -- Use Solana's `realloc` instruction to resize accounts when needed, ensuring - rent-exemption is maintained. +Reallocating accounts in Solana programs has become more flexible due to +Anchor's +[`realloc`](https://docs.rs/anchor-lang/latest/anchor_lang/derive.Accounts.html#normal-constraints) +account constraint and Solana's account resizing capabilities. While adding +fields at the end of an account structure remains straightforward, modern +practices allow for more adaptable designs: + +1. Use Anchor's `realloc` constraint in the `#[account()]` attribute to specify + resizing parameters: + + ```rust + #[account( + mut, + realloc = AccountStruct::INIT_SPACE, + realloc::payer = payer, + realloc::zero = false, + )] + ``` + +2. Use Anchor's `InitSpace` attribute to automatically calculate account space. +3. For variable-length fields like `Vec` or `String`, use the `max_len` + attribute to specify maximum size. +4. When adding new fields, consider using `Option` for backward + compatibility. +5. Implement a versioning system in your account structure to manage different + layouts. +6. Ensure the payer account is mutable and a signer to cover reallocation costs: + + ```rust + #[account(mut)] + pub payer: Signer<'info>, + ``` This approach allows for easier account structure evolution, regardless of where new fields are added, while maintaining efficient querying and -serialization/deserialization through Anchor's built-in capabilities. +serialization/deserialization through Anchor's built-in capabilities. It enables +resizing accounts as needed, automatically handling rent-exemption. Next, let's create our status flags. Remember, we _could_ store our flags as booleans but we save space by storing multiple flags in a single byte. Each flag