diff --git a/aqua-examples/drand/services/drand/Cargo.toml b/aqua-examples/drand/services/drand/Cargo.toml new file mode 100644 index 0000000..e6a99b9 --- /dev/null +++ b/aqua-examples/drand/services/drand/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "drand" +version = "0.1.0" +authors = ["boneyard93501 <4523011+boneyard93501@users.noreply.github.com>"] +edition = "2018" +description = "drand, a Marine wasi module" +license = "Apache-2.0" + +[[bin]] +name = "drand" +path = "src/main.rs" + +[dependencies] +marine-rs-sdk = { version = "0.7.1", features = ["logger"] } +# log = "0.4.14" +# drand-verify = "0.3.0" +hex = "0.4.3" +serde = "1.0.148" +serde_json = "1.0.89" +fff = "0.3.1" +groupy = "0.4.1" +wasm-bindgen = "0.2.83" +paired = "0.22.0" +sha2 = "0.9.9" + +[dev-dependencies] +marine-rs-sdk-test = "0.8.1" +hex-literal = "0.3.4" + +[dev] +[profile.release] +opt-level = "s" diff --git a/aqua-examples/drand/services/drand/src/main.rs b/aqua-examples/drand/services/drand/src/main.rs new file mode 100644 index 0000000..99e1bbd --- /dev/null +++ b/aqua-examples/drand/services/drand/src/main.rs @@ -0,0 +1,335 @@ +/* + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// use drand_verify::{derive_randomness, g1_from_fixed, verify}; +use hex; +use marine_rs_sdk::module_manifest; +use marine_rs_sdk::{marine, MountedBinaryResult}; +use serde::{Deserialize, Serialize}; + +use std::convert::TryInto; + +mod points; +mod randomness; +mod verify; + +use points::g1_from_fixed; +use randomness::derive_randomness; +use verify::verify; + +module_manifest!(); + +pub fn main() {} + +#[marine] +#[derive(Deserialize, Serialize, Debug)] +pub struct Chain { + pub hash: String, +} + +#[marine] +#[derive(Deserialize, Serialize, Debug)] +pub struct Chains { + pub hashes: Vec, +} + +#[marine] +pub struct DResult { + pub stderr: String, + pub stdout: String, +} + +#[marine] +#[derive(Deserialize, Serialize, Debug)] +pub struct DInfo { + pub public_key: String, + pub period: u64, + pub genesis_time: u64, + pub hash: String, +} + +#[marine] +#[derive(Deserialize, Serialize, Debug)] +pub struct Randomness { + pub round: u64, + pub randomness: String, + pub signature: String, + pub previous_signature: String, +} + +#[marine] +pub fn chains(url: String, hash_chain: bool) -> DResult { + let url = format!("{}/chains", url); + let curl_cmd = vec![ + "-X".to_string(), + "GET".to_string(), + "-H".to_string(), + "Accept: application/json".to_string(), + url, + ]; + + let response = curl_request(curl_cmd); + if response.error.len() > 0 { + return DResult { + stderr: response.error.to_string(), + stdout: "".to_string(), + }; + } + match String::from_utf8(response.clone().stdout) { + Ok(r) => { + let obj: Vec = serde_json::from_str(&r).unwrap(); + if !hash_chain { + DResult { + stdout: r, + stderr: "".to_owned(), + } + } else { + DResult { + stdout: format!("{}", obj[0]), + stderr: "".to_owned(), + } + } + } + Err(e) => DResult { + stdout: "".to_owned(), + stderr: e.to_string(), + }, + } +} + +#[marine] +pub fn info(url: String, chain_hash: String, pub_key: bool) -> DResult { + let url = format!("{}/{}/info", url, chain_hash); + let curl_cmd = vec![ + "-X".to_string(), + "GET".to_string(), + "-H".to_string(), + "Accept: application/json".to_string(), + url, + ]; + + let response = curl_request(curl_cmd); + if response.error.len() > 0 { + return DResult { + stderr: response.error.to_string(), + stdout: "".to_string(), + }; + } + match String::from_utf8(response.clone().stdout) { + Ok(r) => { + let obj: DInfo = serde_json::from_str(&r).unwrap(); + if pub_key { + DResult { + stdout: obj.public_key, + stderr: "".to_owned(), + } + } else { + DResult { + stdout: r, + stderr: "".to_owned(), + } + } + } + Err(e) => DResult { + stdout: "".to_owned(), + stderr: e.to_string(), + }, + } +} + +#[marine] +pub fn randomness(url: String, chain_hash: String, round: String) -> DResult { + let mut uri: String; + if &round.to_lowercase() == "latest" { + uri = format!("{}/{}/public/latest", url, chain_hash); + } else { + let round = &round.parse::().unwrap(); + uri = format!("{}/{}/public/{}", url, chain_hash, round); + } + + let curl_cmd = vec![ + "-X".to_string(), + "GET".to_string(), + "-H".to_string(), + "Accept: application/json".to_string(), + uri, + ]; + + let response = curl_request(curl_cmd); + if response.error.len() > 0 { + return DResult { + stderr: response.error.to_string(), + stdout: "".to_string(), + }; + } + match String::from_utf8(response.clone().stdout) { + Ok(r) => DResult { + stdout: r, + stderr: "".to_owned(), + }, + Err(e) => DResult { + stdout: "".to_owned(), + stderr: e.to_string(), + }, + } +} + +#[marine] +pub fn verify_bls(pk: String, round: u64, prev_signature: String, signature: String) -> DResult { + let hex_pk: [u8; 48] = hex::decode(&pk).unwrap().as_slice().try_into().unwrap(); + let pk = g1_from_fixed(hex_pk).unwrap(); + + println!("about to match verify"); + + let hex_sig = hex::decode(signature).unwrap(); + let hex_psig = hex::decode(prev_signature).unwrap(); + + match verify(&pk, round, &hex_psig, &hex_sig) { + Err(err) => DResult { + stderr: format!("Error during verification: {}", err), + stdout: "".to_string(), + }, + Ok(valid) => { + println!("ok verify"); + if valid { + println!("Verification succeeded"); + let randomness = derive_randomness(&hex_sig); + println!("Randomness: {}", hex::encode(&randomness)); + DResult { + stdout: hex::encode(&randomness), + stderr: "".to_string(), + } + } else { + DResult { + stdout: "".to_string(), + stderr: format!("Verification failed"), + } + } + } + } +} + +#[marine] +#[link(wasm_import_module = "curl_adapter")] +extern "C" { + pub fn curl_request(cmd: Vec) -> MountedBinaryResult; +} + +#[cfg(test)] +mod tests { + // use super::*; + use super::Randomness; + use marine_rs_sdk_test::marine_test; + + const URL: &'static str = "https://api.drand.sh"; + + #[marine_test( + config_path = "/Users/bebo/localdev/examples/aqua-examples/drand/services/configs/Config.toml", + modules_dir = "/Users/bebo/localdev/examples/aqua-examples/drand/services/artifacts/" + )] + fn test_chain(drand: marine_test_env::drand::ModuleInterface) { + let res = drand.chains(URL.to_string(), false); + assert_eq!(res.stderr.len(), 0); + + let res = drand.chains(URL.to_string(), true); + assert_eq!(res.stderr.len(), 0); + } + + #[marine_test( + config_path = "/Users/bebo/localdev/examples/aqua-examples/drand/services/configs/Config.toml", + modules_dir = "/Users/bebo/localdev/examples/aqua-examples/drand/services/artifacts/" + )] + fn test_info(drand: marine_test_env::drand::ModuleInterface) { + let chain_hash = + "8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce".to_string(); + let res = drand.info(URL.to_string(), chain_hash.clone(), false); + assert_eq!(res.stderr.len(), 0); + assert!(res.stdout.len() > 0); + + let res = drand.info(URL.to_string(), chain_hash, true); + assert_eq!(res.stderr.len(), 0); + assert!(res.stdout.len() > 0); + } + + #[marine_test( + config_path = "/Users/bebo/localdev/examples/aqua-examples/drand/services/configs/Config.toml", + modules_dir = "/Users/bebo/localdev/examples/aqua-examples/drand/services/artifacts/" + )] + fn test_randomness(drand: marine_test_env::drand::ModuleInterface) { + let chain_hash = + "8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce".to_string(); + let res = drand.randomness(URL.to_string(), chain_hash.clone(), "latest".to_owned()); + assert_eq!(res.stderr.len(), 0); + assert!(res.stdout.len() > 0); + + let rand_obj: Randomness = serde_json::from_str(&res.stdout).unwrap(); + let prev_round = rand_obj.round - 1; + let res = drand.randomness( + URL.to_string(), + chain_hash.clone(), + format!("{}", prev_round), + ); + assert_eq!(res.stderr.len(), 0); + assert!(res.stdout.len() > 0); + + let prev_rand_obj: Randomness = serde_json::from_str(&res.stdout).unwrap(); + + assert_eq!(rand_obj.previous_signature, prev_rand_obj.signature); + } + + #[marine_test( + config_path = "/Users/bebo/localdev/examples/aqua-examples/drand/services/configs/Config.toml", + modules_dir = "/Users/bebo/localdev/examples/aqua-examples/drand/services/artifacts/" + )] + fn test_verify(drand: marine_test_env::drand::ModuleInterface) { + // get chain hash + let chain_hash = drand.chains(URL.to_string(), true).stdout; + println!("verify-chain hash: {:?}", chain_hash); + + // get public key for chain + let pk = drand.info(URL.to_string(), chain_hash.clone(), true).stdout; + println!("verify-pk: {:?}", chain_hash); + + // get latest randomness + let res = drand + .randomness(URL.to_string(), chain_hash.clone(), "latest".to_owned()) + .stdout; + println!("verify randomness: {:?}", res); + + let randomness: Randomness = serde_json::from_str(&res).unwrap(); + println!("verify randomness: {:?}", randomness); + + // verify randomness + let res = drand.verify_bls( + pk, + randomness.round, + randomness.previous_signature, + randomness.signature, + ); + println!("verify: {:?}", res); + } + + #[test] + fn doodle() { + use hex_literal::hex; + const PK_LEO_MAINNET: [u8; 48] = hex!("868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31"); + let pk = "868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31"; + println!("hex : {:?}", hex::decode(pk).unwrap()); + + // let h: [u8; 48] = hex!("868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31"); + println!("hex!: {:?}", PK_LEO_MAINNET); + } +} diff --git a/aqua-examples/drand/services/drand/src/points.rs b/aqua-examples/drand/services/drand/src/points.rs new file mode 100644 index 0000000..80d695c --- /dev/null +++ b/aqua-examples/drand/services/drand/src/points.rs @@ -0,0 +1,233 @@ +// https://raw.githubusercontent.com/noislabs/drand-verify/main/src/points.rs + +use std::fmt; + +use groupy::{EncodedPoint, GroupDecodingError}; +use paired::bls12_381::{G1Affine, G1Compressed, G2Affine, G2Compressed}; + +#[derive(Debug)] +pub enum InvalidPoint { + InvalidLength { expected: usize, actual: usize }, + DecodingError { msg: String }, +} + +impl From for InvalidPoint { + fn from(source: GroupDecodingError) -> Self { + InvalidPoint::DecodingError { + msg: format!("{}", source), + } + } +} + +impl fmt::Display for InvalidPoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + InvalidPoint::InvalidLength { expected, actual } => { + write!(f, "Invalid input length for point (must be in compressed format): Expected {}, actual: {}", expected, actual) + } + InvalidPoint::DecodingError { msg } => { + write!(f, "Invalid point: {}", msg) + } + } + } +} + +pub fn g1_from_variable(data: &[u8]) -> Result { + if data.len() != G1Compressed::size() { + return Err(InvalidPoint::InvalidLength { + expected: G1Compressed::size(), + actual: data.len(), + }); + } + + let mut buf = [0u8; 48]; + buf[..].clone_from_slice(data); + g1_from_fixed(buf) +} + +/// Like [`g1_from_variable`] without guaranteeing that the encoding represents a valid element. +/// Only use this when you know for sure the encoding is correct. +pub fn g1_from_variable_unchecked(data: &[u8]) -> Result { + if data.len() != G1Compressed::size() { + return Err(InvalidPoint::InvalidLength { + expected: G1Compressed::size(), + actual: data.len(), + }); + } + + let mut buf = [0u8; 48]; + buf[..].clone_from_slice(data); + g1_from_fixed_unchecked(buf) +} + +pub fn g2_from_variable(data: &[u8]) -> Result { + if data.len() != G2Compressed::size() { + return Err(InvalidPoint::InvalidLength { + expected: G2Compressed::size(), + actual: data.len(), + }); + } + + let mut buf = [0u8; 96]; + buf[..].clone_from_slice(data); + g2_from_fixed(buf) +} + +/// Like [`g2_from_variable`] without guaranteeing that the encoding represents a valid element. +/// Only use this when you know for sure the encoding is correct. +pub fn g2_from_variable_unchecked(data: &[u8]) -> Result { + if data.len() != G2Compressed::size() { + return Err(InvalidPoint::InvalidLength { + expected: G2Compressed::size(), + actual: data.len(), + }); + } + + let mut buf = [0u8; 96]; + buf[..].clone_from_slice(data); + g2_from_fixed_unchecked(buf) +} + +pub fn g1_from_fixed(data: [u8; 48]) -> Result { + // Workaround for https://github.com/filecoin-project/paired/pull/23 + let mut compressed = G1Compressed::empty(); + compressed.as_mut().copy_from_slice(&data); + Ok(compressed.into_affine()?) +} + +/// Like [`g1_from_fixed`] without guaranteeing that the encoding represents a valid element. +/// Only use this when you know for sure the encoding is correct. +pub fn g1_from_fixed_unchecked(data: [u8; 48]) -> Result { + // Workaround for https://github.com/filecoin-project/paired/pull/23 + let mut compressed = G1Compressed::empty(); + compressed.as_mut().copy_from_slice(&data); + Ok(compressed.into_affine_unchecked()?) +} + +pub fn g2_from_fixed(data: [u8; 96]) -> Result { + // Workaround for https://github.com/filecoin-project/paired/pull/23 + let mut compressed = G2Compressed::empty(); + compressed.as_mut().copy_from_slice(&data); + Ok(compressed.into_affine()?) +} + +/// Like [`g2_from_fixed`] without guaranteeing that the encoding represents a valid element. +/// Only use this when you know for sure the encoding is correct. +pub fn g2_from_fixed_unchecked(data: [u8; 96]) -> Result { + // Workaround for https://github.com/filecoin-project/paired/pull/23 + let mut compressed = G2Compressed::empty(); + compressed.as_mut().copy_from_slice(&data); + Ok(compressed.into_affine_unchecked()?) +} + +#[cfg(test)] +mod tests { + use super::*; + use hex_literal::hex; + + #[test] + fn g1_from_variable_works() { + let result = g1_from_variable(&hex::decode("868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31").unwrap()); + assert!(result.is_ok()); + + let result = g1_from_variable(&hex::decode("868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af").unwrap()); + match result.unwrap_err() { + InvalidPoint::InvalidLength { expected, actual } => { + assert_eq!(expected, 48); + assert_eq!(actual, 47); + } + err => panic!("Unexpected error: {:?}", err), + } + } + + #[test] + fn g1_from_variable_unchecked_works() { + let data = hex::decode("868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31").unwrap(); + let a = g1_from_variable_unchecked(&data).unwrap(); + let b = g1_from_variable(&data).unwrap(); + assert_eq!(a, b); + } + + #[test] + fn g2_from_variable_works() { + let result = g2_from_variable(&hex::decode("82f5d3d2de4db19d40a6980e8aa37842a0e55d1df06bd68bddc8d60002e8e959eb9cfa368b3c1b77d18f02a54fe047b80f0989315f83b12a74fd8679c4f12aae86eaf6ab5690b34f1fddd50ee3cc6f6cdf59e95526d5a5d82aaa84fa6f181e42").unwrap()); + assert!(result.is_ok()); + + let result = g2_from_variable(&hex::decode("82f5d3d2de4db19d40a6980e8aa37842a0e55d1df06bd68bddc8d60002e8e959eb9cfa368b3c1b77d18f02a54fe047b80f0989315f83b12a74fd8679c4f12aae86eaf6ab5690b34f1fddd50ee3cc6f6cdf59e95526d5a5d82aaa84fa6f181e").unwrap()); + match result.unwrap_err() { + InvalidPoint::InvalidLength { expected, actual } => { + assert_eq!(expected, 96); + assert_eq!(actual, 95); + } + err => panic!("Unexpected error: {:?}", err), + } + } + + #[test] + fn g2_from_variable_unchecked_works() { + let data = hex::decode("82f5d3d2de4db19d40a6980e8aa37842a0e55d1df06bd68bddc8d60002e8e959eb9cfa368b3c1b77d18f02a54fe047b80f0989315f83b12a74fd8679c4f12aae86eaf6ab5690b34f1fddd50ee3cc6f6cdf59e95526d5a5d82aaa84fa6f181e42").unwrap(); + let a = g2_from_variable_unchecked(&data).unwrap(); + let b = g2_from_variable(&data).unwrap(); + assert_eq!(a, b); + } + + #[test] + fn g1_from_fixed_works() { + let result = g1_from_fixed(hex_literal::hex!("868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31")); + assert!(result.is_ok()); + + let result = g1_from_fixed(hex_literal::hex!("118f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31")); + match result.unwrap_err() { + InvalidPoint::DecodingError { msg } => { + assert_eq!(msg, "encoding has unexpected compression mode"); + } + err => panic!("Unexpected error: {:?}", err), + } + + let result = g1_from_fixed(hex_literal::hex!("868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af22")); + match result.unwrap_err() { + InvalidPoint::DecodingError { msg } => { + assert_eq!(msg, "coordinate(s) do not lie on the curve"); + } + err => panic!("Unexpected error: {:?}", err), + } + } + + #[test] + fn g1_from_fixed_unchecked_works() { + let data = hex!("868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31"); + let a = g1_from_fixed_unchecked(data).unwrap(); + let b = g1_from_fixed(data).unwrap(); + assert_eq!(a, b); + } + + #[test] + fn g2_from_fixed_works() { + let result = g2_from_fixed(hex_literal::hex!("82f5d3d2de4db19d40a6980e8aa37842a0e55d1df06bd68bddc8d60002e8e959eb9cfa368b3c1b77d18f02a54fe047b80f0989315f83b12a74fd8679c4f12aae86eaf6ab5690b34f1fddd50ee3cc6f6cdf59e95526d5a5d82aaa84fa6f181e42")); + assert!(result.is_ok()); + + let result = g2_from_fixed(hex_literal::hex!("11f5d3d2de4db19d40a6980e8aa37842a0e55d1df06bd68bddc8d60002e8e959eb9cfa368b3c1b77d18f02a54fe047b80f0989315f83b12a74fd8679c4f12aae86eaf6ab5690b34f1fddd50ee3cc6f6cdf59e95526d5a5d82aaa84fa6f181e42")); + match result.unwrap_err() { + InvalidPoint::DecodingError { msg } => { + assert_eq!(msg, "encoding has unexpected compression mode"); + } + err => panic!("Unexpected error: {:?}", err), + } + + let result = g2_from_fixed(hex_literal::hex!("82f5d3d2de4db19d40a6980e8aa37842a0e55d1df06bd68bddc8d60002e8e959eb9cfa368b3c1b77d18f02a54fe047b80f0989315f83b12a74fd8679c4f12aae86eaf6ab5690b34f1fddd50ee3cc6f6cdf59e95526d5a5d82aaa84fa6f181e44")); + match result.unwrap_err() { + InvalidPoint::DecodingError { msg } => { + assert_eq!(msg, "coordinate(s) do not lie on the curve"); + } + err => panic!("Unexpected error: {:?}", err), + } + } + + #[test] + fn g2_from_fixed_unchecked_works() { + let data = hex!("82f5d3d2de4db19d40a6980e8aa37842a0e55d1df06bd68bddc8d60002e8e959eb9cfa368b3c1b77d18f02a54fe047b80f0989315f83b12a74fd8679c4f12aae86eaf6ab5690b34f1fddd50ee3cc6f6cdf59e95526d5a5d82aaa84fa6f181e42"); + let a = g2_from_fixed_unchecked(data).unwrap(); + let b = g2_from_fixed(data).unwrap(); + assert_eq!(a, b); + } +} diff --git a/aqua-examples/drand/services/drand/src/randomness.rs b/aqua-examples/drand/services/drand/src/randomness.rs new file mode 100644 index 0000000..59a6e13 --- /dev/null +++ b/aqua-examples/drand/services/drand/src/randomness.rs @@ -0,0 +1,29 @@ +use sha2::{Digest, Sha256}; + +/// Derives a 32 byte randomness from the beacon's signature +pub fn derive_randomness(signature: &[u8]) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(signature); + hasher.finalize().into() +} + +#[cfg(test)] +mod tests { + use super::*; + use hex_literal::hex; + + #[test] + fn derives_randomness_correctly() { + // curl -sS https://drand.cloudflare.com/public/72785 + let signature = hex::decode("82f5d3d2de4db19d40a6980e8aa37842a0e55d1df06bd68bddc8d60002e8e959eb9cfa368b3c1b77d18f02a54fe047b80f0989315f83b12a74fd8679c4f12aae86eaf6ab5690b34f1fddd50ee3cc6f6cdf59e95526d5a5d82aaa84fa6f181e42").unwrap(); + let expected_randomness = + hex!("8b676484b5fb1f37f9ec5c413d7d29883504e5b669f604a1ce68b3388e9ae3d9"); + assert_eq!(derive_randomness(&signature), expected_randomness); + + // curl -sS https://drand.cloudflare.com/public/1337 + let signature = hex::decode("945b08dcb30e24da281ccf14a646f0630ceec515af5c5895e18cc1b19edd65d156b71c776a369af3487f1bc6af1062500b059e01095cc0eedce91713977d7735cac675554edfa0d0481bb991ed93d333d08286192c05bf6b65d20f23a37fc7bb").unwrap(); + let expected_randomness = + hex!("2660664f8d4bc401194d80d81da20a1e79480f65b8e2d205aecbd143b5bfb0d3"); + assert_eq!(derive_randomness(&signature), expected_randomness); + } +} diff --git a/aqua-examples/drand/services/drand/src/verify.rs b/aqua-examples/drand/services/drand/src/verify.rs new file mode 100644 index 0000000..045190d --- /dev/null +++ b/aqua-examples/drand/services/drand/src/verify.rs @@ -0,0 +1,155 @@ +// from https://raw.githubusercontent.com/noislabs/drand-verify/main/src/verify.rs +use fff::Field; +use groupy::{CurveAffine, CurveProjective}; +use paired::bls12_381::{Bls12, Fq12, G1Affine, G2Affine, G2}; +use paired::{Engine, ExpandMsgXmd, HashToCurve, PairingCurveAffine}; +use sha2::{Digest, Sha256}; +use std::error::Error; +use std::fmt; + +const DOMAIN: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_"; + +use super::points::g2_from_variable; + +#[derive(Debug)] +pub enum VerificationError { + InvalidPoint { field: String, msg: String }, +} + +impl fmt::Display for VerificationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + VerificationError::InvalidPoint { field, msg } => { + write!(f, "Invalid point for field {}: {}", field, msg) + } + } + } +} + +impl Error for VerificationError {} + +// Verify checks beacon components to see if they are valid. +pub fn verify( + pk: &G1Affine, + round: u64, + previous_signature: &[u8], + signature: &[u8], +) -> Result { + let msg_on_g2 = verify_step1(round, previous_signature); + verify_step2(pk, signature, &msg_on_g2) +} + +/// First step of the verification. +/// Should not be used directly in most cases. Use [`verify`] instead. +/// +/// This API is not stable. +#[doc(hidden)] +pub fn verify_step1(round: u64, previous_signature: &[u8]) -> G2Affine { + let msg = message(round, previous_signature); + msg_to_curve(&msg) +} + +/// Second step of the verification. +/// Should not be used directly in most cases. Use [`verify`] instead. +/// +/// This API is not stable. +#[doc(hidden)] +pub fn verify_step2( + pk: &G1Affine, + signature: &[u8], + msg_on_g2: &G2Affine, +) -> Result { + let g1 = G1Affine::one(); + let sigma = match g2_from_variable(signature) { + Ok(sigma) => sigma, + Err(err) => { + return Err(VerificationError::InvalidPoint { + field: "signature".into(), + msg: err.to_string(), + }) + } + }; + Ok(fast_pairing_equality(&g1, &sigma, pk, msg_on_g2)) +} + +/// Checks if e(p, q) == e(r, s) +/// +/// See https://hackmd.io/@benjaminion/bls12-381#Final-exponentiation. +/// +/// Optimized by this trick: +/// Instead of doing e(a,b) (in G2) multiplied by e(-c,d) (in G2) +/// (which is costly is to multiply in G2 because these are very big numbers) +/// we can do FinalExponentiation(MillerLoop( [a,b], [-c,d] )) which is the same +/// in an optimized way. +fn fast_pairing_equality(p: &G1Affine, q: &G2Affine, r: &G1Affine, s: &G2Affine) -> bool { + let minus_p = { + let mut out = *p; + out.negate(); + out + }; + // "some number of (G1, G2) pairs" are the inputs of the miller loop + let pair1 = (&minus_p.prepare(), &q.prepare()); + let pair2 = (&r.prepare(), &s.prepare()); + let looped = Bls12::miller_loop([&pair1, &pair2]); + match Bls12::final_exponentiation(&looped) { + Some(value) => value == Fq12::one(), + None => false, + } +} + +fn message(current_round: u64, prev_sig: &[u8]) -> Vec { + let mut hasher = Sha256::default(); + hasher.update(prev_sig); + hasher.update(round_to_bytes(current_round)); + hasher.finalize().to_vec() +} + +/// https://github.com/drand/drand-client/blob/master/wasm/chain/verify.go#L28-L33 +#[inline] +fn round_to_bytes(round: u64) -> [u8; 8] { + round.to_be_bytes() +} + +fn msg_to_curve(msg: &[u8]) -> G2Affine { + let g = >>::hash_to_curve(msg, DOMAIN); + g.into_affine() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::points::g1_from_fixed; + use hex_literal::hex; + + /// Public key League of Entropy Mainnet (curl -sS https://drand.cloudflare.com/info) + const PK_LEO_MAINNET: [u8; 48] = hex!("868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31"); + + #[test] + fn verify_works() { + let pk = g1_from_fixed(PK_LEO_MAINNET).unwrap(); + + // curl -sS https://drand.cloudflare.com/public/72785 + let previous_signature = hex::decode("a609e19a03c2fcc559e8dae14900aaefe517cb55c840f6e69bc8e4f66c8d18e8a609685d9917efbfb0c37f058c2de88f13d297c7e19e0ab24813079efe57a182554ff054c7638153f9b26a60e7111f71a0ff63d9571704905d3ca6df0b031747").unwrap(); + let signature = hex::decode("82f5d3d2de4db19d40a6980e8aa37842a0e55d1df06bd68bddc8d60002e8e959eb9cfa368b3c1b77d18f02a54fe047b80f0989315f83b12a74fd8679c4f12aae86eaf6ab5690b34f1fddd50ee3cc6f6cdf59e95526d5a5d82aaa84fa6f181e42").unwrap(); + let round: u64 = 72785; + + // good + let result = verify(&pk, round, &previous_signature, &signature).unwrap(); + assert!(result); + + // wrong round + let result = verify(&pk, 321, &previous_signature, &signature).unwrap(); + assert!(!result); + + // wrong previous signature + let previous_signature_corrupted = hex::decode("6a09e19a03c2fcc559e8dae14900aaefe517cb55c840f6e69bc8e4f66c8d18e8a609685d9917efbfb0c37f058c2de88f13d297c7e19e0ab24813079efe57a182554ff054c7638153f9b26a60e7111f71a0ff63d9571704905d3ca6df0b031747").unwrap(); + let result = verify(&pk, round, &previous_signature_corrupted, &signature).unwrap(); + assert!(!result); + + // wrong signature + // (use signature from https://drand.cloudflare.com/public/1 to get a valid curve point) + let wrong_signature = hex::decode("8d61d9100567de44682506aea1a7a6fa6e5491cd27a0a0ed349ef6910ac5ac20ff7bc3e09d7c046566c9f7f3c6f3b10104990e7cb424998203d8f7de586fb7fa5f60045417a432684f85093b06ca91c769f0e7ca19268375e659c2a2352b4655").unwrap(); + let result = verify(&pk, round, &previous_signature, &wrong_signature).unwrap(); + assert!(!result); + } +}