spl_account_compression/
lib.rs

1//!
2//! A buffer of proof-like changelogs is stored on-chain that allow multiple proof-based writes to succeed within the same slot.
3//! This is accomplished by fast-forwarding out-of-date (or possibly invalid) proofs based on information stored in the changelogs.
4//! See a copy of the whitepaper [here](https://drive.google.com/file/d/1BOpa5OFmara50fTvL0VIVYjtg-qzHCVc/view)
5//!
6//! To circumvent proof size restrictions stemming from Solana transaction size restrictions,
7//! SPL Account Compression also provides the ability to cache the upper most leaves of the
8//! concurrent merkle tree. This is called the "canopy", and is stored at the end of the
9//! ConcurrentMerkleTreeAccount. More information can be found in the initialization instruction
10//! documentation.
11//!
12//! While SPL ConcurrentMerkleTrees can generically store arbitrary information,
13//! one exemplified use-case is the [Bubblegum](https://github.com/metaplex-foundation/metaplex-program-library/tree/master/bubblegum) contract,
14//! which uses SPL-Compression to store encoded information about NFTs.
15//! The use of SPL-Compression within Bubblegum allows for:
16//! - up to 1 billion NFTs to be stored in a single account on-chain (>10,000x decrease in on-chain cost)
17//! - up to 2048 concurrent updates per slot
18//!
19//! Operationally, SPL ConcurrentMerkleTrees **must** be supplemented by off-chain indexers to cache information
20//! about leafs and to power an API that can supply up-to-date proofs to allow updates to the tree.
21//! All modifications to SPL ConcurrentMerkleTrees are settled on the Solana ledger via instructions against the SPL Compression contract.
22//! A production-ready indexer (Plerkle) can be found in the [Metaplex program library](https://github.com/metaplex-foundation/digital-asset-validator-plugin)
23
24// Note that this is not functional, it is just a placeholder to create a 2.0.0 compatible SDK
25use anchor_lang::{
26    prelude::*,
27    solana_program::sysvar::{clock::Clock, rent::Rent},
28};
29use borsh::{BorshDeserialize, BorshSerialize};
30
31pub mod canopy;
32pub mod concurrent_tree_wrapper;
33pub mod error;
34pub mod events;
35#[macro_use]
36pub mod macros;
37mod noop;
38pub mod state;
39pub mod zero_copy;
40
41pub use crate::noop::{wrap_application_data_v1, Noop};
42
43use crate::canopy::{fill_in_proof_from_canopy, update_canopy};
44use crate::concurrent_tree_wrapper::*;
45pub use crate::error::AccountCompressionError;
46pub use crate::events::{AccountCompressionEvent, ChangeLogEvent};
47use crate::noop::wrap_event;
48use crate::state::{
49    merkle_tree_get_size, ConcurrentMerkleTreeHeader, CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1,
50};
51use solana_security_txt::security_txt;
52
53/// Exported for Anchor / Solita
54pub use spl_concurrent_merkle_tree::{
55    concurrent_merkle_tree::{ConcurrentMerkleTree, FillEmptyOrAppendArgs},
56    error::ConcurrentMerkleTreeError,
57    node::Node,
58};
59
60declare_id!("cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK");
61
62security_txt! {
63    // Required fields
64    name: "SPL Account Compression",
65    project_url: "https://spl.solana.com/account-compression",
66    contacts: "link:https://github.com/solana-labs/solana-program-library/security/advisories/new,mailto:security@solana.com,discord:https://solana.com/discord",
67    policy: "https://github.com/solana-labs/solana-program-library/blob/master/SECURITY.md",
68
69    // Optional Fields
70    preferred_languages: "en",
71    source_code: "https://github.com/solana-labs/solana-program-library/tree/master/account-compression",
72    branch: "ac-mainnet-tag",
73    auditors: "https://github.com/solana-labs/security-audits#account-compression"
74}
75
76/// Context for initializing a new SPL ConcurrentMerkleTree
77#[derive(Accounts)]
78pub struct Initialize<'info> {
79    // #[account(zero)]
80    /// CHECK: This account will be zeroed out, and the size will be validated
81    pub merkle_tree: UncheckedAccount<'info>,
82
83    /// Authority that controls write-access to the tree
84    /// Typically a program, e.g., the Bubblegum contract validates that leaves are valid NFTs.
85    pub authority: Signer<'info>,
86
87    /// Program used to emit changelogs as cpi instruction data.
88    pub noop: Program<'info, Noop>,
89}
90
91/// Context for inserting, appending, or replacing a leaf in the tree
92///
93/// Modification instructions also require the proof to the leaf to be provided
94/// as 32-byte nodes via "remaining accounts".
95#[derive(Accounts)]
96pub struct Modify<'info> {
97    #[account(mut)]
98    /// CHECK: This account is validated in the instruction
99    pub merkle_tree: UncheckedAccount<'info>,
100
101    /// Authority that controls write-access to the tree
102    /// Typically a program, e.g., the Bubblegum contract validates that leaves are valid NFTs.
103    pub authority: Signer<'info>,
104
105    /// Program used to emit changelogs as cpi instruction data.
106    pub noop: Program<'info, Noop>,
107}
108
109/// Context for validating a provided proof against the SPL ConcurrentMerkleTree.
110/// Throws an error if provided proof is invalid.
111#[derive(Accounts)]
112pub struct VerifyLeaf<'info> {
113    /// CHECK: This account is validated in the instruction
114    pub merkle_tree: UncheckedAccount<'info>,
115}
116
117/// Context for transferring `authority`
118#[derive(Accounts)]
119pub struct TransferAuthority<'info> {
120    #[account(mut)]
121    /// CHECK: This account is validated in the instruction
122    pub merkle_tree: UncheckedAccount<'info>,
123
124    /// Authority that controls write-access to the tree
125    /// Typically a program, e.g., the Bubblegum contract validates that leaves are valid NFTs.
126    pub authority: Signer<'info>,
127}
128
129/// Context for closing a tree
130#[derive(Accounts)]
131pub struct CloseTree<'info> {
132    #[account(mut)]
133    /// CHECK: This account is validated in the instruction
134    pub merkle_tree: AccountInfo<'info>,
135
136    /// Authority that controls write-access to the tree
137    pub authority: Signer<'info>,
138
139    /// CHECK: Recipient of funds after
140    #[account(mut)]
141    pub recipient: AccountInfo<'info>,
142}
143
144#[program]
145pub mod spl_account_compression {
146    use super::*;
147
148    /// Creates a new merkle tree with maximum leaf capacity of `power(2, max_depth)`
149    /// and a minimum concurrency limit of `max_buffer_size`.
150    ///
151    /// Concurrency limit represents the # of replace instructions that can be successfully
152    /// executed with proofs dated for the same root. For example, a maximum buffer size of 1024
153    /// means that a minimum of 1024 replaces can be executed before a new proof must be
154    /// generated for the next replace instruction.
155    ///
156    /// Concurrency limit should be determined by empirically testing the demand for
157    /// state built on top of SPL Compression.
158    ///
159    /// For instructions on enabling the canopy, see [canopy].
160    pub fn init_empty_merkle_tree(
161        ctx: Context<Initialize>,
162        max_depth: u32,
163        max_buffer_size: u32,
164    ) -> Result<()> {
165        require_eq!(
166            *ctx.accounts.merkle_tree.owner,
167            crate::id(),
168            AccountCompressionError::IncorrectAccountOwner
169        );
170        let mut merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_mut_data()?;
171
172        let (mut header_bytes, rest) =
173            merkle_tree_bytes.split_at_mut(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1);
174
175        let mut header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
176        header.initialize(
177            max_depth,
178            max_buffer_size,
179            &ctx.accounts.authority.key(),
180            Clock::get()?.slot,
181        );
182        header.serialize(&mut header_bytes)?;
183
184        let merkle_tree_size = merkle_tree_get_size(&header)?;
185        let (tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size);
186        let id = ctx.accounts.merkle_tree.key();
187
188        let change_log_event = merkle_tree_initialize_empty(&header, id, tree_bytes)?;
189
190        wrap_event(
191            &AccountCompressionEvent::ChangeLog(*change_log_event),
192            &ctx.accounts.noop,
193        )?;
194        update_canopy(canopy_bytes, header.get_max_depth(), None)
195    }
196
197    /// Note:
198    /// Supporting this instruction open a security vulnerability for indexers.
199    /// This instruction has been deemed unusable for publicly indexed compressed NFTs.
200    /// Indexing batched data in this way requires indexers to read in the `uri`s onto physical storage
201    /// and then into their database. This opens up a DOS attack vector, whereby this instruction is
202    /// repeatedly invoked, causing indexers to fail.
203    ///
204    /// Because this instruction was deemed insecure, this instruction has been removed
205    /// until secure usage is available on-chain.
206    // pub fn init_merkle_tree_with_root(
207    //     ctx: Context<Initialize>,
208    //     max_depth: u32,
209    //     max_buffer_size: u32,
210    //     root: [u8; 32],
211    //     leaf: [u8; 32],
212    //     index: u32,
213    //     _changelog_db_uri: String,
214    //     _metadata_db_uri: String,
215    // ) -> Result<()> {
216    //     require_eq!(
217    //         *ctx.accounts.merkle_tree.owner,
218    //         crate::id(),
219    //         AccountCompressionError::IncorrectAccountOwner
220    //     );
221    //     let mut merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_mut_data()?;
222
223    //     let (mut header_bytes, rest) =
224    //         merkle_tree_bytes.split_at_mut(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1);
225
226    //     let mut header = ConcurrentMerkleTreeHeader::try_from_slice(&header_bytes)?;
227    //     header.initialize(
228    //         max_depth,
229    //         max_buffer_size,
230    //         &ctx.accounts.authority.key(),
231    //         Clock::get()?.slot,
232    //     );
233    //     header.serialize(&mut header_bytes)?;
234    //     let merkle_tree_size = merkle_tree_get_size(&header)?;
235    //     let (tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size);
236
237    //     // Get rightmost proof from accounts
238    //     let mut proof = vec![];
239    //     for node in ctx.remaining_accounts.iter() {
240    //         proof.push(node.key().to_bytes());
241    //     }
242    //     fill_in_proof_from_canopy(canopy_bytes, header.max_depth, index, &mut proof)?;
243    //     assert_eq!(proof.len(), max_depth as usize);
244
245    //     let id = ctx.accounts.merkle_tree.key();
246    //     // A call is made to ConcurrentMerkleTree::initialize_with_root(root, leaf, proof, index)
247    //     let change_log = merkle_tree_apply_fn!(
248    //         header,
249    //         id,
250    //         tree_bytes,
251    //         initialize_with_root,
252    //         root,
253    //         leaf,
254    //         &proof,
255    //         index
256    //     )?;
257    //     wrap_event(change_log.try_to_vec()?, &ctx.accounts.log_wrapper)?;
258    //     update_canopy(canopy_bytes, header.max_depth, Some(change_log))
259    // }
260
261    /// Executes an instruction that overwrites a leaf node.
262    /// Composing programs should check that the data hashed into previous_leaf
263    /// matches the authority information necessary to execute this instruction.
264    pub fn replace_leaf(
265        ctx: Context<Modify>,
266        root: [u8; 32],
267        previous_leaf: [u8; 32],
268        new_leaf: [u8; 32],
269        index: u32,
270    ) -> Result<()> {
271        require_eq!(
272            *ctx.accounts.merkle_tree.owner,
273            crate::id(),
274            AccountCompressionError::IncorrectAccountOwner
275        );
276        let mut merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_mut_data()?;
277        let (header_bytes, rest) =
278            merkle_tree_bytes.split_at_mut(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1);
279
280        let header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
281        header.assert_valid_authority(&ctx.accounts.authority.key())?;
282        header.assert_valid_leaf_index(index)?;
283
284        let merkle_tree_size = merkle_tree_get_size(&header)?;
285        let (tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size);
286
287        let mut proof = vec![];
288        for node in ctx.remaining_accounts.iter() {
289            proof.push(node.key().to_bytes());
290        }
291        fill_in_proof_from_canopy(canopy_bytes, header.get_max_depth(), index, &mut proof)?;
292        let id = ctx.accounts.merkle_tree.key();
293        // A call is made to ConcurrentMerkleTree::set_leaf(root, previous_leaf, new_leaf, proof, index)
294        let args = &SetLeafArgs {
295            current_root: root,
296            previous_leaf,
297            new_leaf,
298            proof_vec: proof,
299            index,
300        };
301        let change_log_event = merkle_tree_set_leaf(&header, id, tree_bytes, args)?;
302
303        update_canopy(
304            canopy_bytes,
305            header.get_max_depth(),
306            Some(&change_log_event),
307        )?;
308        wrap_event(
309            &AccountCompressionEvent::ChangeLog(*change_log_event),
310            &ctx.accounts.noop,
311        )
312    }
313
314    /// Transfers `authority`.
315    /// Requires `authority` to sign
316    pub fn transfer_authority(
317        ctx: Context<TransferAuthority>,
318        new_authority: Pubkey,
319    ) -> Result<()> {
320        require_eq!(
321            *ctx.accounts.merkle_tree.owner,
322            crate::id(),
323            AccountCompressionError::IncorrectAccountOwner
324        );
325        let mut merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_mut_data()?;
326        let (mut header_bytes, _) =
327            merkle_tree_bytes.split_at_mut(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1);
328
329        let mut header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
330        header.assert_valid_authority(&ctx.accounts.authority.key())?;
331
332        header.set_new_authority(&new_authority);
333        header.serialize(&mut header_bytes)?;
334
335        Ok(())
336    }
337
338    /// Verifies a provided proof and leaf.
339    /// If invalid, throws an error.
340    pub fn verify_leaf(
341        ctx: Context<VerifyLeaf>,
342        root: [u8; 32],
343        leaf: [u8; 32],
344        index: u32,
345    ) -> Result<()> {
346        require_eq!(
347            *ctx.accounts.merkle_tree.owner,
348            crate::id(),
349            AccountCompressionError::IncorrectAccountOwner
350        );
351        let merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_data()?;
352        let (header_bytes, rest) =
353            merkle_tree_bytes.split_at(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1);
354
355        let header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
356        header.assert_valid()?;
357        header.assert_valid_leaf_index(index)?;
358
359        let merkle_tree_size = merkle_tree_get_size(&header)?;
360        let (tree_bytes, canopy_bytes) = rest.split_at(merkle_tree_size);
361
362        let mut proof = vec![];
363        for node in ctx.remaining_accounts.iter() {
364            proof.push(node.key().to_bytes());
365        }
366        fill_in_proof_from_canopy(canopy_bytes, header.get_max_depth(), index, &mut proof)?;
367        let id = ctx.accounts.merkle_tree.key();
368
369        let args = &ProveLeafArgs {
370            current_root: root,
371            leaf,
372            proof_vec: proof,
373            index,
374        };
375        merkle_tree_prove_leaf(&header, id, tree_bytes, args)?;
376
377        Ok(())
378    }
379
380    /// This instruction allows the tree's `authority` to append a new leaf to the tree
381    /// without having to supply a proof.
382    ///
383    /// Learn more about SPL
384    /// ConcurrentMerkleTree
385    /// [here](https://github.com/solana-labs/solana-program-library/tree/master/libraries/concurrent-merkle-tree)
386    pub fn append(ctx: Context<Modify>, leaf: [u8; 32]) -> Result<()> {
387        require_eq!(
388            *ctx.accounts.merkle_tree.owner,
389            crate::id(),
390            AccountCompressionError::IncorrectAccountOwner
391        );
392        let mut merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_mut_data()?;
393        let (header_bytes, rest) =
394            merkle_tree_bytes.split_at_mut(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1);
395
396        let header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
397        header.assert_valid_authority(&ctx.accounts.authority.key())?;
398
399        let id = ctx.accounts.merkle_tree.key();
400        let merkle_tree_size = merkle_tree_get_size(&header)?;
401        let (tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size);
402        let change_log_event = merkle_tree_append_leaf(&header, id, tree_bytes, &leaf)?;
403        update_canopy(
404            canopy_bytes,
405            header.get_max_depth(),
406            Some(&change_log_event),
407        )?;
408        wrap_event(
409            &AccountCompressionEvent::ChangeLog(*change_log_event),
410            &ctx.accounts.noop,
411        )
412    }
413
414    /// This instruction takes a proof, and will attempt to write the given leaf
415    /// to the specified index in the tree. If the insert operation fails, the leaf will be `append`-ed
416    /// to the tree.
417    /// It is up to the indexer to parse the final location of the leaf from the emitted changelog.
418    pub fn insert_or_append(
419        ctx: Context<Modify>,
420        root: [u8; 32],
421        leaf: [u8; 32],
422        index: u32,
423    ) -> Result<()> {
424        require_eq!(
425            *ctx.accounts.merkle_tree.owner,
426            crate::id(),
427            AccountCompressionError::IncorrectAccountOwner
428        );
429        let mut merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_mut_data()?;
430        let (header_bytes, rest) =
431            merkle_tree_bytes.split_at_mut(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1);
432
433        let header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
434        header.assert_valid_authority(&ctx.accounts.authority.key())?;
435        header.assert_valid_leaf_index(index)?;
436
437        let merkle_tree_size = merkle_tree_get_size(&header)?;
438        let (tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size);
439
440        let mut proof = vec![];
441        for node in ctx.remaining_accounts.iter() {
442            proof.push(node.key().to_bytes());
443        }
444        fill_in_proof_from_canopy(canopy_bytes, header.get_max_depth(), index, &mut proof)?;
445        // A call is made to ConcurrentMerkleTree::fill_empty_or_append
446        let id = ctx.accounts.merkle_tree.key();
447        let args = &FillEmptyOrAppendArgs {
448            current_root: root,
449            leaf,
450            proof_vec: proof,
451            index,
452        };
453        let change_log_event = merkle_tree_fill_empty_or_append(&header, id, tree_bytes, args)?;
454
455        update_canopy(
456            canopy_bytes,
457            header.get_max_depth(),
458            Some(&change_log_event),
459        )?;
460        wrap_event(
461            &AccountCompressionEvent::ChangeLog(*change_log_event),
462            &ctx.accounts.noop,
463        )
464    }
465
466    pub fn close_empty_tree(ctx: Context<CloseTree>) -> Result<()> {
467        require_eq!(
468            *ctx.accounts.merkle_tree.owner,
469            crate::id(),
470            AccountCompressionError::IncorrectAccountOwner
471        );
472        let mut merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_mut_data()?;
473        let (header_bytes, rest) =
474            merkle_tree_bytes.split_at_mut(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1);
475
476        let header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
477        header.assert_valid_authority(&ctx.accounts.authority.key())?;
478
479        let merkle_tree_size = merkle_tree_get_size(&header)?;
480        let (tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size);
481
482        let id = ctx.accounts.merkle_tree.key();
483        merkle_tree_prove_tree_is_empty(&header, id, tree_bytes)?;
484
485        // Close merkle tree account
486        // 1. Move lamports
487        let dest_starting_lamports = ctx.accounts.recipient.lamports();
488        **ctx.accounts.recipient.lamports.borrow_mut() = dest_starting_lamports
489            .checked_add(ctx.accounts.merkle_tree.lamports())
490            .unwrap();
491        **ctx.accounts.merkle_tree.lamports.borrow_mut() = 0;
492
493        // 2. Set all CMT account bytes to 0
494        header_bytes.fill(0);
495        tree_bytes.fill(0);
496        canopy_bytes.fill(0);
497
498        Ok(())
499    }
500}