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

Initial Validation Checks - Message, Timestamp and Block Sig #219

Merged
merged 14 commits into from
Feb 11, 2020
Merged
14 changes: 13 additions & 1 deletion blockchain/blocks/src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
// Copyright 2020 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT

use std::fmt;
use std::{fmt, time::SystemTimeError as TimeErr};

#[derive(Debug, PartialEq)]
pub enum Error {
/// Tipset contains invalid data, as described by the string parameter.
InvalidTipSet(String),
/// The given tipset has no blocks
NoBlocks,
/// Invalid signature
InvalidSignature(String),
/// Error in validating arbitrary data
Validation(String),
}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::InvalidTipSet(msg) => write!(f, "Invalid tipset: {}", msg),
Error::NoBlocks => write!(f, "No blocks for tipset"),
Error::InvalidSignature(msg) => write!(f, "Invalid signature: {}", msg),
Error::Validation(msg) => write!(f, "Error validating data: {}", msg),
}
}
}

impl From<TimeErr> for Error {
fn from(e: TimeErr) -> Error {
Error::Validation(e.to_string())
}
}
47 changes: 45 additions & 2 deletions blockchain/blocks/src/header.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// Copyright 2020 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT

use super::{EPostProof, Ticket, TipSetKeys};
use super::{EPostProof, Error, FullTipset, Ticket, TipSetKeys};
use address::Address;
use cid::{Cid, Error as CidError};
use clock::ChainEpoch;
use crypto::Signature;
use crypto::{is_valid_signature, Signature};
use derive_builder::Builder;
use encoding::{
de::{self, Deserializer},
Expand All @@ -16,6 +16,7 @@ use num_bigint::BigUint;
use raw_block::RawBlock;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::time::{SystemTime, UNIX_EPOCH};

/// Header of a block
///
Expand Down Expand Up @@ -279,6 +280,48 @@ impl BlockHeader {
self.cached_cid = Cid::from_bytes_default(&self.cached_bytes).map_err(|e| e.to_string())?;
Ok(())
}
/// Check to ensure block signature is valid
pub fn check_block_signature(&self, addr: &Address) -> Result<(), Error> {
dutterbutter marked this conversation as resolved.
Show resolved Hide resolved
if self.signature().bytes().is_empty() {
return Err(Error::InvalidSignature(
"Signature is nil in header".to_string(),
));
}

if !is_valid_signature(&self.cid().to_bytes(), addr, self.signature()) {
return Err(Error::InvalidSignature(
"Block signature is invalid".to_string(),
));
}

Ok(())
}
/// Validates timestamps to ensure BlockHeader was generated at the correct time
pub fn validate_timestamps(&self, base_tipset: &FullTipset) -> Result<(), Error> {
// first check that it is not in the future; see https://github.com/filecoin-project/specs/blob/6ab401c0b92efb6420c6e198ec387cf56dc86057/validation.md
// allowing for some small grace period to deal with small asynchrony
// using ALLOWABLE_CLOCK_DRIFT from Lotus; see https://github.com/filecoin-project/lotus/blob/master/build/params_shared.go#L34:7
const ALLOWABLE_CLOCK_DRIFT: u64 = 1;
let time_now = SystemTime::now().duration_since(UNIX_EPOCH)?;
if self.timestamp() > time_now.as_secs() + ALLOWABLE_CLOCK_DRIFT
|| self.timestamp() > time_now.as_secs()
{
return Err(Error::Validation("Header was from the future".to_string()));
}
const FIXED_BLOCK_DELAY: u64 = 45;
// check that it is appropriately delayed from its parents including null blocks
if self.timestamp()
< base_tipset.tipset()?.min_timestamp()?
+ FIXED_BLOCK_DELAY
* (*self.epoch() - *base_tipset.tipset()?.tip_epoch()).chain_epoch()
{
return Err(Error::Validation(
"Header was generated too soon".to_string(),
));
}

Ok(())
}
}

