diff --git a/sequencer/src/context.rs b/sequencer/src/context.rs index 06e27e3b1c..38077308e5 100644 --- a/sequencer/src/context.rs +++ b/sequencer/src/context.rs @@ -16,7 +16,6 @@ use futures::{ stream::{Stream, StreamExt}, }; use hotshot::{ - traits::election::static_committee::StaticCommittee, types::{Event, EventType, SystemContextHandle}, MarketplaceConfig, SystemContext, }; diff --git a/sequencer/src/lib.rs b/sequencer/src/lib.rs index bba6356ad3..cabc4418d3 100644 --- a/sequencer/src/lib.rs +++ b/sequencer/src/lib.rs @@ -20,7 +20,6 @@ use espresso_types::{ }; use futures::FutureExt; use genesis::L1Finalized; -use hotshot::traits::election::static_committee::StaticCommittee; use hotshot_types::traits::election::Membership; use std::sync::Arc; // Should move `STAKE_TABLE_CAPACITY` in the sequencer repo when we have variate stake table support @@ -386,12 +385,6 @@ pub async fn init_node( topics }; - // Create the HotShot membership - let membership = StaticCommittee::new( - network_config.config.known_nodes_with_stake.clone(), - network_config.config.known_nodes_with_stake.clone(), - ); - // Initialize the push CDN network (and perform the initial connection) let cdn_network = PushCdnNetwork::new( network_params.cdn_endpoint, @@ -517,6 +510,13 @@ pub async fn init_node( current_version: V::Base::VERSION, }; + // Create the HotShot membership + let membership = StaticCommittee::new_stake( + network_config.config.known_nodes_with_stake.clone(), + network_config.config.known_nodes_with_stake.clone(), + &instance_state, + ); + let mut ctx = SequencerContext::init( network_config, validator_config, @@ -954,6 +954,7 @@ pub mod testing { .with_upgrades(upgrades); // Create the HotShot membership + // TODO use our own implementation and pull from contract let membership = StaticCommittee::new( config.known_nodes_with_stake.clone(), config.known_nodes_with_stake.clone(), diff --git a/types/src/v0/impls/instance_state.rs b/types/src/v0/impls/instance_state.rs index 71a387bbcb..4e11787f00 100644 --- a/types/src/v0/impls/instance_state.rs +++ b/types/src/v0/impls/instance_state.rs @@ -1,5 +1,5 @@ use crate::v0::{ - traits::StateCatchup, v0_99::ChainConfig, GenesisHeader, L1BlockInfo, L1Client, PubKey, + traits::StateCatchup, v0_3::ChainConfig, GenesisHeader, L1BlockInfo, L1Client, PubKey, Timestamp, Upgrade, UpgradeMode, }; use hotshot_types::traits::states::InstanceState; @@ -17,7 +17,7 @@ use super::state::ValidatedState; #[derive(derive_more::Debug, Clone)] pub struct NodeState { pub node_id: u64, - pub chain_config: crate::v0_99::ChainConfig, + pub chain_config: crate::v0_3::ChainConfig, pub l1_client: L1Client, #[debug("{}", peers.name())] pub peers: Arc, @@ -46,7 +46,7 @@ pub struct NodeState { impl NodeState { pub fn new( node_id: u64, - chain_config: ChainConfig, + chain_config: crate::v0_3::ChainConfig, l1_client: L1Client, catchup: impl StateCatchup + 'static, current_version: Version, diff --git a/types/src/v0/impls/l1.rs b/types/src/v0/impls/l1.rs index aac8404d6b..238f1a9307 100644 --- a/types/src/v0/impls/l1.rs +++ b/types/src/v0/impls/l1.rs @@ -19,7 +19,11 @@ use futures::{ future::Future, stream::{self, StreamExt}, }; -use hotshot_types::traits::metrics::Metrics; +use hotshot::types::SignatureKey; +use hotshot_types::{ + stake_table::StakeTableEntry, + traits::{metrics::Metrics, node_implementation::NodeType}, +}; use lru::LruCache; use serde::{de::DeserializeOwned, Serialize}; use tokio::{ @@ -779,6 +783,17 @@ impl L1Client { }); events.flatten().map(FeeInfo::from).collect().await } + + /// Get `StakeTable` at block height. + pub async fn get_stake_table( + &self, + _block: u64, + _address: Address, + ) -> Vec<::StakeTableEntry> { + // TODO we either need address from configuration or contract-bindings. + // TODO epoch size may need to be passed in as well + unimplemented!(); + } } impl L1State { @@ -1246,4 +1261,39 @@ mod test { }; tracing::info!(?final_state, "state updated"); } + + #[tokio::test] + async fn test_get_stake_table() -> anyhow::Result<()> { + setup_test(); + + // how many deposits will we make + let deposits = 5; + let deploy_txn_count = 2; + + let anvil = Anvil::new().spawn(); + let wallet_address = anvil.addresses().first().cloned().unwrap(); + let l1_client = L1Client::new(anvil.endpoint().parse().unwrap()); + let wallet: LocalWallet = anvil.keys()[0].clone().into(); + + // In order to deposit we need a provider that can sign. + let provider = + Provider::::try_from(anvil.endpoint())?.interval(Duration::from_millis(10u64)); + let client = + SignerMiddleware::new(provider.clone(), wallet.with_chain_id(anvil.chain_id())); + let client = Arc::new(client); + + // Initialize a contract with some deposits + + // deploy the fee contract + let stake_table_contract = + contract_bindings::permissioned_stake_table::PermissionedStakeTable::deploy( + client.clone(), + (), + ) + .unwrap() + .send() + .await?; + + Ok(()) + } } diff --git a/types/src/v0/impls/mod.rs b/types/src/v0/impls/mod.rs index eb72ebb143..29fb39fb61 100644 --- a/types/src/v0/impls/mod.rs +++ b/types/src/v0/impls/mod.rs @@ -8,14 +8,18 @@ mod header; mod instance_state; mod l1; mod solver; +mod stake_table; mod state; mod transaction; pub use auction::SolverAuctionResultsProvider; pub use fee_info::{retain_accounts, FeeError}; pub use instance_state::NodeState; -pub use state::ProposalValidationError; -pub use state::{get_l1_deposits, BuilderValidationError, StateValidationError, ValidatedState}; +pub use stake_table::StaticCommittee; +pub use state::{ + get_l1_deposits, BuilderValidationError, ProposalValidationError, StateValidationError, + ValidatedState, +}; #[cfg(any(test, feature = "testing"))] pub use instance_state::mock; diff --git a/types/src/v0/impls/stake_table.rs b/types/src/v0/impls/stake_table.rs new file mode 100644 index 0000000000..16555fa665 --- /dev/null +++ b/types/src/v0/impls/stake_table.rs @@ -0,0 +1,324 @@ +use super::{L1Client, NodeState}; +use ethers::{abi::Address, types::U256}; +use hotshot::types::SignatureKey; +use hotshot_types::{ + traits::{ + election::Membership, node_implementation::NodeType, signature_key::StakeTableEntryType, + }, + PeerConfig, +}; +use std::{cmp::max, collections::BTreeMap, num::NonZeroU64, str::FromStr}; +use thiserror::Error; +use url::Url; + +#[derive(Clone, Debug)] +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<::StakeTableEntry>, + + /// The nodes on the committee and their stake + stake_table: Vec<::StakeTableEntry>, + + /// The nodes on the committee and their stake + da_stake_table: Vec<::StakeTableEntry>, + + /// The nodes on the committee and their stake, indexed by public key + indexed_stake_table: + BTreeMap::StakeTableEntry>, + + /// The nodes on the committee and their stake, indexed by public key + indexed_da_stake_table: + BTreeMap::StakeTableEntry>, + + /// Number of blocks in an epoch + epoch_size: u64, + + /// Address of StakeTable contract (proxy address) + contract_address: Option
, + + /// L1 provider + provider: L1Client, +} + +impl StaticCommittee { + /// Updates `Self.stake_table` with stake_table for + /// `Self.contract_address` at `l1_block_height`. This is intended + /// to be called before calling `self.stake()` so that + /// `Self.stake_table` only needs to be updated once in a given + /// life-cycle but may be read from many times. + async fn update_stake_table(&mut self, l1_block_height: u64) { + let table: Vec<<::SignatureKey as SignatureKey>::StakeTableEntry> = self + .provider + .get_stake_table::(l1_block_height, self.contract_address.unwrap()) + .await; + self.stake_table = table; + } + // We need a constructor to match our concrete type. + pub fn new_stake( + // TODO remove `new` from trait and rename this to `new`. + // https://github.com/EspressoSystems/HotShot/commit/fcb7d54a4443e29d643b3bbc53761856aef4de8b + committee_members: Vec::SignatureKey>>, + da_members: Vec::SignatureKey>>, + instance_state: &NodeState, + epoch_size: u64, + ) -> Self { + // For each eligible leader, get the stake table entry + let eligible_leaders: Vec<::StakeTableEntry> = + committee_members + .iter() + .map(|member| member.stake_table_entry.clone()) + .filter(|entry| entry.stake() > U256::zero()) + .collect(); + + // For each member, get the stake table entry + let members: Vec<::StakeTableEntry> = + committee_members + .iter() + .map(|member| member.stake_table_entry.clone()) + .filter(|entry| entry.stake() > U256::zero()) + .collect(); + + // For each member, get the stake table entry + let da_members: Vec<::StakeTableEntry> = da_members + .iter() + .map(|member| member.stake_table_entry.clone()) + .filter(|entry| entry.stake() > U256::zero()) + .collect(); + + // Index the stake table by public key + let indexed_stake_table: BTreeMap< + TYPES::SignatureKey, + ::StakeTableEntry, + > = members + .iter() + .map(|entry| (TYPES::SignatureKey::public_key(entry), entry.clone())) + .collect(); + + // Index the stake table by public key + let indexed_da_stake_table: BTreeMap< + TYPES::SignatureKey, + ::StakeTableEntry, + > = da_members + .iter() + .map(|entry| (TYPES::SignatureKey::public_key(entry), entry.clone())) + .collect(); + + Self { + eligible_leaders, + stake_table: members, + da_stake_table: da_members, + indexed_stake_table, + indexed_da_stake_table, + epoch_size, + provider: instance_state.l1_client.clone(), + contract_address: instance_state.chain_config.stake_table_contract, + } + } +} + +#[derive(Error, Debug)] +#[error("Could not lookup leader")] // TODO error variants? message? +pub struct LeaderLookupError; + +impl Membership for StaticCommittee { + type Error = LeaderLookupError; + + /// DO NOT USE. Dummy constructor to comply w/ trait. + fn new( + // TODO remove `new` from trait and remove this fn as well. + // https://github.com/EspressoSystems/HotShot/commit/fcb7d54a4443e29d643b3bbc53761856aef4de8b + committee_members: Vec::SignatureKey>>, + da_members: Vec::SignatureKey>>, + ) -> Self { + // For each eligible leader, get the stake table entry + let eligible_leaders: Vec<::StakeTableEntry> = + committee_members + .iter() + .map(|member| member.stake_table_entry.clone()) + .filter(|entry| entry.stake() > U256::zero()) + .collect(); + + // For each member, get the stake table entry + let members: Vec<::StakeTableEntry> = + committee_members + .iter() + .map(|member| member.stake_table_entry.clone()) + .filter(|entry| entry.stake() > U256::zero()) + .collect(); + + // For each member, get the stake table entry + let da_members: Vec<::StakeTableEntry> = da_members + .iter() + .map(|member| member.stake_table_entry.clone()) + .filter(|entry| entry.stake() > U256::zero()) + .collect(); + + // Index the stake table by public key + let indexed_stake_table: BTreeMap< + TYPES::SignatureKey, + ::StakeTableEntry, + > = members + .iter() + .map(|entry| (TYPES::SignatureKey::public_key(entry), entry.clone())) + .collect(); + + // Index the stake table by public key + let indexed_da_stake_table: BTreeMap< + TYPES::SignatureKey, + ::StakeTableEntry, + > = da_members + .iter() + .map(|entry| (TYPES::SignatureKey::public_key(entry), entry.clone())) + .collect(); + + Self { + eligible_leaders, + stake_table: members, + da_stake_table: da_members, + indexed_stake_table, + indexed_da_stake_table, + epoch_size: 12, // TODO get the real number from config (I think) + provider: L1Client::http(Url::from_str("http:://ab.b").unwrap()), + contract_address: None, + } + } + /// Get the stake table for the current view + fn stake_table( + &self, + _epoch: ::Epoch, + ) -> Vec<<::SignatureKey as SignatureKey>::StakeTableEntry> { + self.stake_table.clone() + } + /// Get the stake table for the current view + fn da_stake_table( + &self, + _epoch: ::Epoch, + ) -> Vec<<::SignatureKey as SignatureKey>::StakeTableEntry> { + self.da_stake_table.clone() + } + + /// Get all members of the committee for the current view + fn committee_members( + &self, + _view_number: ::View, + _epoch: ::Epoch, + ) -> std::collections::BTreeSet<::SignatureKey> { + self.stake_table + .iter() + .map(TYPES::SignatureKey::public_key) + .collect() + } + + /// Get all members of the committee for the current view + fn da_committee_members( + &self, + _view_number: ::View, + _epoch: ::Epoch, + ) -> std::collections::BTreeSet<::SignatureKey> { + self.da_stake_table + .iter() + .map(TYPES::SignatureKey::public_key) + .collect() + } + + /// Get all eligible leaders of the committee for the current view + fn committee_leaders( + &self, + _view_number: ::View, + _epoch: ::Epoch, + ) -> std::collections::BTreeSet<::SignatureKey> { + self.eligible_leaders + .iter() + .map(TYPES::SignatureKey::public_key) + .collect() + } + + /// Get the stake table entry for a public key + fn stake( + &self, + pub_key: &::SignatureKey, + _epoch: ::Epoch, + ) -> Option<::StakeTableEntry> { + // Only return the stake if it is above zero + self.indexed_stake_table.get(pub_key).cloned() + } + + /// Get the DA stake table entry for a public key + fn da_stake( + &self, + pub_key: &::SignatureKey, + _epoch: ::Epoch, + ) -> Option<::StakeTableEntry> { + // Only return the stake if it is above zero + self.indexed_da_stake_table.get(pub_key).cloned() + } + + /// Check if a node has stake in the committee + fn has_stake( + &self, + pub_key: &::SignatureKey, + _epoch: ::Epoch, + ) -> bool { + self.indexed_stake_table + .get(pub_key) + .is_some_and(|x| x.stake() > U256::zero()) + } + + /// Check if a node has stake in the committee + fn has_da_stake( + &self, + pub_key: &::SignatureKey, + _epoch: ::Epoch, + ) -> bool { + self.indexed_da_stake_table + .get(pub_key) + .is_some_and(|x| x.stake() > U256::zero()) + } + + /// Index the vector of public keys with the current view number + fn lookup_leader( + &self, + view_number: TYPES::View, + _epoch: ::Epoch, + ) -> Result { + let index = *view_number as usize % self.eligible_leaders.len(); + let res = self.eligible_leaders[index].clone(); + Ok(TYPES::SignatureKey::public_key(&res)) + } + + /// Get the total number of nodes in the committee + fn total_nodes(&self, _epoch: ::Epoch) -> usize { + self.stake_table.len() + } + + /// Get the total number of DA nodes in the committee + fn da_total_nodes(&self, _epoch: ::Epoch) -> usize { + self.da_stake_table.len() + } + + /// Get the voting success threshold for the committee + fn success_threshold(&self, _epoch: ::Epoch) -> NonZeroU64 { + NonZeroU64::new(((self.stake_table.len() as u64 * 2) / 3) + 1).unwrap() + } + + /// Get the voting success threshold for the committee + fn da_success_threshold(&self, _epoch: ::Epoch) -> NonZeroU64 { + NonZeroU64::new(((self.da_stake_table.len() as u64 * 2) / 3) + 1).unwrap() + } + + /// Get the voting failure threshold for the committee + fn failure_threshold(&self, _epoch: ::Epoch) -> NonZeroU64 { + NonZeroU64::new(((self.stake_table.len() as u64) / 3) + 1).unwrap() + } + + /// Get the voting upgrade threshold for the committee + fn upgrade_threshold(&self, _epoch: ::Epoch) -> NonZeroU64 { + NonZeroU64::new(max( + (self.stake_table.len() as u64 * 9) / 10, + ((self.stake_table.len() as u64 * 2) / 3) + 1, + )) + .unwrap() + } +} diff --git a/types/src/v0/impls/state.rs b/types/src/v0/impls/state.rs index 85fbb06146..71f56626be 100644 --- a/types/src/v0/impls/state.rs +++ b/types/src/v0/impls/state.rs @@ -31,7 +31,8 @@ use super::{ }; use crate::{ traits::StateCatchup, - v0_99::{ChainConfig, FullNetworkTx, IterableFeeInfo, ResolvableChainConfig}, + v0_3::{ChainConfig, ResolvableChainConfig}, + v0_99::{FullNetworkTx, IterableFeeInfo}, BlockMerkleTree, Delta, FeeAccount, FeeAmount, FeeInfo, FeeMerkleTree, Header, Leaf2, NsTableValidationError, PayloadByteLen, SeqTypes, UpgradeType, BLOCK_MERKLE_TREE_HEIGHT, FEE_MERKLE_TREE_HEIGHT, diff --git a/types/src/v0/v0_3/chain_config.rs b/types/src/v0/v0_3/chain_config.rs new file mode 100644 index 0000000000..3e26df4deb --- /dev/null +++ b/types/src/v0/v0_3/chain_config.rs @@ -0,0 +1,175 @@ +use crate::{v0_1, BlockSize, ChainId, FeeAccount, FeeAmount}; +use committable::{Commitment, Committable}; +use ethers::types::{Address, U256}; +use itertools::Either; +use serde::{Deserialize, Serialize}; + +/// Global variables for an Espresso blockchain. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ChainConfig { + /// Espresso chain ID + pub chain_id: ChainId, + + /// Maximum size in bytes of a block + pub max_block_size: BlockSize, + + /// Minimum fee in WEI per byte of payload + pub base_fee: FeeAmount, + + /// Fee contract address on L1. + /// + /// This is optional so that fees can easily be toggled on/off, with no need to deploy a + /// contract when they are off. In a future release, after fees are switched on and thoroughly + /// tested, this may be made mandatory. + pub fee_contract: Option
, + + /// Account that receives sequencing fees. + /// + /// This account in the Espresso fee ledger will always receive every fee paid in Espresso, + /// regardless of whether or not their is a `fee_contract` deployed. Once deployed, the fee + /// contract can decide what to do with tokens locked in this account in Espresso. + pub fee_recipient: FeeAccount, + + /// `StakeTable `(proxy) contract address on L1. + /// + /// This is optional so that stake can easily be toggled on/off, with no need to deploy a + /// contract when they are off. In a future release, after PoS is switched on and thoroughly + /// tested, this may be made mandatory. + pub stake_table_contract: Option
, +} + +#[derive(Clone, Debug, Copy, PartialEq, Deserialize, Serialize, Eq, Hash)] +pub struct ResolvableChainConfig { + pub(crate) chain_config: Either>, +} + +impl Committable for ChainConfig { + fn tag() -> String { + "CHAIN_CONFIG".to_string() + } + + fn commit(&self) -> Commitment { + let comm = committable::RawCommitmentBuilder::new(&Self::tag()) + .fixed_size_field("chain_id", &self.chain_id.to_fixed_bytes()) + .u64_field("max_block_size", *self.max_block_size) + .fixed_size_field("base_fee", &self.base_fee.to_fixed_bytes()) + .fixed_size_field("fee_recipient", &self.fee_recipient.to_fixed_bytes()); + let comm = if let Some(addr) = self.fee_contract { + comm.u64_field("fee_contract", 1).fixed_size_bytes(&addr.0) + } else { + comm.u64_field("fee_contract", 0) + }; + // With `ChainConfig` upgrades we want commitments w/out + // fields added >= v0_3 to have the same commitment as <= v0_3 + // commitment. Therefore `None` values are simply ignored. + let comm = if let Some(addr) = self.stake_table_contract { + comm.u64_field("fee_contract", 1).fixed_size_bytes(&addr.0) + } else { + comm + }; + + comm.finalize() + } +} + +impl ResolvableChainConfig { + pub fn commit(&self) -> Commitment { + match self.chain_config { + Either::Left(config) => config.commit(), + Either::Right(commitment) => commitment, + } + } + pub fn resolve(self) -> Option { + match self.chain_config { + Either::Left(config) => Some(config), + Either::Right(_) => None, + } + } +} + +impl From> for ResolvableChainConfig { + fn from(value: Commitment) -> Self { + Self { + chain_config: Either::Right(value), + } + } +} + +impl From for ResolvableChainConfig { + fn from(value: ChainConfig) -> Self { + Self { + chain_config: Either::Left(value), + } + } +} + +impl From<&v0_1::ResolvableChainConfig> for ResolvableChainConfig { + fn from( + &v0_1::ResolvableChainConfig { chain_config }: &v0_1::ResolvableChainConfig, + ) -> ResolvableChainConfig { + match chain_config { + Either::Left(chain_config) => ResolvableChainConfig { + chain_config: Either::Left(ChainConfig::from(chain_config)), + }, + Either::Right(c) => ResolvableChainConfig { + chain_config: Either::Right(Commitment::from_raw(*c.as_ref())), + }, + } + } +} + +impl From for ChainConfig { + fn from(chain_config: v0_1::ChainConfig) -> ChainConfig { + let v0_1::ChainConfig { + chain_id, + max_block_size, + base_fee, + fee_contract, + fee_recipient, + .. + } = chain_config; + + ChainConfig { + chain_id, + max_block_size, + base_fee, + fee_contract, + fee_recipient, + stake_table_contract: None, + } + } +} + +impl From for v0_1::ChainConfig { + fn from(chain_config: ChainConfig) -> v0_1::ChainConfig { + let ChainConfig { + chain_id, + max_block_size, + base_fee, + fee_contract, + fee_recipient, + .. + } = chain_config; + + v0_1::ChainConfig { + chain_id, + max_block_size, + base_fee, + fee_contract, + fee_recipient, + } + } +} + +impl Default for ChainConfig { + fn default() -> Self { + Self { + chain_id: U256::from(35353).into(), // arbitrarily chosen chain ID + max_block_size: 30720.into(), + base_fee: 0.into(), + fee_contract: None, + fee_recipient: Default::default(), + stake_table_contract: None, + } + } +} diff --git a/types/src/v0/v0_3/mod.rs b/types/src/v0/v0_3/mod.rs index 1a2e7efc9b..dac8e5270f 100644 --- a/types/src/v0/v0_3/mod.rs +++ b/types/src/v0/v0_3/mod.rs @@ -19,6 +19,8 @@ pub(crate) use super::v0_1::{ pub const VERSION: Version = Version { major: 0, minor: 3 }; +mod chain_config; mod stake_table; +pub use chain_config::*; pub use stake_table::CombinedStakeTable;