diff --git a/coordinator/cosign/Cargo.toml b/coordinator/cosign/Cargo.toml index 4aab6348..bbd96399 100644 --- a/coordinator/cosign/Cargo.toml +++ b/coordinator/cosign/Cargo.toml @@ -14,6 +14,9 @@ rust-version = "1.81" all-features = true rustdoc-args = ["--cfg", "docsrs"] +[package.metadata.cargo-machete] +ignored = ["scale"] + [lints] workspace = true @@ -21,6 +24,7 @@ workspace = true blake2 = { version = "0.10", default-features = false, features = ["std"] } schnorrkel = { version = "0.11", default-features = false, features = ["std"] } +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["std", "derive"] } borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] } serai-client = { path = "../../substrate/client", default-features = false, features = ["serai", "borsh"] } diff --git a/coordinator/cosign/src/evaluator.rs b/coordinator/cosign/src/evaluator.rs index 5be7f924..f38b18b1 100644 --- a/coordinator/cosign/src/evaluator.rs +++ b/coordinator/cosign/src/evaluator.rs @@ -5,11 +5,18 @@ use serai_client::{primitives::Amount, Serai}; use serai_db::*; use serai_task::ContinuallyRan; -use crate::{*, intend::BlockHasEvents}; +use crate::{ + *, + intend::{BlockEventData, BlockEvents}, +}; create_db!( SubstrateCosignEvaluator { + // The latest cosigned block number. LatestCosignedBlockNumber: () -> u64, + // The latest global session evaluated. + // TODO: Also include the weights here + LatestGlobalSessionEvaluated: () -> ([u8; 32], Vec), } ); @@ -20,7 +27,23 @@ pub(crate) struct CosignEvaluatorTask { pub(crate) request: R, } -// TODO: Add a cache for the stake values +async fn get_latest_global_session_evaluated( + txn: &mut impl DbTxn, + serai: &Serai, + parent_hash: [u8; 32], +) -> Result<([u8; 32], Vec), String> { + Ok(match LatestGlobalSessionEvaluated::get(txn) { + Some(res) => res, + None => { + // This is the initial global session + // Fetch the sets participating and declare it the latest value recognized + let sets = cosigning_sets_by_parent_hash(serai, parent_hash).await?; + let initial_global_session = GlobalSession::new(sets.clone()).id(); + LatestGlobalSessionEvaluated::set(txn, &(initial_global_session, sets.clone())); + (initial_global_session, sets) + } + }) +} impl ContinuallyRan for CosignEvaluatorTask { fn run_iteration(&mut self) -> impl Send + Future> { @@ -31,23 +54,26 @@ impl ContinuallyRan for CosignEvaluatorTask { + let (global_session, sets) = + get_latest_global_session_evaluated(&mut txn, &self.serai, parent_hash).await?; + let mut weight_cosigned = 0; let mut total_weight = 0; - let (_block, sets) = cosigning_sets_for_block(&self.serai, block_number).await?; - let global_session = GlobalSession::new(sets.clone()).id(); let (_, global_session_start_block) = GlobalSessions::get(&txn, global_session).expect( "checking if intended cosign was satisfied within an unrecognized global session", ); @@ -68,9 +94,9 @@ impl ContinuallyRan for CosignEvaluatorTask ContinuallyRan for CosignEvaluatorTask ContinuallyRan for CosignEvaluatorTask ContinuallyRan for CosignEvaluatorTask= block_number { @@ -167,6 +202,11 @@ impl ContinuallyRan for CosignEvaluatorTask (u64, HasEvents), + BlockEvents: () -> BlockEventData, IntendedCosigns: (set: ValidatorSet) -> CosignIntent, } } async fn block_has_events_justifying_a_cosign( serai: &Serai, - block: u64, -) -> Result { - let serai = serai.as_of( - serai - .finalized_block_by_number(block) - .await? - .expect("couldn't get block which should've been finalized") - .hash(), - ); + block_number: u64, +) -> Result<(Block, HasEvents), SeraiError> { + let block = serai + .finalized_block_by_number(block_number) + .await? + .expect("couldn't get block which should've been finalized"); + let serai = serai.as_of(block.hash()); if !serai.validator_sets().key_gen_events().await?.is_empty() { - return Ok(HasEvents::Notable); + return Ok((block, HasEvents::Notable)); } if !serai.coins().burn_with_instruction_events().await?.is_empty() { - return Ok(HasEvents::NonNotable); + return Ok((block, HasEvents::NonNotable)); } - Ok(HasEvents::No) + Ok((block, HasEvents::No)) } /// A task to determine which blocks we should intend to cosign. @@ -59,23 +65,22 @@ impl ContinuallyRan for CosignIntendTask { for block_number in start_block_number ..= latest_block_number { let mut txn = self.db.txn(); - let mut has_events = block_has_events_justifying_a_cosign(&self.serai, block_number) - .await - .map_err(|e| format!("{e:?}"))?; + let (block, mut has_events) = + block_has_events_justifying_a_cosign(&self.serai, block_number) + .await + .map_err(|e| format!("{e:?}"))?; match has_events { HasEvents::Notable | HasEvents::NonNotable => { - let (block, sets) = cosigning_sets_for_block(&self.serai, block_number).await?; + let sets = cosigning_sets_for_block(&self.serai, &block).await?; // If this is notable, it creates a new global session, which we index into the // database now if has_events == HasEvents::Notable { let sets = cosigning_sets(&self.serai.as_of(block.hash())).await?; - GlobalSessions::set( - &mut txn, - GlobalSession::new(sets).id(), - &(block.number(), block.hash()), - ); + let global_session = GlobalSession::new(sets).id(); + GlobalSessions::set(&mut txn, global_session, &(block.number(), block.hash())); + LatestGlobalSessionIntended::set(&mut txn, &global_session); } // If this block doesn't have any cosigners, meaning it'll never be cosigned, we flag it @@ -104,7 +109,15 @@ impl ContinuallyRan for CosignIntendTask { HasEvents::No => {} } // Populate a singular feed with every block's status for the evluator to work off of - BlockHasEvents::send(&mut txn, &(block_number, has_events)); + BlockEvents::send( + &mut txn, + &(BlockEventData { + block_number, + parent_hash: block.header.parent_hash.into(), + block_hash: block.hash(), + has_events, + }), + ); // Mark this block as handled, meaning we should scan from the next block moving on ScanCosignFrom::set(&mut txn, &(block_number + 1)); txn.commit(); diff --git a/coordinator/cosign/src/lib.rs b/coordinator/cosign/src/lib.rs index f42ffb7c..7dc5a3a1 100644 --- a/coordinator/cosign/src/lib.rs +++ b/coordinator/cosign/src/lib.rs @@ -62,16 +62,20 @@ create_db! { Cosign { // A mapping from a global session's ID to its start block (number, hash). GlobalSessions: (global_session: [u8; 32]) -> (u64, [u8; 32]), - // An archive of all cosigns ever received. + // The latest global session intended. // - // This will only be populated with cosigns predating or during the most recent global session - // to have its start cosigned. - Cosigns: (block_number: u64) -> Vec, + // This is distinct from the latest global session for which we've evaluated the cosigns for. + LatestGlobalSessionIntended: () -> [u8; 32], // The latest cosigned block for each network. // // This will only be populated with cosigns predating or during the most recent global session // to have its start cosigned. - NetworksLatestCosignedBlock: (network: NetworkId) -> SignedCosign, + // + // The global session changes upon a notable block, causing each global session to have exactly + // one notable block. All validator sets will explicitly produce a cosign for their notable + // block, causing the latest cosigned block for a global session to either be the global + // session's notable cosigns or the network's latest cosigns. + NetworksLatestCosignedBlock: (global_session: [u8; 32], network: NetworkId) -> SignedCosign, // Cosigns received for blocks not locally recognized as finalized. Faults: (global_session: [u8; 32]) -> Vec, // The global session which faulted. @@ -146,25 +150,6 @@ impl SignedCosign { } } -/// Construct a `TemporalSerai` bound to the time used for cosigning this block. -async fn temporal_serai_used_for_cosigning( - serai: &Serai, - block_number: u64, -) -> Result<(Block, TemporalSerai<'_>), String> { - let block = serai - .finalized_block_by_number(block_number) - .await - .map_err(|e| format!("{e:?}"))? - .ok_or("block wasn't finalized".to_string())?; - - // If we're cosigning block `n`, it's cosigned by the sets as of block `n-1` - // (as block `n` may update the sets declared but that update shouldn't take effect here - // until it's cosigned) - let serai = serai.as_of(block.header.parent_hash.into()); - - Ok((block, serai)) -} - /// Fetch the keys used for cosigning by a specific network. async fn keys_for_network( serai: &TemporalSerai<'_>, @@ -216,13 +201,25 @@ async fn cosigning_sets(serai: &TemporalSerai<'_>) -> Result, Ok(sets) } +/// Fetch the `ValidatorSet`s used for cosigning a block by the block's parent hash. +async fn cosigning_sets_by_parent_hash( + serai: &Serai, + parent_hash: [u8; 32], +) -> Result, String> { + /* + If we're cosigning block `n`, it's cosigned by the sets as of block `n-1` (as block `n` may + update the sets declared but that update shouldn't take effect until block `n` is cosigned). + That's why fetching the cosigning sets for a block by its parent hash is valid. + */ + cosigning_sets(&serai.as_of(parent_hash)).await +} + /// Fetch the `ValidatorSet`s used for cosigning this block. async fn cosigning_sets_for_block( serai: &Serai, - block_number: u64, -) -> Result<(Block, Vec), String> { - let (block, serai) = temporal_serai_used_for_cosigning(serai, block_number).await?; - cosigning_sets(&serai).await.map(|sets| (block, sets)) + block: &Block, +) -> Result, String> { + cosigning_sets_by_parent_hash(serai, block.header.parent_hash.into()).await } /// An object usable to request notable cosigns for a block. @@ -279,8 +276,17 @@ impl Cosigning { } /// Fetch the notable cosigns for a global session in order to respond to requests. + /// + /// If this global session hasn't produced any notable cosigns, this will return the latest + /// cosigns for this session. pub fn notable_cosigns(&self, global_session: [u8; 32]) -> Vec { - todo!("TODO") + let mut cosigns = Vec::with_capacity(serai_client::primitives::NETWORKS.len()); + for network in serai_client::primitives::NETWORKS { + if let Some(cosign) = NetworksLatestCosignedBlock::get(&self.db, global_session, network) { + cosigns.push(cosign); + } + } + cosigns } /// The cosigns to rebroadcast ever so often. @@ -293,7 +299,7 @@ impl Cosigning { // Also include all of our recognized-as-honest cosigns in an attempt to induce fault // identification in those who see the faulty cosigns as honest for network in serai_client::primitives::NETWORKS { - if let Some(cosign) = NetworksLatestCosignedBlock::get(&self.db, network) { + if let Some(cosign) = NetworksLatestCosignedBlock::get(&self.db, faulted, network) { if cosign.cosign.global_session == faulted { cosigns.push(cosign); } @@ -301,9 +307,14 @@ impl Cosigning { } cosigns } else { + let Some(latest_global_session) = LatestGlobalSessionIntended::get(&self.db) else { + return vec![]; + }; let mut cosigns = Vec::with_capacity(serai_client::primitives::NETWORKS.len()); for network in serai_client::primitives::NETWORKS { - if let Some(cosign) = NetworksLatestCosignedBlock::get(&self.db, network) { + if let Some(cosign) = + NetworksLatestCosignedBlock::get(&self.db, latest_global_session, network) + { cosigns.push(cosign); } } @@ -324,15 +335,24 @@ impl Cosigning { // more relevant, cosign) again. // // Takes `&mut self` as this should only be called once at any given moment. + // TODO: Don't overload bool here pub async fn intake_cosign(&mut self, signed_cosign: SignedCosign) -> Result { let cosign = &signed_cosign.cosign; - // Check if we've prior handled this cosign + let Cosigner::ValidatorSet(network) = cosign.cosigner else { + // TODO + // Individually signed cosign despite that protocol not being implemented + return Ok(false); + }; + + // Check if we should even bother handling this cosign let mut txn = self.db.txn(); - let mut cosigns_for_this_block_position = - Cosigns::get(&txn, cosign.block_number).unwrap_or(vec![]); - if cosigns_for_this_block_position.iter().any(|existing| existing.cosign == *cosign) { - return Ok(true); + // TODO: A malicious validator set can sign a block after their notable block to erase a + // notable cosign + if let Some(existing) = NetworksLatestCosignedBlock::get(&txn, cosign.global_session, network) { + if existing.cosign.block_number >= cosign.block_number { + return Ok(true); + } } // Check we can verify this cosign's signature @@ -342,24 +362,31 @@ impl Cosigning { // Unrecognized global session return Ok(true); }; + if cosign.block_number <= global_session_start_block_number { + // Cosign is for a block predating the global session + return Ok(false); + } // Check the cosign's signature - let network = match cosign.cosigner { - Cosigner::ValidatorSet(network) => { - let Some((_session, keys)) = - keys_for_network(&self.serai.as_of(global_session_start_block_hash), network).await? - else { - return Ok(false); - }; + { + let key = match cosign.cosigner { + Cosigner::ValidatorSet(network) => { + // TODO: Cache this + let Some((_session, keys)) = + keys_for_network(&self.serai.as_of(global_session_start_block_hash), network).await? + else { + return Ok(false); + }; - if !signed_cosign.verify_signature(keys.0) { - return Ok(false); + keys.0 } + Cosigner::Validator(signer) => signer.into(), + }; - network + if !signed_cosign.verify_signature(key) { + return Ok(false); } - Cosigner::Validator(_) => return Ok(false), - }; + } // Check our finalized blockchain exceeds this block number if self.serai.latest_finalized_block().await.map_err(|e| format!("{e:?}"))?.number() < @@ -372,10 +399,6 @@ impl Cosigning { // Since we verified this cosign's signature, and have a chain sufficiently long, handle the // cosign - // Save the cosign to the database - cosigns_for_this_block_position.push(signed_cosign.clone()); - Cosigns::set(&mut txn, cosign.block_number, &cosigns_for_this_block_position); - let our_block_hash = self .serai .block_hash(cosign.block_number) @@ -389,13 +412,7 @@ impl Cosigning { return Ok(true); } - if NetworksLatestCosignedBlock::get(&txn, network) - .map(|cosign| cosign.cosign.block_number) - .unwrap_or(0) < - cosign.block_number - { - NetworksLatestCosignedBlock::set(&mut txn, network, &signed_cosign); - } + NetworksLatestCosignedBlock::set(&mut txn, cosign.global_session, network, &signed_cosign); } else { let mut faults = Faults::get(&txn, cosign.global_session).unwrap_or(vec![]); // Only handle this as a fault if this set wasn't prior faulty