Solana 上的压缩 NFT

这篇文章深入探讨了 Solana 上的压缩 NFT (cNFT),重点介绍了 account-compressionmpl-bubblegum 程序。cNFT 利用 Merkle 树将 NFT 数据存储在链下,从而显著降低成本和复杂性。文章详细解释了 cNFT 与传统 NFT 的区别,以及如何使用相关程序进行创建、Mint、转移和销毁 cNFT。

cNFTcNFT

想象一下尝试在 Solana 上铸造 100 万个 NFT。传统的 NFT 将需要 200 万个链上账户:每个 NFT 一个 mint 账户和一个 token 账户,从而导致过高的存储和交易成本。压缩 NFT (cNFT) 通过将 NFT 数据以 Merkle 树的形式存储在链下,而仅将根存储在链上来解决这个问题。这种方法大幅降低了成本,使大规模 NFT 项目从不切实际变为经济可行。

在这篇文章中,我们将探讨 Solana 上的 压缩 NFT (cNFT),重点关注 account-compressionmpl-bubblegum 程序。我们将介绍 cNFT 如何使用 Merkle 树在链下存储 NFT 数据,从而显著降低成本和复杂性。本指南专为希望了解 cNFT 内部运作方式而无需深入研究代码库的开发人员和审计人员设计。

我们将首先了解什么是 cNFT,然后深入研究 account-compression 程序,最后探讨 mpl-bubblegum

在阅读本文时,你无需打开代码,这里解释了一切!😉

1. NFTs vs cNFTs

1.1 What are NFTs in Solana?

在 Solana 上,NFT 与常规 token 类似。唯一的区别是 NFT 的总供应量为 1。 要持有 NFT,你需要:

  • 一个 token 账户:持有 NFT。
  • 一个 mint 账户:存储 NFT 的数据。

两者都在 SPL Token 程序下进行管理。

对于 100 个 NFT,你需要:

  • 100 个 token 账户。
  • 100 个 mint 账户。

总共 200 个账户,每个账户都有自己的公钥和租金存款。尤其是租金成本会迅速增加。

1.2 What are cNFTs and how are they different?

由于我们需要为每个 NFT 提供链上账户,因此扩展 NFT 会很快变得昂贵。例如,在游戏中,你可能需要数百万个 NFT,从而导致数百万个账户。

为了解决这个问题,引入了 压缩 NFT (cNFT)。它们使用 Merkle 树 (Concurrent Merkle Trees) 在链下存储 NFT 数据(ID、所有者、元数据等),并且仅将 Merkle 树的根 存储在链上。

为什么我们将 Merkle 树的根存储在链上账户中?

对于验证,例如,如果有人想转移自己的 NFT,我们会传递基于该 NFT 的叶子,并在根的帮助下,我们验证该叶子是否存在于 Merkle 树上。

树中的每个叶子代表一个完整的 NFT,包括所有者和元数据。无需创建数千个账户,你只需创建一个 Merkle 树账户并使用 Merkle 证明验证所有权。

这大大降低了成本,从数百万个账户减少到 2 个账户。

memememe

2. account-compression

Account Compression Program 的源代码可在 GitHub 上找到。

这个想法很简单:

  • 创建和管理链下的 Concurrent Merkle Tree
  • 仅将 树的根 存储在链上。
  • 使用链上根和证明验证更改(如转移)。

account-compressionaccount-compression

虽然在本指南中它用于 cNFT,但该程序是通用的,也可以支持其他用例。

每个叶子都可以代表一个 NFT,其中包含其所有者和相关元数据。当发生诸如转移之类的操作时,更改将在链下进行,并且仅使用根在链上验证证明。

叶子的示例:

        LeafSchema::new_v1(
                asset_id,
                leaf_owner.key(),
                leaf_delegate.key(),
                nonce,
                data_hash,
                creator_hash,
        );

让我们深入研究代码,从 Noop 程序开始。

2.1: Noop Program

这是一个占位符程序,什么也不做(no-op 意味着不操作)。它用于发出日志,供链下索引器拾取。(有关无操作日志记录及其计算成本优势的更多信息,请参阅 我们的 100 个 Solana 技巧中的技巧 #11。)

当你通过 noop 发送交易时,它不会进行任何计算,但索引器可以获取调用数据,将其解释为日志并相应地处理这些数据。

use solana_program::{
        account_info::AccountInfo, declare_id, entrypoint::ProgramResult, instruction::Instruction,
        pubkey::Pubkey,
};

declare_id!("noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV");

##[cfg(not(feature = "no-entrypoint"))]
solana_program::entrypoint!(noop);

pub fn noop(
        _program_id: &Pubkey,
        _accounts: &[AccountInfo],
        _instruction_data: &[u8],
) -> ProgramResult {
        Ok(())
}

pub fn instruction(data: Vec<u8>) -> Instruction {
        Instruction {
                program_id: crate::id(),
                accounts: vec![],
                data,
        }
}

调用示例:

pub fn wrap_event<'info>(
        event: &AccountCompressionEvent,
        noop_program: &Program<'info, Noop>,
) -> Result<()> {
        invoke(
                &spl_noop::instruction(event.try_to_vec()?),
                &[noop_program.to_account_info()],
        )?;
        Ok(())
}

event.try_to_vec()? 是我们在链下需要的数据。

2.2: account-compression Program

这是 Merkle 树管理的核心。它包括以下指令:

  • 创建一个树。
  • 追加叶子 (mint nft)。
  • 替换叶子 (transfer, burn nft)。
  • 验证叶子(可选)。

让我们简要总结一下关键指令:

2.2.1 init_empty_merkle_tree

初始化一个新的空 Merkle 树。

  1. 账户
/// 用于初始化新的 SPL ConcurrentMerkleTree 的上下文
##[derive(Accounts)]
pub struct Initialize<'info> {
        #[account(zero)]
        /// CHECK: 此账户将被清零,并且将验证大小
        pub merkle_tree: UncheckedAccount<'info>,

        /// 控制对树的写入访问权限的权限
        /// 通常是一个程序,例如,Bubblegum 合约验证叶子是否为有效的 NFT。
        pub authority: Signer<'info>,

        /// 用于将变更日志作为 cpi 指令数据发出的程序。
        pub noop: Program<'info, Noop>,
}
  • Merkle 树账户,它必须由此程序拥有(将在逻辑中检查)并且必须为零。
  • 将控制 Merkle 树的权限,对于 cNFT,从 Bubblegum 派生的 PDA 将是 Merkle 树的权限。
  • noop 程序。
  1. 输入
        pub fn init_empty_merkle_tree(
                ctx: Context<Initialize>,
                max_depth: u32,
                max_buffer_size: u32,
        ) -> Result<()> {
  • max_depth 是树的高度,它定义了树的最大叶子 (2**max_depth)。
  • max_buffer_size 是最小并发限制,例如,如果它是 1024,则在必须为下一个替换指令生成新证明之前,可以执行至少 1024 个替换。

此数据必须事先计算好,具体取决于需要。

  1. 逻辑
                require_eq!(
                        *ctx.accounts.merkle_tree.owner,
                        crate::id(),
                        AccountCompressionError::IncorrectAccountOwner
                );
                let mut merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_mut_data()?;

                let (mut header_bytes, rest) =
                        merkle_tree_bytes.split_at_mut(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1);

                let mut header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
                header.initialize(
                        max_depth,
                        max_buffer_size,
                        &ctx.accounts.authority.key(),
                        Clock::get()?.slot,
                );
                header.serialize(&mut header_bytes)?;

                let merkle_tree_size = merkle_tree_get_size(&header)?;
                let (tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size);
                let id = ctx.accounts.merkle_tree.key();

                let change_log_event = merkle_tree_initialize_empty(&header, id, tree_bytes)?;

                wrap_event(
                        &AccountCompressionEvent::ChangeLog(*change_log_event),
                        &ctx.accounts.noop,
                )?;
                update_canopy(canopy_bytes, header.get_max_depth(), None)
        }
  • 检查所有者。
  • 分割数据以获取标头并使用输入、权限和时间对其进行初始化。
  • 标头:

这是标头的结构:

pub struct ConcurrentMerkleTreeHeader {
        /// 账户类型
        pub account_type: CompressionAccountType,
        /// 版本化的标头
        pub header: ConcurrentMerkleTreeHeaderData,
}

我们有两种类型:Uninitialized 和 ConcurrentMerkleTree。

pub struct ConcurrentMerkleTreeHeaderDataV1 {
        max_buffer_size: u32,
        max_depth: u32,
        authority: Pubkey,
        creation_slot: u64,
        _padding: [u8; 6],
}

它将类型设置为 ConcurrentMerkleTree 并分配其他字段。

  • 现在这是有趣的部分:在分配标头之后,我们将 merkle_tree_bytes 的其余部分用于 Merkle 树,而另一部分 (rest-merkletreesize) 将用于 canopy_bytes
  • 然后初始化树,我们将使用 spl-concurrent-merkle-treewrap_event 进行初始化。
  • 正如你所看到的,当我们更改树时,我们将获得 change_log_event。我们将把这些数据发送到 noop 程序,以便索引器获取这些数据并在链下进行更改。
  • 毕竟,我们将更新 canopy。那么什么是 canopy_bytes 呢?
Canopy Nodes

canopycanopy

当我们想要更改叶子时,我们必须提供证明。证明在剩余的账户中提供,因此对于每个节点,我们必须传递一个账户。由于 交易大小限制,我们无法传递许多节点。因此,在某些情况下,当最大深度很高时,我们会在交易中遇到大小限制。

解决方案?Canopy Nodes

这些 canopy_bytes 帮助我们生成和提供证明。这些是树中的一些上层节点,我们将在证明中需要这些节点。因此,我们将这些节点存储在链上账户中,并且在提供证明期间,我们不需要提供所有节点,我们传递一些较低的节点,并且 account-compression 程序会为我们附加这些上层节点。

例如:如果我们有 10 的深度并存储 3 个顶部节点,这 3 个顶部节点是 canopy_bytes,我们不需要将它们传递给程序。当我们提供剩余的 7 个节点作为证明时,程序将使用这些 canopy_bytes 更新证明。

pub fn update_canopy(
        canopy_bytes: &mut [u8],
        max_depth: u32,
        change_log: Option<&ChangeLogEvent>,
) -> Result<()> {
        check_canopy_bytes(canopy_bytes)?;
        let canopy = cast_slice_mut::<u8, Node>(canopy_bytes);
        let path_len = get_cached_path_length(canopy, max_depth)?;
        if let Some(cl_event) = change_log {
                match &*cl_event {
                        ChangeLogEvent::V1(cl) => {
                                // 从最新的变更日志更新 canopy
                                for path_node in cl.path.iter().rev().skip(1).take(path_len as usize) {
                                        // node_idx - 2 映射到 canopy 索引
                                        canopy[(path_node.index - 2) as usize] = path_node.node;
                                }
                        }
                }
        }
        Ok(())
}

它首先检查 canopy_bytes 的大小是否乘以每个节点,然后确定应将 Merkle 路径中的多少个节点更新到 canopy 中。

然后,如果我们希望事件为我们创建数据,在我们的例子中我们不需要,我们将 change_log 设置为 None。

2.2.2 transfer_authority

将 Merkle 树的写入访问权限从一个权限转移到另一个权限。

        pub fn transfer_authority(
                ctx: Context<TransferAuthority>,
                new_authority: Pubkey,
        ) -> Result<()> {
                require_eq!(
                        *ctx.accounts.merkle_tree.owner,
                        crate::id(),
                        AccountCompressionError::IncorrectAccountOwner
                );
                let mut merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_mut_data()?;
                let (mut header_bytes, _) =
                        merkle_tree_bytes.split_at_mut(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1);

                let mut header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
                header.assert_valid_authority(&ctx.accounts.authority.key())?;

                header.set_new_authority(&new_authority);
                header.serialize(&mut header_bytes)?;

                Ok(())
        }

2.2.3 close_empty_tree

关闭未使用的 Merkle 树并将租金 (lamports) 返回给接收者。

  1. 账户
