以太坊密碼學原語互動式瀏覽器教學:從理論到實踐的完整指南

本文以互動式瀏覽器範例為核心,幫助讀者在實踐中理解以太坊的密碼學基礎。涵蓋橢圓曲線密碼學(secp256k1、ECDSA 簽名驗證)、Keccak-256 哈希函數、雪崩效應、Merkle 樹建造與驗證、以及以太坊狀態證明的實際應用。每個概念都配有可運行的 JavaScript 程式碼範例,讀者可以直接在瀏覽器控制台中實驗。特別適合視覺型學習者和希望深入理解密碼學的開發者。

以太坊密碼學原語互動式瀏覽器教學:從理論到實踐的完整指南

密碼學,聽起來是不是很嚇人?別擔心,這篇文章就是要打破這個迷思。我會用最直白的方式,帶你在瀏覽器裡實際操作每一個密碼學原語。看什麼看,就是你現在打開的那個瀏覽器!想像一下,當你在 Etherscan 上查一筆交易的時候,背後其實是這堆數學在幫你驗證「這筆交易確實是某個人簽發的,沒被改過」。密碼學就是區塊鏈的地基,地基不穩,上面蓋什麼都是白搭。所以這篇文章,我們就從地基開始挖。

橢圓曲線密碼學:secp256k1 的魔法

以太坊用的數位簽名演算法叫 ECDSA,全名是橢圓曲線數位簽名演算法。數學系的人聽到「橢圓曲線」大概會開始冒冷汗,但概念本身其實沒有那麼可怕。我們可以把橢圓曲線想成一條有特殊性質的曲線,這條曲線上的點可以做一種特別的「加法」運算——兩個點相加會得到第三個點。

secp256k1 這條曲線的方程式是:

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

等等,這方程式看起來也太簡單了吧?沒錯,但重點在「有限域」這三個字。我們不是在實數平面上畫曲線,而是在一個巨大的質數模域上運算。這讓反向推導變得極度困難——你知道 P = G + G + G + ... + G(k次),但想從 P 倒推回 k 是幾乎不可能的。這就是離散對數問題,也是橢圓曲線密碼學安全性的根基。

在瀏覽器裡玩 secp256k1

我們可以用 Web Crypto API 配合第三方庫來操作。實際上原生的 Web Crypto API 不支援 secp256k1,所以我們需要一個庫。強烈推薦 noble-curves,這個庫是 TypeScript 寫的,類型安全,而且用了很多現代的優化技巧。

// 在瀏覽器 console 或 Node.js 環境中
import { secp256k1 } from 'noble-curves';

// 看看基本參數
console.log('橢圓曲線 secp256k1 參數:');
console.log('p (有限域大小):', secp256k1.P.toString(16));
console.log('n (群階):', secp256k1.n.toString(16));
console.log('G (生成點):', secp256k1.G.x.toString(16), secp256k1.G.y.toString(16));

p 的值是 FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F,一個大概 256 位元的質數。n 是另一個質數,代表這條曲線上有多少個點。

私鑰、公鑰、地址的關係

很多新手搞不清楚這三者的關係,我剛開始也是。後來才慢慢理解這其實是一條流水線:

  1. 私鑰(Private Key):一個隨機的 256 位元整數,本質上就是一個密碼。這個密碼超級重要,誰拿到誰就能控制你的資產。
  2. 公鑰(Public Key):用私鑰透過橢圓曲線點乘法算出來的。是一個座標點 (x, y),通常用 64 位元組表示。
  3. 以太坊地址(Address):把公鑰丟進 Keccak-256 哈希,取最後 20 位元組,就是你的以太坊地址。
import { secp256k1 } from 'noble-curves';
import { keccak256 } from 'noble-hashes';

// 1. 生成私鑰(實際使用時要用安全隨機數)
const privateKey = secp256k1.utils.randomPrivateKey();
console.log('私鑰(十六進位):', Buffer.from(privateKey).toString('hex'));

// 2. 用私鑰算出公鑰
const publicKey = secp256k1.getPublicKey(privateKey);
console.log('公鑰(未壓縮):', Buffer.from(publicKey).toString('hex'));
console.log('公鑰(壓縮):', Buffer.from(secp256k1.Point.fromHex(Buffer.from(publicKey).toString('hex')).toRawBytes(true)).toString('hex'));

