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::traits::{
9 Block as BlockT, CheckedMul, CheckedSub, NumberFor, One, SaturatedConversion, Zero,
10};
11use sp_runtime::Saturating;
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: {:?}",
204 best_domain_hash
205 )))?;
206 let mut domain_hash_keys =
207 get_tracked_domain_hash_keys::<_, Block, CBlock>(&**domain_client, best_domain_number)?
208 .unwrap_or_default();
209
210 domain_hash_keys.insert((best_domain_hash, latest_consensus_hash));
211
212 domain_client.insert_aux(
213 &[
214 (
215 (LATEST_CONSENSUS_HASH, best_domain_hash)
216 .encode()
217 .as_slice(),
218 latest_consensus_hash.encode().as_slice(),
219 ),
220 (
221 (BEST_DOMAIN_HASH, latest_consensus_hash)
222 .encode()
223 .as_slice(),
224 best_domain_hash.encode().as_slice(),
225 ),
226 (
227 (BEST_DOMAIN_HASH_KEYS, best_domain_number)
228 .encode()
229 .as_slice(),
230 domain_hash_keys.encode().as_slice(),
231 ),
232 ],
233 vec![],
234 )?;
235
236 if cleanup {
237 cleanup_domain_hash_and_consensus_hash::<_, Block, CBlock>(domain_client)?;
238 }
239
240 Ok(())
241}
242
243fn cleanup_domain_hash_and_consensus_hash<Client, Block, CBlock>(
244 domain_client: &Arc<Client>,
245) -> ClientResult<()>
246where
247 CBlock: BlockT,
248 Block: BlockT,
249 Client: HeaderBackend<Block> + AuxStore,
250{
251 let mut finalized_domain_number = domain_client.info().finalized_number;
252
253 let mut deletions = vec![];
254 while finalized_domain_number > Zero::zero()
255 && let Some(domain_hash_keys) = &get_tracked_domain_hash_keys::<_, Block, CBlock>(
257 &**domain_client,
258 finalized_domain_number,
259 )?
260 {
261 domain_hash_keys
262 .iter()
263 .for_each(|(domain_hash, consensus_hash)| {
264 deletions.push((LATEST_CONSENSUS_HASH, domain_hash).encode());
265 deletions.push((BEST_DOMAIN_HASH, consensus_hash).encode())
266 });
267
268 deletions.push((BEST_DOMAIN_HASH_KEYS, finalized_domain_number).encode());
269
270 finalized_domain_number = match finalized_domain_number.checked_sub(&One::one()) {
271 None => break,
272 Some(number) => number,
273 }
274 }
275
276 domain_client.insert_aux(
277 [],
278 &deletions
279 .iter()
280 .map(|key| key.as_slice())
281 .collect::<Vec<_>>(),
282 )
283}
284
285pub(super) fn best_domain_hash_for<Backend, Hash, CHash>(
286 backend: &Backend,
287 consensus_hash: &CHash,
288) -> ClientResult<Option<Hash>>
289where
290 Backend: AuxStore,
291 Hash: Decode,
292 CHash: Encode,
293{
294 load_decode(
295 backend,
296 (BEST_DOMAIN_HASH, consensus_hash).encode().as_slice(),
297 )
298}
299
300pub(super) fn latest_consensus_block_hash_for<Backend, Hash, CHash>(
301 backend: &Backend,
302 domain_hash: &Hash,
303) -> ClientResult<Option<CHash>>
304where
305 Backend: AuxStore,
306 Hash: Encode,
307 CHash: Decode,
308{
309 load_decode(
310 backend,
311 (LATEST_CONSENSUS_HASH, domain_hash).encode().as_slice(),
312 )
313}
314
315#[derive(Encode, Decode, Debug, PartialEq)]
317pub(super) enum BundleMismatchType {
318 GoodInvalid(InvalidBundleType),
322 BadInvalid(InvalidBundleType),
325 ValidBundleContents,
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use domain_test_service::evm_domain_test_runtime::Block;
334 use parking_lot::Mutex;
335 use sp_core::hash::H256;
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 =
343 sp_domains::ExecutionReceipt<BlockNumber, Hash, BlockNumber, Hash, Balance>;
344
345 fn create_execution_receipt(consensus_block_number: BlockNumber) -> ExecutionReceipt {
346 ExecutionReceipt {
347 domain_block_number: consensus_block_number,
348 domain_block_hash: H256::random(),
349 domain_block_extrinsic_root: H256::random(),
350 parent_domain_block_receipt_hash: H256::random(),
351 consensus_block_number,
352 consensus_block_hash: H256::random(),
353 inboxed_bundles: Vec::new(),
354 final_state_root: Default::default(),
355 execution_trace: Default::default(),
356 execution_trace_root: Default::default(),
357 block_fees: Default::default(),
358 transfers: Default::default(),
359 }
360 }
361
362 #[derive(Default)]
363 struct TestClient(Mutex<HashMap<Vec<u8>, Vec<u8>>>);
364
365 impl AuxStore for TestClient {
366 fn insert_aux<
367 'a,
368 'b: 'a,
369 'c: 'a,
370 I: IntoIterator<Item = &'a (&'c [u8], &'c [u8])>,
371 D: IntoIterator<Item = &'a &'b [u8]>,
372 >(
373 &self,
374 insert: I,
375 delete: D,
376 ) -> sp_blockchain::Result<()> {
377 let mut map = self.0.lock();
378 for d in delete {
379 map.remove(&d.to_vec());
380 }
381 for (k, v) in insert {
382 map.insert(k.to_vec(), v.to_vec());
383 }
384 Ok(())
385 }
386
387 fn get_aux(&self, key: &[u8]) -> sp_blockchain::Result<Option<Vec<u8>>> {
388 Ok(self.0.lock().get(key).cloned())
389 }
390 }
391
392 #[test]
393 fn normal_prune_execution_receipt_works() {
394 let block_tree_pruning_depth = 256;
395 let challenge_period = 500;
396 let client = TestClient::default();
397
398 let receipt_start = || {
399 load_decode::<_, BlockNumber>(&client, EXECUTION_RECEIPT_START.to_vec().as_slice())
400 .unwrap()
401 };
402
403 let hashes_at = |number: BlockNumber| {
404 load_decode::<_, Vec<Hash>>(
405 &client,
406 (EXECUTION_RECEIPT_BLOCK_NUMBER, number).encode().as_slice(),
407 )
408 .unwrap()
409 };
410
411 let target_receipt_is_pruned = |number: BlockNumber| hashes_at(number).is_none();
412
413 let receipt_at = |consensus_block_hash: Hash| {
414 load_execution_receipt::<_, Block, CBlock>(&client, consensus_block_hash).unwrap()
415 };
416
417 let write_receipt_at = |oldest_unconfirmed_receipt_number: Option<BlockNumber>,
418 receipt: &ExecutionReceipt| {
419 write_execution_receipt::<_, Block, CBlock>(
420 &client,
421 oldest_unconfirmed_receipt_number,
422 receipt,
423 challenge_period,
424 )
425 .unwrap()
426 };
427
428 assert_eq!(receipt_start(), None);
429
430 let receipt_count = PRUNING_DEPTH + block_tree_pruning_depth - 1;
432 let block_hash_list = (1..=receipt_count)
433 .map(|block_number| {
434 let receipt = create_execution_receipt(block_number);
435 let consensus_block_hash = receipt.consensus_block_hash;
436 let oldest_unconfirmed_receipt_number = block_number
437 .checked_sub(block_tree_pruning_depth)
438 .map(|n| n + 1);
439 write_receipt_at(oldest_unconfirmed_receipt_number, &receipt);
440 assert_eq!(receipt_at(consensus_block_hash), Some(receipt));
441 assert_eq!(hashes_at(block_number), Some(vec![consensus_block_hash]));
442 assert_eq!(receipt_start(), Some(0));
444 consensus_block_hash
445 })
446 .collect::<Vec<_>>();
447
448 assert_eq!(receipt_start(), Some(0));
449 assert!(!target_receipt_is_pruned(1));
450
451 let receipt = create_execution_receipt(receipt_count + 1);
453 assert!(receipt_at(receipt.consensus_block_hash).is_none());
454 write_receipt_at(Some(PRUNING_DEPTH + 1), &receipt);
455 assert!(receipt_at(receipt.consensus_block_hash).is_some());
456 assert_eq!(receipt_start(), Some(1));
457
458 let receipt = create_execution_receipt(receipt_count + 2);
460 write_receipt_at(Some(PRUNING_DEPTH + 2), &receipt);
461 assert!(receipt_at(receipt.consensus_block_hash).is_some());
462
463 assert!(receipt_at(block_hash_list[0]).is_none());
465 assert!(hashes_at(1).is_none());
466 assert!(target_receipt_is_pruned(1));
467 assert_eq!(receipt_start(), Some(2));
468
469 let receipt = create_execution_receipt(receipt_count + 3);
471 let consensus_block_hash1 = receipt.consensus_block_hash;
472 write_receipt_at(Some(PRUNING_DEPTH + 3), &receipt);
473 assert!(receipt_at(consensus_block_hash1).is_some());
474 assert!(receipt_at(block_hash_list[1]).is_none());
476 assert!(target_receipt_is_pruned(2));
477 assert!(!target_receipt_is_pruned(3));
478 assert_eq!(receipt_start(), Some(3));
479
480 let receipt = create_execution_receipt(receipt_count + 3);
482 let consensus_block_hash2 = receipt.consensus_block_hash;
483 write_receipt_at(Some(PRUNING_DEPTH + 3), &receipt);
484 assert!(receipt_at(consensus_block_hash2).is_some());
485 assert_eq!(
486 hashes_at(receipt_count + 3),
487 Some(vec![consensus_block_hash1, consensus_block_hash2])
488 );
489 assert!(!target_receipt_is_pruned(3));
491 assert_eq!(receipt_start(), Some(3));
492 }
493
494 #[test]
495 fn execution_receipts_should_be_kept_against_oldest_unconfirmed_receipt_number() {
496 let block_tree_pruning_depth = 256;
497 let challenge_period = 500;
498 let client = TestClient::default();
499
500 let receipt_start = || {
501 load_decode::<_, BlockNumber>(&client, EXECUTION_RECEIPT_START.to_vec().as_slice())
502 .unwrap()
503 };
504
505 let hashes_at = |number: BlockNumber| {
506 load_decode::<_, Vec<Hash>>(
507 &client,
508 (EXECUTION_RECEIPT_BLOCK_NUMBER, number).encode().as_slice(),
509 )
510 .unwrap()
511 };
512
513 let receipt_at = |consensus_block_hash: Hash| {
514 load_execution_receipt::<_, Block, CBlock>(&client, consensus_block_hash).unwrap()
515 };
516
517 let write_receipt_at = |oldest_unconfirmed_receipt_number: Option<BlockNumber>,
518 receipt: &ExecutionReceipt| {
519 write_execution_receipt::<_, Block, CBlock>(
520 &client,
521 oldest_unconfirmed_receipt_number,
522 receipt,
523 challenge_period,
524 )
525 .unwrap()
526 };
527
528 let target_receipt_is_pruned = |number: BlockNumber| hashes_at(number).is_none();
529
530 assert_eq!(receipt_start(), None);
531
532 let receipt_count = PRUNING_DEPTH + block_tree_pruning_depth - 1;
535
536 let block_hash_list = (1..=receipt_count)
537 .map(|block_number| {
538 let receipt = create_execution_receipt(block_number);
539 let consensus_block_hash = receipt.consensus_block_hash;
540 write_receipt_at(Some(One::one()), &receipt);
541 assert_eq!(receipt_at(consensus_block_hash), Some(receipt));
542 assert_eq!(hashes_at(block_number), Some(vec![consensus_block_hash]));
543 assert_eq!(receipt_start(), Some(0));
545 consensus_block_hash
546 })
547 .collect::<Vec<_>>();
548
549 assert_eq!(receipt_start(), Some(0));
550 assert!(!target_receipt_is_pruned(1));
551
552 let receipt = create_execution_receipt(receipt_count + 1);
554 assert!(receipt_at(receipt.consensus_block_hash).is_none());
555 write_receipt_at(Some(One::one()), &receipt);
556
557 let receipt = create_execution_receipt(receipt_count + 2);
559 write_receipt_at(Some(One::one()), &receipt);
560
561 assert!(receipt_at(block_hash_list[0]).is_some());
564 assert!(hashes_at(1).is_some());
565 assert!(!target_receipt_is_pruned(1));
566 assert_eq!(receipt_start(), Some(0));
567
568 let receipt = create_execution_receipt(receipt_count + 3);
570 write_receipt_at(Some(One::one()), &receipt);
571
572 let receipt = create_execution_receipt(receipt_count + 4);
574 write_receipt_at(
575 Some(PRUNING_DEPTH + 4), &receipt,
577 );
578 assert!(receipt_at(block_hash_list[0]).is_none());
579 (1..=3).for_each(|pruned| {
581 assert!(hashes_at(pruned).is_none());
582 assert!(target_receipt_is_pruned(pruned));
583 });
584 assert_eq!(receipt_start(), Some(4));
585 }
586}