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        let state = self.backend.state_at(parent_domain_hash)?;
295        let trie_backend = state.as_trie_backend();
296        let state_runtime_code = sp_state_machine::backend::BackendRuntimeCode::new(trie_backend);
297        let runtime_code = state_runtime_code
298            .runtime_code()
299            .map_err(sp_blockchain::Error::RuntimeCode)?
300            .fetch_runtime_code()
301            .ok_or(sp_blockchain::Error::RuntimeCode("missing runtime code"))?
302            .into_owned();
303
304        Ok(StatelessRuntime::<CBlock, Block, _>::new(
305            self.executor.clone(),
306            runtime_code.into(),
307        ))
308    }
309
310    fn batch_check_bundle_validity(
311        &self,
312        bundle: OpaqueBundle<NumberFor<CBlock>, CBlock::Hash, Block::Header, Balance>,
313        tx_range: &U256,
314        (parent_domain_hash, parent_domain_number): (Block::Hash, NumberFor<Block>),
315        at_consensus_hash: CBlock::Hash,
316    ) -> sp_blockchain::Result<BundleValidity<Block::Extrinsic>> {
317        let bundle_vrf_hash = U256::from_be_bytes(*bundle.proof_of_election().vrf_hash());
318        let bundle_length = bundle.body_length();
319        let bundle_estimated_weight = bundle.estimated_weight();
320        let mut maybe_invalid_bundle_type = None;
321
322        // Note: It is okay to use stateless here since all the bundle checks currently do not
323        // require state. But of any checks in the future requires a state read that was part of the
324        // genesis ex: `SelfChainId`, stateless runtime will panic.
325        // So ideal to set the genesis storage as fraud proof already have access to that and would be
326        // no different in terms of verification on both the sides
327        let stateless_runtime_api = self.stateless_runtime_api(parent_domain_hash)?;
328        let consensus_runtime_api = self.consensus_client.runtime_api();
329
330        // Check the validity of extrinsic inside the bundle, the goal is trying to find the first
331        // invalid tx and the first check it failed to pass, thus even an invalid tx that failed to
332        // pass a given check is found we still continue the following check for other txs that before
333        // it.
334        //
335        // NOTE: the checking order must follow `InvalidBundleType::checking_order`
336
337        let mut extrinsics =
338            stateless_runtime_api.decode_extrinsics_prefix(bundle.into_extrinsics())?;
339        if extrinsics.len() != bundle_length {
340            // If the length changed meaning there is undecodable tx at index `extrinsics.len()`
341            maybe_invalid_bundle_type
342                .replace(InvalidBundleType::UndecodableTx(extrinsics.len() as u32));
343        }
344
345        let signers = match stateless_runtime_api.extract_signer_if_all_within_tx_range(
346            &extrinsics,
347            &bundle_vrf_hash,
348            tx_range,
349        )? {
350            Err(index) => {
351                maybe_invalid_bundle_type.replace(InvalidBundleType::OutOfRangeTx(index));
352                extrinsics.truncate(index as usize);
353
354                // This will never used since there is an invalid tx
355                Vec::default()
356            }
357            Ok(signers) => signers,
358        };
359
360        // Check if this extrinsic is an inherent extrinsic.
361        // If so, this is an invalid bundle since these extrinsics should not be included in the
362        // bundle. Extrinsic is always decodable due to the check above.
363        if let Some(index) = stateless_runtime_api.find_first_inherent_extrinsic(&extrinsics)? {
364            maybe_invalid_bundle_type.replace(InvalidBundleType::InherentExtrinsic(index));
365            extrinsics.truncate(index as usize);
366        }
367
368        let batch_xdm_mmr_proof =
369            stateless_runtime_api.batch_extract_native_xdm_mmr_proof(&extrinsics)?;
370        for (index, xdm_mmr_proof) in batch_xdm_mmr_proof {
371            let ConsensusChainMmrLeafProof {
372                opaque_mmr_leaf,
373                proof,
374                ..
375            } = xdm_mmr_proof;
376
377            if consensus_runtime_api
378                .verify_proof(at_consensus_hash, vec![opaque_mmr_leaf], proof)?
379                .is_err()
380            {
381                maybe_invalid_bundle_type.replace(InvalidBundleType::InvalidXDM(index));
382                extrinsics.truncate(index as usize);
383                break;
384            }
385        }
386
387        // Using `check_extrinsics_and_do_pre_dispatch` instead of `check_transaction_validity`
388        // to maintain side-effect between tx in the storage buffer.
389        if let Err(CheckExtrinsicsValidityError {
390            extrinsic_index, ..
391        }) = self
392            .client
393            .runtime_api()
394            .check_extrinsics_and_do_pre_dispatch(
395                parent_domain_hash,
396                extrinsics.clone(),
397                parent_domain_number,
398                parent_domain_hash,
399            )?
400        {
401            maybe_invalid_bundle_type.replace(InvalidBundleType::IllegalTx(extrinsic_index));
402        }
403
404        // If there is any invalid tx, then return the error before checking the bundle weight,
405        // which is a check of the whole bundle and should only perform when all tx are valid.
406        if let Some(invalid_bundle_type) = maybe_invalid_bundle_type {
407            return Ok(BundleValidity::Invalid(invalid_bundle_type));
408        }
409
410        if bundle_estimated_weight != stateless_runtime_api.extrinsics_weight(&extrinsics)? {
411            return Ok(BundleValidity::Invalid(
412                InvalidBundleType::InvalidBundleWeight,
413            ));
414        }
415
416        let signer_and_extrinsics = signers.into_iter().zip(extrinsics).collect();
417        Ok(BundleValidity::ValidWithSigner(signer_and_extrinsics))
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use sp_domains::extrinsics::shuffle_extrinsics;
424    use sp_keyring::sr25519::Keyring;
425    use sp_runtime::traits::{BlakeTwo256, Hash as HashT};
426    use subspace_core_primitives::Randomness;
427
428    #[test]
429    fn shuffle_extrinsics_should_work() {
430        let alice = Keyring::Alice.to_account_id();
431        let bob = Keyring::Bob.to_account_id();
432        let charlie = Keyring::Charlie.to_account_id();
433
434        let extrinsics = vec![
435            (Some(alice.clone()), 10),
436            (None, 100),
437            (Some(bob.clone()), 1),
438            (Some(bob), 2),
439            (Some(charlie.clone()), 30),
440            (Some(alice.clone()), 11),
441            (Some(charlie), 31),
442            (None, 101),
443            (None, 102),
444            (Some(alice), 12),
445        ];
446
447        let dummy_seed = Randomness::from(BlakeTwo256::hash_of(&[1u8; 64]).to_fixed_bytes());
448        let shuffled_extrinsics = shuffle_extrinsics(extrinsics, dummy_seed);
449
450        assert_eq!(
451            shuffled_extrinsics,
452            vec![100, 30, 10, 1, 11, 101, 31, 12, 102, 2]
453        );
454    }
455}