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