4、账户模型概述:外部账户与合约账户的区别、账户状态字段
以太坊的账户模型是其区别于比特币 UTXO 模型的核心设计之一。理解账户模型,是掌握智能合约执行、Gas 机制和去中心化应用(DApp)交互的基石。在以太坊中,所有状态变更——无论是转账、部署合约还是调用合约函数——本质上都是账户状态的变化。本章将深入剖析外部账户(EOA)与合约账户(CA)的本质区别,并逐一拆解账户状态中的关键字段,帮助你建立对以太坊底层数据结构的清晰认知。
4.1 核心原理:从“状态”视角理解以太坊
以太坊被设计为一个“世界计算机”,其全局状态由一组账户状态映射构成。与比特币的 UTXO 模型不同,以太坊的账户模型更接近传统银行的“余额+流水”模式。每个账户独立存储其状态,交易直接修改账户的余额和存储字段。这种设计使得以太坊天然支持复杂的业务逻辑——智能合约可以持有资产、维护状态、响应外部调用,而无需像比特币那样通过脚本堆栈模拟状态。
账户模型的核心优势在于:可组合性。合约之间可以互相调用,共享状态,形成复杂的去中心化应用生态。例如,一个去中心化交易所(DEX)合约可以调用代币合约的转账函数,而代币合约的状态变更又会触发交易所合约的流动性更新。这种原子性的状态联动,是 UTXO 模型难以实现的。
4.2 外部账户(EOA)与合约账户(CA)的深度对比
以太坊账户分为两类,它们在创建方式、控制权、行为模式上存在根本差异。理解这些差异是编写安全智能合约的前提。
| 对比维度 | 外部账户(EOA) | 合约账户(CA) |
|---|---|---|
| 创建方式 | 由用户生成私钥,通过椭圆曲线算法(secp256k1)推导出公钥,再经 Keccak-256 哈希取后 20 字节生成地址。无需上链,本地即可完成。 | 由 EOA 发送一笔 to 字段为空的交易,交易数据(init code)经 EVM 执行后生成合约地址(创建者地址 + nonce 的哈希)。必须上链。 |
| 控制权 | 持有私钥即拥有完全控制权。私钥签名交易,网络验证签名后执行。 | 无私钥。控制权由合约代码中的逻辑决定(如 onlyOwner 修饰符)。代码一旦部署不可更改(除非设计代理模式)。 |
| 发起交易 | 可以主动发起任何交易(转账、调用合约、部署合约)。 | 不能主动发起交易。只能通过 EOA 或其他合约调用来触发执行。 |
| 代码与存储 | codeHash 为空哈希,storageRoot 为空。不存储任何代码或持久化变量。 |
codeHash 为 EVM 字节码的哈希,storageRoot 指向存储树的根节点,保存合约的持久化状态变量。 |
| Gas 支付 | 必须由 EOA 支付 Gas。Gas 从 EOA 余额中扣除。 | 自身无 Gas 概念。执行消耗的 Gas 由发起调用的 EOA 承担。 |
| 地址生成 | 由公钥哈希生成,可离线计算。 | 由创建者地址和 nonce 计算,或由 CREATE2 操作码指定 salt 计算。 |
4.3 账户状态字段逐一拆解
以太坊账户状态由四个字段组成,它们共同定义了账户在某一时刻的完整快照。这些字段存储在世界状态(World State)的 Merkle Patricia Trie 中,以账户地址为键。
4.3.1 nonce(交易计数)
对于 EOA,nonce 表示从该账户发出的交易总数。它从 0 开始,每发送一笔交易递增 1。nonce 的核心作用是防止重放攻击——如果交易不包含 nonce,攻击者可以反复广播同一笔签名交易,导致余额被多次扣减。同时,nonce 也决定了交易在区块中的执行顺序:矿工按 nonce 升序打包交易。
对于 CA,nonce 表示该合约创建的合约数量。每当合约执行 CREATE 操作码时,nonce 递增。注意:CA 的 nonce 不用于交易排序,因为 CA 不能主动发起交易。
4.3.2 balance(余额)
账户持有的以太币数量,以 Wei 为单位(1 ETH = 10^18 Wei)。余额在交易执行时实时更新。需要注意的是,余额的变更遵循“先扣费后执行”的原则:交易发起时,Gas 费用先从 EOA 余额中扣除;交易执行完毕后,剩余 Gas 退还。对于合约调用,如果合约内部发生转账,余额也会相应调整。
4.3.3 storageRoot(存储根)
这是一个 256 位的哈希值,指向账户存储树的根节点。存储树是一个 Merkle Patricia Trie,保存了合约的持久化状态变量。对于 EOA,storageRoot 为空树的哈希(即空字符串的 Keccak-256 哈希)。对于 CA,storageRoot 随着合约状态变量的读写而更新。存储是合约的“记忆”,每次状态变更都会导致 storageRoot 变化,进而影响世界状态的根哈希。
4.3.4 codeHash(代码哈希)
合约代码的 Keccak-256 哈希。对于 EOA,codeHash 是空字符串的哈希(固定值)。对于 CA,codeHash 在合约部署时写入,之后不可更改。EVM 在执行合约时,通过 codeHash 在状态数据库中查找对应的字节码。如果 codeHash 为空,则说明该账户不是合约,无法执行任何代码。
address.code.length 检查一个地址是否为合约。如果返回 0,则为 EOA;如果大于 0,则为 CA。但需注意,在合约构造函数执行期间,code 尚未写入,此时检查会返回 0。
4.4 案例拆解:从交易到状态变更的完整链路
为了将上述概念串联起来,我们模拟一个典型场景:用户 Alice 通过 MetaMask 向一个去中心化借贷合约存入 1 ETH。
- 初始状态:Alice 的 EOA 地址
0xA有 10 ETH,nonce = 5。借贷合约地址0xB有 100 ETH,storageRoot 指向包含“存款余额映射”的存储树。 - 交易构造:Alice 的 MetaMask 构造一笔交易:
to = 0xB,value = 1 ETH,data = deposit()函数选择器,nonce = 5,gasLimit = 100000,gasPrice = 20 Gwei。 - 签名与广播:MetaMask 使用 Alice 的私钥对交易签名,广播到网络。
- 矿工打包:矿工验证签名、nonce 和余额,将交易纳入区块。
- EVM 执行:
- 首先,从 Alice 余额中扣除 Gas 费上限(100000 × 20 Gwei = 0.002 ETH),Alice 余额变为 9.998 ETH。
- 执行
deposit()函数:合约读取 Alice 的地址,将其存款余额增加 1 ETH,更新 storageRoot。 - 将 1 ETH 从 Alice 转账到合约地址,Alice 余额变为 8.998 ETH,合约余额变为 101 ETH。
- 交易执行完毕,实际 Gas 消耗 80000,退还剩余 20000 × 20 Gwei = 0.0004 ETH 给 Alice。
- 状态更新:Alice 的 nonce 从 5 变为 6,余额更新;合约的 storageRoot 和 balance 更新。世界状态根哈希随之改变。
4.5 常见误区与总结复盘
在学习账户模型时,开发者常陷入以下误区,需要特别警惕:
- 误区一:认为合约可以“主动”执行。合约代码只有在被交易触发时才会执行,不存在后台自动运行。定时任务需要依赖外部 keeper(如 Chainlink Keepers)来发起交易。
- 误区二:混淆 nonce 的作用。EOA 的 nonce 用于防重放和排序,CA 的 nonce 仅用于合约创建计数。不要试图通过修改 nonce 来“加速”交易。
- 误区三:忽略 storageRoot 的更新成本。合约存储的读写是 Gas 消耗的大头(SSTORE 操作码消耗 20000 Gas 以上)。频繁修改状态变量会显著增加交易成本,应优先考虑事件日志或链下存储。
- 误区四:认为 EOA 可以持有代币。ERC-20 代币的余额映射存储在代币合约的 storageRoot 中,而非 EOA 的 balance 字段。EOA 的 balance 仅代表原生 ETH。