以太坊黄皮书知识点

区块链范式

以太坊在整体上可以看作一个基于交易的状态机:起始于一个创世区块(Genesis)状态,然后随着交易的执行状态逐步改变一直到最终状态,这个最终状态是以太坊世界的权威"版本"。状态中包含的信息有:账户余额、名誉度、信誉度、现实世界的附属数据等;简而言之,能包含电脑可以描绘的任何信息。而交易则成为了连接两个状态的有效桥梁;"有效"非常重要——因为无效的状态改变远超过有效的状态改变。例如:无效的状态改变可能是减少账户余额,但是没有在其它账户上加上同等的额度。一个有效的状态转换是通过交易进行的。

世界状态

世界状态(state)是地址(160 位的标识符)和账户状态(序列化为 RLP 的数据结构,详见附录 B)的映射。虽然世界状态没有直接储存在区块链上,但会假定在实施过程中会将这个映射维护在一个修改过的Merkle Patricia 树上(即 trie)。这个 trie 需要一个简单的后端数据库去维护字节数组到字节数组的映射;我们称这个后端数据库为状态数据库。它有一系列的好处:

  • 这个结构的根节点是基于密码学依赖于所有内部数据的,它的哈希可以作为整个系统状态的一个安全标识;
  • 作为一个不变的数据结构,我们可以通过简单地改变根节点哈希来召回任何一个先前的状态(在根节点哈希已知的条件下)。

