ATourofEthereum——區塊鏈與智慧合約之旅

dotdot發表於2018-03-13

Welcome

以前粗略瞭解過一些比特幣和區塊鏈的理論知識,利用本次休假的時間,趁機熟悉一下Ethereum 和 Solidity,嘗試建立一個簡單的只能合約。

What is geth

本文中所有的操作都在 geth 中執行,geth是一個全功能的以太坊節點,它使用golang語言實現。geth 為我們提供了區塊資訊檢視、挖礦、執行智慧合約等功能。

ethereum 和 bitcoin 的一些區別
1、ethereum 的裡的指令碼程式碼是圖靈完備的,而比特幣的指令碼則是基於棧的,有一定侷限性。
2、ethereum 基於賬戶模型,沒有UXTO(Unspent Transaction Outputs)概念,交易速度會慢一些。

How to install geth

如果你已經安裝了 homebrew Mac 上 安裝 geth 非常容易:

brew tap ethereum/ethereum
brew install ethereum

其他的安裝方式可以參考文件:
https://ethereum.gitbooks.io/frontier-guide/content/installing_linux.html

Run geth

在使用geth前有兩個重要的引數需要指定:
1、 –datadir “~/ethmain” datadir 指定在哪裡儲存區塊的資料(包含賬戶)
2、–networkid “1” 指定網路ID,約定1代表主網路也就是以太坊的公共網路, 通常用2代表測試網路, 我們可以指定任意非負ID,作為自己的私有鏈。

Create own private ethereum

本文的目標是建立一個私有鏈,並在該鏈上挖礦和執行一個簡單的智慧合約。
1、設定資料的儲存目錄為: “~/ethprivate”
2、網路ID :“9527”

Before create

在建立私有鏈前我們先建立幾個外部賬戶,方便後續操作,以太坊的賬號分為外部賬戶和合約賬戶兩種,本次將建立兩個外部賬戶。
使用 geth account 指令可以進行賬戶相關的操作。

使用 geth account new 指令建立賬戶,如果在js 控制檯中(後面會說明) 則可以用 personal.newAccount(“passphrase”) 指令

geth --datadir "~/ethprivate" --networkid 9527  account new 

指令執行後要求輸入 passphrase 設定一個簡單的口令即可 如:“12345”:

Your new account is locked with a password. Please give a password. Do not forget this password.
Passphrase: 
Repeat passphrase: 
Address: {0df066922b375d8641b992352958664ff26b570d}

執行 geth account list 可以檢視已有的賬戶(在js控制檯裡使用 eth.accounts)

geth --datadir "~/ethprivate" --networkid 9527  account list

可以檢視到剛才建立的賬戶

Account #0: {0df066922b375d8641b992352958664ff26b570d} keystore:///${HOME}/ethprivate/keystore/UTC--2018-02-13T03-27-14.788253000Z--0df066922b375d8641b992352958664ff26b570d

The genesis block

現在準備建立第一個區塊,通常稱該區塊為創世塊。在創世塊裡我們需要進行一些簡單的配置,主要是挖礦的難度設定、發放一些貨幣到剛建立的賬戶中。通過一個json檔案設定,下面是詳細配置

{
    "config": {
        "chainId": 9527,
        "homesteadBlock": 0,
        "eip155Block": 0,
        "eip158Block": 0
    },
  "nonce": "0x0000000000000042",
  "timestamp": "0x2537",
  "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "extraData": "0x2537",
  "gasLimit": "0x80000000",
  "difficulty": "0x10",
  "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "coinbase": "0x3333333333333333333333333333333333333333",
  "alloc": {   
      "0df066922b375d8641b992352958664ff26b570d":{
          "balance":"10"
      }
    }
}

將上面的配置儲存為檔案.json 的檔案,然後執行初始化指令:

geth --datadir "~/ethprivate" --networkid 9527  init genesis.json 

成功建立後會輸出以下資訊

INFO [02-13|11:51:54] Allocated cache and file handles         database=${HOME}/ethprivate/geth/chaindata cache=16 handles=16
INFO [02-13|11:51:54] Writing custom genesis block 
INFO [02-13|11:51:54] Successfully wrote genesis state         database=chaindata                                 hash=eb3e89…697b61
INFO [02-13|11:51:54] Allocated cache and file handles         database=${HOME}/ethprivate/geth/lightchaindata cache=16 handles=16
INFO [02-13|11:51:54] Writing custom genesis block 
INFO [02-13|11:51:54] Successfully wrote genesis state         database=lightchaindata                                 hash=eb3e89…697b61

