位址是指紋,不是金鑰
當你請朋友付款給你,你會傳給他一串短短的字串,例如 `1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa` 或 `0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed`。這串字就是你的位址。它不是你的公鑰,更不是你的私鑰——它是越過這兩者之後、又一道單向的門。學到這一階,你已經把整條鏈建好了:一個祕密數字(私鑰)在曲線上生成一把公鑰,你能用祕密來簽章、用公鑰來驗證。位址是最後一環:把公鑰丟進一個密碼學雜湊。輸出就是一枚指紋——短、公開、且無法逆推。
所以完整的流程是三道單向步驟疊在一起:私鑰 → 公鑰 → 位址。在曲線上對基點做純量乘法,把私鑰藏在公鑰背後;雜湊則把公鑰藏在位址背後。你可以把位址印在名片上,沒有人能沿著這道階梯往回爬到你的錢。
比特幣:HASH160 與 Base58Check
我們來推導一個經典的比特幣位址(傳統的付款給公鑰雜湊,即 P2PKH,就是以 `1` 開頭的那種)。比特幣不是只雜湊一次;它用兩個不同的函式雜湊兩次,再用一個版本位元組與校驗碼把結果包起來,最後把整包用 Base58 編碼。
- 從公鑰出發:`04 || X || Y`(未壓縮,65 位元組)或 `02/03 || X`(壓縮,33 位元組)。同一把金鑰、這兩種形式會產出不同的位址——這是實務上常見的坑。
- 計算 HASH160 = `RIPEMD160(SHA256(pubkey))`。SHA-256 產出 32 位元組;RIPEMD-160 再把它壓成 20 位元組的公鑰雜湊。
- 在前面接上版本位元組 `0x00`(主網 P2PKH)。正是它讓最終位址以 `1` 開頭。
- 計算校驗碼:`SHA256(SHA256(version || hash160))`,只取前 4 位元組。
- 把 `version || hash160 || checksum` 串接起來(共 25 位元組),再做 Base58 編碼。完成。
# Bitcoin legacy (P2PKH) address, from a public key
pubkey = 04 || X || Y # 65-byte uncompressed point on secp256k1
# (or 33-byte compressed: 02/03 || X)
h160 = RIPEMD160(SHA256(pubkey)) # 20-byte "HASH160"
payload = 0x00 || h160 # 0x00 = mainnet P2PKH version byte (-> '1')
checksum = SHA256(SHA256(payload))[0:4] # first 4 bytes = 32-bit checksum
address = Base58( payload || checksum ) # 25 bytes -> a string starting with '1'
# Real example: the very first coinbase output (Bitcoin genesis block) pays to
# 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa以太坊:Keccak-256 與最後 20 位元組
以太坊走的是更乾脆的路。沒有版本位元組、沒有 Base58、不雙重雜湊。你用 Keccak-256 對公鑰雜湊一次,然後只保留最後 20 位元組。接著把這 20 位元組寫成帶 `0x` 前綴的十六進位——共 40 個十六進位字元,這就是位址。
# Ethereum address, from a public key pubkey = X || Y # 64 bytes: the raw curve point, NO 0x04 prefix digest = Keccak256(pubkey) # 32-byte hash address = "0x" + hex( digest[12:32] ) # keep the LAST 20 bytes -> 40 hex chars # e.g. 0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed (all-lowercase, no checksum yet)
為什麼截成 20 位元組是安全的?因為雜湊的碰撞抗性與原像抗性在截斷後的輸出上依然成立——要找到兩把公鑰碰撞到同一個 160 位元位址,依生日界約需 2^80 的工作量,遠超今日所能企及。被丟掉的那 12 位元組,只是拿一絲理論上的安全性,換來更短、更友善的位址,正如比特幣的 RIPEMD-160 所做的一樣。
校驗碼:在打錯字害你破財之前攔下它
校驗碼存在,是為了防範一個殘酷的事實:區塊鏈付款是不可逆的。沒有銀行可以打電話求救。如果你打錯一個字元、而結果剛好長得像個合法位址,你的幣就沒了。校驗碼就是烤進位址裡的幾個額外位元,讓多數打錯字的情況都會產生一個通不過檢查的字串,於是你的錢包拒絕送出。
比特幣的校驗碼又強又外顯。 來自雙重 SHA-256 的那 4 位元組是一個完整的 32 位元校驗碼:一次隨機毀損仍能通過的機率約為 1/2^32 ≈ 43 億分之一。只要改動任何字元,Base58Check 幾乎篤定會拒絕它。
以太坊的校驗碼更隱晦——它藏在字母的大小寫裡。 原始位址是不分大小寫的十六進位,於是 EIP-55 重複利用 `a–f` 這些字母的大小寫來夾帶校驗碼,同時讓位址維持一樣的 40 個字元。你對小寫位址做雜湊,逐一檢視每個十六進位字母,當雜湊對應的位元為 1 時就把它大寫:
# EIP-55: a checksum smuggled into the upper/lower casing of an Ethereum address
def to_checksum(addr40): # addr40 = lowercase hex, no "0x"
h = Keccak256(ascii(addr40)) # hash the lowercase hex STRING (as text)
out = ""
for i in range(40):
c = addr40[i]
if c in "0123456789":
out += c # digits carry no checksum, never change
elif nibble(h, i) >= 8: # is the i-th hex digit of the hash >= 8?
out += upper(c) # yes -> UPPERCASE this letter
else:
out += lower(c) # no -> lowercase this letter
return "0x" + out
# 0x5aaeb6... -> 0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed為什麼位址不是金鑰——以及這換來了什麼
因為位址是一個雜湊,箭頭只指向一個方向。你無法從位址回推公鑰,更絕無可能抵達私鑰——那等於要同時反轉雜湊以及橢圓曲線乘法。這正是自我保管的基石:你可以把位址交給全世界,卻不洩漏任何能讓別人動用你資金的東西。
比特幣裡還有個微妙的紅利。既然位址是公鑰的雜湊,公鑰本身在你第一次從該位址花費之前都藏著(花費時才揭露金鑰,好讓網路驗證你的簽章)。對於一個你從未花費過的位址,攻擊者只看得到雜湊——這是一層額外的小緩衝,常在「未來的量子攻擊者也許能破解曲線、卻破不了雜湊」的討論中被提到。
再補兩個誠實的提醒。第一,隱私:因為帳本是公開的,重複使用同一個位址會把你的所有活動串在一起——錢包之所以每筆付款都產生一個全新位址,正是為此。第二,錯誤無法回頭:在自我保管的錢包裡,位址背後的金鑰只有你一人掌握。弄丟金鑰,該位址裡的資金就再也找不回來;送到錯誤(但合法)的位址,也沒人能替你追回。校驗碼證明一個位址格式正確,但它不證明你擁有它,也不證明它屬於對的人。
不只公鑰:合約位址,以及一顆種子、千百個位址
並非每個位址都是某把公鑰的指紋。在以太坊上,智慧合約也有位址——而它是部署資料的雜湊,不是任何金鑰的雜湊。單純的 `CREATE` 對部署者加上它的交易計數做雜湊;CREATE2 操作碼則對部署者、一個自選的鹽值與合約自身的初始化程式碼做雜湊,這讓你能在部署之前就算出合約的位址。
# Ethereum contract addresses are ALSO hashes -- of deployment data, not a key CREATE : addr = keccak256( rlp_encode([sender, nonce]) )[12:32] CREATE2 : addr = keccak256( 0xff || sender || salt || keccak256(init_code) )[12:32] # ^ same 'last 20 bytes of a Keccak-256 hash' rule as an account address
反過來看,一個祕密能鑄出成千上萬個位址。現代錢包是階層式確定性的(BIP-32):它從單一助記詞推導出一整棵金鑰對的樹,而 BIP-44 把推導路徑標準化,於是同樣這十二個字在任何錢包軟體裡都能重生出同樣的位址。這就是為什麼你的錢包每次都能給你一個全新的收款位址——對隱私更好——而你卻只需要備份一顆種子。