Bitcoin Computer——原理简述

Bitcoin Computer是比特币上的二层智能合约解决方案。该方案用Javascript语言编写智能合约,合约的代码和调用都放到区块链上。在链下进行合约状态的计算和校验。javascript

合约代码

咱们以一个计数器合约为例来分析Bitcoin Computer的运行原理和特色。基本代码以下:java

import Computer from 'bitcoin-computer';

(async () => { 
 
  
  const computer = new Computer.default({ 
 
  
    seed: 'the mnemonic words',
    chain: 'BSV',
    network: 'testnet'
  });

  class Counter { 
 
  
    constructor(n) { 
 
  
      this.n = n
    }
    inc() { 
 
  
      this.n += 1
    }
  }

  const counter = await computer.new(Counter, [2]);
  await counter.inc();
  console.log(counter);
})()
  1. 建立Computer实例。
    在BSV的测试网络上建立一个Computer实例。Computer是Bitcoin Computer的系统库,能够经过npm等包管理工具进行安装。
  2. 定义Counter合约。
    实际上就是一个javascript类。该类的构造函数中初始化了成员变量n的值。inc方法每被调用一次,成员变量n就会加1。实现了一个简单的计数功能。
  3. 部署合约。
    经过computernew方法,将Counter合约部署到区块链上。第二个参数是Counter类的构造函数的参数列表。也就是说咱们在链上部署了一个Counter合约,合约的变量n的初始值为2。同时返回了一个链上合约变量counter
    javascript关键字await说明部署合约是须要与外部交互的。很显然这一步须要经过网络将代码写入区块链。
  4. 运行合约。
    调用inc方法运行合约,让计数器加1。这里也用了关键字await,说明合约的执行也是须要与区块链交互的。

合约的部署、运行和同步

经过console.log语句打印出来的运行结果以下:node

Counter { 
 
  
  n: 3,
  _id: '0a21910877dbfa8990eec667253f140089ea7834f3677f4cab4df3ae416cb379:0',
  _rev: '1aab4b23834b0502c15db98433d7eb50e5440f2a64b4a2553a81b655ae6e2696:0',
  _rootId: '0a21910877dbfa8990eec667253f140089ea7834f3677f4cab4df3ae416cb379:0'
}

看到这个结果,咱们首先能够猜想出n表示合约中成员变量的值,初始值为2,作了一次inc操做后值加1,也就是3。与猜想一致。web

合约部署

观察_id字段,看值的格式,咱们猜这也许是个txid,那咱们打开区块浏览器查查看,发现该tx的第0个output脚本部分用ASCII解码后内容以下:npm

