Ethereum 上的 EVM(Ethereum Virtual Machine)可以執行程式,而 EVM 上的可執行程式基本上是 Bytecode 的形式,所以所謂的 Smart Contract 就是存放在 Ethereum 上的 Bytecode,然後可由 EVM 來執行。

Bytecode Smart Contract

直接用 Bytecode 寫 Smart Contract

我們來嘗試一下直接用 Bytecode 來寫 Smart Contract,以下這段程式碼主要內容是執行運算後,將運算結果存放在 0 這個位置:

PUSH1 0x03
PUSH1 0x05
ADD        // 3 + 5 -> 8
PUSH1 0x02
MUL        // 8 * 2 -> 16
PUSH1 0x00
SSTORE     // 將 16 存到 0 這個位置

這段程式轉成 Bytecode 就是:

0x60 0x03
0x60 0x05
0x01
0x60 0x02
0x02
0x60 0x00
0x55

也就是:

0x6003600501600202600055

接下來讓我們連上在上一篇文章中所建立的私有鏈 net42,我們藉由發送一個交易,但不要指定 to,如此就是告訴 EVM,我們要建立一個 Smart Contract,請在 Geth 輸入:

> var bytecode = "0x6003600501600202600055"

> var createTx = eth.sendTransaction({ from: eth.accounts[0], data: bytecode })

接下來請進行挖礦,以完成交易,交易完成後我們就可以看結果:

> miner.start(1)

> miner.stop()

> var created = eth.getTransactionReceipt(createTx).contractAddress

> eth.getStorageAt(created, 0)
"0x0000000000000000000000000000000000000000000000000000000000000010"

我們可以看到位置 0 存放了 0x10 這個值,這也就是運算結果 16 的 16 進位換算結果。

操作 Smart Contract

我們繼續來對 Smart Contract 做一些操作,我們使用以下的程式:

PUSH1 0x11 // 17
PUSH1 0x00
SSTORE     // 將 17 存放在 0 這個位置

然後在 Geth 送出交易,請注意這時要指定 to 為我們剛剛創建的 Smart Contract 位址,也就是 created:

> var bytecode2 = "0x6011600055"

> eth.sendTransaction({ from: eth.accounts[0], to: created, data: bytecode2 })

然後我們挖礦,完成交易後查看結果:

> eth.getStorageAt(created, 0)
"0x0000000000000000000000000000000000000000000000000000000000000010"

沒想到結果還是 0x10!為什麼?

我們來看看真實記在鏈上 Smart Contract 的原始碼好了:

> eth.getCode(created)
"0x"

我們並沒有存下程式碼在鏈上,一開始的 Bytecode 程式碼只是進行運算,並存放結果到位置 0 而已,我們要想辦法將我們之後要繼續使用的 function bytecode 存下來,以便之後能繼續使用。

撰寫「真的」Smart Contract

假設我們希望 Smart Contract 有一個 function 可在存取位置 0 存取資料,這段程式碼是:

PUSH1 0x00 CALLDATALOAD
PUSH1 0x00
SSTORE

Bytecode 就是:

0x600035600055

但要把這段 function 程式碼發佈成為真正可用的 Smart Contract 還需要做一些事來將這段程式碼「打包」,大概的程式碼如下:

00: PUSH1 0x06
02: PUSH1 0x0c
04: PUSH1 0x00
06: CODECOPY
07: PUSH1 0x06
09: PUSH1 0x00
0b: RETURN
0c: 600035600055

Bytecode 就是:

0x6006600c60003960066000f3600035600055

接下來我們到 Geth 試試看:

> var bytecode3 = "0x6006600c60003960066000f3600035600055"

> var createTx3 = eth.sendTransaction({ from: eth.accounts[0], data: bytecode3 })

> miner.start(1)

> miner.stop()

> var created3 = eth.getTransactionReceipt(createTx3).contractAddress

> eth.getCode(created3)
"0x600035600055"

這次我們有成功將 function 的原始碼存到 Ethereum 了!

操作「真的」Smart Contract

接下來我們就來操作看看這個 Smart Contract 的 function:

> eth.getStorageAt(created3, 0)
"0x0000000000000000000000000000000000000000000000000000000000000000"

> eth.sendTransaction({ from: eth.accounts[0], to: created3, data: "0x67" })

> miner.start(1)

> miner.stop()

> eth.getStorageAt(created3, 0)
"0x6700000000000000000000000000000000000000000000000000000000000000"

我們可以看到原本位置 0 是 0x00,經過操作之後就變成 0x67 了!

Solidity Smart Contract

使用 Bytecode 來寫 Smart Contract 實在太 Hardcore 了!我們身為普通人還是用普通人的方式來寫 Smart Contact 好了,這邊提供一個用來寫 Smart Contract 的程式語言 — Solidity。

我們用 Solidity 提供的一些平易近人的語法,然後在同過編譯器來將 Solidity 轉譯成 Bytecode,如此寫 Smart Contract 就不會像之前用 Bytecode 寫這麼麻煩了!這邊我們稍微先簡單介紹一下 Solidity,之後我們會慢慢深入。

資料型態

Solidity 主要的資料型態有以下:

  • bool
  • int
  • uint(Unsigned)
  • address

其中 address 有 .balace、.transfer(unit) 及.send(unit)這些 function 可以使用,unit 以Wei 為單位。

Functions

Solidity 可以定義 function:

  • 要使用保留字 function
  • function 可以有參數
  • function 可設為 public 或 private
  • function 可以回傳資料(或不回傳),如:returns (unit)

另外像是 for、do while、break、continue、if then else、return 等語法也有提供,如果 function 有要收取 Ether 的話,需要加上保留字 payable。

特殊變數

Solidity 有一些特殊的變數是與目前的交易相關的,例如:

  • msg.sender,代表目前呼叫這個 function 的 address
  • msg.value,代表目前呼叫這個 function 時所付的 Ether,以 Wei 為單位

簡單範例

我們先用一個簡單的範例示範一下如何用 Solidity 撰寫 Smart Contract 吧,範例程式碼如下:

pragma solidity ^0.4.22;

contract Owned {
  address public owner;

  constructor() public {
    owner = msg.sender;
  }
}

這個範例程式碼很簡單,在 Smart Contract 發佈時,建構子就會將 owner 成員變數設成是發佈 Smart Contract 的位址。

由於 Solidity 程式 EVM 看不懂,我們需要將 Solidity 程式碼轉成 Bytecode,我們這邊先使用 Remix 這個工具來編譯看看 Solidity。請在編輯區複製貼上我們的範例程式碼,Remix 會自動編譯 Solidity,接下來請選擇 ”Compile“ “Owned” “Detail”,你可以在 ”Assembly“ 區塊看到編譯完後的 Solidity 實際做了什麼。

結語

我們從最底層的 Bytecode 介紹起,了解了用 Bytecode 撰寫 Smart Contract,但實在太麻煩,所以我們改使用 Solidity 來撰寫 Smart Contract,如此寫 Smart Contract 就不會像之前用 Bytecode 寫這麼麻煩了!後續我們會使用 Solidity 來撰寫功能更豐富的 Smart Contract,慢慢深入了解 Solidity。

要連上 Ethereum 就需要安裝 Ethereum Node,在這邊我們選擇使用 Geth 來安裝 Ethereum Node,接下來就來一步一步的學學怎麼使用 Geth,甚至如何使用 Geth 來架設自己的 Ethereum 私有鏈。

安裝環境

首先我們在 AWS 上開啟兩台 Ubuntu 虛擬機器,記得開 t2.medium(2 vCPU, 4 GB RAM)這個規格以上才跑得動,硬碟可以開 100 G,Security Group 將 TCP 30303 打開,Ethereum Node 之間是用 30303 這個 port 來溝通的。

接下來使用以下指令安裝 Geth:

$ sudo apt-get install -y software-properties-common
$ sudo add-apt-repository -y ppa:ethereum/ethereum
$ sudo apt-get update
$ sudo apt-get install -y ethereum

兩台虛擬機器都要安裝,應該幾分鐘就可以裝好了。

使用 Main Net

安裝完 Geth 之後,我們就可以透過 Geth 連上 Ethereum Network 了,我們就來連上 Main Net 看看:

$ geth

下了這個指令之後,你應該可以看到一些訊息,geth 開始同步帳本資料到你的虛擬機器了,我們打開另一個 terminal 連上同一台虛擬機器,我們可以看到 geth 在以下位置建立了一些資料。

$ ls ~/.ethereum/
geth  geth.ipc  keystore

$ ls ~/.ethereum/geth/
chaindata  ethash  LOCK  nodekey  nodes  transactions.rlp

$ du -hs ~/.ethereum
5.9M /home/xxx/.ethereum

$ du -hs ~/.ethereum/geth/chaindata
5.9M /home/xxx/.ethereum/geth/chaindata

稍微說明一下,在 ~/.ethereum/ 資料夾下的 geth.ipc 只有在 geth 正在執行的時候才會出現,這就是我們之前說的要在本機透過 geth 與 Ethereum 做互動的管道,基本上我們之前的 Mist 也是使用 geth.ipc 來與 Ethereum 做互動。

其中 chaindata 資料夾下載了 Ethereum 帳本資料,如果我們想要重新同步帳本可以直接刪除裡面的所有資料;而 transactions.rlp 則存放了在 local 端蒐集到且還未被挖掘記錄到帳本的交易資料;nodekey 則是代表這台機器的 private key,用以在 Ethereum Network 上辨別不同的機器;ethash 則是運算 proof-of-work 必須產生的資料夾。

其實這樣你的機器就已經連上 Ethereum Network,並成為 Ethereum 中的一份子了,但等待 Main Net 完成同步需要花很久的時間,我們這邊就先換到 Test Net 上繼續練習,請使用 CTRL-C 關閉 geth 程序。

使用 Test Net

接下來讓我們連上 Test Net 看看,Test Net 有個別名 “Ropsten”,network id 是 3 (Main Net 的 network id 是 1),Test Net 有可能會隨著時間做一些改變,請使用以下指令連上 Test Net:

geth --testnet

同樣的,你也會看到一些同步訊息,一樣打開另一個 terminal 連上同一台虛擬機器,我們可以看到 geth 在以下位置建立了一些資料。

$ ls ~/.ethereum/testnet
geth  geth.ipc  keystore

$ ls ~/.ethereum/testnet/geth
chaindata  ethash  LOCK  nodekey  nodes  transactions.rlp

$ du -hs ~/.ethereum/testnet
112M /home/xxx/.ethereum/testnet

其實與連上 Main Net 所建立的資料大同小異,只是變成放在 ~/.ethereum/testnet 這個資料夾底下。雖然 Test Net 同步的速度會比 Main Net 快一些,但仍然需要花一段時間才能完成同步,我們一樣使用 CTRL-C 關閉 geth 程序,把所有已同步的 Main Net 及 Test Net 資料都刪除,我們改使用建立自己的 Private Net 來繼續練習。

以下是刪除同步資料的指令:

$ geth --testnet removedb // 刪除測試鏈資料

$ geth removedb // 刪除主鏈資料

建立自己的 Private Net

要建立自己的 Ethereum Private Net 其實很簡單,只要先定義好以下這兩項就能建立自己的 Private Net:

  • Network id,決定自己的 network id 是什麼
  • Genesis 檔案,決定自己的創世區塊初始帳本資料

而當其他機器要連上這個 Private Net,就需要用一樣的 network id 及相同的 genesis 檔案(代表初始共識一致),如此就能夠連上這個 Private Net 了(由於 Private Net 可能較為稀疏,所以一開始可能還需要提供其他 Peers 的位址給 geth 才能連上 Private Net)。

