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
3 changes: 3 additions & 0 deletions blockchain/blocks/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ pub enum Error {
InvalidTipSet(String),
/// The given tipset has no blocks
NoBlocks,
/// Invalid signature
InvalidSignature(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),
}
}
}
20 changes: 18 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, 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 Down Expand Up @@ -279,6 +279,22 @@ 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(())
}
}

/// human-readable string representation of a block CID
Expand Down
6 changes: 5 additions & 1 deletion 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
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())
}
}
165 changes: 162 additions & 3 deletions blockchain/sync_manager/src/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@

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;
use std::time::{SystemTime, UNIX_EPOCH};

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

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

struct MsgCheck {
metadata: HashMap<Address, MsgMetaData>,
}
dutterbutter marked this conversation as resolved.
Show resolved Hide resolved

impl MsgCheck {
/// Creates new MsgCheck with empty hash map
pub fn new() -> Self {
Self {
metadata: HashMap::new(),
}
}
}
dutterbutter marked this conversation as resolved.
Show resolved Hide resolved

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

Expand Down Expand Up @@ -63,8 +88,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 @@ -122,6 +146,141 @@ 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> {
for _m in block.bls_msgs() {
// TODO retrieve bls public keys for verify_bls_aggregate
}
dutterbutter marked this conversation as resolved.
Show resolved Hide resolved
// TODO verify_bls_aggregate

// check msgs for validity
fn check_msg<T: Message>(
msg: T,
dutterbutter marked this conversation as resolved.
Show resolved Hide resolved
msg_meta_data: &mut MsgCheck,
tree: &HamtStateTree,
) -> Result<(), Error>
where
T: Message,
{
let updated_state: MsgMetaData = match msg_meta_data.metadata.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() {
dutterbutter marked this conversation as resolved.
Show resolved Hide resolved
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,
}
}
// sequence is not found, insert sequence and balance with address as key
_ => {
dutterbutter marked this conversation as resolved.
Show resolved Hide resolved
let actor = tree.get_actor(msg.from());
if let Some(act) = actor {
MsgMetaData {
sequence: *act.sequence(),
balance: act.balance().clone(),
}
} else {
return Err(Error::State(
"Could not retrieve actor from state tree".to_string(),
));
}
dutterbutter marked this conversation as resolved.
Show resolved Hide resolved
}
};
msg_meta_data
.metadata
.insert(msg.from().clone(), updated_state);
Ok(())
}

let mut msg_meta_data = MsgCheck::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.clone(), &mut msg_meta_data, &tree)?;
}
// loop through secp messages and check msg validity and signature
for m in block.secp_msgs() {
check_msg(m.clone(), &mut msg_meta_data, &tree)?;
dutterbutter marked this conversation as resolved.
Show resolved Hide resolved
// 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();
let base_tipset = self.load_fts(TipSetKeys::new((*header.parents().cids).to_vec()))?;
dutterbutter marked this conversation as resolved.
Show resolved Hide resolved
dutterbutter marked this conversation as resolved.
Show resolved Hide resolved

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

// 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
let allowable_clock_drift: u64 = 1;
let time_now = SystemTime::now().duration_since(UNIX_EPOCH)?;
dutterbutter marked this conversation as resolved.
Show resolved Hide resolved
if header.timestamp() > time_now.as_secs() + allowable_clock_drift {
return Err(Error::Validation("Block was from the future".to_string()));
}
if header.timestamp() > time_now.as_secs() {
return Err(Error::Validation(
"Got block from the future, but within threshold".to_string(),
));
}
dutterbutter marked this conversation as resolved.
Show resolved Hide resolved
const FIXED_BLOCK_DELAY: u64 = 45;
// check that it is appropriately delayed from its parents including null blocks
if header.timestamp()
< base_tipset.tipset()?.min_timestamp()?
+ FIXED_BLOCK_DELAY
* (*header.epoch() - *base_tipset.tipset()?.tip_epoch()).chain_epoch()
{
return Err(Error::Validation(
"Block was generated too soon".to_string(),
));
}

// 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
}
}
6 changes: 6 additions & 0 deletions vm/actor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ impl ActorState {
sequence,
}
}
pub fn sequence(&self) -> &u64 {
&self.sequence
}
pub fn balance(&self) -> &BigUint {
&self.balance
}
}

// TODO implement Actor for builtin actors on finished spec
Expand Down
2 changes: 2 additions & 0 deletions vm/message/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ pub trait Message {
fn gas_price(&self) -> &BigUint;
/// Returns the gas limit for the message
fn gas_limit(&self) -> &BigUint;
/// Returns the required funds for the message
fn required_funds(&self) -> BigUint;
}

pub struct MsgMeta {
Expand Down
3 changes: 3 additions & 0 deletions vm/message/src/signed_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ impl Message for SignedMessage {
fn gas_limit(&self) -> &BigUint {
self.message.gas_limit()
}
fn required_funds(&self) -> BigUint {
self.message.required_funds()
}
}

impl RawBlock for SignedMessage {}
Expand Down
4 changes: 4 additions & 0 deletions vm/message/src/unsigned_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ impl Message for UnsignedMessage {
fn gas_limit(&self) -> &BigUint {
&self.gas_limit
}
fn required_funds(&self) -> BigUint {
let total = self.gas_price() * self.gas_limit();
total + self.value().0.clone()
}
}

impl RawBlock for UnsignedMessage {}
Expand Down