基於Vue、web3的以太坊項目開發及交易內幕初探 錯誤解決總結

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

本文經過宏觀和微觀兩個層面窺探以太坊底層執行邏輯。
宏觀層面描述建立並運行一個小型帶錢包的發幣APP的過程,微觀層面是順藤摸瓜從http api深刻go-ethereum源碼執行過程。前端

分析思路:自上而下,從APP深刻EVM。vue

從應用入手,若是一頭扎進ethereum,收穫的多是純理論的東西,要想有所理解還得結合之後的實踐才能恍然大悟。因此我始終堅持從應用入手、自上而下是一種正確、事半功倍的方法論。java

我在講解以太坊基礎概念的那篇專題文章裏,用的是從總體到局部的方法論,由於研究目標就是一個抽象的理論的東西,我對一個全然未知的東西的瞭解老是堅持從總體到局部的思路。node

項目建立、部署合約到私鏈
以前用truffle框架作項目開發,這個框架封裝了合約的建立、編譯、部署過程,爲了研究清楚自上至下的架構,這裏就不用truffle構建項目了。webpack

項目前端基於vue,後端是geth節點,經過web3 http api通訊。git

開發vue、solidity等前端IDE仍是webstorm好,Atom和goland就免了不太好用!
一、全局安裝vue-cligithub

npm i -g vue-cli
二、初始化一個基於webpack的vue項目web

vue init webpack XXXProject
三、項目裏安裝web3依賴vuex

web3.js是ethereum的javascript api庫,經過rpc方式與以太坊節點交互。

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對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 {
first: (sudog )(0xc420230ba0),
last: (sudog )(0xc420231440),},
sendq: waitq {
first: sudog nil,
last:
sudog 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
四、給交易api入口函數設置斷點

b ethapi.(*PublicTransactionPoolAPI).SendTransaction 下面是一個區塊鏈小程序,供你們參考。 圖片描述

相關文章
相關標籤/搜索