Bubblegum

Bubblegum

Overview

The Bubblegum Program is a specialized on-chain program developed by the Metaplex Foundation for the Solana blockchain. It is architected to enhance the scalability of Non-Fungible Tokens (NFTs) by leveraging data compression techniques and concurrent merkle tree structures.

The Challenges in NFT Management

As NFTs have surged in popularity, several challenges have emerged, particularly concerning scalability and efficiency:
  1. On-Chain Data Storage Limitations:
      • High Costs: Storing extensive metadata and multiple NFTs directly on-chain can be prohibitively expensive due to Solana's account rent fee.
  1. Data Redundancy and Inefficiency:
      • Repetitive Data: Storing similar or related NFTs separately leads to redundant data, wasting valuable blockchain storage space.

Account Compression and Merkle Trees

The Bubblegum Program addresses the challenge through a combination of SPL Account Compression and Concurrent Merkle Tree structures, offering a scalable and efficient framework for NFT management.

1. SPL Account Compression

The SPL Account Compression program is a Solana Program Library (SPL) that enables the compression of multiple accounts into a single compressed account. This is achieved by organizing data into a Merkle tree structure, where each leaf node represents individual data points—in this case, NFTs.

2. Merkle Trees for NFT Representation

  • A Merkle tree is a cryptographic data structure that allows efficient and secure verification of large data sets. Each leaf node represents an individual NFT, while parent nodes represent hashes of their child nodes, culminating in a single root hash that encapsulates the entire tree's state.
  • Benefits:
    • Integrity Verification: Easily verify the authenticity and integrity of any NFT by traversing the tree from the leaf to the root.
    • Space Efficiency: Compresses multiple NFTs into a single account, drastically reducing on-chain storage requirements.
    • Enhanced Scalability: Facilitates the management of millions of NFTs without overwhelming the network.

Connection Between Bubblegum and SPL Account Compression

  • Dependency: Bubblegum is built on top of the SPL Account Compression program. It relies on its capabilities to compress and manage large sets of NFTs within Merkle trees.
  • Enhanced Functionality: While SPL Account Compression provides the tools for data compression and Merkle tree management, Bubblegum extends these tools to offer specialized NFT management features, such as metadata updates, authority verifications, and integration with Metaplex's ecosystem.
  • Interoperability: By leveraging SPL Account Compression, Bubblegum ensures compatibility with other Solana programs and tools that utilize the same compression standards, fostering a cohesive and interoperable blockchain environment.

Key Features of the Bubblegum Program

  1. Compressed NFT Trees:
    1. Organizes NFTs into a scalable Merkle tree structure, enabling efficient storage and management.
  1. Metadata Handling:
    1. Facilitates seamless updates to NFT metadata, ensuring compliance with Metaplex standards and maintaining data integrity.
  1. Event Logging:
    1. Integrates with logging mechanisms (e.g., the Noop program) to record events for off-chain indexing and transparency.

Architecture

notion image

Create Tree

create_tree creates a concurrent merkle tree (CMT) which stores cNFTs.
It performs the following tasks:
  1. Validates and configures the Merkle tree’s canopy.
  1. Initializes a TreeConfig account (the PDA tree_authority) which holds metadata and controls around the tree.
  1. Uses the spl_account_compression program (via CPI) to initialize the empty Merkle tree with given parameters max_depth and max_buffer_size.
 
Key inputs:
  • ctx: The Anchor Context holding all the accounts, signers, and programs needed.
  • max_depth: The maximum depth of the Merkle tree. This controls how many leaves (and thus how many NFTs, etc.) the tree can hold. The number of leaves in a full binary tree is 2^max_depth.
  • max_buffer_size: This controls how many concurrent changes can be buffered for compression.
  • public: An optional boolean that, if Some(true), designates the tree as public, meaning anyone can mint cNFT in the tree.
 
Key accounts:
  • tree_authority: A PDA (Program Derived Address) account that will store the tree’s configuration.
  • merkle_tree: An account that will be the on-chain representation of the Merkle tree data structure (initially zeroed and will be initialized by the account compression program).
  • payer: The account paying for the initialization of these accounts.
  • tree_creator: The signer that creates the tree.
  • log_wrapper, compression_program, and system_program: Various programs needed. log_wrapper is the Noop program (used for logging), compression_program is the SPL compression program that provides the Merkle tree functionalities, and system_program is the Solana native program for account creation and basic instructions.
/// --- programs/bubblegum/program/src/lib.rs --- /// Creates a new tree. pub fn create_tree( ctx: Context<CreateTree>, max_depth: u32, max_buffer_size: u32, public: Option<bool>, ) -> Result<()> { processor::create_tree(ctx, max_depth, max_buffer_size, public) } /// --- programs/bubblegum/program/src/processor/create_tree.rs --- #[derive(Accounts)] pub struct CreateTree<'info> { #[account( init, seeds = [merkle_tree.key().as_ref()], payer = payer, space = TREE_AUTHORITY_SIZE, bump, )] pub tree_authority: Account<'info, TreeConfig>, #[account(zero)] /// CHECK: This account must be all zeros pub merkle_tree: UncheckedAccount<'info>, #[account(mut)] pub payer: Signer<'info>, pub tree_creator: Signer<'info>, pub log_wrapper: Program<'info, Noop>, pub compression_program: Program<'info, SplAccountCompression>, pub system_program: Program<'info, System>, }
 
Steps:
  1. Validate Merkle Tree Canopy Size
    1. Before the Merkle tree is fully initialized, the code calls check_canopy_size to ensure that the canopy size (the portion of the tree stored on-chain for quick verification of proofs) is correct. If the canopy is not appropriately sized, it returns an InvalidCanopySize error.
  1. Initialize the TreeConfig Account
    1. Initialize the tree_authority account with a TreeConfig struct:
      • tree_creator and tree_delegate are set to the tree_creator key, meaning the same account controls initial creation and delegation.
      • total_mint_capacity is 2^max_depth.
      • num_minted starts at 0 because no leaves are minted yet.
      • is_public sets whether this tree can be used by everyone or is restricted.
      • is_decompressible is set to DecompressibleState::Disabled initially, meaning leaves cannot be decompressed off-chain by default.
  1. Invoke the SPL Account Compression Program to Initialize the Merkle Tree
      • A CPI (Cross-Program Invocation) context is created to call the init_empty_merkle_tree function of the spl_account_compression program.
      • init_empty_merkle_tree takes max_depth and max_buffer_size and initializes the merkle_tree account's data to represent an empty Merkle tree.
      • This CPI call sets up the initial data structure (nodes, canopy, etc.) that we previously validated.
/// --- programs/bubblegum/program/src/processor/create_tree.rs --- pub(crate) fn create_tree( ctx: Context<CreateTree>, max_depth: u32, max_buffer_size: u32, public: Option<bool>, ) -> Result<()> { let merkle_tree = ctx.accounts.merkle_tree.to_account_info(); /// Validate Merkle Tree Canopy Size check_canopy_size(&ctx, max_depth, max_buffer_size)?; /// Initialize the TreeConfig Account let seed = merkle_tree.key(); let seeds = &[seed.as_ref(), &[*ctx.bumps.get("tree_authority").unwrap()]]; let authority = &mut ctx.accounts.tree_authority; authority.set_inner(TreeConfig { tree_creator: ctx.accounts.tree_creator.key(), tree_delegate: ctx.accounts.tree_creator.key(), total_mint_capacity: 1 << max_depth, num_minted: 0, is_public: public.unwrap_or(false), is_decompressible: DecompressibleState::Disabled, }); /// Invoke the SPL Account Compression Program to Initialize the Merkle Tree let authority_pda_signer = &[&seeds[..]]; let cpi_ctx = CpiContext::new_with_signer( ctx.accounts.compression_program.to_account_info(), spl_account_compression::cpi::accounts::Initialize { authority: ctx.accounts.tree_authority.to_account_info(), merkle_tree, noop: ctx.accounts.log_wrapper.to_account_info(), }, authority_pda_signer, ); spl_account_compression::cpi::init_empty_merkle_tree(cpi_ctx, max_depth, max_buffer_size) }

Check Canopy Size

It checks that the canopy size (the portion of the tree stored on-chain for quick verification of proofs) is correct.
 
Steps:
  1. Reading Account Data:
    1. merkle_tree_bytes is obtained by calling data.borrow() on the merkle_tree account. This provides a runtime borrow of the account’s underlying data buffer. Because merkle_tree is initially zeroed, this will be a slice of zeros before initialization.
  1. Splitting Header and Rest:
    1. header_bytes and rest are obtained by slicing the account data. The header of size CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1 is parsed into a ConcurrentMerkleTreeHeader.
  1. Initialize the Header:
    1. The header is then “initialized” with the provided max_depth, max_buffer_size, and the authority key. This sets fields in the header to expected initial states.
  1. Compute Merkle Tree Size:
    1. merkle_tree_size = merkle_tree_get_size(&header) computes how much space the actual tree nodes (i.e., leaves and branches) need.
  1. Canopy Extraction:
    1. The remaining bytes after the header and the core tree data are the canopy. The canopy is a set of nodes (hashes) that help speed up proof verification by caching partial paths.
  1. Check Cached Path Length:
    1. get_cached_path_length is called to verify the canopy forms a structure consistent with a binary tree missing only the root. It checks if (canopy.len() + 2) is a power of two, which is a requirement for the canopy structure.
  1. Compare Cached Path with Requirements:
    1. The required_canopy is calculated as max_depth - MAX_ACC_PROOFS_SIZE. The code ensures the cached_path_len from the canopy is at least required_canopy. If not, it errors out.
/// --- programs/bubblegum/program/src/processor/create_tree.rs --- fn check_canopy_size( ctx: &Context<CreateTree>, max_depth: u32, max_buffer_size: u32, ) -> Result<()> { /// Reading Account Data: let merkle_tree_bytes = ctx.accounts.merkle_tree.data.borrow(); /// Splitting Header and Rest: let (header_bytes, rest) = merkle_tree_bytes.split_at(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1); let mut header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?; /// Initialize the Header: header.initialize( max_depth, max_buffer_size, &ctx.accounts.tree_authority.key(), Clock::get()?.slot, ); /// Compute Merkle Tree Size: let merkle_tree_size = merkle_tree_get_size(&header)?; /// Canopy Extraction: let (_tree_bytes, canopy_bytes) = rest.split_at(merkle_tree_size); let canopy = cast_slice::<u8, Node>(canopy_bytes); /// Check Cached Path Length: let cached_path_len = get_cached_path_length(canopy, max_depth)?; /// Compare Cached Path with Requirements: let required_canopy = max_depth.saturating_sub(MAX_ACC_PROOFS_SIZE); require!( (cached_path_len as u32) >= required_canopy, BubblegumError::InvalidCanopySize ); Ok(()) } /// --- spl-account-compression-0.2.0/src/state/concurrent_merkle_tree_header.rs --- impl ConcurrentMerkleTreeHeader { pub fn initialize( &mut self, max_depth: u32, max_buffer_size: u32, authority: &Pubkey, creation_slot: u64, ) { self.account_type = CompressionAccountType::ConcurrentMerkleTree; match self.header { ConcurrentMerkleTreeHeaderData::V1(ref mut header) => { // Double check header is empty after deserialization from zero'd bytes assert_eq!(header.max_buffer_size, 0); assert_eq!(header.max_depth, 0); header.max_buffer_size = max_buffer_size; header.max_depth = max_depth; header.authority = *authority; header.creation_slot = creation_slot; } } } /// ... }

