title: Ethereum 以太坊智能合約調(diào)用源碼分析
date: 2018-06-16 09:47:08
tags:
- 以太坊
- 智能合約
categories:
- 區(qū)塊鏈
0 前言
- 基于 Ethereum Coffice (v.1.8.8) 分析
- 需要區(qū)塊鏈基礎(chǔ)知識(shí)
- 有 C++/JAVA 基礎(chǔ)就能理解文中 go 代碼
- 本文不包含合約部署的前序步驟,即sol源碼編譯打包過(guò)程,請(qǐng)參考網(wǎng)絡(luò)上其他文章
- 流程圖使用 ProcessOn.com 在線繪制
- 合約調(diào)用與交易的流程大同小異,可參考之前筆者文章“Ethereum 以太坊智能合約部署源碼分析”
整體流程圖如下所示:

1 API 發(fā)起智能合約執(zhí)行交易
在部署好的智能合約上,可以發(fā)起合約調(diào)用。每次合約調(diào)用將生成一筆新的交易,Ethereum 區(qū)塊鏈網(wǎng)絡(luò)需要處理交易上鏈?zhǔn)录O旅媸墙灰装l(fā)起過(guò)程用到的兩個(gè)主要數(shù)據(jù)結(jié)構(gòu):
1.1 相關(guān)數(shù)據(jù)結(jié)構(gòu)
- 客戶端側(cè)傳輸數(shù)據(jù)結(jié)構(gòu)展示:
/*
file: ethapi\api.go
*/
type SendTxArgs struct {
From common.Address `json:"from"`
To *common.Address `json:"to"`
Gas *hexutil.Uint64 `json:"gas"`
GasPrice *hexutil.Big `json:"gasPrice"`
Value *hexutil.Big `json:"value"`
Nonce *hexutil.Uint64 `json:"nonce"`
Data *hexutil.Bytes `json:"data"`
Input *hexutil.Bytes `json:"input"`
}
- Ethereum 交易數(shù)據(jù)結(jié)構(gòu)展示:
/*
file: core\types\transaction.go
*/
type Transaction struct {
data txdata
hash atomic.Value
size atomic.Value
from atomic.Value
}
type txdata struct {
AccountNonce uint64 `json:"nonce" gencodec:"required"`
Price *big.Int `json:"gasPrice" gencodec:"required"`
GasLimit uint64 `json:"gas" gencodec:"required"`
Recipient *common.Address `json:"to" rlp:"nil"`
Amount *big.Int `json:"value" gencodec:"required"`
Payload []byte `json:"input" gencodec:"required"`
V *big.Int `json:"v" gencodec:"required"`
R *big.Int `json:"r" gencodec:"required"`
S *big.Int `json:"s" gencodec:"required"`
Hash *common.Hash `json:"hash" rlp:"-"`
}
1.2 流程圖細(xì)節(jié)
-
ethapi/api.go識(shí)別到這是一次交易發(fā)送請(qǐng)求,調(diào)用SendTransaction()函數(shù),進(jìn)一步向底層發(fā)送消息。首先,函數(shù)會(huì)檢查發(fā)送交易賬戶的合法性,并且為交易設(shè)置一系列的初始值,例如 gas, nonce 等。交易信息填滿后,放到交易池中,等待下一步處理。
1a. 根據(jù)傳入信息創(chuàng)建Transaction。注意此處有個(gè)判斷語(yǔ)句,根據(jù)To是否有值來(lái)確定是合約創(chuàng)建或調(diào)用。if args.To == nil { // 合約調(diào)用時(shí),此處to不為空 return types.NewContractCreation(...) } return types.NewTransaction(...)1b.
SignTx()使用交易發(fā)起者信息對(duì)交易進(jìn)行簽名。此處有wallet的概念,官方源碼注釋中多次提到“USB”。筆者網(wǎng)上檢索后知道這是以太坊推出的硬件錢(qián)包,它是一個(gè)安全存儲(chǔ)私鑰的硬件設(shè)備,通過(guò)USB與電腦連接。在進(jìn)行交易時(shí),發(fā)送交易到 Ethereum 區(qū)塊鏈網(wǎng)絡(luò)時(shí),需要使用硬件錢(qián)包的私鑰進(jìn)行交易簽名。下面引用一段對(duì)于硬件錢(qián)包的說(shuō)明:Ledger的硬件錢(qián)包圍繞“安全元件”或安全芯片進(jìn)行架構(gòu)。這是和芯片及PIN支付或SIM卡中使用的相同技術(shù)。這些芯片為物理攻擊提供保護(hù),大大提升私鑰的安全性。 我們的研發(fā)人員來(lái)自安全行業(yè)(曾服務(wù)于數(shù)字安全領(lǐng)域的全球領(lǐng)先企業(yè)Gemalto金雅拓,Oberthur 歐貝特等企業(yè)),我們?cè)谥悄芸ê桶踩度胧讲僮飨到y(tǒng)領(lǐng)域擁有豐富的經(jīng)驗(yàn)。
go var chainID *big.Int if config := s.b.ChainConfig(); config.IsEIP155(s.b.CurrentBlock().Number()) { chainID = config.ChainId } signed, err := wallet.SignTx(account, tx, chainID)
若是檢測(cè)到 Ethereum 軟件未運(yùn)行,將直接反饋空的交易信息
go /* file: accouts\usbwallet\ledger.go */ if w.offline() { return common.Address{}, nil, accounts.ErrWalletClosed }
1c.submitTransaction()將交易推進(jìn)交易池,等待下一步處理。
go func submitTransaction(...) (common.Hash, error) { if err := b.SendTx(ctx, tx); err != nil { return common.Hash{}, err } if tx.To() == nil { ... addr := crypto.CreateAddress(from, tx.Nonce()) log.Info(... "contract", addr.Hex()) } ... return tx.Hash(), nil }
入池操作會(huì)調(diào)用如下關(guān)鍵函數(shù)。交易池分為兩個(gè)交易列表,pending中存放可執(zhí)行的交易,queue中存放排隊(duì)中暫不可執(zhí)行的交易,他們之間可以相互轉(zhuǎn)換。入池之前,pool.validateTx()會(huì)對(duì)交易簽名進(jìn)行初步檢查,對(duì)于一筆合法的新交易,pool.enqueueTx()會(huì)將其先放入queue列表,隨后promoteExecutables()視條件將其移入pending中。
```go
/*
file: core\tx_pool.go
*/
func (pool *TxPool) addTx(...) error {
...
replace, err := pool.add(tx, local)
if err != nil {
return err
}if !replace { from, _ := types.Sender(pool.signer, tx) pool.promoteExecutables([]common.Address{from}) } return nil}
2 Worker 交易打包出塊
隨著交易池的增長(zhǎng),礦工服務(wù)根據(jù)一定的條件完成交易打包與出塊操作。生成的塊將在網(wǎng)絡(luò)中通過(guò)P2P服務(wù)傳播。
(細(xì)節(jié)待完成)
3 Blockchain 區(qū)塊上鏈
3.1 概述
在收到網(wǎng)絡(luò)上的傳送來(lái)的區(qū)塊后,觸發(fā) blockchain.go 的 InsertChain() 函數(shù),進(jìn)入?yún)^(qū)塊的處理過(guò)程: 區(qū)塊鏈校驗(yàn) -> 區(qū)塊頭驗(yàn)證->區(qū)塊體驗(yàn)證->交易處理->狀態(tài)驗(yàn)證->區(qū)塊上鏈。
/*
file: core\blockchain.go
*/
func (bc *BlockChain) insertChain(chain types.Blocks) (...) {
for i := 1; i < len(chain); i++ {
if chain[i].NumberU64() != chain[i-1].NumberU64()+1 || chain[i].ParentHash() != chain[i-1].Hash() {
...
return 0, nil, nil, ...
}
}
...
abort, results := bc.engine.VerifyHeaders(...)
...
for i, block := range chain {
...
err := <-results
if err == nil {
err = bc.Validator().ValidateBody(block)
}
...
state, err := state.New(parent.Root(), bc.stateCache)
if err != nil {
return i, events, coalescedLogs, err
}
receipts, logs, usedGas, err := bc.processor.Process(...)
if err != nil {
bc.reportBlock(block, receipts, err)
return i, events, coalescedLogs, err
}
err = bc.Validator().ValidateState(...)
if err != nil {
bc.reportBlock(block, receipts, err)
return i, events, coalescedLogs, err
}
status, err := bc.WriteBlockWithState(...)
if err != nil {
return i, events, coalescedLogs, err
}
switch status {
case CanonStatTy:
...
case SideStatTy:
...
}
...
return 0, events, coalescedLogs, nil
}
- Message 數(shù)據(jù)結(jié)構(gòu)展示
type Message struct {
to *common.Address
from common.Address
nonce uint64
amount *big.Int
gasLimit uint64
gasPrice *big.Int
data []byte
checkNonce bool
}
3.2 流程圖細(xì)節(jié)
- 交易處理過(guò)程調(diào)用
state_processer.go的Process()函數(shù)對(duì)區(qū)塊中的所有交易進(jìn)行逐一處理。
/*
file: core\state_processor.go
*/
func (p *StateProcessor) Process(...) (types.Receipts, []*types.Log, uint64, error) {
...
for i, tx := range block.Transactions() {
statedb.Prepare(tx.Hash(), block.Hash(), i)
receipt, _, err := ApplyTransaction(...)
if err != nil {
return nil, nil, 0, err
}
receipts = append(receipts, receipt)
allLogs = append(allLogs, receipt.Logs...)
}
p.engine.Finalize(...)
return receipts, allLogs, *usedGas, nil
}
2a. AsMessage() 函數(shù)將 Transaction 數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換為 Message 數(shù)據(jù)結(jié)構(gòu)
msg, err := tx.AsMessage(types.MakeSigner(config, header.Number))
if err != nil {
return nil, 0, err
}
2b. newEvm() 為合約的執(zhí)行創(chuàng)建虛擬機(jī)環(huán)境。
context := NewEVMContext(msg, header, bc, author)
vmenv := vm.NewEVM(context, statedb, config, cfg)
2c. ApplyMessage() 創(chuàng)建一個(gè)新的狀態(tài)變更實(shí)例,并且調(diào)用 state_transition.go 的 TransitionDb() 函數(shù)應(yīng)用合約代碼到這個(gè)實(shí)例中,發(fā)生實(shí)質(zhì)上的狀態(tài)轉(zhuǎn)移記錄。 TransitionDb() 首先會(huì)進(jìn)行交易執(zhí)行的入口檢查,確認(rèn) nonce, gas 是否合法可用;隨后根據(jù)是合約創(chuàng)建還是合約調(diào)用,選擇 Create() 或者 Call(),在合約部署中,此處會(huì)調(diào)用 Call()
/*
file: core\state_transition.go
*/
func (st *StateTransition) TransitionDb() () {
if err = st.preCheck(); err != nil {
return
}
...
contractCreation := msg.To() == nil
...
if contractCreation {
ret, _, st.gas, vmerr = evm.Create(...)
} else {
st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1)
ret, st.gas, vmerr = evm.Call(...) // 合約調(diào)用
}
...
return ret, st.gasUsed(), vmerr != nil, err
}
下面進(jìn)入到合約調(diào)用最核心的流程,由 core\vm\evm.go 的 Call() 完成。在真正調(diào)用前,會(huì)做一些基本的合法性檢查,例如余額是否足夠進(jìn)行轉(zhuǎn)賬,賬戶合法性等。注意此處若對(duì)象賬戶不存在,內(nèi)部會(huì)為賬戶地址創(chuàng)建新的賬戶。run() 函數(shù)將執(zhí)行合約代碼,第三個(gè)參數(shù)為nil,代表不調(diào)用合約中的任何函數(shù),只是初始化。
Tips:Ethereum 中,在進(jìn)行賬戶的查詢功能和轉(zhuǎn)賬功能時(shí),都是合約調(diào)用交易。但是我們知道,查詢動(dòng)作實(shí)際無(wú)需上鏈,轉(zhuǎn)賬才需要。Ethereum 把這項(xiàng)控制設(shè)計(jì)在了智能合約的編寫(xiě)中,通過(guò)sol語(yǔ)言的函數(shù)修飾關(guān)鍵字來(lái)控制。
在Solidity中
constant、view、pure三個(gè)函數(shù)修飾詞的作用是告訴編譯器,函數(shù)不改變/不讀取狀態(tài)變量,這樣函數(shù)執(zhí)行就可以不消耗gas了,因?yàn)椴恍枰V工來(lái)驗(yàn)證。在Solidity v4.17之前,只有constant,后續(xù)版本將constant拆成了view和pure。view的作用和constant一模一樣,可以讀取狀態(tài)變量但是不能改;pure則更為嚴(yán)格,pure修飾的函數(shù)不能改也不能讀狀態(tài)變量,智能操作函數(shù)內(nèi)部變量,否則編譯通不過(guò)。
作者:Kirn
鏈接:http://m.itdecent.cn/p/52eec7ac5413
/*
file: core\vm\evm.go
*/
func (evm *EVM) Call(...) (...) {
if evm.vmConfig.NoRecursion && evm.depth > 0 {
return nil, gas, nil
}
if evm.depth > int(params.CallCreateDepth) {
return nil, gas, ErrDepth
}
// 余額是否充足
if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) {
return nil, gas, ErrInsufficientBalance
}
var (
to = AccountRef(addr)
snapshot = evm.StateDB.Snapshot()
)
if !evm.StateDB.Exist(addr) {
...
// 對(duì)方地址無(wú)賬戶,則創(chuàng)建
evm.StateDB.CreateAccount(addr)
}
// 在DB層面執(zhí)行轉(zhuǎn)賬動(dòng)作
evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value)
// 為當(dāng)前合約執(zhí)行創(chuàng)建一個(gè)臨時(shí)的合約
contract := NewContract(caller, to, value, gas)
contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))
...
ret, err = run(evm, contract, input)
// 調(diào)用失敗,根據(jù)快照回滾數(shù)據(jù)
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
...
}
return ret, contract.Gas, err
}
-
blockchain.go的WriteBlockWithState()將掉用accessors_chain.go的 3a.WriteBlock()3b.WriteReceipts()寫(xiě)入?yún)^(qū)塊和收據(jù)。
4 API 獲取收據(jù)
4.1 概述
在上面的流程中,交易執(zhí)行完畢后,不會(huì)顯式地向請(qǐng)求端回復(fù)任何交易執(zhí)行的具體情況。在 Ethereum 區(qū)塊鏈網(wǎng)絡(luò)中,需要請(qǐng)求端顯式地請(qǐng)求交易收據(jù),以此來(lái)確認(rèn)交易執(zhí)行的情況。從上一節(jié)的第3步驟可知,交易執(zhí)行收據(jù)已經(jīng)寫(xiě)入了db中,請(qǐng)求端要做的是發(fā)生獲取交易收據(jù)請(qǐng)求。
4.2 流程圖細(xì)節(jié)
-
ethapi/api.go識(shí)別到這是一次交易發(fā)送請(qǐng)求,調(diào)用GetTransactionReceipt()函數(shù),進(jìn)一步向底層發(fā)送消息。如果收據(jù)中的合約地址不是全0,則認(rèn)為是一次合約部署過(guò)程,填寫(xiě)交易中的合約地址到收據(jù)中供請(qǐng)求端使用。
4a. 調(diào)用ReadTransaction()函數(shù),獲取交易的基礎(chǔ)信息
4b. 調(diào)用GetReceipts()函數(shù),獲取交易收據(jù),其內(nèi)部會(huì)調(diào)用ReadHeadNumber()和ReadReceipts()函數(shù)輔助操作。
/*
file: ethapi/api.go
*/
func (s *PublicTransactionPoolAPI) GetTransactionReceipt(...) (...) {
tx, blockHash, blockNumber, index := rawdb.ReadTransaction(...)
...
receipts, err := s.b.GetReceipts(ctx, blockHash)
if err != nil {
return nil, err
}
...
if receipt.ContractAddress != (common.Address{}) {
fields["contractAddress"] = receipt.ContractAddress
}
return fields, nil
}
——————————THE END——————————
以太坊系列文章:
Ethereum 以太坊節(jié)點(diǎn)啟動(dòng)源碼分析 - FinJmy - 簡(jiǎn)書(shū)
Ethereum 以太坊智能合約部署源碼分析 - FinJmy - 簡(jiǎn)書(shū)
Ethereum 以太坊智能合約調(diào)用源碼分析 - FinJmy - 簡(jiǎn)書(shū)
本文文字和圖片均屬博主原創(chuàng)。未經(jīng)授權(quán),不得轉(zhuǎn)載。
個(gè)人網(wǎng)站:https://jmy5945hh.github.io/