以太坊密碼學互動實驗室:瀏覽器可執行範例與 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(橢圓曲線數位簽章演算法)是以太坊交易認證的核心機制。簽章過程如下:
- 計算消息的雜湊值:e = SHA256(message)
- 生成隨機數 k(臨時私鑰)
- 計算曲線點:(x₁, y₁) = k × G
- 計算簽章第一部分:r = x₁ mod n
- 計算簽章第二部分:s = k^(-1) × (e + r × privateKey) mod n
- 最終簽章:(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 驗證用以下公式確認簽章來自持有私鑰對應公鑰的人:
- 計算 w = s^(-1) mod n
- 計算 u₁ = e × w mod n
- 計算 u₂ = r × w mod n
- 計算 P = u₁ × G + u₂ × publicKey
- 驗證: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 的核心參數:
- 速率(Rate):1600 - 2×256 = 1088 位元
- 容量(Capacity):512 位元
- 輸出長度: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 證明的核心概念。
密碼學是區塊鏈安全的核心。通過親自動手實驗這些演算法,讀者將能夠更深入地理解以太坊的運作原理,為構建更安全的去中心化應用打下堅實基礎。
延伸閱讀
- Ethereum Yellow Paper
- Web Crypto API
- Vitalik's Blog on Cryptography
- Consensys Smart Contract Best Practices
免責聲明:本文提供的代碼僅用於教育目的,不應用於生產環境。生產環境中的密碼學實現應使用經過安全審計的成熟庫。
相關文章
- 以太坊密碼學基礎完整指南:橢圓曲線密碼學、簽章機制與 Merkle Tree 結構 — 本文深入分析以太坊密碼學系統的三大支柱:secp256k1 橢圓曲線與 ECDSA 簽章機制的數學原理、KECCAK-256 雜湊函數的設計特點、以及 Patricia Merkle Trie 資料結構在狀態管理中的關鍵角色。我們從密碼學理論出發,經過詳盡的數學推導,最終落實到 Solidity、Go 與 Rust 的實際程式碼範例。涵蓋離散對數問題、點加法/倍增運算、ECDSA 簽章驗證、Merkle Proof、EIP-1559 等核心概念的完整技術解析。
- 以太坊密碼學原語直覺解釋:橢圓曲線、布隆過濾器與 Keccak 的工程視角 — 本文從工程師的視角出發,提供橢圓曲線、布隆過濾器(Blooom Filter)等密碼學原語的直覺性解釋、完整的數學推導、以及可直接使用的程式碼範例,幫助讀者建立對這些密碼學原語的深入理解。涵蓋 ECDSA 簽名、Keccak-256 哈希、布隆過濾器的設計原理與實際應用。
- 以太坊密碼學基礎完整實作指南:從理論到智能合約部署的工程實踐 — 本文提供以太坊密碼學基礎的完整實作指南,涵蓋 secp256k1 橢圓曲線密碼學、ECDSA 簽章機制、Keccak-256 雜湊函數、RNG 安全、鏈上前置編譯合約等核心主題的技術實作細節。我們提供可直接部署的 Solidity/Vyper 程式碼範例、零知識證明電路開發工具鏈(Circom/Noir/Halo2)教學,以及後量子密碼學遷移方案(CRYSTALS-Dilithium)的完整說明,幫助開發者深入理解以太坊密碼學基礎並實際應用於智能合約開發。
- secp256k1 橢圓曲線密碼學實作:以太坊密碼學原語工程指南 — 本指南從工程師視角出發,提供 secp256k1 橢圓曲線的完整實作,包括有限域運算、點加法與倍點運算、標量乘法、以及 ECDSA 簽名生成與驗證的詳細程式碼。理解這些底層密碼學原語不僅有助於深入理解區塊鏈安全機制,更能為開發高安全性應用奠定基礎。
- 橢圓曲線密碼學直覺式圖解說明:從基礎原理到以太坊應用 — 本文以直覺式圖解說明讓讀者無需深厚數學背景也能理解橢圓曲線密碼學的核心概念。涵蓋橢圓曲線加法的幾何意義、離散對數問題的安全性基礎、以太坊地址生成的完整流程、ECDSA 簽名演算法、Vitalik 恢復機制,以及零知識電路和 Layer 2 中的橢圓曲線應用。
延伸閱讀與來源
- Ethereum.org Developers 官方開發者入口與技術文件
- EIPs 以太坊改進提案完整列表
- Solidity 文檔 智慧合約程式語言官方規格
- EVM 代碼庫 EVM 實作的核心參考
- Alethio EVM 分析 EVM 行為的正規驗證
這篇文章對您有幫助嗎?
請告訴我們如何改進:
評論
發表評論
注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。
目前尚無評論,成為第一個發表評論的人吧!