以太坊没有 UTXO,只有账户。每个账户不是一张"支票",而更像是一个"银行账户"——有自己的余额、交易计数器、合约代码和持久化存储。这一节剖析账户的四种字段、 nonce 的防重放机制,以及以太坊黄皮书中定义的核心状态转换函数 。
7.2.1 两种账户类型:EOA 与合约账户(CA)
以太坊网络中的每个账户由 20 字节地址唯一标识。账户数据结构如下(RLP 编码):
Account = [nonce, balance, storageRoot, codeHash]| 字段 | 类型 | EOA | 合约账户 (CA) |
|---|---|---|---|
| nonce | uint64 | 已发送交易数 | 已创建合约数(通过 CREATE/CREATE2) |
| balance | uint256 | 持有 ETH(以 Wei 计,1 ETH = 10¹⁸ Wei) | 持有 ETH |
| storageRoot | bytes32 | 空 Keccak 根(默认值) | 合约存储的 MPT 根 |
| codeHash | bytes32 | 空 Keccak 根(默认值) | 部署时的字节码哈希 |
外部拥有账户(EOA):由私钥控制,是人类用户与以太坊交互的入口。EOA 的 storageRoot 和 codeHash 为空。
合约账户(CA):由创建交易部署,由自身代码逻辑控制。CA 的 balance 可以非零(合约可持有 ETH)、storeageRoot 和 codeHash 非空。
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(递增序号)实现完全相同的目的:
一旦交易被执行,发送者账户的 nonce 自动递增:
连续 nonce 要求 prevents 攻击:
- 若 tx.nonce 过低(已被使用)→ 拒绝(已存在交易);
- 若 tx.nonce 过高(跳过当前序号)→ 放入队列等待中间 nonce 到来;
- 若 tx.nonce 正确 → 执行,nonce 递增。
这意味着交易必须按严格顺序执行。用户无法故意以 nonce=5 和 nonce=6 发起两笔不同的用相同 ETH 的交易来制造双花——交易池会拒绝冲突。
// 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_low7.2.3 状态转换函数
以太坊黄皮书用数学形式定义了状态转换的通用框架。
对于任意交易 ,及其发送者地址 :
状态转换的初始条件检查:
其中 为交易 nonce, 为 gas price, 为转账金额。若任一条件不满足,交易回滚(但发送者仍需支付已消耗的 Gas 作为 DoS 防护)。
_gas_开销的数学表达:
其中 为交易基本开销, 为每字节/每个操作的 Gas, 为交易设置的上限。
关键认知:以太坊的账户模型将"货币所有权"和"状态执行"统一到同一抽象中。Nonce 不只是计数器,而是整个账户历史执行序列的严格顺序保证。理解这一点,才能理解为何以太坊的交易池(mempool)远比比特币更复杂——它维护的不是一组 UTXO,而是一个按地址维度排序的 nonce 队列森林。
← 7.1 愿景 | 前往 → 7.3 以太坊状态机与状态树(MPT)
评论
0评论加载中…