diff --git a/Cargo.lock b/Cargo.lock index f6d5fa213..95be811e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1249,7 +1249,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#29c6f1d527db5c6a29c6b6afdb9bde32c6809079" +source = "git+https://github.com/ethcore/js-precompiled.git#bf33dd4aabd2adb2178576db5a4d23b8902d39b8" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/dapps/src/router/mod.rs b/dapps/src/router/mod.rs index cb9133886..c0a33f2eb 100644 --- a/dapps/src/router/mod.rs +++ b/dapps/src/router/mod.rs @@ -131,7 +131,7 @@ impl server::Handler for Router { StatusCode::NotFound, "404 Not Found", "Your homepage is not available when Trusted Signer is disabled.", - Some("You can still access dapps by writing a correct address, though. Re-enabled Signer to get your homepage back."), + Some("You can still access dapps by writing a correct address, though. Re-enable Signer to get your homepage back."), self.signer_address.clone(), )) } diff --git a/ethcore/light/Cargo.toml b/ethcore/light/Cargo.toml new file mode 100644 index 000000000..daf141de7 --- /dev/null +++ b/ethcore/light/Cargo.toml @@ -0,0 +1,16 @@ +[package] +description = "Parity LES primitives" +homepage = "https://ethcore.io" +license = "GPL-3.0" +name = "ethcore-light" +version = "1.5.0" +authors = ["Ethcore "] + +[dependencies] +log = "0.3" +ethcore = { path = ".." } +ethcore-util = { path = "../../util" } +ethcore-network = { path = "../../util/network" } +ethcore-io = { path = "../../util/io" } +rlp = { path = "../../util/rlp" } +time = "0.1" \ No newline at end of file diff --git a/ethcore/light/src/client.rs b/ethcore/light/src/client.rs new file mode 100644 index 000000000..e3b5745b2 --- /dev/null +++ b/ethcore/light/src/client.rs @@ -0,0 +1,115 @@ +// 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 . + +//! Light client implementation. Used for raw data queries as well as the header +//! sync. + +use std::sync::Arc; + +use ethcore::engines::Engine; +use ethcore::ids::BlockID; +use ethcore::service::ClientIoMessage; +use ethcore::block_import_error::BlockImportError; +use ethcore::block_status::BlockStatus; +use ethcore::verification::queue::{HeaderQueue, QueueInfo}; +use ethcore::transaction::SignedTransaction; +use ethcore::blockchain_info::BlockChainInfo; + +use io::IoChannel; +use util::hash::H256; +use util::{Bytes, Mutex}; + +use provider::Provider; +use request; + +/// Light client implementation. +pub struct Client { + engine: Arc, + header_queue: HeaderQueue, + message_channel: Mutex>, +} + +impl Client { + /// Import a header as rlp-encoded bytes. + pub fn import_header(&self, bytes: Bytes) -> Result { + let header = ::rlp::decode(&bytes); + + self.header_queue.import(header).map_err(Into::into) + } + + /// Whether the block is already known (but not necessarily part of the canonical chain) + pub fn is_known(&self, _id: BlockID) -> bool { + false + } + + /// Fetch a vector of all pending transactions. + pub fn pending_transactions(&self) -> Vec { + vec![] + } + + /// Inquire about the status of a given block. + pub fn status(&self, _id: BlockID) -> BlockStatus { + BlockStatus::Unknown + } + + /// Get the header queue info. + pub fn queue_info(&self) -> QueueInfo { + self.header_queue.queue_info() + } +} + +// dummy implementation -- may draw from canonical cache further on. +impl Provider for Client { + fn chain_info(&self) -> BlockChainInfo { + unimplemented!() + } + + fn reorg_depth(&self, _a: &H256, _b: &H256) -> Option { + None + } + + fn earliest_state(&self) -> Option { + None + } + + fn block_headers(&self, _req: request::Headers) -> Vec { + Vec::new() + } + + fn block_bodies(&self, _req: request::Bodies) -> Vec { + Vec::new() + } + + fn receipts(&self, _req: request::Receipts) -> Vec { + Vec::new() + } + + fn proofs(&self, _req: request::StateProofs) -> Vec { + Vec::new() + } + + fn code(&self, _req: request::ContractCodes) -> Vec { + Vec::new() + } + + fn header_proofs(&self, _req: request::HeaderProofs) -> Vec { + Vec::new() + } + + fn pending_transactions(&self) -> Vec { + Vec::new() + } +} \ No newline at end of file diff --git a/ethcore/light/src/lib.rs b/ethcore/light/src/lib.rs new file mode 100644 index 000000000..07e6833a7 --- /dev/null +++ b/ethcore/light/src/lib.rs @@ -0,0 +1,47 @@ +// 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 . + +//! Light client logic and implementation. +//! +//! A "light" client stores very little chain-related data locally +//! unlike a full node, which stores all blocks, headers, receipts, and more. +//! +//! This enables the client to have a much lower resource footprint in +//! exchange for the cost of having to ask the network for state data +//! while responding to queries. This makes a light client unsuitable for +//! low-latency applications, but perfectly suitable for simple everyday +//! use-cases like sending transactions from a personal account. +//! +//! It starts by performing a header-only sync, verifying random samples +//! of members of the chain to varying degrees. + +// TODO: remove when integrating with parity. +#![allow(dead_code)] + +pub mod client; +pub mod net; +pub mod provider; +pub mod request; + +extern crate ethcore_util as util; +extern crate ethcore_network as network; +extern crate ethcore_io as io; +extern crate ethcore; +extern crate rlp; +extern crate time; + +#[macro_use] +extern crate log; \ No newline at end of file diff --git a/ethcore/light/src/net/buffer_flow.rs b/ethcore/light/src/net/buffer_flow.rs new file mode 100644 index 000000000..b7bd30f82 --- /dev/null +++ b/ethcore/light/src/net/buffer_flow.rs @@ -0,0 +1,264 @@ +// 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 . + +//! LES buffer flow management. +//! +//! Every request in the LES protocol leads to a reduction +//! of the requester's buffer value as a rate-limiting mechanism. +//! This buffer value will recharge at a set rate. +//! +//! This module provides an interface for configuration of buffer +//! flow costs and recharge rates. + +use request; +use super::packet; +use super::error::Error; + +use rlp::*; +use util::U256; +use time::{Duration, SteadyTime}; + +/// A request cost specification. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Cost(pub U256, pub U256); + +/// Buffer value. +/// +/// Produced and recharged using `FlowParams`. +/// Definitive updates can be made as well -- these will reset the recharge +/// point to the time of the update. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Buffer { + estimate: U256, + recharge_point: SteadyTime, +} + +impl Buffer { + /// Get the current buffer value. + pub fn current(&self) -> U256 { self.estimate.clone() } + + /// Make a definitive update. + /// This will be the value obtained after receiving + /// a response to a request. + pub fn update_to(&mut self, value: U256) { + self.estimate = value; + self.recharge_point = SteadyTime::now(); + } + + /// Attempt to apply the given cost to the buffer. + /// + /// If successful, the cost will be deducted successfully. + /// + /// If unsuccessful, the structure will be unaltered an an + /// error will be produced. + pub fn deduct_cost(&mut self, cost: U256) -> Result<(), Error> { + match cost > self.estimate { + true => Err(Error::BufferEmpty), + false => { + self.estimate = self.estimate - cost; + Ok(()) + } + } + } +} + +/// A cost table, mapping requests to base and per-request costs. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CostTable { + headers: Cost, + bodies: Cost, + receipts: Cost, + state_proofs: Cost, + contract_codes: Cost, + header_proofs: Cost, +} + +impl Default for CostTable { + fn default() -> Self { + // arbitrarily chosen constants. + CostTable { + headers: Cost(100000.into(), 10000.into()), + bodies: Cost(150000.into(), 15000.into()), + receipts: Cost(50000.into(), 5000.into()), + state_proofs: Cost(250000.into(), 25000.into()), + contract_codes: Cost(200000.into(), 20000.into()), + header_proofs: Cost(150000.into(), 15000.into()), + } + } +} + +impl RlpEncodable for CostTable { + fn rlp_append(&self, s: &mut RlpStream) { + fn append_cost(s: &mut RlpStream, msg_id: u8, cost: &Cost) { + s.begin_list(3) + .append(&msg_id) + .append(&cost.0) + .append(&cost.1); + } + + s.begin_list(6); + + append_cost(s, packet::GET_BLOCK_HEADERS, &self.headers); + append_cost(s, packet::GET_BLOCK_BODIES, &self.bodies); + append_cost(s, packet::GET_RECEIPTS, &self.receipts); + append_cost(s, packet::GET_PROOFS, &self.state_proofs); + append_cost(s, packet::GET_CONTRACT_CODES, &self.contract_codes); + append_cost(s, packet::GET_HEADER_PROOFS, &self.header_proofs); + } +} + +impl RlpDecodable for CostTable { + fn decode(decoder: &D) -> Result where D: Decoder { + let rlp = decoder.as_rlp(); + + let mut headers = None; + let mut bodies = None; + let mut receipts = None; + let mut state_proofs = None; + let mut contract_codes = None; + let mut header_proofs = None; + + for row in rlp.iter() { + let msg_id: u8 = try!(row.val_at(0)); + let cost = { + let base = try!(row.val_at(1)); + let per = try!(row.val_at(2)); + + Cost(base, per) + }; + + match msg_id { + packet::GET_BLOCK_HEADERS => headers = Some(cost), + packet::GET_BLOCK_BODIES => bodies = Some(cost), + packet::GET_RECEIPTS => receipts = Some(cost), + packet::GET_PROOFS => state_proofs = Some(cost), + packet::GET_CONTRACT_CODES => contract_codes = Some(cost), + packet::GET_HEADER_PROOFS => header_proofs = Some(cost), + _ => return Err(DecoderError::Custom("Unrecognized message in cost table")), + } + } + + Ok(CostTable { + headers: try!(headers.ok_or(DecoderError::Custom("No headers cost specified"))), + bodies: try!(bodies.ok_or(DecoderError::Custom("No bodies cost specified"))), + receipts: try!(receipts.ok_or(DecoderError::Custom("No receipts cost specified"))), + state_proofs: try!(state_proofs.ok_or(DecoderError::Custom("No proofs cost specified"))), + contract_codes: try!(contract_codes.ok_or(DecoderError::Custom("No contract codes specified"))), + header_proofs: try!(header_proofs.ok_or(DecoderError::Custom("No header proofs cost specified"))), + }) + } +} + +/// A buffer-flow manager handles costs, recharge, limits +#[derive(Debug, Clone, PartialEq)] +pub struct FlowParams { + costs: CostTable, + limit: U256, + recharge: U256, +} + +impl FlowParams { + /// Create new flow parameters from a request cost table, + /// buffer limit, and (minimum) rate of recharge. + pub fn new(limit: U256, costs: CostTable, recharge: U256) -> Self { + FlowParams { + costs: costs, + limit: limit, + recharge: recharge, + } + } + + /// Get a reference to the buffer limit. + pub fn limit(&self) -> &U256 { &self.limit } + + /// Get a reference to the cost table. + pub fn cost_table(&self) -> &CostTable { &self.costs } + + /// Get a reference to the recharge rate. + pub fn recharge_rate(&self) -> &U256 { &self.recharge } + + /// Compute the actual cost of a request, given the kind of request + /// and number of requests made. + pub fn compute_cost(&self, kind: request::Kind, amount: usize) -> U256 { + let cost = match kind { + request::Kind::Headers => &self.costs.headers, + request::Kind::Bodies => &self.costs.bodies, + request::Kind::Receipts => &self.costs.receipts, + request::Kind::StateProofs => &self.costs.state_proofs, + request::Kind::Codes => &self.costs.contract_codes, + request::Kind::HeaderProofs => &self.costs.header_proofs, + }; + + let amount: U256 = amount.into(); + cost.0 + (amount * cost.1) + } + + /// Create initial buffer parameter. + pub fn create_buffer(&self) -> Buffer { + Buffer { + estimate: self.limit, + recharge_point: SteadyTime::now(), + } + } + + /// Recharge the buffer based on time passed since last + /// update. + pub fn recharge(&self, buf: &mut Buffer) { + let now = SteadyTime::now(); + + // recompute and update only in terms of full seconds elapsed + // in order to keep the estimate as an underestimate. + let elapsed = (now - buf.recharge_point).num_seconds(); + buf.recharge_point = buf.recharge_point + Duration::seconds(elapsed); + + let elapsed: U256 = elapsed.into(); + + buf.estimate = ::std::cmp::min(self.limit, buf.estimate + (elapsed * self.recharge)); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_serialize_cost_table() { + let costs = CostTable::default(); + let serialized = ::rlp::encode(&costs); + + let new_costs: CostTable = ::rlp::decode(&*serialized); + + assert_eq!(costs, new_costs); + } + + #[test] + fn buffer_mechanism() { + use std::thread; + use std::time::Duration; + + let flow_params = FlowParams::new(100.into(), Default::default(), 20.into()); + let mut buffer = flow_params.create_buffer(); + + assert!(buffer.deduct_cost(101.into()).is_err()); + assert!(buffer.deduct_cost(10.into()).is_ok()); + + thread::sleep(Duration::from_secs(1)); + + flow_params.recharge(&mut buffer); + + assert_eq!(buffer.estimate, 100.into()); + } +} \ No newline at end of file diff --git a/ethcore/light/src/net/error.rs b/ethcore/light/src/net/error.rs new file mode 100644 index 000000000..e15bd50d3 --- /dev/null +++ b/ethcore/light/src/net/error.rs @@ -0,0 +1,94 @@ +// 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 . + +//! Defines error types and levels of punishment to use upon +//! encountering. + +use rlp::DecoderError; +use network::NetworkError; + +use std::fmt; + +/// Levels of punishment. +/// +/// Currently just encompasses two different kinds of disconnect and +/// no punishment, but this is where reputation systems might come into play. +// In ascending order +#[derive(Debug, PartialEq, Eq)] +pub enum Punishment { + /// Perform no punishment. + None, + /// Disconnect the peer, but don't prevent them from reconnecting. + Disconnect, + /// Disconnect the peer and prevent them from reconnecting. + Disable, +} + +/// Kinds of errors which can be encountered in the course of LES. +#[derive(Debug)] +pub enum Error { + /// An RLP decoding error. + Rlp(DecoderError), + /// A network error. + Network(NetworkError), + /// Out of buffer. + BufferEmpty, + /// Unrecognized packet code. + UnrecognizedPacket(u8), + /// Unexpected handshake. + UnexpectedHandshake, + /// Peer on wrong network (wrong NetworkId or genesis hash) + WrongNetwork, +} + +impl Error { + /// What level of punishment does this error warrant? + pub fn punishment(&self) -> Punishment { + match *self { + Error::Rlp(_) => Punishment::Disable, + Error::Network(_) => Punishment::None, + Error::BufferEmpty => Punishment::Disable, + Error::UnrecognizedPacket(_) => Punishment::Disconnect, + Error::UnexpectedHandshake => Punishment::Disconnect, + Error::WrongNetwork => Punishment::Disable, + } + } +} + +impl From for Error { + fn from(err: DecoderError) -> Self { + Error::Rlp(err) + } +} + +impl From for Error { + fn from(err: NetworkError) -> Self { + Error::Network(err) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Error::Rlp(ref err) => err.fmt(f), + Error::Network(ref err) => err.fmt(f), + Error::BufferEmpty => write!(f, "Out of buffer"), + Error::UnrecognizedPacket(code) => write!(f, "Unrecognized packet: 0x{:x}", code), + Error::UnexpectedHandshake => write!(f, "Unexpected handshake"), + Error::WrongNetwork => write!(f, "Wrong network"), + } + } +} \ No newline at end of file diff --git a/ethcore/light/src/net/mod.rs b/ethcore/light/src/net/mod.rs new file mode 100644 index 000000000..e72ce4bb2 --- /dev/null +++ b/ethcore/light/src/net/mod.rs @@ -0,0 +1,506 @@ +// 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 . + +//! LES Protocol Version 1 implementation. +//! +//! This uses a "Provider" to answer requests. +//! See https://github.com/ethcore/parity/wiki/Light-Ethereum-Subprotocol-(LES) + +use io::TimerToken; +use network::{NetworkProtocolHandler, NetworkContext, NetworkError, PeerId}; +use rlp::{RlpStream, Stream, UntrustedRlp, View}; +use util::hash::H256; +use util::RwLock; + +use std::collections::{HashMap, HashSet}; +use std::sync::atomic::AtomicUsize; + +use provider::Provider; +use request::{self, Request}; + +use self::buffer_flow::{Buffer, FlowParams}; +use self::error::{Error, Punishment}; +use self::status::{Status, Capabilities}; + +mod buffer_flow; +mod error; +mod status; + +pub use self::status::Announcement; + +const TIMEOUT: TimerToken = 0; +const TIMEOUT_INTERVAL_MS: u64 = 1000; + +// LPV1 +const PROTOCOL_VERSION: u32 = 1; + +// TODO [rob] make configurable. +const PROTOCOL_ID: [u8; 3] = *b"les"; + +// packet ID definitions. +mod packet { + // the status packet. + pub const STATUS: u8 = 0x00; + + // announcement of new block hashes or capabilities. + pub const ANNOUNCE: u8 = 0x01; + + // request and response for block headers + pub const GET_BLOCK_HEADERS: u8 = 0x02; + pub const BLOCK_HEADERS: u8 = 0x03; + + // request and response for block bodies + pub const GET_BLOCK_BODIES: u8 = 0x04; + pub const BLOCK_BODIES: u8 = 0x05; + + // request and response for transaction receipts. + pub const GET_RECEIPTS: u8 = 0x06; + pub const RECEIPTS: u8 = 0x07; + + // request and response for merkle proofs. + pub const GET_PROOFS: u8 = 0x08; + pub const PROOFS: u8 = 0x09; + + // request and response for contract code. + pub const GET_CONTRACT_CODES: u8 = 0x0a; + pub const CONTRACT_CODES: u8 = 0x0b; + + // relay transactions to peers. + pub const SEND_TRANSACTIONS: u8 = 0x0c; + + // request and response for header proofs in a CHT. + pub const GET_HEADER_PROOFS: u8 = 0x0d; + pub const HEADER_PROOFS: u8 = 0x0e; +} + +// A pending peer: one we've sent our status to but +// may not have received one for. +struct PendingPeer { + sent_head: H256, +} + +// data about each peer. +struct Peer { + local_buffer: Buffer, // their buffer relative to us + remote_buffer: Buffer, // our buffer relative to them + current_asking: HashSet, // pending request ids. + status: Status, + capabilities: Capabilities, + remote_flow: FlowParams, + sent_head: H256, // last head we've given them. +} + +/// This is an implementation of the light ethereum network protocol, abstracted +/// over a `Provider` of data and a p2p network. +/// +/// This is simply designed for request-response purposes. Higher level uses +/// of the protocol, such as synchronization, will function as wrappers around +/// this system. +pub struct LightProtocol { + provider: Box, + genesis_hash: H256, + network_id: status::NetworkId, + pending_peers: RwLock>, + peers: RwLock>, + pending_requests: RwLock>, + capabilities: RwLock, + flow_params: FlowParams, // assumed static and same for every peer. + req_id: AtomicUsize, +} + +impl LightProtocol { + /// Make an announcement of new chain head and capabilities to all peers. + /// The announcement is expected to be valid. + pub fn make_announcement(&self, mut announcement: Announcement, io: &NetworkContext) { + let mut reorgs_map = HashMap::new(); + + // calculate reorg info and send packets + for (peer_id, peer_info) in self.peers.write().iter_mut() { + let reorg_depth = reorgs_map.entry(peer_info.sent_head) + .or_insert_with(|| { + match self.provider.reorg_depth(&announcement.head_hash, &peer_info.sent_head) { + Some(depth) => depth, + None => { + // both values will always originate locally -- this means something + // has gone really wrong + debug!(target: "les", "couldn't compute reorganization depth between {:?} and {:?}", + &announcement.head_hash, &peer_info.sent_head); + 0 + } + } + }); + + peer_info.sent_head = announcement.head_hash; + announcement.reorg_depth = *reorg_depth; + + if let Err(e) = io.send(*peer_id, packet::ANNOUNCE, status::write_announcement(&announcement)) { + debug!(target: "les", "Error sending to peer {}: {}", peer_id, e); + } + } + } +} + +impl LightProtocol { + // called when a peer connects. + fn on_connect(&self, peer: &PeerId, io: &NetworkContext) { + let peer = *peer; + + match self.send_status(peer, io) { + Ok(pending_peer) => { + self.pending_peers.write().insert(peer, pending_peer); + } + Err(e) => { + trace!(target: "les", "Error while sending status: {}", e); + io.disconnect_peer(peer); + } + } + } + + // called when a peer disconnects. + fn on_disconnect(&self, peer: PeerId) { + // TODO: reassign all requests assigned to this peer. + self.pending_peers.write().remove(&peer); + self.peers.write().remove(&peer); + } + + // send status to a peer. + fn send_status(&self, peer: PeerId, io: &NetworkContext) -> Result { + let chain_info = self.provider.chain_info(); + + // TODO: could update capabilities here. + + let status = Status { + head_td: chain_info.total_difficulty, + head_hash: chain_info.best_block_hash, + head_num: chain_info.best_block_number, + genesis_hash: chain_info.genesis_hash, + protocol_version: PROTOCOL_VERSION, + network_id: self.network_id, + last_head: None, + }; + + let capabilities = self.capabilities.read().clone(); + let status_packet = status::write_handshake(&status, &capabilities, &self.flow_params); + + try!(io.send(peer, packet::STATUS, status_packet)); + + Ok(PendingPeer { + sent_head: chain_info.best_block_hash, + }) + } + + // Handle status message from peer. + fn status(&self, peer: &PeerId, data: UntrustedRlp) -> Result<(), Error> { + let pending = match self.pending_peers.write().remove(peer) { + Some(pending) => pending, + None => { + return Err(Error::UnexpectedHandshake); + } + }; + + let (status, capabilities, flow_params) = try!(status::parse_handshake(data)); + + trace!(target: "les", "Connected peer with chain head {:?}", (status.head_hash, status.head_num)); + + if (status.network_id, status.genesis_hash) != (self.network_id, self.genesis_hash) { + return Err(Error::WrongNetwork); + } + + self.peers.write().insert(*peer, Peer { + local_buffer: self.flow_params.create_buffer(), + remote_buffer: flow_params.create_buffer(), + current_asking: HashSet::new(), + status: status, + capabilities: capabilities, + remote_flow: flow_params, + sent_head: pending.sent_head, + }); + + Ok(()) + } + + // Handle an announcement. + fn announcement(&self, peer: &PeerId, data: UntrustedRlp) -> Result<(), Error> { + if !self.peers.read().contains_key(peer) { + debug!(target: "les", "Ignoring announcement from unknown peer"); + return Ok(()) + } + + let announcement = try!(status::parse_announcement(data)); + let mut peers = self.peers.write(); + + let peer_info = match peers.get_mut(peer) { + Some(info) => info, + None => return Ok(()), + }; + + // update status. + { + // TODO: punish peer if they've moved backwards. + let status = &mut peer_info.status; + let last_head = status.head_hash; + status.head_hash = announcement.head_hash; + status.head_td = announcement.head_td; + status.head_num = announcement.head_num; + status.last_head = Some((last_head, announcement.reorg_depth)); + } + + // update capabilities. + { + let caps = &mut peer_info.capabilities; + caps.serve_headers = caps.serve_headers || announcement.serve_headers; + caps.serve_state_since = caps.serve_state_since.or(announcement.serve_state_since); + caps.serve_chain_since = caps.serve_chain_since.or(announcement.serve_chain_since); + caps.tx_relay = caps.tx_relay || announcement.tx_relay; + } + + // TODO: notify listeners if new best block. + + Ok(()) + } + + // Handle a request for block headers. + fn get_block_headers(&self, peer: &PeerId, io: &NetworkContext, data: UntrustedRlp) -> Result<(), Error> { + const MAX_HEADERS: usize = 512; + + let mut present_buffer = match self.peers.read().get(peer) { + Some(peer) => peer.local_buffer.clone(), + None => { + debug!(target: "les", "Ignoring announcement from unknown peer"); + return Ok(()) + } + }; + + self.flow_params.recharge(&mut present_buffer); + let req_id: u64 = try!(data.val_at(0)); + + let req = request::Headers { + block: { + let rlp = try!(data.at(1)); + (try!(rlp.val_at(0)), try!(rlp.val_at(1))) + }, + max: ::std::cmp::min(MAX_HEADERS, try!(data.val_at(2))), + skip: try!(data.val_at(3)), + reverse: try!(data.val_at(4)), + }; + + let max_cost = self.flow_params.compute_cost(request::Kind::Headers, req.max); + try!(present_buffer.deduct_cost(max_cost)); + + let response = self.provider.block_headers(req); + let actual_cost = self.flow_params.compute_cost(request::Kind::Headers, response.len()); + + let cur_buffer = match self.peers.write().get_mut(peer) { + Some(peer) => { + self.flow_params.recharge(&mut peer.local_buffer); + try!(peer.local_buffer.deduct_cost(actual_cost)); + peer.local_buffer.current() + } + None => { + debug!(target: "les", "peer disconnected during serving of request."); + return Ok(()) + } + }; + + io.respond(packet::BLOCK_HEADERS, { + let mut stream = RlpStream::new_list(response.len() + 2); + stream.append(&req_id).append(&cur_buffer); + + for header in response { + stream.append_raw(&header, 1); + } + + stream.out() + }).map_err(Into::into) + } + + // Receive a response for block headers. + fn block_headers(&self, _: &PeerId, _: &NetworkContext, _: UntrustedRlp) -> Result<(), Error> { + unimplemented!() + } + + // Handle a request for block bodies. + fn get_block_bodies(&self, peer: &PeerId, io: &NetworkContext, data: UntrustedRlp) -> Result<(), Error> { + const MAX_BODIES: usize = 256; + + let mut present_buffer = match self.peers.read().get(peer) { + Some(peer) => peer.local_buffer.clone(), + None => { + debug!(target: "les", "Ignoring announcement from unknown peer"); + return Ok(()) + } + }; + + self.flow_params.recharge(&mut present_buffer); + let req_id: u64 = try!(data.val_at(0)); + + let req = request::Bodies { + block_hashes: try!(data.iter().skip(1).take(MAX_BODIES).map(|x| x.as_val()).collect()) + }; + + let max_cost = self.flow_params.compute_cost(request::Kind::Bodies, req.block_hashes.len()); + try!(present_buffer.deduct_cost(max_cost)); + + let response = self.provider.block_bodies(req); + let response_len = response.iter().filter(|x| &x[..] != &::rlp::EMPTY_LIST_RLP).count(); + let actual_cost = self.flow_params.compute_cost(request::Kind::Bodies, response_len); + + let cur_buffer = match self.peers.write().get_mut(peer) { + Some(peer) => { + self.flow_params.recharge(&mut peer.local_buffer); + try!(peer.local_buffer.deduct_cost(actual_cost)); + peer.local_buffer.current() + } + None => { + debug!(target: "les", "peer disconnected during serving of request."); + return Ok(()) + } + }; + + io.respond(packet::BLOCK_BODIES, { + let mut stream = RlpStream::new_list(response.len() + 2); + stream.append(&req_id).append(&cur_buffer); + + for body in response { + stream.append_raw(&body, 1); + } + + stream.out() + }).map_err(Into::into) + } + + // Receive a response for block bodies. + fn block_bodies(&self, _: &PeerId, _: &NetworkContext, _: UntrustedRlp) -> Result<(), Error> { + unimplemented!() + } + + // Handle a request for receipts. + fn get_receipts(&self, _: &PeerId, _: &NetworkContext, _: UntrustedRlp) -> Result<(), Error> { + unimplemented!() + } + + // Receive a response for receipts. + fn receipts(&self, _: &PeerId, _: &NetworkContext, _: UntrustedRlp) -> Result<(), Error> { + unimplemented!() + } + + // Handle a request for proofs. + fn get_proofs(&self, _: &PeerId, _: &NetworkContext, _: UntrustedRlp) -> Result<(), Error> { + unimplemented!() + } + + // Receive a response for proofs. + fn proofs(&self, _: &PeerId, _: &NetworkContext, _: UntrustedRlp) -> Result<(), Error> { + unimplemented!() + } + + // Handle a request for contract code. + fn get_contract_code(&self, _: &PeerId, _: &NetworkContext, _: UntrustedRlp) -> Result<(), Error> { + unimplemented!() + } + + // Receive a response for contract code. + fn contract_code(&self, _: &PeerId, _: &NetworkContext, _: UntrustedRlp) -> Result<(), Error> { + unimplemented!() + } + + // Handle a request for header proofs + fn get_header_proofs(&self, _: &PeerId, _: &NetworkContext, _: UntrustedRlp) -> Result<(), Error> { + unimplemented!() + } + + // Receive a response for header proofs + fn header_proofs(&self, _: &PeerId, _: &NetworkContext, _: UntrustedRlp) -> Result<(), Error> { + unimplemented!() + } + + // Receive a set of transactions to relay. + fn relay_transactions(&self, _: &PeerId, _: &NetworkContext, _: UntrustedRlp) -> Result<(), Error> { + unimplemented!() + } +} + +impl NetworkProtocolHandler for LightProtocol { + fn initialize(&self, io: &NetworkContext) { + io.register_timer(TIMEOUT, TIMEOUT_INTERVAL_MS).expect("Error registering sync timer."); + } + + fn read(&self, io: &NetworkContext, peer: &PeerId, packet_id: u8, data: &[u8]) { + let rlp = UntrustedRlp::new(data); + + // handle the packet + let res = match packet_id { + packet::STATUS => self.status(peer, rlp), + packet::ANNOUNCE => self.announcement(peer, rlp), + + packet::GET_BLOCK_HEADERS => self.get_block_headers(peer, io, rlp), + packet::BLOCK_HEADERS => self.block_headers(peer, io, rlp), + + packet::GET_BLOCK_BODIES => self.get_block_bodies(peer, io, rlp), + packet::BLOCK_BODIES => self.block_bodies(peer, io, rlp), + + packet::GET_RECEIPTS => self.get_receipts(peer, io, rlp), + packet::RECEIPTS => self.receipts(peer, io, rlp), + + packet::GET_PROOFS => self.get_proofs(peer, io, rlp), + packet::PROOFS => self.proofs(peer, io, rlp), + + packet::GET_CONTRACT_CODES => self.get_contract_code(peer, io, rlp), + packet::CONTRACT_CODES => self.contract_code(peer, io, rlp), + + packet::GET_HEADER_PROOFS => self.get_header_proofs(peer, io, rlp), + packet::HEADER_PROOFS => self.header_proofs(peer, io, rlp), + + packet::SEND_TRANSACTIONS => self.relay_transactions(peer, io, rlp), + + other => { + Err(Error::UnrecognizedPacket(other)) + } + }; + + // if something went wrong, figure out how much to punish the peer. + if let Err(e) = res { + match e.punishment() { + Punishment::None => {} + Punishment::Disconnect => { + debug!(target: "les", "Disconnecting peer {}: {}", peer, e); + io.disconnect_peer(*peer) + } + Punishment::Disable => { + debug!(target: "les", "Disabling peer {}: {}", peer, e); + io.disable_peer(*peer) + } + } + } + } + + fn connected(&self, io: &NetworkContext, peer: &PeerId) { + self.on_connect(peer, io); + } + + fn disconnected(&self, _io: &NetworkContext, peer: &PeerId) { + self.on_disconnect(*peer); + } + + fn timeout(&self, _io: &NetworkContext, timer: TimerToken) { + match timer { + TIMEOUT => { + // broadcast transactions to peers. + } + _ => warn!(target: "les", "received timeout on unknown token {}", timer), + } + } +} \ No newline at end of file diff --git a/ethcore/light/src/net/status.rs b/ethcore/light/src/net/status.rs new file mode 100644 index 000000000..5aaea9f3a --- /dev/null +++ b/ethcore/light/src/net/status.rs @@ -0,0 +1,539 @@ +// 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 . + +//! Peer status and capabilities. + +use rlp::{DecoderError, RlpDecodable, RlpEncodable, RlpStream, Stream, UntrustedRlp, View}; +use util::{H256, U256}; + +use super::buffer_flow::FlowParams; + +// recognized handshake/announcement keys. +// unknown keys are to be skipped, known keys have a defined order. +// their string values are defined in the LES spec. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] +enum Key { + ProtocolVersion, + NetworkId, + HeadTD, + HeadHash, + HeadNum, + GenesisHash, + ServeHeaders, + ServeChainSince, + ServeStateSince, + TxRelay, + BufferLimit, + BufferCostTable, + BufferRechargeRate, +} + +impl Key { + // get the string value of this key. + fn as_str(&self) -> &'static str { + match *self { + Key::ProtocolVersion => "protocolVersion", + Key::NetworkId => "networkId", + Key::HeadTD => "headTd", + Key::HeadHash => "headHash", + Key::HeadNum => "headNum", + Key::GenesisHash => "genesisHash", + Key::ServeHeaders => "serveHeaders", + Key::ServeChainSince => "serveChainSince", + Key::ServeStateSince => "serveStateSince", + Key::TxRelay => "txRelay", + Key::BufferLimit => "flowControl/BL", + Key::BufferCostTable => "flowControl/MRC", + Key::BufferRechargeRate => "flowControl/MRR", + } + } + + // try to parse the key value from a string. + fn from_str(s: &str) -> Option { + match s { + "protocolVersion" => Some(Key::ProtocolVersion), + "networkId" => Some(Key::NetworkId), + "headTd" => Some(Key::HeadTD), + "headHash" => Some(Key::HeadHash), + "headNum" => Some(Key::HeadNum), + "genesisHash" => Some(Key::GenesisHash), + "serveHeaders" => Some(Key::ServeHeaders), + "serveChainSince" => Some(Key::ServeChainSince), + "serveStateSince" => Some(Key::ServeStateSince), + "txRelay" => Some(Key::TxRelay), + "flowControl/BL" => Some(Key::BufferLimit), + "flowControl/MRC" => Some(Key::BufferCostTable), + "flowControl/MRR" => Some(Key::BufferRechargeRate), + _ => None + } + } +} + +/// Network ID structure. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum NetworkId { + /// ID for the mainnet + Mainnet = 1, + /// ID for the testnet + Testnet = 0, +} + +impl NetworkId { + fn from_raw(raw: u32) -> Option { + match raw { + 0 => Some(NetworkId::Testnet), + 1 => Some(NetworkId::Mainnet), + _ => None, + } + } +} + +// helper for decoding key-value pairs in the handshake or an announcement. +struct Parser<'a> { + pos: usize, + rlp: UntrustedRlp<'a>, +} + +impl<'a> Parser<'a> { + // expect a specific next key, and decode the value. + // error on unexpected key or invalid value. + fn expect(&mut self, key: Key) -> Result { + self.expect_raw(key).and_then(|item| item.as_val()) + } + + // expect a specific next key, and get the value's RLP. + // if the key isn't found, the position isn't advanced. + fn expect_raw(&mut self, key: Key) -> Result, DecoderError> { + let pre_pos = self.pos; + if let Some((k, val)) = try!(self.get_next()) { + if k == key { return Ok(val) } + } + + self.pos = pre_pos; + Err(DecoderError::Custom("Missing expected key")) + } + + // get the next key and value RLP. + fn get_next(&mut self) -> Result)>, DecoderError> { + while self.pos < self.rlp.item_count() { + let pair = try!(self.rlp.at(self.pos)); + let k: String = try!(pair.val_at(0)); + + self.pos += 1; + match Key::from_str(&k) { + Some(key) => return Ok(Some((key , try!(pair.at(1))))), + None => continue, + } + } + + Ok(None) + } +} + +// Helper for encoding a key-value pair +fn encode_pair(key: Key, val: &T) -> Vec { + let mut s = RlpStream::new_list(2); + s.append(&key.as_str()).append(val); + s.out() +} + +// Helper for encoding a flag. +fn encode_flag(key: Key) -> Vec { + let mut s = RlpStream::new_list(2); + s.append(&key.as_str()).append_empty_data(); + s.out() +} + +/// A peer status message. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Status { + /// Protocol version. + pub protocol_version: u32, + /// Network id of this peer. + pub network_id: NetworkId, + /// Total difficulty of the head of the chain. + pub head_td: U256, + /// Hash of the best block. + pub head_hash: H256, + /// Number of the best block. + pub head_num: u64, + /// Genesis hash + pub genesis_hash: H256, + /// Last announced chain head and reorg depth to common ancestor. + pub last_head: Option<(H256, u64)>, +} + +/// Peer capabilities. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Capabilities { + /// Whether this peer can serve headers + pub serve_headers: bool, + /// Earliest block number it can serve block/receipt requests for. + pub serve_chain_since: Option, + /// Earliest block number it can serve state requests for. + pub serve_state_since: Option, + /// Whether it can relay transactions to the eth network. + pub tx_relay: bool, +} + +impl Default for Capabilities { + fn default() -> Self { + Capabilities { + serve_headers: true, + serve_chain_since: None, + serve_state_since: None, + tx_relay: false, + } + } +} + +/// Attempt to parse a handshake message into its three parts: +/// - chain status +/// - serving capabilities +/// - buffer flow parameters +pub fn parse_handshake(rlp: UntrustedRlp) -> Result<(Status, Capabilities, FlowParams), DecoderError> { + let mut parser = Parser { + pos: 0, + rlp: rlp, + }; + + let status = Status { + protocol_version: try!(parser.expect(Key::ProtocolVersion)), + network_id: try!(parser.expect(Key::NetworkId) + .and_then(|id: u32| NetworkId::from_raw(id).ok_or(DecoderError::Custom("Invalid network ID")))), + head_td: try!(parser.expect(Key::HeadTD)), + head_hash: try!(parser.expect(Key::HeadHash)), + head_num: try!(parser.expect(Key::HeadNum)), + genesis_hash: try!(parser.expect(Key::GenesisHash)), + last_head: None, + }; + + let capabilities = Capabilities { + serve_headers: parser.expect_raw(Key::ServeHeaders).is_ok(), + serve_chain_since: parser.expect(Key::ServeChainSince).ok(), + serve_state_since: parser.expect(Key::ServeStateSince).ok(), + tx_relay: parser.expect_raw(Key::TxRelay).is_ok(), + }; + + let flow_params = FlowParams::new( + try!(parser.expect(Key::BufferLimit)), + try!(parser.expect(Key::BufferCostTable)), + try!(parser.expect(Key::BufferRechargeRate)), + ); + + Ok((status, capabilities, flow_params)) +} + +/// Write a handshake, given status, capabilities, and flow parameters. +pub fn write_handshake(status: &Status, capabilities: &Capabilities, flow_params: &FlowParams) -> Vec { + let mut pairs = Vec::new(); + pairs.push(encode_pair(Key::ProtocolVersion, &status.protocol_version)); + pairs.push(encode_pair(Key::NetworkId, &(status.network_id as u32))); + pairs.push(encode_pair(Key::HeadTD, &status.head_td)); + pairs.push(encode_pair(Key::HeadHash, &status.head_hash)); + pairs.push(encode_pair(Key::HeadNum, &status.head_num)); + pairs.push(encode_pair(Key::GenesisHash, &status.genesis_hash)); + + if capabilities.serve_headers { + pairs.push(encode_flag(Key::ServeHeaders)); + } + if let Some(ref serve_chain_since) = capabilities.serve_chain_since { + pairs.push(encode_pair(Key::ServeChainSince, serve_chain_since)); + } + if let Some(ref serve_state_since) = capabilities.serve_state_since { + pairs.push(encode_pair(Key::ServeStateSince, serve_state_since)); + } + if capabilities.tx_relay { + pairs.push(encode_flag(Key::TxRelay)); + } + + pairs.push(encode_pair(Key::BufferLimit, flow_params.limit())); + pairs.push(encode_pair(Key::BufferCostTable, flow_params.cost_table())); + pairs.push(encode_pair(Key::BufferRechargeRate, flow_params.recharge_rate())); + + let mut stream = RlpStream::new_list(pairs.len()); + + for pair in pairs { + stream.append_raw(&pair, 1); + } + + stream.out() +} + +/// An announcement of new chain head or capabilities made by a peer. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Announcement { + /// Hash of the best block. + pub head_hash: H256, + /// Number of the best block. + pub head_num: u64, + /// Head total difficulty + pub head_td: U256, + /// reorg depth to common ancestor of last announced head. + pub reorg_depth: u64, + /// optional new header-serving capability. false means "no change" + pub serve_headers: bool, + /// optional new state-serving capability + pub serve_state_since: Option, + /// optional new chain-serving capability + pub serve_chain_since: Option, + /// optional new transaction-relay capability. false means "no change" + pub tx_relay: bool, + // TODO: changes in buffer flow? +} + +/// Parse an announcement. +pub fn parse_announcement(rlp: UntrustedRlp) -> Result { + let mut last_key = None; + + let mut announcement = Announcement { + head_hash: try!(rlp.val_at(0)), + head_num: try!(rlp.val_at(1)), + head_td: try!(rlp.val_at(2)), + reorg_depth: try!(rlp.val_at(3)), + serve_headers: false, + serve_state_since: None, + serve_chain_since: None, + tx_relay: false, + }; + + let mut parser = Parser { + pos: 4, + rlp: rlp, + }; + + while let Some((key, item)) = try!(parser.get_next()) { + if Some(key) <= last_key { return Err(DecoderError::Custom("Invalid announcement key ordering")) } + last_key = Some(key); + + match key { + Key::ServeHeaders => announcement.serve_headers = true, + Key::ServeStateSince => announcement.serve_state_since = Some(try!(item.as_val())), + Key::ServeChainSince => announcement.serve_chain_since = Some(try!(item.as_val())), + Key::TxRelay => announcement.tx_relay = true, + _ => return Err(DecoderError::Custom("Nonsensical key in announcement")), + } + } + + Ok(announcement) +} + +/// Write an announcement out. +pub fn write_announcement(announcement: &Announcement) -> Vec { + let mut pairs = Vec::new(); + if announcement.serve_headers { + pairs.push(encode_flag(Key::ServeHeaders)); + } + if let Some(ref serve_chain_since) = announcement.serve_chain_since { + pairs.push(encode_pair(Key::ServeChainSince, serve_chain_since)); + } + if let Some(ref serve_state_since) = announcement.serve_state_since { + pairs.push(encode_pair(Key::ServeStateSince, serve_state_since)); + } + if announcement.tx_relay { + pairs.push(encode_flag(Key::TxRelay)); + } + + let mut stream = RlpStream::new_list(4 + pairs.len()); + stream + .append(&announcement.head_hash) + .append(&announcement.head_num) + .append(&announcement.head_td) + .append(&announcement.reorg_depth); + + for item in pairs { + stream.append_raw(&item, 1); + } + + stream.out() +} + +#[cfg(test)] +mod tests { + use super::*; + use super::super::buffer_flow::FlowParams; + use util::{U256, H256, FixedHash}; + use rlp::{RlpStream, Stream ,UntrustedRlp, View}; + + #[test] + fn full_handshake() { + let status = Status { + protocol_version: 1, + network_id: NetworkId::Mainnet, + head_td: U256::default(), + head_hash: H256::default(), + head_num: 10, + genesis_hash: H256::zero(), + last_head: None, + }; + + let capabilities = Capabilities { + serve_headers: true, + serve_chain_since: Some(5), + serve_state_since: Some(8), + tx_relay: true, + }; + + let flow_params = FlowParams::new( + 1_000_000.into(), + Default::default(), + 1000.into(), + ); + + let handshake = write_handshake(&status, &capabilities, &flow_params); + + let (read_status, read_capabilities, read_flow) + = parse_handshake(UntrustedRlp::new(&handshake)).unwrap(); + + assert_eq!(read_status, status); + assert_eq!(read_capabilities, capabilities); + assert_eq!(read_flow, flow_params); + } + + #[test] + fn partial_handshake() { + let status = Status { + protocol_version: 1, + network_id: NetworkId::Mainnet, + head_td: U256::default(), + head_hash: H256::default(), + head_num: 10, + genesis_hash: H256::zero(), + last_head: None, + }; + + let capabilities = Capabilities { + serve_headers: false, + serve_chain_since: Some(5), + serve_state_since: None, + tx_relay: true, + }; + + let flow_params = FlowParams::new( + 1_000_000.into(), + Default::default(), + 1000.into(), + ); + + let handshake = write_handshake(&status, &capabilities, &flow_params); + + let (read_status, read_capabilities, read_flow) + = parse_handshake(UntrustedRlp::new(&handshake)).unwrap(); + + assert_eq!(read_status, status); + assert_eq!(read_capabilities, capabilities); + assert_eq!(read_flow, flow_params); + } + + #[test] + fn skip_unknown_keys() { + let status = Status { + protocol_version: 1, + network_id: NetworkId::Mainnet, + head_td: U256::default(), + head_hash: H256::default(), + head_num: 10, + genesis_hash: H256::zero(), + last_head: None, + }; + + let capabilities = Capabilities { + serve_headers: false, + serve_chain_since: Some(5), + serve_state_since: None, + tx_relay: true, + }; + + let flow_params = FlowParams::new( + 1_000_000.into(), + Default::default(), + 1000.into(), + ); + + let handshake = write_handshake(&status, &capabilities, &flow_params); + let interleaved = { + let handshake = UntrustedRlp::new(&handshake); + let mut stream = RlpStream::new_list(handshake.item_count() * 3); + + for item in handshake.iter() { + stream.append_raw(item.as_raw(), 1); + let (mut s1, mut s2) = (RlpStream::new_list(2), RlpStream::new_list(2)); + s1.append(&"foo").append_empty_data(); + s2.append(&"bar").append_empty_data(); + stream.append_raw(&s1.out(), 1); + stream.append_raw(&s2.out(), 1); + } + + stream.out() + }; + + let (read_status, read_capabilities, read_flow) + = parse_handshake(UntrustedRlp::new(&interleaved)).unwrap(); + + assert_eq!(read_status, status); + assert_eq!(read_capabilities, capabilities); + assert_eq!(read_flow, flow_params); + } + + #[test] + fn announcement_roundtrip() { + let announcement = Announcement { + head_hash: H256::random(), + head_num: 100_000, + head_td: 1_000_000.into(), + reorg_depth: 4, + serve_headers: false, + serve_state_since: Some(99_000), + serve_chain_since: Some(1), + tx_relay: true, + }; + + let serialized = write_announcement(&announcement); + let read = parse_announcement(UntrustedRlp::new(&serialized)).unwrap(); + + assert_eq!(read, announcement); + } + + #[test] + fn keys_out_of_order() { + use super::{Key, encode_pair, encode_flag}; + + let mut stream = RlpStream::new_list(6); + stream + .append(&H256::zero()) + .append(&10u64) + .append(&100_000u64) + .append(&2u64) + .append_raw(&encode_pair(Key::ServeStateSince, &44u64), 1) + .append_raw(&encode_flag(Key::ServeHeaders), 1); + + let out = stream.drain(); + assert!(parse_announcement(UntrustedRlp::new(&out)).is_err()); + + let mut stream = RlpStream::new_list(6); + stream + .append(&H256::zero()) + .append(&10u64) + .append(&100_000u64) + .append(&2u64) + .append_raw(&encode_flag(Key::ServeHeaders), 1) + .append_raw(&encode_pair(Key::ServeStateSince, &44u64), 1); + + let out = stream.drain(); + assert!(parse_announcement(UntrustedRlp::new(&out)).is_ok()); + } +} \ No newline at end of file diff --git a/ethcore/light/src/provider.rs b/ethcore/light/src/provider.rs new file mode 100644 index 000000000..b1625f95f --- /dev/null +++ b/ethcore/light/src/provider.rs @@ -0,0 +1,71 @@ +// 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 . + +//! A provider for the LES protocol. This is typically a full node, who can +//! give as much data as necessary to its peers. + +use ethcore::transaction::SignedTransaction; +use ethcore::blockchain_info::BlockChainInfo; +use util::{Bytes, H256}; + +use request; + +/// Defines the operations that a provider for `LES` must fulfill. +/// +/// These are defined at [1], but may be subject to change. +/// Requests which can't be fulfilled should return an empty RLP list. +/// +/// [1]: https://github.com/ethcore/parity/wiki/Light-Ethereum-Subprotocol-(LES) +pub trait Provider: Send + Sync { + /// Provide current blockchain info. + fn chain_info(&self) -> BlockChainInfo; + + /// Find the depth of a common ancestor between two blocks. + fn reorg_depth(&self, a: &H256, b: &H256) -> Option; + + /// Earliest state. + fn earliest_state(&self) -> Option; + + /// Provide a list of headers starting at the requested block, + /// possibly in reverse and skipping `skip` at a time. + /// + /// The returned vector may have any length in the range [0, `max`], but the + /// results within must adhere to the `skip` and `reverse` parameters. + fn block_headers(&self, req: request::Headers) -> Vec; + + /// Provide as many as possible of the requested blocks (minus the headers) encoded + /// in RLP format. + fn block_bodies(&self, req: request::Bodies) -> Vec; + + /// Provide the receipts as many as possible of the requested blocks. + /// Returns a vector of RLP-encoded lists of receipts. + fn receipts(&self, req: request::Receipts) -> Vec; + + /// Provide a set of merkle proofs, as requested. Each request is a + /// block hash and request parameters. + /// + /// Returns a vector to RLP-encoded lists satisfying the requests. + fn proofs(&self, req: request::StateProofs) -> Vec; + + /// Provide contract code for the specified (block_hash, account_hash) pairs. + fn code(&self, req: request::ContractCodes) -> Vec; + + /// Provide header proofs from the Canonical Hash Tries. + fn header_proofs(&self, req: request::HeaderProofs) -> Vec; + + /// Provide pending transactions. + fn pending_transactions(&self) -> Vec; +} \ No newline at end of file diff --git a/ethcore/light/src/request.rs b/ethcore/light/src/request.rs new file mode 100644 index 000000000..f043f0f25 --- /dev/null +++ b/ethcore/light/src/request.rs @@ -0,0 +1,145 @@ +// 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 . + +//! LES request types. + +// TODO: make IPC compatible. + +use util::H256; + +/// A request for block headers. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Headers { + /// Block information for the request being made. + pub block: (u64, H256), + /// The maximum amount of headers which can be returned. + pub max: usize, + /// The amount of headers to skip between each response entry. + pub skip: usize, + /// Whether the headers should proceed in falling number from the initial block. + pub reverse: bool, +} + +/// A request for specific block bodies. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Bodies { + /// Hashes which bodies are being requested for. + pub block_hashes: Vec +} + +/// A request for transaction receipts. +/// +/// This request is answered with a list of transaction receipts for each block +/// requested. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Receipts { + /// Block hashes to return receipts for. + pub block_hashes: Vec, +} + +/// A request for a state proof +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StateProof { + /// Block hash to query state from. + pub block: H256, + /// Key of the state trie -- corresponds to account hash. + pub key1: H256, + /// Key in that account's storage trie; if empty, then the account RLP should be + /// returned. + pub key2: Option, + /// if greater than zero, trie nodes beyond this level may be omitted. + pub from_level: u32, // could even safely be u8; trie w/ 32-byte key can be at most 64-levels deep. +} + +/// A request for state proofs. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StateProofs { + /// All the proof requests. + pub requests: Vec, +} + +/// A request for contract code. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ContractCodes { + /// Block hash and account key (== sha3(address)) pairs to fetch code for. + pub code_requests: Vec<(H256, H256)>, +} + +/// A request for a header proof from the Canonical Hash Trie. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HeaderProof { + /// Number of the CHT. + pub cht_number: u64, + /// Block number requested. + pub block_number: u64, + /// If greater than zero, trie nodes beyond this level may be omitted. + pub from_level: u32, +} + +/// A request for header proofs from the CHT. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HeaderProofs { + /// All the proof requests. + pub requests: Vec, +} + +/// Kinds of requests. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Kind { + /// Requesting headers. + Headers, + /// Requesting block bodies. + Bodies, + /// Requesting transaction receipts. + Receipts, + /// Requesting proofs of state trie nodes. + StateProofs, + /// Requesting contract code by hash. + Codes, + /// Requesting header proofs (from the CHT). + HeaderProofs, +} + +/// Encompasses all possible types of requests in a single structure. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Request { + /// Requesting headers. + Headers(Headers), + /// Requesting block bodies. + Bodies(Bodies), + /// Requesting transaction receipts. + Receipts(Receipts), + /// Requesting state proofs. + StateProofs(StateProofs), + /// Requesting contract codes. + Codes(ContractCodes), + /// Requesting header proofs. + HeaderProofs(HeaderProofs), +} + +impl Request { + /// Get the kind of request this is. + pub fn kind(&self) -> Kind { + match *self { + Request::Headers(_) => Kind::Headers, + Request::Bodies(_) => Kind::Bodies, + Request::Receipts(_) => Kind::Receipts, + Request::StateProofs(_) => Kind::StateProofs, + Request::Codes(_) => Kind::Codes, + Request::HeaderProofs(_) => Kind::HeaderProofs, + } + } +} \ No newline at end of file diff --git a/ethcore/src/lib.rs b/ethcore/src/lib.rs index c7f40418c..bf3e59171 100644 --- a/ethcore/src/lib.rs +++ b/ethcore/src/lib.rs @@ -137,6 +137,7 @@ pub mod miner; pub mod snapshot; pub mod action_params; pub mod db; +pub mod verification; #[macro_use] pub mod evm; mod cache_manager; @@ -150,7 +151,6 @@ mod account_db; mod builtin; mod executive; mod externalities; -mod verification; mod blockchain; mod types; mod factory; diff --git a/ethcore/src/pod_account.rs b/ethcore/src/pod_account.rs index afee32f94..0882b688c 100644 --- a/ethcore/src/pod_account.rs +++ b/ethcore/src/pod_account.rs @@ -89,7 +89,7 @@ impl From for PodAccount { let key: U256 = key.into(); let value: U256 = value.into(); (H256::from(key), H256::from(value)) - }).collect() + }).collect(), } } } @@ -99,8 +99,12 @@ impl From for PodAccount { PodAccount { balance: a.balance.map_or_else(U256::zero, Into::into), nonce: a.nonce.map_or_else(U256::zero, Into::into), - code: a.code.map(Into::into).or_else(|| Some(Vec::new())), - storage: BTreeMap::new() + code: Some(a.code.map_or_else(Vec::new, Into::into)), + storage: a.storage.map_or_else(BTreeMap::new, |s| s.into_iter().map(|(key, value)| { + let key: U256 = key.into(); + let value: U256 = value.into(); + (H256::from(key), H256::from(value)) + }).collect()), } } } @@ -112,7 +116,7 @@ impl fmt::Display for PodAccount { self.nonce, self.code.as_ref().map_or(0, |c| c.len()), self.code.as_ref().map_or_else(H256::new, |c| c.sha3()), - self.storage.len() + self.storage.len(), ) } } diff --git a/ethcore/src/snapshot/error.rs b/ethcore/src/snapshot/error.rs index d634057dc..d417695f0 100644 --- a/ethcore/src/snapshot/error.rs +++ b/ethcore/src/snapshot/error.rs @@ -45,6 +45,8 @@ pub enum Error { MissingCode(Vec), /// Unrecognized code encoding. UnrecognizedCodeState(u8), + /// Restoration aborted. + RestorationAborted, /// Trie error. Trie(TrieError), /// Decoder error. @@ -67,6 +69,7 @@ impl fmt::Display for Error { a pruned database. Please re-run with the --pruning archive flag."), Error::MissingCode(ref missing) => write!(f, "Incomplete snapshot: {} contract codes not found.", missing.len()), Error::UnrecognizedCodeState(state) => write!(f, "Unrecognized code encoding ({})", state), + Error::RestorationAborted => write!(f, "Snapshot restoration aborted."), Error::Io(ref err) => err.fmt(f), Error::Decoder(ref err) => err.fmt(f), Error::Trie(ref err) => err.fmt(f), diff --git a/ethcore/src/snapshot/mod.rs b/ethcore/src/snapshot/mod.rs index 22c44ba3b..f4d791593 100644 --- a/ethcore/src/snapshot/mod.rs +++ b/ethcore/src/snapshot/mod.rs @@ -407,30 +407,28 @@ impl StateRebuilder { } /// Feed an uncompressed state chunk into the rebuilder. - pub fn feed(&mut self, chunk: &[u8]) -> Result<(), ::error::Error> { + pub fn feed(&mut self, chunk: &[u8], flag: &AtomicBool) -> Result<(), ::error::Error> { let rlp = UntrustedRlp::new(chunk); let empty_rlp = StateAccount::new_basic(U256::zero(), U256::zero()).rlp(); - let account_fat_rlps: Vec<_> = rlp.iter().map(|r| r.as_raw()).collect(); let mut pairs = Vec::with_capacity(rlp.item_count()); // initialize the pairs vector with empty values so we have slots to write into. pairs.resize(rlp.item_count(), (H256::new(), Vec::new())); - let chunk_size = account_fat_rlps.len() / ::num_cpus::get() + 1; + let status = try!(rebuild_accounts( + self.db.as_hashdb_mut(), + rlp, + &mut pairs, + &self.code_map, + flag + )); - // new code contained within this chunk. - let mut chunk_code = HashMap::new(); - - for (account_chunk, out_pairs_chunk) in account_fat_rlps.chunks(chunk_size).zip(pairs.chunks_mut(chunk_size)) { - let code_map = &self.code_map; - let status = try!(rebuild_accounts(self.db.as_hashdb_mut(), account_chunk, out_pairs_chunk, code_map)); - chunk_code.extend(status.new_code); - for (addr_hash, code_hash) in status.missing_code { - self.missing_code.entry(code_hash).or_insert_with(Vec::new).push(addr_hash); - } + for (addr_hash, code_hash) in status.missing_code { + self.missing_code.entry(code_hash).or_insert_with(Vec::new).push(addr_hash); } + // patch up all missing code. must be done after collecting all new missing code entries. - for (code_hash, code) in chunk_code { + for (code_hash, code) in status.new_code { for addr_hash in self.missing_code.remove(&code_hash).unwrap_or_else(Vec::new) { let mut db = AccountDBMut::from_hash(self.db.as_hashdb_mut(), addr_hash); db.emplace(code_hash, DBValue::from_slice(&code)); @@ -450,6 +448,8 @@ impl StateRebuilder { }; for (hash, thin_rlp) in pairs { + if !flag.load(Ordering::SeqCst) { return Err(Error::RestorationAborted.into()) } + if &thin_rlp[..] != &empty_rlp[..] { self.bloom.set(&*hash); } @@ -487,17 +487,18 @@ struct RebuiltStatus { } // rebuild a set of accounts and their storage. -// returns +// returns a status detailing newly-loaded code and accounts missing code. fn rebuild_accounts( db: &mut HashDB, - account_chunk: &[&[u8]], + account_fat_rlps: UntrustedRlp, out_chunk: &mut [(H256, Bytes)], - code_map: &HashMap + code_map: &HashMap, + abort_flag: &AtomicBool ) -> Result { let mut status = RebuiltStatus::default(); - for (account_pair, out) in account_chunk.into_iter().zip(out_chunk) { - let account_rlp = UntrustedRlp::new(account_pair); + for (account_rlp, out) in account_fat_rlps.into_iter().zip(out_chunk) { + if !abort_flag.load(Ordering::SeqCst) { return Err(Error::RestorationAborted.into()) } let hash: H256 = try!(account_rlp.val_at(0)); let fat_rlp = try!(account_rlp.at(1)); @@ -580,7 +581,7 @@ impl BlockRebuilder { /// Feed the rebuilder an uncompressed block chunk. /// Returns the number of blocks fed or any errors. - pub fn feed(&mut self, chunk: &[u8], engine: &Engine) -> Result { + pub fn feed(&mut self, chunk: &[u8], engine: &Engine, abort_flag: &AtomicBool) -> Result { use basic_types::Seal::With; use util::U256; use util::triehash::ordered_trie_root; @@ -601,6 +602,8 @@ impl BlockRebuilder { let parent_total_difficulty = try!(rlp.val_at::(2)); for idx in 3..item_count { + if !abort_flag.load(Ordering::SeqCst) { return Err(Error::RestorationAborted.into()) } + let pair = try!(rlp.at(idx)); let abridged_rlp = try!(pair.at(0)).as_raw().to_owned(); let abridged_block = AbridgedBlock::from_raw(abridged_rlp); diff --git a/ethcore/src/snapshot/service.rs b/ethcore/src/snapshot/service.rs index 16f7c6ec6..c0d34a6a9 100644 --- a/ethcore/src/snapshot/service.rs +++ b/ethcore/src/snapshot/service.rs @@ -118,12 +118,12 @@ impl Restoration { }) } - // feeds a state chunk - fn feed_state(&mut self, hash: H256, chunk: &[u8]) -> Result<(), Error> { + // feeds a state chunk, aborts early if `flag` becomes false. + fn feed_state(&mut self, hash: H256, chunk: &[u8], flag: &AtomicBool) -> Result<(), Error> { if self.state_chunks_left.remove(&hash) { let len = try!(snappy::decompress_into(chunk, &mut self.snappy_buffer)); - try!(self.state.feed(&self.snappy_buffer[..len])); + try!(self.state.feed(&self.snappy_buffer[..len], flag)); if let Some(ref mut writer) = self.writer.as_mut() { try!(writer.write_state_chunk(hash, chunk)); @@ -134,11 +134,11 @@ impl Restoration { } // feeds a block chunk - fn feed_blocks(&mut self, hash: H256, chunk: &[u8], engine: &Engine) -> Result<(), Error> { + fn feed_blocks(&mut self, hash: H256, chunk: &[u8], engine: &Engine, flag: &AtomicBool) -> Result<(), Error> { if self.block_chunks_left.remove(&hash) { let len = try!(snappy::decompress_into(chunk, &mut self.snappy_buffer)); - try!(self.blocks.feed(&self.snappy_buffer[..len], engine)); + try!(self.blocks.feed(&self.snappy_buffer[..len], engine, flag)); if let Some(ref mut writer) = self.writer.as_mut() { try!(writer.write_block_chunk(hash, chunk)); } @@ -224,6 +224,7 @@ pub struct Service { db_restore: Arc, progress: super::Progress, taking_snapshot: AtomicBool, + restoring_snapshot: AtomicBool, } impl Service { @@ -244,6 +245,7 @@ impl Service { db_restore: params.db_restore, progress: Default::default(), taking_snapshot: AtomicBool::new(false), + restoring_snapshot: AtomicBool::new(false), }; // create the root snapshot dir if it doesn't exist. @@ -436,6 +438,8 @@ impl Service { state_chunks_done: self.state_chunks.load(Ordering::SeqCst) as u32, block_chunks_done: self.block_chunks.load(Ordering::SeqCst) as u32, }; + + self.restoring_snapshot.store(true, Ordering::SeqCst); Ok(()) } @@ -490,8 +494,8 @@ impl Service { }; (match is_state { - true => rest.feed_state(hash, chunk), - false => rest.feed_blocks(hash, chunk, &*self.engine), + true => rest.feed_state(hash, chunk, &self.restoring_snapshot), + false => rest.feed_blocks(hash, chunk, &*self.engine, &self.restoring_snapshot), }.map(|_| rest.is_done()), rest.db.clone()) }; @@ -573,6 +577,7 @@ impl SnapshotService for Service { } fn abort_restore(&self) { + self.restoring_snapshot.store(false, Ordering::SeqCst); *self.restoration.lock() = None; *self.status.lock() = RestorationStatus::Inactive; } diff --git a/ethcore/src/snapshot/tests/blocks.rs b/ethcore/src/snapshot/tests/blocks.rs index 12efcda77..18637bad1 100644 --- a/ethcore/src/snapshot/tests/blocks.rs +++ b/ethcore/src/snapshot/tests/blocks.rs @@ -17,10 +17,11 @@ //! Block chunker and rebuilder tests. use devtools::RandomTempPath; +use error::Error; use blockchain::generator::{ChainGenerator, ChainIterator, BlockFinalizer}; use blockchain::BlockChain; -use snapshot::{chunk_blocks, BlockRebuilder, Progress}; +use snapshot::{chunk_blocks, BlockRebuilder, Error as SnapshotError, Progress}; use snapshot::io::{PackedReader, PackedWriter, SnapshotReader, SnapshotWriter}; use util::{Mutex, snappy}; @@ -28,6 +29,7 @@ use util::kvdb::{Database, DatabaseConfig}; use std::collections::HashMap; use std::sync::Arc; +use std::sync::atomic::AtomicBool; fn chunk_and_restore(amount: u64) { let mut canon_chain = ChainGenerator::default(); @@ -75,10 +77,11 @@ fn chunk_and_restore(amount: u64) { let mut rebuilder = BlockRebuilder::new(new_chain, new_db.clone(), &manifest).unwrap(); let reader = PackedReader::new(&snapshot_path).unwrap().unwrap(); let engine = ::engines::NullEngine::new(Default::default(), Default::default()); + let flag = AtomicBool::new(true); for chunk_hash in &reader.manifest().block_hashes { let compressed = reader.chunk(*chunk_hash).unwrap(); let chunk = snappy::decompress(&compressed).unwrap(); - rebuilder.feed(&chunk, &engine).unwrap(); + rebuilder.feed(&chunk, &engine, &flag).unwrap(); } rebuilder.finalize(HashMap::new()).unwrap(); @@ -93,3 +96,46 @@ fn chunk_and_restore_500() { chunk_and_restore(500) } #[test] fn chunk_and_restore_40k() { chunk_and_restore(40000) } + +#[test] +fn checks_flag() { + use ::rlp::{RlpStream, Stream}; + use util::H256; + + let mut stream = RlpStream::new_list(5); + + stream.append(&100u64) + .append(&H256::default()) + .append(&(!0u64)); + + stream.append_empty_data().append_empty_data(); + + let genesis = { + let mut canon_chain = ChainGenerator::default(); + let mut finalizer = BlockFinalizer::default(); + canon_chain.generate(&mut finalizer).unwrap() + }; + + let chunk = stream.out(); + let path = RandomTempPath::create_dir(); + + let db_cfg = DatabaseConfig::with_columns(::db::NUM_COLUMNS); + let db = Arc::new(Database::open(&db_cfg, path.as_str()).unwrap()); + let chain = BlockChain::new(Default::default(), &genesis, db.clone()); + let engine = ::engines::NullEngine::new(Default::default(), Default::default()); + + let manifest = ::snapshot::ManifestData { + state_hashes: Vec::new(), + block_hashes: Vec::new(), + state_root: ::util::sha3::SHA3_NULL_RLP, + block_number: 102, + block_hash: H256::default(), + }; + + let mut rebuilder = BlockRebuilder::new(chain, db.clone(), &manifest).unwrap(); + + match rebuilder.feed(&chunk, &engine, &AtomicBool::new(false)) { + Err(Error::Snapshot(SnapshotError::RestorationAborted)) => {} + _ => panic!("Wrong result on abort flag set") + } +} \ No newline at end of file diff --git a/ethcore/src/snapshot/tests/state.rs b/ethcore/src/snapshot/tests/state.rs index e1d4df5f9..05537fa96 100644 --- a/ethcore/src/snapshot/tests/state.rs +++ b/ethcore/src/snapshot/tests/state.rs @@ -16,10 +16,12 @@ //! State snapshotting tests. -use snapshot::{chunk_state, Progress, StateRebuilder}; +use snapshot::{chunk_state, Error as SnapshotError, Progress, StateRebuilder}; use snapshot::io::{PackedReader, PackedWriter, SnapshotReader, SnapshotWriter}; use super::helpers::{compare_dbs, StateProducer}; +use error::Error; + use rand::{XorShiftRng, SeedableRng}; use util::hash::H256; use util::journaldb::{self, Algorithm}; @@ -29,6 +31,7 @@ use util::Mutex; use devtools::RandomTempPath; use std::sync::Arc; +use std::sync::atomic::AtomicBool; #[test] fn snap_and_restore() { @@ -65,11 +68,13 @@ fn snap_and_restore() { let mut rebuilder = StateRebuilder::new(new_db.clone(), Algorithm::Archive); let reader = PackedReader::new(&snap_file).unwrap().unwrap(); + let flag = AtomicBool::new(true); + for chunk_hash in &reader.manifest().state_hashes { let raw = reader.chunk(*chunk_hash).unwrap(); let chunk = ::util::snappy::decompress(&raw).unwrap(); - rebuilder.feed(&chunk).unwrap(); + rebuilder.feed(&chunk, &flag).unwrap(); } assert_eq!(rebuilder.state_root(), state_root); @@ -82,3 +87,52 @@ fn snap_and_restore() { compare_dbs(&old_db, new_db.as_hashdb()); } + +#[test] +fn checks_flag() { + let mut producer = StateProducer::new(); + let mut rng = XorShiftRng::from_seed([5, 6, 7, 8]); + let mut old_db = MemoryDB::new(); + let db_cfg = DatabaseConfig::with_columns(::db::NUM_COLUMNS); + + for _ in 0..10 { + producer.tick(&mut rng, &mut old_db); + } + + let snap_dir = RandomTempPath::create_dir(); + let mut snap_file = snap_dir.as_path().to_owned(); + snap_file.push("SNAP"); + + let state_root = producer.state_root(); + let writer = Mutex::new(PackedWriter::new(&snap_file).unwrap()); + + let state_hashes = chunk_state(&old_db, &state_root, &writer, &Progress::default()).unwrap(); + + writer.into_inner().finish(::snapshot::ManifestData { + state_hashes: state_hashes, + block_hashes: Vec::new(), + state_root: state_root, + block_number: 0, + block_hash: H256::default(), + }).unwrap(); + + let mut db_path = snap_dir.as_path().to_owned(); + db_path.push("db"); + { + let new_db = Arc::new(Database::open(&db_cfg, &db_path.to_string_lossy()).unwrap()); + let mut rebuilder = StateRebuilder::new(new_db.clone(), Algorithm::Archive); + let reader = PackedReader::new(&snap_file).unwrap().unwrap(); + + let flag = AtomicBool::new(false); + + for chunk_hash in &reader.manifest().state_hashes { + let raw = reader.chunk(*chunk_hash).unwrap(); + let chunk = ::util::snappy::decompress(&raw).unwrap(); + + match rebuilder.feed(&chunk, &flag) { + Err(Error::Snapshot(SnapshotError::RestorationAborted)) => {}, + _ => panic!("unexpected result when feeding with flag off"), + } + } + } +} \ No newline at end of file diff --git a/ethcore/src/spec/spec.rs b/ethcore/src/spec/spec.rs index 2babfb708..6bfb1bcc1 100644 --- a/ethcore/src/spec/spec.rs +++ b/ethcore/src/spec/spec.rs @@ -162,7 +162,7 @@ impl Spec { /// Get the configured Network ID. pub fn network_id(&self) -> usize { self.params.network_id } - /// Get the configured Network ID. + /// Get the configured subprotocol name. pub fn subprotocol_name(&self) -> String { self.params.subprotocol_name.clone() } /// Get the configured network fork block. diff --git a/ethcore/src/types/mod.rs.in b/ethcore/src/types/mod.rs.in index 1d2cdb3c0..6ef67009a 100644 --- a/ethcore/src/types/mod.rs.in +++ b/ethcore/src/types/mod.rs.in @@ -33,4 +33,4 @@ pub mod transaction_import; pub mod block_import_error; pub mod restoration_status; pub mod snapshot_manifest; -pub mod mode; \ No newline at end of file +pub mod mode; diff --git a/ethcore/src/verification/canon_verifier.rs b/ethcore/src/verification/canon_verifier.rs index cc6bc448a..b5b01279e 100644 --- a/ethcore/src/verification/canon_verifier.rs +++ b/ethcore/src/verification/canon_verifier.rs @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +//! Canonical verifier. + use blockchain::BlockProvider; use engines::Engine; use error::Error; @@ -21,6 +23,7 @@ use header::Header; use super::Verifier; use super::verification; +/// A canonial verifier -- this does full verification. pub struct CanonVerifier; impl Verifier for CanonVerifier { diff --git a/ethcore/src/verification/mod.rs b/ethcore/src/verification/mod.rs index 239c88597..55663052b 100644 --- a/ethcore/src/verification/mod.rs +++ b/ethcore/src/verification/mod.rs @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +//! Block verification utilities. + pub mod verification; pub mod verifier; pub mod queue; @@ -44,6 +46,7 @@ impl Default for VerifierType { } } +/// Create a new verifier based on type. pub fn new(v: VerifierType) -> Box { match v { VerifierType::Canon | VerifierType::CanonNoSeal => Box::new(CanonVerifier), diff --git a/ethcore/src/verification/noop_verifier.rs b/ethcore/src/verification/noop_verifier.rs index fb798be46..7db688a85 100644 --- a/ethcore/src/verification/noop_verifier.rs +++ b/ethcore/src/verification/noop_verifier.rs @@ -14,12 +14,15 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +//! No-op verifier. + use blockchain::BlockProvider; use engines::Engine; use error::Error; use header::Header; use super::Verifier; +/// A no-op verifier -- this will verify everything it's given immediately. #[allow(dead_code)] pub struct NoopVerifier; diff --git a/ethcore/src/verification/verification.rs b/ethcore/src/verification/verification.rs index bb9f042ae..47b2e16de 100644 --- a/ethcore/src/verification/verification.rs +++ b/ethcore/src/verification/verification.rs @@ -14,12 +14,12 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -/// Block and transaction verification functions -/// -/// Block verification is done in 3 steps -/// 1. Quick verification upon adding to the block queue -/// 2. Signatures verification done in the queue. -/// 3. Final verification against the blockchain done before enactment. +//! Block and transaction verification functions +//! +//! Block verification is done in 3 steps +//! 1. Quick verification upon adding to the block queue +//! 2. Signatures verification done in the queue. +//! 3. Final verification against the blockchain done before enactment. use util::*; use engines::Engine; diff --git a/ethcore/src/verification/verifier.rs b/ethcore/src/verification/verifier.rs index 7f57407f7..05d488f95 100644 --- a/ethcore/src/verification/verifier.rs +++ b/ethcore/src/verification/verifier.rs @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +//! A generic verifier trait. + use blockchain::BlockProvider; use engines::Engine; use error::Error; @@ -21,6 +23,8 @@ use header::Header; /// Should be used to verify blocks. pub trait Verifier: Send + Sync { + /// Verify a block relative to its parent and uncles. fn verify_block_family(&self, header: &Header, bytes: &[u8], engine: &Engine, bc: &BlockProvider) -> Result<(), Error>; + /// Do a final verification check for an enacted header vs its expected counterpart. fn verify_block_final(&self, expected: &Header, got: &Header) -> Result<(), Error>; } diff --git a/js/README.md b/js/README.md index 41252bc91..5ed26e0cf 100644 --- a/js/README.md +++ b/js/README.md @@ -7,6 +7,6 @@ JavaScript APIs and UIs for Parity. 0. Install [Node](https://nodejs.org/) if not already available 0. Change to the `js` directory inside `parity/` 0. Install the npm modules via `npm install` -0. Parity should be run with `parity --signer-no-validation [...options]` (where `options` can be `--chain testnet`) +0. Parity should be run with `parity --ui-no-validation [...options]` (where `options` can be `--chain testnet`) 0. Start the development environment via `npm start` 0. Connect to the [UI](http://localhost:3000) diff --git a/js/package.json b/js/package.json index ca633fd29..137cbd642 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.2.28", + "version": "0.2.37", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team ", @@ -100,6 +100,7 @@ "postcss-loader": "^0.8.1", "postcss-nested": "^1.0.0", "postcss-simple-vars": "^3.0.0", + "raw-loader": "^0.5.1", "react-addons-test-utils": "^15.3.0", "react-copy-to-clipboard": "^4.2.3", "react-hot-loader": "^1.3.0", @@ -118,6 +119,7 @@ "dependencies": { "bignumber.js": "^2.3.0", "blockies": "0.0.2", + "brace": "^0.9.0", "bytes": "^2.4.0", "chart.js": "^2.3.0", "es6-promise": "^3.2.1", @@ -138,9 +140,11 @@ "moment": "^2.14.1", "qs": "^6.3.0", "react": "^15.2.1", + "react-ace": "^4.0.0", "react-addons-css-transition-group": "^15.2.1", "react-chartjs-2": "^1.5.0", "react-dom": "^15.2.1", + "react-dropzone": "^3.7.3", "react-redux": "^4.4.5", "react-router": "^2.6.1", "react-router-redux": "^4.0.5", @@ -152,10 +156,13 @@ "redux-thunk": "^2.1.0", "rlp": "^2.0.0", "scryptsy": "^2.0.0", + "solc": "ngotchac/solc-js", "store": "^1.3.20", "utf8": "^2.1.1", + "valid-url": "^1.0.9", "validator": "^5.7.0", "web3": "^0.17.0-beta", - "whatwg-fetch": "^1.0.0" + "whatwg-fetch": "^1.0.0", + "worker-loader": "^0.7.1" } } diff --git a/js/src/api/api.spec.js b/js/src/api/api.spec.js index bbd140d4d..16831ea66 100644 --- a/js/src/api/api.spec.js +++ b/js/src/api/api.spec.js @@ -34,7 +34,7 @@ describe('api/Api', () => { }); describe('interface', () => { - const api = new Api(new Api.Transport.Http(TEST_HTTP_URL)); + const api = new Api(new Api.Transport.Http(TEST_HTTP_URL, -1)); Object.keys(ethereumRpc).sort().forEach((endpoint) => { describe(endpoint, () => { diff --git a/js/src/api/contract/contract.js b/js/src/api/contract/contract.js index 44e66c925..8d556a118 100644 --- a/js/src/api/contract/contract.js +++ b/js/src/api/contract/contract.js @@ -309,7 +309,6 @@ export default class Contract { try { subscriptions[idx].callback(null, this.parseEventLogs(logs)); } catch (error) { - this.unsubscribe(idx); console.error('_sendSubscriptionChanges', error); } }); diff --git a/js/src/api/contract/contract.spec.js b/js/src/api/contract/contract.spec.js index 96929cc11..3d57c2afa 100644 --- a/js/src/api/contract/contract.spec.js +++ b/js/src/api/contract/contract.spec.js @@ -25,7 +25,7 @@ import Api from '../api'; import Contract from './contract'; import { isInstanceOf, isFunction } from '../util/types'; -const transport = new Api.Transport.Http(TEST_HTTP_URL); +const transport = new Api.Transport.Http(TEST_HTTP_URL, -1); const eth = new Api(transport); describe('api/contract/Contract', () => { diff --git a/js/src/api/rpc/db/db.spec.js b/js/src/api/rpc/db/db.spec.js index 4379b51c4..4a11fc416 100644 --- a/js/src/api/rpc/db/db.spec.js +++ b/js/src/api/rpc/db/db.spec.js @@ -19,7 +19,7 @@ import { TEST_HTTP_URL, mockHttp } from '../../../../test/mockRpc'; import Http from '../../transport/http'; import Db from './db'; -const instance = new Db(new Http(TEST_HTTP_URL)); +const instance = new Db(new Http(TEST_HTTP_URL, -1)); describe('api/rpc/Db', () => { let scope; diff --git a/js/src/api/rpc/eth/eth.spec.js b/js/src/api/rpc/eth/eth.spec.js index 65377db50..85d22f4bd 100644 --- a/js/src/api/rpc/eth/eth.spec.js +++ b/js/src/api/rpc/eth/eth.spec.js @@ -20,7 +20,7 @@ import { isBigNumber } from '../../../../test/types'; import Http from '../../transport/http'; import Eth from './eth'; -const instance = new Eth(new Http(TEST_HTTP_URL)); +const instance = new Eth(new Http(TEST_HTTP_URL, -1)); describe('rpc/Eth', () => { const address = '0x63Cf90D3f0410092FC0fca41846f596223979195'; diff --git a/js/src/api/rpc/net/net.spec.js b/js/src/api/rpc/net/net.spec.js index 55029a29e..4903a0cde 100644 --- a/js/src/api/rpc/net/net.spec.js +++ b/js/src/api/rpc/net/net.spec.js @@ -20,7 +20,7 @@ import { isBigNumber } from '../../../../test/types'; import Http from '../../transport/http'; import Net from './net'; -const instance = new Net(new Http(TEST_HTTP_URL)); +const instance = new Net(new Http(TEST_HTTP_URL, -1)); describe('api/rpc/Net', () => { describe('peerCount', () => { diff --git a/js/src/api/rpc/parity/parity.spec.js b/js/src/api/rpc/parity/parity.spec.js index ea0dd8d8c..557314e5c 100644 --- a/js/src/api/rpc/parity/parity.spec.js +++ b/js/src/api/rpc/parity/parity.spec.js @@ -20,7 +20,7 @@ import { isBigNumber } from '../../../../test/types'; import Http from '../../transport/http'; import Parity from './parity'; -const instance = new Parity(new Http(TEST_HTTP_URL)); +const instance = new Parity(new Http(TEST_HTTP_URL, -1)); describe('api/rpc/parity', () => { describe('accountsInfo', () => { diff --git a/js/src/api/rpc/personal/personal.spec.js b/js/src/api/rpc/personal/personal.spec.js index a9bf4f644..70c8cf4c2 100644 --- a/js/src/api/rpc/personal/personal.spec.js +++ b/js/src/api/rpc/personal/personal.spec.js @@ -19,7 +19,7 @@ import { TEST_HTTP_URL, mockHttp } from '../../../../test/mockRpc'; import Http from '../../transport/http'; import Personal from './personal'; -const instance = new Personal(new Http(TEST_HTTP_URL)); +const instance = new Personal(new Http(TEST_HTTP_URL, -1)); describe('rpc/Personal', () => { const account = '0x63cf90d3f0410092fc0fca41846f596223979195'; diff --git a/js/src/api/rpc/trace/trace.spec.js b/js/src/api/rpc/trace/trace.spec.js index 4a38f7a3f..f36e5537c 100644 --- a/js/src/api/rpc/trace/trace.spec.js +++ b/js/src/api/rpc/trace/trace.spec.js @@ -19,7 +19,7 @@ import { TEST_HTTP_URL, mockHttp } from '../../../../test/mockRpc'; import Http from '../../transport/http'; import Trace from './trace'; -const instance = new Trace(new Http(TEST_HTTP_URL)); +const instance = new Trace(new Http(TEST_HTTP_URL, -1)); describe('api/rpc/Trace', () => { let scope; diff --git a/js/src/api/rpc/web3/web3.spec.js b/js/src/api/rpc/web3/web3.spec.js index eb4a59cd1..b933e805b 100644 --- a/js/src/api/rpc/web3/web3.spec.js +++ b/js/src/api/rpc/web3/web3.spec.js @@ -19,7 +19,7 @@ import { TEST_HTTP_URL, mockHttp } from '../../../../test/mockRpc'; import Http from '../../transport/http'; import Web3 from './web3'; -const instance = new Web3(new Http(TEST_HTTP_URL)); +const instance = new Web3(new Http(TEST_HTTP_URL, -1)); describe('api/rpc/Web3', () => { let scope; diff --git a/js/src/api/subscriptions/manager.js b/js/src/api/subscriptions/manager.js index 08f1a9e53..bc9632592 100644 --- a/js/src/api/subscriptions/manager.js +++ b/js/src/api/subscriptions/manager.js @@ -107,7 +107,6 @@ export default class Manager { callback(error, data); } catch (error) { console.error(`Unable to update callback for subscriptionId ${subscriptionId}`, error); - this.unsubscribe(subscriptionId); } } diff --git a/js/src/api/transport/http/http.js b/js/src/api/transport/http/http.js index 08d9422f8..8ea59f0fb 100644 --- a/js/src/api/transport/http/http.js +++ b/js/src/api/transport/http/http.js @@ -19,11 +19,14 @@ import JsonRpcBase from '../jsonRpcBase'; /* global fetch */ export default class Http extends JsonRpcBase { - constructor (url) { + constructor (url, connectTimeout = 1000) { super(); this._connected = true; this._url = url; + this._connectTimeout = connectTimeout; + + this._pollConnection(); } _encodeOptions (method, params) { @@ -77,4 +80,17 @@ export default class Http extends JsonRpcBase { return response.result; }); } + + _pollConnection = () => { + if (this._connectTimeout <= 0) { + return; + } + + const nextTimeout = () => setTimeout(this._pollConnection, this._connectTimeout); + + this + .execute('net_listening') + .then(nextTimeout) + .catch(nextTimeout); + } } diff --git a/js/src/api/transport/http/http.spec.js b/js/src/api/transport/http/http.spec.js index 94441bc51..718a7e66b 100644 --- a/js/src/api/transport/http/http.spec.js +++ b/js/src/api/transport/http/http.spec.js @@ -17,7 +17,7 @@ import { TEST_HTTP_URL, mockHttp } from '../../../../test/mockRpc'; import Http from './http'; -const transport = new Http(TEST_HTTP_URL); +const transport = new Http(TEST_HTTP_URL, -1); describe('api/transport/Http', () => { describe('instance', () => { diff --git a/js/src/contracts/snippets/human-standard-token.sol b/js/src/contracts/snippets/human-standard-token.sol new file mode 100644 index 000000000..db05bbc7d --- /dev/null +++ b/js/src/contracts/snippets/human-standard-token.sol @@ -0,0 +1,60 @@ +/* +This Token Contract implements the standard token functionality (https://github.com/ethereum/EIPs/issues/20) as well as the following OPTIONAL extras intended for use by humans. + +In other words. This is intended for deployment in something like a Token Factory or Mist wallet, and then used by humans. +Imagine coins, currencies, shares, voting weight, etc. +Machine-based, rapid creation of many tokens would not necessarily need these extra features or will be minted in other manners. + +1) Initial Finite Supply (upon creation one specifies how much is minted). +2) In the absence of a token registry: Optional Decimal, Symbol & Name. +3) Optional approveAndCall() functionality to notify a contract if an approval() has occurred. + +.*/ + +import "StandardToken.sol"; + +contract HumanStandardToken is StandardToken { + + function () { + //if ether is sent to this address, send it back. + throw; + } + + /* Public variables of the token */ + + /* + NOTE: + The following variables are OPTIONAL vanities. One does not have to include them. + They allow one to customise the token contract & in no way influences the core functionality. + Some wallets/interfaces might not even bother to look at this information. + */ + string public name; //fancy name: eg Simon Bucks + uint8 public decimals; //How many decimals to show. ie. There could 1000 base units with 3 decimals. Meaning 0.980 SBX = 980 base units. It's like comparing 1 wei to 1 ether. + string public symbol; //An identifier: eg SBX + string public version = 'H0.1'; //human 0.1 standard. Just an arbitrary versioning scheme. + + function HumanStandardToken( + uint256 _initialAmount, + string _tokenName, + uint8 _decimalUnits, + string _tokenSymbol + ) { + balances[msg.sender] = _initialAmount; // Give the creator all initial tokens + totalSupply = _initialAmount; // Update total supply + name = _tokenName; // Set the name for display purposes + decimals = _decimalUnits; // Amount of decimals for display purposes + symbol = _tokenSymbol; // Set the symbol for display purposes + } + + /* Approves and then calls the receiving contract */ + function approveAndCall(address _spender, uint256 _value, bytes _extraData) returns (bool success) { + allowed[msg.sender][_spender] = _value; + Approval(msg.sender, _spender, _value); + + //call the receiveApproval function on the contract you want to be notified. This crafts the function signature manually so one doesn't have to include a contract in here just for this. + //receiveApproval(address _from, uint256 _value, address _tokenContract, bytes _extraData) + //it is assumed that when does this that the call *should* succeed, otherwise one would use vanilla approve instead. + if(!_spender.call(bytes4(bytes32(sha3("receiveApproval(address,uint256,address,bytes)"))), msg.sender, _value, this, _extraData)) { throw; } + return true; + } +} diff --git a/js/src/contracts/snippets/standard-token.sol b/js/src/contracts/snippets/standard-token.sol new file mode 100644 index 000000000..3d91e5510 --- /dev/null +++ b/js/src/contracts/snippets/standard-token.sol @@ -0,0 +1,55 @@ +/* +You should inherit from StandardToken or, for a token like you would want to +deploy in something like Mist, see HumanStandardToken.sol. +(This implements ONLY the standard functions and NOTHING else. +If you deploy this, you won't have anything useful.) + +Implements ERC 20 Token standard: https://github.com/ethereum/EIPs/issues/20 +.*/ + +import "Token.sol"; + +contract StandardToken is Token { + + function transfer(address _to, uint256 _value) returns (bool success) { + //Default assumes totalSupply can't be over max (2^256 - 1). + //If your token leaves out totalSupply and can issue more tokens as time goes on, you need to check if it doesn't wrap. + //Replace the if with this one instead. + //if (balances[msg.sender] >= _value && balances[_to] + _value > balances[_to]) { + if (balances[msg.sender] >= _value && _value > 0) { + balances[msg.sender] -= _value; + balances[_to] += _value; + Transfer(msg.sender, _to, _value); + return true; + } else { return false; } + } + + function transferFrom(address _from, address _to, uint256 _value) returns (bool success) { + //same as above. Replace this line with the following if you want to protect against wrapping uints. + //if (balances[_from] >= _value && allowed[_from][msg.sender] >= _value && balances[_to] + _value > balances[_to]) { + if (balances[_from] >= _value && allowed[_from][msg.sender] >= _value && _value > 0) { + balances[_to] += _value; + balances[_from] -= _value; + allowed[_from][msg.sender] -= _value; + Transfer(_from, _to, _value); + return true; + } else { return false; } + } + + function balanceOf(address _owner) constant returns (uint256 balance) { + return balances[_owner]; + } + + function approve(address _spender, uint256 _value) returns (bool success) { + allowed[msg.sender][_spender] = _value; + Approval(msg.sender, _spender, _value); + return true; + } + + function allowance(address _owner, address _spender) constant returns (uint256 remaining) { + return allowed[_owner][_spender]; + } + + mapping (address => uint256) balances; + mapping (address => mapping (address => uint256)) allowed; +} diff --git a/js/src/contracts/snippets/token.sol b/js/src/contracts/snippets/token.sol new file mode 100644 index 000000000..d54c5c424 --- /dev/null +++ b/js/src/contracts/snippets/token.sol @@ -0,0 +1,47 @@ +// Abstract contract for the full ERC 20 Token standard +// https://github.com/ethereum/EIPs/issues/20 + +contract Token { + /* This is a slight change to the ERC20 base standard. + function totalSupply() constant returns (uint256 supply); + is replaced with: + uint256 public totalSupply; + This automatically creates a getter function for the totalSupply. + This is moved to the base contract since public getter functions are not + currently recognised as an implementation of the matching abstract + function by the compiler. + */ + /// total amount of tokens + uint256 public totalSupply; + + /// @param _owner The address from which the balance will be retrieved + /// @return The balance + function balanceOf(address _owner) constant returns (uint256 balance); + + /// @notice send `_value` token to `_to` from `msg.sender` + /// @param _to The address of the recipient + /// @param _value The amount of token to be transferred + /// @return Whether the transfer was successful or not + function transfer(address _to, uint256 _value) returns (bool success); + + /// @notice send `_value` token to `_to` from `_from` on the condition it is approved by `_from` + /// @param _from The address of the sender + /// @param _to The address of the recipient + /// @param _value The amount of token to be transferred + /// @return Whether the transfer was successful or not + function transferFrom(address _from, address _to, uint256 _value) returns (bool success); + + /// @notice `msg.sender` approves `_addr` to spend `_value` tokens + /// @param _spender The address of the account able to transfer the tokens + /// @param _value The amount of wei to be approved for transfer + /// @return Whether the approval was successful or not + function approve(address _spender, uint256 _value) returns (bool success); + + /// @param _owner The address of the account owning tokens + /// @param _spender The address of the account able to transfer the tokens + /// @return Amount of remaining tokens allowed to spent + function allowance(address _owner, address _spender) constant returns (uint256 remaining); + + event Transfer(address indexed _from, address indexed _to, uint256 _value); + event Approval(address indexed _owner, address indexed _spender, uint256 _value); +} diff --git a/js/src/dapps/githubhint/services.js b/js/src/dapps/githubhint/services.js index c9d260b73..d4f7fc6b2 100644 --- a/js/src/dapps/githubhint/services.js +++ b/js/src/dapps/githubhint/services.js @@ -28,26 +28,26 @@ export function attachInterface () { return Promise .all([ registry.getAddress.call({}, [api.util.sha3('githubhint'), 'A']), - api.eth.accounts(), api.parity.accounts() ]); }) - .then(([address, addresses, accountsInfo]) => { - accountsInfo = accountsInfo || {}; + .then(([address, accountsInfo]) => { console.log(`githubhint was found at ${address}`); const contract = api.newContract(abis.githubhint, address); - const accounts = addresses.reduce((obj, address) => { - const info = accountsInfo[address] || {}; + const accounts = Object + .keys(accountsInfo) + .filter((address) => accountsInfo[address].uuid) + .reduce((obj, address) => { + const account = accountsInfo[address]; - return Object.assign(obj, { - [address]: { - address, - name: info.name, - uuid: info.uuid - } - }); - }, {}); + return Object.assign(obj, { + [address]: { + address, + name: account.name + } + }); + }, {}); const fromAddress = Object.keys(accounts)[0]; return { diff --git a/js/src/dapps/registry/Application/application.css b/js/src/dapps/registry/Application/application.css index ebdb23baa..b46afbcf7 100644 --- a/js/src/dapps/registry/Application/application.css +++ b/js/src/dapps/registry/Application/application.css @@ -49,3 +49,15 @@ padding-bottom: 0 !important; } } + +.warning { + background: #f80; + bottom: 0; + color: #fff; + left: 0; + opacity: 1; + padding: 1.5em; + position: fixed; + right: 50%; + z-index: 100; +} diff --git a/js/src/dapps/registry/Application/application.js b/js/src/dapps/registry/Application/application.js index e763069a5..5102e5d57 100644 --- a/js/src/dapps/registry/Application/application.js +++ b/js/src/dapps/registry/Application/application.js @@ -53,6 +53,7 @@ export default class Application extends Component { }; render () { + const { api } = window.parity; const { actions, accounts, contacts, @@ -60,9 +61,11 @@ export default class Application extends Component { lookup, events } = this.props; + let warning = null; return (
+ { warning }

