作者:Anthony Towns
来源:https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2021-September/019420.html
首先,我们要快速讨论一下 IN_OUT_AMOUNT。我认为,最容易的处理它的办法就是一个专门的操作码,将两个数值推入栈中;但是,也可以由两个操作码来实现,或者,甚至它可以接受一个参数,让你可以指定读取的是哪个输入(以及对应的输出)(而 -1
意味着是当前的输入)。
但是,这里有一个很大的复杂性:用聪来表示数额,需要高达 51 比特的长度,但脚本只允许你做 32 位的数字的数学运算。不过,引入 IN_OUT_AMOUT 已经意味着要使用一种 OP_SUCCESS 操作码(译者注:需要软分叉),所以这也恰好允许我们任意重新定义其它操作码的行为 —— 所以我们可以根据 IN_OUT_AMOUNT 在脚本中存在与否来变更 ADD、SUB 和比较大小的操作符的语义,使之能支持 64 位的数值。让 MUL、DIV 和 MOD 具备这样的特性可能也是有价值的。
– – –
回到 TLUV。我的理论是,它会从栈中弹出 3 个元素。最上面的元素是 C
,表示用于控制的整数;然后是 H
,额外的默克尔路径步骤;最后是 X
,对内部公钥的调整。如果 H
为空向量,则不会添加额外的默克尔路径步骤;否则 H
必须是一个 32 字节的元素。如果 X
是空向量,那么内部公钥就不会改变;否则 X
必须是一个 32 字节的 x-only 公钥。
C
的低比特(low bit)指明了 X
的奇偶性;如果是 0,则 X
所代表的公钥的 y
坐标值为偶数;如果是 1,则 X
所代表的公钥的 y
坐标值为奇数。
C
的下一个比特指明了当前的脚本是否要从默克尔路径中抛弃,如果它是 0,则会保留当前的脚本;如果它是 1,则当前的脚本会被抛弃。
C
的剩余比特(即 C >> 2
)是要从当前的默克尔路径中抛弃的步骤数量。(C
为负数的行为有待决定 —— 要么总是失败,要么总是成功,或者可以留给未来的软分叉来定义)。
举个例子,假设我们的 taproot uxto 有 5 个脚本(A、B、C、D、E),并且按照 BIP 341 中的定义计算相应的哈希值:
AB = H_TapBranch(A, B)CD = H_TapBranch(C, D)CDE = H_TapBranch(CD, E)ABCDE = H_TapBranch(AB, CDE)
而且,我们要使用脚本 E 来花费,这时候的 “控制块” 包含了脚本 E、通往 E 的默克尔路径,即(AB, CD)。
我们就用这个例子来展示你可以如何用 TLUV 来控制花费脚本的变化(即将输入的脚本公钥转化成输出的脚本公钥)。
最简单的,假设我们的脚本 E 中包含了 0 0 0 TLUV
,它的意思是,我们将保留当前的脚本、保留当前的默克尔路径中的所有步骤、不添加新的步骤,也不会改变内部公钥 —— 也就是让结果的脚本公钥跟我们当前正在花费的脚本公钥没有区别。
如果我们使用脚本 0 F 0 TLUV
(即 H = F, C = 0
),那么我们会保留当前的脚本、保留当前默克尔路径中的所有步骤(AB 和 CD),然后加入一个新步骤(F)到默克尔路径中,于是沿路的哈希值变成:
EF = H_TapBranch(E, F)CDEF =H_TapBranch(CD, EF)ABCDEF = H_TapBranch(AB, CDEF)
如果我们使用脚本 0 F 2 TLUV
(H=F, C=2
),那么我们会抛弃当前的脚本、但保留默克尔路径的其它步骤,然后加入一个新的步骤(本质上,这就是把当前位置的脚本换成了一个新的脚本):
CDF = H_TapBranch(CD, F)ABCDF = H_TapBranch(AB, CDF)
如果我们使用脚本 0 F 4 TLUV
(H=F, C=4
),那么我们会保留当前的脚本,但抛弃默克尔路径上的最后一个步骤,然后添加一个新的步骤(本质上就是替换了当前脚本的 兄弟):
EF = H_TapBranch(E, F)ABEF = H_TapBranch(AB, EF)
如果我们使用了脚本 0 0 4 TLUV
(H=empty, C=4
)那么我们会保留当前的脚本,但抛弃默克尔路径上的最后一个步骤,也不添加新的脚本(也就是抛弃掉了兄弟):
ABE = H_TapBranch(AB, E)
实现带有 “释放/锁定/可用” 结构的保险柜构造,将是这样的:
- 锁定脚本为
OP_RETURN
- 可用脚本为
<HOT> CHECKSIGVERIFY IN_OUT_AMOUNT SWAP <X> SUB DUP 0 GREATERTHAN IF GREATERTHANOREQUAL VERIFY 0 <H_LOCKED> 2 TLUV ELSE 2DROP ENDIF <D> CSV
- 释放脚本为
<HOT> CHECKSIGVERIFY IF <H_LOCKED> ELSE <H_AVAILABLE> ENDIF 0 SWAP 4 TLUV INPUTAMOUNT OUTPUTAMOUNT LESSTHANOREQUAL
- HOT 是 32 字节的热钱包公钥
- X 是热钱包随时可花的最大数额
- D 是资金释放和实际使用之间的强制性延迟
- H_LOCKED 为
H_TapLeaf(locked script)
- H_AVAILABLE 为
H_TapLeaf(available script)
– – –
再说回池化资金的方案。实际上,令人难过的是,更新内部公钥的机制就是事情复杂化的起点。具体来说,因为 taproot 在脚本公钥和内部公钥中使用了 32 字节的 x-only 公钥(隐含 y 为偶数),我们不得不担心一件事,例如,假设 A、B、C 和 A + B + C
的 y 值都为偶数,但 A + B = A + B + C - C
的 y 值并不是偶数。那么,让 C 从池中自己退出,可能会导致脚本公钥发生如下的改变(从原来的 Qabc
变成 Qab
:
Qabc = (A+B+C) + H(A+B+C, (Sa, (Sb, Sc)))*GQab = -(A+B) + H( -(A+B), (Sa, Sb)*G
听起来这也还好,没什么大不了,但要是 B 这时候再从池中退出呢?你取得的内部公钥是 -(A + B)
,因为 (A + B)
的 y 值是奇数;如果你减去 B,那它会给你 - A - 2B
而不是 A
。所以 B 是拿到了自己的资金,但 B 并没有在内部公钥中消失,你依然需要 B 的签名来实现密钥路径花费,这绝对不是我们想要的。
如果我们忽略这个警告(例如,如果得到的内部公钥的 y 为奇数,则让 TLUV 认定这是一个错误),那么,用于从池中退出的脚本是非常直接的(假定你的余额为 BAL
,你的公钥为 KEY
):
<KEY> DUP "" 1 TLUVCHECKSIGVERIFY IN_OUT_AMOUNT SUB <BAL> GREATERTHANOREQUAL
对于一般大小的池子来说,“当这事不存在” 是由可能做到的,比如有 4 个人,你只需要选择 A、B、C、D,使得它们的和的任意组合(A + B + C、A + D,等等)都对应着一个 y 值为偶数的公钥,让每个人都在进入池子之前提前验证一下,就可以了。如果我没算错,你需要做 2^n
次尝试来找出一个幸运集合,但是,每一个不符合要求的集合都会很快暴露出来,导致每次尝试的计算时间可以摊平为常数,所以整体开销应该是 3 * 2 ^ n
这样子。只要 n 不超过 20、30,开销都不算太大。
为了妥帖地处理这个问题,你需要让 UTXO 承诺其内部公钥的奇偶性,并在使用 TLUV 时用某种办法找出这个值。可能有三种方法可以做到。
最直接的办法就是在脚本公钥里承诺 —— 也就是说,在 Q = P + H(P, S) * G
之上,除了定义 P 为一个 32 字节的 x-only 公钥,还在 H(P, S)
中承诺 P 的奇偶性,然后,在使用脚本路径花费时,在控制块中揭晓内部公钥的奇偶性(而 taproot 原本已经要求揭晓脚本公钥点的奇偶性了)。因为 taproot 已经进入激活锁定节点,为 taproot 地址变更行为已经太迟了,但我们可以在未来的启用 entroot 或类似协议的软分叉中加入这一点,或者,我们可以让这个行为变成 33 字节的隔离见证 v1 地址,以 0x00 开头(举个例子)。
如果我们不在脚本公钥中承诺奇偶性,还有两种办法,在 UTXO 里面承诺:要么让脚本来保证它被承诺到了数值中;要么延伸保存在 UTXO 数据库中的数据:
为了在数值中承诺它,你可以这样做:
<P> <H> IN_OUT_AMOUNT 2 MOD SWAP 2 MOD TUCK EQUAL 2 MUL ADD TLUV
并改变 TLUV 的控制参数为:C&1
为 加入/删除 一个点,C&2
为要求结果拥有 奇数/偶数 的 y(而 C&4
和 C >> 3
控制当前的脚本是否要抛弃、要从默克尔路径中删除多少步骤)。这里的想法是,这样做保证了,如果 UTXO 的数值以聪表示,是 0 mod 2
,那就减去这个点;如果是 1 mod 2
,那就加上这个点;而且,输出 的数额以聪(mod 2
)计量,将与输入的数额(mod 2
)不同,这就是结果公钥具有奇数 y 的情形。结合保证输出的数额不会下降超过你的余额的规则,这就意味着,在你取出自己的余额的时候,有一半的概念你将不得不多付 1 聪给留在池内的成员,这样池内剩余数额的奇偶性才是对的。这并不优雅,但似乎可用。
另一种办法在上述两种办法之间,可能需要给 UTXO 数据库中的每个条目增加一个标签,来指明其内部公钥是否被反转过。只有在一个 UTXO 通过一个包含 TLUV 的脚本被创建出来时,才需要设置这个标签,而且 TLUV 将使用这个标签来决定是否要 增加/删除 传入的公钥。对我来说,这实现起来似乎非常复杂,尤其是你想让未来的(现在还未设想出来的)操作码也能设置这个标签的话。
– – –
上述所有内容都假设了,对于任何新的默克尔步骤来说,其哈希值都是在 UTXO 建立的时候就固定下来的。但是,如果 OP_CAT
或类似的东西能够启用,你就可以在脚本中用程序构造出这些哈希值,从而产生一些有趣的行为。举个例子,你可以构造一种脚本,表示 “允许任何人将自己添加到这个 ‘收购岛屿’ 池子中,只要他们能承诺至少 10 BTC”;然后,验证他们控制着自己要加入这个池子的公钥、允许他们添加一个脚本从而可以使用这个公钥取回自己的 10 BTC、允许他们跟其他人一样用自己的公钥参与池子的密钥花费路径。当然,这样的特性也许自然会延伸成人们认为 “限制条款是有害的” 案例中,例如一种 “1 美元拍卖合约”:“Alice 可以在 1000 次确认后花费这个 UTXO” 或者 “任何能够给这个 UTXO 增加 0.1 BTC 的人都可以将兄弟脚本中的 Alice 公钥换成自己的公钥,从而等待收获”。(译者注:“1 美元拍卖” 是一个著名的博弈论课堂案例,其关键条件是出价第二高的人也必须支付。这种条件会吸引竞价失败的人追加竞价。)
需要指出的一个有趣的事情是,用脚本来构造(这样的哈希值),有时候会比硬编码更加高效,例如,我认为
"TapLeaf" SHA256 DUP CAT [0xc0016a] CAT SHA256
对于计算一个 OP_RETURN
脚本的哈希值来说是正确的,而且只需要约 17 字节,比起硬编码哈希值(33 字节)要更便宜。
为了用程序构造一个新脚本,你几乎一定需要使用模板,例如:
SIZE 32 EQUALVERIFY [0xc02220] SWAP CAT [0xac] CAT "TapLeaf" SHA256 DUP CAT SWAP CAT SHA256
它可以从栈中获得一个公钥,然后将它转化成一个哈希值,使用这个哈希值的脚本就可以用来检查这个公钥的签名。我认为你可以构造多个脚本,然后用这个方法(或类似方法)将它们结合起来:
CAT "TapBranch" SHA256 DUP CAT SWAP CAT SHA256
但是,这里有一个严肃的警告:如果你允许人们在构造新脚本时任意添加操作码,它们可以选择加入其中一个 OP_SUCCESS
操作码;而且,如果他们是矿工,使用这个操作码就可以完全绕过限制条款的约束。如果你想要分析它,得到填充的模板可能必须是非常严格的,例如,为在见证脚本中提供的数据包含具体的 PUSH 操作码、并检查数据的长度跟所用的 PUSH 操作码的要求相匹配。