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

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

1. 管理帳戶信息的代碼包accounts

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

在accounts代碼包內部的各類結構體/接口中,accounts.Manager在相互調用關係上無疑是處於頂端的,它自己是公共類,向外暴露包括查詢單個Account,返回單個或多個Wallet對象,訂閱Wallet更新事件等方法。在其內部它維持一個Wallet列表,經過每一個Wallet實現類持有一組Account帳戶對象,並經過一個event.Feed成員變量來管理全部向它訂閱Wallet更新事件的需求。golang

Manager訂閱Wallet的更新事件

着重介紹一下這裏的訂閱(subscribe)操做,Manager的Subscribe()函數定義以下:算法

 

[plain]  view plain  copy
 
  1. // /accounts/manager.go  
  2. func (am *Manager) Subscribe(sink chan<- WallletEvent) event.Subscription {  
  3.     return am.feed.Subscribe(sink)  
  4. }  

首先注意這個Subscribe()函數是讓外部調用對象 向該Manager做訂閱的操做,事實上該Manager自己也是經過相同的訂閱機制去獲知新添的Wallet對象,它的成員變量updates就是該Manager自己獲得所訂閱事件的通道。其次,Manager.Subscribe()函數只有一個chan參數,因爲golang語言中channel機制的強大,訂閱操做僅僅須要一個chan對象就足夠了,真是簡單之極,根本沒必要知道背後是誰發起了訂閱。儘管如此,這裏依然值得思考的是,到底是什麼對象向Manager發起了訂閱呢?其實,向某個Manager對象訂閱Wallet更新事件的,正是另一個Manager對象,也就是<Backend>的實現類。spring

 

得出以上這個結論,是頗有意義的。後面能夠了解到,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關係以下圖:app

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
 
  1. // accounts/keystore/keystore.go  
  2. func (ks *KeyStore) Unlock(a accounts.Account, passphrase string) error {  
  3.     return ks.TimedUnlock(a, passphrase, timeout:0)  
  4. }  
  5. 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
 
  1. // /cmd/geth/main.go  
  2. func main() {  
  3.     if err := app.Run(os.Args); err != nil {  
  4.         fmt.Fprintln(os.Stderr, err)  
  5.         os.Exit(1)  
  6.     }  
  7. }  
  8. func geth(ctx *cil.Context) error {  
  9.     node := makeFullNode(ctx)  
  10.     startNode(ctx, node)  
  11.     node.Wait()  
  12.     return nil  
  13. }  
  14. ...  

從命令行啓動geth客戶端的程序就是以上,建立一個node.Node對象,從配置中讀出想要註冊的服務名,而後一一建立相應的服務對象,Node去啓動它們。

geth是go-ethereum自帶的命令行客戶端程序,目前市場上也存在許多種其餘的以太坊客戶端程序,有興趣的讀者能夠去找來看看,有源代碼就最好了能夠比較一下。

小結:

以太坊的客戶端程序,本來應該是剛接觸以太坊的初學者最先遇到的部分之一。由於下載完整個源代碼包以後,按照相應語言的提示進行編譯,就會獲得一個客戶端的可執行程序。我最初首先看的客戶端的代碼,當追溯到eth.Ethereum{}結構體,看到那麼多模塊的成員變量時,就一會兒明白了,整個以太坊系統運行起來的基礎模塊是哪些部分。

    1. 以太坊中代碼中,accounts.Manager是管理帳戶信息的模塊。Manager能夠管理多個<Wallet>的實現,每一個<Wallet>實現擁有多個Account帳戶,每一個Account對應一個Address地址,而以太幣Ether存放於每一個Address上。以太坊同時提供軟件版和硬件版的<Wallet>實現。
    2. 以太坊中,每一個Address類型變量均來自於橢圓曲線數字簽名算法(ECDSA)所用的公鑰,所以錢包程序還必須提供管理數字簽名公鑰密鑰的功能。軟件版accounts.<Wallet>實現叫keystore,經過在本地文件系統中分別顯式存儲帳戶信息和加密存儲公鑰密鑰的方式,提供以上功能。
    3. 以太坊客戶端程序之間,會經過accounts.Manager模塊相互訂閱Wallet更新事件,以保證每一個客戶端個體(peer),都能及時更新全網絡中的完整Wallet列表。
    4. 客戶端程序的核心是eth.Ethereum,它以RPC service的形式,向外提供內部各模塊的功能,諸如挖掘區塊, 數據庫讀寫,p2p下載等。
相關文章
相關標籤/搜索