1. 程式人生 > >[以太坊源代碼分析] V. 從錢包到客戶端

[以太坊源代碼分析] V. 從錢包到客戶端

多個 本地文件 string 相同 即將 接收 軟件 避免 的人

以太坊作為一種數字貨幣以太幣的運行系統,顯然它也會有類似於錢包的客戶端程序,用來提供管理賬戶余額等功能。我們知道,存放(或者綁定,掛靠)以太幣的賬戶,在代碼中以Address類型變量存在,所以能夠管理多個以太坊賬戶應該屬於客戶端程序基本功能之一。本文會從管理賬戶信息的代碼包開始,自底向上的介紹以太坊客戶端程序的一些主要模塊。
1. 管理賬戶信息的代碼包accounts

在以太坊源代碼的accounts代碼包中,呈現賬戶地址的最小結構體叫Account{},它的主要成員就是一個common.Address類型變量;管理Account的接口類叫Wallet,類如其名,<Wallet>聲明了諸如緩存Account對象及解析Account對象等操作,管理多個<Wallet>對象的結構體叫Manager,這些類型的UML關系如下圖所示:

在accounts代碼包內部的各種結構體/接口中,accounts.Manager在相互調用關系上無疑是處於頂端的,它本身是公共類,向外暴露包括查詢單個Account,返回單個或多個Wallet對象,訂閱Wallet更新事件等方法。在其內部它維持一個Wallet列表,通過每個Wallet實現類持有一組Account賬戶對象,並通過一個event.Feed成員變量來管理所有向它訂閱Wallet更新事件的需求。
Manager訂閱Wallet的更新事件
著重介紹一下這裏的訂閱(subscribe)操作,Manager的Subscribe()函數定義如下:
[plain] view plain copy
// /accounts/manager.go
func (am *Manager) Subscribe(sink chan<- WallletEvent) event.Subscription {
return am.feed.Subscribe(sink)
}
首先註意這個Subscribe()函數是讓外部調用對象 向該Manager作訂閱的操作,事實上該Manager本身也是通過相同的訂閱機制去獲知新添的Wallet對象,它的成員變量updates就是該Manager本身得到所訂閱事件的通道。其次,Manager.Subscribe()函數只有一個chan參數,由於golang語言中channel機制的強大,訂閱操作僅僅需要一個chan對象就足夠了,真是簡單之極,根本不必知道背後是誰發起了訂閱。盡管如此,這裏依然值得思考的是,究竟是什麽對象向Manager發起了訂閱呢?其實,向某個Manager對象訂閱Wallet更新事件的,正是另外一個Manager對象,也就是<Backend>的實現類。
得出以上這個結論,是很有意義的。後面可以了解到,accounts.Manager主要作為eth.Ethereum(或者les.Ethereum)的一個成員存在,而這個eth.Ethereum是以太坊客戶端程序中最主要的部分,它以服務的形式提供幾乎所有以太坊系統運行所需的功能,所以一個以太坊客戶端可視為一個accounts.Manager的存在,那麽真相就是,所有以太坊客戶端之間在通過accouts.Manager相互訂閱Wallet更新事件。

除Manager之外,這裏其他幾個重要的結構體還包括:
event.Feed{}:它可以管理一對多的訂閱模式,每個調用者提供一個chan對象,用以發送所訂閱的內容。Feed{}處理的訂閱內容是類型泛化的,而每一個Feed{}對象,在其生命周期內,只能處理一種類型的訂閱內容,即向chan對象發送的value。Feed.Subscribe()方法返回<Subscription>接口的實現體feedSub{},Feed.Subscribe()幫助Manager實現了<Backend>所聲明的方法Subscribe()。在Feed結構體內部,CaseList被用來管理所有訂閱者發過來的chan對象。
accounts.Account{}:它的成員除了一個common.Address類型,即20bytes長的地址變量外,還有一個可選成員URL,可以是網址,也可以是本地存儲的路徑+文件全名。在以網址形式存在時,URL.Scheme就是網絡協議名,而作為本地存儲文件時,URL.Scheme是字符串常量"keystore"。
accounts.<Wallet>:它很像一般意義上的“錢包”,其管理的多個Account,恰似個人用戶在現實中擁有的多個銀行賬戶,每個Account上的Ether余額,可從數據庫(core.state.StateDB)中查詢。<Wallet>接口聲明的函數中,尤其需要註意的是SignXXX(),其中SignTx()是對一個Transaction(tx)對象進行數字簽名,SignHash()是對一個Hash值進行數字簽名,由於任何一個對象(只要可序列化)可以作Hash運算,所以這裏SignHash()其實是針對任何一個對象,尤其是Block區塊作數字簽名。
<Wallet>是接口類型,它的實現體包括軟件錢包(keystore.keystoreWallet)和硬件錢包(usbwallet.wallet),註意這裏的硬件錢包是有實物的。<Wallet>之下的代碼體系對於外部都不是公共的,所有向外暴露的“錢包”對象以及相關更新事件,都是以<Wallet>形式存在。
軟件實現的Wallet - keystore
軟件實現Wallet主要通過本地存儲文件的方式來管理賬戶地址。同時,<Wallet>對象需要對交易或區塊對象提供數字簽名,這需要用到橢圓曲線數字簽名(ECDSA)中的公鑰+密鑰,而每個公鑰也是某個賬戶地址(Address)的來源,所以我們也需要本地存儲ECDSA的公鑰密鑰信息。以太坊中這個通過本地存儲文件的方案實現accounts.<Wallet>功能的機制被成為keystore。
<Wallet>的軟件錢包實現的相關代碼都處於/accounts/keystore/路徑下,這組代碼的主要UML關系如下圖:

