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

UTXO 模型:把錢看成可拼合與找零的未花費硬幣

比特幣不在任何地方存你的「餘額」——它存的是一枚枚硬幣。跟著 Alice 把兩個 UTXO 拼起來付給 Bob、再拿回找零,看看「把錢當成未花費的硬幣」為何能讓雙重支付容易被抓、讓交易容易被平行驗證。

把你的錢想成口袋裡的一把硬幣

想像你用現金買一份 130 元的午餐。你手上沒有剛好 130 元的鈔票——你遞給店員一張 100 元和一張 50 元,總共 150 元,然後拿回 20 元找零。注意你從來不做的事:你絕不會從那張 100 元的邊緣削下 30 元。實體的錢是以整張為單位流動的。你把鈔票起來付帳,再以全新的鈔票形式拿回找零。

比特幣追蹤金錢的方式幾乎一模一樣。它的「鈔票」叫做 UTXO——未花費交易輸出(Unspent Transaction Output)——每一筆付款都會把其中幾個拼起來、再產出全新的幾個。一旦掌握這個核心觀念,比特幣許多看似古怪的行為就都說得通了。

帳本裡沒有餘額,只有未花費的硬幣

這一點最常讓新手吃驚:比特幣不儲存任何餘額。 銀行資料庫裡有一列寫著「帳號 123:500 元」。比特幣裡哪裡都沒有這樣一列。取而代之的是,每個全節點都維護著一份 UTXO 集合——目前存在、且尚未被花掉的每一枚硬幣的完整集合——而每一枚硬幣上都貼著一把鎖,標明誰有權花用它。

那麼,你的餘額究竟是什麼?它是你的錢包算出來的一個數字,而不是鏈上儲存的數字。錢包會掃過整個 UTXO 集合,找出所有能用你的金鑰解鎖的輸出,把它們加總起來。如果你掌控著三個分別價值 0.6、0.7、0.05 BTC 的 UTXO,錢包會顯示 1.35 BTC——但在鏈上,那是三枚各自獨立的硬幣,而不是一個你可以直接修改的餘額。

一筆實例付款:Alice 付給 Bob 1 BTC

Alice 恰好掌控兩個 UTXO:UTXO_A = 0.6 BTCUTXO_B = 0.7 BTC。她想付給 Bob 1.0 BTC。任何一枚硬幣單獨都不夠,所以——就像剛才的現金例子——她必須把兩枚拼起來。以下是她的錢包組出來的東西:

  1. 挑選輸入。 錢包選了 UTXO_A(0.6)和 UTXO_B(0.7),合計 1.3 BTC——足以涵蓋 1.0 再加上一點手續費。
  2. 建立給 Bob 的輸出。 一個 1.0 BTC 的輸出,鎖定給 Bob 的位址,這樣日後只有 Bob 能花用它。
  3. 建立找零輸出。 剩下的金額不能憑空消失,所以錢包做了第二個輸出——也就是找零——0.2999 BTC,鎖回給一個 Alice 掌控的位址。
  4. 手續費就是剩下的部分。 輸入 − 輸出 = 1.3 −(1.0 + 0.2999)= 0.0001 BTC。並沒有一個明確的手續費欄位;把這筆交易納入區塊的礦工,直接把差額收下。
  5. 簽章並廣播。 Alice 用她的私鑰為每個輸入簽章,證明她有權解鎖那些硬幣,接著把交易送給她的對等節點。
{
  "txid": "f5d8e1...c92a",            // id of THIS transaction
  "vin": [                            // inputs: pointers to coins being spent
    { "txid": "a1b2...", "vout": 0 },   // UTXO_A, worth 0.6 BTC
    { "txid": "c3d4...", "vout": 1 }    // UTXO_B, worth 0.7 BTC
  ],
  "vout": [                           // outputs: brand-new coins created
    { "value": 1.0000, "scriptPubKey": "<lock to Bob's address>" },
    { "value": 0.2999, "scriptPubKey": "<lock to Alice's change address>" }
  ]
}
// fee = (0.6 + 0.7) - (1.0 + 0.2999) = 0.0001 BTC, kept by the miner
Alice 的交易。輸入只是指標;金額其實存在它們所指向的那些輸出裡。

注意這個不對稱:輸入本身不帶金額。 每個輸入只是指向某個較早輸出的指標——指明是哪一筆交易(`txid`)的第幾個輸出(`vout`)。0.6 與 0.7 這兩個數值其實存在被指向的那些輸出裡,驗證者得逐一查表,才知道它們各值多少。

輸入、輸出,與每枚硬幣上的那把鎖

每個輸出有兩個部分:一個金額,以及一個鎖定條件——在比特幣裡,是一段叫做 `scriptPubKey` 的小腳本。最常見的鎖實際上是在說:「誰能拿出這個位址背後私鑰所產生的有效簽章,誰就能花用我。」

每個輸入同樣有兩個部分:一個指向它所花用的前一個輸出的指標(`txid:vout`),以及一個解鎖證明——一段滿足那個輸出之鎖的簽章(即 `scriptSig`/witness)。節點會把鎖與解鎖兜在一起,當成一支小小的堆疊程式來執行;只要結果為真,這個輸入就獲得授權。

