NG#9 - 多签账户

关于合约账户有 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 签名流程是:

Copy
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 的构成，如果能完成会很有收获。