The JavaScript Console

geth 提供了一個Javascript 執行環境,使用 geth console 命令可以進入JS Console。在JS Console可以使用更多的操作,後面的智慧合約部署也將在 JS Console進行。

這裡建議將標準錯誤重定向到另一個檔案裡

geth console 2>>geth.log 

並在另一個終端使用 tail -f geth.log 監控輸出。

geth --datadir "~/ethprivate" --networkid 9527  console 2>>geth.log

Welcome to the Geth JavaScript console!

instance: Geth/v1.7.3-stable/darwin-amd64/go1.9.4
coinbase: 0x0df066922b375d8641b992352958664ff26b570d
at block: 0 (Thu, 01 Jan 1970 10:38:47 CST)
datadir: ~/ethprivate
modules: admin:1.0 debug:1.0 eth:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0

查詢一下第一個賬戶的餘額,我們在創世塊裡向其預先分配了 10個幣:

> eth.getBalance(eth.accounts[0])
10

檢視一下創世塊的資訊

>eht.getBlock(0)

{
  difficulty: 16,
  extraData: "0x00",
  gasLimit: 2147483648,
  gasUsed: 0,
  hash: "0xeb3e8947d2e01afbb6651c0276921f5f44b4c549a89942285716d935aa697b61",
  logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  miner: "0x3333333333333333333333333333333333333333",
  mixHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
  nonce: "0x0000000000000042",
  number: 0,
  parentHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
  receiptsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
  sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
  size: 507,
  stateRoot: "0x2b4a324f7683f0f14efef5b1f5c6ac9432b223da4e9ce56ecbc47482402c13d1",
  timestamp: 9527,
  totalDifficulty: 16,
  transactions: [],
  transactionsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
  uncles: []
}

輸入 exit 可以退出 JS console;

Other Commands

進入 console 時能看到其載入的js modules

Welcome to the Geth JavaScript console!

instance: Geth/v1.7.3-stable/darwin-amd64/go1.9.4
coinbase: 0x0df066922b375d8641b992352958664ff26b570d
at block: 0 (Thu, 01 Jan 1970 10:38:47 CST)
datadir: ~/ethprivate
modules: admin:1.0 debug:1.0 eth:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0
  1. admin 檢視一些網路和 節點相關的資訊

    如:admin.nodeInfo
    
  2. eth 進行一些區塊相關的操作
    如: eth.accounts() 可以檢視當前的賬戶列表

        eth.getBlock(N) 可以檢視具體的區塊資訊。
        eth.getBalance() 檢視某個賬戶的金額
    
  3. admin 檢視一些網路和 節點相關的資訊
    如:admin.nodeInfo
  4. personal 可以進行一些賬號相關的操作,
    如果控制檯丟擲以下錯誤
Error: authentication needed: password or unlock
    at web3.js:3143:20
    at web3.js:6347:15
    at web3.js:5081:36
    at web3.js:4137:16
    at <anonymous>:1:1

說明某項命令需要解鎖賬戶,可以通過:

personal.unlockAccount(eth.accounts[0],"passwd")

因為本身是JS 所以可以在控制檯裡直接檢視某個Module物件支援的屬性和方法。
geth-net.png

The miner

通過miner 提供的函式可以進行挖礦,挖礦會將算出新的區塊,並將交易池中的交易打包到區塊裡。挖礦的獎勵預設發放到第一個賬戶中。
在控制檯中輸入下面指令啟動挖礦。

miner.start()

在我們監控的 log 檔案裡能看到挖礦的資訊

Commit new mining work                   number=8 txs=0 uncles=0 elapsed=139.017µs
INFO [02-13|13:47:08] Successfully sealed new block            number=8 hash=39f5db…99b268

使用eth.blockNumber 可以看到當前的已經挖出了多少區塊。
使用 eth.getBlock(N) 可以檢視具體的區塊資訊。

> eth.getBalance(eth.accounts[0])
115000000000000000010 

可以看到挖礦的獎勵已經發放到第一個賬戶中。

使用

miner.stop()

可以停止挖礦。

Transactions

因為挖礦的獎勵都投放到了第一個賬戶中,我們再建立一個賬戶,將部分獎勵轉到新賬戶上。這就是傳說中的“轉賬”功能。

> eth.getBalance(eth.accounts[0])
150000000000000000010
> eth.getBalance(eth.accounts[1])
0

在js 控制檯上的轉賬很簡單,參考一下指令

