以太坊的技术架构到底有多脏?

2026-09-23

如果今天让一群工程师从零开始设计一条区块链,他们大概率不会设计出今天的 Ethereum。

不是因为 Ethereum 做不到事情。

恰恰相反,Ethereum 最大的问题是:它什么都能做,但很多东西都是后来补进去的。

十多年的运行历史、一次次安全事故、Gas 模型修改、账户抽象、共识切换、Layer 2 扩容,以及为了继续兼容过去而留下来的无数特殊逻辑,最终全部沉积在了同一套协议里。

Ethereum 今天的复杂,并不完全是“功能丰富”带来的复杂。

其中相当一部分,是历史债务带来的复杂。

如果把 Ethereum 当作一套运行了十几年的大型软件来看,它越来越像一个所有人都不敢真正重构、只能不断向上打 Patch 的遗留系统。

最荒诞的是,这些 Patch 并没有消失。

很多十年前发生的事情,今天仍然存在于 Ethereum 客户端的源码里。

这就是我所说的:

以太坊的技术很脏。

这里的“脏”不是说代码水平差,而是说它已经积累了大量只有结合历史背景才能解释的特殊规则、例外、兼容层和协议补丁。

而这种脏,可以一路从 2016 年追溯到今天。


一、The DAO:一次黑客攻击,被永远写进了客户端源码

Ethereum 最经典的一块“历史化石”,是 2016 年的 The DAO。

The DAO 遭到攻击后,Ethereum 社区最终选择通过 Hard Fork 修改状态,将一批 DAO 相关账户中的 ETH 转入退款合约。

关于这件事情到底是否违背“Code is Law”,已经争论了十年。

但如果我们暂时不讨论哲学,只看工程实现,这件事其实更加有意思。

因为今天打开 go-ethereum 的源码,你仍然可以找到:

var DAORefundContract = common.HexToAddress(
    "0xbf4ed7b27f1d666546e30d74d50d173d20bca754",
)

func DAODrainList() []common.Address {
    return []common.Address{
        common.HexToAddress("0xd4fe7bc31cedb7bfb8a345f31e668033056b2728"),
        common.HexToAddress("0xb3fb0e5aba0e20e5c49d252dfd30e102b171a425"),
        common.HexToAddress("0x2c19c7f9ae8b751e37aeb2d93a699722395ae18f"),
        ...
    }
}

不是伪代码。

不是历史文档。

而是 2026 年的 go-ethereum master 分支里仍然存在的代码。

文件就在:

params/dao.go

当前源码中,DAORefundContract 大约位于第 561–563 行附近,而 DAODrainList() 紧接着从第 564 行开始。里面直接 hardcode 了一长串参与 DAO 状态迁移的地址。

甚至注释都非常直白:

DAODrainList is the list of accounts whose full balances will be moved into a refund contract

这句话从工程角度看非常震撼。

一条号称通用、去中心化、状态转移由协议定义的区块链,在客户端源码里永久保存着:

“2016 年某件具体历史事件发生时,把这些具体账户的钱转去这个具体地址。”

这不是抽象的协议规则。

这是历史事件。

是一个 if。

只不过这个 if 被写进了共识。

更准确地说:

Ethereum 的共识规则里存在一块纪念 2016 年 DAO 黑客事件的墓碑。

而所有希望从创世块开始完整执行 Ethereum 历史状态的客户端,都必须理解它。

这就是技术债务最纯粹的形态:

一个已经结束十年的事件,因为不能破坏历史共识,所以永远不能真正从代码里删除。


二、一份客户端,背着十几年所有 Ethereum 活过

接下来打开 go-ethereum 的 params/config.go。

你会看到一种非常壮观的东西:

HomesteadBlock
DAOForkBlock
EIP150Block
EIP155Block
EIP158Block
ByzantiumBlock
ConstantinopleBlock
PetersburgBlock
IstanbulBlock
MuirGlacierBlock
BerlinBlock
LondonBlock
ArrowGlacierBlock
GrayGlacierBlock
MergeNetsplitBlock