Mint

The mint_v1 instruction mints a new compressed NFT (a “leaf” in a Merkle tree). It:
  1. Ensures the caller has the right authority to mint if the tree is not public.
  1. Checks that the tree has enough capacity to mint another asset.
  1. Validates the provided metadata (e.g., name, symbol, URI, creators).
  1. Hashes the metadata and creators to produce the on-chain leaf data.
  1. Records the asset’s data via a CPI (Cross-Program Invocation) to a Noop program for permanent event logging.
  1. Updates the Merkle tree on-chain by appending a new leaf via the spl_account_compression program.
  1. Increments the minted count in the tree’s configuration.
 
Context:
  • tree_authority: A PDA (Program Derived Address) that stores the state of the Merkle tree (e.g., how many items minted, who can mint, etc.).
  • leaf_owner: The owner of the newly minted NFT leaf.
  • leaf_delegate: A delegate allowed to act on behalf of the owner.
  • merkle_tree: The on-chain account representing the compressed Merkle tree structure.
  • payer: The signer paying for the transaction.
  • tree_delegate: A signer that acts as a delegate (in addition to or instead of the tree’s creator) if the tree is not public.
  • log_wrapper and compression_program: Programs used to record logs and modify the Merkle tree.
  • system_program: The standard Solana system program used for basic operations like creating accounts.
/// --- programs/bubblegum/program/src/lib.rs --- /// Mints a new asset. pub fn mint_v1(ctx: Context<MintV1>, message: MetadataArgs) -> Result<LeafSchema> { processor::mint_v1(ctx, message) } /// --- programs/bubblegum/program/src/processor/mint.rs --- #[derive(Accounts)] pub struct MintV1<'info> { #[account( mut, seeds = [merkle_tree.key().as_ref()], bump, )] pub tree_authority: Account<'info, TreeConfig>, /// CHECK: This account is neither written to nor read from. pub leaf_owner: AccountInfo<'info>, /// CHECK: This account is neither written to nor read from. pub leaf_delegate: AccountInfo<'info>, #[account(mut)] /// CHECK: unsafe pub merkle_tree: UncheckedAccount<'info>, pub payer: Signer<'info>, pub tree_delegate: Signer<'info>, pub log_wrapper: Program<'info, Noop>, pub compression_program: Program<'info, SplAccountCompression>, pub system_program: Program<'info, System>, } /// --- programs/bubblegum/program/src/state/metaplex_adapter.rs --- #[derive(AnchorSerialize, AnchorDeserialize, PartialEq, Eq, Debug, Clone)] pub struct MetadataArgs { /// The name of the asset pub name: String, /// The symbol for the asset pub symbol: String, /// URI pointing to JSON representing the asset pub uri: String, /// Royalty basis points that goes to creators in secondary sales (0-10000) pub seller_fee_basis_points: u16, // Immutable, once flipped, all sales of this metadata are considered secondary. pub primary_sale_happened: bool, // Whether or not the data struct is mutable, default is not pub is_mutable: bool, /// nonce for easy calculation of editions, if present pub edition_nonce: Option<u8>, /// Since we cannot easily change Metadata, we add the new DataV2 fields here at the end. pub token_standard: Option<TokenStandard>, /// Collection pub collection: Option<Collection>, /// Uses pub uses: Option<Uses>, pub token_program_version: TokenProgramVersion, pub creators: Vec<Creator>, }
 
Steps:
  1. Access Control:
    1. If the tree is not public, ensure the signer (incoming_tree_delegate) who initiated the mint matches the recorded tree_creator or tree_delegate.
  1. Capacity Check:
    1. Call authority.contains_mint_capacity(1) to ensure at least one more asset can be minted.
  1. Metadata Auth Set:
    1. Create a HashSet (metadata_auth) of all keys that can potentially verify creators. This includes the payer, the incoming tree delegate, and any remaining signer accounts. A creator is considered verified if their public key is in this set.
  1. Call process_mint_v1 to Mint cNFT: Pass all relevant info to process_mint_v1:
      • The message (metadata about the asset),
      • Owner, delegate,
      • The set of metadata_auth keys,
      • The authority bump for signing PDAs,
      • The authority and merkle_tree references,
      • The log_wrapper (for logging data),
      • The compression_program (for modifying the Merkle tree),
      • false for allow_verified_collection (meaning we do not allow verified collections here).
  1. Increment the Mint Count:
    1. If process_mint_v1 succeeds, increment authority.num_minted to reflect the new minted asset.
  1. Return the Leaf:
    1. Return the newly created LeafSchema instance representing the minted asset.
/// --- programs/bubblegum/program/src/processor/mint.rs --- pub(crate) fn mint_v1(ctx: Context<MintV1>, message: MetadataArgs) -> Result<LeafSchema> { // TODO -> Separate V1 / V1 into seperate instructions let payer = ctx.accounts.payer.key(); let incoming_tree_delegate = ctx.accounts.tree_delegate.key(); let owner = ctx.accounts.leaf_owner.key(); let delegate = ctx.accounts.leaf_delegate.key(); let authority = &mut ctx.accounts.tree_authority; let merkle_tree = &ctx.accounts.merkle_tree; /// Access Control: if !authority.is_public { require!( incoming_tree_delegate == authority.tree_creator || incoming_tree_delegate == authority.tree_delegate, BubblegumError::TreeAuthorityIncorrect, ); } /// Capacity Check: if !authority.contains_mint_capacity(1) { return Err(BubblegumError::InsufficientMintCapacity.into()); } /// Metadata Auth Set: // Create a HashSet to store signers to use with creator validation. Any signer can be // counted as a validated creator. let mut metadata_auth = HashSet::<Pubkey>::new(); metadata_auth.insert(payer); metadata_auth.insert(incoming_tree_delegate); // If there are any remaining accounts that are also signers, they can also be used for // creator validation. metadata_auth.extend( ctx.remaining_accounts .iter() .filter(|a| a.is_signer) .map(|a| a.key()), ); /// Call process_mint_v1 to Mint cNFT: let leaf = process_mint_v1( message, owner, delegate, metadata_auth, *ctx.bumps.get("tree_authority").unwrap(), authority, merkle_tree, &ctx.accounts.log_wrapper, &ctx.accounts.compression_program, false, )?; /// Increment the Mint Count: authority.increment_mint_count(); /// Return the Leaf: Ok(leaf) } /// --- programs/bubblegum/program/src/state/mod.rs --- impl TreeConfig { pub fn increment_mint_count(&mut self) { self.num_minted = self.num_minted.saturating_add(1); } pub fn contains_mint_capacity(&self, requested_capacity: u64) -> bool { let remaining_mints = self.total_mint_capacity.saturating_sub(self.num_minted); requested_capacity <= remaining_mints } }

Process Mint

It handles the core logic of producing the leaf data and appending it to the Merkle tree:
 
Steps:
  1. Metadata Validation:
    1. assert_metadata_is_mpl_compatible(&message) ensures the metadata fits Metaplex limits (name, symbol, URI lengths, creator counts, total creator share = 100).
  1. Collection Rules:
    1. If allow_verified_collection is false and the provided message has a verified collection, return an error.
  1. Token Standard Check:
    1. assert_metadata_token_standard(&message) ensures the metadata’s token standard is NonFungible, rejecting anything else.
  1. Hashing Metadata:
    1. Two hashes are computed:
      • metadata_args_hash: a keccak hash of the entire MetadataArgs.
      • data_hash: a keccak hash derived from metadata_args_hash plus the seller fee basis points.
  1. Verifying and Hashing Creators:
    1. For each creator:
      • If verified = true, the creator’s public key must be in metadata_auth.
      • If this check fails, return CreatorDidNotVerify.
      If all creators pass, creator_hash is computed by concatenating each creator’s data and hashing with keccak.
  1. Asset ID Computation:
    1. asset_id is derived using a PDA with [ASSET_PREFIX, merkle_tree.key(), nonce]. The nonce here is authority.num_minted, ensuring each newly minted asset gets a unique ID.
  1. Leaf Schema Construction:
    1. A LeafSchema structure is created to represent the NFT. It includes:
      • The asset_id
      • owner
      • delegate
      • nonce (the current num_minted count)
      • data_hash and creator_hash reflecting the NFT’s metadata.
  1. Logging the Event:
    1. wrap_application_data_v1(leaf.to_event().try_to_vec()?, wrapper) logs the asset data inside a CPI call to the Noop program. This allows the data to be recorded permanently in transaction logs, ensuring indexers can reconstruct the Merkle tree state later.
  1. Appending the Leaf to the Tree:
    1. Finally, append_leaf is called. This is a CPI to spl_account_compression, which updates the Merkle tree to include the new leaf, thus finalizing the mint process on-chain.
/// --- programs/bubblegum/program/src/processor/mint.rs --- pub(crate) fn process_mint_v1<'info>( message: MetadataArgs, owner: Pubkey, delegate: Pubkey, metadata_auth: HashSet<Pubkey>, authority_bump: u8, authority: &mut Account<'info, TreeConfig>, merkle_tree: &AccountInfo<'info>, wrapper: &Program<'info, Noop>, compression_program: &AccountInfo<'info>, allow_verified_collection: bool, ) -> Result<LeafSchema> { /// Metadata Validation: assert_metadata_is_mpl_compatible(&message)?; /// Collection Rules: if !allow_verified_collection { if let Some(collection) = &message.collection { if collection.verified { return Err(BubblegumError::CollectionCannotBeVerifiedInThisInstruction.into()); } } } /// Token Standard Check: assert_metadata_token_standard(&message)?; /// Hashing Metadata: // @dev: seller_fee_basis points is encoded twice so that it can be passed to marketplace // instructions, without passing the entire, un-hashed MetadataArgs struct let metadata_args_hash = keccak::hashv(&[message.try_to_vec()?.as_slice()]); let data_hash = keccak::hashv(&[ &metadata_args_hash.to_bytes(), &message.seller_fee_basis_points.to_le_bytes(), ]); /// Verifying and Hashing Creators: // Use the metadata auth to check whether we can allow `verified` to be set to true in the // creator Vec. let creator_data = message .creators .iter() .map(|c| { if c.verified && !metadata_auth.contains(&c.address) { Err(BubblegumError::CreatorDidNotVerify.into()) } else { Ok([c.address.as_ref(), &[c.verified as u8], &[c.share]].concat()) } }) .collect::<Result<Vec<_>>>()?; // Calculate creator hash. let creator_hash = keccak::hashv( creator_data .iter() .map(|c| c.as_slice()) .collect::<Vec<&[u8]>>() .as_ref(), ); /// Asset ID Computation: let asset_id = get_asset_id(&merkle_tree.key(), authority.num_minted); /// Leaf Schema Construction: let leaf = LeafSchema::new_v0( asset_id, owner, delegate, authority.num_minted, data_hash.to_bytes(), creator_hash.to_bytes(), ); /// Logging the Event: wrap_application_data_v1(leaf.to_event().try_to_vec()?, wrapper)?; /// Appending the Leaf to the Tree: append_leaf( &merkle_tree.key(), authority_bump, &compression_program.to_account_info(), &authority.to_account_info(), &merkle_tree.to_account_info(), &wrapper.to_account_info(), leaf.to_node(), )?; Ok(leaf) }

