Appearance
方法概述
代币以单独的 UTXO 存储,类似于原生的聪(satoshis)。我们使用契约(covenants)
来确保代币在所有后续交易中保持其特有的形式。更具体地说,我们使用递归契约(recursive covenants)
来维护代币状态。
我们将逐步构建 CAT 代币智能合约。从一个简单但不安全的实现开始。
初次尝试
在上图中,当前交易 curTx
将来自两个父交易 parent0Tx
和 parent1Tx
的代币合并。每个代币输出的状态包括:
addr
:所有者的地址amt
:输出中代币的数量
它们的脚本是相同的,并且必须满足代币脚本 T
中规定的所有条件/断言1:
- 条件(1)确保所有者 A 授权合并。
- (2) 和(3)确保脚本从父级传递到当前的花费/赎回交易,接着传递到其子级,依此类推,即递归契约。
- 条件(4)确保合并后代币数量保持不变,不会凭空创建新代币。
一次攻击
代币脚本 T
存在一个安全漏洞:对手可以将两种不同类型的代币合并。下图示例了这种攻击。交易 parent0Tx
和 parent1Tx
分别铸造了 22 和 11 单位的不同代币。这类似于从 coinbase 交易中仅新发出的比特币。
curTx
将它们合并为同一代币的 33 单位。 注意,代币脚本 T
中的所有条件都已满足。
为防止此攻击,我们为每个代币分配一个唯一标识符。一个直接的方法是使用铸造交易所花费的输出点(outpoint)作为 tokenId
,即 txid_vout
。我们称包含该输出点的交易为代币的创世交易(例如,T1
和 T2
)。由于每个创世输出点在全局上是唯一的,我们拥有一个唯一的 tokenId
。我们将 tokenId
添加到其状态中,并在 T
中增加两个条件。 这些条件确保 tokenId
从父级传递到子级、孙级等。代币输出的所有后续花费必须嵌入 tokenId
,以确保代币的整个生命周期。这项技术有效地“标记”了代币,并确保只有相同类型/标记的代币可以合并。 通过这些补充,之前的攻击被阻止,因为条件(2)被违反。
拼图的最后一块
我们还没完成。注意,T2
中的脚本 S2
被评估,但 T
没有在 parent1Tx
中铸造代币时被评估。T
仅在铸造的代币被花费时评估。因此,攻击者可以选择任意值作为 tokenId
。例如,她可以将 tokenId
设置为 T1_0
,而不是 T2_0
,并在 parent1Tx
中伪造具有 ID T1_0
的 11 单位代币。这些代币可以与 parent0Tx
中的 22 个有效代币合并,绕过所有先前的条件检查。 为解决此问题,我们在代币脚本 T
中添加了另一个条件检查,以验证 tokenId
是否如实设置为创世输出。 为了了解其工作原理,让我们看下图。假设我们位于左侧箭头指示的第三个交易 curTx
。由于脚本 S
不是 T
,我们知道 grandparentTx
是创世交易,并强制 parentTx
中的 tokenId
设置为 T1_0
。从创世开始的所有后代交易在铸造时通过算法设置了相同的 tokenId
。假设我们位于右侧箭头指示的第四个交易,我们跳过上述条件检查,因为祖父级和父级的脚本是相同的。对于第 1000 代交易也是如此。在任何代币交易中,其祖父级只能是以下两种之一:
代币创世交易
相同的代币交易
:相同的脚本和tokenId
上述攻击现已被阻止。 攻击者仍然可以伪造代币,但这些代币不可花费,使伪造变得无效。
脚注
Txo
是 Tx.outputs
的缩写。