ShanghaiTime
CancunTime
PragueTime
OsakaTime
...

这不是 Ethereum 历史课。

这是 Ethereum 客户端的运行配置。

甚至源码里直接存在这样的判断:

func (c *ChainConfig) IsHomestead(num *big.Int) bool
func (c *ChainConfig) IsDAOFork(num *big.Int) bool
func (c *ChainConfig) IsEIP150(num *big.Int) bool
func (c *ChainConfig) IsEIP155(num *big.Int) bool
func (c *ChainConfig) IsByzantium(num *big.Int) bool
func (c *ChainConfig) IsConstantinople(num *big.Int) bool
...

这里需要澄清一个技术细节。

不能简单地说“Ethereum 同时运行所有历史版本”。

它并不是在当前区块同时执行 Byzantium、London 和 Cancun 三套规则。

真正的问题更加微妙:

Ethereum 客户端必须知道自己正在处理哪个历史时期,并为那个时期选择正确的状态转换规则。

如果一个节点从创世块同步:

第 100 万块要按照那个时代的 EVM 执行。

第 500 万块要按照另一个时代执行。

London 以后 BASEFEE 的规则又变了。

Merge 前后共识结构又完全不同。

Shanghai 以后又有新的规则。

Cancun 又出现 blob。

也就是说:

Ethereum 并不是一套协议。

从客户端工程角度看,它实际上是一长串按照区块高度和时间戳拼接起来的协议:

Ethereum =
Frontier
+ Homestead
+ DAO
+ Tangerine Whistle
+ Spurious Dragon
+ Byzantium
+ Constantinople
+ Petersburg
+ Istanbul
+ Berlin
+ London
+ Merge
+ Shanghai
+ Cancun
+ Prague
+ ...

而且这些东西不是 README。

它们是共识。

删错一个历史分支,节点就可能算出不同的 state root。

所以 Ethereum 有一个天然问题:

历史越长,协议就越不可能真正干净。

新的规则可以增加。

旧规则却不能随便删除。

于是客户端像地质层一样,一层一层沉积。

2015 年是一层。

2016 年又是一层。

2021 年再来一层。

2022 年 PoW 直接切 PoS,又盖一层。

2024 年 Blob 再盖一层。

2025 年 EOA delegation 再盖一层。

所有这些历史最后都叫:

Ethereum。


三、最尴尬的设计债务之一:EOA 实在太蠢

Ethereum 早期账户体系非常简单:

一类叫 EOA。

一类叫 Contract Account。

EOA 的权限模型基本上就是:

private key
        ↓
ECDSA signature
        ↓
transaction

谁有私钥,谁就是这个账户。

简单得甚至有点原始。

问题是,当区块链真正开始被普通人使用以后,人们才发现钱包真正需要的能力包括:

  • 多签
  • 社交恢复
  • Session Key
  • 权限控制
  • 批量交易
  • Gas 代付
  • ERC-20 支付 Gas
  • 密钥轮换
  • 每日额度
  • Passkey
  • 不同操作使用不同授权策略

这些能力,EOA 原生几乎一个都没有。

这不是因为 Ethereum 使用 account model 就一定做不到。

把问题简单归咎于“没用 UTXO”并不严谨。

真正的问题是:

Ethereum 最早设计的 EOA authorization model 太薄了。

地址和一把 secp256k1 私钥高度绑定,协议原生只理解一种非常简单的交易认证逻辑。

于是 Ethereum 花了之后十年时间,想办法让账户变得不像 EOA。

这才有了 Account Abstraction。


四、ERC-4337:为了不改协议,又在协议上面造了一套“伪交易系统”

4337 是 Ethereum 技术债务里非常漂亮、同时也非常脏的一个例子。

它解决的问题当然是真实的。

但看看它是怎么解决的。