因为我们在区块链中储存了所以这样的根节点哈希值,所以我们可以很容易地恢复到特定的历史状态。 账户状态包含四个字段:

  • nonce:这个值等于由此账户地址发出的交易数量
  • balance:表示这个账户地址拥有多少 Wei(即账户余额)
  • storageRoot:保存了账户的存储内容的 Merkle Patricia 树的根节点的256 位哈希值
  • codeHash:这个账户的 EVM 代码哈希值——当这个地址接收到一个消息调用时,这些代码会被执行;它和其它字段不同,创建后不可更改。

    交易

    这里的交易有两种类型:一种表现为消息调用,另一种则通过代码创建新的账户(称为"合约创建")。这两种类型的交易都有一些共同的字段:

  • nonce: 由交易发送者发出的的交易的数量
  • gasPrice: 为执行这个交易所需要进行的计算步骤消耗的每单位 gas 的价格,以 Wei 为单位
  • gasLimit: 用于执行这个交易的最大 gas 数量。这个值须在交易开始前设置,且设定后不能再增加
  • to: 160 位的消息调用接收者地址
  • value: 转移到接收者账户的 Wei 的数量
  • v, r, s: 与交易签名相符的若干数值,用于确定交易的发送者
  • init: 一个不限制大小的字节数组,用来指定账户初始化程序的 EVM 代码

    区块

    在以太坊中,区块是由以下部分组成的:一些相关信息片段组成的集合(称为 block header,即区块头);组成区块的交易 T 和 其它一些区块头 U (这是一些父区块与当前区块的爷爷辈区块相同的区块,这样的区块称为ommers2)。区块头包含的的信息如下:

  • parentHash: 父区块头的 Keccak 256 位哈希
  • ommersHash: 当前区块的 ommers 列表的 Keccak256 位哈希
  • beneficiary: 成功挖到这个区块所得到的所有交易费的 160 位接收地址
  • stateRoot: 所有交易被执行完且区块定稿后的状态树(state trie)根节点的 Keccak 256 位哈希
  • transactionsRoot: 由当前区块中所包含的所有交易所组成的树结构(transaction trie)根节点的Keccak 256 位哈希
  • receiptsRoot: 由当前区块中所有交易的收据所组成的树结构(receipt trie)根节点的 Keccak 256位哈希
  • logsBloom: 由当前区块中所有交易的收据数据中的可索引信息(产生日志的地址和日志主题)组成的Bloom 过滤器
  • difficulty: 当前区块难度水平的纯量值,它可以根据前一个区块的难度水平和时间戳计算得到
  • number: 当前区块的祖先的数量
  • gasLimit: 目前每个区块的 gas 开支上限
  • gasUsed: 当前区块的所有交易所用掉的 gas 之和
  • timestamp: 当前区块初始化时的 Unix 时间戳
  • extraData: 与当前区块相关的任意字节数据,但必须在 32 字节以内
  • mixHash: 一个 256 位的哈希值,用来与 nonce 一起证明当前区块已经承载了足够的计算量
  • mixHash: 一个 256 位的哈希值,用来与mixHash一起证明当前区块已经承载了足够的计算量

    交易数据

    为了能使交易信息对零知识证明、索引和搜索都是有用的,我们将每个交易执行过程中的一些特定信息编码为交易收据。

    整体有效性

    当且仅当一个区块同时满足以下几个条件,我们才认为它是有效的:

  • 它必须由内部一致的 ommer和交易区块哈希值所组成
  • 且基于起始状态按顺序执行完成所有的给定交易并达到新状态

    序列化

    当需要对区块和区块头进行 RLP变换时要求一定的类型和顺序。RLP 函数 提供了权威的方法来把这个结构转换为一个可以通过网络传输或在本地存储的字节序列。

    区块头验证

    这个机制保证了区块时间的动态平衡;如果最近的两个区块间隔较短,则会导致难度值增加,因此需要额外的计算量,大概率会延长下个区块的出块时间。相反,如果最近的两个区块间隔过长,难度值和下一个区块的预期出块时间也会减少。

    Gas 及其支付

    为了避免网络滥用及回避由于图灵完整性而带来的一些不可避免的问题,在以太坊中所有的程序执行都需要费用。任意的程序片段(包括合约创建、消息调用、分配资源以及访问账户 storage、在虚拟机上执行操作等)都会有一个普遍认同的 gas 消耗。 每一个交易都要指定一个 gas 上限:gasLimit。这些gas 会从发送者的账户的 balance 中扣除。这种购买是通过同样在交易中指定的 gasPrice 来完成的。如果这个账户的balance 不能支持这样的购买,交易会被视为无效交易。之所以将其命名为 gasLimit,是因为剩余的 gas 会在交易完成后被返还(与购买时同样价格)到发送者账户。Gas 不会在交易执行之外存在,因此对于可信任的账户,应该设置一个相对较高的 gas 上限。 高 gas 价格的交易将花费发送者更多的以太币,也就将移交给矿工更多的以太币,因此这个交易自然会被更多的矿工选择打包进区块。

    交易执行

    交易执行过程即是状态转换函数的定义过程。任意交易在执行时前都要先通过初始的基础有效性测试。包含:

  • 交易是 RLP 格式数据,没有多余的后缀字节
  • 交易的签名是有效的
  • 交易的 nonce 是有效的(等于发送者账户当前的nonce)
  • gas 上限不小于交易所要使用的 gas
  • 发送者账户的 balance 应该不少于实际费用 ,且需要提前支付

    子状态

    交易的执行过程中会累积产生一些特定的信息,我们称为交易子状态,它是个元组,包含了:

  • 一组应该在交易完成后被删除的账户
  • 一系列的日志并允许以太坊世界的外部旁观者(例如去中心化应用的前端)来简单地跟踪合约调用
  • 交易所接触过的账户集合,其中的空账户可以在交易结束时删除
  • 是应该返还的余额

    执行

    gas 消耗由以下三部分组成:

  • 交易附带的关联数据的字节序列,取决于长度
  • EVM初始化代码的字节序列,取决于长度
  • 创建还是调用合约,调用则为零 有效交易的执行起始于一个对状态不能撤回的改变:发送者账户的 nonce会加 1,并且从账户余额扣减预付费用。无论是合约创建还是消息调用,计算都会产生一个最终状态(可能等于当前的状态),这种改变是确定 的且从来不会无效:这样来看,其实并不存在无效的交易。

    合约创建

    创建一个合约账户需要很多固有参数:发送者原始交易人可用的 gasgas 价格endowment(即初始捐款)任意长度的字节数组(即 EVM 初始化代码)消息调用/合约创建的当前栈深度以及对状态进行修改的许可。 如果有足够的初始捐款(endowment)成功创建合约那么剩余的 gas 会返还给最原始的交易发起人,对状态的改变也将永久保存; 当gas 不足时,交易中附带的金额并不会转移到被取消的合约地址,不会创建任何合约,不会进行转账并且状态初始化。

    执行模型

    执行模型说明了如何使用一系列的字节代码指令和一个环境数据的元组去更改系统状态。这是通过一个正规的虚拟状态机来实现的,也就是以太坊虚拟机(Ethereum VirtualMachine - EVM)。它是一个准图灵机,这个"准"的限定来源于其中的运算是通过参数 gas 来限制的,也就是限定了可以执行的运算总量。

    EVM基础

    EVM 是一个简单的基于栈的架构。程序代码被保存在一个独立的、仅能通过特定的指令进行交互的虚拟 ROM(即只读存储器)中,而不是保存在一般的可访问内存或存储中。 EVM 存在异常执行的情况,包括堆栈溢出和非法指令。在发生异常会立即停止执行,并告返回异常。

    费用概述

    在三个不同的情况下会收取执行gas费用,这三种情况都是执行操作的先决条件。

  • 最普遍的情况就是计算操作费用
  • 执行一个低级别的消息调用或者合约创建可能需要扣除 gas,这也就是执行 CREATE,CALL 和CALLCODE 的费用的一部分。
  • 内存使用的增加也会消耗一定的 gas。