關於合約帳戶有 3 個子任務,共同組成了 bad account 系列。本文是系列的第二個任務 Bendy Signatures。
分析
題目裡有 2 個合約,sphinx 是抽象帳戶合約,由帳戶去調用 gates 合約的 open 方法。sphinx 有很多限制,給了 3 個公鑰,其中第 1 個有私鑰。題目模擬的是 2/3 多簽問題,需要傳入 2 組簽名都認證成功才能發送驗證交易。
在帳戶合約的__validate__
中使用了starknet::get_tx_info().unbox()
拿到了交易的簽名,需要我們構造長度為 6 的 raw_sig,其中 raw_sig [0] 和 raw_sig [3] 是公鑰,raw_sig [1] 和 raw_sig [4] 是簽名的 r,raw_sig [2] 和 raw_sig [5] 是 簽名的 s。需要公鑰是預設的 3 個公鑰之一,且 r 和 s 不能重複。同時 __execute__
中限制了calls.len() == 1
,所以不能用唯一的私鑰生成 2 筆交易去解題。
由於只有 1 個私鑰,需要用這個私鑰簽名生成兩組 r 和 s 都能通過驗證且不重複。問題變成了如何用 SDK,發送一筆交易,但是有 2 組有效簽名。
過程
我首先嘗試在 Starknet.js 中用 2 個不同的 maxFee 生成了兩組 open 交易並分別簽名,發現無法被測試網驗證。
詢問導師後,發現這題實際要破解的問題是 ECDSA malleability 問題,具體可參考ZK book。
我花了一段時間學習 ECDSA,其中涉及了很多數學知識。
如果你不知道 ECDSA 原理,建議也進行深入學習。上面的 ZK Book 內容很棒。
StarkNet 也是使用了 ECDSA 來生成簽名,ChatGPT 輸出的 ECDSA 簽名流程是:
1. 首先,需要一個私鑰,這是一個隨機選擇的整數。同時,還需要一個公鑰,這是私鑰和基點G的乘積,記作 Q = dG,其中 d 是私鑰,G 是基點。
2. 當你要簽名一個消息時,首先將消息通過哈希函數轉化為一個整數,記作 z。
3. 然後,選擇一個隨機整數 k,並計算點 R = kG。R 的x坐標就是簽名的一部分,記作 r。
4. 接著,計算 s = (z + r * d) / k。這個 s 就是簽名的另一部分。
最終簽名就是 (r, s)。
對應每一步在 Starknet.js 中是:
- 用
ec.starkCurve.getStarkKey(privateKey)
獲取公鑰,公鑰是已知的,可以跳過這步 - 用
hash.calculateTransactionHash
把交易詳情轉換為一個 messageHash。交易需要的 6 個參數需要自己設置,其中 calldata 用transaction.getExecuteCalldata
獲得 - r 是一個隨機數,不用自己設置,下一步可直接獲取
- 通過
ec.starkCurve.sign(msgHash, privateKey)
或者signer.signTransaction
來得到 r 和 s。
通過上面 ECDSA malleability 的參考資料可知由於對稱性,1 個 r 對應了 2 個 s 都可以使簽名有效。第二個 s 的計算很簡單,所需的另一個參數可在 SDK 的源碼中找到。最終生成的 r1 == r2, s1 != s2。
另外一個方法是用不同的 seed 生成兩個 r,python 庫 裡有對應的方法。最終生成的 r1 != r2, s1 != s2。
按題目要求構造一個長度為 6 的列表,傳入account.invokeFunction
的 signature 中,填上其他所需參數,發送即可完成任務。
總結
這題一方面介紹了多簽錢包合約是如何工作的,另一方面涉及 ECDSA malleability 問題,還需要了解 transaction 的構成,如果能完成會很有收獲。