Merge branch 'master' into lightsync
This commit is contained in:
commit
a1f32de2d9
@ -231,10 +231,10 @@ linux-armv6:
|
||||
stage: build
|
||||
image: ethcore/rust-armv6:latest
|
||||
only:
|
||||
# - beta
|
||||
- beta
|
||||
# - tags
|
||||
# - stable
|
||||
- triggers
|
||||
# - triggers
|
||||
script:
|
||||
- export CC=arm-linux-gnueabi-gcc
|
||||
- export CXX=arm-linux-gnueabi-g++
|
||||
@ -312,8 +312,8 @@ darwin:
|
||||
- stable
|
||||
- triggers
|
||||
script:
|
||||
- cargo build -j 8 --release -p ethstore #$CARGOFLAGS
|
||||
- cargo build -j 8 --release #$CARGOFLAGS
|
||||
- cargo build -j 8 --release -p ethstore #$CARGOFLAGS
|
||||
- rm -rf parity.md5
|
||||
- md5sum target/release/parity > parity.md5
|
||||
- packagesbuild -v mac/Parity.pkgproj
|
||||
@ -350,7 +350,7 @@ windows:
|
||||
- set RUST_BACKTRACE=1
|
||||
- set RUSTFLAGS=%RUSTFLAGS%
|
||||
- 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/vc_redist.x64.exe" -o nsis\vc_redist.x64.exe
|
||||
- signtool sign /f %keyfile% /p %certpass% target\release\parity.exe
|
||||
@ -408,7 +408,7 @@ test-darwin:
|
||||
test-windows:
|
||||
stage: test
|
||||
only:
|
||||
- triggers
|
||||
# - triggers
|
||||
before_script:
|
||||
- git submodule update --init --recursive
|
||||
script:
|
||||
|
197
Cargo.lock
generated
197
Cargo.lock
generated
@ -3,6 +3,7 @@ name = "parity"
|
||||
version = "1.5.0"
|
||||
dependencies = [
|
||||
"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)",
|
||||
"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)",
|
||||
@ -32,9 +33,11 @@ dependencies = [
|
||||
"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)",
|
||||
"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)",
|
||||
"rlp 0.1.0",
|
||||
"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_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)",
|
||||
@ -42,7 +45,7 @@ dependencies = [
|
||||
"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)",
|
||||
"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]]
|
||||
@ -58,6 +61,17 @@ name = "ansi_term"
|
||||
version = "0.7.2"
|
||||
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]]
|
||||
name = "arrayvec"
|
||||
version = "0.3.16"
|
||||
@ -196,7 +210,7 @@ source = "git+https://github.com/ethcore/rust-ctrlc.git#f4927770f89eca80ec250911
|
||||
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)",
|
||||
"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]]
|
||||
@ -277,7 +291,7 @@ name = "ethash"
|
||||
version = "1.5.0"
|
||||
dependencies = [
|
||||
"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)",
|
||||
"sha3 0.1.0",
|
||||
]
|
||||
@ -395,7 +409,7 @@ dependencies = [
|
||||
"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)",
|
||||
"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)",
|
||||
]
|
||||
|
||||
@ -501,7 +515,7 @@ dependencies = [
|
||||
"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)",
|
||||
"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)",
|
||||
"rlp 0.1.0",
|
||||
"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-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-macros 0.1.0 (git+https://github.com/ethcore/jsonrpc.git)",
|
||||
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rlp 0.1.0",
|
||||
"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)",
|
||||
"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)",
|
||||
"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)",
|
||||
"regex 0.1.68 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rlp 0.1.0",
|
||||
@ -659,7 +674,7 @@ dependencies = [
|
||||
"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)",
|
||||
"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)",
|
||||
"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)",
|
||||
@ -688,7 +703,7 @@ dependencies = [
|
||||
"ethkey 0.2.0",
|
||||
"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)",
|
||||
"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)",
|
||||
"rlp 0.1.0",
|
||||
"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)",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "gcc"
|
||||
version = "0.3.35"
|
||||
@ -845,7 +868,7 @@ 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)",
|
||||
"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]]
|
||||
@ -860,8 +883,8 @@ source = "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#1500da1b9613a0a17fc0109d825f3ccc60199a53"
|
||||
version = "3.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"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)",
|
||||
@ -870,10 +893,22 @@ dependencies = [
|
||||
"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]]
|
||||
name = "jsonrpc-http-server"
|
||||
version = "6.1.1"
|
||||
source = "git+https://github.com/ethcore/jsonrpc.git#1500da1b9613a0a17fc0109d825f3ccc60199a53"
|
||||
source = "git+https://github.com/ethcore/jsonrpc.git#33262d626a294a00c20435dec331058ba65e224a"
|
||||
dependencies = [
|
||||
"hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)",
|
||||
"jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
|
||||
@ -884,7 +919,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "jsonrpc-ipc-server"
|
||||
version = "0.2.4"
|
||||
source = "git+https://github.com/ethcore/jsonrpc.git#1500da1b9613a0a17fc0109d825f3ccc60199a53"
|
||||
source = "git+https://github.com/ethcore/jsonrpc.git#33262d626a294a00c20435dec331058ba65e224a"
|
||||
dependencies = [
|
||||
"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)",
|
||||
@ -896,10 +931,19 @@ dependencies = [
|
||||
"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]]
|
||||
name = "jsonrpc-tcp-server"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/ethcore/jsonrpc.git#1500da1b9613a0a17fc0109d825f3ccc60199a53"
|
||||
source = "git+https://github.com/ethcore/jsonrpc.git#33262d626a294a00c20435dec331058ba65e224a"
|
||||
dependencies = [
|
||||
"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)",
|
||||
@ -916,7 +960,7 @@ name = "kernel32-sys"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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)",
|
||||
]
|
||||
|
||||
@ -1017,7 +1061,7 @@ dependencies = [
|
||||
"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)",
|
||||
"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]]
|
||||
@ -1033,7 +1077,7 @@ dependencies = [
|
||||
"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)",
|
||||
"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]]
|
||||
@ -1048,7 +1092,7 @@ dependencies = [
|
||||
"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)",
|
||||
"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]]
|
||||
@ -1064,7 +1108,7 @@ dependencies = [
|
||||
"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)",
|
||||
"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]]
|
||||
@ -1074,7 +1118,7 @@ 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)",
|
||||
"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)",
|
||||
]
|
||||
|
||||
@ -1085,7 +1129,7 @@ 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)",
|
||||
"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]]
|
||||
@ -1114,7 +1158,7 @@ dependencies = [
|
||||
"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)",
|
||||
"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)",
|
||||
]
|
||||
|
||||
@ -1253,6 +1297,15 @@ name = "odds"
|
||||
version = "0.2.12"
|
||||
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]]
|
||||
name = "owning_ref"
|
||||
version = "0.2.2"
|
||||
@ -1272,6 +1325,26 @@ dependencies = [
|
||||
"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]]
|
||||
name = "parity-ui"
|
||||
version = "1.5.0"
|
||||
@ -1291,7 +1364,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "parity-ui-precompiled"
|
||||
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 = [
|
||||
"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)",
|
||||
"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)",
|
||||
"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 = "parking_lot"
|
||||
version = "0.3.5"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"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)",
|
||||
"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)",
|
||||
"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]]
|
||||
@ -1548,7 +1621,30 @@ 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.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]]
|
||||
@ -1652,6 +1748,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "siphasher"
|
||||
version = "0.1.1"
|
||||
@ -1786,13 +1891,21 @@ name = "target_info"
|
||||
version = "0.1.0"
|
||||
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]]
|
||||
name = "term"
|
||||
version = "0.2.14"
|
||||
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)",
|
||||
"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]]
|
||||
@ -1801,7 +1914,7 @@ version = "0.4.4"
|
||||
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)",
|
||||
"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]]
|
||||
@ -1836,7 +1949,7 @@ 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)",
|
||||
"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]]
|
||||
@ -1932,7 +2045,7 @@ name = "vecio"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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)",
|
||||
]
|
||||
|
||||
@ -1963,7 +2076,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.2.6"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
@ -1991,10 +2104,15 @@ name = "ws2_32-sys"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xdg"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "xml-rs"
|
||||
version = "0.3.4"
|
||||
@ -2025,6 +2143,7 @@ dependencies = [
|
||||
[metadata]
|
||||
"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 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 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"
|
||||
@ -2055,6 +2174,7 @@ dependencies = [
|
||||
"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 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 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"
|
||||
@ -2068,9 +2188,11 @@ dependencies = [
|
||||
"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 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-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-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 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"
|
||||
@ -2110,11 +2232,12 @@ dependencies = [
|
||||
"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 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 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 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 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"
|
||||
@ -2141,6 +2264,7 @@ dependencies = [
|
||||
"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 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 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"
|
||||
@ -2152,6 +2276,7 @@ dependencies = [
|
||||
"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 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 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>"
|
||||
@ -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.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 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.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"
|
||||
@ -2193,10 +2319,11 @@ dependencies = [
|
||||
"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 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 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 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 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"
|
||||
|
@ -28,6 +28,7 @@ isatty = "0.1"
|
||||
toml = "0.2"
|
||||
serde = "0.8.0"
|
||||
serde_json = "0.8.0"
|
||||
app_dirs = "1.1.1"
|
||||
hyper = { version = "0.9", default-features = false }
|
||||
ctrlc = { git = "https://github.com/ethcore/rust-ctrlc.git" }
|
||||
fdlimit = "0.1"
|
||||
@ -47,6 +48,8 @@ rlp = { path = "util/rlp" }
|
||||
ethcore-stratum = { path = "stratum" }
|
||||
ethcore-dapps = { path = "dapps", 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" }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
|
@ -120,7 +120,7 @@ impl<R: URLHint> ContentFetcher<R> {
|
||||
// Content is already being fetched
|
||||
Some(&mut ContentStatus::Fetching(ref fetch_control)) => {
|
||||
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
|
||||
None => {
|
||||
@ -129,11 +129,12 @@ impl<R: URLHint> ContentFetcher<R> {
|
||||
let content = self.resolver.resolve(content_hex);
|
||||
|
||||
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();
|
||||
match result {
|
||||
Some(endpoint) => {
|
||||
cache.insert(id, ContentStatus::Ready(endpoint));
|
||||
cache.insert(id.clone(), ContentStatus::Ready(endpoint));
|
||||
},
|
||||
// In case of error
|
||||
None => {
|
||||
@ -150,6 +151,7 @@ impl<R: URLHint> ContentFetcher<R> {
|
||||
Some(URLHintResult::Dapp(dapp)) => {
|
||||
let (handler, fetch_control) = ContentFetcherHandler::new(
|
||||
dapp.url(),
|
||||
path,
|
||||
control,
|
||||
DappInstaller {
|
||||
id: content_id.clone(),
|
||||
@ -165,6 +167,7 @@ impl<R: URLHint> ContentFetcher<R> {
|
||||
Some(URLHintResult::Content(content)) => {
|
||||
let (handler, fetch_control) = ContentFetcherHandler::new(
|
||||
content.url,
|
||||
path,
|
||||
control,
|
||||
ContentInstaller {
|
||||
id: content_id.clone(),
|
||||
@ -248,43 +251,45 @@ struct ContentInstaller {
|
||||
id: String,
|
||||
mime: String,
|
||||
content_path: PathBuf,
|
||||
on_done: Box<Fn(String, Option<LocalPageEndpoint>) + Send>,
|
||||
on_done: Box<Fn(Option<LocalPageEndpoint>) + Send>,
|
||||
}
|
||||
|
||||
impl ContentValidator for ContentInstaller {
|
||||
type Error = ValidationError;
|
||||
|
||||
fn validate_and_install(&self, path: PathBuf) -> Result<(String, LocalPageEndpoint), ValidationError> {
|
||||
// Create dir
|
||||
try!(fs::create_dir_all(&self.content_path));
|
||||
fn validate_and_install(&self, path: PathBuf) -> Result<LocalPageEndpoint, ValidationError> {
|
||||
let validate = || {
|
||||
// Create dir
|
||||
try!(fs::create_dir_all(&self.content_path));
|
||||
|
||||
// Validate hash
|
||||
let mut file_reader = io::BufReader::new(try!(fs::File::open(&path)));
|
||||
let hash = try!(sha3(&mut file_reader));
|
||||
let id = try!(self.id.as_str().parse().map_err(|_| ValidationError::InvalidContentId));
|
||||
if id != hash {
|
||||
return Err(ValidationError::HashMismatch {
|
||||
expected: id,
|
||||
got: hash,
|
||||
});
|
||||
}
|
||||
// Validate hash
|
||||
let mut file_reader = io::BufReader::new(try!(fs::File::open(&path)));
|
||||
let hash = try!(sha3(&mut file_reader));
|
||||
let id = try!(self.id.as_str().parse().map_err(|_| ValidationError::InvalidContentId));
|
||||
if id != hash {
|
||||
return Err(ValidationError::HashMismatch {
|
||||
expected: id,
|
||||
got: hash,
|
||||
});
|
||||
}
|
||||
|
||||
// And prepare path for a file
|
||||
let filename = path.file_name().expect("We always fetch a file.");
|
||||
let mut content_path = self.content_path.clone();
|
||||
content_path.push(&filename);
|
||||
// And prepare path for a file
|
||||
let filename = path.file_name().expect("We always fetch a file.");
|
||||
let mut content_path = self.content_path.clone();
|
||||
content_path.push(&filename);
|
||||
|
||||
if content_path.exists() {
|
||||
try!(fs::remove_dir_all(&content_path))
|
||||
}
|
||||
if content_path.exists() {
|
||||
try!(fs::remove_dir_all(&content_path))
|
||||
}
|
||||
|
||||
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)))
|
||||
}
|
||||
|
||||
fn done(&self, endpoint: Option<LocalPageEndpoint>) {
|
||||
(self.on_done)(self.id.clone(), endpoint)
|
||||
// Make sure to always call on_done (even in case of errors)!
|
||||
let result = validate();
|
||||
(self.on_done)(result.as_ref().ok().cloned());
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
@ -292,7 +297,7 @@ impl ContentValidator for ContentInstaller {
|
||||
struct DappInstaller {
|
||||
id: String,
|
||||
dapps_path: PathBuf,
|
||||
on_done: Box<Fn(String, Option<LocalPageEndpoint>) + Send>,
|
||||
on_done: Box<Fn(Option<LocalPageEndpoint>) + Send>,
|
||||
embeddable_on: Option<(String, u16)>,
|
||||
}
|
||||
|
||||
@ -331,69 +336,68 @@ impl DappInstaller {
|
||||
impl ContentValidator for DappInstaller {
|
||||
type Error = ValidationError;
|
||||
|
||||
fn validate_and_install(&self, app_path: PathBuf) -> Result<(String, LocalPageEndpoint), ValidationError> {
|
||||
trace!(target: "dapps", "Opening dapp bundle at {:?}", app_path);
|
||||
let mut file_reader = io::BufReader::new(try!(fs::File::open(app_path)));
|
||||
let hash = try!(sha3(&mut file_reader));
|
||||
let id = try!(self.id.as_str().parse().map_err(|_| ValidationError::InvalidContentId));
|
||||
if id != hash {
|
||||
return Err(ValidationError::HashMismatch {
|
||||
expected: id,
|
||||
got: hash,
|
||||
});
|
||||
}
|
||||
let file = file_reader.into_inner();
|
||||
// Unpack archive
|
||||
let mut zip = try!(zip::ZipArchive::new(file));
|
||||
// First find manifest file
|
||||
let (mut manifest, manifest_dir) = try!(Self::find_manifest(&mut zip));
|
||||
// Overwrite id to match hash
|
||||
manifest.id = self.id.clone();
|
||||
fn validate_and_install(&self, path: PathBuf) -> Result<LocalPageEndpoint, ValidationError> {
|
||||
trace!(target: "dapps", "Opening dapp bundle at {:?}", path);
|
||||
let validate = || {
|
||||
let mut file_reader = io::BufReader::new(try!(fs::File::open(path)));
|
||||
let hash = try!(sha3(&mut file_reader));
|
||||
let id = try!(self.id.as_str().parse().map_err(|_| ValidationError::InvalidContentId));
|
||||
if id != hash {
|
||||
return Err(ValidationError::HashMismatch {
|
||||
expected: id,
|
||||
got: hash,
|
||||
});
|
||||
}
|
||||
let file = file_reader.into_inner();
|
||||
// Unpack archive
|
||||
let mut zip = try!(zip::ZipArchive::new(file));
|
||||
// First find manifest file
|
||||
let (mut manifest, manifest_dir) = try!(Self::find_manifest(&mut zip));
|
||||
// Overwrite id to match hash
|
||||
manifest.id = self.id.clone();
|
||||
|
||||
let target = self.dapp_target_path(&manifest);
|
||||
let target = self.dapp_target_path(&manifest);
|
||||
|
||||
// Remove old directory
|
||||
if target.exists() {
|
||||
warn!(target: "dapps", "Overwriting existing dapp: {}", manifest.id);
|
||||
try!(fs::remove_dir_all(target.clone()));
|
||||
}
|
||||
// Remove old directory
|
||||
if target.exists() {
|
||||
warn!(target: "dapps", "Overwriting existing dapp: {}", manifest.id);
|
||||
try!(fs::remove_dir_all(target.clone()));
|
||||
}
|
||||
|
||||
// Unpack zip
|
||||
for i in 0..zip.len() {
|
||||
let mut file = try!(zip.by_index(i));
|
||||
// TODO [todr] Check if it's consistent on windows.
|
||||
let is_dir = file.name().chars().rev().next() == Some('/');
|
||||
// Unpack zip
|
||||
for i in 0..zip.len() {
|
||||
let mut file = try!(zip.by_index(i));
|
||||
// TODO [todr] Check if it's consistent on windows.
|
||||
let is_dir = file.name().chars().rev().next() == Some('/');
|
||||
|
||||
let file_path = PathBuf::from(file.name());
|
||||
let location_in_manifest_base = file_path.strip_prefix(&manifest_dir);
|
||||
// Create files that are inside manifest directory
|
||||
if let Ok(location_in_manifest_base) = location_in_manifest_base {
|
||||
let p = target.join(location_in_manifest_base);
|
||||
// Check if it's a directory
|
||||
if is_dir {
|
||||
try!(fs::create_dir_all(p));
|
||||
} else {
|
||||
let mut target = try!(fs::File::create(p));
|
||||
try!(io::copy(&mut file, &mut target));
|
||||
let file_path = PathBuf::from(file.name());
|
||||
let location_in_manifest_base = file_path.strip_prefix(&manifest_dir);
|
||||
// Create files that are inside manifest directory
|
||||
if let Ok(location_in_manifest_base) = location_in_manifest_base {
|
||||
let p = target.join(location_in_manifest_base);
|
||||
// Check if it's a directory
|
||||
if is_dir {
|
||||
try!(fs::create_dir_all(p));
|
||||
} else {
|
||||
let mut target = try!(fs::File::create(p));
|
||||
try!(io::copy(&mut file, &mut target));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write manifest
|
||||
let manifest_str = try!(serialize_manifest(&manifest).map_err(ValidationError::ManifestSerialization));
|
||||
let manifest_path = target.join(MANIFEST_FILENAME);
|
||||
let mut manifest_file = try!(fs::File::create(manifest_path));
|
||||
try!(manifest_file.write_all(manifest_str.as_bytes()));
|
||||
// Write manifest
|
||||
let manifest_str = try!(serialize_manifest(&manifest).map_err(ValidationError::ManifestSerialization));
|
||||
let manifest_path = target.join(MANIFEST_FILENAME);
|
||||
let mut manifest_file = try!(fs::File::create(manifest_path));
|
||||
try!(manifest_file.write_all(manifest_str.as_bytes()));
|
||||
// Create endpoint
|
||||
let endpoint = LocalPageEndpoint::new(target, manifest.clone().into(), PageCache::Enabled, self.embeddable_on.clone());
|
||||
Ok(endpoint)
|
||||
};
|
||||
|
||||
// Create endpoint
|
||||
let app = LocalPageEndpoint::new(target, manifest.clone().into(), PageCache::Enabled, self.embeddable_on.clone());
|
||||
|
||||
// Return modified app manifest
|
||||
Ok((manifest.id.clone(), app))
|
||||
}
|
||||
|
||||
fn done(&self, endpoint: Option<LocalPageEndpoint>) {
|
||||
(self.on_done)(self.id.clone(), endpoint)
|
||||
let result = validate();
|
||||
(self.on_done)(result.as_ref().ok().cloned());
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,35 +22,41 @@ use std::sync::{mpsc, Arc};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{Instant, Duration};
|
||||
use util::Mutex;
|
||||
use url::Url;
|
||||
use fetch::{Client, Fetch, FetchResult};
|
||||
|
||||
use hyper::{server, Decoder, Encoder, Next, Method, Control};
|
||||
use hyper::net::HttpStream;
|
||||
use hyper::uri::RequestUri;
|
||||
use hyper::status::StatusCode;
|
||||
|
||||
use handlers::{ContentHandler, Redirection, extract_url};
|
||||
use page::LocalPageEndpoint;
|
||||
use endpoint::EndpointPath;
|
||||
use handlers::ContentHandler;
|
||||
use page::{LocalPageEndpoint, PageHandlerWaiting};
|
||||
|
||||
const FETCH_TIMEOUT: u64 = 30;
|
||||
|
||||
enum FetchState {
|
||||
Waiting,
|
||||
NotStarted(String),
|
||||
Error(ContentHandler),
|
||||
InProgress(mpsc::Receiver<FetchResult>),
|
||||
Done(String, LocalPageEndpoint, Redirection),
|
||||
Done(LocalPageEndpoint, Box<PageHandlerWaiting>),
|
||||
}
|
||||
|
||||
enum WaitResult {
|
||||
Error(ContentHandler),
|
||||
Done(LocalPageEndpoint),
|
||||
}
|
||||
|
||||
pub trait ContentValidator {
|
||||
type Error: fmt::Debug + fmt::Display;
|
||||
|
||||
fn validate_and_install(&self, app: PathBuf) -> Result<(String, LocalPageEndpoint), Self::Error>;
|
||||
fn done(&self, Option<LocalPageEndpoint>);
|
||||
fn validate_and_install(&self, path: PathBuf) -> Result<LocalPageEndpoint, Self::Error>;
|
||||
}
|
||||
|
||||
pub struct FetchControl {
|
||||
abort: Arc<AtomicBool>,
|
||||
listeners: Mutex<Vec<(Control, mpsc::Sender<FetchState>)>>,
|
||||
listeners: Mutex<Vec<(Control, mpsc::Sender<WaitResult>)>>,
|
||||
deadline: Instant,
|
||||
}
|
||||
|
||||
@ -65,9 +71,10 @@ impl Default for 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();
|
||||
for (control, sender) in listeners.drain(..) {
|
||||
trace!(target: "dapps", "Resuming request waiting for content...");
|
||||
if let Err(e) = sender.send(status()) {
|
||||
trace!(target: "dapps", "Waiting listener notification failed: {:?}", e);
|
||||
} else {
|
||||
@ -78,9 +85,9 @@ impl FetchControl {
|
||||
|
||||
fn set_status(&self, status: &FetchState) {
|
||||
match *status {
|
||||
FetchState::Error(ref handler) => self.notify(|| FetchState::Error(handler.clone())),
|
||||
FetchState::Done(ref id, ref endpoint, ref handler) => self.notify(|| FetchState::Done(id.clone(), endpoint.clone(), handler.clone())),
|
||||
FetchState::NotStarted(_) | FetchState::InProgress(_) => {},
|
||||
FetchState::Error(ref handler) => self.notify(|| WaitResult::Error(handler.clone())),
|
||||
FetchState::Done(ref endpoint, _) => self.notify(|| WaitResult::Done(endpoint.clone())),
|
||||
FetchState::NotStarted(_) | FetchState::InProgress(_) | FetchState::Waiting => {},
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,44 +95,66 @@ impl FetchControl {
|
||||
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();
|
||||
self.listeners.lock().push((control, tx));
|
||||
|
||||
Box::new(WaitingHandler {
|
||||
receiver: rx,
|
||||
state: None,
|
||||
state: FetchState::Waiting,
|
||||
uri: RequestUri::default(),
|
||||
path: path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WaitingHandler {
|
||||
receiver: mpsc::Receiver<FetchState>,
|
||||
state: Option<FetchState>,
|
||||
receiver: mpsc::Receiver<WaitResult>,
|
||||
state: FetchState,
|
||||
uri: RequestUri,
|
||||
path: EndpointPath,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
fn on_request_readable(&mut self, _decoder: &mut Decoder<HttpStream>) -> Next {
|
||||
self.state = self.receiver.try_recv().ok();
|
||||
Next::write()
|
||||
fn on_request_readable(&mut self, decoder: &mut Decoder<HttpStream>) -> Next {
|
||||
let result = self.receiver.try_recv().ok();
|
||||
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 {
|
||||
match self.state {
|
||||
Some(FetchState::Done(_, _, ref mut handler)) => handler.on_response(res),
|
||||
Some(FetchState::Error(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),
|
||||
_ => Next::end(),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_response_writable(&mut self, encoder: &mut Encoder<HttpStream>) -> Next {
|
||||
match self.state {
|
||||
Some(FetchState::Done(_, _, ref mut handler)) => handler.on_response_writable(encoder),
|
||||
Some(FetchState::Error(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),
|
||||
_ => Next::end(),
|
||||
}
|
||||
}
|
||||
@ -137,29 +166,19 @@ pub struct ContentFetcherHandler<H: ContentValidator> {
|
||||
status: FetchState,
|
||||
client: Option<Client>,
|
||||
installer: H,
|
||||
request_url: Option<Url>,
|
||||
path: EndpointPath,
|
||||
uri: RequestUri,
|
||||
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> {
|
||||
|
||||
pub fn new(
|
||||
url: String,
|
||||
path: EndpointPath,
|
||||
control: Control,
|
||||
handler: H,
|
||||
embeddable_on: Option<(String, u16)>,
|
||||
) -> (Self, Arc<FetchControl>) {
|
||||
|
||||
let fetch_control = Arc::new(FetchControl::default());
|
||||
let client = Client::default();
|
||||
let handler = ContentFetcherHandler {
|
||||
@ -168,7 +187,8 @@ impl<H: ContentValidator> ContentFetcherHandler<H> {
|
||||
client: Some(client),
|
||||
status: FetchState::NotStarted(url),
|
||||
installer: handler,
|
||||
request_url: None,
|
||||
path: path,
|
||||
uri: RequestUri::default(),
|
||||
embeddable_on: embeddable_on,
|
||||
};
|
||||
|
||||
@ -192,7 +212,6 @@ impl<H: ContentValidator> ContentFetcherHandler<H> {
|
||||
|
||||
impl<H: ContentValidator> server::Handler<HttpStream> for ContentFetcherHandler<H> {
|
||||
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 {
|
||||
Some(match *request.method() {
|
||||
// Start fetching content
|
||||
@ -205,8 +224,8 @@ impl<H: ContentValidator> server::Handler<HttpStream> for ContentFetcherHandler<
|
||||
Ok(receiver) => FetchState::InProgress(receiver),
|
||||
Err(e) => FetchState::Error(ContentHandler::error(
|
||||
StatusCode::BadGateway,
|
||||
"Unable To Start Dapp Download",
|
||||
"Could not initialize download of the dapp. It might be a problem with the remote server.",
|
||||
"Unable To Start Content Download",
|
||||
"Could not initialize download of the content. It might be a problem with the remote server.",
|
||||
Some(&format!("{}", e)),
|
||||
self.embeddable_on.clone(),
|
||||
)),
|
||||
@ -227,6 +246,7 @@ impl<H: ContentValidator> server::Handler<HttpStream> for ContentFetcherHandler<
|
||||
self.fetch_control.set_status(&status);
|
||||
self.status = status;
|
||||
}
|
||||
self.uri = request.uri().clone();
|
||||
|
||||
Next::read()
|
||||
}
|
||||
@ -266,11 +286,10 @@ impl<H: ContentValidator> server::Handler<HttpStream> for ContentFetcherHandler<
|
||||
self.embeddable_on.clone(),
|
||||
))
|
||||
},
|
||||
Ok((id, result)) => {
|
||||
let url: String = self.request_url.take()
|
||||
.map(|url| url.raw.into_string())
|
||||
.expect("Request URL always read in on_request; qed");
|
||||
FetchState::Done(id, result, Redirection::new(&url))
|
||||
Ok(endpoint) => {
|
||||
let mut handler = endpoint.to_page_handler(self.path.clone());
|
||||
handler.set_uri(&self.uri);
|
||||
FetchState::Done(endpoint, handler)
|
||||
},
|
||||
};
|
||||
// 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 {
|
||||
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),
|
||||
_ => 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 {
|
||||
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),
|
||||
_ => Next::end(),
|
||||
}
|
||||
|
@ -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.
|
||||
/// Resolves correct paths and serves as a plumbing code between
|
||||
/// hyper server and dapp.
|
||||
pub struct PageHandler<T: Dapp> {
|
||||
/// A Dapp.
|
||||
pub app: T,
|
||||
/// File currently being served (or `None` if file does not exist).
|
||||
/// File currently being served
|
||||
pub file: ServedFile<T>,
|
||||
/// Optional prefix to strip from path.
|
||||
pub prefix: Option<String>,
|
||||
@ -101,6 +107,21 @@ pub struct PageHandler<T: Dapp> {
|
||||
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> {
|
||||
fn extract_path(&self, path: &str) -> String {
|
||||
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> {
|
||||
fn on_request(&mut self, req: server::Request<HttpStream>) -> Next {
|
||||
self.file = match *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));
|
||||
self.set_uri(req.uri());
|
||||
Next::write()
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ use mime_guess;
|
||||
use std::io::{Seek, Read, SeekFrom};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use page::handler::{self, PageCache};
|
||||
use page::handler::{self, PageCache, PageHandlerWaiting};
|
||||
use endpoint::{Endpoint, EndpointInfo, EndpointPath, Handler};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -54,6 +54,36 @@ impl LocalPageEndpoint {
|
||||
pub fn path(&self) -> PathBuf {
|
||||
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 {
|
||||
@ -63,23 +93,9 @@ impl Endpoint for LocalPageEndpoint {
|
||||
|
||||
fn to_handler(&self, path: EndpointPath) -> Box<Handler> {
|
||||
if let Some(ref mime) = self.mime {
|
||||
Box::new(handler::PageHandler {
|
||||
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,
|
||||
})
|
||||
Box::new(self.page_handler_with_mime(path, mime))
|
||||
} else {
|
||||
Box::new(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,
|
||||
})
|
||||
Box::new(self.page_handler(path))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,5 +21,5 @@ mod handler;
|
||||
|
||||
pub use self::local::LocalPageEndpoint;
|
||||
pub use self::builtin::PageEndpoint;
|
||||
pub use self::handler::PageCache;
|
||||
pub use self::handler::{PageCache, PageHandlerWaiting};
|
||||
|
||||
|
@ -21,8 +21,9 @@
|
||||
},
|
||||
"genesis": {
|
||||
"seal": {
|
||||
"generic": {
|
||||
"rlp": "0xc28080"
|
||||
"authority_round": {
|
||||
"step": "0x0",
|
||||
"signature": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
}
|
||||
},
|
||||
"difficulty": "0x20000",
|
||||
|
@ -17,10 +17,7 @@
|
||||
},
|
||||
"genesis": {
|
||||
"seal": {
|
||||
"generic": {
|
||||
"fields": 1,
|
||||
"rlp": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa"
|
||||
}
|
||||
"generic": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa"
|
||||
},
|
||||
"difficulty": "0x20000",
|
||||
"author": "0x0000000000000000000000000000000000000000",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Ethereum Classic",
|
||||
"forkName": "classic",
|
||||
"dataDir": "classic",
|
||||
"engine": {
|
||||
"Ethash": {
|
||||
"params": {
|
||||
@ -10,14 +10,15 @@
|
||||
"durationLimit": "0x0d",
|
||||
"blockReward": "0x4563918244F40000",
|
||||
"registrar" : "0xc6d9d2cd449a754c494264e1809c50e34d64562b",
|
||||
"homesteadTransition": "0x118c30",
|
||||
"eip150Transition": "0x2625a0",
|
||||
"eip155Transition": "0x7fffffffffffffff",
|
||||
"eip160Transition": "0x7fffffffffffffff",
|
||||
"homesteadTransition": 1150000,
|
||||
"eip150Transition": 2500000,
|
||||
"eip155Transition": 3000000,
|
||||
"eip160Transition": 3000000,
|
||||
"ecip1010PauseTransition": 3000000,
|
||||
"ecip1010ContinueTransition": 5000000,
|
||||
|
||||
"eip161abcTransition": "0x7fffffffffffffff",
|
||||
"eip161dTransition": "0x7fffffffffffffff",
|
||||
"ecip1010PauseTransition": "0x2dc6c0",
|
||||
"ecip1010ContinueTransition": "0x4c4b40"
|
||||
"eip161dTransition": "0x7fffffffffffffff"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Expanse",
|
||||
"forkName": "expanse",
|
||||
"dataDir": "expanse",
|
||||
"engine": {
|
||||
"Ethash": {
|
||||
"params": {
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "Frontier/Homestead",
|
||||
"dataDir": "ethereum",
|
||||
"engine": {
|
||||
"Ethash": {
|
||||
"params": {
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "Morden",
|
||||
"dataDir": "test",
|
||||
"engine": {
|
||||
"Ethash": {
|
||||
"params": {
|
||||
@ -9,12 +10,15 @@
|
||||
"durationLimit": "0x0d",
|
||||
"blockReward": "0x4563918244F40000",
|
||||
"registrar": "0x52dff57a8a1532e6afb3dc07e2af58bb9eb05b3d",
|
||||
"homesteadTransition": "0x789b0",
|
||||
"eip150Transition": "0x1b34d8",
|
||||
"eip155Transition": 1885000,
|
||||
"eip160Transition": 1885000,
|
||||
"eip161abcTransition": 1885000,
|
||||
"eip161dTransition": 1885000
|
||||
"homesteadTransition": 494000,
|
||||
"eip150Transition": 1783000,
|
||||
"eip155Transition": 1915000,
|
||||
"eip160Transition": 1915000,
|
||||
"ecip1010PauseTransition": 1915000,
|
||||
"ecip1010ContinueTransition": 3415000,
|
||||
|
||||
"eip161abcTransition": "0x7fffffffffffffff",
|
||||
"eip161dTransition": "0x7fffffffffffffff"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "Ropsten",
|
||||
"dataDir": "test",
|
||||
"engine": {
|
||||
"Ethash": {
|
||||
"params": {
|
||||
|
@ -4,29 +4,27 @@
|
||||
"InstantSeal": null
|
||||
},
|
||||
"params": {
|
||||
"accountStartNonce": "0x0100000",
|
||||
"accountStartNonce": "0x0",
|
||||
"maximumExtraDataSize": "0x20",
|
||||
"minGasLimit": "0x1388",
|
||||
"networkID" : "0x2"
|
||||
"networkID" : "0x11"
|
||||
},
|
||||
"genesis": {
|
||||
"seal": {
|
||||
"generic": {
|
||||
"rlp": "0x0"
|
||||
}
|
||||
"generic": "0x0"
|
||||
},
|
||||
"difficulty": "0x20000",
|
||||
"author": "0x0000000000000000000000000000000000000000",
|
||||
"timestamp": "0x00",
|
||||
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"extraData": "0x",
|
||||
"gasLimit": "0x2fefd8"
|
||||
"gasLimit": "0x5B8D80"
|
||||
},
|
||||
"accounts": {
|
||||
"0000000000000000000000000000000000000001": { "balance": "1", "nonce": "1048576", "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } },
|
||||
"0000000000000000000000000000000000000002": { "balance": "1", "nonce": "1048576", "builtin": { "name": "sha256", "pricing": { "linear": { "base": 60, "word": 12 } } } },
|
||||
"0000000000000000000000000000000000000003": { "balance": "1", "nonce": "1048576", "builtin": { "name": "ripemd160", "pricing": { "linear": { "base": 600, "word": 120 } } } },
|
||||
"0000000000000000000000000000000000000004": { "balance": "1", "nonce": "1048576", "builtin": { "name": "identity", "pricing": { "linear": { "base": 15, "word": 3 } } } },
|
||||
"0x00a329c0648769a73afac7f9381e08fb43dbea72": { "balance": "1606938044258990275541962092341162602522202993782792835301376", "nonce": "1048576" }
|
||||
"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 } } } },
|
||||
"0x00a329c0648769a73afac7f9381e08fb43dbea72": { "balance": "1606938044258990275541962092341162602522202993782792835301376" }
|
||||
}
|
||||
}
|
||||
|
44
ethcore/res/tendermint.json
Normal file
44
ethcore/res/tendermint.json
Normal 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" }
|
||||
}
|
||||
}
|
@ -18,14 +18,14 @@
|
||||
|
||||
mod stores;
|
||||
|
||||
use self::stores::{AddressBook, DappsSettingsStore};
|
||||
use self::stores::{AddressBook, DappsSettingsStore, NewDappsPolicy};
|
||||
|
||||
use std::fmt;
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Instant, Duration};
|
||||
use util::{Mutex, RwLock};
|
||||
use ethstore::{SecretStore, Error as SSError, SafeAccount, EthStore};
|
||||
use ethstore::dir::{KeyDirectory};
|
||||
use util::RwLock;
|
||||
use ethstore::{SimpleSecretStore, SecretStore, Error as SSError, EthStore, EthMultiStore, random_string};
|
||||
use ethstore::dir::MemoryDirectory;
|
||||
use ethstore::ethkey::{Address, Message, Public, Secret, Random, Generator};
|
||||
use ethjson::misc::AccountMeta;
|
||||
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
|
||||
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.
|
||||
/// Responsible for unlocking accounts.
|
||||
pub struct AccountProvider {
|
||||
unlocked: Mutex<HashMap<Address, AccountData>>,
|
||||
sstore: Box<SecretStore>,
|
||||
unlocked: RwLock<HashMap<Address, AccountData>>,
|
||||
address_book: RwLock<AddressBook>,
|
||||
dapps_settings: RwLock<DappsSettingsStore>,
|
||||
/// Accounts on disk
|
||||
sstore: Box<SecretStore>,
|
||||
/// Accounts unlocked with rolling tokens
|
||||
transient_sstore: EthMultiStore,
|
||||
}
|
||||
|
||||
impl AccountProvider {
|
||||
/// Creates new account provider.
|
||||
pub fn new(sstore: Box<SecretStore>) -> Self {
|
||||
AccountProvider {
|
||||
unlocked: Mutex::new(HashMap::new()),
|
||||
unlocked: RwLock::new(HashMap::new()),
|
||||
address_book: RwLock::new(AddressBook::new(sstore.local_path().into())),
|
||||
dapps_settings: RwLock::new(DappsSettingsStore::new(sstore.local_path().into())),
|
||||
sstore: sstore,
|
||||
transient_sstore: transient_sstore(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates not disk backed provider.
|
||||
pub fn transient_provider() -> Self {
|
||||
AccountProvider {
|
||||
unlocked: Mutex::new(HashMap::new()),
|
||||
unlocked: RwLock::new(HashMap::new()),
|
||||
address_book: RwLock::new(AddressBook::transient()),
|
||||
dapps_settings: RwLock::new(DappsSettingsStore::transient()),
|
||||
sstore: Box::new(EthStore::open(Box::new(NullDir::default()))
|
||||
.expect("NullDir load always succeeds; qed"))
|
||||
sstore: Box::new(EthStore::open(Box::new(MemoryDirectory::default())).expect("MemoryDirectory load always succeeds; qed")),
|
||||
transient_sstore: transient_sstore(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,10 +156,49 @@ impl AccountProvider {
|
||||
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.
|
||||
pub fn dapps_addresses(&self, dapp: DappId) -> Result<Vec<Address>, Error> {
|
||||
let accounts = self.dapps_settings.read().get();
|
||||
Ok(accounts.get(&dapp).map(|settings| settings.accounts.clone()).unwrap_or_else(Vec::new))
|
||||
let dapps = self.dapps_settings.read();
|
||||
|
||||
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.
|
||||
@ -231,11 +259,8 @@ impl AccountProvider {
|
||||
|
||||
/// Returns `true` if the password for `account` is `password`. `false` if not.
|
||||
pub fn test_password(&self, account: &Address, password: &str) -> Result<bool, Error> {
|
||||
match self.sstore.sign(account, password, &Default::default()) {
|
||||
Ok(_) => Ok(true),
|
||||
Err(SSError::InvalidPassword) => Ok(false),
|
||||
Err(e) => Err(Error::SStore(e)),
|
||||
}
|
||||
self.sstore.test_password(account, password)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Permanently removes an account.
|
||||
@ -256,7 +281,7 @@ impl AccountProvider {
|
||||
let _ = try!(self.sstore.sign(&account, &password, &Default::default()));
|
||||
|
||||
// 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 Unlock::Perm = data.unlock {
|
||||
return Ok(())
|
||||
@ -273,7 +298,7 @@ impl AccountProvider {
|
||||
}
|
||||
|
||||
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();
|
||||
if let Unlock::Temp = data.unlock {
|
||||
unlocked.remove(account).expect("data exists: so key must exist: qed");
|
||||
@ -304,7 +329,7 @@ impl AccountProvider {
|
||||
|
||||
/// Checks if given account is unlocked
|
||||
pub fn is_unlocked(&self, account: Address) -> bool {
|
||||
let unlocked = self.unlocked.lock();
|
||||
let unlocked = self.unlocked.read();
|
||||
unlocked.get(&account).is_some()
|
||||
}
|
||||
|
||||
@ -314,6 +339,48 @@ impl AccountProvider {
|
||||
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.
|
||||
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)));
|
||||
@ -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(), "test".into(), 60000).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());
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn should_set_dapps_addresses() {
|
||||
// given
|
||||
let ap = AccountProvider::transient_provider();
|
||||
let app = "app1".to_owned();
|
||||
// set `AllAccounts` policy
|
||||
ap.set_new_dapps_whitelist(None).unwrap();
|
||||
|
||||
// when
|
||||
ap.set_dapps_addresses(app.clone(), vec![1.into(), 2.into()]).unwrap();
|
||||
@ -386,4 +471,23 @@ mod tests {
|
||||
// then
|
||||
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()]);
|
||||
}
|
||||
}
|
||||
|
@ -17,11 +17,11 @@
|
||||
//! Address Book and Dapps Settings Store
|
||||
|
||||
use std::{fs, fmt, hash, ops};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::path::PathBuf;
|
||||
|
||||
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;
|
||||
|
||||
/// 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.
|
||||
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 {
|
||||
/// Creates new store at given directory path.
|
||||
pub fn new(path: String) -> Self {
|
||||
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
|
||||
}
|
||||
|
||||
/// Creates transient store (no changes are saved to disk).
|
||||
pub fn transient() -> Self {
|
||||
DappsSettingsStore {
|
||||
cache: DiskMap::transient()
|
||||
settings: DiskMap::transient(),
|
||||
policy: DiskMap::transient(),
|
||||
recent: VecDeque::with_capacity(MAX_RECENT_DAPPS),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get copy of the dapps settings
|
||||
pub fn get(&self) -> HashMap<DappId, DappsSettings> {
|
||||
self.cache.clone()
|
||||
pub fn settings(&self) -> HashMap<DappId, DappsSettings> {
|
||||
self.settings.clone()
|
||||
}
|
||||
|
||||
fn save(&self) {
|
||||
self.cache.save(JsonSettings::write_dapps_settings)
|
||||
/// Returns current new dapps policy
|
||||
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>) {
|
||||
{
|
||||
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;
|
||||
}
|
||||
self.save();
|
||||
self.settings.save(JsonSettings::write_dapps_settings);
|
||||
}
|
||||
}
|
||||
|
||||
@ -216,7 +279,7 @@ impl<K: hash::Hash + Eq, V> DiskMap<K, V> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{AddressBook, DappsSettingsStore, DappsSettings};
|
||||
use super::{AddressBook, DappsSettingsStore, DappsSettings, NewDappsPolicy};
|
||||
use std::collections::HashMap;
|
||||
use ethjson::misc::AccountMeta;
|
||||
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}]);
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn should_remove_address() {
|
||||
let temp = RandomTempPath::create_dir();
|
||||
@ -268,4 +312,58 @@ mod tests {
|
||||
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()])
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use ipc::IpcConfig;
|
||||
use util::H256;
|
||||
use util::{H256, Bytes};
|
||||
|
||||
/// Represents what has to be handled by actor listening to chain events
|
||||
#[ipc]
|
||||
@ -27,6 +27,8 @@ pub trait ChainNotify : Send + Sync {
|
||||
_enacted: Vec<H256>,
|
||||
_retracted: Vec<H256>,
|
||||
_sealed: Vec<H256>,
|
||||
// Block bytes.
|
||||
_proposed: Vec<Bytes>,
|
||||
_duration: u64) {
|
||||
// does nothing by default
|
||||
}
|
||||
@ -41,6 +43,9 @@ pub trait ChainNotify : Send + Sync {
|
||||
// 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
|
||||
fn transactions_received(&self,
|
||||
_hashes: Vec<H256>,
|
||||
|
@ -24,8 +24,8 @@ use time::precise_time_ns;
|
||||
// util
|
||||
use util::{Bytes, PerfTimer, Itertools, Mutex, RwLock, Hashable};
|
||||
use util::{journaldb, TrieFactory, Trie};
|
||||
use util::trie::TrieSpec;
|
||||
use util::{U256, H256, Address, H2048, Uint, FixedHash};
|
||||
use util::trie::TrieSpec;
|
||||
use util::kvdb::*;
|
||||
|
||||
// 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
|
||||
pub fn import_verified_blocks(&self) -> usize {
|
||||
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 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 _import_lock = self.import_lock.lock();
|
||||
@ -417,12 +418,17 @@ impl Client {
|
||||
continue;
|
||||
}
|
||||
if let Ok(closed_block) = self.check_and_close_block(&block) {
|
||||
imported_blocks.push(header.hash());
|
||||
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());
|
||||
|
||||
let route = self.commit_block(closed_block, &header.hash(), &block.bytes);
|
||||
import_results.push(route);
|
||||
let route = self.commit_block(closed_block, &header.hash(), &block.bytes);
|
||||
import_results.push(route);
|
||||
|
||||
self.report.write().accrue_block(&block);
|
||||
self.report.write().accrue_block(&block);
|
||||
}
|
||||
} else {
|
||||
invalid_blocks.insert(header.hash());
|
||||
}
|
||||
@ -436,7 +442,7 @@ impl Client {
|
||||
}
|
||||
let is_empty = self.block_queue.mark_as_good(&imported_blocks);
|
||||
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(),
|
||||
retracted.clone(),
|
||||
Vec::new(),
|
||||
proposed_blocks.clone(),
|
||||
duration,
|
||||
);
|
||||
});
|
||||
@ -577,9 +584,10 @@ impl Client {
|
||||
self.miner.clone()
|
||||
}
|
||||
|
||||
/// Used by PoA to try sealing on period change.
|
||||
pub fn update_sealing(&self) {
|
||||
self.miner.update_sealing(self)
|
||||
|
||||
/// Replace io channel. Useful for testing.
|
||||
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.
|
||||
@ -1290,6 +1298,18 @@ impl BlockChainClient for Client {
|
||||
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> {
|
||||
self.engine.signing_network_id(&self.latest_env_info())
|
||||
}
|
||||
@ -1314,7 +1334,6 @@ impl BlockChainClient for Client {
|
||||
}
|
||||
|
||||
impl MiningBlockChainClient for Client {
|
||||
|
||||
fn latest_schedule(&self) -> Schedule {
|
||||
self.engine.schedule(&self.latest_env_info())
|
||||
}
|
||||
@ -1357,6 +1376,30 @@ impl MiningBlockChainClient for Client {
|
||||
&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 {
|
||||
let h = block.header().hash();
|
||||
let start = precise_time_ns();
|
||||
@ -1381,6 +1424,7 @@ impl MiningBlockChainClient for Client {
|
||||
enacted.clone(),
|
||||
retracted.clone(),
|
||||
vec![h.clone()],
|
||||
vec![],
|
||||
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)]
|
||||
mod tests {
|
||||
|
||||
|
@ -90,6 +90,8 @@ pub struct TestBlockChainClient {
|
||||
pub ancient_block: RwLock<Option<(H256, u64)>>,
|
||||
/// First block info.
|
||||
pub first_block: RwLock<Option<(H256, u64)>>,
|
||||
/// Traces to return
|
||||
pub traces: RwLock<Option<Vec<LocalizedTrace>>>,
|
||||
}
|
||||
|
||||
/// Used for generating test client blocks.
|
||||
@ -151,6 +153,7 @@ impl TestBlockChainClient {
|
||||
latest_block_timestamp: RwLock::new(10_000_000),
|
||||
ancient_block: RwLock::new(None),
|
||||
first_block: RwLock::new(None),
|
||||
traces: RwLock::new(None),
|
||||
};
|
||||
client.add_blocks(1, EachBlockWith::Nothing); // add genesis block
|
||||
client.genesis_hash = client.last_hash.read().clone();
|
||||
@ -360,6 +363,18 @@ impl MiningBlockChainClient for TestBlockChainClient {
|
||||
fn import_sealed_block(&self, _block: SealedBlock) -> ImportResult {
|
||||
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 {
|
||||
@ -642,19 +657,19 @@ impl BlockChainClient for TestBlockChainClient {
|
||||
}
|
||||
|
||||
fn filter_traces(&self, _filter: TraceFilter) -> Option<Vec<LocalizedTrace>> {
|
||||
unimplemented!();
|
||||
self.traces.read().clone()
|
||||
}
|
||||
|
||||
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>> {
|
||||
unimplemented!();
|
||||
self.traces.read().clone()
|
||||
}
|
||||
|
||||
fn block_traces(&self, _trace: BlockId) -> Option<Vec<LocalizedTrace>> {
|
||||
unimplemented!();
|
||||
self.traces.read().clone()
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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> {
|
||||
self.miner.pending_transactions(self.chain_info().best_block_number)
|
||||
}
|
||||
|
@ -202,6 +202,12 @@ pub trait BlockChainClient : Sync + Send {
|
||||
/// Queue transactions for importing.
|
||||
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
|
||||
fn pending_transactions(&self) -> Vec<SignedTransaction>;
|
||||
|
||||
@ -273,6 +279,15 @@ pub trait MiningBlockChainClient: BlockChainClient {
|
||||
/// Returns 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.
|
||||
fn import_sealed_block(&self, block: SealedBlock) -> ImportResult;
|
||||
|
||||
|
@ -25,7 +25,7 @@ use rlp::{UntrustedRlp, Rlp, View, encode};
|
||||
use account_provider::AccountProvider;
|
||||
use block::*;
|
||||
use spec::CommonParams;
|
||||
use engines::Engine;
|
||||
use engines::{Engine, Seal, EngineError};
|
||||
use header::Header;
|
||||
use error::{Error, BlockError};
|
||||
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
|
||||
/// be returned.
|
||||
fn generate_seal(&self, block: &ExecutedBlock) -> Option<Vec<Bytes>> {
|
||||
if self.proposed.load(AtomicOrdering::SeqCst) { return None; }
|
||||
fn generate_seal(&self, block: &ExecutedBlock) -> Seal {
|
||||
if self.proposed.load(AtomicOrdering::SeqCst) { return Seal::None; }
|
||||
let header = block.header();
|
||||
let step = self.step();
|
||||
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()) {
|
||||
trace!(target: "poa", "generate_seal: Issuing a block for step {}.", step);
|
||||
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 {
|
||||
warn!(target: "poa", "generate_seal: FAIL: Accounts secret key unavailable.");
|
||||
}
|
||||
@ -245,7 +246,7 @@ impl Engine for AuthorityRound {
|
||||
} else {
|
||||
trace!(target: "poa", "generate_seal: Not a proposer for step {}.", step);
|
||||
}
|
||||
None
|
||||
Seal::None
|
||||
}
|
||||
|
||||
/// Check the number of seal fields.
|
||||
@ -288,7 +289,7 @@ impl Engine for AuthorityRound {
|
||||
// Check if parent is from a previous step.
|
||||
if step == try!(header_step(parent)) {
|
||||
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;
|
||||
@ -347,6 +348,7 @@ mod tests {
|
||||
use tests::helpers::*;
|
||||
use account_provider::AccountProvider;
|
||||
use spec::Spec;
|
||||
use engines::Seal;
|
||||
|
||||
#[test]
|
||||
fn has_valid_metadata() {
|
||||
@ -416,17 +418,17 @@ mod tests {
|
||||
let b2 = b2.close_and_lock();
|
||||
|
||||
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());
|
||||
// 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());
|
||||
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());
|
||||
// Second proposal is forbidden.
|
||||
assert!(engine.generate_seal(b2.block()).is_none());
|
||||
assert!(engine.generate_seal(b2.block()) == Seal::None);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@ use account_provider::AccountProvider;
|
||||
use block::*;
|
||||
use builtin::Builtin;
|
||||
use spec::CommonParams;
|
||||
use engines::Engine;
|
||||
use engines::{Engine, Seal};
|
||||
use env_info::EnvInfo;
|
||||
use error::{BlockError, Error};
|
||||
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
|
||||
/// 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() {
|
||||
let header = block.header();
|
||||
let message = header.bare_hash();
|
||||
// account should be pernamently unlocked, otherwise sealing will fail
|
||||
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 {
|
||||
trace!(target: "basicauthority", "generate_seal: FAIL: accounts secret key unavailable");
|
||||
}
|
||||
} else {
|
||||
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> {
|
||||
@ -199,6 +199,7 @@ mod tests {
|
||||
use account_provider::AccountProvider;
|
||||
use header::Header;
|
||||
use spec::Spec;
|
||||
use engines::Seal;
|
||||
|
||||
/// Create a new test chain spec with `BasicAuthority` consensus engine.
|
||||
fn new_test_authority() -> Spec {
|
||||
@ -269,8 +270,9 @@ mod tests {
|
||||
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 = b.close_and_lock();
|
||||
let seal = engine.generate_seal(b.block()).unwrap();
|
||||
assert!(b.try_seal(engine, seal).is_ok());
|
||||
if let Seal::Regular(seal) = engine.generate_seal(b.block()) {
|
||||
assert!(b.try_seal(engine, seal).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -17,12 +17,11 @@
|
||||
use std::collections::BTreeMap;
|
||||
use util::Address;
|
||||
use builtin::Builtin;
|
||||
use engines::Engine;
|
||||
use engines::{Engine, Seal};
|
||||
use env_info::EnvInfo;
|
||||
use spec::CommonParams;
|
||||
use evm::Schedule;
|
||||
use block::ExecutedBlock;
|
||||
use util::Bytes;
|
||||
|
||||
/// An engine which does not provide any consensus mechanism, just seals blocks internally.
|
||||
pub struct InstantSeal {
|
||||
@ -54,13 +53,13 @@ impl Engine for InstantSeal {
|
||||
}
|
||||
|
||||
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 generate_seal(&self, _block: &ExecutedBlock) -> Option<Vec<Bytes>> {
|
||||
Some(Vec::new())
|
||||
fn generate_seal(&self, _block: &ExecutedBlock) -> Seal {
|
||||
Seal::Regular(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,6 +71,7 @@ mod tests {
|
||||
use spec::Spec;
|
||||
use header::Header;
|
||||
use block::*;
|
||||
use engines::Seal;
|
||||
|
||||
#[test]
|
||||
fn instant_can_seal() {
|
||||
@ -84,8 +84,9 @@ mod tests {
|
||||
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 = b.close_and_lock();
|
||||
let seal = engine.generate_seal(b.block()).unwrap();
|
||||
assert!(b.try_seal(engine, seal).is_ok());
|
||||
if let Seal::Regular(seal) = engine.generate_seal(b.block()) {
|
||||
assert!(b.try_seal(engine, seal).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -20,11 +20,13 @@ mod null_engine;
|
||||
mod instant_seal;
|
||||
mod basic_authority;
|
||||
mod authority_round;
|
||||
mod tendermint;
|
||||
|
||||
pub use self::null_engine::NullEngine;
|
||||
pub use self::instant_seal::InstantSeal;
|
||||
pub use self::basic_authority::BasicAuthority;
|
||||
pub use self::authority_round::AuthorityRound;
|
||||
pub use self::tendermint::Tendermint;
|
||||
|
||||
use util::*;
|
||||
use account_provider::AccountProvider;
|
||||
@ -42,6 +44,47 @@ use ethereum::ethash;
|
||||
use blockchain::extras::BlockDetails;
|
||||
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.
|
||||
/// Provides hooks into each of the major parts of block import.
|
||||
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
|
||||
/// 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)
|
||||
/// 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());
|
||||
}
|
||||
|
||||
/// 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
|
||||
// from Spec into here and removing the Spec::builtins field.
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
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.
|
||||
fn register_message_channel(&self, _message_channel: IoChannel<ClientIoMessage>) {}
|
||||
|
||||
|
279
ethcore/src/engines/tendermint/message.rs
Normal file
279
ethcore/src/engines/tendermint/message.rs
Normal 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());
|
||||
}
|
||||
}
|
962
ethcore/src/engines/tendermint/mod.rs
Normal file
962
ethcore/src/engines/tendermint/mod.rs
Normal 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();
|
||||
}
|
||||
}
|
72
ethcore/src/engines/tendermint/params.rs
Normal file
72
ethcore/src/engines/tendermint/params.rs
Normal 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),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
96
ethcore/src/engines/tendermint/transition.rs
Normal file
96
ethcore/src/engines/tendermint/transition.rs
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
272
ethcore/src/engines/tendermint/vote_collector.rs
Normal file
272
ethcore/src/engines/tendermint/vote_collector.rs
Normal 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);
|
||||
}
|
||||
}
|
@ -24,6 +24,7 @@ use client::Error as ClientError;
|
||||
use ipc::binary::{BinaryConvertError, BinaryConvertable};
|
||||
use types::block_import_error::BlockImportError;
|
||||
use snapshot::Error as SnapshotError;
|
||||
use engines::EngineError;
|
||||
use ethkey::Error as EthkeyError;
|
||||
|
||||
pub use types::executed::{ExecutionError, CallError};
|
||||
@ -167,8 +168,6 @@ pub enum BlockError {
|
||||
UnknownParent(H256),
|
||||
/// Uncle parent given is unknown.
|
||||
UnknownUncleParent(H256),
|
||||
/// The same author issued different votes at the same step.
|
||||
DoubleVote(H160),
|
||||
}
|
||||
|
||||
impl fmt::Display for BlockError {
|
||||
@ -202,7 +201,6 @@ impl fmt::Display for BlockError {
|
||||
RidiculousNumber(ref oob) => format!("Implausible block number. {}", oob),
|
||||
UnknownParent(ref hash) => format!("Unknown 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))
|
||||
@ -263,6 +261,8 @@ pub enum Error {
|
||||
Snappy(::util::snappy::InvalidInput),
|
||||
/// Snapshot error.
|
||||
Snapshot(SnapshotError),
|
||||
/// Consensus vote error.
|
||||
Engine(EngineError),
|
||||
/// Ethkey error.
|
||||
Ethkey(EthkeyError),
|
||||
}
|
||||
@ -285,6 +285,7 @@ impl fmt::Display for Error {
|
||||
Error::StdIo(ref err) => err.fmt(f),
|
||||
Error::Snappy(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),
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
fn from(err: EthkeyError) -> Error {
|
||||
Error::Ethkey(err)
|
||||
|
@ -26,12 +26,12 @@ use state::{State, CleanupMode};
|
||||
use client::{MiningBlockChainClient, Executive, Executed, EnvInfo, TransactOptions, BlockId, CallAnalytics, TransactionId};
|
||||
use client::TransactionImportResult;
|
||||
use executive::contract_address;
|
||||
use block::{ClosedBlock, SealedBlock, IsBlock, Block};
|
||||
use block::{ClosedBlock, IsBlock, Block};
|
||||
use error::*;
|
||||
use transaction::{Action, SignedTransaction};
|
||||
use receipt::{Receipt, RichReceipt};
|
||||
use spec::Spec;
|
||||
use engines::Engine;
|
||||
use engines::{Engine, Seal};
|
||||
use miner::{MinerService, MinerStatus, TransactionQueue, PrioritizationStrategy, AccountDetails, TransactionOrigin};
|
||||
use miner::banning_queue::{BanningTransactionQueue, Threshold};
|
||||
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),
|
||||
/// 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.
|
||||
/// Attempts to perform internal sealing (one that does not require work) and handles the result depending on the type of Seal.
|
||||
fn seal_and_import_block_internally(&self, chain: &MiningBlockChainClient, block: ClosedBlock) -> bool {
|
||||
if !block.transactions().is_empty() || self.forced_sealing() {
|
||||
if let Ok(sealed) = self.seal_block_internally(block) {
|
||||
if chain.import_sealed_block(sealed).is_ok() {
|
||||
trace!(target: "miner", "import_block_internally: imported internally sealed block");
|
||||
return true
|
||||
}
|
||||
trace!(target: "miner", "seal_block_internally: attempting internal seal.");
|
||||
match self.engine.generate_seal(block.block()) {
|
||||
// Save proposal for later seal submission and broadcast it.
|
||||
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
|
||||
})
|
||||
},
|
||||
// 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
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Prepares work which has to be done to seal.
|
||||
@ -1024,7 +1033,6 @@ impl MinerService for Miner {
|
||||
self.transaction_queue.lock().last_nonce(address)
|
||||
}
|
||||
|
||||
|
||||
/// Update sealing if required.
|
||||
/// Prepare the block and work if the Engine does not seal internally.
|
||||
fn update_sealing(&self, chain: &MiningBlockChainClient) {
|
||||
@ -1039,7 +1047,9 @@ impl MinerService for Miner {
|
||||
let (block, original_work_hash) = self.prepare_block(chain);
|
||||
if self.seals_internally {
|
||||
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 {
|
||||
trace!(target: "miner", "update_sealing: engine does not seal internally, preparing work");
|
||||
self.prepare_work(block, original_work_hash);
|
||||
|
@ -20,7 +20,7 @@ use util::*;
|
||||
use io::*;
|
||||
use spec::Spec;
|
||||
use error::*;
|
||||
use client::{Client, ClientConfig, ChainNotify};
|
||||
use client::{Client, BlockChainClient, MiningBlockChainClient, ClientConfig, ChainNotify};
|
||||
use miner::Miner;
|
||||
use snapshot::ManifestData;
|
||||
use snapshot::service::{Service as SnapshotService, ServiceParams as SnapServiceParams};
|
||||
@ -28,11 +28,9 @@ use std::sync::atomic::AtomicBool;
|
||||
|
||||
#[cfg(feature="ipc")]
|
||||
use nanoipc;
|
||||
#[cfg(feature="ipc")]
|
||||
use client::BlockChainClient;
|
||||
|
||||
/// Message type for external and internal events
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub enum ClientIoMessage {
|
||||
/// Best Block Hash in chain has been changed
|
||||
NewChainHead,
|
||||
@ -50,6 +48,12 @@ pub enum ClientIoMessage {
|
||||
TakeSnapshot(u64),
|
||||
/// Trigger sealing update (useful for internal sealing).
|
||||
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.
|
||||
@ -77,9 +81,6 @@ impl ClientService {
|
||||
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()));
|
||||
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);
|
||||
|
||||
@ -220,9 +221,11 @@ impl IoHandler<ClientIoMessage> for ClientIoHandler {
|
||||
debug!(target: "snapshot", "Failed to initialize periodic snapshot thread: {:?}", e);
|
||||
}
|
||||
},
|
||||
ClientIoMessage::UpdateSealing => {
|
||||
trace!(target: "authorityround", "message: UpdateSealing");
|
||||
self.client.update_sealing()
|
||||
ClientIoMessage::UpdateSealing => self.client.update_sealing(),
|
||||
ClientIoMessage::SubmitSeal(ref hash, ref seal) => self.client.submit_seal(*hash, seal.clone()),
|
||||
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
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ use service::ClientIoMessage;
|
||||
use views::HeaderView;
|
||||
|
||||
use io::IoChannel;
|
||||
use util::hash::H256;
|
||||
use util::{H256, Bytes};
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@ -107,6 +107,7 @@ impl ChainNotify for Watcher {
|
||||
_: Vec<H256>,
|
||||
_: Vec<H256>,
|
||||
_: Vec<H256>,
|
||||
_: Vec<Bytes>,
|
||||
_duration: u64)
|
||||
{
|
||||
if self.oracle.is_major_importing() { return }
|
||||
@ -174,6 +175,7 @@ mod tests {
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
//! Spec seal.
|
||||
|
||||
use rlp::*;
|
||||
use util::hash::{H64, H256};
|
||||
use util::hash::{H64, H256, H520};
|
||||
use ethjson;
|
||||
|
||||
/// Classic ethereum seal.
|
||||
@ -32,23 +32,55 @@ impl Into<Generic> for Ethereum {
|
||||
fn into(self) -> Generic {
|
||||
let mut s = RlpStream::new_list(2);
|
||||
s.append(&self.mix_hash).append(&self.nonce);
|
||||
Generic {
|
||||
rlp: s.out()
|
||||
}
|
||||
Generic(s.out())
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic seal.
|
||||
pub struct Generic {
|
||||
/// Seal rlp.
|
||||
pub rlp: Vec<u8>,
|
||||
/// AuthorityRound seal.
|
||||
pub struct AuthorityRound {
|
||||
/// Seal step.
|
||||
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.
|
||||
pub enum Seal {
|
||||
/// Classic ethereum seal.
|
||||
Ethereum(Ethereum),
|
||||
/// Generic seal.
|
||||
/// AuthorityRound seal.
|
||||
AuthorityRound(AuthorityRound),
|
||||
/// Tendermint seal.
|
||||
Tendermint(Tendermint),
|
||||
/// Generic RLP seal.
|
||||
Generic(Generic),
|
||||
}
|
||||
|
||||
@ -59,9 +91,16 @@ impl From<ethjson::spec::Seal> for Seal {
|
||||
nonce: eth.nonce.into(),
|
||||
mix_hash: eth.mix_hash.into()
|
||||
}),
|
||||
ethjson::spec::Seal::Generic(g) => Seal::Generic(Generic {
|
||||
rlp: g.rlp.into()
|
||||
})
|
||||
ethjson::spec::Seal::AuthorityRound(ar) => Seal::AuthorityRound(AuthorityRound {
|
||||
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 {
|
||||
match self {
|
||||
Seal::Generic(generic) => generic,
|
||||
Seal::Ethereum(eth) => eth.into()
|
||||
Seal::Ethereum(eth) => eth.into(),
|
||||
Seal::AuthorityRound(ar) => ar.into(),
|
||||
Seal::Tendermint(tender) => tender.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@
|
||||
|
||||
use util::*;
|
||||
use builtin::Builtin;
|
||||
use engines::{Engine, NullEngine, InstantSeal, BasicAuthority, AuthorityRound};
|
||||
use engines::{Engine, NullEngine, InstantSeal, BasicAuthority, AuthorityRound, Tendermint};
|
||||
use pod_state::*;
|
||||
use account_db::*;
|
||||
use header::{BlockNumber, Header};
|
||||
@ -66,8 +66,8 @@ pub struct Spec {
|
||||
pub name: String,
|
||||
/// What engine are we using for this?
|
||||
pub engine: Arc<Engine>,
|
||||
/// The fork identifier for this chain. Only needed to distinguish two chains sharing the same genesis.
|
||||
pub fork_name: Option<String>,
|
||||
/// Name of the subdir inside the main data dir to use for chain data and settings.
|
||||
pub data_dir: String,
|
||||
|
||||
/// Known nodes on the network in enode format.
|
||||
pub nodes: Vec<String>,
|
||||
@ -107,13 +107,13 @@ impl From<ethjson::spec::Spec> for Spec {
|
||||
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 g = Genesis::from(s.genesis);
|
||||
let seal: GenericSeal = g.seal.into();
|
||||
let GenericSeal(seal_rlp) = g.seal.into();
|
||||
let params = CommonParams::from(s.params);
|
||||
Spec {
|
||||
name: s.name.into(),
|
||||
name: s.name.clone().into(),
|
||||
params: params.clone(),
|
||||
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),
|
||||
parent_hash: g.parent_hash,
|
||||
transactions_root: g.transactions_root,
|
||||
@ -124,7 +124,7 @@ impl From<ethjson::spec::Spec> for Spec {
|
||||
gas_used: g.gas_used,
|
||||
timestamp: g.timestamp,
|
||||
extra_data: g.extra_data,
|
||||
seal_rlp: seal.rlp,
|
||||
seal_rlp: seal_rlp,
|
||||
state_root_memo: RwLock::new(g.state_root),
|
||||
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::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::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.
|
||||
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.transactions_root = g.transactions_root;
|
||||
self.receipts_root = g.receipts_root;
|
||||
@ -218,7 +219,7 @@ impl Spec {
|
||||
self.gas_used = g.gas_used;
|
||||
self.timestamp = g.timestamp;
|
||||
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);
|
||||
}
|
||||
|
||||
@ -275,6 +276,10 @@ impl Spec {
|
||||
/// 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.
|
||||
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)]
|
||||
|
@ -457,7 +457,6 @@ impl StateDB {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use util::{U256, H256, FixedHash, Address, DBTransaction};
|
||||
use tests::helpers::*;
|
||||
use state::Account;
|
||||
@ -531,4 +530,3 @@ mod tests {
|
||||
assert!(s.get_cached_account(&address).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@ use super::trace::{Action, Res};
|
||||
use header::BlockNumber;
|
||||
|
||||
/// Localized trace.
|
||||
#[derive(Debug, PartialEq, Binary)]
|
||||
#[derive(Debug, PartialEq, Clone, Binary)]
|
||||
pub struct LocalizedTrace {
|
||||
/// Type of action performed by a transaction.
|
||||
pub action: Action,
|
||||
|
@ -18,6 +18,7 @@ use std::ops::{Deref, DerefMut};
|
||||
use std::cmp::PartialEq;
|
||||
use std::{mem, fmt};
|
||||
use std::str::FromStr;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use secp256k1::{Message as SecpMessage, RecoverableSignature, RecoveryId, Error as SecpError};
|
||||
use secp256k1::key::{SecretKey, PublicKey};
|
||||
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 {
|
||||
fn from(s: [u8; 65]) -> Self {
|
||||
Signature(s)
|
||||
|
@ -18,7 +18,6 @@ use std::{fs, io};
|
||||
use std::path::{PathBuf, Path};
|
||||
use std::collections::HashMap;
|
||||
use time;
|
||||
use ethkey::Address;
|
||||
use {json, SafeAccount, Error};
|
||||
use json::Uuid;
|
||||
use super::KeyDirectory;
|
||||
@ -106,6 +105,11 @@ impl KeyDirectory for DiskDirectory {
|
||||
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> {
|
||||
// transform account into key file
|
||||
let keyfile: json::KeyFile = account.clone().into();
|
||||
@ -138,12 +142,12 @@ impl KeyDirectory for DiskDirectory {
|
||||
Ok(account)
|
||||
}
|
||||
|
||||
fn remove(&self, address: &Address) -> Result<(), Error> {
|
||||
fn remove(&self, account: &SafeAccount) -> Result<(), Error> {
|
||||
// enumerate all entries in keystore
|
||||
// and find entry with given address
|
||||
let to_remove = try!(self.files())
|
||||
.into_iter()
|
||||
.find(|&(_, ref account)| &account.address == address);
|
||||
.find(|&(_, ref acc)| acc == account);
|
||||
|
||||
// remove it
|
||||
match to_remove {
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use ethkey::Address;
|
||||
use {SafeAccount, Error};
|
||||
use super::{KeyDirectory, DiskDirectory, DirectoryType};
|
||||
|
||||
@ -89,7 +88,11 @@ impl KeyDirectory for GethDirectory {
|
||||
self.dir.insert(account)
|
||||
}
|
||||
|
||||
fn remove(&self, address: &Address) -> Result<(), Error> {
|
||||
self.dir.remove(address)
|
||||
fn update(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
|
||||
self.dir.update(account)
|
||||
}
|
||||
|
||||
fn remove(&self, account: &SafeAccount) -> Result<(), Error> {
|
||||
self.dir.remove(account)
|
||||
}
|
||||
}
|
||||
|
67
ethstore/src/dir/memory.rs
Normal file
67
ethstore/src/dir/memory.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
|
@ -14,12 +14,12 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use ethkey::Address;
|
||||
use std::path::{PathBuf};
|
||||
use {SafeAccount, Error};
|
||||
|
||||
mod disk;
|
||||
mod geth;
|
||||
mod memory;
|
||||
mod parity;
|
||||
|
||||
pub enum DirectoryType {
|
||||
@ -30,10 +30,12 @@ pub enum DirectoryType {
|
||||
pub trait KeyDirectory: Send + Sync {
|
||||
fn load(&self) -> Result<Vec<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 }
|
||||
}
|
||||
|
||||
pub use self::disk::DiskDirectory;
|
||||
pub use self::geth::GethDirectory;
|
||||
pub use self::memory::MemoryDirectory;
|
||||
pub use self::parity::ParityDirectory;
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use ethkey::Address;
|
||||
use {SafeAccount, Error};
|
||||
use super::{KeyDirectory, DiskDirectory, DirectoryType};
|
||||
|
||||
@ -68,7 +67,11 @@ impl KeyDirectory for ParityDirectory {
|
||||
self.dir.insert(account)
|
||||
}
|
||||
|
||||
fn remove(&self, address: &Address) -> Result<(), Error> {
|
||||
self.dir.remove(address)
|
||||
fn update(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
|
||||
self.dir.update(account)
|
||||
}
|
||||
|
||||
fn remove(&self, account: &SafeAccount) -> Result<(), Error> {
|
||||
self.dir.remove(account)
|
||||
}
|
||||
}
|
||||
|
@ -16,23 +16,19 @@
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::mem;
|
||||
use ethkey::KeyPair;
|
||||
use parking_lot::RwLock;
|
||||
|
||||
use crypto::KEY_ITERATIONS;
|
||||
use random::Random;
|
||||
use ethkey::{Signature, Address, Message, Secret, Public};
|
||||
use ethkey::{Signature, Address, Message, Secret, Public, KeyPair};
|
||||
use dir::KeyDirectory;
|
||||
use account::SafeAccount;
|
||||
use {Error, SecretStore};
|
||||
use json;
|
||||
use json::Uuid;
|
||||
use parking_lot::RwLock;
|
||||
use presale::PresaleWallet;
|
||||
use import;
|
||||
use json::{self, Uuid};
|
||||
use {import, Error, SimpleSecretStore, SecretStore};
|
||||
|
||||
pub struct EthStore {
|
||||
dir: Box<KeyDirectory>,
|
||||
iterations: u32,
|
||||
cache: RwLock<BTreeMap<Address, SafeAccount>>,
|
||||
store: EthMultiStore,
|
||||
}
|
||||
|
||||
impl EthStore {
|
||||
@ -41,57 +37,46 @@ impl EthStore {
|
||||
}
|
||||
|
||||
pub fn open_with_iterations(directory: Box<KeyDirectory>, iterations: u32) -> Result<Self, Error> {
|
||||
let accounts = try!(directory.load());
|
||||
let cache = accounts.into_iter().map(|account| (account.address.clone(), account)).collect();
|
||||
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(())
|
||||
Ok(EthStore {
|
||||
store: try!(EthMultiStore::open_with_iterations(directory, iterations)),
|
||||
})
|
||||
}
|
||||
|
||||
fn get(&self, address: &Address) -> Result<SafeAccount, Error> {
|
||||
{
|
||||
let cache = self.cache.read();
|
||||
if let Some(account) = cache.get(address) {
|
||||
return Ok(account.clone())
|
||||
}
|
||||
}
|
||||
try!(self.reload_accounts());
|
||||
let cache = self.cache.read();
|
||||
cache.get(address).cloned().ok_or(Error::InvalidAccount)
|
||||
let mut accounts = try!(self.store.get(address)).into_iter();
|
||||
accounts.next().ok_or(Error::InvalidAccount)
|
||||
}
|
||||
}
|
||||
|
||||
impl SimpleSecretStore for EthStore {
|
||||
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 {
|
||||
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> {
|
||||
let json_wallet = try!(json::PresaleWallet::load(json).map_err(|_| Error::InvalidKeyFile("Invalid JSON format".to_owned())));
|
||||
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));
|
||||
safe_account.address = try!(KeyPair::from_secret(secret)).address();
|
||||
let address = safe_account.address.clone();
|
||||
try!(self.save(safe_account));
|
||||
try!(self.store.import(safe_account));
|
||||
Ok(address)
|
||||
}
|
||||
|
||||
fn accounts(&self) -> Result<Vec<Address>, Error> {
|
||||
try!(self.reload_accounts());
|
||||
Ok(self.cache.read().keys().cloned().collect())
|
||||
}
|
||||
|
||||
fn change_password(&self, address: &Address, old_password: &str, new_password: &str) -> Result<(), Error> {
|
||||
// change password
|
||||
fn test_password(&self, address: &Address, password: &str) -> Result<bool, Error> {
|
||||
let account = try!(self.get(address));
|
||||
let account = try!(account.change_password(old_password, new_password, self.iterations));
|
||||
|
||||
// save to file
|
||||
self.save(account)
|
||||
Ok(account.check_password(password))
|
||||
}
|
||||
|
||||
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(())
|
||||
} else {
|
||||
Err(Error::InvalidPassword)
|
||||
}
|
||||
}
|
||||
|
||||
fn sign(&self, address: &Address, password: &str, message: &Message) -> Result<Signature, Error> {
|
||||
fn copy_account(&self, new_store: &SimpleSecretStore, address: &Address, password: &str, new_password: &str) -> Result<(), 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)
|
||||
let secret = try!(account.crypto.secret(password));
|
||||
try!(new_store.insert_account(secret, new_password));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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> {
|
||||
let mut account = try!(self.get(address));
|
||||
let old = try!(self.get(address));
|
||||
let mut account = old.clone();
|
||||
account.name = name;
|
||||
|
||||
// save to file
|
||||
self.save(account)
|
||||
self.store.update(old, account)
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// save to file
|
||||
self.save(account)
|
||||
self.store.update(old, account)
|
||||
}
|
||||
|
||||
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> {
|
||||
@ -194,6 +153,288 @@ impl SecretStore for EthStore {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -50,8 +50,8 @@ mod secret_store;
|
||||
|
||||
pub use self::account::SafeAccount;
|
||||
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::presale::PresaleWallet;
|
||||
pub use self::secret_store::SecretStore;
|
||||
pub use self::random::random_phrase;
|
||||
pub use self::secret_store::{SimpleSecretStore, SecretStore};
|
||||
pub use self::random::{random_phrase, random_string};
|
||||
|
@ -51,10 +51,16 @@ pub fn random_phrase(words: usize) -> String {
|
||||
.map(|s| s.to_owned())
|
||||
.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(" ")
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
mod tests {
|
||||
use super::random_phrase;
|
||||
|
@ -18,18 +18,25 @@ use ethkey::{Address, Message, Signature, Secret, Public};
|
||||
use Error;
|
||||
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 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 remove_account(&self, account: &Address, password: &str) -> Result<(), 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 public(&self, account: &Address, password: &str) -> Result<Public, 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 name(&self, account: &Address) -> Result<String, Error>;
|
||||
fn meta(&self, account: &Address) -> Result<String, Error>;
|
||||
|
@ -19,7 +19,7 @@ extern crate ethstore;
|
||||
|
||||
mod util;
|
||||
|
||||
use ethstore::{SecretStore, EthStore};
|
||||
use ethstore::{EthStore, SimpleSecretStore};
|
||||
use ethstore::ethkey::{Random, Generator, Secret, KeyPair, verify_address};
|
||||
use ethstore::dir::DiskDirectory;
|
||||
use util::TransientDir;
|
||||
|
@ -18,7 +18,6 @@ use std::path::PathBuf;
|
||||
use std::{env, fs};
|
||||
use rand::{Rng, OsRng};
|
||||
use ethstore::dir::{KeyDirectory, DiskDirectory};
|
||||
use ethstore::ethkey::Address;
|
||||
use ethstore::{Error, SafeAccount};
|
||||
|
||||
pub fn random_dir() -> PathBuf {
|
||||
@ -64,11 +63,15 @@ impl KeyDirectory for TransientDir {
|
||||
self.dir.load()
|
||||
}
|
||||
|
||||
fn update(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
|
||||
self.dir.update(account)
|
||||
}
|
||||
|
||||
fn insert(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
|
||||
self.dir.insert(account)
|
||||
}
|
||||
|
||||
fn remove(&self, address: &Address) -> Result<(), Error> {
|
||||
self.dir.remove(address)
|
||||
fn remove(&self, account: &SafeAccount) -> Result<(), Error> {
|
||||
self.dir.remove(account)
|
||||
}
|
||||
}
|
||||
|
8
js/.stylelintrc.json
Normal file
8
js/.stylelintrc.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "stylelint-config-standard",
|
||||
"rules": {
|
||||
"selector-pseudo-class-no-unknown": [
|
||||
true, { "ignorePseudoClasses": ["global"] }
|
||||
]
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parity.js",
|
||||
"version": "0.2.119",
|
||||
"version": "0.2.127",
|
||||
"main": "release/index.js",
|
||||
"jsnext:main": "src/index.js",
|
||||
"author": "Parity Team <admin@parity.io>",
|
||||
@ -38,8 +38,11 @@
|
||||
"start:app": "node webpack/dev.server",
|
||||
"clean": "rm -rf ./build ./coverage",
|
||||
"coveralls": "npm run testCoverage && coveralls < coverage/lcov.info",
|
||||
"lint": "eslint --ignore-path .gitignore ./src/",
|
||||
"lint:cached": "eslint --cache --ignore-path .gitignore ./src/",
|
||||
"lint": "npm run lint:css && npm run lint:js",
|
||||
"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:coverage": "NODE_ENV=test istanbul cover _mocha -- 'src/**/*.spec.js'",
|
||||
"test:e2e": "NODE_ENV=test mocha 'src/**/*.e2e.js'",
|
||||
@ -118,6 +121,8 @@
|
||||
"sinon-as-promised": "4.0.2",
|
||||
"sinon-chai": "2.8.0",
|
||||
"style-loader": "0.13.1",
|
||||
"stylelint": "7.6.0",
|
||||
"stylelint-config-standard": "15.0.0",
|
||||
"url-loader": "0.5.7",
|
||||
"webpack": "2.1.0-beta.27",
|
||||
"webpack-dev-middleware": "1.8.4",
|
||||
|
33
js/src/3rdparty/email-verification/index.js
vendored
Normal file
33
js/src/3rdparty/email-verification/index.js
vendored
Normal 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');
|
||||
});
|
||||
});
|
||||
};
|
23
js/src/3rdparty/email-verification/terms-of-service.js
vendored
Normal file
23
js/src/3rdparty/email-verification/terms-of-service.js
vendored
Normal 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>
|
||||
);
|
11
js/src/3rdparty/sms-verification/index.js
vendored
11
js/src/3rdparty/sms-verification/index.js
vendored
@ -15,17 +15,6 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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. Twilio’s 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) => {
|
||||
const port = isTestnet ? 8443 : 443;
|
||||
|
27
js/src/3rdparty/sms-verification/terms-of-service.js
vendored
Normal file
27
js/src/3rdparty/sms-verification/terms-of-service.js
vendored
Normal 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. Twilio’s 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>
|
||||
);
|
@ -342,7 +342,8 @@ export default class Contract {
|
||||
options: _options,
|
||||
autoRemove,
|
||||
callback,
|
||||
filterId
|
||||
filterId,
|
||||
id: subscriptionId
|
||||
};
|
||||
|
||||
if (skipInitFetch) {
|
||||
@ -452,13 +453,13 @@ export default class Contract {
|
||||
})
|
||||
)
|
||||
.then((logsArray) => {
|
||||
logsArray.forEach((logs, subscriptionId) => {
|
||||
logsArray.forEach((logs, index) => {
|
||||
if (!logs || !logs.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.sendData(subscriptionId, null, this.parseEventLogs(logs));
|
||||
this._sendData(subscriptions[index].id, null, this.parseEventLogs(logs));
|
||||
} catch (error) {
|
||||
console.error('_sendSubscriptionChanges', error);
|
||||
}
|
||||
|
@ -1,9 +1,30 @@
|
||||
// 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';
|
||||
|
||||
// jsdom doesn't have all the browser features, blockies fail
|
||||
const TEST_ENV = process.env.NODE_ENV === 'test';
|
||||
|
||||
export function createIdentityImg (address, scale = 8) {
|
||||
return blockies({
|
||||
seed: (address || '').toLowerCase(),
|
||||
size: 8,
|
||||
scale
|
||||
}).toDataURL();
|
||||
return TEST_ENV
|
||||
? ''
|
||||
: blockies({
|
||||
seed: (address || '').toLowerCase(),
|
||||
size: 8,
|
||||
scale
|
||||
}).toDataURL();
|
||||
}
|
||||
|
1
js/src/contracts/abi/email-verification.json
Normal file
1
js/src/contracts/abi/email-verification.json
Normal 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"}]
|
@ -19,6 +19,7 @@ import basiccoin from './basiccoin.json';
|
||||
import basiccoinmanager from './basiccoinmanager.json';
|
||||
import dappreg from './dappreg.json';
|
||||
import eip20 from './eip20.json';
|
||||
import emailverification from './email-verification.json';
|
||||
import gavcoin from './gavcoin.json';
|
||||
import githubhint from './githubhint.json';
|
||||
import owned from './owned.json';
|
||||
@ -34,6 +35,7 @@ export {
|
||||
basiccoinmanager,
|
||||
dappreg,
|
||||
eip20,
|
||||
emailverification,
|
||||
gavcoin,
|
||||
githubhint,
|
||||
owned,
|
||||
|
@ -18,7 +18,8 @@ import { bytesToHex, hex2Ascii } from '~/api/util/format';
|
||||
|
||||
import ABI from './abi/certifier.json';
|
||||
|
||||
const ZERO = '0x0000000000000000000000000000000000000000000000000000000000000000';
|
||||
const ZERO20 = '0x0000000000000000000000000000000000000000';
|
||||
const ZERO32 = '0x0000000000000000000000000000000000000000000000000000000000000000';
|
||||
|
||||
export default class BadgeReg {
|
||||
constructor (api, registry) {
|
||||
@ -26,32 +27,57 @@ export default class BadgeReg {
|
||||
this._registry = registry;
|
||||
|
||||
registry.getContract('badgereg');
|
||||
this.certifiers = {}; // by name
|
||||
this.certifiers = []; // by id
|
||||
this.contracts = {}; // by name
|
||||
}
|
||||
|
||||
fetchCertifier (name) {
|
||||
if (this.certifiers[name]) {
|
||||
return Promise.resolve(this.certifiers[name]);
|
||||
certifierCount () {
|
||||
return this._registry.getContract('badgereg')
|
||||
.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')
|
||||
.then((badgeReg) => {
|
||||
return badgeReg.instance.fromName.call({}, [name])
|
||||
.then(([ id, address ]) => {
|
||||
return Promise.all([
|
||||
badgeReg.instance.meta.call({}, [id, 'TITLE']),
|
||||
badgeReg.instance.meta.call({}, [id, 'IMG'])
|
||||
])
|
||||
.then(([ title, img ]) => {
|
||||
title = bytesToHex(title);
|
||||
title = title === ZERO ? null : hex2Ascii(title);
|
||||
if (bytesToHex(img) === ZERO) img = null;
|
||||
return badgeReg.instance.badge.call({}, [ id ]);
|
||||
})
|
||||
.then(([ address, name ]) => {
|
||||
if (address === ZERO20) {
|
||||
throw new Error(`Certifier ${id} does not exist.`);
|
||||
}
|
||||
|
||||
const data = { address, name, title, icon: img };
|
||||
this.certifiers[name] = data;
|
||||
return data;
|
||||
});
|
||||
});
|
||||
name = bytesToHex(name);
|
||||
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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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 };
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ import Registry from './registry';
|
||||
import SignatureReg from './signaturereg';
|
||||
import TokenReg from './tokenreg';
|
||||
import GithubHint from './githubhint';
|
||||
import * as smsVerification from './sms-verification';
|
||||
import * as verification from './verification';
|
||||
import BadgeReg from './badgereg';
|
||||
|
||||
let instance = null;
|
||||
@ -58,7 +58,11 @@ export default class Contracts {
|
||||
}
|
||||
|
||||
get smsVerification () {
|
||||
return smsVerification;
|
||||
return verification;
|
||||
}
|
||||
|
||||
get emailVerification () {
|
||||
return verification;
|
||||
}
|
||||
|
||||
static create (api) {
|
||||
|
@ -14,4 +14,5 @@
|
||||
// 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 './SMSVerification';
|
||||
import './parity';
|
||||
import './web3';
|
@ -58,23 +58,23 @@ export default class DetailsStep extends Component {
|
||||
<Form>
|
||||
{ this.renderWarning() }
|
||||
<AddressSelect
|
||||
label='from account'
|
||||
hint='the account to transact with'
|
||||
value={ fromAddress }
|
||||
error={ fromAddressError }
|
||||
accounts={ accounts }
|
||||
balances={ balances }
|
||||
onChange={ onFromAddressChange } />
|
||||
error={ fromAddressError }
|
||||
hint='the account to transact with'
|
||||
label='from account'
|
||||
onChange={ onFromAddressChange }
|
||||
value={ fromAddress } />
|
||||
{ this.renderFunctionSelect() }
|
||||
{ this.renderParameters() }
|
||||
<div className={ styles.columns }>
|
||||
<div>
|
||||
<Input
|
||||
label='transaction value (in ETH)'
|
||||
hint='the amount to send to with the transaction'
|
||||
value={ amount }
|
||||
error={ amountError }
|
||||
onSubmit={ onAmountChange } />
|
||||
hint='the amount to send to with the transaction'
|
||||
label='transaction value (in ETH)'
|
||||
onSubmit={ onAmountChange }
|
||||
value={ amount } />
|
||||
</div>
|
||||
<div>
|
||||
<Checkbox
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -25,6 +25,7 @@ import ContentClear from 'material-ui/svg-icons/content/clear';
|
||||
import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
|
||||
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 { MAX_GAS_ESTIMATION } from '~/util/constants';
|
||||
import { validateAddress, validateUint } from '~/util/validation';
|
||||
@ -56,12 +57,12 @@ class ExecuteContract extends Component {
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
isTest: PropTypes.bool,
|
||||
fromAddress: PropTypes.string,
|
||||
accounts: PropTypes.object,
|
||||
balances: PropTypes.object,
|
||||
contract: PropTypes.object,
|
||||
contract: PropTypes.object.isRequired,
|
||||
fromAddress: PropTypes.string,
|
||||
gasLimit: PropTypes.object.isRequired,
|
||||
isTest: PropTypes.bool,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onFromAddressChange: PropTypes.func.isRequired
|
||||
}
|
||||
@ -77,11 +78,11 @@ class ExecuteContract extends Component {
|
||||
funcError: null,
|
||||
gasEdit: false,
|
||||
rejected: false,
|
||||
step: STEP_DETAILS,
|
||||
sending: false,
|
||||
step: STEP_DETAILS,
|
||||
txhash: null,
|
||||
values: [],
|
||||
valuesError: [],
|
||||
txhash: null
|
||||
valuesError: []
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
@ -255,10 +256,6 @@ class ExecuteContract extends Component {
|
||||
valueError = validateAddress(_value).addressError;
|
||||
break;
|
||||
|
||||
case 'bool':
|
||||
value = _value === 'true';
|
||||
break;
|
||||
|
||||
case 'uint':
|
||||
valueError = validateUint(_value).valueError;
|
||||
break;
|
||||
@ -278,13 +275,12 @@ class ExecuteContract extends Component {
|
||||
}
|
||||
|
||||
estimateGas = (_fromAddress) => {
|
||||
const { api } = this.context;
|
||||
const { fromAddress } = this.props;
|
||||
const { amount, func, values } = this.state;
|
||||
const options = {
|
||||
gas: MAX_GAS_ESTIMATION,
|
||||
from: _fromAddress || fromAddress,
|
||||
value: api.util.toWei(amount || 0)
|
||||
value: toWei(amount || 0)
|
||||
};
|
||||
|
||||
if (!func) {
|
||||
|
69
js/src/modals/ExecuteContract/executeContract.spec.js
Normal file
69
js/src/modals/ExecuteContract/executeContract.spec.js
Normal 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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
64
js/src/modals/ExecuteContract/executeContract.test.js
Normal file
64
js/src/modals/ExecuteContract/executeContract.test.js
Normal 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
|
||||
};
|
@ -15,6 +15,7 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import ActionDone from 'material-ui/svg-icons/action/done';
|
||||
import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
|
||||
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'];
|
||||
|
||||
export default class FirstRun extends Component {
|
||||
class FirstRun extends Component {
|
||||
static contextTypes = {
|
||||
api: PropTypes.object.isRequired,
|
||||
store: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
visible: PropTypes.bool,
|
||||
hasAccounts: PropTypes.bool.isRequired,
|
||||
visible: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
@ -109,6 +111,7 @@ export default class FirstRun extends Component {
|
||||
}
|
||||
|
||||
renderDialogActions () {
|
||||
const { hasAccounts } = this.props;
|
||||
const { canCreate, stage, hasAcceptedTnc } = this.state;
|
||||
|
||||
switch (stage) {
|
||||
@ -130,13 +133,26 @@ export default class FirstRun extends Component {
|
||||
);
|
||||
|
||||
case 2:
|
||||
return (
|
||||
const buttons = [
|
||||
<Button
|
||||
icon={ <ActionDone /> }
|
||||
label='Create'
|
||||
key='create'
|
||||
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:
|
||||
return [
|
||||
@ -219,6 +235,10 @@ export default class FirstRun extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
skipAccountCreation = () => {
|
||||
this.setState({ stage: this.state.stage + 2 });
|
||||
}
|
||||
|
||||
newError = (error) => {
|
||||
const { store } = this.context;
|
||||
|
||||
@ -232,3 +252,9 @@ export default class FirstRun extends Component {
|
||||
print(recoveryPage({ phrase, name, identity, address, logo: ParityLogo }));
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return { hasAccounts: state.personal.hasAccounts };
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(FirstRun);
|
||||
|
@ -15,10 +15,6 @@
|
||||
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.list li {
|
||||
padding: .1em 0;
|
||||
}
|
||||
|
||||
.spacing {
|
||||
margin-top: 1.5em;
|
||||
}
|
@ -25,41 +25,33 @@ import { fromWei } from '~/api/util/wei';
|
||||
import { Form, Input } from '~/ui';
|
||||
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';
|
||||
|
||||
export default class GatherData extends Component {
|
||||
static propTypes = {
|
||||
fee: React.PropTypes.instanceOf(BigNumber),
|
||||
isNumberValid: PropTypes.bool.isRequired,
|
||||
method: PropTypes.string.isRequired,
|
||||
fields: PropTypes.array.isRequired,
|
||||
isVerified: nullableProptype(PropTypes.bool.isRequired),
|
||||
hasRequested: nullableProptype(PropTypes.bool.isRequired),
|
||||
setNumber: PropTypes.func.isRequired,
|
||||
setConsentGiven: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isNumberValid, isVerified } = this.props;
|
||||
const { method, isVerified } = this.props;
|
||||
const termsOfService = method === 'email' ? emailTermsOfService : smsTermsOfService;
|
||||
const howItWorks = method === 'email' ? howEmailVerificationWorks : howSMSVerificationWorks;
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<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>
|
||||
{ howItWorks }
|
||||
{ this.renderFee() }
|
||||
{ this.renderCertified() }
|
||||
{ this.renderRequested() }
|
||||
<Input
|
||||
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 }
|
||||
/>
|
||||
{ this.renderFields() }
|
||||
<Checkbox
|
||||
className={ styles.spacing }
|
||||
label={ 'I agree to the terms and conditions below.' }
|
||||
@ -136,12 +128,28 @@ export default class GatherData extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
numberOnSubmit = (value) => {
|
||||
this.props.setNumber(value);
|
||||
}
|
||||
renderFields () {
|
||||
const { isVerified, fields } = this.props;
|
||||
|
||||
numberOnChange = (_, value) => {
|
||||
this.props.setNumber(value);
|
||||
const rendered = fields.map((field) => {
|
||||
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) => {
|
@ -20,20 +20,25 @@ import { Form, Input } from '~/ui';
|
||||
|
||||
export default class QueryCode extends Component {
|
||||
static propTypes = {
|
||||
number: PropTypes.string.isRequired,
|
||||
receiver: PropTypes.string.isRequired,
|
||||
hint: PropTypes.string,
|
||||
isCodeValid: PropTypes.bool.isRequired,
|
||||
setCode: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
hint: 'Enter the code you received.'
|
||||
}
|
||||
|
||||
render () {
|
||||
const { number, isCodeValid } = this.props;
|
||||
const { receiver, hint, isCodeValid } = this.props;
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<p>The verification code has been sent to { number }.</p>
|
||||
<p>The verification code has been sent to { receiver }.</p>
|
||||
<Input
|
||||
label={ 'verification code' }
|
||||
hint={ 'Enter the code you received via SMS.' }
|
||||
hint={ hint }
|
||||
error={ isCodeValid ? null : 'invalid code' }
|
||||
onChange={ this.onChange }
|
||||
onSubmit={ this.onSubmit }
|
@ -19,7 +19,7 @@ import React, { Component, PropTypes } from 'react';
|
||||
import { nullableProptype } from '~/util/proptypes';
|
||||
import TxHash from '~/ui/TxHash';
|
||||
import {
|
||||
POSTING_REQUEST, POSTED_REQUEST, REQUESTING_SMS
|
||||
POSTING_REQUEST, POSTED_REQUEST, REQUESTING_CODE
|
||||
} from '../store';
|
||||
|
||||
import styles from './sendRequest.css';
|
||||
@ -45,9 +45,9 @@ export default class SendRequest extends Component {
|
||||
</div>
|
||||
);
|
||||
|
||||
case REQUESTING_SMS:
|
||||
case REQUESTING_CODE:
|
||||
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:
|
72
js/src/modals/Verification/email-store.js
Normal file
72
js/src/modals/Verification/email-store.js
Normal 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);
|
||||
}
|
||||
}
|
41
js/src/modals/Verification/how-it-works.js
Normal file
41
js/src/modals/Verification/how-it-works.js
Normal 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>
|
||||
);
|
17
js/src/modals/Verification/index.js
Normal file
17
js/src/modals/Verification/index.js
Normal 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';
|
69
js/src/modals/Verification/sms-store.js
Normal file
69
js/src/modals/Verification/sms-store.js
Normal 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);
|
||||
}
|
||||
}
|
@ -14,21 +14,19 @@
|
||||
// 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, autorun, action } from 'mobx';
|
||||
import phone from 'phoneformat.js';
|
||||
import { observable, autorun, action } from 'mobx';
|
||||
import { sha3 } from '~/api/util/sha3';
|
||||
|
||||
import Contract from '~/api/contract';
|
||||
import Contracts from '~/contracts';
|
||||
|
||||
import { checkIfVerified, checkIfRequested, awaitPuzzle } from '~/contracts/sms-verification';
|
||||
import { postToServer } from '~/3rdparty/sms-verification';
|
||||
import { checkIfVerified, checkIfRequested, awaitPuzzle } from '~/contracts/verification';
|
||||
import { checkIfTxFailed, waitForConfirmations } from '~/util/tx';
|
||||
|
||||
export const LOADING = 'fetching-contract';
|
||||
export const QUERY_DATA = 'query-data';
|
||||
export const POSTING_REQUEST = 'posting-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 POSTING_CONFIRMATION = 'posting-confirmation';
|
||||
export const POSTED_CONFIRMATION = 'posted-confirmation';
|
||||
@ -43,56 +41,30 @@ export default class VerificationStore {
|
||||
@observable isVerified = null;
|
||||
@observable hasRequested = null;
|
||||
@observable consentGiven = false;
|
||||
@observable number = '';
|
||||
@observable requestTx = null;
|
||||
@observable code = '';
|
||||
@observable isCodeValid = null;
|
||||
@observable confirmationTx = null;
|
||||
|
||||
@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) {
|
||||
constructor (api, abi, certifierId, account, isTestnet) {
|
||||
this.api = api;
|
||||
this.account = account;
|
||||
this.isTestnet = isTestnet;
|
||||
|
||||
this.step = LOADING;
|
||||
Contracts.create(api).registry.getContract('smsverification')
|
||||
.then((contract) => {
|
||||
this.contract = contract;
|
||||
Contracts.get().badgeReg.fetchCertifier(certifierId)
|
||||
.then(({ address }) => {
|
||||
this.contract = new Contract(api, abi).at(address);
|
||||
this.load();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('error', err);
|
||||
this.error = 'Failed to fetch the contract: ' + err.message;
|
||||
});
|
||||
|
||||
autorun(() => {
|
||||
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) => {
|
||||
this.consentGiven = consentGiven;
|
||||
}
|
||||
@ -166,19 +134,22 @@ export default class VerificationStore {
|
||||
});
|
||||
}
|
||||
|
||||
requestValues = () => []
|
||||
|
||||
@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 options = { from: account, value: fee.toString() };
|
||||
const values = this.requestValues();
|
||||
|
||||
let chain = Promise.resolve();
|
||||
if (!hasRequested) {
|
||||
this.step = POSTING_REQUEST;
|
||||
chain = request.estimateGas(options, [])
|
||||
chain = request.estimateGas(options, values)
|
||||
.then((gas) => {
|
||||
options.gas = gas.mul(1.2).toFixed(0);
|
||||
return request.postTransaction(options, []);
|
||||
return request.postTransaction(options, values);
|
||||
})
|
||||
.then((handle) => {
|
||||
// TODO: The "request rejected" error doesn't have any property to
|
||||
@ -200,18 +171,15 @@ export default class VerificationStore {
|
||||
|
||||
chain
|
||||
.then(() => {
|
||||
return api.parity.netChain();
|
||||
})
|
||||
.then((chain) => {
|
||||
this.step = REQUESTING_SMS;
|
||||
return postToServer({ number, address: account }, this.isTestnet);
|
||||
this.step = REQUESTING_CODE;
|
||||
return this.requestCode();
|
||||
})
|
||||
.then(() => awaitPuzzle(api, contract, account))
|
||||
.then(() => {
|
||||
this.step = QUERY_CODE;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.error = 'Failed to request a confirmation SMS: ' + err.message;
|
||||
this.error = 'Failed to request a confirmation code: ' + err.message;
|
||||
});
|
||||
}
|
||||
|
25
js/src/modals/Verification/verification.css
Normal file
25
js/src/modals/Verification/verification.css
Normal 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;
|
||||
}
|
@ -20,12 +20,27 @@ import DoneIcon from 'material-ui/svg-icons/action/done-all';
|
||||
import CancelIcon from 'material-ui/svg-icons/content/clear';
|
||||
|
||||
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 {
|
||||
LOADING,
|
||||
QUERY_DATA,
|
||||
POSTING_REQUEST, POSTED_REQUEST,
|
||||
REQUESTING_SMS, QUERY_CODE,
|
||||
REQUESTING_CODE, QUERY_CODE,
|
||||
POSTING_CONFIRMATION, POSTED_CONFIRMATION,
|
||||
DONE
|
||||
} from './store';
|
||||
@ -37,34 +52,44 @@ import SendConfirmation from './SendConfirmation';
|
||||
import Done from './Done';
|
||||
|
||||
@observer
|
||||
export default class SMSVerification extends Component {
|
||||
export default class Verification extends Component {
|
||||
static propTypes = {
|
||||
store: PropTypes.any.isRequired,
|
||||
store: nullableProptype(PropTypes.object.isRequired),
|
||||
account: PropTypes.string.isRequired,
|
||||
onSelectMethod: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
static phases = { // mapping (store steps -> steps)
|
||||
[LOADING]: 0,
|
||||
[QUERY_DATA]: 1,
|
||||
[POSTING_REQUEST]: 2, [POSTED_REQUEST]: 2, [REQUESTING_SMS]: 2,
|
||||
[LOADING]: 1, [QUERY_DATA]: 1,
|
||||
[POSTING_REQUEST]: 2, [POSTED_REQUEST]: 2, [REQUESTING_CODE]: 2,
|
||||
[QUERY_CODE]: 3,
|
||||
[POSTING_CONFIRMATION]: 4, [POSTED_CONFIRMATION]: 4,
|
||||
[DONE]: 5
|
||||
}
|
||||
|
||||
state = {
|
||||
method: 'sms'
|
||||
};
|
||||
|
||||
render () {
|
||||
const phase = SMSVerification.phases[this.props.store.step];
|
||||
const { error, isStepValid } = this.props.store;
|
||||
const { store } = this.props;
|
||||
let phase = 0; let error = false; let isStepValid = true;
|
||||
|
||||
if (store) {
|
||||
phase = Verification.phases[store.step];
|
||||
error = store.error;
|
||||
isStepValid = store.isStepValid;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
actions={ this.renderDialogActions(phase, error, isStepValid) }
|
||||
title='verify your account via SMS'
|
||||
title='verify your account'
|
||||
visible
|
||||
current={ phase }
|
||||
steps={ ['Prepare', 'Enter Data', 'Request', 'Enter Code', 'Confirm', 'Done!'] }
|
||||
waiting={ error ? [] : [ 0, 2, 4 ] }
|
||||
steps={ ['Method', 'Enter Data', 'Request', 'Enter Code', 'Confirm', 'Done!'] }
|
||||
waiting={ error ? [] : [ 2, 4 ] }
|
||||
>
|
||||
{ this.renderStep(phase, error) }
|
||||
</Modal>
|
||||
@ -101,6 +126,13 @@ export default class SMSVerification extends Component {
|
||||
|
||||
let action = () => {};
|
||||
switch (phase) {
|
||||
case 0:
|
||||
action = () => {
|
||||
const { onSelectMethod } = this.props;
|
||||
const { method } = this.state;
|
||||
onSelectMethod(method);
|
||||
};
|
||||
break;
|
||||
case 1:
|
||||
action = store.sendRequest;
|
||||
break;
|
||||
@ -133,26 +165,58 @@ export default class SMSVerification extends Component {
|
||||
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 {
|
||||
step,
|
||||
fee, number, isNumberValid, isVerified, hasRequested,
|
||||
fee, isVerified, hasRequested,
|
||||
requestTx, isCodeValid, confirmationTx,
|
||||
setCode
|
||||
} = this.props.store;
|
||||
|
||||
switch (phase) {
|
||||
case 0:
|
||||
return (
|
||||
<p>Loading SMS Verification.</p>
|
||||
);
|
||||
|
||||
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 (
|
||||
<GatherData
|
||||
fee={ fee } isNumberValid={ isNumberValid }
|
||||
isVerified={ isVerified } hasRequested={ hasRequested }
|
||||
setNumber={ setNumber } setConsentGiven={ setConsentGiven }
|
||||
method={ method } fields={ fields }
|
||||
fee={ fee } isVerified={ isVerified } hasRequested={ hasRequested }
|
||||
setConsentGiven={ setConsentGiven }
|
||||
/>
|
||||
);
|
||||
|
||||
@ -162,9 +226,19 @@ export default class SMSVerification extends Component {
|
||||
);
|
||||
|
||||
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 (
|
||||
<QueryCode
|
||||
number={ number } fee={ fee } isCodeValid={ isCodeValid }
|
||||
receiver={ receiver }
|
||||
hint={ hint }
|
||||
isCodeValid={ isCodeValid }
|
||||
setCode={ setCode }
|
||||
/>
|
||||
);
|
||||
@ -183,4 +257,8 @@ export default class SMSVerification extends Component {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
selectMethod = (choice, i) => {
|
||||
this.setState({ method: choice.value });
|
||||
}
|
||||
}
|
@ -24,7 +24,7 @@ import EditMeta from './EditMeta';
|
||||
import ExecuteContract from './ExecuteContract';
|
||||
import FirstRun from './FirstRun';
|
||||
import Shapeshift from './Shapeshift';
|
||||
import SMSVerification from './SMSVerification';
|
||||
import Verification from './Verification';
|
||||
import Transfer from './Transfer';
|
||||
import PasswordManager from './PasswordManager';
|
||||
import SaveContract from './SaveContract';
|
||||
@ -42,7 +42,7 @@ export {
|
||||
ExecuteContract,
|
||||
FirstRun,
|
||||
Shapeshift,
|
||||
SMSVerification,
|
||||
Verification,
|
||||
Transfer,
|
||||
PasswordManager,
|
||||
LoadContract,
|
||||
|
@ -14,10 +14,18 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
export const fetchCertifiers = () => ({
|
||||
type: 'fetchCertifiers'
|
||||
});
|
||||
|
||||
export const fetchCertifications = (address) => ({
|
||||
type: 'fetchCertifications', address
|
||||
});
|
||||
|
||||
export const addCertification = (address, name, title, icon) => ({
|
||||
type: 'addCertification', address, name, title, icon
|
||||
export const addCertification = (address, id, name, title, icon) => ({
|
||||
type: 'addCertification', address, id, name, title, icon
|
||||
});
|
||||
|
||||
export const removeCertification = (address, id) => ({
|
||||
type: 'removeCertification', address, id
|
||||
});
|
||||
|
@ -14,38 +14,90 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import Contracts from '~/contracts';
|
||||
import { addCertification } from './actions';
|
||||
import { uniq } from 'lodash';
|
||||
|
||||
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 {
|
||||
toMiddleware () {
|
||||
return (store) => (next) => (action) => {
|
||||
if (action.type !== 'fetchCertifications') {
|
||||
return next(action);
|
||||
}
|
||||
const api = Contracts.get()._api;
|
||||
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');
|
||||
|
||||
const { address } = action;
|
||||
const badgeReg = Contracts.get().badgeReg;
|
||||
let certifiers = [];
|
||||
let accounts = []; // these are addresses
|
||||
|
||||
knownCertifiers.forEach((name) => {
|
||||
badgeReg.fetchCertifier(name)
|
||||
.then((cert) => {
|
||||
return badgeReg.checkIfCertified(cert.address, address)
|
||||
.then((isCertified) => {
|
||||
if (isCertified) {
|
||||
const { name, title, icon } = cert;
|
||||
store.dispatch(addCertification(address, name, title, icon));
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err) {
|
||||
console.error(`Failed to check if ${address} certified by ${name}:`, err);
|
||||
const fetchConfirmedEvents = (dispatch) => {
|
||||
if (certifiers.length === 0 || accounts.length === 0) return;
|
||||
api.eth.getLogs({
|
||||
fromBlock: 0,
|
||||
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) => {
|
||||
console.error('Failed to fetch Confirmed events:', 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -17,17 +17,27 @@
|
||||
const initialState = {};
|
||||
|
||||
export default (state = initialState, action) => {
|
||||
if (action.type !== 'addCertification') {
|
||||
return state;
|
||||
if (action.type === 'addCertification') {
|
||||
const { address, id, name, icon, title } = action;
|
||||
const certifications = state[address] || [];
|
||||
|
||||
if (certifications.some((c) => c.id === id)) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const newCertifications = certifications.concat({
|
||||
id, name, icon, title
|
||||
});
|
||||
return { ...state, [address]: newCertifications };
|
||||
}
|
||||
|
||||
const { address, name, icon, title } = action;
|
||||
const certifications = state[address] || [];
|
||||
if (action.type === 'removeCertification') {
|
||||
const { address, id } = action;
|
||||
const certifications = state[address] || [];
|
||||
|
||||
if (certifications.some((c) => c.name === name)) {
|
||||
return state;
|
||||
const newCertifications = certifications.filter((c) => c.id !== id);
|
||||
return { ...state, [address]: newCertifications };
|
||||
}
|
||||
const newCertifications = certifications.concat({ name, icon, title });
|
||||
|
||||
return { ...state, [address]: newCertifications };
|
||||
return state;
|
||||
};
|
||||
|
@ -16,10 +16,8 @@
|
||||
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
|
||||
import { hashToImageUrl } from '~/redux/providers/imagesReducer';
|
||||
import { fetchCertifications } from '~/redux/providers/certifications/actions';
|
||||
|
||||
import defaultIcon from '../../../assets/images/certifications/unknown.svg';
|
||||
|
||||
@ -29,14 +27,7 @@ class Certifications extends Component {
|
||||
static propTypes = {
|
||||
account: PropTypes.string.isRequired,
|
||||
certifications: PropTypes.array.isRequired,
|
||||
dappsUrl: PropTypes.string.isRequired,
|
||||
|
||||
fetchCertifications: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
const { account, fetchCertifications } = this.props;
|
||||
fetchCertifications(account);
|
||||
dappsUrl: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
render () {
|
||||
@ -73,15 +64,13 @@ function mapStateToProps (_, initProps) {
|
||||
|
||||
return (state) => {
|
||||
const certifications = state.certifications[account] || [];
|
||||
return { certifications };
|
||||
};
|
||||
}
|
||||
const dappsUrl = state.api.dappsUrl;
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return bindActionCreators({ fetchCertifications }, dispatch);
|
||||
return { certifications, dappsUrl };
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
null
|
||||
)(Certifications);
|
||||
|
@ -46,26 +46,26 @@ export default class Select extends Component {
|
||||
}
|
||||
|
||||
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 (
|
||||
<SelectField
|
||||
className={ className }
|
||||
autoComplete='off'
|
||||
className={ className }
|
||||
disabled={ disabled }
|
||||
errorText={ error }
|
||||
floatingLabelFixed
|
||||
floatingLabelText={ label }
|
||||
fullWidth
|
||||
hintText={ hint }
|
||||
name={ NAME_ID }
|
||||
id={ NAME_ID }
|
||||
underlineDisabledStyle={ UNDERLINE_DISABLED }
|
||||
underlineStyle={ UNDERLINE_NORMAL }
|
||||
value={ value }
|
||||
name={ NAME_ID }
|
||||
onBlur={ onBlur }
|
||||
onChange={ onChange }
|
||||
onKeyDown={ onKeyDown }>
|
||||
onKeyDown={ onKeyDown }
|
||||
underlineDisabledStyle={ UNDERLINE_DISABLED }
|
||||
underlineStyle={ UNDERLINE_NORMAL }
|
||||
value={ value }>
|
||||
{ children }
|
||||
</SelectField>
|
||||
);
|
||||
|
@ -289,9 +289,8 @@ export default class TypedInput extends Component {
|
||||
return (
|
||||
<MenuItem
|
||||
key={ bool }
|
||||
value={ bool }
|
||||
label={ bool }
|
||||
>
|
||||
value={ bool }>
|
||||
{ bool }
|
||||
</MenuItem>
|
||||
);
|
||||
@ -299,19 +298,23 @@ export default class TypedInput extends Component {
|
||||
|
||||
return (
|
||||
<Select
|
||||
label={ label }
|
||||
hint={ hint }
|
||||
value={ value ? 'true' : 'false' }
|
||||
error={ error }
|
||||
hint={ hint }
|
||||
label={ label }
|
||||
onChange={ this.onChangeBool }
|
||||
>
|
||||
value={
|
||||
value
|
||||
? 'true'
|
||||
: 'false'
|
||||
}>
|
||||
{ boolitems }
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user