以太坊源码解读(19)EVM 创建智能合约

我们要知道,evm解释器的执行上下文是stateTransition,是交易,但evm的服务对象是智能合约。智能合约与evm的解释器有紧密的联系,所以我们这一节先从智能合约的创建和执行开始学习。

合约创建函数的调用时机,一是Worker执行交易的过程,交易如果是合约创建,则会在EVM执行交易时生成智能合约地址并部署智能合约;二是通过opCreate指令,这个指令使得在智能合约内部创建新的智能合约成为可能。

1、交易执行前的检查 2、确保当前要创建的地址在世界状态中没有合约存在,如果存在,直接返回; 3、创建一个新账户,设置新账户为nonce为1; 4、进行转账 5、创建一个待执行的合约对象,并执行; 6、处理返回值

我们首先看一看创建一个合约所需要的参数:

func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *big.Int) (ret []byte, 
contractAddr common.Address, leftOverGas uint64, err error) {
	contractAddr = crypto.CreateAddress(caller.Address(), evm.StateDB.GetNonce(caller.Address()))
	return evm.create(caller, &codeAndHash{code: code}, gas, value, contractAddr)
}

caller:转出方地址; code:代码(input) gas:当前交易的剩余gas; value:转账额度;

Create()方法首先对发送者地址(caller.Address)和账户的nonce进行keccak256计算得到合约地址,然后将合约地址传入create()方法,后者是合约创建的真正函数。

首先,在执行交易之前需要进行检查:1、深度判断;2、余额是否足够;

// 如果递归深度大于1024,直接退出
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
}

然后,给交易发送者的账户nonce加1(普通转账时,是在外面加1的,即在TransitionDb中),然后判断当前要创建的地址在是世界状态中没有合约存在,如果存在直接返回。

nonce := evm.StateDB.GetNonce(caller.Address())
evm.StateDB.SetNonce(caller.Address(), nonce+1)

// 传入的合约地址nonce不为0,或contractHash不为空
contractHash := evm.StateDB.GetCodeHash(address)
if evm.StateDB.GetNonce(address) != 0 || (contractHash != (common.Hash{}) && contractHash != emptyCodeHash) {
	return nil, common.Address{}, 0, ErrContractAddressCollision
}

第三步,如果上面两个检查都没有问题,那么我们就可以创建新的合约账户了。先用合约地址创建一个合约账户,然后给合约账户设置nonce为1。

// 先对当前StateDB进行快照
snapshot := evm.StateDB.Snapshot()
// 创建新合约并将合约的nonce设置为1
evm.StateDB.CreateAccount(address)
if evm.ChainConfig().IsEIP158(evm.BlockNumber) {
    evm.StateDB.SetNonce(address, 1)
}

下图比较直观的看到我们的合约储存在什么地方以及账户的结构:

第四步是进行转账,将我们构建合约交易时的value转入智能合约账户。转账的过程很简单,就是sender的账户减减(--),合约账户加加(++)。

evm.Transfer(evm.StateDB, caller.Address(), address, value)

第五步是创建合约对象,并执行。

contract := NewContract(caller, AccountRef(address), value, gas)
contract.SetCodeOptionalHash(&address, codeAndHash)

// 如果vm配置规定不允许递归,但递归深度大于0,则直接退出
if evm.vmConfig.NoRecursion && evm.depth > 0 {
    return nil, address, gas, nil
}

if evm.vmConfig.Debug && evm.depth == 0 {
    evm.vmConfig.Tracer.CaptureStart(caller.Address(), address, true, codeAndHash.code, gas, value)
}

start := time.Now()

使用caller地址 、合约地址、转账额和交易余额传入NewContract()方法。然后执行contract.SetCodeOptionalHash(),将合约代码code设置到合约中:

func (c *Contract) SetCodeOptionalHash(addr *common.Address, codeAndHash *codeAndHash) {
	c.Code = codeAndHash.code
	c.CodeHash = codeAndHash.hash
	c.CodeAddr = addr
}

执行合约初始化代码

ret, err := run(evm, contract, nil, false)

run()函数将contract交给了evm解释器,返回interpreter.Run(contract, input, readOnly)的执行结果。至于interpreter如何执行合约,我们后面再分析。

func run(evm *EVM, contract *Contract, input []byte, readOnly bool) ([]byte, error) {
	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)
		}
	}
	for _, interpreter := range evm.interpreters {
		if interpreter.CanRun(contract.Code) {
			if evm.interpreter != interpreter {
				// Ensure that the interpreter pointer is set back
				// to its current value upon return.
				defer func(i Interpreter) {
					evm.interpreter = i
				}(evm.interpreter)
				evm.interpreter = interpreter
			}
			return interpreter.Run(contract, input, readOnly)
		}
	}
	return nil, ErrNoCompatibleInterpreter
}

第六步,处理返回值

run函数的两个返回值分别是ret(合约代码)和err,我们需要判断合约代码的长度以及err是否为nil:

// 约定合约代码最大长度为24576
maxCodeSizeExceeded := evm.ChainConfig().IsEIP158(evm.BlockNumber) && len(ret) > params.MaxCodeSize
// 如果执行没有错且代码长度不超过24576
if err == nil && !maxCodeSizeExceeded {
    // 计算本次合约创建消耗的gas,每字节200gas
    createDataGas := uint64(len(ret)) * params.CreateDataGas
    // 如果交易gas余额足够,则成功部署合约,将合约代码设置到账户储存中
    if contract.UseGas(createDataGas) {
        evm.StateDB.SetCode(address, ret)
    // 否则返回余额不足
    } else {
        err = ErrCodeStoreOutOfGas
    }
}

// 如果代码长度受限或执行错误
if maxCodeSizeExceeded || (err != nil && (evm.ChainConfig().IsHomestead(evm.BlockNumber) || err != ErrCodeStoreOutOfGas)) {
    // 则恢复之前的快照
    evm.StateDB.RevertToSnapshot(snapshot)
    // 如果不是revert指令导致的错误,要扣除所有的gas
    if err != errExecutionReverted {
        contract.UseGas(contract.Gas)
    }
}
// 如果只是代码过长,则返回相应的错误
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)
}
// 最后返回合约代码、合约地址、gas余额和错误
return ret, address, contract.Gas, err

值得注意的是,如果代码执行错误是revert错误,则不会收取gas,否则gas会被扣除。那么这个revert是什么?revert是evm中的一条指令,在我们智能函数编程语言中(solidity)中有require和revert这两个判断。如果require和revert判断错误,那么就会返回一个revert指令错误,此时就不会收取gas。这也就是为什么solidity中require和revert执行不会扣除gas的原因。当然,这个方法是在拜占庭分叉后出现的。

最后,一张图看看智能合约创建的过程:

下一章:以太坊源码解读(20)EVM 转账或调用合约

在了解合约调用之前,我们需要知道调用合约的本质是什么。在我们创建合约的时候,由run函数初始化的智能合约code(ret)储存在stateDB中。也就是说在内存中并没有Contract这个对象, ...