1use codec::{Decode, Encode};
30use cumulus_primitives_core::relay_chain::{Header as RelayHeader, SessionIndex};
31use sc_client_api::{
32 backend::AuxStore,
33 client::{AuxDataOperations, FinalityNotification},
34 HeaderBackend,
35};
36use sp_blockchain::{Error as ClientError, Result as ClientResult};
37use sp_runtime::traits::{Block as BlockT, Header as HeaderT, Zero};
38use sp_trie::StorageProof;
39use std::{marker::PhantomData, sync::Arc};
40
41const STORE_VERSION_KEY: &[u8] = b"cumulus_resubmission_store_version";
42const STORE_CURRENT_VERSION: u32 = 2;
43const STORE_ENTRY_PREFIX: &[u8] = b"cumulus_resubmission_store";
44
45#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
47pub struct StoredEntry {
48 pub proof: Arc<StorageProof>,
50 pub relay_parent_header: RelayHeader,
52 pub relay_parent_session: SessionIndex,
54 pub max_pov_size: u32,
57}
58
59fn entry_key<H: Encode>(block_hash: H) -> Vec<u8> {
60 (STORE_ENTRY_PREFIX, block_hash).encode()
61}
62
63pub struct ResubmissionStore<Block: BlockT, B> {
65 backend: Arc<B>,
66 _marker: PhantomData<fn() -> Block>,
67}
68
69impl<Block: BlockT, B> Clone for ResubmissionStore<Block, B> {
70 fn clone(&self) -> Self {
71 Self { backend: self.backend.clone(), _marker: PhantomData }
72 }
73}
74
75impl<Block: BlockT, B> ResubmissionStore<Block, B> {
76 pub fn new(backend: Arc<B>) -> Self {
78 Self { backend, _marker: PhantomData }
79 }
80}
81
82pub fn prepare_resubmission_aux_data<Block: BlockT>(
87 block_hash: Block::Hash,
88 proof: Arc<StorageProof>,
89 relay_parent_header: RelayHeader,
90 relay_parent_session: SessionIndex,
91 max_pov_size: u32,
92) -> impl Iterator<Item = (Vec<u8>, Vec<u8>)> {
93 let encoded_entry =
94 (&proof, &relay_parent_header, &relay_parent_session, &max_pov_size).encode();
95 let encoded_version = STORE_CURRENT_VERSION.encode();
96
97 [(entry_key(block_hash), encoded_entry), (STORE_VERSION_KEY.to_vec(), encoded_version)]
98 .into_iter()
99}
100
101impl<Block: BlockT, B: AuxStore> ResubmissionStore<Block, B> {
102 pub fn load(&self, block_hash: Block::Hash) -> ClientResult<Option<StoredEntry>> {
104 let version = self.decode_aux::<u32>(STORE_VERSION_KEY)?;
105
106 match version {
107 None => Ok(None),
108 Some(STORE_CURRENT_VERSION) => self.decode_aux(entry_key(block_hash).as_slice()),
109 Some(other) => Err(ClientError::Backend(format!(
110 "Unsupported resubmission store DB version: {:?}",
111 other
112 ))),
113 }
114 }
115
116 fn decode_aux<T: Decode>(&self, key: &[u8]) -> ClientResult<Option<T>> {
117 match self.backend.get_aux(key)? {
118 None => Ok(None),
119 Some(t) => T::decode(&mut &t[..]).map(Some).map_err(|e| {
120 ClientError::Backend(format!(
121 "Resubmission store DB is corrupted. Decode error: {}",
122 e
123 ))
124 }),
125 }
126 }
127}
128
129pub fn prune_finalized_entries<Block, B>(
132 backend: &B,
133 notification: &FinalityNotification<Block>,
134) -> ClientResult<()>
135where
136 Block: BlockT,
137 B: AuxStore + HeaderBackend<Block>,
138{
139 let ops = finality_cleanup_ops::<Block>(
140 notification.hash,
141 ¬ification.tree_route,
142 notification.stale_blocks.iter().map(|b| b.hash),
143 );
144
145 let deletes: Vec<_> =
146 ops.iter().filter_map(|(k, v)| v.is_none().then_some(k.as_slice())).collect();
147
148 backend.insert_aux(&[], &deletes)
149}
150
151pub fn prune_missed_finalized_entries<Block, B>(backend: &B) -> ClientResult<()>
155where
156 Block: BlockT,
157 B: AuxStore + HeaderBackend<Block>,
158{
159 let mut hash = backend.info().finalized_hash;
160 let mut deletes: Vec<Vec<u8>> = Vec::new();
161
162 while let Some(header) = backend.header(hash)? {
163 let key = entry_key(hash);
164 if backend.get_aux(&key)?.is_none() {
166 break;
167 }
168 deletes.push(key);
169 if header.number().is_zero() {
170 break;
171 }
172 hash = *header.parent_hash();
173 }
174
175 if !deletes.is_empty() {
176 let delete_refs: Vec<&[u8]> = deletes.iter().map(|k| k.as_slice()).collect();
177 backend.insert_aux(&[], &delete_refs)?;
178 }
179
180 Ok(())
181}
182
183fn finality_cleanup_ops<Block: BlockT>(
189 just_finalized_hash: Block::Hash,
190 tree_route: &[Block::Hash],
191 stale_block_hashes: impl IntoIterator<Item = Block::Hash>,
192) -> AuxDataOperations {
193 let stale_iter = stale_block_hashes.into_iter();
194
195 let mut ops = Vec::with_capacity(stale_iter.size_hint().0 + tree_route.len() + 1);
196 ops.extend(stale_iter.map(|hash| (entry_key(hash), None)));
197 ops.extend(tree_route.iter().map(|hash| (entry_key(hash), None)));
198 ops.push((entry_key(just_finalized_hash), None));
199
200 ops
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206 use sc_client_api::backend::AuxStore;
207
208 type Block = substrate_test_runtime::Block;
209 type Hash = <Block as BlockT>::Hash;
210 type TestBackend = sc_client_api::in_mem::Backend<Block>;
211 type Store = ResubmissionStore<Block, TestBackend>;
212
213 fn test_relay_header(number: u32) -> RelayHeader {
214 RelayHeader {
215 parent_hash: Default::default(),
216 number,
217 state_root: Default::default(),
218 extrinsics_root: Default::default(),
219 digest: Default::default(),
220 }
221 }
222
223 fn create_test_entry() -> StoredEntry {
224 StoredEntry {
225 proof: Arc::new(StorageProof::new(vec![vec![1, 2, 3], vec![4, 5, 6]])),
226 relay_parent_header: test_relay_header(7),
227 relay_parent_session: 1,
228 max_pov_size: 5_242_880,
229 }
230 }
231
232 fn new_store() -> (Arc<TestBackend>, Store) {
233 let backend = Arc::new(TestBackend::new());
234 let store = Store::new(backend.clone());
235 (backend, store)
236 }
237
238 fn write_via_store(backend: &Arc<TestBackend>, hash: Hash, entry: &StoredEntry) {
239 let pairs: Vec<_> = prepare_resubmission_aux_data::<Block>(
240 hash,
241 entry.proof.clone(),
242 entry.relay_parent_header.clone(),
243 entry.relay_parent_session,
244 entry.max_pov_size,
245 )
246 .collect();
247 let insert_pairs: Vec<_> =
248 pairs.iter().map(|(k, v)| (k.as_slice(), v.as_slice())).collect();
249 AuxStore::insert_aux(&**backend, &insert_pairs, &[]).expect("aux insert should succeed");
250 }
251
252 #[test]
253 fn prepare_produces_expected_key_value_pairs() {
254 let hash = Hash::repeat_byte(0xAB);
255 let proof = Arc::new(StorageProof::new(vec![vec![10, 20, 30]]));
256 let relay_parent_header = test_relay_header(42);
257 let relay_parent_session = 42;
258 let max_pov_size = 1_048_576;
259 let pairs: Vec<_> = prepare_resubmission_aux_data::<Block>(
260 hash,
261 proof.clone(),
262 relay_parent_header.clone(),
263 relay_parent_session,
264 max_pov_size,
265 )
266 .collect();
267
268 assert_eq!(pairs.len(), 2);
269
270 let expected_key = (STORE_ENTRY_PREFIX, hash).encode();
271 assert_eq!(pairs[0].0, expected_key);
272
273 let decoded_entry =
274 StoredEntry::decode(&mut pairs[0].1.as_slice()).expect("entry should decode");
275 assert_eq!(decoded_entry.proof, proof);
276 assert_eq!(decoded_entry.relay_parent_header, relay_parent_header);
277 assert_eq!(decoded_entry.relay_parent_session, relay_parent_session);
278 assert_eq!(decoded_entry.max_pov_size, max_pov_size);
279
280 assert_eq!(pairs[1].0, STORE_VERSION_KEY.to_vec());
281 let decoded_version =
282 u32::decode(&mut pairs[1].1.as_slice()).expect("version should decode");
283 assert_eq!(decoded_version, STORE_CURRENT_VERSION);
284 }
285
286 #[test]
287 fn load_returns_none_when_no_entry_exists() {
288 let (_backend, store) = new_store();
289 let hash = Hash::repeat_byte(0xEF);
290
291 assert_eq!(store.load(hash).expect("load should succeed"), None);
292 }
293
294 #[test]
295 fn cleanup_combines_all_categories() {
296 let stale_1 = Hash::repeat_byte(0xAA);
297 let stale_2 = Hash::repeat_byte(0xBB);
298 let route_1 = Hash::repeat_byte(0xC1);
299 let route_2 = Hash::repeat_byte(0xC2);
300 let just_finalized = Hash::repeat_byte(0xFF);
301
302 let ops =
303 finality_cleanup_ops::<Block>(just_finalized, &[route_1, route_2], [stale_1, stale_2]);
304
305 let keys: Vec<_> = ops.iter().map(|(k, _)| k.clone()).collect();
306
307 assert!(keys.contains(&entry_key(stale_1)));
308 assert!(keys.contains(&entry_key(stale_2)));
309 assert!(keys.contains(&entry_key(route_1)));
310 assert!(keys.contains(&entry_key(route_2)));
311 assert!(keys.contains(&entry_key(just_finalized)));
312
313 assert!(ops.iter().all(|(_, v)| v.is_none()));
314 }
315
316 #[test]
317 fn cleanup_handles_empty_inputs() {
318 let just_finalized = Hash::repeat_byte(0xFF);
319
320 let ops = finality_cleanup_ops::<Block>(just_finalized, &[], std::iter::empty::<Hash>());
321
322 assert_eq!(ops.len(), 1);
323 assert!(ops.iter().all(|(_, v)| v.is_none()));
324 }
325
326 #[test]
327 fn stored_entry_round_trips() {
328 let entry = create_test_entry();
332
333 let encoded = entry.encode();
334 let decoded = StoredEntry::decode(&mut encoded.as_slice()).expect("decode should succeed");
335 assert_eq!(entry, decoded);
336 }
337
338 #[test]
339 fn decode_corrupted_entry_body() {
340 let (backend, store) = new_store();
341 let hash = Hash::repeat_byte(0xAB);
342
343 let version_encoded = STORE_CURRENT_VERSION.encode();
345 AuxStore::insert_aux(&*backend, &[(STORE_VERSION_KEY, version_encoded.as_slice())], &[])
346 .expect("aux insert should succeed");
347
348 let key = entry_key(hash);
350 let bogus_data = vec![0xFF, 0xAA, 0xBB];
351 AuxStore::insert_aux(&*backend, &[(&key[..], bogus_data.as_slice())], &[])
352 .expect("aux insert should succeed");
353
354 let result = store.load(hash);
355 assert!(result.is_err());
356 let err_msg = result.unwrap_err().to_string();
357 assert!(
358 err_msg.contains("DB is corrupted") && err_msg.contains("Decode error"),
359 "unexpected error: {}",
360 err_msg
361 );
362 }
363
364 #[test]
365 fn end_to_end_write_cleanup_load() {
366 let (backend, store) = new_store();
367
368 let hash1 = Hash::repeat_byte(0x01);
369 let hash2 = Hash::repeat_byte(0x02);
370 let hash3 = Hash::repeat_byte(0x03);
371
372 let entry1 = create_test_entry();
373 let entry2 = create_test_entry();
374 let entry3 = create_test_entry();
375
376 write_via_store(&backend, hash1, &entry1);
377 write_via_store(&backend, hash2, &entry2);
378 write_via_store(&backend, hash3, &entry3);
379
380 assert_eq!(store.load(hash1).expect("load"), Some(entry1));
381 assert_eq!(store.load(hash2).expect("load"), Some(entry2));
382 assert_eq!(store.load(hash3).expect("load"), Some(entry3.clone()));
383
384 let ops = finality_cleanup_ops::<Block>(hash1, &[hash2], std::iter::empty::<Hash>());
386 let delete_keys: Vec<_> =
387 ops.iter().filter_map(|(k, v)| v.is_none().then(|| k.as_slice())).collect();
388
389 AuxStore::insert_aux(&*backend, &[], &delete_keys).expect("delete should succeed");
390
391 assert_eq!(store.load(hash1).expect("load"), None, "hash1 should be deleted");
392 assert_eq!(store.load(hash2).expect("load"), None, "hash2 should be deleted");
393 assert_eq!(store.load(hash3).expect("load"), Some(entry3), "hash3 should survive");
394 }
395
396 #[test]
397 fn entries_survive_disk_restart() {
398 use sc_client_db::{
399 Backend as DbBackend, BlocksPruning, DatabaseSettings, DatabaseSource, PruningMode,
400 };
401
402 fn with_backend<R>(
403 path: &std::path::Path,
404 f: impl FnOnce(&Arc<DbBackend<Block>>) -> R,
405 ) -> R {
406 let backend = Arc::new(
407 DbBackend::<Block>::new(
408 DatabaseSettings {
409 trie_cache_maximum_size: Some(16 * 1024 * 1024),
410 state_pruning: Some(PruningMode::ArchiveAll),
411 blocks_pruning: BlocksPruning::KeepAll,
412 pruning_filters: Default::default(),
413 source: DatabaseSource::ParityDb { path: path.to_path_buf() },
414 metrics_registry: None,
415 },
416 0,
417 )
418 .expect("open backend"),
419 );
420 let result = f(&backend);
421 result
423 }
424
425 let tmp = tempfile::tempdir().expect("tempdir");
426 let path = tmp.path();
427
428 let hash_a = Hash::repeat_byte(0x10);
429 let hash_b = Hash::repeat_byte(0x20);
430 let entry_a = create_test_entry();
431 let entry_b = create_test_entry();
432
433 with_backend(path, |backend| {
435 let pairs: Vec<_> = prepare_resubmission_aux_data::<Block>(
436 hash_a,
437 entry_a.proof.clone(),
438 entry_a.relay_parent_header.clone(),
439 entry_a.relay_parent_session,
440 entry_a.max_pov_size,
441 )
442 .chain(prepare_resubmission_aux_data::<Block>(
443 hash_b,
444 entry_b.proof.clone(),
445 entry_b.relay_parent_header.clone(),
446 entry_b.relay_parent_session,
447 entry_b.max_pov_size,
448 ))
449 .collect();
450 let refs: Vec<_> = pairs.iter().map(|(k, v)| (k.as_slice(), v.as_slice())).collect();
451 AuxStore::insert_aux(&**backend, &refs, &[]).expect("aux insert");
452 });
453
454 with_backend(path, |backend| {
456 let store = ResubmissionStore::<Block, _>::new(backend.clone());
457 assert_eq!(store.load(hash_a).expect("load a"), Some(entry_a.clone()));
458 assert_eq!(store.load(hash_b).expect("load b"), Some(entry_b.clone()));
459
460 let ops = finality_cleanup_ops::<Block>(hash_a, &[], std::iter::empty::<Hash>());
461 let delete_keys: Vec<_> =
462 ops.iter().filter_map(|(k, v)| v.is_none().then(|| k.as_slice())).collect();
463 AuxStore::insert_aux(&**backend, &[], &delete_keys).expect("delete");
464 });
465
466 with_backend(path, |backend| {
468 let store = ResubmissionStore::<Block, _>::new(backend.clone());
469 assert_eq!(store.load(hash_a).expect("load a"), None, "hash_a delete must persist");
470 assert_eq!(store.load(hash_b).expect("load b"), Some(entry_b));
471 });
472
473 }
475
476 #[test]
477 fn load_returns_error_on_unsupported_version() {
478 let (backend, store) = new_store();
479 let hash = Hash::repeat_byte(0xAB);
480
481 let unsupported_version = 99u32;
483 let encoded_version = unsupported_version.encode();
484 AuxStore::insert_aux(&*backend, &[(STORE_VERSION_KEY, encoded_version.as_slice())], &[])
485 .expect("aux insert should succeed");
486
487 let result = store.load(hash);
488 assert!(result.is_err());
489 let err_msg = result.unwrap_err().to_string();
490 assert!(
491 err_msg.contains("Unsupported") && err_msg.contains("version"),
492 "unexpected error: {}",
493 err_msg
494 );
495 }
496
497 #[derive(Default)]
501 struct MockBackend {
502 headers: std::collections::HashMap<Hash, <Block as BlockT>::Header>,
503 by_number: std::collections::HashMap<u64, Hash>,
504 finalized: (Hash, u64),
505 aux: std::sync::Mutex<std::collections::HashMap<Vec<u8>, Vec<u8>>>,
506 }
507
508 impl MockBackend {
509 fn push(&mut self, number: u64, parent: Hash) -> Hash {
511 let header = <<Block as BlockT>::Header as HeaderT>::new(
512 number,
513 Default::default(),
514 Default::default(),
515 parent,
516 Default::default(),
517 );
518 let hash = header.hash();
519 self.headers.insert(hash, header);
520 self.by_number.insert(number, hash);
521 hash
522 }
523
524 fn write_entry(&self, hash: Hash, entry: StoredEntry) {
525 let pairs: Vec<_> = prepare_resubmission_aux_data::<Block>(
526 hash,
527 entry.proof,
528 entry.relay_parent_header,
529 entry.relay_parent_session,
530 entry.max_pov_size,
531 )
532 .collect();
533 let refs: Vec<_> = pairs.iter().map(|(k, v)| (k.as_slice(), v.as_slice())).collect();
534 self.insert_aux(&refs, &[]).unwrap();
535 }
536
537 fn has_entry(&self, hash: Hash) -> bool {
538 self.get_aux(&entry_key(hash)).unwrap().is_some()
539 }
540 }
541
542 impl AuxStore for MockBackend {
543 fn insert_aux<
544 'a,
545 'b: 'a,
546 'c: 'a,
547 I: IntoIterator<Item = &'a (&'c [u8], &'c [u8])>,
548 D: IntoIterator<Item = &'a &'b [u8]>,
549 >(
550 &self,
551 insert: I,
552 delete: D,
553 ) -> ClientResult<()> {
554 let mut aux = self.aux.lock().unwrap();
555 for (k, v) in insert {
556 aux.insert(k.to_vec(), v.to_vec());
557 }
558 for k in delete {
559 aux.remove(*k);
560 }
561 Ok(())
562 }
563
564 fn get_aux(&self, key: &[u8]) -> ClientResult<Option<Vec<u8>>> {
565 Ok(self.aux.lock().unwrap().get(key).cloned())
566 }
567 }
568
569 impl HeaderBackend<Block> for MockBackend {
570 fn header(&self, hash: Hash) -> ClientResult<Option<<Block as BlockT>::Header>> {
571 Ok(self.headers.get(&hash).cloned())
572 }
573
574 fn info(&self) -> sp_blockchain::Info<Block> {
575 sp_blockchain::Info {
576 best_hash: self.finalized.0,
577 best_number: self.finalized.1,
578 genesis_hash: self.by_number.get(&0).copied().unwrap_or_default(),
579 finalized_hash: self.finalized.0,
580 finalized_number: self.finalized.1,
581 finalized_state: None,
582 number_leaves: 1,
583 block_gap: None,
584 }
585 }
586
587 fn status(&self, hash: Hash) -> ClientResult<sp_blockchain::BlockStatus> {
588 Ok(if self.headers.contains_key(&hash) {
589 sp_blockchain::BlockStatus::InChain
590 } else {
591 sp_blockchain::BlockStatus::Unknown
592 })
593 }
594
595 fn number(&self, hash: Hash) -> ClientResult<Option<u64>> {
596 Ok(self.headers.get(&hash).map(|h| *h.number()))
597 }
598
599 fn hash(&self, number: u64) -> ClientResult<Option<Hash>> {
600 Ok(self.by_number.get(&number).cloned())
601 }
602 }
603
604 #[test]
605 fn prune_missed_reclaims_entries_finalized_while_down() {
606 let mut backend = MockBackend::default();
607
608 let mut parent = backend.push(0, Default::default());
610 let mut hashes = Vec::new();
611 for number in 1..=5u64 {
612 parent = backend.push(number, parent);
613 hashes.push(parent);
614 }
615 for hash in hashes.iter() {
616 backend.write_entry(*hash, create_test_entry());
617 }
618
619 backend.finalized = (hashes[2], 3);
623 assert!(
624 backend.has_entry(hashes[0]) &&
625 backend.has_entry(hashes[1]) &&
626 backend.has_entry(hashes[2]),
627 "finalized entries leaked",
628 );
629
630 prune_missed_finalized_entries::<Block, _>(&backend).unwrap();
633 assert!(
634 !backend.has_entry(hashes[0]) &&
635 !backend.has_entry(hashes[1]) &&
636 !backend.has_entry(hashes[2]),
637 "finalized entries reclaimed",
638 );
639 assert!(
640 backend.has_entry(hashes[3]) && backend.has_entry(hashes[4]),
641 "unincluded entries kept",
642 );
643 }
644}