keystoreWallet{}:它是accounts.<Wallet>的實現類,它有一個Account對象,用來表示自身的地址,並通過Account.URL()方法,來實現上層接口<Wallet>.URL()方法;另外有一個KeyStore{}對象,這是這組代碼中的核心類。
KeyStore{}:它為keystoreWallet結構體提供所有與Account相關的實質性的數據和操作。KeyStore{}內部有兩個作數據緩存用的成員:
accountCache類型的成員cache,是所有待查找的地址信息(Account{}類型)集合;
map[Address]unlocked{}形式的成員unlocked,由於unlocked{}結構體僅僅簡單封裝了Key{}對象(Key{}中顯式含有數字簽名公鑰密鑰對),所以map[]中可通過Address變量查找到該地址對應的原始公鑰以及密鑰。
另外,KeyStore{}中有一個<keyStore>接口類型的成員storage,用來對存儲在本地文件中的公鑰信息Key做操作。
Unlocked{}:公鑰密鑰數據類Key{}的封裝類,其內部成員除了Key{}之外,還提供了一個chan類型變量abort,它會在KeyStore對於公鑰密鑰信息的管理機制中發揮作用。
Key{}:存放數字簽名公鑰密鑰的數據類,其內部顯式存儲了一個ecdsa.PrivateKey{}類型的成員變量,前文介紹過,Golang原生代碼包中的ecdsa.PrivateKey{}中含有PublicKey{}類型的成員。而Key{}中同時攜帶Address類型成員變量,也可以避免公鑰向地址類型轉化的操作重復發生。
<keyStore>:這個接口類型聲明了操作Key的函數,註意它與KeyStore{}在名字上僅有一個字母大小寫的差異。
keyStorePassphrase{}:<keyStore>接口的實現類,它實現了以Web3 Secret Storage加密方法為公鑰密鑰信息進行加密管理。
accountCache{}:在內存中緩存keystore中某個已知路徑下所有Account對象,可提供由Address類型查找到對應Account對象的操作。
fileCache{}:keystore中可觀察到的文件的緩存,它可對某個路徑下存放的文件進行掃描,分別返回新增文件,缺失文件,改動文件的集合。
watcher{}:用來監測某個路徑中存儲的賬戶文件的變化,可以定時調用accountCache的方法對文件進行掃描。
本地文件顯式存儲賬戶信息

accountCache緩存的帳號信息,均來自於某個已知路徑下存儲的本地文件集合。每個文件都是JSON格式,以顯式存放Address: {Address: "@Address"},所以accountCache在讀取文件後,可以直接轉化成Account{}對象,在代碼中使用。這裏以顯式文件存儲Address信息沒有任何問題,既不用擔心Address信息泄露造成危害(無法從Address反向解析出源頭的ECDSA所用公鑰),又可以方便代碼調用。
在使用中,watcher對象會維護一個定時器,不斷的通知accountCache掃描某個給定的路徑;accountCache會調用fileCache對象去掃描該路徑下的文件,並根據fileCache返回的三種文件集合:新添文件、缺失文件、改動文件,在自身維護的Account集合中作相應操作。
以本地加密文件存儲公鑰密鑰

Key{}通過ecdsa.PrivateKey對象從而同時攜帶ECDSA所用的公鑰密鑰,所以這裏涉及到公鑰密鑰部分,都是針對Key對象做的操作。keystore機制中,在本地存儲的是經過加密的Key對象的JSON格式,所用的加密方法被稱為Web3 Secret Storage,其實現細節可在ethereum git wiki上找到。下圖是該存儲方式的簡單示意圖:

對一個加密存儲的Key對象做操作時,總共需要三個參數,包括調用方提供一個名為passphrase的任意字符串,以及keyStorePassphrase{}中給定的兩個整型數scryptN,scryptP,這兩個整型參數在keyStorePassphrase對象生命周期內部是固定不變的,只能在創建時賦值。這樣不管是每次新存儲一個Key對象,還是取出一個已存的Key對象,調用方都必須傳入正確的參數passphrase,所以在實際應用中,以太坊錢包的客戶必須自行記憶該字符串。實際上,客戶為每個賬戶創建的密碼password,程序中正是這個加密參數passphrase。
取出的公鑰密鑰,在內存中限時公開

Key{}對象從加密過的本地文件中取出後,會被封裝成unlocked{}對象,並被KeyStore放進其map[Address]*unlocked類型成員中。由於公鑰密鑰的重要性,顯然keystore中存有的unlocked對象也應該控制公開時長。對於不同的時限需求,KeyStore{}提供了如下兩個函數:
[plain] view plain copy
// accounts/keystore/keystore.go
func (ks *KeyStore) Unlock(a accounts.Account, passphrase string) error {
return ks.TimedUnlock(a, passphrase, timeout:0)
}
func (ks *KeyStore) TimedUnlock(a accounts.Account, passphrase string, timeout time.Duration) error
TimedUnlock()函數會在給定的時限到達後,立即將已知Account對應的unlocked對象中的PrivateKey的私鑰銷毀(逐個bit清0),並將該unlocked對象從KeyStore成員中刪除。而Unlock()函數會將該unlocked對象一直公開,直到程序退出。註意,這裏的清理工作僅僅是針對內存中的Key對象,而以加密方式存在本地的key文件不受影響。

keystore機制以本地文件的形式提供對賬戶信息和數字簽名公鑰私鑰的存儲和讀取,從而以軟件方式實現了accounts.<Wallet>的功能。它的兩套獨立的本地存儲文件,既考慮了公鑰私鑰的加密又兼顧了賬戶信息的快速讀取,體現出很全面的設計思路。
硬件設備實現的Wallet

以太坊除了提供軟件實現的錢包之外,還有硬件實現的錢包。當然,對於硬件錢包,以太坊代碼中肯定有上層代碼對此進行封裝。這些代碼都處於/accounts/usbwallet/下,它們的UML關系如下圖所示:

pkg accounts/usbwallet中 主要的結構包括wallet{}, Hub{}以及<driver>接口。
wallet{}結構體實現了上層接口accounts.<Wallet>,向外提供accounts.<Wallet>的函數實現;
<driver> 接口從命名就看得出來,它用來封裝下層硬件實現錢包的代碼。盡管嚴格來說,這個接口及其實現體跟一般意義上的"驅動程序"沒什麽關系。
ledgerDriver{},trezorDriver{} 分別對應於兩家供應商發布的硬件數字貨幣錢包,Ledger 和 Trezor 分別是品牌名。它們都可以支持包括以太幣在內的多種數字貨幣。
<Hub> 結構體,它實現了上層accounts.<Backend>接口,地位相當於account.Manager。從代碼來看,所有硬件實現的<Wallet>部分,都會由這個Hub對象來管理。Hub{}向外以<Backend>接口的形式暴露,這樣更上層的代碼就不必區分下層錢包的具體實現是軟件還是硬件了。
需要註意的是,在目前以太坊的主幹代碼中,硬件實現錢包有關數字簽名部分,目前只能提供針對交易進行原生的數字簽名功能,即僅僅<Wallet>.SignTx()函數可用,其他簽名功能包括SignHash(),以及SignXXXWithPassphrase()均不支持,不知道其他分支代碼是否有所不同。
2. Ethereum服務

在了解accounts代碼包之後,我們就可以來看看以太坊源代碼中最著名的類型,同時也是客戶端程序中最核心的部分 - eth.Ethereum。能夠以整個系統名命名的結構體類型,想必功能應該非常強大,下圖是它的一個簡單UML圖:

上圖中央就是eth.Ethereum類型,四周都是它的成員變量類型,我們來看看其中哪些是已經了解過的:
ethdb.<Database> 是對應於core.state.StateDB{}的函數接口,有了<Database>接口類型的成員變量,可以在使用中調用StateDB{}
consensus.<Engine> 是共識算法代碼包向外暴露的函數接口,其實現包括基於PoW的Ethash算法,和基於PoA的Clique算法。
accounts.Manager 是管理賬戶信息和數字簽名公鑰密鑰信息的代碼。
miner.Miner 是挖掘新區塊的代碼,它可以管理挖掘新區塊的整個流程,調用consensus.<Engine>完成新區塊的授勛/認證,並向外廣播 新區塊事件。
core.TxPool 是積累新交易(Transaction, tx)對象的代碼,每個新挖掘區塊,都需要從TxPool中監聽Tx更新事件並獲取新交易集合以組裝成新區塊。
core.BlockChain 是管理整個區塊鏈數據結構的結構體。

