Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

BABE's revert procedure #11022

Merged
merged 15 commits into from
Mar 24, 2022
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion bin/node-template/node/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ pub fn run() -> sc_cli::Result<()> {
runner.async_run(|config| {
let PartialComponents { client, task_manager, backend, .. } =
service::new_partial(&config)?;
Ok((cmd.run(client, backend), task_manager))
Ok((cmd.run(client, backend, None), task_manager))
})
},
Some(Subcommand::Benchmark(cmd)) =>
Expand Down
9 changes: 8 additions & 1 deletion bin/node/cli/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,14 @@ pub fn run() -> Result<()> {
let runner = cli.create_runner(cmd)?;
runner.async_run(|config| {
let PartialComponents { client, task_manager, backend, .. } = new_partial(&config)?;
Ok((cmd.run(client, backend), task_manager))
let client_clone = client.clone();
let revert_aux = Box::new(move |blocks| {
sc_consensus_babe::revert(client_clone, blocks)?;
// TODO: grandpa
Ok(())
});
wigy-opensource-developer marked this conversation as resolved.
Show resolved Hide resolved

Ok((cmd.run(client, backend, Some(revert_aux)), task_manager))
})
},
#[cfg(feature = "try-runtime")]
Expand Down
15 changes: 13 additions & 2 deletions client/cli/src/commands/revert_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use crate::{
use clap::Parser;
use sc_client_api::{Backend, UsageProvider};
use sc_service::chain_ops::revert_chain;
use sp_runtime::traits::{Block as BlockT, Header as HeaderT};
use sp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor};
use std::{fmt::Debug, str::FromStr, sync::Arc};

/// The `revert` command used revert the chain to a previous state.
Expand All @@ -43,16 +43,27 @@ pub struct RevertCmd {
pub pruning_params: PruningParams,
}

/// Revert handler for auxiliary data (e.g. consensus).
type AuxRevertHandler<B> = Box<dyn FnOnce(NumberFor<B>) -> error::Result<()>>;
Copy link
Member Author

@davxy davxy Mar 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The aux data revert can be made atomic by returning a Vec<(Vec<u8>, Option<Vec<u8>>)>.

This vector has the same purpose of BlockImportParams::auxiliary for block import, i.e. perform a bunch of operations on aux data atomically together with blockchain state operations.

The only drawback is that currently the blockchain state is reverted one block at a time (see here). If we want an "atomic" revert of "aux-data + blockchain-state" then we will have to call AuxRevertHandler multiple times as well (once for each reverted block)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am surprised we do not support begin/commit/rollback transaction for batching multiple mutations across aux and main state storage already.