##[derive(Accounts)]
pub struct CloseTree<'info> {
        #[account(mut)]
        /// CHECK: 此账户在指令中验证
        pub merkle_tree: AccountInfo<'info>,

        /// 控制对树的写入访问权限的权限
        pub authority: Signer<'info>,

        /// CHECK: 之后的资金接收者
        #[account(mut)]
        pub recipient: AccountInfo<'info>,
}
  • Merkle 树账户,它必须由此程序拥有(将在逻辑中检查)。
  • 控制 Merkle 树的权限。
  • 用于获取资金的接收者。
  1. 输入
  • 无需输入。
  1. 逻辑
                require_eq!(
                        *ctx.accounts.merkle_tree.owner,
                        crate::id(),
                        AccountCompressionError::IncorrectAccountOwner
                );
                let mut merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_mut_data()?;
                let (header_bytes, rest) =
                        merkle_tree_bytes.split_at_mut(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1);

                let header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
                header.assert_valid_authority(&ctx.accounts.authority.key())?;

                let merkle_tree_size = merkle_tree_get_size(&header)?;
                let (tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size);

                let id = ctx.accounts.merkle_tree.key();
                merkle_tree_prove_tree_is_empty(&header, id, tree_bytes)?;

                // 关闭 Merkle 树账户
                // 1. 移动 lamports
                let dest_starting_lamports = ctx.accounts.recipient.lamports();
                **ctx.accounts.recipient.lamports.borrow_mut() = dest_starting_lamports
                        .checked_add(ctx.accounts.merkle_tree.lamports())
                        .unwrap();
                **ctx.accounts.merkle_tree.lamports.borrow_mut() = 0;

                // 2. 将所有 CMT 账户字节设置为 0
                header_bytes.fill(0);
                tree_bytes.fill(0);
                canopy_bytes.fill(0);

                Ok(())
  • 它首先检查树是否归程序所有。
  • 验证权限。
  • 检查树是否为空。
  • 删除树并将 Lamports 发送给接收者。

2.2.4 append

这是权限可以将新叶子追加到树的函数。

这是我们想要铸造 NFT 时在 Bubblegum 中使用的函数。

  1. 账户
##[derive(Accounts)]
pub struct Modify<'info> {
        #[account(mut)]
        /// CHECK: 此账户在指令中验证
        pub merkle_tree: UncheckedAccount<'info>,

        /// 控制对树的写入访问权限的权限
        /// 通常是一个程序,例如,Bubblegum 合约验证叶子是否为有效的 NFT。
        pub authority: Signer<'info>,

        /// 用于将变更日志作为 cpi 指令数据发出的程序。
        pub noop: Program<'info, Noop>,
}
  • Merkle 树账户,它必须由此程序拥有(将在逻辑中检查)。
  • 控制 Merkle 树的权限。
  • 我们将调用以获取链下数据并链下追加叶子的 noop 程序。
  1. 输入
leaf: [u8; 32]
  • 这是我们想要追加的叶子,对于 cNFT 情况,Bubblegum 将使用以下叶子:
        LeafSchema::new_v1(
                asset_id,
                leaf_owner.key(),
                leaf_delegate.key(),
                nonce,
                data_hash,
                creator_hash,
        );
  1. 逻辑
                require_eq!(
                        *ctx.accounts.merkle_tree.owner,
                        crate::id(),
                        AccountCompressionError::IncorrectAccountOwner
                );
                let mut merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_mut_data()?;
                let (header_bytes, rest) =
                        merkle_tree_bytes.split_at_mut(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1);

                let header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
                header.assert_valid_authority(&ctx.accounts.authority.key())?;

                let id = ctx.accounts.merkle_tree.key();
                let merkle_tree_size = merkle_tree_get_size(&header)?;
                let (tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size);
                let change_log_event = merkle_tree_append_leaf(&header, id, tree_bytes, &leaf)?;
                update_canopy(
                        canopy_bytes,
                        header.get_max_depth(),
                        Some(&change_log_event),
                )?;
                wrap_event(
                        &AccountCompressionEvent::ChangeLog(*change_log_event),
                        &ctx.accounts.noop,
                )
  • 它首先检查树是否归程序所有。
  • 验证权限。
  • 获取 tree_bytes 和 canopy_bytes。
  • 通过调用 noop 程序记录事件。
  • 更新 canopy。

2.2.5 insert_or_append

尝试在特定索引处插入叶子。如果失败,则追加它。

  1. 账户
  • 与上面相同。
  1. 输入
        root: [u8; 32],
        leaf: [u8; 32],
        index: u32,
  • 除了叶子之外,它还将获取要插入的叶子的索引以及用于验证的根。
  1. 逻辑
                require_eq!(
                        *ctx.accounts.merkle_tree.owner,
                        crate::id(),
                        AccountCompressionError::IncorrectAccountOwner
                );
                let mut merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_mut_data()?;
                let (header_bytes, rest) =
                        merkle_tree_bytes.split_at_mut(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1);

                let header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
                header.assert_valid_authority(&ctx.accounts.authority.key())?;
                header.assert_valid_leaf_index(index)?;

                let merkle_tree_size = merkle_tree_get_size(&header)?;
                let (tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size);

                let mut proof = vec![];
                for node in ctx.remaining_accounts.iter() {
                        proof.push(node.key().to_bytes());
                }
                fill_in_proof_from_canopy(canopy_bytes, header.get_max_depth(), index, &mut proof)?;
                // 调用 ConcurrentMerkleTree::fill_empty_or_append
                let id = ctx.accounts.merkle_tree.key();
                let args = &FillEmptyOrAppendArgs {
                        current_root: root,
                        leaf,
                        proof_vec: proof,
                        index,
                };
                let change_log_event = merkle_tree_fill_empty_or_append(&header, id, tree_bytes, args)?;

                update_canopy(
                        canopy_bytes,
                        header.get_max_depth(),
                        Some(&change_log_event),
                )?;
                wrap_event(
                        &AccountCompressionEvent::ChangeLog(*change_log_event),
                        &ctx.accounts.noop,
                )
  • 它首先检查树是否归程序所有。
  • 验证权限。
  • 验证索引是否在范围内 leaf_index < (1 << self.get_max_depth())
  • 获取 tree_bytes 和 canopy_bytes。
  • 获取证明:证明将从 remaining_accounts 获得。
  • 使用 canopy 更新证明,正如我们所说,由于交易限制,由于我们无法在剩余账户中传递完整的证明,因此 canopy 将在此处提供帮助。
  • 通过调用 noop 程序记录事件。
  • 更新 canopy。 我们将看到的不同之处在于,我们没有传递 merkle_tree_append_leaf 作为 change_log_event,而是传递了 merkle_tree_fill_empty_or_append