普通 Ethereum 交易:

Wallet
  ↓
Transaction
  ↓
Ethereum mempool
  ↓
Block

4337:

Smart Account
     ↓
UserOperation
     ↓
UserOp Mempool
     ↓
Bundler
     ↓
EntryPoint
     ↓
handleOps()
     ↓
Ethereum Transaction
     ↓
Block

Ethereum 官方 EIP 对它的描述甚至非常坦白:

4337 刻意避免修改 consensus layer。

于是它没有真正创造 Ethereum 原生交易。

而是创造了一种:

pseudo-transaction object。

名字叫:

UserOperation

用户发送的甚至不是 Ethereum transaction。

而是一个 UserOperation。

然后需要一种新的角色:

Bundler

Bundler 再把若干 UserOperation 打包起来,最终构造一笔真正的 Ethereum transaction,调用一个特殊的:

EntryPoint.handleOps()

最终才真正进入 Ethereum。

于是为了让账户拥有现代钱包理应拥有的权限模型,我们得到了:

EOA Transaction

+

Smart Account

+

UserOperation

+

UserOp Mempool

+

Bundler

+

EntryPoint

+

Paymaster

Paymaster 又负责 Gas sponsorship。

Bundler 又需要自己的模拟和验证逻辑。

UserOperation 又有自己的 nonce。

自己的 gas limit。

自己的 validation。

自己的 mempool。

甚至自己的 DoS 防御规则。

从功能角度说,这是非常聪明的工程设计。

但从系统设计角度说,它同时也是一个极其典型的补丁:

底层交易模型不好改,于是在底层交易系统上面,再造一个交易系统。

然后把第二套交易最终塞进第一套交易里。

这就是 Ethereum 最典型的技术哲学:

尽量不推翻过去,在过去上面再盖一层。


五、然后 Ethereum 又发现:4337 终究不是原生账户抽象

问题来了。

既然 4337 那么好,为什么后来又出现 EIP-7702?

因为 4337 最大的问题从一开始就写在设计目标里:

它不修改 Ethereum 共识层。

这既是优势,也是它的原罪。

大量现存 ETH 和资产仍然存放在普通 EOA 中。

你不能让全世界用户突然:

“请重新部署一个 Smart Account,再把资产迁过去。”

Ethereum 最终只能继续给 EOA 打补丁。

于是出现 EIP-7702。


六、EIP-7702:一个 EOA,现在居然可以“指向代码”

7702 的设计相当魔幻。

它允许一个 EOA 写入一种特殊的 delegation indicator:

0xef0100 || address

然后这个 EOA 在执行时,可以把自己的代码行为委托给另一个地址的代码。

也就是说:

以前:

EOA = 没代码
Contract = 有代码

这是 Ethereum 最基本的账户分类。

现在:

EOA = 没代码

但是

EOA.code = 0xef0100 || contract_address

然后执行 contract_address 的代码

EIP-7702 甚至需要专门规定 CALL、CALLCODE、DELEGATECALL、STATICCALL,以及 EXTCODESIZE、CODESIZE 等操作遇到这种 delegation indicator 时到底应该看到什么。

于是一个曾经非常简单的问题:

“这个地址有没有代码?”

现在都不再那么简单。

这就是兼容性的代价。

Ethereum 不能说:

EOA 设计错了,我们废弃 EOA。

因为几亿个地址在那里。

几千亿美元资产在那里。

几十万套软件假设 EOA 就是 EOA。

所以只能说:

EOA 还是 EOA,但是我们允许 EOA 里面放一点特殊 code;这个 code 又不是真的 code,而是一个 delegation marker;EVM 看见这个 marker 后,要去另一个账户找真正 code。

这就是典型的 legacy system engineering。

不能拆墙。

于是就在墙里面再埋一根管子。


七、还没结束:现在又来了 EIP-8141

