diff --git a/coordinator/cosign/src/evaluator.rs b/coordinator/cosign/src/evaluator.rs index 152830f0..06a3019f 100644 --- a/coordinator/cosign/src/evaluator.rs +++ b/coordinator/cosign/src/evaluator.rs @@ -1,6 +1,6 @@ use core::future::Future; -use serai_client::{primitives::Amount, Serai}; +use serai_client::Serai; use serai_db::*; use serai_task::ContinuallyRan; @@ -15,12 +15,12 @@ create_db!( // The latest cosigned block number. LatestCosignedBlockNumber: () -> u64, // The latest global session evaluated. - // TODO: Also include the weights here LatestGlobalSessionEvaluated: () -> ([u8; 32], Vec), } ); /// A task to determine if a block has been cosigned and we should handle it. +// TODO: Remove `serai` from this pub(crate) struct CosignEvaluatorTask { pub(crate) db: D, pub(crate) serai: Serai, @@ -38,7 +38,7 @@ async fn get_latest_global_session_evaluated( // 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(); + let initial_global_session = GlobalSession::id(sets.clone()); LatestGlobalSessionEvaluated::set(txn, &(initial_global_session, sets.clone())); (initial_global_session, sets) } @@ -73,39 +73,26 @@ impl ContinuallyRan for CosignEvaluatorTask ContinuallyRan for CosignEvaluatorTask>(); + let global_session = GlobalSession::id(sets.clone()); LatestGlobalSessionEvaluated::set(&mut txn, &(global_session, sets)); } } @@ -149,28 +137,15 @@ impl ContinuallyRan for CosignEvaluatorTask = None; for set in sets { - let stake = self - .serai - .as_of(global_session_start_block) - .validator_sets() - .total_allocated_stake(set.network) - .await - .map_err(|e| format!("{e:?}"))? - .unwrap_or(Amount(0)) - .0; - // Increment total_weight with this set's stake - total_weight += stake; - // Check if this set cosigned this block or not let Some(cosign) = NetworksLatestCosignedBlock::get(&txn, global_session, set.network) @@ -178,7 +153,10 @@ impl ContinuallyRan for CosignEvaluatorTask= block_number { - weight_cosigned += total_weight + weight_cosigned += + global_session_info.stakes.get(&set.network).ok_or_else(|| { + "ValidatorSet in global session yet didn't have its stake".to_string() + })?; } // Update the lowest block common to all of these cosigns @@ -188,7 +166,7 @@ impl ContinuallyRan for CosignEvaluatorTask ContinuallyRan for CosignIntendTask { .await .map_err(|e| format!("{e:?}"))?; + // Check we are indexing a linear chain + if (block_number > 1) && + (<[u8; 32]>::from(block.header.parent_hash) != + SubstrateBlocks::get(&txn, block_number - 1) + .expect("indexing a block but haven't indexed its parent")) + { + Err(format!( + "node's block #{block_number} doesn't build upon the block #{} prior indexed", + block_number - 1 + ))?; + } + + SubstrateBlocks::set(&mut txn, block_number, &block.hash()); + match has_events { HasEvents::Notable | HasEvents::NonNotable => { + // TODO: Replace with LatestGlobalSessionIntended, GlobalSessions let sets = cosigning_sets_for_block(&self.serai, &block).await?; + // If this block doesn't have any cosigners, meaning it'll never be cosigned, we flag + // it as not having any events requiring cosigning so we don't attempt to sign/require + // a cosign for it + if sets.is_empty() { + has_events = HasEvents::No; + } + // 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?; - let global_session = GlobalSession::new(sets).id(); - GlobalSessions::set(&mut txn, global_session, &(block_number, block.hash())); + let serai = self.serai.as_of(block.hash()); + let sets = cosigning_sets(&serai).await?; + let global_session = GlobalSession::id(sets.iter().map(|(set, _key)| *set).collect()); + + let mut keys = HashMap::new(); + let mut stakes = HashMap::new(); + let mut total_stake = 0; + for (set, key) in &sets { + keys.insert(set.network, SeraiAddress::from(*key)); + let stake = serai + .validator_sets() + .total_allocated_stake(set.network) + .await + .map_err(|e| format!("{e:?}"))? + .unwrap_or(Amount(0)) + .0; + stakes.insert(set.network, stake); + total_stake += stake; + } + if total_stake == 0 { + Err(format!("cosigning sets for block #{block_number} had 0 stake in total"))?; + } + + GlobalSessions::set( + &mut txn, + global_session, + &(GlobalSession { start_block_number: block_number, keys, stakes, total_stake }), + ); if let Some(ending_global_session) = LatestGlobalSessionIntended::get(&txn) { - GlobalSessionLastBlock::set(&mut txn, ending_global_session, &block_number); + GlobalSessionsLastBlock::set(&mut txn, ending_global_session, &block_number); } LatestGlobalSessionIntended::set(&mut txn, &global_session); } - // If this block doesn't have any cosigners, meaning it'll never be cosigned, we flag it - // as not having any events requiring cosigning so we don't attempt to sign/require a - // cosign for it - if sets.is_empty() { - has_events = HasEvents::No; - } else { - let global_session = GlobalSession::new(sets.clone()).id(); + if has_events != HasEvents::No { + let global_session = GlobalSession::id(sets.clone()); // Tell each set of their expectation to cosign this block for set in sets { log::debug!("{:?} will be cosigning block #{block_number}", set); diff --git a/coordinator/cosign/src/lib.rs b/coordinator/cosign/src/lib.rs index d0abee08..baeee8ea 100644 --- a/coordinator/cosign/src/lib.rs +++ b/coordinator/cosign/src/lib.rs @@ -3,15 +3,16 @@ #![deny(missing_docs)] use core::{fmt::Debug, future::Future}; +use std::collections::HashMap; use blake2::{Digest, Blake2s256}; use borsh::{BorshSerialize, BorshDeserialize}; use serai_client::{ - primitives::{Amount, NetworkId, SeraiAddress}, + primitives::{NetworkId, SeraiAddress}, validator_sets::primitives::{Session, ValidatorSet, KeyPair}, - Block, Serai, TemporalSerai, + Public, Block, Serai, TemporalSerai, }; use serai_db::*; @@ -45,29 +46,36 @@ pub const COSIGN_CONTEXT: &[u8] = b"serai-cosign"; cosigning protocol. */ #[derive(Debug, BorshSerialize, BorshDeserialize)] -struct GlobalSession { - cosigners: Vec, +pub(crate) struct GlobalSession { + pub(crate) start_block_number: u64, + pub(crate) keys: HashMap, + pub(crate) stakes: HashMap, + pub(crate) total_stake: u64, } impl GlobalSession { - fn new(mut cosigners: Vec) -> Self { + fn id(mut cosigners: Vec) -> [u8; 32] { cosigners.sort_by_key(|a| borsh::to_vec(a).unwrap()); - Self { cosigners } - } - fn id(&self) -> [u8; 32] { - Blake2s256::digest(borsh::to_vec(self).unwrap()).into() + Blake2s256::digest(borsh::to_vec(&cosigners).unwrap()).into() } } 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]), + // The following are populated by the intend task and used throughout the library + + // An index of Substrate blocks + SubstrateBlocks: (block_number: u64) -> [u8; 32], + // A mapping from a global session's ID to its relevant information. + GlobalSessions: (global_session: [u8; 32]) -> GlobalSession, // The last block to be cosigned by a global session. - GlobalSessionLastBlock: (global_session: [u8; 32]) -> u64, + GlobalSessionsLastBlock: (global_session: [u8; 32]) -> u64, // The latest global session intended. // // This is distinct from the latest global session for which we've evaluated the cosigns for. LatestGlobalSessionIntended: () -> [u8; 32], + + // The following are managed by the `intake_cosign` function present in this file + // The latest cosigned block for each network. // // This will only be populated with cosigns predating or during the most recent global session @@ -189,21 +197,22 @@ async fn keys_for_network( Ok(None) } -/// Fetch the `ValidatorSet`s used for cosigning as of this block. -async fn cosigning_sets(serai: &TemporalSerai<'_>) -> Result, String> { +/// Fetch the `ValidatorSet`s, and their associated keys, used for cosigning as of this block. +async fn cosigning_sets(serai: &TemporalSerai<'_>) -> Result, String> { let mut sets = Vec::with_capacity(serai_client::primitives::NETWORKS.len()); for network in serai_client::primitives::NETWORKS { - let Some((session, _)) = keys_for_network(serai, network).await? else { + let Some((session, keys)) = keys_for_network(serai, network).await? else { // If this network doesn't have usable keys, move on continue; }; - sets.push(ValidatorSet { network, session }); + sets.push((ValidatorSet { network, session }, keys.0)); } Ok(sets) } -/// Fetch the `ValidatorSet`s used for cosigning a block by the block's parent hash. +/// Fetch the `ValidatorSet`s, and their associated keys, used for cosigning a block by the block's +/// parent hash. async fn cosigning_sets_by_parent_hash( serai: &Serai, parent_hash: [u8; 32], @@ -213,10 +222,11 @@ async fn cosigning_sets_by_parent_hash( 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 + let sets = cosigning_sets(&serai.as_of(parent_hash)).await?; + Ok(sets.into_iter().map(|(set, _key)| set).collect::>()) } -/// Fetch the `ValidatorSet`s used for cosigning this block. +/// Fetch the `ValidatorSet`s, and their associated keys, used for cosigning this block. async fn cosigning_sets_for_block( serai: &Serai, block: &Block, @@ -242,7 +252,6 @@ pub struct Faulted; /// The interface to manage cosigning with. pub struct Cosigning { db: D, - serai: Serai, } impl Cosigning { /// Spawn the tasks to intend and evaluate cosigns. @@ -262,10 +271,10 @@ impl Cosigning { .continually_run(intend_task, vec![evaluator_task_handle]), ); tokio::spawn( - (evaluator::CosignEvaluatorTask { db: db.clone(), serai: serai.clone(), request }) + (evaluator::CosignEvaluatorTask { db: db.clone(), serai, request }) .continually_run(evaluator_task, tasks_to_run_upon_cosigning), ); - Self { db, serai } + Self { db } } /// The latest cosigned block number. @@ -338,7 +347,7 @@ impl Cosigning { // // 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 { + pub fn intake_cosign(&mut self, signed_cosign: &SignedCosign) -> Result { let cosign = &signed_cosign.cosign; let Cosigner::ValidatorSet(network) = cosign.cosigner else { @@ -356,41 +365,37 @@ impl Cosigning { } } - // Check our finalized (and indexed by intend) blockchain exceeds this block number - if cosign.block_number >= intend::ScanCosignFrom::get(&self.db).unwrap_or(0) { + // Check our indexed blockchain includes a block with this block number + let Some(our_block_hash) = SubstrateBlocks::get(&self.db, cosign.block_number) else { return Ok(true); - } + }; - let Some((global_session_start_block_number, global_session_start_block_hash)) = - GlobalSessions::get(&self.db, cosign.global_session) - else { + let Some(global_session) = GlobalSessions::get(&self.db, cosign.global_session) else { // Unrecognized global session return Ok(true); }; - if cosign.block_number <= global_session_start_block_number { + if cosign.block_number <= global_session.start_block_number { // Cosign is for a block predating the global session return Ok(false); } - if Some(cosign.block_number) > GlobalSessionLastBlock::get(&self.db, cosign.global_session) { - // Cosign is for a block after the last block this global session should have signed - return Ok(false); + if let Some(last_block) = GlobalSessionsLastBlock::get(&self.db, cosign.global_session) { + if cosign.block_number > last_block { + // Cosign is for a block after the last block this global session should have signed + return Ok(false); + } } // Check the cosign's signature { - let key = match cosign.cosigner { + let key = Public::from(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 { + let Some(key) = global_session.keys.get(&network) else { return Ok(false); }; - - keys.0 + *key } - Cosigner::Validator(signer) => signer.into(), - }; + Cosigner::Validator(signer) => signer, + }); if !signed_cosign.verify_signature(key) { return Ok(false); @@ -402,20 +407,14 @@ impl Cosigning { let mut txn = self.db.txn(); - let our_block_hash = self - .serai - .block_hash(cosign.block_number) - .await - .map_err(|e| format!("{e:?}"))? - .ok_or_else(|| "requested hash of a finalized block yet received None".to_string())?; if our_block_hash == cosign.block_hash { // If this is for a future global session, we don't acknowledge this cosign at this time - if global_session_start_block_number > LatestCosignedBlockNumber::get(&txn).unwrap_or(0) { + if global_session.start_block_number > LatestCosignedBlockNumber::get(&txn).unwrap_or(0) { drop(txn); return Ok(true); } - NetworksLatestCosignedBlock::set(&mut txn, cosign.global_session, 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 @@ -424,31 +423,19 @@ impl Cosigning { Faults::set(&mut txn, cosign.global_session, &faults); let mut weight_cosigned = 0; - let mut total_weight = 0; - for set in cosigning_sets(&self.serai.as_of(global_session_start_block_hash)).await? { - let stake = self - .serai - .as_of(global_session_start_block_hash) - .validator_sets() - .total_allocated_stake(set.network) - .await - .map_err(|e| format!("{e:?}"))? - .unwrap_or(Amount(0)) - .0; - // Increment total_weight with this set's stake - total_weight += stake; - - // Check if this set cosigned this block or not - if faults - .iter() - .any(|cosign| cosign.cosign.cosigner == Cosigner::ValidatorSet(set.network)) - { - weight_cosigned += total_weight - } + for fault in &faults { + let Cosigner::ValidatorSet(network) = fault.cosign.cosigner else { + // TODO when we implement the non-ValidatorSet cosigner protocol + Err("non-ValidatorSet cosigner had a fault".to_string())? + }; + let Some(stake) = global_session.stakes.get(&network) else { + Err("cosigner with recognized key didn't have a stake entry saved".to_string())? + }; + weight_cosigned += stake; } // Check if the sum weight means a fault has occurred - if weight_cosigned >= ((total_weight * 17) / 100) { + if weight_cosigned >= ((global_session.total_stake * 17) / 100) { FaultedSession::set(&mut txn, &cosign.global_session); } }