use std::{cmp::max, hash::Hash, ops::Index};
use parity_scale_codec::{Decode, Encode};
use sp_runtime::SaturatedConversion;
use crate::{
phron_primitives::{BlockHash, BlockNumber},
data_io::legacy::MAX_DATA_BRANCH_LEN,
BlockId, SessionBoundaries,
};
#[derive(Clone, Debug, Encode, Decode, Hash, PartialEq, Eq)]
pub struct UnvalidatedPhronProposal {
pub branch: Vec<BlockHash>,
pub number: BlockNumber,
}
#[derive(Debug, PartialEq, Eq)]
pub enum ValidationError {
BranchEmpty,
BranchTooLong {
branch_size: usize,
},
BlockNumberOutOfBounds {
branch_size: usize,
block_number: BlockNumber,
},
BlockOutsideSessionBoundaries {
session_start: BlockNumber,
session_end: BlockNumber,
top_block: BlockNumber,
bottom_block: BlockNumber,
},
}
impl UnvalidatedPhronProposal {
pub(crate) fn new(branch: Vec<BlockHash>, block_number: BlockNumber) -> Self {
UnvalidatedPhronProposal {
branch,
number: block_number,
}
}
pub(crate) fn validate_bounds(
&self,
session_boundaries: &SessionBoundaries,
) -> Result<PhronProposal, ValidationError> {
use ValidationError::*;
if self.branch.len() > MAX_DATA_BRANCH_LEN {
return Err(BranchTooLong {
branch_size: self.branch.len(),
});
}
if self.branch.is_empty() {
return Err(BranchEmpty);
}
if self.number < <BlockNumber>::saturated_from(self.branch.len()) {
return Err(BlockNumberOutOfBounds {
branch_size: self.branch.len(),
block_number: self.number,
});
}
let bottom_block = self.number - <BlockNumber>::saturated_from(self.branch.len() - 1);
let top_block = self.number;
let session_start = session_boundaries.first_block();
let session_end = session_boundaries.last_block();
if session_start > bottom_block || top_block > session_end {
return Err(BlockOutsideSessionBoundaries {
session_start,
session_end,
top_block,
bottom_block,
});
}
Ok(PhronProposal {
branch: self.branch.clone(),
number: self.number,
})
}
}
#[derive(Clone, Debug, Encode, Decode, Hash, PartialEq, Eq)]
pub struct PhronProposal {
branch: Vec<BlockHash>,
number: BlockNumber,
}
impl Index<usize> for PhronProposal {
type Output = BlockHash;
fn index(&self, index: usize) -> &Self::Output {
&self.branch[index]
}
}
impl PhronProposal {
pub fn len(&self) -> usize {
self.branch.len()
}
pub fn top_block(&self) -> BlockId {
(
*self
.branch
.last()
.expect("cannot be empty for correct data"),
self.number_top_block(),
)
.into()
}
pub fn bottom_block(&self) -> BlockId {
(
*self
.branch
.first()
.expect("cannot be empty for correct data"),
self.number_bottom_block(),
)
.into()
}
pub fn number_below_branch(&self) -> BlockNumber {
self.number - <BlockNumber>::saturated_from(self.branch.len())
}
pub fn number_bottom_block(&self) -> BlockNumber {
self.number - <BlockNumber>::saturated_from(self.branch.len() - 1)
}
pub fn number_top_block(&self) -> BlockNumber {
self.number
}
pub fn block_at_num(&self, num: BlockNumber) -> Option<BlockId> {
if self.number_bottom_block() <= num && num <= self.number_top_block() {
let ind: usize = (num - self.number_bottom_block()).saturated_into();
return Some((self.branch[ind], num).into());
}
None
}
pub fn blocks_from_num(&self, num: BlockNumber) -> impl Iterator<Item = BlockId> + '_ {
let num = max(num, self.number_bottom_block());
self.branch
.iter()
.skip((num - self.number_bottom_block()).saturated_into())
.cloned()
.zip(0u32..)
.map(move |(hash, index)| (hash, num + index).into())
}
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum PendingProposalStatus {
PendingTopBlock,
TopBlockImportedButIncorrectBranch,
TopBlockImportedButNotFinalizedAncestor,
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum ProposalStatus {
Finalize(Vec<BlockId>),
Ignore,
Pending(PendingProposalStatus),
}
#[cfg(test)]
mod tests {
use sp_core::hash::H256;
use super::{UnvalidatedPhronProposal, ValidationError::*};
use crate::{
phron_primitives::BlockNumber, data_io::MAX_DATA_BRANCH_LEN, SessionBoundaryInfo,
SessionId, SessionPeriod,
};
#[test]
fn proposal_with_empty_branch_is_invalid() {
let session_boundaries =
SessionBoundaryInfo::new(SessionPeriod(20)).boundaries_for_session(SessionId(1));
let branch = vec![];
let proposal = UnvalidatedPhronProposal::new(branch, session_boundaries.first_block());
assert_eq!(
proposal.validate_bounds(&session_boundaries),
Err(BranchEmpty)
);
}
#[test]
fn too_long_proposal_is_invalid() {
let session_boundaries =
SessionBoundaryInfo::new(SessionPeriod(20)).boundaries_for_session(SessionId(1));
let session_end = session_boundaries.last_block();
let branch = vec![H256::default(); MAX_DATA_BRANCH_LEN + 1];
let branch_size = branch.len();
let proposal = UnvalidatedPhronProposal::new(branch, session_end);
assert_eq!(
proposal.validate_bounds(&session_boundaries),
Err(BranchTooLong { branch_size })
);
}
#[test]
fn proposal_not_within_session_is_invalid() {
let session_boundaries =
SessionBoundaryInfo::new(SessionPeriod(20)).boundaries_for_session(SessionId(1));
let session_start = session_boundaries.first_block();
let session_end = session_boundaries.last_block();
let branch = vec![H256::default(); 2];
let proposal = UnvalidatedPhronProposal::new(branch.clone(), session_start);
assert_eq!(
proposal.validate_bounds(&session_boundaries),
Err(BlockOutsideSessionBoundaries {
session_start,
session_end,
bottom_block: session_start - 1,
top_block: session_start
})
);
let proposal = UnvalidatedPhronProposal::new(branch, session_end + 1);
assert_eq!(
proposal.validate_bounds(&session_boundaries),
Err(BlockOutsideSessionBoundaries {
session_start,
session_end,
bottom_block: session_end,
top_block: session_end + 1
})
);
}
#[test]
fn proposal_starting_at_zero_block_is_invalid() {
let session_boundaries =
SessionBoundaryInfo::new(SessionPeriod(20)).boundaries_for_session(SessionId(0));
let branch = vec![H256::default(); 2];
let proposal = UnvalidatedPhronProposal::new(branch, 1);
assert_eq!(
proposal.validate_bounds(&session_boundaries),
Err(BlockNumberOutOfBounds {
branch_size: 2,
block_number: 1
})
);
}
#[test]
fn valid_proposal_is_validated_positively() {
let session_boundaries =
SessionBoundaryInfo::new(SessionPeriod(20)).boundaries_for_session(SessionId(0));
let branch = vec![H256::default(); MAX_DATA_BRANCH_LEN];
let proposal =
UnvalidatedPhronProposal::new(branch, (MAX_DATA_BRANCH_LEN + 1) as BlockNumber);
assert!(proposal.validate_bounds(&session_boundaries).is_ok());
let branch = vec![H256::default(); 1];
let proposal =
UnvalidatedPhronProposal::new(branch, (MAX_DATA_BRANCH_LEN + 1) as BlockNumber);
assert!(proposal.validate_bounds(&session_boundaries).is_ok());
}
}