Skip to main content

domain_block_preprocessor/
lib.rs

1//! This crate provides a preprocessor for the domain block, which is used to construct
2//! domain extrinsics from the consensus block.
3//!
4//! The workflow is as follows:
5//! 1. Extract domain-specific bundles from the consensus block.
6//! 2. Compile the domain bundles into a list of extrinsics.
7//! 3. Shuffle the extrisnics using the seed from the consensus chain.
8//! 4. Filter out the invalid xdm extrinsics.
9//! 5. Push back the potential new domain runtime extrisnic.
10
11#![warn(rust_2018_idioms)]
12
13pub mod inherents;
14pub mod stateless_runtime;
15
16use crate::inherents::is_runtime_upgraded;
17use crate::stateless_runtime::StatelessRuntime;
18use domain_runtime_primitives::opaque::AccountId;
19use domain_runtime_primitives::{CheckExtrinsicsValidityError, opaque};
20use parity_scale_codec::Encode;
21use sc_client_api::{BlockBackend, backend};
22use sc_executor::RuntimeVersionOf;
23use sp_api::{ApiError, ProvideRuntimeApi};
24use sp_blockchain::HeaderBackend;
25use sp_core::H256;
26use sp_core::traits::{CodeExecutor, FetchRuntimeCode};
27use sp_domains::bundle::{InboxedBundle, InvalidBundleType, OpaqueBundle, OpaqueBundles};
28use sp_domains::core_api::DomainCoreApi;
29use sp_domains::execution_receipt::ExecutionReceipt;
30use sp_domains::extrinsics::deduplicate_and_shuffle_extrinsics;
31use sp_domains::{DomainId, DomainsApi, ExtrinsicDigest, HeaderHashingFor, ReceiptValidity};
32use sp_messenger::MessengerApi;
33use sp_mmr_primitives::MmrApi;
34use sp_runtime::traits::{Block as BlockT, Hash as HashT, NumberFor};
35use sp_state_machine::LayoutV1;
36use sp_state_machine::backend::AsTrieBackend;
37use sp_subspace_mmr::ConsensusChainMmrLeafProof;
38use std::collections::VecDeque;
39use std::marker::PhantomData;
40use std::sync::Arc;
41use subspace_core_primitives::{Randomness, U256};
42use subspace_runtime_primitives::{Balance, ExtrinsicFor};
43
44type DomainBlockElements<CBlock> = (Vec<ExtrinsicFor<CBlock>>, Randomness);
45
46/// A wrapper indicating a valid bundle contents, or an invalid bundle reason.
47enum BundleValidity<Extrinsic> {
48    /// A valid bundle contents with signer of each extrinsic.
49    ValidWithSigner(Vec<(Option<opaque::AccountId>, Extrinsic)>),
50    /// An invalid bundle reason.
51    Invalid(InvalidBundleType),
52}
53
54/// Extracts the raw materials for building a new domain block from the primary block.
55fn prepare_domain_block_elements<Block, CBlock, CClient>(
56    consensus_client: &CClient,
57    block_hash: CBlock::Hash,
58) -> sp_blockchain::Result<DomainBlockElements<CBlock>>
59where
60    Block: BlockT,
61    CBlock: BlockT,
62    CClient: HeaderBackend<CBlock> + BlockBackend<CBlock> + ProvideRuntimeApi<CBlock> + Send + Sync,
63    CClient::Api: DomainsApi<CBlock, Block::Header>,
64{
65    let extrinsics = consensus_client.block_body(block_hash)?.ok_or_else(|| {
66        sp_blockchain::Error::Backend(format!("BlockBody of {block_hash:?} unavailable"))
67    })?;
68
69    let shuffling_seed = consensus_client
70        .runtime_api()
71        .extrinsics_shuffling_seed(block_hash)?;
72
73    Ok((extrinsics, shuffling_seed))
74}
75
76pub struct PreprocessResult<Block: BlockT> {
77    pub extrinsics: VecDeque<Block::Extrinsic>,
78    pub bundles: Vec<InboxedBundle<Block::Hash>>,
79}
80
81pub struct DomainBlockPreprocessor<Block, CBlock, Client, CClient, Exec, Backend, ReceiptValidator>
82{
83    domain_id: DomainId,
84    client: Arc<Client>,
85    consensus_client: Arc<CClient>,
86    executor: Arc<Exec>,
87    backend: Arc<Backend>,
88    receipt_validator: ReceiptValidator,
89    _phantom_data: PhantomData<(Block, CBlock)>,
90}
91
92impl<Block, CBlock, Client, CClient, Exec, Backend, ReceiptValidator: Clone> Clone
93    for DomainBlockPreprocessor<Block, CBlock, Client, CClient, Exec, Backend, ReceiptValidator>
94{
95    fn clone(&self) -> Self {
96        Self {
97            domain_id: self.domain_id,
98            client: self.client.clone(),
99            consensus_client: self.consensus_client.clone(),
100            executor: self.executor.clone(),
101            backend: self.backend.clone(),
102            receipt_validator: self.receipt_validator.clone(),
103            _phantom_data: self._phantom_data,
104        }
105    }
106}
107
108pub trait ValidateReceipt<Block, CBlock>
109where
110    Block: BlockT,
111    CBlock: BlockT,
112{
113    fn validate_receipt(
114        &self,
115        receipt: &ExecutionReceipt<
116            NumberFor<CBlock>,
117            CBlock::Hash,
118            NumberFor<Block>,
119            Block::Hash,
120            Balance,
121        >,
122    ) -> sp_blockchain::Result<ReceiptValidity>;
123}
124
125impl<Block, CBlock, Client, CClient, Exec, Backend, ReceiptValidator>
126    DomainBlockPreprocessor<Block, CBlock, Client, CClient, Exec, Backend, ReceiptValidator>
127where
128    Block: BlockT,
129    Block::Hash: Into<H256>,
130    CBlock: BlockT,
131    CBlock::Hash: From<Block::Hash>,
132    NumberFor<CBlock>: From<NumberFor<Block>>,
133    Client: HeaderBackend<Block> + ProvideRuntimeApi<Block> + 'static,
134    Client::Api: DomainCoreApi<Block> + MessengerApi<Block, NumberFor<CBlock>, CBlock::Hash>,
135    CClient: HeaderBackend<CBlock>
136        + BlockBackend<CBlock>
137        + ProvideRuntimeApi<CBlock>
138        + Send
139        + Sync
140        + 'static,
141    CClient::Api: DomainsApi<CBlock, Block::Header>
142        + MessengerApi<CBlock, NumberFor<CBlock>, CBlock::Hash>
143        + MmrApi<CBlock, H256, NumberFor<CBlock>>,
144    Backend: backend::Backend<Block>,
145    Exec: CodeExecutor + RuntimeVersionOf,
146    ReceiptValidator: ValidateReceipt<Block, CBlock>,
147{
148    pub fn new(
149        domain_id: DomainId,
150        client: Arc<Client>,
151        consensus_client: Arc<CClient>,
152        executor: Arc<Exec>,
153        backend: Arc<Backend>,
154        receipt_validator: ReceiptValidator,
155    ) -> Self {
156        Self {
157            domain_id,
158            client,
159            consensus_client,
160            executor,
161            backend,
162            receipt_validator,
163            _phantom_data: Default::default(),
164        }
165    }
166
167    pub fn preprocess_consensus_block(
168        &self,
169        consensus_block_hash: CBlock::Hash,
170        parent_domain_block: (Block::Hash, NumberFor<Block>),
171    ) -> sp_blockchain::Result<Option<PreprocessResult<Block>>> {
172        let (primary_extrinsics, shuffling_seed) = prepare_domain_block_elements::<Block, CBlock, _>(
173            &*self.consensus_client,
174            consensus_block_hash,
175        )?;
176
177        let runtime_api = self.consensus_client.runtime_api();
178
179        let bundles = runtime_api.extract_successful_bundles(
180            consensus_block_hash,
181            self.domain_id,
182            primary_extrinsics,
183        )?;
184
185        if bundles.is_empty()
186            && !is_runtime_upgraded::<_, _, Block>(
187                &self.consensus_client,
188                consensus_block_hash,
189                self.domain_id,
190            )?
191        {
192            return Ok(None);
193        }
194
195        let tx_range = self
196            .consensus_client
197            .runtime_api()
198            .domain_tx_range(consensus_block_hash, self.domain_id)?;
199
200        let (inboxed_bundles, extrinsics) = self.compile_bundles_to_extrinsics(
201            bundles,
202            tx_range,
203            parent_domain_block,
204            consensus_block_hash,
205        )?;
206
207        let extrinsics =
208            deduplicate_and_shuffle_extrinsics::<Block::Extrinsic>(extrinsics, shuffling_seed);
209
210        Ok(Some(PreprocessResult {
211            extrinsics,
212            bundles: inboxed_bundles,
213        }))
214    }
215
216    /// Filter out the invalid bundles first and then convert the remaining valid ones to
217    /// a list of extrinsics.
218    #[allow(clippy::type_complexity)]
219    fn compile_bundles_to_extrinsics(
220        &self,
221        bundles: OpaqueBundles<CBlock, Block::Header, Balance>,
222        tx_range: U256,
223        (parent_domain_hash, parent_domain_number): (Block::Hash, NumberFor<Block>),
224        at_consensus_hash: CBlock::Hash,
225    ) -> sp_blockchain::Result<(
226        Vec<InboxedBundle<Block::Hash>>,
227        Vec<(Option<AccountId>, Block::Extrinsic)>,
228    )> {
229        let mut inboxed_bundles = Vec::with_capacity(bundles.len());
230        let mut valid_extrinsics = Vec::new();
231
232        for bundle in bundles {
233            // For the honest operator the validity of the extrinsic of the bundle is committed
234            // to (or say verified against) the receipt that is submitted with the bundle, the
235            // consensus runtime should only accept the bundle if the receipt is derived from
236            // the parent domain block. If it is not then either there is a bug in the consensus
237            // runtime (for validating the bundle) or in the domain client (for finding the parent
238            // domain block).
239            //
240            // NOTE: The receipt's `domain_block_number` is verified by the consensus runtime while
241            // the `domain_block_hash` is not (which is take care of by the fraud proof) so we can't
242            // check the parent domain block hash here.
243            if *bundle.receipt_domain_block_number() != parent_domain_number {
244                return Err(sp_blockchain::Error::RuntimeApiError(
245                    ApiError::Application(
246                        format!(
247                            "Unexpected bundle in consensus block: {at_consensus_hash:?}, something must be wrong"
248                        )
249                        .into(),
250                    ),
251                ));
252            }
253
254            let extrinsic_root = bundle.extrinsics_root();
255            let bundle_validity = self.batch_check_bundle_validity(
256                bundle,
257                &tx_range,
258                (parent_domain_hash, parent_domain_number),
259                at_consensus_hash,
260            )?;
261            match bundle_validity {
262                BundleValidity::ValidWithSigner(signer_and_extrinsics) => {
263                    let bundle_digest: Vec<_> = signer_and_extrinsics
264                        .iter()
265                        .map(|(signer, tx)| {
266                            (
267                                signer.clone(),
268                                ExtrinsicDigest::new::<LayoutV1<HeaderHashingFor<Block::Header>>>(
269                                    tx.encode(),
270                                ),
271                            )
272                        })
273                        .collect();
274                    inboxed_bundles.push(InboxedBundle::valid(
275                        HeaderHashingFor::<Block::Header>::hash_of(&bundle_digest),
276                        extrinsic_root,
277                    ));
278                    valid_extrinsics.extend(signer_and_extrinsics);
279                }
280                BundleValidity::Invalid(invalid_bundle_type) => {
281                    inboxed_bundles
282                        .push(InboxedBundle::invalid(invalid_bundle_type, extrinsic_root));
283                }
284            }
285        }
286
287        Ok((inboxed_bundles, valid_extrinsics))
288    }
289
290    fn stateless_runtime_api(
291        &self,
292        parent_domain_hash: Block::Hash,
293    ) -> sp_blockchain::Result<StatelessRuntime<CBlock, Block, Exec>> {
294        // Trusted: bounded bundle-validation context
295        let state = self
296            .backend
297            .state_at(parent_domain_hash, sc_client_api::TrieCacheContext::Trusted)?;
298        let trie_backend = state.as_trie_backend();
299        let state_runtime_code = sp_state_machine::backend::BackendRuntimeCode::new(trie_backend);
300        let runtime_code = state_runtime_code
301            .runtime_code()
302            .map_err(sp_blockchain::Error::RuntimeCode)?
303            .fetch_runtime_code()
304            .ok_or(sp_blockchain::Error::RuntimeCode("missing runtime code"))?
305            .into_owned();
306
307        Ok(StatelessRuntime::<CBlock, Block, _>::new(
308            self.executor.clone(),
309            runtime_code.into(),
310        ))
311    }
312
313    fn batch_check_bundle_validity(
314        &self,
315        bundle: OpaqueBundle<NumberFor<CBlock>, CBlock::Hash, Block::Header, Balance>,
316        tx_range: &U256,
317        (parent_domain_hash, parent_domain_number): (Block::Hash, NumberFor<Block>),
318        at_consensus_hash: CBlock::Hash,
319    ) -> sp_blockchain::Result<BundleValidity<Block::Extrinsic>> {
320        let bundle_vrf_hash = U256::from_be_bytes(*bundle.proof_of_election().vrf_hash());
321        let bundle_length = bundle.body_length();
322        let bundle_estimated_weight = bundle.estimated_weight();
323        let mut maybe_invalid_bundle_type = None;
324
325        // Note: It is okay to use stateless here since all the bundle checks currently do not
326        // require state. But of any checks in the future requires a state read that was part of the
327        // genesis ex: `SelfChainId`, stateless runtime will panic.
328        // So ideal to set the genesis storage as fraud proof already have access to that and would be
329        // no different in terms of verification on both the sides
330        let stateless_runtime_api = self.stateless_runtime_api(parent_domain_hash)?;
331        let consensus_runtime_api = self.consensus_client.runtime_api();
332
333        // Check the validity of extrinsic inside the bundle, the goal is trying to find the first
334        // invalid tx and the first check it failed to pass, thus even an invalid tx that failed to
335        // pass a given check is found we still continue the following check for other txs that before
336        // it.
337        //
338        // NOTE: the checking order must follow `InvalidBundleType::checking_order`
339
340        let mut extrinsics =
341            stateless_runtime_api.decode_extrinsics_prefix(bundle.into_extrinsics())?;
342        if extrinsics.len() != bundle_length {
343            // If the length changed meaning there is undecodable tx at index `extrinsics.len()`
344            maybe_invalid_bundle_type
345                .replace(InvalidBundleType::UndecodableTx(extrinsics.len() as u32));
346        }
347
348        let signers = match stateless_runtime_api.extract_signer_if_all_within_tx_range(
349            &extrinsics,
350            &bundle_vrf_hash,
351            tx_range,
352        )? {
353            Err(index) => {
354                maybe_invalid_bundle_type.replace(InvalidBundleType::OutOfRangeTx(index));
355                extrinsics.truncate(index as usize);
356
357                // This will never used since there is an invalid tx
358                Vec::default()
359            }
360            Ok(signers) => signers,
361        };
362
363        // Check if this extrinsic is an inherent extrinsic.
364        // If so, this is an invalid bundle since these extrinsics should not be included in the
365        // bundle. Extrinsic is always decodable due to the check above.
366        if let Some(index) = stateless_runtime_api.find_first_inherent_extrinsic(&extrinsics)? {
367            maybe_invalid_bundle_type.replace(InvalidBundleType::InherentExtrinsic(index));
368            extrinsics.truncate(index as usize);
369        }
370
371        let batch_xdm_mmr_proof =
372            stateless_runtime_api.batch_extract_native_xdm_mmr_proof(&extrinsics)?;
373        for (index, xdm_mmr_proof) in batch_xdm_mmr_proof {
374            let ConsensusChainMmrLeafProof {
375                opaque_mmr_leaf,
376                proof,
377                ..
378            } = xdm_mmr_proof;
379
380            if consensus_runtime_api
381                .verify_proof(at_consensus_hash, vec![opaque_mmr_leaf], proof)?
382                .is_err()
383            {
384                maybe_invalid_bundle_type.replace(InvalidBundleType::InvalidXDM(index));
385                extrinsics.truncate(index as usize);
386                break;
387            }
388        }
389
390        // Using `check_extrinsics_and_do_pre_dispatch` instead of `check_transaction_validity`
391        // to maintain side-effect between tx in the storage buffer.
392        if let Err(CheckExtrinsicsValidityError {
393            extrinsic_index, ..
394        }) = self
395            .client
396            .runtime_api()
397            .check_extrinsics_and_do_pre_dispatch(
398                parent_domain_hash,
399                extrinsics.clone(),
400                parent_domain_number,
401                parent_domain_hash,
402            )?
403        {
404            maybe_invalid_bundle_type.replace(InvalidBundleType::IllegalTx(extrinsic_index));
405        }
406
407        // If there is any invalid tx, then return the error before checking the bundle weight,
408        // which is a check of the whole bundle and should only perform when all tx are valid.
409        if let Some(invalid_bundle_type) = maybe_invalid_bundle_type {
410            return Ok(BundleValidity::Invalid(invalid_bundle_type));
411        }
412
413        if bundle_estimated_weight != stateless_runtime_api.extrinsics_weight(&extrinsics)? {
414            return Ok(BundleValidity::Invalid(
415                InvalidBundleType::InvalidBundleWeight,
416            ));
417        }
418
419        let signer_and_extrinsics = signers.into_iter().zip(extrinsics).collect();
420        Ok(BundleValidity::ValidWithSigner(signer_and_extrinsics))
421    }
422}
423
424#[cfg(test)]
425mod tests {
426    use sp_domains::extrinsics::shuffle_extrinsics;
427    use sp_keyring::sr25519::Keyring;
428    use sp_runtime::traits::{BlakeTwo256, Hash as HashT};
429    use subspace_core_primitives::Randomness;
430
431    #[test]
432    fn shuffle_extrinsics_should_work() {
433        let alice = Keyring::Alice.to_account_id();
434        let bob = Keyring::Bob.to_account_id();
435        let charlie = Keyring::Charlie.to_account_id();
436
437        let extrinsics = vec![
438            (Some(alice.clone()), 10),
439            (None, 100),
440            (Some(bob.clone()), 1),
441            (Some(bob), 2),
442            (Some(charlie.clone()), 30),
443            (Some(alice.clone()), 11),
444            (Some(charlie), 31),
445            (None, 101),
446            (None, 102),
447            (Some(alice), 12),
448        ];
449
450        let dummy_seed = Randomness::from(BlakeTwo256::hash_of(&[1u8; 64]).to_fixed_bytes());
451        let shuffled_extrinsics = shuffle_extrinsics(extrinsics, dummy_seed);
452
453        assert_eq!(
454            shuffled_extrinsics,
455            vec![100, 30, 10, 1, 11, 101, 31, 12, 102, 2]
456        );
457    }
458}