以太坊源碼分析報告_第1頁
以太坊源碼分析報告_第2頁
以太坊源碼分析報告_第3頁
以太坊源碼分析報告_第4頁
以太坊源碼分析報告_第5頁
已閱讀5頁,還剩47頁未讀 繼續免費閱讀

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領

文檔簡介

以太坊源碼分析報告前言以比特幣為代表的虛擬貨幣時代,代表著區塊鏈1.0,基于P2P網絡構建,實現了去中心化的數字貨幣交易功效。但是1.0只滿足了虛擬貨幣的需要,很難普及到其它行業。例如比特幣只提供了有限的非圖靈完備的腳本能力,不大可能在其上搭建第三方的應用。發展到區塊鏈2.0,便出現了以以太坊為代表的智能合約平臺,提供強大的合約編程環境,能夠實現復雜的業務邏輯。與比特幣系統相比以太坊并沒有本質的區別,只是全方面實現和支持智能合約,讓區塊鏈技術不只是發幣。本文以以太坊的官方go語言版本實現go-ethereum為目的,分析其源碼實現。以太坊架構介紹以以太坊實現Web3.js核心去中心化應用層智能合約層EVMRPC區塊鏈管理模塊共識模塊挖礦模塊網絡模塊P2P加解密庫levelDBSolidityMath&Number賬戶管理模塊最頂層是去中心化應用層,即DApp。它使用truffle開發測試框架(最流行)編寫布署和測試客戶端,并通過web3.js和智能合約層交互;智能合約層通過以太坊虛擬機EVM交互解決BlockChain及共識有關的事務,同時通過RPC合同進行挖礦和網絡層事務的交互;區塊鏈管理模塊圍繞交易、塊和狀態進行管理,涉及區塊的同時驗證及異常和分叉解決、交易的廣播接受解決和驗證及執行、底層數據的讀寫更新等;共識模塊是制訂的認定區塊正當的機制,涉及PoW(ProofOfWork工作量證明,以太坊使用變種的Ethash算法)及PoS(ProofOfStake權益證明,只在測試網絡中使用),符合共識算法的新區塊才會被節點承認和接納,鏈接到分布式賬本中,同時才干讓礦工得到收益;挖礦模塊管理挖礦工作,將爭奪記賬權的過程分解成多個并行子任務進行;賬戶管理模塊管理以太坊系統中的賬戶,涉及普通賬戶及合約賬戶的生成和管理,尚有錢包及密鑰的生成、導入和導出;網絡模塊管理著系統中的Peer、Protocol、Downloader、Sync等角色,為整個分布式網絡提供節點間的共識基礎。涉及節點對端連接的動態管理、ETH/LES/LES2合同的支持、各類數據包的下載和同時;架構的最底層功效為上層模塊提供了基礎P2P網絡的通訊、secp251和sha3等加解密算法、高效的LevelDB鍵值對存儲數據庫、合約語言基礎及大數字的基本運算。源碼目錄構造基本概念在以太坊的YellowPaper中,把整個以太坊當作是一種基于交易的狀態機。從創世狀態開始,在一批交易執行后便進入到下一種新的狀態,直到現在的終態。創世態創世態狀態1交易狀態N……..交易交易Block0HeaderTXsBlock1HeaderTXs

BlockNHeaderTXs