// 3. 用 Keccak-256 算出地址
const publicKeyBytes = Buffer.from(publicKey).toString('hex');
// 去掉 "04" 前綴(未壓縮公鑰的標記)
const pubKeyWithoutPrefix = publicKeyBytes.slice(2);
const hash = keccak256(Buffer.from(pubKeyWithoutPrefix, 'hex'));
const address = '0x' + Buffer.from(hash).toString('hex').slice(-40);
console.log('以太坊地址:', address);

執行完這段程式碼,你就完成了一次完整的「私鑰 → 公鑰 → 地址」的生成過程。建議你在瀏覽器 console 裡多跑幾次,感受一下每次產生的值都不一樣,但結構都是固定的。

有個細節要特別注意:壓縮格式的公鑰只有 33 位元組,用 "02" 或 "03" 開頭(取決於 y 座標的奇偶性)。這個優化能省不少儲存空間。轉帳時鏈上存的、用來驗證簽名的都是壓縮格式,別搞混了。

ECDSA 簽名:你說的話,真的是你說的?

數位簽名的核心目標很簡單:證明「這份訊息確實是我簽的,而且傳輸過程中沒被改過」。傳統金融靠手寫簽名、指紋、甚至印章,但在區塊鏈這個純數位世界,我們只能用密碼學。

ECDSA 簽名過程有點曲折,但每一步都有其意義:

  1. 對訊息取 Hash(就是 Keccak-256)
  2. 生成一個臨時的「一次性私鑰」k
  3. 用 k 算出第一個簽名分量 r(是生成點的 k 倍在 x 軸的投影)
  4. 用私鑰、r、和訊息 Hash 算出第二個分量 s
  5. 最終簽名是 (r, s)

簽名驗證的過程更複雜,但概念是:利用公鑰和簽名分量反向驗證這個數學關係是否成立。

import { secp256k1 } from 'noble-curves';
import { keccak256 } from 'noble-hashes';

// 生成金鑰對
const privateKey = secp256k1.utils.randomPrivateKey();
const publicKey = secp256k1.getPublicKey(privateKey);

// 要簽名的訊息
const message = 'Hello, Ethereum!';
const messageHash = keccak256(Buffer.from(message));
console.log('訊息 Hash:', Buffer.from(messageHash).toString('hex'));

// 簽名
const signature = secp256k1.sign(messageHash, privateKey);
console.log('簽名 r:', signature.r.toString(16));
console.log('簽名 s:', signature.s.toString(16));
console.log('簽名 v:', signature.recovery);

// 驗證簽名
const isValid = secp256k1.verify(messageHash, signature, publicKey);
console.log('簽名有效?', isValid);

// 篡改訊息後再驗證
const tamperedMessage = 'Hello, Ethereum??';
const tamperedHash = keccak256(Buffer.from(tamperedMessage));
const isValidAfterTamper = secp256k1.verify(tamperedHash, signature, publicKey);
console.log('篡改後驗證:', isValidAfterTamper); // 應該是 false

看!篡改訊息後,簽名驗證立馬失敗。這就是密碼學的威力——數學不會撒謊。

為什麼要有 v(recovery)分量?

你可能注意到簽名多了一個 v 值,這個值在以太坊裡超級重要。v 不是單純的「簽名附加資料」,而是指「從哪個公鑰推導出來的」。因為橢圓曲線的對稱性,從簽名 (r, s) 可以恢復出兩個可能的公鑰(一個是 y 為奇數,一個是偶數),v 就是用來區分這兩種情況的。

以太坊交易結構裡的 v,其實有兩層含義:在 legacy 交易格式它是 ECRECOVER 的參數,在 EIP-155 之後它同時包含了 chainId 的資訊,確保簽名只能用在特定鏈上。這個小設計防止了很多攻擊——比如有人把你的簽名拿來在其他鏈上重放。

Keccak-256:區塊鏈的萬金油

如果說橢圓曲線密碼學是以太坊的身份認證系統,那 Keccak-256 就是它的瑞士軍刀。幾乎所有地方都在用它:交易 hash、区塊 hash、狀態根、收據根、Merkle 樹的節點值......你可以把它理解成一個「數位指紋產生器」。