2.2.6 replace_leaf

这是替换现有叶子的函数。

这是我们想要转移、销毁或对 NFT 执行其他操作时在 Bubblegum 中使用的函数。

  1. 账户
  • 与上面相同。
  1. 输入
        root: [u8; 32],
        previous_leaf: [u8; 32],
        new_leaf: [u8; 32],
        index: u32,
  1. 逻辑
                require_eq!(
                        *ctx.accounts.merkle_tree.owner,
                        crate::id(),
                        AccountCompressionError::IncorrectAccountOwner
                );
                let mut merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_mut_data()?;
                let (header_bytes, rest) =
                        merkle_tree_bytes.split_at_mut(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1);

                let header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
                header.assert_valid_authority(&ctx.accounts.authority.key())?;
                header.assert_valid_leaf_index(index)?;

                let merkle_tree_size = merkle_tree_get_size(&header)?;
                let (tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size);

                let mut proof = vec![];
                for node in ctx.remaining_accounts.iter() {
                        proof.push(node.key().to_bytes());
                }
                fill_in_proof_from_canopy(canopy_bytes, header.get_max_depth(), index, &mut proof)?;
                let id = ctx.accounts.merkle_tree.key();
                // 调用 ConcurrentMerkleTree::set_leaf(root, previous_leaf, new_leaf, proof, index)
                let args = &SetLeafArgs {
                        current_root: root,
                        previous_leaf,
                        new_leaf,
                        proof_vec: proof,
                        index,
                };
                let change_log_event = merkle_tree_set_leaf(&header, id, tree_bytes, args)?;

                update_canopy(
                        canopy_bytes,
                        header.get_max_depth(),
                        Some(&change_log_event),
                )?;
                wrap_event(
                        &AccountCompressionEvent::ChangeLog(*change_log_event),
                        &ctx.accounts.noop,
                )

与上面相同:

  • 它首先检查树是否归程序所有。
  • 验证权限。
  • 验证索引是否在范围内 leaf_index < (1 << self.get_max_depth())
  • 获取 tree_bytes 和 canopy_bytes。
  • 获取证明:证明将从 remaining_accounts 获得。
  • 使用 canopy 更新证明,正如我们所说,由于交易限制,我们无法在剩余账户中传递完整的证明,因此 canopy 将在此处提供帮助。
  • 通过调用 noop 程序记录事件。
  • 更新 canopy。 我们将看到的不同之处在于,我们没有传递 merkle_tree_append_leaf 作为 change_log_event,而是传递了 merkle_tree_set_leaf

2.2.7 verify_leaf

验证给定的叶子是否存在于 Merkle 树中。

  1. 账户
pub struct VerifyLeaf<'info> {
        /// CHECK: 此账户在指令中验证
        pub merkle_tree: UncheckedAccount<'info>,
}
  • Merkle 树账户,它必须由此程序拥有(将在逻辑中检查)。
  1. 输入
        root: [u8; 32],
        leaf: [u8; 32],
        index: u32,
  1. 逻辑
                require_eq!(
                        *ctx.accounts.merkle_tree.owner,
                        crate::id(),
                        AccountCompressionError::IncorrectAccountOwner
                );
                let merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_data()?;
                let (header_bytes, rest) =
                        merkle_tree_bytes.split_at(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1);

                let header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
                header.assert_valid()?;
                header.assert_valid_leaf_index(index)?;

                let merkle_tree_size = merkle_tree_get_size(&header)?;
                let (tree_bytes, canopy_bytes) = rest.split_at(merkle_tree_size);

                let mut proof = vec![];
                for node in ctx.remaining_accounts.iter() {
                        proof.push(node.key().to_bytes());
                }
                fill_in_proof_from_canopy(canopy_bytes, header.get_max_depth(), index, &mut proof)?;
                let id = ctx.accounts.merkle_tree.key();

                let args = &ProveLeafArgs {
                        current_root: root,
                        leaf,
                        proof_vec: proof,
                        index,
                };
                merkle_tree_prove_leaf(&header, id, tree_bytes, args)?;

                Ok(())
  • 它首先检查树是否归程序所有。
  • 验证标头。
  • 验证索引是否在范围内 leaf_index < (1 << self.get_max_depth())
  • 获取 tree_bytes 和 canopy_bytes。
  • 获取证明:证明将从 remaining_accounts 获得。
  • 使用 canopy 更新证明,正如我们所说,由于交易限制,我们无法在剩余账户中传递完整的证明,因此 canopy 将在此处提供帮助。
  • 验证叶子。

3. mpl-bubblegum

MPL-bubblegum 程序的源代码可在 GitHub 上找到。

该程序处理铸造、转移和销毁 cNFT 的逻辑。它与 account-compression 程序协同工作。

某些指令有两个版本(v1v2)。这篇文章涵盖了 v1

v1 和 v2 之间的区别:

  1. 使用 MPL Core 集合而不是 Token Metadata 集合。
  2. 使用精简的 MetadataV2 参数,它消除了 collection verified 标志。在 MetadataV2 中,任何包含的集合都自动被认为是已验证的。
  3. 允许在 MPL Core 集合上使用插件,例如 Royalties、Permanent Burn Delegate 等,以授权对 Bubblegum 资产的操作。请注意,BubblegumV2 插件也必须存在于 MPL Core 集合上,才能用于 Bubblegum。有关兼容的 集合级别插件的列表,请参见 MPL Core BubblegumV2 插件。
  4. 允许冻结/解冻资产,以及将资产设置为永久不可转让(soulbound)。不可转让类似于冻结,但允许所有者销毁资产,而冻结则不允许。
  5. 尚未可用,但可以选择指定与资产关联的数据(和模式)。

3.1 CreateTree

