在Solana
上,状态压缩是一种创建离链数据的“指纹”(或哈希)并将该指纹存储在链上以进行安全验证的方法。有效地利用Solana
账本的安全性来安全验证离链数据,以确保其未被篡改。
这种“压缩”方法使得Solana的程序和dApps能够使用廉价的区块链账本空间来安全存储数据,而不是更昂贵的账户空间。
这是通过使用一种特殊的二叉树结构,称为并发默克尔树,对每个数据片段(称为 leaf
)创建哈希,将它们哈希在一起,并仅将最终哈希存储在链上来实现的。
什么是状态压缩?
简单来说,状态压缩使用“树”结构将链外数据以确定性的方式进行加密哈希,计算出一个最终的哈希值,并将其存储在链上。
这些树是通过这个“确定性”过程创建的:
- 获取任何数据
- 创建这些数据的哈希值
- 将此哈希值存储为树底部的
leaf
- 每个
leaf
对都会被一起哈希,创建一个 branch
- 每个
branch
然后一起哈希 - 不断攀爬树木并将相邻的树枝连接在一起
- 树顶上一旦到达,就会产生最后的
root hash
这个 root hash
然后存储在链上,作为每个叶子节点中所有数据的可验证证据。这样任何人都可以通过加密验证树中所有离链数据,而实际上只需在链上存储少量数据。因此,由于这种"状态压缩",大大降低了存储/证明大量数据的成本。
默克尔树和并发默克尔树
Solana
的状态压缩使用了一种特殊类型的默克尔树,允许对任何给定的树进行多次更改,同时仍然保持树的完整性和有效性。
这棵特殊的树被称为“并发默克尔树”,有效地在链上保留了树的“更改日志”。允许在一个证明失效之前对同一棵树进行多次快速更改(即在同一个区块中)。
默克尔树是什么?
默克尔树,有时也被称为“哈希树”,是一种基于哈希的二叉树结构,其中每个leaf
节点都被表示为其内部数据的加密哈希。而每个非叶节点,也被称为“branch
节点”,则被表示为其子叶节点哈希的哈希值。
每个分支也被哈希在一起,沿着树向上爬,直到最后只剩下一个哈希。这个最终的哈希,称为 root hash
或者"根",可以与一个"证明路径"结合使用,来验证存储在叶节点中的任何数据。
一旦计算出最终的根哈希值(root hash
),可以通过重新计算特定叶子(leaf
)节点的数据和每个相邻分支的哈希标签(称为“证明路径”)来验证存储在节点中的任何数据。将这个“重新哈希”与根哈希值进行比较,可以验证底层叶子数据的准确性。如果它们匹配,数据就被验证为准确的。如果它们不匹配,叶子数据已被更改。
只要需要,原始叶子数据可以通过对新的叶子数据进行哈希运算并重新计算根哈希值来进行更改,方法与原始根哈希值的计算方式相同。然后,这个新的根哈希值用于验证任何数据,并且有效地使之前的根哈希值和证明无效。因此,对这些传统的默克尔树的每一次更改都需要按顺序执行。
当使用默克尔树时,更改叶子数据并计算新的根哈希的过程可能是非常常见的事情!虽然这是树的设计要点之一,但它可能导致最显著的缺点之一:快速变化。
什么是并发默克尔树?
在高吞吐量的应用中,比如在Solana
运行时中,对于链上传统Merkle树的更改请求可能会相对快速地连续接收到验证者(例如在同一个槽中)。每个叶子数据的更改仍然需要按顺序执行。这导致每个后续的更改请求都会失败,因为根哈希和证明已经被同一槽中之前的更改请求无效化了。
进入,并发默克尔树。
并发默克尔树存储了最近更改的安全日志、它们的根哈希以及用于推导根哈希的证明。这个日志缓冲区存储在链上的每个树对应的特定账户中,最大记录数为(也称为 maxBufferSize
)。
当同一时隙内的验证者收到多个叶子数据变更请求时,链上并发 Merkle
树可以将这个“变更日志缓冲区”作为更可接受的证明的真实来源。有效地允许在同一时隙内对同一棵树进行多达 maxBufferSize
次变更。大幅提升吞吐量。
并发默克尔树的大小调整
创建这种链上树时,有三个值将决定您的树的大小、创建树的成本以及对树的并发更改数量:
max depth
最大深度max buffer size
最大缓冲区大小canopy depth
max depth
树的“最大深度”是从任何数据 leaf
到树的 root
所需的最大跳数。
由于默克尔树是二叉树,每个叶子节点只与另一个叶子节点相连;存在于一个 leaf pair
中。
因此,树的 maxDepth
被用来确定可以通过简单的计算存储在树中的最大节点数(也称为数据或 leafs
)
nodes_count = 2 ^ maxDepth
由于树的深度必须在创建树时设置,您必须决定您希望树存储多少个数据。然后使用上述简单的计算,您可以确定存储数据的最低 maxDepth
。
示例1:铸造100个NFTs
如果你想创建一个用于存储100
个压缩NFT的树,我们至少需要"100
个叶子"或"100
个节点"。
// maxDepth=6 -> 64 nodes
2^6 = 64
// maxDepth=7 -> 128 nodes
2^7 = 128
因此,我们需要一个最大深度为 7
的树,以存储 100
个数据。
例子2:铸造15000个NFTs
如果你想创建一个用于存储15000
个压缩NFT的树,我们将需要至少"15000
个叶子"或"15000
个节点"。
// maxDepth=13 -> 8192 nodes
2^13 = 8192
// maxDepth=14 -> 16384 nodes
2^14 = 16384
因此,我们需要一个最大深度为 14
的树,以存储 15000
个数据。
最大深度越高,成本越高
创建树时, maxDepth
值将是成本的主要驱动因素之一,因为您将在创建树时支付这笔成本。最大树深度越高,您可以存储的数据指纹(也称为哈希)越多,成本就越高。
max buffer size
“max buffer size
” 实际上是树上可以发生的最大变化数量,同时仍然有效的 root hash
。
由于根哈希有效地是所有叶子数据的单一哈希,改变任何一个叶子将使得所有后续尝试改变常规树的叶子所需的证明无效。
但是使用并发树,对于这些证明来说,实际上有一个更新的日志。这个日志缓冲区的大小和设置是通过这个 maxBufferSize
值在树创建时完成的。
Canopy depth
“Canopy depth
”,有时也称为Canopy
大小,是指在任何给定的证明路径上缓存/存储在链上的证明节点数量。
在对 leaf
执行更新操作时,例如转让所有权(例如出售压缩的NFT
),必须使用完整的证明路径来验证叶子节点的原始所有权,从而允许进行更新操作。此验证是使用完整的证明路径来正确计算当前的 root hash
(或通过链上的“并发缓冲区”缓存的任何 root hash
)来执行的。
树的最大深度越大,执行此验证所需的证明节点就越多。例如,如果您的最大深度是 14
,则需要使用 14
个总的证明节点进行验证。随着树的增大,完整的证明路径也会变得更长。
通常情况下,每个这些证明节点都需要在每个树更新事务中包含。由于每个证明节点的值在事务中占用 32 bytes
(类似于提供公钥),较大的树很快就会超过最大事务大小限制。
进入Canopy
。Canopy
可以在链上存储一定数量的验证节点(对于任何给定的验证路径)。这样可以在每个更新交易中包含较少的验证节点,从而保持整体交易大小低于限制。
例如,深度为 14
的树需要 14
个总的验证节点。而有 10
个Canopy
的情况下,每个更新事务只需要提交 4
个验证节点。
Canopy
深度值越大,成本越高
canopyDepth
值也是创建树时成本的主要因素,因为您将在树的创建时支付这个成本。canopyDepth
越高,链上存储的数据证明节点越多,成本也越高。
较小的Canopy
限制了可组合性
虽然树的创建成本随着Canopy
的高度而增加,但较低的Canopy
将需要在每个更新事务中包含更多的证明节点。所需提交的节点越多,事务的大小就越大,因此超过事务大小限制就越容易。
这也适用于任何其他试图与您的树/叶子进行交互的Solana
程序或dApp
。如果您的树需要太多的证明节点(因为Canopy
深度较低),那么任何其他链上程序可能提供的额外操作都将受到其特定指令大小加上您的证明节点列表大小的限制。这限制了可组合性,并限制了您的特定树的潜在附加效用。
例如,如果您的树被用于压缩的非同质化代币(NFTs
),并且Canopy
深度非常低,一个NFT
市场可能只能支持简单的NFT
转移,而无法支持链上竞标系统。
创建一棵树的成本
创建并发 Merkle
树的成本基于树的大小参数: maxDepth
, maxBufferSize
和 canopyDepth
。这些值都用于计算在链上存在树所需的链上存储空间(以字节为单位)。
一旦计算出所需的空间(以字节为单位),并使用 getMinimumBalanceForRentExemption RPC
方法,请求在链上分配这些字节所需的费用(以lamports
为单位)。
在JavaScript中计算树木成本
在 @solana/spl-account-compression
包中,开发人员可以使用 getConcurrentMerkleTreeAccountSize
函数来计算给定树大小参数所需的空间。
然后使用 getMinimumBalanceForRentExemption
函数来获取在链上分配所需空间的最终成本(以lamports
计算)。
然后确定以lamports
计算的成本,使得这个大小的账户免除租金,与其他账户创建类似。
const requiredSpace = getConcurrentMerkleTreeAccountSize(
maxDepth,
maxBufferSize,
canopyDepth,
);
const storageCost = await connection.getMinimumBalanceForRentExemption(
requiredSpace,
);
示例费用
以下是几个不同树大小的示例成本,包括每个树可能的叶节点数量:
例子 #1:16,384个节点,成本为0.222 SOL
- 最大深度为
14
,最大缓冲区大小为 64
- 叶节点的最大数量:
16,384
- 创建
0.222 SOL
的Canopy
深度大约需要 0
的成本
例子 #2:16,384个节点,成本为1.134 SOL
- 最大深度为
14
,最大缓冲区大小为 64
- 叶节点的最大数量:
16,384
- 创建
1.134 SOL
的Canopy
深度大约需要 11
的成本
示例 #3:1,048,576个节点,成本为1.673 SOL
- 最大深度为
20
,最大缓冲区大小为 256
- 叶节点的最大数量:
1,048,576
- 创建
1.673 SOL
的Canopy
深度大约需要 10
的成本
示例#4:1,048,576个节点,成本为15.814 SOL
- 最大深度为
20
,最大缓冲区大小为 256
- 叶节点的最大数量:
1,048,576
- 创建
15.814 SOL
的Canopy
深度大约需要 15
的成本
压缩的NFTs
压缩的NFT是Solana
上状态压缩的最受欢迎的应用之一。通过压缩,一个拥有一百万个NFT
的收藏品可以以 ~50 SOL
的价格铸造,而不是其未压缩的等价收藏品。
开发者指南:
阅读我们的开发者指南,了解如何铸造和转移压缩的NFT。