Bitcoin Computer——Token合约

前面介绍了Bitcoin Computer的原理以及如何发送比特币。此次介绍Token方案。要理解本文,须要先阅读前两篇文章。web

建立合约

先来看示例代码浏览器

import Computer from 'bitcoin-computer';

class Token{ 
 
  
    constructor(supply, to) { 
 
  
        this.tokens = supply;
        this._owners = [to];
    }

    send(amount, to){ 
 
  
        if(this.tokens < amount){ 
 
  
            throw new Error(`Not enough tokens: ${ 
    amount} < ${ 
    this.tokens}`);
        }

        this.tokens -= amount;
        return new Token(amount, to);
    }
}

(async () => { 
 
  
    const computerA = new Computer.default({ 
 
  
        seed: 'seed of computer A',
        chain: 'BSV',
        network: 'testnet'
    });

    let tokens = await computerA.new(Token, [100, computerA.db.wallet.getPublicKey().toString()]);
    console.log(tokens);
})();
  1. 首先定一个了一个名为Token的智能合约。
    • 成员变量tokens表示token数量;成员变量_owners表示这些数量的token属于谁。
    • 构造函数有两个参数,supply表示该合约实例有多少个token,to是token拥有者的公钥。若是是首次建立合约,那么supply表示token总量是多少。
    • send方法用于发送token,amount参数表示发送多少,to参数是接收方的公钥。
  2. 建立Computer实例computerA
  3. computerA建立Token合约,总量100,发放到本身的公钥里。

运行结果:安全

Token { 
 
  
   tokens: 100,
       _owners: [
       '03367b59cc6ba5cdb93b3bdc61c7018655462251b3608383c5a1b4adcf5f1bcc1f'
   ],
       _id: '37e47c71219a0f7f3b80c04b24bbfa4613364cb1a81a9b166a33178c375ba628:0',
       _rev: '37e47c71219a0f7f3b80c04b24bbfa4613364cb1a81a9b166a33178c375ba628:0',
       _rootId: '37e47c71219a0f7f3b80c04b24bbfa4613364cb1a81a9b166a33178c375ba628:0'
}

能够在区块浏览器查看这笔转帐,若是你读了前两篇文章,能够猜想出合约输出的内容,为了方便查看,我使用ASM格式的脚本,但把其中的可见字符串部分直接用UTF8格式表示:app

1
03367b59cc6ba5cdb93b3bdc61c7018655462251b3608383c5a1b4adcf5f1bcc1f
1
OP_CHECKMULTISIG

{"__cls":"class Token{\n constructor(supply, to) {\n this.tokens = supply;\n this._owners = [to];\n }\n\n send(amount, to){\n if(this.tokens < amount){\n throw new Error(`Not enough tokens: ${amount} < ${this.tokens}`);\n }\n\n this.tokens -= amount;\n return new Token(amount, to);\n }\n}","__index":{"obj":0},"__args":[100,"03367b59cc6ba5cdb93b3bdc61c7018655462251b3608383c5a1b4adcf5f1bcc1f"],"__func":"constructor"}

OP_DROP

多签名的公钥为ComputerA的公钥。合约数据部分为Token类的定义,以及构造函数名称及运行参数,表示用构造函数和参数建立了一个合约实例。async

token转帐

const computerB = new Computer.default({ 
 
  
        seed: 'seed of computer B',
        chain: 'BSV',
        network: 'testnet'
    });
await tokens.send(10, computerB.db.wallet.getPublicKey().toString());
    console.log(tokens);
  1. 建立另外一个Computer实例computerB
  2. computerA把10个token转给computerB

运行结果:svg

Token { 
 
  
    tokens: 90,
        _owners: [
        '03367b59cc6ba5cdb93b3bdc61c7018655462251b3608383c5a1b4adcf5f1bcc1f'
    ],
        _id: '37e47c71219a0f7f3b80c04b24bbfa4613364cb1a81a9b166a33178c375ba628:0',
        _rev: '171c0c8713b8c8a97c556b286c95b821e0ff64170b681c946d4123aee9134e79:0',
        _rootId: '37e47c71219a0f7f3b80c04b24bbfa4613364cb1a81a9b166a33178c375ba628:0'
}

能够看到,转帐结束后,computerA的token数量还剩下90。函数

经过区块链浏览器查看这个tx,输出结构上跟以往的例子有些不同。以往的合约都有两个输出,一个合约输出,一个找零输出。而这个token转帐tx有三个输出,其中第三个是找零。接下来详细看看前两个输出。区块链

输出0

1
03367b59cc6ba5cdb93b3bdc61c7018655462251b3608383c5a1b4adcf5f1bcc1f
1
OP_CHECKMULTISIG

{"__index":{"obj":0,"res":1},"__args":[10,"02c9788a60264523ba77500e19a0b2626c9b09b25daa16cfee09b4e1135d610c90"],"__func":"send"}

OP_DROP

很明显这是一个合约运行输出脚本,多签名的公钥是computerA的,数据部分是合约运行的函数和参数。
跟以往例子不一样的是,合约数据中的__index字段里多了一个res字段,值为1。由于开发者并无公布协议,因此并不能准确知道这个字段的含义,我猜想这里的1表示输出1:这个合约的运行给输出1提供了一些必要信息。测试

输出1

1
02c9788a60264523ba77500e19a0b2626c9b09b25daa16cfee09b4e1135d610c90
1
OP_CHECKMULTISIG

