mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-12 22:19:26 +00:00
Compare commits
3 Commits
4de1a5804d
...
5b337c3ce8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b337c3ce8 | ||
|
|
e119fb4c16 | ||
|
|
ef972b2658 |
@@ -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"] }
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user