subspace_malicious_operator/
malicious_bundle_tamper.rs

1use domain_client_operator::{ExecutionReceiptFor, OpaqueBundleFor};
2use parity_scale_codec::{Decode, Encode};
3use sc_client_api::HeaderBackend;
4use sp_api::ProvideRuntimeApi;
5use sp_domain_digests::AsPredigest;
6use sp_domains::bundle::{BundleValidity, InvalidBundleType};
7use sp_domains::core_api::DomainCoreApi;
8use sp_domains::execution_receipt::{
9    BlockFees, ExecutionReceipt, ExecutionReceiptMutRef, ExecutionReceiptRef,
10};
11use sp_domains::merkle_tree::MerkleTree;
12use sp_domains::{ChainId, HeaderHashingFor, OperatorPublicKey, OperatorSignature};
13use sp_keystore::KeystorePtr;
14use sp_runtime::traits::{Block as BlockT, Hash as HashT, Header as HeaderT, NumberFor, One, Zero};
15use sp_runtime::{DigestItem, OpaqueExtrinsic, RuntimeAppPublic};
16use sp_weights::Weight;
17use std::collections::{BTreeMap, HashMap};
18use std::error::Error;
19use std::sync::Arc;
20use subspace_runtime_primitives::{Balance, BlockHashFor};
21
22const MAX_BAD_RECEIPT_CACHE: u32 = 128;
23
24// TODO: remove dead_code once the `InboxedBundle` variant is used
25// currently blocked due to https://github.com/autonomys/subspace/issues/2287
26#[allow(dead_code)]
27#[derive(Debug)]
28enum BadReceiptType {
29    InboxedBundle,
30    ExtrinsicsRoot,
31    ExecutionTrace,
32    BlockFees,
33    Transfers,
34    DomainBlockHash,
35    ParentReceipt,
36}
37
38struct Random;
39
40impl Random {
41    fn seed() -> u32 {
42        rand::random::<u32>()
43    }
44
45    // Return `true` based on the given probability
46    fn probability(p: f64) -> bool {
47        assert!(p <= 1f64);
48        Self::seed() < ((u32::MAX as f64) * p) as u32
49    }
50}
51
52#[allow(clippy::type_complexity)]
53pub struct MaliciousBundleTamper<Block, CBlock, Client>
54where
55    Block: BlockT,
56    CBlock: BlockT,
57{
58    domain_client: Arc<Client>,
59    keystore: KeystorePtr,
60    // A cache for recently produced bad receipts
61    bad_receipts_cache:
62        BTreeMap<NumberFor<Block>, HashMap<CBlock::Hash, ExecutionReceiptFor<Block, CBlock>>>,
63}
64
65pub type ExecutionReceiptMutRefFor<'a, Block, CBlock> = ExecutionReceiptMutRef<
66    'a,
67    NumberFor<CBlock>,
68    BlockHashFor<CBlock>,
69    NumberFor<Block>,
70    BlockHashFor<Block>,
71    Balance,
72>;
73
74impl<Block, CBlock, Client> MaliciousBundleTamper<Block, CBlock, Client>
75where
76    Block: BlockT,
77    CBlock: BlockT,
78    CBlock::Hash: Decode,
79    Client: HeaderBackend<Block> + ProvideRuntimeApi<Block> + 'static,
80    Client::Api: DomainCoreApi<Block>,
81{
82    pub fn new(domain_client: Arc<Client>, keystore: KeystorePtr) -> Self {
83        MaliciousBundleTamper {
84            domain_client,
85            keystore,
86            bad_receipts_cache: BTreeMap::new(),
87        }
88    }
89
90    pub fn maybe_tamper_bundle(
91        &mut self,
92        opaque_bundle: &mut OpaqueBundleFor<Block, CBlock>,
93        operator_signing_key: &OperatorPublicKey,
94    ) -> Result<(), Box<dyn Error>> {
95        if Random::probability(0.2) {
96            self.make_receipt_fraudulent(opaque_bundle.execution_receipt_as_mut())?;
97            self.reseal_bundle(opaque_bundle, operator_signing_key)?;
98        }
99
100        if Random::probability(0.1) {
101            self.make_bundle_invalid(opaque_bundle)?;
102            self.reseal_bundle(opaque_bundle, operator_signing_key)?;
103        }
104        Ok(())
105    }
106
107    fn make_receipt_fraudulent(
108        &mut self,
109        receipt: ExecutionReceiptMutRefFor<Block, CBlock>,
110    ) -> Result<(), Box<dyn Error>> {
111        let ExecutionReceiptMutRef::V0(receipt) = receipt;
112        // We can't make the genesis receipt into a bad ER
113        if receipt.domain_block_number.is_zero() {
114            return Ok(());
115        }
116        // If a bad receipt is already made for the same domain block, reuse it
117        if let Some(bad_receipts_at) = self.bad_receipts_cache.get(&receipt.domain_block_number)
118            && let Some(ExecutionReceipt::V0(previous_bad_receipt)) =
119                bad_receipts_at.get(&receipt.consensus_block_hash)
120        {
121            *receipt = previous_bad_receipt.clone();
122            return Ok(());
123        }
124
125        let random_seed = Random::seed();
126        let bad_receipt_type = match random_seed % 7 {
127            0 => BadReceiptType::InboxedBundle,
128            1 => BadReceiptType::ExtrinsicsRoot,
129            2 => BadReceiptType::ExecutionTrace,
130            3 => BadReceiptType::BlockFees,
131            4 => BadReceiptType::Transfers,
132            5 => BadReceiptType::DomainBlockHash,
133            6 => BadReceiptType::ParentReceipt,
134            _ => return Ok(()),
135        };
136
137        tracing::info!(
138            ?bad_receipt_type,
139            "Generate bad ER of domain block {}#{}",
140            receipt.domain_block_number,
141            receipt.domain_block_hash,
142        );
143
144        match bad_receipt_type {
145            BadReceiptType::BlockFees => {
146                receipt.block_fees = BlockFees::new(
147                    random_seed.into(),
148                    random_seed.into(),
149                    random_seed.into(),
150                    BTreeMap::default(),
151                );
152            }
153            BadReceiptType::Transfers => {
154                receipt.transfers.transfers_in =
155                    BTreeMap::from_iter([(ChainId::consensus_chain_id(), random_seed.into())]);
156                receipt.transfers.transfers_out =
157                    BTreeMap::from_iter([(0.into(), random_seed.into())]);
158                receipt.transfers.rejected_transfers_claimed =
159                    BTreeMap::from_iter([(random_seed.into(), random_seed.into())]);
160                receipt.transfers.transfers_rejected =
161                    BTreeMap::from_iter([(1.into(), random_seed.into())]);
162            }
163            BadReceiptType::ExecutionTrace => {
164                let mismatch_index = random_seed as usize % receipt.execution_trace.len();
165                match random_seed as usize % 3 {
166                    0 => receipt.execution_trace.push(Default::default()),
167                    1 => {
168                        receipt.execution_trace = receipt
169                            .execution_trace
170                            .clone()
171                            .drain(..)
172                            .take(mismatch_index + 1)
173                            .collect();
174                    }
175                    2 => receipt.execution_trace[mismatch_index] = Default::default(),
176                    _ => unreachable!(),
177                };
178                receipt.final_state_root = *receipt.execution_trace.last().unwrap();
179                receipt.execution_trace_root = {
180                    let trace: Vec<_> = receipt
181                        .execution_trace
182                        .iter()
183                        .map(|t| t.encode().try_into().unwrap())
184                        .collect();
185                    MerkleTree::from_leaves(trace.as_slice())
186                        .root()
187                        .unwrap()
188                        .into()
189                };
190            }
191            BadReceiptType::ExtrinsicsRoot => {
192                receipt.domain_block_extrinsic_root = Default::default();
193            }
194            BadReceiptType::DomainBlockHash => {
195                receipt.domain_block_hash = Default::default();
196            }
197            BadReceiptType::ParentReceipt => {
198                let parent_domain_number = receipt.domain_block_number - One::one();
199                let parent_block_consensus_hash: CBlock::Hash = {
200                    let parent_domain_hash = *self
201                        .domain_client
202                        .header(receipt.domain_block_hash)?
203                        .ok_or_else(|| {
204                            sp_blockchain::Error::Backend(format!(
205                                "Domain block header for #{:?} not found",
206                                receipt.domain_block_hash
207                            ))
208                        })?
209                        .parent_hash();
210                    let parent_domain_header = self
211                        .domain_client
212                        .header(parent_domain_hash)?
213                        .ok_or_else(|| {
214                            sp_blockchain::Error::Backend(format!(
215                                "Domain block header for #{parent_domain_hash:?} not found",
216                            ))
217                        })?;
218                    parent_domain_header
219                        .digest()
220                        .convert_first(DigestItem::as_consensus_block_info)
221                        .expect("Domain block header must have the consensus block info digest")
222                };
223                let maybe_parent_bad_receipt = self
224                    .bad_receipts_cache
225                    .get(&parent_domain_number)
226                    .and_then(|bad_receipts_at| bad_receipts_at.get(&parent_block_consensus_hash));
227                match maybe_parent_bad_receipt {
228                    Some(parent_bad_receipt) => {
229                        receipt.parent_domain_block_receipt_hash =
230                            parent_bad_receipt.hash::<HeaderHashingFor<Block::Header>>();
231                    }
232                    // The parent receipt is not a bad receipt so even we modify this field to a random
233                    // value, the receipt will be rejected by the consensus node directly thus just skip
234                    None => return Ok(()),
235                }
236            }
237            // NOTE: Not need to modify the bundle `extrinsics_root` or the length of `inboxed_bundles`
238            // since the consensus runtime will perform the these checks and reject the bundle directly
239            BadReceiptType::InboxedBundle => {
240                let mismatch_index = random_seed as usize % receipt.inboxed_bundles.len();
241                receipt.inboxed_bundles[mismatch_index].bundle = if random_seed % 2 == 0 {
242                    BundleValidity::Valid(Default::default())
243                } else {
244                    let extrincis_index = random_seed % 2;
245                    let invalid_bundle_type = match random_seed as usize % 5 {
246                        0 => InvalidBundleType::UndecodableTx(extrincis_index),
247                        1 => InvalidBundleType::OutOfRangeTx(extrincis_index),
248                        2 => InvalidBundleType::IllegalTx(extrincis_index),
249                        3 => InvalidBundleType::InherentExtrinsic(extrincis_index),
250                        4 => InvalidBundleType::InvalidBundleWeight,
251                        _ => unreachable!(),
252                    };
253                    BundleValidity::Invalid(invalid_bundle_type)
254                }
255            }
256        }
257
258        // Add the bad receipt to cache and remove the oldest receipt from cache
259        self.bad_receipts_cache
260            .entry(receipt.domain_block_number)
261            .or_default()
262            .insert(
263                receipt.consensus_block_hash,
264                ExecutionReceipt::V0(receipt.clone()),
265            );
266        if self.bad_receipts_cache.len() as u32 > MAX_BAD_RECEIPT_CACHE {
267            self.bad_receipts_cache.pop_first();
268        }
269
270        Ok(())
271    }
272
273    #[allow(clippy::modulo_one)]
274    fn make_bundle_invalid(
275        &self,
276        opaque_bundle: &mut OpaqueBundleFor<Block, CBlock>,
277    ) -> Result<(), Box<dyn Error>> {
278        let random_seed = Random::seed();
279        let invalid_bundle_type = match random_seed % 4 {
280            0 => InvalidBundleType::UndecodableTx(0),
281            1 => InvalidBundleType::IllegalTx(0),
282            2 => InvalidBundleType::InherentExtrinsic(0),
283            3 => InvalidBundleType::InvalidBundleWeight,
284            // TODO: `OutOfRangeTx` invalid bundle is tricky to simulate because the
285            // tx range is dynamically change based on the `proof_of_election.vrf_hash`
286            // 1 => InvalidBundleType::OutOfRangeTx(0),
287            _ => unreachable!(),
288        };
289        let ExecutionReceiptRef::V0(receipt) = opaque_bundle.receipt();
290        tracing::info!(
291            ?invalid_bundle_type,
292            "Generate invalid bundle, receipt domain block {}#{}",
293            receipt.domain_block_number,
294            receipt.domain_block_hash,
295        );
296
297        let invalid_tx = match invalid_bundle_type {
298            InvalidBundleType::UndecodableTx(_) => OpaqueExtrinsic::default(),
299            // The duplicated extrinsic will be illegal due to `Nonce` if it is a signed extrinsic
300            InvalidBundleType::IllegalTx(_) if !opaque_bundle.extrinsics().is_empty() => {
301                opaque_bundle.extrinsics()[0].clone()
302            }
303            InvalidBundleType::InherentExtrinsic(_) => {
304                let inherent_tx = self
305                    .domain_client
306                    .runtime_api()
307                    .construct_timestamp_extrinsic(
308                        self.domain_client.info().best_hash,
309                        Default::default(),
310                    )?;
311                OpaqueExtrinsic::from_bytes(&inherent_tx.encode())
312                    .expect("We have just encoded a valid extrinsic; qed")
313            }
314            InvalidBundleType::InvalidBundleWeight => {
315                opaque_bundle.set_estimated_bundle_weight(Weight::from_all(123456));
316                return Ok(());
317            }
318            _ => return Ok(()),
319        };
320
321        let mut exts = opaque_bundle.extrinsics().to_vec();
322        exts.push(invalid_tx);
323        opaque_bundle.set_extrinsics(exts);
324
325        Ok(())
326    }
327
328    fn reseal_bundle(
329        &self,
330        opaque_bundle: &mut OpaqueBundleFor<Block, CBlock>,
331        operator_signing_key: &OperatorPublicKey,
332    ) -> Result<(), Box<dyn Error>> {
333        opaque_bundle.set_bundle_extrinsics_root(
334            HeaderHashingFor::<Block::Header>::ordered_trie_root(
335                opaque_bundle
336                    .extrinsics()
337                    .iter()
338                    .map(|xt| xt.encode())
339                    .collect(),
340                sp_core::storage::StateVersion::V1,
341            ),
342        );
343
344        let pre_hash = opaque_bundle.sealed_header().pre_hash();
345        opaque_bundle.set_signature({
346            let s = self
347                .keystore
348                .sr25519_sign(
349                    OperatorPublicKey::ID,
350                    operator_signing_key.as_ref(),
351                    pre_hash.as_ref(),
352                )?
353                .expect("The malicious operator's key pair must exist");
354            OperatorSignature::decode(&mut s.as_ref())
355                .expect("Decode as OperatorSignature must succeed")
356        });
357        Ok(())
358    }
359}