創世區塊

我們會使用 Genesis 檔案建立我們的創世區塊,這是整條區塊鏈中,唯一由人類共識決定的區塊,我們來看一下 Genesis 檔案的格式:

其中比較重要的是 chainId,這個就是我們剛剛說的 network id,在這邊我們設定為 42 (the answer to life, the universe, and everything),你可以設成任何想要的數字,但請避開主鏈及知名的測試鏈(基本上 1–10 盡量不要用);而 difficulty 則定義了 proof-of-work 的難度,這邊設成 0x400 其實就代表 1024,代表運算難度只有 1024,讓我們的 private chain 大約只要 10–15 秒就能產生一個區塊。

執行 Private Chain

我們使用以上內容,新增 Genesis 檔案,將檔名設為 genesis42.json,放置在 ~/chain/genesis42.json (請自行新增 chain 資料夾),然後我們也必須告訴 geth 這個 private chain 的資料要放在哪個資料夾,我們就放在 ~/.ethereum/net42 這個資料夾好了,讓我們來將執行指令寫成 shell script:

#!/bin/bash

geth --datadir ~/.ethereum/net42 init ~/chain/genesis42.json

將以上內容新增為檔案放置在 ~/chain/net42-init.sh ,這是 private chain 初始化時需要執行的指令,每個 Node 只要執行一次就可以。

#!/bin/bash

geth --datadir ~/.ethereum/net42 --networkid 42 console

將以上內容新增為檔案放置在 ~/chain/net42-start.sh ,這個指令會使 private chain 的這個 Node 跑起來,所以每次要跑 Node 時都需要執行這個指令,需要注意我們要告訴 geth 我們跑的 networkid 是 42,這樣才會接上我們剛剛設定的 private chain,而最後的 console 則是讓我們進入 geth 的命令介面。

讓我們將上面兩個 shell script 變為可執行檔,並執行這些執行檔:

$ cd ~/chain

$ chmod a+x net42-init.sh

$ chmod a+x net42-start.sh

$ ./net42-init.sh

$ ./net42-start.sh

執行完後,我們會看到我們進入 console 命令介面了,基本上這是 Javascript console,我們可以下一些指令給 geth,讓我們來試試看下一些指令吧。我們先輸入 eth ,然後按兩下 TAB,你會發現 console 會給出一些指令建議(如果一開始輸入 2 個空格,再按兩下 TAB,則會建議最基礎的指令,如果什麼指令都忘記了,一定要記得輸入 2 個空格,再按兩下 TAB)。我們輸入看看:

> eth.accounts
[]

這代表取出這個 Node 的帳號,目前沒有任何帳號。我們再試一下其他指令:

> eth.blockNumber
0

這代表我們同步的 private chain 還沒有任何區塊,我們可以看一下這個區塊的資料(注意:這邊使用了 … 省略了一些真實資料):

> eth.getBlock(0)
{
  difficulty: 1024,
  extraData: "0x00",
  gasLimit: 5000000,
  gasUsed: 0,
  hash: "...",
  logsBloom: "...",
  miner: "0x0000000000000000000000000000000000000000",
  mixHash: "",
  nonce: "0x0000000000000042",
  number: 0,
  parentHash: "...",
  receiptsRoot: "...",
  sha3Uncles: "...",
  size: 506,
  stateRoot: "...",
  timestamp: 0,
  totalDifficulty: 1024,
  transactions: [],
  transactionsRoot: "...",
  uncles: []
}

我們來看看這個 Node 是否有在挖礦:

> eth.mining
false

目前沒有在挖礦,我們檢查一下是不是在正確的鏈上:

> net.version
"42"

的確在 42 這條鏈上,接下來看有沒有其他 peers:

> net.peerCount
0

目前這個 Private chain 上,我們的 Node 並沒有跟其他 Node 有連接。

帳號

接下來我們需要一個帳號用來在這個 private chain 上面走跳,我們來創建一個帳號吧:

> personal.newAccount()
Passphrase:
Repeat passphrase:
"0xe00f575ea205035ca9c530effa728202e5385cb2"

> eth.accounts
["0xe00f575ea205035ca9c530effa728202e5385cb2"]

> eth.coinbase
"0xe00f575ea205035ca9c530effa728202e5385cb2"

請記得帳號的 passphrase,我們在做任何交易時,都會需要 passphrase 來解鎖帳號,這是為了保障帳號的安全性,解鎖的指令如下:

> personal.unlockAccount(eth.accounts[0])
Unlock account 0xe00f575ea205035ca9c530effa728202e5385cb2
Passphrase:
true

帳號其實就是這個帳號的 public key,也就是我們可以公開的 Ethereum 帳戶位址,而 private key 我們會存在 keystore 裡:

$ ls ~/.ethereum/net42/keystore/
UTC--2018-09-17T09-31-22.219973375Z--e00f575ea205035ca9c530effa728202e5385cb2

Private key 會用你所輸入的 passphrase 加密存在這個 keystore 檔案裡,如果要備份帳號到其他機器使用,那就需要備份這些 keystore,及其所對應的密碼。

我們來看看這個帳號有多少 Ether:

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

目前當然是沒有任何 Ether,所以我們需要挖礦來獲取 Ether!

挖礦

進行挖礦時會將挖到的 Ether 給 eth.coinbase 這個帳號,eth.coinbase預設就是eth.accounts[0] ,我們來挖礦吧:

> eth.coinbase
"0xe00f575ea205035ca9c530effa728202e5385cb2"

> miner.start(1)
true

其中 miner.start(1) 就代表開始挖礦,而參數 1 就是代表要開幾個 process 來挖礦,我們設成 1 就好,因為我們是自己在挖,會跟自己競爭,參數越高反而不一定能越快挖到區塊。而在第一次執行挖礦時,geth 會下載 ethash 檔案,這個檔案很大,也因此需要花一些時間,所以我們要耐心等待(看到 mined potential block 的訊息時才代表真的開始挖礦了ㄋ),請喝杯咖啡再回來吧!

過一陣子之後,我們回來看挖礦結果:

> miner.stop() // 停止挖礦
true

> eth.blockNumber
7

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

> eth.getBalance(eth.coinbase)
21000000000000000000

> web3.fromWei(eth.getBalance(eth.accounts[0]), "ether")
21

我們看到區塊數變成 7,且帳號裡的 Ether 變成 21 了,我們果然有成功挖到 Ether 了!

交易

目前我們都還沒有做任何交易:

> eth.getBlock(0).transactions.length
0

> eth.getBlock(7).transactions.length
0

我們再開一個帳號來進行一下兩個帳號之間的交易:

> eth.mining
false

> personal.newAccount()
Passphrase:
Repeat passphrase:
"0x7a5005c95252e35d58e38f6dc5d2ac6e4b6f625b"

> eth.accounts
["0xe00f575ea205035ca9c530effa728202e5385cb2", "0x7a5005c95252e35d58e38f6dc5d2ac6e4b6f625b"]

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

我們來送一些 Ether 到第 2 個帳號:

> web3.toWei(5, "ether")
"5000000000000000000"

> eth.sendTransaction({ from: eth.accounts[0], to: eth.accounts[1], value: web3.toWei(5, "ether") })
Error: authentication needed: password or unlock

這時我們會看到 console 報錯,這是因為我們還沒有 unlock 帳號,在 from 的帳號需要 unlock,to 的帳號不需要 unlock,所以我們需要 unlock eth.accounts[0] :

> personal.unlockAccount(eth.accounts[0])
Unlock account 0xe00f575ea205035ca9c530effa728202e5385cb2 Passphrase:
true

> var txHash = eth.sendTransaction({ from: eth.accounts[0], to: eth.accounts[1], value: web3.toWei(5, "ether") })

> txHash "0x9f494ebc7b9ad6e6a9abd36798c6d04dcd7e883acf553a99e581b8550ec16178"

我們可以成功送出交易了,讓我們看一下交易的詳細資料:

> eth.getTransaction(txHash)
{
  blockHash: "...",
  blockNumber: null,
  from: "0xe00f575ea205035ca9c530effa728202e5385cb2",
  gas: 90000,
  gasPrice: 20304857463,
  hash: "0x9f494ebc7b9ad6e6a9abd36798c6d04dcd7e883acf553a99e581b8550ec16178",
  input: "0x",
  nonce: 0,
  r: "...",
  s: "...",
  to: "0x7a5005c95252e35d58e38f6dc5d2ac6e4b6f625b",
  transactionIndex: null,
  v: "0x78",
  value: 5000000000000000000
}

我們可以看到這個交易的詳細資料中,blockNumber 是 null,這代表交易還未確認,如果我們查看交易收據:

> eth.getTransactionReceipt(txHash)
null

回傳得到 null,我們看看第 2 個帳號是否有收到 Ether:

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

仍然是 0 Ether,所以第 2 的帳號還沒有收到 Ether,我們需要將挖礦打開,這樣新的交易才能在新的挖礦區塊裡被確認:

> miner.start(1)
true

// 等待挖礦完成幾個區塊之後

> miner.stop()
true

> eth.getBalance(eth.accounts[1])
5000000000000000000

> eth.getTransaction(txHash)
{
  blockHash: "...",
  blockNumber: 8,
  from: "0xe00f575ea205035ca9c530effa728202e5385cb2",
  gas: 90000,
  gasPrice: 20304857463,
  hash: "0x9f494ebc7b9ad6e6a9abd36798c6d04dcd7e883acf553a99e581b8550ec16178",
  input: "0x",
  nonce: 0,
  r: "...",
  s: "...",
  to: "0x7a5005c95252e35d58e38f6dc5d2ac6e4b6f625b",
  transactionIndex: null,
  v: "0x78",
  value: 5000000000000000000
}

我們可以看到第 2 個帳號拿到 5 Ether 了,而這個交易的 blockNumber 也變成了 8,接下來我們看看交易收據:

> eth.getTransactionReceipt(txHash)
{
  blockHash: "...",
  blockNumber: 8,
  contractAddress: null,
  cumulativeGasUsed: 21000,
  from: "0xe00f575ea205035ca9c530effa728202e5385cb2",
  gasUsed: 21000,
  logs: [],
  logsBloom: "...",
  status: "0x1",
  to: "0x7a5005c95252e35d58e38f6dc5d2ac6e4b6f625b",
  transactionHash: "...",
  transactionIndex: 0
}

我們可以看到交易收據的詳細資料了,其中的 status 代表交易的情況,0x1 代表交易完成,如果是 0x0 就代表交易發生錯誤了。

以上我們已經學會了如何自己架 Private Chain,如何自己挖礦,如何在自己的 Private Chain 上做交易了,很簡單吧!

Private Chain 連結其他 Node

現在架一個 Private Chain 對我們來說不是問題了,不過現在整個 Private Chain 只有一個 Node,怎麼與其他 Node 連結呢?

我們一開始有開了兩台 AWS 機器,目前都在其中一台練習,另外一台現在也仿造之前的步驟裝好環境,使用相同的 Genesis.json(一定要一模一樣)檔案建立、開啟 Private Chain,並建立好一個帳號,然後先不要進行挖礦。

現在我們兩個 Node 是分開的,並不知道彼此的存在:

Node 1 > admin.peers
[]

Node 2 > admin.peers
[]

但查看 blockNumber:

Node 1 > eth.blockNumber
16

Node 2 > eth.blockNumber
0