impl RevertCmd {
/// Run the revert command
pub async fn run<B, BA, C>(&self, client: Arc<C>, backend: Arc<BA>) -> error::Result<()>
pub async fn run<B, BA, C>(
&self,
client: Arc<C>,
backend: Arc<BA>,
aux_revert: Option<AuxRevertHandler<B>>,
) -> error::Result<()>
where
B: BlockT,
BA: Backend<B>,
C: UsageProvider<B>,
<<<B as BlockT>::Header as HeaderT>::Number as FromStr>::Err: Debug,
{
let blocks = self.num.parse()?;
if let Some(aux_revert) = aux_revert {
aux_revert(blocks)?;
}
revert_chain(client, backend, blocks)?;

Ok(())
Expand Down
37 changes: 37 additions & 0 deletions client/consensus/babe/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1830,3 +1830,40 @@ where

Ok(BasicQueue::new(verifier, Box::new(block_import), justification_import, spawner, registry))
}

/// Reverts aux data.
pub fn revert<Block, Client>(client: Arc<Client>, blocks: NumberFor<Block>) -> ClientResult<()>
where
Block: BlockT,
Client: AuxStore
+ HeaderMetadata<Block, Error = sp_blockchain::Error>
+ HeaderBackend<Block>
+ ProvideRuntimeApi<Block>
+ UsageProvider<Block>,
Client::Api: BabeApi<Block>,
{
let best_number = client.info().best_number;
let finalized = client.info().finalized_number;
let revertible = blocks.min(best_number - finalized);

let number = best_number.saturating_sub(revertible);
let hash = client
.block_hash_from_id(&BlockId::Number(number))?
.ok_or(ClientError::Backend(format!("Hash lookup failure for block number: {}", number)))?;

let config = Config::get(&*client)?;
let epoch_changes =
aux_schema::load_epoch_changes::<Block, Client>(&*client, config.genesis_config())?;
let mut epoch_changes = epoch_changes.shared_data();

if number == Zero::zero() {
// Special case, no epoch changes data were present on genesis.
*epoch_changes = EpochChangesFor::<Block, Epoch>::default();
} else {
epoch_changes.revert(descendent_query(&*client), hash, number);
}

aux_schema::write_epoch_changes::<Block, _, _>(&epoch_changes, |values| {
client.insert_aux(values, &[])
})
}
71 changes: 71 additions & 0 deletions client/consensus/babe/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,77 @@ fn importing_block_one_sets_genesis_epoch() {
assert_eq!(epoch_for_second_block, genesis_epoch);
}

#[test]
fn revert_prunes_epoch_changes_tree() {
let mut net = BabeTestNet::new(1);

let peer = net.peer(0);
let data = peer.data.as_ref().expect("babe link set up during initialization");

let client = peer.client().as_client();
let mut block_import = data.block_import.lock().take().expect("import set up during init");
let epoch_changes = data.link.epoch_changes.clone();

let mut proposer_factory = DummyFactory {
client: client.clone(),
config: data.link.config.clone(),
epoch_changes: data.link.epoch_changes.clone(),
mutator: Arc::new(|_, _| ()),
};

let mut propose_and_import_blocks_wrap = |parent_id, n| {
propose_and_import_blocks(&client, &mut proposer_factory, &mut block_import, parent_id, n)
};

// Test scenario.
// Information for epoch 19 is produced on three different forks at block #13.
// One branch starts before the revert point (epoch data should be maintained).
// One branch starts after the revert point (epoch data should be removed).
//
// *----------------- F(#13) < fork #2
// /
// A(#1) ---- B(#7) ----#8----+-----#12----- C(#13) ---- D(#19) < canon
// \ ^ \
// \ revert *---- G(#13) -----H(#19) < fork #3
// \ to #10
// *-----E(#7) < fork #1

let canon = propose_and_import_blocks_wrap(BlockId::Number(0), 21);
let fork1 = propose_and_import_blocks_wrap(BlockId::Hash(canon[0]), 10);
let fork2 = propose_and_import_blocks_wrap(BlockId::Hash(canon[7]), 10);
let _fork3 = propose_and_import_blocks_wrap(BlockId::Hash(canon[11]), 10);

// We should be tracking a total of 9 epochs in the fork tree
assert_eq!(epoch_changes.shared_data().tree().iter().count(), 8);
// And only one root
assert_eq!(epoch_changes.shared_data().tree().roots().count(), 1);

// Revert to block #10 (best(22) - 12)
revert(client.clone(), 12).expect("revert should work for the baked test scenario");

// Load and check epoch changes.

let actual_nodes = aux_schema::load_epoch_changes::<Block, TestClient>(
&*client,
data.link.config.genesis_config(),
)
.expect("load epoch changes")
.shared_data()
.tree()
.iter()
.map(|(h, _, _)| *h)
.collect::<Vec<_>>();

let expected_nodes = vec![
canon[0], // A
canon[6], // B
fork2[4], // F
fork1[5], // E
];

assert_eq!(actual_nodes, expected_nodes);
}

#[test]
fn importing_epoch_change_block_prunes_tree() {
let mut net = BabeTestNet::new(1);
Expand Down
54 changes: 45 additions & 9 deletions client/consensus/epochs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -660,15 +660,6 @@ where
parent_number: Number,
slot: E::Slot,
) -> Result<Option<ViableEpochDescriptor<Hash, Number, E>>, fork_tree::Error<D::Error>> {
// find_node_where will give you the node in the fork-tree which is an ancestor
// of the `parent_hash` by default. if the last epoch was signalled at the parent_hash,
// then it won't be returned. we need to create a new fake chain head hash which
// "descends" from our parent-hash.
let fake_head_hash = fake_head_hash(parent_hash);

let is_descendent_of =
descendent_of_builder.build_is_descendent_of(Some((fake_head_hash, *parent_hash)));

if parent_number == Zero::zero() {
// need to insert the genesis epoch.
return Ok(Some(ViableEpochDescriptor::UnimportedGenesis(slot)))
Expand All @@ -683,6 +674,15 @@ where
}
}

// find_node_where will give you the node in the fork-tree which is an ancestor
// of the `parent_hash` by default. if the last epoch was signalled at the parent_hash,
// then it won't be returned. we need to create a new fake chain head hash which
// "descends" from our parent-hash.
let fake_head_hash = fake_head_hash(parent_hash);

let is_descendent_of =
descendent_of_builder.build_is_descendent_of(Some((fake_head_hash, *parent_hash)));

// We want to find the deepest node in the tree which is an ancestor
// of our block and where the start slot of the epoch was before the
// slot of our block. The genesis special-case doesn't need to look
Expand Down Expand Up @@ -798,6 +798,42 @@ where
});
self.epochs.insert((hash, number), persisted);
}

