Skip to content

Commit

Permalink
Forbid appending blocks to forks that are competing with finalized ch…
Browse files Browse the repository at this point in the history
…ain (paritytech#138)

* Error::TryingToFinalizeSibling

* cargo fmt --all

* updated PruningStrategy comment
  • Loading branch information
svyatonik authored and bkchr committed Apr 10, 2024
1 parent a0c8206 commit f5a0014
Show file tree
Hide file tree
Showing 5 changed files with 296 additions and 68 deletions.
3 changes: 3 additions & 0 deletions bridges/modules/ethereum/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ pub enum Error {
TransactionsReceiptsMismatch = 18,
/// Can't accept unsigned header from the far future.
UnsignedTooFarInTheFuture = 19,
/// Trying to finalize sibling of finalized block.
TryingToFinalizeSibling = 20,
}

impl Error {
Expand All @@ -85,6 +87,7 @@ impl Error {
Error::RedundantTransactionsReceipts => "Redundant transactions receipts are provided",
Error::TransactionsReceiptsMismatch => "Invalid transactions receipts provided",
Error::UnsignedTooFarInTheFuture => "The unsigned header is too far in future",
Error::TryingToFinalizeSibling => "Trying to finalize sibling of finalized block",
}
}

Expand Down
90 changes: 73 additions & 17 deletions bridges/modules/ethereum/src/finality.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,13 @@ use sp_std::collections::{
use sp_std::prelude::*;

/// Cached finality votes for given block.
#[derive(RuntimeDebug, Default)]
#[derive(RuntimeDebug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct CachedFinalityVotes<Submitter> {
/// True if we have stopped at best finalized block' sibling. This means
/// that we are trying to finalize block from fork that has forked before
/// best finalized.
pub stopped_at_finalized_sibling: bool,
/// Header ancestors that were read while we have been searching for
/// cached votes entry. Newest header has index 0.
pub unaccounted_ancestry: VecDeque<(HeaderId, Option<Submitter>, Header)>,
Expand Down Expand Up @@ -88,10 +92,15 @@ pub fn finalize_blocks<S: Storage>(
// compute count of voters for every unfinalized block in ancestry
let validators = header_validators.1.iter().collect();
let votes = prepare_votes(
storage.cached_finality_votes(&header.parent_hash, |hash| {
*hash == header_validators.0.hash || *hash == best_finalized.hash
}),
best_finalized.number,
header
.parent_id()
.map(|parent_id| {
storage.cached_finality_votes(&parent_id, &best_finalized, |hash| {
*hash == header_validators.0.hash || *hash == best_finalized.hash
})
})
.unwrap_or_else(|| CachedFinalityVotes::default()),
best_finalized,
&validators,
id,
header,
Expand Down Expand Up @@ -133,12 +142,18 @@ fn is_finalized(
/// Prepare 'votes' of header and its ancestors' signers.
fn prepare_votes<Submitter>(
mut cached_votes: CachedFinalityVotes<Submitter>,
best_finalized_number: u64,
best_finalized: HeaderId,
validators: &BTreeSet<&Address>,
id: HeaderId,
header: &Header,
submitter: Option<Submitter>,
) -> Result<FinalityVotes<Submitter>, Error> {
// if we have reached finalized block sibling, then we're trying
// to switch finalized blocks
if cached_votes.stopped_at_finalized_sibling {
return Err(Error::TryingToFinalizeSibling);
}

// this fn can only work with single validators set
if !validators.contains(&header.author) {
return Err(Error::NotValidator);
Expand All @@ -154,7 +169,7 @@ fn prepare_votes<Submitter>(

// remove votes from finalized blocks
while let Some(old_ancestor) = votes.ancestry.pop_front() {
if old_ancestor.id.number > best_finalized_number {
if old_ancestor.id.number > best_finalized.number {
votes.ancestry.push_front(old_ancestor);
break;
}
Expand Down Expand Up @@ -245,6 +260,16 @@ fn empty_step_signer(empty_step: &SealedEmptyStep, parent_hash: &H256) -> Option
.map(|public| public_to_address(&public))
}

impl<Submitter> Default for CachedFinalityVotes<Submitter> {
fn default() -> Self {
CachedFinalityVotes {
stopped_at_finalized_sibling: false,
unaccounted_ancestry: VecDeque::new(),
votes: None,
}
}
}

impl<Submitter> Default for FinalityVotes<Submitter> {
fn default() -> Self {
FinalityVotes {
Expand Down Expand Up @@ -307,7 +332,7 @@ mod tests {
assert_eq!(
finalize_blocks(
&storage,
Default::default(),
genesis().compute_id(),
(Default::default(), &validators_addresses(5)),
id1,
None,
Expand All @@ -331,7 +356,7 @@ mod tests {
assert_eq!(
finalize_blocks(
&storage,
Default::default(),
genesis().compute_id(),
(Default::default(), &validators_addresses(5)),
id2,
None,
Expand All @@ -355,7 +380,7 @@ mod tests {
assert_eq!(
finalize_blocks(
&storage,
Default::default(),
genesis().compute_id(),
(Default::default(), &validators_addresses(5)),
id3,
None,
Expand Down Expand Up @@ -397,6 +422,7 @@ mod tests {
assert_eq!(
prepare_votes::<()>(
CachedFinalityVotes {
stopped_at_finalized_sibling: false,
unaccounted_ancestry: vec![(headers[3].compute_id(), None, headers[3].clone()),]
.into_iter()
.collect(),
Expand All @@ -407,7 +433,7 @@ mod tests {
ancestry: ancestry[..3].iter().cloned().collect(),
}),
},
2,
headers[1].compute_id(),
&validators.iter().collect(),
header5.compute_id(),
&header5,
Expand Down Expand Up @@ -471,8 +497,12 @@ mod tests {
let id7 = headers[6].compute_id();
assert_eq!(
prepare_votes(
storage.cached_finality_votes(&hashes.get(5).unwrap(), |_| false,),
0,
storage.cached_finality_votes(
&headers.get(5).unwrap().compute_id(),
&genesis().compute_id(),
|_| false,
),
Default::default(),
&validators_addresses.iter().collect(),
id7,
headers.get(6).unwrap(),
Expand All @@ -498,8 +528,12 @@ mod tests {
// check that votes at #7 are computed correctly with cache
assert_eq!(
prepare_votes(
storage.cached_finality_votes(&hashes.get(5).unwrap(), |_| false,),
0,
storage.cached_finality_votes(
&headers.get(5).unwrap().compute_id(),
&genesis().compute_id(),
|_| false,
),
Default::default(),
&validators_addresses.iter().collect(),
id7,
headers.get(6).unwrap(),
Expand All @@ -522,8 +556,12 @@ mod tests {
};
assert_eq!(
prepare_votes(
storage.cached_finality_votes(&hashes.get(5).unwrap(), |hash| *hash == hashes[2],),
3,
storage.cached_finality_votes(
&headers.get(5).unwrap().compute_id(),
&headers.get(2).unwrap().compute_id(),
|hash| *hash == hashes[2],
),
headers[2].compute_id(),
&validators_addresses.iter().collect(),
id7,
headers.get(6).unwrap(),
Expand All @@ -534,4 +572,22 @@ mod tests {
);
});
}

#[test]
fn prepare_votes_fails_when_finalized_sibling_is_in_ancestry() {
assert_eq!(
prepare_votes::<()>(
CachedFinalityVotes {
stopped_at_finalized_sibling: true,
..Default::default()
},
Default::default(),
&validators_addresses(3).iter().collect(),
Default::default(),
&Default::default(),
None,
),
Err(Error::TryingToFinalizeSibling),
);
}
}
Loading

0 comments on commit f5a0014

Please sign in to comment.