MAST, Merkelized Abstract Syntax Tree

写在前面的话

个人认为比特币脚本发展有三个阶段:

第一阶段,原始脚本阶段,仅有脚本。典型的有:P2PH, P2PKH。Script = scriptSig + scriptPubKey构成。所有数据都是显式的,有诸多限制的。脚本升级比较困难。

第二阶段,半自由阶段:脚本+脚本哈希。脚本形式有所突破,变得更加灵活。从P2SH开始,以及后面的Segwit,都是采用这种形式构成,特征是输出里仅存放执行脚本的哈希。这一阶段的脚本依然受到一些限制,大小和OP操作数等,但灵活性已经大大增强,可轻松软分叉升级。脚本执行过程也变为两步:1. 验证哈希 2. 执行子脚本。

  • P2SH: 输出中存放的是RedeemScript脚本哈希
  • SegWit: 输出中存放的是witness脚本哈希

第三阶段,自由阶段:脚本+脚本哈希+语法树。在第二阶段的基础上,发展为语法树。输出依然存储的是哈希,但进化为脚本语法树的根哈希值,即MAST结构。MAST突破了诸多限制,大小和OP限制等。灵活性,扩展性均极大增强,可衍生出丰富的应用场景。MAST目前依然是处于提议阶段,尚未实施。

比特币的脚本输出

比特币通过定义一系列OP码,将输入、输出的脚本压栈运行,根据运行结果判定花费行为是否合法。通常花费条件全部记录在交易的输出中。这个模式有一些缺点:

  1. 接受者很难指定条件
    • 当前主要是匹配公钥或者其哈希,然后花费时校验其签名
  2. 大脚本占用更多的UTXO空间
  3. 发送者付出更高的成本:交易体积大,通常需要付出更高的手续费
  4. 为了防止潜在的DoS攻击,比特币对脚本设定了一些限制:脚本体积小于10KBytes,或小于201个OP码
  5. 未执行的部分脚本,其非共识层面的数据都是公开的,即占用区块空间又暴露隐私

P2SH的提出,解决了上述1、2、3三个问题。P2SH分离出RedeemScript,将其20字节Hash值放入输出,花费时提供出RedeemScript并执行。但依然有520字节限制,并需要公开完整的RedeemScript。

在Segwit里,定义了一个新的脚本输出格式,P2WSH,pay to witness script hash。与P2SH非常类似,但hash是32字节的,script存放在witness字段中(而不是原来的scriptSig字段)。

Merkelized Abstract Syntax Tree

抽象语法树, Abstract Syntax Tree

抽象语法树,或语法树,计算里非常常见。通过树形来表达编程语言的一种结构。

例如这段的代码展开后的语法树:

1
2
3
4
5
6
while b ≠ 0
if a > b
a := a − b
else
b := b − a
return a

400px-abstract_syntax_tree_for_euclidean_algorithm svg

MAST也是采用树状结构的一种语法树,但更加精简:去掉条件判断,每个独立脚本就是一个叶子节点,每一个叶子节点就是一个执行分支。

MAST

MAST的作者是Johnson Lau,于2016年04月由BIP114提出,相关贡献者有:Russell O’Connor, Pieter Wuille和Peter Todd。

MAST采用Merkle树将各个分支编码进脚本(Script)。花费时,仅需要提供执行的分支脚本和相关层的hash进行验证,不执行的分支脚本无需公开。需要验证的哈希层数是O(log N)复杂度,假设可执行的脚本分支多达1024个,那么层数也仅有10层,依然可以保持很小的体积。因无需展示出所有的脚本,实际也突破了脚本大小的520字节硬限制。

值得注意的是:MAST是二叉树,但无需平衡构造,形状任意。

在BIP141(Segregated Witness)里,设定了交易输出witness program的格式:

[version][program]

这里我们设定MAST类型输出的version为1,program的大小为32字节,其中witness program即为MAST Root

欲花费这种的输入,则输入witness为:

1
2
3
4
5
6
7
8
9
10
11
12
13
Script_stack_1
Script_stack_2
.
.
Script_stack_X (X ≥ 0)
Subscript_1
Subscript_2
.
.
Subscript_Y (1 ≤ Y ≤ 255)
Position
Path
Metadata (Y|MAST Version)

这些字段可以划分为三个部分:

  1. 脚本栈元素,即入栈的操作码和数据,早于MAST Script入栈
  2. sub_script,子脚本,MAST Script会拆分为N个子脚本,拆分规则非常的自由,当然也可以选择不拆分
  3. 三个描述字段:Postion, Path, Metadata

MAST witness中各字段含义

Metadata的长度是1~5字节:

  • 第一个字节表示Subscript的数量,范围是1~255
  • 接下来的N个字节(0<=N<=4)表示MAST version版本号,若版本为零,则可省略

Path表示Merkle树路径,用于Script Hash

  • Path大小必须是32的整数倍,但不超过1024字节
  • 每个32字节,是各层与这个路径相关分支的哈希值,哈希函数是double-sha256