/// Revert to a specified block given its `hash` and `number`.
/// This removes all the epoch changes information that were announced by
/// all the given block descendents.
pub fn revert<D: IsDescendentOfBuilder<Hash>>(
&mut self,
descendent_of_builder: D,
hash: Hash,
number: Number,
) {
let is_descendent_of = descendent_of_builder.build_is_descendent_of(None);

let mut some_removed = false;
let mut predicate = |node_hash: &Hash, node_num: &Number, _: &PersistedEpochHeader<E>| {
if number >= *node_num &&
(is_descendent_of(node_hash, &hash).unwrap_or_default() || *node_hash == hash)
{
// Continue the search in this subtree.
(false, None)
} else if is_descendent_of(&hash, node_hash).unwrap_or_default() {
// Found a node to be removed.
some_removed = true;
(true, None)
} else if some_removed && *node_num < number {
// Backtrack detected, we can early stop the overall filtering operation.
(false, Some(true))
davxy marked this conversation as resolved.
Show resolved Hide resolved
} else {
// Not a parent or child of the one we're looking for, stop processing this branch.
(false, Some(false))
}
};

self.inner.filter(&mut predicate).for_each(|(h, n, _)| {
self.epochs.remove(&(h, n));
});
}
}

/// Type alias to produce the epoch-changes tree from a block type.
Expand Down
107 changes: 102 additions & 5 deletions utils/fork-tree/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,35 @@ where
(None, false) => Ok(FinalizationResult::Unchanged),
}
}

/// Remove from the tree some nodes (and their subtrees) using a `predicate`.
/// The `predicate` should return a tuple where the first `bool` indicates
/// if a node and its subtree should be removed, the second `Option<bool>`
/// indicates if the filtering operation should be stopped.
/// If `Some(false)` stop is requested only for the current node subtree,
/// if `Some(true)` stop is requested for the full filtering operation.
/// Tree traversal is performed using an pre-order depth-first search.
/// An iterator over all the pruned nodes is returned.
pub fn filter<F>(&mut self, predicate: &mut F) -> impl Iterator<Item = (H, N, V)>
davxy marked this conversation as resolved.
Show resolved Hide resolved
where
F: FnMut(&H, &N, &V) -> (bool, Option<bool>),
{
let mut removed = Vec::new();
let mut i = 0;
while i < self.roots.len() {
let (remove, stop) = self.roots[i].filter(predicate, &mut removed);
if remove {
removed.push(self.roots.remove(i));
} else {
i += 1;
}
if stop {
break
}
}
self.rebalance();
RemovedIterator { stack: removed }
}
}

// Workaround for: https://github.com/rust-lang/rust/issues/34537
Expand Down Expand Up @@ -849,6 +878,45 @@ mod node_implementation {
},
}
}

/// Calls a `predicate` for the given node.
/// The `predicate` should return a tuple where the first `bool` indicates
/// if the node and its subtree should be removed, the second `Option<bool>`
/// indicates if the filtering operation should be stopped.
/// If `Some(false)` stop is requested only for the node subtree,
/// if `Some(true)` stop is requested for the full filtering operation.
/// Pruned subtrees are added to the `removed` list.
/// Removal of this node optionally enacted by the caller.
/// Returns a couple of booleans where the fist indicates if a node
/// (and its subtree) should be removed, the second indicates if the
/// overall filtering operation stopped.
pub fn filter<F>(
&mut self,
predicate: &mut F,
removed: &mut Vec<Node<H, N, V>>,
) -> (bool, bool)
where
F: FnMut(&H, &N, &V) -> (bool, Option<bool>),
{
let (remove, pred_stop) = predicate(&self.hash, &self.number, &self.data);
let mut stop = pred_stop == Some(true);
if !remove && pred_stop.is_none() {
let mut i = 0;
while i < self.children.len() {
let (child_remove, child_stop) = self.children[i].filter(predicate, removed);
if child_remove {
removed.push(self.children.remove(i));
} else {
i += 1;
}
if child_stop {
stop = child_stop;
break
}
}
}
(remove, stop)
}
}
}

Expand Down Expand Up @@ -919,11 +987,11 @@ mod test {
// / - G
// / /
// A - F - H - I
// \
// - L - M \
// - O
// \
// J - K
// \ \
// \ - L - M
// \ \
// \ - O
// - J - K
//
// (where N is not a part of fork tree)
//
Expand Down Expand Up @@ -1458,4 +1526,33 @@ mod test {
["A", "F", "H", "L", "O", "P", "M", "I", "G", "B", "C", "D", "E", "J", "K"]
);
}

#[test]
fn tree_filter() {
let (mut tree, _) = test_fork_tree();

let mut predicate = |h: &&str, _: &u64, _: &()| {
match *h {
// Don't remove and continue filtering.
"A" | "B" | "F" => (false, None),
// Don't remove and don't filter the node subtree.
"C" => (false, Some(false)),
// Don't remove and stop the overall filtering operation.
"G" => (false, Some(true)),
// Remove and continue filtering.
"H" => (true, None),
// Should never happen because of the stop and pruning conditions.
_ => panic!("Unexpected filtering for node: {}", *h),
}
};

let removed = tree.filter(&mut predicate);

assert_eq!(
tree.iter().map(|(h, _, _)| *h).collect::<Vec<_>>(),
["A", "B", "C", "D", "E", "F", "G", "J", "K"]
);

assert_eq!(removed.map(|(h, _, _)| h).collect::<Vec<_>>(), vec!["H", "L", "M", "O", "I"]);
}
}