关于合约账户有 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 的构成,如果能完成会很有收获。