MultiversX财产测试技能Skill multiversx-property-testing

本技能专注于使用财产测试和模糊测试技术来测试MultiversX智能合约,自动发现逻辑中的边缘案例和违反不变量情况,确保合约的鲁棒性和安全性。关键词:财产测试、模糊测试、智能合约测试、MultiversX、不变量验证、边缘案例发现。

智能合约 0 次安装 0 次浏览 更新于 3/21/2026

名称:multiversx-property-testing 描述:使用财产测试和模糊测试来发现智能合约逻辑中的边缘案例。适用于编写全面测试、验证不变量或使用随机输入搜索意外行为时。

MultiversX财产测试

注意proptest 是一个外部crate(不属于MultiversX SDK)。在您的Cargo.toml中将其添加为dev-dependency。

使用财产测试(模糊测试)自动发现MultiversX智能合约逻辑中的边缘案例和违反不变量情况。这种方法生成随机输入以发现手动测试遗漏的错误。

何时使用

  • 编写全面测试套件
  • 验证数学不变量成立
  • 测试复杂状态机
  • 发现业务逻辑中的边缘案例
  • 验证输入处理跨范围

1. 工具和设置

Rust财产测试库

proptest - 最常用:

# Cargo.toml (dev-dependencies)
[dev-dependencies]
proptest = "1.4"

cargo-fuzz - 基于LLVM的模糊测试:

# 安装
cargo install cargo-fuzz

# 初始化模糊目标
cargo fuzz init

MultiversX测试环境

[dev-dependencies]
multiversx-sc-scenario = "0.64.0"

2. 定义不变量

不变量是无论输入或状态如何都必须始终成立的属性。

常见智能合约不变量

不变量 描述 示例
守恒 部分之和等于总量 总供应量 == 所有余额之和
单调性 值只朝一个方向移动 质押金额不减除非明确解质押
边界 值保持在限制内 用户余额 <= 总供应量
一致性 相关值保持同步 白名单计数 == 白名单集合长度
幂等性 重复调用有相同效果 二次索赔返回0

在代码中定义不变量

// 不变量:总供应量等于所有余额之和
fn check_supply_invariant(state: &ContractState) -> bool {
    let total_supply = state.total_supply;
    let sum_of_balances: BigUint = state.balances.values().sum();
    total_supply == sum_of_balances
}

// 不变量:用户余额从不超出总供应量
fn check_balance_bounds(state: &ContractState, user: &Address) -> bool {
    let user_balance = state.balances.get(user).unwrap_or_default();
    user_balance <= state.total_supply
}

3. 使用proptest进行财产测试

基本财产测试

use proptest::prelude::*;

proptest! {
    #[test]
    fn test_deposit_increases_balance(amount in 1u64..1_000_000u64) {
        let mut setup = TestSetup::new();
        let initial_balance = setup.get_balance();

        setup.deposit(amount);

        let final_balance = setup.get_balance();
        prop_assert_eq!(final_balance, initial_balance + amount);
    }
}

测试多个输入

proptest! {
    #[test]
    fn test_transfer_conservation(
        sender_initial in 1000u64..1_000_000u64,
        amount in 1u64..1000u64
    ) {
        prop_assume!(amount <= sender_initial);  // 前提条件

        let mut setup = TestSetup::new();
        setup.set_balance(SENDER, sender_initial);
        setup.set_balance(RECEIVER, 0);

        let total_before = sender_initial;

        setup.transfer(SENDER, RECEIVER, amount);

        let sender_after = setup.get_balance(SENDER);
        let receiver_after = setup.get_balance(RECEIVER);
        let total_after = sender_after + receiver_after;

        // 守恒不变量
        prop_assert_eq!(total_before, total_after);
    }
}

自定义策略

use proptest::strategy::Strategy;

// 生成有效MultiversX地址
fn arb_address() -> impl Strategy<Value = String> {
    "[a-f0-9]{64}".prop_map(|hex| format!("erd1{}", &hex[..62]))
}

// 生成有效令牌金额(避免溢出)
fn arb_token_amount() -> impl Strategy<Value = BigUint> {
    (0u64..u64::MAX / 2).prop_map(BigUint::from)
}

// 生成有效令牌标识符
fn arb_token_id() -> impl Strategy<Value = String> {
    "[A-Z]{3,10}-[a-f0-9]{6}".prop_map(|s| s.to_uppercase())
}

proptest! {
    #[test]
    fn test_with_custom_strategies(
        addr in arb_address(),
        amount in arb_token_amount()
    ) {
        // 测试逻辑在此
    }
}

4. 状态财产测试

测试操作序列,而不仅仅是单个调用。

use proptest::prelude::*;
use proptest_state_machine::*;

// 定义可能操作
#[derive(Debug, Clone)]
enum Operation {
    Deposit { user: usize, amount: u64 },
    Withdraw { user: usize, amount: u64 },
    Transfer { from: usize, to: usize, amount: u64 },
}

// 模型预期状态
#[derive(Debug, Clone, Default)]
struct ModelState {
    balances: HashMap<usize, u64>,
    total_supply: u64,
}

