diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 84d10e1bc..28604d0cd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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: diff --git a/Cargo.lock b/Cargo.lock index e76a6749b..0caea9b8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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)", ] @@ -500,7 +514,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)", @@ -597,7 +611,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 +673,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 +702,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 +743,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 +867,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]] @@ -858,13 +880,25 @@ name = "itoa" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "jsonrpc-core" +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)", + "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-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.5 (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)", @@ -925,7 +959,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)", ] @@ -1026,7 +1060,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]] @@ -1042,7 +1076,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]] @@ -1057,7 +1091,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]] @@ -1073,7 +1107,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]] @@ -1083,7 +1117,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)", ] @@ -1094,7 +1128,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]] @@ -1123,7 +1157,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)", ] @@ -1262,6 +1296,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" @@ -1281,6 +1324,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" @@ -1300,14 +1363,25 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#175003ae159b126302fd1a90dd875dc86d7adba0" +source = "git+https://github.com/ethcore/js-precompiled.git#ad6617a73dbb17c53dddc0fc567e70ea5b8e882f" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "parking_lot" -version = "0.3.5" +version = "0.2.8" +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)", + "smallvec 0.1.8 (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.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)", @@ -1323,7 +1397,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]] @@ -1546,7 +1620,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]] @@ -1650,6 +1747,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" @@ -1779,13 +1885,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]] @@ -1794,7 +1908,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]] @@ -1829,7 +1943,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]] @@ -1925,7 +2039,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)", ] @@ -1956,7 +2070,7 @@ dependencies = [ [[package]] name = "winapi" -version = "0.2.6" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -1984,10 +2098,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" @@ -2018,6 +2137,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" @@ -2048,6 +2168,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" @@ -2061,6 +2182,7 @@ 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)" = "" "checksum jsonrpc-http-server 6.1.1 (git+https://github.com/ethcore/jsonrpc.git)" = "" "checksum jsonrpc-ipc-server 0.2.4 (git+https://github.com/ethcore/jsonrpc.git)" = "" @@ -2104,10 +2226,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)" = "" -"checksum parking_lot 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "dbc5847584161f273e69edc63c1a86254a22f570a0b5dd87aa6f9773f6f7d125" +"checksum parking_lot 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "968f685642555d2f7e202c48b8b11de80569e9bfea817f7f12d7c61aac62d4e6" +"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" @@ -2134,6 +2258,7 @@ dependencies = [ "checksum rocksdb-sys 0.3.0 (git+https://github.com/ethcore/rust-rocksdb)" = "" "checksum rotor 0.6.3 (git+https://github.com/ethcore/rotor)" = "" "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" @@ -2145,6 +2270,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)" = "" @@ -2162,6 +2288,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" @@ -2185,10 +2312,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)" = "" "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" diff --git a/Cargo.toml b/Cargo.toml index bce959f61..2d16e0bd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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] diff --git a/ethcore/res/authority_round.json b/ethcore/res/authority_round.json index 85beb51b4..efc0cdeb4 100644 --- a/ethcore/res/authority_round.json +++ b/ethcore/res/authority_round.json @@ -21,8 +21,9 @@ }, "genesis": { "seal": { - "generic": { - "rlp": "0xc28080" + "authority_round": { + "step": "0x0", + "signature": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" } }, "difficulty": "0x20000", diff --git a/ethcore/res/basic_authority.json b/ethcore/res/basic_authority.json index 51276d487..db4374160 100644 --- a/ethcore/res/basic_authority.json +++ b/ethcore/res/basic_authority.json @@ -17,10 +17,7 @@ }, "genesis": { "seal": { - "generic": { - "fields": 1, - "rlp": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa" - } + "generic": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa" }, "difficulty": "0x20000", "author": "0x0000000000000000000000000000000000000000", diff --git a/ethcore/res/ethereum/classic.json b/ethcore/res/ethereum/classic.json index a08dde33b..223978ca4 100644 --- a/ethcore/res/ethereum/classic.json +++ b/ethcore/res/ethereum/classic.json @@ -1,6 +1,6 @@ { "name": "Ethereum Classic", - "forkName": "classic", + "dataDir": "classic", "engine": { "Ethash": { "params": { diff --git a/ethcore/res/ethereum/expanse.json b/ethcore/res/ethereum/expanse.json index 8d580b6f5..d8cbd5b0c 100644 --- a/ethcore/res/ethereum/expanse.json +++ b/ethcore/res/ethereum/expanse.json @@ -1,6 +1,6 @@ { "name": "Expanse", - "forkName": "expanse", + "dataDir": "expanse", "engine": { "Ethash": { "params": { diff --git a/ethcore/res/ethereum/frontier.json b/ethcore/res/ethereum/frontier.json index 3a9dce456..91a8ae9e6 100644 --- a/ethcore/res/ethereum/frontier.json +++ b/ethcore/res/ethereum/frontier.json @@ -1,5 +1,6 @@ { "name": "Frontier/Homestead", + "dataDir": "ethereum", "engine": { "Ethash": { "params": { diff --git a/ethcore/res/ethereum/morden.json b/ethcore/res/ethereum/morden.json index d24e0ec0c..d21756250 100644 --- a/ethcore/res/ethereum/morden.json +++ b/ethcore/res/ethereum/morden.json @@ -1,5 +1,6 @@ { "name": "Morden", + "dataDir": "test", "engine": { "Ethash": { "params": { diff --git a/ethcore/res/ethereum/ropsten.json b/ethcore/res/ethereum/ropsten.json index 62282801d..d388ce9a1 100644 --- a/ethcore/res/ethereum/ropsten.json +++ b/ethcore/res/ethereum/ropsten.json @@ -1,5 +1,6 @@ { "name": "Ropsten", + "dataDir": "test", "engine": { "Ethash": { "params": { diff --git a/ethcore/res/instant_seal.json b/ethcore/res/instant_seal.json index fbb650102..6a3964e89 100644 --- a/ethcore/res/instant_seal.json +++ b/ethcore/res/instant_seal.json @@ -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" } } } diff --git a/ethcore/res/tendermint.json b/ethcore/res/tendermint.json new file mode 100644 index 000000000..778757107 --- /dev/null +++ b/ethcore/res/tendermint.json @@ -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" } + } +} diff --git a/ethcore/src/account_provider/mod.rs b/ethcore/src/account_provider/mod.rs index 1175f2d02..dab19dbc0 100644 --- a/ethcore/src/account_provider/mod.rs +++ b/ethcore/src/account_provider/mod.rs @@ -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 for Error { } } -#[derive(Default)] -struct NullDir { - accounts: RwLock>, -} - -impl KeyDirectory for NullDir { - fn load(&self) -> Result, SSError> { - Ok(self.accounts.read().values().cloned().collect()) - } - - fn insert(&self, account: SafeAccount) -> Result { - 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>, - sstore: Box, + unlocked: RwLock>, address_book: RwLock, dapps_settings: RwLock, + /// Accounts on disk + sstore: Box, + /// Accounts unlocked with rolling tokens + transient_sstore: EthMultiStore, } impl AccountProvider { /// Creates new account provider. pub fn new(sstore: Box) -> 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>) -> 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>, 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, 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, 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 { - 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 { - 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, 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, shared_mac: &[u8], message: &[u8]) -> Result, 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()]); + } } diff --git a/ethcore/src/account_provider/stores.rs b/ethcore/src/account_provider/stores.rs index d7e96243c..d4f2093ee 100644 --- a/ethcore/src/account_provider/stores.rs +++ b/ethcore/src/account_provider/stores.rs @@ -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 for JsonSettings { } } +/// Dapps user settings +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum NewDappsPolicy { + AllAccounts, + Whitelist(Vec
), +} + +impl From 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 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, + /// Dapps Settings + settings: DiskMap, + /// New Dapps Policy + policy: DiskMap, + /// Recently Accessed Dapps (transient) + recent: VecDeque, } 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 { - self.cache.clone() + pub fn settings(&self) -> HashMap { + 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 { + 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
) { { - 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 DiskMap { #[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()]) + ]); + } } diff --git a/ethcore/src/client/chain_notify.rs b/ethcore/src/client/chain_notify.rs index a9f8d0204..a6dbbeacc 100644 --- a/ethcore/src/client/chain_notify.rs +++ b/ethcore/src/client/chain_notify.rs @@ -15,7 +15,7 @@ // along with Parity. If not, see . 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, _retracted: Vec, _sealed: Vec, + // Block bytes. + _proposed: Vec, _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) {} + /// fires when new transactions are received from a peer fn transactions_received(&self, _hashes: Vec, diff --git a/ethcore/src/client/client.rs b/ethcore/src/client/client.rs index 39f7420f8..64ba8d756 100644 --- a/ethcore/src/client/client.rs +++ b/ethcore/src/client/client.rs @@ -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) { + *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 { 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) { + 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 { diff --git a/ethcore/src/client/test_client.rs b/ethcore/src/client/test_client.rs index f730c7d9c..99134f597 100644 --- a/ethcore/src/client/test_client.rs +++ b/ethcore/src/client/test_client.rs @@ -90,6 +90,8 @@ pub struct TestBlockChainClient { pub ancient_block: RwLock>, /// First block info. pub first_block: RwLock>, + /// Traces to return + pub traces: RwLock>>, } /// 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) { + 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> { - unimplemented!(); + self.traces.read().clone() } fn trace(&self, _trace: TraceId) -> Option { - unimplemented!(); + self.traces.read().clone().and_then(|vec| vec.into_iter().next()) } fn transaction_traces(&self, _trace: TransactionId) -> Option> { - unimplemented!(); + self.traces.read().clone() } fn block_traces(&self, _trace: BlockId) -> Option> { - unimplemented!(); + self.traces.read().clone() } fn queue_transactions(&self, transactions: Vec, _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 { self.miner.pending_transactions(self.chain_info().best_block_number) } diff --git a/ethcore/src/client/traits.rs b/ethcore/src/client/traits.rs index 7afe96f57..c91dc0347 100644 --- a/ethcore/src/client/traits.rs +++ b/ethcore/src/client/traits.rs @@ -202,6 +202,12 @@ pub trait BlockChainClient : Sync + Send { /// Queue transactions for importing. fn queue_transactions(&self, transactions: Vec, 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; @@ -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); + + /// 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; diff --git a/ethcore/src/engines/authority_round.rs b/ethcore/src/engines/authority_round.rs index 9f78d8cec..8d1c004c5 100644 --- a/ethcore/src/engines/authority_round.rs +++ b/ethcore/src/engines/authority_round.rs @@ -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> { - 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); } } diff --git a/ethcore/src/engines/basic_authority.rs b/ethcore/src/engines/basic_authority.rs index 5676365da..37ac4066b 100644 --- a/ethcore/src/engines/basic_authority.rs +++ b/ethcore/src/engines/basic_authority.rs @@ -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> { + 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] diff --git a/ethcore/src/engines/instant_seal.rs b/ethcore/src/engines/instant_seal.rs index f15ccba81..74f71168c 100644 --- a/ethcore/src/engines/instant_seal.rs +++ b/ethcore/src/engines/instant_seal.rs @@ -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 { Some(true) } - fn generate_seal(&self, _block: &ExecutedBlock) -> Option> { - 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] diff --git a/ethcore/src/engines/mod.rs b/ethcore/src/engines/mod.rs index 5e5c5530f..a3e57dd65 100644 --- a/ethcore/src/engines/mod.rs +++ b/ethcore/src/engines/mod.rs @@ -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
), + /// Message was not expected. + UnexpectedMessage, + /// Seal field has an unexpected size. + BadSealFieldSize(OutOfBounds), +} + +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), + /// Regular block seal; should be part of the blockchain. + Regular(Vec), + /// 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> { 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) {} diff --git a/ethcore/src/engines/tendermint/message.rs b/ethcore/src/engines/tendermint/message.rs new file mode 100644 index 000000000..3e5da592d --- /dev/null +++ b/ethcore/src/engines/tendermint/message.rs @@ -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 . + +//! 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, +} + + +fn consensus_round(header: &Header) -> Result { + 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) -> Self { + ConsensusMessage { + signature: signature, + height: height, + round: round, + step: step, + block_hash: block_hash, + } + } + + pub fn new_proposal(header: &Header) -> Result { + 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) -> 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 { + 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 { + 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(decoder: &D) -> Result 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(decoder: &D) -> Result 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) -> 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()); + } +} diff --git a/ethcore/src/engines/tendermint/mod.rs b/ethcore/src/engines/tendermint/mod.rs new file mode 100644 index 000000000..bb6d54ca5 --- /dev/null +++ b/ethcore/src/engines/tendermint/mod.rs @@ -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 . + +/// 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, + step_service: IoService, + /// Address to be used as authority. + authority: RwLock
, + /// Password used for signing messages. + password: RwLock>, + /// Blockchain height. + height: AtomicUsize, + /// Consensus round. + round: AtomicUsize, + /// Consensus step. + step: RwLock, + /// Vote accumulator. + votes: VoteCollector, + /// Channel for updating the sealing. + message_channel: Mutex>>, + /// Used to sign messages and proposals. + account_provider: Mutex>>, + /// Message for the last PoLC. + lock_change: RwLock>, + /// Last lock round. + last_lock: AtomicUsize, + /// Bare hash of the proposed block, used for seal submission. + proposal: RwLock>, +} + +impl Tendermint { + /// Create a new instance of Tendermint engine + pub fn new(params: CommonParams, our_params: TendermintParams, builtins: BTreeMap) -> Result, Error> { + let engine = Arc::new( + Tendermint { + params: params, + our_params: our_params, + builtins: builtins, + step_service: try!(IoService::::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) { + 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) -> Option { + 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) { + 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 { &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 { + 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 { + 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) { + trace!(target: "poa", "Register the IoChannel."); + *self.message_channel.lock() = Some(message_channel); + } + + fn register_account_provider(&self, account_provider: Arc) { + *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) { + 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) { + 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(engine: &Arc, signer: F, height: usize, round: usize, step: Step, block_hash: Option) -> Bytes where F: FnOnce(H256) -> Result { + 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, header: &Header, round: Round) -> Vec { + 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, height: Height, round: Round, bare_hash: Option, 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, 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, engine: &Arc, acc: &str) -> Address { + let addr = insert_and_unlock(tap, acc); + engine.set_signer(addr.clone(), acc.into()); + addr + } + + struct TestIo { + received: RwLock> + } + + impl TestIo { + fn new() -> Arc { Arc::new(TestIo { received: RwLock::new(Vec::new()) }) } + } + + impl IoHandler for TestIo { + fn message(&self, _io: &IoContext, 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>))); + 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>))); + 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(); + } +} diff --git a/ethcore/src/engines/tendermint/params.rs b/ethcore/src/engines/tendermint/params.rs new file mode 100644 index 000000000..cf723713b --- /dev/null +++ b/ethcore/src/engines/tendermint/params.rs @@ -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 . + +//! 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
, + /// 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 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), + }, + } + } +} diff --git a/ethcore/src/engines/tendermint/transition.rs b/ethcore/src/engines/tendermint/transition.rs new file mode 100644 index 000000000..83b390d74 --- /dev/null +++ b/ethcore/src/engines/tendermint/transition.rs @@ -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 . + +//! 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, +} + +/// 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, 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 for TransitionHandler { + fn initialize(&self, io: &IoContext) { + if let Some(engine) = self.engine.upgrade() { + set_timeout(io, engine.our_params.timeouts.propose) + } + } + + fn timeout(&self, _io: &IoContext, timer: TimerToken) { + if timer == ENGINE_TIMEOUT_TOKEN { + if let Some(engine) = self.engine.upgrade() { + engine.step(); + } + } + } + + fn message(&self, io: &IoContext, 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), + }; + } + } +} diff --git a/ethcore/src/engines/tendermint/vote_collector.rs b/ethcore/src/engines/tendermint/vote_collector.rs new file mode 100644 index 000000000..be592bc8f --- /dev/null +++ b/ethcore/src/engines/tendermint/vote_collector.rs @@ -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 . + +//! 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>, +} + +#[derive(Debug)] +pub struct SealSignatures { + pub proposal: H520, + pub votes: Vec, +} + +impl PartialEq for SealSignatures { + fn eq(&self, other: &SealSignatures) -> bool { + self.proposal == other.proposal + && self.votes.iter().collect::>() == other.votes.iter().collect::>() + } +} + +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
{ + 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 { + 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::>(); + (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 { + 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
{ + 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) -> Option { + 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, address: Address) -> Option { + 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); + } +} diff --git a/ethcore/src/error.rs b/ethcore/src/error.rs index 4afbe25b8..846972c02 100644 --- a/ethcore/src/error.rs +++ b/ethcore/src/error.rs @@ -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 for Error { } } +impl From for Error { + fn from(err: EngineError) -> Error { + Error::Engine(err) + } +} + impl From for Error { fn from(err: EthkeyError) -> Error { Error::Ethkey(err) diff --git a/ethcore/src/miner/miner.rs b/ethcore/src/miner/miner.rs index a01f16523..3002a1ffc 100644 --- a/ethcore/src/miner/miner.rs +++ b/ethcore/src/miner/miner.rs @@ -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, PendingTransaction}; 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; @@ -467,34 +467,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> { - 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. @@ -1035,7 +1044,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) { @@ -1050,7 +1058,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); diff --git a/ethcore/src/service.rs b/ethcore/src/service.rs index d809de51a..732d12a5b 100644 --- a/ethcore/src/service.rs +++ b/ethcore/src/service.rs @@ -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), + /// 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 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 } diff --git a/ethcore/src/snapshot/watcher.rs b/ethcore/src/snapshot/watcher.rs index 2ee186020..91d94174e 100644 --- a/ethcore/src/snapshot/watcher.rs +++ b/ethcore/src/snapshot/watcher.rs @@ -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, _: Vec, _: Vec, + _: Vec, _duration: u64) { if self.oracle.is_major_importing() { return } @@ -174,6 +175,7 @@ mod tests { vec![], vec![], vec![], + vec![], 0, ); } diff --git a/ethcore/src/spec/seal.rs b/ethcore/src/spec/seal.rs index eaf951189..967ffc22b 100644 --- a/ethcore/src/spec/seal.rs +++ b/ethcore/src/spec/seal.rs @@ -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 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, +/// 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, +} + +impl Into 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 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); + /// 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 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 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(), } } } diff --git a/ethcore/src/spec/spec.rs b/ethcore/src/spec/spec.rs index 5d0cc8360..bdcd5eee2 100644 --- a/ethcore/src/spec/spec.rs +++ b/ethcore/src/spec/spec.rs @@ -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, - /// The fork identifier for this chain. Only needed to distinguish two chains sharing the same genesis. - pub fork_name: Option, + /// 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, @@ -107,13 +107,13 @@ impl From 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 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)] diff --git a/ethcore/src/state_db.rs b/ethcore/src/state_db.rs index 79c1a4a2c..d38449f7a 100644 --- a/ethcore/src/state_db.rs +++ b/ethcore/src/state_db.rs @@ -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()); } } - diff --git a/ethcore/src/types/trace_types/localized.rs b/ethcore/src/types/trace_types/localized.rs index 57abea362..f65c47415 100644 --- a/ethcore/src/types/trace_types/localized.rs +++ b/ethcore/src/types/trace_types/localized.rs @@ -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, diff --git a/ethkey/src/signature.rs b/ethkey/src/signature.rs index 97a2e0715..ad595cfb9 100644 --- a/ethkey/src/signature.rs +++ b/ethkey/src/signature.rs @@ -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(&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) diff --git a/ethstore/src/dir/disk.rs b/ethstore/src/dir/disk.rs index 80e95fb66..d049def48 100644 --- a/ethstore/src/dir/disk.rs +++ b/ethstore/src/dir/disk.rs @@ -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 { + // Disk store handles updates correctly iff filename is the same + self.insert(account) + } + fn insert(&self, account: SafeAccount) -> Result { // 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 { diff --git a/ethstore/src/dir/geth.rs b/ethstore/src/dir/geth.rs index 40c3d938a..fe2ba8d1d 100644 --- a/ethstore/src/dir/geth.rs +++ b/ethstore/src/dir/geth.rs @@ -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 { + self.dir.update(account) + } + + fn remove(&self, account: &SafeAccount) -> Result<(), Error> { + self.dir.remove(account) } } diff --git a/ethstore/src/dir/memory.rs b/ethstore/src/dir/memory.rs new file mode 100644 index 000000000..c4f20f0e9 --- /dev/null +++ b/ethstore/src/dir/memory.rs @@ -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 . + +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>>, +} + +impl KeyDirectory for MemoryDirectory { + fn load(&self) -> Result, Error> { + Ok(self.accounts.read().values().cloned().flatten().collect()) + } + + fn update(&self, account: SafeAccount) -> Result { + 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 { + 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(()) + } +} + diff --git a/ethstore/src/dir/mod.rs b/ethstore/src/dir/mod.rs index 8f5a8a7ad..9b9051e69 100644 --- a/ethstore/src/dir/mod.rs +++ b/ethstore/src/dir/mod.rs @@ -14,12 +14,12 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -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, Error>; fn insert(&self, account: SafeAccount) -> Result; - fn remove(&self, address: &Address) -> Result<(), Error>; + fn update(&self, account: SafeAccount) -> Result; + 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; diff --git a/ethstore/src/dir/parity.rs b/ethstore/src/dir/parity.rs index 8c5e9c2d6..75c21ea13 100644 --- a/ethstore/src/dir/parity.rs +++ b/ethstore/src/dir/parity.rs @@ -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 { + self.dir.update(account) + } + + fn remove(&self, account: &SafeAccount) -> Result<(), Error> { + self.dir.remove(account) } } diff --git a/ethstore/src/ethstore.rs b/ethstore/src/ethstore.rs index ec1f82626..e100594de 100644 --- a/ethstore/src/ethstore.rs +++ b/ethstore/src/ethstore.rs @@ -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, - iterations: u32, - cache: RwLock>, + store: EthMultiStore, } impl EthStore { @@ -41,57 +37,46 @@ impl EthStore { } pub fn open_with_iterations(directory: Box, iterations: u32) -> Result { - 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 { - { - 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 { + self.store.insert_account(secret, password) + } + + fn accounts(&self) -> Result, 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 { + let account = try!(self.get(address)); + account.sign(password, message) + } + + fn decrypt(&self, account: &Address, password: &str, shared_mac: &[u8], message: &[u8]) -> Result, 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 { - 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 { 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, 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 { 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 { + 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, 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 { @@ -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
{ @@ -194,6 +153,288 @@ impl SecretStore for EthStore { } fn import_geth_accounts(&self, desired: Vec
, testnet: bool) -> Result, 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, + iterations: u32, + cache: RwLock>>, +} + +impl EthMultiStore { + + pub fn open(directory: Box) -> Result { + Self::open_with_iterations(directory, KEY_ITERATIONS as u32) + } + + pub fn open_with_iterations(directory: Box, iterations: u32) -> Result { + 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, 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 { + 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, 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 { + 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, 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); + } + +} diff --git a/ethstore/src/lib.rs b/ethstore/src/lib.rs index a55ad207a..e38b04ee4 100644 --- a/ethstore/src/lib.rs +++ b/ethstore/src/lib.rs @@ -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}; diff --git a/ethstore/src/random.rs b/ethstore/src/random.rs index 6140f0fae..baee08e62 100644 --- a/ethstore/src/random.rs +++ b/ethstore/src/random.rs @@ -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; diff --git a/ethstore/src/secret_store.rs b/ethstore/src/secret_store.rs index 2b3afb2ea..e3eea59c6 100644 --- a/ethstore/src/secret_store.rs +++ b/ethstore/src/secret_store.rs @@ -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; - fn import_presale(&self, json: &[u8], password: &str) -> Result; - fn import_wallet(&self, json: &[u8], password: &str) -> Result; 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; fn decrypt(&self, account: &Address, password: &str, shared_mac: &[u8], message: &[u8]) -> Result, Error>; - fn public(&self, account: &Address, password: &str) -> Result; fn accounts(&self) -> Result, Error>; +} + +pub trait SecretStore: SimpleSecretStore { + fn import_presale(&self, json: &[u8], password: &str) -> Result; + fn import_wallet(&self, json: &[u8], password: &str) -> Result; + 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; + + fn public(&self, account: &Address, password: &str) -> Result; + fn uuid(&self, account: &Address) -> Result; fn name(&self, account: &Address) -> Result; fn meta(&self, account: &Address) -> Result; diff --git a/ethstore/tests/api.rs b/ethstore/tests/api.rs index 0b3e3ca23..6485c3347 100644 --- a/ethstore/tests/api.rs +++ b/ethstore/tests/api.rs @@ -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; diff --git a/ethstore/tests/util/transient_dir.rs b/ethstore/tests/util/transient_dir.rs index 839e9722d..6a22a602d 100644 --- a/ethstore/tests/util/transient_dir.rs +++ b/ethstore/tests/util/transient_dir.rs @@ -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 { + self.dir.update(account) + } + fn insert(&self, account: SafeAccount) -> Result { 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) } } diff --git a/js/.stylelintrc.json b/js/.stylelintrc.json new file mode 100644 index 000000000..9248483c6 --- /dev/null +++ b/js/.stylelintrc.json @@ -0,0 +1,8 @@ +{ + "extends": "stylelint-config-standard", + "rules": { + "selector-pseudo-class-no-unknown": [ + true, { "ignorePseudoClasses": ["global"] } + ] + } +} diff --git a/js/package.json b/js/package.json index 8581c9cb6..66a60385b 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.2.122", + "version": "0.2.125", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team ", @@ -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", diff --git a/js/src/api/contract/contract.js b/js/src/api/contract/contract.js index 95dcf2e72..68c0371a1 100644 --- a/js/src/api/contract/contract.js +++ b/js/src/api/contract/contract.js @@ -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); } diff --git a/js/src/contracts/badgereg.js b/js/src/contracts/badgereg.js index 6cf3d8bc9..8075f456e 100644 --- a/js/src/contracts/badgereg.js +++ b/js/src/contracts/badgereg.js @@ -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 }; }); } diff --git a/js/src/inject.js b/js/src/inject.js new file mode 100644 index 000000000..56095809e --- /dev/null +++ b/js/src/inject.js @@ -0,0 +1,18 @@ +// 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 . + +import './parity'; +import './web3'; diff --git a/js/src/modals/FirstRun/firstRun.js b/js/src/modals/FirstRun/firstRun.js index a273d6e63..03bc8d770 100644 --- a/js/src/modals/FirstRun/firstRun.js +++ b/js/src/modals/FirstRun/firstRun.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . 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 = [