Skip to content

Commit 2b3c192

Browse files
davxyark0f
authored andcommitted
BABE's revert procedure (paritytech#11022)
* First rough draft for BABE revert * Proper babe revert test * Cleanup * Test trivial cleanup * Fix to make clippy happy * Check polkadot companion * Check cumulus companion * Remove babe's blocks weight on revert * Handle "empty" blockchain edge case * Run companions * Simplify the filter predicate * Saturating sub is not required * Run pipeline * Run pipeline again...
1 parent 04e3375 commit 2b3c192

File tree

7 files changed

+314
-22
lines changed

7 files changed

+314
-22
lines changed

bin/node-template/node/src/command.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ pub fn run() -> sc_cli::Result<()> {
9595
runner.async_run(|config| {
9696
let PartialComponents { client, task_manager, backend, .. } =
9797
service::new_partial(&config)?;
98-
Ok((cmd.run(client, backend), task_manager))
98+
Ok((cmd.run(client, backend, None), task_manager))
9999
})
100100
},
101101
Some(Subcommand::Benchmark(cmd)) =>

bin/node/cli/src/command.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,13 @@ pub fn run() -> Result<()> {
168168
let runner = cli.create_runner(cmd)?;
169169
runner.async_run(|config| {
170170
let PartialComponents { client, task_manager, backend, .. } = new_partial(&config)?;
171-
Ok((cmd.run(client, backend), task_manager))
171+
let revert_aux = Box::new(|client, backend, blocks| {
172+
sc_consensus_babe::revert(client, backend, blocks)?;
173+
// TODO: grandpa revert
174+
Ok(())
175+
});
176+
177+
Ok((cmd.run(client, backend, Some(revert_aux)), task_manager))
172178
})
173179
},
174180
#[cfg(feature = "try-runtime")]

client/cli/src/commands/revert_cmd.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use crate::{
2424
use clap::Parser;
2525
use sc_client_api::{Backend, UsageProvider};
2626
use sc_service::chain_ops::revert_chain;
27-
use sp_runtime::traits::{Block as BlockT, Header as HeaderT};
27+
use sp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor};
2828
use std::{fmt::Debug, str::FromStr, sync::Arc};
2929

3030
/// The `revert` command used revert the chain to a previous state.
@@ -43,16 +43,28 @@ pub struct RevertCmd {
4343
pub pruning_params: PruningParams,
4444
}
4545