我們會發現 Node 1 有 16 個區塊了,但 Node 2 還沒有任何區塊。我們在 Node 1 取得 Node 的位址相關資訊:

Node 1 > admin.nodeInfo.enode
"enode://cf2d54937d2e7ee080e69ecde67352837fd8230482fea2cc34ec756e2f36c10608d2cdbb66f7788c84a0069c162f043fa1f660a942dddf8fcdf9d74de87061b4@[::]:30303"

我們將enode://cf2d54937d2e7ee080e69ecde67352837fd8230482fea2cc34ec756e2f36c10608d2cdbb66f7788c84a0069c162f043fa1f660a942dddf8fcdf9d74de87061b4@[::]:30303 中的 [::] 改成 Node 1 的 Private IP,看起來可能會像:enode://cf2d54937d2e7ee080e69ecde67352837fd8230482fea2cc34ec756e2f36c10608d2cdbb66f7788c84a0069c162f043fa1f660a942dddf8fcdf9d74de87061b4@172.31.22.132:30303,然後在 Node 2 加入這個 Peer:

Node 2 > admin.addPeer("enode://cf2d54937d2e7ee080e69ecde67352837fd8230482fea2cc34ec756e2f36c10608d2cdbb66f7788c84a0069c162f043fa1f660a942dddf8fcdf9d74de87061b4@172.31.22.132:30303")

之後我們查看:

Node 1 > eth.blockNumber
16

Node 2 > eth.blockNumber
16

我們發現 eth.blockNumber 變成一樣了,如果查看其中每個區塊的資料也會發現一模一樣,我們成功將多個 Node 連上自己建立的 Private Chain 並讓帳本在多個 Node 同步了!真神奇!

結語

在這一篇文章中我們介紹了 geth 的一些基礎用法,我們知道如何連上 Main Net,如何連上 Test Net,而且我們也學會了如何自己架設區塊鏈,建立自己的 Private Net,我們可以建立帳號、解鎖帳號、進行挖礦、進行交易,並查看交易詳細資料,最後學會將多個 Node 連上自己建立的 Private Net,建立一個完整的共識系統。學會這些,基本上就可以到處唬人了呢!

在第一次接觸 Ethereum 應用程式開發時,會發現有各式各樣工具,不知要從何下手,我們用一個圖來說明一下與 Ethereum 互動時的整體脈絡及這之間的工具主要做了什麼事,了解之後自己就可以挑選開發時、甚至使用在產品上時要用什麼適合的工具了。

要在自己的機器接上 Ethereum 首先需要安裝 Ethereum Node,我們之前安裝的 Mist 其實就會在我們的機器上安裝 Ethereum Node 並同步帳本,而像這樣安裝 Node 並同步帳本甚至進行挖礦的軟體有很多,大家可以去選擇適合自己使用的。Mist 其實是將一個叫 geth 的軟體用 GUI 包裝起來,如果是開發者的話,可以選擇直接安裝 geth。

geth 提供了許多 API 指令可以讓我們跟 Ethereum 做互動,但有時下指令並不是那麼親和,所以 geth 提供了 RPC(Remote Procedure Calls) 與 IPC(Inter-process Communications) 兩種方式來與 geth 互動,如果你要在 local 機器連上 geth,那就可以使用 IPC;如果要讓遠端連上 geth,那就使用 RPC,可以開 HTTP 或 Web Socket 兩種方式來讓遠端使用。

以上就是 Ethereum 應用程式開發的基礎環境,接下來跟開發網頁應用程式一樣,Ethereum 應用程式也分成後端與前端,後端程式就是 Smart Contract,前端程式就是 Dapp。Smart Contract 可使用 Solidity 撰寫,目前也有許多其他語言可以撰寫 Smart Contract。Smart Contract 要在 Ethereum 上的 EVM 執行要先 Compile 成 Byte Code 之後,再透過 IPC 或 RPC 發佈到 Ethereum 上。前端程式的 Dapp 可用 Web3 JavaScript 透過 RPC 接上 Ethereum,以及使用網頁應用常用到的 HTML、CSS、JavaScript 製作成使用者互動介面,如此就能執行發佈在 Ethereum 上 Smart Contract 所提供的一些程式功能了。

以上整體脈絡如果了解了,那在 Ethereum 應用程式開發上就跨進了第一步,後續我們會循著這個脈絡來一步一步學習 Ethereum 開發。

Bitcoin 的獎勵機制基本上是挖到新區塊的節點獲得記帳權及獎勵,Ethereum 大體也是遵循這樣的概念,但做了一些調整與變化,讓我們整個脈絡了解一下。

由於 Blockchain 是一種去中心化的系統,所有的礦工(節點)可以同時挖礦(計算合法 hash),彼此獨立運作,所以極有可能出現兩的礦工同時發現不同的滿足條件的區塊,如此就會產生我們之前有提過的分叉(Fork)。

那我們該採用誰的區塊當主鏈呢?我們會先依工作量最大的區塊為主鏈,如果工作量一樣,就看誰先接了子區塊,一般來說只有成了主鏈的區塊才能獲得獎勵。但這樣沒有變成主鏈的區塊之前的算力就都白費了,所以 Ethereum 創造了 Uncle Block(叔塊)這樣的概念,不能成為主鏈的區塊如果後來被收留成為 Uncle Block,那這些沒有成為主鏈的區塊也有機會可以做為 Uncle Block 而獲得獎勵。

這就是 Ethereum 共識機制中的 GHOST(Greedy Heaviest Observed Subtree)協議,Ethereum 會這樣設計的原因,是由於 Ethereum 產生區塊的速度較快,也因此較容易產生分叉,也會使得新區塊較難以在整個網絡傳播,這對於傳播速度較慢的區塊並不公平。且分叉後的區塊可能在幾個區塊之後整併起來,我們會發現裡面的交易可能會與主鏈一致(雖然單獨查看分塊交易內容不同,不過數個區塊整體一起看交易內容就一致了),符合這種條件的分叉區塊我們就會納入主鏈參考,這些區塊就成了所謂的 Uncle Block,這某種角度也是更確認了 Blockchain 上的交易內容一致,因此 Uncle Block 也有貢獻,應該給予獎勵。

