diff --git a/Cargo.toml b/Cargo.toml index 3c836a7..e97206c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,12 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -hex = "0.4.3" +hex = { version = "0.4.3", features = ["serde"] } url = "2.3.1" reqwest = { version = "0.11", features = ["blocking"] } json = "0.12.4" bls-signatures = "0.13.0" -thiserror = "1.0.38" \ No newline at end of file +thiserror = "1.0.38" +sha256 = "1.1.1" +serde = { version = "1.0.151", features = ["derive"] } +serde_json = "1.0.91" \ No newline at end of file diff --git a/README.md b/README.md index b5b21dd..afb7539 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 🦀 drand-client-rs +# 🎲 🦀 drand-client-rs A simple drand client implementation written in rust @@ -6,7 +6,7 @@ A simple drand client implementation written in rust - [x] http transport - [x] json parser - [x] chained scheme -- [ ] unchained scheme +- [x] unchained scheme - [ ] unchained scheme with G1 and G2 swapped - [ ] protobuf parser - [ ] libp2p transport diff --git a/src/bls.rs b/src/bls.rs new file mode 100644 index 0000000..6bcfe78 --- /dev/null +++ b/src/bls.rs @@ -0,0 +1,30 @@ +use bls_signatures::{hash, PublicKey, Serialize, Signature, verify}; +use crate::chain_info::ChainInfo; +use crate::SchemeError; + +pub trait BlsVerifiable { + fn signature(&self) -> &Vec; + fn to_message(&self) -> Result, SchemeError>; +} + +pub(crate) fn bls_verify(info: &ChainInfo, beacon: B) -> Result { + let public_key = PublicKey::from_bytes(info.public_key.as_slice()) + .map_err(|_| SchemeError::InvalidChainInfo)?; + + let signature = Signature::from_bytes(&beacon.signature().as_slice()) + .map_err(|_| SchemeError::InvalidBeacon)?; + + let bls_message_bytes = beacon.to_message() + .map(|bytes| sha256::digest(bytes.as_slice())) + .and_then(|hex_str| hex::decode(hex_str) + .map_err(|_| SchemeError::InvalidBeacon) + )?; + + + let point_on_curve = hash(bls_message_bytes.as_slice()); + if !verify(&signature, &[point_on_curve], &[public_key]) { + return Err(SchemeError::InvalidBeacon); + } + + return Ok(beacon); +} diff --git a/src/chain_info.rs b/src/chain_info.rs new file mode 100644 index 0000000..4a85c33 --- /dev/null +++ b/src/chain_info.rs @@ -0,0 +1,23 @@ +use serde::Deserialize; + +#[derive(Deserialize, Debug, PartialEq, Clone)] +pub struct ChainInfo { + #[serde(alias = "schemeID")] + pub scheme_id: String, + #[serde(with = "hex")] + pub public_key: Vec, + #[serde(with = "hex", alias = "hash")] + pub chain_hash: Vec, + #[serde(with = "hex", alias = "groupHash")] + pub group_hash: Vec, + pub genesis_time: u64, + #[serde(alias = "period")] + pub period_seconds: usize, + pub metadata: ChainInfoMetadata, +} + +#[derive(Deserialize, Debug, PartialEq, Clone)] +pub struct ChainInfoMetadata { + #[serde(alias = "beaconID")] + pub beacon_id: String, +} \ No newline at end of file diff --git a/src/chained.rs b/src/chained.rs index 21a0f3f..ff47335 100644 --- a/src/chained.rs +++ b/src/chained.rs @@ -1,96 +1,26 @@ -use bls_signatures::{PublicKey, Serialize, Signature, verify}; -use json::JsonValue; -use crate::{ChainInfo, Scheme, SchemeError}; -use thiserror::Error; - -pub(crate) struct ChainedBeacon { +use std::io::{Write}; +use crate::{bls, Scheme, SchemeError}; +use crate::chain_info::ChainInfo; +use serde::Deserialize; +use crate::bls::BlsVerifiable; + +#[derive(Deserialize, Debug, PartialEq, Clone)] +pub struct ChainedBeacon { + #[serde(alias = "round")] pub round_number: u64, + #[serde(with = "hex")] pub randomness: Vec, + #[serde(with = "hex")] pub signature: Vec, + #[serde(with = "hex")] pub previous_signature: Vec, } - -#[derive(Error, Debug)] -pub enum JsonParseError { - #[error("unknown error")] - UnknownError, - #[error("empty payload")] - EmptyPayload, - #[error("could not parse")] - CouldNotParse, - #[error("invalid json")] - InvalidJson, - #[error("invalid type")] - InvalidType { key: String }, - #[error("could not parse value")] - CouldNotParseValue { key: String }, - #[error("unknown key")] - UnknownKey { key: String }, -} - -impl TryFrom for ChainedBeacon { - type Error = JsonParseError; - - fn try_from(value: JsonValue) -> Result { - if !value.is_object() { - return Err(JsonParseError::CouldNotParse); - } - - let mut out = ChainedBeacon { - round_number: 0, - randomness: vec![], - signature: vec![], - previous_signature: vec![], - }; - - for (key, value) in value.entries() { - match key { - "round" => { - if !value.is_number() { - return Err(JsonParseError::CouldNotParseValue { key: key.to_string() }); - } - if let Some(round_number) = value.as_u64() { - out.round_number = round_number - } else { - return Err(JsonParseError::CouldNotParseValue { key: key.to_string() }); - } - } - "randomness" => { - out.randomness = parse_bytes(key, value)? - } - "signature" => { - out.signature = parse_bytes(key, value)? - } - "previous_signature" => { - out.previous_signature = parse_bytes(key, value)? - } - _ => return Err(JsonParseError::UnknownKey { key: key.to_string() }) - } - } - - return Ok(out); - } -} - -fn parse_bytes<'a>(key: &str, value: &JsonValue) -> Result, JsonParseError> { - if !value.is_string() { - return Err(JsonParseError::InvalidType { key: key.to_string() }); - } - return match value.as_str() { - None => - Err(JsonParseError::CouldNotParseValue { key: key.to_string() }), - Some(hex_str) => - hex::decode(hex_str) - .map_err(|_| JsonParseError::CouldNotParseValue { key: key.to_string() }) - }; -} - pub struct ChainedScheme {} impl Scheme for ChainedScheme { fn supports(&self, scheme_id: &str) -> bool { - return scheme_id.eq_ignore_ascii_case("bls-pedersen-chained"); + return scheme_id.eq_ignore_ascii_case("pedersen-bls-chained"); } fn verify(&self, info: &ChainInfo, beacon: ChainedBeacon) -> Result { @@ -98,16 +28,25 @@ impl Scheme for ChainedScheme { return Err(SchemeError::InvalidScheme); } - let public_key = PublicKey::from_bytes(info.public_key.as_slice()) - .map_err(|_| SchemeError::InvalidChainInfo)?; + return bls::bls_verify(info, beacon); + } +} + +impl BlsVerifiable for ChainedBeacon { + fn signature(&self) -> &Vec { + &self.signature + } - let signature = Signature::from_bytes(&beacon.signature.as_slice()) - .map_err(|_| SchemeError::InvalidBeacon)?; + fn to_message(&self) -> Result, SchemeError> { + let mut bytes: Vec = vec![]; - if !verify(&signature, &[], &[public_key]) { + if bytes.write_all(self.previous_signature.as_slice()).is_err() { return Err(SchemeError::InvalidBeacon); } + if bytes.write_all(&self.round_number.to_be_bytes()).is_err() { + return Err(SchemeError::InvalidBeacon); + }; - return Ok(beacon); + return Ok(bytes); } -} \ No newline at end of file +} diff --git a/src/http.rs b/src/http.rs new file mode 100644 index 0000000..d1f5e0d --- /dev/null +++ b/src/http.rs @@ -0,0 +1,33 @@ +use reqwest::blocking::Client; +use reqwest::StatusCode; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum HttpError { + #[error("not found")] + NotFound, + #[error("unexpected")] + Unexpected, +} + +pub struct HttpTransport { + pub client: Client, +} + +impl HttpTransport { + pub fn fetch<'a>(&self, url: &str) -> Result { + let res = self.client.get(url) + .send() + .map_err(|_| HttpError::Unexpected)?; + + return match res.status() { + StatusCode::OK => res.text() + .map_err(|_| HttpError::Unexpected), + + StatusCode::NOT_FOUND => + Err(HttpError::NotFound), + + _ => Err(HttpError::Unexpected), + }; + } +} diff --git a/src/lib.rs b/src/lib.rs index 5cc7104..6581e1b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,97 +1,79 @@ +extern crate core; + mod chained; +mod unchained; +mod http; +mod chain_info; +mod bls; -use json::JsonValue; -use reqwest::{StatusCode}; use reqwest::blocking::Client; +use serde::de::DeserializeOwned; use thiserror::Error; +use crate::chain_info::ChainInfo; use crate::chained::{ChainedBeacon, ChainedScheme}; - - -#[derive(Error, Debug)] -enum HttpError { - #[error("not found")] - NotFound, - #[error("unexpected")] - Unexpected, -} - -struct HttpTransport { - client: Client, -} - -impl HttpTransport { - fn fetch(&self, url: &str) -> Result { - let res = self.client.get(url) - .send() - .map_err(|_| HttpError::Unexpected)?; - - return match res.status() { - StatusCode::OK => res.text() - .map_err(|_| HttpError::Unexpected), - - StatusCode::NOT_FOUND => - Err(HttpError::NotFound), - - _ => Err(HttpError::Unexpected), - }; - } -} - -#[derive(Error, Debug)] -enum ParseError { - #[error("malformed input")] - MalformedInput, - #[error("did not parse")] - DidNotParse(#[from] E), -} - -struct JsonParser {} - -impl JsonParser { - fn parse>(&self, input: &str) -> Result> { - return json::parse(input) - .map_err(|_| ParseError::MalformedInput) - .map(|json| - B::try_from(json) - .map_err(|e| ParseError::DidNotParse(e)) - )?; - } -} +use crate::DrandClientError::{InvalidChainInfo, InvalidRound}; +use crate::http::HttpTransport; +use crate::unchained::{UnchainedBeacon, UnchainedScheme}; struct DrandClient<'a, B> { scheme: &'a dyn Scheme, transport: HttpTransport, - parser: JsonParser, base_url: &'a str, + chain_info: ChainInfo, } -fn new_chained_client(base_url: &str) -> DrandClient { - return DrandClient { - scheme: &ChainedScheme {}, - transport: HttpTransport { - client: reqwest::blocking::Client::new(), - }, - parser: JsonParser {}, +fn new_chained_client(base_url: &str) -> Result, DrandClientError> { + return new_client(&ChainedScheme {}, base_url); +} + +fn new_unchained_client(base_url: &str) -> Result, DrandClientError> { + return new_client(&UnchainedScheme {}, base_url); +} + +fn new_client<'a, S: Scheme, B>(scheme: &'a S, base_url: &'a str) -> Result, DrandClientError> { + let http_transport = HttpTransport { + client: Client::new(), + }; + let chain_info = fetch_chain_info(&http_transport, base_url)?; + let client = DrandClient { + transport: http_transport, + chain_info, + scheme, base_url, }; + return Ok(client); } -#[derive(Error, Debug)] +#[derive(Error, Debug, PartialEq)] enum DrandClientError { #[error("invalid round")] InvalidRound, #[error("invalid beacon")] InvalidBeacon, + #[error("invalid chain info")] + InvalidChainInfo, #[error("not responding")] NotResponding, } -impl<'a, B> DrandClient<'a, B> where B: TryFrom { +fn fetch_chain_info(transport: &HttpTransport, base_url: &str) -> Result { + let url = format!("{}/info", base_url); + return match transport.fetch(&url) { + Err(_) => Err(DrandClientError::NotResponding), + Ok(body) => serde_json::from_str(&body) + .map_err(|_| InvalidChainInfo) + }; +} + +impl<'a, B> DrandClient<'a, B> where B: DeserializeOwned { fn latest_randomness(&self) -> Result { return self.fetch_beacon_tag("latest"); } fn randomness(&self, round_number: u64) -> Result { + if round_number == 0 { + return Err(InvalidRound); + } return self.fetch_beacon_tag(&format!("{}", round_number)); } @@ -101,16 +83,20 @@ impl<'a, B> DrandClient<'a, B> where B: TryFrom { Err(_) => Err(DrandClientError::NotResponding), - Ok(beacon_str) => { - self.parser.parse::(&beacon_str) - .map_err(|_| DrandClientError::InvalidBeacon) + Ok(body) => match serde_json::from_str(&body) { + Ok(json) => self.scheme.verify(&self.chain_info, json) + .map_err(|_| DrandClientError::InvalidBeacon), + Err(e) => { + println!("{:?}", e); + Err(DrandClientError::InvalidBeacon) + } } }; } } #[derive(Error, Debug)] -enum SchemeError { +pub enum SchemeError { #[error("invalid beacon")] InvalidBeacon, #[error("invalid scheme")] @@ -119,21 +105,6 @@ enum SchemeError { InvalidChainInfo, } -struct ChainInfo { - scheme_id: String, - public_key: Vec, - chain_hash: Vec, - hash: Vec, - group_hash: Vec, - genesis_time: u64, - period_seconds: usize, - metadata: ChainInfoMetadata, -} - -struct ChainInfoMetadata { - beacon_id: String, -} - trait Scheme { fn supports(&self, scheme_id: &str) -> bool; fn verify(&self, info: &ChainInfo, beacon: B) -> Result; @@ -141,13 +112,52 @@ trait Scheme { #[cfg(test)] mod test { - use crate::{DrandClientError, new_chained_client}; + use crate::{DrandClientError, new_chained_client, new_unchained_client}; + use crate::DrandClientError::InvalidRound; #[test] - fn request_some_randomness() -> Result<(), DrandClientError> { - let client = new_chained_client("https://api.drand.sh"); + fn request_chained_randomness_success() -> Result<(), DrandClientError> { + let chained_url = "https://api.drand.sh"; + let client = new_chained_client(chained_url)?; let randomness = client.latest_randomness()?; assert!(randomness.round_number > 0); return Ok(()); } + + #[test] + fn request_unchained_randomness_success() -> Result<(), DrandClientError> { + let unchained_url = "https://pl-eu.testnet.drand.sh/7672797f548f3f4748ac4bf3352fc6c6b6468c9ad40ad456a397545c6e2df5bf"; + let client = new_unchained_client(unchained_url)?; + let randomness = client.latest_randomness()?; + assert!(randomness.round_number > 0); + return Ok(()); + } + + #[test] + fn request_unchained_randomness_wrong_client_error() -> Result<(), DrandClientError> { + let unchained_url = "https://pl-eu.testnet.drand.sh/7672797f548f3f4748ac4bf3352fc6c6b6468c9ad40ad456a397545c6e2df5bf"; + let client = new_chained_client(unchained_url)?; + let result = client.latest_randomness(); + assert!(result.is_err()); + return Ok(()); + } + + #[test] + fn request_chained_randomness_wrong_client_error() -> Result<(), DrandClientError> { + let chained_url = "https://api.drand.sh"; + let client = new_unchained_client(chained_url)?; + let result = client.latest_randomness(); + assert!(result.is_err()); + return Ok(()); + } + + #[test] + fn request_genesis_returns_error() -> Result<(), DrandClientError> { + let chained_url = "https://api.drand.sh"; + let client = new_chained_client(chained_url); + let result = client?.randomness(0); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), InvalidRound); + return Ok(()); + } } \ No newline at end of file diff --git a/src/unchained.rs b/src/unchained.rs new file mode 100644 index 0000000..873650c --- /dev/null +++ b/src/unchained.rs @@ -0,0 +1,47 @@ +use std::io::Write; +use crate::{bls, Scheme, SchemeError}; +use crate::chain_info::ChainInfo; +use serde::Deserialize; +use crate::bls::BlsVerifiable; + +#[derive(Deserialize, Debug, PartialEq, Clone)] +pub struct UnchainedBeacon { + #[serde(alias = "round")] + pub round_number: u64, + #[serde(with = "hex")] + pub randomness: Vec, + #[serde(with = "hex")] + pub signature: Vec, +} + +pub struct UnchainedScheme {} + +impl Scheme for UnchainedScheme { + fn supports(&self, scheme_id: &str) -> bool { + return scheme_id.eq_ignore_ascii_case("pedersen-bls-unchained"); + } + + fn verify(&self, info: &ChainInfo, beacon: UnchainedBeacon) -> Result { + if !self.supports(&info.scheme_id) { + return Err(SchemeError::InvalidScheme); + } + + return bls::bls_verify(info, beacon); + } +} + +impl BlsVerifiable for UnchainedBeacon { + fn signature(&self) -> &Vec { + &self.signature + } + + fn to_message(&self) -> Result, SchemeError> { + let mut bytes: Vec = vec![]; + + if bytes.write_all(&self.round_number.to_be_bytes()).is_err() { + return Err(SchemeError::InvalidBeacon); + }; + + return Ok(bytes); + } +} \ No newline at end of file