1use crate::ExecutionReceiptFor;
4use parity_scale_codec::{Decode, Encode};
5use sc_client_api::backend::AuxStore;
6use sp_blockchain::{Error as ClientError, HeaderBackend, Result as ClientResult};
7use sp_domains::bundle::InvalidBundleType;
8use sp_runtime::Saturating;
9use sp_runtime::traits::{
10 Block as BlockT, CheckedMul, CheckedSub, NumberFor, One, SaturatedConversion, Zero,
11};
12use std::collections::BTreeSet;
13use std::sync::Arc;
14use subspace_core_primitives::BlockNumber;
15use subspace_runtime_primitives::{BlockHashFor, DOMAINS_PRUNING_DEPTH_MULTIPLIER};
16
17const EXECUTION_RECEIPT: &[u8] = b"execution_receipt";
18const EXECUTION_RECEIPT_START: &[u8] = b"execution_receipt_start";
19const EXECUTION_RECEIPT_BLOCK_NUMBER: &[u8] = b"execution_receipt_block_number";
20
21const LATEST_CONSENSUS_HASH: &[u8] = b"latest_consensus_hash";
33
34const BEST_DOMAIN_HASH: &[u8] = b"best_domain_hash";
42
43const BEST_DOMAIN_HASH_KEYS: &[u8] = b"best_domain_hash_keys";
46
47fn execution_receipt_key(block_hash: impl Encode) -> Vec<u8> {
48 (EXECUTION_RECEIPT, block_hash).encode()
49}
50
51fn load_decode<Backend: AuxStore, T: Decode>(
52 backend: &Backend,
53 key: &[u8],
54) -> ClientResult<Option<T>> {
55 match backend.get_aux(key)? {
56 None => Ok(None),
57 Some(t) => T::decode(&mut &t[..])
58 .map_err(|e| {
59 ClientError::Backend(format!("Operator DB is corrupted. Decode error: {e}"))
60 })
61 .map(Some),
62 }
63}
64
65pub(super) fn write_execution_receipt<Backend, Block, CBlock>(
68 backend: &Backend,
69 oldest_unconfirmed_receipt_number: Option<NumberFor<Block>>,
70 execution_receipt: &ExecutionReceiptFor<Block, CBlock>,
71 challenge_period: NumberFor<CBlock>,
72) -> Result<(), sp_blockchain::Error>
73where
74 Backend: AuxStore,
75 Block: BlockT,
76 CBlock: BlockT,
77{
78 let block_number = execution_receipt.consensus_block_number();
79 let consensus_hash = *execution_receipt.consensus_block_hash();
80
81 let block_number_key = (EXECUTION_RECEIPT_BLOCK_NUMBER, block_number).encode();
82 let mut hashes_at_block_number =
83 load_decode::<_, Vec<CBlock::Hash>>(backend, block_number_key.as_slice())?
84 .unwrap_or_default();
85 hashes_at_block_number.push(consensus_hash);
86
87 let first_saved_receipt =
88 load_decode::<_, NumberFor<CBlock>>(backend, EXECUTION_RECEIPT_START)?
89 .unwrap_or(Zero::zero());
90
91 let mut new_first_saved_receipt = first_saved_receipt;
92
93 let mut keys_to_delete = vec![];
94
95 if let Some(pruning_block_number) =
96 challenge_period.checked_mul(&DOMAINS_PRUNING_DEPTH_MULTIPLIER.saturated_into())
97 {
98 if let Some(delete_receipts_to) = oldest_unconfirmed_receipt_number
100 .map(|oldest_unconfirmed_receipt_number| {
101 oldest_unconfirmed_receipt_number.saturating_sub(One::one())
102 })
103 .and_then(|latest_confirmed_receipt_number| {
104 latest_confirmed_receipt_number
105 .saturated_into::<BlockNumber>()
106 .checked_sub(pruning_block_number.saturated_into())
107 })
108 {
109 new_first_saved_receipt =
110 Into::<NumberFor<CBlock>>::into(delete_receipts_to) + One::one();
111 for receipt_to_delete in first_saved_receipt.saturated_into()..=delete_receipts_to {
112 let delete_block_number_key =
113 (EXECUTION_RECEIPT_BLOCK_NUMBER, receipt_to_delete).encode();
114
115 if let Some(hashes_to_delete) = load_decode::<_, Vec<CBlock::Hash>>(
116 backend,
117 delete_block_number_key.as_slice(),
118 )? {
119 keys_to_delete.extend(
120 hashes_to_delete
121 .into_iter()
122 .map(|h| (EXECUTION_RECEIPT, h).encode()),
123 );
124 keys_to_delete.push(delete_block_number_key);
125 }
126 }
127 }
128 }
129
130 backend.insert_aux(
131 &[
132 (
133 execution_receipt_key(consensus_hash).as_slice(),
134 execution_receipt.encode().as_slice(),
135 ),
136 (
137 block_number_key.as_slice(),
138 hashes_at_block_number.encode().as_slice(),
139 ),
140 (
141 EXECUTION_RECEIPT_START,
142 new_first_saved_receipt.encode().as_slice(),
143 ),
144 ],
145 &keys_to_delete
146 .iter()
147 .map(|k| &k[..])
148 .collect::<Vec<&[u8]>>()[..],
149 )
150}
151
152pub fn load_execution_receipt<Backend, Block, CBlock>(
154 backend: &Backend,
155 consensus_block_hash: CBlock::Hash,
156) -> ClientResult<Option<ExecutionReceiptFor<Block, CBlock>>>
157where
158 Backend: AuxStore,
159 Block: BlockT,
160 CBlock: BlockT,
161{
162 load_decode(
163 backend,
164 execution_receipt_key(consensus_block_hash).as_slice(),
165 )
166}
167
168type MaybeTrackedDomainHashes<Block, CBlock> =
169 Option<BTreeSet<(BlockHashFor<Block>, BlockHashFor<CBlock>)>>;
170
171fn get_tracked_domain_hash_keys<Backend, Block, CBlock>(
172 backend: &Backend,
173 domain_block_number: NumberFor<Block>,
174) -> ClientResult<MaybeTrackedDomainHashes<Block, CBlock>>
175where
176 Backend: AuxStore,
177 Block: BlockT,
178 CBlock: BlockT,
179{
180 load_decode(
181 backend,
182 (BEST_DOMAIN_HASH_KEYS, domain_block_number)
183 .encode()
184 .as_slice(),
185 )
186}
187
188pub(super) fn track_domain_hash_and_consensus_hash<Client, Block, CBlock>(
189 domain_client: &Arc<Client>,
190 best_domain_hash: Block::Hash,
191 latest_consensus_hash: CBlock::Hash,
192 cleanup: bool,
193) -> ClientResult<()>
194where
195 Client: HeaderBackend<Block> + AuxStore,
196 CBlock: BlockT,
197 Block: BlockT,
198{
199 let best_domain_number =
200 domain_client
201 .number(best_domain_hash)?
202 .ok_or(sp_blockchain::Error::MissingHeader(format!(
203 "Block hash: {best_domain_hash:?}"
204 )))?;
205 let mut domain_hash_keys =
206 get_tracked_domain_hash_keys::<_, Block, CBlock>(&**domain_client, best_domain_number)?
207 .unwrap_or_default();
208
209 domain_hash_keys.insert((best_domain_hash, latest_consensus_hash));
210
211 domain_client.insert_aux(
212 &[
213 (
214 (LATEST_CONSENSUS_HASH, best_domain_hash)
215 .encode()
216 .as_slice(),
217 latest_consensus_hash.encode().as_slice(),
218 ),
219 (
220 (BEST_DOMAIN_HASH, latest_consensus_hash)
221 .encode()
222 .as_slice(),
223 best_domain_hash.encode().as_slice(),
224 ),
225 (
226 (BEST_DOMAIN_HASH_KEYS, best_domain_number)
227 .encode()
228 .as_slice(),
229 domain_hash_keys.encode().as_slice(),
230 ),
231 ],
232 vec![],
233 )?;
234
235 if cleanup {
236 cleanup_domain_hash_and_consensus_hash::<_, Block, CBlock>(domain_client)?;
237 }
238
239 Ok(())
240}
241
242fn cleanup_domain_hash_and_consensus_hash<Client, Block, CBlock>(
243 domain_client: &Arc<Client>,
244) -> ClientResult<()>
245where
246 CBlock: BlockT,
247 Block: BlockT,
248 Client: HeaderBackend<Block> + AuxStore,
249{
250 let mut finalized_domain_number = domain_client.info().finalized_number;
251
252 let mut deletions = vec![];
253 while finalized_domain_number > Zero::zero()
254 && let Some(domain_hash_keys) = &get_tracked_domain_hash_keys::<_, Block, CBlock>(
256 &**domain_client,
257 finalized_domain_number,
258 )?
259 {
260 domain_hash_keys
261 .iter()
262 .for_each(|(domain_hash, consensus_hash)| {
263 deletions.push((LATEST_CONSENSUS_HASH, domain_hash).encode());
264 deletions.push((BEST_DOMAIN_HASH, consensus_hash).encode())
265 });
266
267 deletions.push((BEST_DOMAIN_HASH_KEYS, finalized_domain_number).encode());
268
269 finalized_domain_number = match finalized_domain_number.checked_sub(&One::one()) {
270 None => break,
271 Some(number) => number,
272 }
273 }
274
275 domain_client.insert_aux(
276 [],
277 &deletions
278 .iter()
279 .map(|key| key.as_slice())
280 .collect::<Vec<_>>(),
281 )
282}
283
284pub(super) fn best_domain_hash_for<Backend, Hash, CHash>(
285 backend: &Backend,
286 consensus_hash: &CHash,
287) -> ClientResult<Option<Hash>>
288where
289 Backend: AuxStore,
290 Hash: Decode,
291 CHash: Encode,
292{
293 load_decode(
294 backend,
295 (BEST_DOMAIN_HASH, consensus_hash).encode().as_slice(),
296 )
297}
298
299pub(super) fn latest_consensus_block_hash_for<Backend, Hash, CHash>(
300 backend: &Backend,
301 domain_hash: &Hash,
302) -> ClientResult<Option<CHash>>
303where
304 Backend: AuxStore,
305 Hash: Encode,
306 CHash: Decode,
307{
308 load_decode(
309 backend,
310 (LATEST_CONSENSUS_HASH, domain_hash).encode().as_slice(),
311 )
312}
313
314#[derive(Encode, Decode, Debug, PartialEq)]
316pub(super) enum BundleMismatchType {
317 GoodInvalid(InvalidBundleType),
321 BadInvalid(InvalidBundleType),
324 ValidBundleContents,
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332 use domain_test_service::evm_domain_test_runtime::Block;
333 use parking_lot::Mutex;
334 use sp_core::hash::H256;
335 use sp_domains::execution_receipt::execution_receipt_v0::ExecutionReceiptV0;
336 use std::collections::HashMap;
337 use subspace_runtime_primitives::{Balance, Hash};
338 use subspace_test_runtime::Block as CBlock;
339
340 const PRUNING_DEPTH: BlockNumber = 1000;
341
342 type ExecutionReceipt = sp_domains::execution_receipt::ExecutionReceipt<
343 BlockNumber,
344 Hash,
345 BlockNumber,
346 Hash,
347 Balance,
348 >;
349
350 fn create_execution_receipt(consensus_block_number: BlockNumber) -> ExecutionReceipt {
351 ExecutionReceipt::V0(ExecutionReceiptV0 {
352 domain_block_number: consensus_block_number,
353 domain_block_hash: H256::random(),
354 domain_block_extrinsic_root: H256::random(),
355 parent_domain_block_receipt_hash: H256::random(),
356 consensus_block_number,
357 consensus_block_hash: H256::random(),
358 inboxed_bundles: Vec::new(),
359 final_state_root: Default::default(),
360 execution_trace: Default::default(),
361 execution_trace_root: Default::default(),
362 block_fees: Default::default(),
363 transfers: Default::default(),
364 })
365 }
366
367 #[derive(Default)]
368 struct TestClient(Mutex<HashMap<Vec<u8>, Vec<u8>>>);
369
370 impl AuxStore for TestClient {
371 fn insert_aux<
372 'a,
373 'b: 'a,
374 'c: 'a,
375 I: IntoIterator<Item = &'a (&'c [u8], &'c [u8])>,
376 D: IntoIterator<Item = &'a &'b [u8]>,
377 >(
378 &self,
379 insert: I,
380 delete: D,
381 ) -> sp_blockchain::Result<()> {
382 let mut map = self.0.lock();
383 for d in delete {
384 map.remove(&d.to_vec());
385 }
386 for (k, v) in insert {
387 map.insert(k.to_vec(), v.to_vec());
388 }
389 Ok(())
390 }
391
392 fn get_aux(&self, key: &[u8]) -> sp_blockchain::Result<Option<Vec<u8>>> {
393 Ok(self.0.lock().get(key).cloned())
394 }
395 }
396
397 #[test]
398 fn normal_prune_execution_receipt_works() {
399 let block_tree_pruning_depth = 256;
400 let challenge_period = 500;
401 let client = TestClient::default();
402
403 let receipt_start = || {
404 load_decode::<_, BlockNumber>(&client, EXECUTION_RECEIPT_START.to_vec().as_slice())
405 .unwrap()
406 };
407
408 let hashes_at = |number: BlockNumber| {
409 load_decode::<_, Vec<Hash>>(
410 &client,
411 (EXECUTION_RECEIPT_BLOCK_NUMBER, number).encode().as_slice(),
412 )
413 .unwrap()
414 };
415
416 let target_receipt_is_pruned = |number: BlockNumber| hashes_at(number).is_none();
417
418 let receipt_at = |consensus_block_hash: Hash| {
419 load_execution_receipt::<_, Block, CBlock>(&client, consensus_block_hash).unwrap()
420 };
421
422 let write_receipt_at = |oldest_unconfirmed_receipt_number: Option<BlockNumber>,
423 receipt: &ExecutionReceipt| {
424 write_execution_receipt::<_, Block, CBlock>(
425 &client,
426 oldest_unconfirmed_receipt_number,
427 receipt,
428 challenge_period,
429 )
430 .unwrap()
431 };
432
433 assert_eq!(receipt_start(), None);
434
435 let receipt_count = PRUNING_DEPTH + block_tree_pruning_depth - 1;
437 let block_hash_list = (1..=receipt_count)
438 .map(|block_number| {
439 let receipt = create_execution_receipt(block_number);
440 let consensus_block_hash = *receipt.consensus_block_hash();
441 let oldest_unconfirmed_receipt_number = block_number
442 .checked_sub(block_tree_pruning_depth)
443 .map(|n| n + 1);
444 write_receipt_at(oldest_unconfirmed_receipt_number, &receipt);
445 assert_eq!(receipt_at(consensus_block_hash), Some(receipt));
446 assert_eq!(hashes_at(block_number), Some(vec![consensus_block_hash]));
447 assert_eq!(receipt_start(), Some(0));
449 consensus_block_hash
450 })
451 .collect::<Vec<_>>();
452
453 assert_eq!(receipt_start(), Some(0));
454 assert!(!target_receipt_is_pruned(1));
455
456 let receipt = create_execution_receipt(receipt_count + 1);
458 assert!(receipt_at(*receipt.consensus_block_hash()).is_none());
459 write_receipt_at(Some(PRUNING_DEPTH + 1), &receipt);
460 assert!(receipt_at(*receipt.consensus_block_hash()).is_some());
461 assert_eq!(receipt_start(), Some(1));
462
463 let receipt = create_execution_receipt(receipt_count + 2);
465 write_receipt_at(Some(PRUNING_DEPTH + 2), &receipt);
466 assert!(receipt_at(*receipt.consensus_block_hash()).is_some());
467
468 assert!(receipt_at(block_hash_list[0]).is_none());
470 assert!(hashes_at(1).is_none());
471 assert!(target_receipt_is_pruned(1));
472 assert_eq!(receipt_start(), Some(2));
473
474 let receipt = create_execution_receipt(receipt_count + 3);
476 let consensus_block_hash1 = *receipt.consensus_block_hash();
477 write_receipt_at(Some(PRUNING_DEPTH + 3), &receipt);
478 assert!(receipt_at(consensus_block_hash1).is_some());
479 assert!(receipt_at(block_hash_list[1]).is_none());
481 assert!(target_receipt_is_pruned(2));
482 assert!(!target_receipt_is_pruned(3));
483 assert_eq!(receipt_start(), Some(3));
484
485 let receipt = create_execution_receipt(receipt_count + 3);
487 let consensus_block_hash2 = *receipt.consensus_block_hash();
488 write_receipt_at(Some(PRUNING_DEPTH + 3), &receipt);
489 assert!(receipt_at(consensus_block_hash2).is_some());
490 assert_eq!(
491 hashes_at(receipt_count + 3),
492 Some(vec![consensus_block_hash1, consensus_block_hash2])
493 );
494 assert!(!target_receipt_is_pruned(3));
496 assert_eq!(receipt_start(), Some(3));
497 }
498
499 #[test]
500 fn execution_receipts_should_be_kept_against_oldest_unconfirmed_receipt_number() {
501 let block_tree_pruning_depth = 256;
502 let challenge_period = 500;
503 let client = TestClient::default();
504
505 let receipt_start = || {
506 load_decode::<_, BlockNumber>(&client, EXECUTION_RECEIPT_START.to_vec().as_slice())
507 .unwrap()
508 };
509
510 let hashes_at = |number: BlockNumber| {
511 load_decode::<_, Vec<Hash>>(
512 &client,
513 (EXECUTION_RECEIPT_BLOCK_NUMBER, number).encode().as_slice(),
514 )
515 .unwrap()
516 };
517
518 let receipt_at = |consensus_block_hash: Hash| {
519 load_execution_receipt::<_, Block, CBlock>(&client, consensus_block_hash).unwrap()
520 };
521
522 let write_receipt_at = |oldest_unconfirmed_receipt_number: Option<BlockNumber>,
523 receipt: &ExecutionReceipt| {
524 write_execution_receipt::<_, Block, CBlock>(
525 &client,
526 oldest_unconfirmed_receipt_number,
527 receipt,
528 challenge_period,
529 )
530 .unwrap()
531 };
532
533 let target_receipt_is_pruned = |number: BlockNumber| hashes_at(number).is_none();
534
535 assert_eq!(receipt_start(), None);
536
537 let receipt_count = PRUNING_DEPTH + block_tree_pruning_depth - 1;
540
541 let block_hash_list = (1..=receipt_count)
542 .map(|block_number| {
543 let receipt = create_execution_receipt(block_number);
544 let consensus_block_hash = *receipt.consensus_block_hash();
545 write_receipt_at(Some(One::one()), &receipt);
546 assert_eq!(receipt_at(consensus_block_hash), Some(receipt));
547 assert_eq!(hashes_at(block_number), Some(vec![consensus_block_hash]));
548 assert_eq!(receipt_start(), Some(0));
550 consensus_block_hash
551 })
552 .collect::<Vec<_>>();
553
554 assert_eq!(receipt_start(), Some(0));
555 assert!(!target_receipt_is_pruned(1));
556
557 let receipt = create_execution_receipt(receipt_count + 1);
559 assert!(receipt_at(*receipt.consensus_block_hash()).is_none());
560 write_receipt_at(Some(One::one()), &receipt);
561
562 let receipt = create_execution_receipt(receipt_count + 2);
564 write_receipt_at(Some(One::one()), &receipt);
565
566 assert!(receipt_at(block_hash_list[0]).is_some());
569 assert!(hashes_at(1).is_some());
570 assert!(!target_receipt_is_pruned(1));
571 assert_eq!(receipt_start(), Some(0));
572
573 let receipt = create_execution_receipt(receipt_count + 3);
575 write_receipt_at(Some(One::one()), &receipt);
576
577 let receipt = create_execution_receipt(receipt_count + 4);
579 write_receipt_at(
580 Some(PRUNING_DEPTH + 4), &receipt,
582 );
583 assert!(receipt_at(block_hash_list[0]).is_none());
584 (1..=3).for_each(|pruned| {
586 assert!(hashes_at(pruned).is_none());
587 assert!(target_receipt_is_pruned(pruned));
588 });
589 assert_eq!(receipt_start(), Some(4));
590 }
591}