mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Dedicated library for intending and evaluating cosigns
Not only cleans the existing cosign code but enables non-Serai-coordinators to evaluate cosigns if they gain access to a feed of them (such as over an RPC). This would let centralized services not only track the finalized chain yet the cosigned chain without directly running a coordinator. Still being wrapped up.
This commit is contained in:
416
coordinator/cosign/src/lib.rs
Normal file
416
coordinator/cosign/src/lib.rs
Normal file
@@ -0,0 +1,416 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use core::{fmt::Debug, future::Future};
|
||||
|
||||
use blake2::{Digest, Blake2s256};
|
||||
|
||||
use borsh::{BorshSerialize, BorshDeserialize};
|
||||
|
||||
use serai_client::{
|
||||
primitives::{Amount, NetworkId, SeraiAddress},
|
||||
validator_sets::primitives::{Session, ValidatorSet, KeyPair},
|
||||
Block, Serai, TemporalSerai,
|
||||
};
|
||||
|
||||
use serai_db::*;
|
||||
use serai_task::*;
|
||||
|
||||
/// The cosigns which are intended to be performed.
|
||||
mod intend;
|
||||
/// The evaluator of the cosigns.
|
||||
mod evaluator;
|
||||
use evaluator::LatestCosignedBlockNumber;
|
||||
|
||||
/// 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
|
||||
/// distinct blocks at distinct positions within a global session, we still identify the faults.
|
||||
/*
|
||||
There is the attack where a validator set is given an alternate blockchain with a key generation
|
||||
event at block #n, while most validator sets are given a blockchain with a key generation event
|
||||
at block number #(n+1). This prevents whoever has the alternate blockchain from verifying the
|
||||
cosigns on the primary blockchain, and detecting the faults, if they use the keys as of the block
|
||||
prior to the block being cosigned.
|
||||
|
||||
We solve this by binding cosigns to a global session ID, which has a specific start block, and
|
||||
reading the keys from the start block. This means that so long as all validator sets agree on the
|
||||
start of a global session, they can verify all cosigns produced by that session, regardless of
|
||||
how it advances. Since agreeing on the start of a global session is mandated, there's no way to
|
||||
have validator sets follow two distinct global sessions without breaking the bounds of the
|
||||
cosigning protocol.
|
||||
*/
|
||||
#[derive(Debug, BorshSerialize, BorshDeserialize)]
|
||||
struct GlobalSession {
|
||||
cosigners: Vec<ValidatorSet>,
|
||||
}
|
||||
impl GlobalSession {
|
||||
fn new(mut cosigners: Vec<ValidatorSet>) -> Self {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
//
|
||||
// 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<Cosign>,
|
||||
// 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) -> Cosign,
|
||||
// Cosigns received for blocks not locally recognized as finalized.
|
||||
Faults: (global_session: [u8; 32]) -> Vec<Cosign>,
|
||||
// The global session which faulted.
|
||||
FaultedSession: () -> [u8; 32],
|
||||
}
|
||||
}
|
||||
|
||||
/// If the block has events.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
|
||||
enum HasEvents {
|
||||
/// The block had a notable event.
|
||||
///
|
||||
/// This is a special case as blocks with key gen events change the keys used for cosigning, and
|
||||
/// accordingly must be cosigned before we advance past them.
|
||||
Notable,
|
||||
/// The block had an non-notable event justifying a cosign.
|
||||
NonNotable,
|
||||
/// The block didn't have an event justifying a cosign.
|
||||
No,
|
||||
}
|
||||
|
||||
/// An intended cosign.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
|
||||
struct CosignIntent {
|
||||
/// The global session this cosign is being performed under.
|
||||
global_session: [u8; 32],
|
||||
/// The number of the block to cosign.
|
||||
block_number: u64,
|
||||
/// The hash of the block to cosign.
|
||||
block_hash: [u8; 32],
|
||||
/// If this cosign must be handled before further cosigns are.
|
||||
notable: bool,
|
||||
}
|
||||
|
||||
/// The identification of a cosigner.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
|
||||
enum Cosigner {
|
||||
/// The network which produced this cosign.
|
||||
ValidatorSet(NetworkId),
|
||||
/// The individual validator which produced this cosign.
|
||||
Validator(SeraiAddress),
|
||||
}
|
||||
|
||||
/// A cosign.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
|
||||
struct Cosign {
|
||||
/// The global session this cosign is being performed under.
|
||||
global_session: [u8; 32],
|
||||
/// The number of the block to cosign.
|
||||
block_number: u64,
|
||||
/// The hash of the block to cosign.
|
||||
block_hash: [u8; 32],
|
||||
/// The actual cosigner.
|
||||
cosigner: Cosigner,
|
||||
}
|
||||
|
||||
/// 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<'_>,
|
||||
network: NetworkId,
|
||||
) -> Result<Option<(Session, KeyPair)>, String> {
|
||||
let Some(latest_session) =
|
||||
serai.validator_sets().session(network).await.map_err(|e| format!("{e:?}"))?
|
||||
else {
|
||||
// If this network hasn't had a session declared, move on
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Get the keys for the latest session
|
||||
if let Some(keys) = serai
|
||||
.validator_sets()
|
||||
.keys(ValidatorSet { network, session: latest_session })
|
||||
.await
|
||||
.map_err(|e| format!("{e:?}"))?
|
||||
{
|
||||
return Ok(Some((latest_session, keys)));
|
||||
}
|
||||
|
||||
// If the latest session has yet to set keys, use the prior session
|
||||
if let Some(prior_session) = latest_session.0.checked_sub(1).map(Session) {
|
||||
if let Some(keys) = serai
|
||||
.validator_sets()
|
||||
.keys(ValidatorSet { network, session: prior_session })
|
||||
.await
|
||||
.map_err(|e| format!("{e:?}"))?
|
||||
{
|
||||
return Ok(Some((prior_session, keys)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Fetch the `ValidatorSet`s used for cosigning as of this block.
|
||||
async fn cosigning_sets(serai: &TemporalSerai<'_>) -> Result<Vec<ValidatorSet>, 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 {
|
||||
// If this network doesn't have usable keys, move on
|
||||
continue;
|
||||
};
|
||||
|
||||
sets.push(ValidatorSet { network, session });
|
||||
}
|
||||
Ok(sets)
|
||||
}
|
||||
|
||||
/// Fetch the `ValidatorSet`s used for cosigning this block.
|
||||
async fn cosigning_sets_for_block(
|
||||
serai: &Serai,
|
||||
block_number: u64,
|
||||
) -> Result<(Block, Vec<ValidatorSet>), String> {
|
||||
let (block, serai) = temporal_serai_used_for_cosigning(serai, block_number).await?;
|
||||
cosigning_sets(&serai).await.map(|sets| (block, sets))
|
||||
}
|
||||
|
||||
/// An object usable to request notable cosigns for a block.
|
||||
pub trait RequestNotableCosigns: 'static + Send {
|
||||
/// The error type which may be encountered when requesting notable cosigns.
|
||||
type Error: Debug;
|
||||
|
||||
/// Request the notable cosigns for this global session.
|
||||
fn request_notable_cosigns(
|
||||
&self,
|
||||
global_session: [u8; 32],
|
||||
) -> impl Send + Future<Output = Result<(), Self::Error>>;
|
||||
}
|
||||
|
||||
/// An error used to indicate the cosigning protocol has faulted.
|
||||
pub struct Faulted;
|
||||
|
||||
/// The interface to manage cosigning with.
|
||||
pub struct Cosigning<D: Db> {
|
||||
db: D,
|
||||
serai: Serai,
|
||||
}
|
||||
impl<D: Db> Cosigning<D> {
|
||||
/// Spawn the tasks to intend and evaluate cosigns.
|
||||
///
|
||||
/// The database specified must only be used with a singular instance of the Serai network, and
|
||||
/// only used once at any given time.
|
||||
pub fn spawn<R: RequestNotableCosigns>(
|
||||
db: D,
|
||||
serai: Serai,
|
||||
request: R,
|
||||
tasks_to_run_upon_cosigning: Vec<TaskHandle>,
|
||||
) -> Self {
|
||||
let (intend_task, _intend_task_handle) = Task::new();
|
||||
let (evaluator_task, evaluator_task_handle) = Task::new();
|
||||
tokio::spawn(
|
||||
(intend::CosignIntendTask { db: db.clone(), serai: serai.clone() })
|
||||
.continually_run(intend_task, vec![evaluator_task_handle]),
|
||||
);
|
||||
tokio::spawn(
|
||||
(evaluator::CosignEvaluatorTask { db: db.clone(), serai: serai.clone(), request })
|
||||
.continually_run(evaluator_task, tasks_to_run_upon_cosigning),
|
||||
);
|
||||
Self { db, serai }
|
||||
}
|
||||
|
||||
/// The latest cosigned block number.
|
||||
pub fn latest_cosigned_block_number(&self) -> Result<u64, Faulted> {
|
||||
if FaultedSession::get(&self.db).is_some() {
|
||||
Err(Faulted)?;
|
||||
}
|
||||
|
||||
Ok(LatestCosignedBlockNumber::get(&self.db).unwrap_or(0))
|
||||
}
|
||||
|
||||
/// 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")
|
||||
}
|
||||
|
||||
/// The cosigns to rebroadcast ever so often.
|
||||
///
|
||||
/// 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.
|
||||
pub fn cosigns_to_rebroadcast(&self) -> Vec<Cosign> {
|
||||
if let Some(faulted) = FaultedSession::get(&self.db) {
|
||||
let mut cosigns = Faults::get(&self.db, faulted).unwrap();
|
||||
// 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 cosign.global_session == faulted {
|
||||
cosigns.push(cosign);
|
||||
}
|
||||
}
|
||||
}
|
||||
cosigns
|
||||
} else {
|
||||
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) {
|
||||
cosigns.push(cosign);
|
||||
}
|
||||
}
|
||||
cosigns
|
||||
}
|
||||
}
|
||||
|
||||
/// Intake a cosign from the Serai network.
|
||||
///
|
||||
/// - Returns Err(_) if there was an error trying to validate the cosign and it should be retired
|
||||
/// later.
|
||||
/// - Returns Ok(true) if the cosign was successfully handled or could not be handled at this
|
||||
/// time.
|
||||
/// - Returns Ok(false) if the cosign was invalid.
|
||||
//
|
||||
// We collapse a cosign which shouldn't be handled yet into a valid cosign (`Ok(true)`) as we
|
||||
// assume we'll either explicitly request it if we need it or we'll naturally see it (or a later,
|
||||
// more relevant, cosign) again.
|
||||
//
|
||||
// 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> {
|
||||
// Check if we've prior handled 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) {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Check we can verify this cosign's signature
|
||||
let Some((global_session_start_block_number, global_session_start_block_hash)) =
|
||||
GlobalSessions::get(&txn, cosign.global_session)
|
||||
else {
|
||||
// Unrecognized global session
|
||||
return Ok(true);
|
||||
};
|
||||
|
||||
// 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
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// 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(cosign);
|
||||
Cosigns::set(&mut txn, cosign.block_number, &cosigns_for_this_block_position);
|
||||
|
||||
let our_block_hash = self
|
||||
.serai
|
||||
.block_hash(cosign.block_number)
|
||||
.await
|
||||
.map_err(|e| format!("{e:?}"))?
|
||||
.expect("requested hash of a finalized block yet received None");
|
||||
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(&self.db).unwrap_or(0) {
|
||||
drop(txn);
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
if NetworksLatestCosignedBlock::get(&txn, network)
|
||||
.map(|cosign| cosign.block_number)
|
||||
.unwrap_or(0) <
|
||||
cosign.block_number
|
||||
{
|
||||
NetworksLatestCosignedBlock::set(&mut txn, network, &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
|
||||
if !faults.iter().any(|cosign| cosign.cosigner == Cosigner::ValidatorSet(network)) {
|
||||
faults.push(cosign);
|
||||
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.cosigner == Cosigner::ValidatorSet(set.network)) {
|
||||
weight_cosigned += total_weight
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
FaultedSession::set(&mut txn, &cosign.global_session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
txn.commit();
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user