主题
字号
CHAPTER 03 ≈ 35 MIN READ

第三部分:以太坊与可编程区块链

第7章 以太坊:可编程的区块链

7.1 比特币留下的那个问题

学完比特币,你可能会有一个感觉:这个系统很精妙,但也很"死板"。它只做一件事——转账。你可以把比特币从地址A发到地址B,仅此而已。

这当然已经很了不起了。但工程师的天性就是:看到一个好的基础设施,就会想在上面盖更多东西

2013年,一个19岁的俄裔加拿大程序员 Vitalik Buterin(维塔利克·布特林)正在研究比特币。他看到了一些开发者试图在比特币上构建应用——比如去中心化域名系统 Namecoin,或者代表现实资产的彩色币(Colored Coins)。但他注意到一个根本性的问题:比特币的脚本语言(Script)太弱了

比特币的 Script 是故意设计成非图灵完备的。它没有循环,没有复杂的条件分支,这样做是为了安全——防止矿工在验证交易时陷入无限循环。但这也意味着,你没法在比特币上写复杂的逻辑。

Vitalik 的想法是:与其为每种应用单独做一条链,不如做一条通用的链,让任何人都可以在上面部署任意逻辑。

这就像操作系统的诞生。在操作系统出现之前,每台机器只能跑一个程序;有了操作系统,你可以在同一台机器上运行无数应用。以太坊想做的,是区块链的操作系统

2013年底,Vitalik 发布了以太坊白皮书。2014年初,他在迈阿密比特币大会上公开介绍了这个想法。同年,以太坊团队通过 ICO(首次代币发行)筹集了约1800万美元。2015年7月30日,以太坊主网正式上线,这一天被称为"创世纪元"(Genesis Block)。


7.2 以太坊的核心抽象:世界状态机

要理解以太坊,最好的切入点是它的抽象模型。

以太坊把自己定义为一台基于交易的状态机(Transaction-Based State Machine)

什么是状态机?简单说:系统在任意时刻都处于某个状态(State),当接收到**输入(交易)**时,状态按照确定的规则转换到下一个状态。

$\sigma_{t+1} \equiv \Upsilon(\sigma_t, T)$

这里 $\sigma_t$ 是当前世界状态,$T$ 是一笔交易,$\Upsilon$ 是以太坊的状态转换函数,$\sigma_{t+1}$ 是新状态。

以太坊的**世界状态(World State)**是什么?它是所有账户地址到账户状态的一个映射:

$\sigma: \text{address} \rightarrow \text{account state}$

每个账户状态包含四个字段:

字段 含义
nonce 该账户发出的交易数量(防重放攻击)
balance 账户持有的 ETH 数量(单位:Wei,$1 \text{ ETH} = 10^{18} \text{ Wei}$)
storageRoot 账户存储的 Merkle Patricia Trie 根哈希
codeHash 账户代码的哈希(对于普通账户,是空字符串的哈希)

这里出现了两种账户:

外部账户(Externally Owned Account, EOA):由私钥控制,codeHash 是空字符串哈希,没有存储。你我的钱包地址都是 EOA。

合约账户(Contract Account):由代码控制,有 codeHashstorageRoot,没有私钥,不能主动发起交易。合约只能被其他账户调用时"醒来"执行。

这个区分非常重要。合约账户就像一个自动执行的程序,住在区块链上,任何人都可以通过发送交易来触发它。


7.3 以太坊虚拟机(EVM):区块链上的 CPU

以太坊能运行任意代码的关键,是以太坊虚拟机(Ethereum Virtual Machine, EVM)

EVM 是一个基于栈的虚拟机,类似于 Java 的 JVM,但专门为区块链环境设计。它运行在全球所有以太坊节点上——每当有合约调用发生,每个节点都会在自己的 EVM 里执行相同的代码,得到相同的结果,从而保证共识。

EVM 的主要组成部分:

1. 程序计数器(PC):指向当前执行的指令。

2. 栈(Stack):EVM 是栈机,所有操作都在栈上进行。栈最大深度为1024,每个元素是256位整数(32字节)。

3. 内存(Memory):字节寻址的线性空间,每次调用时重新初始化,只在当前执行上下文中存在。内存扩展需要消耗 Gas。

4. 存储(Storage):持久化的 Key-Value 存储,$2^{256}$ 个槽,每个槽32字节。存储跨调用持久存在,修改存储是最昂贵的操作之一。

5. 调用数据(Calldata):调用合约时传入的只读数据。

EVM 支持的指令集(Opcodes)包括:

一个简单的 EVM 执行示例,计算 1 + 2

PUSH1 0x02   // 栈: [2]
PUSH1 0x01   // 栈: [1, 2]
ADD          // 栈: [3]  (弹出两个元素,推入其和)

EVM 的设计是确定性的:给定相同的输入(初始状态 + 交易),任何节点执行都会得到完全相同的输出。这是去中心化共识的基础。


7.4 Gas:给计算定价

现在来讨论以太坊最精妙的设计之一:Gas 机制

问题:如果任何人都可以在以太坊上运行任意代码,怎么防止有人写一个死循环让全网节点卡死?

比特币的解决方案是:限制脚本语言的表达能力,禁止循环。但这牺牲了灵活性。

以太坊的解决方案更聪明:给每条指令定价,执行完整个程序需要付钱

具体机制:

发送交易时,你需要指定:

交易执行时,每执行一条 EVM 指令,消耗相应数量的 Gas:

操作 Gas 消耗
ADD, SUB 3
MUL, DIV 5
SLOAD(热访问) 100
SLOAD(冷访问) 2100
SSTORE(首次写入) 22100
SHA3 30 + 6/字
部署新合约 32000
基础交易 21000

