以太坊是一个状态机,最终区块交易的状态就是区块链的状态,根据以太坊白皮书《A Next-Generation Smart Contract and Decentralized Application Platform》的描述,以太坊架构为
accountSate | 描述说明 |
---|---|
nonce | 基于0的计数器,账户发起交易计数,对于合约账户,表示此帐户创建的合约数量并初始化为1 |
balance | 余额 |
storageRoot | 账户存储树根哈希 |
codeHash | 如果有关联代码就是合约账户 |
transaction | 描述说明 |
---|---|
nonce | 发出交易的账户的nonce值 |
gasPrice | 余额 |
gasLimit | transaction里最多能用费用 |
to | 接受者地址 |
value | 转账金额、合约创建初始充值 |
data | 附加数据 |
v,r,s | 交易签名 |
receipt | 描述说明 |
---|---|
postState | []byte,交易之后的状态的RLP编码后结果 |
cumulativeGasUsed | 交易总共gas消耗余额 |
logsBloom | [256]byte,布隆过滤器,用于检索一笔交易的收据中所有的日志 |
logs | []*Log,存储涉及的日志 |
block Header | 描述说明 |
---|---|
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) |
block Body | 描述说明 |
---|---|
TransactionList | 交易列表 |
ommersList | 叔块头哈希列表(PoS后固定为空[] ) |
Key | value | 描述说明 |
---|---|---|
keccak256(ethereumAddress) | RLP(accountState) | 关联block header的stateRoot |
key | value | 描述说明 |
---|---|---|
RLP(transactionIndex) | RLP(transaction) | 关联block header的transactionsRoot |
Key | value | 描述说明 |
---|---|---|
RLP(transactionIndex) | RLP(transactionReceipt) | 关联block header的receiptsRoot |
key | value | 描述说明 |
---|---|---|
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 执行智能合约时,它会逐条读取并执行每条指令。如果无法执行指令(例如,因为堆栈上没有足够的值),则执行将返回。
1)HP编码
十六进制前缀 Hex-Prefix 编码:
主要树节点结构 Merkle Patricia tree (Trie)
2)RLP编码
递归长度前缀编码 Recursive Length Prefix Encoding:
EVM只认序列化数据,state数据保存与传输,将任意格式的数据编码串型
BE 是将正整数值扩展为最小长度的高端字节数组的函数,点运算符是执行序列拼接
3)RLP与ABI-Encoding
应用二进制接口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不会返还。