BSV智能合约(二):计数器合约代码分析

为了实现上一篇文章中说的“计数器合约”,需要介绍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中的脚本内容,编译后可以分两部分:

  1. 逻辑部分:检查新TX output是否符合规则
  2. 数据部分:计数器的值。位于output脚本的最后,占1字节。

output script structure

逻辑部分的主要检查两个条件:

  1. 新TX output脚本的逻辑部分与旧TX output脚本的逻辑部分一样。

    满足了新TX的output只能是“计数器”合约,不能是其他类型(如P2PKH)output的需求。

  2. 新TX output脚本的数据部分比旧TX output脚本的数据部分的值大1。

    满足了每次执行(花费)合约,计数器值加1且只加1的需求。

解锁参数

首先,分析参数声明。

public function increment(bytes sighashPreimage, int amount)

这一行的关键是两个输入参数,这两个输入参数就是花费(执行)合约output时提供的解锁参数,包含在了新TX的input中。amount参数不关键,先忽略。重点看sighashPreimage参数。这个参数实际上是一系列数据的集合,其中包括了两个重要数据:

  1. 新TX output内容的hash值
  2. 旧TX output脚本内容

input parameters

检查sighashPreimage参数真实性

合约的原理很简单,关键是如何保证解锁参数的真实性。因为解锁参数在构造新TX时是可以随意输入的,那么如何保证sighashPreimage参数中的两个重要数据是真实的(一定与新TX实际的output数据hash旧TX实际的output脚本完全相同),而不是随意输入的假数据呢?下面这两行代码就是做这个检查的:

Tx tx = new Tx();
require(tx.validate(sighashPreimage));

你一定会好奇这两行代码到底是如何保证数据真实的,这是合约最精妙的地方,我会在下一篇文章里专门解释。

总之,如果数据不真实,那么脚本就会返回失败,UTXO不能被花费,也就是说合约不能被执行。

解析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 :]);

sighashPreimage的结构是固定的,按固定结构从中解析出:新TX output hash值hashOutputs、旧TX output脚本scriptCode,然后再从scriptCode中取出最后一个字节,这就是旧TX中计数器的值,存在变量counter里。

计算符合合约规则的新output hash值

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参数的解释。

检查计算出的hash值与实际的新TX output的hash值是否一致

require(hashOutputs == hashOutputs_);

前面已经从sighashPreimage参数中解析出了实际的新TX output hash值hashOutputs,也计算出了符合合约规则的预期hash值hashOutputs_。只要比较这两个值是否相等,就可以确定实际的TX output是否符合规则。如果不符合规则,那么旧TX的UTXO就无法被花费。

这样,整个合约就完成了。这个合约已经部署在了BSV的测试网络上:0, 1, 2

看明白了吗?

没看明白再看一遍吧。

下一篇我们将填上这篇中挖的坑:如何保证sighashPreimage参数的真实性。


参考资料:


下一篇:神奇的OP_CHECKSIG
上一篇:看似不可能完成的任务

猜你喜欢

转载自blog.csdn.net/Edward_sv/article/details/106688661