如果 Gas 耗尽(Out of Gas),交易回滚:所有状态改变撤销,但已消耗的 Gas 不退还(因为矿工/验证者已经付出了计算资源)。

Gas 机制带来三个好处:

  1. 防止 DoS 攻击:死循环会耗尽 Gas,自然终止
  2. 资源定价:稀缺的计算资源按市场定价
  3. 费用收益:激励矿工/验证者处理交易

EIP-1559 的改革(2021年8月,伦敦升级)

原来的 Gas 机制是简单拍卖:用户出价,矿工选最高价。这导致 Gas 费用极度不稳定,用户体验差。

EIP-1559 引入了新的费用结构:

$\text{总费用} = (\text{baseFee} + \text{priorityFee}) \times \text{gasUsed}$

baseFee 的调整规则:如果上一个区块 Gas 使用量超过目标值(区块上限的50%),baseFee 增加最多12.5%;如果低于目标值,相应减少。这使得 Gas 价格更可预测。

ETH 的通缩机制:由于 baseFee 被销毁,当网络繁忙时,每个区块销毁的 ETH 可能超过新发行的 ETH,使得 ETH 变成通缩货币。这被称为 Ultra Sound Money(超健全货币)。


7.5 以太坊的账户与交易模型

以太坊和比特币的账户模型有根本区别。

比特币:UTXO(未花费交易输出)模型。你的"余额"实际上是一堆未花费的输出,每笔交易消耗若干 UTXO,产生新的 UTXO。

以太坊:账户余额模型(Account/Balance Model)。更像传统银行账户,直接记录每个地址的余额。

以太坊的账户余额模型优点:

UTXO 模型的优点(以太坊没选择这条路):

以太坊的交易包含以下字段:

{
  "nonce": "0x15",           // 发送者的交易计数,防止重放
  "gasPrice": "0x4A817C800", // Gas 价格(Wei)
  "gasLimit": "0x5208",      // Gas 上限(21000 = 0x5208)
  "to": "0xABCDEF...",       // 接收者地址(合约或 EOA)
  "value": "0xDE0B6B3A7640000", // 发送的 ETH 数量(Wei)
  "data": "0x",              // 附加数据(调用合约时包含函数选择器和参数)
  "v": "0x1c",               // 签名参数
  "r": "0x...",              // 签名参数
  "s": "0x..."               // 签名参数
}

to 是合约地址时,data 字段包含函数调用编码

例如,调用 transfer(address,uint256) 函数:

函数选择器 = keccak256("transfer(address,uint256)")[0:4]
           = 0xa9059cbb

合约创建交易to 字段为空(null0x0),data 字段包含合约的字节码。交易执行后,EVM 将字节码部署到一个新地址,该地址由发送者地址和 nonce 确定性计算得出:

$\text{contractAddress} = \text{keccak256}(\text{RLP}(\text{sender}, \text{nonce}))[12:]$


7.6 以太坊的数据结构:Merkle Patricia Trie

比特币使用 Merkle 树组织交易。以太坊需要更复杂的数据结构来高效表示可修改的状态

以太坊引入了 Merkle Patricia Trie(MPT),融合了三种数据结构:

  1. Trie(前缀树/字典树):按键的字符前缀组织数据,公共前缀共享路径
  2. Patricia Trie:压缩版 Trie,将只有一个子节点的路径合并,节省空间
  3. Merkle 特性:每个节点的哈希由其子节点内容决定,根节点哈希唯一表示整棵树的状态

MPT 中有三种节点类型:

以太坊的区块头包含三棵 MPT 的根哈希:

字段 内容
stateRoot 世界状态树(所有账户状态)的根哈希
transactionsRoot 本区块交易的 Merkle 树根哈希
receiptsRoot 交易收据树的根哈希

这三个根哈希将整个区块链状态"压缩"进区块头,使得轻客户端验证成为可能:无需下载全部数据,仅凭区块头和 Merkle Proof 就能验证任何账户的状态或任何交易的存在。


7.7 以太坊的区块结构

以太坊的区块头(Block Header)包含以下主要字段:

parentHash      // 父区块的哈希
unclesHash      // 叔块哈希(Uncle/Ommer 机制)
coinbase        // 矿工/验证者地址(接收区块奖励)
stateRoot       // 世界状态 MPT 根哈希
transactionsRoot // 交易 MPT 根哈希
receiptsRoot    // 收据 MPT 根哈希
logsBloom       // 日志过滤器(Bloom Filter)
difficulty      // 挖矿难度(PoW 时代)
number          // 区块高度
gasLimit        // 本区块 Gas 上限
gasUsed         // 本区块实际消耗的 Gas
timestamp       // Unix 时间戳
extraData       // 额外数据(最多32字节,矿工/验证者自定义)
mixHash/prevRandao // PoW 混合哈希 或 PoS 随机数
nonce           // PoW 的 nonce(PoS 后固定为0)
baseFeePerGas   // EIP-1559 基础费率

叔块(Uncle/Ommer)机制(PoW 时代特有):

比特币中,几乎同时挖出的区块,只有一个能进入主链,其他的作废。以太坊认为这浪费了有效工作,于是引入了叔块机制:在主链区块的6个以内高度范围内的孤块,可以被后续区块引用为"叔块",获得部分奖励(7/8的区块奖励),引用者也获得额外奖励(1/32)。这提高了矿工收益,减少了中心化压力。


7.8 以太坊的生态:从单一货币到通证经济

以太坊最重要的创新不是技术本身,而是它催生的通证(Token)生态

ERC-20 标准

2015年,开发者 Fabian Vogelsteller 提出了 ERC-20(Ethereum Request for Comment 20)标准。它定义了一个合约接口,任何实现这个接口的合约都可以创建互操作的同质化代币(Fungible Token)

