來源:登鏈社區
為工作程式設計師提供的ZKP 教程介紹。
你知道為什麼斑馬有條紋嗎?一種理論是這是一種偽裝。當斑馬聚集在一起時,這使得獅子更難以區分它們的獵物。獅子必須將獵物從群體中隔離才能追捕它[^1]。
人類也喜歡在人群中隱藏。一個具體的例子是,當多個人在一個集體名稱下作為一個整體行動。 《聯邦黨人文集》就是這樣創作的[^2]。 另一個例子是Bourbaki,這是1930 年代一群法國數學家的集體筆名。這導致了現代數學大部分內容的徹底重寫,重點在於嚴謹性和公理化方法[^3]。
Bourbaki Congress
在數位時代,假設你在一個群組聊天中,想要發送一個有爭議的訊息。你想證明你是其中的一員,而不透露是哪一位。我們如何在數位領域使用密碼學來做到這一點?我們可以使用一種叫做群組簽名的東西。
從傳統上講,群簽名在數學上相當複雜且難以實現。然而,使用零知識證明(ZKP),這個數學問題變成了一個簡單的程式設計任務。在本文結束時,你將能夠自己編寫群組簽名。
介紹
這篇文章將向你展示如何從零開始寫基本的零知識證明(ZKP)。
在學習新的技術堆疊時,我們希望盡快掌握編輯-建置-運行的循環。只有這樣,我們才能開始從自己的經驗中學習。
我們將首先讓你設定環境,編寫一個簡單的程序,執行所謂的可信任設置,然後儘快產生和驗證證明。之後,我們將識別一些改進我們程序的方法,實施這些改進並進行測試。在此過程中,我們將建立一個更好的心理模型,以便在實踐中編程ZKP。最後,你將熟悉(某種方式)從零開始寫ZKP。
我們將逐步建立一個簡單的簽名方案,你可以證明你發送了特定的訊息。你將能夠理解這段程式碼的作用及其原因:
# 複製倉庫並執行準備腳本
git clone git@github.com:oskarth/zkintro-tutorial.git
cd zkintro-tutorial
# 在執行之前瀏覽此文件的內容
less ./scripts/prepare.sh
./scripts/prepare.sh
我們建議你瀏覽./scripts/prepare.sh 的內容,以查看這將安裝什麼,或者如果你更喜歡手動安裝。執行後,你應該會看到Installation complete 並且沒有錯誤。
如果你遇到問題,請查看最新的官方文件這裡[7]。完成後,你應該安裝以下版本(或更高版本):
pragma circom 2.0.0;
template Multiplier2 () {
signal input a;
signal input b;
signal output c;
c <== a * b;
}
component main = Multiplier2();
這就是我們的特殊程式或_電路_。 [^6] 按行分析:
-
pragma circom 2.0.0; – 定義所使用的Circom 版本
-
template Multiplier() – 模板是大多數程式語言中物件的等價物,是一種常見的抽象形式
-
signal input a; – 我們的第一個輸入,a;輸入預設是私有的
-
signal input b; – 我們的第二個輸入,b;同樣預設是私有的
-
signal output b; – 我們的輸出,c;輸出總是公共的
-
c <== a * b; - 這做了兩件事:將訊號c 賦值並約束c 等於a 和b 的乘積
-
component main = Multiplier2() – 實例化我們的主元件
最重要的行是c <== a * b;。這是我們實際聲明約束的地方。這個表達式其實是兩個的組合:<--(賦值)和===(等式限制)。 [^7] Circom 中的約束只能使用涉及常數、加法或乘法的運算。它強制要求方程式的兩邊必須相等。 [^8]
關於約束
約束是如何運作的?在類似數獨的上下文中,我們可能會說一個限制是「一個介於1 和9 之間的數字」。然而,在Circom 的上下文中,這不是一個單一的約束,而是我們必須使用一組更簡單的等式約束(===)來表達的東西。 [^9]
為什麼會這樣呢?這與底層的數學原理有關。從根本上講,大多數ZKP 使用_算術電路_,它表示對多項式的計算。在處理多項式時,你可以輕鬆引入常數,將它們相加、相乘並檢查它們是否相等。 [^10] 其他操作必須用這些基本操作來表達。你不必詳細了解這一點才能編寫ZKP,但了解底層發生的事情可能會很有用。 [^11]
我們可以將電路視覺化如下:
建構我們的電路
供你參考,最終文件可以在example1-solution.circom 中找到。有關語法的更多詳細信息,請參見官方文檔[9]。
我們可以透過執行以下命令來編譯我們的電路:
這是呼叫circom 建立example1.r1cs 和example1.wasm 檔案的一個簡單包裝。你應該會看到類似以下內容:
{
“pi_a”: [“15932[…]3948”, “66284[…]7222”, “1”],
“pi_b”: [
[“17667[…]0525”, “13094[…]1600”],
[“12020[…]5738”, “10182[…]7650”],
[“1”, “0”]
],
“pi_c”: [“18501[…]3969”, “13175[…]3552”, “1”],
“protocol”: “groth16”,
“curve”: “bn128”
}
這以一些數學物件(三個橢圓曲線元素)pi_a、pi_b 和pi_c 的形式指定了證明。[^20] 它還包括有關協議(groth16)和使用的_curve_(bn128,我們暫時忽略的數學實作細節)的元資料。這使得驗證者知道如何處理此證明以正確驗證。
請注意,證明是多麼簡短;無論我們的特殊程序多麼複雜,它的大小都只有這個。這展示了我們在_友善的零知識證明介紹_[10] 中討論的ZKP 的succinctness 屬性。上述命令也輸出了我們的_公共輸出_:
這是與我們的見證和電路對應的所有公共輸出的清單。在這種情況下,有一個公共輸出對應於c:33。[^21]
我們證明了什麼?我們知道兩個秘密值a 和b,它們的乘積是33。這展示了我們在上一篇文章中討論的隱私屬性。
請注意,證明在孤立狀態下沒有用,它需要隨之而來的公共輸出。
驗證證明
接下來,讓我們驗證這個證明。運行:
just verify_proof example1
這需要驗證金鑰、公共輸出和證明。透過這些,我們能夠驗證證明。它應該會列印“證明已驗證”。請注意,驗證者從未接觸到任何私有輸入。
如果我們更改輸出會發生什麼?打開example1/target/public.json,將33 更改為34,然後再次執行上述命令。
你會注意到證明不再被驗證。這是因為我們的證明並沒有證明我們有兩個數字,其乘積是34。
恭喜你,你現在已經編寫了你的第一個ZKP 程序,進行了可信設置,生成了證明並最終驗證了它!
練習
-
ZKP 的兩個關鍵屬性是什麼,它們意味著什麼?
-
證明者的角色是什麼,她需要什麼輸入?驗證者呢?
-
解釋c <== a * b; 這行的作用。
-
為什麼我們需要進行可信任設定?我們如何使用其產物?
-
程式碼:完成example1,直到你產生並驗證了一個證明。
第二次迭代
透過上述電路,我們證明了我們知道兩個(秘密)數字的乘積。這與質因數分解問題密切相關,這是許多密碼學的基礎。[^22] 這個想法是,如果你有一個非常大的數字,要找到兩個質數使其乘積等於這個大數字是很困難的。相反,檢查兩個數字的乘積是否等於另一個數字是非常簡單的。[^23]
然而,我們的電路存在一個大問題。你能看到嗎?
我們可以輕鬆地將輸入更改為“1”和“33”。也就是說,一個數字c 總是1 和c 的乘積。這一點並不令人印象深刻,對吧?
我們想要做的是再增加一個_約束_,使得a 或b 不能等於1。這樣,我們就被迫進行適當的整數因式分解。
我們如何加入這個約束,需要做哪些改變?
更新我們的電路
我們將為這些變更使用example2 資料夾。不幸的是,我們不能只是寫a !== 1,因為這不是有效的限制。[^24] 它不是由常數、加法、乘法和等式檢查組成的。我們如何表達「某物不是」?
這並不是立即直觀的,這種類型的問題是編寫電路的藝術所在。發展這種技能需要時間,並超出了本初始教程的範圍;幸運的是,有許多好的資源可以參考。[^25]
不過,有一些常見的慣用語。基本的想法是使用IsZero() 模板來檢查一個表達式是否等於零。它對真值輸出1,對假值輸出0。
使用真值表[^26] 來顯示可能的值通常是有幫助的。以下是IsZero() 的真值表:
這是一個如此有用的構建塊,以至於它被包含在Circom 的庫circomlib 中。在circomlib 中還有許多其他有用的元件。[^27]
我們可以透過建立一個npm 專案(JavaScript)並將其作為依賴項新增來包含它。在example2 資料夾中,我們已經為你完成了這一步。若要匯入相關模組,我們在example2.circom 的頂部新增以下行:
include “circomlib/circuits/comparators.circom”;
使用IsZero(),我們可以檢查a 或b 是否等於1。修改example2.circom 文件,使其包含以下行:
just generate_proof example2
just verify_proof example2
它仍然按預期生成和驗證證明。
如果我們將example2/input.json 的輸入更改為1 和33 並嘗試執行上述命令,我們將看到一個斷言錯誤。也就是說,Circom 甚至不會讓我們產生證明,因為輸入違反了我們的約束。
完整流程圖
現在我們已經經歷了整個流程兩次,讓我們退後一步,看看所有部分是如何結合在一起的。
希望事情開始變得有點明朗。接下來,讓我們提升一下,讓我們的電路更有用。
練習
-
為什麼我們必須執行example2 的第2 階段,而不是第1 階段?
-
上一個例子的主要問題是什麼,我們是如何解決的?
-
程式碼:完成example2,直到你無法產生證明。
第三次迭代
透過上述電路,我們已經證明了我們知道兩個秘密值的乘積。單靠這一點並不是很有用。在現實世界中,有用的是_數位簽章方案_。透過它,你可以向其他人證明你寫了特定的訊息。我們如何使用ZKP 來實現這一點?要實現這一點,我們必須先涵蓋一些基本概念。
現在是短暫休息的好時機,去喝一杯你最喜歡的飲料。
數位簽名
數位簽名已經存在,並且在我們的數位時代無處不在。現代網路沒有它們是無法運作的。通常,這些是使用公鑰密碼學實現的。在公鑰密碼學中,你有一個私鑰和一個公鑰。私鑰僅供你自己使用,而公鑰則是公開分享的,代表你的身分。
數位簽章方案由以下部分組成:
-
金鑰產生:產生一個私鑰和對應的公鑰
-
簽名:使用私鑰和訊息建立簽名
-
簽名驗證:驗證訊息是否由對應的公鑰簽名
雖然具體細節看起來不同,但我們寫的程式和上述金鑰產生演算法共享一個共同元素:它們都使用_單向函數_,更具體地說是_陷門函數_。陷門是容易掉進去但難以爬出來的東西(除非你能找到一把隱藏的梯子) [^30]。
對於公鑰密碼學,從私鑰建構公鑰是容易的,但反過來卻非常困難。我們的前一個程式也是如此。如果這兩個秘密數字是非常大的質數,那麼將該乘積轉回原始值是非常困難的。現代公鑰密碼學通常在底層使用_橢圓曲線密碼學_。
傳統上,創建像這些數位簽名方案這樣的密碼協議需要大量的工作,並需要提出一個涉及一些巧妙數學的特定協議。我們不想這樣做。相反,我們想使用ZKP 編寫一個程序,以實現相同的結果。
而不是這樣:[^31]
我們只想寫一個程序,產生我們想要的證明,然後驗證這個證明。
哈希函數和承諾
我們將使用兩個更簡單的工具:_雜湊函數_ 和_承諾_,而不是使用橢圓曲線密碼學。
哈希函數也是一種單向函數。例如,在命令列中,我們可以這樣使用SHA-256 雜湊函數:
commitment = hash(some_secret)
signature = hash(some_secret, message)
此時你可能有一些問題。讓我們解決一些你腦中可能存在的問題。
首先,為什麼這有效,我們為什麼需要ZKP?當有人驗證證明時,他們只能存取承諾、訊息和簽名。沒有直接的方法可以驗證承諾是否對應於秘密,而不揭示秘密。在這種情況下,我們只是在生成證明時「揭示」秘密,因此我們的秘密保持安全。
其次,為什麼在ZKP 內部使用這些雜湊函數和承諾,而不是公鑰密碼學?你絕對可以在ZKP 內部使用公鑰密碼學,而且這樣做是有合理理由的。就約束而言,它的實現成本遠高於上述方案。這使得它比上述更慢,更複雜。正如我們將在下一節中看到的,雜湊函數的選擇非常重要。
最後,為什麼在我們已經擁有公鑰密碼學的情況下還要使用ZKP?在這個簡單的例子中,沒有必要使用ZKP。然而,它作為更有趣的應用的構建塊,例如本文開頭提到的群簽名示例。畢竟,我們想要_程式密碼學_。
這真是太多了!幸運的是,我們已經過了難關。讓我們開始編碼吧。如果你一開始沒有完全理解上述內容,也不用擔心。習慣這種推理方式需要一些時間。
回到程式碼
我們將從example3 目錄開始工作。
要實現數位簽名,我們需要做的第一件事是產生我們的金鑰。這些對應於公鑰密碼學中的私鑰和公鑰。由於金鑰對應於一個身分(你,證明者),我們將分別稱為identity_secret 和identity_commitment。它們共同形成一個身分對。
這些將作為電路的輸入,與我們要簽名的訊息一起使用。作為公共輸出,我們將擁有簽名、承諾和訊息。這將允許某人驗證簽名確實是正確的。
由於我們需要身份對作為電路的輸入,因此我們將單獨產生這些:just generate_identity
這會產生類似以下內容:
include “circomlib/circuits/poseidon.circom”;
Poseidon 哈希模板的使用如下:
component main {public [identity_commitment, message]} = SignMessage();
預設情況下,我們電路的所有輸入都是私有的。透過這個,我們明確標記identity_commitment 和message 為公共。這意味著它們將成為公共輸出的一部分。
有了這些訊息,你應該有足夠的知識來完成example3.circom 電路。如果你仍然卡住,可以參考example3-solution.circom 來取得完整程式碼。
像之前一樣,我們必須建立電路並運行受信任設定的第2 階段:
{
“identity_secret”: “21879[…]1709”,
“identity_commitment”: “48269[…]7915”,
“message”: “42”
}
隨意將身分對改為自己使用just generate_identity 產生的身分對。畢竟,你想把身分秘密保留給自己!
你可能會注意到訊息只是一個作為字串引用的數字(“42”)。不幸的是,由於約束在數學上的工作方式(使用線性代數和_算術電路_),我們只能使用數字而不能使用字串。電路內部支援的唯一操作是基本的算術操作,如加法和乘法。[^37]
我們現在可以產生和驗證一個證明:
[“48968[…]5499”, “48269[…]7915”, “42”]
這分別對應於簽名、承諾和訊息。
讓我們看看如果我們不小心,事情可能會出錯。 [^38]
首先,如果我們將身分承諾更改為input.json 中的隨機內容,會發生什麼事?你會注意到我們無法再產生證明。這是因為我們還在電路內部檢查身份承諾。維持身分秘密和承諾之間的關係至關重要。
其次,如果我們不將訊息包含在輸出中,會發生什麼事?我們確實得到了一個證明,並且它得到了驗證。但訊息可以是_任何東西_,因此它實際上並不能證明你發送了特定的訊息。類似地,如果我們不將身分承諾包含在公共輸出中,會發生什麼事?這意味著身份承諾可以是任何東西,因此我們實際上不知道誰簽署了訊息。
作為思考練習,想想如果我們省略這兩個關鍵約束中的任何一個會發生什麼:
-
identity_commitment === identityHasher.out
-
signature <== signatureHasher.out
恭喜你,現在你知道如何程式加密了![^39]
練習
-
數位簽章方案的三個組成部分是什麼?
-
使用像Poseidon 這樣的”ZK-Friendly hash function” 的目的是什麼?
-
什麼是承諾?我們如何將它們用於數位簽章方案?
-
為什麼我們將身份承諾和訊息標記為公共?
-
為什麼我們需要身分承諾和簽名約束?
-
程式碼:完成example3,直到你產生並驗證了一個證明。
下一步
透過上述數位簽章方案,以及我們在文章中看到的一些技巧,你擁有了實現文章開頭提到的群組簽章方案的所有工具。[^40]
在example4 中存在骨架程式碼。你只需要5-10 行程式碼。唯一的新語法是for 循環,它的工作方式與大多數其他語言相同。[^41]。
這個電路將允許你:
-
簽署一則訊息
-
證明你是三個人之一(身分承諾)
-
但不透露是哪一個
你可以把它看作一個謎題。關鍵的見解基本上歸結為一個算術表達式。如果可以的話,請嘗試在紙上解決它。如果你卡住了,可以像之前一樣查看解決方案。
最後,如果你想要一些額外的挑戰,這裡有一些擴展的方法:
-
允許組內任意多的人
-
實作一個新的電路reveal,證明你簽署了特定的訊息
-
實作一個新的電路deny,證明你沒有簽署特定的訊息
使用經典工具創建這樣的加密協定將是一項巨大的任務,需要大量的專業知識。[^42] 使用ZKP,你可以在一個下午變得有效率和危險,將這些問題視為程式設計任務。這只是我們可以做的冰山一角。
練習
-
群組簽名與普通簽名有什麼不同?它們可以如何使用?
問題
這些問題是可選的,需要更多的努力。
-
找出IsZero() 是如何實現的。
-
程式碼:完成上述群組簽名方案(見example4)。
-
程式碼:擴展上述群組簽名範例:允許更多人並實作reveal 和/或deny 電路。
-
你將如何設計一個”ZK 身分” 系統來證明你已滿18 歲?你可能想證明的其他屬性是什麼?從高層次來看,你將如何實現它,以及你看到的挑戰是什麼?研究現有解決方案以更好地理解它們是如何實現的。
-
對於像以太坊這樣的公共區塊鏈,有時會使用Layer 2 (L2) 來允許更快、更便宜和更多的交易。從高層次來看,你將如何使用ZKP 設計一個L2?解釋你看到的一些挑戰。研究現有解決方案以更好地理解它們是如何實現的。 ## 結論
在本教學介紹中,我們熟悉如何從頭開始撰寫和修改基本的零知識證明(ZKPs)。我們設定了程式設計環境並編寫了一個基本電路。然後我們進行了可信設置,創建並驗證了證明。我們識別了一些問題並改進了電路,確保測試我們的變更。之後,我們使用雜湊函數和承諾實作了一個基本的數位簽章方案。
我們也學習了足夠的技能和工具,以便能夠實現群體簽名,這在沒有零知識證明的情況下是很難實現的。
我希望你對編寫零知識證明所涉及的內容有了更好的心理模型,並對實際中的編輯-運行-調試週期有了更好的理解。這將為你將來可能編寫的任何其他零知識證明程式打下良好的基礎,無論你最終使用什麼技術堆疊。
致謝
感謝Hanno Cornelius、Marc Köhlbrugge、Michelle Lai、lenilsonjr 和Chih-Cheng Liang 閱讀草稿並提供回饋。
圖片
-
Bourbaki Congress 1938 – 未知,公有領域,透過Wikimedia[11]
-
Hartmann’s Zebras – J. Huber,CC BY-SA 2.0,透過Wikimedia[12]
-
Trapdoor Spider – PS Foresman,公有領域,透過 [Wikimedia](https://commons.wikimedia.org/wiki/File:Trapdoor_(PSF\ “Wikimedia”).png)
-
Kingsley Lockbox – PS Foresman,公有領域,透過Wikimedia[13]
參考資料
[1] AI翻譯官: https://learnblockchain.cn/people/19584
[2] 翻譯小組: https://learnblockchain.cn/people/412
[3] learnblockchain.cn/article…: https://learnblockchain.cn/article/9178
[4] 零知識的友善介紹: https://learnblockchain.cn/article/6184
[5] git 倉庫: https://github.com/oskarth/zkintro-tutorial
[6]git 倉庫: https://github.com/oskarth/zkintro-tutorial
[7]這裡: https://docs.circom.io/getting-started/installation/
[8]zkrepl.dev: https://zkrepl.dev/
[9]官方文件: https://docs.circom.io/circom-language/signals/
[10]友善的零知識證明介紹: https://learnblockchain.cn/article/6184
[11]Wikimedia: https://commons.wikimedia.org/wiki/File:Bourbaki_congress1938.png
[12]Wikimedia: https://commons.wikimedia.org/wiki/File:Hartmann_zebras_hobatereS.jpg
[13]Wikimedia: https://commons.wikimedia.org/wiki/File:Kingsley_lockbox.jpg
[14]AI 翻譯官: https://learnblockchain.cn/people/19584
[15]這裡: https://github.com/lbc-team/Pioneer/blob/master/translations/9178.md
[16]^2]: 參見 [联邦党人文集(维基百科): https://en.wikipedia.org/wiki/The_Federalist_Papers#Authorship
[17]^3]: 參見 [Bourbaki(维基百科): https://en.wikipedia.org/wiki/Nicolas_Bourbaki#Membership
[18]^8]: 這使得編寫約束相當具有挑戰性,正如你可以想像的那樣。有關Circom 中約束的更多詳細信息,請參見 [https://docs.circom.io/circom-language/constraint-generation/: https://docs.circom.io/circom-language/constraint-generation/
[19]^12]: 線性約束意味著它可以僅透過加法表示為線性組合。這相當於使用常數進行乘法。需要注意的主要是線性約束比非線性約束更簡單。有關更多詳細信息,請參見 [约束生成: https://docs.circom.io/circom-language/constraint-generation/
[20]算術電路: https://docs.circom.io/background/background/#arithmetic-circuits
[21]^13]: 從數學上講,我們所做的是確保方程式Az * Bz = Cz 成立,其中Z=(W,x,1)。 A、B 和C 是矩陣,W 是見證(私有輸入),x 是公用輸入/輸出。雖然知道這一點很有用,但編寫電路時並不需要理解這一點。有關更多詳細信息,請參見 [Rank-1 约束系统: https://docs.circom.io/background/background/#rank-1-constraint-system
[22]^15]: 正如在友好的介紹文章中提到的那樣,2016 年Zcash 舉辦的儀式有一個很好的外行播客,你可以在 [这里: https://radiolab.org/podcast/ceremony
[23]^17]: 我們稱之為1-out-of N 信任模型。還有許多其他信任模型;你最熟悉的可能是多數規則,即你信任大多數人做出正確的決定。這基本上就是民主和大多數投票的運作方式。 [↩: #user-content-fnref-17
[24]^22]: 又稱_密碼學難度假設_。請參見 [计算难度假设 (维基百科): https://en.wikipedia.org/wiki/Computational_hardness_assumption#Common_cryptographic_hardness_assumptions
[25]^23]: 有關更多信息,請參見 [https://en.wikipedia.org/wiki/Integer_factorization: https://en.wikipedia.org/wiki/Integer_factorization
[26]^24]: 雖然我們可以加上_asserts_,但這些其實不是約束,只用於清理輸入。有關其工作原理,請參見 [https://docs.circom.io/circom-language/code-quality/code-assertion/: https://docs.circom.io/circom-language/code-quality/code-assertion/
[27]https://www.chainsecurity.com/blog/circom-assertions-misconceptions-and-deceptions: https://www.chainsecurity.com/blog/circom-assertions-misconceptions-and-deceptions
[28]^25]: 這份由0xPARC 提供的資源非常出色,如果你想深入了解編寫(Circom) 電路的藝術: [https://learn.0xparc.org/materials/circom/learning-group-1/circom-1/: https://learn.0xparc.org/materials/circom/learning-group-1/circom-1/
[29]^26]: 由於編寫約束的性質,這種情況經常出現。請參見 [https://en.wikipedia.org/wiki/Truth_table: https://en.wikipedia.org/wiki/Truth_table
[30]^27]: 有關circomlib 的更多信息,請參見 [https://github.com/iden3/circomlib: https://github.com/iden3/circomlib
[31]^28]: 請參見 [https://github.com/iden3/circomlib/blob/master/circuits/comparators.circom: https://github.com/iden3/circomlib/blob/master/circuits/comparators.circom
[32]^29]: 人們通常在專案之間共享這些ptau 檔案以提高安全性。有關詳細信息,請參見 [https://github.com/privacy-scaling-explorations/perpetualpowersoftau: https://github.com/privacy-scaling-explorations/perpetualpowersoftau
[33]https://github.com/iden3/snarkjs: https://github.com/iden3/snarkjs
[34]^30]: 這裡的梯子代表某種值,使我們能夠以相反的「困難」方式進行。另一種思考方式是將其視為一個掛鎖。你可以輕鬆鎖定它,但很難解鎖,除非你有鑰匙。陷門函數也有更正式的定義,請參見 [https://en.wikipedia.org/wiki/Trapdoor_function: https://en.wikipedia.org/wiki/Trapdoor_function
[35]^31]: 來自維基百科的截圖。請參見 [ECDSA (维基百科): https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm#Signature_verification_algorithm
[36]^38]: 在現實世界的數位簽章方案中,當多個訊息交換時,我們可能也希望引入一個加密隨機數。這是為了避免重播攻擊,即某人可以在稍後時間重用相同的簽名。請參見 [https://en.wikipedia.org/wiki/Replay_attack: https://en.wikipedia.org/wiki/Replay_attack
[37]^40]: 在ZKP 中實現群簽名的靈感來自0xPARC,請參見 [https://0xparc.org/blog/zk-group-sigs: https://0xparc.org/blog/zk-group-sigs
[38]^41]: 請參見 [https://docs.circom.io/circom-language/control-flow/: https://docs.circom.io/circom-language/control-flow/
[39]^42]: 相較之下,實施群簽的論文如[https://eprintiacrorg/2015/043pdf:https://eprintiacrorg/2015/043pdf[https://eprintiacrorg/2015/043pdf:https://eprintiacrorg/2015/043pdf