> user1=eth.accounts[0]
"0x0df066922b375d8641b992352958664ff26b570d"
> user2=eth.accounts[1]
"0x87eaff177048b0931475efd0b7837b67c91b7232"
> personal.unlockAccount(user1,"12345")
true

> eth.sendTransaction({from:user1,to:user2,value:web3.toWei(100,"ether")})

轉賬前如果賬戶是被鎖住的,需要使用密碼進行解鎖。

查詢一下,指令執行後的結果

> eth.getBalance(user1)
150000000000000000010
> eth.getBalance(user2)
0

發現金額並沒有減少, 原因是這個交易只是進入了交易池,還沒有打包到區塊裡,也就是沒有被真正的認可。
使用txpool.status 可以待交易的資料。

> txpool.status
{
  pending: 1,
  queued: 0
}

啟動挖礦完成交易

> miner.start(1)
null
> miner.stop()
true
> eth.getBalance(user2)
3000000000000000000
> txpool.status
{
  pending: 0,
  queued: 0
}

檢視挖礦時的日誌輸出:

INFO [02-22|10:32:09] Starting mining operation 
INFO [02-22|10:32:09] Commit new mining work                   number=31 txs=1 uncles=0 elapsed=416.345µs
INFO [02-22|10:32:18] Successfully sealed new block            number=31 hash=6b9c9d…0fd396
INFO [02-22|10:32:18] ? mined potential block                  number=31 hash=6b9c9d…0fd396
INFO [02-22|10:32:18] Commit new mining work                   number=32 txs=0 uncles=0 elapsed=102.669µs
INFO [02-22|10:32:19] Successfully sealed new block            number=32 hash=94bdc9…2c48da
INFO [02-22|10:32:19] ? mined potential block                  number=32 hash=94bdc9…2c48da

可以看到在第31一個區塊下有一個 “txs=1” 交易被打包。
第31個Block的資訊:

> eth.getBlock(31)
{
  difficulty: 131072,
  extraData: "0xd883010703846765746887676f312e392e348664617277696e",
  gasLimit: 2083415374,
  gasUsed: 21000,
  hash: "0x6b9c9d4fec6a61b4b4294393340613084e96f6b14c3931697225ecdb130fd396",
  logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  miner: "0x0df066922b375d8641b992352958664ff26b570d",
  mixHash: "0xe66e02b7e4e0797c5e125b42c2e6ac333de3d05c20c6050922652a1466c8b5f4",
  nonce: "0x2edd4febb730901b",
  number: 31,
  parentHash: "0xb04cbb2b541c7a5240d7d5bef817ca8717fdf156fb1e1372971e63676c2a86e1",
  receiptsRoot: "0x1b6be00820be77d7acc990b0fba653a758eeadd7018c9854f09645c0dd00a259",
  sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
  size: 653,
  stateRoot: "0xe06d4c3ec4245c189ee415b965876c8c937f5321b7f8f1175e7f1bd97bcead31",
  timestamp: 1519266729,
  totalDifficulty: 4080784,
  transactions: ["0x4a9e719868f7713cd450c9c69d0ad87995999fac1be27d119d1ba24b9bd31c96"],
  transactionsRoot: "0x5ca299ef95bfc52addf5a3a344d71fff750562ab97316476a5b44954089aefd7",
  uncles: []
}

看到 transactions 裡包含了:[“0x4a9e719868f7713cd450c9c69d0ad87995999fac1be27d119d1ba24b9bd31c96”] 這是該交易的hash索引。

> eth.getTransaction("0x4a9e719868f7713cd450c9c69d0ad87995999fac1be27d119d1ba24b9bd31c96")
{
  blockHash: "0x6b9c9d4fec6a61b4b4294393340613084e96f6b14c3931697225ecdb130fd396",
  blockNumber: 31,
  from: "0x0df066922b375d8641b992352958664ff26b570d",
  gas: 90000,
  gasPrice: 18000000000,
  hash: "0x4a9e719868f7713cd450c9c69d0ad87995999fac1be27d119d1ba24b9bd31c96",
  input: "0x",
  nonce: 0,
  r: "0xfcb2fc5a36cd29b2af329714a1776b35fa6f45f8b0b68d4e6b08269e95fbde67",
  s: "0x64afcee6177262bf84ce416ac9c9ff6983394f9a6c686ee8f72f3d6fb88619d7",
  to: "0x87eaff177048b0931475efd0b7837b67c91b7232",
  transactionIndex: 0,
  v: "0x4a91",
  value: 3000000000000000000
}
> eth.accounts
["0x0df066922b375d8641b992352958664ff26b570d", "0x87eaff177048b0931475efd0b7837b67c91b7232"]

