Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A0-2770: Handler unit tests part 3 #1322

Merged
merged 21 commits into from
Aug 4, 2023
253 changes: 252 additions & 1 deletion finality-aleph/src/sync/handler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ where
mod tests {
use std::collections::HashSet;

use super::{DatabaseIO, HandleStateAction, Handler};
use super::{DatabaseIO, HandleStateAction, HandleStateAction::*, Handler};
use crate::{
session::SessionBoundaryInfo,
sync::{
Expand Down Expand Up @@ -675,6 +675,39 @@ mod tests {
response
}

async fn grow_trunk(
handler: &mut TestHandler,
backend: &mut Backend,
notifier: &mut impl ChainStatusNotifier<MockHeader>,
bottom: &MockIdentifier,
length: usize,
) -> MockIdentifier {
let branch: Vec<_> = bottom.random_branch().take(length).collect();
let top = branch.last().expect("should not be empty").id();
for header in branch.iter() {
let block = MockBlock::new(header.clone(), true);
let justification = MockJustification::for_header(header.clone());
handler
.handle_justification_from_user(justification)
.expect("should work");
backend.import_block(block);
match notifier.next().await {
Ok(BlockImported(header)) => {
handler.block_imported(header).expect("should work");
}
_ => panic!("should notify about imported block"),
}
match notifier.next().await {
Ok(BlockFinalized(finalized_header)) => assert_eq!(
header, &finalized_header,
"should finalize the current header"
),
_ => panic!("shoould notify about finalized block"),
}
}
top
}

#[test]
fn creates_dangling_branch() {
let (mut handler, _backend, _notifier, _genesis) = setup();
Expand All @@ -686,13 +719,16 @@ mod tests {
#[tokio::test]
async fn prunes_dangling_branch() {
let (mut handler, _backend, mut notifier, genesis) = setup();

// grow the branch that will be finalized
let branch = grow_light_branch(&mut handler, &genesis, 15, 4);
let top_main = branch.last().expect("branch not empty").id();

// grow the dangling branch that will be pruned
let peer_id = 3;
let (bottom, top) = create_dangling_branch(&mut handler, 10, 20, peer_id);
assert_dangling_branch_required(&handler, &top, &bottom, HashSet::from_iter(vec![peer_id]));

// begin finalizing the main branch
let response = finalizing_response(branch, false, true, true);
let (maybe_id, maybe_error) = handler.handle_request_response(response, 7);
Expand All @@ -702,15 +738,18 @@ mod tests {
"should create new highest justified"
);
assert!(maybe_error.is_none(), "should work");

// check that still not finalized
assert!(
handler.interest_provider().get(&top) != Interest::Uninterested,
"branch should not be pruned"
);

// finalize
while let Ok(BlockImported(header)) = notifier.next().await {
handler.block_imported(header).expect("should work");
}

// check if dangling branch was pruned
assert_eq!(
handler.interest_provider().get(&top),
Expand All @@ -719,6 +758,218 @@ mod tests {
);
}

#[tokio::test]
async fn prunes_dangling_branch_when_connected_below_finalized() {
kostekIV marked this conversation as resolved.
Show resolved Hide resolved
let (mut handler, _backend, mut notifier, genesis) = setup();

// grow the branch that will be finalized
let branch = grow_light_branch(&mut handler, &genesis, 15, 4);
let top_main = branch.last().expect("branch not empty").id();

// grow the dangling branch that will be pruned
let fork_peer = 6;
let fork_bottom = MockIdentifier::new_random(15);
let fork_child = fork_bottom.random_child();
let fork = grow_light_branch(&mut handler, &fork_child.id(), 10, fork_peer);
let fork_top = fork.last().expect("fork not empty").id();

// finalize the main branch
let response = finalizing_response(branch, false, true, true);
let (maybe_id, maybe_error) = handler.handle_request_response(response, 7);
assert_eq!(
maybe_id,
Some(top_main),
"should create new highest justified"
);
assert!(maybe_error.is_none(), "should work");
kostekIV marked this conversation as resolved.
Show resolved Hide resolved
while let Ok(BlockImported(header)) = notifier.next().await {
handler.block_imported(header).expect("should work");
kostekIV marked this conversation as resolved.
Show resolved Hide resolved
}

// check that the fork is still interesting
assert_eq!(
handler.interest_provider().get(&fork_top),
Interest::Required {
know_most: HashSet::from_iter(vec![fork_peer]),
branch_knowledge: LowestId(fork_child.id()),
},
"should be required"
);

// import fork_child that connects the fork to fork_bottom,
// which is at the same height as an already finalized block
let response = finalizing_response(vec![fork_child], true, false, false);
fixxxedpoint marked this conversation as resolved.
Show resolved Hide resolved
let (maybe_id, maybe_error) = handler.handle_request_response(response, 12);
assert!(
maybe_id.is_none(),
"should not create new highest justified"
);
assert!(maybe_error.is_none(), "should work");
kostekIV marked this conversation as resolved.
Show resolved Hide resolved

// check that the fork is pruned
assert_eq!(
handler.interest_provider().get(&fork_top),
Interest::Uninterested,
"should be pruned"
lesniak43 marked this conversation as resolved.
Show resolved Hide resolved
);
}

#[tokio::test]
async fn prunes_dangling_branch_when_connected_to_composted_header() {
kostekIV marked this conversation as resolved.
Show resolved Hide resolved
let (mut handler, _backend, mut notifier, genesis) = setup();

// grow the branch that will be finalized
let branch = grow_light_branch(&mut handler, &genesis, 15, 4);
let top_main = branch.last().expect("branch not empty").id();

// grow the branch that will be pruned in the first place
let fork_bottom = branch[10].id();
let fork = grow_light_branch(&mut handler, &fork_bottom, 15, 5);

// grow the dangling branch that will be pruned
let fork_peer = 6;
let further_fork_bottom = fork.last().expect("branch not empty").id();
let further_fork_child = further_fork_bottom.random_child();
let further_fork = grow_light_branch(&mut handler, &further_fork_child.id(), 10, fork_peer);
let fork_top = further_fork.last().expect("fork not empty").id();

// finalize the main branch
let response = finalizing_response(branch, false, true, true);
let (maybe_id, maybe_error) = handler.handle_request_response(response, 7);
assert_eq!(
maybe_id,
Some(top_main),
"should create new highest justified"
);
assert!(maybe_error.is_none(), "should work");
while let Ok(BlockImported(header)) = notifier.next().await {
handler.block_imported(header).expect("should work");
}

// check if the bottom part of the fork was pruned
assert_eq!(
handler.interest_provider().get(&further_fork_bottom),
Interest::Uninterested,
"branch should be pruned"
);

// check that the fork is still interesting
assert_eq!(
handler.interest_provider().get(&fork_top),
Interest::Required {
know_most: HashSet::from_iter(vec![fork_peer]),
branch_knowledge: LowestId(further_fork_child.id()),
},
"should be required"
);

// check that further_fork_child is higher than top finalized
assert!(
further_fork_child.id().number()
> handler
.state()
.expect("should work")
.top_justification()
.header()
.id()
.number()
);

// import further_fork_child that connects the fork to further_fork_bottom,
// which is composted
let response = finalizing_response(vec![further_fork_child], true, false, false);
let (maybe_id, maybe_error) = handler.handle_request_response(response, 12);
assert!(
maybe_id.is_none(),
"should not create new highest justified"
);
assert!(maybe_error.is_none(), "should work");

// check that the fork is pruned
assert_eq!(
handler.interest_provider().get(&fork_top),
Interest::Uninterested,
"should be pruned"
lesniak43 marked this conversation as resolved.
Show resolved Hide resolved
);
}

#[tokio::test]
async fn syncs_to_a_long_trunk() {
let (mut handler, mut backend, mut notifier, _genesis) = setup();
let (mut syncing_handler, _syncing_backend, mut syncing_notifier, genesis) = setup();
let _top_main = grow_trunk(&mut handler, &mut backend, &mut notifier, &genesis, 2345).await;
let peer_id = 0;
let syncing_peer_id = 1;
loop {
// syncing peer broadcasts the state
let state = syncing_handler.state().expect("should work");

// peer responds
let response = match handler
.handle_state(state, syncing_peer_id)
.expect("should create response")
{
Response(data) => data,
HighestJustified(_) => panic!("should not request anything from the syncing peer"),
Noop => break,
};
let (justification, maybe_justification) = match response {
NetworkData::StateBroadcastResponse(justification, maybe_justification) => {
(justification, maybe_justification)
}
_ => panic!("should create state broadcast response"),
};

// syncing peer processes the response and sends a request
let mut target_id = justification.header().id();
if let Some(justification) = &maybe_justification {
target_id = justification.header().id();
}
let (maybe_id, maybe_error) =
syncing_handler.handle_state_response(justification, maybe_justification, peer_id);
assert!(maybe_error.is_none(), "should work");
match maybe_id {
Some(id) => assert_eq!(id, target_id, "should start requesting target_id"),
None => panic!("should request"),
};
let branch_knowledge = match syncing_handler.interest_provider().get(&target_id) {
Interest::HighestJustified {
know_most,
branch_knowledge,
} => {
assert!(know_most.contains(&peer_id), "should come from the peer");
branch_knowledge
}
_ => panic!("should be highly interested"),
};
let state = syncing_handler.state().expect("should work");
let request = Request::new(target_id.clone(), branch_knowledge, state);

// peer responds
let response_items = match handler.handle_request(request).expect("should work") {
fixxxedpoint marked this conversation as resolved.
Show resolved Hide resolved
Action::Response(items) => items,
_ => panic!("should prepare response"),
};

// syncing peer processes the response
let (maybe_id, maybe_error) =
syncing_handler.handle_request_response(response_items, peer_id);
assert!(maybe_error.is_none(), "should work");
assert!(maybe_id.is_none(), "should already know about target_id");

// syncing peer finalizes received blocks
while let Ok(notification) = syncing_notifier.next().await {
match notification {
BlockImported(header) => {
syncing_handler.block_imported(header).expect("should work")
}
BlockFinalized(header) if header.id() == target_id => break,
_ => (),
kostekIV marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}

#[test]
fn finalizes_imported_and_justified() {
let (mut handler, mut backend, _keep, _genesis) = setup();
Expand Down
22 changes: 18 additions & 4 deletions finality-aleph/src/sync/mock/backend.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::{
collections::{HashMap, VecDeque},
collections::{HashMap, HashSet, VecDeque},
fmt::{Display, Error as FmtError, Formatter},
sync::Arc,
};
Expand All @@ -23,6 +23,7 @@ struct BackendStorage {
session_boundary_info: SessionBoundaryInfo,
blockchain: HashMap<MockIdentifier, MockBlock>,
finalized: Vec<MockIdentifier>,
prune_candidates: HashSet<MockIdentifier>,
}

#[derive(Clone, Debug)]
Expand All @@ -41,6 +42,9 @@ fn is_predecessor(
if header.id().number() != parent.number() + 1 {
break;
}
if parent.number() < maybe_predecessor.number() {
break;
}
if &parent == maybe_predecessor {
return true;
}
Expand Down Expand Up @@ -68,7 +72,8 @@ impl Backend {
notification_sender: UnboundedSender<MockNotification>,
session_boundary_info: SessionBoundaryInfo,
) -> Self {
let header = MockHeader::random_parentless(0);
// genesis has fixed hash to allow creating multiple compatible Backends
let header = MockHeader::genesis();
let id = header.id();

let block = MockBlock {
Expand All @@ -81,6 +86,7 @@ impl Backend {
session_boundary_info,
blockchain: HashMap::from([(id.clone(), block)]),
finalized: vec![id],
prune_candidates: HashSet::new(),
}));

Self {
Expand Down Expand Up @@ -109,8 +115,10 @@ impl Backend {
.id();
let mut storage = self.inner.lock();
let to_prune: Vec<_> = storage
.blockchain
.keys()
// .blockchain
// .keys()
kostekIV marked this conversation as resolved.
Show resolved Hide resolved
.prune_candidates
.iter()
.filter(|id| {
!&storage.finalized.contains(id)
&& !is_predecessor(&storage.blockchain, id, top_finalized_id)
lesniak43 marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -119,6 +127,7 @@ impl Backend {
.collect();
for id in to_prune {
storage.blockchain.remove(&id);
storage.prune_candidates.remove(&id);
}
}
}
Expand Down Expand Up @@ -226,9 +235,13 @@ impl Finalizer<MockJustification> for Backend {
panic!("finalizing a block that is not a child of top finalized.");
}

for id in &blocks_to_finalize {
storage.prune_candidates.remove(id);
}
storage.finalized.extend(blocks_to_finalize);
std::mem::drop(storage);
self.prune();

self.notify_finalized(header);

Ok(())
Expand Down Expand Up @@ -262,6 +275,7 @@ impl BlockImport<MockBlock> for Backend {
return;
}

storage.prune_candidates.insert(block.id());
storage.blockchain.insert(block.id(), block.clone());

self.notify_imported(block.header);
Expand Down
Loading
Loading