以太坊源码解读(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这个对象, ...