Position表示叶子的位置,左侧开始,从零开始计数,长度通常小于4字节。若树的深度是4,那么可选范围是[0, 2^n-1]即[0, 15]。

Script Hash的定义:

1
2
Script Hash = H(Y|H(Subscript_1)|H(Subscript_2)|...|H(Subscript_Y))
H() = SHA256(SHA256())
  • Y是一个字节的数,表示脚本数量
  • 后面紧接着是每个子脚本的哈希值
  • 把N个SubScript_n连接起来就是完整的MAST Script,这里可以自由的对MAST Script进行拆分,增强隐私性

Script Root就是脚本树的树根哈希值。

MAST RootH(MAST Version|Script Root),数据长度是固定的36字节:前4字节表示版本,后32字节是脚本树的根哈希值。执行脚本前,需要验证MAST Root值,不相等则执行失败。

MAST Root哈希值验证相等,MAST Version大于零,则脚本无需执行便直接返回成功。SigOpsCost为零,这个设定可以用于未来的脚本升级。

约定

在BIP114中所表述的MAST,其实是限制版本。广义的MAST,可以自由选择脚本组合,构成更加复杂的情况。

我们先看看在“广义MAST”下的情况,假设一个花费条件可以设定为:

1
(A or B) and (C or D or E) and (F or G)

A~G一共七个脚本,构成三个主要条件(本例都是与运算),每个条件由各子脚本再布尔运算后返回结果。七个可构成一个三层的Merkle树,2^2=4 < 7 < 2^3=8。

在BIP114里,则限定为仅可执行单一脚本分支,那么欲实现上面的花费条件,则需要展开每个可能性,一共2*3*2=12个分支,则需要构造一个四层树,2^3=8 < 12 < 2^4=16。

1
2
3
4
5
6
A and C and F
B and C and F
A and D and F
.
.
B and E and G

也就是说,即使不允许脚本之间进行组合逻辑运算,通常也不过就是树多了一层深度而已。

实现广义MAST的一个方法是组合使用几个操作码:OP_EVAL, OP_CAT, OP_HASH256。不过,OP_EVAL的引入会带来诸多问题,例如可能陷入死循环、无法做静态程序分析。

当前设计方案的一些优点:

  • SubScript位于特定的堆栈位置,可以做静态程序分析,可以统计SigOps操作数,可以提前终止脚本如果遇到非法OP等
  • 不同的参与方仅需公布脚本哈希,H(SubScript),直到花费时暴露完整脚本,带来更好的隐私性
  • 如果需要公开最终的脚本,可以组合进一个子脚本里SubScript,稍微节约一些SigOps操作数

也有一些缺点:

  • 例如需要展开各种条件,以形成某个分支
  • 创建和存储MAST可能花费一些时间和空间,但这个仅影响合约中的相关参与方,不影响其他比特币用户

MAST Version

当前的提案中允许用户自己制定版本,比起scriptPubKeyscriptSig会相对廉价一些。未定义的版本,则依然保持人人可花费的特性,可用于未来做软分叉等。新版本可放宽限制(例如放开MAST Script的10K字节限制),添加或者重定义OP码,甚至在未来引入其他脚本语言和系统。

一个示例

Calculation of MAST Root

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Subscript:
SA = OP_1 OP_EQUALVERIFY (0x5188)
SB = OP_2 OP_EQUALVERIFY (0x5288)
SC = OP_3 OP_EQUALVERIFY (0x5388)
SD = OP_4 OP_EQUALVERIFY (0x5488)
SE = OP_5 OP_EQUALVERIFY (0x5588)
SF = OP_6 OP_EQUALVERIFY (0x5688)
SG = OP_7 OP_EQUALVERIFY (0x5788)
SH = OP_8 OP_EQUALVERIFY (0x5888)
M = OP_RETURN "Hello" (0x6a0548656c6c6f)

Hash:
//
// Script Hash
//
HA = H(0x01|H(SA)) = H(0x015acb54166e0db370cd1b05a29120373568dacea2abc3748459ec3da2106e4b4e) = 0xd385d7268ad7e1ec51660f833d54787d2d8d79b6b1809d9c1d06c9e71f7be204
HB = H(0x02|H(SB)|H(SC)) = 0x7cbfa08e44ea9f4f996873be95d9bffd97d4b91a5af32cc5f64efb8461727cdd
HF = H(0x03|H(SD)|H(SE)|H(SF)) = 0x4611414355945a7c2fcc62a53a0004821b87e68f93048ffba7a55a3cb1e9783b
HG = H(0x01|H(SG)) = 0xaa5fbdf58264650eadec33691ba1e7606d0a62f570eea348a465c55bc86ffc10
HC = H(0x01|H(M)) = 0x70426d480d5b28d93c5be54803681f99abf4e8df4eab4dc87aaa543f0d138159
HD = H(0x0x|H(SH)) = 0x8482f6c9c3fe90dd4d533b4efedb6a241b95ec9267d1bd5aaaee36d2ce2dd6da

