├── rust-toolchain ├── .gitignore ├── assets ├── demo.gif ├── handwaving-cryptography.md └── building-dee.md ├── dee ├── src │ ├── cmd.rs │ ├── print.rs │ ├── time.rs │ ├── main.rs │ ├── cmd │ │ ├── remote.rs │ │ ├── rand.rs │ │ └── crypt.rs │ ├── config.rs │ └── cli.rs ├── build.rs ├── Cargo.toml └── CHANGELOG.md ├── Cargo.toml ├── drand_core ├── examples │ └── coin_flip.rs ├── Cargo.toml ├── src │ ├── lib.rs │ ├── bls_signatures.rs │ ├── chain.rs │ ├── http_client.rs │ └── beacon.rs ├── CHANGELOG.md └── README.md ├── LICENSE ├── .github └── workflows │ ├── ci.yaml │ └── release.yaml └── README.md /rust-toolchain: -------------------------------------------------------------------------------- 1 | 1.74 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thibmeu/drand-rs/HEAD/assets/demo.gif -------------------------------------------------------------------------------- /dee/src/cmd.rs: -------------------------------------------------------------------------------- 1 | pub mod crypt; 2 | pub mod rand; 3 | pub use rand::rand; 4 | pub mod remote; 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "dee", 5 | "drand_core", 6 | ] 7 | 8 | resolver = "1" 9 | 10 | [workspace.dependencies] 11 | hex = "0.4.3" 12 | serde = "1.0.197" 13 | serde_json = "1.0.114" 14 | time = "0.3.34" 15 | -------------------------------------------------------------------------------- /drand_core/examples/coin_flip.rs: -------------------------------------------------------------------------------- 1 | use drand_core::HttpClient; 2 | use rand::{seq::SliceRandom, SeedableRng}; 3 | use rand_chacha::ChaCha20Rng; 4 | 5 | /// Flip a coin using the latest drand beacon. 6 | /// The output is deterministic, and based on this latest beacon. 7 | fn main() { 8 | // Create a new client and retrieve the latest beacon. By default, it verifies its signature against the chain info. 9 | let client: HttpClient = "https://api.drand.sh".try_into().unwrap(); 10 | let latest = client.latest().unwrap(); 11 | let round = latest.round(); 12 | 13 | // Create a new seeded RNG. For a given beacon, the coin flip result is deterministic. 14 | let seed: ::Seed = latest.randomness().try_into().unwrap(); 15 | let mut rng = ChaCha20Rng::from_seed(seed); 16 | 17 | // Flip a coin using the seeded RNG. 18 | let coin = ["HEAD", "TAIL"]; 19 | let flip = coin.choose(&mut rng).unwrap(); 20 | println!("{flip} (round {round})"); 21 | } 22 | -------------------------------------------------------------------------------- /dee/src/print.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::ValueEnum; 3 | 4 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] 5 | pub enum Format { 6 | /// Text based format with a single result 7 | Short, 8 | /// Text based format with colors and font weight 9 | Long, 10 | /// Raw and minified JSON 11 | Json, 12 | } 13 | 14 | impl Format { 15 | pub fn new(long: bool, json: bool) -> Self { 16 | match (long, json) { 17 | (false, false) => Self::Short, 18 | (true, false) => Self::Long, 19 | (false, true) => Self::Json, 20 | (true, true) => unreachable!("long and json format cannot be true together"), 21 | } 22 | } 23 | } 24 | 25 | pub trait Print { 26 | fn short(&self) -> Result; 27 | fn long(&self) -> Result; 28 | fn json(&self) -> Result; 29 | } 30 | 31 | pub fn print_with_format(t: T, format: Format) -> Result { 32 | match format { 33 | Format::Short => t.short(), 34 | Format::Long => t.long(), 35 | Format::Json => t.json(), 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Thibault Meunier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /drand_core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "drand_core" 3 | description = "A drand client library." 4 | version = "0.0.16" 5 | authors = ["Thibault Meunier "] 6 | edition = "2021" 7 | readme = "./README.md" 8 | homepage = "https://github.com/thibmeu/drand-rs/tree/main/drand_core" 9 | repository = "https://github.com/thibmeu/drand-rs" 10 | keywords = ["drand", "rng", "random"] 11 | categories = ["cryptography"] 12 | license = "MIT" 13 | 14 | [dependencies] 15 | ark-bls12-381 = "0.4.0" 16 | ark-ec = "0.4.2" 17 | ark-ff = "0.4.2" 18 | ark-serialize = "0.4.2" 19 | hex = { workspace = true, features = ["serde"] } 20 | rand = "0.8.5" 21 | serde = { workspace = true, features = ["derive", "rc"] } 22 | serde_json = { workspace = true } 23 | sha2 = "0.10.8" 24 | thiserror = "1.0.57" 25 | time = { workspace = true, features = ["parsing", "serde-well-known"], optional = true } 26 | ureq = { version = "2.9.6", features = ["json"] } 27 | url = { version = "2.5", features = ["serde"] } 28 | 29 | [target.'cfg(wasm32)'.dependencies] 30 | getrandom = { version = "0.2.12", features = ["js"] } 31 | ring = { version = "0.17.8", features = ["less-safe-getrandom-custom-or-rdrand", "wasm32_unknown_unknown_js"]} 32 | 33 | [features] 34 | default = ["time", "native-certs"] 35 | native-certs = ["ureq/native-certs"] 36 | time = ["dep:time"] 37 | 38 | [dev-dependencies] 39 | hex-literal = "0.4.1" 40 | mockito = "1.4.0" 41 | rand_chacha = "0.3.1" 42 | -------------------------------------------------------------------------------- /dee/build.rs: -------------------------------------------------------------------------------- 1 | #[path = "src/cli.rs"] 2 | mod cli; 3 | 4 | use clap_complete::Shell::{Bash, Elvish, Fish, PowerShell, Zsh}; 5 | use flate2::{write::GzEncoder, Compression}; 6 | use std::io::Write; 7 | 8 | const COMPLETIONS_DIR: &str = "../target/completions"; 9 | const MANPAGES_DIR: &str = "../target/manpages"; 10 | 11 | fn create_folder(path: &str) -> std::path::PathBuf { 12 | let path = std::path::PathBuf::from(path); 13 | std::fs::create_dir_all(path.clone()).unwrap(); 14 | path 15 | } 16 | 17 | fn main() -> std::io::Result<()> { 18 | let completions_dir = create_folder(COMPLETIONS_DIR); 19 | let manpages_dir = create_folder(MANPAGES_DIR); 20 | 21 | let cmd = ::command(); 22 | 23 | let man = clap_mangen::Man::new(cmd); 24 | let mut buffer: Vec = Default::default(); 25 | man.render(&mut buffer)?; 26 | 27 | let path = manpages_dir.join("dee.1.gz"); 28 | let file = 29 | std::fs::File::create(path).expect("Should be able to open file in target directory"); 30 | let mut encoder = GzEncoder::new(file, Compression::best()); 31 | encoder 32 | .write_all(&buffer) 33 | .expect("Should be able to write to file in target directory"); 34 | 35 | let cmd = &mut ::command(); 36 | for shell in [Bash, Elvish, Fish, PowerShell, Zsh] { 37 | let _path = clap_complete::generate_to(shell, cmd, "dee", completions_dir.clone())?; 38 | } 39 | 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /drand_core/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # drand-core 2 | //! 3 | //! drand-core is a library to retrieve public randomness generated by drand beacons. It features an HTTP client, and verification method. 4 | //! 5 | //! The format specification is at [drand.love/docs/specification](https://drand.love/docs/specification/). drand was designed in [Scalable Bias-Resistant Distributed Randomness](https://eprint.iacr.org/2016/1067.pdf). 6 | //! 7 | //! The reference interroperable Go implementation is available at [drand/drand](https://github.com/drand/drand). 8 | //! 9 | //! ## Usage 10 | //! 11 | //! ```rust 12 | //! use drand_core::HttpClient; 13 | //! 14 | //! // Create a new client 15 | //! let client: HttpClient = "https://drand.cloudflare.com".try_into().unwrap(); 16 | //! 17 | //! // Get the latest beacon. By default, it verifies its signature against the chain info. 18 | //! let beacon = client.latest().unwrap(); 19 | //! 20 | //! // Print the beacon 21 | //! println!("{:?}", beacon); 22 | //! ``` 23 | 24 | pub mod beacon; 25 | mod bls_signatures; 26 | pub mod chain; 27 | pub use chain::ChainOptions; 28 | mod http_client; 29 | pub use http_client::HttpClient; 30 | use thiserror::Error; 31 | 32 | #[derive(Error, Debug)] 33 | pub enum DrandError { 34 | #[error(transparent)] 35 | Beacon(#[from] Box), 36 | #[error(transparent)] 37 | HTTPClient(#[from] Box), 38 | #[error(transparent)] 39 | Signature(#[from] Box), 40 | } 41 | 42 | type Result = std::result::Result; 43 | -------------------------------------------------------------------------------- /dee/src/time.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use colored::Colorize; 3 | use drand_core::beacon::RandomnessBeaconTime; 4 | use drand_core::{ChainOptions, HttpClient}; 5 | use time::format_description::well_known::Rfc3339; 6 | 7 | use crate::config::ConfigChain; 8 | use crate::print::Print; 9 | 10 | impl Print for RandomnessBeaconTime { 11 | fn short(&self) -> Result { 12 | Ok(format!("{}", self.round())) 13 | } 14 | 15 | fn long(&self) -> Result { 16 | Ok(format!( 17 | r"{: <10}: {} 18 | {: <10}: {} 19 | {: <10}: {}", 20 | "Round".bold(), 21 | self.round(), 22 | "Relative".bold(), 23 | self.relative(), 24 | "Absolute".bold(), 25 | self.absolute().format(&Rfc3339)?, 26 | )) 27 | } 28 | 29 | fn json(&self) -> Result { 30 | Ok(serde_json::to_string(self)?) 31 | } 32 | } 33 | 34 | pub fn round_from_option( 35 | chain: &ConfigChain, 36 | round: Option, 37 | ) -> Result { 38 | let info = chain.info(); 39 | 40 | let client = HttpClient::new( 41 | &chain.url(), 42 | Some(ChainOptions::new(true, true, Some(info.clone().into()))), 43 | )?; 44 | 45 | let round = match round { 46 | Some(round) => round, 47 | None => client.latest()?.round().to_string(), 48 | }; 49 | 50 | match RandomnessBeaconTime::parse(&info.into(), &round) { 51 | Ok(time) => Ok(time), 52 | Err(_) => Err(anyhow!("Invalid beacon round \"{}\"", round)), 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /dee/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dee" 3 | description = "An cli for drand, with support for timelock encryption." 4 | version = "0.0.16" 5 | authors = ["Thibault Meunier "] 6 | edition = "2021" 7 | readme = "../README.md" 8 | homepage = "https://github.com/thibmeu/drand-rs" 9 | repository = "https://github.com/thibmeu/drand-rs" 10 | keywords = ["drand", "cli", "rng", "random"] 11 | categories = ["command-line-utilities", "cryptography"] 12 | default-run = "dee" 13 | license = "MIT" 14 | 15 | build = "build.rs" 16 | 17 | [package.metadata.deb] 18 | name = "dee" 19 | extended-description = """\ 20 | dee is a tool to retrieve public randomness generated by drand beacons, and \ 21 | perform. It features a drand client, automated verification, and timelock \ 22 | encryption.""" 23 | section = "utils" 24 | assets = [ 25 | ["target/release/dee", "usr/bin/", "755"], 26 | ["../target/completions/dee.bash", "usr/share/bash-completion/completions/dee", "644"], 27 | ["../target/completions/dee.fish", "usr/share/fish/completions/", "644"], 28 | ["../target/completions/_dee", "usr/share/zsh/functions/Completion/Debian/", "644"], 29 | ["../target/manpages/dee.1.gz", "usr/share/man/man1/", "644"], 30 | ["../README.md", "usr/share/doc/dee/README.md", "644"], 31 | ] 32 | 33 | [badges] 34 | maintenance = { status = "experimental" } 35 | 36 | [dependencies] 37 | anyhow = "1.0.80" 38 | clap = { version = "4.5.2", features = ["derive"] } 39 | clap-verbosity-flag = "2.2.0" 40 | colored = "2.1.0" 41 | confy = "0.6.1" 42 | drand_core = { path = "../drand_core", version = "0.0.16" } 43 | env_logger = "0.10.2" 44 | hex = { workspace = true } 45 | log = "0.4.21" 46 | serde = { workspace = true, features = ["derive"] } 47 | serde_json = { workspace = true } 48 | time = { workspace = true, features = ["parsing", "serde-well-known"] } 49 | tlock_age = { features = ["armor"], version = "0.0.5" } 50 | tlock_age_non_rfc9380 = { package="tlock_age", version = "0.0.3", features = ["armor"] } 51 | 52 | [build-dependencies] 53 | clap = "4.5.2" 54 | clap-verbosity-flag = "2.2.0" 55 | clap_complete = "4.5.1" 56 | clap_mangen = "0.2.20" 57 | flate2 = "1.0.28" 58 | -------------------------------------------------------------------------------- /dee/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. Changes to the [drand_core crate](../drand_core/CHANGELOG.md) also apply to the dee CLI tools, and are not duplicated here. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.0.16] - 2024-03-09 10 | 11 | ### Changed 12 | 13 | - Update dependencies 14 | 15 | ### Fix 16 | 17 | - Fix error message when round cannot be parsed 18 | 19 | ## [0.0.15] - 2024-02-29 20 | 21 | ### Changed 22 | 23 | - Update dependencies 24 | - Update Rust to 1.74 25 | 26 | ## [0.0.14] - 2023-08-30 27 | 28 | ### Added 29 | 30 | - Improved error messages on network failure 31 | - Automatically set upstream on the first added remote 32 | - RFC3339 time format cut to second precision for readability 33 | - Suggest compatible network when upstream is incorrect 34 | 35 | ## [0.0.13] - 2023-08-23 36 | 37 | ### Fix 38 | 39 | - RFC 9380 and non RFC 9380 beacons 40 | 41 | ## [0.0.12] - 2023-08-09 42 | 43 | ### Added 44 | 45 | - dee: add metadata for future beacons 46 | - dee: add inspect for tlock age header 47 | 48 | ## [0.0.11] - 2023-08-08 49 | 50 | ## [0.0.10] - 2023-08-08 51 | 52 | ### Changed 53 | 54 | - Update dependencies 55 | 56 | ## [0.0.9] - 2023-08-01 57 | 58 | ### Fix 59 | 60 | - Fix GitHub Actions apt package install 61 | 62 | ## [0.0.8] - 2023-07-23 63 | 64 | ### Fix 65 | 66 | - Fix typo in `dee rand` help message 67 | 68 | ## [0.0.7] - 2023-04-10 69 | 70 | ### Added 71 | 72 | - Add documentation for design considerations 73 | 74 | ### Changed 75 | 76 | - Update dependencies 77 | - Update time library from chrono to time 78 | - Update dee error handling on HTTP remotes 79 | - Update tlock_age to v0.0.2 for improved performance 80 | 81 | ### Fix 82 | 83 | - Fix Chain error handling on wrong URL string 84 | 85 | ## [0.0.6] - 2023-03-22 86 | 87 | ### Added 88 | 89 | - Decryption from stdin 90 | - Compatibility with [drand/tlock](https://github.com/drand/tlock) and [drand/tlock-js](https://github.com/drand/tlock) 91 | 92 | ### Changed 93 | - README to detail current and planned features, as well as references 94 | -------------------------------------------------------------------------------- /drand_core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.0.16] - 2024-03-09 10 | 11 | ### Changed 12 | 13 | - Change RFC 3339 datetime parsing to allow space and date 14 | 15 | ### Fix 16 | 17 | - Fix clippy warning for wasm32-unknown-unknown target 18 | 19 | ## [0.0.15] - 2024-02-29 20 | 21 | ### Added 22 | 23 | - Add a check on the returned round value 24 | 25 | ### Changed 26 | 27 | - Update dependencies 28 | - Update Rust to 1.74 29 | 30 | ## [0.0.14] - 2023-08-30 31 | 32 | ### Added 33 | 34 | - Dedicated error type 35 | - Add use of OS certificate store when available 36 | 37 | ## [0.0.13] - 2023-08-23 38 | 39 | ### Added 40 | 41 | - Add rfc9380 helper method for ChainInfo 42 | 43 | ### Fix 44 | 45 | - Scheme ID of RFC 9380 is bls-unchained-g1-rfc9380 46 | 47 | ## [0.0.11] - 2023-08-08 48 | 49 | ### Changed 50 | 51 | - Public struct for chain time info 52 | 53 | ## [0.0.10] - 2023-08-08 54 | 55 | ### Added 56 | 57 | - Built-in beacon time estimation 58 | 59 | ### Fix 60 | 61 | - Wasm32 build 62 | 63 | ## [0.0.9] - 2023-08-01 64 | 65 | ### Changed 66 | 67 | - Update HTTP Client to use ureq 68 | 69 | ### Fix 70 | 71 | - Fix unecessary ark_bls12_381 boilerplate 72 | - Fix G1 -> G2 in error messages 73 | 74 | ## [0.0.8] - 2023-07-23 75 | 76 | ### Added 77 | 78 | - Add G1 scheme conformant to Hash to Curve RFC 79 | 80 | ## [0.0.7] - 2023-04-10 81 | 82 | ### Added 83 | 84 | - Add coin_clip example for drand_core 85 | - Add `get_by_unix_time` method on drand HttpClient 86 | 87 | ### Changed 88 | 89 | - Update dependencies 90 | - Update drand_core public API to expose HttpClient 91 | - Update bls12-381 library to arkworks/curves 92 | 93 | ### Fix 94 | 95 | - Fix Chain error handling on wrong URL string 96 | 97 | ### Remove 98 | 99 | - Remove Chain struct abstraction 100 | 101 | ## [0.0.6] - 2023-03-22 102 | 103 | ### Changed 104 | 105 | - README to detail current and planned features 106 | -------------------------------------------------------------------------------- /assets/handwaving-cryptography.md: -------------------------------------------------------------------------------- 1 | # Handwaving Cryptography 2 | 3 | > This is not a rigurous cryptography explanation, coming from a non-cryptographer. Takes this with caution, and refer to [drand](https://eprint.iacr.org/2016/1067) and [tlock](https://eprint.iacr.org/2023/189) paper as a source. 4 | 5 | ## drand 6 | 7 | In the [drand network](https://drand.love), each participants has its own secret key, and the group (the League of Entropy) uses a public key derived from these private key shares. 8 | 9 | During the setup call, each participants choose a number at random. This gives us group secret key `s`, and it's associated public key `S = s*g1`. 10 | 11 | drand leverages a bilinear group `G1xG2 -> Gt`. On this group, we know a function `e` , implying `e(aP, bQ) = ab*e(P,Q)`. We also know a secure [hash function](https://en.wikipedia.org/wiki/Hash_function) `H`. 12 | 13 | At each round, the group performs a BLS signature `sigma` over a deterministic message `m` : `sigma = s*H(m)`. 14 | 15 | To verify a signature, dee client computes `e(S, H(m)) = e(s*g1, H(m)) = e(g1, s*H(m)) = e(g1, sigma)` using bilinearity. 16 | 17 | Randomness is `rand = sha256(sigma)`. 18 | 19 | The message `m` depends on the network mode: 20 | * With chained randomness, message is `(round || previous_signature)` 21 | * With unchained randomness, message is `(round)`. 22 | 23 | The advantage of unchained randomness is it allows the message which the group is going to sign ahead of time. The security assumption remains the same: nodes do not collude. 24 | 25 | ## Timelock encryption 26 | 27 | drand beacons produce signatures at regular interval (quicknet uses 3s for instance), which is the minimum interval we can lock data for. We assimilates this to a clock. We know that signature for round `p` is going to be for message `m = (p)`. We also know the public key `S`. 28 | 29 | We want to encrypt text `M` towards round `p`. 30 | 31 | To encrypt, we perform the following operations 32 | 33 | ```text 34 | INPUT 35 | (M, p, S) 36 | COMPUTE 37 | PK = e(S, H(p)) 38 | nonce = 32 random bytes 39 | r = H(nonce, M) 40 | // ciphertext 41 | U = r*g1 42 | V = nonce xor H(r*PK) 43 | W = M xor H(nonce) 44 | OUTPUT 45 | (U, V, W) 46 | ``` 47 | 48 | Once we know the signature `sigma = s*H(p)` for decryption round `p`, we can decrypt `M` 49 | 50 | ```text 51 | INPUT 52 | (U, V, W, sigma, p) 53 | COMPUTE 54 | nonce' = V xor H(e(U, sigma)) 55 | = sigma xor H(r*e(S, H(p))) xor H(e(U, s*H(p))) 56 | = sigma xor H(rs*e(g1, H(p))) xor H(rs*e(g1, H(p))) 57 | = nonce 58 | M' = W xor H(nonce') 59 | = M xor H(nonce) xor H(nonce') 60 | = M 61 | OUTPUT 62 | (M') 63 | ``` 64 | -------------------------------------------------------------------------------- /dee/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use std::process; 3 | 4 | mod cli; 5 | mod cmd; 6 | mod config; 7 | mod print; 8 | mod time; 9 | 10 | fn main() { 11 | let cli = cli::build(); 12 | let mut cfg: config::Local = config::Local::load(); 13 | env_logger::Builder::new() 14 | .filter_level(cli.verbose.log_level_filter()) 15 | .init(); 16 | 17 | let output = match cli.command { 18 | cli::Commands::Rand { 19 | set_upstream, 20 | verify, 21 | long, 22 | json, 23 | beacon, 24 | } => match cfg.set_upstream_and_chain(set_upstream) { 25 | Ok(chain) => cmd::rand(&cfg, print::Format::new(long, json), chain, beacon, verify), 26 | Err(err) => Err(err), 27 | }, 28 | cli::Commands::Crypt { 29 | encrypt, 30 | decrypt, 31 | inspect, 32 | set_upstream, 33 | round, 34 | armor, 35 | output, 36 | input, 37 | } => { 38 | let is_inspect = inspect.is_true(); 39 | match cfg.set_upstream_and_chain(set_upstream) { 40 | Ok(chain) => match (encrypt, decrypt, is_inspect) { 41 | (true, false, false) => { 42 | cmd::crypt::encrypt(&cfg, output, input, armor, chain, round) 43 | } 44 | (_, true, _) => cmd::crypt::decrypt(&cfg, output, input, chain), 45 | (_, _, true) => cmd::crypt::inspect( 46 | &cfg, 47 | print::Format::new(inspect.long(), inspect.json()), 48 | input, 49 | chain, 50 | ), 51 | _ => unreachable!(), 52 | }, 53 | Err(err) => Err(err), 54 | } 55 | } 56 | cli::Commands::Remote { command } => match command { 57 | Some(command) => match command { 58 | cli::RemoteCommand::Add { name, url } => cmd::remote::add(&mut cfg, name, &url), 59 | cli::RemoteCommand::Remove { name } => cmd::remote::remove(&mut cfg, name), 60 | cli::RemoteCommand::Rename { old, new } => cmd::remote::rename(&mut cfg, old, new), 61 | cli::RemoteCommand::SetUrl { name, url } => { 62 | cmd::remote::set_url(&mut cfg, name, &url) 63 | } 64 | cli::RemoteCommand::Show { long, json, name } => cmd::remote::show( 65 | &cfg, 66 | print::Format::new(long, json), 67 | name.or(cfg.upstream()) 68 | .ok_or(anyhow!("No chain or upstream")) 69 | .unwrap(), 70 | ), 71 | }, 72 | None => cmd::remote::list(&cfg), 73 | }, 74 | }; 75 | 76 | match output { 77 | Ok(result) => { 78 | cfg.store().unwrap(); 79 | if !result.is_empty() { 80 | println!("{result}") 81 | } 82 | } 83 | Err(err) => { 84 | eprintln!("error: {err}"); 85 | process::exit(1) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI checks 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Test on ${{ matrix.os }} 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions-rs/toolchain@v1 16 | with: 17 | toolchain: 1.74 18 | override: true 19 | - name: cargo fetch 20 | uses: actions-rs/cargo@v1 21 | with: 22 | command: fetch 23 | - name: Build tests 24 | uses: actions-rs/cargo@v1 25 | with: 26 | command: build 27 | args: --all --verbose --exclude dee --all-features --tests 28 | - name: Run tests 29 | uses: actions-rs/cargo@v1 30 | with: 31 | command: test 32 | args: --all --verbose --exclude dee --all-features 33 | 34 | build: 35 | name: Build target ${{ matrix.target }} 36 | runs-on: ubuntu-latest 37 | strategy: 38 | matrix: 39 | target: 40 | - wasm32-unknown-unknown 41 | 42 | steps: 43 | - uses: actions/checkout@v3 44 | - uses: actions-rs/toolchain@v1 45 | with: 46 | toolchain: 1.74 47 | override: true 48 | - name: Add target 49 | run: rustup target add ${{ matrix.target }} 50 | - name: cargo fetch 51 | uses: actions-rs/cargo@v1 52 | with: 53 | command: fetch 54 | - name: Build for target 55 | working-directory: ./drand_core 56 | run: cargo build --verbose --no-default-features --target ${{ matrix.target }} 57 | 58 | bitrot: 59 | name: Bitrot 60 | runs-on: ubuntu-latest 61 | 62 | steps: 63 | - uses: actions/checkout@v3 64 | - uses: actions-rs/toolchain@v1 65 | with: 66 | toolchain: 1.74 67 | override: true 68 | - name: cargo check 69 | uses: actions-rs/cargo@v1 70 | with: 71 | command: check 72 | args: --tests --examples --benches --all-features 73 | 74 | clippy: 75 | name: Clippy (1.74) 76 | runs-on: ubuntu-latest 77 | 78 | steps: 79 | - uses: actions/checkout@v3 80 | - uses: actions-rs/toolchain@v1 81 | with: 82 | toolchain: 1.74 83 | components: clippy 84 | override: true 85 | - name: Clippy check 86 | uses: actions-rs/clippy-check@v1 87 | with: 88 | name: Clippy (1.74) 89 | token: ${{ secrets.GITHUB_TOKEN }} 90 | args: --all-features --all-targets -- -D warnings 91 | 92 | doc-links: 93 | name: Intra-doc links 94 | runs-on: ubuntu-latest 95 | 96 | steps: 97 | - uses: actions/checkout@v3 98 | - uses: actions-rs/toolchain@v1 99 | with: 100 | toolchain: 1.74 101 | override: true 102 | - name: cargo fetch 103 | uses: actions-rs/cargo@v1 104 | with: 105 | command: fetch 106 | 107 | # Ensure intra-documentation links all resolve correctly 108 | # Requires #![deny(intra_doc_link_resolution_failure)] in crates. 109 | - name: Check intra-doc links 110 | uses: actions-rs/cargo@v1 111 | with: 112 | command: doc 113 | args: --all --exclude dee --all-features --document-private-items 114 | 115 | fmt: 116 | name: Rustfmt 117 | runs-on: ubuntu-latest 118 | 119 | steps: 120 | - uses: actions/checkout@v3 121 | - uses: actions-rs/toolchain@v1 122 | with: 123 | toolchain: 1.74 124 | components: rustfmt 125 | override: true 126 | - name: Check formatting 127 | uses: actions-rs/cargo@v1 128 | with: 129 | command: fmt 130 | args: --all -- --check 131 | -------------------------------------------------------------------------------- /drand_core/README.md: -------------------------------------------------------------------------------- 1 | # drand-core: Rust implementation of drand 2 | 3 | [![Documentation](https://img.shields.io/badge/docs-main-blue.svg)][Documentation] 4 | ![License](https://img.shields.io/crates/l/drand_core.svg) 5 | [![crates.io](https://img.shields.io/crates/v/drand_core.svg)][Crates.io] 6 | 7 | [Crates.io]: https://crates.io/crates/drand_core 8 | [Documentation]: https://docs.rs/drand_core/ 9 | 10 | drand-core is a library to retrieve public randomness generated by drand beacons. It features an HTTP client, and verification method. 11 | 12 | The format specification is at [drand.love/docs/specification](https://drand.love/docs/specification/). drand was designed in [Scalable Bias-Resistant Distributed Randomness](https://eprint.iacr.org/2016/1067.pdf). 13 | 14 | The reference interroperable Go implementation is available at [drand/drand](https://github.com/drand/drand). 15 | 16 | ## Tables of Content 17 | 18 | * [Features](#features) 19 | * [What's next](#whats-next) 20 | * [Installation](#installation) 21 | * [Usage](#usage) 22 | * [Common remotes](#common-remotes) 23 | * [Security Considerations](#security-considerations) 24 | * [License](#license) 25 | 26 | ## Features 27 | 28 | * Retrieve and verify drand randomness 29 | * Built-in beacon time estimation 30 | * Chain and unchained randomness 31 | * Signatures verification on G1 and G2 32 | * Interroperability with Go and JS implementation 33 | * wasm32 compatible library 34 | 35 | ## What's next 36 | 37 | * P2P randomness retrieval 38 | 39 | ## Installation 40 | 41 | | Environment | CLI Command | 42 | |:-------------------|:---------------------------| 43 | | Cargo (Rust 1.74+) | `cargo install drand_core` | 44 | 45 | The library is tested against the following targets: `x86_64-unknown-linux-gnu`, `armv7-unknown-linux-gnueabihf`, `aarch64-unknown-linux-gnu`, `wasm32-unknown-unknown` 46 | 47 | ## Usage 48 | 49 | Retrieve the latest beacon from `https://drand.cloudflare.com`. 50 | 51 | ```rust 52 | use drand_core::HttpClient; 53 | 54 | // Create a new client. 55 | let client: HttpClient = "https://drand.cloudflare.com".try_into().unwrap(); 56 | 57 | // Get the latest beacon. By default, it verifies its signature against the chain info, and correlates the returned round number with the chain genesis time. 58 | let latest = client.latest()?; 59 | ``` 60 | 61 | Code examples are provided in [drand_core/examples](./examples). You can run them using `cargo run --examples `. 62 | 63 | ### Common remotes 64 | 65 | | ID | Remote | Timelock encryption | 66 | | :---------------------|:------------------------------------------------------------------------------------------------|:--------------------| 67 | | `quicknet-cloudflare` | `https://drand.cloudflare.com/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971` | Yes | 68 | | `quicknet-pl` | `https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971` | Yes | 69 | | `mainnet-cloudflare` | `https://drand.cloudflare.com` | No | 70 | | `mainnet-pl` | `https://api.drand.sh` | No | 71 | 72 | `drand_core` does not come with a default remote beacon. You should decide whichever suit your needs. 73 | 74 | More beacon origins are available on [drand website](https://drand.love/developer/). 75 | 76 | ## Security Considerations 77 | 78 | This library has not been audited. Please use at your sole discretion. 79 | 80 | ## License 81 | 82 | This project is under the MIT license. 83 | 84 | ### Contribution 85 | 86 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you shall be MIT licensed as above, without any additional terms or conditions. -------------------------------------------------------------------------------- /dee/src/cmd/remote.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use colored::Colorize; 3 | use drand_core::HttpClient; 4 | use log::{log_enabled, Level}; 5 | use time::OffsetDateTime; 6 | 7 | use crate::{ 8 | config::{self, ConfigChain}, 9 | print::{self, print_with_format}, 10 | }; 11 | 12 | pub fn add(cfg: &mut config::Local, name: String, url: &str) -> Result { 13 | if cfg.chain(&name).is_some() { 14 | return Err(anyhow!("remote {name} already exists.")); 15 | } 16 | let client: HttpClient = url.try_into()?; 17 | let info = client.chain_info().map_err(|err| { 18 | anyhow!("failed to retrieve information from remote '{name}'. server response: {err}") 19 | })?; 20 | 21 | cfg.add_chain(name.clone(), ConfigChain::new(url, info))?; 22 | 23 | Ok(name) 24 | } 25 | 26 | pub fn remove(cfg: &mut config::Local, name: String) -> Result { 27 | if cfg.chain(&name).is_none() { 28 | return Err(anyhow!("no such remote '{name}'.")); 29 | } 30 | cfg.remove_chain(name.clone())?; 31 | 32 | Ok(name) 33 | } 34 | 35 | pub fn rename(cfg: &mut config::Local, old: String, new: String) -> Result { 36 | if cfg.chain(&old).is_none() { 37 | return Err(anyhow!("no such remote '{old}'.")); 38 | } 39 | if cfg.chain(&new).is_some() { 40 | return Err(anyhow!("remote {new} already exists.")); 41 | } 42 | 43 | cfg.rename_chain(old.clone(), new.clone())?; 44 | 45 | if let Some(upstream) = cfg.upstream() { 46 | if upstream == old { 47 | cfg.set_upstream(&new)?; 48 | } 49 | } 50 | 51 | Ok(new) 52 | } 53 | 54 | pub fn set_url(cfg: &mut config::Local, name: String, url: &str) -> Result { 55 | if cfg.chain(&name).is_none() { 56 | return Err(anyhow!("no such remote '{name}'.")); 57 | } 58 | cfg.set_url_chain(name.clone(), url)?; 59 | 60 | Ok(name) 61 | } 62 | 63 | impl print::Print for ConfigChain { 64 | fn short(&self) -> Result { 65 | Ok(hex::encode(self.info().public_key())) 66 | } 67 | 68 | fn long(&self) -> Result { 69 | let info = self.info(); 70 | Ok(format!( 71 | r"{: <10}: {} 72 | {: <10}: {} 73 | {: <10}: {}s 74 | {: <10}: {} 75 | {: <10}: {} 76 | {: <10}: {} 77 | {: <10}: {} 78 | {: <10}: {}", 79 | "URL".bold(), 80 | self.url(), 81 | "Public Key".bold(), 82 | hex::encode(info.public_key()), 83 | "Period".bold(), 84 | info.period(), 85 | "Genesis".bold(), 86 | OffsetDateTime::from_unix_timestamp(info.genesis_time() as i64).unwrap(), 87 | "Chain Hash".bold(), 88 | hex::encode(info.hash()), 89 | "Group Hash".bold(), 90 | hex::encode(info.group_hash()), 91 | "Scheme ID".bold(), 92 | info.scheme_id(), 93 | "Beacon ID".bold(), 94 | info.metadata().beacon_id() 95 | )) 96 | } 97 | 98 | fn json(&self) -> Result { 99 | serde_json::to_string(&self.info()).map_err(|e| anyhow!(e)) 100 | } 101 | } 102 | 103 | pub fn show(cfg: &config::Local, format: print::Format, name: String) -> Result { 104 | let chain = match cfg.chain(&name) { 105 | Some(chain) => chain, 106 | None => return Err(anyhow!("no such remote '{name}'.")), 107 | }; 108 | 109 | print_with_format(chain, format) 110 | } 111 | 112 | pub fn list(cfg: &config::Local) -> Result { 113 | let chains: Vec = cfg.chains().keys().cloned().collect(); 114 | if chains.is_empty() { 115 | Ok("".into()) 116 | } else { 117 | let output: Vec = chains 118 | .iter() 119 | .map(|key| (key.to_owned(), cfg.chain(key.as_str()).unwrap())) 120 | .map(|(name, chain)| { 121 | if log_enabled!(Level::Warn) { 122 | format!("{name: <20}\t{url}", url = chain.url()) 123 | } else { 124 | name 125 | } 126 | }) 127 | .collect(); 128 | Ok(output.join("\n")) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /dee/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use anyhow::{anyhow, Result}; 4 | 5 | use drand_core::chain::ChainInfo; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | pub type Chains = HashMap; 9 | 10 | #[derive(Serialize, Deserialize, Debug, Default)] 11 | pub struct Local { 12 | upstream: Option, 13 | chains: Chains, 14 | } 15 | 16 | impl Local { 17 | const APP_NAME: &'static str = env!("CARGO_PKG_NAME"); 18 | const CONFIG_NAME: Option<&'static str> = Some("default"); 19 | 20 | pub fn load() -> Self { 21 | confy::load(Self::APP_NAME, Self::CONFIG_NAME).unwrap() 22 | } 23 | 24 | pub fn path() -> Result { 25 | confy::get_configuration_file_path(Self::APP_NAME, Self::CONFIG_NAME) 26 | .map(|path| path.to_str().unwrap().to_string()) 27 | .map_err(|err| anyhow!(err)) 28 | } 29 | 30 | pub fn store(&self) -> Result<()> { 31 | confy::store(Self::APP_NAME, Self::CONFIG_NAME, self).map_err(|err| anyhow!(err)) 32 | } 33 | 34 | pub fn upstream(&self) -> Option { 35 | self.upstream.clone() 36 | } 37 | 38 | pub fn upstream_chain(&self) -> Option { 39 | self.upstream().and_then(|u| self.chains.get(&u)).cloned() 40 | } 41 | 42 | pub fn chain(&self, name: &str) -> Option { 43 | self.chains.get(name).cloned() 44 | } 45 | 46 | pub fn chains(&self) -> Chains { 47 | self.chains.clone() 48 | } 49 | 50 | pub fn chain_by_hash(&self, hash: &[u8]) -> Option<(String, ConfigChain)> { 51 | self.chains 52 | .iter() 53 | .find(|(_, chain)| chain.info().hash() == hash) 54 | .map(|(name, chain)| (name.clone(), chain.clone())) 55 | } 56 | 57 | pub fn add_chain(&mut self, name: String, config_chain: ConfigChain) -> Result<()> { 58 | self.chains.insert(name.clone(), config_chain); 59 | if self.chains.len() == 1 { 60 | self.set_upstream(&name) 61 | } else { 62 | Ok(()) 63 | } 64 | } 65 | 66 | pub fn remove_chain(&mut self, name: String) -> Result<()> { 67 | self.chains.remove(&name); 68 | Ok(()) 69 | } 70 | 71 | pub fn rename_chain(&mut self, old: String, new: String) -> Result<()> { 72 | self.chains 73 | .remove(&old) 74 | .map(|v| { 75 | self.chains.insert(new, v); 76 | }) 77 | .ok_or(anyhow!("no such remote '{old}'.")) 78 | } 79 | 80 | pub fn set_url_chain(&mut self, name: String, url: &str) -> Result<()> { 81 | self.chains 82 | .get_mut(&name) 83 | .map(|v| { 84 | v.url = url.to_string(); 85 | }) 86 | .ok_or(anyhow!("no such remote '{name}'.")) 87 | } 88 | 89 | pub fn set_upstream(&mut self, upstream: &str) -> Result<()> { 90 | self.chains 91 | .get(upstream) 92 | .map(|_| { 93 | self.upstream = Some(upstream.to_owned()); 94 | }) 95 | .ok_or(anyhow!("no such remote '{upstream}'.")) 96 | } 97 | 98 | pub fn set_upstream_and_chain(&mut self, set_upstream: Option) -> Result { 99 | let chain = set_upstream 100 | .map(|upstream| { 101 | self.set_upstream(&upstream).unwrap(); 102 | upstream 103 | }) 104 | .or(self.upstream()); 105 | 106 | match chain { 107 | Some(chain) => Ok(self.chain(&chain).unwrap()), 108 | None => Err(anyhow!("No upstream")), 109 | } 110 | } 111 | } 112 | 113 | impl From for Option<&str> { 114 | fn from(_val: Local) -> Self { 115 | Some("default") 116 | } 117 | } 118 | 119 | #[derive(Serialize, Deserialize, Clone, Debug)] 120 | pub struct ConfigChain { 121 | url: String, 122 | info: ChainInfo, 123 | } 124 | 125 | impl ConfigChain { 126 | pub fn new(url: &str, info: ChainInfo) -> Self { 127 | Self { 128 | url: url.to_string(), 129 | info, 130 | } 131 | } 132 | 133 | pub fn url(&self) -> String { 134 | self.url.clone() 135 | } 136 | 137 | pub fn info(&self) -> ChainInfo { 138 | self.info.clone() 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /dee/src/cmd/rand.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | 3 | use anyhow::{anyhow, Result}; 4 | 5 | use colored::Colorize; 6 | use drand_core::{ 7 | beacon::{BeaconError, RandomnessBeacon, RandomnessBeaconTime}, 8 | ChainOptions, DrandError, HttpClient, 9 | }; 10 | use serde::Serialize; 11 | 12 | use crate::{ 13 | config::{self, ConfigChain}, 14 | print::{print_with_format, Format, Print}, 15 | }; 16 | 17 | #[derive(Serialize)] 18 | pub(crate) struct RandResult { 19 | beacon: Option, 20 | time: RandomnessBeaconTime, 21 | } 22 | 23 | impl RandResult { 24 | pub(crate) fn new(beacon: Option, time: RandomnessBeaconTime) -> Self { 25 | Self { beacon, time } 26 | } 27 | } 28 | 29 | impl Print for RandResult { 30 | fn short(&self) -> Result { 31 | match self.beacon.as_ref() { 32 | Some(beacon) => Ok(hex::encode(beacon.randomness())), 33 | None => { 34 | let format = time::format_description::parse( 35 | "[year]-[month]-[day]T[hour]:[minute]:[second]Z", 36 | )?; 37 | let relative = self.time.relative(); 38 | let seconds = relative.whole_seconds().abs() % 60; 39 | let minutes = relative.whole_minutes().abs() % 60; 40 | let hours = relative.whole_hours().abs(); 41 | let relative = format!("{hours:0>2}:{minutes:0>2}:{seconds:0>2}"); 42 | Err(anyhow!( 43 | "Too early. Beacon round is {}, estimated in {} ({}).", 44 | self.time.round(), 45 | relative, 46 | self.time.absolute().format(&format)?, 47 | )) 48 | } 49 | } 50 | } 51 | fn long(&self) -> Result { 52 | let format = 53 | time::format_description::parse("[year]-[month]-[day]T[hour]:[minute]:[second]Z")?; 54 | let relative = self.time.relative(); 55 | let seconds = relative.whole_seconds().abs() % 60; 56 | let minutes = (relative.whole_minutes()).abs() % 60; 57 | let hours = relative.whole_hours().abs(); 58 | let epoch = match relative.whole_seconds().cmp(&0) { 59 | Ordering::Less => "ago", 60 | Ordering::Equal => "now", 61 | Ordering::Greater => "from now", 62 | }; 63 | let relative = format!("{hours:0>2}:{minutes:0>2}:{seconds:0>2} {epoch}"); 64 | let mut output = format!( 65 | r"{: <10}: {} 66 | {: <10}: {} 67 | {: <10}: {}", 68 | "Round".bold(), 69 | self.time.round(), 70 | "Relative".bold(), 71 | relative, 72 | "Absolute".bold(), 73 | self.time.absolute().format(&format)?, 74 | ); 75 | if let Some(beacon) = self.beacon.as_ref() { 76 | output = format!( 77 | r"{output} 78 | {: <10}: {} 79 | {: <10}: {}", 80 | "Randomness".bold(), 81 | hex::encode(beacon.randomness()), 82 | "Signature".bold(), 83 | hex::encode(beacon.signature()), 84 | ); 85 | } 86 | Ok(output) 87 | } 88 | 89 | fn json(&self) -> Result { 90 | Ok(serde_json::to_string(&self.beacon)?) 91 | } 92 | } 93 | 94 | pub fn rand( 95 | _cfg: &config::Local, 96 | format: Format, 97 | chain: ConfigChain, 98 | beacon: Option, 99 | verify: bool, 100 | ) -> Result { 101 | let base_url = chain.url(); 102 | let info = chain.info(); 103 | let latest = beacon.is_none(); 104 | 105 | let beacon = beacon.unwrap_or("0s".to_owned()); 106 | let time = match RandomnessBeaconTime::parse(&info.clone().into(), &beacon) { 107 | Ok(time) => time, 108 | Err(_) => return Err(anyhow!("Invalid beacon round \"{beacon}\"")), 109 | }; 110 | 111 | let client = HttpClient::new( 112 | &base_url, 113 | Some(ChainOptions::new(verify, true, Some(info.into()))), 114 | )?; 115 | 116 | let beacon = if latest { 117 | client.latest() 118 | } else { 119 | client.get(time.round()) 120 | }; 121 | 122 | match beacon { 123 | Ok(beacon) => print_with_format(RandResult::new(Some(beacon), time), format), 124 | Err(DrandError::Beacon(e)) => match *e { 125 | BeaconError::NotFound => print_with_format(RandResult::new(None, time), format), 126 | _ => Ok(e.to_string()), 127 | }, 128 | Err(e) => Err(e.into()), 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /drand_core/src/bls_signatures.rs: -------------------------------------------------------------------------------- 1 | /// Verify BLS Signatures used in drand 2 | /// inspired from https://github.com/noislabs/drand-verify/blob/1017235f6bcfcc9fb433926c0dc1b9a013bd4df3/src/verify.rs#L58 3 | use std::ops::Neg; 4 | 5 | use ark_bls12_381::{g1, g2, G1Affine, G2Affine}; 6 | use ark_ec::{ 7 | bls12::Bls12, 8 | hashing::{curve_maps::wb::WBMap, map_to_curve_hasher::MapToCurveBasedHasher, HashToCurve}, 9 | models::short_weierstrass, 10 | pairing::Pairing, 11 | AffineRepr, 12 | }; 13 | use ark_ff::{field_hashers::DefaultFieldHasher, Zero}; 14 | use ark_serialize::CanonicalDeserialize; 15 | use thiserror::Error; 16 | 17 | use crate::{DrandError, Result}; 18 | 19 | pub const G1_DOMAIN: &[u8] = b"BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_"; 20 | pub const G2_DOMAIN: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_"; 21 | 22 | #[derive(Error, Debug)] 23 | pub enum VerificationError { 24 | #[error("deserialization failed")] 25 | Deserialization, 26 | #[error("cannot initialise mapper for sha2 to BLS12-381 {curve}")] 27 | Initialisation { curve: String }, 28 | #[error("invalid point")] 29 | InvalidPoint, 30 | } 31 | 32 | /// Check that signature is the actual aggregate of message and public key. 33 | /// Calculated by `e(g2, signature) == e(pk, hash)`. 34 | /// `signature` and `hash` are on G2, `public_key` is on G1. 35 | pub fn verify(dst: &[u8], signature: &[u8], hash: &[u8], public_key: &[u8]) -> Result { 36 | // 48 is bytes of G1 37 | // G1Affine::identity().to_compressed().len() 38 | if signature.len() == 48 { 39 | verify_g1_on_g2(dst, signature, hash, public_key) 40 | } else { 41 | verify_g2_on_g1(dst, signature, hash, public_key) 42 | } 43 | } 44 | 45 | /// Check that signature is the actual aggregate of message and public key. 46 | /// Calculated by `e(g2, signature) == e(pk, hash)`. 47 | /// `signature` and `hash` are on G2, `public_key` is on G1. 48 | pub fn verify_g2_on_g1( 49 | dst: &[u8], 50 | signature: &[u8], 51 | hash: &[u8], 52 | public_key: &[u8], 53 | ) -> Result { 54 | let mapper = MapToCurveBasedHasher::< 55 | short_weierstrass::Projective, 56 | DefaultFieldHasher, 57 | WBMap, 58 | >::new(dst) 59 | .map_err(|_| -> DrandError { 60 | Box::new(VerificationError::Initialisation { 61 | curve: "G2".to_owned(), 62 | }) 63 | .into() 64 | })?; 65 | let hash_on_curve = mapper.hash(hash).map_err(|_| -> DrandError { 66 | Box::new(VerificationError::Initialisation { 67 | curve: "G2".to_owned(), 68 | }) 69 | .into() 70 | })?; 71 | 72 | let g1 = G1Affine::generator(); 73 | let sigma = g2_from_variable(signature)?; 74 | let r = g1_from_variable(public_key)?; 75 | Ok(fast_pairing_equality(&g1, &sigma, &r, &hash_on_curve)) 76 | } 77 | 78 | /// Check that signature is the actual aggregate of message and public key. 79 | /// Calculated by `e(g1, signature) == e(pk, hash)`. 80 | /// `signature` is on G1, `public_key` and `hash` are on G2. 81 | pub fn verify_g1_on_g2( 82 | dst: &[u8], 83 | signature: &[u8], 84 | hash: &[u8], 85 | public_key: &[u8], 86 | ) -> Result { 87 | let mapper = MapToCurveBasedHasher::< 88 | short_weierstrass::Projective, 89 | DefaultFieldHasher, 90 | WBMap, 91 | >::new(dst) 92 | .map_err(|_| -> DrandError { 93 | Box::new(VerificationError::Initialisation { 94 | curve: "G1".to_owned(), 95 | }) 96 | .into() 97 | })?; 98 | let hash_on_curve = mapper.hash(hash).map_err(|_| -> DrandError { 99 | Box::new(VerificationError::Initialisation { 100 | curve: "G1".to_owned(), 101 | }) 102 | .into() 103 | })?; 104 | 105 | let g2 = G2Affine::generator(); 106 | let sigma = g1_from_variable(signature)?; 107 | let s = g2_from_variable(public_key)?; 108 | Ok(fast_pairing_equality(&sigma, &g2, &hash_on_curve, &s)) 109 | } 110 | 111 | /// Checks if e(p, q) == e(r, s) 112 | /// 113 | /// See https://hackmd.io/@benjaminion/bls12-381#Final-exponentiation. 114 | /// 115 | /// Optimized by this trick: 116 | /// Instead of doing e(a,b) (in G2) multiplied by e(-c,d) (in G2) 117 | /// (which is costly is to multiply in G2 because these are very big numbers) 118 | /// we can do FinalExponentiation(MillerLoop( [a,b], [-c,d] )) which is the same 119 | /// in an optimized way. 120 | fn fast_pairing_equality(p: &G1Affine, q: &G2Affine, r: &G1Affine, s: &G2Affine) -> bool { 121 | let minus_p = p.neg(); 122 | // "some number of (G1, G2) pairs" are the inputs of the miller loop 123 | let looped = Bls12::::multi_miller_loop([minus_p, *r], [*q, *s]); 124 | let value = Bls12::final_exponentiation(looped); 125 | value.unwrap().is_zero() 126 | } 127 | 128 | fn g1_from_variable(data: &[u8]) -> Result { 129 | if data.len() != 48 { 130 | return Err(Box::new(VerificationError::InvalidPoint).into()); 131 | } 132 | 133 | G1Affine::deserialize_compressed(data) 134 | .map_err(|_| Box::new(VerificationError::Deserialization).into()) 135 | } 136 | 137 | fn g2_from_variable(data: &[u8]) -> Result { 138 | if data.len() != 96 { 139 | return Err(Box::new(VerificationError::InvalidPoint).into()); 140 | } 141 | 142 | G2Affine::deserialize_compressed(data) 143 | .map_err(|_| Box::new(VerificationError::Deserialization).into()) 144 | } 145 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Publish release binaries 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | workflow_dispatch: 8 | inputs: 9 | test: 10 | description: 'Testing the release workflow' 11 | required: true 12 | default: 'true' 13 | 14 | jobs: 15 | build: 16 | name: Publish for ${{ matrix.name }} 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | matrix: 20 | name: [linux, armv7, arm64, windows, macos] 21 | include: 22 | - name: linux 23 | os: ubuntu-22.04 24 | build_deps: > 25 | archive_name: dee.tar.gz 26 | asset_suffix: x86_64-linux.tar.gz 27 | 28 | - name: armv7 29 | os: ubuntu-22.04 30 | target: armv7-unknown-linux-gnueabihf 31 | build_deps: > 32 | gcc-arm-linux-gnueabihf 33 | cargo_config: | 34 | [target.armv7-unknown-linux-gnueabihf] 35 | linker = "arm-linux-gnueabihf-gcc" 36 | build_flags: --target armv7-unknown-linux-gnueabihf 37 | archive_name: dee.tar.gz 38 | asset_suffix: armv7-linux.tar.gz 39 | 40 | - name: arm64 41 | os: ubuntu-22.04 42 | target: aarch64-unknown-linux-gnu 43 | build_deps: > 44 | gcc-aarch64-linux-gnu 45 | cargo_config: | 46 | [target.aarch64-unknown-linux-gnu] 47 | linker = "aarch64-linux-gnu-gcc" 48 | build_flags: --target aarch64-unknown-linux-gnu 49 | archive_name: dee.tar.gz 50 | asset_suffix: arm64-linux.tar.gz 51 | 52 | - name: windows 53 | os: windows-latest 54 | archive_name: dee.zip 55 | asset_suffix: x86_64-windows.zip 56 | 57 | - name: macos 58 | os: macos-latest 59 | archive_name: dee.tar.gz 60 | asset_suffix: x86_64-darwin.tar.gz 61 | 62 | steps: 63 | - uses: actions/checkout@v3 64 | - uses: actions-rs/toolchain@v1 65 | with: 66 | toolchain: stable 67 | override: true 68 | - name: Add target 69 | run: rustup target add ${{ matrix.target }} 70 | if: matrix.target != '' 71 | 72 | - name: Install linux build dependencies 73 | run: sudo apt update && sudo apt install ${{ matrix.build_deps }} 74 | if: matrix.build_deps != '' 75 | 76 | - name: Set up .cargo/config 77 | run: | 78 | mkdir .cargo 79 | echo '${{ matrix.cargo_config }}' >.cargo/config 80 | if: matrix.cargo_config != '' 81 | 82 | - name: cargo build 83 | run: cargo build --release --locked ${{ matrix.build_flags }} 84 | working-directory: ./dee 85 | 86 | - name: Create archive 87 | run: | 88 | mkdir -p release/dee 89 | mv target/${{ matrix.target }}/release/dee* release/dee/ 90 | rm release/dee/*.d 91 | tar czf ${{ matrix.archive_name }} -C release/ dee/ 92 | if: matrix.name != 'windows' 93 | 94 | - name: Create archive [Windows] 95 | run: | 96 | mkdir -p release/dee 97 | mv target/release/dee.exe release/dee/ 98 | cd release/ 99 | 7z.exe a ../${{ matrix.archive_name }} dee/ 100 | shell: bash 101 | if: matrix.name == 'windows' 102 | 103 | - name: Upload archive to release 104 | uses: svenstaro/upload-release-action@2.5.0 105 | with: 106 | repo_token: ${{ secrets.GITHUB_TOKEN }} 107 | file: ${{ matrix.archive_name }} 108 | asset_name: dee-$tag-${{ matrix.asset_suffix }} 109 | tag: ${{ github.ref }} 110 | prerelease: true 111 | if: github.event.inputs.test != 'true' 112 | 113 | deb: 114 | name: Debian ${{ matrix.name }} 115 | runs-on: ubuntu-22.04 116 | strategy: 117 | matrix: 118 | name: [linux, armv7, arm64] 119 | include: 120 | - name: linux 121 | target: x86_64-unknown-linux-gnu 122 | 123 | - name: armv7 124 | target: armv7-unknown-linux-gnueabihf 125 | build_deps: > 126 | gcc-arm-linux-gnueabihf 127 | cargo_config: | 128 | [target.armv7-unknown-linux-gnueabihf] 129 | linker = "arm-linux-gnueabihf-gcc" 130 | 131 | - name: arm64 132 | target: aarch64-unknown-linux-gnu 133 | build_deps: > 134 | gcc-aarch64-linux-gnu 135 | cargo_config: | 136 | [target.aarch64-unknown-linux-gnu] 137 | linker = "aarch64-linux-gnu-gcc" 138 | 139 | steps: 140 | - uses: actions/checkout@v3 141 | - uses: actions-rs/toolchain@v1 142 | with: 143 | toolchain: stable 144 | override: true 145 | - name: Add target 146 | run: rustup target add ${{ matrix.target }} 147 | - name: cargo install cargo-deb 148 | uses: actions-rs/cargo@v1 149 | with: 150 | command: install 151 | args: cargo-deb 152 | 153 | - name: Install build dependencies 154 | run: sudo apt update && sudo apt install ${{ matrix.build_deps }} 155 | if: matrix.build_deps != '' 156 | 157 | - name: Set up .cargo/config 158 | run: | 159 | mkdir .cargo 160 | echo '${{ matrix.cargo_config }}' >.cargo/config 161 | if: matrix.cargo_config != '' 162 | 163 | - name: cargo build 164 | run: cargo build --release --locked --target ${{ matrix.target }} ${{ matrix.build_flags }} 165 | working-directory: ./dee 166 | 167 | - name: cargo deb 168 | uses: actions-rs/cargo@v1 169 | with: 170 | command: deb 171 | args: --package dee --no-build --target ${{ matrix.target }} ${{ matrix.deb_flags }} 172 | 173 | - name: Upload Debian package to release 174 | uses: svenstaro/upload-release-action@2.5.0 175 | with: 176 | repo_token: ${{ secrets.GITHUB_TOKEN }} 177 | file: target/${{ matrix.target }}/debian/*.deb 178 | tag: ${{ github.ref }} 179 | file_glob: true 180 | prerelease: true 181 | if: github.event.inputs.test != 'true' 182 | -------------------------------------------------------------------------------- /assets/building-dee.md: -------------------------------------------------------------------------------- 1 | --- 2 | marp: true 3 | paginate: true 4 | _paginate: false 5 | --- 6 | 7 | # Building `dee`, a simple timelock client 8 | 9 | Thibault Meunier 10 | 11 | --- 12 | 13 | ## In the next 15min 14 | 15 | 1. Demo 16 | 2. CLI design 17 | 3. Timelock API 18 | 4. Final words 19 | 20 | --- 21 | 22 | # Demo 23 | 24 | _Try it at home_ 25 | 26 | --- 27 | 28 | ## Live demo 29 | 30 | Installation 31 | 32 | ```bash 33 | cargo install dee 34 | ``` 35 | 36 | Add a remote chain 37 | 38 | ```bash 39 | dee remote add quicknet https://drand.cloudflare.com/dbd506d... 40 | quicknet 41 | ``` 42 | 43 | --- 44 | 45 | ## Live demo - 2 46 | 47 | Retrieve public randomness 48 | 49 | ```bash 50 | dee rand -u quicknet 51 | 3129db460507ff559f7fa5e71d6f8bc66aec27516de3d78f7461f6299a2bd483 52 | ``` 53 | 54 | Encrypt 30 seconds to the future 55 | 56 | ``` 57 | echo "Hello dee!" | dee crypt -r 30s > locked.dee 58 | ``` 59 | 60 | Decrypt, the future is now 61 | 62 | ``` 63 | dee crypt --decrypt locked.dee 64 | Hello dee! 65 | ``` 66 | 67 | --- 68 | 69 | # Designing a CLI 70 | 71 | _CLI experience is real_ 72 | 73 | --- 74 | 75 | 76 | ## Limit default 77 | 78 | No default network 79 | 80 | ```bash 81 | dee remote add mainnet https://api.drand.sh 82 | ``` 83 | 84 | Choose your own 85 | ```bash 86 | dee rand --set-upstream mainnet 87 | ``` 88 | 89 | --- 90 | 91 | ## Communication for everyone 92 | 93 | Configurable output level 94 | 95 | ```bash 96 | dee rand -l 97 | Round : 2820083 98 | Relative : 00:00:24 ago 99 | Absolute : 2023-03-28 19:58:30 100 | Randomness: 66aba01bb54f200ef6363143615e1e193eaacbb89dcc7b38... 101 | Signature : 82fb1e24bd603216241d75d51c3378b193d62e4fb8fdbeab... 102 | ``` 103 | 104 | Informative error 105 | 106 | ```bash 107 | echo "Hello world!" | dee crypt -r 30s 108 | error: remote must use unchained signatures 109 | ``` 110 | 111 | --- 112 | 113 | ## Mimic existing CLIs 114 | 115 | git inspired 116 | 117 | ```bash 118 | dee remote show mainnet 119 | ``` 120 | 121 | age inspired 122 | 123 | ```bash 124 | dee crypt --decrypt --armor < cat.png 125 | ``` 126 | 127 | drand inspired 128 | 129 | ```bash 130 | dee rand -u mainnet --json 1000 131 | ``` 132 | 133 | --- 134 | 135 | ## Rust specific devtooling 136 | 137 | [clap](https://docs.rs/clap/latest/clap/) all in one argument parser, documentation, and manpages generation 138 | ```rust 139 | /// Set default upstream. If empty, use the latest upstream. 140 | #[arg(short = 'u', long, value_hint = ValueHint::Url)] 141 | set_upstream: Option, 142 | ``` 143 | 144 | Cross-platform support is simpler without openssl 145 | 146 | ```rust 147 | cargo build --target wasm32-wasi 148 | ``` 149 | 150 | Considered two BLS12-381 libraries: [zkcrypto/bls12_381](https://github.com/zkcrypto/bls12_381) and [arkworks-rs/curves](https://github.com/arkworks-rs/curves). 151 | 152 | ```bash 153 | cargo bench --all-features 154 | ``` 155 | 156 | --- 157 | 158 | 159 | 160 | # Timelock API 161 | 162 | _Encrypting towards the future doesn't negate API considerations_ 163 | 164 | --- 165 | 166 | 167 | 168 | ## Work offline 169 | 170 | Go 171 | 172 | ```go 173 | func (t Tlock) Encrypt( 174 | dst io.Writer, src io.Reader, roundNumber uint64 175 | ) (err error) { 176 | ``` 177 | 178 | Rust 179 | 180 | ```rust 181 | fn encrypt( 182 | dst: Write, mut src: Read, roundNumber: u64, 183 | hash: &[u8], pk: &[u8], 184 | ) -> Result<()> { 185 | ``` 186 | 187 | --- 188 | 189 | ## Work offline 190 | 191 | Go 192 | 193 | ```go 194 | network := "https://api.drand.sh" 195 | tlock := tlock.New(network) 196 | tlock.Encrypt(dst, src, roundNumber) 197 | ``` 198 | 199 | Rust 200 | ```rust 201 | let chain = Chain::new("https://api.drand.sh"); 202 | let client = HttpClient::new(chain, None); 203 | let info = client.chain().info().await?; 204 | 205 | tlock_age::encrypt( 206 | &mut dst, 207 | src, 208 | &info.hash(), 209 | &info.public_key(), 210 | roundNumber, 211 | )?; 212 | ``` 213 | 214 | --- 215 | 216 | ## Interroperability 217 | 218 | Two existing implementations: [drand/tlock](drand/tlock) (Go), [drand/tlock-js](https://github.com/drand/tlock-js) (JavaScript). 219 | 220 | [rage](https://github.com/str4d/rage) (Rust implementation of age) adds a [grease stanza](https://github.com/str4d/rage/pull/365): `-grease `. 221 | 222 | [Hash to curve RFC](https://datatracker.ietf.org/doc/draft-irtf-cfrg-hash-to-curve/) is a beacon of light: [hash_to_field](https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-16.html#name-hash_to_field-implementatio), [expand_message](https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-16.html#name-expand_message). 223 | 224 | Elliptic curve serialisation is not standardised. 225 | $$ 226 | \begin{align} 227 | \mathbb{F}_{p^{12}} \rightarrow c_0 \| c_1 && 228 | \mathbb{F}_{p^{12}} \rightarrow c_1 \| c_0 229 | \end{align} 230 | $$ 231 | $$ 232 | \begin{align} 233 | c_0 \rightarrow \text{big-endian} && 234 | c_0 \rightarrow \text{little-endian} 235 | \end{align} 236 | $$ 237 | 238 | 239 | --- 240 | 241 | 242 | 243 | # Final words 244 | 245 | _Time to move on_ 246 | 247 | --- 248 | 249 | 250 | 251 | ## What could be different 252 | 253 | Hostname instead of chain hash 254 | 255 | ```bash 256 | https://api.drand.sh/dbd506d6ef76e5f386f41c651dcb808c5bcbd75471cc... 257 | -> https://quicknet.api.drand.sh 258 | ``` 259 | 260 | Stanza format 261 | 262 | ``` 263 | tlock {round} {chain_hash} 264 | -> tlock REDACTED REDACTED 265 | ``` 266 | 267 | Stateless CLI 268 | 269 | ``` 270 | dee remote 271 | -> dee rand -u https://api.drand.sh/ 272 | -> DEE_REMOTE=https://api.drand.sh/ 273 | ``` 274 | 275 | --- 276 | 277 | ## Takeaways 278 | 279 | 1. A new [drand](https://github.com/thibmeu/drand-rs) and [tlock](https://github.com/thibmeu/tlock-rs) implementation. 280 | 2. One [academic paper](https://eprint.iacr.org/2023/189), multiple engineering tradeoffs. 281 | 3. tlock is not be constrained to existing drand API. 282 | 4. [Discussions](https://join.slack.com/t/drandworkspace/shared_invite/zt-19u4rf6if-bf7lxIvF2zYn4~TrBwfkiA) improve software. Thanks to everyone that answered questions. 283 | 284 | --- 285 | 286 | 287 | 288 | # Thank you 289 | 290 | For more information, go to: 291 | [github.com/thibmeu/drand-rs](https://github.com/thibmeu/drand-rs) 292 | [github.com/thibmeu/tlock-rs](https://github.com/thibmeu/tlock-rs) -------------------------------------------------------------------------------- /dee/src/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::{Args, Parser, Subcommand, ValueHint}; 2 | 3 | /// 1. First interaction 4 | /// drand get --url https://drand.cloudflare.com # latest beacon 5 | /// drand get --url https://drand.cloudflare.com 100 # round 100 6 | /// drand get --url https://drand.cloudflare.com 2022...Z # round time from UTC 7 | /// drand 8 | /// 2. Second allow disabling verification 9 | /// drand get --verify=false --chain-url https://drand.cloudflare.com # disable beacon verification 10 | /// 3. Chain management 11 | /// drand chain add cloudflare https://drand.cloudflare.com # add chain to local configuration 12 | /// drand chain set-url cloudflare https://drand.cloudflare.com 13 | /// drand chain # list all chains 14 | /// drand chain info cloudflare 15 | /// drand chain info --cache=false cloudflare # chain is cached locally for validation 16 | 17 | #[derive(Parser)] 18 | #[command(author, version, about, long_about = None)] 19 | #[command(propagate_version = true)] 20 | pub struct Cli { 21 | #[clap(flatten)] 22 | pub verbose: clap_verbosity_flag::Verbosity, 23 | #[command(subcommand)] 24 | pub command: Commands, 25 | } 26 | 27 | #[derive(Subcommand)] 28 | pub enum Commands { 29 | /// Interact with timelock encryption 30 | /// 31 | /// INPUT defaults to standard input, and OUTPUT defaults to standard output. 32 | /// 33 | /// ROUND can be: 34 | /// * a specific round (123), 35 | /// * a duration (30s), 36 | /// * an RFC3339 date (2023-06-28 21:30:22) 37 | /// 38 | /// UPSTREAM is an existing remote, and defaults to the lastest used. 39 | /// 40 | /// Example: 41 | /// $ tar cvz ~/data | dee crypt -u myremote -r 30s > data.tar.gz.age 42 | /// $ dee crypt --decrypt -o data.tar.gz data.tar.gz.age 43 | #[command(verbatim_doc_comment)] 44 | Crypt { 45 | /// Encrypt the input (the default). 46 | #[arg(short, long, default_value_t = true, group = "action")] 47 | // todo(thibault): add group for armor 48 | encrypt: bool, 49 | /// Decrypt the input. 50 | #[arg(short, long, group = "action")] 51 | decrypt: bool, 52 | /// Inspect the input header. 53 | #[command(flatten)] 54 | inspect: InspectArg, 55 | /// Set default upstream. If empty, use the latest upstream. 56 | #[arg(short = 'u', long)] 57 | set_upstream: Option, 58 | /// Encrypt to the specified ROUND. 59 | /// ROUND can be: 60 | /// * a specific round. e.g. 123, 61 | /// * a duration. e.g. 30s, 62 | /// * an RFC3339 date. e.g. 2023-06-28 21:30:22 63 | #[arg(short, long, verbatim_doc_comment)] 64 | round: Option, 65 | /// Encrypt to a PEM encoded format. 66 | #[arg(short, long)] 67 | armor: bool, 68 | /// Write the result to the file at path OUTPUT. 69 | #[arg(short, long)] 70 | output: Option, 71 | /// Path to a file to read from. 72 | input: Option, 73 | }, 74 | /// Retrieve public randomness. 75 | /// 76 | /// BEACON defaults to the latest beacon, and FORMAT to long. 77 | /// 78 | /// UPSTREAM is an existing remote, and defaults to the lastest used. 79 | /// 80 | /// Example: 81 | /// $ dee rand -u myremote 1000 82 | /// $ dee rand -l 83 | #[command(verbatim_doc_comment)] 84 | Rand { 85 | /// Set default upstream. If empty, use the lastest upstream. 86 | #[arg(short = 'u', long)] 87 | set_upstream: Option, 88 | /// Enable beacon response validation. 89 | #[arg(long, default_value_t = true)] 90 | verify: bool, 91 | /// Enable detailed output 92 | #[arg(short, long, default_value_t = false, group = "format")] 93 | long: bool, 94 | /// Enable json output, as defined per drand API 95 | #[arg(long, default_value_t = false, group = "format")] 96 | json: bool, 97 | /// Round number to retrieve. 98 | /// ROUND can be: 99 | /// * a specific round. e.g. 123, 100 | /// * a duration. e.g. 30s, 101 | /// * an RFC3339 date. e.g. 2023-06-28 21:30:22, 102 | /// * empty to retrieve the latest round 103 | beacon: Option, 104 | }, 105 | /// Manage set of tracked chains. 106 | /// 107 | /// With no arguments, shows a list of existing remotes. Several subcommands are available to perform operations on the remotes. 108 | /// 109 | /// With the -v option, remote URLs are shown as well. 110 | Remote { 111 | #[command(subcommand)] 112 | command: Option, 113 | }, 114 | } 115 | 116 | #[derive(Args)] 117 | pub struct InspectArg { 118 | /// Inspect the input header. 119 | #[arg(long, default_value_t = false, group = "action")] 120 | inspect: bool, 121 | #[command(flatten)] 122 | print: PrintArg, 123 | } 124 | 125 | #[allow(dead_code)] 126 | impl InspectArg { 127 | pub fn is_true(&self) -> bool { 128 | self.inspect 129 | } 130 | 131 | pub fn long(&self) -> bool { 132 | self.print.long 133 | } 134 | 135 | pub fn json(&self) -> bool { 136 | self.print.json 137 | } 138 | } 139 | 140 | #[derive(Args)] 141 | #[group(multiple = false)] 142 | pub struct PrintArg { 143 | /// Enable detailed output 144 | #[arg(short, long, default_value_t = false, group = "format")] 145 | long: bool, 146 | /// Enable json output, as defined per drand API 147 | #[arg(long, default_value_t = false, group = "format")] 148 | json: bool, 149 | } 150 | 151 | #[derive(Subcommand)] 152 | pub enum RemoteCommand { 153 | /// Add a remote named for the chain at . The command dee rand -u can then be used to create and update remote-tracking chain . 154 | /// 155 | /// By default, only information on managed chains are imported. 156 | Add { 157 | name: String, 158 | #[arg(value_hint = ValueHint::Url)] 159 | url: String, 160 | }, 161 | /// Rename the remote named to . The remote-tracking chain and configuration settings for the remote are updated. 162 | Rename { old: String, new: String }, 163 | /// Remove the remote named . The remote-tracking chain and configuration settings for the remote are removed. 164 | Remove { name: String }, 165 | /// Change URLs for the remote. 166 | SetUrl { 167 | name: String, 168 | #[arg(value_hint = ValueHint::Url)] 169 | url: String, 170 | }, 171 | /// Give some information about the remote . 172 | Show { 173 | /// Enable detailed output 174 | #[arg(short, long, default_value_t = false, group = "format")] 175 | long: bool, 176 | /// Enable json output, as defined per drand API 177 | #[arg(long, default_value_t = false, group = "format")] 178 | json: bool, 179 | name: Option, 180 | }, 181 | } 182 | 183 | #[allow(dead_code)] 184 | pub fn build() -> Cli { 185 | Cli::parse() 186 | } 187 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dee: Rust cli for drand 2 | 3 | [![Documentation](https://img.shields.io/badge/docs-main-blue.svg)][Documentation] 4 | ![License](https://img.shields.io/crates/l/dee.svg) 5 | [![crates.io](https://img.shields.io/crates/v/dee.svg)][Crates.io] 6 | 7 | [Crates.io]: https://crates.io/crates/dee 8 | [Documentation]: https://docs.rs/dee/ 9 | 10 | Retrieve public randomness, and encrypt your files to the future. **dee** provides a drand client, and support for timelock encryption. 11 | 12 |

