diff --git a/coordinator/cosign/README.md b/coordinator/cosign/README.md index 10f31378..50ce52a6 100644 --- a/coordinator/cosign/README.md +++ b/coordinator/cosign/README.md @@ -66,7 +66,7 @@ to exhibit the same behavior), yet prevents interaction with it. If cosigns representing 17% of the non-Serai validators sets by weight are detected for distinct blocks at the same position, the protocol halts. An -explicit latency period of five seconds is enacted after receiving a cosign +explicit latency period of seventy seconds is enacted after receiving a cosign commit for the detection of such an equivocation. This is largely redundant given how the Serai blockchain node will presumably have halted itself by this time. @@ -114,8 +114,8 @@ asynchronous network or 11.33% of non-Serai validator sets' stake. ### TODO The Serai node no longer responding to RPC requests upon detecting any -equivocation, the delayed acknowledgement of cosigns, and the fallback protocol -where validators individually produce signatures, are not implemented at this -time. The former means the detection of equivocating cosigns not redundant and -the latter makes 5.67% of non-Serai validator sets' stake the DoS threshold, -even without control of an asynchronous network. +equivocation, and the fallback protocol where validators individually produce +signatures, are not implemented at this time. The former means the detection of +equivocating cosigns is not redundant and the latter makes 5.67% of non-Serai +validator sets' stake the DoS threshold, even without control of an +asynchronous network. diff --git a/coordinator/cosign/src/delay.rs b/coordinator/cosign/src/delay.rs new file mode 100644 index 00000000..5593eaf7 --- /dev/null +++ b/coordinator/cosign/src/delay.rs @@ -0,0 +1,55 @@ +use core::future::Future; +use std::time::{Duration, SystemTime}; + +use serai_db::*; +use serai_task::ContinuallyRan; + +use crate::evaluator::CosignedBlocks; + +/// How often callers should broadcast the cosigns flagged for rebroadcasting. +pub const BROADCAST_FREQUENCY: Duration = Duration::from_secs(60); +const SYNCHRONY_EXPECTATION: Duration = Duration::from_secs(10); +const ACKNOWLEDGEMENT_DELAY: Duration = + Duration::from_secs(BROADCAST_FREQUENCY.as_secs() + SYNCHRONY_EXPECTATION.as_secs()); + +create_db!( + SubstrateCosignDelay { + // The latest cosigned block number. + LatestCosignedBlockNumber: () -> u64, + } +); + +/// A task to delay acknowledgement of cosigns. +pub(crate) struct CosignDelayTask { + pub(crate) db: D, +} + +impl ContinuallyRan for CosignDelayTask { + fn run_iteration(&mut self) -> impl Send + Future> { + async move { + let mut made_progress = false; + loop { + let mut txn = self.db.txn(); + + // Receive the next block to mark as cosigned + let Some((block_number, time_evaluated)) = CosignedBlocks::try_recv(&mut txn) else { + break; + }; + // Calculate when we should mark it as valid + let time_valid = + SystemTime::UNIX_EPOCH + Duration::from_secs(time_evaluated) + ACKNOWLEDGEMENT_DELAY; + // Sleep until then + tokio::time::sleep(SystemTime::now().duration_since(time_valid).unwrap_or(Duration::ZERO)) + .await; + + // Set the cosigned block + LatestCosignedBlockNumber::set(&mut txn, &block_number); + txn.commit(); + + made_progress = true; + } + + Ok(made_progress) + } + } +} diff --git a/coordinator/cosign/src/evaluator.rs b/coordinator/cosign/src/evaluator.rs index 91f92b44..856a6e00 100644 --- a/coordinator/cosign/src/evaluator.rs +++ b/coordinator/cosign/src/evaluator.rs @@ -1,4 +1,5 @@ use core::future::Future; +use std::time::{Duration, SystemTime}; use serai_db::*; use serai_task::ContinuallyRan; @@ -10,13 +11,18 @@ use crate::{ create_db!( SubstrateCosignEvaluator { - // The latest cosigned block number. - LatestCosignedBlockNumber: () -> u64, // The global session currently being evaluated. CurrentlyEvaluatedGlobalSession: () -> ([u8; 32], GlobalSession), } ); +db_channel!( + SubstrateCosignEvaluatorChannels { + // (cosigned block, time cosign was evaluated) + CosignedBlocks: () -> (u64, u64), + } +); + // This is a strict function which won't panic, even with a malicious Serai node, so long as: // - It's called incrementally // - It's only called for block numbers we've completed indexing on within the intend task @@ -72,8 +78,6 @@ pub(crate) struct CosignEvaluatorTask { impl ContinuallyRan for CosignEvaluatorTask { fn run_iteration(&mut self) -> impl Send + Future> { async move { - let latest_cosigned_block_number = LatestCosignedBlockNumber::get(&self.db).unwrap_or(0); - let mut known_cosign = None; let mut made_progress = false; loop { @@ -82,11 +86,6 @@ impl ContinuallyRan for CosignEvaluatorTask ContinuallyRan for CosignEvaluatorTask {} } - // Since we checked we had the necessary cosigns, increment the latest cosigned block - LatestCosignedBlockNumber::set(&mut txn, &block_number); + // Since we checked we had the necessary cosigns, send it for delay before acknowledgement + CosignedBlocks::send( + &mut txn, + &( + block_number, + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or(Duration::ZERO) + .as_secs(), + ), + ); txn.commit(); made_progress = true; diff --git a/coordinator/cosign/src/lib.rs b/coordinator/cosign/src/lib.rs index 3f771983..dd8eb833 100644 --- a/coordinator/cosign/src/lib.rs +++ b/coordinator/cosign/src/lib.rs @@ -22,7 +22,10 @@ use serai_task::*; mod intend; /// The evaluator of the cosigns. mod evaluator; -use evaluator::LatestCosignedBlockNumber; +/// The task to delay acknowledgement of the cosigns. +mod delay; +pub use delay::BROADCAST_FREQUENCY; +use delay::LatestCosignedBlockNumber; /// The schnorrkel context to used when signing a cosign. pub const COSIGN_CONTEXT: &[u8] = b"serai-cosign"; @@ -235,13 +238,18 @@ impl Cosigning { ) -> Self { let (intend_task, _intend_task_handle) = Task::new(); let (evaluator_task, evaluator_task_handle) = Task::new(); + let (delay_task, delay_task_handle) = Task::new(); tokio::spawn( (intend::CosignIntendTask { db: db.clone(), serai }) .continually_run(intend_task, vec![evaluator_task_handle]), ); tokio::spawn( (evaluator::CosignEvaluatorTask { db: db.clone(), request }) - .continually_run(evaluator_task, tasks_to_run_upon_cosigning), + .continually_run(evaluator_task, vec![delay_task_handle]), + ); + tokio::spawn( + (delay::CosignDelayTask { db: db.clone() }) + .continually_run(delay_task, tasks_to_run_upon_cosigning), ); Self { db } } @@ -269,7 +277,7 @@ impl Cosigning { cosigns } - /// The cosigns to rebroadcast ever so often. + /// The cosigns to rebroadcast every `BROADCAST_FREQUENCY` seconds. /// /// This will be the most recent cosigns, in case the initial broadcast failed, or the faulty /// cosigns, in case of a fault, to induce identification of the fault by others. @@ -348,6 +356,8 @@ impl Cosigning { return Ok(false); } if !faulty { + // This prevents a malicious validator set, on the same chain, from producing a cosign after + // their final block, replacing their notable cosign 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