3 Commits

Author SHA1 Message Date
Luke Parker
5b337c3ce8 Prevent a malicious validator set from overwriting a notable cosign
Also prevents panics from an invalid Serai node (removing the assumption of an
honest Serai node).
2024-12-25 02:11:05 -05:00
Luke Parker
e119fb4c16 Replace Cosigns by extending NetworksLatestCosignedBlock
Cosigns was an archive of every single cosign ever received. By scoping
NetworksLatestCosignedBlock to be by the global session, we have the latest
cosign for each network in a session (valid to replace all prior cosigns by
that network within that session, even for the purposes of fault) and
automatically have the notable cosigns indexed (as they are the latest ones
within their session). This not only saves space yet also allows optimizing
evaluation a bit.
2024-12-25 01:45:37 -05:00
Luke Parker
ef972b2658 Add cosign signature verification 2024-12-25 00:06:46 -05:00
5 changed files with 249 additions and 145 deletions

View File

@@ -14,18 +14,18 @@ rust-version = "1.81"
all-features = true all-features = true
rustdoc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs"]
[package.metadata.cargo-machete]
ignored = ["scale"]
[lints] [lints]
workspace = true workspace = true
[dependencies] [dependencies]
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"] } 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"] } borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] }
blake2 = { version = "0.10", default-features = false, features = ["std"] }
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["std"] }
schnorr = { package = "schnorr-signatures", path = "../../crypto/schnorr", default-features = false, features = ["std"] }
sp-application-crypto = { git = "https://github.com/serai-dex/substrate", default-features = false, features = ["std"] }
serai-client = { path = "../../substrate/client", default-features = false, features = ["serai", "borsh"] } serai-client = { path = "../../substrate/client", default-features = false, features = ["serai", "borsh"] }
log = { version = "0.4", default-features = false, features = ["std"] } log = { version = "0.4", default-features = false, features = ["std"] }

View File

@@ -114,8 +114,8 @@ asynchronous network or 11.33% of non-Serai validator sets' stake.
### TODO ### TODO
The Serai node no longer responding to RPC requests upon detecting any The Serai node no longer responding to RPC requests upon detecting any
equivocation, and the fallback protocol where validators individually produce equivocation, the delayed acknowledgement of cosigns, and the fallback protocol
signatures, are not implemented at this time. The former means the detection of where validators individually produce signatures, are not implemented at this
equivocating cosigns not redundant and the latter makes 5.67% of non-Serai time. The former means the detection of equivocating cosigns not redundant and
validator sets' stake the DoS threshold, even without control of an the latter makes 5.67% of non-Serai validator sets' stake the DoS threshold,
asynchronous network. even without control of an asynchronous network.

View File