13 | 14 | ## Tables of Content 15 | 16 | * [Features](#features) 17 | * [What's next](#whats-next) 18 | * [Installation](#installation) 19 | * [Usage](#usage) 20 | * [Manage remote beacons](#manage-remote-beacons) 21 | * [Retrieve public randomness](#retrieve-public-randomness) 22 | * [Timelock encryption](#timelock-encryption) 23 | * [Common remotes](#common-remotes) 24 | * [Security Considerations](#security-considerations) 25 | * [FAQ](#faq) 26 | * [License](#license) 27 | 28 | ## Features 29 | 30 | * Retrieve drand randomness 31 | * Manages multiple beacons locally 32 | * Timelock encryption and decryption 33 | * Chain and unchained randomness 34 | * Signatures verification on G1 and G2 35 | * Customizable output format 36 | * Cross platform (Linux, Windows, macOS) 37 | * Interroperability with Go and JS implementation 38 | * wasm32 compatible library 39 | 40 | ## What's next 41 | 42 | * P2P randomness retrieval 43 | * Offline timelock decryption 44 | 45 | ## Installation 46 | 47 | | Environment | CLI Command | 48 | |:-------------------|:--------------------------------------------------------------| 49 | | Cargo (Rust 1.74+) | `cargo install dee --git https://github.com/thibmeu/drand-rs` | 50 | 51 | On Linux, Windows, or macOS, you can use the [pre-built binaries](https://github.com/thibmeu/drand-rs/releases). 52 | 53 | ## Usage 54 | 55 | You can use the `--help` option to get more details about the commands and their options. 56 | 57 | ```bash 58 | dee [OPTIONS] 59 | ``` 60 | 61 | ### Manage remote beacons 62 | 63 | Add quicknet remote beacon, and shows details about it. 64 | ```bash 65 | dee remote add quicknet https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971 66 | quicknet 67 | ``` 68 | 69 | ```bash 70 | dee remote show --long quicknet 71 | URL : https://drand.cloudflare.com/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971 72 | Public Key: 83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a 73 | Period : 3s 74 | Genesis : 2023-08-23 15:09:27.0 +00:00:00 75 | Chain Hash: 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971 76 | Group Hash: f477d5c89f21a17c863a7f937c6a6d15859414d2be09cd448d4279af331c5d3e 77 | Scheme ID : bls-unchained-g1-rfc9380 78 | Beacon ID : quicknet 79 | ``` 80 | 81 | ### Retrieve public randomness 82 | 83 | Retrieve round 1000 from quicknet. 84 | 85 | ```bash 86 | dee rand -u quicknet --long 1000 87 | Round : 1000 88 | Relative : 100:09:43 ago 89 | Absolute : 2023-08-23 15:59:24 90 | Randomness: fe290beca10872ef2fb164d2aa4442de4566183ec51c56ff3cd603d930e54fdd 91 | Signature : b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39 92 | ``` 93 | 94 | ### Timelock encryption 95 | 96 | Encrypt `Hello dee!` string to 30 seconds in the future, using quicknet publickey. If you wait 30 seconds before decrypting, the message is decrypted using the new quicknet signature. 97 | 98 | ``` 99 | echo 'Hello dee!' | dee crypt -u quicknet -r 30s > data.dee 100 | dee crypt --decrypt data.dee 101 | Hello dee! 102 | ``` 103 | 104 | ### Common remotes 105 | 106 | | ID | Remote | Timelock encryption | 107 | | :---------------------|:------------------------------------------------------------------------------------------------|:--------------------| 108 | | `quicknet-cloudflare` | `https://drand.cloudflare.com/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971` | Yes | 109 | | `quicknet-pl` | `https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971` | Yes | 110 | | `mainnet-cloudflare` | `https://drand.cloudflare.com` | No | 111 | | `mainnet-pl` | `https://api.drand.sh` | No | 112 | 113 | `dee` does not come with a default remote beacon. You should decide whichever suit your needs. 114 | 115 | More beacons origin are available on [drand website](https://drand.love/developer/). 116 | 117 | ## Security Considerations 118 | 119 | This software has not been audited. Please use at your sole discretion. With this in mind, dee security relies on the following: 120 | * [tlock: Practical Timelock Encryption from Threshold BLS](https://eprint.iacr.org/2023/189) by Nicolas Gailly, Kelsey Melissaris, and Yolan Romailler, and its implementation in [thibmeu/tlock-rs](https://github.com/thibmeu/tlock-rs), 121 | * [Identity-Based Encryption](https://crypto.stanford.edu/~dabo/papers/bfibe.pdf) by Dan Boneh, and Matthew Franklin, and its implementation in [thibmeu/tlock-rs](https://github.com/thibmeu/tlock-rs), 122 | * The [League of Entropy](https://www.cloudflare.com/leagueofentropy/) to remain honest, 123 | * [age](https://github.com/C2SP/C2SP/blob/main/age.md) encryption protocol, and its implementation in [str4d/rage](https://github.com/str4d/rage), 124 | 125 | ## FAQ 126 | 127 | ### Default configuration path 128 | 129 | `dee` configuration file is available at the following 130 | 131 | | OS | Path | 132 | |:--------|:---------------------------------------------------------------| 133 | | Linux | `/home/alice/.config/dee/default.toml` | 134 | | Windows | `C:\Users\Alice\AppData\Roaming\dee\config\default.toml` | 135 | | macOS | `/Users/Alice/Library/Application Support/rs.dee/default.toml` | 136 | 137 | ### Other implementations 138 | 139 | drand API specification is at [drand.love/docs/specification](https://drand.love/docs/specification/). drand is based on [Scalable Bias-Resistant Distributed Randomness](https://eprint.iacr.org/2016/1067) by Ewa Syta, Philipp Jovanovic, Eleftherios Kokoris Kogias, Nicolas Gailly, Linus Gasser, Ismail Khoffi, Michael J. Fischer, and Bryan Ford. 140 | The reference interroperable Go implementation is available at [drand/drand](https://github.com/drand/drand). 141 | 142 | timelock encryption was published in [tlock: Practical Timelock Encryption from Threshold BLS](https://eprint.iacr.org/2023/189) by Nicolas Gailly, Kelsey Melissaris, and Yolan Romailler. 143 | The reference interroperable Go implementation is available at [drand/tlock](https://github.com/drand/tlock). 144 | 145 | ### Rust libraries 146 | 147 | dee focuses on building a cli. It relies on Rust libraries to use drand or perform timelock encryption. 148 | 149 | If you're looking to implement your own Rust application on top of drand and/or timelock encryption, you can use the following: 150 | * [drand_core](https://github.com/thibmeu/drand-rs/tree/main/drand_core): drand client, 151 | * [tlock](https://github.com/thibmeu/tlock-rs): raw tlock implementation, allowing messages up to 16 bytes, 152 | * [tlock_age](https://github.com/thibmeu/tlock-rs): hybrid encryption, age phassphrase is encrypted using tlock, 153 | 154 | ## License 155 | 156 | This project is under the MIT license. 157 | 158 | ### Contribution 159 | 160 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you shall be MIT licensed as above, without any additional terms or conditions. 161 | -------------------------------------------------------------------------------- /dee/src/cmd/crypt.rs: -------------------------------------------------------------------------------- 1 | use std::{cmp::Ordering, fs, io}; 2 | 3 | use anyhow::{anyhow, Result}; 4 | use colored::Colorize; 5 | use drand_core::{ 6 | beacon::{BeaconError, RandomnessBeaconTime}, 7 | chain::ChainInfo, 8 | ChainOptions, DrandError, HttpClient, 9 | }; 10 | use serde::Serialize; 11 | use tlock_age::Header; 12 | 13 | use crate::{ 14 | config::{self, ConfigChain}, 15 | print::{print_with_format, Format, Print}, 16 | }; 17 | 18 | pub fn file_or_stdin(input: Option) -> Result> { 19 | let reader: Box = match input { 20 | Some(path) => Box::new(io::BufReader::new( 21 | fs::File::open(path).map_err(|_e| anyhow!("cannot read input file"))?, 22 | )), 23 | None => Box::new(io::BufReader::new(io::stdin())), 24 | }; 25 | Ok(reader) 26 | } 27 | 28 | pub fn file_or_stdout(output: Option) -> Result> { 29 | let writer: Box = match output { 30 | Some(path) => Box::new(io::BufWriter::new( 31 | fs::File::create(path).map_err(|_e| anyhow!("cannot create output file"))?, 32 | )), 33 | None => Box::new(io::BufWriter::new(io::stdout())), 34 | }; 35 | Ok(writer) 36 | } 37 | 38 | pub fn encrypt( 39 | _cfg: &config::Local, 40 | output: Option, 41 | input: Option, 42 | armor: bool, 43 | chain: ConfigChain, 44 | round: Option, 45 | ) -> Result { 46 | let info = chain.info(); 47 | if !info.is_unchained() { 48 | return Err(anyhow!("remote must use unchained signatures")); 49 | } 50 | 51 | let beacon_time = crate::time::round_from_option(&chain, round)?; 52 | 53 | let src = file_or_stdin(input)?; 54 | let dst = file_or_stdout(output)?; 55 | if armor { 56 | let mut dst = tlock_age::armor::ArmoredWriter::wrap_output(dst)?; 57 | if info.is_rfc9380() { 58 | tlock_age::encrypt( 59 | &mut dst, 60 | src, 61 | &info.hash(), 62 | &info.public_key(), 63 | beacon_time.round(), 64 | )?; 65 | } else { 66 | tlock_age_non_rfc9380::encrypt( 67 | &mut dst, 68 | src, 69 | &info.hash(), 70 | &info.public_key(), 71 | beacon_time.round(), 72 | )?; 73 | } 74 | dst.finish()?; 75 | Ok(()) 76 | } else if info.is_rfc9380() { 77 | tlock_age::encrypt( 78 | dst, 79 | src, 80 | &info.hash(), 81 | &info.public_key(), 82 | beacon_time.round(), 83 | ) 84 | .map_err(|err| anyhow!(err)) 85 | } else { 86 | tlock_age_non_rfc9380::encrypt( 87 | dst, 88 | src, 89 | &info.hash(), 90 | &info.public_key(), 91 | beacon_time.round(), 92 | ) 93 | .map_err(|err| anyhow!(err)) 94 | } 95 | .map(|()| String::from("")) 96 | .map_err(|err| anyhow!(err)) 97 | } 98 | 99 | pub fn inspect( 100 | cfg: &config::Local, 101 | format: Format, 102 | input: Option, 103 | chain: ConfigChain, 104 | ) -> Result { 105 | let src = file_or_stdin(input)?; 106 | let header = tlock_age::decrypt_header(src)?; 107 | 108 | let result = if let Some((name, chain_config)) = cfg.chain_by_hash(&header.hash()) { 109 | let is_upstream = chain.info().hash() == chain_config.info().hash(); 110 | InspectResult::new(header, Some(name), is_upstream, Some(chain_config.info())) 111 | } else { 112 | InspectResult::new(header, None, false, None) 113 | }; 114 | 115 | print_with_format(result, format) 116 | } 117 | 118 | pub fn decrypt( 119 | cfg: &config::Local, 120 | output: Option, 121 | input: Option, 122 | chain: ConfigChain, 123 | ) -> Result { 124 | let mut src = ResetReader::new(file_or_stdin(input)?); 125 | let header = tlock_age::decrypt_header(&mut src)?; 126 | // Once headers have been read, reset the reader to pass it as if unmodified to tlock_age::decrypt 127 | // This allows the same reader to be used twice. 128 | src.reset(); 129 | 130 | let info = chain.info(); 131 | 132 | if header.hash() != info.hash() { 133 | if let Some((name, _chain_config)) = cfg.chain_by_hash(&header.hash()) { 134 | return Err(anyhow!("decryption failed.\nDid you forget `-u {name}`?")); 135 | } 136 | }; 137 | 138 | let client = HttpClient::new( 139 | &chain.url(), 140 | Some(ChainOptions::new(true, true, Some(info.clone().into()))), 141 | )?; 142 | 143 | let time = RandomnessBeaconTime::from_round(&info.clone().into(), header.round()); 144 | 145 | let beacon = match client.get(header.round()) { 146 | Ok(beacon) => beacon, 147 | Err(DrandError::Beacon(e)) => match *e { 148 | BeaconError::NotFound => return crate::cmd::rand::RandResult::new(None, time).short(), 149 | err => return Err(err.into()), 150 | }, 151 | Err(e) => return Err(e.into()), 152 | }; 153 | 154 | let dst = file_or_stdout(output)?; 155 | if info.is_rfc9380() { 156 | tlock_age::decrypt(dst, src, &header.hash(), &beacon.signature()) 157 | .map(|()| String::from("")) 158 | .map_err(|err| anyhow!(err)) 159 | } else { 160 | tlock_age_non_rfc9380::decrypt(dst, src, &header.hash(), &beacon.signature()) 161 | .map(|()| String::from("")) 162 | .map_err(|err| anyhow!(err)) 163 | } 164 | } 165 | 166 | #[derive(Serialize)] 167 | struct InspectResult { 168 | round: u64, 169 | #[serde(with = "hex::serde")] 170 | hash: Vec, 171 | chain_name: Option, 172 | is_upstream: bool, 173 | chain_info: Option, 174 | } 175 | 176 | impl InspectResult { 177 | pub fn new( 178 | header: Header, 179 | chain_name: Option, 180 | is_upstream: bool, 181 | chain_info: Option, 182 | ) -> Self { 183 | Self { 184 | round: header.round(), 185 | hash: header.hash(), 186 | chain_name, 187 | is_upstream, 188 | chain_info, 189 | } 190 | } 191 | 192 | pub fn round(&self) -> u64 { 193 | self.round 194 | } 195 | 196 | pub fn hash(&self) -> Vec { 197 | self.hash.clone() 198 | } 199 | 200 | pub fn chain_name(&self) -> Option { 201 | self.chain_name.clone() 202 | } 203 | 204 | pub fn chain(&self) -> Option { 205 | self.chain_info.clone() 206 | } 207 | 208 | pub fn is_upstream(&self) -> bool { 209 | self.is_upstream 210 | } 211 | } 212 | 213 | impl Print for InspectResult { 214 | fn short(&self) -> Result { 215 | Ok(format!("{} {}", self.round(), hex::encode(self.hash()))) 216 | } 217 | 218 | fn long(&self) -> Result { 219 | let mut output: Vec = vec![]; 220 | 221 | // Round information 222 | output.push(format!("{: <11}: {}", "Round".bold(), self.round())); 223 | if let Some(chain) = self.chain() { 224 | let format = 225 | time::format_description::parse("[year]-[month]-[day]T[hour]:[minute]:[second]Z")?; 226 | let time = match RandomnessBeaconTime::parse(&chain.into(), &self.round().to_string()) { 227 | Ok(time) => time, 228 | Err(_) => { 229 | return Err(anyhow!( 230 | "Invalid beacon round \"{round}\"", 231 | round = self.round() 232 | )) 233 | } 234 | }; 235 | let relative = time.relative(); 236 | let seconds = relative.whole_seconds().abs() % 60; 237 | let minutes = (relative.whole_minutes()).abs() % 60; 238 | let hours = relative.whole_hours().abs(); 239 | let epoch = match relative.whole_seconds().cmp(&0) { 240 | Ordering::Less => "ago", 241 | Ordering::Equal => "now", 242 | Ordering::Greater => "from now", 243 | }; 244 | let relative = format!("{hours:0>2}:{minutes:0>2}:{seconds:0>2} {epoch}"); 245 | output.push(format!("{: <11}: {}", "Relative".bold(), relative)); 246 | output.push(format!( 247 | "{: <11}: {}", 248 | "Absolute".bold(), 249 | time.absolute().format(&format)? 250 | )); 251 | } 252 | 253 | // Hash information 254 | if let Some(name) = self.chain_name() { 255 | output.push(format!("{: <11}: {}", "Remote Name".bold(), name)); 256 | } 257 | output.push(format!( 258 | "{: <11}: {}", 259 | "Upstream".bold(), 260 | self.is_upstream() 261 | )); 262 | output.push(format!( 263 | "{: <11}: {}", 264 | "Chain Hash".bold(), 265 | hex::encode(self.hash()) 266 | )); 267 | Ok(output.join("\n")) 268 | } 269 | 270 | fn json(&self) -> Result { 271 | Ok(serde_json::to_string(&self)?) 272 | } 273 | } 274 | 275 | // Reader buffering every read, and with the ability to re-read what's been read already. 276 | // This is useful when one need to use stdin reader twice. 277 | struct ResetReader { 278 | inner: R, 279 | buf: Vec, 280 | offset: usize, 281 | is_buffer_enabled: bool, 282 | } 283 | 284 | impl ResetReader { 285 | pub fn new(inner: R) -> Self { 286 | Self { 287 | inner, 288 | buf: vec![], 289 | offset: 0, 290 | is_buffer_enabled: true, 291 | } 292 | } 293 | 294 | /// Reset offset, allowing read from a buf again 295 | pub fn reset(&mut self) { 296 | self.offset = 0; 297 | self.is_buffer_enabled = false; 298 | } 299 | } 300 | 301 | impl std::io::Read for ResetReader { 302 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result { 303 | // Buffer contains enough bytes for this read 304 | if self.buf.len() < self.offset { 305 | buf.copy_from_slice(&self.buf[..buf.len()]); 306 | self.offset += buf.len(); 307 | return Ok(buf.len()); 308 | } 309 | 310 | // Read is a mix of read from buffer and from the reader 311 | let n_read_bytes = self.buf.len() - self.offset; 312 | let (from_buf, from_read) = buf.split_at_mut(n_read_bytes); 313 | // First read from the inner buffer 314 | if n_read_bytes > 0 { 315 | from_buf.copy_from_slice(self.buf.as_slice()); 316 | self.offset += n_read_bytes; 317 | } 318 | 319 | if from_read.is_empty() { 320 | return Ok(n_read_bytes); 321 | } 322 | 323 | // Now read from the reader 324 | let r = match self.inner.read(from_read) { 325 | Ok(size) => size, 326 | Err(e) => return Err(e), 327 | }; 328 | if self.is_buffer_enabled { 329 | self.buf.append(from_read[..r].to_vec().as_mut()); 330 | self.offset += r; 331 | } 332 | Ok(n_read_bytes + r) 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /drand_core/src/chain.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::{beacon::RandomnessBeacon, Result}; 4 | 5 | #[derive(Debug, Serialize, Deserialize, Clone)] 6 | /// Additional information about the chain. 7 | pub struct ChainMetadata { 8 | #[serde(rename(serialize = "beaconID", deserialize = "beaconID"))] 9 | beacon_id: String, 10 | } 11 | 12 | impl ChainMetadata { 13 | pub fn new(beacon_id: String) -> Self { 14 | Self { beacon_id } 15 | } 16 | 17 | /// The ID of the beacon chain this `ChainInfo` corresponds to. 18 | pub fn beacon_id(&self) -> String { 19 | self.beacon_id.clone() 20 | } 21 | } 22 | 23 | impl PartialEq for ChainMetadata { 24 | fn eq(&self, other: &Self) -> bool { 25 | self.beacon_id == other.beacon_id 26 | } 27 | } 28 | 29 | #[derive(Debug, Serialize, Deserialize, Clone)] 30 | pub struct ChainInfo { 31 | #[serde(with = "hex::serde")] 32 | public_key: Vec, 33 | period: u64, 34 | genesis_time: u64, 35 | #[serde(with = "hex::serde")] 36 | hash: Vec, 37 | #[serde( 38 | rename(serialize = "groupHash", deserialize = "groupHash"), 39 | with = "hex::serde" 40 | )] 41 | group_hash: Vec, 42 | #[serde(rename(serialize = "schemeID", deserialize = "schemeID"))] 43 | scheme_id: String, 44 | metadata: ChainMetadata, 45 | } 46 | 47 | impl ChainInfo { 48 | /// Hex encoded BLS12-381 public key. 49 | pub fn public_key(&self) -> Vec { 50 | self.public_key.clone() 51 | } 52 | 53 | /// How often the network emits randomness (in seconds). 54 | pub fn period(&self) -> u64 { 55 | self.period 56 | } 57 | 58 | /// Time of the round 1 of the network (in epoch seconds). 59 | pub fn genesis_time(&self) -> u64 { 60 | self.genesis_time 61 | } 62 | 63 | /// Hash identifying this specific chain of beacons. 64 | pub fn hash(&self) -> Vec { 65 | self.hash.clone() 66 | } 67 | 68 | /// A hash of the group file containing details of all the nodes participating in the network. 69 | pub fn group_hash(&self) -> Vec { 70 | self.group_hash.clone() 71 | } 72 | 73 | /// The version/format of cryptography. 74 | pub fn scheme_id(&self) -> String { 75 | self.scheme_id.clone() 76 | } 77 | 78 | /// Is the chain relying on RFC 9380 Hashing to elliptic curves 79 | pub fn is_rfc9380(&self) -> bool { 80 | self.scheme_id.contains("rfc9380") 81 | } 82 | 83 | pub fn is_unchained(&self) -> bool { 84 | self.scheme_id.contains("unchained") 85 | } 86 | 87 | /// Additional information about the chain. 88 | pub fn metadata(&self) -> ChainMetadata { 89 | self.metadata.clone() 90 | } 91 | } 92 | 93 | impl PartialEq for ChainInfo { 94 | fn eq(&self, other: &Self) -> bool { 95 | self.public_key == other.public_key 96 | && self.period == other.period 97 | && self.genesis_time == other.genesis_time 98 | && self.hash == other.hash 99 | && self.group_hash == other.group_hash 100 | && self.scheme_id == other.scheme_id 101 | && self.metadata == other.metadata 102 | } 103 | } 104 | 105 | #[derive(Debug, Clone)] 106 | /// Retrieval and validation options when interacting with a chain. 107 | /// This controls beacons validation, chain validation, and cache on retrieval. 108 | pub struct ChainOptions { 109 | is_beacon_verification: bool, 110 | is_cache: bool, 111 | chain_verification: ChainVerification, 112 | } 113 | 114 | impl ChainOptions { 115 | pub fn new( 116 | is_beacon_verification: bool, 117 | is_cache: bool, 118 | chain_verification: Option, 119 | ) -> Self { 120 | Self { 121 | is_beacon_verification, 122 | is_cache, 123 | chain_verification: chain_verification.unwrap_or_default(), 124 | } 125 | } 126 | 127 | pub fn is_beacon_verification(&self) -> bool { 128 | self.is_beacon_verification 129 | } 130 | 131 | pub fn is_cache(&self) -> bool { 132 | self.is_cache 133 | } 134 | 135 | pub fn verify(&self, info: &ChainInfo) -> bool { 136 | self.chain_verification.verify(info) 137 | } 138 | } 139 | 140 | impl Default for ChainOptions { 141 | fn default() -> Self { 142 | Self::new(true, true, None) 143 | } 144 | } 145 | 146 | #[derive(Debug, Clone)] 147 | /// Parameters that can be used to validate a chain is the expected one. 148 | pub struct ChainVerification { 149 | hash: Option>, 150 | public_key: Option>, 151 | } 152 | 153 | impl ChainVerification { 154 | pub fn new(hash: Option>, public_key: Option>) -> Self { 155 | Self { hash, public_key } 156 | } 157 | 158 | pub fn verify(&self, info: &ChainInfo) -> bool { 159 | let ok_hash = match &self.hash { 160 | Some(h) => info.hash == *h, 161 | None => true, 162 | }; 163 | let ok_public_key = match &self.public_key { 164 | Some(pk) => info.public_key == *pk, 165 | None => true, 166 | }; 167 | ok_hash && ok_public_key 168 | } 169 | } 170 | 171 | impl Default for ChainVerification { 172 | fn default() -> Self { 173 | Self::new(None, None) 174 | } 175 | } 176 | 177 | impl From for ChainVerification { 178 | fn from(info: ChainInfo) -> Self { 179 | Self::new(Some(info.hash()), Some(info.public_key())) 180 | } 181 | } 182 | 183 | /// Drand client, that can retrieve and validate information from a given chain. 184 | pub trait ChainClient { 185 | /// Options that are used to validate chain result. 186 | fn options(&self) -> ChainOptions; 187 | /// Retrieve latest beacon. 188 | /// This is retrieved and validated based on the client options. 189 | fn latest(&self) -> Result; 190 | /// Retrieve specific round beacon. 191 | /// This is retrieved and validated based on the client options. 192 | fn get(&self, round_number: u64) -> Result; 193 | /// Chain info the client is associated to. 194 | fn chain_info(&self) -> Result; 195 | } 196 | 197 | #[cfg(feature = "time")] 198 | #[derive(Debug, Serialize, Deserialize)] 199 | /// Time information for a chain. 200 | /// Genesis and period, allowing to reconstruct time information of a given beacon. 201 | pub struct ChainTimeInfo { 202 | genesis_time: u64, 203 | period: u64, 204 | } 205 | 206 | #[cfg(feature = "time")] 207 | impl ChainTimeInfo { 208 | pub fn new(genesis_time: u64, period: u64) -> Self { 209 | Self { 210 | genesis_time, 211 | period, 212 | } 213 | } 214 | 215 | pub fn genesis_time(&self) -> u64 { 216 | self.genesis_time 217 | } 218 | 219 | pub fn period(&self) -> u64 { 220 | self.period 221 | } 222 | } 223 | 224 | #[cfg(test)] 225 | pub mod tests { 226 | use super::*; 227 | 228 | /// drand mainnet (curl -sS https://drand.cloudflare.com/info) 229 | pub fn chained_chain_info() -> ChainInfo { 230 | serde_json::from_str(r#"{ 231 | "public_key": "868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31", 232 | "period": 30, 233 | "genesis_time": 1595431050, 234 | "hash": "8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce", 235 | "groupHash": "176f93498eac9ca337150b46d21dd58673ea4e3581185f869672e59fa4cb390a", 236 | "schemeID": "pedersen-bls-chained", 237 | "metadata": { 238 | "beaconID": "default" 239 | } 240 | }"#).unwrap() 241 | } 242 | 243 | /// drand testnet (curl -sS https://pl-us.testnet.drand.sh/7672797f548f3f4748ac4bf3352fc6c6b6468c9ad40ad456a397545c6e2df5bf/info) 244 | pub fn unchained_chain_info() -> ChainInfo { 245 | serde_json::from_str(r#"{ 246 | "public_key": "8200fc249deb0148eb918d6e213980c5d01acd7fc251900d9260136da3b54836ce125172399ddc69c4e3e11429b62c11", 247 | "period": 3, 248 | "genesis_time": 1651677099, 249 | "hash": "7672797f548f3f4748ac4bf3352fc6c6b6468c9ad40ad456a397545c6e2df5bf", 250 | "groupHash": "65083634d852ae169e21b6ce5f0410be9ed4cc679b9970236f7875cff667e13d", 251 | "schemeID": "pedersen-bls-unchained", 252 | "metadata": { 253 | "beaconID": "testnet-unchained-3s" 254 | } 255 | }"#).unwrap() 256 | } 257 | 258 | /// drand fastnet (curl -sS https://drand.cloudflare.com/dbd506d6ef76e5f386f41c651dcb808c5bcbd75471cc4eafa3f4df7ad4e4c493/info) 259 | pub fn unchained_chain_on_g1_info() -> ChainInfo { 260 | serde_json::from_str(r#"{ 261 | "public_key": "a0b862a7527fee3a731bcb59280ab6abd62d5c0b6ea03dc4ddf6612fdfc9d01f01c31542541771903475eb1ec6615f8d0df0b8b6dce385811d6dcf8cbefb8759e5e616a3dfd054c928940766d9a5b9db91e3b697e5d70a975181e007f87fca5e", 262 | "period": 3, 263 | "genesis_time": 1677685200, 264 | "hash": "dbd506d6ef76e5f386f41c651dcb808c5bcbd75471cc4eafa3f4df7ad4e4c493", 265 | "groupHash": "a81e9d63f614ccdb144b8ff79fbd4d5a2d22055c0bfe4ee9a8092003dab1c6c0", 266 | "schemeID": "bls-unchained-on-g1", 267 | "metadata": { 268 | "beaconID": "fastnet" 269 | } 270 | }"#).unwrap() 271 | } 272 | 273 | /// From drand Slack https://drandworkspace.slack.com/archives/C02FWA217GF/p1686583505902169 274 | pub fn unchained_chain_on_g1_rfc_info() -> ChainInfo { 275 | serde_json::from_str(r#"{ 276 | "public_key": "a1ee12542360bf75742bcade13d6134e7d5283d9eb782887c47d3d9725f05805d37b0106b7f744395bf82c175dd7434a169e998f188a657a030d588892c0cd2c01f996aaf331c4d8bc5b9734bbe261d09e7d2d39ef88b635077f262bd7bbb30f", 277 | "period": 3, 278 | "genesis_time": 1677685200, 279 | "hash": "dbd506d6ef76e5f386f41c651dcb808c5bcbd75471cc4eafa3f4df7ad4e4c493", 280 | "groupHash": "a81e9d63f614ccdb144b8ff79fbd4d5a2d22055c0bfe4ee9a8092003dab1c6c0", 281 | "schemeID": "bls-unchained-g1-rfc9380", 282 | "metadata": { 283 | "beaconID": "does-not-exist-slacn" 284 | } 285 | }"#).unwrap() 286 | } 287 | 288 | #[test] 289 | fn chain_verification_success_works() { 290 | // Full validation should pass 291 | let full_verification = ChainVerification::new( 292 | Some(chained_chain_info().hash()), 293 | Some(chained_chain_info().public_key()), 294 | ); 295 | assert!(full_verification.verify(&chained_chain_info())); 296 | 297 | // Validate only the hash 298 | let hash_verification = ChainVerification::new(Some(chained_chain_info().hash()), None); 299 | assert!(hash_verification.verify(&chained_chain_info())); 300 | let hash_verification = ChainVerification::new(Some(chained_chain_info().hash()), None); 301 | let mut chain_info = chained_chain_info(); 302 | chain_info.public_key = unchained_chain_info().public_key(); 303 | assert!(hash_verification.verify(&chain_info)); 304 | 305 | // Validate only the public key 306 | let public_key_verification = 307 | ChainVerification::new(None, Some(chained_chain_info().public_key())); 308 | assert!(public_key_verification.verify(&chained_chain_info())); 309 | let mut chain_info = chained_chain_info(); 310 | chain_info.hash = unchained_chain_info().hash(); 311 | assert!(public_key_verification.verify(&chain_info)); 312 | 313 | // Don't validate 314 | let no_verification = ChainVerification::new(None, None); 315 | assert!(no_verification.verify(&chained_chain_info())); 316 | } 317 | 318 | #[test] 319 | fn chain_verification_failure_works() { 320 | // Full validation should fail when public key is invalid 321 | let full_verification = ChainVerification::new( 322 | Some(chained_chain_info().hash()), 323 | Some(unchained_chain_info().public_key()), 324 | ); 325 | assert!(!full_verification.verify(&chained_chain_info())); 326 | // Full validation should fail when hash is invalid 327 | let full_verification = ChainVerification::new( 328 | Some(unchained_chain_info().hash()), 329 | Some(chained_chain_info().public_key()), 330 | ); 331 | assert!(!full_verification.verify(&chained_chain_info())); 332 | 333 | // Validate only the hash works with invalid public key 334 | let hash_verification = ChainVerification::new(Some(unchained_chain_info().hash()), None); 335 | assert!(!hash_verification.verify(&chained_chain_info())); 336 | 337 | // Validate only the public key 338 | let public_key_verification = 339 | ChainVerification::new(None, Some(unchained_chain_info().public_key())); 340 | assert!(!public_key_verification.verify(&chained_chain_info())); 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /drand_core/src/http_client.rs: -------------------------------------------------------------------------------- 1 | use std::{str::FromStr, sync::Mutex}; 2 | use thiserror::Error; 3 | #[cfg(feature = "time")] 4 | use time::{format_description::well_known::Rfc3339, OffsetDateTime}; 5 | use url::Url; 6 | 7 | #[cfg(feature = "time")] 8 | use crate::beacon::RandomnessBeaconTime; 9 | use crate::{ 10 | beacon::{ApiBeacon, BeaconError, RandomnessBeacon}, 11 | chain::{ChainInfo, ChainOptions}, 12 | DrandError, Result, 13 | }; 14 | 15 | #[derive(Error, Debug)] 16 | pub enum HttpClientError { 17 | #[error("Chain info is invalid")] 18 | InvalidChainInfo, 19 | #[error("Failed to retrieve chain info {message}")] 20 | FailedToRetrieveChainInfo { message: String }, 21 | #[error("{e}. You might need to add \"https://\" to the provided URL.")] 22 | NoProtocol { e: url::ParseError }, 23 | #[error(transparent)] 24 | ParseURL(#[from] url::ParseError), 25 | #[error(transparent)] 26 | RequestFailed(#[from] Box), 27 | } 28 | 29 | /// HTTP Client for drand 30 | /// Queries a specified HTTP endpoint given by `chain`, with specific `options` 31 | /// By default, the client verifies answers, and caches retrieved chain informations 32 | pub struct HttpClient { 33 | base_url: url::Url, 34 | options: ChainOptions, 35 | cached_chain_info: Mutex>, 36 | http_client: ureq::Agent, 37 | } 38 | 39 | impl HttpClient { 40 | pub fn new(base_url: &str, options: Option) -> Result { 41 | // The most common error is when user forget to add protocol in front of the provided URL string. 42 | // The error provided by url::Url is rather obscure when that happens. 43 | let mut url = Url::parse(base_url).map_err(|e| { 44 | if e == url::ParseError::RelativeUrlWithoutBase { 45 | Box::new(HttpClientError::NoProtocol { e }) 46 | } else { 47 | Box::new(HttpClientError::ParseURL(e)) 48 | } 49 | })?; 50 | // Ensure base URL ends with a trailing slash. 51 | // Given it's the base for API calls, it allows for easier joins in other methods. 52 | if !url.path().ends_with('/') { 53 | url.set_path(&format!("{}/", url.path())); 54 | } 55 | Ok(Self { 56 | base_url: url, 57 | options: options.unwrap_or_default(), 58 | cached_chain_info: Mutex::new(None), 59 | http_client: ureq::AgentBuilder::new().build(), 60 | }) 61 | } 62 | 63 | fn chain_info_no_cache(&self) -> Result { 64 | let response = self 65 | .http_client 66 | .get( 67 | self.base_url 68 | .join("info") 69 | .map_err(|e| -> DrandError { Box::new(HttpClientError::ParseURL(e)).into() })? 70 | .as_str(), 71 | ) 72 | .call() 73 | .map_err(|e| -> DrandError { 74 | Box::new(HttpClientError::RequestFailed(e.into())).into() 75 | })?; 76 | let info = if response.status() < 400 { 77 | response 78 | .into_json::() 79 | .map_err(|_| Box::new(BeaconError::Parsing))? 80 | } else { 81 | return Err(Box::new(HttpClientError::FailedToRetrieveChainInfo { 82 | message: response.into_string().unwrap_or_default(), 83 | }) 84 | .into()); 85 | }; 86 | match self.options().verify(&info) { 87 | true => Ok(info), 88 | false => Err(Box::new(HttpClientError::InvalidChainInfo).into()), 89 | } 90 | } 91 | 92 | fn beacon_url(&self, round: String) -> Result { 93 | let mut url = self 94 | .base_url 95 | .join(&format!("public/{round}")) 96 | .map_err(|e| -> DrandError { Box::new(HttpClientError::ParseURL(e)).into() })?; 97 | if !self.options().is_cache() { 98 | url.query_pairs_mut() 99 | .append_key_only(format!("{}", rand::random::()).as_str()); 100 | } 101 | Ok(url) 102 | } 103 | 104 | fn verify_beacon(&self, beacon: RandomnessBeacon, round: String) -> Result { 105 | if !self.options().is_beacon_verification() { 106 | return Ok(beacon); 107 | } 108 | 109 | if !beacon.verify(self.chain_info()?)? { 110 | return Err(Box::new(BeaconError::Validation).into()); 111 | } 112 | 113 | if round == "latest" { 114 | return Ok(beacon); 115 | } 116 | let round: u64 = round 117 | .parse() 118 | .map_err(|_| -> DrandError { Box::new(BeaconError::Parsing).into() })?; 119 | if beacon.round() != round { 120 | return Err(Box::new(BeaconError::RoundMismatch).into()); 121 | } 122 | Ok(beacon) 123 | } 124 | 125 | fn get_with_string(&self, round: String) -> Result { 126 | let beacon = self 127 | .http_client 128 | .get(self.beacon_url(round.clone())?.as_str()) 129 | .call() 130 | .map_err(|e| -> DrandError { 131 | match e { 132 | ureq::Error::Status(404, _) => Box::new(BeaconError::NotFound).into(), 133 | _ => Box::new(HttpClientError::RequestFailed(e.into())).into(), 134 | } 135 | })? 136 | .into_json::() 137 | .map_err(|_| -> DrandError { Box::new(BeaconError::Parsing).into() })?; 138 | 139 | let info = self.chain_info()?; 140 | let unix_time = info.genesis_time() + beacon.round() * info.period(); 141 | let beacon = RandomnessBeacon::new(beacon, unix_time); 142 | 143 | self.verify_beacon(beacon, round) 144 | } 145 | 146 | pub fn base_url(&self) -> String { 147 | self.base_url.to_string() 148 | } 149 | 150 | pub fn options(&self) -> ChainOptions { 151 | self.options.clone() 152 | } 153 | 154 | pub fn chain_info(&self) -> Result { 155 | if self.options().is_cache() { 156 | let cached = self.cached_chain_info.lock().unwrap().to_owned(); 157 | match cached { 158 | Some(info) => Ok(info), 159 | None => { 160 | let info = self.chain_info_no_cache()?; 161 | *self.cached_chain_info.lock().unwrap() = Some(info.clone()); 162 | Ok(info) 163 | } 164 | } 165 | } else { 166 | self.chain_info_no_cache() 167 | } 168 | } 169 | 170 | #[cfg(feature = "time")] 171 | pub fn latest(&self) -> Result { 172 | // it is possible to either use round number 0, latest, or to infer the round number based on the current time 173 | // to allow for round verification, using inferance seems to be the best approach 174 | // without verification, latest is used instead 175 | if self.options().is_beacon_verification() { 176 | let info = self.chain_info()?; 177 | let now = OffsetDateTime::now_utc().format(&Rfc3339).unwrap(); 178 | let time = 179 | RandomnessBeaconTime::parse(&info.into(), &now).expect("time should be valid"); 180 | self.get_with_string(time.round().to_string()) 181 | } else { 182 | self.get_with_string("latest".to_owned()) 183 | } 184 | } 185 | 186 | #[cfg(not(feature = "time"))] 187 | pub fn latest(&self) -> Result { 188 | self.get_with_string("latest".to_owned()) 189 | } 190 | 191 | pub fn get(&self, round_number: u64) -> Result { 192 | self.get_with_string(round_number.to_string()) 193 | } 194 | 195 | pub fn get_by_unix_time(&self, round_unix_time: u64) -> Result { 196 | let info = self.chain_info()?; 197 | let round = (round_unix_time - info.genesis_time()) / info.period(); 198 | 199 | self.get(round) 200 | } 201 | } 202 | 203 | impl crate::chain::ChainClient for HttpClient { 204 | fn options(&self) -> ChainOptions { 205 | self.options() 206 | } 207 | 208 | fn latest(&self) -> Result { 209 | self.latest() 210 | } 211 | 212 | fn get(&self, round_number: u64) -> Result { 213 | self.get(round_number) 214 | } 215 | 216 | fn chain_info(&self) -> Result { 217 | self.chain_info() 218 | } 219 | } 220 | 221 | impl TryFrom<&str> for HttpClient { 222 | type Error = DrandError; 223 | 224 | fn try_from(value: &str) -> std::result::Result { 225 | Self::from_str(value) 226 | } 227 | } 228 | 229 | impl FromStr for HttpClient { 230 | type Err = DrandError; 231 | 232 | fn from_str(s: &str) -> std::result::Result { 233 | Self::new(s, None) 234 | } 235 | } 236 | 237 | #[cfg(test)] 238 | mod tests { 239 | use crate::beacon::{tests::chained_beacon, tests::invalid_beacon, tests::unchained_beacon}; 240 | use crate::chain::{ 241 | tests::chained_chain_info, tests::unchained_chain_info, ChainOptions, ChainVerification, 242 | }; 243 | #[cfg(feature = "time")] 244 | use time::Duration; 245 | 246 | use super::*; 247 | 248 | #[test] 249 | fn client_no_cache_works() { 250 | let mut server = mockito::Server::new(); 251 | let info_mock = server 252 | .mock("GET", "/info") 253 | .match_query(mockito::Matcher::Any) 254 | .with_status(200) 255 | .with_header("content-type", "application/json") 256 | .with_body(serde_json::to_string(&chained_chain_info()).unwrap()) 257 | .expect_at_least(2) 258 | .create(); 259 | let expected_round = chained_beacon().round(); 260 | let get_mock = server 261 | .mock("GET", format!("/public/{expected_round}").as_str()) 262 | .match_query(mockito::Matcher::Any) 263 | .with_status(200) 264 | .with_header("content-type", "application/json") 265 | .with_body(serde_json::to_string(&chained_beacon()).unwrap()) 266 | .expect_at_least(2) 267 | .create(); 268 | 269 | // test client without cache 270 | let no_cache_client = HttpClient::new( 271 | server.url().as_str(), 272 | Some(ChainOptions::new(true, false, None)), 273 | ) 274 | .unwrap(); 275 | 276 | // info endpoint 277 | let info = match no_cache_client.chain_info() { 278 | Ok(info) => info, 279 | Err(_err) => panic!("fetch should have succeded"), 280 | }; 281 | assert_eq!(info, chained_chain_info()); 282 | // do it again to see if it's cached or not 283 | let _ = no_cache_client.chain_info(); 284 | info_mock.assert(); 285 | 286 | // get endpoint 287 | let beacon = match no_cache_client.get(expected_round) { 288 | Ok(beacon) => beacon, 289 | Err(_err) => panic!("fetch should have succeded"), 290 | }; 291 | assert_eq!(beacon.beacon(), chained_beacon()); 292 | assert_eq!(beacon.time(), 1625431050); 293 | // do it again to see if it's cached or not 294 | let _ = no_cache_client.get(expected_round); 295 | get_mock.assert(); 296 | } 297 | 298 | #[test] 299 | fn client_cache_works() { 300 | let mut server = mockito::Server::new(); 301 | let info_mock = server 302 | .mock("GET", "/info") 303 | .match_query(mockito::Matcher::Any) 304 | .with_status(200) 305 | .with_header("content-type", "application/json") 306 | .with_body(serde_json::to_string(&chained_chain_info()).unwrap()) 307 | .expect_at_least(1) 308 | .create(); 309 | let expected_round = chained_beacon().round(); 310 | let get_mock = server 311 | .mock("GET", format!("/public/{expected_round}").as_str()) 312 | .match_query(mockito::Matcher::Any) 313 | .with_status(200) 314 | .with_header("content-type", "application/json") 315 | .with_body(serde_json::to_string(&chained_beacon()).unwrap()) 316 | .expect_at_least(1) 317 | .create(); 318 | 319 | // test client with cache 320 | let cache_client = HttpClient::new( 321 | server.url().as_str(), 322 | Some(ChainOptions::new(false, true, None)), 323 | ) 324 | .unwrap(); 325 | 326 | // info endpoint 327 | let info = match cache_client.chain_info() { 328 | Ok(info) => info, 329 | Err(_err) => panic!("fetch should have succeded"), 330 | }; 331 | assert_eq!(info, chained_chain_info()); 332 | // do it again to see if it's cached or not 333 | let _ = cache_client.chain_info(); 334 | info_mock.assert(); 335 | 336 | // get endpoint 337 | let beacon = match cache_client.get(expected_round) { 338 | Ok(beacon) => beacon, 339 | Err(_err) => panic!("fetch should have succeded"), 340 | }; 341 | assert_eq!(beacon.beacon(), chained_beacon()); 342 | assert_eq!(beacon.time(), 1625431050); 343 | // do it again to see if it's cached or not 344 | let _ = cache_client.get(expected_round); 345 | get_mock.assert(); 346 | } 347 | 348 | // Fakes the chain info genesis time so that the provided beacon is the latest one for the current time 349 | pub fn chain_info_with_latest(beacon: &ApiBeacon) -> ChainInfo { 350 | let info = unchained_chain_info(); 351 | let latest_round = beacon.round(); 352 | let period = info.period(); 353 | let genesis_time = OffsetDateTime::now_utc() 354 | .checked_sub(Duration::seconds((latest_round * period - 1) as i64)) 355 | .unwrap() 356 | .unix_timestamp(); 357 | serde_json::from_str(&format!( 358 | r#"{{ 359 | "public_key": "{public_key}", 360 | "period": {period}, 361 | "genesis_time": {genesis_time}, 362 | "hash": "{hash}", 363 | "groupHash": "{group_hash}", 364 | "schemeID": "{scheme_id}", 365 | "metadata": {{ 366 | "beaconID": "default" 367 | }} 368 | }}"#, 369 | public_key = hex::encode(info.public_key()), 370 | hash = hex::encode(info.hash()), 371 | group_hash = hex::encode(info.group_hash()), 372 | scheme_id = info.scheme_id() 373 | )) 374 | .unwrap() 375 | } 376 | 377 | #[test] 378 | fn client_beacon_verification_works() { 379 | // unchained beacon 380 | let mut valid_server = mockito::Server::new(); 381 | let _info_mock = valid_server 382 | .mock("GET", "/info") 383 | .match_query(mockito::Matcher::Any) 384 | .with_status(200) 385 | .with_header("content-type", "application/json") 386 | .with_body(serde_json::to_string(&chain_info_with_latest(&unchained_beacon())).unwrap()) 387 | .expect_at_least(1) 388 | .create(); 389 | let _latest_mock = valid_server 390 | .mock("GET", "/public/latest") 391 | .match_query(mockito::Matcher::Any) 392 | .with_status(200) 393 | .with_header("content-type", "application/json") 394 | .with_body(serde_json::to_string(&unchained_beacon()).unwrap()) 395 | .expect_at_least(1) 396 | .create(); 397 | let expected_round = unchained_beacon().round(); 398 | let _get_mock = valid_server 399 | .mock("GET", format!("/public/{expected_round}").as_str()) 400 | .match_query(mockito::Matcher::Any) 401 | .with_status(200) 402 | .with_header("content-type", "application/json") 403 | .with_body(serde_json::to_string(&unchained_beacon()).unwrap()) 404 | .expect_at_least(1) 405 | .create(); 406 | 407 | // test client without cache 408 | let client = HttpClient::new( 409 | valid_server.url().as_str(), 410 | Some(ChainOptions::new(true, false, None)), 411 | ) 412 | .unwrap(); 413 | 414 | // latest endpoint 415 | let latest = match client.latest() { 416 | Ok(beacon) => beacon, 417 | Err(err) => panic!("fetch should have succeded {}", err), 418 | }; 419 | assert_eq!(latest.beacon(), unchained_beacon()); 420 | 421 | // test client with round verification 422 | let client = HttpClient::new( 423 | valid_server.url().as_str(), 424 | Some(ChainOptions::new(true, false, None)), 425 | ) 426 | .unwrap(); 427 | 428 | match client.get(expected_round) { 429 | Ok(beacon) => assert_eq!(beacon.beacon(), unchained_beacon()), 430 | Err(err) => panic!("fetch should have succeded {}", err), 431 | } 432 | 433 | let mut invalid_server = mockito::Server::new(); 434 | let _info_mock = invalid_server 435 | .mock("GET", "/info") 436 | .match_query(mockito::Matcher::Any) 437 | .with_status(200) 438 | .with_header("content-type", "application/json") 439 | .with_body(serde_json::to_string(&chained_chain_info()).unwrap()) 440 | .expect_at_least(1) 441 | .create(); 442 | let expected_round = invalid_beacon().round(); 443 | let _get_mock = invalid_server 444 | .mock("GET", format!("/public/{expected_round}").as_str()) 445 | .match_query(mockito::Matcher::Any) 446 | .with_status(200) 447 | .with_header("content-type", "application/json") 448 | .with_body(serde_json::to_string(&invalid_beacon()).unwrap()) 449 | .expect_at_least(1) 450 | .create(); 451 | 452 | // test client without cache 453 | let client = HttpClient::new( 454 | invalid_server.url().as_str(), 455 | Some(ChainOptions::new(true, false, None)), 456 | ) 457 | .unwrap(); 458 | 459 | // get endpoint 460 | match client.get(expected_round) { 461 | Ok(_beacon) => panic!("Beacon should not validate"), 462 | Err(_err) => (), 463 | } 464 | } 465 | 466 | #[test] 467 | fn client_chain_verification_works() { 468 | // unchained beacon 469 | let mut valid_server = mockito::Server::new(); 470 | let _info_mock = valid_server 471 | .mock("GET", "/info") 472 | .match_query(mockito::Matcher::Any) 473 | .with_status(200) 474 | .with_header("content-type", "application/json") 475 | .with_body(serde_json::to_string(&unchained_chain_info()).unwrap()) 476 | .expect_at_least(1) 477 | .create(); 478 | let _latest_mock = valid_server 479 | .mock("GET", "/public/latest") 480 | .match_query(mockito::Matcher::Any) 481 | .with_status(200) 482 | .with_header("content-type", "application/json") 483 | .with_body(serde_json::to_string(&unchained_beacon()).unwrap()) 484 | .expect_at_least(1) 485 | .create(); 486 | let expected_round = chained_beacon().round(); 487 | let _get_mock = valid_server 488 | .mock("GET", format!("/public/{expected_round}").as_str()) 489 | .match_query(mockito::Matcher::Any) 490 | .with_status(200) 491 | .with_header("content-type", "application/json") 492 | .with_body(serde_json::to_string(&unchained_beacon()).unwrap()) 493 | .expect_at_least(1) 494 | .create(); 495 | let invalid_round = expected_round + 1; 496 | let _get_mock = valid_server 497 | .mock("GET", format!("/public/{invalid_round}").as_str()) 498 | .match_query(mockito::Matcher::Any) 499 | .with_status(200) 500 | .with_header("content-type", "application/json") 501 | .with_body(serde_json::to_string(&unchained_beacon()).unwrap()) 502 | .expect_at_least(1) 503 | .create(); 504 | 505 | // test client without cache 506 | let unchained_info = unchained_chain_info(); 507 | let unchained_client = HttpClient::new( 508 | valid_server.url().as_str(), 509 | Some(ChainOptions::new( 510 | true, 511 | false, 512 | Some(ChainVerification::new( 513 | Some(unchained_info.hash()), 514 | Some(unchained_info.public_key()), 515 | )), 516 | )), 517 | ) 518 | .unwrap(); 519 | 520 | // get endpoint 521 | let beacon = match unchained_client.get(expected_round) { 522 | Ok(beacon) => beacon, 523 | Err(err) => panic!("fetch should have succeded {}", err), 524 | }; 525 | assert_eq!(beacon.beacon(), unchained_beacon()); 526 | assert_eq!(beacon.time(), 1654677099); 527 | 528 | // test with not the correct hash 529 | let chained_info = chained_chain_info(); 530 | let invalid_client = HttpClient::new( 531 | valid_server.url().as_str(), 532 | Some(ChainOptions::new( 533 | true, 534 | false, 535 | Some(ChainVerification::new(Some(chained_info.hash()), None)), 536 | )), 537 | ) 538 | .unwrap(); 539 | 540 | match invalid_client.get(expected_round) { 541 | Ok(_beacon) => panic!("Beacon should not validate"), 542 | Err(_err) => (), 543 | }; 544 | // test with not the correct public_key 545 | let chained_info = chained_chain_info(); 546 | let invalid_client = HttpClient::new( 547 | valid_server.url().as_str(), 548 | Some(ChainOptions::new( 549 | true, 550 | false, 551 | Some(ChainVerification::new( 552 | None, 553 | Some(chained_info.public_key()), 554 | )), 555 | )), 556 | ) 557 | .unwrap(); 558 | 559 | match invalid_client.get(expected_round) { 560 | Ok(_beacon) => panic!("Beacon should not validate"), 561 | Err(_err) => (), 562 | }; 563 | 564 | // test with incorrect round 565 | let chained_info = chained_chain_info(); 566 | let invalid_client = HttpClient::new( 567 | valid_server.url().as_str(), 568 | Some(ChainOptions::new( 569 | true, 570 | false, 571 | Some(ChainVerification::new( 572 | Some(chained_info.hash()), 573 | Some(chained_info.public_key()), 574 | )), 575 | )), 576 | ) 577 | .unwrap(); 578 | 579 | match invalid_client.get(invalid_round) { 580 | Ok(_beacon) => panic!("Beacon should not validate"), 581 | Err(_err) => (), 582 | }; 583 | } 584 | } 585 | -------------------------------------------------------------------------------- /drand_core/src/beacon.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use sha2::{Digest, Sha256}; 3 | use thiserror::Error; 4 | #[cfg(feature = "time")] 5 | use time::{ 6 | ext::NumericalDuration, format_description::well_known::Rfc3339, Duration, OffsetDateTime, 7 | }; 8 | 9 | #[cfg(feature = "time")] 10 | use crate::chain::ChainTimeInfo; 11 | #[cfg(feature = "time")] 12 | use crate::DrandError; 13 | use crate::{chain::ChainInfo, Result}; 14 | 15 | #[derive(Error, Debug)] 16 | pub enum BeaconError { 17 | #[cfg(feature = "time")] 18 | #[error("cannot parse duration")] 19 | DurationParse, 20 | #[error("beacon not found")] 21 | NotFound, 22 | #[error("parsing failed")] 23 | Parsing, 24 | #[error("round mismatch")] 25 | RoundMismatch, 26 | #[error("validation failed")] 27 | Validation, 28 | } 29 | 30 | #[derive(Clone, Debug, Serialize)] 31 | pub struct RandomnessBeacon { 32 | #[serde(flatten)] 33 | beacon: ApiBeacon, 34 | #[serde(skip_serializing)] 35 | time: u64, 36 | } 37 | 38 | impl RandomnessBeacon { 39 | pub(crate) fn new(beacon: ApiBeacon, time: u64) -> Self { 40 | Self { beacon, time } 41 | } 42 | 43 | pub fn verify(&self, info: ChainInfo) -> Result { 44 | self.beacon.verify(info) 45 | } 46 | 47 | pub fn round(&self) -> u64 { 48 | self.beacon.round() 49 | } 50 | 51 | pub fn randomness(&self) -> Vec { 52 | self.beacon.randomness() 53 | } 54 | 55 | pub fn is_unchained(&self) -> bool { 56 | self.beacon.is_unchained() 57 | } 58 | 59 | pub fn signature(&self) -> Vec { 60 | self.beacon.signature() 61 | } 62 | 63 | pub fn time(&self) -> u64 { 64 | self.time 65 | } 66 | 67 | #[cfg(test)] 68 | pub(crate) fn beacon(&self) -> ApiBeacon { 69 | self.beacon.clone() 70 | } 71 | } 72 | 73 | #[derive(Clone, Debug, Serialize, Deserialize)] 74 | #[serde(untagged)] 75 | /// Random beacon as generated by drand. 76 | /// These can be chained or unchained, and should be verifiable against a chain. 77 | pub enum ApiBeacon { 78 | ChainedBeacon(ChainedBeacon), 79 | UnchainedBeacon(UnchainedBeacon), 80 | } 81 | 82 | impl ApiBeacon { 83 | pub fn verify(&self, info: ChainInfo) -> Result { 84 | if self.is_unchained() != info.is_unchained() 85 | || self.is_g1() && !info.scheme_id().contains("g1") 86 | { 87 | return Ok(false); 88 | } 89 | 90 | let signature_verify = crate::bls_signatures::verify( 91 | self.dst(&info), 92 | &self.signature(), 93 | &self.message()?, 94 | &info.public_key(), 95 | )?; 96 | 97 | let mut hasher = Sha256::new(); 98 | hasher.update(self.signature()); 99 | let randomness = hasher.finalize().to_vec(); 100 | let randomness_verify = randomness == self.randomness(); 101 | 102 | Ok(signature_verify && randomness_verify) 103 | } 104 | 105 | pub fn round(&self) -> u64 { 106 | match self { 107 | Self::ChainedBeacon(chained) => chained.round, 108 | Self::UnchainedBeacon(unchained) => unchained.round, 109 | } 110 | } 111 | 112 | pub fn randomness(&self) -> Vec { 113 | match self { 114 | Self::ChainedBeacon(chained) => chained.randomness.clone(), 115 | Self::UnchainedBeacon(unchained) => unchained.randomness.clone(), 116 | } 117 | } 118 | 119 | fn dst(&self, info: &ChainInfo) -> &[u8] { 120 | // Name of the HashToCurve RFC compliant scheme has been decided upon in https://github.com/drand/drand/pull/1249 121 | if info.is_rfc9380() && info.scheme_id().contains("g1") { 122 | crate::bls_signatures::G1_DOMAIN 123 | } else { 124 | crate::bls_signatures::G2_DOMAIN 125 | } 126 | } 127 | 128 | pub fn is_unchained(&self) -> bool { 129 | match self { 130 | Self::ChainedBeacon(_) => false, 131 | Self::UnchainedBeacon(_) => true, 132 | } 133 | } 134 | 135 | fn is_g1(&self) -> bool { 136 | match self { 137 | Self::ChainedBeacon(_) => false, 138 | Self::UnchainedBeacon(unchained) => unchained.signature.len() == 48, 139 | } 140 | } 141 | 142 | pub fn signature(&self) -> Vec { 143 | match self { 144 | Self::ChainedBeacon(chained) => chained.signature.clone(), 145 | Self::UnchainedBeacon(unchained) => unchained.signature.clone(), 146 | } 147 | } 148 | } 149 | 150 | impl Message for ApiBeacon { 151 | fn message(&self) -> Result> { 152 | match self { 153 | Self::ChainedBeacon(chained) => chained.message(), 154 | Self::UnchainedBeacon(unchained) => unchained.message(), 155 | } 156 | } 157 | } 158 | 159 | impl From for ApiBeacon { 160 | fn from(b: ChainedBeacon) -> Self { 161 | Self::ChainedBeacon(b) 162 | } 163 | } 164 | 165 | impl From for ApiBeacon { 166 | fn from(b: UnchainedBeacon) -> Self { 167 | Self::UnchainedBeacon(b) 168 | } 169 | } 170 | 171 | /// Package item to be validated against a BLS signature given a public key. 172 | trait Message { 173 | fn message(&self) -> Result>; 174 | } 175 | 176 | #[derive(Clone, Debug, Serialize, Deserialize)] 177 | /// Chained drand beacon. 178 | /// Each signature depends on the previous one, as well as on the round. 179 | pub struct ChainedBeacon { 180 | round: u64, 181 | #[serde(with = "hex::serde")] 182 | randomness: Vec, 183 | #[serde(with = "hex::serde")] 184 | signature: Vec, 185 | #[serde(with = "hex::serde")] 186 | previous_signature: Vec, 187 | } 188 | 189 | impl Message for ChainedBeacon { 190 | fn message(&self) -> Result> { 191 | // First round signature is on the genesis seed, which size is 32B, and not 96B like G2 signatures. 192 | let len = if self.round == 1 { 32 } else { 96 }; 193 | let mut buf = vec![0; len + 8]; 194 | let (signature_buf, round_buf) = buf.split_at_mut(len); 195 | 196 | signature_buf.clone_from_slice(&self.previous_signature); 197 | round_buf.clone_from_slice(&self.round.to_be_bytes()); 198 | 199 | let mut hasher = Sha256::new(); 200 | hasher.update(buf); 201 | Ok(hasher.finalize().to_vec()) 202 | } 203 | } 204 | 205 | #[derive(Clone, Debug, Serialize, Deserialize)] 206 | /// Unchained drand beacon. 207 | /// Each signature only depends on the round number. 208 | pub struct UnchainedBeacon { 209 | round: u64, 210 | #[serde(with = "hex::serde")] 211 | randomness: Vec, 212 | #[serde(with = "hex::serde")] 213 | signature: Vec, 214 | } 215 | 216 | impl Message for UnchainedBeacon { 217 | fn message(&self) -> Result> { 218 | let buf = self.round.to_be_bytes(); 219 | 220 | let mut hasher = Sha256::new(); 221 | hasher.update(buf); 222 | Ok(hasher.finalize().to_vec()) 223 | } 224 | } 225 | 226 | #[cfg(feature = "time")] 227 | impl From for ChainTimeInfo { 228 | fn from(value: ChainInfo) -> Self { 229 | Self::new(value.genesis_time(), value.period()) 230 | } 231 | } 232 | 233 | #[cfg(feature = "time")] 234 | #[derive(Debug, Serialize, Deserialize)] 235 | /// Time of a randomness beacon as seen by drand. 236 | /// Round and absolute are uniquely tied to a round. 237 | /// Relative time is generated upon object creation. 238 | pub struct RandomnessBeaconTime { 239 | round: u64, 240 | relative: Duration, 241 | #[cfg_attr(feature = "time", serde(with = "time::serde::rfc3339"))] 242 | absolute: OffsetDateTime, 243 | } 244 | 245 | #[cfg(feature = "time")] 246 | impl RandomnessBeaconTime { 247 | /// round can be: 248 | /// * a specific round. e.g. 123, 249 | /// * a duration. e.g. 30s, 250 | /// * an RFC3339 date. e.g. 2023-06-28 21:30:22 251 | pub fn parse(info: &ChainTimeInfo, round: &str) -> Result { 252 | match ( 253 | round.parse::(), 254 | Self::parse_duration(round), 255 | Self::parse_offset_datetime(round), 256 | ) { 257 | (Ok(round), Err(_), Err(_)) => Ok(Self::from_round(info, round)), 258 | (Err(_), Ok(relative), Err(_)) => Ok(Self::from_duration(info, relative)), 259 | (Err(_), Err(_), Ok(absolute)) => Ok(Self::from_datetime(info, absolute)), 260 | _ => Err(Box::new(BeaconError::Parsing).into()), 261 | } 262 | } 263 | 264 | pub fn round(&self) -> u64 { 265 | self.round 266 | } 267 | 268 | pub fn relative(&self) -> Duration { 269 | self.relative 270 | } 271 | 272 | pub fn absolute(&self) -> OffsetDateTime { 273 | self.absolute 274 | } 275 | 276 | pub fn from_round(info: &ChainTimeInfo, round: u64) -> Self { 277 | let genesis = OffsetDateTime::from_unix_timestamp(info.genesis_time() as i64).unwrap(); 278 | 279 | let absolute = genesis + (((round - 1) * info.period()) as i64).seconds(); 280 | let relative = absolute - OffsetDateTime::now_utc(); 281 | Self { 282 | round, 283 | relative, 284 | absolute, 285 | } 286 | } 287 | 288 | fn from_duration(info: &ChainTimeInfo, relative: Duration) -> Self { 289 | let genesis = OffsetDateTime::from_unix_timestamp(info.genesis_time() as i64).unwrap(); 290 | 291 | let absolute = OffsetDateTime::now_utc() + relative; 292 | let round = ((absolute - genesis).whole_seconds() / (info.period() as i64) + 1) as u64; 293 | 294 | Self { 295 | round, 296 | relative, 297 | absolute, 298 | } 299 | } 300 | 301 | fn from_datetime(info: &ChainTimeInfo, absolute: OffsetDateTime) -> Self { 302 | let genesis = OffsetDateTime::from_unix_timestamp(info.genesis_time() as i64).unwrap(); 303 | 304 | let relative = absolute - OffsetDateTime::now_utc(); 305 | let round = ((absolute - genesis).whole_seconds() / (info.period() as i64) + 1) as u64; 306 | 307 | Self { 308 | round, 309 | relative, 310 | absolute, 311 | } 312 | } 313 | 314 | fn parse_duration(duration: &str) -> Result { 315 | let l = duration.len() - 1; 316 | let principal = duration[0..l] 317 | .parse::() 318 | .map_err(|_| -> DrandError { Box::new(BeaconError::DurationParse).into() })?; 319 | 320 | let duration = match duration.chars().last().unwrap() { 321 | 's' => principal.seconds(), 322 | 'm' => principal.minutes(), 323 | 'h' => principal.hours(), 324 | 'd' => principal.days(), 325 | _char => return Err(Box::new(BeaconError::DurationParse).into()), 326 | }; 327 | Ok(duration) 328 | } 329 | 330 | fn parse_offset_datetime(datetime: &str) -> Result { 331 | let datetime = datetime.to_string().replace(' ', "T"); 332 | let datetime = if datetime.len() == 10 { 333 | format!("{}T00:00:00Z", datetime) 334 | } else { 335 | datetime 336 | }; 337 | let datetime = if datetime.contains('+') || datetime.ends_with('Z') { 338 | datetime 339 | } else { 340 | format!("{}Z", datetime) 341 | }; 342 | 343 | OffsetDateTime::parse(&datetime, &Rfc3339) 344 | .map_err(|_| -> DrandError { Box::new(BeaconError::Parsing).into() }) 345 | } 346 | } 347 | 348 | #[cfg(test)] 349 | pub mod tests { 350 | use std::ops::Sub; 351 | 352 | use crate::chain::{ 353 | tests::chained_chain_info, 354 | tests::{unchained_chain_info, unchained_chain_on_g1_info, unchained_chain_on_g1_rfc_info}, 355 | }; 356 | 357 | use super::*; 358 | 359 | /// drand mainnet (curl -sS https://drand.cloudflare.com/public/1000000) 360 | pub fn chained_beacon() -> ApiBeacon { 361 | serde_json::from_str(r#"{ 362 | "round": 1000000, 363 | "randomness": "a26ba4d229c666f52a06f1a9be1278dcc7a80dbc1dd2004a1ae7b63cb79fd37e", 364 | "signature": "87e355169c4410a8ad6d3e7f5094b2122932c1062f603e6628aba2e4cb54f46c3bf1083c3537cd3b99e8296784f46fb40e090961cf9634f02c7dc2a96b69fc3c03735bc419962780a71245b72f81882cf6bb9c961bcf32da5624993bb747c9e5", 365 | "previous_signature": "86bbc40c9d9347568967add4ddf6e351aff604352a7e1eec9b20dea4ca531ed6c7d38de9956ffc3bb5a7fabe28b3a36b069c8113bd9824135c3bff9b03359476f6b03beec179d4aeff456f4d34bbf702b9af78c3bb44e1892ace8e581bf4afa9" 366 | }"#).unwrap() 367 | } 368 | 369 | /// drand mainnet (curl -sS https://drand.cloudflare.com/public/1) 370 | pub fn chained_beacon_1() -> ApiBeacon { 371 | serde_json::from_str(r#"{ 372 | "round": 1, 373 | "randomness": "101297f1ca7dc44ef6088d94ad5fb7ba03455dc33d53ddb412bbc4564ed986ec", 374 | "signature": "8d61d9100567de44682506aea1a7a6fa6e5491cd27a0a0ed349ef6910ac5ac20ff7bc3e09d7c046566c9f7f3c6f3b10104990e7cb424998203d8f7de586fb7fa5f60045417a432684f85093b06ca91c769f0e7ca19268375e659c2a2352b4655", 375 | "previous_signature": "176f93498eac9ca337150b46d21dd58673ea4e3581185f869672e59fa4cb390a" 376 | }"#).unwrap() 377 | } 378 | 379 | /// drand testnet (curl -sS https://pl-us.testnet.drand.sh/7672797f548f3f4748ac4bf3352fc6c6b6468c9ad40ad456a397545c6e2df5bf/public/1000000) 380 | pub fn unchained_beacon() -> ApiBeacon { 381 | serde_json::from_str(r#"{ 382 | "round": 1000000, 383 | "randomness": "6671747f7d838f18159c474579ea19e8d863e8c25e5271fd7f18ca2ac85181cf", 384 | "signature": "86b265e10e060805d20dca88f70f6b5e62d5956e7790d32029dfb73fbcd1996bc7aebdea7aeaf74dac0ca2b3ce8f7a6a0399f224a05fe740c0bac9da638212082b0ed21b1a8c5e44a33123f28955ef0713e93e21f6af0cda4073d9a73387434d" 385 | }"#).unwrap() 386 | } 387 | 388 | /// drand fastnet (curl -sS https://drand.cloudflare.com/dbd506d6ef76e5f386f41c651dcb808c5bcbd75471cc4eafa3f4df7ad4e4c493/public/100000) 389 | pub fn unchained_beacon_on_g1() -> ApiBeacon { 390 | serde_json::from_str(r#"{ 391 | "round": 100000, 392 | "randomness": "37aa25aa1e0b52440502e6f841c956bf72d693770a511e59768ecb7777c172ce", 393 | "signature": "b370f411d5479fc342b504347226e4b543fee28698fa721876d55d36c12a20f3f49b7abd31ee99979e2d28e14f1d3152" 394 | }"#).unwrap() 395 | } 396 | 397 | /// From drand Slack https://drandworkspace.slack.com/archives/C02FWA217GF/p1686583505902169 398 | pub fn unchained_beacon_on_g1_rfc() -> ApiBeacon { 399 | serde_json::from_str(r#"{ 400 | "round": 3, 401 | "randomness":"9e9829dfb34bd8db3e21c28e13aefecd86e007ebd19d6bb8a5cee99c0a34798f", 402 | "signature":"b98dae74f6a9d2ec79d75ba273dcfda86a45d589412860eb4c0fd056b00654dbf667c1b6884987c9aee0d43f8ba9db52" 403 | }"#).unwrap() 404 | } 405 | 406 | /// invalid beacon. Round should be 1,000,000, but it 1 407 | pub fn invalid_beacon() -> ApiBeacon { 408 | serde_json::from_str(r#"{ 409 | "round": 1234, 410 | "randomness": "a26ba4d229c666f52a06f1a9be1278dcc7a80dbc1dd2004a1ae7b63cb79fd37e", 411 | "signature": "87e355169c4410a8ad6d3e7f5094b2122932c1062f603e6628aba2e4cb54f46c3bf1083c3537cd3b99e8296784f46fb40e090961cf9634f02c7dc2a96b69fc3c03735bc419962780a71245b72f81882cf6bb9c961bcf32da5624993bb747c9e5", 412 | "previous_signature": "86bbc40c9d9347568967add4ddf6e351aff604352a7e1eec9b20dea4ca531ed6c7d38de9956ffc3bb5a7fabe28b3a36b069c8113bd9824135c3bff9b03359476f6b03beec179d4aeff456f4d34bbf702b9af78c3bb44e1892ace8e581bf4afa9" 413 | }"#).unwrap() 414 | } 415 | 416 | impl PartialEq for ApiBeacon { 417 | fn eq(&self, other: &Self) -> bool { 418 | match (self, other) { 419 | (Self::ChainedBeacon(chained), Self::ChainedBeacon(other)) => chained == other, 420 | (Self::UnchainedBeacon(unchained), Self::UnchainedBeacon(other)) => { 421 | unchained == other 422 | } 423 | _ => false, 424 | } 425 | } 426 | } 427 | 428 | impl PartialEq for ChainedBeacon { 429 | fn eq(&self, other: &Self) -> bool { 430 | self.randomness == other.randomness 431 | && self.round == other.round 432 | && self.signature == other.signature 433 | && self.previous_signature == other.previous_signature 434 | } 435 | } 436 | 437 | impl PartialEq for UnchainedBeacon { 438 | fn eq(&self, other: &Self) -> bool { 439 | self.randomness == other.randomness 440 | && self.round == other.round 441 | && self.signature == other.signature 442 | } 443 | } 444 | 445 | #[test] 446 | fn randomness_beacon_verification_success_works() { 447 | match chained_beacon().verify(chained_chain_info()) { 448 | Ok(ok) => assert!(ok), 449 | Err(_err) => panic!("Chained beacon should validate on chained info"), 450 | } 451 | 452 | match chained_beacon_1().verify(chained_chain_info()) { 453 | Ok(ok) => assert!(ok), 454 | Err(_err) => { 455 | panic!("Chained beacon should validate on chained info for the first beacon") 456 | } 457 | } 458 | 459 | match unchained_beacon().verify(unchained_chain_info()) { 460 | Ok(ok) => assert!(ok), 461 | Err(_err) => panic!("Unchained beacon should validate on unchained info"), 462 | } 463 | 464 | match unchained_beacon_on_g1().verify(unchained_chain_on_g1_info()) { 465 | Ok(ok) => assert!(ok), 466 | Err(_err) => panic!("Unchained beacon on G1 should validate on unchained info"), 467 | } 468 | 469 | match unchained_beacon_on_g1_rfc().verify(unchained_chain_on_g1_rfc_info()) { 470 | Ok(ok) => assert!(ok), 471 | Err(_err) => panic!("Unchained beacon on G1 RFC should validate on unchained info"), 472 | } 473 | } 474 | 475 | #[test] 476 | fn randomness_beacon_verification_failure_works() { 477 | match invalid_beacon().verify(chained_chain_info()) { 478 | Ok(ok) => assert!(!ok, "Invalid beacon should not validate"), 479 | Err(_err) => panic!("Invalid beacon should not validate without returning an error"), 480 | } 481 | 482 | match unchained_beacon().verify(chained_chain_info()) { 483 | Ok(ok) => assert!(!ok, "Unchained beacon should not validate on chained info"), 484 | Err(_err) => panic!( 485 | "Unchained beacon should not validate on chained info without returning an error" 486 | ), 487 | } 488 | 489 | match unchained_beacon_on_g1().verify(unchained_chain_info()) { 490 | Ok(ok) => assert!(!ok, "Unchained beacon on G1 should not validate on chained info"), 491 | Err(_err) => panic!( 492 | "Unchained beacon on G1 should not validate on chained info without returning an error" 493 | ), 494 | } 495 | 496 | // Regression test to confirm the introduction of RFC compliant chain does not break existing integration 497 | // Original change is on [drand/drand#1249](https://github.com/drand/drand/pull/1249) 498 | match unchained_beacon_on_g1().verify(unchained_chain_on_g1_rfc_info()) { 499 | Ok(ok) => assert!(!ok, "Unchained beacon on G1 (not RFC compliant) should not validate on unchained compliant info"), 500 | Err(_err) => panic!( 501 | "Unchained beacon on G1 (non Hash to curve RFC compliant) should not validate on unchained G1 info without returning an error" 502 | ), 503 | } 504 | 505 | match unchained_beacon_on_g1_rfc().verify(unchained_chain_on_g1_info()) { 506 | Ok(ok) => assert!(!ok, "Unchained beacon on G1 should not validate on unchained G1 (non Hash to curve RFC compliant) info"), 507 | Err(_err) => panic!( 508 | "Unchained beacon on G1 should not validate on unchained G1 (non Hash to curve RFC compliant) info without returning an error" 509 | ), 510 | } 511 | } 512 | 513 | #[test] 514 | fn randomness_beacon_time_success_works() { 515 | const FIRST_ROUND: u64 = 1; 516 | let chain = unchained_chain_info().into(); 517 | let beacon_time = RandomnessBeaconTime::parse(&chain, &FIRST_ROUND.to_string()).unwrap(); 518 | assert!( 519 | beacon_time.round() == FIRST_ROUND, 520 | "Round number has been modified when computing its time" 521 | ); 522 | assert!( 523 | beacon_time.absolute().unix_timestamp() as u64 == chain.genesis_time(), 524 | "Time of the first round must be genesis time" 525 | ); 526 | assert!( 527 | beacon_time.relative().is_negative(), 528 | "First round should be before current time" 529 | ); 530 | 531 | let genesis_beacon_time = 532 | RandomnessBeaconTime::parse(&chain, &beacon_time.absolute().format(&Rfc3339).unwrap()) 533 | .unwrap(); 534 | assert!( 535 | genesis_beacon_time.round() == FIRST_ROUND, 536 | "Parsing genesis from absolute time should provide the first round" 537 | ); 538 | assert!( 539 | genesis_beacon_time 540 | .relative() 541 | .abs() 542 | .sub(beacon_time.relative().abs()) 543 | .is_positive(), 544 | "Parsing the same beacon at two different interval should advance relative time" 545 | ); 546 | 547 | const FUTURE_ROUND: u64 = 10 * 1000 * 1000 * 1000; // attempt of max round. cannot use u64::MAX because we're going to perform multiplication and additions, which would go past the limit 548 | let chain = unchained_chain_info().into(); 549 | let beacon_time = RandomnessBeaconTime::parse(&chain, &FUTURE_ROUND.to_string()).unwrap(); 550 | assert!( 551 | beacon_time.round() == FUTURE_ROUND, 552 | "Round number has been modified when computing its time" 553 | ); 554 | assert!( 555 | beacon_time.absolute().unix_timestamp() as u64 556 | == chain.genesis_time() + (FUTURE_ROUND - 1) * chain.period(), 557 | "Time of a future round should be genesis + period" 558 | ); 559 | assert!( 560 | beacon_time.relative().is_positive(), 561 | "Future round should be after current time" 562 | ); 563 | 564 | const FUTURE_ROUND_RELATIVE: u64 = 10; 565 | const FUTURE_ROUND_RELATIVE_TIME: &str = "30s"; 566 | let chain = unchained_chain_info().into(); 567 | let beacon_time = RandomnessBeaconTime::parse(&chain, "0s").unwrap(); 568 | let future_beacon_time = 569 | RandomnessBeaconTime::parse(&chain, FUTURE_ROUND_RELATIVE_TIME).unwrap(); 570 | assert!( 571 | beacon_time.round() + FUTURE_ROUND_RELATIVE == future_beacon_time.round(), 572 | "Round number should match period*difference in round" 573 | ); 574 | assert!( 575 | future_beacon_time 576 | .relative() 577 | .sub(beacon_time.relative()) 578 | .whole_seconds() 579 | .to_string() 580 | + "s" 581 | == FUTURE_ROUND_RELATIVE_TIME, 582 | "Relative time parsing should be precise up to the second" 583 | ); 584 | } 585 | } 586 | --------------------------------------------------------------------------------