Merge branch 'master' into lightsync

This commit is contained in:
Robert Habermeier 2016-12-15 23:55:04 +01:00
commit a1f32de2d9
186 changed files with 6978 additions and 1910 deletions

View File

@ -231,10 +231,10 @@ linux-armv6:
stage: build stage: build
image: ethcore/rust-armv6:latest image: ethcore/rust-armv6:latest
only: only:
# - beta - beta
# - tags # - tags
# - stable # - stable
- triggers # - triggers
script: script:
- export CC=arm-linux-gnueabi-gcc - export CC=arm-linux-gnueabi-gcc
- export CXX=arm-linux-gnueabi-g++ - export CXX=arm-linux-gnueabi-g++
@ -312,8 +312,8 @@ darwin:
- stable - stable
- triggers - triggers
script: script:
- cargo build -j 8 --release -p ethstore #$CARGOFLAGS
- cargo build -j 8 --release #$CARGOFLAGS - cargo build -j 8 --release #$CARGOFLAGS
- cargo build -j 8 --release -p ethstore #$CARGOFLAGS
- rm -rf parity.md5 - rm -rf parity.md5
- md5sum target/release/parity > parity.md5 - md5sum target/release/parity > parity.md5
- packagesbuild -v mac/Parity.pkgproj - packagesbuild -v mac/Parity.pkgproj
@ -350,7 +350,7 @@ windows:
- set RUST_BACKTRACE=1 - set RUST_BACKTRACE=1
- set RUSTFLAGS=%RUSTFLAGS% - set RUSTFLAGS=%RUSTFLAGS%
- rustup default stable-x86_64-pc-windows-msvc - rustup default stable-x86_64-pc-windows-msvc
- cargo build -j 8 --release #%CARGOFLAGS% - cargo build --release #%CARGOFLAGS%
- curl -sL --url "https://github.com/ethcore/win-build/raw/master/SimpleFC.dll" -o nsis\SimpleFC.dll - curl -sL --url "https://github.com/ethcore/win-build/raw/master/SimpleFC.dll" -o nsis\SimpleFC.dll
- curl -sL --url "https://github.com/ethcore/win-build/raw/master/vc_redist.x64.exe" -o nsis\vc_redist.x64.exe - curl -sL --url "https://github.com/ethcore/win-build/raw/master/vc_redist.x64.exe" -o nsis\vc_redist.x64.exe
- signtool sign /f %keyfile% /p %certpass% target\release\parity.exe - signtool sign /f %keyfile% /p %certpass% target\release\parity.exe
@ -408,7 +408,7 @@ test-darwin:
test-windows: test-windows:
stage: test stage: test
only: only:
- triggers # - triggers
before_script: before_script:
- git submodule update --init --recursive - git submodule update --init --recursive
script: script:

197
Cargo.lock generated
View File

