以太坊源码解读(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()方法。该方法初始化执行过程中所需要的一些变量,然 ...