From 32831d33f4b6456e2b848d8593ae5d26cca317cc Mon Sep 17 00:00:00 2001 From: PM <3749956+CluEleSsUK@users.noreply.github.com> Date: Mon, 11 Sep 2023 13:14:52 +0200 Subject: [PATCH] added support RFC compliant and non-compliant g1/g2 swapped beacons (#1) - added support RFC compliant and non-compliant g1/g2 swapped beacons - replaced BLS12-381 lib with ZKcrypto's lib as it's lower level - changed the interface almost completely, so users don't need to know whether they're using a chained or unchained network - added many tests for verification - removed overly strict check of prev_sig on unchained beacon --- .gitignore | 1 + Cargo.toml | 12 +- src/bls.rs | 28 --- src/chain_info.rs | 3 +- src/chained.rs | 56 ----- src/http.rs | 28 +-- src/lib.rs | 171 ++++++------- src/unchained.rs | 51 ---- src/verify.rs | 610 ++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 712 insertions(+), 248 deletions(-) delete mode 100644 src/bls.rs delete mode 100644 src/chained.rs delete mode 100644 src/unchained.rs create mode 100644 src/verify.rs diff --git a/.gitignore b/.gitignore index 4fffb2f..c9ae557 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target /Cargo.lock +drand-client-rs.iml diff --git a/Cargo.toml b/Cargo.toml index e97206c..9e63c59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,11 +7,9 @@ edition = "2021" [dependencies] 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" +sha2 = "0.9" +bls12_381 = { version = "0.8.0", features = ["experimental"] } 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 +serde = { version = "1.0.187", features = ["derive"] } +serde_json = "1.0.105" +reqwest = { version = "0.11.20", features = ["blocking", "json"] } \ No newline at end of file diff --git a/src/bls.rs b/src/bls.rs deleted file mode 100644 index 49ef78f..0000000 --- a/src/bls.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::chain_info::ChainInfo; -use crate::SchemeError; -use bls_signatures::{hash, verify, PublicKey, Serialize, Signature}; - -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]) { - Err(SchemeError::InvalidBeacon) - } else { - Ok(beacon) - } -} diff --git a/src/chain_info.rs b/src/chain_info.rs index 4b4e572..fd9d83b 100644 --- a/src/chain_info.rs +++ b/src/chain_info.rs @@ -1,9 +1,10 @@ +use crate::verify::SchemeID; use serde::Deserialize; #[derive(Deserialize, Debug, PartialEq, Clone)] pub struct ChainInfo { #[serde(alias = "schemeID")] - pub scheme_id: String, + pub scheme_id: SchemeID, #[serde(with = "hex")] pub public_key: Vec, #[serde(with = "hex", alias = "hash")] diff --git a/src/chained.rs b/src/chained.rs deleted file mode 100644 index 30266b5..0000000 --- a/src/chained.rs +++ /dev/null @@ -1,56 +0,0 @@ -use crate::bls::BlsVerifiable; -use crate::chain_info::ChainInfo; -use crate::{bls, Scheme, SchemeError}; -use serde::Deserialize; -use std::io::Write; - -#[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, -} - -pub struct ChainedScheme {} - -impl Scheme for ChainedScheme { - fn supports(&self, scheme_id: &str) -> bool { - scheme_id.eq_ignore_ascii_case("pedersen-bls-chained") - } - - fn verify( - &self, - info: &ChainInfo, - beacon: ChainedBeacon, - ) -> Result { - if !self.supports(&info.scheme_id) { - Err(SchemeError::InvalidScheme) - } else { - bls::bls_verify(info, beacon) - } - } -} - -impl BlsVerifiable for ChainedBeacon { - fn signature(&self) -> &Vec { - &self.signature - } - - fn to_message(&self) -> Result, SchemeError> { - let mut bytes: Vec = vec![]; - - 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() { - Err(SchemeError::InvalidBeacon) - } else { - Ok(bytes) - } - } -} diff --git a/src/http.rs b/src/http.rs index f58e57e..1a66195 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,33 +1,31 @@ +use crate::{Transport, TransportError}; 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(&self, url: &str) -> Result { +impl Transport for HttpTransport { + fn fetch(&self, url: &str) -> Result { let res = self .client .get(url) .send() - .map_err(|_| HttpError::Unexpected)?; + .map_err(|_| TransportError::Unexpected)?; match res.status() { - StatusCode::OK => res.text().map_err(|_| HttpError::Unexpected), + StatusCode::OK => res.text().map_err(|_| TransportError::Unexpected), - StatusCode::NOT_FOUND => Err(HttpError::NotFound), + StatusCode::NOT_FOUND => Err(TransportError::NotFound), - _ => Err(HttpError::Unexpected), + _ => Err(TransportError::Unexpected), } } } + +pub fn new_http_transport() -> HttpTransport { + HttpTransport { + client: Client::new(), + } +} diff --git a/src/lib.rs b/src/lib.rs index 4dcdbf6..9e2f18b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,172 +1,163 @@ extern crate core; -mod bls; mod chain_info; -mod chained; mod http; -mod unchained; +mod verify; use crate::chain_info::ChainInfo; -use crate::chained::{ChainedBeacon, ChainedScheme}; -use crate::http::HttpTransport; -use crate::unchained::{UnchainedBeacon, UnchainedScheme}; +use crate::http::{new_http_transport, HttpTransport}; +use crate::verify::{verify_beacon, Beacon}; use crate::DrandClientError::{InvalidChainInfo, InvalidRound}; -use reqwest::blocking::Client; -use serde::de::DeserializeOwned; use thiserror::Error; -pub struct DrandClient<'a, B> { - scheme: &'a dyn Scheme, - transport: HttpTransport, +pub struct DrandClient<'a, T: Transport> { + transport: T, base_url: &'a str, chain_info: ChainInfo, } -pub fn new_chained_client(base_url: &str) -> Result, DrandClientError> { - return new_client(&ChainedScheme {}, base_url); -} - -pub fn new_unchained_client( - base_url: &str, -) -> Result, DrandClientError> { - return new_client(&UnchainedScheme {}, base_url); -} - -pub fn new_client<'a, S: Scheme, B>( - scheme: &'a S, - base_url: &'a str, -) -> Result, DrandClientError> { - let http_transport = HttpTransport { - client: Client::new(), - }; +pub fn new_http_client(base_url: &str) -> Result, DrandClientError> { + let http_transport = new_http_transport(); let chain_info = fetch_chain_info(&http_transport, base_url)?; - let client = DrandClient { + Ok(DrandClient { + base_url, transport: http_transport, chain_info, - scheme, - base_url, - }; - - Ok(client) + }) } -#[derive(Error, Debug, PartialEq)] -pub enum DrandClientError { - #[error("invalid round")] - InvalidRound, - #[error("invalid beacon")] - InvalidBeacon, - #[error("invalid chain info")] - InvalidChainInfo, - #[error("not responding")] - NotResponding, +pub trait Transport { + fn fetch(&self, url: &str) -> Result; } pub fn fetch_chain_info( transport: &HttpTransport, base_url: &str, ) -> Result { - let url = format!("{}/info", base_url); + let url = format!("{base_url}/info"); match transport.fetch(&url) { Err(_) => Err(DrandClientError::NotResponding), - Ok(body) => serde_json::from_str(&body).map_err(|_| InvalidChainInfo), + Ok(body) => serde_json::from_str(&body).map_err(|e| { + println!("{}", e); + InvalidChainInfo + }), } } -impl<'a, B> DrandClient<'a, B> -where - B: DeserializeOwned, -{ - pub fn latest_randomness(&self) -> Result { +impl<'a, T: Transport> DrandClient<'a, T> { + pub fn latest_randomness(&self) -> Result { self.fetch_beacon_tag("latest") } - pub fn randomness(&self, round_number: u64) -> Result { + pub fn randomness(&self, round_number: u64) -> Result { if round_number == 0 { - return Err(InvalidRound); + Err(InvalidRound) + } else { + self.fetch_beacon_tag(&format!("{round_number}")) } - self.fetch_beacon_tag(&format!("{}", round_number)) } - fn fetch_beacon_tag(&self, tag: &str) -> Result { + fn fetch_beacon_tag(&self, tag: &str) -> Result { let url = format!("{}/public/{}", self.base_url, tag); + match self.transport.fetch(&url) { Err(_) => Err(DrandClientError::NotResponding), - Ok(body) => match serde_json::from_str(&body) { - Ok(json) => self - .scheme - .verify(&self.chain_info, json) - .map_err(|_| DrandClientError::InvalidBeacon), + Ok(body) => match serde_json::from_str::(&body) { + Ok(beacon) => { + verify_beacon( + &self.chain_info.scheme_id, + &self.chain_info.public_key, + &beacon, + ) + .map_err(|_| DrandClientError::FailedVerification)?; + Ok(beacon) + } Err(_) => Err(DrandClientError::InvalidBeacon), }, } } } -#[derive(Error, Debug)] -pub enum SchemeError { +#[derive(Error, Debug, PartialEq)] +pub enum DrandClientError { + #[error("invalid round")] + InvalidRound, #[error("invalid beacon")] InvalidBeacon, - #[error("invalid scheme")] - InvalidScheme, + #[error("beacon failed verification")] + FailedVerification, #[error("invalid chain info")] InvalidChainInfo, + #[error("not responding")] + NotResponding, } -pub trait Scheme { - fn supports(&self, scheme_id: &str) -> bool; - fn verify(&self, info: &ChainInfo, beacon: B) -> Result; +#[derive(Error, Debug)] +pub enum TransportError { + #[error("not found")] + NotFound, + #[error("unexpected")] + Unexpected, } #[cfg(test)] mod test { use crate::DrandClientError::InvalidRound; - use crate::{new_chained_client, new_unchained_client, DrandClientError}; + use crate::{new_http_client, DrandClientError}; #[test] fn request_chained_randomness_success() -> Result<(), DrandClientError> { let chained_url = "https://api.drand.sh"; - let client = new_chained_client(chained_url)?; + let client = new_http_client(chained_url)?; let randomness = client.latest_randomness()?; assert!(randomness.round_number > 0); - return Ok(()); + 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 client = new_http_client(unchained_url)?; let randomness = client.latest_randomness()?; assert!(randomness.round_number > 0); - return Ok(()); + 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(); + fn request_genesis_returns_error() -> Result<(), DrandClientError> { + let chained_url = "https://api.drand.sh"; + let client = new_http_client(chained_url)?; + let result = client.randomness(0); assert!(result.is_err()); - return Ok(()); + assert_eq!(result.unwrap_err(), InvalidRound); + 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(()); + fn request_g1g2swapped_beacon_succeeds() -> Result<(), DrandClientError> { + let unchained_url = + "https://api.drand.sh/dbd506d6ef76e5f386f41c651dcb808c5bcbd75471cc4eafa3f4df7ad4e4c493"; + let client = new_http_client(unchained_url)?; + client.randomness(1)?; + 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(()); + fn request_g1g2swapped_rfc_beacon_succeeds() -> Result<(), DrandClientError> { + let unchained_url = + "https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"; + let client = new_http_client(unchained_url)?; + client.randomness(1)?; + Ok(()) + } + + #[test] + fn request_g1g2swapped_rfc_latest_succeeds() -> Result<(), DrandClientError> { + let unchained_url = + "https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"; + let client = new_http_client(unchained_url)?; + client.latest_randomness()?; + Ok(()) } } diff --git a/src/unchained.rs b/src/unchained.rs deleted file mode 100644 index 3fd5207..0000000 --- a/src/unchained.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::bls::BlsVerifiable; -use crate::chain_info::ChainInfo; -use crate::{bls, Scheme, SchemeError}; -use serde::Deserialize; -use std::io::Write; - -#[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 { - scheme_id.eq_ignore_ascii_case("pedersen-bls-unchained") - } - - fn verify( - &self, - info: &ChainInfo, - beacon: UnchainedBeacon, - ) -> Result { - if !self.supports(&info.scheme_id) { - Err(SchemeError::InvalidScheme) - } else { - 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() { - Err(SchemeError::InvalidBeacon) - } else { - Ok(bytes) - } - } -} diff --git a/src/verify.rs b/src/verify.rs new file mode 100644 index 0000000..df4dd31 --- /dev/null +++ b/src/verify.rs @@ -0,0 +1,610 @@ +use bls12_381::hash_to_curve::{ExpandMsgXmd, HashToCurve}; +use bls12_381::{ + multi_miller_loop, G1Affine, G1Projective, G2Affine, G2Prepared, G2Projective, Gt, +}; +use serde::{Deserialize, Deserializer}; +use sha2::{Digest, Sha256}; +use std::ops::Neg; +use thiserror::Error; + +#[derive(Deserialize, Debug, PartialEq, Clone)] +pub struct Beacon { + #[serde(alias = "round")] + pub round_number: u64, + #[serde(with = "hex")] + pub randomness: Vec, + #[serde(with = "hex")] + pub signature: Vec, + #[serde(default, with = "hex")] + pub previous_signature: Vec, +} + +const DST_G1: &str = "BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_"; +const DST_G2: &str = "BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_"; + +#[derive(Debug, PartialEq, Clone)] +pub enum SchemeID { + PedersenBlsChained, + PedersenBlsUnchained, + UnchainedOnG1, + UnchainedOnG1RFC9380, +} + +impl<'de> Deserialize<'de> for SchemeID { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = Deserialize::deserialize(deserializer)?; + match s { + "pedersen-bls-chained" => Ok(SchemeID::PedersenBlsChained), + "pedersen-bls-unchained" => Ok(SchemeID::PedersenBlsUnchained), + "bls-unchained-on-g1" => Ok(SchemeID::UnchainedOnG1), + "bls-unchained-g1-rfc9380" => Ok(SchemeID::UnchainedOnG1RFC9380), + _ => Err(serde::de::Error::unknown_variant( + s, + &[ + "pedersen-bls-chained", + "pedersen-bls-unchained", + "bls-unchained-on-g1", + "bls-unchained-g1-rfc9380", + ], + )), + } + } +} + +#[derive(Error, Debug, PartialEq)] +pub enum VerificationError { + #[error("chained beacons must have a `previous_signature`")] + ChainedBeaconNeedsPreviousSignature, + #[error("invalid signature length")] + InvalidSignatureLength, + #[error("invalid public key")] + InvalidPublicKey, + #[error("message can't be empty")] + EmptyMessage, + #[error("signature verification failed")] + SignatureFailedVerification, + #[error("the randomness for the beacon did not match the signature")] + InvalidRandomness, +} + +pub fn verify_beacon( + scheme_id: &SchemeID, + public_key: &[u8], + beacon: &Beacon, +) -> Result<(), VerificationError> { + if Sha256::digest(&beacon.signature).to_vec() != beacon.randomness { + return Err(VerificationError::InvalidRandomness); + } + match scheme_id { + SchemeID::PedersenBlsChained => verify_on_g2( + public_key, + &chained_beacon_message(beacon)?, + &beacon.signature, + DST_G2, + ), + SchemeID::PedersenBlsUnchained => verify_on_g2( + public_key, + &unchained_beacon_message(beacon)?, + &beacon.signature, + DST_G2, + ), + SchemeID::UnchainedOnG1 => verify_on_g1( + public_key, + &unchained_beacon_message(beacon)?, + &beacon.signature, + DST_G2, + ), + SchemeID::UnchainedOnG1RFC9380 => verify_on_g1( + public_key, + &unchained_beacon_message(beacon)?, + &beacon.signature, + DST_G1, + ), + } +} + +fn unchained_beacon_message(beacon: &Beacon) -> Result, VerificationError> { + let round_bytes = beacon.round_number.to_be_bytes(); + + Ok(Sha256::digest(&round_bytes).to_vec()) +} + +fn chained_beacon_message(beacon: &Beacon) -> Result, VerificationError> { + if beacon.previous_signature.is_empty() { + Err(VerificationError::ChainedBeaconNeedsPreviousSignature) + } else { + let message: Vec = beacon + .previous_signature + .clone() + .into_iter() + .chain(beacon.round_number.to_be_bytes()) + .collect(); + + Ok(Sha256::digest(message.as_slice()).to_vec()) + } +} + +pub fn verify_on_g2( + public_key: &[u8], + message: &[u8], + signature: &[u8], + domain_separation_tag: &str, +) -> Result<(), VerificationError> { + let pub_key_bytes: &[u8; 48] = public_key + .try_into() + .map_err(|_| VerificationError::InvalidPublicKey)?; + + let sig_bytes: &[u8; 96] = signature + .try_into() + .map_err(|_| VerificationError::InvalidSignatureLength)?; + + let p = G1Affine::from_compressed(pub_key_bytes).unwrap_or(G1Affine::identity()); + + let q = G2Affine::from_compressed(sig_bytes).unwrap_or(G2Affine::identity()); + + if p.is_on_curve().unwrap_u8() != 1 { + return Err(VerificationError::InvalidPublicKey); + } + + if p.is_identity().unwrap_u8() == 1 { + return Err(VerificationError::InvalidPublicKey); + } + + if message.is_empty() { + return Err(VerificationError::EmptyMessage); + } + + if signature.is_empty() { + return Err(VerificationError::InvalidSignatureLength); + } + + let m = >>::hash_to_curve( + message, + domain_separation_tag.as_bytes(), + ); + + let m_prepared = G2Prepared::from(G2Affine::from(m)); + let q_prepared = G2Prepared::from(q); + let exp = multi_miller_loop(&[ + (&p.neg(), &m_prepared), + (&G1Affine::generator(), &q_prepared), + ]); + + if exp.final_exponentiation() != Gt::identity() { + Err(VerificationError::SignatureFailedVerification) + } else { + Ok(()) + } +} + +pub fn verify_on_g1( + public_key: &[u8], + message: &[u8], + signature: &[u8], + domain_separation_tag: &str, +) -> Result<(), VerificationError> { + let pub_key_bytes: &[u8; 96] = public_key + .try_into() + .map_err(|_| VerificationError::InvalidPublicKey)?; + + let sig_bytes: &[u8; 48] = signature + .try_into() + .map_err(|_| VerificationError::InvalidSignatureLength)?; + + let signature_point = G1Affine::from_compressed(sig_bytes).unwrap_or(G1Affine::identity()); + let pubkey_point = G2Affine::from_compressed(pub_key_bytes).unwrap_or(G2Affine::identity()); + + if pubkey_point.is_on_curve().unwrap_u8() != 1 { + return Err(VerificationError::InvalidPublicKey); + } + + if pubkey_point.is_identity().unwrap_u8() == 1 { + return Err(VerificationError::InvalidPublicKey); + } + + if message.is_empty() { + return Err(VerificationError::EmptyMessage); + } + + if signature.is_empty() { + return Err(VerificationError::InvalidSignatureLength); + } + + let m = >>::hash_to_curve( + message, + domain_separation_tag.as_bytes(), + ); + + let pubkey_prepared = G2Prepared::from(pubkey_point.neg()); + let g2_base = G2Prepared::from(G2Affine::generator()); + let exp = multi_miller_loop(&[ + (&G1Affine::from(m), &pubkey_prepared), + (&signature_point, &g2_base), + ]); + + if exp.final_exponentiation() != Gt::identity() { + Err(VerificationError::SignatureFailedVerification) + } else { + Ok(()) + } +} + +#[cfg(test)] +mod test { + use crate::verify::{verify_beacon, Beacon, SchemeID, VerificationError}; + use bls12_381::{G1Affine, G2Affine}; + + #[test] + fn default_beacon_verifies() { + let public_key = dehexify("88a8227b75dba145599d894d33eebde3b36fef900d456ae2cc4388867adb4769c40359f783750a41b4d17e40f578bfdb"); + let prev_sig = dehexify("a2237ee39a1a6569cb8e02c6e979c07efe1f30be0ac501436bd325015f1cd6129dc56fd60efcdf9158d74ebfa34bfcbd17803dbca6d2ae8bc3a968e4dc582f8710c69de80b2e649663fef5742d22fff7d1619b75d5f222e8c9b8840bc2044bce"); + + let beacon = Beacon { + round_number: 397089, + randomness: dehexify("cd435675735e459fb4d9c68a9d9f7b719e59e0a9f5f86fe6bd86b730d01fba42"), + signature: dehexify("88ccd9a91946bc0bbef2c6c60a09bbf4a247b1d2059522449aa1a35758feddfad85efe818bbde3e1e4ab0c852d96e65f0b1f97f239bf3fc918860ea846cbb500fcf7c9d0dd3d851320374460b5fc596b8cfd629f4c07c7507c259bf9beca850a"), + previous_signature: prev_sig, + }; + + assert!(matches!( + verify_beacon(&SchemeID::PedersenBlsChained, &public_key, &beacon), + Ok(()), + )); + } + + #[test] + fn default_wrong_round_fails() { + let public_key = dehexify("88a8227b75dba145599d894d33eebde3b36fef900d456ae2cc4388867adb4769c40359f783750a41b4d17e40f578bfdb"); + let prev_sig = dehexify("a2237ee39a1a6569cb8e02c6e979c07efe1f30be0ac501436bd325015f1cd6129dc56fd60efcdf9158d74ebfa34bfcbd17803dbca6d2ae8bc3a968e4dc582f8710c69de80b2e649663fef5742d22fff7d1619b75d5f222e8c9b8840bc2044bce"); + + let beacon = Beacon { + round_number: 1, // wrong round for randomness + randomness: dehexify("cd435675735e459fb4d9c68a9d9f7b719e59e0a9f5f86fe6bd86b730d01fba42"), + signature: dehexify("88ccd9a91946bc0bbef2c6c60a09bbf4a247b1d2059522449aa1a35758feddfad85efe818bbde3e1e4ab0c852d96e65f0b1f97f239bf3fc918860ea846cbb500fcf7c9d0dd3d851320374460b5fc596b8cfd629f4c07c7507c259bf9beca850a"), + previous_signature: prev_sig, + }; + + assert_error( + verify_beacon(&SchemeID::PedersenBlsChained, &public_key, &beacon), + VerificationError::SignatureFailedVerification, + ); + } + + #[test] + fn default_with_invalid_randomness_fails() { + let public_key = dehexify("88a8227b75dba145599d894d33eebde3b36fef900d456ae2cc4388867adb4769c40359f783750a41b4d17e40f578bfdb"); + let prev_sig = dehexify("a2237ee39a1a6569cb8e02c6e979c07efe1f30be0ac501436bd325015f1cd6129dc56fd60efcdf9158d74ebfa34bfcbd17803dbca6d2ae8bc3a968e4dc582f8710c69de80b2e649663fef5742d22fff7d1619b75d5f222e8c9b8840bc2044bce"); + + let beacon = Beacon { + round_number: 397089, + // updated the randomness hex to be wrong + randomness: dehexify("bd435675735e459fb4d9c68a9d9f7b719e59e0a9f5f86fe6bd86b730d01fba42"), + signature: dehexify("88ccd9a91946bc0bbef2c6c60a09bbf4a247b1d2059522449aa1a35758feddfad85efe818bbde3e1e4ab0c852d96e65f0b1f97f239bf3fc918860ea846cbb500fcf7c9d0dd3d851320374460b5fc596b8cfd629f4c07c7507c259bf9beca850a"), + previous_signature: prev_sig, + }; + + assert_error( + verify_beacon(&SchemeID::PedersenBlsChained, &public_key, &beacon), + VerificationError::InvalidRandomness, + ); + } + + #[test] + fn default_beacon_missing_previous_sig_fails() { + let public_key = dehexify("88a8227b75dba145599d894d33eebde3b36fef900d456ae2cc4388867adb4769c40359f783750a41b4d17e40f578bfdb"); + + let beacon = Beacon { + round_number: 397089, + randomness: dehexify("cd435675735e459fb4d9c68a9d9f7b719e59e0a9f5f86fe6bd86b730d01fba42"), + signature: dehexify("88ccd9a91946bc0bbef2c6c60a09bbf4a247b1d2059522449aa1a35758feddfad85efe818bbde3e1e4ab0c852d96e65f0b1f97f239bf3fc918860ea846cbb500fcf7c9d0dd3d851320374460b5fc596b8cfd629f4c07c7507c259bf9beca850a"), + previous_signature: Vec::new(), + }; + + assert_error( + verify_beacon(&SchemeID::PedersenBlsChained, &public_key, &beacon), + VerificationError::ChainedBeaconNeedsPreviousSignature, + ); + } + + #[test] + fn default_beacon_invalid_public_key_fails() { + // public key is not correct + let public_key = dehexify("78a8227b75dba145599d894d33eebde3b36fef900d456ae2cc4388867adb4769c40359f783750a41b4d17e40f578bfdb"); + let prev_sig = dehexify("a2237ee39a1a6569cb8e02c6e979c07efe1f30be0ac501436bd325015f1cd6129dc56fd60efcdf9158d74ebfa34bfcbd17803dbca6d2ae8bc3a968e4dc582f8710c69de80b2e649663fef5742d22fff7d1619b75d5f222e8c9b8840bc2044bce"); + + let beacon = Beacon { + round_number: 397089, + randomness: dehexify("cd435675735e459fb4d9c68a9d9f7b719e59e0a9f5f86fe6bd86b730d01fba42"), + signature: dehexify("88ccd9a91946bc0bbef2c6c60a09bbf4a247b1d2059522449aa1a35758feddfad85efe818bbde3e1e4ab0c852d96e65f0b1f97f239bf3fc918860ea846cbb500fcf7c9d0dd3d851320374460b5fc596b8cfd629f4c07c7507c259bf9beca850a"), + previous_signature: prev_sig, + }; + + assert_error( + verify_beacon(&SchemeID::PedersenBlsChained, &public_key, &beacon), + VerificationError::InvalidPublicKey, + ); + } + + #[test] + fn default_beacon_empty_public_key_fails() { + let public_key = Vec::new(); + let prev_sig = dehexify("a2237ee39a1a6569cb8e02c6e979c07efe1f30be0ac501436bd325015f1cd6129dc56fd60efcdf9158d74ebfa34bfcbd17803dbca6d2ae8bc3a968e4dc582f8710c69de80b2e649663fef5742d22fff7d1619b75d5f222e8c9b8840bc2044bce"); + + let beacon = Beacon { + round_number: 397089, + randomness: dehexify("cd435675735e459fb4d9c68a9d9f7b719e59e0a9f5f86fe6bd86b730d01fba42"), + signature: dehexify("88ccd9a91946bc0bbef2c6c60a09bbf4a247b1d2059522449aa1a35758feddfad85efe818bbde3e1e4ab0c852d96e65f0b1f97f239bf3fc918860ea846cbb500fcf7c9d0dd3d851320374460b5fc596b8cfd629f4c07c7507c259bf9beca850a"), + previous_signature: prev_sig, + }; + + assert_error( + verify_beacon(&SchemeID::PedersenBlsChained, &public_key, &beacon), + VerificationError::InvalidPublicKey, + ); + } + + #[test] + fn default_beacon_infinity_public_key_fails() { + let public_key = G1Affine::identity().to_compressed(); + let prev_sig = dehexify("a2237ee39a1a6569cb8e02c6e979c07efe1f30be0ac501436bd325015f1cd6129dc56fd60efcdf9158d74ebfa34bfcbd17803dbca6d2ae8bc3a968e4dc582f8710c69de80b2e649663fef5742d22fff7d1619b75d5f222e8c9b8840bc2044bce"); + + let beacon = Beacon { + round_number: 397089, + randomness: dehexify("cd435675735e459fb4d9c68a9d9f7b719e59e0a9f5f86fe6bd86b730d01fba42"), + signature: dehexify("88ccd9a91946bc0bbef2c6c60a09bbf4a247b1d2059522449aa1a35758feddfad85efe818bbde3e1e4ab0c852d96e65f0b1f97f239bf3fc918860ea846cbb500fcf7c9d0dd3d851320374460b5fc596b8cfd629f4c07c7507c259bf9beca850a"), + previous_signature: prev_sig, + }; + + assert_error( + verify_beacon(&SchemeID::PedersenBlsChained, &public_key, &beacon), + VerificationError::InvalidPublicKey, + ); + } + + #[test] + fn testnet_unchained_beacon_verifies() { + let public_key = dehexify("8d91ae0f4e3cd277cfc46aba26680232b0d5bb4444602cdb23442d62e17f43cdffb1104909e535430c10a6a1ce680a65"); + let beacon = Beacon { + round_number: 397092, + randomness: dehexify("7731783ab8118d7484d0e8e237f3023a4c7ef4532f35016f2e56e89a7570c796"), + signature: dehexify("94da96b5b985a22a3d99fa3051a42feb4da9218763f6c836fca3770292dbf4b01f5d378859a113960548d167eaa144250a2c8e34c51c5270152ac2bc7a52632236f746545e0fae52f69068c017745204240d19dae2b4d038cef3c6047fcd6539"), + previous_signature: Vec::new(), + }; + + assert!(matches!( + verify_beacon(&SchemeID::PedersenBlsUnchained, &public_key, &beacon), + Ok(_), + )); + } + + #[test] + fn testnet_unchained_beacon_wrong_round_fails() { + let public_key = dehexify("8d91ae0f4e3cd277cfc46aba26680232b0d5bb4444602cdb23442d62e17f43cdffb1104909e535430c10a6a1ce680a65"); + let beacon = Beacon { + round_number: 1, // wrong round + randomness: dehexify("7731783ab8118d7484d0e8e237f3023a4c7ef4532f35016f2e56e89a7570c796"), + signature: dehexify("94da96b5b985a22a3d99fa3051a42feb4da9218763f6c836fca3770292dbf4b01f5d378859a113960548d167eaa144250a2c8e34c51c5270152ac2bc7a52632236f746545e0fae52f69068c017745204240d19dae2b4d038cef3c6047fcd6539"), + previous_signature: Vec::new(), + }; + + assert_error( + verify_beacon(&SchemeID::PedersenBlsUnchained, &public_key, &beacon), + VerificationError::SignatureFailedVerification, + ); + } + + #[test] + fn testnet_unchained_beacon_randomness_not_matching_signature_fails() { + let public_key = dehexify("8d91ae0f4e3cd277cfc46aba26680232b0d5bb4444602cdb23442d62e17f43cdffb1104909e535430c10a6a1ce680a65"); + let beacon = Beacon { + round_number: 397092, + // mismatching randomness + randomness: dehexify("a731783ab8118d7484d0e8e237f3023a4c7ef4532f35016f2e56e89a7570c796"), + signature: dehexify("94da96b5b985a22a3d99fa3051a42feb4da9218763f6c836fca3770292dbf4b01f5d378859a113960548d167eaa144250a2c8e34c51c5270152ac2bc7a52632236f746545e0fae52f69068c017745204240d19dae2b4d038cef3c6047fcd6539"), + previous_signature: Vec::new(), + }; + + assert_error( + verify_beacon(&SchemeID::PedersenBlsUnchained, &public_key, &beacon), + VerificationError::InvalidRandomness, + ); + } + + #[test] + fn testnet_unchained_beacon_containing_previous_sig_ignores_previous_sig() { + let public_key = dehexify("8d91ae0f4e3cd277cfc46aba26680232b0d5bb4444602cdb23442d62e17f43cdffb1104909e535430c10a6a1ce680a65"); + let beacon = Beacon { + round_number: 397092, + randomness: dehexify("7731783ab8118d7484d0e8e237f3023a4c7ef4532f35016f2e56e89a7570c796"), + signature: dehexify("94da96b5b985a22a3d99fa3051a42feb4da9218763f6c836fca3770292dbf4b01f5d378859a113960548d167eaa144250a2c8e34c51c5270152ac2bc7a52632236f746545e0fae52f69068c017745204240d19dae2b4d038cef3c6047fcd6539"), + previous_signature: dehexify("94da96b5b985a22a3d99fa3051a42feb4da9218763f6c836fca3770292dbf4b01f5d378859a113960548d167eaa144250a2c8e34c51c5270152ac2bc7a52632236f746545e0fae52f69068c017745204240d19dae2b4d038cef3c6047fcd6539"), + }; + + assert!(matches!( + verify_beacon(&SchemeID::PedersenBlsUnchained, &public_key, &beacon), + Ok(()) + )); + } + + #[test] + fn testnet_unchained_invalid_public_key_fails() { + // valid public key, but for wrong scheme + let public_key = dehexify("868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31"); + let beacon = Beacon { + round_number: 397092, + randomness: dehexify("7731783ab8118d7484d0e8e237f3023a4c7ef4532f35016f2e56e89a7570c796"), + signature: dehexify("94da96b5b985a22a3d99fa3051a42feb4da9218763f6c836fca3770292dbf4b01f5d378859a113960548d167eaa144250a2c8e34c51c5270152ac2bc7a52632236f746545e0fae52f69068c017745204240d19dae2b4d038cef3c6047fcd6539"), + previous_signature: Vec::new(), + }; + + assert_error( + verify_beacon(&SchemeID::PedersenBlsUnchained, &public_key, &beacon), + VerificationError::SignatureFailedVerification, + ); + } + + #[test] + fn testnet_unchained_beacon_empty_public_key_fails() { + let public_key = Vec::new(); + let beacon = Beacon { + round_number: 397092, + randomness: dehexify("7731783ab8118d7484d0e8e237f3023a4c7ef4532f35016f2e56e89a7570c796"), + signature: dehexify("94da96b5b985a22a3d99fa3051a42feb4da9218763f6c836fca3770292dbf4b01f5d378859a113960548d167eaa144250a2c8e34c51c5270152ac2bc7a52632236f746545e0fae52f69068c017745204240d19dae2b4d038cef3c6047fcd6539"), + previous_signature: Vec::new(), + }; + + assert_error( + verify_beacon(&SchemeID::PedersenBlsUnchained, &public_key, &beacon), + VerificationError::InvalidPublicKey, + ); + } + + #[test] + fn testnet_unchained_beacon_infinity_public_key_fails() { + let public_key = G2Affine::identity().to_uncompressed(); + let beacon = Beacon { + round_number: 397092, + randomness: dehexify("7731783ab8118d7484d0e8e237f3023a4c7ef4532f35016f2e56e89a7570c796"), + signature: dehexify("94da96b5b985a22a3d99fa3051a42feb4da9218763f6c836fca3770292dbf4b01f5d378859a113960548d167eaa144250a2c8e34c51c5270152ac2bc7a52632236f746545e0fae52f69068c017745204240d19dae2b4d038cef3c6047fcd6539"), + previous_signature: Vec::new(), + }; + + assert_error( + verify_beacon(&SchemeID::PedersenBlsUnchained, &public_key, &beacon), + VerificationError::InvalidPublicKey, + ); + } + + #[test] + fn g1g2_swap_non_rfc_beacon_verifies() { + let public_key = dehexify("a0b862a7527fee3a731bcb59280ab6abd62d5c0b6ea03dc4ddf6612fdfc9d01f01c31542541771903475eb1ec6615f8d0df0b8b6dce385811d6dcf8cbefb8759e5e616a3dfd054c928940766d9a5b9db91e3b697e5d70a975181e007f87fca5e"); + let beacon = Beacon { + round_number: 3, + randomness: dehexify("a4eb0ed6c4132da066843c3bfdce732ce5013eda86e74c136ab8ccc387b798dd"), + signature: dehexify("8176555f90d71aa49ceb37739683749491c2bab15a46094b255289ed25cf8f01cdfb1fe8bd9cd5a19eb09448a3e53186"), + previous_signature: Vec::new(), + }; + + assert!(matches!( + verify_beacon(&SchemeID::UnchainedOnG1, &public_key, &beacon), + Ok(_) + )); + } + + #[test] + fn g1g2_swap_rfc_beacon_verifies() { + let public_key = dehexify("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a"); + let beacon = Beacon { + round_number: 1000, + randomness: dehexify("fe290beca10872ef2fb164d2aa4442de4566183ec51c56ff3cd603d930e54fdd"), + signature: dehexify("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39"), + previous_signature: Vec::new(), + }; + + assert!(matches!( + verify_beacon(&SchemeID::UnchainedOnG1RFC9380, &public_key, &beacon), + Ok(_) + )); + } + + #[test] + fn g1g2_swap_empty_public_key_fails() { + let public_key = Vec::new(); + let beacon = Beacon { + round_number: 1000, + randomness: dehexify("fe290beca10872ef2fb164d2aa4442de4566183ec51c56ff3cd603d930e54fdd"), + signature: dehexify("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39"), + previous_signature: Vec::new(), + }; + + assert_error( + verify_beacon(&SchemeID::UnchainedOnG1RFC9380, &public_key, &beacon), + VerificationError::InvalidPublicKey, + ); + } + + #[test] + fn g1g2_swap_infinity_public_key_fails() { + let public_key = G2Affine::identity().to_uncompressed(); + let beacon = Beacon { + round_number: 1000, + randomness: dehexify("fe290beca10872ef2fb164d2aa4442de4566183ec51c56ff3cd603d930e54fdd"), + signature: dehexify("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39"), + previous_signature: Vec::new(), + }; + + assert_error( + verify_beacon(&SchemeID::UnchainedOnG1RFC9380, &public_key, &beacon), + VerificationError::InvalidPublicKey, + ); + } + + #[test] + fn g1g2_swap_wrong_round_fails() { + let public_key = dehexify("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a"); + let beacon = Beacon { + round_number: 1, + randomness: dehexify("fe290beca10872ef2fb164d2aa4442de4566183ec51c56ff3cd603d930e54fdd"), + signature: dehexify("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39"), + previous_signature: Vec::new(), + }; + + assert_error( + verify_beacon(&SchemeID::UnchainedOnG1RFC9380, &public_key, &beacon), + VerificationError::SignatureFailedVerification, + ); + } + + #[test] + fn g1g2_swap_invalid_randomness_fails() { + let public_key = dehexify("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a"); + let beacon = Beacon { + round_number: 1000, + // incorrect hash for the signature + randomness: dehexify("aa290beca10872ef2fb164d2aa4442de4566183ec51c56ff3cd603d930e54fdd"), + signature: dehexify("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39"), + previous_signature: Vec::new(), + }; + + assert_error( + verify_beacon(&SchemeID::UnchainedOnG1RFC9380, &public_key, &beacon), + VerificationError::InvalidRandomness, + ); + } + + #[test] + fn g1g2_swap_invalid_signature_fails() { + let public_key = dehexify("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a"); + let beacon = Beacon { + round_number: 1000, + // this is not a valid signature + signature: dehexify("a44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39"), + // but the hash matches it + randomness: dehexify("5993706587c56d4e7079d175bfa5d52295694896e68c691b93765242096c9fa7"), + previous_signature: Vec::new(), + }; + + assert_error( + verify_beacon(&SchemeID::UnchainedOnG1RFC9380, &public_key, &beacon), + VerificationError::SignatureFailedVerification, + ); + } + + fn dehexify(s: &str) -> Vec { + hex::decode(s).unwrap().to_vec() + } + + fn assert_error(actual: Result<(), VerificationError>, expected: VerificationError) { + match actual { + Ok(_) => panic!("expected error but got success"), + Err(e) => { + if e != expected { + panic!("expected {expected:?} but got {e:?}"); + } + } + } + } +}