MSG Chain 链上随机数 VRF 实现指南
数据来源:MSG Chain 代码库核实
主网状态: No-Go — 当前 MSGChain 主网裁决为 No-Go,以下内容反映代码实际状态,不代表生产可用。
1 概述
1.1 为什么链上随机性至关重要
区块链是一个确定性状态机——给定相同的输入和状态,任何节点执行相同的交易都会得到相同的结果。这种确定性是共识的基础,但也使得"原生随机数"成为一个矛盾的概念。然而,大量去中心化应用场景依赖不可预测的随机性:
| 场景 | 随机性用途 | 安全要求 |
|---|---|---|
| NFT 盲盒铸造 | 随机分配稀有度/特征 | 高(经济价值) |
| 链上游戏(PVP/PVE) | 伤害计算、掉落判定、地图生成 | 极高(公平性) |
| 彩票/抽奖 | 中奖者选取 | 极高(资金安全) |
| DAO 治理 | 提案排序、委员会随机选取 | 中 |
| 验证者选举 | 随机分片/轮次分配 | 高 |
如果随机性机制存在漏洞,攻击者可以:
- 仅铸造有稀有度的 NFT(选择性铸造攻击)
- 预测并操纵游戏结果
- 在对自己有利时参与/退出抽奖
1.2 MSG Chain 的 DAR 共识
MSG Chain 采用 DAR (Dilithium-based Autonomous Randomness) 共识机制,其核心创新在于:
- 后量子安全:基于 CRYSTALS-Dilithium 5 数字签名方案,抵抗量子计算攻击
- 自主随机性:每个区块提议者在区块生成时包含一个 VRF 证明,无需外部 Oracle
- 无偏性:随机性来源分散在多个验证者之间,单个验证者无法偏斜输出
- 即时可用:随机种子在区块头中即可获取,无需等待额外的提交-揭示轮次
1.3 VRF 概览
可验证随机函数 (Verifiable Random Function, VRF) 是一种密码学原语,它接受一个输入并产生一个随机输出,同时生成一个证明,允许任何人验证该输出确实是由特定公钥对应的私钥正确生成的。
VRF 的数学定义:
(π, β) = VRF_Eval(SK, α) // 评估:私钥 SK 对输入 α 生成证明 π 和输出 β
b = VRF_Verify(PK, α, π, β) // 验证:公钥 PK 下,α→β 是否由 π 证明
VRF 的三个核心性质:
- 唯一性 (Unique):对给定的 (PK, α),只有唯一的 β 是有效的
- 伪随机性 (Pseudorandom):没有私钥的情况下,输出 β 与随机数不可区分
- 可验证性 (Verifiable):任何持有公钥的人都可以验证输出的正确性
1.4 VRF vs 其他链上随机方案
| 方案 | 可预测攻击 | 验证者偏斜 | 延迟 | 成本 | 后量子安全 |
|---|---|---|---|---|---|
| Block Hash | ❌ 矿工可操纵 | ❌ | 低 | 无 | ✅ |
| Commit-Reveal | ✅ | ❌ 最后一个揭示者可偏斜 | 高(多轮) | 中 | ✅ |
| Oracle (Chainlink) | ✅ | ✅ | 中 | 高(LINK 费用) | ❌ 部分 |
| DAR (本链原生) | ✅ | ✅ Dilithium-5 VRF | 低(区块级别) | 无 | ✅ |
| 本指南(DAR+Oracle) | ✅ | ✅ | 低 | 低 | ✅ |
1.5 本指南涵盖的内容
- 第一层:DAR 共识原生 — 直接使用区块随机性,无需额外信任假设,零额外 Gas 成本,即时可用
- 第二层:VRF Oracle 回退 — VRF Oracle 合约,跨链/离链随机源,可审计可验证,自定义随机策略
1.6 MSG Chain 开发环境准备
# 安装 msg-chain-devkit
cargo install msg-chain-devkit
# 创建项目
msg-chain-devkit new my-vrf-project
cd my-vrf-project
# 合约目录结构
# contracts/
# vrf-oracle/
# src/
# contract.rs # 合约入口
# msg.rs # 消息类型
# state.rs # 合约状态
# vrf.rs # VRF 验证逻辑
# Cargo.toml
# random-consumer/
# src/
# contract.rs
# msg.rs
# state.rs
# Cargo.toml
2 DAR 共识随机性
2.1 DAR 工作原理
DAR(Dilithium-based Autonomous Randomness)是 MSG Chain 共识层的核心组件。与以太坊的 RANDAO 或 Polkadot 的 BABE 不同,DAR 使用后量子安全的 Dilithium-5 作为 VRF 基础原语。
2.1.1 随机性生成流程
区块高度 H
|
├─ 输入: prev_randomness (H-1 的随机种子)
│ round_number (当前轮次)
│ proposer_address (提议者地址)
│ timestamp (区块时间戳)
|
├─ VRF_Eval(SK_proposer, input)
│ |
│ ├─ π = VRF_proof (纳入区块头)
│ └─ β = VRF_output (H 的随机种子)
|
├─ 区块头额外字段:
│ ├─ randomness: [u8; 32] // VRF 输出 (256 bits)
│ ├─ vrf_proof: [u8; 2700] // Dilithium-5 VRF 证明
│ └─ vrf_pubkey: [u8; 1312] // 提议者 Dilithium-5 公钥
|
└─ 验证者接收到区块:
├─ VRF_Verify(pubkey, input, π, β)
└─ 验证通过 → 更新本地随机种子
2.1.2 Dilithium-5 在 DAR 中的参数
| 参数 | 值 | 说明 |
|---|---|---|
| 安全级别 | NIST Level 5 | 与 AES-256 同等安全 |
| 公钥大小 | 1,312 字节 | |
| 私钥大小 | 2,560 字节 | |
| 签名/证明大小 | ~2,700 字节 | VRF 证明在此约束内 |
| 底层困难问题 | Module-LWE + Module-SIS | 格密码学 |
2.2 在合约中访问 DAR 随机性
MSG Chain 的 CosmWasm 实现扩展了 QuerierWrapper,提供了直接访问 DAR 随机性的接口。
2.2.1 标准 DAR API
use cosmwasm_std::{
from_binary, Binary, Deps, DepsMut, Env, MessageInfo,
Response, StdError, StdResult, Storage,
};
use serde::{Deserialize, Serialize};
/// DAR 随机性查询响应
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct DarRandomnessResponse {
/// 当前区块的随机种子 (32 字节)
pub randomness: Binary,
/// VRF 证明 (用于验证)
pub vrf_proof: Binary,
/// 提议者的 Dilithium-5 公钥
pub pubkey: Binary,
/// 区块高度
pub height: u64,
/// 区块时间戳
pub timestamp: u64,
}
/// 通过环境变量获取 DAR 随机性
pub fn get_dar_randomness(env: &Env) -> StdResult<[u8; 32]> {
// MSG Chain 扩展:env.block.randomness() 返回 Option<Binary>
match &env.block.randomness {
Some(r) => {
let bytes: &[u8] = r.as_slice();
if bytes.len() != 32 {
return Err(StdError::generic_err(
format!("DAR randomness length mismatch: expected 32, got {}", bytes.len())
));
}
let mut seed = [0u8; 32];
seed.copy_from_slice(&bytes[..32]);
Ok(seed)
}
None => Err(StdError::generic_err(
"DAR randomness not available in this block. Ensure you are on MSG Chain with DAR consensus."
)),
}
}
/// 通过查询获取 DAR 随机性(指定区块)
pub fn query_dar_randomness(deps: &Deps, height: Option<u64>) -> StdResult<DarRandomnessResponse> {
let request = DarRandomnessQuery {
height: height.unwrap_or(0),
};
let raw = deps.querier.query(&cosmwasm_std::QueryRequest::Custom(
DarQueryMsg::Randomness(request).into(),
))?;
from_binary(&raw)
}
2.2.2 实际合约示例:简单的随机数门限合约
use cosmwasm_std::{
attr, ensure, Addr, Binary, DepsMut, Env, MessageInfo,
Response, StdResult, Uint128,
};
use cw_storage_plus::Item;
const THRESHOLD: Item<Uint128> = Item::new("threshold");
pub fn execute_set_threshold(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
threshold: Uint128,
) -> StdResult<Response> {
THRESHOLD.save(deps.storage, &threshold)?;
Ok(Response::new().add_attribute("method", "set_threshold"))
}
pub fn execute_random_draw(
deps: DepsMut,
env: Env,
info: MessageInfo,
) -> StdResult<Response> {
// Step 1: 获取 DAR 随机种子
let seed = get_dar_randomness(&env)?;
// Step 2: 结合调用者地址和当前时间戳派生随机值
let entropy = (seed, info.sender.as_bytes(), env.block.time.nanos());
let random_value = compute_random_uint128(&entropy);
// Step 3: 与门限比较
let threshold = THRESHOLD.load(deps.storage)?;
let won = random_value < threshold;
Ok(Response::new()
.add_attribute("method", "random_draw")
.add_attribute("sender", info.sender)
.add_attribute("random_value", random_value)
.add_attribute("threshold", threshold)
.add_attribute("won", won.to_string()))
}
/// 从熵元组计算 Uint128 随机数
fn compute_random_uint128(entropy: &([u8; 32], &[u8], u64)) -> Uint128 {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(entropy.0); // DAR 种子
hasher.update(entropy.1); // 调用者地址
hasher.update(entropy.2.to_le_bytes()); // 纳秒时间戳
let hash = hasher.finalize();
let mut bytes = [0u8; 16];
bytes.copy_from_slice(&hash[..16]);
Uint128::new(u128::from_le_bytes(bytes))
}
2.3 DAR 随机性验证(离链)
虽然合约内可以直接信任区块头中的 randomness 字段,但 dApp 前端或者需要额外安全保证的场景下,可以离线验证 DAR 的 VRF 证明。
2.3.1 Python 验证示例
"""
DAR VRF 证明验证
依赖: pip install msg-chain-sdk cryptography
"""
import hashlib
import struct
from typing import Tuple
from msg_chain_sdk.dilithium import Dilithium5
from msg_chain_sdk.vrf import Vrf
def verify_dar_randomness(
block_header: dict,
prev_randomness: bytes,
proposer_pubkey: bytes,
) -> Tuple[bool, bytes]:
"""
验证 DAR 区块随机性。
Args:
block_header: 区块头字典,包含 randomness, vrf_proof, height, timestamp
prev_randomness: 前一个区块的随机种子 (32 字节)
proposer_pubkey: 提议者 Dilithium-5 公钥 (1312 字节)
Returns:
(验证是否通过, 当前区块随机种子)
"""
randomness = bytes.fromhex(block_header["randomness"])
vrf_proof = bytes.fromhex(block_header["vrf_proof"])
height = block_header["height"]
timestamp = block_header["timestamp"]
proposer = block_header["proposer_address"]
# 重建 VRF 输入
input_seed = hashlib.sha256(
prev_randomness
+ height.to_bytes(8, "big")
+ proposer.encode("utf-8")
+ timestamp.to_bytes(8, "big")
).digest()
# 验证 VRF
vrf = Vrf(Dilithium5())
is_valid = vrf.verify(
public_key=proposer_pubkey,
alpha=input_seed,
pi=vrf_proof,
beta=randomness,
)
if is_valid:
print(f"[DAR Verify] ✅ 区块 {height} 随机性验证通过")
else:
print(f"[DAR Verify] ❌ 区块 {height} 随机性验证失败")
return is_valid, randomness
def verify_dar_chain(
blocks: list[dict],
genesis_randomness: bytes,
) -> bool:
"""
验证从创世区块开始的整个 DAR 随机性链。
Args:
blocks: 按高度排序的区块头列表
genesis_randomness: 创世区块随机种子
Returns:
整条链的随机性是否全部有效
"""
prev = genesis_randomness
for block in blocks:
ok, curr = verify_dar_randomness(block, prev, ...)
if not ok:
return False
prev = curr
return True
2.3.2 TypeScript 验证示例
import { Dilithium5, Vrf } from '@msg-chain/crypto';
import { BlockHeader } from '@msg-chain/client';
import { sha256 } from '@noble/hashes/sha256';
import { concatBytes, bytesToHex } from '@noble/hashes/utils';
interface DarBlockHeader extends BlockHeader {
randomness: string;
vrfProof: string;
vrfPubkey: string;
proposerAddress: string;
}
async function verifyDarRandomness(
header: DarBlockHeader,
prevRandomness: Uint8Array,
): Promise<{ valid: boolean; seed: Uint8Array }> {
const heightBytes = new Uint8Array(8);
new DataView(heightBytes.buffer).setBigUint64(0, BigInt(header.height), false);
const timestampBytes = new Uint8Array(8);
new DataView(timestampBytes.buffer).setBigUint64(
0, BigInt(header.timestamp), false,
);
const inputSeed = sha256(
concatBytes(
prevRandomness,
heightBytes,
new TextEncoder().encode(header.proposerAddress),
timestampBytes,
),
);
const vrf = new Vrf(new Dilithium5());
const valid = vrf.verify(
Uint8Array.from(Buffer.from(header.vrfPubkey, 'hex')),
inputSeed,
Uint8Array.from(Buffer.from(header.vrfProof, 'hex')),
Uint8Array.from(Buffer.from(header.randomness, 'hex')),
);
return {
valid,
seed: Uint8Array.from(Buffer.from(header.randomness, 'hex')),
};
}
2.4 DAR 随机性的安全保证
2.4.1 无偏性 (Bias Resistance)
DAR 的 VRF 输出无法被任何单一实体偏斜。
- 提议者偏斜:VRF 的伪随机性保证,即使提议者恶意选择输入,输出仍均匀分布
- 验证者集体偏斜:Tendermint 风格的 BFT 共识保证,只要 < 1/3 验证者是恶意的,无法影响随机性
- 区块回退攻击:如果提议者不喜欢产生的随机种子,它无法"撤回"区块;一旦区块被 2/3+ 验证者签名,即最终确认
2.4.2 不可预测性 (Unpredictability)
| 攻击者模型 | 能否预测 β | 说明 |
|---|---|---|
| 外部用户 | ❌ 不能 | 没有 VRF 私钥,无法预计算输出 |
| 区块提议者 | ❌ 不能提前 | 知道 β 但无法改变已提交的区块 |
| 下一个提议者 | ❌ 不能提前 | 上一个随机种子作为输入的一部分 |
| 全网(事后) | ✅ 可验证 | 所有人在区块确认后可验证 |
2.5 何时应使用 DAR 原生随机性
适合 DAR 的场景
/// NFT 盲盒铸造 - 使用 DAR 是理想选择
pub fn execute_mint_nft(deps: DepsMut, env: Env, info: MessageInfo) -> StdResult<Response> {
let seed = get_dar_randomness(&env)?;
let traits = derive_traits(seed, info.sender.as_ref(), env.block.time.nanos());
Ok(Response::new()
.add_attribute("action", "mint_nft")
.add_attribute("traits", traits.to_string()))
}
/// DAO 投票随机排序 - DAR 足够安全
pub fn execute_shuffle_proposals(deps: DepsMut, env: Env) -> StdResult<Response> {
let seed = get_dar_randomness(&env)?;
let shuffled = shuffle(proposals, seed);
// ...
}
不适合 DAR 的场景(需要 Oracle 回退)
/// 高价值彩票 - 攻击者可能通过影响提议者选择来偏斜
/// 建议:DAR + VRF Oracle 双重验证
pub fn execute_lottery_draw(deps: DepsMut, env: Env) -> StdResult<Response> {
let dar_seed = get_dar_randomness(&env)?;
let oracle_seed = VRF_ORACLE.load(deps.storage)?.last_random;
let combined = combine_seeds(dar_seed, oracle_seed.to_be_bytes());
// ...
}
3 VRF Oracle 合约实现
当应用对随机性的安全要求极高(例如大型彩票、高价值 NFT),或者需要可审计的随机性来源时,可以使用基于 Oracle 的 VRF 方案作为 DAR 的补充或回退。
3.1 架构设计
+----------+ +-------------------+ +--------------+
| Consumer | | VRF Oracle | | Oracle Node |
| (dApp) |----->| (Contract) |<---->| (Off-chain) |
| | | | | |
| 1. 请求 |----->| 2. 存储请求 | | 3. 监听事件 |
| | | 4. VRF Request |<-----| 5. 计算 VRF |
| 7. 消费 |<-----| 6. 存储随机数 |----->| (Dilithium)|
| | | 8. 验证 VRF | | |
+----------+ +-------------------+ +--------------+
3.2 完整的 VRF Oracle 合约
3.2.1 合约状态 (state.rs)
use cosmwasm_std::{Addr, Binary, Timestamp, Uint128};
use cw_storage_plus::{Item, Map};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// VRF Oracle 合约配置
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Config {
/// 管理员的 Dilithium-5 公钥(用于 VRF 验证)
pub pubkey: Binary,
/// Oracle 节点地址(允许提交 VRF 结果)
pub oracle_address: Addr,
/// 请求超时(以秒为单位)
pub request_timeout: u64,
/// 最大待处理请求数
pub max_pending_requests: u32,
/// 每次请求费用
pub fee: Uint128,
}
/// VRF 请求
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct VrfRequest {
/// 请求 ID(自增)
pub id: u64,
/// 请求者地址
pub requester: Addr,
/// 请求者指定的种子(可选,用于防重放)
pub seed: Binary,
/// 请求时区块高度
pub request_height: u64,
/// 请求时间戳
pub request_time: Timestamp,
/// 是否已完成(已生成随机数)
pub fulfilled: bool,
/// 回调合约地址(谁消费这个随机数)
pub callback_contract: Option<Addr>,
/// 回调消息(Base64 编码的 JSON)
pub callback_msg: Option<Binary>,
}
/// VRF 响应
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct VrfResponse {
/// 对应的请求 ID
pub request_id: u64,
/// VRF 输出(随机数)
pub randomness: Binary,
/// VRF 证明
pub proof: Binary,
/// 提交此结果的 Oracle 地址
pub submitter: Addr,
/// 提交时的区块高度
pub submit_height: u64,
}
/// 合约全局状态
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct State {
/// 当前请求计数器
pub request_count: u64,
/// 已完成的请求数
pub fulfilled_count: u64,
}
pub const CONFIG: Item<Config> = Item::new("config");
pub const STATE: Item<State> = Item::new("state");
pub const REQUESTS: Map<u64, VrfRequest> = Map::new("requests");
pub const RESPONSES: Map<u64, VrfResponse> = Map::new("responses");
pub const PENDING_BY_REQUESTER: Map<&Addr, Vec<u64>> = Map::new("pending");
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct RandomnessResult {
pub request_id: u64,
pub randomness: Binary,
pub proof: Binary,
pub pubkey: Binary,
}
3.2.2 消息类型 (msg.rs)
use cosmwasm_std::{Addr, Binary, Uint128};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
// ========== 实例化 ==========
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
pub pubkey: Binary,
pub oracle_address: String,
pub request_timeout: u64,
pub fee: Option<Uint128>,
pub max_pending_requests: Option<u32>,
}
// ========== 执行消息 ==========
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
RequestRandomness {
seed: Option<Binary>,
callback_contract: Option<String>,
callback_msg: Option<Binary>,
},
FulfillRandomness {
request_id: u64,
randomness: Binary,
proof: Binary,
},
UpdateConfig {
pubkey: Option<Binary>,
oracle_address: Option<String>,
request_timeout: Option<u64>,
fee: Option<Uint128>,
},
CancelExpiredRequest {
request_id: u64,
},
}
// ========== 查询消息 ==========
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
GetConfig {},
GetRequest { request_id: u64 },
GetResponse { request_id: u64 },
GetPendingRequests { requester: String },
GetLatestRandomness {},
VerifyRandomness {
request_id: u64,
randomness: Binary,
proof: Binary,
},
}
// ========== 查询响应 ==========
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct ConfigResponse {
pub pubkey: Binary,
pub oracle_address: Addr,
pub request_timeout: u64,
pub max_pending_requests: u32,
pub fee: Uint128,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct PendingRequestsResponse {
pub requests: Vec<u64>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct LatestRandomnessResponse {
pub request_id: u64,
pub randomness: Binary,
pub height: u64,
pub timestamp: u64,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct VerifyResponse {
pub valid: bool,
}
3.2.3 合约实现 (contract.rs)
use cosmwasm_std::{
attr, to_binary, Binary, Deps, DepsMut, Env,
MessageInfo, Reply, Response, StdError, StdResult,
SubMsg, Timestamp, Uint128, WasmMsg, CosmosMsg,
};
use crate::msg::{
ConfigResponse, ExecuteMsg, InstantiateMsg, LatestRandomnessResponse,
PendingRequestsResponse, QueryMsg, VerifyResponse,
};
use crate::state::{
Config, RandomnessResult, State, VrfRequest, VrfResponse,
CONFIG, PENDING_BY_REQUESTER, REQUESTS, RESPONSES, STATE,
};
use crate::vrf::verify_vrf;
const DEFAULT_MAX_PENDING: u32 = 100;
pub fn instantiate(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
if msg.pubkey.len() != 1312 {
return Err(StdError::generic_err(format!(
"Invalid Dilithium-5 public key length: expected 1312, got {}",
msg.pubkey.len()
)));
}
let oracle_address = deps.api.addr_validate(&msg.oracle_address)?;
let config = Config {
pubkey: msg.pubkey,
oracle_address,
request_timeout: msg.request_timeout,
max_pending_requests: msg.max_pending_requests.unwrap_or(DEFAULT_MAX_PENDING),
fee: msg.fee.unwrap_or(Uint128::zero()),
};
let state = State {
request_count: 0,
fulfilled_count: 0,
};
CONFIG.save(deps.storage, &config)?;
STATE.save(deps.storage, &state)?;
Ok(Response::new()
.add_attribute("method", "instantiate")
.add_attribute("oracle", msg.oracle_address))
}
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> StdResult<Response> {
match msg {
ExecuteMsg::RequestRandomness { seed, callback_contract, callback_msg } => {
execute_request_randomness(deps, env, info, seed, callback_contract, callback_msg)
}
ExecuteMsg::FulfillRandomness { request_id, randomness, proof } => {
execute_fulfill_randomness(deps, env, info, request_id, randomness, proof)
}
ExecuteMsg::UpdateConfig { pubkey, oracle_address, request_timeout, fee } => {
execute_update_config(deps, env, info, pubkey, oracle_address, request_timeout, fee)
}
ExecuteMsg::CancelExpiredRequest { request_id } => {
execute_cancel_request(deps, env, info, request_id)
}
}
}
pub fn execute_request_randomness(
deps: DepsMut,
env: Env,
info: MessageInfo,
seed: Option<Binary>,
callback_contract: Option<String>,
callback_msg: Option<Binary>,
) -> StdResult<Response> {
let config = CONFIG.load(deps.storage)?;
let mut state = STATE.load(deps.storage)?;
if !config.fee.is_zero() && info.funds.iter().all(|c| c.denom != "umsg" || c.amount < config.fee) {
return Err(StdError::generic_err(format!(
"Insufficient fee: required {}", config.fee
)));
}
let pending = PENDING_BY_REQUESTER
.may_load(deps.storage, &info.sender)?
.unwrap_or_default();
if pending.len() as u32 >= config.max_pending_requests {
return Err(StdError::generic_err(
"Maximum pending requests reached",
));
}
state.request_count += 1;
let request_id = state.request_count;
let callback_addr = match &callback_contract {
Some(addr) => Some(deps.api.addr_validate(addr)?),
None => None,
};
let mut entropy = Vec::new();
if let Some(r) = &env.block.randomness {
entropy.extend_from_slice(r.as_slice());
}
entropy.extend_from_slice(info.sender.as_bytes());
if let Some(ref s) = seed {
entropy.extend_from_slice(s.as_slice());
}
entropy.extend_from_slice(&request_id.to_be_bytes());
let derived_seed = Binary::from(sha2::Sha256::digest(&entropy).as_slice());
let request = VrfRequest {
id: request_id,
requester: info.sender.clone(),
seed: derived_seed,
request_height: env.block.height,
request_time: env.block.time,
fulfilled: false,
callback_contract: callback_addr,
callback_msg,
};
REQUESTS.save(deps.storage, request_id, &request)?;
let mut pending = PENDING_BY_REQUESTER
.may_load(deps.storage, &info.sender)?
.unwrap_or_default();
pending.push(request_id);
PENDING_BY_REQUESTER.save(deps.storage, &info.sender, &pending)?;
STATE.save(deps.storage, &state)?;
Ok(Response::new()
.add_attribute("method", "request_randomness")
.add_attribute("request_id", request_id.to_string())
.add_attribute("requester", info.sender)
.add_attribute("seed", derived_seed.to_hex())
.add_event(
cosmwasm_std::Event::new("vrf-request")
.add_attribute("request_id", request_id.to_string())
.add_attribute("requester", info.sender)
.add_attribute("seed", derived_seed.to_hex()),
))
}
pub fn execute_fulfill_randomness(
deps: DepsMut,
env: Env,
info: MessageInfo,
request_id: u64,
randomness: Binary,
proof: Binary,
) -> StdResult<Response> {
let config = CONFIG.load(deps.storage)?;
if info.sender != config.oracle_address {
return Err(StdError::generic_err(
"Only the configured oracle address can fulfill requests",
));
}
let mut request = REQUESTS
.load(deps.storage, request_id)
.map_err(|_| StdError::generic_err(format!("Request {} not found", request_id)))?;
if request.fulfilled {
return Err(StdError::generic_err(format!(
"Request {} already fulfilled", request_id
)));
}
let elapsed = env.block.height - request.request_height;
if elapsed > config.request_timeout {
return Err(StdError::generic_err(format!(
"Request {} has expired after {} blocks", request_id, elapsed
)));
}
// ---- 验证 VRF 证明 ----
let mut alpha_bytes = Vec::new();
alpha_bytes.extend_from_slice(request.seed.as_slice());
alpha_bytes.extend_from_slice(&request_id.to_be_bytes());
alpha_bytes.extend_from_slice(request.requester.as_bytes());
let alpha = sha2::Sha256::digest(&alpha_bytes);
let valid = verify_vrf(
config.pubkey.as_slice(),
alpha.as_slice(),
proof.as_slice(),
randomness.as_slice(),
);
if !valid {
return Err(StdError::generic_err(
"VRF proof verification failed",
));
}
// ---- 验证通过 ----
request.fulfilled = true;
REQUESTS.save(deps.storage, request_id, &request)?;
let response = VrfResponse {
request_id,
randomness: randomness.clone(),
proof: proof.clone(),
submitter: info.sender.clone(),
submit_height: env.block.height,
};
RESPONSES.save(deps.storage, request_id, &response)?;
let mut pending = PENDING_BY_REQUESTER
.load(deps.storage, &request.requester)?;
pending.retain(|&id| id != request_id);
if pending.is_empty() {
PENDING_BY_REQUESTER.remove(deps.storage, &request.requester);
} else {
PENDING_BY_REQUESTER.save(deps.storage, &request.requester, &pending)?;
}
let mut state = STATE.load(deps.storage)?;
state.fulfilled_count += 1;
STATE.save(deps.storage, &state)?;
// ---- 可选回调 ----
let mut msgs: Vec<SubMsg> = Vec::new();
if let (Some(ref callback_addr), Some(_)) =
(&request.callback_contract, &request.callback_msg)
{
let exec_msg = WasmMsg::Execute {
contract_addr: callback_addr.to_string(),
msg: Binary::from(
serde_json::to_vec(&serde_json::json!({
"randomness_callback": {
"request_id": request_id,
"randomness": randomness.to_hex(),
"proof": proof.to_hex(),
"pubkey": config.pubkey.to_hex(),
}
}))
.map_err(|e| StdError::generic_err(format!("JSON encoding: {}", e)))?,
),
funds: vec![],
};
msgs.push(SubMsg::new(exec_msg));
}
Ok(Response::new()
.add_submessages(msgs)
.add_attribute("method", "fulfill_randomness")
.add_attribute("request_id", request_id.to_string())
.add_attribute("randomness", randomness.to_hex())
.add_attribute("submitter", info.sender)
.add_event(
cosmwasm_std::Event::new("vrf-fulfilled")
.add_attribute("request_id", request_id.to_string())
.add_attribute("randomness", randomness.to_hex()),
))
}
pub fn execute_update_config(
deps: DepsMut,
_env: Env,
info: MessageInfo,
pubkey: Option<Binary>,
oracle_address: Option<String>,
request_timeout: Option<u64>,
fee: Option<Uint128>,
) -> StdResult<Response> {
let mut config = CONFIG.load(deps.storage)?;
if info.sender != config.oracle_address {
return Err(StdError::generic_err(
"Only the oracle address can update configuration",
));
}
if let Some(pk) = pubkey {
if pk.len() != 1312 {
return Err(StdError::generic_err("Invalid Dilithium-5 public key length"));
}
config.pubkey = pk;
}
if let Some(addr) = oracle_address {
config.oracle_address = deps.api.addr_validate(&addr)?;
}
if let Some(t) = request_timeout {
config.request_timeout = t;
}
if let Some(f) = fee {
config.fee = f;
}
CONFIG.save(deps.storage, &config)?;
Ok(Response::new().add_attribute("method", "update_config"))
}
pub fn execute_cancel_request(
deps: DepsMut,
env: Env,
info: MessageInfo,
request_id: u64,
) -> StdResult<Response> {
let config = CONFIG.load(deps.storage)?;
let request = REQUESTS.load(deps.storage, request_id)?;
if info.sender != request.requester && info.sender != config.oracle_address {
return Err(StdError::generic_err(
"Only the requester or oracle can cancel this request",
));
}
if request.fulfilled {
return Err(StdError::generic_err("Cannot cancel an already fulfilled request"));
}
let elapsed = env.block.height - request.request_height;
if elapsed < config.request_timeout && info.sender == request.requester {
return Err(StdError::generic_err(format!(
"Request {} has not expired yet", request_id
)));
}
let mut pending = PENDING_BY_REQUESTER
.load(deps.storage, &request.requester)?;
pending.retain(|&id| id != request_id);
if pending.is_empty() {
PENDING_BY_REQUESTER.remove(deps.storage, &request.requester);
} else {
PENDING_BY_REQUESTER.save(deps.storage, &request.requester, &pending)?;
}
let cancelled = VrfResponse {
request_id,
randomness: Binary::default(),
proof: Binary::default(),
submitter: info.sender.clone(),
submit_height: env.block.height,
};
RESPONSES.save(deps.storage, request_id, &cancelled)?;
Ok(Response::new()
.add_attribute("method", "cancel_request")
.add_attribute("request_id", request_id.to_string()))
}
// ========== 查询入口 ==========
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::GetConfig {} => to_binary(&query_config(deps)?),
QueryMsg::GetRequest { request_id } => to_binary(&query_request(deps, request_id)?),
QueryMsg::GetResponse { request_id } => to_binary(&query_response(deps, request_id)?),
QueryMsg::GetPendingRequests { requester } => {
to_binary(&query_pending_requests(deps, requester)?)
}
QueryMsg::GetLatestRandomness {} => to_binary(&query_latest_randomness(deps)?),
QueryMsg::VerifyRandomness { request_id, randomness, proof } => {
to_binary(&query_verify_randomness(deps, request_id, randomness, proof)?)
}
}
}
pub fn query_config(deps: Deps) -> StdResult<ConfigResponse> {
let config = CONFIG.load(deps.storage)?;
Ok(ConfigResponse {
pubkey: config.pubkey,
oracle_address: config.oracle_address,
request_timeout: config.request_timeout,
max_pending_requests: config.max_pending_requests,
fee: config.fee,
})
}
pub fn query_request(deps: Deps, request_id: u64) -> StdResult<VrfRequest> {
REQUESTS.load(deps.storage, request_id)
}
pub fn query_response(deps: Deps, request_id: u64) -> StdResult<VrfResponse> {
RESPONSES.load(deps.storage, request_id)
}
pub fn query_pending_requests(deps: Deps, requester: String) -> StdResult<PendingRequestsResponse> {
let addr = deps.api.addr_validate(&requester)?;
let ids = PENDING_BY_REQUESTER
.may_load(deps.storage, &addr)?
.unwrap_or_default();
Ok(PendingRequestsResponse { requests: ids })
}
pub fn query_latest_randomness(deps: Deps) -> StdResult<LatestRandomnessResponse> {
let state = STATE.load(deps.storage)?;
if state.fulfilled_count == 0 {
return Err(StdError::generic_err("No fulfilled requests yet"));
}
let mut latest_id = state.request_count;
loop {
if let Ok(resp) = RESPONSES.may_load(deps.storage, latest_id)? {
if !resp.randomness.is_empty() {
return Ok(LatestRandomnessResponse {
request_id: resp.request_id,
randomness: resp.randomness,
height: resp.submit_height,
timestamp: 0,
});
}
}
if latest_id == 0 {
break;
}
latest_id -= 1;
}
Err(StdError::generic_err("No fulfilled requests found"))
}
pub fn query_verify_randomness(
deps: Deps,
_request_id: u64,
randomness: Binary,
proof: Binary,
) -> StdResult<VerifyResponse> {
let config = CONFIG.load(deps.storage)?;
let valid = verify_vrf(
config.pubkey.as_slice(),
randomness.as_slice(),
proof.as_slice(),
randomness.as_slice(),
);
Ok(VerifyResponse { valid })
}
3.2.4 合约入口 (lib.rs)
pub mod contract;
pub mod msg;
pub mod state;
pub mod vrf;
use cosmwasm_std::{
entry_point, Binary, Deps, DepsMut, Env, MessageInfo,
Response, StdResult,
};
use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
#[entry_point]
pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
contract::instantiate(deps, env, info, msg)
}
#[entry_point]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> StdResult<Response> {
contract::execute(deps, env, info, msg)
}
#[entry_point]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
contract::query(deps, env, msg)
}
3.3 VRF Oracle 测试
#[cfg(test)]
mod tests {
use cosmwasm_std::testing::{
mock_dependencies, mock_env, mock_info,
MockApi, MockQuerier, MockStorage,
};
use cosmwasm_std::{
from_binary, Addr, BlockInfo, ContractInfo, Env,
MessageInfo, Timestamp, TransactionInfo,
};
use crate::contract::*;
use crate::msg::*;
use crate::state::*;
const DILITHIUM5_PUBKEY: &[u8; 1312] = &[0u8; 1312];
fn setup_env(height: u64, timestamp: u64) -> Env {
Env {
block: BlockInfo {
height,
time: Timestamp::from_nanos(timestamp),
chain_id: "msg-chain-1".to_string(),
randomness: Some(Binary::from([0u8; 32])),
},
contract: ContractInfo {
address: Addr::unchecked("contract0"),
},
transaction: Some(TransactionInfo { index: 0 }),
..mock_env()
}
}
#[test]
fn test_instantiate() {
let mut deps = mock_dependencies();
let env = setup_env(1, 1_000_000_000);
let msg = InstantiateMsg {
pubkey: Binary::from(DILITHIUM5_PUBKEY.as_slice()),
oracle_address: "oracle".to_string(),
request_timeout: 100,
fee: None,
max_pending_requests: None,
};
let info = mock_info("admin", &[]);
let res = instantiate(deps.as_mut(), env, info, msg).unwrap();
assert_eq!(res.attributes[0].value, "instantiate");
}
#[test]
fn test_request_randomness() {
let mut deps = mock_dependencies();
let env = setup_env(1, 1_000_000_000);
let init_msg = InstantiateMsg {
pubkey: Binary::from(DILITHIUM5_PUBKEY.as_slice()),
oracle_address: "oracle".to_string(),
request_timeout: 100,
fee: None,
max_pending_requests: None,
};
instantiate(deps.as_mut(), env.clone(), mock_info("admin", &[]), init_msg).unwrap();
let req_msg = ExecuteMsg::RequestRandomness {
seed: None,
callback_contract: None,
callback_msg: None,
};
let info = mock_info("user1", &[]);
let res = execute(deps.as_mut(), env.clone(), info, req_msg).unwrap();
assert_eq!(res.attributes[1].value, "1");
let query_resp: VrfRequest = from_binary(
&query(deps.as_ref(), env.clone(), QueryMsg::GetRequest { request_id: 1 }).unwrap(),
).unwrap();
assert_eq!(query_resp.id, 1);
assert!(!query_resp.fulfilled);
}
#[test]
fn test_fulfill_randomness_wrong_sender() {
let mut deps = mock_dependencies();
let env = setup_env(1, 1_000_000_000);
let init_msg = InstantiateMsg {
pubkey: Binary::from(DILITHIUM5_PUBKEY.as_slice()),
oracle_address: "oracle".to_string(),
request_timeout: 100,
fee: None,
max_pending_requests: None,
};
instantiate(deps.as_mut(), env.clone(), mock_info("admin", &[]), init_msg).unwrap();
execute(
deps.as_mut(),
env.clone(),
mock_info("user1", &[]),
ExecuteMsg::RequestRandomness {
seed: None,
callback_contract: None,
callback_msg: None,
},
).unwrap();
let result = execute(
deps.as_mut(),
env.clone(),
mock_info("attacker", &[]),
ExecuteMsg::FulfillRandomness {
request_id: 1,
randomness: Binary::from([1u8; 32]),
proof: Binary::from([2u8; 2700]),
},
);
assert!(result.is_err());
}
#[test]
fn test_expired_request_cancellation() {
let mut deps = mock_dependencies();
let env = setup_env(1, 1_000_000_000);
let init_msg = InstantiateMsg {
pubkey: Binary::from(DILITHIUM5_PUBKEY.as_slice()),
oracle_address: "oracle".to_string(),
request_timeout: 10,
fee: None,
max_pending_requests: None,
};
instantiate(deps.as_mut(), env.clone(), mock_info("admin", &[]), init_msg).unwrap();
execute(
deps.as_mut(),
env.clone(),
mock_info("user1", &[]),
ExecuteMsg::RequestRandomness {
seed: None,
callback_contract: None,
callback_msg: None,
},
).unwrap();
let future_env = setup_env(21, 2_000_000_000);
let res = execute(
deps.as_mut(),
future_env.clone(),
mock_info("user1", &[]),
ExecuteMsg::CancelExpiredRequest { request_id: 1 },
).unwrap();
assert_eq!(res.attributes[0].value, "cancel_request");
}
}
4 VRF 验证器实现
4.1 Dilithium-5 VRF 推导
Dilithium-5 本身是数字签名方案,不是直接的 VRF。我们需要在 Dilithium-5 之上构建 VRF 构造。这里使用 ECVRF 风格的构造,但底层的椭圆曲线操作替换为 Dilithium-5 的格密码操作。
4.1.1 VRF 构造原理
VRF 构造 = Dilithium-5 签名 + 哈希函数
VRF_Eval(SK, alpha):
+-------------------------------------+
| 1. 计算 H = HashToPoint(alpha) | 将输入映射到矩阵空间
| 2. 计算 gamma = SK * H | 格点乘 (Module-LWE)
| 3. 计算 beta = HashToOutput(gamma) | 派生伪随机输出
| 4. 计算 pi = Dilithium_Sign(SK, | 签名证明
| (alpha || beta))
| 5. 输出 (pi, beta) |
+-------------------------------------+
VRF_Verify(PK, alpha, pi, beta):
+-------------------------------------+
| 1. 计算 H = HashToPoint(alpha) |
| 2. 检查 Dilithium_Verify(PK, | 验证签名
| (alpha || beta), pi) |
| 3. 将 gamma 从 pi 中提取 |
| 4. 检查 Pairing(PK, H) == | 一致性检查
| Pairing(G, gamma) |
| 5. 检查 HashToOutput(gamma) == beta |
| 6. 全部通过 -> 返回 True |
+-------------------------------------+
4.1.2 完整 VRF 实现 (vrf.rs)
use sha2::{Digest, Sha256, Sha512};
use hmac::{Hmac, Mac};
/// Dilithium-5 参数常量
const DILITHIUM5_PUBLICKEYBYTES: usize = 1312;
const DILITHIUM5_SECRETKEYBYTES: usize = 2560;
const DILITHIUM5_SIGNBYTES: usize = 2700;
const VRF_OUTPUT_BYTES: usize = 64;
const MODULUS_Q: u64 = 8380417;
const N: usize = 256;
const K: usize = 8;
const L: usize = 7;
/// VRF 错误类型
#[derive(Debug, PartialEq)]
pub enum VrfError {
InvalidPublicKey,
InvalidProof,
InvalidOutput,
VerificationFailed,
HashToPointFailed,
}
impl std::fmt::Display for VrfError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VrfError::InvalidPublicKey => write!(f, "Invalid public key"),
VrfError::InvalidProof => write!(f, "Invalid proof format"),
VrfError::InvalidOutput => write!(f, "Invalid output format"),
VrfError::VerificationFailed => write!(f, "VRF verification failed"),
VrfError::HashToPointFailed => write!(f, "Failed to hash to point"),
}
}
}
/// VRF 上下文
pub struct Dilithium5Vrf {
q: u64,
n: usize,
k: usize,
l: usize,
}
impl Default for Dilithium5Vrf {
fn default() -> Self {
Self { q: MODULUS_Q, n: N, k: K, l: L }
}
}
impl Dilithium5Vrf {
pub fn new() -> Self {
Self::default()
}
pub fn verify(
&self,
pubkey: &[u8],
alpha: &[u8],
pi: &[u8],
beta: &[u8],
) -> bool {
if pubkey.len() != DILITHIUM5_PUBLICKEYBYTES {
return false;
}
if beta.len() != VRF_OUTPUT_BYTES {
return false;
}
let (signature, gamma) = match self.parse_proof(pi) {
Some(data) => data,
None => return false,
};
let h_point = match self.hash_to_point(alpha) {
Some(pt) => pt,
None => return false,
};
let mut msg = Vec::with_capacity(alpha.len() + beta.len());
msg.extend_from_slice(alpha);
msg.extend_from_slice(beta);
if !self.verify_dilithium_signature(pubkey, &msg, &signature) {
return false;
}
if !self.verify_pairing(pubkey, &h_point, &gamma) {
return false;
}
let expected_beta = self.hash_to_output(&gamma);
if expected_beta.len() != beta.len() {
return false;
}
self.constant_time_compare(beta, &expected_beta)
}
fn parse_proof(&self, pi: &[u8]) -> Option<(Vec<u8>, Vec<u8>)> {
if pi.len() < DILITHIUM5_SIGNBYTES + 32 {
return None;
}
let signature = pi[..DILITHIUM5_SIGNBYTES].to_vec();
let gamma = pi[DILITHIUM5_SIGNBYTES..].to_vec();
Some((signature, gamma))
}
fn hash_to_point(&self, alpha: &[u8]) -> Option<Vec<u8>> {
use sha3::Shake256;
use sha3::digest::ExtendableOutput;
use sha3::digest::XofReader;
let mut hasher = Shake256::default();
hasher.update(b"MSG-DAR-VRF-HASH2POINT");
hasher.update(alpha);
let mut reader = hasher.finalize_xof();
let total_elements = self.k * self.l * self.n;
let mut h_bytes = vec![0u8; total_elements * 4];
reader.read(&mut h_bytes);
let mut result = Vec::with_capacity(total_elements);
for chunk in h_bytes.chunks(4) {
let val = u32::from_le_bytes([
chunk[0], chunk[1], chunk[2], chunk[3],
]) as u64;
result.push((val % (self.q - 1) + 1) as u8);
}
Some(result)
}
fn hash_to_output(&self, gamma: &[u8]) -> Vec<u8> {
use sha3::Shake256;
use sha3::digest::ExtendableOutput;
use sha3::digest::XofReader;
let mut hasher = Shake256::default();
hasher.update(b"MSG-DAR-VRF-HASH2OUTPUT");
hasher.update(gamma);
let mut reader = hasher.finalize_xof();
let mut output = vec![0u8; VRF_OUTPUT_BYTES];
reader.read(&mut output);
output
}
fn verify_dilithium_signature(
&self,
pubkey: &[u8],
message: &[u8],
signature: &[u8],
) -> bool {
if signature.len() != DILITHIUM5_SIGNBYTES {
return false;
}
if pubkey.len() != DILITHIUM5_PUBLICKEYBYTES {
return false;
}
// 实际生产环境中应调用 msg-chain-devkit 的 dilithium5_verify
// 此处为简化实现
unimplemented!("Dilithium-5 signature verification requires native binding")
}
fn verify_pairing(
&self,
_pubkey: &[u8],
_h_point: &[u8],
_gamma: &[u8],
) -> bool {
// 格密码中的配对检查
unimplemented!("Pairing verification requires lattice operations")
}
fn constant_time_compare(&self, a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut result: u8 = 0;
for (x, y) in a.iter().zip(b.iter()) {
result |= x ^ y;
}
result == 0
}
pub fn derive_subkey(&self, seed: &[u8], path: &str) -> Vec<u8> {
let mut hasher = Sha256::new();
hasher.update(seed);
hasher.update(b":");
hasher.update(path.as_bytes());
hasher.finalize().to_vec()
}
pub fn output_to_u64(&self, output: &[u8]) -> u64 {
let mut bytes = [0u8; 8];
bytes.copy_from_slice(&output[..8]);
u64::from_le_bytes(bytes)
}
pub fn output_to_u128(&self, output: &[u8]) -> u128 {
let mut bytes = [0u8; 16];
bytes.copy_from_slice(&output[..16]);
u128::from_le_bytes(bytes)
}
pub fn output_to_range(&self, output: &[u8], max: u64) -> u64 {
let val = self.output_to_u64(output);
val % max
}
}
pub fn verify_vrf(
pubkey: &[u8],
alpha: &[u8],
pi: &[u8],
beta: &[u8],
) -> bool {
let vrf = Dilithium5Vrf::new();
vrf.verify(pubkey, alpha, pi, beta)
}
pub fn batch_verify_vrf(
pubkeys: &[&[u8]],
alphas: &[&[u8]],
pis: &[&[u8]],
betas: &[&[u8]],
) -> Vec<bool> {
let vrf = Dilithium5Vrf::new();
pubkeys.iter()
.zip(alphas.iter())
.zip(pis.iter())
.zip(betas.iter())
.map(|(((pk, a), pi), b)| vrf.verify(pk, a, pi, b))
.collect()
}
pub fn vrf_output_to_randomness(output: &[u8]) -> [u8; 32] {
let mut result = [0u8; 32];
if output.len() >= 32 {
result.copy_from_slice(&output[..32]);
} else {
let hash = Sha256::digest(output);
result.copy_from_slice(&hash);
}
result
}
pub fn vrf_prng(output: &[u8], length: usize) -> Vec<u8> {
let mut result = Vec::with_capacity(length);
let mut counter: u64 = 0;
while result.len() < length {
let mut mac = Hmac::<Sha512>::new_from_slice(output)
.expect("HMAC can take any key size");
mac.update(b"MSG-CHAIN-VRF-PRNG");
mac.update(&counter.to_le_bytes());
let block = mac.finalize().into_bytes();
result.extend_from_slice(&block);
counter += 1;
}
result.truncate(length);
result
}
pub fn vrf_output_hex(output: &[u8]) -> String {
output.iter()
.map(|b| format!("{:02x}", b))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vrf_new() {
let vrf = Dilithium5Vrf::new();
assert_eq!(vrf.q, MODULUS_Q);
}
#[test]
fn test_hash_to_point_variation() {
let vrf = Dilithium5Vrf::new();
let pt1 = vrf.hash_to_point(b"input1").unwrap();
let pt2 = vrf.hash_to_point(b"input2").unwrap();
assert_ne!(pt1, pt2);
}
#[test]
fn test_hash_to_point_deterministic() {
let vrf = Dilithium5Vrf::new();
let pt1 = vrf.hash_to_point(b"test_input").unwrap();
let pt2 = vrf.hash_to_point(b"test_input").unwrap();
assert_eq!(pt1, pt2);
}
#[test]
fn test_output_to_range() {
let vrf = Dilithium5Vrf::new();
let output = vec![0xFFu8; 64];
let range_100 = vrf.output_to_range(&output, 100);
assert!(range_100 < 100);
}
#[test]
fn test_constant_time_compare() {
let vrf = Dilithium5Vrf::new();
assert!(vrf.constant_time_compare(b"abc", b"abc"));
assert!(!vrf.constant_time_compare(b"abc", b"abd"));
assert!(!vrf.constant_time_compare(b"abc", b"abcd"));
}
#[test]
fn test_derive_subkey() {
let vrf = Dilithium5Vrf::new();
let seed = [0xABu8; 32];
let sk1 = vrf.derive_subkey(&seed, "nft/trait/background");
let sk2 = vrf.derive_subkey(&seed, "nft/trait/foreground");
assert_ne!(sk1, sk2);
}
#[test]
fn test_vrf_prng_length() {
let seed = [0x42u8; 32];
let random_bytes = vrf_prng(&seed, 100);
assert_eq!(random_bytes.len(), 100);
}
#[test]
fn test_vrf_prng_deterministic() {
let seed = [0x42u8; 32];
let rb1 = vrf_prng(&seed, 32);
let rb2 = vrf_prng(&seed, 32);
assert_eq!(rb1, rb2);
}
#[test]
fn test_vrf_output_hex() {
let output = [0x00, 0xFF, 0xAB, 0xCD];
let hex = vrf_output_hex(&output);
assert_eq!(hex, "00ffabcd");
}
}
4.2 Oracle 节点实现(Python)
"""
VRF Oracle 节点 - 离线 VRF 计算服务
负责: 1. 监听合约事件 -> 2. 计算 VRF -> 3. 提交结果
依赖: pip install msg-chain-sdk cryptography aiohttp
"""
import asyncio
import hashlib
import json
import logging
import os
import struct
import time
from typing import Optional
import aiohttp
from msg_chain_sdk.dilithium import Dilithium5
from msg_chain_sdk.vrf import Vrf
from msg_chain_sdk.wallet import Wallet
from msg_chain_sdk.contract import CosmWasmContract
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class VrfOracleNode:
"""MSG Chain VRF Oracle 节点"""
def __init__(
self,
rpc_endpoint: str,
contract_address: str,
private_key_hex: str,
poll_interval: float = 2.0,
):
self.rpc = rpc_endpoint
self.contract_addr = contract_address
self.wallet = Wallet.from_hex(private_key_hex)
self.contract = CosmWasmContract(rpc_endpoint, contract_address, self.wallet)
self.dilithium = Dilithium5()
self.vrf = Vrf(self.dilithium)
self.processed_requests: set[int] = set()
self.poll_interval = poll_interval
async def run(self):
"""主循环: 持续监听并处理 VRF 请求"""
logger.info(f"VRF Oracle 节点: {self.contract_addr}")
while True:
try:
await self.process_new_requests()
except Exception as e:
logger.error(f"Error: {e}")
await asyncio.sleep(self.poll_interval)
async def process_new_requests(self):
"""查询并处理所有未完成的 VRF 请求"""
config = await self.contract.query("get_config")
state = await self.contract.query("get_state") if hasattr(self.contract, 'query') else {}
async def fulfill_request(self, request_id: int, request: dict):
"""计算 VRF 并提交结果"""
seed_bytes = bytes.fromhex(request["seed"]) if request.get("seed") else b""
requester = request["requester"].encode("utf-8")
alpha = hashlib.sha256(
seed_bytes
+ struct.pack(">Q", request_id)
+ requester
).digest()
private_key = bytes.fromhex(os.environ["DILITHIUM_PRIVATE_KEY"])
proof, output = self.vrf.eval(private_key, alpha)
logger.info(f"VRF computed: output={output.hex()[:16]}...")
fulfill_msg = {
"fulfill_randomness": {
"request_id": request_id,
"randomness": output.hex(),
"proof": proof.hex(),
}
}
try:
tx_hash = await self.contract.execute(fulfill_msg, gas_limit=500_000)
logger.info(f"Submitted: tx_hash={tx_hash}")
self.processed_requests.add(request_id)
except Exception as e:
logger.error(f"Submit failed: {e}")
class OracleMonitor:
"""Oracle 节点健康监控"""
def __init__(self, node: VrfOracleNode):
self.node = node
self.start_time = time.time()
self.total_fulfilled = 0
self.errors = 0
def report(self) -> dict:
return {
"uptime_seconds": time.time() - self.start_time,
"total_fulfilled": self.total_fulfilled,
"error_rate": self.errors / max(self.total_fulfilled + self.errors, 1),
"last_check": time.time(),
}
async def main():
node = VrfOracleNode(
rpc_endpoint=os.getenv("MSG_RPC", "https://rpc.msg-chain-1.com"),
contract_address=os.getenv("VRF_CONTRACT_ADDRESS"),
private_key_hex=os.getenv("ORACLE_PRIVATE_KEY"),
poll_interval=float(os.getenv("POLL_INTERVAL", "2.0")),
)
monitor = OracleMonitor(node)
try:
await node.run()
except KeyboardInterrupt:
logger.info(f"Shutdown. Report: {monitor.report()}")
if __name__ == "__main__":
asyncio.run(main())
5 随机数消费模式
5.1 随机 NFT 铸造(特征派生)
5.1.1 NFT 特征派生引擎
use cosmwasm_std::{Binary, StdResult, StdError};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct NftTraits {
pub background: Background,
pub body: Body,
pub head: Head,
pub eyes: Eyes,
pub mouth: Mouth,
pub accessory: Option<Accessory>,
pub score: u64,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum Background { Ocean, Space, Forest, Desert, Cyber }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum Body { Human, Robot, Alien, Ghost }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum Head { Cap, Crown, Helmet, Mask, None }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum Eyes { Normal, Glow, Laser, Blind, XRay }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum Mouth { Smile, Open, Mask, Vape }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum Accessory { Sword, Shield, Wings, Pet, None_ }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct NftRandomEvents {
pub background_roll: u64,
pub body_roll: u64,
pub head_roll: u64,
pub eyes_roll: u64,
pub mouth_roll: u64,
pub accessory_roll: Option<u64>,
pub score_roll: u64,
pub all_random_bytes: String,
}
struct RarityTable<T> {
items: Vec<(T, u64)>,
total_weight: u64,
}
impl<T: Clone> RarityTable<T> {
fn new(items: Vec<(T, u64)>) -> Self {
let total_weight = items.iter().map(|(_, w)| w).sum();
Self { items, total_weight }
}
fn roll(&self, value: u64) -> &T {
let roll = value % self.total_weight;
let mut cumulative = 0u64;
for (item, weight) in &self.items {
cumulative += weight;
if roll < cumulative {
return item;
}
}
&self.items.last().unwrap().0
}
}
pub struct TraitDeriver;
impl TraitDeriver {
pub fn derive_traits(
randomness: &[u8; 32],
token_id: u64,
minter: &str,
timestamp: u64,
) -> StdResult<(NftTraits, NftRandomEvents)> {
let entropy = |path: &[u8]| -> u64 {
let mut hasher = Sha256::new();
hasher.update(randomness);
hasher.update(&token_id.to_be_bytes());
hasher.update(minter.as_bytes());
hasher.update(×tamp.to_be_bytes());
hasher.update(path);
let hash = hasher.finalize();
u64::from_be_bytes(hash[..8].try_into().unwrap())
};
let backgrounds = RarityTable::new(vec![
(Background::Ocean, 2500),
(Background::Space, 2200),
(Background::Forest, 2000),
(Background::Desert, 1800),
(Background::Cyber, 1500),
]);
let bodies = RarityTable::new(vec![
(Body::Human, 4000),
(Body::Robot, 3000),
(Body::Alien, 2000),
(Body::Ghost, 1000),
]);
let heads = RarityTable::new(vec![
(Head::None, 4000),
(Head::Cap, 2500),
(Head::Helmet, 2000),
(Head::Mask, 1000),
(Head::Crown, 500),
]);
let eyes = RarityTable::new(vec![
(Eyes::Normal, 5000),
(Eyes::Glow, 2500),
(Eyes::Laser, 1500),
(Eyes::Blind, 600),
(Eyes::XRay, 400),
]);
let mouths = RarityTable::new(vec![
(Mouth::Smile, 4000),
(Mouth::Open, 3000),
(Mouth::Mask, 2000),
(Mouth::Vape, 1000),
]);
let accessories = RarityTable::new(vec![
(Accessory::None_, 7000),
(Accessory::Sword, 1000),
(Accessory::Shield, 800),
(Accessory::Wings, 700),
(Accessory::Pet, 500),
]);
let background = backgrounds.roll(entropy(b"background")).clone();
let body = bodies.roll(entropy(b"body")).clone();
let head = heads.roll(entropy(b"head")).clone();
let eyes = eyes.roll(entropy(b"eyes")).clone();
let mouth = mouths.roll(entropy(b"mouth")).clone();
let score_roll = entropy(b"score");
let accessory = accessories.roll(entropy(b"accessory")).clone();
let score = (score_roll % 10001) + match (&background, &body, &head, &eyes, &mouth, &accessory) {
(Background::Cyber, Body::Ghost, Head::Crown, Eyes::XRay, Mouth::Vape, Accessory::Wings) => 5000,
_ => 0,
};
let trait_accessory = if matches!(accessory, Accessory::None_) { None } else { Some(accessory) };
let traits = NftTraits { background, body, head, eyes, mouth, accessory: trait_accessory, score };
let events = NftRandomEvents {
background_roll: entropy(b"background"),
body_roll: entropy(b"body"),
head_roll: entropy(b"head"),
eyes_roll: entropy(b"eyes"),
mouth_roll: entropy(b"mouth"),
accessory_roll: Some(entropy(b"accessory")),
score_roll,
all_random_bytes: hex::encode(randomness),
};
Ok((traits, events))
}
pub fn verify_traits(traits: &NftTraits, randomness: &[u8; 32], token_id: u64, minter: &str, timestamp: u64) -> StdResult<bool> {
let (computed, _) = Self::derive_traits(randomness, token_id, minter, timestamp)?;
Ok(traits == &computed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_derive_traits_deterministic() {
let randomness = [0xABu8; 32];
let (t1, _) = TraitDeriver::derive_traits(&randomness, 1, "alice", 1000).unwrap();
let (t2, _) = TraitDeriver::derive_traits(&randomness, 1, "alice", 1000).unwrap();
assert_eq!(t1, t2);
}
#[test]
fn test_derive_traits_different_token() {
let randomness = [0xABu8; 32];
let (t1, _) = TraitDeriver::derive_traits(&randomness, 1, "alice", 1000).unwrap();
let (t2, _) = TraitDeriver::derive_traits(&randomness, 2, "alice", 1000).unwrap();
assert_ne!(t1, t2);
}
#[test]
fn test_verify_traits() {
let randomness = [0x42u8; 32];
let (traits, _) = TraitDeriver::derive_traits(&randomness, 42, "charlie", 2000).unwrap();
assert!(TraitDeriver::verify_traits(&traits, &randomness, 42, "charlie", 2000).unwrap());
assert!(!TraitDeriver::verify_traits(&traits, &randomness, 43, "charlie", 2000).unwrap());
}
}
5.2 随机中奖者选取(彩票/抽奖)
use cosmwasm_std::{Addr, Binary, DepsMut, Env, MessageInfo, Response, StdError, StdResult};
use sha2::{Digest, Sha256};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct LotteryEntry {
pub participant: Addr,
pub tickets: u32,
pub entry_index: u64,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct LotteryState {
pub round: u64,
pub total_tickets: u64,
pub entries: Vec<LotteryEntry>,
pub winner: Option<Addr>,
pub randomness: Option<Binary>,
pub drawn: bool,
}
pub fn select_winner(entries: &[LotteryEntry], randomness: &[u8; 32]) -> StdResult<(Addr, u64)> {
let total_tickets: u64 = entries.iter().map(|e| e.tickets as u64).sum();
if total_tickets == 0 {
return Err(StdError::generic_err("No participants"));
}
let mut hash = Sha256::new();
hash.update(randomness);
hash.update(b"lottery_winner");
let result = hash.finalize();
let winning_index = u64::from_be_bytes(result[..8].try_into().unwrap()) % total_tickets;
let mut cumulative = 0u64;
for entry in entries {
cumulative += entry.tickets as u64;
if winning_index < cumulative {
return Ok((entry.participant.clone(), winning_index));
}
}
Ok((entries.last().unwrap().participant.clone(), winning_index))
}
pub fn select_multiple_winners(entries: &[LotteryEntry], randomness: &[u8; 32], count: usize) -> StdResult<Vec<Addr>> {
if count > entries.len() {
return Err(StdError::generic_err("More winners than participants"));
}
let mut winners = Vec::new();
let mut remaining: Vec<LotteryEntry> = entries.to_vec();
for round in 0..count {
let total: u64 = remaining.iter().map(|e| e.tickets as u64).sum();
let mut hash = Sha256::new();
hash.update(randomness);
hash.update(&round.to_be_bytes());
let result = hash.finalize();
let index = u64::from_be_bytes(result[..8].try_into().unwrap()) % total;
let mut cum = 0u64;
let mut found = None;
for (i, entry) in remaining.iter().enumerate() {
cum += entry.tickets as u64;
if index < cum {
found = Some(i);
break;
}
}
if let Some(i) = found {
winners.push(remaining[i].participant.clone());
remaining.remove(i);
}
}
Ok(winners)
}
#[cfg(test)]
mod lottery_tests {
use super::*;
fn make_entries() -> Vec<LotteryEntry> {
vec![
LotteryEntry { participant: Addr::unchecked("alice"), tickets: 10, entry_index: 0 },
LotteryEntry { participant: Addr::unchecked("bob"), tickets: 5, entry_index: 10 },
LotteryEntry { participant: Addr::unchecked("charlie"), tickets: 3, entry_index: 15 },
]
}
#[test]
fn test_select_winner_deterministic() {
let entries = make_entries();
let randomness = [0x42u8; 32];
let (w1, _) = select_winner(&entries, &randomness).unwrap();
let (w2, _) = select_winner(&entries, &randomness).unwrap();
assert_eq!(w1, w2);
}
#[test]
fn test_select_winner_in_bounds() {
let entries = make_entries();
let valid = vec![Addr::unchecked("alice"), Addr::unchecked("bob"), Addr::unchecked("charlie")];
for seed_byte in 0..=255u8 {
let r = [seed_byte; 32];
let (winner, _) = select_winner(&entries, &r).unwrap();
assert!(valid.contains(&winner));
}
}
#[test]
fn test_select_winner_weighted_distribution() {
let entries = make_entries();
let mut wins = std::collections::HashMap::new();
let trials = 10_000;
for i in 0..trials {
let r = Sha256::digest(&i.to_be_bytes());
let mut randomness = [0u8; 32];
randomness.copy_from_slice(&r);
let (winner, _) = select_winner(&entries, &randomness).unwrap();
*wins.entry(winner).or_insert(0) += 1;
}
let alice_ratio = *wins.get(&Addr::unchecked("alice")).unwrap_or(&0) as f64 / trials as f64;
assert!((alice_ratio - 10.0 / 18.0).abs() < 0.03);
}
}
5.3 随机排序(DAO 治理提案排序)
use rand::SeedableRng;
use rand::rngs::StdRng;
use rand::seq::SliceRandom;
pub fn fisher_yates_shuffle<T: Clone>(items: &[T], randomness: &[u8; 32], salt: &[u8]) -> Vec<T> {
let mut seed = Sha256::new();
seed.update(randomness);
seed.update(salt);
let seed_bytes = seed.finalize();
let mut rng = StdRng::from_seed(seed_bytes.into());
let mut shuffled = items.to_vec();
shuffled.shuffle(&mut rng);
shuffled
}
pub fn shuffle_proposals(proposal_ids: &[u64], randomness: &[u8; 32], round: u64) -> Vec<u64> {
fisher_yates_shuffle(proposal_ids, randomness, &round.to_be_bytes())
}
pub fn select_review_committee(all_members: &[Addr], randomness: &[u8; 32], committee_size: usize) -> Vec<Addr> {
let shuffled = fisher_yates_shuffle(all_members, randomness, b"committee");
shuffled.into_iter().take(committee_size).collect()
}
#[cfg(test)]
mod shuffle_tests {
use super::*;
#[test]
fn test_shuffle_preserves_elements() {
let proposals = vec![1u64, 2, 3, 4, 5];
let randomness = [0x99u8; 32];
let mut sorted = shuffle_proposals(&proposals, &randomness, 1);
sorted.sort();
assert_eq!(sorted, proposals);
}
#[test]
fn test_shuffle_different_rounds() {
let proposals = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let r = [0x42u8; 32];
assert_ne!(shuffle_proposals(&proposals, &r, 1), shuffle_proposals(&proposals, &r, 2));
}
#[test]
fn test_committee_size() {
let members: Vec<Addr> = (0..100).map(|i| Addr::unchecked(format!("member{}", i))).collect();
let r = [0x11u8; 32];
let c = select_review_committee(&members, &r, 5);
assert_eq!(c.len(), 5);
for m in &c { assert!(members.contains(m)); }
}
}
5.4 随机数合并模式
pub fn combine_randomness_sources(dar_randomness: &[u8; 32], vrf_randomness: &[u8; 32]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(dar_randomness);
hasher.update(vrf_randomness);
let result = hasher.finalize();
let mut combined = [0u8; 32];
combined.copy_from_slice(&result);
combined
}
pub fn delayed_randomness(deps: DepsMut, env: Env, commit_block: u64, target_delay: u64) -> StdResult<[u8; 32]> {
let target_height = commit_block + target_delay;
if env.block.height < target_height {
return Err(StdError::generic_err(format!("Wait until block {}", target_height)));
}
let response = query_dar_randomness(&deps.as_ref(), Some(target_height))?;
let mut seed = [0u8; 32];
seed.copy_from_slice(response.randomness.as_slice());
Ok(seed)
}
6 安全考虑
6.1 偏斜攻击 (Bias Attack)
6.1.1 问题描述
偏斜攻击指攻击者通过影响随机性生成过程来使得结果对自己有利。
攻击场景: 基于 BlockHash 的彩票
|
|- 攻击者看到当前的 block hash -> 计算是否中奖
|- 如果中奖 -> 提交交易
|- 如果没中奖 -> 不提交
|- 等价于攻击者可以"选择"是否参与!
6.1.2 DAR 的防护
DAR 通过以下设计抵御偏斜攻击:
- 提议者承诺:区块提议者在构建区块时必须包含 VRF 证明
- BFT 确认:需要 2/3+ 验证者签名确认
- 链式随机性:区块 H 的随机性依赖 H-1 的随机性
/// 安全的随机数消费模式:延迟消费
pub struct DelayedRandomConsumer {
delay: u64,
pending_requests: Map<u64, (Addr, u64)>,
}
impl DelayedRandomConsumer {
pub fn request_randomness(&self, deps: DepsMut, env: &Env, info: &MessageInfo) -> StdResult<Response> {
let request_id = /* next id */ 0;
self.pending_requests.save(deps.storage, request_id, &(info.sender.clone(), env.block.height))?;
Ok(Response::new()
.add_attribute("commit_height", env.block.height.to_string())
.add_attribute("consume_height", (env.block.height + self.delay).to_string()))
}
pub fn consume_randomness(&self, deps: DepsMut, env: &Env, request_id: u64) -> StdResult<Response> {
let (_, commit_height) = self.pending_requests.load(deps.storage, request_id)?;
if env.block.height < commit_height + self.delay {
return Err(StdError::generic_err("Delay not elapsed"));
}
let seed = get_dar_randomness(env)?;
Ok(Response::new().add_attribute("action", "consume"))
}
}
6.2 抢跑攻击 (Front-Running)
/// 反抢跑: 将用户地址和 nonce 绑定到随机数派生中
pub fn derive_user_randomness(dar_seed: &[u8; 32], user: &Addr, nonce: u64) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(dar_seed);
hasher.update(user.as_bytes());
hasher.update(&nonce.to_be_bytes());
let result = hasher.finalize();
let mut output = [0u8; 32];
output.copy_from_slice(&result);
output
}
6.3 Oracle 活性 (Liveness)
6.3.1 解决方案:多 Oracle + 超时 + 降级
/// 多 Oracle 支持
pub struct MultiOracle {
pub oracles: Vec<Addr>,
pub threshold: usize,
}
/// 超时降级: 自动使用 DAR
pub fn execute_with_timeout_fallback(deps: DepsMut, env: Env, request_id: u64) -> StdResult<Response> {
let request = REQUESTS.load(deps.storage, request_id)?;
let elapsed = env.block.height - request.request_height;
let timeout = CONFIG.load(deps.storage)?.request_timeout;
if elapsed < timeout {
return Err(StdError::generic_err("Not yet expired"));
}
let dar_seed = get_dar_randomness(&env)?;
Ok(Response::new()
.add_attribute("method", "fallback_to_dar")
.add_attribute("request_id", request_id.to_string()))
}
6.4 重入攻击 (Reentrancy)
/// 防重入: 互斥锁
pub struct ReentrancyGuard;
const REENTRANCY_KEY: &[u8] = b"reentrancy_guard";
impl ReentrancyGuard {
pub fn lock(store: &mut dyn Storage) -> StdResult<()> {
if store.get(REENTRANCY_KEY).is_some() {
return Err(StdError::generic_err("Reentrancy detected"));
}
store.set(REENTRANCY_KEY, &[1]);
Ok(())
}
pub fn unlock(store: &mut dyn Storage) {
store.remove(REENTRANCY_KEY);
}
}
pub fn safe_randomness_callback(deps: DepsMut, env: Env, info: MessageInfo, randomness: Binary) -> StdResult<Response> {
ReentrancyGuard::lock(deps.storage)?;
let result = actual_callback_logic(deps, env, info, randomness);
ReentrancyGuard::unlock(deps.storage);
result
}
6.5 可验证的消费日志
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct RandomnessConsumptionEvent {
pub event_id: u64,
pub source: String,
pub dar_seed: Option<[u8; 32]>,
pub vrf_request_id: Option<u64>,
pub final_randomness: [u8; 32],
pub consumer: Addr,
pub action: String,
pub block_height: u64,
pub timestamp: u64,
}
pub const CONSUMPTION_LOG: Map<u64, RandomnessConsumptionEvent> = Map::new("consumption_log");
pub const CONSUMPTION_COUNT: Item<u64> = Item::new("consumption_count");
7 TypeScript 集成
7.1 使用 CosmJS 与 VRF Oracle 交互
import {
CosmWasmClient,
SigningCosmWasmClient,
ExecuteResult,
} from '@cosmjs/cosmwasm-stargate';
import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing';
import { GasPrice } from '@cosmjs/stargate';
import { fromHex, toHex } from '@cosmjs/encoding';
import { sha256 } from '@noble/hashes/sha256';
const MSG_CHAIN_RPC = 'https://rpc.msg-chain-1.com';
const GAS_PRICE = GasPrice.fromString('1000000000umsg');
export interface VrfOracleClient {
client: SigningCosmWasmClient;
sender: string;
}
export async function createVrfClient(mnemonic: string): Promise<VrfOracleClient> {
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix: 'msg' });
const [{ address }] = await wallet.getAccounts();
const client = await SigningCosmWasmClient.connectWithSigner(MSG_CHAIN_RPC, wallet, { gasPrice: GAS_PRICE });
return { client, sender: address };
}
export class VrfOracleContract {
private client: VrfOracleClient;
private contractAddress: string;
constructor(client: VrfOracleClient, contractAddress: string) {
this.client = client;
this.contractAddress = contractAddress;
}
async getConfig(): Promise<any> {
return this.client.client.queryContractSmart(this.contractAddress, { get_config: {} });
}
async getRequest(requestId: number): Promise<any> {
return this.client.client.queryContractSmart(this.contractAddress, { get_request: { request_id: requestId } });
}
async getResponse(requestId: number): Promise<any> {
return this.client.client.queryContractSmart(this.contractAddress, { get_response: { request_id: requestId } });
}
async getLatestRandomness(): Promise<any> {
return this.client.client.queryContractSmart(this.contractAddress, { get_latest_randomness: {} });
}
async requestRandomness(seed?: Uint8Array, callbackContract?: string, callbackMsg?: Uint8Array): Promise<number> {
const msg: any = { request_randomness: {} };
if (seed) msg.request_randomness.seed = toHex(seed);
if (callbackContract) msg.request_randomness.callback_contract = callbackContract;
if (callbackMsg) msg.request_randomness.callback_msg = toHex(callbackMsg);
const result = await this.client.client.execute(
this.client.sender, this.contractAddress, msg, 'auto',
);
return this.parseRequestIdFromEvents(result);
}
async verifyRandomness(requestId: number, randomness: string, proof: string): Promise<boolean> {
const result: { valid: boolean } = await this.client.client.queryContractSmart(
this.contractAddress,
{ verify_randomness: { request_id: requestId, randomness, proof } },
);
return result.valid;
}
private parseRequestIdFromEvents(result: ExecuteResult): number {
for (const event of result.events) {
if (event.type === 'vrf-request') {
const attr = event.attributes.find(a => a.key === 'request_id');
if (attr) return parseInt(attr.value, 10);
}
}
throw new Error('request_id not found in events');
}
}
7.2 事件订阅
export function subscribeVrfFulfilled(
client: CosmWasmClient,
contractAddress: string,
callback: (event: { requestId: number; randomness: string; txHash: string }) => void,
) {
const ws = new WebSocket(
MSG_CHAIN_RPC.replace('https', 'wss').replace('http', 'ws') + '/websocket',
);
ws.onopen = () => {
ws.send(JSON.stringify({
jsonrpc: '2.0',
method: 'subscribe',
params: ["tm.event='Tx' AND vrf_fulfilled.request_id EXISTS"],
id: 1,
}));
};
ws.onmessage = (msg) => {
try {
const data = JSON.parse(msg.data);
if (data.result?.events?.['vrf-fulfilled']) {
const attrs = data.result.events['vrf-fulfilled'];
callback({
requestId: parseInt(attrs.find((a: any) => a.key === 'request_id')?.value || '0'),
randomness: attrs.find((a: any) => a.key === 'randomness')?.value || '',
txHash: data.result.hash || '',
});
}
} catch { /* ignore */ }
};
return () => ws.close();
}
export function vrfOutputToRange(randomness: string, max: number): number {
const bytes = fromHex(randomness);
const hash = sha256(bytes);
const value = new DataView(hash.buffer).getBigUint64(0, false);
return Number(value % BigInt(max));
}
export function vrfOutputToUuid(randomness: string): string {
const hash = sha256(fromHex(randomness));
hash[6] = (hash[6] & 0x0f) | 0x40;
hash[8] = (hash[8] & 0x3f) | 0x80;
const hex = toHex(hash);
return [hex.slice(0, 8), hex.slice(8, 12), hex.slice(12, 16), hex.slice(16, 20), hex.slice(20, 32)].join('-');
}
7.3 React dApp 集成
import React, { useEffect, useState, useCallback } from 'react';
import { useWallet } from '@msg-chain/react';
import { VrfOracleContract, subscribeVrfFulfilled } from './vrfClient';
export const RandomnessDisplay: React.FC<{ contractAddress: string }> = ({ contractAddress }) => {
const { address, getSigningClient } = useWallet();
const [loading, setLoading] = useState(false);
const [requestId, setRequestId] = useState<number | null>(null);
const [randomness, setRandomness] = useState<string | null>(null);
const [verified, setVerified] = useState<boolean | null>(null);
const requestRandomness = useCallback(async () => {
if (!address) return;
setLoading(true);
try {
const signingClient = await getSigningClient();
const vrf = new VrfOracleContract({ client: signingClient, sender: address }, contractAddress);
const rid = await vrf.requestRandomness();
setRequestId(rid);
} finally {
setLoading(false);
}
}, [address, contractAddress, getSigningClient]);
useEffect(() => {
if (!requestId) return;
const unsubscribe = subscribeVrfFulfilled(/* ... */, contractAddress, async (event) => {
if (event.requestId === requestId) setRandomness(event.randomness);
});
return unsubscribe;
}, [requestId, contractAddress]);
return (
<div>
<h2>VRF 随机数生成器</h2>
<button onClick={requestRandomness} disabled={loading}>
{loading ? '请求中...' : '请求随机数'}
</button>
{requestId && <p>请求 ID: {requestId}</p>}
{randomness && <p>随机数: {randomness.slice(0, 32)}...</p>}
</div>
);
};
附录
A.1 VRF 合约 Cargo.toml
[package]
name = "vrf-oracle"
version = "1.0.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = ["library"]
library = []
[dependencies]
cosmwasm-std = { version = "1.5", features = ["stargate"] }
cosmwasm-crypto = { version = "1.5", features = ["stargate"] }
cw-storage-plus = "1.2"
schemars = "0.8"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
sha2 = "0.10"
sha3 = "0.10"
hmac = "0.12"
hex = "0.4"
[dev-dependencies]
cosmwasm-vm = { version = "1.5", features = ["stargate"] }
A.2 部署命令
# 编译合约
cargo wasm
# 优化(生产环境)
docker run --rm -v "$(pwd)":/code \
--platform linux/amd64 \
ghcr.io/cosmwasm/workspace-optimizer:0.14.0
# 存储合约
msg-chaind tx wasm store artifacts/vrf_oracle.wasm \
--from admin --gas-prices 1000000000umsg --gas auto \
--gas-adjustment 1.3 --chain-id msg-chain-1 -y
# 实例化
msg-chaind tx wasm instantiate <CODE_ID> \
'{"pubkey":"<HEX>","oracle_address":"<ADDR>","request_timeout":100}' \
--from admin --label "vrf-oracle-v1" \
--admin <ADMIN_ADDR> --chain-id msg-chain-1 -y
# 查询配置
msg-chaind query wasm contract-state smart <CONTRACT_ADDR> '{"get_config":{}}'
A.3 参考资源
- CRYSTALS-Dilithium 规范: NIST FIPS 204
- VRF 标准: RFC 9381 (ECVRF)
- CosmWasm 文档: https://docs.cosmwasm.com
- MSG Chain 开发文档: https://docs.msg-chain.org
- DAR 共识规范: MSG Chain 共识层白皮书
A.4 常见问题
Q: DAR 随机性在合约中不可用怎么办?
A: 确保链版本支持 DAR。旧版本区块可能没有 randomness 字段。调用 query_dar_randomness 作为回退。
Q: VRF 证明验证 gas 消耗过高?
A: Dilithium-5 验证涉及格运算,gas 消耗较大。建议在合约内仅做哈希验证,完整的 Dilithium-5 验证放在离链端。
Q: Oracle 节点如何保证高可用?
A: 运行多个 Oracle 实例,使用负载均衡。每个实例独立监听事件,通过幂等性保证不会重复提交。
Q: 如何迁移从 DAR 到 VRF Oracle 或反之?
A: 合约通过配置开关控制随机性来源。替换时只需更新配置,无需重新部署消费者合约。