以上我們已經了解了 Ethereum 上的區塊大致分成兩種,普通區塊和 Uncle Block,Ethereum 對這兩種區塊的獎勵方式是不同的。我們分別來看一下。

普通區塊獎勵

  • 固定獎勵 5 ETH
  • 區塊內所有的 Gas Fee
  • 如果區塊納入了 Uncle Block,那每包含一個 Uncle Block 可以得到固定獎勵 5 ETH * 1/32,也就是 0.15625 ETH,一個區塊最多隻能包含 2 個 Uncle Block,也因此不會無限延伸,同時又可鼓勵區塊納入 Uncle Block,增加交易內容的一致性。

Uncle Block 獎勵

  • 用公式計算:(Uncle Block 高度 + 8 - 包含此 Uncle Block 的區塊的高度)* 普通區塊固定獎勵 / 8

我們用個實例來看一下獎勵怎麼算。首先我們來看一個普通區塊:https://etherscan.io/block/1234757

我們可以看到這個普通區塊的獎勵是 5.31485368 ETH,是由固定獎勵 5 ETH、總 Gas Fee 0.00235368 ETH 及包含了 2 個 Uncle Block 所以是 2*5*132 = 0.3125 ETH,所以結果就是 5.31485368 ETH。

接下來我們來看一個 Uncle Block:https://etherscan.io/uncle/0x54c3a32edc5b23dfeaac80ef50ed9a49faf269f2e7380b81b39f44b630346c70

我們帶入公式運算一下,Uncle Block 高度是 1234756,包含此 Uncle Block 的區塊高度是 1234757,所以是 (1234756+8–1234757)*58 = 4.375 ETH。

以上就是 Ethereum Blockchain 的獎勵機制,應該還算淺顯易懂,我第一次看白皮書的 Uncle Block 完全不知道在講什麼啊,希望這樣有幫忙大家看懂。

我們這邊再次總結一下 Blockchain 中幾點較重要的性質,包含共識機制、不可竄改、經濟激勵三項。

共識機制(Consensus)

在分散式系統中,我們需要有一套用於協同合作的共識機制來組織行動,但有時候系統中的成員可能會出錯或是故意傳送出錯誤的資訊,而使得網路中不同成員對於全體協作的策略得出不同的結論,進而破壞系統的一致性,這就是所謂的拜占庭將軍問題。

拜占庭將軍問題(Byzantine Generals Problem)

拜占庭將軍問題這個故事是這樣的:

一組拜占庭將軍分別各率領一支軍隊共同圍困一座城市,這個敵人雖不比拜占庭帝國,但也足以抵禦 5 支拜占庭軍隊的同時襲擊。這 10 支軍隊在分開的包圍狀態下,他們任 1 支軍隊單獨進攻都毫無勝算,除非有至少 6 支軍隊(一半以上)同時襲擊才能攻下敵國。他們分散在敵國的四周,依靠通信兵騎馬相互通信來協商進攻意向及進攻時間。困擾這些將軍的問題是,他們不確定他們中是否有叛徒,叛徒可能擅自變更進攻意向或者進攻時間。在這種狀態下,拜占庭將軍們才能保證有多於 6 支軍隊在同一時間一起發起進攻,從而贏取戰鬥?

上述的故事對映到電腦系統裡,將軍便成了電腦,而通信兵就是通訊系統。叛徒發送前後不一致的進攻提議,被稱為「拜占庭錯誤」,而能夠處理拜占庭錯誤的這種容錯性稱為「Byzantine Fault Tolerance」。Blockchain 上的共識機制通常具有容錯的設計來達成一致性,主要比較常見的共識機制方法有兩個,「工作量證明」以及「股權證明」兩種方法。

工作量證明演算法(Proof of Work, PoW)

中本聰在 Bitcoin 中創造性的引入了「工作量證明」(俗稱挖礦)來解決拜占庭將軍問題,顧名思義,工作量證明就是用來證明你做了一定量的工作,可用工作成果來證明完成相應的工作量。其中的工作技術原理可以看之前這篇文章:Ethereum 開發筆記 1–4:Blockchain 技術原理簡介

由於工作量證明具相當高的計算成本,因此無誘因去偽造,只有遵守協議約定,才能夠回收成本並獲得收益,也因此減少了叛徒的產生,減少拜占庭錯誤。

股權證明演算法(Proof of Stake, PoS)

股權證明的出現,主要是希望取代工作量證明,進而減少「挖礦」的大量運算。它與工作量證明不同地方在於:工作量證明中,大家比的是「算力」(運算能力),透過大量運算得出符合難度的 Hash 值,進而得到獎勵;而在股權證明,大家比拼的是「股權」,「股權」越大的人(節點)越大機會負責產生新區塊,進而得到獎勵。

舉例來說,在股權證明系統中所有擁有股權(此 Blockchain 的數位貨幣)的人都有機會被挑選為產生新區塊(也就是記帳)的人,擁有更多股權的人被選中的機率越大。假這這個系統中共有三個人:Alice 持有 50 股、Bob 持有 30 股、Cathy 持有 20 股,那每次 Alice 被選為記帳人的機率會是 Cathy 的兩倍。所以股權證明會驅使人們購買更多的股權,進而增加獲選為記帳人的機率,以買股權來代替挖礦,同樣需要付出高成本,也因此可以減少叛徒的產生,減少拜占庭錯誤。

不可竄改(Immutability)

