本文经过宏观和微观两个层面窥探以太坊底层执行逻辑。
宏观层面描述建立并运行一个小型带钱包的发币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
下面是一个区块链小程序,供你们参考。