Skip to content

状态压缩

Solana 状态压缩是一种在链上存储链下数据指纹(哈希)并用于安全验证的方法。有效利用 Solana 账本的安全性来验证链下数据的完整性,确保其未被篡改。

通过状态压缩, Solana 程序和 dApp 能够以较低的成本使用链上存储空间来安全存储数据,而不是昂贵的账户空间。

这种压缩方法采用了一种特殊的二叉树结构(称为并发默克尔树)来创建每条数据(称为叶子)的哈希值,并将这些哈希值相互连接,最终只将根哈希值存储在链上。

什么是状态压缩?

状态压缩使用树形结构以确定性方式将链下数据加密地散列在一起,以计算最终的链上哈希值。

这些树是通过以下过程创建的:

  • 获取任何数据
  • 创建此数据的哈希值
  • 将此哈希值存储为树底部的叶子 leaf
  • 然后将每个叶子对散列在一起,创建分支 branch节点
  • 然后将每个分支节点哈希在一起
  • 并将相邻的分支哈希在一起
  • 不断重复此过程,直到到达树的顶部,得到最终的根哈希 root hash

然后,将该根哈希存储在链上,作为链下数据完整性的验证证明。这使得任何人都能以加密方式验证所有链下数据的完整性,同时实际上只在链上存储了最少量的数据。因此,通过状态压缩,大大降低了存储和验证大量数据的成本。

默克尔树和并发默克尔树

Solana 的状态压缩使用了一种特殊类型的默克尔树,它允许对树进行多次更改,同时仍然保持树的完整性和有效性。

这种特殊的树,被称为“并发默克尔树”,有效地在链上保留了树的“变更日志”。允许对同一棵树进行多次快速更改(即所有更改都在同一块中), 直到证明失效。

什么是默克尔树?

Merkle 树,有时称为“哈希树”,是一种基于哈希的二叉树结构,其中每个叶节点都表示为其内部数据的加密哈希。每个非叶子节点(称为分支)都表示为其子叶子哈希的哈希。

然后,每个分支也被散列在一起,一直到树的根节点,最终只剩下一个散列。这个最终的哈希值称为根哈希值或“根”,然后可以与“证明路径”结合使用来验证叶节点中存储的任何数据。

通过重新哈希特定叶子的数据并沿着树的每个相邻分支散列(称为证明路径)来验证叶节点中存储的任何数据。将此“重新散列”与根散列进行比较是对底层叶数据的验证。如果它们匹配,则数据被验证为准确。如果不匹配,则叶数据已更改。

无论何时需要,都可以通过简单地散列新叶数据并以与原始根相同的方式重新计算根散列来改变原始叶数据。然后使用这个新的根哈希来验证任何数据,并有效地使先前的根哈希和先前的证明无效。因此,对这些传统 merkle 树的每次改变都需要串行执行。

INFO

使用 Merkle 树时,更改叶数据和计算新的根哈希的过程可能非常常见!虽然这是树的设计点之一,但它可能导致最明显的缺点之一:快速变化。

什么是并发默克尔树?

在 Solana 上,验证者可以在同一槽内接收到多个对传统 Merkle 树的更改请求。然而,每个叶数据更改仍然需要串行执行。这样的串行执行会导致在相同槽内对同一棵树进行多次更改的请求失败,因为前一个更改会使根哈希和证明无效。

输入并发默克尔树。

并发默克尔树解决了这个问题,它存储了最近的更改日志、根哈希以及生成该根哈希的证明。这个变更日志缓冲区存储在链上特定于每棵树的账户中,其大小由最大数量的变更日志记录(也称为 maxBufferSize)确定。

当验证者在同一槽内接收到多个叶数据更改请求时,链上的并发默克尔树可以使用这个“变更日志缓冲区”作为更可接受的证明来源。这有效地允许在同一槽内对同一棵树进行最多 maxBufferSize 次的更改,从而显著提高了吞吐量。

调整并发默克尔树的大小

在创建并发默克尔树时,有 3 个值将决定树的大小、创建树的成本以及树的并发更改数量:

  1. max depth 最大深度
  2. max buffer size 最大缓冲区大小
  3. canopy depth 冠层深度

最大深度

树的“最大深度”是从任何数据叶到树根的最大跳数。

由于默克尔树是二叉树,因此每个叶子仅与另一个叶子相连;作为一对叶存在。

因此,树的 maxDepth 用于通过简单的计算来确定要在树中存储的节点(也称为数据块或叶子)的最大数量:

nodes_count = 2 ^ maxDepth

由于必须在创建树时设置树深度,因此必须决定希望树存储多少数据。然后使用上面的简单计算,确定存储数据的最低 maxDepth

示例 1:铸造 100 nfts

如果想创建一棵树来存储 100 个压缩的 nft,至少需要“100 个叶子”或“100 个节点”。

js
// maxDepth=6 -> 64 nodes2^6 = 64
// maxDepth=7 -> 128 nodes2^7 = 128

必须使用 maxDepth 7 以确保我们可以存储所有数据。

示例 2:铸造 15000 nfts

如果想创建一棵树来存储 15000 个压缩的 nft,至少需要“15000 个叶子”或“15000 个节点”。

js
// maxDepth=13 -> 8192 nodes2^13 = 8192
// maxDepth=14 -> 16384 nodes2^14 = 16384

必须使用 maxDepth 14 以确保可以存储所有数据。