ERC-20 接口包含:

// 必须实现的函数
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);

// 必须发出的事件
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);

有了 ERC-20,任何人都可以在以太坊上发行自己的代币,且这些代币可以被所有以太坊钱包、交易所自动支持。这引爆了 2017 年的 ICO 热潮——无数项目在以太坊上发行 ERC-20 代币融资。

ERC-721:NFT 的诞生

2018年,ERC-721 标准定义了非同质化代币(Non-Fungible Token, NFT)。每个 NFT 有唯一的 tokenId,代表不可替换的数字资产(艺术品、游戏道具、域名等)。我们将在第12章详细讨论 NFT。

ERC-1155:半同质化代币

Enjin 团队提出的标准,允许单个合约同时管理同质化代币和非同质化代币,大幅降低了区块链游戏的 Gas 成本。


7.9 以太坊 2.0:一场持续的升级

以太坊从诞生起就是一个持续进化的系统。主要里程碑:


7.10 以太坊与比特币:一个类比

理解了以太坊,我们可以做一个简洁的对比:

维度 比特币 以太坊
定位 数字黄金,价值存储 世界计算机,通用平台
主要功能 点对点转账 智能合约执行
脚本语言 非图灵完备 Script 图灵完备 EVM
账户模型 UTXO 账户余额
出块时间 ~10分钟 ~12秒(PoS 后)
总量 2100万,固定 无硬上限,动态调节
共识机制 PoW PoS(2022年后)
创始人 匿名(中本聪) 公开(Vitalik Buterin)

比特币是极简主义:只做一件事,做到极致,永不改变。以太坊是进化主义:持续升级,拥抱复杂性,追求通用性。

两种哲学没有对错,只有取舍。


7.11 直觉总结

为什么以太坊是一个突破性的创新?

核心洞察:区块链解决的是"在不信任的环境中建立共识"的问题。比特币把这个能力用于货币。以太坊把这个能力泛化了——任何需要在不信任方之间执行的逻辑,都可以用以太坊来实现

你可以把以太坊想象成一台全球共享的计算机

这台计算机很慢(每秒约15笔交易),很贵(Gas 费),但它有一个无可替代的特性:它执行的每一行代码,都被全球数千个节点见证和验证

这种"可验证计算"(Verifiable Computation)的能力,是以太坊最核心的价值所在。


本章小结

以太坊在比特币的去中心化共识基础上,引入了图灵完备的 EVM 和 Gas 机制,将区块链从"数字货币"升级为"世界计算机"。其账户模型、MPT 数据结构和 ERC 标准体系共同构成了智能合约生态的基础设施,而持续的协议升级(尤其是 The Merge)则展示了去中心化系统在极大复杂性下的演化能力。


第8章 智能合约:代码即法律

8.1 一个古老的设想

"智能合约"这个词听起来很新,但这个概念来自1994年——互联网都还没普及的年代。

密码学家、法学学者 Nick Szabo(尼克·萨博,也是许多人认为的"中本聪候选人"之一)提出了这个概念:

"智能合约是一套以数字形式规定的承诺,包括合约参与方执行这些承诺的协议。"

他的直觉来自一个日常例子:自动售货机。你投入硬币,选择商品,机器自动完成交易——无需人工介入,无需信任机器的主人,合约逻辑完全由机器的物理机制保证。

Szabo 的设想是:用代码替代法律合同中的自然语言,用密码学和计算机网络保证执行。但1994年没有合适的基础设施来实现这个想法。

直到以太坊出现。

以太坊的智能合约(Smart Contract)本质上就是部署在区块链上的程序:一旦部署,按照预定规则自动执行,任何人(包括合约的创建者)都无法单方面修改其行为。


8.2 Solidity:以太坊的主要编程语言

以太坊智能合约可以用多种语言编写,最主流的是 Solidity——一门专为 EVM 设计的静态类型语言,语法上借鉴了 JavaScript/C++。

让我们从一个最简单的例子开始:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SimpleStorage {
    // 状态变量:存储在区块链上
    uint256 private storedNumber;
    address public owner;

    // 构造函数:合约部署时执行一次
    constructor() {
        owner = msg.sender; // msg.sender 是调用者的地址
    }

    // 写入函数:修改状态,需要 Gas
    function setNumber(uint256 newNumber) public {
        storedNumber = newNumber;
    }

    // 读取函数:不修改状态,不需要 Gas(本地执行)
    function getNumber() public view returns (uint256) {
        return storedNumber;
    }
}

这个合约做了一件非常简单的事:在区块链上存储一个数字。但它展示了智能合约的核心特征:

  1. 状态变量(State Variables)storedNumberowner 存储在合约的永久存储(Storage)中,跨调用持久存在。
  2. msg.sender:Solidity 的内置全局变量,代表当前调用者的地址。
  3. view 函数:声明不修改状态,节点可以本地执行,不需要上链,不消耗 Gas。
  4. 构造函数:只在合约部署时执行一次,用于初始化。

8.3 深入 Solidity:类型系统与数据位置

Solidity 的类型系统相对严格,理解它对写出高效、安全的合约至关重要。

值类型(Value Types)

bool flag = true;          // 布尔型
uint256 count = 100;       // 无符号整数(uint = uint256)
int128 temperature = -30;  // 有符号整数
address owner = 0xABCD...; // 以太坊地址(20字节)
bytes32 hash = keccak256(abi.encodePacked("hello")); // 定长字节数组

特别注意 uint256:EVM 原生操作32字节(256位)整数,所以 uint256 是最高效的整数类型。

引用类型(Reference Types)

uint256[] public dynamicArray;     // 动态数组
uint256[5] public fixedArray;      // 定长数组
mapping(address => uint256) public balances; // 映射(类似哈希表)
string public name;                 // 字符串
bytes public data;                  // 动态字节数组