Check Metadata Compatibility with Metaplex

/// --- programs/bubblegum/program/src/asserts.rs --- /// Assert that the provided MetadataArgs are compatible with MPL `Data` pub fn assert_metadata_is_mpl_compatible(metadata: &MetadataArgs) -> Result<()> { if metadata.name.len() > mpl_token_metadata::MAX_NAME_LENGTH { return Err(BubblegumError::MetadataNameTooLong.into()); } if metadata.symbol.len() > mpl_token_metadata::MAX_SYMBOL_LENGTH { return Err(BubblegumError::MetadataSymbolTooLong.into()); } if metadata.uri.len() > mpl_token_metadata::MAX_URI_LENGTH { return Err(BubblegumError::MetadataUriTooLong.into()); } if metadata.seller_fee_basis_points > 10000 { return Err(BubblegumError::MetadataBasisPointsTooHigh.into()); } if !metadata.creators.is_empty() { if metadata.creators.len() > mpl_token_metadata::MAX_CREATOR_LIMIT { return Err(BubblegumError::CreatorsTooLong.into()); } let mut total: u8 = 0; for i in 0..metadata.creators.len() { let creator = &metadata.creators[i]; for iter in metadata.creators.iter().skip(i + 1) { if iter.address == creator.address { return Err(BubblegumError::DuplicateCreatorAddress.into()); } } total = total .checked_add(creator.share) .ok_or(BubblegumError::CreatorShareTotalMustBe100)?; } if total != 100 { return Err(BubblegumError::CreatorShareTotalMustBe100.into()); } } Ok(()) }

Token Standard Check

/// --- programs/bubblegum/program/src/asserts.rs --- /// Assert that the provided MetadataArgs contains info about Token Standard /// and ensures that it's NonFungible pub fn assert_metadata_token_standard(metadata: &MetadataArgs) -> Result<()> { match metadata.token_standard { Some(MetadataTokenStandard::NonFungible) => Ok(()), _ => Err(BubblegumError::InvalidTokenStandard.into()), } }

Calc Asset Id

/// --- programs/bubblegum/program/src/utils.rs --- pub fn get_asset_id(tree_id: &Pubkey, nonce: u64) -> Pubkey { Pubkey::find_program_address( &[ ASSET_PREFIX.as_ref(), tree_id.as_ref(), &nonce.to_le_bytes(), ], &crate::id(), ) .0 }

Leaf Scheme

/// --- programs/bubblegum/program/src/state/leaf_schema.rs --- #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] pub enum LeafSchema { V1 { id: Pubkey, owner: Pubkey, delegate: Pubkey, nonce: u64, data_hash: [u8; 32], creator_hash: [u8; 32], }, } #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone, Default)] pub enum Version { #[default] V1, } impl LeafSchema { pub fn new_v0( id: Pubkey, owner: Pubkey, delegate: Pubkey, nonce: u64, data_hash: [u8; 32], creator_hash: [u8; 32], ) -> Self { Self::V1 { id, owner, delegate, nonce, data_hash, creator_hash, } } pub fn version(&self) -> Version { match self { LeafSchema::V1 { .. } => Version::V1, } } pub fn to_node(&self) -> Node { let hashed_leaf = match self { LeafSchema::V1 { id, owner, delegate, nonce, data_hash, creator_hash, } => keccak::hashv(&[ &[self.version().to_bytes()], id.as_ref(), owner.as_ref(), delegate.as_ref(), nonce.to_le_bytes().as_ref(), data_hash.as_ref(), creator_hash.as_ref(), ]) .to_bytes(), }; hashed_leaf } /// ... }
 

Append Leaf

/// --- programs/bubblegum/program/src/utils.rs --- pub fn append_leaf<'info>( seed: &Pubkey, bump: u8, compression_program: &AccountInfo<'info>, authority: &AccountInfo<'info>, merkle_tree: &AccountInfo<'info>, log_wrapper: &AccountInfo<'info>, leaf_node: Node, ) -> Result<()> { let seeds = &[seed.as_ref(), &[bump]]; let authority_pda_signer = &[&seeds[..]]; let cpi_ctx = CpiContext::new_with_signer( compression_program.clone(), spl_account_compression::cpi::accounts::Modify { authority: authority.clone(), merkle_tree: merkle_tree.clone(), noop: log_wrapper.clone(), }, authority_pda_signer, ); spl_account_compression::cpi::append(cpi_ctx, leaf_node) }

Mint to Collection

mint_to_collection_v1 allows minting a new compressed NFT asset (a leaf in a Merkle tree) that is directly associated with a specific collection. This can be understood as minting an NFT and simultaneously verifying that it belongs to a particular on-chain collection.
 
The function does the following:
  1. Validates that the caller has permission to mint to this tree.
  1. Checks that the tree has remaining mint capacity.
  1. Prepares a set of signer accounts that can verify creators.
  1. Processes collection verification via Metaplex Token Metadata program constraints.
  1. Once verified, it proceeds with the minting of the leaf (similar to mint_v1) but includes the additional step of ensuring the asset is attached to a valid, verified collection.
 
Key Accounts and Their Roles:
  • tree_authority: PDA that holds the Merkle tree’s configuration (creator, delegate, capacity, etc.).
  • leaf_owner: The eventual owner of the minted NFT.
  • leaf_delegate: A delegate that can manage the NFT on behalf of the owner.
  • merkle_tree: The Merkle tree account that stores the compressed NFT leaves.
  • payer: The account paying for the transaction.
  • tree_delegate: A signer who must match certain constraints if the tree is not public.
  • collection_authority: The signer that has authority over the collection.
  • collection_authority_record_pda: Optional PDA that proves the collection_authority’s rights.
  • collection_mint, collection_metadata, edition_account: Accounts related to the collection NFT, ensuring that the new NFT is part of a valid and verified collection.
/// --- programs/bubblegum/program/src/lib.rs --- /// Mints a new asset and adds it to a collection. pub fn mint_to_collection_v1( ctx: Context<MintToCollectionV1>, metadata_args: MetadataArgs, ) -> Result<LeafSchema> { processor::mint_to_collection_v1(ctx, metadata_args) } /// -- programs/bubblegum/program/src/processor/mint_to_collection.rs --- #[derive(Accounts)] pub struct MintToCollectionV1<'info> { #[account( mut, seeds = [merkle_tree.key().as_ref()], bump, )] pub tree_authority: Account<'info, TreeConfig>, /// CHECK: This account is neither written to nor read from. pub leaf_owner: AccountInfo<'info>, /// CHECK: This account is neither written to nor read from. pub leaf_delegate: AccountInfo<'info>, #[account(mut)] /// CHECK: unsafe pub merkle_tree: UncheckedAccount<'info>, pub payer: Signer<'info>, pub tree_delegate: Signer<'info>, pub collection_authority: Signer<'info>, /// CHECK: Optional collection authority record PDA. /// If there is no collecton authority record PDA then /// this must be the Bubblegum program address. pub collection_authority_record_pda: UncheckedAccount<'info>, /// CHECK: This account is checked in the instruction pub collection_mint: UncheckedAccount<'info>, #[account(mut)] pub collection_metadata: Box<Account<'info, TokenMetadata>>, /// CHECK: This account is checked in the instruction pub edition_account: UncheckedAccount<'info>, /// CHECK: This is no longer needed but kept for backwards compatibility. pub bubblegum_signer: UncheckedAccount<'info>, pub log_wrapper: Program<'info, Noop>, pub compression_program: Program<'info, SplAccountCompression>, /// CHECK: This is no longer needed but kept for backwards compatibility. pub token_metadata_program: UncheckedAccount<'info>, pub system_program: Program<'info, System>, }
 
Steps:
  1. Extract Accounts
  1. Authority Check:
    1. If the tree is not public, ensure that the tree_delegate signer matches either the stored tree_creator or tree_delegate in TreeConfig.
  1. Capacity Check:
    1. Check if the tree has at least one remaining mint slot.
  1. Metadata Auth Set:
    1. Create a HashSet of signers who can verify creators. Creators marked as verified in MetadataArgs must be signed by one of these accounts.
  1. Process Collection Verification:
    1. It calls process_collection_verification_mpl_only to ensure that:
      • The collection details in message.collection correspond to the provided collection_metadata, collection_mint, edition_account.
      • The collection_authority is indeed authorized to verify this collection.
      • message.collection.verified is set to true upon successful verification (since true is passed as the verify parameter).
      This step ensures the NFT being minted will be tied to a valid and verified collection on-chain.
  1. Mint the Asset:
    1. After collection verification, the code calls process_mint_v1:
      • process_mint_v1 is a shared helper used by mint_v1 as well.
      • It validates metadata again, checks creators, hashes data, and appends a new leaf to the Merkle tree.
      • The difference here is that allow_verified_collection is true, meaning it will accept a verified collection in the metadata.
      This will return a LeafSchema that represents the newly minted NFT.
  1. Increment the Mint Count:
    1. If process_mint_v1 succeeds, increment authority.num_minted to reflect the new minted asset.
  1. Return the Leaf:
    1. Return the newly created LeafSchema instance representing the minted asset.
