diff --git a/.gitignore b/.gitignore index 212181d360e4..9102759836a5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target **/*.rs.bk -/Cargo.lock \ No newline at end of file +/Cargo.lock +.idea/ \ No newline at end of file diff --git a/blockchain/Cargo.toml b/blockchain/Cargo.toml index 687339067d00..6e8074bf9956 100644 --- a/blockchain/Cargo.toml +++ b/blockchain/Cargo.toml @@ -10,3 +10,4 @@ edition = "2018" vm = {path = "../vm"} address = {path = "../vm/address"} cid = "0.3.1" +multihash = "0.8.0" \ No newline at end of file diff --git a/blockchain/src/blocks/block.rs b/blockchain/src/blocks/block.rs index d844cb7e1889..ee20519a1aa0 100644 --- a/blockchain/src/blocks/block.rs +++ b/blockchain/src/blocks/block.rs @@ -2,11 +2,18 @@ use super::ticket::{Ticket, VRFProofIndex}; use super::TipSetKeys; + use address::Address; -use cid::Cid; +use cid::{Cid, Codec, Prefix, Version}; +use multihash::Hash; use vm::message::Message; +// DefaultHashFunction represents the default hashing function to use +// TODO SHOULD BE BLAKE2B +const DEFAULT_HASH_FUNCTION: Hash = Hash::Keccak256; + /// BlockHeader defines header of a block in the Filecoin blockchain +#[derive(Clone, Debug)] pub struct BlockHeader { /// CHAIN LINKING /// @@ -17,23 +24,23 @@ pub struct BlockHeader { /// weight is the aggregate chain weight of the parent set pub weight: u64, /// epoch is the period in which a new block is generated. There may be multiple rounds in an epoch - epoch: u64, + pub epoch: u64, /// height is the block height pub height: u64, /// MINER INFO /// /// miner_address is the address of the miner actor that mined this block - miner_address: Address, + pub miner_address: Address, /// STATE /// /// messages is the Cid of the root of an array of Messages - messages: Cid, + pub messages: Cid, /// message_receipts is the Cid of the root of an array of MessageReceipts - message_receipts: Cid, + pub message_receipts: Cid, /// state_root is a cid pointer to the state tree after application of the transactions state transitions - state_root: Cid, + pub state_root: Cid, /// CONSENSUS /// @@ -43,11 +50,16 @@ pub struct BlockHeader { pub ticket: Ticket, /// election_proof is the "scratched ticket" proving that this block won /// an election - election_proof: VRFProofIndex, + pub election_proof: VRFProofIndex, // SIGNATURES // // block_sig filCrypto Signature // BLSAggregateSig + /// CACHE + /// + pub cached_cid: Cid, + + pub cached_bytes: u8, } /// Block defines a full block @@ -55,3 +67,23 @@ pub struct Block { header: BlockHeader, messages: Vec, } + +impl BlockHeader { + /// cid returns the content id of this header + pub fn cid(&mut self) -> Cid { + // TODO + // Encode blockheader into cache_bytes + // Change DEFAULT_HASH_FUNCTION to utilize blake2b + // + // Currently content id for headers will be incomplete until encoding and supporting libraries are completed + let c = Prefix { + version: Version::V1, + codec: Codec::DagCBOR, + mh_type: DEFAULT_HASH_FUNCTION, + mh_len: 0, + }; + let new_cid = Cid::new_from_prefix(&c, &[self.cached_bytes]); + self.cached_cid = new_cid; + self.cached_cid.clone() + } +} diff --git a/blockchain/src/blocks/errors.rs b/blockchain/src/blocks/errors.rs index e213bb2e9e6a..e69b4aea93ed 100644 --- a/blockchain/src/blocks/errors.rs +++ b/blockchain/src/blocks/errors.rs @@ -2,14 +2,14 @@ use std::fmt; #[derive(Debug, PartialEq)] pub enum Error { - UndefinedTipSet, + UndefinedTipSet(String), NoBlocks, } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { - Error::UndefinedTipSet => write!(f, "Undefined tipset"), + Error::UndefinedTipSet(ref msg) => write!(f, "Invalid tipset: {}", msg), Error::NoBlocks => write!(f, "No blocks for tipset"), } } diff --git a/blockchain/src/blocks/ticket.rs b/blockchain/src/blocks/ticket.rs index 4bf1d6257a3d..6e2999bc1d8a 100644 --- a/blockchain/src/blocks/ticket.rs +++ b/blockchain/src/blocks/ticket.rs @@ -4,7 +4,7 @@ pub type VRFProofIndex = Vec; /// A Ticket is a marker of a tick of the blockchain's clock. It is the source /// of randomness for proofs of storage and leader election. It is generated /// by the miner of a block using a VRF and a VDF. -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq, PartialOrd, Eq)] pub struct Ticket { /// A proof output by running a VRF on the VDFResult of the parent ticket pub vrfproof: VRFProofIndex, diff --git a/blockchain/src/blocks/tipset.rs b/blockchain/src/blocks/tipset.rs index 03a1b6d805a8..e33a641786d1 100644 --- a/blockchain/src/blocks/tipset.rs +++ b/blockchain/src/blocks/tipset.rs @@ -1,13 +1,10 @@ #![allow(unused_variables)] #![allow(dead_code)] -use cid::Cid; - use super::block::BlockHeader; -use super::ticket::Ticket; - use super::errors::Error; - +use super::ticket::Ticket; +use cid::Cid; /// TipSet is an immutable set of blocks at the same height with the same parent set /// Blocks in a tipset are canonically ordered by ticket size pub struct Tipset { @@ -18,28 +15,96 @@ pub struct Tipset { /// TipSetKeys is a set of CIDs forming a unique key for a TipSet /// Equal keys will have equivalent iteration order, but note that the CIDs are *not* maintained in /// the same order as the canonical iteration order of blocks in a tipset (which is by ticket) -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct TipSetKeys { pub cids: Vec, } impl Tipset { /// new builds a new TipSet from a collection of blocks - //// The blocks must be distinct (different CIDs), have the same height, and same parent set - fn new(headers: Vec) -> Result { - // TODO - // check length of blocks is not 0 - // loop through headers to ensure blocks have same height and parent set - // sort headers by ticket size - // check and assign uniqueness of key - // return TipSet type + /// A valid tipset contains a non-empty collection of blocks that have distinct miners and all specify identical + /// epoch, parents, weight, height, state root, receipt root; + /// contentID for headers are supposed to be distinct but until encoding is added will be equal + pub fn new(headers: Vec) -> Result { + // check header is non-empty if headers.is_empty() { return Err(Error::NoBlocks); } - Tipset::new(headers) + + let mut sorted_headers = Vec::new(); + let mut cids = Vec::new(); + + // loop through headers and validate conditions against 0th header + for i in 0..headers.len() { + if i > 0 { + // Skip redundant check + // check height is equal + if headers[i].height != headers[0].height { + return Err(Error::UndefinedTipSet("heights are not equal".to_string())); + } + // check parent cids are equal + if !headers[i].parents.equals(headers[0].parents.clone()) { + return Err(Error::UndefinedTipSet( + "parent cids are not equal".to_string(), + )); + } + // check weights are equal + if headers[i].weight != headers[0].weight { + return Err(Error::UndefinedTipSet("weights are not equal".to_string())); + } + // check state_roots are equal + if headers[i].state_root != headers[0].state_root.clone() { + return Err(Error::UndefinedTipSet( + "state_roots are not equal".to_string(), + )); + } + // check epochs are equal + if headers[i].epoch != headers[0].epoch { + return Err(Error::UndefinedTipSet("epochs are not equal".to_string())); + } + // check message_receipts are equal + if headers[i].message_receipts != headers[0].message_receipts.clone() { + return Err(Error::UndefinedTipSet( + "message_receipts are not equal".to_string(), + )); + } + // check miner_addresses are distinct + if headers[i].miner_address == headers[0].miner_address.clone() { + return Err(Error::UndefinedTipSet( + "miner_addresses are not distinct".to_string(), + )); + } + } + // push headers into vec for sorting + sorted_headers.push(headers[i].clone()); + // push header cid into vec for unique check + cids.push(headers[i].clone().cid()); + } + + // sort headers by ticket size + // break ticket ties with the header CIDs, which are distinct + sorted_headers.sort_by_key(|header| { + let mut h = header.clone(); + (h.ticket.vrfproof.clone(), h.cid().hash) + }); + + // TODO + // Have a check the ensures CIDs are distinct + // blocked by CBOR encoding + + // return tipset where sorted headers have smallest ticket size is in the 0th index + // and the distinct keys + Ok(Self { + blocks: sorted_headers, + key: TipSetKeys { + // interim until CID check is in place + cids, + }, + }) } + /// min_ticket returns the smallest ticket of all blocks in the tipset - fn min_ticket(&self) -> Result<(Ticket), Error> { + fn min_ticket(&self) -> Result { if self.blocks.is_empty() { return Err(Error::NoBlocks); } @@ -83,3 +148,151 @@ impl Tipset { self.blocks[0].weight } } + +impl TipSetKeys { + /// equals checks whether the set contains exactly the same CIDs as another. + fn equals(&self, key: TipSetKeys) -> bool { + if self.cids.len() != key.cids.len() { + return false; + } + for i in 0..key.cids.len() { + if self.cids[i] != key.cids[i] { + return false; + } + } + true + } +} + +#[cfg(test)] +mod tests { + use super::*; + use address::Address; + use cid::{Cid, Codec, Version}; + + const WEIGHT: u64 = 1; + const EPOCH: u64 = 1; + const HEIGHT: u64 = 1; + const CACHED_BYTES: u8 = 0; + + fn template_key(data: &[u8]) -> Cid { + let h = multihash::encode(multihash::Hash::SHA2256, data).unwrap(); + let cid = Cid::new(Codec::DagProtobuf, Version::V1, &h); + return cid; + } + + // key_setup returns a vec of 4 distinct CIDs + fn key_setup() -> Vec { + return vec![ + template_key(b"test content"), + template_key(b"awesome test content "), + template_key(b"even better test content"), + template_key(b"the best test content out there"), + ]; + } + + // template_header defines a block header used in testing + fn template_header(ticket_p: Vec, cid: Cid, timestamp: u64) -> BlockHeader { + let cids = key_setup(); + BlockHeader { + parents: TipSetKeys { + cids: vec![cids[3].clone()], + }, + weight: WEIGHT, + epoch: EPOCH, + height: HEIGHT, + miner_address: Address::new_secp256k1(ticket_p.clone()).unwrap(), + messages: cids[0].clone(), + message_receipts: cids[0].clone(), + state_root: cids[0].clone(), + timestamp, + ticket: Ticket { vrfproof: ticket_p }, + election_proof: vec![], + cached_cid: cid, + cached_bytes: CACHED_BYTES, + } + } + + // header_setup returns a vec of block headers to be used for testing purposes + fn header_setup() -> Vec { + let data0: Vec = vec![1, 4, 3, 6, 7, 1, 2]; + let data1: Vec = vec![1, 4, 3, 6, 1, 1, 2, 2, 4, 5, 3, 12, 2, 1]; + let data2: Vec = vec![1, 4, 3, 6, 1, 1, 2, 2, 4, 5, 3, 12, 2]; + let cids = key_setup(); + return vec![ + template_header(data0.clone(), cids[0].clone(), 1), + template_header(data1.clone(), cids[1].clone(), 2), + template_header(data2.clone(), cids[2].clone(), 3), + ]; + } + + fn setup() -> Tipset { + let headers = header_setup(); + return Tipset::new(headers.clone()).expect("tipset is invalid"); + } + + #[test] + fn new_test() { + let headers = header_setup(); + assert!(Tipset::new(headers).is_ok(), "result is invalid"); + } + + #[test] + fn min_ticket_test() { + let tipset = setup(); + let expected_value = vec![1, 4, 3, 6, 1, 1, 2, 2, 4, 5, 3, 12, 2]; + let min = Tipset::min_ticket(&tipset).unwrap(); + assert_eq!(min.vrfproof, expected_value); + } + + #[test] + fn min_timestamp_test() { + let tipset = setup(); + let min_time = Tipset::min_timestamp(&tipset).unwrap(); + assert_eq!(min_time, 1); + } + + #[test] + fn len_test() { + let tipset = setup(); + assert_eq!(Tipset::len(&tipset), 3); + } + + #[test] + fn is_empty_test() { + let tipset = setup(); + assert_eq!(Tipset::is_empty(&tipset), false); + } + + #[test] + fn height_test() { + let tipset = setup(); + assert_eq!(Tipset::height(&tipset), 1); + } + + #[test] + fn parents_test() { + let tipset = setup(); + let expected_value = template_key(b"the best test content out there"); + assert_eq!( + Tipset::parents(&tipset), + TipSetKeys { + cids: vec!(expected_value) + } + ); + } + + #[test] + fn weight_test() { + let tipset = setup(); + assert_eq!(Tipset::weight(&tipset), 1); + } + + #[test] + fn equals_test() { + let tipset_keys = TipSetKeys { + cids: key_setup().clone(), + }; + assert_eq!(TipSetKeys::equals(&tipset_keys, tipset_keys.clone()), true); + } +}