用于使用 account-compression 在链上创建一个新的 Merkle 树。

  1. 账户
     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>,
        /// CHECK: This account must be all zeros
        #[account(zero)]
        pub merkle_tree: UncheckedAccount<'info>,
        #[account(mut)]
        pub payer: Signer<'info>,
        pub tree_creator: Signer<'info>,
        pub log_wrapper: Program<'info, SplNoop>,
        pub compression_program: Program<'info, SplAccountCompression>,
        pub system_program: Program<'info, System>,
}
  • tree_authority 是一个由 merkle_tree 派生的 PDA。
  1. 输入
        max_depth: u32,
        max_buffer_size: u32,
        public: Option<bool>,
  • max_depth 和 max_buffer_size 如前所述。
  • public:是否允许任何人铸造新 token,或者只允许相应的 authority。
  1. 逻辑
        let merkle_tree = ctx.accounts.merkle_tree.to_account_info();

        check_canopy_size(
                ctx.accounts.merkle_tree.data.borrow(),
                ctx.accounts.tree_authority.key(),
                max_depth,
                max_buffer_size,
        )?;

        let seed = merkle_tree.key();
        let seeds = &[seed.as_ref(), &[ctx.bumps.tree_authority]];
        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,
                version: crate::state::leaf_schema::Version::V1,
        });

        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)
  • 基于 merkle_tree 和 header 检查 canopy size。
  • 基于 tree_creatortree_delegatetotal_mint_capacity 等初始化 TreeConfig。 tree_delegate 能够铸造新 token,tree_creator 也能。
  • 调用 spl_account_compression 并创建一个新的空 Merkle 树。

3.2 MintV1

将一个新的 cNFT 铸造到树中。

  1. 账户
##[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>,
        /// CHECK: This account is modified in the downstream program
        #[account(mut)]
        pub merkle_tree: UncheckedAccount<'info>,
        pub payer: Signer<'info>,
        pub tree_delegate: Signer<'info>,
        pub log_wrapper: Program<'info, SplNoop>,
        pub compression_program: Program<'info, SplAccountCompression>,
        pub system_program: Program<'info, System>,
}
  • tree_creator: 如果它不是 public,那么它必须是 tree_creator 或 tree_delegate。
  • 用于 CPI to account-compression 的 tree_authority,以及 TreeConfig 的数据是必需的。
  • leaf_ownerleaf_delegateleaf_delegate 也可以转移或销毁 token。
  1. 输入
message: MetadataArgs
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>,
        /// Token standard.  Currently only `NonFungible` is allowed.
        pub token_standard: Option<TokenStandard>,
        /// Collection
        pub collection: Option<Collection>,
        /// Uses
        pub uses: Option<Uses>,
        pub token_program_version: TokenProgramVersion,
        pub creators: Vec<Creator>,
}
  • 这是与 token 相关的元数据。
  1. 逻辑
        let payer = ctx.accounts.payer.key();
        let incoming_tree_delegate = ctx.accounts.tree_delegate.key();
        let merkle_tree = &ctx.accounts.merkle_tree;

        // V1 instructions only work with V1 trees.
        let authority = &mut ctx.accounts.tree_authority;
        require!(
                authority.version == Version::V1,
                BubblegumError::UnsupportedSchemaVersion
        );

        if !authority.is_public {
                require!(
                        incoming_tree_delegate == authority.tree_creator
                                || incoming_tree_delegate == authority.tree_delegate,
                        BubblegumError::TreeAuthorityIncorrect,
                );
        }

        if !authority.contains_mint_capacity(1) {
                return Err(BubblegumError::InsufficientMintCapacity.into());
        }

        // 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()),
        );

        let leaf = process_mint(
                message,
                &ctx.accounts.leaf_owner,
                Some(&ctx.accounts.leaf_delegate),
                metadata_auth,
                ctx.bumps.tree_authority,
                authority,
                merkle_tree,
                &ctx.accounts.log_wrapper,
                &ctx.accounts.compression_program,
                false,
        )?;

        authority.increment_mint_count();

        Ok(leaf)
  • 检查版本。
  • 如果不是 public,则检查 authority。
  • 检查铸造容量。
  • 做一些元数据相关的事情。
  • 调用 process_mint(重要部分)。
  • 递增铸造的 token 数量。
  • process_mint
        let asset_id = get_asset_id(&merkle_tree.key(), tree_authority.num_minted);
        solana_program::msg!("Leaf asset ID: {}", asset_id);

        let leaf_delegate = leaf_delegate.unwrap_or(leaf_owner);
        let version = message.version();
        let leaf = match version {
                Version::V1 => LeafSchema::new_v1(
                        asset_id,
                        leaf_owner.key(),
                        leaf_delegate.key(),
                        tree_authority.num_minted,
                        data_hash.to_bytes(),
                        creator_hash.to_bytes(),
                ),
                Version::V2 => {
                        let collection_hash = hash_collection_option(message.collection_key())?;

                        LeafSchema::new_v2(
                                asset_id,
                                leaf_owner.key(),
                                leaf_delegate.key(),
                                tree_authority.num_minted,
                                data_hash.to_bytes(),
                                creator_hash.to_bytes(),
                                collection_hash,
                                DEFAULT_ASSET_DATA_HASH,
                                DEFAULT_FLAGS,
                        )
                }
        };

        crate::utils::wrap_application_data_v1(version, leaf.to_event().try_to_vec()?, wrapper)?;

        append_leaf(
                version,
                &merkle_tree.key(),
                authority_bump,
                &compression_program.to_account_info(),
                &tree_authority.to_account_info(),
                &merkle_tree.to_account_info(),
                &wrapper.to_account_info(),
                leaf.to_node(),
        )?;

由于每个 NFT 必须具有唯一的 ID,因此它基于 Merkle 树和 token 的索引来计算 asset_id。请注意,每次我们铸造一个新 token 时,num_minted 都会增加,并且在销毁过程中不会减少。

  • 基于版本创建一个 leaf,然后将该 leaf 附加到 Merkle 树。
优化 NFT 权利验证

正如我们所见,链上没有创建新账户。以前,我们需要两个账户来拥有一个 NFT。 那么 Bubblegum 是如何做到这一点并检查 cNFT 所有者和其他数据的呢?

每当铸造新的 CNFT 时,不会创建新账户;我们只需根据需要的数据计算 leaf,然后插入 leaf。