……BlockChain交易當一種賬戶向另一種賬戶發送一筆被簽名的消息數據包時,就產生了一筆交易。賬戶能夠是普通賬戶,也能夠是合約賬戶。交易執行時需要耗費手續費。交易Transaction定義在core/types/transaction.go中:Transaction的主體定義在txdata中,其它組員都只是交易慣用信息的緩存:hash:交易RLP編碼后的哈希值;size:交易RLP編碼后的大小;from:交易的發送地址,它并不存儲在交易體里,而是由txdata中的V,R,S值推導出來;交易的重要信息包含在txdata中,涉及以下字段:AccountNonce:代表發送賬戶發出的第幾筆交易;Price:交易發送者樂意支付的一單位gas費用的價格;GasLimit:交易執行所耗費最大的gas值。如果超出該值則交易失敗;Receipient:交易的接受地址;Amount:從發送地址向接受地址轉移的以太幣數量;Payload:可選,在創立合約時表達合約代碼,或者調用合約時調用參數;V|R|S:secp256k1簽名數據;Hash:同Transaction.hash,在轉換為Json格式時用到;區塊一種區塊包含了一系列的交易,礦工節點收集本地發起的及網絡中其它節點廣播的新交易,驗證交易的有效性,然后將它們打包到一種原始區塊中,最后通過挖礦得到一種數學機制的“工作量證明”寫到該區塊,從而得到一種新的正當區塊,廣播到網絡中,在其它礦工驗證區塊有效后添加到主鏈上。區塊的定義在core/types/block.go中:header:Block的核心,由背面給出其定義;uncles:叔塊,以太坊對孤塊(發現晚但是正當的新塊)的解決和比特幣的拋棄式解決不同,由于以太坊十幾秒的出塊間隔會造成大量的孤塊,因此以太坊激勵礦工引用孤塊成為叔塊并支付酬勞,減少昂貴成本的浪費,使得主鏈更重提高安全性,也緩和礦池中心化問題;transactions:區塊打包的一批交易;td:TotalDifficulty,總難度值,主鏈是td值最大的鏈;ReceivedAt:統計塊的接受時間;ReceivedFrom:統計塊的發送peer;Header的定義也在core/types/block.go中:ParentHash:父區塊的哈希值,除了創世塊以外每個區塊都有且只有一種父區塊;UncleHash:Block.uncles的RLP編碼后的哈希值;Coinbase:挖出該區塊的礦工地址,礦工的挖礦收益都是發給這個地址;Root:“statetrie”的根節點的RLP哈希值。全部賬戶對象逐個插入一種Merkle-PatricaTrie(MPT)構造里形成一棵“statetrie”;TxHash:“txtrie”的根節點的RLP哈希值。Block.transactions中的全部tx對象逐個插入一種MPT構造,形成一棵”txtrie”;ReceiptHash:“receipttrie”的根節點的RLP哈希值。Block的每一筆交易執行完后會生成一種Receipt數組,這個數組中的全部Recipt被逐個插入一種MPT構造里,形成一棵“receipttrie”;Bloom:Bloom過濾器,由Block中的全部交易收據中的log生成有關地址和topic的索引,用于快速判斷指定的條件(指定地址或指定的事件)與否存在于一組已知的Log集合;Difficulty:Block的難度值。由父塊的難度值和時間戳計算得到;Number:Block的序號,等于其父塊Number+1;GasLimit:Block內的全部gas消耗的上限;GasUsed:Block內全部交易執行后實際消耗的gas總和;Time:Block的創立時間;Extra:和該Block有關的任意字節數組;MixDigest:256位的哈希值,和Nonce一起用來證明該塊持有有效的工作量證明;Nonce:64位的哈希值,和MixDigest一起用來證明該塊持有有效的工作量證明;區塊鏈上節說到Block的組員header.ParentHash是父區塊的指針,把全部區塊按照這種鏈接有關系連接起來,便形成了一條從創世塊到現在塊的反向鏈表,即區塊鏈。該區塊鏈包含了全部的歷史交易信息,且只有在共識機制下被礦工承認的正當區塊才干被添加到鏈中,稱為主鏈。如果有多個正當區塊同時產生,但是由于網絡延時問題被不同的礦工節點接受到添加到鏈上,便會出現分叉。以太坊使用“GHOST(GreedyHeaviestObservedSubtree)”機制擬定有效的途徑,即選擇一種擁有最多計算量的途徑。分叉分叉分叉權威鏈,主鏈,擁有最多計算量創世塊BlockChain的定義和維護操作在core/blockchain.go中:chainConfig:包含鏈配備,涉及以太坊各歷史版本升級時的區塊高度,鏈ID,采用的共識引擎等;cacheConfig:重要用于控制Trie的緩存開關和大小;db:底層db操作接口,用于讀寫leveldb數據;triegc:寄存已插入數據庫的Block對應的trie樹根,在超出cacheConfig配備的內存限制時將其回收;gcproc:累計解決正當塊的總時間,超出配備的trie刷新到磁盤的等待時間的話,在trie樹內存超出上限時選擇一種舊塊把其trie寫到磁盤,并重置;hc:頭鏈,把區塊鏈的頭部數據連接起來形成的鏈。由于區塊鏈的諸多操作如驗證、獲取塊信息都只需要頭部,因此獨立出來方便操作調用;rmLogsFeed/chainFeed/chainSideFeed/logsFeed/scope:事件訂閱有關,其它組件需要監聽主鏈的狀態變化來觸發對應的解決過程;genesisBlock:創世塊,在peer握手時的溝通協商與否是始于同一區塊;mu/chainmu/procmu:互斥鎖;currentBlock:該節點的現在塊,即它所承認的最新塊;currentFastBock:快速同時模式下的現在塊;stateCache:封裝trie.Database加了一層緩存cachingDB以快速訪問trie,重要用于根據trieroot讀入trie樹,而trie樹里包含了全部賬戶狀態;bodyCache/bodyRLPCache/blockCache:body/RLP編碼body/block的緩存,用于快速訪問;futureBlocks:如果新區塊的時間戳是在距現在15s之后,或者父區塊在futureBlocks中則把新區塊放到futureBlocks中,然后定時加到區塊鏈里;quit:接受退出信號;running:服務正在運行標志;procInterrupt:服務中斷運行標志,如果它為1則停止Block解決;wg:等待鎖,用于服務停止時等待運行的goroutine結束;engine:共識引擎接口,共識有關操作都是調用這個接口;processor:區塊解決接口,用于運行區塊里的交易并生成收據;validator:塊和狀態的檢查接口;vmConfig:EVM虛擬機配備,在執行交易生成新的EVM時使用;badBlocks:已經的壞區塊。程序啟動從磁盤加載鏈時需要檢查與否包含壞區塊;BlockChain對內提供Block的管理,如初始化時loadLastState(…)從磁盤加載區塊鏈并檢查鏈的對的性,調用Validator().ValidateState(..)、Validator().ValidateBody(…)檢查塊中的狀態和區塊體數據,調用InsertChain(…)插入新的區塊等,這些都會在背面的功效流程里分析到。啟動流程go-ethereum編譯出來的官方客戶端程序geth,提供了龐大的子命令和命令行參數,分別控制節點的運行模式、挖礦參數、網絡參數、交易及調試參數等,這些選項由geth解決后修改對應的默認配備項,控制geth節點的行為。我們先從geth的啟動流程開始分析,理解節點運行所需的核心組件及互有關系。geth使用urfave/cli庫封裝了命令行參數解析過程,抽象出Flags/Commands這些模塊,顧客只需要提供某些模塊的配備即可。導入包后來調用cli.NewApp()創立一種實例,然后調用Run()辦法執行app.Action入口函數。geth的啟動入口在main包cmd/geth/main.go中,默認首先執行包體的init()函數,為app指定Action(geth)、Commands、Flags、Copyright等信息,然后在main()中調用app.Run()正式啟動,進入geth(…)函數:geth(…)函數所做的事看起來很簡樸,就是先創立一種Node,然后啟動節點運行,直到Node退出,程序結束。我們先看makeFullNode(ctx)是如何創立一種節點的。(1).調用makeConfigNode(ctx)根據配備創立一種Node(命名為stack)a.先創立默認配備,分成四部分:Eth(客戶端有關)、Shh(Whisper有關)、Node(節點有關)、Dashboard(dashboard有關);b.如果命令行里指定了配備文獻,則從配備文獻加載覆蓋對應的配備項;c.將node有關的命令行選項配備應用生效,涉及P2P網絡配備、IPC/HTTP/WebSocket的有關配備,及數據目錄途徑、賬戶密鑰目錄等;d.解決完node配備后便能夠調用node.New(&cfg.Node)創立一種Node類對象,該對象還包含一種AccountManager,用于管理本地賬戶和密鑰;e.將Eth有關的命令行選項配備生效,涉及coinbase、同時模式、gcMode、挖礦協程數、交易池有關的配備等;f.將Whisper有關的命令行選項配備生效;g.將Dashboard有關的命令行選項配備生效;至此,Eth、Dashboard、Whisper有關的配備都已經準備好了。(2).向Node注冊Eth服務(即創立客戶端類,重點核心)(3).向Node注冊Dashboard服務(如果指定了—dashboard選項)(4).向Node注冊Whisper服務(如果指定了—shh選項)所謂注冊,就是把注冊的服務的啟動函數添加到Node的服務啟動函數數組中,在背面的startNode(..)會取出并調用。現在我們有了配備好的Node節點,和Eth、Dashboard、Whisper服務有關的配備,現在調用startNode(…)在節點上啟動對應的服務。startNode的重要調用在utils.StartNode(stack)中,stack是上面創立的Node節點,其它的幾個部分代碼是賬戶密碼解鎖、設立錢包打開關閉事件監聽及啟動挖礦(如果指定了—mine)。我們直接進到stack.Start()函數里去瞅瞅它的啟動過程(node/node.go)。(1).調用OpenDataDir()在數據目錄下創立一種LOCK文獻,避免數據被多個程序訪問造成數據不一致;(2).配備n.serverConfig,它是P2P網絡的配備,涉及:節點私鑰,用于與網絡中其它P2P節點握手交換密鑰并生成公共密鑰。從數據目錄的nodekey文獻里讀取,并轉換成一種橢圓加密算法的私鑰。如果沒有則生成新密鑰并寫到該文獻。節點名字,如

