diff --git a/content/courses/state-compression/generalized-state-compression.md b/content/courses/state-compression/generalized-state-compression.md index 4333e308d..2ab747d71 100644 --- a/content/courses/state-compression/generalized-state-compression.md +++ b/content/courses/state-compression/generalized-state-compression.md @@ -14,7 +14,7 @@ description: - State Compression on Solana is most commonly used for compressed NFTs, but it's possible to use it for arbitrary data - State Compression lowers the amount of data you have to store onchain by - leveraging Merkle trees. + leveraging [Merkle trees](https://en.wikipedia.org/wiki/Merkle_tree). - Merkle trees store a single hash that represents an entire binary tree of hashes. Each leaf on a Merkle tree is a hash of that leaf's data. - Concurrent Merkle trees are a specialized version of Merkle trees that allow @@ -263,18 +263,48 @@ a simple messaging program, your `Message` struct might look as follows: ```rust #[derive(AnchorSerialize)] pub struct MessageLog { - leaf_node: [u8; 32], // The leaf node hash - from: Pubkey, // Pubkey of the message sender - to: Pubkey, // Pubkey of the message recipient - message: String, // The message to send + pub leaf_node: [u8; 32], // leaf node hash + pub from: Pubkey, + pub to: Pubkey, + pub message: String, // message to send } impl MessageLog { // Constructs a new message log from given leaf node and message pub fn new(leaf_node: [u8; 32], from: Pubkey, to: Pubkey, message: String) -> Self { - Self { leaf_node, from, to, message } + Self { + leaf_node, + from, + to, + message, + } } } + +#[derive(Accounts)] +pub struct MessageAccounts<'info> { + // The payer for the transaction + #[account(mut)] + pub owner: Signer<'info>, + + // The pda authority for the merkle tree, only used for signing + #[account( + seeds = [merkle_tree.key().as_ref()], + bump, + )] + pub tree_authority: SystemAccount<'info>, + + // The merkle tree account + /// CHECK: This account is validated by the spl account compression program + #[account(mut)] + pub merkle_tree: UncheckedAccount<'info>, + + // The noop program to log data + pub log_wrapper: Program<'info, Noop>, + + // The spl account compression program + pub compression_program: Program<'info, SplAccountCompression>, +} ``` To be abundantly clear, **this is not an account that you will be able to read @@ -311,18 +341,19 @@ pub fn create_messages_tree( ) -> Result<()> { // Get the address for the Merkle tree account let merkle_tree = ctx.accounts.merkle_tree.key(); + // Define the seeds for pda signing let signer_seeds: &[&[&[u8]]] = &[ &[ merkle_tree.as_ref(), // The address of the Merkle tree account as a seed - &[*ctx.bumps.get("tree_authority").unwrap()], // The bump seed for the pda + &[ctx.bumps.tree_authority], // The bump seed for the pda ], ]; // Create cpi context for init_empty_merkle_tree instruction. let cpi_ctx = CpiContext::new_with_signer( ctx.accounts.compression_program.to_account_info(), // The spl account compression program - Initialize { + spl_account_compression::cpi::accounts::Initialize { authority: ctx.accounts.tree_authority.to_account_info(), // The authority for the Merkle tree, using a PDA merkle_tree: ctx.accounts.merkle_tree.to_account_info(), // The Merkle tree account to be initialized noop: ctx.accounts.log_wrapper.to_account_info(), // The noop program to log data @@ -331,7 +362,7 @@ pub fn create_messages_tree( ); // CPI to initialize an empty Merkle tree with given max depth and buffer size - init_empty_merkle_tree(cpi_ctx, max_depth, max_buffer_size)?; + spl_account_compression::cpi::init_empty_merkle_tree(cpi_ctx, max_depth, max_buffer_size)?; Ok(()) } @@ -367,19 +398,29 @@ like this: pub fn append_message(ctx: Context, message: String) -> Result<()> { // Hash the message + whatever key should have update authority let leaf_node = keccak::hashv(&[message.as_bytes(), ctx.accounts.sender.key().as_ref()]).to_bytes(); - // Create a new "message log" using the leaf node hash, sender, receipient, and message - let message_log = MessageLog::new(leaf_node.clone(), ctx.accounts.sender.key().clone(), ctx.accounts.receipient.key().clone(), message); + + // Create a new "message log" using the leaf node hash, sender, recipient, and message + let message_log = MessageLog::new( + leaf_node.clone(), + ctx.accounts.sender.key().clone(), + ctx.accounts.recipient.key().clone(), + message + ); + // Log the "message log" data using noop program wrap_application_data_v1(message_log.try_to_vec()?, &ctx.accounts.log_wrapper)?; + // Get the address for the Merkle tree account let merkle_tree = ctx.accounts.merkle_tree.key(); + // Define the seeds for pda signing let signer_seeds: &[&[&[u8]]] = &[ &[ merkle_tree.as_ref(), // The address of the Merkle tree account as a seed - &[*ctx.bumps.get("tree_authority").unwrap()], // The bump seed for the pda + &[ctx.bumps.tree_authority], // The bump seed for the pda ], ]; + // Create a new cpi context and append the leaf node to the Merkle tree. let cpi_ctx = CpiContext::new_with_signer( ctx.accounts.compression_program.to_account_info(), // The spl account compression program @@ -390,8 +431,10 @@ pub fn append_message(ctx: Context, message: String) -> Result< }, signer_seeds // The seeds for pda signing ); + // CPI to append the leaf node to the Merkle tree append(cpi_ctx, leaf_node)?; + Ok(()) } ``` @@ -442,17 +485,14 @@ pub fn update_message( old_message: String, new_message: String ) -> Result<()> { - let old_leaf = keccak - ::hashv(&[old_message.as_bytes(), ctx.accounts.sender.key().as_ref()]) - .to_bytes(); - + let old_leaf = keccak::hashv(&[old_message.as_bytes(), ctx.accounts.sender.key().as_ref()]).to_bytes(); let merkle_tree = ctx.accounts.merkle_tree.key(); // Define the seeds for pda signing let signer_seeds: &[&[&[u8]]] = &[ &[ merkle_tree.as_ref(), // The address of the Merkle tree account as a seed - &[*ctx.bumps.get("tree_authority").unwrap()], // The bump seed for the pda + &[ctx.bumps.tree_authority], // The bump seed for the pda ], ]; @@ -462,7 +502,6 @@ pub fn update_message( msg!("Messages are the same!"); return Ok(()); } - let cpi_ctx = CpiContext::new_with_signer( ctx.accounts.compression_program.to_account_info(), // The spl account compression program VerifyLeaf { @@ -474,12 +513,16 @@ pub fn update_message( verify_leaf(cpi_ctx, root, old_leaf, index)?; } - let new_leaf = keccak - ::hashv(&[new_message.as_bytes(), ctx.accounts.sender.key().as_ref()]) - .to_bytes(); + let new_leaf = keccak::hashv(&[new_message.as_bytes(), ctx.accounts.sender.key().as_ref()]).to_bytes(); // Log out for indexers - let message_log = MessageLog::new(new_leaf.clone(), ctx.accounts.sender.key().clone(), ctx.accounts.recipient.key().clone(), new_message); + let message_log = MessageLog::new( + new_leaf.clone(), + ctx.accounts.sender.key().clone(), + ctx.accounts.recipient.key().clone(), + new_message + ); + // Log the "message log" data using noop program wrap_application_data_v1(message_log.try_to_vec()?, &ctx.accounts.log_wrapper)?; @@ -494,7 +537,7 @@ pub fn update_message( }, signer_seeds // The seeds for pda signing ); - // CPI to append the leaf node to the Merkle tree + // CPI to replace the leaf node in the Merkle tree replace_leaf(cpi_ctx, root, old_leaf, new_leaf, index)?; } @@ -628,7 +671,7 @@ app. Start by initializing an Anchor program: ```bash -anchor init compressed-notes +anchor init compressed-notes --template=multiple ``` We’ll be using the `spl-account-compression` crate with the `cpi` feature @@ -638,7 +681,7 @@ enabled. Let’s add it as a dependency in `programs/compressed-notes/Cargo.toml [dependencies] anchor-lang = "0.28.0" spl-account-compression = { version="0.2.0", features = ["cpi"] } -solana-program = "1.16.0" +solana-program = ">=1.18.11,<=2" ``` We’ll be testing locally but we need both the Compression program and the Noop @@ -806,7 +849,7 @@ pub mod compressed_notes { // Define the seeds for pda signing let signer_seeds: &[&[&[u8]]] = &[&[ merkle_tree.as_ref(), // The address of the Merkle tree account as a seed - &[*ctx.bumps.get("tree_authority").unwrap()], // The bump seed for the pda + &[ctx.bumps.tree_authority], // The bump seed for the pda ]]; // Create cpi context for init_empty_merkle_tree instruction. @@ -878,7 +921,7 @@ pub mod compressed_notes { // Define the seeds for pda signing let signer_seeds: &[&[&[u8]]] = &[&[ merkle_tree.as_ref(), // The address of the Merkle tree account as a seed - &[*ctx.bumps.get("tree_authority").unwrap()], // The bump seed for the pda + &[ctx.bumps.tree_authority], // The bump seed for the pda ]]; // Create a new cpi context and append the leaf node to the Merkle tree. let cpi_ctx = CpiContext::new_with_signer( @@ -958,7 +1001,7 @@ pub mod compressed_notes { // Define the seeds for pda signing let signer_seeds: &[&[&[u8]]] = &[&[ merkle_tree.as_ref(), // The address of the Merkle tree account as a seed - &[*ctx.bumps.get("tree_authority").unwrap()], // The bump seed for the pda + &[ctx.bumps.tree_authority], // The bump seed for the pda ]]; // Verify Leaf @@ -1019,6 +1062,12 @@ install it: yarn add @solana/spl-account-compression ``` +or + +```bash +npm install @solana/spl-account-compression +``` + Next, we’re going to give you the contents of a utility file we’ve created to make testing easier. Create a `utils.ts` file in the `tests` directory, add in the below, then we’ll explain it.