從 from 、to 、value 的資料裡可以看到剛才的轉賬資訊。
至此已經成功完成一筆轉賬交易。

A sample contract

我們將定義一個簡單的智慧合約,該合約提供付費刻字功能,可以將使用者的名字燒錄到區塊鏈裡,類似於“到此一遊” %>_<%

Writing a contract

下面是相關的solidity 程式碼,呼叫carve 時需要支付大於 costForCarve 的費用。

pragma solidity ^0.4.20;
contract CarveName {
    address owner;
    string[] public names; 
    mapping (address=>uint) nameToOwner;
    uint costForCarve = 1 wei;

    event OnNameCarve(uint nameId,string name);

    function CarveName() public {
        owner = msg.sender;
    }

    function carve(string _name) external payable returns (uint) {
        require(msg.value>=costForCarve);
        uint id = names.push(_name);
        nameToOwner[msg.sender] = id;
        OnNameCarve(id,_name);
        return id;
    }

    function getNameById(uint _id) external view returns(string) {
        return names[_id];
    }

   function setCost(uint _cost) external onlyOwner {
       costForCarve = _cost;
   }

   function withdrawCash() external onlyOwner {
        owner.transfer(this.balance);
   }

   modifier onlyOwner() {
    require(msg.sender == owner);
    _;
  }

}

Compiling and deploying

為了簡單方便,本次先使用線上的編譯器Solidity瀏覽器–Remix 進行編譯。

編譯後獲得ABI

