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-4563: Introduce notion of non-direct parents in Extender #506

Merged
merged 10 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/cargo-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v4

- name: Install cargo audit
shell: bash
run: |
cargo install cargo-audit --locked

- name: Run `cargo-audit`
uses: actions-rs/audit-check@v1
with:
Expand Down
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ More details are available [in the book][reference-link-implementation-details].
- Import AlephBFT in your crate
```toml
[dependencies]
aleph-bft = "^0.39"
aleph-bft = "^0.40"
```
- The main entry point is the `run_session` function, which returns a Future that runs the
consensus algorithm.
Expand Down
2 changes: 1 addition & 1 deletion consensus/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "aleph-bft"
version = "0.39.1"
version = "0.40.0"
edition = "2021"
authors = ["Cardinal Cryptography"]
categories = ["algorithms", "data-structures", "cryptography", "database"]
Expand Down
1 change: 0 additions & 1 deletion consensus/src/dag/reconstruction/dag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ impl<U: UnitWithParents> Dag<U> {
}
let missing_parents = unit
.parents()
.values()
.filter(|parent| !self.dag_units.contains(parent))
.cloned()
.collect();
Expand Down
60 changes: 48 additions & 12 deletions consensus/src/dag/reconstruction/mod.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
use std::collections::HashMap;

use crate::{
units::{ControlHash, FullUnit, HashFor, Unit, UnitCoord, UnitWithParents, WrappedUnit},
Hasher, NodeMap, SessionId,
};
use aleph_bft_rmc::NodeCount;
use std::collections::HashMap;

mod dag;
mod parents;

use aleph_bft_types::{Data, MultiKeychain, OrderedUnit, Signed};
use aleph_bft_types::{Data, MultiKeychain, NodeIndex, OrderedUnit, Round, Signed};
use dag::Dag;
use parents::Reconstruction as ParentReconstruction;

/// A unit with its parents represented explicitly.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct ReconstructedUnit<U: Unit> {
unit: U,
parents: NodeMap<HashFor<U>>,
parents: NodeMap<(HashFor<U>, Round)>,
}

