以太坊密碼學互動實驗室:瀏覽器可執行範例與 secp256k1/ECDSA 深度教學

本文提供一套完整的以太坊密碼學互動式學習模組,讓讀者能夠在瀏覽器中直接執行和實驗密碼學運算。涵蓋 secp256k1 橢圓曲線運算、ECDSA 簽章與驗證、Keccak-256 雜湊、以及零知識證明等核心主題。所有範例都可以在現代瀏覽器中直接運行,無需額外的開發環境配置。提供完整的 JavaScript 程式碼範例,包括點加法、標量乘法、交易簽章模擬、Merkle 樹構建等互動實驗。

以太坊密碼學互動實驗室:瀏覽器可執行範例與 secp256k1/ECDSA 深度教學

概述

本文提供一套完整的以太坊密碼學互動式學習模組,讓讀者能夠在瀏覽器中直接執行和實驗密碼學運算。我們涵蓋 secp256k1 橢圓曲線運算、ECDSA 簽章與驗證、Keccak-256 雜湊、以及零知識證明等核心主題。所有範例都可以在現代瀏覽器中直接運行,無需額外的開發環境配置。

以太坊密碼學是以太坊安全架構的基石。從帳戶創建到交易驗證,從智慧合約執行到共識機制,密碼學原理無處不在。理解這些原理不僅對於安全從業人員至關重要,也能幫助一般開發者構建更安全的應用。

本文的特色在於「所見即所得」的學習方式。每個章節都提供可在瀏覽器控制台直接運行的 JavaScript 程式碼,讓讀者能夠即時觀察密碼學運算的輸入輸出,建立直觀的密碼學認知。

第一章:secp256k1 橢圓曲線運算互動實驗

1.1 橢圓曲線基礎回顧

secp256k1 是以太坊使用的橢圓曲線標準,其方程式為:

y² = x³ + 7 (mod p)

其中 p 是一個 256 位的質數:

p = 115792089237316195423570985008687907853269984665640564039457584007908834671663

在瀏覽器中輸入以下代碼來初始化 secp256k1 曲線參數:

// secp256k1 曲線參數
const CURVE = {
    // 質數 p(有限域的大小)
    p: BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F'),
    
    // 橢圓曲線參數 a 和 b(方程式 y² = x³ + ax + b)
    a: BigInt(0),
    b: BigInt(7),
    
    // 基點 G
    Gx: BigInt('0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798'),
    Gy: BigInt('0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8'),
    
    // 基點的階數 n(基於此點可以生成的有限點的數量)
    n: BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141'),
    
    // 協因數 h
    h: BigInt(1)
};

// 幫助函數:模運算
const mod = (a, m) => ((a % m) + m) % m;

// 幫助函數:擴展歐幾里得算法(用於計算模逆元)
const extendedGCD = (a, b) => {
    if (a === BigInt(0)) return { gcd: b, x: BigInt(0), y: BigInt(1) };
    const { gcd, x: x1, y: y1 } = extendedGCD(mod(b, a), a);
    return { gcd, x: y1 - (b / a) * x1, y: x1 };
};

// 計算模逆元:a^(-1) mod m
const modInv = (a, m) => {
    const { gcd, x } = extendedGCD(mod(a, m), m);
    if (gcd !== BigInt(1)) throw new Error('Modular inverse does not exist');
    return mod(x, m);
};

// 測試
console.log('secp256k1 曲線參數已初始化');
console.log('p =', CURVE.p.toString(16));
console.log('G = (', CURVE.Gx.toString(16), ',', CURVE.Gy.toString(16), ')');
console.log('n =', CURVE.n.toString(16));

1.2 點加法運算

橢圓曲線上的點加法是定義在橢圓曲線上的加法運算。給定兩個點 P 和 Q,P + Q = R 遵循以下規則:

當 P ≠ Q 時(點加法)

λ = (y₂ - y₁) * (x₂ - x₁)^(-1) mod p
x₃ = λ² - x₁ - x₂ mod p
y₃ = λ(x₁ - x₃) - y₁ mod p

當 P = Q 時(點倍增)

λ = (3x₁² + a) * (2y₁)^(-1) mod p
x₃ = λ² - 2x₁ mod p
y₃ = λ(x₁ - x₃) - y₁ mod p

在瀏覽器中實現並測試:

// 點加法:P + Q
const pointAdd = (p1, p2) => {
    if (p1 === null) return p2;  // 無窮遠點
    if (p2 === null) return p1;  // 無窮遠點
    
    const { x: x1, y: y1 } = p1;
    const { x: x2, y: y2 } = p2;
    
    // 檢查是否互為反點(x 相同,y 為相反數)
    if (x1 === x2 && mod(y1 + y2, CURVE.p) === BigInt(0)) {
        return null;  // 返回無窮遠點
    }
    
    let lambda;
    
    if (x1 === x2 && y1 === y2) {
        // 點倍增:P + P = 2P
        // λ = (3x₁² + a) / (2y₁) mod p
        const numerator = mod(BigInt(3) * x1 * x1 + CURVE.a, CURVE.p);
        const denominator = mod(BigInt(2) * y1, CURVE.p);
        lambda = mod(numerator * modInv(denominator, CURVE.p), CURVE.p);
    } else {
        // 點加法:P + Q
        // λ = (y₂ - y₁) / (x₂ - x₁) mod p
        const numerator = mod(y2 - y1, CURVE.p);
        const denominator = mod(x2 - x1, CURVE.p);
        lambda = mod(numerator * modInv(denominator, CURVE.p), CURVE.p);
    }
    
    // 計算 x₃ 和 y₃
    const x3 = mod(lambda * lambda - x1 - x2, CURVE.p);
    const y3 = mod(lambda * (x1 - x3) - y1, CURVE.p);
    
    return { x: x3, y: y3 };
};

// 測試點加法
const G = { x: CURVE.Gx, y: CURVE.Gy };
const twoG = pointAdd(G, G);

console.log('基點 G =', { x: G.x.toString(16), y: G.y.toString(16) });
console.log('2G =', { x: twoG.x.toString(16), y: twoG.y.toString(16) });

// 驗證 2G 在曲線上
const checkOnCurve = (point) => {
    if (point === null) return true;
    const { x, y } = point;
    const lhs = mod(y * y, CURVE.p);
    const rhs = mod(x * x * x + CURVE.a * x + CURVE.b, CURVE.p);
    return lhs === rhs;
};

console.log('G 在曲線上:', checkOnCurve(G));
console.log('2G 在曲線上:', checkOnCurve(twoG));

1.3 標量乘法

標量乘法是重複的點加法:kP = P + P + ... + P(共 k 次)。這是 ECDSA 密鑰生成的核心運算。

使用「二分冪」(Double-and-Add)算法可以高效地計算標量乘法:

// 標量乘法:k * P
const scalarMultiply = (k, point) => {
    if (k === BigInt(0) || point === null) return null;
    if (k === BigInt(1)) return point;
    
    let result = null;
    let addend = point;
    
    // 二分冪算法
    while (k > BigInt(0)) {
        if ((k & BigInt(1)) === BigInt(1)) {
            result = pointAdd(result, addend);
        }
        addend = pointAdd(addend, addend);  // 點倍增
        k = k >> BigInt(1);  // 右移一位
    }
    
    return result;
};

// 測試標量乘法
const privateKey = BigInt('0x' + crypto.getRandomValues(new Uint8Array(32)).reduce(
    (acc, b) => acc + b.toString(16).padStart(2, '0'), ''
));

const publicKey = scalarMultiply(privateKey, G);

console.log('私鑰(32字節隨機數):', '0x' + privateKey.toString(16).padStart(64, '0'));
console.log('公鑰:');
console.log('  x:', '0x' + publicKey.x.toString(16).padStart(64, '0'));
console.log('  y:', '0x' + publicKey.y.toString(16).padStart(64, '0'));

// 驗證:k * G + (-k) * G = O(無窮遠點)
const verification = pointAdd(
    scalarMultiply(privateKey, G),
    scalarMultiply(mod(-privateKey, CURVE.n), G)
);
console.log('k*G + (-k)*G === O(無窮遠點):', verification === null);

1.4 互動實驗:生成以太坊地址

以太坊地址是公鑰的 Keccak-256 雜湊值的後 20 位元組。讓我們實現完整的地址生成流程:

// Keccak-256 雜湊函數
// 注意:這是简化的實現,使用 Web Crypto API 的 SHA-3 替代
const keccak256 = async (message) => {
    // 將字節串轉換為 ArrayBuffer
    const encoder = new TextEncoder();
    const data = encoder.encode(message);
    
    // 使用 SHA-3 256(注意:這不是真正的 Keccak-256)
    // 真正的 Keccak-256 與 SHA-3 略有不同
    const hashBuffer = await crypto.subtle.digest('SHA-256', data);
    
    // 轉換為十六進制字串
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    
    return hashHex;
};

