以太坊密碼學互動式視覺化指南:用蠟筆和故事解開區塊鏈的神秘面紗
密碼學讓人又愛又恨——愛是因為它保護了我們的資產,恨是因為那些數學公式看起來像是從火星語翻譯過來的。這篇文章用非正式、口語化的風格,帶你一步一步走過每個概念,從橢圓曲線到雜湊函數,從 Merkle Tree 到 ECDSA 簽章。用 Python 代碼實際演示每一個運算過程,讓密碼學不再是黑盒子。我們涵蓋 secp256k1 曲線的點加法與標量乘法、Keccak-256 的雪崩效應、Merkle Proof 的逐步構建、以及簽章驗證的 ecrecover 原理。適合想要深入理解以太坊底層密碼學基礎、但被傳統教學方式搞暈的工程師和愛好者。
以太坊密碼學互動式視覺化指南:用蠟筆和故事解開區塊鏈的神秘面紗
密碼學讓人又愛又恨對吧?愛是因為它保護了我們的資產,恨是因為那些數學公式看起來像是從火星語翻譯過來的。我當年第一次看到橢圓曲線方程的時候,盯著看了半小時,最後決定去泡杯咖啡冷靜一下。
這篇文章就是要讓這些東西變得可以「觸摸」。我不會丢給你一堆定義讓你自己悟,而是帶你一步一步走過每個概念,用簡單的比喻和實際的數字,讓密碼學不再是黑盒子。
老實說,我身邊很多工程師(包括我自己)一開始都把密碼學當成「魔法」——只要東西能跑,我們就不問為什麼。但當你需要debug某個簽章驗證失敗的問題,或者想理解為什麼以太坊地址是長這樣的時候,那些「魔法」就變成噩夢了。
讓我們開始這段解密之旅吧。
橢圓曲線:區塊鏈的心臟,你真的該認識一下
那條讓人困惑的方程式
secp256k1用的曲線方程是:
y² = x³ + 7 (mod p)
看起來很簡單對吧?就一個三次多項式。但這裡有個「mod p」,意思是「在質數 p 的世界中運算」。這個 p 是個天文數字:
p = 0xFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F
轉換成人類能理解的形式,大約是 1.15 × 10⁷⁷。這數字有多大?宇宙中所有原子的數量大約是 10⁸⁰。所以這個 p 雖然比宇宙原子數少一些,但也差不了多少。
用Python實際跑一遍橢圓曲線
我強烈建議你自己動手跑一下這段代碼。眼睜睜看著數字變化,絕對比讀一百遍定義更能理解。
# 橢圓曲線互動式視覺化
# 試著修改這些參數看看會發生什麼事
p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
def is_on_curve(x, y):
"""檢查一個點是否在橢圓曲線上"""
return (y * y - x**3 - 7) % p == 0
# 讓我們找一個在曲線上的點
# 我們知道基點 G 一定在曲線上,所以用 G 來驗證
Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
print(f"Gx 在曲線上? {is_on_curve(Gx, Gy)}")
print(f"基點 G = ({hex(Gx)}, {hex(Gy)})")
# 驗證基點的階數
# G * n = O (無窮遠點),其中 n 是曲線階數
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
print(f"\n曲線階數 n ≈ 2²⁵⁶,約 {n:,}")
print(f"這個數字告訴我們什麼?")
print(f"從 G 出發加法 {n} 次,會回到原點")
執行這段代碼的時候,我特別喜歡把 Gx 和 Gy 随便改一個數字,然後看看 is_on_curve 會不會回傳 False。試幾次你就會對「在曲線上」這個概念有感覺了。
點加法:兩點成一線的神秘操作
橢圓曲線最神奇的地方來了——「點加法」。
在普通代數中,加法就是把兩個數加在一起。但在橢圓曲線上,「加法」有完全不同的定義,而且這個定義是刻意設計的。
給你兩點 P 和 Q,連接它們畫一條直線。這條直線會和曲線相交於第三個點。把這個第三個點沿著 x 軸對稱過去,就得到 P + Q。
這個定義瘋狂嗎?超瘋的。但它為什麼重要?因為按照這個定義,我們定義的「加法」滿足:
- 交換律:P + Q = Q + P
- 結合律:(P + Q) + R = P + (Q + R)
- 存在零元素:P + O = P
- 每個元素都有反元素:P + (-P) = O
換句話說,橢圓曲線上的點形成了一個「群」(Group)。這就是密碼學家興奮的地方——他們可以把熟悉的代數工具用在幾何對象上。
標量乘法:重複加法的威力
現在有了點加法,我們自然可以定義「標量乘法」:
k * P = P + P + P + ... + P (共 k 次)
這個操作看起來很簡單,但它是整個以太坊的基礎。你錢包的私鑰就是一個標量,而公鑰就是 私鑰 * G。
問題來了:如果我知道 P 和 k*P,能不能反推出 k?
答案是:在大多數情況下,不能。至少在傳統電腦上,你需要大約 2¹²⁸ 次操作才能暴力破解。這個數字有多大?
2¹²⁸ ≈ 3.4 × 10³⁸
人類大腦很難直觀感受這個數量級。讓我做個比喻:假設你有 10 億台電腦,每台每秒能計算 10 億次乘法。你讓這些電腦連續工作 100 億年,運算的次數仍然遠遠不夠。這就是橢圓曲線離散對數問題的難度。
用代碼實作標量乘法
def point_addition(P, Q, p):
"""
橢圓曲線點加法
P, Q: (x, y) 座標的 tuple
p: 質數域
"""
if P is None:
return Q
if Q is None:
return P
x1, y1 = P
x2, y2 = Q
# 如果 P = -Q,結果是無窮遠點
if x1 == x2 and (y1 + y2) % p == 0:
return None
if P == Q:
# 倍點公式
# λ = (3x₁² + a) / (2y₁)
# x₃ = λ² - 2x₁
# y₃ = λ(x₁ - x₃) - y₁
lam = (3 * x1 * x1) * pow(2 * y1, p - 2, p) % p
else:
# 普通加法
# λ = (y₂ - y₁) / (x₂ - x₁)
# x₃ = λ² - x₁ - x₂
# y₃ = λ(x₁ - x₃) - y₁
lam = (y2 - y1) * pow(x2 - x1, p - 2, p) % p
x3 = (lam * lam - x1 - x2) % p
y3 = (lam * (x1 - x3) - y1) % p
return (x3, y3)
def scalar_multiply(k, P, p):
"""
標量乘法:計算 k * P
使用二元展開法(binary expansion)加速
"""
result = None
base = P
while k:
if k & 1:
result = point_addition(result, base, p)
base = point_addition(base, base, p)
k >>= 1
return result
# 測試
G = (Gx, Gy)
# 計算 2 * G
double_G = scalar_multiply(2, G, p)
print(f"2G = {double_G}")
# 計算 10 * G
ten_G = scalar_multiply(10, G, p)
print(f"10G = {ten_G}")
# 驗證:10G = 2G + 2G + 2G + 2G + 2G
check = scalar_multiply(2, G, p)
check = point_addition(check, scalar_multiply(2, G, p), p)
check = point_addition(check, scalar_multiply(2, G, p), p)
check = point_addition(check, scalar_multiply(2, G, p), p)
check = point_addition(check, scalar_multiply(2, G, p), p)
print(f"2G+2G+2G+2G+2G = {check}")
print(f"兩者相等? {ten_G == check}")
跑完這段代碼,你就會明白「標量乘法」到底是怎麼運作的。我特別喜歡用這個比喻:想像你有 2⁵¹² 個 G,你不需要真的加 2⁵¹² 次——只要用二元展開,512 步就能算完。這就是效率的力量。
以太坊地址是怎麼來的
現在讓我把密碼學和實際應用串起來。
私鑰 → 公鑰 → 地址,這條鍊子是怎麼走的?
步驟 1:私鑰(256位隨機數)
↓
步驟 2:公鑰 = 私鑰 × G(橢圓曲線標量乘法)
↓
步驟 3:Keccak-256(公鑰)
↓
步驟 4:取最後 20 位元組
↓
步驟 5:加上 "0x" 前綴 = 以太坊地址
為什麼地址只有 20 位元組?因為 Keccak-256 產生的 32 位元組雜湊值,取後面 20 位元組剛好就是一個「足夠隨機但人類還能閱讀」的長度。如果用完整 32 位元組,地址就會長得像 0x8b1a980b163c0c26a6c0e8b4e2c9f1a3d5e7b9c2f6a1e8d0b4c5f6a7e8d9c1b2,看都看暈了。
讓我用代碼走一遍:
import hashlib
def private_key_to_address(private_key_hex):
"""
完整演示:私鑰 → 以太坊地址
"""
# 把十六進制字串轉成位元組
private_key_bytes = bytes.fromhex(private_key_hex.replace('0x', ''))
# 這裡我省略了真正的橢圓曲線運算(太慢了)
# 實際上:公鑰 = 私鑰 × G
# 我們用一個已知結果來演示後面的步驟
# 假設這是一個有效的公鑰(未壓縮格式,0x04 前綴)
public_key_hex = "04" + private_key_hex[2:] * 2 # 假的,純示範
public_key_bytes = bytes.fromhex(public_key_hex[:130]) # 取前 130 個十六進位
# Keccak-256 雜湊
keccak = hashlib.new('keccak256')
keccak.update(public_key_bytes)
hash_bytes = keccak.digest()
# 取最後 20 位元組作為地址
address_bytes = hash_bytes[-20:]
address = '0x' + address_bytes.hex()
return address
# 測試
test_address = private_key_to_address("0x" + "ab" * 32)
print(f"測試地址:{test_address}")
print(f"長度:{len(test_address)} 字元(0x + 40 個十六進位)")
老實說,看到地址是怎麼來的,我第一次有種「哦!原來如此!」的感覺。之前只知道「這是一串隨機數字」,現在起碼理解它在數學上的意義了。
雜湊函數:數位指紋的藝術
雜湊函數該具備的三個本領
密碼學雜湊函數不是普通的雜湊演算法。它們必須是專門設計用來對抗各種攻擊的。這裡有幾個核心要求:
原像阻力(Preimage Resistance)
已知一個雜湊值 h,你幾乎不可能找到任何 m 使得 hash(m) = h。
這保證了什麼?你的以太坊地址並不會洩漏你的公鑰——雖然地址是從公鑰算出來的,但逆向幾乎不可能。當然別忘了,如果你真的不小心公開了公鑰,那就要小心了。
碰撞阻力(Collision Resistance)
幾乎不可能找到兩個不同的輸入 m₁ 和 m₂,使得 hash(m₁) = hash(m₂)。
比特幣和以太坊都靠這個保護區塊鏈的完整性。任何區塊的內容改了,區塊頭的 Merkle Root 就會變,別人一眼就能發現你作弊了。
雪崩效應(Avalanche Effect)
輸入的一點點變化會導致輸出的大幅變化。精確地說,改變輸入的任意一個位元,輸出大約會有一半的位元發生變化。
這個特性讓雜湊函數的輸出看起來像「隨機」的。即使你知道輸入模式,也很難預測輸出。
Keccak-256:太初的混沌製造機
以太坊用的是 Keccak-256,不是 NIST 的 SHA-3。兩者經常被混淆,但 NIST 在標準化過程中改了一些東西,導致輸出不同。這個差異在歷史上造成過一些 bug,有人還指控 NIST 故意這麼做。究竟真相如何,我也不知道,但保持警覺總沒錯的。
Keccak 的核心是一個海綿結構(Sponge Construction):
┌─────────────────────────────────────────────┐
│ 海綿結構 │
├─────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 吸收 │ → │ 擠壓 │ → │ 吸收 │ │
│ │ Phase │ │ Phase │ │ Phase │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ Keccak-f 函數 │ │
│ │ (重複進行的置換函數) │ │
│ └─────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────┘
海綿結構有兩個階段:
- 吸收階段(Absorbing):輸入被切成固定大小的塊,逐塊和內部狀態進行 XOR 運算,然後通過 Keccak-f 函數
- 擠壓階段(Squeezing):輸出被「擠」出來,每擠一次都先經過 Keccak-f 函數
這個設計優雅的地方在於:無論輸入多長,輸出長度都可以根據需求調整。以太坊固定用 256 位元輸出,所以是 Keccak-256。
動手驗證 Keccak-256
import hashlib
def keccak256(data):
"""Keccak-256 雜湊(Python 實現)"""
keccak = hashlib.new('keccak256')
keccak.update(data)
return keccak.digest()
# 測試雪崩效應
msg1 = b"Hello, Ethereum!"
msg2 = b"Hello, Etherem!" # 只有一個字母不同:um → em
hash1 = keccak256(msg1).hex()
hash2 = keccak256(msg2).hex()
print("輸入 1:", msg1)
print("雜湊:", hash1)
print()
print("輸入 2:", msg2)
print("雜湊:", hash2)
print()
# 計算有多少位元不同
def hamming_distance(h1, h2):
"""計算兩個十六進制字串的漢明距離"""
b1 = bytes.fromhex(h1)
b2 = bytes.fromhex(h2)
return sum(bin(x ^ y).count('1') for x, y in zip(b1, b2))
distance = hamming_distance(hash1, hash2)
print(f"位元差異數: {distance} / 256")
print(f"差異比例: {distance / 256 * 100:.1f}%")
跑完這個測試,你會看到即使只改一個字母,輸出也會天差地別。這就是雪崩效應。理想情況下,大約 50% 的位元會改變,實際數字應該接近這個值。
以太坊如何用雜湊
雜湊函數在以太坊中無處不在:
交易雜湊(Transaction Hash)
txHash = keccak256(rlp_encode(transaction))
每一筆交易都有唯一的 ID。只要 ID 變了,就代表交易內容被改過。
區塊頭雜湊
blockHash = keccak256( header.parentHash || header.ommersHash || header.beneficiary ||
header.stateRoot || header.transactionsRoot || header.receiptsRoot ||
header.logsBloom || header.difficulty || header.number ||
header.gasLimit || header.gasUsed || header.timestamp ||
header.extra || header.mixHash || header.nonce )
區塊頭的雜湊是工作量證明的核心。礦工瘋狂改 nonce,就是为了让这个哈希值的前导零数量达标。
狀態根(State Root)
stateRoot = PatriciaMerkleTrie(state)
整個以太坊的狀態(帳戶餘額、合約代碼、存儲)都被压缩进一棵樹裡,樹根的雜湊值就是狀態根。
交易根(Transactions Root)
transactionsRoot = MerkleTrie(transactions)
區塊中所有交易的 Merkle Tree 根節點。用這個可以在不下載完整交易列表的情況下驗證某筆交易是否存在。
Merkle Tree:區塊鏈的戶口名簿
終於到 Merkle Tree 了!這個結構在區塊鏈中扮演核心角色,卻很少有人能說清楚它的來龍去脈。讓我一次把它講透。
從婚禮說起
Merkle Tree 是由 Ralph Merkle 在 1979 年發明的。為什麼要發明這個東西?因為他想解決一個問題:如何驗證一大筆資料中的某一筆資料是正確的,而不需要下載整筆資料。
這個問題在現實中到處都是。想像你是海關官員,要查驗一貨櫃的結婚證書。傳統做法是打開每個箱子、翻閱每本證書、核對每個簽名。但如果我告訴你:「我已經算好了,所有證書的指紋都在這張紙上,而且我還能證明每本證書都正確地影響了這個指紋」,你只需要驗證指紋和幾個關鍵樣本,就能確認整個貨櫃的可信度。
Merkle Tree 就是這個「指紋」系統的數學實現。
Merkle Tree 的工作原理
假設你有 4 筆資料:A、B、C、D。
┌──────────────────┐
│ Merkle │
│ Root │
│ (ABCD 的指紋) │
└────────┬─────────┘
│
┌───────────────┴───────────────┐
│ │
┌──────┴──────┐ ┌──────┴──────┐
│ Hash │ │ Hash │
│ (AB 的指紋) │ │ (CD 的指紋) │
└──────┬──────┘ └──────┬──────┘
│ │
┌─────┴─────┐ ┌─────┴─────┐
│ │ │ │
┌─────┴─────┐ ┌───┴────┐ ┌─────┴─────┐ ┌───┴────┐
│ Hash(A) │ │ Hash(B) │ │ Hash(C) │ │ Hash(D) │
│ A 的指紋 │ │ B 的指紋 │ │ C 的指紋 │ │ D 的指紋 │
└───────────┘ └─────────┘ └───────────┘ └─────────┘
要驗證 C 是正確的,你只需要:
- 知道 Merkle Root(ABCD 的指紋)
- 拿到 C 的值
- 拿到
Hash(D)和Hash(AB) - 自己計算
Hash(C)、Hash(CD)、Merkle Root - 確認計算出的 Merkle Root 與已知的一致
整個驗證過程只需要 3 個雜湊運算,而不是 4 個。數量越大,節省越明顯。
用 Python 建一棵 Merkle Tree
import hashlib
def keccak256(data):
"""Keccak-256 雜湊"""
if isinstance(data, str):
data = data.encode('utf-8')
return hashlib.new('keccak256', data).digest()
def hash_pair(left, right):
"""對兩個雜湊值進行配對雜湊"""
return keccak256(left + right)
class MerkleTree:
"""Merkle Tree 實現"""
def __init__(self, leaves):
"""
初始化:接受任意數量的葉節點
"""
self.leaves = [keccak256(leaf) for leaf in leaves]
self.root = self._build_tree()
def _build_tree(self):
"""建立 Merkle Tree"""
current_level = self.leaves[:]
# 如果葉節點數量是奇數,複製最後一個(常見的以太坊處理方式)
if len(current_level) % 2 == 1:
current_level.append(current_level[-1])
while len(current_level) > 1:
next_level = []
for i in range(0, len(current_level), 2):
left = current_level[i]
right = current_level[i + 1]
next_level.append(hash_pair(left, right))
current_level = next_level
# 再次處理奇數節點
if len(current_level) % 2 == 1:
current_level.append(current_level[-1])
return current_level[0]
def get_proof(self, index):
"""
獲取指定葉節點的 Merkle Proof
返回需要驗證的「兄弟節點」列表
"""
if index < 0 or index >= len(self.leaves):
raise ValueError("Invalid index")
proof = []
current_index = index
current_level = self.leaves[:]
# 處理初始奇數情況
if len(current_level) % 2 == 1:
current_level.append(current_level[-1])
while len(current_level) > 1:
# 確定兄弟節點的索引
if current_index % 2 == 0:
sibling_index = current_index + 1
else:
sibling_index = current_index - 1
# 記錄兄弟節點
proof.append({
'side': 'left' if current_index % 2 == 1 else 'right',
'hash': current_level[sibling_index]
})
# 計算上一層
next_level = []
for i in range(0, len(current_level), 2):
next_level.append(hash_pair(current_level[i], current_level[i + 1]))
current_level = next_level
current_index = current_index // 2
# 處理奇數節點
if len(current_level) % 2 == 1 and len(current_level) > 1:
current_level.append(current_level[-1])
return proof
def verify_proof(self, leaf_hash, proof, root):
"""驗證 Merkle Proof"""
current_hash = leaf_hash
for item in proof:
if item['side'] == 'left':
# 兄弟節點在左邊
current_hash = hash_pair(item['hash'], current_hash)
else:
# 兄弟節點在右邊
current_hash = hash_pair(current_hash, item['hash'])
return current_hash == root
# 實際操作!
transactions = ["轉帳給 Alice 1 ETH", "轉帳給 Bob 2 ETH",
"Mint NFT #1234", "質押 10 ETH"]
tree = MerkleTree(transactions)
print("Merkle Root:", tree.root.hex())
print()
# 為第三筆交易("Mint NFT #1234",index=2)生成 proof
proof = tree.get_proof(2)
print("為第三筆交易生成的 Proof:")
for i, item in enumerate(proof):
print(f" 步驟 {i+1}: {item['side']} 節點 = {item['hash'].hex()[:20]}...")
print()
# 驗證
leaf_hash = keccak256("Mint NFT #1234")
is_valid = tree.verify_proof(leaf_hash, proof, tree.root)
print(f"驗證結果: {'✓ 通過' if is_valid else '✗ 失敗'}")
這個代碼有點長,但我建議你實際跑一次。跑完之後,你會對 Merkle Proof 的形成過程有非常清晰的理解。
為什麼區塊鏈離不開 Merkle Tree
現在你明白 Merkle Tree 的原理了,讓我說說它到底解決了什麼問題。
輕客戶端驗證
想像你是個輕客戶端(Light Client),不想下載整個區塊鏈。你可以只下載區塊頭(Header),區塊頭包含 Merkle Root。當你想驗證某筆交易是否存在,只需要向全節點索取 Merkle Proof。這樣你就能在只下載幾百位元組的情況下,驗證任意交易的內容。
數據可用性證明
如果有人說「我的區塊包含這些交易,但我不給你看詳細內容」,你可以要求他提供 Merkle Proof。只要他能給出正確的 Proof,你就知道數據確實存在,只是對方不願意公開。這在 Rollup 場景中特別重要——Sequencer 聲稱某些交易已經執行並產生了新狀態,但他需要證明數據可用,否則 Validator 無法挑戰他。
狀態快照和同步
以太坊的全節點同步一開始會下載一堆歷史區塊。透過 Merkle Tree,可以快速驗證下載的狀態片段是否正確,而不用重新執行所有交易。
簽章與驗證:以你的名字發送交易
ECDSA:你其實每天都在用
ECDSA(Elliptic Curve Digital Signature Algorithm)是以太坊交易的基礎。每一筆從你錢包發出的交易,都需要用你的私鑰簽名,礦工則用公鑰驗證。
很多人把 ECDSA 當成「魔法」——反正 metamask 幫我做這件事,我不需要知道原理。但當你遇到「簽章不合」之類的錯誤時,理解底層原理就很有用了。
簽章的數學原理
ECDSA 簽章由兩個數字組成:(r, s)。
簽名的過程大概長這樣:
1. 選擇一個隨機數 k(這個 k 超級重要!)
2. 計算 R = k × G,取 R 的 x 座標
3. r = R.x mod n
4. s = k⁻¹ × (message_hash + r × private_key) mod n
5. 簽章 = (r, s)
等等,這裡有個大問題:k 必須是隨機的,而且每次簽名都要不同!
如果同一個 k 被用了兩次,攻擊者可以從兩個簽章中反推出私鑰。2010 年,Sony PS3 的 ECDSA 實現就是這個問題——他們重用了 k,結果駭客直接拿到了 PS3 的私鑰,可以自己簽名安裝任何遊戲。
以太坊錢包在生成簽章時,會確保 k 的隨機性。但如果你自己實現簽章,千萬別在這裡省事。
用代碼驗證簽章
def ecdsa_sign(message_hash, private_key, k):
"""
簡化的 ECDSA 簽名過程
實際使用時,請用專業的密碼學庫!
"""
# 計算 R = k * G
R = scalar_multiply(k, G, p)
if R is None:
return None
r = R[0] % n
# 計算 s = k⁻¹ × (message_hash + r × private_key) mod n
k_inv = pow(k, n - 2, n) # 費馬小定理
s = (k_inv * (message_hash + r * private_key)) % n
return (r, s)
def ecdsa_verify(message_hash, signature, public_key):
"""
驗證 ECDSA 簽章
"""
r, s = signature
if r <= 0 or r >= n or s <= 0 or s >= n:
return False
# 計算 w = s⁻¹ mod n
w = pow(s, n - 2, n)
# 計算 u1 = message_hash × w mod n
# 計算 u2 = r × w mod n
u1 = (message_hash * w) % n
u2 = (r * w) % n
# 計算 P = u1 × G + u2 × public_key
P1 = scalar_multiply(u1, G, p)
P2 = scalar_multiply(u2, public_key, p)
P = point_addition(P1, P2, p)
if P is None:
return False
# 驗證 r ≡ P.x mod n
return (P[0] % n) == r
# 測試
private_key = 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
public_key = scalar_multiply(private_key, G, p)
message = b"Transfer 1 ETH to 0x1234..."
message_hash = int.from_bytes(keccak256(message), 'big') % n
# 用不同的 k 簽名兩次
k1 = 0x1111111111111111111111111111111111111111111111111111111111111111
k2 = 0x2222222222222222222222222222222222222222222222222222222222222222
sig1 = ecdsa_sign(message_hash, private_key, k1)
sig2 = ecdsa_sign(message_hash, private_key, k2)
print("簽章 1:", sig1)
print("簽章 2:", sig2)
print("兩者不同?", sig1 != sig2)
print()
print("驗證簽章 1:", ecdsa_verify(message_hash, sig1, public_key))
print("驗證簽章 2:", ecdsa_verify(message_hash, sig2, public_key))
再次強調:實際應用中千萬別自己實現密碼學演算法!這段代碼只是用來教學的。現實中使用 OpenSSL、libsodium 或 Web3.py 這種經過安全審計的庫。
以太坊的 ecrecover:聰明的內建函數
以太坊聰明的地方在於:它內建了一個叫 ecrecover 的函數,可以從簽章直接恢復簽章者的地址。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SignatureVerifier {
/**
* 驗證消息是否由特定地址簽署
* 這個函數展示了 ecrecover 的基本用法
*/
function verify(
bytes32 message,
bytes memory signature,
address signer
) public pure returns (bool) {
// 確保簽章長度是 65 位元組 (r:32 + s:32 + v:1)
require(signature.length == 65, "Invalid signature length");
// 解析簽章
bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := calldataload(signature.offset)
s := calldataload(add(signature.offset, 0x20))
v := byte(0, calldataload(add(signature.offset, 0x40)))
}
// 驗證 v 是 27 或 28
// (某些實現用 0 和 1,這裡做個相容性處理)
if (v < 27) {
v += 27;
}
require(v == 27 || v == 28, "Invalid signature v value");
// 使用 ecrecover 恢復地址
address recovered = ecrecover(message, v, r, s);
// 比較
return recovered == signer;
}
/**
* 驗證 EIP-191 格式的消息
* 這是以太坊錢包簽署消息的標準格式
*/
function verifyEIP191(
bytes memory data,
bytes memory signature,
address signer
) public pure returns (bool) {
// EIP-191: 0x19 + 版本(1) + 驗證者地址 + 數據雜湊
bytes32 messageHash = keccak256(abi.encodePacked(
bytes1(0x19),
bytes1(0x01),
signer,
keccak256(data)
));
return verify(messageHash, signature, signer);
}
}
ecrecover 的神奇之處在於它是可見的——任何人都可以用簽章和消息反推出地址,但無法反推私鑰。這就是非對稱密碼學的力量。
密碼學的局限性:不是銀彈
我花了很大篇幅解釋密碼學的原理,但最後我想泼点冷水。
密碼學很強大,但它不是萬能的。很多安全問題的根本不在密碼學,而在於「人是脆弱的環節」。
私鑰洩漏:再強的加密演算法也保護不了一個被盜的私鑰。社會工程、釣魚網站、惡意軟體每天都讓無數人的加密資產蒸發。
**隨機數生成器漏洞」:密碼學協議的安全性經常依賴於隨機數的質量。2012008 年,Android 系統的隨機數生成器有 bug,導致某些比特幣錢包的私鑰可被預測。沒錯,問題不在比特幣的密碼學,在於可憐的隨機數。
合約漏洞:即使簽章是安全的,合約代碼本身可能有漏洞。The DAO 事件就是這樣——密碼學沒問題,但 Solidity 代碼有 bug,攻擊者反覆呼叫 withdraw() 函數,把錢全部抽走。
**密碼學的進化」:現在看似安全的演算法,未來可能會被量子電腦破解。Shor 演算法在理論上可以在多項式時間內解決離散對數問題。當然,實用級量子電腦還需要幾十年,但以太坊社區已經開始研究後量子密碼學遷移了。
所以,密碼學是必要的,但不足以保證安全。它只是整個安全架構中的一環。
結語:密碼學不是魔法
這篇文章寫得很長,但密碼學的內容遠不止這些。我想讓你帶走的主要觀點是:
橢圓曲線離散對數問題是安全的,不是因為數學家證明了它很難,而是因為經過幾十年的研究,還沒有人找到簡單的破解方法。這是一種「經驗性」的安全,而非「證明」出來的安全。
雜湊函數的雪崩效應和碰撞阻力,讓它成為區塊鏈不可或缺的工具。用一個固定長度的指紋表示任意長度的數據,並能檢測任何篡改,這在密碼學出現之前是不可想像的。
Merkle Tree 把「局部驗證整體」的夢想變成現實。這種資料結構的優雅之處在於:你只需要 O(log n) 的資料,就能驗證 O(n) 的資料完整性。
ECDSA 和 ecrecover 的組合,實現了「用私鑰證明身份,卻不洩漏私鑰」的奇蹟。區塊鏈的帳戶模型就是建立在這個基礎上。
密碼學很美,但它不是銀彈。真正的安全需要密碼學、程式碼品質、運營安全、使用者教育等多方面的配合。缺任何一環,代價可能就是你所有的資產。
希望這篇文章能讓你對以太坊的密碼學基礎有更深入的理解。下次當你簽署一筆交易,或者看到一個以太坊地址時,你能夠說:「我知道背後發生了什麼。」
那就夠了。
本網站內容僅供教育與資訊目的。密碼學和智能合約涉及高度複雜的技術領域,任何實際應用都應經過專業審計並自行承擔風險。
相關文章
- 以太坊密碼學基礎完整實作指南:從理論到智能合約部署的工程實踐 — 本文提供以太坊密碼學基礎的完整實作指南,涵蓋 secp256k1 橢圓曲線密碼學、ECDSA 簽章機制、Keccak-256 雜湊函數、RNG 安全、鏈上前置編譯合約等核心主題的技術實作細節。我們提供可直接部署的 Solidity/Vyper 程式碼範例、零知識證明電路開發工具鏈(Circom/Noir/Halo2)教學,以及後量子密碼學遷移方案(CRYSTALS-Dilithium)的完整說明,幫助開發者深入理解以太坊密碼學基礎並實際應用於智能合約開發。
- 以太坊密碼學互動實驗室:瀏覽器可執行範例與 secp256k1/ECDSA 深度教學 — 本文提供一套完整的以太坊密碼學互動式學習模組,讓讀者能夠在瀏覽器中直接執行和實驗密碼學運算。涵蓋 secp256k1 橢圓曲線運算、ECDSA 簽章與驗證、Keccak-256 雜湊、以及零知識證明等核心主題。所有範例都可以在現代瀏覽器中直接運行,無需額外的開發環境配置。提供完整的 JavaScript 程式碼範例,包括點加法、標量乘法、交易簽章模擬、Merkle 樹構建等互動實驗。
- 以太坊密碼學基礎完整指南:橢圓曲線密碼學、簽章機制與 Merkle Tree 結構 — 本文深入分析以太坊密碼學系統的三大支柱:secp256k1 橢圓曲線與 ECDSA 簽章機制的數學原理、KECCAK-256 雜湊函數的設計特點、以及 Patricia Merkle Trie 資料結構在狀態管理中的關鍵角色。我們從密碼學理論出發,經過詳盡的數學推導,最終落實到 Solidity、Go 與 Rust 的實際程式碼範例。涵蓋離散對數問題、點加法/倍增運算、ECDSA 簽章驗證、Merkle Proof、EIP-1559 等核心概念的完整技術解析。
- 以太坊密碼學原語直覺解釋:橢圓曲線、布隆過濾器與 Keccak 的工程視角 — 本文從工程師的視角出發,提供橢圓曲線、布隆過濾器(Blooom Filter)等密碼學原語的直覺性解釋、完整的數學推導、以及可直接使用的程式碼範例,幫助讀者建立對這些密碼學原語的深入理解。涵蓋 ECDSA 簽名、Keccak-256 哈希、布隆過濾器的設計原理與實際應用。
- 橢圓曲線密碼學直覺式圖解說明:從基礎原理到以太坊應用 — 本文以直覺式圖解說明讓讀者無需深厚數學背景也能理解橢圓曲線密碼學的核心概念。涵蓋橢圓曲線加法的幾何意義、離散對數問題的安全性基礎、以太坊地址生成的完整流程、ECDSA 簽名演算法、Vitalik 恢復機制,以及零知識電路和 Layer 2 中的橢圓曲線應用。
延伸閱讀與來源
- Ethereum.org Developers 官方開發者入口與技術文件
- EIPs 以太坊改進提案完整列表
- Solidity 文檔 智慧合約程式語言官方規格
- EVM 代碼庫 EVM 實作的核心參考
- Alethio EVM 分析 EVM 行為的正規驗證
這篇文章對您有幫助嗎?
請告訴我們如何改進:
評論
發表評論
注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。
目前尚無評論,成為第一個發表評論的人吧!