Keccak 是 SHA-3 的前身,但又不完全相同。NIST 在標準化 SHA-3 的時候做了一些小改動,而以太坊選擇了原始的 Keccak 實現。所以"Ethereum 的 SHA-3"和"標準的 SHA-3"其實是不一樣的東西,這個坑很多人都踩過。

哈希函數的基本性質

一個好的密碼學哈希函數必須滿足以下特性:

第三個和第四個最關鍵,它們保證了「指紋」的「唯一性」。

import { keccak256 } from 'noble-hashes';

// 基本哈希
const data1 = Buffer.from('hello');
const data2 = Buffer.from('hello');
const data3 = Buffer.from('hello!');

console.log('"hello" hash:', Buffer.from(keccak256(data1)).toString('hex'));
console.log('"hello" again:', Buffer.from(keccak256(data2)).toString('hex'));
console.log('"hello!" hash:', Buffer.from(keccak256(data3)).toString('hex'));

// 看看雪崩效應
const original = Buffer.from('test');
const modified = Buffer.from('tfst');

console.log('\n雪崩效應測試:');
console.log('"test" hash:', Buffer.from(keccak256(original)).toString('hex'));
console.log('"tfst" hash:', Buffer.from(keccak256(modified)).toString('hex'));

// 計算有多少位元不同
const hash1 = keccak256(original);
const hash2 = keccak256(modified);
let diffBits = 0;
for (let i = 0; i < hash1.length; i++) {
    let xor = hash1[i] ^ hash2[i];
    while (xor) {
        diffBits += xor & 1;
        xor >>= 1;
    }
}
console.log(`\n差異位元數: ${diffBits}/256 (約 ${(diffBits/256*100).toFixed(1)}%)`);

理想情況下,改變一個位元應該讓輸出改變大約 50% 的位元。上面的程式碼可以幫你驗證這一點。實際跑過你就會發現,Keccak-256 的雪崩效應是真的猛——真的差不多一半的位元會翻轉。

數據類型對哈希的影響

有個新手特別容易踩的坑:數據類型。同樣的字串 123 和數字 123 在內存中的表示可能完全不同,導致哈希結果天差地別。

import { keccak256 } from 'noble-hashes';

// 字串 vs UTF-8 bytes
const str = Buffer.from('0x1234');
const hex = Buffer.from('0x1234', 'hex');
const num = Buffer.from([0x12, 0x34]);

console.log('字串 "0x1234":', Buffer.from(keccak256(str)).toString('hex'));
console.log('hex bytes:     ', Buffer.from(keccak256(hex)).toString('hex'));
console.log('number bytes:   ', Buffer.from(keccak256(num)).toString('hex'));

這個差異在處理智能合約輸入的時候特別重要。ABI 編碼、事件主題、函數選擇器——全都涉及到精確的位元組處理。差一個位元組,哈希就完全不同。

Merkle 樹:證明「這筆交易在區塊裡」

區塊鏈裡有個核心問題:如果有人聲稱「第 5478 區塊包含這筆交易」,我們怎麼驗證?下載整個區塊太慢了。Merkle 樹就是為了解決這個問題而生的。

Merkle 樹的結構很直觀:葉子是交易的哈希,兩兩配對後算出父節點,父節點再兩兩配對,一路往上直到只剩一個根節點——Merkle Root。這個根節點就像是「所有交易的指紋」,任何葉子的改變都會導致根節點完全不同。

import { keccak256 } from 'noble-hashes';

// 手動實現 Merkle Tree
class SimpleMerkleTree {
    constructor(leaves) {
        this.leaves = leaves.map(l => 
            typeof l === 'string' ? Buffer.from(l) : l
        );
        this.tree = this.buildTree();
    }

    buildTree() {
        let level = this.leaves.map(l => keccak256(l));
        let tree = [level];

        while (level.length > 1) {
            const nextLevel = [];
            for (let i = 0; i < level.length; i += 2) {
                const left = level[i];
                const right = level[i + 1] || level[i]; // 奇數時複製自己
                const combined = Buffer.concat([left, right]);
                nextLevel.push(keccak256(combined));
            }
            level = nextLevel;
            tree.push(level);
        }

        return tree;
    }

    getRoot() {
        return this.tree[this.tree.length - 1][0];
    }