@@ -5,11 +5,18 @@ use serai_client::{primitives::Amount, Serai};
use serai_db::*; use serai_db::*;
use serai_task::ContinuallyRan; use serai_task::ContinuallyRan;
use crate::{*, intend::BlockHasEvents}; use crate::{
*,
intend::{BlockEventData, BlockEvents},
};
create_db!( create_db!(
SubstrateCosignEvaluator { SubstrateCosignEvaluator {
// The latest cosigned block number.
LatestCosignedBlockNumber: () -> u64, LatestCosignedBlockNumber: () -> u64,
// The latest global session evaluated.
// TODO: Also include the weights here
LatestGlobalSessionEvaluated: () -> ([u8; 32], Vec<ValidatorSet>),
} }
); );
@@ -20,7 +27,23 @@ pub(crate) struct CosignEvaluatorTask<D: Db, R: RequestNotableCosigns> {
pub(crate) request: R, 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<ValidatorSet>), 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<D: Db, R: RequestNotableCosigns> ContinuallyRan for CosignEvaluatorTask<D, R> { impl<D: Db, R: RequestNotableCosigns> ContinuallyRan for CosignEvaluatorTask<D, R> {
fn run_iteration(&mut self) -> impl Send + Future<Output = Result<bool, String>> { fn run_iteration(&mut self) -> impl Send + Future<Output = Result<bool, String>> {
@@ -31,26 +54,31 @@ impl<D: Db, R: RequestNotableCosigns> ContinuallyRan for CosignEvaluatorTask<D,
let mut made_progress = false; let mut made_progress = false;
loop { loop {
let mut txn = self.db.txn(); let mut txn = self.db.txn();
let Some((block_number, has_events)) = BlockHasEvents::try_recv(&mut txn) else { break }; let Some(BlockEventData { block_number, parent_hash, block_hash, has_events }) =
BlockEvents::try_recv(&mut txn)
else {
break;
};
// Make sure these two feeds haven't desynchronized somehow // Make sure these two feeds haven't desynchronized somehow
// We could remove our `LatestCosignedBlockNumber`, making the latest cosigned block number // We could remove our `LatestCosignedBlockNumber`, making the latest cosigned block number
// the next message in the channel's block number minus one, but that'd only work when the // the next message in the channel's block number minus one, but that'd only work when the
// channel isn't empty // channel isn't empty
assert_eq!(block_number, latest_cosigned_block_number + 1); assert_eq!(block_number, latest_cosigned_block_number + 1);
let cosigns_for_block = Cosigns::get(&txn, block_number).unwrap_or(vec![]);
match has_events { match has_events {
// Because this had notable events, we require an explicit cosign for this block by a // Because this had notable events, we require an explicit cosign for this block by a
// supermajority of the prior block's validator sets // supermajority of the prior block's validator sets
HasEvents::Notable => { HasEvents::Notable => {
let (global_session, sets) =
get_latest_global_session_evaluated(&mut txn, &self.serai, parent_hash).await?;
let mut weight_cosigned = 0; let mut weight_cosigned = 0;
let mut total_weight = 0; let mut total_weight = 0;
let (_block, sets) = cosigning_sets_for_block(&self.serai, block_number).await?; let (_, global_session_start_block) = GlobalSessions::get(&txn, global_session)
let global_session = GlobalSession::new(sets.clone()).id(); .ok_or_else(|| {
let (_, global_session_start_block) = GlobalSessions::get(&txn, global_session).expect( "checking if intended cosign was satisfied within an unrecognized global session"
"checking if intended cosign was satisfied within an unrecognized global session", .to_string()
); })?;
for set in sets { for set in sets {
// Fetch the weight for this set, as of the start of the global session // Fetch the weight for this set, as of the start of the global session
// This simplifies the logic around which set of stakes to use when evaluating // This simplifies the logic around which set of stakes to use when evaluating
@@ -68,9 +96,9 @@ impl<D: Db, R: RequestNotableCosigns> ContinuallyRan for CosignEvaluatorTask<D,
total_weight += stake; total_weight += stake;
// Check if we have the cosign from this set // Check if we have the cosign from this set
if cosigns_for_block if NetworksLatestCosignedBlock::get(&txn, global_session, set.network)
.iter() .map(|signed_cosign| signed_cosign.cosign.block_number) ==
.any(|cosign| cosign.cosigner == Cosigner::ValidatorSet(set.network)) Some(block_number)
{ {
// Since have this cosign, add the set's weight to the weight which has cosigned // Since have this cosign, add the set's weight to the weight which has cosigned
weight_cosigned += stake; weight_cosigned += stake;
@@ -90,6 +118,13 @@ impl<D: Db, R: RequestNotableCosigns> ContinuallyRan for CosignEvaluatorTask<D,
"notable block (#{block_number}) wasn't yet cosigned. this should resolve shortly", "notable block (#{block_number}) wasn't yet cosigned. this should resolve shortly",
)); ));
} }
// Since this block changes the global session, update it
{
let sets = cosigning_sets(&self.serai.as_of(block_hash)).await?;
let global_session = GlobalSession::new(sets.clone()).id();
LatestGlobalSessionEvaluated::set(&mut txn, &(global_session, sets));
}
} }
// Since this block didn't have any notable events, we simply require a cosign for this // Since this block didn't have any notable events, we simply require a cosign for this
// block or a greater block by the current validator sets // block or a greater block by the current validator sets
@@ -112,12 +147,13 @@ impl<D: Db, R: RequestNotableCosigns> ContinuallyRan for CosignEvaluatorTask<D,
*/ */
// Get the global session for this block // Get the global session for this block
let (_block, sets) = cosigning_sets_for_block(&self.serai, block_number).await?; let (global_session, sets) =
let global_session = GlobalSession::new(sets.clone()).id(); get_latest_global_session_evaluated(&mut txn, &self.serai, parent_hash).await?;
let (_, global_session_start_block) = GlobalSessions::get(&txn, global_session) let (_, global_session_start_block) = GlobalSessions::get(&txn, global_session)
.expect( .ok_or_else(|| {
"checking if intended cosign was satisfied within an unrecognized global session", "checking if intended cosign was satisfied within an unrecognized global session"
); .to_string()
})?;
let mut weight_cosigned = 0; let mut weight_cosigned = 0;
let mut total_weight = 0; let mut total_weight = 0;
@@ -136,17 +172,19 @@ impl<D: Db, R: RequestNotableCosigns> ContinuallyRan for CosignEvaluatorTask<D,
total_weight += stake; total_weight += stake;
// Check if this set cosigned this block or not // Check if this set cosigned this block or not
let Some(cosign) = NetworksLatestCosignedBlock::get(&txn, set.network) else { let Some(cosign) =
NetworksLatestCosignedBlock::get(&txn, global_session, set.network)
else {
continue; continue;
}; };
if cosign.block_number >= block_number { if cosign.cosign.block_number >= block_number {
weight_cosigned += total_weight weight_cosigned += total_weight
} }
// Update the lowest block common to all of these cosigns // Update the lowest block common to all of these cosigns
lowest_common_block = lowest_common_block lowest_common_block = lowest_common_block
.map(|existing| existing.min(cosign.block_number)) .map(|existing| existing.min(cosign.cosign.block_number))
.or(Some(cosign.block_number)); .or(Some(cosign.cosign.block_number));
} }
// Check if the sum weight doesn't cross the required threshold // Check if the sum weight doesn't cross the required threshold
@@ -167,6 +205,11 @@ impl<D: Db, R: RequestNotableCosigns> ContinuallyRan for CosignEvaluatorTask<D,
} }
// Update the cached result for the block we know is cosigned // Update the cached result for the block we know is cosigned
/*
There may be a higher block which was cosigned, but once we get to this block,
we'll re-evaluate and find it then. The alternative would be an optimistic
re-evaluation now. Both are fine, so the lower-complexity option is preferred.
*/
known_cosign = lowest_common_block; known_cosign = lowest_common_block;
} }
} }