//
// Node Hash
//
HE = H(HA|HB) = 0x049b9f2f94f0a9bdea624e39cd7d6b27a365c6a0545bf0e9d88d86eff4894210
HH = H(HC|HD) = 0xc709fdc632f370f3367da45378d1cf430c5fda6805e731ad5761c213cf2d276e
HI = H(HE|HF) = 0xead5e1a1e7e41b77b794f091df9be3f0e9f41d47304eb43dece90688f69843b7
HJ = H(HG|HH) = 0xd00fc690c4700d0f983f9700740066531ea826b21a4cbc62f80317261723d477

//
// Root Hash
//
Script Root = H(HI|HJ) = 0x26d5235d20daf1440a15a248f5b5b4f201392128072c55afa64a26ccc6f56bd9

MAST Root = H(MAST Version|Script Root)
= H(0x0000000026d5235d20daf1440a15a248f5b5b4f201392128072c55afa64a26ccc6f56bd9)
= 0xb4b706e0c02eab9aba58419eb7ea2a286fb1c01d7406105fc12742bf8a3f97c9

交易输出(即scriptPubKey)为:

1
2
3
4
5
6
7
8
9
scriptPubKey:
0x5120b4b706e0c02eab9aba58419eb7ea2a286fb1c01d7406105fc12742bf8a3f97c9

// 0x51,即OP_1
// 0x20=32,表示witness_program为32字节长度
// 0xb4b706...3f97c9 即MAST Root Hash
//
// Decode:
// OP_1 0xb4b706e0c02eab9aba58419eb7ea2a286fb1c01d7406105fc12742bf8a3f97c9

Witness为:

1
2
3
4
5
6
7
8
9
Script_stack_1: 0x06
Script_stack_2: 0x05
Script_stack_3: 0x04
Subscript_1: 0x5488
Subscript_2: 0x5588
Subscript_3: 0x5688
Position: 0x01 (HF is the second hash in its level)
Path (HE|HJ): 0x049b9f2f94f0a9bdea624e39cd7d6b27a365c6a0545bf0e9d88d86eff4894210d00fc690c4700d0f983f9700740066531ea826b21a4cbc62f80317261723d477
Metadata: 0x03 (3 Subscript, version is 0 so ignore it)

非平衡式构造

在构造脚本树时,可以把执行概率大的脚本放在接近树根的层,执行概率低的分支脚本放到远离树根的层。这种有意的不平衡构造方式,可以有效减少花费时witness的体积。

多重签名和过期

1
2
3
4
5
6
IF
2 <Alice's pubkey> <Bob's pubkey> <Escrow's pubkey> 3 CHECKMULTISIG
ELSE
"30d" CHECKSEQUENCEVERIFY DROP
<Alice's pubkey> CHECKSIG
ENDIF

使用压缩公钥的话,那么上面脚本大小为150字节。采用MAST,可以把此脚本展开为两个分支:

1
2
3
4
5
// 105字节
2 <Alice's pubkey> <Bob's pubkey> <Escrow's pubkey> 3 CHECKMULTISIGVERIFY

// 42字节
"30d" CHECKSEQUENCEVERIFY <Alice's pubkey> CHECKSIGVERIFY

Hashed Time-Lock Contract

1
2
3
4
5
6
7
8
9
10
11
12
13
HASH160 DUP <R-HASH> EQUAL
IF
"24h" CHECKSEQUENCEVERIFY
2DROP
<Alice's pubkey>
ELSE
<Commit-Revocation-Hash> EQUAL
NOTIF
"Timestamp" CHECKLOCKTIMEVERIFY DROP
ENDIF
<Bob's pubkey>
ENDIF
CHECKSIG

采用MAST,展开为三个分支:

1
2
3
4
5
6
7
8
// 1
HASH160 <R-HASH> EQUALVERIFY“24h”CHECKSEQUENCEVERIFY <Alice's pubkey> CHECKSIGVERIFY

// 2
HASH160 <Commit-Revocation-Hash> EQUALVERIFY <Bob's pubkey> CHECKSIGVERIFY

// 3
"Timestamp" CHECKLOCKTIMEVERIFY <Bob's pubkey> CHECKSIGVERIFY

在提高可读性的同时,也可以显著减少witness体积。

大型多重签名合约

当前的多重签名最多允许20把公钥,通过CHECKSIGsIF/ELSE组合一下,可以突破20把公钥限制,但不会突破太多数量,因为很快就会受到10,000字节和201个sigops限制。

通过MAST,可以轻松的构造出 3 of 2000的大型多重签名。从2000人随时抽取3个,那么组合数量为:(2000*1999*1998)/(3*2*1)=1,331,334,000。即1,331,334,000个3 of 3的CHECKMULTISIGVERIFY多签验证。即使这么多可能性分支,也不过就是31层的MAST,2^30 < 1,331,334,000 < 2^31。输出依然是34字节,花费时的witness也不会超过1500字节。

非共识层面的强数据

当前,这些数据一般通过OP_RETURN进行提交,放到交易的输出中。例如存在证明,可以将某个哈希值通过放入交易的输出里,而提交至区块链存储。

通过MAST,可以包含进更多的数据,无需提交每一个值,将其作为MAST分支,那么永远只需要提交32字节即可。


参考: