三、交易执行
交易执行是以太坊中最为重要的部分。
在执行交易之前首先需要对交易进行初步校验:
- 交易是RLP格式的,无多余字符
- 交易的签名是有效的
- 交易的nonce是有效的(与发送者账户的nonce值一致)
- gasLimit的值不小于固有gas
- 账户余额至少够支付预付费用
当交易满足上述条件后,交易才会被执行。
// preCheck校验的后三条。
//交易校验的前两条是在其他地方执行的。对于矿工来说交易签名在加txpool的时候会检查,在commitTransactions的时候也会检查。
func (st *StateTransition) preCheck() error {
// Make sure this transaction's nonce is correct.
// 检查nonce值
if st.msg.CheckNonce() {
nonce := st.state.GetNonce(st.msg.From())
if nonce < st.msg.Nonce() {
return ErrNonceTooHigh
} else if nonce > st.msg.Nonce() {
return ErrNonceTooLow
}
}
return st.buyGas()
}
func (st *StateTransition) buyGas() error {
//gasLimit*gasPrice即为预付的费用v0
mgval := new(big.Int).Mul(new(big.Int).SetUint64(st.msg.Gas()), st.gasPrice)
if st.state.GetBalance(st.msg.From()).Cmp(mgval) < 0 {
return errInsufficientBalanceForGas
}
if err := st.gp.SubGas(st.msg.Gas()); err != nil {
return err
}
st.gas += st.msg.Gas()
//initialGas
st.initialGas = st.msg.Gas()
st.state.SubBalance(st.msg.From(), mgval)
return nil
}
交易的形式化表示
公式51,是对交易的形式化定义。交易的执行,相当于当前状态和交易,通过交易转变函数,到达新的状态。
3.1 子状态
在交易执行的整个过程中,以太坊保持跟踪“子状态”。子状态是纪录交易中生成信息的一种方式,当交易完成时会立即需要这些信息。
交易的子状态包含:
- 自毁集合(self-destruct set),用表示,指在交易完成之后需要被销毁的账户集合。
- 日志序列 (log series),用表示,指虚拟机代码执行的归档的和可检索的检查点。
- 账户集合(touched accounts),用表示,其中空的账户在交易结束时将被删除。
- 退款余额(refund balance),用,指在交易完成之后需要退还给发送账户的总额。
子状态的形式化表示
公式52,是交易子状态的形式化表示.
公式53,定义了空的子状态.
3.2 执行
- 对交易进行初步检查,从发送者账户中扣除预付的交易费。预付交易费值如公式57所示,为gasLimit*gasPrice + value。(代码中是gasLimit*gasPrice,Value是在Call的过程中判断和扣除的)
- 计算固有gas消耗,如公式54-56所示。并消耗掉该花费。
- 如果是创建合约,则走合约创建流程。消耗相应花费。
- 如果是合约执行,则走合约执行流程。消耗相应花费。
- 计算退款余额,将余额退还到发送者账户。
- 将交易的交易费加到矿工账户。
- 返回当前状态,以及交易的花费。
/*
The State Transitioning Model
A state transition is a change made when a transaction is applied to the current world state
The state transitioning model does all all the necessary work to work out a valid new state root.
1) Nonce handling
2) Pre pay gas
3) Create a new state object if the recipient is \0*32
4) Value transfer
== If contract creation ==
4a) Attempt to run transaction data
4b) If valid, use result as code for the new state object
== end ==
5) Run Script section
6) Derive new state root
*/
//gp 中一开始有gasLimit数量的gas
type StateTransition struct {
gp *GasPool
msg Message
gas uint64
gasPrice *big.Int
initialGas uint64
value *big.Int
data []byte
state vm.StateDB
evm *vm.EVM
}
// TransitionDb will transition the state by applying the current message and
// returning the result including the the used gas. It returns an error if it
// failed. An error indicates a consensus issue.
func (st *StateTransition) TransitionDb() (ret []byte, usedGas uint64, failed bool, err error) {
//交易检查,检查正确的话,st.gas为gasPrice*gasLimit,即预付的交易费。
if err = st.preCheck(); err != nil {
return
}
msg := st.msg
sender := vm.AccountRef(msg.From())
homestead := st.evm.ChainConfig().IsHomestead(st.evm.BlockNumber)
contractCreation := msg.To() == nil
// Pay intrinsic gas
// 固有gas,也就是g0
gas, err := IntrinsicGas(st.data, contractCreation, homestead)
if err != nil {
return nil, 0, false, err
}
if err = st.useGas(gas); err != nil {
return nil, 0, false, err
}
var (
evm = st.evm
// vm errors do not effect consensus and are therefor
// not assigned to err, except for insufficient balance
// error.
vmerr error
)
//创建合约
if contractCreation {
ret, _, st.gas, vmerr = evm.Create(sender, st.data, st.gas, st.value)
} else {
// Increment the nonce for the next transaction
//执行合约
st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1)
ret, st.gas, vmerr = evm.Call(sender, st.to(), st.data, st.gas, st.value)
}
if vmerr != nil {
log.Debug("VM returned with error", "err", vmerr)
// The only possible consensus-error would be if there wasn't
// sufficient balance to make the transfer happen. The first
// balance transfer may never fail.
if vmerr == vm.ErrInsufficientBalance {
return nil, 0, false, vmerr
}
}
//计算退款,并返回到发送者账户
st.refundGas()
//付交易费给矿工
st.state.AddBalance(st.evm.Coinbase, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.gasPrice))
return ret, st.gasUsed(), vmerr != nil, err
}
3.2.1 每一步的形式化表示
计算固有gas消耗的形式化表示
公式54-56为固有gas消耗的计算方式。
- 统计,,即交易的init和data字段,0和非0分开计算。为0的字节数TxDataZeroGas,非0字节数TxDataNonZeroGas,并求和。
- 如果是创建合约,则加上创建合约的固定消耗。
- 如果是执行合约,加上合约执行的固定消耗。
// IntrinsicGas computes the 'intrinsic gas' for a message with the given data.
// 计算g0
func IntrinsicGas(data []byte, contractCreation, homestead bool) (uint64, error) {
// Set the starting gas for the raw transaction
var gas uint64
//公式55和56
if contractCreation && homestead {
gas = params.TxGasContractCreation
} else {
gas = params.TxGas
}
//公式54,根据non-zero和zero data进行计算
// Bump the required gas by the amount of transactional data
if len(data) > 0 {
// Zero and non-zero bytes are priced differently
var nz uint64
for _, byt := range data {
if byt != 0 {
nz++
}
}
// Make sure we don't exceed uint64 for all data combinations
if (math.MaxUint64-gas)/params.TxDataNonZeroGas < nz {
return 0, vm.ErrOutOfGas
}
gas += nz * params.TxDataNonZeroGas
z := uint64(len(data)) - nz
if (math.MaxUint64-gas)/params.TxDataZeroGas < z {
return 0, vm.ErrOutOfGas
}
gas += z * params.TxDataZeroGas
}
return gas, nil
}
计算预付交易费的形式化表示
公式57表示预付费用的计算。表示预付费用为gasLimit*gasPrice+value。实际代码中如上文buyGas.
交易初步校验的形式化表示
公式58为交易的初步验证的形式化表示。第一和第二行表示发送者账户不为空,且存在。第三行表示交易的nonce值为发送者的当前nonce值。第四行表示固定消耗小于等于gasLimit(否则连固定消耗都不够)。第五行表示当前账户余额必须足够支付预付费用。第六行表示当前区块的gasLimit-已经消耗掉的gas值大于等于交易的gasLimit。
交易初始状态(虚拟机执行之前)的形式化表示
公式59-61表示交易执行时的初始状态,该处也是一个检查点。用于后续操作中出错时的回滚。
交易开始执行时,会首先将发送者账户的balance减去预付gas(gasLimit*gasPrice),并将该账户的nonce加一。其他与原状态相同。
虚拟机执行的形式化表示
公式62表示
- 如果交易的to为空,则是合约创建交易,状态转变函数为。
- 否则为合约执行,状态转变函数为。
- 两个转变的共同参数有:初始状态,发送者账户地址S(T),可用gas值g,gasPrice,交易value,原始调用者,0,和.其中可用gas值g为gasLimit-,如公式63所示。原始调用者,根据交易是创建合约还是执行合约会有所不同,不由交易控制,由虚拟机来控制。
- 接收账户
经过虚拟机执行后状态从转变为,剩余的gas为,交易子状态为A,交易的状态码为z。
计算退款的形式化表示
公式64表示交易执行之后的子状态的退款额A'为原先子状态退款额与此次需要销毁的自毁集合返回的退款的和。
公式65时交易执行后的退款额的计算,退款额为交易执行后剩余gas值g'加上,子状态退款额A'和用掉的gas(-g')的一半中较小的那个。
func (st *StateTransition) refundGas() {
// Apply refund counter, capped to half of the used gas.
// 用掉gas的一半,与子状态退款额比较
refund := st.gasUsed() / 2
if refund > st.state.GetRefund() {
refund = st.state.GetRefund()
}
//剩余的总gas值
st.gas += refund
// Return ETH for remaining gas, exchanged at the original rate.
remaining := new(big.Int).Mul(new(big.Int).SetUint64(st.gas), st.gasPrice)
st.state.AddBalance(st.msg.From(), remaining)
// Also return remaining gas to the block gas counter so it is
// available for the next transaction.
st.gp.AddGas(st.gas)
}
交易之后状态的形式化表示
公式66-69定义了交易之后的预结束状态(之所以说是预结束状态是这个时候一些需要销毁的状态尚未销毁),其和虚拟机执行后的状态的区别为:
- 将退款额返回给交易发送者账户。公式67。
- 支付交易费给矿工。公式68。其中m为矿工的地址。公式69。
公式70-72定义了,交易的结束状态。结束状态与预结束状态的区别为,将自毁集合中的账户置为空,公式71,将可控的账户集合中的死掉的账户,置为空。公式72.
公式73-75,定义了交易的其他几个相关字段。其中公式73,定义了交易的花费为,gasLimit-退款。公式74定义了交易的日志序列即为交易子状态中的日志序列。公式75定义了交易的最终状态即为虚拟机执行后的状态码。
四、合约创建
第三部分于合约创建和合约执行的具体过程未详细介绍。该部分对合约创建的过程进行详细的解释。
以太坊中有两类账户,一类为外部拥有账户,即通常意义上的用户账户。一类为合约账户。当一个交易是合约创建,是指该交易的目的是创建一个新的合约账户。合约账户创建的过程如下:
- 根据规则生成合约账户地址。公式77.
- 设置合约账户nonce值为1,其balance设为捐献值,storageRoot设为空,codeHash为空的Hash。公式78-79.
- 当前账户余额中减去捐献值。公式80-81.
- 运行合约,进行合约的初始化工作。如果运行过程中gas不足,则所有状态回滚,消耗掉所有gas。也就是被创建的合约账户也会被回滚掉,捐献值回滚到原账户。
- 如果合约初始化运行成功,则计算存储code的花费,若成功,则设置账户的code。
- 如果不足以支付存储费用,回滚状态。(这个地方根据配置不同,homestead 和 byzantium会有所不同)
// Create creates a new contract using code as deployment code.
func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *big.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) {
// Depth check execution. Fail if we're trying to execute above the
// limit.
if evm.depth > int(params.CallCreateDepth) {
return nil, common.Address{}, gas, ErrDepth
}
if !evm.CanTransfer(evm.StateDB, caller.Address(), value) {
return nil, common.Address{}, gas, ErrInsufficientBalance
}
// Ensure there's no existing contract already at the designated address
nonce := evm.StateDB.GetNonce(caller.Address())
evm.StateDB.SetNonce(caller.Address(), nonce+1)
//生成合约账户地址
contractAddr = crypto.CreateAddress(caller.Address(), nonce)
//若之前该合约账户对应的地址不空,则返回地址冲突的错误
contractHash := evm.StateDB.GetCodeHash(contractAddr)
if evm.StateDB.GetNonce(contractAddr) != 0 || (contractHash != (common.Hash{}) && contractHash != emptyCodeHash) {
return nil, common.Address{}, 0, ErrContractAddressCollision
}
// Create a new account on the state
snapshot := evm.StateDB.Snapshot()
//创建合约账户
evm.StateDB.CreateAccount(contractAddr)
if evm.ChainConfig().IsEIP158(evm.BlockNumber) {
evm.StateDB.SetNonce(contractAddr, 1)
}
//将捐献值转移给合约账户
evm.Transfer(evm.StateDB, caller.Address(), contractAddr, value)
// initialise a new contract and set the code that is to be used by the
// EVM. The contract is a scoped environment for this execution context
// only.
contract := NewContract(caller, AccountRef(contractAddr), value, gas)
contract.SetCallCode(&contractAddr, crypto.Keccak256Hash(code), code)
if evm.vmConfig.NoRecursion && evm.depth > 0 {
return nil, contractAddr, gas, nil
}
if evm.vmConfig.Debug && evm.depth == 0 {
evm.vmConfig.Tracer.CaptureStart(caller.Address(), contractAddr, true, code, gas, value)
}
start := time.Now()
//运行合约,进行合约的初始化,错误交由最后处理
ret, err = run(evm, contract, nil)
// check whether the max code size has been exceeded
maxCodeSizeExceeded := evm.ChainConfig().IsEIP158(evm.BlockNumber) && len(ret) > params.MaxCodeSize
// if the contract creation ran successfully and no errors were returned
// calculate the gas required to store the code. If the code could not
// be stored due to not enough gas set an error and let it be handled
// by the error checking condition below.
//如果合约创建成功,无错误返回,则计算合约存储代码的花费。成功的话,设置合约账户的code。如果存储时gas不足,err交由下面处理。
if err == nil && !maxCodeSizeExceeded {
createDataGas := uint64(len(ret)) * params.CreateDataGas
if contract.UseGas(createDataGas) {
evm.StateDB.SetCode(contractAddr, ret)
} else {
err = ErrCodeStoreOutOfGas
}
}
// When an error was returned by the EVM or when setting the creation code
// above we revert to the snapshot and consume any gas remaining. Additionally
// when we're in homestead this also counts for code storage gas errors.
// 如果不是ErrCodeStoreOutOfGas的话,revert当前状态,消耗gas。说明ErrCodeStoreOutOfGas账户会创建成功。
if maxCodeSizeExceeded || (err != nil && (evm.ChainConfig().IsHomestead(evm.BlockNumber) || err != ErrCodeStoreOutOfGas)) {
evm.StateDB.RevertToSnapshot(snapshot)
if err != errExecutionReverted {
contract.UseGas(contract.Gas)
}
}
// Assign err if contract code size exceeds the max while the err is still empty.
if maxCodeSizeExceeded && err == nil {
err = errMaxCodeSizeExceeded
}
if evm.vmConfig.Debug && evm.depth == 0 {
evm.vmConfig.Tracer.CaptureEnd(ret, gas-contract.Gas, time.Since(start), err)
}
return ret, contractAddr, contract.Gas, err
}
4.1 形式化表示
合约创建流程的形式化表示
公式76表示合约创建的形式化表示。
合约创建需要的参数有:系统状态,发送者(s),原始调用者(o),可用gas值(g),gas价格(p),捐献值(v),虚拟机的初始化代码其实际为一段任意长度的字节数组(i),当前虚拟机调用的栈深度(e),以及权限控制列表(w)。
虚拟机执行合约创建的结果为新的中间过程状态集合,剩余的gas值g',交易子状态A,交易状态码z,合约的body code .
新建合约账户地址的形式化表示
公式77给出了合约账户的地址的计算方法,可以看出是跟发送者账户地址以及发送者的nonce值有关。
// CreateAddress creates an ethereum address given the bytes and the nonce
func CreateAddress(b common.Address, nonce uint64) common.Address {
data, _ := rlp.EncodeToBytes([]interface{}{b, nonce})
return common.BytesToAddress(Keccak256(data)[12:])
}
合约账户初始化的形式化表示
生成了合约账户的地址后,需要对账户进行相应的初始化。公式78-82给出了创建合约之后的相关状态。
公式79表示新建的合约账户,其nonce值为1,balance值为捐献值+原有值(如果原合约账户不为空,公式82),storage为空,codeHash为空的hash。
公式80-81表示如果调用账户若为空,则仍为空。若不为空,则相应的balance值减去捐献值v。
虚拟机执行的形式化表示
合约账户地址生成且初始化后,需要虚拟机执行合约的相应初始化代码。公式83给出了虚拟机执行的过程。
公式83表示,虚拟机在基础上执行,可用gas值为g,运行环境参数为I,s为合约创建的调用者地址,a为新生成的合约账户地址。 虚拟机执行之后生成新的临时状态,剩余的gas值,子状态A,以及状态码z。
其中I中的项如公式84-92所示。
- 为新生成的合约账户地址,即a;
- 为原始调用者,即o;
- 为gas价格,即p;
- 为虚拟机调用的input data,因为是合约创建,所以data段为空;
- 为发送者,或者说调用者,即s;
- 为捐献值,即v;
- 为初始化代码段,即i;
- 为当前调用栈深度,即e;
- 为权限管理列表,即w。
虚拟机执行过程中如果出现了gas值不足的情况,则会回滚所有的状态。状态回到调用合约创建开始时的状态,也就是调用过程中消耗掉了所有的gas值,但是合约账户被混滚掉,变成没有被创建的状态。如果有捐献值,捐献值回滚至原账户。
如果虚拟机执行初始化合约的操作成功,则需要存储相应的合约代码到新创建的合约账户。公式93时计算存储合约代码所需的花费,其值与合约的代码长度有关。
虚拟机执行之后根据执行的情况
- gas值,如果没有出错。当前的临时gas值为。即在虚拟机执行完成后,再消耗掉存储code的gas。如果出错,则剩余gas值为0.公式94.
- 状态集合,如果出错,则回滚到虚拟机执行前的状态。若成功,则状态转变为临时状态集,然后进行一些后续处理,如果合约账户是个死账户,则将该账户置为空,否则对合约账户的code进行设置。
- 状态码,如果临时状态集为空(虚拟机运行初始化的时候就出错了),或者gas不足以支付存储code的费用,则状态码为0.没有出错则为1.
- 出错的情况有,1. 生成的临时状态集为空且code为空 2. gas不足以支付存储code的费用 3. code长度>24576。
4.2 特别提示
当初始化代码交由虚拟机执行的时候,新创建的合约账户地址是存在的,只不过是没有body code。因此任何调用该合约的代码都会因为没有可执行的代码而返回错误。如果初始化代码中是以自毁操作为结束的,那么合约账户在交易完成之前就会被删除掉,目前该问题还有争议。而对于正常的STOP代码,或者是初始化执行返回的代码为空,虚拟机执行完成后,判定到该合约账户的code也还是空,那么这个合约账户就会变成一个僵尸账户,而其余额也会被永久的冻结在里面。
五、合约调用
合约调用的流程如下:
- 如果to账户地址不存在,则新建.
- 从sender中转账value值到to账户.
- 从合约账户中获取合约代码,进行设置,供虚拟机执行.
- 虚拟机执行合约代码。
- 如果合约执行出错,则回滚到合约执行之前的状态。
// Call executes the contract associated with the addr with the given input as
// parameters. It also handles any necessary value transfer required and takes
// the necessary steps to create accounts and reverses the state in case of an
// execution error or failed value transfer.
func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) {
if evm.vmConfig.NoRecursion && evm.depth > 0 {
return nil, gas, nil
}
// Fail if we're trying to execute above the call depth limit
if evm.depth > int(params.CallCreateDepth) {
return nil, gas, ErrDepth
}
// Fail if we're trying to transfer more than the available balance
if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) {
return nil, gas, ErrInsufficientBalance
}
var (
to = AccountRef(addr)
snapshot = evm.StateDB.Snapshot()
)
//to账户不存在,则新建
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 {
// Calling a non existing account, don't do antything, but ping the tracer
if evm.vmConfig.Debug && evm.depth == 0 {
evm.vmConfig.Tracer.CaptureStart(caller.Address(), addr, false, input, gas, value)
evm.vmConfig.Tracer.CaptureEnd(ret, 0, 0, nil)
}
return nil, gas, nil
}
evm.StateDB.CreateAccount(addr)
}
//转账
evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value)
// Initialise a new contract and set the code that is to be used by the EVM.
// The contract is a scoped environment for this execution context only.
//设置要执行的代码
contract := NewContract(caller, to, value, gas)
contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))
start := time.Now()
// Capture the tracer start/end events in debug mode
if evm.vmConfig.Debug && evm.depth == 0 {
evm.vmConfig.Tracer.CaptureStart(caller.Address(), addr, false, input, gas, value)
defer func() { // Lazy evaluation of the parameters
evm.vmConfig.Tracer.CaptureEnd(ret, gas-contract.Gas, time.Since(start), err)
}()
}
//执行代码
ret, err = run(evm, contract, input)
// When an error was returned by the EVM or when setting the creation code
// above we revert to the snapshot and consume any gas remaining. Additionally
// when we're in homestead this also counts for code storage gas errors.
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
if err != errExecutionReverted {
contract.UseGas(contract.Gas)
}
}
return ret, contract.Gas, err
}
5.1 合约调用的形式化表示
公式98为合约调用的形式化表示。
合约调用需要的参数有:系统状态,发送者(s),原始调用者(o),收款人(r),合约账户地址(c),可用gas值(g),gas价格(p),转账额(v),捐献值(),虚拟机的执行代码的input data其实际为一段任意长度的字节数组(d),当前虚拟机调用的栈深度(e),以及权限控制列表(w)。
虚拟机执行合约调用的结果为新的中间过程状态集合,剩余的gas值g',交易子状态A,交易状态码z,合约的调用结果.
合约调用前的状态
公式99-105为虚拟机执行合约代码之前的一些临时状态。
在虚拟机执行合约代码之前,首先进行转账。(除非发送者和接收者相同)公式99.
由于调用者有可能是未定义的。所以更严谨的定义如公式100-105.
- 调用者如果之前为空且value为0,则调用者依然为空。否则,调用者的balance减去转账值。公式100-102.
- 如果接收者账户不存在,且转账值不为0,则新建接收者,将其nonce值0,余额为转账额v,storageRoot为空的TRIE,codeHash为空的hash。
- 如果接收者账户不存在,且转账值为0.则不处理。
- 如果接收者账户存在,则其余额为原先的余额加上转账值。
虚拟机执行合约调用的形式化表示
虚拟机执行的参数为,公式109-118:
- 为接收者账户,即r,(在合约创建的时候,该处为新创建的合约账户);
- 为原始调用者,即o;
- 为gasPric,即p;
- 为输入数据(input data),即d;
- 为调用者账户,即s;
- 为捐献值(注意这里并不是转账值),即;
- 当前调用栈深度,即e;
- 权限管理,即w;
- 可控的账户(touched accounts)为调用者账户和接收者账户;
虚拟机执行的合约为公式119-120,其中公式119的前8种为预编译好的合约,主要完成一些基本的加密和运算等操作,最后一种即为调用用户的合约。
合约执行的结果如公式106-118所示。
- 如果合约执行失败,则状态回滚到之前的状态。如果执行成功,则状态转变为执行后状态。公式106.
- 如果合约执行失败,则消耗掉所有的gas,gas剩余值为0.成功,则消耗掉执行过程中的gas。公式107.
- 如果合约执行失败,则返回0,成功返回1.