EVM底层结构
Last updated
Last updated
以太坊是一个状态机,最终区块交易的状态就是区块链的状态,根据以太坊白皮书《A Next-Generation Smart Contract and Decentralized Application Platform》的描述,以太坊架构为
nonce
发出交易的账户的nonce值
gasPrice
余额
gasLimit
transaction里最多能用费用
to
接受者地址
value
转账金额、合约创建初始充值
data
附加数据
v,r,s
交易签名
postState
[]byte,交易之后的状态的RLP编码后结果
cumulativeGasUsed
交易总共gas消耗余额
logsBloom
[256]byte,布隆过滤器,用于检索一笔交易的收据中所有的日志
logs
[]*Log,存储涉及的日志
parentHash
父块头哈希,正是通过此记录,才能完整的将区块有序组织,形成一条区块链
sha3Uncles
叔块头哈希列表的哈希(PoS不存在该概念,uncles=[]
,该值固定为0xc0=RLP([])
)
beneficiary
挖矿奖励获取人地址address(miner)
stateRoot
状态树根节点Hash
transactionsRoot
交易树根节点Hash
receiptsRoot
收据树根节点Hash
logsBloom
[256]byte,用于检索当前区块中所有的receipt收据的日志的布隆过滤器
difficulty
big.Int,PoW时难度值 12s-16s自动调整(PoS后固定为0)
number
big.Int,区块高度
gasLimit
uint64,区块内能用的gas 允许矿工可以有5%的上下浮动(伦敦升级后为1500W,但块的大小将根据网络需求增加或减少,直到块限制为 3000 W{原目标大小的 2 倍},solidity合约大小限制为的 24576bytes
)
gasUsed
uint64,区块内所有交易实际消耗的gas
timeStamp
uint64,表示此区块创建的UTC时间戳,单位秒(PoW矿工可以将区块时间戳修改 +/-15 秒,PoS为固定12s,极少数会有12的整数倍的情况,会检查时间戳)
extraData
<=32字节数组,由矿工自定义,一般会写一些公开推广类内容或者作为投票使用。
mixHash
本区块标识哈希(hash(区块头数据不包含nonce))
nonce
uint64(8字节)随机数,PoW工作量证明(PoS后固定为0)
TransactionList
交易列表
ommersList
叔块头哈希列表(PoS后固定为空[]
)
World State Trie世界状态树
keccak256(ethereumAddress)
RLP(accountState)
关联block header的stateRoot
Transactions Trie区块级交易树
RLP(transactionIndex)
RLP(transaction)
关联block header的transactionsRoot
Receipts Trie区块级数据树
RLP(transactionIndex)
RLP(transactionReceipt)
关联block header的receiptsRoot
Account Storage Trie账户存储树
keccak256(slot position)
RLP(slot position)
关联acountState的storageRoot
注意:四个Trie 都不在区块链网络传输 ,网络中只传输交易数据
以太坊虚拟机(或EVM)是基于堆栈的计算机。这意味着所有指令都从堆栈中获取它们的参数,并将它们的结果写入堆栈。因此,每条指令都有堆栈输入、它需要的参数(如果有的话)、堆栈输出和返回值(如果有的话)。所有指令都编码为 1 个字节,PUSH 指令除外,它允许将任意值放入堆栈并在指令后直接对该值进行编码。可用指令列表及其操作码显示在参考资料中。
虚拟机VM:运行在真实机器上的软件,提供操作系统(在系统VM的情况下)或应用程序(在进程 VM的情况下)的运行环境。
传统CPU以及诸如 Dalvik 虚拟机(Android移动设备平台的核心组成部分之一,可以支持已转换为.dex 格式的Java「注:JVM是基于堆栈的」应用程序的运行),是基于寄存器的结构:区别
ROM
用来保存所有EVM程序代码的“只读”存储,由以太坊客户端独立维护
存储只读代码,Code is Law
Stack
即所谓的“运行栈”,用来保存EVM指令的输入和输出数据
256 bit(32字节)位宽,最大深度为1024
Momory
内存,一个简单的字节数组,用于临时存储EVM代码运行中需要的存取的各种数据
256bit / 8bit位宽,无限大小,基于32字节进行寻址和扩展
Storage
存储,由以太坊客户端独立维护的持久化数据区域
256 * 2 bit位宽(key & value),2^256大小,每个账户的存储区域被以32字节为单位划分为若干”槽(slot)“,合约中的“状态变量”会根据其具体类型分别保存到这些“槽”中
在 Solidity 中,有两个地方可以存储变量 :存储(storage)以及内存(memory)。Storage变量是指永久存储在区块链中的变量。Memory 变量则是临时的,只能用于函数内部,当外部函数对某合约调用完成时,内存型变量即被移除。
storage 在区块链中是用key/value的形式存储slot[x]=y,而memory则表现为字节数组[0x01, 0x02...]
存储在内存的memory,根据存储位置还可以细分为2种类型的存储数据位置,一种是Calldata,一种是Stack
(1) Calldata这是一块只读的,且不会永久存储的位置,用来存储函数参数。 一般将外部函数的参数(非返回参数)的数据位置指定为 calldata ,效果跟 memory 差不多。
(2) Stack ,EVM是一个基于堆栈的虚拟机,opcodes指令的运算需要借助Stack,而Stack实际是在计算机内存中的一个数据结构,每个栈元素占为256位,栈最大长度为1024。 值类型的局部变量是存储在栈上,但注意,堆栈仅有高处的 16 层是可以被快速访问的(solidity中stack overflow报错的原因)
操作码Opcodes(字节指令):EVM指令被分配了一个介于 0 和 255(或十六进制中的 FF)之间的值。它是帮助我们人类阅读指令的文本表示。智能合约是一组指令。当 EVM 执行智能合约时,它会逐条读取并执行每条指令。如果无法执行指令(例如,因为堆栈上没有足够的值),则执行将返回。
十六进制前缀 Hex-Prefix 编码:
主要树节点结构 Merkle Patricia tree (Trie)
递归长度前缀编码 Recursive Length Prefix Encoding:
EVM只认序列化数据,state数据保存与传输,将任意格式的数据编码串型
BE 是将正整数值扩展为最小长度的高端字节数组的函数,点运算符是执行序列拼接
应用二进制接口ABI( Application Binary Interface)是与合约交互的标准。 EVM使用 ABI 编码的数据来理解要执行字节码的哪一部分。合约交互只是以太上的一种交易。payload(要做什么) 位于transaction的data
字段中。
调用合约的用户通过 ABI-Encoding 对的输入参数和函数签名进行编码(当然也包括合约代码的输出参数,比如日志Log的data),并将其放在transaction的data
字段中。之后队transaction进行签名作为所有权证明。最后,对整个transaction数据进行 RLP 编码,即可传递给EVM。
注意:ABI编码仅用于合约交互,transaction的
data
字段包含目标函数选择器和函数所需入参。而 RLP,可以理解为是Ethereum在数据通过传输之前进行数据编码(转换)的低级方法
具体合约交互的数据转换逻辑:
发送方:
确定在合约地址与对应的函数
ABI 编码函数选择器和输入参数
将 ABI 编码器的输出原始字节放入交易的data
将所有值放入交易包括 to、 value 和 nonce
RLP 对这些字段进行编码
签名交易
再次RLP 编码并发送
接收方:
RLP解码收到的transaction
验证签名,检查交易的有效性
再次RLP解码
执行 (EVM 将在内部处理 ABI 解码与执行)
注意:所有的 refund 都在 tx 最后执行而不是实时返还,这解释了即便在某些情况下 gasUsed<gaslimit, tx 仍会因out of gas而失败
EVM 代码执行的实际gas消耗与其对内存memory的使用有关,并不是固定的。
鼓励最小化使用存储storage,用sstore操作将非0值存储区域重置为0值,会在最后获得的gas返还。
交易执行的最后会删除执行过程中接触过的所有“空账户”和自毁列表中的账户,这也会返还一定量的gas。
EVM代码的执行必定会持续到一个正常终止或一个异常终止,但无法用代码直接触发一个异常终止。
EVM代码执行的异常终止会撤销当前交易中所有对状态的更改,但执行过程中所有消耗的gas不会返还。