    getProof(leafIndex) {
        const proof = [];
        let idx = leafIndex;

        for (let i = 0; i < this.tree.length - 1; i++) {
            const level = this.tree[i];
            const isRightNode = idx % 2 === 1;
            const siblingIndex = isRightNode ? idx - 1 : idx + 1;
            
            proof.push({
                position: isRightNode ? 'left' : 'right',
                data: siblingIndex < level.length ? level[siblingIndex] : level[idx]
            });

            idx = Math.floor(idx / 2);
        }

        return proof;
    }

    verifyProof(leaf, proof, root) {
        let hash = keccak256(Buffer.from(leaf));

        for (const { position, data } of proof) {
            const combined = position === 'left' 
                ? Buffer.concat([data, hash])
                : Buffer.concat([hash, data]);
            hash = keccak256(combined);
        }

        return Buffer.from(hash).toString('hex') === Buffer.from(root).toString('hex');
    }
}

// 使用範例
const transactions = ['TX1: Alice pays Bob 1 ETH', 'TX2: Carol pays Dave 2 ETH', 
                    'TX3: Eve pays Frank 3 ETH', 'TX4: Grace pays Hank 4 ETH'];

const merkleTree = new SimpleMerkleTree(transactions);
console.log('Merkle Root:', Buffer.from(merkleTree.getRoot()).toString('hex'));

// 取得第一筆交易的 Merkle Proof
const proof = merkleTree.getProof(0);
console.log('\n第 0 筆交易的 Merkle Proof:');
proof.forEach((p, i) => {
    console.log(`Level ${i}: ${p.position} => ${Buffer.from(p.data).toString('hex').slice(0, 16)}...`);
});

// 驗證 proof
const isValid = merkleTree.verifyProof(transactions[0], proof, merkleTree.getRoot());
console.log('\nProof 驗證結果:', isValid);

// 篡改測試
const tamperedTx = 'TX1: Alice pays Bob 10 ETH';
const isValidTampered = merkleTree.verifyProof(tamperedTx, proof, merkleTree.getRoot());
console.log('篡改後驗證結果:', isValidTampered);

這個程式碼展示了一個完整的 Merkle Tree 實現。Merkle Proof 的魔力在於:你只需要 O(log n) 的資料量,就能證明任意葉子確實是樹的一部分。在以太坊輕客戶端場景中,這個優化超級關鍵——手機錢包不需要下載整個區塊,只要驗證 Merkle Proof 就知道交易有沒有被包含。

以太坊狀態證明:prove 這筆余額真的存在

Merkle Patricia Trie 是以太坊實際使用的狀態樹結構。跟上面的簡化版 Merkle Tree 不太一樣,它是一個更複雜的「字典樹 + Merkle 樹」的混合體,專門用來高效處理鍵值對儲存。

以太坊的狀態包含:

每個帳戶的狀態都存在這個龐大的MPT樹結構中。當你要證明某個帳戶的余額是多少,就會用到「狀態證明」。

import { keccak256, createKeccak } from 'noble-hashes';

// 簡化版 MPT 節點概念
const keccak = createKeccak(256);

// 帳戶狀態 RLP 編碼(簡化版)
function encodeAccountState(balance, nonce, codeHash, storageRoot) {
    return Buffer.from(JSON.stringify({
        balance: balance.toString(),
        nonce: nonce.toString(),
        codeHash: Buffer.from(codeHash).toString('hex'),
        storageRoot: Buffer.from(storageRoot).toString('hex')
    }));
}

// 模擬一個簡單的狀態樹
const stateTree = {
    '0x1234...abcd': { balance: '1.5', nonce: 5 },
    '0x5678...efgh': { balance: '2.0', nonce: 12 },
    '0x9abc...ijkl': { balance: '0.5', nonce: 1 }
};

// 計算狀態根
const stateRoot = keccak(Buffer.from(JSON.stringify(stateTree)));
console.log('模擬狀態根:', Buffer.from(stateRoot).toString('hex'));

// 生成帳戶余額的「證明」
function generateBalanceProof(address, stateRoot, stateTree) {
    const accountState = stateTree[address];
    if (!accountState) {
        throw new Error('帳戶不存在');
    }
    
    return {
        accountState: accountState,
        stateRoot: stateRoot,
        merklePath: keccak(Buffer.from(address)) // 簡化的路徑
    };
}

