subspace_malicious_operator/
malicious_bundle_tamper.rs1use 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#[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 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 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 if receipt.domain_block_number.is_zero() {
114 return Ok(());
115 }
116 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 None => return Ok(()),
235 }
236 }
237 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 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 _ => 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 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}