1use std::fmt::Debug;
22
23use codec::{Decode, Encode};
24use finality_grandpa::round::State as RoundState;
25use log::{info, warn};
26
27use fork_tree::ForkTree;
28use sc_client_api::backend::AuxStore;
29use sp_blockchain::{Error as ClientError, Result as ClientResult};
30use sp_consensus_grandpa::{AuthorityList, RoundNumber, SetId};
31use sp_runtime::traits::{Block as BlockT, NumberFor};
32
33use crate::{
34 authorities::{
35 AuthoritySet, AuthoritySetChanges, DelayKind, PendingChange, SharedAuthoritySet,
36 },
37 environment::{
38 CompletedRound, CompletedRounds, CurrentRounds, HasVoted, SharedVoterSetState,
39 VoterSetState,
40 },
41 GrandpaJustification, NewAuthoritySet, LOG_TARGET,
42};
43
44const VERSION_KEY: &[u8] = b"grandpa_schema_version";
45const SET_STATE_KEY: &[u8] = b"grandpa_completed_round";
46const CONCLUDED_ROUNDS: &[u8] = b"grandpa_concluded_rounds";
47const AUTHORITY_SET_KEY: &[u8] = b"grandpa_voters";
48const BEST_JUSTIFICATION: &[u8] = b"grandpa_best_justification";
49
50const CURRENT_VERSION: u32 = 3;
51
52#[derive(Debug, Clone, Encode, Decode)]
54#[cfg_attr(test, derive(PartialEq))]
55pub enum V1VoterSetState<H, N> {
56 Paused(RoundNumber, RoundState<H, N>),
58 Live(RoundNumber, RoundState<H, N>),
60}
61
62type V0VoterSetState<H, N> = (RoundNumber, RoundState<H, N>);
63
64#[derive(Debug, Clone, Encode, Decode, PartialEq)]
65struct V0PendingChange<H, N> {
66 next_authorities: AuthorityList,
67 delay: N,
68 canon_height: N,
69 canon_hash: H,
70}
71
72#[derive(Debug, Clone, Encode, Decode, PartialEq)]
73struct V0AuthoritySet<H, N> {
74 current_authorities: AuthorityList,
75 set_id: SetId,
76 pending_changes: Vec<V0PendingChange<H, N>>,
77}
78
79impl<H, N> Into<AuthoritySet<H, N>> for V0AuthoritySet<H, N>
80where
81 H: Clone + Debug + PartialEq,
82 N: Clone + Debug + Ord,
83{
84 fn into(self) -> AuthoritySet<H, N> {
85 let mut pending_standard_changes = ForkTree::new();
86
87 for old_change in self.pending_changes {
88 let new_change = PendingChange {
89 next_authorities: old_change.next_authorities,
90 delay: old_change.delay,
91 canon_height: old_change.canon_height,
92 canon_hash: old_change.canon_hash,
93 delay_kind: DelayKind::Finalized,
94 };
95
96 if let Err(err) = pending_standard_changes.import::<_, ClientError>(
97 new_change.canon_hash.clone(),
98 new_change.canon_height.clone(),
99 new_change,
100 &|_, _| Ok(false),
102 ) {
103 warn!(target: LOG_TARGET, "Error migrating pending authority set change: {}", err);
104 warn!(target: LOG_TARGET, "Node is in a potentially inconsistent state.");
105 }
106 }
107
108 let authority_set = AuthoritySet::new(
109 self.current_authorities,
110 self.set_id,
111 pending_standard_changes,
112 Vec::new(),
113 AuthoritySetChanges::empty(),
114 );
115
116 authority_set.expect("current_authorities is non-empty and weights are non-zero; qed.")
117 }
118}
119
120impl<H, N> Into<AuthoritySet<H, N>> for V2AuthoritySet<H, N>
121where
122 H: Clone + Debug + PartialEq,
123 N: Clone + Debug + Ord,
124{
125 fn into(self) -> AuthoritySet<H, N> {
126 AuthoritySet::new(
127 self.current_authorities,
128 self.set_id,
129 self.pending_standard_changes,
130 self.pending_forced_changes,
131 AuthoritySetChanges::empty(),
132 )
133 .expect("current_authorities is non-empty and weights are non-zero; qed.")
134 }
135}
136
137#[derive(Debug, Clone, Encode, Decode, PartialEq)]
138struct V2AuthoritySet<H, N> {
139 current_authorities: AuthorityList,
140 set_id: u64,
141 pending_standard_changes: ForkTree<H, N, PendingChange<H, N>>,
142 pending_forced_changes: Vec<PendingChange<H, N>>,
143}
144
145pub(crate) fn load_decode<B: AuxStore, T: Decode>(
146 backend: &B,
147 key: &[u8],
148) -> ClientResult<Option<T>> {
149 match backend.get_aux(key)? {
150 None => Ok(None),
151 Some(t) => T::decode(&mut &t[..])
152 .map_err(|e| ClientError::Backend(format!("GRANDPA DB is corrupted: {}", e)))
153 .map(Some),
154 }
155}
156
157pub(crate) struct PersistentData<Block: BlockT> {
159 pub(crate) authority_set: SharedAuthoritySet<Block::Hash, NumberFor<Block>>,
160 pub(crate) set_state: SharedVoterSetState<Block>,
161}
162
163fn migrate_from_version0<Block: BlockT, B, G>(
164 backend: &B,
165 genesis_round: &G,
166) -> ClientResult<Option<(AuthoritySet<Block::Hash, NumberFor<Block>>, VoterSetState<Block>)>>
167where
168 B: AuxStore,
169 G: Fn() -> RoundState<Block::Hash, NumberFor<Block>>,
170{
171 CURRENT_VERSION.using_encoded(|s| backend.insert_aux(&[(VERSION_KEY, s)], &[]))?;
172
173 if let Some(old_set) =
174 load_decode::<_, V0AuthoritySet<Block::Hash, NumberFor<Block>>>(backend, AUTHORITY_SET_KEY)?
175 {
176 let new_set: AuthoritySet<Block::Hash, NumberFor<Block>> = old_set.into();
177 backend.insert_aux(&[(AUTHORITY_SET_KEY, new_set.encode().as_slice())], &[])?;
178
179 let (last_round_number, last_round_state) = match load_decode::<
180 _,
181 V0VoterSetState<Block::Hash, NumberFor<Block>>,
182 >(backend, SET_STATE_KEY)?
183 {
184 Some((number, state)) => (number, state),
185 None => (0, genesis_round()),
186 };
187
188 let set_id = new_set.set_id;
189
190 let base = last_round_state.prevote_ghost.expect(
191 "state is for completed round; completed rounds must have a prevote ghost; qed.",
192 );
193
194 let mut current_rounds = CurrentRounds::<Block>::new();
195 current_rounds.insert(last_round_number + 1, HasVoted::No);
196
197 let set_state = VoterSetState::Live {
198 completed_rounds: CompletedRounds::new(
199 CompletedRound {
200 number: last_round_number,
201 state: last_round_state,
202 votes: Vec::new(),
203 base,
204 },
205 set_id,
206 &new_set,
207 ),
208 current_rounds,
209 };
210
211 backend.insert_aux(&[(SET_STATE_KEY, set_state.encode().as_slice())], &[])?;
212
213 return Ok(Some((new_set, set_state)))
214 }
215
216 Ok(None)
217}
218
219fn migrate_from_version1<Block: BlockT, B, G>(
220 backend: &B,
221 genesis_round: &G,
222) -> ClientResult<Option<(AuthoritySet<Block::Hash, NumberFor<Block>>, VoterSetState<Block>)>>
223where
224 B: AuxStore,
225 G: Fn() -> RoundState<Block::Hash, NumberFor<Block>>,
226{
227 CURRENT_VERSION.using_encoded(|s| backend.insert_aux(&[(VERSION_KEY, s)], &[]))?;
228
229 if let Some(set) =
230 load_decode::<_, AuthoritySet<Block::Hash, NumberFor<Block>>>(backend, AUTHORITY_SET_KEY)?
231 {
232 let set_id = set.set_id;
233
234 let completed_rounds = |number, state, base| {
235 CompletedRounds::new(
236 CompletedRound { number, state, votes: Vec::new(), base },
237 set_id,
238 &set,
239 )
240 };
241
242 let set_state = match load_decode::<_, V1VoterSetState<Block::Hash, NumberFor<Block>>>(
243 backend,
244 SET_STATE_KEY,
245 )? {
246 Some(V1VoterSetState::Paused(last_round_number, set_state)) => {
247 let base = set_state.prevote_ghost
248 .expect("state is for completed round; completed rounds must have a prevote ghost; qed.");
249
250 VoterSetState::Paused {
251 completed_rounds: completed_rounds(last_round_number, set_state, base),
252 }
253 },
254 Some(V1VoterSetState::Live(last_round_number, set_state)) => {
255 let base = set_state.prevote_ghost
256 .expect("state is for completed round; completed rounds must have a prevote ghost; qed.");
257
258 let mut current_rounds = CurrentRounds::<Block>::new();
259 current_rounds.insert(last_round_number + 1, HasVoted::No);
260
261 VoterSetState::Live {
262 completed_rounds: completed_rounds(last_round_number, set_state, base),
263 current_rounds,
264 }
265 },
266 None => {
267 let set_state = genesis_round();
268 let base = set_state.prevote_ghost
269 .expect("state is for completed round; completed rounds must have a prevote ghost; qed.");
270
271 VoterSetState::live(set_id, &set, base)
272 },
273 };
274
275 backend.insert_aux(&[(SET_STATE_KEY, set_state.encode().as_slice())], &[])?;
276
277 return Ok(Some((set, set_state)))
278 }
279
280 Ok(None)
281}
282
283fn migrate_from_version2<Block: BlockT, B, G>(
284 backend: &B,
285 genesis_round: &G,
286) -> ClientResult<Option<(AuthoritySet<Block::Hash, NumberFor<Block>>, VoterSetState<Block>)>>
287where
288 B: AuxStore,
289 G: Fn() -> RoundState<Block::Hash, NumberFor<Block>>,
290{
291 CURRENT_VERSION.using_encoded(|s| backend.insert_aux(&[(VERSION_KEY, s)], &[]))?;
292
293 if let Some(old_set) =
294 load_decode::<_, V2AuthoritySet<Block::Hash, NumberFor<Block>>>(backend, AUTHORITY_SET_KEY)?
295 {
296 let new_set: AuthoritySet<Block::Hash, NumberFor<Block>> = old_set.into();
297 backend.insert_aux(&[(AUTHORITY_SET_KEY, new_set.encode().as_slice())], &[])?;
298
299 let set_state = match load_decode::<_, VoterSetState<Block>>(backend, SET_STATE_KEY)? {
300 Some(state) => state,
301 None => {
302 let state = genesis_round();
303 let base = state.prevote_ghost
304 .expect("state is for completed round; completed rounds must have a prevote ghost; qed.");
305
306 VoterSetState::live(new_set.set_id, &new_set, base)
307 },
308 };
309
310 return Ok(Some((new_set, set_state)))
311 }
312
313 Ok(None)
314}
315
316pub(crate) fn load_persistent<Block: BlockT, B, G>(
318 backend: &B,
319 genesis_hash: Block::Hash,
320 genesis_number: NumberFor<Block>,
321 genesis_authorities: G,
322) -> ClientResult<PersistentData<Block>>
323where
324 B: AuxStore,
325 G: FnOnce() -> ClientResult<AuthorityList>,
326{
327 let version: Option<u32> = load_decode(backend, VERSION_KEY)?;
328
329 let make_genesis_round = move || RoundState::genesis((genesis_hash, genesis_number));
330
331 match version {
332 None => {
333 if let Some((new_set, set_state)) =
334 migrate_from_version0::<Block, _, _>(backend, &make_genesis_round)?
335 {
336 return Ok(PersistentData {
337 authority_set: new_set.into(),
338 set_state: set_state.into(),
339 })
340 }
341 },
342 Some(1) => {
343 if let Some((new_set, set_state)) =
344 migrate_from_version1::<Block, _, _>(backend, &make_genesis_round)?
345 {
346 return Ok(PersistentData {
347 authority_set: new_set.into(),
348 set_state: set_state.into(),
349 })
350 }
351 },
352 Some(2) => {
353 if let Some((new_set, set_state)) =
354 migrate_from_version2::<Block, _, _>(backend, &make_genesis_round)?
355 {
356 return Ok(PersistentData {
357 authority_set: new_set.into(),
358 set_state: set_state.into(),
359 })
360 }
361 },
362 Some(3) => {
363 if let Some(set) = load_decode::<_, AuthoritySet<Block::Hash, NumberFor<Block>>>(
364 backend,
365 AUTHORITY_SET_KEY,
366 )? {
367 let set_state =
368 match load_decode::<_, VoterSetState<Block>>(backend, SET_STATE_KEY)? {
369 Some(state) => state,
370 None => {
371 let state = make_genesis_round();
372 let base = state.prevote_ghost
373 .expect("state is for completed round; completed rounds must have a prevote ghost; qed.");
374
375 VoterSetState::live(set.set_id, &set, base)
376 },
377 };
378
379 return Ok(PersistentData { authority_set: set.into(), set_state: set_state.into() })
380 }
381 },
382 Some(other) =>
383 return Err(ClientError::Backend(format!("Unsupported GRANDPA DB version: {:?}", other))),
384 }
385
386 info!(
388 target: LOG_TARGET,
389 "👴 Loading GRANDPA authority set \
390 from genesis on what appears to be first startup."
391 );
392
393 let genesis_authorities = genesis_authorities()?;
394 let genesis_set = AuthoritySet::genesis(genesis_authorities)
395 .expect("genesis authorities is non-empty; all weights are non-zero; qed.");
396 let state = make_genesis_round();
397 let base = state
398 .prevote_ghost
399 .expect("state is for completed round; completed rounds must have a prevote ghost; qed.");
400
401 let genesis_state = VoterSetState::live(0, &genesis_set, base);
402
403 backend.insert_aux(
404 &[
405 (AUTHORITY_SET_KEY, genesis_set.encode().as_slice()),
406 (SET_STATE_KEY, genesis_state.encode().as_slice()),
407 ],
408 &[],
409 )?;
410
411 Ok(PersistentData { authority_set: genesis_set.into(), set_state: genesis_state.into() })
412}
413
414pub(crate) fn update_authority_set<Block: BlockT, F, R>(
420 set: &AuthoritySet<Block::Hash, NumberFor<Block>>,
421 new_set: Option<&NewAuthoritySet<Block::Hash, NumberFor<Block>>>,
422 write_aux: F,
423) -> R
424where
425 F: FnOnce(&[(&'static [u8], &[u8])]) -> R,
426{
427 let encoded_set = set.encode();
429
430 if let Some(new_set) = new_set {
431 let set_state = VoterSetState::<Block>::live(
435 new_set.set_id,
436 set,
437 (new_set.canon_hash, new_set.canon_number),
438 );
439 let encoded = set_state.encode();
440
441 write_aux(&[(AUTHORITY_SET_KEY, &encoded_set[..]), (SET_STATE_KEY, &encoded[..])])
442 } else {
443 write_aux(&[(AUTHORITY_SET_KEY, &encoded_set[..])])
444 }
445}
446
447pub(crate) fn update_best_justification<Block: BlockT, F, R>(
453 justification: &GrandpaJustification<Block>,
454 write_aux: F,
455) -> R
456where
457 F: FnOnce(&[(&'static [u8], &[u8])]) -> R,
458{
459 let encoded_justification = justification.encode();
460 write_aux(&[(BEST_JUSTIFICATION, &encoded_justification[..])])
461}
462
463pub fn best_justification<B, Block>(
465 backend: &B,
466) -> ClientResult<Option<GrandpaJustification<Block>>>
467where
468 B: AuxStore,
469 Block: BlockT,
470{
471 load_decode::<_, GrandpaJustification<Block>>(backend, BEST_JUSTIFICATION)
472}
473
474pub(crate) fn write_voter_set_state<Block: BlockT, B: AuxStore>(
476 backend: &B,
477 state: &VoterSetState<Block>,
478) -> ClientResult<()> {
479 backend.insert_aux(&[(SET_STATE_KEY, state.encode().as_slice())], &[])
480}
481
482pub(crate) fn write_concluded_round<Block: BlockT, B: AuxStore>(
484 backend: &B,
485 round_data: &CompletedRound<Block>,
486) -> ClientResult<()> {
487 let mut key = CONCLUDED_ROUNDS.to_vec();
488 let round_number = round_data.number;
489 round_number.using_encoded(|n| key.extend(n));
490
491 backend.insert_aux(&[(&key[..], round_data.encode().as_slice())], &[])
492}
493
494#[cfg(test)]
495pub(crate) fn load_authorities<B: AuxStore, H: Decode, N: Decode + Clone + Ord>(
496 backend: &B,
497) -> Option<AuthoritySet<H, N>> {
498 load_decode::<_, AuthoritySet<H, N>>(backend, AUTHORITY_SET_KEY).expect("backend error")
499}
500
501#[cfg(test)]
502mod test {
503 use super::*;
504 use sp_consensus_grandpa::AuthorityId;
505 use sp_core::{crypto::UncheckedFrom, H256};
506 use substrate_test_runtime_client::{self, runtime::Block};
507
508 fn dummy_id() -> AuthorityId {
509 AuthorityId::unchecked_from([1; 32])
510 }
511
512 #[test]
513 fn load_decode_from_v0_migrates_data_format() {
514 let client = substrate_test_runtime_client::new();
515
516 let authorities = vec![(dummy_id(), 100)];
517 let set_id = 3;
518 let round_number: RoundNumber = 42;
519 let round_state = RoundState::<H256, u64> {
520 prevote_ghost: Some((H256::random(), 32)),
521 finalized: None,
522 estimate: None,
523 completable: false,
524 };
525
526 {
527 let authority_set = V0AuthoritySet::<H256, u64> {
528 current_authorities: authorities.clone(),
529 pending_changes: Vec::new(),
530 set_id,
531 };
532
533 let voter_set_state = (round_number, round_state.clone());
534
535 client
536 .insert_aux(
537 &[
538 (AUTHORITY_SET_KEY, authority_set.encode().as_slice()),
539 (SET_STATE_KEY, voter_set_state.encode().as_slice()),
540 ],
541 &[],
542 )
543 .unwrap();
544 }
545
546 assert_eq!(load_decode::<_, u32>(&client, VERSION_KEY).unwrap(), None);
547
548 load_persistent::<substrate_test_runtime_client::runtime::Block, _, _>(
550 &client,
551 H256::random(),
552 0,
553 || unreachable!(),
554 )
555 .unwrap();
556
557 assert_eq!(load_decode::<_, u32>(&client, VERSION_KEY).unwrap(), Some(3));
558
559 let PersistentData { authority_set, set_state, .. } =
560 load_persistent::<substrate_test_runtime_client::runtime::Block, _, _>(
561 &client,
562 H256::random(),
563 0,
564 || unreachable!(),
565 )
566 .unwrap();
567
568 assert_eq!(
569 *authority_set.inner(),
570 AuthoritySet::new(
571 authorities.clone(),
572 set_id,
573 ForkTree::new(),
574 Vec::new(),
575 AuthoritySetChanges::empty(),
576 )
577 .unwrap(),
578 );
579
580 let mut current_rounds = CurrentRounds::<Block>::new();
581 current_rounds.insert(round_number + 1, HasVoted::No);
582
583 assert_eq!(
584 &*set_state.read(),
585 &VoterSetState::Live {
586 completed_rounds: CompletedRounds::new(
587 CompletedRound {
588 number: round_number,
589 state: round_state.clone(),
590 base: round_state.prevote_ghost.unwrap(),
591 votes: vec![],
592 },
593 set_id,
594 &*authority_set.inner(),
595 ),
596 current_rounds,
597 },
598 );
599 }
600
601 #[test]
602 fn load_decode_from_v1_migrates_data_format() {
603 let client = substrate_test_runtime_client::new();
604
605 let authorities = vec![(dummy_id(), 100)];
606 let set_id = 3;
607 let round_number: RoundNumber = 42;
608 let round_state = RoundState::<H256, u64> {
609 prevote_ghost: Some((H256::random(), 32)),
610 finalized: None,
611 estimate: None,
612 completable: false,
613 };
614
615 {
616 let authority_set = AuthoritySet::<H256, u64>::new(
617 authorities.clone(),
618 set_id,
619 ForkTree::new(),
620 Vec::new(),
621 AuthoritySetChanges::empty(),
622 )
623 .unwrap();
624
625 let voter_set_state = V1VoterSetState::Live(round_number, round_state.clone());
626
627 client
628 .insert_aux(
629 &[
630 (AUTHORITY_SET_KEY, authority_set.encode().as_slice()),
631 (SET_STATE_KEY, voter_set_state.encode().as_slice()),
632 (VERSION_KEY, 1u32.encode().as_slice()),
633 ],
634 &[],
635 )
636 .unwrap();
637 }
638
639 assert_eq!(load_decode::<_, u32>(&client, VERSION_KEY).unwrap(), Some(1));
640
641 load_persistent::<substrate_test_runtime_client::runtime::Block, _, _>(
643 &client,
644 H256::random(),
645 0,
646 || unreachable!(),
647 )
648 .unwrap();
649
650 assert_eq!(load_decode::<_, u32>(&client, VERSION_KEY).unwrap(), Some(3));
651
652 let PersistentData { authority_set, set_state, .. } =
653 load_persistent::<substrate_test_runtime_client::runtime::Block, _, _>(
654 &client,
655 H256::random(),
656 0,
657 || unreachable!(),
658 )
659 .unwrap();
660
661 assert_eq!(
662 *authority_set.inner(),
663 AuthoritySet::new(
664 authorities.clone(),
665 set_id,
666 ForkTree::new(),
667 Vec::new(),
668 AuthoritySetChanges::empty(),
669 )
670 .unwrap(),
671 );
672
673 let mut current_rounds = CurrentRounds::<Block>::new();
674 current_rounds.insert(round_number + 1, HasVoted::No);
675
676 assert_eq!(
677 &*set_state.read(),
678 &VoterSetState::Live {
679 completed_rounds: CompletedRounds::new(
680 CompletedRound {
681 number: round_number,
682 state: round_state.clone(),
683 base: round_state.prevote_ghost.unwrap(),
684 votes: vec![],
685 },
686 set_id,
687 &*authority_set.inner(),
688 ),
689 current_rounds,
690 },
691 );
692 }
693
694 #[test]
695 fn load_decode_from_v2_migrates_data_format() {
696 let client = substrate_test_runtime_client::new();
697
698 let authorities = vec![(dummy_id(), 100)];
699 let set_id = 3;
700
701 {
702 let authority_set = V2AuthoritySet::<H256, u64> {
703 current_authorities: authorities.clone(),
704 set_id,
705 pending_standard_changes: ForkTree::new(),
706 pending_forced_changes: Vec::new(),
707 };
708
709 let genesis_state = (H256::random(), 32);
710 let voter_set_state: VoterSetState<substrate_test_runtime_client::runtime::Block> =
711 VoterSetState::live(
712 set_id,
713 &authority_set.clone().into(), genesis_state,
715 );
716
717 client
718 .insert_aux(
719 &[
720 (AUTHORITY_SET_KEY, authority_set.encode().as_slice()),
721 (SET_STATE_KEY, voter_set_state.encode().as_slice()),
722 (VERSION_KEY, 2u32.encode().as_slice()),
723 ],
724 &[],
725 )
726 .unwrap();
727 }
728
729 assert_eq!(load_decode::<_, u32>(&client, VERSION_KEY).unwrap(), Some(2));
730
731 load_persistent::<substrate_test_runtime_client::runtime::Block, _, _>(
733 &client,
734 H256::random(),
735 0,
736 || unreachable!(),
737 )
738 .unwrap();
739
740 assert_eq!(load_decode::<_, u32>(&client, VERSION_KEY).unwrap(), Some(3));
741
742 let PersistentData { authority_set, .. } = load_persistent::<
743 substrate_test_runtime_client::runtime::Block,
744 _,
745 _,
746 >(
747 &client, H256::random(), 0, || unreachable!()
748 )
749 .unwrap();
750
751 assert_eq!(
752 *authority_set.inner(),
753 AuthoritySet::new(
754 authorities.clone(),
755 set_id,
756 ForkTree::new(),
757 Vec::new(),
758 AuthoritySetChanges::empty(),
759 )
760 .unwrap(),
761 );
762 }
763
764 #[test]
765 fn write_read_concluded_rounds() {
766 let client = substrate_test_runtime_client::new();
767 let hash = H256::random();
768 let round_state = RoundState::genesis((hash, 0));
769
770 let completed_round = CompletedRound::<substrate_test_runtime_client::runtime::Block> {
771 number: 42,
772 state: round_state.clone(),
773 base: round_state.prevote_ghost.unwrap(),
774 votes: vec![],
775 };
776
777 assert!(write_concluded_round(&client, &completed_round).is_ok());
778
779 let round_number = completed_round.number;
780 let mut key = CONCLUDED_ROUNDS.to_vec();
781 round_number.using_encoded(|n| key.extend(n));
782
783 assert_eq!(
784 load_decode::<_, CompletedRound::<substrate_test_runtime_client::runtime::Block>>(
785 &client, &key
786 )
787 .unwrap(),
788 Some(completed_round),
789 );
790 }
791}