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::core_api::DomainCoreApi;
7use sp_domains::merkle_tree::MerkleTree;
8use sp_domains::{
9 BlockFees, BundleValidity, ChainId, HeaderHashingFor, InvalidBundleType, OperatorPublicKey,
10 OperatorSignature,
11};
12use sp_keystore::KeystorePtr;
13use sp_runtime::traits::{Block as BlockT, Hash as HashT, Header as HeaderT, NumberFor, One, Zero};
14use sp_runtime::{DigestItem, OpaqueExtrinsic, RuntimeAppPublic};
15use sp_weights::Weight;
16use std::collections::{BTreeMap, HashMap};
17use std::error::Error;
18use std::sync::Arc;
19
20const MAX_BAD_RECEIPT_CACHE: u32 = 128;
21
22#[allow(dead_code)]
25#[derive(Debug)]
26enum BadReceiptType {
27 InboxedBundle,
28 ExtrinsicsRoot,
29 ExecutionTrace,
30 BlockFees,
31 Transfers,
32 DomainBlockHash,
33 ParentReceipt,
34}
35
36struct Random;
37
38impl Random {
39 fn seed() -> u32 {
40 rand::random::<u32>()
41 }
42
43 fn probability(p: f64) -> bool {
45 assert!(p <= 1f64);
46 Self::seed() < ((u32::MAX as f64) * p) as u32
47 }
48}
49
50#[allow(clippy::type_complexity)]
51pub struct MaliciousBundleTamper<Block, CBlock, Client>
52where
53 Block: BlockT,
54 CBlock: BlockT,
55{
56 domain_client: Arc<Client>,
57 keystore: KeystorePtr,
58 bad_receipts_cache:
60 BTreeMap<NumberFor<Block>, HashMap<CBlock::Hash, ExecutionReceiptFor<Block, CBlock>>>,
61}
62
63impl<Block, CBlock, Client> MaliciousBundleTamper<Block, CBlock, Client>
64where
65 Block: BlockT,
66 CBlock: BlockT,
67 CBlock::Hash: Decode,
68 Client: HeaderBackend<Block> + ProvideRuntimeApi<Block> + 'static,
69 Client::Api: DomainCoreApi<Block>,
70{
71 pub fn new(domain_client: Arc<Client>, keystore: KeystorePtr) -> Self {
72 MaliciousBundleTamper {
73 domain_client,
74 keystore,
75 bad_receipts_cache: BTreeMap::new(),
76 }
77 }
78
79 pub fn maybe_tamper_bundle(
80 &mut self,
81 opaque_bundle: &mut OpaqueBundleFor<Block, CBlock>,
82 operator_signing_key: &OperatorPublicKey,
83 ) -> Result<(), Box<dyn Error>> {
84 if Random::probability(0.2) {
85 self.make_receipt_fraudulent(&mut opaque_bundle.sealed_header.header.receipt)?;
86 self.reseal_bundle(opaque_bundle, operator_signing_key)?;
87 }
88 if Random::probability(0.1) {
89 self.make_bundle_invalid(opaque_bundle)?;
90 self.reseal_bundle(opaque_bundle, operator_signing_key)?;
91 }
92 Ok(())
93 }
94
95 fn make_receipt_fraudulent(
96 &mut self,
97 receipt: &mut ExecutionReceiptFor<Block, CBlock>,
98 ) -> Result<(), Box<dyn Error>> {
99 if receipt.domain_block_number.is_zero() {
101 return Ok(());
102 }
103 if let Some(bad_receipts_at) = self.bad_receipts_cache.get(&receipt.domain_block_number) {
105 if let Some(previous_bad_receipt) = bad_receipts_at.get(&receipt.consensus_block_hash) {
106 *receipt = previous_bad_receipt.clone();
107 return Ok(());
108 }
109 }
110
111 let random_seed = Random::seed();
112 let bad_receipt_type = match random_seed % 7 {
113 0 => BadReceiptType::InboxedBundle,
114 1 => BadReceiptType::ExtrinsicsRoot,
115 2 => BadReceiptType::ExecutionTrace,
116 3 => BadReceiptType::BlockFees,
117 4 => BadReceiptType::Transfers,
118 5 => BadReceiptType::DomainBlockHash,
119 6 => BadReceiptType::ParentReceipt,
120 _ => return Ok(()),
121 };
122
123 tracing::info!(
124 ?bad_receipt_type,
125 "Generate bad ER of domain block {}#{}",
126 receipt.domain_block_number,
127 receipt.domain_block_hash,
128 );
129
130 match bad_receipt_type {
131 BadReceiptType::BlockFees => {
132 receipt.block_fees = BlockFees::new(
133 random_seed.into(),
134 random_seed.into(),
135 random_seed.into(),
136 BTreeMap::default(),
137 );
138 }
139 BadReceiptType::Transfers => {
140 receipt.transfers.transfers_in =
141 BTreeMap::from_iter([(ChainId::consensus_chain_id(), random_seed.into())]);
142 receipt.transfers.transfers_out =
143 BTreeMap::from_iter([(0.into(), random_seed.into())]);
144 receipt.transfers.rejected_transfers_claimed =
145 BTreeMap::from_iter([(random_seed.into(), random_seed.into())]);
146 receipt.transfers.transfers_rejected =
147 BTreeMap::from_iter([(1.into(), random_seed.into())]);
148 }
149 BadReceiptType::ExecutionTrace => {
150 let mismatch_index = random_seed as usize % receipt.execution_trace.len();
151 match random_seed as usize % 3 {
152 0 => receipt.execution_trace.push(Default::default()),
153 1 => {
154 receipt.execution_trace = receipt
155 .execution_trace
156 .clone()
157 .drain(..)
158 .take(mismatch_index + 1)
159 .collect();
160 }
161 2 => receipt.execution_trace[mismatch_index] = Default::default(),
162 _ => unreachable!(),
163 };
164 receipt.final_state_root = *receipt.execution_trace.last().unwrap();
165 receipt.execution_trace_root = {
166 let trace: Vec<_> = receipt
167 .execution_trace
168 .iter()
169 .map(|t| t.encode().try_into().unwrap())
170 .collect();
171 MerkleTree::from_leaves(trace.as_slice())
172 .root()
173 .unwrap()
174 .into()
175 };
176 }
177 BadReceiptType::ExtrinsicsRoot => {
178 receipt.domain_block_extrinsic_root = Default::default();
179 }
180 BadReceiptType::DomainBlockHash => {
181 receipt.domain_block_hash = Default::default();
182 }
183 BadReceiptType::ParentReceipt => {
184 let parent_domain_number = receipt.domain_block_number - One::one();
185 let parent_block_consensus_hash: CBlock::Hash = {
186 let parent_domain_hash = *self
187 .domain_client
188 .header(receipt.domain_block_hash)?
189 .ok_or_else(|| {
190 sp_blockchain::Error::Backend(format!(
191 "Domain block header for #{:?} not found",
192 receipt.domain_block_hash
193 ))
194 })?
195 .parent_hash();
196 let parent_domain_header = self
197 .domain_client
198 .header(parent_domain_hash)?
199 .ok_or_else(|| {
200 sp_blockchain::Error::Backend(format!(
201 "Domain block header for #{parent_domain_hash:?} not found",
202 ))
203 })?;
204 parent_domain_header
205 .digest()
206 .convert_first(DigestItem::as_consensus_block_info)
207 .expect("Domain block header must have the consensus block info digest")
208 };
209 let maybe_parent_bad_receipt = self
210 .bad_receipts_cache
211 .get(&parent_domain_number)
212 .and_then(|bad_receipts_at| bad_receipts_at.get(&parent_block_consensus_hash));
213 match maybe_parent_bad_receipt {
214 Some(parent_bad_receipt) => {
215 receipt.parent_domain_block_receipt_hash =
216 parent_bad_receipt.hash::<HeaderHashingFor<Block::Header>>();
217 }
218 None => return Ok(()),
221 }
222 }
223 BadReceiptType::InboxedBundle => {
226 let mismatch_index = random_seed as usize % receipt.inboxed_bundles.len();
227 receipt.inboxed_bundles[mismatch_index].bundle = if random_seed % 2 == 0 {
228 BundleValidity::Valid(Default::default())
229 } else {
230 let extrincis_index = random_seed % 2;
231 let invalid_bundle_type = match random_seed as usize % 5 {
232 0 => InvalidBundleType::UndecodableTx(extrincis_index),
233 1 => InvalidBundleType::OutOfRangeTx(extrincis_index),
234 2 => InvalidBundleType::IllegalTx(extrincis_index),
235 3 => InvalidBundleType::InherentExtrinsic(extrincis_index),
236 4 => InvalidBundleType::InvalidBundleWeight,
237 _ => unreachable!(),
238 };
239 BundleValidity::Invalid(invalid_bundle_type)
240 }
241 }
242 }
243
244 self.bad_receipts_cache
246 .entry(receipt.domain_block_number)
247 .or_default()
248 .insert(receipt.consensus_block_hash, receipt.clone());
249 if self.bad_receipts_cache.len() as u32 > MAX_BAD_RECEIPT_CACHE {
250 self.bad_receipts_cache.pop_first();
251 }
252
253 Ok(())
254 }
255
256 #[allow(clippy::modulo_one)]
257 fn make_bundle_invalid(
258 &self,
259 opaque_bundle: &mut OpaqueBundleFor<Block, CBlock>,
260 ) -> Result<(), Box<dyn Error>> {
261 let random_seed = Random::seed();
262 let invalid_bundle_type = match random_seed % 4 {
263 0 => InvalidBundleType::UndecodableTx(0),
264 1 => InvalidBundleType::IllegalTx(0),
265 2 => InvalidBundleType::InherentExtrinsic(0),
266 3 => InvalidBundleType::InvalidBundleWeight,
267 _ => unreachable!(),
271 };
272 tracing::info!(
273 ?invalid_bundle_type,
274 "Generate invalid bundle, receipt domain block {}#{}",
275 opaque_bundle.receipt().domain_block_number,
276 opaque_bundle.receipt().domain_block_hash,
277 );
278
279 let invalid_tx = match invalid_bundle_type {
280 InvalidBundleType::UndecodableTx(_) => OpaqueExtrinsic::default(),
281 InvalidBundleType::IllegalTx(_) if !opaque_bundle.extrinsics.is_empty() => {
283 opaque_bundle.extrinsics[0].clone()
284 }
285 InvalidBundleType::InherentExtrinsic(_) => {
286 let inherent_tx = self
287 .domain_client
288 .runtime_api()
289 .construct_timestamp_extrinsic(
290 self.domain_client.info().best_hash,
291 Default::default(),
292 )?;
293 OpaqueExtrinsic::from_bytes(&inherent_tx.encode())
294 .expect("We have just encoded a valid extrinsic; qed")
295 }
296 InvalidBundleType::InvalidBundleWeight => {
297 opaque_bundle.sealed_header.header.estimated_bundle_weight =
298 Weight::from_all(123456);
299 return Ok(());
300 }
301 _ => return Ok(()),
302 };
303
304 opaque_bundle.extrinsics.push(invalid_tx);
305
306 Ok(())
307 }
308
309 fn reseal_bundle(
310 &self,
311 opaque_bundle: &mut OpaqueBundleFor<Block, CBlock>,
312 operator_signing_key: &OperatorPublicKey,
313 ) -> Result<(), Box<dyn Error>> {
314 opaque_bundle.sealed_header.header.bundle_extrinsics_root =
315 HeaderHashingFor::<Block::Header>::ordered_trie_root(
316 opaque_bundle
317 .extrinsics
318 .iter()
319 .map(|xt| xt.encode())
320 .collect(),
321 sp_core::storage::StateVersion::V1,
322 );
323
324 let pre_hash = opaque_bundle.sealed_header.pre_hash();
325 opaque_bundle.sealed_header.signature = {
326 let s = self
327 .keystore
328 .sr25519_sign(
329 OperatorPublicKey::ID,
330 operator_signing_key.as_ref(),
331 pre_hash.as_ref(),
332 )?
333 .expect("The malicious operator's key pair must exist");
334 OperatorSignature::decode(&mut s.as_ref())
335 .expect("Deconde as OperatorSignature must succeed")
336 };
337 Ok(())
338 }
339}