{"__cls":"class Token{\n constructor(supply, to) {\n this.tokens = supply;\n this._owners = [to];\n }\n\n send(amount, to){\n if(this.tokens < amount){\n throw new Error(`Not enough tokens: ${amount} < ${this.tokens}`);\n }\n\n this.tokens -= amount;\n return new Token(amount, to);\n }\n}"}

OP_DROP

这个输出的多签名公约是computerB的,因此应该表示一部分token转到了computerB名下。合约数据部分是一份Token类的定义。this

到这里,须要再看看Token类中send方法的实现。其中真正的转帐代码是经过再建立一个Token类实例来实现的,构造函数的参数就是发送数量接收者公钥。因此,我猜想,合约代码中这行代码的逻辑体如今tx中就是这个输出。

同时,合约数据中并无出现合约建立tx输出中的__args__func字段,__args字段应该是来自输出0中的__args,这也就是咱们前面说到的输出0给输出1提供了必要信息。

computerB的视角

前面咱们用computerA的token实例转完帐后,token数量还剩90。接下来咱们用compuerB把转帐tx同步下来,用它的视角来看看token的数量。
由于输出1才是属于computerB的,因此首先先把版本号的outputIndex部分改成1,而后再进行同步。

const computerBTokenRev = tokens._rev.split(':')[0]+':1';

tokens = await computerB.sync(computerBTokenRev);
console.log(tokens);

运行后tokens变量的值为:

Token {
    tokens: 10,
        _owners: [
        '02c9788a60264523ba77500e19a0b2626c9b09b25daa16cfee09b4e1135d610c90'
    ],
        _rev: '171c0c8713b8c8a97c556b286c95b821e0ff64170b681c946d4123aee9134e79:1',
        _id: '171c0c8713b8c8a97c556b286c95b821e0ff64170b681c946d4123aee9134e79:1',
        _rootId: '37e47c71219a0f7f3b80c04b24bbfa4613364cb1a81a9b166a33178c375ba628:0'
}

属于computerB的token数量为10,跟预期一致。
同时还发现一个有意思的现象:_id字段再也不是合约部署的outPoint,而是转帐tx的输出1。而_rootId字段仍然是合约部署的outPoint。所以,我猜想_rootId表示部署合约的outPoint,_id表示当前合约实例开始建立的outPoint(属于compuerB的合约实例在部署时尚未,在compuerA转帐后才出现)。

安全性

上链前保护

Token方案的首要安全性是对token数量的计算是否正确。咱们沿用上面的程序继续对安全性进行测试。

tokens = await computerA.sync('171c0c8713b8c8a97c556b286c95b821e0ff64170b681c946d4123aee9134e79:0');
tokens.send(91, computerB.db.wallet.getPrivateKey().toString());
  1. computerA把它的合约UTXO同步下来,此时还剩90个token。
  2. 尝试给computerB转出91个token。

运行时抛出了一个异常:

UnhandledPromiseRejectionWarning: Error: Not enough tokens: 91 < 90

仔细看send函数的实现,最开始就是对token数量的保护,若是实例没有那么多token,就抛出了异常,并跳出了send函数的运行。也就是说会发送失败,从而在上链前就保证了token数量的正确。
因此,Bitcoin Computer用Javascript的异常功能巧妙地对合约的安全性进行了保护。

上链后保护

若是手工构造一笔不合法的转帐,是否能够突破token数量的异常保护呢?我构造了一笔转帐,标记computerAcomputerB转了91个token,实际上computerA只有90个。

{
  "__index":{"obj":0,"res":1},
  "__args":[91,"02c9788a60264523ba77500e19a0b2626c9b09b25daa16cfee09b4e1135d610c90"],
  "__func":"send"
}

对于已经在链上的非法转帐,咱们看Bitcoin Computer会如何表现:

const tokens = await computerA.sync('51bdee16d6b452aec909908c228c355ab7ca7bc311952ce7897d5e9bfa2fcb3d:0');
console.log(tokens);

computerA从记载非法转帐记录的outPoint加载合约数据,运行程序,抛出了以下异常:

UnhandledPromiseRejectionWarning: Error: Not enough tokens: 91 < 90

再来看看computerB加载非法转帐会如何表现:

const tokens = await computerB.sync('51bdee16d6b452aec909908c228c355ab7ca7bc311952ce7897d5e9bfa2fcb3d:1');
console.log(tokens);

代码运行时仍然抛出了异常:

UnhandledPromiseRejectionWarning: Error: Not enough tokens: 91 < 90

综上,虽然不合法的合约数据能够手动写入链上,但在运行Javascript合约代码时,合约能够检查出逻辑错误,并抛出异常,程序中止运行。

总结

Bitcoin Computer的Token合约虽然简短,但实现上比较精巧:

  1. 经过建立新的合约实例实现转帐。
  2. 直接用Javascript代码对token数量进行保护。

后记

写完了三篇关于Bitcoin Computer的介绍文章,这个系列就暂告一断落了。

因为Bitcoin Computer的协议并无公开,我只是凭经验和测试来猜想Bitcoin Computer的运行原理,细节不必定准确,欢迎你们一块儿交流。同时也但愿开发者能够尽快公布协议,对于二层合约方案来讲,协议的公开性是必须的,不然没法知足用户的安全需求。

另外,有一个原理相似的二层合约方案Run,该方案只运行在BSV链,RelayX基于该方案发布了USDC Token。最初我是想介绍这个方案的,但因为该方案没有公开代码和文档,因此选择了Bitcoin Computer。但愿Run也能够尽快公布本身的协议、文档和代码。