数据位置(Data Location):这是 Solidity 的独特概念,引用类型必须声明存储在哪里:

// 错误示范:数组作为参数时忘记指定 memory
function processArray(uint256[] memory arr) public pure returns (uint256) {
    uint256 sum = 0;
    for (uint256 i = 0; i < arr.length; i++) {
        sum += arr[i];
    }
    return sum;
}

理解 mapping

mapping(address => uint256) public balances 是以太坊中最常用的数据结构,实现方式非常有趣:它不存储键的列表,而是将每个键哈希后直接映射到一个存储槽。

$\text{slot}(\text{key}) = \text{keccak256}(\text{key} | \text{baseSlot})$

这意味着 mapping 无法遍历所有键,也无法获取其大小——它只支持 O(1) 的读写。


8.4 一个完整的代币合约:ERC-20 实现

理论讲多了容易发困。我们来看一个有实际意义的例子:实现一个简单的 ERC-20 代币。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MyToken {
    string public name = "MyToken";
    string public symbol = "MTK";
    uint8 public decimals = 18;
    uint256 public totalSupply;

    // 余额映射
    mapping(address => uint256) public balanceOf;
    // 授权映射:owner 允许 spender 最多花多少
    mapping(address => mapping(address => uint256)) public allowance;

    // 事件:链下监听
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    // 构造函数:铸造初始供应量给部署者
    constructor(uint256 _initialSupply) {
        totalSupply = _initialSupply * (10 ** uint256(decimals));
        balanceOf[msg.sender] = totalSupply;
        emit Transfer(address(0), msg.sender, totalSupply);
    }

    // 转账
    function transfer(address to, uint256 amount) public returns (bool) {
        require(to != address(0), "Transfer to zero address");
        require(balanceOf[msg.sender] >= amount, "Insufficient balance");

        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;

        emit Transfer(msg.sender, to, amount);
        return true;
    }

    // 授权第三方花费
    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    // 第三方代转账(需要先 approve)
    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(allowance[from][msg.sender] >= amount, "Allowance exceeded");
        require(balanceOf[from] >= amount, "Insufficient balance");

        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;

        emit Transfer(from, to, amount);
        return true;
    }
}

这段代码很好地展示了几个重要概念:

require(condition, message):条件检查。如果条件为假,交易回滚并抛出错误信息,已消耗的 Gas 部分退还(EIP-140后)。类似于 C 中的 assert,但专门用于输入验证。

事件(Events)emit Transfer(...) 将日志写入交易收据。事件不存储在合约存储中(更便宜),但可以被链下应用监听和查询,是合约与前端通信的主要方式。

address(0):以太坊中的零地址(0x0000...0000),类似空指针。铸造代币时 from 是零地址,销毁代币时 to 是零地址,约定俗成的规范。

decimals:ERC-20 代币通常设 18位小数,对应 ETH 的 Wei 精度。1 MTK 在合约内部存储为 1 * 10^18


8.5 合约安全性:那些昂贵的教训

智能合约一旦部署,代码就在区块链上永久存在,漏洞也永久存在。历史上,漏洞导致的损失以亿美元计。

最著名的漏洞:重入攻击(Reentrancy Attack)

回顾2016年的 DAO 黑客事件。漏洞的核心是这样的逻辑(简化版):

// 存在漏洞的提款函数
mapping(address => uint256) public balances;

function withdraw(uint256 amount) public {
    require(balances[msg.sender] >= amount, "Insufficient");
    
    // 1. 先发送 ETH
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success, "Transfer failed");
    
    // 2. 再更新余额  ← 漏洞在这里
    balances[msg.sender] -= amount;
}

问题在哪?当合约调用 msg.sender.call{value: amount}("") 时,如果 msg.sender 是一个恶意合约,它的 receive() 函数可以balances[msg.sender] -= amount 执行之前,再次调用 withdraw。此时余额还没扣减,检查仍然通过,于是可以一直递归提款,直到合约资金耗尽。

攻击流程图:

攻击者合约.attack()
  → 受害合约.withdraw(1 ETH)
    → 发送 1 ETH 给攻击者合约
      → 攻击者合约.receive() 触发
        → 受害合约.withdraw(1 ETH)  // 余额还没扣!
          → 发送 1 ETH 给攻击者合约
            → ... 递归 ...

修复方法:Checks-Effects-Interactions 模式(检查-效果-交互)

// 安全版本
function withdraw(uint256 amount) public {
    // 1. Checks(检查条件)
    require(balances[msg.sender] >= amount, "Insufficient");
    
    // 2. Effects(先更新状态)
    balances[msg.sender] -= amount;
    
    // 3. Interactions(最后做外部调用)
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success, "Transfer failed");
}

或者使用重入锁:

bool private locked;

modifier noReentrant() {
    require(!locked, "Reentrant call");
    locked = true;
    _;
    locked = false;
}

function withdraw(uint256 amount) public noReentrant {
    // ...
}

整数溢出/下溢(Integer Overflow/Underflow)

在 Solidity 0.8.0 之前,整数运算不自动检查溢出:

// Solidity < 0.8.0 的陷阱
uint8 x = 255;
x += 1; // x = 0(溢出!)

uint8 y = 0;
y -= 1; // y = 255(下溢!)

攻击者可以利用这一点:例如,如果余额检查 require(balances[sender] - amount >= 0) 在无符号整数下溢后反而变成了巨大的正数,检查就会通过。

0.8.0 版本后,Solidity 默认启用溢出检查,出现溢出时自动回滚。老合约则需要使用 OpenZeppelin 的 SafeMath 库。

其他常见漏洞


8.6 合约升级:不可变性的悖论

