From 5dba542442395542d1b70dadd7137aec896a06f2 Mon Sep 17 00:00:00 2001 From: Rahul Subramaniyam Date: Wed, 26 Jul 2023 14:29:27 -0700 Subject: [PATCH 1/9] Use NonEmptyVec<> for proof check points, helper methods in PotProof types --- crates/subspace-core-primitives/src/lib.rs | 181 +++++++++++++++++-- crates/subspace-proof-of-time/benches/pot.rs | 32 ++-- crates/subspace-proof-of-time/src/lib.rs | 33 ++-- 3 files changed, 211 insertions(+), 35 deletions(-) diff --git a/crates/subspace-core-primitives/src/lib.rs b/crates/subspace-core-primitives/src/lib.rs index 26841e2cab..a6055eebce 100644 --- a/crates/subspace-core-primitives/src/lib.rs +++ b/crates/subspace-core-primitives/src/lib.rs @@ -44,9 +44,11 @@ use crate::crypto::kzg::{Commitment, Witness}; use crate::crypto::{blake2b_256_hash, blake2b_256_hash_list, blake2b_256_hash_with_key, Scalar}; #[cfg(feature = "serde")] use ::serde::{Deserialize, Serialize}; +use alloc::boxed::Box; use alloc::vec::Vec; use core::convert::AsRef; use core::fmt; +use core::iter::Iterator; use core::num::NonZeroU64; use core::simd::Simd; use derive_more::{Add, AsMut, AsRef, Deref, DerefMut, Display, Div, From, Into, Mul, Rem, Sub}; @@ -230,25 +232,73 @@ impl PosProof { /// Proof of time key(input to the encryption). #[derive( - Debug, Default, Copy, Clone, From, Into, AsRef, AsMut, Encode, Decode, TypeInfo, MaxEncodedLen, + Debug, + Default, + Copy, + Clone, + Eq, + PartialEq, + From, + Into, + AsRef, + AsMut, + Encode, + Decode, + TypeInfo, + MaxEncodedLen, )] pub struct PotKey(PotBytes); /// Proof of time seed (input to the encryption). #[derive( - Debug, Default, Copy, Clone, From, Into, AsRef, AsMut, Encode, Decode, TypeInfo, MaxEncodedLen, + Debug, + Default, + Copy, + Clone, + Eq, + PartialEq, + From, + Into, + AsRef, + AsMut, + Encode, + Decode, + TypeInfo, + MaxEncodedLen, )] pub struct PotSeed(PotBytes); +impl PotSeed { + /// Builds the seed from block hash (e.g) used to create initial seed from + /// genesis block hash. + #[inline] + pub fn from_block_hash(block_hash: BlockHash) -> Self { + Self(truncate_32_bytes(block_hash)) + } +} + /// Proof of time ciphertext (output from the encryption). #[derive( - Debug, Default, Copy, Clone, From, Into, AsRef, AsMut, Encode, Decode, TypeInfo, MaxEncodedLen, + Debug, + Default, + Copy, + Clone, + Eq, + PartialEq, + From, + Into, + AsRef, + AsMut, + Encode, + Decode, + TypeInfo, + MaxEncodedLen, )] pub struct PotCheckpoint(PotBytes); /// Proof of time. /// TODO: versioning. -#[derive(Debug, Clone, Encode, Decode)] +#[derive(Debug, Clone, Encode, Decode, Eq, PartialEq)] pub struct PotProof { /// Slot the proof was evaluated for. pub slot_number: SlotNumber, @@ -260,7 +310,7 @@ pub struct PotProof { pub key: PotKey, /// The encrypted outputs from each stage. - pub checkpoints: Vec, + pub checkpoints: NonEmptyVec, /// Hash of last block at injection point. pub injected_block_hash: BlockHash, @@ -272,7 +322,7 @@ impl PotProof { slot_number: SlotNumber, seed: PotSeed, key: PotKey, - checkpoints: Vec, + checkpoints: NonEmptyVec, injected_block_hash: BlockHash, ) -> Self { Self { @@ -285,17 +335,57 @@ impl PotProof { } /// Returns the last check point. - pub fn output(&self) -> Option { - self.checkpoints.last().cloned() + pub fn output(&self) -> PotCheckpoint { + self.checkpoints.last() } /// Derives the global randomness from the output. - pub fn derive_global_randomness(&self) -> Option { - self.output() - .map(|checkpoint| blake2b_256_hash(&PotBytes::from(checkpoint))) + pub fn derive_global_randomness(&self) -> Blake2b256Hash { + blake2b_256_hash(&PotBytes::from(self.output())) + } + + /// Derives the next seed based on the injected randomness. + pub fn next_seed(&self, injected_hash: Option) -> PotSeed { + match injected_hash { + Some(injected_hash) => { + // Next seed = Hash(last checkpoint + injected hash). + let hash = blake2b_256_hash_list(&[&self.output().0, &injected_hash]); + PotSeed::from(truncate_32_bytes(hash)) + } + None => { + // No injected randomness, next seed = last checkpoint. + PotSeed::from(self.output().0) + } + } + } + + /// Derives the next key from the hash of the current seed. + pub fn next_key(&self) -> PotKey { + PotKey::from(truncate_32_bytes(blake2b_256_hash(&self.seed.0))) } } +impl fmt::Display for PotProof { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "PotProof: [slot={}, seed={}, key={}, injected={}, checkpoints={}]", + self.slot_number, + hex::encode(self.seed.0), + hex::encode(self.key.0), + hex::encode(self.injected_block_hash), + self.checkpoints.len() + ) + } +} + +/// Helper to truncate the 32 bytes to 16 bytes. +fn truncate_32_bytes(bytes: [u8; 32]) -> PotBytes { + bytes[..core::mem::size_of::()] + .try_into() + .expect("Hash is longer than seed; qed") +} + /// A Ristretto Schnorr public key as bytes produced by `schnorrkel` crate. #[derive( Debug, @@ -982,3 +1072,72 @@ impl SectorId { Some(HistorySize::from(expiration_history_size)) } } + +/// A Vec<> that enforces the invariant that it cannot be empty. +#[derive(Debug, Clone, Encode, Decode, Eq, PartialEq)] +pub struct NonEmptyVec(Vec); + +/// Error codes for `NonEmptyVec`. +#[derive(Debug)] +pub enum NonEmptyVecErr { + /// Tried to create with an empty Vec + EmptyVec, +} + +#[allow(clippy::len_without_is_empty)] +impl NonEmptyVec { + /// Creates the Vec. + pub fn new(vec: Vec) -> Result { + if vec.is_empty() { + return Err(NonEmptyVecErr::EmptyVec); + } + + Ok(Self(vec)) + } + + /// Returns the number of entries. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns the slice of the entries. + pub fn as_slice(&self) -> &[T] { + self.0.as_slice() + } + + /// Returns an iterator for the entries. + pub fn iter(&self) -> Box + '_> { + Box::new(self.0.iter()) + } + + /// Returns a mutable iterator for the entries. + pub fn iter_mut(&mut self) -> Box + '_> { + Box::new(self.0.iter_mut()) + } + + /// Returns the first entry. + pub fn first(&self) -> T { + self.0 + .first() + .expect("NonEmptyVec::first(): collection cannot be empty") + .clone() + } + + /// Returns the last entry. + pub fn last(&self) -> T { + self.0 + .last() + .expect("NonEmptyVec::last(): collection cannot be empty") + .clone() + } + + /// Adds an entry to the end. + pub fn push(&mut self, entry: T) { + self.0.push(entry); + } + + /// Returns the entries in the collection. + pub fn to_vec(self) -> Vec { + self.0 + } +} diff --git a/crates/subspace-proof-of-time/benches/pot.rs b/crates/subspace-proof-of-time/benches/pot.rs index 4a27cbc0dd..908d2030dc 100644 --- a/crates/subspace-proof-of-time/benches/pot.rs +++ b/crates/subspace-proof-of-time/benches/pot.rs @@ -19,27 +19,33 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("prove/sequential", |b| { b.iter(|| { - proof_of_time_sequential.create( - black_box(seed), - black_box(key), - black_box(slot_number), - black_box(injected_block_hash), - ); + proof_of_time_sequential + .create( + black_box(seed), + black_box(key), + black_box(slot_number), + black_box(injected_block_hash), + ) + .unwrap(); }) }); c.bench_function("prove/checkpoints", |b| { b.iter(|| { - proof_of_time.create( - black_box(seed), - black_box(key), - black_box(slot_number), - black_box(injected_block_hash), - ); + proof_of_time + .create( + black_box(seed), + black_box(key), + black_box(slot_number), + black_box(injected_block_hash), + ) + .unwrap(); }) }); - let proof = proof_of_time.create(seed, key, slot_number, injected_block_hash); + let proof = proof_of_time + .create(seed, key, slot_number, injected_block_hash) + .unwrap(); c.bench_function("verify", |b| { b.iter(|| { diff --git a/crates/subspace-proof-of-time/src/lib.rs b/crates/subspace-proof-of-time/src/lib.rs index d8ff6094b9..dd3af2e9be 100644 --- a/crates/subspace-proof-of-time/src/lib.rs +++ b/crates/subspace-proof-of-time/src/lib.rs @@ -3,7 +3,16 @@ #![cfg_attr(not(feature = "std"), no_std)] mod pot_aes; -use subspace_core_primitives::{BlockHash, PotKey, PotProof, PotSeed, SlotNumber}; +use subspace_core_primitives::{ + BlockHash, NonEmptyVec, NonEmptyVecErr, PotKey, PotProof, PotSeed, SlotNumber, +}; + +#[derive(Debug)] +#[cfg_attr(feature = "thiserror", derive(thiserror::Error))] +pub enum PotCreationError { + #[cfg_attr(feature = "thiserror", error("Failed to initialize checkpoints"))] + CheckpointInitFailed(NonEmptyVecErr), +} #[derive(Debug)] #[cfg_attr(feature = "thiserror", derive(thiserror::Error))] @@ -43,19 +52,21 @@ impl ProofOfTime { key: PotKey, slot_number: SlotNumber, injected_block_hash: BlockHash, - ) -> PotProof { - PotProof::new( + ) -> Result { + let checkpoints = NonEmptyVec::new(pot_aes::create( + &seed, + &key, + self.num_checkpoints, + self.checkpoint_iterations, + )) + .map_err(PotCreationError::CheckpointInitFailed)?; + Ok(PotProof::new( slot_number, seed, key, - pot_aes::create( - &seed, - &key, - self.num_checkpoints, - self.checkpoint_iterations, - ), + checkpoints, injected_block_hash, - ) + )) } /// Verifies the proof. @@ -71,7 +82,7 @@ impl ProofOfTime { if pot_aes::verify_sequential( &proof.seed, &proof.key, - &proof.checkpoints, + proof.checkpoints.as_slice(), self.checkpoint_iterations, ) { Ok(()) From 8f5761af9ab9a4aee6f41f7ea3c319ca1a70f532 Mon Sep 17 00:00:00 2001 From: Rahul Subramaniyam Date: Fri, 28 Jul 2023 14:09:08 -0700 Subject: [PATCH 2/9] Change proof of time to use non-zero arguments --- crates/subspace-proof-of-time/benches/pot.rs | 44 ++++++++--------- crates/subspace-proof-of-time/src/lib.rs | 51 ++++++++++++-------- 2 files changed, 53 insertions(+), 42 deletions(-) diff --git a/crates/subspace-proof-of-time/benches/pot.rs b/crates/subspace-proof-of-time/benches/pot.rs index 908d2030dc..aecc95495a 100644 --- a/crates/subspace-proof-of-time/benches/pot.rs +++ b/crates/subspace-proof-of-time/benches/pot.rs @@ -1,3 +1,4 @@ +use core::num::{NonZeroU32, NonZeroU8}; use criterion::{black_box, criterion_group, criterion_main, Criterion}; use rand::{thread_rng, Rng}; use subspace_core_primitives::{BlockHash, PotKey, PotSeed}; @@ -11,41 +12,38 @@ fn criterion_benchmark(c: &mut Criterion) { let slot_number = 1; let mut injected_block_hash = BlockHash::default(); thread_rng().fill(injected_block_hash.as_mut()); - let checkpoints = 8; + let checkpoints_1 = NonZeroU8::new(1).expect("Creating checkpoints cannot fail"); + let checkpoints_8 = NonZeroU8::new(8).expect("Creating checkpoints cannot fail"); // About 1s on 5.5 GHz Raptor Lake CPU - let iterations = 166_000_000; - let proof_of_time_sequential = ProofOfTime::new(1, iterations); - let proof_of_time = ProofOfTime::new(checkpoints, iterations / u32::from(checkpoints)); + let pot_iterations = NonZeroU32::new(166_000_000).expect("Creating pot_iterations cannot fail"); + let proof_of_time_sequential = ProofOfTime::new(pot_iterations, checkpoints_1) + .expect("Failed to create proof_of_time_sequential"); + let proof_of_time = + ProofOfTime::new(pot_iterations, checkpoints_8).expect("Failed to create proof_of_time"); c.bench_function("prove/sequential", |b| { b.iter(|| { - proof_of_time_sequential - .create( - black_box(seed), - black_box(key), - black_box(slot_number), - black_box(injected_block_hash), - ) - .unwrap(); + proof_of_time_sequential.create( + black_box(seed), + black_box(key), + black_box(slot_number), + black_box(injected_block_hash), + ); }) }); c.bench_function("prove/checkpoints", |b| { b.iter(|| { - proof_of_time - .create( - black_box(seed), - black_box(key), - black_box(slot_number), - black_box(injected_block_hash), - ) - .unwrap(); + proof_of_time.create( + black_box(seed), + black_box(key), + black_box(slot_number), + black_box(injected_block_hash), + ); }) }); - let proof = proof_of_time - .create(seed, key, slot_number, injected_block_hash) - .unwrap(); + let proof = proof_of_time.create(seed, key, slot_number, injected_block_hash); c.bench_function("verify", |b| { b.iter(|| { diff --git a/crates/subspace-proof-of-time/src/lib.rs b/crates/subspace-proof-of-time/src/lib.rs index dd3af2e9be..59de2a4a5a 100644 --- a/crates/subspace-proof-of-time/src/lib.rs +++ b/crates/subspace-proof-of-time/src/lib.rs @@ -3,15 +3,22 @@ #![cfg_attr(not(feature = "std"), no_std)] mod pot_aes; -use subspace_core_primitives::{ - BlockHash, NonEmptyVec, NonEmptyVecErr, PotKey, PotProof, PotSeed, SlotNumber, -}; +use core::num::{NonZeroU32, NonZeroU8}; +use subspace_core_primitives::{BlockHash, NonEmptyVec, PotKey, PotProof, PotSeed, SlotNumber}; #[derive(Debug)] #[cfg_attr(feature = "thiserror", derive(thiserror::Error))] -pub enum PotCreationError { - #[cfg_attr(feature = "thiserror", error("Failed to initialize checkpoints"))] - CheckpointInitFailed(NonEmptyVecErr), +pub enum PotInitError { + #[cfg_attr( + feature = "thiserror", + error( + "pot_iterations not multiple of num_checkpoints: {pot_iterations}, {num_checkpoints}" + ) + )] + NotMultiple { + pot_iterations: u32, + num_checkpoints: u8, + }, } #[derive(Debug)] @@ -38,11 +45,23 @@ pub struct ProofOfTime { impl ProofOfTime { /// Creates the AES wrapper. - pub fn new(num_checkpoints: u8, checkpoint_iterations: u32) -> Self { - Self { - num_checkpoints, - checkpoint_iterations, + pub fn new( + pot_iterations: NonZeroU32, + num_checkpoints: NonZeroU8, + ) -> Result { + let pot_iterations = pot_iterations.get(); + let num_checkpoints = num_checkpoints.get(); + if pot_iterations % (num_checkpoints as u32) != 0 { + return Err(PotInitError::NotMultiple { + pot_iterations, + num_checkpoints, + }); } + + Ok(Self { + num_checkpoints, + checkpoint_iterations: pot_iterations / (num_checkpoints as u32), + }) } /// Builds the proof. @@ -52,21 +71,15 @@ impl ProofOfTime { key: PotKey, slot_number: SlotNumber, injected_block_hash: BlockHash, - ) -> Result { + ) -> PotProof { let checkpoints = NonEmptyVec::new(pot_aes::create( &seed, &key, self.num_checkpoints, self.checkpoint_iterations, )) - .map_err(PotCreationError::CheckpointInitFailed)?; - Ok(PotProof::new( - slot_number, - seed, - key, - checkpoints, - injected_block_hash, - )) + .expect("Failed to create proof of time"); + PotProof::new(slot_number, seed, key, checkpoints, injected_block_hash) } /// Verifies the proof. From 46704668ea439921137f37b1fa7d8d396c2a7bd9 Mon Sep 17 00:00:00 2001 From: Rahul Subramaniyam Date: Wed, 26 Jul 2023 15:15:07 -0700 Subject: [PATCH 3/9] Introduce `sc-proof-of-time` crate as PoT client component --- Cargo.lock | 12 + crates/sc-proof-of-time/Cargo.toml | 19 ++ crates/sc-proof-of-time/src/lib.rs | 49 ++++ crates/sc-proof-of-time/src/state_manager.rs | 291 +++++++++++++++++++ 4 files changed, 371 insertions(+) create mode 100644 crates/sc-proof-of-time/Cargo.toml create mode 100644 crates/sc-proof-of-time/src/lib.rs create mode 100644 crates/sc-proof-of-time/src/state_manager.rs diff --git a/Cargo.lock b/Cargo.lock index 89c04e14bc..6a56efc588 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8671,6 +8671,18 @@ dependencies = [ "tracing", ] +[[package]] +name = "sc-proof-of-time" +version = "0.1.0" +dependencies = [ + "parking_lot 0.12.1", + "sc-network", + "subspace-core-primitives", + "subspace-proof-of-time", + "thiserror", + "tracing", +] + [[package]] name = "sc-proposer-metrics" version = "0.10.0-dev" diff --git a/crates/sc-proof-of-time/Cargo.toml b/crates/sc-proof-of-time/Cargo.toml new file mode 100644 index 0000000000..718860b848 --- /dev/null +++ b/crates/sc-proof-of-time/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "sc-proof-of-time" +description = "Subspace proof of time implementation" +license = "MIT OR Apache-2.0" +version = "0.1.0" +authors = ["Rahul Subramaniyam "] +edition = "2021" +include = [ + "/src", + "/Cargo.toml", +] + +[dependencies] +sc-network = { version = "0.10.0-dev", git = "https://github.com/subspace/substrate", rev = "55c157cff49b638a59d81a9f971f0f9a66829c71" } +subspace-core-primitives = { version = "0.1.0", path = "../subspace-core-primitives" } +subspace-proof-of-time = { version = "0.1.0", path = "../subspace-proof-of-time" } +parking_lot = "0.12.1" +thiserror = "1.0.38" +tracing = "0.1.37" \ No newline at end of file diff --git a/crates/sc-proof-of-time/src/lib.rs b/crates/sc-proof-of-time/src/lib.rs new file mode 100644 index 0000000000..c3db72b400 --- /dev/null +++ b/crates/sc-proof-of-time/src/lib.rs @@ -0,0 +1,49 @@ +//! Subspace proof of time implementation. + +mod state_manager; + +use core::num::{NonZeroU32, NonZeroU8}; +use subspace_core_primitives::{BlockNumber, SlotNumber}; + +// TODO: change the fields that can't be zero to NonZero types. +#[derive(Debug, Clone)] +pub struct PotConfig { + /// Frequency of entropy injection from consensus. + pub randomness_update_interval_blocks: BlockNumber, + + /// Starting point for entropy injection from consensus. + pub injection_depth_blocks: BlockNumber, + + /// Number of slots it takes for updated global randomness to + /// take effect. + pub global_randomness_reveal_lag_slots: SlotNumber, + + /// Number of slots it takes for injected randomness to + /// take effect. + pub pot_injection_lag_slots: SlotNumber, + + /// If the received proof is more than max_future_slots into the + /// future from the current tip's slot, reject it. + pub max_future_slots: SlotNumber, + + /// Total iterations per proof. + pub pot_iterations: NonZeroU32, + + /// Number of checkpoints per proof. + pub num_checkpoints: NonZeroU8, +} + +impl Default for PotConfig { + fn default() -> Self { + // TODO: fill proper values + Self { + randomness_update_interval_blocks: 18, + injection_depth_blocks: 90, + global_randomness_reveal_lag_slots: 6, + pot_injection_lag_slots: 6, + max_future_slots: 10, + pot_iterations: NonZeroU32::new(16 * 200_000).expect("pot_iterations cannot be zero"), + num_checkpoints: NonZeroU8::new(16).expect("num_checkpoints cannot be zero"), + } + } +} diff --git a/crates/sc-proof-of-time/src/state_manager.rs b/crates/sc-proof-of-time/src/state_manager.rs new file mode 100644 index 0000000000..51cd79edd1 --- /dev/null +++ b/crates/sc-proof-of-time/src/state_manager.rs @@ -0,0 +1,291 @@ +//! PoT state management. + +use crate::PotConfig; +use parking_lot::Mutex; +use sc_network::PeerId; +use std::collections::btree_map::Entry; +use std::collections::BTreeMap; +use std::sync::Arc; +use subspace_core_primitives::{NonEmptyVec, PotKey, PotProof, PotSeed, SlotNumber}; +use subspace_proof_of_time::{PotVerificationError, ProofOfTime}; + +#[derive(Debug, thiserror::Error)] +pub(crate) enum PotProtocolStateError { + #[error("Failed to extend chain: {expected}/{actual}")] + TipMismatch { + expected: SlotNumber, + actual: SlotNumber, + }, + + #[error("Proof for an older slot number: {tip_slot}/{proof_slot}")] + StaleProof { + tip_slot: SlotNumber, + proof_slot: SlotNumber, + }, + + #[error("Proof had an unexpected seed: {expected:?}/{actual:?}")] + InvalidSeed { expected: PotSeed, actual: PotSeed }, + + #[error("Proof had an unexpected key: {expected:?}/{actual:?}")] + InvalidKey { expected: PotKey, actual: PotKey }, + + #[error("Proof verification failed: {0:?}")] + InvalidProof(PotVerificationError), + + #[error("Proof is too much into future: {tip_slot}/{proof_slot}")] + TooFuturistic { + tip_slot: SlotNumber, + proof_slot: SlotNumber, + }, + + #[error("Duplicate proof from peer: {0:?}")] + DuplicateProofFromPeer(PeerId), +} + +/// The shared PoT state. +struct InternalState { + /// Last N entries of the PotChain, sorted by height. + /// TODO: purging to be implemented. + chain: Vec, + + /// Proofs for future slot numbers, indexed by slot number. + /// Each entry holds the proofs indexed by sender. The proofs + /// are already verified before being added to the future list. + /// TODO: limit the number of proofs per future slot. + future_proofs: BTreeMap>, +} + +/// Wrapper to manage the state. +struct StateManager { + /// Pot config + config: PotConfig, + + /// PoT wrapper for verification. + proof_of_time: Arc, + + /// The PoT state + state: Mutex, +} + +impl StateManager { + /// Creates the state. + pub fn new(config: PotConfig, proof_of_time: Arc, chain: Vec) -> Self { + Self { + config, + proof_of_time, + state: Mutex::new(InternalState { + chain, + future_proofs: BTreeMap::new(), + }), + } + } + + /// Extends the chain with the given proof, without verifying it + /// (e.g) called when clock maker locally produces a proof. + pub fn extend_chain(&self, proof: &PotProof) -> Result<(), PotProtocolStateError> { + let mut state = self.state.lock(); + let tip = match state.chain.last() { + Some(tip) => tip, + None => { + self.add_to_tip(&mut state, proof); + return Ok(()); + } + }; + + if (tip.slot_number + 1) == proof.slot_number { + self.add_to_tip(&mut state, proof); + Ok(()) + } else { + // The tip moved by the time the proof was computed. + Err(PotProtocolStateError::TipMismatch { + expected: tip.slot_number + 1, + actual: proof.slot_number, + }) + } + } + + /// Extends the chain with the given proof, after verifying it + /// (e.g) called when the proof is received from a peer via gossip. + pub fn verify_and_extend_chain( + &self, + sender: PeerId, + proof: &PotProof, + ) -> Result<(), PotProtocolStateError> { + // Verify the proof outside the lock. + // TODO: penalize peers that send too many bad proofs. + self.proof_of_time + .verify(proof) + .map_err(PotProtocolStateError::InvalidProof)?; + + let mut state = self.state.lock(); + let tip = match state.chain.last() { + Some(tip) => tip.clone(), + None => { + self.add_to_tip(&mut state, proof); + return Ok(()); + } + }; + + // Case 1: the proof is for an older slot + if proof.slot_number <= tip.slot_number { + return Err(PotProtocolStateError::StaleProof { + tip_slot: tip.slot_number, + proof_slot: proof.slot_number, + }); + } + + // Case 2: the proof extends the tip + if (tip.slot_number + 1) == proof.slot_number { + let expected_seed = tip.next_seed(None); + if proof.seed != expected_seed { + return Err(PotProtocolStateError::InvalidSeed { + expected: expected_seed, + actual: proof.seed, + }); + } + + let expected_key = tip.next_key(); + if proof.key != expected_key { + return Err(PotProtocolStateError::InvalidKey { + expected: expected_key, + actual: proof.key, + }); + } + + // All checks passed, advance the tip with the new proof + self.add_to_tip(&mut state, proof); + return Ok(()); + } + + // Case 3: proof for a future slot + self.handle_future_proof(&mut state, &tip, sender, proof) + } + + /// Handles the received proof for a future slot. + fn handle_future_proof( + &self, + state: &mut InternalState, + tip: &PotProof, + sender: PeerId, + proof: &PotProof, + ) -> Result<(), PotProtocolStateError> { + // Reject if too much into future + if (proof.slot_number - tip.slot_number) > self.config.max_future_slots { + return Err(PotProtocolStateError::TooFuturistic { + tip_slot: tip.slot_number, + proof_slot: proof.slot_number, + }); + } + + match state.future_proofs.entry(proof.slot_number) { + Entry::Vacant(entry) => { + let mut proofs = BTreeMap::new(); + proofs.insert(sender, proof.clone()); + entry.insert(proofs); + Ok(()) + } + Entry::Occupied(mut entry) => { + let proofs_for_slot = entry.get_mut(); + // Reject if the sender already sent a proof for same slot number. + if proofs_for_slot.contains_key(&sender) { + return Err(PotProtocolStateError::DuplicateProofFromPeer(sender)); + } + + // TODO: put a max limit on future proofs per slot number. + proofs_for_slot.insert(sender, proof.clone()); + Ok(()) + } + } + } + + /// Called when the chain is extended with a new proof. + /// Tries to advance the tip as much as possible, by merging with + /// the pending future proofs. + fn merge_future_proofs(&self, state: &mut InternalState) { + let mut cur_tip = state.chain.last().cloned(); + while let Some(tip) = cur_tip.as_ref() { + // At this point, we know the expected seed/key for the next proof + // in the sequence. If there is at least an entry with the expected + // key/seed(there could be several from different peers), extend the + // chain. + let next_slot = tip.slot_number + 1; + let proofs_for_slot = match state.future_proofs.remove(&next_slot) { + Some(proofs) => proofs, + None => return, + }; + + let next_seed = tip.next_seed(None); + let next_key = tip.next_key(); + match proofs_for_slot + .values() + .find(|proof| proof.seed == next_seed && proof.key == next_key) + .cloned() + { + Some(next_proof) => { + // Extend the tip with the next proof, continue merging. + state.chain.push(next_proof.clone()); + cur_tip = Some(next_proof); + } + None => { + // TODO: penalize peers that sent invalid key/seed + return; + } + } + } + } + + /// Adds the proof to the current tip + fn add_to_tip(&self, state: &mut InternalState, proof: &PotProof) { + state.chain.push(proof.clone()); + state.future_proofs.remove(&proof.slot_number); + self.merge_future_proofs(state); + } +} + +/// Interface to the internal protocol components (clock master, PoT client). +pub(crate) trait PotProtocolState: Send + Sync { + /// Re(initializes) the chain with the given set of proofs. + /// TODO: the proofs are assumed to have been validated, validate + /// if needed. + fn reset(&self, proofs: NonEmptyVec); + + /// Returns the current tip. + fn tip(&self) -> Option; + + /// Called when a proof is produced locally. It tries to extend the + /// chain without verifying the proof. + fn on_proof(&self, proof: &PotProof) -> Result<(), PotProtocolStateError>; + + /// Called when a proof is received via gossip from a peer. The proof + /// is first verified before trying to extend the chain. + fn on_proof_from_peer( + &self, + sender: PeerId, + proof: &PotProof, + ) -> Result<(), PotProtocolStateError>; +} + +impl PotProtocolState for StateManager { + fn reset(&self, proofs: NonEmptyVec) { + let mut proofs = proofs.to_vec(); + let mut state = self.state.lock(); + state.chain.clear(); + state.chain.append(&mut proofs); + state.future_proofs.clear(); + } + fn tip(&self) -> Option { + self.state.lock().chain.last().cloned() + } + + fn on_proof(&self, proof: &PotProof) -> Result<(), PotProtocolStateError> { + self.extend_chain(proof) + } + + fn on_proof_from_peer( + &self, + sender: PeerId, + proof: &PotProof, + ) -> Result<(), PotProtocolStateError> { + self.verify_and_extend_chain(sender, proof) + } +} From 64a3912428c2440381daa4a626fd8170e3fd4d0c Mon Sep 17 00:00:00 2001 From: Rahul Subramaniyam Date: Wed, 26 Jul 2023 15:45:24 -0700 Subject: [PATCH 4/9] Add clock master --- Cargo.lock | 8 + crates/sc-proof-of-time/Cargo.toml | 10 +- crates/sc-proof-of-time/src/clock_master.rs | 232 ++++++++++++++++++++ crates/sc-proof-of-time/src/gossip.rs | 137 ++++++++++++ crates/sc-proof-of-time/src/lib.rs | 21 +- crates/sc-proof-of-time/src/utils.rs | 59 +++++ crates/subspace-proof-of-time/src/lib.rs | 1 + 7 files changed, 466 insertions(+), 2 deletions(-) create mode 100644 crates/sc-proof-of-time/src/clock_master.rs create mode 100644 crates/sc-proof-of-time/src/gossip.rs create mode 100644 crates/sc-proof-of-time/src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 6a56efc588..e5f2593314 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8675,11 +8675,19 @@ dependencies = [ name = "sc-proof-of-time" version = "0.1.0" dependencies = [ + "futures", + "parity-scale-codec", "parking_lot 0.12.1", "sc-network", + "sc-network-gossip", + "sp-blockchain", + "sp-consensus", + "sp-consensus-subspace", + "sp-runtime", "subspace-core-primitives", "subspace-proof-of-time", "thiserror", + "tokio", "tracing", ] diff --git a/crates/sc-proof-of-time/Cargo.toml b/crates/sc-proof-of-time/Cargo.toml index 718860b848..b7cfe41204 100644 --- a/crates/sc-proof-of-time/Cargo.toml +++ b/crates/sc-proof-of-time/Cargo.toml @@ -11,9 +11,17 @@ include = [ ] [dependencies] +futures = "0.3.28" +parity-scale-codec = { version = "3.6.1", features = ["derive"] } sc-network = { version = "0.10.0-dev", git = "https://github.com/subspace/substrate", rev = "55c157cff49b638a59d81a9f971f0f9a66829c71" } +sc-network-gossip = { version = "0.10.0-dev", git = "https://github.com/subspace/substrate", rev = "55c157cff49b638a59d81a9f971f0f9a66829c71" } +sp-blockchain = { version = "4.0.0-dev", git = "https://github.com/subspace/substrate", rev = "55c157cff49b638a59d81a9f971f0f9a66829c71" } +sp-consensus = { version = "0.10.0-dev", git = "https://github.com/subspace/substrate", rev = "55c157cff49b638a59d81a9f971f0f9a66829c71" } +sp-consensus-subspace = { version = "0.1.0", path = "../sp-consensus-subspace" } +sp-runtime = { version = "24.0.0", git = "https://github.com/subspace/substrate", rev = "55c157cff49b638a59d81a9f971f0f9a66829c71" } subspace-core-primitives = { version = "0.1.0", path = "../subspace-core-primitives" } subspace-proof-of-time = { version = "0.1.0", path = "../subspace-proof-of-time" } parking_lot = "0.12.1" thiserror = "1.0.38" -tracing = "0.1.37" \ No newline at end of file +tokio = { version = "1.28.2", features = ["time"] } +tracing = "0.1.37" diff --git a/crates/sc-proof-of-time/src/clock_master.rs b/crates/sc-proof-of-time/src/clock_master.rs new file mode 100644 index 0000000000..8f87c7a81d --- /dev/null +++ b/crates/sc-proof-of-time/src/clock_master.rs @@ -0,0 +1,232 @@ +//! Clock master implementation. + +use crate::gossip::PotGossip; +use crate::state_manager::PotProtocolState; +use crate::utils::get_consensus_tip_proofs; +use crate::PotComponents; +use futures::{FutureExt, StreamExt}; +use parity_scale_codec::{Decode, Encode}; +use sc_network::PeerId; +use sp_blockchain::{HeaderBackend, Info}; +use sp_consensus::SyncOracle; +use sp_runtime::traits::Block as BlockT; +use std::sync::Arc; +use std::thread; +use std::time::Instant; +use subspace_core_primitives::{BlockHash, NonEmptyVec, PotKey, PotProof, PotSeed, SlotNumber}; +use subspace_proof_of_time::ProofOfTime; +use tokio::sync::mpsc::{channel, Sender}; +use tracing::{error, trace, warn}; + +/// Channel size to send the produced proofs. +/// The proof producer thread will block if the receiver is behind and +/// the channel fills up. +const PROOFS_CHANNEL_SIZE: usize = 12; // 2 * reveal lag. + +/// Inputs for bootstrapping. +#[derive(Debug, Clone)] +pub struct BootstrapParams { + /// Genesis block hash. + pub genesis_hash: BlockHash, + + /// The initial key to be used. + pub key: PotKey, + + /// Initial slot number. + pub slot: SlotNumber, +} + +impl BootstrapParams { + pub fn new(genesis_hash: BlockHash, key: PotKey, slot: SlotNumber) -> Self { + Self { + genesis_hash, + key, + slot, + } + } +} + +/// The clock master manages the protocol: periodic proof generation/verification, gossip. +pub struct ClockMaster { + proof_of_time: ProofOfTime, + gossip: PotGossip, + client: Arc, + sync_oracle: Arc, + pot_state: Arc, + chain_info_fn: Arc Info + Send + Sync>, +} + +impl ClockMaster +where + Block: BlockT, + Client: HeaderBackend, + SO: SyncOracle + Send + Sync + Clone + 'static, +{ + /// Creates the clock master instance. + /// TODO: chain_info() is not a trait method, but part of the + /// client::Client struct itself. Passing it in brings in lot + /// of unnecessary generics/dependencies. chain_info_fn() tries + /// to avoid that by using a Fn instead. Follow up with upstream + /// to include this in the trait. + pub fn new( + components: PotComponents, + gossip: PotGossip, + client: Arc, + sync_oracle: Arc, + chain_info_fn: Arc Info + Send + Sync>, + ) -> Self { + let PotComponents { + proof_of_time, + protocol_state: pot_state, + .. + } = components; + + Self { + proof_of_time, + pot_state, + gossip, + client, + sync_oracle, + chain_info_fn, + } + } + + /// Starts the workers. + pub async fn run(self, bootstrap_params: Option) { + if let Some(params) = bootstrap_params.as_ref() { + // The clock master is responsible for bootstrapping, build/add the + // initial proof to the state and start the proof producer. + self.add_bootstrap_proof(params); + } else { + // Wait for sync to complete, get the proof from the tip. + let proofs = match get_consensus_tip_proofs( + self.client.clone(), + self.sync_oracle.clone(), + self.chain_info_fn.clone(), + ) + .await + { + Ok(proofs) => proofs, + Err(err) => { + error!("clock master: Failed to get initial proofs: {err:?}"); + return; + } + }; + self.pot_state.reset(proofs); + } + + let (local_proof_sender, mut local_proof_receiver) = channel(PROOFS_CHANNEL_SIZE); + + // Filter out incoming messages without sender_id or that fail to decode. + let mut incoming_messages = Box::pin(self.gossip.incoming_messages().filter_map( + |notification| async move { + let mut ret = None; + if let Some(sender) = notification.sender { + if let Ok(msg) = PotProof::decode(&mut ¬ification.message[..]) { + ret = Some((sender, msg)) + } + } + ret + }, + )); + + let proof_of_time = self.proof_of_time.clone(); + let pot_state = self.pot_state.clone(); + thread::Builder::new() + .name("pot-proof-producer".to_string()) + .spawn(move || { + Self::produce_proofs(proof_of_time, pot_state, local_proof_sender); + }) + .expect("Failed to spawn PoT proof producer thread"); + + loop { + //let engine = self.gossip.engine.clone(); + //let gossip_engine = futures::future::poll_fn(|cx| engine.lock().poll_unpin(cx)); + + futures::select! { + local_proof = local_proof_receiver.recv().fuse() => { + if let Some(proof) = local_proof { + trace!("clock_master: got local proof: {proof}"); + self.handle_local_proof(proof); + } + }, + gossiped = incoming_messages.next().fuse() => { + if let Some((sender, proof)) = gossiped { + trace!("clock_master: got gossiped proof: {sender} => {proof}"); + self.handle_gossip_message(self.pot_state.as_ref(), sender, proof); + } + }, + _ = self.gossip.is_terminated().fuse() => { + error!("clock_master: gossip engine has terminated."); + return; + } + } + } + } + + /// Long running loop to produce the proofs. + fn produce_proofs( + proof_of_time: ProofOfTime, + state: Arc, + proof_sender: Sender, + ) { + loop { + // Build the next proof on top of the latest tip. + let last_proof = state.tip().expect("Clock master chain cannot be empty"); + + // TODO: injected block hash from consensus + let start_ts = Instant::now(); + let next_slot_number = last_proof.slot_number + 1; + let next_seed = last_proof.next_seed(None); + let next_key = last_proof.next_key(); + let next_proof = proof_of_time.create( + next_seed, + next_key, + next_slot_number, + last_proof.injected_block_hash, + ); + let elapsed = start_ts.elapsed(); + trace!("clock_master::produce proofs: {next_proof}, time=[{elapsed:?}]"); + + // Store the new proof back into the chain and gossip to other clock masters. + if let Err(e) = state.on_proof(&next_proof) { + trace!("clock_master::produce proofs: failed to extend chain: {e:?}"); + continue; + } else if let Err(e) = proof_sender.blocking_send(next_proof.clone()) { + warn!("clock_master::produce proofs: send failed: {e:?}"); + return; + } + } + } + + /// Gossips the locally generated proof. + fn handle_local_proof(&self, proof: PotProof) { + self.gossip.gossip_message(proof.encode()); + } + + /// Handles the incoming gossip message. + fn handle_gossip_message(&self, state: &dyn PotProtocolState, sender: PeerId, proof: PotProof) { + let start_ts = Instant::now(); + let ret = state.on_proof_from_peer(sender, &proof); + let elapsed = start_ts.elapsed(); + + if let Err(err) = ret { + trace!("clock_master::on gossip: {err:?}, {sender}"); + } else { + trace!("clock_master::on gossip: {proof}, time=[{elapsed:?}], {sender}"); + self.gossip.gossip_message(proof.encode()); + } + } + + /// Builds/adds the bootstrap proof to the state. + fn add_bootstrap_proof(&self, params: &BootstrapParams) { + let proof = self.proof_of_time.create( + PotSeed::from_block_hash(params.genesis_hash), + params.key, + params.slot, + params.genesis_hash, + ); + let proofs = NonEmptyVec::new(vec![proof]).expect("Vec is non empty"); + self.pot_state.reset(proofs); + } +} diff --git a/crates/sc-proof-of-time/src/gossip.rs b/crates/sc-proof-of-time/src/gossip.rs new file mode 100644 index 0000000000..e3579b7d0b --- /dev/null +++ b/crates/sc-proof-of-time/src/gossip.rs @@ -0,0 +1,137 @@ +//! PoT gossip functionality. + +use futures::channel::mpsc::Receiver; +use futures::FutureExt; +use parity_scale_codec::Decode; +use parking_lot::{Mutex, RwLock}; +use sc_network::config::NonDefaultSetConfig; +use sc_network::PeerId; +use sc_network_gossip::{ + GossipEngine, MessageIntent, Syncing as GossipSyncing, TopicNotification, ValidationResult, + Validator, ValidatorContext, +}; +use sp_runtime::traits::{Block as BlockT, Hash as HashT, Header as HeaderT}; +use std::collections::HashSet; +use std::sync::Arc; +use subspace_core_primitives::crypto::blake2b_256_hash; +use subspace_core_primitives::PotProof; + +pub(crate) const GOSSIP_PROTOCOL: &str = "/subspace/subspace-proof-of-time"; + +type MessageHash = [u8; 32]; + +/// PoT gossip components. +#[derive(Clone)] +pub struct PotGossip { + engine: Arc>>, + validator: Arc, +} + +impl PotGossip { + /// Creates the gossip components. + pub fn new(network: Network, sync: Arc) -> Self + where + Network: sc_network_gossip::Network + Send + Sync + Clone + 'static, + GossipSync: GossipSyncing + 'static, + { + let validator = Arc::new(PotGossipValidator::new()); + let engine = Arc::new(Mutex::new(GossipEngine::new( + network, + sync, + GOSSIP_PROTOCOL, + validator.clone(), + None, + ))); + Self { engine, validator } + } + + /// Gossips the message to the network. + pub fn gossip_message(&self, message: Vec) { + self.validator.on_broadcast(&message); + self.engine + .lock() + .gossip_message(topic::(), message, false); + } + + /// Returns the receiver for the messages. + pub fn incoming_messages(&self) -> Receiver { + self.engine.lock().messages_for(topic::()) + } + + /// Waits for gossip engine to terminate. + pub async fn is_terminated(&self) { + let poll_fn = futures::future::poll_fn(|cx| self.engine.lock().poll_unpin(cx)); + poll_fn.await; + } +} + +/// Validator for gossiped messages +#[derive(Debug)] +struct PotGossipValidator { + pending: RwLock>, +} + +impl PotGossipValidator { + /// Creates the validator. + fn new() -> Self { + Self { + pending: RwLock::new(HashSet::new()), + } + } + + /// Called when the message is broadcast. + fn on_broadcast(&self, msg: &[u8]) { + let hash = blake2b_256_hash(msg); + let mut pending = self.pending.write(); + pending.insert(hash); + } +} + +impl Validator for PotGossipValidator { + fn validate( + &self, + _context: &mut dyn ValidatorContext, + _sender: &PeerId, + mut data: &[u8], + ) -> ValidationResult { + match PotProof::decode(&mut data) { + Ok(_) => ValidationResult::ProcessAndKeep(topic::()), + Err(_) => ValidationResult::Discard, + } + } + + fn message_expired<'a>(&'a self) -> Box bool + 'a> { + Box::new(move |_topic, data| { + let hash = blake2b_256_hash(data); + let pending = self.pending.read(); + !pending.contains(&hash) + }) + } + + fn message_allowed<'a>( + &'a self, + ) -> Box bool + 'a> { + Box::new(move |_who, _intent, _topic, data| { + let hash = blake2b_256_hash(data); + let mut pending = self.pending.write(); + if pending.contains(&hash) { + pending.remove(&hash); + true + } else { + false + } + }) + } +} + +/// PoT message topic. +fn topic() -> Block::Hash { + <::Hashing as HashT>::hash(b"subspace-proof-of-time-gossip") +} + +/// Returns the network configuration for PoT gossip. +pub fn pot_gossip_peers_set_config() -> NonDefaultSetConfig { + let mut cfg = NonDefaultSetConfig::new(GOSSIP_PROTOCOL.into(), 5 * 1024 * 1024); + cfg.allow_non_reserved(25, 25); + cfg +} diff --git a/crates/sc-proof-of-time/src/lib.rs b/crates/sc-proof-of-time/src/lib.rs index c3db72b400..0877760d65 100644 --- a/crates/sc-proof-of-time/src/lib.rs +++ b/crates/sc-proof-of-time/src/lib.rs @@ -1,9 +1,18 @@ //! Subspace proof of time implementation. +mod clock_master; +mod gossip; mod state_manager; +mod utils; +use crate::state_manager::PotProtocolState; use core::num::{NonZeroU32, NonZeroU8}; +use std::sync::Arc; use subspace_core_primitives::{BlockNumber, SlotNumber}; +use subspace_proof_of_time::ProofOfTime; + +pub use clock_master::{BootstrapParams, ClockMaster}; +pub use gossip::{pot_gossip_peers_set_config, PotGossip}; // TODO: change the fields that can't be zero to NonZero types. #[derive(Debug, Clone)] @@ -35,7 +44,8 @@ pub struct PotConfig { impl Default for PotConfig { fn default() -> Self { - // TODO: fill proper values + // TODO: fill proper values. These are set to produce + // approximately 1 proof/sec during testing. Self { randomness_update_interval_blocks: 18, injection_depth_blocks: 90, @@ -47,3 +57,12 @@ impl Default for PotConfig { } } } + +/// Components initialized during the new_partial() phase of set up. +pub struct PotComponents { + /// Proof of time implementation. + proof_of_time: ProofOfTime, + + /// Protocol state. + protocol_state: Arc, +} diff --git a/crates/sc-proof-of-time/src/utils.rs b/crates/sc-proof-of-time/src/utils.rs new file mode 100644 index 0000000000..e2d682d481 --- /dev/null +++ b/crates/sc-proof-of-time/src/utils.rs @@ -0,0 +1,59 @@ +//! Common utils. + +use sp_blockchain::{HeaderBackend, Info}; +use sp_consensus::SyncOracle; +use sp_consensus_subspace::digests::extract_pre_digest; +use sp_runtime::traits::{Block as BlockT, Zero}; +use std::sync::Arc; +use subspace_core_primitives::{NonEmptyVec, PotProof}; +use tracing::info; + +/// Helper to retrieve the PoT state from latest tip. +pub(crate) async fn get_consensus_tip_proofs( + client: Arc, + sync_oracle: Arc, + chain_info_fn: Arc Info + Send + Sync>, +) -> Result, String> +where + Block: BlockT, + Client: HeaderBackend, + SO: SyncOracle + Send + Sync + Clone + 'static, +{ + // Wait for sync to complete + let delay = tokio::time::Duration::from_secs(1); + info!("get_consensus_tip_proofs(): waiting for sync to complete ..."); + let info = loop { + while sync_oracle.is_major_syncing() { + tokio::time::sleep(delay).await; + } + + // Get the hdr of the best block hash + let info = (chain_info_fn)(); + if !info.best_number.is_zero() { + break info; + } + info!("get_consensus_tip_proofs(): chain_info: {info:?}, to retry ..."); + tokio::time::sleep(delay).await; + }; + + let header = client + .header(info.best_hash) + .map_err(|err| format!("get_consensus_tip_proofs(): failed to get hdr: {err:?}, {info:?}"))? + .ok_or(format!("get_consensus_tip_proofs(): missing hdr: {info:?}"))?; + + // Get the pre-digest from the block hdr + let _pre_digest = extract_pre_digest(&header).map_err(|err| { + format!("get_consensus_tip_proofs(): failed to get pre digest: {err:?}, {info:?}") + })?; + + // TODO: enable this after adding the proofs to pre-digest. + /* + info!( + "get_consensus_tip_proofs(): {info:?}, pre_digest: slot = {}, num_proofs = {}", + pre_digest.slot, + pre_digest.proof_of_time.len() + ); + NonEmptyVec::new(pre_digest.proof_of_time).map_err(|err| format!("{err:?}")) + */ + Err("TODO".to_string()) +} diff --git a/crates/subspace-proof-of-time/src/lib.rs b/crates/subspace-proof-of-time/src/lib.rs index 59de2a4a5a..a49348249a 100644 --- a/crates/subspace-proof-of-time/src/lib.rs +++ b/crates/subspace-proof-of-time/src/lib.rs @@ -35,6 +35,7 @@ pub enum PotVerificationError { } /// Wrapper for the low level AES primitives +#[derive(Clone)] pub struct ProofOfTime { /// Number of checkpoints per PoT. num_checkpoints: u8, From 61f1a76982d5499776c4582ae41249a1be08cf9e Mon Sep 17 00:00:00 2001 From: Rahul Subramaniyam Date: Wed, 26 Jul 2023 15:50:29 -0700 Subject: [PATCH 5/9] Add PoT client --- crates/sc-proof-of-time/src/lib.rs | 2 + crates/sc-proof-of-time/src/node_client.rs | 109 +++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 crates/sc-proof-of-time/src/node_client.rs diff --git a/crates/sc-proof-of-time/src/lib.rs b/crates/sc-proof-of-time/src/lib.rs index 0877760d65..206e996b33 100644 --- a/crates/sc-proof-of-time/src/lib.rs +++ b/crates/sc-proof-of-time/src/lib.rs @@ -2,6 +2,7 @@ mod clock_master; mod gossip; +mod node_client; mod state_manager; mod utils; @@ -13,6 +14,7 @@ use subspace_proof_of_time::ProofOfTime; pub use clock_master::{BootstrapParams, ClockMaster}; pub use gossip::{pot_gossip_peers_set_config, PotGossip}; +pub use node_client::PotClient; // TODO: change the fields that can't be zero to NonZero types. #[derive(Debug, Clone)] diff --git a/crates/sc-proof-of-time/src/node_client.rs b/crates/sc-proof-of-time/src/node_client.rs new file mode 100644 index 0000000000..c61f4d6294 --- /dev/null +++ b/crates/sc-proof-of-time/src/node_client.rs @@ -0,0 +1,109 @@ +//! Consensus node interface to the clock master network. + +use crate::gossip::PotGossip; +use crate::state_manager::PotProtocolState; +use crate::utils::get_consensus_tip_proofs; +use crate::PotComponents; +use futures::{FutureExt, StreamExt}; +use parity_scale_codec::Decode; +use sc_network::PeerId; +use sp_blockchain::{HeaderBackend, Info}; +use sp_consensus::SyncOracle; +use sp_runtime::traits::Block as BlockT; +use std::sync::Arc; +use std::time::Instant; +use subspace_core_primitives::PotProof; +use tracing::{error, trace}; + +/// The PoT client implementation +pub struct PotClient { + gossip: PotGossip, + pot_state: Arc, + client: Arc, + sync_oracle: Arc, + chain_info_fn: Arc Info + Send + Sync>, +} + +impl PotClient +where + Block: BlockT, + Client: HeaderBackend, + SO: SyncOracle + Send + Sync + Clone + 'static, +{ + /// Creates the PoT client instance. + pub fn new( + components: PotComponents, + gossip: PotGossip, + client: Arc, + sync_oracle: Arc, + chain_info_fn: Arc Info + Send + Sync>, + ) -> Self { + Self { + gossip, + pot_state: components.protocol_state, + client, + sync_oracle, + chain_info_fn, + } + } + + /// Starts the workers. + pub async fn run(self) { + // Wait for sync to complete, get the proof from the tip. + let proofs = match get_consensus_tip_proofs( + self.client.clone(), + self.sync_oracle.clone(), + self.chain_info_fn.clone(), + ) + .await + { + Ok(proofs) => proofs, + Err(err) => { + error!("PoT client: Failed to get initial proofs: {err:?}"); + return; + } + }; + self.pot_state.reset(proofs); + + // Filter out incoming messages without sender_id or that fail to decode. + let mut incoming_messages = Box::pin(self.gossip.incoming_messages().filter_map( + |notification| async move { + let mut ret = None; + if let Some(sender) = notification.sender { + if let Ok(msg) = PotProof::decode(&mut ¬ification.message[..]) { + ret = Some((sender, msg)) + } + } + ret + }, + )); + + loop { + futures::select! { + gossiped = incoming_messages.next().fuse() => { + if let Some((sender, proof)) = gossiped { + trace!("pot_client: got gossiped proof: {sender} => {proof}"); + self.handle_gossip_message(self.pot_state.as_ref(), sender, proof); + } + }, + _ = self.gossip.is_terminated().fuse() => { + error!("pot_client: gossip engine has terminated."); + return; + } + } + } + } + + /// Handles the incoming gossip message. + fn handle_gossip_message(&self, state: &dyn PotProtocolState, sender: PeerId, proof: PotProof) { + let start_ts = Instant::now(); + let ret = state.on_proof_from_peer(sender, &proof); + let elapsed = start_ts.elapsed(); + + if let Err(err) = ret { + trace!("pot_client::on gossip: {err:?}, {sender}"); + } else { + trace!("pot_client::on gossip: {proof}, time=[{elapsed:?}], {sender}"); + } + } +} From c7566bf2e275233ff55c2d5906879a0534961fe9 Mon Sep 17 00:00:00 2001 From: Rahul Subramaniyam Date: Wed, 26 Jul 2023 16:07:33 -0700 Subject: [PATCH 6/9] Add consensus interface --- crates/sc-proof-of-time/src/clock_master.rs | 2 +- crates/sc-proof-of-time/src/lib.rs | 62 +++- crates/sc-proof-of-time/src/node_client.rs | 2 +- crates/sc-proof-of-time/src/state_manager.rs | 306 ++++++++++++++++++- 4 files changed, 366 insertions(+), 6 deletions(-) diff --git a/crates/sc-proof-of-time/src/clock_master.rs b/crates/sc-proof-of-time/src/clock_master.rs index 8f87c7a81d..08c1812620 100644 --- a/crates/sc-proof-of-time/src/clock_master.rs +++ b/crates/sc-proof-of-time/src/clock_master.rs @@ -69,7 +69,7 @@ where /// to avoid that by using a Fn instead. Follow up with upstream /// to include this in the trait. pub fn new( - components: PotComponents, + components: PotComponents, gossip: PotGossip, client: Arc, sync_oracle: Arc, diff --git a/crates/sc-proof-of-time/src/lib.rs b/crates/sc-proof-of-time/src/lib.rs index 206e996b33..62a22f4d38 100644 --- a/crates/sc-proof-of-time/src/lib.rs +++ b/crates/sc-proof-of-time/src/lib.rs @@ -6,8 +6,9 @@ mod node_client; mod state_manager; mod utils; -use crate::state_manager::PotProtocolState; +use crate::state_manager::{init_pot_state, PotProtocolState}; use core::num::{NonZeroU32, NonZeroU8}; +use sp_runtime::traits::Block as BlockT; use std::sync::Arc; use subspace_core_primitives::{BlockNumber, SlotNumber}; use subspace_proof_of_time::ProofOfTime; @@ -15,6 +16,7 @@ use subspace_proof_of_time::ProofOfTime; pub use clock_master::{BootstrapParams, ClockMaster}; pub use gossip::{pot_gossip_peers_set_config, PotGossip}; pub use node_client::PotClient; +pub use state_manager::{PotConsensusState, PotStateSummary}; // TODO: change the fields that can't be zero to NonZero types. #[derive(Debug, Clone)] @@ -61,10 +63,66 @@ impl Default for PotConfig { } /// Components initialized during the new_partial() phase of set up. -pub struct PotComponents { +pub struct PotComponents { /// Proof of time implementation. proof_of_time: ProofOfTime, /// Protocol state. protocol_state: Arc, + + /// Consensus state. + consensus_state: Arc>, +} + +impl PotComponents { + /// Sets up the partial components. + pub fn new() -> Self { + let config = PotConfig::default(); + let proof_of_time = ProofOfTime::new(config.pot_iterations, config.num_checkpoints) + .expect("Failed to initialize proof of time"); + let (protocol_state, consensus_state) = + init_pot_state(config, proof_of_time.clone(), vec![]); + + Self { + proof_of_time, + protocol_state, + consensus_state, + } + } + + /// Returns the consensus interface. + pub fn consensus_state(&self) -> Arc> { + self.consensus_state.clone() + } +} + +impl Default for PotComponents { + fn default() -> Self { + Self::new() + } +} + +/// The role assigned to subspace-node. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum PotRole { + /// Clock master role of producing proofs + initial bootstrapping. + ClockMasterBootStrap, + + /// Clock master role of producing proofs. + ClockMaster, + + /// Consensus PoT client, listens for proofs from clock masters. + Client, +} + +impl PotRole { + /// Checks if the role is clock master. + pub fn is_clock_master(&self) -> bool { + *self == Self::ClockMasterBootStrap || *self == Self::ClockMaster + } + + /// Checks if the role is clock master bootstrap. + pub fn is_clock_master_bootstrap(&self) -> bool { + *self == Self::ClockMasterBootStrap + } } diff --git a/crates/sc-proof-of-time/src/node_client.rs b/crates/sc-proof-of-time/src/node_client.rs index c61f4d6294..3214b509eb 100644 --- a/crates/sc-proof-of-time/src/node_client.rs +++ b/crates/sc-proof-of-time/src/node_client.rs @@ -32,7 +32,7 @@ where { /// Creates the PoT client instance. pub fn new( - components: PotComponents, + components: PotComponents, gossip: PotGossip, client: Arc, sync_oracle: Arc, diff --git a/crates/sc-proof-of-time/src/state_manager.rs b/crates/sc-proof-of-time/src/state_manager.rs index 51cd79edd1..0869de2a22 100644 --- a/crates/sc-proof-of-time/src/state_manager.rs +++ b/crates/sc-proof-of-time/src/state_manager.rs @@ -3,12 +3,14 @@ use crate::PotConfig; use parking_lot::Mutex; use sc_network::PeerId; +use sp_runtime::traits::{Block as BlockT, NumberFor, One}; use std::collections::btree_map::Entry; use std::collections::BTreeMap; use std::sync::Arc; use subspace_core_primitives::{NonEmptyVec, PotKey, PotProof, PotSeed, SlotNumber}; use subspace_proof_of_time::{PotVerificationError, ProofOfTime}; +/// Error codes for PotProtocolState APIs. #[derive(Debug, thiserror::Error)] pub(crate) enum PotProtocolStateError { #[error("Failed to extend chain: {expected}/{actual}")] @@ -42,6 +44,85 @@ pub(crate) enum PotProtocolStateError { DuplicateProofFromPeer(PeerId), } +/// Error codes for PotConsensusState APIs. +#[derive(Debug, thiserror::Error)] +pub enum PotConsensusStateError { + #[error("Parent block proofs empty: {summary:?}/{slot_number}/{block_number}")] + ParentProofsEmpty { + summary: PotStateSummary, + slot_number: SlotNumber, + block_number: String, + }, + + #[error("Invalid slot range: {summary:?}/{slot_number}/{block_number}/{start_slot}")] + InvalidRange { + summary: PotStateSummary, + slot_number: SlotNumber, + block_number: String, + start_slot: SlotNumber, + }, + + #[error("Proof unavailable to send: {summary:?}/{slot_number}/{block_number}")] + ProofUnavailable { + summary: PotStateSummary, + slot_number: SlotNumber, + block_number: String, + }, + + #[error( + "Unexpected proof count: {summary:?}/{slot_number}/{block_number}/{expected}/{actual}" + )] + UnexpectedProofCount { + summary: PotStateSummary, + slot_number: SlotNumber, + block_number: String, + expected: usize, + actual: usize, + }, + + #[error("Received proof locally missing: {summary:?}/{slot_number}/{block_number}")] + ReceivedSlotMissing { + summary: PotStateSummary, + slot_number: SlotNumber, + block_number: String, + }, + + #[error("Received proof did not match local proof: {summary:?}/{slot_number}/{block_number}")] + ReceivedProofMismatch { + summary: PotStateSummary, + slot_number: SlotNumber, + block_number: String, + }, + + #[error("Received block with no proofs: {summary:?}/{slot_number}/{block_number}")] + ReceivedProofsEmpty { + summary: PotStateSummary, + slot_number: SlotNumber, + block_number: String, + }, + + #[error( + "Received proofs with unexpected slot number: {summary:?}/{slot_number}/{block_number}/{expected}/{actual}" + )] + ReceivedUnexpectedSlotNumber { + summary: PotStateSummary, + slot_number: SlotNumber, + block_number: String, + expected: SlotNumber, + actual: SlotNumber, + }, +} + +/// Summary of the current state. +#[derive(Debug, Clone)] +pub struct PotStateSummary { + /// Current tip. + pub tip: Option, + + /// Length of chain. + pub chain_length: usize, +} + /// The shared PoT state. struct InternalState { /// Last N entries of the PotChain, sorted by height. @@ -55,13 +136,22 @@ struct InternalState { future_proofs: BTreeMap>, } +impl InternalState { + fn summary(&self) -> PotStateSummary { + PotStateSummary { + tip: self.chain.iter().last().map(|proof| proof.slot_number), + chain_length: self.chain.len(), + } + } +} + /// Wrapper to manage the state. struct StateManager { /// Pot config config: PotConfig, /// PoT wrapper for verification. - proof_of_time: Arc, + proof_of_time: ProofOfTime, /// The PoT state state: Mutex, @@ -69,7 +159,7 @@ struct StateManager { impl StateManager { /// Creates the state. - pub fn new(config: PotConfig, proof_of_time: Arc, chain: Vec) -> Self { + pub fn new(config: PotConfig, proof_of_time: ProofOfTime, chain: Vec) -> Self { Self { config, proof_of_time, @@ -289,3 +379,215 @@ impl PotProtocolState for StateManager { self.verify_and_extend_chain(sender, proof) } } + +/// Interface to consensus. +pub trait PotConsensusState: Send + Sync { + /// Called by consensus when trying to claim the slot. + /// Returns the proofs in the slot range + /// [parent.last_proof.slot + 1, slot_number - global_randomness_reveal_lag_slots]. + fn get_block_proofs( + &self, + slot_number: SlotNumber, + block_number: NumberFor, + parent_block_proofs: &[PotProof], + ) -> Result, PotConsensusStateError>; + + /// Called during block import validation. + /// Verifies the sequence of proofs in the block being validated. + fn verify_block_proofs( + &self, + slot_number: SlotNumber, + block_number: NumberFor, + block_proofs: &[PotProof], + parent_block_proofs: &[PotProof], + ) -> Result<(), PotConsensusStateError>; +} + +impl PotConsensusState for StateManager { + fn get_block_proofs( + &self, + slot_number: SlotNumber, + block_number: NumberFor, + parent_block_proofs: &[PotProof], + ) -> Result, PotConsensusStateError> { + let state = self.state.lock(); + let summary = state.summary(); + let proof_slot = slot_number - self.config.global_randomness_reveal_lag_slots; + + // For block 1, just return one proof at the target slot, + // as the parent(genesis) does not have any proofs. + if block_number.is_one() { + let proof = state + .chain + .iter() + .find(|proof| proof.slot_number == proof_slot) + .ok_or(PotConsensusStateError::ProofUnavailable { + summary, + slot_number, + block_number: format!("{block_number}"), + })?; + return Ok(vec![proof.clone()]); + } + + let start_slot = parent_block_proofs + .iter() + .last() + .ok_or(PotConsensusStateError::ParentProofsEmpty { + summary: summary.clone(), + slot_number, + block_number: format!("{block_number}"), + })? + .slot_number + + 1; + + if start_slot > proof_slot { + return Err(PotConsensusStateError::InvalidRange { + summary: summary.clone(), + slot_number, + block_number: format!("{block_number}"), + start_slot, + }); + } + + // Collect the proofs in the requested range. + let mut proofs = Vec::with_capacity((proof_slot - start_slot + 1) as usize); + for slot in start_slot..=proof_slot { + // TODO: avoid repeated search by copying the range. + let proof = state + .chain + .iter() + .find(|proof| proof.slot_number == slot) + .ok_or(PotConsensusStateError::ProofUnavailable { + summary: summary.clone(), + slot_number: slot, + block_number: format!("{block_number}"), + })?; + proofs.push(proof.clone()); + } + + Ok(proofs) + } + + fn verify_block_proofs( + &self, + slot_number: SlotNumber, + block_number: NumberFor, + block_proofs: &[PotProof], + parent_block_proofs: &[PotProof], + ) -> Result<(), PotConsensusStateError> { + let state = self.state.lock(); + let summary = state.summary(); + + if block_number.is_one() { + // If block 1, check it has one proof. + // TODO: we currently don't have a way to check the slot number at the + // sender, to be resolved. + if block_proofs.len() != 1 { + return Err(PotConsensusStateError::UnexpectedProofCount { + summary, + slot_number, + block_number: format!("{block_number}"), + expected: 1, + actual: block_proofs.len(), + }); + } + + let received = &block_proofs[0]; // Safe to index. + let proof = state + .chain + .iter() + .find(|proof| proof.slot_number == received.slot_number) + .ok_or(PotConsensusStateError::ReceivedSlotMissing { + summary: summary.clone(), + slot_number: received.slot_number, + block_number: format!("{block_number}"), + })?; + // Safe to index. + if *proof != *received { + return Err(PotConsensusStateError::ReceivedProofMismatch { + summary: summary.clone(), + slot_number: received.slot_number, + block_number: format!("{block_number}"), + }); + } + + return Ok(()); + } + + // Check that the parent last proof and the block first proof + // form a chain. + let last_parent_proof = + parent_block_proofs + .iter() + .last() + .ok_or(PotConsensusStateError::ParentProofsEmpty { + summary: summary.clone(), + slot_number, + block_number: format!("{block_number}"), + })?; + let first_block_proof = + block_proofs + .get(0) + .ok_or(PotConsensusStateError::ReceivedProofsEmpty { + summary: summary.clone(), + slot_number, + block_number: format!("{block_number}"), + })?; + if first_block_proof.slot_number != (last_parent_proof.slot_number + 1) { + return Err(PotConsensusStateError::ReceivedUnexpectedSlotNumber { + summary: summary.clone(), + slot_number, + block_number: format!("{block_number}"), + expected: last_parent_proof.slot_number + 1, + actual: first_block_proof.slot_number, + }); + } + + // Compare the received proofs against the local chain. Since the + // local chain is already validated, not doing the AES check on the + // received proofs. + let mut expected_slot = first_block_proof.slot_number; + for received in block_proofs { + if received.slot_number != expected_slot { + return Err(PotConsensusStateError::ReceivedUnexpectedSlotNumber { + summary: summary.clone(), + slot_number, + block_number: format!("{block_number}"), + expected: expected_slot, + actual: received.slot_number, + }); + } + expected_slot += 1; + + // TODO: avoid repeated lookups, locate start of range + let local_proof = state + .chain + .iter() + .find(|local_proof| local_proof.slot_number == received.slot_number) + .ok_or(PotConsensusStateError::ReceivedSlotMissing { + summary: summary.clone(), + slot_number: received.slot_number, + block_number: format!("{block_number}"), + })?; + + if *local_proof != *received { + return Err(PotConsensusStateError::ReceivedProofMismatch { + summary: summary.clone(), + slot_number: received.slot_number, + block_number: format!("{block_number}"), + }); + } + } + + Ok(()) + } +} + +pub(crate) fn init_pot_state( + config: PotConfig, + proof_of_time: ProofOfTime, + chain: Vec, +) -> (Arc, Arc>) { + let state = Arc::new(StateManager::new(config, proof_of_time, chain)); + (state.clone(), state) +} From 13bdd475ec7c856663a5e597f320f1e7c88244ed Mon Sep 17 00:00:00 2001 From: Rahul Subramaniyam Date: Sat, 29 Jul 2023 18:32:55 -0700 Subject: [PATCH 7/9] Refactor handling of state --- crates/sc-proof-of-time/src/lib.rs | 3 +- crates/sc-proof-of-time/src/state_manager.rs | 333 ++++++++++--------- 2 files changed, 186 insertions(+), 150 deletions(-) diff --git a/crates/sc-proof-of-time/src/lib.rs b/crates/sc-proof-of-time/src/lib.rs index 62a22f4d38..8bec6c2568 100644 --- a/crates/sc-proof-of-time/src/lib.rs +++ b/crates/sc-proof-of-time/src/lib.rs @@ -80,8 +80,7 @@ impl PotComponents { let config = PotConfig::default(); let proof_of_time = ProofOfTime::new(config.pot_iterations, config.num_checkpoints) .expect("Failed to initialize proof of time"); - let (protocol_state, consensus_state) = - init_pot_state(config, proof_of_time.clone(), vec![]); + let (protocol_state, consensus_state) = init_pot_state(config, proof_of_time.clone()); Self { proof_of_time, diff --git a/crates/sc-proof-of-time/src/state_manager.rs b/crates/sc-proof-of-time/src/state_manager.rs index 0869de2a22..4f16bb39d6 100644 --- a/crates/sc-proof-of-time/src/state_manager.rs +++ b/crates/sc-proof-of-time/src/state_manager.rs @@ -125,6 +125,9 @@ pub struct PotStateSummary { /// The shared PoT state. struct InternalState { + /// Config. + config: PotConfig, + /// Last N entries of the PotChain, sorted by height. /// TODO: purging to be implemented. chain: Vec, @@ -137,53 +140,42 @@ struct InternalState { } impl InternalState { - fn summary(&self) -> PotStateSummary { - PotStateSummary { - tip: self.chain.iter().last().map(|proof| proof.slot_number), - chain_length: self.chain.len(), - } - } -} - -/// Wrapper to manage the state. -struct StateManager { - /// Pot config - config: PotConfig, - - /// PoT wrapper for verification. - proof_of_time: ProofOfTime, - - /// The PoT state - state: Mutex, -} - -impl StateManager { /// Creates the state. - pub fn new(config: PotConfig, proof_of_time: ProofOfTime, chain: Vec) -> Self { + fn new(config: PotConfig) -> Self { Self { config, - proof_of_time, - state: Mutex::new(InternalState { - chain, - future_proofs: BTreeMap::new(), - }), + chain: Vec::new(), + future_proofs: BTreeMap::new(), } } - /// Extends the chain with the given proof, without verifying it - /// (e.g) called when clock maker locally produces a proof. - pub fn extend_chain(&self, proof: &PotProof) -> Result<(), PotProtocolStateError> { - let mut state = self.state.lock(); - let tip = match state.chain.last() { + /// Re-initializes the state with the given chain. + fn reset(&mut self, proofs: NonEmptyVec) { + let mut proofs = proofs.to_vec(); + self.chain.clear(); + self.chain.append(&mut proofs); + self.future_proofs.clear(); + } + + /// Adds the proof to the current tip. + fn add_to_tip(&mut self, proof: &PotProof) { + self.chain.push(proof.clone()); + self.future_proofs.remove(&proof.slot_number); + self.merge_future_proofs(); + } + + /// Tries to extend the chain with the locally produced proof. + fn handle_local_proof(&mut self, proof: &PotProof) -> Result<(), PotProtocolStateError> { + let tip = match self.chain.last() { Some(tip) => tip, None => { - self.add_to_tip(&mut state, proof); + self.add_to_tip(proof); return Ok(()); } }; if (tip.slot_number + 1) == proof.slot_number { - self.add_to_tip(&mut state, proof); + self.add_to_tip(proof); Ok(()) } else { // The tip moved by the time the proof was computed. @@ -194,24 +186,17 @@ impl StateManager { } } - /// Extends the chain with the given proof, after verifying it - /// (e.g) called when the proof is received from a peer via gossip. - pub fn verify_and_extend_chain( - &self, + /// Tries to extend the chain with the proof received from a peer. + /// The proof is assumed to have passed the AES verification. + fn handle_peer_proof( + &mut self, sender: PeerId, proof: &PotProof, ) -> Result<(), PotProtocolStateError> { - // Verify the proof outside the lock. - // TODO: penalize peers that send too many bad proofs. - self.proof_of_time - .verify(proof) - .map_err(PotProtocolStateError::InvalidProof)?; - - let mut state = self.state.lock(); - let tip = match state.chain.last() { + let tip = match self.chain.last() { Some(tip) => tip.clone(), None => { - self.add_to_tip(&mut state, proof); + self.add_to_tip(proof); return Ok(()); } }; @@ -243,18 +228,17 @@ impl StateManager { } // All checks passed, advance the tip with the new proof - self.add_to_tip(&mut state, proof); + self.add_to_tip(proof); return Ok(()); } // Case 3: proof for a future slot - self.handle_future_proof(&mut state, &tip, sender, proof) + self.handle_future_proof(&tip, sender, proof) } /// Handles the received proof for a future slot. fn handle_future_proof( - &self, - state: &mut InternalState, + &mut self, tip: &PotProof, sender: PeerId, proof: &PotProof, @@ -267,7 +251,7 @@ impl StateManager { }); } - match state.future_proofs.entry(proof.slot_number) { + match self.future_proofs.entry(proof.slot_number) { Entry::Vacant(entry) => { let mut proofs = BTreeMap::new(); proofs.insert(sender, proof.clone()); @@ -291,15 +275,15 @@ impl StateManager { /// Called when the chain is extended with a new proof. /// Tries to advance the tip as much as possible, by merging with /// the pending future proofs. - fn merge_future_proofs(&self, state: &mut InternalState) { - let mut cur_tip = state.chain.last().cloned(); + fn merge_future_proofs(&mut self) { + let mut cur_tip = self.chain.last().cloned(); while let Some(tip) = cur_tip.as_ref() { // At this point, we know the expected seed/key for the next proof // in the sequence. If there is at least an entry with the expected // key/seed(there could be several from different peers), extend the // chain. let next_slot = tip.slot_number + 1; - let proofs_for_slot = match state.future_proofs.remove(&next_slot) { + let proofs_for_slot = match self.future_proofs.remove(&next_slot) { Some(proofs) => proofs, None => return, }; @@ -313,7 +297,7 @@ impl StateManager { { Some(next_proof) => { // Extend the tip with the next proof, continue merging. - state.chain.push(next_proof.clone()); + self.chain.push(next_proof.clone()); cur_tip = Some(next_proof); } None => { @@ -324,100 +308,20 @@ impl StateManager { } } - /// Adds the proof to the current tip - fn add_to_tip(&self, state: &mut InternalState, proof: &PotProof) { - state.chain.push(proof.clone()); - state.future_proofs.remove(&proof.slot_number); - self.merge_future_proofs(state); - } -} - -/// Interface to the internal protocol components (clock master, PoT client). -pub(crate) trait PotProtocolState: Send + Sync { - /// Re(initializes) the chain with the given set of proofs. - /// TODO: the proofs are assumed to have been validated, validate - /// if needed. - fn reset(&self, proofs: NonEmptyVec); - - /// Returns the current tip. - fn tip(&self) -> Option; - - /// Called when a proof is produced locally. It tries to extend the - /// chain without verifying the proof. - fn on_proof(&self, proof: &PotProof) -> Result<(), PotProtocolStateError>; - - /// Called when a proof is received via gossip from a peer. The proof - /// is first verified before trying to extend the chain. - fn on_proof_from_peer( - &self, - sender: PeerId, - proof: &PotProof, - ) -> Result<(), PotProtocolStateError>; -} - -impl PotProtocolState for StateManager { - fn reset(&self, proofs: NonEmptyVec) { - let mut proofs = proofs.to_vec(); - let mut state = self.state.lock(); - state.chain.clear(); - state.chain.append(&mut proofs); - state.future_proofs.clear(); - } - fn tip(&self) -> Option { - self.state.lock().chain.last().cloned() - } - - fn on_proof(&self, proof: &PotProof) -> Result<(), PotProtocolStateError> { - self.extend_chain(proof) - } - - fn on_proof_from_peer( - &self, - sender: PeerId, - proof: &PotProof, - ) -> Result<(), PotProtocolStateError> { - self.verify_and_extend_chain(sender, proof) - } -} - -/// Interface to consensus. -pub trait PotConsensusState: Send + Sync { - /// Called by consensus when trying to claim the slot. - /// Returns the proofs in the slot range - /// [parent.last_proof.slot + 1, slot_number - global_randomness_reveal_lag_slots]. - fn get_block_proofs( - &self, - slot_number: SlotNumber, - block_number: NumberFor, - parent_block_proofs: &[PotProof], - ) -> Result, PotConsensusStateError>; - - /// Called during block import validation. - /// Verifies the sequence of proofs in the block being validated. - fn verify_block_proofs( - &self, - slot_number: SlotNumber, - block_number: NumberFor, - block_proofs: &[PotProof], - parent_block_proofs: &[PotProof], - ) -> Result<(), PotConsensusStateError>; -} - -impl PotConsensusState for StateManager { - fn get_block_proofs( + /// Returns the proofs for the block. + fn get_block_proofs( &self, slot_number: SlotNumber, block_number: NumberFor, parent_block_proofs: &[PotProof], ) -> Result, PotConsensusStateError> { - let state = self.state.lock(); - let summary = state.summary(); + let summary = self.summary(); let proof_slot = slot_number - self.config.global_randomness_reveal_lag_slots; // For block 1, just return one proof at the target slot, // as the parent(genesis) does not have any proofs. if block_number.is_one() { - let proof = state + let proof = self .chain .iter() .find(|proof| proof.slot_number == proof_slot) @@ -453,7 +357,7 @@ impl PotConsensusState for StateManager { let mut proofs = Vec::with_capacity((proof_slot - start_slot + 1) as usize); for slot in start_slot..=proof_slot { // TODO: avoid repeated search by copying the range. - let proof = state + let proof = self .chain .iter() .find(|proof| proof.slot_number == slot) @@ -468,15 +372,15 @@ impl PotConsensusState for StateManager { Ok(proofs) } - fn verify_block_proofs( + /// Verifies the block proofs. + fn verify_block_proofs( &self, slot_number: SlotNumber, block_number: NumberFor, block_proofs: &[PotProof], parent_block_proofs: &[PotProof], ) -> Result<(), PotConsensusStateError> { - let state = self.state.lock(); - let summary = state.summary(); + let summary = self.summary(); if block_number.is_one() { // If block 1, check it has one proof. @@ -493,7 +397,7 @@ impl PotConsensusState for StateManager { } let received = &block_proofs[0]; // Safe to index. - let proof = state + let proof = self .chain .iter() .find(|proof| proof.slot_number == received.slot_number) @@ -560,7 +464,7 @@ impl PotConsensusState for StateManager { expected_slot += 1; // TODO: avoid repeated lookups, locate start of range - let local_proof = state + let local_proof = self .chain .iter() .find(|local_proof| local_proof.slot_number == received.slot_number) @@ -581,13 +485,146 @@ impl PotConsensusState for StateManager { Ok(()) } + + /// Returns the current tip of the chain. + fn tip(&self) -> Option { + self.chain.last().cloned() + } + + /// Returns the summary of the current state. + fn summary(&self) -> PotStateSummary { + PotStateSummary { + tip: self.chain.iter().last().map(|proof| proof.slot_number), + chain_length: self.chain.len(), + } + } +} + +/// Wrapper to manage the state. +struct StateManager { + /// The PoT state + state: Mutex, + + /// PoT wrapper for verification. + proof_of_time: ProofOfTime, +} + +impl StateManager { + /// Creates the state. + pub fn new(config: PotConfig, proof_of_time: ProofOfTime) -> Self { + Self { + state: Mutex::new(InternalState::new(config)), + proof_of_time, + } + } +} + +/// Interface to the internal protocol components (clock master, PoT client). +pub(crate) trait PotProtocolState: Send + Sync { + /// Re(initializes) the chain with the given set of proofs. + /// TODO: the proofs are assumed to have been validated, validate + /// if needed. + fn reset(&self, proofs: NonEmptyVec); + + /// Returns the current tip. + fn tip(&self) -> Option; + + /// Called when a proof is produced locally. It tries to extend the + /// chain without verifying the proof. + fn on_proof(&self, proof: &PotProof) -> Result<(), PotProtocolStateError>; + + /// Called when a proof is received via gossip from a peer. The proof + /// is first verified before trying to extend the chain. + fn on_proof_from_peer( + &self, + sender: PeerId, + proof: &PotProof, + ) -> Result<(), PotProtocolStateError>; +} + +impl PotProtocolState for StateManager { + fn reset(&self, proofs: NonEmptyVec) { + self.state.lock().reset(proofs); + } + + fn tip(&self) -> Option { + self.state.lock().tip() + } + + fn on_proof(&self, proof: &PotProof) -> Result<(), PotProtocolStateError> { + self.state.lock().handle_local_proof(proof) + } + + fn on_proof_from_peer( + &self, + sender: PeerId, + proof: &PotProof, + ) -> Result<(), PotProtocolStateError> { + // Verify the proof outside the lock. + // TODO: penalize peers that send too many bad proofs. + self.proof_of_time + .verify(proof) + .map_err(PotProtocolStateError::InvalidProof)?; + + self.state.lock().handle_peer_proof(sender, proof) + } +} + +/// Interface to consensus. +pub trait PotConsensusState: Send + Sync { + /// Called by consensus when trying to claim the slot. + /// Returns the proofs in the slot range + /// [parent.last_proof.slot + 1, slot_number - global_randomness_reveal_lag_slots]. + fn get_block_proofs( + &self, + slot_number: SlotNumber, + block_number: NumberFor, + parent_block_proofs: &[PotProof], + ) -> Result, PotConsensusStateError>; + + /// Called during block import validation. + /// Verifies the sequence of proofs in the block being validated. + fn verify_block_proofs( + &self, + slot_number: SlotNumber, + block_number: NumberFor, + block_proofs: &[PotProof], + parent_block_proofs: &[PotProof], + ) -> Result<(), PotConsensusStateError>; +} + +impl PotConsensusState for StateManager { + fn get_block_proofs( + &self, + slot_number: SlotNumber, + block_number: NumberFor, + parent_block_proofs: &[PotProof], + ) -> Result, PotConsensusStateError> { + self.state + .lock() + .get_block_proofs::(slot_number, block_number, parent_block_proofs) + } + + fn verify_block_proofs( + &self, + slot_number: SlotNumber, + block_number: NumberFor, + block_proofs: &[PotProof], + parent_block_proofs: &[PotProof], + ) -> Result<(), PotConsensusStateError> { + self.state.lock().verify_block_proofs::( + slot_number, + block_number, + block_proofs, + parent_block_proofs, + ) + } } pub(crate) fn init_pot_state( config: PotConfig, proof_of_time: ProofOfTime, - chain: Vec, ) -> (Arc, Arc>) { - let state = Arc::new(StateManager::new(config, proof_of_time, chain)); + let state = Arc::new(StateManager::new(config, proof_of_time)); (state.clone(), state) } From 005f8be84297c1266ee2df26702fb92bc10e1ddf Mon Sep 17 00:00:00 2001 From: Rahul Subramaniyam Date: Sun, 30 Jul 2023 22:17:57 -0700 Subject: [PATCH 8/9] Pass cloned copy to add_tip() --- crates/sc-proof-of-time/src/state_manager.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/sc-proof-of-time/src/state_manager.rs b/crates/sc-proof-of-time/src/state_manager.rs index 4f16bb39d6..63cf54f448 100644 --- a/crates/sc-proof-of-time/src/state_manager.rs +++ b/crates/sc-proof-of-time/src/state_manager.rs @@ -158,9 +158,9 @@ impl InternalState { } /// Adds the proof to the current tip. - fn add_to_tip(&mut self, proof: &PotProof) { - self.chain.push(proof.clone()); + fn add_to_tip(&mut self, proof: PotProof) { self.future_proofs.remove(&proof.slot_number); + self.chain.push(proof); self.merge_future_proofs(); } @@ -169,13 +169,13 @@ impl InternalState { let tip = match self.chain.last() { Some(tip) => tip, None => { - self.add_to_tip(proof); + self.add_to_tip(proof.clone()); return Ok(()); } }; if (tip.slot_number + 1) == proof.slot_number { - self.add_to_tip(proof); + self.add_to_tip(proof.clone()); Ok(()) } else { // The tip moved by the time the proof was computed. @@ -196,7 +196,7 @@ impl InternalState { let tip = match self.chain.last() { Some(tip) => tip.clone(), None => { - self.add_to_tip(proof); + self.add_to_tip(proof.clone()); return Ok(()); } }; @@ -228,7 +228,7 @@ impl InternalState { } // All checks passed, advance the tip with the new proof - self.add_to_tip(proof); + self.add_to_tip(proof.clone()); return Ok(()); } From 27e2d880a11215bb959524843482889d6bfb916c Mon Sep 17 00:00:00 2001 From: Nazar Mokrynskyi Date: Mon, 31 Jul 2023 14:55:18 +0300 Subject: [PATCH 9/9] Proper `.expect()` proofs and TODOs to do proper handling in other places --- crates/sc-proof-of-time/src/clock_master.rs | 4 +++- crates/sc-proof-of-time/src/lib.rs | 5 +++-- crates/subspace-proof-of-time/benches/pot.rs | 12 +++++------- crates/subspace-proof-of-time/src/lib.rs | 2 +- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/crates/sc-proof-of-time/src/clock_master.rs b/crates/sc-proof-of-time/src/clock_master.rs index 08c1812620..93f1c5c268 100644 --- a/crates/sc-proof-of-time/src/clock_master.rs +++ b/crates/sc-proof-of-time/src/clock_master.rs @@ -137,6 +137,7 @@ where .spawn(move || { Self::produce_proofs(proof_of_time, pot_state, local_proof_sender); }) + // TODO: Proper error handling or proof .expect("Failed to spawn PoT proof producer thread"); loop { @@ -172,6 +173,7 @@ where ) { loop { // Build the next proof on top of the latest tip. + // TODO: Proper error handling or proof let last_proof = state.tip().expect("Clock master chain cannot be empty"); // TODO: injected block hash from consensus @@ -226,7 +228,7 @@ where params.slot, params.genesis_hash, ); - let proofs = NonEmptyVec::new(vec![proof]).expect("Vec is non empty"); + let proofs = NonEmptyVec::new(vec![proof]).expect("Vec is non empty; qed"); self.pot_state.reset(proofs); } } diff --git a/crates/sc-proof-of-time/src/lib.rs b/crates/sc-proof-of-time/src/lib.rs index 8bec6c2568..0c2b505981 100644 --- a/crates/sc-proof-of-time/src/lib.rs +++ b/crates/sc-proof-of-time/src/lib.rs @@ -56,8 +56,8 @@ impl Default for PotConfig { global_randomness_reveal_lag_slots: 6, pot_injection_lag_slots: 6, max_future_slots: 10, - pot_iterations: NonZeroU32::new(16 * 200_000).expect("pot_iterations cannot be zero"), - num_checkpoints: NonZeroU8::new(16).expect("num_checkpoints cannot be zero"), + pot_iterations: NonZeroU32::new(16 * 200_000).expect("Not zero; qed"), + num_checkpoints: NonZeroU8::new(16).expect("Not zero; qed"), } } } @@ -79,6 +79,7 @@ impl PotComponents { pub fn new() -> Self { let config = PotConfig::default(); let proof_of_time = ProofOfTime::new(config.pot_iterations, config.num_checkpoints) + // TODO: Proper error handling or proof .expect("Failed to initialize proof of time"); let (protocol_state, consensus_state) = init_pot_state(config, proof_of_time.clone()); diff --git a/crates/subspace-proof-of-time/benches/pot.rs b/crates/subspace-proof-of-time/benches/pot.rs index aecc95495a..eaa32c8ba6 100644 --- a/crates/subspace-proof-of-time/benches/pot.rs +++ b/crates/subspace-proof-of-time/benches/pot.rs @@ -12,14 +12,12 @@ fn criterion_benchmark(c: &mut Criterion) { let slot_number = 1; let mut injected_block_hash = BlockHash::default(); thread_rng().fill(injected_block_hash.as_mut()); - let checkpoints_1 = NonZeroU8::new(1).expect("Creating checkpoints cannot fail"); - let checkpoints_8 = NonZeroU8::new(8).expect("Creating checkpoints cannot fail"); + let checkpoints_1 = NonZeroU8::new(1).expect("Not zero; qed"); + let checkpoints_8 = NonZeroU8::new(8).expect("Not zero; qed"); // About 1s on 5.5 GHz Raptor Lake CPU - let pot_iterations = NonZeroU32::new(166_000_000).expect("Creating pot_iterations cannot fail"); - let proof_of_time_sequential = ProofOfTime::new(pot_iterations, checkpoints_1) - .expect("Failed to create proof_of_time_sequential"); - let proof_of_time = - ProofOfTime::new(pot_iterations, checkpoints_8).expect("Failed to create proof_of_time"); + let pot_iterations = NonZeroU32::new(166_000_000).expect("Not zero; qed"); + let proof_of_time_sequential = ProofOfTime::new(pot_iterations, checkpoints_1).unwrap(); + let proof_of_time = ProofOfTime::new(pot_iterations, checkpoints_8).unwrap(); c.bench_function("prove/sequential", |b| { b.iter(|| { diff --git a/crates/subspace-proof-of-time/src/lib.rs b/crates/subspace-proof-of-time/src/lib.rs index a49348249a..e07d277d49 100644 --- a/crates/subspace-proof-of-time/src/lib.rs +++ b/crates/subspace-proof-of-time/src/lib.rs @@ -79,7 +79,7 @@ impl ProofOfTime { self.num_checkpoints, self.checkpoint_iterations, )) - .expect("Failed to create proof of time"); + .expect("List of checkpoints is never empty; qed"); PotProof::new(slot_number, seed, key, checkpoints, injected_block_hash) }