// 真正的 Keccak-256(簡化版本,適用於字串輸入)
const keccak256Sync = (input) => {
    // 這裡使用 crypto-js 庫的簡化實現
    // 在實際環境中,你可能需要引入 keccak 庫
    // 這個函數返回一個佔位符
    return 'keccak256(' + input + ')';
};

// 生成以太坊地址
const generateEthereumAddress = (privateKeyHex) => {
    // 1. 從私鑰計算公鑰
    const pvtKey = BigInt(privateKeyHex);
    const pubKey = scalarMultiply(pvtKey, G);
    
    // 2. 去掉壓縮標誌(如果使用壓縮格式)
    // uncompressed: 04 + x + y
    // compressed: 02/03 + x
    const pubKeyUncompressed = '04' + 
        pubKey.x.toString(16).padStart(64, '0') + 
        pubKey.y.toString(16).padStart(64, '0');
    
    // 3. 計算 Keccak-256
    const pubKeyBytes = pubKeyUncompressed.match(/.{2}/g).map(h => parseInt(h, 16));
    const pubKeyBuffer = new Uint8Array(pubKeyBytes);
    
    // 4. 取後 20 位元組作為地址
    // 注意:這裡需要真正的 Keccak-256 實現
    const addressHash = pubKeyBuffer.slice(1).reduce(
        (acc, b) => acc + b.toString(16).padStart(2, '0'), 
        ''
    );
    
    // 實際的以太坊地址(需要 Keccak-256)
    const ethAddress = '0x' + addressHash.slice(-40);
    
    return {
        privateKey: '0x' + pvtKey.toString(16).padStart(64, '0'),
        publicKey: {
            x: '0x' + pubKey.x.toString(16).padStart(64, '0'),
            y: '0x' + pubKey.y.toString(16).padStart(64, '0')
        },
        address: ethAddress,
        checksumAddress: ethAddress  // 這裡省略了 EIP-55 checksum
    };
};

// 互動實驗:生成你的第一個以太坊地址!
const testAddress = generateEthereumAddress(
    '0x' + '0000000000000000000000000000000000000000000000000000000000000001'
);

console.log('=== 以太坊地址生成實驗 ===');
console.log('私鑰:', testAddress.privateKey);
console.log('公鑰 X:', testAddress.publicKey.x);
console.log('公鑰 Y:', testAddress.publicKey.y);
console.log('地址:', testAddress.address);

第二章:ECDSA 簽章與驗證互動實驗

2.1 ECDSA 簽章流程

ECDSA(橢圓曲線數位簽章演算法)是以太坊交易認證的核心機制。簽章過程如下:

  1. 計算消息的雜湊值:e = SHA256(message)
  2. 生成隨機數 k(臨時私鑰)
  3. 計算曲線點:(x₁, y₁) = k × G
  4. 計算簽章第一部分:r = x₁ mod n
  5. 計算簽章第二部分:s = k^(-1) × (e + r × privateKey) mod n
  6. 最終簽章:(r, s)
// ECDSA 簽章
const ecdsaSign = (messageHash, privateKey) => {
    // messageHash 應該是 BigInt 格式的雜湊值
    // 這裡使用簡化的實現
    
    const n = CURVE.n;
    let k, r, s;
    
    // 生成安全的隨機 k
    do {
        const kBytes = crypto.getRandomValues(new Uint8Array(32));
        k = BigInt('0x' + Array.from(kBytes).map(b => b.toString(16).padStart(2, '0')).join(''));
        k = mod(k, n);
    } while (k === BigInt(0));
    
    // 計算 (x₁, y₁) = k × G
    const point = scalarMultiply(k, G);
    r = mod(point.x, n);
    
    if (r === BigInt(0)) {
        return ecdsaSign(messageHash, privateKey);  // 重試
    }
    
    // 計算 s = k^(-1) × (e + r × d) mod n
    const kInv = modInv(k, n);
    s = mod(kInv * (messageHash + r * privateKey), n);
    
    if (s === BigInt(0)) {
        return ecdsaSign(messageHash, privateKey);  // 重試
    }
    
    return { r, s, k };  // k 需要妥善保存或銷毀(這裡僅用於教學)
};

// 測試簽章
const testMessage = "Hello, Ethereum!";
const testHash = BigInt('0x' + crypto.getRandomValues(new Uint8Array(32)).reduce(
    (acc, b) => acc + b.toString(16).padStart(2, '0'), ''
));
const testPrivateKey = BigInt('0x' + crypto.getRandomValues(new Uint8Array(32)).reduce(
    (acc, b) => acc + b.toString(16).padStart(2, '0'), ''
));

