【区块链】以太坊源码学习 -- EVM

以太坊源码学习 – EVM


学习文档链接:here

一、虚拟机外

主要功能:

执行前将Transaction类型转化成Message,创建虚拟机(EVM)对象,计算一些Gas消耗,以及执行交易完毕后创建收据(Receipt)对象并返回

1.1 入口 和 返回值

文件:/core/state_processor.go  --- Process()

for i, tx := range block.Transactions() {
    statedb.Prepare(tx.Hash(), block.Hash(), i)
    receipt, _, err := ApplyTransaction(p.config, p.bc, nil, gp, statedb, header, tx, totalUsedGas, cfg)
    if err != nil {
        return nil, nil, nil, err
    }
    receipts = append(receipts, receipt)
    allLogs = append(allLogs, receipt.Logs...)
}

//将block里面所有的tx逐个遍历执行,ApplyTransaction, 每次执行完返回一个收据(Receipt)对象

我们来看下Receipt结构体:

type Receipt struct {
    // Consensus fields
    PostState         []byte   `json:"root"`
    Failed            bool     `json:"failed"`
    CumulativeGasUsed *big.Int `json:"cumulativeGasUsed" gencodec:"required"`
    Bloom             Bloom    `json:"logsBloom"         gencodec:"required"`
    Logs              []*Log   `json:"logs"              gencodec:"required"`

    // Implementation fields (don't reorder!)
    TxHash          common.Hash    `json:"transactionHash" gencodec:"required"`
    ContractAddress common.Address `json:"contractAddress"`
    GasUsed         *big.Int       `json:"gasUsed" gencodec:"required"`
}

解释:

Logs:  Log类型的数组,其中每一个Log对象记录了Tx中一小步的操作。所以,每一个tx的执行结果,由一个Receipt对象来表示;更详细的内容,由一组Log对象来记录。这个Log数组很重要,比如在不同Ethereum节点(Node)的相互同步过程中,待同步区块的Log数组有助于验证同步中收到的block是否正确和完整,所以会被单独同步(传输)。

PostState:  保存了创建该Receipt对象时,整个Block内所有“帐户”的当时状态。Ethereum 里用stateObject来表示一个账户Account,这个账户可转帐(transfer value), 可执行tx, 它的唯一标示符是一个Address类型变量。 这个Receipt.PostState 就是当时所在Block里所有stateObject对象的RLP Hash值。

Bloom: Ethereum内部实现的一个256bit长Bloom Filter。 Bloom Filter概念定义可见wikipedia,它可用来快速验证一个新收到的对象是否处于一个已知的大量对象集合之中。这里Receipt的Bloom,被用以验证某个给定的Log是否处于Receipt已有的Log数组中。

1.2 封装EVM对象和Message对象

我们来看一下ApplyTransaction():

文件:/core/state_processor.go  --- ApplyTransaction()

//=====Message对象=====
msg, err := tx.AsMessage(types.MakeSigner(config, header.Number))
if err != nil { return nil, nil, err }

//=====EVM对象=====
context := NewEVMContext(msg, header, bc, author)
vmenv := vm.NewEVM(context, statedb, config, cfg)

//完成tx的执行
_, gas, failed, err := ApplyMessage(vmenv, msg, gp)

//创建一个收据Receipt对象,最后返回该Recetip对象,以及整个tx执行过程所消耗Gas数量。
... 

我们来看一下ApplyMessage()

文件:/core/state_transition.go  --- ApplyMessage()

//发现调用了TransitionDb()
, _, gasUsed, failed, err := st.TransitionDb()

我们来看一下TransitionDb()

文件:/core/state_transition.go  --- TransitionDb()

//购买gas
//计算tx固有gas
//EVM执行
//计算本次执行交易的实际gas消耗
//偿退gas
//奖励所属区块的挖掘者

二、 虚拟机内

包括执行转帐,和创建合约并执行合约的指令数组

2.1 EVM结构体

我们来看一下EVM的结构体:

文件:/core/vm/evm.go

type EVM struct {

    Context  --携带辅助信息:Transaction的信息(GasPrice, GasLimit),Block的信息(Number, Difficulty),以及转帐函数等
    StateDB StateDB --为EVM提供statedb的相关操作
    depth int
    chainConfig *params.ChainConfig
    chainRules params.Rules
    vmConfig Config
    interpreter *Interpreter --解释器,用来解释执行EVM中合约的指令
    abort int32
}

2.2 完成转账

交易的转帐操作由Context对象中的TransferFunc类型函数来实现,类似的函数类型,还有CanTransferFunc, 和GetHashFunc。
文件:/core/evm.go --Transfer()

db.SubBalance(sender, amount)  //转出账户减到一定金额以太币
db.AddBalance(recipient, amount) //转入账户增加一定金额以太币