/// --- programs/bubblegum/program/src/processor/mint_to_collection.rs --- pub(crate) fn mint_to_collection_v1( ctx: Context<MintToCollectionV1>, metadata_args: MetadataArgs, ) -> Result<LeafSchema> { let mut message = metadata_args; /// Extract Accounts: // TODO -> Separate V1 / V1 into seperate instructions let payer = ctx.accounts.payer.key(); let incoming_tree_delegate = ctx.accounts.tree_delegate.key(); let owner = ctx.accounts.leaf_owner.key(); let delegate = ctx.accounts.leaf_delegate.key(); let authority = &mut ctx.accounts.tree_authority; let merkle_tree = &ctx.accounts.merkle_tree; let collection_metadata = &ctx.accounts.collection_metadata; let collection_mint = ctx.accounts.collection_mint.to_account_info(); let edition_account = ctx.accounts.edition_account.to_account_info(); let collection_authority = ctx.accounts.collection_authority.to_account_info(); let collection_authority_record_pda = ctx .accounts .collection_authority_record_pda .to_account_info(); /// Authority Check: if !authority.is_public { require!( incoming_tree_delegate == authority.tree_creator || incoming_tree_delegate == authority.tree_delegate, BubblegumError::TreeAuthorityIncorrect, ); } /// Capacity Check: if !authority.contains_mint_capacity(1) { return Err(BubblegumError::InsufficientMintCapacity.into()); } /// Metadata Auth Set: // Create a HashSet to store signers to use with creator validation. Any signer can be // counted as a validated creator. let mut metadata_auth = HashSet::<Pubkey>::new(); metadata_auth.insert(payer); metadata_auth.insert(incoming_tree_delegate); // If there are any remaining accounts that are also signers, they can also be used for // creator validation. metadata_auth.extend( ctx.remaining_accounts .iter() .filter(|a| a.is_signer) .map(|a| a.key()), ); /// Process Collection Verification: process_collection_verification_mpl_only( collection_metadata, &collection_mint, &collection_authority, &collection_authority_record_pda, &edition_account, &mut message, true, )?; /// Mint the Asset: let leaf = process_mint_v1( message, owner, delegate, metadata_auth, *ctx.bumps.get("tree_authority").unwrap(), authority, merkle_tree, &ctx.accounts.log_wrapper, &ctx.accounts.compression_program, true, )?; /// Increment the Mint Count: authority.increment_mint_count(); /// Return the Leaf: Ok(leaf) }

Process Collection Verification

process_collection_verification_mpl_only is responsible for verifying that a given NFT’s metadata and collection details conform to the Metaplex Token Metadata (MPL) program’s rules and that the provided collection_authority is authorized to verify the collection.
 
Parameters:
  • collection_metadata: An Account wrapper around the TokenMetadata account, representing the metadata for the collection NFT.
  • collection_mint: The account info for the mint of the collection NFT.
  • collection_authority: The account info for the authority allowed to verify or unverify the collection.
  • collection_authority_record_pda: An optional PDA (Program Derived Address) that may store collection authority records. If this is not used, the Bubblegum program’s ID will appear here instead.
  • edition_account: The account info for the Master Edition or Edition account associated with the collection NFT.
  • message: A mutable reference to MetadataArgs. This structure holds the metadata arguments (e.g., collection info) for the NFT being minted or updated.
  • verify: A boolean indicating whether we are verifying (true) or un-verifying (false) the collection on this NFT.
 
Steps:
  1. Collection Authority Record Handling:
    1. The function first checks if the collection_authority_record_pda is simply the Bubblegum program’s ID. If it is, it means no dedicated collection authority record PDA was provided, and it sets collection_authority_record to None. Otherwise, it treats collection_authority_record_pda as a valid authority record account (Some(...)).
      This enables the function to handle two cases:
      • No special authority record: rely on the default authority checks.
      • Special authority record: rely on the provided record for more granular checks.
  1. Ownership Checks:
    1. The function requires that:
      • The collection_metadata account is owned by the Metaplex Token Metadata program (mpl_token_metadata::ID).
      • The collection_mint account is owned by the SPL Token program (Not token program 2022) (spl_token::id()).
      • The edition_account (Master Edition or Edition) is also owned by mpl_token_metadata::ID.
      This ensures that the accounts involved match the expected on-chain programs.
  1. Collection Validation and Verification:
    1. The code checks if message.collection is Some(...). If no collection is present, it returns error.
      If a collection is present:
      • Calls assert_collection_membership(...) to ensure that:
        • The collection referenced in message.collection matches the collection_metadata.
        • The collection_mint and edition_account correspond to a valid NFT collection that meets Metaplex standards.
      • Calls assert_has_collection_authority(...) to verify that the provided collection_authority (and optionally the collection_authority_record_pda) is legitimately allowed to modify the verification status of this collection. This ensures only authorized entities can verify/unverify the collection association.
      If both checks succeed, the collection.verified field in message.collection is updated to be verify parameter which is true.
/// --- programs/bubblegum/program/src/processor/mod.rs --- #[allow(deprecated)] fn process_collection_verification_mpl_only<'info>( collection_metadata: &Account<'info, TokenMetadata>, collection_mint: &AccountInfo<'info>, collection_authority: &AccountInfo<'info>, collection_authority_record_pda: &AccountInfo<'info>, edition_account: &AccountInfo<'info>, message: &mut MetadataArgs, verify: bool, ) -> Result<()> { /// See if a collection authority record PDA was provided. let collection_authority_record = if collection_authority_record_pda.key() == crate::id() { None } else { Some(collection_authority_record_pda) }; // Verify correct account ownerships. require!( *collection_metadata.to_account_info().owner == mpl_token_metadata::ID, BubblegumError::IncorrectOwner ); require!( *collection_mint.owner == spl_token::id(), BubblegumError::IncorrectOwner ); require!( *edition_account.owner == mpl_token_metadata::ID, BubblegumError::IncorrectOwner ); /// Collection Validation and Verification: // If the NFT has collection data, we set it to the correct value after doing some validation. if let Some(collection) = &mut message.collection { assert_collection_membership( &Some(collection.adapt()), collection_metadata, collection_mint.key, edition_account, )?; assert_has_collection_authority( collection_metadata, collection_mint.key, collection_authority.key, collection_authority_record, )?; // Update collection in metadata args. Note since this is a mutable reference, // it is still updating `message.collection` after being destructured. collection.verified = verify; } else { return Err(BubblegumError::CollectionNotFound.into()); } Ok(()) }
 

Membership Check

/// --- programs/bubblegum/program/src/asserts.rs --- pub fn assert_collection_membership( membership: &Option<Collection>, collection_metadata: &Metadata, collection_mint: &Pubkey, collection_edition: &AccountInfo, ) -> Result<()> { match membership { Some(collection) => { if collection.key != *collection_mint || collection_metadata.mint != *collection_mint { return Err(BubblegumError::CollectionNotFound.into()); } } None => { return Err(BubblegumError::CollectionNotFound.into()); } } let (expected, _) = mpl_token_metadata::accounts::MasterEdition::find_pda(collection_mint); if collection_edition.key != &expected { return Err(BubblegumError::CollectionMasterEditionAccountInvalid.into()); } let edition = mpl_token_metadata::accounts::MasterEdition::try_from(collection_edition) .map_err(|_err| BubblegumError::CollectionMustBeAUniqueMasterEdition)?; match collection_metadata.token_standard { Some(TokenStandard::NonFungible) | Some(TokenStandard::ProgrammableNonFungible) => (), _ => return Err(BubblegumError::CollectionMustBeAUniqueMasterEdition.into()), } if edition.max_supply != Some(0) { return Err(BubblegumError::CollectionMustBeAUniqueMasterEdition.into()); } Ok(()) }

Authority Check

/// --- programs/bubblegum/program/src/asserts.rs --- // Checks both delegate types: old collection_authority_record and newer // metadata_delegate pub fn assert_has_collection_authority( collection_data: &Metadata, mint: &Pubkey, collection_authority: &Pubkey, delegate_record: Option<&AccountInfo>, ) -> Result<()> { // Mint is the correct one for the metadata account. if collection_data.mint != *mint { return Err(BubblegumError::MetadataMintMismatch.into()); } if let Some(record_info) = delegate_record { let (ca_pda, ca_bump) = CollectionAuthorityRecord::find_pda(mint, collection_authority); let (md_pda, md_bump) = MetadataDelegateRecord::find_pda( mint, MetadataDelegateRole::Collection, &collection_data.update_authority, collection_authority, ); let data = record_info.try_borrow_data()?; if data.len() == 0 { return Err(BubblegumError::InvalidCollectionAuthority.into()); } if record_info.key == &ca_pda { let record = CollectionAuthorityRecord::safe_deserialize(&data)?; if record.bump != ca_bump { return Err(BubblegumError::InvalidCollectionAuthority.into()); } match record.update_authority { Some(update_authority) => { if update_authority != collection_data.update_authority { return Err(BubblegumError::InvalidCollectionAuthority.into()); } } None => return Err(BubblegumError::InvalidCollectionAuthority.into()), } } else if record_info.key == &md_pda { let record = MetadataDelegateRecord::safe_deserialize(&data)?; if record.bump != md_bump { return Err(BubblegumError::InvalidCollectionAuthority.into()); } if record.update_authority != collection_data.update_authority { return Err(BubblegumError::InvalidCollectionAuthority.into()); } } else { return Err(BubblegumError::InvalidDelegateRecord.into()); } } else if collection_data.update_authority != *collection_authority { return Err(BubblegumError::InvalidCollectionAuthority.into()); } Ok(()) }

Transfer

The transfer function updates the ownership of an already-existing compressed NFT (represented as a leaf in a Merkle tree) to a new owner.
 
Parameters:
  • root: The current root of the Merkle tree (before the update).
  • data_hash and creator_hash: Hashes representing the metadata and creators of the NFT. These remain unchanged during a transfer.
  • nonce: A unique nonce for this particular NFT, used to generate a unique asset ID.
  • index: The index of the leaf being modified in the Merkle tree.
/// --- programs/bubblegum/program/src/lib.rs --- /// Transfers a leaf node from one account to another. pub fn transfer<'info>( ctx: Context<'_, '_, '_, 'info, Transfer<'info>>, root: [u8; 32], data_hash: [u8; 32], creator_hash: [u8; 32], nonce: u64, index: u32, ) -> Result<()> { processor::transfer(ctx, root, data_hash, creator_hash, nonce, index) } /// --- programs/bubblegum/program/src/processor/transfer.rs --- #[derive(Accounts)] pub struct Transfer<'info> { #[account( seeds = [merkle_tree.key().as_ref()], bump, )] /// CHECK: This account is neither written to nor read from. pub tree_authority: Account<'info, TreeConfig>, /// CHECK: This account is checked in the instruction pub leaf_owner: UncheckedAccount<'info>, /// CHECK: This account is chekced in the instruction pub leaf_delegate: UncheckedAccount<'info>, /// CHECK: This account is neither written to nor read from. pub new_leaf_owner: UncheckedAccount<'info>, #[account(mut)] /// CHECK: This account is modified in the downstream program pub merkle_tree: UncheckedAccount<'info>, pub log_wrapper: Program<'info, Noop>, pub compression_program: Program<'info, SplAccountCompression>, pub system_program: Program<'info, System>, }
 
