Skip to content
This repository has been archived by the owner on Nov 15, 2022. It is now read-only.

Implement shuffle query #4

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ version = "0.2.0"
license = "AGPL-3.0"
authors = ["Simon Warta <[email protected]>"]
edition = "2018"
resolver = "2"

exclude = [
# Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication.
Expand Down Expand Up @@ -38,6 +39,9 @@ cw2 = "0.13"
schemars = "0.8.3"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
thiserror = "1.0"
rand_xoshiro = { version = "0.6.0", default-features = false }
rand_core = { version = "0.6", default-features = false }
shuffle = { git = "https://github.com/webmaster128/shuffle", branch = "rm-getrandom" }

[dev-dependencies]
cosmwasm-schema = { version = "1.0.0-beta6" }
Expand Down
154 changes: 151 additions & 3 deletions src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
use std::convert::TryInto;

use cosmwasm_std::entry_point;
use cosmwasm_std::{
coins, to_binary, BankMsg, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, StdResult,
Storage, SubMsg,
coins, to_binary, BankMsg, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, StdError,
StdResult, Storage, SubMsg,
};
use drand_verify::{derive_randomness, g1_from_variable, verify};
use rand_core::SeedableRng;
use rand_xoshiro::Xoshiro128PlusPlus;
use shuffle::{fy::FisherYates, shuffler::Shuffler};

use crate::errors::ContractError;
use crate::msg::{
BountiesResponse, Bounty, ConfigResponse, ExecuteMsg, GetResponse, InstantiateMsg,
LatestResponse, QueryMsg,
LatestResponse, QueryMsg, ShuffleResponse,
};
use crate::state::{
beacons_storage, beacons_storage_read, bounties_storage, bounties_storage_read, config,
Expand Down Expand Up @@ -129,6 +134,7 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result<Binary, ContractErr
QueryMsg::Get { round } => to_binary(&query_get(deps, round)?)?,
QueryMsg::Latest {} => to_binary(&query_latest(deps)?)?,
QueryMsg::Bounties {} => to_binary(&query_bounties(deps)?)?,
QueryMsg::Shuffle { round, from, to } => to_binary(&query_shuffle(deps, round, from, to)?)?,
};
Ok(response)
}
Expand Down Expand Up @@ -182,6 +188,37 @@ fn query_bounties(deps: Deps) -> Result<BountiesResponse, ContractError> {
})
}

fn query_shuffle(
deps: Deps,
round: u64,
from: u32,
to: u32,
) -> Result<ShuffleResponse, ContractError> {
if from > to {
return Err(ContractError::InvalidRange {});
}
let beacons = beacons_storage_read(deps.storage);
let randomness = beacons
.get(&round.to_be_bytes())
.ok_or(ContractError::BeaconNotFound {})?;
let randomness: [u8; 16] = randomness[0..16].try_into().unwrap(); // Cut first 16 bytes from 32 byte value

// A PRNG that is not cryptographically secure.
// See https://docs.rs/rand/0.8.5/rand/rngs/struct.SmallRng.html
// where this is used for 32 bit systems.
// We don't use the SmallRng in order to get the same implementation
// in unit tests (64 bit dev machines) and the real contract (32 bit Wasm)
let mut rng = Xoshiro128PlusPlus::from_seed(randomness);
let mut list: Vec<u32> = (from..=to).collect();

let mut shuffler = FisherYates::default();
shuffler
.shuffle(&mut list, &mut rng)
.map_err(StdError::generic_err)?;

Ok(ShuffleResponse { list })
}

fn get_bounty(storage: &dyn Storage, round: u64) -> StdResult<u128> {
let key = round.to_be_bytes();
let bounties = bounties_storage_read(storage);
Expand Down Expand Up @@ -699,4 +736,115 @@ mod tests {
}
);
}