//注意:转出和转入账户的操作不会立即生效,StateDB 并不是真正的数据库,只是一行为类似数据库的结构体它在内部以Trie的数据结构来管理各个基于地址的账户,可以理解成一个cache;当该账户的信息有变化时,变化先存储在Trie中。仅当整个Block要被插入到BlockChain时,StateDB 里缓存的所有账户的所有改动,才会被真正的提交到底层数据库。

2.3 合约的创建、赋值

我们先来看一下contract 结构体

文件:/core/vm/contract.go  

type Contract struct {
    CallerAddress common.Address
    caller        ContractRef  //转账转出方地址
    self          ContractRef  //转入方地址

    jumpdests destinations // result of JUMPDEST analysis.

    Code     []byte  //指令数组,其中每一个byte都对应于一个预定义的虚拟机指令
    CodeHash common.Hash
    CodeAddr *common.Address
    Input    []byte  //数据数组,是指令所操作的数据集合

    Gas   uint64
    value *big.Int
    Args []byte
    DelegateCall bool
}
创建合约: call(),create() -- 二者均在StateProcessor的ApplyTransaction()被调用以执行单个交易,并且都有调用转帐函数完成转帐。

我们来看一下call()

文件:/core/vm/call.go  

var (
    to = AccountRef(addr)
    snapshot = evm.StateDB.Snapshot()
)
if !evm.StateDB.Exist(addr) {
    precompiles := PrecompiledContractsHomestead
    if evm.ChainConfig().IsByzantium(evm.BlockNumber) {
        precompiles = PrecompiledContractsByzantium
    }
    if precompiles[addr] == nil && evm.ChainConfig().IsEIP158(evm.BlockNumber) && value.Sign() == 0 {
        return nil, gas, nil
    }
    evm.StateDB.CreateAccount(addr)
}

//转账
evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value)

//赋值Contract对象
contract := NewContract(caller, to, value, gas)
contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))

//调用run,执行该合约的指令
ret, err = run(evm, snapshot, contract, input)

if err != nil {
    evm.StateDB.RevertToSnapshot(snapshot)
    if err != errExecutionReverted {
        contract.UseGas(contract.Gas)
    }
}
return ret, contract.Gas, err

2.4 预编译合约

我们来看一下run():

文件:/core/vm/run.go  

if contract.CodeAddr != nil {
    precompiles := PrecompiledContractsHomestead
    if evm.ChainConfig().IsByzantium(evm.BlockNumber) {
        precompiles = PrecompiledContractsByzantium
    }
    if p := precompiles[*contract.CodeAddr]; p != nil {
        return RunPrecompiledContract(p, input, contract)
    }
}
return evm.interpreter.Run(snapshot, contract, input)
可见如果待执行的Contract对象恰好属于一组预编译的合约集合-此时以指令地址CodeAddr为匹配项-那么它可以直接运行;没有经过预编译的Contract,才会由Interpreter解释执行。这里的"预编译",可理解为不需要编译(解释)指令(Code)。预编译的合约,其逻辑全部固定且已知,所以执行中不再需要Code,仅需Input即可。

在代码实现中,预编译合约只需实现两个方法Required()和Run()即可,这两方法仅需一个入参input。

2.5 解释器执行合约的指令

我们来看一下interpreter.go

可以看到一个Config结构体

文件:/core/vm/.interpreter.go

type Config struct {
    Debug bool
    EnableJit bool
    ForceJit bool
    Tracer Tracer
    NoRecursion bool
    DisableGasMetering bool
    EnablePreimageRecording bool
    JumpTable [256]operation  //
}
operation: 每个operation对象正对应一个已定义的虚拟机指令,它所含有的四个函数变量execute, gasCost, validateStack, memorySize 提供了这个虚拟机指令所代表的所有操作。每个指令长度1byte,Contract对象的成员变量Code类型为[]byte,就是这些虚拟机指令的任意集合。operation对象的函数操作,主要会用到Stack,Memory, IntPool 这几个自定义的数据结构。

然后我们看一下interpreter.run()

文件: 文件:/core/vm/.interpreter.go --run()

核心: 逐个byte遍历入参Contract对象的Code变量,将其解释为一个已知的operation,然后依次调用该operation对象的四个函数

operation在操作过程中,会需要几个数据结构: Stack,实现了标准容器 -栈的行为;Memory,一个字节数组,可表示线性排列的任意数据;还有一个intPool,提供对big.Int数据的存储和读取。

需要特别注意的是LOGn指令操作,它用来创建n个Log对象,这里n最大是4。还记得Log在何时被用到么?每个交易(Transaction,tx)执行完成后,会创建一个Receipt对象用来记录这个交易的执行结果。Receipt携带一个Log数组,用来记录tx操作过程中的所有变动细节,而这些Log,正是通过合适的LOGn指令-即合约指令数组(Contract.Code)中的单个byte,在其对应的operation里被创建出来的。每个新创建的Log对象被缓存在StateDB中的相对应的stateObject里,待需要时从StateDB中读取。

你可能感兴趣的:(区块链)