BitsLabSec/movy
GitHub: BitsLabSec/movy
Movy 是一个面向 Move 语言的先进测试框架,为 Sui 等链上智能合约提供模糊测试、不变量验证和执行追踪能力。
Stars: 16 | Forks: 8
# Movy

**Movy** 是一个 Move 测试框架,提供以下功能:
- 用于 Move 语言的模块化底层基础构件。具体而言,借鉴自 [revm](https://github.com/bluealloy/revm) 的 executor 和 tracer 抽象以及分层数据库设计,允许您模拟和检查执行过程。
- 继承自最先进的静态分析器 [MoveScan](https://dl.acm.org/doi/10.1145/3650212.3680391) 的静态分析能力。
- 借鉴 [Belobog](https://github.com/abortfuzz/belobog) 从头重新实现的尖端模糊测试,支持属性测试和链上模糊测试,其风格类似于 [foundry](https://getfoundry.sh/forge/advanced-testing/overview),允许通过使用 Move 语言编写不变量来实现。
- 一个类似于 `forge test` 的运行器(`movy sui test`),可执行您的 `test_*` 函数并自动填充其 object 和类型参数。
- 以及更多功能……
在[这里](https://docs.movy.rs)查看我们的文档。
__Movy 仍处于非常早期的 alpha 阶段,我们正在全力以赴开发新功能。__
## 演示案例
### 追踪 Transaction
```
let mut tracer = TreeTracer::new();
let _ = executor.run_tx_trace(
tx,
epoch,
timestamp_ms,
Some(tracer),
)?;
println!("The trace is:\n{}", trace.take_inner().pprint());
```
此代码片段可追踪任意 transaction `tx`,无论是链上的还是您自己构建的。
### 不变量测试
在单个函数中部署您的 Move module,即使它需要多个 transaction。
```
public fun movy_init(
deployer: address,
attacker: address
) {
let mut scenario = ts::begin(deployer);
{
ts::next_tx(&mut scenario, deployer);
counter::create(ts::ctx(&mut scenario));
};
ts::next_tx(&mut scenario, attacker);
{
let mut counter_val = ts::take_shared(&scenario);
counter::increment(&mut counter_val, 0);
assert!(counter::value(&counter_val) == 1, 0);
ts::return_shared(counter_val);
};
ts::end(scenario);
}
```
在您的 Move 测试 module 中为函数编写一个不变量测试:
```
#[test]
public fun movy_pre_increment(
movy: &mut context::MovyContext,
ctr: &mut Counter,
_n: u64
) {
let (ctr_id, val) = extract_counter(ctr);
let state = context::borrow_mut_state(movy);
bag::add(state, ctr_id, val);
}
#[test]
public fun movy_post_increment(
movy: &mut context::MovyContext,
ctr: &mut Counter,
n: u64
) {
let (ctr_id, new_val) = extract_counter(ctr);
let state = context::borrow_state(movy);
let previous_val = bag::borrow(state, ctr_id);
if (*previous_val + n != new_val) {
crash_because(b"Increment does not correctly inreases internal value.".to_string());
}
}
```
### 使用 `sui test` 运行测试
`movy sui test` 会构建并部署您的 package,运行 `movy_init`,然后执行每个名称以 `test_` 开头的 `#[test]`
函数——这与 `forge test` 非常相似。与原生的 Move 测试
运行器不同,这些测试函数可以带有 **参数**,包括 Sui object 和类型参数,
并且 `movy` 会为您自动填充它们。
```
// test-data/counter/tests/movy.move
#[test]
fun test_counter_smoke() {
assert!(1 + 1 == 2, 0);
}
// Object and type-parameter arguments are filled by movy:
#[test]
public fun test_increment_typed(ctr: &mut Counter) {
let _ty = std::type_name::get();
let before = counter::value(ctr);
counter::increment(ctr, 3);
assert!(counter::value(ctr) == before + 3, 300);
}
```
运行 package 中的所有测试:
```
movy sui test --locals ./test-data/counter
```
#### 发现 object 和待定参数:`--only-init`
`--only-init` 会运行 `movy_init`,然后打印它生成的 object 以及每个测试
函数及其仍然需要的参数——这是填充参数的起点:
```
movy sui test --locals ./test-data/counter --only-init
```
```
=== objects after movy_init (3) ===
deployer: 0xb641...
attacker: 0xa773...
0x95e1… 0x2::coin::Coin<0x2::sui::SUI> [owned by 0xa773… (attacker)] v3
0xd726… ::counter::Counter [shared (v3)] v3
0xdbcf… 0x2::package::UpgradeCap [owned by 0xb641… (deployer)] v2
=== test functions ===
::counter_tests::test_counter_smoke() [no args]
::counter_tests::test_increment_typed(&mut ::counter::Counter) [needs args]
```
#### 填充参数:`--object-mapping` 和 `--test-ty`
将 object 参数绑定到特定的 object,并将类型参数固定为具体类型(使用
`--only-init` 打印的 object id):
```
movy sui test --locals ./test-data/counter \
--object-mapping 'counter::counter::Counter/0xd726…e5d3' \
--test-ty 'counter::counter_tests::test_increment_typed:0/0x2::sui::SUI'
```
- `--object-mapping /0x` — 使用给定的 object 填充 object 参数。
可重复使用(或以逗号分隔);相同类型的条目按参数顺序被消耗。
- `--test-ty :/` — 设置测试函数的类型参数 ``。
类型和函数选择器接受**本地 package 名称**(例如 `counter::counter::Counter`),
并解析为当前部署的地址。因此,即使部署的 package id 发生变化,映射在重新构建后依然有效,
并且您可以直接从 `--only-init` 或 `--trace` 中粘贴类型。未映射的 object/类型参数将退回到自动的、模糊测试器式的填充方式。
#### 固定部署地址:`--deploy-at`
由 `movy_init` 生成的 object id 在源码编辑后保持稳定,但新部署的 package 在其 bytecode 发生变化时会被分配一个新的 id——这也会改变每个类型字符串(`::counter::Counter`)。将 package 固定到一个特定地址,以保持 id 和类型字符串的稳定:
```
movy sui test --locals ./test-data/counter --deploy-at counter:0xcafe…
```
`--deploy-at :0x` 可重复使用,并通过名称匹配 package。
#### 失败与可复现性
当 transaction 中止时(例如 `assert!` 失败),测试即会失败(退出码非零)。此外,
您的 `movy_pre_*` / `movy_post_*` 不变量会在每个测试运行期间被应用,因此通过 `crash_because` 报告的不变量违规也会导致测试失败,并显示原因:
```
oracle crash detected for ::counter_tests::test_increment_typed: Counter should be always increasing
```
`--seed` 会固定 RNG(进而固定 gas object 和新派生的 package/object id),而
`--checkpoint` / `--epoch` / `--epoch-ms` 会固定链上上下文,让运行过程完全离线工作(否则它们将从 `--rpc` 获取)。传递 `--trace` 以打印每个测试的执行轨迹。
### Call Graph 与 Type Graph
为 Move package 生成 Type Graph。

为 Move package 生成 Call Graph。

### 静态分析
TODO。
## 用法
### 将 Movy 作为工具使用
安装依赖:
```
apt install -y libssl-dev libclang-dev
```
构建 `movy` 二进制文件。
```
git clone https://github.com/BitsLabSec/movy
cd movy
cargo build --release
```
请注意,必须安装稳定的 Rust 工具链。
查看用法菜单。
```
./target/release/movy --help
```
### 将 Movy 作为库使用
将此内容添加到您的 `Cargo.toml` 中
```
movy = {git = "https://github.com/BitsLabSec/movy", branch = "master"}
```
遗憾的是,`sui` 和 `aptos` 都不在 `crates.io` 上,因此我们目前无法发布 crate,除非我们完全重新实现这两个链的 MoveVM。
### 编写不变量
要为合约编写不变量,请参阅 [counter 示例](./test-data/counter/tests/movy.move)。请注意,您需要将这一行添加到您的 `Move.toml` 中。它是测试依赖项,永远不会在链上运行。
```
[dev-dependencies]
movy = {git = "https://github.com/BitsLabSec/movy", subdir = "move/movy", rev = "master"}
```
## Roadmap
目前,`movy` 处于非常早期的 alpha 阶段,尚缺少以下功能:
- 将我们的更改上游至 [sui](https://github.com/MystenLabs/sui) 和 [aptos-core](https://github.com/aptos-labs/aptos-core)
- 完整的 Aptos 支持。(我们有一个用于此目的的私有分支,但仍在摸索一个良好的 API 设计。)
- 链上事件回测。
## 致谢
Belobog 的灵感来源于几个开创性的项目:
- [Belobog](https://github.com/abortfuzz/belobog)
- [ityfuzz](https://github.com/fuzzland/ityfuzz)
- [move-fuzzer](https://github.com/fuzzland/move-fuzzer)
- [sui-fuzzer](https://github.com/FuzzingLabs/sui-fuzzer)
- [historical-dev-inspect](https://github.com/kklas/historical-dev-inspect)
标签:Move语言, SOC Prime, 云安全监控, 区块链, 可视化界面, 开发工具, 测试框架, 静态分析