// 驗證余額證明
function verifyBalanceProof(proof, expectedBalance) {
    // 簡化的驗證邏輯
    return proof.accountState.balance === expectedBalance;
}

const address = '0x1234...abcd';
const proof = generateBalanceProof(address, stateRoot, stateTree);
console.log('\n帳戶狀態:', proof.accountState);
console.log('驗證余額 1.5:', verifyBalanceProof(proof, '1.5'));

實際上以太坊的狀態證明要複雜得多。你需要處理分叉、hex prefix 編碼、擴展節點、枝葉節點等各種情況。但如果只是想理解原理,上面的例子應該足夠了。

在瀏覽器裡實驗:開發者工具是你的好朋友

說了那麼多理論,不如直接動手。你可以:

  1. 打開 Chrome/Firefox 的開發者工具(F12)
  2. 切到 Console 標籤
  3. 用 CDN 引入 noble-hashes 和 noble-curves:
// 在 console 裡直接貼這段(使用 ESM CDN)
import('https://esm.sh/noble-curves@1.4.0').then(async ({ secp256k1 }) => {
    import('https://esm.sh/noble-hashes@1.0.0').then(async ({ keccak256, sha256 }) => {
        
        // 測試私鑰生成
        const privateKey = secp256k1.utils.randomPrivateKey();
        console.log('你的臨時私鑰:', Buffer.from(privateKey).toString('hex'));
        
        // 測試簽名
        const msgHash = new Uint8Array(32);
        crypto.getRandomValues(msgHash);
        const sig = secp256k1.sign(msgHash, privateKey);
        console.log('簽名成功!');
        
        // 測試 Keccak
        const hash = keccak256(new TextEncoder().encode('test'));
        console.log('Keccak-256("test"):', Buffer.from(hash).toString('hex'));
    });
});

這個互動式實驗能讓你更直觀地感受密碼學運算的速度和結果。你可以隨便改參數、測試各種輸入、觀察輸出變化。數學不會說謊,但很多人對密碼學有種莫名的恐懼感——我覺得很大原因是沒有實際摸過。多折騰幾次,你會發現這東西沒有那麼神秘。

常見的密碼學錯誤和坑

在區塊鏈開發過程中,我見過太多因為密碼學使用錯誤導致的問題。有些真的很基本,但偏偏就是容易踩:

第一個坑是隨機數不安全。很多新手用 Math.random() 來生成私鑰,但這個函數不是密碼學安全的。攻擊者如果知道你的 RNG 演算法和時機,理論上可以預測你生成的私鑰。必須用 crypto.getRandomValues()crypto.randomBytes()

第二個坑是位元組序混淆。智能合約用的是大端序(Big Endian),而很多庫內部用小端序(Little Endian)。不一樣的話算出來的簽名對不上,交易就會失敗。

第三個坑是沒有做 input validation。很多人直接拿外部輸入做哈希或簽名驗證,根本不檢查格式對不對。攻擊者傳個畸形資料進來,你的系統可能就崩了。

第四個坑是重用 nonce。在 ECDSA 簽名中,如果用同一個臨時私鑰 k 簽了兩次不同的訊息,攻擊者可以直接從兩個簽名推導出你的真正私鑰。這個錯誤曾經導致比特幣和 PlayStation 3 被盜。

結語:密碼學不神秘,動手才是王道

好了,密碼學的神秘面紗應該被揭開了吧。secp256k1、ECDSA、Keccak-256、Merkle 樹——這些名詞聽起來嚇人,但只要你實際操作過一遍,就會發現它們的運作邏輯其實挺優雅的。

區塊鏈的偉大之處就是把密碼學從「實驗室裡的神秘黑科技」變成了「每個人都能使用的金融基礎設施」。而作為開發者,理解這些底層原語不是選修課,是必修課。哪天你的智能合約被黑、或者你的錢包私鑰洩漏、或者你設計的協議有安全漏洞——回想起來,問題往往就出在最基礎的密碼學理解上。

所以,多折騰、多實驗、多踩坑。這才是學習密碼學最有效的方式。別人說「雪崩效應」說得再天花亂墜,不如你自己跑一遍程式碼,數一數有多少位元翻轉來得印象深刻。


參考資源:

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

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

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