JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

帳戶模型:餘額、nonce 與全域狀態

比特幣追蹤的是一枚枚散落的硬幣;以太坊保管的卻像一本銀行帳本,記的是餘額。來認識帳戶模型——一個全域狀態,一筆轉帳只是這邊扣、那邊加——並看清它為何如此適合智慧合約。

一本銀行帳本,而非一罐硬幣

上一篇你像 比特幣 那樣花錢:伸手到一罐彷彿摸得到的 未花費硬幣(UTXO) 裡,挑出湊得夠數的幾枚,整枚交出去,再把找零收回來。這行得通,但有點像買咖啡只能用你手上剛好面額的鈔票。現在改成想像你的銀行對帳單。銀行並不保管「你那幾張特定的鈔票」——它只記一個數字,也就是你的餘額,一筆付款不過是把你的數字調低、把別人的數字調高。後面這幅畫面就是 帳戶模型,也是 以太坊 與多數智慧合約鏈記帳的方式。

這差別不只是口味問題。在 UTXO 的世界裡,根本沒有任何地方存著一個叫「Alice 的餘額」的東西——她的餘額是你的錢包掃描整條鏈、找出她能花的硬幣後算出來的。而在帳戶的世界裡,餘額是直接寫進帳本的第一級事實。這篇接下來的一切,都從這個設計抉擇衍生而來。

兩種帳戶

以太坊恰好有兩種帳戶類型,且共用同一套位址空間(一個 20 位元組/40 個十六進位字元的識別碼)。外部擁有帳戶(EOA) 由私鑰控制——這就是你的 錢包 所管理的東西,唯有一枚有效的 數位簽章 才能動用其資金。合約帳戶 則不靠鑰匙、改由程式碼控制;它是一份 智慧合約,被呼叫時就執行。光看位址你分不出誰是誰,但鏈上為兩者保存的內容並不相同。

不論哪一種,每個帳戶都保存著相同的四個欄位。其中兩個——nonce餘額——所有帳戶都有;另外兩個——儲存根(storage root)與程式碼雜湊(code hash)——只對合約有意義,對 EOA 而言它們是空的。

// The state stored for ONE Ethereum account (the "account state")
Account {
    nonce:       uint    // EOA: number of txs sent. Contract: number of contracts it created.
    balance:     uint    // amount owned, in wei  (1 ETH = 10^18 wei)
    storageRoot: hash    // Merkle root of this contract's key->value storage (EOA: empty)
    codeHash:    hash    // hash of this account's EVM code               (EOA: hash of "")
}
EOA 只用到 nonce +餘額;合約則額外指向自己的程式碼與專屬的儲存樹。

全域狀態:一張巨大的地圖

如果每個帳戶是一筆紀錄,那麼全域狀態(world state) 就是整個檔案櫃:一張把每個位址對應到其帳戶狀態的單一映射。在概念上,它不過就是一本字典。

worldState = {
  "0xAlice...": { nonce: 3, balance: 5_000000000000000000, code: none      },  // EOA, 5 ETH
  "0xBob...":   { nonce: 0, balance: 2_000000000000000000, code: none      },  // EOA, 2 ETH
  "0xToken..":  { nonce: 1, balance: 0,                     code: 0x6080.., storageRoot: 0x9f.. }  // a contract
}
某一瞬間的全域狀態:誰存在、各自持有什麼。每產生一個新區塊,就會產出這張地圖的一個新版本。

當然,你沒辦法把上百萬個帳戶塞進一個區塊。所以以太坊並不把這張地圖存「在」區塊裡——它只把整張地圖的一枚 32 位元組指紋,也就是狀態根(state root),存進 區塊標頭。這枚指紋正是一棵 Merkle Patricia Trie(默克爾帕特里夏樹) 的根:一棵以位址為鍵、建立在 Keccak-256 之上的雜湊樹,於是任何帳戶的任何改動——哪怕只搬動了一個 wei——都會改變這個根。這正是帳本能「一改就露餡」的原因:一個節點拿到整份狀態後,能立刻驗算它是否雜湊到全鏈共識的那個根,這跟先前某篇講到的區塊交易 Merkle 根,是同一個把戲。

所以一個區塊其實根本不含餘額。它含的是對狀態的一份承諾(commitment),外加一串把前一個狀態轉換成現在這個狀態的交易。執行這些交易,就是所謂的狀態轉移函數:`新狀態 = apply(舊狀態, 區塊.交易)`。說到底,整條鏈就是一份人人都能重新跑一遍、並達成一致的公開狀態轉移紀錄。

一筆轉帳的解剖

重頭戲來了。在 UTXO 的世界裡,匯款意味著挑硬幣、組裝輸入與輸出、再把找零繞回給自己。而在帳戶模型裡,一筆轉帳簡單到近乎難為情:扣掉發送方,加給接收方。 我們連同 gas 一起,照節點實際的做法把它走一遍。

