diff --git a/.travis.yml b/.travis.yml index 675eb0be1..b2859589b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,8 @@ branches: - master - /^beta-.*$/ - /^stable-.*$/ + - /^beta$/ + - /^stable$/ matrix: fast_finish: true include: diff --git a/Cargo.lock b/Cargo.lock index c451a6477..1fa63e621 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,10 +19,10 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "memchr 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -64,7 +64,7 @@ name = "clippy" version = "0.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "regex-syntax 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "semver 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-normalization 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -113,7 +113,7 @@ name = "docopt" version = "0.6.78" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "regex 0.1.48 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)", "strsim 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -137,7 +137,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "log 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 0.1.48 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -146,11 +146,11 @@ version = "0.5.4" source = "git+https://github.com/arkpar/rust-secp256k1.git#321e6c22a83606d1875f89cb61c9cb37c7d249ae" dependencies = [ "arrayvec 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", - "gcc 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)", + "gcc 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -193,7 +193,7 @@ dependencies = [ "jsonrpc-core 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpc-http-server 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde_macros 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", "target_info 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -220,7 +220,7 @@ dependencies = [ "rocksdb 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "rust-crypto 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", "sha3 0.1.0", "slab 0.1.4 (git+https://github.com/arkpar/slab.git)", "target_info 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -250,7 +250,7 @@ dependencies = [ [[package]] name = "gcc" -version = "0.3.23" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -326,7 +326,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "hyper 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 0.1.48 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)", "xml-rs 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)", "xmltree 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -349,7 +349,7 @@ name = "jsonrpc-core" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "serde 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde_macros 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -425,7 +425,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "memchr" -version = "0.1.7" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "libc 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", @@ -437,7 +437,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "log 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -556,17 +556,18 @@ dependencies = [ [[package]] name = "regex" -version = "0.1.48" +version = "0.1.51" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "aho-corasick 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "regex-syntax 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "aho-corasick 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "regex-syntax" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -582,7 +583,7 @@ name = "rust-crypto" version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "gcc 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)", + "gcc 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)", @@ -617,7 +618,7 @@ dependencies = [ [[package]] name = "serde" -version = "0.6.12" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "num 0.1.30 (registry+https://github.com/rust-lang/crates.io-index)", @@ -639,7 +640,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "num 0.1.30 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -654,7 +655,7 @@ dependencies = [ name = "sha3" version = "0.1.0" dependencies = [ - "gcc 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)", + "gcc 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -754,6 +755,11 @@ dependencies = [ "uuid 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "utf8-ranges" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "uuid" version = "0.1.18" diff --git a/ethcore/src/error.rs b/ethcore/src/error.rs index d441929c5..f75f338bf 100644 --- a/ethcore/src/error.rs +++ b/ethcore/src/error.rs @@ -20,59 +20,39 @@ use util::*; use header::BlockNumber; use basic_types::LogBloom; -#[derive(Debug, PartialEq, Eq)] -/// Error indicating an expected value was not found. -pub struct Mismatch { - /// Value expected. - pub expected: T, - /// Value found. - pub found: T, -} - -#[derive(Debug, PartialEq, Eq)] -/// Error indicating value found is outside of a valid range. -pub struct OutOfBounds { - /// Minimum allowed value. - pub min: Option, - /// Maximum allowed value. - pub max: Option, - /// Value found. - pub found: T, -} - /// Result of executing the transaction. #[derive(PartialEq, Debug)] pub enum ExecutionError { /// Returned when there gas paid for transaction execution is /// lower than base gas required. - NotEnoughBaseGas { + NotEnoughBaseGas { /// Absolute minimum gas required. - required: U256, + required: U256, /// Gas provided. got: U256 }, /// Returned when block (gas_used + gas) > gas_limit. - /// + /// /// If gas =< gas_limit, upstream may try to execute the transaction /// in next block. - BlockGasLimitReached { + BlockGasLimitReached { /// Gas limit of block for transaction. gas_limit: U256, /// Gas used in block prior to transaction. gas_used: U256, /// Amount of gas in block. - gas: U256 + gas: U256 }, /// Returned when transaction nonce does not match state nonce. - InvalidNonce { + InvalidNonce { /// Nonce expected. expected: U256, /// Nonce found. got: U256 }, - /// Returned when cost of transaction (value + gas_price * gas) exceeds + /// Returned when cost of transaction (value + gas_price * gas) exceeds /// current sender balance. - NotEnoughCash { + NotEnoughCash { /// Minimum required balance. required: U512, /// Actual balance. diff --git a/sync/src/chain.rs b/sync/src/chain.rs index 7a1643477..1c830ba7e 100644 --- a/sync/src/chain.rs +++ b/sync/src/chain.rs @@ -155,13 +155,17 @@ struct PeerInfo { /// Peer network id network_id: U256, /// Peer best block hash - latest: H256, + latest_hash: H256, + /// Peer best block number if known + latest_number: Option, /// Peer total difficulty difficulty: U256, /// Type of data currenty being requested from peer. asking: PeerAsking, /// A set of block numbers being requested asking_blocks: Vec, + /// Holds requested header hash if currently requesting block header by hash + asking_hash: Option, /// Request timestamp ask_time: f64, } @@ -179,6 +183,8 @@ pub struct ChainSync { downloading_headers: HashSet, /// Set of block body numbers being downloaded downloading_bodies: HashSet, + /// Set of block headers being downloaded by hash + downloading_hashes: HashSet, /// Downloaded headers. headers: Vec<(BlockNumber, Vec
)>, //TODO: use BTreeMap once range API is sable. For now it is a vector sorted in descending order /// Downloaded bodies @@ -195,6 +201,8 @@ pub struct ChainSync { syncing_difficulty: U256, /// True if common block for our and remote chain has been found have_common_block: bool, + /// Last propagated block number + last_send_block_number: BlockNumber, } type RlpResponseResult = Result, PacketDecodeError>; @@ -208,6 +216,7 @@ impl ChainSync { highest_block: None, downloading_headers: HashSet::new(), downloading_bodies: HashSet::new(), + downloading_hashes: HashSet::new(), headers: Vec::new(), bodies: Vec::new(), peers: HashMap::new(), @@ -216,6 +225,7 @@ impl ChainSync { last_imported_hash: None, syncing_difficulty: U256::from(0u64), have_common_block: false, + last_send_block_number: 0, } } @@ -250,6 +260,7 @@ impl ChainSync { self.bodies.clear(); for (_, ref mut p) in &mut self.peers { p.asking_blocks.clear(); + p.asking_hash = None; } self.header_ids.clear(); self.syncing_difficulty = From::from(0u64); @@ -275,15 +286,21 @@ impl ChainSync { protocol_version: try!(r.val_at(0)), network_id: try!(r.val_at(1)), difficulty: try!(r.val_at(2)), - latest: try!(r.val_at(3)), + latest_hash: try!(r.val_at(3)), + latest_number: None, genesis: try!(r.val_at(4)), asking: PeerAsking::Nothing, asking_blocks: Vec::new(), + asking_hash: None, ask_time: 0f64, }; - trace!(target: "sync", "New peer {} (protocol: {}, network: {:?}, difficulty: {:?}, latest:{}, genesis:{})", peer_id, peer.protocol_version, peer.network_id, peer.difficulty, peer.latest, peer.genesis); + trace!(target: "sync", "New peer {} (protocol: {}, network: {:?}, difficulty: {:?}, latest:{}, genesis:{})", peer_id, peer.protocol_version, peer.network_id, peer.difficulty, peer.latest_hash, peer.genesis); + if self.peers.contains_key(&peer_id) { + warn!("Unexpected status packet from {}:{}", peer_id, io.peer_info(peer_id)); + return Ok(()); + } let chain_info = io.chain().chain_info(); if peer.genesis != chain_info.genesis_hash { io.disable_peer(peer_id); @@ -296,10 +313,7 @@ impl ChainSync { return Ok(()); } - let old = self.peers.insert(peer_id.clone(), peer); - if old.is_some() { - panic!("ChainSync: new peer already exists"); - } + self.peers.insert(peer_id.clone(), peer); info!(target: "sync", "Connected {}:{}", peer_id, io.peer_info(peer_id)); self.sync_peer(io, peer_id, false); Ok(()) @@ -439,6 +453,11 @@ impl ChainSync { trace!(target: "sync", "{} -> NewBlock ({})", peer_id, h); let header: BlockHeader = try!(header_rlp.as_val()); let mut unknown = false; + { + let peer = self.peers.get_mut(&peer_id).unwrap(); + peer.latest_hash = header.hash(); + peer.latest_number = Some(header.number()); + } // TODO: Decompose block and add to self.headers and self.bodies instead if header.number == From::from(self.current_base_block() + 1) { match io.chain().import_block(block_rlp.as_raw().to_vec()) { @@ -449,6 +468,7 @@ impl ChainSync { trace!(target: "sync", "New block already queued {:?}", h); }, Ok(_) => { + self.last_imported_block = Some(header.number); trace!(target: "sync", "New block queued {:?}", h); }, Err(ImportError::UnknownParent) => { @@ -468,13 +488,9 @@ impl ChainSync { trace!(target: "sync", "New block unknown {:?}", h); //TODO: handle too many unknown blocks let difficulty: U256 = try!(r.val_at(1)); - let peer_difficulty = self.peers.get_mut(&peer_id).expect("ChainSync: unknown peer").difficulty; + let peer_difficulty = self.peers.get_mut(&peer_id).unwrap().difficulty; if difficulty > peer_difficulty { trace!(target: "sync", "Received block {:?} with no known parent. Peer needs syncing...", h); - { - let peer = self.peers.get_mut(&peer_id).expect("ChainSync: unknown peer"); - peer.latest = header.hash(); - } self.sync_peer(io, peer_id, true); } } @@ -483,16 +499,19 @@ impl ChainSync { /// Handles NewHashes packet. Initiates headers download for any unknown hashes. fn on_peer_new_hashes(&mut self, io: &mut SyncIo, peer_id: PeerId, r: &UntrustedRlp) -> Result<(), PacketDecodeError> { - if self.peers.get_mut(&peer_id).expect("ChainSync: unknown peer").asking != PeerAsking::Nothing { + if self.peers.get_mut(&peer_id).unwrap().asking != PeerAsking::Nothing { trace!(target: "sync", "Ignoring new hashes since we're already downloading."); return Ok(()); } trace!(target: "sync", "{} -> NewHashes ({} entries)", peer_id, r.item_count()); - let hashes = r.iter().map(|item| (item.val_at::(0), item.val_at::(1))); - let mut max_height: U256 = From::from(0); + let hashes = r.iter().map(|item| (item.val_at::(0), item.val_at::(1))); + let mut max_height: BlockNumber = 0; for (rh, rd) in hashes { let h = try!(rh); let d = try!(rd); + if self.downloading_hashes.contains(&h) { + continue; + } match io.chain().block_status(BlockId::Hash(h.clone())) { BlockStatus::InChain => { trace!(target: "sync", "New block hash already in chain {:?}", h); @@ -501,10 +520,11 @@ impl ChainSync { trace!(target: "sync", "New hash block already queued {:?}", h); }, BlockStatus::Unknown => { - trace!(target: "sync", "New unknown block hash {:?}", h); if d > max_height { - let peer = self.peers.get_mut(&peer_id).expect("ChainSync: unknown peer"); - peer.latest = h.clone(); + trace!(target: "sync", "New unknown block hash {:?}", h); + let peer = self.peers.get_mut(&peer_id).unwrap(); + peer.latest_hash = h.clone(); + peer.latest_number = Some(d); max_height = d; } }, @@ -515,7 +535,7 @@ impl ChainSync { } } }; - if max_height != x!(0) { + if max_height != 0 { self.sync_peer(io, peer_id, true); } Ok(()) @@ -525,7 +545,7 @@ impl ChainSync { pub fn on_peer_aborting(&mut self, io: &mut SyncIo, peer: PeerId) { trace!(target: "sync", "== Disconnecting {}", peer); if self.peers.contains_key(&peer) { - info!(target: "sync", "Disconnected {}:{}", peer, io.peer_info(peer)); + info!(target: "sync", "Disconnected {}", peer); self.clear_peer_download(peer); self.peers.remove(&peer); self.continue_sync(io); @@ -563,7 +583,7 @@ impl ChainSync { /// Find something to do for a peer. Called for a new peer or when a peer is done with it's task. fn sync_peer(&mut self, io: &mut SyncIo, peer_id: PeerId, force: bool) { let (peer_latest, peer_difficulty) = { - let peer = self.peers.get_mut(&peer_id).expect("ChainSync: unknown peer"); + let peer = self.peers.get_mut(&peer_id).unwrap(); if peer.asking != PeerAsking::Nothing { return; } @@ -571,7 +591,7 @@ impl ChainSync { trace!(target: "sync", "Waiting for block queue"); return; } - (peer.latest.clone(), peer.difficulty.clone()) + (peer.latest_hash.clone(), peer.difficulty.clone()) }; let td = io.chain().chain_info().pending_total_difficulty; @@ -583,6 +603,8 @@ impl ChainSync { self.state = SyncState::Blocks; } trace!(target: "sync", "Starting sync with better chain"); + self.peers.get_mut(&peer_id).unwrap().asking_hash = Some(peer_latest.clone()); + self.downloading_hashes.insert(peer_latest.clone()); self.request_headers_by_hash(io, peer_id, &peer_latest, 1, 0, false); } else if self.state == SyncState::Blocks && io.chain().block_status(BlockId::Hash(peer_latest)) == BlockStatus::Unknown { @@ -675,6 +697,8 @@ impl ChainSync { } } else { + // continue search for common block + self.downloading_headers.insert(start as BlockNumber); self.request_headers_by_number(io, peer_id, start as BlockNumber, 1, 0, false); } } @@ -682,7 +706,10 @@ impl ChainSync { /// Clear all blocks/headers marked as being downloaded by a peer. fn clear_peer_download(&mut self, peer_id: PeerId) { - let peer = self.peers.get_mut(&peer_id).expect("ChainSync: unknown peer"); + let peer = self.peers.get_mut(&peer_id).unwrap(); + if let Some(hash) = peer.asking_hash.take() { + self.downloading_hashes.remove(&hash); + } for b in &peer.asking_blocks { self.downloading_headers.remove(&b); self.downloading_bodies.remove(&b); @@ -815,7 +842,7 @@ impl ChainSync { /// Reset peer status after request is complete. fn reset_peer_asking(&mut self, peer_id: PeerId, asking: PeerAsking) { - let peer = self.peers.get_mut(&peer_id).expect("ChainSync: unknown peer"); + let peer = self.peers.get_mut(&peer_id).unwrap(); if peer.asking != asking { warn!(target:"sync", "Asking {:?} while expected {:?}", peer.asking, asking); } @@ -827,9 +854,9 @@ impl ChainSync { /// Generic request sender fn send_request(&mut self, sync: &mut SyncIo, peer_id: PeerId, asking: PeerAsking, packet_id: PacketId, packet: Bytes) { { - let peer = self.peers.get_mut(&peer_id).expect("ChainSync: unknown peer"); + let peer = self.peers.get_mut(&peer_id).unwrap(); if peer.asking != PeerAsking::Nothing { - warn!(target:"sync", "Asking {:?} while requesting {:?}", asking, peer.asking); + warn!(target:"sync", "Asking {:?} while requesting {:?}", peer.asking, asking); } } match sync.send(peer_id, packet_id, packet) { @@ -846,6 +873,14 @@ impl ChainSync { } } + /// Generic packet sender + fn send_packet(&mut self, sync: &mut SyncIo, peer_id: PeerId, packet_id: PacketId, packet: Bytes) { + if let Err(e) = sync.send(peer_id, packet_id, packet) { + warn!(target:"sync", "Error sending packet: {:?}", e); + sync.disable_peer(peer_id); + self.on_peer_aborting(sync, peer_id); + } + } /// Called when peer sends us new transactions fn on_peer_transactions(&mut self, _io: &mut SyncIo, _peer_id: PeerId, _r: &UntrustedRlp) -> Result<(), PacketDecodeError> { Ok(()) @@ -1002,6 +1037,11 @@ impl ChainSync { /// Dispatch incoming requests and responses pub fn on_packet(&mut self, io: &mut SyncIo, peer: PeerId, packet_id: u8, data: &[u8]) { let rlp = UntrustedRlp::new(data); + + if packet_id != STATUS_PACKET && !self.peers.contains_key(&peer) { + warn!(target:"sync", "Unexpected packet from unregistered peer: {}:{}", peer, io.peer_info(peer)); + return; + } let result = match packet_id { STATUS_PACKET => self.on_peer_status(io, peer, &rlp), TRANSACTIONS_PACKET => self.on_peer_transactions(io, peer, &rlp), @@ -1063,7 +1103,6 @@ impl ChainSync { for block_hash in route.blocks { let mut hash_rlp = RlpStream::new_list(2); let difficulty = chain.block_total_difficulty(BlockId::Hash(block_hash.clone())).expect("Mallformed block without a difficulty on the chain!"); - hash_rlp.append(&block_hash); hash_rlp.append(&difficulty); rlp_stream.append_raw(&hash_rlp.out(), 1); @@ -1079,32 +1118,34 @@ impl ChainSync { /// creates latest block rlp for the given client fn create_latest_block_rlp(chain: &BlockChainClient) -> Bytes { let mut rlp_stream = RlpStream::new_list(2); - rlp_stream.append_raw(&chain.block(BlockId::Hash(chain.chain_info().best_block_hash)).expect("Creating latest block when there is none"), 1); + rlp_stream.append_raw(&chain.block(BlockId::Hash(chain.chain_info().best_block_hash)).unwrap(), 1); rlp_stream.append(&chain.chain_info().total_difficulty); rlp_stream.out() } /// returns peer ids that have less blocks than our chain - fn get_lagging_peers(&self, io: &SyncIo) -> Vec { + fn get_lagging_peers(&mut self, io: &SyncIo) -> Vec<(PeerId, BlockNumber)> { let chain = io.chain(); let chain_info = chain.chain_info(); let latest_hash = chain_info.best_block_hash; let latest_number = chain_info.best_block_number; - self.peers.iter().filter(|&(_, peer_info)| - match io.chain().block_status(BlockId::Hash(peer_info.latest.clone())) - { + self.peers.iter_mut().filter_map(|(&id, ref mut peer_info)| + match io.chain().block_status(BlockId::Hash(peer_info.latest_hash.clone())) { BlockStatus::InChain => { - let peer_number = HeaderView::new(&io.chain().block_header(BlockId::Hash(peer_info.latest.clone())).unwrap()).number(); - peer_info.latest != latest_hash && latest_number > peer_number && latest_number - peer_number < MAX_PEER_LAG_PROPAGATION + if peer_info.latest_number.is_none() { + peer_info.latest_number = Some(HeaderView::new(&io.chain().block_header(BlockId::Hash(peer_info.latest_hash.clone())).unwrap()).number()); + } + if peer_info.latest_hash != latest_hash && latest_number > peer_info.latest_number.unwrap() { + Some((id, peer_info.latest_number.unwrap())) + } else { None } }, - _ => false + _ => None }) - .map(|(peer_id, _)| peer_id) - .cloned().collect::>() + .collect::>() } /// propagades latest block to lagging peers - fn propagade_blocks(&mut self, io: &mut SyncIo) -> usize { + fn propagade_blocks(&mut self, local_best: &H256, best_number: BlockNumber, io: &mut SyncIo) -> usize { let updated_peers = { let lagging_peers = self.get_lagging_peers(io); @@ -1112,37 +1153,43 @@ impl ChainSync { let fraction = (self.peers.len() as f64).powf(-0.5).mul(u32::max_value() as f64).round() as u32; let lucky_peers = match lagging_peers.len() { 0 ... MIN_PEERS_PROPAGATION => lagging_peers, - _ => lagging_peers.iter().filter(|_| ::rand::random::() < fraction).cloned().collect::>() + _ => lagging_peers.into_iter().filter(|_| ::rand::random::() < fraction).collect::>() }; // taking at max of MAX_PEERS_PROPAGATION - lucky_peers.iter().take(min(lucky_peers.len(), MAX_PEERS_PROPAGATION)).cloned().collect::>() + lucky_peers.iter().map(|&(id, _)| id.clone()).take(min(lucky_peers.len(), MAX_PEERS_PROPAGATION)).collect::>() }; let mut sent = 0; - let local_best = io.chain().chain_info().best_block_hash; for peer_id in updated_peers { let rlp = ChainSync::create_latest_block_rlp(io.chain()); - self.send_request(io, peer_id, PeerAsking::Nothing, NEW_BLOCK_PACKET, rlp); - self.peers.get_mut(&peer_id).expect("ChainSync: unknown peer").latest = local_best.clone(); + self.send_packet(io, peer_id, NEW_BLOCK_PACKET, rlp); + self.peers.get_mut(&peer_id).unwrap().latest_hash = local_best.clone(); + self.peers.get_mut(&peer_id).unwrap().latest_number = Some(best_number); sent = sent + 1; } sent } /// propagades new known hashes to all peers - fn propagade_new_hashes(&mut self, io: &mut SyncIo) -> usize { + fn propagade_new_hashes(&mut self, local_best: &H256, best_number: BlockNumber, io: &mut SyncIo) -> usize { let updated_peers = self.get_lagging_peers(io); let mut sent = 0; - let local_best = io.chain().chain_info().best_block_hash; - for peer_id in updated_peers { - sent = sent + match ChainSync::create_new_hashes_rlp(io.chain(), &self.peers.get(&peer_id).expect("ChainSync: unknown peer").latest, &local_best) { + let last_parent = HeaderView::new(&io.chain().block_header(BlockId::Hash(local_best.clone())).unwrap()).parent_hash(); + for (peer_id, peer_number) in updated_peers { + let mut peer_best = self.peers.get(&peer_id).unwrap().latest_hash.clone(); + if best_number - peer_number > MAX_PEERS_PROPAGATION as BlockNumber { + // If we think peer is too far behind just end one latest hash + peer_best = last_parent.clone(); + } + sent = sent + match ChainSync::create_new_hashes_rlp(io.chain(), &peer_best, &local_best) { Some(rlp) => { { - let peer = self.peers.get_mut(&peer_id).expect("ChainSync: unknown peer"); - peer.latest = local_best.clone(); + let peer = self.peers.get_mut(&peer_id).unwrap(); + peer.latest_hash = local_best.clone(); + peer.latest_number = Some(best_number); } - self.send_request(io, peer_id, PeerAsking::Nothing, NEW_BLOCK_HASHES_PACKET, rlp); + self.send_packet(io, peer_id, NEW_BLOCK_HASHES_PACKET, rlp); 1 }, None => 0 @@ -1154,15 +1201,19 @@ impl ChainSync { /// Maintain other peers. Send out any new blocks and transactions pub fn maintain_sync(&mut self, io: &mut SyncIo) { self.check_resume(io); - - let peers = self.propagade_new_hashes(io); - trace!(target: "sync", "Sent new hashes to peers: {:?}", peers); } /// should be called once chain has new block, triggers the latest block propagation pub fn chain_blocks_verified(&mut self, io: &mut SyncIo) { - let peers = self.propagade_blocks(io); - trace!(target: "sync", "Sent latest block to peers: {:?}", peers); + let chain = io.chain().chain_info(); + if (((chain.best_block_number as i64) - (self.last_send_block_number as i64)).abs() as BlockNumber) < MAX_PEER_LAG_PROPAGATION { + let blocks = self.propagade_blocks(&chain.best_block_hash, chain.best_block_number, io); + let hashes = self.propagade_new_hashes(&chain.best_block_hash, chain.best_block_number, io); + if blocks != 0 || hashes != 0 { + trace!(target: "sync", "Sent latest {} blocks and {} hashes to peers.", blocks, hashes); + } + } + self.last_send_block_number = chain.best_block_number; } } @@ -1248,9 +1299,9 @@ mod tests { // the length of two rlp-encoded receipts assert_eq!(597, rlp_result.unwrap().1.out().len()); - let mut sync = ChainSync::new(); + let mut sync = dummy_sync_with_peer(H256::new()); io.sender = Some(2usize); - sync.on_packet(&mut io, 1usize, super::GET_RECEIPTS_PACKET, &receipts_request); + sync.on_packet(&mut io, 0usize, super::GET_RECEIPTS_PACKET, &receipts_request); assert_eq!(1, io.queue.len()); } @@ -1276,9 +1327,9 @@ mod tests { // the length of one rlp-encoded hashe assert_eq!(34, rlp_result.unwrap().1.out().len()); - let mut sync = ChainSync::new(); + let mut sync = dummy_sync_with_peer(H256::new()); io.sender = Some(2usize); - sync.on_packet(&mut io, 1usize, super::GET_NODE_DATA_PACKET, &node_request); + sync.on_packet(&mut io, 0usize, super::GET_NODE_DATA_PACKET, &node_request); assert_eq!(1, io.queue.len()); } @@ -1289,10 +1340,12 @@ mod tests { protocol_version: 0, genesis: H256::zero(), network_id: U256::zero(), - latest: peer_latest_hash, + latest_hash: peer_latest_hash, + latest_number: None, difficulty: U256::zero(), asking: PeerAsking::Nothing, asking_blocks: Vec::::new(), + asking_hash: None, ask_time: 0f64, }); sync @@ -1303,7 +1356,7 @@ mod tests { let mut client = TestBlockChainClient::new(); client.add_blocks(100, false); let mut queue = VecDeque::new(); - let sync = dummy_sync_with_peer(client.block_hash_delta_minus(10)); + let mut sync = dummy_sync_with_peer(client.block_hash_delta_minus(10)); let io = TestIo::new(&mut client, &mut queue, None); let lagging_peers = sync.get_lagging_peers(&io); @@ -1334,9 +1387,11 @@ mod tests { client.add_blocks(100, false); let mut queue = VecDeque::new(); let mut sync = dummy_sync_with_peer(client.block_hash_delta_minus(5)); + let best_hash = client.chain_info().best_block_hash.clone(); + let best_number = client.chain_info().best_block_number; let mut io = TestIo::new(&mut client, &mut queue, None); - let peer_count = sync.propagade_new_hashes(&mut io); + let peer_count = sync.propagade_new_hashes(&best_hash, best_number, &mut io); // 1 message should be send assert_eq!(1, io.queue.len()); @@ -1352,9 +1407,11 @@ mod tests { client.add_blocks(100, false); let mut queue = VecDeque::new(); let mut sync = dummy_sync_with_peer(client.block_hash_delta_minus(5)); + let best_hash = client.chain_info().best_block_hash.clone(); + let best_number = client.chain_info().best_block_number; let mut io = TestIo::new(&mut client, &mut queue, None); - let peer_count = sync.propagade_blocks(&mut io); + let peer_count = sync.propagade_blocks(&best_hash, best_number, &mut io); // 1 message should be send assert_eq!(1, io.queue.len()); @@ -1456,9 +1513,11 @@ mod tests { client.add_blocks(100, false); let mut queue = VecDeque::new(); let mut sync = dummy_sync_with_peer(client.block_hash_delta_minus(5)); + let best_hash = client.chain_info().best_block_hash.clone(); + let best_number = client.chain_info().best_block_number; let mut io = TestIo::new(&mut client, &mut queue, None); - sync.propagade_new_hashes(&mut io); + sync.propagade_new_hashes(&best_hash, best_number, &mut io); let data = &io.queue[0].data.clone(); let result = sync.on_peer_new_hashes(&mut io, 0, &UntrustedRlp::new(&data)); @@ -1473,9 +1532,11 @@ mod tests { client.add_blocks(100, false); let mut queue = VecDeque::new(); let mut sync = dummy_sync_with_peer(client.block_hash_delta_minus(5)); + let best_hash = client.chain_info().best_block_hash.clone(); + let best_number = client.chain_info().best_block_number; let mut io = TestIo::new(&mut client, &mut queue, None); - sync.propagade_blocks(&mut io); + sync.propagade_blocks(&best_hash, best_number, &mut io); let data = &io.queue[0].data.clone(); let result = sync.on_peer_new_block(&mut io, 0, &UntrustedRlp::new(&data)); diff --git a/sync/src/tests/chain.rs b/sync/src/tests/chain.rs index f560f4ca6..1dd9a1e78 100644 --- a/sync/src/tests/chain.rs +++ b/sync/src/tests/chain.rs @@ -109,7 +109,7 @@ fn status_empty() { #[test] fn status_packet() { let mut net = TestNet::new(2); - net.peer_mut(0).chain.add_blocks(1000, false); + net.peer_mut(0).chain.add_blocks(100, false); net.peer_mut(1).chain.add_blocks(1, false); net.start(); @@ -122,18 +122,28 @@ fn status_packet() { #[test] fn propagade_hashes() { - let mut net = TestNet::new(3); - net.peer_mut(1).chain.add_blocks(1000, false); - net.peer_mut(2).chain.add_blocks(1000, false); + let mut net = TestNet::new(6); + net.peer_mut(1).chain.add_blocks(10, false); net.sync(); net.peer_mut(0).chain.add_blocks(10, false); - net.sync_step_peer(0); + net.sync(); + net.trigger_block_verified(0); //first event just sets the marker + net.trigger_block_verified(0); - // 2 peers to sync - assert_eq!(2, net.peer(0).queue.len()); - // NEW_BLOCK_HASHES_PACKET - assert_eq!(0x01, net.peer(0).queue[0].packet_id); + // 5 peers to sync + assert_eq!(5, net.peer(0).queue.len()); + let mut hashes = 0; + let mut blocks = 0; + for i in 0..5 { + if net.peer(0).queue[i].packet_id == 0x1 { + hashes += 1; + } + if net.peer(0).queue[i].packet_id == 0x7 { + blocks += 1; + } + } + assert!(blocks + hashes == 5); } #[test] @@ -143,9 +153,21 @@ fn propagade_blocks() { net.sync(); net.peer_mut(0).chain.add_blocks(10, false); + net.trigger_block_verified(0); //first event just sets the marker net.trigger_block_verified(0); assert!(!net.peer(0).queue.is_empty()); // NEW_BLOCK_PACKET assert_eq!(0x07, net.peer(0).queue[0].packet_id); } + +#[test] +fn restart_on_malformed_block() { + let mut net = TestNet::new(2); + net.peer_mut(1).chain.add_blocks(10, false); + net.peer_mut(1).chain.corrupt_block(6); + net.sync_steps(10); + + assert_eq!(net.peer(0).chain.chain_info().best_block_number, 4); +} + diff --git a/sync/src/tests/helpers.rs b/sync/src/tests/helpers.rs index d8cd5e54a..c561b65a3 100644 --- a/sync/src/tests/helpers.rs +++ b/sync/src/tests/helpers.rs @@ -71,6 +71,17 @@ impl TestBlockChainClient { } } + pub fn corrupt_block(&mut self, n: BlockNumber) { + let hash = self.block_hash(BlockId::Number(n)).unwrap(); + let mut header: BlockHeader = decode(&self.block_header(BlockId::Number(n)).unwrap()); + header.parent_hash = H256::new(); + let mut rlp = RlpStream::new_list(3); + rlp.append(&header); + rlp.append_raw(&rlp::NULL_RLP, 1); + rlp.append_raw(&rlp::NULL_RLP, 1); + self.blocks.write().unwrap().insert(hash, rlp.out()); + } + pub fn block_hash_delta_minus(&mut self, delta: usize) -> H256 { let blocks_read = self.numbers.read().unwrap(); let index = blocks_read.len() - delta; diff --git a/util/src/error.rs b/util/src/error.rs index 465174b4e..68aa3e648 100644 --- a/util/src/error.rs +++ b/util/src/error.rs @@ -20,6 +20,7 @@ use rustc_serialize::hex::FromHexError; use network::NetworkError; use rlp::DecoderError; use io; +use std::fmt; #[derive(Debug)] /// Error in database subsystem. @@ -55,6 +56,27 @@ pub enum UtilError { BadSize, } + +#[derive(Debug, PartialEq, Eq)] +/// Error indicating an expected value was not found. +pub struct Mismatch { + /// Value expected. + pub expected: T, + /// Value found. + pub found: T, +} + +#[derive(Debug, PartialEq, Eq)] +/// Error indicating value found is outside of a valid range. +pub struct OutOfBounds { + /// Minimum allowed value. + pub min: Option, + /// Maximum allowed value. + pub max: Option, + /// Value found. + pub found: T, +} + impl From for UtilError { fn from(err: FromHexError) -> UtilError { UtilError::FromHex(err) diff --git a/util/src/keys/directory.rs b/util/src/keys/directory.rs new file mode 100644 index 000000000..c5e17100f --- /dev/null +++ b/util/src/keys/directory.rs @@ -0,0 +1,1088 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +//! Keys Directory + +use common::*; +use std::path::{PathBuf}; + +const CURRENT_DECLARED_VERSION: u64 = 3; +const MAX_KEY_FILE_LEN: u64 = 1024 * 80; +const MAX_CACHE_USAGE_TRACK: usize = 128; + +/// Cipher type (currently only aes-128-ctr) +#[derive(PartialEq, Debug, Clone)] +pub enum CryptoCipherType { + /// aes-128-ctr with 128-bit initialisation vector(iv) + Aes128Ctr(U128) +} + +#[derive(PartialEq, Debug, Clone)] +enum KeyFileVersion { + V3(u64) +} + +/// key generator function +#[derive(PartialEq, Debug, Clone)] +pub enum Pbkdf2CryptoFunction { + /// keyed-hash generator (HMAC-256) + HMacSha256 +} + +#[derive(Clone)] +/// Kdf of type `Pbkdf2` +/// https://en.wikipedia.org/wiki/PBKDF2 +pub struct KdfPbkdf2Params { + /// desired length of the derived key, in octets + pub dk_len: u32, + /// cryptographic salt + pub salt: H256, + /// number of iterations for derived key + pub c: u32, + /// pseudo-random 2-parameters function + pub prf: Pbkdf2CryptoFunction +} + +#[derive(Debug)] +enum Pbkdf2ParseError { + InvalidParameter(&'static str), + InvalidPrf(Mismatch), + InvalidSaltFormat(UtilError), + MissingParameter(&'static str), +} + +impl KdfPbkdf2Params { + fn from_json(json: &BTreeMap) -> Result { + Ok(KdfPbkdf2Params{ + salt: match try!(json.get("salt").ok_or(Pbkdf2ParseError::MissingParameter("salt"))).as_string() { + None => { return Err(Pbkdf2ParseError::InvalidParameter("salt")) }, + Some(salt_value) => match H256::from_str(salt_value) { + Ok(salt_hex_value) => salt_hex_value, + Err(from_hex_error) => { return Err(Pbkdf2ParseError::InvalidSaltFormat(from_hex_error)); }, + } + }, + prf: match try!(json.get("prf").ok_or(Pbkdf2ParseError::MissingParameter("prf"))).as_string() { + Some("hmac-sha256") => Pbkdf2CryptoFunction::HMacSha256, + Some(unexpected_prf) => { return Err(Pbkdf2ParseError::InvalidPrf(Mismatch { expected: "hmac-sha256".to_owned(), found: unexpected_prf.to_owned() })); }, + None => { return Err(Pbkdf2ParseError::InvalidParameter("prf")); }, + }, + dk_len: try!(try!(json.get("dklen").ok_or(Pbkdf2ParseError::MissingParameter("dklen"))).as_u64().ok_or(Pbkdf2ParseError::InvalidParameter("dkLen"))) as u32, + c: try!(try!(json.get("c").ok_or(Pbkdf2ParseError::MissingParameter("c"))).as_u64().ok_or(Pbkdf2ParseError::InvalidParameter("c"))) as u32, + }) + } + + fn to_json(&self) -> Json { + let mut map = BTreeMap::new(); + map.insert("dklen".to_owned(), json_from_u32(self.dk_len)); + map.insert("salt".to_owned(), Json::String(format!("{:?}", self.salt))); + map.insert("prf".to_owned(), Json::String("hmac-sha256".to_owned())); + map.insert("c".to_owned(), json_from_u32(self.c)); + Json::Object(map) + } +} + +#[derive(Clone)] +/// Kdf of type `Scrypt`. +/// https://en.wikipedia.org/wiki/Scrypt +pub struct KdfScryptParams { + /// Desired length of the derived key, in octets. + pub dk_len: u32, + /// Parallelization parameter. + pub p: u32, + /// CPU/memory cost parameter. + pub n: u32, + /// TODO: comment + pub r: u32, + /// Cryptographic salt. + pub salt: H256, +} + +#[derive(Debug)] +enum ScryptParseError { + InvalidParameter(&'static str), + InvalidSaltFormat(UtilError), + MissingParameter(&'static str), +} + +fn json_from_u32(number: u32) -> Json { Json::U64(number as u64) } + +impl KdfScryptParams { + fn from_json(json: &BTreeMap) -> Result { + Ok(KdfScryptParams{ + salt: match try!(json.get("salt").ok_or(ScryptParseError::MissingParameter("salt"))).as_string() { + None => { return Err(ScryptParseError::InvalidParameter("salt")) }, + Some(salt_value) => match H256::from_str(salt_value) { + Ok(salt_hex_value) => salt_hex_value, + Err(from_hex_error) => { return Err(ScryptParseError::InvalidSaltFormat(from_hex_error)); }, + } + }, + dk_len: try!(try!(json.get("dklen").ok_or(ScryptParseError::MissingParameter("dklen"))).as_u64().ok_or(ScryptParseError::InvalidParameter("dkLen"))) as u32, + p: try!(try!(json.get("p").ok_or(ScryptParseError::MissingParameter("p"))).as_u64().ok_or(ScryptParseError::InvalidParameter("p"))) as u32, + n: try!(try!(json.get("n").ok_or(ScryptParseError::MissingParameter("n"))).as_u64().ok_or(ScryptParseError::InvalidParameter("n"))) as u32, + r: try!(try!(json.get("r").ok_or(ScryptParseError::MissingParameter("r"))).as_u64().ok_or(ScryptParseError::InvalidParameter("r"))) as u32, + }) + } + + fn to_json(&self) -> Json { + let mut map = BTreeMap::new(); + map.insert("dklen".to_owned(), json_from_u32(self.dk_len)); + map.insert("salt".to_owned(), Json::String(format!("{:?}", self.salt))); + map.insert("p".to_owned(), json_from_u32(self.p)); + map.insert("n".to_owned(), json_from_u32(self.n)); + map.insert("r".to_owned(), json_from_u32(self.r)); + Json::Object(map) + } +} + +#[derive(Clone)] +/// Settings for password derived key geberator function. +pub enum KeyFileKdf { + /// Password-Based Key Derivation Function 2 (PBKDF2) type. + /// https://en.wikipedia.org/wiki/PBKDF2 + Pbkdf2(KdfPbkdf2Params), + /// Scrypt password-based key derivation function. + /// https://en.wikipedia.org/wiki/Scrypt + Scrypt(KdfScryptParams) +} + +#[derive(Clone)] +/// Encrypted password or other arbitrary message +/// with settings for password derived key generator for decrypting content. +pub struct KeyFileCrypto { + /// Cipher type. + pub cipher_type: CryptoCipherType, + /// Cipher text (encrypted message). + pub cipher_text: Bytes, + /// Password derived key generator function settings. + pub kdf: KeyFileKdf, +} + +impl KeyFileCrypto { + fn from_json(json: &Json) -> Result { + let as_object = match json.as_object() { + None => { return Err(CryptoParseError::InvalidJsonFormat); } + Some(obj) => obj + }; + + let cipher_type = match try!(as_object.get("cipher").ok_or(CryptoParseError::NoCipherType)).as_string() { + None => { return Err(CryptoParseError::InvalidCipherType(Mismatch { expected: "aes-128-ctr".to_owned(), found: "not a json string".to_owned() })); } + Some("aes-128-ctr") => CryptoCipherType::Aes128Ctr( + match try!(as_object.get("cipherparams").ok_or(CryptoParseError::NoCipherParameters)).as_object() { + None => { return Err(CryptoParseError::NoCipherParameters); }, + Some(cipher_param) => match U128::from_str(match cipher_param["iv"].as_string() { + None => { return Err(CryptoParseError::NoInitialVector); }, + Some(iv_hex_string) => iv_hex_string + }) + { + Ok(iv_value) => iv_value, + Err(hex_error) => { return Err(CryptoParseError::InvalidInitialVector(hex_error)); } + } + } + ), + Some(other_cipher_type) => { + return Err(CryptoParseError::InvalidCipherType( + Mismatch { expected: "aes-128-ctr".to_owned(), found: other_cipher_type.to_owned() })); + } + }; + + let kdf = match (try!(as_object.get("kdf").ok_or(CryptoParseError::NoKdf)).as_string(), try!(as_object.get("kdfparams").ok_or(CryptoParseError::NoKdfType)).as_object()) { + (None, _) => { return Err(CryptoParseError::NoKdfType); }, + (Some("scrypt"), Some(kdf_params)) => + match KdfScryptParams::from_json(kdf_params) { + Err(scrypt_params_error) => { return Err(CryptoParseError::Scrypt(scrypt_params_error)); }, + Ok(scrypt_params) => KeyFileKdf::Scrypt(scrypt_params) + }, + (Some("pbkdf2"), Some(kdf_params)) => + match KdfPbkdf2Params::from_json(kdf_params) { + Err(pbkdf2_params_error) => { return Err(CryptoParseError::KdfPbkdf2(pbkdf2_params_error)); }, + Ok(pbkdf2_params) => KeyFileKdf::Pbkdf2(pbkdf2_params) + }, + (Some(other_kdf), _) => { + return Err(CryptoParseError::InvalidKdfType( + Mismatch { expected: "pbkdf2/scrypt".to_owned(), found: other_kdf.to_owned()})); + } + }; + + let cipher_text = match as_object["ciphertext"].as_string() { + None => { return Err(CryptoParseError::NoCipherText); } + Some(text) => text + }; + + Ok(KeyFileCrypto { + cipher_text: Bytes::from(cipher_text), + cipher_type: cipher_type, + kdf: kdf, + }) + } + + fn to_json(&self) -> Json { + let mut map = BTreeMap::new(); + match self.cipher_type { + CryptoCipherType::Aes128Ctr(iv) => { + map.insert("cipher".to_owned(), Json::String("aes-128-ctr".to_owned())); + let mut cipher_params = BTreeMap::new(); + cipher_params.insert("iv".to_owned(), Json::String(format!("{:?}", iv))); + map.insert("cipherparams".to_owned(), Json::Object(cipher_params)); + } + } + map.insert("ciphertext".to_owned(), Json::String( + self.cipher_text.iter().map(|b| format!("{:02x}", b)).collect::>().join(""))); + + map.insert("kdf".to_owned(), Json::String(match self.kdf { + KeyFileKdf::Pbkdf2(_) => "pbkdf2".to_owned(), + KeyFileKdf::Scrypt(_) => "scrypt".to_owned() + })); + + map.insert("kdfparams".to_owned(), match self.kdf { + KeyFileKdf::Pbkdf2(ref pbkdf2_params) => pbkdf2_params.to_json(), + KeyFileKdf::Scrypt(ref scrypt_params) => scrypt_params.to_json() + }); + + Json::Object(map) + } + + /// New pbkdf2-type secret. + /// `cipher-text` - encrypted cipher text. + /// `dk-len` - desired length of the derived key, in octets. + /// `c` - number of iterations for derived key. + /// `salt` - cryptographic site, random 256-bit hash (ensure it's crypto-random). + /// `iv` - initialisation vector. + pub fn new_pbkdf2(cipher_text: Bytes, iv: U128, salt: H256, c: u32, dk_len: u32) -> KeyFileCrypto { + KeyFileCrypto { + cipher_type: CryptoCipherType::Aes128Ctr(iv), + cipher_text: cipher_text, + kdf: KeyFileKdf::Pbkdf2(KdfPbkdf2Params { + dk_len: dk_len, + salt: salt, + c: c, + prf: Pbkdf2CryptoFunction::HMacSha256 + }), + } + } +} + +/// Universally unique identifier +pub type Uuid = H128; + +fn new_uuid() -> Uuid { + H128::random() +} + +fn uuid_to_string(uuid: &Uuid) -> String { + let d1 = &uuid.as_slice()[0..4]; + let d2 = &uuid.as_slice()[4..6]; + let d3 = &uuid.as_slice()[6..8]; + let d4 = &uuid.as_slice()[8..10]; + let d5 = &uuid.as_slice()[10..16]; + format!("{}-{}-{}-{}-{}", d1.to_hex(), d2.to_hex(), d3.to_hex(), d4.to_hex(), d5.to_hex()) +} + +fn uuid_from_string(s: &str) -> Result { + let parts: Vec<&str> = s.split("-").collect(); + if parts.len() != 5 { return Err(UtilError::BadSize); } + + let mut uuid = H128::zero(); + + if parts[0].len() != 8 { return Err(UtilError::BadSize); } + uuid[0..4].clone_from_slice(&try!(FromHex::from_hex(parts[0]))); + if parts[1].len() != 4 { return Err(UtilError::BadSize); } + uuid[4..6].clone_from_slice(&try!(FromHex::from_hex(parts[1]))); + if parts[2].len() != 4 { return Err(UtilError::BadSize); } + uuid[6..8].clone_from_slice(&try!(FromHex::from_hex(parts[2]))); + if parts[3].len() != 4 { return Err(UtilError::BadSize); } + uuid[8..10].clone_from_slice(&try!(FromHex::from_hex(parts[3]))); + if parts[4].len() != 12 { return Err(UtilError::BadSize); } + uuid[10..16].clone_from_slice(&try!(FromHex::from_hex(parts[4]))); + + Ok(uuid) +} + + +#[derive(Clone)] +/// Stored key file struct with encrypted message (cipher_text) +/// also contains password derivation function settings (PBKDF2/Scrypt) +pub struct KeyFileContent { + version: KeyFileVersion, + /// Holds cypher and decrypt function settings. + pub crypto: KeyFileCrypto, + /// The identifier. + pub id: Uuid +} + +#[derive(Debug)] +enum CryptoParseError { + NoCipherText, + NoCipherType, + InvalidJsonFormat, + InvalidKdfType(Mismatch), + InvalidCipherType(Mismatch), + NoInitialVector, + NoCipherParameters, + InvalidInitialVector(FromHexError), + NoKdf, + NoKdfType, + Scrypt(ScryptParseError), + KdfPbkdf2(Pbkdf2ParseError) +} + +#[derive(Debug)] +enum KeyFileParseError { + InvalidVersion, + UnsupportedVersion(OutOfBounds), + InvalidJsonFormat, + InvalidJson, + InvalidIdentifier, + NoCryptoSection, + Crypto(CryptoParseError), +} + +impl KeyFileContent { + /// New stored key file struct with encrypted message (cipher_text) + /// also contains password derivation function settings (PBKDF2/Scrypt) + /// to decrypt cipher_text given the password is provided. + pub fn new(crypto: KeyFileCrypto) -> KeyFileContent { + KeyFileContent { + id: new_uuid(), + version: KeyFileVersion::V3(3), + crypto: crypto + } + } + + /// Returns key file version if it is known. + pub fn version(&self) -> Option { + match self.version { + KeyFileVersion::V3(declared) => Some(declared) + } + } + + fn from_json(json: &Json) -> Result { + let as_object = match json.as_object() { + None => { return Err(KeyFileParseError::InvalidJsonFormat); }, + Some(obj) => obj + }; + + let version = match as_object["version"].as_u64() { + None => { return Err(KeyFileParseError::InvalidVersion); }, + Some(json_version) => { + if json_version <= 2 { + return Err(KeyFileParseError::UnsupportedVersion(OutOfBounds { min: Some(3), max: None, found: json_version })) + }; + KeyFileVersion::V3(json_version) + } + }; + + let id_text = try!(as_object.get("id").and_then(|json| json.as_string()).ok_or(KeyFileParseError::InvalidIdentifier)); + let id = match uuid_from_string(&id_text) { + Err(_) => { return Err(KeyFileParseError::InvalidIdentifier); }, + Ok(id) => id + }; + + let crypto = match as_object.get("crypto") { + None => { return Err(KeyFileParseError::NoCryptoSection); } + Some(crypto_json) => match KeyFileCrypto::from_json(crypto_json) { + Ok(crypto) => crypto, + Err(crypto_error) => { return Err(KeyFileParseError::Crypto(crypto_error)); } + } + }; + + Ok(KeyFileContent { + version: version, + id: id.clone(), + crypto: crypto + }) + } + + fn to_json(&self) -> Json { + let mut map = BTreeMap::new(); + map.insert("id".to_owned(), Json::String(uuid_to_string(&self.id))); + map.insert("version".to_owned(), Json::U64(CURRENT_DECLARED_VERSION)); + map.insert("crypto".to_owned(), self.crypto.to_json()); + Json::Object(map) + } +} + +#[derive(Debug)] +enum KeyFileLoadError { + TooLarge(OutOfBounds), + ParseError(KeyFileParseError), + ReadError(::std::io::Error), +} + +/// Represents directory for saving/loading key files. +pub struct KeyDirectory { + /// Directory path for key management. + path: String, + cache: HashMap, + cache_usage: VecDeque, +} + +impl KeyDirectory { + /// Initializes new cache directory context with a given `path` + pub fn new(path: &Path) -> KeyDirectory { + KeyDirectory { + cache: HashMap::new(), + path: path.to_str().expect("Initialized key directory with empty path").to_owned(), + cache_usage: VecDeque::new(), + } + } + + /// saves (inserts or updates) given key + pub fn save(&mut self, key_file: KeyFileContent) -> Result<(Uuid), ::std::io::Error> { + { + let mut file = try!(fs::File::create(self.key_path(&key_file.id))); + let json = key_file.to_json(); + let json_text = format!("{}", json.pretty()); + let json_bytes = json_text.into_bytes(); + try!(file.write(&json_bytes)); + } + let id = key_file.id.clone(); + self.cache.insert(id.clone(), key_file); + Ok(id.clone()) + } + + /// Returns key given by id if corresponding file exists and no load error occured. + /// Warns if any error occured during the key loading + pub fn get(&mut self, id: &Uuid) -> Option<&KeyFileContent> { + let path = self.key_path(id); + self.cache_usage.push_back(id.clone()); + Some(self.cache.entry(id.to_owned()).or_insert( + match KeyDirectory::load_key(&path) { + Ok(loaded_key) => loaded_key, + Err(error) => { + warn!(target: "sstore", "error loading key {:?}: {:?}", id, error); + return None; + } + } + )) + } + + /// Returns current path to the directory with keys + pub fn path(&self) -> &str { + &self.path + } + + /// Removes keys that never been requested during last `MAX_USAGE_TRACK` times + pub fn collect_garbage(&mut self) { + let total_usages = self.cache_usage.len(); + let untracked_usages = max(total_usages as i64 - MAX_CACHE_USAGE_TRACK as i64, 0) as usize; + if untracked_usages > 0 { + self.cache_usage.drain(..untracked_usages); + } + + if self.cache.len() <= MAX_CACHE_USAGE_TRACK { return; } + + let uniqs: HashSet<&Uuid> = self.cache_usage.iter().collect(); + let mut removes = HashSet::new(); + + for key in self.cache.keys() { + if !uniqs.contains(key) { + removes.insert(key.clone()); + } + } + + for removed_key in removes { self.cache.remove(&removed_key); } + } + + /// Reports how many keys are currently cached. + pub fn cache_size(&self) -> usize { + self.cache.len() + } + + fn key_path(&self, id: &Uuid) -> PathBuf { + let mut path = PathBuf::new(); + path.push(self.path.clone()); + path.push(uuid_to_string(&id)); + path + } + + fn load_key(path: &PathBuf) -> Result { + match fs::File::open(path.clone()) { + Ok(mut open_file) => { + match open_file.metadata() { + Ok(metadata) => + if metadata.len() > MAX_KEY_FILE_LEN { Err(KeyFileLoadError::TooLarge(OutOfBounds { min: Some(2), max: Some(MAX_KEY_FILE_LEN), found: metadata.len() })) } + else { KeyDirectory::load_from_file(&mut open_file) }, + Err(read_error) => Err(KeyFileLoadError::ReadError(read_error)) + } + }, + Err(read_error) => Err(KeyFileLoadError::ReadError(read_error)) + } + } + + fn load_from_file(file: &mut fs::File) -> Result { + let mut buf = String::new(); + match file.read_to_string(&mut buf) { + Ok(_) => {}, + Err(read_error) => { return Err(KeyFileLoadError::ReadError(read_error)); } + } + match Json::from_str(&buf) { + Ok(json) => match KeyFileContent::from_json(&json) { + Ok(key_file_content) => Ok(key_file_content), + Err(parse_error) => Err(KeyFileLoadError::ParseError(parse_error)) + }, + Err(_) => Err(KeyFileLoadError::ParseError(KeyFileParseError::InvalidJson)) + } + } +} + + +#[cfg(test)] +mod file_tests { + use super::{KeyFileContent, KeyFileVersion, KeyFileKdf, KeyFileParseError, CryptoParseError, uuid_from_string, uuid_to_string, KeyFileCrypto, KdfPbkdf2Params}; + use common::*; + + #[test] + fn uuid_parses() { + let uuid = uuid_from_string("3198bc9c-6672-5ab3-d995-4942343ae5b6").unwrap(); + assert!(uuid > H128::zero()); + } + + #[test] + fn uuid_serializes() { + let uuid = uuid_from_string("3198bc9c-6fff-5ab3-d995-4942343ae5b6").unwrap(); + assert_eq!(uuid_to_string(&uuid), "3198bc9c-6fff-5ab3-d995-4942343ae5b6"); + } + + #[test] + fn can_read_keyfile() { + let json = Json::from_str( + r#" + { + "crypto" : { + "cipher" : "aes-128-ctr", + "cipherparams" : { + "iv" : "6087dab2f9fdbbfaddc31a909735c1e6" + }, + "ciphertext" : "5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46", + "kdf" : "pbkdf2", + "kdfparams" : { + "c" : 262144, + "dklen" : 32, + "prf" : "hmac-sha256", + "salt" : "ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd" + }, + "mac" : "517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2" + }, + "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", + "version" : 3 + } + "#).unwrap(); + + match KeyFileContent::from_json(&json) { + Ok(key_file) => { + assert_eq!(KeyFileVersion::V3(3), key_file.version) + }, + Err(e) => panic!("Error parsing valid file: {:?}", e) + } + } + + #[test] + fn can_read_scrypt_krf() { + let json = Json::from_str( + r#" + { + "crypto" : { + "cipher" : "aes-128-ctr", + "cipherparams" : { + "iv" : "83dbcc02d8ccb40e466191a123791e0e" + }, + "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", + "kdf" : "scrypt", + "kdfparams" : { + "dklen" : 32, + "n" : 262144, + "r" : 1, + "p" : 8, + "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" + }, + "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" + }, + "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", + "version" : 3 + } + "#).unwrap(); + + match KeyFileContent::from_json(&json) { + Ok(key_file) => { + match key_file.crypto.kdf { + KeyFileKdf::Scrypt(_) => {}, + _ => { panic!("expected kdf params of crypto to be of scrypt type" ); } + } + }, + Err(e) => panic!("Error parsing valid file: {:?}", e) + } + } + + #[test] + fn can_return_error_no_id() { + let json = Json::from_str( + r#" + { + "crypto" : { + "cipher" : "aes-128-ctr", + "cipherparams" : { + "iv" : "83dbcc02d8ccb40e466191a123791e0e" + }, + "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", + "kdf" : "scrypt", + "kdfparams" : { + "dklen" : 32, + "n" : 262144, + "r" : 1, + "p" : 8, + "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" + }, + "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" + }, + "version" : 3 + } + "#).unwrap(); + + match KeyFileContent::from_json(&json) { + Ok(_) => { + panic!("Should be error of no crypto section, got ok"); + }, + Err(KeyFileParseError::InvalidIdentifier) => { }, + Err(other_error) => { panic!("should be error of no crypto section, got {:?}", other_error); } + } + } + + #[test] + fn can_return_error_no_crypto() { + let json = Json::from_str( + r#" + { + "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", + "version" : 3 + } + "#).unwrap(); + + match KeyFileContent::from_json(&json) { + Ok(_) => { + panic!("Should be error of no identifier, got ok"); + }, + Err(KeyFileParseError::NoCryptoSection) => { }, + Err(other_error) => { panic!("should be error of no identifier, got {:?}", other_error); } + } + } + + #[test] + fn can_return_error_unsupported_version() { + let json = Json::from_str( + r#" + { + "crypto" : { + "cipher" : "aes-128-ctr", + "cipherparams" : { + "iv" : "83dbcc02d8ccb40e466191a123791e0e" + }, + "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", + "kdf" : "scrypt", + "kdfparams" : { + "dklen" : 32, + "n" : 262144, + "r" : 1, + "p" : 8, + "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" + }, + "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" + }, + "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", + "version" : 1 + } + "#).unwrap(); + + match KeyFileContent::from_json(&json) { + Ok(_) => { + panic!("should be error of unsupported version, got ok"); + }, + Err(KeyFileParseError::UnsupportedVersion(_)) => { }, + Err(other_error) => { panic!("should be error of unsupported version, got {:?}", other_error); } + } + } + + + #[test] + fn can_return_error_initial_vector() { + let json = Json::from_str( + r#" + { + "crypto" : { + "cipher" : "aes-128-ctr", + "cipherparams" : { + "iv" : "83dbcc02d8ccb40e4______66191a123791e0e" + }, + "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", + "kdf" : "scrypt", + "kdfparams" : { + "dklen" : 32, + "n" : 262144, + "r" : 1, + "p" : 8, + "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" + }, + "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" + }, + "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", + "version" : 3 + } + "#).unwrap(); + + match KeyFileContent::from_json(&json) { + Ok(_) => { + panic!("should be error of invalid initial vector, got ok"); + }, + Err(KeyFileParseError::Crypto(CryptoParseError::InvalidInitialVector(_))) => { }, + Err(other_error) => { panic!("should be error of invalid initial vector, got {:?}", other_error); } + } + } + + #[test] + fn can_return_error_for_invalid_scrypt_kdf() { + let json = Json::from_str( + r#" + { + "crypto" : { + "cipher" : "aes-128-ctr", + "cipherparams" : { + "iv" : "83dbcc02d8ccb40e466191a123791e0e" + }, + "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", + "kdf" : "scrypt", + "kdfparams" : { + "dklen2" : 32, + "n5" : "xx", + "r" : 1, + "p" : 8, + "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" + }, + "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" + }, + "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", + "version" : 3 + } + "#).unwrap(); + + match KeyFileContent::from_json(&json) { + Ok(_) => { + panic!("Should be error of no identifier, got ok"); + }, + Err(KeyFileParseError::Crypto(CryptoParseError::Scrypt(_))) => { }, + Err(other_error) => { panic!("should be error of no identifier, got {:?}", other_error); } + } + } + + #[test] + fn can_serialize_scrypt_back() { + let json = Json::from_str( + r#" + { + "crypto" : { + "cipher" : "aes-128-ctr", + "cipherparams" : { + "iv" : "83dbcc02d8ccb40e466191a123791e0e" + }, + "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", + "kdf" : "scrypt", + "kdfparams" : { + "dklen" : 32, + "n" : 262144, + "r" : 1, + "p" : 8, + "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" + }, + "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" + }, + "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", + "version" : 3 + } + "#).unwrap(); + + let key = KeyFileContent::from_json(&json).unwrap(); + let serialized = key.to_json(); + + assert!(serialized.as_object().is_some()); + } + + #[test] + fn can_create_key_with_new_id() { + let cipher_text: Bytes = FromHex::from_hex("a0f05555").unwrap(); + let key = KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text, U128::zero(), H256::random(), 32, 32)); + assert!(!uuid_to_string(&key.id).is_empty()); + } + + #[test] + fn can_load_json_from_itself() { + let cipher_text: Bytes = FromHex::from_hex("aaaaaaaaaaaaaaaaaaaaaaaaaaa22222222222222222222222").unwrap(); + let key = KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text, U128::zero(), H256::random(), 32, 32)); + let json = key.to_json(); + + let loaded_key = KeyFileContent::from_json(&json).unwrap(); + + assert_eq!(loaded_key.id, key.id); + } + + #[test] + fn can_parse_kdf_params_fail() { + let json = Json::from_str( + r#" + { + "dklen" : 32, + "n" : 262144, + "r" : 1, + "p" : 8, + "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" + } + "#).unwrap(); + { + let mut invalid_json = json.as_object().unwrap().clone(); + invalid_json.remove("dklen"); + let kdf = KdfPbkdf2Params::from_json(&invalid_json); + assert!(!kdf.is_ok()); + } + { + let mut invalid_json = json.as_object().unwrap().clone(); + invalid_json.remove("n"); + let kdf = KdfPbkdf2Params::from_json(&invalid_json); + assert!(!kdf.is_ok()); + } + { + let mut invalid_json = json.as_object().unwrap().clone(); + invalid_json.remove("r"); + let kdf = KdfPbkdf2Params::from_json(&invalid_json); + assert!(!kdf.is_ok()); + } + { + let mut invalid_json = json.as_object().unwrap().clone(); + invalid_json.remove("p"); + let kdf = KdfPbkdf2Params::from_json(&invalid_json); + assert!(!kdf.is_ok()); + } + { + let mut invalid_json = json.as_object().unwrap().clone(); + invalid_json.remove("salt"); + let kdf = KdfPbkdf2Params::from_json(&invalid_json); + assert!(!kdf.is_ok()); + } + + } + + #[test] + fn can_parse_kdf_params_scrypt_fail() { + let json = Json::from_str( + r#" + { + "dklen" : 32, + "r" : 1, + "p" : 8, + "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" + } + "#).unwrap(); + { + let mut invalid_json = json.as_object().unwrap().clone(); + invalid_json.remove("dklen"); + let kdf = KdfPbkdf2Params::from_json(&invalid_json); + assert!(!kdf.is_ok()); + } + { + let mut invalid_json = json.as_object().unwrap().clone(); + invalid_json.remove("r"); + let kdf = KdfPbkdf2Params::from_json(&invalid_json); + assert!(!kdf.is_ok()); + } + { + let mut invalid_json = json.as_object().unwrap().clone(); + invalid_json.remove("p"); + let kdf = KdfPbkdf2Params::from_json(&invalid_json); + assert!(!kdf.is_ok()); + } + { + let mut invalid_json = json.as_object().unwrap().clone(); + invalid_json.remove("salt"); + let kdf = KdfPbkdf2Params::from_json(&invalid_json); + assert!(!kdf.is_ok()); + } + } + + #[test] + fn can_parse_crypto_fails() { + let json = Json::from_str( + r#" + { + "cipher" : "aes-128-ctr", + "cipherparams" : { + "iv" : "83dbcc02d8ccb40e466191a123791e0e" + }, + "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", + "kdf" : "scrypt", + "kdfparams" : { + "dklen" : 32, + "n" : 262144, + "r" : 1, + "p" : 8, + "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" + }, + "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" + }"#).unwrap(); + + { + let mut invalid_json = json.as_object().unwrap().clone(); + invalid_json.insert("cipher".to_owned(), Json::String("unknown".to_owned())); + let crypto = KeyFileCrypto::from_json(&Json::Object(invalid_json)); + assert!(!crypto.is_ok()); + } + + { + let mut invalid_json = json.as_object().unwrap().clone(); + invalid_json.insert("kdfparams".to_owned(), Json::String("122".to_owned())); + let crypto = KeyFileCrypto::from_json(&Json::Object(invalid_json)); + assert!(!crypto.is_ok()); + } + + { + let mut invalid_json = json.as_object().unwrap().clone(); + invalid_json.insert("kdf".to_owned(), Json::String("15522".to_owned())); + let crypto = KeyFileCrypto::from_json(&Json::Object(invalid_json)); + assert!(!crypto.is_ok()); + } + + } + +} + +#[cfg(test)] +mod directory_tests { + use super::{KeyDirectory, new_uuid, uuid_to_string, KeyFileContent, KeyFileCrypto, MAX_CACHE_USAGE_TRACK}; + use common::*; + use tests::helpers::*; + + #[test] + fn key_directory_locates_keys() { + let temp_path = RandomTempPath::create_dir(); + let directory = KeyDirectory::new(temp_path.as_path()); + let uuid = new_uuid(); + + let path = directory.key_path(&uuid); + + assert!(path.to_str().unwrap().contains(&uuid_to_string(&uuid))); + } + + #[test] + fn loads_key() { + let cipher_text: Bytes = FromHex::from_hex("a0f05555").unwrap(); + let temp_path = RandomTempPath::create_dir(); + let mut directory = KeyDirectory::new(&temp_path.as_path()); + let uuid = directory.save(KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text, U128::zero(), H256::random(), 32, 32))).unwrap(); + let path = directory.key_path(&uuid); + + let key = KeyDirectory::load_key(&path).unwrap(); + + assert_eq!(key.id, uuid); + } + + #[test] + fn caches_keys() { + let temp_path = RandomTempPath::create_dir(); + let mut directory = KeyDirectory::new(&temp_path.as_path()); + + let cipher_text: Bytes = FromHex::from_hex("a0f05555").unwrap(); + let mut keys = Vec::new(); + for _ in 0..1000 { + let key = KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text.clone(), U128::zero(), H256::random(), 32, 32)); + keys.push(directory.save(key).unwrap()); + } + + for key_id in keys { + directory.get(&key_id).unwrap(); + } + + assert_eq!(1000, directory.cache_size()) + + } + + #[test] + fn collects_garbage() { + let temp_path = RandomTempPath::create_dir(); + let mut directory = KeyDirectory::new(&temp_path.as_path()); + + let cipher_text: Bytes = FromHex::from_hex("a0f05555").unwrap(); + let mut keys = Vec::new(); + for _ in 0..1000 { + let key = KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text.clone(), U128::zero(), H256::random(), 32, 32)); + keys.push(directory.save(key).unwrap()); + } + + for key_id in keys { + directory.get(&key_id).unwrap(); + } + + directory.collect_garbage(); + // since all keys are different, should be exactly MAX_CACHE_USAGE_TRACK + assert_eq!(MAX_CACHE_USAGE_TRACK, directory.cache_size()) + } +} + +#[cfg(test)] +mod specs { + use super::*; + use common::*; + use tests::helpers::*; + + #[test] + fn can_initiate_key_directory() { + let temp_path = RandomTempPath::create_dir(); + let directory = KeyDirectory::new(&temp_path.as_path()); + assert!(directory.path().len() > 0); + } + + #[test] + fn can_save_key() { + let cipher_text: Bytes = FromHex::from_hex("a0f05555").unwrap(); + let temp_path = RandomTempPath::create_dir(); + let mut directory = KeyDirectory::new(&temp_path.as_path()); + + let uuid = directory.save(KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text, U128::zero(), H256::random(), 32, 32))); + + assert!(uuid.is_ok()); + } + + #[test] + fn can_load_key() { + let cipher_text: Bytes = FromHex::from_hex("a0f05555").unwrap(); + let temp_path = RandomTempPath::create_dir(); + let mut directory = KeyDirectory::new(&temp_path.as_path()); + let uuid = directory.save(KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text.clone(), U128::zero(), H256::random(), 32, 32))).unwrap(); + + let key = directory.get(&uuid).unwrap(); + + assert_eq!(key.crypto.cipher_text, cipher_text); + } + + #[test] + fn can_store_10_keys() { + let temp_path = RandomTempPath::create_dir(); + let mut directory = KeyDirectory::new(&temp_path.as_path()); + + let cipher_text: Bytes = FromHex::from_hex("a0f05555").unwrap(); + let mut keys = Vec::new(); + for _ in 0..10 { + let key = KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text.clone(), U128::zero(), H256::random(), 32, 32)); + keys.push(directory.save(key).unwrap()); + } + + assert_eq!(10, keys.len()) + } +} diff --git a/util/src/keys/mod.rs b/util/src/keys/mod.rs new file mode 100644 index 000000000..d7ffdb0dd --- /dev/null +++ b/util/src/keys/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +//! Key management module + +pub mod directory; diff --git a/util/src/lib.rs b/util/src/lib.rs index 59e9b966c..d4f972800 100644 --- a/util/src/lib.rs +++ b/util/src/lib.rs @@ -141,6 +141,7 @@ pub mod io; pub mod network; pub mod log; pub mod panics; +pub mod keys; pub use common::*; pub use misc::*; @@ -161,3 +162,6 @@ pub use semantic_version::*; pub use network::*; pub use io::*; pub use log::*; + +#[cfg(test)] +mod tests; diff --git a/util/src/network/host.rs b/util/src/network/host.rs index c1423dbb3..430850453 100644 --- a/util/src/network/host.rs +++ b/util/src/network/host.rs @@ -217,6 +217,7 @@ impl<'s, Message> NetworkContext<'s, Message> where Message: Send + Sync + Clone s.send_packet(self.protocol, packet_id as u8, &data).unwrap_or_else(|e| { warn!(target: "net", "Send error: {:?}", e); }); //TODO: don't copy vector data + try!(self.io.update_registration(peer)); }, _ => warn!(target: "net", "Send: Peer is not connected yet") } diff --git a/util/src/tests/helpers.rs b/util/src/tests/helpers.rs new file mode 100644 index 000000000..fee3d2cbb --- /dev/null +++ b/util/src/tests/helpers.rs @@ -0,0 +1,31 @@ +use common::*; +use std::path::PathBuf; +use std::fs::{remove_dir_all}; +use std::env; + +pub struct RandomTempPath { + path: PathBuf +} + +impl RandomTempPath { + pub fn create_dir() -> RandomTempPath { + let mut dir = env::temp_dir(); + dir.push(H32::random().hex()); + fs::create_dir_all(dir.as_path()).unwrap(); + RandomTempPath { + path: dir.clone() + } + } + + pub fn as_path(&self) -> &PathBuf { + &self.path + } +} + +impl Drop for RandomTempPath { + fn drop(&mut self) { + if let Err(e) = remove_dir_all(self.as_path()) { + panic!("failed to remove temp directory, probably something failed to destroyed ({})", e); + } + } +} diff --git a/util/src/tests/mod.rs b/util/src/tests/mod.rs new file mode 100644 index 000000000..1630fabcd --- /dev/null +++ b/util/src/tests/mod.rs @@ -0,0 +1 @@ +pub mod helpers;