FlashProve 跑出来的证明总被拒 / 是步骤缺项还是验证器参数没对齐?

文章导读
FlashProve 的证明被拒,通常不是单一原因:步骤缺项和验证器参数没对齐都会表现成同一句“验证失败”。可行的判断顺序是先拿报错原文与退出码,再在同一份输入上重复运行,看被拒是否稳定复现;随后把生成端写出的字段与验证器读入的参数逐项对照;最后裁到最小步骤集,确认缺项是否由步骤省略造成。如果这几步都不做,只靠反复改配置重跑,往往只是在换一种方式碰运气。
📋 目录
  1. 一 先把被拒的报错原文和退出码完整留存
  2. 二 在同一份输入上连跑三次,看被拒是否稳定
  3. 三 核对验证器读入的参数与生成端写出的字段
  4. 四 把生成步骤减到最少,看缺项是否来自步骤裁剪
  5. 五 固定一组能通过的最小样例当回归基线
A A

FlashProve 的证明被拒,通常不是单一原因:步骤缺项和验证器参数没对齐都会表现成同一句“验证失败”。可行的判断顺序是先拿报错原文与退出码,再在同一份输入上重复运行,看被拒是否稳定复现;随后把生成端写出的字段与验证器读入的参数逐项对照;最后裁到最小步骤集,确认缺项是否由步骤省略造成。如果这几步都不做,只靠反复改配置重跑,往往只是在换一种方式碰运气。

遇到 FlashProve 证明被拒,不要先在配置里乱改参数。先完整留存标准输出、错误输出和退出码,再在同一输入上连跑三次:三次结果一致,说明是稳定复现,优先查步骤缺项和字段错位;三次结果不一致,则先查输入、随机量或并发共享状态。字段对照和最小步骤集是确认手段,不是修复手段;参数名与字段对应关系需要结合你本地 FlashProve 版本的实际配置和返回确认。

先把被拒的报错原文和退出码完整留存

只看到终端最后一行“verify failed”,无法判断是解析阶段失败、证明生成阶段失败还是验证阶段失败。建议把生成与验证包成一个函数,同时捕获标准输出和错误输出,并把退出码单独记录。下面的骨架中,命令和参数名需要替换成你本地 FlashProve 的实际入口。

set -o pipefail

prove_run() {
  # 替换为实际的生成/验证入口与参数
  flashprove prove \
    `--input` ./case.json \
    `--steps` ./steps.json \
    `--verifier-config` ./verifier.toml
}

out=$(mktemp)
prove_run >"$out" 2>&1
rc=$?

{
  echo "exit_code=$rc"
  echo "--- stdout + stderr ---"
  cat "$out"
} | tee "./logs/prove_$(date +%s).log"

两点值得注意:一是 2>&1 必须在重定向到文件或管道之前,否则错误输出会绕过记录;二是退出码要在命令执行后立刻取,中间不要插入其他命令。日志里保留完整的命令行和配置路径,后续比对时才说得清是哪一次改动引入了拒绝。

在同一份输入上连跑三次,看被拒是否稳定

偶发被拒和稳定被拒的排查方向完全不同。偶发问题通常指向输入文件被并发改写、随机盐或临时目录复用、验证器读取了上一次的中间产物;稳定被拒才更适合往步骤缺项和参数错位方向查。用同一份 case.json、同一份 steps.json、同一个验证器配置连跑三次即可,不要中途修改任何文件。

mkdir -p ./logs
: > ./logs/repeat.tsv

for i in 1 2 3; do
  start=$(date +%s%N)
  out=$(mktemp)
  prove_run >"$out" 2>&1
  rc=$?
  end=$(date +%s%N)
  ms=$(( (end - start) / 1000000 ))
  printf "%s\t%s\t%s\t%s\n" \
    "$i" "$rc" "$ms" "$(head -n 1 "$out")" >> ./logs/repeat.tsv
done

记录表建议按下表填写,报错首行只取第一行,不做主观归纳,保留原文更利于比对。

FlashProve 跑出来的证明总被拒 / 是步骤缺项还是验证器参数没对齐?
运行序号退出码报错首行耗时(ms)
1待填待填,原文抄录待填
2待填待填待填
3待填待填待填

三行退出码与报错首行一致,基本可以按稳定复现处理;若退出码一致但报错内容在变,或者耗时波动明显,需要先排除输入和中间产物被覆盖的可能,再进入字段对照环节。

核对验证器读入的参数与生成端写出的字段

这一步的目的是确认“生成端以为写进去了”和“验证器实际读到了”是不是同一组值。做法是先跑一次生成,保留输出文件或日志中的字段清单,再对照验证器配置里被读取的键名。下表给的是常见对应关系,具体键名需要按你本地的 FlashProve 版本和配置文件确认。

生成端写出的字段验证器读入的参数不一致时的常见表现
input_hash / input_digest`--input-digest` 或配置中的输入摘要项摘要不匹配,验证直接失败
step_count / steps[]`--steps` 或步骤数组长度步数不一致,验证器按少算或多算的步数校验
public_inputs`--public-inputs` 路径或内联值公开输入个数或顺序不一致
circuit_id / version验证器声明的电路标识与版本版本不同导致的兼容性拒绝

缺字段和多字段的报错形态通常不一样:缺字段一般出现在参数解析或反序列化阶段,报错里带 missing、required、null 之类的字样,往往在证明计算之前就退出;多出的、未被声明的字段在严格模式下会报 unknown field,在宽松模式下可能被静默忽略,表现成“参数看起来对但验证对不上”。以上形态以你实际运行返回的内容为准,不要把这里的描述当成字段名的权威定义。

把生成步骤减到最少,看缺项是否来自步骤裁剪

怀疑步骤被写漏时,与其逐条读配置,不如反向裁减:先构造一个最小步骤集,保留“输入解析 → 步骤展开 → 证明生成 → 输出序列化”这四个必要环节,去掉所有可选的后处理步骤,确认在小步骤集下能否通过。若能通过,再每次加回一个步骤,加回后开始被拒的那一步就是问题所在。

FlashProve 跑出来的证明总被拒 / 是步骤缺项还是验证器参数没对齐?

要判断步骤数量确实变了,看日志中输出的步数统计行更直接,例如类似 steps=4 或 step 3/4 的记录;如果日志只打印总数不打印明细,可以在生成端把步骤数组的键名或长度写进日志。比对裁剪前后的同一行内容,比凭印象数配置条目可靠得多。

固定一组能通过的最小样例当回归基线

定位完问题之后,建议立刻固化一组能通过的最小样例,否则下次改动又会回到同样的猜测里。基线的内容范围控制在最小:一份输入文件、一份步骤定义、一份验证器配置,以及一次已知通过的完整输出;不要把大体积真实数据放进基线目录。

存放位置建议放在仓库内的独立目录,例如 ./baseline/,与业务配置分开,并在目录里留一份说明,写清这组样例对应的 FlashProve 入口命令、版本标识和期望退出码。改动之后重新比对的命令骨架如下:

./scripts/prove_run.sh `--config` ./baseline/verifier.toml \
  `--input` ./baseline/case.json \
  `--steps` ./baseline/steps.json \
  > ./logs/baseline_after.log 2>&1
echo "exit=$?"

diff -u ./baseline/expected.log ./logs/baseline_after.log

判定标准建议定成两条:退出码与基线一致,且 diff 除时间戳、临时路径这类必然变化的字段外没有其他差异。任何一条不满足,就先不要继续改业务配置,回到前面几步重新确认是步骤缺项还是参数错位。这套基线不解决性能问题,也不保证所有场景都能通过,它只保证你在同一条已知可用的路径上做比较。