每当用户想要转移或对其 NFT 执行操作时,此处不进行验证,并且正如所见,account-compression 也不进行验证,唯一完成的检查是 leaf 必须存在于树中,因此 Bubblegum 巧妙地实现了这一点,不是从用户那里获取 leaf 并对所有者进行验证,而是每次都计算 leaf,因此如果有人想要转移其他用户的 token,则 leaf 将被错误地计算,因此只有当正确的 authority 调用转移时,才会计算出正确的 leaf。以 Burn 指令为例进行检查。

3.3 Burn

通过将其 Merkle leaf 替换为一个空的 leaf 来销毁 cNFT。

所有者或 delegate 必须签署交易。

  1. 账户
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>,
        /// CHECK: This account is modified in the downstream program
        #[account(mut)]
        pub merkle_tree: UncheckedAccount<'info>,
        pub log_wrapper: Program<'info, SplNoop>,
        pub compression_program: Program<'info, SplAccountCompression>,
        pub system_program: Program<'info, System>,
}
  • 正如我们所说,leaf 将被再次计算,因此我们需要 merkle_treeleaf_ownerleaf_delegate(其中之一必须是签署者)。
  1. 输入
        root: [u8; 32],
        data_hash: [u8; 32],
        creator_hash: [u8; 32],
        nonce: u64,
        index: u32,
  • 用于验证的 Root,用于 leaf 计算的 data_hash、creator_hash、nonce 和 index,nonce 是 leaf 的索引。
  1. 逻辑
        // V1 instructions only work with V1 trees.
        require!(
                ctx.accounts.tree_authority.version == Version::V1,
                BubblegumError::UnsupportedSchemaVersion
        );

        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
        );
        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_v1(
                asset_id,
                owner.key(),
                delegate.key(),
                nonce,
                data_hash,
                creator_hash,
        );

        let new_leaf = Node::default();

        replace_leaf(
                Version::V1,
                &merkle_tree.key(),
                ctx.bumps.tree_authority,
                &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,
        )
  • 检查版本。
  • 检查所有者或 delegate 是否必须是签署者(leaf 将基于这些计算)。
  • 获取资产 ID。
  • 计算 previous_leaf
  • 在 account compression 上调用 replace_leaf。

3.4 Transfer

类似于 burn,但替换为分配给新所有者的新 leaf。

快速浏览:

  • 它将获取 new_leaf_owner 账户。
  • 进行一些验证
  • 计算 previous_leaf 和 new_leaf
  • 调用 replace_leaf

3.5 Redeem

cNFT 能够从 Merkle 树中赎回并存在于链上。为此,NFT 必须先被赎回,然后再被解压缩。

准备将 cNFT 从压缩的 Merkle 树转移到链上 NFT。

  1. 账户
pub struct Redeem<'info> {
        #[account(\
                seeds = [merkle_tree.key().as_ref()],\
                bump,\
        )]
        pub tree_authority: Account<'info, TreeConfig>,
        #[account(mut)]
        pub leaf_owner: Signer<'info>,
        /// CHECK: This account is checked in the instruction
        pub leaf_delegate: UncheckedAccount<'info>,
        /// CHECK: This account is modified in the downstream program
        #[account(mut)]
        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, SplNoop>,
        pub compression_program: Program<'info, SplAccountCompression>,
        pub system_program: Program<'info, System>,
}
  • 只有所有者可以赎回 NFT,因此与之前的不同,leaf_owner 必须是签署者。
  • voucher:这是一个账户,当初始化时,意味着 NFT 处于赎回状态,可以被取消或解压缩。
  1. 输入
        root: [u8; 32],
        data_hash: [u8; 32],
        creator_hash: [u8; 32],
        nonce: u64,
        index: u32,
  • 用于计算 leaf 的 Root 和 data。
  1. 逻辑
        // V1 instructions only work with V1 trees.
        require!(
                ctx.accounts.tree_authority.version == Version::V1,
                BubblegumError::UnsupportedSchemaVersion
        );

        if ctx.accounts.tree_authority.is_decompressible == DecompressibleState::Disabled {
                return Err(BubblegumError::DecompressionDisabled.into());
        }

        let leaf_owner = ctx.accounts.leaf_owner.key();
        let leaf_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_v1(
                asset_id,
                leaf_owner,
                leaf_delegate,
                nonce,
                data_hash,
                creator_hash,
        );

        let new_leaf = Node::default();

        replace_leaf(
                Version::V1,
                &merkle_tree.key(),
                ctx.bumps.tree_authority,
                &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,
        )?;
        ctx.accounts
                .voucher
                .set_inner(Voucher::new(previous_leaf, index, merkle_tree.key()));

        Ok(())
  • 与之前相同:进行一些验证,计算 leaf,并将其替换为 Node::default()
  • is_decompressible 进行额外检查,以确保它能够解压缩。
  • 使用 previous_leaf 数据初始化凭证。
pub struct Voucher {
        pub leaf_schema: LeafSchema,
        pub index: u32,
        pub merkle_tree: Pubkey,
}

3.6 CancelRedeem

取消赎回操作,因此与之前相同;它仅关闭凭证并将 leaf 替换为凭证上的 leaf。

3.7 DecompressV1

这是必须调用的指令,用于将 cNFT 从链下(Merkle 树上的 leaf)转移到链上账户。它必须在调用 redeem 之后调用。

  1. 账户
##[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, SplNoop>,
        }
  • voucher:先前初始化的 voucher 账户,用于验证解压缩。它将在此指令结束时关闭,以回收租金。
  • leaf_owner:必须是签署者。表示压缩资产的合法所有者。注意:leaf_delegate 授权执行此操作。
  • token_accountleaf_ownermint 的关联 token 账户。如果它不存在,将被创建。
  • mint:从 Merkle 树和资产的 nonce 派生的 PDA。用作解压缩 NFT 的唯一链上 mint。
  • mint_authority:用于签署铸造和元数据指令的 PDA。暂时分配给你的程序,然后重新分配给系统程序以锁定它。
  • metadata:通过 Token Metadata 程序创建的元数据账户。存储链上 NFT 数据,例如名称、符号、URI、创建者和集合信息。
  • master_edition:NFT 的 Master Edition 账户,表明它是一种非同质化资产(1-of-1 或限量版)。大多数市场都需要它来验证资产的唯一性和元数据。
  • 需要的程序。
  1. 输入
        metadata: MetadataArgs
  • 仅需要的元数据。
  1. 逻辑