如果你刚才说的“4E81 / 4E91”指的是近期 Ethereum 在讨论的原生 Gas sponsorship / Native Account Abstraction,你想说的很可能是:

EIP-8141:Frame Transaction。

它比 4337 更进一步。

8141 直接提出新的 transaction type:

Frame Transaction。

一笔交易被拆成多个 frame,其中不同 frame 可以负责:

VALIDATION
PAYMENT
EXECUTION

换句话说:

以前 Ethereum transaction 的逻辑基本是:

signature → sender → sender pays gas → execute

4337 说:

我们在外面套一个 UserOperation。

7702 说:

EOA 可以 delegate code。

8141 则进一步说:

不如把 transaction 本身拆了。
谁验证交易、谁付款、谁执行,可以分别定义。

EIP-8141 自己明确把 alternative fee payment schemes、key rotation、smart accounts 等列为目标。

技术能力当然更强。

但是回头看看这条进化路线:

EOA
 ↓
Smart Contract Wallet
 ↓
ERC-4337
 ↓
UserOperation
 ↓
Bundler
 ↓
EntryPoint
 ↓
Paymaster
 ↓
EIP-7702
 ↓
EOA Delegation
 ↓
EIP-8141
 ↓
Frame Transaction

十年之后,Ethereum 终于越来越接近一个合理的 programmable account model。

代价是什么?

前面所有东西基本都已经存在了。

于是新设计不是替换旧设计。

而是:

继续共存。


八、Blob:为了 Layer 2,Layer 1 甚至专门长出了一个新的数据器官

Ethereum 另一个极其典型的工程妥协,是 EIP-4844。

Ethereum 最初的扩容故事并不是:

L1 不负责执行大量交易,以后主要给 Rollup 提供 Data Availability。

Ethereum 最初本身就是一条执行链。

后来发现 L1 扩容太困难,于是战略逐渐转向 Rollup-centric。

问题来了。

Rollup 需要向 Ethereum 发布大量数据。

以前放 calldata。

贵。

怎么办?

最直觉的做法是优化 calldata。

Ethereum 最后的选择却是:

给协议增加一种全新的数据结构。

Blob。

于是 Ethereum 出现一种非常奇怪的交易:

Blob-carrying transaction。

Blob 数据:

  • 跟 Ethereum 区块绑定;
  • 由共识层保证 availability;
  • EVM 却不能像 calldata 一样直接读取;
  • EVM 主要看到的是其 commitment;
  • 数据也不要求永久保存在执行层状态里。

EIP-4844 自己写得非常清楚:

它的直接目标就是为 Rollup 提供廉价 Data Availability。

于是现在 Ethereum 不只是一条:

执行智能合约的区块链。

它又兼任:

Rollup 数据发布层。

为了这个角色,L1 专门增加:

Blob Transaction
Blob Gas
Blob Base Fee
Blob Commitment
KZG
Point Evaluation
Blob Sidecar

这是什么?

这就是架构路线改变之后,底层协议留下的器官。

如果认为 Rollup-centric roadmap 是 Ethereum 的成功,那么 Blob 是高明设计。

但如果从另一个角度看——尤其是从“L1 本身应该保持结构简洁”的角度——你会得到完全相反的结论:

Ethereum 为了拯救一个越来越复杂的 L2 扩容体系,把 L2 的特殊需求直接写进了 L1。

Rollup 原本应该是 Ethereum 上面的应用。

最后 Ethereum 协议反过来为了 Rollup 修改自己。

尾巴开始摇狗。


九、Layer 2 越成功,Ethereum 的整体系统反而越不像“一条链”

这也是我认为 Rollup 路线最荒诞的地方。

Ethereum 曾经最强大的叙事之一叫:

Composability。

所有合约在同一个 state machine。

Uniswap 可以调 Aave。

Aave 可以调 Maker。

一个 transaction 内完成。

这才叫真正的 composability。

Rollup-centric Ethereum 做了什么?

把整个 Ethereum 生态切成:

Ethereum L1

Arbitrum
Optimism
Base
zkSync
Scroll
Linea
Starknet
...

然后每个链:
自己的 sequencer
自己的 bridge
自己的状态
自己的提现规则
自己的 fee
自己的 explorer
自己的 RPC
自己的故障模式

Ethereum 最初解决的问题是:

不要让每个人自己维护数据库和信任系统。

Rollup 时代最后变成:

每个 Rollup 再维护自己的状态机。

然后大家再研究:

  • shared sequencing
  • intents
  • chain abstraction
  • interoperability
  • based rollups
  • canonical bridges
  • liquidity aggregation

先把一个统一系统拆开。

再用五年时间研究怎么把它拼回来。

这很难不让人怀疑:

到底是在解决扩容问题,还是在制造下一代基础设施问题?

更讽刺的是,为了维护这套架构,Ethereum L1 还要不断修改。

Blob 就是最明显的例子。


十、Difficulty Bomb:一个“临时机制”,延期了一次又一次

Ethereum 历史上还有一个非常经典的设计:

Difficulty Bomb。

当年为了迫使网络最终从 PoW 转向 PoS,Ethereum 在协议里设计了 Difficulty Bomb,让挖矿难度最终指数级提高。

理论上很漂亮:

到时候 PoW 会越来越难,逼着大家升级 PoS。

实际发生了什么?

PoS 没按计划完成。

于是:

延期。

然后又延期。

然后继续延期。

Ethereum 历史上多个升级都包含 Difficulty Bomb delay。

比如:

Byzantium
Constantinople
Muir Glacier
London
Arrow Glacier
Gray Glacier

于是本来用来强迫技术路线按时迁移的协议机制,最后自己变成了一项需要反复维护的历史债务。

这几乎是一幅 Ethereum 工程史的缩影:

设计一个机制解决未来问题。

未来没有按预计发生。

于是再写一个 EIP,修改上一个机制。


十一、Gas 规则本身,也是一部考古学

Ethereum EVM 看起来像一个固定虚拟机。

其实不是。

很多 opcode 的 Gas cost 在历史上发生过变化。

为什么?

因为有人发现原来的 Gas 定价不合理,可以 DoS。

于是重新定价。

典型例子:

EIP-150。

EIP-1884。

EIP-2929。

一次次重新评估:

SLOAD 到底应该多少钱?

BALANCE 应该多少钱?

EXTCODESIZE 呢?

Cold Access 和 Warm Access 是否应该不同?

Berlin 之后甚至引入:

cold access
warm access
access list

于是执行一条 opcode 到底需要多少 Gas,不只取决于 opcode。

还取决于:

这笔交易此前访问过什么。

这当然有充分的安全理由。

但从 VM elegance 的角度看:

这很难叫漂亮。

这是一个现实系统在遭遇攻击和性能瓶颈以后,不断重新校准成本模型留下来的疤。


十二、SELFDESTRUCT:一个 opcode 活着活着,语义被改了

还有一个特别能说明 Ethereum 问题的东西:

SELFDESTRUCT

早期 Ethereum 中,合约可以 SELFDESTRUCT:

删除代码。

清理 storage。

发送 ETH。

后来人们发现这会带来大量复杂性,并且阻碍 Verkle Tree 等未来状态结构。

怎么办?

删 opcode?

不能删。

因为历史合约可能依赖它。

于是 EIP-6780 干了一件非常 Ethereum 的事情:

保留 SELFDESTRUCT 这个名字和 opcode,但是改变它的语义。

现在,除非 SELFDESTRUCT 和合约创建发生在同一笔交易里,否则它基本不再真正“destroy”这个 account 的 code 和 storage。

所以:

SELFDESTRUCT

这个 opcode:

已经不一定 self destruct。

如果一个语言设计走到:

函数名字不能改,函数也不能删,但是函数行为我们改掉。

