Add cosign signature verification

This commit is contained in:
Luke Parker
2024-12-25 00:06:46 -05:00
parent 4de1a5804d
commit ef972b2658
4 changed files with 62 additions and 38 deletions

View File

@@ -18,14 +18,10 @@ rustdoc-args = ["--cfg", "docsrs"]
workspace = true workspace = true
[dependencies] [dependencies]
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"] }
blake2 = { version = "0.10", default-features = false, features = ["std"] } blake2 = { version = "0.10", default-features = false, features = ["std"] }
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["std"] } schnorrkel = { version = "0.11", 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"] } borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] }
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

@@ -70,7 +70,7 @@ impl<D: Db, R: RequestNotableCosigns> ContinuallyRan for CosignEvaluatorTask<D,
// Check if we have the cosign from this set // Check if we have the cosign from this set
if cosigns_for_block if cosigns_for_block
.iter() .iter()
.any(|cosign| cosign.cosigner == Cosigner::ValidatorSet(set.network)) .any(|cosign| cosign.cosign.cosigner == Cosigner::ValidatorSet(set.network))
{ {
// 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;
@@ -139,14 +139,14 @@ impl<D: Db, R: RequestNotableCosigns> ContinuallyRan for CosignEvaluatorTask<D,
let Some(cosign) = NetworksLatestCosignedBlock::get(&txn, set.network) else { let Some(cosign) = NetworksLatestCosignedBlock::get(&txn, 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

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
@@ -63,14 +66,14 @@ create_db! {
// //
// 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.
Cosigns: (block_number: u64) -> Vec<Cosign>, Cosigns: (block_number: u64) -> Vec<SignedCosign>,
// 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, NetworksLatestCosignedBlock: (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 +108,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,16 +116,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,
}
/// A signed cosign.
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)]
pub struct SignedCosign {
/// The cosign.
pub cosign: Cosign,
/// The signature for the cosign.
pub signature: [u8; 64],
}
impl SignedCosign {
fn verify_signature(&self, signer: serai_client::Public) -> bool {
let Ok(signer) = schnorrkel::PublicKey::from_bytes(&signer.0) else { return false };
let Ok(signature) = schnorrkel::Signature::from_bytes(&self.signature) else { return false };
signer.verify_simple(COSIGN_CONTEXT, &borsh::to_vec(&self.cosign).unwrap(), &signature).is_ok()
}
} }
/// Construct a `TemporalSerai` bound to the time used for cosigning this block. /// Construct a `TemporalSerai` bound to the time used for cosigning this block.
@@ -258,7 +279,7 @@ 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> { pub fn notable_cosigns(&self, global_session: [u8; 32]) -> Vec<SignedCosign> {
todo!("TODO") todo!("TODO")
} }
@@ -266,14 +287,14 @@ impl<D: Db> Cosigning<D> {
/// ///
/// 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).unwrap();
// 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, network) {
if cosign.global_session == faulted { if cosign.cosign.global_session == faulted {
cosigns.push(cosign); cosigns.push(cosign);
} }
} }
@@ -303,12 +324,14 @@ 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> { pub async fn intake_cosign(&mut self, signed_cosign: SignedCosign) -> Result<bool, String> {
let cosign = &signed_cosign.cosign;
// Check if we've prior handled this cosign // Check if we've prior handled this cosign
let mut txn = self.db.txn(); let mut txn = self.db.txn();
let mut cosigns_for_this_block_position = let mut cosigns_for_this_block_position =
Cosigns::get(&txn, cosign.block_number).unwrap_or(vec![]); Cosigns::get(&txn, cosign.block_number).unwrap_or(vec![]);
if cosigns_for_this_block_position.iter().any(|existing| *existing == cosign) { if cosigns_for_this_block_position.iter().any(|existing| existing.cosign == *cosign) {
return Ok(true); return Ok(true);
} }
@@ -329,7 +352,9 @@ impl<D: Db> Cosigning<D> {
return Ok(false); return Ok(false);
}; };
todo!("TODO"); if !signed_cosign.verify_signature(keys.0) {
return Ok(false);
}
network network
} }
@@ -348,7 +373,7 @@ impl<D: Db> Cosigning<D> {
// cosign // cosign
// Save the cosign to the database // Save the cosign to the database
cosigns_for_this_block_position.push(cosign); cosigns_for_this_block_position.push(signed_cosign.clone());
Cosigns::set(&mut txn, cosign.block_number, &cosigns_for_this_block_position); Cosigns::set(&mut txn, cosign.block_number, &cosigns_for_this_block_position);
let our_block_hash = self let our_block_hash = self
@@ -359,23 +384,23 @@ impl<D: Db> Cosigning<D> {
.expect("requested hash of a finalized block yet received None"); .expect("requested hash of a finalized block yet received None");
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) if NetworksLatestCosignedBlock::get(&txn, network)
.map(|cosign| cosign.block_number) .map(|cosign| cosign.cosign.block_number)
.unwrap_or(0) < .unwrap_or(0) <
cosign.block_number cosign.block_number
{ {
NetworksLatestCosignedBlock::set(&mut txn, network, &cosign); NetworksLatestCosignedBlock::set(&mut txn, network, &signed_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,7 +419,10 @@ 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
} }
} }