Logger靜態節點:p2p節點啟動后會和靜態節點建立連接可信任節點:即使超出最大允許連接數,可信任節點仍允許繼續連接(3).使用n.serverConfig配備創立一種p2p.Server,從而便該節點成為p2p網絡的一員,能夠監聽新連接,與網絡中別的節點握手建立新連接,及通信交換數據。這里不討論細節,下一節會進一步P2P網絡進行分析(4).還記得之前在node上注冊的那3個服務嗎,現在是時候關照一下它們了(在我的測試里并未啟用dashboard,而whisper也是用于DApp的分布式通信,臨時不進一步。重要以ETH服務為根本,由于它是節點功效實現的核心)。我們先回到過去,看看ETH服務的啟動函數:根據同時模式的不同,如果是LightSync則服務函數是創立一種輕節點客戶端les.New(…),否則創立一種全節點客戶端Eth.New(…)。回到node.Start(),之前被注冊到node的服務函數被取出來并執行,對于ETH服務則返回一種eth.Ethereum類指針(假設未指定輕同時),然后寄存到map[類型]=>對應實例的services中(5).P2P節點在握手的時候還需要其上運行的合同信息,因此要把services中每個service對應的Protocol信息加到p2p.Server中(Protocol信息在實例創立的時候已經創立了)(6).現在能夠正式開搞了,首先啟動p2p節點服務(running.Start())(7).依次啟動services里的服務,如果有一種服務啟動失敗則全部停止并返回錯誤(servcie.Start(running))。對于全節點這里調用的是eth.Ethereum.Start(running)啟動了客戶端(8).最后啟動RPC有關服務,IPC/HTTP/WebSocket作為核心的eth.Ethereum,它的Start()正式宣布了節點的啟動完畢。在探究Start()的過程前,有必要先分析一下eth.Ethereum類和它的創立過程,先看它在代碼中的定義(eth/backend.go:62):eth.Ethereum類其實現以太坊全節點功效模塊的容器,它內含管理動態變化交易的交易池TxPool,完畢ETH合同交互的ProtocolManager,有執行挖礦的Mine,有維護區塊鏈數據的BlockChain,而它需要為這些功效模塊提供對的運行所需的ETH有關配備、鏈配備、數據庫接口、共識引擎、賬戶管理、gasPrice等。它的創立過程就是創立上述核心模塊并初始化的過程:(1).打開數據目錄下的$DATADIR/chaindata數據庫(2).從數據庫里讀出chain配備和創世塊的哈希值(3).創立一種新的Ethereum構造,除了從入參config和ctx取出需要的變量config、accountManager、gasPrice等進行傳遞賦值,還創立了共識引擎、BloomIndexer(4).core.NewBlockChain(…)從數據庫“chaindata”中加載創世塊、已知的最新塊和對應的TD(TotalDifficulty,用來擬定最重的權威主鏈)(5).啟動bloomIndexer服務(6).對于本地交易如果指定了交易池的Journal選項,則會將本地交易持久化到數據庫(7).創立交易池TxPool,交易池中寄存本地交易和從網絡接受到的交易(8).創立ProtocolManager(9).創立礦工Miner(10).創立API解決服務(11).返回創立的Ethereum指針Now,eth.Ethereum.Start(..): (1).啟動bloom位數據獲取的協程提供服務(2).提供RPC接口中的network有關命令的解決函數(3).啟動protocolManager(4).如果提供LES請求支持的話啟動lesServer為什么這里只啟動了protocolManager?TxPool和Miner呢?其實TxPool在創立后就已經開始了無休止的loop循環解決過程,至于Miner,還記得Node啟動時調用的StartNode(…)函數嗎,該函數最后是這樣說的:如果命令行指定了啟動挖礦,或者是開發模式,就啟動挖礦.如果沒有指定的話,尚有一種方式啟動挖礦,就是在終端console里輸入命令:web3.miner.start().好了,現在我們說回ProtocolManager的啟動,也是啟動流程的最后任務.它一共啟動了4個go協程:(1).向TxPool訂閱新交易事件,然后啟動BroadcastLoop(),一旦有新交易,它就會把該交易告知給不包含該交易的節點;(2).訂閱挖出新區塊的事件,然后啟動minedBroadcastLoop(),一旦有新區塊,它就會把該區塊發送給網絡中的其它節點,告訴它們本節點有該區塊(3).啟動syncer()同時區塊(4).啟動txsyncLoop(),向新連接的節點發送本交易池中Pending的交易ProtocolManager合同有關的部分留到下一節P2P網絡中一起分析.到現在為止我們大致梳理了一下geth主啟動流程,忽視了某些不太緊要的東西,如沒考慮輕客戶端(類似全客戶端),沒展開whisper和dashboard,也沒有提Bloom位有什么功效,由于現在只考慮啟動過程。現在我們啟動了P2P網絡服務能夠和網絡中其它節點通信交換信息,有了一種運行中的eth.Ethereum全節點客戶端能夠接受管理交易,挖礦賺取收益,同時區塊鏈和維護數據庫,還提供了對外訪問接口的RPC服務。如果沒有底層的P2P網絡,全節點只是一種毫無憤怒的死循環程序,沒有數據流動,沒有交易交換,沒有塊溝通,區塊鏈不可篡改的分布式賬本定位也是空中樓閣.因此緊接著在下一節我們就先分析區塊鏈整以生存的P2P網絡實現.P2P網絡和節點go-ethereum代碼中P2P的實現在p2p/目錄下。回想node.Start()啟動和p2p有關的部分:創立了一種p2p.Server,只初始化了它的配備,然后把其它服務所支持的合同收藏起來,然后啟動。p2p.Server構造固然不止這樣簡樸,先看其定義(p2p/server.go:147):newTransport:一種生成一種transport接口的函數,transport提供傳輸層的握手功效和消息讀取;ntab:kademlia算法的節點發現實現(重要);listener:監聽接口;ourHandshake:在peer握手時發送的數據,由于慣用并且不變因此寄存起來;lastLookup:用于控制去網絡中尋找新節點的頻率;DiscV5:輕節點LES使用的節點發現合同(未研究);其它組員基本都是內部使用的channel,用于支持添加/刪除靜態節點、握手告知等;這里先果斷圈個重點,peer.Server中有個很重要的組員ntab,以太坊使用Kademlia分布式路由存儲合同來進行網絡拓撲維護,其算法實現在ntabdiscoverTable中,discoverTable是一種提供Resolve(..)、Lookup(…)、ReadRandomNodes(…)功效的接口。先簡樸介紹一下這種常見又巧妙Kademlia算法:Kademlia是一種分布式散列表(DHT)技術,以異或運算為距離(而不是物理距離)度量基礎。異或有一種重要的性質:假設a、b、c為任意三個數,如果aXorb=aXorc成立,那就一定有b=c。因此,如果給定一種結點a和距離L,那就有且僅有一種結點b,會使得D(a,b)=L。通過這種方式,就能有效度量Kademlia網絡中不同節點之間的邏輯距離。Kademlia使用了名為K-桶的概念來儲存其它(臨近)節點的狀態信息,這里的狀態信息重要指的就是節點ID,IP,和端口。對于160bit的節點ID,就有160個K-桶,對于每一種K-桶i,它會儲存與自己距離在區間[2^i,2^(i+1))范疇內的節點的信息,每個K-桶中儲存有k個其它節點的信息,每個節點根據與鄰居節點距離之間的距離(NodeID的差距),分別放到不同的桶(bucket)中。下表反映了每個K-桶所儲存的信息K-桶儲存的距離區間儲存的距離范疇儲存比率0[20,21)1100%1[21,22)2-3100%2[22,23)4-7100%3[23,24)8-15100%4[24,25)16-3175%5[25,26)32-6357%10[210,211)1024-204713%i[2i,2i+1)/0.75i-3每個節點都更傾向于儲存與自己距離近的節點的信息,形成

儲存的離自己近的節點多,儲存離自己遠的節點少