逻辑很简单,但相当长。

    // Validate the incoming metadata
    let incoming_data_hash = hash_metadata(&metadata)?;
    if !cmp_bytes(
        &ctx.accounts.voucher.leaf_schema.data_hash(),
        &incoming_data_hash,
        32,
    ) {
        return Err(BubblegumError::HashingMismatch.into());
    }
    if !cmp_pubkeys(
        &ctx.accounts.voucher.leaf_schema.owner(),
        ctx.accounts.leaf_owner.key,
    ) {
        return Err(BubblegumError::AssetOwnerMismatch.into());
    }

    let voucher = &ctx.accounts.voucher;
    match metadata.token_program_version {
        TokenProgramVersion::Original => {
            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.mint],\
                    ]],
                )?;
                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(),\
                    ],
                )?;
            }
            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(),\
                    ],
                )?;
            }
            // 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()),
    }
    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],\
        ]])?;

    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],\
        ]])?;

    ctx.accounts
        .mint_authority
        .to_account_info()
        .assign(&System::id());

    Ok(())
  • 验证元数据。
  • 验证所有者。
  • 仅允许正常的 token 程序(不是 2022)。
  • 如果 mint 账户为空,则创建该账户,并使用 mint_authority 作为 authority 进行初始化。
  • 为 leaf 所有者创建一个 ATA 并铸造 token。
  • mint_authority 分配给 crate::id(),这是创建元数据所必需的。与 SPL 程序不同,它将检查此 PDA 的所有者,因此需要此步骤。
  • 创建元数据。
  • mint_authority 分配回 System::id()

3.8 SetDecompressibleState

这是一个用于更改 tree.is_decompressible 状态的小指令(默认情况下为 false)。

正如你所记得的,这个变量在赎回过程中被检查。

##[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<()> {
        // V1 instructions only work with V1 trees.
        require!(
                ctx.accounts.tree_authority.version == Version::V1,
                BubblegumError::UnsupportedSchemaVersion
        );

        ctx.accounts.tree_authority.is_decompressible = decompressable_state;

        Ok(())
        }

3.9 MintToCollectionV1

该指令用于将新的压缩 NFT (cNFT) 铸造到 Merkle 树中,并将其分配给经过验证的集合。它仅支持 Bubblegum V1 Merkle 树,并使用 Token Metadata 程序验证集合。

  1. 账户
##[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>,
    /// CHECK: This account is modified in the downstream program
    #[account(mut)]
    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 not present, must be the Bubblegum program address.
    pub collection_authority_record_pda: UncheckedAccount<'info>,
    /// CHECK: Checked inside the downstream logic
    pub collection_mint: UncheckedAccount<'info>,
    #[account(mut)]
    pub collection_metadata: Box<Account<'info, TokenMetadata>>,
    /// CHECK: Checked inside the downstream logic
    pub edition_account: UncheckedAccount<'info>,
    /// CHECK: Legacy; not used but kept for compatibility
    pub bubblegum_signer: UncheckedAccount<'info>,
    pub log_wrapper: Program<'info, Noop>,
    pub compression_program: Program<'info, SplAccountCompression>,
    /// CHECK: Legacy; not used but kept for compatibility
    pub token_metadata_program: UncheckedAccount<'info>,
    pub system_program: Program<'info, System>,
}
  • tree_authority:Merkle 树的 authority。包含权限和铸造容量。
  • leaf_ownerleaf_delegate:压缩 NFT 的未来所有者。
  • merkle_tree:存储压缩 NFT leaf 的 Merkle 树账户。
  • payer:支付任何指令开销。如果包含在元数据中,也算作创建者 authority。
  • tree_delegate:授权铸造到 Merkle 树的签署者,必须与 tree_delegatetree_creator 匹配。
  • collection_authority:验证集合的签署者(必须获得集合的批准)。
  • collection_authority_record_pda:可选的 PDA,证明对集合的委托 authority。
  • collection_mint:经过验证的集合的 mint 账户。
  • collection_metadata:保存链上集合元数据的账户。
  • edition_account:用于验证集合的版本账户。
  • 需要的程序。
  1. 输入
metadata_args: MetadataArgs
  • 需要的元数据。
  1. 逻辑