以上這些都是前文中都已經具體介紹過的代碼部分,接著再來看看那些新的類型:
node.<Service>,這是客戶端程序用以對節點進行功能抽象的接口。每個客戶端都把自身視為網絡中的一個節點(node),這個節點向外所提供的所有功能,由<Service>接口來定義。
<LesServer>:實現LES協議的函數接口,eth.<LesServer>其實是為了調用les.LesServer{}而專門創建的本地函數接口。
EthApiBackend, 它是幫助Ethereum把各項功能以RPC 服務(service)的方式暴露出去的模塊,外部調用方以API的方式調用這些功能/服務。
ProtocolManager,用來管理p2p通信。以太坊內部把每個個體(peer)與其他個體群之間的通信協議稱為一種基於p2p通信協議的新協議。考慮到eth.Ethereum提供功能的全面性,它也被稱為全節點服務的通信協議。
ProtocolManager的成員變量中,Fetcher用以接收其他個體發來的宣布挖掘出新區塊的消息並決定向對方獲取需要的部分,Downloader負責整個區塊鏈結構的同步(下載)。
特別介紹下LES:Light Ethereum Subprotocol(LES) 是為輕量級客戶端專門設計的子協議。相比於eth.Ethereum提供全節點服務的客戶端,那些輕量級客戶端不參與挖掘新區塊,在與其他節點的通信中僅僅下載每個區快的頭部(Block.Header),對於區塊鏈的其他部分僅僅按需對部分同步。eth.Ehereum同時也支持LES,這樣一個提供全節點服務的客戶端就可以與其他輕量級客戶端以相同的協議通信了。

對數字貨幣稍有了解的人應該都清楚p2p通信協議對於此類“去中心化”系統的重大意義。的確,把p2p通信協議稱為以太坊系統的基石之一都不為過,從代碼角度考慮, ProtocolManager及其代碼族 也屬於eth代碼包的一部分,不過由於這部分代碼比較復雜,會在下一篇文章中專門介紹這些通信協議的實現細節。
3.以太坊客戶端程序

在了解eth.Ethereum這個核心服務之後,客戶端執行程序也就呼之欲出了。首先有一個node.Node{}作為承載類似eth,Ethereum這樣服務模塊的容器:

Node{}對象內部有一個Service列表,所有實現了node.<Service>接口的對象都可以存放在Node裏,比如eth.Ethereum。
接著,go-ethereum的客戶端程序geth的代碼就很簡單了:
[plain] view plain copy
// www.yibaoyule1.com/cmd/geth/main.go
func main(www.wangcai157.com) {
if err := app.Run(os.www.wanhengyl157.com Args)www.jpg157.com ; err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func geth(ctx *cil.Context) error {
node := makeFullNode(ctx)
startNode(ctx, node)
node.Wait(www.471060.com )
return nil
}
...
從命令行啟動geth客戶端的程序就是以上,創建一個node.Node對象,從配置中讀出想要註冊的服務名,然後一一創建相應的服務對象,Node去啟動它們。
geth是go-ethereum自帶的命令行客戶端程序,目前市場上也存在許多種其他的以太坊客戶端程序,有興趣的讀者可以去找來看看,有源代碼就最好了可以比較一下。
小結:

以太坊的客戶端程序,原本應該是剛接觸以太坊的初學者最早遇到的部分之一。因為下載完整個源代碼包之後,按照相應語言的提示進行編譯,就會得到一個客戶端的可執行程序。我最初首先看的客戶端的代碼,當追溯到eth.Ethereum{}結構體,看到那麽多模塊的成員變量時,就一下子明白了,整個以太坊系統運行起來的基礎模塊是哪些部分。
以太坊中代碼中,accounts.Manager是管理賬戶信息的模塊。Manager可以管理多個<Wallet>的實現,每個<Wallet>實現擁有多個Account賬戶,每個Account對應一個Address地址,而以太幣Ether存放於每個Address上。以太坊同時提供軟件版和硬件版的<Wallet>實現。
以太坊中,每個Address類型變量均來自於橢圓曲線數字簽名算法(ECDSA)所用的公鑰,因此錢包程序還必須提供管理數字簽名公鑰密鑰的功能。軟件版accounts.<Wallet>實現叫keystore,通過在本地文件系統中分別顯式存儲賬戶信息和加密存儲公鑰密鑰的方式,提供以上功能。
以太坊客戶端程序之間,會通過accounts.Manager模塊相互訂閱Wallet更新事件,以保證每個客戶端個體(peer),都能及時更新全網絡中的完整Wallet列表。
客戶端程序的核心是eth.Ethereum,它以RPC service的形式,向外提供內部各模塊的功能,諸如挖掘區塊, 數據庫讀寫,p2p下載等。

[以太坊源代碼分析] V. 從錢包到客戶端