owned this note
owned this note
Published
Linked with GitHub
# Drand Random 與 Sui Move 的結合
這次我們要用 Drand 產生 安全性的隨機數
並在 Sui 智能合約上 驗證簽章
Drand :
https://drand.love/developer/
Sui Move Example(drand_lib.move):
https://github.com/MystenLabs/sui/blob/main/sui_programmability/examples/games/sources/drand_lib.move
## Drand Lib 智能合約
首先我們先參考 Sui Move 提供的 drand_lib.move檔
我這邊把它翻成中文
```javascript=
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
/// 用於處理 drand 輸出的輔助模組。
/// 目前適用於鏈 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971 (quicknet)。
///
/// 請參閱如何在 drand_based_lottery.move 和 drand_based_scratch_card.move 中使用此範例。
///
/// 如果您想在預設網路上使用此模組,該網路有30s的周期,您需要更改公鑰,
/// 創世時間並將先前的簽章包含在 verify_drand_signature 中。請參閱 https://drand.love/developer/ 或
/// 此文件的先前版本:https://github.com/MystenLabs/sui/blob/92df778310679626f00bc4226d7f7a281322cfdd/sui_programmability/examples/games/sources/drand_lib.move
module sui_drand_demo::drand_lib {
use std::hash::sha2_256;
use sui::bls12381;
/// Error codes
const EInvalidRndLength: u64 = 0;
const EInvalidProof: u64 = 1;
/// The genesis time of chain 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971.
const GENESIS: u64 = 1692803367;
/// The public key of chain 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971.
const DRAND_PK: vector<u8> =
x"83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a";
/// The time in seconds between randomness beacon rounds.
const PERIOD: u64 = 3;
/// 透過驗證稍後時間的 drand 簽章來檢查給定的紀元時間是否已經過去。
/// 回合必須至少為 (epoch_time - GENESIS)/PERIOD + 1)。
public fun verify_time_has_passed(epoch_time: u64, sig: vector<u8>, round: u64) {
assert!(epoch_time <= GENESIS + PERIOD * (round - 1), EInvalidProof);
verify_drand_signature(sig, round);
}
/// 驗證 drand 簽章
public fun verify_drand_signature(sig: vector<u8>, mut round: u64) {
// Convert round to a byte array in big-endian order.
let mut round_bytes: vector<u8> = vector[0, 0, 0, 0, 0, 0, 0, 0];
let mut i = 7;
// 請注意,此循環永遠不會複製 round_bytes 的最後一個位元組,儘管預計它不會非零。
while (i > 0) {
let curr_byte = round % 0x100;
let curr_element = vector::borrow_mut(&mut round_bytes, i);
*curr_element = (curr_byte as u8);
round = round >> 8;
i = i - 1;
};
// 計算 sha256(prev_sig, round_bytes)。
let digest = sha2_256(round_bytes);
// 驗證哈希上的簽章。
let drand_pk = DRAND_PK;
assert!(bls12381::bls12381_min_sig_verify(&sig, &drand_pk, &digest), EInvalidProof);
}
/// 對 drand 的簽章做 sha2_256 雜湊加密,並取到一個vector<u8>
public fun derive_randomness(drand_sig: vector<u8>): vector<u8> {
sha2_256(drand_sig)
}
// 將 rnd 的前 16 個位元組轉換為 u128 數字並輸出其與輸入 n 的模。
// 由於 n 為 u64,假設 rnd 是均勻隨機的,則輸出最多有 2^{-64} 偏差。
public fun safe_selection(n: u64, rnd: &vector<u8>): u64 {
assert!(vector::length(rnd) >= 16, EInvalidRndLength);
let mut m: u128 = 0;
let mut i = 0;
while (i < 16) {
m = m << 8;
let curr_byte = *vector::borrow(rnd, i);
m = m + (curr_byte as u128);
i = i + 1;
};
let n_128 = (n as u128);
let module_128 = m % n_128;
let res = (module_128 as u64);
res
}
}
```
我們可以看到最上方有 drand 的3個常數
GENESIS ( 自 Unix 紀元以來該組開始產生隨機性的時間,以秒為單位)
DRAND_PK ( drand 某個hashkey的分散式公鑰 )
PERIOD ( 多久時間會產生下一輪,以秒為單位 )
drand_lib.move內主要用的hashkey為
```
52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971
```
我們可以使用下面這個 api 來查看這個 hashkey 的基本資訊
```
https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/info
```
```json
{
"public_key": "83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a",
"period": 3,
"genesis_time": 1692803367,
"hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971",
"groupHash": "f477d5c89f21a17c863a7f937c6a6d15859414d2be09cd448d4279af331c5d3e",
"schemeID": "bls-unchained-g1-rfc9380",
"metadata": {
"beaconID": "quicknet"
}
}
```
這次我們主要會用到 3個 方法
verify_drand_signature (驗證 drand 簽章)
derive_randomness (對 drand 的簽章做 sha2_256 雜湊加密)
safe_selection (安全選擇)
## 使用 Drand Lib 方法 簡易智能合約
下方為一個簡易的 使用 Drand 智能合約範例
```javascript=
module sui_drand_demo::sui_drand_demo {
use sui_drand_demo::drand_lib::{derive_randomness, verify_drand_signature, safe_selection};
// 獲獎者物件 - Shared Object
public struct Winner has key, store{
id: UID,
winner_index: vector<u64>
}
// 初始化
fun init (ctx: &mut TxContext){
// 把獲獎者物件變成Shared Object
transfer::share_object(
Winner {
id: object::new(ctx),
winner_index: vector::empty()
}
);
}
// 設定 winner
public entry fun set_winner (
winner: &mut Winner, // Winner Shared Object
max_number: u64, // 最大值
current_round: u64, // drand current round
drand_sig: vector<u8> // drand signature
){
// 驗證 drand 簽章
verify_drand_signature(drand_sig, current_round);
// 取得 drand randomness
let digest = derive_randomness(drand_sig);
// 取得 安全性的 隨機數
let random_index = safe_selection(max_number, &digest);
// 塞入 winnder_index vector
winner.winner_index.push_back(random_index);
}
// 清除 winner shared object 內的 winner
public entry fun clear_winner (
winner: &mut Winner // Winner Shared Object
){
winner.winner_index = vector::empty();
}
}
```
## 前端串接
如果還沒建立Dapp的話,可以先參考我的這篇教學
[快速建立 Sui Move Dapp 並實現贊助交易](https://hackmd.io/iqcfxMR0SwmmEKXhnXe-VA?view)
Drand JS Install :
https://drand.love/developer/clients/#js
Drand JS Example :
https://github.com/drand/drand-client#usage
主要要先安裝他的依賴包
```
npm install drand-client
```
創建一個新的 DrandRandom.tsx
```javascript=
import { TransactionBlock } from '@mysten/sui.js/transactions';
import {
fetchBeacon,
HttpChainClient,
HttpCachingChain,
} from 'drand-client'
// 獲獎者 Shared Object ID
const winner_shared_object_id = "0x47a5401cf3e63981383ceb49984ac0f7ed04788acc4b76f5051c905adefc0d84";
// 智能合約 Package ID
const contract_package_id = "0xc964346ca241df9e9b9f5a7995cc4ad470a0cc46747bde87a542cd36a9867bd7";
// 智能合約 module 名稱
const contract_module_name = "sui_drand_demo";
// 智能合約 呼叫方法 名稱
const contract_set_winner_method = "set_winner";
const contract_clear_winner_method = "clear_winner";
const winner_count = 5;
const lottery_count = 100;
/**
* 設定獲獎者資訊
*/
export async function getWinnerInfoTransactionBlock() {
// 產生一個新的交易區塊
const txb = new TransactionBlock();
// hash key
const chainHash = '52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971'
// 公鑰
const publicKey = "83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a";
// 選項
const options = {
disableBeaconVerification: false, // `true` disables checking of signatures on beacons - faster but insecure!!!
noCache: false, // `true` disables caching when retrieving beacons for some providers
chainVerificationParams: { chainHash, publicKey } // these are optional, but recommended! They are compared for parity against the `/info` output of a given node
}
// 使用這個 chain
const chain = new HttpCachingChain(`https://api.drand.sh/${chainHash}`, options)
// 得到 client cli
const client = new HttpChainClient(chain, options)
// 取得目前的 RandomnessBeacon
// 其實就跟使用 https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/latest 一樣
/*
{
"round": 7283409,
"randomness": "f6f92b1dc4a2e5231573423aa6e7e026afc59e46bddd087e0126377a038682c1",
"signature": "b58e76a3dc5cb831d681c5d9e02f7fe95b8b42ec1cb3c6640470d08c1f7bf60b63f443297f12a9c24a53ff488b22cc31"
}
*/
const theLatestBeacon = await fetchBeacon(client)
// 取得目前 round
const drand_round: number = theLatestBeacon.round;
// 要抽的獲獎者數量
let run_count = winner_count;
// 存入 daran round
let randomRoundArray: number[] = [];
while (run_count > 0){
// 隨機抽選 round
let random_round = Math.floor(Math.random() * drand_round);
// 如果重複了,則略過
if (randomRoundArray.includes(random_round)){
continue
}
randomRoundArray.push(random_round);
run_count = run_count -1;
}
for (let round of randomRoundArray){
// 取得某個round的RandomnessBeacon 物件
// https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/123
/*
{
"round": 123,
"randomness": "fb8f7bc29bf24db51871ec8c79f3a1e4bd0557bc0dfcee9ed1d924e69d1c60dc",
"signature": "b75c69d0b72a5d906e854e808ba7e2accb1542ac355ae486d591aa9d43765482e26cd02df835d3546d23c4b13e0dfc92"
}
*/
let randomnessBeacon = await client.get(round);
// signature 16進制字串 轉換成 vector<u8>
const byteArray = hex16String2Vector(randomnessBeacon.signature);
let args = [
// 第1個為 Winner Shared Object Id
txb.object(winner_shared_object_id),
// 第2個為 最大值 ex:100
txb.pure(lottery_count),
// 第3個為 drand current round
txb.pure(randomnessBeacon.round),
// 第4個為 drand signature
txb.pure(byteArray)
]
// 呼叫智能合約 設定獲獎者
txb.moveCall({
// target 要帶入 PageckId::moduleName::methodName
// ex: 0xc964346ca241df9e9b9f5a7995cc4ad470a0cc46747bde87a542cd36a9867bd7::sui_drand_demo::set_winner
target: `${contract_package_id}::${contract_module_name}::${contract_set_winner_method}`,
// 參數
arguments: args,
});
}
return txb;
}
// 取得 清除獲獎者資訊 交易區塊
export async function getClearWinnerInfoTransactionBlock() {
// 產生一個新的交易區塊
const txb = new TransactionBlock();
let args = [
// Winner Shared Object Id
txb.object(winner_shared_object_id),
]
// 呼叫智能合約 清除獲獎者資訊
txb.moveCall({
// target 要帶入 PageckId::moduleName::methodName
// ex: 0xd9326566facfff6e8250ce92b71bf91427cc197e052d0c8a162b6cd7cb9c3e83::sui_drand_demo::clear_winner
target: `${contract_package_id}::${contract_module_name}::${contract_clear_winner_method}`,
// 參數
arguments: args,
});
return txb;
}
function hex16String2Vector(str: string) {
// 定義一個空數組來存儲結果
let byteArray = [];
// 將十六進制字符串每兩個字符分割並將其轉換為十進制數字,然後添加到數組中
for (let i = 0; i < str.length; i += 2) {
byteArray.push(parseInt(str.slice(i, i + 2), 16));
}
return byteArray;
}
```
```javascript=
import { useCurrentAccount, useSignTransactionBlock, useSignAndExecuteTransactionBlock } from "@mysten/dapp-kit";
import { Button, Container, Flex, Heading, Text } from "@radix-ui/themes";
import { OwnedObjects } from "./OwnedObjects";
import { getWinnerInfoTransactionBlock, getClearWinnerInfoTransactionBlock } from "./DrandRandom";
export function WalletStatus() {
const account = useCurrentAccount();
const { mutate: signTransactionBlock } = useSignTransactionBlock();
const { mutate: signAndExecuteTransactionBlock } = useSignAndExecuteTransactionBlock();
return (
<Container my="2">
<Heading mb="2">Wallet Status</Heading>
{account ? (
<Flex direction="column">
<Text>Wallet connected</Text>
<Text>Address: {account.address}</Text>
<Button
onClick={() => {
getWinnerInfoTransactionBlock().then(txb => {
signAndExecuteTransactionBlock(
{
transactionBlock: txb
},
{
onSuccess: (successResult) => {
console.log('executed transaction block success', successResult);
},
onError: (errorResult) => {
console.log('executed transaction block error', errorResult);
},
},
);
});
}}
>
Set Winner By Drand Random
</Button>
<Button
onClick={() => {
getClearWinnerInfoTransactionBlock().then(txb => {
signAndExecuteTransactionBlock(
{
transactionBlock: txb
},
{
onSuccess: (successResult) => {
console.log('executed transaction block success', successResult);
},
onError: (errorResult) => {
console.log('executed transaction block error', errorResult);
},
},
);
});
}}
>
Clear Winner By Drand Random
</Button>
</Flex>
) : (
<Text>Wallet not connected</Text>
)}
<OwnedObjects />
</Container>
);
}
```
## 驗證結果
目前 Winner Shared Object 是沒有任何獲獎者的
https://suiscan.xyz/devnet/object/0x47a5401cf3e63981383ceb49984ac0f7ed04788acc4b76f5051c905adefc0d84

執行結果 - 抽5個人

清空獲獎者資訊