"代码即法律"有一个悖论:如果发现了 Bug,怎么办?

区块链的不可变性是一把双刃剑。部署的代码无法修改,这保证了可信度,但也意味着修复漏洞需要特殊机制。

代理模式(Proxy Pattern)

这是目前最主流的合约升级方案,核心思想是分离逻辑和状态

用户 → 代理合约(Proxy)→ 逻辑合约(Implementation)
         ↓ 存储状态            ↓ 包含逻辑,无状态

代理合约使用 EVM 的 DELEGATECALL 指令调用逻辑合约。DELEGATECALL 的特殊之处在于:它在调用方的上下文中执行被调用方的代码——也就是说,逻辑合约的代码读写的是代理合约的存储

当需要升级时,只需将代理合约指向的逻辑合约地址改为新版本。用户的地址、余额等状态都保留在代理合约中。

// 简化版透明代理
contract Proxy {
    address public implementation; // 逻辑合约地址
    address public admin;

    fallback() external payable {
        address impl = implementation;
        assembly {
            // 将调用转发给逻辑合约,使用 delegatecall
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }

    function upgrade(address newImplementation) external {
        require(msg.sender == admin, "Not admin");
        implementation = newImplementation;
    }
}

代理模式的主要方案:


8.7 OpenZeppelin:智能合约的标准库

就像 C++ 有 STL、Python 有 NumPy,智能合约开发也有标准库:OpenZeppelin

OpenZeppelin 提供了一套经过严格审计的合约模板,涵盖:

使用 OpenZeppelin 实现一个有铸造和燃烧功能的 ERC-20 代币只需几行:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyAdvancedToken is ERC20, Ownable {
    constructor(uint256 initialSupply) ERC20("MyAdvancedToken", "MAT") {
        _mint(msg.sender, initialSupply * 10 ** decimals());
    }

    // 只有 owner 可以铸造新代币
    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }

    // 任何人可以销毁自己的代币
    function burn(uint256 amount) public {
        _burn(msg.sender, amount);
    }
}

8.8 智能合约的实际应用场景

让我们脱离代码,从应用层面看智能合约能做什么。

去中心化交易所(DEX)

传统交易所(中心化):你把资产存入交易所,交易所用数据库撮合买卖双方,你信任交易所不跑路、不作假账。

去中心化交易所(如 Uniswap):资产锁定在智能合约中,交易逻辑完全由合约执行,任何人都可以验证规则,没有中心方可以"跑路"。

Uniswap v2 的核心公式就是一个简单的数学不变量:

$x \cdot y = k$

$x$ 和 $y$ 是流动性池中两种代币的数量,$k$ 是常数。买入一种代币时,另一种代币数量增加,乘积保持不变。价格由供需自动决定。

链上借贷(如 Aave、Compound)

用户存入抵押品,智能合约根据抵押率自动决定可借出的金额。当抵押品价值跌破清算线时,合约自动清算。整个过程无需银行、无需信用评分。

多签钱包(Multisig)

$m/n$ 多签:需要 $m$ 个私钥中的 $n$ 个签名才能执行交易。常用于公司资金管理、DAO 金库。一人拿走资金或一人私钥泄露都无法造成损失。

预言机(Oracle)

区块链是封闭系统,合约无法直接获取链外数据(股价、天气、体育赛果)。预言机(如 Chainlink)是连接链内外的桥梁:由去中心化网络的节点汇报真实世界数据,并通过加密经济学激励保证数据可信性。


8.9 "代码即法律"的哲学反思

尼克·萨博的"代码即法律"(Code is Law)有一个极端解读:合约一旦部署,其行为就是最终裁决,哪怕结果看起来不公平。

2016年的 DAO 事件是对这一理念的最大考验。黑客的行为在技术上完全符合合约代码——他只是调用了合约定义好的函数。但以太坊社区选择了硬分叉,否定了这笔交易。

这引发了一个深刻的问题:去中心化系统的最终权威是代码还是人?

持极端代码主义的人认为:任何人为干预都破坏了区块链的可信中性(Credible Neutrality)。今天可以为 DAO 分叉,明天可以为政府压力分叉。

持实用主义的人认为:代码也是人写的,有 Bug 不是真正的"法律意图"。在不可逆损失面前,人的判断应该优先。

这个争论至今没有答案,它本质上是关于规则与结果哪个更重要的古老哲学问题。

更现实的看法是:智能合约提供的是规则的确定性执行,而不是替代所有人类法律。智能合约最适合的场景是:

  1. 规则可以被精确编码(没有模糊性)
  2. 所有相关数据都在链上
  3. 结果的可验证性比灵活性更重要

对于需要主观判断、处理链外争议、或需要随时间演化的复杂关系,法律合同仍然是更好的工具。


8.10 开发工具链

现代以太坊开发的标准工具链:

Hardhat / Foundry:开发框架

测试网络

钱包

区块链浏览器

合约验证:部署后在 Etherscan 上验证合约源码,任何人都可以阅读实际运行的代码。这是去中心化信任的重要组成部分。

一个简单的 Hardhat 工作流:

// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");

module.exports = {
  solidity: "0.8.19",
  networks: {
    sepolia: {
      url: process.env.SEPOLIA_RPC_URL,
      accounts: [process.env.PRIVATE_KEY]
    }
  }
};
// scripts/deploy.js
const { ethers } = require("hardhat");

async function main() {
  const MyToken = await ethers.getContractFactory("MyToken");
  const token = await MyToken.deploy(1000000); // 100万初始供应
  await token.waitForDeployment();
  console.log("MyToken deployed to:", await token.getAddress());
}

main().catch(console.error);
npx hardhat compile       # 编译合约
npx hardhat test          # 运行测试
npx hardhat run scripts/deploy.js --network sepolia  # 部署到测试网