impl ModelState {
    fn apply(&mut self, op: &Operation) -> Result<(), &'static str> {
        match op {
            Operation::Deposit { user, amount } => {
                *self.balances.entry(*user).or_default() += amount;
                self.total_supply += amount;
                Ok(())
            }
            Operation::Withdraw { user, amount } => {
                let balance = self.balances.get(user).copied().unwrap_or(0);
                if balance < *amount {
                    return Err("余额不足");
                }
                *self.balances.get_mut(user).unwrap() -= amount;
                self.total_supply -= amount;
                Ok(())
            }
            Operation::Transfer { from, to, amount } => {
                let from_balance = self.balances.get(from).copied().unwrap_or(0);
                if from_balance < *amount {
                    return Err("余额不足");
                }
                *self.balances.get_mut(from).unwrap() -= amount;
                *self.balances.entry(*to).or_default() += amount;
                Ok(())
            }
        }
    }

    fn check_invariants(&self) -> bool {
        // 守恒:余额之和 == 总供应量
        let sum: u64 = self.balances.values().sum();
        sum == self.total_supply
    }
}

proptest! {
    #[test]
    fn test_operation_sequence(ops in prop::collection::vec(arb_operation(), 0..100)) {
        let mut model = ModelState::default();
        let mut contract = TestContract::new();

        for op in ops {
            let model_result = model.apply(&op);
            let contract_result = contract.execute(&op);

            // 模型和合约应同意成功/失败
            prop_assert_eq!(model_result.is_ok(), contract_result.is_ok());

            // 每次操作后不变量应成立
            prop_assert!(model.check_invariants());
            prop_assert!(contract.check_invariants());
        }
    }
}

5. 使用cargo-fuzz进行模糊测试

设置模糊目标

// fuzz/fuzz_targets/deposit_fuzz.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
use my_contract::*;

fuzz_target!(|data: &[u8]| {
    if data.len() < 8 {
        return;
    }

    let amount = u64::from_le_bytes(data[..8].try_into().unwrap());

    let mut setup = TestSetup::new();

    // 这应该无论输入如何都不会恐慌
    let _ = setup.try_deposit(amount);

    // 不变量应始终成立
    assert!(setup.check_invariants());
});

运行模糊器

# 运行模糊测试
cargo +nightly fuzz run deposit_fuzz

# 运行特定持续时间
cargo +nightly fuzz run deposit_fuzz -- -max_total_time=300

# 运行特定种子语料库
cargo +nightly fuzz run deposit_fuzz corpus/deposit_fuzz

6. 与场景测试集成

从财产测试生成场景测试:

fn generate_scenario(ops: &[Operation]) -> String {
    let mut steps = vec![];

    for (i, op) in ops.iter().enumerate() {
        let step = match op {
            Operation::Deposit { user, amount } => {
                format!(r#"{{
                    "step": "scCall",
                    "id": "step-{}",
                    "tx": {{
                        "from": "address:user{}",
                        "to": "sc:contract",
                        "function": "deposit",
                        "egldValue": "{}",
                        "gasLimit": "5,000,000"
                    }}
                }}"#, i, user, amount)
            }
            // ... 其他操作
        };
        steps.push(step);
    }

    format!(r#"{{
        "name": "生成的财产测试",
        "steps": [{}]
    }}"#, steps.join(",
"))
}

7. 常见测试模式

溢出测试

proptest! {
    #[test]
    fn test_no_overflow_on_addition(a in 0u64..u64::MAX, b in 0u64..u64::MAX) {
        let mut setup = TestSetup::new();

        // 应优雅处理溢出
        let result = setup.try_add(a, b);

        if a.checked_add(b).is_some() {
            prop_assert!(result.is_ok());
        } else {
            prop_assert!(result.is_err());
        }
    }
}

边界测试

proptest! {
    #[test]
    fn test_boundaries(amount in prop_oneof![
        Just(0u64),           // 零
        Just(1u64),           // 最小正数
        Just(u64::MAX - 1),   // 接近最大值
        Just(u64::MAX),       // 最大值
        0u64..u64::MAX        // 随机
    ]) {
        let mut setup = TestSetup::new();
        let result = setup.try_process(amount);

        // 验证边界处的正确行为
        if amount == 0 {
            prop_assert!(result.is_err());  // 应拒绝零
        } else {
            prop_assert!(result.is_ok());
        }
    }
}

幂等性测试

proptest! {
    #[test]
    fn test_claim_idempotency(user_id in 0usize..10) {
        let mut setup = TestSetup::new();
        setup.add_rewards(user_id, 1000);

        let first_claim = setup.claim(user_id);
        let second_claim = setup.claim(user_id);

        // 第一次索赔获得奖励,第二次无
        prop_assert_eq!(first_claim, 1000);
        prop_assert_eq!(second_claim, 0);
    }
}

8. 最佳实践

  1. 从简单不变量开始:从如守恒等明显属性开始
  2. 使用缩小:proptest自动缩小失败案例为最小示例
  3. 种子您的语料库:添加已知边缘案例到模糊语料库
  4. 持续运行:财产测试应在每次提交时在CI中运行
  5. 记录不变量:每个不变量应有注释解释为何必须成立
  6. 测试失败模式:验证无效输入被正确拒绝