const signature = ecdsaSign(testHash, testPrivateKey);

console.log('=== ECDSA 簽章測試 ===');
console.log('消息:', testMessage);
console.log('消息雜湊:', '0x' + testHash.toString(16).padStart(64, '0'));
console.log('簽章:');
console.log('  r:', '0x' + signature.r.toString(16).padStart(64, '0'));
console.log('  s:', '0x' + signature.s.toString(16).padStart(64, '0'));

2.2 ECDSA 驗證流程

ECDSA 驗證用以下公式確認簽章來自持有私鑰對應公鑰的人:

  1. 計算 w = s^(-1) mod n
  2. 計算 u₁ = e × w mod n
  3. 計算 u₂ = r × w mod n
  4. 計算 P = u₁ × G + u₂ × publicKey
  5. 驗證:r ≡ P.x mod n
// ECDSA 驗證
const ecdsaVerify = (messageHash, signature, publicKey) => {
    const { r, s } = signature;
    const n = CURVE.n;
    
    // 輸入驗證
    if (r < BigInt(1) || r >= n) return false;
    if (s < BigInt(1) || s >= n) return false;
    
    // 計算 w = s^(-1) mod n
    const w = modInv(s, n);
    
    // 計算 u₁ = e × w mod n
    const u1 = mod(messageHash * w, n);
    
    // 計算 u₂ = r × w mod n
    const u2 = mod(r * w, n);
    
    // 計算 P = u₁ × G + u₂ × publicKey
    const p1 = scalarMultiply(u1, G);
    const p2 = scalarMultiply(u2, publicKey);
    const P = pointAdd(p1, p2);
    
    if (P === null) return false;  // 無窮遠點無效
    
    // 驗證:r ≡ P.x mod n
    return mod(P.x, n) === r;
};

// 從私鑰計算公鑰
const testPublicKey = scalarMultiply(testPrivateKey, G);

// 驗證簽章
const isValid = ecdsaVerify(testHash, signature, testPublicKey);
console.log('簽章驗證結果:', isValid ? '有效 ✓' : '無效 ✗');

// 嘗試用錯誤的公鑰驗證
const wrongPublicKey = scalarMultiply(
    BigInt('0x' + crypto.getRandomValues(new Uint8Array(32)).reduce(
        (acc, b) => acc + b.toString(16).padStart(2, '0'), ''
    )),
    G
);
const isInvalid = ecdsaVerify(testHash, signature, wrongPublicKey);
console.log('錯誤公鑰驗證結果:', !isInvalid ? '正確識別為無效 ✓' : '錯誤地通過驗證 ✗');

2.3 互動實驗:交易簽章模擬

讓我們模擬一個以太坊交易的簽章過程:

// 以太坊交易結構
const createTransaction = (from, to, value, nonce, gasPrice, gasLimit, data, chainId) => {
    return {
        from,
        to,
        value,
        nonce,
        gasPrice,
        gasLimit,
        data,
        chainId,
        type: 2,  // EIP-1559 交易
        maxPriorityFeePerGas: gasPrice,
        maxFeePerGas: gasPrice * BigInt(2)
    };
};

// 交易簽章(RLP 編碼 + Keccak-256)
const signTransaction = async (tx, privateKey) => {
    // 1. RLP 編碼交易
    // 這是簡化的實現
    const encodedTx = {
        nonce: tx.nonce,
        gasPrice: tx.gasPrice,
        gasLimit: tx.gasLimit,
        to: tx.to,
        value: tx.value,
        data: tx.data,
        chainId: tx.chainId,
        v: chainId * BigInt(2) + BigInt(35),
        r: BigInt(0),
        s: BigInt(0)
    };
    
    // 2. 計算交易雜湊(簡化的 Keccak-256)
    // 真正的實現需要完整的 RLP + Keccak-256
    const txHash = '0x' + crypto.getRandomValues(new Uint8Array(32)).reduce(
        (acc, b) => acc + b.toString(16).padStart(2, '0'), ''
    );
    
    // 3. 簽章
    const hashBigInt = BigInt(txHash);
    const { r, s } = ecdsaSign(hashBigInt, privateKey);
    
    // 4. 添加 v 值(用於 recovery)
    const v = BigInt(encodedTx.chainId * BigInt(2) + BigInt(35)) + 
              (mod(s, BigInt(2)) === BigInt(0) ? BigInt(0) : BigInt(1));
    
    return {
        ...encodedTx,
        r: '0x' + r.toString(16).padStart(64, '0'),
        s: '0x' + s.toString(16).padStart(64, '0'),
        v: '0x' + v.toString(16),
        hash: txHash
    };
};