/// human-readable string representation of a block CID
Expand Down
9 changes: 7 additions & 2 deletions blockchain/blocks/src/tipset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ pub struct TipSetKeys {
// https://github.com/ChainSafe/forest/issues/143

impl TipSetKeys {
/// constructor
pub fn new(cids: Vec<Cid>) -> Self {
Self { cids }
}
/// checks whether the set contains exactly the same CIDs as another.
fn equals(&self, key: &TipSetKeys) -> bool {
if self.cids.len() != key.cids.len() {
Expand Down Expand Up @@ -157,7 +161,7 @@ impl Tipset {
Ok(self.blocks[0].ticket().clone())
}
/// Returns the smallest timestamp of all blocks in the tipset
fn min_timestamp(&self) -> Result<u64, Error> {
pub fn min_timestamp(&self) -> Result<u64, Error> {
if self.blocks.is_empty() {
return Err(Error::NoBlocks);
}
Expand Down Expand Up @@ -216,7 +220,8 @@ impl FullTipset {
for block in self.blocks() {
headers.push(block.to_header().clone())
}
Ok(Tipset::new(headers))?
let tip: Tipset = Tipset::new(headers)?;
Ok(tip)
}
}

Expand Down
3 changes: 3 additions & 0 deletions blockchain/sync_manager/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ chain = { path ="../chain" }
message = { package = "forest_message", path = "../../vm/message" }
raw_block = { package = "raw_block", path = "../raw_block" }
multihash = "0.9.4"
state = { package = "state_tree", path = "../../vm/state_tree/" }
num-bigint = { git = "https://github.com/austinabell/num-bigint", rev = "f7084a9ed5a2b08d9bfb67790cb4ce9212193f31" }
crypto = { path= "../../crypto" }

[dev-dependencies]
cid = { package = "forest_cid", path = "../../ipld/cid" }
Expand Down
14 changes: 13 additions & 1 deletion blockchain/sync_manager/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use chain::Error as StoreErr;
use cid::Error as CidErr;
use db::Error as DbErr;
use encoding::{error::Error as SerdeErr, Error as EncErr};
use std::fmt;
use std::{fmt, time::SystemTimeError as TimeErr};

#[derive(Debug, PartialEq)]
pub enum Error {
Expand All @@ -26,6 +26,10 @@ pub enum Error {
KeyValueStore(String),
/// Error originating from the AMT
AMT(String),
/// Error originating from state
State(String),
/// Error in validating arbitrary data
Validation(String),
}

impl fmt::Display for Error {
Expand All @@ -45,6 +49,8 @@ impl fmt::Display for Error {
Error::InvalidCid(msg) => write!(f, "Error originating from CID construction: {}", msg),
Error::Store(msg) => write!(f, "Error originating from ChainStore: {}", msg),
Error::AMT(msg) => write!(f, "Error originating from the AMT: {}", msg),
Error::State(msg) => write!(f, "Error originating from the State: {}", msg),
Error::Validation(msg) => write!(f, "Error validating data: {}", msg),
}
}
}
Expand Down Expand Up @@ -90,3 +96,9 @@ impl From<AmtErr> for Error {
Error::AMT(e.to_string())
}
}

impl From<TimeErr> for Error {
fn from(e: TimeErr) -> Error {
Error::Validation(e.to_string())
}
}
126 changes: 122 additions & 4 deletions blockchain/sync_manager/src/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@

use super::errors::Error;
use super::manager::SyncManager;
use address::Address;
use amt::{BlockStore, AMT};
use blocks::{Block, FullTipset, TipSetKeys, Tipset};
use chain::ChainStore;
use cid::{Cid, Error as CidError};
use crypto::is_valid_signature;
use libp2p::core::PeerId;
use message::MsgMeta;
use message::{Message, MsgMeta};
use num_bigint::BigUint;
use raw_block::RawBlock;
use state::{HamtStateTree, StateTree};
use std::collections::HashMap;

pub struct Syncer<'a> {
// TODO add ability to send msg to all subscribers indicating incoming blocks
Expand All @@ -28,6 +33,12 @@ pub struct Syncer<'a> {
_own: PeerId,
}

/// Message data used to ensure valid state transition
struct MsgMetaData {
balance: BigUint,
sequence: u64,
}

impl<'a> Syncer<'a> {
/// TODO add constructor

Expand Down Expand Up @@ -63,8 +74,7 @@ impl<'a> Syncer<'a> {
/// bls and secp messages contained in the passed in block and stores them in a key-value store
fn validate_msg_data(&self, block: &Block) -> Result<(), Error> {
let sm_root = self.compute_msg_data(block)?;
// TODO change message_receipts to messages() once #192 is in
if block.to_header().message_receipts() != &sm_root {
if block.to_header().messages() != &sm_root {
return Err(Error::InvalidRoots);
}

Expand Down Expand Up @@ -102,7 +112,7 @@ impl<'a> Syncer<'a> {
Ok(fts)
}
/// Returns a reconstructed FullTipset from store if keys exist
fn load_fts(&self, keys: TipSetKeys) -> Result<FullTipset, Error> {
pub fn load_fts(&self, keys: TipSetKeys) -> Result<FullTipset, Error> {
dutterbutter marked this conversation as resolved.
Show resolved Hide resolved
let mut blocks = Vec::new();
// retrieve tipset from store based on passed in TipSetKeys
let ts = self.chain_store.tipset(keys.tipset_keys())?;
Expand All @@ -122,6 +132,114 @@ impl<'a> Syncer<'a> {
let fts = FullTipset::new(blocks);
Ok(fts)
}
// Block message validation checks
pub fn check_blk_msgs(&self, block: Block, _tip: Tipset) -> Result<(), Error> {
// TODO retrieve bls public keys for verify_bls_aggregate
// for _m in block.bls_msgs() {
// }
// TODO verify_bls_aggregate

// check msgs for validity
fn check_msg<T: Message>(
msg: &T,
msg_meta_data: &mut HashMap<Address, MsgMetaData>,
tree: &HamtStateTree,
) -> Result<(), Error>
where
T: Message,
{
let updated_state: MsgMetaData = match msg_meta_data.get(msg.from()) {
// address is present begin validity checks
Some(MsgMetaData { sequence, balance }) => {
// sequence equality check
if *sequence != msg.sequence() {
return Err(Error::Validation("Sequences are not equal".to_string()));
}

// sufficient funds check
if *balance < msg.required_funds() {
return Err(Error::Validation(
"Insufficient funds for message execution".to_string(),
));
}
// update balance and increment sequence by 1
MsgMetaData {
balance: balance - msg.required_funds(),
sequence: sequence + 1,
}
}
// MsgMetaData not found with provided address key, insert sequence and balance with address as key
None => {
let actor = tree.get_actor(msg.from()).ok_or_else(|| {
Error::State("Could not retrieve actor from state tree".to_owned())
})?;

MsgMetaData {
sequence: actor.sequence,
balance: actor.balance,
}
}
};
// update hash map with updated state
msg_meta_data.insert(msg.from().clone(), updated_state);
Ok(())
}
let mut msg_meta_data: HashMap<Address, MsgMetaData> = HashMap::new();
// TODO retrieve tipset state and load state tree
// temporary
let tree = HamtStateTree::default();
// loop through bls messages and check msg validity
for m in block.bls_msgs() {
check_msg(m, &mut msg_meta_data, &tree)?;
}
// loop through secp messages and check msg validity and signature
for m in block.secp_msgs() {
check_msg(m, &mut msg_meta_data, &tree)?;
// signature validation
if !is_valid_signature(&m.cid()?.to_bytes(), m.from(), m.signature()) {
return Err(Error::Validation(
"Message signature is not valid".to_string(),
));
}
}
// validate message root from header matches message root
let sm_root = self.compute_msg_data(&block)?;
if block.to_header().messages() != &sm_root {
return Err(Error::InvalidRoots);
}

Ok(())
}

/// Validates block semantically according to https://github.com/filecoin-project/specs/blob/6ab401c0b92efb6420c6e198ec387cf56dc86057/validation.md
pub fn validate(&self, block: Block) -> Result<(), Error> {
ansermino marked this conversation as resolved.
Show resolved Hide resolved
// get header from full block
let header = block.to_header();

// check if block has been signed
if header.signature().bytes().is_empty() {
return Err(Error::Validation("Signature is nil in header".to_string()));
}

let base_tipset = self.load_fts(TipSetKeys::new(header.parents().cids.clone()))?;
// time stamp checks
header.validate_timestamps(&base_tipset)?;

// check messages to ensure valid state transitions
self.check_blk_msgs(block.clone(), base_tipset.tipset()?)?;

// block signature check
// TODO need to pass in raw miner address; temp using header miner address
// see https://github.com/filecoin-project/lotus/blob/master/chain/sync.go#L611
header.check_block_signature(header.miner_address())?;

// TODO winner_check
// TODO miner_check
// TODO verify_ticket_vrf
// TODO verify_election_proof_check

Ok(())
}
}

pub fn cids_from_messages<T: RawBlock>(messages: &[T]) -> Result<Vec<Cid>, CidError> {
Expand Down
15 changes: 15 additions & 0 deletions node/clock/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use encoding::{
};
use std::convert::TryInto;
use std::num::TryFromIntError;
use std::ops::Sub;

const _ISO_FORMAT: &str = "%FT%X.%.9F";
const EPOCH_DURATION: i32 = 15;
Expand Down Expand Up @@ -75,9 +76,23 @@ impl ChainEpochClock {
}
}

impl Sub for ChainEpoch {
type Output = ChainEpoch;

fn sub(self, other: ChainEpoch) -> ChainEpoch {
ChainEpoch {
0: self.0 - other.0,
}
}
}

impl ChainEpoch {
/// Returns ChainEpoch based on the given unix timestamp
pub fn new(timestamp: i64) -> Result<ChainEpoch, TryFromIntError> {
Ok(ChainEpoch(timestamp.try_into()?))
}
// Returns chain epoch
pub fn chain_epoch(&self) -> &u64 {
&self.0
}
}
Loading