Blockchain 不可竄改的性質主要來自資料結構及 hash 方式的設計,讓資料的順序緊密鏈結,若從中竄改了某些資料,那之後的鏈結 hash 都會發生錯誤,形成了 Blockchain 不可竄改的特性。

在之前《Ethereum 開發筆記 1–4:Blockchain 技術原理簡介》中有部影片可以很清楚地了解為何 Blockchain 不可竄改(除非擁有全網路 51% 以上算力,否則基本上無法竄改),我們可以再看一次這部影片:

經濟激勵(Economic Incentives)

Blockchain 通常會有經濟激勵的設計,如此 Blockchain 上的記帳人(礦工)便可以得到獎勵,進而繼續維持共識機制運作。反過來說在 Blockchain 上進行交易的人就需要支付費用,除了可以用於獎勵礦工之外,其實也是為了 Blockchain 的安全,讓攻擊者無法大量發送交易攻擊 Blockchain 的網路,通常 Blockchain 會在交易量大時提高交易費,也因此攻擊者越是發送大量交易就需要付出越多成本,基本上不可行。

而在 Ethereum 上交易費又稱為 Gas Fee,Gas Fee 由 Gas Price 與 Gas Limit 相乘得出,會有這樣的設計主要是因為 Ethereum 上可以運行程式,攻擊者可能在程式上寫了惡意的無窮迴圈用以耗盡礦工算力,Gas Limit 會在計算時消耗,也因此無窮迴圈程式會在 Gas Limit 耗盡時停止,攻擊者便會失去 Gas Fee,這樣的攻擊方式基本上不可行。

結語

以上就是 Blockchain 中幾點較重要的性質,Blockchain 經常被視為一個信任機器(Trust Machine),這個信任是由分佈在網絡各處的節點透過無法竄改(Immutability)的運算證明所達成的共識(Consensus)所形成,共識機制就是驅動 Blockchain 這個信任機器運轉的引擎,而其中的經濟激勵(Economic Incentives)就是驅動 Blockchain 引擎的燃料。

Blockchain 裡應用了一些加密技術來保證及驗證交易訊息的正確性,這也更加強了 Blockchain 資料不可竄改的特性。我們來介紹其中比較重要的「公私鑰加密」以及「Merkle Tree」加密樹。

公私鑰加密

公私鑰加密算法是目前資訊通訊安全的基石,它保證了加密訊息不可被破解,相關的加解密原理大家可以參考這兩篇文章:

  1. RSA算法原理(一)http://www.ruanyifeng.com/blog/2013/06/rsa_algorithm_part_one.html
  2. RSA算法原理(二)http://www.ruanyifeng.com/blog/2013/07/rsa_algorithm_part_two.html

加密與解密

公私鑰加密方法是一種非對稱式加密,透過公鑰加密過後的訊息只有私鑰可以解密,也因此只要保護好私鑰就能保證資訊的安全。

現在假設 Alice 要傳一個訊息給 Bob,希望訊息加密過後只有 Bob 可以解密,大概會經過如下步驟:

  1. Bob 傳他的公鑰給 Alice
  2. Alice 使用 Bob 的公鑰加密訊息
  3. Alice 將加密過後的訊息傳給 Bob
  4. Bob 用他的私鑰解密訊息

我們這邊使用 openssl 來練習一下加密與解密,首先我們來產生一對公私鑰:

// Create RSA private key
$ openssl genrsa -des3 -out rsa-key.pem 2048
// Create public key
$ openssl rsa -in rsa-key.pem -outform PEM -pubout -out rsa-key-pub.pem

其中 rsa-key.pem 就是私鑰,rsa-key-pub.pem 為公鑰,私鑰會要求設置密碼,請妥善記下密碼。

我們先用 rsa-key-pub.pem 加密資料:

openssl rsautl -encrypt -pubin -inkey rsa-key-pub.pem -in helloworld.txt -out helloworld.enc

其中 helloworld.enc 就是被加密過後的資料,接下來我們用 rsa-key.pem 來解密:

openssl rsautl -decrypt -inkey rsa-key.pem -in helloworld.enc -out helloworld2.txt

解密過後我們查看 helloworld2.txt 就會發現內容與 helloworld.txt 完全一致,我們成功解密了。

簽名與驗證

有時我們會想知道訊息是否是由本人傳送的,這時我們就會使用公私鑰來做簽名與驗證,大致會經過如下步驟:

  1. Bob 傳他的公鑰給 Alice
  2. Bob 將他的訊息使用私鑰進行簽名
  3. Bob 將他的訊息以及簽名傳給 Alice
  4. Alice 使用 Bob 的公鑰驗證簽名是否來自 Bob
  5. 我們這邊使用 openssl 來練習一下簽名與驗證,首先我們使用私要進行簽名:
openssl dgst -sign rsa-key.pem helloworld.txt > signature.bin

接下來我們要用公鑰驗證簽名:

openssl dgst -verify rsa-key-pub.pem -signature signature.bin helloworld.txt

我們會發現如果沒有正確的簽名,或內容不正確就無法通過驗證,也因此我們可以知道能通過驗證的內容就是沒有被竄改過且簽名也一定是來自本人。

通常實務上我們會將公私鑰加解密與簽名驗證一起使用,以確保資訊傳遞的安全。

Merkle Trees

Blockchain 中的交易會組成 Merkle Tree 的形式,再將 Merkle Root 放入 Blockchain 的區塊中,Merkle tree 的資料結構如下:

  • 樹葉節點放的就是資料
  • 父節點放的是子節點資料的 hash 值

有了這樣的 Merkle Tree,我們就可以:

  • 用 hash 來代表資料,即使是很大量的資料也可以用一個簡單的 hash 來表示
  • 如此要下載資料與驗證資料就可以同步進行,這在分散式系統非常重要

底下有一個影片前顯易懂地說明了 Merkle Tree 的結構。

以上就是學習 Blockchain 必備的一些相關的加密基礎知識。