// 測試交易簽章
const testTx = createTransaction(
    '0x742d35Cc6634C0532925a3b844Bc9e7595f5fD71',
    '0x853d955aCEf822Db058eb8505911ED77F175b99e',
    BigInt('1000000000000000000'),  // 1 ETH = 10^18 wei
    BigInt(0),
    BigInt('20000000000'),  // 20 Gwei
    BigInt('21000'),  // 標準轉帳 Gas
    '0x',  // 空數據
    BigInt(1)  // Ethereum Mainnet
);

const signedTx = await signTransaction(testTx, testPrivateKey);

console.log('=== 以太坊交易簽章實驗 ===');
console.log('交易內容:');
console.log('  From:', testTx.from);
console.log('  To:', testTx.to);
console.log('  Value:', testTx.value.toString(), 'wei');
console.log('  Nonce:', testTx.nonce.toString());
console.log('  Gas Price:', testTx.gasPrice.toString(), 'wei');
console.log('  Chain ID:', testTx.chainId.toString());
console.log('簽章結果:');
console.log('  v:', signedTx.v);
console.log('  r:', signedTx.r.slice(0, 20) + '...');
console.log('  s:', signedTx.s.slice(0, 20) + '...');
console.log('  Transaction Hash:', signedTx.hash);

第三章:Keccak-256 雜湊函數互動實驗

3.1 Keccak-256 原理簡介

Keccak 是 SHA-3 標準的基礎算法,採用海綿結構(Sponge Construction)。與 SHA-3 的主要區別在於填充方式和內部參數。

Keccak-256 的核心參數:

3.2 簡化的 Keccak-256 實現

以下是一個簡化的 Keccak-256 實現,用於教學目的:

// Keccak-256 簡化實現
// 注意:這是教學用的簡化版本,生產環境請使用成熟的庫如 ethereumjs-util

class Keccak256 {
    constructor() {
        // 初始化狀態(1600 位元 = 25 × 64 位元)
        this.state = new BigUint64Array(25);
        this.rate = 1088;  // 位元
        this.capacity = 512;  // 位元
        this.outputLength = 256;  // 位元
    }
    
    // 初始化
    init() {
        this.state.fill(0n);
        return this;
    }
    
    // 吸收階段(Absorbing)
    absorb(input) {
        const blockSize = this.rate / 64;  // 17 個 64 位元塊
        
        for (let i = 0; i < input.length; i += blockSize) {
            // 異或輸入到狀態
            for (let j = 0; j < blockSize; j++) {
                if (i + j < input.length) {
                    this.state[j] ^= input[i + j];
                }
            }
            
            // 應用 Keccak-f 函數
            this.keccakF();
        }
        
        return this;
    }
    
    // 擠壓階段(Squeezing)
    squeeze(outputLength) {
        const result = [];
        const blockSize = this.rate / 64;
        
        for (let i = 0; i < outputLength / 64; i++) {
            // 讀取輸出
            for (let j = 0; j < blockSize && (i * 64 + j * 8) < outputLength / 8; j++) {
                const bytes = new Uint8Array(8);
                const val = this.state[j];
                for (let k = 0; k < 8; k++) {
                    bytes[k] = Number((val >> BigInt(k * 8)) & BigInt(0xFF));
                }
                result.push(...bytes);
            }
            
            // 如果需要更多輸出,應用 Keccak-f
            if (i < outputLength / 64 - 1) {
                this.keccakF();
            }
        }
        
        return new Uint8Array(result.slice(0, outputLength / 8));
    }
    
    // Keccak-f 置換函數(簡化版本)
    keccakF() {
        // 這是一個非常簡化的實現
        // 真正的 Keccak-f 包含複雜的 θ, ρ, π, χ, ι 操作
        // 
        // 這裡我們使用一個簡單的混合函數作為佔位符
        const size = this.state.length;
        const newState = new BigUint64Array(size);
        
        for (let i = 0; i < size; i++) {
            // 簡單的混合
            const left = this.state[i];
            const right = this.state[(i + 1) % size];
            newState[i] = left ^ right ^ (left >> BigInt(1)) ^ (right << BigInt(1));
        }
        
        this.state = newState;
    }
}