impl<U: Unit> ReconstructedUnit<U> {
Expand All @@ -25,7 +25,18 @@ impl<U: Unit> ReconstructedUnit<U> {
match unit.control_hash().combined_hash
== ControlHash::<U::Hasher>::combine_hashes(&parents)
{
true => Ok(ReconstructedUnit { unit, parents }),
true => {
let unit_round = unit.round();
let mut parents_with_rounds = NodeMap::with_size(parents.size());
for (parent_index, hash) in parents.into_iter() {
// we cannot have here round 0 units
parents_with_rounds.insert(parent_index, (hash, unit_round.saturating_sub(1)));
}
Ok(ReconstructedUnit {
unit,
parents: parents_with_rounds,
})
}
false => Err(unit),
}
}
Expand Down Expand Up @@ -72,8 +83,33 @@ impl<U: Unit> WrappedUnit<U::Hasher> for ReconstructedUnit<U> {
}

impl<U: Unit> UnitWithParents for ReconstructedUnit<U> {
fn parents(&self) -> &NodeMap<HashFor<Self>> {
&self.parents
fn parents(&self) -> impl Iterator<Item = &HashFor<U>> {
self.parents.values().map(|(hash, _)| hash)
}

fn direct_parents(&self) -> impl Iterator<Item = &HashFor<Self>> {
self.parents
.values()
.filter_map(|(hash, parent_round)| match self.unit.coord().round() {
// round 0 units cannot have non-empty parents
0 => None,

unit_round => {
if unit_round - 1 == *parent_round {
Some(hash)
} else {
None
}
}
})
}

fn parent_for(&self, index: NodeIndex) -> Option<&HashFor<Self>> {
self.parents.get(index).map(|(hash, _)| hash)
}

fn node_count(&self) -> NodeCount {
self.parents.size()
}
}

Expand All @@ -89,7 +125,7 @@ impl<D: Data, H: Hasher, K: MultiKeychain> From<ReconstructedUnit<Signed<FullUni
for OrderedUnit<D, H>
{
fn from(unit: ReconstructedUnit<Signed<FullUnit<H, D>, K>>) -> Self {
let parents = unit.parents().values().cloned().collect();
let parents = unit.parents().cloned().collect();
let unit = unit.unpack();
let creator = unit.creator();
let round = unit.round();
Expand Down Expand Up @@ -233,7 +269,7 @@ mod test {
assert_eq!(units.len(), 1);
let reconstructed_unit = units.pop().expect("just checked its there");
assert_eq!(reconstructed_unit, ReconstructedUnit::initial(unit.clone()));
assert_eq!(reconstructed_unit.parents().item_count(), 0);
assert_eq!(reconstructed_unit.parents().count(), 0);
}
}

Expand All @@ -254,15 +290,15 @@ mod test {
match round {
0 => {
assert_eq!(reconstructed_unit, ReconstructedUnit::initial(unit.clone()));
assert_eq!(reconstructed_unit.parents().item_count(), 0);
assert_eq!(reconstructed_unit.parents().count(), 0);
}
round => {
assert_eq!(reconstructed_unit.parents().item_count(), 4);
assert_eq!(reconstructed_unit.parents().count(), 4);
let parents = dag
.get((round - 1) as usize)
.expect("the parents are there");
for (parent, reconstructed_parent) in
parents.iter().zip(reconstructed_unit.parents().values())
parents.iter().zip(reconstructed_unit.parents())
{
assert_eq!(&parent.hash(), reconstructed_parent);
}
Expand Down
12 changes: 6 additions & 6 deletions consensus/src/dag/reconstruction/parents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ mod test {
assert_eq!(units.len(), 1);
let reconstructed_unit = units.pop().expect("just checked its there");
assert_eq!(reconstructed_unit, ReconstructedUnit::initial(unit.clone()));
assert_eq!(reconstructed_unit.parents().item_count(), 0);
assert_eq!(reconstructed_unit.parents().count(), 0);
}
}

Expand All @@ -258,15 +258,15 @@ mod test {
match round {
0 => {
assert_eq!(reconstructed_unit, ReconstructedUnit::initial(unit.clone()));
assert_eq!(reconstructed_unit.parents().item_count(), 0);
assert_eq!(reconstructed_unit.parents().count(), 0);
}
round => {
assert_eq!(reconstructed_unit.parents().item_count(), 4);
assert_eq!(reconstructed_unit.parents().count(), 4);
let parents = dag
.get((round - 1) as usize)
.expect("the parents are there");
for (parent, reconstructed_parent) in
parents.iter().zip(reconstructed_unit.parents().values())
parents.iter().zip(reconstructed_unit.parents())
{
assert_eq!(&parent.hash(), reconstructed_parent);
}
Expand Down Expand Up @@ -371,11 +371,11 @@ mod test {
assert!(requests.is_empty());
assert_eq!(units.len(), 1);
let reconstructed_unit = units.pop().expect("just checked its there");
assert_eq!(reconstructed_unit.parents().item_count(), 4);
assert_eq!(reconstructed_unit.parents().count(), 4);
for (coord, parent_hash) in parent_hashes {
assert_eq!(
Some(&parent_hash),
reconstructed_unit.parents().get(coord.creator())
reconstructed_unit.parent_for(coord.creator())
);
}
}
Expand Down
5 changes: 2 additions & 3 deletions consensus/src/dissemination/responder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ impl<H: Hasher, D: Data, MK: MultiKeychain> Responder<H, D, MK> {
.map(|unit| {
let parents = unit
.parents()
.values()
.map(|parent_hash| {
units
.unit(parent_hash)
Expand Down Expand Up @@ -270,8 +269,8 @@ mod test {
match response {
Response::Parents(response_hash, parents) => {
assert_eq!(response_hash, requested_unit.hash());
assert_eq!(parents.len(), requested_unit.parents().size().0);
for (parent, parent_hash) in zip(parents, requested_unit.parents().values()) {
assert_eq!(parents.len(), requested_unit.parents().count());
for (parent, parent_hash) in zip(parents, requested_unit.parents()) {
assert_eq!(&parent.as_signable().hash(), parent_hash);
}
}
Expand Down
22 changes: 13 additions & 9 deletions consensus/src/extension/election.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::collections::HashMap;
use crate::{
extension::units::Units,
units::{HashFor, UnitWithParents},
Hasher, NodeCount, NodeIndex, NodeMap, Round,
Hasher, NodeCount, NodeIndex, Round,
};

fn common_vote(relative_round: Round) -> bool {
Expand Down Expand Up @@ -49,11 +49,11 @@ impl<U: UnitWithParents> CandidateElection<U> {

fn parent_votes(
&mut self,
parents: &NodeMap<HashFor<U>>,
parents: Vec<HashFor<U>>,
) -> Result<(NodeCount, NodeCount), CandidateOutcome<U::Hasher>> {
let (mut votes_for, mut votes_against) = (NodeCount(0), NodeCount(0));
for parent in parents.values() {
match self.votes.get(parent).expect("units are added in order") {
for parent in parents {
match self.votes.get(&parent).expect("units are added in order") {
true => votes_for += NodeCount(1),
false => votes_against += NodeCount(1),
}
Expand All @@ -63,11 +63,11 @@ impl<U: UnitWithParents> CandidateElection<U> {

fn vote_from_parents(
&mut self,
parents: &NodeMap<HashFor<U>>,
parents: Vec<HashFor<U>>,
threshold: NodeCount,
relative_round: Round,
) -> Result<bool, CandidateOutcome<U::Hasher>> {
use CandidateOutcome::*;
let threshold = parents.size().consensus_threshold();
// Gather parents' votes.
let (votes_for, votes_against) = self.parent_votes(parents)?;
assert!(votes_for + votes_against >= threshold);
Expand Down Expand Up @@ -105,9 +105,13 @@ impl<U: UnitWithParents> CandidateElection<U> {
let vote = match relative_round {
0 => unreachable!("just checked that voter and election rounds are not equal"),
// Direct descendands vote for, all other units of that round against.
1 => voter.parents().get(self.candidate_creator) == Some(&self.candidate_hash),
1 => voter.parent_for(self.candidate_creator) == Some(&self.candidate_hash),
// Otherwise we compute the vote based on the parents' votes.
_ => self.vote_from_parents(voter.parents(), relative_round)?,
_ => {
let threshold = voter.node_count().consensus_threshold();
let direct_parents = voter.direct_parents().cloned().collect();
self.vote_from_parents(direct_parents, threshold, relative_round)?
}
};
self.votes.insert(voter.hash(), vote);
Ok(())
Expand Down Expand Up @@ -360,7 +364,7 @@ mod test {

#[test]
#[ignore]
// TODO(A0-4563) Uncomment after changes to parent voting code
// TODO(A0-4559) Uncomment
fn given_minimal_dag_with_orphaned_node_when_electing_then_orphaned_node_is_not_head() {
use ElectionResult::*;
let mut units = Units::new();
Expand Down
2 changes: 1 addition & 1 deletion consensus/src/extension/extender.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ mod test {

#[test]
#[ignore]
// TODO(A0-4563) Uncomment after changes to parent voting code
// TODO(A0-4559) Uncomment
fn given_minimal_dag_with_orphaned_node_when_producing_batches_have_correct_length() {
let mut extender = Extender::new();
let n_members = NodeCount(4);
Expand Down
4 changes: 2 additions & 2 deletions consensus/src/extension/units.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ impl<U: UnitWithParents> Units<U> {
.expect("head is picked among units we have"),
);
while let Some(u) = queue.pop_front() {
for u_hash in u.parents().clone().into_values() {
if let Some(v) = self.units.remove(&u_hash) {
for u_hash in u.parents() {
if let Some(v) = self.units.remove(u_hash) {
queue.push_back(v);
}
}
Expand Down
8 changes: 3 additions & 5 deletions consensus/src/testing/dag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,18 +139,16 @@ impl DagFeeder {

fn on_reconstructed_unit(&mut self, unit: ReconstructedUnit) {
let h = unit.hash();
let parents = unit.parents();
let parents: HashSet<_> = unit.parents().cloned().collect();
let expected_hashes: HashSet<_> = self
.units_map
.get(&h)
.expect("we have the unit")
.parent_hashes()
.into_iter()
.collect();
assert_eq!(parents.item_count(), expected_hashes.len());
for (_, hash) in parents {
assert!(expected_hashes.contains(hash));
}

assert_eq!(parents, expected_hashes);
self.result.push(unit.clone());
self.store.insert(unit);
}
Expand Down
5 changes: 4 additions & 1 deletion consensus/src/units/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,10 @@ pub trait WrappedUnit<H: Hasher>: Unit<Hasher = H> {
}

pub trait UnitWithParents: Unit {
fn parents(&self) -> &NodeMap<HashFor<Self>>;
fn parents(&self) -> impl Iterator<Item = &HashFor<Self>>;
fn direct_parents(&self) -> impl Iterator<Item = &HashFor<Self>>;
fn parent_for(&self, index: NodeIndex) -> Option<&HashFor<Self>>;
fn node_count(&self) -> NodeCount;
}

impl<H: Hasher, D: Data> Unit for FullUnit<H, D> {
Expand Down
Loading