在 Ethereum 開發筆記 1–4 應該已經將 Blockchain 的技術原理說明得很清楚了,不過如果要向一般大眾簡單說明 Blockchain 是什麼,要怎麼說呢?我會說:Blockchain 就是一個分散式帳本,大家都有一樣的帳本,大家都可一起參與記帳,且記完帳大家的帳本就會自動更新到最新版本,而帳裡的紀錄都會分塊並用密碼按順序鏈結起來,用以驗證帳的正確性,如果中間有人改了資料,那後面的鏈結密碼都會發生錯誤,因此沒有人可以亂改帳,這就是 Blockchain。

但 Blockchain 這個名詞還包含了許多概念與內涵,我們之前說過,Blockchain 是因為分散式去中心化帳本的發展而慢慢產生出來的,這樣慢慢被統稱出來的名詞裡底下也就會包含了許多內涵,很難用三言兩語來說明,所以有一些 Blockchain 相關的定義與名詞我們都可以了解一下,這樣就能更了解 Blockchain。

交易(Transaction)

交易是 Blockchain 帳本中的原子單位,如果將交易再往下拆分就會變得沒有意義,比如下列就是一個交易:

  1. A 減少了 $10
  2. B 增加了 $9
  3. C 增加了 $1

如果只看 1,我們就會想那減少的 $10 到哪裡去了?所以 1、2、3 一起看才算是一個交易。

Blockchain 是一個分散式帳本(Distributed Ledger)

不像銀行依靠自己的帳本來記帳,Blockchain 提供了可靠的分散式帳本,當銀行之間要進行交易時,會需要一個受信任的第三方來進行銀行之間的交易,這也是為何你在做跨國轉帳時,需要付出高昂的手續費以及等待數天處理交易,Blockchain 可靠的分散式帳本讓跨國交易可以在幾分鐘甚至幾秒之內完成,這也是為何銀行想要應用 Blockchain 在金融交易上以降低交易成本。

Blockchain 是一個資料結構(Data Structure)

通常 Blockchain 的資料結構如下組成:

  • 交易是原子單位
  • 區塊是由一系列的交易組成
  • 區塊鏈由排序良好的區塊所組成

Blockchain 會有分叉(Fork)

當有兩名礦工 A 及 B 幾乎在相同時間內算出了合法的 hash,這兩個區塊傳播到鄰近節點時,有些節點收到了 A 的區塊,有些節點收到了 B 的區塊,這兩個區塊都可以是主鏈的延伸,這時就會產生區塊鏈分叉。

這時會比較分叉中哪個鏈有更多的工作量(工作量證明)來決定作為主鏈,另一個分叉作為備用鏈保存(因為未來可能會超過主鏈工作量成為新主鏈),如此各節點繼續工作下去延長區塊鏈。

更短的區塊產生間隔可以讓較易確認更快完成,但也可能更頻繁的產生分叉,而長的區塊產生間隔可以減少分叉數量,但卻會導致更長的交易確認時間。為了減少分叉的發生,Bitcoin 將區塊間隔設計為10分鐘(用工作難度來調整時間),這個時間設計是在交易確認速度與分叉機率之間做出的妥協。

Blockchain 是一個去中心化共識系統(Decentralized Consensus)

Blockchain 是個分散式帳本,且各個節點之間並沒有哪個節點比其他節點更有權威性,也因此許多人稱之為「去中心化」。沒有了權威中心的控制系統要如何讓各節點之間可以協同運作?Blockachin 就是使用了共識機制來彼此協同運作,決定整個鏈如何延展下去。

Blockchain 代表數位自由化

由於 Blockchain 中各個節點之間並沒有哪個節點比其他節點更有權威性,任何節點都可以自由的存取資料,也因此在 Blockchain 相較於其他中心化的系統更具自由,言論審查在 Blockchain 上基本上不可行。

Blockchain 是新的運算平台

在 Ethereum 這樣的 Blockchain,由於可以將程式在 Blockchain 上運行,這讓一個全球性的運算平台成為可能,也因此我們可以將 Blockchain 視為一種新的運算平台。

公有鏈、聯盟鏈、私有鏈(Public Chain、Consortium Chain、Private Chain)

公有鏈是指任何人都可以存取與發送交易、任何人都可以參與共識過程的區塊鏈,這就是我們一般在說的 Blockchain。

有時我們在應用場景上不希望完全公開,但卻又希望借重 Blockchain 的不可竄改及 P2P 共識機制,比如在銀行間進行支付、結算等交易就可能不希望完全公開,這時就可以用聯盟鏈的形式來整合各家銀行的記帳節點。

私有鏈與聯盟鏈最大的不同在於,聯盟鏈是為一個聯盟,比如為一個行業服務;而私有鏈則是為一個組織,比如一家公司的內部服務,完全私有的區塊鏈,寫入權限僅在一個組織手裡的區塊鏈。

分散式與去中心化的差異(Distributed vs. Decentralised)

分散式與去中心化常混為一談,但分散式與去中心化是不同的,分散式代表系統不存在單點脆弱性,而去中心化則代表各系統中沒有不同的權威性,比如 Google 其實是一個分散式系統,但卻是中心化系統,Google 可以控制整個系統,而 Bitcoin 就是一個既分散也去中心的系統,由各系統彼此協同共治。我們可以用下圖更清楚地了解分散式與去中心化(沒有控制的手)的概念。

以上就是你可能會常常聽到和 Blockchain 一起出現的一些定義與名詞,其實就稍微了解就好,畢竟這還是會隨著發展而改變啊。

Fukuball

我是林志傑,網路上常用的名字是 Fukuball。我使用 PHP 及 Python,對機器學習及區塊鏈技術感到興趣。 https://www.fukuball.com

Co-Founder / Head of Engineering at OurSong

Taipei, Taiwan