sc_consensus_subspace/
verifier.rs

1//! Stateless and parallelized block verification that happens before block is imported (except for locally produced
2//! blocks that are imported directly).
3//!
4//! The goal of verifier is to check internal consistency of the block, which includes things like
5//! solution according to claimed inputs, signature, Proof of Time checkpoints in justifications,
6//! etc.
7//!
8//! This should be the majority of the block verification computation such that all that is left for
9//! [`block_import`](crate::block_import) to check is that information in the block corresponds to
10//! the state of the parent block, which for the most part is comparing bytes against known good
11//! values.
12//!
13//! This is a significant tradeoff in the protocol: having a smaller header vs being able to verify
14//! a lot of things stateless and in parallel.
15
16use futures::lock::Mutex;
17use rand::prelude::*;
18use rayon::prelude::*;
19use sc_client_api::backend::AuxStore;
20use sc_consensus::block_import::BlockImportParams;
21use sc_consensus::import_queue::Verifier;
22use sc_consensus_slots::check_equivocation;
23use sc_proof_of_time::verifier::PotVerifier;
24use sc_telemetry::{telemetry, TelemetryHandle, CONSENSUS_TRACE};
25use schnorrkel::context::SigningContext;
26use sp_api::ProvideRuntimeApi;
27use sp_block_builder::BlockBuilder as BlockBuilderApi;
28use sp_blockchain::HeaderBackend;
29use sp_consensus::BlockOrigin;
30use sp_consensus_slots::Slot;
31use sp_consensus_subspace::digests::{
32    extract_subspace_digest_items, CompatibleDigestItem, PreDigest, SubspaceDigestItems,
33};
34use sp_consensus_subspace::{ChainConstants, PotNextSlotInput, SubspaceApi, SubspaceJustification};
35use sp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor};
36use sp_runtime::{DigestItem, Justifications};
37use std::iter;
38use std::marker::PhantomData;
39use std::num::NonZeroUsize;
40use std::sync::atomic::{AtomicU32, Ordering};
41use std::sync::Arc;
42use std::thread::available_parallelism;
43use subspace_core_primitives::{BlockNumber, PublicKey};
44use subspace_kzg::Kzg;
45use subspace_proof_of_space::Table;
46use subspace_verification::{check_reward_signature, verify_solution, VerifySolutionParams};
47use tokio::runtime::Handle;
48use tracing::{debug, info, trace, warn};
49
50/// Errors encountered by the Subspace verification task.
51#[derive(Debug, Eq, PartialEq, thiserror::Error)]
52pub enum VerificationError<Header: HeaderT> {
53    /// Header has a bad seal
54    #[error("Header {0:?} has a bad seal")]
55    HeaderBadSeal(Header::Hash),
56    /// Header is unsealed
57    #[error("Header {0:?} is unsealed")]
58    HeaderUnsealed(Header::Hash),
59    /// Bad reward signature
60    #[error("Bad reward signature on {0:?}")]
61    BadRewardSignature(Header::Hash),
62    /// Missing Subspace justification
63    #[error("Missing Subspace justification")]
64    MissingSubspaceJustification,
65    /// Invalid Subspace justification
66    #[error("Invalid Subspace justification: {0}")]
67    InvalidSubspaceJustification(parity_scale_codec::Error),
68    /// Invalid Subspace justification contents
69    #[error("Invalid Subspace justification contents")]
70    InvalidSubspaceJustificationContents,
71    /// Invalid proof of time
72    #[error("Invalid proof of time")]
73    InvalidProofOfTime,
74    /// Verification error
75    #[error("Verification error on slot {0:?}: {1:?}")]
76    VerificationError(Slot, subspace_verification::Error),
77}
78
79/// A header which has been checked
80struct CheckedHeader<H> {
81    /// A header which is fully checked, including signature. This is the pre-header accompanied by
82    /// the seal components.
83    ///
84    /// Includes the digest item that encoded the seal.
85    pre_header: H,
86    /// Pre-digest
87    pre_digest: PreDigest<PublicKey>,
88    /// Seal (signature)
89    seal: DigestItem,
90}
91
92/// Subspace verification parameters
93struct VerificationParams<'a, Header>
94where
95    Header: HeaderT + 'a,
96{
97    /// The header being verified.
98    header: Header,
99    /// Parameters for solution verification
100    verify_solution_params: &'a VerifySolutionParams,
101}
102
103/// Options for Subspace block verifier
104pub struct SubspaceVerifierOptions<Client> {
105    /// Substrate client
106    pub client: Arc<Client>,
107    /// Subspace chain constants
108    pub chain_constants: ChainConstants,
109    /// Kzg instance
110    pub kzg: Kzg,
111    /// Telemetry
112    pub telemetry: Option<TelemetryHandle>,
113    /// Context for reward signing
114    pub reward_signing_context: SigningContext,
115    /// Approximate target block number for syncing purposes
116    pub sync_target_block_number: Arc<AtomicU32>,
117    /// Whether this node is authoring blocks
118    pub is_authoring_blocks: bool,
119    /// Proof of time verifier
120    pub pot_verifier: PotVerifier,
121}
122
123/// A verifier for Subspace blocks.
124struct Inner<PosTable, Block, Client>
125where
126    Block: BlockT,
127{
128    client: Arc<Client>,
129    kzg: Kzg,
130    telemetry: Option<TelemetryHandle>,
131    chain_constants: ChainConstants,
132    reward_signing_context: SigningContext,
133    sync_target_block_number: Arc<AtomicU32>,
134    is_authoring_blocks: bool,
135    pot_verifier: PotVerifier,
136    equivocation_mutex: Mutex<()>,
137    _pos_table: PhantomData<PosTable>,
138    _block: PhantomData<Block>,
139}
140
141impl<PosTable, Block, Client> Inner<PosTable, Block, Client>
142where
143    PosTable: Table,
144    Block: BlockT,
145    BlockNumber: From<NumberFor<Block>>,
146    Client: HeaderBackend<Block> + ProvideRuntimeApi<Block> + AuxStore + 'static,
147    Client::Api: BlockBuilderApi<Block> + SubspaceApi<Block, PublicKey>,
148{
149    /// Create new instance
150    fn new(options: SubspaceVerifierOptions<Client>) -> Self {
151        let SubspaceVerifierOptions {
152            client,
153            chain_constants,
154            kzg,
155            telemetry,
156            reward_signing_context,
157            sync_target_block_number,
158            is_authoring_blocks,
159            pot_verifier,
160        } = options;
161
162        Self {
163            client,
164            kzg,
165            telemetry,
166            chain_constants,
167            reward_signing_context,
168            sync_target_block_number,
169            is_authoring_blocks,
170            pot_verifier,
171            equivocation_mutex: Mutex::default(),
172            _pos_table: Default::default(),
173            _block: Default::default(),
174        }
175    }
176
177    /// Determine if full proof of time verification is needed for this block number
178    fn full_pot_verification(&self, block_number: NumberFor<Block>) -> bool {
179        let sync_target_block_number: BlockNumber =
180            self.sync_target_block_number.load(Ordering::Relaxed);
181        let Some(diff) = sync_target_block_number.checked_sub(BlockNumber::from(block_number))
182        else {
183            return true;
184        };
185
186        let sample_size = match diff {
187            ..=1_581 => {
188                return true;
189            }
190            1_582..=6_234 => 1_581,
191            6_235..=63_240 => 3_162 * (diff - 3_162) / (diff - 1),
192            63_241..=3_162_000 => 3_162,
193            _ => diff / 1_000,
194        };
195
196        let n = thread_rng().gen_range(0..=diff);
197
198        n < sample_size
199    }
200
201    /// Check a header has been signed correctly and whether solution is correct. If the slot is too
202    /// far in the future, an error will be returned. If successful, returns the pre-header and the
203    /// digest item containing the seal.
204    ///
205    /// The seal must be the last digest. Otherwise, the whole header is considered unsigned. This
206    /// is required for security and must not be changed.
207    ///
208    /// This digest item will always return `Some` when used with `as_subspace_pre_digest`.
209    ///
210    /// `pre_digest` argument is optional in case it is available to avoid doing the work of
211    /// extracting it from the header twice.
212    fn check_header(
213        &self,
214        params: VerificationParams<'_, Block::Header>,
215        subspace_digest_items: SubspaceDigestItems<PublicKey>,
216        full_pot_verification: bool,
217        justifications: &Option<Justifications>,
218    ) -> Result<CheckedHeader<Block::Header>, VerificationError<Block::Header>> {
219        let VerificationParams {
220            mut header,
221            verify_solution_params,
222        } = params;
223
224        let pre_digest = subspace_digest_items.pre_digest;
225        let slot = pre_digest.slot();
226
227        let seal = header
228            .digest_mut()
229            .pop()
230            .ok_or_else(|| VerificationError::HeaderUnsealed(header.hash()))?;
231
232        let signature = seal
233            .as_subspace_seal()
234            .ok_or_else(|| VerificationError::HeaderBadSeal(header.hash()))?;
235
236        // The pre-hash of the header doesn't include the seal and that's what we sign
237        let pre_hash = header.hash();
238
239        // With justifications we can verify PoT checkpoints quickly and efficiently, the only check
240        // that will remain is to ensure that seed and number of iterations (inputs) is correct
241        // during block import.
242        {
243            let Some(subspace_justification) = justifications
244                .as_ref()
245                .and_then(|justifications| {
246                    justifications
247                        .iter()
248                        .find_map(SubspaceJustification::try_from_justification)
249                })
250                .transpose()
251                .map_err(VerificationError::InvalidSubspaceJustification)?
252            else {
253                return Err(VerificationError::MissingSubspaceJustification);
254            };
255
256            let SubspaceJustification::PotCheckpoints { seed, checkpoints } =
257                subspace_justification;
258
259            // Last checkpoint must be our future proof of time, this is how we anchor the rest of
260            // checks together
261            if checkpoints.last().map(|checkpoints| checkpoints.output())
262                != Some(pre_digest.pot_info().future_proof_of_time())
263            {
264                return Err(VerificationError::InvalidSubspaceJustificationContents);
265            }
266
267            let future_slot = slot + self.chain_constants.block_authoring_delay();
268            let first_slot_to_check = Slot::from(
269                future_slot
270                    .checked_sub(checkpoints.len() as u64 - 1)
271                    .ok_or(VerificationError::InvalidProofOfTime)?,
272            );
273            let slot_iterations = subspace_digest_items
274                .pot_parameters_change
275                .as_ref()
276                .and_then(|parameters_change| {
277                    (parameters_change.slot <= first_slot_to_check)
278                        .then_some(parameters_change.slot_iterations)
279                })
280                .unwrap_or(subspace_digest_items.pot_slot_iterations);
281
282            let mut pot_input = PotNextSlotInput {
283                slot: first_slot_to_check,
284                slot_iterations,
285                seed,
286            };
287            // Collect all the data we will use for verification so we can process it in parallel
288            let checkpoints_verification_input = iter::once((
289                pot_input,
290                *checkpoints
291                    .first()
292                    .expect("Not empty, contents was checked above; qed"),
293            ));
294            let checkpoints_verification_input = checkpoints_verification_input
295                .chain(checkpoints.windows(2).map(|checkpoints_pair| {
296                    pot_input = PotNextSlotInput::derive(
297                        pot_input.slot_iterations,
298                        pot_input.slot,
299                        checkpoints_pair[0].output(),
300                        &subspace_digest_items.pot_parameters_change,
301                    );
302
303                    (pot_input, checkpoints_pair[1])
304                }))
305                .collect::<Vec<_>>();
306
307            // All checkpoints must be valid, at least according to the seed included in
308            // justifications, search for the first error
309            let pot_verifier = &self.pot_verifier;
310            checkpoints_verification_input
311                .into_par_iter()
312                .find_map_first(|(pot_input, checkpoints)| {
313                    if full_pot_verification {
314                        // Try to find invalid checkpoints
315                        if !pot_verifier.verify_checkpoints(
316                            pot_input.seed,
317                            pot_input.slot_iterations,
318                            &checkpoints,
319                        ) {
320                            return Some(VerificationError::InvalidProofOfTime);
321                        }
322                    } else {
323                        // We inject verified checkpoints in order to avoid full proving when votes
324                        // included in the block will inevitably be verified during block execution
325                        pot_verifier.inject_verified_checkpoints(
326                            pot_input.seed,
327                            pot_input.slot_iterations,
328                            checkpoints,
329                        );
330                    }
331
332                    // We search for errors
333                    None
334                })
335                .map_or(Ok(()), Err)?;
336        }
337
338        // Verify that block is signed properly
339        if check_reward_signature(
340            pre_hash.as_ref(),
341            &signature,
342            &pre_digest.solution().public_key,
343            &self.reward_signing_context,
344        )
345        .is_err()
346        {
347            return Err(VerificationError::BadRewardSignature(pre_hash));
348        }
349
350        // Verify that solution is valid
351        verify_solution::<PosTable, _>(
352            pre_digest.solution(),
353            slot.into(),
354            verify_solution_params,
355            &self.kzg,
356        )
357        .map_err(|error| VerificationError::VerificationError(slot, error))?;
358
359        Ok(CheckedHeader {
360            pre_header: header,
361            pre_digest,
362            seal,
363        })
364    }
365
366    async fn check_and_report_equivocation(
367        &self,
368        slot_now: Slot,
369        slot: Slot,
370        header: &Block::Header,
371        author: &PublicKey,
372        origin: &BlockOrigin,
373    ) -> Result<(), String> {
374        // don't report any equivocations during initial sync
375        // as they are most likely stale.
376        if *origin == BlockOrigin::NetworkInitialSync {
377            return Ok(());
378        }
379
380        // Equivocation verification uses `AuxStore` in a way that is not safe from concurrency,
381        // this lock ensures that we process one header at a time
382        let _guard = self.equivocation_mutex.lock().await;
383
384        // check if authorship of this header is an equivocation and return a proof if so.
385        let equivocation_proof =
386            match check_equivocation(&*self.client, slot_now, slot, header, author)
387                .map_err(|error| error.to_string())?
388            {
389                Some(proof) => proof,
390                None => return Ok(()),
391            };
392
393        info!(
394            "Slot author {:?} is equivocating at slot {} with headers {:?} and {:?}",
395            author,
396            slot,
397            equivocation_proof.first_header.hash(),
398            equivocation_proof.second_header.hash(),
399        );
400
401        if self.is_authoring_blocks {
402            // TODO: Handle equivocation
403        } else {
404            info!("Not submitting equivocation report because node is not authoring blocks");
405        }
406
407        Ok(())
408    }
409
410    async fn verify(
411        &self,
412        mut block: BlockImportParams<Block>,
413    ) -> Result<BlockImportParams<Block>, String> {
414        trace!(
415            origin = ?block.origin,
416            header = ?block.header,
417            justifications = ?block.justifications,
418            body = ?block.body,
419            "Verifying",
420        );
421
422        let best_number = self.client.info().best_number;
423        // Reject block below archiving point, but only if we received it from the network
424        if *block.header.number() + self.chain_constants.confirmation_depth_k().into() < best_number
425            && matches!(block.origin, BlockOrigin::NetworkBroadcast)
426        {
427            debug!(
428                header = ?block.header,
429                %best_number,
430                "Rejecting block below archiving point"
431            );
432
433            return Err(format!(
434                "Rejecting block #{} below archiving point",
435                block.header.number()
436            ));
437        }
438
439        let hash = block.header.hash();
440
441        debug!(
442            "We have {:?} logs in this header",
443            block.header.digest().logs().len()
444        );
445
446        let subspace_digest_items =
447            extract_subspace_digest_items::<Block::Header, PublicKey>(&block.header)?;
448
449        let full_pot_verification = self.full_pot_verification(*block.header.number());
450
451        // Stateless header verification only. This means only check that header contains required
452        // contents, correct signature and valid Proof-of-Space, but because previous block is not
453        // guaranteed to be imported at this point, it is not possible to verify
454        // Proof-of-Archival-Storage. In order to verify PoAS randomness and solution range
455        // from the header are checked against expected correct values during block import as well
456        // as whether piece in the header corresponds to the actual archival history of the
457        // blockchain.
458        let checked_header = self
459            .check_header(
460                VerificationParams {
461                    header: block.header.clone(),
462                    verify_solution_params: &VerifySolutionParams {
463                        proof_of_time: subspace_digest_items.pre_digest.pot_info().proof_of_time(),
464                        solution_range: subspace_digest_items.solution_range,
465                        piece_check_params: None,
466                    },
467                },
468                subspace_digest_items,
469                full_pot_verification,
470                &block.justifications,
471            )
472            .map_err(|error| error.to_string())?;
473
474        let CheckedHeader {
475            pre_header,
476            pre_digest,
477            seal,
478        } = checked_header;
479
480        let slot = pre_digest.slot();
481        // Estimate what the "current" slot is according to sync target since we don't have other
482        // way to know it
483        let diff_in_blocks = self
484            .sync_target_block_number
485            .load(Ordering::Relaxed)
486            .saturating_sub(BlockNumber::from(*pre_header.number()));
487        let slot_now = if diff_in_blocks > 0 {
488            slot + Slot::from(
489                u64::from(diff_in_blocks) * self.chain_constants.slot_probability().1
490                    / self.chain_constants.slot_probability().0,
491            )
492        } else {
493            slot
494        };
495
496        // the header is valid but let's check if there was something else already proposed at the
497        // same slot by the given author. if there was, we will report the equivocation to the
498        // runtime.
499        if let Err(error) = self
500            .check_and_report_equivocation(
501                slot_now,
502                slot,
503                &block.header,
504                &pre_digest.solution().public_key,
505                &block.origin,
506            )
507            .await
508        {
509            warn!(
510                %error,
511                "Error checking/reporting Subspace equivocation"
512            );
513        }
514
515        trace!(?pre_header, "Checked header; importing");
516        telemetry!(
517            self.telemetry;
518            CONSENSUS_TRACE;
519            "subspace.checked_and_importing";
520            "pre_header" => ?pre_header,
521        );
522
523        block.header = pre_header;
524        block.post_digests.push(seal);
525        block.post_hash = Some(hash);
526
527        Ok(block)
528    }
529}
530
531/// A verifier for Subspace blocks.
532pub struct SubspaceVerifier<PosTable, Block, Client>
533where
534    Block: BlockT,
535{
536    inner: Arc<Inner<PosTable, Block, Client>>,
537}
538
539impl<PosTable, Block, Client> SubspaceVerifier<PosTable, Block, Client>
540where
541    PosTable: Table,
542    Block: BlockT,
543    BlockNumber: From<NumberFor<Block>>,
544    Client: HeaderBackend<Block> + ProvideRuntimeApi<Block> + AuxStore + 'static,
545    Client::Api: BlockBuilderApi<Block> + SubspaceApi<Block, PublicKey>,
546{
547    /// Create new instance
548    pub fn new(options: SubspaceVerifierOptions<Client>) -> Self {
549        Self {
550            inner: Arc::new(Inner::new(options)),
551        }
552    }
553}
554
555#[async_trait::async_trait]
556impl<PosTable, Block, Client> Verifier<Block> for SubspaceVerifier<PosTable, Block, Client>
557where
558    PosTable: Table,
559    Block: BlockT,
560    BlockNumber: From<NumberFor<Block>>,
561    Client: HeaderBackend<Block> + ProvideRuntimeApi<Block> + AuxStore + 'static,
562    Client::Api: BlockBuilderApi<Block> + SubspaceApi<Block, PublicKey>,
563{
564    fn verification_concurrency(&self) -> NonZeroUsize {
565        available_parallelism().unwrap_or(NonZeroUsize::new(1).expect("Not zero; qed"))
566    }
567
568    async fn verify(
569        &self,
570        block: BlockImportParams<Block>,
571    ) -> Result<BlockImportParams<Block>, String> {
572        let inner = Arc::clone(&self.inner);
573        tokio::task::spawn_blocking(move || Handle::current().block_on(inner.verify(block)))
574            .await
575            .map_err(|error| format!("Failed to join block verification task: {error}"))?
576    }
577}