前面介绍了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); })();
Token
的智能合约。
tokens
表示token数量;成员变量_owners
表示这些数量的token属于谁。supply
表示该合约实例有多少个token,to
是token拥有者的公钥。若是是首次建立合约,那么supply
表示token总量是多少。send
方法用于发送token,amount
参数表示发送多少,to
参数是接收方的公钥。Computer
实例computerA
。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
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);
Computer
实例computerB
。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有三个输出,其中第三个是找零。接下来详细看看前两个输出。区块链
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 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());
computerA
把它的合约UTXO同步下来,此时还剩90个token。computerB
转出91个token。运行时抛出了一个异常:
UnhandledPromiseRejectionWarning: Error: Not enough tokens: 91 < 90
仔细看send
函数的实现,最开始就是对token数量的保护,若是实例没有那么多token,就抛出了异常,并跳出了send
函数的运行。也就是说会发送失败,从而在上链前就保证了token数量的正确。
因此,Bitcoin Computer用Javascript的异常功能巧妙地对合约的安全性进行了保护。
若是手工构造一笔不合法的转帐,是否能够突破token数量的异常保护呢?我构造了一笔转帐,标记computerA
给computerB
转了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合约虽然简短,但实现上比较精巧:
写完了三篇关于Bitcoin Computer的介绍文章,这个系列就暂告一断落了。
因为Bitcoin Computer的协议并无公开,我只是凭经验和测试来猜想Bitcoin Computer的运行原理,细节不必定准确,欢迎你们一块儿交流。同时也但愿开发者能够尽快公布协议,对于二层合约方案来讲,协议的公开性是必须的,不然没法知足用户的安全需求。
另外,有一个原理相似的二层合约方案Run,该方案只运行在BSV链,RelayX基于该方案发布了USDC Token。最初我是想介绍这个方案的,但因为该方案没有公开代码和文档,因此选择了Bitcoin Computer。但愿Run也能够尽快公布本身的协议、文档和代码。