以太坊密碼學原語互動式教學:從橢圓曲線到 Merkle Patricia Tree

本文以互動式教學方式解析以太坊核心密碼學原語,包括 Keccak 雜湊函數的海綿結構與實作、secp256k1 橢圓曲線密碼學的數學原理、ECDSA 簽章機制與 recovery id 解析、Merkle Patricia Tree 的混合設計與狀態驗證。透過大量圖解說明和 Python 程式碼範例,讓讀者從直觀理解密碼學而非陷入數學公式的泥沼。

以太坊密碼學原語互動式教學:從橢圓曲線到 Merkle Patricia Tree

說到以太坊的密碼學,很多人第一反應就是「不懂」「太數學了」「看著就頭疼」。我也是這麼走過來的——剛開始學區塊鏈的時候,每次看到「橢圓曲線」四個字就自動跳過,心想「反正我知道公鑰私鑰是怎麼回事就行了」。

後來被現實毒打多了,才發現不懂密碼學真的很吃虧。比如:

這些問題不懂密碼學根本沒法回答。所以今天這篇文章,我決定用一種輕鬆有趣的方式,把以太坊核心的密碼學原語全部拆解給你看。會有一些數學,但我不會讓你算微分方程式;會有一些程式碼,但我不會讓你一行行去實現。我們的目的不是成為密碼學博士,而是搞懂「為什麼以太坊這樣設計」。

起點:Keccak 雜湊函數——區塊鏈的萬物之源

為什麼區塊鏈離不開雜湊函數?

在開始講 Keccak 之前,我想先回答一個根本問題:為什麼區塊鏈這麼喜歡用雜湊函數?

答案超級簡單:三個特性讓它成為區塊鏈的命根子。

雜湊函數的三個核心特性:

1. 確定性(Deterministic)
   hash("hello") 每次算出來都是一樣的
   不像隨機數,同樣輸入每次結果不同
   
2. 單向性(One-way)
   從輸出反推輸入是不可能的
   就像從煎好的漢堡推不回生肉排
   
3. 抗碰撞(Collision Resistance)
   幾乎不可能找到兩個不同輸入產生相同輸出
   就像找不到兩把完全不同的鑰匙開同一把鎖

區塊鏈處處用到這些特性:

Keccak 的故事:NIST 競賽的贏家

Keccak 不是橫空出世的。2007 年,美國國家標準與技術研究院(NIST)舉辦了一個公開競賽,要找一個新的 SHA-3 雜湊函數標準。經過幾年的激烈角逐,Keccak 在 2012 年擊敗其他競爭者,成為 SHA-3 的基礎。

不過有個小插曲:最終標準出來時,NIST 做了一個「微妙的」修改,叫做「調整參數」。這個調整讓很多密碼學家很不爽,覺得這破壞了 Keccak 的某些數學特性。不過這是個政治議題而非技術議題,咱們就不展開了。

以太坊選擇了原始的 Keccak(而非 NIST 調整後的版本),所以你在以太坊看到的是 keccak256 而不是 sha3_256。這也是為什麼有時候你在網上搜到的 SHA-3 教程算不出以太坊的結果——因為根本不是同一個函數!

動手玩:keccak256 的直觀理解

# 用 Python 計算 keccak256
from web3 import Web3

# 這個你應該很熟悉:以太坊地址就是 keccak256 公鑰的後 20 bytes
message = "Hello, Ethereum!"
message_hash = Web3.keccak(text=message)

print(f"原始訊息: {message}")
print(f"Hash (hex): {message_hash.hex()}")
print(f"Hash (bytes): {message_hash}")
print(f"長度: {len(message_hash)} bytes")

# 試著改一個字元看看 hash 變成什麼
message2 = "Hello, Etherelum!"  # 把 'e' 改成 'e'
hash2 = Web3.keccak(text=message2)

print("\n--- 改了一個字元後 ---")
print(f"Hash (hex): {hash2.hex()}")
print(f"和原來的 hash 一樣嗎? {message_hash == hash2}")
執行結果(概念):
原始訊息: Hello, Ethereum!
Hash (hex): 0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b
長度: 32 bytes (256 bits)

--- 改了一個字元後 ---
Hash (hex): 0x9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e
和原來的 hash 一樣嗎? False

注意到了嗎?只改了一個字元,整個 hash 翻天覆地。這就是所謂的「雪崩效應」——輸入的一點點變化,會導致輸出完全不同。這種特性讓 hash 成為區塊鏈「指紋」功能的完美選擇。

Keccak 的內部結構:不看會後悔

Keccak 的核心是一個叫做「海綿結構」(Sponge Construction)的設計。這名字挺浪漫的對吧?想象一塊海綿在吸收數據,然後擠出精華。

Keccak 海綿結構:

┌─────────────────────────────────────────────────────┐
│                    海綿函數                          │
├─────────────────────────────────────────────────────┤
│                                                     │
│   ┌─────────┐     ┌─────────┐     ┌─────────┐    │
│   │ 吸收階段 │ → │ 擠出階段 │     │  狀態    │    │
│   │(Absorb) │     │(Squeeze)│     │ (1600b) │    │
│   └─────────┘     └─────────┘     └─────────┘    │
│                                                     │
│   輸入數據被切成小塊,一塊一塊「吸收」進去           │
│   吸收完後,一塊一塊「擠出」作為輸出                │
│                                                     │
└─────────────────────────────────────────────────────┘

1600 bits 的狀態 = 5×5×64 = 800 bytes
這是一個 5×5 的「片」(plane),每個片 64 bits

每一輪的運算叫做「round」,總共有 24 個 rounds。每個 round 包括五個步驟:

數學細節我就不展開了——說實在的,我也不完全搞懂了每一個步驟背後的數學證明。重點是:這整套設計被密碼學家仔細分析過,目前沒有發現已知的弱點。

橢圓曲線密碼學:你的私鑰其實是個大秘密

橢圓曲線不是橢圓

這裡有個冷知識:橢圓曲線跟橢圓其實沒什麼關係。它之所以叫「橢圓」,是因為它的公式跟計算橢圓周長的積分有點像——僅此而已。

以太坊用的是一條叫做 secp256k1 的橢圓曲線。它的方程式很簡單:

y² = x³ + 7 (在有限域 Fp 上)

就這樣?對,就這麼簡單。當然,實際上是在一個很大的有限域上計算,而且所有運算都要 mod p,其中:

p = 2^256 - 2^32 - 2^9 - 2^8 - 2^7 - 2^6 - 2^4 - 1
  = 115792089237316195423570985008687907853269984665640564039457584007908834671663

這個數字大得荒謬——大到你寫都寫不完整。但它正好讓你在 256 bits 的空間裡工作,這是電腦最舒服的位元數。

橢圓曲線上的「加法」

橢圓曲線有個神奇的性質:在這條曲線上,你可以定義一種「加法」運算,而且這種加法滿足交換群的所有特性——封閉性、結合性、單位元、逆元素。

橢圓曲線加法的幾何意義:

情況一:P + Q(P 和 Q 是不同的點)
        把 P 和 Q 連成一條線
        這條線會和曲線交於第三個點 R'
        然後把 R' 沿 X 軸反射,得到 R
        R 就是 P + Q 的結果

情況二:P + P = 2P(倍點)
        在 P 點畫切線
        切線和曲線交於 Q
        把 Q 反射得到 2P

數學上,這個加法的公式是:

假設 P(x1, y1), Q(x2, y2)

P ≠ Q 時:
  λ = (y2 - y1) / (x2 - x1) mod p
  x3 = λ² - x1 - x2 mod p
  y3 = λ(x1 - x3) - y1 mod p

P = Q 時(倍點):
  λ = (3x1² + a) / (2y1) mod p
  x3 = λ² - 2x1 mod p
  y3 = λ(x1 - x3) - y1 mod p

我知道你看到這些公式想關掉視窗了。深呼吸,讓我給你一個更直觀的理解:

把橢圓曲線想成一個時鐘,只不過這個時鐘有 2^256 個刻度。在這個時鐘上,「加法」就是沿著曲線往前跳。跳一下是 G,跳兩下是 2G,跳 k 下是 kG。

公鑰和私鑰的誕生

這就是以太坊密碼學最優雅的部分——單向函數。

私鑰:一個隨機的 256 位整數,記作 k
公鑰:k 倍的生成點,記作 K = k × G

其中 G 是 secp256k1 的生成點
G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
     0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8)

整個以太坊的安全性都建立在這個假設上:從 k 計算 K 很簡單,但從 K 反推 k 是不可能的。這叫做「離散對數問題」(Discrete Logarithm Problem),是密碼學家認為無法在合理時間內解決的問題。

# 實際操作看看
from eth_keys import keys
import os

# 生成的私鑰
private_key_bytes = os.urandom(32)
private_key = keys.PrivateKey(private_key_bytes)

# 推導公鑰
public_key = private_key.public_key
print(f"私鑰 (32 bytes): {private_key_bytes.hex()}")
print(f"公鑰 (64 bytes): {public_key.to_bytes().hex()}")
print(f"公鑰長度: {len(public_key.to_bytes())} bytes")

# 從公鑰無法反推私鑰——這就是密碼學的力量

以太坊地址是怎麼來的?

現在我們知道了公鑰。但以太坊地址不是直接用公鑰——而是經過兩次 hash:

地址計算流程:

1. 取公鑰(64 bytes,無壓縮格式)
   04 || X || Y

2. 對整個公鑗做 keccak256
   keccak256(04 || X || Y)
   
3. 取 hash 的最後 20 bytes
   address = keccak256(...)[12:]

4. 通常顯示為 0x... 格式
   address = "0x" + last_20_bytes_hex

為什麼只取最後 20 bytes?這是歷史原因和方便性的平衡。20 bytes = 40 個十六進位字元,長度適中,不太短也不太長。取「最後」而不是「最先」沒有密碼學原因,純粹是當年做設計時的一個任意選擇。

# 完整的地址推導
from web3 import Web3
from eth_keys import keys

# 生成一個測試私鑰
test_private_key = bytes.fromhex(
    'a' * 64  # 這只是演示用,別用真的!
)

private_key = keys.PrivateKey(test_private_key)
public_key = private_key.public_key

# 以太坊地址:keccak256(公鑰)[12:]
public_key_bytes = public_key.to_bytes()
address = Web3.keccak(public_key_bytes)[12:].hex()

print(f"公鑰: {public_key_bytes.hex()}")
print(f"以太坊地址: 0x{address}")

ECDSA 簽章:證明你擁有私鑰

簽章的直覺

ECDSA(橢圓曲線數位簽章算法)讓你可以「證明」你擁有某個私鑰對應的公鑰,但又不把私鑰洩漏出去。這就像你在一份文件上簽名——銀行可以驗證是你的筆跡,但他們不會要求你把印章交給他們。

ECDSA 的基本原理:

簽名過程(你需要私鑰):
1. 選一個隨機數 k
2. 計算 R = k × G
3. 計算 r = R.x(取 R 的 x 座標)
4. 計算 s = k⁻¹ × (hash + r × private_key) mod p

驗證過程(只需要公鑰):
1. 計算 w = s⁻¹ mod p
2. 計算 u1 = hash × w mod p
3. 計算 u2 = r × w mod p
4. 計算 P = u1 × G + u2 × public_key
5. 驗證 P.x = r

如果成立,代表簽名有效

這套機制厲害的地方在於:驗證者從頭到尾都接觸不到私鑰或隨機數 k。只能看到 (r, s) 這個簽名組合,然後決定「有效」或「無效」。

recovery id 的秘密

如果你用 Web3.js 或其他庫生成過以太坊交易,你會發現簽名返回的不只是 r 和 s,還有一個 v 值(通常叫 recovery id 或 rec_id)。這個 v 的作用是什麼?

答案是:用來從簽名「恢復」出簽名者的地址。

從簽名恢復地址的原理:

給定 (r, s, v) 和消息 hash,有兩個可能的公鑰:
- 一個是 r 對應的 x 座標(假設 y 是偶數)
- 一個是 r 對應的 x 座標(假設 y 是奇數)

v 用來指定用哪個:
- v = 27 或 0:y 是偶數
- v = 28 或 1:y 是奇數

所以從簽名可以唯一確定公鑰

這就是以太坊著名的「EOA 帳戶模型」的基礎。沒有智能合約錢包之前,每筆交易只需要簽名——驗證者可以從簽名直接恢復出發送者地址,不需要事先知道這個地址。這種設計極大地簡化了帳戶模型。

// 用 ethers.js 看看簽名的結構
const ethers = require('ethers');

const wallet = new ethers.Wallet.createRandom();
const message = "Hello, Ethereum!";
const signature = wallet.signMessage(message);

console.log("簽名組成:");
console.log("r:", signature.slice(0, 66));
console.log("s:", "0x" + signature.slice(66, 130));
console.log("v:", parseInt(signature.slice(130, 132), 16));

// 驗證簽名
const recoveredAddress = ethers.utils.verifyMessage(message, signature);
console.log("原始地址:", wallet.address);
console.log("恢復地址:", recoveredAddress);
console.log("匹配:", wallet.address === recoveredAddress);

Merkle Patricia Tree:以太坊的大殺器

為什麼區塊鏈需要 Merkle Tree?

先從普通 Merkle Tree 說起。假設你有 4 筆交易:

交易 A: "Alice 轉給 Bob 1 ETH"
交易 B: "Carol 轉給 Dave 2 ETH"
交易 C: "Eve 轉給 Frank 3 ETH"
交易 D: "Grace 轉給 Helen 4 ETH"

你把這些交易的 hash 組織成一個 Merkle Tree:

                    Root
                   /    \
              HashAB     HashCD
              /    \     /    \
          HashA  HashB HashC  HashD
            |      |     |      |
            A      B     C      D

現在的好處是:

在比特幣和以太坊中,Merkle Tree 讓「輕客戶端」可以只下載區塊頭(header),就能驗證某筆交易確實在區塊中。這是 SPV(Simplified Payment Verification)的核心。

Patricia Trie:高效查詢的前綴樹

普通 Merkle Tree 的問題是:如果你要查「所有以 0x1234 開頭的帳戶」,Merkle Tree 完全幫不上忙。

Patricia Trie(又叫前綴樹或基數樹)解決了這個問題。它的特點是:

Patricia Trie 的查詢:
假設儲存了以下鍵值對:
- "apple": 100
- "app": 200
- "application": 300
- "banana": 400

儲存結構:
root
└── "app"  # 共用前綴
    ├── "" -> 200 (app 本身)
    ├── "lication" -> 300
    └── "le" -> 100
└── "banana" -> 400

查詢 "apple":
root → "app" → "le" → 找到!
時間複雜度 = O(key長度),不是 O(n)

每一步都跟 key 的下一個字元相關,所以查詢時間只取決於 key 的長度,與總共有多少資料無關。以太坊的帳戶有數百萬個,但查詢任意帳戶的狀態只需要十幾步。

Merkle Patricia Tree:以太坊的混合設計

以太坊沒有單純用 Merkle Tree 或 Patricia Trie,而是發明了一種混合結構:Merkle Patricia Trie(MPT)。這個設計結合了兩者的優點。

以太坊有三棵主要的 MPT:

1. 狀態 Trie(State Trie)
   - 儲存所有帳戶的完整狀態
   - Key = sha3(以太坊地址) 的最後 64 個 nibbles(4 bits)
   - Value = rlp(帳戶資訊)
   
2. 儲存 Trie(Storage Trie)
   - 每個合約帳戶有自己的 Storage Trie
   - 儲存合約的所有狀態變數
   - Key = sha3(變數 slot) 的最後 64 個 nibbles
   
3. 交易 Trie(Transaction Trie)
   - 每個區塊有自己的 Transaction Trie
   - Key = 交易在區塊中的索引
   - Value = 交易的 RLP 編碼
   
4. 收據 Trie(Receipt Trie)
   - 每個區塊有自己的 Receipt Trie
   - 儲存交易的執行結果

MPT 的節點有三種類型:

MPT 節點類型:

1. 分支節點(Branch Node)
   - 17 個子節點(0-15 的路徑 + 1 個 value)
   - 用於分叉點
   
2. 葉子節點(Leaf Node)
   - 路徑編碼(nibbles)
   - 終端值
   
3. 擴展節點(Extension Node)
   - 共用的路徑前綴
   - 單個子節點
   - 用於壓縮常見前綴

實際看看 MPT 的結構

# 用一個簡化的例子看看 MPT 的運作
# 這不是真正的以太坊客戶端代碼,只是教學示範

# 假設我們有兩個帳戶:
# 地址 0x1234...:餘額 1 ETH, nonce 0
# 地址 0x5678...:餘額 2 ETH, nonce 1

# 第一步:計算路徑
# 路徑 = keccak256(地址) 的 nibbles

address1 = "0x1234567890123456789012345678901234567890"
path1 = keccak256(address1.encode())  # 32 bytes = 64 nibbles

# 以太坊 MPT 只用路徑的最後 64 nibbles
path1_nibbles = nibbles(path1)[-64:]

# 第二步:構建葉子節點
# 葉子節點格式:encoded_path + value
leaf1 = encode_path(path1_nibbles, is_leaf=True) + rlp(value1)

# 第三步:構建分支和擴展節點
# 這就是以太坊客戶端做的事情...

MPT 的設計非常精巧,但代價是複雜性。以太坊的狀態爆炸問題(state bloat)很大程度上來自 MPT 的設計——每個狀態更新都需要更新整條路的 hash,這導致了龐大的計算和存儲開銷。這也是為什麼以太坊正在開發 Verkle Tree 作為未來的替代方案。

密碼學在以太坊的應用全景圖

各類操作的密碼學依賴

以太坊操作 ────────────────────────────────────────────────────── 密碼學原語
                                                     
創建帳戶 (createAccount)    →    橢圓曲線 (secp256k1) 產生私鑰/公鑰對
                                        ↓
                                 keccak256 公鑰 → 取後 20 bytes = 地址

發送交易 (sendTransaction)    →    ECDSA 簽章交易
                                        ↓
                                 keccak256(交易內容) = tx hash
                                        ↓
                                 驗證簽章 → 恢復 sender 地址

部署合約 (deployContract)    →    合約位元組碼 hash
                                        ↓
                                 CREATE2: keccak256(0xff + sender + salt + code_hash)
                                 = 預定址

狀態查詢 (eth_getBalance)    →    MPT 結構驗證
                                        ↓
                                 merkle_proof: 路徑上的 hash 串聯
                                        ↓
                                 驗證 = hash(leaf) 沿途正確計算

智能合約執行 (eth_call)      →    狀態變數讀寫
                                        ↓
                                 SLOAD: 從 MPT 讀取
                                        ↓
                                 SSTORE: 更新 MPT

質押驗證 (Beacon Chain)      →    BLS 簽章聚合
                                        ↓
                                 質押者註冊: keccak256 + 橢圓曲線
                                        ↓
                                 區塊簽章: BLS 聚合多個簽章

密碼學的演化:以太坊的未來方向

當前面臨的挑戰:

1. 量子計算威脅
   - 量子電腦可以破解 ECDSA(用 Shor's algorithm)
   - 這是為什麼以太坊在研究後量子密碼學
   
2. 狀態爆炸
   - MPT 的存儲開銷太大
   - Verkle Tree 是潛在解決方案
   
3. 隱私問題
   - 所有交易公開可見
   - ZK 證明提供解決方向

未來的密碼學方向:

├── 後量子 ECDSA(基於 Hash 的簽章?)
├── Verkle Tree(替代 MPT)
├── ZK-SNARK/STARK(隱私和擴容)
├── 聚合簽章(BLS,減少驗證成本)
└── 多方計算(MPC,分散信任)

結語:密碼學是區塊鏈的根基

寫到這裡,我帶你看了以太坊密碼學的四大支柱:

  1. Keccak 雜湊函數:區塊鏈的指紋,無處不在
  2. 橢圓曲線 secp256k1:私鑰到公鑰的單向函數
  3. ECDSA:不透露私鑰的簽章機制
  4. Merkle Patricia Tree:高效、可驗證的狀態結構

這些密碼學原語不是獨立存在的——它們像齒輪一樣相互咬合,共同支撐起整個以太坊系統的運作。理解了這些基礎,你就有了看懂以太坊更深層設計的本錢。

下次當你看到「EIP-2930 改變了交易簽章格式」或「EIP-4844 引入 Blob」這類新聞時,希望你能會心一笑——因為你知道這些改動在密碼學層面意味著什麼。

密碼學的世界很深,但起點就在這裡。繼續探索吧!


延伸閱讀


免責聲明:本網站內容僅供教育與資訊目的。密碼學是高度專業的領域,實際系統的設計和實作需要專業審計。在部署任何基於密碼學的系統前,請諮詢合格的密碼學專家。

數據截止日期:2026-03-27

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。

目前尚無評論,成為第一個發表評論的人吧!