8.11 智能合约的 Gas 优化

在以太坊上,Gas 直接等于钱。Gas 优化是一门专门的技艺,几个核心原则:

1. 减少存储操作

存储(SSTORE)是最贵的操作之一。能用内存变量就不用存储变量;能用事件记录历史就不要用数组存历史。

2. 变量打包

EVM 以32字节(一个存储槽)为单位读写。多个小变量如果总大小不超过32字节,可以打包到同一个槽:

// Gas 低效:占用 3 个存储槽(3 × 32字节)
uint256 a; // 槽 0
uint128 b; // 槽 1(浪费了128位)
uint128 c; // 槽 2

// Gas 高效:占用 2 个存储槽
uint256 a; // 槽 0
uint128 b; // 槽 1 前16字节
uint128 c; // 槽 1 后16字节(b 和 c 合并!)

3. 用 calldata 替代 memory

对于外部函数的只读数组参数,用 calldatamemory 便宜:

// 更贵
function sum(uint256[] memory arr) external pure returns (uint256) { ... }

// 更便宜
function sum(uint256[] calldata arr) external pure returns (uint256) { ... }

4. 短路求值

require(cheapCondition && expensiveCondition) 中,把便宜的检查放前面,一旦前面失败,后面的不会执行。

5. 使用 unchecked

在确保不会溢出的场合,用 unchecked 关闭自动溢出检查:

for (uint256 i = 0; i < array.length; ) {
    // 处理 array[i]
    unchecked { ++i; } // ++i 比 i++ 稍便宜;unchecked 更省 Gas
}

本章小结

智能合约是部署在区块链上的自动执行程序,以 Solidity 为主要开发语言,在 EVM 上运行,通过 Gas 机制定价计算资源。合约安全是重中之重——重入攻击、整数溢出等漏洞曾造成数亿美元的损失,Checks-Effects-Interactions 模式和 OpenZeppelin 标准库是防御的基础。"代码即法律"既是智能合约的力量来源,也是它最深刻的哲学张力所在。


第9章 共识机制的演化:从 PoW 到 PoS

9.1 共识问题:为什么难?

我们先退一步,重新审视区块链要解决的根本问题。

想象一个简单的场景:10个互不认识的人,分散在世界各地,都持有一份账本的副本。现在有人想记录一笔交易"Alice 转给 Bob 5 ETH"。问题是:

  1. 谁有权记账? 如果任何人都可以随意写,系统会被垃圾信息淹没
  2. 双花攻击 :Alice 同时广播"我给 Bob 5 ETH"和"我给 Carol 5 ETH",哪个算数?
  3. 节点故障与恶意:部分节点掉线、发送错误信息怎么办?

这是计算机科学中著名的拜占庭将军问题(Byzantine Generals Problem)

Leslie Lamport 在1982年提出这个问题:几个将军要协调进攻一座城市,通过信使传递消息,但部分将军可能是叛徒,会发送虚假信息。如何在存在叛徒的情况下达成共识?

可以证明,在异步网络中,如果超过1/3的节点是恶意的,共识无法达成(拜占庭容错,BFT)。

不同的区块链共识机制,本质上都是对这个问题的不同工程权衡——在安全性、去中心化程度、可扩展性之间寻找平衡。


9.2 工作量证明(PoW):用物理世界约束数字世界

比特币的 PoW 是一个天才的设计,它通过引入现实世界的资源消耗来解决数字世界的信任问题。

核心思想:让记账权的获取变得"昂贵"。如果想作恶,必须花费大量电力,而这些电力是真实的经济成本。正直行事的奖励(区块奖励 + 交易费)远超作恶收益,系统在博弈论意义上是稳定的。

PoW 的工作流程(比特币为例):

  1. 矿工收集待确认交易,打包成候选区块
  2. 区块头包含:父区块哈希、Merkle Root、时间戳、难度目标(target)、nonce(待填入的随机数)
  3. 矿工不断尝试不同的 nonce,计算: $H = \text{SHA256}(\text{SHA256}(\text{blockHeader}))$
  4. 直到找到一个 nonce 使得 $H < \text{target}$
  5. 广播成功的区块,其他节点验证(一次 SHA256 运算即可),接受后继续在其后挖矿

难度调整:每2016个区块(约2周),比特币根据实际出块时间调整难度目标,使平均出块时间保持在10分钟。

数学直觉:哈希函数的输出均匀分布在 $[0, 2^{256})$,target 越小,满足条件的哈希越少,难度越高。当前比特币难度约为 $10^{23}$,意味着每次尝试成功概率约为 $10^{-23}$。全网每秒尝试约 $5 \times 10^{20}$ 次哈希(2024年数据)。

PoW 的安全性

51% 攻击:如果一个攻击者控制超过全网50%的算力,可以进行双花攻击——先广播一笔交易给商家(支付),等商家发货后,私下用更强的算力挖一条更长的链,其中不包含该支付交易,从而让原始交易被撤销,资金回到自己手中。

但发动51%攻击的成本极高。以比特币为例,2024年全网算力约 $5 \times 10^{20}$ H/s,攻击者需要购买大量矿机(硬件成本数十亿美元),消耗大量电力,而一旦攻击成功,比特币的价值可能暴跌,攻击者持有的BTC也变得一文不值。这是 PoW 的博弈论均衡。


9.3 PoW 的局限性

然而,PoW 存在几个根本性的问题:

1. 能源消耗

比特币每年的电力消耗约为 100~150 TWh(太瓦时),相当于荷兰、阿根廷这类中等国家的全年用电量。这个数字引发了持续的环保争议。

支持者的反驳:

反对者的反驳:

2. 专业化矿机造成中心化

