以太坊源码研读(一):账户、合约与存储全解

·

关键词:以太坊、Go-Ethereum、账户模型、StateObject、合约存储、EVM

以太坊与比特币最大的不同,是采用 账户/状态模型 代替 UTXO。读者可以把“账户”想象成链上银行户头,而“状态”就是这个户头在某一瞬间的余额、Nonce 等全部信息。本文将从源码角度拆解 AccountStateObjectContract 三个核心概念,并手把手分析合约存储(Storage)在 EVM 中的落地细节。


一、从交易视角看以太坊:状态机模型

以太坊运行的是 交易驱动的状态机——当一笔交易被执行,账户数据随之更新,系统完成一次 状态转移(State Transition)

换句话说:StateObject 在内存中的字段变化 = 链上世界状态更新


二、StateObject:所有账户的内存抽象

core/state/state_object.go 中,stateObject 结构统一管理两种账户的数据,主要字段如下:

  1. address / addrHash

    • 20 字节的以太坊地址标识身份。
  2. data types.StateAccount

    • 对外暴露的账户元数据,上链时以 MPT 账户树 存储。
    • 四个字段:NonceBalanceRootCodeHash
  3. triecode 与四组 Storage 缓存

    • originStorage:交易开始前从磁盘加载的原始值。
    • dirtyStorage:当前交易执行过程中 已修改 的键值对。
    • pendingStorage:整区块执行完后再一次性写入磁盘。
    • fakeStorage:调试专用,不参与共识。

四类 Storage 的使用顺序是:

originStorage ⇢ dirtyStorage ⇢ pendingStorage

每次交易结束,dirtyStorage 被批量迁移到 pendingStorage;当区块被 finalize 后,再统一刷到磁盘。


三、StateAccount 四要素:Nonce、余额、Storage Root、字节码哈希

字段含义EOAContract
Nonce已发送交易次数初始为 1
Balance账户当前以太余额
Root对应 Storage Tree 根空值合约字节码变量树
CodeHash合约字节码 Keccak-256 值空值非空

👉 深入了解 Go-Ethereum 状态树的实现差异


四、私钥 → 地址:链上由你说了算

4.1 账号生命周期

4.2 地址计算公式(Go 代码简写)

priv, _ := crypto.GenerateKey()          // 32 字节随机私钥
pub  := priv.Public().(*ecdsa.PublicKey) // 64 字节公钥 X,Y
addr := crypto.PubkeyToAddress(*pub)     // Keccak256(pub)[-20:]

👉 重要提醒:私钥持有即资产绝对控制,但合约 Token 的安全同时取决于合约逻辑!


五、合约存储(Storage)分片结构

每个合约独立拥有一棵 Storage Trie,最大情况下可容纳 ²²⁵⁶ 个 32 字节 Slot,相当于 10⁷⁷ TB——理论上近乎无限。

EVM 通过 OpSloadOpSstore 操作 Storage,两者都围绕 Storage 缓存进行读写。


六、合约存储实战五例

案例 1:定长变量顺序存

uint256 a; uint256 b; uint256 c;

顺序写入 keccak256(0), keccak256(1), keccak256(2)

案例 2:声明顺序决定位置

结果印证:变量在源码出现顺序 = Slot.position

案例 3:只给部分变量赋值

number1, number2 写入 Slot 1、2,而 Slot 0 依旧存在,只是持零值。

案例 4:打包小于 32 字节的变量

address + bool 被打包到同一个 Slot,节省 Gas 但增加读写放大

案例 5:Map 的存储哈希

余额 balances[user] 的真实 Slot 位置通过

keccak256(userAddress ++ slotNumber)

计算而来,保证位置冲突概率趋近于零。


七、FAQ

Q1:为何不看 UTXO 模型?
A:账户模型天然支持智能合约的“全局状态读取”,与 EVM 的指令集耦合度高,这也是 DeFi 在以太坊繁荣的核心原因。

Q2:Nonce 会溢出吗?
A:理论上限 2⁶⁴-1,按每毫秒一笔交易连续发 5 万年才能用完,无需担心。

Q3:修改合约后地址会变?
A:会。每次 CREATE 都会根据 deployer + nonce 生成新的地址;CREATE2 方案则改用 keccak256(0xff ++ deployer ++ salt ++ initcode)

Q4:我只要读数据,为何要担心 Gas?
A:广播交易时需要付 Gas;若仅做链下查询,可使用本地节点 eth_call 免费读,只是不打块上链。


八、总结

读完本文,你应该能够:

  1. 精准定位源码中 accountStateObject 角色的职责边界;
  2. 用短短几行 Go 代码完成私钥 → 地址的周流程;
  3. 画出一份 Slot 分配映射表 并解释 Map 的哈希寻址原理;

接下来,下一篇我们将深入 StateDB 快照机制与快照同步(Snap-sync),继续拆解百万级状态数据的存储与同步策略。

👉 第一时间获取后续源码剖析更新