# Output lock (scriptPubKey) - pay-to-public-key-hash, the common case:
    DUP  HASH160  <PubKeyHash>  EQUALVERIFY  CHECKSIG

# Input unlock (scriptSig / witness) supplied by the spender:
    <signature>  <publicKey>

# A node runs unlock + lock as ONE stack program. It passes only if:
#   1. hash160(publicKey) == <PubKeyHash>   -> right key for this coin
#   2. CHECKSIG verifies <signature> over this tx using publicKey
一個比特幣 pay-to-public-key-hash 的鎖,以及解開它的那份證明。

那份簽章是用 secp256k1 曲線上的 ECDSA 製作的,而且關鍵在於:它涵蓋了這筆交易的內容。所以你沒辦法抓走 Alice 簽好的輸入、再把它接到另一筆付款上——那會改變被簽章的內容,簽章也就驗不過了。

花費會就地更新 UTXO 集合:被消耗的硬幣被移除,新的硬幣被加入。但歷史本身永不改變——舊區塊永遠維持當初寫下的樣子。集合隨時間有增有減,而它背後那條鏈始終是「只追加」的,這正是帳本不可竄改性的根源。

為何要這樣設計?雙重支付偵測與平行驗證

現在來看回報。因為每一枚硬幣都是 UTXO 集合裡一個獨立的條目,花用某一枚就是一個結構性事件:驗證者去查表,若它在表裡,就移除它。如果某枚硬幣根本不在集合裡,這筆花費就直接被拒——它要嘛從未存在,要嘛早已被花掉。以下基本上就是整套驗證規則:

def validate(tx, utxo_set):
    in_sum = 0
    for inp in tx.inputs:
        utxo = utxo_set.get(inp.txid, inp.vout)
        if utxo is None:
            reject("input missing: already spent or never existed")
        if not verify_signature(inp, utxo.lock, tx):
            reject("bad signature: spender not authorized")
        in_sum += utxo.value

    out_sum = sum(o.value for o in tx.outputs)
    if out_sum > in_sum:
        reject("outputs exceed inputs: cannot mint money")
    fee = in_sum - out_sum

    # commit: consume the inputs, create the new outputs
    for inp in tx.inputs:
        utxo_set.remove(inp.txid, inp.vout)
    for i, o in enumerate(tx.outputs):
        utxo_set.add(tx.txid, i, o)
驗證一筆 UTXO 交易,主要就是一次集合查表、一次簽章檢查,再加一次算術比較。

雙重支付——試圖把同一枚硬幣花兩次——就此變成一個直接的衝突。第一筆交易把 UTXO_A 從集合裡移除;第二筆同樣指向 UTXO_A 的交易在 `get` 這步就失敗、被拒。誠實的節點會在這筆衝突交易抵達任何區塊之前,就把它從記憶池(mempool)中丟掉。

第二個好處是平行驗證。因為每個輸入都精確點名它所觸及的硬幣,兩筆作用在互不相交的 UTXO 上的交易彼此不會干擾,所以節點可以同時在不同 CPU 核心上驗證它們的簽章——Bitcoin Core 真的就是這麼做的。這裡沒有一個所有交易都得依序更新的共享「餘額」計數器。對照你接下來會學到的帳戶模型:同一帳戶的兩筆轉帳,必須依嚴格的 nonce 順序,對同一個可變餘額逐一套用。

錢包暗地裡的工作:選幣、找零與取捨

這一切意味著錢包做的事,遠比它顯示出來的多。決定要把哪些 UTXO 餵進一筆付款,是一套真正的演算法,叫做選幣(coin selection);它是個貨真價實的最佳化問題,兩邊都有實際代價。

  1. 用盡可能少的輸入,去涵蓋「金額加上手續費」——每多一個輸入都會讓交易變大,而手續費是按位元組計、不是按比特幣計。
  2. 盡量選會留下乾淨找零、甚至完全不需找零的組合,以免製造出灰塵(dust)——小到「花它的手續費比它本身還貴」的硬幣。
  3. 把找零送到一個全新的位址,而不是送回某個輸入位址,這樣旁觀者就無法一眼看出哪個輸出是付款、哪個是找零——這是一種隱私上的作法。

把找零送到新位址,也正是為什麼錢包會在單一餘額背後,悄悄管理許多個位址,以及為什麼大家不鼓勵重複使用位址。即便如此,鏈上分析的啟發式手法——例如「假設同一筆交易的所有輸入屬於同一個擁有者」——往往還是能把它們重新串連起來。UTXO 的隱私性勝過帳戶模型,但離完美還很遠。

UTXO 模型替你換來了天生的平行性、極其單純的雙重支付規則,以及更好的隱私——但這些好處是用複雜度換來的。沒有一個整潔的「帳戶餘額」可以直接讀;錢包必須追蹤一整組硬幣;而要寫出豐富的有狀態程式也很彆扭,因為沒有一個可持久保存、可變動的帳戶,能讓合約把資料放進去。這正是為什麼以太坊選擇了你下一篇會學到的帳戶模型——也是為什麼像閃電網路這類比特幣擴容層,會把支付通道建在 UTXO 之上,而不是去重寫底層記帳的方式。