View File

@@ -1,6 +1,6 @@
use core::future::Future; use core::future::Future;
use serai_client::{SeraiError, Serai, validator_sets::primitives::ValidatorSet}; use serai_client::{Serai, validator_sets::primitives::ValidatorSet};
use serai_db::*; use serai_db::*;
use serai_task::ContinuallyRan; use serai_task::ContinuallyRan;
@@ -13,34 +13,41 @@ create_db!(
} }
); );
#[derive(Debug, BorshSerialize, BorshDeserialize)]
pub(crate) struct BlockEventData {
pub(crate) block_number: u64,
pub(crate) parent_hash: [u8; 32],
pub(crate) block_hash: [u8; 32],
pub(crate) has_events: HasEvents,
}
db_channel! { db_channel! {
CosignIntendChannels { CosignIntendChannels {
BlockHasEvents: () -> (u64, HasEvents), BlockEvents: () -> BlockEventData,
IntendedCosigns: (set: ValidatorSet) -> CosignIntent, IntendedCosigns: (set: ValidatorSet) -> CosignIntent,
} }
} }
async fn block_has_events_justifying_a_cosign( async fn block_has_events_justifying_a_cosign(
serai: &Serai, serai: &Serai,
block: u64, block_number: u64,
) -> Result<HasEvents, SeraiError> { ) -> Result<(Block, HasEvents), String> {
let serai = serai.as_of( let block = serai
serai .finalized_block_by_number(block_number)
.finalized_block_by_number(block) .await
.await? .map_err(|e| format!("{e:?}"))?
.expect("couldn't get block which should've been finalized") .ok_or_else(|| "couldn't get block which should've been finalized".to_string())?;
.hash(), let serai = serai.as_of(block.hash());
);
if !serai.validator_sets().key_gen_events().await?.is_empty() { if !serai.validator_sets().key_gen_events().await.map_err(|e| format!("{e:?}"))?.is_empty() {
return Ok(HasEvents::Notable); return Ok((block, HasEvents::Notable));
} }
if !serai.coins().burn_with_instruction_events().await?.is_empty() { if !serai.coins().burn_with_instruction_events().await.map_err(|e| format!("{e:?}"))?.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. /// A task to determine which blocks we should intend to cosign.
@@ -59,23 +66,25 @@ impl<D: Db> ContinuallyRan for CosignIntendTask<D> {
for block_number in start_block_number ..= latest_block_number { for block_number in start_block_number ..= latest_block_number {
let mut txn = self.db.txn(); let mut txn = self.db.txn();
let mut has_events = block_has_events_justifying_a_cosign(&self.serai, block_number) let (block, mut has_events) =
.await block_has_events_justifying_a_cosign(&self.serai, block_number)
.map_err(|e| format!("{e:?}"))?; .await
.map_err(|e| format!("{e:?}"))?;
match has_events { match has_events {
HasEvents::Notable | HasEvents::NonNotable => { 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 // If this is notable, it creates a new global session, which we index into the
// database now // database now
if has_events == HasEvents::Notable { if has_events == HasEvents::Notable {
let sets = cosigning_sets(&self.serai.as_of(block.hash())).await?; let sets = cosigning_sets(&self.serai.as_of(block.hash())).await?;
GlobalSessions::set( let global_session = GlobalSession::new(sets).id();
&mut txn, GlobalSessions::set(&mut txn, global_session, &(block_number, block.hash()));
GlobalSession::new(sets).id(), if let Some(ending_global_session) = LatestGlobalSessionIntended::get(&txn) {
&(block.number(), block.hash()), GlobalSessionLastBlock::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 // If this block doesn't have any cosigners, meaning it'll never be cosigned, we flag it
@@ -104,7 +113,15 @@ impl<D: Db> ContinuallyRan for CosignIntendTask<D> {
HasEvents::No => {} HasEvents::No => {}
} }
// Populate a singular feed with every block's status for the evluator to work off of // 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 // Mark this block as handled, meaning we should scan from the next block moving on
ScanCosignFrom::set(&mut txn, &(block_number + 1)); ScanCosignFrom::set(&mut txn, &(block_number + 1));
txn.commit(); txn.commit();

View File

@@ -23,6 +23,9 @@ mod intend;
mod evaluator; mod evaluator;
use evaluator::LatestCosignedBlockNumber; use evaluator::LatestCosignedBlockNumber;
/// The schnorrkel context to used when signing a cosign.
pub const COSIGN_CONTEXT: &[u8] = b"serai-cosign";
/// A 'global session', defined as all validator sets used for cosigning at a given moment. /// A 'global session', defined as all validator sets used for cosigning at a given moment.
/// ///
/// We evaluate cosign faults within a global session. This ensures even if cosigners cosign /// We evaluate cosign faults within a global session. This ensures even if cosigners cosign
@@ -59,18 +62,24 @@ create_db! {
Cosign { Cosign {
// A mapping from a global session's ID to its start block (number, hash). // A mapping from a global session's ID to its start block (number, hash).
GlobalSessions: (global_session: [u8; 32]) -> (u64, [u8; 32]), GlobalSessions: (global_session: [u8; 32]) -> (u64, [u8; 32]),
// An archive of all cosigns ever received. // The last block to be cosigned by a global session.
GlobalSessionLastBlock: (global_session: [u8; 32]) -> u64,
// The latest global session intended.
// //
// This will only be populated with cosigns predating or during the most recent global session // This is distinct from the latest global session for which we've evaluated the cosigns for.
// to have its start cosigned. LatestGlobalSessionIntended: () -> [u8; 32],
Cosigns: (block_number: u64) -> Vec<Cosign>,
// The latest cosigned block for each network. // The latest cosigned block for each network.
// //
// This will only be populated with cosigns predating or during the most recent global session // This will only be populated with cosigns predating or during the most recent global session
// to have its start cosigned. // to have its start cosigned.
NetworksLatestCosignedBlock: (network: NetworkId) -> Cosign, //
// 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. // Cosigns received for blocks not locally recognized as finalized.
Faults: (global_session: [u8; 32]) -> Vec<Cosign>, Faults: (global_session: [u8; 32]) -> Vec<SignedCosign>,
// The global session which faulted. // The global session which faulted.
FaultedSession: () -> [u8; 32], FaultedSession: () -> [u8; 32],
} }
@@ -105,7 +114,7 @@ struct CosignIntent {
/// The identification of a cosigner. /// The identification of a cosigner.
#[derive(Clone, Copy, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] #[derive(Clone, Copy, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
enum Cosigner { pub enum Cosigner {
/// The network which produced this cosign. /// The network which produced this cosign.
ValidatorSet(NetworkId), ValidatorSet(NetworkId),
/// The individual validator which produced this cosign. /// The individual validator which produced this cosign.
@@ -113,35 +122,34 @@ enum Cosigner {
} }
/// A cosign. /// A cosign.
#[derive(Clone, Copy, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] #[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
struct Cosign { pub struct Cosign {
/// The global session this cosign is being performed under. /// The global session this cosign is being performed under.
global_session: [u8; 32], pub global_session: [u8; 32],
/// The number of the block to cosign. /// The number of the block to cosign.
block_number: u64, pub block_number: u64,
/// The hash of the block to cosign. /// The hash of the block to cosign.
block_hash: [u8; 32], pub block_hash: [u8; 32],
/// The actual cosigner. /// The actual cosigner.
cosigner: Cosigner, pub cosigner: Cosigner,
} }
/// Construct a `TemporalSerai` bound to the time used for cosigning this block. /// A signed cosign.
async fn temporal_serai_used_for_cosigning( #[derive(Clone, Debug, BorshSerialize, BorshDeserialize)]
serai: &Serai, pub struct SignedCosign {
block_number: u64, /// The cosign.
) -> Result<(Block, TemporalSerai<'_>), String> { pub cosign: Cosign,
let block = serai /// The signature for the cosign.
.finalized_block_by_number(block_number) pub signature: [u8; 64],
.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` impl SignedCosign {
// (as block `n` may update the sets declared but that update shouldn't take effect here fn verify_signature(&self, signer: serai_client::Public) -> bool {
// until it's cosigned) let Ok(signer) = schnorrkel::PublicKey::from_bytes(&signer.0) else { return false };
let serai = serai.as_of(block.header.parent_hash.into()); let Ok(signature) = schnorrkel::Signature::from_bytes(&self.signature) else { return false };
Ok((block, serai)) signer.verify_simple(COSIGN_CONTEXT, &borsh::to_vec(&self.cosign).unwrap(), &signature).is_ok()
}
} }
/// Fetch the keys used for cosigning by a specific network. /// Fetch the keys used for cosigning by a specific network.
@@ -195,13 +203,25 @@ async fn cosigning_sets(serai: &TemporalSerai<'_>) -> Result<Vec<ValidatorSet>,
Ok(sets) 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<Vec<ValidatorSet>, 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. /// Fetch the `ValidatorSet`s used for cosigning this block.
async fn cosigning_sets_for_block( async fn cosigning_sets_for_block(
serai: &Serai, serai: &Serai,
block_number: u64, block: &Block,
) -> Result<(Block, Vec<ValidatorSet>), String> { ) -> Result<Vec<ValidatorSet>, String> {
let (block, serai) = temporal_serai_used_for_cosigning(serai, block_number).await?; cosigning_sets_by_parent_hash(serai, block.header.parent_hash.into()).await
cosigning_sets(&serai).await.map(|sets| (block, sets))
} }
/// An object usable to request notable cosigns for a block. /// An object usable to request notable cosigns for a block.
@@ -258,31 +278,45 @@ impl<D: Db> Cosigning<D> {
} }
/// Fetch the notable cosigns for a global session in order to respond to requests. /// Fetch the notable cosigns for a global session in order to respond to requests.
pub fn notable_cosigns(&self, global_session: [u8; 32]) -> Vec<Cosign> { ///
todo!("TODO") /// 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<SignedCosign> {
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. /// The cosigns to rebroadcast ever so often.
/// ///
/// This will be the most recent cosigns, in case the initial broadcast failed, or the faulty /// 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. /// cosigns, in case of a fault, to induce identification of the fault by others.
pub fn cosigns_to_rebroadcast(&self) -> Vec<Cosign> { pub fn cosigns_to_rebroadcast(&self) -> Vec<SignedCosign> {
if let Some(faulted) = FaultedSession::get(&self.db) { if let Some(faulted) = FaultedSession::get(&self.db) {
let mut cosigns = Faults::get(&self.db, faulted).unwrap(); let mut cosigns = Faults::get(&self.db, faulted).expect("faulted with no faults");
// Also include all of our recognized-as-honest cosigns in an attempt to induce fault // 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 // identification in those who see the faulty cosigns as honest
for network in serai_client::primitives::NETWORKS { 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.global_session == faulted { if cosign.cosign.global_session == faulted {
cosigns.push(cosign); cosigns.push(cosign);
} }
} }
} }
cosigns cosigns
} else { } else {
let Some(latest_global_session) = LatestGlobalSessionIntended::get(&self.db) else {
return vec![];
};
let mut cosigns = Vec::with_capacity(serai_client::primitives::NETWORKS.len()); let mut cosigns = Vec::with_capacity(serai_client::primitives::NETWORKS.len());
for network in serai_client::primitives::NETWORKS { 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); cosigns.push(cosign);
} }
} }
@@ -303,79 +337,90 @@ impl<D: Db> Cosigning<D> {
// more relevant, cosign) again. // more relevant, cosign) again.
// //
// Takes `&mut self` as this should only be called once at any given moment. // Takes `&mut self` as this should only be called once at any given moment.
pub async fn intake_cosign(&mut self, cosign: Cosign) -> Result<bool, String> { // TODO: Don't overload bool here
// Check if we've prior handled this cosign pub async fn intake_cosign(&mut self, signed_cosign: SignedCosign) -> Result<bool, String> {
let mut txn = self.db.txn(); let cosign = &signed_cosign.cosign;
let mut cosigns_for_this_block_position =
Cosigns::get(&txn, cosign.block_number).unwrap_or(vec![]); let Cosigner::ValidatorSet(network) = cosign.cosigner else {
if cosigns_for_this_block_position.iter().any(|existing| *existing == cosign) { // TODO
// Individually signed cosign despite that protocol not being implemented
return Ok(false);
};
// Check this isn't a dated cosign
if let Some(existing) =
NetworksLatestCosignedBlock::get(&self.db, cosign.global_session, network)
{
if existing.cosign.block_number >= cosign.block_number {
return Ok(true);
}
}
// Check our finalized (and indexed by intend) blockchain exceeds this block number
if cosign.block_number >= intend::ScanCosignFrom::get(&self.db).unwrap_or(0) {
return Ok(true); return Ok(true);
} }
// Check we can verify this cosign's signature
let Some((global_session_start_block_number, global_session_start_block_hash)) = let Some((global_session_start_block_number, global_session_start_block_hash)) =
GlobalSessions::get(&txn, cosign.global_session) GlobalSessions::get(&self.db, cosign.global_session)
else { else {
// Unrecognized global session // Unrecognized global session
return Ok(true); return Ok(true);
}; };
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);
}
// Check the cosign's signature // 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);
};
todo!("TODO");
network
}
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() <
cosign.block_number
{ {
// Unrecognized block number let key = match cosign.cosigner {
return Ok(true); 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);
};
keys.0
}
Cosigner::Validator(signer) => signer.into(),
};
if !signed_cosign.verify_signature(key) {
return Ok(false);
}
} }
// Since we verified this cosign's signature, and have a chain sufficiently long, handle the // Since we verified this cosign's signature, and have a chain sufficiently long, handle the
// cosign // cosign
// Save the cosign to the database let mut txn = self.db.txn();
cosigns_for_this_block_position.push(cosign);
Cosigns::set(&mut txn, cosign.block_number, &cosigns_for_this_block_position);
let our_block_hash = self let our_block_hash = self
.serai .serai
.block_hash(cosign.block_number) .block_hash(cosign.block_number)
.await .await
.map_err(|e| format!("{e:?}"))? .map_err(|e| format!("{e:?}"))?
.expect("requested hash of a finalized block yet received None"); .ok_or_else(|| "requested hash of a finalized block yet received None".to_string())?;
if our_block_hash == cosign.block_hash { 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 this is for a future global session, we don't acknowledge this cosign at this time
if global_session_start_block_number > LatestCosignedBlockNumber::get(&self.db).unwrap_or(0) { if global_session_start_block_number > LatestCosignedBlockNumber::get(&txn).unwrap_or(0) {
drop(txn); drop(txn);
return Ok(true); return Ok(true);
} }
if NetworksLatestCosignedBlock::get(&txn, network) NetworksLatestCosignedBlock::set(&mut txn, cosign.global_session, network, &signed_cosign);
.map(|cosign| cosign.block_number)
.unwrap_or(0) <
cosign.block_number
{
NetworksLatestCosignedBlock::set(&mut txn, network, &cosign);
}
} else { } else {
let mut faults = Faults::get(&txn, cosign.global_session).unwrap_or(vec![]); 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 // Only handle this as a fault if this set wasn't prior faulty
if !faults.iter().any(|cosign| cosign.cosigner == Cosigner::ValidatorSet(network)) { if !faults.iter().any(|cosign| cosign.cosign.cosigner == Cosigner::ValidatorSet(network)) {
faults.push(cosign); faults.push(signed_cosign.clone());
Faults::set(&mut txn, cosign.global_session, &faults); Faults::set(&mut txn, cosign.global_session, &faults);
let mut weight_cosigned = 0; let mut weight_cosigned = 0;
@@ -394,16 +439,15 @@ impl<D: Db> Cosigning<D> {
total_weight += stake; total_weight += stake;
// Check if this set cosigned this block or not // Check if this set cosigned this block or not
if faults.iter().any(|cosign| cosign.cosigner == Cosigner::ValidatorSet(set.network)) { if faults
.iter()
.any(|cosign| cosign.cosign.cosigner == Cosigner::ValidatorSet(set.network))
{
weight_cosigned += total_weight weight_cosigned += total_weight
} }
} }
// Check if the sum weight means a fault has occurred // Check if the sum weight means a fault has occurred
assert!(
total_weight != 0,
"evaluating valid cosign when no stake was present in the system"
);
if weight_cosigned >= ((total_weight * 17) / 100) { if weight_cosigned >= ((total_weight * 17) / 100) {
FaultedSession::set(&mut txn, &cosign.global_session); FaultedSession::set(&mut txn, &cosign.global_session);
} }