Skip to main content

mpl_account_compression/
lib.rs

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