该逻辑执行多个验证并处理铸造:

 // V1 instructions only work with V1 trees.
    require!(
        ctx.accounts.tree_authority.version == Version::V1,
        BubblegumError::UnsupportedSchemaVersion
    );

    let mut message = metadata_args;
    let payer = ctx.accounts.payer.key();
    let incoming_tree_delegate = ctx.accounts.tree_delegate.key();
    let authority = &mut ctx.accounts.tree_authority;```
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();

    if !authority.is_public {
        require!(
            incoming_tree_delegate == authority.tree_creator
                || incoming_tree_delegate == authority.tree_delegate,
            BubblegumError::TreeAuthorityIncorrect,
        );
    }

    if !authority.contains_mint_capacity(1) {
        return Err(BubblegumError::InsufficientMintCapacity.into());
    }

    // Create a HashSet to store signers to use with creator validation.  Any signer can be
    // counted as a validated creator.
    // 创建一个 HashSet 来存储签名者,以便与创建者验证一起使用。任何签名者都可以被算作已验证的创建者。
    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()),
    );

    let collection = message
        .collection
        .as_mut()
        .ok_or(BubblegumError::CollectionNotFound)?;

    collection.verified = true;

    process_collection_verification_mpl_only(
        collection_metadata,
        &collection_mint,
        &collection_authority,
        &collection_authority_record_pda,
        &edition_account,
        collection,
    )?;

    let leaf = process_mint(
        message,
        &ctx.accounts.leaf_owner,
        Some(&ctx.accounts.leaf_delegate),
        metadata_auth,
        ctx.bumps.tree_authority,
        authority,
        merkle_tree,
        &ctx.accounts.log_wrapper,
        &ctx.accounts.compression_program,
        true,
    )?;

    authority.increment_mint_count();

    Ok(leaf)
  • Validates tree version.
  • Checks mint permissions, if it's public or not.
  • Checks that the collection is present in the metadata and sets it to verified.
  • Uses the Token Metadata program logic to verify the collection.
  • Processes the mint as before.

3.10 Delegate

This is the process that you could delegate your cNFT. 这是一个你可以委托你的 cNFT 的过程。

Setting a new leaf_delegate that can transfer or burn the nft. 设置一个新的 leaf_delegate,它可以转移或销毁 NFT。

  1. Accounts
##[derive(Accounts)]
pub struct Delegate<'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>,
        pub leaf_owner: Signer<'info>,
        /// CHECK: This account is neither written to nor read from.
        /// 检查:这个账户既没有被写入也没有被读取。
        pub previous_leaf_delegate: UncheckedAccount<'info>,
        /// CHECK: This account is neither written to nor read from.
        /// 检查:这个账户既没有被写入也没有被读取。
        pub new_leaf_delegate: UncheckedAccount<'info>,
        /// CHECK: This account is modified in the downstream program
        /// 检查:这个账户在下游程序中被修改
        #[account(mut)]
        pub merkle_tree: UncheckedAccount<'info>,
        pub log_wrapper: Program<'info, SplNoop>,
        pub compression_program: Program<'info, SplAccountCompression>,
        pub system_program: Program<'info, System>,
        }
  • leaf_owner that must be signer(only the owner is able to delegate not delagator).
  • leaf_owner 必须是签名者(只有所有者能够委托,而不是委托者)。
  1. Inputs
    root: [u8; 32],
    data_hash: [u8; 32],
    creator_hash: [u8; 32],
    nonce: u64,
    index: u32,

data needed for the calculation of the leaf. 计算 leaf 所需的数据。

  1. Logic
    // V1 instructions only work with V1 trees.
    // V1 指令仅适用于 V1 树。
    require!(
        ctx.accounts.tree_authority.version == Version::V1,
        BubblegumError::UnsupportedSchemaVersion
    );

    let merkle_tree = ctx.accounts.merkle_tree.to_account_info();
    let owner = ctx.accounts.leaf_owner.key();
    let previous_delegate = ctx.accounts.previous_leaf_delegate.key();
    let new_delegate = ctx.accounts.new_leaf_delegate.key();
    let asset_id = get_asset_id(&merkle_tree.key(), nonce);
    let previous_leaf = LeafSchema::new_v1(
        asset_id,
        owner,
        previous_delegate,
        nonce,
        data_hash,
        creator_hash,
    );
    let new_leaf = LeafSchema::new_v1(
        asset_id,
        owner,
        new_delegate,
        nonce,
        data_hash,
        creator_hash,
    );

    crate::utils::wrap_application_data_v1(
        Version::V1,
        new_leaf.to_event().try_to_vec()?,
        &ctx.accounts.log_wrapper,
    )?;

    replace_leaf(
        Version::V1,
        &merkle_tree.key(),
        ctx.bumps.tree_authority,
        &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,
    )
  • Validates tree version.
  • Calculate the previous leaf.
  • Calculate the new leaf; the only difference is new_delegate.
  • Checks that the collection is present in the metadata and sets it to verified.
  • Call replace leaf on account-compression.

3.11 SetTreeDelegate

Set a new tree_delegate for a tree; tree_delegate can mint new NFTs. 为一个树设置一个新的 tree_delegatetree_delegate 可以铸造新的 NFT。

##[derive(Accounts)]
pub struct SetTreeDelegate<'info> {
    #[account(\
        mut,\
        seeds = [merkle_tree.key().as_ref()],\
        bump,\
        has_one = tree_creator\
    )]
    pub tree_authority: Account<'info, TreeConfig>,
    pub tree_creator: Signer<'info>,
    /// CHECK: this account is neither read from or written to
    /// 检查:此账户既未读取也未写入
    pub new_tree_delegate: UncheckedAccount<'info>,
    /// CHECK: Used to derive `tree_authority`
    /// 检查:用于派生 `tree_authority`
    pub merkle_tree: UncheckedAccount<'info>,
    pub system_program: Program<'info, System>,
}

pub(crate) fn set_tree_delegate(ctx: Context<SetTreeDelegate>) -> Result<()> {
    ctx.accounts.tree_authority.tree_delegate = ctx.accounts.new_tree_delegate.key();
    Ok(())
}

Conclusion

Compressed NFTs (cNFTs) offer a scalable, cost-effective way to manage large volumes of NFTs on Solana, especially for use cases like gaming, collectables, and ticketing. By leveraging Merkle trees and off-chain data storage, developers can drastically reduce the number of on-chain accounts, saving on fees and improving efficiency. (For specific compute costs of Merkle proof verification and other operations, see our cost of security analysis.) 压缩 NFT (cNFT) 提供了一种可扩展、经济高效的方式来管理 Solana 上大量的 NFT,尤其适用于游戏、收藏品和票务等用例。通过利用 Merkle 树和链下数据存储,开发人员可以大大减少链上账户的数量,从而节省费用并提高效率。(有关 Merkle 证明验证和其他操作的具体计算成本,请参阅我们的安全成本分析。)

The combination of account-compression and mpl-bubblegum provides a powerful toolkit for creating, managing, and interacting with cNFTs, without compromising on validation or ownership security. 账户压缩和 mpl-bubblegum 的结合提供了一个强大的工具包,用于创建、管理和交互 cNFT,而不会影响验证或所有权安全。

Whether you're building a high-throughput dApp or auditing an existing system, understanding how these programs work under the hood gives you the edge to optimise and innovate. 无论你是构建高吞吐量的 dApp 还是审计现有系统,了解这些程序在底层是如何工作的,都能让你在优化和创新方面取得优势。

If you’ve made it this far, you now have a solid grasp of how cNFTs work on Solana. The future of NFTs is not just about art, it’s about scale. And cNFTs make that scale possible. 如果你已经走到了这一步,那么你现在已经扎实地掌握了 cNFT 在 Solana 上的工作原理。NFT 的未来不仅仅是关于艺术,而是关于规模。而 cNFT 使这种规模成为可能。

  • 原文链接: accretion.xyz/blog/compr...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
accretionxyz
accretionxyz
Solana Audits | Solana Security & Research | Looking for an audit?