Light friendly dapps (#5634)

* move native_contracts ABIs to JSON files, add urlhint

* port hash-fetch to futures, fix tests

* fix dapps compilation, defer async port to later

* activate dapps server in the light client

* better formatting
This commit is contained in:
Robert Habermeier
2017-05-18 12:44:09 +02:00
committed by Gav Wood
parent 95d9706fe1
commit b1eab698d2
23 changed files with 347 additions and 240 deletions

View File

@@ -7,7 +7,7 @@ version = "1.7.0"
authors = ["Parity Technologies <admin@parity.io>"]
[dependencies]
ethabi = "1.0.0"
ethabi = "1.0.4"
futures = "0.1"
log = "0.3"
mime = "0.2"
@@ -17,3 +17,4 @@ rustc-serialize = "0.3"
fetch = { path = "../util/fetch" }
ethcore-util = { path = "../util" }
parity-reactor = { path = "../util/reactor" }
native-contracts = { path = "../ethcore/native_contracts" }

View File

@@ -87,6 +87,28 @@ impl From<io::Error> for Error {
}
}
fn validate_hash(path: PathBuf, hash: H256, result: Result<Response, FetchError>) -> Result<PathBuf, Error> {
let response = result?;
if !response.is_success() {
return Err(Error::InvalidStatus);
}
// Read the response
let mut reader = io::BufReader::new(response);
let mut writer = io::BufWriter::new(fs::File::create(&path)?);
io::copy(&mut reader, &mut writer)?;
writer.flush()?;
// And validate the hash
let mut file_reader = io::BufReader::new(fs::File::open(&path)?);
let content_hash = sha3(&mut file_reader)?;
if content_hash != hash {
Err(Error::HashMismatch{ got: content_hash, expected: hash })
} else {
Ok(path)
}
}
/// Default Hash-fetching client using on-chain contract to resolve hashes to URLs.
pub struct Client<F: Fetch + 'static = FetchClient> {
contract: URLHintContract,
@@ -103,7 +125,6 @@ impl Client {
}
impl<F: Fetch + 'static> Client<F> {
/// Creates new instance of the `Client` given on-chain contract client, fetch service and task runner.
pub fn with_fetch(contract: Arc<ContractClient>, fetch: F, remote: Remote) -> Self {
Client {
@@ -119,44 +140,22 @@ impl<F: Fetch + 'static> HashFetch for Client<F> {
fn fetch(&self, hash: H256, on_done: Box<Fn(Result<PathBuf, Error>) + Send>) {
debug!(target: "fetch", "Fetching: {:?}", hash);
let url = self.contract.resolve(hash.to_vec()).map(|content| match content {
URLHintResult::Dapp(dapp) => {
dapp.url()
},
URLHintResult::Content(content) => {
content.url
},
}).ok_or_else(|| Error::NoResolution);
debug!(target: "fetch", "Resolved {:?} to {:?}. Fetching...", hash, url);
match url {
Err(err) => on_done(Err(err)),
Ok(url) => {
let random_path = self.random_path.clone();
let future = self.fetch.fetch(&url).then(move |result| {
fn validate_hash(path: PathBuf, hash: H256, result: Result<Response, FetchError>) -> Result<PathBuf, Error> {
let response = result?;
if !response.is_success() {
return Err(Error::InvalidStatus);
}
// Read the response
let mut reader = io::BufReader::new(response);
let mut writer = io::BufWriter::new(fs::File::create(&path)?);
io::copy(&mut reader, &mut writer)?;
writer.flush()?;
// And validate the hash
let mut file_reader = io::BufReader::new(fs::File::open(&path)?);
let content_hash = sha3(&mut file_reader)?;
if content_hash != hash {
Err(Error::HashMismatch{ got: content_hash, expected: hash })
} else {
Ok(path)
}
}
let random_path = self.random_path.clone();
let remote_fetch = self.fetch.clone();
let future = self.contract.resolve(hash.to_vec())
.map_err(|e| { warn!("Error resolving URL: {}", e); Error::NoResolution })
.and_then(|maybe_url| maybe_url.ok_or(Error::NoResolution))
.map(|content| match content {
URLHintResult::Dapp(dapp) => {
dapp.url()
},
URLHintResult::Content(content) => {
content.url
},
})
.and_then(move |url| {
debug!(target: "fetch", "Resolved {:?} to {:?}. Fetching...", hash, url);
let future = remote_fetch.fetch(&url).then(move |result| {
debug!(target: "fetch", "Content fetched, validating hash ({:?})", hash);
let path = random_path();
let res = validate_hash(path.clone(), hash, result);
@@ -165,13 +164,13 @@ impl<F: Fetch + 'static> HashFetch for Client<F> {
// Remove temporary file in case of error
let _ = fs::remove_file(&path);
}
on_done(res);
Ok(()) as Result<(), ()>
res
});
self.remote.spawn(self.fetch.process(future));
},
}
remote_fetch.process(future)
})
.then(move |res| { on_done(res); Ok(()) as Result<(), ()> });
self.remote.spawn(future);
}
}
@@ -225,7 +224,7 @@ mod tests {
let mut registrar = FakeRegistrar::new();
registrar.responses = Mutex::new(vec![
Ok(format!("000000000000000000000000{}", URLHINT).from_hex().unwrap()),
Ok("00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000deadcafebeefbeefcafedeaddeedfeedffffffff000000000000000000000000000000000000000000000000000000000000003d68747470733a2f2f657468636f72652e696f2f6173736574732f696d616765732f657468636f72652d626c61636b2d686f72697a6f6e74616c2e706e67000000".from_hex().unwrap()),
Ok("00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000deadcafebeefbeefcafedeaddeedfeedffffffff000000000000000000000000000000000000000000000000000000000000003c68747470733a2f2f7061726974792e696f2f6173736574732f696d616765732f657468636f72652d626c61636b2d686f72697a6f6e74616c2e706e6700000000".from_hex().unwrap()),
]);
registrar
}

View File

@@ -25,12 +25,14 @@ extern crate mime;
extern crate ethabi;
extern crate ethcore_util as util;
pub extern crate fetch;
extern crate futures;
extern crate mime_guess;
extern crate native_contracts;
extern crate parity_reactor;
extern crate rand;
extern crate rustc_serialize;
extern crate parity_reactor;
pub extern crate fetch;
mod client;

View File

@@ -16,13 +16,13 @@
//! URLHint Contract
use std::fmt;
use std::sync::Arc;
use rustc_serialize::hex::ToHex;
use mime::Mime;
use mime_guess;
use ethabi::{Interface, Contract, Token};
use futures::{future, BoxFuture, Future};
use native_contracts::{Registry, Urlhint};
use util::{Address, Bytes, Hashable};
const COMMIT_LEN: usize = 20;
@@ -33,7 +33,7 @@ pub trait ContractClient: Send + Sync {
/// Get registrar address
fn registrar(&self) -> Result<Address, String>;
/// Call Contract
fn call(&self, address: Address, data: Bytes) -> Result<Bytes, String>;
fn call(&self, address: Address, data: Bytes) -> BoxFuture<Bytes, String>;
}
/// Github-hosted dapp.
@@ -44,7 +44,7 @@ pub struct GithubApp {
/// Github Repository
pub repo: String,
/// Commit on Github
pub commit: [u8;COMMIT_LEN],
pub commit: [u8; COMMIT_LEN],
/// Dapp owner address
pub owner: Address,
}
@@ -94,146 +94,91 @@ pub enum URLHintResult {
/// URLHint Contract interface
pub trait URLHint: Send + Sync {
/// Resolves given id to registrar entry.
fn resolve(&self, id: Bytes) -> Option<URLHintResult>;
fn resolve(&self, id: Bytes) -> BoxFuture<Option<URLHintResult>, String>;
}
/// `URLHintContract` API
#[derive(Clone)]
pub struct URLHintContract {
urlhint: Contract,
registrar: Contract,
urlhint: Arc<Urlhint>,
registrar: Registry,
client: Arc<ContractClient>,
}
impl URLHintContract {
/// Creates new `URLHintContract`
pub fn new(client: Arc<ContractClient>) -> Self {
let urlhint = Interface::load(include_bytes!("../res/urlhint.json")).expect("urlhint.json is valid ABI");
let registrar = Interface::load(include_bytes!("../res/registrar.json")).expect("registrar.json is valid ABI");
URLHintContract {
urlhint: Contract::new(urlhint),
registrar: Contract::new(registrar),
urlhint: Arc::new(Urlhint::new(Default::default())),
registrar: Registry::new(Default::default()),
client: client,
}
}
fn urlhint_address(&self) -> Option<Address> {
let res = || {
let get_address = self.registrar.function("getAddress".into()).map_err(as_string)?;
let params = get_address.encode_call(
vec![Token::FixedBytes((*"githubhint".sha3()).to_vec()), Token::String("A".into())]
).map_err(as_string)?;
let output = self.client.call(self.client.registrar()?, params)?;
let result = get_address.decode_output(output).map_err(as_string)?;
match result.get(0) {
Some(&Token::Address(address)) if address != *Address::default() => Ok(address.into()),
Some(&Token::Address(_)) => Err(format!("Contract not found.")),
e => Err(format!("Invalid result: {:?}", e)),
}
};
match res() {
Ok(res) => Some(res),
Err(e) => {
warn!(target: "dapps", "Error while calling registrar: {:?}", e);
None
}
}
}
fn encode_urlhint_call(&self, id: Bytes) -> Option<Bytes> {
let call = self.urlhint
.function("entries".into())
.and_then(|f| f.encode_call(vec![Token::FixedBytes(id)]));
match call {
Ok(res) => {
Some(res)
},
Err(e) => {
warn!(target: "dapps", "Error while encoding urlhint call: {:?}", e);
None
}
}
}
fn decode_urlhint_output(&self, output: Bytes) -> Option<URLHintResult> {
trace!(target: "dapps", "Output: {:?}", output.to_hex());
let output = self.urlhint
.function("entries".into())
.and_then(|f| f.decode_output(output));
if let Ok(vec) = output {
if vec.len() != 3 {
warn!(target: "dapps", "Invalid contract output: {:?}", vec);
return None;
}
let mut it = vec.into_iter();
let account_slash_repo = it.next().expect("element 0 of 3-len vector known to exist; qed");
let commit = it.next().expect("element 1 of 3-len vector known to exist; qed");
let owner = it.next().expect("element 2 of 3-len vector known to exist qed");
match (account_slash_repo, commit, owner) {
(Token::String(account_slash_repo), Token::FixedBytes(commit), Token::Address(owner)) => {
let owner = owner.into();
if owner == Address::default() {
return None;
}
let commit = GithubApp::commit(&commit);
if commit == Some(Default::default()) {
let mime = guess_mime_type(&account_slash_repo).unwrap_or(mime!(Application/_));
return Some(URLHintResult::Content(Content {
url: account_slash_repo,
mime: mime,
owner: owner,
}));
}
let (account, repo) = {
let mut it = account_slash_repo.split('/');
match (it.next(), it.next()) {
(Some(account), Some(repo)) => (account.into(), repo.into()),
_ => return None,
}
};
commit.map(|commit| URLHintResult::Dapp(GithubApp {
account: account,
repo: repo,
commit: commit,
owner: owner,
}))
},
e => {
warn!(target: "dapps", "Invalid contract output parameters: {:?}", e);
None
},
}
} else {
warn!(target: "dapps", "Invalid contract output: {:?}", output);
None
}
}
}
fn decode_urlhint_output(output: (String, ::util::H160, Address)) -> Option<URLHintResult> {
let (account_slash_repo, commit, owner) = output;
if owner == Address::default() {
return None;
}
let commit = GithubApp::commit(&commit);
if commit == Some(Default::default()) {
let mime = guess_mime_type(&account_slash_repo).unwrap_or(mime!(Application/_));
return Some(URLHintResult::Content(Content {
url: account_slash_repo,
mime: mime,
owner: owner,
}));
}
let (account, repo) = {
let mut it = account_slash_repo.split('/');
match (it.next(), it.next()) {
(Some(account), Some(repo)) => (account.into(), repo.into()),
_ => return None,
}
};
commit.map(|commit| URLHintResult::Dapp(GithubApp {
account: account,
repo: repo,
commit: commit,
owner: owner,
}))
}
impl URLHint for URLHintContract {
fn resolve(&self, id: Bytes) -> Option<URLHintResult> {
self.urlhint_address().and_then(|address| {
// Prepare contract call
self.encode_urlhint_call(id)
.and_then(|data| {
let call = self.client.call(address, data);
if let Err(ref e) = call {
warn!(target: "dapps", "Error while calling urlhint: {:?}", e);
fn resolve(&self, id: Bytes) -> BoxFuture<Option<URLHintResult>, String> {
use futures::future::Either;
let do_call = |_, data| {
let addr = match self.client.registrar() {
Ok(addr) => addr,
Err(e) => return future::err(e).boxed(),
};
self.client.call(addr, data)
};
let urlhint = self.urlhint.clone();
let client = self.client.clone();
self.registrar.get_address(do_call, "githubhint".sha3(), "A".into())
.map(|addr| if addr == Address::default() { None } else { Some(addr) })
.and_then(move |address| {
let mut fixed_id = [0; 32];
let len = ::std::cmp::min(32, id.len());
fixed_id[..len].copy_from_slice(&id[..len]);
match address {
None => Either::A(future::ok(None)),
Some(address) => {
let do_call = move |_, data| client.call(address, data);
Either::B(urlhint.entries(do_call, ::util::H256(fixed_id)).map(decode_urlhint_output))
}
call.ok()
})
.and_then(|output| self.decode_urlhint_output(output))
})
}
}).boxed()
}
}
@@ -260,16 +205,14 @@ fn guess_mime_type(url: &str) -> Option<Mime> {
})
}
fn as_string<T: fmt::Debug>(e: T) -> String {
format!("{:?}", e)
}
#[cfg(test)]
pub mod tests {
use std::sync::Arc;
use std::str::FromStr;
use rustc_serialize::hex::FromHex;
use futures::{BoxFuture, Future, IntoFuture};
use super::*;
use super::guess_mime_type;
use util::{Bytes, Address, Mutex, ToPretty};
@@ -297,14 +240,14 @@ pub mod tests {
}
impl ContractClient for FakeRegistrar {
fn registrar(&self) -> Result<Address, String> {
Ok(REGISTRAR.parse().unwrap())
}
fn call(&self, address: Address, data: Bytes) -> Result<Bytes, String> {
fn call(&self, address: Address, data: Bytes) -> BoxFuture<Bytes, String> {
self.calls.lock().push((address.to_hex(), data.to_hex()));
self.responses.lock().remove(0)
let res = self.responses.lock().remove(0);
res.into_future().boxed()
}
}
@@ -312,11 +255,19 @@ pub mod tests {
fn should_call_registrar_and_urlhint_contracts() {
// given
let registrar = FakeRegistrar::new();
let resolve_result = {
use ethabi::{Encoder, Token};
Encoder::encode(vec![Token::String(String::new()), Token::FixedBytes(vec![0; 20]), Token::Address([0; 20])])
};
registrar.responses.lock()[1] = Ok(resolve_result);
let calls = registrar.calls.clone();
let urlhint = URLHintContract::new(Arc::new(registrar));
// when
let res = urlhint.resolve("test".bytes().collect());
let res = urlhint.resolve("test".bytes().collect()).wait().unwrap();
let calls = calls.lock();
let call0 = calls.get(0).expect("Registrar resolve called");
let call1 = calls.get(1).expect("URLHint Resolve called");
@@ -344,7 +295,7 @@ pub mod tests {
let urlhint = URLHintContract::new(Arc::new(registrar));
// when
let res = urlhint.resolve("test".bytes().collect());
let res = urlhint.resolve("test".bytes().collect()).wait().unwrap();
// then
assert_eq!(res, Some(URLHintResult::Dapp(GithubApp {
@@ -361,12 +312,12 @@ pub mod tests {
let mut registrar = FakeRegistrar::new();
registrar.responses = Mutex::new(vec![
Ok(format!("000000000000000000000000{}", URLHINT).from_hex().unwrap()),
Ok("00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000deadcafebeefbeefcafedeaddeedfeedffffffff000000000000000000000000000000000000000000000000000000000000003d68747470733a2f2f657468636f72652e696f2f6173736574732f696d616765732f657468636f72652d626c61636b2d686f72697a6f6e74616c2e706e67000000".from_hex().unwrap()),
Ok("00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000deadcafebeefbeefcafedeaddeedfeedffffffff000000000000000000000000000000000000000000000000000000000000003c68747470733a2f2f7061726974792e696f2f6173736574732f696d616765732f657468636f72652d626c61636b2d686f72697a6f6e74616c2e706e6700000000".from_hex().unwrap()),
]);
let urlhint = URLHintContract::new(Arc::new(registrar));
// when
let res = urlhint.resolve("test".bytes().collect());
let res = urlhint.resolve("test".bytes().collect()).wait().unwrap();
// then
assert_eq!(res, Some(URLHintResult::Content(Content {