Alice 有 5 ETH,nonce 為 3。她簽署一筆 交易,要把 1 ETH 寄給 Bob(Bob 原有 2 ETH)。假設打包進鏈要付 0.001 ETHgas 費。一個驗證中的節點會這樣檢查並套用它:

  1. 簽章:從簽章中還原出發送方,確認它就是 Alice 的位址。沒有有效簽章,就沒有交易。
  2. Nonce 檢查:交易的 nonce 必須等於 Alice 目前的帳戶 nonce(3)。不能是 2(已用過),也不能是 4(跳太前面)——必須剛好是下一個。
  3. 餘額檢查:Alice 的餘額必須足以涵蓋金額+手續費,即 5 ≥ 1 + 0.001。成立。
  4. 套用效果:從 Alice 扣掉 1.001(→ 3.999),加 1 給 Bob(→ 3.0),並把 0.001 付給區塊的手續費收受者。
  5. 遞增 nonce:把 Alice 的 nonce 設為 4,永久讓這筆交易退場,使它再也無法被套用第二次。
def apply_transfer(state, tx):
    sender = recover_address(tx.signature, tx)      # 1. who signed?
    acct   = state[sender]
    assert tx.nonce == acct.nonce                   # 2. exactly the next nonce
    fee = tx.gas_used * tx.gas_price
    assert acct.balance >= tx.value + fee           # 3. can she afford it?

    acct.balance            -= tx.value + fee        # 4. debit sender (value + gas)
    state[tx.to].balance    += tx.value              #    credit recipient
    state[coinbase].balance += fee                   #    pay the block producer
    acct.nonce              += 1                      # 5. retire this tx forever
    return state
整筆轉帳就是一次狀態轉移。注意接收方完全不必「接受」任何東西——他們的餘額就這麼漲了上去。

Nonce:每個帳戶的私人計數器

nonce 在上面默默做了關鍵的工,所以我們把聚光燈打在它身上。對一個 EOA 而言,它就只是「這個帳戶至今送出過的交易數量」——一個每帳戶各自的計數器,從 0 開始,每送出一筆就恰好加一。它一口氣解決了兩個問題。

排序。 由於每筆交易都必須依序使用下一個 nonce,網路對你送出的所有東西都有一個毫不含糊的順序:nonce 5 不可能搶在 nonce 4 之前被處理。防重放。 一旦 nonce 3 被礦工打包、你的帳戶前進到 nonce 4,攻擊者若把你那筆已簽好的 nonce-3 交易複製下來重新廣播,就會被拒絕——它已對不上你目前的 nonce。這正是 重放攻擊,而 UTXO 是用另一種方式防範它(一枚硬幣一旦花掉,就乾脆不在 UTXO 集合裡了)。

這條依序規則帶來一個錢包必須處理的實務後果。如果你廣播了 nonce 4 卻卡住了(手續費太低),接著又廣播 nonce 5,那麼第二筆在 4 先動之前根本無法被打包——它卡在一個 nonce 缺口後面。解法是 nonce 管理:用更高的手續費重送 nonce 4(即「以費換位/replace-by-fee」),或在 nonce 4 發一筆什麼都不做的 0-ETH 交易來清出通路。這就是為什麼一筆凍住的待處理交易,會把你後面所有交易全卡死。

帳戶對上 UTXO:真正的取捨

兩種模型沒有誰單純「比較好」——它們各自交換了不同的東西,這也是為什麼比特幣與以太坊各自選了最契合自身目標的那一個。

帳戶的勝場。 這套模型很直覺(就是銀行餘額),交易更小更簡單(不必張羅輸入/輸出或找零),而且——這是決定性的——帳戶讓一份 智慧合約 有了一個帶著持久、可變狀態的安穩住所。一份代幣合約可以維護一張 `balances` 映射,年復一年就地更新。正是這種與「有狀態程式」的契合,成為 以太坊 選擇用帳戶來當「世界電腦」的核心理由。

UTXO 的勝場。 各自獨立的硬幣可以平行驗證——兩筆無關的 UTXO 花費不會碰到任何共用資料,而從同一個帳戶發出的兩筆轉帳,卻會爭搶同一個餘額與同一個 nonce,被迫只能依序處理。UTXO 在預設下也較不洩漏隱私(每筆付款換一個全新找零位址很容易),而一個帳戶位址卻會累積一份永久、可被串連的歷史。此外,一枚 UTXO 的有效性更為自足,這讓輕用戶端的證明更簡單。

現在,帳本記帳的兩大流派你都掌握了,也懂了把帳戶鏈綁在一起的那個 全域狀態。下一篇,我們會跟著一筆組裝完成、已簽好的交易,離開你的錢包、穿過網路、進入區塊——正是這趟旅程,把這一切狀態變成一筆獲得確認的付款。