以太坊 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 年的拜占庭升級加入 REVERTSTATICCALL,工程師們折騰了好幾個月。更別說現在大家想要整合零知識證明,這簡直是要把整個密碼學工廠搬進 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)))))

但老實說,除非你在實現硬體加速器,否則這些細節對你幫助不大。你只需要知道:

  1. 抗碰撞性:找不到兩個不同的輸入 x 和 y 使得 Keccak(x) = Keccak(y)
  2. 抗原像性:給定輸出 h,找不到輸入 x 使得 Keccak(x) = h
  3. 隱藏性:從輸出看不出任何關於輸入的訊息

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 成本
0x01ecrecoverECDSA 簽章恢復3000
0x02sha256SHA-256 哈希60 + 12/word
0x03ripemd160RIPEMD-160 哈希600 + 120/word
0x04identity內存拷貝15 + 3/word
0x05modExp模指數運算complexity
0x06ecAdd橢圓曲線加法500
0x07ecMul橢圓曲線乘法40000
0x08ecPairing橢圓曲線配對100000 + 80000*k

地址 0x05 到 0x08 是拜占庭升級加入的,讓我們能在鏈上做複雜的密碼學運算。特別是 ecPairing,它是 zk-SNARK 驗證的基礎!

橢圓曲線配對與 zk-SNARK 驗證

這裡我要說點有意思的。zk-SNARK(簡潔非互動式零知識知識論證)之所以能在以太坊上運作,全靠預編譯合約提供的橢圓曲線配對運算。

數學上,配對(Pairing)是一種將兩個橢圓曲線點映射到一個有限域元素的操作:

e: G₁ × G₂ → Gₜ

其中 G₁、G₂ 是兩個橢圓曲線子群,Gₜ 是目標群。配對有兩個關鍵性質:

  1. 雙線性:e(aP, bQ) = e(P, Q)^(ab)
  2. 非退化性:如果 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 的執行過程可被零知識證明驗證。

為什麼這麼重要?因為:

  1. Layer 2 擴容:zkRollup 可以用零知識證明壓縮交易,把以太坊的 TPS 從 ~15 提升到數千
  2. 即時最終性:一旦 proof 被驗證,交易就是最終確認的,不需要像 Optimistic Rollup 那樣等 7 天挑戰期
  3. 安全性:數學上可證明的正確性,而不是依賴經濟博弈

但挑戰在於:EVM 原本不是為零知識證明設計的!

zkEVM 的類型

目前有幾種 zkEVM 實現路徑:

類型代表項目特點
Type 1 (完全等效)Taiko, Ethereum Foundation完全兼容以太坊,驗證成本極高
Type 2 (EVM 等效)Scroll保留 EVM 語義,電路稍大
Type 3 (幾乎等效)Polygon zkEVM犧牲少量兼容性換取效率
Type 4 (高效率)zkSync只編譯 Solidity,功能可能有差異

讓我用更口語的方式解釋:

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 實現。

下次見!


延伸閱讀與參考資源

免責聲明:本文內容僅供教育目的,不構成任何投資建議。密碼學實現請咨询专业安全审计师。

最後更新:2026 年 3 月 26 日

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

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

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