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::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 std::collections::HashMap;
336 use subspace_runtime_primitives::{Balance, Hash};
337 use subspace_test_runtime::Block as CBlock;
338
339 const PRUNING_DEPTH: BlockNumber = 1000;
340
341 type ExecutionReceipt =
342 sp_domains::ExecutionReceipt<BlockNumber, Hash, BlockNumber, Hash, Balance>;
343
344 fn create_execution_receipt(consensus_block_number: BlockNumber) -> ExecutionReceipt {
345 ExecutionReceipt {
346 domain_block_number: consensus_block_number,
347 domain_block_hash: H256::random(),
348 domain_block_extrinsic_root: H256::random(),
349 parent_domain_block_receipt_hash: H256::random(),
350 consensus_block_number,
351 consensus_block_hash: H256::random(),
352 inboxed_bundles: Vec::new(),
353 final_state_root: Default::default(),
354 execution_trace: Default::default(),
355 execution_trace_root: Default::default(),
356 block_fees: Default::default(),
357 transfers: Default::default(),
358 }
359 }
360
361 #[derive(Default)]
362 struct TestClient(Mutex<HashMap<Vec<u8>, Vec<u8>>>);
363
364 impl AuxStore for TestClient {
365 fn insert_aux<
366 'a,
367 'b: 'a,
368 'c: 'a,
369 I: IntoIterator<Item = &'a (&'c [u8], &'c [u8])>,
370 D: IntoIterator<Item = &'a &'b [u8]>,
371 >(
372 &self,
373 insert: I,
374 delete: D,
375 ) -> sp_blockchain::Result<()> {
376 let mut map = self.0.lock();
377 for d in delete {
378 map.remove(&d.to_vec());
379 }
380 for (k, v) in insert {
381 map.insert(k.to_vec(), v.to_vec());
382 }
383 Ok(())
384 }
385
386 fn get_aux(&self, key: &[u8]) -> sp_blockchain::Result<Option<Vec<u8>>> {
387 Ok(self.0.lock().get(key).cloned())
388 }
389 }
390
391 #[test]
392 fn normal_prune_execution_receipt_works() {
393 let block_tree_pruning_depth = 256;
394 let challenge_period = 500;
395 let client = TestClient::default();
396
397 let receipt_start = || {
398 load_decode::<_, BlockNumber>(&client, EXECUTION_RECEIPT_START.to_vec().as_slice())
399 .unwrap()
400 };
401
402 let hashes_at = |number: BlockNumber| {
403 load_decode::<_, Vec<Hash>>(
404 &client,
405 (EXECUTION_RECEIPT_BLOCK_NUMBER, number).encode().as_slice(),
406 )
407 .unwrap()
408 };
409
410 let target_receipt_is_pruned = |number: BlockNumber| hashes_at(number).is_none();
411
412 let receipt_at = |consensus_block_hash: Hash| {
413 load_execution_receipt::<_, Block, CBlock>(&client, consensus_block_hash).unwrap()
414 };
415
416 let write_receipt_at = |oldest_unconfirmed_receipt_number: Option<BlockNumber>,
417 receipt: &ExecutionReceipt| {
418 write_execution_receipt::<_, Block, CBlock>(
419 &client,
420 oldest_unconfirmed_receipt_number,
421 receipt,
422 challenge_period,
423 )
424 .unwrap()
425 };
426
427 assert_eq!(receipt_start(), None);
428
429 let receipt_count = PRUNING_DEPTH + block_tree_pruning_depth - 1;
431 let block_hash_list = (1..=receipt_count)
432 .map(|block_number| {
433 let receipt = create_execution_receipt(block_number);
434 let consensus_block_hash = receipt.consensus_block_hash;
435 let oldest_unconfirmed_receipt_number = block_number
436 .checked_sub(block_tree_pruning_depth)
437 .map(|n| n + 1);
438 write_receipt_at(oldest_unconfirmed_receipt_number, &receipt);
439 assert_eq!(receipt_at(consensus_block_hash), Some(receipt));
440 assert_eq!(hashes_at(block_number), Some(vec![consensus_block_hash]));
441 assert_eq!(receipt_start(), Some(0));
443 consensus_block_hash
444 })
445 .collect::<Vec<_>>();
446
447 assert_eq!(receipt_start(), Some(0));
448 assert!(!target_receipt_is_pruned(1));
449
450 let receipt = create_execution_receipt(receipt_count + 1);
452 assert!(receipt_at(receipt.consensus_block_hash).is_none());
453 write_receipt_at(Some(PRUNING_DEPTH + 1), &receipt);
454 assert!(receipt_at(receipt.consensus_block_hash).is_some());
455 assert_eq!(receipt_start(), Some(1));
456
457 let receipt = create_execution_receipt(receipt_count + 2);
459 write_receipt_at(Some(PRUNING_DEPTH + 2), &receipt);
460 assert!(receipt_at(receipt.consensus_block_hash).is_some());
461
462 assert!(receipt_at(block_hash_list[0]).is_none());
464 assert!(hashes_at(1).is_none());
465 assert!(target_receipt_is_pruned(1));
466 assert_eq!(receipt_start(), Some(2));
467
468 let receipt = create_execution_receipt(receipt_count + 3);
470 let consensus_block_hash1 = receipt.consensus_block_hash;
471 write_receipt_at(Some(PRUNING_DEPTH + 3), &receipt);
472 assert!(receipt_at(consensus_block_hash1).is_some());
473 assert!(receipt_at(block_hash_list[1]).is_none());
475 assert!(target_receipt_is_pruned(2));
476 assert!(!target_receipt_is_pruned(3));
477 assert_eq!(receipt_start(), Some(3));
478
479 let receipt = create_execution_receipt(receipt_count + 3);
481 let consensus_block_hash2 = receipt.consensus_block_hash;
482 write_receipt_at(Some(PRUNING_DEPTH + 3), &receipt);
483 assert!(receipt_at(consensus_block_hash2).is_some());
484 assert_eq!(
485 hashes_at(receipt_count + 3),
486 Some(vec![consensus_block_hash1, consensus_block_hash2])
487 );
488 assert!(!target_receipt_is_pruned(3));
490 assert_eq!(receipt_start(), Some(3));
491 }
492
493 #[test]
494 fn execution_receipts_should_be_kept_against_oldest_unconfirmed_receipt_number() {
495 let block_tree_pruning_depth = 256;
496 let challenge_period = 500;
497 let client = TestClient::default();
498
499 let receipt_start = || {
500 load_decode::<_, BlockNumber>(&client, EXECUTION_RECEIPT_START.to_vec().as_slice())
501 .unwrap()
502 };
503
504 let hashes_at = |number: BlockNumber| {
505 load_decode::<_, Vec<Hash>>(
506 &client,
507 (EXECUTION_RECEIPT_BLOCK_NUMBER, number).encode().as_slice(),
508 )
509 .unwrap()
510 };
511
512 let receipt_at = |consensus_block_hash: Hash| {
513 load_execution_receipt::<_, Block, CBlock>(&client, consensus_block_hash).unwrap()
514 };
515
516 let write_receipt_at = |oldest_unconfirmed_receipt_number: Option<BlockNumber>,
517 receipt: &ExecutionReceipt| {
518 write_execution_receipt::<_, Block, CBlock>(
519 &client,
520 oldest_unconfirmed_receipt_number,
521 receipt,
522 challenge_period,
523 )
524 .unwrap()
525 };
526
527 let target_receipt_is_pruned = |number: BlockNumber| hashes_at(number).is_none();
528
529 assert_eq!(receipt_start(), None);
530
531 let receipt_count = PRUNING_DEPTH + block_tree_pruning_depth - 1;
534
535 let block_hash_list = (1..=receipt_count)
536 .map(|block_number| {
537 let receipt = create_execution_receipt(block_number);
538 let consensus_block_hash = receipt.consensus_block_hash;
539 write_receipt_at(Some(One::one()), &receipt);
540 assert_eq!(receipt_at(consensus_block_hash), Some(receipt));
541 assert_eq!(hashes_at(block_number), Some(vec![consensus_block_hash]));
542 assert_eq!(receipt_start(), Some(0));
544 consensus_block_hash
545 })
546 .collect::<Vec<_>>();
547
548 assert_eq!(receipt_start(), Some(0));
549 assert!(!target_receipt_is_pruned(1));
550
551 let receipt = create_execution_receipt(receipt_count + 1);
553 assert!(receipt_at(receipt.consensus_block_hash).is_none());
554 write_receipt_at(Some(One::one()), &receipt);
555
556 let receipt = create_execution_receipt(receipt_count + 2);
558 write_receipt_at(Some(One::one()), &receipt);
559
560 assert!(receipt_at(block_hash_list[0]).is_some());
563 assert!(hashes_at(1).is_some());
564 assert!(!target_receipt_is_pruned(1));
565 assert_eq!(receipt_start(), Some(0));
566
567 let receipt = create_execution_receipt(receipt_count + 3);
569 write_receipt_at(Some(One::one()), &receipt);
570
571 let receipt = create_execution_receipt(receipt_count + 4);
573 write_receipt_at(
574 Some(PRUNING_DEPTH + 4), &receipt,
576 );
577 assert!(receipt_at(block_hash_list[0]).is_none());
578 (1..=3).for_each(|pruned| {
580 assert!(hashes_at(pruned).is_none());
581 assert!(target_receipt_is_pruned(pruned));
582 });
583 assert_eq!(receipt_start(), Some(4));
584 }
585}