最大深度越高,成本越高

最大深度是创建并发默克尔树时的一个关键因素,因为它直接影响到树的存储能力和成本。较高的最大深度意味着树可以存储更多的数据指纹(或哈希),但也会增加创建树的成本。

最大缓冲区大小

最大缓冲区大小确定了树上可以发生的最大更改数量,而不会使根哈希无效。

由于根哈希实际上是所有叶子数据的单个哈希,因此更改任何单个叶子都会使更改常规树的任何叶子的所有后续尝试所需的证明无效。

但对于并发树,实际上存在这些证明的更新的变更日志。该变更日志缓冲区的大小是在树创建时通过 maxBufferSize 值进行调整和设置的。

冠层深度

冠层深度(或树冠大小)是指在链上缓存或存储的证明节点的数量。

当对叶子执行更新操作时,例如转让所有权(例如出售压缩的 NFT),必须使用完整的证明路径来验证叶子的原始所有权,从而允许更新操作。此验证是使用完整的证明路径来正确计算当前根哈希(或通过链上“并发缓冲区”的任何缓存根哈希)来执行的。

树的最大深度越大,执行此验证所需的证明节点就越多。例如,如果最大深度为 14,则总共需要使用 14 个证明节点来进行验证。随着树变得越来越大,完整的证明路径也变得越来越大。

通常,每个证明节点都需要包含在每个树更新交易中。由于每个证明节点值在事务中占用 32 个字节(类似于提供公钥),因此较大的树将很快超过最大事务大小限制。

进入 canopy。 Canopy 能够在链上存储一定数量的证明节点(对于任何给定的证明路径)。允许在每个更新交易中包含较少的证明节点,从而使总体交易大小保持在限制以下。

例如,最大深度为 14 的树总共需要 14 个证明节点。如果 canopy 为 10,则每个更新交易只需要提交 4 个证明节点。

冠层深度值越大,成本越高

canopyDepth 值也是创建树时的主要成本因素,因为将在创建树时预先支付此成本。 Canopy 深度越高,链上存储的数据证明节点越多,成本越高。

较小的冠层限制了可组合性

尽管树的冠层越高,创建成本就越高,但较小的冠层深度意味着每个更新事务需要包含更多的证明节点。随着需要提交的节点数量增加,交易规模也会增大,因此更容易超出交易规模限制。

这不仅对树和叶子交互的 Solana 程序或 dApp 产生影响,对于任何试图与树/叶子进行交互的其他 Solana 程序或 dApp 也是如此。如果树需要太多的证明节点(由于较小的冠层深度),那么其他链上程序可以提供的任何额外操作都将受到指令大小以及证明节点列表大小的限制。这限制了可组合性,并可能限制了特定树的附加实用性。

例如,如果树用于压缩 NFT 并且树冠深度非常低,则 NFT 市场可能只能支持简单的 NFT 传输。并且无法支持链上竞价系统或其他复杂功能。

创建一棵树的成本

创建并发默克尔树的成本基于树的大小参数:maxDepthmaxBufferSizecanopyDepth。这些值都用于计算树存在于链上所需的链上存储(以字节为单位)。

一旦计算出所需的空间(以字节为单位),并使用 getMinimumBalanceForRentExemption RPC 方法,请求在链上分配此字节数的成本(以 lamports 为单位)。

在 JavaScript 中计算树成本

@solana/spl-account-compression 包中,开发人员可以使用 getConcurrentMerkleTreeAccountSize 函数来计算给定树大小参数所需的空间。

然后使用 getMinimumBalanceForRentExemption 函数获取为链上树分配所需空间的最终成本(以 lamports 为单位)。

然后确定 lamports 的成本,以使此规模的帐户免租金,类似于任何其他帐户创建。

js
// 计算树空间
const requiredSpace = getConcurrentMerkleTreeAccountSize(
  maxDepth,
  maxBufferSize,
  canopyDepth
);

// 获取链上存储花费
const storageCost = await connection.getMinimumBalanceForRentExemption(
  requiredSpace
);

成本示例

下面列出了不同树大小的几个示例成本,包括每个树可能有多少个叶节点:

示例 #1:16,384 个节点,成本为 0.222 SOL

  • 最大深度为 14,最大缓冲区大小为 64
  • 最大叶节点数:16,384
  • 创建树冠深度为 0 的成本约为 0.222 SOL

示例 #2:16,384 个节点,成本 1.134 SOL

  • 最大深度为 14,最大缓冲区大小为 64
  • 最大叶节点数:16,384
  • 树冠深度 11 的创建成本约为 1.134 SOL

示例 #3:1,048,576 个节点,花费 1.673 SOL

  • 最大深度为 20,最大缓冲区大小为 256
  • 最大叶节点数:1,048,576
  • 创建深度为 10 的树冠大约需要 1.673 SOL

示例 #4:1,048,576 个节点,花费 15.814 SOL

  • 最大深度为 20,最大缓冲区大小为 256
  • 最大叶节点数:1,048,576
  • 树冠深度 15 的创建成本约为 15.814 SOL

压缩 NFT

压缩 NFT 是 Solana 上状态压缩最流行的用例之一。通过压缩,一百万个 NFT 集合的铸造成本约为 50 SOL,而未压缩的等效集合则约为 12,000 SOL。

如果有兴趣自己创建压缩 NFT,请阅读我们有关 铸造和传输压缩 NFT 的开发人员指南。