本文經過宏觀和微觀兩個層面窺探以太坊底層執行邏輯。
宏觀層面描述建立並運行一個小型帶錢包的發幣APP的過程,微觀層面是順藤摸瓜從http api深刻go-ethereum源碼執行過程。javascript
分析思路:自上而下,從APP深刻EVM。前端
從應用入手,若是一頭扎進ethereum,收穫的多是純理論的東西,要想有所理解還得結合之後的實踐才能恍然大悟。因此我始終堅持從應用入手、自上而下是一種正確、事半功倍的方法論。vue
我在講解以太坊基礎概念的那篇專題文章裏,用的是從總體到局部的方法論,由於研究目標就是一個抽象的理論的東西,我對一個全然未知的東西的瞭解老是堅持從總體到局部的思路。java
以前用truffle框架作項目開發,這個框架封裝了合約的建立、編譯、部署過程,爲了研究清楚自上至下的架構,這裏就不用truffle構建項目了。node
項目前端基於vue,後端是geth節點,經過web3 http api通訊。webpack
開發vue、solidity等前端IDE仍是webstorm好,Atom和goland就免了不太好用!
一、全局安裝vue-cligit
npm i -g vue-cli
二、初始化一個基於webpack的vue項目github
vue init webpack XXXProject
三、項目裏安裝web3依賴web
web3.js是ethereum的javascript api庫,經過rpc方式與以太坊節點交互。vuex
npm install --save web3@1.0.0-beta.34
儘可能用npm安裝,不要用cnpm,有時候是個坑玩意,會生成「_」開頭的不少垃圾還要求各類install。也能夠寫好了package.json,刪除node_modules文件夾,再執行npm i。web3版本用1.0以上,和1.0如下語法有很大不一樣。
四、項目裏建立全局web3對象
用vuex有點囉嗦,這裏就寫個vue插件,提供全局的web3對象。
import Web3 from "web3" export default { install: function (Vue, options) { var web3 = window.web3 if (typeof web3 !== 'undefined') { web3 = new Web3(web3.currentProvider) } else { web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545')) } Vue.prototype.$web3 = web3 } }
在main.js裏啓用該插件,之後就能夠這樣使用this.$web3這個全局對象了。
Vue.use(插件名)
五、寫一個ERC20合約
代碼省略
項目所有代碼地址:\
https://github.com/m3shine/To...
六、編譯&部署合約
有必要說明一下編譯和部署方式的選擇,它嚴重關係到你實際項目的開發:
1)使用Remix,把本身寫好的合約拷貝到Remix裏進行編譯和部署。這種方式最方便。\
2)使用truffle這類的框架,這種方式是須要項目基於框架開發了,編譯和部署也是在truffle控制檯進行。\
3)基於web3和solc依賴,寫編譯(solc)和部署(web3)程序,這些代碼就獨立(vue是前端,nodejs是後端,運行環境不一樣)於項目了,用node單獨運行。\
4)本地安裝solidity進行編譯,部署的話也須要本身想辦法完成。\
5)使用geth錢包、mist等編譯部署。\
……
從geth1.6.0開始,剝離了solidity編譯函數,因此web3也不能調用編譯方法了。能夠本地安裝solidity帶編譯器,也能夠在項目裏依賴solc進行編譯。
編譯部署的方式眼花繚亂,這裏選擇方式3。
編譯部署參考代碼(web3的1.0及以上版本):token_deploy.js
const Web3 = require('web3') const fs = require('fs') const solc = require('solc') const web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")); const input = fs.readFileSync('../contracts/Token.sol'); const output = solc.compile(input.toString(), 1); fs.writeFile('../abi/Token.abi', output.contracts[':Token'].interface, err => { if (err) { console.log(err) } }) const bytecode = output.contracts[':Token'].bytecode; const abi = JSON.parse(output.contracts[':Token'].interface); const tokenContract = new web3.eth.Contract(abi); let log = { time: new Date(Date.now()), transactionHash: '', contractAddress: '' } // 部署合約 tokenContract.deploy({ data: '0x' + bytecode, arguments: ['200000000', '魔法幣', 'MFC'] // Token.sol構造參數 }) .send({ from: '0x2d2afb7d0ef71f85dfbdc89d288cb3ce8e049e10', //寫你本身的礦工(發幣)地址 gas: 5000000, // 這個值很微妙 }, (err, transactionHash) => { if (err) { console.log(err); return; } log.transactionHash = transactionHash }) .on('error', error => { console.log(error); }) // 不是總能成功獲取newContractInstance, 包括監聽receipt也可能發生異常,緣由是receipt獲取時機可能發生在交易未完成前。 .then(function (newContractInstance) { if(newContractInstance){ log.contractAddress = newContractInstance.options.address } fs.appendFile('Token_deploy.log',JSON.stringify(log) + '\r\n', err => { if (err) { console.log(err) } }) }); ;
七、在執行部署腳本前,須要有一個帳戶並解鎖,在geth控制檯執行如下命令:
personal.newAccount('密碼') personal.unlockAccount(eth.coinbase,'密碼','20000')
八、發佈合約是須要eth幣的,因此先挖礦弄點以太幣:
miner.start()
九、如今能夠執行編譯部署腳本了:
node token_deploy.js
若是前面miner.stop()過,那麼在執行部署的時候,要確保miner.start(),有礦工打包才能出塊。\
這裏還要知道,由於就是本礦工帳戶建立合約,因此交易費又回到了本帳戶,所以餘額看起來老是沒有減小。
至此,咱們已經在私鏈上部署了一個合約,產生了一筆交易(即建立合約自己這個交易)、一個礦工帳戶、一個合約帳戶。
Error: insufficient funds for gas * price + value
意思是帳戶裏沒有足夠的eth幣,給建立合約的帳戶里弄些比特幣。
Error: intrinsic gas too low
調高如下發布合約時的gas值。
Error: Invalid number of parameters for "undefined". Got 0 expected 3! (相似這樣的)
沒有傳入合約構造函數參數
合約部署成功,就有了合約地址,根據合約地址構建合約實例。
let tokenContract = new this.$web3.eth.Contract(JSON.parse(abi),'合約地址')
tokenContract.methods.myMethod.
call()調用的都是abi裏的constant方法,即合約裏定義的狀態屬性,EVM裏不會發送交易,不會改變合約狀態。
send()調用的是合約裏定義的函數,是要發送交易到合約並執行合約方法的,會改變合約狀態。
以上就簡單說一下,不寫太多了。看官能夠自行下載本項目源碼(上面第5步有github連接),本身運行起來看看界面和發幣/轉帳操做。
當咱們在項目中建立一個合約的時候,發生了什麼?\
geth節點默認開放了8545 RPC端口,web3經過鏈接這個rpc端口,以http的方式調用geth開放的rpc方法。從這一web3與以太坊節點交互基本原理入手,先分析web3源碼是怎樣調用rpc接口,對應的geth接口是否同名,再看geth源碼該接口又是怎麼執行的。
new web3.eth.Contract(jsonInterface[, address][, options])這個函數,jsonInterface就是abi,無論傳不傳options,options.data屬性老是abi的編碼。\
這個web3接口源碼中調用eth.sendTransaction,options.data編碼前面加了簽名,options.to賦值一個地址,最後返回這筆交易的hash。
再返回上面第6步看一下部署腳本,代碼截止到deploy都是在構造web3裏的對象,首次與本地geth節點通訊的方法是send,它是web3的一個接口方法。
deploy返回的是個web3定義的泛型TransactionObject<Contract>。\
Contract對send接口方法的實現以下:
var sendTransaction = (new Method({ name: 'sendTransaction', call: 'eth_sendTransaction', params: 1, inputFormatter: [formatters.inputTransactionFormatter], requestManager: _this._parent._requestManager, accounts: Contract._ethAccounts, // is eth.accounts (necessary for wallet signing) defaultAccount: _this._parent.defaultAccount, defaultBlock: _this._parent.defaultBlock, extraFormatters: extraFormatters })).createFunction(); return sendTransaction(args.options, args.callback);
這個send最終由XMLHttpRequest2的request.send(JSON.stringify(payload))與節點通訊。
var sendSignedTx = function(sign){ payload.method = 'eth_sendRawTransaction'; payload.params = [sign.rawTransaction]; method.requestManager.send(payload, sendTxCallback); };
因此send方法對應的節點api是eth_sendRawTransaction。
go-ethereum/ethclient/ethclient.go
func (ec *Client) SendTransaction(ctx context.Context, tx *types.Transaction) error { data, err := rlp.EncodeToBytes(tx) if err != nil { return err } return ec.c.CallContext(ctx, nil, "eth_sendRawTransaction", common.ToHex(data)) }
找到該api執行入口\
go-ethereum/internal/ethapi.SendTransaction
func (s *PublicTransactionPoolAPI) SendTransaction(ctx context.Context, args SendTxArgs) (common.Hash, error) { // Look up the wallet containing the requested signer account := accounts.Account{Address: args.From} wallet, err := s.b.AccountManager().Find(account) if err != nil { return common.Hash{}, err } …… return submitTransaction(ctx, s.b, signed) }
咱們在這個函數處打一個斷點!而後執行部署腳本(能夠屢次執行),運行到斷點處:
要調試geth須要對其從新編譯,去掉它原來編譯的優化,參見下面「調試源碼」一節。
(dlv) p args github.com/ethereum/go-ethereum/internal/ethapi.SendTxArgs { From: github.com/ethereum/go-ethereum/common.Address [45,42,251,125,14,247,31,133,223,189,200,157,40,140,179,206,142,4,158,16], To: *github.com/ethereum/go-ethereum/common.Address nil, Gas: *5000000, GasPrice: *github.com/ethereum/go-ethereum/common/hexutil.Big { neg: false, abs: math/big.nat len: 1, cap: 1, [18000000000],}, Value: *github.com/ethereum/go-ethereum/common/hexutil.Big nil, Nonce: *github.com/ethereum/go-ethereum/common/hexutil.Uint64 nil, Data: *github.com/ethereum/go-ethereum/common/hexutil.Bytes len: 2397, cap: 2397, [96,96,96,64,82,96,2,128,84,96,255,25,22,96,18,23,144,85,52,21,97,0,28,87,96,0,128,253,91,96,64,81,97,8,125,56,3,128,97,8,125,131,57,129,1,96,64,82,128,128,81,145,144,96,32,1,128,81,130,1,145,144,96,32,...+2333 more], Input: *github.com/ethereum/go-ethereum/common/hexutil.Bytes nil,}
(dlv) p wallet github.com/ethereum/go-ethereum/accounts.Wallet(*github.com/ethereum/go-ethereum/accounts/keystore.keystoreWallet) *{ account: github.com/ethereum/go-ethereum/accounts.Account { Address: github.com/ethereum/go-ethereum/common.Address [45,42,251,125,14,247,31,133,223,189,200,157,40,140,179,206,142,4,158,16], URL: (*github.com/ethereum/go-ethereum/accounts.URL)(0xc4200d9f18),}, keystore: *github.com/ethereum/go-ethereum/accounts/keystore.KeyStore { storage: github.com/ethereum/go-ethereum/accounts/keystore.keyStore(*github.com/ethereum/go-ethereum/accounts/keystore.keyStorePassphrase) ..., cache: *(*github.com/ethereum/go-ethereum/accounts/keystore.accountCache)(0xc4202fe360), changes: chan struct {} { qcount: 0, dataqsiz: 1, buf: *[1]struct struct {} [ {}, ], elemsize: 0, closed: 0, elemtype: *runtime._type { size: 0, ptrdata: 0, hash: 670477339, tflag: 2, align: 1, fieldalign: 1, kind: 153, alg: *(*runtime.typeAlg)(0x59e69d0), gcdata: *1, str: 67481, ptrToThis: 601472,}, sendx: 0, recvx: 0, recvq: waitq<struct {}> { first: *(*sudog<struct {}>)(0xc42006ed20), last: *(*sudog<struct {}>)(0xc42006ed20),}, sendq: waitq<struct {}> { first: *sudog<struct {}> nil, last: *sudog<struct {}> nil,}, lock: runtime.mutex {key: 0},}, unlocked: map[github.com/ethereum/go-ethereum/common.Address]*github.com/ethereum/go-ethereum/accounts/keystore.unlocked [...], wallets: []github.com/ethereum/go-ethereum/accounts.Wallet len: 2, cap: 2, [ ..., ..., ], updateFeed: (*github.com/ethereum/go-ethereum/event.Feed)(0xc4202c4040), updateScope: (*github.com/ethereum/go-ethereum/event.SubscriptionScope)(0xc4202c40b0), updating: true, mu: (*sync.RWMutex)(0xc4202c40cc),},}
(dlv) p s.b github.com/ethereum/go-ethereum/internal/ethapi.Backend(*github.com/ethereum/go-ethereum/eth.EthApiBackend) *{ eth: *github.com/ethereum/go-ethereum/eth.Ethereum { config: *(*github.com/ethereum/go-ethereum/eth.Config)(0xc420153000), chainConfig: *(*github.com/ethereum/go-ethereum/params.ChainConfig)(0xc4201da540), shutdownChan: chan bool { qcount: 0, dataqsiz: 0, buf: *[0]bool [], elemsize: 1, closed: 0, elemtype: *runtime._type { size: 1, ptrdata: 0, hash: 335480517, tflag: 7, align: 1, fieldalign: 1, kind: 129, alg: *(*runtime.typeAlg)(0x59e69e0), gcdata: *1, str: 21072, ptrToThis: 452544,}, sendx: 0, recvx: 0, recvq: waitq<bool> { first: *(*sudog<bool>)(0xc420230ba0), last: *(*sudog<bool>)(0xc420231440),}, sendq: waitq<bool> { first: *sudog<bool> nil, last: *sudog<bool> nil,}, lock: runtime.mutex {key: 0},}, stopDbUpgrade: nil, txPool: *(*github.com/ethereum/go-ethereum/core.TxPool)(0xc420012380), blockchain: *(*github.com/ethereum/go-ethereum/core.BlockChain)(0xc42029c000), protocolManager: *(*github.com/ethereum/go-ethereum/eth.ProtocolManager)(0xc420320270), lesServer: github.com/ethereum/go-ethereum/eth.LesServer nil, chainDb: github.com/ethereum/go-ethereum/ethdb.Database(*github.com/ethereum/go-ethereum/ethdb.LDBDatabase) ..., eventMux: *(*github.com/ethereum/go-ethereum/event.TypeMux)(0xc4201986c0), engine: github.com/ethereum/go-ethereum/consensus.Engine(*github.com/ethereum/go-ethereum/consensus/ethash.Ethash) ..., accountManager: *(*github.com/ethereum/go-ethereum/accounts.Manager)(0xc420089860), bloomRequests: chan chan *github.com/ethereum/go-ethereum/core/bloombits.Retrieval { qcount: 0, dataqsiz: 0, buf: *[0]chan *github.com/ethereum/go-ethereum/core/bloombits.Retrieval [], elemsize: 8, closed: 0, elemtype: *runtime._type { size: 8, ptrdata: 8, hash: 991379238, tflag: 2, align: 8, fieldalign: 8, kind: 50, alg: *(*runtime.typeAlg)(0x59e6a10), gcdata: *1, str: 283111, ptrToThis: 0,}, sendx: 0, recvx: 0, recvq: waitq<chan *github.com/ethereum/go-ethereum/core/bloombits.Retrieval> { first: *(*sudog<chan *github.com/ethereum/go-ethereum/core/bloombits.Retrieval>)(0xc420230c00), last: *(*sudog<chan *github.com/ethereum/go-ethereum/core/bloombits.Retrieval>)(0xc4202314a0),}, sendq: waitq<chan *github.com/ethereum/go-ethereum/core/bloombits.Retrieval> { first: *sudog<chan *github.com/ethereum/go-ethereum/core/bloombits.Retrieval> nil, last: *sudog<chan *github.com/ethereum/go-ethereum/core/bloombits.Retrieval> nil,}, lock: runtime.mutex {key: 0},}, bloomIndexer: unsafe.Pointer(0xc4201b23c0), ApiBackend: *(*github.com/ethereum/go-ethereum/eth.EthApiBackend)(0xc4202b8910), miner: *(*github.com/ethereum/go-ethereum/miner.Miner)(0xc420379540), gasPrice: *(*math/big.Int)(0xc420233c40), etherbase: github.com/ethereum/go-ethereum/common.Address [45,42,251,125,14,247,31,133,223,189,200,157,40,140,179,206,142,4,158,16], networkId: 13, netRPCService: *(*github.com/ethereum/go-ethereum/internal/ethapi.PublicNetAPI)(0xc42007feb0), lock: (*sync.RWMutex)(0xc4202ea528),}, gpo: *github.com/ethereum/go-ethereum/eth/gasprice.Oracle { backend: github.com/ethereum/go-ethereum/internal/ethapi.Backend(*github.com/ethereum/go-ethereum/eth.EthApiBackend) ..., lastHead: github.com/ethereum/go-ethereum/common.Hash [139,147,220,247,224,227,136,250,220,62,217,102,160,96,23,182,90,90,108,254,82,158,234,95,150,120,163,5,61,248,168,168], lastPrice: *(*math/big.Int)(0xc420233c40), cacheLock: (*sync.RWMutex)(0xc420010938), fetchLock: (*sync.Mutex)(0xc420010950), checkBlocks: 20, maxEmpty: 10, maxBlocks: 100, percentile: 60,},}
// submitTransaction is a helper function that submits tx to txPool and logs a message. func submitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (common.Hash, error) { if err := b.SendTx(ctx, tx); err != nil { return common.Hash{}, err } if tx.To() == nil { signer := types.MakeSigner(b.ChainConfig(), b.CurrentBlock().Number()) from, err := types.Sender(signer, tx) if err != nil { return common.Hash{}, err } addr := crypto.CreateAddress(from, tx.Nonce()) log.Info("Submitted contract creation", "fullhash", tx.Hash().Hex(), "contract", addr.Hex()) } else { log.Info("Submitted transaction", "fullhash", tx.Hash().Hex(), "recipient", tx.To()) } return tx.Hash(), nil }
(dlv) p tx *github.com/ethereum/go-ethereum/core/types.Transaction { data: github.com/ethereum/go-ethereum/core/types.txdata { AccountNonce: 27, Price: *(*math/big.Int)(0xc4217f5640), GasLimit: 5000000, Recipient: *github.com/ethereum/go-ethereum/common.Address nil, Amount: *(*math/big.Int)(0xc4217f5620), Payload: []uint8 len: 2397, cap: 2397, [96,96,96,64,82,96,2,128,84,96,255,25,22,96,18,23,144,85,52,21,97,0,28,87,96,0,128,253,91,96,64,81,97,8,125,56,3,128,97,8,125,131,57,129,1,96,64,82,128,128,81,145,144,96,32,1,128,81,130,1,145,144,96,32,...+2333 more], V: *(*math/big.Int)(0xc4217e0a20), R: *(*math/big.Int)(0xc4217e09c0), S: *(*math/big.Int)(0xc4217e09e0), Hash: *github.com/ethereum/go-ethereum/common.Hash nil,}, hash: sync/atomic.Value { noCopy: sync/atomic.noCopy {}, v: interface {} nil,}, size: sync/atomic.Value { noCopy: sync/atomic.noCopy {}, v: interface {} nil,}, from: sync/atomic.Value { noCopy: sync/atomic.noCopy {}, v: interface {} nil,},}
(dlv) bt 0 0x00000000048d9248 in github.com/ethereum/go-ethereum/internal/ethapi.submitTransaction at ./go/src/github.com/ethereum/go-ethereum/internal/ethapi/api.go:1130 1 0x00000000048d9bd1 in github.com/ethereum/go-ethereum/internal/ethapi.(*PublicTransactionPoolAPI).SendTransaction at ./go/src/github.com/ethereum/go-ethereum/internal/ethapi/api.go:1176
(dlv) frame 0 l > github.com/ethereum/go-ethereum/internal/ethapi.submitTransaction() ./go/src/github.com/ethereum/go-ethereum/internal/ethapi/api.go:1130 (PC: 0x48d9248) Warning: debugging optimized function 1125: if err := b.SendTx(ctx, tx); err != nil { 1126: return common.Hash{}, err 1127: } 1128: if tx.To() == nil { 1129: signer := types.MakeSigner(b.ChainConfig(), b.CurrentBlock().Number()) =>1130: from, err := types.Sender(signer, tx) 1131: if err != nil { 1132: return common.Hash{}, err 1133: } 1134: addr := crypto.CreateAddress(from, tx.Nonce()) 1135: log.Info("Submitted contract creation", "fullhash", tx.Hash().Hex(), "contract", addr.Hex()) (dlv) frame 1 l Goroutine 3593 frame 1 at /Users/jiang/go/src/github.com/ethereum/go-ethereum/internal/ethapi/api.go:1176 (PC: 0x48d9bd1) 1171: } 1172: signed, err := wallet.SignTx(account, tx, chainID) 1173: if err != nil { 1174: return common.Hash{}, err 1175: } =>1176: return submitTransaction(ctx, s.b, signed) 1177: } 1178: 1179: // SendRawTransaction will add the signed transaction to the transaction pool. 1180: // The sender is responsible for signing the transaction and using the correct nonce. 1181: func (s *PublicTransactionPoolAPI) SendRawTransaction(ctx context.Context, encodedTx hexutil.Bytes) (common.Hash, error) {
先把調試結果展現出來,經過對一個交易的內部分析,能夠了解EVM執行的大部分細節,此處須要另開篇幅詳述。請關注後續專題。
一、從新強制編譯geth,去掉編譯內聯優化,方便跟蹤調試。
cd path/go-ethereum sudo go install -a -gcflags '-N -l' -v ./cmd/geth
編譯後的geth執行文件就在$gopath/bin下。
二、在datadir下啓動這個從新編譯後的geth
geth --datadir "./" --rpc --rpccorsdomain="*" --networkid 13 console 2>>00.glog
三、調試這個進程
dlv attach <gethPid>
四、給交易api入口函數設置斷點
b ethapi.(*PublicTransactionPoolAPI).SendTransaction
下面是一個區塊鏈小程序,供你們參考。