前言
本文主要介紹智能合約的工作原理及其部署過程。
合約部署流程
一般來說,部署智能合約的步驟為1:
- 啟動一個以太坊節點 (例如geth或者testrpc)。
- 使用solc編譯智能合約。 => 獲得二進制代碼。
- 將編譯好的合約部署到網絡。(這一步會消耗以太幣,還需要使用你的節點的默認地址或者指定地址來給合約簽名。) => 獲得合約的區塊鏈地址和ABI(合約接口的JSON表示,包括變量,事件和可以調用的方法)。(譯注:作者在這里把ABI與合約接口弄混 了。ABI是合約接口的二進制表示。)
- 用web3.js提供的JavaScript API來調用合約。(根據調用的類型有可能會消耗以太幣。)
下 圖表示了部署流程:
你的DApp可以給用戶提供一個界面先部署所需合約再使用之(如圖1到4步),也可以假設合約已經部署了(常見方法),直接從使用合約(如圖第6步)的界面開始。
智能合約實例
接下來我們將使用geth的控制台開發一個簡單的智能合約並編譯部署在私鏈上,最后與之交互。完成這些后,我們就能對智能合約的運行機制理解得更加深刻。本例子結合了汪曉明關於以太坊的開發的演示視頻和以太坊項目有關交易和合約的wiki。
打開測試網絡的控制台
輸入以下命令:
geth --datadir "~/ethdev" --dev console 2>> geth.log
- 1
顯示如下:
zcc@ubuntu:~$ geth --datadir "~/ethdev" --dev console 2>> geth.log
Welcome to the Geth JavaScript console!
instance: Geth/v1.4.18-stable/linux/go1.6.2 coinbase: 0xb005804a49e73acb17d1e7645dfd0a33dde6eb0e at block: 217 (Tue, 01 Nov 2016 05:21:38 PDT) datadir: /home/zcc/ethdev modules: admin:1.0 debug:1.0 eth:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 shh:1.0 txpool:1.0 web3:1.0 >
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
這樣我們就打開了測試網的控制台,之后的步驟如無特別說明都將在控制台中完成。
檢查編譯器
我們接下來的智能合約的例子是使用solidity語言開發的。因此,至此之前我們要確保solidity編譯器已經正確安裝了。輸入以下命令檢查:
> eth.getCompilers()
["Solidity"]
- 1
- 2
我們發現solidity的編譯器已經正確安裝了。如果返回值為空數組,那么輸入以下命令安裝:
sudo add-apt-repository ppa:ethereum/ethereum
sudo apt-get update
sudo apt-get install solc
- 1
- 2
- 3
如果輸入第一條命令的時候返回錯誤,請嘗試重啟系統。
編寫智能合約
我們編寫一個求解與7相乘結果的函數,即輸入一個值a
,返回a*7
的值。
> source = "contract test { function multiply(uint a) returns(uint d) { return a * 7; }}"
"contract test { function multiply(uint a) returns(uint d) { return a * 7; }}"
- 1
- 2
編譯智能合約
> contract = eth.compile.solidity(source).test
{
code: "0x606060405260388060106000396000f3606060405260e060020a6000350463c6888fa18114601c575b6002565b3460025760076004350260408051918252519081900360200190f3",
info: {
abiDefinition: [{
constant: false,
inputs: [...],
name: "multiply",
outputs: [...],
payable: false,
type: "function"
}],
compilerOptions: "--bin --abi --userdoc --devdoc --add-std --optimize -o /tmp/solc359648392",
compilerVersion: "0.4.3",
developerDoc: {
methods: {}
},
language: "Solidity",
languageVersion: "0.4.3",
source: "contract test { function multiply(uint a) returns(uint d) { return a * 7; }}",
userDoc: {
methods: {}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
准備一個創建合約的賬戶
在以太坊上創建智能合約就是使用一個外部賬戶(EOA)向區塊鏈中發送一個交易。因此,我們需要准備一個有余額並被激活的以太坊外部賬戶。
查看是否有可用賬戶:
> personal.listAccounts
[]
- 1
- 2
返回為空,說明沒有可用賬戶。創建一個外部賬戶:
> personal.newAccount('123456')
"0x62b1746767522b36f6421e630fa0198151d72964"
- 1
- 2
注意:personal.newAccount()
函數里的參數是賬號密碼,返回值是創建的新賬號地址。
這個時候,我們再次使用personal.listAccounts
命令查看可用賬戶:
> personal.listAccounts
["0x62b1746767522b36f6421e630fa0198151d72964"]
- 1
- 2
我們看到函數返回值為一個數組,數組目前只有一個元素,就是我們剛才創建的賬號。
我們查看一下剛才創建的賬戶余額:
> web3.eth.getBalance(personal.listAccounts[0])
0
- 1
- 2
返回值為0,說明新創建的賬戶沒有以太幣。這個時候我們就可以開啟挖礦來獲得以太幣。
首先開啟挖礦:
> miner.start()
true
- 1
- 2
為了檢測挖礦的狀態,我們可以再另開起一個終端用於檢測挖礦的狀態。在新開起的終端中輸入以下命令實時顯示挖礦的狀態:
tail -f geth.log
- 1
這樣我們就能看到如下所示的挖礦狀態:
zcc@ubuntu:~$ tail -f geth.log
I1102 10:10:21.382666 eth/backend.go:201] Blockchain DB Version: 3
I1102 10:10:21.382691 eth/backend.go:226] ethash used in test mode
I1102 10:10:21.384471 core/blockchain.go:214] Last header: #219 [7a2335e9…] TD=28838912
I1102 10:10:21.384501 core/blockchain.go:215] Last block: #219 [7a2335e9…] TD=28838912
I1102 10:10:21.384507 core/blockchain.go:216] Fast block: #219 [7a2335e9…] TD=28838912
I1102 10:10:21.389663 p2p/server.go:313] Starting Server
I1102 10:10:23.428074 p2p/discover/udp.go:217] Listening, enode://c7afa32e281c06bb529b9c0f0d9537ab4386cac1b2ba7de6bd777bdb15e3b7d0cbd260fc57f82b8f33b0c0ff83772b7d4da7c68017f8e05a2c08c77754d6ed62@[::]:39428
I1102 10:10:23.429805 whisper/whisper.go:176] Whisper started
I1102 10:10:23.439973 p2p/server.go:556] Listening on [::]:46364
I1102 10:10:23.440985 node/node.go:296] IPC endpoint opened: /home/zcc/ethdev/geth.ipc
I1102 10:13:11.541025 miner/miner.go:119] Starting mining operation (CPU=1 TOT=2)
I1102 10:13:11.541389 miner/worker.go:539] commit new work on block 220 with 0 txs & 0 uncles. Took 196.057µs
I1102 10:13:11.541464 ethash.go:259] Generating DAG for epoch 0 (size 32768) (0000000000000000000000000000000000000000000000000000000000000000)
I1102 10:13:11.541625 ethash.go:291] Generating DAG: 0%
I1102 10:13:11.541678 ethash.go:291] Generating DAG: 1%
I1102 10:13:11.541716 ethash.go:291] Generating DAG: 2%
I1102 10:13:11.541751 ethash.go:291] Generating DAG: 3%
I1102 10:13:11.541787 ethash.go:291] Generating DAG: 4%
I1102 10:13:11.541822 ethash.go:291] Generating DAG: 5%
...
I1102 10:13:11.547403 ethash.go:291] Generating DAG: 95%
I1102 10:13:11.547450 ethash.go:291] Generating DAG: 96%
I1102 10:13:11.547497 ethash.go:291] Generating DAG: 97%
I1102 10:13:11.547543 ethash.go:291] Generating DAG: 98%
I1102 10:13:11.547590 ethash.go:291] Generating DAG: 99%
I1102 10:13:11.547646 ethash.go:291] Generating DAG: 100%
I1102 10:13:11.547691 ethash.go:276] Done generating DAG for epoch 0, it took 6.229048ms
I1102 10:13:11.548248 eth/backend.go:454] Automatic pregeneration of ethash DAG ON (ethash dir: /home/zcc/.ethash)
I1102 10:13:11.548375 eth/backend.go:461] checking DAG (ethash dir: /home/zcc/.ethash)
I1102 10:13:24.938687 miner/worker.go:342] �� Mined block (#220 / 1fdebe6d). Wait 5 blocks for confirmation
I1102 10:13:24.938953 miner/worker.go:539] commit new work on block 221 with 0 txs & 0 uncles. Took 86.067µs
I1102 10:13:24.939156 miner/worker.go:539] commit new work on block 221 with 0 txs & 0 uncles. Took 66.063µs
I1102 10:13:29.874128 miner/worker.go:342] �� Mined block (#221 / ff995b19). Wait 5 blocks for confirmation
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
因為我之前已經挖過礦了,所以可以看到現在開始挖的區塊編號為220。
再次使用web3.eth.getBalance(personal.listAccounts[0])
命令來查看新建立的賬號的余額:
> web3.eth.getBalance(personal.listAccounts[0])
65000000000000000000
- 1
- 2
這時我們看到當前賬戶余額為6.5e19wei,即65ether。我們可以參看Denominations來互相轉換以太幣的幾種單位。
至此,用於創建合約的外部賬戶已經准備完畢。
指定創建合約的外部賬戶
我們首先需要從當前的賬戶里選擇一個作為創建智能合約的外部賬戶:
> address = eth.accounts[0]
"0x62b1746767522b36f6421e630fa0198151d72964"
- 1
- 2
然后將該賬戶激活:
> personal.unlockAccount(address)
Unlock account 0x62b1746767522b36f6421e630fa0198151d72964
Passphrase:
true
- 1
- 2
- 3
- 4
我們在使用personal.unlockAccount()
函數的時候,可以選擇指定一個解鎖時間(以秒為單位),超出這個時間后會自動上鎖。如果不指定的話,那么用戶的賬戶就會在一定時間后自動上鎖。例如:
> personal.unlockAccount(address,'123456',10000)
true
- 1
- 2
personal.unlockAccount()
函數里第一個參數是要解鎖的賬戶,第二個參數是賬號密碼,第三個是需要解鎖的時間。在這個例子中,我們設定在10000秒內賬戶都處於激活狀態。設定賬號的解鎖時間是個好方式,尤其是之后我們需要使用這個賬號再次進行交易操作時,這樣就省去了再次解鎖賬號的麻煩。不過,解鎖時間不應設定過長,否則可能有安全隱患。
部署合約
部署合約就是將編譯好的合約字節碼通過外部賬號發送交易的形式部署到以太坊區塊鏈上。輸入以下命令:
> abi = [{constant:false,inputs:{name:'a',type:'uint256'}}]
[{
constant: false,
inputs: {
name: "a",
type: "uint256"
}
}]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
> MyContract = eth.contract(abi)
{
abi: [{
constant: false,
inputs: {
name: "a",
type: "uint256"
}
}],
eth: {
accounts: ["0x62b1746767522b36f6421e630fa0198151d72964"],
blockNumber: 292,
coinbase: "0x62b1746767522b36f6421e630fa0198151d72964",
compile: {
lll: function(),
serpent: function(),
solidity: function()
},
defaultAccount: undefined,
defaultBlock: "latest",
gasPrice: 20000000000,
hashrate: 3498,
mining: true,
pendingTransactions: [],
syncing: false,
call: function(),
contract: function(abi),
estimateGas: function(),
filter: function(fil, callback),
getAccounts: function(callback),
getBalance: function(),
getBlock: function(),
getBlockNumber: function(callback),
getBlockTransactionCount: function(),
getBlockUncleCount: function(),
getCode: function(),
getCoinbase: function(callback),
getCompilers: function(),
getGasPrice: function(callback),
getHashrate: function(callback),
getMining: function(callback),
getNatSpec: function(),
getPendingTransactions: function(callback),
getStorageAt: function(),
getSyncing: function(callback),
getTransaction: function(),
getTransactionCount: function(),
getTransactionFromBlock: function(),
getTransactionReceipt: function(),
getUncle: function(),
getWork: function(),
iban: function(iban),
icapNamereg: function(),
isSyncing: function(callback),
namereg: function(),
resend: function(),
sendIBANTransaction: function(),
sendRawTransaction: function(),
sendTransaction: function(),
sign: function(),
signTransaction: function(),
submitTransaction: function(),
submitWork: function()
},
at: function(address, callback),
getData: function(),
new: function()
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
> myContract = MyContract.new({from:address,data:contract.code})
{
abi: [{
constant: false,
inputs: {
name: "a",
type: "uint256"
}
}],
address: undefined,
transactionHash: "0xd10602e2099ab5873c762f070eb90a9fd559270484fbcebd4170d441848b9232"
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
這時,我們可以檢查一下交易池,查看當前交易的待處理狀況:
> txpool.status
{ pending: 1, queued: 0 }
- 1
- 2
- 3
- 4
- 5
我們可以看到當前的交易池中有一個交易正在等待確認。然后,我們查看待確認交易的詳細內容:
> eth.getBlock("pending",true).transactions
[{
blockHash: "0x0299731121321b817206fb07187f94cd4537e3196e940b45e95b4e1709aadbf4",
blockNumber: 294,
from: "0x62b1746767522b36f6421e630fa0198151d72964",
gas: 90000,
gasPrice: 20000000000,
hash: "0xd10602e2099ab5873c762f070eb90a9fd559270484fbcebd4170d441848b9232",
input: "0x606060405260388060106000396000f3606060405260e060020a6000350463c6888fa18114601c575b6002565b3460025760076004350260408051918252519081900360200190f3",
nonce: 0,
to: null,
transactionIndex: 0,
value: 0
}]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
我們從顯示出來的結果可以看出當前交易的一些內容。例如,from
數據項就是我們發送交易的地址,input
就是合約編譯完成的字節碼,這些內容均與我們之前的設定相同。而且,我們可以看到新的交易創建在第294號區塊中。
與此同時,我們也可以查看一下剛才在新的終端中創建的挖礦日志。我們在日志中找到這樣的一行信息:
I1102 11:37:46.573298 eth/api.go:1183] Tx(0xd10602e2099ab5873c762f070eb90a9fd559270484fbcebd4170d441848b9232) created: 0x115ced3f8b7ea92d324902e3a3a421a07540eb2b
- 1
這說明交易已經發送到區塊鏈中了,正在等待礦工的確認。
耐心等待一段時間,等待礦工確認完成后,我們再次使用txpool.status
命令查看交易池的狀態:
> txpool.status
{ pending: 0, queued: 0 }
- 1
- 2
- 3
- 4
- 5
我們發現交易池已經沒有待確認的交易了。我們使用eth.getBlock(294)
命令查看第294號區塊的信息:
> eth.getBlock(294)
{ difficulty: 131072, extraData: "0xd783010412844765746887676f312e362e32856c696e7578", gasLimit: 4712388, gasUsed: 36946, hash: "0x4da580cce8bc5ed34aa5a7bbeeb730d98cd7b425698f40433365f1463bc572ee", logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", miner: "0x62b1746767522b36f6421e630fa0198151d72964", nonce: "0x02956290420fe41f", number: 294, parentHash: "0x3fd3a8126d25dec98aaf2696c2812de2d2f4f28bae729dacd78634f046d8a1cc", receiptRoot: "0x44c28d68986fb814e431117f3759b24d5e668feeff8dacaadb7c08fd11b4f2fc", sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", size: 696, stateRoot: "0xab235f738e080d41efb4692633401917dfb9f04625d9476b6875e5d335f1b014", timestamp: 1478057886, totalDifficulty: 38670080, transactions: ["0xd10602e2099ab5873c762f070eb90a9fd559270484fbcebd4170d441848b9232"], transactionsRoot: "0xc983a661a81a1173bd0196a44e8d1092250f6c6a21acc1847cd7416de9d76f55", uncles: [] }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
我們發現hash值為0xd10602e2099ab5873c762f070eb90a9fd559270484fbcebd4170d441848b9232
的交易確實在第294號區塊中。
與合約交互
首先我們需要使用eth.contract
來定義一個合約類,定義的合約類遵從ABI定義2:
> Multiply7 = eth.contract(contract.info.abiDefinition)
{
abi: [{
constant: false,
inputs: [{...}],
name: "multiply",
outputs: [{...}],
payable: false,
type: "function"
}],
eth: {
accounts: ["0x62b1746767522b36f6421e630fa0198151d72964"],
blockNumber: 346,
coinbase: "0x62b1746767522b36f6421e630fa0198151d72964",
compile: {
lll: function(),
serpent: function(),
solidity: function()
},
defaultAccount: undefined,
defaultBlock: "latest",
gasPrice: 20000000000,
hashrate: 39840,
mining: true,
pendingTransactions: [],
syncing: false,
call: function(),
contract: function(abi),
estimateGas: function(),
filter: function(fil, callback),
getAccounts: function(callback),
getBalance: function(),
getBlock: function(),
getBlockNumber: function(callback),
getBlockTransactionCount: function(),
getBlockUncleCount: function(),
getCode: function(),
getCoinbase: function(callback),
getCompilers: function(),
getGasPrice: function(callback),
getHashrate: function(callback),
getMining: function(callback),
getNatSpec: function(),
getPendingTransactions: function(callback),
getStorageAt: function(),
getSyncing: function(callback),
getTransaction: function(),
getTransactionCount: function(),
getTransactionFromBlock: function(),
getTransactionReceipt: function(),
getUncle: function(),
getWork: function(),
iban: function(iban),
icapNamereg: function(),
isSyncing: function(callback),
namereg: function(),
resend: function(),
sendIBANTransaction: function(),
sendRawTransaction: function(),
sendTransaction: function(),
sign: function(),
signTransaction: function(),
submitTransaction: function(),
submitWork: function()
},
at: function(address, callback),
getData: function(),
new: function()
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
然后得到合約實例:
> myMultiply7 = Multiply7.at(myContract.address)
{
abi: [{
constant: false,
inputs: [{...}],
name: "multiply",
outputs: [{...}],
payable: false,
type: "function"
}],
address: "0x115ced3f8b7ea92d324902e3a3a421a07540eb2b",
transactionHash: null,
allEvents: function(),
multiply: function()
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
我們可以看到,在實例中能夠調用的函數有兩個:allEvents
和multiply
。multiply
顯然是一開始我們定義合約的時候寫的函數。我們可以使用兩種方法來調用multiply
函數:sendTransaction(3, {from: address})
和call(3)
。
方法一:使用sendTransaction(3, {from: address})
調用:
> myMultiply7.multiply.sendTransaction(3, {from:address})
"0x1c46e9e85f5db00ba2bcbb2e8774f61cdb81620a46a19ea9cf6a532795d5950c"
- 1
- 2
我們可以發現返回的值為一個字符串,這個字符串代表的是發送的交易的hash值。使用這個方法調用合約將會使調用的結果成為全局共識的一部分3。
方法二:使用call(3)
調用:
> myMultiply7.multiply.call(3)
21
- 1
- 2
我們可以發現返回值為21,這個值剛好是3*7
的運算結果。使用這個方法調用合約只會在本地上運行4。
從以上兩種方法的調用結果來看,如果你只想得知運算的結果,那么只需要使用方法二。而如果想要改變合約的狀態,那么就使用方法一。
參考資料
- Contracts and Transactions:以太坊客戶端geth的wiki里編寫智能合約的詳細流程和例子。
- Contracts:以太坊文檔中有關智能合約編寫、編譯、部署和交互等的例子。
- 明說(04):如何開發編譯部署調用智能合約?:這個是汪曉明演示在以太坊geth命令行上編譯部署智能合約的視頻。