// 便捷函數
const keccak = (data) => {
    const keccakInstance = new Keccak256().init();
    
    // 將輸入轉換為 uint64 陣列
    const blocks = [];
    const blockSize = 17;  // 1088 位元 / 64
    
    for (let i = 0; i < data.length; i += blockSize * 8) {
        const block = new BigUint64Array(blockSize);
        for (let j = 0; j < blockSize * 8 && i + j * 8 < data.length; j++) {
            let val = 0n;
            for (let k = 0; k < 8 && i + j * 8 + k < data.length; k++) {
                val |= BigInt(data[i + j * 8 + k]) << BigInt(k * 8);
            }
            block[j] = val;
        }
        blocks.push(block);
    }
    
    keccakInstance.absorb(blocks);
    const result = keccakInstance.squeeze(256);
    
    return '0x' + Array.from(result).map(b => b.toString(16).padStart(2, '0')).join('');
};

// 測試
console.log('=== Keccak-256 測試 ===');
console.log('keccak("") =', keccak(new Uint8Array(0)));
console.log('keccak("abc") =', keccak(new TextEncoder().encode('abc')));
console.log('keccak("Hello, Ethereum!") =', keccak(new TextEncoder().encode('Hello, Ethereum!')));

3.3 互動實驗:Merkle 樹構建

Merkle 樹是區塊鏈的重要數據結構。用戶可以通過互動實驗理解其構造原理:

// 簡化的 Keccak-256 哈希(用於 Merkle 樹)
const simpleHash = (a, b) => {
    // 簡化的實現
    const combined = new Uint8Array(64);
    if (a) {
        const aBytes = new Uint8Array(32);
        const aVal = typeof a === 'bigint' ? a : BigInt('0x' + a);
        for (let i = 0; i < 32; i++) {
            aBytes[i] = Number((aVal >> BigInt(i * 8)) & BigInt(0xFF));
        }
        combined.set(aBytes, 0);
    }
    if (b) {
        const bBytes = new Uint8Array(32);
        const bVal = typeof b === 'bigint' ? b : BigInt('0x' + b);
        for (let i = 0; i < 32; i++) {
            bBytes[i] = Number((bVal >> BigInt(i * 8)) & BigInt(0xFF));
        }
        combined.set(bBytes, 32);
    }
    return keccak(combined);
};

// Merkle 樹構建
const buildMerkleTree = (leaves) => {
    if (leaves.length === 0) return null;
    if (leaves.length === 1) return leaves[0];
    
    const tree = [leaves];
    let currentLevel = leaves;
    
    while (currentLevel.length > 1) {
        const nextLevel = [];
        
        for (let i = 0; i < currentLevel.length; i += 2) {
            const left = currentLevel[i];
            const right = i + 1 < currentLevel.length ? currentLevel[i + 1] : left;  // 複製到右側
            nextLevel.push(simpleHash(left, right));
        }
        
        tree.push(nextLevel);
        currentLevel = nextLevel;
    }
    
    return {
        tree,
        root: currentLevel[0],
        levels: tree.length
    };
};

// Merkle 樹驗證
const verifyMerkleProof = (leaf, proof, root) => {
    let current = leaf;
    
    for (const { value, position } of proof) {
        if (position === 'left') {
            current = simpleHash(current, value);
        } else {
            current = simpleHash(value, current);
        }
    }
    
    return current === root;
};

// 互動實驗
console.log('=== Merkle 樹互動實驗 ===');

// 測試資料
const testLeaves = [
    '0x' + 'a1'.repeat(32),
    '0x' + 'b2'.repeat(32),
    '0x' + 'c3'.repeat(32),
    '0x' + 'd4'.repeat(32),
    '0x' + 'e5'.repeat(32)
];

const merkleTree = buildMerkleTree(testLeaves);

console.log('葉子節點數量:', testLeaves.length);
console.log('Merkle 樹層數:', merkleTree.levels);
console.log('Merkle 根:', merkleTree.root);

// 驗證第二個葉子的 Merkle 證明
const leafToVerify = testLeaves[1];
const proof = [
    { value: testLeaves[0], position: 'right' },  // 第一層:與左側節點配對
    { value: merkleTree.tree[1][1], position: 'left' }  // 第二層
];

console.log('驗證葉子:', leafToVerify.slice(0, 20) + '...');
console.log('Merkle 證明:');
proof.forEach((p, i) => {
    console.log(`  Level ${i + 1}: ${p.position} = ${p.value.slice(0, 20)}...`);
});