[{"constant":false,"inputs":[{"name":"_cost","type":"uint256"}],"name":"setCost","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"names","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_name","type":"string"}],"name":"carve","outputs":[{"name":"","type":"uint256"}],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[{"name":"_id","type":"uint256"}],"name":"getNameById","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"withdrawCash","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"nameId","type":"uint256"},{"indexed":false,"name":"name","type":"string"}],"name":"OnNameCarve","type":"event"}]

對應的BYTECODE

606060405266038d7ea4c68000600355341.................

回到控制檯,利用上面的資料部署合約。

1、通過獲得的“ABI”建立合約物件,其實就是建立一個js 物件。

  //將ABI 資料轉換為json
  > carveNameAbi=JSON.parse(`....`)
  //建立 Contract 物件
  > carveNameContract = eth.contract(carveNameAbi)
  //輸入bytecod,注意需要以16進製表示,即0x 開頭
  > carveNameBytecode="0x..............."
  //預估一下部署合約損耗的Gas
  > eth.estimateGas({data:carveNameBytecode})
    615383
  > eth.getBalance(eth.coinbase)
    182000000000000000010
 //部署合約,即提交一個交易
  > contractInterface = carveNameContract.new({data:carveNameBytecode,gas:625383,from:eth.coinbase})

  //以下是輸出:
  {
  abi: [....],
  address: undefined,
  transactionHash: "0x30a506b4d5c970ca0316c03e840ab8094346f5d981b18d5e09ce477f208f938f"
    }
> txpool.status
{
  pending: 1,
  queued: 0
}
  

此時合約的部署還在待交易池中,啟動挖礦完成交易。檢視挖礦時的輸出日誌輸出:

  Submitted contract creation              fullhash=0x559ad19c5e70c433febef9eb494c827d831e41b15ca596d35ee20a546aed2b5e contract=0x75ce54374b6f9CC304Ec2a912dCd54078BD163cd

Starting mining operation 
INFO [02-22|15:21:29] Commit new mining work                   number=42 txs=1 uncles=0 elapsed=561.894µs
INFO [02-22|15:21:29] Successfully sealed new block            number=42 hash=4426e8…df6472
INFO [02-22|15:21:29] ? block reached canonical chain          number=37 hash=dcbccd…141029
INFO [02-22|15:21:29] ? mined potential block                  number=42 hash=4426e8…df6472

可以看到交易已記錄在第42個區塊上,同時獲得了合約的地址:contract=0x75ce54374b6f9CC304Ec2a912dCd54078BD163cd ,該地址在執行合約時使用。

完整的 ContractInterface 資訊如下:

{
  abi: [....],
  address: "0x75ce54374b6f9cc304ec2a912dcd54078bd163cd",
  transactionHash: "0x559ad19c5e70c433febef9eb494c827d831e41b15ca596d35ee20a546aed2b5e",
  OnNameCarve: function(),
  allEvents: function(),
  carve: function(),
  getNameById: function(),
  names: function(),
  setCost: function(),
  withdrawCash: function()
}

The transactions about contract

在js 控制檯,通過 eth.getBlock 查詢區塊上的交易資訊,本例中合約部署的資訊寫在了第42個區塊上,
使用eth.getBlock 檢視區塊的資訊,找到交易的地址。

> eth.getBlock(42)
{
  difficulty: 131072,
  extraData: "0xd883010703846765746887676f312e392e348664617277696e",
  gasLimit: 2069214973,
  gasUsed: 615383,
.....
  transactions: ["0x559ad19c5e70c433febef9eb494c827d831e41b15ca596d35ee20a546aed2b5e"],
  transactionsRoot: "0xa731fe9030b335a49e46c557d37717fba5fc045fd39400575e58d21282da0eb7",
  uncles: []
}

通過eth.getTransactionReceipt 查詢交易的詳細資訊:

> eth.getTransactionReceipt("0x559ad19c5e70c433febef9eb494c827d831e41b15ca596d35ee20a546aed2b5e")
{
  blockHash: "0x4426e8e1680f290432a571871f2b2f97bb83b0b6603874665bcab44792df6472",
  blockNumber: 42,
  contractAddress: "0x75ce54374b6f9cc304ec2a912dcd54078bd163cd",
  cumulativeGasUsed: 615383,
  from: "0x0df066922b375d8641b992352958664ff26b570d",
  gasUsed: 615383,
 ........
  transactionHash: "0x559ad19c5e70c433febef9eb494c827d831e41b15ca596d35ee20a546aed2b5e",
  transactionIndex: 0
}

可以看到交易的詳細資訊裡包含了合約的地址:contractAddress: “0x75ce54374b6f9cc304ec2a912dcd54078bd163cd”,

Other people run The code

在使用他人釋出的合約前需要通過 ABI 和 contract Address 可以獲取建立合約物件,和部署合約程式碼不同的是,部署時使用new方法,而獲取則使用 at 方法。

> contract=eth.contract(abi)
> interface = contract.at(address)

有了介面物件後就可以呼叫合約上面的方法了,方法的呼叫可以分成兩種。

   > interface.carve.call("Hello 9527",{from:eth.accounts[0],value:web3.toWei(1)})
   1

這裡使用 call 方式呼叫,此時只是本地呼叫,不會產生交易,不會損耗Gas。

> interface.carve.sendTransaction("Hello 9527",{from:eth.accounts[0],value:web3.toWei(1)})
"0xb20a62ce226e77f766cbb540ba7dccdd15981227cc1f9932db9db12affdbca15"

使用sendTransaction 的方式抵用合約函式,會建立一個交易。
啟動挖礦,等待礦工打包交易完成,完成後可以通過下面的指令查詢到已寫入的資料

> interface.getNameById.call(0)
"Hello 9527"

至此一個簡單的智慧合約已執行完備。

Thinking about blockchain

Q:如何看待比特幣、區塊鏈 ?前景如何?
A: 我的回答是看不透。

第一次接觸比特幣大概是在2012年的時候(年輕不懂事?,錯過了一次暴富的機會),最近區塊鏈的技術又火了起來。

區塊鏈通過技術手段,建立一種去中心權威的 共識 機制,在一定條件下這個共識是可信任的。現實世界中往往需要第三方的權威機構(如各種企業公司、銀行、**部門等)來保證這份信任。第三方機構也是人管理的,未必就萬無一失。各種虛擬貨幣就是利用其“可信任性”和相對的“稀缺性”,將自身轉換為資源。資源的存在可以使人投入價值,進行買賣,也就演變成了現在的炒幣。 網上還有一種說法:“以前的網路是資訊互聯,而比特幣是價值互聯”。

在我看來這種“共識”機制,會觸發權力的變革。仔細想想一旦機制成熟,很多的權威機構將失去權力,甚至不復存在。可是“權力的慾望” 可比普通的“物質欲” 高階多了。

對開發者而言,程式設計的模式又多了一種”智慧合約”,你可以直接程式碼的執行中獲利。一個成熟的公有鏈上執行著無數的智慧合約,執行產生的費用一部分給了礦工,一部分給了合約的創造者。說不定會形成一個 “合約市場” 客戶根據需求找尋合適的“合約”。

以上都是我自己的一些胡亂想法,愚見莫笑,區塊鏈技術到底會如何?

參考連結

1、https://ethereum.gitbooks.io/
2、https://cryptozombies.io/zh/course/
3、https://ethereum.gitbooks.io/frontier-guide/content/jsre.html


相關文章