EVM即以太坊虚拟机,用于执行智能合约。智能合约可用高级开发语言Solidity进行开发,合约源代码经过编译得到可在EVM中运行的字节码。在部署合约、与合约交互的时候,字节码都是以16进制字符串形式传递和展现。
EVM运行过程中,其本身并不是一个独立的协程、线程更不是进程,它只是交易处理的一部分,在交易处理过程中以函数方式被调用。
调用路径为:StateProcessor.Process --> core.ApplyTransaction(初始化evm对象) --> StateTransition.TransitionDb() --> 根据交易类型执行 evm.Create 或 evm.Call。
在evm.Create中会执行相关验证、转账、初始化Contract对象并调用 run 函数开始执行合约代码,执行成功后获得返回值也就是要存储到链上的合约代码,将返回值存储到链上。
而evm.Call既是调用入口,同时它本身也是一个可递归的函数,在合约字节码中指令 0xf1 即代表CALL操作,在CALL操作中会递归调用evm.Call。在evm.Call中会执行验证及转账、从数据库获取合约代码初始化Contract对象、并调用 run 函数开始执行合约代码这些处理。
重要:上述"从数据库获取合约代码初始化Contract对象"意味着,给合约账户进行转账时,合约账户所关联的合约代码将会被执行。
在合约代码中,会固定包含检查转账金额的逻辑,如果金额大于0,则会执行合约的fallback函数。
如果合约没有fallback函数,或者fallback函数没有payable修饰符,则代码执行会抛异常,从而交易失败。
EVM是基于栈的虚拟机,另外会有一个内存空间用于临时存储数据,而最终大部分的指令都是对栈中的数据进行操作。
合约状态值(状态变量、状态常量,即需要持久化的内容)在底层数据库中的存储方式,可用几条规则概括:
从而,对于字符串,如果编码长度是奇数,则代表的是长字符串,如果是偶数则代表不超过31字节的字符串。
evm的代码都在core包里面,除了入口相关的一些代码,具体运行合约的代码都在core/vm包下
代码文件或结构体 | 说明 |
---|---|
evm.go | 定义了EVM运行环境结构体,并实现 转账处理 这些比较高级的,跟交易本身有关的功能 |
vm/evm.go | 定义了EVM结构体,提供Create和Call方法,作为虚拟机的入口,分别对应创建合约和执行合约代码 |
vm/interpreter.go | 虚拟机的调度器,开始真正的解析执行合约代码 |
vm/opcodes.go | 定义了虚拟机指令码(操作码) |
vm/instructions.go | 绝大部分的操作码对应的实现都在这里 |
vm/gas_table.go | 绝大部分操作码所需的gas都在这里计算 |
vm/jump_table.go | 定义了operation,就是将opcode和gas计算函数、具体实现函数等关联起来 |
vm/stack.go | evm所需要的栈 |
vm/memory.go | evm的内存结构 |
vm/intpool.go | *big.Int的池子,主要是性能上的考虑,跟业务逻辑无关 |
evm的opcode,大体上可以粗略地分为两类,一类是基础操作,如压栈、出栈、加减乘除等数学运算、逻辑比较、hash等等;另一类是跟交易业务密切相关的,可以称为业务指令,比如BALANCE、ADDRESS、CALLER、CALL等等,这些指令有些对应了Solidity中的全局函数或属性。
通过Solidity编写的合约,需要进行编译,编译后变成可供虚拟机执行的二进制码,这些二进制码实际上在所有地方都按16进制字节数组或字符串表示。
以一个最简单的无实际功能无构造器的合约为例,看看编译后代码的结构。 合约代码:
pragma solidity ^0.4.11;
contract C {
}
编译后得到这样一串数据:60606040523415600e57600080fd5b5b603680601c6000396000f30060606040525b600080fd00a165627a7a723058209747525da0f525f1132dde30c8276ec70c4786d4b08a798eda3c8314bf796cc30029
这串数据需要作为16进制格式显示的字节数组对待,这就是evm需要执行的代码。
上面这串代码可分为三部分,即:部署代码、合约代码、Auxdata。
部署代码 在创建合约的时候,evm.Create会先创建合约账户,然后运行部署代码,运行完成后,它会将 合约代码+Auxdata 作为返回值返回,然后evm.Create函数中就会将返回值跟合约账户关联起来存储在区块链上,这样就完成了合约的部署。 上述代码中,部署代码为前面的
60606040523415600e57600080fd5b5b603680601c6000396000f300
合约代码 在这个合约中,合约代码只有11字节:
60606040525b600080fd00
这部分的代码就是存储在链上,供后续调用的代码。Auxdata 每个合约最后面的43字节,就是Auxdata,会跟在合约代码后面被存储起来。即
a165627a7a723058209747525da0f525f1132dde30c8276ec70c4786d4b08a798eda3c8314bf796cc30029
由于后续跟合约交互,是需要知道合约的ABI(应用二进制接口,也就是合约接口的描述信息)的,而在链上却没有存储ABI信息, 所以,要么就只能在部署合约时,用户自己保存好ABI以及合约地址(以太坊钱包就有这样的功能,帮我们保存了这些信息),但这样的话就只有自己能调用这个合约,别人不知道ABI就不能调用合约。 想要让别人也能调用我们部署的合约,在以太坊中提供了两种机制,一个就是用户直接将相关信息上传到etherscan.io这个网站跟合约关联起来,另一个就是以太坊的swarm网络。 而这里的Auxdata,就是给swarm网络使用的,可以认为就是swarm网络的地址,也就是以太坊希望后续自动将合约的相关信息包含ABI存储到swarm中,这样任何一个人从区块上查询到合约代码后,就可以通过auxdata到swarm网络中下载合约的信息。 Auxdata的固定格式为:0xa1 0x65 'b' 'z' 'z' 'r' '0' 0x58 0x20 <32 bytes swarm hash> 0x00 0x29
构造函数参数 如果合约有构造函数参数,则创建合约时还需要跟上编码之后的参数,代码就变成如下结构:部署代码+合约代码+Auxdata+构造参数 构造参数的编码方式,就是将参数值按顺序编码成32字节的数据,连接起来。不同的类型有不同的规则,具体见Solidity文档。
evm是以太坊合约的执行部分,想要从合约编程语言层面扩展功能,就需要同时在Solidity上和evm上实现扩展。Solidity上实现功能扩展,最重要的就是弄清楚它的编译过程。 Solidity是面向用户的高级编程语言,其中的一句代码,可能编译后就对应了一堆字节码。 真正要扩展功能,主要涉及以下几点:
以下几点,有助于理解如何进行功能扩展:
在evm中扩展功能,相对比较简单,具体就是: