第二章:以太坊架构与核心概念
2.1 以太坊概述
以太坊(Ethereum)是由Vitalik Buterin在2013年提出的区块链平台,于2015年正式启动。与比特币专注于数字货币不同,以太坊的目标是成为"世界计算机",提供一个图灵完备的去中心化计算平台。
2.1.1 以太坊的设计理念
以太坊的核心设计理念包括:
- 通用性:不仅是货币,而是可编程的区块链平台
- 图灵完备:支持任意复杂的计算逻辑
- 智能合约:自动执行的程序,无需中介
- 去中心化应用:DApp可在以太坊上运行
- 开发者友好:提供完善的开发工具和生态
2.1.2 以太坊与比特币的核心区别
| 特性 | 比特币 | 以太坊 |
|---|---|---|
| 定位 | 数字货币 | 智能合约平台 |
| 账本模型 | UTXO模型 | 账户模型 |
| 脚本语言 | 非图灵完备 | 图灵完备(Solidity等) |
| 出块时间 | 约10分钟 | 约12秒 |
| 共识机制 | PoW(SHA-256) | PoW → PoS(2022年合并) |
| 状态存储 | 简单(UTXO集) | 复杂(世界状态树) |
| Gas机制 | 无 | 有(防止无限循环) |
| 供应上限 | 2100万枚 | 无硬性上限 |
2.1.3 以太坊的发展历程
以太坊经历了多个重要阶段:
Frontier(前沿,2015年7月)
- 初始版本,仅供开发者测试
- PoW共识,Ethash算法
- 基础功能上线
Homestead(家园,2016年3月)
- 首个稳定版本
- 移除"Canary"合约
- 改进Gas机制
Metropolis(大都会)
- Byzantium(拜占庭,2017年10月):引入zk-SNARKs,难度炸弹延迟
- Constantinople(君士坦丁堡,2019年2月):优化Gas成本,区块奖励降至2 ETH
The Merge(合并,2022年9月)
- 从PoW切换到PoS
- 信标链与主网合并
- 能耗降低99.95%
Shanghai/Capella(上海升级,2023年4月)
- 启用质押提款功能
- 优化EVM性能
未来计划:
- The Surge:通过分片提升扩展性
- The Scourge:去中心化排序,抗MEV
- The Verge:无状态客户端,Verkle树
- The Purge:简化协议,清理历史数据
- The Splurge:其他重要升级
2.2 账户模型详解
以太坊采用账户模型(Account Model),这是与比特币UTXO模型的最大区别之一。
2.2.1 账户类型
以太坊有两种账户类型:
外部账户(EOA - Externally Owned Account)
结构:
{
Nonce: 交易计数器
Balance: 以太币余额(单位:Wei)
StorageRoot: 空(0x)
CodeHash: 空代码哈希
}
特点:
- 由私钥控制
- 可以发起交易
- 无关联代码
- 创建账户无需Gas
合约账户(Contract Account)
结构:
{
Nonce: 创建的合约数量
Balance: 以太币余额
StorageRoot: 存储树根哈希(状态变量)
CodeHash: 合约代码哈希
}
特点:
- 由代码控制
- 不能主动发起交易
- 包含智能合约代码
- 创建需要Gas费用
2.2.2 账户地址生成
EOA地址生成:
1. 生成私钥(256位随机数)
PrivateKey = random(256 bits)
2. 计算公钥(使用secp256k1)
PublicKey = PrivateKey × G
3. 计算公钥哈希
Hash = Keccak256(PublicKey)
4. 取后20字节作为地址
Address = Hash[12:32]
示例:
私钥: 0x1234567890abcdef...
公钥: 0x04xyz...(64字节)
哈希: 0xabc...def(32字节)
地址: 0x...(20字节,40个十六进制字符)
合约地址生成:
方式1:CREATE操作码
Address = Keccak256(RLP(sender, nonce))[12:32]
方式2:CREATE2操作码(确定性部署)
Address = Keccak256(0xff, sender, salt, Keccak256(bytecode))[12:32]
优势:
- CREATE2允许预先计算地址
- 适用于工厂模式和跨链部署
2.2.3 账户模型的优势与劣势
优势:
- 简洁直观:余额直接记录在账户中,易于理解
- 状态管理:天然支持复杂状态存储(智能合约)
- Gas优化:不需要查找和验证多个UTXO
- 原子性:交易要么全部成功,要么全部失败
- 可编程性:合约可以维护复杂的内部状态
劣势:
- 隐私性较弱:所有余额变化都关联到同一地址
- 并发性受限:同一账户的交易必须按Nonce顺序执行
- 状态膨胀:全局状态不断增长
- 重放攻击:需要Nonce和ChainID防护
2.3 世界状态与状态树
2.3.1 世界状态(World State)
以太坊的世界状态是所有账户状态的集合,可以理解为一个巨大的键值对映射:
WorldState = {
Address1 → Account1,
Address2 → Account2,
...
AddressN → AccountN
}
每个Account包含:
{
nonce: uint64
balance: *big.Int
storageRoot: Hash (指向存储树)
codeHash: Hash (合约代码哈希)
}
世界状态在每个区块执行后都会更新,形成新的状态根(State Root)。
2.3.2 Merkle Patricia Trie(MPT)
以太坊使用改进的Merkle树结构:Merkle Patricia Trie(MPT),结合了Merkle树和Patricia树的优点。
MPT特点:
- 确定性:相同数据总是产生相同的树结构
- 高效验证:通过Merkle证明验证数据存在性
- 压缩路径:Patricia树压缩相同前缀
- 动态更新:支持高效的增删改查
MPT节点类型:
// 扩展节点(Extension Node)
type ExtensionNode struct {
SharedNibbles []byte // 共享的路径片段
Next Hash // 下一个节点的哈希
}
// 分支节点(Branch Node)
type BranchNode struct {
Branches [16]Hash // 16个分支(0-F)
Value []byte // 可选的值(如果路径在此结束)
}
// 叶子节点(Leaf Node)
type LeafNode struct {
Path []byte // 剩余路径
Value []byte // 实际数据
}
MPT示例:
存储以下键值对:
"dog" → "puppy"
"dodge" → "coin"
"horse" → "stallion"
转换为十六进制路径:
"dog" → 0x646f67
"dodge" → 0x646f646765
"horse" → 0x686f727365
构建的MPT结构:
Root
|
Extension(0x)
|
Branch Node
/ \
(0x64) (0x68)
| |
Extension(0x6f) Extension(0x6f727365)
| |
Branch Node Leaf("" → "stallion")
/ \
(0x67) (0x646765)
| |
Leaf Leaf
("" → ("" → "coin")
"puppy")
2.3.3 三种状态树
以太坊实际维护三种MPT:
1. 状态树(State Trie)
Key: Keccak256(Address)
Value: RLP([nonce, balance, storageRoot, codeHash])
作用:存储所有账户的基本信息
根哈希:包含在区块头的stateRoot字段
2. 存储树(Storage Trie)
Key: Keccak256(StorageKey)
Value: StorageValue
作用:每个合约账户都有自己的存储树
根哈希:包含在账户的storageRoot字段
示例:
合约状态变量:
mapping(address => uint256) balances;
存储位置:
key = Keccak256(address, slot)
value = balance
3. 交易树(Transaction Trie)
Key: RLP(transactionIndex)
Value: Transaction Data
作用:存储区块内的所有交易
根哈希:包含在区块头的transactionRoot字段
特点:每个区块一个独立的交易树
4. 收据树(Receipt Trie)
Key: RLP(transactionIndex)
Value: Transaction Receipt
Receipt包含:
- Status(成功/失败)
- CumulativeGas(累计Gas)
- Logs(事件日志)
- Bloom Filter(日志布隆过滤器)
根哈希:包含在区块头的receiptRoot字段
2.3.4 状态根的作用
Block Header {
...
StateRoot: 世界状态树根哈希
TransactionRoot: 交易树根哈希
ReceiptRoot: 收据树根哈希
...
}
状态根的重要性:
- 完整性验证:一个哈希代表整个世界状态
- 轻节点验证:只需区块头即可验证状态
- 历史回溯:可以查询任意历史区块的状态
- 共识保证:所有节点必须达成相同的状态根
状态转换:
StateRoot_N = ApplyTransactions(StateRoot_N-1, Transactions_N)
执行流程:
1. 从StateRoot_N-1加载世界状态
2. 按顺序执行Transactions_N中的每笔交易
3. 更新受影响账户的状态
4. 计算新的StateRoot_N
5. 所有节点必须得到相同的StateRoot_N
2.4 交易结构与生命周期
2.4.1 交易结构
以太坊交易包含以下字段:
type Transaction struct {
Nonce uint64 // 发送者交易计数
GasPrice *big.Int // Gas价格(Wei/Gas)
GasLimit uint64 // Gas上限
To *Address // 接收地址(nil表示合约创建)
Value *big.Int // 转账金额(Wei)
Data []byte // 交易数据/合约调用
V, R, S *big.Int // ECDSA签名
}
// EIP-1559后的新交易类型
type DynamicFeeTx struct {
ChainID *big.Int
Nonce uint64
GasTipCap *big.Int // 小费上限(优先费)
GasFeeCap *big.Int // Gas费用上限
GasLimit uint64
To *Address
Value *big.Int
Data []byte
AccessList []AccessTuple // EIP-2930访问列表
V, R, S *big.Int
}
交易类型:
- 转账交易:To指向EOA,Data为空
- 合约调用:To指向合约地址,Data包含函数调用
- 合约创建:To为nil,Data包含合约字节码
2.4.2 交易签名
以太坊使用ECDSA签名,并包含ChainID防止重放攻击:
签名过程:
1. 构造交易数据
txData = RLP([nonce, gasPrice, gasLimit, to, value, data, chainID, 0, 0])
2. 计算交易哈希
txHash = Keccak256(txData)
3. 使用私钥签名
(r, s, v) = ECDSA_Sign(privateKey, txHash)
4. 计算恢复标识
v = recoveryID + 35 + 2 × chainID (EIP-155)
验证过程:
1. 从签名恢复公钥
publicKey = ECDSA_Recover(txHash, v, r, s)
2. 从公钥计算地址
sender = Keccak256(publicKey)[12:32]
3. 验证发送者
确保sender有足够余额和正确的nonce
EIP-155(防重放攻击):
主网:chainID = 1
Ropsten测试网:chainID = 3
Rinkeby测试网:chainID = 4
Goerli测试网:chainID = 5
Sepolia测试网:chainID = 11155111
v值计算:
v = {27, 28} (原始ECDSA)
v = {35 + 2×chainID, 36 + 2×chainID} (EIP-155)
主网示例:
v ∈ {37, 38} (35 + 2×1, 36 + 2×1)
2.4.3 交易生命周期
完整的交易生命周期包括以下阶段:
1. 创建与签名
// Web3.js示例
const tx = {
from: '0xSenderAddress',
to: '0xReceiverAddress',
value: web3.utils.toWei('1', 'ether'),
gas: 21000,
gasPrice: web3.utils.toWei('50', 'gwei'),
nonce: await web3.eth.getTransactionCount(senderAddress)
};
const signedTx = await web3.eth.accounts.signTransaction(tx, privateKey);
2. 广播到网络
用户 → 连接节点 → 内存池(Mempool) → 全网传播
广播协议:
1. 使用RLPx协议发送交易
2. 节点验证交易有效性
3. 加入本地待处理交易池
4. 通过gossip协议传播
3. 进入交易池(Mempool)
交易池管理:
1. 待处理队列(Pending):可执行的交易
2. 队列(Queued):Nonce不连续的交易
3. 优先级排序:按Gas价格排序
4. 容量限制:防止内存溢出
验证规则:
- 签名有效性
- Nonce正确性(≥ 账户当前nonce)
- 余额充足(value + gasLimit × gasPrice)
- Gas价格满足最低要求
- 交易格式正确
4. 矿工/验证者选择
交易选择策略:
1. 按Gas价格降序排序
2. 优先选择高Gas价格交易
3. 考虑Nonce顺序(同一账户)
4. 限制区块Gas总量(Gas Limit)
EIP-1559后:
1. 基础费用(Base Fee)自动调整
2. 优先费(Priority Fee)给矿工
3. 用户设置最大费用(Max Fee)
4. 超出基础费用部分被销毁
5. 执行与打包
执行流程:
1. 检查发送者余额和nonce
2. 扣除初始Gas费用(gasLimit × gasPrice)
3. 执行交易(转账或合约调用)
4. 计算实际Gas消耗
5. 退还未使用的Gas
6. 更新账户状态(余额、nonce、存储)
7. 生成收据和日志
8. 更新状态根
伪代码:
if sender.balance < value + gasLimit × gasPrice {
return "Insufficient funds"
}
if sender.nonce != tx.nonce {
return "Invalid nonce"
}
sender.balance -= gasLimit × gasPrice
gasUsed = ExecuteTransaction(tx)
sender.balance += (gasLimit - gasUsed) × gasPrice
sender.nonce += 1
6. 确认与最终性
确认阶段:
- 0确认:交易在内存池中
- 1确认:交易被打包进区块
- 6确认:相对安全(约72秒)
- 32确认:高安全性(约6.4分钟)
PoS后的最终性:
- Justified:2/3验证者投票确认
- Finalized:连续两个Epoch被确认(约12.8分钟)
- 最终性后交易不可逆
2.4.4 Nonce机制
Nonce是防止重放攻击和保证交易顺序的关键:
Nonce特性:
1. 从0开始,每笔交易递增1
2. 必须连续,不能跳过
3. 同一账户的交易按Nonce顺序执行
示例:
当前账户nonce = 5
情况1:发送nonce=5的交易 → 立即执行
情况2:发送nonce=6的交易 → 等待nonce=5执行
情况3:发送nonce=4的交易 → 拒绝(已使用)
问题场景:
用户发送:
Tx1 (nonce=10, gasPrice=20 Gwei)
Tx2 (nonce=11, gasPrice=50 Gwei)
即使Tx2 Gas更高,也必须等Tx1执行
加速/取消交易:
方法1:提高Gas价格(Replace-by-Fee)
- 发送相同nonce的交易
- 提高Gas价格(至少提高10%)
- 新交易会替换旧交易
示例:
原交易:nonce=10, gasPrice=20 Gwei
新交易:nonce=10, gasPrice=22 Gwei (提高10%)
方法2:取消交易
- 发送nonce相同、to为自己、value=0的交易
- 使用更高的Gas价格
- 占用该nonce位置,原交易失效
2.5 Gas机制深度解析
2.5.1 为什么需要Gas
Gas机制是以太坊的核心创新,解决以下问题:
- 防止无限循环:图灵完备导致的停机问题
- 资源计量:量化计算、存储、带宽消耗
- 经济激励:补偿矿工/验证者的资源消耗
- 防止垃圾交易:提高攻击成本
- 优先级排序:市场化的交易排序机制
图灵完备的代价:
// 危险的无限循环(无Gas会导致网络瘫痪)
contract Dangerous {
function infiniteLoop() public {
while(true) {
// 永远不会停止
}
}
}
// Gas机制解决方案
// 当Gas耗尽,交易回滚,状态不变
2.5.2 Gas的基本概念
核心概念:
1. Gas:计量单位,衡量计算复杂度
2. Gas Price:用户愿意为每单位Gas支付的价格(Gwei)
3. Gas Limit:用户愿意为交易支付的最大Gas数量
4. Gas Used:实际消耗的Gas数量
计算公式:
交易费用 = Gas Used × Gas Price
单位换算:
1 Ether = 10^9 Gwei = 10^18 Wei
1 Gwei = 10^9 Wei
示例计算:
简单转账:
Gas Used: 21,000
Gas Price: 50 Gwei
Total Fee = 21,000 × 50 = 1,050,000 Gwei
= 0.00105 ETH
≈ $2 (假设ETH价格=$2000)
复杂合约调用:
Gas Used: 150,000
Gas Price: 100 Gwei
Total Fee = 150,000 × 100 = 15,000,000 Gwei
= 0.015 ETH
≈ $30
2.5.3 Gas消耗明细
不同操作消耗的Gas量:
基础操作:
ADD/SUB/MUL/DIV : 3-5 Gas
KECCAK256 (哈希) : 30 Gas + 6 Gas/word
BALANCE (查询余额) : 2600 Gas (冷访问)
SLOAD (读取存储) : 2100 Gas (冷访问), 100 Gas (热访问)
SSTORE (写入存储) : 20,000 Gas (从零到非零)
: 5,000 Gas (修改非零值)
: 2,900 Gas (删除,退还15,000)
CALL (外部调用) : 2600 Gas + 传输成本
CREATE (创建合约) : 32,000 Gas + 部署代码成本
SELFDESTRUCT : 5,000 Gas (有退款)
交易基础成本:
Base Fee : 21,000 Gas (所有交易)
Calldata (零字节) : 4 Gas/byte
Calldata (非零字节) : 16 Gas/byte
合约创建额外成本 : 32,000 Gas
示例:计算合约调用Gas
contract Example {
uint256 public value; // 存储插槽0
function setValue(uint256 _value) public {
value = _value;
}
}
调用setValue(123)的Gas消耗:
1. 基础交易成本:21,000 Gas
2. Calldata:
- 函数选择器:4字节 × 16 = 64 Gas
- 参数(123):32字节 × 16 = 512 Gas
3. SSTORE操作:
- 从0改为123:20,000 Gas (假设初始为0)
4. 执行开销:约2,000 Gas
总计:≈ 43,576 Gas
2.5.4 EIP-1559:新Gas模型
2021年8月的伦敦升级引入EIP-1559,彻底改变了Gas机制:
旧模型(拍卖模式):
用户设置Gas Price(出价)
矿工选择高价交易
费用全部给矿工
价格波动大,难以预测
新模型(EIP-1559):
组成部分:
1. Base Fee(基础费用):
- 协议自动调整
- 随区块拥堵度动态变化
- 被销毁(Burn),不给验证者
2. Priority Fee(优先费/小费):
- 用户自定义
- 给验证者的激励
- 加速交易打包
3. Max Fee(最大费用):
- 用户愿意支付的上限
- 实际费用 = min(Base Fee + Priority Fee, Max Fee)
计算公式:
实际Gas Price = Base Fee + min(Priority Fee, Max Fee - Base Fee)
未使用部分退还用户
Base Fee调整算法:
def calculate_base_fee(parent_base_fee, parent_gas_used, parent_gas_limit):
"""
目标:保持区块50%满(15M Gas)
最大区块容量:30M Gas(2倍目标)
"""
target_gas = parent_gas_limit // 2
if parent_gas_used == target_gas:
return parent_base_fee
elif parent_gas_used > target_gas:
# 区块超过目标,增加Base Fee
gas_delta = parent_gas_used - target_gas
fee_delta = parent_base_fee * gas_delta // target_gas // 8
return parent_base_fee + max(fee_delta, 1)
else:
# 区块低于目标,降低Base Fee
gas_delta = target_gas - parent_gas_used
fee_delta = parent_base_fee * gas_delta // target_gas // 8
return parent_base_fee - fee_delta
# 每个区块最多变化12.5%(1/8)
# 平滑调整,避免剧烈波动
EIP-1559优势:
- 费用预测:Base Fee可预测,用户体验更好
- 通缩机制:销毁Base Fee,减少ETH供应
- 防止操纵:矿工无法通过打包自己的交易来哄抬价格
- 弹性区块:短期可容纳2倍Gas,处理峰值需求
示例:
当前Base Fee: 50 Gwei
用户设置:
Max Fee: 100 Gwei
Priority Fee: 2 Gwei
实际费用计算:
Gas Price = 50 (Base Fee) + 2 (Priority Fee) = 52 Gwei
退还金额 = (100 - 52) × Gas Used
如果下个区块Base Fee涨到70 Gwei:
Gas Price = 70 + 2 = 72 Gwei (仍低于100 Max Fee)
如果Base Fee涨到99 Gwei:
Gas Price = 99 + 1 = 100 Gwei (触达Max Fee上限)
2.5.5 Gas优化技巧
智能合约开发中的Gas优化策略:
1. 存储优化
// 不推荐:每次写入消耗大量Gas
contract Inefficient {
uint256[] public data;
function addMultiple(uint256[] memory values) public {
for(uint i = 0; i < values.length; i++) {
data.push(values[i]); // 每次SSTORE
}
}
}
// 推荐:批量操作
contract Efficient {
uint256[] public data;
function addMultiple(uint256[] memory values) public {
uint256 len = data.length;
for(uint i = 0; i < values.length; i++) {
data.push(); // 扩展数组
}
for(uint i = 0; i < values.length; i++) {
data[len + i] = values[i]; // 批量写入
}
}
}
// 最优:使用内存临时存储
contract Optimal {
function processData(uint256[] memory values) public pure returns(uint256) {
uint256 sum = 0;
for(uint i = 0; i < values.length; i++) {
sum += values[i]; // 内存操作,Gas低
}
return sum;
}
}
2. 变量打包
// 不推荐:3个存储槽(96 Gas读取)
contract Unoptimized {
uint256 a; // 槽0
uint256 b; // 槽1
uint256 c; // 槽2
}
// 推荐:1个存储槽(32 Gas读取)
contract Optimized {
uint128 a; // 槽0 (前16字节)
uint64 b; // 槽0 (中8字节)
uint64 c; // 槽0 (后8字节)
}
// 类型对齐
contract BetterPacked {
uint128 a;
uint128 b; // 与a共用槽0
address c; // 槽1(20字节)
uint96 d; // 槽1(12字节,填满)
}
3. 短路运算
// 推荐:便宜的条件在前
function check(uint256 value) public view returns(bool) {
return value > 0 && expensiveCheck(value);
// 如果value <= 0,不会调用expensiveCheck
}
// 不推荐:昂贵的条件在前
function checkBad(uint256 value) public view returns(bool) {
return expensiveCheck(value) && value > 0;
// 总是先执行expensiveCheck
}
4. 循环优化
// 不推荐
function sumArray(uint256[] memory arr) public pure returns(uint256) {
uint256 sum = 0;
for(uint256 i = 0; i < arr.length; i++) { // 每次读取arr.length
sum += arr[i];
}
return sum;
}
// 推荐
function sumArrayOptimized(uint256[] memory arr) public pure returns(uint256) {
uint256 sum = 0;
uint256 len = arr.length; // 缓存长度
for(uint256 i = 0; i < len; ++i) { // 使用++i而非i++
sum += arr[i];
}
return sum;
}
5. 事件代替存储
// 不推荐:存储历史记录(Gas高)
contract Expensive {
struct Record {
uint256 value;
uint256 timestamp;
}
Record[] public history;
function addRecord(uint256 value) public {
history.push(Record(value, block.timestamp));
}
}
// 推荐:使用事件(Gas低)
contract Cheap {
event RecordAdded(uint256 value, uint256 timestamp);
function addRecord(uint256 value) public {
emit RecordAdded(value, block.timestamp);
// 事件存储在日志中,Gas消耗远低于存储
}
}
2.6 以太坊虚拟机(EVM)
2.6.1 EVM架构
EVM是以太坊的核心,一个基于栈的虚拟机,负责执行智能合约代码。
EVM组件:
1. 程序计数器(PC):指向当前执行的指令
2. 栈(Stack):256位元素,最大深度1024
3. 内存(Memory):临时存储,按字节寻址,扩展式增长
4. 存储(Storage):持久化键值存储,每个合约独立
5. 代码(Code):不可变的合约字节码
6. Gas:剩余可用Gas
7. 调用栈深度:最大1024层
架构图:
┌─────────────────────────────────┐
│ EVM Instance │
├─────────────────────────────────┤
│ PC: 0x0000 │
│ Gas: 100000 │
│ Stack: [item1, item2, ...] │
│ Memory: [0x00, 0x01, ...] │
│ Storage: {key→value} │
│ Code: [0x60, 0x80, ...] │
└─────────────────────────────────┘
2.6.2 EVM指令集
EVM有140多个操作码(Opcode),分为以下类别:
1. 算术运算
ADD (0x01): a + b
MUL (0x02): a × b
SUB (0x03): a - b
DIV (0x04): a ÷ b (整数除法)
MOD (0x06): a % b
ADDMOD (0x08): (a + b) % N
MULMOD (0x09): (a × b) % N
EXP (0x0a): a^b
SIGNEXTEND (0x0b): 符号扩展
2. 比较与位运算
LT (0x10): a < b
GT (0x11): a > b
EQ (0x14): a == b
ISZERO (0x15): a == 0
AND (0x16): a & b
OR (0x17): a | b
XOR (0x18): a ^ b
NOT (0x19): ~a
BYTE (0x1a): 获取字节
SHL (0x1b): 左移
SHR (0x1c): 右移
SAR (0x1d): 算术右移
3. 栈操作
POP (0x50): 弹出栈顶
PUSH1-32(0x60-0x7f): 压入1-32字节常量
DUP1-16 (0x80-0x8f): 复制栈中第N个元素
SWAP1-16(0x90-0x9f): 交换栈顶与第N个元素
4. 内存操作
MLOAD (0x51): 从内存加载32字节
MSTORE (0x52): 存储32字节到内存
MSTORE8 (0x53): 存储1字节到内存
MSIZE (0x59): 获取内存大小
5. 存储操作
SLOAD (0x54): 从存储读取
SSTORE (0x55): 写入存储
6. 控制流
JUMP (0x56): 无条件跳转
JUMPI (0x57): 条件跳转
PC (0x58): 获取程序计数器
JUMPDEST(0x5b): 跳转目标标记
7. 区块信息
BLOCKHASH (0x40): 获取区块哈希
COINBASE (0x41): 矿工地址
TIMESTAMP (0x42): 区块时间戳
NUMBER (0x43): 区块号
DIFFICULTY (0x44): 难度(PoS后为PREVRANDAO)
GASLIMIT (0x45): 区块Gas限制
CHAINID (0x46): 链ID
BASEFEE (0x48): EIP-1559基础费用
8. 交易信息
ORIGIN (0x32): 原始发送者(EOA)
CALLER (0x33): 直接调用者
CALLVALUE (0x34): 调用附带的ETH
CALLDATALOAD(0x35): 加载调用数据
CALLDATASIZE(0x36): 调用数据大小
CALLDATACOPY(0x37): 复制调用数据
GASPRICE (0x3a): 交易Gas价格
9. 账户操作
ADDRESS (0x30): 当前合约地址
BALANCE (0x31): 查询余额
SELFBALANCE(0x47): 当前合约余额
10. 合约调用
CALL (0xf1): 普通调用
CALLCODE (0xf2): 已弃用
DELEGATECALL(0xf4): 委托调用(保持msg.sender)
STATICCALL (0xfa): 静态调用(只读)
CREATE (0xf0): 创建合约
CREATE2 (0xf5): 确定性创建合约
SELFDESTRUCT(0xff): 销毁合约
2.6.3 字节码示例
简单Solidity合约及其字节码:
// Solidity源代码
pragma solidity ^0.8.0;
contract SimpleStorage {
uint256 value;
function set(uint256 _value) public {
value = _value;
}
function get() public view returns (uint256) {
return value;
}
}
编译后的字节码(简化版):
部署字节码(Constructor):
608060405234801561001057600080fd5b50610150806100206000396000f3fe
运行时字节码:
6080604052348015600f57600080fd5b506004361060325760003560e01c806360fe47b11460375780636d4ce63c146062575b600080fd5b606060048036036020811015604b57600080fd5b8101908080359060200190929190505050607e565b005b60686088565b6040518082815260200191505060405180910390f35b8060008190555050565b6000805490509056fea265627a7a72315820...
指令分解(set函数):
PUSH1 0x80 // 60 80 - 压入0x80
PUSH1 0x40 // 60 40 - 压入0x40
MSTORE // 52 - 存储到内存0x40
...
PUSH1 0x00 // 60 00 - 压入存储槽0
SSTORE // 55 - 存储value
STOP // 00 - 停止执行
函数选择器(Function Selector):
计算方式:
selector = Keccak256("set(uint256)")[0:4]
= 0x60fe47b1
selector = Keccak256("get()")[0:4]
= 0x6d4ce63c
调用数据格式:
set(123)的calldata:
0x60fe47b1 // 函数选择器
000000000000000000000000000000000000000000000000000000000000007b // 参数123
2.6.4 EVM执行模型
执行流程:
1. 初始化EVM实例
- 设置Gas限制
- 加载合约代码
- 准备调用数据
2. 执行字节码
while gas > 0 and not stopped:
opcode = code[PC]
gasRequired = getGasCost(opcode)
if gas < gasRequired:
raise OutOfGas
gas -= gasRequired
execute(opcode)
PC += 1
3. 处理结果
- 成功:返回数据,更新状态
- 失败:回滚状态,消耗所有Gas(旧版)或部分退款(新版)
4. 生成收据
- Status (成功/失败)
- Gas使用量
- 日志和事件
栈操作示例:
代码:ADD
执行前:
Stack: [5, 3, ...]
执行后:
Stack: [8, ...] // 5 + 3 = 8
代码:PUSH1 0x10, PUSH1 0x20, ADD
执行过程:
1. PUSH1 0x10 → Stack: [0x10]
2. PUSH1 0x20 → Stack: [0x20, 0x10]
3. ADD → Stack: [0x30] // 0x10 + 0x20
2.6.5 预编译合约
以太坊在特定地址内置了一些预编译合约,用于高效执行特定操作:
地址 | 功能
-----------------|------------------
0x01 | ecRecover (ECDSA恢复公钥)
0x02 | SHA256哈希
0x03 | RIPEMD160哈希
0x04 | Identity (数据复制)
0x05 | ModExp (模幂运算)
0x06 | ecAdd (椭圆曲线加法)
0x07 | ecMul (椭圆曲线乘法)
0x08 | ecPairing (配对检查,用于zk-SNARKs)
0x09 | Blake2压缩函数
优势:
- 原生实现,比EVM字节码快数百倍
- Gas消耗低
- 用于密码学操作
使用示例:
// 恢复签名者地址
function recoverSigner(bytes32 hash, bytes memory signature)
public pure returns (address)
{
bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := mload(add(signature, 32))
s := mload(add(signature, 64))
v := byte(0, mload(add(signature, 96)))
}
// 调用预编译合约0x01
return ecrecover(hash, v, r, s);
}
2.7 智能合约生命周期
2.7.1 合约创建
创建方式1:交易创建
用户发送交易:
{
from: 用户地址
to: null (表示合约创建)
data: 合约字节码
value: 初始ETH (可选)
gas: Gas限制
}
执行流程:
1. 执行构造函数(Constructor)
2. 返回运行时字节码
3. 将字节码存储到新地址
4. 计算合约地址:
address = Keccak256(RLP(sender, nonce))[12:32]
创建方式2:合约创建合约
// 使用new关键字
contract Factory {
function createChild() public returns (address) {
Child child = new Child();
return address(child);
}
}
// 使用CREATE2(确定性地址)
contract FactoryV2 {
function createChild(bytes32 salt) public returns (address) {
Child child = new Child{salt: salt}();
return address(child);
}
// 预计算地址
function computeAddress(bytes32 salt) public view returns (address) {
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(type(Child).creationCode)
)
);
return address(uint160(uint256(hash)));
}
}
2.7.2 合约调用
外部调用方式:
contract Caller {
// 1. 直接调用
function callDirect(address target) public {
ITarget(target).someFunction();
}
// 2. call (低级调用)
function callLowLevel(address target, bytes memory data)
public returns (bool, bytes memory)
{
return target.call(data);
}
// 3. delegatecall (委托调用,使用调用者存储)
function delegateCallExample(address target, bytes memory data)
public returns (bool, bytes memory)
{
return target.delegatecall(data);
}
// 4. staticcall (只读调用,不修改状态)
function staticCallExample(address target, bytes memory data)
public view returns (bool, bytes memory)
{
return target.staticcall(data);
}
}
调用类型对比:
CALL:
- msg.sender = 调用者合约
- 使用被调用合约的存储
- 可修改状态
- 可转账
DELEGATECALL:
- msg.sender = 原始调用者
- 使用调用者合约的存储
- 代理模式核心
- 不可转账
STATICCALL:
- 只读调用
- 不可修改状态
- 用于view/pure函数
- Gas优化
代理模式示例:
// 代理合约
contract Proxy {
address public implementation;
constructor(address _impl) {
implementation = _impl;
}
fallback() external payable {
address impl = implementation;
assembly {
// 复制calldata
calldatacopy(0, 0, calldatasize())
// delegatecall到实现合约
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
// 复制返回数据
returndatacopy(0, 0, returndatasize())
// 返回或回滚
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
// 实现合约
contract Implementation {
uint256 public value;
function setValue(uint256 _value) public {
value = _value; // 实际存储在Proxy合约中
}
}
2.7.3 合约销毁
contract SelfDestructExample {
address payable owner;
constructor() {
owner = payable(msg.sender);
}
function destroy() public {
require(msg.sender == owner);
selfdestruct(owner); // 销毁合约,余额转给owner
}
}
销毁效果:
1. 合约代码被删除
2. 存储被清空
3. 余额转移到指定地址
4. 退还部分Gas(24,000 Gas)
注意:
- EIP-6049计划弃用selfdestruct
- 新标准下仅清空余额,不删除代码
2.8 事件与日志
2.8.1 事件机制
事件是智能合约与外部世界通信的主要方式:
contract EventExample {
// 定义事件
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
// 触发事件
function transfer(address to, uint256 amount) public {
// ... 转账逻辑
emit Transfer(msg.sender, to, amount);
}
}
事件特点:
1. 存储在交易收据的日志中
2. 不可从合约内部访问
3. Gas成本远低于存储
4. 支持索引参数(最多3个)
5. 前端可订阅和过滤
2.8.2 日志结构
Log {
Address: 合约地址
Topics: [
Topic0: Keccak256(事件签名)
Topic1: 第1个indexed参数
Topic2: 第2个indexed参数
Topic3: 第3个indexed参数
]
Data: 非indexed参数(ABI编码)
}
示例:
Transfer事件:
Topics[0] = Keccak256("Transfer(address,address,uint256)")
= 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
Topics[1] = from地址(填充到32字节)
Topics[2] = to地址
Data = amount(32字节)
2.8.3 布隆过滤器
以太坊使用布隆过滤器加速日志查询:
每个区块头包含:
LogsBloom: 256字节(2048位)布隆过滤器
工作原理:
1. 对每个日志的地址和Topics进行哈希
2. 设置布隆过滤器的对应位
3. 查询时快速判断日志可能存在的区块
4. 减少全链扫描
优势:
- 空间高效(每个区块仅256字节)
- 快速过滤不相关区块
- 允许假阳性,但无假阴性
查询流程:
1. 计算目标事件的布隆位
2. 扫描区块头,过滤不匹配的区块
3. 仅在匹配区块中查找完整日志
2.9 以太坊的网络层
2.9.1 DevP2P协议栈
以太坊使用DevP2P协议进行节点通信:
协议层次:
┌─────────────────────────┐
│ Application Layer │
│ (eth, snap, les) │
├─────────────────────────┤
│ RLPx Layer │
│ (加密、认证、多路复用) │
├─────────────────────────┤
│ Node Discovery │
│ (Kademlia DHT) │
├─────────────────────────┤
│ Transport Layer │
│ (TCP/UDP) │
└─────────────────────────┘
主要子协议:
- eth: 以太坊主协议
- snap: 快照同步
- les: 轻客户端
- whisper: 消息协议(已弃用)
2.9.2 节点发现
使用Kademlia DHT进行节点发现:
节点ID:
NodeID = Keccak256(PublicKey)
距离度量:
distance(A, B) = XOR(NodeID_A, NodeID_B)
发现流程:
1. Ping:探测节点是否在线
2. Pong:响应Ping
3. FindNode:查询邻近节点
4. Neighbors:返回已知节点列表
DHT特性:
- 每个节点维护K桶(K-buckets)
- 存储距离相近的节点
- 对数复杂度查找
2.9.3 区块同步
Full Sync(全同步):
从创世区块开始:
1. 下载所有区块
2. 执行所有交易
3. 重建完整状态
4. 验证每个状态转换
优点:
- 最高安全性
- 完整验证
- 可追溯所有历史
缺点:
- 极慢(数周)
- 存储需求大(>1TB)
- 不适合普通用户
Fast Sync(快速同步):
下载区块头和收据:
1. 下载所有区块头
2. 下载最近N个区块的完整数据
3. 下载最新状态的快照
4. 验证状态一致性
优点:
- 快速(数小时)
- 存储需求适中
- 适合全节点
缺点:
- 信任最新状态
- 无法验证早期历史
Snap Sync(快照同步):
下载状态快照:
1. 获取最新状态根
2. 按账户哈希范围下载状态
3. 并行下载多个范围
4. 验证Merkle证明
优点:
- 最快(1-2小时)
- 高效并行化
- 当前默认方式
缺点:
- 网络开销大
- 需要较好带宽
Warp Sync(扭曲同步,Geth Archive):
仅下载检查点:
1. 信任硬编码检查点
2. 跳过中间验证
3. 仅同步最新数据
优点:
- 极快
- 资源需求低
缺点:
- 安全性最低
- 需要信任检查点
2.10 以太坊2.0与PoS
2.10.1 从PoW到PoS的转变
2022年9月15日,以太坊完成"The Merge",从PoW切换到PoS:
PoW的问题:
1. 能耗高:
- 年耗电量约100 TWh(相当于荷兰)
- 碳排放严重
- 硬件浪费
2. 中心化风险:
- 矿池集中(前5个矿池>50%算力)
- ASIC专业化
- 电费驱动地理集中
3. 安全成本:
- 51%攻击成本高但可行
- 需持续发行新币激励矿工
PoS的优势:
1. 能耗降低99.95%:
- 无需算力竞争
- 仅需普通服务器
2. 降低准入门槛:
- 32 ETH即可质押
- 无需专业硬件
3. 经济安全性:
- 攻击者损失质押资产
- Slashing惩罚机制
4. 支持分片:
- PoW难以实现分片
- PoS天然支持
2.10.2 PoS共识机制
Casper FFG(Friendly Finality Gadget):
核心概念:
1. Validator(验证者):质押32 ETH的节点
2. Epoch:32个Slot(约6.4分钟)
3. Slot:12秒,一个出块时间
4. Committee:随机选择的验证者组
出块流程:
Slot 0: Validator_A提议区块
Committee投票证明
Slot 1: Validator_B提议区块
Committee投票证明
...
Slot 31: Validator_X提议区块
Committee投票证明
当前Epoch结束,新Epoch开始
LMD GHOST(Latest Message Driven GHOST):
分叉选择规则:
1. 每个验证者投票给最新看到的区块
2. 选择累计权重最大的分支
3. 权重 = 投票的验证者质押量
示例:
A (100票)
/ \
B C (150票)
| |
D E
选择分支C,因为权重更大
最终性(Finality):
检查点(Checkpoint):
- 每个Epoch的第一个Slot的区块
状态:
1. Justified(合理化):
- 2/3验证者投票支持
2. Finalized(最终化):
- 连续两个Epoch都是Justified
- 之前的检查点变为Finalized
时间线:
Epoch N: 区块A成为检查点
2/3验证者投票 → A变为Justified
Epoch N+1: 区块B成为检查点
2/3验证者投票 → B变为Justified, A变为Finalized
最终化后的区块不可逆转
2.10.3 质押与奖惩
质押要求:
最低质押:32 ETH
激活时间:约24小时(排队机制)
提款:需等待提款队列
验证者职责:
1. 提议区块(每个Epoch约1次机会)
2. 证明区块(每个Epoch 1次)
3. 参与同步委员会(随机选择,27小时周期)
奖励计算:
基础奖励 = 有效余额 × 基础奖励系数 ÷ √(总质押量)
年化收益率(APR):
APR ≈ 64 ÷ √(总质押ETH数量)
示例:
总质押:30M ETH
APR ≈ 64 ÷ √30,000,000 ≈ 64 ÷ 5,477 ≈ 1.17%
总质押:10M ETH
APR ≈ 64 ÷ √10,000,000 ≈ 64 ÷ 3,162 ≈ 2.02%
Slashing惩罚:
严重违规:
1. 双重提议:同一Slot提议两个区块
2. 双重投票:为同一Epoch投两次票
3. 环绕投票:投票包围之前的投票
惩罚力度:
基础惩罚:约1 ETH
相关惩罚:同时被Slash的验证者越多,惩罚越重
最高可损失全部32 ETH
示例:
如果1%的验证者同时被Slash:
单个验证者损失 ≈ 1 + (32 × 1%) = 1.32 ETH
如果33%同时被Slash:
单个验证者损失 ≈ 1 + (32 × 33%) = 11.56 ETH
离线惩罚:
小额扣款:
- 未能及时证明:扣除基础奖励
- 累计离线:逐渐增加惩罚
- 长期离线:强制退出
激励在线率:
网络整体在线率 > 66.7% → 正收益
网络整体在线率 < 66.7% → 负收益
2.11 扩展性解决方案
2.11.1 Layer 2方案
Rollup技术:
核心思想:
1. 链下执行交易
2. 链上发布数据
3. 继承以太坊安全性
两种类型:
┌───────────────┬──────────────────┬──────────────────┐
│ 特性 │ Optimistic │ ZK-Rollup │
├───────────────┼──────────────────┼──────────────────┤
│ 验证方式 │ 欺诈证明 │ 零知识证明 │
│ 提款时间 │ 7天 │ 几分钟 │
│ TPS │ 1000-4000 │ 2000-10000 │
│ Gas成本 │ 较低 │ 中等 │
│ EVM兼容性 │ 高 │ 中等 │
│ 代表项目 │ Optimism, Arbitrum│ zkSync, StarkNet│
└───────────────┴──────────────────┴──────────────────┘
Optimistic Rollup:
工作流程:
1. Sequencer(排序器)收集交易
2. 批量执行,生成状态根
3. 提交状态根和交易数据到L1
4. 7天挑战期
5. 如有欺诈,提交欺诈证明
欺诈证明:
if submitted_state != computed_state:
revert_transaction()
slash_sequencer()
reward_challenger()
优势:
- EVM等效,易于移植
- 开发成本低
劣势:
- 提款延迟长
- 需要诚实验证者监控
ZK-Rollup:
工作流程:
1. Sequencer执行交易
2. 生成零知识证明(zk-SNARK或zk-STARK)
3. 提交状态根和证明到L1
4. L1验证证明(快速)
5. 立即最终化
零知识证明特性:
- 证明"我知道X,但不告诉你X"
- 验证时间短(毫秒级)
- 数学保证正确性
优势:
- 提款快
- 安全性高(密码学保证)
劣势:
- 生成证明成本高
- EVM兼容性差(需重写合约)
State Channels(状态通道):
原理:
1. 双方锁定资金到链上合约
2. 链下进行无限次交易
3. 仅最终状态上链
示例:支付通道
Alice和Bob各锁定10 ETH
链下交易:
Alice → Bob: 1 ETH (Alice:9, Bob:11)
Bob → Alice: 2 ETH (Alice:11, Bob:9)
...
关闭通道时才上链最终状态
优势:
- 即时确认
- 几乎无Gas成本
- 高隐私性
劣势:
- 仅限参与方
- 需要在线
- 流动性锁定
Plasma:
子链结构:
Main Chain (Ethereum)
↓
Plasma Chain
↓ ↓ ↓
Child Chains
特点:
- 独立的侧链
- 定期向主链提交Merkle根
- 退出机制保证安全
问题:
- 数据可用性假设
- 大规模退出拥堵
- 已被Rollup取代
2.11.2 数据可用性采样
未来的分片方案依赖数据可用性采样(DAS):
问题:
如何确保区块数据可用,而不下载全部数据?
解决方案:
1. 将区块数据编码为纠删码(Reed-Solomon)
2. 分割为N个片段
3. 仅需任意50%片段即可重建
4. 轻节点随机采样几个片段
5. 高概率保证数据可用
数学原理:
采样30个片段:
数据不可用但采样通过的概率 < 10^-9
应用:
- 支持大区块(>1MB)
- 轻节点也能验证
- 为分片铺路
2.12 以太坊与比特币深度对比
2.12.1 设计哲学差异
比特币:
- 极简主义
- 单一功能(货币)
- 保守升级
- 安全优先
以太坊:
- 功能丰富
- 通用平台
- 快速迭代
- 创新优先
2.12.2 技术架构对比
账本模型:
比特币:UTXO
- 并发友好
- 隐私性好
- 状态简单
以太坊:Account
- 编程友好
- 状态复杂
- 并发受限
智能合约:
比特币:Script(非图灵完备)
- 简单安全
- 功能有限
- 难以表达复杂逻辑
以太坊:EVM(图灵完备)
- 功能强大
- 需Gas限制
- 支持任意计算
共识机制:
比特币:PoW (SHA-256)
- 成熟稳定
- 能耗高
- 难以升级
以太坊:PoS (Casper)
- 节能
- 更安全(经济惩罚)
- 支持分片
2.12.3 性能对比
指标 | 比特币 | 以太坊(PoS)
--------------|------------|-------------
出块时间 | ~10分钟 | 12秒
TPS | ~7 | ~30
最终性时间 | ~60分钟 | ~12.8分钟(2 Epochs)
区块大小 | ~1-4MB | 动态(Gas限制)
年发行率 | <1.8% | ~0.5%(通缩)
能耗 | ~100 TWh/年| ~0.01 TWh/年
2.13 本章总结
本章系统介绍了以太坊的架构和核心概念:
账户模型:
- 两种账户类型:EOA和合约账户
- 直观的余额记录方式
- 天然支持智能合约
世界状态:
- Merkle Patricia Trie存储结构
- 状态树、存储树、交易树、收据树
- 高效的状态验证和历史追溯
Gas机制:
- 防止无限循环和资源滥用
- EIP-1559引入基础费用和优先费
- 通缩机制(销毁Base Fee)
EVM:
- 基于栈的虚拟机
- 140多个操作码
- 图灵完备的执行环境
交易生命周期:
- 创建、签名、广播、打包、执行、确认
- Nonce机制保证顺序
- 收据和事件记录结果
PoS共识:
- 从PoW到PoS的历史性转变
- Casper FFG + LMD GHOST
- 质押、奖励、Slashing机制
扩展性方案:
- Layer 2:Rollup、State Channel
- Optimistic vs ZK-Rollup
- 数据可用性采样
以太坊的创新:
以太坊在比特币的基础上实现了重大创新:
- 从货币到平台的飞跃
- 智能合约开启DeFi、NFT、DAO等应用
- 账户模型更适合复杂状态管理
- Gas机制平衡计算资源
- PoS提升可持续性
挑战与未来:
尽管以太坊功能强大,仍面临挑战:
- 扩展性瓶颈(TPS有限)
- 高Gas费用(网络拥堵时)
- 状态膨胀(历史数据不断增长)
- MEV问题(矿工可提取价值)
未来路线图致力于解决这些问题:
- Danksharding:大规模数据可用性
- PBS:提议者-构建者分离
- Verkle树:减少状态大小
- 无状态客户端:降低节点要求
下一章将深入智能合约开发,讲解Solidity编程语言、常见设计模式、安全最佳实践以及DApp开发全流程。