教程区块链区块链技术ch077.2 账户模型与状态转换

本页目录

以太坊没有 UTXO,只有账户。每个账户不是一张"支票",而更像是一个"银行账户"——有自己的余额、交易计数器、合约代码和持久化存储。这一节剖析账户的四种字段、 nonce 的防重放机制,以及以太坊黄皮书中定义的核心状态转换函数 Υ(σ,T)\Upsilon(\sigma, T)


7.2.1 两种账户类型:EOA 与合约账户(CA)

以太坊网络中的每个账户由 20 字节地址唯一标识。账户数据结构如下(RLP 编码):

text
Account = [nonce, balance, storageRoot, codeHash]
字段类型EOA合约账户 (CA)
nonceuint64已发送交易数已创建合约数(通过 CREATE/CREATE2)
balanceuint256持有 ETH(以 Wei 计,1 ETH = 10¹⁸ Wei)持有 ETH
storageRootbytes32空 Keccak 根(默认值)合约存储的 MPT 根
codeHashbytes32空 Keccak 根(默认值)部署时的字节码哈希

外部拥有账户(EOA):由私钥控制,是人类用户与以太坊交互的入口。EOA 的 storageRootcodeHash 为空。

合约账户(CA):由创建交易部署,由自身代码逻辑控制。CA 的 balance 可以非零(合约可持有 ETH)、storeageRootcodeHash 非空。

graph TD
    A[地址 0xabc...
    20字节] --> B{codeHash 为空?}
    B -->|是| C[EOA<br>用户控制
    nonce=发送tx数
    storage为空]
    B -->|否| D[CA<br>代码控制
    nonce=创建数
    storage=MPT根
    code=字节码哈希]
    style C fill:#e1f5e1
    style D fill:#ccebff

7.2.2 Nonce 的防重放机制

在没有账户模型的系统中,双重花费通过 UTXO 引用的唯一性防止。以太坊使用 nonce(递增序号)实现完全相同的目的:

txvalid    tx.nonce=σ[sender].nonce\text{tx}_{\text{valid}} \iff \text{tx.nonce} = \sigma[\text{sender}].\text{nonce}

一旦交易被执行,发送者账户的 nonce 自动递增:

σ[sender].nonce=σ[sender].nonce+1\sigma'[sender].\text{nonce} = \sigma[sender].\text{nonce} + 1

连续 nonce 要求 prevents 攻击

  • 若 tx.nonce 过低(已被使用)→ 拒绝(已存在交易);
  • 若 tx.nonce 过高(跳过当前序号)→ 放入队列等待中间 nonce 到来;
  • 若 tx.nonce 正确 → 执行,nonce 递增。

这意味着交易必须按严格顺序执行。用户无法故意以 nonce=5 和 nonce=6 发起两笔不同的用相同 ETH 的交易来制造双花——交易池会拒绝冲突。

ts
// account-nonce-sim.ts
// 纯内置:模拟 EOA 账户状态与 nonce 防重放

interface EOA { address: string; nonce: number; balance: bigint; }

class WorldState {
  private accounts: Map<string, EOA> = new Map();
  private nonceQueue: Map<string, Array<{ tx: { to: string; value: bigint; nonce: number } }>> = new Map();

  ensureAccount(addr: string): EOA {
    if (!this.accounts.has(addr)) {
      this.accounts.set(addr, { address: addr, nonce: 0, balance: 0n });
    }
    return this.accounts.get(addr)!;
  }

  sendTx(from: string, tx: { to: string; value: bigint; nonce: number; gasLimit: bigint; gasPrice: bigint }): string {
    const acc = this.ensureAccount(from);
    const cost = tx.value + (tx.gasLimit * tx.gasPrice);
    
    if (tx.nonce !== acc.nonce) {
      if (tx.nonce > acc.nonce) {
        // 未来 nonce:挂起
        const q = this.nonceQueue.get(from) || [];
        q.push({ tx });
        this.nonceQueue.set(from, q);
        return `queued_for_nonce_${tx.nonce}`;
      }
      return 'rejected_nonce_too_low';
    }
    if (cost > acc.balance) return 'rejected_insufficient_balance';
    
    // 执行
    acc.balance -= cost;
    const to = this.ensureAccount(tx.to);
    to.balance += tx.value;
    acc.nonce++;
    
    // 处理挂起的中间 nonce
    let next = this.nonceQueue.get(from)?.shift();
    // 简化:实际应递归处理直到 gap
    return `executed_at_nonce_${tx.nonce}`;
  }
}

const state = new WorldState();
const alice = '0xAlice';
state.ensureAccount(alice);
state.sendTx(alice, { to: '0xBob', value: 1000n, nonce: 0, gasLimit: 21000n, gasPrice: 20n }); // 转账给 Bob
console.log(`Alice nonce: ${(state['accounts'] as any).get(alice).nonce}`);
// 输出 nonce 从 0 递增到 1
// 若再次以 nonce=0 发送 → rejected_nonce_too_low

7.2.3 状态转换函数 Υ(σ,T)\Upsilon(\sigma, T)

以太坊黄皮书用数学形式定义了状态转换的通用框架。

对于任意交易 TT,及其发送者地址 S(T)S(T)

σ=Υ(σ,T)\sigma' = \Upsilon(\sigma, T)

状态转换的初始条件检查:

σ[S(T)].nonce=Tnandσ[S(T)].balanceTnp+v\sigma[S(T)].\text{nonce} = T_n \quad \text{and} \quad \sigma[S(T)].\text{balance} \geq T_n \cdot p + v

其中 TnT_n 为交易 nonce,pp 为 gas price,vv 为转账金额。若任一条件不满足,交易回滚(但发送者仍需支付已消耗的 Gas 作为 DoS 防护)。

_gas_开销的数学表达

g=min(g0+iTigi, glimit)g = \min\left(g_0 + \sum_{i \in T_i} g_i, \ g_{limit}\right)

其中 g0g_0 为交易基本开销,gig_i 为每字节/每个操作的 Gas,glimitg_{limit} 为交易设置的上限。


关键认知:以太坊的账户模型将"货币所有权"和"状态执行"统一到同一抽象中。Nonce 不只是计数器,而是整个账户历史执行序列的严格顺序保证。理解这一点,才能理解为何以太坊的交易池(mempool)远比比特币更复杂——它维护的不是一组 UTXO,而是一个按地址维度排序的 nonce 队列森林。


← 7.1 愿景 | 前往 → 7.3 以太坊状态机与状态树(MPT)

评论

0

评论加载中…

发表评论

0/2000