SUI Move官方示例合约实践——游戏类:基于drand的抽奖游戏(drand_based_lottery)
本合约是基于drand
随机数的抽奖游戏合约。drand是分布式随机数信标,提供可验证、不可预测和无偏的随机数即服务。(Distributed randomness beacon. Verifiable, unpredictable and unbiased random numbers as a service.)
drand
所在的quicknet chain
每3秒产生一个可验证的随机32字节,任何人都可以检查给定的32字节是否确实是drand的第i个输出。故可以使用drand
作为随机性来源实现需要不可偏倚且不可预测的随机性的链上游戏。即:每次游戏需要随机性时,它从drand
接收下一个32字节(作为交易的一部分或从现有对象中读取),并相应地进行操作。然而,在某些情况下,这种简单的流程可能不安全,因为区块链不知道drand
的最新轮次,因此可能依赖已经公开的随机性。)
任何人可以 创建(create
) 抽奖游戏,并指定开奖轮次N
。
任何人也能通过提供N-20
轮次(即:N轮次前1分钟)的drand
随机数来 终止(close
) 游戏,使其不再接受新参与者,即:开奖前的一分钟可以终止新用户参与。
只要有人及时关闭游戏(或者至少在N轮次之前),我们就有保证获胜者是由不可预测且不可偏倚的随机性选出的。否则,有人可能会等到N轮次的随机性公开,看看是否可以赢得游戏,如果可以,加入游戏并推动其完成。因此,诚实的用户被鼓励及时关闭游戏。
只要抽奖游戏正常运行,任何人都可以参与(participate
),并获得奖票Ticket
对象
任何人都可以通过提供第N轮次的drand随机数签名来完成(complete
)游戏,并宣布胜利者
获胜者通过持有的Ticket
进行兑奖(redeem
)
create
)round
,表示开奖的轮次数 /// Create a shared-object Game.
public entry fun create(round: u64, ctx: &mut TxContext) {
let game = Game {
id: object::new(ctx),
round,
status: IN_PROGRESS,
participants: 0,
winner: option::none(),
};
transfer::public_share_object(game);
}
close
)任何人都可以通过提供N-20
轮次(即:N轮次前1分钟)的drand
随机数来终止游戏,使其不再接受新参与者
注:示例合约代码是N-2
,这里是之前老的网络(30s
一轮),但当前使用的已是快速网络(3s
一轮),为了保持之前逻辑,相应把这里改成N-20
,意为在开奖前的1分钟,便可以终止游戏,来阻止新参与者的加入。
签名的内容是:将round
转成大端序后,进行sha256
哈希,在验签函数(verify_drand_signature
)中通过公钥drand_pk
验证该哈希签名,若验签失败会抛出异常
/// Anyone can close the game by providing the randomness of round-2.
public entry fun close(game: &mut Game, drand_sig: vector<u8>) {
assert!(game.status == IN_PROGRESS, EGameNotInProgress);
verify_drand_signature(drand_sig, closing_round(game.round));
game.status = CLOSED;
}
fun closing_round(round: u64): u64 {
round - 20
}
/// Check a drand output.
public fun verify_drand_signature(sig: vector<u8>, round: u64) {
// Convert round to a byte array in big-endian order.
let round_bytes: vector<u8> = vector[0, 0, 0, 0, 0, 0, 0, 0];
let i = 7;
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;
};
// Compute sha256(prev_sig, round_bytes).
let digest = sha2_256(round_bytes);
// Verify the signature on the hash.
let drand_pk = DRAND_PK;
assert!(bls12381::bls12381_min_sig_verify(&sig, &drand_pk, &digest), EInvalidProof);
}
participate
)Ticket
对象,记录了参与编号(participant_index
) /// Anyone can participate in the game and receive a ticket.
public entry fun participate(game: &mut Game, ctx: &mut TxContext) {
assert!(game.status == IN_PROGRESS, EGameNotInProgress);
let ticket = Ticket {
id: object::new(ctx),
game_id: object::id(game),
participant_index: game.participants,
};
game.participants = game.participants + 1;
transfer::public_transfer(ticket, tx_context::sender(ctx));
}
/// Anyone can complete the game by providing the randomness of round.
public entry fun complete(game: &mut Game, drand_sig: vector<u8>) {
assert!(game.status != COMPLETED, EGameAlreadyCompleted);
verify_drand_signature(drand_sig, game.round);
game.status = COMPLETED;
// The randomness is derived from drand_sig by passing it through sha2_256 to make it uniform.
let digest = derive_randomness(drand_sig);
game.winner = option::some(safe_selection(game.participants, &digest));
}
redeem
)Ticket
进行兑奖,获得GameWinner
荣誉称号 /// The winner can redeem its ticket.
public entry fun redeem(ticket: &Ticket, game: &Game, ctx: &mut TxContext) {
assert!(object::id(game) == ticket.game_id, EInvalidTicket);
assert!(option::contains(&game.winner, &ticket.participant_index), EInvalidTicket);
let winner = GameWinner {
id: object::new(ctx),
game_id: ticket.game_id,
};
transfer::public_transfer(winner, tx_context::sender(ctx));
}
别名 | 地址 | 角色 |
---|---|---|
Jason | 0x5c5882d73a6e5b6ea1743fb028eff5e0d7cc8b7ae123d27856c5fe666d91569a |
游戏创建者 |
Alice | 0x2d178b9704706393d2630fe6cf9415c2c50b181e9e3c7a977237bb2929f82d19 |
游戏参与者 |
Bob | 0xf2e6ffef7d0543e258d4c47a53d6fa9872de4630cc186950accbd83415b009f0 |
游戏参与者 |
export JASON=0x5c5882d73a6e5b6ea1743fb028eff5e0d7cc8b7ae123d27856c5fe666d91569a
export ALICE=0x2d178b9704706393d2630fe6cf9415c2c50b181e9e3c7a977237bb2929f82d19
export BOB=0xf2e6ffef7d0543e258d4c47a53d6fa9872de4630cc186950accbd83415b009f0
curl -s https://drand.cloudflare.com/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/latest | jq
{
"round": 4810223,
"randomness": "3a7bb41dc289dcce3b5f4c9ef1067b4861777f30a95a663e97cee46ec6cabc07",
"signature": "a276c597d6525953db73c02713eac9409e847f5c696898d6f5827a41926ddface6133b0da770947a5f1151131a197142"
}
希望10分钟后开奖,计算开奖轮次
4810223 + 10 * 60 / 3 = 4810423
export ROUND=4810423
切换到Jason账号
sui client publish --gas-budget 100000000
export PACKAGE_ID=0x8f56999250f11fbe56d2991fb04ecde09a26a90f996c6158f448afecc0a6f44a
create
)切换到Jason
sui client call --function create --package $PACKAGE_ID --module drand_based_lottery --args $ROUND --gas-budget 10000000
# PACKAGE_ID::drand_based_lottery::Game
export GAME=0xe8adb4acfd37cbcdf4c31ce81ccaf7b156640469901902edaa7b111abf0aee64
sui client object $GAME
participate
)切换到Alice
sui client call --function participate --package $PACKAGE_ID --module drand_based_lottery --args $GAME --gas-budget 10000000
export TICKET_ALICE=0x4adae34c24d3d15d4f4ddd29fef9cfb165227cb04e0bd1e79c93c7e10bfbfac3
sui client object $TICKET_ALICE
participate
)切换到Bob
sui client call --function participate --package $PACKAGE_ID --module drand_based_lottery --args $GAME --gas-budget 10000000
export TICKET_BOB=0x44049281fb0949ca687145cef6df7f6ea131ebcbdf07c8df71c82e97ebf61d9e
sui client object $TICKET_BOB
close
)curl -s https://drand.cloudflare.com/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/`expr $ROUND - 20` | jq
{
"round": 4810403,
"randomness": "05bb6056fa30ec0b08e32ee87615049a46a18cde2034286b72b0313d0e05d3b8",
"signature": "8d6325568164ceb741f5a14802c18eb15e05929580fabd7e4969e7aa815432dd6422f7fbfa67db46d80abb552722235c"
}
切换到Bob
export DRAND_SIG=0x8d6325568164ceb741f5a14802c18eb15e05929580fabd7e4969e7aa815432dd6422f7fbfa67db46d80abb552722235c
sui client call --function close --package $PACKAGE_ID --module drand_based_lottery --args $GAME $DRAND_SIG --gas-budget 10000000
sui client object $GAME
$ sui client call --function participate --package $PACKAGE_ID --module drand_based_lottery --args $GAME --gas-budget 10000000
Error executing transaction: Failure {
error: "MoveAbort(MoveLocation { module: ModuleId { address: 8f56999250f11fbe56d2991fb04ecde09a26a90f996c6158f448afecc0a6f44a, name: Identifier(\"drand_based_lottery\") }, function: 3, instruction: 12, function_name: Some(\"participate\") }, 0) in command 0",
}
curl -s https://drand.cloudflare.com/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/$ROUND | jq
{
"round": 4810423,
"randomness": "11605d0a4b80663f8be4a1b30b8e12f73c84ec21c47f4bb4febc0fd7a916e795",
"signature": "8f86d67345c5e296556c551757d7b28fb5d34b0c60377e63b56358f1db987fff604f0534ad968ae9bc75f1434d9f46f9"
}
export DRAND_SIG=0x8f86d67345c5e296556c551757d7b28fb5d34b0c60377e63b56358f1db987fff604f0534ad968ae9bc75f1434d9f46f9
sui client call --function complete --package $PACKAGE_ID --module drand_based_lottery --args $GAME $DRAND_SIG --gas-budget 10000000
sui client object $GAME
redeem
)切换到中奖者Bob
sui client call --function redeem --package $PACKAGE_ID --module drand_based_lottery --args $TICKET_BOB $GAME --gas-budget 10000000
$ sui client object 0x5d6a9bc43ecaaab57e7fb2fcb2b03a76437325cab1c619da70ff360ff70ab594
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!