Steps:
  1. Context Extraction:
    1. The function receives a Context<Transfer>, which includes all the accounts defined in the Transfer account struct.
      • merkle_tree is the on-chain pda account owned by account compression program holding the Merkle tree state.
      • owner is the account currently listed as the NFT’s owner.
      • delegate is the account currently listed as the NFT’s delegate.
  1. Authority Check:
    1. To ensure security, the code checks that either the current leaf_owner or the leaf_delegate is a signer of this transaction. If neither is signing, it returns LeafAuthorityMustSign. This prevents unauthorized transfers.
  1. Determining the New Owner:
      • new_owner is simply the public key of the new_leaf_owner account.
      • asset_id is computed by calling get_asset_id, which derives a program address (PDA) based on:
        • A prefix (ASSET_PREFIX),
        • The merkle_tree public key,
        • The nonce.
      This asset_id uniquely identifies the NFT (leaf) inside the Merkle tree.
  1. Constructing Old and New Leaves:
      • previous_leaf: Represents the NFT’s current state before transfer. This includes the current owner and delegate.
      • new_leaf: Represents the updated NFT state after the transfer. Notice that for the new leaf:
        • The owner is now new_owner.
        • The delegate is also set to new_owner. This effectively removes the old delegate and sets the new owner as their own delegate, simplifying ownership control.
      Importantly, data_hash and creator_hash remain unchanged. The NFT’s identity and metadata do not alter on transfer—only the ownership and delegate fields change.
  1. Event Logging:
      • new_leaf.to_event() produces an event representation of the leaf’s new state.
      • try_to_vec() serializes this event into bytes.
      • wrap_application_data_v1(...) sends this serialized data through a Noop program, leveraging CPI (Cross-Program Invocation) for logging. This ensures the transfer event is recorded and can be indexed by off-chain indexers.
      This logging step is crucial for transparency and replaying the state of the Merkle tree off-chain if needed.
  1. Replacing the Leaf in the Merkle Tree:
    1. The replace_leaf function is called to update the Merkle tree on-chain. This is a CPI call into the spl_account_compression program:
      Parameters:
      • &merkle_tree.key(): The Merkle tree’s public key, used to identify the tree in the compression program.
      • The PDA bump for tree_authority so the tree authority PDA can sign the instruction.
      • References to compression_program, tree_authority, merkle_tree, log_wrapper, and any remaining_accounts needed for proof or additional constraints.
      • root: The old root of the tree before updating.
      • previous_leaf.to_node() and new_leaf.to_node(): The old and new leaf nodes, representing the leaf before and after the transfer.
      • index: The position of the leaf in the Merkle tree.
      The replace_leaf CPI call uses a secure protocol to recalculate the Merkle tree root and store the updated leaf on-chain. Post-execution, the tree will reflect the new ownership of the NFT.
      Note that the proof is stored in remaining accounts.
/// --- programs/bubblegum/program/src/processor/transfer.rs --- pub(crate) fn transfer<'info>( ctx: Context<'_, '_, '_, 'info, Transfer<'info>>, root: [u8; 32], data_hash: [u8; 32], creator_hash: [u8; 32], nonce: u64, index: u32, ) -> Result<()> { /// Context Extraction: // TODO add back version to select hash schema let merkle_tree = ctx.accounts.merkle_tree.to_account_info(); let owner = ctx.accounts.leaf_owner.to_account_info(); let delegate = ctx.accounts.leaf_delegate.to_account_info(); /// Authority Check: // Transfers must be initiated by either the leaf owner or leaf delegate. require!( owner.is_signer || delegate.is_signer, BubblegumError::LeafAuthorityMustSign ); /// Determining the New Owner: let new_owner = ctx.accounts.new_leaf_owner.key(); let asset_id = get_asset_id(&merkle_tree.key(), nonce); /// Constructing Old and New Leaves: let previous_leaf = LeafSchema::new_v0( asset_id, owner.key(), delegate.key(), nonce, data_hash, creator_hash, ); // New leafs are instantiated with no delegate let new_leaf = LeafSchema::new_v0( asset_id, new_owner, new_owner, nonce, data_hash, creator_hash, ); /// Event Logging: wrap_application_data_v1(new_leaf.to_event().try_to_vec()?, &ctx.accounts.log_wrapper)?; /// Replacing the Leaf in the Merkle Tree: replace_leaf( &merkle_tree.key(), *ctx.bumps.get("tree_authority").unwrap(), &ctx.accounts.compression_program.to_account_info(), &ctx.accounts.tree_authority.to_account_info(), &ctx.accounts.merkle_tree.to_account_info(), &ctx.accounts.log_wrapper.to_account_info(), ctx.remaining_accounts, root, previous_leaf.to_node(), new_leaf.to_node(), index, ) }

Burn

burn instruction handles removing (or “burning”) a compressed NFT (a leaf in a Merkle tree) by replacing it with a default/empty leaf node.
 
Parameters:
  • ctx: The Context<Burn> providing all the required accounts and signers.
  • root: The current root of the Merkle tree.
  • data_hash & creator_hash: Hashes representing the NFT’s metadata and creators.
  • nonce: Unique nonce used to derive the NFT’s asset ID.
  • index: The position of the leaf to be burned in the Merkle tree.
/// Burns a leaf node from the tree. pub fn burn<'info>( ctx: Context<'_, '_, '_, 'info, Burn<'info>>, root: [u8; 32], data_hash: [u8; 32], creator_hash: [u8; 32], nonce: u64, index: u32, ) -> Result<()> { processor::burn(ctx, root, data_hash, creator_hash, nonce, index) } #[derive(Accounts)] pub struct Burn<'info> { #[account( seeds = [merkle_tree.key().as_ref()], bump, )] pub tree_authority: Account<'info, TreeConfig>, /// CHECK: This account is checked in the instruction pub leaf_owner: UncheckedAccount<'info>, /// CHECK: This account is checked in the instruction pub leaf_delegate: UncheckedAccount<'info>, #[account(mut)] /// CHECK: This account is modified in the downstream program pub merkle_tree: UncheckedAccount<'info>, pub log_wrapper: Program<'info, Noop>, pub compression_program: Program<'info, SplAccountCompression>, pub system_program: Program<'info, System>, }
 
Steps:
  1. Authority Check:
    1. The function ensures that the transaction is authorized by either the leaf_owner or leaf_delegate.
  1. Identify the Leaf Being Burned:
      • asset_id is derived using the get_asset_id function, which deterministically computes a PDA from the Merkle tree's public key and the nonce. This unique asset ID corresponds to the NFT.
      • previous_leaf is constructed using LeafSchema::new_v0. It represents the leaf in its current state (before burning). This includes:
        • asset_id
        • Current owner and delegate public keys
        • nonce
        • data_hash and creator_hash that identify the metadata and creator data for the NFT
  1. Creating the "Burned" Leaf:
    1. new_leaf is set to a default Node, which is essentially an empty placeholder. By replacing the current leaf with this empty node, we effectively remove the NFT from the Merkle tree.
  1. Replacing the Leaf in the Merkle Tree:
      • The core of the burn operation is the call to replace_leaf. This function:
      • Takes the old leaf (previous_leaf) and the new leaf (new_leaf = empty node).
      • Updates the Merkle tree at index to use the new_leaf.
      • Recomputes and updates the Merkle tree root accordingly via a CPI call to the spl_account_compression program.
/// --- programs/bubblegum/program/src/processor/burn.rs --- pub(crate) fn burn<'info>( ctx: Context<'_, '_, '_, 'info, Burn<'info>>, root: [u8; 32], data_hash: [u8; 32], creator_hash: [u8; 32], nonce: u64, index: u32, ) -> Result<()> { /// Authority Check: let owner = ctx.accounts.leaf_owner.to_account_info(); let delegate = ctx.accounts.leaf_delegate.to_account_info(); // Burn must be initiated by either the leaf owner or leaf delegate. require!( owner.is_signer || delegate.is_signer, BubblegumError::LeafAuthorityMustSign ); /// Identify the Leaf Being Burned: let merkle_tree = ctx.accounts.merkle_tree.to_account_info(); let asset_id = get_asset_id(&merkle_tree.key(), nonce); let previous_leaf = LeafSchema::new_v0( asset_id, owner.key(), delegate.key(), nonce, data_hash, creator_hash, ); /// Creating the "Burned" Leaf: let new_leaf = Node::default(); /// Replacing the Leaf in the Merkle Tree: replace_leaf( &merkle_tree.key(), *ctx.bumps.get("tree_authority").unwrap(), &ctx.accounts.compression_program.to_account_info(), &ctx.accounts.tree_authority.to_account_info(), &ctx.accounts.merkle_tree.to_account_info(), &ctx.accounts.log_wrapper.to_account_info(), ctx.remaining_accounts, root, previous_leaf.to_node(), new_leaf, index, ) }

Redeem

redeem instruction is designed to handle the redemption of a compressed NFT (represented as a leaf in a Merkle tree) by effectively removing it from the tree.
Also, redeem process creates an voucher account which records the original cNFT info, which can be used in decompress instruction to mint a standard NFT.
 
The redeem function serves to:
  1. Validate Decompression Capability: Ensure that the tree permits decompression operations.
  1. Authorize the Redemption: Confirm that the entity initiating the redemption has the necessary permissions (either as the leaf owner or delegate).
  1. Update the Merkle Tree: Replace the targeted leaf node with a default (empty) node, effectively marking it as redeemed.
  1. Create a Voucher: Instantiate a Voucher account to record the redemption event for off-chain tracking or further processing.
/// --- programs/bubblegum/program/src/lib.rs --- /// Redeems a vouches. /// /// Once a vouch is redeemed, the corresponding leaf node is removed from the tree. pub fn redeem<'info>( ctx: Context<'_, '_, '_, 'info, Redeem<'info>>, root: [u8; 32], data_hash: [u8; 32], creator_hash: [u8; 32], nonce: u64, index: u32, ) -> Result<()> { processor::redeem(ctx, root, data_hash, creator_hash, nonce, index) } /// --- programs/bubblegum/program/src/processor/redeem.rs -- #[derive(Accounts)] #[instruction( _root: [u8; 32], _data_hash: [u8; 32], _creator_hash: [u8; 32], nonce: u64, _index: u32, )] pub struct Redeem<'info> { #[account( seeds = [merkle_tree.key().as_ref()], bump, )] /// CHECK: This account is neither written to nor read from. pub tree_authority: Account<'info, TreeConfig>, #[account(mut)] pub leaf_owner: Signer<'info>, /// CHECK: This account is chekced in the instruction pub leaf_delegate: UncheckedAccount<'info>, #[account(mut)] /// CHECK: checked in cpi pub merkle_tree: UncheckedAccount<'info>, #[account( init, seeds = [ VOUCHER_PREFIX.as_ref(), merkle_tree.key().as_ref(), & nonce.to_le_bytes() ], payer = leaf_owner, space = VOUCHER_SIZE, bump )] pub voucher: Account<'info, Voucher>, pub log_wrapper: Program<'info, Noop>, pub compression_program: Program<'info, SplAccountCompression>, pub system_program: Program<'info, System>, }
 
Steps:
  1. Decompression Capability Check
    1. Ensures that the Merkle tree is configured to allow decompression operations.
  1. Extracting Accounts
  1. Creating a Default (Empty) Leaf
    1. Initializes a new leaf node as a default Node, effectively emptying the space previously occupied by the NFT.
      Replacing the old leaf with this default node marks the NFT as burned or redeemed.
  1. Replacing the Leaf in the Merkle Tree
    1. Performs a Cross-Program Invocation (CPI) to the SPL Account Compression program to replace a leaf in the Merkle tree.
  1. Creating the Voucher Account
    1. Initializes the voucher account with details of the redeemed leaf. Voucher is used to cancel redeem in future.