中本聪最初设想用普通 CPU 挖矿(人人参与)。但经济激励驱动了矿机的军备竞赛:

ASIC 矿机极其昂贵,只有大型矿场才能负担。加之挖矿利润薄、需要廉价电力,矿场高度集中于中国(2021年前)、美国、哈萨克斯坦等电价低廉的地区。

某个时期,全球前四大矿池控制了超过50%的比特币算力,这与去中心化理想大相径庭。

3. 可扩展性瓶颈

比特币每秒约7笔交易(TPS),以太坊 PoW 时代约15 TPS。而 Visa 峰值可处理约65000 TPS。PoW 的区块时间和大小限制了吞吐量。


9.4 权益证明(PoS):用经济质押代替算力竞争

核心思想:不用算力竞争记账权,而是用质押代币获得记账资格。想作恶的人需要先质押大量代币,一旦作恶被检测到,质押的代币被惩罚销毁(Slashing)

PoW 的安全性来源:物理资源(电力、矿机) PoS 的安全性来源:经济权益(质押代币)

两种机制的博弈论本质是相似的:作恶成本 > 作恶收益。

以太坊 PoS(Gasper 协议)

以太坊的 PoS 实现相当复杂,是 Casper FFG(最终性机制)和 LMD-GHOST(分叉选择规则)的组合,合称 Gasper。我们来理解其主要概念:

验证者(Validator):质押32 ETH即可成为验证者(截至2024年,已有超过100万验证者)。质押通过调用信标链上的存款合约完成。

Epoch 和 Slot:时间被划分为 Epoch,每个 Epoch 包含32个 Slot,每个 Slot 12秒。因此每个 Epoch 约6.4分钟。

每个 Slot,协议从验证者池中随机选出:

这种随机性是关键——攻击者无法提前知道自己何时成为提议者,无法针对性攻击。随机数来自 RANDAO,通过聚合所有验证者的随机贡献生成。

LMD-GHOST 分叉选择规则

GHOST(Greedy Heaviest-Observed SubTree):当出现分叉时,选择累积权重(投票数 × 质押量)最重的分支,而非最长的链(比特币的规则)。

LMD(Latest Message Driven):只考虑每个验证者最新的投票,防止旧投票被利用于攻击。

Casper FFG 最终性(Finality)

LMD-GHOST 解决了分叉选择,但不提供"最终性"——理论上任何区块都可能被重组。Casper FFG 在上面增加了一层最终性协议:

$\text{最终确定} \Rightarrow \text{回滚代价} \geq \frac{1}{3} \times \text{总质押量}$

截至2024年,以太坊总质押量超过3400万 ETH(约1000亿美元),攻击代价极高。


9.5 Slashing:惩罚机制

PoS 的关键创新是 **Slashing(惩罚销毁)**机制。

当验证者做出以下恶意行为时,其质押的 ETH 会被部分没收:

1. 双重提议(Double Proposal):在同一个 Slot 提议两个不同的区块

2. 双重投票(Double Vote / Surround Vote):对同一个 Epoch 投两个相互矛盾的票,或投出包围(surround)先前投票的票

被惩罚的验证者会:

  1. 立即损失一部分质押(初始惩罚约1/32)
  2. 被强制退出验证者池,进入36天的退出队列
  3. 如果在同一时期有大量验证者同时被惩罚,额外的相关惩罚会更大("相关惩罚系数",最高可达全部质押)

这个设计非常精妙:孤立的技术故障(一台机器崩溃后另一台重启,发出了重复签名)损失很小;而协调一致的攻击(大量验证者同时作恶)损失极大。


9.6 委托权益证明(DPoS)与其他变体

以太坊的 PoS 需要质押32 ETH(约10万美元),普通用户无法参与验证。这促生了流动质押协议(如 Lido、Rocket Pool),用户质押任意数量的 ETH,获得流动性代币(stETH)。

但 Lido 等协议占据了超过30%的质押份额,这本身就是新的中心化风险。

DPoS(Delegated Proof of Stake):EOS、Tron 等链使用的变体。代币持有者投票选出21个(或类似数量的)超级节点(Block Producers),只有这些节点有记账权。

优点:出块速度极快(毫秒级),吞吐量高(数千 TPS) 缺点:高度中心化,21个超级节点本质上是寡头,容易被操控或勾结

Nominated Proof of Stake(NPoS):Polkadot 使用。代币持有者提名验证者,协议根据提名分布选出验证者集合,同时最大化安全性和去中心化。

Proof of Authority(PoA):由预先批准的验证者轮流出块。高效、便宜,但完全中心化。适用于企业联盟链、测试网。

Proof of History(PoH):Solana 独创。不是共识机制本身,而是一种时间证明——通过可验证的顺序延迟函数(VDF)在链上建立时间顺序,大幅减少共识所需的消息传递。Solana 实现了数千 TPS,但节点硬件要求极高,去中心化程度存疑。


9.7 "区块链不可能三角"

2017年,Vitalik Buterin 提出了著名的可扩展性三难困境(Blockchain Trilemma)

$\text{安全性(Security)} \quad \text{去中心化(Decentralization)} \quad \text{可扩展性(Scalability)}$

任何区块链系统在现有技术条件下,至多只能同时做好其中两项

系统 优先保证 牺牲
比特币 安全性 + 去中心化 可扩展性(7 TPS)
EOS/DPoS 安全性 + 可扩展性 去中心化(21个超级节点)
早期 PoA 侧链 可扩展性 + 去中心化(表面) 安全性(依赖信任)

以太坊的解决策略是:Layer 1 保证安全性和去中心化,Layer 2 处理可扩展性

这就是以太坊的 Rollup 中心路线图