RΞgistry

@@ -70,13 +73,11 @@ export default class Application extends Component { { contract && fee ? (
- { this.renderActions() } - -

- The Registry is provided by the contract at { contract.address }. -

+
+ WARNING: The name registry is experimental. Please ensure that you understand the risks, benefits & consequences of registering a name before doing so. A non-refundable fee of { api.util.fromWei(fee).toFormat(3) }ETH is required for all registrations. +
) : ( diff --git a/js/src/dapps/registry/addresses/actions.js b/js/src/dapps/registry/addresses/actions.js index 2341d716c..666196e88 100644 --- a/js/src/dapps/registry/addresses/actions.js +++ b/js/src/dapps/registry/addresses/actions.js @@ -19,18 +19,16 @@ import { api } from '../parity'; export const set = (addresses) => ({ type: 'addresses set', addresses }); export const fetch = () => (dispatch) => { - return Promise - .all([ - api.eth.accounts(), - api.parity.accounts() - ]) - .then(([ accounts, data ]) => { - data = data || {}; - const addresses = Object.keys(data) - .filter((address) => data[address] && !data[address].meta.deleted) + return api.parity + .accounts() + .then((accountsInfo) => { + const addresses = Object + .keys(accountsInfo) + .filter((address) => accountsInfo[address] && !accountsInfo[address].meta.deleted) .map((address) => ({ - ...data[address], address, - isAccount: accounts.includes(address) + ...accountsInfo[address], + address, + isAccount: !!accountsInfo[address].uuid })); dispatch(set(addresses)); }) diff --git a/js/src/dapps/signaturereg/Import/import.js b/js/src/dapps/signaturereg/Import/import.js index dcf2b3f98..90edf9415 100644 --- a/js/src/dapps/signaturereg/Import/import.js +++ b/js/src/dapps/signaturereg/Import/import.js @@ -146,7 +146,7 @@ export default class Import extends Component { } sortFunctions = (a, b) => { - return a.name.localeCompare(b.name); + return (a.name || '').localeCompare(b.name || ''); } countFunctions () { diff --git a/js/src/dapps/signaturereg/services.js b/js/src/dapps/signaturereg/services.js index 54394c4b8..eab498fc4 100644 --- a/js/src/dapps/signaturereg/services.js +++ b/js/src/dapps/signaturereg/services.js @@ -49,26 +49,26 @@ export function attachInterface (callback) { return Promise .all([ registry.getAddress.call({}, [api.util.sha3('signaturereg'), 'A']), - api.eth.accounts(), api.parity.accounts() ]); }) - .then(([address, addresses, accountsInfo]) => { - accountsInfo = accountsInfo || {}; + .then(([address, accountsInfo]) => { console.log(`signaturereg was found at ${address}`); const contract = api.newContract(abis.signaturereg, address); - const accounts = addresses.reduce((obj, address) => { - const info = accountsInfo[address] || {}; + const accounts = Object + .keys(accountsInfo) + .filter((address) => accountsInfo[address].uuid) + .reduce((obj, address) => { + const info = accountsInfo[address] || {}; - return Object.assign(obj, { - [address]: { - address, - name: info.name || 'Unnamed', - uuid: info.uuid - } - }); - }, {}); + return Object.assign(obj, { + [address]: { + address, + name: info.name || 'Unnamed' + } + }); + }, {}); const fromAddress = Object.keys(accounts)[0]; return { diff --git a/js/src/dapps/tokenreg/Accounts/AccountSelector/account-selector.js b/js/src/dapps/tokenreg/Accounts/AccountSelector/account-selector.js index 4c8525d7e..4d29a6692 100644 --- a/js/src/dapps/tokenreg/Accounts/AccountSelector/account-selector.js +++ b/js/src/dapps/tokenreg/Accounts/AccountSelector/account-selector.js @@ -70,7 +70,8 @@ export default class AccountSelector extends Component { static propTypes = { list: PropTypes.array.isRequired, selected: PropTypes.object.isRequired, - handleSetSelected: PropTypes.func.isRequired + handleSetSelected: PropTypes.func.isRequired, + onAccountChange: PropTypes.func }; state = { @@ -85,7 +86,8 @@ export default class AccountSelector extends Component { nestedItems={ nestedAccounts } open={ this.state.open } onSelectAccount={ this.onToggleOpen } - autoGenerateNestedIndicator={ false } /> + autoGenerateNestedIndicator={ false } + nestedListStyle={ { maxHeight: '14em', overflow: 'auto' } } /> ); return ( @@ -110,6 +112,10 @@ export default class AccountSelector extends Component { onToggleOpen = () => { this.setState({ open: !this.state.open }); + + if (typeof this.props.onAccountChange === 'function') { + this.props.onAccountChange(); + } } onSelectAccount = (address) => { diff --git a/js/src/dapps/tokenreg/Accounts/actions.js b/js/src/dapps/tokenreg/Accounts/actions.js index 58a74dfd8..a310baf7d 100644 --- a/js/src/dapps/tokenreg/Accounts/actions.js +++ b/js/src/dapps/tokenreg/Accounts/actions.js @@ -35,16 +35,13 @@ export const setSelectedAccount = (address) => ({ }); export const loadAccounts = () => (dispatch) => { - Promise - .all([ - api.eth.accounts(), - api.parity.accounts() - ]) - .then(([ accounts, accountsInfo ]) => { - accountsInfo = accountsInfo || {}; - - const accountsList = accounts - .map(address => ({ + api.parity + .accounts() + .then((accountsInfo) => { + const accountsList = Object + .keys(accountsInfo) + .filter((address) => accountsInfo[address].uuid) + .map((address) => ({ ...accountsInfo[address], address })); diff --git a/js/src/dapps/tokenreg/Actions/Register/register.js b/js/src/dapps/tokenreg/Actions/Register/register.js index 5eb2d9e1b..8ae042494 100644 --- a/js/src/dapps/tokenreg/Actions/Register/register.js +++ b/js/src/dapps/tokenreg/Actions/Register/register.js @@ -81,6 +81,7 @@ export default class RegisterAction extends Component { className={ styles.dialog } onRequestClose={ this.onClose } actions={ this.renderActions() } + ref='dialog' autoScrollBodyContent > { this.renderContent() } @@ -149,7 +150,9 @@ export default class RegisterAction extends Component { renderForm () { return (
- + { this.renderInputs() }
); @@ -175,6 +178,11 @@ export default class RegisterAction extends Component { }); } + onAccountChange = () => { + const { dialog } = this.refs; + dialog.forceUpdate(); + } + onChange (fieldKey, valid, value) { const { fields } = this.state; const field = fields[fieldKey]; diff --git a/js/src/dapps/tokenreg/Application/application.css b/js/src/dapps/tokenreg/Application/application.css index 07bc74b40..033147ae3 100644 --- a/js/src/dapps/tokenreg/Application/application.css +++ b/js/src/dapps/tokenreg/Application/application.css @@ -20,3 +20,15 @@ flex-direction: column; align-items: center; } + +.warning { + background: #f80; + bottom: 0; + color: #fff; + left: 0; + opacity: 1; + padding: 1.5em; + position: fixed; + right: 50%; + z-index: 100; +} diff --git a/js/src/dapps/tokenreg/Application/application.js b/js/src/dapps/tokenreg/Application/application.js index e48922b05..6a94f5c9c 100644 --- a/js/src/dapps/tokenreg/Application/application.js +++ b/js/src/dapps/tokenreg/Application/application.js @@ -17,6 +17,8 @@ import React, { Component, PropTypes } from 'react'; import getMuiTheme from 'material-ui/styles/getMuiTheme'; +import { api } from '../parity'; + import Loading from '../Loading'; import Status from '../Status'; import Tokens from '../Tokens'; @@ -59,6 +61,9 @@ export default class Application extends Component { +
+ WARNING: The token registry is experimental. Please ensure that you understand the steps, risks, benefits & consequences of registering a token before doing so. A non-refundable fee of { api.util.fromWei(contract.fee).toFormat(3) }ETH is required for all registrations. +
); } diff --git a/js/src/dapps/tokenreg/Inputs/validation.js b/js/src/dapps/tokenreg/Inputs/validation.js index b2e0688a8..38eba5ef1 100644 --- a/js/src/dapps/tokenreg/Inputs/validation.js +++ b/js/src/dapps/tokenreg/Inputs/validation.js @@ -75,7 +75,7 @@ const validateTokenAddress = (address, contract, simple) => { return getTokenTotalSupply(address) .then(balance => { - if (balance === null) { + if (balance === null || balance.equals(0)) { return { error: ERRORS.invalidTokenAddress, valid: false diff --git a/js/src/dapps/tokenreg/Status/status.css b/js/src/dapps/tokenreg/Status/status.css index 27ef53607..7333194b7 100644 --- a/js/src/dapps/tokenreg/Status/status.css +++ b/js/src/dapps/tokenreg/Status/status.css @@ -31,6 +31,12 @@ .title { font-size: 3rem; font-weight: 300; - margin-top: 0; + margin: 0; text-transform: uppercase; } + +.byline { + font-size: 1.25em; + opacity: 0.75; + margin: 0 0 1.75em 0; +} diff --git a/js/src/dapps/tokenreg/Status/status.js b/js/src/dapps/tokenreg/Status/status.js index f8c7b347a..4ca47a6ea 100644 --- a/js/src/dapps/tokenreg/Status/status.js +++ b/js/src/dapps/tokenreg/Status/status.js @@ -29,17 +29,12 @@ export default class Status extends Component { }; render () { - const { address, fee } = this.props; + const { fee } = this.props; return (

Token Registry

- - - +

A global registry of all recognised tokens on the network

{ if (!token || !token.tla) { @@ -61,7 +61,8 @@ export default class Tokens extends Component { handleMetaLookup={ this.props.handleMetaLookup } handleAddMeta={ this.props.handleAddMeta } key={ index } - isTokenOwner={ isTokenOwner } /> + isTokenOwner={ isTokenOwner } + isContractOwner={ isOwner } /> ); }); } diff --git a/js/src/index.js b/js/src/index.js index 966e2708e..c0f4f94ad 100644 --- a/js/src/index.js +++ b/js/src/index.js @@ -31,7 +31,7 @@ import ContractInstances from './contracts'; import { initStore } from './redux'; import { ContextProvider, muiTheme } from './ui'; -import { Accounts, Account, Addresses, Address, Application, Contract, Contracts, Dapp, Dapps, Settings, SettingsBackground, SettingsParity, SettingsProxy, SettingsViews, Signer, Status } from './views'; +import { Accounts, Account, Addresses, Address, Application, Contract, Contracts, WriteContract, Dapp, Dapps, Settings, SettingsBackground, SettingsParity, SettingsProxy, SettingsViews, Signer, Status } from './views'; import { setApi } from './redux/providers/apiActions'; @@ -76,6 +76,7 @@ ReactDOM.render( + diff --git a/js/src/modals/DeployContract/DetailsStep/detailsStep.js b/js/src/modals/DeployContract/DetailsStep/detailsStep.js index e0f02bc70..854715396 100644 --- a/js/src/modals/DeployContract/DetailsStep/detailsStep.js +++ b/js/src/modals/DeployContract/DetailsStep/detailsStep.js @@ -25,7 +25,7 @@ import styles from '../deployContract.css'; export default class DetailsStep extends Component { static contextTypes = { api: PropTypes.object.isRequired - } + }; static propTypes = { accounts: PropTypes.object.isRequired, @@ -46,16 +46,33 @@ export default class DetailsStep extends Component { onFromAddressChange: PropTypes.func.isRequired, onDescriptionChange: PropTypes.func.isRequired, onNameChange: PropTypes.func.isRequired, - onParamsChange: PropTypes.func.isRequired - } + onParamsChange: PropTypes.func.isRequired, + readOnly: PropTypes.bool + }; + + static defaultProps = { + readOnly: false + }; state = { inputs: [] } + componentDidMount () { + const { abi, code } = this.props; + + if (abi) { + this.onAbiChange(abi); + } + + if (code) { + this.onCodeChange(code); + } + } + render () { const { accounts } = this.props; - const { abi, abiError, code, codeError, fromAddress, fromAddressError, name, nameError } = this.props; + const { abi, abiError, code, codeError, fromAddress, fromAddressError, name, nameError, readOnly } = this.props; return (
@@ -77,13 +94,15 @@ export default class DetailsStep extends Component { hint='the abi of the contract to deploy' error={ abiError } value={ abi } - onSubmit={ this.onAbiChange } /> + onSubmit={ this.onAbiChange } + readOnly={ readOnly } /> + onSubmit={ this.onCodeChange } + readOnly={ readOnly } /> { this.renderConstructorInputs() }
); diff --git a/js/src/modals/DeployContract/deployContract.js b/js/src/modals/DeployContract/deployContract.js index 588d16f6a..768723d1f 100644 --- a/js/src/modals/DeployContract/deployContract.js +++ b/js/src/modals/DeployContract/deployContract.js @@ -36,8 +36,17 @@ export default class DeployContract extends Component { static propTypes = { accounts: PropTypes.object.isRequired, - onClose: PropTypes.func.isRequired - } + onClose: PropTypes.func.isRequired, + abi: PropTypes.string, + code: PropTypes.string, + readOnly: PropTypes.bool, + source: PropTypes.string + }; + + static defaultProps = { + readOnly: false, + source: '' + }; state = { abi: '', @@ -57,6 +66,31 @@ export default class DeployContract extends Component { deployError: null } + componentWillMount () { + const { abi, code } = this.props; + + if (abi && code) { + this.setState({ abi, code }); + } + } + + componentWillReceiveProps (nextProps) { + const { abi, code } = nextProps; + const newState = {}; + + if (abi !== this.props.abi) { + newState.abi = abi; + } + + if (code !== this.props.code) { + newState.code = code; + } + + if (Object.keys(newState).length) { + this.setState(newState); + } + } + render () { const { step, deployError } = this.state; @@ -115,7 +149,7 @@ export default class DeployContract extends Component { } renderStep () { - const { accounts } = this.props; + const { accounts, readOnly } = this.props; const { address, deployError, step, deployState, txhash } = this.state; if (deployError) { @@ -129,6 +163,7 @@ export default class DeployContract extends Component { return ( { const { api, store } = this.context; + const { source } = this.props; const { abiParsed, code, description, name, params, fromAddress } = this.state; const options = { data: code, @@ -219,6 +255,7 @@ export default class DeployContract extends Component { contract: true, timestamp: Date.now(), deleted: false, + source, description }) ]) diff --git a/js/src/modals/LoadContract/index.js b/js/src/modals/LoadContract/index.js new file mode 100644 index 000000000..be5e62af5 --- /dev/null +++ b/js/src/modals/LoadContract/index.js @@ -0,0 +1,17 @@ +// 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 . + +export default from './loadContract'; diff --git a/js/src/modals/LoadContract/loadContract.css b/js/src/modals/LoadContract/loadContract.css new file mode 100644 index 000000000..f3144eeb8 --- /dev/null +++ b/js/src/modals/LoadContract/loadContract.css @@ -0,0 +1,52 @@ +/* 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 . +*/ + +.loadContainer { + display: flex; + flex-direction: row; + + > * { + flex: 50%; + width: 0; + } +} + +.editor { + display: flex; + flex-direction: column; + padding-left: 1em; + + p { + line-height: 48px; + height: 48px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + margin: 0; + font-size: 1.2em; + } +} + +.confirmRemoval { + text-align: center; + + .editor { + text-align: left; + margin-top: 0.5em; + } +} diff --git a/js/src/modals/LoadContract/loadContract.js b/js/src/modals/LoadContract/loadContract.js new file mode 100644 index 000000000..3de55561a --- /dev/null +++ b/js/src/modals/LoadContract/loadContract.js @@ -0,0 +1,284 @@ +// 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 . + +import React, { Component, PropTypes } from 'react'; + +import ContentClear from 'material-ui/svg-icons/content/clear'; +import CheckIcon from 'material-ui/svg-icons/navigation/check'; +import DeleteIcon from 'material-ui/svg-icons/action/delete'; + +import { List, ListItem, makeSelectable } from 'material-ui/List'; +import { Subheader, IconButton, Tabs, Tab } from 'material-ui'; +import moment from 'moment'; + +import { Button, Modal, Editor } from '../../ui'; + +import styles from './loadContract.css'; + +const SelectableList = makeSelectable(List); + +const SELECTED_STYLE = { + backgroundColor: 'rgba(255, 255, 255, 0.1)' +}; + +export default class LoadContract extends Component { + + static propTypes = { + onClose: PropTypes.func.isRequired, + onLoad: PropTypes.func.isRequired, + onDelete: PropTypes.func.isRequired, + contracts: PropTypes.object.isRequired, + snippets: PropTypes.object.isRequired + }; + + state = { + selected: -1, + deleteRequest: false, + deleteId: -1 + }; + + render () { + const { deleteRequest } = this.state; + + const title = deleteRequest + ? 'confirm removal' + : 'view contracts'; + + return ( + + { this.renderBody() } + + ); + } + + renderBody () { + if (this.state.deleteRequest) { + return this.renderConfirmRemoval(); + } + + const { contracts, snippets } = this.props; + + const contractsTab = Object.keys(contracts).length === 0 + ? null + : ( + + { this.renderEditor() } + + + Saved Contracts + { this.renderContracts(contracts) } + + + ); + + return ( +
+ + { contractsTab } + + + { this.renderEditor() } + + + Contract Snippets + { this.renderContracts(snippets, false) } + + + +
+ ); + } + + renderConfirmRemoval () { + const { deleteId } = this.state; + const { name, timestamp, sourcecode } = this.props.contracts[deleteId]; + + return ( +
+

+ Are you sure you want to remove the following + contract from your saved contracts? +

+ + +
+ +
+
+ ); + } + + renderEditor () { + const { contracts, snippets } = this.props; + const { selected } = this.state; + + const mergedContracts = Object.assign({}, contracts, snippets); + + if (selected === -1 || !mergedContracts[selected]) { + return null; + } + + const { sourcecode, name } = mergedContracts[selected]; + + return ( +
+

{ name }

+ +
+ ); + } + + renderContracts (contracts, removable = true) { + const { selected } = this.state; + + return Object + .values(contracts) + .map((contract) => { + const { id, name, timestamp, description } = contract; + const onDelete = () => this.onDeleteRequest(id); + + const secondaryText = description || `Saved ${moment(timestamp).fromNow()}`; + const remove = removable + ? ( + + + + ) + : null; + + return ( + + ); + }); + } + + renderDialogActions () { + const { deleteRequest } = this.state; + + if (deleteRequest) { + return [ +
+ ); + } + + renderModal () { + const { title, renderValidation } = this.props; + const { show, step } = this.state; + + if (!show) { + return null; + } + + const hasSteps = typeof renderValidation === 'function'; + + const steps = hasSteps ? [ 'select a file', 'validate' ] : null; + + return ( + + { this.renderBody() } + + ); + } + + renderActions () { + const { validate } = this.state; + + const cancelBtn = ( +
); } + renderDetails (contract) { + const { showDetailsDialog } = this.state; + + if (!showDetailsDialog) { + return null; + } + + const cancelBtn = ( +