pub(crate) fn redeem<'info>( ctx: Context<'_, '_, '_, 'info, Redeem<'info>>, root: [u8; 32], data_hash: [u8; 32], creator_hash: [u8; 32], nonce: u64, index: u32, ) -> Result<()> { /// Decompression Capability Check if ctx.accounts.tree_authority.is_decompressible == DecompressibleState::Disabled { return Err(BubblegumError::DecompressionDisabled.into()); } /// Extracting Accounts let owner = ctx.accounts.leaf_owner.key(); let delegate = ctx.accounts.leaf_delegate.key(); let merkle_tree = ctx.accounts.merkle_tree.to_account_info(); let asset_id = get_asset_id(&merkle_tree.key(), nonce); let previous_leaf = LeafSchema::new_v0(asset_id, owner, delegate, nonce, data_hash, creator_hash); /// Creating a Default (Empty) Leaf let new_leaf = Node::default(); /// Replacing the Leaf in the Merkle Tree replace_leaf( &merkle_tree.key(), *ctx.bumps.get("tree_authority").unwrap(), &ctx.accounts.compression_program.to_account_info(), &ctx.accounts.tree_authority.to_account_info(), &ctx.accounts.merkle_tree.to_account_info(), &ctx.accounts.log_wrapper.to_account_info(), ctx.remaining_accounts, root, previous_leaf.to_node(), new_leaf, index, )?; /// Creating the Voucher Account ctx.accounts .voucher .set_inner(Voucher::new(previous_leaf, index, merkle_tree.key())); Ok(()) }

Cancel Redeem

cancel_redeem cancels previous redeem, re-mint the original cNFT according to info recorded in the voucher.
Only the original cNFT’s owner can redeem the NFT.
This design has a problem, if a NFT protocol wants to convert users’ cNFTs to ordinary NFTs. They can’t prevent users from cancelling the redemption. But of course, the project can let users first transfer cNFT to them, then it redeems those cNFTs, and finally mints ordinary NFTs to users.
/// --- programs/bubblegum/program/src/lib.rs --- /// Cancels a redeem. pub fn cancel_redeem<'info>( ctx: Context<'_, '_, '_, 'info, CancelRedeem<'info>>, root: [u8; 32], ) -> Result<()> { processor::cancel_redeem(ctx, root) } /// --- programs/bubblegum/program/src/processor/cancel_redeem.rs --- #[derive(Accounts)] pub struct CancelRedeem<'info> { #[account( seeds = [merkle_tree.key().as_ref()], bump, )] /// CHECK: This account is neither written to nor read from. pub tree_authority: Account<'info, TreeConfig>, #[account(mut)] pub leaf_owner: Signer<'info>, #[account(mut)] /// CHECK: unsafe pub merkle_tree: UncheckedAccount<'info>, #[account( mut, close = leaf_owner, seeds = [ VOUCHER_PREFIX.as_ref(), merkle_tree.key().as_ref(), & voucher.leaf_schema.nonce().to_le_bytes() ], bump )] pub voucher: Account<'info, Voucher>, pub log_wrapper: Program<'info, Noop>, pub compression_program: Program<'info, SplAccountCompression>, pub system_program: Program<'info, System>, }
 
/// --- programs/bubblegum/program/src/processor/cancel_redeem.rs --- pub(crate) fn cancel_redeem<'info>( ctx: Context<'_, '_, '_, 'info, CancelRedeem<'info>>, root: [u8; 32], ) -> Result<()> { let voucher = &ctx.accounts.voucher; match ctx.accounts.voucher.leaf_schema { LeafSchema::V1 { owner, .. } => assert_pubkey_equal( &ctx.accounts.leaf_owner.key(), &owner, Some(BubblegumError::AssetOwnerMismatch.into()), ), }?; let merkle_tree = ctx.accounts.merkle_tree.to_account_info(); wrap_application_data_v1( voucher.leaf_schema.to_event().try_to_vec()?, &ctx.accounts.log_wrapper, )?; replace_leaf( &merkle_tree.key(), *ctx.bumps.get("tree_authority").unwrap(), &ctx.accounts.compression_program.to_account_info(), &ctx.accounts.tree_authority.to_account_info(), &ctx.accounts.merkle_tree.to_account_info(), &ctx.accounts.log_wrapper.to_account_info(), ctx.remaining_accounts, root, [0; 32], voucher.leaf_schema.to_node(), voucher.index, ) }

Decompress

The decompress_v1 function is designed to handle the decompression or redemption of a compressed NFT within a Merkle tree.
Decompression involves:
  1. Redeeming a Voucher: A voucher represents a compressed NFT that can be redeemed to instantiate a standard SPL Token (NFT) with associated metadata.
  1. Creating Token Accounts: Upon redemption, the function creates necessary token accounts if they don't already exist.
  1. Initializing Metadata: It sets up the NFT's metadata using the Metaplex Token Metadata program.
  1. Managing Authorities: Ensures that the correct authorities are assigned to the minted NFT.
  1. Updating the Merkle Tree: Replaces the redeemed leaf with a default node, effectively marking it as redeemed.
 
Parameters:
  • ctx: Context<DecompressV1> – Provides access to the accounts defined in the DecompressV1 struct.
  • metadata: MetadataArgs – User-supplied metadata for the NFT to be created upon redemption.
/// --- programs/bubblegum/program/src/lib.rs --- /// Decompresses a leaf node from the tree. pub fn decompress_v1(ctx: Context<DecompressV1>, metadata: MetadataArgs) -> Result<()> { processor::decompress_v1(ctx, metadata) } #[derive(Accounts)] pub struct DecompressV1<'info> { #[account( mut, close = leaf_owner, seeds = [ VOUCHER_PREFIX.as_ref(), voucher.merkle_tree.as_ref(), voucher.leaf_schema.nonce().to_le_bytes().as_ref() ], bump )] pub voucher: Box<Account<'info, Voucher>>, #[account(mut)] pub leaf_owner: Signer<'info>, /// CHECK: versioning is handled in the instruction #[account(mut)] pub token_account: UncheckedAccount<'info>, /// CHECK: versioning is handled in the instruction #[account( mut, seeds = [ ASSET_PREFIX.as_ref(), voucher.merkle_tree.as_ref(), voucher.leaf_schema.nonce().to_le_bytes().as_ref(), ], bump )] pub mint: UncheckedAccount<'info>, /// CHECK: #[account( mut, seeds = [mint.key().as_ref()], bump, )] pub mint_authority: UncheckedAccount<'info>, /// CHECK: #[account(mut)] pub metadata: UncheckedAccount<'info>, /// CHECK: Initialized in Token Metadata Program #[account(mut)] pub master_edition: UncheckedAccount<'info>, pub system_program: Program<'info, System>, pub sysvar_rent: Sysvar<'info, Rent>, /// CHECK: pub token_metadata_program: Program<'info, MplTokenMetadata>, pub token_program: Program<'info, Token>, pub associated_token_program: Program<'info, AssociatedToken>, pub log_wrapper: Program<'info, Noop>, }
 
Steps:
  1. Metadata Validation
      • Ensures that the provided metadata aligns with the data_hash stored in the voucher.
      • Validates the integrity and ownership before proceeding with redemption. Only leaf owner can decompress NFT.
  1. Asset Identification
      • Determines the version of the token program being used.
      • Currently, only Original version is supported; Token2022 is not handled and will result in an error.
  1. Mint Initialization
    1. Initializes the mint account for the NFT if it hasn't been initialized yet.
      • Check if Mint Account is Empty:
        • ctx.accounts.mint.data_is_empty() checks whether the mint account has been initialized.
      • Create Mint Account
      • Initialize Mint
  1. Token Account Initialization
    1. Creates an associated token account for the minted NFT if it doesn't already exist.
  1. Minting NFT to ATA
    1. Mints NFT to the associated token account.
  1. Assigning Mint Authority
    1. Assigns the mint authority to the current program.
  1. Creating Metadata Account
    1. Initializes the Metaplex Token Metadata account for the newly minted NFT.
      This transaction sets the metadata.token_standard to be TokenStandard::FungibleAsset .
  1. Creating Master Edition
    1. Creates a Master Edition account for the NFT, which is essential for enforcing uniqueness and handling editions.
      It transfers mint_authority to master edition acocunt to ensure NFT’s uniqueness.
      This transaction sets the metadata.token_standard to be TokenStandard::NonFungible.
  1. Finalizing Mint Authority
    1. Reassigns the mint_authority account's owner to the System Program, effectively removing program control over it.
  1. Returning Success
/// --- programs/bubblegum/program/src/processor/decompress.rs --- pub(crate) fn decompress_v1(ctx: Context<DecompressV1>, metadata: MetadataArgs) -> Result<()> { /// Metadata Validation match ctx.accounts.voucher.leaf_schema { LeafSchema::V1 { owner, data_hash, .. } => { let incoming_data_hash: [u8; 32] = hash_metadata(&metadata)?; if !cmp_bytes(&data_hash, &incoming_data_hash, 32) { return Err(BubblegumError::HashingMismatch.into()); } if !cmp_pubkeys(&owner, ctx.accounts.leaf_owner.key) { return Err(BubblegumError::AssetOwnerMismatch.into()); } } } /// Asset Identification let voucher = &ctx.accounts.voucher; match metadata.token_program_version { TokenProgramVersion::Original => { /// Mint Initialization if ctx.accounts.mint.data_is_empty() { invoke_signed( &system_instruction::create_account( &ctx.accounts.leaf_owner.key(), &ctx.accounts.mint.key(), Rent::get()?.minimum_balance(Mint::LEN), Mint::LEN as u64, &spl_token::id(), ), &[ ctx.accounts.leaf_owner.to_account_info(), ctx.accounts.mint.to_account_info(), ctx.accounts.system_program.to_account_info(), ], &[&[ ASSET_PREFIX.as_bytes(), voucher.merkle_tree.key().as_ref(), voucher.leaf_schema.nonce().to_le_bytes().as_ref(), &[*ctx.bumps.get("mint").unwrap()], ]], )?; invoke( &spl_token::instruction::initialize_mint2( &spl_token::id(), &ctx.accounts.mint.key(), &ctx.accounts.mint_authority.key(), Some(&ctx.accounts.mint_authority.key()), 0, )?, &[ ctx.accounts.token_program.to_account_info(), ctx.accounts.mint.to_account_info(), ], )?; } /// Token Account Initialization if ctx.accounts.token_account.data_is_empty() { invoke( &spl_associated_token_account::instruction::create_associated_token_account( &ctx.accounts.leaf_owner.key(), &ctx.accounts.leaf_owner.key(), &ctx.accounts.mint.key(), &spl_token::ID, ), &[ ctx.accounts.leaf_owner.to_account_info(), ctx.accounts.mint.to_account_info(), ctx.accounts.token_account.to_account_info(), ctx.accounts.token_program.to_account_info(), ctx.accounts.associated_token_program.to_account_info(), ctx.accounts.system_program.to_account_info(), ctx.accounts.sysvar_rent.to_account_info(), ], )?; } /// Minting NFT to ATA // SPL token will check that the associated token account is initialized, that it // has the correct owner, and that the mint (which is a PDA of this program) // matches. invoke_signed( &spl_token::instruction::mint_to( &spl_token::id(), &ctx.accounts.mint.key(), &ctx.accounts.token_account.key(), &ctx.accounts.mint_authority.key(), &[], 1, )?, &[ ctx.accounts.mint.to_account_info(), ctx.accounts.token_account.to_account_info(), ctx.accounts.mint_authority.to_account_info(), ctx.accounts.token_program.to_account_info(), ], &[&[ ctx.accounts.mint.key().as_ref(), &[ctx.bumps["mint_authority"]], ]], )?; } TokenProgramVersion::Token2022 => return Err(ProgramError::InvalidArgument.into()), } /// Assigning Mint Authority invoke_signed( &system_instruction::assign(&ctx.accounts.mint_authority.key(), &crate::id()), &[ctx.accounts.mint_authority.to_account_info()], &[&[ ctx.accounts.mint.key().as_ref(), &[*ctx.bumps.get("mint_authority").unwrap()], ]], )?; /// Creating Metadata Account msg!("Creating metadata"); CreateMetadataAccountV3CpiBuilder::new(&ctx.accounts.token_metadata_program) .metadata(&ctx.accounts.metadata) .mint(&ctx.accounts.mint) .mint_authority(&ctx.accounts.mint_authority) .payer(&ctx.accounts.leaf_owner) .update_authority(&ctx.accounts.mint_authority, true) .system_program(&ctx.accounts.system_program) .data(DataV2 { name: metadata.name.clone(), symbol: metadata.symbol.clone(), uri: metadata.uri.clone(), creators: if metadata.creators.is_empty() { None } else { Some(metadata.creators.iter().map(|c| c.adapt()).collect()) }, collection: metadata.collection.map(|c| c.adapt()), seller_fee_basis_points: metadata.seller_fee_basis_points, uses: metadata.uses.map(|u| u.adapt()), }) .is_mutable(metadata.is_mutable) .invoke_signed(&[&[ ctx.accounts.mint.key().as_ref(), &[ctx.bumps["mint_authority"]], ]])?; /// Creating Master Edition msg!("Creating master edition"); CreateMasterEditionV3CpiBuilder::new(&ctx.accounts.token_metadata_program) .edition(&ctx.accounts.master_edition) .mint(&ctx.accounts.mint) .mint_authority(&ctx.accounts.mint_authority) .update_authority(&ctx.accounts.mint_authority) .metadata(&ctx.accounts.metadata) .payer(&ctx.accounts.leaf_owner) .system_program(&ctx.accounts.system_program) .token_program(&ctx.accounts.token_program) .max_supply(0) .invoke_signed(&[&[ ctx.accounts.mint.key().as_ref(), &[ctx.bumps["mint_authority"]], ]])?; /// Finalizing Mint Authority ctx.accounts .mint_authority .to_account_info() .assign(&System::id()); Ok(()) }
 

Assign Mint Authority to

During NFT mint and metadata initialization, it needs to also migrate the creator and collection verification information. These information has been verified in bubblegum program already, so there is no need to verify it again in the metaplex program.
In metaplex program, it checks the mint_authority account of NFT mint, if its owner is bubblegum program and the address is calculated using bubblegum program id and the mint address, then metaplex just migrates creator and collection information without checks.
/// --- programs/token-metadata/program/src/utils/metadata.rs --- #[allow(clippy::too_many_arguments)] pub fn process_create_metadata_accounts_logic( program_id: &Pubkey, accounts: CreateMetadataAccountsLogicArgs, data: DataV2, allow_direct_creator_writes: bool, mut is_mutable: bool, is_edition: bool, add_token_standard: bool, collection_details: Option<CollectionDetails>, token_standard_override: Option<TokenStandard>, programmable_config: Option<ProgrammableConfig>, ) -> ProgramResult { /// ... // This allows the Bubblegum program to create metadata with verified creators since they were // verified already by the Bubblegum program. let is_decompression = is_decompression(mint_info, mint_authority_info); let allow_direct_creator_writes = allow_direct_creator_writes || is_decompression; assert_data_valid( &compatible_data, &update_authority_key, &metadata, allow_direct_creator_writes, update_authority_info.is_signer, )?; /// ... // This allows for either print editions or the Bubblegum program to create metadata with verified collection. let allow_direct_collection_verified_writes = is_edition || is_decompression; assert_collection_update_is_valid( allow_direct_collection_verified_writes, &None, &data.collection, )?; /// ... }

Set Decompressible State

set_decompressible_state instruction sets whether the NFTs in certain tree can be decompressed.
/// --- programs/bubblegum/program/src/lib.rs --- /// Sets the `decompressible_state` of a tree. pub fn set_decompressible_state( ctx: Context<SetDecompressibleState>, decompressable_state: DecompressibleState, ) -> Result<()> { processor::set_decompressible_state(ctx, decompressable_state) } /// --- programs/bubblegum/program/src/processor/set_decompressible_state.rs --- #[derive(Accounts)] pub struct SetDecompressibleState<'info> { #[account(mut, has_one = tree_creator)] pub tree_authority: Account<'info, TreeConfig>, pub tree_creator: Signer<'info>, } pub(crate) fn set_decompressible_state( ctx: Context<SetDecompressibleState>, decompressable_state: DecompressibleState, ) -> Result<()> { ctx.accounts.tree_authority.is_decompressible = decompressable_state; Ok(()) }

Update Metadata

The update_metadata instruction is designed to update the metadata of an existing NFT (Non-Fungible Token) within a Merkle tree. It ensures that only authorized entities can modify an NFT's metadata and that any updates adhere to the constraints imposed by the Metaplex Token Metadata program.
 
It handles:
  1. Authorization: Ensuring that the updater has the right to modify the NFT's metadata, especially in the context of collections.
  1. Metadata Validation: Confirming that the new metadata conforms to Metaplex's standards, including proper creator verification and adherence to maximum length constraints.
  1. Merkle Tree Integrity: Updating the leaf node within the Merkle tree to reflect the new metadata while maintaining the tree's cryptographic integrity.
  1. Event Logging: Recording the metadata update event for off-chain indexing and transparency.
 
Parameters:
  • ctx: Context<UpdateMetadata<'info>>: Provides access to the accounts defined in the UpdateMetadata struct.
  • root: [u8; 32]: The current root hash of the Merkle tree before the update.
  • nonce: u64: A unique number associated with the NFT, used for generating asset IDs.
  • index: u32: The index position of the NFT's leaf within the Merkle tree.
  • current_metadata: MetadataArgs: The existing metadata of the NFT.
  • update_args: UpdateArgs: The new metadata fields to be updated.
/// --- programs/bubblegum/program/src/lib.rs --- /// Updates metadata for a leaf node that is not part of a verified collection. pub fn update_metadata<'info>( ctx: Context<'_, '_, '_, 'info, UpdateMetadata<'info>>, root: [u8; 32], nonce: u64, index: u32, current_metadata: MetadataArgs, update_args: UpdateArgs, ) -> Result<()> { processor::update_metadata(ctx, root, nonce, index, current_metadata, update_args) } /// --- programs/bubblegum/program/src/processor/update_metadata.rs --- #[derive(Accounts)] pub struct UpdateMetadata<'info> { #[account( seeds = [merkle_tree.key().as_ref()], bump, )] /// CHECK: This account is neither written to nor read from. pub tree_authority: Account<'info, TreeConfig>, /// Either collection authority or tree owner/delegate, depending /// on whether the item is in a verified collection pub authority: Signer<'info>, /// CHECK: This account is checked in the instruction /// Used when item is in a verified collection pub collection_mint: Option<UncheckedAccount<'info>>, /// Used when item is in a verified collection pub collection_metadata: Option<Box<Account<'info, TokenMetadata>>>, /// CHECK: This account is checked in the instruction pub collection_authority_record_pda: Option<UncheckedAccount<'info>>, /// CHECK: This account is checked in the instruction pub leaf_owner: UncheckedAccount<'info>, /// CHECK: This account is checked in the instruction pub leaf_delegate: UncheckedAccount<'info>, pub payer: Signer<'info>, #[account(mut)] /// CHECK: This account is modified in the downstream program pub merkle_tree: UncheckedAccount<'info>, pub log_wrapper: Program<'info, Noop>, pub compression_program: Program<'info, SplAccountCompression>, /// CHECK: This is no longer needed but kept for backwards compatibility. pub token_metadata_program: UncheckedAccount<'info>, pub system_program: Program<'info, System>, } /// --- programs/bubblegum/program/src/state/metaplex_adapter.rs --- #[derive(AnchorSerialize, AnchorDeserialize, PartialEq, Eq, Debug, Clone, Default)] pub struct UpdateArgs { pub name: Option<String>, pub symbol: Option<String>, pub uri: Option<String>, pub creators: Option<Vec<Creator>>, pub seller_fee_basis_points: Option<u16>, pub primary_sale_happened: Option<bool>, pub is_mutable: Option<bool>, }
 
Steps:
  1. Collection Verification
    1. Determines whether the NFT is part of a verified collection and verifies the authority accordingly.
      • Case 1: NFT is in a Verified Collection
        • Only metadata update authority or delegate of the collection mint can update the cNFT’s metadata.
      • Case 2: NFT is Not in a Verified Collection or Collection is Unverified
        • Only tree creator or delegate can update cNFT’s metadata.
  1. Processing Metadata Updates
    1. Handles the core logic of updating the metadata, including validation, updating the Merkle tree, and ensuring consistency.
/// --- programs/bubblegum/program/src/processor/update_metadata.rs --- pub fn update_metadata<'info>( ctx: Context<'_, '_, '_, 'info, UpdateMetadata<'info>>, root: [u8; 32], nonce: u64, index: u32, current_metadata: MetadataArgs, update_args: UpdateArgs, ) -> Result<()> { match &current_metadata.collection { // Verified collection case. Some(collection) if collection.verified => { let collection_mint = ctx .accounts .collection_mint .as_ref() .ok_or(BubblegumError::MissingCollectionMintAccount)?; let collection_metadata = ctx .accounts .collection_metadata .as_ref() .ok_or(BubblegumError::MissingCollectionMetadataAccount)?; assert_authority_matches_collection( collection, &ctx.accounts.authority.to_account_info(), &ctx.accounts.collection_authority_record_pda, collection_mint, &collection_metadata.to_account_info(), collection_metadata, )?; } // No collection or unverified collection case. _ => { require!( ctx.accounts.authority.key() == ctx.accounts.tree_authority.tree_creator || ctx.accounts.authority.key() == ctx.accounts.tree_authority.tree_delegate, BubblegumError::TreeAuthorityIncorrect, ); } } process_update_metadata( &ctx.accounts.merkle_tree.to_account_info(), &ctx.accounts.authority, &ctx.accounts.leaf_owner, &ctx.accounts.leaf_delegate, &ctx.accounts.compression_program.to_account_info(), &ctx.accounts.tree_authority.to_account_info(), *ctx.bumps.get("tree_authority").unwrap(), &ctx.accounts.log_wrapper, ctx.remaining_accounts, root, current_metadata, update_args, nonce, index, ) }

Assert Authority Matches Collection

Ensures that the provided collection_authority is indeed authorized to manage the collection's NFTs.
Steps:
  1. Mint Account Validation:
      • Ensures that the collection_mint matches the collection's key.
      • Ensures that the collection_metadata's mint matches the collection's key.
  1. Ownership Checks:
      • Confirms that the collection_metadata account is owned by the Metaplex Token Metadata program.
      • Ensures that the collection_mint is owned by the SPL Token program.
  1. Collection Authority Verification:
      • Extracts the collection_authority_record_pda if provided.
      • Calls assert_has_collection_authority to verify that the collection_authority is legitimate.
/// --- programs/bubblegum/program/src/processor/update_metadata.rs --- fn assert_authority_matches_collection<'info>( collection: &Collection, collection_authority: &AccountInfo<'info>, collection_authority_record_pda: &Option<UncheckedAccount<'info>>, collection_mint: &AccountInfo<'info>, collection_metadata_account_info: &AccountInfo, collection_metadata: &TokenMetadata, ) -> Result<()> { // Mint account must match Collection mint require!( collection_mint.key() == collection.key, BubblegumError::CollectionMismatch ); // Metadata mint must match Collection mint require!( collection_metadata.mint == collection.key, BubblegumError::CollectionMismatch ); // Verify correct account ownerships. require!( *collection_metadata_account_info.owner == mpl_token_metadata::ID, BubblegumError::IncorrectOwner ); // Collection mint must be owned by SPL token require!( *collection_mint.owner == spl_token::id(), BubblegumError::IncorrectOwner ); let collection_authority_record = collection_authority_record_pda .as_ref() .map(|authority_record_pda| authority_record_pda.to_account_info()); // Assert that the correct Collection Authority was provided using token-metadata assert_has_collection_authority( collection_metadata, collection_mint.key, collection_authority.key, collection_authority_record.as_ref(), )?; Ok(()) } /// --- programs/bubblegum/program/src/asserts.rs --- // Checks both delegate types: old collection_authority_record and newer // metadata_delegate pub fn assert_has_collection_authority( collection_data: &Metadata, mint: &Pubkey, collection_authority: &Pubkey, delegate_record: Option<&AccountInfo>, ) -> Result<()> { // Mint is the correct one for the metadata account. if collection_data.mint != *mint { return Err(BubblegumError::MetadataMintMismatch.into()); } if let Some(record_info) = delegate_record { let (ca_pda, ca_bump) = CollectionAuthorityRecord::find_pda(mint, collection_authority); let (md_pda, md_bump) = MetadataDelegateRecord::find_pda( mint, MetadataDelegateRole::Collection, &collection_data.update_authority, collection_authority, ); let data = record_info.try_borrow_data()?; if data.len() == 0 { return Err(BubblegumError::InvalidCollectionAuthority.into()); } if record_info.key == &ca_pda { let record = CollectionAuthorityRecord::safe_deserialize(&data)?; if record.bump != ca_bump { return Err(BubblegumError::InvalidCollectionAuthority.into()); } match record.update_authority { Some(update_authority) => { if update_authority != collection_data.update_authority { return Err(BubblegumError::InvalidCollectionAuthority.into()); } } None => return Err(BubblegumError::InvalidCollectionAuthority.into()), } } else if record_info.key == &md_pda { let record = MetadataDelegateRecord::safe_deserialize(&data)?; if record.bump != md_bump { return Err(BubblegumError::InvalidCollectionAuthority.into()); } if record.update_authority != collection_data.update_authority { return Err(BubblegumError::InvalidCollectionAuthority.into()); } } else { return Err(BubblegumError::InvalidDelegateRecord.into()); } } else if collection_data.update_authority != *collection_authority { return Err(BubblegumError::InvalidCollectionAuthority.into()); } Ok(()) }

Process Update Metadata

The process_update_metadata function is responsible for updating the metadata of an existing NFT within a Merkle tree. This involves:
  1. Validating Mutability: Ensuring that the NFT's metadata is allowed to be updated.
  1. Updating Metadata Fields: Applying the requested changes to the metadata.
  1. Ensuring Creator Integrity: Maintaining the verification status of creators to prevent unauthorized alterations.
  1. Validating Compatibility: Ensuring the updated metadata adheres to Metaplex's standards.
  1. Updating the Merkle Tree: Reflecting the metadata changes in the Merkle tree to maintain its cryptographic integrity.
  1. Logging the Update: Recording the update event for off-chain indexing and transparency.
 
Steps:
  1. Metadata Mutability Check
    1. Ensures that the existing metadata allows for updates. If the is_mutable flag is false, the function aborts, preventing any unauthorized or unintended modifications.
  1. Hash Calculations
      • current_data_hash: Computes a Keccak-256 hash of the current metadata to ensure data integrity.
      • current_creator_hash: Computes a Keccak-256 hash of the current creators' data to maintain creator verification integrity.
      This is to ensure that both metadata and creator information are consistent and have not been tampered with.
  1. Applying Updates
    1. Applies the requested updates to the NFT's metadata based on the provided update_args.
      • Name, Symbol, URI Updates:
        • If name, symbol, or uri are provided in update_args, they replace the corresponding fields in updated_metadata.
      • Verification Constraints:
        • No New Verified Creators: Ensures that no new creators are marked as verified unless they are the authority (typically the tree delegate).
        • No Unverified Removals: Ensures that existing verified creators are not unverified or removed unless they are the authority.
      • Seller Fee Basis Points Update:
        • If seller_fee_basis_points is provided, it updates the corresponding field in updated_metadata.
      • Primary Sale Happened Update:
        • Allows flipping primary_sale_happened from false to true only.
        • Constraint: Prevents reverting primary_sale_happened from true back to false.
      • Mutability Flag Update:
        • If is_mutable is provided, updates the corresponding flag in updated_metadata.
  1. Metadata Compatibility Check
    1. Ensures that the updated metadata complies with the Metaplex Token Metadata program's standards and constraints.
      • Name Length: Must not exceed MAX_NAME_LENGTH.
      • Symbol Length: Must not exceed MAX_SYMBOL_LENGTH.
      • URI Length: Must not exceed MAX_URI_LENGTH.
      • Seller Fee Basis Points: Must not exceed 10000 (equivalent to 100%).
      • Creators Constraints:
        • Maximum Creators: Must not exceed MAX_CREATOR_LIMIT.
        • Unique Addresses: No duplicate creator addresses.
        • Total Share: The sum of all creator shares must equal 100.
  1. Recalculating Hashes
      • updated_data_hash: Computes the Keccak-256 hash of the updated metadata.
      • updated_creator_hash: Computes the Keccak-256 hash of the updated creators' data.
  1. Constructing New Leaf Schemas
  1. Event Logging
    1. Logs the metadata update event, enabling off-chain indexing.
  1. Updating the Merkle Tree
    1. Replaces the old leaf in the Merkle tree with the updated leaf, ensuring the tree's cryptographic integrity.
/// --- programs/bubblegum/program/src/processor/update_metadata.rs --- fn process_update_metadata<'info>( merkle_tree: &AccountInfo<'info>, authority: &AccountInfo<'info>, owner: &AccountInfo<'info>, delegate: &AccountInfo<'info>, compression_program: &AccountInfo<'info>, tree_authority: &AccountInfo<'info>, tree_authority_bump: u8, log_wrapper: &Program<'info, Noop>, remaining_accounts: &[AccountInfo<'info>], root: [u8; 32], current_metadata: MetadataArgs, update_args: UpdateArgs, nonce: u64, index: u32, ) -> Result<()> { /// Metadata Mutability Check // Old metadata must be mutable to allow metadata update require!( current_metadata.is_mutable, BubblegumError::MetadataImmutable ); /// Hash Calculations let current_data_hash = hash_metadata(&current_metadata)?; let current_creator_hash = hash_creators(&current_metadata.creators)?; /// Applying Updates let mut updated_metadata = current_metadata; if let Some(name) = update_args.name { updated_metadata.name = name; }; if let Some(symbol) = update_args.symbol { updated_metadata.symbol = symbol; }; if let Some(uri) = update_args.uri { updated_metadata.uri = uri; }; if let Some(updated_creators) = update_args.creators { let current_creators = updated_metadata.creators; // Make sure no new creator is verified (unless it is the tree delegate). let no_new_creators_verified = all_verified_creators_in_a_are_in_b( &updated_creators, &current_creators, authority.key(), ); require!( no_new_creators_verified, BubblegumError::CreatorDidNotVerify ); // Make sure no current verified creator is unverified or removed (unless it is the tree // delegate). let no_current_creators_unverified = all_verified_creators_in_a_are_in_b( &current_creators, &updated_creators, authority.key(), ); require!( no_current_creators_unverified, BubblegumError::CreatorDidNotUnverify ); updated_metadata.creators = updated_creators; } if let Some(seller_fee_basis_points) = update_args.seller_fee_basis_points { updated_metadata.seller_fee_basis_points = seller_fee_basis_points }; if let Some(primary_sale_happened) = update_args.primary_sale_happened { // a new value of primary_sale_happened should only be specified if primary_sale_happened was false in the original metadata require!( !updated_metadata.primary_sale_happened, BubblegumError::PrimarySaleCanOnlyBeFlippedToTrue ); updated_metadata.primary_sale_happened = primary_sale_happened; }; if let Some(is_mutable) = update_args.is_mutable { updated_metadata.is_mutable = is_mutable; }; /// Metadata Compatibility Check assert_metadata_is_mpl_compatible(&updated_metadata)?; /// Recalculating Hashes let updated_data_hash = hash_metadata(&updated_metadata)?; let updated_creator_hash = hash_creators(&updated_metadata.creators)?; /// Constructing Leaf Schemas let asset_id = get_asset_id(&merkle_tree.key(), nonce); let previous_leaf = LeafSchema::new_v0( asset_id, owner.key(), delegate.key(), nonce, current_data_hash, current_creator_hash, ); let new_leaf = LeafSchema::new_v0( asset_id, owner.key(), delegate.key(), nonce, updated_data_hash, updated_creator_hash, ); /// Event Logging wrap_application_data_v1(new_leaf.to_event().try_to_vec()?, log_wrapper)?; /// Updating the Merkle Tree replace_leaf( &merkle_tree.key(), tree_authority_bump, compression_program, tree_authority, merkle_tree, &log_wrapper.to_account_info(), remaining_accounts, root, previous_leaf.to_node(), new_leaf.to_node(), index, ) }

Reference