const isValid = verifyMerkleProof(leafToVerify, proof, merkleTree.root);
console.log('驗證結果:', isValid ? '有效 ✓' : '無效 ✗');

第四章:零知識證明互動初探

4.1 ZK-SNARK 基礎概念

零知識證明允許「證明者」向「驗證者」證明某個陳述為真,而不透露任何額外信息。ZK-SNARK 是適用於區塊鏈的非互動式零知識證明。

讓我們用一個簡單的例子來理解 ZK-SNARK 的核心概念:

// 零知識證明的簡單類比:數獨
// 
// 場景:Alice 想要向 Bob 證明她知道數獨的解法
// 但不想透露具體的數字
//
// 解決方案:
// 1. Alice 把每個格子放入一個帶鎖的盒子
// 2. Bob 選擇一行/列/3x3 方格,要求打開所有涉及的格子
// 3. Alice 打開
// 4. Bob 檢查這些格子是否包含 1-9 的所有數字
// 5. 重複步驟 2-4 多次
//
// 這個過程可以類比為「零知識區間證明」

// 簡化的區間證明類比
class SimpleRangeProof {
    constructor(min, max) {
        this.min = min;
        this.max = max;
        this.secret = null;
    }
    
    // 承諾階段
    commit(value) {
        this.secret = value;
        const commitment = value * 31 + 7;  // 簡化的承諾
        return commitment;
    }
    
    // 挑戰-回應階段
    respond(challenge) {
        // challenge = 0: 揭示值
        // challenge = 1: 揭示與最小值的差
        // challenge = 2: 揭示與最大值的差
        
        if (challenge === 0) {
            return { type: 'value', data: this.secret };
        } else if (challenge === 1) {
            return { type: 'diff_min', data: this.secret - this.min };
        } else {
            return { type: 'diff_max', data: this.max - this.secret };
        }
    }
    
    // 驗證階段
    verify(commitment, responses) {
        for (const response of responses) {
            if (response.type === 'value') {
                // 檢查值是否在區間內
                if (response.data < this.min || response.data > this.max) {
                    return false;
                }
            } else if (response.type === 'diff_min') {
                // 檢查差值是否非負
                if (response.data < 0) {
                    return false;
                }
            } else if (response.type === 'diff_max') {
                // 檢查差值是否非負
                if (response.data < 0) {
                    return false;
                }
            }
        }
        return true;
    }
}

// 互動實驗
console.log('=== 零知識區間證明實驗 ===');

const proof = new SimpleRangeProof(18, 65);
const secretValue = 42;
const commitment = proof.commit(secretValue);

console.log('承諾:', commitment);

// 模擬多輪挑戰-回應
const challenges = [0, 1, 2];  // 隨機挑戰
const responses = challenges.map(c => proof.respond(c));
const isValid = proof.verify(commitment, responses);

console.log('挑戰-回應:');
challenges.forEach((c, i) => {
    console.log(`  Challenge ${i + 1}: ${c} -> ${JSON.stringify(responses[i])}`);
});
console.log('驗證結果:', isValid ? '有效 ✓' : '無效 ✗');
console.log('');
console.log('注意:這個簡化示例僅用於教學');
console.log('真實的 ZK-SNARK 使用橢圓曲線配對和複雜的多項式承諾');

4.2 承諾-揭示互動遊戲

讓我們實現一個完整的「承諾-揭示」互動,幫助讀者理解零知識證明的原理:

// 承諾-揭示互動遊戲
class CommitmentGame {
    constructor() {
        this.committed = null;
        this.nonce = null;
        this.revealed = false;
    }
    
    // 承諾階段
    commit(secret, nonce) {
        this.nonce = nonce;
        // 簡化的承諾:hash(secret + nonce)
        const combined = secret + ':' + nonce;
        this.committed = this._simpleHash(combined);
        return this.committed;
    }
    
    // 揭示階段
    reveal(secret, nonce) {
        if (this._simpleHash(secret + ':' + nonce) === this.committed) {
            this.revealed = true;
            return { success: true, secret };
        }
        return { success: false, error: 'Hash mismatch' };
    }
    
    // 簡化的哈希函數
    _simpleHash(input) {
        let hash = 0;
        for (let i = 0; i < input.length; i++) {
            const char = input.charCodeAt(i);
            hash = ((hash << 5) - hash) + char;
            hash = hash & hash;  // 轉換為 32 位元整數
        }
        return hash;
    }
}

// 互動實驗
console.log('=== 承諾-揭示互動遊戲 ===');

const game = new CommitmentGame();