@ -3,6 +3,7 @@ name = "parity"
version = "1.5.0" version = "1.5.0"
dependencies = [ dependencies = [
"ansi_term 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "ansi_term 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
"app_dirs 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"clippy 0.0.103 (registry+https://github.com/rust-lang/crates.io-index)", "clippy 0.0.103 (registry+https://github.com/rust-lang/crates.io-index)",
"ctrlc 1.1.1 (git+https://github.com/ethcore/rust-ctrlc.git)", "ctrlc 1.1.1 (git+https://github.com/ethcore/rust-ctrlc.git)",
"daemonize 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "daemonize 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
@ -32,9 +33,11 @@ dependencies = [
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"num_cpus 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", "num_cpus 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"number_prefix 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", "number_prefix 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
"parity-rpc-client 1.4.0",
"regex 0.1.68 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.1.68 (registry+https://github.com/rust-lang/crates.io-index)",
"rlp 0.1.0", "rlp 0.1.0",
"rpassword 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "rpassword 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"rpc-cli 1.4.0",
"rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
"semver 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "semver 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
@ -42,7 +45,7 @@ dependencies = [
"serde_json 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
"toml 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
@ -58,6 +61,17 @@ name = "ansi_term"
version = "0.7.2" version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "app_dirs"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"ole32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"shell32-sys 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"xdg 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.3.16" version = "0.3.16"
@ -196,7 +210,7 @@ source = "git+https://github.com/ethcore/rust-ctrlc.git#f4927770f89eca80ec250911
dependencies = [ dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
@ -277,7 +291,7 @@ name = "ethash"
version = "1.5.0" version = "1.5.0"
dependencies = [ dependencies = [
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"primal 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "primal 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"sha3 0.1.0", "sha3 0.1.0",
] ]
@ -395,7 +409,7 @@ dependencies = [
"crossbeam 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)", "crossbeam 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"mio 0.6.1 (git+https://github.com/ethcore/mio)", "mio 0.6.1 (git+https://github.com/ethcore/mio)",
"parking_lot 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"slab 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "slab 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
@ -501,7 +515,7 @@ dependencies = [
"libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"mio 0.6.1 (git+https://github.com/ethcore/mio)", "mio 0.6.1 (git+https://github.com/ethcore/mio)",
"parking_lot 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
"rlp 0.1.0", "rlp 0.1.0",
"rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",
@ -531,6 +545,7 @@ dependencies = [
"jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git)", "jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-http-server 6.1.1 (git+https://github.com/ethcore/jsonrpc.git)", "jsonrpc-http-server 6.1.1 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-ipc-server 0.2.4 (git+https://github.com/ethcore/jsonrpc.git)", "jsonrpc-ipc-server 0.2.4 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-macros 0.1.0 (git+https://github.com/ethcore/jsonrpc.git)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"rlp 0.1.0", "rlp 0.1.0",
"rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
@ -597,7 +612,7 @@ dependencies = [
"libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"lru-cache 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "lru-cache 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 0.1.68 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.1.68 (registry+https://github.com/rust-lang/crates.io-index)",
"rlp 0.1.0", "rlp 0.1.0",
@ -659,7 +674,7 @@ dependencies = [
"itertools 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)", "itertools 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
"rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
@ -688,7 +703,7 @@ dependencies = [
"ethkey 0.2.0", "ethkey 0.2.0",
"heapsize 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "heapsize 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
"rlp 0.1.0", "rlp 0.1.0",
"semver 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "semver 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
@ -729,6 +744,14 @@ dependencies = [
"miniz-sys 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "miniz-sys 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "futures"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "gcc" name = "gcc"
version = "0.3.35" version = "0.3.35"
@ -845,7 +868,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
@ -860,8 +883,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "jsonrpc-core" name = "jsonrpc-core"
version = "4.0.0" version = "3.0.2"
source = "git+https://github.com/ethcore/jsonrpc.git#1500da1b9613a0a17fc0109d825f3ccc60199a53" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
@ -870,10 +893,22 @@ dependencies = [
"serde_json 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "jsonrpc-core"
version = "4.0.0"
source = "git+https://github.com/ethcore/jsonrpc.git#33262d626a294a00c20435dec331058ba65e224a"
dependencies = [
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_codegen 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "jsonrpc-http-server" name = "jsonrpc-http-server"
version = "6.1.1" version = "6.1.1"
source = "git+https://github.com/ethcore/jsonrpc.git#1500da1b9613a0a17fc0109d825f3ccc60199a53" source = "git+https://github.com/ethcore/jsonrpc.git#33262d626a294a00c20435dec331058ba65e224a"
dependencies = [ dependencies = [
"hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)", "hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)",
"jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git)", "jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
@ -884,7 +919,7 @@ dependencies = [
[[package]] [[package]]
name = "jsonrpc-ipc-server" name = "jsonrpc-ipc-server"
version = "0.2.4" version = "0.2.4"
source = "git+https://github.com/ethcore/jsonrpc.git#1500da1b9613a0a17fc0109d825f3ccc60199a53" source = "git+https://github.com/ethcore/jsonrpc.git#33262d626a294a00c20435dec331058ba65e224a"
dependencies = [ dependencies = [
"bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
@ -896,10 +931,19 @@ dependencies = [
"slab 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "slab 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "jsonrpc-macros"
version = "0.1.0"
source = "git+https://github.com/ethcore/jsonrpc.git#33262d626a294a00c20435dec331058ba65e224a"
dependencies = [
"jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"serde 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "jsonrpc-tcp-server" name = "jsonrpc-tcp-server"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/ethcore/jsonrpc.git#1500da1b9613a0a17fc0109d825f3ccc60199a53" source = "git+https://github.com/ethcore/jsonrpc.git#33262d626a294a00c20435dec331058ba65e224a"
dependencies = [ dependencies = [
"bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
@ -916,7 +960,7 @@ name = "kernel32-sys"
version = "0.2.2" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
@ -1017,7 +1061,7 @@ dependencies = [
"nix 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "nix 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"slab 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "slab 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
@ -1033,7 +1077,7 @@ dependencies = [
"nix 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "nix 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"slab 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "slab 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
@ -1048,7 +1092,7 @@ dependencies = [
"net2 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", "net2 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
"nix 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "nix 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"slab 0.2.0 (git+https://github.com/carllerche/slab?rev=5476efcafb)", "slab 0.2.0 (git+https://github.com/carllerche/slab?rev=5476efcafb)",
"winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
@ -1064,7 +1108,7 @@ dependencies = [
"net2 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", "net2 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
"nix 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "nix 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
@ -1074,7 +1118,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"net2 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", "net2 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
@ -1085,7 +1129,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
@ -1114,7 +1158,7 @@ dependencies = [
"cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
@ -1253,6 +1297,15 @@ name = "odds"
version = "0.2.12" version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "ole32-sys"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "owning_ref" name = "owning_ref"
version = "0.2.2" version = "0.2.2"
@ -1272,6 +1325,26 @@ dependencies = [
"syntex_syntax 0.33.0 (registry+https://github.com/rust-lang/crates.io-index)", "syntex_syntax 0.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "parity-rpc-client"
version = "1.4.0"
dependencies = [
"ethcore-rpc 1.5.0",
"ethcore-signer 1.5.0",
"ethcore-util 1.5.0",
"futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonrpc-core 3.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
"tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"ws 0.5.3 (git+https://github.com/ethcore/ws-rs.git?branch=mio-upstream-stable)",
]
[[package]] [[package]]
name = "parity-ui" name = "parity-ui"
version = "1.5.0" version = "1.5.0"
@ -1291,7 +1364,7 @@ dependencies = [
[[package]] [[package]]
name = "parity-ui-precompiled" name = "parity-ui-precompiled"
version = "1.4.0" version = "1.4.0"
source = "git+https://github.com/ethcore/js-precompiled.git#eb9d978ed5ad1c514b37e89c716f80b3c8d613b5" source = "git+https://github.com/ethcore/js-precompiled.git#2cdda91549dfeebd94775b348a443f8ee5446e9f"
dependencies = [ dependencies = [
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
@ -1304,12 +1377,12 @@ dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
"smallvec 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", "smallvec 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.3.5" version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"owning_ref 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "owning_ref 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1325,7 +1398,7 @@ dependencies = [
"libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
"smallvec 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", "smallvec 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
@ -1548,7 +1621,30 @@ dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
"termios 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "termios 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rpassword"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
"termios 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rpc-cli"
version = "1.4.0"
dependencies = [
"ethcore-bigint 0.1.2",
"ethcore-rpc 1.5.0",
"ethcore-util 1.5.0",
"futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"parity-rpc-client 1.4.0",
"rpassword 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
@ -1652,6 +1748,15 @@ dependencies = [
"gcc 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", "gcc 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "shell32-sys"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "siphasher" name = "siphasher"
version = "0.1.1" version = "0.1.1"
@ -1786,13 +1891,21 @@ name = "target_info"
version = "0.1.0" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "tempdir"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "term" name = "term"
version = "0.2.14" version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
@ -1801,7 +1914,7 @@ version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
@ -1836,7 +1949,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
@ -1932,7 +2045,7 @@ name = "vecio"
version = "0.1.0" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
@ -1963,7 +2076,7 @@ dependencies = [
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.2.6" version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
@ -1991,10 +2104,15 @@ name = "ws2_32-sys"
version = "0.2.1" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "xdg"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "xml-rs" name = "xml-rs"
version = "0.3.4" version = "0.3.4"
@ -2025,6 +2143,7 @@ dependencies = [
[metadata] [metadata]
"checksum aho-corasick 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "67077478f0a03952bed2e6786338d400d40c25e9836e08ad50af96607317fd03" "checksum aho-corasick 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "67077478f0a03952bed2e6786338d400d40c25e9836e08ad50af96607317fd03"
"checksum ansi_term 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1f46cd5b1d660c938e3f92dfe7a73d832b3281479363dd0cd9c1c2fbf60f7962" "checksum ansi_term 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1f46cd5b1d660c938e3f92dfe7a73d832b3281479363dd0cd9c1c2fbf60f7962"
"checksum app_dirs 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b7d1c0d48a81bbb13043847f957971f4d87c81542d80ece5e84ba3cba4058fd4"
"checksum arrayvec 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)" = "16e3bdb2f54b3ace0285975d59a97cf8ed3855294b2b6bc651fcf22a9c352975" "checksum arrayvec 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)" = "16e3bdb2f54b3ace0285975d59a97cf8ed3855294b2b6bc651fcf22a9c352975"
"checksum aster 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = "07d344974f0a155f091948aa389fb1b912d3a58414fbdb9c8d446d193ee3496a" "checksum aster 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = "07d344974f0a155f091948aa389fb1b912d3a58414fbdb9c8d446d193ee3496a"
"checksum aster 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4df293303e8a52e1df7984ac1415e195f5fcbf51e4bb7bda54557861a3954a08" "checksum aster 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4df293303e8a52e1df7984ac1415e195f5fcbf51e4bb7bda54557861a3954a08"
@ -2055,6 +2174,7 @@ dependencies = [
"checksum ethabi 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f7b0c53453517f620847be51943db329276ae52f2e210cfc659e81182864be2f" "checksum ethabi 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f7b0c53453517f620847be51943db329276ae52f2e210cfc659e81182864be2f"
"checksum fdlimit 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b1ee15a7050e5580b3712877157068ea713b245b080ff302ae2ca973cfcd9baa" "checksum fdlimit 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b1ee15a7050e5580b3712877157068ea713b245b080ff302ae2ca973cfcd9baa"
"checksum flate2 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "3eeb481e957304178d2e782f2da1257f1434dfecbae883bafb61ada2a9fea3bb" "checksum flate2 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "3eeb481e957304178d2e782f2da1257f1434dfecbae883bafb61ada2a9fea3bb"
"checksum futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0bad0a2ac64b227fdc10c254051ae5af542cf19c9328704fd4092f7914196897"
"checksum gcc 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)" = "91ecd03771effb0c968fd6950b37e89476a578aaf1c70297d8e92b6516ec3312" "checksum gcc 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)" = "91ecd03771effb0c968fd6950b37e89476a578aaf1c70297d8e92b6516ec3312"
"checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" "checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb"
"checksum hamming 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "65043da274378d68241eb9a8f8f8aa54e349136f7b8e12f63e3ef44043cc30e1" "checksum hamming 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "65043da274378d68241eb9a8f8f8aa54e349136f7b8e12f63e3ef44043cc30e1"
@ -2068,9 +2188,11 @@ dependencies = [
"checksum isatty 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7408a548dc0e406b7912d9f84c261cc533c1866e047644a811c133c56041ac0c" "checksum isatty 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7408a548dc0e406b7912d9f84c261cc533c1866e047644a811c133c56041ac0c"
"checksum itertools 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)" = "086e1fa5fe48840b1cfdef3a20c7e3115599f8d5c4c87ef32a794a7cdd184d76" "checksum itertools 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)" = "086e1fa5fe48840b1cfdef3a20c7e3115599f8d5c4c87ef32a794a7cdd184d76"
"checksum itoa 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ae3088ea4baeceb0284ee9eea42f591226e6beaecf65373e41b38d95a1b8e7a1" "checksum itoa 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ae3088ea4baeceb0284ee9eea42f591226e6beaecf65373e41b38d95a1b8e7a1"
"checksum jsonrpc-core 3.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3c5094610b07f28f3edaf3947b732dadb31dbba4941d4d0c1c7a8350208f4414"
"checksum jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>" "checksum jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>"
"checksum jsonrpc-http-server 6.1.1 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>" "checksum jsonrpc-http-server 6.1.1 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>"
"checksum jsonrpc-ipc-server 0.2.4 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>" "checksum jsonrpc-ipc-server 0.2.4 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>"
"checksum jsonrpc-macros 0.1.0 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>"
"checksum jsonrpc-tcp-server 0.1.0 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>" "checksum jsonrpc-tcp-server 0.1.0 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>"
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
"checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" "checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a"
@ -2110,11 +2232,12 @@ dependencies = [
"checksum num_cpus 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "51fedae97a05f7353612fe017ab705a37e6db8f4d67c5c6fe739a9e70d6eed09" "checksum num_cpus 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "51fedae97a05f7353612fe017ab705a37e6db8f4d67c5c6fe739a9e70d6eed09"
"checksum number_prefix 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "084d05f4bf60621a9ac9bde941a410df548f4de9545f06e5ee9d3aef4b97cd77" "checksum number_prefix 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "084d05f4bf60621a9ac9bde941a410df548f4de9545f06e5ee9d3aef4b97cd77"
"checksum odds 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "b28c06e81b0f789122d415d6394b5fe849bde8067469f4c2980d3cdc10c78ec1" "checksum odds 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "b28c06e81b0f789122d415d6394b5fe849bde8067469f4c2980d3cdc10c78ec1"
"checksum ole32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5d2c49021782e5233cd243168edfa8037574afed4eba4bbaf538b3d8d1789d8c"
"checksum owning_ref 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8d91377085359426407a287ab16884a0111ba473aa6844ff01d4ec20ce3d75e7" "checksum owning_ref 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8d91377085359426407a287ab16884a0111ba473aa6844ff01d4ec20ce3d75e7"
"checksum parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "98378dec0a185da2b7180308752f0bad73aaa949c3e0a3b0528d0e067945f7ab" "checksum parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "98378dec0a185da2b7180308752f0bad73aaa949c3e0a3b0528d0e067945f7ab"
"checksum parity-ui-precompiled 1.4.0 (git+https://github.com/ethcore/js-precompiled.git)" = "<none>" "checksum parity-ui-precompiled 1.4.0 (git+https://github.com/ethcore/js-precompiled.git)" = "<none>"
"checksum parking_lot 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "968f685642555d2f7e202c48b8b11de80569e9bfea817f7f12d7c61aac62d4e6" "checksum parking_lot 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "968f685642555d2f7e202c48b8b11de80569e9bfea817f7f12d7c61aac62d4e6"
"checksum parking_lot 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "dbc5847584161f273e69edc63c1a86254a22f570a0b5dd87aa6f9773f6f7d125" "checksum parking_lot 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "e1435e7a2a00dfebededd6c6bdbd54008001e94b4a2aadd6aef0dc4c56317621"
"checksum parking_lot_core 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fb1b97670a2ffadce7c397fb80a3d687c4f3060140b885621ef1653d0e5d5068" "checksum parking_lot_core 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fb1b97670a2ffadce7c397fb80a3d687c4f3060140b885621ef1653d0e5d5068"
"checksum phf 0.7.14 (registry+https://github.com/rust-lang/crates.io-index)" = "447d9d45f2e0b4a9b532e808365abf18fc211be6ca217202fcd45236ef12f026" "checksum phf 0.7.14 (registry+https://github.com/rust-lang/crates.io-index)" = "447d9d45f2e0b4a9b532e808365abf18fc211be6ca217202fcd45236ef12f026"
"checksum phf_codegen 0.7.14 (registry+https://github.com/rust-lang/crates.io-index)" = "8af7ae7c3f75a502292b491e5cc0a1f69e3407744abe6e57e2a3b712bb82f01d" "checksum phf_codegen 0.7.14 (registry+https://github.com/rust-lang/crates.io-index)" = "8af7ae7c3f75a502292b491e5cc0a1f69e3407744abe6e57e2a3b712bb82f01d"
@ -2141,6 +2264,7 @@ dependencies = [
"checksum rocksdb-sys 0.3.0 (git+https://github.com/ethcore/rust-rocksdb)" = "<none>" "checksum rocksdb-sys 0.3.0 (git+https://github.com/ethcore/rust-rocksdb)" = "<none>"
"checksum rotor 0.6.3 (git+https://github.com/ethcore/rotor)" = "<none>" "checksum rotor 0.6.3 (git+https://github.com/ethcore/rotor)" = "<none>"
"checksum rpassword 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5d3a99497c5c544e629cc8b359ae5ede321eba5fa8e5a8078f3ced727a976c3f" "checksum rpassword 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5d3a99497c5c544e629cc8b359ae5ede321eba5fa8e5a8078f3ced727a976c3f"
"checksum rpassword 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ab6e42be826e215f30ff830904f8f4a0933c6e2ae890e1af8b408f5bae60081e"
"checksum rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)" = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a" "checksum rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)" = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a"
"checksum rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)" = "6159e4e6e559c81bd706afe9c8fd68f547d3e851ce12e76b1de7914bab61691b" "checksum rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)" = "6159e4e6e559c81bd706afe9c8fd68f547d3e851ce12e76b1de7914bab61691b"
"checksum rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c5f5376ea5e30ce23c03eb77cbe4962b988deead10910c372b226388b594c084" "checksum rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c5f5376ea5e30ce23c03eb77cbe4962b988deead10910c372b226388b594c084"
@ -2152,6 +2276,7 @@ dependencies = [
"checksum serde_codegen_internals 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f877e2781ed0a323295d1c9f0e26556117b5a11489fc47b1848dfb98b3173d21" "checksum serde_codegen_internals 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f877e2781ed0a323295d1c9f0e26556117b5a11489fc47b1848dfb98b3173d21"
"checksum serde_json 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0e10f8a9d94b06cf5d3bef66475f04c8ff90950f1be7004c357ff9472ccbaebc" "checksum serde_json 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0e10f8a9d94b06cf5d3bef66475f04c8ff90950f1be7004c357ff9472ccbaebc"
"checksum sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cc30b1e1e8c40c121ca33b86c23308a090d19974ef001b4bf6e61fd1a0fb095c" "checksum sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cc30b1e1e8c40c121ca33b86c23308a090d19974ef001b4bf6e61fd1a0fb095c"
"checksum shell32-sys 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "72f20b8f3c060374edb8046591ba28f62448c369ccbdc7b02075103fb3a9e38d"
"checksum siphasher 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5c44e42fa187b5a8782489cf7740cc27c3125806be2bf33563cf5e02e9533fcd" "checksum siphasher 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5c44e42fa187b5a8782489cf7740cc27c3125806be2bf33563cf5e02e9533fcd"
"checksum slab 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d807fd58c4181bbabed77cb3b891ba9748241a552bcc5be698faaebefc54f46e" "checksum slab 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d807fd58c4181bbabed77cb3b891ba9748241a552bcc5be698faaebefc54f46e"
"checksum slab 0.2.0 (git+https://github.com/carllerche/slab?rev=5476efcafb)" = "<none>" "checksum slab 0.2.0 (git+https://github.com/carllerche/slab?rev=5476efcafb)" = "<none>"
@ -2170,6 +2295,7 @@ dependencies = [
"checksum syntex_syntax 0.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "44bded3cabafc65c90b663b1071bd2d198a9ab7515e6ce729e4570aaf53c407e" "checksum syntex_syntax 0.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "44bded3cabafc65c90b663b1071bd2d198a9ab7515e6ce729e4570aaf53c407e"
"checksum syntex_syntax 0.42.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7628a0506e8f9666fdabb5f265d0059b059edac9a3f810bda077abb5d826bd8d" "checksum syntex_syntax 0.42.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7628a0506e8f9666fdabb5f265d0059b059edac9a3f810bda077abb5d826bd8d"
"checksum target_info 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c63f48baada5c52e65a29eef93ab4f8982681b67f9e8d29c7b05abcfec2b9ffe" "checksum target_info 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c63f48baada5c52e65a29eef93ab4f8982681b67f9e8d29c7b05abcfec2b9ffe"
"checksum tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87974a6f5c1dfb344d733055601650059a3363de2a6104819293baff662132d6"
"checksum term 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "f2077e54d38055cf1ca0fd7933a2e00cd3ec8f6fed352b2a377f06dcdaaf3281" "checksum term 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "f2077e54d38055cf1ca0fd7933a2e00cd3ec8f6fed352b2a377f06dcdaaf3281"
"checksum term 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3deff8a2b3b6607d6d7cc32ac25c0b33709453ca9cceac006caac51e963cf94a" "checksum term 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3deff8a2b3b6607d6d7cc32ac25c0b33709453ca9cceac006caac51e963cf94a"
"checksum termios 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d5d9cf598a6d7ce700a4e6a9199da127e6819a61e64b68609683cc9a01b5683a" "checksum termios 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d5d9cf598a6d7ce700a4e6a9199da127e6819a61e64b68609683cc9a01b5683a"
@ -2193,10 +2319,11 @@ dependencies = [
"checksum vergen 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "56b639f935488eb40f06d17c3e3bcc3054f6f75d264e187b1107c8d1cba8d31c" "checksum vergen 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "56b639f935488eb40f06d17c3e3bcc3054f6f75d264e187b1107c8d1cba8d31c"
"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
"checksum webpki 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "813503a5985585e0812d430cd1328ee322f47f66629c8ed4ecab939cf9e92f91" "checksum webpki 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "813503a5985585e0812d430cd1328ee322f47f66629c8ed4ecab939cf9e92f91"
"checksum winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "4dfaaa8fbdaa618fa6914b59b2769d690dd7521920a18d84b42d254678dd5fd4" "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
"checksum ws 0.5.3 (git+https://github.com/ethcore/ws-rs.git?branch=mio-upstream-stable)" = "<none>" "checksum ws 0.5.3 (git+https://github.com/ethcore/ws-rs.git?branch=mio-upstream-stable)" = "<none>"
"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" "checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
"checksum xdg 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "77b831a5ba77110f438f0ac5583aafeb087f70432998ba6b7dcb1d32185db453"
"checksum xml-rs 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "65e74b96bd3179209dc70a980da6df843dff09e46eee103a0376c0949257e3ef" "checksum xml-rs 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "65e74b96bd3179209dc70a980da6df843dff09e46eee103a0376c0949257e3ef"
"checksum xmltree 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "472a9d37c7c53ab2391161df5b89b1f3bf76dab6ab150d7941ecbdd832282082" "checksum xmltree 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "472a9d37c7c53ab2391161df5b89b1f3bf76dab6ab150d7941ecbdd832282082"
"checksum zip 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "3ceb33a75b3d0608942302eed325b59d2c3ed777cc6c01627ae14e5697c6a31c" "checksum zip 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "3ceb33a75b3d0608942302eed325b59d2c3ed777cc6c01627ae14e5697c6a31c"

View File

@ -28,6 +28,7 @@ isatty = "0.1"
toml = "0.2" toml = "0.2"
serde = "0.8.0" serde = "0.8.0"
serde_json = "0.8.0" serde_json = "0.8.0"
app_dirs = "1.1.1"
hyper = { version = "0.9", default-features = false } hyper = { version = "0.9", default-features = false }
ctrlc = { git = "https://github.com/ethcore/rust-ctrlc.git" } ctrlc = { git = "https://github.com/ethcore/rust-ctrlc.git" }
fdlimit = "0.1" fdlimit = "0.1"
@ -47,6 +48,8 @@ rlp = { path = "util/rlp" }
ethcore-stratum = { path = "stratum" } ethcore-stratum = { path = "stratum" }
ethcore-dapps = { path = "dapps", optional = true } ethcore-dapps = { path = "dapps", optional = true }
clippy = { version = "0.0.103", optional = true} clippy = { version = "0.0.103", optional = true}
rpc-cli = { path = "rpc_cli" }
parity-rpc-client = { path = "rpc_client" }
ethcore-light = { path = "ethcore/light" } ethcore-light = { path = "ethcore/light" }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]

View File

@ -120,7 +120,7 @@ impl<R: URLHint> ContentFetcher<R> {
// Content is already being fetched // Content is already being fetched
Some(&mut ContentStatus::Fetching(ref fetch_control)) => { Some(&mut ContentStatus::Fetching(ref fetch_control)) => {
trace!(target: "dapps", "Content fetching in progress. Waiting..."); trace!(target: "dapps", "Content fetching in progress. Waiting...");
(None, fetch_control.to_handler(control)) (None, fetch_control.to_async_handler(path, control))
}, },
// We need to start fetching the content // We need to start fetching the content
None => { None => {
@ -129,11 +129,12 @@ impl<R: URLHint> ContentFetcher<R> {
let content = self.resolver.resolve(content_hex); let content = self.resolver.resolve(content_hex);
let cache = self.cache.clone(); let cache = self.cache.clone();
let on_done = move |id: String, result: Option<LocalPageEndpoint>| { let id = content_id.clone();
let on_done = move |result: Option<LocalPageEndpoint>| {
let mut cache = cache.lock(); let mut cache = cache.lock();
match result { match result {
Some(endpoint) => { Some(endpoint) => {
cache.insert(id, ContentStatus::Ready(endpoint)); cache.insert(id.clone(), ContentStatus::Ready(endpoint));
}, },
// In case of error // In case of error
None => { None => {
@ -150,6 +151,7 @@ impl<R: URLHint> ContentFetcher<R> {
Some(URLHintResult::Dapp(dapp)) => { Some(URLHintResult::Dapp(dapp)) => {
let (handler, fetch_control) = ContentFetcherHandler::new( let (handler, fetch_control) = ContentFetcherHandler::new(
dapp.url(), dapp.url(),
path,
control, control,
DappInstaller { DappInstaller {
id: content_id.clone(), id: content_id.clone(),
@ -165,6 +167,7 @@ impl<R: URLHint> ContentFetcher<R> {
Some(URLHintResult::Content(content)) => { Some(URLHintResult::Content(content)) => {
let (handler, fetch_control) = ContentFetcherHandler::new( let (handler, fetch_control) = ContentFetcherHandler::new(
content.url, content.url,
path,
control, control,
ContentInstaller { ContentInstaller {
id: content_id.clone(), id: content_id.clone(),
@ -248,13 +251,14 @@ struct ContentInstaller {
id: String, id: String,
mime: String, mime: String,
content_path: PathBuf, content_path: PathBuf,
on_done: Box<Fn(String, Option<LocalPageEndpoint>) + Send>, on_done: Box<Fn(Option<LocalPageEndpoint>) + Send>,
} }
impl ContentValidator for ContentInstaller { impl ContentValidator for ContentInstaller {
type Error = ValidationError; type Error = ValidationError;
fn validate_and_install(&self, path: PathBuf) -> Result<(String, LocalPageEndpoint), ValidationError> { fn validate_and_install(&self, path: PathBuf) -> Result<LocalPageEndpoint, ValidationError> {
let validate = || {
// Create dir // Create dir
try!(fs::create_dir_all(&self.content_path)); try!(fs::create_dir_all(&self.content_path));
@ -279,12 +283,13 @@ impl ContentValidator for ContentInstaller {
} }
try!(fs::copy(&path, &content_path)); try!(fs::copy(&path, &content_path));
Ok(LocalPageEndpoint::single_file(content_path, self.mime.clone(), PageCache::Enabled))
};
Ok((self.id.clone(), LocalPageEndpoint::single_file(content_path, self.mime.clone(), PageCache::Enabled))) // Make sure to always call on_done (even in case of errors)!
} let result = validate();
(self.on_done)(result.as_ref().ok().cloned());
fn done(&self, endpoint: Option<LocalPageEndpoint>) { result
(self.on_done)(self.id.clone(), endpoint)
} }
} }
@ -292,7 +297,7 @@ impl ContentValidator for ContentInstaller {
struct DappInstaller { struct DappInstaller {
id: String, id: String,
dapps_path: PathBuf, dapps_path: PathBuf,
on_done: Box<Fn(String, Option<LocalPageEndpoint>) + Send>, on_done: Box<Fn(Option<LocalPageEndpoint>) + Send>,
embeddable_on: Option<(String, u16)>, embeddable_on: Option<(String, u16)>,
} }
@ -331,9 +336,10 @@ impl DappInstaller {
impl ContentValidator for DappInstaller { impl ContentValidator for DappInstaller {
type Error = ValidationError; type Error = ValidationError;
fn validate_and_install(&self, app_path: PathBuf) -> Result<(String, LocalPageEndpoint), ValidationError> { fn validate_and_install(&self, path: PathBuf) -> Result<LocalPageEndpoint, ValidationError> {
trace!(target: "dapps", "Opening dapp bundle at {:?}", app_path); trace!(target: "dapps", "Opening dapp bundle at {:?}", path);
let mut file_reader = io::BufReader::new(try!(fs::File::open(app_path))); let validate = || {
let mut file_reader = io::BufReader::new(try!(fs::File::open(path)));
let hash = try!(sha3(&mut file_reader)); let hash = try!(sha3(&mut file_reader));
let id = try!(self.id.as_str().parse().map_err(|_| ValidationError::InvalidContentId)); let id = try!(self.id.as_str().parse().map_err(|_| ValidationError::InvalidContentId));
if id != hash { if id != hash {
@ -384,16 +390,14 @@ impl ContentValidator for DappInstaller {
let manifest_path = target.join(MANIFEST_FILENAME); let manifest_path = target.join(MANIFEST_FILENAME);
let mut manifest_file = try!(fs::File::create(manifest_path)); let mut manifest_file = try!(fs::File::create(manifest_path));
try!(manifest_file.write_all(manifest_str.as_bytes())); try!(manifest_file.write_all(manifest_str.as_bytes()));
// Create endpoint // Create endpoint
let app = LocalPageEndpoint::new(target, manifest.clone().into(), PageCache::Enabled, self.embeddable_on.clone()); let endpoint = LocalPageEndpoint::new(target, manifest.clone().into(), PageCache::Enabled, self.embeddable_on.clone());
Ok(endpoint)
};
// Return modified app manifest let result = validate();
Ok((manifest.id.clone(), app)) (self.on_done)(result.as_ref().ok().cloned());
} result
fn done(&self, endpoint: Option<LocalPageEndpoint>) {
(self.on_done)(self.id.clone(), endpoint)
} }
} }

View File

@ -22,35 +22,41 @@ use std::sync::{mpsc, Arc};
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Instant, Duration}; use std::time::{Instant, Duration};
use util::Mutex; use util::Mutex;
use url::Url;
use fetch::{Client, Fetch, FetchResult}; use fetch::{Client, Fetch, FetchResult};
use hyper::{server, Decoder, Encoder, Next, Method, Control}; use hyper::{server, Decoder, Encoder, Next, Method, Control};
use hyper::net::HttpStream; use hyper::net::HttpStream;
use hyper::uri::RequestUri;
use hyper::status::StatusCode; use hyper::status::StatusCode;
use handlers::{ContentHandler, Redirection, extract_url}; use endpoint::EndpointPath;
use page::LocalPageEndpoint; use handlers::ContentHandler;
use page::{LocalPageEndpoint, PageHandlerWaiting};
const FETCH_TIMEOUT: u64 = 30; const FETCH_TIMEOUT: u64 = 30;
enum FetchState { enum FetchState {
Waiting,
NotStarted(String), NotStarted(String),
Error(ContentHandler), Error(ContentHandler),
InProgress(mpsc::Receiver<FetchResult>), InProgress(mpsc::Receiver<FetchResult>),
Done(String, LocalPageEndpoint, Redirection), Done(LocalPageEndpoint, Box<PageHandlerWaiting>),
}
enum WaitResult {
Error(ContentHandler),
Done(LocalPageEndpoint),
} }
pub trait ContentValidator { pub trait ContentValidator {
type Error: fmt::Debug + fmt::Display; type Error: fmt::Debug + fmt::Display;
fn validate_and_install(&self, app: PathBuf) -> Result<(String, LocalPageEndpoint), Self::Error>; fn validate_and_install(&self, path: PathBuf) -> Result<LocalPageEndpoint, Self::Error>;
fn done(&self, Option<LocalPageEndpoint>);
} }
pub struct FetchControl { pub struct FetchControl {
abort: Arc<AtomicBool>, abort: Arc<AtomicBool>,
listeners: Mutex<Vec<(Control, mpsc::Sender<FetchState>)>>, listeners: Mutex<Vec<(Control, mpsc::Sender<WaitResult>)>>,
deadline: Instant, deadline: Instant,
} }
@ -65,9 +71,10 @@ impl Default for FetchControl {
} }
impl FetchControl { impl FetchControl {
fn notify<F: Fn() -> FetchState>(&self, status: F) { fn notify<F: Fn() -> WaitResult>(&self, status: F) {
let mut listeners = self.listeners.lock(); let mut listeners = self.listeners.lock();
for (control, sender) in listeners.drain(..) { for (control, sender) in listeners.drain(..) {
trace!(target: "dapps", "Resuming request waiting for content...");
if let Err(e) = sender.send(status()) { if let Err(e) = sender.send(status()) {
trace!(target: "dapps", "Waiting listener notification failed: {:?}", e); trace!(target: "dapps", "Waiting listener notification failed: {:?}", e);
} else { } else {
@ -78,9 +85,9 @@ impl FetchControl {
fn set_status(&self, status: &FetchState) { fn set_status(&self, status: &FetchState) {
match *status { match *status {
FetchState::Error(ref handler) => self.notify(|| FetchState::Error(handler.clone())), FetchState::Error(ref handler) => self.notify(|| WaitResult::Error(handler.clone())),
FetchState::Done(ref id, ref endpoint, ref handler) => self.notify(|| FetchState::Done(id.clone(), endpoint.clone(), handler.clone())), FetchState::Done(ref endpoint, _) => self.notify(|| WaitResult::Done(endpoint.clone())),
FetchState::NotStarted(_) | FetchState::InProgress(_) => {}, FetchState::NotStarted(_) | FetchState::InProgress(_) | FetchState::Waiting => {},
} }
} }
@ -88,44 +95,66 @@ impl FetchControl {
self.abort.store(true, Ordering::SeqCst); self.abort.store(true, Ordering::SeqCst);
} }
pub fn to_handler(&self, control: Control) -> Box<server::Handler<HttpStream> + Send> { pub fn to_async_handler(&self, path: EndpointPath, control: Control) -> Box<server::Handler<HttpStream> + Send> {
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
self.listeners.lock().push((control, tx)); self.listeners.lock().push((control, tx));
Box::new(WaitingHandler { Box::new(WaitingHandler {
receiver: rx, receiver: rx,
state: None, state: FetchState::Waiting,
uri: RequestUri::default(),
path: path,
}) })
} }
} }
pub struct WaitingHandler { pub struct WaitingHandler {
receiver: mpsc::Receiver<FetchState>, receiver: mpsc::Receiver<WaitResult>,
state: Option<FetchState>, state: FetchState,
uri: RequestUri,
path: EndpointPath,
} }
impl server::Handler<HttpStream> for WaitingHandler { impl server::Handler<HttpStream> for WaitingHandler {
fn on_request(&mut self, _request: server::Request<HttpStream>) -> Next { fn on_request(&mut self, request: server::Request<HttpStream>) -> Next {
self.uri = request.uri().clone();
Next::wait() Next::wait()
} }
fn on_request_readable(&mut self, _decoder: &mut Decoder<HttpStream>) -> Next { fn on_request_readable(&mut self, decoder: &mut Decoder<HttpStream>) -> Next {
self.state = self.receiver.try_recv().ok(); let result = self.receiver.try_recv().ok();
Next::write() self.state = match result {
Some(WaitResult::Error(handler)) => FetchState::Error(handler),
Some(WaitResult::Done(endpoint)) => {
let mut page_handler = endpoint.to_page_handler(self.path.clone());
page_handler.set_uri(&self.uri);
FetchState::Done(endpoint, page_handler)
},
None => {
warn!("A result for waiting request was not received.");
FetchState::Waiting
},
};
match self.state {
FetchState::Done(_, ref mut handler) => handler.on_request_readable(decoder),
FetchState::Error(ref mut handler) => handler.on_request_readable(decoder),
_ => Next::write(),
}
} }
fn on_response(&mut self, res: &mut server::Response) -> Next { fn on_response(&mut self, res: &mut server::Response) -> Next {
match self.state { match self.state {
Some(FetchState::Done(_, _, ref mut handler)) => handler.on_response(res), FetchState::Done(_, ref mut handler) => handler.on_response(res),
Some(FetchState::Error(ref mut handler)) => handler.on_response(res), FetchState::Error(ref mut handler) => handler.on_response(res),
_ => Next::end(), _ => Next::end(),
} }
} }
fn on_response_writable(&mut self, encoder: &mut Encoder<HttpStream>) -> Next { fn on_response_writable(&mut self, encoder: &mut Encoder<HttpStream>) -> Next {
match self.state { match self.state {
Some(FetchState::Done(_, _, ref mut handler)) => handler.on_response_writable(encoder), FetchState::Done(_, ref mut handler) => handler.on_response_writable(encoder),
Some(FetchState::Error(ref mut handler)) => handler.on_response_writable(encoder), FetchState::Error(ref mut handler) => handler.on_response_writable(encoder),
_ => Next::end(), _ => Next::end(),
} }
} }
@ -137,29 +166,19 @@ pub struct ContentFetcherHandler<H: ContentValidator> {
status: FetchState, status: FetchState,
client: Option<Client>, client: Option<Client>,
installer: H, installer: H,
request_url: Option<Url>, path: EndpointPath,
uri: RequestUri,
embeddable_on: Option<(String, u16)>, embeddable_on: Option<(String, u16)>,
} }
impl<H: ContentValidator> Drop for ContentFetcherHandler<H> {
fn drop(&mut self) {
let result = match self.status {
FetchState::Done(_, ref result, _) => Some(result.clone()),
_ => None,
};
self.installer.done(result);
}
}
impl<H: ContentValidator> ContentFetcherHandler<H> { impl<H: ContentValidator> ContentFetcherHandler<H> {
pub fn new( pub fn new(
url: String, url: String,
path: EndpointPath,
control: Control, control: Control,
handler: H, handler: H,
embeddable_on: Option<(String, u16)>, embeddable_on: Option<(String, u16)>,
) -> (Self, Arc<FetchControl>) { ) -> (Self, Arc<FetchControl>) {
let fetch_control = Arc::new(FetchControl::default()); let fetch_control = Arc::new(FetchControl::default());
let client = Client::default(); let client = Client::default();
let handler = ContentFetcherHandler { let handler = ContentFetcherHandler {
@ -168,7 +187,8 @@ impl<H: ContentValidator> ContentFetcherHandler<H> {
client: Some(client), client: Some(client),
status: FetchState::NotStarted(url), status: FetchState::NotStarted(url),
installer: handler, installer: handler,
request_url: None, path: path,
uri: RequestUri::default(),
embeddable_on: embeddable_on, embeddable_on: embeddable_on,
}; };
@ -192,7 +212,6 @@ impl<H: ContentValidator> ContentFetcherHandler<H> {
impl<H: ContentValidator> server::Handler<HttpStream> for ContentFetcherHandler<H> { impl<H: ContentValidator> server::Handler<HttpStream> for ContentFetcherHandler<H> {
fn on_request(&mut self, request: server::Request<HttpStream>) -> Next { fn on_request(&mut self, request: server::Request<HttpStream>) -> Next {
self.request_url = extract_url(&request);
let status = if let FetchState::NotStarted(ref url) = self.status { let status = if let FetchState::NotStarted(ref url) = self.status {
Some(match *request.method() { Some(match *request.method() {
// Start fetching content // Start fetching content
@ -205,8 +224,8 @@ impl<H: ContentValidator> server::Handler<HttpStream> for ContentFetcherHandler<
Ok(receiver) => FetchState::InProgress(receiver), Ok(receiver) => FetchState::InProgress(receiver),
Err(e) => FetchState::Error(ContentHandler::error( Err(e) => FetchState::Error(ContentHandler::error(
StatusCode::BadGateway, StatusCode::BadGateway,
"Unable To Start Dapp Download", "Unable To Start Content Download",
"Could not initialize download of the dapp. It might be a problem with the remote server.", "Could not initialize download of the content. It might be a problem with the remote server.",
Some(&format!("{}", e)), Some(&format!("{}", e)),
self.embeddable_on.clone(), self.embeddable_on.clone(),
)), )),
@ -227,6 +246,7 @@ impl<H: ContentValidator> server::Handler<HttpStream> for ContentFetcherHandler<
self.fetch_control.set_status(&status); self.fetch_control.set_status(&status);
self.status = status; self.status = status;
} }
self.uri = request.uri().clone();
Next::read() Next::read()
} }
@ -266,11 +286,10 @@ impl<H: ContentValidator> server::Handler<HttpStream> for ContentFetcherHandler<
self.embeddable_on.clone(), self.embeddable_on.clone(),
)) ))
}, },
Ok((id, result)) => { Ok(endpoint) => {
let url: String = self.request_url.take() let mut handler = endpoint.to_page_handler(self.path.clone());
.map(|url| url.raw.into_string()) handler.set_uri(&self.uri);
.expect("Request URL always read in on_request; qed"); FetchState::Done(endpoint, handler)
FetchState::Done(id, result, Redirection::new(&url))
}, },
}; };
// Remove temporary zip file // Remove temporary zip file
@ -306,7 +325,7 @@ impl<H: ContentValidator> server::Handler<HttpStream> for ContentFetcherHandler<
fn on_response(&mut self, res: &mut server::Response) -> Next { fn on_response(&mut self, res: &mut server::Response) -> Next {
match self.status { match self.status {
FetchState::Done(_, _, ref mut handler) => handler.on_response(res), FetchState::Done(_, ref mut handler) => handler.on_response(res),
FetchState::Error(ref mut handler) => handler.on_response(res), FetchState::Error(ref mut handler) => handler.on_response(res),
_ => Next::end(), _ => Next::end(),
} }
@ -314,7 +333,7 @@ impl<H: ContentValidator> server::Handler<HttpStream> for ContentFetcherHandler<
fn on_response_writable(&mut self, encoder: &mut Encoder<HttpStream>) -> Next { fn on_response_writable(&mut self, encoder: &mut Encoder<HttpStream>) -> Next {
match self.status { match self.status {
FetchState::Done(_, _, ref mut handler) => handler.on_response_writable(encoder), FetchState::Done(_, ref mut handler) => handler.on_response_writable(encoder),
FetchState::Error(ref mut handler) => handler.on_response_writable(encoder), FetchState::Error(ref mut handler) => handler.on_response_writable(encoder),
_ => Next::end(), _ => Next::end(),
} }

View File

@ -83,13 +83,19 @@ impl Default for PageCache {
} }
} }
/// A generic type for `PageHandler` allowing to set the URL.
/// Used by dapps fetching to set the URL after the content was downloaded.
pub trait PageHandlerWaiting: server::Handler<HttpStream> + Send {
fn set_uri(&mut self, uri: &RequestUri);
}
/// A handler for a single webapp. /// A handler for a single webapp.
/// Resolves correct paths and serves as a plumbing code between /// Resolves correct paths and serves as a plumbing code between
/// hyper server and dapp. /// hyper server and dapp.
pub struct PageHandler<T: Dapp> { pub struct PageHandler<T: Dapp> {
/// A Dapp. /// A Dapp.
pub app: T, pub app: T,
/// File currently being served (or `None` if file does not exist). /// File currently being served
pub file: ServedFile<T>, pub file: ServedFile<T>,
/// Optional prefix to strip from path. /// Optional prefix to strip from path.
pub prefix: Option<String>, pub prefix: Option<String>,
@ -101,6 +107,21 @@ pub struct PageHandler<T: Dapp> {
pub cache: PageCache, pub cache: PageCache,
} }
impl<T: Dapp> PageHandlerWaiting for PageHandler<T> {
fn set_uri(&mut self, uri: &RequestUri) {
trace!(target: "dapps", "Setting URI: {:?}", uri);
self.file = match *uri {
RequestUri::AbsolutePath { ref path, .. } => {
self.app.file(&self.extract_path(path))
},
RequestUri::AbsoluteUri(ref url) => {
self.app.file(&self.extract_path(url.path()))
},
_ => None,
}.map_or_else(|| ServedFile::new(self.safe_to_embed_on.clone()), |f| ServedFile::File(f));
}
}
impl<T: Dapp> PageHandler<T> { impl<T: Dapp> PageHandler<T> {
fn extract_path(&self, path: &str) -> String { fn extract_path(&self, path: &str) -> String {
let app_id = &self.path.app_id; let app_id = &self.path.app_id;
@ -124,15 +145,7 @@ impl<T: Dapp> PageHandler<T> {
impl<T: Dapp> server::Handler<HttpStream> for PageHandler<T> { impl<T: Dapp> server::Handler<HttpStream> for PageHandler<T> {
fn on_request(&mut self, req: server::Request<HttpStream>) -> Next { fn on_request(&mut self, req: server::Request<HttpStream>) -> Next {
self.file = match *req.uri() { self.set_uri(req.uri());
RequestUri::AbsolutePath { ref path, .. } => {
self.app.file(&self.extract_path(path))
},
RequestUri::AbsoluteUri(ref url) => {
self.app.file(&self.extract_path(url.path()))
},
_ => None,
}.map_or_else(|| ServedFile::new(self.safe_to_embed_on.clone()), |f| ServedFile::File(f));
Next::write() Next::write()
} }

View File

@ -18,7 +18,7 @@ use mime_guess;
use std::io::{Seek, Read, SeekFrom}; use std::io::{Seek, Read, SeekFrom};
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use page::handler::{self, PageCache}; use page::handler::{self, PageCache, PageHandlerWaiting};
use endpoint::{Endpoint, EndpointInfo, EndpointPath, Handler}; use endpoint::{Endpoint, EndpointInfo, EndpointPath, Handler};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -54,6 +54,36 @@ impl LocalPageEndpoint {
pub fn path(&self) -> PathBuf { pub fn path(&self) -> PathBuf {
self.path.clone() self.path.clone()
} }
fn page_handler_with_mime(&self, path: EndpointPath, mime: &str) -> handler::PageHandler<LocalSingleFile> {
handler::PageHandler {
app: LocalSingleFile { path: self.path.clone(), mime: mime.into() },
prefix: None,
path: path,
file: handler::ServedFile::new(None),
safe_to_embed_on: self.embeddable_on.clone(),
cache: self.cache,
}
}
fn page_handler(&self, path: EndpointPath) -> handler::PageHandler<LocalDapp> {
handler::PageHandler {
app: LocalDapp { path: self.path.clone() },
prefix: None,
path: path,
file: handler::ServedFile::new(None),
safe_to_embed_on: self.embeddable_on.clone(),
cache: self.cache,
}
}
pub fn to_page_handler(&self, path: EndpointPath) -> Box<PageHandlerWaiting> {
if let Some(ref mime) = self.mime {
Box::new(self.page_handler_with_mime(path, mime))
} else {
Box::new(self.page_handler(path))
}
}
} }
impl Endpoint for LocalPageEndpoint { impl Endpoint for LocalPageEndpoint {
@ -63,23 +93,9 @@ impl Endpoint for LocalPageEndpoint {
fn to_handler(&self, path: EndpointPath) -> Box<Handler> { fn to_handler(&self, path: EndpointPath) -> Box<Handler> {
if let Some(ref mime) = self.mime { if let Some(ref mime) = self.mime {
Box::new(handler::PageHandler { Box::new(self.page_handler_with_mime(path, mime))
app: LocalSingleFile { path: self.path.clone(), mime: mime.clone() },
prefix: None,
path: path,
file: handler::ServedFile::new(None),
safe_to_embed_on: self.embeddable_on.clone(),
cache: self.cache,
})
} else { } else {
Box::new(handler::PageHandler { Box::new(self.page_handler(path))
app: LocalDapp { path: self.path.clone() },
prefix: None,
path: path,
file: handler::ServedFile::new(None),
safe_to_embed_on: self.embeddable_on.clone(),
cache: self.cache,
})
} }
} }
} }

View File

@ -21,5 +21,5 @@ mod handler;
pub use self::local::LocalPageEndpoint; pub use self::local::LocalPageEndpoint;
pub use self::builtin::PageEndpoint; pub use self::builtin::PageEndpoint;
pub use self::handler::PageCache; pub use self::handler::{PageCache, PageHandlerWaiting};

View File

@ -21,8 +21,9 @@
}, },
"genesis": { "genesis": {
"seal": { "seal": {
"generic": { "authority_round": {
"rlp": "0xc28080" "step": "0x0",
"signature": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
} }
}, },
"difficulty": "0x20000", "difficulty": "0x20000",

View File

@ -17,10 +17,7 @@
}, },
"genesis": { "genesis": {
"seal": { "seal": {
"generic": { "generic": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa"
"fields": 1,
"rlp": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa"
}
}, },
"difficulty": "0x20000", "difficulty": "0x20000",
"author": "0x0000000000000000000000000000000000000000", "author": "0x0000000000000000000000000000000000000000",

View File

@ -1,6 +1,6 @@
{ {
"name": "Ethereum Classic", "name": "Ethereum Classic",
"forkName": "classic", "dataDir": "classic",
"engine": { "engine": {
"Ethash": { "Ethash": {
"params": { "params": {
@ -10,14 +10,15 @@
"durationLimit": "0x0d", "durationLimit": "0x0d",
"blockReward": "0x4563918244F40000", "blockReward": "0x4563918244F40000",
"registrar" : "0xc6d9d2cd449a754c494264e1809c50e34d64562b", "registrar" : "0xc6d9d2cd449a754c494264e1809c50e34d64562b",
"homesteadTransition": "0x118c30", "homesteadTransition": 1150000,
"eip150Transition": "0x2625a0", "eip150Transition": 2500000,
"eip155Transition": "0x7fffffffffffffff", "eip155Transition": 3000000,
"eip160Transition": "0x7fffffffffffffff", "eip160Transition": 3000000,
"ecip1010PauseTransition": 3000000,
"ecip1010ContinueTransition": 5000000,
"eip161abcTransition": "0x7fffffffffffffff", "eip161abcTransition": "0x7fffffffffffffff",
"eip161dTransition": "0x7fffffffffffffff", "eip161dTransition": "0x7fffffffffffffff"
"ecip1010PauseTransition": "0x2dc6c0",
"ecip1010ContinueTransition": "0x4c4b40"
} }
} }
}, },

View File

@ -1,6 +1,6 @@
{ {
"name": "Expanse", "name": "Expanse",
"forkName": "expanse", "dataDir": "expanse",
"engine": { "engine": {
"Ethash": { "Ethash": {
"params": { "params": {

View File

@ -1,5 +1,6 @@
{ {
"name": "Frontier/Homestead", "name": "Frontier/Homestead",
"dataDir": "ethereum",
"engine": { "engine": {
"Ethash": { "Ethash": {
"params": { "params": {

View File

@ -1,5 +1,6 @@
{ {
"name": "Morden", "name": "Morden",
"dataDir": "test",
"engine": { "engine": {
"Ethash": { "Ethash": {
"params": { "params": {
@ -9,12 +10,15 @@
"durationLimit": "0x0d", "durationLimit": "0x0d",
"blockReward": "0x4563918244F40000", "blockReward": "0x4563918244F40000",
"registrar": "0x52dff57a8a1532e6afb3dc07e2af58bb9eb05b3d", "registrar": "0x52dff57a8a1532e6afb3dc07e2af58bb9eb05b3d",
"homesteadTransition": "0x789b0", "homesteadTransition": 494000,
"eip150Transition": "0x1b34d8", "eip150Transition": 1783000,
"eip155Transition": 1885000, "eip155Transition": 1915000,
"eip160Transition": 1885000, "eip160Transition": 1915000,
"eip161abcTransition": 1885000, "ecip1010PauseTransition": 1915000,
"eip161dTransition": 1885000 "ecip1010ContinueTransition": 3415000,
"eip161abcTransition": "0x7fffffffffffffff",
"eip161dTransition": "0x7fffffffffffffff"
} }
} }
}, },

View File

@ -1,5 +1,6 @@
{ {
"name": "Ropsten", "name": "Ropsten",
"dataDir": "test",
"engine": { "engine": {
"Ethash": { "Ethash": {
"params": { "params": {

View File

@ -4,29 +4,27 @@
"InstantSeal": null "InstantSeal": null
}, },
"params": { "params": {
"accountStartNonce": "0x0100000", "accountStartNonce": "0x0",
"maximumExtraDataSize": "0x20", "maximumExtraDataSize": "0x20",
"minGasLimit": "0x1388", "minGasLimit": "0x1388",
"networkID" : "0x2" "networkID" : "0x11"
}, },
"genesis": { "genesis": {
"seal": { "seal": {
"generic": { "generic": "0x0"
"rlp": "0x0"
}
}, },
"difficulty": "0x20000", "difficulty": "0x20000",
"author": "0x0000000000000000000000000000000000000000", "author": "0x0000000000000000000000000000000000000000",
"timestamp": "0x00", "timestamp": "0x00",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"extraData": "0x", "extraData": "0x",
"gasLimit": "0x2fefd8" "gasLimit": "0x5B8D80"
}, },
"accounts": { "accounts": {
"0000000000000000000000000000000000000001": { "balance": "1", "nonce": "1048576", "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } }, "0000000000000000000000000000000000000001": { "balance": "1", "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } },
"0000000000000000000000000000000000000002": { "balance": "1", "nonce": "1048576", "builtin": { "name": "sha256", "pricing": { "linear": { "base": 60, "word": 12 } } } }, "0000000000000000000000000000000000000002": { "balance": "1", "builtin": { "name": "sha256", "pricing": { "linear": { "base": 60, "word": 12 } } } },
"0000000000000000000000000000000000000003": { "balance": "1", "nonce": "1048576", "builtin": { "name": "ripemd160", "pricing": { "linear": { "base": 600, "word": 120 } } } }, "0000000000000000000000000000000000000003": { "balance": "1", "builtin": { "name": "ripemd160", "pricing": { "linear": { "base": 600, "word": 120 } } } },
"0000000000000000000000000000000000000004": { "balance": "1", "nonce": "1048576", "builtin": { "name": "identity", "pricing": { "linear": { "base": 15, "word": 3 } } } }, "0000000000000000000000000000000000000004": { "balance": "1", "builtin": { "name": "identity", "pricing": { "linear": { "base": 15, "word": 3 } } } },
"0x00a329c0648769a73afac7f9381e08fb43dbea72": { "balance": "1606938044258990275541962092341162602522202993782792835301376", "nonce": "1048576" } "0x00a329c0648769a73afac7f9381e08fb43dbea72": { "balance": "1606938044258990275541962092341162602522202993782792835301376" }
} }
} }

View File

@ -0,0 +1,44 @@
{
"name": "TestBFT",
"engine": {
"Tendermint": {
"params": {
"gasLimitBoundDivisor": "0x0400",
"authorities" : [
"0x82a978b3f5962a5b0957d9ee9eef472ee55b42f1",
"0x7d577a597b2742b498cb5cf0c26cdcd726d39e6e"
]
}
}
},
"params": {
"accountStartNonce": "0x0",
"maximumExtraDataSize": "0x20",
"minGasLimit": "0x1388",
"networkID" : "0x2323"
},
"genesis": {
"seal": {
"tendermint": {
"round": "0x0",
"proposal": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"precommits": [
"0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
]
}
},
"difficulty": "0x20000",
"author": "0x0000000000000000000000000000000000000000",
"timestamp": "0x00",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"extraData": "0x",
"gasLimit": "0x2fefd8"
},
"accounts": {
"0000000000000000000000000000000000000001": { "balance": "1", "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } },
"0000000000000000000000000000000000000002": { "balance": "1", "builtin": { "name": "sha256", "pricing": { "linear": { "base": 60, "word": 12 } } } },
"0000000000000000000000000000000000000003": { "balance": "1", "builtin": { "name": "ripemd160", "pricing": { "linear": { "base": 600, "word": 120 } } } },
"0000000000000000000000000000000000000004": { "balance": "1", "builtin": { "name": "identity", "pricing": { "linear": { "base": 15, "word": 3 } } } },
"9cce34f7ab185c7aba1b7c8140d620b4bda941d6": { "balance": "1606938044258990275541962092341162602522202993782792835301376" }
}
}

View File

@ -18,14 +18,14 @@
mod stores; mod stores;
use self::stores::{AddressBook, DappsSettingsStore}; use self::stores::{AddressBook, DappsSettingsStore, NewDappsPolicy};
use std::fmt; use std::fmt;
use std::collections::HashMap; use std::collections::HashMap;
use std::time::{Instant, Duration}; use std::time::{Instant, Duration};
use util::{Mutex, RwLock}; use util::RwLock;
use ethstore::{SecretStore, Error as SSError, SafeAccount, EthStore}; use ethstore::{SimpleSecretStore, SecretStore, Error as SSError, EthStore, EthMultiStore, random_string};
use ethstore::dir::{KeyDirectory}; use ethstore::dir::MemoryDirectory;
use ethstore::ethkey::{Address, Message, Public, Secret, Random, Generator}; use ethstore::ethkey::{Address, Message, Public, Secret, Random, Generator};
use ethjson::misc::AccountMeta; use ethjson::misc::AccountMeta;
pub use ethstore::ethkey::Signature; pub use ethstore::ethkey::Signature;
@ -73,58 +73,47 @@ impl From<SSError> for Error {
} }
} }
#[derive(Default)]
struct NullDir {
accounts: RwLock<HashMap<Address, SafeAccount>>,
}
impl KeyDirectory for NullDir {
fn load(&self) -> Result<Vec<SafeAccount>, SSError> {
Ok(self.accounts.read().values().cloned().collect())
}
fn insert(&self, account: SafeAccount) -> Result<SafeAccount, SSError> {
self.accounts.write().insert(account.address.clone(), account.clone());
Ok(account)
}
fn remove(&self, address: &Address) -> Result<(), SSError> {
self.accounts.write().remove(address);
Ok(())
}
}
/// Dapp identifier /// Dapp identifier
pub type DappId = String; pub type DappId = String;
fn transient_sstore() -> EthMultiStore {
EthMultiStore::open(Box::new(MemoryDirectory::default())).expect("MemoryDirectory load always succeeds; qed")
}
type AccountToken = String;
/// Account management. /// Account management.
/// Responsible for unlocking accounts. /// Responsible for unlocking accounts.
pub struct AccountProvider { pub struct AccountProvider {
unlocked: Mutex<HashMap<Address, AccountData>>, unlocked: RwLock<HashMap<Address, AccountData>>,
sstore: Box<SecretStore>,
address_book: RwLock<AddressBook>, address_book: RwLock<AddressBook>,
dapps_settings: RwLock<DappsSettingsStore>, dapps_settings: RwLock<DappsSettingsStore>,
/// Accounts on disk
sstore: Box<SecretStore>,
/// Accounts unlocked with rolling tokens
transient_sstore: EthMultiStore,
} }
impl AccountProvider { impl AccountProvider {
/// Creates new account provider. /// Creates new account provider.
pub fn new(sstore: Box<SecretStore>) -> Self { pub fn new(sstore: Box<SecretStore>) -> Self {
AccountProvider { AccountProvider {
unlocked: Mutex::new(HashMap::new()), unlocked: RwLock::new(HashMap::new()),
address_book: RwLock::new(AddressBook::new(sstore.local_path().into())), address_book: RwLock::new(AddressBook::new(sstore.local_path().into())),
dapps_settings: RwLock::new(DappsSettingsStore::new(sstore.local_path().into())), dapps_settings: RwLock::new(DappsSettingsStore::new(sstore.local_path().into())),
sstore: sstore, sstore: sstore,
transient_sstore: transient_sstore(),
} }
} }
/// Creates not disk backed provider. /// Creates not disk backed provider.
pub fn transient_provider() -> Self { pub fn transient_provider() -> Self {
AccountProvider { AccountProvider {
unlocked: Mutex::new(HashMap::new()), unlocked: RwLock::new(HashMap::new()),
address_book: RwLock::new(AddressBook::transient()), address_book: RwLock::new(AddressBook::transient()),
dapps_settings: RwLock::new(DappsSettingsStore::transient()), dapps_settings: RwLock::new(DappsSettingsStore::transient()),
sstore: Box::new(EthStore::open(Box::new(NullDir::default())) sstore: Box::new(EthStore::open(Box::new(MemoryDirectory::default())).expect("MemoryDirectory load always succeeds; qed")),
.expect("NullDir load always succeeds; qed")) transient_sstore: transient_sstore(),
} }
} }
@ -167,10 +156,49 @@ impl AccountProvider {
Ok(accounts) Ok(accounts)
} }
/// Sets a whitelist of accounts exposed for unknown dapps.
/// `None` means that all accounts will be visible.
pub fn set_new_dapps_whitelist(&self, accounts: Option<Vec<Address>>) -> Result<(), Error> {
self.dapps_settings.write().set_policy(match accounts {
None => NewDappsPolicy::AllAccounts,
Some(accounts) => NewDappsPolicy::Whitelist(accounts),
});
Ok(())
}
/// Gets a whitelist of accounts exposed for unknown dapps.
/// `None` means that all accounts will be visible.
pub fn new_dapps_whitelist(&self) -> Result<Option<Vec<Address>>, Error> {
Ok(match self.dapps_settings.read().policy() {
NewDappsPolicy::AllAccounts => None,
NewDappsPolicy::Whitelist(accounts) => Some(accounts),
})
}
/// Gets a list of dapps recently requesting accounts.
pub fn recent_dapps(&self) -> Result<Vec<DappId>, Error> {
Ok(self.dapps_settings.read().recent_dapps())
}
/// Marks dapp as recently used.
pub fn note_dapp_used(&self, dapp: DappId) -> Result<(), Error> {
let mut dapps = self.dapps_settings.write();
dapps.mark_dapp_used(dapp.clone());
Ok(())
}
/// Gets addresses visile for dapp. /// Gets addresses visile for dapp.
pub fn dapps_addresses(&self, dapp: DappId) -> Result<Vec<Address>, Error> { pub fn dapps_addresses(&self, dapp: DappId) -> Result<Vec<Address>, Error> {
let accounts = self.dapps_settings.read().get(); let dapps = self.dapps_settings.read();
Ok(accounts.get(&dapp).map(|settings| settings.accounts.clone()).unwrap_or_else(Vec::new))
let accounts = dapps.settings().get(&dapp).map(|settings| settings.accounts.clone());
match accounts {
Some(accounts) => Ok(accounts),
None => match dapps.policy() {
NewDappsPolicy::AllAccounts => self.accounts(),
NewDappsPolicy::Whitelist(accounts) => Ok(accounts),
}
}
} }
/// Sets addresses visile for dapp. /// Sets addresses visile for dapp.
@ -231,11 +259,8 @@ impl AccountProvider {
/// Returns `true` if the password for `account` is `password`. `false` if not. /// Returns `true` if the password for `account` is `password`. `false` if not.
pub fn test_password(&self, account: &Address, password: &str) -> Result<bool, Error> { pub fn test_password(&self, account: &Address, password: &str) -> Result<bool, Error> {
match self.sstore.sign(account, password, &Default::default()) { self.sstore.test_password(account, password)
Ok(_) => Ok(true), .map_err(Into::into)
Err(SSError::InvalidPassword) => Ok(false),
Err(e) => Err(Error::SStore(e)),
}
} }
/// Permanently removes an account. /// Permanently removes an account.
@ -256,7 +281,7 @@ impl AccountProvider {
let _ = try!(self.sstore.sign(&account, &password, &Default::default())); let _ = try!(self.sstore.sign(&account, &password, &Default::default()));
// check if account is already unlocked pernamently, if it is, do nothing // check if account is already unlocked pernamently, if it is, do nothing
let mut unlocked = self.unlocked.lock(); let mut unlocked = self.unlocked.write();
if let Some(data) = unlocked.get(&account) { if let Some(data) = unlocked.get(&account) {
if let Unlock::Perm = data.unlock { if let Unlock::Perm = data.unlock {
return Ok(()) return Ok(())
@ -273,7 +298,7 @@ impl AccountProvider {
} }
fn password(&self, account: &Address) -> Result<String, Error> { fn password(&self, account: &Address) -> Result<String, Error> {
let mut unlocked = self.unlocked.lock(); let mut unlocked = self.unlocked.write();
let data = try!(unlocked.get(account).ok_or(Error::NotUnlocked)).clone(); let data = try!(unlocked.get(account).ok_or(Error::NotUnlocked)).clone();
if let Unlock::Temp = data.unlock { if let Unlock::Temp = data.unlock {
unlocked.remove(account).expect("data exists: so key must exist: qed"); unlocked.remove(account).expect("data exists: so key must exist: qed");
@ -304,7 +329,7 @@ impl AccountProvider {
/// Checks if given account is unlocked /// Checks if given account is unlocked
pub fn is_unlocked(&self, account: Address) -> bool { pub fn is_unlocked(&self, account: Address) -> bool {
let unlocked = self.unlocked.lock(); let unlocked = self.unlocked.read();
unlocked.get(&account).is_some() unlocked.get(&account).is_some()
} }
@ -314,6 +339,48 @@ impl AccountProvider {
Ok(try!(self.sstore.sign(&account, &password, &message))) Ok(try!(self.sstore.sign(&account, &password, &message)))
} }
/// Signs given message with supplied token. Returns a token to use in next signing within this session.
pub fn sign_with_token(&self, account: Address, token: AccountToken, message: Message) -> Result<(Signature, AccountToken), Error> {
let is_std_password = try!(self.sstore.test_password(&account, &token));
let new_token = random_string(16);
let signature = if is_std_password {
// Insert to transient store
try!(self.sstore.copy_account(&self.transient_sstore, &account, &token, &new_token));
// sign
try!(self.sstore.sign(&account, &token, &message))
} else {
// check transient store
try!(self.transient_sstore.change_password(&account, &token, &new_token));
// and sign
try!(self.transient_sstore.sign(&account, &new_token, &message))
};
Ok((signature, new_token))
}
/// Decrypts a message with given token. Returns a token to use in next operation for this account.
pub fn decrypt_with_token(&self, account: Address, token: AccountToken, shared_mac: &[u8], message: &[u8])
-> Result<(Vec<u8>, AccountToken), Error>
{
let is_std_password = try!(self.sstore.test_password(&account, &token));
let new_token = random_string(16);
let message = if is_std_password {
// Insert to transient store
try!(self.sstore.copy_account(&self.transient_sstore, &account, &token, &new_token));
// decrypt
try!(self.sstore.decrypt(&account, &token, shared_mac, message))
} else {
// check transient store
try!(self.transient_sstore.change_password(&account, &token, &new_token));
// and decrypt
try!(self.transient_sstore.decrypt(&account, &token, shared_mac, message))
};
Ok((message, new_token))
}
/// Decrypts a message. If password is not provided the account must be unlocked. /// Decrypts a message. If password is not provided the account must be unlocked.
pub fn decrypt(&self, account: Address, password: Option<String>, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error> { pub fn decrypt(&self, account: Address, password: Option<String>, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error> {
let password = try!(password.map(Ok).unwrap_or_else(|| self.password(&account))); let password = try!(password.map(Ok).unwrap_or_else(|| self.password(&account)));
@ -370,15 +437,33 @@ mod tests {
assert!(ap.unlock_account_timed(kp.address(), "test1".into(), 60000).is_err()); assert!(ap.unlock_account_timed(kp.address(), "test1".into(), 60000).is_err());
assert!(ap.unlock_account_timed(kp.address(), "test".into(), 60000).is_ok()); assert!(ap.unlock_account_timed(kp.address(), "test".into(), 60000).is_ok());
assert!(ap.sign(kp.address(), None, Default::default()).is_ok()); assert!(ap.sign(kp.address(), None, Default::default()).is_ok());
ap.unlocked.lock().get_mut(&kp.address()).unwrap().unlock = Unlock::Timed(Instant::now()); ap.unlocked.write().get_mut(&kp.address()).unwrap().unlock = Unlock::Timed(Instant::now());
assert!(ap.sign(kp.address(), None, Default::default()).is_err()); assert!(ap.sign(kp.address(), None, Default::default()).is_err());
} }
#[test]
fn should_sign_and_return_token() {
// given
let kp = Random.generate().unwrap();
let ap = AccountProvider::transient_provider();
assert!(ap.insert_account(kp.secret().clone(), "test").is_ok());
// when
let (_signature, token) = ap.sign_with_token(kp.address(), "test".into(), Default::default()).unwrap();
// then
ap.sign_with_token(kp.address(), token.clone(), Default::default())
.expect("First usage of token should be correct.");
assert!(ap.sign_with_token(kp.address(), token, Default::default()).is_err(), "Second usage of the same token should fail.");
}
#[test] #[test]
fn should_set_dapps_addresses() { fn should_set_dapps_addresses() {
// given // given
let ap = AccountProvider::transient_provider(); let ap = AccountProvider::transient_provider();
let app = "app1".to_owned(); let app = "app1".to_owned();
// set `AllAccounts` policy
ap.set_new_dapps_whitelist(None).unwrap();
// when // when
ap.set_dapps_addresses(app.clone(), vec![1.into(), 2.into()]).unwrap(); ap.set_dapps_addresses(app.clone(), vec![1.into(), 2.into()]).unwrap();
@ -386,4 +471,23 @@ mod tests {
// then // then
assert_eq!(ap.dapps_addresses(app.clone()).unwrap(), vec![1.into(), 2.into()]); assert_eq!(ap.dapps_addresses(app.clone()).unwrap(), vec![1.into(), 2.into()]);
} }
#[test]
fn should_set_dapps_policy() {
// given
let ap = AccountProvider::transient_provider();
let address = ap.new_account("test").unwrap();
// When returning nothing
ap.set_new_dapps_whitelist(Some(vec![])).unwrap();
assert_eq!(ap.dapps_addresses("app1".into()).unwrap(), vec![]);
// change to all
ap.set_new_dapps_whitelist(None).unwrap();
assert_eq!(ap.dapps_addresses("app1".into()).unwrap(), vec![address]);
// change to a whitelist
ap.set_new_dapps_whitelist(Some(vec![1.into()])).unwrap();
assert_eq!(ap.dapps_addresses("app1".into()).unwrap(), vec![1.into()]);
}
} }

View File

@ -17,11 +17,11 @@
//! Address Book and Dapps Settings Store //! Address Book and Dapps Settings Store
use std::{fs, fmt, hash, ops}; use std::{fs, fmt, hash, ops};
use std::collections::HashMap; use std::collections::{HashMap, VecDeque};
use std::path::PathBuf; use std::path::PathBuf;
use ethstore::ethkey::Address; use ethstore::ethkey::Address;
use ethjson::misc::{AccountMeta, DappsSettings as JsonSettings}; use ethjson::misc::{AccountMeta, DappsSettings as JsonSettings, NewDappsPolicy as JsonNewDappsPolicy};
use account_provider::DappId; use account_provider::DappId;
/// Disk-backed map from Address to String. Uses JSON. /// Disk-backed map from Address to String. Uses JSON.
@ -105,43 +105,106 @@ impl From<DappsSettings> for JsonSettings {
} }
} }
/// Dapps user settings
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum NewDappsPolicy {
AllAccounts,
Whitelist(Vec<Address>),
}
impl From<JsonNewDappsPolicy> for NewDappsPolicy {
fn from(s: JsonNewDappsPolicy) -> Self {
match s {
JsonNewDappsPolicy::AllAccounts => NewDappsPolicy::AllAccounts,
JsonNewDappsPolicy::Whitelist(accounts) => NewDappsPolicy::Whitelist(
accounts.into_iter().map(Into::into).collect()
),
}
}
}
impl From<NewDappsPolicy> for JsonNewDappsPolicy {
fn from(s: NewDappsPolicy) -> Self {
match s {
NewDappsPolicy::AllAccounts => JsonNewDappsPolicy::AllAccounts,
NewDappsPolicy::Whitelist(accounts) => JsonNewDappsPolicy::Whitelist(
accounts.into_iter().map(Into::into).collect()
),
}
}
}
const MAX_RECENT_DAPPS: usize = 10;
/// Disk-backed map from DappId to Settings. Uses JSON. /// Disk-backed map from DappId to Settings. Uses JSON.
pub struct DappsSettingsStore { pub struct DappsSettingsStore {
cache: DiskMap<DappId, DappsSettings>, /// Dapps Settings
settings: DiskMap<DappId, DappsSettings>,
/// New Dapps Policy
policy: DiskMap<String, NewDappsPolicy>,
/// Recently Accessed Dapps (transient)
recent: VecDeque<DappId>,
} }
impl DappsSettingsStore { impl DappsSettingsStore {
/// Creates new store at given directory path. /// Creates new store at given directory path.
pub fn new(path: String) -> Self { pub fn new(path: String) -> Self {
let mut r = DappsSettingsStore { let mut r = DappsSettingsStore {
cache: DiskMap::new(path, "dapps_accounts.json".into()) settings: DiskMap::new(path.clone(), "dapps_accounts.json".into()),
policy: DiskMap::new(path.clone(), "dapps_policy.json".into()),
recent: VecDeque::with_capacity(MAX_RECENT_DAPPS),
}; };
r.cache.revert(JsonSettings::read_dapps_settings); r.settings.revert(JsonSettings::read_dapps_settings);
r.policy.revert(JsonNewDappsPolicy::read_new_dapps_policy);
r r
} }
/// Creates transient store (no changes are saved to disk). /// Creates transient store (no changes are saved to disk).
pub fn transient() -> Self { pub fn transient() -> Self {
DappsSettingsStore { DappsSettingsStore {
cache: DiskMap::transient() settings: DiskMap::transient(),
policy: DiskMap::transient(),
recent: VecDeque::with_capacity(MAX_RECENT_DAPPS),
} }
} }
/// Get copy of the dapps settings /// Get copy of the dapps settings
pub fn get(&self) -> HashMap<DappId, DappsSettings> { pub fn settings(&self) -> HashMap<DappId, DappsSettings> {
self.cache.clone() self.settings.clone()
} }
fn save(&self) { /// Returns current new dapps policy
self.cache.save(JsonSettings::write_dapps_settings) pub fn policy(&self) -> NewDappsPolicy {
self.policy.get("default").cloned().unwrap_or(NewDappsPolicy::AllAccounts)
} }
/// Returns recent dapps (in order of last request)
pub fn recent_dapps(&self) -> Vec<DappId> {
self.recent.iter().cloned().collect()
}
/// Marks recent dapp as used
pub fn mark_dapp_used(&mut self, dapp: DappId) {
self.recent.retain(|id| id != &dapp);
self.recent.push_front(dapp);
while self.recent.len() > MAX_RECENT_DAPPS {
self.recent.pop_back();
}
}
/// Sets current new dapps policy
pub fn set_policy(&mut self, policy: NewDappsPolicy) {
self.policy.insert("default".into(), policy);
self.policy.save(JsonNewDappsPolicy::write_new_dapps_policy);
}
/// Sets accounts for specific dapp.
pub fn set_accounts(&mut self, id: DappId, accounts: Vec<Address>) { pub fn set_accounts(&mut self, id: DappId, accounts: Vec<Address>) {
{ {
let mut settings = self.cache.entry(id).or_insert_with(DappsSettings::default); let mut settings = self.settings.entry(id).or_insert_with(DappsSettings::default);
settings.accounts = accounts; settings.accounts = accounts;
} }
self.save(); self.settings.save(JsonSettings::write_dapps_settings);
} }
} }
@ -216,7 +279,7 @@ impl<K: hash::Hash + Eq, V> DiskMap<K, V> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{AddressBook, DappsSettingsStore, DappsSettings}; use super::{AddressBook, DappsSettingsStore, DappsSettings, NewDappsPolicy};
use std::collections::HashMap; use std::collections::HashMap;
use ethjson::misc::AccountMeta; use ethjson::misc::AccountMeta;
use devtools::RandomTempPath; use devtools::RandomTempPath;
@ -232,25 +295,6 @@ mod tests {
assert_eq!(b.get(), hash_map![1.into() => AccountMeta{name: "One".to_owned(), meta: "{1:1}".to_owned(), uuid: None}]); assert_eq!(b.get(), hash_map![1.into() => AccountMeta{name: "One".to_owned(), meta: "{1:1}".to_owned(), uuid: None}]);
} }
#[test]
fn should_save_and_reload_dapps_settings() {
// given
let temp = RandomTempPath::create_dir();
let path = temp.as_str().to_owned();
let mut b = DappsSettingsStore::new(path.clone());
// when
b.set_accounts("dappOne".into(), vec![1.into(), 2.into()]);
// then
let b = DappsSettingsStore::new(path);
assert_eq!(b.get(), hash_map![
"dappOne".into() => DappsSettings {
accounts: vec![1.into(), 2.into()],
}
]);
}
#[test] #[test]
fn should_remove_address() { fn should_remove_address() {
let temp = RandomTempPath::create_dir(); let temp = RandomTempPath::create_dir();
@ -268,4 +312,58 @@ mod tests {
3.into() => AccountMeta{name: "Three".to_owned(), meta: "{}".to_owned(), uuid: None} 3.into() => AccountMeta{name: "Three".to_owned(), meta: "{}".to_owned(), uuid: None}
]); ]);
} }
#[test]
fn should_save_and_reload_dapps_settings() {
// given
let temp = RandomTempPath::create_dir();
let path = temp.as_str().to_owned();
let mut b = DappsSettingsStore::new(path.clone());
// when
b.set_accounts("dappOne".into(), vec![1.into(), 2.into()]);
// then
let b = DappsSettingsStore::new(path);
assert_eq!(b.settings(), hash_map![
"dappOne".into() => DappsSettings {
accounts: vec![1.into(), 2.into()],
}
]);
}
#[test]
fn should_maintain_a_list_of_recent_dapps() {
let mut store = DappsSettingsStore::transient();
assert!(store.recent_dapps().is_empty(), "Initially recent dapps should be empty.");
store.mark_dapp_used("dapp1".into());
assert_eq!(store.recent_dapps(), vec!["dapp1".to_owned()]);
store.mark_dapp_used("dapp2".into());
assert_eq!(store.recent_dapps(), vec!["dapp2".to_owned(), "dapp1".to_owned()]);
store.mark_dapp_used("dapp1".into());
assert_eq!(store.recent_dapps(), vec!["dapp1".to_owned(), "dapp2".to_owned()]);
}
#[test]
fn should_store_dapps_policy() {
// given
let temp = RandomTempPath::create_dir();
let path = temp.as_str().to_owned();
let mut store = DappsSettingsStore::new(path.clone());
// Test default policy
assert_eq!(store.policy(), NewDappsPolicy::AllAccounts);
// when
store.set_policy(NewDappsPolicy::Whitelist(vec![1.into(), 2.into()]));
// then
let store = DappsSettingsStore::new(path);
assert_eq!(store.policy.clone(), hash_map![
"default".into() => NewDappsPolicy::Whitelist(vec![1.into(), 2.into()])
]);
}
} }

View File

@ -15,7 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
use ipc::IpcConfig; use ipc::IpcConfig;
use util::H256; use util::{H256, Bytes};
/// Represents what has to be handled by actor listening to chain events /// Represents what has to be handled by actor listening to chain events
#[ipc] #[ipc]
@ -27,6 +27,8 @@ pub trait ChainNotify : Send + Sync {
_enacted: Vec<H256>, _enacted: Vec<H256>,
_retracted: Vec<H256>, _retracted: Vec<H256>,
_sealed: Vec<H256>, _sealed: Vec<H256>,
// Block bytes.
_proposed: Vec<Bytes>,
_duration: u64) { _duration: u64) {
// does nothing by default // does nothing by default
} }
@ -41,6 +43,9 @@ pub trait ChainNotify : Send + Sync {
// does nothing by default // does nothing by default
} }
/// fires when chain broadcasts a message
fn broadcast(&self, _data: Vec<u8>) {}
/// fires when new transactions are received from a peer /// fires when new transactions are received from a peer
fn transactions_received(&self, fn transactions_received(&self,
_hashes: Vec<H256>, _hashes: Vec<H256>,

View File

@ -24,8 +24,8 @@ use time::precise_time_ns;
// util // util
use util::{Bytes, PerfTimer, Itertools, Mutex, RwLock, Hashable}; use util::{Bytes, PerfTimer, Itertools, Mutex, RwLock, Hashable};
use util::{journaldb, TrieFactory, Trie}; use util::{journaldb, TrieFactory, Trie};
use util::trie::TrieSpec;
use util::{U256, H256, Address, H2048, Uint, FixedHash}; use util::{U256, H256, Address, H2048, Uint, FixedHash};
use util::trie::TrieSpec;
use util::kvdb::*; use util::kvdb::*;
// other // other
@ -396,9 +396,10 @@ impl Client {
/// This is triggered by a message coming from a block queue when the block is ready for insertion /// This is triggered by a message coming from a block queue when the block is ready for insertion
pub fn import_verified_blocks(&self) -> usize { pub fn import_verified_blocks(&self) -> usize {
let max_blocks_to_import = 4; let max_blocks_to_import = 4;
let (imported_blocks, import_results, invalid_blocks, imported, duration, is_empty) = { let (imported_blocks, import_results, invalid_blocks, imported, proposed_blocks, duration, is_empty) = {
let mut imported_blocks = Vec::with_capacity(max_blocks_to_import); let mut imported_blocks = Vec::with_capacity(max_blocks_to_import);
let mut invalid_blocks = HashSet::new(); let mut invalid_blocks = HashSet::new();
let mut proposed_blocks = Vec::with_capacity(max_blocks_to_import);
let mut import_results = Vec::with_capacity(max_blocks_to_import); let mut import_results = Vec::with_capacity(max_blocks_to_import);
let _import_lock = self.import_lock.lock(); let _import_lock = self.import_lock.lock();
@ -417,12 +418,17 @@ impl Client {
continue; continue;
} }
if let Ok(closed_block) = self.check_and_close_block(&block) { if let Ok(closed_block) = self.check_and_close_block(&block) {
if self.engine.is_proposal(&block.header) {
self.block_queue.mark_as_good(&[header.hash()]);
proposed_blocks.push(block.bytes);
} else {
imported_blocks.push(header.hash()); imported_blocks.push(header.hash());
let route = self.commit_block(closed_block, &header.hash(), &block.bytes); let route = self.commit_block(closed_block, &header.hash(), &block.bytes);
import_results.push(route); import_results.push(route);
self.report.write().accrue_block(&block); self.report.write().accrue_block(&block);
}
} else { } else {
invalid_blocks.insert(header.hash()); invalid_blocks.insert(header.hash());
} }
@ -436,7 +442,7 @@ impl Client {
} }
let is_empty = self.block_queue.mark_as_good(&imported_blocks); let is_empty = self.block_queue.mark_as_good(&imported_blocks);
let duration_ns = precise_time_ns() - start; let duration_ns = precise_time_ns() - start;
(imported_blocks, import_results, invalid_blocks, imported, duration_ns, is_empty) (imported_blocks, import_results, invalid_blocks, imported, proposed_blocks, duration_ns, is_empty)
}; };
{ {
@ -454,6 +460,7 @@ impl Client {
enacted.clone(), enacted.clone(),
retracted.clone(), retracted.clone(),
Vec::new(), Vec::new(),
proposed_blocks.clone(),
duration, duration,
); );
}); });
@ -577,9 +584,10 @@ impl Client {
self.miner.clone() self.miner.clone()
} }
/// Used by PoA to try sealing on period change.
pub fn update_sealing(&self) { /// Replace io channel. Useful for testing.
self.miner.update_sealing(self) pub fn set_io_channel(&self, io_channel: IoChannel<ClientIoMessage>) {
*self.io_channel.lock() = io_channel;
} }
/// Attempt to get a copy of a specific block's final state. /// Attempt to get a copy of a specific block's final state.
@ -1290,6 +1298,18 @@ impl BlockChainClient for Client {
self.miner.pending_transactions(self.chain.read().best_block_number()) self.miner.pending_transactions(self.chain.read().best_block_number())
} }
fn queue_consensus_message(&self, message: Bytes) {
let channel = self.io_channel.lock().clone();
if let Err(e) = channel.send(ClientIoMessage::NewMessage(message)) {
debug!("Ignoring the message, error queueing: {}", e);
}
}
fn broadcast_consensus_message(&self, message: Bytes) {
self.notify(|notify| notify.broadcast(message.clone()));
}
fn signing_network_id(&self) -> Option<u64> { fn signing_network_id(&self) -> Option<u64> {
self.engine.signing_network_id(&self.latest_env_info()) self.engine.signing_network_id(&self.latest_env_info())
} }
@ -1314,7 +1334,6 @@ impl BlockChainClient for Client {
} }
impl MiningBlockChainClient for Client { impl MiningBlockChainClient for Client {
fn latest_schedule(&self) -> Schedule { fn latest_schedule(&self) -> Schedule {
self.engine.schedule(&self.latest_env_info()) self.engine.schedule(&self.latest_env_info())
} }
@ -1357,6 +1376,30 @@ impl MiningBlockChainClient for Client {
&self.factories.vm &self.factories.vm
} }
fn update_sealing(&self) {
self.miner.update_sealing(self)
}
fn submit_seal(&self, block_hash: H256, seal: Vec<Bytes>) {
if self.miner.submit_seal(self, block_hash, seal).is_err() {
warn!(target: "poa", "Wrong internal seal submission!")
}
}
fn broadcast_proposal_block(&self, block: SealedBlock) {
self.notify(|notify| {
notify.new_blocks(
vec![],
vec![],
vec![],
vec![],
vec![],
vec![block.rlp_bytes()],
0,
);
});
}
fn import_sealed_block(&self, block: SealedBlock) -> ImportResult { fn import_sealed_block(&self, block: SealedBlock) -> ImportResult {
let h = block.header().hash(); let h = block.header().hash();
let start = precise_time_ns(); let start = precise_time_ns();
@ -1381,6 +1424,7 @@ impl MiningBlockChainClient for Client {
enacted.clone(), enacted.clone(),
retracted.clone(), retracted.clone(),
vec![h.clone()], vec![h.clone()],
vec![],
precise_time_ns() - start, precise_time_ns() - start,
); );
}); });
@ -1416,6 +1460,12 @@ impl ::client::ProvingBlockChainClient for Client {
} }
} }
impl Drop for Client {
fn drop(&mut self) {
self.engine.stop();
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {

View File

@ -90,6 +90,8 @@ pub struct TestBlockChainClient {
pub ancient_block: RwLock<Option<(H256, u64)>>, pub ancient_block: RwLock<Option<(H256, u64)>>,
/// First block info. /// First block info.
pub first_block: RwLock<Option<(H256, u64)>>, pub first_block: RwLock<Option<(H256, u64)>>,
/// Traces to return
pub traces: RwLock<Option<Vec<LocalizedTrace>>>,
} }
/// Used for generating test client blocks. /// Used for generating test client blocks.
@ -151,6 +153,7 @@ impl TestBlockChainClient {
latest_block_timestamp: RwLock::new(10_000_000), latest_block_timestamp: RwLock::new(10_000_000),
ancient_block: RwLock::new(None), ancient_block: RwLock::new(None),
first_block: RwLock::new(None), first_block: RwLock::new(None),
traces: RwLock::new(None),
}; };
client.add_blocks(1, EachBlockWith::Nothing); // add genesis block client.add_blocks(1, EachBlockWith::Nothing); // add genesis block
client.genesis_hash = client.last_hash.read().clone(); client.genesis_hash = client.last_hash.read().clone();
@ -360,6 +363,18 @@ impl MiningBlockChainClient for TestBlockChainClient {
fn import_sealed_block(&self, _block: SealedBlock) -> ImportResult { fn import_sealed_block(&self, _block: SealedBlock) -> ImportResult {
Ok(H256::default()) Ok(H256::default())
} }
fn broadcast_proposal_block(&self, _block: SealedBlock) {}
fn update_sealing(&self) {
self.miner.update_sealing(self)
}
fn submit_seal(&self, block_hash: H256, seal: Vec<Bytes>) {
if self.miner.submit_seal(self, block_hash, seal).is_err() {
warn!(target: "poa", "Wrong internal seal submission!")
}
}
} }
impl BlockChainClient for TestBlockChainClient { impl BlockChainClient for TestBlockChainClient {
@ -642,19 +657,19 @@ impl BlockChainClient for TestBlockChainClient {
} }
fn filter_traces(&self, _filter: TraceFilter) -> Option<Vec<LocalizedTrace>> { fn filter_traces(&self, _filter: TraceFilter) -> Option<Vec<LocalizedTrace>> {
unimplemented!(); self.traces.read().clone()
} }
fn trace(&self, _trace: TraceId) -> Option<LocalizedTrace> { fn trace(&self, _trace: TraceId) -> Option<LocalizedTrace> {
unimplemented!(); self.traces.read().clone().and_then(|vec| vec.into_iter().next())
} }
fn transaction_traces(&self, _trace: TransactionId) -> Option<Vec<LocalizedTrace>> { fn transaction_traces(&self, _trace: TransactionId) -> Option<Vec<LocalizedTrace>> {
unimplemented!(); self.traces.read().clone()
} }
fn block_traces(&self, _trace: BlockId) -> Option<Vec<LocalizedTrace>> { fn block_traces(&self, _trace: BlockId) -> Option<Vec<LocalizedTrace>> {
unimplemented!(); self.traces.read().clone()
} }
fn queue_transactions(&self, transactions: Vec<Bytes>, _peer_id: usize) { fn queue_transactions(&self, transactions: Vec<Bytes>, _peer_id: usize) {
@ -663,6 +678,12 @@ impl BlockChainClient for TestBlockChainClient {
self.miner.import_external_transactions(self, txs); self.miner.import_external_transactions(self, txs);
} }
fn queue_consensus_message(&self, message: Bytes) {
self.spec.engine.handle_message(&message).unwrap();
}
fn broadcast_consensus_message(&self, _message: Bytes) {}
fn pending_transactions(&self) -> Vec<SignedTransaction> { fn pending_transactions(&self) -> Vec<SignedTransaction> {
self.miner.pending_transactions(self.chain_info().best_block_number) self.miner.pending_transactions(self.chain_info().best_block_number)
} }

View File

@ -202,6 +202,12 @@ pub trait BlockChainClient : Sync + Send {
/// Queue transactions for importing. /// Queue transactions for importing.
fn queue_transactions(&self, transactions: Vec<Bytes>, peer_id: usize); fn queue_transactions(&self, transactions: Vec<Bytes>, peer_id: usize);
/// Queue conensus engine message.
fn queue_consensus_message(&self, message: Bytes);
/// Used by PoA to communicate with peers.
fn broadcast_consensus_message(&self, message: Bytes);
/// list all transactions /// list all transactions
fn pending_transactions(&self) -> Vec<SignedTransaction>; fn pending_transactions(&self) -> Vec<SignedTransaction>;
@ -273,6 +279,15 @@ pub trait MiningBlockChainClient: BlockChainClient {
/// Returns EvmFactory. /// Returns EvmFactory.
fn vm_factory(&self) -> &EvmFactory; fn vm_factory(&self) -> &EvmFactory;
/// Used by PoA to try sealing on period change.
fn update_sealing(&self);
/// Used by PoA to submit gathered signatures.
fn submit_seal(&self, block_hash: H256, seal: Vec<Bytes>);
/// Broadcast a block proposal.
fn broadcast_proposal_block(&self, block: SealedBlock);
/// Import sealed block. Skips all verifications. /// Import sealed block. Skips all verifications.
fn import_sealed_block(&self, block: SealedBlock) -> ImportResult; fn import_sealed_block(&self, block: SealedBlock) -> ImportResult;

View File

@ -25,7 +25,7 @@ use rlp::{UntrustedRlp, Rlp, View, encode};
use account_provider::AccountProvider; use account_provider::AccountProvider;
use block::*; use block::*;
use spec::CommonParams; use spec::CommonParams;
use engines::Engine; use engines::{Engine, Seal, EngineError};
use header::Header; use header::Header;
use error::{Error, BlockError}; use error::{Error, BlockError};
use blockchain::extras::BlockDetails; use blockchain::extras::BlockDetails;
@ -225,8 +225,8 @@ impl Engine for AuthorityRound {
/// ///
/// This operation is synchronous and may (quite reasonably) not be available, in which `false` will /// This operation is synchronous and may (quite reasonably) not be available, in which `false` will
/// be returned. /// be returned.
fn generate_seal(&self, block: &ExecutedBlock) -> Option<Vec<Bytes>> { fn generate_seal(&self, block: &ExecutedBlock) -> Seal {
if self.proposed.load(AtomicOrdering::SeqCst) { return None; } if self.proposed.load(AtomicOrdering::SeqCst) { return Seal::None; }
let header = block.header(); let header = block.header();
let step = self.step(); let step = self.step();
if self.is_step_proposer(step, header.author()) { if self.is_step_proposer(step, header.author()) {
@ -235,7 +235,8 @@ impl Engine for AuthorityRound {
if let Ok(signature) = ap.sign(*header.author(), self.password.read().clone(), header.bare_hash()) { if let Ok(signature) = ap.sign(*header.author(), self.password.read().clone(), header.bare_hash()) {
trace!(target: "poa", "generate_seal: Issuing a block for step {}.", step); trace!(target: "poa", "generate_seal: Issuing a block for step {}.", step);
self.proposed.store(true, AtomicOrdering::SeqCst); self.proposed.store(true, AtomicOrdering::SeqCst);
return Some(vec![encode(&step).to_vec(), encode(&(&*signature as &[u8])).to_vec()]); let rlps = vec![encode(&step).to_vec(), encode(&(&*signature as &[u8])).to_vec()];
return Seal::Regular(rlps);
} else { } else {
warn!(target: "poa", "generate_seal: FAIL: Accounts secret key unavailable."); warn!(target: "poa", "generate_seal: FAIL: Accounts secret key unavailable.");
} }
@ -245,7 +246,7 @@ impl Engine for AuthorityRound {
} else { } else {
trace!(target: "poa", "generate_seal: Not a proposer for step {}.", step); trace!(target: "poa", "generate_seal: Not a proposer for step {}.", step);
} }
None Seal::None
} }
/// Check the number of seal fields. /// Check the number of seal fields.
@ -288,7 +289,7 @@ impl Engine for AuthorityRound {
// Check if parent is from a previous step. // Check if parent is from a previous step.
if step == try!(header_step(parent)) { if step == try!(header_step(parent)) {
trace!(target: "poa", "Multiple blocks proposed for step {}.", step); trace!(target: "poa", "Multiple blocks proposed for step {}.", step);
try!(Err(BlockError::DoubleVote(header.author().clone()))); try!(Err(EngineError::DoubleVote(header.author().clone())));
} }
let gas_limit_divisor = self.our_params.gas_limit_bound_divisor; let gas_limit_divisor = self.our_params.gas_limit_bound_divisor;
@ -347,6 +348,7 @@ mod tests {
use tests::helpers::*; use tests::helpers::*;
use account_provider::AccountProvider; use account_provider::AccountProvider;
use spec::Spec; use spec::Spec;
use engines::Seal;
#[test] #[test]
fn has_valid_metadata() { fn has_valid_metadata() {
@ -416,17 +418,17 @@ mod tests {
let b2 = b2.close_and_lock(); let b2 = b2.close_and_lock();
engine.set_signer(addr1, "1".into()); engine.set_signer(addr1, "1".into());
if let Some(seal) = engine.generate_seal(b1.block()) { if let Seal::Regular(seal) = engine.generate_seal(b1.block()) {
assert!(b1.clone().try_seal(engine, seal).is_ok()); assert!(b1.clone().try_seal(engine, seal).is_ok());
// Second proposal is forbidden. // Second proposal is forbidden.
assert!(engine.generate_seal(b1.block()).is_none()); assert!(engine.generate_seal(b1.block()) == Seal::None);
} }
engine.set_signer(addr2, "2".into()); engine.set_signer(addr2, "2".into());
if let Some(seal) = engine.generate_seal(b2.block()) { if let Seal::Regular(seal) = engine.generate_seal(b2.block()) {
assert!(b2.clone().try_seal(engine, seal).is_ok()); assert!(b2.clone().try_seal(engine, seal).is_ok());
// Second proposal is forbidden. // Second proposal is forbidden.
assert!(engine.generate_seal(b2.block()).is_none()); assert!(engine.generate_seal(b2.block()) == Seal::None);
} }
} }

View File

@ -21,7 +21,7 @@ use account_provider::AccountProvider;
use block::*; use block::*;
use builtin::Builtin; use builtin::Builtin;
use spec::CommonParams; use spec::CommonParams;
use engines::Engine; use engines::{Engine, Seal};
use env_info::EnvInfo; use env_info::EnvInfo;
use error::{BlockError, Error}; use error::{BlockError, Error};
use evm::Schedule; use evm::Schedule;
@ -112,20 +112,20 @@ impl Engine for BasicAuthority {
/// ///
/// This operation is synchronous and may (quite reasonably) not be available, in which `false` will /// This operation is synchronous and may (quite reasonably) not be available, in which `false` will
/// be returned. /// be returned.
fn generate_seal(&self, block: &ExecutedBlock) -> Option<Vec<Bytes>> { fn generate_seal(&self, block: &ExecutedBlock) -> Seal {
if let Some(ref ap) = *self.account_provider.lock() { if let Some(ref ap) = *self.account_provider.lock() {
let header = block.header(); let header = block.header();
let message = header.bare_hash(); let message = header.bare_hash();
// account should be pernamently unlocked, otherwise sealing will fail // account should be pernamently unlocked, otherwise sealing will fail
if let Ok(signature) = ap.sign(*block.header().author(), self.password.read().clone(), message) { if let Ok(signature) = ap.sign(*block.header().author(), self.password.read().clone(), message) {
return Some(vec![::rlp::encode(&(&*signature as &[u8])).to_vec()]); return Seal::Regular(vec![::rlp::encode(&(&*signature as &[u8])).to_vec()]);
} else { } else {
trace!(target: "basicauthority", "generate_seal: FAIL: accounts secret key unavailable"); trace!(target: "basicauthority", "generate_seal: FAIL: accounts secret key unavailable");
} }
} else { } else {
trace!(target: "basicauthority", "generate_seal: FAIL: accounts not provided"); trace!(target: "basicauthority", "generate_seal: FAIL: accounts not provided");
} }
None Seal::None
} }
fn verify_block_basic(&self, header: &Header, _block: Option<&[u8]>) -> result::Result<(), Error> { fn verify_block_basic(&self, header: &Header, _block: Option<&[u8]>) -> result::Result<(), Error> {
@ -199,6 +199,7 @@ mod tests {
use account_provider::AccountProvider; use account_provider::AccountProvider;
use header::Header; use header::Header;
use spec::Spec; use spec::Spec;
use engines::Seal;
/// Create a new test chain spec with `BasicAuthority` consensus engine. /// Create a new test chain spec with `BasicAuthority` consensus engine.
fn new_test_authority() -> Spec { fn new_test_authority() -> Spec {
@ -269,9 +270,10 @@ mod tests {
let last_hashes = Arc::new(vec![genesis_header.hash()]); let last_hashes = Arc::new(vec![genesis_header.hash()]);
let b = OpenBlock::new(engine, Default::default(), false, db, &genesis_header, last_hashes, addr, (3141562.into(), 31415620.into()), vec![]).unwrap(); let b = OpenBlock::new(engine, Default::default(), false, db, &genesis_header, last_hashes, addr, (3141562.into(), 31415620.into()), vec![]).unwrap();
let b = b.close_and_lock(); let b = b.close_and_lock();
let seal = engine.generate_seal(b.block()).unwrap(); if let Seal::Regular(seal) = engine.generate_seal(b.block()) {
assert!(b.try_seal(engine, seal).is_ok()); assert!(b.try_seal(engine, seal).is_ok());
} }
}
#[test] #[test]
fn seals_internally() { fn seals_internally() {

View File

@ -17,12 +17,11 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use util::Address; use util::Address;
use builtin::Builtin; use builtin::Builtin;
use engines::Engine; use engines::{Engine, Seal};
use env_info::EnvInfo; use env_info::EnvInfo;
use spec::CommonParams; use spec::CommonParams;
use evm::Schedule; use evm::Schedule;
use block::ExecutedBlock; use block::ExecutedBlock;
use util::Bytes;
/// An engine which does not provide any consensus mechanism, just seals blocks internally. /// An engine which does not provide any consensus mechanism, just seals blocks internally.
pub struct InstantSeal { pub struct InstantSeal {
@ -54,13 +53,13 @@ impl Engine for InstantSeal {
} }
fn schedule(&self, _env_info: &EnvInfo) -> Schedule { fn schedule(&self, _env_info: &EnvInfo) -> Schedule {
Schedule::new_post_eip150(usize::max_value(), false, false, false) Schedule::new_post_eip150(usize::max_value(), true, true, true)
} }
fn is_sealer(&self, _author: &Address) -> Option<bool> { Some(true) } fn is_sealer(&self, _author: &Address) -> Option<bool> { Some(true) }
fn generate_seal(&self, _block: &ExecutedBlock) -> Option<Vec<Bytes>> { fn generate_seal(&self, _block: &ExecutedBlock) -> Seal {
Some(Vec::new()) Seal::Regular(Vec::new())
} }
} }
@ -72,6 +71,7 @@ mod tests {
use spec::Spec; use spec::Spec;
use header::Header; use header::Header;
use block::*; use block::*;
use engines::Seal;
#[test] #[test]
fn instant_can_seal() { fn instant_can_seal() {
@ -84,9 +84,10 @@ mod tests {
let last_hashes = Arc::new(vec![genesis_header.hash()]); let last_hashes = Arc::new(vec![genesis_header.hash()]);
let b = OpenBlock::new(engine, Default::default(), false, db, &genesis_header, last_hashes, Address::default(), (3141562.into(), 31415620.into()), vec![]).unwrap(); let b = OpenBlock::new(engine, Default::default(), false, db, &genesis_header, last_hashes, Address::default(), (3141562.into(), 31415620.into()), vec![]).unwrap();
let b = b.close_and_lock(); let b = b.close_and_lock();
let seal = engine.generate_seal(b.block()).unwrap(); if let Seal::Regular(seal) = engine.generate_seal(b.block()) {
assert!(b.try_seal(engine, seal).is_ok()); assert!(b.try_seal(engine, seal).is_ok());
} }
}
#[test] #[test]
fn instant_cant_verify() { fn instant_cant_verify() {

View File

@ -20,11 +20,13 @@ mod null_engine;
mod instant_seal; mod instant_seal;
mod basic_authority; mod basic_authority;
mod authority_round; mod authority_round;
mod tendermint;
pub use self::null_engine::NullEngine; pub use self::null_engine::NullEngine;
pub use self::instant_seal::InstantSeal; pub use self::instant_seal::InstantSeal;
pub use self::basic_authority::BasicAuthority; pub use self::basic_authority::BasicAuthority;
pub use self::authority_round::AuthorityRound; pub use self::authority_round::AuthorityRound;
pub use self::tendermint::Tendermint;
use util::*; use util::*;
use account_provider::AccountProvider; use account_provider::AccountProvider;
@ -42,6 +44,47 @@ use ethereum::ethash;
use blockchain::extras::BlockDetails; use blockchain::extras::BlockDetails;
use views::HeaderView; use views::HeaderView;
/// Voting errors.
#[derive(Debug)]
pub enum EngineError {
/// Signature does not belong to an authority.
NotAuthorized(Address),
/// The same author issued different votes at the same step.
DoubleVote(Address),
/// The received block is from an incorrect proposer.
NotProposer(Mismatch<Address>),
/// Message was not expected.
UnexpectedMessage,
/// Seal field has an unexpected size.
BadSealFieldSize(OutOfBounds<usize>),
}
impl fmt::Display for EngineError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::EngineError::*;
let msg = match *self {
DoubleVote(ref address) => format!("Author {} issued too many blocks.", address),
NotProposer(ref mis) => format!("Author is not a current proposer: {}", mis),
NotAuthorized(ref address) => format!("Signer {} is not authorized.", address),
UnexpectedMessage => "This Engine should not be fed messages.".into(),
BadSealFieldSize(ref oob) => format!("Seal field has an unexpected length: {}", oob),
};
f.write_fmt(format_args!("Engine error ({})", msg))
}
}
/// Seal type.
#[derive(Debug, PartialEq, Eq)]
pub enum Seal {
/// Proposal seal; should be broadcasted, but not inserted into blockchain.
Proposal(Vec<Bytes>),
/// Regular block seal; should be part of the blockchain.
Regular(Vec<Bytes>),
/// Engine does generate seal for this block right now.
None,
}
/// A consensus mechanism for the chain. Generally either proof-of-work or proof-of-stake-based. /// A consensus mechanism for the chain. Generally either proof-of-work or proof-of-stake-based.
/// Provides hooks into each of the major parts of block import. /// Provides hooks into each of the major parts of block import.
pub trait Engine : Sync + Send { pub trait Engine : Sync + Send {
@ -94,7 +137,7 @@ pub trait Engine : Sync + Send {
/// ///
/// This operation is synchronous and may (quite reasonably) not be available, in which None will /// This operation is synchronous and may (quite reasonably) not be available, in which None will
/// be returned. /// be returned.
fn generate_seal(&self, _block: &ExecutedBlock) -> Option<Vec<Bytes>> { None } fn generate_seal(&self, _block: &ExecutedBlock) -> Seal { Seal::None }
/// Phase 1 quick block verification. Only does checks that are cheap. `block` (the header's full block) /// Phase 1 quick block verification. Only does checks that are cheap. `block` (the header's full block)
/// may be provided for additional checks. Returns either a null `Ok` or a general error detailing the problem with import. /// may be provided for additional checks. Returns either a null `Ok` or a general error detailing the problem with import.
@ -133,6 +176,10 @@ pub trait Engine : Sync + Send {
header.set_gas_limit(parent.gas_limit().clone()); header.set_gas_limit(parent.gas_limit().clone());
} }
/// Handle any potential consensus messages;
/// updating consensus state and potentially issuing a new one.
fn handle_message(&self, _message: &[u8]) -> Result<(), Error> { Err(EngineError::UnexpectedMessage.into()) }
// TODO: builtin contract routing - to do this properly, it will require removing the built-in configuration-reading logic // TODO: builtin contract routing - to do this properly, it will require removing the built-in configuration-reading logic
// from Spec into here and removing the Spec::builtins field. // from Spec into here and removing the Spec::builtins field.
/// Determine whether a particular address is a builtin contract. /// Determine whether a particular address is a builtin contract.
@ -153,9 +200,16 @@ pub trait Engine : Sync + Send {
ethash::is_new_best_block(best_total_difficulty, parent_details, new_header) ethash::is_new_best_block(best_total_difficulty, parent_details, new_header)
} }
/// Find out if the block is a proposal block and should not be inserted into the DB.
/// Takes a header of a fully verified block.
fn is_proposal(&self, _verified_header: &Header) -> bool { false }
/// Register an account which signs consensus messages. /// Register an account which signs consensus messages.
fn set_signer(&self, _address: Address, _password: String) {} fn set_signer(&self, _address: Address, _password: String) {}
/// Stops any services that the may hold the Engine and makes it safe to drop.
fn stop(&self) {}
/// Add a channel for communication with Client which can be used for sealing. /// Add a channel for communication with Client which can be used for sealing.
fn register_message_channel(&self, _message_channel: IoChannel<ClientIoMessage>) {} fn register_message_channel(&self, _message_channel: IoChannel<ClientIoMessage>) {}

View File

@ -0,0 +1,279 @@
// 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 <http://www.gnu.org/licenses/>.
//! Tendermint message handling.
use util::*;
use super::{Height, Round, BlockHash, Step};
use error::Error;
use header::Header;
use rlp::*;
use ethkey::{recover, public_to_address};
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct ConsensusMessage {
pub signature: H520,
pub height: Height,
pub round: Round,
pub step: Step,
pub block_hash: Option<BlockHash>,
}
fn consensus_round(header: &Header) -> Result<Round, ::rlp::DecoderError> {
let round_rlp = header.seal().get(0).expect("seal passed basic verification; seal has 3 fields; qed");
UntrustedRlp::new(round_rlp.as_slice()).as_val()
}
impl ConsensusMessage {
pub fn new(signature: H520, height: Height, round: Round, step: Step, block_hash: Option<BlockHash>) -> Self {
ConsensusMessage {
signature: signature,
height: height,
round: round,
step: step,
block_hash: block_hash,
}
}
pub fn new_proposal(header: &Header) -> Result<Self, ::rlp::DecoderError> {
Ok(ConsensusMessage {
signature: try!(UntrustedRlp::new(header.seal().get(1).expect("seal passed basic verification; seal has 3 fields; qed").as_slice()).as_val()),
height: header.number() as Height,
round: try!(consensus_round(header)),
step: Step::Propose,
block_hash: Some(header.bare_hash()),
})
}
pub fn new_commit(proposal: &ConsensusMessage, signature: H520) -> Self {
ConsensusMessage {
signature: signature,
height: proposal.height,
round: proposal.round,
step: Step::Precommit,
block_hash: proposal.block_hash,
}
}
pub fn is_height(&self, height: Height) -> bool {
self.height == height
}
pub fn is_round(&self, height: Height, round: Round) -> bool {
self.height == height && self.round == round
}
pub fn is_step(&self, height: Height, round: Round, step: Step) -> bool {
self.height == height && self.round == round && self.step == step
}
pub fn is_block_hash(&self, h: Height, r: Round, s: Step, block_hash: Option<BlockHash>) -> bool {
self.height == h && self.round == r && self.step == s && self.block_hash == block_hash
}
pub fn is_aligned(&self, m: &ConsensusMessage) -> bool {
self.is_block_hash(m.height, m.round, m.step, m.block_hash)
}
pub fn verify(&self) -> Result<Address, Error> {
let full_rlp = ::rlp::encode(self);
let block_info = Rlp::new(&full_rlp).at(1);
let public_key = try!(recover(&self.signature.into(), &block_info.as_raw().sha3()));
Ok(public_to_address(&public_key))
}
pub fn precommit_hash(&self) -> H256 {
message_info_rlp(self.height, self.round, Step::Precommit, self.block_hash).sha3()
}
}
impl PartialOrd for ConsensusMessage {
fn partial_cmp(&self, m: &ConsensusMessage) -> Option<Ordering> {
Some(self.cmp(m))
}
}
impl Step {
fn number(&self) -> u8 {
match *self {
Step::Propose => 0,
Step::Prevote => 1,
Step::Precommit => 2,
Step::Commit => 3,
}
}
}
impl Ord for ConsensusMessage {
fn cmp(&self, m: &ConsensusMessage) -> Ordering {
if self.height != m.height {
self.height.cmp(&m.height)
} else if self.round != m.round {
self.round.cmp(&m.round)
} else if self.step != m.step {
self.step.number().cmp(&m.step.number())
} else {
self.signature.cmp(&m.signature)
}
}
}
impl Decodable for Step {
fn decode<D>(decoder: &D) -> Result<Self, DecoderError> where D: Decoder {
match try!(decoder.as_rlp().as_val()) {
0u8 => Ok(Step::Propose),
1 => Ok(Step::Prevote),
2 => Ok(Step::Precommit),
_ => Err(DecoderError::Custom("Invalid step.")),
}
}
}
impl Encodable for Step {
fn rlp_append(&self, s: &mut RlpStream) {
s.append(&self.number());
}
}
/// (signature, height, round, step, block_hash)
impl Decodable for ConsensusMessage {
fn decode<D>(decoder: &D) -> Result<Self, DecoderError> where D: Decoder {
let rlp = decoder.as_rlp();
let m = try!(rlp.at(1));
let block_message: H256 = try!(m.val_at(3));
Ok(ConsensusMessage {
signature: try!(rlp.val_at(0)),
height: try!(m.val_at(0)),
round: try!(m.val_at(1)),
step: try!(m.val_at(2)),
block_hash: match block_message.is_zero() {
true => None,
false => Some(block_message),
}
})
}
}
impl Encodable for ConsensusMessage {
fn rlp_append(&self, s: &mut RlpStream) {
let info = message_info_rlp(self.height, self.round, self.step, self.block_hash);
s.begin_list(2)
.append(&self.signature)
.append_raw(&info, 1);
}
}
pub fn message_info_rlp(height: Height, round: Round, step: Step, block_hash: Option<BlockHash>) -> Bytes {
// TODO: figure out whats wrong with nested list encoding
let mut s = RlpStream::new_list(5);
s.append(&height).append(&round).append(&step).append(&block_hash.unwrap_or_else(H256::zero));
s.out()
}
pub fn message_full_rlp(signature: &H520, vote_info: &Bytes) -> Bytes {
let mut s = RlpStream::new_list(2);
s.append(signature).append_raw(vote_info, 1);
s.out()
}
#[cfg(test)]
mod tests {
use util::*;
use rlp::*;
use super::super::Step;
use super::*;
use account_provider::AccountProvider;
use header::Header;
#[test]
fn encode_decode() {
let message = ConsensusMessage {
signature: H520::default(),
height: 10,
round: 123,
step: Step::Precommit,
block_hash: Some("1".sha3())
};
let raw_rlp = ::rlp::encode(&message).to_vec();
let rlp = Rlp::new(&raw_rlp);
assert_eq!(message, rlp.as_val());
let message = ConsensusMessage {
signature: H520::default(),
height: 1314,
round: 0,
step: Step::Prevote,
block_hash: None
};
let raw_rlp = ::rlp::encode(&message);
let rlp = Rlp::new(&raw_rlp);
assert_eq!(message, rlp.as_val());
}
#[test]
fn generate_and_verify() {
let tap = Arc::new(AccountProvider::transient_provider());
let addr = tap.insert_account("0".sha3(), "0").unwrap();
tap.unlock_account_permanently(addr, "0".into()).unwrap();
let mi = message_info_rlp(123, 2, Step::Precommit, Some(H256::default()));
let raw_rlp = message_full_rlp(&tap.sign(addr, None, mi.sha3()).unwrap().into(), &mi);
let rlp = UntrustedRlp::new(&raw_rlp);
let message: ConsensusMessage = rlp.as_val().unwrap();
match message.verify() { Ok(a) if a == addr => {}, _ => panic!(), };
}
#[test]
fn proposal_message() {
let mut header = Header::default();
let seal = vec![
::rlp::encode(&0u8).to_vec(),
::rlp::encode(&H520::default()).to_vec(),
Vec::new()
];
header.set_seal(seal);
let message = ConsensusMessage::new_proposal(&header).unwrap();
assert_eq!(
message,
ConsensusMessage {
signature: Default::default(),
height: 0,
round: 0,
step: Step::Propose,
block_hash: Some(header.bare_hash())
}
);
}
#[test]
fn message_info_from_header() {
let header = Header::default();
let pro = ConsensusMessage {
signature: Default::default(),
height: 0,
round: 0,
step: Step::Propose,
block_hash: Some(header.bare_hash())
};
let pre = message_info_rlp(0, 0, Step::Precommit, Some(header.bare_hash()));
assert_eq!(pro.precommit_hash(), pre.sha3());
}
}

View File

@ -0,0 +1,962 @@
// 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 <http://www.gnu.org/licenses/>.
/// Tendermint BFT consensus engine with round robin proof-of-authority.
/// At each blockchain `Height` there can be multiple `Round`s of voting.
/// Signatures always sign `Height`, `Round`, `Step` and `BlockHash` which is a block hash without seal.
/// First a block with `Seal::Proposal` is issued by the designated proposer.
/// Next the `Round` proceeds through `Prevote` and `Precommit` `Step`s.
/// Block is issued when there is enough `Precommit` votes collected on a particular block at the end of a `Round`.
/// Once enough votes have been gathered the proposer issues that block in the `Commit` step.
mod message;
mod transition;
mod params;
mod vote_collector;
use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
use util::*;
use error::{Error, BlockError};
use header::Header;
use builtin::Builtin;
use env_info::EnvInfo;
use transaction::SignedTransaction;
use rlp::{UntrustedRlp, View};
use ethkey::{recover, public_to_address};
use account_provider::AccountProvider;
use block::*;
use spec::CommonParams;
use engines::{Engine, Seal, EngineError};
use blockchain::extras::BlockDetails;
use views::HeaderView;
use evm::Schedule;
use io::{IoService, IoChannel};
use service::ClientIoMessage;
use self::message::*;
use self::transition::TransitionHandler;
use self::params::TendermintParams;
use self::vote_collector::VoteCollector;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Step {
Propose,
Prevote,
Precommit,
Commit
}
impl Step {
pub fn is_pre(self) -> bool {
match self {
Step::Prevote | Step::Precommit => true,
_ => false,
}
}
}
pub type Height = usize;
pub type Round = usize;
pub type BlockHash = H256;
/// Engine using `Tendermint` consensus algorithm, suitable for EVM chain.
pub struct Tendermint {
params: CommonParams,
our_params: TendermintParams,
builtins: BTreeMap<Address, Builtin>,
step_service: IoService<Step>,
/// Address to be used as authority.
authority: RwLock<Address>,
/// Password used for signing messages.
password: RwLock<Option<String>>,
/// Blockchain height.
height: AtomicUsize,
/// Consensus round.
round: AtomicUsize,
/// Consensus step.
step: RwLock<Step>,
/// Vote accumulator.
votes: VoteCollector,
/// Channel for updating the sealing.
message_channel: Mutex<Option<IoChannel<ClientIoMessage>>>,
/// Used to sign messages and proposals.
account_provider: Mutex<Option<Arc<AccountProvider>>>,
/// Message for the last PoLC.
lock_change: RwLock<Option<ConsensusMessage>>,
/// Last lock round.
last_lock: AtomicUsize,
/// Bare hash of the proposed block, used for seal submission.
proposal: RwLock<Option<H256>>,
}
impl Tendermint {
/// Create a new instance of Tendermint engine
pub fn new(params: CommonParams, our_params: TendermintParams, builtins: BTreeMap<Address, Builtin>) -> Result<Arc<Self>, Error> {
let engine = Arc::new(
Tendermint {
params: params,
our_params: our_params,
builtins: builtins,
step_service: try!(IoService::<Step>::start()),
authority: RwLock::new(Address::default()),
password: RwLock::new(None),
height: AtomicUsize::new(1),
round: AtomicUsize::new(0),
step: RwLock::new(Step::Propose),
votes: VoteCollector::new(),
message_channel: Mutex::new(None),
account_provider: Mutex::new(None),
lock_change: RwLock::new(None),
last_lock: AtomicUsize::new(0),
proposal: RwLock::new(None),
});
let handler = TransitionHandler { engine: Arc::downgrade(&engine) };
try!(engine.step_service.register_handler(Arc::new(handler)));
Ok(engine)
}
fn update_sealing(&self) {
if let Some(ref channel) = *self.message_channel.lock() {
match channel.send(ClientIoMessage::UpdateSealing) {
Ok(_) => trace!(target: "poa", "UpdateSealing message sent."),
Err(err) => warn!(target: "poa", "Could not send a sealing message {}.", err),
}
}
}
fn submit_seal(&self, block_hash: H256, seal: Vec<Bytes>) {
if let Some(ref channel) = *self.message_channel.lock() {
match channel.send(ClientIoMessage::SubmitSeal(block_hash, seal)) {
Ok(_) => trace!(target: "poa", "SubmitSeal message sent."),
Err(err) => warn!(target: "poa", "Could not send a sealing message {}.", err),
}
}
}
fn broadcast_message(&self, message: Bytes) {
let channel = self.message_channel.lock().clone();
if let Some(ref channel) = channel {
match channel.send(ClientIoMessage::BroadcastMessage(message)) {
Ok(_) => trace!(target: "poa", "BroadcastMessage message sent."),
Err(err) => warn!(target: "poa", "broadcast_message: Could not send a sealing message {}.", err),
}
} else {
warn!(target: "poa", "broadcast_message: No IoChannel available.");
}
}
fn generate_message(&self, block_hash: Option<BlockHash>) -> Option<Bytes> {
if let Some(ref ap) = *self.account_provider.lock() {
let h = self.height.load(AtomicOrdering::SeqCst);
let r = self.round.load(AtomicOrdering::SeqCst);
let s = self.step.read();
let vote_info = message_info_rlp(h, r, *s, block_hash);
let authority = self.authority.read();
match ap.sign(*authority, self.password.read().clone(), vote_info.sha3()).map(Into::into) {
Ok(signature) => {
let message_rlp = message_full_rlp(&signature, &vote_info);
let message = ConsensusMessage::new(signature, h, r, *s, block_hash);
self.votes.vote(message.clone(), *authority);
debug!(target: "poa", "Generated {:?} as {}.", message, *authority);
self.handle_valid_message(&message);
Some(message_rlp)
},
Err(e) => {
trace!(target: "poa", "Could not sign the message {}", e);
None
},
}
} else {
warn!(target: "poa", "No AccountProvider available.");
None
}
}
fn generate_and_broadcast_message(&self, block_hash: Option<BlockHash>) {
if let Some(message) = self.generate_message(block_hash) {
self.broadcast_message(message);
}
}
/// Broadcast all messages since last issued block to get the peers up to speed.
fn broadcast_old_messages(&self) {
for m in self.votes.get_up_to(self.height.load(AtomicOrdering::SeqCst)).into_iter() {
self.broadcast_message(m);
}
}
fn to_next_height(&self, height: Height) {
let new_height = height + 1;
debug!(target: "poa", "Received a Commit, transitioning to height {}.", new_height);
self.last_lock.store(0, AtomicOrdering::SeqCst);
self.height.store(new_height, AtomicOrdering::SeqCst);
self.round.store(0, AtomicOrdering::SeqCst);
*self.lock_change.write() = None;
}
/// Use via step_service to transition steps.
fn to_step(&self, step: Step) {
if let Err(io_err) = self.step_service.send_message(step) {
warn!(target: "poa", "Could not proceed to step {}.", io_err)
}
*self.step.write() = step;
match step {
Step::Propose => {
*self.proposal.write() = None;
self.update_sealing()
},
Step::Prevote => {
let block_hash = match *self.lock_change.read() {
Some(ref m) if !self.should_unlock(m.round) => m.block_hash,
_ => self.proposal.read().clone(),
};
self.generate_and_broadcast_message(block_hash);
},
Step::Precommit => {
trace!(target: "poa", "to_step: Precommit.");
let block_hash = match *self.lock_change.read() {
Some(ref m) if self.is_round(m) && m.block_hash.is_some() => {
trace!(target: "poa", "Setting last lock: {}", m.round);
self.last_lock.store(m.round, AtomicOrdering::SeqCst);
m.block_hash
},
_ => None,
};
self.generate_and_broadcast_message(block_hash);
},
Step::Commit => {
trace!(target: "poa", "to_step: Commit.");
// Commit the block using a complete signature set.
let round = self.round.load(AtomicOrdering::SeqCst);
let height = self.height.load(AtomicOrdering::SeqCst);
if let Some(block_hash) = *self.proposal.read() {
// Generate seal and remove old votes.
if self.is_proposer(&*self.authority.read()).is_ok() {
if let Some(seal) = self.votes.seal_signatures(height, round, block_hash) {
trace!(target: "poa", "Collected seal: {:?}", seal);
let seal = vec![
::rlp::encode(&round).to_vec(),
::rlp::encode(&seal.proposal).to_vec(),
::rlp::encode(&seal.votes).to_vec()
];
self.submit_seal(block_hash, seal);
self.to_next_height(height);
} else {
warn!(target: "poa", "Not enough votes found!");
}
}
}
},
}
}
fn is_authority(&self, address: &Address) -> bool {
self.our_params.authorities.contains(address)
}
fn is_above_threshold(&self, n: usize) -> bool {
n > self.our_params.authority_n * 2/3
}
/// Check if address is a proposer for given round.
fn is_round_proposer(&self, height: Height, round: Round, address: &Address) -> Result<(), EngineError> {
let ref p = self.our_params;
let proposer_nonce = height + round;
trace!(target: "poa", "is_proposer: Proposer nonce: {}", proposer_nonce);
let proposer = p.authorities.get(proposer_nonce % p.authority_n).expect("There are authority_n authorities; taking number modulo authority_n gives number in authority_n range; qed");
if proposer == address {
Ok(())
} else {
Err(EngineError::NotProposer(Mismatch { expected: proposer.clone(), found: address.clone() }))
}
}
/// Check if address is the current proposer.
fn is_proposer(&self, address: &Address) -> Result<(), EngineError> {
self.is_round_proposer(self.height.load(AtomicOrdering::SeqCst), self.round.load(AtomicOrdering::SeqCst), address)
}
fn is_height(&self, message: &ConsensusMessage) -> bool {
message.is_height(self.height.load(AtomicOrdering::SeqCst))
}
fn is_round(&self, message: &ConsensusMessage) -> bool {
message.is_round(self.height.load(AtomicOrdering::SeqCst), self.round.load(AtomicOrdering::SeqCst))
}
fn increment_round(&self, n: Round) {
trace!(target: "poa", "increment_round: New round.");
self.round.fetch_add(n, AtomicOrdering::SeqCst);
}
fn should_unlock(&self, lock_change_round: Round) -> bool {
self.last_lock.load(AtomicOrdering::SeqCst) < lock_change_round
&& lock_change_round < self.round.load(AtomicOrdering::SeqCst)
}
fn has_enough_any_votes(&self) -> bool {
let step_votes = self.votes.count_step_votes(self.height.load(AtomicOrdering::SeqCst), self.round.load(AtomicOrdering::SeqCst), *self.step.read());
self.is_above_threshold(step_votes)
}
fn has_enough_future_step_votes(&self, message: &ConsensusMessage) -> bool {
if message.round > self.round.load(AtomicOrdering::SeqCst) {
let step_votes = self.votes.count_step_votes(message.height, message.round, message.step);
self.is_above_threshold(step_votes)
} else {
false
}
}
fn has_enough_aligned_votes(&self, message: &ConsensusMessage) -> bool {
let aligned_count = self.votes.count_aligned_votes(&message);
self.is_above_threshold(aligned_count)
}
fn handle_valid_message(&self, message: &ConsensusMessage) {
let is_newer_than_lock = match *self.lock_change.read() {
Some(ref lock) => message > lock,
None => true,
};
let lock_change = is_newer_than_lock
&& message.step == Step::Prevote
&& message.block_hash.is_some()
&& self.has_enough_aligned_votes(message);
if lock_change {
trace!(target: "poa", "handle_valid_message: Lock change.");
*self.lock_change.write() = Some(message.clone());
}
// Check if it can affect the step transition.
if self.is_height(message) {
let next_step = match *self.step.read() {
Step::Precommit if self.has_enough_aligned_votes(message) => {
if message.block_hash.is_none() {
self.increment_round(1);
Some(Step::Propose)
} else {
Some(Step::Commit)
}
},
Step::Precommit if self.has_enough_future_step_votes(message) => {
self.increment_round(message.round - self.round.load(AtomicOrdering::SeqCst));
Some(Step::Precommit)
},
// Avoid counting twice.
Step::Prevote if lock_change => Some(Step::Precommit),
Step::Prevote if self.has_enough_aligned_votes(message) => Some(Step::Precommit),
Step::Prevote if self.has_enough_future_step_votes(message) => {
self.increment_round(message.round - self.round.load(AtomicOrdering::SeqCst));
Some(Step::Prevote)
},
_ => None,
};
if let Some(step) = next_step {
trace!(target: "poa", "Transition to {:?} triggered.", step);
self.to_step(step);
}
}
}
}
impl Engine for Tendermint {
fn name(&self) -> &str { "Tendermint" }
fn version(&self) -> SemanticVersion { SemanticVersion::new(1, 0, 0) }
/// (consensus round, proposal signature, authority signatures)
fn seal_fields(&self) -> usize { 3 }
fn params(&self) -> &CommonParams { &self.params }
fn builtins(&self) -> &BTreeMap<Address, Builtin> { &self.builtins }
fn maximum_uncle_count(&self) -> usize { 0 }
fn maximum_uncle_age(&self) -> usize { 0 }
/// Additional engine-specific information for the user/developer concerning `header`.
fn extra_info(&self, header: &Header) -> BTreeMap<String, String> {
let message = ConsensusMessage::new_proposal(header).expect("Invalid header.");
map![
"signature".into() => message.signature.to_string(),
"height".into() => message.height.to_string(),
"round".into() => message.round.to_string(),
"block_hash".into() => message.block_hash.as_ref().map(ToString::to_string).unwrap_or("".into())
]
}
fn schedule(&self, _env_info: &EnvInfo) -> Schedule {
Schedule::new_post_eip150(usize::max_value(), true, true, true)
}
fn populate_from_parent(&self, header: &mut Header, parent: &Header, gas_floor_target: U256, _gas_ceil_target: U256) {
header.set_difficulty(parent.difficulty().clone());
header.set_gas_limit({
let gas_limit = parent.gas_limit().clone();
let bound_divisor = self.our_params.gas_limit_bound_divisor;
if gas_limit < gas_floor_target {
min(gas_floor_target, gas_limit + gas_limit / bound_divisor - 1.into())
} else {
max(gas_floor_target, gas_limit - gas_limit / bound_divisor + 1.into())
}
});
}
/// Should this node participate.
fn is_sealer(&self, address: &Address) -> Option<bool> {
Some(self.is_authority(address))
}
/// Attempt to seal generate a proposal seal.
fn generate_seal(&self, block: &ExecutedBlock) -> Seal {
if let Some(ref ap) = *self.account_provider.lock() {
let header = block.header();
let author = header.author();
// Only proposer can generate seal if None was generated.
if self.is_proposer(author).is_err() || self.proposal.read().is_some() {
return Seal::None;
}
let height = header.number() as Height;
let round = self.round.load(AtomicOrdering::SeqCst);
let bh = Some(header.bare_hash());
let vote_info = message_info_rlp(height, round, Step::Propose, bh.clone());
if let Ok(signature) = ap.sign(*author, self.password.read().clone(), vote_info.sha3()).map(H520::from) {
// Insert Propose vote.
debug!(target: "poa", "Submitting proposal {} at height {} round {}.", header.bare_hash(), height, round);
self.votes.vote(ConsensusMessage::new(signature, height, round, Step::Propose, bh), *author);
// Remember proposal for later seal submission.
*self.proposal.write() = bh;
Seal::Proposal(vec![
::rlp::encode(&round).to_vec(),
::rlp::encode(&signature).to_vec(),
::rlp::EMPTY_LIST_RLP.to_vec()
])
} else {
warn!(target: "poa", "generate_seal: FAIL: accounts secret key unavailable");
Seal::None
}
} else {
warn!(target: "poa", "generate_seal: FAIL: accounts not provided");
Seal::None
}
}
fn handle_message(&self, rlp: &[u8]) -> Result<(), Error> {
let rlp = UntrustedRlp::new(rlp);
let message: ConsensusMessage = try!(rlp.as_val());
if !self.votes.is_old_or_known(&message) {
let sender = public_to_address(&try!(recover(&message.signature.into(), &try!(rlp.at(1)).as_raw().sha3())));
if !self.is_authority(&sender) {
try!(Err(EngineError::NotAuthorized(sender)));
}
self.broadcast_message(rlp.as_raw().to_vec());
trace!(target: "poa", "Handling a valid {:?} from {}.", message, sender);
self.votes.vote(message.clone(), sender);
self.handle_valid_message(&message);
}
Ok(())
}
fn verify_block_basic(&self, header: &Header, _block: Option<&[u8]>) -> Result<(), Error> {
let seal_length = header.seal().len();
if seal_length == self.seal_fields() {
let signatures_len = header.seal()[2].len();
if signatures_len >= 1 {
Ok(())
} else {
Err(From::from(EngineError::BadSealFieldSize(OutOfBounds {
min: Some(1),
max: None,
found: signatures_len
})))
}
} else {
Err(From::from(BlockError::InvalidSealArity(
Mismatch { expected: self.seal_fields(), found: seal_length }
)))
}
}
fn verify_block_unordered(&self, header: &Header, _block: Option<&[u8]>) -> Result<(), Error> {
let proposal = try!(ConsensusMessage::new_proposal(header));
let proposer = try!(proposal.verify());
if !self.is_authority(&proposer) {
try!(Err(EngineError::NotAuthorized(proposer)))
}
let precommit_hash = proposal.precommit_hash();
let ref signatures_field = header.seal()[2];
let mut signature_count = 0;
let mut origins = HashSet::new();
for rlp in UntrustedRlp::new(signatures_field).iter() {
let precommit: ConsensusMessage = ConsensusMessage::new_commit(&proposal, try!(rlp.as_val()));
let address = match self.votes.get(&precommit) {
Some(a) => a,
None => public_to_address(&try!(recover(&precommit.signature.into(), &precommit_hash))),
};
if !self.our_params.authorities.contains(&address) {
try!(Err(EngineError::NotAuthorized(address.to_owned())))
}
if origins.insert(address) {
signature_count += 1;
} else {
warn!(target: "poa", "verify_block_unordered: Duplicate signature from {} on the seal.", address);
try!(Err(BlockError::InvalidSeal));
}
}
// Check if its a proposal if there is not enough precommits.
if !self.is_above_threshold(signature_count) {
let signatures_len = signatures_field.len();
// Proposal has to have an empty signature list.
if signatures_len != 1 {
try!(Err(EngineError::BadSealFieldSize(OutOfBounds {
min: Some(1),
max: Some(1),
found: signatures_len
})));
}
try!(self.is_round_proposer(proposal.height, proposal.round, &proposer));
}
Ok(())
}
fn verify_block_family(&self, header: &Header, parent: &Header, _block: Option<&[u8]>) -> Result<(), Error> {
if header.number() == 0 {
try!(Err(BlockError::RidiculousNumber(OutOfBounds { min: Some(1), max: None, found: header.number() })));
}
let gas_limit_divisor = self.our_params.gas_limit_bound_divisor;
let min_gas = parent.gas_limit().clone() - parent.gas_limit().clone() / gas_limit_divisor;
let max_gas = parent.gas_limit().clone() + parent.gas_limit().clone() / gas_limit_divisor;
if header.gas_limit() <= &min_gas || header.gas_limit() >= &max_gas {
try!(Err(BlockError::InvalidGasLimit(OutOfBounds { min: Some(min_gas), max: Some(max_gas), found: header.gas_limit().clone() })));
}
Ok(())
}
fn verify_transaction_basic(&self, t: &SignedTransaction, _header: &Header) -> Result<(), Error> {
try!(t.check_low_s());
Ok(())
}
fn verify_transaction(&self, t: &SignedTransaction, _header: &Header) -> Result<(), Error> {
t.sender().map(|_|()) // Perform EC recovery and cache sender
}
fn set_signer(&self, address: Address, password: String) {
*self.authority.write() = address;
*self.password.write() = Some(password);
self.to_step(Step::Propose);
}
fn stop(&self) {
self.step_service.stop()
}
fn is_new_best_block(&self, _best_total_difficulty: U256, best_header: HeaderView, _parent_details: &BlockDetails, new_header: &HeaderView) -> bool {
let new_number = new_header.number();
let best_number = best_header.number();
trace!(target: "poa", "new_header: {}, best_header: {}", new_number, best_number);
if new_number != best_number {
new_number > best_number
} else {
let new_seal = new_header.seal();
let best_seal = best_header.seal();
let new_signatures = new_seal.get(2).expect("Tendermint seal should have three elements.").len();
let best_signatures = best_seal.get(2).expect("Tendermint seal should have three elements.").len();
if new_signatures > best_signatures {
true
} else {
let new_round: Round = ::rlp::Rlp::new(&new_seal.get(0).expect("Tendermint seal should have three elements.")).as_val();
let best_round: Round = ::rlp::Rlp::new(&best_seal.get(0).expect("Tendermint seal should have three elements.")).as_val();
new_round > best_round
}
}
}
fn is_proposal(&self, header: &Header) -> bool {
let signatures_len = header.seal()[2].len();
// Signatures have to be an empty list rlp.
let proposal = ConsensusMessage::new_proposal(header).expect("block went through full verification; this Engine verifies new_proposal creation; qed");
if signatures_len != 1 {
// New Commit received, skip to next height.
trace!(target: "poa", "Received a commit for height {}, round {}.", proposal.height, proposal.round);
self.to_next_height(proposal.height);
return false;
}
let proposer = proposal.verify().expect("block went through full verification; this Engine tries verify; qed");
debug!(target: "poa", "Received a new proposal for height {}, round {} from {}.", proposal.height, proposal.round, proposer);
if self.is_round(&proposal) {
*self.proposal.write() = proposal.block_hash.clone();
}
self.votes.vote(proposal, proposer);
true
}
/// Equivalent to a timeout: to be used for tests.
fn step(&self) {
let next_step = match *self.step.read() {
Step::Propose => {
trace!(target: "poa", "Propose timeout.");
Step::Prevote
},
Step::Prevote if self.has_enough_any_votes() => {
trace!(target: "poa", "Prevote timeout.");
Step::Precommit
},
Step::Prevote => {
trace!(target: "poa", "Prevote timeout without enough votes.");
self.broadcast_old_messages();
Step::Prevote
},
Step::Precommit if self.has_enough_any_votes() => {
trace!(target: "poa", "Precommit timeout.");
self.increment_round(1);
Step::Propose
},
Step::Precommit => {
trace!(target: "poa", "Precommit timeout without enough votes.");
self.broadcast_old_messages();
Step::Precommit
},
Step::Commit => {
trace!(target: "poa", "Commit timeout.");
Step::Propose
},
};
self.to_step(next_step);
}
fn register_message_channel(&self, message_channel: IoChannel<ClientIoMessage>) {
trace!(target: "poa", "Register the IoChannel.");
*self.message_channel.lock() = Some(message_channel);
}
fn register_account_provider(&self, account_provider: Arc<AccountProvider>) {
*self.account_provider.lock() = Some(account_provider);
}
}
#[cfg(test)]
mod tests {
use util::*;
use util::trie::TrieSpec;
use io::{IoContext, IoHandler};
use block::*;
use error::{Error, BlockError};
use header::Header;
use io::IoChannel;
use env_info::EnvInfo;
use tests::helpers::*;
use account_provider::AccountProvider;
use service::ClientIoMessage;
use spec::Spec;
use engines::{Engine, EngineError, Seal};
use super::*;
use super::message::*;
/// Accounts inserted with "0" and "1" are authorities. First proposer is "0".
fn setup() -> (Spec, Arc<AccountProvider>) {
let tap = Arc::new(AccountProvider::transient_provider());
let spec = Spec::new_test_tendermint();
spec.engine.register_account_provider(tap.clone());
(spec, tap)
}
fn propose_default(spec: &Spec, proposer: Address) -> (LockedBlock, Vec<Bytes>) {
let mut db_result = get_temp_state_db();
let mut db = db_result.take();
spec.ensure_db_good(&mut db, &TrieFactory::new(TrieSpec::Secure)).unwrap();
let genesis_header = spec.genesis_header();
let last_hashes = Arc::new(vec![genesis_header.hash()]);
let b = OpenBlock::new(spec.engine.as_ref(), Default::default(), false, db.boxed_clone(), &genesis_header, last_hashes, proposer, (3141562.into(), 31415620.into()), vec![]).unwrap();
let b = b.close_and_lock();
if let Seal::Proposal(seal) = spec.engine.generate_seal(b.block()) {
(b, seal)
} else {
panic!()
}
}
fn vote<F>(engine: &Arc<Engine>, signer: F, height: usize, round: usize, step: Step, block_hash: Option<H256>) -> Bytes where F: FnOnce(H256) -> Result<H520, ::account_provider::Error> {
let mi = message_info_rlp(height, round, step, block_hash);
let m = message_full_rlp(&signer(mi.sha3()).unwrap().into(), &mi);
engine.handle_message(&m).unwrap();
m
}
fn proposal_seal(tap: &Arc<AccountProvider>, header: &Header, round: Round) -> Vec<Bytes> {
let author = header.author();
let vote_info = message_info_rlp(header.number() as Height, round, Step::Propose, Some(header.bare_hash()));
let signature = tap.sign(*author, None, vote_info.sha3()).unwrap();
vec![
::rlp::encode(&round).to_vec(),
::rlp::encode(&H520::from(signature)).to_vec(),
::rlp::EMPTY_LIST_RLP.to_vec()
]
}
fn precommit_signatures(tap: &Arc<AccountProvider>, height: Height, round: Round, bare_hash: Option<H256>, v1: H160, v2: H160) -> Bytes {
let vote_info = message_info_rlp(height, round, Step::Precommit, bare_hash);
::rlp::encode(&vec![
H520::from(tap.sign(v1, None, vote_info.sha3()).unwrap()),
H520::from(tap.sign(v2, None, vote_info.sha3()).unwrap())
]).to_vec()
}
fn insert_and_unlock(tap: &Arc<AccountProvider>, acc: &str) -> Address {
let addr = tap.insert_account(acc.sha3(), acc).unwrap();
tap.unlock_account_permanently(addr, acc.into()).unwrap();
addr
}
fn insert_and_register(tap: &Arc<AccountProvider>, engine: &Arc<Engine>, acc: &str) -> Address {
let addr = insert_and_unlock(tap, acc);
engine.set_signer(addr.clone(), acc.into());
addr
}
struct TestIo {
received: RwLock<Vec<ClientIoMessage>>
}
impl TestIo {
fn new() -> Arc<Self> { Arc::new(TestIo { received: RwLock::new(Vec::new()) }) }
}
impl IoHandler<ClientIoMessage> for TestIo {
fn message(&self, _io: &IoContext<ClientIoMessage>, net_message: &ClientIoMessage) {
self.received.write().push(net_message.clone());
}
}
#[test]
fn has_valid_metadata() {
let engine = Spec::new_test_tendermint().engine;
assert!(!engine.name().is_empty());
assert!(engine.version().major >= 1);
}
#[test]
fn can_return_schedule() {
let engine = Spec::new_test_tendermint().engine;
let schedule = engine.schedule(&EnvInfo {
number: 10000000,
author: 0.into(),
timestamp: 0,
difficulty: 0.into(),
last_hashes: Arc::new(vec![]),
gas_used: 0.into(),
gas_limit: 0.into(),
});
assert!(schedule.stack_limit > 0);
}
#[test]
fn verification_fails_on_short_seal() {
let engine = Spec::new_test_tendermint().engine;
let header = Header::default();
let verify_result = engine.verify_block_basic(&header, None);
match verify_result {
Err(Error::Block(BlockError::InvalidSealArity(_))) => {},
Err(_) => { panic!("should be block seal-arity mismatch error (got {:?})", verify_result); },
_ => { panic!("Should be error, got Ok"); },
}
}
#[test]
fn allows_correct_proposer() {
let (spec, tap) = setup();
let engine = spec.engine;
let mut header = Header::default();
let validator = insert_and_unlock(&tap, "0");
header.set_author(validator);
let seal = proposal_seal(&tap, &header, 0);
header.set_seal(seal);
// Good proposer.
assert!(engine.verify_block_unordered(&header.clone(), None).is_ok());
let validator = insert_and_unlock(&tap, "1");
header.set_author(validator);
let seal = proposal_seal(&tap, &header, 0);
header.set_seal(seal);
// Bad proposer.
match engine.verify_block_unordered(&header, None) {
Err(Error::Engine(EngineError::NotProposer(_))) => {},
_ => panic!(),
}
let random = insert_and_unlock(&tap, "101");
header.set_author(random);
let seal = proposal_seal(&tap, &header, 0);
header.set_seal(seal);
// Not authority.
match engine.verify_block_unordered(&header, None) {
Err(Error::Engine(EngineError::NotAuthorized(_))) => {},
_ => panic!(),
};
engine.stop();
}
#[test]
fn seal_signatures_checking() {
let (spec, tap) = setup();
let engine = spec.engine;
let mut header = Header::default();
let proposer = insert_and_unlock(&tap, "1");
header.set_author(proposer);
let mut seal = proposal_seal(&tap, &header, 0);
let vote_info = message_info_rlp(0, 0, Step::Precommit, Some(header.bare_hash()));
let signature1 = tap.sign(proposer, None, vote_info.sha3()).unwrap();
seal[2] = ::rlp::encode(&vec![H520::from(signature1.clone())]).to_vec();
header.set_seal(seal.clone());
// One good signature is not enough.
match engine.verify_block_unordered(&header, None) {
Err(Error::Engine(EngineError::BadSealFieldSize(_))) => {},
_ => panic!(),
}
let voter = insert_and_unlock(&tap, "0");
let signature0 = tap.sign(voter, None, vote_info.sha3()).unwrap();
seal[2] = ::rlp::encode(&vec![H520::from(signature1.clone()), H520::from(signature0.clone())]).to_vec();
header.set_seal(seal.clone());
assert!(engine.verify_block_unordered(&header, None).is_ok());
let bad_voter = insert_and_unlock(&tap, "101");
let bad_signature = tap.sign(bad_voter, None, vote_info.sha3()).unwrap();
seal[2] = ::rlp::encode(&vec![H520::from(signature1), H520::from(bad_signature)]).to_vec();
header.set_seal(seal);
// One good and one bad signature.
match engine.verify_block_unordered(&header, None) {
Err(Error::Engine(EngineError::NotAuthorized(_))) => {},
_ => panic!(),
};
engine.stop();
}
#[test]
fn can_generate_seal() {
let (spec, tap) = setup();
let proposer = insert_and_register(&tap, &spec.engine, "1");
let (b, seal) = propose_default(&spec, proposer);
assert!(b.try_seal(spec.engine.as_ref(), seal).is_ok());
spec.engine.stop();
}
#[test]
fn can_recognize_proposal() {
let (spec, tap) = setup();
let proposer = insert_and_register(&tap, &spec.engine, "1");
let (b, seal) = propose_default(&spec, proposer);
let sealed = b.seal(spec.engine.as_ref(), seal).unwrap();
assert!(spec.engine.is_proposal(sealed.header()));
spec.engine.stop();
}
#[test]
fn relays_messages() {
let (spec, tap) = setup();
let engine = spec.engine.clone();
let mut db_result = get_temp_state_db();
let mut db = db_result.take();
spec.ensure_db_good(&mut db, &TrieFactory::new(TrieSpec::Secure)).unwrap();
let v0 = insert_and_register(&tap, &engine, "0");
let v1 = insert_and_register(&tap, &engine, "1");
let h = 0;
let r = 0;
// Propose
let (b, _) = propose_default(&spec, v1.clone());
let proposal = Some(b.header().bare_hash());
// Register IoHandler remembers messages.
let test_io = TestIo::new();
let channel = IoChannel::to_handler(Arc::downgrade(&(test_io.clone() as Arc<IoHandler<ClientIoMessage>>)));
engine.register_message_channel(channel);
let prevote_current = vote(&engine, |mh| tap.sign(v0, None, mh).map(H520::from), h, r, Step::Prevote, proposal);
let precommit_current = vote(&engine, |mh| tap.sign(v0, None, mh).map(H520::from), h, r, Step::Precommit, proposal);
let prevote_future = vote(&engine, |mh| tap.sign(v0, None, mh).map(H520::from), h + 1, r, Step::Prevote, proposal);
// Relays all valid present and future messages.
assert!(test_io.received.read().contains(&ClientIoMessage::BroadcastMessage(prevote_current)));
assert!(test_io.received.read().contains(&ClientIoMessage::BroadcastMessage(precommit_current)));
assert!(test_io.received.read().contains(&ClientIoMessage::BroadcastMessage(prevote_future)));
engine.stop();
}
#[test]
fn seal_submission() {
let (spec, tap) = setup();
let engine = spec.engine.clone();
let mut db_result = get_temp_state_db();
let mut db = db_result.take();
spec.ensure_db_good(&mut db, &TrieFactory::new(TrieSpec::Secure)).unwrap();
let v0 = insert_and_register(&tap, &engine, "0");
let v1 = insert_and_register(&tap, &engine, "1");
let h = 1;
let r = 0;
// Register IoHandler remembers messages.
let test_io = TestIo::new();
let channel = IoChannel::to_handler(Arc::downgrade(&(test_io.clone() as Arc<IoHandler<ClientIoMessage>>)));
engine.register_message_channel(channel);
// Propose
let (b, mut seal) = propose_default(&spec, v1.clone());
let proposal = Some(b.header().bare_hash());
engine.step();
// Prevote.
vote(&engine, |mh| tap.sign(v1, None, mh).map(H520::from), h, r, Step::Prevote, proposal);
vote(&engine, |mh| tap.sign(v0, None, mh).map(H520::from), h, r, Step::Prevote, proposal);
vote(&engine, |mh| tap.sign(v1, None, mh).map(H520::from), h, r, Step::Precommit, proposal);
vote(&engine, |mh| tap.sign(v0, None, mh).map(H520::from), h, r, Step::Precommit, proposal);
seal[2] = precommit_signatures(&tap, h, r, Some(b.header().bare_hash()), v1, v0);
let first = test_io.received.read().contains(&ClientIoMessage::SubmitSeal(proposal.unwrap(), seal.clone()));
seal[2] = precommit_signatures(&tap, h, r, Some(b.header().bare_hash()), v0, v1);
let second = test_io.received.read().contains(&ClientIoMessage::SubmitSeal(proposal.unwrap(), seal));
assert!(first ^ second);
engine.stop();
}
}

View File

@ -0,0 +1,72 @@
// 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 <http://www.gnu.org/licenses/>.
//! Tendermint specific parameters.
use ethjson;
use super::transition::TendermintTimeouts;
use util::{Address, U256};
use time::Duration;
/// `Tendermint` params.
#[derive(Debug, Clone)]
pub struct TendermintParams {
/// Gas limit divisor.
pub gas_limit_bound_divisor: U256,
/// List of authorities.
pub authorities: Vec<Address>,
/// Number of authorities.
pub authority_n: usize,
/// Timeout durations for different steps.
pub timeouts: TendermintTimeouts,
}
impl Default for TendermintParams {
fn default() -> Self {
let authorities = vec!["0x7d577a597b2742b498cb5cf0c26cdcd726d39e6e".into(), "0x82a978b3f5962a5b0957d9ee9eef472ee55b42f1".into()];
let val_n = authorities.len();
TendermintParams {
gas_limit_bound_divisor: 0x0400.into(),
authorities: authorities,
authority_n: val_n,
timeouts: TendermintTimeouts::default(),
}
}
}
fn to_duration(ms: ethjson::uint::Uint) -> Duration {
let ms: usize = ms.into();
Duration::milliseconds(ms as i64)
}
impl From<ethjson::spec::TendermintParams> for TendermintParams {
fn from(p: ethjson::spec::TendermintParams) -> Self {
let val: Vec<_> = p.authorities.into_iter().map(Into::into).collect();
let val_n = val.len();
let dt = TendermintTimeouts::default();
TendermintParams {
gas_limit_bound_divisor: p.gas_limit_bound_divisor.into(),
authorities: val,
authority_n: val_n,
timeouts: TendermintTimeouts {
propose: p.timeout_propose.map_or(dt.propose, to_duration),
prevote: p.timeout_prevote.map_or(dt.prevote, to_duration),
precommit: p.timeout_precommit.map_or(dt.precommit, to_duration),
commit: p.timeout_commit.map_or(dt.commit, to_duration),
},
}
}
}

View File

@ -0,0 +1,96 @@
// 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 <http://www.gnu.org/licenses/>.
//! Tendermint timeout handling.
use std::sync::Weak;
use time::Duration;
use io::{IoContext, IoHandler, TimerToken};
use super::{Tendermint, Step};
use engines::Engine;
pub struct TransitionHandler {
pub engine: Weak<Tendermint>,
}
/// Base timeout of each step in ms.
#[derive(Debug, Clone)]
pub struct TendermintTimeouts {
pub propose: Duration,
pub prevote: Duration,
pub precommit: Duration,
pub commit: Duration,
}
impl TendermintTimeouts {
pub fn for_step(&self, step: Step) -> Duration {
match step {
Step::Propose => self.propose,
Step::Prevote => self.prevote,
Step::Precommit => self.precommit,
Step::Commit => self.commit,
}
}
}
impl Default for TendermintTimeouts {
fn default() -> Self {
TendermintTimeouts {
propose: Duration::milliseconds(10000),
prevote: Duration::milliseconds(10000),
precommit: Duration::milliseconds(10000),
commit: Duration::milliseconds(10000),
}
}
}
/// Timer token representing the consensus step timeouts.
pub const ENGINE_TIMEOUT_TOKEN: TimerToken = 23;
fn set_timeout(io: &IoContext<Step>, timeout: Duration) {
io.register_timer_once(ENGINE_TIMEOUT_TOKEN, timeout.num_milliseconds() as u64)
.unwrap_or_else(|e| warn!(target: "poa", "Failed to set consensus step timeout: {}.", e))
}
impl IoHandler<Step> for TransitionHandler {
fn initialize(&self, io: &IoContext<Step>) {
if let Some(engine) = self.engine.upgrade() {
set_timeout(io, engine.our_params.timeouts.propose)
}
}
fn timeout(&self, _io: &IoContext<Step>, timer: TimerToken) {
if timer == ENGINE_TIMEOUT_TOKEN {
if let Some(engine) = self.engine.upgrade() {
engine.step();
}
}
}
fn message(&self, io: &IoContext<Step>, next_step: &Step) {
if let Some(engine) = self.engine.upgrade() {
if let Err(io_err) = io.clear_timer(ENGINE_TIMEOUT_TOKEN) {
warn!(target: "poa", "Could not remove consensus timer {}.", io_err)
}
match *next_step {
Step::Propose => set_timeout(io, engine.our_params.timeouts.propose),
Step::Prevote => set_timeout(io, engine.our_params.timeouts.prevote),
Step::Precommit => set_timeout(io, engine.our_params.timeouts.precommit),
Step::Commit => set_timeout(io, engine.our_params.timeouts.commit),
};
}
}
}

View File

@ -0,0 +1,272 @@
// 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 <http://www.gnu.org/licenses/>.
//! Collects votes on hashes at each height and round.
use util::*;
use super::message::ConsensusMessage;
use super::{Height, Round, Step};
#[derive(Debug)]
pub struct VoteCollector {
/// Storing all Proposals, Prevotes and Precommits.
votes: RwLock<BTreeMap<ConsensusMessage, Address>>,
}
#[derive(Debug)]
pub struct SealSignatures {
pub proposal: H520,
pub votes: Vec<H520>,
}
impl PartialEq for SealSignatures {
fn eq(&self, other: &SealSignatures) -> bool {
self.proposal == other.proposal
&& self.votes.iter().collect::<HashSet<_>>() == other.votes.iter().collect::<HashSet<_>>()
}
}
impl Eq for SealSignatures {}
impl VoteCollector {
pub fn new() -> VoteCollector {
let mut collector = BTreeMap::new();
// Insert dummy message to fulfill invariant: "only messages newer than the oldest are inserted".
collector.insert(ConsensusMessage {
signature: H520::default(),
height: 0,
round: 0,
step: Step::Propose,
block_hash: None
},
Address::default());
VoteCollector { votes: RwLock::new(collector) }
}
/// Insert vote if it is newer than the oldest one.
pub fn vote(&self, message: ConsensusMessage, voter: Address) -> Option<Address> {
self.votes.write().insert(message, voter)
}
pub fn is_old_or_known(&self, message: &ConsensusMessage) -> bool {
self.votes.read().get(message).map_or(false, |a| {
trace!(target: "poa", "Known message from {}: {:?}.", a, message);
true
}) || {
let guard = self.votes.read();
let is_old = guard.keys().next().map_or(true, |oldest| message <= oldest);
if is_old { trace!(target: "poa", "Old message {:?}.", message); }
is_old
}
}
/// Throws out messages older than message, leaves message as marker for the oldest.
pub fn throw_out_old(&self, message: &ConsensusMessage) {
let mut guard = self.votes.write();
let new_collector = guard.split_off(message);
*guard = new_collector;
}
pub fn seal_signatures(&self, height: Height, round: Round, block_hash: H256) -> Option<SealSignatures> {
let bh = Some(block_hash);
let (proposal, votes) = {
let guard = self.votes.read();
let mut current_signatures = guard.keys().skip_while(|m| !m.is_block_hash(height, round, Step::Propose, bh));
let proposal = current_signatures.next().cloned();
let votes = current_signatures
.skip_while(|m| !m.is_block_hash(height, round, Step::Precommit, bh))
.filter(|m| m.is_block_hash(height, round, Step::Precommit, bh))
.cloned()
.collect::<Vec<_>>();
(proposal, votes)
};
if votes.is_empty() {
return None;
}
// Remove messages that are no longer relevant.
votes.last().map(|m| self.throw_out_old(m));
let mut votes_vec: Vec<_> = votes.into_iter().map(|m| m.signature).collect();
votes_vec.sort();
proposal.map(|p| SealSignatures {
proposal: p.signature,
votes: votes_vec,
})
}
pub fn count_aligned_votes(&self, message: &ConsensusMessage) -> usize {
let guard = self.votes.read();
guard.keys()
.skip_while(|m| !m.is_aligned(message))
// sorted by signature so might not be continuous
.filter(|m| m.is_aligned(message))
.count()
}
pub fn count_step_votes(&self, height: Height, round: Round, step: Step) -> usize {
let guard = self.votes.read();
let current = guard.iter().skip_while(|&(m, _)| !m.is_step(height, round, step));
let mut origins = HashSet::new();
let mut n = 0;
for (message, origin) in current {
if message.is_step(height, round, step) {
if origins.insert(origin) {
n += 1;
} else {
warn!("count_step_votes: Authority {} has cast multiple step votes, this indicates malicious behaviour.", origin)
}
}
}
n
}
pub fn get_up_to(&self, height: Height) -> Vec<Bytes> {
let guard = self.votes.read();
guard
.keys()
.filter(|m| m.step.is_pre())
.take_while(|m| m.height <= height)
.map(|m| ::rlp::encode(m).to_vec())
.collect()
}
pub fn get(&self, message: &ConsensusMessage) -> Option<Address> {
let guard = self.votes.read();
guard.get(message).cloned()
}
}
#[cfg(test)]
mod tests {
use util::*;
use super::*;
use super::super::{Height, Round, BlockHash, Step};
use super::super::message::ConsensusMessage;
fn random_vote(collector: &VoteCollector, signature: H520, h: Height, r: Round, step: Step, block_hash: Option<BlockHash>) -> Option<H160> {
full_vote(collector, signature, h, r, step, block_hash, H160::random())
}
fn full_vote(collector: &VoteCollector, signature: H520, h: Height, r: Round, step: Step, block_hash: Option<BlockHash>, address: Address) -> Option<H160> {
collector.vote(ConsensusMessage { signature: signature, height: h, round: r, step: step, block_hash: block_hash }, address)
}
#[test]
fn seal_retrieval() {
let collector = VoteCollector::new();
let bh = Some("1".sha3());
let h = 1;
let r = 2;
let mut signatures = Vec::new();
for _ in 0..5 {
signatures.push(H520::random());
}
// Wrong height proposal.
random_vote(&collector, signatures[4].clone(), h - 1, r, Step::Propose, bh.clone());
// Good proposal
random_vote(&collector, signatures[0].clone(), h, r, Step::Propose, bh.clone());
// Wrong block proposal.
random_vote(&collector, signatures[0].clone(), h, r, Step::Propose, Some("0".sha3()));
// Wrong block precommit.
random_vote(&collector, signatures[3].clone(), h, r, Step::Precommit, Some("0".sha3()));
// Wrong round proposal.
random_vote(&collector, signatures[0].clone(), h, r - 1, Step::Propose, bh.clone());
// Prevote.
random_vote(&collector, signatures[0].clone(), h, r, Step::Prevote, bh.clone());
// Relevant precommit.
random_vote(&collector, signatures[2].clone(), h, r, Step::Precommit, bh.clone());
// Replcated vote.
random_vote(&collector, signatures[2].clone(), h, r, Step::Precommit, bh.clone());
// Wrong round precommit.
random_vote(&collector, signatures[4].clone(), h, r + 1, Step::Precommit, bh.clone());
// Wrong height precommit.
random_vote(&collector, signatures[3].clone(), h + 1, r, Step::Precommit, bh.clone());
// Relevant precommit.
random_vote(&collector, signatures[1].clone(), h, r, Step::Precommit, bh.clone());
// Wrong round precommit, same signature.
random_vote(&collector, signatures[1].clone(), h, r + 1, Step::Precommit, bh.clone());
// Wrong round precommit.
random_vote(&collector, signatures[4].clone(), h, r - 1, Step::Precommit, bh.clone());
let seal = SealSignatures {
proposal: signatures[0],
votes: signatures[1..3].to_vec()
};
assert_eq!(seal, collector.seal_signatures(h, r, bh.unwrap()).unwrap());
}
#[test]
fn count_votes() {
let collector = VoteCollector::new();
// good prevote
random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("0".sha3()));
random_vote(&collector, H520::random(), 3, 1, Step::Prevote, Some("0".sha3()));
// good precommit
random_vote(&collector, H520::random(), 3, 2, Step::Precommit, Some("0".sha3()));
random_vote(&collector, H520::random(), 3, 3, Step::Precommit, Some("0".sha3()));
// good prevote
random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("1".sha3()));
// good prevote
let same_sig = H520::random();
random_vote(&collector, same_sig.clone(), 3, 2, Step::Prevote, Some("1".sha3()));
random_vote(&collector, same_sig, 3, 2, Step::Prevote, Some("1".sha3()));
// good precommit
random_vote(&collector, H520::random(), 3, 2, Step::Precommit, Some("1".sha3()));
// good prevote
random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("0".sha3()));
random_vote(&collector, H520::random(), 2, 2, Step::Precommit, Some("2".sha3()));
assert_eq!(collector.count_step_votes(3, 2, Step::Prevote), 4);
assert_eq!(collector.count_step_votes(3, 2, Step::Precommit), 2);
let message = ConsensusMessage {
signature: H520::default(),
height: 3,
round: 2,
step: Step::Prevote,
block_hash: Some("1".sha3())
};
assert_eq!(collector.count_aligned_votes(&message), 2);
}
#[test]
fn remove_old() {
let collector = VoteCollector::new();
random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("0".sha3()));
random_vote(&collector, H520::random(), 3, 1, Step::Prevote, Some("0".sha3()));
random_vote(&collector, H520::random(), 3, 3, Step::Precommit, Some("0".sha3()));
random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("1".sha3()));
random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("1".sha3()));
random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("0".sha3()));
random_vote(&collector, H520::random(), 2, 2, Step::Precommit, Some("2".sha3()));
let message = ConsensusMessage {
signature: H520::default(),
height: 3,
round: 2,
step: Step::Precommit,
block_hash: Some("1".sha3())
};
collector.throw_out_old(&message);
assert_eq!(collector.votes.read().len(), 1);
}
#[test]
fn malicious_authority() {
let collector = VoteCollector::new();
full_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("0".sha3()), Address::default());
full_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("1".sha3()), Address::default());
assert_eq!(collector.count_step_votes(3, 2, Step::Prevote), 1);
}
}

View File

@ -24,6 +24,7 @@ use client::Error as ClientError;
use ipc::binary::{BinaryConvertError, BinaryConvertable}; use ipc::binary::{BinaryConvertError, BinaryConvertable};
use types::block_import_error::BlockImportError; use types::block_import_error::BlockImportError;
use snapshot::Error as SnapshotError; use snapshot::Error as SnapshotError;
use engines::EngineError;
use ethkey::Error as EthkeyError; use ethkey::Error as EthkeyError;
pub use types::executed::{ExecutionError, CallError}; pub use types::executed::{ExecutionError, CallError};
@ -167,8 +168,6 @@ pub enum BlockError {
UnknownParent(H256), UnknownParent(H256),
/// Uncle parent given is unknown. /// Uncle parent given is unknown.
UnknownUncleParent(H256), UnknownUncleParent(H256),
/// The same author issued different votes at the same step.
DoubleVote(H160),
} }
impl fmt::Display for BlockError { impl fmt::Display for BlockError {
@ -202,7 +201,6 @@ impl fmt::Display for BlockError {
RidiculousNumber(ref oob) => format!("Implausible block number. {}", oob), RidiculousNumber(ref oob) => format!("Implausible block number. {}", oob),
UnknownParent(ref hash) => format!("Unknown parent: {}", hash), UnknownParent(ref hash) => format!("Unknown parent: {}", hash),
UnknownUncleParent(ref hash) => format!("Unknown uncle parent: {}", hash), UnknownUncleParent(ref hash) => format!("Unknown uncle parent: {}", hash),
DoubleVote(ref address) => format!("Author {} issued too many blocks.", address),
}; };
f.write_fmt(format_args!("Block error ({})", msg)) f.write_fmt(format_args!("Block error ({})", msg))
@ -263,6 +261,8 @@ pub enum Error {
Snappy(::util::snappy::InvalidInput), Snappy(::util::snappy::InvalidInput),
/// Snapshot error. /// Snapshot error.
Snapshot(SnapshotError), Snapshot(SnapshotError),
/// Consensus vote error.
Engine(EngineError),
/// Ethkey error. /// Ethkey error.
Ethkey(EthkeyError), Ethkey(EthkeyError),
} }
@ -285,6 +285,7 @@ impl fmt::Display for Error {
Error::StdIo(ref err) => err.fmt(f), Error::StdIo(ref err) => err.fmt(f),
Error::Snappy(ref err) => err.fmt(f), Error::Snappy(ref err) => err.fmt(f),
Error::Snapshot(ref err) => err.fmt(f), Error::Snapshot(ref err) => err.fmt(f),
Error::Engine(ref err) => err.fmt(f),
Error::Ethkey(ref err) => err.fmt(f), Error::Ethkey(ref err) => err.fmt(f),
} }
} }
@ -383,6 +384,12 @@ impl From<SnapshotError> for Error {
} }
} }
impl From<EngineError> for Error {
fn from(err: EngineError) -> Error {
Error::Engine(err)
}
}
impl From<EthkeyError> for Error { impl From<EthkeyError> for Error {
fn from(err: EthkeyError) -> Error { fn from(err: EthkeyError) -> Error {
Error::Ethkey(err) Error::Ethkey(err)

View File

@ -26,12 +26,12 @@ use state::{State, CleanupMode};
use client::{MiningBlockChainClient, Executive, Executed, EnvInfo, TransactOptions, BlockId, CallAnalytics, TransactionId}; use client::{MiningBlockChainClient, Executive, Executed, EnvInfo, TransactOptions, BlockId, CallAnalytics, TransactionId};
use client::TransactionImportResult; use client::TransactionImportResult;
use executive::contract_address; use executive::contract_address;
use block::{ClosedBlock, SealedBlock, IsBlock, Block}; use block::{ClosedBlock, IsBlock, Block};
use error::*; use error::*;
use transaction::{Action, SignedTransaction}; use transaction::{Action, SignedTransaction};
use receipt::{Receipt, RichReceipt}; use receipt::{Receipt, RichReceipt};
use spec::Spec; use spec::Spec;
use engines::Engine; use engines::{Engine, Seal};
use miner::{MinerService, MinerStatus, TransactionQueue, PrioritizationStrategy, AccountDetails, TransactionOrigin}; use miner::{MinerService, MinerStatus, TransactionQueue, PrioritizationStrategy, AccountDetails, TransactionOrigin};
use miner::banning_queue::{BanningTransactionQueue, Threshold}; use miner::banning_queue::{BanningTransactionQueue, Threshold};
use miner::work_notify::WorkPoster; use miner::work_notify::WorkPoster;
@ -466,34 +466,43 @@ impl Miner {
} }
} }
/// Attempts to perform internal sealing (one that does not require work) to return Ok(sealed), /// Attempts to perform internal sealing (one that does not require work) and handles the result depending on the type of Seal.
/// Err(Some(block)) returns for unsuccesful sealing while Err(None) indicates misspecified engine.
fn seal_block_internally(&self, block: ClosedBlock) -> Result<SealedBlock, Option<ClosedBlock>> {
trace!(target: "miner", "seal_block_internally: attempting internal seal.");
let s = self.engine.generate_seal(block.block());
if let Some(seal) = s {
trace!(target: "miner", "seal_block_internally: managed internal seal. importing...");
block.lock().try_seal(&*self.engine, seal).or_else(|(e, _)| {
warn!("prepare_sealing: ERROR: try_seal failed when given internally generated seal: {}", e);
Err(None)
})
} else {
trace!(target: "miner", "seal_block_internally: unable to generate seal internally");
Err(Some(block))
}
}
/// Uses Engine to seal the block internally and then imports it to chain.
fn seal_and_import_block_internally(&self, chain: &MiningBlockChainClient, block: ClosedBlock) -> bool { fn seal_and_import_block_internally(&self, chain: &MiningBlockChainClient, block: ClosedBlock) -> bool {
if !block.transactions().is_empty() || self.forced_sealing() { if !block.transactions().is_empty() || self.forced_sealing() {
if let Ok(sealed) = self.seal_block_internally(block) { trace!(target: "miner", "seal_block_internally: attempting internal seal.");
if chain.import_sealed_block(sealed).is_ok() { match self.engine.generate_seal(block.block()) {
trace!(target: "miner", "import_block_internally: imported internally sealed block"); // Save proposal for later seal submission and broadcast it.
return true Seal::Proposal(seal) => {
} trace!(target: "miner", "Received a Proposal seal.");
} {
let mut sealing_work = self.sealing_work.lock();
sealing_work.queue.push(block.clone());
sealing_work.queue.use_last_ref();
} }
block
.lock()
.seal(&*self.engine, seal)
.map(|sealed| { chain.broadcast_proposal_block(sealed); true })
.unwrap_or_else(|e| {
warn!("ERROR: seal failed when given internally generated seal: {}", e);
false false
})
},
// Directly import a regular sealed block.
Seal::Regular(seal) =>
block
.lock()
.seal(&*self.engine, seal)
.map(|sealed| chain.import_sealed_block(sealed).is_ok())
.unwrap_or_else(|e| {
warn!("ERROR: seal failed when given internally generated seal: {}", e);
false
}),
Seal::None => false,
}
} else {
false
}
} }
/// Prepares work which has to be done to seal. /// Prepares work which has to be done to seal.
@ -1024,7 +1033,6 @@ impl MinerService for Miner {
self.transaction_queue.lock().last_nonce(address) self.transaction_queue.lock().last_nonce(address)
} }
/// Update sealing if required. /// Update sealing if required.
/// Prepare the block and work if the Engine does not seal internally. /// Prepare the block and work if the Engine does not seal internally.
fn update_sealing(&self, chain: &MiningBlockChainClient) { fn update_sealing(&self, chain: &MiningBlockChainClient) {
@ -1039,7 +1047,9 @@ impl MinerService for Miner {
let (block, original_work_hash) = self.prepare_block(chain); let (block, original_work_hash) = self.prepare_block(chain);
if self.seals_internally { if self.seals_internally {
trace!(target: "miner", "update_sealing: engine indicates internal sealing"); trace!(target: "miner", "update_sealing: engine indicates internal sealing");
self.seal_and_import_block_internally(chain, block); if self.seal_and_import_block_internally(chain, block) {
trace!(target: "miner", "update_sealing: imported internally sealed block");
}
} else { } else {
trace!(target: "miner", "update_sealing: engine does not seal internally, preparing work"); trace!(target: "miner", "update_sealing: engine does not seal internally, preparing work");
self.prepare_work(block, original_work_hash); self.prepare_work(block, original_work_hash);

View File

@ -20,7 +20,7 @@ use util::*;
use io::*; use io::*;
use spec::Spec; use spec::Spec;
use error::*; use error::*;
use client::{Client, ClientConfig, ChainNotify}; use client::{Client, BlockChainClient, MiningBlockChainClient, ClientConfig, ChainNotify};
use miner::Miner; use miner::Miner;
use snapshot::ManifestData; use snapshot::ManifestData;
use snapshot::service::{Service as SnapshotService, ServiceParams as SnapServiceParams}; use snapshot::service::{Service as SnapshotService, ServiceParams as SnapServiceParams};
@ -28,11 +28,9 @@ use std::sync::atomic::AtomicBool;
#[cfg(feature="ipc")] #[cfg(feature="ipc")]
use nanoipc; use nanoipc;
#[cfg(feature="ipc")]
use client::BlockChainClient;
/// Message type for external and internal events /// Message type for external and internal events
#[derive(Clone)] #[derive(Clone, PartialEq, Eq, Debug)]
pub enum ClientIoMessage { pub enum ClientIoMessage {
/// Best Block Hash in chain has been changed /// Best Block Hash in chain has been changed
NewChainHead, NewChainHead,
@ -50,6 +48,12 @@ pub enum ClientIoMessage {
TakeSnapshot(u64), TakeSnapshot(u64),
/// Trigger sealing update (useful for internal sealing). /// Trigger sealing update (useful for internal sealing).
UpdateSealing, UpdateSealing,
/// Submit seal (useful for internal sealing).
SubmitSeal(H256, Vec<Bytes>),
/// Broadcast a message to the network.
BroadcastMessage(Bytes),
/// New consensus message received.
NewMessage(Bytes)
} }
/// Client service setup. Creates and registers client and network services with the IO subsystem. /// Client service setup. Creates and registers client and network services with the IO subsystem.
@ -77,9 +81,6 @@ impl ClientService {
panic_handler.forward_from(&io_service); panic_handler.forward_from(&io_service);
info!("Configured for {} using {} engine", Colour::White.bold().paint(spec.name.clone()), Colour::Yellow.bold().paint(spec.engine.name())); info!("Configured for {} using {} engine", Colour::White.bold().paint(spec.name.clone()), Colour::Yellow.bold().paint(spec.engine.name()));
if spec.fork_name.is_some() {
warn!("Your chain is an alternative fork. {}", Colour::Red.bold().paint("TRANSACTIONS MAY BE REPLAYED ON THE MAINNET!"));
}
let mut db_config = DatabaseConfig::with_columns(::db::NUM_COLUMNS); let mut db_config = DatabaseConfig::with_columns(::db::NUM_COLUMNS);
@ -220,9 +221,11 @@ impl IoHandler<ClientIoMessage> for ClientIoHandler {
debug!(target: "snapshot", "Failed to initialize periodic snapshot thread: {:?}", e); debug!(target: "snapshot", "Failed to initialize periodic snapshot thread: {:?}", e);
} }
}, },
ClientIoMessage::UpdateSealing => { ClientIoMessage::UpdateSealing => self.client.update_sealing(),
trace!(target: "authorityround", "message: UpdateSealing"); ClientIoMessage::SubmitSeal(ref hash, ref seal) => self.client.submit_seal(*hash, seal.clone()),
self.client.update_sealing() ClientIoMessage::BroadcastMessage(ref message) => self.client.broadcast_consensus_message(message.clone()),
ClientIoMessage::NewMessage(ref message) => if let Err(e) = self.client.engine().handle_message(message) {
trace!(target: "poa", "Invalid message received: {}", e);
}, },
_ => {} // ignore other messages _ => {} // ignore other messages
} }

View File

@ -23,7 +23,7 @@ use service::ClientIoMessage;
use views::HeaderView; use views::HeaderView;
use io::IoChannel; use io::IoChannel;
use util::hash::H256; use util::{H256, Bytes};
use std::sync::Arc; use std::sync::Arc;
@ -107,6 +107,7 @@ impl ChainNotify for Watcher {
_: Vec<H256>, _: Vec<H256>,
_: Vec<H256>, _: Vec<H256>,
_: Vec<H256>, _: Vec<H256>,
_: Vec<Bytes>,
_duration: u64) _duration: u64)
{ {
if self.oracle.is_major_importing() { return } if self.oracle.is_major_importing() { return }
@ -174,6 +175,7 @@ mod tests {
vec![], vec![],
vec![], vec![],
vec![], vec![],
vec![],
0, 0,
); );
} }

View File

@ -17,7 +17,7 @@
//! Spec seal. //! Spec seal.
use rlp::*; use rlp::*;
use util::hash::{H64, H256}; use util::hash::{H64, H256, H520};
use ethjson; use ethjson;
/// Classic ethereum seal. /// Classic ethereum seal.
@ -32,23 +32,55 @@ impl Into<Generic> for Ethereum {
fn into(self) -> Generic { fn into(self) -> Generic {
let mut s = RlpStream::new_list(2); let mut s = RlpStream::new_list(2);
s.append(&self.mix_hash).append(&self.nonce); s.append(&self.mix_hash).append(&self.nonce);
Generic { Generic(s.out())
rlp: s.out()
}
} }
} }
/// Generic seal. /// AuthorityRound seal.
pub struct Generic { pub struct AuthorityRound {
/// Seal rlp. /// Seal step.
pub rlp: Vec<u8>, pub step: usize,
/// Seal signature.
pub signature: H520,
} }
/// Tendermint seal.
pub struct Tendermint {
/// Seal round.
pub round: usize,
/// Proposal seal signature.
pub proposal: H520,
/// Precommit seal signatures.
pub precommits: Vec<H520>,
}
impl Into<Generic> for AuthorityRound {
fn into(self) -> Generic {
let mut s = RlpStream::new_list(2);
s.append(&self.step).append(&self.signature);
Generic(s.out())
}
}
impl Into<Generic> for Tendermint {
fn into(self) -> Generic {
let mut s = RlpStream::new_list(3);
s.append(&self.round).append(&self.proposal).append(&self.precommits);
Generic(s.out())
}
}
pub struct Generic(pub Vec<u8>);
/// Genesis seal type. /// Genesis seal type.
pub enum Seal { pub enum Seal {
/// Classic ethereum seal. /// Classic ethereum seal.
Ethereum(Ethereum), Ethereum(Ethereum),
/// Generic seal. /// AuthorityRound seal.
AuthorityRound(AuthorityRound),
/// Tendermint seal.
Tendermint(Tendermint),
/// Generic RLP seal.
Generic(Generic), Generic(Generic),
} }
@ -59,9 +91,16 @@ impl From<ethjson::spec::Seal> for Seal {
nonce: eth.nonce.into(), nonce: eth.nonce.into(),
mix_hash: eth.mix_hash.into() mix_hash: eth.mix_hash.into()
}), }),
ethjson::spec::Seal::Generic(g) => Seal::Generic(Generic { ethjson::spec::Seal::AuthorityRound(ar) => Seal::AuthorityRound(AuthorityRound {
rlp: g.rlp.into() step: ar.step.into(),
}) signature: ar.signature.into()
}),
ethjson::spec::Seal::Tendermint(tender) => Seal::Tendermint(Tendermint {
round: tender.round.into(),
proposal: tender.proposal.into(),
precommits: tender.precommits.into_iter().map(Into::into).collect()
}),
ethjson::spec::Seal::Generic(g) => Seal::Generic(Generic(g.into())),
} }
} }
} }
@ -70,7 +109,9 @@ impl Into<Generic> for Seal {
fn into(self) -> Generic { fn into(self) -> Generic {
match self { match self {
Seal::Generic(generic) => generic, Seal::Generic(generic) => generic,
Seal::Ethereum(eth) => eth.into() Seal::Ethereum(eth) => eth.into(),
Seal::AuthorityRound(ar) => ar.into(),
Seal::Tendermint(tender) => tender.into(),
} }
} }
} }

View File

@ -18,7 +18,7 @@
use util::*; use util::*;
use builtin::Builtin; use builtin::Builtin;
use engines::{Engine, NullEngine, InstantSeal, BasicAuthority, AuthorityRound}; use engines::{Engine, NullEngine, InstantSeal, BasicAuthority, AuthorityRound, Tendermint};
use pod_state::*; use pod_state::*;
use account_db::*; use account_db::*;
use header::{BlockNumber, Header}; use header::{BlockNumber, Header};
@ -66,8 +66,8 @@ pub struct Spec {
pub name: String, pub name: String,
/// What engine are we using for this? /// What engine are we using for this?
pub engine: Arc<Engine>, pub engine: Arc<Engine>,
/// The fork identifier for this chain. Only needed to distinguish two chains sharing the same genesis. /// Name of the subdir inside the main data dir to use for chain data and settings.
pub fork_name: Option<String>, pub data_dir: String,
/// Known nodes on the network in enode format. /// Known nodes on the network in enode format.
pub nodes: Vec<String>, pub nodes: Vec<String>,
@ -107,13 +107,13 @@ impl From<ethjson::spec::Spec> for Spec {
fn from(s: ethjson::spec::Spec) -> Self { fn from(s: ethjson::spec::Spec) -> Self {
let builtins = s.accounts.builtins().into_iter().map(|p| (p.0.into(), From::from(p.1))).collect(); let builtins = s.accounts.builtins().into_iter().map(|p| (p.0.into(), From::from(p.1))).collect();
let g = Genesis::from(s.genesis); let g = Genesis::from(s.genesis);
let seal: GenericSeal = g.seal.into(); let GenericSeal(seal_rlp) = g.seal.into();
let params = CommonParams::from(s.params); let params = CommonParams::from(s.params);
Spec { Spec {
name: s.name.into(), name: s.name.clone().into(),
params: params.clone(), params: params.clone(),
engine: Spec::engine(s.engine, params, builtins), engine: Spec::engine(s.engine, params, builtins),
fork_name: s.fork_name.map(Into::into), data_dir: s.data_dir.unwrap_or(s.name).into(),
nodes: s.nodes.unwrap_or_else(Vec::new), nodes: s.nodes.unwrap_or_else(Vec::new),
parent_hash: g.parent_hash, parent_hash: g.parent_hash,
transactions_root: g.transactions_root, transactions_root: g.transactions_root,
@ -124,7 +124,7 @@ impl From<ethjson::spec::Spec> for Spec {
gas_used: g.gas_used, gas_used: g.gas_used,
timestamp: g.timestamp, timestamp: g.timestamp,
extra_data: g.extra_data, extra_data: g.extra_data,
seal_rlp: seal.rlp, seal_rlp: seal_rlp,
state_root_memo: RwLock::new(g.state_root), state_root_memo: RwLock::new(g.state_root),
genesis_state: From::from(s.accounts), genesis_state: From::from(s.accounts),
} }
@ -146,7 +146,8 @@ impl Spec {
ethjson::spec::Engine::InstantSeal => Arc::new(InstantSeal::new(params, builtins)), ethjson::spec::Engine::InstantSeal => Arc::new(InstantSeal::new(params, builtins)),
ethjson::spec::Engine::Ethash(ethash) => Arc::new(ethereum::Ethash::new(params, From::from(ethash.params), builtins)), ethjson::spec::Engine::Ethash(ethash) => Arc::new(ethereum::Ethash::new(params, From::from(ethash.params), builtins)),
ethjson::spec::Engine::BasicAuthority(basic_authority) => Arc::new(BasicAuthority::new(params, From::from(basic_authority.params), builtins)), ethjson::spec::Engine::BasicAuthority(basic_authority) => Arc::new(BasicAuthority::new(params, From::from(basic_authority.params), builtins)),
ethjson::spec::Engine::AuthorityRound(authority_round) => AuthorityRound::new(params, From::from(authority_round.params), builtins).expect("Consensus engine could not be started."), ethjson::spec::Engine::AuthorityRound(authority_round) => AuthorityRound::new(params, From::from(authority_round.params), builtins).expect("Failed to start AuthorityRound consensus engine."),
ethjson::spec::Engine::Tendermint(tendermint) => Tendermint::new(params, From::from(tendermint.params), builtins).expect("Failed to start the Tendermint consensus engine."),
} }
} }
@ -208,7 +209,7 @@ impl Spec {
/// Overwrite the genesis components. /// Overwrite the genesis components.
pub fn overwrite_genesis_params(&mut self, g: Genesis) { pub fn overwrite_genesis_params(&mut self, g: Genesis) {
let seal: GenericSeal = g.seal.into(); let GenericSeal(seal_rlp) = g.seal.into();
self.parent_hash = g.parent_hash; self.parent_hash = g.parent_hash;
self.transactions_root = g.transactions_root; self.transactions_root = g.transactions_root;
self.receipts_root = g.receipts_root; self.receipts_root = g.receipts_root;
@ -218,7 +219,7 @@ impl Spec {
self.gas_used = g.gas_used; self.gas_used = g.gas_used;
self.timestamp = g.timestamp; self.timestamp = g.timestamp;
self.extra_data = g.extra_data; self.extra_data = g.extra_data;
self.seal_rlp = seal.rlp; self.seal_rlp = seal_rlp;
self.state_root_memo = RwLock::new(g.state_root); self.state_root_memo = RwLock::new(g.state_root);
} }
@ -275,6 +276,10 @@ impl Spec {
/// Create a new Spec with AuthorityRound consensus which does internal sealing (not requiring work). /// Create a new Spec with AuthorityRound consensus which does internal sealing (not requiring work).
/// Accounts with secrets "0".sha3() and "1".sha3() are the authorities. /// Accounts with secrets "0".sha3() and "1".sha3() are the authorities.
pub fn new_test_round() -> Self { load_bundled!("authority_round") } pub fn new_test_round() -> Self { load_bundled!("authority_round") }
/// Create a new Spec with Tendermint consensus which does internal sealing (not requiring work).
/// Account "0".sha3() and "1".sha3() are a authorities.
pub fn new_test_tendermint() -> Self { load_bundled!("tendermint") }
} }
#[cfg(test)] #[cfg(test)]

View File

@ -457,7 +457,6 @@ impl StateDB {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use util::{U256, H256, FixedHash, Address, DBTransaction}; use util::{U256, H256, FixedHash, Address, DBTransaction};
use tests::helpers::*; use tests::helpers::*;
use state::Account; use state::Account;
@ -531,4 +530,3 @@ mod tests {
assert!(s.get_cached_account(&address).is_none()); assert!(s.get_cached_account(&address).is_none());
} }
} }

View File

@ -21,7 +21,7 @@ use super::trace::{Action, Res};
use header::BlockNumber; use header::BlockNumber;
/// Localized trace. /// Localized trace.
#[derive(Debug, PartialEq, Binary)] #[derive(Debug, PartialEq, Clone, Binary)]
pub struct LocalizedTrace { pub struct LocalizedTrace {
/// Type of action performed by a transaction. /// Type of action performed by a transaction.
pub action: Action, pub action: Action,

View File

@ -18,6 +18,7 @@ use std::ops::{Deref, DerefMut};
use std::cmp::PartialEq; use std::cmp::PartialEq;
use std::{mem, fmt}; use std::{mem, fmt};
use std::str::FromStr; use std::str::FromStr;
use std::hash::{Hash, Hasher};
use secp256k1::{Message as SecpMessage, RecoverableSignature, RecoveryId, Error as SecpError}; use secp256k1::{Message as SecpMessage, RecoverableSignature, RecoveryId, Error as SecpError};
use secp256k1::key::{SecretKey, PublicKey}; use secp256k1::key::{SecretKey, PublicKey};
use rustc_serialize::hex::{ToHex, FromHex}; use rustc_serialize::hex::{ToHex, FromHex};
@ -116,6 +117,18 @@ impl Default for Signature {
} }
} }
impl Hash for Signature {
fn hash<H: Hasher>(&self, state: &mut H) {
H520::from(self.0).hash(state);
}
}
impl Clone for Signature {
fn clone(&self) -> Self {
Signature(self.0)
}
}
impl From<[u8; 65]> for Signature { impl From<[u8; 65]> for Signature {
fn from(s: [u8; 65]) -> Self { fn from(s: [u8; 65]) -> Self {
Signature(s) Signature(s)

View File

@ -18,7 +18,6 @@ use std::{fs, io};
use std::path::{PathBuf, Path}; use std::path::{PathBuf, Path};
use std::collections::HashMap; use std::collections::HashMap;
use time; use time;
use ethkey::Address;
use {json, SafeAccount, Error}; use {json, SafeAccount, Error};
use json::Uuid; use json::Uuid;
use super::KeyDirectory; use super::KeyDirectory;
@ -106,6 +105,11 @@ impl KeyDirectory for DiskDirectory {
Ok(accounts) Ok(accounts)
} }
fn update(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
// Disk store handles updates correctly iff filename is the same
self.insert(account)
}
fn insert(&self, account: SafeAccount) -> Result<SafeAccount, Error> { fn insert(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
// transform account into key file // transform account into key file
let keyfile: json::KeyFile = account.clone().into(); let keyfile: json::KeyFile = account.clone().into();
@ -138,12 +142,12 @@ impl KeyDirectory for DiskDirectory {
Ok(account) Ok(account)
} }
fn remove(&self, address: &Address) -> Result<(), Error> { fn remove(&self, account: &SafeAccount) -> Result<(), Error> {
// enumerate all entries in keystore // enumerate all entries in keystore
// and find entry with given address // and find entry with given address
let to_remove = try!(self.files()) let to_remove = try!(self.files())
.into_iter() .into_iter()
.find(|&(_, ref account)| &account.address == address); .find(|&(_, ref acc)| acc == account);
// remove it // remove it
match to_remove { match to_remove {

View File

@ -16,7 +16,6 @@
use std::env; use std::env;
use std::path::PathBuf; use std::path::PathBuf;
use ethkey::Address;
use {SafeAccount, Error}; use {SafeAccount, Error};
use super::{KeyDirectory, DiskDirectory, DirectoryType}; use super::{KeyDirectory, DiskDirectory, DirectoryType};
@ -89,7 +88,11 @@ impl KeyDirectory for GethDirectory {
self.dir.insert(account) self.dir.insert(account)
} }
fn remove(&self, address: &Address) -> Result<(), Error> { fn update(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
self.dir.remove(address) self.dir.update(account)
}
fn remove(&self, account: &SafeAccount) -> Result<(), Error> {
self.dir.remove(account)
} }
} }

View File

@ -0,0 +1,67 @@
// 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 <http://www.gnu.org/licenses/>.
use std::collections::HashMap;
use parking_lot::RwLock;
use itertools::Itertools;
use ethkey::Address;
use {SafeAccount, Error};
use super::KeyDirectory;
#[derive(Default)]
pub struct MemoryDirectory {
accounts: RwLock<HashMap<Address, Vec<SafeAccount>>>,
}
impl KeyDirectory for MemoryDirectory {
fn load(&self) -> Result<Vec<SafeAccount>, Error> {
Ok(self.accounts.read().values().cloned().flatten().collect())
}
fn update(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
let mut lock = self.accounts.write();
let mut accounts = lock.entry(account.address.clone()).or_insert_with(Vec::new);
// If the filename is the same we just need to replace the entry
accounts.retain(|acc| acc.filename != account.filename);
accounts.push(account.clone());
Ok(account)
}
fn insert(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
let mut lock = self.accounts.write();
let mut accounts = lock.entry(account.address.clone()).or_insert_with(Vec::new);
accounts.push(account.clone());
Ok(account)
}
fn remove(&self, account: &SafeAccount) -> Result<(), Error> {
let mut accounts = self.accounts.write();
let is_empty = if let Some(mut accounts) = accounts.get_mut(&account.address) {
if let Some(position) = accounts.iter().position(|acc| acc == account) {
accounts.remove(position);
}
accounts.is_empty()
} else {
false
};
if is_empty {
accounts.remove(&account.address);
}
Ok(())
}
}

View File

@ -14,12 +14,12 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
use ethkey::Address;
use std::path::{PathBuf}; use std::path::{PathBuf};
use {SafeAccount, Error}; use {SafeAccount, Error};
mod disk; mod disk;
mod geth; mod geth;
mod memory;
mod parity; mod parity;
pub enum DirectoryType { pub enum DirectoryType {
@ -30,10 +30,12 @@ pub enum DirectoryType {
pub trait KeyDirectory: Send + Sync { pub trait KeyDirectory: Send + Sync {
fn load(&self) -> Result<Vec<SafeAccount>, Error>; fn load(&self) -> Result<Vec<SafeAccount>, Error>;
fn insert(&self, account: SafeAccount) -> Result<SafeAccount, Error>; fn insert(&self, account: SafeAccount) -> Result<SafeAccount, Error>;
fn remove(&self, address: &Address) -> Result<(), Error>; fn update(&self, account: SafeAccount) -> Result<SafeAccount, Error>;
fn remove(&self, account: &SafeAccount) -> Result<(), Error>;
fn path(&self) -> Option<&PathBuf> { None } fn path(&self) -> Option<&PathBuf> { None }
} }
pub use self::disk::DiskDirectory; pub use self::disk::DiskDirectory;
pub use self::geth::GethDirectory; pub use self::geth::GethDirectory;
pub use self::memory::MemoryDirectory;
pub use self::parity::ParityDirectory; pub use self::parity::ParityDirectory;

View File

@ -16,7 +16,6 @@
use std::env; use std::env;
use std::path::PathBuf; use std::path::PathBuf;
use ethkey::Address;
use {SafeAccount, Error}; use {SafeAccount, Error};
use super::{KeyDirectory, DiskDirectory, DirectoryType}; use super::{KeyDirectory, DiskDirectory, DirectoryType};
@ -68,7 +67,11 @@ impl KeyDirectory for ParityDirectory {
self.dir.insert(account) self.dir.insert(account)
} }
fn remove(&self, address: &Address) -> Result<(), Error> { fn update(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
self.dir.remove(address) self.dir.update(account)
}
fn remove(&self, account: &SafeAccount) -> Result<(), Error> {
self.dir.remove(account)
} }
} }

View File

@ -16,23 +16,19 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::mem; use std::mem;
use ethkey::KeyPair; use parking_lot::RwLock;
use crypto::KEY_ITERATIONS; use crypto::KEY_ITERATIONS;
use random::Random; use random::Random;
use ethkey::{Signature, Address, Message, Secret, Public}; use ethkey::{Signature, Address, Message, Secret, Public, KeyPair};
use dir::KeyDirectory; use dir::KeyDirectory;
use account::SafeAccount; use account::SafeAccount;
use {Error, SecretStore};
use json;
use json::Uuid;
use parking_lot::RwLock;
use presale::PresaleWallet; use presale::PresaleWallet;
use import; use json::{self, Uuid};
use {import, Error, SimpleSecretStore, SecretStore};
pub struct EthStore { pub struct EthStore {
dir: Box<KeyDirectory>, store: EthMultiStore,
iterations: u32,
cache: RwLock<BTreeMap<Address, SafeAccount>>,
} }
impl EthStore { impl EthStore {
@ -41,57 +37,46 @@ impl EthStore {
} }
pub fn open_with_iterations(directory: Box<KeyDirectory>, iterations: u32) -> Result<Self, Error> { pub fn open_with_iterations(directory: Box<KeyDirectory>, iterations: u32) -> Result<Self, Error> {
let accounts = try!(directory.load()); Ok(EthStore {
let cache = accounts.into_iter().map(|account| (account.address.clone(), account)).collect(); store: try!(EthMultiStore::open_with_iterations(directory, iterations)),
let store = EthStore { })
dir: directory,
iterations: iterations,
cache: RwLock::new(cache),
};
Ok(store)
}
fn save(&self, account: SafeAccount) -> Result<(), Error> {
// save to file
let account = try!(self.dir.insert(account.clone()));
// update cache
let mut cache = self.cache.write();
cache.insert(account.address.clone(), account);
Ok(())
}
fn reload_accounts(&self) -> Result<(), Error> {
let mut cache = self.cache.write();
let accounts = try!(self.dir.load());
let new_accounts: BTreeMap<_, _> = accounts.into_iter().map(|account| (account.address.clone(), account)).collect();
mem::replace(&mut *cache, new_accounts);
Ok(())
} }
fn get(&self, address: &Address) -> Result<SafeAccount, Error> { fn get(&self, address: &Address) -> Result<SafeAccount, Error> {
{ let mut accounts = try!(self.store.get(address)).into_iter();
let cache = self.cache.read(); accounts.next().ok_or(Error::InvalidAccount)
if let Some(account) = cache.get(address) {
return Ok(account.clone())
} }
} }
try!(self.reload_accounts());
let cache = self.cache.read(); impl SimpleSecretStore for EthStore {
cache.get(address).cloned().ok_or(Error::InvalidAccount) fn insert_account(&self, secret: Secret, password: &str) -> Result<Address, Error> {
self.store.insert_account(secret, password)
}
fn accounts(&self) -> Result<Vec<Address>, Error> {
self.store.accounts()
}
fn change_password(&self, address: &Address, old_password: &str, new_password: &str) -> Result<(), Error> {
self.store.change_password(address, old_password, new_password)
}
fn remove_account(&self, address: &Address, password: &str) -> Result<(), Error> {
self.store.remove_account(address, password)
}
fn sign(&self, address: &Address, password: &str, message: &Message) -> Result<Signature, Error> {
let account = try!(self.get(address));
account.sign(password, message)
}
fn decrypt(&self, account: &Address, password: &str, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error> {
let account = try!(self.get(account));
account.decrypt(password, shared_mac, message)
} }
} }
impl SecretStore for EthStore { impl SecretStore for EthStore {
fn insert_account(&self, secret: Secret, password: &str) -> Result<Address, Error> {
let keypair = try!(KeyPair::from_secret(secret).map_err(|_| Error::CreationFailed));
let id: [u8; 16] = Random::random();
let account = SafeAccount::create(&keypair, id, password, self.iterations, "".to_owned(), "{}".to_owned());
let address = account.address.clone();
try!(self.save(account));
Ok(address)
}
fn import_presale(&self, json: &[u8], password: &str) -> Result<Address, Error> { fn import_presale(&self, json: &[u8], password: &str) -> Result<Address, Error> {
let json_wallet = try!(json::PresaleWallet::load(json).map_err(|_| Error::InvalidKeyFile("Invalid JSON format".to_owned()))); let json_wallet = try!(json::PresaleWallet::load(json).map_err(|_| Error::InvalidKeyFile("Invalid JSON format".to_owned())));
let wallet = PresaleWallet::from(json_wallet); let wallet = PresaleWallet::from(json_wallet);
@ -105,48 +90,20 @@ impl SecretStore for EthStore {
let secret = try!(safe_account.crypto.secret(password).map_err(|_| Error::InvalidPassword)); let secret = try!(safe_account.crypto.secret(password).map_err(|_| Error::InvalidPassword));
safe_account.address = try!(KeyPair::from_secret(secret)).address(); safe_account.address = try!(KeyPair::from_secret(secret)).address();
let address = safe_account.address.clone(); let address = safe_account.address.clone();
try!(self.save(safe_account)); try!(self.store.import(safe_account));
Ok(address) Ok(address)
} }
fn accounts(&self) -> Result<Vec<Address>, Error> { fn test_password(&self, address: &Address, password: &str) -> Result<bool, Error> {
try!(self.reload_accounts()); let account = try!(self.get(address));
Ok(self.cache.read().keys().cloned().collect()) Ok(account.check_password(password))
} }
fn change_password(&self, address: &Address, old_password: &str, new_password: &str) -> Result<(), Error> { fn copy_account(&self, new_store: &SimpleSecretStore, address: &Address, password: &str, new_password: &str) -> Result<(), Error> {
// change password
let account = try!(self.get(address)); let account = try!(self.get(address));
let account = try!(account.change_password(old_password, new_password, self.iterations)); let secret = try!(account.crypto.secret(password));
try!(new_store.insert_account(secret, new_password));
// save to file
self.save(account)
}
fn remove_account(&self, address: &Address, password: &str) -> Result<(), Error> {
let can_remove = {
let account = try!(self.get(address));
account.check_password(password)
};
if can_remove {
try!(self.dir.remove(address));
let mut cache = self.cache.write();
cache.remove(address);
Ok(()) Ok(())
} else {
Err(Error::InvalidPassword)
}
}
fn sign(&self, address: &Address, password: &str, message: &Message) -> Result<Signature, Error> {
let account = try!(self.get(address));
account.sign(password, message)
}
fn decrypt(&self, account: &Address, password: &str, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error> {
let account = try!(self.get(account));
account.decrypt(password, shared_mac, message)
} }
fn public(&self, account: &Address, password: &str) -> Result<Public, Error> { fn public(&self, account: &Address, password: &str) -> Result<Public, Error> {
@ -170,23 +127,25 @@ impl SecretStore for EthStore {
} }
fn set_name(&self, address: &Address, name: String) -> Result<(), Error> { fn set_name(&self, address: &Address, name: String) -> Result<(), Error> {
let mut account = try!(self.get(address)); let old = try!(self.get(address));
let mut account = old.clone();
account.name = name; account.name = name;
// save to file // save to file
self.save(account) self.store.update(old, account)
} }
fn set_meta(&self, address: &Address, meta: String) -> Result<(), Error> { fn set_meta(&self, address: &Address, meta: String) -> Result<(), Error> {
let mut account = try!(self.get(address)); let old = try!(self.get(address));
let mut account = old.clone();
account.meta = meta; account.meta = meta;
// save to file // save to file
self.save(account) self.store.update(old, account)
} }
fn local_path(&self) -> String { fn local_path(&self) -> String {
self.dir.path().map(|p| p.to_string_lossy().into_owned()).unwrap_or_else(|| String::new()) self.store.dir.path().map(|p| p.to_string_lossy().into_owned()).unwrap_or_else(|| String::new())
} }
fn list_geth_accounts(&self, testnet: bool) -> Vec<Address> { fn list_geth_accounts(&self, testnet: bool) -> Vec<Address> {
@ -194,6 +153,288 @@ impl SecretStore for EthStore {
} }
fn import_geth_accounts(&self, desired: Vec<Address>, testnet: bool) -> Result<Vec<Address>, Error> { fn import_geth_accounts(&self, desired: Vec<Address>, testnet: bool) -> Result<Vec<Address>, Error> {
import::import_geth_accounts(&*self.dir, desired.into_iter().collect(), testnet) import::import_geth_accounts(&*self.store.dir, desired.into_iter().collect(), testnet)
} }
} }
/// Similar to `EthStore` but may store many accounts (with different passwords) for the same `Address`
pub struct EthMultiStore {
dir: Box<KeyDirectory>,
iterations: u32,
cache: RwLock<BTreeMap<Address, Vec<SafeAccount>>>,
}
impl EthMultiStore {
pub fn open(directory: Box<KeyDirectory>) -> Result<Self, Error> {
Self::open_with_iterations(directory, KEY_ITERATIONS as u32)
}
pub fn open_with_iterations(directory: Box<KeyDirectory>, iterations: u32) -> Result<Self, Error> {
let store = EthMultiStore {
dir: directory,
iterations: iterations,
cache: Default::default(),
};
try!(store.reload_accounts());
Ok(store)
}
fn reload_accounts(&self) -> Result<(), Error> {
let mut cache = self.cache.write();
let accounts = try!(self.dir.load());
let mut new_accounts = BTreeMap::new();
for account in accounts {
let mut entry = new_accounts.entry(account.address.clone()).or_insert_with(Vec::new);
entry.push(account);
}
mem::replace(&mut *cache, new_accounts);
Ok(())
}
fn get(&self, address: &Address) -> Result<Vec<SafeAccount>, Error> {
{
let cache = self.cache.read();
if let Some(accounts) = cache.get(address) {
if !accounts.is_empty() {
return Ok(accounts.clone())
}
}
}
try!(self.reload_accounts());
let cache = self.cache.read();
let accounts = try!(cache.get(address).cloned().ok_or(Error::InvalidAccount));
if accounts.is_empty() {
Err(Error::InvalidAccount)
} else {
Ok(accounts)
}
}
fn import(&self, account: SafeAccount) -> Result<(), Error> {
// save to file
let account = try!(self.dir.insert(account));
// update cache
let mut cache = self.cache.write();
let mut accounts = cache.entry(account.address.clone()).or_insert_with(Vec::new);
accounts.push(account);
Ok(())
}
fn update(&self, old: SafeAccount, new: SafeAccount) -> Result<(), Error> {
// save to file
let account = try!(self.dir.update(new));
// update cache
let mut cache = self.cache.write();
let mut accounts = cache.entry(account.address.clone()).or_insert_with(Vec::new);
// Remove old account
accounts.retain(|acc| acc != &old);
// And push updated to the end
accounts.push(account);
Ok(())
}
}
impl SimpleSecretStore for EthMultiStore {
fn insert_account(&self, secret: Secret, password: &str) -> Result<Address, Error> {
let keypair = try!(KeyPair::from_secret(secret).map_err(|_| Error::CreationFailed));
let id: [u8; 16] = Random::random();
let account = SafeAccount::create(&keypair, id, password, self.iterations, "".to_owned(), "{}".to_owned());
let address = account.address.clone();
try!(self.import(account));
Ok(address)
}
fn accounts(&self) -> Result<Vec<Address>, Error> {
try!(self.reload_accounts());
Ok(self.cache.read().keys().cloned().collect())
}
fn remove_account(&self, address: &Address, password: &str) -> Result<(), Error> {
let accounts = try!(self.get(address));
for account in accounts {
// Skip if password is invalid
if !account.check_password(password) {
continue;
}
// Remove from dir
try!(self.dir.remove(&account));
// Remove from cache
let mut cache = self.cache.write();
let is_empty = {
let mut accounts = cache.get_mut(address).expect("Entry exists, because it was returned by `get`; qed");
if let Some(position) = accounts.iter().position(|acc| acc == &account) {
accounts.remove(position);
}
accounts.is_empty()
};
if is_empty {
cache.remove(address);
}
return Ok(());
}
Err(Error::InvalidPassword)
}
fn change_password(&self, address: &Address, old_password: &str, new_password: &str) -> Result<(), Error> {
let accounts = try!(self.get(address));
for account in accounts {
// Change password
let new_account = try!(account.change_password(old_password, new_password, self.iterations));
try!(self.update(account, new_account));
}
Ok(())
}
fn sign(&self, address: &Address, password: &str, message: &Message) -> Result<Signature, Error> {
let accounts = try!(self.get(address));
for account in accounts {
if account.check_password(password) {
return account.sign(password, message);
}
}
Err(Error::InvalidPassword)
}
fn decrypt(&self, account: &Address, password: &str, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error> {
let accounts = try!(self.get(account));
for account in accounts {
if account.check_password(password) {
return account.decrypt(password, shared_mac, message);
}
}
Err(Error::InvalidPassword)
}
}
#[cfg(test)]
mod tests {
use dir::MemoryDirectory;
use ethkey::{Random, Generator, KeyPair};
use secret_store::{SimpleSecretStore, SecretStore};
use super::{EthStore, EthMultiStore};
fn keypair() -> KeyPair {
Random.generate().unwrap()
}
fn store() -> EthStore {
EthStore::open(Box::new(MemoryDirectory::default())).expect("MemoryDirectory always load successfuly; qed")
}
fn multi_store() -> EthMultiStore {
EthMultiStore::open(Box::new(MemoryDirectory::default())).expect("MemoryDirectory always load successfuly; qed")
}
#[test]
fn should_insert_account_successfully() {
// given
let store = store();
let keypair = keypair();
// when
let address = store.insert_account(keypair.secret().clone(), "test").unwrap();
// then
assert_eq!(address, keypair.address());
assert!(store.get(&address).is_ok(), "Should contain account.");
assert_eq!(store.accounts().unwrap().len(), 1, "Should have one account.");
}
#[test]
fn should_update_meta_and_name() {
// given
let store = store();
let keypair = keypair();
let address = store.insert_account(keypair.secret().clone(), "test").unwrap();
assert_eq!(&store.meta(&address).unwrap(), "{}");
assert_eq!(&store.name(&address).unwrap(), "");
// when
store.set_meta(&address, "meta".into()).unwrap();
store.set_name(&address, "name".into()).unwrap();
// then
assert_eq!(&store.meta(&address).unwrap(), "meta");
assert_eq!(&store.name(&address).unwrap(), "name");
assert_eq!(store.accounts().unwrap().len(), 1);
}
#[test]
fn should_remove_account() {
// given
let store = store();
let keypair = keypair();
let address = store.insert_account(keypair.secret().clone(), "test").unwrap();
// when
store.remove_account(&address, "test").unwrap();
// then
assert_eq!(store.accounts().unwrap().len(), 0, "Should remove account.");
}
#[test]
fn should_return_true_if_password_is_correct() {
// given
let store = store();
let keypair = keypair();
let address = store.insert_account(keypair.secret().clone(), "test").unwrap();
// when
let res1 = store.test_password(&address, "x").unwrap();
let res2 = store.test_password(&address, "test").unwrap();
assert!(!res1, "First password should be invalid.");
assert!(res2, "Second password should be correct.");
}
#[test]
fn multistore_should_be_able_to_have_the_same_account_twice() {
// given
let store = multi_store();
let keypair = keypair();
let address = store.insert_account(keypair.secret().clone(), "test").unwrap();
let address2 = store.insert_account(keypair.secret().clone(), "xyz").unwrap();
assert_eq!(address, address2);
// when
assert!(store.remove_account(&address, "test").is_ok(), "First password should work.");
assert_eq!(store.accounts().unwrap().len(), 1);
assert!(store.remove_account(&address, "xyz").is_ok(), "Second password should work too.");
assert_eq!(store.accounts().unwrap().len(), 0);
}
#[test]
fn should_copy_account() {
// given
let store = store();
let multi_store = multi_store();
let keypair = keypair();
let address = store.insert_account(keypair.secret().clone(), "test").unwrap();
assert_eq!(multi_store.accounts().unwrap().len(), 0);
// when
store.copy_account(&multi_store, &address, "test", "xyz").unwrap();
// then
assert!(store.test_password(&address, "test").unwrap(), "First password should work for store.");
assert!(multi_store.sign(&address, "xyz", &Default::default()).is_ok(), "Second password should work for second store.");
assert_eq!(multi_store.accounts().unwrap().len(), 1);
}
}

View File

@ -50,8 +50,8 @@ mod secret_store;
pub use self::account::SafeAccount; pub use self::account::SafeAccount;
pub use self::error::Error; pub use self::error::Error;
pub use self::ethstore::EthStore; pub use self::ethstore::{EthStore, EthMultiStore};
pub use self::import::{import_accounts, read_geth_accounts}; pub use self::import::{import_accounts, read_geth_accounts};
pub use self::presale::PresaleWallet; pub use self::presale::PresaleWallet;
pub use self::secret_store::SecretStore; pub use self::secret_store::{SimpleSecretStore, SecretStore};
pub use self::random::random_phrase; pub use self::random::{random_phrase, random_string};

View File

@ -51,10 +51,16 @@ pub fn random_phrase(words: usize) -> String {
.map(|s| s.to_owned()) .map(|s| s.to_owned())
.collect(); .collect();
} }
let mut rng = OsRng::new().unwrap(); let mut rng = OsRng::new().expect("Not able to operate without random source.");
(0..words).map(|_| rng.choose(&WORDS).unwrap()).join(" ") (0..words).map(|_| rng.choose(&WORDS).unwrap()).join(" ")
} }
/// Generate a random string of given length.
pub fn random_string(length: usize) -> String {
let mut rng = OsRng::new().expect("Not able to operate without random source.");
rng.gen_ascii_chars().take(length).collect()
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::random_phrase; use super::random_phrase;

View File

@ -18,18 +18,25 @@ use ethkey::{Address, Message, Signature, Secret, Public};
use Error; use Error;
use json::Uuid; use json::Uuid;
pub trait SecretStore: Send + Sync { pub trait SimpleSecretStore: Send + Sync {
fn insert_account(&self, secret: Secret, password: &str) -> Result<Address, Error>; fn insert_account(&self, secret: Secret, password: &str) -> Result<Address, Error>;
fn import_presale(&self, json: &[u8], password: &str) -> Result<Address, Error>;
fn import_wallet(&self, json: &[u8], password: &str) -> Result<Address, Error>;
fn change_password(&self, account: &Address, old_password: &str, new_password: &str) -> Result<(), Error>; fn change_password(&self, account: &Address, old_password: &str, new_password: &str) -> Result<(), Error>;
fn remove_account(&self, account: &Address, password: &str) -> Result<(), Error>; fn remove_account(&self, account: &Address, password: &str) -> Result<(), Error>;
fn sign(&self, account: &Address, password: &str, message: &Message) -> Result<Signature, Error>; fn sign(&self, account: &Address, password: &str, message: &Message) -> Result<Signature, Error>;
fn decrypt(&self, account: &Address, password: &str, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error>; fn decrypt(&self, account: &Address, password: &str, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error>;
fn public(&self, account: &Address, password: &str) -> Result<Public, Error>;
fn accounts(&self) -> Result<Vec<Address>, Error>; fn accounts(&self) -> Result<Vec<Address>, Error>;
}
pub trait SecretStore: SimpleSecretStore {
fn import_presale(&self, json: &[u8], password: &str) -> Result<Address, Error>;
fn import_wallet(&self, json: &[u8], password: &str) -> Result<Address, Error>;
fn copy_account(&self, new_store: &SimpleSecretStore, account: &Address, password: &str, new_password: &str) -> Result<(), Error>;
fn test_password(&self, account: &Address, password: &str) -> Result<bool, Error>;
fn public(&self, account: &Address, password: &str) -> Result<Public, Error>;
fn uuid(&self, account: &Address) -> Result<Uuid, Error>; fn uuid(&self, account: &Address) -> Result<Uuid, Error>;
fn name(&self, account: &Address) -> Result<String, Error>; fn name(&self, account: &Address) -> Result<String, Error>;
fn meta(&self, account: &Address) -> Result<String, Error>; fn meta(&self, account: &Address) -> Result<String, Error>;

View File

@ -19,7 +19,7 @@ extern crate ethstore;
mod util; mod util;
use ethstore::{SecretStore, EthStore}; use ethstore::{EthStore, SimpleSecretStore};
use ethstore::ethkey::{Random, Generator, Secret, KeyPair, verify_address}; use ethstore::ethkey::{Random, Generator, Secret, KeyPair, verify_address};
use ethstore::dir::DiskDirectory; use ethstore::dir::DiskDirectory;
use util::TransientDir; use util::TransientDir;

View File

@ -18,7 +18,6 @@ use std::path::PathBuf;
use std::{env, fs}; use std::{env, fs};
use rand::{Rng, OsRng}; use rand::{Rng, OsRng};
use ethstore::dir::{KeyDirectory, DiskDirectory}; use ethstore::dir::{KeyDirectory, DiskDirectory};
use ethstore::ethkey::Address;
use ethstore::{Error, SafeAccount}; use ethstore::{Error, SafeAccount};
pub fn random_dir() -> PathBuf { pub fn random_dir() -> PathBuf {
@ -64,11 +63,15 @@ impl KeyDirectory for TransientDir {
self.dir.load() self.dir.load()
} }
fn update(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
self.dir.update(account)
}
fn insert(&self, account: SafeAccount) -> Result<SafeAccount, Error> { fn insert(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
self.dir.insert(account) self.dir.insert(account)
} }
fn remove(&self, address: &Address) -> Result<(), Error> { fn remove(&self, account: &SafeAccount) -> Result<(), Error> {
self.dir.remove(address) self.dir.remove(account)
} }
} }

8
js/.stylelintrc.json Normal file
View File

@ -0,0 +1,8 @@
{
"extends": "stylelint-config-standard",
"rules": {
"selector-pseudo-class-no-unknown": [
true, { "ignorePseudoClasses": ["global"] }
]
}
}

View File

@ -1,6 +1,6 @@
{ {
"name": "parity.js", "name": "parity.js",
"version": "0.2.119", "version": "0.2.127",
"main": "release/index.js", "main": "release/index.js",
"jsnext:main": "src/index.js", "jsnext:main": "src/index.js",
"author": "Parity Team <admin@parity.io>", "author": "Parity Team <admin@parity.io>",
@ -38,8 +38,11 @@
"start:app": "node webpack/dev.server", "start:app": "node webpack/dev.server",
"clean": "rm -rf ./build ./coverage", "clean": "rm -rf ./build ./coverage",
"coveralls": "npm run testCoverage && coveralls < coverage/lcov.info", "coveralls": "npm run testCoverage && coveralls < coverage/lcov.info",
"lint": "eslint --ignore-path .gitignore ./src/", "lint": "npm run lint:css && npm run lint:js",
"lint:cached": "eslint --cache --ignore-path .gitignore ./src/", "lint:cached": "npm run lint:css && npm run lint:js:cached",
"lint:css": "stylelint ./src/**/*.css",
"lint:js": "eslint --ignore-path .gitignore ./src/",
"lint:js:cached": "eslint --cache --ignore-path .gitignore ./src/",
"test": "NODE_ENV=test mocha 'src/**/*.spec.js'", "test": "NODE_ENV=test mocha 'src/**/*.spec.js'",
"test:coverage": "NODE_ENV=test istanbul cover _mocha -- 'src/**/*.spec.js'", "test:coverage": "NODE_ENV=test istanbul cover _mocha -- 'src/**/*.spec.js'",
"test:e2e": "NODE_ENV=test mocha 'src/**/*.e2e.js'", "test:e2e": "NODE_ENV=test mocha 'src/**/*.e2e.js'",
@ -118,6 +121,8 @@
"sinon-as-promised": "4.0.2", "sinon-as-promised": "4.0.2",
"sinon-chai": "2.8.0", "sinon-chai": "2.8.0",
"style-loader": "0.13.1", "style-loader": "0.13.1",
"stylelint": "7.6.0",
"stylelint-config-standard": "15.0.0",
"url-loader": "0.5.7", "url-loader": "0.5.7",
"webpack": "2.1.0-beta.27", "webpack": "2.1.0-beta.27",
"webpack-dev-middleware": "1.8.4", "webpack-dev-middleware": "1.8.4",

View File

@ -0,0 +1,33 @@
// 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 <http://www.gnu.org/licenses/>.
import { stringify } from 'querystring';
export const postToServer = (query, isTestnet = false) => {
const port = isTestnet ? 28443 : 18443;
query = stringify(query);
return fetch(`https://email-verification.parity.io:${port}/?` + query, {
method: 'POST', mode: 'cors', cache: 'no-store'
})
.then((res) => {
return res.json().then((data) => {
if (res.ok) {
return data.message;
}
throw new Error(data.message || 'unknown error');
});
});
};

View File

@ -0,0 +1,23 @@
// 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 <http://www.gnu.org/licenses/>.
import React from 'react';
export default (
<ul>
<li>todo</li>
</ul>
);

View File

@ -15,17 +15,6 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { stringify } from 'querystring'; import { stringify } from 'querystring';
import React from 'react';
export const termsOfService = (
<ul>
<li>This privacy notice relates to your use of the Parity SMS verification service. We take your privacy seriously and deal in an honest, direct and transparent way when it comes to your data.</li>
<li>We collect your phone number when you use this service. This is temporarily kept in memory, and then encrypted and stored in our EU servers. We only retain the cryptographic hash of the number to prevent duplicated accounts. You consent to this use.</li>
<li>You pay a fee for the cost of this service using the account you want to verify.</li>
<li>Your phone number is transmitted to a third party US SMS verification service Twilio for the sole purpose of the SMS verification. You consent to this use. Twilios privacy policy is here: <a href={ 'https://www.twilio.com/legal/privacy/developer' }>https://www.twilio.com/legal/privacy/developer</a>.</li>
<li><i>Parity Technology Limited</i> is registered in England and Wales under company number <code>09760015</code> and complies with the Data Protection Act 1998 (UK). You may contact us via email at <a href={ 'mailto:admin@parity.io' }>admin@parity.io</a>. Our general privacy policy can be found here: <a href={ 'https://ethcore.io/legal.html' }>https://ethcore.io/legal.html</a>.</li>
</ul>
);
export const postToServer = (query, isTestnet = false) => { export const postToServer = (query, isTestnet = false) => {
const port = isTestnet ? 8443 : 443; const port = isTestnet ? 8443 : 443;

View File

@ -0,0 +1,27 @@
// 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 <http://www.gnu.org/licenses/>.
import React from 'react';
export default (
<ul>
<li>This privacy notice relates to your use of the Parity SMS verification service. We take your privacy seriously and deal in an honest, direct and transparent way when it comes to your data.</li>
<li>We collect your phone number when you use this service. This is temporarily kept in memory, and then encrypted and stored in our EU servers. We only retain the cryptographic hash of the number to prevent duplicated accounts. You consent to this use.</li>
<li>You pay a fee for the cost of this service using the account you want to verify.</li>
<li>Your phone number is transmitted to a third party US SMS verification service Twilio for the sole purpose of the SMS verification. You consent to this use. Twilios privacy policy is here: <a href={ 'https://www.twilio.com/legal/privacy/developer' }>https://www.twilio.com/legal/privacy/developer</a>.</li>
<li><i>Parity Technology Limited</i> is registered in England and Wales under company number <code>09760015</code> and complies with the Data Protection Act 1998 (UK). You may contact us via email at <a href={ 'mailto:admin@parity.io' }>admin@parity.io</a>. Our general privacy policy can be found here: <a href={ 'https://ethcore.io/legal.html' }>https://ethcore.io/legal.html</a>.</li>
</ul>
);

View File

@ -342,7 +342,8 @@ export default class Contract {
options: _options, options: _options,
autoRemove, autoRemove,
callback, callback,
filterId filterId,
id: subscriptionId
}; };
if (skipInitFetch) { if (skipInitFetch) {
@ -452,13 +453,13 @@ export default class Contract {
}) })
) )
.then((logsArray) => { .then((logsArray) => {
logsArray.forEach((logs, subscriptionId) => { logsArray.forEach((logs, index) => {
if (!logs || !logs.length) { if (!logs || !logs.length) {
return; return;
} }
try { try {
this.sendData(subscriptionId, null, this.parseEventLogs(logs)); this._sendData(subscriptions[index].id, null, this.parseEventLogs(logs));
} catch (error) { } catch (error) {
console.error('_sendSubscriptionChanges', error); console.error('_sendSubscriptionChanges', error);
} }

View File

@ -1,7 +1,28 @@
// Copyright 2015, 2016 Parity Technologies (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 <http://www.gnu.org/licenses/>.
import blockies from 'blockies'; import blockies from 'blockies';
// jsdom doesn't have all the browser features, blockies fail
const TEST_ENV = process.env.NODE_ENV === 'test';
export function createIdentityImg (address, scale = 8) { export function createIdentityImg (address, scale = 8) {
return blockies({ return TEST_ENV
? ''
: blockies({
seed: (address || '').toLowerCase(), seed: (address || '').toLowerCase(),
size: 8, size: 8,
scale scale

View File

@ -0,0 +1 @@
[{"constant":false,"inputs":[{"name":"_new","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes32"}],"name":"reverse","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_who","type":"address"},{"name":"_puzzle","type":"bytes32"},{"name":"_emailHash","type":"bytes32"}],"name":"puzzle","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"getAddress","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_emailHash","type":"bytes32"}],"name":"request","outputs":[],"payable":true,"type":"function"},{"constant":false,"inputs":[{"name":"_new","type":"uint256"}],"name":"setFee","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_code","type":"bytes32"}],"name":"confirm","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"drain","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"getUint","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"}],"name":"certified","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"fee","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"get","outputs":[{"name":"","type":"bytes32"}],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"},{"indexed":false,"name":"emailHash","type":"bytes32"}],"name":"Requested","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"},{"indexed":true,"name":"emailHash","type":"bytes32"},{"indexed":false,"name":"puzzle","type":"bytes32"}],"name":"Puzzled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"}],"name":"Confirmed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"}],"name":"Revoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"old","type":"address"},{"indexed":true,"name":"current","type":"address"}],"name":"NewOwner","type":"event"}]

View File

@ -19,6 +19,7 @@ import basiccoin from './basiccoin.json';
import basiccoinmanager from './basiccoinmanager.json'; import basiccoinmanager from './basiccoinmanager.json';
import dappreg from './dappreg.json'; import dappreg from './dappreg.json';
import eip20 from './eip20.json'; import eip20 from './eip20.json';
import emailverification from './email-verification.json';
import gavcoin from './gavcoin.json'; import gavcoin from './gavcoin.json';
import githubhint from './githubhint.json'; import githubhint from './githubhint.json';
import owned from './owned.json'; import owned from './owned.json';
@ -34,6 +35,7 @@ export {
basiccoinmanager, basiccoinmanager,
dappreg, dappreg,
eip20, eip20,
emailverification,
gavcoin, gavcoin,
githubhint, githubhint,
owned, owned,

View File

@ -18,7 +18,8 @@ import { bytesToHex, hex2Ascii } from '~/api/util/format';
import ABI from './abi/certifier.json'; import ABI from './abi/certifier.json';
const ZERO = '0x0000000000000000000000000000000000000000000000000000000000000000'; const ZERO20 = '0x0000000000000000000000000000000000000000';
const ZERO32 = '0x0000000000000000000000000000000000000000000000000000000000000000';
export default class BadgeReg { export default class BadgeReg {
constructor (api, registry) { constructor (api, registry) {
@ -26,32 +27,57 @@ export default class BadgeReg {
this._registry = registry; this._registry = registry;
registry.getContract('badgereg'); registry.getContract('badgereg');
this.certifiers = {}; // by name this.certifiers = []; // by id
this.contracts = {}; // by name this.contracts = {}; // by name
} }
fetchCertifier (name) { certifierCount () {
if (this.certifiers[name]) { return this._registry.getContract('badgereg')
return Promise.resolve(this.certifiers[name]); .then((badgeReg) => {
return badgeReg.instance.badgeCount.call({}, [])
.then((count) => count.valueOf());
});
}
fetchCertifier (id) {
if (this.certifiers[id]) {
return Promise.resolve(this.certifiers[id]);
} }
return this._registry.getContract('badgereg') return this._registry.getContract('badgereg')
.then((badgeReg) => { .then((badgeReg) => {
return badgeReg.instance.fromName.call({}, [name]) return badgeReg.instance.badge.call({}, [ id ]);
.then(([ id, address ]) => { })
return Promise.all([ .then(([ address, name ]) => {
badgeReg.instance.meta.call({}, [id, 'TITLE']), if (address === ZERO20) {
badgeReg.instance.meta.call({}, [id, 'IMG']) throw new Error(`Certifier ${id} does not exist.`);
]) }
.then(([ title, img ]) => {
title = bytesToHex(title);
title = title === ZERO ? null : hex2Ascii(title);
if (bytesToHex(img) === ZERO) img = null;
const data = { address, name, title, icon: img }; name = bytesToHex(name);
this.certifiers[name] = data; name = name === ZERO32
? null
: hex2Ascii(name);
return this.fetchMeta(id)
.then(({ title, icon }) => {
const data = { address, id, name, title, icon };
this.certifiers[id] = data;
return data; return data;
}); });
}); });
}
fetchMeta (id) {
return this._registry.getContract('badgereg')
.then((badgeReg) => {
return Promise.all([
badgeReg.instance.meta.call({}, [id, 'TITLE']),
badgeReg.instance.meta.call({}, [id, 'IMG'])
]);
})
.then(([ title, icon ]) => {
title = bytesToHex(title);
title = title === ZERO32 ? null : hex2Ascii(title);
if (bytesToHex(icon) === ZERO32) icon = null;
return { title, icon };
}); });
} }

View File

@ -19,7 +19,7 @@ import Registry from './registry';
import SignatureReg from './signaturereg'; import SignatureReg from './signaturereg';
import TokenReg from './tokenreg'; import TokenReg from './tokenreg';
import GithubHint from './githubhint'; import GithubHint from './githubhint';
import * as smsVerification from './sms-verification'; import * as verification from './verification';
import BadgeReg from './badgereg'; import BadgeReg from './badgereg';
let instance = null; let instance = null;
@ -58,7 +58,11 @@ export default class Contracts {
} }
get smsVerification () { get smsVerification () {
return smsVerification; return verification;
}
get emailVerification () {
return verification;
} }
static create (api) { static create (api) {

View File

@ -14,4 +14,5 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './SMSVerification'; import './parity';
import './web3';

View File

@ -58,23 +58,23 @@ export default class DetailsStep extends Component {
<Form> <Form>
{ this.renderWarning() } { this.renderWarning() }
<AddressSelect <AddressSelect
label='from account'
hint='the account to transact with'
value={ fromAddress }
error={ fromAddressError }
accounts={ accounts } accounts={ accounts }
balances={ balances } balances={ balances }
onChange={ onFromAddressChange } /> error={ fromAddressError }
hint='the account to transact with'
label='from account'
onChange={ onFromAddressChange }
value={ fromAddress } />
{ this.renderFunctionSelect() } { this.renderFunctionSelect() }
{ this.renderParameters() } { this.renderParameters() }
<div className={ styles.columns }> <div className={ styles.columns }>
<div> <div>
<Input <Input
label='transaction value (in ETH)'
hint='the amount to send to with the transaction'
value={ amount }
error={ amountError } error={ amountError }
onSubmit={ onAmountChange } /> hint='the amount to send to with the transaction'
label='transaction value (in ETH)'
onSubmit={ onAmountChange }
value={ amount } />
</div> </div>
<div> <div>
<Checkbox <Checkbox

View File

@ -0,0 +1,83 @@
// Copyright 2015, 2016 Parity Technologies (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 <http://www.gnu.org/licenses/>.
import { mount } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import { ContextProvider, muiTheme } from '~/ui';
import DetailsStep from './';
import { CONTRACT } from '../executeContract.test.js';
let component;
let onAmountChange;
let onClose;
let onFromAddressChange;
let onFuncChange;
let onGasEditClick;
let onValueChange;
function render (props) {
onAmountChange = sinon.stub();
onClose = sinon.stub();
onFromAddressChange = sinon.stub();
onFuncChange = sinon.stub();
onGasEditClick = sinon.stub();
onValueChange = sinon.stub();
component = mount(
<ContextProvider api={ {} } muiTheme={ muiTheme } store={ {} }>
<DetailsStep
{ ...props }
contract={ CONTRACT }
onAmountChange={ onAmountChange }
onClose={ onClose }
onFromAddressChange={ onFromAddressChange }
onFuncChange={ onFuncChange }
onGasEditClick={ onGasEditClick }
onValueChange={ onValueChange } />
</ContextProvider>
);
return component;
}
describe('modals/ExecuteContract/DetailsStep', () => {
it('renders', () => {
expect(render({ accounts: {}, values: [ true ], valuesError: [ null ] })).to.be.ok;
});
describe('parameter values', () => {
beforeEach(() => {
render({
accounts: {},
func: CONTRACT.functions[0],
values: [ false ],
valuesError: [ null ]
});
});
describe('bool parameters', () => {
it('toggles from false to true', () => {
component.find('DropDownMenu').last().simulate('change', { target: { value: 'true' } });
expect(onValueChange).to.have.been.calledWith(null, 0, true);
});
});
});
});

View File

@ -25,6 +25,7 @@ import ContentClear from 'material-ui/svg-icons/content/clear';
import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back'; import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward'; import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
import { toWei } from '~/api/util/wei';
import { BusyStep, Button, CompletedStep, GasPriceEditor, IdentityIcon, Modal, TxHash } from '~/ui'; import { BusyStep, Button, CompletedStep, GasPriceEditor, IdentityIcon, Modal, TxHash } from '~/ui';
import { MAX_GAS_ESTIMATION } from '~/util/constants'; import { MAX_GAS_ESTIMATION } from '~/util/constants';
import { validateAddress, validateUint } from '~/util/validation'; import { validateAddress, validateUint } from '~/util/validation';
@ -56,12 +57,12 @@ class ExecuteContract extends Component {
} }
static propTypes = { static propTypes = {
isTest: PropTypes.bool,
fromAddress: PropTypes.string,
accounts: PropTypes.object, accounts: PropTypes.object,
balances: PropTypes.object, balances: PropTypes.object,
contract: PropTypes.object, contract: PropTypes.object.isRequired,
fromAddress: PropTypes.string,
gasLimit: PropTypes.object.isRequired, gasLimit: PropTypes.object.isRequired,
isTest: PropTypes.bool,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
onFromAddressChange: PropTypes.func.isRequired onFromAddressChange: PropTypes.func.isRequired
} }
@ -77,11 +78,11 @@ class ExecuteContract extends Component {
funcError: null, funcError: null,
gasEdit: false, gasEdit: false,
rejected: false, rejected: false,
step: STEP_DETAILS,
sending: false, sending: false,
step: STEP_DETAILS,
txhash: null,
values: [], values: [],
valuesError: [], valuesError: []
txhash: null
} }
componentDidMount () { componentDidMount () {
@ -255,10 +256,6 @@ class ExecuteContract extends Component {
valueError = validateAddress(_value).addressError; valueError = validateAddress(_value).addressError;
break; break;
case 'bool':
value = _value === 'true';
break;
case 'uint': case 'uint':
valueError = validateUint(_value).valueError; valueError = validateUint(_value).valueError;
break; break;
@ -278,13 +275,12 @@ class ExecuteContract extends Component {
} }
estimateGas = (_fromAddress) => { estimateGas = (_fromAddress) => {
const { api } = this.context;
const { fromAddress } = this.props; const { fromAddress } = this.props;
const { amount, func, values } = this.state; const { amount, func, values } = this.state;
const options = { const options = {
gas: MAX_GAS_ESTIMATION, gas: MAX_GAS_ESTIMATION,
from: _fromAddress || fromAddress, from: _fromAddress || fromAddress,
value: api.util.toWei(amount || 0) value: toWei(amount || 0)
}; };
if (!func) { if (!func) {

View File

@ -0,0 +1,69 @@
// Copyright 2015, 2016 Parity Technologies (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 <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import ExecuteContract from './';
import { CONTRACT, STORE } from './executeContract.test.js';
let component;
let onClose;
let onFromAddressChange;
function render (props) {
onClose = sinon.stub();
onFromAddressChange = sinon.stub();
component = shallow(
<ExecuteContract
{ ...props }
contract={ CONTRACT }
onClose={ onClose }
onFromAddressChange={ onFromAddressChange } />,
{ context: { api: {}, store: STORE } }
).find('ExecuteContract').shallow();
return component;
}
describe('modals/ExecuteContract/DetailsStep', () => {
it('renders', () => {
expect(render({ accounts: {} })).to.be.ok;
});
describe('instance functions', () => {
beforeEach(() => {
render({
accounts: {}
});
});
describe('onValueChange', () => {
it('toggles boolean from false to true', () => {
component.setState({
func: CONTRACT.functions[0],
values: [false]
});
component.instance().onValueChange(null, 0, true);
expect(component.state().values).to.deep.equal([true]);
});
});
});
});

View File

@ -0,0 +1,64 @@
// Copyright 2015, 2016 Parity Technologies (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 <http://www.gnu.org/licenses/>.
import BigNumber from 'bignumber.js';
import sinon from 'sinon';
const CONTRACT = {
functions: [
{
name: 'test_a',
signature: 'test_a',
estimateGas: sinon.stub().resolves(new BigNumber(123)),
inputs: [
{
name: 'test_bool',
kind: {
type: 'bool'
}
}
],
abi: {
inputs: [
{
name: 'test_bool',
type: 'bool'
}
]
}
}
]
};
const STORE = {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
balances: {
balances: {}
},
nodeStatus: {
gasLimit: new BigNumber(123)
}
};
}
};
export {
CONTRACT,
STORE
};

View File

@ -15,6 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import ActionDone from 'material-ui/svg-icons/action/done'; import ActionDone from 'material-ui/svg-icons/action/done';
import ActionDoneAll from 'material-ui/svg-icons/action/done-all'; import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward'; import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
@ -35,14 +36,15 @@ import ParityLogo from '../../../assets/images/parity-logo-black-no-text.svg';
const STAGE_NAMES = ['welcome', 'terms', 'new account', 'recovery', 'completed']; const STAGE_NAMES = ['welcome', 'terms', 'new account', 'recovery', 'completed'];
export default class FirstRun extends Component { class FirstRun extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired, api: PropTypes.object.isRequired,
store: PropTypes.object.isRequired store: PropTypes.object.isRequired
} }
static propTypes = { static propTypes = {
visible: PropTypes.bool, hasAccounts: PropTypes.bool.isRequired,
visible: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired onClose: PropTypes.func.isRequired
} }
@ -109,6 +111,7 @@ export default class FirstRun extends Component {
} }
renderDialogActions () { renderDialogActions () {
const { hasAccounts } = this.props;
const { canCreate, stage, hasAcceptedTnc } = this.state; const { canCreate, stage, hasAcceptedTnc } = this.state;
switch (stage) { switch (stage) {
@ -130,13 +133,26 @@ export default class FirstRun extends Component {
); );
case 2: case 2:
return ( const buttons = [
<Button <Button
icon={ <ActionDone /> } icon={ <ActionDone /> }
label='Create' label='Create'
key='create'
disabled={ !canCreate } disabled={ !canCreate }
onClick={ this.onCreate } /> onClick={ this.onCreate }
/>
];
if (hasAccounts) {
buttons.unshift(
<Button
icon={ <NavigationArrowForward /> }
label='Skip'
key='skip'
onClick={ this.skipAccountCreation }
/>
); );
}
return buttons;
case 3: case 3:
return [ return [
@ -219,6 +235,10 @@ export default class FirstRun extends Component {
}); });
} }
skipAccountCreation = () => {
this.setState({ stage: this.state.stage + 2 });
}
newError = (error) => { newError = (error) => {
const { store } = this.context; const { store } = this.context;
@ -232,3 +252,9 @@ export default class FirstRun extends Component {
print(recoveryPage({ phrase, name, identity, address, logo: ParityLogo })); print(recoveryPage({ phrase, name, identity, address, logo: ParityLogo }));
} }
} }
function mapStateToProps (state) {
return { hasAccounts: state.personal.hasAccounts };
}
export default connect(mapStateToProps, null)(FirstRun);

View File

@ -15,10 +15,6 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/ */
.list li {
padding: .1em 0;
}
.spacing { .spacing {
margin-top: 1.5em; margin-top: 1.5em;
} }

View File

@ -25,41 +25,33 @@ import { fromWei } from '~/api/util/wei';
import { Form, Input } from '~/ui'; import { Form, Input } from '~/ui';
import { nullableProptype } from '~/util/proptypes'; import { nullableProptype } from '~/util/proptypes';
import { termsOfService } from '../../../3rdparty/sms-verification'; import smsTermsOfService from '~/3rdparty/sms-verification/terms-of-service';
import emailTermsOfService from '~/3rdparty/email-verification/terms-of-service';
import { howSMSVerificationWorks, howEmailVerificationWorks } from '../how-it-works';
import styles from './gatherData.css'; import styles from './gatherData.css';
export default class GatherData extends Component { export default class GatherData extends Component {
static propTypes = { static propTypes = {
fee: React.PropTypes.instanceOf(BigNumber), fee: React.PropTypes.instanceOf(BigNumber),
isNumberValid: PropTypes.bool.isRequired, method: PropTypes.string.isRequired,
fields: PropTypes.array.isRequired,
isVerified: nullableProptype(PropTypes.bool.isRequired), isVerified: nullableProptype(PropTypes.bool.isRequired),
hasRequested: nullableProptype(PropTypes.bool.isRequired), hasRequested: nullableProptype(PropTypes.bool.isRequired),
setNumber: PropTypes.func.isRequired,
setConsentGiven: PropTypes.func.isRequired setConsentGiven: PropTypes.func.isRequired
} }
render () { render () {
const { isNumberValid, isVerified } = this.props; const { method, isVerified } = this.props;
const termsOfService = method === 'email' ? emailTermsOfService : smsTermsOfService;
const howItWorks = method === 'email' ? howEmailVerificationWorks : howSMSVerificationWorks;
return ( return (
<Form> <Form>
<p>The following steps will let you prove that you control both an account and a phone number.</p> { howItWorks }
<ol className={ styles.list }>
<li>You send a verification request to a specific contract.</li>
<li>Our server puts a puzzle into this contract.</li>
<li>The code you receive via SMS is the solution to this puzzle.</li>
</ol>
{ this.renderFee() } { this.renderFee() }
{ this.renderCertified() } { this.renderCertified() }
{ this.renderRequested() } { this.renderRequested() }
<Input { this.renderFields() }
label={ 'phone number in international format' }
hint={ 'the SMS will be sent to this number' }
error={ isNumberValid ? null : 'invalid number' }
disabled={ isVerified }
onChange={ this.numberOnChange }
onSubmit={ this.numberOnSubmit }
/>
<Checkbox <Checkbox
className={ styles.spacing } className={ styles.spacing }
label={ 'I agree to the terms and conditions below.' } label={ 'I agree to the terms and conditions below.' }
@ -136,12 +128,28 @@ export default class GatherData extends Component {
); );
} }
numberOnSubmit = (value) => { renderFields () {
this.props.setNumber(value); const { isVerified, fields } = this.props;
}
numberOnChange = (_, value) => { const rendered = fields.map((field) => {
this.props.setNumber(value); const onChange = (_, v) => {
field.onChange(v);
};
const onSubmit = field.onChange;
return (
<Input
key={ field.key }
label={ field.label }
hint={ field.hint }
error={ field.error }
disabled={ isVerified }
onChange={ onChange }
onSubmit={ onSubmit }
/>
);
});
return (<div>{rendered}</div>);
} }
consentOnChange = (_, consentGiven) => { consentOnChange = (_, consentGiven) => {

View File

@ -20,20 +20,25 @@ import { Form, Input } from '~/ui';
export default class QueryCode extends Component { export default class QueryCode extends Component {
static propTypes = { static propTypes = {
number: PropTypes.string.isRequired, receiver: PropTypes.string.isRequired,
hint: PropTypes.string,
isCodeValid: PropTypes.bool.isRequired, isCodeValid: PropTypes.bool.isRequired,
setCode: PropTypes.func.isRequired setCode: PropTypes.func.isRequired
} }
static defaultProps = {
hint: 'Enter the code you received.'
}
render () { render () {
const { number, isCodeValid } = this.props; const { receiver, hint, isCodeValid } = this.props;
return ( return (
<Form> <Form>
<p>The verification code has been sent to { number }.</p> <p>The verification code has been sent to { receiver }.</p>
<Input <Input
label={ 'verification code' } label={ 'verification code' }
hint={ 'Enter the code you received via SMS.' } hint={ hint }
error={ isCodeValid ? null : 'invalid code' } error={ isCodeValid ? null : 'invalid code' }
onChange={ this.onChange } onChange={ this.onChange }
onSubmit={ this.onSubmit } onSubmit={ this.onSubmit }

View File

@ -19,7 +19,7 @@ import React, { Component, PropTypes } from 'react';
import { nullableProptype } from '~/util/proptypes'; import { nullableProptype } from '~/util/proptypes';
import TxHash from '~/ui/TxHash'; import TxHash from '~/ui/TxHash';
import { import {
POSTING_REQUEST, POSTED_REQUEST, REQUESTING_SMS POSTING_REQUEST, POSTED_REQUEST, REQUESTING_CODE
} from '../store'; } from '../store';
import styles from './sendRequest.css'; import styles from './sendRequest.css';
@ -45,9 +45,9 @@ export default class SendRequest extends Component {
</div> </div>
); );
case REQUESTING_SMS: case REQUESTING_CODE:
return ( return (
<p>Requesting an SMS from the Parity server and waiting for the puzzle to be put into the contract.</p> <p>Requesting a code from the Parity server and waiting for the puzzle to be put into the contract.</p>
); );
default: default:

View File

@ -0,0 +1,72 @@
// 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 <http://www.gnu.org/licenses/>.
import { observable, computed, action } from 'mobx';
import { sha3 } from '~/api/util/sha3';
import EmailVerificationABI from '~/contracts/abi/email-verification.json';
import VerificationStore, {
LOADING, QUERY_DATA, QUERY_CODE, POSTED_CONFIRMATION, DONE
} from './store';
import { postToServer } from '../../3rdparty/email-verification';
const EMAIL_VERIFICATION = 4; // id in the `BadgeReg.sol` contract
export default class EmailVerificationStore extends VerificationStore {
@observable email = '';
@computed get isEmailValid () {
// See https://davidcel.is/posts/stop-validating-email-addresses-with-regex/
return this.email && this.email.indexOf('@') >= 0;
}
@computed get isStepValid () {
if (this.step === DONE) {
return true;
}
if (this.error) {
return false;
}
switch (this.step) {
case LOADING:
return this.contract && this.fee && this.isVerified !== null && this.hasRequested !== null;
case QUERY_DATA:
return this.isEmailValid && this.consentGiven;
case QUERY_CODE:
return this.requestTx && this.isCodeValid === true;
case POSTED_CONFIRMATION:
return !!this.confirmationTx;
default:
return false;
}
}
constructor (api, account, isTestnet) {
super(api, EmailVerificationABI, EMAIL_VERIFICATION, account, isTestnet);
}
requestValues = () => [ sha3(this.email) ]
@action setEmail = (email) => {
this.email = email;
}
requestCode = () => {
const { email, account, isTestnet } = this;
return postToServer({ email, address: account }, isTestnet);
}
}

View File

@ -0,0 +1,41 @@
// 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 <http://www.gnu.org/licenses/>.
import React from 'react';
import styles from './verification.css';
export const howSMSVerificationWorks = (
<div>
<p>The following steps will let you prove that you control both an account and a phone number.</p>
<ol className={ styles.list }>
<li>You send a verification request to a specific contract.</li>
<li>Our server puts a puzzle into this contract.</li>
<li>The code you receive via SMS is the solution to this puzzle.</li>
</ol>
</div>
);
export const howEmailVerificationWorks = (
<div>
<p>The following steps will let you prove that you control both an account and an e-mail address.</p>
<ol className={ styles.list }>
<li>You send a verification request to a specific contract.</li>
<li>Our server puts a puzzle into this contract.</li>
<li>The code you receive via e-mail is the solution to this puzzle.</li>
</ol>
</div>
);

View File

@ -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 <http://www.gnu.org/licenses/>.
export default from './verification';

View File

@ -0,0 +1,69 @@
// 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 <http://www.gnu.org/licenses/>.
import { observable, computed, action } from 'mobx';
import phone from 'phoneformat.js';
import SMSVerificationABI from '~/contracts/abi/sms-verification.json';
import VerificationStore, {
LOADING, QUERY_DATA, QUERY_CODE, POSTED_CONFIRMATION, DONE
} from './store';
import { postToServer } from '../../3rdparty/sms-verification';
const SMS_VERIFICATION = 0; // id in the `BadgeReg.sol` contract
export default class SMSVerificationStore extends VerificationStore {
@observable number = '';
@computed get isNumberValid () {
return phone.isValidNumber(this.number);
}
@computed get isStepValid () {
if (this.step === DONE) {
return true;
}
if (this.error) {
return false;
}
switch (this.step) {
case LOADING:
return this.contract && this.fee && this.isVerified !== null && this.hasRequested !== null;
case QUERY_DATA:
return this.isNumberValid && this.consentGiven;
case QUERY_CODE:
return this.requestTx && this.isCodeValid === true;
case POSTED_CONFIRMATION:
return !!this.confirmationTx;
default:
return false;
}
}
constructor (api, account, isTestnet) {
super(api, SMSVerificationABI, SMS_VERIFICATION, account, isTestnet);
}
@action setNumber = (number) => {
this.number = number;
}
requestCode = () => {
const { number, account, isTestnet } = this;
return postToServer({ number, address: account }, isTestnet);
}
}

View File

@ -14,21 +14,19 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observable, computed, autorun, action } from 'mobx'; import { observable, autorun, action } from 'mobx';
import phone from 'phoneformat.js';
import { sha3 } from '~/api/util/sha3'; import { sha3 } from '~/api/util/sha3';
import Contract from '~/api/contract';
import Contracts from '~/contracts'; import Contracts from '~/contracts';
import { checkIfVerified, checkIfRequested, awaitPuzzle } from '~/contracts/sms-verification'; import { checkIfVerified, checkIfRequested, awaitPuzzle } from '~/contracts/verification';
import { postToServer } from '~/3rdparty/sms-verification';
import { checkIfTxFailed, waitForConfirmations } from '~/util/tx'; import { checkIfTxFailed, waitForConfirmations } from '~/util/tx';
export const LOADING = 'fetching-contract'; export const LOADING = 'fetching-contract';
export const QUERY_DATA = 'query-data'; export const QUERY_DATA = 'query-data';
export const POSTING_REQUEST = 'posting-request'; export const POSTING_REQUEST = 'posting-request';
export const POSTED_REQUEST = 'posted-request'; export const POSTED_REQUEST = 'posted-request';
export const REQUESTING_SMS = 'requesting-sms'; export const REQUESTING_CODE = 'requesting-code';
export const QUERY_CODE = 'query-code'; export const QUERY_CODE = 'query-code';
export const POSTING_CONFIRMATION = 'posting-confirmation'; export const POSTING_CONFIRMATION = 'posting-confirmation';
export const POSTED_CONFIRMATION = 'posted-confirmation'; export const POSTED_CONFIRMATION = 'posted-confirmation';
@ -43,56 +41,30 @@ export default class VerificationStore {
@observable isVerified = null; @observable isVerified = null;
@observable hasRequested = null; @observable hasRequested = null;
@observable consentGiven = false; @observable consentGiven = false;
@observable number = '';
@observable requestTx = null; @observable requestTx = null;
@observable code = ''; @observable code = '';
@observable isCodeValid = null; @observable isCodeValid = null;
@observable confirmationTx = null; @observable confirmationTx = null;
@computed get isNumberValid () { constructor (api, abi, certifierId, account, isTestnet) {
return phone.isValidNumber(this.number);
}
@computed get isStepValid () {
if (this.step === DONE) {
return true;
}
if (this.error) {
return false;
}
switch (this.step) {
case LOADING:
return this.contract && this.fee && this.isVerified !== null && this.hasRequested !== null;
case QUERY_DATA:
return this.isNumberValid && this.consentGiven;
case QUERY_CODE:
return this.requestTx && this.isCodeValid === true;
case POSTED_CONFIRMATION:
return !!this.confirmationTx;
default:
return false;
}
}
constructor (api, account, isTestnet) {
this.api = api; this.api = api;
this.account = account; this.account = account;
this.isTestnet = isTestnet; this.isTestnet = isTestnet;
this.step = LOADING; this.step = LOADING;
Contracts.create(api).registry.getContract('smsverification') Contracts.get().badgeReg.fetchCertifier(certifierId)
.then((contract) => { .then(({ address }) => {
this.contract = contract; this.contract = new Contract(api, abi).at(address);
this.load(); this.load();
}) })
.catch((err) => { .catch((err) => {
console.error('error', err);
this.error = 'Failed to fetch the contract: ' + err.message; this.error = 'Failed to fetch the contract: ' + err.message;
}); });
autorun(() => { autorun(() => {
if (this.error) { if (this.error) {
console.error('sms verification: ' + this.error); console.error('verification: ' + this.error);
} }
}); });
} }
@ -135,10 +107,6 @@ export default class VerificationStore {
}); });
} }
@action setNumber = (number) => {
this.number = number;
}
@action setConsentGiven = (consentGiven) => { @action setConsentGiven = (consentGiven) => {
this.consentGiven = consentGiven; this.consentGiven = consentGiven;
} }
@ -166,19 +134,22 @@ export default class VerificationStore {
}); });
} }
requestValues = () => []
@action sendRequest = () => { @action sendRequest = () => {
const { api, account, contract, fee, number, hasRequested } = this; const { api, account, contract, fee, hasRequested } = this;
const request = contract.functions.find((fn) => fn.name === 'request'); const request = contract.functions.find((fn) => fn.name === 'request');
const options = { from: account, value: fee.toString() }; const options = { from: account, value: fee.toString() };
const values = this.requestValues();
let chain = Promise.resolve(); let chain = Promise.resolve();
if (!hasRequested) { if (!hasRequested) {
this.step = POSTING_REQUEST; this.step = POSTING_REQUEST;
chain = request.estimateGas(options, []) chain = request.estimateGas(options, values)
.then((gas) => { .then((gas) => {
options.gas = gas.mul(1.2).toFixed(0); options.gas = gas.mul(1.2).toFixed(0);
return request.postTransaction(options, []); return request.postTransaction(options, values);
}) })
.then((handle) => { .then((handle) => {
// TODO: The "request rejected" error doesn't have any property to // TODO: The "request rejected" error doesn't have any property to
@ -200,18 +171,15 @@ export default class VerificationStore {
chain chain
.then(() => { .then(() => {
return api.parity.netChain(); this.step = REQUESTING_CODE;
}) return this.requestCode();
.then((chain) => {
this.step = REQUESTING_SMS;
return postToServer({ number, address: account }, this.isTestnet);
}) })
.then(() => awaitPuzzle(api, contract, account)) .then(() => awaitPuzzle(api, contract, account))
.then(() => { .then(() => {
this.step = QUERY_CODE; this.step = QUERY_CODE;
}) })
.catch((err) => { .catch((err) => {
this.error = 'Failed to request a confirmation SMS: ' + err.message; this.error = 'Failed to request a confirmation code: ' + err.message;
}); });
} }

View File

@ -0,0 +1,25 @@
/* 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 <http://www.gnu.org/licenses/>.
*/
.noSpacing {
margin-top: 0;
margin-bottom: 0;
}
.list li {
padding: .1em 0;
}

View File

@ -20,12 +20,27 @@ import DoneIcon from 'material-ui/svg-icons/action/done-all';
import CancelIcon from 'material-ui/svg-icons/content/clear'; import CancelIcon from 'material-ui/svg-icons/content/clear';
import { Button, IdentityIcon, Modal } from '~/ui'; import { Button, IdentityIcon, Modal } from '~/ui';
import RadioButtons from '~/ui/Form/RadioButtons';
import { nullableProptype } from '~/util/proptypes';
import styles from './verification.css';
const methods = {
sms: {
label: 'SMS Verification', key: 0, value: 'sms',
description: (<p className={ styles.noSpacing }>It will be stored on the blockchain that you control a phone number (not <em>which</em>).</p>)
},
email: {
label: 'E-mail Verification', key: 1, value: 'email',
description: (<p className={ styles.noSpacing }>The hash of the e-mail address you prove control over will be stored on the blockchain.</p>)
}
};
import { import {
LOADING, LOADING,
QUERY_DATA, QUERY_DATA,
POSTING_REQUEST, POSTED_REQUEST, POSTING_REQUEST, POSTED_REQUEST,
REQUESTING_SMS, QUERY_CODE, REQUESTING_CODE, QUERY_CODE,
POSTING_CONFIRMATION, POSTED_CONFIRMATION, POSTING_CONFIRMATION, POSTED_CONFIRMATION,
DONE DONE
} from './store'; } from './store';
@ -37,34 +52,44 @@ import SendConfirmation from './SendConfirmation';
import Done from './Done'; import Done from './Done';
@observer @observer
export default class SMSVerification extends Component { export default class Verification extends Component {
static propTypes = { static propTypes = {
store: PropTypes.any.isRequired, store: nullableProptype(PropTypes.object.isRequired),
account: PropTypes.string.isRequired, account: PropTypes.string.isRequired,
onSelectMethod: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired onClose: PropTypes.func.isRequired
} }
static phases = { // mapping (store steps -> steps) static phases = { // mapping (store steps -> steps)
[LOADING]: 0, [LOADING]: 1, [QUERY_DATA]: 1,
[QUERY_DATA]: 1, [POSTING_REQUEST]: 2, [POSTED_REQUEST]: 2, [REQUESTING_CODE]: 2,
[POSTING_REQUEST]: 2, [POSTED_REQUEST]: 2, [REQUESTING_SMS]: 2,
[QUERY_CODE]: 3, [QUERY_CODE]: 3,
[POSTING_CONFIRMATION]: 4, [POSTED_CONFIRMATION]: 4, [POSTING_CONFIRMATION]: 4, [POSTED_CONFIRMATION]: 4,
[DONE]: 5 [DONE]: 5
} }
state = {
method: 'sms'
};
render () { render () {
const phase = SMSVerification.phases[this.props.store.step]; const { store } = this.props;
const { error, isStepValid } = this.props.store; let phase = 0; let error = false; let isStepValid = true;
if (store) {
phase = Verification.phases[store.step];
error = store.error;
isStepValid = store.isStepValid;
}
return ( return (
<Modal <Modal
actions={ this.renderDialogActions(phase, error, isStepValid) } actions={ this.renderDialogActions(phase, error, isStepValid) }
title='verify your account via SMS' title='verify your account'
visible visible
current={ phase } current={ phase }
steps={ ['Prepare', 'Enter Data', 'Request', 'Enter Code', 'Confirm', 'Done!'] } steps={ ['Method', 'Enter Data', 'Request', 'Enter Code', 'Confirm', 'Done!'] }
waiting={ error ? [] : [ 0, 2, 4 ] } waiting={ error ? [] : [ 2, 4 ] }
> >
{ this.renderStep(phase, error) } { this.renderStep(phase, error) }
</Modal> </Modal>
@ -101,6 +126,13 @@ export default class SMSVerification extends Component {
let action = () => {}; let action = () => {};
switch (phase) { switch (phase) {
case 0:
action = () => {
const { onSelectMethod } = this.props;
const { method } = this.state;
onSelectMethod(method);
};
break;
case 1: case 1:
action = store.sendRequest; action = store.sendRequest;
break; break;
@ -133,26 +165,58 @@ export default class SMSVerification extends Component {
return (<p>{ error }</p>); return (<p>{ error }</p>);
} }
const { method } = this.state;
if (phase === 0) {
const values = Object.values(methods);
const value = values.findIndex((v) => v.value === method);
return (
<RadioButtons
value={ value < 0 ? 0 : value }
values={ values }
onChange={ this.selectMethod }
/>
);
}
const { const {
step, step,
fee, number, isNumberValid, isVerified, hasRequested, fee, isVerified, hasRequested,
requestTx, isCodeValid, confirmationTx, requestTx, isCodeValid, confirmationTx,
setCode setCode
} = this.props.store; } = this.props.store;
switch (phase) { switch (phase) {
case 0:
return (
<p>Loading SMS Verification.</p>
);
case 1: case 1:
const { setNumber, setConsentGiven } = this.props.store; if (step === LOADING) {
return (<p>Loading verification data.</p>);
}
const { setConsentGiven } = this.props.store;
const fields = [];
if (method === 'sms') {
fields.push({
key: 'number',
label: 'phone number in international format',
hint: 'the SMS will be sent to this number',
error: this.props.store.isNumberValid ? null : 'invalid number',
onChange: this.props.store.setNumber
});
} else if (method === 'email') {
fields.push({
key: 'email',
label: 'email address',
hint: 'the code will be sent to this address',
error: this.props.store.isEmailValid ? null : 'invalid email',
onChange: this.props.store.setEmail
});
}
return ( return (
<GatherData <GatherData
fee={ fee } isNumberValid={ isNumberValid } method={ method } fields={ fields }
isVerified={ isVerified } hasRequested={ hasRequested } fee={ fee } isVerified={ isVerified } hasRequested={ hasRequested }
setNumber={ setNumber } setConsentGiven={ setConsentGiven } setConsentGiven={ setConsentGiven }
/> />
); );
@ -162,9 +226,19 @@ export default class SMSVerification extends Component {
); );
case 3: case 3:
let receiver, hint;
if (method === 'sms') {
receiver = this.props.store.number;
hint = 'Enter the code you received via SMS.';
} else if (method === 'email') {
receiver = this.props.store.email;
hint = 'Enter the code you received via e-mail.';
}
return ( return (
<QueryCode <QueryCode
number={ number } fee={ fee } isCodeValid={ isCodeValid } receiver={ receiver }
hint={ hint }
isCodeValid={ isCodeValid }
setCode={ setCode } setCode={ setCode }
/> />
); );
@ -183,4 +257,8 @@ export default class SMSVerification extends Component {
return null; return null;
} }
} }
selectMethod = (choice, i) => {
this.setState({ method: choice.value });
}
} }

View File

@ -24,7 +24,7 @@ import EditMeta from './EditMeta';
import ExecuteContract from './ExecuteContract'; import ExecuteContract from './ExecuteContract';
import FirstRun from './FirstRun'; import FirstRun from './FirstRun';
import Shapeshift from './Shapeshift'; import Shapeshift from './Shapeshift';
import SMSVerification from './SMSVerification'; import Verification from './Verification';
import Transfer from './Transfer'; import Transfer from './Transfer';
import PasswordManager from './PasswordManager'; import PasswordManager from './PasswordManager';
import SaveContract from './SaveContract'; import SaveContract from './SaveContract';
@ -42,7 +42,7 @@ export {
ExecuteContract, ExecuteContract,
FirstRun, FirstRun,
Shapeshift, Shapeshift,
SMSVerification, Verification,
Transfer, Transfer,
PasswordManager, PasswordManager,
LoadContract, LoadContract,

View File

@ -14,10 +14,18 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
export const fetchCertifiers = () => ({
type: 'fetchCertifiers'
});
export const fetchCertifications = (address) => ({ export const fetchCertifications = (address) => ({
type: 'fetchCertifications', address type: 'fetchCertifications', address
}); });
export const addCertification = (address, name, title, icon) => ({ export const addCertification = (address, id, name, title, icon) => ({
type: 'addCertification', address, name, title, icon type: 'addCertification', address, id, name, title, icon
});
export const removeCertification = (address, id) => ({
type: 'removeCertification', address, id
}); });

View File

@ -14,38 +14,90 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import Contracts from '~/contracts'; import { uniq } from 'lodash';
import { addCertification } from './actions';
const knownCertifiers = [ 'smsverification' ]; import ABI from '~/contracts/abi/certifier.json';
import Contract from '~/api/contract';
import Contracts from '~/contracts';
import { addCertification, removeCertification } from './actions';
export default class CertificationsMiddleware { export default class CertificationsMiddleware {
toMiddleware () { toMiddleware () {
return (store) => (next) => (action) => { const api = Contracts.get()._api;
if (action.type !== 'fetchCertifications') {
return next(action);
}
const { address } = action;
const badgeReg = Contracts.get().badgeReg; const badgeReg = Contracts.get().badgeReg;
const contract = new Contract(api, ABI);
const Confirmed = contract.events.find((e) => e.name === 'Confirmed');
const Revoked = contract.events.find((e) => e.name === 'Revoked');
knownCertifiers.forEach((name) => { let certifiers = [];
badgeReg.fetchCertifier(name) let accounts = []; // these are addresses
.then((cert) => {
return badgeReg.checkIfCertified(cert.address, address) const fetchConfirmedEvents = (dispatch) => {
.then((isCertified) => { if (certifiers.length === 0 || accounts.length === 0) return;
if (isCertified) { api.eth.getLogs({
const { name, title, icon } = cert; fromBlock: 0,
store.dispatch(addCertification(address, name, title, icon)); toBlock: 'latest',
address: certifiers.map((c) => c.address),
topics: [ [ Confirmed.signature, Revoked.signature ], accounts ]
})
.then((logs) => contract.parseEventLogs(logs))
.then((logs) => {
logs.forEach((log) => {
const certifier = certifiers.find((c) => c.address === log.address);
if (!certifier) {
throw new Error(`Could not find certifier at ${log.address}.`);
}
const { id, name, title, icon } = certifier;
if (log.event === 'Revoked') {
dispatch(removeCertification(log.params.who.value, id));
} else {
dispatch(addCertification(log.params.who.value, id, name, title, icon));
} }
}); });
}) })
.catch((err) => { .catch((err) => {
if (err) { console.error('Failed to fetch Confirmed events:', err);
console.error(`Failed to check if ${address} certified by ${name}:`, err); });
};
return (store) => (next) => (action) => {
switch (action.type) {
case 'fetchCertifiers':
badgeReg.certifierCount().then((count) => {
new Array(+count).fill(null).forEach((_, id) => {
badgeReg.fetchCertifier(id)
.then((cert) => {
if (!certifiers.some((c) => c.id === cert.id)) {
certifiers = certifiers.concat(cert);
fetchConfirmedEvents(store.dispatch);
} }
})
.catch((err) => {
console.warn(`Could not fetch certifier ${id}:`, err);
}); });
}); });
});
break;
case 'fetchCertifications':
const { address } = action;
if (!accounts.includes(address)) {
accounts = accounts.concat(address);
fetchConfirmedEvents(store.dispatch);
}
break;
case 'setVisibleAccounts':
const { addresses } = action;
accounts = uniq(accounts.concat(addresses));
fetchConfirmedEvents(store.dispatch);
break;
default:
next(action);
}
}; };
} }
} }

View File

@ -17,17 +17,27 @@
const initialState = {}; const initialState = {};
export default (state = initialState, action) => { export default (state = initialState, action) => {
if (action.type !== 'addCertification') { if (action.type === 'addCertification') {
return state; const { address, id, name, icon, title } = action;
}
const { address, name, icon, title } = action;
const certifications = state[address] || []; const certifications = state[address] || [];
if (certifications.some((c) => c.name === name)) { if (certifications.some((c) => c.id === id)) {
return state; return state;
} }
const newCertifications = certifications.concat({ name, icon, title });
const newCertifications = certifications.concat({
id, name, icon, title
});
return { ...state, [address]: newCertifications }; return { ...state, [address]: newCertifications };
}
if (action.type === 'removeCertification') {
const { address, id } = action;
const certifications = state[address] || [];
const newCertifications = certifications.filter((c) => c.id !== id);
return { ...state, [address]: newCertifications };
}
return state;
}; };

View File

@ -16,10 +16,8 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { hashToImageUrl } from '~/redux/providers/imagesReducer'; import { hashToImageUrl } from '~/redux/providers/imagesReducer';
import { fetchCertifications } from '~/redux/providers/certifications/actions';
import defaultIcon from '../../../assets/images/certifications/unknown.svg'; import defaultIcon from '../../../assets/images/certifications/unknown.svg';
@ -29,14 +27,7 @@ class Certifications extends Component {
static propTypes = { static propTypes = {
account: PropTypes.string.isRequired, account: PropTypes.string.isRequired,
certifications: PropTypes.array.isRequired, certifications: PropTypes.array.isRequired,
dappsUrl: PropTypes.string.isRequired, dappsUrl: PropTypes.string.isRequired
fetchCertifications: PropTypes.func.isRequired
}
componentWillMount () {
const { account, fetchCertifications } = this.props;
fetchCertifications(account);
} }
render () { render () {
@ -73,15 +64,13 @@ function mapStateToProps (_, initProps) {
return (state) => { return (state) => {
const certifications = state.certifications[account] || []; const certifications = state.certifications[account] || [];
return { certifications }; const dappsUrl = state.api.dappsUrl;
};
}
function mapDispatchToProps (dispatch) { return { certifications, dappsUrl };
return bindActionCreators({ fetchCertifications }, dispatch); };
} }
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps null
)(Certifications); )(Certifications);

View File

@ -46,26 +46,26 @@ export default class Select extends Component {
} }
render () { render () {
const { disabled, error, label, hint, value, children, className, onBlur, onChange, onKeyDown } = this.props; const { children, className, disabled, error, hint, label, onBlur, onChange, onKeyDown, value } = this.props;
return ( return (
<SelectField <SelectField
className={ className }
autoComplete='off' autoComplete='off'
className={ className }
disabled={ disabled } disabled={ disabled }
errorText={ error } errorText={ error }
floatingLabelFixed floatingLabelFixed
floatingLabelText={ label } floatingLabelText={ label }
fullWidth fullWidth
hintText={ hint } hintText={ hint }
name={ NAME_ID }
id={ NAME_ID } id={ NAME_ID }
underlineDisabledStyle={ UNDERLINE_DISABLED } name={ NAME_ID }
underlineStyle={ UNDERLINE_NORMAL }
value={ value }
onBlur={ onBlur } onBlur={ onBlur }
onChange={ onChange } onChange={ onChange }
onKeyDown={ onKeyDown }> onKeyDown={ onKeyDown }
underlineDisabledStyle={ UNDERLINE_DISABLED }
underlineStyle={ UNDERLINE_NORMAL }
value={ value }>
{ children } { children }
</SelectField> </SelectField>
); );

View File

@ -289,9 +289,8 @@ export default class TypedInput extends Component {
return ( return (
<MenuItem <MenuItem
key={ bool } key={ bool }
value={ bool }
label={ bool } label={ bool }
> value={ bool }>
{ bool } { bool }
</MenuItem> </MenuItem>
); );
@ -299,19 +298,23 @@ export default class TypedInput extends Component {
return ( return (
<Select <Select
label={ label }
hint={ hint }
value={ value ? 'true' : 'false' }
error={ error } error={ error }
hint={ hint }
label={ label }
onChange={ this.onChangeBool } onChange={ this.onChangeBool }
> value={
value
? 'true'
: 'false'
}>
{ boolitems } { boolitems }
</Select> </Select>
); );
} }
onChangeBool = (event, _index, value) => { onChangeBool = (event, _index, value) => {
this.props.onChange(value === 'true'); // NOTE: event.target.value added for enzyme simulated event testing
this.props.onChange((value || event.target.value) === 'true');
} }
onEthTypeChange = () => { onEthTypeChange = () => {

Some files were not shown because too many files have changed in this diff Show More