Q!6{ 
 
  YÌk¥Í¹;;ÜadžUF"Q³`ƒƒÅ¡´­Ï_ÌQ®L®{"__cls":"class Counter { 
 
  \n constructor(n) { 
 
  \n this.n = n\n }\n inc() { 
 
  \n this.n += 1\n }\n }","__index":{"obj":0},"__args":[2],"__func":"constructor"}u

其中把可读字符串部分单独提出来并格式化后内容以下:api

{ 
 
  
  "__cls":"class Counter {\n constructor(n) {\n this.n = n\n }\n inc() {\n this.n += 1\n }\n }",
  "__index":{ 
 
  "obj":0},
  "__args":[2],
  "__func":"constructor"
}

能够推测,这个JSON数据应该就是合约部署数据,合约的javascript代码、初始化参数等都放到了链上。根据这个内容,就能够恢复出一个javascript语言的Counter类实例。浏览器

ASM格式的脚本以下:网络

1 03367b59cc6ba5cdb93b3bdc61c7018655462251b3608383c5a1b4adcf5f1bcc1f 1 OP_CHECKMULTISIG 
7b225f5f636c73223a22636c61737320436f756e746572207b5c6e20202020636f6e7374727563746f72286e29207b5c6e202020202020746869732e6e203d206e5c6e202020207d5c6e20202020696e632829207b5c6e202020202020746869732e6e202b3d20315c6e202020207d5c6e20207d222c225f5f696e646578223a7b226f626a223a307d2c225f5f61726773223a5b325d2c225f5f66756e63223a22636f6e7374727563746f72227d OP_DROP

脚本能够分两个部分:async

  1. OP_CHECKMULTISIG为止,是一个多重签名模板,这个多签模板中只有一个公钥,是个1/1签名。说明这个UTXO只能被该公钥对应的私钥花费。可见,合约的运行是有权限控制的。
  2. PUSH一段数据,而后再DROP掉。对比ASCII格式,咱们知道这段数据实际上就是上面的合约部署数据。

同时,咱们还能够发现0a21910877dbfa8990eec667253f140089ea7834f3677f4cab4df3ae416cb379:0表示的是一个outpoint,:前面为txid,后面为output index。svg

合约运行

观察_rev字段,这也是个outpoint,经过区块链查询发现ASCII格式内容以下:

Q!6{ 
 
  YÌk¥Í¹;;ÜadžUF"Q³`ƒƒÅ¡´­Ï_ÌQ®0{"__index":{"obj":0},"__args":[],"__func":"inc"}u

剥去不可读字符并格式化后内容以下:

{ 
 
  
  "__index":{ 
 
  "obj":0},
  "__args":[],
  "__func":"inc"
}

这部分记录了合约运行时被调用的方法和参数。

ASM格式的脚本以下:

1 03367b59cc6ba5cdb93b3bdc61c7018655462251b3608383c5a1b4adcf5f1bcc1f 1 OP_CHECKMULTISIG 
7b225f5f696e646578223a7b226f626a223a307d2c225f5f61726773223a5b5d2c225f5f66756e63223a22696e63227d OP_DROP

不出所料,套路跟部署部分是同样的。

同时,咱们还发现_revtx的input之一就是_id,也就是部署合约的outpoint。很明显,合约从部署到运行,新的状态花费前一个状态的output而造成的新output,造成了一条tx链。_id记录合约的最初outpoint,也就是部署outpoint,_rev记录最新状态的outpoint。

合约同步

咱们运行一个新的程序,看合约是如何在不一样电脑之间实现同步的。

import Computer from 'bitcoin-computer';

(async () => { 
 
  
  const computer = new Computer.default({ 
 
  
    seed: 'the same mnemonic words',
    chain: 'BSV',
    network: 'testnet'
  });

  const counter = await computer.sync('1aab4b23834b0502c15db98433d7eb50e5440f2a64b4a2553a81b655ae6e2696:0');
  console.log(counter);

  await counter.inc();
  console.log(counter);
})()

computer.sync函数经过网络从区块链获取部署和运行数据,参数就是合约最新的outpoint。sync运行完毕后得到的counter变量为:

Counter { 
 
  
  n: 3,
  _rev: '1aab4b23834b0502c15db98433d7eb50e5440f2a64b4a2553a81b655ae6e2696:0',
  _id: '0a21910877dbfa8990eec667253f140089ea7834f3677f4cab4df3ae416cb379:0',
  _rootId: '0a21910877dbfa8990eec667253f140089ea7834f3677f4cab4df3ae416cb379:0'
}

这与前面运行的结果是同样的。咱们能够推测整个sync过程大体是这样的:

  1. 经过函数参数定位到合约最新outpoint。
  2. 回溯整个tx链直到合约部署。
  3. 下载整个合约部署和运行数据,并在本地运行,算出最新状态。

接下来再运行一次合约,结果以下:

Counter { 
 
  
  n: 4,
  _rev: '9407b32d7e5e701949a4b00accbd74f04c4fb651451904d77ec1a8ce56d334b4:0',
  _id: '0a21910877dbfa8990eec667253f140089ea7834f3677f4cab4df3ae416cb379:0',
  _rootId: '0a21910877dbfa8990eec667253f140089ea7834f3677f4cab4df3ae416cb379:0'
}

结果如咱们所预期。

此时我突发奇想,假设我不一样步到最新的合约状态,而是同步到中间的状态,而后就运行,会怎么样呢?
咱们复制一份一样的代码,由于最新的n值已经变成了4,而代码中sync的参数是n值为3时的outpoint,因此咱们同步的是一个中间状态。
运行代码,同步后的counter为

Counter { 
 
  
  n: 3,
  _rev: '1aab4b23834b0502c15db98433d7eb50e5440f2a64b4a2553a81b655ae6e2696:0',
  _id: '0a21910877dbfa8990eec667253f140089ea7834f3677f4cab4df3ae416cb379:0',
  _rootId: '0a21910877dbfa8990eec667253f140089ea7834f3677f4cab4df3ae416cb379:0'
}

这一步跟咱们的预期同样,同步下来了合约的中间状态。

而后来看看await counter.inc()的执行结果:

(node:85000) UnhandledPromiseRejectionWarning: Error:
        Communication Error
        message	Request failed with status code 400
        request	post https://api.whatsonchain.com/v1/bsv/test/tx/raw
        transaction	 {
......
}
        response	"Missing inputs"
......

运行失败了。我用......忽略了一些细节。经过关键信息Missing inputs咱们能够知道,失败缘由是要花费的UTXO不存在。这就符合逻辑了,迁移状态就须要花费该状态对应的UTXO,但这是个中间状态,output已经被花费过了,tx遭到矿工拒绝。

Bitcoin Computer用UTXO为模型,解决了合约执行的前后顺序问题。

合约权限

在_合约同步_这一节能够观察到这样一个细节:合约部署和合约同步两部分代码,在建立computer实例时,用了相同的助记词。若是咱们用不一样的助记词,在进行合约的同步和运行时会怎么样呢?接下来咱们就试试。
把合约同步部分的代码复制一份,改掉助记词部分,sync参数改成合约最新的outpoint 9407b32d7e5e701949a4b00accbd74f04c4fb651451904d77ec1a8ce56d334b4:0,而后运行。
首先,合约的同步是正确的。同步下来的counter变量内容为:

Counter { 
 
  
  n: 4,
  _rev: '9407b32d7e5e701949a4b00accbd74f04c4fb651451904d77ec1a8ce56d334b4:0',
  _id: '0a21910877dbfa8990eec667253f140089ea7834f3677f4cab4df3ae416cb379:0',
  _rootId: '0a21910877dbfa8990eec667253f140089ea7834f3677f4cab4df3ae416cb379:0'
}

接下来合约执行await counter.inc()语句失败,失败信息为

(node:26304) UnhandledPromiseRejectionWarning: Error:
        Communication Error
        message	Request failed with status code 400
        request	post https://api.whatsonchain.com/v1/bsv/test/tx/raw
        transaction	 {
......
}
        response	"16: mandatory-script-verify-flag-failed (Operation not valid with the current stack size)"
......

在_合约运行_小节里,咱们知道合约的脚本是以多签名为基础的,不一样的助记词没法计算出相同的公私钥对,所以没法解锁记录最新状态的UTXO,因此会产生上述失败。

是否可让多个私钥运行同一个合约呢?能够的。Bitcoin Computer的合约里有一个关键成员变量_owers用于管理合约的权限。咱们来看一个新的例子:

import Computer from 'bitcoin-computer';

(async () => { 
 
  
  const computerA = new Computer.default({ 
 
  
    seed: 'the mnemonic words',
    chain: 'BSV',
    network: 'testnet'
  });

  const computerB = new Computer.default({ 
 
  
    seed: 'different mnemonic words',
    chain: 'BSV',
    network: 'testnet'
  });

  class Counter { 
 
  
    constructor(n, pubKeys) { 
 
  
      this.n = n;
      this._owners = pubKeys;
    }
    inc() { 
 
  
      this.n += 1;
    }
  }

  const pubKeys = [computerA.db.wallet.getPublicKey().toString(), computerB.db.wallet.getPublicKey().toString()];
  const counter = await computerA.new(Counter, [0, pubKeys]);
  await counter.inc();
  console.log(counter);

  const syncCounter = await computerB.sync(counter._rev);
  await syncCounter.inc();
  console.log(syncCounter);
})();
  1. 咱们用两组不一样的助记词建立了两个Computer实例computerAcomputerB,每一个实例都有与对方不一样的公私钥对。
  2. 建立一个新的计数器合约,与以前的区别是在构造函数中增长了一个参数pubKeys,该参数用来表示一组公钥,同时该参数传给了Bitcoin Computer系统预留的成员变量_owners
  3. 咱们用computerA建立合约实例counter,并把computerAcomputerB的两个公钥都传给了合约。其中.db.wallet是Computer中的组件,能够用来获取助记词对应的公钥等信息。
  4. 合约部署成功后,用computerAcounter运行一次inc方法。观察结果。
  5. 而后再用computerB把计数器合约同步到syncCounter中,用syncCounter运行一次inc方法。观察结果。

先看computerA运行inc方法后的结果:

Counter {
  n: 1,
  _owners: [
    '03367b59cc6ba5cdb93b3bdc61c7018655462251b3608383c5a1b4adcf5f1bcc1f',
    '02c9788a60264523ba77500e19a0b2626c9b09b25daa16cfee09b4e1135d610c90'
  ],
  _id: 'dfd0a1a6792ff3fddc9277d8d18577ce0285a0c549ee77ec3adb0c4e4decc531:0',
  _rev: '2b302ed6eb74d92b51ce1491e9cc108a0f594aaa451efde9822b4968bb6fc3a3:0',
  _rootId: 'dfd0a1a6792ff3fddc9277d8d18577ce0285a0c549ee77ec3adb0c4e4decc531:0'
}

首先说明合约执行成功,咱们再经过区块链来查看ASM格式的合约部署脚本

1 03367b59cc6ba5cdb93b3bdc61c7018655462251b3608383c5a1b4adcf5f1bcc1f 
02c9788a60264523ba77500e19a0b2626c9b09b25daa16cfee09b4e1135d610c90 2 OP_CHECKMULTISIG 
7b225f5f636c73223a22636c61737320436f756e746572207b5c6e20202020636f6e7374727563746f72286e2c207075624b65797329207b5c6e202020202020746869732e6e203d206e3b5c6e202020202020746869732e5f6f776e657273203d207075624b6579733b5c6e202020207d5c6e20202020696e632829207b5c6e202020202020746869732e6e202b3d20313b5c6e202020207d5c6e20207d222c225f5f696e646578223a7b226f626a223a307d2c225f5f61726773223a5b302c5b22303333363762353963633662613563646239336233626463363163373031383635353436323235316233363038333833633561316234616463663566316263633166222c22303263393738386136303236343532336261373735303065313961306232363236633962303962323564616131366366656530396234653131333564363130633930225d5d2c225f5f66756e63223a22636f6e7374727563746f72227d OP_DROP

发现多签名中有两个公钥了,而不是以前咱们看到的一个,这两个公钥就对应了合约中_owners中的两个变量。说明Bitcoin Computer在部署时对_owners变量作了特殊处理,让该变量里的全部公钥都放入了多签中。若是不设置该参数,则只会放入建立者computer实例的公钥。
既然computerB的公钥也在多签里,那么咱们就能够预测computerB的合约执行也会成功。

computerBsyncCounter执行inc方法结果以下

Counter { 
 
  
  n: 2,
  _owners: [
    '03367b59cc6ba5cdb93b3bdc61c7018655462251b3608383c5a1b4adcf5f1bcc1f',
    '02c9788a60264523ba77500e19a0b2626c9b09b25daa16cfee09b4e1135d610c90'
  ],
  _rev: 'c6cfdc8dcbaa3b331641f21a26173227664d685b31ec366f4f246bbf28be07ba:0',
  _id: 'dfd0a1a6792ff3fddc9277d8d18577ce0285a0c549ee77ec3adb0c4e4decc531:0',
  _rootId: 'dfd0a1a6792ff3fddc9277d8d18577ce0285a0c549ee77ec3adb0c4e4decc531:0'
}

跟咱们预想的同样,用computerB也能够执行成功。

可见,Bitcoin Computer是经过多签名的方式来让多个拥有不一样私钥的用户执行同一个合约。

总结

Bitcoin Computer巧妙地将javascript、区块链、UTXO、多签名等融合在一块儿,创造了一个开发友好的二层合约解决方案。若是想进一步了解,能够参考官方文档。 之后将会继续对Bitcoin Computer作进一步探讨。