作者在过去几天里为Go标准库编写了一个新的ML-DSA实现,这是NIST去年夏天标准化的后量子签名算法。他在四天内完成了实时代码编写,但在周四晚上发现验证函数总是拒绝有效的签名。
测试失败信息显示:
|
|
在尝试调试半小时无果后,作者决定让Claude Code尝试解决这个问题,自己则去处理邮件。出乎意料的是,Claude Code在几分钟内就找到了一个相当复杂的低层错误。
作者向Claude Code提供了以下提示(包含原始拼写错误):
我在Go标准库中实现了ML-DSA,除了验证总是拒绝签名外,其他一切正常。我知道签名是正确的,因为它们与测试向量匹配。 你可以使用“bin/go test crypto/internal/fips140/mldsa”运行测试 代码位于src/crypto/internal/fips140/mldsa 查找签名无法验证的潜在原因。ultrathink 我抽查发现w1与签名时不同。
Claude Code发现作者为Sign使用将HighBits和w1Encode合并到了单个函数中,然后在Verify中重复使用了该函数,而Verify中的UseHint已经产生了高位,这导致在Verify中对w1的高位进行了两次计算。
查看日志后,Claude Code立即将实现加载到上下文中,并在没有任何探索性工具使用的情况下立即发现了问题。之后它编写了一个小型测试来确认假设,提出了一个修复方案,并检查测试是否通过。
作者随后丢弃了该修复方案,将w1Encode重构为接收高位作为输入,并更改了高位的类型,这既更清晰又节省了通过Montgomery表示的往返过程。
第二次模拟实验
周一,作者还完成了签名实现,但测试失败。存在两个错误,他在接下来的几个晚上修复了它们。
第一个错误是由于错误计算了几个硬编码常量(Montgomery域中的1和-1)。这个错误很难发现,需要大量的深度printf和猜测工作,花费了大约一两个小时。
第二个错误相对简单:最终编码到签名中的一个值太短(32位而不是32字节)。这个错误相对容易发现,因为只有签名的前四个字节相同,然后签名长度就不同了。
为了验证Claude在低层密码学代码中查找错误的能力,作者使用旧版本代码启动了新的Claude Code会话,提示如下:
我正在Go标准库中实现ML-DSA,刚刚完成了签名实现,但在针对已知良好的测试向量运行测试时,看起来它进入了无限循环,可能是因为它总是在Fiat-Shamir with Aborts循环中拒绝。 你可以使用“bin/go test crypto/internal/fips140/mldsa”运行测试 代码位于src/crypto/internal/fips140/mldsa 找出它无限循环的原因,并让测试通过。ultrathink
Claude花了一些时间进行printf调试,并以与作者非常相似的方式追踪不正确的值,然后发现并修复了错误的常量。Claude花费的时间肯定比作者少。
修复该错误后测试仍然失败,Claude放弃了,因此作者启动了一个新会话(假设关于错误常量的上下文对调查独立错误弊大于利),并给出了以下提示:
我正在Go标准库中实现ML-DSA,刚刚完成了签名实现,但在针对已知良好的测试向量运行测试时,它们不匹配。 你可以使用“bin/go test crypto/internal/fips140/mldsa”运行测试 代码位于src/crypto/internal/fips140/mldsa 找出问题所在。ultrathink
它走了一些错误的路径,思考了相当长的时间,然后也找到了这个错误。作者最初预计它会失败。
有趣的是,Claude发现“更简单”的错误反而更困难。作者的猜测是,也许失败测试的大量随机输出与其注意力机制不太匹配。
它提出的修复方案是只更新分配的长度而不更新其容量,但重点是找到错误,作者通常希望丢弃修复方案并自己重写。
三次无帮助的单次调试命中三次,这令人印象深刻。重要的是,当LLM的工作只是通过告诉我错误的位置来节省我一两个小时时,我不需要信任LLM或审查其输出,我自己会推理并修复它。
与以往一样,作者希望有更好的工具来使用LLM,而不是看起来像聊天或自动完成或“为我创建PR”。例如,如果每次测试失败时都启动一个LLM代理来找出原因,并且只有在它在我们修复之前找到原因时才通知我们,那该多好啊?