那基本就是 legacy compatibility 最标准的症状之一。


十三、Ethereum 今天已经不是一套优雅设计,而是一座城市

这时候有人可能会反驳:

Windows 不也兼容几十年前的软件?

Linux Kernel 不也有历史包袱?

TCP/IP 不也有大量历史问题?

没错。

这正好说明问题。

Ethereum 今天最大的成就之一,可能恰恰也是它最大的技术缺陷:

它已经无法推倒重来了。

它不再是一个实验性区块链项目。

它已经是一座城市。

一座城市不会因为道路规划很差,就把城市全部推平重建。

只能:

修立交桥。

修地铁。

增加红绿灯。

增加高架。

修改单行道。

再挖隧道。

然后几十年以后你看地图,会发现:

为什么这条路这么奇怪?

因为 20 年前这里有栋楼。

为什么地铁绕一个弯?

因为 15 年前拆迁没谈下来。

为什么这里突然三层立交?

因为后来车太多。

这就是 Ethereum。


十四、Ethereum 的真正问题,不是复杂,而是“历史复杂度不可回收”

复杂本身不是罪。

Solana 也复杂。

Bitcoin 也有历史兼容。

操作系统更复杂。

Ethereum 真正令人难受的是:

它大量复杂度来自历史选择,而且这些复杂度很难被回收。

DAO 已经过去十年。

DAO fork 代码还在那里。

PoW 已经消失。

客户端仍然必须理解 PoW 时代的链。

EOA 不够用了。

不能删。

于是加 4337。

4337 不够原生。

于是加 7702。

7702 仍然只是过渡。

于是研究 8141。

L1 扩容困难。

于是发展 L2。

L2 需要数据。

于是 L1 增加 Blob。

Blob 又有自己的 Fee Market。

以后再继续 PeerDAS、Danksharding。

每一步单独看:

都有理由。

这才是最危险的地方。

糟糕的系统通常不是因为某一天有人做了一个极其愚蠢的决定。

而是:

每一次局部决定看起来都非常合理。

然后十年之后,把所有“合理决定”叠在一起,就得到一座没有人能够完整装进脑子里的系统。


十五、这就是 Ethereum 最“脏”的地方

所以当我说:

Ethereum 的技术架构很脏。

我的意思并不是:

Ethereum 开发者能力差。

恰恰相反。

Ethereum 可能拥有整个区块链行业最强的一批协议工程师。

真正讽刺的地方就在这里:

需要这么多顶级工程师,很大程度上正是因为这个系统已经复杂到必须由顶级工程师维持。

DAO Fork。

十几代 Hard Fork。

Gas repricing。

EIP-1559。

PoW → PoS。

Execution Layer + Consensus Layer。

Smart Contract Wallet。

ERC-4337。

UserOperation。

Bundler。

EntryPoint。

Paymaster。

EIP-7702。

EOA delegation。

EIP-8141。

Frame Transaction。

Rollup。

Blob。

Blob Gas。

KZG。

Data Availability。

这一切单独拿出来,都能写一篇漂亮的技术论文。

但把它们全部放到一起,看到的就不再是“优雅”。

而是:

一个系统为了永远不能停机、永远不能推倒重来,而支付的全部历史成本。

Ethereum 最值得研究的地方,或许已经不再是:

怎样设计一条区块链?

而是:

一条区块链运行十几年、承载数千亿美元资产之后,到底会变成什么?

Ethereum 给出的答案是:

它会变成一个几乎任何东西都不能删、任何历史都不能忘、任何错误都只能继续向前兼容的巨大状态机。

每一代工程师都觉得自己只加了一个很合理的功能。

最后整个协议却变成了一座技术考古遗址。

你甚至可以在 2026 年最新版客户端里,翻开一个叫 dao.go 的文件,

然后看到:

2016 年那场黑客攻击留下来的地址,

仍然静静地躺在那里。

这就是 Ethereum 的技术架构到底有多脏。