Rollup 的基本思想:在链下(Layer 2)执行大量交易,定期将交易结果的压缩证明提交到以太坊主链(Layer 1)。主链只做最终裁决和数据存储,不做具体计算。

主要有两种 Rollup:

Optimistic Rollup(乐观型)

ZK-Rollup(零知识证明型)

EIP-4844(Proto-Danksharding):2024年坎昆升级引入,增加了 Blob 交易类型,专门为 Rollup 提供临时数据存储,成本约为原来的1/10。Rollup 的 Gas 费用大幅降低(从数美元降至几分钱)。


9.8 The Merge:一次工程奇迹

以太坊从 PoW 切换到 PoS 的过程,被称为 The Merge(合并),发生在2022年9月15日。

这次迁移面临的挑战是空前的:

工程师的方案是:不停机切换。信标链(Beacon Chain)从2020年12月就开始独立运行,积累了超过10万验证者。The Merge 的时刻,以太坊主网在一个特定的 PoW 总难度(Terminal Total Difficulty,TTD)后,直接切换到由信标链提议的区块。

整个切换过程:

  1. 以太坊主网继续出 PoW 区块,直到总累积难度达到 TTD(约5.875 × 10^22)
  2. 达到 TTD 的那个区块成为最后一个 PoW 区块
  3. 下一个区块由信标链的验证者提议,通过 PoS 共识产生
  4. 系统无缝切换,普通用户几乎感觉不到

The Merge 的直接效果:


9.9 PoS 的争议与批评

PoS 并非没有争议,来自比特币社区的批评尤为尖锐:

"富者愈富":质押更多 ETH 获得更多奖励,大持有者的市场份额会不断增长,长期看可能导致寡头垄断。

反驳:PoW 同样"算力愈富愈强"——大矿场规模优势明显,小矿工早已被边缘化。

"无利害关系"问题(Nothing-at-Stake): 这是 PoS 早期的一个理论问题:在 PoW 中,矿工只能在一条链上挖矿(算力不可分割);但在早期朴素的 PoS 设计中,验证者可以同时在所有分叉上投票,因为投票几乎没有成本。这意味着验证者没有动力收敛到单一链。

解决方案:Slashing 机制。双重投票会被惩罚销毁,验证者不得不选择一条链。

"长程攻击"(Long-Range Attack): 攻击者如果拥有历史上某个时点的大量私钥,可以从那个时点重写整条历史链,因为历史区块的重写不需要消耗当前的物理资源。

解决方案:弱主观性(Weak Subjectivity)。新加入的节点需要从可信来源获取一个近期的"检查点",而不是从创世区块开始验证。这要求节点至少偶尔上线,而不是完全离线数年后再接入。

中心化压力: 液态质押协议(如 Lido)占据了以太坊质押的30%以上。以太坊基金会认为这是一个严重的风险,但去中心化社区对此无法强行干预。


9.10 其他共识机制简述

区块链生态系统还有很多其他的共识设计,简要介绍几个有代表性的:

BFT 类共识(如 Tendermint/PBFT)

用于 Cosmos、BNB Chain 等。基于拜占庭容错(BFT)的经典研究,通过多轮投票(Propose → PreVote → PreCommit)在一个 Round 内达成共识。

特点:即时最终性(一旦提交,永不回滚),吞吐量高,但验证者数量不能太多(消息复杂度 $O(n^2)$),适合数十到数百个验证者。

雪崩协议(Avalanche / Snowflake)

Avalanche 链使用一种基于亚稳态(Metastability)的概率共识:每个节点随机采样一组其他节点,采纳多数意见,反复迭代。系统以极高概率快速收敛到一致状态。

理论上支持数千个验证者,同时保持高吞吐量和低延迟。

Ouroboros(Cardano 的 PoS)

学术驱动的 PoS 协议,有严格的密码学安全证明。将时间划分为 Epoch 和 Slot,通过安全的多方计算(Secure Multiparty Computation)生成随机数,公平选出每个 Slot 的出块人。


9.11 共识机制选择的本质

回到最开始的问题:什么是好的共识机制?

没有普遍最优解。共识机制的选择本质上是一组价值取舍

你最在乎什么?

一个有趣的思考框架是:共识机制本质上是谁有权参与决策的问题。

这与现实世界的治理结构惊人地相似:金本位(消耗实物资源)、股东投票(权益决定权力)、代议制民主(委托代表)、精英任命制(预先批准的权威)。

区块链不是在创造全新的政治哲学,而是在用密码学和博弈论重新实现那些古老的治理模式——只是在不信任的网络中。


9.12 以太坊的未来:走向 Danksharding

以太坊的共识机制演化还没有结束。当前路线图(由 Vitalik 等人规划)分为几个阶段:

完整 Danksharding 是路线图的核心目标:通过数据分片(Sharding),让不同验证者负责不同数据片,同时通过**数据可用性采样(DAS)**确保任何人都可以随机验证数据存在而不需要下载全部数据。

目标是使以太坊的 Layer 1 数据带宽提升到每个 Slot 约1MB(当前约100KB),配合 Rollup,使整体系统吞吐量达到10万+ TPS。

这是一场漫长的工程旅程,但方向是清晰的:安全和去中心化作为基础,可扩展性在上层解决


本章小结

共识机制是区块链的"宪法",规定谁有权写入历史、违规如何惩罚。PoW 用物理能源消耗建立信任,安全但高耗能;PoS 用经济权益质押建立信任,高效但需要精心设计的激励机制(尤其是 Slashing)来防范恶意行为。以太坊的 The Merge 是人类历史上最复杂的不停机系统迁移之一,其成功证明了去中心化系统在极大工程挑战下依然可以演化——而"区块链不可能三角"则提醒我们,可扩展性终究需要在 Layer 2 而非共识层去解决。