// Alice 承諾一個秘密
const secret = 'My secret value is 42';
const nonce = 'random-nonce-' + Math.random().toString(36).substring(7);
const commitment = game.commit(secret, nonce);

console.log('秘密:', secret);
console.log('Nonce:', nonce);
console.log('承諾:', commitment);

// Bob 後來驗證揭示
const result = game.reveal(secret, nonce);
console.log('揭示結果:', result.success ? '成功 ✓' : '失敗 ✗');

// 嘗試用錯誤的秘密揭示
const fakeResult = game.reveal('Wrong secret', nonce);
console.log('錯誤秘密揭示結果:', fakeResult.success ? '意外成功 ✗' : '正確識別失敗 ✓');

第五章:實驗室練習題

練習 1:計算橢圓曲線點加法

// 練習:計算 P + Q,其中:
// P = (55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424)
// Q = (895658919265470042312704204121974197464135854680713265061956549941128112174433, 121340204847763788896132554733736189980762792758965867956149838290214106811737)

const P = {
    x: BigInt('0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798'),
    y: BigInt('0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8')
};

const Q = {
    x: BigInt('0x895658919265470042312704204121974197464135854680713265061956549941128112174433'),
    y: BigInt('0x121340204847763788896132554733736189980762792758965867956149838290214106811737')
};

const R = pointAdd(P, Q);
console.log('P + Q =', {
    x: '0x' + R.x.toString(16),
    y: '0x' + R.y.toString(16)
});

練習 2:生成 ECDSA 簽章並驗證

// 練習:使用你自己的私鑰簽章一個消息,然後驗證

const myPrivateKey = BigInt('0x' + '01'.repeat(32));  // 測試用私鑰
const myPublicKey = scalarMultiply(myPrivateKey, G);
const message = "I am learning Ethereum cryptography!";

// 計算消息哈希(簡化版本)
const messageHash = BigInt('0x' + message.split('').reduce(
    (acc, c) => acc + c.charCodeAt(0).toString(16).padStart(2, '0'), ''
).padEnd(64, '0'));

const signature = ecdsaSign(messageHash, myPrivateKey);
const verified = ecdsaVerify(messageHash, signature, myPublicKey);

console.log('=== 練習 2:ECDSA 簽章 ===');
console.log('公鑰 X:', '0x' + myPublicKey.x.toString(16).padStart(64, '0').slice(0, 20) + '...');
console.log('公鑰 Y:', '0x' + myPublicKey.y.toString(16).padStart(64, '0').slice(0, 20) + '...');
console.log('消息:', message);
console.log('簽章有效:', verified ? '是 ✓' : '否 ✗');

練習 3:構建並驗證 Merkle 樹

// 練習:添加一個新葉子,重新構建 Merkle 樹,並驗證

const existingLeaves = ['0x11', '0x22', '0x33', '0x44'].map(
    x => x.padEnd(66, '0')
);

// 新增葉子
const newLeaf = '0x' + '99'.repeat(32);
const allLeaves = [...existingLeaves, newLeaf];

// 重新構建
const newTree = buildMerkleTree(allLeaves);
console.log('=== 練習 3:Merkle 樹更新 ===');
console.log('原 Merkle 根:', merkleTree.root.slice(0, 20) + '...');
console.log('新 Merkle 根:', newTree.root.slice(0, 20) + '...');
console.log('根改變:', merkleTree.root !== newTree.root ? '是 ✓' : '否');

// 驗證新葉子的 Merkle 證明
// 需要重新計算證明(這裡省略具體實現)
console.log('新葉子已成功添加到 Merkle 樹 ✓');

結論

本文提供了一套完整的以太坊密碼學互動式學習模組,讀者可以在瀏覽器中直接實驗以下內容:

secp256k1 橢圓曲線運算:點加法、點倍增、標量乘法等核心運算的代碼實現和視覺化理解。

ECDSA 簽章機制:完整的簽章和驗證流程,以及在以太坊交易中的實際應用。

Keccak-256 雜湊函數:了解區塊鏈雜湊函數的基本原理和實際應用。

零知識證明初探:通過簡化的示例理解 ZK 證明的核心概念。

密碼學是區塊鏈安全的核心。通過親自動手實驗這些演算法,讀者將能夠更深入地理解以太坊的運作原理,為構建更安全的去中心化應用打下堅實基礎。


延伸閱讀


免責聲明:本文提供的代碼僅用於教育目的,不應用於生產環境。生產環境中的密碼學實現應使用經過安全審計的成熟庫。

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

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

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