2018年5月15日 星期二

Ethereum 基礎概念與原理

Ethereum 是一種分散式帳本的技術,雖然說大家更熟知的是「以太幣」這個虛擬貨幣~。不過這裡會嘗試從系統工程師的角度來看帶這個技術。

這篇文章大概會寫得有點雜亂,因為這主要是要紀錄一些本來我自己不太清楚的問題。如果需要比較完整、有條理的說明,推薦可以認真看看 [1]。

    1. 分散式帳本:不可修改的帳本

    「分散式帳本」(Distributed Ledger)網路上也有很多解釋了 [2],所以這裡也不特別再重述 XD。不過從我個人的觀點來看,我覺得某種程度上可以想像成就是一種分散式的檔案系統,只不過它儲存的內容被限定在「交易」(transaction)這種資料型態。

    Ethereum、以及其他區塊鏈的技術,基本上都有個共通的特性,也就是存入區塊鏈內的交易是不可修改的。為什麼存進去以後不可修改呢?主要的原因在於「分散式帳本」這個特性。「分散式帳本」意味著每個人都持有自己的一本帳本,並且自行維護自己的帳本。當有一個新人加入這個區塊鏈時,他首先會做的事情,是先問問他聯繫上的那個人,過去發生過哪些交易。接著他會把這些交易全部同步過來,紀錄在自己的帳本上。也就是說,這個新人在做的事情是 Replication,複製了一整份別人已經存好的東西。

    接著,當有交易發生、並且被發送到這個人手上時,這個人會根據自己手上的帳本去核准這筆交易、並且同時更新自己的帳本。接著再把這個交易發送到網路上,讓其他人知道這筆交易發生了。

    但這跟不可修改有什麼關係呢?

    可以想像,因為每個人手上都有一個帳本,然後帳本上紀錄了一樣的資訊。當有人想要偽造帳本時,他想騙過所有的人就必須要去修改所有人手上的帳本,否則如果只修改了一兩個人的帳本,其他人只要再去問別人,就能知道真偽了。

    2. 礦工(miner)

    平常常聽到「礦工」這個詞,一般會有的概念是礦工挖礦可以獲得 ether(即以太幣)。不過礦工挖礦到底是在幹麻呢?

    所有的礦工的目的都是打包新的 block,也就是「記帳」。在 Ethereum 中,礦工代表的是有權力記帳的人,也就是他會收集別人想要進行的交易,然後把一群交易打包成一個 block(區塊),寫入到自己的帳本(blockchain)中。

    其中,在每個 block 當中都會存在第一個交易是沒有發送者的交易,這個交易是礦工宣告把 ether 匯入自己的錢包。這是所有礦工共通的協議,同時也被視為是礦工挖礦所獲得的「無中生有的薪水」。

    2.1. 礦工如何算帳?

    「算帳」指的是,礦工怎麼知道哪個錢包有多少錢?一般來說,從系統角度來看,我們會認為應該有個地方在紀錄每個錢包最後剩下多少。因此接下來要對這個錢包做操作時,就會產生我要對它修改、他也要對它修改的 concurrent access issue。

    不過在 ethereum 中,因為 ethereum 是分散式帳本的概念,實際上並沒有任何中央的地方去儲存「每個錢包剩多少錢」,而是交由每個節點自行計算。也就是說,當節點剛加入鏈的時候,他需要先同步別人已經寫好的帳本的內容過來。在同步的過程中,他也會自行開始統計每個錢包有多少錢~(或者也可以不要統計,這基本上是由 client 的實作自行決定)。

    因此,當有人來問這個節點說某個錢包有多少錢時,這個節點會依照自己手上的帳本的紀錄來回覆對方。

    2.2. 礦工的 GPU 在運算什麼?

    在以太幣的公鏈上,因為有著太多的礦工,如果讓大家各自記自己的帳,估計合併時應該會是大麻煩吧。雖然不確定是不是因為這個因素,不過總之 Ethereum 設計了一種叫做 PoW(Proof of Work)的機制,讓礦工們可以搶奪記帳權。

    說是搶奪,但其實礦工並不會干擾他人,更正確一點的講法是比賽賽跑,誰在時限內跑到終點就可以記帳。而搶奪的過程,就是透過數學方法,讓礦工們各自去嘗試找某個滿足特定條件的數字出來。如果成功找出來的礦工,就會開始進行打包 block 的工作,並且獲得無中生有的 ether 作為獎勵。

    2.3. 如何進行合併?

    當有兩個以上的礦工,各自打包了新的 block,那麼就產生了兩個內容不同的 block,這時該怎麼辦呢?詳細的解說建議可以參考 [3],這裡引用了 [3] 的說明圖示。

    首先,在第 i+1 個 block 時,如果下個 block 同時有兩個礦工挖出礦,這時整個鏈上在相同位置會存在兩個 block。基本的機制是,看哪邊先挖出下個 block,就會以它作為主要的區塊鏈。以圖來說,就是綠色的 block 會成為主要的鏈,而黃色的則被稱為 uncle block(叔父塊)。

    uncle block 並非一定會進入區塊鏈,這要看後來有沒有其他的 block 打包時會順便把他包進去。如果很不幸都沒有的話,那這個 uncle block 就會被廢棄。廢棄的意思是,出塊的那個礦工就沒有獎勵金可拿,並且~這個廢棄的叔父塊內的交易也都當作沒發生過。

    換言之,在 Ethereum 上就算交易已經被礦工存下,在一定期間內也不完全保證不會被廢棄,因為問題在於他會不會是 uncle block,以及如果是的話,之後會不會被重新包回主鏈裡。

    2.4. 分叉

    上面講到,節點都是依照自己手上的帳本,自己算出每個錢包的餘額的。但這同時也導致了一個問題,不同的 client 對於同一筆紀錄,有可能會存在不同的認知。例如如果 client 的升級如果產生了重大的演算法分歧,會造成不同節點對同樣一群交易存在不同的看法。這時,在區塊鏈上就形成了「分叉」[4-5]。也就是某一群節點互相認可對方的紀錄,但卻不認可其他群體的紀錄的現象。

    不過如果分叉的起因是來自於 client 的升級,因為這個終歸是大家都升級後就會恢復和諧的問題,因此通常稱之為「軟分叉」。而另一種狀況則是「硬分叉」,這通常來自於社群的意識分裂。例如 ethereum 的 The DAO 事件,因為以太幣遭到竊盜,一部分社群人士認為應該取消那段時間的所有交易。讓帳本回復到事件前的樣子重新開始記帳。但另外一部份人則不認同~(就我個人而言我也不認同就是了 XD)。因此在這個事件上,就變成認同的人使用了新的方法來重新記帳,而不認同的人則繼續使用本來的帳本。

    實際上的影響是,硬分叉導致了一個區塊鏈變成兩個,而且這兩個區塊鏈,在某個時間點以前的帳本內容是完全相同的。這也意味著,在這個時間點以前的交易,在兩本帳本上都是合法的。所以如果我本來在 The DAO 事件前,我的以太幣(ETH)錢包裡有 10 個以太幣。The DAO 事件後,因為 ETH 分裂成 ETH 和 ETC,因此這時會變成我的錢包同時有 10 個 ETH 和 10 個 ETC。因為在 ETH 的鏈上,可以統計出我的錢包累計有 10 個 ether 存入;而在 ETC 的鏈上,一樣也可以統計出相同的結果。

    3. 交易(transaction)

    在 Ethereum 中,所有的事情都起因於交易,也就是說,沒有發起交易,在區塊鏈上就不會發生任何事。關於錢包以及交易的更詳細的原理(包含基本的數學原理),推薦可以參考 [6-7]。

    3.1. 什麼是錢包?

    在談論交易以前,首先先稍微談談錢包 [6]。錢包在 Ethereum 上,實際上是以「地址(address,或者稱為帳號)」來表示。而一個 address 又是由 public key(公鑰)所表示,public key 則可由 private key(私鑰)來表示,因此關係如下:

    錢包地址 ← 公鑰 ← 私鑰

    箭頭的意思代表有了私鑰即可推演出公鑰,而有了公鑰就可以推演出錢包地址了。反過來說,要產生一個新錢包,首先要產生私鑰,然後對應著私鑰就會有一組公鑰,最後藉由公鑰就可以推斷出產生的錢包地址了。

    3.2. 私鑰的產生

    私鑰在 Ethereum 中,實際上是代表一個介於 1 ~ 2256 的數字。也就是說,產生的過程實際上就是在 1 ~ 2256 之間選擇一個數字。2256 是個什麼量級的數字呢?換算成十進位是 1.158 x 1077 左右。作為用來想像這是什麼數字的對照,目前科學家預測人類可視的宇宙的總原子數量大概是 1080 個左右。所以使用正確的工具來產生私鑰時,理論上產生出相同私鑰是個近乎不可能的事情。

    一把 256-bit 的私鑰,一般來說會透過十六進位的表示法來表達,可以表達成類似這樣的字串:

    f8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315

    接著,對應這把私鑰,可以演算出一把公鑰。公鑰所代表的數字是由私鑰的數字乘上某個數字:

    046e145ccef1033dea239875dd00dfb4fee6e3348b84985c92f103444683bae07b83b5c38e5e2b0c8529d7fa3f64d46daa1ece2d9ac14cab9477d042c84c32ccd0

    最後,對這個公鑰做 Keccak-256 雜湊,然後取出最不具代表性的最後 20-bit,就形成了錢包的地址了:

    001d3f1ef827552ae1114027bd3ecf1f086ba0f9

    或者也可以寫成以 0x 開頭,代表這個字串是使用十六進位編碼:

    0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9
    3.3. 每個交易存放的內容

    每一個交易紀錄中,都會由以下這幾個欄位所組成。

    • nonce - 由交易發起者寫入的循序序號
    • gas price - 交易發起者願意為這筆交易付出的 gas 價格(以 wei 來表達,也就是交易手續費)
    • gas limit - 交易發起者願意為這筆交易付出的 gas 單位
    • to - 接受這筆交易的錢包地址
    • value - 交易金額
    • data - 其他訊息或者是智能合約相關的指令
    • v, r, s - 代表交易發起者的 ECDSA 數位簽章

    其中,交易內實際上是以類似封包的結構來存放,也就是說第 n ~ n+i 的位置應該是什麼欄位這樣。所以交易內會以最簡潔只寫值的方式來存放。

    如果仔細一點看的話,可能會發現交易內容裡並沒有 from 的資訊,那麼到底要如何知道這是誰發出來的交易呢?這時就要利用 v, r, s 了 [7-8]。v, r, s 這三個參數合併起來可以組成 ECDSA 數位簽章,代表交易發起者對這個交易訊息簽名以證明這確實是他發出的。驗證時,可以很輕易地從 v, r, s 算出一個公鑰,然後公鑰又可以很容易推出錢包帳號,因此 v, r, s 就足以代表交易發起者了。

    3.4. 發起交易時的執行流程

    在 Ethereum 中發起交易時,交易會先被自己聯繫上的節點,然後節點再把交易廣播到他知道的其他節點,以類似 Gossip Protocol 的形式廣播出去。每個礦工節點都會先將收到的交易暫存起來放在一個籃子裡,當礦工節點要開始打包 block 時,會從籃子裡面挑出他想要打包的交易。通常來說,礦工會以利潤最高的交易優先,也就是交易發起者願意付出的 gas 越多越好。因為礦工節點成功打包交易時會獲得的「手續費」,是由打包成功的獎勵金再加上他這輪打包的所有交易付給他的 gas 的總和,因此選擇 gas 越多的交易對他越有利。不過需要注意的是,如果交易中夾帶了需要執行的智能合約,那麼智能合約越龐大,礦工就越不傾向去執行它,這筆交易就越難被礦工選上。

    因為交易會以不特定形式廣播、打包交易時會有各種因素來選擇被打包的交易、以及打包完成後還有可能有 uncle block 的狀況,因此交易在何時才會被礦工節點打包,在 Ethereum 中是無法明確被保證的。以以太幣目前的公鏈來說,目前是每 15 秒會打包一個新的 block,考慮到 uncle block 的狀況的話,通常會等看到交易被寫入區塊鏈後再多等 12 個 block 的時間(也就是 15 x 12 = 180 秒),才能大致確保這個交易應該不會被推翻了。但這個時間是不包含等待礦工節點選到這筆交易的時間,實務上一個以太幣的交易大多都得花上數十分鐘到數小時才會完成。

    3.5. nonce 的重要性:如何確保不會重複消費?

    上面講到了,交易被廣播時存在著各種不確定因素,而且每個礦工節點都是依據自己的帳本自行運作的,那麼該怎麼解決共時的議題呢?想像一下,如果有個錢包裡面有 10 個 ether,接著這個錢包發起了兩個轉帳的交易,把 8 個 ether 匯給兩個不同的錢包地址,而且最糟糕的狀況是這兩筆交易在廣播的過程中,被兩個不同的礦工節點選上且打包了,Ethereum 系統該如何解決呢?

    要解決這個議題,就必須引入交易中的「nonce」這個參數的概念了。nonce 是個從 0 開始的序號,代表某個錢包的第 n 筆交易。例如 0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9 這個錢包發起的第一筆交易 nonce 必須寫上 0,第二筆交易必須寫上 1,以此類推。當礦工節點在打包交易時,如果看到一筆交易是來自某錢包,上面標註的 nonce 是 n,但自己手上的帳本中這個錢包的最後一次交易的 nonce 是 n-2,這時礦工節點會預期 nonce = n-1 的這個交易大概在網路傳輸時延遲了,因此會把 nonce = n 的交易先繼續放在籃子擺著,等到 nonce = n-1 的交易出現在帳本上或者籃子裡時,才會開始依序處理 nonce = n-1 和 nonce = n 的交易。換言之,在這個概念底下,雖然整個 Ethereum 是完全可共時操作的分散式系統,但對每個錢包地址來說,錢包地址的交易都是單一路線循序執行的 singleton,因此「重複消費」這類的問題先天就不會發生在 Ethereum 上。

    不過這裡也同時會存在一個很需要注意的狀況,如果在發起交易時 nonce 亂寫,有很高的機率會導致礦工節點把你的交易一直延遲(因為他一直等不到中間少掉的交易),或者是直接把你的交易廢棄掉(如果出現已經出現過的 nonce 的話)。因此如果是在需要自己寫 nonce 的狀況,要非常謹慎地去處理它。

    也因為存在這樣的機制,外加一些來自於像是 geth 或 parity 這些 client 的緣由 [7],目前大部分的實作在嘗試針對同一個錢包做大量的交易發佈時,通常不會考慮 concurrent access 的操作模型,因為無論如何都難以避免單點故障的問題(總得有個分配及管理 nonce 這個交易序號的方法,而這個管理的地方就形成單點故障的成因)。

    參考資料
    1. ethereumbook
    2. Distributed Ledger
    3. 以太坊(Ethereum ETH)的奖励机制详解
    4. 以太坊硬分叉:區塊鏈升級倒數
    5. 区块链分叉是怎么回事儿?终于懂了
    6. ethereumbook – Keys and Addresses
    7. ethereumbook – Transactions
    8. 基于以太坊(Ethereum)完成对数据的签名及验证
    9. 創辦人 Vitalik Buterin:有了新技術,以太坊交易能力兩年就能趕上 Visa!

    沒有留言: