前言
在学习Aptos官方文档中的The Move Book时,发现后面还有一个The Move Prover Book。意外发现除了通过写Move的单测之外,还有另一种方法来验证Move合约的正确性。此篇就是学习Move形式化验证的学习笔记。
Move prover的环境准备
话不多说,先来跑一下demo,直观感受一下Move Prover是做什么的。
js
//环境准备,可以直接在aptos-core的路径下执行
./scripts/dev_setup.sh -yp
source ~/.profile
//跑一下Example
aptos move prove --package-dir aptos-move/move-examples/hello_prover/
得到运行结果:
js
[INFO] checking specifications
[INFO] rewriting specifications
[INFO] preparing module 0x42::prove
[INFO] transforming bytecode
[INFO] generating verification conditions
[INFO] 2 verification conditions
[INFO] running solver
[INFO] 117.237s build, 0.025s trafo, 0.042s gen, 0.677s verify, total 117.981s
{
"Result": "Success"
}
那么这是执行了些什么呢?查看运行的hello_prover目录下的prove.move代码如下:
js
module 0x42::prove {
fun plus1(x: u64): u64 {
x+1
}
spec plus1 {
ensures result == x+1;
}
fun abortsIf0(x: u64) {
if (x == 0) {
abort(0)
};
}
spec abortsIf0 {
aborts_if x == 0;
}
}
根据之前学习到的Move基础知识,这段代码中有两个函数,分别是plus1和abortsIf0,其函数实现功能也非常简单,一个实现对入参加1,另一个判断传入参数是否为0,如果是0就异常中止。
除此之外,上述代码中还包含了两个以"spec"定义的代码片段,这是什么呢? 这当然就是今天要学习的Move的形式化验证方法了。
形式化验证概念
形式化验证(formal verification)是指使用数学工具分析设计可能行为的空间,是一种使用严格的数学方法来描述行为和推理计算机系统的正确性的技术。
形式化验证经常被应用在操作系统、编译器等对正确性要求高的领域。智能合约在其设计的正确性和安全性方面都有较高的要求,Move Prover就是利用形式化验证方法,为防止 Move 语言编写的智能合约中的错误而设计的工具。
在前面的prove.move代码中的spec代码就是用 Move 规范语言(MSL)写成的,用以说明程序代码应该满足的条件,或者描述函数的行为。
比如spec plus1就是描述了确保plus1函数的结果要等于x+1,spec abortsIf0确保当且仅当x为0时异常中止。
如何编写验证代码
我们先根据aptos.dev/move/prover... 中的示例来学习,首先是一个代码示例:
js
module 0x0::m {
struct Counter has key {
value: u8,
}
public fun increment(a: address) acquires Counter {
let r = borrow_global_mut<Counter>(a);
r.value = r.value + 1;
}
spec increment {
aborts_if !exists<Counter>(a);
ensures global<Counter>(a).value == old(global<Counter>(a)).value + 1;
}
}
该代码示例原本实现的是一个计数器功能,对一个u8类型的变量进行每次加一的计数功能。
我们按照之前aptos工程的配置设置好之后,在工程目录下运行 aptos move prove
,得到结果如下:
可见Move Prover执行过程中出现了错误,这段错误提示的意思就是当这个u8类型的变量是255时,再进行加一操作,就会出现整形溢出的异常。我们已经知道在Move中如果出现整形溢出,Move当前运行程序会异常中止,而在我们编写spec规则时,仅考虑了a这个值不存在的情况,未考虑整形溢出的异常中止情况,因此可以对spec代码进行修正,增加对整形溢出的考虑,代码如下:
js
spec increment {
aborts_if global<Counter>(a).value == 255;
aborts_if !exists<Counter>(a);
ensures global<Counter>(a).value == old(global<Counter>(a)).value + 1;
}
在进行验证,得到了验证成功的结果,如下图:
ps:有时候仅运行"aptos move prove"得到的错误信息不能很好的帮助我们定位问题,可以通过增加-t (--trace) 指令,查看详细的错误信息。
除了这个简单的例子外,还可以查看aptos-core/third_party/move/move-examples/experimental/basic-coin/sources/BasicCoin.move中的示例,这是一个典型的coin基本功能的合约,可以看到示例中为coin的余额查询,transfer, withdraw, deposit, burn这些常用功能都写了spec代码,并且有避免代码冗余而抽象出来的Schema写法。
我们平时对自定义的Aptos Coin做形式化验证时,可以参考示例代码中的写法,此处就不一一展开细讲了。看完示例后,按照笔者自己的理解,编写spec代码就是用MSL语言编写函数应该遵循的条件,常用的设计spec代码的思路可以有:
- 给出函数触发异常中止的条件,一般用aborts_if
- 对函数的返回值的判断,一般配合ensures使用
- 函数的入参需要满足的条件,比如transfer函数中转账金额不得大于转出账户余额
- 函数一定需要满足的条件,比如transfer函数的转入账户最终余额不会超过MAX值
对Move的形式化验证学习和理解就到此结束了,掌握了基本方法就能够编写出基本的验证代码。但若想发挥出形式化验证的更强功效,还是需要多进行实践和练习。