4、账户模型概述:外部账户与合约账户的区别、账户状态字段

以太坊的账户模型是其区别于比特币 UTXO 模型的核心设计之一。理解账户模型,是掌握智能合约执行、Gas 机制和去中心化应用(DApp)交互的基石。在以太坊中,所有状态变更——无论是转账、部署合约还是调用合约函数——本质上都是账户状态的变化。本章将深入剖析外部账户(EOA)与合约账户(CA)的本质区别,并逐一拆解账户状态中的关键字段,帮助你建立对以太坊底层数据结构的清晰认知。

以太坊双账户模型与状态字段 外部账户(EOA) 由私钥控制 · 主动发起交易 nonce 交易计数(从0开始) balance ETH 余额(Wei) storageRoot 空(无存储) codeHash 空哈希(无代码) 调用 合约账户(CA) 由代码控制 · 被动执行逻辑 nonce 合约创建交易计数 balance ETH 余额(Wei) storageRoot Merkle Patricia Trie 根 codeHash EVM 字节码哈希 关键区别:EOA 有私钥无代码,CA 有代码无私钥;CA 的 nonce 仅用于合约创建计数

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 计算。
关键理解:EOA 是“人”的化身,CA 是“程序”的化身。EOA 通过私钥签名表达意图,CA 通过代码逻辑响应意图。在以太坊中,只有 EOA 能主动发起交易,CA 永远是被动的。这一设计保证了系统的确定性——所有状态变更都有明确的触发源。

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 为空,则说明该账户不是合约,无法执行任何代码。

实战提示:在 Solidity 中,你可以通过 address.code.length 检查一个地址是否为合约。如果返回 0,则为 EOA;如果大于 0,则为 CA。但需注意,在合约构造函数执行期间,code 尚未写入,此时检查会返回 0。

4.4 案例拆解:从交易到状态变更的完整链路

为了将上述概念串联起来,我们模拟一个典型场景:用户 Alice 通过 MetaMask 向一个去中心化借贷合约存入 1 ETH。

  1. 初始状态:Alice 的 EOA 地址 0xA 有 10 ETH,nonce = 5。借贷合约地址 0xB 有 100 ETH,storageRoot 指向包含“存款余额映射”的存储树。
  2. 交易构造:Alice 的 MetaMask 构造一笔交易:to = 0xB,value = 1 ETH,data = deposit() 函数选择器,nonce = 5,gasLimit = 100000,gasPrice = 20 Gwei。
  3. 签名与广播:MetaMask 使用 Alice 的私钥对交易签名,广播到网络。
  4. 矿工打包:矿工验证签名、nonce 和余额,将交易纳入区块。
  5. 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。
  6. 状态更新:Alice 的 nonce 从 5 变为 6,余额更新;合约的 storageRoot 和 balance 更新。世界状态根哈希随之改变。
风险提示:如果 Alice 的 nonce 设置错误(例如重复使用 nonce=5),交易会被网络拒绝。如果 gasLimit 设置过低,交易会因 Out of Gas 失败,但已消耗的 Gas 不会退还。此外,合约代码中的漏洞(如重入攻击)可能导致状态被恶意篡改,因此审计合约逻辑至关重要。

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。
总结复盘:账户模型是以太坊的“神经系统”。外部账户(EOA)代表用户身份,通过私钥签名驱动交易;合约账户(CA)代表业务逻辑,通过代码响应调用。四个状态字段(nonce、balance、storageRoot、codeHash)共同定义了账户的完整状态。理解这些基础概念,是后续学习 Gas 机制、ERC 标准、代理模式和跨链桥的必备前提。建议读者在 Remix 或 Ganache 中实际部署一个简单合约,观察不同操作对状态字段的影响,以加深理解。