关键词:以太坊、Go-Ethereum、账户模型、StateObject、合约存储、EVM
以太坊与比特币最大的不同,是采用 账户/状态模型 代替 UTXO。读者可以把“账户”想象成链上银行户头,而“状态”就是这个户头在某一瞬间的余额、Nonce 等全部信息。本文将从源码角度拆解 Account、StateObject、Contract 三个核心概念,并手把手分析合约存储(Storage)在 EVM 中的落地细节。
一、从交易视角看以太坊:状态机模型
以太坊运行的是 交易驱动的状态机——当一笔交易被执行,账户数据随之更新,系统完成一次 状态转移(State Transition)。
- EOA 由私钥控制,直接发起交易;
- Contract 由 EOA 通过
CREATE/CREATE2交易部署并执行。
换句话说:StateObject 在内存中的字段变化 = 链上世界状态更新。
二、StateObject:所有账户的内存抽象
在 core/state/state_object.go 中,stateObject 结构统一管理两种账户的数据,主要字段如下:
address/addrHash- 20 字节的以太坊地址标识身份。
data types.StateAccount- 对外暴露的账户元数据,上链时以 MPT 账户树 存储。
- 四个字段:
Nonce、Balance、Root、CodeHash。
trie、code与四组Storage缓存originStorage:交易开始前从磁盘加载的原始值。dirtyStorage:当前交易执行过程中 已修改 的键值对。pendingStorage:整区块执行完后再一次性写入磁盘。fakeStorage:调试专用,不参与共识。
四类 Storage 的使用顺序是:
originStorage ⇢ dirtyStorage ⇢ pendingStorage每次交易结束,dirtyStorage 被批量迁移到 pendingStorage;当区块被 finalize 后,再统一刷到磁盘。
三、StateAccount 四要素:Nonce、余额、Storage Root、字节码哈希
| 字段 | 含义 | EOA | Contract |
|---|---|---|---|
| Nonce | 已发送交易次数 | ✅ | 初始为 1 |
| Balance | 账户当前以太余额 | ✅ | ✅ |
| Root | 对应 Storage Tree 根 | 空值 | 合约字节码变量树 |
| CodeHash | 合约字节码 Keccak-256 值 | 空值 | 非空 |
四、私钥 → 地址:链上由你说了算
4.1 账号生命周期
- 本地创建:钱包(Metamask、Keystore)随机生私钥→推公钥→计算地址。此时链上 并未真正注册 该账户。
- 链上注册:当该地址第一次接收转账或发起交易,
StateDB会自动在 世界状态树 为其新建节点。
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——理论上近乎无限。
- Slot 是 32 字节/256 位的最小存储单元;
- 寻址空间:[0x0 … 0xFF…FF],32 字节 key;
- 磁盘缓存 只有 Root 保存在
StateAccount,真正的键值对存放在底层 LevelDB。
EVM 通过 OpSload 与 OpSstore 操作 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 免费读,只是不打块上链。
八、总结
读完本文,你应该能够:
- 精准定位源码中
account与StateObject角色的职责边界; - 用短短几行 Go 代码完成私钥 → 地址的周流程;
- 画出一份 Slot 分配映射表 并解释 Map 的哈希寻址原理;
接下来,下一篇我们将深入 StateDB 快照机制与快照同步(Snap-sync),继续拆解百万级状态数据的存储与同步策略。