Bitcoin Computer是位元幣上的二層智能合約解決方案,該方案用Javascript語言撰寫智能合約,合約的代碼和呼叫都放到區塊鏈上,在鏈下進行合約狀態的計算和校驗,
合約代碼
我們以一個計數器合約為例來分析Bitcoin Computer的運行原理和特點,基本代碼如下:
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);
})()
- 創建
Computer實體,
在BSV的測驗網路上創建一個Computer實體,Computer是Bitcoin Computer的系統庫,可以通過npm等包管理工具進行安裝, - 定義
Counter合約,
實際上就是一個javascript類,該類的建構式中初始化了成員變數n的值,inc方法每被呼叫一次,成員變數n就會加1,實作了一個簡單的計數功能, - 部署合約,
通過computer的new方法,將Counter合約部署到區塊鏈上,第二個引數是Counter類的建構式的引數串列,也就是說我們在鏈上部署了一個Counter合約,合約的變數n的初始值為2,同時回傳了一個鏈上合約變數counter,
javascript關鍵字await說明部署合約是需要與外部互動的,很顯然這一步需要通過網路將代碼寫入區塊鏈, - 運行合約,
呼叫inc方法運行合約,讓計數器加1,這里也用了關鍵字await,說明合約的執行也是需要與區塊鏈互動的,
合約的部署、運行和同步
通過console.log陳述句列印出來的運行結果如下:
Counter {
n: 3,
_id: '0a21910877dbfa8990eec667253f140089ea7834f3677f4cab4df3ae416cb379:0',
_rev: '1aab4b23834b0502c15db98433d7eb50e5440f2a64b4a2553a81b655ae6e2696:0',
_rootId: '0a21910877dbfa8990eec667253f140089ea7834f3677f4cab4df3ae416cb379:0'
}
看到這個結果,我們首先可以猜測出n表示合約中成員變數的值,初始值為2,做了一次inc操作后值加1,也就是3,與猜測一致,
合約部署
觀察_id欄位,看值的格式,我們猜這也許是個txid,那我們打開區塊瀏覽器查查看,發現該tx的第0個output腳本部分用ASCII解碼后內容如下:
Q!6{Yìk¥í1;;üa??UF"Q3`????′-?_ì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
其中把可讀字串部分單獨提出來并格式化后內容如下:
{
"__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
腳本可以分兩個部分:
- 到
OP_CHECKMULTISIG為止,是一個多重簽名模板,這個多簽模板中只有一個公鑰,是個1/1簽名,說明這個UTXO只能被該公鑰對應的私鑰花費,可見,合約的運行是有權限控制的, - PUSH一段資料,然后再DROP掉,對比ASCII格式,我們知道這段資料實際上就是上面的合約部署資料,
同時,我們還可以發現0a21910877dbfa8990eec667253f140089ea7834f3677f4cab4df3ae416cb379:0表示的是一個outpoint,:前面為txid,后面為output index,
合約運行
觀察_rev欄位,這也是個outpoint,通過區塊鏈查詢發現ASCII格式內容如下:
Q!6{Yìk¥í1;;üa??UF"Q3`????′-?_ì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程序大致是這樣的:
- 通過函式引數定位到合約最新outpoint,
- 回溯整個tx鏈直到合約部署,
- 下載整個合約部署和運行資料,并在本地運行,算出最新狀態,
接下來再運行一次合約,結果如下:
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);
})();
- 我們用兩組不同的助記詞創建了兩個Computer實體
computerA和computerB,每個實體都有與對方不同的公私鑰對, - 創建一個新的計數器合約,與之前的區別是在建構式中增加了一個引數
pubKeys,該引數用來表示一組公鑰,同時該引數傳給了Bitcoin Computer系統預留的成員變數_owners, - 我們用
computerA創建合約實體counter,并把computerA和computerB的兩個公鑰都傳給了合約,其中.db.wallet是Computer中的組件,可以用來獲取助記詞對應的公鑰等資訊, - 合約部署成功后,用
computerA的counter運行一次inc方法,觀察結果, - 然后再用
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的合約執行也會成功,
computerB的syncCounter執行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做進一步探討,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/203372.html
標籤:其他
上一篇:NGK打造超強社區共識
