From 9e96b1e30dd4e35b97cebdba2936646ed560c940 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Mon, 15 Jan 2024 17:43:06 +0800 Subject: [PATCH] feat: support drand quicket --- Cargo.lock | 3 + Cargo.toml | 3 + src/beacon/beacon_entries.rs | 19 +- src/beacon/drand.rs | 220 ++++++++++++---------- src/beacon/mock_beacon.rs | 18 +- src/beacon/mod.rs | 7 +- src/beacon/signatures/mod.rs | 135 +++++++++++++ src/beacon/signatures/public_key_impls.rs | 48 +++++ src/beacon/signatures/signature_impls.rs | 49 +++++ src/beacon/signatures/tests.rs | 148 +++++++++++++++ src/beacon/tests/drand.rs | 88 ++++++--- src/blocks/header.rs | 55 +++--- src/fil_cns/validation.rs | 6 +- src/networks/devnet/mod.rs | 20 +- src/networks/drand.rs | 20 +- src/networks/mainnet/mod.rs | 9 +- src/state_manager/chain_rand.rs | 4 +- src/state_manager/mod.rs | 2 +- 18 files changed, 674 insertions(+), 180 deletions(-) create mode 100644 src/beacon/signatures/mod.rs create mode 100644 src/beacon/signatures/public_key_impls.rs create mode 100644 src/beacon/signatures/signature_impls.rs create mode 100644 src/beacon/signatures/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 630aa8d2f2d2..8a340578a6df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3138,6 +3138,8 @@ dependencies = [ "bigdecimal", "blake2b_simd", "bls-signatures", + "blst", + "blstrs", "boa_engine", "boa_interner", "boa_parser", @@ -3196,6 +3198,7 @@ dependencies = [ "fvm_shared 4.0.0", "gethostname", "git-version", + "group", "hex", "http 0.2.11", "http 1.0.0", diff --git a/Cargo.toml b/Cargo.toml index 340785eef005..8ee8581c47d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,8 @@ bls-signatures = { version = "0.15", default-features = false, features = [ "multicore", "blst-portable", ] } # prevent SIGINT on CI runners by using portable assembly +blst = { version = "0.3", features = ["portable"] } +blstrs = { version = "0.7", features = ["portable"] } boa_engine = "0.17.0" boa_interner = "0.17.0" boa_parser = "0.17.0" @@ -79,6 +81,7 @@ fvm_shared3 = { package = "fvm_shared", version = "~3.6", features = ["testing", fvm_shared4 = { package = "fvm_shared", version = "~4.0.0", features = ["testing", "proofs"] } gethostname = "0.4" git-version = "0.3" +group = "0.13" hex = { version = "0.4", features = ["serde"] } http = "1.0" http0 = { package = "http", version = "0.2" } diff --git a/src/beacon/beacon_entries.rs b/src/beacon/beacon_entries.rs index 6ef0a5f8188b..6baa1f41cae3 100644 --- a/src/beacon/beacon_entries.rs +++ b/src/beacon/beacon_entries.rs @@ -10,29 +10,32 @@ use serde_tuple::{self, Deserialize_tuple, Serialize_tuple}; /// This beacon entry is stored on chain in the block header. #[cfg_attr(test, derive(derive_quickcheck_arbitrary::Arbitrary))] #[derive( - Clone, Debug, Default, Eq, PartialEq, Deserialize_tuple, Serialize_tuple, Hash, Ord, PartialOrd, + Clone, Debug, Default, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize_tuple, Deserialize_tuple, )] pub struct BeaconEntry { round: u64, #[serde(with = "serde_byte_array")] - data: Vec, + signature: Vec, } impl BeaconEntry { pub fn new(round: u64, data: Vec) -> Self { - Self { round, data } + Self { + round, + signature: data, + } } /// Returns the current round number. pub fn round(&self) -> u64 { self.round } - /// The signature of message `H(prev_round, prev_round.data, round)`. - pub fn data(&self) -> &[u8] { - &self.data + /// The signature of message `H(prev_round, prev_round.signature, round)`. + pub fn signature(&self) -> &[u8] { + &self.signature } pub fn into_parts(self) -> (u64, Vec) { - let Self { round, data } = self; - (round, data) + let Self { round, signature } = self; + (round, signature) } } diff --git a/src/beacon/drand.rs b/src/beacon/drand.rs index c5d929ef60e4..535edf62b82e 100644 --- a/src/beacon/drand.rs +++ b/src/beacon/drand.rs @@ -4,15 +4,22 @@ use std::borrow::Cow; use std::time::Duration; -use super::beacon_entries::BeaconEntry; +use super::{ + beacon_entries::BeaconEntry, + signatures::{ + verify_messages_mainnet, verify_messages_quicknet, PublicKeyOnG1, PublicKeyOnG2, + SignatureOnG1, SignatureOnG2, + }, +}; use crate::shim::clock::ChainEpoch; use crate::shim::version::NetworkVersion; use crate::utils::net::global_http_client; use ahash::HashMap; use anyhow::Context as _; use async_trait::async_trait; -use bls_signatures::{PublicKey, Serialize, Signature}; +use bls_signatures::Serialize as _; use byteorder::{BigEndian, ByteOrder}; +use itertools::Itertools; use parking_lot::RwLock; use serde::{Deserialize as SerdeDeserialize, Serialize as SerdeSerialize}; use sha2::Digest; @@ -21,29 +28,21 @@ use sha2::Digest; /// `LOTUS_IGNORE_DRAND` pub const IGNORE_DRAND_VAR: &str = "IGNORE_DRAND"; -/// Coefficients of the publicly available `Drand` keys. -/// This is shared by all participants on the `Drand` network. -#[derive(Clone, Debug, SerdeSerialize, SerdeDeserialize)] -pub struct DrandPublic { - /// Public key used to verify beacon entries. - pub coefficient: Vec, -} - -impl DrandPublic { - /// Returns the public key for the `Drand` beacon. - pub fn key(&self) -> Result { - PublicKey::from_bytes(&self.coefficient) - } -} - -/// Type of the `drand` network. In general only `mainnet` and its chain -/// information should be considered stable. -#[derive(PartialEq, Eq, Clone)] +/// Type of the `drand` network. `mainnet` is chained and `quicknet` is unchained. +/// For the details, see +#[derive(PartialEq, Eq, Copy, Clone)] pub enum DrandNetwork { Mainnet, + Quicknet, Incentinet, } +impl DrandNetwork { + pub fn is_unchained(&self) -> bool { + matches!(self, Self::Quicknet) + } +} + #[derive(Clone)] /// Configuration used when initializing a `Drand` beacon. pub struct DrandConfig<'a> { @@ -75,17 +74,10 @@ impl BeaconSchedule { parent_epoch: ChainEpoch, prev: &BeaconEntry, ) -> Result, anyhow::Error> { - let (cb_epoch, curr_beacon) = self.beacon_for_epoch(epoch)?; - let (pb_epoch, _) = self.beacon_for_epoch(parent_epoch)?; - if cb_epoch != pb_epoch { - // Fork logic, take entries from the last two rounds of the new beacon. - let round = curr_beacon.max_beacon_round_for_epoch(network_version, epoch); - let mut entries = Vec::with_capacity(2); - entries.push(curr_beacon.entry(round - 1).await?); - entries.push(curr_beacon.entry(round).await?); - return Ok(entries); - } + let (_cb_epoch, curr_beacon) = self.beacon_for_epoch(epoch)?; + let (_pb_epoch, _) = self.beacon_for_epoch(parent_epoch)?; let max_round = curr_beacon.max_beacon_round_for_epoch(network_version, epoch); + // We don't expect this to ever be the case if max_round == prev.round() { // Our chain has encountered two epochs before beacon chain has elapsed one, // return no beacon entries for this epoch. @@ -100,16 +92,22 @@ impl BeaconSchedule { prev.round() }; - let mut cur = max_round; - let mut out = Vec::new(); - while cur > prev_round { - // Push all entries from rounds elapsed since the last chain epoch. - let entry = curr_beacon.entry(cur).await?; - cur = entry.round() - 1; - out.push(entry); + // We only ever need one entry after nv22 (FIP-0063) + if network_version > NetworkVersion::V21 { + let entry = curr_beacon.entry(max_round).await?; + Ok(vec![entry]) + } else { + let mut cur = max_round; + let mut out = Vec::new(); + while cur > prev_round { + // Push all entries from rounds elapsed since the last chain epoch. + let entry = curr_beacon.entry(cur).await?; + cur = entry.round() - 1; + out.push(entry); + } + out.reverse(); + Ok(out) } - out.reverse(); - Ok(out) } pub fn beacon_for_epoch(&self, epoch: ChainEpoch) -> anyhow::Result<(ChainEpoch, &dyn Beacon)> { @@ -137,12 +135,16 @@ pub trait Beacon where Self: Send + Sync + 'static, { - /// Verify a new beacon entry against the most recent one before it. - fn verify_entry(&self, curr: &BeaconEntry, prev: &BeaconEntry) -> Result; + /// Verify beacon entries that are sorted by round. + fn verify_entries( + &self, + entries: &[BeaconEntry], + prev: &BeaconEntry, + ) -> Result; /// Returns a `BeaconEntry` given a round. It fetches the `BeaconEntry` from a `Drand` node over [`gRPC`](https://grpc.io/) /// In the future, we will cache values, and support streaming. - async fn entry(&self, round: u64) -> Result; + async fn entry(&self, round: u64) -> anyhow::Result; /// Returns the most recent beacon round for the given Filecoin chain epoch. fn max_beacon_round_for_epoch( @@ -154,8 +156,12 @@ where #[async_trait] impl Beacon for Box { - fn verify_entry(&self, curr: &BeaconEntry, prev: &BeaconEntry) -> Result { - self.as_ref().verify_entry(curr, prev) + fn verify_entries( + &self, + entries: &[BeaconEntry], + prev: &BeaconEntry, + ) -> Result { + self.as_ref().verify_entries(entries, prev) } async fn entry(&self, round: u64) -> Result { @@ -192,7 +198,7 @@ pub struct BeaconEntryJson { round: u64, randomness: String, signature: String, - previous_signature: String, + previous_signature: Option, } /// `Drand` randomness beacon that can be used to generate randomness for the @@ -200,8 +206,9 @@ pub struct BeaconEntryJson { pub struct DrandBeacon { server: &'static str, hash: String, + network: DrandNetwork, - pub_key: DrandPublic, + public_key: Vec, /// Interval between beacons, in seconds. interval: u64, drand_gen_time: u64, @@ -216,41 +223,14 @@ impl DrandBeacon { /// Construct a new `DrandBeacon`. pub fn new(genesis_ts: u64, interval: u64, config: &DrandConfig<'_>) -> Self { assert_ne!(genesis_ts, 0, "Genesis timestamp cannot be 0"); - - let chain_info = &config.chain_info; - - if cfg!(debug_assertions) && config.network_type == DrandNetwork::Mainnet { - let info_url = format!("{}/{}/info", config.server, config.chain_info.hash); - let remote_chain_info = std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new()?; - rt.block_on(async { - let client = global_http_client(); - let remote_chain_info: ChainInfo = client - .get(info_url) - .send() - .await? - .error_for_status()? - .json() - .await - .map_err(|e| anyhow::anyhow!("{e}"))?; - Ok::(remote_chain_info) - }) - }) - .join() - .expect("thread panicked") - .expect("failed to fetch remote drand chain info"); - debug_assert!(&remote_chain_info == chain_info); - } - Self { server: config.server, hash: config.chain_info.hash.to_string(), - pub_key: DrandPublic { - coefficient: hex::decode(chain_info.public_key.as_ref()) - .expect("invalid static encoding of drand hex public key"), - }, - interval: chain_info.period as u64, - drand_gen_time: chain_info.genesis_time as u64, + network: config.network_type, + public_key: hex::decode(config.chain_info.public_key.as_ref()) + .expect("invalid static encoding of drand hex public key"), + interval: config.chain_info.period as u64, + drand_gen_time: config.chain_info.genesis_time as u64, fil_round_time: interval, fil_gen_time: genesis_ts, local_cache: Default::default(), @@ -260,31 +240,68 @@ impl DrandBeacon { #[async_trait] impl Beacon for DrandBeacon { - fn verify_entry(&self, curr: &BeaconEntry, prev: &BeaconEntry) -> Result { - // TODO(forest): https://github.com/ChainSafe/forest/issues/3572 - if prev.round() == 0 { - return Ok(true); - } + fn verify_entries<'a>( + &self, + entries: &'a [BeaconEntry], + mut prev: &'a BeaconEntry, + ) -> Result { + if self.network.is_unchained() { + let mut messages = vec![]; + let mut signatures = vec![]; + let pk = PublicKeyOnG2::from_bytes(&self.public_key)?; + for entry in entries.iter() { + // Hash the messages (H(curr_round)) + let message = { + let mut round_bytes = [0; std::mem::size_of::()]; + BigEndian::write_u64(&mut round_bytes, entry.round()); + sha2::Sha256::digest(round_bytes) + }; + messages.push(message); + signatures.push(SignatureOnG1::from_bytes(entry.signature())?); + } - // Hash the messages (H(prev sig | curr_round)) - let digest = { - let mut round_bytes = [0; std::mem::size_of::()]; - BigEndian::write_u64(&mut round_bytes, curr.round()); - let mut hasher = sha2::Sha256::default(); - hasher.update(prev.data()); - hasher.update(round_bytes); - hasher.finalize() - }; - // Signature - let sig = Signature::from_bytes(curr.data())?; - let sig_match = bls_signatures::verify_messages(&sig, &[&digest], &[self.pub_key.key()?]); - - // Cache the result - let contains_curr = self.local_cache.read().contains_key(&curr.round()); - if sig_match && !contains_curr { - self.local_cache.write().insert(curr.round(), curr.clone()); + Ok(verify_messages_quicknet( + &pk, + messages + .iter() + .map(|a| a.as_slice()) + .collect_vec() + .as_slice(), + signatures.iter().collect_vec().as_slice(), + )) + } else { + let mut messages = vec![]; + let mut signatures = vec![]; + + let pk = PublicKeyOnG1::from_bytes(&self.public_key)?; + for curr in entries.iter() { + if prev.round() > 0 { + // Hash the messages (H(prev sig | curr_round)) + let message = { + let mut round_bytes = [0; std::mem::size_of::()]; + BigEndian::write_u64(&mut round_bytes, curr.round()); + let mut hasher = sha2::Sha256::default(); + hasher.update(prev.signature()); + hasher.update(round_bytes); + hasher.finalize() + }; + messages.push(message); + signatures.push(SignatureOnG2::from_bytes(curr.signature())?); + } + + prev = curr; + } + + Ok(verify_messages_mainnet( + &pk, + messages + .iter() + .map(|a| a.as_slice()) + .collect_vec() + .as_slice(), + &signatures, + )) } - Ok(sig_match) } async fn entry(&self, round: u64) -> anyhow::Result { @@ -330,6 +347,7 @@ impl Beacon for DrandBeacon { if latest_ts < self.drand_gen_time { return 1; } + let from_genesis = latest_ts - self.drand_gen_time; // we take the time from genesis divided by the periods in seconds, that // gives us the number of periods since genesis. We also add +1 because diff --git a/src/beacon/mock_beacon.rs b/src/beacon/mock_beacon.rs index f52b68039009..1b3b1c3a26e8 100644 --- a/src/beacon/mock_beacon.rs +++ b/src/beacon/mock_beacon.rs @@ -22,9 +22,21 @@ impl MockBeacon { #[async_trait] impl Beacon for MockBeacon { - fn verify_entry(&self, curr: &BeaconEntry, prev: &BeaconEntry) -> Result { - let oe = Self::entry_for_index(prev.round()); - Ok(oe.data() == curr.data()) + fn verify_entries<'a>( + &self, + entries: &'a [BeaconEntry], + mut prev: &'a BeaconEntry, + ) -> Result { + for curr in entries.iter() { + let oe = Self::entry_for_index(prev.round()); + if oe.signature() != curr.signature() { + return Ok(false); + } + + prev = curr; + } + + Ok(true) } async fn entry(&self, round: u64) -> Result { diff --git a/src/beacon/mod.rs b/src/beacon/mod.rs index d63236f34679..351e68fa4236 100644 --- a/src/beacon/mod.rs +++ b/src/beacon/mod.rs @@ -3,11 +3,12 @@ pub mod beacon_entries; mod drand; -#[cfg(test)] -pub mod mock_beacon; - +pub mod signatures; pub use beacon_entries::*; pub use drand::*; + +#[cfg(test)] +pub mod mock_beacon; #[cfg(test)] mod tests { mod drand; diff --git a/src/beacon/signatures/mod.rs b/src/beacon/signatures/mod.rs new file mode 100644 index 000000000000..e7fd9d68a1e5 --- /dev/null +++ b/src/beacon/signatures/mod.rs @@ -0,0 +1,135 @@ +// Copyright 2019-2023 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +mod public_key_impls; +mod signature_impls; + +use bls_signatures::Error; +use blstrs::{G1Affine, G1Projective, G2Affine, G2Projective}; +use group::{prime::PrimeCurveAffine, Curve}; +use rayon::prelude::*; + +// re-exports +pub use bls_signatures::{PublicKey as PublicKeyOnG1, Signature as SignatureOnG2}; + +const CSUITE_G1: &[u8] = b"BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_"; +const CSUITE_G2: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_"; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct PublicKeyOnG2(pub(crate) G2Projective); + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct SignatureOnG1(pub(crate) G1Affine); + +/// Ported from +pub fn verify_messages_quicknet( + public_key: &PublicKeyOnG2, + messages: &[&[u8]], + signatures: &[&SignatureOnG1], +) -> bool { + use blst::BLST_ERROR; + + if messages.is_empty() || signatures.is_empty() { + return false; + } + + let n_messages = messages.len(); + + if n_messages != signatures.len() { + return false; + } + + let public_key: G2Affine = public_key.as_affine(); + // zero key & single message should fail + if n_messages == 1 && public_key.is_identity().into() { + return false; + } + + // Enforce that messages are distinct as a countermeasure against BLS's rogue-key attack. + // See Section 3.1. of the IRTF's BLS signatures spec: + // https://tools.ietf.org/html/draft-irtf-cfrg-bls-signature-02#section-3.1 + if !blstrs::unique_messages(messages) { + return false; + } + + let n_workers = std::cmp::min(rayon::current_num_threads(), n_messages); + let Some(Ok(acc)) = messages + .par_iter() + .zip(signatures.par_iter()) + .chunks(n_messages / n_workers) + .map(|chunk| -> Result<_, BLST_ERROR> { + let mut pairing = blstrs::PairingG2G1::new(true, CSUITE_G1); + for (message, signature) in chunk { + pairing.aggregate(&public_key, Some(&signature.0), message, &[])?; + } + pairing.commit(); + Ok(pairing) + }) + .try_reduce_with(|mut acc, pairing| -> Result<_, BLST_ERROR> { + acc.merge(&pairing)?; + Ok(acc) + }) + else { + return false; + }; + + acc.finalverify(None) +} + +/// Ported from +pub fn verify_messages_mainnet( + public_key: &PublicKeyOnG1, + messages: &[&[u8]], + signatures: &[SignatureOnG2], +) -> bool { + use blst::BLST_ERROR; + + if messages.is_empty() || signatures.is_empty() { + return false; + } + + let n_messages = messages.len(); + + if n_messages != signatures.len() { + return false; + } + + let public_key: G1Affine = public_key.as_affine(); + // zero key & single message should fail + if n_messages == 1 && public_key.is_identity().into() { + return false; + } + + // Enforce that messages are distinct as a countermeasure against BLS's rogue-key attack. + // See Section 3.1. of the IRTF's BLS signatures spec: + // https://tools.ietf.org/html/draft-irtf-cfrg-bls-signature-02#section-3.1 + if !blstrs::unique_messages(messages) { + return false; + } + + let n_workers = std::cmp::min(rayon::current_num_threads(), n_messages); + let Some(Ok(acc)) = messages + .par_iter() + .zip(signatures.par_iter()) + .chunks(n_messages / n_workers) + .map(|chunk| -> Result<_, BLST_ERROR> { + let mut pairing = blstrs::PairingG1G2::new(true, CSUITE_G2); + for (message, signature) in chunk { + pairing.aggregate(&public_key, Some(&(*signature).into()), message, &[])?; + } + pairing.commit(); + Ok(pairing) + }) + .try_reduce_with(|mut acc, pairing| -> Result<_, BLST_ERROR> { + acc.merge(&pairing)?; + Ok(acc) + }) + else { + return false; + }; + + acc.finalverify(None) +} + +#[cfg(test)] +mod tests; diff --git a/src/beacon/signatures/public_key_impls.rs b/src/beacon/signatures/public_key_impls.rs new file mode 100644 index 000000000000..8cb6af26da08 --- /dev/null +++ b/src/beacon/signatures/public_key_impls.rs @@ -0,0 +1,48 @@ +// Copyright 2019-2023 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +use super::*; + +impl PublicKeyOnG2 { + pub fn as_affine(&self) -> G2Affine { + self.0.to_affine() + } + + pub fn verify(&self, message: impl AsRef<[u8]>, signature: &SignatureOnG1) -> bool { + // verify_messages(sig, &[message.as_ref()], &[self]) + verify_messages_quicknet(self, &[message.as_ref()], &[&signature]) + } + + pub fn verify_batch(&self, messages: &[&[u8]], signatures: &[&SignatureOnG1]) -> bool { + // verify_messages(sig, &[message.as_ref()], &[self]) + verify_messages_quicknet(self, messages, signatures) + } + + pub fn from_bytes(raw: &[u8]) -> Result { + if raw.len() != G2Affine::compressed_size() { + return Err(Error::SizeMismatch); + } + + let mut res = [0u8; G2Affine::compressed_size()]; + res.as_mut().copy_from_slice(raw); + let affine: G2Affine = + Option::from(G2Affine::from_compressed(&res)).ok_or(Error::GroupDecode)?; + + Ok(PublicKeyOnG2(affine.into())) + } + + pub fn as_bytes(&self) -> [u8; G2Affine::compressed_size()] { + self.0.to_affine().to_compressed() + } +} + +impl From for PublicKeyOnG2 { + fn from(val: G2Projective) -> Self { + PublicKeyOnG2(val) + } +} +impl From for G2Projective { + fn from(val: PublicKeyOnG2) -> Self { + val.0 + } +} diff --git a/src/beacon/signatures/signature_impls.rs b/src/beacon/signatures/signature_impls.rs new file mode 100644 index 000000000000..1fe7df819c67 --- /dev/null +++ b/src/beacon/signatures/signature_impls.rs @@ -0,0 +1,49 @@ +// Copyright 2019-2023 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +use super::*; + +impl From for SignatureOnG1 { + fn from(val: G1Projective) -> Self { + SignatureOnG1(val.into()) + } +} +impl From for G1Projective { + fn from(val: SignatureOnG1) -> Self { + val.0.into() + } +} + +impl From for SignatureOnG1 { + fn from(val: G1Affine) -> Self { + SignatureOnG1(val) + } +} + +impl From for G1Affine { + fn from(val: SignatureOnG1) -> Self { + val.0 + } +} + +fn g1_from_slice(raw: &[u8]) -> Result { + if raw.len() != G1Affine::compressed_size() { + return Err(Error::SizeMismatch); + } + + let mut res = [0u8; G1Affine::compressed_size()]; + res.copy_from_slice(raw); + + Option::from(G1Affine::from_compressed(&res)).ok_or(Error::GroupDecode) +} + +impl SignatureOnG1 { + pub fn from_bytes(raw: &[u8]) -> Result { + let g1 = g1_from_slice(raw)?; + Ok(g1.into()) + } + + pub fn as_bytes(&self) -> [u8; G1Affine::compressed_size()] { + self.0.to_compressed() + } +} diff --git a/src/beacon/signatures/tests.rs b/src/beacon/signatures/tests.rs new file mode 100644 index 000000000000..5c596ee2a970 --- /dev/null +++ b/src/beacon/signatures/tests.rs @@ -0,0 +1,148 @@ +// Copyright 2019-2023 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +use byteorder::{BigEndian, ByteOrder}; +use digest::Digest; +use itertools::Itertools; + +use super::*; + +mod quicknet { + use super::*; + + #[test] + fn test_verify_messages_quicknet_single_success() { + assert!(pk().verify(message(3), &signature_3())); + } + + #[test] + fn test_verify_messages_quicknet_single_failure() { + assert!(!pk().verify(message(2), &signature_3())); + } + + #[test] + fn test_verify_messages_quicknet_batch_success() { + let messages = vec![message(2), message(3)]; + let sig_2 = signature_2(); + let sig_3 = signature_3(); + let signatures = vec![&sig_2, &sig_3]; + assert!(pk().verify_batch( + &messages.iter().map(AsRef::as_ref).collect_vec(), + &signatures + )); + } + + #[test] + fn test_verify_messages_quicknet_batch_failure() { + let messages = vec![message(1), message(3)]; + let sig_2 = signature_2(); + let sig_3 = signature_3(); + let signatures = vec![&sig_2, &sig_3]; + assert!(!pk().verify_batch( + &messages.iter().map(AsRef::as_ref).collect_vec(), + &signatures + )); + } + + // https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/info + fn pk() -> PublicKeyOnG2 { + let pk_hex="83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a"; + PublicKeyOnG2::from_bytes(&hex::decode(pk_hex).unwrap()).unwrap() + } + + fn message(round: u64) -> impl AsRef<[u8]> { + let mut round_bytes = [0; std::mem::size_of::()]; + BigEndian::write_u64(&mut round_bytes, round); + sha2::Sha256::digest(round_bytes) + } + + // https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/2 + fn signature_2() -> SignatureOnG1 { + let sig_hex = "b6b6a585449b66eb12e875b64fcbab3799861a00e4dbf092d99e969a5eac57dd3f798acf61e705fe4f093db926626807"; + SignatureOnG1::from_bytes(&hex::decode(sig_hex).unwrap()).unwrap() + } + + // https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/3 + fn signature_3() -> SignatureOnG1 { + let sig_hex = "b3fab6df720b68cc47175f2c777e86d84187caab5770906f515ff1099cb01e4deaa027075d860823e49477b93c72bd64"; + SignatureOnG1::from_bytes(&hex::decode(sig_hex).unwrap()).unwrap() + } +} + +mod mainnet { + use super::*; + use bls_signatures::Serialize as _; + + #[test] + fn test_verify_messages_mainnet_single_success() { + assert!(verify_messages_mainnet( + &pk(), + &[message(&signature_2(), 3).as_ref()], + &[signature_3()], + )); + } + + #[test] + fn test_verify_messages_mainnet_single_failure() { + assert!(!verify_messages_mainnet( + &pk(), + &[message(&signature_2(), 2).as_ref()], + &[signature_3()], + )); + } + + #[test] + fn test_verify_messages_mainnet_batch_success() { + let messages = vec![message(&signature_2(), 3), message(&signature_3(), 4)]; + let signatures = vec![signature_3(), signature_4()]; + assert!(verify_messages_mainnet( + &pk(), + &messages.iter().map(AsRef::as_ref).collect_vec(), + &signatures, + )); + } + + #[test] + fn test_verify_messages_mainnet_batch_failure() { + let messages = vec![message(&signature_2(), 3), message(&signature_3(), 3)]; + let signatures = vec![signature_3(), signature_4()]; + assert!(!verify_messages_mainnet( + &pk(), + &messages.iter().map(AsRef::as_ref).collect_vec(), + &signatures, + )); + } + + // https://api.drand.sh/8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce/info + fn pk() -> PublicKeyOnG1 { + let pk_hex="868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31"; + PublicKeyOnG1::from_bytes(&hex::decode(pk_hex).unwrap()).unwrap() + } + + fn message(prev_signature: &SignatureOnG2, round: u64) -> impl AsRef<[u8]> { + let mut round_bytes = [0; std::mem::size_of::()]; + BigEndian::write_u64(&mut round_bytes, round); + let mut hasher = sha2::Sha256::default(); + hasher.update(prev_signature.as_bytes()); + hasher.update(round_bytes); + hasher.finalize() + } + + // https://api.drand.sh/8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce/public/2 + fn signature_2() -> SignatureOnG2 { + let sig_hex = "aa18facd2d51b616511d542de6f9af8a3b920121401dad1434ed1db4a565f10e04fad8d9b2b4e3e0094364374caafe9b10478bf75650124831509c638b5a36a7a232ec70289f8751a2adb47fc32eb70b57dc81c39d48cbcac9fec46cdfc31663"; + SignatureOnG2::from_bytes(&hex::decode(sig_hex).unwrap()).unwrap() + } + + // https://api.drand.sh/8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce/public/3 + fn signature_3() -> SignatureOnG2 { + let sig_hex = "a7b0877eaea7a0222f4c39a2c03434c34f5fe3ea47c533d24b88e5c3053b84775ccb78e984addcb55173f40428513f280cc6e0fccc3c89bb1625c7c0b477deb6faae43fc6ec036f09233bf38da16586b3042dd01a7e9ed97c8bafa343cc6071e"; + SignatureOnG2::from_bytes(&hex::decode(sig_hex).unwrap()).unwrap() + } + + // https://api.drand.sh/8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce/public/4 + fn signature_4() -> SignatureOnG2 { + let sig_hex = "b3d74f1ab9da993e3e3c01d1a395cce8834b42de57f5ad922ac63b2e715c238f986f3098d379ce6aad07b8581e4cfd6d1533835d5e2a7e299beec8851a21c8f9ca2d714d87a471641427d21838fb2ca1a406707bb0b372f74ab667f0509fa341"; + SignatureOnG2::from_bytes(&hex::decode(sig_hex).unwrap()).unwrap() + } +} diff --git a/src/beacon/tests/drand.rs b/src/beacon/tests/drand.rs index 461aeb8fd595..e197f18108ba 100644 --- a/src/beacon/tests/drand.rs +++ b/src/beacon/tests/drand.rs @@ -1,25 +1,44 @@ // Copyright 2019-2023 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -use crate::beacon::{Beacon, ChainInfo, DrandBeacon, DrandConfig}; +use crate::beacon::{Beacon, ChainInfo, DrandBeacon, DrandConfig, DrandNetwork}; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; -fn new_beacon() -> DrandBeacon { - // https://pl-us.incentinet.drand.sh/info +fn new_beacon_mainnet() -> DrandBeacon { DrandBeacon::new( 15904451751, - 25, + 30, &DrandConfig { - server: "https://pl-us.incentinet.drand.sh", - chain_info: ChainInfo { - public_key: "8d4dc143b2128e18b4cdace6e5abece8012bfeca48551a008a69a1bbc88b71d37da840d2c8b028170f0a8704c90c1617" - .into(), + server: "https://api.drand.sh", + // https://api.drand.sh/8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce/info + chain_info: ChainInfo { + public_key: Cow::Borrowed("868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31"), period: 30, - genesis_time: 1698856390, - hash: "f11df9e56edb49c6b049cd73a68214be4e879688fdd696f96f0750ad377f9be4".into(), - group_hash: "36ab1415e2967a7571f70f88cbf733eb77ef1a3ed34173ecc5e7bac924aeb17f".into(), + genesis_time: 1595431050, + hash: Cow::Borrowed("8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce"), + group_hash: Cow::Borrowed("176f93498eac9ca337150b46d21dd58673ea4e3581185f869672e59fa4cb390a"), }, - network_type: crate::beacon::DrandNetwork::Incentinet, + network_type: DrandNetwork::Mainnet, + }, + ) +} + +fn new_beacon_quicknet() -> DrandBeacon { + DrandBeacon::new( + 15904451751, + 30, + &DrandConfig { + server: "https://api.drand.sh", + // https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/info + chain_info: ChainInfo { + public_key: Cow::Borrowed("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a"), + period: 3, + genesis_time: 1692803367, + hash: Cow::Borrowed("52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"), + group_hash: Cow::Borrowed("f477d5c89f21a17c863a7f937c6a6d15859414d2be09cd448d4279af331c5d3e"), + }, + network_type: DrandNetwork::Quicknet, }, ) } @@ -29,28 +48,55 @@ pub struct BeaconEntryJson { round: u64, randomness: String, signature: String, - previous_signature: String, + previous_signature: Option, +} + +#[test] +fn construct_drand_beacon_mainnet() { + new_beacon_mainnet(); } #[test] -fn construct_drand_beacon() { - new_beacon(); +fn construct_drand_beacon_quicknet() { + new_beacon_quicknet(); +} + +#[tokio::test] +async fn ask_and_verify_mainnet_beacon_entry_success() { + let beacon = new_beacon_mainnet(); + + let e1 = beacon.entry(1).await.unwrap(); + let e2 = beacon.entry(2).await.unwrap(); + let e3 = beacon.entry(3).await.unwrap(); + assert!(beacon.verify_entries(&[e2, e3], &e1).unwrap()); +} + +#[tokio::test] +async fn ask_and_verify_mainnet_beacon_entry_fail() { + let beacon = new_beacon_mainnet(); + + let e1 = beacon.entry(1).await.unwrap(); + let e2 = beacon.entry(2).await.unwrap(); + let e3 = beacon.entry(3).await.unwrap(); + assert!(!beacon.verify_entries(&[e3, e2], &e1).unwrap()); } #[tokio::test] -async fn ask_and_verify_beacon_entry_success() { - let beacon = new_beacon(); +async fn ask_and_verify_quicknet_beacon_entry_success() { + let beacon = new_beacon_quicknet(); + let e1 = beacon.entry(1).await.unwrap(); let e2 = beacon.entry(2).await.unwrap(); let e3 = beacon.entry(3).await.unwrap(); - assert!(beacon.verify_entry(&e3, &e2).unwrap()); + assert!(beacon.verify_entries(&[e2, e3], &e1).unwrap()); } #[tokio::test] -async fn ask_and_verify_beacon_entry_fail() { - let beacon = new_beacon(); +async fn ask_and_verify_quicknet_beacon_entry_success_2() { + let beacon = new_beacon_quicknet(); + let e1 = beacon.entry(1).await.unwrap(); let e2 = beacon.entry(2).await.unwrap(); let e3 = beacon.entry(3).await.unwrap(); - assert!(!beacon.verify_entry(&e2, &e3).unwrap()); + assert!(beacon.verify_entries(&[e3, e2], &e1).unwrap()); } diff --git a/src/blocks/header.rs b/src/blocks/header.rs index c26021e5a2c7..484d05949a7d 100644 --- a/src/blocks/header.rs +++ b/src/blocks/header.rs @@ -86,40 +86,39 @@ impl RawBlockHeader { parent_epoch: ChainEpoch, prev_entry: &BeaconEntry, ) -> Result<(), Error> { - let (cb_epoch, curr_beacon) = b_schedule + let (_cb_epoch, curr_beacon) = b_schedule .beacon_for_epoch(self.epoch) .map_err(|e| Error::Validation(e.to_string()))?; - let (pb_epoch, _) = b_schedule + let (_pb_epoch, _) = b_schedule .beacon_for_epoch(parent_epoch) .map_err(|e| Error::Validation(e.to_string()))?; - if cb_epoch != pb_epoch { - // Fork logic - if self.beacon_entries.len() != 2 { - return Err(Error::Validation(format!( - "Expected two beacon entries at beacon fork, got {}", - self.beacon_entries.len() - ))); - } - - curr_beacon - .verify_entry(&self.beacon_entries[1], &self.beacon_entries[0]) - .map_err(|e| Error::Validation(e.to_string()))?; - - return Ok(()); - } - let max_round = curr_beacon.max_beacon_round_for_epoch(network_version, self.epoch); + // We don't expect to ever actually meet this condition if max_round == prev_entry.round() { if !self.beacon_entries.is_empty() { return Err(Error::Validation(format!( - "expected not to have any beacon entries in this block, got: {:?}", + "expected not to have any beacon entries in this block, got: {}", self.beacon_entries.len() ))); } return Ok(()); } + if network_version > NetworkVersion::V21 && self.beacon_entries.len() != 1 { + return Err(Error::Validation(format!( + "exactly one beacon entry expected for network version {}, got: {}", + network_version.deref(), + self.beacon_entries.len() + ))); + } + + if network_version <= NetworkVersion::V21 && prev_entry.round() == 0 { + // This basically means that the drand entry of the first non-genesis tipset isn't verified IF we are starting on Drand mainnet (the "chained" drand) + // Networks that start on drand quicknet, or other unchained randomness sources, will still verify it + return Ok(()); + } + let last = match self.beacon_entries.last() { Some(last) => last, None => { @@ -128,6 +127,7 @@ impl RawBlockHeader { )); } }; + if last.round() != max_round { return Err(Error::Validation(format!( "expected final beacon entry in block to be at round {}, got: {}", @@ -136,18 +136,13 @@ impl RawBlockHeader { ))); } - let mut prev = prev_entry; - for curr in &self.beacon_entries { - if !curr_beacon - .verify_entry(curr, prev) - .map_err(|e| Error::Validation(e.to_string()))? - { - return Err(Error::Validation(format!( - "beacon entry was invalid: curr:{curr:?}, prev: {prev:?}" - ))); - } - prev = curr; + if !curr_beacon + .verify_entries(&self.beacon_entries, prev_entry) + .map_err(|e| Error::Validation(e.to_string()))? + { + return Err(Error::Validation("beacon entry was invalid".into())); } + Ok(()) } diff --git a/src/fil_cns/validation.rs b/src/fil_cns/validation.rs index bdfb397c845a..cc83e91ffcc3 100644 --- a/src/fil_cns/validation.rs +++ b/src/fil_cns/validation.rs @@ -262,7 +262,7 @@ fn validate_winner_election( let miner_address_buf = to_vec(miner_address)?; let vrf_base = crate::state_manager::chain_rand::draw_randomness( - beacon.data(), + beacon.signature(), DomainSeparationTag::ElectionProofProduction as i64, header.epoch, &miner_address_buf, @@ -318,7 +318,7 @@ fn validate_ticket_election( let beacon_base = header.beacon_entries.last().unwrap_or(prev_beacon); let vrf_base = crate::state_manager::chain_rand::draw_randomness( - beacon_base.data(), + beacon_base.signature(), DomainSeparationTag::TicketProduction as i64, header.epoch - TICKET_RANDOMNESS_LOOKBACK, &miner_address_buf, @@ -361,7 +361,7 @@ fn verify_winning_post_proof( .last() .unwrap_or(prev_beacon_entry); let rand = crate::state_manager::chain_rand::draw_randomness( - rand_base.data(), + rand_base.signature(), DomainSeparationTag::WinningPoStChallengeSeed as i64, header.epoch, &miner_addr_buf, diff --git a/src/networks/devnet/mod.rs b/src/networks/devnet/mod.rs index c1b5d0b1499c..3cc69db2a073 100644 --- a/src/networks/devnet/mod.rs +++ b/src/networks/devnet/mod.rs @@ -4,7 +4,10 @@ use cid::Cid; use once_cell::sync::Lazy; -use super::{drand::DRAND_MAINNET, DrandPoint, Height, HeightInfo}; +use super::{ + drand::{DRAND_MAINNET, DRAND_QUICKNET}, + DrandPoint, Height, HeightInfo, +}; // https://github.com/ethereum-lists/chains/blob/6b1e3ccad1cfcaae5aa1ab917960258f0ef1a6b6/_data/chains/eip155-31415926.json pub const ETH_CHAIN_ID: u64 = 31415926; @@ -137,7 +140,14 @@ pub static HEIGHT_INFOS: Lazy<[HeightInfo; 22]> = Lazy::new(|| { ] }); -pub(super) static DRAND_SCHEDULE: [DrandPoint<'static>; 1] = [DrandPoint { - height: 0, - config: &DRAND_MAINNET, -}]; +pub(super) static DRAND_SCHEDULE: [DrandPoint<'static>; 2] = [ + DrandPoint { + height: 0, + config: &DRAND_MAINNET, + }, + DrandPoint { + // height is TDB + height: i64::MAX, + config: &DRAND_QUICKNET, + }, +]; diff --git a/src/networks/drand.rs b/src/networks/drand.rs index c4e0d73135a8..bdd134057891 100644 --- a/src/networks/drand.rs +++ b/src/networks/drand.rs @@ -7,7 +7,7 @@ use crate::beacon::{ChainInfo, DrandConfig, DrandNetwork}; pub(super) static DRAND_MAINNET: DrandConfig<'static> = DrandConfig { server: "https://api.drand.sh", - // Source json: serde_json::from_str(r#"{"public_key":"868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31","period":30,"genesis_time":1595431050,"hash":"8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce","groupHash":"176f93498eac9ca337150b46d21dd58673ea4e3581185f869672e59fa4cb390a"}"#).unwrap(), + // https://api.drand.sh/8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce/info chain_info: ChainInfo { public_key: Cow::Borrowed("868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31"), period: 30, @@ -18,6 +18,19 @@ pub(super) static DRAND_MAINNET: DrandConfig<'static> = DrandConfig { network_type: DrandNetwork::Mainnet, }; +pub(super) static DRAND_QUICKNET: DrandConfig<'static> = DrandConfig { + server: "https://api.drand.sh", + // https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/info + chain_info: ChainInfo { + public_key: Cow::Borrowed("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a"), + period: 3, + genesis_time: 1692803367, + hash: Cow::Borrowed("52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"), + group_hash: Cow::Borrowed("f477d5c89f21a17c863a7f937c6a6d15859414d2be09cd448d4279af331c5d3e"), + }, + network_type: DrandNetwork::Quicknet, +}; + pub(super) static DRAND_INCENTINET: DrandConfig<'static> = DrandConfig { // Note: This URL is no longer valid. // See and its related issues @@ -43,6 +56,11 @@ mod tests { test_drand(&DRAND_MAINNET).await } + #[tokio::test] + async fn test_drand_quicknet() { + test_drand(&DRAND_QUICKNET).await + } + #[tokio::test] #[ignore = "server url is no longer valid"] async fn test_drand_incentinet() { diff --git a/src/networks/mainnet/mod.rs b/src/networks/mainnet/mod.rs index 0b76751eaee1..99622b33c2c8 100644 --- a/src/networks/mainnet/mod.rs +++ b/src/networks/mainnet/mod.rs @@ -8,7 +8,7 @@ use once_cell::sync::Lazy; use std::str::FromStr; use super::{ - drand::{DRAND_INCENTINET, DRAND_MAINNET}, + drand::{DRAND_INCENTINET, DRAND_MAINNET, DRAND_QUICKNET}, parse_bootstrap_peers, DrandPoint, Height, HeightInfo, }; @@ -161,7 +161,7 @@ pub static HEIGHT_INFOS: Lazy<[HeightInfo; 22]> = Lazy::new(|| { ] }); -pub(super) static DRAND_SCHEDULE: [DrandPoint<'static>; 2] = [ +pub(super) static DRAND_SCHEDULE: [DrandPoint<'static>; 3] = [ DrandPoint { height: 0, config: &DRAND_INCENTINET, @@ -170,6 +170,11 @@ pub(super) static DRAND_SCHEDULE: [DrandPoint<'static>; 2] = [ height: SMOKE_HEIGHT, config: &DRAND_MAINNET, }, + DrandPoint { + // height is TDB + height: i64::MAX, + config: &DRAND_QUICKNET, + }, ]; #[cfg(test)] diff --git a/src/state_manager/chain_rand.rs b/src/state_manager/chain_rand.rs index c32bd2e8e5c5..0468bdb0332b 100644 --- a/src/state_manager/chain_rand.rs +++ b/src/state_manager/chain_rand.rs @@ -102,7 +102,7 @@ where } let beacon_entry = self.extract_beacon_entry_for_epoch(round)?; - Ok(digest(beacon_entry.data())) + Ok(digest(beacon_entry.signature())) } /// Gets 32 bytes of randomness for `ChainRand` parameterized by the @@ -115,7 +115,7 @@ where ) -> anyhow::Result<[u8; 32]> { let rand_ts: Arc = self.get_beacon_randomness_tipset(round, lookback)?; let be = self.chain_index.latest_beacon_entry(&rand_ts)?; - Ok(digest(be.data())) + Ok(digest(be.signature())) } pub fn extract_beacon_entry_for_epoch(&self, epoch: ChainEpoch) -> anyhow::Result { diff --git a/src/state_manager/mod.rs b/src/state_manager/mod.rs index a8a670e292a1..80f75b4bd0cc 100644 --- a/src/state_manager/mod.rs +++ b/src/state_manager/mod.rs @@ -1193,7 +1193,7 @@ where let addr_buf = to_vec(&addr)?; let rand = draw_randomness( - base.data(), + base.signature(), DomainSeparationTag::WinningPoStChallengeSeed as i64, epoch, &addr_buf,