# 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 ![image](https://hackmd.io/_uploads/S1KU1fWMA.png) 執行結果 - 抽5個人 ![image](https://hackmd.io/_uploads/r1J3kMWfC.png) 清空獲獎者資訊 ![image](https://hackmd.io/_uploads/BkwwbfZzR.png)