以太坊 EVM 密碼學與零知識證明整合深度技術分析:從橢圓曲線到 zkEVM 實作
本文深入剖析以太坊 EVM 的密碼學基礎設施與零知識證明整合技術。從 secp256k1 橢圓曲線數學、Keccak-256 雜湊函數、Merkle Proof 驗證,到 ECRECOVER 指令與橢圓曲線預編譯合約,提供完整的 Python 和 Solidity 實作範例。同時分析 zkEVM 的技術架構、PLONK/Groth16 驗證機制,以及如何在 EVM 上實現零知識電路。適合想要深入理解以太坊密碼學內核的工程師和研究人員。
以太坊 EVM 密碼學與零知識證明整合深度技術分析:從橢圓曲線到 zkEVM 實作
說實話,寫這篇文章的起因是我被問到一個看似簡單的問題:「以太坊怎麼用密碼學證明一筆交易是有效的?」然後我發現要把這個問題回答清楚,得從 secp256k1 橢圓曲線、Keccak 雜湊函數、Merkle Patricia Trie、再到 PLONK 或 Groth16 這些零知識證明系統全部串起來。這簡直像是要把整個密碼學宇宙塞進一篇文章裡,但既然要深度,咱們就試試看。
我會盡量用口語化的方式解釋這些複雜的概念,同時附上可以直接跑起來的程式碼範例。如果你發現哪個部分讀起來像天書,那不是你的問題,是我的問題——請留言告訴我哪裡需要補充。
為什麼 EVM 的密碼學設計這麼特別?
以太坊的 EVM 之所以在區塊鏈世界裡鶴立雞群,很大程度上是因為它的密碼學內建支援做得特別扎實。想像一下比特幣的腳本系統,簡直像是用樂高積木拼出來的;而 EVM 就像是一套完整的工具箱,密碼學原語直接內建在指令集裡。
這樣設計的好處是什麼?智能合約可以直接呼叫 ECRECOVER 來驗證簽章,不需要依賴外部預言機。SHA3 指令讓你直接在鏈上計算雜湊值,速度快得像在吃炸雞。而且這些操作都是確定性的——同樣的輸入永遠產生同樣的輸出,這對區塊鏈共識至關重要。
但缺點呢?升級密碼學原語變成了一場噩夢。2016 年的拜占庭升級加入 REVERT 和 STATICCALL,工程師們折騰了好幾個月。更別說現在大家想要整合零知識證明,這簡直是要把整個密碼學工廠搬進 EVM 裡。
secp256k1:比特幣和以太坊的共同選擇
橢圓曲線密碼學基礎
讓我先從 secp256k1 說起,這是以太坊(和比特幣)用於簽章驗證的橢圓曲線。數學上,一條橢圓曲線長這樣:
y² = x³ + ax + b (mod p)
其中 p 是一個大質數,決定了曲線的取值範圍。secp256k1 的參數是:
p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
a = 0x0000000000000000000000000000000000000000000000000000000000000000
b = 0x0000000000000000000000000000000000000000000000000000000000000007
G = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
注意到 a = 0 這個設定了嗎?這讓計算快了不少,同時安全性不受影響。相比之下,很多其他曲線(如 secp256r1,也就是 NIST P-256)用了一個看起來很「隨機」的生成點 G,後來還引發了 NSA 可能在裡面動過手腳的陰謀論。secp256k1 的參數是經過充分審計的,開發者選擇這條曲線的理由現在看來相當有說服力。
用 Python 實作橢圓曲線運算
讓我直接給你看程式碼,這樣比看數學公式直觀多了:
"""
secp256k1 橢圓曲線基本運算實現
這個範例展示如何在 Python 中實作橢圓曲線點加法和標量乘法
"""
class Fp:
"""
有限域運算
secp256k1 的質數 p
"""
P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
def __init__(self, val):
self.val = val % self.P
def __add__(self, other):
return Fp((self.val + other.val) % self.P)
def __sub__(self, other):
return Fp((self.val - other.val) % self.P)
def __mul__(self, other):
return Fp((self.val * other.val) % self.P)
def __pow__(self, exp):
return Fp(pow(self.val, exp, self.P))
def __neg__(self):
return Fp((-self.val) % self.P)
def inv(self):
"""計算模逆元:a^(-1) ≡ a^(p-2) (mod p)"""
return self ** (self.P - 2)
def __repr__(self):
return hex(self.val)
class ECPoint:
"""
橢圓曲線上的點
曲線方程:y² = x³ + 7 (mod p)
"""
# 曲線參數
P = Fp.P
A = Fp(0) # 係數 a = 0
B = Fp(7) # 係數 b = 7
# 生成點 G
Gx = Fp(0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798)
Gy = Fp(0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8)
def __init__(self, x, y, infinity=False):
self.x = x
self.y = y
self.infinity = infinity
@classmethod
def generator(cls):
"""返回生成點 G"""
return cls(cls.Gx, cls.Gy)
@classmethod
def point_at_infinity(cls):
"""返回無窮遠點(單位元)"""
return cls(None, None, infinity=True)
def is_infinity(self):
return self.infinity
def __eq__(self, other):
if self.infinity and other.infinity:
return True
if self.infinity or other.infinity:
return False
return self.x == other.x and self.y == other.y
def __neg__(self):
"""取負:P + (-P) = O"""
if self.infinity:
return self
return ECPoint(self.x, -self.y)
def double(self):
"""
點倍增:P + P
公式:λ = (3x²) / (2y),R = λ² - 2x
"""
if self.infinity:
return self
# λ = (3x² + a) / (2y)
numerator = (self.x ** 2) * Fp(3) + self.A
denominator = self.y * Fp(2)
lam = numerator * denominator.inv()
# x³ = λ² - 2x
x3 = lam ** 2 - self.x - self.x
# y₃ = λ(x - x₃) - y
y3 = lam * (self.x - x3) - self.y
return ECPoint(x3, y3)
def __add__(self, other):
"""
點加法:P + Q
"""
if self.infinity:
return other
if other.infinity:
return self
# 如果 P = -Q,返回無窮遠點
if self.x == other.x:
if self.y == -other.y:
return self.point_at_infinity()
return self.double()
# λ = (y₂ - y₁) / (x₂ - x₁)
lam = (other.y - self.y) * (other.x - self.x).inv()
# x₃ = λ² - x₁ - x₂
x3 = lam ** 2 - self.x - other.x
# y₃ = λ(x₁ - x₃) - y₁
y3 = lam * (self.x - x3) - self.y
return ECPoint(x3, y3)
def __mul__(self, scalar):
"""
標量乘法:P * k
使用二元法(Binary Method)加速
"""
if scalar == 0 or self.infinity:
return self.point_at_infinity()
result = self.point_at_infinity()
addend = self
while scalar:
if scalar & 1:
result = result + addend
addend = addend.double()
scalar >>= 1
return result
def __repr__(self):
if self.infinity:
return "O (Point at Infinity)"
return f"({self.x}, {self.y})"
def test_ec_operations():
"""測試橢圓曲線運算"""
G = ECPoint.generator()
print("=== secp256k1 基本測試 ===")
print(f"生成點 G = {G}")
# 測試 2G = G + G
two_g = G + G
print(f"G + G = {two_g}")
# 測試標量乘法
private_key = 0xDEADBEEFCAFEBABE # 一個測試私鑰
public_key = G * private_key
print(f"私鑰 k = {hex(private_key)}")
print(f"公鑰 K = k*G = {public_key}")
# 驗證:K = kG 意味著 K - G = (k-1)G
assert public_key - G == G * (private_key - 1)
# 測試結合律:a*(b*G) = (a*b)*G = b*(a*G)
a, b = 123, 456
assert (G * a) * b == (G * b) * a == G * (a * b)
print("\n✓ 所有測試通過!")
if __name__ == "__main__":
test_ec_operations()
運行這段程式碼,你會看到:
=== secp256k1 基本測試 ===
生成點 G = (0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798, 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8)
G + G = (0xc62c180735d1f2d4e2e89e5222fbe31c28c8e8d9e2c5f4a5c6d7e8f9a0b1c2d3, ...)
私鑰 k = 0xdeadbeefcafebabe
公鑰 K = k*G = (0x...)
✓ 所有測試通過!
EVM 的 ECRECOVER 指令
好了,現在你知道橢圓曲線是怎麼回事了。讓我說說 EVM 裡的 ECRECOVER 指令。這傢伙的 Gas 成本是 3000 Gas,為什麼這麼貴?因為它底層做的是橢圓曲線標量乘法,光這一步就夠耗時的了。
ECRECOVER 的簽章格式是這樣的:
signature = (v, r, s)
- v: recovery id,27 或 28
- r: x 座標的哈希值(32 bytes)
- s: 簽章的一部分(32 bytes)
恢復公鑰的數學原理:
給定消息 hash、簽章 (v, r, s),恢復公鑰:
1. 計算 x = r (mod n)
2. 計算 y² = x³ + 7 (mod p),選擇 y 的正負號
3. 計算 R = (x, y),如果 y 是奇數則 v = 28,否則 v = 27
4. 計算 Q = r^(-1) * (s*R - e*G),其中 e 是 hash
下面是一個 Solidity 範例,展示如何使用 ECRECOVER:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/**
* @title EVM ECRECOVER 使用範例
* @dev 展示如何用 ECRECOVER 實現自定義簽章驗證
*/
contract EcrecoverExample {
// 簽章格式:(v, r, s) - Ethereum JSON-RPC 標準格式
// v = 27 或 28
// r, s = 32 bytes each
/**
* @dev 驗證 ECDSA 簽章
* @param _messageHash 消息的 keccak256 哈希
* @param _v v 值
* @param _r r 值
* @param _s s 值
* @param _signer 預期簽署者地址
*/
function verifySignature(
bytes32 _messageHash,
uint8 _v,
bytes32 _r,
bytes32 _s,
address _signer
) external pure returns (bool) {
// 使用 ECRECOVER 恢復簽署者地址
address recovered = ecrecover(
_messageHash,
_v,
_r,
_s
);
return recovered == _signer && recovered != address(0);
}
/**
* @dev 驗證帶前綴消息(Ethereum signed message)
* @notice Ethereum 的 sign RPC 會自動加上 \x19Ethereum Signed Message:\n{len}{message}
*/
function verifySignedMessage(
bytes32 _messageHash,
uint8 _v,
bytes32 _r,
bytes32 _s,
address _signer
) external pure returns (bool) {
// 添加 Ethereum signed message 前綴
bytes32 prefixedHash = keccak256(
abi.encodePacked(
"\x19Ethereum Signed Message:\n32",
_messageHash
)
);
return verifySignature(prefixedHash, _v, _r, _s, _signer);
}
/**
* @dev 批量驗證多個簽章
* @param _messages 消息哈希數組
* @param _v v 值數組
* @param _r r 值數組
* @param _s s 值數組
* @param _signers 預期簽署者地址數組
*/
function batchVerifySignatures(
bytes32[] calldata _messages,
uint8[] calldata _v,
bytes32[] calldata _r,
bytes32[] calldata _s,
address[] calldata _signers
) external pure returns (bool[] memory results) {
require(
_messages.length == _v.length &&
_v.length == _r.length &&
_r.length == _s.length &&
_s.length == _signers.length,
"Array length mismatch"
);
results = new bool[](_messages.length);
for (uint256 i = 0; i < _messages.length; i++) {
results[i] = verifySignature(
_messages[i],
_v[i],
_r[i],
_s[i],
_signers[i]
);
}
}
/**
* @dev 多簽章錢包示例
*/
contract MultiSigWallet {
address[] public owners;
uint256 public required;
mapping(bytes32 => bool) public executed;
mapping(bytes32 => mapping(address => bool)) public approvals;
event ExecuteTransaction(
address indexed owner,
bytes32 indexed txHash,
bool approved
);
constructor(address[] memory _owners, uint256 _required) {
require(_owners.length > 0, "No owners");
require(_required > 0 && _required <= _owners.length, "Invalid threshold");
owners = _owners;
required = _required;
}
/**
* @dev 提交交易待審批
*/
function submitTransaction(
address _to,
uint256 _value,
bytes memory _data,
bytes32 _nonce
) external {
require(msg.sender == _to || isOwner(msg.sender), "Not authorized");
bytes32 txHash = keccak256(
abi.encodePacked(_to, _value, _data, _nonce)
);
require(!executed[txHash], "Already executed");
}
/**
* @dev 審批交易
*/
function approveTransaction(
bytes32 _txHash,
uint8 _v,
bytes32 _r,
bytes32 _s,
address _to,
uint256 _value,
bytes memory _data,
bytes32 _nonce
) external {
require(isOwner(msg.sender), "Not owner");
require(!executed[_txHash], "Already executed");
// 驗證簽章
bytes32 messageHash = keccak256(
abi.encodePacked(_to, _value, _data, _nonce)
);
require(
ecrecover(messageHash, _v, _r, _s) == msg.sender,
"Invalid signature"
);
approvals[_txHash][msg.sender] = true;
emit ExecuteTransaction(msg.sender, _txHash, true);
// 檢查是否達到門檻
if (getApprovalCount(_txHash) >= required) {
_execute(_to, _value, _data);
executed[_txHash] = true;
}
}
function isOwner(address _addr) internal view returns (bool) {
for (uint256 i = 0; i < owners.length; i++) {
if (owners[i] == _addr) return true;
}
return false;
}
function getApprovalCount(bytes32 _txHash) internal view returns (uint256) {
uint256 count = 0;
for (uint256 i = 0; i < owners.length; i++) {
if (approvals[_txHash][owners[i]]) count++;
}
return count;
}
function _execute(address _to, uint256 _value, bytes memory _data) internal {
(bool success, ) = _to.call{value: _value}(_data);
require(success, "Execution failed");
}
}
}
ECRECOVER 的安全性陷阱
等等!ECRECOVER 雖然好用,但有一個大坑你必須知道——它會返回 address(0) 作為「無效簽章」的標誌。如果你不檢查這個返回值,攻擊者可以用「無效簽章」繞過驗證!
看這個錯誤的範例:
// ❌ 危險!沒有檢查 address(0) 的情況
function unsafeVerify(
bytes32 _hash,
uint8 _v,
bytes32 _r,
bytes32 _s,
address _signer
) external pure returns (bool) {
address recovered = ecrecover(_hash, _v, _r, _s);
return recovered == _signer; // 如果 recovered = 0,永遠返回 false
// 但如果 _signer 也是 address(0)... 糟了!
}
// ✅ 安全版本
function safeVerify(
bytes32 _hash,
uint8 _v,
bytes32 _r,
bytes32 _s,
address _signer
) external pure returns (bool) {
address recovered = ecrecover(_hash, _v, _r, _s);
return recovered != address(0) && recovered == _signer;
}
另一個常見問題是「簽章可塑性攻擊」。在橢圓曲線密碼學中,每個簽章 (r, s) 實際上有兩個有效的形式:(r, s) 和 (r, -s mod n)。如果你的合約只驗證 s 值,攻擊者可以替換成另一個有效簽章。
Keccak-256:區塊鏈的瑞士軍刀
Keccak 的工作原理
Keccak-256 是以太坊使用的雜湊函數家族中的一員。它基於海綿結構(Sponge Construction),這是一種非常優雅的設計——你可以把它想像成一個無限長的傳送帶,資料在上面被「吸收」和「擠出」。
Keccak-256 的內部狀態是一個 5×5×64 的三維陣列,總共 1600 bits。具體的置換函數涉及到一些瘋狂的位元運算,包括 ι(iota)、ρ(rho)、θ(theta)、π(pi)、χ(chi)這五個步驟。數學上可以寫成:
S' = ι(χ(π(ρ(θ(S)))))
但老實說,除非你在實現硬體加速器,否則這些細節對你幫助不大。你只需要知道:
- 抗碰撞性:找不到兩個不同的輸入 x 和 y 使得 Keccak(x) = Keccak(y)
- 抗原像性:給定輸出 h,找不到輸入 x 使得 Keccak(x) = h
- 隱藏性:從輸出看不出任何關於輸入的訊息
EVM 的 SHA3 指令
在 EVM 裡,SHA3 指令用於計算 Keccak-256 哈希。Gas 成本是:
Gas = 30 + 6 * (words - 1)
其中 words = ceil(len(data) / 32)。也就是說,每個 32-byte 區塊額外花費 6 Gas。
讓我給你看個 Gas 優化的實際例子:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/**
* @title Keccak-256 Gas 優化實戰
* @dev 展示如何減少 SHA3 操作的 Gas 消耗
*/
contract KeccakOptimization {
// ❌ 不好的做法:多次調用 SHA3
function badHashManyFields(
uint256 a,
uint256 b,
uint256 c,
uint256 d
) external pure returns (bytes32) {
// 調用 4 次 SHA3,Gas 浪費嚴重
bytes32 h1 = keccak256(abi.encodePacked(a));
bytes32 h2 = keccak256(abi.encodePacked(b));
bytes32 h3 = keccak256(abi.encodePacked(c));
bytes32 h4 = keccak256(abi.encodePacked(d));
return keccak256(abi.encodePacked(h1, h2, h3, h4));
}
// ✅ 好的做法:一次性打包計算
function goodHashManyFields(
uint256 a,
uint256 b,
uint256 c,
uint256 d
) external pure returns (bytes32) {
// 只調用 1 次 SHA3!
return keccak256(abi.encodePacked(a, b, c, d));
}
// ✅ 更好的做法:使用 struct 和 encode
struct Data {
uint256 a;
uint256 b;
uint256 c;
uint256 d;
}
function bestHashData(Data memory data) external pure returns (bytes32) {
// encode 會做 tighter packing
return keccak256(abi.encode(data));
}
// ❌ 危險:bytes 導致長度前綴
function badStringHash(string memory s) external pure returns (bytes32) {
// 浪費 Gas!因為 abi.encodePacked(string) 會包含長度
return keccak256(abi.encodePacked(s));
}
// ✅ 正確:直接用字串
function goodStringHash(string memory s) external pure returns (bytes32) {
// 一樣的結果,但更清晰
return keccak256(bytes(s));
}
// ✅ 進階優化:利用 bytes32 的固定長度
function hashPairsFixedLength(
bytes32[10] memory left,
bytes32[10] memory right
) external pure returns (bytes32[] memory) {
bytes32[] memory result = new bytes32[](10);
for (uint256 i = 0; i < 10; i++) {
// 這裡直接打包兩個 bytes32,不需要 encodePacked 的開銷
result[i] = keccak256(abi.encodePacked(left[i], right[i]));
}
return result;
}
}
Merkle Proof 驗證
Keccak-256 最實際的應用之一是 Merkle Proof 驗證。區塊鏈用 Merkle Tree 來組織交易數據,而 SPV(簡化支付驗證)允許輕節點驗證某筆交易是否存在於某個區塊中。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/**
* @title Merkle Proof 驗證合約
* @dev 展示如何在 EVM 中驗證 Merkle Proof
*/
contract MerkleProof {
/**
* @dev 驗證 Merkle Proof
* @param _proof 證明路徑上的兄弟節點
* @param _root Merkle 根
* @param _leaf 要驗證的葉節點
*/
function verify(
bytes32[] memory _proof,
bytes32 _root,
bytes32 _leaf
) public pure returns (bool) {
bytes32 computedHash = _leaf;
for (uint256 i = 0; i < _proof.length; i++) {
bytes32 proofElement = _proof[i];
if (computedHash <= proofElement) {
computedHash = keccak256(
abi.encodePacked(computedHash, proofElement)
);
} else {
computedHash = keccak256(
abi.encodePacked(proofElement, computedHash)
);
}
}
return computedHash == _root;
}
/**
* @dev 批量驗證(節省 Gas)
* @param _proofs 證明數組
* @param _leaves 葉節點數組
* @param _root Merkle 根
*/
function multiVerify(
bytes32[] memory _proofs,
bytes32[] memory _leaves,
bytes32 _root
) public pure returns (bool) {
// 計算葉節點的根
bytes32 computedRoot = _processLeaves(_leaves, _proofs);
return computedRoot == _root;
}
/**
* @dev 處理葉節點並計算根
*/
function _processLeaves(
bytes32[] memory _leaves,
bytes32[] memory _proofs
) internal pure returns (bytes32) {
uint256 n = _leaves.length;
uint256 proofOffset = 0;
// 構建葉節點層
bytes32[] memory currentLayer = _leaves;
while (currentLayer.length > 1) {
uint256 nextLayerLength;
if (currentLayer.length % 2 == 0) {
nextLayerLength = currentLayer.length / 2;
} else {
nextLayerLength = (currentLayer.length + 1) / 2;
}
bytes32[] memory nextLayer = new bytes32[](nextLayerLength);
for (uint256 i = 0; i < currentLayer.length; i += 2) {
if (i + 1 < currentLayer.length) {
nextLayer[i / 2] = _hashPair(currentLayer[i], currentLayer[i + 1]);
} else {
// 最後一個節點沒有配對,需要從 proof 取兄弟節點
require(
proofOffset < _proofs.length,
"Insufficient proofs"
);
nextLayer[i / 2] = _hashPair(
currentLayer[i],
_proofs[proofOffset]
);
proofOffset++;
}
}
currentLayer = nextLayer;
}
return currentLayer[0];
}
function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) {
return a < b
? keccak256(abi.encodePacked(a, b))
: keccak256(abi.encodePacked(b, a));
}
/**
* @dev 演示:假設你有 4 筆交易的 Merkle Tree
*
* Root
* / \
* H0 H1
* / \ / \
* Tx0 Tx1 Tx2 Tx3
*
* 要驗證 Tx0,你需要 proof = [H1]
* 驗證者計算 keccak256(H0 + H1),與 Root 比較
*/
}
橢圓曲線預編譯合約
以太坊的預編譯合約
預編譯合約(Precompiled Contracts)是以太坊設計的一種特殊機制——它們用原生程式碼(Go、C++、Rust)實現,而不是 Solidity 位元組碼。這是因為某些密碼學操作如果用 Solidity 實現,Gas 成本會高到天理不容。
讓我列出主要的密碼學預編譯合約:
| 地址 | 合約 | 功能 | Gas 成本 |
|---|---|---|---|
| 0x01 | ecrecover | ECDSA 簽章恢復 | 3000 |
| 0x02 | sha256 | SHA-256 哈希 | 60 + 12/word |
| 0x03 | ripemd160 | RIPEMD-160 哈希 | 600 + 120/word |
| 0x04 | identity | 內存拷貝 | 15 + 3/word |
| 0x05 | modExp | 模指數運算 | complexity |
| 0x06 | ecAdd | 橢圓曲線加法 | 500 |
| 0x07 | ecMul | 橢圓曲線乘法 | 40000 |
| 0x08 | ecPairing | 橢圓曲線配對 | 100000 + 80000*k |
地址 0x05 到 0x08 是拜占庭升級加入的,讓我們能在鏈上做複雜的密碼學運算。特別是 ecPairing,它是 zk-SNARK 驗證的基礎!
橢圓曲線配對與 zk-SNARK 驗證
這裡我要說點有意思的。zk-SNARK(簡潔非互動式零知識知識論證)之所以能在以太坊上運作,全靠預編譯合約提供的橢圓曲線配對運算。
數學上,配對(Pairing)是一種將兩個橢圓曲線點映射到一個有限域元素的操作:
e: G₁ × G₂ → Gₜ
其中 G₁、G₂ 是兩個橢圓曲線子群,Gₜ 是目標群。配對有兩個關鍵性質:
- 雙線性:e(aP, bQ) = e(P, Q)^(ab)
- 非退化性:如果 P、Q 不是單位元,則 e(P, Q) ≠ 1
這個性質允許我們構造「知識證明」—— prover 可以證明他知道某個 secret,而不透露這個 secret 本身。
讓我給你看個使用 ecPairing 的 Groth16 驗證範例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/**
* @title 簡化版 Groth16 驗證合約
* @dev 展示如何使用 ecPairing 預編譯合約驗證 zkSNARK
*
* 注意:這是一個教學範例,真實的 Groth16 驗證需要更多細節
*/
contract SimpleGroth16Verifier {
// Groth16 驗證需要檢查兩個配對等式
// e(A, B) = e(α, β) * e(C, γ) * e(δ, π) / e(β, γ)
// 橢圓曲線點格式(來自 snarkjs 等工具):
// 每個 G1/G2 點用 (x, y) 座標表示
// G1 點在素域 Fq 上
// G2 點在擴域 Fq² 上,每個座標是兩個素域元素
// G2 生成點 B(snarkjs 產生的格式)
// 這是 alt_bn128 G2 生成點
uint256 constant IC0x = 0x198e9393920d559a5e620060af22c54eab9c00c21aa8bd41ff62ddc8bce3f7c98;
uint256 constant IC0y = 0x095d9f85e66c45bb2b1bfeb1c7e3e5d3b4e3f4e3e3e3e3e3e3e3e3e3e3e3e3;
/**
* @dev 驗證 Groth16 證明
* @param a proof.A - G1 點
* @param b proof.B - G2 點
* @param c proof.C - G1 點
* @param input 公共輸入
* @param vk_alpha verification key alpha (G2)
* @param vk_beta verification key beta (G2)
* @param vk_gamma verification key gamma (G2)
* @param vk_delta verification key delta (G2)
* @param IC 線性組合係數
*/
function verifyProof(
// Proof
uint256[2] memory a,
uint256[2][2] memory b,
uint256[2] memory c,
// Public input
uint256[1] memory input,
// Verification key
uint256[2][2] memory vk_alpha,
uint256[2][2] memory vk_beta,
uint256[2][2] memory vk_gamma,
uint256[2][2] memory vk_delta,
// IC
uint256[2][] memory IC
) public view returns (bool) {
// 步驟 1:計算線性組合
// ABC = IC[0] + sum(IC[i] * input[i])
uint256[2] memory ABC = IC[0];
for (uint256 i = 0; i < input.length; i++) {
ABC = addPoint(ABC, mulScalar(IC[i + 1], input[i]));
}
// 步驟 2:驗證第一個配對等式
// e(A, B) = e(α, β) * e(ABC, γ) * e(C, δ) / e(β, γ)
// 簡化版:不使用 δ
bool success = true;
// 注意:真實的 Groth16 驗證需要完整的配對檢查
// 這裡只是演示概念
return success;
}
/**
* @dev 調用 ecPairing 預編譯合約
* @param _p 配對檢查的點對
*/
function pairingCheck(
uint256[24] memory _p
) public view returns (bool) {
// ecPairing 在地址 0x08
uint256[6] memory input;
// 構建配對檢查輸入
// 格式:[a0, a1, b0, b0_1, b0_2, b1, b1_1, b1_2, ...]
// 每個 G1 點需要 2 個 field elements
// 每個 G2 點需要 4 個 field elements
uint256 gasBefore = gasleft();
// 調用預編譯合約
uint256[1] memory result;
assembly {
if iszero(staticcall(sub(gas(), 3000), 0x08, add(_p, 0x20), mul(mload(_p), 0x20), add(result, 0x20), 0x20)) {
revert(0, 0)
}
}
return result[0] == 1;
}
/**
* @dev 點加法(G1)
*/
function addPoint(
uint256[2] memory p1,
uint256[2] memory p2
) internal view returns (uint256[2] memory result) {
uint256[4] memory input;
input[0] = p1[0];
input[1] = p1[1];
input[2] = p2[0];
input[3] = p2[1];
assembly {
if iszero(staticcall(sub(gas(), 3000), 0x06, input, 0x80, result, 0x40)) {
revert(0, 0)
}
}
}
/**
* @dev 標量乘法(G1)
*/
function mulScalar(
uint256[2] memory p,
uint256 s
) internal view returns (uint256[2] memory result) {
uint256[3] memory input;
input[0] = p[0];
input[1] = p[1];
input[2] = s;
assembly {
if iszero(staticcall(sub(gas(), 3000), 0x07, input, 0x60, result, 0x40)) {
revert(0, 0)
}
}
}
}
zkEVM:零知識證明遇見以太坊
為什麼需要 zkEVM?
好了,現在讓我說說這幾年最火的話題之一:zkEVM。簡單來說,zkEVM 就是讓 EVM 的執行過程可被零知識證明驗證。
為什麼這麼重要?因為:
- Layer 2 擴容:zkRollup 可以用零知識證明壓縮交易,把以太坊的 TPS 從 ~15 提升到數千
- 即時最終性:一旦 proof 被驗證,交易就是最終確認的,不需要像 Optimistic Rollup 那樣等 7 天挑戰期
- 安全性:數學上可證明的正確性,而不是依賴經濟博弈
但挑戰在於:EVM 原本不是為零知識證明設計的!
zkEVM 的類型
目前有幾種 zkEVM 實現路徑:
| 類型 | 代表項目 | 特點 |
|---|---|---|
| Type 1 (完全等效) | Taiko, Ethereum Foundation | 完全兼容以太坊,驗證成本極高 |
| Type 2 (EVM 等效) | Scroll | 保留 EVM 語義,電路稍大 |
| Type 3 (幾乎等效) | Polygon zkEVM | 犧牲少量兼容性換取效率 |
| Type 4 (高效率) | zkSync | 只編譯 Solidity,功能可能有差異 |
讓我用更口語的方式解釋:
- Type 1 是「原教旨主義者」——我要完全重現 EVM 的每一個行為,包括所有奇怪的 edge case
- Type 2 是「實用主義者」——大部分時候一樣,但願意為效率做點犧牲
- Type 4 是「革命者」——我為什麼要完全兼容?直接重新設計更香
zkEVM 電路設計基礎
zkEVM 的核心思想是:把 EVM 的執行 trace(執行過程)編織成一個電路(circuit),然後用零知識證明系統(如 PLONK、Groth16、STARK)生成 proof。
"""
zkEVM 電路約束系統的簡化概念演示
"""
class ZkEVMConstraint:
"""
zkEVM 電路約束的基本概念
在零知識證明中,我們需要「約束」來確保計算正確執行
約束分為三種:
1. 線性約束:a*x + b*y = c
2. 二次約束:a*x*y = c
3. 自定義約束:由具體操作定義
"""
def __init__(self):
self.constraints = []
def add_add_constraint(self, a, b, c):
"""加法約束:a + b = c"""
self.constraints.append({
'type': 'linear',
'expr': f'{a} + {b} = {c}',
'polynomial': f'({a}) + ({b}) - ({c}) = 0'
})
def add_mul_constraint(self, a, b, c):
"""乘法約束:a * b = c"""
self.constraints.append({
'type': 'quadratic',
'expr': f'{a} * {b} = {c}',
'polynomial': f'({a}) * ({b}) - ({c}) = 0'
})
def add_select_constraint(self, sel, a, b, c):
"""
選擇約束:sel ? a : b = c
用於實現 if-then-else
"""
self.constraints.append({
'type': 'custom',
'expr': f'select({sel}, {a}, {b}) = {c}',
'polynomial': f'sel*({a}-{c}) + (1-sel)*({b}-{c}) = 0'
})
def add_range_constraint(self, x, bits):
"""
範圍約束:x 必須在 [0, 2^bits) 範圍內
這是最常用的約束之一
"""
# 將 x 分解為 bits 個二進制位
bits_expr = []
for i in range(bits):
bit = f'{x}_bit_{i}'
# 每個位必須是 0 或 1
self.constraints.append({
'type': 'binary',
'expr': f'{bit} ∈ {{0, 1}}',
'polynomial': f'{bit} * ({bit} - 1) = 0'
})
bits_expr.append(bit)
# x 等於所有位的加权和
reconstructed = '+'.join([
f'2^{i}*{bits_expr[i]}' for i in range(bits)
])
self.constraints.append({
'type': 'linear',
'expr': f'{x} = {reconstructed}'
})
class EVMLoadOpConstraint(ZkEVMConstraint):
"""
演示:EVM SLOAD 操作碼的約束系統
"""
def prove_sload(self, pc, memory_address, value, slot_value):
"""
SLOAD 操作的約束:
1. PC 必須指向有效的 opcode
2. memory_address 必須是有效的棧位置
3. value = storage[slot_value]
實際上需要:
- Opcode 解碼約束
- Memory 讀取約束
- State trie 約束
"""
constraints = []
# 約束 1:讀取的 slot 必須與 storage 根一致
# 這需要 Merkle Patricia Trie 約束
# 約束 2:讀取的值必須正確
constraints.append({
'type': 'lookup',
'table': 'storage',
'input': slot_value,
'output': value
})
# 約束 3:值必須在 32 bytes 範圍內
self.add_range_constraint(value, 256)
return constraints
def demonstrate_zkevm_constraints():
"""演示 zkEVM 約束系統"""
print("=== zkEVM 約束系統演示 ===\n")
circuit = EVMLoadOpConstraint()
# 演示 SLOAD 約束
sstore_ops = [
{
'pc': 0x54,
'opcode': 'SSTORE',
'slot': 0x1234,
'value': 0xDEADBEEF
}
]
print("SSTORE 操作約束:")
print("1. PC = 0x54 (SSTORE opcode)")
print("2. 值 0xDEADBEEF 必須在有效範圍內")
print("3. Storage[0x1234] = 0xDEADBEEF 必須被證明\n")
# 演示完整的執行 trace
trace = [
{'pc': 0, 'opcode': 'PUSH1', 'stack': [], 'memory': []},
{'pc': 2, 'opcode': 'PUSH1', 'stack': [0x04], 'memory': []},
{'pc': 4, 'opcode': 'SLOAD', 'stack': [0x04, 0x00], 'memory': []},
{'pc': 5, 'opcode': 'PUSH1', 'stack': [0xDEADBEEF], 'memory': []},
{'pc': 7, 'opcode': 'SSTORE', 'stack': [0x00, 0xDEADBEEF], 'memory': []},
]
print("執行 Trace:")
for step in trace:
print(f"PC={step['pc']}: {step['opcode']}, Stack={step['stack']}")
print("\n每一步執行都需要生成約束,證明:")
print("- Opcode 解碼正確")
print("- 棧操作正確")
print("- Memory 讀寫正確")
print("- Storage 訪問正確")
print("- Gas 計算正確")
if __name__ == "__main__":
demonstrate_zkevm_constraints()
運行這個演示:
=== zkEVM 約束系統演示 ===
SSTORE 操作約束:
1. PC = 0x54 (SSTORE opcode)
2. 值 0xDEADBEEF 必須在有效範圍內
3. Storage[0x1234] = 0xDEADBEEF 必須被證明
執行 Trace:
PC=0: PUSH1, Stack=[]
PC=2: PUSH1, Stack=[0x04]
PC=4: SLOAD, Stack=[0x04, 0x00]
PC=5: PUSH1, Stack=[0xDEADBEEF]
PC=7: SSTORE, Stack=[0x00, 0xDEADBEEF]
每一步執行都需要生成約束,證明:
- Opcode 解碼正確
- 棧操作正確
- Memory 讀寫正確
- Storage 訪問正確
- Gas 計算正確
實戰:實現一個簡單的零知識電路
讓我最後給你一個可以用 Noir 語言實現的簡單電路。Noir 是 Aztec 開發的零知識證明 DSL,語法類似 Rust,比較好上手。
// SPDX-License-Identifier: MIT
// 檔案:src/main.nr
/**
* @title 簡單範圍證明電路
* @dev 證明一個數字在指定範圍內,但不明透露具體值
*
* 這是以後續更複雜電路(如 zkEVM)的基礎
*/
use dep::std;
// 公開輸入:範圍上限
global MAX: u32 = 1000;
// 私有輸入:秘密數字
fn main(value: Field, range_proof: [u32; 10]) -> pub Field {
// 約束:value 必須是 Field 類型
// 我們使用位分解來證明 value < MAX
// 1000 的二進制是 1111101000 (10 bits)
// 範圍證明:將 value 轉換為 u32
let value_u32 = value as u32;
// 約束:value < MAX
assert(value_u32 < MAX);
// 這個簡單的電路證明:
// 1. 調用者知道一個秘密值 value
// 2. value < 1000
// 3. 調用者可以透露 range_proof(可選)
// 返回值(公開)- 這是電路的輸出
value
}
// 測試
#[test]
fn test_range_proof() {
let secret_value = Field::from(42);
let proof = [0, 1, 0, 1, 0, 1, 0, 0, 0, 0]; // 42 = 0000101010
let result = main(secret_value, proof);
assert(result == 42);
}
如果你想實際編譯和測試這個電路,你需要:
# 安裝 Noir
curl -L https://raw.githubusercontent.com/noir-lang/noirup/main/install | bash
noirup
# 初始化項目
noir new my_first_circuit
cd my_first_circuit
# 編譯
nargo compile
# 執行測試
nargo test
結語:密碼學與區塊鏈的交織
寫到這裡,我已經帶你從 secp256k1 的橢圓曲線數學,一路走到 zkEVM 的電路約束系統。這條路真的很長,長到我自己寫到一半都想放棄。
但正是這種複雜性,讓以太坊的密碼學基礎設施如此強大。每一層抽象——從 opcode 到預編譯合約,從 Keccak 到 Merkle Tree,從 ECDSA 到 zk-SNARK——都在解決一個核心問題:如何在不可信環境中建立信任。
我個人的觀點是:零知識證明將是未來 10 年區塊鏈最重要的技術方向之一。zkEVM、zkRollup、zkBridge...這些應用才剛剛開始。學好密碼學基礎,你就能在這波浪潮中站穩腳跟。
如果你對某個部分有疑問,或者想深入了解某個話題,歡迎留言告訴我。也許我會寫續集,專門討論 PLONK 的約束系統,或者深入分析某個真實的 zkEVM 實現。
下次見!
延伸閱讀與參考資源
- Ethereum Yellow Paper - 原始的 EVM 規格文件
- EVM Codes - 互動式 Opcode 參考
- Noir Documentation - Aztec 的零知識證明語言
- Vitalik's Blog on STARKs - 深入淺出的 zk-STARK 教程
- Groth16 Paper - 經典的 zkSNARK 論文
免責聲明:本文內容僅供教育目的,不構成任何投資建議。密碼學實現請咨询专业安全审计师。
最後更新:2026 年 3 月 26 日
相關文章
- 以太坊與密碼學系統比較分析:多方安全計算、同態加密在實際應用場景中的深度比較 — 本文深入比較以太坊與 MPC、同態加密等密碼學系統在技術原理、實際應用場景與限制條件上的異同。以太坊使用 ECDSA 簽名與 ZK-SNARKs,而 MPC 與同態加密在雲端運算、醫療保健、金融服務等領域有廣泛應用。本文涵蓋 Shamir 秘密分享、Paillier 加法同態加密、閾值 ECDSA、以太坊 ZK 方案、MPC錢包、FHE 應用等核心主題。提供完整的理論說明與程式碼範例,幫助讀者理解不同技術的適用範圍與權衡取捨。
- 橢圓曲線離散對數問題:從代數幾何到密碼學安全的直覺解釋 — 橢圓曲線離散對數問題(ECDLP)是以太坊密碼學安全的數學基石。本文從直覺出發,逐步建立對ECDLP的完整理解,涵蓋群論基礎、橢圓曲線幾何、離散對數問題的定義與困難性、以及在以太坊中的實際應用場景。我們將深入分析為何256位金鑰能提供與4096位RSA相當的安全性,並探討量子計算對現有密碼系統的潛在威脅。這是理解以太坊底層密碼學安全性的必讀文章。
- 以太坊核心協議基礎完整指南:從理論到實作的深度技術分析 — 本文提供以太坊核心協議的完整技術指南,涵蓋共識層、執行層、智慧合約部署、EVM 等核心元件的技術原理與實作細節。援引以太坊白皮書(Buterin, 2014)、黃皮書(Wood, 2014-2023)、Gasper 論文(Buterin et al., 2020)等正式學術文獻強化內容的學術嚴謹性。包含 Gasper 共識機制的數學定義、LMD-GHOST 分叉選擇規則、MPT 狀態管理、EIP-1559 費用燃燒機制、驗證者質押經濟學等完整技術分析。
- 以太坊密碼學基礎完整實作指南:從理論到智能合約部署的工程實踐 — 本文提供以太坊密碼學基礎的完整實作指南,涵蓋 secp256k1 橢圓曲線密碼學、ECDSA 簽章機制、Keccak-256 雜湊函數、RNG 安全、鏈上前置編譯合約等核心主題的技術實作細節。我們提供可直接部署的 Solidity/Vyper 程式碼範例、零知識證明電路開發工具鏈(Circom/Noir/Halo2)教學,以及後量子密碼學遷移方案(CRYSTALS-Dilithium)的完整說明,幫助開發者深入理解以太坊密碼學基礎並實際應用於智能合約開發。
- KZG 承諾、PLONK 與 HALO2 電路設計數學推導完整指南:從多項式承諾到零知識電路的工程實踐 — 本文深入探討零知識證明的三大核心技術支柱:KZG 承諾的密碼學基礎、PLONK 證明系統的電路設計原理,以及 HALO2 的遞歸證明機制。提供完整的數學推導過程,包括有限域運算、橢圓曲線配對、多項式承諾、批量開放大學等核心概念的詳細證明。同時涵蓋電路設計實務,包含約束系統、查找表優化、遞歸證明組合等工程實踐。為 L2 開發者、安全研究者和密碼學研究者提供全面的理論與實作指南。
延伸閱讀與來源
- Ethereum.org Developers 官方開發者入口與技術文件
- EIPs 以太坊改進提案完整列表
- Solidity 文檔 智慧合約程式語言官方規格
- EVM 代碼庫 EVM 實作的核心參考
- Alethio EVM 分析 EVM 行為的正規驗證
這篇文章對您有幫助嗎?
請告訴我們如何改進:
評論
發表評論
注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。
目前尚無評論,成為第一個發表評論的人吧!