以太坊源码解读(20)EVM 转账或调用合约
在了解合约调用之前,我们需要知道调用合约的本质是什么。在我们创建合约的时候,由run函数初始化的智能合约code(ret)储存在stateDB中。也就是说在内存中并没有Contract这个对象,而只是存在智能合约code。那我们如何调用合约呢?本质上,调用合约实际上是从合约账户中取出合约代码,然后NewContract()创建出一个临时的contract对象(如下图),然后执行contract的SetCallCode()或其他方法,确定智能合约的执行环境,然后执行run()函数,返回执行后的代码。
知道了这个过程,我们再来看看下面这个函数,智能合约的调用或普通交易——Call()。
Call()的主要功能是执行一笔交易,具体的步骤如下:
1、交易执行前的检查:深度判断和余额状况;
2、如果世界状态中不存在这个账号,则创建这个账号;
3、进行转账;
4、创建一个待执行的合约对象,并执行;
5、处理交易执行返回值
一、Call()的参数
func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int)
caller:转出方地址;
addr:转入方地址,如果是调用智能合约,那就是智能合约的地址;
input:调用函数的参数
gas:当前交易的剩余gas;
value:转账额度;
二、Call的执行步骤
首先,执行前检查。
// 如果不允许递归深度大于0,直接退出 if evm.vmConfig.NoRecursion && evm.depth > 0 { return nil, gas, nil } // 如果递归深度大于1024,直接退出 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) { 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 anything, 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)
第四步创建一个待执行的合约对象,并执行。
contract := NewContract(caller, to, value, gas) contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr)) // Even if the account has no code, we need to continue because it might be a precompile 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, false)
这里create()不同的是构建新contract对象的时候,create()调用了SetCodeOptionalHash(&address, codeAndHash),而这里我们调用的是SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))。create()中构建的contract对象中的Code是来自原始transaction中的Payload,而在Call()构建的contract对象中的Code则是create()中初始化智能合约即执行run()之后返回的ret,这两者在结构上是有区别的。
当我们写完智能合约通过solidity编译器生成合约代码时,最前面是一段合约部署代码,用来引导合约的部署,transaction中的Payload就包含这一段,它可以引导执行合约构造函数。当我们部署完成后,在智能合约地址中储存的Code却不包含合约部署代码部分,而是只有后面的部分,即ret。所以当我们调用智能合约的时候,构建的contract的code只有ret,直接从合约函数引导的代码开始执行。
三、智能合约的连环调用
我们说过智能合约EVM的递归调用深度为1024,也就是指通过一个合约调用另一个合约,像这样的调用可以递归1024次。
为什么说是“递归”?因为从一个智能合约调用另一个智能合约,比如通过Call()方法,都要重新构建contract实例,然后执行run()。而run()的执行是通过EVMinterpreter.Run()进行的。而在EVMInterpreter结构体中又传入了*EVM的地址,然后执行了evm.depth++。所以实际上每一次调用都是在同一个EVM内进行的。
连环调用的方法有下面几种:1、Call();2、CallCode();3、DelegateCall()。
Call()
to = AccountRef(addr) contract := NewContract(caller, to, value, gas) // 假设有外部账户A,合约账户B和合约账户C A Call B ——> ContractB CallerAddress: A Caller: A self: B B Call C ——> ContractC CallerAddress: B Caller: B self: C
CallCode()
to = AccountRef(caller.Address()) contract := NewContract(caller, to, value, gas) // 假设有外部账户A,合约账户B和合约账户C A Call B ——> ContractB CallerAddress: A Caller: A self: B B Callcode C ——> ContractC CallerAddress: B Caller: B self: B
delegateCall()
to = AccountRef(caller.Address()) contract := NewContract(caller, to, nil, gas).AsDelegate() func (c *Contract) AsDelegate() *Contract { parent := c.caller.(*Contract) c.CallerAddress = parent.CallerAddress c.value = parent.value return c } // 假设有外部账户A,合约账户B和合约账户C A Call B ——> ContractB CallerAddress: A Caller: A self: B B DelegateCall C ——> ContractC CallerAddress: A Caller: B self: B
从代码上看,这三者的主要区别就是这一点,主要就是在contract结构中的callerAddress、caller和self这三个的值不同。
如果外部账户A的某个操作通过Call方法调用B合约,而B合约又通过Call方法调用了C合约,那么最后实际上修改的是合约C账户的值;
如果外部账户A的某个操作通过Call方法调用B合约,而B合约通过CallCode方法调用了C合约,那么B只是调用了C中的函数代码,而最终改变的还是合约B账户的值。
DelegateCall其实跟CallCode方法的目的类似,都是只调用指定地址(合约C)的代码,而操作B的值。只不过它明确了CallerAddress是来自A,而不是B。所以这两种方法都可以用来实现动态库:即你调用我的函数和方法改动的都是你自己的数据)。
之前我们说到EVM解释器是面对Contract对象的,不论是Contract的创建还是调用,都会通过run()函数来调用Interpreter的Run()方法。该方法初始化执行过程中所需要的一些变量,然 ...