的局面。從上表能夠看出,在1-15這個范疇內的節點,只要發現,就會被100%地儲存下來,而離自己距離在1000左右的節點,只會儲存13%.Kademila使用UDP進行節點間消息通信,它定義了4種消息:*ping-用于探測其它節點與否還存在*store-接受者受到后,將信息中key/value對存儲在本節點*findnode-接受者向發送者返回k個它懂得的與目的結點距離近來的節點*findvalue-和findnode差不多,區別是如果接受者本地存在與目的結點對應的value,那么就回復這個值給發送者。回到以太坊的Kademila實現,重要圍繞3個文獻(p2p/dial.go、p2p/discover/udp.go、p2p/discover/table.go):dial.go:要與節點建立連接,首先得懂得這個節點,查找、解析節點的發起者table.go:算法實現核心,查找結點、節點存活管理、使用距離管理k-桶即bucketsudp.go:底層UDP包的發送者,你告訴我要發什么包,我就發什么包在udp.go中定義了四種類型的數據包:pingPacket:用來查看節點與否存活;pongPacket:對PingPacket消息的響應;findnodePacket:節點查詢包,向目的節點詢問其臨近的節點列表NeighborsPacket:響應findnodePacket,包中攜帶了此節點的附近節點K桶在table.go#Table構造中:Table.buckets寄存了nBuckets(len(common.Hash{})*8/15=17)個桶,每個桶放16(bucketSize)個節點,寄存的是Node指針類型。這個Node不是前面啟動時的Node,正如前面介紹Kademlia算法提到的狀態信息,此Node(p2p/discover/node.go)包含IP、UDP/TCP端口、ID、sha(ID的hash值)。和本節點距離不大于239(bucketMinDistance)的節點都放到第0個桶,否則放到第(d-239-1)個桶:這里用于計算距離的指標不是nodeId,而是“sha”,根據nodeId計算Keccak256Hash哈希值。邏輯距離的計算在logdist(…)函數中完畢:先看lzcount數組,它表達一種8bit字節取值從0~255,出現的第一種1前面有多少個0。如B00100000=D32,lzcount[32]=2。結合logdist(…)來看,將a、b按字節由低到高異或(common.Hash是[32]byte),當找到第一種成果非0的byte,則lz累加異或成果的前置0的個數然后break,否則累加8。最后返回256-lz作為距離值。實現上是獲取異或成果的二進制表達時的最高位1所在的位置(Really?)下面K-桶的維護工作理解一下。Table通過loop()中的for循環定時或外部調用Refresh和Revalidate操作,進行路由表的刷新和檢查存活。Refresh:尋找新節點的過程有兩種方式,一是以本身的ID為目的尋找我附近的節點,二是隨機找3個節點。注意調用定時doRefresh前會更新隨機種子,tab.seedRand(),盡量確保隨機的隨機性。兩種方式都是調用tab.lookup(…)進行實際查找:(1).計算targetID的hash值,前面提到節點距離的計算是基于哈希的。result用于存儲需要查詢的節點(2).從本地桶中找16個近來的點,放到result中。由于桶是按距離排序的,因此只需要依次掃描buckets(3).從result中依次取出要查詢的節點,同時啟動3(alpha)個findnode協程向節點發出查詢請求。由于查詢得到的成果會重新放入result,因此查詢過的節點會被放入asked,避免重復查詢。同理重復的節點也會被seen[]過濾掉。Table.findnode再進一步調用udp.findnode向指定節點發送findnodePacket,查詢結束后Table.findnode把返回成果r中的node一一添加到對應的桶中。如果node在桶中已存在,則把它移到頭部;不存在則添加到頭部。那么如果超出單桶大小的話會被放到Table.replacements中作為候補。Revalidate:(1).隨機挑選一種桶的最后一種節點,發pingPacket探測存活(2).如果有返回響應,err為nil,則通過b.bump(…)把節點移到桶的頭部(3).如果無響應,則從Table.replacements中隨機找一種節點替代它,或者直接刪除。除了Table本身的定時刷新維護,它還提供了其它功效的接口供上層調用,重要有:ReadRandomNode:取出K桶中的全部Node隨機選用出指定的NodesResolve(targetIDNodeID):查找指定的ID的節點Lookup(targetIDNodeID):查找指定的ID節點附近的節點到這里就出現個問題,就雞生蛋還是蛋生雞之,啟動的時候本節點是一種節點都不懂的,那我該去問誰找其它節點?其實在params/bootnodes.go就已經硬編碼了不同的網絡對應的節點,它們會在創立Table時就加到K-桶buckets里。例如主網:如果是私鏈的話,能夠手動添加節點。這里就不深究udp.go中的具體代碼,它所完畢的功效基本圍繞Ping/Pong、Findnode/Neighbors這4種請求,定義請求格式,然后完畢發包,收包,解包這些動作。介紹了這樣久的底層P2P之底層節點發現,我已經等不及要回到p2p.Server.Start()把哦下收編到主流程里舒暢地串起來了。記得p2p.Server還是一種只賦值了Config和Protocols的早期構造體,Start()函數首先就把構造體里該有的東西都創立好:組員初始化:(1).Log賦值、置running標志避免重入(2).newTransport是一種生成P2P通訊的傳輸層握手和數據傳輸解決對象的函數,這里賦值為newRLPX,在后續調用中會生成rlpx對象(3).創立Dialer用于dialer.go#dialer建立連接,Dialer其實是封裝了net.Dialer的一種TCPDialer(4).創立quit/addpeer/…等通道用于后續的內部數據傳遞和告知(5).啟動UDP監聽,解決節點發現請求(net.ListenUDP)(6).創立節點發現功效模塊Table,即srv.ntab(7).先懂得最多允許的動態節點數目,默認是MaxPeer/3。相對動態節點,即通過節點發現添加的節點,尚有初始化時指定的靜態節點,手動添加的inbound節點,和信任節點,分別對應有dynDialedConn、staticDialedConn、inboundConn和trustedConn標志。然后創立一種任務調度器,一共調度3種任務,dialTask(連接指定節點)、discoverTask(隨機發現新節點)、waitExpireTask(無節點可連時睡眠)(8).初始化ourHandsShake,包含版本、所支持的合同、節點ID(由節點公鑰生成)等信息,用于握手階段發給對節點。由于該值是靜態不變的,因此能夠預初始化好,背面只要取用便行peer.Server的運行重要涉及到兩個協程,一種用于listen新連接,一種用于peer節點的握手、添加和刪除等功效。兩個協程互相協作動態維護本節點的peer連接。它們是在peer.Service.Start()最后階段啟動執行的:Listen協程:在peer.Server.listenLoop()中對處在握手階段的最大節點數(max)做了控制,辦法是創立一種最多緩存max個請求的可阻塞channel,然后預先填滿,相稱于分派了max個指標。然后每次for循環取一種指標,等待Accept()一種新連接,在握手完畢后把指標釋放回去。如果指標都被占用了則請求被阻塞。以下圖中源碼所示:現在我們就以這為起點,假設已經收到一種新的連接請求,Accept()成功。我們從#801行的srv.SetupConn(…)開始,看看接下來會發生什么。由上圖能夠看到新連接的解決過程是由listen協程和run協程協作完畢的。SetupConn(…)為新連接fd創立了新的conn,其組員transport即為peer.Server.Start()中賦值的srv.newTransport,即為rlpx對象然后進入srv.setupConn(…)子函數開始握手,這里進行了兩次握手:首先調用c.doEncHandshake(..)與peer交換密鑰,并生成臨時共享密鑰用于本次通信加密,還創立了一種幀解決器RLPXFrameRW然后調用srv.checkpoint(…),它所做的事就是把連接c發給第二個參數指定的通信,然后在c.cont通道上等待解決成果。這里發給srv.posthandshake,監聽這個通道事務的正是run協程。在run中,判斷c與否在可信任節點列表中,在的話為其正身加上trustedConn,在之后的srv.encHandshakeChecks(..)的檢查中就不受最大允許peer數的條件限制了,最后將檢查成果(涉及peer數限制、與否已有連接等)返回給srv.checkpoint。Check無問題,則再進行協商握手,發送srv.ourHandshake信息給對方,接受并解包得到對方的合同。這里通訊用的是RLPXFrameRW幀再次調用srv.checkpoint(…),這次是發送給srv.addpeer通道。在run端,先檢查和對方與否有共同支持的合同,沒有的話就要saybye。同樣再做一下peer限制數之類的檢查。都通過的話就能夠創立一種新的peer對象(注意這里攜帶的第二個參數srv.Protocols,在前面啟動的時候我們懂得這個Protocols是全節點eth.Ethereum的ProtocolManager給它的,這樣就讓peer能夠懂得要解決哪些上層合同了),并調用srv.runPeer(p)讓它跑起來了。最后把這個新peer加到run內部維護的peers列表里基本上listen協程所做的就是這些事情,接受連接,完畢兩次握手生成通訊的共享密鑰,最后得到一種新的peer并運行起來。要讓peer跑起來,其實也是一件不簡樸的事。runPeer(p*Peer)正常正常狀況p會始終運行,直到連接關閉或異常告知有關人員有節點被移除告知有關人員有新節點SendPeerEventTypeDropSendPeerEventTypeAddp.run()gop.readLoop(readErr)gop.pingLoop()p.startProtocols(writeStart,writeErr)定時發送ping包的協程1解決ping請求,返回2解決連接斷開請求,返回3否則按MsgCode分發給對應的合同數據包傳給對應的合同通過API能夠在外部向peer.Server注冊有關peer事件的監聽。這里先告知訂閱者添加了新節點調用p.run()啟動協程從peer連接conn中讀取數據并解出msg消息,如果msg.Code表明是ping包則返回pong包,如果是斷開連接消息則再從rlp格式的包中解碼出斷開因素,而都不是的話就根據msg.Code找到對應的合同分發出去啟動另一種協程每隔15s向peer發送一種ping包保持活躍對于peer支持的每一種合同都啟動一種go協程運行(proto.Run(p,rw))如果p.run()返回的話闡明節點已經停止運行且失效,同樣告知訂閱者節點移除事件上圖有兩個坑沒填,一是在p.readLoop(…)中是怎么找到對應的合同去分發的?二是p.startProtocols(…)是怎么執行自己的合同的?我們一一來填。合同分發問題在p2p.Server的run協程從<-addpeer通道收到新連接創立newPeer(conn*conn,protocols[]Protocol)的時候,會從conn中取出對端節點所支持的合同,和本節點所支持的合同做交叉比較,共同支持的合同會放到一種合同映射里,以合同名為鍵,以一種protoRW{…}對象為值:一種Protocol對象所包含的內容有合同名稱、合同版本,尚有一種屬性是合同長度,也就是它全部支持的命令的總個數。例如說合同A支持10種命令,合同B支持5種命令,那么我們把全部命令排個序并編號0~14,從0開始,A的offset是0,0~9是合同A的命令,B的offset是0+10=10,10~14是合同B的命令,如果收到一種msg的code是11,那么它對應合同B,在傳給B合同前會先11-offset=11-10=1,再給B,確保給B的Code是按B合同定義的命令值。由于合同是雙方都支持的,并且按合同名和版本號做了排序,因此能夠確保通信雙方使用的合同和offset的一致性。現在就不難理解尋找匹配合同的函數getProto(…)的實現了:找到合同后再發給對應的合同解決協程:這里的proto.in,其實關聯到p.startProtocol(…)的合同解決流程,為其提供數據包:當合同調用ReadMsg()從peer中取數據時,就是從in通道取數據,由readLoop()把數據包分發到in。正如前面所說,msg.Code-=offset更新為合同對應的Code。p.startProtocols(…)執行流程循環解決每一種合同proto即上面創立的protoRW,轉換成MsgReadWriter接口rw,為合同解決提供數據來源,提供應proto的Run函數啟動新協程運行proto以ETH合同為例,回想一下,在程序啟動的時候,我們創立了eth.Ethereum,它的一種很重要的組員,ProtocolManager,也隨之創立好了,同時也定義好了它所支持運行的合同:找到里面定義的Run函數,首先通過newPeer包裝了一下p2p.peer,變成了eth.peer,加入某些區塊和交易有關的信息;然后--->將新peer發到manager.newPeerCh(在eth/sync.go中進行同時,后續再提)--->進入manager.handle(peer)正式開始ETH合同的解決獲取獲取本節點區塊鏈的創世塊genesis、現在區塊的hash、number、td發送一種狀態包【protocolVersion,networkID,td,hash,genesis哈希】用于握手,并等待返回得到它的TD和head哈希把peer添加到ProtocolManager管理的peer集里,并啟動一種新協程,向peer告知新交易、新區塊、新區塊聲明把peer注冊到ProtocolManager的下載器Downloader,以成為同時候選者把本節點交易池中未打包的正當交易發送給peer循環調用pm.handleMsg(p)解決peer發過來的請求從上述流程里能夠提取幾個核心點,一是創世塊、TD(totaldifficulty)、header,二是downloader,三是handleMsg。這涉及到一種對的的p2p網絡怎么做對的的事。首先創世塊相似才干確保我們是在同一條鏈上對話,另首先我們需要信任和互相溝通,如果你有新的交易和新的區塊告訴我,同樣我也會告訴你,同時我會響應你的塊請求、頭部請求等合同范疇內的多種請求,這樣才干達成共同維護一本權威賬本的目的。因此再回頭看上面的流程,先通過握手交換創世塊和所知最新塊的信息,然后把新peer加到列表里同時啟動協程,只要有新的交易和區塊便發送給它。如果你的區塊鏈更新,則通過Downloader進行同時。最后循環解決peer的請求和返回。在eth/protocol.go中定義了ETH合同支持的全部命令://Protocolmessagesbelongingtoeth/62舊版本StatusMsg=0x00ETH握手時發送的狀態消息NewBlockHashesMsg=0x01peer聲明它有新的區塊Hash值TxMsg=0x02新的交易GetBlockHeadersMsg=0x03獲取區塊頭部消息BlockHeadersMsg=0x04區塊頭部響應消息GetBlockBodiesMsg=0x05獲取區塊體消息BlockBodiesMsg=0x06區塊體響應消息NewBlockMsg=0x07peer發送新的區塊消息//Protocolmessagesbelongingtoeth/63新版本,兼容舊版本命令GetNodeDataMsg=0x0d獲取trienode消息NodeDataMsg=0x0e響應trienode消息GetReceiptsMsg=0x0f獲取收據消息ReceiptsMsg=0x10響應收據消息這些消息都會在handleMsg(…)中被一一解決。流程走到現在,我們已經啟動了全節點和p2p網絡,能夠接受外部節點的連接請求并握手建立連接,解決ETH合同通訊。但是,如果沒有節點主動發起連接請求,也就是Listen協程未收到請求呢?要加入到p2p網絡中我們就需要主動連接節點,除了預設的啟動節點,我們還要主動發現新節點加入進來,連接靜態節點,這就是run協程的任務。終于輪到run協程正式出場,它的功效就相對復雜某些(如果撇開在listen協程里生成密鑰的復雜算法過程的話)。我們從頭看起。func(srv*Server)run(dialstatedialer)run的函數簽名中唯一參數是p2p.dialer類型的dialstate,dialer是一種接口類型,實現是p2p.dialstate,在p2p.Server.Start()中初始化。能夠說run的功效基本上是圍繞這個dialerstate進行的。那么,dialerstate長啥樣?官方解釋說,dialstate用于調度撥號和節點發現任務,在p2p.Server.run的每次主循環里被調用來生成新任務。看它的構造:maxDynDials:最大允許的動態節點數,在創立時指定,maxPeer/3ntab:實現Kademlia算法discover.Table對象netrestrict:IP黑名單,對于該名單中的IP不進行連接lookupRunning:確保任何時刻只有一種節點發現任務在執行dialing:保存撥號中的IP,避免二次撥號,連接成功后會從dialing里去除lookupBuf:保存發現的新節點randomNodes:在最大動態節點數沒填滿時,從ntab中隨機讀取節點來填充static:靜態節點列表,必然會連接的節點hist:節點連接成功后來會加到歷史列表中,30s后過期刪除bootnodes:當沒有可用的peer時,使用bootnodes中的節點它包含maxDynDials控制動態peer數,ntab用于在沒有節點可用時去發現節點,和節點未知時發起查詢,有static和bootnodes收納靜態節點和備用節點。上面提到run會調用dialstate生成新任務,調用的就是newTasks(…)[]task函數,它會根據需要決定是不是要生成新任務、生成哪些任務。它共分派三種任務:task是接口,dialTask、discoverTask和waitExpireTask都實現了task的Do(*Server)函數。dialTask:和指定節點建立連接的任務discoverTask:存量節點局限性時執行的節點發現任務waitExpireTask:超時任務newTasks(…)重要關注三點,一是靜態節點,二是最大動態節點數,三是節點要符合連接條件。全部靜態節點都要默認連接;另首先,它會盡量多地去連接動態節點,這是函數的重要工作;最后已連接或是連接中的、IP黑名單的、剛連過并出現在歷史統計里的節點都不嘗試連接。展開分析一下函數是怎么竭力連接多的動態節點的:需要動態連接的總數=配備的最大動態節點數–已連接動態節點數–正在連接動態節點數needDynDials=s.maxDynDials–peers.rw.is(dynDialedConn)–s.dialing.flag&dynDialedConn如果現在一種peer都沒有且配備了bootnodes,就取出一種來連接,needDynDials減1剩余的needDynDials/2從ntab(即discover.Table)中隨機取s.maxDynDials/2個節點來連接,竭力填充如果還不夠則再從s.lookupBuf(存儲了節點發現的新節點)取節點來連接,竭力填充如果還沒填滿,闡明節點發現給的節點還不夠,就分派discoverTask任務去發現更多新節點如果我們這樣努力了,卻連一種可連接的涉及靜態的節點都沒有,但是有歷史連接紀錄,那就分派一種waitExpireTasks任務等最老的節點在歷史紀錄里過期,再進行下一輪嘗試。這里的每個連接都會分派一種dialTask任務。當沒有足夠的新節點時分派discoverTask任務調用ntab隨機尋找新節點。而當p.Server.run中既沒有舊任務,現在也沒有節點可連接時,就分派一種waitExpireTasks任務等待節點歷史紀錄過期再嘗試,由于在歷史紀錄中的節點也不會去嘗試連接。說完newTasks函數,再回到p.Server.run協程看它的主循環,就明朗多了:主循環每次開始先調用scheduleTasks()執行和分派新任務從queuedTasks中取出第一種任務并執行,任務啟動后放入runningTask隊列中,并在任務結束后告知taskdone通道以解決任務成果未超出運行中任務總數限制的話,調用dialstate.newTasks分派新任務放到queuedTasks隊列接受來自API調用通道的請求解決添加靜態節點、刪除靜態節點、獲取peers信息請求任務完畢后回調dialstate.taskDone,對于dialTask會把節點加到歷史紀錄,discoverTask會把新節點加到lookupBuf中,waitExpireTasks則什么也不做posthandshake、addpeer通道用于和Listen協程一起完畢新節點的握手和創立delpeer用于接受peer停止運行后發出的停止信息通過run協程的for循環,本節點得以在P2P網絡中維持動態變化的關聯網。構想有一種動態連接的節點下線了,連接斷開會告知到srv.delpeer通道,該節點會從維護的peers列表中刪除,這樣再調用dialstate.newTasks的時候會多出一種動態節點名額,然后通過存量或發現新節點去連接一種新的節點。最后一種問題,dialTask是如何把節點加入到系統中的?答案就在p2p.dialTask.dial(….)函數中:如代碼所言,在調用srv.Dialer.Dial(..)初始化TCP連接后,調用到srv.SetupConn(…),剩余的流程就和Listen協程監聽新連接的后續解決同樣,handshake->posthandshake->addpeer…..花了很大的篇幅,我們從p2p.Service的Start()函數入手,講究竟層用于節點發現和查詢的discover.Table和它使用的Kademlia算法及輔助的通訊功效discover.udp,再進到p2p.Server運行的兩大主協程listen和run,分別負責監聽新節點的連接和通過動態連接新節點,run又是怎么使用dialstate任務分派功效維護網絡。另外我們還提到節點在連接時有關檢查和密鑰握手、合同握手,生成RLPXFrame,在基礎連接完畢后進一步完畢上層ETH合同的握手和重要數據獲取,再之后數據流向有關合同進行解決。一種可運行可溝通可動態維護的P2P網絡已經搭建好了,現在我們需要在網絡上面加入數據讓它變得真正故意義。根據不同合同ETH或LES(輕以太坊子合同,lightethereumsub-protocol)的不同網絡上流通的包會略有不同,但是都是和交易和區塊有關的。交易是以太坊的基本元素,區塊是交易的集合,而區塊需要由礦工通過挖礦才干被添加到賬本里,礦工需要從內存中臨時存儲交易的交易池中取出交易打包,并且交易池對交易的管理和驗證也是很重要的功效。因此下一節我們就從交易池出發,關聯到挖礦功效,繼續分析以太坊源碼。交易池和挖礦交易、交易池和挖礦之間的關系大致以下圖所示:挖礦挖礦模塊Peer傳過來的交易本地發起的交易交易池交易池在啟動流程里,我們創立了eth.Ethereum,提到過它有一種很重要的組員就是交易池TxPool。交易池涉及了現在已知的交易,它收集從網絡中發過來的(遠程交易)、本地提交的交易,進行驗證、過濾和排序,對遠程交易和本地交易使用的檢查條件也是有差別的。然后根據檢查成果放到內部不同的隊列中,供挖礦模塊使用。挖礦模塊也能夠通過在交易池中定義特定的交易過濾條件,只打包它想要的交易。我們還是從交易池的定義開始(core/tx_pool.go#184):config:交易池的有關配備,重要是與否要持久化本地發起的交易、交易gasPrice的下限、每個賬戶允許寄存的交易個數和總的寄存個數以控制池中的交易總量、遠程交易的過期時間等;chainconfig:鏈配備信息chain:鏈接口,用于獲取鏈有關信息gasPrice:允許進入交易池的最小gasPrice,初始化為config.PriceLimit,可通過API更改txFeed/scope:用于告知外部訂閱者有新交易的訂閱告知工具chainHeadCh/chainHeadSub:向BlockChain注冊監聽新區塊頭事件signer:簽名工具,重要用于從交易中解出發送者的地址currentState:現在的賬戶狀態,涉及賬戶余額和代表該賬戶已交易次數的NoncependingState:在currentState.Nonce基礎上為每筆可執行交易遞增分派一種虛擬的Nonce,稱它為虛擬是由于Nonce并不更新到賬戶狀態里,只用于臨時計算currentMaxGas:每筆交易的gas限制locals:存儲每個本地賬戶發起的交易journals:用于本地交易持久化到磁盤pending:可執行交易,礦工從這里取交易打包,每個發送者有自己的交易列表,方便管理queue:已接受但臨時不可執行的交易,同樣每個發送者有自己的交易列表beats:遠程賬戶的近來連接時間all:按[key:交易Hash=>value:交易]的map方式存儲全部交易priced:包含all中全部交易,但是這是按gasPrice由低到高排序,txPricedList內部使用小根堆存儲homestead:與否為Homestead版本,homestead版本創立合約需要53000單位的gas,否則只要21000eth.Ethereum調用NewTxPool(configTxPoolConfig,chainconfig*params.ChainConfig,chainblockChain)創立一種新的*TxPool,根據入參把內部的組員都初始化好,向BlockChain訂閱ChainHeadEvent事件。如果節點可接受本地交易,且啟動了journal選項,則從本地加載保存著的未打包的交易到池里。最后啟動新協程執行loop(),解決下列事件:監聽解決pool.chainHeadCh(由BlockChain告知的新區塊頭事件,交易池會刪掉新區塊中的交易,由于它們已經被打包執行過,不需要再打包執行)每過一分鐘就去除池中過期的不可執行的遠程交易,過期時間默認為3小時,可配每隔一小時(默認,可配)就把本地交易刷新到journal文獻每8秒通過日志輸出交易池的狀態。我們先從一種簡樸的流程,新交易怎么進到交易池,分析交易池的功效要點。交易池包含兩種交易,一是通過網絡中其它peer給的,另一種是通過web3.eth.SendTransaction或者JONS-RPC調用eth_sendTransaction發起一種本地交易。在上一節P2P網絡的基礎上,我們先分析第一種方式,其它Peer發送過來的方式。上一節最后分析到ETH合同,在握手和初始的同時之后調用handleMsg(p*peer)解決請求,還提到有一種請求叫“TxMsg”,這就是peer發過來的包含新交易的數據包。我們進到handleMsg的這個解決分支去看看:從peer中讀出新消息判斷是新交易告知類型,Decode出交易集合把全部交易加到peer的已知交易列表中,這樣本節點再Relay該交易的時候就不會再發回給它了調用txpool.AddRemotes(txs)把全部交易加入交易池AddRemotes(txs)AddRemotes(txs)addTxs(txs,false)AddTxsLocked(txs,local)add(tx,local)txs中每個tx依次調用promoteExecutables(addr)通過層層調用先來到AddTxsLocked。它最后分別循環調用add(tx,local),然后調用promoteExecutables(addr)。我們剛介紹過TxPool的構造,它包含pending寄存可執行的交易、queue寄存臨時不可執行的交易。而只有pending中的交易才會被拿來打包出塊。首先由add(tx,local)函數將交易放到queue隊列,然后由promoteExecutables把queue中的交易放到pending中。注意到local標志tx與否為本地交易,從addTxs傳遞下來為false,表明它是遠程交易。在add的交易檢查過程中會對本地tx體現出區別看待。當add完畢5、6步的時候交易就被成功添加到交易池里了,在這之前要針對交易的gas、gasPrice、Nonce全方面檢查。這里提一下第5步中的pool.queue,它是一種map[common.Address]*txList構造,意味著每個交易發送者都有自己獨自的交易列表,txList內部由txSortedMap來管理Nonce和關聯交易,體現它的兩個組員上:map[uint64]*types.Transaction,映射了Nonce和交易的關聯性index*nonceHeap,根據Nonce排序的小根堆這樣可方便地找出Nonce低于某值的交易,方便過濾。現在再分析promoteExecutables函數,記住它要做的是把能夠執行的交易從pool.queue移到pool.pending中:在權衡維護交易池中pending的交易總數在可控范疇內,和竭力保全pending交易之間,交易池可謂是做了很精細的考量。對pending中交易總數的控制有兩級,一級全局的總交易數控制,另一級是單個賬戶的交易總數控制。在不超出全局交易總數時單賬戶的交易數不受限,超出的話就會以賬戶級限制為閾值進行交易的方略性丟棄。丟棄也是分級的,首先丟棄擁有最多交易的地址的高Nonce交易,向次多交易的數量靠攏;然后把賬戶的交易數控制在單賬戶限制下列;最后按地址的交易時間,批量刪除老地址的交易,直到pending中的總交易數低于全局總交易數限制。本地發起的交易在交易池中的解決流程差不多,只但是是調用入口是AddLocal(…),置local標志位,在清理交易階段就能夠在安全區穩穩地呆著。交易池準備好了,就能夠開始挖礦了。挖礦在考慮挖礦前,我們會想懂得它是怎么和程序啟動流程、交易池串通起來的呢。從啟動階段main.startNode(…)函數的最后,在P2P、Ethereum、RPC等服務都啟動成功后,會判斷命令行標志,如打開了挖礦功效或處在開發模式,則啟動挖礦(正常主網都是通過終端交互啟動挖礦,在我布署的私網是通過啟動時打開挖礦開關啟動挖礦的,且使用的CPU挖礦,因此走的是下列流程):mmain.startNode()Ethereum.StartMing(true)goethereum.miner.Start(eb)miner.worker.start()mitNewWork()通過main->Ethereum->miner一路進入到miner模塊。挖礦工作由Miner、worker和Agent共同完畢。Miner是上層管理,它接受外部發來的請求,啟動或停止挖礦、設立coinbase,保存變化并傳遞給內部worker。而worker才是負責具體挖礦工作的對象,它的工作核心是一組agents,由Agent完畢最后的出塊工作。另外worker尚有一種Work表達現在挖塊的工作環境,涉及現在待挖的Block、賬戶狀態、交易集合、收據等,但跟Agent只關心和共識有關的Block。Agent接口有兩個實現:CpuAgent和RemoteAgent。在我的測試機上使用的是CpuAgent,只有在礦機上才使用RemoteAgent。在worker和agent都是在創立miner時由miner創立,并把agent注冊到worker中:miner的update()循環始終監聽模塊的同時狀態,如果正在同時則停止挖礦,否則同時正常或異常結束再啟動挖礦。啟動挖礦由miner發起,miner.Start()–>worker.start()->agent.start(),啟動Agent從worker與agent的通信管道中不停取原始塊并出新的正當塊。具體的打包挖礦功效在mitNewWork()函數中,也是由miner.Start()調用,我們重點關注這個函數的有關功效:實際的共識挖礦工作由worker、agent、engine三部分完畢,以太坊正式環境使用的是類似PoW的ethash共識引擎:Worker:從交易池取出并執行可執行交易,然后生成一種包含頭部、叔塊、收據、狀態和交易的原始塊,把它交給agents解決,得到一種共識塊并傳輸到其它peer節點和內部的其它功效模塊中Agent:以CpuAgent為例,在收到workerpush過來的新Work后,它會取出Work.Block交給共識引擎執行Seal函數,并得到一種共識塊返回給worker(如果是remoteAgent的話Seal過程在遠程礦機上執行,礦機會調用API遠程從以太坊上獲得新Work和挖礦有關參數進行挖礦)Ethash:共識引擎,目的是為了找到一對Nonce和MixHash填充到Block.Header中,這一對隨機數滿足這樣一種條件:把Block.Header中的父區塊Hash、叔塊Hash、coinbase、賬戶狀態根等域使用rlp編碼得到一串二進制數據,再生成Keccak256哈希值,寫為hash得到現在塊的難度值Difficulty(在ethash計算前的初始化中已經通過父區塊的時間戳和難度值計算過),2^256/Difficulty得到一種target值把hash和一種隨機nonce值放入hashimotoFull(..)進行計算,得到一種MixDigest和result,result需要小等于b中的target值。這樣我們就有了符合工作量證明的Nonce和mixDigest,填到原始塊的對應字段就成了一種被網絡承認的正當塊了為了找到這一對隨機值,ethash.Seal創立多個協程,每個協程使用一種隨機值作為起始種子,不停通過加1作為新值去嘗試hashimotoFull計算成果與否小等于mitNewWork函數的第6步會盡量執行交易池中的每一筆可執行交易,執行過的交易會被打包進新的區塊。因此區塊中的交易數量受限于塊的GasLimit。它是結合父區塊的gasLimit和它用掉的gas通過CalcGasLimit()計算調節,如果使用的parent.UsedGas>parent.GasLimit*2/3,則增加新塊的GasLimit,否則減小。交易的執行由commitTransaction(…)完畢,它調用core.ApplyTransaction(…)具體執行交易,得到交易的收據,保存起來。core.ApplyTransaction()的實現涉及到以太坊虛擬機EVM,我們通過它的內部執行過程先對EVM預熱,具體執行過程將進入下一節進行專門分析。由上圖可知交易的執行是在轉換成消息類型后進行虛擬機中完畢。由于EVM也接受合約與合約之間的消息調用,因此交易也被轉換成消息統一解決。交易完畢后賬戶狀態發生變化,并且可能在合約中產生事件日志,這些都被統計在收據Receipt里,方便后來查詢。Receipt也有一種Bloom對象,它Block的Bloom一起構成Bloom多級構造,和塊中其它收據的Bloom位一起合并到塊Bloom位中,這樣想要查找某個塊中與否包含某條日志時,只要把查詢目的轉換成Bloom位,再核對塊Bloom的對應位與否置位就能快速地判斷該塊是不是可能包含對應日志。有的話再進一步核對塊中Receipt的Bloom位與否有對應的置位,這樣一步步分級按位查找,大大加緊了日志的查詢。現在,我們立即進入EVM看看大名鼎鼎的以太坊虛擬機是怎么執行消息(交易)的。EthereumVirtualMachine(EVM)以智能合約為標志性更新,以太坊把區塊鏈帶入2.0時代。使用智能合約高級語言Solidity編寫合約代碼,編譯得到元數據后布署到以太坊上,后來只要通過調用合約接口就能夠執行合約功效。每個合約都對應一種合約賬戶,它和普通外部賬戶同樣有以太坊余額,只但是合約賬戶擁有有關聯的代碼,代碼的執行是通過交易或其它合約的call調用來激活。合約的執行與調用通過底層的EVM模塊完畢。上一節最后我們提到交易的執行是先把它轉換成Message,再由EVM執行。我們先一探這兩個構造體。Message定義在core/type/transaction.go#381:結合AsMessage(..)函數,Transaction交易中的信息被填充到Message中,涉及nonce(跟生成合約地址有關),gasLimit、gasPrice(gas局限性交易將失敗),to(轉賬目的地址),amount(轉賬ether數量),data(創立合約時為合約代碼,調用合約時為合約參數),checkNonce(true意味著執行前先要檢查nonce與賬戶的Nonce一致),from(從簽名中提取出發送者地址)。Message精簡地從Tranasction提取出執行需要的信息。再來看EVM構造:Context:上下文執行環境,提供blockchain有關信息**********交易執行有關**********CanTransfer:判斷賬戶余額與否足夠的函數Transfer:實際轉賬函數GetHash:計算哈希值函數Origin:消息發送者GasPrice:交易設立的gasPrice*********鏈有關***********Coinbase:挖礦的收益地址,也就是交易手續費的收益地址GasLimit:區塊的GasLimitBlockNumber:區塊鏈高度Time:區塊頭時間戳Diffuculty:區塊難度值StateDB:賬戶trie樹depth:EVM調用棧深度,不超出1024vmConfig:interpreter有關配備interpreter:合約代碼的解釋器,真正的執行者在EVM中最核心的組員是EVM.interpreter及其配備EVM.vmConfig。vmConfig,即上圖中的Config,其組員JumpTable涉及了EVM指令集,如:合約代碼的執行最后映射到每個指令集的操作,每個指令集涉及執行的調用的函數、計算gas消耗、棧檢查函數、內存使用計算函數及一系列控制停止運行、跳轉等標志。如ADD加法指令的執行函數:彈出棧頂的x,獲得y的值計算x+y,并更新到y,y保存的就是ADD的成果釋放int回pool而取指令并執行的過程就在EVM.interpreter.Run()里。我們先從上一節最后的ApplyTransaction()函數開始一步一步查看調用通過的流程:AApplyTransction(…)_,gas,failed,err:=ApplyMessage(vmenv,msg,gp)st:=NewStateTransition(evm,msg,gp)st.TransactionDB()ContractCreation?Evm.Create(….)Evm.Call(…)創立合約調用合約交易以正常的執行流程到最后只有兩種,一種是創立合約,二是調用合約。它們都包含了Ether幣的轉賬操作。如果Call調用的To

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯系上傳者。文件的所有權益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
  • 4. 未經權益所有人同意不得將文件中的內容挪作商業或盈利用途。
  • 5. 人人文庫網僅提供信息存儲空間,僅對用戶上傳內容的表現方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
  • 6. 下載文件中如有侵權或不適當內容,請與我們聯系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論