diff --git a/Cargo.lock b/Cargo.lock index f59f544b13..ad3a38c493 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5526,6 +5526,7 @@ dependencies = [ "hotshot-utils", "jf-vid", "lru 0.12.5", + "primitive-types", "rand 0.8.5", "serde", "sha2 0.10.8", @@ -5609,7 +5610,9 @@ dependencies = [ "either", "futures", "hotshot-utils", + "jf-crhf 0.1.0", "jf-pcs", + "jf-rescue", "jf-signature 0.2.0", "jf-utils", "jf-vid", @@ -10273,6 +10276,7 @@ dependencies = [ "futures", "hotshot", "hotshot-contract-adapter", + "hotshot-example-types", "hotshot-types", "log-panics", "portpicker", diff --git a/builder/src/lib.rs b/builder/src/lib.rs index 627fcfccba..3081f4bf1c 100755 --- a/builder/src/lib.rs +++ b/builder/src/lib.rs @@ -80,7 +80,6 @@ pub mod testing { }, HotShotConfig, PeerConfig, ValidatorConfig, }; - use jf_signature::bls_over_bn254::VerKey; use sequencer::{context::Consensus, network, SequencerApiVersion}; use surf_disco::Client; use vbs::version::StaticVersion; @@ -90,7 +89,7 @@ pub mod testing { #[derive(Clone)] pub struct HotShotTestConfig { - pub config: HotShotConfig, + pub config: HotShotConfig, priv_keys_staking_nodes: Vec, priv_keys_non_staking_nodes: Vec, staking_nodes_state_key_pairs: Vec, @@ -115,7 +114,7 @@ pub mod testing { let builder_url = hotshot_builder_url(); - let config: HotShotConfig = HotShotConfig { + let config: HotShotConfig = HotShotConfig { num_nodes_with_stake: NonZeroUsize::new(num_nodes_with_stake).unwrap(), known_da_nodes: known_nodes_with_stake.clone(), known_nodes_with_stake: known_nodes_with_stake.clone(), @@ -157,7 +156,11 @@ pub mod testing { pub fn generate_stake_table_entries( num_nodes: u64, stake_value: u64, - ) -> (Vec, Vec, Vec>) { + ) -> ( + Vec, + Vec, + Vec>, + ) { // Generate keys for the nodes. let priv_keys = (0..num_nodes) .map(|_| PrivKey::generate(&mut rand::thread_rng())) @@ -173,7 +176,7 @@ pub mod testing { let nodes_with_stake = pub_keys .iter() .zip(&state_key_pairs) - .map(|(pub_key, state_key_pair)| PeerConfig:: { + .map(|(pub_key, state_key_pair)| PeerConfig:: { stake_table_entry: pub_key.stake_table_entry(stake_value), state_ver_key: state_key_pair.ver_key(), }) @@ -201,11 +204,7 @@ pub mod testing { pub fn get_anvil(&self) -> Arc { self.anvil.clone() } - pub fn get_validator_config( - &self, - i: usize, - is_staked: bool, - ) -> ValidatorConfig { + pub fn get_validator_config(&self, i: usize, is_staked: bool) -> ValidatorConfig { if is_staked { ValidatorConfig { public_key: self.config.known_nodes_with_stake[i] @@ -216,7 +215,8 @@ pub mod testing { .stake_table_entry .stake_amount .as_u64(), - state_key_pair: self.staking_nodes_state_key_pairs[i].clone(), + state_public_key: self.staking_nodes_state_key_pairs[i].ver_key(), + state_private_key: self.staking_nodes_state_key_pairs[i].sign_key(), is_da: true, } } else { @@ -226,7 +226,8 @@ pub mod testing { .stake_key, private_key: self.priv_keys_non_staking_nodes[i].clone(), stake_value: 0, - state_key_pair: self.non_staking_nodes_state_key_pairs[i].clone(), + state_public_key: self.non_staking_nodes_state_key_pairs[i].ver_key(), + state_private_key: self.non_staking_nodes_state_key_pairs[i].sign_key(), is_da: true, } } @@ -267,7 +268,7 @@ pub mod testing { // enable hotshot event streaming pub fn enable_hotshot_node_event_streaming( hotshot_events_api_url: Url, - known_nodes_with_stake: Vec>, + known_nodes_with_stake: Vec>, num_non_staking_nodes: usize, hotshot_context_handle: Arc>, ) { diff --git a/contracts/rust/adapter/src/light_client.rs b/contracts/rust/adapter/src/light_client.rs index 4bca08f08a..5bb7a8a4db 100644 --- a/contracts/rust/adapter/src/light_client.rs +++ b/contracts/rust/adapter/src/light_client.rs @@ -54,8 +54,8 @@ impl From for ParsedLi impl From for GenericLightClientState { fn from(v: ParsedLightClientState) -> Self { Self { - view_number: v.view_num as usize, - block_height: v.block_height as usize, + view_number: v.view_num, + block_height: v.block_height, block_comm_root: u256_to_field(v.block_comm_root), } } @@ -64,8 +64,8 @@ impl From for GenericLightClientState impl From> for ParsedLightClientState { fn from(v: GenericLightClientState) -> Self { Self { - view_num: v.view_number as u64, - block_height: v.block_height as u64, + view_num: v.view_number, + block_height: v.block_height, block_comm_root: field_to_u256(v.block_comm_root), } } diff --git a/contracts/rust/adapter/src/stake_table.rs b/contracts/rust/adapter/src/stake_table.rs index 5e853ad24f..be22a63aea 100644 --- a/contracts/rust/adapter/src/stake_table.rs +++ b/contracts/rust/adapter/src/stake_table.rs @@ -26,7 +26,7 @@ use hotshot_types::{ network::PeerConfigKeys, signature_key::BLSPubKey, stake_table::StakeTableEntry, - traits::signature_key::SignatureKey as _, + traits::{node_implementation::NodeType, signature_key::SignatureKey as _}, PeerConfig, }; use serde::{Deserialize, Serialize}; @@ -219,7 +219,10 @@ impl From for StakeTableEntry { } } -impl From for PeerConfig { +impl From for PeerConfig +where + TYPES: NodeType, +{ fn from(value: NodeInfoJf) -> Self { Self { stake_table_entry: StakeTableEntry { @@ -291,8 +294,11 @@ impl From for NodeInfoJf { } } -impl From> for NodeInfoJf { - fn from(value: PeerConfigKeys) -> Self { +impl From> for NodeInfoJf +where + TYPES: NodeType, +{ + fn from(value: PeerConfigKeys) -> Self { let PeerConfigKeys { stake_table_key, state_ver_key, diff --git a/hotshot-builder-core/src/testing/basic_test.rs b/hotshot-builder-core/src/testing/basic_test.rs index bd6cbb5635..d4ae0be390 100644 --- a/hotshot-builder-core/src/testing/basic_test.rs +++ b/hotshot-builder-core/src/testing/basic_test.rs @@ -30,7 +30,7 @@ mod tests { }; use hotshot_types::{ data::{vid_commitment, DaProposal2, Leaf2, QuorumProposal2, QuorumProposalWrapper}, - signature_key::BuilderKey, + signature_key::{BuilderKey, SchnorrPubKey}, simple_vote::QuorumData2, traits::{block_contents::BlockHeader, node_implementation::Versions, EncodeBytes}, utils::BuilderCommitment, @@ -89,6 +89,7 @@ mod tests { type Membership = StaticCommittee; type BuilderSignatureKey = BuilderKey; type AuctionResult = TestAuctionResult; + type StateSignatureKey = SchnorrPubKey; } // no of test messages to send let num_test_messages = 5; diff --git a/hotshot-events-service/src/events_source.rs b/hotshot-events-service/src/events_source.rs index 22e6793384..3753561fde 100644 --- a/hotshot-events-service/src/events_source.rs +++ b/hotshot-events-service/src/events_source.rs @@ -26,8 +26,9 @@ where } #[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(bound = "Types::SignatureKey: for<'a> Deserialize<'a>")] pub struct StartupInfo { - pub known_node_with_stake: Vec>, + pub known_node_with_stake: Vec>, pub non_staked_node_count: usize, } @@ -46,12 +47,12 @@ pub struct EventsStreamer { subscriber_send_channel: BroadcastSender>>, // required for sending startup info - known_nodes_with_stake: Vec>, + known_nodes_with_stake: Vec>, non_staked_node_count: usize, } impl EventsStreamer { - pub fn known_node_with_stake(&self) -> Vec> { + pub fn known_node_with_stake(&self) -> Vec> { self.known_nodes_with_stake.clone() } @@ -155,7 +156,7 @@ impl EventsSource for EventsStreamer { impl EventsStreamer { pub fn new( - known_nodes_with_stake: Vec>, + known_nodes_with_stake: Vec>, non_staked_node_count: usize, ) -> Self { let (mut subscriber_send_channel, to_subscribe_clone_recv) = diff --git a/hotshot-events-service/src/test.rs b/hotshot-events-service/src/test.rs index be5dde136b..22918dd7ff 100644 --- a/hotshot-events-service/src/test.rs +++ b/hotshot-events-service/src/test.rs @@ -96,7 +96,7 @@ mod tests { let pub_key = BLSPubKey::from_private(&private_key); let state_key_pair = StateKeyPair::generate(); - let peer_config = PeerConfig:: { + let peer_config = PeerConfig:: { stake_table_entry: pub_key.stake_table_entry(1), state_ver_key: state_key_pair.ver_key(), }; diff --git a/hotshot-example-types/src/block_types.rs b/hotshot-example-types/src/block_types.rs index c96e1c7ce3..785560ca2c 100644 --- a/hotshot-example-types/src/block_types.rs +++ b/hotshot-example-types/src/block_types.rs @@ -14,9 +14,10 @@ use async_trait::async_trait; use committable::{Commitment, Committable, RawCommitmentBuilder}; use hotshot_types::{ data::{BlockError, Leaf2, VidCommitment}, + light_client::LightClientState, traits::{ block_contents::{BlockHeader, BuilderFee, EncodeBytes, TestableBlock, Transaction}, - node_implementation::NodeType, + node_implementation::{ConsensusTime, NodeType}, BlockPayload, ValidatedState, }, utils::BuilderCommitment, @@ -403,6 +404,14 @@ impl< fn get_auction_results(&self) -> Option { Some(TYPES::AuctionResult { urls: vec![] }) } + + fn get_light_client_state(&self, view: TYPES::View) -> anyhow::Result { + LightClientState::new( + view.u64(), + self.block_number, + self.payload_commitment.as_ref(), + ) + } } impl Committable for TestBlockHeader { diff --git a/hotshot-example-types/src/node_types.rs b/hotshot-example-types/src/node_types.rs index 0c8dc1750b..07526c6369 100644 --- a/hotshot-example-types/src/node_types.rs +++ b/hotshot-example-types/src/node_types.rs @@ -23,7 +23,7 @@ use hotshot::traits::{ use hotshot_types::{ constants::TEST_UPGRADE_CONSTANTS, data::{EpochNumber, ViewNumber}, - signature_key::{BLSPubKey, BuilderKey}, + signature_key::{BLSPubKey, BuilderKey, SchnorrPubKey}, traits::node_implementation::{NodeType, Versions}, upgrade_config::UpgradeConstants, }; @@ -67,6 +67,7 @@ impl NodeType for TestTypes { type InstanceState = TestInstanceState; type Membership = StaticCommittee; type BuilderSignatureKey = BuilderKey; + type StateSignatureKey = SchnorrPubKey; } #[derive( @@ -99,6 +100,7 @@ impl NodeType for TestTypesRandomizedLeader { type InstanceState = TestInstanceState; type Membership = Committee; type BuilderSignatureKey = BuilderKey; + type StateSignatureKey = SchnorrPubKey; } #[derive( @@ -129,6 +131,7 @@ impl NodeType for TestTypesEpochCatchupTypes { type InstanceState = TestInstanceState; type Membership = DummyCatchupCommittee; type BuilderSignatureKey = BuilderKey; + type StateSignatureKey = SchnorrPubKey; } #[derive( @@ -165,6 +168,7 @@ impl NodeType for TestTypesRandomizedCommitteeMember type Membership = RandomizedCommitteeMembers, CONFIG>; type BuilderSignatureKey = BuilderKey; + type StateSignatureKey = SchnorrPubKey; } #[derive( @@ -197,6 +201,7 @@ impl NodeType for TestConsecutiveLeaderTypes { type InstanceState = TestInstanceState; type Membership = StaticCommitteeLeaderForTwoViews; type BuilderSignatureKey = BuilderKey; + type StateSignatureKey = SchnorrPubKey; } #[derive( @@ -229,6 +234,7 @@ impl NodeType for TestTwoStakeTablesTypes { type InstanceState = TestInstanceState; type Membership = TwoStaticCommittees; type BuilderSignatureKey = BuilderKey; + type StateSignatureKey = SchnorrPubKey; } /// The Push CDN implementation diff --git a/hotshot-example-types/src/storage_types.rs b/hotshot-example-types/src/storage_types.rs index 26156b53d5..18cf48c92c 100644 --- a/hotshot-example-types/src/storage_types.rs +++ b/hotshot-example-types/src/storage_types.rs @@ -22,7 +22,10 @@ use hotshot_types::{ drb::DrbResult, event::HotShotAction, message::{convert_proposal, Proposal}, - simple_certificate::{NextEpochQuorumCertificate2, QuorumCertificate2, UpgradeCertificate}, + simple_certificate::{ + LightClientStateUpdateCertificate, NextEpochQuorumCertificate2, QuorumCertificate2, + UpgradeCertificate, + }, traits::{ node_implementation::{ConsensusTime, NodeType}, storage::Storage, @@ -59,6 +62,7 @@ pub struct TestStorageState { epoch: Option, drb_results: BTreeMap, epoch_roots: BTreeMap, + state_cert: Option>, } impl Default for TestStorageState { @@ -78,6 +82,7 @@ impl Default for TestStorageState { epoch: None, drb_results: BTreeMap::new(), epoch_roots: BTreeMap::new(), + state_cert: None, } } } @@ -141,6 +146,10 @@ impl TestStorage { pub async fn vids_cloned(&self) -> VidShares2 { self.inner.read().await.vid2.clone() } + + pub async fn state_cert_cloned(&self) -> Option> { + self.inner.read().await.state_cert.clone() + } } #[async_trait] @@ -313,6 +322,25 @@ impl Storage for TestStorage { Ok(()) } + async fn update_state_cert( + &self, + state_cert: hotshot_types::simple_certificate::LightClientStateUpdateCertificate, + ) -> Result<()> { + if self.should_return_err { + bail!("Failed to update state_cert to storage"); + } + Self::run_delay_settings_from_config(&self.delay_config).await; + let mut inner = self.inner.write().await; + if let Some(ref current_state_cert) = inner.state_cert { + if state_cert.epoch > current_state_cert.epoch { + inner.state_cert = Some(state_cert); + } + } else { + inner.state_cert = Some(state_cert); + } + Ok(()) + } + async fn update_next_epoch_high_qc2( &self, new_next_epoch_high_qc: hotshot_types::simple_certificate::NextEpochQuorumCertificate2< diff --git a/hotshot-examples/infra/mod.rs b/hotshot-examples/infra/mod.rs index 01e9d6223f..79a45d59ed 100755 --- a/hotshot-examples/infra/mod.rs +++ b/hotshot-examples/infra/mod.rs @@ -77,7 +77,7 @@ pub struct OrchestratorArgs { /// The url the orchestrator runs on; this should be in the form of `http://localhost:5555` or `http://0.0.0.0:5555` pub url: Url, /// The configuration file to be used for this run - pub config: NetworkConfig, + pub config: NetworkConfig, } #[derive(Parser, Debug, Clone)] @@ -103,8 +103,7 @@ impl Default for ConfigArgs { /// # Panics /// If unable to read the config file from the command line #[allow(clippy::too_many_lines)] -pub fn read_orchestrator_init_config() -> (NetworkConfig, Url) -{ +pub fn read_orchestrator_init_config() -> (NetworkConfig, Url) { // assign default setting let mut orchestrator_url = Url::parse("http://localhost:4444").unwrap(); let mut args = ConfigArgs::default(); @@ -208,8 +207,7 @@ pub fn read_orchestrator_init_config() -> (NetworkConfig = - load_config_from_file::(&args.config_file); + let mut config: NetworkConfig = load_config_from_file::(&args.config_file); if let Some(total_nodes_string) = matches.get_one::("total_nodes") { config.config.num_nodes_with_stake = total_nodes_string.parse::().unwrap(); @@ -263,16 +261,14 @@ pub fn read_orchestrator_init_config() -> (NetworkConfig( - config_file: &str, -) -> NetworkConfig { +pub fn load_config_from_file(config_file: &str) -> NetworkConfig { let config_file_as_string: String = fs::read_to_string(config_file) .unwrap_or_else(|_| panic!("Could not read config file located at {config_file}")); - let config_toml: NetworkConfigFile = - toml::from_str::>(&config_file_as_string) + let config_toml: NetworkConfigFile = + toml::from_str::>(&config_file_as_string) .expect("Unable to convert config file to TOML"); - let mut config: NetworkConfig = config_toml.into(); + let mut config: NetworkConfig = config_toml.into(); // initialize it with size for better assignment of peers' config config.config.known_nodes_with_stake = @@ -286,7 +282,7 @@ pub async fn run_orchestrator( OrchestratorArgs { url, config }: OrchestratorArgs, ) { println!("Starting orchestrator",); - let _ = hotshot_orchestrator::run_orchestrator::(config, url).await; + let _ = hotshot_orchestrator::run_orchestrator::(config, url).await; } /// Helper function to calculate the number of transactions to send per node per round @@ -352,8 +348,8 @@ pub trait RunDa< { /// Initializes networking, returns self async fn initialize_networking( - config: NetworkConfig, - validator_config: ValidatorConfig, + config: NetworkConfig, + validator_config: ValidatorConfig, libp2p_advertise_address: Option, membership: &Arc::Membership>>, ) -> Self; @@ -381,6 +377,7 @@ pub trait RunDa< // Get KeyPair for certificate Aggregation let pk = validator_config.public_key.clone(); let sk = validator_config.private_key.clone(); + let state_sk = validator_config.state_private_key.clone(); let network = self.network(); @@ -394,6 +391,7 @@ pub trait RunDa< SystemContext::init( pk, sk, + state_sk, config.node_index, config.config, EpochMembershipCoordinator::new(membership, epoch_height), @@ -584,10 +582,10 @@ pub trait RunDa< fn network(&self) -> NETWORK; /// Returns the config for this run - fn config(&self) -> NetworkConfig; + fn config(&self) -> NetworkConfig; /// Returns the validator config with private signature keys for this run. - fn validator_config(&self) -> ValidatorConfig; + fn validator_config(&self) -> ValidatorConfig; } // Push CDN @@ -595,9 +593,9 @@ pub trait RunDa< /// Represents a Push CDN-based run pub struct PushCdnDaRun { /// The underlying configuration - config: NetworkConfig, + config: NetworkConfig, /// The private validator config - validator_config: ValidatorConfig, + validator_config: ValidatorConfig, /// The underlying network network: PushCdnNetwork, } @@ -625,8 +623,8 @@ where Self: Sync, { async fn initialize_networking( - config: NetworkConfig, - validator_config: ValidatorConfig, + config: NetworkConfig, + validator_config: ValidatorConfig, _libp2p_advertise_address: Option, _membership: &Arc::Membership>>, ) -> PushCdnDaRun { @@ -668,11 +666,11 @@ where self.network.clone() } - fn config(&self) -> NetworkConfig { + fn config(&self) -> NetworkConfig { self.config.clone() } - fn validator_config(&self) -> ValidatorConfig { + fn validator_config(&self) -> ValidatorConfig { self.validator_config.clone() } } @@ -682,9 +680,9 @@ where /// Represents a libp2p-based run pub struct Libp2pDaRun { /// The underlying network configuration - config: NetworkConfig, + config: NetworkConfig, /// The private validator config - validator_config: ValidatorConfig, + validator_config: ValidatorConfig, /// The underlying network network: Libp2pNetwork, } @@ -712,8 +710,8 @@ where Self: Sync, { async fn initialize_networking( - config: NetworkConfig, - validator_config: ValidatorConfig, + config: NetworkConfig, + validator_config: ValidatorConfig, libp2p_advertise_address: Option, membership: &Arc::Membership>>, ) -> Libp2pDaRun { @@ -777,11 +775,11 @@ where self.network.clone() } - fn config(&self) -> NetworkConfig { + fn config(&self) -> NetworkConfig { self.config.clone() } - fn validator_config(&self) -> ValidatorConfig { + fn validator_config(&self) -> ValidatorConfig { self.validator_config.clone() } } @@ -791,9 +789,9 @@ where /// Represents a combined-network-based run pub struct CombinedDaRun { /// The underlying network configuration - config: NetworkConfig, + config: NetworkConfig, /// The private validator config - validator_config: ValidatorConfig, + validator_config: ValidatorConfig, /// The underlying network network: CombinedNetworks, } @@ -821,8 +819,8 @@ where Self: Sync, { async fn initialize_networking( - config: NetworkConfig, - validator_config: ValidatorConfig, + config: NetworkConfig, + validator_config: ValidatorConfig, libp2p_advertise_address: Option, membership: &Arc::Membership>>, ) -> CombinedDaRun { @@ -876,11 +874,11 @@ where self.network.clone() } - fn config(&self) -> NetworkConfig { + fn config(&self) -> NetworkConfig { self.config.clone() } - fn validator_config(&self) -> ValidatorConfig { + fn validator_config(&self) -> ValidatorConfig { self.validator_config.clone() } } @@ -919,7 +917,7 @@ pub async fn main_entry_point< let orchestrator_client: OrchestratorClient = OrchestratorClient::new(args.url.clone()); // We assume one node will not call this twice to generate two validator_config-s with same identity. - let validator_config = NetworkConfig::::generate_init_validator_config( + let validator_config = NetworkConfig::::generate_init_validator_config( orchestrator_client .get_node_index_for_init_validator_config() .await, @@ -933,8 +931,7 @@ pub async fn main_entry_point< .expect("failed to derive Libp2p keypair"); // We need this to be able to register our node - let peer_config = - PeerConfig::::to_bytes(&validator_config.public_config()).clone(); + let peer_config = PeerConfig::::to_bytes(&validator_config.public_config()).clone(); // Derive the advertise multiaddress from the supplied string let advertise_multiaddress = args.advertise_address.clone().map(|advertise_address| { @@ -1061,8 +1058,8 @@ async fn initialize_builder< InstanceState = TestInstanceState, >, >( - run_config: &mut NetworkConfig<::SignatureKey>, - validator_config: &ValidatorConfig<::SignatureKey>, + run_config: &mut NetworkConfig, + validator_config: &ValidatorConfig, args: &ValidatorArgs, orchestrator_client: &OrchestratorClient, ) -> Option>> diff --git a/hotshot-examples/push-cdn/whitelist-adapter.rs b/hotshot-examples/push-cdn/whitelist-adapter.rs index e855a41aba..9445295610 100644 --- a/hotshot-examples/push-cdn/whitelist-adapter.rs +++ b/hotshot-examples/push-cdn/whitelist-adapter.rs @@ -15,10 +15,7 @@ use cdn_broker::reexports::discovery::{DiscoveryClient, Embedded, Redis}; use clap::Parser; use hotshot_example_types::node_types::TestTypes; use hotshot_orchestrator::client::OrchestratorClient; -use hotshot_types::{ - network::NetworkConfig, - traits::{node_implementation::NodeType, signature_key::SignatureKey}, -}; +use hotshot_types::{network::NetworkConfig, traits::signature_key::SignatureKey}; use surf_disco::Url; #[derive(Parser, Debug)] @@ -55,8 +52,7 @@ async fn main() -> Result<()> { // Attempt to get the config from the orchestrator. // Loops internally until the config is received. - let config: NetworkConfig<::SignatureKey> = - orchestrator_client.get_config_after_collection().await; + let config: NetworkConfig = orchestrator_client.get_config_after_collection().await; tracing::info!("Received config from orchestrator"); diff --git a/hotshot-orchestrator/src/client.rs b/hotshot-orchestrator/src/client.rs index 3c1d8e0884..c64d68cf93 100644 --- a/hotshot-orchestrator/src/client.rs +++ b/hotshot-orchestrator/src/client.rs @@ -10,7 +10,7 @@ use clap::Parser; use futures::{Future, FutureExt}; use hotshot_types::{ network::{NetworkConfig, NetworkConfigSource}, - traits::signature_key::SignatureKey, + traits::node_implementation::NodeType, PeerConfig, ValidatorConfig, }; use libp2p_identity::PeerId; @@ -167,15 +167,19 @@ pub struct MultiValidatorArgs { /// /// # Errors /// If we are unable to get the configuration from the orchestrator -pub async fn get_complete_config( +pub async fn get_complete_config( client: &OrchestratorClient, - mut validator_config: ValidatorConfig, + mut validator_config: ValidatorConfig, libp2p_advertise_address: Option, libp2p_public_key: Option, -) -> anyhow::Result<(NetworkConfig, ValidatorConfig, NetworkConfigSource)> { +) -> anyhow::Result<( + NetworkConfig, + ValidatorConfig, + NetworkConfigSource, +)> { // get the configuration from the orchestrator - let run_config: NetworkConfig = client - .post_and_wait_all_public_keys::( + let run_config: NetworkConfig = client + .post_and_wait_all_public_keys::( &mut validator_config, libp2p_advertise_address, libp2p_public_key, @@ -250,11 +254,11 @@ impl OrchestratorClient { /// # Errors /// If we were unable to serialize the Libp2p data #[allow(clippy::type_complexity)] - pub async fn get_config_without_peer( + pub async fn get_config_without_peer( &self, libp2p_advertise_address: Option, libp2p_public_key: Option, - ) -> anyhow::Result> { + ) -> anyhow::Result> { // Serialize our (possible) libp2p-specific data let request_body = vbs::Serializer::::serialize(&( libp2p_advertise_address, @@ -281,7 +285,7 @@ impl OrchestratorClient { // get the corresponding config let f = |client: Client| { async move { - let config: Result, ClientError> = client + let config: Result, ClientError> = client .post(&format!("api/config/{node_index}")) .send() .await; @@ -322,7 +326,7 @@ impl OrchestratorClient { /// /// Does not fail, retries internally until success. #[instrument(skip_all, name = "orchestrator config")] - pub async fn get_config_after_collection(&self) -> NetworkConfig { + pub async fn get_config_after_collection(&self) -> NetworkConfig { // Define the request for post-register configurations let get_config_after_collection = |client: Client| { async move { @@ -396,13 +400,14 @@ impl OrchestratorClient { /// # Panics /// if unable to post #[instrument(skip(self), name = "orchestrator public keys")] - pub async fn post_and_wait_all_public_keys( + pub async fn post_and_wait_all_public_keys( &self, - validator_config: &mut ValidatorConfig, + validator_config: &mut ValidatorConfig, libp2p_advertise_address: Option, libp2p_public_key: Option, - ) -> NetworkConfig { - let pubkey: Vec = PeerConfig::::to_bytes(&validator_config.public_config()).clone(); + ) -> NetworkConfig { + let pubkey: Vec = + PeerConfig::::to_bytes(&validator_config.public_config()).clone(); let da_requested: bool = validator_config.is_da; // Serialize our (possible) libp2p-specific data diff --git a/hotshot-orchestrator/src/lib.rs b/hotshot-orchestrator/src/lib.rs index bafdccbe2c..596ef2295e 100644 --- a/hotshot-orchestrator/src/lib.rs +++ b/hotshot-orchestrator/src/lib.rs @@ -23,7 +23,10 @@ use csv::Writer; use futures::{stream::FuturesUnordered, FutureExt, StreamExt}; use hotshot_types::{ network::{BuilderType, NetworkConfig, PublicKeysFile}, - traits::signature_key::{SignatureKey, StakeTableEntryType}, + traits::{ + node_implementation::NodeType, + signature_key::{SignatureKey, StakeTableEntryType}, + }, PeerConfig, }; use libp2p_identity::{ @@ -70,13 +73,13 @@ pub fn libp2p_generate_indexed_identity(seed: [u8; 32], index: u64) -> Keypair { /// The state of the orchestrator #[derive(Default, Clone)] #[allow(clippy::struct_excessive_bools)] -struct OrchestratorState { +struct OrchestratorState { /// Tracks the latest node index we have generated a configuration for latest_index: u16, /// Tracks the latest temporary index we have generated for init validator's key pair tmp_latest_index: u16, /// The network configuration - config: NetworkConfig, + config: NetworkConfig, /// Whether the network configuration has been updated with all the peer's public keys/configs peer_pub_ready: bool, /// A map from public keys to `(node_index, is_da)`. @@ -85,7 +88,7 @@ struct OrchestratorState { /// Will be set to true once all nodes post they are ready to start start: bool, /// The total nodes that have posted they are ready to start - nodes_connected: HashSet>, + nodes_connected: HashSet>, /// The results of the benchmarks bench_results: BenchResults, /// The number of nodes that have posted their results @@ -100,9 +103,9 @@ struct OrchestratorState { fixed_stake_table: bool, } -impl OrchestratorState { +impl OrchestratorState { /// create a new [`OrchestratorState`] - pub fn new(network_config: NetworkConfig) -> Self { + pub fn new(network_config: NetworkConfig) -> Self { let mut peer_pub_ready = false; let mut fixed_stake_table = false; @@ -173,7 +176,7 @@ impl OrchestratorState { } /// An api exposed by the orchestrator -pub trait OrchestratorApi { +pub trait OrchestratorApi { /// Post an identity to the orchestrator. Takes in optional /// arguments so others can identify us on the Libp2p network. /// # Errors @@ -186,7 +189,7 @@ pub trait OrchestratorApi { /// post endpoint for each node's config /// # Errors /// if unable to serve - fn post_getconfig(&mut self, _node_index: u16) -> Result, ServerError>; + fn post_getconfig(&mut self, _node_index: u16) -> Result, ServerError>; /// get endpoint for the next available temporary node index /// # Errors /// if unable to serve @@ -208,7 +211,7 @@ pub trait OrchestratorApi { /// get endpoint for the network config after all peers public keys are collected /// # Errors /// if unable to serve - fn post_config_after_peer_collected(&mut self) -> Result, ServerError>; + fn post_config_after_peer_collected(&mut self) -> Result, ServerError>; /// get endpoint for whether or not the run has started /// # Errors /// if unable to serve @@ -220,7 +223,7 @@ pub trait OrchestratorApi { /// A node POSTs its public key to let the orchestrator know that it is ready /// # Errors /// if unable to serve - fn post_ready(&mut self, peer_config: &PeerConfig) -> Result<(), ServerError>; + fn post_ready(&mut self, peer_config: &PeerConfig) -> Result<(), ServerError>; /// post endpoint for manually starting the orchestrator /// # Errors /// if unable to serve @@ -235,9 +238,9 @@ pub trait OrchestratorApi { fn get_builders(&self) -> Result, ServerError>; } -impl OrchestratorState +impl OrchestratorState where - KEY: serde::Serialize + Clone + SignatureKey + 'static, + TYPES::SignatureKey: serde::Serialize + Clone + SignatureKey + 'static, { /// register a node with an unknown public key. /// this method should be used when we don't have a fixed stake table @@ -264,7 +267,7 @@ where let node_index = self.pub_posted.len() as u64; // Deserialize the public key - let staked_pubkey = PeerConfig::::from_bytes(pubkey).unwrap(); + let staked_pubkey = PeerConfig::::from_bytes(pubkey).unwrap(); self.config .config @@ -333,7 +336,7 @@ where } // Deserialize the public key - let staked_pubkey = PeerConfig::::from_bytes(pubkey).unwrap(); + let staked_pubkey = PeerConfig::::from_bytes(pubkey).unwrap(); // Check if the node is allowed to connect, returning its index and config entry if so. let Some((node_index, node_config)) = @@ -382,9 +385,9 @@ where } } -impl OrchestratorApi for OrchestratorState +impl OrchestratorApi for OrchestratorState where - KEY: serde::Serialize + Clone + SignatureKey + 'static, + TYPES::SignatureKey: serde::Serialize + Clone + SignatureKey + 'static, { /// Post an identity to the orchestrator. Takes in optional /// arguments so others can identify us on the Libp2p network. @@ -425,7 +428,7 @@ where // Assumes nodes will set their own index that they received from the // 'identity' endpoint - fn post_getconfig(&mut self, _node_index: u16) -> Result, ServerError> { + fn post_getconfig(&mut self, _node_index: u16) -> Result, ServerError> { Ok(self.config.clone()) } @@ -469,7 +472,7 @@ where Ok(self.peer_pub_ready) } - fn post_config_after_peer_collected(&mut self) -> Result, ServerError> { + fn post_config_after_peer_collected(&mut self) -> Result, ServerError> { if !self.peer_pub_ready { return Err(ServerError { status: tide_disco::StatusCode::BAD_REQUEST, @@ -492,7 +495,7 @@ where } // Assumes nodes do not post 'ready' twice - fn post_ready(&mut self, peer_config: &PeerConfig) -> Result<(), ServerError> { + fn post_ready(&mut self, peer_config: &PeerConfig) -> Result<(), ServerError> { // If we have not disabled registration verification. // Is this node allowed to connect? if !self @@ -657,11 +660,12 @@ where /// Sets up all API routes #[allow(clippy::too_many_lines)] -fn define_api() -> Result, ApiError> +fn define_api() -> Result, ApiError> where + TYPES: NodeType, State: 'static + Send + Sync + ReadState + WriteState, - ::State: Send + Sync + OrchestratorApi, - KEY: serde::Serialize + SignatureKey, + ::State: Send + Sync + OrchestratorApi, + TYPES::SignatureKey: serde::Serialize, VER: StaticVersionType + 'static, { let api_toml = toml::from_str::(include_str!(concat!( @@ -735,7 +739,7 @@ where let mut body_bytes = req.body_bytes(); body_bytes.drain(..12); // Decode the payload-supplied pubkey - let Some(pubkey) = PeerConfig::::from_bytes(&body_bytes) else { + let Some(pubkey) = PeerConfig::::from_bytes(&body_bytes) else { return Err(ServerError { status: tide_disco::StatusCode::BAD_REQUEST, message: "Malformed body".to_string(), @@ -817,12 +821,12 @@ where /// This errors if tide disco runs into an issue during serving /// # Panics /// This panics if unable to register the api with tide disco -pub async fn run_orchestrator( - mut network_config: NetworkConfig, +pub async fn run_orchestrator( + mut network_config: NetworkConfig, url: Url, ) -> io::Result<()> where - KEY: SignatureKey + 'static + serde::Serialize, + TYPES::SignatureKey: 'static + serde::Serialize, { let env_password = std::env::var("ORCHESTRATOR_MANUAL_START_PASSWORD"); @@ -841,8 +845,8 @@ where let config_file_as_string: String = fs::read_to_string(filepath.clone()) .unwrap_or_else(|_| panic!("Could not read config file located at {filepath}")); - let file: PublicKeysFile = - toml::from_str::>(&config_file_as_string) + let file: PublicKeysFile = + toml::from_str::>(&config_file_as_string) .expect("Unable to convert config file to TOML"); network_config.public_keys = file.public_keys; @@ -871,9 +875,10 @@ where let web_api = define_api().map_err(|_e| io::Error::new(ErrorKind::Other, "Failed to define api")); - let state: RwLock> = RwLock::new(OrchestratorState::new(network_config)); + let state: RwLock> = + RwLock::new(OrchestratorState::new(network_config)); - let mut app = App::>, ServerError>::with_state(state); + let mut app = App::>, ServerError>::with_state(state); app.register_module::("api", web_api.unwrap()) .expect("Error registering api"); tracing::error!("listening on {:?}", url); diff --git a/hotshot-query-service/examples/simple-server.rs b/hotshot-query-service/examples/simple-server.rs index 916e8a5c7d..f4515ac8fc 100644 --- a/hotshot-query-service/examples/simple-server.rs +++ b/hotshot-query-service/examples/simple-server.rs @@ -163,7 +163,7 @@ async fn init_consensus( let known_nodes_with_stake = pub_keys .iter() .zip(&state_key_pairs) - .map(|(pub_key, state_key_pair)| PeerConfig:: { + .map(|(pub_key, state_key_pair)| PeerConfig:: { stake_table_entry: pub_key.stake_table_entry(1u64), state_ver_key: state_key_pair.ver_key(), }) @@ -227,6 +227,10 @@ async fn init_consensus( let pub_keys = pub_keys.clone(); let config = config.clone(); let master_map = master_map.clone(); + let state_private_keys = state_key_pairs + .iter() + .map(|kp| kp.sign_key()) + .collect::>(); let membership = membership.clone(); async move { @@ -246,6 +250,7 @@ async fn init_consensus( SystemContext::init( pub_keys[node_id], priv_key, + state_private_keys[node_id].clone(), node_id as u64, config, coordinator, diff --git a/hotshot-query-service/src/testing/consensus.rs b/hotshot-query-service/src/testing/consensus.rs index ce65187d9c..7a594f09ff 100644 --- a/hotshot-query-service/src/testing/consensus.rs +++ b/hotshot-query-service/src/testing/consensus.rs @@ -83,7 +83,7 @@ impl MockNetwork { } pub async fn init_with_config( - update_config: impl FnOnce(&mut HotShotConfig), + update_config: impl FnOnce(&mut HotShotConfig), leaf_only: bool, ) -> Self { let (pub_keys, priv_keys): (Vec<_>, Vec<_>) = (0..NUM_NODES) @@ -163,6 +163,10 @@ impl MockNetwork { let pub_keys = pub_keys.clone(); let master_map = master_map.clone(); + let state_priv_keys = state_key_pairs + .iter() + .map(|kp| kp.sign_key()) + .collect::>(); let span = info_span!("initialize node", node_id); async move { @@ -187,6 +191,7 @@ impl MockNetwork { let hotshot = SystemContext::init( pub_keys[node_id], priv_key, + state_priv_keys[node_id].clone(), node_id as u64, config, memberships, diff --git a/hotshot-query-service/src/testing/mocks.rs b/hotshot-query-service/src/testing/mocks.rs index 8883294f79..9fcde8a734 100644 --- a/hotshot-query-service/src/testing/mocks.rs +++ b/hotshot-query-service/src/testing/mocks.rs @@ -23,7 +23,7 @@ use hotshot_example_types::{ }; use hotshot_types::{ data::{QuorumProposal, ViewNumber}, - signature_key::BLSPubKey, + signature_key::{BLSPubKey, SchnorrPubKey}, traits::node_implementation::{NodeType, Versions}, }; use jf_merkle_tree::{ @@ -140,6 +140,7 @@ impl NodeType for MockTypes { type Membership = StaticCommittee; type BuilderSignatureKey = BLSPubKey; type AuctionResult = TestAuctionResult; + type StateSignatureKey = SchnorrPubKey; } #[derive(Clone, Debug, Copy)] diff --git a/hotshot-state-prover/src/circuit.rs b/hotshot-state-prover/src/circuit.rs index c45f517c9a..3c16fe30f7 100644 --- a/hotshot-state-prover/src/circuit.rs +++ b/hotshot-state-prover/src/circuit.rs @@ -79,8 +79,8 @@ impl LightClientStateVar { state: &GenericLightClientState, ) -> Result { Ok(Self { - view_num: circuit.create_public_variable(F::from(state.view_number as u64))?, - block_height: circuit.create_public_variable(F::from(state.block_height as u64))?, + view_num: circuit.create_public_variable(F::from(state.view_number))?, + block_height: circuit.create_public_variable(F::from(state.block_height))?, block_comm_root: circuit.create_public_variable(state.block_comm_root)?, }) } diff --git a/hotshot-state-prover/src/service.rs b/hotshot-state-prover/src/service.rs index d03dfd51d1..6f7ae73cf4 100644 --- a/hotshot-state-prover/src/service.rs +++ b/hotshot-state-prover/src/service.rs @@ -9,6 +9,7 @@ use std::{ use anyhow::{anyhow, Context, Result}; use contract_bindings_ethers::light_client::{LightClient, LightClientErrors}; use displaydoc::Display; +use espresso_types::SeqTypes; use ethers::{ core::k256::ecdsa::SigningKey, middleware::{ @@ -123,7 +124,7 @@ pub fn init_stake_table( #[derive(Debug, Deserialize)] struct PublicHotShotConfig { - known_nodes_with_stake: Vec>, + known_nodes_with_stake: Vec>, } #[derive(Debug, Deserialize)] diff --git a/hotshot-task-impls/Cargo.toml b/hotshot-task-impls/Cargo.toml index 672f36a1d8..579d3ff3ec 100644 --- a/hotshot-task-impls/Cargo.toml +++ b/hotshot-task-impls/Cargo.toml @@ -26,6 +26,7 @@ hotshot-types = { workspace = true } hotshot-utils = { workspace = true } jf-vid = { workspace = true } lru = { workspace = true } +primitive-types = { workspace = true } rand = { workspace = true } serde = { workspace = true } sha2 = { workspace = true } diff --git a/hotshot-task-impls/src/consensus/handlers.rs b/hotshot-task-impls/src/consensus/handlers.rs index e33b2596ee..d69a0675f6 100644 --- a/hotshot-task-impls/src/consensus/handlers.rs +++ b/hotshot-task-impls/src/consensus/handlers.rs @@ -10,7 +10,8 @@ use async_broadcast::{Receiver, Sender}; use chrono::Utc; use hotshot_types::{ event::{Event, EventType}, - simple_vote::{HasEpoch, QuorumVote2, TimeoutData2, TimeoutVote2}, + simple_certificate::ExtendedQuorumCertificate, + simple_vote::{ExtendedQuorumVote, HasEpoch, QuorumVote2, TimeoutData2, TimeoutVote2}, traits::node_implementation::{ConsensusTime, NodeImplementation, NodeType}, utils::EpochTransitionIndicator, vote::{HasViewNumber, Vote}, @@ -25,7 +26,7 @@ use crate::{ consensus::Versions, events::HotShotEvent, helpers::{broadcast_event, wait_for_next_epoch_qc}, - vote_collection::handle_vote, + vote_collection::{handle_extended_quorum_vote, handle_vote}, }; /// Handle a `QuorumVoteRecv` event. @@ -104,6 +105,76 @@ pub(crate) async fn handle_quorum_vote_recv< Ok(()) } +/// Handle a `ExtendedQuorumVoteRecv` event. +pub(crate) async fn handle_extended_quorum_vote_recv< + TYPES: NodeType, + I: NodeImplementation, + V: Versions, +>( + vote: &ExtendedQuorumVote, + event: Arc>, + sender: &Sender>>, + task_state: &mut ConsensusTaskState, +) -> Result<()> { + let in_transition = task_state + .consensus + .read() + .await + .is_high_qc_for_last_block(); + let epoch_membership = task_state + .membership_coordinator + .membership_for_epoch(vote.vote.data.epoch) + .await + .context(warn!("No stake table for epoch"))?; + + let we_are_leader = + epoch_membership.leader(vote.view_number() + 1).await? == task_state.public_key; + ensure!( + in_transition || we_are_leader, + info!( + "We are not the leader for view {:?} and we are not in the epoch transition", + vote.view_number() + 1 + ) + ); + + handle_extended_quorum_vote( + &mut task_state.extended_quorum_vote_collectors, + vote, + task_state.public_key.clone(), + &epoch_membership, + task_state.id, + &event, + sender, + &task_state.upgrade_lock, + ) + .await?; + + if vote.epoch().is_some() { + // If the vote sender belongs to the next epoch, collect it separately to form the second QC + let has_stake = epoch_membership + .next_epoch() + .await? + .has_stake(&vote.vote.signing_key()) + .await; + if has_stake { + handle_vote( + &mut task_state.next_epoch_vote_collectors, + &vote.vote.clone().into(), + task_state.public_key.clone(), + &epoch_membership.next_epoch().await?.clone(), + task_state.id, + &event, + sender, + &task_state.upgrade_lock, + EpochTransitionIndicator::InTransition, + ) + .await?; + } + } + + Ok(()) +} + /// Handle a `TimeoutVoteRecv` event. pub(crate) async fn handle_timeout_vote_recv< TYPES: NodeType, @@ -168,6 +239,8 @@ pub async fn send_high_qc, V: V, >, + /// A map of collector tasks for light client state update vote + pub extended_quorum_vote_collectors: ExtendedQuorumVoteCollectorsMap, + /// A map of `TimeoutVote` collector tasks. pub timeout_vote_collectors: VoteCollectorsMap, TimeoutCertificate2, V>, @@ -122,6 +128,13 @@ impl, V: Versions> ConsensusTaskSt tracing::debug!("Failed to handle QuorumVoteRecv event; error = {e}"); } }, + HotShotEvent::ExtendedQuorumVoteRecv(ref vote) => { + if let Err(e) = + handle_extended_quorum_vote_recv(vote, Arc::clone(&event), &sender, self).await + { + tracing::debug!("Failed to handle ExtendedQuorumVoteRecv event; error = {e}"); + } + }, HotShotEvent::TimeoutVoteRecv(ref vote) => { if let Err(e) = handle_timeout_vote_recv(vote, Arc::clone(&event), &sender, self).await @@ -143,45 +156,18 @@ impl, V: Versions> ConsensusTaskSt tracing::debug!("Failed to handle Timeout event; error = {e}"); } }, - HotShotEvent::ExtendedQc2Formed(eqc) => { - let cert_view = eqc.view_number(); - let cert_block_number = self - .consensus - .read() - .await - .saved_leaves() - .get(&eqc.data.leaf_commit) - .context(error!( - "Could not find the leaf for the eQC. It shouldn't happen." - ))? - .height(); - - let cert_epoch = option_epoch_from_block_number::( - true, - cert_block_number, - self.epoch_height, - ); - // Transition to the new epoch by sending ViewChange - let next_epoch = cert_epoch.map(|x| x + 1); - tracing::info!("Entering new epoch: {:?}", next_epoch); - broadcast_event( - Arc::new(HotShotEvent::ViewChange(cert_view + 1, next_epoch)), - &sender, - ) - .await; - }, - HotShotEvent::ExtendedQcRecv(high_qc, next_epoch_high_qc, _) => { + HotShotEvent::ExtendedQcRecv(eqc, next_epoch_high_qc, _) => { if !self .consensus .read() .await - .is_leaf_extended(high_qc.data.leaf_commit) + .is_leaf_extended(eqc.qc.data.leaf_commit) { tracing::warn!("Received extended QC but we can't verify the leaf is extended"); return Ok(()); } if let Err(e) = validate_qc_and_next_epoch_qc( - high_qc, + &eqc.qc, Some(next_epoch_high_qc), &self.consensus, &self.membership_coordinator, @@ -193,8 +179,23 @@ impl, V: Versions> ConsensusTaskSt return Ok(()); } + if let Err(e) = validate_light_client_state_update_certificate( + &eqc.state_cert, + &self.membership_coordinator, + ) + .await + { + tracing::error!( + "Received invalid light client state update certificate: {}", + e + ); + return Ok(()); + } + let mut consensus_writer = self.consensus.write().await; - let high_qc_updated = consensus_writer.update_high_qc(high_qc.clone()).is_ok(); + let high_qc_updated = consensus_writer + .update_high_qc_and_state_cert(eqc.qc.clone(), eqc.state_cert.clone()) + .is_ok(); let next_high_qc_updated = consensus_writer .update_next_epoch_high_qc(next_epoch_high_qc.clone()) .is_ok(); @@ -202,18 +203,15 @@ impl, V: Versions> ConsensusTaskSt tracing::debug!( "Received Extended QC for view {:?} and epoch {:?}.", - high_qc.view_number(), - high_qc.epoch() + eqc.view_number(), + eqc.epoch() ); if high_qc_updated || next_high_qc_updated { // Send ViewChange indicating new view and new epoch. - let next_epoch = high_qc.data.epoch().map(|x| x + 1); + let next_epoch = eqc.qc.data.epoch().map(|x| x + 1); tracing::info!("Entering new epoch: {:?}", next_epoch); broadcast_event( - Arc::new(HotShotEvent::ViewChange( - high_qc.view_number() + 1, - next_epoch, - )), + Arc::new(HotShotEvent::ViewChange(eqc.view_number() + 1, next_epoch)), &sender, ) .await; diff --git a/hotshot-task-impls/src/events.rs b/hotshot-task-impls/src/events.rs index c543265ef3..0e7e4ca13e 100644 --- a/hotshot-task-impls/src/events.rs +++ b/hotshot-task-impls/src/events.rs @@ -17,12 +17,12 @@ use hotshot_types::{ message::Proposal, request_response::ProposalRequestPayload, simple_certificate::{ - DaCertificate2, NextEpochQuorumCertificate2, QuorumCertificate, QuorumCertificate2, - TimeoutCertificate, TimeoutCertificate2, UpgradeCertificate, ViewSyncCommitCertificate2, - ViewSyncFinalizeCertificate2, ViewSyncPreCommitCertificate2, + DaCertificate2, ExtendedQuorumCertificate, NextEpochQuorumCertificate2, QuorumCertificate, + QuorumCertificate2, TimeoutCertificate, TimeoutCertificate2, UpgradeCertificate, + ViewSyncCommitCertificate2, ViewSyncFinalizeCertificate2, ViewSyncPreCommitCertificate2, }, simple_vote::{ - DaVote2, QuorumVote2, TimeoutVote2, UpgradeVote, ViewSyncCommitVote2, + DaVote2, ExtendedQuorumVote, QuorumVote2, TimeoutVote2, UpgradeVote, ViewSyncCommitVote2, ViewSyncFinalizeVote2, ViewSyncPreCommitVote2, }, traits::{ @@ -77,6 +77,8 @@ pub enum HotShotEvent { ), /// A quorum vote has been received from the network; handled by the consensus task QuorumVoteRecv(QuorumVote2), + /// Quorum vote for extended QC + ExtendedQuorumVoteRecv(ExtendedQuorumVote), /// A timeout vote received from the network; handled by consensus task TimeoutVoteRecv(TimeoutVote2), /// Send a timeout vote to the network; emitted by consensus task replicas @@ -98,8 +100,8 @@ pub enum HotShotEvent { ), /// Send a quorum vote to the next leader; emitted by a replica in the consensus task after seeing a valid quorum proposal QuorumVoteSend(QuorumVote2), - /// Broadcast a quorum vote to form an eQC; emitted by a replica in the consensus task after seeing a valid quorum proposal - ExtendedQuorumVoteSend(QuorumVote2), + /// Quorum vote for extended QC + ExtendedQuorumVoteSend(ExtendedQuorumVote), /// A quorum proposal with the given parent leaf is validated. /// The full validation checks include: /// 1. The proposal is not for an old view @@ -132,10 +134,10 @@ pub enum HotShotEvent { QcFormed(Either, TimeoutCertificate>), /// The next leader has collected enough votes to form a QC; emitted by the next leader in the consensus task; an internal event only Qc2Formed(Either, TimeoutCertificate2>), + /// The next leader has collected enough votes to form a extended QC; emitted by the next leader in the consensus task; an internal event only + ExtendedQcFormed(Either, TimeoutCertificate2>), /// The next leader has collected enough votes from the next epoch nodes to form a QC; emitted by the next leader in the consensus task; an internal event only NextEpochQc2Formed(Either, TimeoutCertificate>), - /// A validator formed both a current epoch eQC and a next epoch eQC - ExtendedQc2Formed(QuorumCertificate2), /// The DA leader has collected enough votes to form a DAC; emitted by the DA leader in the DA task; sent to the entire network via the networking task DacSend(DaCertificate2, TYPES::SignatureKey), /// The current view has changed; emitted by the replica in the consensus task or replica in the view sync task; received by almost all other tasks @@ -262,14 +264,14 @@ pub enum HotShotEvent { /// A replica sent us an extended QuorumCertificate and NextEpochQuorumCertificate ExtendedQcRecv( - QuorumCertificate2, + ExtendedQuorumCertificate, NextEpochQuorumCertificate2, TYPES::SignatureKey, ), /// Send our extended QuorumCertificate and NextEpochQuorumCertificate to all nodes in the old and new epoch ExtendedQcSend( - QuorumCertificate2, + ExtendedQuorumCertificate, NextEpochQuorumCertificate2, TYPES::SignatureKey, ), @@ -280,7 +282,6 @@ impl HotShotEvent { /// Return the view number for a hotshot event if present pub fn view_number(&self) -> Option { match self { - HotShotEvent::QuorumVoteRecv(v) => Some(v.view_number()), HotShotEvent::TimeoutVoteRecv(v) | HotShotEvent::TimeoutVoteSend(v) => { Some(v.view_number()) }, @@ -292,9 +293,11 @@ impl HotShotEvent { | HotShotEvent::QuorumProposalPreliminarilyValidated(proposal) => { Some(proposal.data.view_number()) }, - HotShotEvent::QuorumVoteSend(vote) | HotShotEvent::ExtendedQuorumVoteSend(vote) => { + HotShotEvent::QuorumVoteSend(vote) | HotShotEvent::QuorumVoteRecv(vote) => { Some(vote.view_number()) }, + HotShotEvent::ExtendedQuorumVoteSend(vote) + | HotShotEvent::ExtendedQuorumVoteRecv(vote) => Some(vote.view_number()), HotShotEvent::DaProposalRecv(proposal, _) | HotShotEvent::DaProposalValidated(proposal, _) | HotShotEvent::DaProposalSend(proposal, _) => Some(proposal.data.view_number()), @@ -309,11 +312,14 @@ impl HotShotEvent { either::Left(qc) => Some(qc.view_number()), either::Right(tc) => Some(tc.view_number()), }, + HotShotEvent::ExtendedQcFormed(cert) => match cert { + either::Left(qc) => Some(qc.view_number()), + either::Right(tc) => Some(tc.view_number()), + }, HotShotEvent::NextEpochQc2Formed(cert) => match cert { either::Left(qc) => Some(qc.view_number()), either::Right(tc) => Some(tc.view_number()), }, - HotShotEvent::ExtendedQc2Formed(cert) => Some(cert.view_number()), HotShotEvent::ViewSyncCommitVoteSend(vote) | HotShotEvent::ViewSyncCommitVoteRecv(vote) => Some(vote.view_number()), HotShotEvent::ViewSyncPreCommitVoteRecv(vote) @@ -357,10 +363,12 @@ impl HotShotEvent { | HotShotEvent::VidRequestRecv(request, _) => Some(request.view), HotShotEvent::VidResponseSend(_, _, proposal) | HotShotEvent::VidResponseRecv(_, proposal) => Some(proposal.data.view_number()), - HotShotEvent::HighQcRecv(qc, _) - | HotShotEvent::HighQcSend(qc, ..) - | HotShotEvent::ExtendedQcRecv(qc, ..) - | HotShotEvent::ExtendedQcSend(qc, ..) => Some(qc.view_number()), + HotShotEvent::HighQcRecv(qc, ..) | HotShotEvent::HighQcSend(qc, ..) => { + Some(qc.view_number()) + }, + HotShotEvent::ExtendedQcRecv(qc, ..) | HotShotEvent::ExtendedQcSend(qc, ..) => { + Some(qc.view_number()) + }, } } } @@ -385,6 +393,13 @@ impl Display for HotShotEvent { v.view_number() ) }, + HotShotEvent::ExtendedQuorumVoteRecv(v) => { + write!( + f, + "ExtendedQuorumVoteRecv(view_number={:?})", + v.view_number() + ) + }, HotShotEvent::TimeoutVoteRecv(v) => { write!(f, "TimeoutVoteRecv(view_number={:?})", v.view_number()) }, @@ -441,6 +456,10 @@ impl Display for HotShotEvent { either::Left(qc) => write!(f, "QcFormed2(view_number={:?})", qc.view_number()), either::Right(tc) => write!(f, "QcFormed2(view_number={:?})", tc.view_number()), }, + HotShotEvent::ExtendedQcFormed(cert) => match cert { + either::Left(qc) => write!(f, "QcFormed2(view_number={:?})", qc.view_number()), + either::Right(tc) => write!(f, "QcFormed2(view_number={:?})", tc.view_number()), + }, HotShotEvent::NextEpochQc2Formed(cert) => match cert { either::Left(qc) => { write!(f, "NextEpochQc2Formed(view_number={:?})", qc.view_number()) @@ -449,9 +468,6 @@ impl Display for HotShotEvent { write!(f, "NextEpochQc2Formed(view_number={:?})", tc.view_number()) }, }, - HotShotEvent::ExtendedQc2Formed(cert) => { - write!(f, "ExtendedQc2Formed(view_number={:?})", cert.view_number()) - }, HotShotEvent::DacSend(cert, _) => { write!(f, "DacSend(view_number={:?})", cert.view_number()) }, diff --git a/hotshot-task-impls/src/helpers.rs b/hotshot-task-impls/src/helpers.rs index 6884bc0169..54168a35d6 100644 --- a/hotshot-task-impls/src/helpers.rs +++ b/hotshot-task-impls/src/helpers.rs @@ -22,13 +22,16 @@ use hotshot_types::{ event::{Event, EventType, LeafInfo}, message::{Proposal, UpgradeLock}, request_response::ProposalRequestPayload, - simple_certificate::{NextEpochQuorumCertificate2, QuorumCertificate2, UpgradeCertificate}, + simple_certificate::{ + LightClientStateUpdateCertificate, NextEpochQuorumCertificate2, QuorumCertificate2, + UpgradeCertificate, + }, simple_vote::HasEpoch, traits::{ block_contents::BlockHeader, election::Membership, node_implementation::{ConsensusTime, NodeImplementation, NodeType, Versions}, - signature_key::SignatureKey, + signature_key::{SignatureKey, StakeTableEntryType, StateSignatureKey}, storage::Storage, BlockPayload, ValidatedState, }, @@ -40,6 +43,7 @@ use hotshot_types::{ StakeTableEntries, }; use hotshot_utils::anytrace::*; +use primitive_types::U256; use tokio::time::timeout; use tracing::instrument; @@ -996,3 +1000,43 @@ pub async fn validate_qc_and_next_epoch_qc( } Ok(()) } + +/// Validates qc's signatures and, if provided, validates next_epoch_qc's signatures and whether it +/// corresponds to the provided high_qc. +pub async fn validate_light_client_state_update_certificate( + state_cert: &LightClientStateUpdateCertificate, + membership_coordinator: &EpochMembershipCoordinator, +) -> Result<()> { + let epoch_membership = membership_coordinator + .membership_for_epoch(state_cert.epoch()) + .await?; + + let membership_stake_table = epoch_membership.stake_table().await; + let membership_success_threshold = epoch_membership.success_threshold().await; + + let mut state_key_map = HashMap::new(); + membership_stake_table.into_iter().for_each(|config| { + state_key_map.insert( + config.state_ver_key.clone(), + config.stake_table_entry.stake(), + ); + }); + + let mut accumulated_stake = U256::zero(); + let state_msg = (&state_cert.light_client_state).into(); + for (key, sig) in state_cert.signatures.iter() { + if let Some(stake) = state_key_map.get(key) { + accumulated_stake += *stake; + if !key.verify_state_sig(sig, &state_msg) { + bail!("Invalid light client state update certificate signature"); + } + } else { + bail!("Invalid light client state update certificate signature"); + } + } + if accumulated_stake < U256::from(membership_success_threshold.get()) { + bail!("Light client state update certificate does not meet the success threshold"); + } + + Ok(()) +} diff --git a/hotshot-task-impls/src/network.rs b/hotshot-task-impls/src/network.rs index a7ebb87c0a..b6309e3d50 100644 --- a/hotshot-task-impls/src/network.rs +++ b/hotshot-task-impls/src/network.rs @@ -311,6 +311,9 @@ impl NetworkMessageTaskState { GeneralConsensusMessage::ExtendedQc(qc, next_epoch_qc) => { HotShotEvent::ExtendedQcRecv(qc, next_epoch_qc, sender) }, + GeneralConsensusMessage::ExtendedQuorumVote(vote) => { + HotShotEvent::ExtendedQuorumVoteRecv(vote) + }, }, SequencingMessage::Da(da_message) => match da_message { DaConsensusMessage::DaProposal(proposal) => { @@ -759,15 +762,15 @@ impl< *maybe_action = Some(HotShotAction::Vote); let message = if self.upgrade_lock.epochs_enabled(vote.view_number()).await { MessageKind::::from_consensus_message(SequencingMessage::General( - GeneralConsensusMessage::Vote2(vote.clone()), + GeneralConsensusMessage::ExtendedQuorumVote(vote.clone()), )) } else { MessageKind::::from_consensus_message(SequencingMessage::General( - GeneralConsensusMessage::Vote(vote.clone().to_vote()), + GeneralConsensusMessage::Vote(vote.vote.clone().to_vote()), )) }; - Some((vote.signing_key(), message, TransmitType::Broadcast)) + Some((vote.vote.signing_key(), message, TransmitType::Broadcast)) }, HotShotEvent::QuorumProposalRequestSend(req, signature) => Some(( req.key.clone(), diff --git a/hotshot-task-impls/src/quorum_proposal/handlers.rs b/hotshot-task-impls/src/quorum_proposal/handlers.rs index 16f50edec8..180e0cf60d 100644 --- a/hotshot-task-impls/src/quorum_proposal/handlers.rs +++ b/hotshot-task-impls/src/quorum_proposal/handlers.rs @@ -13,7 +13,6 @@ use std::{ time::{Duration, Instant}, }; -use anyhow::{ensure, Context, Result}; use async_broadcast::{Receiver, Sender}; use async_lock::RwLock; use committable::{Commitment, Committable}; @@ -23,7 +22,7 @@ use hotshot_types::{ data::{Leaf2, QuorumProposal2, QuorumProposalWrapper, VidDisperse, ViewChangeEvidence2}, epoch_membership::EpochMembership, message::Proposal, - simple_certificate::{QuorumCertificate2, UpgradeCertificate}, + simple_certificate::{ExtendedQuorumCertificate, QuorumCertificate2, UpgradeCertificate}, traits::{ block_contents::BlockHeader, node_implementation::{NodeImplementation, NodeType}, @@ -128,23 +127,31 @@ impl ProposalDependencyHandle { rx: &mut Receiver>>, ) -> Option> { while let Ok(event) = rx.recv_direct().await { - if let HotShotEvent::HighQcRecv(qc, _sender) = event.as_ref() { - let prev_epoch = qc.data.epoch; - let epoch_membership = self.membership.get_new_epoch(prev_epoch).await.ok()?; - let membership_stake_table = epoch_membership.stake_table().await; - let membership_success_threshold = epoch_membership.success_threshold().await; - - if qc - .is_valid_cert( - StakeTableEntries::::from(membership_stake_table).0, - membership_success_threshold, - &self.upgrade_lock, - ) - .await - .is_ok() - { - return Some(qc.clone()); - } + match event.as_ref() { + HotShotEvent::HighQcRecv(qc, _) + | HotShotEvent::ExtendedQcRecv( + ExtendedQuorumCertificate { qc, state_cert: _ }, + _, + _, + ) => { + let prev_epoch = qc.data.epoch; + let epoch_membership = self.membership.get_new_epoch(prev_epoch).await.ok()?; + let membership_stake_table = epoch_membership.stake_table().await; + let membership_success_threshold = epoch_membership.success_threshold().await; + + if qc + .is_valid_cert( + StakeTableEntries::::from(membership_stake_table).0, + membership_success_threshold, + &self.upgrade_lock, + ) + .await + .is_ok() + { + return Some(qc.clone()); + } + }, + _ => {}, } } None @@ -441,6 +448,14 @@ impl HandleDepOutput for ProposalDependencyHandle< parent_qc = Some(qc.clone()); }, }, + HotShotEvent::ExtendedQcFormed(cert) => match cert { + either::Right(timeout) => { + timeout_certificate = Some(timeout.clone()); + }, + either::Left(eqc) => { + parent_qc = Some(eqc.qc.clone()); + }, + }, HotShotEvent::ViewSyncFinalizeCertificateRecv(cert) => { view_sync_finalize_cert = Some(cert.clone()); }, @@ -510,41 +525,54 @@ pub(super) async fn handle_eqc_formed< leaf_commit: Commitment>, task_state: &QuorumProposalTaskState, event_sender: &Sender>>, -) { - if !task_state.upgrade_lock.epochs_enabled(cert_view).await { - tracing::debug!("QC2 formed but epochs not enabled. Do nothing"); - return; - } - if !task_state - .consensus - .read() - .await - .is_leaf_extended(leaf_commit) - { - tracing::debug!("We formed QC but not eQC. Do nothing"); - return; - } + epoch_height: u64, +) -> Result<()> { + ensure!( + task_state.upgrade_lock.epochs_enabled(cert_view).await, + debug!("QC2 formed but epochs not enabled. Do nothing") + ); + ensure!( + task_state + .consensus + .read() + .await + .is_leaf_extended(leaf_commit), + debug!("We formed QC but not eQC. Do nothing") + ); let consensus_reader = task_state.consensus.read().await; let current_epoch_qc = consensus_reader.high_qc(); + let cert_view = current_epoch_qc.view_number(); + let cert_block_number = consensus_reader + .saved_leaves() + .get(¤t_epoch_qc.data.leaf_commit) + .context(error!( + "Could not find the leaf for the eQC. It shouldn't happen." + ))? + .height(); + let Some(next_epoch_qc) = consensus_reader.next_epoch_high_qc() else { - tracing::debug!("We formed the eQC but we don't have the next epoch eQC at all."); - return; + return Err(debug!( + "We formed the eQC but we don't have the next epoch eQC at all." + )); }; if current_epoch_qc.view_number() != next_epoch_qc.view_number() || current_epoch_qc.data != *next_epoch_qc.data { - tracing::debug!( + return Err(debug!( "We formed the eQC but the current and next epoch QCs do not correspond to each other." - ); - return; + )); } - let current_epoch_qc_clone = current_epoch_qc.clone(); drop(consensus_reader); + let cert_epoch = option_epoch_from_block_number::(true, cert_block_number, epoch_height); + // Transition to the new epoch by sending ViewChange + let next_epoch = cert_epoch.map(|x| x + 1); + tracing::info!("Entering new epoch: {:?}", next_epoch); broadcast_event( - Arc::new(HotShotEvent::ExtendedQc2Formed(current_epoch_qc_clone)), + Arc::new(HotShotEvent::ViewChange(cert_view + 1, next_epoch)), event_sender, ) .await; + Ok(()) } diff --git a/hotshot-task-impls/src/quorum_proposal/mod.rs b/hotshot-task-impls/src/quorum_proposal/mod.rs index 1b933f8374..968d4d1eb4 100644 --- a/hotshot-task-impls/src/quorum_proposal/mod.rs +++ b/hotshot-task-impls/src/quorum_proposal/mod.rs @@ -19,7 +19,7 @@ use hotshot_types::{ consensus::OuterConsensus, epoch_membership::EpochMembershipCoordinator, message::UpgradeLock, - simple_certificate::{QuorumCertificate2, UpgradeCertificate}, + simple_certificate::{ExtendedQuorumCertificate, QuorumCertificate2, UpgradeCertificate}, traits::{ node_implementation::{ConsensusTime, NodeImplementation, NodeType, Versions}, signature_key::SignatureKey, @@ -111,6 +111,8 @@ impl, V: Versions> ProposalDependency::Qc => { if let HotShotEvent::Qc2Formed(either::Left(qc)) = event { qc.view_number() + 1 + } else if let HotShotEvent::ExtendedQcFormed(either::Left(eqc)) = event { + eqc.qc.view_number() + 1 } else { return false; } @@ -118,6 +120,9 @@ impl, V: Versions> ProposalDependency::TimeoutCert => { if let HotShotEvent::Qc2Formed(either::Right(timeout)) = event { timeout.view_number() + 1 + } else if let HotShotEvent::ExtendedQcFormed(either::Right(timeout)) = event + { + timeout.view_number() + 1 } else { return false; } @@ -230,6 +235,10 @@ impl, V: Versions> qc_dependency.mark_as_completed(event); }, }, + HotShotEvent::ExtendedQcFormed(eqc) => match eqc { + Either::Right(_) => timeout_dependency.mark_as_completed(event), + Either::Left(_) => qc_dependency.mark_as_completed(event), + }, HotShotEvent::ViewSyncFinalizeCertificateRecv(_) => { view_sync_dependency.mark_as_completed(event); }, @@ -448,9 +457,6 @@ impl, V: Versions> .wrap() .context(error!("Failed to update high QC in storage!"))?; - handle_eqc_formed(qc.view_number(), qc.data.leaf_commit, self, &event_sender) - .await; - let view_number = qc.view_number() + 1; self.create_dependency_task_if_new( view_number, @@ -463,6 +469,67 @@ impl, V: Versions> .await?; }, }, + HotShotEvent::ExtendedQcFormed(cert) => match cert { + either::Right(timeout_cert) => { + let view_number = timeout_cert.view_number + 1; + self.create_dependency_task_if_new( + view_number, + epoch_number, + event_receiver, + event_sender, + Arc::clone(&event), + epoch_transition_indicator, + ) + .await?; + }, + either::Left(eqc) => { + // Only update if the qc is from a newer view + if eqc.qc.view_number() <= self.consensus.read().await.high_qc().view_number { + tracing::trace!( + "Received a QC for a view that was not > than our current high QC" + ); + } + self.consensus + .write() + .await + .update_high_qc_and_state_cert(eqc.qc.clone(), eqc.state_cert.clone()) + .wrap() + .context(error!( + "Failed to update high QC or state certificate in internal consensus state!" + ))?; + + // Then update the high QC and the state certificate in storage + self.storage + .write() + .await + .update_high_qc2_and_state_cert(eqc.qc.clone(), eqc.state_cert.clone()) + .await + .wrap() + .context(error!( + "Failed to update high QC or state certificate in storage!" + ))?; + + handle_eqc_formed( + eqc.qc.view_number(), + eqc.qc.data.leaf_commit, + self, + &event_sender, + self.epoch_height, + ) + .await?; + + let view_number = eqc.qc.view_number() + 1; + self.create_dependency_task_if_new( + view_number, + epoch_number, + event_receiver, + event_sender, + Arc::clone(&event), + epoch_transition_indicator, + ) + .await?; + }, + }, HotShotEvent::SendPayloadCommitmentAndMetadata( _payload_commitment, _builder_commitment, @@ -569,7 +636,8 @@ impl, V: Versions> let keep_view = TYPES::View::new(view.saturating_sub(1)); self.cancel_tasks(keep_view); }, - HotShotEvent::HighQcSend(qc, ..) | HotShotEvent::ExtendedQcSend(qc, ..) => { + HotShotEvent::HighQcSend(qc, ..) + | HotShotEvent::ExtendedQcSend(ExtendedQuorumCertificate { qc, .. }, ..) => { ensure!(qc.view_number() > self.highest_qc.view_number()); let cert_epoch_number = qc.data.epoch; @@ -621,8 +689,9 @@ impl, V: Versions> next_epoch_qc.data.leaf_commit, self, &event_sender, + self.epoch_height, ) - .await; + .await?; }, _ => {}, } diff --git a/hotshot-task-impls/src/quorum_vote/handlers.rs b/hotshot-task-impls/src/quorum_vote/handlers.rs index eb47f7c3e4..1da99b5f84 100644 --- a/hotshot-task-impls/src/quorum_vote/handlers.rs +++ b/hotshot-task-impls/src/quorum_vote/handlers.rs @@ -17,12 +17,14 @@ use hotshot_types::{ epoch_membership::{EpochMembership, EpochMembershipCoordinator}, event::{Event, EventType}, message::{Proposal, UpgradeLock}, - simple_vote::{HasEpoch, QuorumData2, QuorumVote2}, + simple_vote::{ + ExtendedQuorumVote, HasEpoch, LightClientStateUpdateVote, QuorumData2, QuorumVote2, + }, traits::{ block_contents::BlockHeader, election::Membership, node_implementation::{ConsensusTime, NodeImplementation, NodeType}, - signature_key::SignatureKey, + signature_key::{SignatureKey, StateSignatureKey}, storage::Storage, ValidatedState, }, @@ -698,6 +700,7 @@ pub(crate) async fn submit_vote, V vid_share: Proposal>, extended_vote: bool, epoch_height: u64, + state_private_key: &::StatePrivateKey, ) -> Result<()> { let committee_member_in_current_epoch = membership.has_stake(&public_key).await; // If the proposed leaf is for the last block in the epoch and the node is part of the quorum committee @@ -739,8 +742,27 @@ pub(crate) async fn submit_vote, V if extended_vote { tracing::debug!("sending extended vote to everybody",); + let light_client_state = leaf + .block_header() + .get_light_client_state(view_number) + .wrap() + .context(error!("Failed to generate light client state"))?; + let signature = ::sign_state( + state_private_key, + &(&light_client_state).into(), + ) + .wrap() + .context(error!("Failed to sign the light client state"))?; + let state_vote = LightClientStateUpdateVote { + epoch: TYPES::Epoch::new(epoch_from_block_number(leaf.height(), epoch_height)), + light_client_state, + signature, + }; broadcast_event( - Arc::new(HotShotEvent::ExtendedQuorumVoteSend(vote)), + Arc::new(HotShotEvent::ExtendedQuorumVoteSend(ExtendedQuorumVote { + vote, + state_vote, + })), &sender, ) .await; diff --git a/hotshot-task-impls/src/quorum_vote/mod.rs b/hotshot-task-impls/src/quorum_vote/mod.rs index 85b033ea1d..ec278d448f 100644 --- a/hotshot-task-impls/src/quorum_vote/mod.rs +++ b/hotshot-task-impls/src/quorum_vote/mod.rs @@ -27,7 +27,7 @@ use hotshot_types::{ traits::{ block_contents::BlockHeader, node_implementation::{ConsensusTime, NodeImplementation, NodeType, Versions}, - signature_key::SignatureKey, + signature_key::{SignatureKey, StateSignatureKey}, storage::Storage, }, utils::{epoch_from_block_number, option_epoch_from_block_number}, @@ -99,6 +99,9 @@ pub struct VoteDependencyHandle, V /// Number of blocks in an epoch, zero means there are no epochs pub epoch_height: u64, + + /// Signature key for light client state + pub state_private_key: ::StatePrivateKey, } impl + 'static, V: Versions> HandleDepOutput @@ -295,6 +298,7 @@ impl + 'static, V: Versions> Handl vid_share, false, self.epoch_height, + &self.state_private_key, ) .await { @@ -357,6 +361,9 @@ pub struct QuorumVoteTaskState, V: /// Block height at which to enable the epoch upgrade pub epoch_upgrade_block_height: u64, + + /// Signature key for light client state + pub state_private_key: ::StatePrivateKey, } impl, V: Versions> QuorumVoteTaskState { @@ -462,6 +469,7 @@ impl, V: Versions> QuorumVoteTaskS id: self.id, epoch_height: self.epoch_height, consensus_metrics: Arc::clone(&self.consensus_metrics), + state_private_key: self.state_private_key.clone(), }, ); self.vote_dependencies @@ -807,6 +815,7 @@ impl, V: Versions> QuorumVoteTaskS updated_vid, is_vote_leaf_extended, self.epoch_height, + &self.state_private_key, ) .await .context(|e| debug!("Failed to submit vote; error = {}", e)) diff --git a/hotshot-task-impls/src/vote_collection.rs b/hotshot-task-impls/src/vote_collection.rs index 9dc3ebc477..7145f33780 100644 --- a/hotshot-task-impls/src/vote_collection.rs +++ b/hotshot-task-impls/src/vote_collection.rs @@ -19,17 +19,19 @@ use hotshot_types::{ epoch_membership::EpochMembership, message::UpgradeLock, simple_certificate::{ - DaCertificate2, NextEpochQuorumCertificate2, QuorumCertificate, QuorumCertificate2, - TimeoutCertificate2, UpgradeCertificate, ViewSyncCommitCertificate2, + DaCertificate2, ExtendedQuorumCertificate, NextEpochQuorumCertificate2, QuorumCertificate, + QuorumCertificate2, TimeoutCertificate2, UpgradeCertificate, ViewSyncCommitCertificate2, ViewSyncFinalizeCertificate2, ViewSyncPreCommitCertificate2, }, simple_vote::{ - DaVote2, NextEpochQuorumVote2, QuorumVote, QuorumVote2, TimeoutVote2, UpgradeVote, - ViewSyncCommitVote2, ViewSyncFinalizeVote2, ViewSyncPreCommitVote2, + DaVote2, ExtendedQuorumVote, NextEpochQuorumVote2, QuorumVote, QuorumVote2, TimeoutVote2, + UpgradeVote, ViewSyncCommitVote2, ViewSyncFinalizeVote2, ViewSyncPreCommitVote2, }, traits::node_implementation::{ConsensusTime, NodeType, Versions}, utils::EpochTransitionIndicator, - vote::{Certificate, HasViewNumber, Vote, VoteAccumulator}, + vote::{ + Certificate, HasViewNumber, LightClientStateUpdateVoteAccumulator, Vote, VoteAccumulator, + }, }; use hotshot_utils::anytrace::*; @@ -518,11 +520,18 @@ impl // #3967 REVIEW NOTE: Should we error if self.epoch is None? self.accumulate_vote(&vote.clone().into(), sender).await }, + HotShotEvent::ExtendedQuorumVoteRecv(vote) => { + self.accumulate_vote(&vote.vote.clone().into(), sender) + .await + }, _ => Ok(None), } } fn filter(event: Arc>) -> bool { - matches!(event.as_ref(), HotShotEvent::QuorumVoteRecv(_)) + matches!( + event.as_ref(), + HotShotEvent::QuorumVoteRecv(_) | HotShotEvent::ExtendedQuorumVoteRecv(_) + ) } } @@ -649,3 +658,210 @@ impl matches!(event.as_ref(), HotShotEvent::ViewSyncFinalizeVoteRecv(_)) } } + +/// A map for extended quorum vote collectors +pub type ExtendedQuorumVoteCollectorsMap = + BTreeMap<::View, ExtendedQuorumVoteCollectionTaskState>; + +pub struct ExtendedQuorumVoteCollectionTaskState { + // pub vote_task_state: + // VoteCollectionTaskState, QuorumCertificate2, V>, + // pub light_client_task_state: LightClientStateUpdateVoteCollectionTaskState, + /// Public key for this node. + pub public_key: TYPES::SignatureKey, + + /// Membership for voting + pub membership: EpochMembership, + + /// accumulator for quorum votes + pub accumulator: + Option, QuorumCertificate2, V>>, + + /// accumulator for light client state update votes + pub state_vote_accumulator: Option>, + + /// The view which we are collecting votes for + pub view: TYPES::View, + + /// The epoch which we are collecting votes for + pub epoch: Option, + + /// Node id + pub id: u64, +} + +// Handlers for extended quorum vote accumulators +impl ExtendedQuorumVoteCollectionTaskState { + /// Take one vote and accumulate it. Returns the certs once formed. + async fn handle_vote_event( + &mut self, + event: Arc>, + sender: &Sender>>, + ) -> Result>> { + match event.as_ref() { + HotShotEvent::ExtendedQuorumVoteRecv(vote) => self.accumulate_vote(vote, sender).await, + _ => Ok(None), + } + } + + /// Accumulate a vote and return the certificates if formed + async fn accumulate_vote( + &mut self, + vote: &ExtendedQuorumVote, + event_stream: &Sender>>, + ) -> Result>> { + let ExtendedQuorumVote { vote, state_vote } = vote; + + ensure!( + vote.leader(&self.membership).await? == self.public_key, + info!("Received vote for a view in which we were not the leader.") + ); + + ensure!( + vote.view_number() == self.view, + error!( + "Vote view does not match! vote view is {} current view is {}. This vote should not have been passed to this accumulator.", + *vote.view_number(), + *self.view + ) + ); + + let accumulator = self.accumulator.as_mut().context(warn!( + "No accumulator to handle extended quorum vote with. This shouldn't happen." + ))?; + + let state_vote_accumulator = self.state_vote_accumulator.as_mut().context(warn!( + "No accumulator to handle light client state update vote with. This shouldn't happen." + ))?; + + let key = vote.signing_key(); + let epoch = state_vote.epoch; + let stake_table_entry = + QuorumCertificate2::::stake_table_entry(&self.membership, &key) + .await + .ok_or(error!("Cannot find the voting key {:?}", key))?; + let stake_table = QuorumCertificate2::::stake_table(&self.membership).await; + let threshold = QuorumCertificate2::::threshold(&self.membership).await; + + match ( + accumulator.accumulate(vote, self.membership.clone()).await, + state_vote_accumulator.accumulate( + epoch, + state_vote, + &stake_table_entry.state_ver_key, + &stake_table, + threshold.into(), + ), + ) { + (None, None) => Ok(None), + (Some(cert), Some(state_cert)) => { + let eqc = ExtendedQuorumCertificate { + qc: cert, + state_cert, + }; + + tracing::debug!("Certificate Formed! {:?}", eqc); + + broadcast_event( + Arc::new(HotShotEvent::ExtendedQcFormed(Left(eqc.clone()))), + event_stream, + ) + .await; + self.accumulator = None; + + Ok(Some(eqc)) + }, + _ => Err(error!( + "Only one certificate formed during epoch transition, this should not happen." + )), + } + } +} + +async fn create_extended_quorum_vote_accumulator( + info: &AccumulatorInfo, + event: Arc>, + sender: &Sender>>, + upgrade_lock: UpgradeLock, +) -> Result> { + let new_accumulator = + VoteAccumulator::, QuorumCertificate2, V> { + vote_outcomes: HashMap::new(), + signers: HashMap::new(), + phantom: PhantomData, + upgrade_lock, + }; + let state_vote_accumulator = LightClientStateUpdateVoteAccumulator { + vote_outcomes: HashMap::new(), + }; + + let mut state = ExtendedQuorumVoteCollectionTaskState:: { + membership: info.membership.clone(), + public_key: info.public_key.clone(), + accumulator: Some(new_accumulator), + state_vote_accumulator: Some(state_vote_accumulator), + view: info.view, + epoch: info.membership.epoch, + id: info.id, + }; + + state.handle_vote_event(Arc::clone(&event), sender).await?; + + Ok(state) +} + +/// A helper function that handles extended quorum vote collection +/// +/// # Errors +/// If we fail to handle the vote +#[allow(clippy::too_many_arguments)] +pub async fn handle_extended_quorum_vote( + collectors: &mut ExtendedQuorumVoteCollectorsMap, + vote: &ExtendedQuorumVote, + public_key: TYPES::SignatureKey, + membership: &EpochMembership, + id: u64, + event: &Arc>, + event_stream: &Sender>>, + upgrade_lock: &UpgradeLock, +) -> Result<()> { + match collectors.entry(vote.view_number()) { + Entry::Vacant(entry) => { + tracing::debug!( + "Starting extended quorum vote handle for view {:?}", + vote.view_number() + ); + let info = AccumulatorInfo { + public_key, + membership: membership.clone(), + view: vote.view_number(), + id, + }; + let collector = create_extended_quorum_vote_accumulator( + &info, + Arc::clone(event), + event_stream, + upgrade_lock.clone(), + ) + .await?; + + entry.insert(collector); + + Ok(()) + }, + Entry::Occupied(mut entry) => { + // handle the vote, and garbage collect if the vote collector is finished + if entry + .get_mut() + .handle_vote_event(Arc::clone(event), event_stream) + .await? + .is_some() + { + entry.remove(); + *collectors = collectors.split_off(&vote.view_number()); + } + + Ok(()) + }, + } +} diff --git a/hotshot-testing/src/helpers.rs b/hotshot-testing/src/helpers.rs index c28cc276b3..dfac74140b 100644 --- a/hotshot-testing/src/helpers.rs +++ b/hotshot-testing/src/helpers.rs @@ -128,10 +128,11 @@ pub async fn build_system_handle_from_launcher< let is_da = node_id < hotshot_config.da_staked_committee_size as u64; // We assign node's public key and stake value rather than read from config file since it's a test - let validator_config: ValidatorConfig = + let validator_config: ValidatorConfig = ValidatorConfig::generated_from_seed_indexed([0u8; 32], node_id, 1, is_da); let private_key = validator_config.private_key.clone(); let public_key = validator_config.public_key.clone(); + let state_private_key = validator_config.state_private_key.clone(); let memberships = Arc::new(RwLock::new(TYPES::Membership::new( hotshot_config.known_nodes_with_stake.clone(), @@ -144,6 +145,7 @@ pub async fn build_system_handle_from_launcher< let (c, s, r) = SystemContext::init( public_key, private_key, + state_private_key, node_id, hotshot_config, coordinator, diff --git a/hotshot-testing/src/spinning_task.rs b/hotshot-testing/src/spinning_task.rs index ac92dc2287..9c039ef312 100644 --- a/hotshot-testing/src/spinning_task.rs +++ b/hotshot-testing/src/spinning_task.rs @@ -29,7 +29,9 @@ use hotshot_types::{ data::Leaf2, event::Event, message::convert_proposal, - simple_certificate::{NextEpochQuorumCertificate2, QuorumCertificate2}, + simple_certificate::{ + LightClientStateUpdateCertificate, NextEpochQuorumCertificate2, QuorumCertificate2, + }, traits::{ network::{AsyncGenerator, ConnectedNetwork}, node_implementation::{ConsensusTime, NodeImplementation, NodeType, Versions}, @@ -82,6 +84,8 @@ pub struct SpinningTask< pub(crate) restart_contexts: HashMap>, /// Generate network channel for restart nodes pub(crate) channel_generator: AsyncGenerator>, + /// The light client state update certificate + pub(crate) state_cert: LightClientStateUpdateCertificate, } #[async_trait] @@ -177,6 +181,7 @@ where BTreeMap::new(), BTreeMap::new(), None, + self.state_cert.clone(), ); // We assign node's public key and stake value rather than read from config file since it's a test let validator_config = @@ -260,6 +265,10 @@ where ) .await, ); + let state_cert = read_storage + .state_cert_cloned() + .await + .unwrap_or(LightClientStateUpdateCertificate::genesis()); let saved_proposals = read_storage.proposals_cloned().await; let mut vid_shares = BTreeMap::new(); for (view, hash_map) in read_storage.vids_cloned().await { @@ -283,6 +292,7 @@ where saved_proposals, vid_shares, decided_upgrade_certificate, + state_cert, ); // We assign node's public key and stake value rather than read from config file since it's a test let validator_config = ValidatorConfig::generated_from_seed_indexed( diff --git a/hotshot-testing/src/test_builder.rs b/hotshot-testing/src/test_builder.rs index 31c69768e7..5ceed0fae5 100644 --- a/hotshot-testing/src/test_builder.rs +++ b/hotshot-testing/src/test_builder.rs @@ -59,12 +59,12 @@ pub struct TimingData { } pub fn default_hotshot_config( - known_nodes_with_stake: Vec>, - known_da_nodes: Vec>, + known_nodes_with_stake: Vec>, + known_da_nodes: Vec>, num_bootstrap_nodes: usize, epoch_height: u64, epoch_start_block: u64, -) -> HotShotConfig { +) -> HotShotConfig { HotShotConfig { start_threshold: (1, 1), num_nodes_with_stake: NonZeroUsize::new(known_nodes_with_stake.len()).unwrap(), @@ -96,15 +96,12 @@ pub fn default_hotshot_config( pub fn gen_node_lists( num_staked_nodes: u64, num_da_nodes: u64, -) -> ( - Vec>, - Vec>, -) { +) -> (Vec>, Vec>) { let mut staked_nodes = Vec::new(); let mut da_nodes = Vec::new(); for n in 0..num_staked_nodes { - let validator_config: ValidatorConfig = + let validator_config: ValidatorConfig = ValidatorConfig::generated_from_seed_indexed([0u8; 32], n, 1, n < num_da_nodes); let peer_config = validator_config.public_config(); @@ -125,7 +122,7 @@ pub struct TestDescription, V: Ver /// /// Note: this is not the same as the `HotShotConfig` passed to test nodes for `SystemContext::init`; /// those configs are instead provided by the resource generators in the test launcher. - pub test_config: HotShotConfig, + pub test_config: HotShotConfig, /// Whether to skip initializing nodes that will start late, which will catch up later with /// `HotShotInitializer::from_reload` in the spinning task. pub skip_late: bool, @@ -235,7 +232,7 @@ pub async fn create_test_handle< node_id: u64, network: Network, memberships: Arc>, - config: HotShotConfig, + config: HotShotConfig, storage: I::Storage, marketplace_config: MarketplaceConfig, ) -> SystemContextHandle { @@ -255,13 +252,14 @@ pub async fn create_test_handle< // See whether or not we should be DA let is_da = node_id < config.da_staked_committee_size as u64; - let validator_config: ValidatorConfig = + let validator_config: ValidatorConfig = ValidatorConfig::generated_from_seed_indexed([0u8; 32], node_id, 1, is_da); // Get key pair for certificate aggregation let private_key = validator_config.private_key.clone(); let public_key = validator_config.public_key.clone(); let membership_coordinator = EpochMembershipCoordinator::new(memberships, config.epoch_height); + let state_private_key = validator_config.state_private_key.clone(); let behaviour = (metadata.behaviour)(node_id); match behaviour { @@ -271,6 +269,7 @@ pub async fn create_test_handle< .spawn_twin_handles( public_key, private_key, + state_private_key, node_id, config, membership_coordinator, @@ -290,6 +289,7 @@ pub async fn create_test_handle< .spawn_handle( public_key, private_key, + state_private_key, node_id, config, membership_coordinator, @@ -305,6 +305,7 @@ pub async fn create_test_handle< let hotshot = SystemContext::::new( public_key, private_key, + state_private_key, node_id, config, membership_coordinator, @@ -561,7 +562,7 @@ where let da_staked_committee_size = test_config.da_staked_committee_size; let validator_config = Rc::new(move |node_id| { - ValidatorConfig::::generated_from_seed_indexed( + ValidatorConfig::::generated_from_seed_indexed( [0u8; 32], node_id, 1, @@ -579,7 +580,7 @@ where view_sync_timeout, } = timing_data; // TODO this should really be using the timing config struct - let mod_hotshot_config = move |hotshot_config: &mut HotShotConfig| { + let mod_hotshot_config = move |hotshot_config: &mut HotShotConfig| { hotshot_config.next_view_timeout = next_view_timeout; hotshot_config.builder_timeout = builder_timeout; hotshot_config.data_request_delay = data_request_delay; diff --git a/hotshot-testing/src/test_launcher.rs b/hotshot-testing/src/test_launcher.rs index 35c77d64de..1290321868 100644 --- a/hotshot-testing/src/test_launcher.rs +++ b/hotshot-testing/src/test_launcher.rs @@ -35,9 +35,9 @@ pub struct ResourceGenerators>, /// configuration used to generate each hotshot node - pub hotshot_config: Generator>, + pub hotshot_config: Generator>, /// config that contains the signature keys - pub validator_config: Generator>, + pub validator_config: Generator>, /// generate a new marketplace config for each node pub marketplace_config: Generator>, } @@ -67,10 +67,7 @@ impl, V: Versions> TestLau } /// Modifies the config used when generating nodes with `f` #[must_use] - pub fn map_hotshot_config( - mut self, - f: impl Fn(&mut HotShotConfig) + 'static, - ) -> Self { + pub fn map_hotshot_config(mut self, f: impl Fn(&mut HotShotConfig) + 'static) -> Self { let mut test_config = self.metadata.test_config.clone(); f(&mut test_config); diff --git a/hotshot-testing/src/test_runner.rs b/hotshot-testing/src/test_runner.rs index d640050dae..e993503dd6 100644 --- a/hotshot-testing/src/test_runner.rs +++ b/hotshot-testing/src/test_runner.rs @@ -33,7 +33,7 @@ use hotshot_types::{ data::Leaf2, drb::INITIAL_DRB_RESULT, epoch_membership::EpochMembershipCoordinator, - simple_certificate::QuorumCertificate2, + simple_certificate::{LightClientStateUpdateCertificate, QuorumCertificate2}, traits::{ election::Membership, network::ConnectedNetwork, @@ -199,6 +199,7 @@ where async_delay_config: launcher.metadata.async_delay_config, restart_contexts: HashMap::new(), channel_generator: launcher.resource_generators.channel_generator, + state_cert: LightClientStateUpdateCertificate::::genesis(), }; let spinning_task = TestTask::>::new( spinning_task_state, @@ -589,8 +590,8 @@ where network: Network, memberships: TYPES::Membership, initializer: HotShotInitializer, - config: HotShotConfig, - validator_config: ValidatorConfig, + config: HotShotConfig, + validator_config: ValidatorConfig, storage: I::Storage, marketplace_config: MarketplaceConfig, ) -> Arc> { @@ -598,10 +599,12 @@ where let private_key = validator_config.private_key.clone(); let public_key = validator_config.public_key.clone(); let epoch_height = config.epoch_height; + let state_private_key = validator_config.state_private_key.clone(); SystemContext::new( public_key, private_key, + state_private_key, node_id, config, EpochMembershipCoordinator::new(Arc::new(RwLock::new(memberships)), epoch_height), @@ -623,8 +626,8 @@ where network: Network, memberships: Arc>, initializer: HotShotInitializer, - config: HotShotConfig, - validator_config: ValidatorConfig, + config: HotShotConfig, + validator_config: ValidatorConfig, storage: I::Storage, marketplace_config: MarketplaceConfig, internal_channel: ( @@ -637,10 +640,12 @@ where let private_key = validator_config.private_key.clone(); let public_key = validator_config.public_key.clone(); let epoch_height = config.epoch_height; + let state_private_key = validator_config.state_private_key.clone(); SystemContext::new_from_channels( public_key, private_key, + state_private_key, node_id, config, EpochMembershipCoordinator::new(memberships, epoch_height), @@ -676,7 +681,7 @@ pub struct LateNodeContextParameters, + pub config: HotShotConfig, /// The marketplace config for this node. pub marketplace_config: MarketplaceConfig, diff --git a/hotshot-testing/tests/tests_3/memory_network.rs b/hotshot-testing/tests/tests_3/memory_network.rs index 875e5b40e9..d66924df52 100644 --- a/hotshot-testing/tests/tests_3/memory_network.rs +++ b/hotshot-testing/tests/tests_3/memory_network.rs @@ -25,7 +25,7 @@ use hotshot_example_types::{ use hotshot_types::{ data::{EpochNumber, ViewNumber}, message::{DataMessage, Message, MessageKind, UpgradeLock}, - signature_key::{BLSPubKey, BuilderKey}, + signature_key::{BLSPubKey, BuilderKey, SchnorrPubKey}, traits::{ network::{BroadcastDelay, ConnectedNetwork, TestableNetworkingImplementation, Topic}, node_implementation::{ConsensusTime, NodeType}, @@ -63,6 +63,7 @@ impl NodeType for Test { type InstanceState = TestInstanceState; type Membership = StaticCommittee; type BuilderSignatureKey = BuilderKey; + type StateSignatureKey = SchnorrPubKey; } #[derive(Clone, Debug, Deserialize, Serialize, Hash, PartialEq, Eq)] diff --git a/hotshot-types/Cargo.toml b/hotshot-types/Cargo.toml index e65ea80623..c1113343ef 100644 --- a/hotshot-types/Cargo.toml +++ b/hotshot-types/Cargo.toml @@ -29,7 +29,9 @@ dyn-clone = "1.0.17" either = { workspace = true } futures = { workspace = true, features = ["alloc"] } hotshot-utils = { workspace = true } +jf-crhf = { workspace = true } jf-pcs = { workspace = true } +jf-rescue = { workspace = true } jf-signature = { workspace = true, features = ["bls", "schnorr"] } jf-utils = { workspace = true } jf-vid = { workspace = true } diff --git a/hotshot-types/src/consensus.rs b/hotshot-types/src/consensus.rs index c85fce8b7c..925d9b649b 100644 --- a/hotshot-types/src/consensus.rs +++ b/hotshot-types/src/consensus.rs @@ -27,7 +27,10 @@ use crate::{ error::HotShotError, event::{HotShotAction, LeafInfo}, message::{Proposal, UpgradeLock}, - simple_certificate::{DaCertificate2, NextEpochQuorumCertificate2, QuorumCertificate2}, + simple_certificate::{ + DaCertificate2, LightClientStateUpdateCertificate, NextEpochQuorumCertificate2, + QuorumCertificate2, + }, traits::{ block_contents::BuilderFee, metrics::{Counter, Gauge, Histogram, Metrics, NoMetrics}, @@ -330,6 +333,9 @@ pub struct Consensus { /// Tables for the DRB seeds and results. pub drb_seeds_and_results: DrbSeedsAndResults, + + /// The light client state update certificate + pub state_cert: LightClientStateUpdateCertificate, } /// This struct holds a payload and its metadata @@ -431,6 +437,7 @@ impl Consensus { next_epoch_high_qc: Option>, metrics: Arc, epoch_height: u64, + state_cert: LightClientStateUpdateCertificate, ) -> Self { Consensus { validated_state_map, @@ -449,6 +456,7 @@ impl Consensus { metrics, epoch_height, drb_seeds_and_results: DrbSeedsAndResults::new(), + state_cert, } } @@ -477,6 +485,11 @@ impl Consensus { &self.high_qc } + /// Get the light client state certificate + pub fn state_cert(&self) -> &LightClientStateUpdateCertificate { + &self.state_cert + } + /// Get the next epoch high QC. pub fn next_epoch_high_qc(&self) -> Option<&NextEpochQuorumCertificate2> { self.next_epoch_high_qc.as_ref() @@ -793,6 +806,35 @@ impl Consensus { Ok(()) } + /// Update the light client state update certificate if given a newer one. + /// # Errors + /// Can return an error when the provided state_cert is not newer than the existing entry. + pub fn update_state_cert( + &mut self, + state_cert: LightClientStateUpdateCertificate, + ) -> Result<()> { + ensure!( + state_cert.epoch > self.state_cert.epoch || state_cert == self.state_cert, + debug!("Light client state update certification with an equal or higher epoch exists.") + ); + tracing::debug!("Updating light client state update certification"); + self.state_cert = state_cert; + + Ok(()) + } + + /// Update the light client state update certificate if given a newer one. + /// # Errors + /// Can return an error when the provided state_cert is not newer than the existing entry. + pub fn update_high_qc_and_state_cert( + &mut self, + high_qc: QuorumCertificate2, + state_cert: LightClientStateUpdateCertificate, + ) -> Result<()> { + self.update_high_qc(high_qc)?; + self.update_state_cert(state_cert) + } + /// Add a new entry to the vid_shares map. pub fn update_vid_shares( &mut self, diff --git a/hotshot-types/src/epoch_membership.rs b/hotshot-types/src/epoch_membership.rs index 3a316cf033..7eb55f4603 100644 --- a/hotshot-types/src/epoch_membership.rs +++ b/hotshot-types/src/epoch_membership.rs @@ -240,7 +240,7 @@ impl EpochMembership { } /// Get all participants in the committee (including their stake) for a specific epoch - pub async fn stake_table(&self) -> Vec> { + pub async fn stake_table(&self) -> Vec> { self.coordinator .membership .read() @@ -249,7 +249,7 @@ impl EpochMembership { } /// Get all participants in the committee (including their stake) for a specific epoch - pub async fn da_stake_table(&self) -> Vec> { + pub async fn da_stake_table(&self) -> Vec> { self.coordinator .membership .read() @@ -295,10 +295,7 @@ impl EpochMembership { /// Get the stake table entry for a public key, returns `None` if the /// key is not in the table for a specific epoch - pub async fn stake( - &self, - pub_key: &TYPES::SignatureKey, - ) -> Option> { + pub async fn stake(&self, pub_key: &TYPES::SignatureKey) -> Option> { self.coordinator .membership .read() @@ -308,10 +305,7 @@ impl EpochMembership { /// Get the DA stake table entry for a public key, returns `None` if the /// key is not in the table for a specific epoch - pub async fn da_stake( - &self, - pub_key: &TYPES::SignatureKey, - ) -> Option> { + pub async fn da_stake(&self, pub_key: &TYPES::SignatureKey) -> Option> { self.coordinator .membership .read() diff --git a/hotshot-types/src/hotshot_config_file.rs b/hotshot-types/src/hotshot_config_file.rs index 7eca53be10..56c68556d2 100644 --- a/hotshot-types/src/hotshot_config_file.rs +++ b/hotshot-types/src/hotshot_config_file.rs @@ -10,8 +10,8 @@ use url::Url; use vec1::Vec1; use crate::{ - constants::REQUEST_DATA_DELAY, traits::signature_key::SignatureKey, - upgrade_config::UpgradeConfig, HotShotConfig, PeerConfig, ValidatorConfig, + constants::REQUEST_DATA_DELAY, upgrade_config::UpgradeConfig, HotShotConfig, NodeType, + PeerConfig, ValidatorConfig, }; /// Default builder URL, used as placeholder @@ -22,7 +22,7 @@ fn default_builder_urls() -> Vec1 { /// Holds configuration for a `HotShot` #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(bound(deserialize = ""))] -pub struct HotShotConfigFile { +pub struct HotShotConfigFile { /// The proportion of nodes required before the orchestrator issues the ready signal, /// expressed as (numerator, denominator) pub start_threshold: (u64, u64), @@ -30,10 +30,10 @@ pub struct HotShotConfigFile { pub num_nodes_with_stake: NonZeroUsize, #[serde(skip)] /// The known nodes' public key and stake value - pub known_nodes_with_stake: Vec>, + pub known_nodes_with_stake: Vec>, #[serde(skip)] /// The known DA nodes' public key and stake values - pub known_da_nodes: Vec>, + pub known_da_nodes: Vec>, /// Number of staking DA nodes pub staked_da_nodes: usize, /// Number of fixed leaders for GPU VID @@ -59,8 +59,8 @@ pub struct HotShotConfigFile { pub epoch_start_block: u64, } -impl From> for HotShotConfig { - fn from(val: HotShotConfigFile) -> Self { +impl From> for HotShotConfig { + fn from(val: HotShotConfigFile) -> Self { HotShotConfig { start_threshold: val.start_threshold, num_nodes_with_stake: val.num_nodes_with_stake, @@ -90,7 +90,7 @@ impl From> for HotShotConfig { } } -impl HotShotConfigFile { +impl HotShotConfigFile { /// Creates a new `HotShotConfigFile` with 5 nodes and 10 DA nodes. /// /// # Panics @@ -104,7 +104,7 @@ impl HotShotConfigFile { let gen_known_nodes_with_stake = (0..10) .map(|node_id| { - let mut cur_validator_config: ValidatorConfig = + let mut cur_validator_config: ValidatorConfig = ValidatorConfig::generated_from_seed_indexed([0u8; 32], node_id, 1, false); if node_id < staked_da_nodes as u64 { diff --git a/hotshot-types/src/lib.rs b/hotshot-types/src/lib.rs index 59b2d65d22..dab204849e 100644 --- a/hotshot-types/src/lib.rs +++ b/hotshot-types/src/lib.rs @@ -9,9 +9,11 @@ use std::{fmt::Debug, future::Future, num::NonZeroUsize, pin::Pin, time::Duratio use bincode::Options; use displaydoc::Display; -use light_client::StateVerKey; use tracing::error; -use traits::{node_implementation::NodeType, signature_key::SignatureKey}; +use traits::{ + node_implementation::NodeType, + signature_key::{SignatureKey, StateSignatureKey}, +}; use url::Url; use vec1::Vec1; @@ -67,20 +69,22 @@ where #[derive(Clone, Debug, Display)] /// config for validator, including public key, private key, stake value -pub struct ValidatorConfig { +pub struct ValidatorConfig { /// The validator's public key and stake value - pub public_key: KEY, + pub public_key: TYPES::SignatureKey, /// The validator's private key, should be in the mempool, not public - pub private_key: KEY::PrivateKey, + pub private_key: ::PrivateKey, /// The validator's stake pub stake_value: u64, - /// the validator's key pairs for state signing/verification - pub state_key_pair: light_client::StateKeyPair, + /// the validator's key pairs for state verification + pub state_public_key: TYPES::StateSignatureKey, + /// the validator's key pairs for state verification + pub state_private_key: ::StatePrivateKey, /// Whether or not this validator is DA pub is_da: bool, } -impl ValidatorConfig { +impl ValidatorConfig { /// generate validator config from input seed, index, stake value, and whether it's DA #[must_use] pub fn generated_from_seed_indexed( @@ -89,27 +93,30 @@ impl ValidatorConfig { stake_value: u64, is_da: bool, ) -> Self { - let (public_key, private_key) = KEY::generated_from_seed_indexed(seed, index); - let state_key_pairs = light_client::StateKeyPair::generate_from_seed_indexed(seed, index); + let (public_key, private_key) = + TYPES::SignatureKey::generated_from_seed_indexed(seed, index); + let (state_public_key, state_private_key) = + TYPES::StateSignatureKey::generated_from_seed_indexed(seed, index); Self { public_key, private_key, stake_value, - state_key_pair: state_key_pairs, + state_public_key, + state_private_key, is_da, } } /// get the public config of the validator - pub fn public_config(&self) -> PeerConfig { + pub fn public_config(&self) -> PeerConfig { PeerConfig { stake_table_entry: self.public_key.stake_table_entry(self.stake_value), - state_ver_key: self.state_key_pair.0.ver_key(), + state_ver_key: self.state_public_key.clone(), } } } -impl Default for ValidatorConfig { +impl Default for ValidatorConfig { fn default() -> Self { Self::generated_from_seed_indexed([0u8; 32], 0, 1, true) } @@ -118,16 +125,16 @@ impl Default for ValidatorConfig { #[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Display, PartialEq, Eq, Hash)] #[serde(bound(deserialize = ""))] /// structure of peers' config, including public key, stake value, and state key. -pub struct PeerConfig { +pub struct PeerConfig { ////The peer's public key and stake value. The key is the BLS Public Key used to /// verify Stake Holder in the application layer. - pub stake_table_entry: KEY::StakeTableEntry, + pub stake_table_entry: ::StakeTableEntry, //// The peer's state public key. This is the Schnorr Public Key used to /// verify HotShot state in the state-prover. - pub state_ver_key: StateVerKey, + pub state_ver_key: TYPES::StateSignatureKey, } -impl PeerConfig { +impl PeerConfig { /// Serialize a peer's config to bytes pub fn to_bytes(config: &Self) -> Vec { let x = bincode_opts().serialize(config); @@ -144,7 +151,7 @@ impl PeerConfig { /// # Errors /// Will return `None` if deserialization fails pub fn from_bytes(bytes: &[u8]) -> Option { - let x: Result, _> = bincode_opts().deserialize(bytes); + let x: Result, _> = bincode_opts().deserialize(bytes); match x { Ok(pub_key) => Some(pub_key), Err(e) => { @@ -155,9 +162,9 @@ impl PeerConfig { } } -impl Default for PeerConfig { +impl Default for PeerConfig { fn default() -> Self { - let default_validator_config = ValidatorConfig::::default(); + let default_validator_config = ValidatorConfig::::default(); default_validator_config.public_config() } } @@ -166,8 +173,8 @@ pub struct StakeTableEntries( pub Vec<<::SignatureKey as SignatureKey>::StakeTableEntry>, ); -impl From>> for StakeTableEntries { - fn from(peers: Vec>) -> Self { +impl From>> for StakeTableEntries { + fn from(peers: Vec>) -> Self { Self( peers .into_iter() @@ -180,7 +187,7 @@ impl From>> for StakeTableE /// Holds configuration for a `HotShot` #[derive(Clone, derive_more::Debug, serde::Serialize, serde::Deserialize)] #[serde(bound(deserialize = ""))] -pub struct HotShotConfig { +pub struct HotShotConfig { /// The proportion of nodes required before the orchestrator issues the ready signal, /// expressed as (numerator, denominator) pub start_threshold: (u64, u64), @@ -188,9 +195,9 @@ pub struct HotShotConfig { // Earlier it was total_nodes pub num_nodes_with_stake: NonZeroUsize, /// List of known node's public keys and stake value for certificate aggregation, serving as public parameter - pub known_nodes_with_stake: Vec>, + pub known_nodes_with_stake: Vec>, /// All public keys known to be DA nodes - pub known_da_nodes: Vec>, + pub known_da_nodes: Vec>, /// List of DA committee (staking)nodes for static DA committee pub da_staked_committee_size: usize, /// Number of fixed leaders for GPU VID, normally it will be 0, it's only used when running GPU VID @@ -229,7 +236,7 @@ pub struct HotShotConfig { pub epoch_start_block: u64, } -impl HotShotConfig { +impl HotShotConfig { /// Update a hotshot config to have a view-based upgrade. pub fn set_view_upgrade(&mut self, view: u64) { self.start_proposing_view = view; diff --git a/hotshot-types/src/light_client.rs b/hotshot-types/src/light_client.rs index 7bafaee784..460df94f64 100644 --- a/hotshot-types/src/light_client.rs +++ b/hotshot-types/src/light_client.rs @@ -11,6 +11,8 @@ use std::collections::HashMap; use ark_ed_on_bn254::EdwardsConfig as Config; use ark_ff::PrimeField; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use jf_crhf::CRHF; +use jf_rescue::{crhf::VariableLengthRescueCRHF, RescueError, RescueParameter}; use jf_signature::schnorr; use primitive_types::U256; use rand::SeedableRng; @@ -22,6 +24,8 @@ use tagged_base64::tagged; pub type CircuitField = ark_ed_on_bn254::Fq; /// Concrete type for light client state pub type LightClientState = GenericLightClientState; +/// Concreate type for light client state message to sign +pub type LightClientStateMsg = GenericLightClientStateMsg; /// Concrete type for stake table state pub type StakeTableState = GenericStakeTableState; /// Signature scheme @@ -77,33 +81,49 @@ pub struct StateSignaturesBundle { )] pub struct GenericLightClientState { /// Current view number - pub view_number: usize, + pub view_number: u64, /// Current block height - pub block_height: usize, + pub block_height: u64, /// Root of the block commitment tree pub block_comm_root: F, } -impl From> for [F; 3] { +pub type GenericLightClientStateMsg = [F; 3]; + +impl From> for GenericLightClientStateMsg { fn from(state: GenericLightClientState) -> Self { [ - F::from(state.view_number as u64), - F::from(state.block_height as u64), + F::from(state.view_number), + F::from(state.block_height), state.block_comm_root, ] } } -impl From<&GenericLightClientState> for [F; 3] { +impl From<&GenericLightClientState> for GenericLightClientStateMsg { fn from(state: &GenericLightClientState) -> Self { [ - F::from(state.view_number as u64), - F::from(state.block_height as u64), + F::from(state.view_number), + F::from(state.block_height), state.block_comm_root, ] } } +impl GenericLightClientState { + pub fn new( + view_number: u64, + block_height: u64, + block_comm_root: &[u8], + ) -> anyhow::Result { + Ok(Self { + view_number, + block_height, + block_comm_root: hash_bytes_to_field(block_comm_root)?, + }) + } +} + /// Stake table state #[tagged("STAKE_TABLE_STATE")] #[derive( @@ -255,3 +275,13 @@ impl GenericPublicInput { self.0[6] } } + +pub fn hash_bytes_to_field(bytes: &[u8]) -> Result { + // make sure that `mod_order` won't happen. + let bytes_len = ((::MODULUS_BIT_SIZE + 7) / 8 - 1) as usize; + let elem = bytes + .chunks(bytes_len) + .map(F::from_le_bytes_mod_order) + .collect::>(); + Ok(VariableLengthRescueCRHF::<_, 1>::evaluate(elem)?[0]) +} diff --git a/hotshot-types/src/message.rs b/hotshot-types/src/message.rs index 8f7ac65386..aef157adf7 100644 --- a/hotshot-types/src/message.rs +++ b/hotshot-types/src/message.rs @@ -33,15 +33,15 @@ use crate::{ epoch_membership::EpochMembership, request_response::ProposalRequestPayload, simple_certificate::{ - DaCertificate, DaCertificate2, NextEpochQuorumCertificate2, QuorumCertificate2, - UpgradeCertificate, ViewSyncCommitCertificate, ViewSyncCommitCertificate2, - ViewSyncFinalizeCertificate, ViewSyncFinalizeCertificate2, ViewSyncPreCommitCertificate, - ViewSyncPreCommitCertificate2, + DaCertificate, DaCertificate2, ExtendedQuorumCertificate, NextEpochQuorumCertificate2, + QuorumCertificate2, UpgradeCertificate, ViewSyncCommitCertificate, + ViewSyncCommitCertificate2, ViewSyncFinalizeCertificate, ViewSyncFinalizeCertificate2, + ViewSyncPreCommitCertificate, ViewSyncPreCommitCertificate2, }, simple_vote::{ - DaVote, DaVote2, HasEpoch, QuorumVote, QuorumVote2, TimeoutVote, TimeoutVote2, UpgradeVote, - ViewSyncCommitVote, ViewSyncCommitVote2, ViewSyncFinalizeVote, ViewSyncFinalizeVote2, - ViewSyncPreCommitVote, ViewSyncPreCommitVote2, + DaVote, DaVote2, ExtendedQuorumVote, HasEpoch, QuorumVote, QuorumVote2, TimeoutVote, + TimeoutVote2, UpgradeVote, ViewSyncCommitVote, ViewSyncCommitVote2, ViewSyncFinalizeVote, + ViewSyncFinalizeVote2, ViewSyncPreCommitVote, ViewSyncPreCommitVote2, }, traits::{ election::Membership, @@ -243,10 +243,13 @@ pub enum GeneralConsensusMessage { /// Message for the next leader containing our highest QC ExtendedQc( - QuorumCertificate2, + ExtendedQuorumCertificate, NextEpochQuorumCertificate2, ), + /// Message with a epoch transition quorum vote + ExtendedQuorumVote(ExtendedQuorumVote), + /// Message with a view sync pre-commit vote ViewSyncPreCommitVote2(ViewSyncPreCommitVote2), @@ -372,8 +375,9 @@ impl SequencingMessage { }, GeneralConsensusMessage::UpgradeProposal(message) => message.data.view_number(), GeneralConsensusMessage::UpgradeVote(message) => message.view_number(), - GeneralConsensusMessage::HighQc(qc) - | GeneralConsensusMessage::ExtendedQc(qc, _) => qc.view_number(), + GeneralConsensusMessage::HighQc(qc) => qc.view_number(), + GeneralConsensusMessage::ExtendedQc(qc, _) => qc.view_number(), + GeneralConsensusMessage::ExtendedQuorumVote(message) => message.view_number(), } }, SequencingMessage::Da(da_message) => { @@ -443,8 +447,9 @@ impl SequencingMessage { }, GeneralConsensusMessage::UpgradeProposal(message) => message.data.epoch(), GeneralConsensusMessage::UpgradeVote(message) => message.epoch(), - GeneralConsensusMessage::HighQc(qc) - | GeneralConsensusMessage::ExtendedQc(qc, _) => qc.epoch(), + GeneralConsensusMessage::HighQc(qc) => qc.epoch(), + GeneralConsensusMessage::ExtendedQc(qc, _) => qc.epoch(), + GeneralConsensusMessage::ExtendedQuorumVote(message) => message.epoch(), } }, SequencingMessage::Da(da_message) => { diff --git a/hotshot-types/src/network.rs b/hotshot-types/src/network.rs index 3a5dd4fe31..dbcc80f9fe 100644 --- a/hotshot-types/src/network.rs +++ b/hotshot-types/src/network.rs @@ -19,9 +19,7 @@ use crate::{ ORCHESTRATOR_DEFAULT_TRANSACTION_SIZE, REQUEST_DATA_DELAY, }, hotshot_config_file::HotShotConfigFile, - light_client::StateVerKey, - traits::signature_key::SignatureKey, - HotShotConfig, ValidatorConfig, + HotShotConfig, NodeType, ValidatorConfig, }; /// Configuration describing a libp2p node @@ -74,11 +72,11 @@ pub enum BuilderType { /// Node PeerConfig keys #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] #[serde(bound(deserialize = ""))] -pub struct PeerConfigKeys { +pub struct PeerConfigKeys { /// The peer's public key - pub stake_table_key: KEY, + pub stake_table_key: TYPES::SignatureKey, /// the peer's state public key - pub state_ver_key: StateVerKey, + pub state_ver_key: TYPES::StateSignatureKey, /// the peer's stake pub stake: u64, /// whether the node is a DA node @@ -109,7 +107,7 @@ impl Default for RandomBuilderConfig { /// a network configuration #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] #[serde(bound(deserialize = ""))] -pub struct NetworkConfig { +pub struct NetworkConfig { /// number of views to run pub rounds: usize, /// whether DA membership is determined by index. @@ -142,7 +140,7 @@ pub struct NetworkConfig { /// the libp2p config pub libp2p_config: Option, /// the hotshot config - pub config: HotShotConfig, + pub config: HotShotConfig, /// The address for the Push CDN's "marshal", A.K.A. load balancer pub cdn_marshal_address: Option, /// combined network config @@ -154,7 +152,7 @@ pub struct NetworkConfig { /// random builder config pub random_builder: Option, /// The list of public keys that are allowed to connect to the orchestrator - pub public_keys: Vec>, + pub public_keys: Vec>, } /// the source of the network config @@ -165,10 +163,13 @@ pub enum NetworkConfigSource { File, } -impl NetworkConfig { +impl NetworkConfig { /// Get a temporary node index for generating a validator config #[must_use] - pub fn generate_init_validator_config(cur_node_index: u16, is_da: bool) -> ValidatorConfig { + pub fn generate_init_validator_config( + cur_node_index: u16, + is_da: bool, + ) -> ValidatorConfig { // This cur_node_index is only used for key pair generation, it's not bound with the node, // later the node with the generated key pair will get a new node_index from orchestrator. ValidatorConfig::generated_from_seed_indexed([0u8; 32], cur_node_index.into(), 1, is_da) @@ -267,7 +268,7 @@ impl NetworkConfig { } } -impl Default for NetworkConfig { +impl Default for NetworkConfig { fn default() -> Self { Self { rounds: ORCHESTRATOR_DEFAULT_NUM_ROUNDS, @@ -279,7 +280,7 @@ impl Default for NetworkConfig { manual_start_password: None, libp2p_config: None, config: HotShotConfigFile::hotshot_config_5_nodes_10_da().into(), - key_type_name: std::any::type_name::().to_string(), + key_type_name: std::any::type_name::().to_string(), cdn_marshal_address: None, combined_network_config: None, next_view_timeout: 10, @@ -299,19 +300,19 @@ impl Default for NetworkConfig { #[serde_inline_default] #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[serde(bound(deserialize = ""))] -pub struct PublicKeysFile { +pub struct PublicKeysFile { /// The list of public keys that are allowed to connect to the orchestrator /// /// If nonempty, this list becomes the stake table and is used to determine DA membership (ignoring the node's request). #[serde(default)] - pub public_keys: Vec>, + pub public_keys: Vec>, } /// a network config stored in a file #[serde_inline_default] #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[serde(bound(deserialize = ""))] -pub struct NetworkConfigFile { +pub struct NetworkConfigFile { /// number of views to run #[serde_inline_default(ORCHESTRATOR_DEFAULT_NUM_ROUNDS)] pub rounds: usize, @@ -336,7 +337,7 @@ pub struct NetworkConfigFile { pub transaction_size: usize, /// the hotshot config file #[serde(default = "HotShotConfigFile::hotshot_config_5_nodes_10_da")] - pub config: HotShotConfigFile, + pub config: HotShotConfigFile, /// The address of the Push CDN's "marshal", A.K.A. load balancer #[serde(default)] pub cdn_marshal_address: Option, @@ -353,11 +354,11 @@ pub struct NetworkConfigFile { /// /// If nonempty, this list becomes the stake table and is used to determine DA membership (ignoring the node's request). #[serde(default)] - pub public_keys: Vec>, + pub public_keys: Vec>, } -impl From> for NetworkConfig { - fn from(val: NetworkConfigFile) -> Self { +impl From> for NetworkConfig { + fn from(val: NetworkConfigFile) -> Self { NetworkConfig { rounds: val.rounds, indexed_da: val.indexed_da, @@ -378,7 +379,7 @@ impl From> for NetworkConfig { bootstrap_nodes: Vec::new(), }), config: val.config.into(), - key_type_name: std::any::type_name::().to_string(), + key_type_name: std::any::type_name::().to_string(), cdn_marshal_address: val.cdn_marshal_address, combined_network_config: val.combined_network_config, commit_sha: String::new(), diff --git a/hotshot-types/src/signature_key.rs b/hotshot-types/src/signature_key.rs index 5df67a872b..c00a2996c8 100644 --- a/hotshot-types/src/signature_key.rs +++ b/hotshot-types/src/signature_key.rs @@ -19,11 +19,14 @@ use rand_chacha::ChaCha20Rng; use tracing::instrument; use crate::{ + light_client::LightClientStateMsg, qc::{BitVectorQc, QcParams}, stake_table::StakeTableEntry, traits::{ qc::QuorumCertificateScheme, - signature_key::{BuilderSignatureKey, PrivateSignatureKey, SignatureKey}, + signature_key::{ + BuilderSignatureKey, PrivateSignatureKey, SignatureKey, StateSignatureKey, + }, }, }; @@ -185,3 +188,54 @@ impl BuilderSignatureKey for BuilderKey { (kp.ver_key(), kp.sign_key_ref().clone()) } } + +pub type SchnorrPubKey = jf_signature::schnorr::VerKey; +pub type SchnorrPrivKey = jf_signature::schnorr::SignKey; +pub type SchnorrSignatureScheme = + jf_signature::schnorr::SchnorrSignatureScheme; + +impl PrivateSignatureKey for SchnorrPrivKey { + fn to_bytes(&self) -> Vec { + self.to_bytes() + } + + fn from_bytes(bytes: &[u8]) -> anyhow::Result { + Ok(Self::from_bytes(bytes)) + } + + fn to_tagged_base64(&self) -> Result { + self.to_tagged_base64() + } +} + +impl StateSignatureKey for SchnorrPubKey { + type StatePrivateKey = SchnorrPrivKey; + + type StateSignature = jf_signature::schnorr::Signature; + + type SignError = SignatureError; + + fn sign_state( + sk: &Self::StatePrivateKey, + state: &LightClientStateMsg, + ) -> Result { + SchnorrSignatureScheme::sign(&(), sk, state, &mut rand::thread_rng()) + } + + fn verify_state_sig( + &self, + signature: &Self::StateSignature, + state: &LightClientStateMsg, + ) -> bool { + SchnorrSignatureScheme::verify(&(), self, state, signature).is_ok() + } + + fn generated_from_seed_indexed(seed: [u8; 32], index: u64) -> (Self, Self::StatePrivateKey) { + let mut hasher = blake3::Hasher::new(); + hasher.update(&seed); + hasher.update(&index.to_le_bytes()); + let new_seed = *hasher.finalize().as_bytes(); + let kp = jf_signature::schnorr::KeyPair::generate(&mut ChaCha20Rng::from_seed(new_seed)); + (kp.ver_key(), kp.sign_key()) + } +} diff --git a/hotshot-types/src/simple_certificate.rs b/hotshot-types/src/simple_certificate.rs index 2c9dbeaa63..ee9c15fe48 100644 --- a/hotshot-types/src/simple_certificate.rs +++ b/hotshot-types/src/simple_certificate.rs @@ -24,6 +24,7 @@ use serde::{Deserialize, Serialize}; use crate::{ data::serialize_signature2, epoch_membership::EpochMembership, + light_client::LightClientState, message::UpgradeLock, simple_vote::{ DaData, DaData2, HasEpoch, NextEpochQuorumData2, QuorumData, QuorumData2, QuorumMarker, @@ -33,7 +34,7 @@ use crate::{ }, traits::{ node_implementation::{ConsensusTime, NodeType, Versions}, - signature_key::SignatureKey, + signature_key::{SignatureKey, StateSignatureKey}, }, vote::{Certificate, HasViewNumber}, PeerConfig, StakeTableEntries, @@ -181,14 +182,12 @@ impl> Certificate async fn stake_table_entry( membership: &EpochMembership, pub_key: &TYPES::SignatureKey, - ) -> Option> { + ) -> Option> { membership.da_stake(pub_key).await } /// Proxy's to `Membership.da_stake_table` - async fn stake_table( - membership: &EpochMembership, - ) -> Vec> { + async fn stake_table(membership: &EpochMembership) -> Vec> { membership.da_stake_table().await } /// Proxy's to `Membership.da_total_nodes` @@ -262,14 +261,12 @@ impl> Certificate, pub_key: &TYPES::SignatureKey, - ) -> Option> { + ) -> Option> { membership.da_stake(pub_key).await } /// Proxy's to `Membership.da_stake_table` - async fn stake_table( - membership: &EpochMembership, - ) -> Vec> { + async fn stake_table(membership: &EpochMembership) -> Vec> { membership.da_stake_table().await } /// Proxy's to `Membership.da_total_nodes` @@ -349,13 +346,11 @@ impl< async fn stake_table_entry( membership: &EpochMembership, pub_key: &TYPES::SignatureKey, - ) -> Option> { + ) -> Option> { membership.stake(pub_key).await } - async fn stake_table( - membership: &EpochMembership, - ) -> Vec> { + async fn stake_table(membership: &EpochMembership) -> Vec> { membership.stake_table().await } @@ -752,3 +747,73 @@ pub type ViewSyncFinalizeCertificate2 = /// Type alias for a `UpgradeCertificate`, which is a `SimpleCertificate` of `UpgradeProposalData` pub type UpgradeCertificate = SimpleCertificate, UpgradeThreshold>; + +/// Type for light client state update certificate +#[derive(Serialize, Deserialize, Eq, Hash, PartialEq, Debug, Clone)] +pub struct LightClientStateUpdateCertificate { + /// The epoch of the light client state + pub epoch: TYPES::Epoch, + /// Light client state for epoch transition + pub light_client_state: LightClientState, + /// Signatures to the light client state + pub signatures: Vec<( + TYPES::StateSignatureKey, + ::StateSignature, + )>, +} + +impl HasViewNumber for LightClientStateUpdateCertificate { + fn view_number(&self) -> TYPES::View { + TYPES::View::new(self.light_client_state.view_number) + } +} + +impl HasEpoch for LightClientStateUpdateCertificate { + fn epoch(&self) -> Option { + Some(self.epoch) + } +} + +impl LightClientStateUpdateCertificate { + pub fn genesis() -> Self { + Self { + epoch: TYPES::Epoch::genesis(), + light_client_state: Default::default(), + signatures: vec![], + } + } +} + +/// Type for light client state update certificate +#[derive(Serialize, Deserialize, Eq, Hash, PartialEq, Debug, Clone)] +#[serde(bound = "QuorumCertificate2: Serialize + for<'a> Deserialize<'a>")] +pub struct ExtendedQuorumCertificate { + /// The quorum certificate + pub qc: QuorumCertificate2, + /// The light client state update certificate + pub state_cert: LightClientStateUpdateCertificate, +} + +impl HasViewNumber for ExtendedQuorumCertificate { + fn view_number(&self) -> TYPES::View { + self.qc.view_number() + } +} + +impl HasEpoch for ExtendedQuorumCertificate { + fn epoch(&self) -> Option { + self.qc.epoch() + } +} + +impl ExtendedQuorumCertificate { + pub async fn genesis( + validated_state: &TYPES::ValidatedState, + instance_state: &TYPES::InstanceState, + ) -> Self { + Self { + qc: QuorumCertificate2::::genesis::(validated_state, instance_state).await, + state_cert: LightClientStateUpdateCertificate::::genesis(), + } + } +} diff --git a/hotshot-types/src/simple_vote.rs b/hotshot-types/src/simple_vote.rs index 1ae78c4f20..1ce146189a 100644 --- a/hotshot-types/src/simple_vote.rs +++ b/hotshot-types/src/simple_vote.rs @@ -20,10 +20,11 @@ use vbs::version::Version; use crate::{ data::{Leaf, Leaf2, VidCommitment}, + light_client::LightClientState, message::UpgradeLock, traits::{ - node_implementation::{NodeType, Versions}, - signature_key::SignatureKey, + node_implementation::{ConsensusTime, NodeType, Versions}, + signature_key::{SignatureKey, StateSignatureKey}, }, vote::{HasViewNumber, Vote}, }; @@ -919,3 +920,46 @@ impl From> for NextEpochQuorumVote2 { } } } + +/// Type for light client state update vote +#[derive(Serialize, Deserialize, Eq, Hash, PartialEq, Debug, Clone)] +pub struct LightClientStateUpdateVote { + /// The epoch number + pub epoch: TYPES::Epoch, + /// The light client state + pub light_client_state: LightClientState, + /// The signature to the light client state + pub signature: ::StateSignature, +} + +impl HasViewNumber for LightClientStateUpdateVote { + fn view_number(&self) -> TYPES::View { + TYPES::View::new(self.light_client_state.view_number) + } +} + +impl HasEpoch for LightClientStateUpdateVote { + fn epoch(&self) -> Option { + Some(self.epoch) + } +} + +/// Type for extended quorum vote +#[derive(Serialize, Deserialize, Eq, Hash, PartialEq, Debug, Clone)] +#[serde(bound(deserialize = "QuorumVote2: for<'a> Deserialize<'a>"))] +pub struct ExtendedQuorumVote { + pub vote: QuorumVote2, + pub state_vote: LightClientStateUpdateVote, +} + +impl HasViewNumber for ExtendedQuorumVote { + fn view_number(&self) -> TYPES::View { + self.vote.view_number() + } +} + +impl HasEpoch for ExtendedQuorumVote { + fn epoch(&self) -> Option { + self.vote.epoch() + } +} diff --git a/hotshot-types/src/traits/block_contents.rs b/hotshot-types/src/traits/block_contents.rs index 781a7b7673..2b72aa0e17 100644 --- a/hotshot-types/src/traits/block_contents.rs +++ b/hotshot-types/src/traits/block_contents.rs @@ -25,6 +25,7 @@ use vbs::version::Version; use super::signature_key::BuilderSignatureKey; use crate::{ data::{Leaf2, VidCommitment}, + light_client::LightClientState, traits::{node_implementation::NodeType, states::InstanceState, ValidatedState}, utils::BuilderCommitment, }; @@ -214,4 +215,7 @@ pub trait BlockHeader: /// Get the results of the auction for this Header. Only used in post-marketplace versions fn get_auction_results(&self) -> Option; + + /// Get the light client state + fn get_light_client_state(&self, view: TYPES::View) -> anyhow::Result; } diff --git a/hotshot-types/src/traits/consensus_api.rs b/hotshot-types/src/traits/consensus_api.rs index 4c4daff9a7..0d0fe5133f 100644 --- a/hotshot-types/src/traits/consensus_api.rs +++ b/hotshot-types/src/traits/consensus_api.rs @@ -14,7 +14,7 @@ use crate::{ event::Event, traits::{ node_implementation::{NodeImplementation, NodeType}, - signature_key::SignatureKey, + signature_key::{SignatureKey, StateSignatureKey}, }, }; @@ -34,6 +34,11 @@ pub trait ConsensusApi>: Send + Sy /// Get a reference to the private key. fn private_key(&self) -> &::PrivateKey; + /// Get a reference to the light client signing key. + fn state_private_key( + &self, + ) -> &::StatePrivateKey; + /// Notify the system of an event within `hotshot-consensus`. async fn send_event(&self, event: Event); } diff --git a/hotshot-types/src/traits/election.rs b/hotshot-types/src/traits/election.rs index 382034ba3d..72e084ce42 100644 --- a/hotshot-types/src/traits/election.rs +++ b/hotshot-types/src/traits/election.rs @@ -20,15 +20,15 @@ pub trait Membership: Debug + Send + Sync { fn new( // Note: eligible_leaders is currently a hack because the DA leader == the quorum leader // but they should not have voting power. - stake_committee_members: Vec>, - da_committee_members: Vec>, + stake_committee_members: Vec>, + da_committee_members: Vec>, ) -> Self; /// Get all participants in the committee (including their stake) for a specific epoch - fn stake_table(&self, epoch: Option) -> Vec>; + fn stake_table(&self, epoch: Option) -> Vec>; /// Get all participants in the committee (including their stake) for a specific epoch - fn da_stake_table(&self, epoch: Option) -> Vec>; + fn da_stake_table(&self, epoch: Option) -> Vec>; /// Get all participants in the committee for a specific view for a specific epoch fn committee_members( @@ -57,7 +57,7 @@ pub trait Membership: Debug + Send + Sync { &self, pub_key: &TYPES::SignatureKey, epoch: Option, - ) -> Option>; + ) -> Option>; /// Get the DA stake table entry for a public key, returns `None` if the /// key is not in the table for a specific epoch @@ -65,7 +65,7 @@ pub trait Membership: Debug + Send + Sync { &self, pub_key: &TYPES::SignatureKey, epoch: Option, - ) -> Option>; + ) -> Option>; /// See if a node has stake in the committee in a specific epoch fn has_stake(&self, pub_key: &TYPES::SignatureKey, epoch: Option) -> bool; diff --git a/hotshot-types/src/traits/node_implementation.rs b/hotshot-types/src/traits/node_implementation.rs index 515e8c3647..1857db3a85 100644 --- a/hotshot-types/src/traits/node_implementation.rs +++ b/hotshot-types/src/traits/node_implementation.rs @@ -29,7 +29,7 @@ use super::{ network::{ AsyncGenerator, ConnectedNetwork, NetworkReliability, TestableNetworkingImplementation, }, - signature_key::BuilderSignatureKey, + signature_key::{BuilderSignatureKey, StateSignatureKey}, states::TestableState, storage::Storage, ValidatedState, @@ -257,6 +257,9 @@ pub trait NodeType: /// The type builder uses to sign its messages type BuilderSignatureKey: BuilderSignatureKey; + + /// The type replica uses to sign the light client state + type StateSignatureKey: StateSignatureKey; } /// Version information for HotShot diff --git a/hotshot-types/src/traits/signature_key.rs b/hotshot-types/src/traits/signature_key.rs index eaf1b33ec5..4ae8dd9457 100644 --- a/hotshot-types/src/traits/signature_key.rs +++ b/hotshot-types/src/traits/signature_key.rs @@ -23,7 +23,10 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize}; use tagged_base64::{TaggedBase64, Tb64Error}; use super::EncodeBytes; -use crate::{bundle::Bundle, traits::node_implementation::NodeType, utils::BuilderCommitment}; +use crate::{ + bundle::Bundle, light_client::LightClientStateMsg, traits::node_implementation::NodeType, + utils::BuilderCommitment, +}; /// Type representing stake table entries in a `StakeTable` pub trait StakeTableEntryType { @@ -360,3 +363,53 @@ fn aggregate_block_info_data( block_info.extend_from_slice(payload_commitment.as_ref()); block_info } + +/// Light client state signature key with minimal requirements +pub trait StateSignatureKey: + Send + + Sync + + Clone + + Sized + + Debug + + Hash + + Serialize + + for<'a> Deserialize<'a> + + PartialEq + + Eq + + Display + + for<'a> TryFrom<&'a TaggedBase64> + + Into +{ + /// The private key type + type StatePrivateKey: PrivateSignatureKey; + + /// The type of the signature + type StateSignature: Send + + Sync + + Sized + + Clone + + Debug + + Eq + + Serialize + + for<'a> Deserialize<'a> + + Hash; + + /// Type of error that can occur when signing data + type SignError: std::error::Error + Send + Sync; + + /// Sign the light client state + fn sign_state( + private_key: &Self::StatePrivateKey, + state: &LightClientStateMsg, + ) -> Result; + + /// Verify the light client state signature + fn verify_state_sig( + &self, + signature: &Self::StateSignature, + state: &LightClientStateMsg, + ) -> bool; + + /// Generate a new key pair + fn generated_from_seed_indexed(seed: [u8; 32], index: u64) -> (Self, Self::StatePrivateKey); +} diff --git a/hotshot-types/src/traits/storage.rs b/hotshot-types/src/traits/storage.rs index 7ad83f58ed..487abddcec 100644 --- a/hotshot-types/src/traits/storage.rs +++ b/hotshot-types/src/traits/storage.rs @@ -27,7 +27,8 @@ use crate::{ event::HotShotAction, message::{convert_proposal, Proposal}, simple_certificate::{ - NextEpochQuorumCertificate2, QuorumCertificate, QuorumCertificate2, UpgradeCertificate, + LightClientStateUpdateCertificate, NextEpochQuorumCertificate2, QuorumCertificate, + QuorumCertificate2, UpgradeCertificate, }, }; @@ -37,7 +38,6 @@ pub trait Storage: Send + Sync + Clone { /// Add a proposal to the stored VID proposals. async fn append_vid(&self, proposal: &Proposal>) -> Result<()>; /// Add a proposal to the stored VID proposals. - /// TODO(Chengyu): fix this async fn append_vid2(&self, proposal: &Proposal>) -> Result<()>; @@ -114,6 +114,21 @@ pub trait Storage: Send + Sync + Clone { async fn update_high_qc2(&self, high_qc: QuorumCertificate2) -> Result<()> { self.update_high_qc(high_qc.to_qc()).await } + /// Update the light client state update certificate in storage. + async fn update_state_cert( + &self, + state_cert: LightClientStateUpdateCertificate, + ) -> Result<()>; + + async fn update_high_qc2_and_state_cert( + &self, + high_qc: QuorumCertificate2, + state_cert: LightClientStateUpdateCertificate, + ) -> Result<()> { + self.update_high_qc2(high_qc).await?; + self.update_state_cert(state_cert).await + } + /// Update the current high QC in storage. async fn update_next_epoch_high_qc2( &self, diff --git a/hotshot-types/src/vote.rs b/hotshot-types/src/vote.rs index 1631896652..f399dbb15e 100644 --- a/hotshot-types/src/vote.rs +++ b/hotshot-types/src/vote.rs @@ -21,12 +21,13 @@ use tracing::error; use crate::{ epoch_membership::EpochMembership, + light_client::LightClientState, message::UpgradeLock, - simple_certificate::Threshold, - simple_vote::{VersionedVoteData, Voteable}, + simple_certificate::{LightClientStateUpdateCertificate, Threshold}, + simple_vote::{LightClientStateUpdateVote, VersionedVoteData, Voteable}, traits::{ node_implementation::{NodeType, Versions}, - signature_key::{SignatureKey, StakeTableEntryType}, + signature_key::{SignatureKey, StakeTableEntryType, StateSignatureKey}, }, PeerConfig, StakeTableEntries, }; @@ -87,7 +88,7 @@ pub trait Certificate: HasViewNumber { /// Get Stake Table from Membership implementation. fn stake_table( membership: &EpochMembership, - ) -> impl Future>> + Send; + ) -> impl Future>> + Send; /// Get Total Nodes from Membership implementation. fn total_nodes(membership: &EpochMembership) -> impl Future + Send; @@ -96,7 +97,7 @@ pub trait Certificate: HasViewNumber { fn stake_table_entry( membership: &EpochMembership, pub_key: &TYPES::SignatureKey, - ) -> impl Future>> + Send; + ) -> impl Future>> + Send; /// Get the commitment which was voted on fn data(&self) -> &Self::Voteable; @@ -241,3 +242,72 @@ impl< /// Mapping of commitments to vote tokens by key. type VoteMap2 = HashMap)>; + +/// Accumulator for light client state update vote +#[allow(clippy::type_complexity)] +pub struct LightClientStateUpdateVoteAccumulator { + pub vote_outcomes: HashMap< + LightClientState, + ( + U256, + HashMap< + TYPES::StateSignatureKey, + ::StateSignature, + >, + ), + >, +} + +impl LightClientStateUpdateVoteAccumulator { + /// Add a vote to the total accumulated votes for the given epoch. + /// Returns the accumulator or the certificate if we + /// have accumulated enough votes to exceed the threshold for creating a certificate. + pub fn accumulate( + &mut self, + epoch: TYPES::Epoch, + vote: &LightClientStateUpdateVote, + key: &TYPES::StateSignatureKey, + stake_table: &[PeerConfig], + threshold: U256, + ) -> Option> { + // Find the signer + if let Some(key_index) = stake_table + .iter() + .position(|config| &config.state_ver_key == key) + { + // Verify the vote validity + let state_msg = (&vote.light_client_state).into(); + if !key.verify_state_sig(&vote.signature, &state_msg) { + error!("Invalid light client state update vote {:?}", vote); + return None; + } + let (total_stake_casted, vote_map) = self + .vote_outcomes + .entry(vote.light_client_state.clone()) + .or_insert_with(|| (U256::from(0), HashMap::new())); + + // Check for duplicate vote + if vote_map.contains_key(key) { + tracing::warn!("Duplicate vote (key: {:?}, vote: {:?})", key, vote); + return None; + } + + *total_stake_casted += stake_table[key_index].stake_table_entry.stake(); + vote_map.insert(key.clone(), vote.signature.clone()); + + if *total_stake_casted >= threshold { + return Some(LightClientStateUpdateCertificate { + epoch, + light_client_state: vote.light_client_state.clone(), + signatures: Vec::from_iter( + vote_map.iter().map(|(k, v)| (k.clone(), v.clone())), + ), + }); + } + None + } else { + error!("Light client state update vote with invalid key {:?}", key); + None + } + } +} diff --git a/hotshot/src/lib.rs b/hotshot/src/lib.rs index 660984de2d..ae4135723f 100644 --- a/hotshot/src/lib.rs +++ b/hotshot/src/lib.rs @@ -17,9 +17,10 @@ use hotshot_types::{ drb::{DrbResult, INITIAL_DRB_RESULT}, epoch_membership::EpochMembershipCoordinator, message::UpgradeLock, + simple_certificate::LightClientStateUpdateCertificate, traits::{ block_contents::BlockHeader, election::Membership, network::BroadcastDelay, - node_implementation::Versions, + node_implementation::Versions, signature_key::StateSignatureKey, }, }; use rand::Rng; @@ -108,8 +109,11 @@ pub struct SystemContext, V: Versi /// The private key of this node private_key: ::PrivateKey, + /// The private key to sign the light client state + state_private_key: ::StatePrivateKey, + /// Configuration items for this hotshot instance - pub config: HotShotConfig, + pub config: HotShotConfig, /// The underlying network pub network: Arc, @@ -168,6 +172,7 @@ impl, V: Versions> Clone Self { public_key: self.public_key.clone(), private_key: self.private_key.clone(), + state_private_key: self.state_private_key.clone(), config: self.config.clone(), network: Arc::clone(&self.network), membership_coordinator: self.membership_coordinator.clone(), @@ -204,8 +209,9 @@ impl, V: Versions> SystemContext::PrivateKey, + state_private_key: ::StatePrivateKey, nonce: u64, - config: HotShotConfig, + config: HotShotConfig, memberships: EpochMembershipCoordinator, network: Arc, initializer: HotShotInitializer, @@ -227,6 +233,7 @@ impl, V: Versions> SystemContext, V: Versions> SystemContext::PrivateKey, + state_private_key: ::StatePrivateKey, nonce: u64, - config: HotShotConfig, + config: HotShotConfig, membership_coordinator: EpochMembershipCoordinator, network: Arc, initializer: HotShotInitializer, @@ -351,6 +359,7 @@ impl, V: Versions> SystemContext, V: Versions> SystemContext, V: Versions> SystemContext::PrivateKey, + state_private_key: ::StatePrivateKey, node_id: u64, - config: HotShotConfig, + config: HotShotConfig, memberships: EpochMembershipCoordinator, network: Arc, initializer: HotShotInitializer, @@ -644,6 +655,7 @@ impl, V: Versions> SystemContext::PrivateKey, + state_private_key: ::StatePrivateKey, nonce: u64, - config: HotShotConfig, + config: HotShotConfig, memberships: EpochMembershipCoordinator, network: Arc, initializer: HotShotInitializer, @@ -804,6 +817,7 @@ where let left_system_context = SystemContext::new( public_key.clone(), private_key.clone(), + state_private_key.clone(), nonce, config.clone(), memberships.clone(), @@ -817,6 +831,7 @@ where let right_system_context = SystemContext::new( public_key, private_key, + state_private_key, nonce, config, memberships, @@ -991,6 +1006,12 @@ impl, V: Versions> ConsensusApi &::PrivateKey { &self.hotshot.private_key } + + fn state_private_key( + &self, + ) -> &::StatePrivateKey { + &self.hotshot.state_private_key + } } #[derive(Clone, Debug, PartialEq)] @@ -1058,6 +1079,9 @@ pub struct HotShotInitializer { /// Saved epoch information. This must be sorted ascending by epoch. pub start_epoch_info: Vec>, + + /// The last formed light client state update certificate + pub state_cert: LightClientStateUpdateCertificate, } impl HotShotInitializer { @@ -1091,6 +1115,7 @@ impl HotShotInitializer { epoch_height, epoch_start_block, start_epoch_info, + state_cert: LightClientStateUpdateCertificate::::genesis(), }) } @@ -1153,6 +1178,7 @@ impl HotShotInitializer { saved_proposals: BTreeMap>>, saved_vid_shares: VidShares, decided_upgrade_certificate: Option>, + state_cert: LightClientStateUpdateCertificate, ) -> Self { let anchor_state = Arc::new(TYPES::ValidatedState::from_header( anchor_leaf.block_header(), @@ -1177,6 +1203,7 @@ impl HotShotInitializer { undecided_leaves: BTreeMap::new(), undecided_state: BTreeMap::new(), start_epoch_info, + state_cert, }; initializer.update_undecided() diff --git a/hotshot/src/tasks/mod.rs b/hotshot/src/tasks/mod.rs index 743327ea4b..682701ac9a 100644 --- a/hotshot/src/tasks/mod.rs +++ b/hotshot/src/tasks/mod.rs @@ -47,7 +47,7 @@ use crate::{ genesis_epoch_from_version, tasks::task_state::CreateTaskState, types::SystemContextHandle, ConsensusApi, ConsensusMetricsValue, ConsensusTaskRegistry, EpochMembershipCoordinator, HotShotConfig, HotShotInitializer, MarketplaceConfig, NetworkTaskRegistry, SignatureKey, - SystemContext, Versions, + StateSignatureKey, SystemContext, Versions, }; /// event for global event stream @@ -320,8 +320,9 @@ where &'static mut self, public_key: TYPES::SignatureKey, private_key: ::PrivateKey, + state_private_key: ::StatePrivateKey, nonce: u64, - config: HotShotConfig, + config: HotShotConfig, memberships: EpochMembershipCoordinator, network: Arc, initializer: HotShotInitializer, @@ -334,6 +335,7 @@ where let hotshot = SystemContext::new( public_key, private_key, + state_private_key, nonce, config, memberships.clone(), diff --git a/hotshot/src/tasks/task_state.rs b/hotshot/src/tasks/task_state.rs index b793d08930..f1fc5f9ed9 100644 --- a/hotshot/src/tasks/task_state.rs +++ b/hotshot/src/tasks/task_state.rs @@ -233,6 +233,7 @@ impl, V: Versions> CreateTaskState Self { public_key: handle.public_key().clone(), private_key: handle.private_key().clone(), + state_private_key: handle.state_private_key().clone(), consensus: OuterConsensus::new(consensus), instance_state: handle.hotshot.instance_state(), latest_voted_view: handle.cur_view().await, @@ -318,6 +319,7 @@ impl, V: Versions> CreateTaskState network: Arc::clone(&handle.hotshot.network), membership_coordinator: handle.hotshot.membership_coordinator.clone(), vote_collectors: BTreeMap::default(), + extended_quorum_vote_collectors: BTreeMap::default(), next_epoch_vote_collectors: BTreeMap::default(), timeout_vote_collectors: BTreeMap::default(), cur_view: handle.cur_view().await, diff --git a/hotshot/src/traits/election/dummy_catchup_membership.rs b/hotshot/src/traits/election/dummy_catchup_membership.rs index 9d1347f944..c1a1aa824f 100644 --- a/hotshot/src/traits/election/dummy_catchup_membership.rs +++ b/hotshot/src/traits/election/dummy_catchup_membership.rs @@ -38,8 +38,8 @@ where fn new( // Note: eligible_leaders is currently a haMemck because the DA leader == the quorum leader // but they should not have voting power. - stake_committee_members: Vec>, - da_committee_members: Vec>, + stake_committee_members: Vec>, + da_committee_members: Vec>, ) -> Self { Self { inner: StaticCommittee::new(stake_committee_members, da_committee_members), @@ -48,18 +48,12 @@ where } } - fn stake_table( - &self, - epoch: Option, - ) -> Vec> { + fn stake_table(&self, epoch: Option) -> Vec> { self.assert_has_epoch(epoch); self.inner.stake_table(epoch) } - fn da_stake_table( - &self, - epoch: Option, - ) -> Vec> { + fn da_stake_table(&self, epoch: Option) -> Vec> { self.assert_has_epoch(epoch); self.inner.da_stake_table(epoch) } @@ -95,7 +89,7 @@ where &self, pub_key: &TYPES::SignatureKey, epoch: Option, - ) -> Option> { + ) -> Option> { self.assert_has_epoch(epoch); self.inner.stake(pub_key, epoch) } @@ -104,7 +98,7 @@ where &self, pub_key: &TYPES::SignatureKey, epoch: Option, - ) -> Option> { + ) -> Option> { self.assert_has_epoch(epoch); self.inner.da_stake(pub_key, epoch) } diff --git a/hotshot/src/traits/election/randomized_committee.rs b/hotshot/src/traits/election/randomized_committee.rs index 1564d7647a..8e655ff424 100644 --- a/hotshot/src/traits/election/randomized_committee.rs +++ b/hotshot/src/traits/election/randomized_committee.rs @@ -32,76 +32,70 @@ pub struct Committee { /// The nodes eligible for leadership. /// NOTE: This is currently a hack because the DA leader needs to be the quorum /// leader but without voting rights. - eligible_leaders: Vec>, + eligible_leaders: Vec>, /// The nodes on the committee and their stake - stake_table: Vec>, + stake_table: Vec>, /// The nodes on the committee and their stake - da_stake_table: Vec>, + da_stake_table: Vec>, /// Stake tables randomized with the DRB, used (only) for leader election randomized_committee: RandomizedCommittee<::StakeTableEntry>, /// The nodes on the committee and their stake, indexed by public key - indexed_stake_table: BTreeMap>, + indexed_stake_table: BTreeMap>, /// The nodes on the committee and their stake, indexed by public key - indexed_da_stake_table: BTreeMap>, + indexed_da_stake_table: BTreeMap>, } impl Membership for Committee { type Error = hotshot_utils::anytrace::Error; /// Create a new election - fn new( - committee_members: Vec::SignatureKey>>, - da_members: Vec::SignatureKey>>, - ) -> Self { + fn new(committee_members: Vec>, da_members: Vec>) -> Self { // For each eligible leader, get the stake table entry - let eligible_leaders: Vec::SignatureKey>> = - committee_members - .iter() - .filter(|&member| member.stake_table_entry.stake() > U256::zero()) - .cloned() - .collect(); + let eligible_leaders: Vec> = committee_members + .iter() + .filter(|&member| member.stake_table_entry.stake() > U256::zero()) + .cloned() + .collect(); // For each member, get the stake table entry - let members: Vec::SignatureKey>> = committee_members + let members: Vec> = committee_members .iter() .filter(|&member| member.stake_table_entry.stake() > U256::zero()) .cloned() .collect(); // For each member, get the stake table entry - let da_members: Vec::SignatureKey>> = da_members + let da_members: Vec> = da_members .iter() .filter(|&member| member.stake_table_entry.stake() > U256::zero()) .cloned() .collect(); // Index the stake table by public key - let indexed_stake_table: BTreeMap> = - members - .iter() - .map(|config| { - ( - TYPES::SignatureKey::public_key(&config.stake_table_entry), - config.clone(), - ) - }) - .collect(); + let indexed_stake_table: BTreeMap> = members + .iter() + .map(|config| { + ( + TYPES::SignatureKey::public_key(&config.stake_table_entry), + config.clone(), + ) + }) + .collect(); // Index the stake table by public key - let indexed_da_stake_table: BTreeMap> = - da_members - .iter() - .map(|config| { - ( - TYPES::SignatureKey::public_key(&config.stake_table_entry), - config.clone(), - ) - }) - .collect(); + let indexed_da_stake_table: BTreeMap> = da_members + .iter() + .map(|config| { + ( + TYPES::SignatureKey::public_key(&config.stake_table_entry), + config.clone(), + ) + }) + .collect(); // We use a constant value of `[0u8; 32]` for the drb, since this is just meant to be used in tests let randomized_committee = generate_stake_cdf( @@ -124,18 +118,12 @@ impl Membership for Committee { } /// Get the stake table for the current view - fn stake_table( - &self, - _epoch: Option<::Epoch>, - ) -> Vec> { + fn stake_table(&self, _epoch: Option<::Epoch>) -> Vec> { self.stake_table.clone() } /// Get the stake table for the current view - fn da_stake_table( - &self, - _epoch: Option<::Epoch>, - ) -> Vec> { + fn da_stake_table(&self, _epoch: Option<::Epoch>) -> Vec> { self.da_stake_table.clone() } @@ -180,7 +168,7 @@ impl Membership for Committee { &self, pub_key: &::SignatureKey, _epoch: Option<::Epoch>, - ) -> Option> { + ) -> Option> { // Only return the stake if it is above zero self.indexed_stake_table.get(pub_key).cloned() } @@ -190,7 +178,7 @@ impl Membership for Committee { &self, pub_key: &::SignatureKey, _epoch: Option<::Epoch>, - ) -> Option> { + ) -> Option> { // Only return the stake if it is above zero self.indexed_da_stake_table.get(pub_key).cloned() } diff --git a/hotshot/src/traits/election/randomized_committee_members.rs b/hotshot/src/traits/election/randomized_committee_members.rs index cc11e78e83..da1c663c98 100644 --- a/hotshot/src/traits/election/randomized_committee_members.rs +++ b/hotshot/src/traits/election/randomized_committee_members.rs @@ -32,19 +32,19 @@ pub struct RandomizedCommitteeMembers { /// The nodes eligible for leadership. /// NOTE: This is currently a hack because the DA leader needs to be the quorum /// leader but without voting rights. - eligible_leaders: Vec>, + eligible_leaders: Vec>, /// The nodes on the committee and their stake - stake_table: Vec>, + stake_table: Vec>, /// The nodes on the da committee and their stake - da_stake_table: Vec>, + da_stake_table: Vec>, /// The nodes on the committee and their stake, indexed by public key - indexed_stake_table: BTreeMap>, + indexed_stake_table: BTreeMap>, /// The nodes on the da committee and their stake, indexed by public key - indexed_da_stake_table: BTreeMap>, + indexed_da_stake_table: BTreeMap>, /// Phantom _pd: PhantomData, @@ -97,10 +97,7 @@ impl Membership { type Error = hotshot_utils::anytrace::Error; /// Create a new election - fn new( - committee_members: Vec::SignatureKey>>, - da_members: Vec::SignatureKey>>, - ) -> Self { + fn new(committee_members: Vec>, da_members: Vec>) -> Self { // For each eligible leader, get the stake table entry let eligible_leaders = committee_members .iter() @@ -109,14 +106,14 @@ impl Membership .collect(); // For each member, get the stake table entry - let members: Vec::SignatureKey>> = committee_members + let members: Vec> = committee_members .iter() .filter(|&entry| entry.stake_table_entry.stake() > U256::zero()) .cloned() .collect(); // For each da member, get the stake table entry - let da_members: Vec::SignatureKey>> = da_members + let da_members: Vec> = da_members .iter() .filter(|&entry| entry.stake_table_entry.stake() > U256::zero()) .cloned() @@ -159,10 +156,7 @@ impl Membership } /// Get the stake table for the current view - fn stake_table( - &self, - epoch: Option<::Epoch>, - ) -> Vec> { + fn stake_table(&self, epoch: Option<::Epoch>) -> Vec> { if let Some(epoch) = epoch { let filter = self.make_quorum_filter(epoch); //self.stake_table.clone()s @@ -178,10 +172,7 @@ impl Membership } /// Get the da stake table for the current view - fn da_stake_table( - &self, - epoch: Option<::Epoch>, - ) -> Vec> { + fn da_stake_table(&self, epoch: Option<::Epoch>) -> Vec> { if let Some(epoch) = epoch { let filter = self.make_da_quorum_filter(epoch); //self.stake_table.clone()s @@ -254,7 +245,7 @@ impl Membership &self, pub_key: &::SignatureKey, epoch: Option<::Epoch>, - ) -> Option::SignatureKey>> { + ) -> Option> { if let Some(epoch) = epoch { let filter = self.make_quorum_filter(epoch); let actual_members: BTreeSet<_> = self @@ -282,7 +273,7 @@ impl Membership &self, pub_key: &::SignatureKey, epoch: Option<::Epoch>, - ) -> Option::SignatureKey>> { + ) -> Option> { if let Some(epoch) = epoch { let filter = self.make_da_quorum_filter(epoch); let actual_members: BTreeSet<_> = self diff --git a/hotshot/src/traits/election/static_committee.rs b/hotshot/src/traits/election/static_committee.rs index adcb4c220b..0e06eed2e9 100644 --- a/hotshot/src/traits/election/static_committee.rs +++ b/hotshot/src/traits/election/static_committee.rs @@ -28,19 +28,19 @@ pub struct StaticCommittee { /// The nodes eligible for leadership. /// NOTE: This is currently a hack because the DA leader needs to be the quorum /// leader but without voting rights. - eligible_leaders: Vec>, + eligible_leaders: Vec>, /// The nodes on the committee and their stake - stake_table: Vec>, + stake_table: Vec>, /// The nodes on the committee and their stake - da_stake_table: Vec>, + da_stake_table: Vec>, /// The nodes on the committee and their stake, indexed by public key - indexed_stake_table: BTreeMap>, + indexed_stake_table: BTreeMap>, /// The nodes on the committee and their stake, indexed by public key - indexed_da_stake_table: BTreeMap>, + indexed_da_stake_table: BTreeMap>, /// The first epoch which will be encountered. For testing, will panic if an epoch-carrying function is called /// when first_epoch is None or is Some greater than that epoch. @@ -62,26 +62,22 @@ impl StaticCommittee { impl Membership for StaticCommittee { type Error = hotshot_utils::anytrace::Error; /// Create a new election - fn new( - committee_members: Vec::SignatureKey>>, - da_members: Vec::SignatureKey>>, - ) -> Self { + fn new(committee_members: Vec>, da_members: Vec>) -> Self { // For each eligible leader, get the stake table entry - let eligible_leaders: Vec::SignatureKey>> = - committee_members - .clone() - .into_iter() - .filter(|member| member.stake_table_entry.stake() > U256::zero()) - .collect(); + let eligible_leaders: Vec> = committee_members + .clone() + .into_iter() + .filter(|member| member.stake_table_entry.stake() > U256::zero()) + .collect(); // For each member, get the stake table entry - let members: Vec::SignatureKey>> = committee_members + let members: Vec> = committee_members .into_iter() .filter(|member| member.stake_table_entry.stake() > U256::zero()) .collect(); // For each member, get the stake table entry - let da_members: Vec::SignatureKey>> = da_members + let da_members: Vec> = da_members .into_iter() .filter(|member| member.stake_table_entry.stake() > U256::zero()) .collect(); @@ -119,19 +115,13 @@ impl Membership for StaticCommittee { } /// Get the stake table for the current view - fn stake_table( - &self, - epoch: Option<::Epoch>, - ) -> Vec::SignatureKey>> { + fn stake_table(&self, epoch: Option<::Epoch>) -> Vec> { self.check_first_epoch(epoch); self.stake_table.clone() } /// Get the stake table for the current view - fn da_stake_table( - &self, - epoch: Option<::Epoch>, - ) -> Vec::SignatureKey>> { + fn da_stake_table(&self, epoch: Option<::Epoch>) -> Vec> { self.check_first_epoch(epoch); self.da_stake_table.clone() } @@ -180,7 +170,7 @@ impl Membership for StaticCommittee { &self, pub_key: &::SignatureKey, epoch: Option<::Epoch>, - ) -> Option::SignatureKey>> { + ) -> Option> { self.check_first_epoch(epoch); // Only return the stake if it is above zero self.indexed_stake_table.get(pub_key).cloned() @@ -191,7 +181,7 @@ impl Membership for StaticCommittee { &self, pub_key: &::SignatureKey, epoch: Option<::Epoch>, - ) -> Option::SignatureKey>> { + ) -> Option> { self.check_first_epoch(epoch); // Only return the stake if it is above zero self.indexed_da_stake_table.get(pub_key).cloned() diff --git a/hotshot/src/traits/election/static_committee_leader_two_views.rs b/hotshot/src/traits/election/static_committee_leader_two_views.rs index 85a467057a..3f5d05e000 100644 --- a/hotshot/src/traits/election/static_committee_leader_two_views.rs +++ b/hotshot/src/traits/election/static_committee_leader_two_views.rs @@ -28,72 +28,67 @@ pub struct StaticCommitteeLeaderForTwoViews { /// The nodes eligible for leadership. /// NOTE: This is currently a hack because the DA leader needs to be the quorum /// leader but without voting rights. - eligible_leaders: Vec>, + eligible_leaders: Vec>, /// The nodes on the committee and their stake - stake_table: Vec>, + stake_table: Vec>, /// The nodes on the committee and their stake - da_stake_table: Vec>, + da_stake_table: Vec>, /// The nodes on the committee and their stake, indexed by public key - indexed_stake_table: BTreeMap>, + indexed_stake_table: BTreeMap>, /// The nodes on the committee and their stake, indexed by public key - indexed_da_stake_table: BTreeMap>, + indexed_da_stake_table: BTreeMap>, } impl Membership for StaticCommitteeLeaderForTwoViews { type Error = hotshot_utils::anytrace::Error; /// Create a new election - fn new( - committee_members: Vec::SignatureKey>>, - da_members: Vec::SignatureKey>>, - ) -> Self { + fn new(committee_members: Vec>, da_members: Vec>) -> Self { // For each eligible leader, get the stake table entry - let eligible_leaders: Vec> = committee_members + let eligible_leaders: Vec> = committee_members .iter() .filter(|&member| member.stake_table_entry.stake() > U256::zero()) .cloned() .collect(); // For each member, get the stake table entry - let members: Vec> = committee_members + let members: Vec> = committee_members .iter() .filter(|&member| member.stake_table_entry.stake() > U256::zero()) .cloned() .collect(); // For each member, get the stake table entry - let da_members: Vec> = da_members + let da_members: Vec> = da_members .iter() .filter(|&member| member.stake_table_entry.stake() > U256::zero()) .cloned() .collect(); // Index the stake table by public key - let indexed_stake_table: BTreeMap> = - members - .iter() - .map(|member| { - ( - TYPES::SignatureKey::public_key(&member.stake_table_entry), - member.clone(), - ) - }) - .collect(); + let indexed_stake_table: BTreeMap> = members + .iter() + .map(|member| { + ( + TYPES::SignatureKey::public_key(&member.stake_table_entry), + member.clone(), + ) + }) + .collect(); // Index the stake table by public key - let indexed_da_stake_table: BTreeMap> = - da_members - .iter() - .map(|member| { - ( - TYPES::SignatureKey::public_key(&member.stake_table_entry), - member.clone(), - ) - }) - .collect(); + let indexed_da_stake_table: BTreeMap> = da_members + .iter() + .map(|member| { + ( + TYPES::SignatureKey::public_key(&member.stake_table_entry), + member.clone(), + ) + }) + .collect(); Self { eligible_leaders, @@ -105,18 +100,12 @@ impl Membership for StaticCommitteeLeaderForTwoViews::Epoch>, - ) -> Vec::SignatureKey>> { + fn stake_table(&self, _epoch: Option<::Epoch>) -> Vec> { self.stake_table.clone() } /// Get the stake table for the current view - fn da_stake_table( - &self, - _epoch: Option<::Epoch>, - ) -> Vec::SignatureKey>> { + fn da_stake_table(&self, _epoch: Option<::Epoch>) -> Vec> { self.da_stake_table.clone() } @@ -161,7 +150,7 @@ impl Membership for StaticCommitteeLeaderForTwoViews::SignatureKey, _epoch: Option<::Epoch>, - ) -> Option> { + ) -> Option> { // Only return the stake if it is above zero self.indexed_stake_table.get(pub_key).cloned() } @@ -171,7 +160,7 @@ impl Membership for StaticCommitteeLeaderForTwoViews::SignatureKey, _epoch: Option<::Epoch>, - ) -> Option> { + ) -> Option> { // Only return the stake if it is above zero self.indexed_da_stake_table.get(pub_key).cloned() } diff --git a/hotshot/src/traits/election/two_static_committees.rs b/hotshot/src/traits/election/two_static_committees.rs index 3825c4ed47..12feabc5cb 100644 --- a/hotshot/src/traits/election/two_static_committees.rs +++ b/hotshot/src/traits/election/two_static_committees.rs @@ -23,21 +23,15 @@ use hotshot_utils::anytrace::Result; use primitive_types::U256; /// Tuple type for eligible leaders -type EligibleLeaders = ( - Vec::SignatureKey>>, - Vec::SignatureKey>>, -); +type EligibleLeaders = (Vec>, Vec>); /// Tuple type for stake tables -type StakeTables = ( - Vec::SignatureKey>>, - Vec::SignatureKey>>, -); +type StakeTables = (Vec>, Vec>); /// Tuple type for indexed stake tables type IndexedStakeTables = ( - BTreeMap<::SignatureKey, PeerConfig<::SignatureKey>>, - BTreeMap<::SignatureKey, PeerConfig<::SignatureKey>>, + BTreeMap<::SignatureKey, PeerConfig>, + BTreeMap<::SignatureKey, PeerConfig>, ); #[derive(Clone, Debug, Eq, PartialEq, Hash)] @@ -64,17 +58,13 @@ pub struct TwoStaticCommittees { impl Membership for TwoStaticCommittees { type Error = hotshot_utils::anytrace::Error; /// Create a new election - fn new( - committee_members: Vec::SignatureKey>>, - da_members: Vec::SignatureKey>>, - ) -> Self { + fn new(committee_members: Vec>, da_members: Vec>) -> Self { // For each eligible leader, get the stake table entry - let eligible_leaders: Vec::SignatureKey>> = - committee_members - .clone() - .into_iter() - .filter(|member| member.stake_table_entry.stake() > U256::zero()) - .collect(); + let eligible_leaders: Vec> = committee_members + .clone() + .into_iter() + .filter(|member| member.stake_table_entry.stake() > U256::zero()) + .collect(); let eligible_leaders1 = eligible_leaders .iter() @@ -90,19 +80,19 @@ impl Membership for TwoStaticCommittees { .collect(); // For each member, get the stake table entry - let members: Vec::SignatureKey>> = committee_members + let members: Vec> = committee_members .clone() .into_iter() .filter(|member| member.stake_table_entry.stake() > U256::zero()) .collect(); - let members1: Vec::SignatureKey>> = members + let members1: Vec> = members .iter() .enumerate() .filter(|(idx, _)| idx % 2 == 0) .map(|(_, leader)| leader.clone()) .collect(); - let members2: Vec::SignatureKey>> = members + let members2: Vec> = members .iter() .enumerate() .filter(|(idx, _)| idx % 2 == 1) @@ -110,19 +100,19 @@ impl Membership for TwoStaticCommittees { .collect(); // For each member, get the stake table entry - let da_members: Vec::SignatureKey>> = da_members + let da_members: Vec> = da_members .clone() .into_iter() .filter(|member| member.stake_table_entry.stake() > U256::zero()) .collect(); - let da_members1: Vec::SignatureKey>> = da_members + let da_members1: Vec> = da_members .iter() .enumerate() .filter(|(idx, _)| idx % 2 == 0) .map(|(_, leader)| leader.clone()) .collect(); - let da_members2: Vec::SignatureKey>> = da_members + let da_members2: Vec> = da_members .iter() .enumerate() .filter(|(idx, _)| idx % 2 == 1) @@ -181,10 +171,7 @@ impl Membership for TwoStaticCommittees { } /// Get the stake table for the current view - fn stake_table( - &self, - epoch: Option<::Epoch>, - ) -> Vec::SignatureKey>> { + fn stake_table(&self, epoch: Option<::Epoch>) -> Vec> { let epoch = epoch.expect("epochs cannot be disabled with TwoStaticCommittees"); if *epoch != 0 && *epoch % 2 == 0 { self.stake_table.0.clone() @@ -194,10 +181,7 @@ impl Membership for TwoStaticCommittees { } /// Get the stake table for the current view - fn da_stake_table( - &self, - epoch: Option<::Epoch>, - ) -> Vec::SignatureKey>> { + fn da_stake_table(&self, epoch: Option<::Epoch>) -> Vec> { let epoch = epoch.expect("epochs cannot be disabled with TwoStaticCommittees"); if *epoch != 0 && *epoch % 2 == 0 { self.da_stake_table.0.clone() @@ -277,7 +261,7 @@ impl Membership for TwoStaticCommittees { &self, pub_key: &::SignatureKey, epoch: Option<::Epoch>, - ) -> Option::SignatureKey>> { + ) -> Option> { // Only return the stake if it is above zero let epoch = epoch.expect("epochs cannot be disabled with TwoStaticCommittees"); if *epoch != 0 && *epoch % 2 == 0 { @@ -292,7 +276,7 @@ impl Membership for TwoStaticCommittees { &self, pub_key: &::SignatureKey, epoch: Option<::Epoch>, - ) -> Option::SignatureKey>> { + ) -> Option> { // Only return the stake if it is above zero let epoch = epoch.expect("epochs cannot be disabled with TwoStaticCommittees"); if *epoch != 0 && *epoch % 2 == 0 { diff --git a/hotshot/src/traits/networking/libp2p_network.rs b/hotshot/src/traits/networking/libp2p_network.rs index 52ce406350..73dba0beb6 100644 --- a/hotshot/src/traits/networking/libp2p_network.rs +++ b/hotshot/src/traits/networking/libp2p_network.rs @@ -392,7 +392,7 @@ impl Libp2pNetwork { /// If we are unable to calculate the replication factor #[allow(clippy::too_many_arguments)] pub async fn from_config( - mut config: NetworkConfig, + mut config: NetworkConfig, dht_persistent_storage: D, quorum_membership: Arc>, gossip_config: GossipConfig, diff --git a/hotshot/src/types.rs b/hotshot/src/types.rs index 5013e44298..27181910b1 100644 --- a/hotshot/src/types.rs +++ b/hotshot/src/types.rs @@ -11,6 +11,6 @@ pub use event::{Event, EventType}; pub use handle::SystemContextHandle; pub use hotshot_types::{ message::Message, - signature_key::{BLSPrivKey, BLSPubKey}, + signature_key::{BLSPrivKey, BLSPubKey, SchnorrPrivKey, SchnorrPubKey}, traits::signature_key::SignatureKey, }; diff --git a/marketplace-solver/src/events.rs b/marketplace-solver/src/events.rs index 85a49ca1dc..5f22675b67 100644 --- a/marketplace-solver/src/events.rs +++ b/marketplace-solver/src/events.rs @@ -94,7 +94,7 @@ pub mod mock { const STAKED_NODES: usize = 10; pub type StaticVer01 = StaticVersion<0, 1>; - pub fn generate_stake_table() -> Vec> { + pub fn generate_stake_table() -> Vec> { (0..STAKED_NODES) .map(|_| { let private_key = @@ -102,7 +102,7 @@ pub mod mock { let pub_key = BLSPubKey::from_private(&private_key); let state_key_pair = StateKeyPair::generate(); - PeerConfig:: { + PeerConfig:: { stake_table_entry: pub_key.stake_table_entry(NODE_STAKE), state_ver_key: state_key_pair.ver_key(), } diff --git a/marketplace-solver/src/state.rs b/marketplace-solver/src/state.rs index 697a273539..8a798d7473 100644 --- a/marketplace-solver/src/state.rs +++ b/marketplace-solver/src/state.rs @@ -7,7 +7,7 @@ use espresso_types::{ BidTx, RollupRegistration, RollupRegistrationBody, RollupUpdate, RollupUpdatebody, SolverAuctionResults, }, - PubKey, SeqTypes, + SeqTypes, Update::Set, }; use hotshot::types::SignatureKey; @@ -48,7 +48,7 @@ pub struct SolverState { } pub struct StakeTable { - pub known_nodes_with_stake: Vec>, + pub known_nodes_with_stake: Vec>, } #[async_trait] diff --git a/node-metrics/src/api/node_validator/v0/mod.rs b/node-metrics/src/api/node_validator/v0/mod.rs index c152a8e81b..47b52b6a29 100644 --- a/node-metrics/src/api/node_validator/v0/mod.rs +++ b/node-metrics/src/api/node_validator/v0/mod.rs @@ -295,7 +295,7 @@ where #[derive(Debug, Deserialize)] pub struct PublishHotShotConfig { - pub known_nodes_with_stake: Vec>, + pub known_nodes_with_stake: Vec>, } #[derive(Debug, Deserialize)] diff --git a/sequencer-sqlite/Cargo.lock b/sequencer-sqlite/Cargo.lock index 96be4a2167..655c2aa357 100644 --- a/sequencer-sqlite/Cargo.lock +++ b/sequencer-sqlite/Cargo.lock @@ -5099,6 +5099,7 @@ dependencies = [ "hotshot-utils", "jf-vid", "lru 0.12.5", + "primitive-types", "rand 0.8.5", "serde", "sha2 0.10.8", @@ -5182,7 +5183,9 @@ dependencies = [ "either", "futures", "hotshot-utils", + "jf-crhf 0.1.0", "jf-pcs", + "jf-rescue", "jf-signature 0.2.0", "jf-utils", "jf-vid", @@ -9568,6 +9571,7 @@ dependencies = [ "futures", "hotshot", "hotshot-contract-adapter", + "hotshot-example-types", "hotshot-types", "log-panics", "portpicker", diff --git a/sequencer/src/api.rs b/sequencer/src/api.rs index 6412a86cca..7d435bdced 100644 --- a/sequencer/src/api.rs +++ b/sequencer/src/api.rs @@ -62,7 +62,7 @@ struct ConsensusState, P: SequencerPersistence, V: V state_signer: Arc>, event_streamer: Arc>>, node_state: NodeState, - network_config: NetworkConfig, + network_config: NetworkConfig, #[derivative(Debug = "ignore")] handle: Arc>>, @@ -112,7 +112,7 @@ impl, P: SequencerPersistence, V: Versions> ApiState Arc::clone(&self.consensus.as_ref().get().await.get_ref().handle) } - async fn network_config(&self) -> NetworkConfig { + async fn network_config(&self) -> NetworkConfig { self.consensus .as_ref() .get() @@ -167,14 +167,12 @@ impl, D: Sync, V: Versions, P: SequencerPersistence> async fn get_stake_table( &self, epoch: Option<::Epoch>, - ) -> Vec::SignatureKey>> { + ) -> Vec> { self.as_ref().get_stake_table(epoch).await } /// Get the stake table for the current epoch if not provided - async fn get_stake_table_current( - &self, - ) -> Vec::SignatureKey>> { + async fn get_stake_table_current(&self) -> Vec> { self.as_ref().get_stake_table_current().await } } @@ -185,7 +183,7 @@ impl, V: Versions, P: SequencerPersistence> async fn get_stake_table( &self, epoch: Option<::Epoch>, - ) -> Vec::SignatureKey>> { + ) -> Vec> { let Ok(mem) = self .consensus() .await @@ -201,9 +199,7 @@ impl, V: Versions, P: SequencerPersistence> } /// Get the stake table for the current epoch if not provided - async fn get_stake_table_current( - &self, - ) -> Vec::SignatureKey>> { + async fn get_stake_table_current(&self) -> Vec> { let epoch = self.consensus().await.read().await.cur_epoch().await; self.get_stake_table(epoch).await diff --git a/sequencer/src/api/data_source.rs b/sequencer/src/api/data_source.rs index a88ad528be..5f27a9bb40 100644 --- a/sequencer/src/api/data_source.rs +++ b/sequencer/src/api/data_source.rs @@ -115,12 +115,10 @@ pub(crate) trait StakeTableDataSource { fn get_stake_table( &self, epoch: Option<::Epoch>, - ) -> impl Send + Future>>; + ) -> impl Send + Future>>; /// Get the stake table for the current epoch if not provided - fn get_stake_table_current( - &self, - ) -> impl Send + Future>>; + fn get_stake_table_current(&self) -> impl Send + Future>>; } pub(crate) trait CatchupDataSource: Sync { diff --git a/sequencer/src/bin/cdn-whitelist.rs b/sequencer/src/bin/cdn-whitelist.rs index 2e47b3d049..af8c2486a2 100644 --- a/sequencer/src/bin/cdn-whitelist.rs +++ b/sequencer/src/bin/cdn-whitelist.rs @@ -9,10 +9,7 @@ use cdn_broker::reexports::discovery::{DiscoveryClient, Embedded, Redis}; use clap::Parser; use espresso_types::SeqTypes; use hotshot_orchestrator::client::OrchestratorClient; -use hotshot_types::{ - network::NetworkConfig, - traits::{node_implementation::NodeType, signature_key::SignatureKey}, -}; +use hotshot_types::{network::NetworkConfig, traits::signature_key::SignatureKey}; use surf_disco::Url; #[derive(Parser, Debug)] @@ -54,8 +51,7 @@ async fn main() -> Result<()> { // Attempt to get the config from the orchestrator. // Loops internally until the config is received. - let config: NetworkConfig<::SignatureKey> = - orchestrator_client.get_config_after_collection().await; + let config: NetworkConfig = orchestrator_client.get_config_after_collection().await; tracing::info!("Received config from orchestrator"); diff --git a/sequencer/src/bin/deploy.rs b/sequencer/src/bin/deploy.rs index 8a5916bd43..e0214bdf25 100644 --- a/sequencer/src/bin/deploy.rs +++ b/sequencer/src/bin/deploy.rs @@ -1,7 +1,7 @@ use std::{fs::File, io::stdout, path::PathBuf, time::Duration}; use clap::Parser; -use espresso_types::parse_duration; +use espresso_types::{parse_duration, SeqTypes}; use ethers::types::Address; use futures::FutureExt; use hotshot_stake_table::config::STAKE_TABLE_CAPACITY; @@ -155,7 +155,7 @@ async fn main() -> anyhow::Result<()> { let initial_stake_table = if let Some(path) = opt.initial_stake_table_path { tracing::info!("Loading initial stake table from {:?}", path); - Some(PermissionedStakeTableConfig::from_toml_file(&path)?.into()) + Some(PermissionedStakeTableConfig::::from_toml_file(&path)?.into()) } else { None }; diff --git a/sequencer/src/bin/orchestrator.rs b/sequencer/src/bin/orchestrator.rs index e37069fc6f..b7eca0e441 100644 --- a/sequencer/src/bin/orchestrator.rs +++ b/sequencer/src/bin/orchestrator.rs @@ -2,7 +2,7 @@ use std::{num::NonZeroUsize, time::Duration}; use clap::Parser; use derive_more::From; -use espresso_types::{parse_duration, PubKey, Ratio}; +use espresso_types::{parse_duration, Ratio, SeqTypes}; use ethers::utils::hex::{self, FromHexError}; use hotshot_orchestrator::run_orchestrator; use hotshot_types::network::{Libp2pConfig, NetworkConfig}; @@ -117,7 +117,7 @@ async fn main() { let args = Args::parse(); args.logging.init(); - let mut config = NetworkConfig:: { + let mut config = NetworkConfig:: { manual_start_password: args.manual_start_password, indexed_da: false, ..Default::default() diff --git a/sequencer/src/bin/update-permissioned-stake-table.rs b/sequencer/src/bin/update-permissioned-stake-table.rs index 37534bc66c..50ec059e31 100644 --- a/sequencer/src/bin/update-permissioned-stake-table.rs +++ b/sequencer/src/bin/update-permissioned-stake-table.rs @@ -2,7 +2,7 @@ use std::{path::PathBuf, time::Duration}; use anyhow::Result; use clap::Parser; -use espresso_types::parse_duration; +use espresso_types::{parse_duration, SeqTypes}; use ethers::types::Address; use sequencer_utils::{ logging, @@ -88,7 +88,7 @@ struct Options { async fn main() -> Result<()> { let opts = Options::parse(); opts.logging.init(); - let update = PermissionedStakeTableUpdate::from_toml_file(&opts.update_toml_path)?; + let update = PermissionedStakeTableUpdate::::from_toml_file(&opts.update_toml_path)?; update_stake_table( opts.rpc_url, diff --git a/sequencer/src/catchup.rs b/sequencer/src/catchup.rs index 59068e3f84..de4b88fc1f 100644 --- a/sequencer/src/catchup.rs +++ b/sequencer/src/catchup.rs @@ -7,7 +7,7 @@ use committable::{Commitment, Committable}; use espresso_types::{ config::PublicNetworkConfig, traits::SequencerPersistence, v0::traits::StateCatchup, v0_99::ChainConfig, BackoffParams, BlockMerkleTree, FeeAccount, FeeAccountProof, - FeeMerkleCommitment, FeeMerkleTree, Leaf2, NodeState, + FeeMerkleCommitment, FeeMerkleTree, Leaf2, NodeState, SeqTypes, }; use futures::future::{Future, FutureExt, TryFuture, TryFutureExt}; use hotshot_types::{ @@ -29,7 +29,7 @@ use tokio::time::timeout; use url::Url; use vbs::version::StaticVersionType; -use crate::{api::BlocksFrontier, PubKey}; +use crate::api::BlocksFrontier; // This newtype is probably not worth having. It's only used to be able to log // URLs before doing requests. @@ -221,8 +221,8 @@ impl StatePeers { #[tracing::instrument(skip(self, my_own_validator_config))] pub async fn fetch_config( &self, - my_own_validator_config: ValidatorConfig, - ) -> anyhow::Result> { + my_own_validator_config: ValidatorConfig, + ) -> anyhow::Result> { self.backoff() .retry(self, move |provider, retry| { let my_own_validator_config = my_own_validator_config.clone(); diff --git a/sequencer/src/context.rs b/sequencer/src/context.rs index 1ad2d1a2da..e31307dc5d 100644 --- a/sequencer/src/context.rs +++ b/sequencer/src/context.rs @@ -26,11 +26,7 @@ use hotshot_types::{ data::{Leaf2, ViewNumber}, epoch_membership::EpochMembershipCoordinator, network::NetworkConfig, - traits::{ - metrics::Metrics, - network::ConnectedNetwork, - node_implementation::{NodeType, Versions}, - }, + traits::{metrics::Metrics, network::ConnectedNetwork, node_implementation::Versions}, PeerConfig, ValidatorConfig, }; use parking_lot::Mutex; @@ -94,18 +90,18 @@ pub struct SequencerContext, P: SequencerPersistence node_state: NodeState, - network_config: NetworkConfig, + network_config: NetworkConfig, #[derivative(Debug = "ignore")] - validator_config: ValidatorConfig<::SignatureKey>, + validator_config: ValidatorConfig, } impl, P: SequencerPersistence, V: Versions> SequencerContext { #[tracing::instrument(skip_all, fields(node_id = instance_state.node_id))] #[allow(clippy::too_many_arguments)] pub async fn init( - network_config: NetworkConfig, - validator_config: ValidatorConfig<::SignatureKey>, + network_config: NetworkConfig, + validator_config: ValidatorConfig, membership: EpochCommittees, instance_state: NodeState, persistence: P, @@ -141,7 +137,7 @@ impl, P: SequencerPersistence, V: Versions> Sequence .try_into() .context("stake table capacity out of range")?, ); - let state_key_pair = validator_config.state_key_pair.clone(); + // let state_key_pair = validator_config.state_key_pair.clone(); let event_streamer = Arc::new(RwLock::new(EventsStreamer::::new( config.known_nodes_with_stake.clone(), @@ -156,6 +152,7 @@ impl, P: SequencerPersistence, V: Versions> Sequence let handle = SystemContext::init( validator_config.public_key, validator_config.private_key.clone(), + validator_config.state_private_key.clone(), instance_state.node_id, config.clone(), coordinator, @@ -168,7 +165,11 @@ impl, P: SequencerPersistence, V: Versions> Sequence .await? .0; - let mut state_signer = StateSigner::new(state_key_pair, stake_table_commit); + let mut state_signer = StateSigner::new( + validator_config.state_private_key.clone(), + validator_config.state_public_key.clone(), + stake_table_commit, + ); if let Some(url) = state_relay_server { state_signer = state_signer.with_relay_server(url); } @@ -247,8 +248,8 @@ impl, P: SequencerPersistence, V: Versions> Sequence >, event_streamer: Arc>>, node_state: NodeState, - network_config: NetworkConfig, - validator_config: ValidatorConfig<::SignatureKey>, + network_config: NetworkConfig, + validator_config: ValidatorConfig, event_consumer: impl PersistenceEventConsumer + 'static, anchor_view: Option, proposal_fetcher_cfg: ProposalFetcherConfig, @@ -422,7 +423,7 @@ impl, P: SequencerPersistence, V: Versions> Sequence } /// Get the network config - pub fn network_config(&self) -> NetworkConfig { + pub fn network_config(&self) -> NetworkConfig { self.network_config.clone() } } diff --git a/sequencer/src/lib.rs b/sequencer/src/lib.rs index 1ebd47d0a7..9db5eacba2 100644 --- a/sequencer/src/lib.rs +++ b/sequencer/src/lib.rs @@ -289,7 +289,8 @@ pub async fn init_node( public_key: pub_key, private_key: network_params.private_staking_key, stake_value: 1, - state_key_pair, + state_public_key: state_key_pair.ver_key(), + state_private_key: state_key_pair.sign_key(), is_da, }; @@ -705,7 +706,7 @@ pub mod testing { } pub struct TestConfigBuilder { - config: HotShotConfig, + config: HotShotConfig, priv_keys: Vec, state_key_pairs: Vec, master_map: Arc>, @@ -779,7 +780,7 @@ pub mod testing { let known_nodes_with_stake = pub_keys .iter() .zip(&state_key_pairs) - .map(|(pub_key, state_key_pair)| PeerConfig:: { + .map(|(pub_key, state_key_pair)| PeerConfig:: { stake_table_entry: pub_key.stake_table_entry(1), state_ver_key: state_key_pair.ver_key(), }) @@ -787,7 +788,7 @@ pub mod testing { let master_map = MasterMap::new(); - let config: HotShotConfig = HotShotConfig { + let config: HotShotConfig = HotShotConfig { fixed_leader_for_gpuvid: 0, num_nodes_with_stake: num_nodes.try_into().unwrap(), known_da_nodes: known_nodes_with_stake.clone(), @@ -835,7 +836,7 @@ pub mod testing { #[derive(Clone)] pub struct TestConfig { - config: HotShotConfig, + config: HotShotConfig, priv_keys: Vec, state_key_pairs: Vec, master_map: Arc>, @@ -851,7 +852,7 @@ pub mod testing { self.priv_keys.len() } - pub fn hotshot_config(&self) -> &HotShotConfig { + pub fn hotshot_config(&self) -> &HotShotConfig { &self.config } @@ -944,7 +945,8 @@ pub mod testing { public_key: my_peer_config.stake_table_entry.stake_key, private_key: self.priv_keys[i].clone(), stake_value: my_peer_config.stake_table_entry.stake_amount.as_u64(), - state_key_pair: self.state_key_pairs[i].clone(), + state_public_key: self.state_key_pairs[i].ver_key(), + state_private_key: self.state_key_pairs[i].sign_key(), is_da, }; diff --git a/sequencer/src/persistence/fs.rs b/sequencer/src/persistence/fs.rs index 95e75b5e81..62827e699e 100644 --- a/sequencer/src/persistence/fs.rs +++ b/sequencer/src/persistence/fs.rs @@ -29,7 +29,8 @@ use hotshot_types::{ event::{Event, EventType, HotShotAction, LeafInfo}, message::{convert_proposal, Proposal}, simple_certificate::{ - NextEpochQuorumCertificate2, QuorumCertificate, QuorumCertificate2, UpgradeCertificate, + LightClientStateUpdateCertificate, NextEpochQuorumCertificate2, QuorumCertificate, + QuorumCertificate2, UpgradeCertificate, }, traits::{ block_contents::{BlockHeader, BlockPayload}, @@ -224,6 +225,10 @@ impl Inner { self.path.join("epoch_root_block_header") } + fn light_client_state_update_certificate_dir_path(&self) -> PathBuf { + self.path.join("state_cert") + } + fn update_migration(&mut self) -> anyhow::Result<()> { let path = self.migration(); let bytes = bincode::serialize(&self.migrated)?; @@ -1362,6 +1367,28 @@ impl SequencerPersistence for Persistence { Ok(()) } + async fn add_state_cert( + &self, + state_cert: LightClientStateUpdateCertificate, + ) -> anyhow::Result<()> { + let inner = self.inner.write().await; + let epoch = state_cert.epoch; + let dir_path = inner.light_client_state_update_certificate_dir_path(); + + fs::create_dir_all(dir_path.clone()) + .context("failed to create light client state update certificate dir")?; + + let bytes = bincode::serialize(&state_cert) + .context("serialize light client state update certificate")?; + + let file_path = dir_path.join(epoch.to_string()).with_extension("txt"); + fs::write(file_path, bytes).context(format!( + "writing light client state update certificate file for epoch {epoch:?}" + ))?; + + Ok(()) + } + async fn load_start_epoch_info(&self) -> anyhow::Result>> { let inner = self.inner.read().await; let drb_dir_path = inner.epoch_drb_result_dir_path(); @@ -1405,6 +1432,38 @@ impl SequencerPersistence for Persistence { Ok(result) } + + async fn load_state_cert(&self) -> anyhow::Result> { + let inner = self.inner.read().await; + let dir_path = inner.light_client_state_update_certificate_dir_path(); + // let block_header_dir_path = inner.epoch_root_block_header_dir_path(); + + let mut result = LightClientStateUpdateCertificate::genesis(); + + if !dir_path.is_dir() { + return Ok(result); + } + for (epoch, path) in epoch_files(dir_path)? { + if epoch <= result.epoch { + continue; + } + let bytes = fs::read(&path).context(format!( + "reading light client state update certificate {}", + path.display() + ))?; + let cert = bincode::deserialize::>(&bytes) + .context(format!( + "parsing light client state update certificate {}", + path.display() + ))?; + + if cert.epoch > result.epoch { + result = cert; + } + } + + Ok(result) + } } /// Update a `NetworkConfig` that may have originally been persisted with an old version. diff --git a/sequencer/src/persistence/no_storage.rs b/sequencer/src/persistence/no_storage.rs index 313ba445dd..35e155db3d 100644 --- a/sequencer/src/persistence/no_storage.rs +++ b/sequencer/src/persistence/no_storage.rs @@ -21,7 +21,10 @@ use hotshot_types::{ drb::DrbResult, event::{Event, EventType, HotShotAction, LeafInfo}, message::Proposal, - simple_certificate::{NextEpochQuorumCertificate2, QuorumCertificate2, UpgradeCertificate}, + simple_certificate::{ + LightClientStateUpdateCertificate, NextEpochQuorumCertificate2, QuorumCertificate2, + UpgradeCertificate, + }, utils::View, }; @@ -252,4 +255,15 @@ impl SequencerPersistence for NoStorage { async fn store_stake(&self, _epoch: EpochNumber, _stake: StakeTables) -> anyhow::Result<()> { Ok(()) } + + async fn add_state_cert( + &self, + _state_cert: LightClientStateUpdateCertificate, + ) -> anyhow::Result<()> { + Ok(()) + } + + async fn load_state_cert(&self) -> anyhow::Result> { + Ok(LightClientStateUpdateCertificate::::genesis()) + } } diff --git a/sequencer/src/persistence/sql.rs b/sequencer/src/persistence/sql.rs index 3b3f31bd57..458e2ab5d7 100644 --- a/sequencer/src/persistence/sql.rs +++ b/sequencer/src/persistence/sql.rs @@ -44,7 +44,8 @@ use hotshot_types::{ event::{Event, EventType, HotShotAction, LeafInfo}, message::{convert_proposal, Proposal}, simple_certificate::{ - NextEpochQuorumCertificate2, QuorumCertificate, QuorumCertificate2, UpgradeCertificate, + LightClientStateUpdateCertificate, NextEpochQuorumCertificate2, QuorumCertificate, + QuorumCertificate2, UpgradeCertificate, }, traits::{ block_contents::{BlockHeader, BlockPayload}, @@ -1946,6 +1947,39 @@ impl SequencerPersistence for Persistence { tx.commit().await } + async fn add_state_cert( + &self, + state_cert: LightClientStateUpdateCertificate, + ) -> anyhow::Result<()> { + let state_cert_bytes = bincode::serialize(&state_cert) + .context("serializing light client state update certificate")?; + + let mut tx = self.db.write().await?; + tx.upsert( + "state_cert", + ["epoch", "state_cert"], + ["epoch"], + [(state_cert.epoch.u64() as i64, state_cert_bytes)], + ) + .await?; + tx.commit().await + } + + async fn load_state_cert(&self) -> anyhow::Result> { + let row = self + .db + .read() + .await? + .fetch_one("SELECT state_cert, MAX(epoch) from state_cert GROUP BY epoch") + .await?; + if let Some(data) = row.get::>, _>("state_cert") { + bincode::deserialize(&data) + .context("deserializing light client state update certificate") + } else { + Ok(LightClientStateUpdateCertificate::genesis()) + } + } + async fn load_start_epoch_info(&self) -> anyhow::Result>> { let rows = self .db diff --git a/sequencer/src/restart_tests.rs b/sequencer/src/restart_tests.rs index b7a365cd49..b0143f99db 100755 --- a/sequencer/src/restart_tests.rs +++ b/sequencer/src/restart_tests.rs @@ -828,7 +828,7 @@ fn start_orchestrator(port: u16, nodes: &[NodeParams], builder_port: u16) -> Joi }) .collect(); - let mut config = NetworkConfig:: { + let mut config = NetworkConfig:: { indexed_da: false, libp2p_config: Some(Libp2pConfig { bootstrap_nodes }), ..Default::default() diff --git a/sequencer/src/state_signature.rs b/sequencer/src/state_signature.rs index 9eeb1bf798..43329323d2 100644 --- a/sequencer/src/state_signature.rs +++ b/sequencer/src/state_signature.rs @@ -2,34 +2,29 @@ use std::collections::{HashMap, VecDeque}; -use ark_ff::PrimeField; -use ark_serialize::CanonicalSerialize; use async_lock::RwLock; -use espresso_types::Leaf2; use hotshot::types::{Event, EventType}; use hotshot_stake_table::vec_based::StakeTable; use hotshot_types::{ event::LeafInfo, light_client::{ - CircuitField, LightClientState, StateSignature, StateSignatureRequestBody, + CircuitField, LightClientState, StateSignKey, StateSignature, StateSignatureRequestBody, StateSignatureScheme, StateVerKey, }, signature_key::BLSPubKey, traits::{ - node_implementation::ConsensusTime, + block_contents::BlockHeader, signature_key::StakeTableEntryType, stake_table::{SnapshotVersion, StakeTableScheme as _}, }, PeerConfig, }; -use jf_crhf::CRHF; -use jf_rescue::{crhf::VariableLengthRescueCRHF, RescueError}; use jf_signature::SignatureScheme; use surf_disco::{Client, Url}; use tide_disco::error::ServerError; use vbs::version::StaticVersionType; -use crate::{SeqTypes, StateKeyPair}; +use crate::SeqTypes; /// A relay server that's collecting and serving the light client state signatures pub mod relay_server; @@ -39,8 +34,11 @@ const SIGNATURE_STORAGE_CAPACITY: usize = 100; #[derive(Debug)] pub struct StateSigner { - /// Key pair for signing a new light client state - key_pair: StateKeyPair, + /// Key for signing a new light client state + sign_key: StateSignKey, + + /// Key for verifying a light client state + ver_key: StateVerKey, /// The most recent light client state signatures signatures: RwLock, @@ -54,9 +52,14 @@ pub struct StateSigner { } impl StateSigner { - pub fn new(key_pair: StateKeyPair, stake_table_comm: StakeTableCommitmentType) -> Self { + pub fn new( + sign_key: StateSignKey, + ver_key: StateVerKey, + stake_table_comm: StakeTableCommitmentType, + ) -> Self { Self { - key_pair, + sign_key, + ver_key, stake_table_comm, signatures: Default::default(), relay_server_client: Default::default(), @@ -76,14 +79,17 @@ impl StateSigner { let Some(LeafInfo { leaf, .. }) = leaf_chain.first() else { return; }; - match form_light_client_state(leaf) { + match leaf + .block_header() + .get_light_client_state(leaf.view_number()) + { Ok(state) => { let signature = self.sign_new_state(&state).await; tracing::debug!("New leaves decided. Latest block height: {}", leaf.height(),); if let Some(client) = &self.relay_server_client { let request_body = StateSignatureRequestBody { - key: self.key_pair.ver_key(), + key: self.ver_key.clone(), state, signature, }; @@ -113,18 +119,13 @@ impl StateSigner { /// Sign the light client state at given height and store it. async fn sign_new_state(&self, state: &LightClientState) -> StateSignature { let msg: [CircuitField; 3] = state.into(); - let signature = StateSignatureScheme::sign( - &(), - self.key_pair.sign_key_ref(), - msg, - &mut rand::thread_rng(), - ) - .unwrap(); + let signature = + StateSignatureScheme::sign(&(), &self.sign_key, msg, &mut rand::thread_rng()).unwrap(); let mut pool_guard = self.signatures.write().await; pool_guard.push( - state.block_height as u64, + state.block_height, StateSignatureRequestBody { - key: self.key_pair.ver_key(), + key: self.ver_key.clone(), state: state.clone(), signature: signature.clone(), }, @@ -137,34 +138,6 @@ impl StateSigner { } } -fn hash_bytes_to_field(bytes: &[u8]) -> Result { - // make sure that `mod_order` won't happen. - let bytes_len = ((::MODULUS_BIT_SIZE + 7) / 8 - 1) as usize; - let elem = bytes - .chunks(bytes_len) - .map(CircuitField::from_le_bytes_mod_order) - .collect::>(); - Ok(VariableLengthRescueCRHF::<_, 1>::evaluate(elem)?[0]) -} - -fn form_light_client_state(leaf: &Leaf2) -> anyhow::Result { - let header = leaf.block_header(); - let mut block_comm_root_bytes = vec![]; - header - .block_merkle_tree_root() - .serialize_compressed(&mut block_comm_root_bytes)?; - - let mut fee_ledger_comm_bytes = vec![]; - header - .fee_merkle_tree_root() - .serialize_compressed(&mut fee_ledger_comm_bytes)?; - Ok(LightClientState { - view_number: leaf.view_number().u64() as usize, - block_height: leaf.height() as usize, - block_comm_root: hash_bytes_to_field(&block_comm_root_bytes)?, - }) -} - /// A rolling in-memory storage for the most recent light client state signatures. #[derive(Debug, Default)] pub struct StateSignatureMemStorage { @@ -191,7 +164,7 @@ pub type StakeTableCommitmentType = (CircuitField, CircuitField, CircuitField); /// Helper function for stake table commitment pub fn static_stake_table_commitment( - known_nodes_with_stakes: &[PeerConfig], + known_nodes_with_stakes: &[PeerConfig], capacity: usize, ) -> (CircuitField, CircuitField, CircuitField) { let mut st = StakeTable::::new(capacity); diff --git a/sequencer/src/state_signature/relay_server.rs b/sequencer/src/state_signature/relay_server.rs index c718d8a185..94c8495ead 100644 --- a/sequencer/src/state_signature/relay_server.rs +++ b/sequencer/src/state_signature/relay_server.rs @@ -105,7 +105,7 @@ impl StateRelayServerDataSource for StateRelayServerState { state: LightClientState, signature: StateSignature, ) -> Result<(), Error> { - if (state.block_height as u64) <= self.latest_block_height.unwrap_or(0) { + if state.block_height <= self.latest_block_height.unwrap_or(0) { // This signature is no longer needed return Ok(()); } @@ -124,7 +124,7 @@ impl StateRelayServerDataSource for StateRelayServerState { "The posted signature is not valid.".to_owned(), )); } - let block_height = state.block_height as u64; + let block_height = state.block_height; // TODO(Chengyu): this serialization should be removed once `LightClientState` implements `Eq`. let bundles_at_height = self.bundles.entry(block_height).or_insert_with(|| { self.queue.insert(block_height); diff --git a/types/src/v0/config.rs b/types/src/v0/config.rs index 860871f0ca..274552b1db 100644 --- a/types/src/v0/config.rs +++ b/types/src/v0/config.rs @@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize}; use tide_disco::Url; use vec1::Vec1; +use super::SeqTypes; use crate::PubKey; /// This struct defines the public Hotshot validator configuration. @@ -25,18 +26,17 @@ pub struct PublicValidatorConfig { state_key_pair: String, } -impl From> for PublicValidatorConfig { - fn from(v: ValidatorConfig) -> Self { - let ValidatorConfig:: { +impl From> for PublicValidatorConfig { + fn from(v: ValidatorConfig) -> Self { + let ValidatorConfig:: { public_key, private_key: _, stake_value, - state_key_pair, + state_public_key, + state_private_key: _, is_da, } = v; - let state_public_key = state_key_pair.ver_key(); - Self { public_key, stake_value, @@ -55,8 +55,8 @@ impl From> for PublicValidatorConfig { pub struct PublicHotShotConfig { start_threshold: (u64, u64), num_nodes_with_stake: NonZeroUsize, - known_nodes_with_stake: Vec>, - known_da_nodes: Vec>, + known_nodes_with_stake: Vec>, + known_da_nodes: Vec>, da_staked_committee_size: usize, fixed_leader_for_gpuvid: usize, next_view_timeout: u64, @@ -77,12 +77,12 @@ pub struct PublicHotShotConfig { epoch_start_block: u64, } -impl From> for PublicHotShotConfig { - fn from(v: HotShotConfig) -> Self { +impl From> for PublicHotShotConfig { + fn from(v: HotShotConfig) -> Self { // Destructure all fields from HotShotConfig to return an error // if new fields are added to HotShotConfig. This makes sure that we handle // all fields appropriately and do not miss any updates. - let HotShotConfig:: { + let HotShotConfig:: { start_threshold, num_nodes_with_stake, known_nodes_with_stake, @@ -135,7 +135,7 @@ impl From> for PublicHotShotConfig { } impl PublicHotShotConfig { - pub fn into_hotshot_config(self) -> HotShotConfig { + pub fn into_hotshot_config(self) -> HotShotConfig { HotShotConfig { start_threshold: self.start_threshold, num_nodes_with_stake: self.num_nodes_with_stake, @@ -162,11 +162,11 @@ impl PublicHotShotConfig { } } - pub fn known_nodes_with_stake(&self) -> Vec> { + pub fn known_nodes_with_stake(&self) -> Vec> { self.known_nodes_with_stake.clone() } - pub fn known_da_nodes(&self) -> Vec> { + pub fn known_da_nodes(&self) -> Vec> { self.known_da_nodes.clone() } } @@ -195,8 +195,8 @@ pub struct PublicNetworkConfig { random_builder: Option, } -impl From> for PublicNetworkConfig { - fn from(cfg: NetworkConfig) -> Self { +impl From> for PublicNetworkConfig { + fn from(cfg: NetworkConfig) -> Self { Self { rounds: cfg.rounds, indexed_da: cfg.indexed_da, @@ -225,8 +225,8 @@ impl From> for PublicNetworkConfig { impl PublicNetworkConfig { pub fn into_network_config( self, - my_own_validator_config: ValidatorConfig, - ) -> anyhow::Result> { + my_own_validator_config: ValidatorConfig, + ) -> anyhow::Result> { let node_index = self .config .known_nodes_with_stake diff --git a/types/src/v0/impls/header.rs b/types/src/v0/impls/header.rs index 3f765bca55..91e75cb680 100644 --- a/types/src/v0/impls/header.rs +++ b/types/src/v0/impls/header.rs @@ -7,9 +7,10 @@ use ethers_conv::ToAlloy; use hotshot_query_service::{availability::QueryableHeader, explorer::ExplorerHeader}; use hotshot_types::{ data::VidCommitment, + light_client::LightClientState, traits::{ block_contents::{BlockHeader, BuilderFee}, - node_implementation::NodeType, + node_implementation::{ConsensusTime, NodeType}, signature_key::BuilderSignatureKey, BlockPayload, ValidatedState as _, }, @@ -1123,6 +1124,23 @@ impl BlockHeader for Header { fn builder_commitment(&self) -> BuilderCommitment { self.builder_commitment().clone() } + + fn get_light_client_state( + &self, + view: ::View, + ) -> anyhow::Result { + let mut block_comm_root_bytes = vec![]; + self.block_merkle_tree_root() + .serialize_compressed(&mut block_comm_root_bytes)?; + + Ok(LightClientState { + view_number: view.u64(), + block_height: self.height(), + block_comm_root: hotshot_types::light_client::hash_bytes_to_field( + &block_comm_root_bytes, + )?, + }) + } } impl QueryableHeader for Header { diff --git a/types/src/v0/impls/instance_state.rs b/types/src/v0/impls/instance_state.rs index cf5c2dbeaf..1c8c8a5add 100644 --- a/types/src/v0/impls/instance_state.rs +++ b/types/src/v0/impls/instance_state.rs @@ -7,7 +7,7 @@ use vbs::version::{StaticVersion, StaticVersionType}; use super::state::ValidatedState; use crate::v0::{ - traits::StateCatchup, v0_99::ChainConfig, GenesisHeader, L1BlockInfo, L1Client, PubKey, + traits::StateCatchup, v0_99::ChainConfig, GenesisHeader, L1BlockInfo, L1Client, SeqTypes, Timestamp, Upgrade, UpgradeMode, }; @@ -163,7 +163,7 @@ impl Default for NodeState { impl InstanceState for NodeState {} impl Upgrade { - pub fn set_hotshot_config_parameters(&self, config: &mut HotShotConfig) { + pub fn set_hotshot_config_parameters(&self, config: &mut HotShotConfig) { match &self.mode { UpgradeMode::View(v) => { config.start_proposing_view = v.start_proposing_view; diff --git a/types/src/v0/impls/stake_table.rs b/types/src/v0/impls/stake_table.rs index d65d6f8335..f56accfe4a 100644 --- a/types/src/v0/impls/stake_table.rs +++ b/types/src/v0/impls/stake_table.rs @@ -81,8 +81,8 @@ impl StakeTables { StakeTableChange::Remove(_) => None, }); - let mut consensus_stake_table: Vec> = vec![]; - let mut da_members: Vec> = vec![]; + let mut consensus_stake_table: Vec> = vec![]; + let mut da_members: Vec> = vec![]; for node in currently_staking { consensus_stake_table.push(node.clone().into()); if node.da { @@ -143,19 +143,19 @@ struct Committee { /// The nodes eligible for leadership. /// NOTE: This is currently a hack because the DA leader needs to be the quorum /// leader but without voting rights. - eligible_leaders: Vec>, + eligible_leaders: Vec>, /// Keys for nodes participating in the network - stake_table: Vec>, + stake_table: Vec>, /// Keys for DA members - da_members: Vec>, + da_members: Vec>, /// Stake entries indexed by public key, for efficient lookup. - indexed_stake_table: HashMap>, + indexed_stake_table: HashMap>, /// DA entries indexed by public key, for efficient lookup. - indexed_da_members: HashMap>, + indexed_da_members: HashMap>, } impl EpochCommittees { @@ -219,8 +219,8 @@ impl EpochCommittees { pub fn new_stake( // TODO remove `new` from trait and rename this to `new`. // https://github.com/EspressoSystems/HotShot/commit/fcb7d54a4443e29d643b3bbc53761856aef4de8b - committee_members: Vec>, - da_members: Vec>, + committee_members: Vec>, + da_members: Vec>, instance_state: &NodeState, epoch_size: u64, ) -> Self { @@ -312,14 +312,14 @@ impl Membership for EpochCommittees { fn new( // TODO remove `new` from trait and remove this fn as well. // https://github.com/EspressoSystems/HotShot/commit/fcb7d54a4443e29d643b3bbc53761856aef4de8b - _committee_members: Vec>, - _da_members: Vec>, + _committee_members: Vec>, + _da_members: Vec>, ) -> Self { panic!("This function has been replaced with new_stake()"); } /// Get the stake table for the current view - fn stake_table(&self, epoch: Option) -> Vec> { + fn stake_table(&self, epoch: Option) -> Vec> { if let Some(st) = self.state(&epoch) { st.stake_table.clone() } else { @@ -327,7 +327,7 @@ impl Membership for EpochCommittees { } } /// Get the stake table for the current view - fn da_stake_table(&self, epoch: Option) -> Vec> { + fn da_stake_table(&self, epoch: Option) -> Vec> { if let Some(sc) = self.state(&epoch) { sc.da_members.clone() } else { @@ -376,14 +376,14 @@ impl Membership for EpochCommittees { } /// Get the stake table entry for a public key - fn stake(&self, pub_key: &PubKey, epoch: Option) -> Option> { + fn stake(&self, pub_key: &PubKey, epoch: Option) -> Option> { // Only return the stake if it is above zero self.state(&epoch) .and_then(|h| h.indexed_stake_table.get(pub_key).cloned()) } /// Get the DA stake table entry for a public key - fn da_stake(&self, pub_key: &PubKey, epoch: Option) -> Option> { + fn da_stake(&self, pub_key: &PubKey, epoch: Option) -> Option> { // Only return the stake if it is above zero self.state(&epoch) .and_then(|h| h.indexed_da_members.get(pub_key).cloned()) diff --git a/types/src/v0/mod.rs b/types/src/v0/mod.rs index 09126a1402..4376d49d74 100644 --- a/types/src/v0/mod.rs +++ b/types/src/v0/mod.rs @@ -2,7 +2,7 @@ use std::marker::PhantomData; use hotshot_types::{ data::{EpochNumber, ViewNumber}, - signature_key::BLSPubKey, + signature_key::{BLSPubKey, SchnorrPubKey}, traits::{ node_implementation::{NodeType, Versions}, signature_key::SignatureKey, @@ -142,6 +142,7 @@ impl NodeType for SeqTypes { type Membership = EpochCommittees; type BuilderSignatureKey = FeeAccount; type AuctionResult = SolverAuctionResults; + type StateSignatureKey = SchnorrPubKey; } #[derive(Clone, Default, Debug, Copy)] @@ -187,7 +188,7 @@ pub type Event = hotshot::types::Event; pub type PubKey = BLSPubKey; pub type PrivKey = ::PrivateKey; -pub type NetworkConfig = hotshot_types::network::NetworkConfig; +pub type NetworkConfig = hotshot_types::network::NetworkConfig; pub use self::impls::{NodeState, SolverAuctionResultsProvider, ValidatedState}; pub use crate::v0_1::{ diff --git a/types/src/v0/traits.rs b/types/src/v0/traits.rs index 839a3fe1fa..b4fa0571a5 100644 --- a/types/src/v0/traits.rs +++ b/types/src/v0/traits.rs @@ -18,7 +18,8 @@ use hotshot_types::{ event::{HotShotAction, LeafInfo}, message::{convert_proposal, Proposal, UpgradeLock}, simple_certificate::{ - NextEpochQuorumCertificate2, QuorumCertificate, QuorumCertificate2, UpgradeCertificate, + LightClientStateUpdateCertificate, NextEpochQuorumCertificate2, QuorumCertificate, + QuorumCertificate2, UpgradeCertificate, }, traits::{ node_implementation::{ConsensusTime, NodeType, Versions}, @@ -542,6 +543,7 @@ pub trait SequencerPersistence: Sized + Send + Sync + Clone + 'static { async fn load_start_epoch_info(&self) -> anyhow::Result>>; async fn load_stake(&self, epoch: EpochNumber) -> anyhow::Result>; async fn store_stake(&self, epoch: EpochNumber, stake: StakeTables) -> anyhow::Result<()>; + async fn load_state_cert(&self) -> anyhow::Result>; /// Load the latest known consensus state. /// @@ -650,6 +652,11 @@ pub trait SequencerPersistence: Sized + Send + Sync + Clone + 'static { .await .context("loading start epoch info")?; + let state_cert = self + .load_state_cert() + .await + .context("loading light client state update certificate")?; + tracing::info!( ?leaf, ?view, @@ -660,6 +667,7 @@ pub trait SequencerPersistence: Sized + Send + Sync + Clone + 'static { ?undecided_state, ?saved_proposals, ?upgrade_certificate, + ?state_cert, "loaded consensus state" ); @@ -685,6 +693,7 @@ pub trait SequencerPersistence: Sized + Send + Sync + Clone + 'static { undecided_state, saved_vid_shares: Default::default(), // TODO: implement saved_vid_shares start_epoch_info, + state_cert, }, anchor_view, )) @@ -849,6 +858,11 @@ pub trait SequencerPersistence: Sized + Send + Sync + Clone + 'static { epoch: ::Epoch, block_header: ::BlockHeader, ) -> anyhow::Result<()>; + + async fn add_state_cert( + &self, + state_cert: LightClientStateUpdateCertificate, + ) -> anyhow::Result<()>; } #[async_trait] @@ -993,6 +1007,13 @@ impl Storage for Arc

{ ) -> anyhow::Result<()> { (**self).add_epoch_root(epoch, block_header).await } + + async fn update_state_cert( + &self, + state_cert: LightClientStateUpdateCertificate, + ) -> anyhow::Result<()> { + (**self).add_state_cert(state_cert).await + } } /// Data that can be deserialized from a subslice of namespace payload bytes. diff --git a/types/src/v0/v0_3/stake_table.rs b/types/src/v0/v0_3/stake_table.rs index 28337fc320..87939ee6ff 100644 --- a/types/src/v0/v0_3/stake_table.rs +++ b/types/src/v0/v0_3/stake_table.rs @@ -1,4 +1,4 @@ -use crate::PubKey; +use crate::SeqTypes; use derive_more::derive::{From, Into}; use hotshot_contract_adapter::stake_table::NodeInfoJf; use hotshot_types::{network::PeerConfigKeys, PeerConfig}; @@ -9,15 +9,15 @@ pub struct PermissionedStakeTableEntry(NodeInfoJf); /// Stake table holding all staking information (DA and non-DA stakers) #[derive(Debug, Clone, Serialize, Deserialize, From)] -pub struct CombinedStakeTable(Vec>); +pub struct CombinedStakeTable(Vec>); #[derive(Clone, Debug, From, Into, Serialize, Deserialize)] /// NewType to disambiguate DA Membership -pub struct DAMembers(pub Vec>); +pub struct DAMembers(pub Vec>); #[derive(Clone, Debug, From, Into, Serialize, Deserialize)] /// NewType to disambiguate StakeTable -pub struct StakeTable(pub Vec>); +pub struct StakeTable(pub Vec>); #[derive(Clone, Debug, Serialize, Deserialize)] pub struct StakeTables { diff --git a/utils/Cargo.toml b/utils/Cargo.toml index e0ced8190d..b6305ce70b 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -23,6 +23,7 @@ ethers-conv = { workspace = true } futures = { workspace = true } hotshot = { workspace = true } hotshot-contract-adapter = { workspace = true } +hotshot-example-types = { workspace = true } hotshot-types = { workspace = true } log-panics = { workspace = true } portpicker = { workspace = true } diff --git a/utils/src/stake_table.rs b/utils/src/stake_table.rs index f82d6ac66c..d34ceb45de 100644 --- a/utils/src/stake_table.rs +++ b/utils/src/stake_table.rs @@ -14,22 +14,25 @@ use ethers::{ signers::{coins_bip39::English, MnemonicBuilder, Signer as _}, types::Address, }; -use hotshot::types::BLSPubKey; +use hotshot::types::{BLSPubKey, SchnorrPubKey}; use hotshot_contract_adapter::stake_table::{bls_jf_to_sol, NodeInfoJf}; -use hotshot_types::network::PeerConfigKeys; +use hotshot_types::{network::PeerConfigKeys, traits::node_implementation::NodeType}; use url::Url; /// A stake table config stored in a file #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[serde(bound(deserialize = ""))] -pub struct PermissionedStakeTableConfig { +pub struct PermissionedStakeTableConfig { /// The list of public keys that are initially inserted into the /// permissioned stake table contract. #[serde(default)] - pub public_keys: Vec>, + pub public_keys: Vec>, } -impl PermissionedStakeTableConfig { +impl PermissionedStakeTableConfig +where + TYPES: NodeType, +{ pub fn from_toml_file(path: &Path) -> anyhow::Result { let config_file_as_string: String = fs::read_to_string(path) .unwrap_or_else(|_| panic!("Could not read config file located at {}", path.display())); @@ -45,8 +48,11 @@ impl PermissionedStakeTableConfig { } } -impl From for Vec { - fn from(value: PermissionedStakeTableConfig) -> Self { +impl From> for Vec +where + TYPES: NodeType, +{ + fn from(value: PermissionedStakeTableConfig) -> Self { value .public_keys .into_iter() @@ -72,14 +78,17 @@ impl From for BLSPubKey { /// Information to add and remove stakers in the permissioned stake table contract. #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[serde(bound(deserialize = ""))] -pub struct PermissionedStakeTableUpdate { +pub struct PermissionedStakeTableUpdate { #[serde(default)] stakers_to_remove: Vec, #[serde(default)] - new_stakers: Vec>, + new_stakers: Vec>, } -impl PermissionedStakeTableUpdate { +impl PermissionedStakeTableUpdate +where + TYPES: NodeType, +{ pub fn from_toml_file(path: &Path) -> anyhow::Result { let config_file_as_string: String = fs::read_to_string(path) .unwrap_or_else(|_| panic!("Could not read config file located at {}", path.display())); @@ -112,14 +121,17 @@ impl PermissionedStakeTableUpdate { } } -pub async fn update_stake_table( +pub async fn update_stake_table( l1url: Url, l1_interval: Duration, mnemonic: String, account_index: u32, contract_address: Address, - update: PermissionedStakeTableUpdate, -) -> anyhow::Result<()> { + update: PermissionedStakeTableUpdate, +) -> anyhow::Result<()> +where + TYPES: NodeType, +{ let provider = Provider::::try_from(l1url.to_string())?.interval(l1_interval); let chain_id = provider.get_chainid().await?.as_u64(); let wallet = MnemonicBuilder::::default() @@ -145,7 +157,11 @@ pub async fn update_stake_table( #[cfg(test)] mod test { use hotshot::types::{BLSPubKey, SignatureKey}; - use hotshot_types::{light_client::StateKeyPair, network::PeerConfigKeys}; + use hotshot_example_types::node_types::TestTypes; + use hotshot_types::{ + light_client::StateKeyPair, network::PeerConfigKeys, signature_key::SchnorrPubKey, + traits::node_implementation::NodeType, + }; use toml::toml; use crate::{ @@ -153,20 +169,24 @@ mod test { test_utils::setup_test, }; - fn assert_peer_config_eq(p1: &PeerConfigKeys, p2: &PeerConfigKeys) { + fn assert_peer_config_eq( + p1: &PeerConfigKeys, + p2: &PeerConfigKeys, + ) { assert_eq!(p1.stake_table_key, p2.stake_table_key); assert_eq!(p1.state_ver_key, p2.state_ver_key); assert_eq!(p1.stake, p2.stake); assert_eq!(p1.da, p2.da); } - fn mk_keys() -> Vec> { + fn mk_keys>( + ) -> Vec> { let mut keys = Vec::new(); for i in 0..3 { let (pubkey, _) = BLSPubKey::generated_from_seed_indexed([0; 32], i); let state_kp = StateKeyPair::generate_from_seed_indexed([0; 32], i).0; let ver_key = state_kp.ver_key(); - keys.push(PeerConfigKeys { + keys.push(PeerConfigKeys:: { stake_table_key: pubkey, state_ver_key: ver_key, stake: i + 1, @@ -180,7 +200,7 @@ mod test { fn test_permissioned_stake_table_from_toml() { setup_test(); - let keys = mk_keys(); + let keys = mk_keys::(); let st_key_1 = keys[0].stake_table_key.to_string(); let verkey_1 = keys[0].state_ver_key.to_string(); @@ -233,7 +253,7 @@ mod test { fn test_permissioned_stake_table_update_from_toml() { setup_test(); - let keys = mk_keys(); + let keys = mk_keys::(); let st_key_1 = keys[0].stake_table_key.to_string();