随机性 API
它能做什么:快速示例
Section titled “它能做什么:快速示例”以前如何获取随机数(不安全/笨拙的方法)
Section titled “以前如何获取随机数(不安全/笨拙的方法)”在中心化世界中,构建一个抽奖系统并从 n
个参与者中随机选出一个获胜者非常简单:后端只需调用一个随机整数采样函数(在 python 中是 random.randint(0, n-1)
,在 JS 中是 Math.floor(Math.random() * n)
).
不幸的是,在 Aptos Move 中没有等价的 random.randint()
,因此在 dApp 中实现这一功能曾经非常困难.
有些人可能会写一个合约,从区块链时间戳等不安全的来源采样随机数:
module module_owner::lottery { // ...
struct LotteryState { players: vector<address>, winner_idx: std::option::Option<u64>, }
fun load_lottery_state_mut(): &mut LotteryState { // ... }
entry fun decide_winner() { let lottery_state = load_lottery_state_mut(); let n = std::vector::length(&lottery_state.players); let winner_idx = aptos_framework::timestamp::now_microseconds() % n; lottery_state.winner_idx = std::option::some(winner_idx); }}
上述实现存在多种不安全之处:
- 恶意用户可以通过选择交易提交时间来操控结果;
- 恶意验证者可以轻松操控结果,通过选择
decide_winner
交易进入的区块.
还有一些 dApp 选择使用外部安全随机源(如 drand),但这通常流程复杂:
- 参与者约定使用随机源承诺的未来随机种子来决定获胜者.
- 随机种子揭晓后,客户端获取并在本地推导出获胜者.
- 其中一位参与者将种子和获胜者提交到链上.
module module_owner::lottery { // ...
struct LotteryState { players: vector<address>, /// 关于"未来随机性"的公开信息,通常是 VRF 公钥和输入。 seed_verifier: vector<u8>, winner_idx: std::option::Option<u64>, }
fun load_lottery_state_mut(): &mut LotteryState { // ... }
fun is_valid_seed(seed_verifier: vector<u8>, seed: vector<u8>): bool { // ... }
fun derive_winner(n: u64, seed: vector<u8>): u64 { // ... }
entry fun update_winner(winner_idx: u64, seed: vector<u8>) { let lottery_state = load_lottery_state_mut(); assert!(is_valid_seed(lottery_state.seed_verifier, seed), ERR_INVALID_SEED); let n = std::vector::length(players); let expected_winner_idx = derive_winner(n, seed); assert!(expected_winner_idx == winner_idx, ERR_INCORRECT_DERIVATION); lottery_state.winner_idx = std::option::some(winner_idx); }}
用 Aptos 随机性 API 实现简单与安全
Section titled “用 Aptos 随机性 API 实现简单与安全”使用 Aptos 随机性 API,代码实现如下:
module module_owner::lottery { // ...
struct LotteryState { players: vector<address>, winner_idx: std::option::Option<u64>, }
fun load_lottery_state_mut(): &mut Lottery { // ... }
#[randomness] entry fun decide_winner() { let lottery_state = load_lottery_state_mut(); let n = vector::length(&lottery_state.players); let winner_idx = aptos_framework::randomness::u64_range(0, n); lottery_state.winner_idx = std::option::some(winner_idx); }}
其中:
let winner_idx = aptos_framework::randomness::u64_range(0, n);
是随机性 API 调用,返回[0, n)
区间内均匀分布的 u64 整数.#[randomness]
是启用该 API 调用所需的属性.
如何使用 Aptos 随机性 API
Section titled “如何使用 Aptos 随机性 API”确保你已安装最新版 aptos-cli.
注意 undergasing 攻击
Section titled “注意 undergasing 攻击”识别依赖随机性的入口函数并使其合规
Section titled “识别依赖随机性的入口函数并使其合规”为安全起见(后文有详细讨论),随机性 API 只能在满足以下条件的入口函数中调用:
- 私有,且
- 带有
#[randomness]
注解.
现在是思考哪些用户操作需要用到随机性 API 的好时机,将其列出,并确保它们是私有且带有正确属性,如下例所示:
module module_owner::lottery { // ...
#[randomness] entry fun decide_winner() { // ... }}
运行时,调用随机性 API 时,虚拟机会检查调用栈最外层是否为带 #[randomness]
注解的私有入口函数.
否则,整个交易会被中止.
调用 API
Section titled “调用 API”这些 API 是 0x1::randomness
下的公共函数,可直接引用,如上方抽奖示例所示.
module module_owner::lottery { // ...
#[randomness] entry fun decide_winner() { // ... let winner_idx = aptos_framework::randomness::u64_range(0, n); lottery_state.winner_idx = std::option::some(winner_idx); }}
上述示例使用了 u64_range()
,但还支持许多其他基础类型.
以下是所有 API 的简要概览,其中 T
可以是 u8, u16, u32, u64, u128, u256
之一.
module aptos_framework::randomness { /// 均匀生成一个随机数。 fun u8_integer(): u8 {}
/// 均匀生成一个随机数。 fun u16_integer(): u16 {}
// fun u32_integer(), fun u64_integer() ...
/// 均匀生成 `[min_incl, max_excl)` 区间的随机数。 fun u8_range(min_incl: u8, max_excl: u8): u8 {}
/// 均匀生成 `[min_incl, max_excl)` 区间的随机数。 fun u16_range(min_incl: u16, max_excl: u16): u16 {}
// fun u32_range(), fun u64_range() ...
/// 均匀生成指定字节数的随机字节序列 /// n 为字节数 /// 若 n 为 0,返回空向量。 fun bytes(n: u64): vector<u8> {}
/// 均匀生成 `[0, 1, ..., n-1]` 的一个排列。 /// n 为字节数 /// 若 n 为 0,返回空向量。 fun permutation(n: u64): vector<u64> {}}
完整 API 函数列表及文档见 这里.
安全性注意事项
Section titled “安全性注意事项”随机性 API 功能强大,能解锁新的 dApp 设计; 但若使用不当,可能让你的 dApp 暴露于攻击之下! 以下是一些常见错误,务必避免.
在公共函数中调用随机性 API
Section titled “在公共函数中调用随机性 API”随着 dApp 复杂度提升,可能有多个入口函数需要共享依赖随机性的逻辑,并希望将其提取为辅助函数.
如下面所示,这是支持的,但需格外小心.
module module_owner::lottery { // ...
#[randomness] entry fun decide_winner_v0() { // ... decide_winner_internal(lottery_state); }
#[randomness] entry fun decide_winner_v1() { // ... decide_winner_internal(lottery_state); }
// 私有辅助函数 fun decide_winner_internal(lottery_state: &mut lottery_state) { let n = std::vector::length(&lottery_state.players); let winner_idx = aptos_framework::randomness::u64_range(0, n); lottery_state.winner_idx = std::option::some(winner_idx); }}
如果 decide_winner_internal()
被错误地标记为 public,
恶意玩家可以部署自己的合约:
- 调用
decide_winner_internal()
; - 读取抽奖结果(假设
lottery
模块有结果 getter); - 若结果不利于自己则中止. 通过反复调用自己的合约直到交易成功, 恶意用户可以操控获胜者分布(违背 dApp 开发者初衷). 这被称为 test-and-abort 攻击.
Aptos Move 编译器已更新以防止此类攻击,保障你的合约安全: 依赖随机性的 public 函数会被视为编译错误. 如果你已完成”构建 Aptos CLI”部分, 那么你的 Aptos CLI 已配备了新版编译器.
module module_owner::lottery { // 编译错误! public fun decide_winner_internal(lottery_state: &mut lottery_state) { let n = std::vector::length(&lottery_state.players); let winner_idx = aptos_framework::randomness::u64_range(0, n); lottery_state.winner_idx = std::option::some(winner_idx); }}
不推荐,但如你确实想将此类依赖随机性的函数暴露为 public,可通过注解 #[lint::allow_unsafe_randomness]
绕过编译器检查.
module module_owner::lottery { // 可以编译,但风险自负! #[lint::allow_unsafe_randomness] public fun decide_winner_internal(lottery_state: &mut lottery_state) { let n = std::vector::length(&lottery_state.players); let winner_idx = aptos_framework::randomness::u64_range(0, n); lottery_state.winner_idx = std::option::some(winner_idx); }}
Undergasing 攻击及防范方法
Section titled “Undergasing 攻击及防范方法”假设有这样一个 dApp.它定义了一个私有入口函数,用户可以:
- 抛硬币(gas 消耗:9),然后
- 若为正面获得奖励(gas 消耗:10),否则做清理(gas 消耗:100).
恶意用户可以控制其账户余额,使其最多覆盖 108 gas 单位(或设置交易参数 max_gas=108
),
这样清理分支(总 gas 消耗:110)总会因 gas 不足而中止.
用户便可反复调用入口函数直到获得奖励.
严格来说,这被称为 undergasing 攻击, 攻击者可控制入口函数剩余 gas,从而随意中止高 gas 路径, 进而操控随机数分布(即改变原本的概率分布).
它是随机的,但不是秘密
Section titled “它是随机的,但不是秘密”虽然随机性 API 模仿了你在中心化服务器上实现的标准库, 但请记住:种子是公开的,交易执行过程也是公开的, 并非所有依赖随机性的私有逻辑都能安全迁移到链上, 尤其涉及只有服务器可见的秘密时.
例如,在合约中请勿尝试如下操作:
- 用随机性 API 生成非对称密钥对,丢弃私钥,认为公钥是安全的.
- 用随机性 API 洗牌后遮盖部分牌面,认为没人知道排列顺序.
Aptogotchi Random Mint 是官方演示随机性 API 用法的 dApp.
完整 API 函数列表及文档见 这里.
部分 API 实现及单元测试见 这里.