#[test]
fn query_shuffle_works() {
let mut deps = mock_dependencies();

let info = mock_info("creator", &[]);
let msg = InstantiateMsg {
pubkey: pubkey_loe_mainnet(),
bounty_denom: BOUNTY_DENOM.into(),
};
instantiate(deps.as_mut(), mock_env(), info, msg).unwrap();

// Beacon does not exist

let response: GetResponse =
from_binary(&query(deps.as_ref(), mock_env(), QueryMsg::Get { round: 42 }).unwrap())
.unwrap();
assert_eq!(response.randomness, Binary::default());

// Beacon exists

let msg = ExecuteMsg::Add {
// curl -sS https://drand.cloudflare.com/public/42 | jq
round: 42,
previous_signature: hex::decode("a418fccbfaa0c84aba8cbcd4e3c0555170eb2382dfed108ecfc6df249ad43efe00078bdcb5060fe2deed4731ca5b4c740069aaf77927ba59c5870ab3020352aca3853adfdb9162d40ec64f71b121285898e28cdf237e982ac5c4deb287b0d57b").unwrap().into(),
signature: hex::decode("9469186f38e5acdac451940b1b22f737eb0de060b213f0326166c7882f2f82b92ce119bdabe385941ef46f72736a4b4d02ce206e1eb46cac53019caf870080fede024edcd1bd0225eb1335b83002ae1743393e83180e47d9948ab8ba7568dd99").unwrap().into(),
};
execute(deps.as_mut(), mock_env(), mock_info("anyone", &[]), msg).unwrap();

let response_data = query(
deps.as_ref(),
mock_env(),
QueryMsg::Shuffle {
round: 42,
from: 0,
to: 4,
},
)
.unwrap();
let response: ShuffleResponse = from_binary(&response_data).unwrap();
assert_eq!(response.list, [4, 3, 0, 2, 1]);

let response_data = query(
deps.as_ref(),
mock_env(),
QueryMsg::Shuffle {
round: 42,
from: 0,
to: 5,
},
)
.unwrap();
let response: ShuffleResponse = from_binary(&response_data).unwrap();
assert_eq!(response.list, [5, 0, 4, 3, 2, 1]);

let response_data = query(
deps.as_ref(),
mock_env(),
QueryMsg::Shuffle {
round: 42,
from: 3,
to: 5,
},
)
.unwrap();
let response: ShuffleResponse = from_binary(&response_data).unwrap();
assert_eq!(response.list, [5, 3, 4]);

let response_data = query(
deps.as_ref(),
mock_env(),
QueryMsg::Shuffle {
round: 42,
from: 5,
to: 5,
},
)
.unwrap();
let response: ShuffleResponse = from_binary(&response_data).unwrap();
assert_eq!(response.list, [5]);

let err = query(
deps.as_ref(),
mock_env(),
QueryMsg::Shuffle {
round: 42,
from: 6,
to: 5,
},
)
.unwrap_err();
match err {
ContractError::InvalidRange {} => {}
err => panic!("Unexpected error: {}", err),
}

let err = query(
deps.as_ref(),
mock_env(),
QueryMsg::Shuffle {
round: 33,
from: 4,
to: 10,
},
)
.unwrap_err();
match err {
ContractError::BeaconNotFound {} => {}
err => panic!("Unexpected error: {}", err),
}
}
}
4 changes: 4 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@ pub enum ContractError {
NoFundsSent { expected_denom: String },
#[error("No beacon exists in the database")]
NoBeacon {},
#[error("Beacon does not exist for this round")]
BeaconNotFound {},
#[error("Range is not valid")]
InvalidRange {},
}
17 changes: 16 additions & 1 deletion src/msg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,18 @@ pub enum ExecuteMsg {
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
Config {},
Get { round: u64 },
Get {
round: u64,
},
Latest {},
Bounties {},
/// Creates a list of integers [from, to] and shuffles it
/// given the randomess of the provided round.
Shuffle {
round: u64,
from: u32,
to: u32,
},
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
Expand Down Expand Up @@ -58,3 +67,9 @@ pub struct Bounty {
pub struct BountiesResponse {
pub bounties: Vec<Bounty>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct ShuffleResponse {
/// The shuffled list
pub list: Vec<u32>,
}
Loading