46+
/// Revert handler for auxiliary data (e.g. consensus).
47+
type AuxRevertHandler<C, BA, B> =
48+
Box<dyn FnOnce(Arc<C>, Arc<BA>, NumberFor<B>) -> error::Result<()>>;
49+
4650
impl RevertCmd {
4751
/// Run the revert command
48-
pub async fn run<B, BA, C>(&self, client: Arc<C>, backend: Arc<BA>) -> error::Result<()>
52+
pub async fn run<B, BA, C>(
53+
&self,
54+
client: Arc<C>,
55+
backend: Arc<BA>,
56+
aux_revert: Option<AuxRevertHandler<C, BA, B>>,
57+
) -> error::Result<()>
4958
where
5059
B: BlockT,
5160
BA: Backend<B>,
5261
C: UsageProvider<B>,
5362
<<<B as BlockT>::Header as HeaderT>::Number as FromStr>::Err: Debug,
5463
{
5564
let blocks = self.num.parse()?;
65+
if let Some(aux_revert) = aux_revert {
66+
aux_revert(client.clone(), backend.clone(), blocks)?;
67+
}
5668
revert_chain(client, backend, blocks)?;
5769

5870
Ok(())

client/consensus/babe/src/lib.rs

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ use retain_mut::RetainMut;
9292
use schnorrkel::SignatureError;
9393

9494
use sc_client_api::{
95-
backend::AuxStore, AuxDataOperations, BlockchainEvents, FinalityNotification, PreCommitActions,
96-
ProvideUncles, UsageProvider,
95+
backend::AuxStore, AuxDataOperations, Backend as BackendT, BlockchainEvents,
96+
FinalityNotification, PreCommitActions, ProvideUncles, UsageProvider,
9797
};
9898
use sc_consensus::{
9999
block_import::{
@@ -113,7 +113,9 @@ use sc_telemetry::{telemetry, TelemetryHandle, CONSENSUS_DEBUG, CONSENSUS_TRACE}
113113
use sp_api::{ApiExt, ProvideRuntimeApi};
114114
use sp_application_crypto::AppKey;
115115
use sp_block_builder::BlockBuilder as BlockBuilderApi;
116-
use sp_blockchain::{Error as ClientError, HeaderBackend, HeaderMetadata, Result as ClientResult};
116+
use sp_blockchain::{
117+
Backend as _, Error as ClientError, HeaderBackend, HeaderMetadata, Result as ClientResult,
118+
};
117119
use sp_consensus::{
118120
BlockOrigin, CacheKeyId, CanAuthorWith, Environment, Error as ConsensusError, Proposer,
119121
SelectChain,
@@ -1830,3 +1832,74 @@ where
18301832

18311833
Ok(BasicQueue::new(verifier, Box::new(block_import), justification_import, spawner, registry))
18321834
}
1835+
1836+
/// Reverts aux data.
1837+
pub fn revert<Block, Client, Backend>(
1838+
client: Arc<Client>,
1839+
backend: Arc<Backend>,
1840+
blocks: NumberFor<Block>,
1841+
) -> ClientResult<()>
1842+
where
1843+
Block: BlockT,
1844+
Client: AuxStore
1845+
+ HeaderMetadata<Block, Error = sp_blockchain::Error>
1846+
+ HeaderBackend<Block>
1847+
+ ProvideRuntimeApi<Block>
1848+
+ UsageProvider<Block>,
1849+
Client::Api: BabeApi<Block>,
1850+
Backend: BackendT<Block>,
1851+
{
1852+
let best_number = client.info().best_number;
1853+
let finalized = client.info().finalized_number;
1854+
let revertible = blocks.min(best_number - finalized);
1855+
1856+
let number = best_number - revertible;
1857+
let hash = client
1858+
.block_hash_from_id(&BlockId::Number(number))?
1859+
.ok_or(ClientError::Backend(format!(
1860+
"Unexpected hash lookup failure for block number: {}",
1861+
number
1862+
)))?;
1863+
1864+
// Revert epoch changes tree.
1865+
1866+
let config = Config::get(&*client)?;
1867+
let epoch_changes =
1868+
aux_schema::load_epoch_changes::<Block, Client>(&*client, config.genesis_config())?;
1869+
let mut epoch_changes = epoch_changes.shared_data();
1870+
1871+
if number == Zero::zero() {
1872+
// Special case, no epoch changes data were present on genesis.
1873+
*epoch_changes = EpochChangesFor::<Block, Epoch>::default();
1874+
} else {
1875+
epoch_changes.revert(descendent_query(&*client), hash, number);
1876+
}
1877+
1878+
// Remove block weights added after the revert point.
1879+
1880+
let mut weight_keys = HashSet::with_capacity(revertible.saturated_into());
1881+
let leaves = backend.blockchain().leaves()?.into_iter().filter(|&leaf| {
1882+
sp_blockchain::tree_route(&*client, hash, leaf)
1883+
.map(|route| route.retracted().is_empty())
1884+
.unwrap_or_default()
1885+
});
1886+
for leaf in leaves {
1887+
let mut hash = leaf;
1888+
// Insert parent after parent until we don't hit an already processed
1889+
// branch or we reach a direct child of the rollback point.
1890+
while weight_keys.insert(aux_schema::block_weight_key(hash)) {
1891+
let meta = client.header_metadata(hash)?;
1892+
if meta.number <= number + One::one() {
1893+
// We've reached a child of the revert point, stop here.
1894+
break
1895+
}
1896+
hash = client.header_metadata(hash)?.parent;
1897+
}
1898+
}
1899+
let weight_keys: Vec<_> = weight_keys.iter().map(|val| val.as_slice()).collect();
1900+
1901+
// Write epoch changes and remove weights in one shot.
1902+
aux_schema::write_epoch_changes::<Block, _, _>(&epoch_changes, |values| {
1903+
client.insert_aux(values, weight_keys.iter())
1904+
})
1905+
}

client/consensus/babe/src/tests.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,88 @@ fn importing_block_one_sets_genesis_epoch() {
735735
assert_eq!(epoch_for_second_block, genesis_epoch);
736736
}
737737

738+
#[test]
739+
fn revert_prunes_epoch_changes_and_removes_weights() {
740+
let mut net = BabeTestNet::new(1);
741+
742+
let peer = net.peer(0);
743+
let data = peer.data.as_ref().expect("babe link set up during initialization");
744+
745+
let client = peer.client().as_client();
746+
let backend = peer.client().as_backend();
747+
let mut block_import = data.block_import.lock().take().expect("import set up during init");
748+
let epoch_changes = data.link.epoch_changes.clone();
749+
750+
let mut proposer_factory = DummyFactory {
751+
client: client.clone(),
752+
config: data.link.config.clone(),
753+
epoch_changes: data.link.epoch_changes.clone(),
754+
mutator: Arc::new(|_, _| ()),
755+
};
756+
757+
let mut propose_and_import_blocks_wrap = |parent_id, n| {
758+
propose_and_import_blocks(&client, &mut proposer_factory, &mut block_import, parent_id, n)
759+
};
760+
761+
// Test scenario.
762+
// Information for epoch 19 is produced on three different forks at block #13.
763+
// One branch starts before the revert point (epoch data should be maintained).
764+
// One branch starts after the revert point (epoch data should be removed).
765+
//
766+
// *----------------- F(#13) --#18 < fork #2
767+
// /
768+
// A(#1) ---- B(#7) ----#8----+-----#12----- C(#13) ---- D(#19) ------#21 < canon
769+
// \ ^ \
770+
// \ revert *---- G(#13) ---- H(#19) ---#20 < fork #3
771+
// \ to #10
772+
// *-----E(#7)---#11 < fork #1
773+
let canon = propose_and_import_blocks_wrap(BlockId::Number(0), 21);
774+
let fork1 = propose_and_import_blocks_wrap(BlockId::Hash(canon[0]), 10);
775+
let fork2 = propose_and_import_blocks_wrap(BlockId::Hash(canon[7]), 10);
776+
let fork3 = propose_and_import_blocks_wrap(BlockId::Hash(canon[11]), 8);
777+
778+
// We should be tracking a total of 9 epochs in the fork tree
779+
assert_eq!(epoch_changes.shared_data().tree().iter().count(), 8);
780+
// And only one root
781+
assert_eq!(epoch_changes.shared_data().tree().roots().count(), 1);
782+
783+
// Revert canon chain to block #10 (best(21) - 11)
784+
revert(client.clone(), backend, 11).expect("revert should work for baked test scenario");
785+
786+
// Load and check epoch changes.
787+
788+
let actual_nodes = aux_schema::load_epoch_changes::<Block, TestClient>(
789+
&*client,
790+
data.link.config.genesis_config(),
791+
)
792+
.expect("load epoch changes")
793+
.shared_data()
794+
.tree()
795+
.iter()
796+
.map(|(h, _, _)| *h)
797+
.collect::<Vec<_>>();
798+
799+
let expected_nodes = vec![
800+
canon[0], // A
801+
canon[6], // B
802+
fork2[4], // F
803+
fork1[5], // E
804+
];
805+
806+
assert_eq!(actual_nodes, expected_nodes);
807+
808+
let weight_data_check = |hashes: &[Hash], expected: bool| {
809+
hashes.iter().all(|hash| {
810+
aux_schema::load_block_weight(&*client, hash).unwrap().is_some() == expected
811+
})
812+
};
813+
assert!(weight_data_check(&canon[..10], true));
814+
assert!(weight_data_check(&canon[10..], false));
815+
assert!(weight_data_check(&fork1, true));
816+
assert!(weight_data_check(&fork2, true));
817+
assert!(weight_data_check(&fork3, false));
818+
}
819+
738820
#[test]
739821
fn importing_epoch_change_block_prunes_tree() {
740822
let mut net = BabeTestNet::new(1);

client/consensus/epochs/src/lib.rs

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
pub mod migration;
2222

2323
use codec::{Decode, Encode};
24-
use fork_tree::ForkTree;
24+
use fork_tree::{FilterAction, ForkTree};
2525
use sc_client_api::utils::is_descendent_of;
2626
use sp_blockchain::{Error as ClientError, HeaderBackend, HeaderMetadata};
2727
use sp_runtime::traits::{Block as BlockT, NumberFor, One, Zero};
@@ -660,15 +660,6 @@ where
660660
parent_number: Number,
661661
slot: E::Slot,
662662
) -> Result<Option<ViableEpochDescriptor<Hash, Number, E>>, fork_tree::Error<D::Error>> {
663-
// find_node_where will give you the node in the fork-tree which is an ancestor
664-
// of the `parent_hash` by default. if the last epoch was signalled at the parent_hash,
665-
// then it won't be returned. we need to create a new fake chain head hash which
666-
// "descends" from our parent-hash.
667-
let fake_head_hash = fake_head_hash(parent_hash);
668-
669-
let is_descendent_of =
670-
descendent_of_builder.build_is_descendent_of(Some((fake_head_hash, *parent_hash)));
671-
672663
if parent_number == Zero::zero() {
673664
// need to insert the genesis epoch.
674665
return Ok(Some(ViableEpochDescriptor::UnimportedGenesis(slot)))
@@ -683,6 +674,15 @@ where
683674
}
684675
}
685676

677+
// find_node_where will give you the node in the fork-tree which is an ancestor
678+
// of the `parent_hash` by default. if the last epoch was signalled at the parent_hash,
679+
// then it won't be returned. we need to create a new fake chain head hash which
680+
// "descends" from our parent-hash.
681+
let fake_head_hash = fake_head_hash(parent_hash);
682+
683+
let is_descendent_of =
684+
descendent_of_builder.build_is_descendent_of(Some((fake_head_hash, *parent_hash)));
685+
686686
// We want to find the deepest node in the tree which is an ancestor
687687
// of our block and where the start slot of the epoch was before the
688688
// slot of our block. The genesis special-case doesn't need to look
@@ -798,6 +798,37 @@ where
798798
});
799799
self.epochs.insert((hash, number), persisted);
800800
}
801+
802+
/// Revert to a specified block given its `hash` and `number`.
803+
/// This removes all the epoch changes information that were announced by
804+
/// all the given block descendents.
805+
pub fn revert<D: IsDescendentOfBuilder<Hash>>(
806+
&mut self,
807+
descendent_of_builder: D,
808+
hash: Hash,
809+
number: Number,
810+
) {
811+
let is_descendent_of = descendent_of_builder.build_is_descendent_of(None);
812+
813+
let filter = |node_hash: &Hash, node_num: &Number, _: &PersistedEpochHeader<E>| {
814+
if number >= *node_num &&
815+
(is_descendent_of(node_hash, &hash).unwrap_or_default() || *node_hash == hash)
816+
{
817+
// Continue the search in this subtree.
818+
FilterAction::KeepNode
819+
} else if number < *node_num && is_descendent_of(&hash, node_hash).unwrap_or_default() {
820+
// Found a node to be removed.
821+
FilterAction::Remove
822+
} else {
823+
// Not a parent or child of the one we're looking for, stop processing this branch.
824+
FilterAction::KeepTree
825+
}
826+
};
827+
828+
self.inner.drain_filter(filter).for_each(|(h, n, _)| {
829+
self.epochs.remove(&(h, n));
830+
});
831+
}
801832
}
802833

803834
/// Type alias to produce the epoch-changes tree from a block type.

0 commit comments

Comments
 (0)