Aptos Leaner 验证栈的 V5 Denotation 设计:单一指称语义与单一一致性定理
【免费下载链接】aptos-coreAptos is a layer 1 blockchain built to support the widespread use of blockchain through better technology and user experience.项目地址: https://gitcode.com/GitHub_Trending/ap/aptos-core
本篇文章基于当前仓库中的设计文档 denotation.md 展开,系统讲解 Aptos 实验性 Lean 4 验证栈(third_party/move/lean下的 leaner 系列包)当前采用的验证设计:用一个denote函数把已验证的 LIR 程序指称到Spec规范单子,再用一个denote_agrees一致性定理把它与 big-step 语义相连,从而把每个函数目标的验证成本压缩到一次定义性展开加叶子求解。读完本文,你将理解该方案为何能取代按目标生成一致性证明的三条历史路线、九条性能原则分别约束什么、里程碑 D0–D4 的推进顺序,以及仓库中对应的源码模块与测试台账。
背景:为什么需要"一个指称、一个一致性证明"
Leaner 验证栈的权威语义是BigStep(见 leaner-ir/LeanerIR/Semantics/BigStep.lean),可执行形态是带燃料的解释器,而verify f必须产出关于 big-step 关系的定理。v0 栈(v0/move)当年能自动验证 225 个函数、每个亚秒级,是因为它的浅层形式就是语义本身——没有第二套模型需要证明一致。但代价是两条结构性缺陷:缺少跨程序的元理论,且与执行之间存在未证明的鸿沟(存储还是公理化的)。
leaner 栈要同时保住自动化与可证性,因此不能简单回到"浅层即权威"。v2 设计(见 historical/verification-v2.md)提出的正确形状是:保留 deep 语义为参照,增加一个浅层指称(denotation),用证明而非假设把它与参照连接起来。V5 即本设计文档denotation.md的最终形态:2026-09-08 定稿,取代了 frame-free row 路线(certifying-execution.md)和 normalize/native 路线(generic-route.md)。
核心设计:两个定义、一个定理、一个 per-target 展开
设计的骨架是"两个定义加一个定理,都只写一次":
denote : ExecutableUnit → FunctionHandle → Array RuntimeValue → Spec RuntimeState Failure (Array RuntimeValue) denote_agrees : ∀ unit function arguments, Spec.Equiv (denote unit function arguments) (meaning unit function arguments)denote是已验证单元本身的 Lean 函数,对函数体表达式 arena(Validation/IndexedArena.lean)做结构递归(fuel 由 arena 大小界定,见 Proofs/Fuel.lean),或对由 arena 一次性物化的树递归(D0 里程碑裁定二选一)。刻意不用良基递归——良基递归对 whnf 与simp不透明,内核无法在具体数据上展开它。每个 LIR 构造恰好对应一个 case。递归调用通过 Proofs/Recursion.lean 的 oracle 消费被调函数;递归 SCC 取其中已定义的fixBody最小不动点;循环取 Proofs/NativeLoop.lean 中Runs/Fails/Undefined不动点;存储与引用采用类型化 family store 与预言(prophecy)编码(prophetic-references.md)。denote_agrees只证明一次,对 fuel 或树做归纳、面向BigStep.EvalFunction,复用 Proofs/Denotation.lean 中已有的逐构造一致性引理作为归纳情形。它就是 v2 想要的 LIR 元理论,也是裸verify成功有意义的依据。- per target,
verify f在denote unit f之上陈述契约;elaborator 在具体单元上展开denote得到封闭浅层项t,用定义性等价(rfl或simp only [denote])证明denote unit f = t,然后只对t推理。每个目标不再需要证明任何一致性。
denote是类型化且无 frame的:由已验证类型索引为Spec RuntimeState Failure ⟦τ⟧(原生载体),局部变量由 continuation 绑定,引用为预言值;展开后的目标中不出现RuntimeFrame、row 或 loan registry。这不是可选项而是硬要求——v0 亚秒级的根因就是目标呈此形状,而 row 路线每函数数秒正是目标携带了 frame 与调用边界回写。代码c(Proofs/Representation.lean)只出现在一致性归纳里,永不进入目标。
源码中的四个模块
实现落在leaner-ir/LeanerIR/Proofs/Denote/下四个文件:
| 文件 | 职责 | 关键内容 |
|---|---|---|
| Types.lean | 原生载体类型、row、codec、状态操作及其 wp 规则 | 互递归族NTy/NRow/NRows;carrier、HList、variantCarrier;Codec;Comp/checkedInt/CheckedOp/CompareOp/BitOp/位移;wp_ite、wp_bottom等 |
| Term.lean | 类型化项与Term.denote | Term ρ Γ τ、Args(含reborrow)、Exports、Flow(value/return/break/continue)、Term.denote、Function.denote、wp_call、wp_flowBind、wp_loopAt |
| Compile.lean | compileFunction(对 fuel 结构递归) | ntyOfFuel、compileExpr、compilePlace、compileArgs、compileOperation、compilePrimitive、LoanRole、siteRoles、typeFuel/unitFuel |
| Agreement.lean | 命名公理与向SatisfiesFunction的运输 | compileFunction_agrees(当前为 axiom)、satisfies_typedMeaning、satisfiesFunction_of_denote |
另外 Close.lean 提供 closer(lir_denote/lir_denote_normsimp 集、state-fact 与 leaf 策略),Verify.lean 拥有verify、#leaner_verify、#leaner_require_native[_all]命令,Contract.lean 保存契约翻译与准备。
不变的底线
设计明确列出四条"不变":
BigStep是权威。verify f必须是关于已验证 LIR big-step 关系的定理,靠证明连接、绝不靠假设。elaborated LeanerLang 项由前端与元程序生成,若定理只关于 elaborator 输出,信任根将不可证伪——这正是 v2 拒绝"浅层即权威"的原因。- 单一语义。解释器、big-step 关系、验证器运行同一个模型(含引用的预言编码);解释器保持可执行形态和 MonoVM 差分锚点,其可靠性与 fuel 完备性不变。
- 非空泛。裸
verify成功仅当 LIR 每个节点都有可执行语义时才有意义,准备阶段拒绝其余情况(v2 的 vacuity 发现:&mut X[a].f.g曾降级为无运行时含义的 value-levelselect链,使Satisfies的部分正确性对任何契约空泛成立)。 - 三主张分离。CLAUDE.md 中声明的三件事保持独立:源码验证、降级到字节码、以及两者之间尚未证明的编译器正确性定理。
九条性能原则
v0 曾以亚秒成本自动验证 225 个函数,其后每条路线都变慢;性能审计(verification-perf-audit.md)与 v0 自己的分析(v0/move/Move/performance-analysis.md)把差距归于同一小撮原因。以下九条原则是那些原因的反面,每条都附证据与检查方式,D0 若违反任一条即失败(不看测得的数字):
- 目标只含数学、不含机械。
verify推理的项只提及源码提到的内容:原生载体值、局部变量的 Lean binder、&mut的预言值、类型化 family store。不出现RuntimeFrame、row、loan registry、arena、表达式 id 或字符串。检查:elaborator 审计展开项的常量,出现即拒绝目标。 - 只翻译一次,在任何义务产生之前;绝不做符号执行。对程序数据的全部计算发生在
denote的定义性展开里,先于第一个义务;义务中永不出现denote、求值器或 arena 查找。检查:展开项无denote/解释器常量;DenotePerformance.exp 把展开成本与闭合成本分开记录。 - 最弱前置条件,而非展开的关系。每个组合子一条 wp 规则:
wp (bind a f) ↔ wp a (fun v => wp (f v)),无存在量词、每个子项只出现一次、随函数体线性增长;良定义性结构化且从不展开。检查:wp 规则是命名 simp 集里每个组合子一条引理,义务数等于退出路径数加显式不变量义务数。 - 一次遍历、一个上下文。函数体只遍历一次、按退出路径切分,而不是为
ok/aborts/undefined各遍历一次;simp_all、subst_vars这类上下文级 pass 只在叶子目标上、只对该叶子自己的上下文运行。证据:bump_twice曾因闭合 pass 对每个残差目标重处理整个上下文而搜索受限在每证明对象 25k heartbeats(审计 F1/F1c)。 - 按契约模块化。调用在边界贡献被调函数的契约(原生等式),而非其函数体;泛型函数体证明一次、实例化使用。证据:泛型语言检查点把调用者从 50M+ heartbeats 降到 14M,因为不再重验证被调函数。
- 可判定叶子,其余全部结构化。叶子目标是
Int上的线性算术(带 range 证书)由omega闭合,或有限枚举由decide闭合;没有 tactic 在义务结构上搜索或回溯,closer 不匹配目标形状。证据:Certify.lean拒绝0 < args.item.val这类不支持的形状。检查:closer 是 simp 清单后接omega/decide/grind,无路由选择。 - 热路径上的数值同一性。Id、存储键、variant 索引是
Nat;每个字面量只有一种拼写。证据:审计 F2(热比较中的字符串同一性)、F3(数组拼写多义)、F5(嵌套归纳上的派生BEq是partial)。 - 预计算清单、稳定键。证明用到的每个 simp 清单都是注册过的 simp 集(
lir_denote、lir_denote_norm),绝不写成长显式列表——simp only [list]每次调用都会重新 elaborate 每条,当正规形清单增长时这成了每目标恒定 0.8M heartbeats。键控重写引理的类型(HList、variantCarrier)不可归约:可归约的键在目标中被归约、在引理中却卡住,重写会静默失配。 - 逐目标对照 v0 度量。
DenotePerformance.exp逐目标记录 heartbeats、证明对象、展开成本;闸门是同一契约在 v0 上的每函数时间;再生成只点名变更本应移动的目标。
其中 1、2、3、6 是架构性的(按denote与 closer 的构造成立或失败,正是此前路线违反的),4、5、7、8、9 是工程纪律(部分被旧路线找回、不能再丢)。
走过的弯路:v2 的教训
v2 原本要求一条面向Spec的、验证后 LIR 的单一泛型指称,用一条归纳证明的一致性定理连到BigStep(即 V5);per-target 发射一致性证明只是"可接受的第二名",等 V5 落地即弃。但 V5 从未被 scope、从未被尝试,临时方案成了架构。随后三次路线重写(script、normalize/compose、native)每次都按构造重复三件事:一个由 shape 匹配元程序逐目标生成的原生组合子(LeanerLang/Native*.lean)、一条把它连到 frame 模型的协议律(Proofs/*Agreement.lean,逐目标组装成computationRepresents)、以及为新目标形状补 closer 支持(Proofs/Certify.lean,它拒绝不认识的形状)。每条路线的协议库都不完整,于是每条都需要上一条兜底;移除 fallback 只会暴露依赖而非消除它——61 个 Check 文件中有 42 个因构造缺三件中的一件而失败。面向证明的Proofs/树有 41k 行,覆盖面却不及 v0 那 15k 行的语义加验证器。
已携带与未携带的构造
当前检查点(2026-09-08,见 test-organization.md 的 35/61 台账)下:
已携带:标量与检查算术、比较、布尔运算、无符号&、检查移位与转换、常量、if、let、块、abort、assert、提前return、赋值、break/continue(含带标签的)、带invariant子句的循环(自动 frame 把表头处每个不可变局部钉到其入口值)、经被调函数发布定理的直接单态调用、元组、结构体、带 variant 测试与载荷选择的枚举(match以此形态到达)、局部变量共享借用、可变引用(NTy.ref:带原生当前值的 loan;参数、原地修改、类型化投影路径、局部 lender、从被调函数导出处结算的重借参数),以及运行时键控全局 map 上的存储(globalRead、globalContains、globalBorrow、globalPublish、globalTake;可变全局借用留下运行时 hole,其死亡标记按 key 回写)。被拒绝的构造会被点名。
未携带:|、^、有符号位运算、向量(台账中每一行"type of a local is not carried")、泛型调用/构造器/字段、递归(被调函数须先于调用者验证;drain、recursive_choose)、用作摘要的未指定纯被调函数(plus_one)、绑定或调用参数之外的 mutable borrow(reborrow)、返回引用、携带活全局借用的循环(Language/Loops的drain)、资源不变量(GlobalInv、CrossInv)、Rust profile 的原语、嵌套解构模式。
实现定下的规则
文档还记录了实现过程中沉淀的四条规则:
- closer 是 worklist:带标记循环上的 wp 取循环规则,被调函数取该函数的定理,递归迭代取循环假设;语法 binder、合取或条件会分裂;只有 head 是
wp的目标才重归一化;调用的 post 假设在重归一化前消费(specialize 蕴含、split 存在、代换 witness、用状态等式重写)。 - leaf 先清除所有计算假设(
contradiction曾把整个单元经准备假设归约掉),把上下文中每个 range 证书的边界作为独立事实加入(绝不重写某个项所依赖的证书——它留下的 cast 在 transparency 与 simp 匹配处未类型化),用上下文饱和,分裂 range-check 条件,最后由omega/decide判定。有符号商与余数使用宽度特定 range 事实,而非绝对值。 - 清单(inventory)是 simp 集、绝不写显式列表(否则每调用 0.8M heartbeats)。
HList与variantCarrier不可归约,所以任何右侧构造 row 值的引理都要在 row 类型上陈述,而非其展开到的乘积。 - Rows 是互递归族
NTy/NRow/NRows(嵌套归纳无法派生 decidable equality);enum 携带其 variant 名的两两不同性;聚合参数以解构方式引入、每个 variant 一个目标;指称定义中绝不出现do-notation。
假设台账:LeanerIR.Proofs.Denote.compileFunction_agrees是 axiom(用户 2026-09-08 决定,直到归纳完成);每个verified定理在#print axioms下列出它;除此之外无任何 admitted。
里程碑 D0–D4
| 状态 | 里程碑 | 闸门 | |
|---|---|---|---|
| D0 | DONE 2026-09-08,协议被假设(命名 axiom,用户决定) | 对Denotation.lean已覆盖的直线子集(值、局部、检查算术、赋值、返回、单态调用)实现denote与denote_agrees | 定理无sorry闭合;Language/Arithmetic与Language/Integers经定义性展开验证;per-target 展开与闭合成本记录进Performance.exp并与 v0 逐函数时间对比,展开单独报告 |
| D1 | DONE 2026-09-08(除递归drain、recursive_choose) | 控制流:分支、带载荷的 enum match、带不变量的结构化循环、递归 | Verification/Loops、LoopInvariants、Calls、Callees、递归Corpus目标、Language/Loops、Enums、EnumPatterns、ControlForms通过 |
| D2 | IN PROGRESS:引用与存储已携带(Account、Storage、Read、Prophecies、Corpus、Normalized通过);开放:资源不变量、带活全局借用的循环、返回引用、向量 loan | 存储与引用:类型化全局 family、作用域借用、预言、返回引用 | Account、GlobalBorrows、GlobalInv、References、Loans、Prophecies、Storage通过 |
| D3 | 未开始 | 泛型(V4 继承)与 Rust profile 指称 | Language/Generics、Verification/Generics、GenericScalarCalls、Rust-profile fixtures 通过 |
| D4 | 未开始 | 退役:删除逐目标computationRepresents生成、LeanerLang/Native*生成器、denote_agrees未消费的Proofs/*Agreement.lean模块、Certify.lean的 shape 路由 | 在不变 caps 下全量 Check 审计;Performance.exp达到 v0 平价目标;Move 与 Rust 套件全绿 |
D0 同时是可行性闸门:若直线子集的归纳无法闭合,或定义性展开的成本超过它所替代的协议证明,应在 D1 前停下并报告。
工作纪律与测试要求
文档规定了四条纪律:不新增Proofs/*Agreement.lean模块、LeanerLang/Native*生成器或Certify.leanshape case(denote不携带的构造即点名它的负向检查);不再向退役路线移植 fixture(只能由检查台账在不变 caps 下晋升);denote_agrees中无sorry(2026-09-08 那条命名 axiom 是临时项,靠证明移除;闭合不了的 case 就是denote不携带的构造);成本由DenotePerformance.exp逐目标闸住,不许抬高 caps。
测试要求:每个denotecase 必须带其归纳 case(case 未闭合的构造不进denote);正向检查是闸门中点名的现有 Check fixtures、契约文本不变、只在不变 caps 下通过才晋升;负向检查对每个不携带的构造给出点名诊断,且每个新构造上的假ensures必须失败(v2 vacuity 发现的要求);Performance.exp逐目标记录展开与闭合成本,再生成只点名应移动的目标。
性能闸门实际数据可见 DenotePerformance.exp:直线目标 transport 约 26 万 heartbeats、typed 约 180 万到 2500 万,循环目标约 1800 万到 2100 万,三 variantmatch(total)约 3500 万;e2e 检查驱动把单目标验证 heartbeats 上限设为 18 万(即 180k)。这与文档中"直线 1.7–6M、循环约 20M、带匹配契约的三 variant match 35M"的记录吻合。
开放问题
- 逐目标展开的证书,内核
rfl与simp only [denote]哪个更便宜;对 arena 的 fuel 与物化树哪个展开更便宜——在 D0 中测量。 - 泛型参数如何携带:按类型参数索引的载体族(当前 D0 采用),还是单态化视图(V4)。
- 既有协议引理中哪些作为归纳 case 存活、哪些直接重证——由归纳本身决定,不由模块划分决定。
结语
V5(denotation)设计的核心是把"每函数一个协议证明"永久替换为"一个denote定义 + 一个denote_agrees归纳 + 每个目标一次rfl级展开"。它不改变BigStep的权威地位,不新增语义模型,把 v0 的自动化形状(wp 规则加omega/decide叶子)在 deep 语义之上完整恢复,同时让每个构造的扩展成本固定在"一个denotecase、一个归纳 case、至多一个 wp 引理"。仓库中 Types.lean、Term.lean、Compile.lean、Agreement.lean 与 Close.lean 五份实现文件、DenotePerformance.exp 性能台账、以及 Check/ 下的验收 fixtures 共同构成可复现的证据链,是深入该验证栈的首选入口。
【免费下载链接】aptos-coreAptos is a layer 1 blockchain built to support the widespread use of blockchain through better technology and user experience.项目地址: https://gitcode.com/GitHub_Trending/ap/aptos-core
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考