为了实现上一篇文章中说的“计数器合约”,需要介绍BSV上的高级语言sCrypt,并解释如何用sCrypt实现这个合约。
sCrypt和BSV脚本的关系就像C语言和汇编语言的关系。通过sCrypt VS Code插件,可以把sCrypt编译成BSV脚本语言,还可以在VSCode中进行调试。
sCrypt语言实现的“计数器合约”代码如下。一会我们会详细分析。
contract Counter {
public function increment(bytes sighashPreimage, int amount) {
Tx tx = new Tx();
require(tx.validate(sighashPreimage));
int len = length(sighashPreimage);
bytes hashOutputs = sighashPreimage[len - 40 : len - 8];
bytes scriptCode = Util.readVarint(sighashPreimage[104:]);
int scriptLen = length(scriptCode);
int counter = unpack(scriptCode[scriptLen - 1 :]);
bytes scriptCode_ = scriptCode[: scriptLen - 1] ++ num2bin(counter + 1, 1);
Sha256 hashOutputs_ = hash256(num2bin(amount, 8) ++ Util.writeVarint(scriptCode_));
require(hashOutputs == hashOutputs_);
}
}
关键问题:旧TX的合约output被花费时要检查新TX的output是否符合规则,但旧TX并不知道新TX的output,那怎么检查呢?
解决方案倒也不复杂,在花费旧TX output时,把新TX output作为解锁参数告诉旧TX的output脚本,让脚本对该参数做分析和检查,如果符合条件才允许花费。
increment
函数的内容就是“计数器合约”output中的脚本内容,编译后可以分两部分:
逻辑部分的主要检查两个条件:
新TX output脚本的逻辑部分与旧TX output脚本的逻辑部分一样。
满足了新TX的output只能是“计数器”合约,不能是其他类型(如P2PKH)output的需求。
新TX output脚本的数据部分比旧TX output脚本的数据部分的值大1。
满足了每次执行(花费)合约,计数器值加1且只加1的需求。
首先,分析参数声明。
public function increment(bytes sighashPreimage, int amount)
这一行的关键是两个输入参数,这两个输入参数就是花费(执行)合约output时提供的解锁参数,包含在了新TX的input中。amount
参数不关键,先忽略。重点看sighashPreimage
参数。这个参数实际上是一系列数据的集合,其中包括了两个重要数据:
合约的原理很简单,关键是如何保证解锁参数的真实性。因为解锁参数在构造新TX时是可以随意输入的,那么如何保证sighashPreimage
参数中的两个重要数据是真实的(一定与新TX实际的output数据hash
、旧TX实际的output脚本
完全相同),而不是随意输入的假数据呢?下面这两行代码就是做这个检查的:
Tx tx = new Tx();
require(tx.validate(sighashPreimage));
你一定会好奇这两行代码到底是如何保证数据真实的,这是合约最精妙的地方,我会在下一篇文章里专门解释。
总之,如果数据不真实,那么脚本就会返回失败,UTXO不能被花费,也就是说合约不能被执行。
int len = length(sighashPreimage);
bytes hashOutputs = sighashPreimage[len - 40 : len - 8];
bytes scriptCode = Util.readVarint(sighashPreimage[104:]);
int scriptLen = length(scriptCode);
int counter = unpack(scriptCode[scriptLen - 1 :]);
sighashPreimage
的结构是固定的,按固定结构从中解析出:新TX output hash值hashOutputs
、旧TX output脚本scriptCode
,然后再从scriptCode
中取出最后一个字节,这就是旧TX中计数器的值,存在变量counter
里。
bytes scriptCode_ = scriptCode[: scriptLen - 1] ++ num2bin(counter + 1, 1);
Sha256 hashOutputs_ = hash256(num2bin(amount, 8) ++ Util.writeVarint(scriptCode_));
前面提到,新TX output脚本的逻辑部分与旧TX一样,只是数据部分的值增加了1。所以,把旧TX output脚本scriptCode
的逻辑部分保留,把数据部分counter
加1,并放到逻辑部分后面,这就是符合合约规则的新TX output脚本了。第一行代码就是这个作用,新脚本存在了变量scriptCode_
中。
再把scriptCode_
与新TX output的satoshis数量amount
合并在一起做sha256 hash,就得到了新TX output的hash值hashOutputs_
。这里仍然暂时忽略对amount
参数的解释。
require(hashOutputs == hashOutputs_);
前面已经从sighashPreimage
参数中解析出了实际的新TX output hash值hashOutputs
,也计算出了符合合约规则的预期hash值hashOutputs_
。只要比较这两个值是否相等,就可以确定实际的TX output是否符合规则。如果不符合规则,那么旧TX的UTXO就无法被花费。
这样,整个合约就完成了。这个合约已经部署在了BSV的测试网络上:0, 1, 2 …
没看明白再看一遍吧。
下一篇我们将填上这篇中挖的坑:如何保证sighashPreimage
参数的真实性。
参考资料: