基於Vue、web3的以太坊項目開發及交易內幕初探

本文經過宏觀和微觀兩個層面窺探以太坊底層執行邏輯。
宏觀層面描述建立並運行一個小型帶錢包的發幣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

下面是一個區塊鏈小程序,供你們參考。
圖片描述

相關文章
相關標籤/搜索