diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f4d5fb44ba448e79d489e891b871d78860ec6ec3..d5a5a5ecafa65be7c50f7f67f1426bca3826a3e8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,7 @@ stages: - build_and_tests - - clippy - fmt + - clippy - publish before_script: @@ -11,8 +11,9 @@ build_and_tests:stable: stage: build_and_tests tags: - redshift-rs-stable - script: - - cargo test --all-features + script: + - cargo build --features strict + - cargo test --all build_and_tests:beta: stage: build_and_tests @@ -20,7 +21,8 @@ build_and_tests:beta: - redshift-rs-beta script: - rustup update - - cargo test --all-features + - cargo build --features strict + - cargo test --all when: manual allow_failure: true @@ -30,33 +32,34 @@ build_and_tests:nightly: tags: - redshift-rs-nightly script: - - cargo test --all-features + - cargo build --features strict + - cargo test --all when: manual allow_failure: true -clippy: - stage: clippy +fmt: + stage: fmt image: rustlang/rust:nightly tags: - redshift-rs-nightly before_script: - export PATH="$HOME/.cargo/bin:$PATH" - - cargo install --force clippy --verbose + - cargo install --force rustfmt-nightly script: - - cargo clippy --all -- -D warnings --verbose - allow_failure: true - -fmt: - stage: fmt + - cargo fmt -- --check + allow_failure: true + +clippy: + stage: clippy image: rustlang/rust:nightly tags: - redshift-rs-nightly before_script: - export PATH="$HOME/.cargo/bin:$PATH" - - cargo install --force rustfmt-nightly + - cargo install --force clippy --verbose script: - - cargo fmt -- --write-mode=diff - allow_failure: true + - cargo clippy --all -- -D warnings --verbose + allow_failure: true publish:crate: stage: publish @@ -70,4 +73,21 @@ publish:crate: only: - tags allow_failure: false + when: manual + +pages: + stage: publish + tags: + - redshift-rs-stable + before_script: + - export PATH="$HOME/.cargo/bin:$PATH" + script: + - cargo doc + - mv target/doc public + - ls public + artifacts: + untracked: true + paths: + - public + allow_failure: true when: manual \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 0b4444798383eaf1f6a355dd8a39fe9f28e7cdfa..7679a35cbf9582871ab3c10de679551ebf5b011a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,6 +6,14 @@ dependencies = [ "memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "arrayvec" version = "0.4.7" @@ -14,11 +22,38 @@ dependencies = [ "nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "atty" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", + "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "base58" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "base64" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "base64" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "base64" version = "0.8.0" @@ -37,6 +72,11 @@ dependencies = [ "serde 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "bitflags" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "bitflags" version = "1.0.3" @@ -47,11 +87,67 @@ name = "byteorder" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "bytes" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "cc" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "cfg-if" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "chrono" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "clap" +version = "2.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "atty 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "yaml-rust 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "core-foundation" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "core-foundation-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "core-foundation-sys" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "crossbeam-deque" version = "0.2.0" @@ -61,6 +157,15 @@ dependencies = [ "crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "crossbeam-deque" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crossbeam-epoch 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-utils 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "crossbeam-epoch" version = "0.3.1" @@ -75,6 +180,19 @@ dependencies = [ "scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "crossbeam-epoch" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "arrayvec 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-utils 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "crossbeam-utils" version = "0.2.2" @@ -83,6 +201,75 @@ dependencies = [ "cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "crossbeam-utils" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "dtoa" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "duniter-blockchain" +version = "0.1.0" +dependencies = [ + "duniter-conf 0.1.0", + "duniter-crypto 0.1.2", + "duniter-dal 0.1.0", + "duniter-documents 0.7.1", + "duniter-message 0.1.0", + "duniter-module 0.1.0", + "duniter-network 0.1.0", + "duniter-wotb 0.8.0-a0.6", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "pbr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "sqlite 0.23.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "duniter-conf" +version = "0.1.0" +dependencies = [ + "duniter-crypto 0.1.2", + "duniter-module 0.1.0", + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "duniter-core" +version = "0.1.0" +dependencies = [ + "clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)", + "duniter-blockchain 0.1.0", + "duniter-conf 0.1.0", + "duniter-crypto 0.1.2", + "duniter-message 0.1.0", + "duniter-module 0.1.0", + "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "simplelog 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "sqlite 0.23.9 (registry+https://github.com/rust-lang/crates.io-index)", + "threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "duniter-crypto" version = "0.1.2" @@ -93,6 +280,27 @@ dependencies = [ "serde 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "duniter-dal" +version = "0.1.0" +dependencies = [ + "duniter-crypto 0.1.2", + "duniter-documents 0.7.1", + "duniter-module 0.1.0", + "duniter-network 0.1.0", + "duniter-wotb 0.8.0-a0.6", + "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "sqlite 0.23.9 (registry+https://github.com/rust-lang/crates.io-index)", + "websocket 0.20.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "duniter-documents" version = "0.7.1" @@ -107,6 +315,62 @@ dependencies = [ "serde 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "duniter-message" +version = "0.1.0" +dependencies = [ + "duniter-crypto 0.1.2", + "duniter-dal 0.1.0", + "duniter-documents 0.7.1", + "duniter-module 0.1.0", + "duniter-network 0.1.0", + "serde 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "duniter-module" +version = "0.1.0" +dependencies = [ + "duniter-crypto 0.1.2", + "duniter-documents 0.7.1", + "serde 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "duniter-network" +version = "0.1.0" +dependencies = [ + "duniter-crypto 0.1.2", + "duniter-documents 0.7.1", + "duniter-module 0.1.0", + "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "duniter-tui" +version = "0.1.0" +dependencies = [ + "duniter-conf 0.1.0", + "duniter-crypto 0.1.2", + "duniter-dal 0.1.0", + "duniter-documents 0.7.1", + "duniter-message 0.1.0", + "duniter-module 0.1.0", + "duniter-network 0.1.0", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "duniter-wotb" version = "0.8.0-a0.6" @@ -118,11 +382,61 @@ dependencies = [ "serde_derive 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "duniter-ws2p" +version = "0.1.0" +dependencies = [ + "duniter-conf 0.1.0", + "duniter-crypto 0.1.2", + "duniter-dal 0.1.0", + "duniter-documents 0.7.1", + "duniter-message 0.1.0", + "duniter-module 0.1.0", + "duniter-network 0.1.0", + "duniter-wotb 0.8.0-a0.6", + "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "sqlite 0.23.9 (registry+https://github.com/rust-lang/crates.io-index)", + "websocket 0.20.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "durs" +version = "0.1.0" +dependencies = [ + "clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)", + "duniter-core 0.1.0", + "duniter-tui 0.1.0", + "duniter-ws2p 0.1.0", + "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "websocket 0.20.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "either" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "fuchsia-zircon" version = "0.3.3" @@ -137,16 +451,92 @@ name = "fuchsia-zircon-sys" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "futures" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "gcc" version = "0.3.54" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "httparse" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "hyper" +version = "0.10.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "httparse 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", + "traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "idna" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-normalization 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "iovec" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "itoa" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +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 = "language-tags" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "lazy_static" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "lazy_static" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "lazycell" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "libc" version = "0.2.40" @@ -158,30 +548,170 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] -name = "memchr" -version = "2.0.1" +name = "log" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "log" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "matches" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "memchr" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "memoffset" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "mime" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "mio" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lazycell 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "net2 0.2.32 (registry+https://github.com/rust-lang/crates.io-index)", + "slab 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "miow" +version = "0.2.1" +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.32 (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)", +] + +[[package]] +name = "native-tls" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)", + "schannel 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", + "security-framework 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", + "security-framework-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", + "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "net2" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "nodrop" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "num-integer" +version = "0.1.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "num_cpus" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "openssl" +version = "0.9.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.9.30 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "openssl-sys" +version = "0.9.30" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ + "cc 1.0.15 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)", + "vcpkg 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "memoffset" -version = "0.2.1" +name = "pbr" +version = "1.0.1" 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.40 (registry+https://github.com/rust-lang/crates.io-index)", + "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] [[package]] -name = "nodrop" -version = "0.1.12" +name = "percent-encoding" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] -name = "num_cpus" -version = "1.8.0" +name = "pkg-config" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", -] [[package]] name = "proc-macro2" @@ -245,6 +775,14 @@ name = "redox_syscall" version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "redox_termios" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "regex" version = "0.2.11" @@ -265,6 +803,14 @@ dependencies = [ "ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "remove_dir_all" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rust-crypto" version = "0.2.36" @@ -287,11 +833,45 @@ name = "safemem" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "schannel" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "scoped-tls" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "scopeguard" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "security-framework" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "core-foundation 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", + "security-framework-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "security-framework-sys" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "core-foundation-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "serde" version = "1.0.50" @@ -307,6 +887,68 @@ dependencies = [ "syn 0.13.7 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "serde_json" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "sha1" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "simplelog" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "term 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "slab" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "sqlite" +version = "0.23.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", + "sqlite3-sys 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "sqlite3-src" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.15 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "sqlite3-sys" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", + "sqlite3-src 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "strsim" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "syn" version = "0.13.7" @@ -317,6 +959,42 @@ dependencies = [ "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "term" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "termion" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "textwrap" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "thread_local" version = "0.3.5" @@ -326,6 +1004,14 @@ dependencies = [ "unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "threadpool" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "time" version = "0.1.39" @@ -336,11 +1022,182 @@ dependencies = [ "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "tokio" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "mio 0.6.14 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-executor 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-fs 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-reactor 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-tcp 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-threadpool 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-timer 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-udp 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-core" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "mio 0.6.14 (registry+https://github.com/rust-lang/crates.io-index)", + "scoped-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-executor 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-reactor 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-timer 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-executor" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-fs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-threadpool 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-io" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-reactor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "mio 0.6.14 (registry+https://github.com/rust-lang/crates.io-index)", + "slab 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-executor 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-tcp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "mio 0.6.14 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-reactor 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-threadpool" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crossbeam-deque 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-executor 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-timer" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-executor 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-tls" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "native-tls 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-udp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "mio 0.6.14 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-reactor 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "traitobject" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "typeable" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "ucd-util" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "unicase" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "version_check 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-width" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "unicode-xid" version = "0.1.0" @@ -354,16 +1211,67 @@ dependencies = [ "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "url" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "utf8-ranges" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "vcpkg" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "vec_map" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "version_check" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "void" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "websocket" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.10.13 (registry+https://github.com/rust-lang/crates.io-index)", + "native-tls 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", + "sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-tls 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "winapi" version = "0.3.4" @@ -373,6 +1281,11 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -383,29 +1296,85 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "ws2_32-sys" +version = "0.2.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 = "yaml-rust" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + [metadata] "checksum aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d6531d44de723825aa81398a6415283229725a00fa30713812ab9323faa82fc4" +"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" "checksum arrayvec 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a1e964f9e24d588183fcb43503abda40d288c8657dfc27311516ce2f05675aef" +"checksum atty 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "2fc4a1aa4c24c0718a250f0681885c1af91419d242f29eb8f2ab28502d80dbd1" "checksum base58 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5024ee8015f02155eee35c711107ddd9a9bf3cb689cf2a9089c97e79b6e1ae83" +"checksum base64 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "30e93c03064e7590d0466209155251b90c22e37fab1daf2771582598b5827557" +"checksum base64 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "96434f987501f0ed4eb336a411e0631ecd1afa11574fe148587adc4ff96143c9" "checksum base64 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7c4a342b450b268e1be8036311e2c613d7f8a7ed31214dff1cc3b60852a3168d" "checksum bincode 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9a6301db0b49fb63551bc15b5ae348147101cdf323242b93ec7546d5002ff1af" +"checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" "checksum bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d0c54bb8f454c567f21197eefcdbf5679d0bd99f2ddbe52e84c77061952e6789" "checksum byteorder 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "73b5bdfe7ee3ad0b99c9801d58807a9dbc9e09196365b0203853b99889ab3c87" +"checksum bytes 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "2f1d50c876fb7545f5f289cd8b2aee3f359d073ae819eed5d6373638e2c61e59" +"checksum cc 1.0.15 (registry+https://github.com/rust-lang/crates.io-index)" = "0ebb87d1116151416c0cf66a0e3fb6430cccd120fd6300794b4dfaa050ac40ba" "checksum cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "405216fd8fe65f718daa7102ea808a946b6ce40c742998fbfd3463645552de18" +"checksum chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1cce36c92cb605414e9b824f866f5babe0a0368e39ea07393b9b63cf3844c0e6" +"checksum clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0f16b89cbb9ee36d87483dc939fe9f1e13c05898d56d7b230a0d4dff033a536" +"checksum core-foundation 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "25bfd746d203017f7d5cbd31ee5d8e17f94b6521c7af77ece6c9e4b2d4b16c67" +"checksum core-foundation-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "065a5d7ffdcbc8fa145d6f0746f3555025b9097a9e9cda59f7467abae670c78d" "checksum crossbeam-deque 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f739f8c5363aca78cfb059edf753d8f0d36908c348f3d8d1503f03d8b75d9cf3" +"checksum crossbeam-deque 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fe8153ef04a7594ded05b427ffad46ddeaf22e63fd48d42b3e1e3bb4db07cae7" "checksum crossbeam-epoch 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "927121f5407de9956180ff5e936fe3cf4324279280001cd56b669d28ee7e9150" +"checksum crossbeam-epoch 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9b4e2817eb773f770dcb294127c011e22771899c21d18fce7dd739c0b9832e81" "checksum crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2760899e32a1d58d5abb31129f8fae5de75220bc2176e77ff7c627ae45c918d9" +"checksum crossbeam-utils 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d636a8b3bcc1b409d7ffd3facef8f21dcb4009626adbd0c5e6c4305c07253c7b" +"checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab" "checksum either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3be565ca5c557d7f59e7cfcf1844f9e3033650c929c6566f511e8005f205c1d0" +"checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +"checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +"checksum futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)" = "1a70b146671de62ec8c8ed572219ca5d594d9b06c0b364d5e67b722fc559b48c" "checksum gcc 0.3.54 (registry+https://github.com/rust-lang/crates.io-index)" = "5e33ec290da0d127825013597dbdfc28bee4964690c7ce1166cbc2a7bd08b1bb" +"checksum httparse 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c2f407128745b78abc95c0ffbe4e5d37427fdc0d45470710cfef8c44522a2e37" +"checksum hyper 0.10.13 (registry+https://github.com/rust-lang/crates.io-index)" = "368cb56b2740ebf4230520e2b90ebb0461e69034d85d1945febd9b3971426db2" +"checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d" +"checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08" +"checksum itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c069bbec61e1ca5a596166e55dfe4773ff745c3d16b700013bcaff9a6df2c682" +"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +"checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" +"checksum lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" "checksum lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c8f31047daa365f19be14b47c29df4f7c3b581832407daabe6ae77397619237d" +"checksum lazycell 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a6f08839bc70ef4a3fe1d566d5350f519c5912ea86be0df1740a7d247c7fc0ef" "checksum libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)" = "6fd41f331ac7c5b8ac259b8bf82c75c0fb2e469bbf37d2becbba9a6a2221965b" "checksum linked-hash-map 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "70fb39025bc7cdd76305867c4eccf2f2dcf6e9a57f5b21a93e1c2d86cd03ec9e" +"checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" +"checksum log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "89f010e843f2b1a31dbd316b3b8d443758bc634bed37aabade59c686d644e0a2" +"checksum matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "100aabe6b8ff4e4a7e32c1c13523379802df0772b82466207ac25b013f193376" "checksum memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "796fba70e76612589ed2ce7f45282f5af869e0fdd7cc6199fa1aa1f1d591ba9d" "checksum memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0f9dc261e2b62d7a622bf416ea3c5245cdd5d9a7fcc428c0d06804dfce1775b3" +"checksum mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ba626b8a6de5da682e1caa06bdb42a335aee5a84db8e5046a3e8ab17ba0a3ae0" +"checksum mio 0.6.14 (registry+https://github.com/rust-lang/crates.io-index)" = "6d771e3ef92d58a8da8df7d6976bfca9371ed1de6619d9d5a5ce5b1f29b85bfe" +"checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" +"checksum native-tls 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f74dbadc8b43df7864539cedb7bc91345e532fdd913cfdc23ad94f4d2d40fbc0" +"checksum net2 0.2.32 (registry+https://github.com/rust-lang/crates.io-index)" = "9044faf1413a1057267be51b5afba8eb1090bd2231c693664aa1db716fe1eae0" "checksum nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "9a2228dca57108069a5262f2ed8bd2e82496d2e074a06d1ccc7ce1687b6ae0a2" +"checksum num-integer 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)" = "6ac0ea58d64a89d9d6b7688031b3be9358d6c919badcf7fbb0527ccfd891ee45" +"checksum num-traits 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "775393e285254d2f5004596d69bb8bc1149754570dcc08cf30cabeba67955e28" "checksum num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c51a3322e4bca9d212ad9a158a02abc6934d005490c054a2778df73a70aa0a30" +"checksum openssl 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)" = "a3605c298474a3aa69de92d21139fb5e2a81688d308262359d85cdd0d12a7985" +"checksum openssl-sys 0.9.30 (registry+https://github.com/rust-lang/crates.io-index)" = "73ae718c3562989cd3a0a5c26610feca02f8116822f6f195e6cf4887481e57f5" +"checksum pbr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "deb73390ab68d81992bd994d145f697451bb0b54fd39738e72eef32458ad6907" +"checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" +"checksum pkg-config 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "110d5ee3593dbb73f56294327fe5668bcc997897097cbc76b51e7aed3f52452f" "checksum proc-macro2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1b06e2f335f48d24442b35a19df506a835fb3547bc3c06ef27340da9acf5cae7" "checksum quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9949cfe66888ffe1d53e6ec9d9f3b70714083854be20fd5e271b232a017401e8" "checksum rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)" = "15a732abf9d20f0ad8eeb6f909bf6868722d9a06e1e50802b6a70351f40b4eb1" @@ -413,22 +1382,67 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum rayon 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "80e811e76f1dbf68abf87a759083d34600017fc4e10b6bd5ad84a700f9dba4b1" "checksum rayon-core 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9d24ad214285a7729b174ed6d3bcfcb80177807f959d95fafd5bfc5c4f201ac8" "checksum redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "0d92eecebad22b767915e4d529f89f28ee96dbbf5a4810d2b844373f136417fd" +"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" "checksum regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9329abc99e39129fcceabd24cf5d85b4671ef7c29c50e972bc5afe32438ec384" "checksum regex-syntax 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7" +"checksum remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3488ba1b9a2084d38645c4c08276a1752dcbf2c7130d74f1569681ad5d2799c5" "checksum rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)" = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a" "checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" "checksum safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f" +"checksum schannel 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "85fd9df495640643ad2d00443b3d78aae69802ad488debab4f1dd52fc1806ade" +"checksum scoped-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "332ffa32bf586782a3efaeb58f127980944bbc8c4d6913a86107ac2a5ab24b28" "checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27" +"checksum security-framework 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "dfa44ee9c54ce5eecc9de7d5acbad112ee58755239381f687e564004ba4a2332" +"checksum security-framework-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "5421621e836278a0b139268f36eee0dc7e389b784dc3f79d8f11aabadf41bead" "checksum serde 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)" = "428d3d818cb94ee037a17bf4f2200db2552e19b1825d33df2196624290716f92" "checksum serde_derive 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)" = "ee76093b16868c4c9c8e5329c3d30745833e35390624019738472bd13e996e79" +"checksum serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)" = "f3ad6d546e765177cf3dded3c2e424a8040f870083a0e64064746b958ece9cb1" +"checksum sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cc30b1e1e8c40c121ca33b86c23308a090d19974ef001b4bf6e61fd1a0fb095c" +"checksum simplelog 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ce595117de34b75e057b41e99079e43e9fcc4e5ec9c7ba5f2fea55321f0c624e" +"checksum slab 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fdeff4cd9ecff59ec7e3744cbca73dfe5ac35c2aedb2cfba8a1c715a18912e9d" +"checksum sqlite 0.23.9 (registry+https://github.com/rust-lang/crates.io-index)" = "d18d7b10278336e7fd9dc259399a0f9ff419616738b6a841b136c362e16db626" +"checksum sqlite3-src 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)" = "46e0bc115b563b1ee6c665ef895b56bf488522f57d1c6571887547c57c8f5a88" +"checksum sqlite3-sys 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "71fec807a1534bd13eeaaec396175d67c79bdc68df55e18a452726ec62a8fb08" +"checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550" "checksum syn 0.13.7 (registry+https://github.com/rust-lang/crates.io-index)" = "61b8f1b737f929c6516ba46a3133fd6d5215ad8a62f66760f851f7048aebedfb" +"checksum tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +"checksum term 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5e6b677dd1e8214ea1ef4297f85dbcbed8e8cdddb561040cc998ca2551c37561" +"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" +"checksum textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0b59b6b4b44d867f1370ef1bd91bfb262bf07bf0ae65c202ea2fbc16153b693" "checksum thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "279ef31c19ededf577bfd12dfae728040a21f635b06a24cd670ff510edd38963" +"checksum threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e2f0c90a5f3459330ac8bc0d2f879c693bb7a2f59689c1083fc4ef83834da865" "checksum time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "a15375f1df02096fb3317256ce2cee6a1f42fc84ea5ad5fc8c421cfe40c73098" +"checksum tokio 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7d00555353b013e170ed8bc4e13f648a317d1fd12157dbcae13f7013f6cf29f5" +"checksum tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "aeeffbbb94209023feaef3c196a41cbcdafa06b4a6f893f68779bb5e53796f71" +"checksum tokio-executor 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8cac2a7883ff3567e9d66bb09100d09b33d90311feca0206c7ca034bc0c55113" +"checksum tokio-fs 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "76766830bbf9a2d5bfb50c95350d56a2e79e2c80f675967fff448bc615899708" +"checksum tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "6af9eb326f64b2d6b68438e1953341e00ab3cf54de7e35d92bfc73af8555313a" +"checksum tokio-reactor 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b3cedc8e5af5131dc3423ffa4f877cce78ad25259a9a62de0613735a13ebc64b" +"checksum tokio-tcp 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ec9b094851aadd2caf83ba3ad8e8c4ce65a42104f7b94d9e6550023f0407853f" +"checksum tokio-threadpool 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5783254b10c7c84a56f62c74766ef7e5b83d1f13053218c7cab8d3f2c826fa0e" +"checksum tokio-timer 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535fed0ccee189f3d48447587697ba3fd234b3dbbb091f0ec4613ddfec0a7c4c" +"checksum tokio-tls 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "772f4b04e560117fe3b0a53e490c16ddc8ba6ec437015d91fa385564996ed913" +"checksum tokio-udp 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "137bda266504893ac4774e0ec4c2108f7ccdbcb7ac8dced6305fe9e4e0b5041a" +"checksum traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" +"checksum typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887" "checksum ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fd2be2d6639d0f8fe6cdda291ad456e23629558d466e2789d2c3e9892bda285d" +"checksum unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33" +"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +"checksum unicode-normalization 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "6a0180bc61fc5a987082bfa111f4cc95c4caff7f9799f3e46df09163a937aa25" +"checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f" "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" "checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +"checksum url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f808aadd8cfec6ef90e4a14eb46f24511824d1ac596b9682703c87056c8678b7" "checksum utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "662fab6525a98beff2921d7f61a39e7d59e0b425ebc7d0d9e66d316e55124122" +"checksum vcpkg 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7ed0f6789c8a85ca41bbc1c9d175422116a9869bd1cf31bb08e1493ecce60380" +"checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" +"checksum version_check 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6b772017e347561807c1aa192438c5fd74242a670a6cffacc40f2defd1dc069d" "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +"checksum websocket 0.20.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eb277e7f4c23dc49176f74ae200e77651764efb2c25f56ad2d22623b63826369" +"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" "checksum winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "04e3bd221fcbe8a271359c04f21a76db7d0c6028862d1bb5512d85e1e2eb5bb3" +"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +"checksum yaml-rust 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e66366e18dc58b46801afbf2ca7661a9f59cc8c5962c29892b6039b4f86fa992" diff --git a/Cargo.toml b/Cargo.toml index cb5535375711974cca62b64deaef2e95f5f5b793..4f26b8dcec354f666bee4ad07f156ae913af3714 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,35 @@ +[package] +name = "durs" +version = "0.1.0" +authors = ["librelois <elois@duniter.org>","nanocryk <nanocryk@duniter.org>"] +description = "DUniter-RS (durs) is a new implementation of Duniter protocol and software in Rust, a safe, concurrent, practical language" +license = "AGPL-3.0" + +[dependencies] +clap = "2.31.2" +duniter-core = { path = "./core" } +duniter-tui = { path = "./tui" } +duniter-ws2p = { path = "./ws2p" } +lazy_static = "1.0.0" +serde_json = "1.0.9" +websocket = "0.20.2" + +[features] +# Treat warnings as a build error. +strict = [] + [workspace] members = [ - "wotb", + "blockchain", + "conf", + "core", "crypto", + "dal", "documents", + "message", + "module", + "network", + "tui", + "wotb", + "ws2p", ] diff --git a/blockchain/Cargo.toml b/blockchain/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..1f4e9e0ecee27ee39cf63b179879bf05376b458a --- /dev/null +++ b/blockchain/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "duniter-blockchain" +version = "0.1.0" +authors = ["librelois <elois@ifee.fr>"] +description = "Blockchain module for the Duniter project." +license = "AGPL-3.0" + +[lib] +path = "lib.rs" + +[dependencies] +duniter-conf = { path = "../conf" } +duniter-crypto = { path = "../crypto" } +duniter-dal = { path = "../dal" } +duniter-documents = { path = "../documents" } +duniter-message = { path = "../message" } +duniter-module = { path = "../module" } +duniter-network = { path = "../network" } +duniter-wotb = { path = "../wotb" } +log = "0.4.1" +pbr = "1.0.0" +rand = "0.4.2" +serde = "1.0.24" +serde_derive = "1.0.24" +serde_json = "1.0.9" +sqlite = "0.23.9" \ No newline at end of file diff --git a/blockchain/lib.rs b/blockchain/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..8710a5214a4064bf7d46218666a320b285cb2113 --- /dev/null +++ b/blockchain/lib.rs @@ -0,0 +1,1006 @@ +// Copyright (C) 2018 The Duniter Project Developers. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! Module managing the Duniter blockchain. + +#![cfg_attr(feature = "strict", deny(warnings))] +#![deny( + missing_docs, missing_debug_implementations, missing_copy_implementations, trivial_casts, + trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces, + unused_qualifications +)] + +#[macro_use] +extern crate log; + +extern crate duniter_conf; +extern crate duniter_crypto; +extern crate duniter_dal; +extern crate duniter_documents; +extern crate duniter_message; +extern crate duniter_module; +extern crate duniter_network; +extern crate duniter_wotb; +extern crate serde; +extern crate serde_json; +extern crate sqlite; + +mod stack_up_block; +mod sync; + +use std::collections::HashMap; +use std::env; +use std::ops::Deref; +use std::path::PathBuf; +use std::str; +use std::sync::mpsc; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use self::stack_up_block::try_stack_up_completed_block; +use duniter_crypto::keys::ed25519; +use duniter_dal::block::{DALBlock, WotEvent}; +use duniter_dal::constants::MAX_FORKS; +use duniter_dal::dal_event::DALEvent; +use duniter_dal::dal_requests::{DALReqBlockchain, DALRequest, DALResBlockchain, DALResponse}; +use duniter_dal::identity::DALIdentity; +use duniter_dal::parsers::memberships::MembershipParseError; +use duniter_dal::writers::requests::DBWriteRequest; +use duniter_dal::{DuniterDB, ForkState}; +use duniter_documents::blockchain::v10::documents::{BlockDocument, V10Document}; +use duniter_documents::blockchain::{BlockchainProtocol, Document, VerificationResult}; +use duniter_documents::{BlockHash, BlockId, Blockstamp}; +use duniter_message::DuniterMessage; +use duniter_module::*; +use duniter_network::{ + NetworkBlock, NetworkDocument, NetworkEvent, NetworkRequest, NetworkResponse, NodeFullId, +}; +use duniter_wotb::data::rusty::RustyWebOfTrust; +use duniter_wotb::operations::file::{BinaryFileFormater, FileFormater}; +use duniter_wotb::{NodeId, WebOfTrust}; + +/// The blocks are requested by packet groups. This constant sets the block packet size. +pub static CHUNK_SIZE: &'static u32 = &50; +/// The blocks are requested by packet groups. This constant sets the number of packets per group. +pub static MAX_BLOCKS_REQUEST: &'static u32 = &500; +/// There can be several implementations of the wot file backup, this constant fixes the implementation used by the blockchain module. +pub static WOT_FILE_FORMATER: BinaryFileFormater = BinaryFileFormater {}; + +/// Blockchain Module +#[derive(Debug)] +pub struct BlockchainModule { + /// Subscribers + pub followers: Vec<mpsc::Sender<DuniterMessage>>, + /// Name of the user datas profile + pub conf_profile: String, + /// Currency + pub currency: Currency, + /// Database containing the blockchain + pub db: DuniterDB, + /// The block under construction + pub pending_block: Option<Box<BlockDocument>>, +} + +#[derive(Debug, Clone)] +/// Block +enum Block<'a> { + /// Block coming from Network + NetworkBlock(&'a NetworkBlock), + /// Block coming from local database + LocalBlock(&'a BlockDocument), +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +/// When synchronizing the blockchain, checking all rules at each block really takes a long time. +/// The user is therefore offered a fast synchronization that checks only what is strictly necessary for indexing the data. +pub enum SyncVerificationLevel { + /// Fast sync, checks only what is strictly necessary for indexing the data. + FastSync(), + /// Cautious sync, checking all protocol rules (really takes a long time). + Cautious(), +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +/// Error returned by function complete_network_block() +pub enum CompletedBlockError { + /// MembershipParseError + MembershipParseError(MembershipParseError), + /// Invalid block inner hash + InvalidInnerHash(), + /// Invalid block signature + InvalidSig(), + /// Invalid block hash + InvalidHash(), + /// Invalid block version + InvalidVersion(), +} + +impl From<MembershipParseError> for CompletedBlockError { + fn from(e: MembershipParseError) -> CompletedBlockError { + CompletedBlockError::MembershipParseError(e) + } +} + +impl BlockchainModule { + /// Return module identifier + pub fn id() -> ModuleId { + ModuleId::Str("blockchain") + } + /// Loading blockchain configuration + pub fn load_blockchain_conf( + conf: &DuniterConf, + _keys: RequiredKeysContent<ed25519::KeyPair>, + sync: bool, + ) -> BlockchainModule { + // Get db path + let db_path = duniter_conf::get_db_path(conf.profile().as_str(), &conf.currency(), sync); + + // Open duniter database + let db = duniter_dal::open_db(&db_path, false).unwrap(); + + // Instanciate BlockchainModule + BlockchainModule { + followers: Vec::new(), + conf_profile: conf.profile(), + currency: conf.currency(), + db, + pending_block: None, + } + } + /// Synchronize blockchain from a duniter-ts database + pub fn sync_ts(conf: &DuniterConf, ts_profile: &str, cautious: bool) { + // Open local blockchain db + let db_path = duniter_conf::get_db_path(&conf.profile(), &conf.currency(), false); + let db = duniter_dal::open_db(&db_path, false).expect(&format!( + "Fatal error : fail to open blockchain database as path : {} !", + db_path.as_path().to_str().unwrap() + )); + // Get local current blockstamp + debug!("Get local current blockstamp..."); + let current_block: Option<BlockDocument> = duniter_dal::new_get_current_block(&db); + let current_blockstamp = match current_block.clone() { + Some(block) => block.blockstamp(), + None => Blockstamp::default(), + }; + debug!("Success to get local current blockstamp."); + // get db_ts_path + let mut db_ts_path = match env::home_dir() { + Some(path) => path, + None => panic!("Impossible to get your home dir!"), + }; + db_ts_path.push(".config/duniter/"); + db_ts_path.push(ts_profile); + db_ts_path.push("duniter.db"); + if !db_ts_path.as_path().exists() { + panic!("Fatal error : duniter-ts database don't exist !"); + } + sync::sync_ts(conf, ¤t_blockstamp, db_ts_path, cautious); + } + /// Request chunk from network (chunk = group of blocks) + fn request_chunk(&self, req_id: &ModuleReqId, from: u32) -> (ModuleReqId, NetworkRequest) { + let req = NetworkRequest::GetBlocks( + ModuleReqFullId(BlockchainModule::id(), req_id.clone()), + NodeFullId::default(), + *CHUNK_SIZE, + from, + ); + (self.request_network(req.clone()), req) + } + /// Requests blocks from current to `to` + fn request_blocks_to( + &self, + pending_network_requests: &HashMap<ModuleReqId, NetworkRequest>, + current_blockstamp: &Blockstamp, + to: BlockId, + ) -> HashMap<ModuleReqId, NetworkRequest> { + let mut from = if *current_blockstamp == Blockstamp::default() { + 0 + } else { + current_blockstamp.id.0 + 1 + }; + info!( + "BlockchainModule : request_blocks_to({}-{})", + current_blockstamp.id.0 + 1, + to + ); + let mut requests_ids = HashMap::new(); + if current_blockstamp.id < to { + let mut real_to = to.0; + if (to.0 - current_blockstamp.id.0) > *MAX_BLOCKS_REQUEST { + real_to = current_blockstamp.id.0 + *MAX_BLOCKS_REQUEST; + } + while from <= real_to { + let mut req_id = ModuleReqId(0); + while pending_network_requests.contains_key(&req_id) + || requests_ids.contains_key(&req_id) + { + req_id = ModuleReqId(req_id.0 + 1); + } + let (req_id, req) = self.request_chunk(&req_id, from); + requests_ids.insert(req_id, req); + from += *CHUNK_SIZE; + } + } + requests_ids + } + /// Send network request + fn request_network(&self, request: NetworkRequest) -> ModuleReqId { + for follower in &self.followers { + if follower + .send(DuniterMessage::NetworkRequest(request.clone())) + .is_err() + { + debug!("BlockchainModule : one follower is unreachable !"); + } + } + request.get_req_id() + } + /// Send blockchain event + fn send_event(&self, event: DALEvent) { + for follower in &self.followers { + match follower.send(DuniterMessage::DALEvent(event.clone())) { + Ok(_) => {} + Err(_) => {} + } + } + } + fn send_req_response(&self, response: DALResponse) { + for follower in &self.followers { + match follower.send(DuniterMessage::DALResponse(response.clone())) { + Ok(_) => {} + Err(_) => {} + } + } + } + fn receive_network_documents<W: WebOfTrust + Sync>( + &self, + network_documents: &Vec<NetworkDocument>, + current_blockstamp: &Blockstamp, + forks: &mut Vec<ForkState>, + wotb_index: &HashMap<ed25519::PublicKey, NodeId>, + wot: &W, + ) -> (Blockstamp, Vec<WotEvent>) { + let mut blockchain_documents = Vec::new(); + let mut current_blockstamp = current_blockstamp.clone(); + let mut wot_events = Vec::new(); + for network_document in network_documents { + match network_document { + &NetworkDocument::Block(ref network_block) => { + let (success, _new_forks, mut new_wot_events) = self.apply_block( + &Block::NetworkBlock(network_block), + ¤t_blockstamp, + forks, + wotb_index, + wot, + ); + if success { + current_blockstamp = network_block.blockstamp(); + wot_events.append(&mut new_wot_events); + // Update isolates forks + let stackables_forks = + DALBlock::get_stackables_forks(&self.db, ¤t_blockstamp); + for fork in stackables_forks { + debug!("unisolate fork {}", fork); + if forks.len() > fork { + forks[fork] = ForkState::Full(); + DALBlock::unisolate_fork(&self.db, fork); + } + } + } /*else if !new_forks.is_empty() { + forks = new_forks; + }*/ + } + &NetworkDocument::Identity(ref doc) => blockchain_documents.push( + BlockchainProtocol::V10(Box::new(V10Document::Identity(doc.deref().clone()))), + ), + &NetworkDocument::Membership(ref doc) => blockchain_documents.push( + BlockchainProtocol::V10(Box::new(V10Document::Membership(doc.deref().clone()))), + ), + &NetworkDocument::Certification(ref doc) => { + blockchain_documents.push(BlockchainProtocol::V10(Box::new( + V10Document::Certification(Box::new(doc.deref().clone())), + ))) + } + &NetworkDocument::Revocation(ref doc) => { + blockchain_documents.push(BlockchainProtocol::V10(Box::new( + V10Document::Revocation(Box::new(doc.deref().clone())), + ))) + } + &NetworkDocument::Transaction(ref doc) => { + blockchain_documents.push(BlockchainProtocol::V10(Box::new( + V10Document::Transaction(Box::new(doc.deref().clone())), + ))) + } + } + } + if !blockchain_documents.is_empty() { + self.receive_documents(&blockchain_documents); + } + (current_blockstamp, wot_events) + } + fn receive_documents(&self, documents: &Vec<BlockchainProtocol>) { + debug!("BlockchainModule : receive_documents()"); + for document in documents { + trace!("BlockchainModule : Treat one document."); + match document { + &BlockchainProtocol::V10(ref doc_v10) => match doc_v10.deref() { + _ => {} + }, + _ => self.send_event(DALEvent::RefusedPendingDoc(document.clone())), + } + } + } + fn receive_blocks<W: WebOfTrust + Sync>( + &self, + blocks_in_box: &Vec<Box<NetworkBlock>>, + current_blockstamp: &Blockstamp, + forks: &Vec<ForkState>, + wotb_index: &HashMap<ed25519::PublicKey, NodeId>, + wot: &W, + ) -> (Blockstamp, Vec<ForkState>, Vec<WotEvent>) { + debug!("BlockchainModule : receive_blocks()"); + let blocks: Vec<&NetworkBlock> = blocks_in_box.into_iter().map(|b| b.deref()).collect(); + let mut current_blockstamp = current_blockstamp.clone(); + let mut all_wot_events = Vec::new(); + let mut forks = forks.clone(); + let mut wot_copy: W = wot.clone(); + let mut wotb_index_copy = wotb_index.clone(); + for block in blocks { + let (success, _new_forks, mut wot_events) = self.apply_block::<W>( + &Block::NetworkBlock(block), + ¤t_blockstamp, + &mut forks, + &wotb_index_copy, + &wot_copy, + ); + all_wot_events.append(&mut wot_events); + if success { + current_blockstamp = block.blockstamp(); + } /*else if !new_forks.is_empty() { + forks = new_forks; + }*/ + if !wot_events.is_empty() { + for wot_event in wot_events { + match wot_event { + WotEvent::AddNode(pubkey, wotb_id) => { + wot_copy.add_node(); + wotb_index_copy.insert(pubkey, wotb_id); + } + WotEvent::RemNode(pubkey) => { + wot_copy.rem_node(); + wotb_index_copy.remove(&pubkey); + } + WotEvent::AddLink(source, target) => { + wot_copy.add_link(source, target); + } + WotEvent::RemLink(source, target) => { + wot_copy.rem_link(source, target); + } + WotEvent::EnableNode(wotb_id) => { + wot_copy.set_enabled(wotb_id, true); + } + WotEvent::DisableNode(wotb_id) => { + wot_copy.set_enabled(wotb_id, false); + } + } + } + } + } + (current_blockstamp, forks, all_wot_events) + } + /*fn apply_local_block<W: WebOfTrust>( + db: &sqlite::connexion, + current_blockstamp: &Blockstamp, + wotb_index: &HashMap<ed25519::PublicKey, NodeId>, + wot: &W, + ) { + for f in 1..10 { + let potential_next_block = get_block(db, ); + } + }*/ + fn apply_block<W: WebOfTrust + Sync>( + &self, + block: &Block, + current_blockstamp: &Blockstamp, + forks: &mut Vec<ForkState>, + wotb_index: &HashMap<ed25519::PublicKey, NodeId>, + wot: &W, + ) -> (bool, Vec<ForkState>, Vec<WotEvent>) { + let mut already_have_block = false; + let block_doc = match block { + &Block::NetworkBlock(network_block) => match network_block { + &NetworkBlock::V10(ref network_block_v10) => { + let (hashs, _) = DALBlock::get_blocks_hashs_all_forks( + &self.db, + &network_block_v10.uncompleted_block_doc.number, + ); + for hash in hashs { + if hash == network_block_v10.uncompleted_block_doc.hash.unwrap() { + already_have_block = true; + } + } + &network_block_v10.uncompleted_block_doc + } + _ => return (false, Vec::with_capacity(0), Vec::with_capacity(0)), + }, + &Block::LocalBlock(block_doc) => { + already_have_block = true; + block_doc + } + }; + if (block_doc.number.0 == current_blockstamp.id.0 + 1 + && block_doc.previous_hash.to_string() == current_blockstamp.hash.0.to_string()) + || (block_doc.number.0 == 0 && current_blockstamp.clone() == Blockstamp::default()) + { + debug!( + "stackable_block : block {} chainable !", + block_doc.blockstamp() + ); + let (success, db_requests, wot_events) = match block { + &Block::NetworkBlock(network_block) => self.try_stack_up_block( + &network_block, + wotb_index, + wot, + SyncVerificationLevel::Cautious(), + ), + &Block::LocalBlock(block_doc) => { + try_stack_up_completed_block(&block_doc, wotb_index, wot) + } + }; + debug!( + "stackable_block_ : block {} chainable !", + block_doc.blockstamp() + ); + if success { + // Apply db requests + db_requests + .iter() + .map(|req| req.apply(&block_doc.currency, &self.db)) + .collect::<()>(); + info!("StackUpValidBlock({})", block_doc.number.0); + self.send_event(DALEvent::StackUpValidBlock(Box::new(block_doc.clone()))); + return (true, Vec::with_capacity(0), wot_events); + } else { + warn!("RefusedBlock({})", block_doc.number.0); + self.send_event(DALEvent::RefusedPendingDoc(BlockchainProtocol::V10( + Box::new(V10Document::Block(Box::new(block_doc.clone()))), + ))); + } + } else if !already_have_block + && (block_doc.number.0 >= current_blockstamp.id.0 + || (current_blockstamp.id.0 - block_doc.number.0) < 100) + { + debug!( + "stackable_block : block {} not chainable, store this for future !", + block_doc.blockstamp() + ); + //let mut forks = forks.clone(); + let (fork, fork_state) = match DALBlock::get_block_fork( + &self.db, + &Blockstamp { + id: BlockId(block_doc.number.0 - 1), + hash: BlockHash(block_doc.previous_hash), + }, + ) { + Some(fork) => if forks.len() > fork { + (fork, forks[fork]) + } else { + panic!(format!("Error: fork n° {} is indicated as non-existent whereas it exists in database !", fork)); + }, + None => { + let mut free_fork = 0; + while forks.len() > free_fork && forks[free_fork] != ForkState::Free() { + free_fork += 1; + } + if free_fork >= *MAX_FORKS { + return (false, Vec::with_capacity(0), Vec::with_capacity(0)); + } + info!("BlockchainModule : New Isolate fork : {}", free_fork); + if free_fork == forks.len() { + forks.push(ForkState::Isolate()); + (forks.len() - 1, ForkState::Isolate()) + } else { + forks[free_fork] = ForkState::Isolate(); + (free_fork, ForkState::Isolate()) + } + } + }; + let mut isolate = true; + match fork_state { + ForkState::Full() => isolate = false, + ForkState::Isolate() => {} + ForkState::Free() => { + warn!("fork n° {} is indicated as free when it is not !", fork); + forks[fork] = ForkState::Isolate(); + } + } + match block { + &Block::NetworkBlock(network_block) => match network_block { + &NetworkBlock::V10(ref network_block_v10) => { + duniter_dal::writers::block::write_network_block( + &self.db, + &network_block_v10.uncompleted_block_doc, + fork, + isolate, + &network_block_v10.revoked, + &network_block_v10.certifications, + ) + } + _ => return (false, Vec::with_capacity(0), Vec::with_capacity(0)), + }, + &Block::LocalBlock(block_doc) => { + duniter_dal::writers::block::write(&self.db, &block_doc, fork, isolate) + } + }; + return (false, forks.to_vec(), Vec::with_capacity(0)); + } else { + debug!( + "stackable_block : block {} not chainable and already stored !", + block_doc.blockstamp() + ); + } + (false, Vec::with_capacity(0), Vec::with_capacity(0)) + } + /// Try stack up block + pub fn try_stack_up_block<W: WebOfTrust + Sync>( + &self, + network_block: &NetworkBlock, + wotb_index: &HashMap<ed25519::PublicKey, NodeId>, + wot: &W, + verif_level: SyncVerificationLevel, + ) -> (bool, Vec<DBWriteRequest>, Vec<WotEvent>) { + let block_doc = match complete_network_block( + &self.currency.to_string(), + Some(&self.db), + network_block, + verif_level.clone(), + ) { + Ok(block_doc) => block_doc, + Err(_) => return (false, Vec::with_capacity(0), Vec::with_capacity(0)), + }; + try_stack_up_completed_block::<W>(&block_doc, wotb_index, wot) + } + /// Start blockchain module. + pub fn start_blockchain(&mut self, blockchain_receiver: mpsc::Receiver<DuniterMessage>) -> () { + info!("BlockchainModule::start_blockchain()"); + + // Get wot path + let wot_path = duniter_conf::get_wot_path(self.conf_profile.clone(), &self.currency); + + // Get wotb index + let mut wotb_index: HashMap<ed25519::PublicKey, NodeId> = + DALIdentity::get_wotb_index(&self.db); + + // Open wot file + let (mut wot, mut _wot_blockstamp): (RustyWebOfTrust, Blockstamp) = if wot_path + .as_path() + .exists() + { + match WOT_FILE_FORMATER.from_file( + wot_path.as_path().to_str().unwrap(), + duniter_dal::constants::G1_PARAMS.sig_stock as usize, + ) { + Ok((wot, binary_blockstamp)) => match str::from_utf8(&binary_blockstamp) { + Ok(str_blockstamp) => (wot, Blockstamp::from_string(str_blockstamp).unwrap()), + Err(e) => panic!("Invalid UTF-8 sequence: {}", e), + }, + Err(e) => panic!("Fatal Error : fail to read wot file : {:?}", e), + } + } else { + ( + RustyWebOfTrust::new(duniter_dal::constants::G1_PARAMS.sig_stock as usize), + Blockstamp::default(), + ) + }; + + // Get forks + let mut forks: Vec<ForkState> = duniter_dal::block::get_forks(&self.db); + let mut last_get_stackables_blocks = UNIX_EPOCH; + let mut last_request_blocks = UNIX_EPOCH; + + // Get current block + let current_block: Option<BlockDocument> = duniter_dal::new_get_current_block(&self.db); + let mut current_blockstamp = match current_block.clone() { + Some(block) => block.blockstamp(), + None => Blockstamp::default(), + }; + + // Init datas + let mut pending_network_requests: HashMap<ModuleReqId, NetworkRequest> = HashMap::new(); + let mut consensus = Blockstamp::default(); + + loop { + let mut wot_events = Vec::new(); + // Request Consensus + let req = NetworkRequest::GetConsensus(ModuleReqFullId( + BlockchainModule::id(), + ModuleReqId(pending_network_requests.len() as u32), + )); + let req_id = self.request_network(req.clone()); + pending_network_requests.insert(req_id, req); + // Request Blocks + let now = SystemTime::now(); + if now.duration_since(last_request_blocks).unwrap() > Duration::new(20, 0) { + last_request_blocks = now; + // Request begin blocks + let to = match consensus.id.0 { + 0 => (current_blockstamp.id.0 + *MAX_BLOCKS_REQUEST), + _ => consensus.id.0, + }; + let new_pending_network_requests = self.request_blocks_to( + &pending_network_requests, + ¤t_blockstamp, + BlockId(to), + ); + for (new_req_id, new_req) in new_pending_network_requests { + pending_network_requests.insert(new_req_id, new_req); + } + // Request end blocks + if consensus.id.0 > (current_blockstamp.id.0 + 100) { + let mut req_id = ModuleReqId(0); + while pending_network_requests.contains_key(&req_id) { + req_id = ModuleReqId(req_id.0 + 1); + } + let from = consensus.id.0 - *CHUNK_SIZE - 1; + let (new_req_id, new_req) = self.request_chunk(&req_id, from); + pending_network_requests.insert(new_req_id, new_req); + } + } + match blockchain_receiver.recv_timeout(Duration::from_millis(1000)) { + Ok(ref message) => match message { + &DuniterMessage::Followers(ref new_followers) => { + info!("Blockchain module receive followers !"); + for new_follower in new_followers { + self.followers.push(new_follower.clone()); + } + } + &DuniterMessage::DALRequest(ref dal_request) => match dal_request { + &DALRequest::BlockchainRequest(ref blockchain_req) => { + match blockchain_req { + &DALReqBlockchain::CurrentBlock(ref requester_full_id) => { + debug!("BlockchainModule : receive DALReqBc::CurrentBlock()"); + + if let Some(current_block) = DALBlock::get_block( + &self.currency.to_string(), + &self.db, + ¤t_blockstamp, + ) { + debug!("BlockchainModule : send_req_response(CurrentBlock({}))", current_block.block.blockstamp()); + self.send_req_response(DALResponse::Blockchain( + DALResBlockchain::CurrentBlock( + requester_full_id.clone(), + current_block.block, + ), + )); + } else { + warn!("BlockchainModule : Req : fail to get current_block in bdd !"); + } + } + &DALReqBlockchain::UIDs(ref pubkeys) => { + self.send_req_response(DALResponse::Blockchain( + DALResBlockchain::UIDs( + pubkeys + .iter() + .map(|p| { + if let Some(wotb_id) = wotb_index.get(p) { + ( + p.clone(), + duniter_dal::get_uid( + &self.db, *wotb_id, + ), + ) + } else { + (p.clone(), None) + } + }) + .collect(), + ), + )); + } + _ => {} + } + } + _ => {} + }, + &DuniterMessage::NetworkEvent(ref network_event) => match network_event { + &NetworkEvent::ReceiveDocuments(ref network_docs) => { + let (new_current_blockstamp, mut new_wot_events) = self + .receive_network_documents( + network_docs, + ¤t_blockstamp, + &mut forks, + &wotb_index, + &wot, + ); + current_blockstamp = new_current_blockstamp; + wot_events.append(&mut new_wot_events); + } + &NetworkEvent::ReqResponse(ref network_response) => { + debug!("BlockchainModule : receive NetworkEvent::ReqResponse() !"); + if let Some(request) = + pending_network_requests.remove(&network_response.get_req_id()) + { + match request { + NetworkRequest::GetConsensus(_) => { + if let &NetworkResponse::Consensus(_, response) = + network_response.deref() + { + if let Ok(blockstamp) = response { + consensus = blockstamp.clone(); + } + } + } + NetworkRequest::GetBlocks(_, _, _, _) => { + if let &NetworkResponse::Chunk(_, _, ref blocks) = + network_response.deref() + { + let ( + new_current_blockstamp, + new_forks, + mut new_wot_events, + ) = self.receive_blocks( + blocks, + ¤t_blockstamp, + &forks, + &wotb_index, + &wot, + ); + current_blockstamp = new_current_blockstamp; + wot_events.append(&mut new_wot_events); + if !new_forks.is_empty() { + forks = new_forks; + } + } + } + _ => {} + } + } + } + _ => {} + }, + &DuniterMessage::ReceiveDocsFromClient(ref docs) => { + self.receive_documents(docs); + } + &DuniterMessage::Stop() => break, + _ => {} + }, + Err(e) => match e { + mpsc::RecvTimeoutError::Disconnected => { + panic!("Disconnected blockchain module !"); + } + mpsc::RecvTimeoutError::Timeout => {} + }, + } + // Write wot + BlockchainModule::apply_wot_events( + &wot_events, + &wot_path, + ¤t_blockstamp, + &mut wot, + &mut wotb_index, + ); + // Try to apply local stackable blocks + let mut wot_events = Vec::new(); + let now = SystemTime::now(); + if now.duration_since(last_get_stackables_blocks).unwrap() > Duration::new(20, 0) { + last_get_stackables_blocks = now; + loop { + let stackable_blocks = duniter_dal::block::DALBlock::get_stackables_blocks( + &self.currency.to_string(), + &self.db, + ¤t_blockstamp, + ); + if stackable_blocks.is_empty() { + break; + } else { + let mut find_valid_block = false; + for stackable_block in stackable_blocks { + debug!("stackable_block({})", stackable_block.block.number); + let (success, _new_forks, mut new_wot_events) = self.apply_block( + &Block::LocalBlock(&stackable_block.block), + ¤t_blockstamp, + &mut forks, + &wotb_index, + &wot, + ); + if success { + debug!( + "success to stackable_block({})", + stackable_block.block.number + ); + current_blockstamp = stackable_block.block.blockstamp(); + wot_events.append(&mut new_wot_events); + find_valid_block = true; + /*if !new_forks.is_empty() { + forks = new_forks; + }*/ + break; + } else { + warn!( + "DEBUG: fail to stackable_block({})", + stackable_block.block.number + ); + // Delete this fork + DALBlock::delete_fork(&self.db, stackable_block.fork); + forks[stackable_block.fork] = ForkState::Free(); + } + } + if !find_valid_block { + break; + } + } + } + // Print current_blockstamp + info!( + "BlockchainModule : current_blockstamp() = {:?}", + current_blockstamp + ); + } + // Write wot + BlockchainModule::apply_wot_events( + &wot_events, + &wot_path, + ¤t_blockstamp, + &mut wot, + &mut wotb_index, + ); + } + } + fn apply_wot_events<W: WebOfTrust + Sync>( + wot_events: &Vec<WotEvent>, + wot_path: &PathBuf, + current_blockstamp: &Blockstamp, + wot: &mut W, + wotb_index: &mut HashMap<ed25519::PublicKey, NodeId>, + ) { + if !wot_events.is_empty() { + for wot_event in wot_events { + match wot_event { + &WotEvent::AddNode(pubkey, wotb_id) => { + wot.add_node(); + wotb_index.insert(pubkey.clone(), wotb_id.clone()); + } + &WotEvent::RemNode(pubkey) => { + wot.rem_node(); + wotb_index.remove(&pubkey); + } + &WotEvent::AddLink(source, target) => { + wot.add_link(source.clone(), target.clone()); + } + &WotEvent::RemLink(source, target) => { + wot.rem_link(source.clone(), target.clone()); + } + &WotEvent::EnableNode(wotb_id) => { + wot.set_enabled(wotb_id.clone(), true); + } + &WotEvent::DisableNode(wotb_id) => { + wot.set_enabled(wotb_id.clone(), false); + } + } + } + // Save wot + WOT_FILE_FORMATER + .to_file( + wot, + current_blockstamp.to_string().as_bytes(), + wot_path.as_path().to_str().unwrap(), + ) + .expect("Fatal Error: Fail to write wotb in file !"); + } + } +} + +/// Complete Network Block +pub fn complete_network_block( + currency: &str, + db: Option<&DuniterDB>, + network_block: &NetworkBlock, + verif_level: SyncVerificationLevel, +) -> Result<BlockDocument, CompletedBlockError> { + if let &NetworkBlock::V10(ref network_block_v10) = network_block { + let mut block_doc = network_block_v10.uncompleted_block_doc.clone(); + trace!("complete_network_block #{}...", block_doc.number); + if verif_level == SyncVerificationLevel::Cautious() { + // Indexing block_identities + let mut block_identities = HashMap::new(); + block_doc + .identities + .iter() + .map(|idty| { + if idty.issuers().is_empty() { + panic!("idty without issuer !") + } + block_identities.insert(idty.issuers()[0], idty.clone()); + }) + .collect::<()>(); + block_doc.certifications = + duniter_dal::parsers::certifications::parse_certifications_from_json_value( + currency, + db.expect("complete_network_block() : Cautious mode need access to blockchain database !"), + &block_identities, + &network_block_v10.certifications, + ); + trace!("Success to complete certs."); + block_doc.revoked = duniter_dal::parsers::revoked::parse_revocations_from_json_value( + currency, + db.expect( + "complete_network_block() : Cautious mode need access to blockchain database !", + ), + &block_identities, + &network_block_v10.revoked, + ); + } else { + block_doc.certifications = + duniter_dal::parsers::certifications::parse_certifications_into_compact( + &network_block_v10.certifications, + ); + trace!("Success to complete certs."); + block_doc.revoked = duniter_dal::parsers::revoked::parse_revocations_into_compact( + &network_block_v10.revoked, + ); + } + trace!("Success to complete certs & revocations."); + // In cautions mode, verify all signatures ! + if verif_level == SyncVerificationLevel::Cautious() { + for idty in block_doc.clone().identities { + if idty.verify_signatures() != VerificationResult::Valid() { + error!( + "Fail to sync block #{} : Idty with invalid singature !", + block_doc.number + ); + panic!("Idty with invalid singature !"); + } + } + } + let inner_hash = block_doc.inner_hash.expect( + "BlockchainModule : complete_network_block() : fatal error : block.inner_hash = None", + ); + if block_doc.number.0 > 0 { + block_doc.compute_inner_hash(); + } + let hash = block_doc.hash; + block_doc.compute_hash(); + if block_doc.inner_hash.expect( + "BlockchainModule : complete_network_block() : fatal error : block.inner_hash = None", + ) == inner_hash + { + let nonce = block_doc.nonce; + block_doc.change_nonce(nonce); + if verif_level == SyncVerificationLevel::FastSync() + || block_doc.verify_signatures() == VerificationResult::Valid() + || block_doc.number.0 <= 1 + { + if block_doc.hash == hash { + trace!("Succes to complete_network_block #{}", block_doc.number.0); + Ok(block_doc) + } else { + warn!("BlockchainModule : Refuse Bloc : invalid hash !"); + Err(CompletedBlockError::InvalidHash()) + } + } else { + warn!("BlockchainModule : Refuse Bloc : invalid signature !"); + Err(CompletedBlockError::InvalidSig()) + } + } else { + warn!("BlockchainModule : Refuse Bloc : invalid inner hash !"); + debug!( + "BlockInnerFormat={}", + block_doc.generate_compact_inner_text() + ); + Err(CompletedBlockError::InvalidInnerHash()) + } + } else { + Err(CompletedBlockError::InvalidVersion()) + } +} diff --git a/blockchain/stack_up_block.rs b/blockchain/stack_up_block.rs new file mode 100644 index 0000000000000000000000000000000000000000..bcc7b15d316dc45d6afde7600e9d00a4fa84d934 --- /dev/null +++ b/blockchain/stack_up_block.rs @@ -0,0 +1,178 @@ +// Copyright (C) 2018 The Duniter Project Developers. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +extern crate duniter_crypto; +extern crate duniter_dal; +extern crate duniter_documents; +extern crate duniter_wotb; + +use duniter_crypto::keys::ed25519; +use duniter_dal::block::{DALBlock, WotEvent}; +use duniter_dal::writers::requests::DBWriteRequest; +use duniter_documents::blockchain::v10::documents::BlockDocument; +use duniter_documents::blockchain::Document; +use duniter_wotb::{NodeId, WebOfTrust}; + +use std::collections::HashMap; + +pub fn try_stack_up_completed_block<W: WebOfTrust + Sync>( + block: &BlockDocument, + wotb_index: &HashMap<ed25519::PublicKey, NodeId>, + wot: &W, +) -> (bool, Vec<DBWriteRequest>, Vec<WotEvent>) { + debug!( + "BlockchainModule : try stack up complete block {}", + block.blockstamp() + ); + let mut db_requests = Vec::new(); + let mut wot_events = Vec::new(); + let mut wot_copy: W = wot.clone(); + let mut wotb_index_copy: HashMap<ed25519::PublicKey, NodeId> = wotb_index.clone(); + let current_blockstamp = block.blockstamp(); + let mut identities = HashMap::with_capacity(block.identities.len()); + for identity in block.identities.clone() { + identities.insert(identity.issuers()[0], identity); + } + for joiner in block.joiners.clone() { + let pubkey = joiner.clone().issuers()[0]; + if let Some(idty_doc) = identities.get(&pubkey) { + // Newcomer + let wotb_id = NodeId(wot_copy.size()); + wot_events.push(WotEvent::AddNode(pubkey, wotb_id)); + wot_copy.add_node(); + wotb_index_copy.insert(pubkey, wotb_id); + db_requests.push(DBWriteRequest::CreateIdentity( + wotb_id, + current_blockstamp.clone(), + block.median_time, + idty_doc.clone(), + )); + } else { + // Renewer + let wotb_id = wotb_index_copy[&joiner.issuers()[0]]; + wot_events.push(WotEvent::EnableNode(wotb_id)); + wot_copy.set_enabled(wotb_id, true); + db_requests.push(DBWriteRequest::RenewalIdentity( + joiner.issuers()[0], + block.blockstamp(), + block.median_time, + )); + } + } + for active in block.actives.clone() { + let pubkey = active.issuers()[0]; + if !identities.contains_key(&pubkey) { + let wotb_id = wotb_index_copy[&pubkey]; + wot_events.push(WotEvent::EnableNode(wotb_id)); + wot_copy.set_enabled(wotb_id, true); + db_requests.push(DBWriteRequest::RenewalIdentity( + pubkey, + block.blockstamp(), + block.median_time, + )); + } + } + for exclusion in block.excluded.clone() { + let wotb_id = wotb_index_copy[&exclusion]; + wot_events.push(WotEvent::DisableNode(wotb_id)); + wot_copy.set_enabled(wotb_id, false); + db_requests.push(DBWriteRequest::ExcludeIdentity( + wotb_id, + block.blockstamp(), + block.median_time, + )); + } + for revocation in block.revoked.clone() { + let compact_revoc = revocation.to_compact_document(); + let wotb_id = wotb_index_copy[&compact_revoc.issuer]; + wot_events.push(WotEvent::DisableNode(wotb_id)); + wot_copy.set_enabled(wotb_id, false); + db_requests.push(DBWriteRequest::RevokeIdentity( + wotb_id, + block.blockstamp(), + block.median_time, + )); + } + for certification in block.certifications.clone() { + trace!("try_stack_up_completed_block: apply cert..."); + let compact_cert = certification.to_compact_document(); + let wotb_node_from = wotb_index_copy[&compact_cert.issuer]; + let wotb_node_to = wotb_index_copy[&compact_cert.target]; + wot_events.push(WotEvent::AddLink(wotb_node_from, wotb_node_to)); + wot_copy.add_link(wotb_node_from, wotb_node_to); + db_requests.push(DBWriteRequest::CreateCert( + block.blockstamp(), + block.median_time, + compact_cert, + )); + trace!("try_stack_up_completed_block: apply cert...success."); + } + + /*// Calculate the state of the wot + if !wot_events.is_empty() && verif_level != SyncVerificationLevel::FastSync() { + // Calculate sentries_count + let sentries_count = wot_copy.get_sentries(3).len(); + // Calculate average_density + let average_density = calculate_average_density::<W>(&wot_copy); + let sentry_requirement = + get_sentry_requirement(block.members_count, G1_PARAMS.step_max); + // Calculate distances and connectivities + let (average_distance, distances, average_connectivity, connectivities) = + compute_distances::<W>( + &wot_copy, + sentry_requirement, + G1_PARAMS.step_max, + G1_PARAMS.x_percent, + ); + // Calculate centralities and average_centrality + let centralities = + calculate_distance_stress_centralities::<W>(&wot_copy, G1_PARAMS.step_max); + let average_centrality = + (centralities.iter().sum::<u64>() as f64 / centralities.len() as f64) as usize; + // Register the state of the wot + duniter_dal::register_wot_state( + db, + &WotState { + block_number: block.number.0, + block_hash: block.hash.unwrap().to_string(), + sentries_count, + average_density, + average_distance, + distances, + average_connectivity, + connectivities: connectivities + .iter() + .map(|c| { + if *c > *G1_CONNECTIVITY_MAX { + *G1_CONNECTIVITY_MAX + } else { + *c + } + }) + .collect(), + average_centrality, + centralities, + }, + ); + }*/ + // Write block in bdd + db_requests.push(DBWriteRequest::WriteBlock(DALBlock { + block: block.clone(), + fork: 0, + isolate: false, + })); + + (true, db_requests, wot_events) +} diff --git a/blockchain/sync.rs b/blockchain/sync.rs new file mode 100644 index 0000000000000000000000000000000000000000..3fc1000d64d2fbd60bafc8d5b36d3d13fc09b724 --- /dev/null +++ b/blockchain/sync.rs @@ -0,0 +1,504 @@ +// Copyright (C) 2018 The Duniter Project Developers. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +extern crate duniter_conf; +extern crate duniter_crypto; +extern crate duniter_dal; +extern crate duniter_documents; +extern crate duniter_message; +extern crate duniter_module; +extern crate duniter_network; +extern crate pbr; +extern crate serde; +extern crate serde_json; +extern crate sqlite; + +use self::pbr::ProgressBar; +use duniter_crypto::keys::{ed25519, PublicKey, Signature}; +use duniter_dal::parsers::identities::parse_compact_identity; +use duniter_dal::parsers::transactions::parse_transaction; +//use duniter_dal::writers::requests::DBWriteRequest; +use duniter_documents::blockchain::v10::documents::membership::MembershipType; +use duniter_documents::blockchain::v10::documents::BlockDocument; +use duniter_documents::{BlockHash, BlockId, Hash}; +use duniter_network::{NetworkBlock, NetworkBlockV10}; +use duniter_wotb::{NodeId, WebOfTrust}; +use std::collections::HashMap; +use std::fs; +use std::sync::mpsc; +use std::thread; +use std::time::SystemTime; + +use super::*; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BlockHeader { + pub number: BlockId, + pub hash: BlockHash, + pub issuer: ed25519::PublicKey, +} + +enum ParserWorkMess { + TargetBlockstamp(Blockstamp), + NetworkBlock(NetworkBlock), + //DBWriteRequest(DBWriteRequest), + End(), +} + +pub fn sync_ts( + conf: &DuniterConf, + current_blockstamp: &Blockstamp, + db_ts_path: PathBuf, + cautious: bool, +) { + // get profile and currency and current_blockstamp + let profile = &conf.profile(); + let currency = &conf.currency(); + let mut current_blockstamp = current_blockstamp.clone(); + + // Copy blockchain db in ramfs + let db_path = duniter_conf::get_db_path(profile, currency, false); + if db_path.as_path().exists() { + info!("Copy blockchain DB in ramfs..."); + fs::copy(db_path, format!("/dev/shm/{}_durs.db", profile)) + .expect("Fatal error : fail to copy DB in ramfs !"); + } + + // Get wot path + let wot_path = duniter_conf::get_wot_path(profile.clone().to_string(), currency); + + // Open wot file + let (mut wot, mut _wot_blockstamp): (RustyWebOfTrust, Blockstamp) = + if wot_path.as_path().exists() { + match WOT_FILE_FORMATER.from_file( + wot_path.as_path().to_str().unwrap(), + duniter_dal::constants::G1_PARAMS.sig_stock as usize, + ) { + Ok((wot, binary_blockstamp)) => match str::from_utf8(&binary_blockstamp) { + Ok(str_blockstamp) => (wot, Blockstamp::from_string(str_blockstamp).unwrap()), + Err(e) => panic!("Invalid UTF-8 sequence: {}", e), + }, + Err(e) => panic!("Fatal Error : fail te read wot file : {:?}", e), + } + } else { + ( + RustyWebOfTrust::new(duniter_dal::constants::G1_PARAMS.sig_stock as usize), + Blockstamp::default(), + ) + }; + + // Get verification level + let verif_level = if cautious { + println!("Start cautious sync..."); + info!("Start cautious sync..."); + SyncVerificationLevel::Cautious() + } else { + println!("Start fast sync..."); + info!("Start fast sync..."); + SyncVerificationLevel::FastSync() + }; + + // Create sync_thread channel + let (sender_sync_thread, recv_sync_thread) = mpsc::channel(); + + // Lauch ts thread + thread::spawn(move || { + // open db_ts + let ts_db = sqlite::open(db_ts_path.as_path()) + .expect("Fatal error : fail to open duniter-ts database !"); + info!("sync_ts : Success to open duniter-ts database."); + + // Get ts current blockstamp + debug!("Get ts-db current blockstamp..."); + let mut cursor: sqlite::Cursor = ts_db + .prepare("SELECT hash, number FROM block WHERE fork=? ORDER BY number DESC LIMIT 1;") + .expect("Request SQL get_ts_current_block is wrong !") + .cursor(); + cursor + .bind(&[sqlite::Value::Integer(0)]) + .expect("Fail to get ts current block !"); + let current_ts_blockstamp = if let Some(row) = cursor.next().expect("cursor error") { + let block_id = BlockId(row[1] + .as_integer() + .expect("Fail to parse current ts blockstamp !") + as u32); + let block_hash = BlockHash( + Hash::from_hex( + row[0] + .as_string() + .expect("Fail to parse current ts blockstamp !"), + ).expect("Fail to parse current ts blockstamp !"), + ); + Blockstamp { + id: block_id, + hash: block_hash, + } + } else { + panic!("Fail to get current ts blockstamp !"); + }; + debug!("Success to ts-db current blockstamp."); + + // Send ts current blockstamp + sender_sync_thread + .send(ParserWorkMess::TargetBlockstamp(current_ts_blockstamp)) + .expect("Fatal error : sync_thread unrechable !"); + + // Get genesis block + if current_blockstamp == Blockstamp::default() { + let mut cursor: sqlite::Cursor = ts_db + .prepare( + "SELECT hash, inner_hash, signature, currency, issuer, parameters, previousHash, + previousIssuer, version, membersCount, monetaryMass, medianTime, dividend, unitbase, + time, powMin, number, nonce, transactions, certifications, identities, joiners, + actives, leavers, revoked, excluded, issuersFrame, issuersFrameVar, issuersCount + FROM block WHERE fork=0 AND number=? LIMIT 1;", + ) + .expect("Request SQL get_ts_blocks is wrong !") + .cursor(); + cursor + .bind(&[sqlite::Value::Integer(0)]) + .expect("Fail to get genesis block !"); + if let Some(row) = cursor.next().expect("cursor error") { + sender_sync_thread + .send(ParserWorkMess::NetworkBlock(parse_ts_block(row))) + .expect("Fatal error : sync_thread unrechable !"); + } + } + + // Request ts blocks + let mut cursor: sqlite::Cursor = ts_db + .prepare( + "SELECT hash, inner_hash, signature, currency, issuer, parameters, previousHash, + previousIssuer, version, membersCount, monetaryMass, medianTime, dividend, unitbase, + time, powMin, number, nonce, transactions, certifications, identities, joiners, + actives, leavers, revoked, excluded, issuersFrame, issuersFrameVar, issuersCount + FROM block WHERE fork=? AND number > ? ORDER BY number ASC;", + ) + .expect("Request SQL get_ts_blocks is wrong !") + .cursor(); + cursor + .bind(&[ + sqlite::Value::Integer(0), + sqlite::Value::Integer(current_blockstamp.id.0 as i64), + ]) + .expect("0"); + + // Parse ts blocks + //let mut ts_blocks = Vec::with_capacity(current_ts_blockstamp.id.0 + 1); + //let pool = ThreadPool::new(4); + while let Some(row) = cursor.next().expect("cursor error") { + //let sender_sync_thread_clone = sender_sync_thread.clone(); + //pool.execute(move || { + sender_sync_thread + .send(ParserWorkMess::NetworkBlock(parse_ts_block(row))) + .expect("Fatal error : sync_thread unrechable !"); + //}); + } + sender_sync_thread + .send(ParserWorkMess::End()) + .expect("Fatal error : sync_thread unrechable !"); + }); + + // Get target blockstamp + let target_blockstamp = + if let Ok(ParserWorkMess::TargetBlockstamp(target_blockstamp)) = recv_sync_thread.recv() { + target_blockstamp + } else { + panic!("Fatal error : no TargetBlockstamp !") + }; + + // Instanciate blockchain module + let blockchain_module = + BlockchainModule::load_blockchain_conf(conf, RequiredKeysContent::None(), true); + + // Node is already synchronized ? + if target_blockstamp.id.0 < current_blockstamp.id.0 { + println!("Your duniter-rs node is already synchronized."); + return; + } + + // Get wotb index + let mut wotb_index: HashMap<ed25519::PublicKey, NodeId> = + DALIdentity::get_wotb_index(&blockchain_module.db); + + // Start sync + let sync_start_time = SystemTime::now(); + println!( + "Sync from #{} to #{} :", + current_blockstamp.id.0, target_blockstamp.id.0 + ); + info!( + "Sync from #{} to #{}...", + current_blockstamp.id.0, target_blockstamp.id.0 + ); + let mut pb = ProgressBar::new((target_blockstamp.id.0 + 1 - current_blockstamp.id.0).into()); + + // Apply blocks + while let Ok(ParserWorkMess::NetworkBlock(network_block)) = recv_sync_thread.recv() { + // Complete block + let block_doc = match complete_network_block( + &blockchain_module.currency.to_string(), + None, + &network_block, + SyncVerificationLevel::FastSync(), + ) { + Ok(block_doc) => block_doc, + Err(_) => panic!("Receive wrong block, please reset data and resync !"), + }; + // Apply block + let (success, db_requests, new_wot_events) = + try_stack_up_completed_block::<RustyWebOfTrust>(&block_doc, &wotb_index, &wot); + + blockchain_module.try_stack_up_block::<RustyWebOfTrust>( + &network_block, + &wotb_index, + &wot, + verif_level, + ); + if success { + current_blockstamp = network_block.blockstamp(); + debug!("Apply db requests..."); + // Apply db requests + db_requests + .iter() + .map(|req| req.apply(&conf.currency().to_string(), &blockchain_module.db)) + .collect::<()>(); + debug!("Finish to apply db requests."); + // Apply WotEvents + if !new_wot_events.is_empty() { + for wot_event in new_wot_events { + match wot_event { + WotEvent::AddNode(pubkey, wotb_id) => { + wot.add_node(); + wotb_index.insert(pubkey, wotb_id); + } + WotEvent::RemNode(pubkey) => { + wot.rem_node(); + wotb_index.remove(&pubkey); + } + WotEvent::AddLink(source, target) => { + wot.add_link(source, target); + } + WotEvent::RemLink(source, target) => { + wot.rem_link(source, target); + } + WotEvent::EnableNode(wotb_id) => { + wot.set_enabled(wotb_id, true); + } + WotEvent::DisableNode(wotb_id) => { + wot.set_enabled(wotb_id, false); + } + } + } + if current_blockstamp.id.0 > target_blockstamp.id.0 - 100 { + // Save wot file + WOT_FILE_FORMATER + .to_file( + &wot, + current_blockstamp.to_string().as_bytes(), + wot_path.as_path().to_str().unwrap(), + ) + .expect("Fatal Error: Fail to write wotb in file !"); + } + } + pb.inc(); + debug!("Success to apply block #{}", current_blockstamp.id.0); + if current_blockstamp.id.0 >= target_blockstamp.id.0 { + if current_blockstamp == target_blockstamp { + // Sync completed + break; + } else { + panic!("Fatal Error : we get a fork, please reset data and sync again !"); + } + } + } else { + panic!( + "Fatal error : fail to stack up block #{}", + current_blockstamp.id.0 + 1 + ) + } + } + + // Copy memory db to real db + info!("Save blockchain DB in profile folder..."); + fs::copy( + format!("/dev/shm/{}_durs.db", profile), + duniter_conf::get_db_path(profile, currency, false).as_path(), + ).expect("Fatal error : fail to copy DB in profile folder !"); + + // Remove memory db + fs::remove_file(format!("/dev/shm/{}_durs.db", profile)) + .expect("Fatal error : fail to remove memory DB !"); + + // Print sync duration + let sync_duration = SystemTime::now().duration_since(sync_start_time).unwrap(); + println!( + "Sync {} blocks in {}m {}s.", + current_blockstamp.id.0, + sync_duration.as_secs() / 60, + sync_duration.as_secs() % 60, + ); +} + +pub fn parse_ts_block(row: &[sqlite::Value]) -> NetworkBlock { + // Parse block + let current_header = BlockHeader { + number: BlockId(row[16].as_integer().expect("Fail to parse block number") as u32), + hash: BlockHash( + Hash::from_hex(row[0].as_string().expect("Fail to parse block hash")) + .expect("Fail to parse block hash (2)"), + ), + issuer: PublicKey::from_base58(row[4].as_string().expect("Fail to parse block issuer")) + .expect("Failt to parse block issuer (2)"), + }; + let previous_header = if current_header.number.0 > 0 { + Some(BlockHeader { + number: BlockId(current_header.number.0 - 1), + hash: BlockHash( + Hash::from_hex( + row[6] + .as_string() + .expect("Fail to parse block previous hash"), + ).expect("Fail to parse block previous hash (2)"), + ), + issuer: PublicKey::from_base58( + row[7] + .as_string() + .expect("Fail to parse previous block issuer"), + ).expect("Fail to parse previous block issuer (2)"), + }) + } else { + None + }; + let currency = row[3].as_string().expect("Fail to parse currency"); + let dividend = match row[12].as_integer() { + Some(dividend) => Some(dividend as usize), + None => None, + }; + let json_identities: serde_json::Value = serde_json::from_str( + row[20].as_string().expect("Fail to parse block identities"), + ).expect("Fail to parse block identities (2)"); + let mut identities = Vec::new(); + for raw_idty in json_identities + .as_array() + .expect("Fail to parse block identities (3)") + { + identities + .push(parse_compact_identity(¤cy, &raw_idty).expect("Fail to parse block idty")); + } + let json_txs: serde_json::Value = serde_json::from_str( + row[18].as_string().expect("Fail to parse block txs"), + ).expect("Fail to parse block txs (2)"); + let mut transactions = Vec::new(); + for json_tx in json_txs.as_array().expect("Fail to parse block txs (3)") { + transactions.push(parse_transaction(currency, &json_tx).expect("Fail to parse block tx")); + } + let previous_hash = match previous_header.clone() { + Some(previous_header_) => previous_header_.hash.0, + None => Hash::default(), + }; + let previous_issuer = match previous_header { + Some(previous_header_) => Some(previous_header_.issuer), + None => None, + }; + let excluded: serde_json::Value = serde_json::from_str( + row[25].as_string().expect("Fail to parse excluded"), + ).expect("Fail to parse excluded (2)"); + let uncompleted_block_doc = BlockDocument { + nonce: row[17].as_integer().expect("Fail to parse nonce") as u64, + number: current_header.number, + pow_min: row[15].as_integer().expect("Fail to parse pow_min") as usize, + time: row[14].as_integer().expect("Fail to parse time") as u64, + median_time: row[11].as_integer().expect("Fail to parse median_time") as u64, + members_count: row[9].as_integer().expect("Fail to parse members_count") as usize, + monetary_mass: row[10] + .as_string() + .expect("Fail to parse monetary_mass") + .parse() + .expect("Fail to parse monetary_mass (2)"), + unit_base: row[13].as_integer().expect("Fail to parse unit_base") as usize, + issuers_count: row[28].as_integer().expect("Fail to parse issuers_count") as usize, + issuers_frame: row[26].as_integer().expect("Fail to parse issuers_frame") as isize, + issuers_frame_var: row[27] + .as_integer() + .expect("Fail to parse issuers_frame_var") as isize, + currency: String::from(currency), + issuers: vec![ + PublicKey::from_base58(row[4].as_string().expect("Fail to parse issuer")) + .expect("Fail to parse issuer '2)"), + ], + signatures: vec![ + Signature::from_base64(row[2].as_string().expect("Fail to parse signature")) + .expect("Fail to parse signature (2)"), + ], + hash: Some(current_header.hash), + parameters: None, + previous_hash, + previous_issuer, + inner_hash: Some( + Hash::from_hex(row[1].as_string().expect("Fail to parse block inner_hash")) + .expect("Fail to parse block inner_hash (2)"), + ), + dividend: dividend, + identities, + joiners: duniter_dal::parsers::memberships::parse_memberships( + currency, + MembershipType::In(), + row[21].as_string().expect("Fail to parse joiners"), + ).expect("Fail to parse joiners (2)"), + actives: duniter_dal::parsers::memberships::parse_memberships( + currency, + MembershipType::In(), + row[22].as_string().expect("Fail to parse actives"), + ).expect("Fail to parse actives (2)"), + leavers: duniter_dal::parsers::memberships::parse_memberships( + currency, + MembershipType::In(), + row[23].as_string().expect("Fail to parse leavers"), + ).expect("Fail to parse leavers (2)"), + revoked: Vec::new(), + excluded: excluded + .as_array() + .expect("Fail to parse excluded (3)") + .to_vec() + .into_iter() + .map(|e| { + PublicKey::from_base58(e.as_str().expect("Fail to parse excluded (4)")) + .expect("Fail to parse excluded (5)") + }) + .collect(), + certifications: Vec::new(), + transactions, + inner_hash_and_nonce_str: String::new(), + }; + let revoked: serde_json::Value = serde_json::from_str( + row[24].as_string().expect("Fail to parse revoked"), + ).expect("Fail to parse revoked (2)"); + let certifications: serde_json::Value = serde_json::from_str( + row[19].as_string().expect("Fail to parse certifications"), + ).expect("Fail to parse certifications (2)"); + // return NetworkBlock + NetworkBlock::V10(Box::new(NetworkBlockV10 { + uncompleted_block_doc, + revoked: revoked + .as_array() + .expect("Fail to parse revoked (3)") + .to_vec(), + certifications: certifications + .as_array() + .expect("Fail to parse certifications (3)") + .to_vec(), + })) +} diff --git a/conf/Cargo.toml b/conf/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..433ccf6dd0c3e14b17ba95a83574c6a45fb612de --- /dev/null +++ b/conf/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "duniter-conf" +version = "0.1.0" +authors = ["librelois <elois@ifee.fr>"] +description = "Configuration module for the Duniter project." +license = "AGPL-3.0" + +[lib] +path = "lib.rs" + +[dependencies] +rand = "0.4.2" +serde = "1.0.24" +serde_derive = "1.0.24" +serde_json = "1.0.9" +duniter-crypto = { path = "../crypto" } +duniter-module = { path = "../module" } + +[features] +# Treat warnings as a build error. +strict = [] \ No newline at end of file diff --git a/conf/lib.rs b/conf/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..5994cc80fa3148feeca9ad002ccf991752e534c4 --- /dev/null +++ b/conf/lib.rs @@ -0,0 +1,380 @@ +// Copyright (C) 2018 The Duniter Project Developers. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! Defined the few global types used by all modules, +//! as well as the DuniterModule trait that all modules must implement. + +#![cfg_attr(feature = "strict", deny(warnings))] +#![deny( + missing_docs, missing_debug_implementations, missing_copy_implementations, trivial_casts, + trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces, + unused_qualifications +)] + +#[macro_use] +extern crate serde_json; + +extern crate duniter_crypto; +extern crate duniter_module; +extern crate rand; +extern crate serde; +use duniter_crypto::keys::{ed25519, KeyPair, PrivateKey, PublicKey}; +use duniter_module::{Currency, DuniterConf, DuniterConfV1, RequiredKeys, RequiredKeysContent}; +use rand::Rng; +use serde::ser::{Serialize, SerializeStruct, Serializer}; +use std::env; +use std::fs; +use std::fs::File; +use std::io::prelude::*; +use std::path::PathBuf; + +static USER_DATAS_FOLDER: &'static str = "durs-dev"; + +/// If no currency is specified by the user, is the currency will be chosen by default +pub static DEFAULT_CURRRENCY: &'static str = "g1"; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +/// Keypairs filled in by the user (via a file or by direct entry in the terminal). +pub struct DuniterKeyPairs { + /// Keypair used by the node to sign its communications with other nodes. This keypair is mandatory, if it's not filled in, a random keypair is generated. + pub network_keypair: ed25519::KeyPair, + /// Keypair used to sign the blocks forged by this node. If this keypair is'nt filled in, the node will not calculate blocks. + pub member_keypair: Option<ed25519::KeyPair>, +} + +impl Serialize for DuniterKeyPairs { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let member_sec = if let Some(member_keypair) = self.member_keypair { + member_keypair.private_key().to_string() + } else { + String::from("") + }; + let member_pub = if let Some(member_keypair) = self.member_keypair { + member_keypair.pubkey.to_string() + } else { + String::from("") + }; + let mut state = serializer.serialize_struct("DuniterKeyPairs", 4)?; + state.serialize_field( + "network_sec", + &self.network_keypair.private_key().to_string().as_str(), + )?; + state.serialize_field( + "network_pub", + &self.network_keypair.pubkey.to_string().as_str(), + )?; + state.serialize_field("member_sec", member_sec.as_str())?; + state.serialize_field("member_pub", member_pub.as_str())?; + state.end() + } +} + +impl DuniterKeyPairs { + /// Returns only the keys indicated as required + pub fn get_required_keys_content( + required_keys: RequiredKeys, + keypairs: DuniterKeyPairs, + ) -> RequiredKeysContent<ed25519::KeyPair> { + match required_keys { + RequiredKeys::MemberKeyPair() => { + RequiredKeysContent::MemberKeyPair(keypairs.member_keypair) + } + RequiredKeys::MemberPublicKey() => { + RequiredKeysContent::MemberPublicKey(if let Some(keys) = keypairs.member_keypair { + Some(keys.pubkey) + } else { + None + }) + } + RequiredKeys::NetworkKeyPair() => { + RequiredKeysContent::NetworkKeyPair(keypairs.network_keypair) + } + RequiredKeys::NetworkPublicKey() => { + RequiredKeysContent::NetworkPublicKey(keypairs.network_keypair.pubkey) + } + RequiredKeys::None() => RequiredKeysContent::None(), + } + } +} + +fn _use_json_macro() -> serde_json::Value { + json!({}) +} + +fn generate_random_keypair() -> ed25519::KeyPair { + let mut rng = rand::thread_rng(); + let generator = ed25519::KeyPairFromSaltedPasswordGenerator::with_default_parameters(); + generator.generate(&[rng.gen::<u8>(); 8], &[rng.gen::<u8>(); 8]) +} + +fn generate_random_node_id() -> u32 { + let mut rng = rand::thread_rng(); + rng.gen::<u32>() +} + +/// Return the user datas folder name +pub fn get_user_datas_folder() -> &'static str { + USER_DATAS_FOLDER +} + +/// Returns the path to the folder containing the user data of the running profile +pub fn datas_path(profile: &str, currency: &Currency) -> PathBuf { + let mut datas_path = match env::home_dir() { + Some(path) => path, + None => panic!("Impossible to get your home dir!"), + }; + datas_path.push(".config/"); + datas_path.push(USER_DATAS_FOLDER); + datas_path.push(profile); + datas_path.push(currency.to_string()); + datas_path +} + +/// Load configuration. +pub fn load_conf(profile: &str) -> (DuniterConf, DuniterKeyPairs) { + // Define and create datas directory if not exist + let mut profile_path = match env::home_dir() { + Some(path) => path, + None => panic!("Impossible to get your home dir !"), + }; + profile_path.push(".config/"); + if !profile_path.as_path().exists() { + fs::create_dir(profile_path.as_path()).expect("Impossible to create ~/.config dir !"); + } + profile_path.push(USER_DATAS_FOLDER); + if !profile_path.as_path().exists() { + fs::create_dir(profile_path.as_path()).expect(&format!( + "Impossible to create ~/.config/{} dir !", + USER_DATAS_FOLDER + )); + } + profile_path.push(profile); + if !profile_path.as_path().exists() { + fs::create_dir(profile_path.as_path()).expect("Impossible to create your profile dir !"); + } + + // Load conf + let (conf, keypairs) = load_conf_at_path(profile, &profile_path); + + // Create currency dir + profile_path.push(conf.currency().to_string()); + if !profile_path.as_path().exists() { + fs::create_dir(profile_path.as_path()).expect("Impossible to create currency dir !"); + } + + // Return conf and keypairs + (conf, keypairs) +} + +/// Load configuration. at specified path +pub fn load_conf_at_path(profile: &str, profile_path: &PathBuf) -> (DuniterConf, DuniterKeyPairs) { + // Default conf + let mut conf = DuniterConfV1 { + profile: String::from(profile), + currency: Currency::Str(DEFAULT_CURRRENCY.to_string()), + my_node_id: generate_random_node_id(), + modules: serde_json::Value::Null, + }; + + // Get KeyPairs + let mut keypairs_path = profile_path.clone(); + keypairs_path.push("keypairs.json"); + let keypairs = if keypairs_path.as_path().exists() { + if let Ok(mut f) = File::open(keypairs_path.as_path()) { + let mut contents = String::new(); + if f.read_to_string(&mut contents).is_ok() { + let json_conf: serde_json::Value = + serde_json::from_str(&contents).expect("Conf: Fail to parse keypairs file !"); + if let Some(network_sec) = json_conf.get("network_sec") { + if let Some(network_pub) = json_conf.get("network_pub") { + let network_sec = network_sec + .as_str() + .expect("Conf: Fail to parse keypairs file !"); + let network_pub = network_pub + .as_str() + .expect("Conf: Fail to parse keypairs file !"); + DuniterKeyPairs { + network_keypair: ed25519::KeyPair { + privkey: PrivateKey::from_base58(network_sec) + .expect("conf : keypairs file : fail to parse network_sec !"), + pubkey: PublicKey::from_base58(network_pub) + .expect("conf : keypairs file : fail to parse network_pub !"), + }, + member_keypair: None, + } + } else { + panic!("Fatal error : keypairs file wrong format : no field salt !") + } + } else { + panic!("Fatal error : keypairs file wrong format : no field password !") + } + } else { + panic!("Fail to read keypairs file !"); + } + } else { + panic!("Fail to open keypairs file !"); + } + } else { + // Create keypairs file with random keypair + let keypairs = DuniterKeyPairs { + network_keypair: generate_random_keypair(), + member_keypair: None, + }; + write_keypairs_file(&keypairs_path, &keypairs) + .expect("Fatal error : fail to write default keypairs file !"); + keypairs + }; + + // Open conf file + let mut conf_path = profile_path.clone(); + conf_path.push("conf.json"); + if conf_path.as_path().exists() { + if let Ok(mut f) = File::open(conf_path.as_path()) { + let mut contents = String::new(); + if f.read_to_string(&mut contents).is_ok() { + let json_conf: serde_json::Value = + serde_json::from_str(&contents).expect("Conf: Fail to parse conf file !"); + if let Some(currency) = json_conf.get("currency") { + conf.currency = Currency::Str( + currency + .as_str() + .expect("Conf: fail to parse currency field !") + .to_string(), + ); + }; + if let Some(node_id) = json_conf.get("node_id") { + conf.my_node_id = + u32::from_str_radix( + node_id + .as_str() + .expect("Conf : fail to parse node_id field !"), + 16, + ).expect("Fail to load conf: node_id must be an hexadecimal number !"); + }; + if let Some(modules_conf) = json_conf.get("modules") { + conf.modules = modules_conf.clone(); + }; + } + } else { + panic!("Fail to open conf file !"); + } + } else { + // Create conf file with default conf + write_conf_file(&conf_path, &DuniterConf::V1(conf.clone())) + .expect("Fatal error : fail to write default conf file !"); + } + + // Return conf and keypairs + (DuniterConf::V1(conf), keypairs) +} + +/// Save keypairs in profile folder +pub fn write_keypairs_file( + file_path: &PathBuf, + keypairs: &DuniterKeyPairs, +) -> Result<(), std::io::Error> { + let mut f = try!(File::create(file_path.as_path())); + try!( + f.write_all( + serde_json::to_string_pretty(keypairs) + .expect("Fatal error : fail to write default keypairs file !") + .as_bytes() + ) + ); + try!(f.sync_all()); + Ok(()) +} + +/// Save configuration in profile folder +pub fn write_conf_file(file_path: &PathBuf, conf: &DuniterConf) -> Result<(), std::io::Error> { + match *conf { + DuniterConf::V1(ref conf_v1) => { + let mut f = try!(File::create(file_path.as_path())); + try!( + f.write_all( + serde_json::to_string_pretty(conf_v1) + .expect("Fatal error : fail to write default conf file !") + .as_bytes() + ) + ); + try!(f.sync_all()); + } + _ => { + panic!("Fatal error : Conf version is not supported !"); + } + } + Ok(()) +} + +/// Returns the path to the database containing the blockchain +pub fn get_db_path(profile: &str, currency: &Currency, sync: bool) -> PathBuf { + if sync { + let mut db_path = PathBuf::new(); + let mut db_name = String::from(profile); + db_name.push_str("_durs.db"); + db_path.push("/dev/shm"); + db_path.push(db_name); + db_path + } else { + let mut db_path = datas_path(profile, ¤cy); + db_path.push("blockchain.db"); + db_path + } +} + +/// Returns the path to the binary file containing the state of the web of trust +pub fn get_wot_path(profile: String, currency: &Currency) -> PathBuf { + let mut wot_path = match env::home_dir() { + Some(path) => path, + None => panic!("Impossible to get your home dir!"), + }; + wot_path.push(".config/"); + wot_path.push(USER_DATAS_FOLDER); + wot_path.push(profile); + wot_path.push(currency.to_string()); + wot_path.push("wot.bin"); + wot_path +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn load_conf_file() { + let (conf, _keys) = load_conf_at_path("test", &PathBuf::from("./test/")); + assert_eq!( + conf.modules() + .get("ws2p") + .expect("Not found ws2p conf") + .clone(), + json!({ + "sync_peers": [{ + "pubkey": "D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx", + "ws2p_endpoints": ["WS2P c1c39a0a i3.ifee.fr 80 /ws2p"] + },{ + "pubkey": "BoZP6aqtErHjiKLosLrQxBafi4ATciyDZQ6XRQkNefqG", + "ws2p_endpoints": ["WS2P 15af24db g1.ifee.fr 80 /ws2p"] + },{ + "pubkey": "7v2J4badvfWQ6qwRdCwhhJfAsmKwoxRUNpJHiJHj7zef", + "ws2p_endpoints": ["WS2P b48824f0 g1.monnaielibreoccitanie.org 80 /ws2p"] + }] + }) + ); + } +} diff --git a/conf/test/conf.json b/conf/test/conf.json new file mode 100644 index 0000000000000000000000000000000000000000..bf8541562facac5e99e634e0267272b2b254e6a4 --- /dev/null +++ b/conf/test/conf.json @@ -0,0 +1,19 @@ +{ + "currency": "g1", + "node_id": "357fb4b", + "modules": { + "tui": null, + "ws2p": { + "sync_peers": [{ + "pubkey": "D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx", + "ws2p_endpoints": ["WS2P c1c39a0a i3.ifee.fr 80 /ws2p"] + },{ + "pubkey": "BoZP6aqtErHjiKLosLrQxBafi4ATciyDZQ6XRQkNefqG", + "ws2p_endpoints": ["WS2P 15af24db g1.ifee.fr 80 /ws2p"] + },{ + "pubkey": "7v2J4badvfWQ6qwRdCwhhJfAsmKwoxRUNpJHiJHj7zef", + "ws2p_endpoints": ["WS2P b48824f0 g1.monnaielibreoccitanie.org 80 /ws2p"] + }] + } + } +} diff --git a/conf/test/keypairs.json b/conf/test/keypairs.json new file mode 100644 index 0000000000000000000000000000000000000000..e52b2647843d9ad42b1b4ab4b11e39f39680d460 --- /dev/null +++ b/conf/test/keypairs.json @@ -0,0 +1,6 @@ +{ + "network_sec": "RcrnPyfgEaet2VqQvLeBsgKvfaoUqinQnCooP9bFoDr1W1Focos351r4xBee6Hj2RaV6RRV8PxpdMR6hVVhNNDb", + "network_pub": "39SvsGzGZjdCh1wFS1QKdCR1ZB6rTxRbTZRW8sY5fhq7", + "member_sec": "", + "member_pub": "" + } \ No newline at end of file diff --git a/core/Cargo.toml b/core/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..d2a02d380ed74b4606c89ade69283da5c741b9b4 --- /dev/null +++ b/core/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "duniter-core" +version = "0.1.0" +authors = ["librelois <elois@ifee.fr>"] +description = "Duniter-rs core." +license = "AGPL-3.0" + +[lib] +path = "lib.rs" + +[dependencies] +clap = {version = "2.31.2", features = ["yaml"]} +duniter-blockchain = { path = "../blockchain" } +duniter-conf = { path = "../conf" } +duniter-crypto = { path = "../crypto" } +duniter-message = { path = "../message" } +duniter-module = { path = "../module" } +lazy_static = "1.0.0" +log = "0.4.1" +rand = "0.4.2" +regex = "0.2.6" +rust-crypto = "0.2.36" +serde = "1.0.24" +serde_derive = "1.0.24" +serde_json = "1.0.9" +simplelog = "0.5.1" +sqlite = "0.23.9" +threadpool = "1.7.1" + +[features] +# Treat warnings as a build error. +strict = [] \ No newline at end of file diff --git a/core/cli/en.yml b/core/cli/en.yml new file mode 100644 index 0000000000000000000000000000000000000000..6c0c983b81520e9ba1a7287c7566371db856a989 --- /dev/null +++ b/core/cli/en.yml @@ -0,0 +1,64 @@ +name: durs +version: "0.1.0" +author: Elois L. <elois@duniter.org> +about: Rust implementation of Duniter +args: + - profile: + short: p + long: profile + value_name: CUSTOM_PROFILE + help: Set a custom datas folder + takes_value: true + - logs: + short: l + long : logs + value_name: LOGS_LEVEL + takes_value: true + possible_values: ["e", "w", "i", "d", "t", "error", "warn", "info", "debug", "trace"] + help: Set the level of logs verbosity + long_help: "Set the level of logs verbosity :\n + error : print serious errors\n + warn : print hazardous situations\n + info : default level\n + debug : print a lot of debug informations\n + trace : print all traces (highly verbose)" +subcommands: + - start: + about: start duniter server + version: "0.1.0" + author: Elois L. <elois@duniter.org> + - sync_ts: + about: synchronization via a duniter-ts database + version: "0.1.0" + author: Elois L. <elois@duniter.org> + args: + - TS_PROFILE: + help: Set the ts profile to use + index: 1 + - cautious: + short: c + long: cautious + help: cautious mode (check all protocol rules, very slow) + - msync_ts: + about: synchronization in memory mode via a duniter-ts database + version: "0.1.0" + author: Elois L. <elois@duniter.org> + args: + - TS_PROFILE: + help: Set the ts profile to use + index: 1 + - cautious: + short: c + long: cautious + help: cautious mode (check all protocol rules, very slow) + - reset: + about: reset data or conf or all + version: "0.1.0" + author: Elois L. <elois@duniter.org> + args: + - DATAS_TYPE: + help : choose type datas to reset + index: 1 + possible_values: ["data","conf","all"] + required: true + \ No newline at end of file diff --git a/core/lib.rs b/core/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..ac340eb402005f04855a7925dfec52f2f7f37e7f --- /dev/null +++ b/core/lib.rs @@ -0,0 +1,440 @@ +// Copyright (C) 2018 The Duniter Project Developers. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! Crate containing Duniter-rust core. + +#![cfg_attr(feature = "strict", deny(warnings))] +#![deny( + missing_docs, missing_debug_implementations, missing_copy_implementations, trivial_casts, + trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces, + unused_qualifications +)] + +#[macro_use] +extern crate clap; + +#[macro_use] +extern crate log; + +extern crate duniter_blockchain; +extern crate duniter_conf; +extern crate duniter_crypto; +extern crate duniter_message; +extern crate duniter_module; +extern crate serde_json; +extern crate simplelog; +extern crate sqlite; +extern crate threadpool; + +use self::threadpool::ThreadPool; +use clap::{App, ArgMatches}; +use duniter_blockchain::BlockchainModule; +use duniter_conf::DuniterKeyPairs; +use duniter_crypto::keys::ed25519; +use duniter_message::DuniterMessage; +use duniter_module::*; +use log::Level; +use simplelog::*; +use std::env; +use std::fs; +use std::fs::{File, OpenOptions}; +use std::sync::mpsc; +use std::thread; +use std::time::Duration; + +#[derive(Debug)] +/// Duniter Core Datas +pub struct DuniterCore { + /// Does the entered command require to launch server ? + pub start: bool, + /// Software name + pub soft_name: &'static str, + /// Soft version + pub soft_version: &'static str, + /// Keypairs + pub keypairs: DuniterKeyPairs, + /// Duniter configuration + pub conf: DuniterConf, + /// Run duration. Zero = infinite duration. + pub run_duration_in_secs: u64, + /// Sender channel of rooter thread + pub rooter_sender: mpsc::Sender<RooterThreadMessage<DuniterMessage>>, + /// Count the number of plugged modules + pub modules_count: usize, + /// ThreadPool that execute plugged modules + pub thread_pool: ThreadPool, +} + +impl DuniterCore { + /// Instantiate Duniter classic node + pub fn new(soft_name: &'static str, soft_version: &'static str) -> Option<DuniterCore> { + DuniterCore::new_specialized_node(soft_name, soft_version, 0, vec![], vec![], None) + } + /// Instantiate Duniter specialize node + pub fn new_specialized_node<'a, 'b>( + soft_name: &'static str, + soft_version: &'static str, + run_duration_in_secs: u64, + external_followers: Vec<mpsc::Sender<DuniterMessage>>, + sup_apps: Vec<App<'a, 'b>>, + sup_apps_fn: Option<&Fn(&str, &ArgMatches) -> ()>, + ) -> Option<DuniterCore> { + // Get cli conf + let yaml = load_yaml!("./cli/en.yml"); + let cli_conf = App::from_yaml(yaml); + + // Math command line arguments + let cli_args = if !sup_apps.is_empty() { + cli_conf.subcommands(sup_apps).get_matches() + } else { + cli_conf.get_matches() + }; + + // Get datas profile name + let profile = match_profile(&cli_args); + + // Init logger + init_logger(profile.as_str(), soft_name, &cli_args); + + // Load global conf + let (conf, keypairs) = duniter_conf::load_conf(profile.as_str()); + info!("Success to load global conf."); + + if let Some(_matches) = cli_args.subcommand_matches("start") { + Some(start( + soft_name, + soft_version, + keypairs, + conf, + run_duration_in_secs, + external_followers, + )) + } else if let Some(matches) = cli_args.subcommand_matches("sync_ts") { + let ts_profile = matches.value_of("TS_PROFILE").unwrap_or("duniter_default"); + sync_ts(conf, ts_profile, matches.is_present("cautious")); + None + } else if let Some(matches) = cli_args.subcommand_matches("reset") { + let mut profile_path = match env::home_dir() { + Some(path) => path, + None => panic!("Impossible to get your home dir !"), + }; + profile_path.push(".config"); + profile_path.push(duniter_conf::get_user_datas_folder()); + profile_path.push(profile.clone()); + if !profile_path.as_path().exists() { + panic!(format!("Error : {} profile don't exist !", profile)); + } + match matches.value_of("DATAS_TYPE").unwrap() { + "data" => { + let mut currency_datas_path = profile_path.clone(); + currency_datas_path.push("g1"); + fs::remove_dir_all(currency_datas_path.as_path()) + .expect("Fail to remove all currency datas !"); + } + "conf" => { + let mut conf_file_path = profile_path.clone(); + conf_file_path.push("conf.json"); + fs::remove_file(conf_file_path.as_path()).expect("Fail to remove conf file !"); + let mut conf_keys_path = profile_path.clone(); + conf_keys_path.push("keypairs.json"); + fs::remove_file(conf_keys_path.as_path()) + .expect("Fail to remove keypairs file !"); + } + "all" => { + fs::remove_dir_all(profile_path.as_path()) + .expect("Fail to remove all profile datas !"); + } + _ => {} + } + None + } else if let Some(sup_apps_fn) = sup_apps_fn { + sup_apps_fn(profile.as_str(), &cli_args); + None + } else { + panic!("unknow sub-command !") + } + } + /// Start blockchain module + pub fn start_blockchain(&self) { + if self.start { + thread::sleep(Duration::from_secs(2)); + // Create blockchain module channel + let (blockchain_sender, blockchain_receiver): ( + mpsc::Sender<DuniterMessage>, + mpsc::Receiver<DuniterMessage>, + ) = mpsc::channel(); + + // Send blockchain sender to rooter thread + self.rooter_sender + .send(RooterThreadMessage::ModuleSender(blockchain_sender)) + .expect("Fatal error: fail to send blockchain sender to rooter thread !"); + + // Send modules_count to rooter thread + self.rooter_sender + .send(RooterThreadMessage::ModulesCount(self.modules_count + 1)) + .expect("Fatal error: fail to send modules count to rooter thread !"); + + // Instantiate blockchain module and load is conf + let mut blockchain_module = BlockchainModule::load_blockchain_conf( + &self.conf, + RequiredKeysContent::MemberKeyPair(None), + false, + ); + info!("Success to load Blockchain module."); + + // Start blockchain module in main thread + blockchain_module.start_blockchain(blockchain_receiver); + } + } + /// Plug a module + pub fn plug<M: DuniterModule<ed25519::KeyPair, DuniterMessage>>(&mut self) { + if self.start { + // Start module in a new thread + let soft_name_clone = self.soft_name.clone(); + let soft_version_clone = self.soft_version.clone(); + let required_keys = DuniterKeyPairs::get_required_keys_content( + M::ask_required_keys(), + self.keypairs.clone(), + ); + let module_conf = if let Some(module_conf_) = self + .conf + .clone() + .modules() + .get(&M::id().to_string().as_str()) + { + module_conf_.clone() + } else { + M::default_conf() + }; + let rooter_sender_clone = self.rooter_sender.clone(); + let conf_clone = self.conf.clone(); + self.thread_pool.execute(move || { + M::start( + soft_name_clone, + soft_version_clone, + required_keys, + &conf_clone, + &module_conf, + rooter_sender_clone, + false, + ).expect(&format!( + "Fatal error : fail to load {} Module !", + M::id().to_string() + )); + }); + self.modules_count += 1; + info!("Success to load {} module.", M::id().to_string()); + } + } +} + +/// Match cli option --profile +pub fn match_profile(cli_args: &ArgMatches) -> String { + String::from(cli_args.value_of("profile").unwrap_or("default")) +} + +/// Launch duniter server +pub fn start( + soft_name: &'static str, + soft_version: &'static str, + keypairs: DuniterKeyPairs, + conf: DuniterConf, + run_duration_in_secs: u64, + external_followers: Vec<mpsc::Sender<DuniterMessage>>, +) -> DuniterCore { + info!("Starting Duniter-rs..."); + + // Create senders channel + let (rooter_sender, main_receiver): ( + mpsc::Sender<RooterThreadMessage<DuniterMessage>>, + mpsc::Receiver<RooterThreadMessage<DuniterMessage>>, + ) = mpsc::channel(); + + // Create rooter thread + thread::spawn(move || { + // Wait to receiver modules senders + let mut modules_senders: Vec<mpsc::Sender<DuniterMessage>> = Vec::new(); + let mut modules_count_expected = None; + while modules_count_expected.is_none() + || modules_senders.len() < modules_count_expected.unwrap() + 1 + { + match main_receiver.recv_timeout(Duration::from_secs(20)) { + Ok(mess) => { + match mess { + RooterThreadMessage::ModuleSender(module_sender) => { + // Subscribe this module to all others modules + for other_module in modules_senders.clone() { + if other_module + .send(DuniterMessage::Followers(vec![module_sender.clone()])) + .is_err() + { + panic!("Fatal error : fail to send all modules senders to all modules !"); + } + } + // Subcribe this module to all external_followers + for external_follower in external_followers.clone() { + if external_follower + .send(DuniterMessage::Followers(vec![module_sender.clone()])) + .is_err() + { + panic!("Fatal error : fail to send all modules senders to all external_followers !"); + } + } + // Subscribe all other modules to this module + if module_sender + .send(DuniterMessage::Followers(modules_senders.clone())) + .is_err() + { + panic!("Fatal error : fail to send all modules senders to all modules !"); + } + // Subcribe all external_followers to this module + if module_sender + .send(DuniterMessage::Followers(external_followers.clone())) + .is_err() + { + panic!("Fatal error : fail to send all external_followers to all modules !"); + } + // Push this module to modules_senders list + modules_senders.push(module_sender); + // Log the number of modules_senders received + info!( + "Rooter thread receive {} module senders", + modules_senders.len() + ); + } + RooterThreadMessage::ModulesCount(modules_count) => { + info!("Rooter thread receive ModulesCount({})", modules_count); + if modules_senders.len() == modules_count { + break; + } else if modules_senders.len() < modules_count { + modules_count_expected = Some(modules_count); + } else { + panic!("Fatal error : Receive more modules_sender than expected !") + } + } + } + } + Err(e) => match e { + mpsc::RecvTimeoutError::Timeout => { + panic!("Fatal error : not receive all modules_senders after 20 secs !") + } + mpsc::RecvTimeoutError::Disconnected => { + panic!("Fatal error : rooter thread disconnnected !") + } + }, + } + } + info!("Receive all modules senders."); + if run_duration_in_secs > 0 { + thread::sleep(Duration::from_secs(run_duration_in_secs)); + // Send DuniterMessage::Stop() to all modules + for sender in modules_senders { + if sender.send(DuniterMessage::Stop()).is_err() { + panic!("Fail to send Stop() message to one module !") + } + } + thread::sleep(Duration::from_secs(2)); + } + }); + + // Instanciate DuniterCore + DuniterCore { + start: true, + soft_name, + soft_version, + keypairs, + conf, + run_duration_in_secs, + rooter_sender, + modules_count: 0, + thread_pool: ThreadPool::new(2), + } +} + +/// Launch synchronisation from a duniter-ts database +pub fn sync_ts(conf: DuniterConf, ts_profile: &str, cautious: bool) { + // Launch sync-ts + BlockchainModule::sync_ts(&conf, ts_profile, cautious); +} + +/// Initialize logger +pub fn init_logger(profile: &str, soft_name: &'static str, cli_args: &ArgMatches) { + // Get datas folder path + let mut log_file_path = match env::home_dir() { + Some(path) => path, + None => panic!("Fatal error : Impossible to get your home dir!"), + }; + log_file_path.push(".config"); + if !log_file_path.as_path().exists() { + fs::create_dir(log_file_path.as_path()).expect("Impossible to create ~/.config dir !"); + } + log_file_path.push(duniter_conf::get_user_datas_folder()); + if !log_file_path.as_path().exists() { + fs::create_dir(log_file_path.as_path()).expect("Impossible to create ~/.config/durs dir !"); + } + log_file_path.push(profile); + // Create datas folder if not exist + if !log_file_path.as_path().exists() { + fs::create_dir(log_file_path.as_path()).expect("Impossible to create your profile dir !"); + } + + // Get log_file_path + log_file_path.push(format!("{}.log", soft_name)); + + // Get log level + let log_level = match cli_args.value_of("logs").unwrap_or("i") { + "e" | "error" => Level::Error, + "w" | "warn" => Level::Warn, + "i" | "info" => Level::Info, + "d" | "debug" => Level::Debug, + "t" | "trace" => Level::Trace, + _ => panic!("Fatal error : unknow log level !"), + }; + + // Config logger + let logger_config = Config { + time: Some(Level::Error), + level: Some(Level::Error), + target: Some(Level::Debug), + location: Some(Level::Debug), + time_format: Some("%Y-%m-%d %H:%M:%S%:z"), + }; + + // Create log file if not exist + if !log_file_path.as_path().exists() { + File::create( + log_file_path + .to_str() + .expect("Fatal error : fail to get log file path !"), + ).expect("Fatal error : fail to create log file path !"); + } + + CombinedLogger::init(vec![ + TermLogger::new(LevelFilter::Error, logger_config).unwrap(), + WriteLogger::new( + log_level.to_level_filter(), + logger_config, + OpenOptions::new() + .write(true) + .append(true) + .open( + log_file_path + .to_str() + .expect("Fatal error : fail to get log file path !"), + ) + .expect("Fatal error : fail to open log file !"), + ), + ]).expect("Fatal error : fail to init logger !"); +} diff --git a/dal/Cargo.toml b/dal/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..a7a36783342acec4de9056629244e6a5712599bb --- /dev/null +++ b/dal/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "duniter-dal" +version = "0.1.0" +authors = ["librelois <elois@ifee.fr>"] +description = "Data Access Layer for the Duniter project." +license = "AGPL-3.0" + +[lib] +path = "lib.rs" + +[dependencies] +duniter-crypto = { path = "../crypto" } +duniter-documents = { path = "../documents" } +duniter-module = { path = "../module" } +duniter-network = { path = "../network" } +duniter-wotb = { path = "../wotb" } +lazy_static = "1.0.0" +log = "0.4.1" +rand = "0.4.2" +rust-crypto = "0.2.36" +regex = "0.2.6" +sqlite = "0.23.9" +serde = "1.0.24" +serde_derive = "1.0.24" +serde_json = "1.0.9" +websocket = "0.20.2" + +[features] +exp = [] +# Treat warnings as a build error. +strict = [] \ No newline at end of file diff --git a/dal/block.rs b/dal/block.rs new file mode 100644 index 0000000000000000000000000000000000000000..ae29b95d44f867a822ff2dd6adb294a181fbe656 --- /dev/null +++ b/dal/block.rs @@ -0,0 +1,510 @@ +extern crate duniter_crypto; +extern crate duniter_documents; +extern crate duniter_wotb; +extern crate serde; +extern crate serde_json; +extern crate sqlite; + +use self::duniter_crypto::keys; +use self::duniter_crypto::keys::{ed25519, PublicKey, Signature}; +use self::duniter_documents::blockchain::v10::documents::identity::IdentityDocument; +use self::duniter_documents::blockchain::v10::documents::membership::MembershipType; +use self::duniter_documents::blockchain::v10::documents::BlockDocument; +use self::duniter_documents::blockchain::Document; +use self::duniter_documents::{BlockHash, BlockId, Blockstamp, Hash}; +use self::duniter_wotb::NodeId; +use super::constants::MAX_FORKS; +use super::parsers::certifications::parse_certifications; +use super::parsers::excluded::parse_exclusions; +use super::parsers::identities::parse_identities; +use super::parsers::memberships::parse_memberships; +use super::parsers::revoked::parse_revocations; +use super::parsers::transactions::parse_compact_transactions; +use super::{DuniterDB, ForkState}; +use std::collections::HashMap; + +pub fn blockstamp_to_timestamp(blockstamp: &Blockstamp, db: &DuniterDB) -> Option<u64> { + if blockstamp.id.0 == 0 { + return Some(1_488_987_127); + } + let mut cursor = db + .0 + .prepare("SELECT median_time FROM blocks WHERE number=? AND hash=? LIMIT 1;") + .expect("convert blockstamp to timestamp failure at step 0 !") + .cursor(); + + cursor + .bind(&[ + sqlite::Value::Integer(blockstamp.id.0 as i64), + sqlite::Value::String(blockstamp.hash.0.to_hex()), + ]) + .expect("convert blockstamp to timestamp failure at step 1 !"); + + if let Some(row) = cursor + .next() + .expect("convert blockstamp to timestamp failure at step 2 !") + { + return Some(row[0] + .as_integer() + .expect("convert blockstamp to timestamp failure at step 3 !") + as u64); + } + None +} + +#[derive(Debug, Copy, Clone)] +pub enum WotEvent { + AddNode(ed25519::PublicKey, NodeId), + RemNode(ed25519::PublicKey), + AddLink(NodeId, NodeId), + RemLink(NodeId, NodeId), + EnableNode(NodeId), + DisableNode(NodeId), +} + +#[derive(Debug, Clone)] +pub struct BlockContext { + pub blockstamp: Blockstamp, + pub wot_events: Vec<WotEvent>, +} + +#[derive(Debug, Clone)] +pub struct BlockContextV2 { + pub blockstamp: Blockstamp, + pub wot_events: Vec<WotEvent>, +} + +#[derive(Debug, Clone)] +pub struct DALBlock { + pub fork: usize, + pub isolate: bool, + pub block: BlockDocument, +} + +impl DALBlock { + pub fn blockstamp(&self) -> Blockstamp { + self.block.blockstamp() + } +} + +pub fn get_forks(db: &DuniterDB) -> Vec<ForkState> { + let mut forks = Vec::new(); + forks.push(ForkState::Full()); + for fork in 1..*MAX_FORKS { + let mut cursor = db + .0 + .prepare("SELECT isolate FROM blocks WHERE fork=? ORDER BY median_time DESC LIMIT 1;") + .expect("Fail to get block !") + .cursor(); + + cursor + .bind(&[sqlite::Value::Integer(fork as i64)]) + .expect("Fail to get block !"); + + if let Some(row) = cursor.next().unwrap() { + if row[0].as_integer().unwrap() == 0 { + forks.push(ForkState::Full()) + } else { + forks.push(ForkState::Isolate()) + } + } else { + forks.push(ForkState::Free()); + } + } + forks +} + +impl DALBlock { + pub fn unisolate_fork(db: &DuniterDB, fork: usize) { + db.0 + .execute(format!("UPDATE blocks SET isolate=0 WHERE fork={};", fork)) + .unwrap(); + } + pub fn delete_fork(db: &DuniterDB, fork: usize) { + db.0 + .execute(format!("DELETE FROM blocks WHERE fork={};", fork)) + .unwrap(); + } + pub fn get_block_fork(db: &DuniterDB, blockstamp: &Blockstamp) -> Option<usize> { + let mut cursor = db + .0 + .prepare("SELECT fork FROM blocks WHERE number=? AND hash=?;") + .expect("Fail to get block !") + .cursor(); + + cursor + .bind(&[ + sqlite::Value::Integer(blockstamp.id.0 as i64), + sqlite::Value::String(blockstamp.hash.0.to_string()), + ]) + .expect("Fail to get block !"); + + if let Some(row) = cursor.next().unwrap() { + Some(row[0].as_integer().unwrap() as usize) + } else { + None + } + } + pub fn get_block_hash(db: &DuniterDB, block_number: &BlockId) -> Option<BlockHash> { + let mut cursor = db + .0 + .prepare("SELECT hash FROM blocks WHERE number=? AND fork=0;") + .expect("Fail to get block !") + .cursor(); + + cursor + .bind(&[sqlite::Value::Integer(block_number.0 as i64)]) + .expect("Fail to get block !"); + + if let Some(row) = cursor.next().unwrap() { + Some(BlockHash( + Hash::from_hex(row[0].as_string().unwrap()).unwrap(), + )) + } else { + None + } + } + + pub fn get_blocks_hashs_all_forks( + db: &DuniterDB, + block_number: &BlockId, + ) -> (Vec<BlockHash>, Vec<Hash>) { + let mut cursor = db + .0 + .prepare("SELECT hash, previous_hash FROM blocks WHERE number=?;") + .expect("Fail to get block !") + .cursor(); + + cursor + .bind(&[sqlite::Value::Integer(block_number.0 as i64)]) + .expect("Fail to get block !"); + + let mut hashs = Vec::new(); + let mut previous_hashs = Vec::new(); + while let Some(row) = cursor.next().unwrap() { + hashs.push(BlockHash( + Hash::from_hex(row[0].as_string().unwrap()).unwrap(), + )); + previous_hashs.push(Hash::from_hex(row[1].as_string().unwrap()).unwrap()); + } + (hashs, previous_hashs) + } + + pub fn get_stackables_blocks( + currency: &str, + db: &DuniterDB, + current_blockstamp: &Blockstamp, + ) -> Vec<DALBlock> { + debug!("get_stackables_blocks() after {}", current_blockstamp); + let mut stackables_blocks = Vec::new(); + let block_id = BlockId(current_blockstamp.id.0 + 1); + let (hashs, previous_hashs) = DALBlock::get_blocks_hashs_all_forks(db, &block_id); + for (hash, previous_hash) in hashs.into_iter().zip(previous_hashs) { + if previous_hash == current_blockstamp.hash.0 { + if let Some(dal_block) = + DALBlock::get_block(currency, db, &Blockstamp { id: block_id, hash }) + { + stackables_blocks.push(dal_block); + } else { + panic!(format!( + "Fail to get stackable block {} !", + Blockstamp { id: block_id, hash } + )); + } + } + } + stackables_blocks + } + pub fn get_stackables_forks(db: &DuniterDB, current_blockstamp: &Blockstamp) -> Vec<usize> { + let mut stackables_forks = Vec::new(); + let block_id = BlockId(current_blockstamp.id.0 + 1); + let (hashs, previous_hashs) = DALBlock::get_blocks_hashs_all_forks(db, &block_id); + for (hash, previous_hash) in hashs.into_iter().zip(previous_hashs) { + if previous_hash == current_blockstamp.hash.0 { + if let Some(fork) = DALBlock::get_block_fork(db, &Blockstamp { id: block_id, hash }) + { + if fork > 0 { + stackables_forks.push(fork); + } + } + } + } + stackables_forks + } + pub fn get_block(currency: &str, db: &DuniterDB, blockstamp: &Blockstamp) -> Option<DALBlock> { + let mut cursor = db + .0 + .prepare( + "SELECT fork, isolate, nonce, number, + pow_min, time, median_time, members_count, + monetary_mass, unit_base, issuers_count, issuers_frame, + issuers_frame_var, median_frame, second_tiercile_frame, + currency, issuer, signature, hash, previous_hash, dividend, identities, joiners, + actives, leavers, revoked, excluded, certifications, + transactions FROM blocks WHERE number=? AND hash=?;", + ) + .expect("Fail to get block !") + .cursor(); + + cursor + .bind(&[ + sqlite::Value::Integer(blockstamp.id.0 as i64), + sqlite::Value::String(blockstamp.hash.0.to_string()), + ]) + .expect("Fail to get block !"); + + if let Some(row) = cursor.next().expect("block not found in bdd !") { + let dividend_amount = row[20] + .as_integer() + .expect("dal::get_block() : fail to parse dividend !"); + let dividend = if dividend_amount > 0 { + Some(dividend_amount as usize) + } else if dividend_amount == 0 { + None + } else { + return None; + }; + let nonce = row[2] + .as_integer() + .expect("dal::get_block() : fail to parse nonce !") as u64; + let inner_hash = Hash::from_hex( + row[18] + .as_string() + .expect("dal::get_block() : fail to parse inner_hash !"), + ).expect("dal::get_block() : fail to parse inner_hash (2) !"); + let identities = parse_identities( + currency, + row[21] + .as_string() + .expect("dal::get_block() : fail to parse identities !"), + ).expect("dal::get_block() : fail to parse identities (2) !"); + let hashmap_identities = identities + .iter() + .map(|i| (i.issuers()[0], i.clone())) + .collect::<HashMap<ed25519::PublicKey, IdentityDocument>>(); + Some(DALBlock { + fork: row[0] + .as_integer() + .expect("dal::get_block() : fail to parse fork !") + as usize, + isolate: if row[1] + .as_integer() + .expect("dal::get_block() : fail to parse isolate !") + == 0 + { + false + } else { + true + }, + block: BlockDocument { + nonce, + number: BlockId(row[3] + .as_integer() + .expect("dal::get_block() : fail to parse number !") + as u32), + pow_min: row[4] + .as_integer() + .expect("dal::get_block() : fail to parse pow min !") + as usize, + time: row[5] + .as_integer() + .expect("dal::get_block() : fail to parse time !") + as u64, + median_time: row[6] + .as_integer() + .expect("dal::get_block() : fail to parse median_time !") + as u64, + members_count: row[7] + .as_integer() + .expect("dal::get_block() : fail to parse members_count !") + as usize, + monetary_mass: row[8] + .as_integer() + .expect("dal::get_block() : fail to parse monetary_mass !") + as usize, + unit_base: row[9] + .as_integer() + .expect("dal::get_block() : fail to parse unit_base !") + as usize, + issuers_count: row[10] + .as_integer() + .expect("dal::get_block() : fail to parse issuers_count !") + as usize, + issuers_frame: row[11] + .as_integer() + .expect("dal::get_block() : fail to parse issuers_frame !") + as isize, + issuers_frame_var: row[12] + .as_integer() + .expect("dal::get_block() : fail to parse issuers_frame_var !") + as isize, + currency: row[15] + .as_string() + .expect("dal::get_block() : fail to parse currency !") + .to_string(), + issuers: vec![ + PublicKey::from_base58( + row[16] + .as_string() + .expect("dal::get_block() : fail to parse issuer !"), + ).expect("dal::get_block() : fail to parse pubkey !"), + ], + signatures: vec![ + Signature::from_base64( + row[17] + .as_string() + .expect("dal::get_block() : fail to parse signature !"), + ).expect("dal::get_block() : fail to parse signature (2) !"), + ], + hash: Some(BlockHash( + Hash::from_hex( + row[18] + .as_string() + .expect("dal::get_block() : fail to parse hash !"), + ).expect("dal::get_block() : fail to parse hash (2) !"), + )), + parameters: None, + previous_hash: Hash::from_hex( + row[19] + .as_string() + .expect("dal::get_block() : fail to parse previous_hash !"), + ).expect( + "dal::get_block() : fail to parse previous_hash (2) !", + ), + previous_issuer: None, + inner_hash: Some(inner_hash), + dividend, + identities: identities.clone(), + joiners: parse_memberships( + currency, + MembershipType::In(), + row[22] + .as_string() + .expect("dal::get_block() : fail to parse joiners !"), + ).expect("dal::get_block() : fail to parse joiners (2) !"), + actives: parse_memberships( + currency, + MembershipType::In(), + row[23] + .as_string() + .expect("dal::get_block() : fail to parse actives !"), + ).expect("dal::get_block() : fail to parse actives (2) !"), + leavers: parse_memberships( + currency, + MembershipType::Out(), + row[24] + .as_string() + .expect("dal::get_block() : fail to parse leavers !"), + ).expect("dal::get_block() : fail to parse leavers (2) !"), + revoked: parse_revocations( + currency, + db, + &hashmap_identities, + row[25] + .as_string() + .expect("dal::get_block() : fail to parse revoked !"), + ).expect("dal::get_block() : fail to parse revoked (2) !"), + excluded: parse_exclusions( + row[26] + .as_string() + .expect("dal::get_block() : fail to parse excluded !"), + ).expect("dal::get_block() : fail to parse excluded (2) !"), + certifications: parse_certifications( + currency, + db, + &hashmap_identities, + row[27] + .as_string() + .expect("dal::get_block() : fail to parse certifications !"), + ).expect( + "dal::get_block() : fail to parse certifications (2) !", + ), + transactions: parse_compact_transactions( + currency, + row[28] + .as_string() + .expect("dal::get_block() : fail to parse transactions !"), + ).expect("dal::get_block() : fail to parse transactions (2) !"), + inner_hash_and_nonce_str: format!( + "InnerHash: {}\nNonce: {}\n", + inner_hash.to_hex(), + nonce + ), + }, + //median_frame: row[13].as_integer().unwrap_or(0) as usize, + //second_tiercile_frame: row[14].as_integer().unwrap_or(0) as usize, + }) + } else { + None + } + } + + pub fn get_current_frame(&self, db: &DuniterDB) -> HashMap<keys::ed25519::PublicKey, usize> { + let frame_begin = self.block.number.0 as i64 - (self.block.issuers_frame as i64); + let mut current_frame: HashMap<keys::ed25519::PublicKey, usize> = HashMap::new(); + let mut cursor = db + .0 + .prepare("SELECT issuer FROM blocks WHERE fork=0 AND number>=? LIMIT ?;") + .expect("get current frame blocks failure at step 1 !") + .cursor(); + cursor + .bind(&[ + sqlite::Value::Integer(frame_begin), + sqlite::Value::Integer(self.block.issuers_frame as i64), + ]) + .expect("get current frame blocks failure at step 2 !"); + + while let Some(row) = cursor + .next() + .expect("get current frame blocks failure at step 3 !") + { + let current_frame_copy = current_frame.clone(); + match current_frame_copy + .get(&PublicKey::from_base58(row[0].as_string().unwrap()).unwrap()) + { + Some(blocks_count) => { + if let Some(new_blocks_count) = current_frame + .get_mut(&PublicKey::from_base58(row[0].as_string().unwrap()).unwrap()) + { + *new_blocks_count = *blocks_count + 1; + } + } + None => { + current_frame.insert( + PublicKey::from_base58(row[0].as_string().unwrap()).unwrap(), + 0, + ); + } + } + } + current_frame + } + + pub fn compute_median_issuers_frame(&mut self, db: &DuniterDB) -> () { + let current_frame = self.get_current_frame(db); + if !current_frame.is_empty() { + let mut current_frame_vec: Vec<_> = current_frame.values().cloned().collect(); + current_frame_vec.sort_unstable(); + + /*// Calculate median + let mut median_index = match self.block.issuers_count % 2 { + 1 => (self.block.issuers_count / 2) + 1, + _ => self.block.issuers_count / 2, + }; + if median_index >= self.block.issuers_count { + median_index = self.block.issuers_count - 1; + } + self.median_frame = current_frame_vec[median_index]; + + // Calculate second tiercile index + let mut second_tiercile_index = match self.block.issuers_count % 3 { + 1 | 2 => (self.block.issuers_count as f64 * (2.0 / 3.0)) as usize + 1, + _ => (self.block.issuers_count as f64 * (2.0 / 3.0)) as usize, + }; + if second_tiercile_index >= self.block.issuers_count { + second_tiercile_index = self.block.issuers_count - 1; + } + self.second_tiercile_frame = current_frame_vec[second_tiercile_index];*/ + } + } +} diff --git a/dal/constants.rs b/dal/constants.rs new file mode 100644 index 0000000000000000000000000000000000000000..a35986fcd9853f13f592c16643c3b7aae14f3b9b --- /dev/null +++ b/dal/constants.rs @@ -0,0 +1,48 @@ +#[derive(Debug, Copy, Clone)] +pub struct CurrencyParametersV10 { + pub c: f64, + pub dt: i64, + pub ud0: i64, + pub sig_period: u64, + pub sig_stock: i64, + pub sig_window: i64, + pub sig_validity: i64, + pub sig_qty: i64, + pub idty_window: i64, + pub ms_window: i64, + pub x_percent: f64, + pub ms_validity: u64, + pub step_max: u32, + pub median_time_blocks: i64, + pub avg_gen_time: i64, + pub dt_diff_eval: i64, + pub percent_rot: f64, + pub ud_time0: i64, + pub ud_reeval_time0: i64, + pub dt_reeval: i64, +} + +pub static G1_PARAMS: &'static CurrencyParametersV10 = &CurrencyParametersV10 { + c: 0.0488, + dt: 86_400, + ud0: 1_000, + sig_period: 432_000, + sig_stock: 100, + sig_window: 5_259_600, + sig_validity: 63_115_200, + sig_qty: 5, + idty_window: 5_259_600, + ms_window: 5_259_600, + x_percent: 0.8, + ms_validity: 31_557_600, + step_max: 5, + median_time_blocks: 24, + avg_gen_time: 300, + dt_diff_eval: 12, + percent_rot: 0.67, + ud_time0: 1_488_970_800, + ud_reeval_time0: 1_490_094_000, + dt_reeval: 15_778_800, +}; +pub static G1_CONNECTIVITY_MAX: &'static usize = &125; +pub static MAX_FORKS: &'static usize = &50; diff --git a/dal/dal_event.rs b/dal/dal_event.rs new file mode 100644 index 0000000000000000000000000000000000000000..99e41058ea4b33366263c6b7cfef62717866f303 --- /dev/null +++ b/dal/dal_event.rs @@ -0,0 +1,13 @@ +extern crate duniter_documents; +extern crate serde; + +use self::duniter_documents::blockchain::v10::documents::BlockDocument; +use self::duniter_documents::blockchain::BlockchainProtocol; + +#[derive(Debug, Clone)] +pub enum DALEvent { + StackUpValidBlock(Box<BlockDocument>), + RevertBlocks(Vec<Box<BlockDocument>>), + NewValidPendingDoc(BlockchainProtocol), + RefusedPendingDoc(BlockchainProtocol), +} diff --git a/dal/dal_requests.rs b/dal/dal_requests.rs new file mode 100644 index 0000000000000000000000000000000000000000..efd50dc2b7b821c160ccfb44552396f63b86d4b5 --- /dev/null +++ b/dal/dal_requests.rs @@ -0,0 +1,63 @@ +extern crate duniter_crypto; +extern crate duniter_documents; +extern crate duniter_module; +extern crate serde; + +use self::duniter_crypto::keys::ed25519; +use self::duniter_documents::blockchain::v10::documents::{ + BlockDocument, CertificationDocument, IdentityDocument, MembershipDocument, RevocationDocument, +}; +use self::duniter_documents::Hash; +use self::duniter_module::ModuleReqFullId; +use std::collections::HashMap; + +#[derive(Debug, Copy, Clone)] +pub enum DALReqPendings { + AllPendingIdentyties(ModuleReqFullId, usize), + AllPendingIdentytiesWithoutCerts(ModuleReqFullId, usize), + PendingWotDatasForPubkey(ModuleReqFullId, ed25519::PublicKey), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum DALReqBlockchain { + CurrentBlock(ModuleReqFullId), + BlockByNumber(ModuleReqFullId, u64), + Chunk(ModuleReqFullId, u64, usize), + UIDs(Vec<ed25519::PublicKey>), +} + +#[derive(Debug, Clone)] +pub enum DALRequest { + BlockchainRequest(DALReqBlockchain), + PendingsRequest(DALReqPendings), +} + +#[derive(Debug, Clone)] +pub struct PendingIdtyDatas { + pub idty: IdentityDocument, + pub memberships: Vec<MembershipDocument>, + pub certs_count: usize, + pub certs: Vec<CertificationDocument>, + pub revocation: Option<RevocationDocument>, +} + +#[derive(Debug, Clone)] +pub enum DALResPendings { + AllPendingIdentyties(HashMap<Hash, PendingIdtyDatas>), + AllPendingIdentytiesWithoutCerts(HashMap<Hash, PendingIdtyDatas>), + PendingWotDatasForPubkey(Vec<PendingIdtyDatas>), +} + +#[derive(Debug, Clone)] +pub enum DALResBlockchain { + CurrentBlock(ModuleReqFullId, BlockDocument), + BlockByNumber(ModuleReqFullId, BlockDocument), + Chunk(ModuleReqFullId, Vec<BlockDocument>), + UIDs(HashMap<ed25519::PublicKey, Option<String>>), +} + +#[derive(Debug, Clone)] +pub enum DALResponse { + Blockchain(DALResBlockchain), + Pendings(ModuleReqFullId, DALResPendings), +} diff --git a/dal/endpoint.rs b/dal/endpoint.rs new file mode 100644 index 0000000000000000000000000000000000000000..2bd5567ca2fa58578fdf66d4fe0b619aa41807b5 --- /dev/null +++ b/dal/endpoint.rs @@ -0,0 +1,166 @@ +extern crate crypto; +extern crate duniter_crypto; +extern crate sqlite; + +use std::time::Duration; + +use self::crypto::digest::Digest; +use self::crypto::sha2::Sha256; +use self::duniter_crypto::keys::PublicKey; +use self::duniter_crypto::keys::ed25519::PublicKey as ed25519PublicKey; +use super::DuniterDB; +use super::WriteToDuniterDB; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum DALEndpointApi { + WS2P, + //WS2PS, + //WS2PTOR, + //DASA, + //BMA, + //BMAS, +} + +impl From<u32> for DALEndpointApi { + fn from(integer: u32) -> Self { + match integer { + _ => DALEndpointApi::WS2P, + } + } +} + +pub fn string_to_api(api: &str) -> Option<DALEndpointApi> { + match api { + "WS2P" => Some(DALEndpointApi::WS2P), + //"WS2PS" => Some(DALEndpointApi::WS2PS), + //"WS2PTOR" => Some(DALEndpointApi::WS2PTOR), + //"DASA" => Some(DALEndpointApi::DASA), + //"BASIC_MERKLED_API" => Some(DALEndpointApi::BMA), + //"BMAS" => Some(DALEndpointApi::BMAS), + &_ => None, + } +} + +pub fn api_to_integer(api: &DALEndpointApi) -> i64 { + match *api { + DALEndpointApi::WS2P => 0, + //DALEndpointApi::WS2PS => 1, + //DALEndpointApi::WS2PTOR => 2, + //DALEndpointApi::DASA => 3, + //DALEndpointApi::BMA => 4, + //DALEndpointApi::BMAS => 5, + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DALEndpoint { + pub hash_full_id: String, + pub status: u32, + pub node_id: u32, + pub pubkey: ed25519PublicKey, + pub api: DALEndpointApi, + pub version: usize, + pub endpoint: String, + pub last_check: u64, +} + +impl DALEndpoint { + pub fn new( + status: u32, + node_id: u32, + pubkey: ed25519PublicKey, + api: DALEndpointApi, + version: usize, + endpoint: String, + last_check: Duration, + ) -> DALEndpoint { + let mut sha = Sha256::new(); + sha.input_str(&format!( + "{}{}{}{}", + node_id, + pubkey, + api_to_integer(&api), + version + )); + DALEndpoint { + hash_full_id: sha.result_str(), + status, + node_id, + pubkey, + api, + version, + endpoint, + last_check: last_check.as_secs(), + } + } + pub fn get_endpoints_for_api(db: &DuniterDB, api: DALEndpointApi) -> Vec<DALEndpoint> { + let mut cursor:sqlite::Cursor = db.0 + .prepare("SELECT hash_full_id, status, node_id, pubkey, api, version, endpoint, last_check FROM endpoints WHERE api=? ORDER BY status DESC;") + .expect("get_endpoints_for_api() : Error in SQL request !") + .cursor(); + + cursor + .bind(&[sqlite::Value::Integer(api_to_integer(&api))]) + .expect("get_endpoints_for_api() : Error in cursor binding !"); + let mut endpoints = Vec::new(); + while let Some(row) = cursor + .next() + .expect("get_endpoints_for_api() : Error in cursor.next()") + { + endpoints.push(DALEndpoint { + hash_full_id: row[0].as_string().unwrap().to_string(), + status: row[1].as_integer().unwrap() as u32, + node_id: row[2].as_integer().unwrap() as u32, + pubkey: ed25519PublicKey::from_base58(row[3].as_string().unwrap()).unwrap(), + api: DALEndpointApi::from(row[4].as_integer().unwrap() as u32), + version: row[5].as_integer().unwrap() as usize, + endpoint: row[6].as_string().unwrap().to_string(), + last_check: row[7].as_integer().unwrap() as u64, + }); + } + endpoints + } +} + +impl WriteToDuniterDB for DALEndpoint { + fn write( + &self, + db: &DuniterDB, + _written_blockstamp: super::block_v10::BlockStampV10, + _written_timestamp: u64, + ) { + // Check if endpoint it's already written + let mut cursor: sqlite::Cursor = db.0 + .prepare("SELECT status FROM endpoints WHERE hash_full_id=? ORDER BY status DESC;") + .expect("get_endpoints_for_api() : Error in SQL request !") + .cursor(); + cursor + .bind(&[sqlite::Value::String(self.hash_full_id.clone())]) + .expect("get_endpoints_for_api() : Error in cursor binding !"); + + // If endpoint it's already written, update status + if let Some(row) = cursor + .next() + .expect("get_endpoints_for_api() : Error in cursor.next()") + { + if row[0].as_integer().unwrap() as u32 != self.status { + db.0 + .execute(format!( + "UPDATE endpoints SET status={} WHERE hash_full_id='{}'", + self.status, self.hash_full_id + )) + .unwrap(); + } + } else { + db.0 + .execute( + format!( + "INSERT INTO endpoints (hash_full_id, status, node_id, pubkey, api, version, endpoint, last_check) VALUES ('{}', {}, {}, '{}', {}, {}, '{}', {});", + self.hash_full_id, self.status, self.node_id, self.pubkey.to_string(), + api_to_integer(&self.api), self.version, self.endpoint, self.last_check + ) + ) + .unwrap(); + } + } +} diff --git a/dal/identity.rs b/dal/identity.rs new file mode 100644 index 0000000000000000000000000000000000000000..2486c628b3e9ff9ceeeb17fb42b06b52c6fbae7b --- /dev/null +++ b/dal/identity.rs @@ -0,0 +1,280 @@ +extern crate sqlite; + +use super::block::{blockstamp_to_timestamp, DALBlock}; +use super::DuniterDB; +use duniter_crypto::keys::{ed25519, PublicKey, Signature}; +use duniter_documents::blockchain::v10::documents::identity::IdentityDocumentBuilder; +use duniter_documents::blockchain::v10::documents::IdentityDocument; +use duniter_documents::blockchain::{Document, DocumentBuilder}; +use duniter_documents::Blockstamp; +use duniter_wotb::NodeId; +use std::collections::HashMap; + +#[derive(Debug, Clone)] +pub struct DALIdentity { + pub hash: String, + pub state: isize, + pub joined_on: Blockstamp, + pub penultimate_renewed_on: Blockstamp, + pub last_renewed_on: Blockstamp, + pub expires_on: u64, + pub revokes_on: u64, + pub expired_on: Option<Blockstamp>, + pub revoked_on: Option<Blockstamp>, + pub idty_doc: IdentityDocument, +} + +impl DALIdentity { + pub fn exclude_identity( + db: &DuniterDB, + wotb_id: NodeId, + renewal_blockstamp: Blockstamp, + revert: bool, + ) { + let state = if revert { 0 } else { 1 }; + let expired_on = if revert { + None + } else { + Some(renewal_blockstamp) + }; + let mut cursor = db + .0 + .prepare("UPDATE identities SET state=?, expired_on=? WHERE wotb_id=?;") + .expect("Fail to exclude idty !") + .cursor(); + + cursor + .bind(&[ + sqlite::Value::Integer(i64::from(state)), + sqlite::Value::String( + expired_on + .clone() + .unwrap_or_else(Blockstamp::default) + .to_string(), + ), + sqlite::Value::Integer(wotb_id.0 as i64), + ]) + .expect("Fail to exclude idty !"); + } + + pub fn get_wotb_index(db: &DuniterDB) -> HashMap<ed25519::PublicKey, NodeId> { + let mut wotb_index: HashMap<ed25519::PublicKey, NodeId> = HashMap::new(); + + let mut cursor = db + .0 + .prepare("SELECT wotb_id, pubkey FROM identities ORDER BY wotb_id ASC;") + .unwrap() + .cursor(); + + while let Some(row) = cursor.next().unwrap() { + wotb_index.insert( + PublicKey::from_base58(row[1].as_string().unwrap()).unwrap(), + NodeId(row[0].as_integer().unwrap() as usize), + ); + } + wotb_index + } + + pub fn create_identity( + db: &DuniterDB, + idty_doc: &IdentityDocument, + current_blockstamp: Blockstamp, + ) -> DALIdentity { + let created_on = idty_doc.blockstamp(); + let created_time = blockstamp_to_timestamp(&created_on, &db) + .expect("convert blockstamp to timestamp failure !"); + + DALIdentity { + hash: "0".to_string(), + state: 0, + joined_on: current_blockstamp, + penultimate_renewed_on: created_on.clone(), + last_renewed_on: created_on, + expires_on: created_time + super::constants::G1_PARAMS.ms_validity, + revokes_on: created_time + super::constants::G1_PARAMS.ms_validity, + expired_on: None, + revoked_on: None, + idty_doc: idty_doc.clone(), + } + } + + pub fn revoke_identity( + db: &DuniterDB, + wotb_id: NodeId, + renewal_blockstamp: &Blockstamp, + revert: bool, + ) { + let state = if revert { 2 } else { 1 }; + let revoked_on = if revert { + String::from("") + } else { + renewal_blockstamp.to_string() + }; + let mut cursor = db + .0 + .prepare("UPDATE identities SET state=?, revoked_on=? WHERE wotb_id=?;") + .expect("Fail to exclude idty !") + .cursor(); + + cursor + .bind(&[ + sqlite::Value::Integer(state), + sqlite::Value::String(revoked_on), + sqlite::Value::Integer(wotb_id.0 as i64), + ]) + .expect("Fail to exclude idty !"); + } + + pub fn renewal_identity( + &mut self, + db: &DuniterDB, + pubkey: &ed25519::PublicKey, + renewal_blockstamp: &Blockstamp, + renawal_timestamp: u64, + revert: bool, + ) { + let mut penultimate_renewed_block: Option<DALBlock> = None; + let revert_excluding = if revert { + penultimate_renewed_block = Some( + DALBlock::get_block( + self.idty_doc.currency(), + db, + &self.penultimate_renewed_on.clone(), + ).expect("renewal_identity: Fail to get penultimate_renewed_block"), + ); + penultimate_renewed_block.clone().unwrap().block.median_time + + super::constants::G1_PARAMS.ms_validity < renawal_timestamp + } else { + false + }; + self.state = if revert && revert_excluding { 1 } else { 0 }; + self.expires_on = if revert { + penultimate_renewed_block.unwrap().block.median_time + + super::constants::G1_PARAMS.ms_validity + } else { + renawal_timestamp + super::constants::G1_PARAMS.ms_validity + }; + let mut cursor = db.0 + .prepare( + "UPDATE identities SET state=?, last_renewed_on=?, expires_on=?, revokes_on=? WHERE pubkey=?;", + ) + .expect("Fail to renewal idty !") + .cursor(); + + cursor + .bind(&[ + sqlite::Value::Integer(self.state as i64), + sqlite::Value::String(renewal_blockstamp.to_string()), + sqlite::Value::Integer(self.expires_on as i64), + sqlite::Value::Integer( + (renawal_timestamp + (super::constants::G1_PARAMS.ms_validity * 2)) as i64, + ), + sqlite::Value::String(pubkey.to_string()), + ]) + .expect("Fail to renewal idty !"); + } + + pub fn remove_identity(db: &DuniterDB, wotb_id: NodeId) -> () { + db.0 + .execute(format!( + "DELETE FROM identities WHERE wotb_id={}", + wotb_id.0 + )) + .unwrap(); + } + + pub fn get_identity( + currency: &str, + db: &DuniterDB, + pubkey: &ed25519::PublicKey, + ) -> Option<DALIdentity> { + let mut cursor = db + .0 + .prepare( + "SELECT uid, hash, sig, + state, created_on, joined_on, penultimate_renewed_on, last_renewed_on, + expires_on, revokes_on, expired_on, revoked_on FROM identities WHERE pubkey=?;", + ) + .expect("Fail to get idty !") + .cursor(); + + cursor + .bind(&[sqlite::Value::String(pubkey.to_string())]) + .expect("Fail to get idty !"); + + if let Some(row) = cursor.next().expect("get_identity: cursor error") { + let idty_doc_builder = IdentityDocumentBuilder { + currency, + username: row[0] + .as_string() + .expect("get_identity: fail to parse username"), + blockstamp: &Blockstamp::from_string( + row[4] + .as_string() + .expect("DB Error : idty created_on invalid !"), + ).expect("DB Error : idty created_on invalid (2) !"), + issuer: &pubkey, + }; + let idty_sig = Signature::from_base64( + row[2].as_string().expect("get_identity: fail to parse sig"), + ).expect("get_identity: fail to parse sig (2)"); + let idty_doc = idty_doc_builder.build_with_signature(vec![idty_sig]); + + let expired_on = match Blockstamp::from_string( + row[10] + .as_string() + .expect("get_identity: fail to parse expire on"), + ) { + Ok(blockstamp) => Some(blockstamp), + Err(_) => None, + }; + let revoked_on = match Blockstamp::from_string( + row[11] + .as_string() + .expect("get_identity: fail to parse revoked on"), + ) { + Ok(blockstamp) => Some(blockstamp), + Err(_) => None, + }; + Some(DALIdentity { + hash: row[2] + .as_string() + .expect("get_identity: fail to parse hash") + .to_string(), + state: row[3] + .as_integer() + .expect("get_identity: fail to parse state") as isize, + joined_on: Blockstamp::from_string( + row[5] + .as_string() + .expect("DB Error : idty joined_on invalid !"), + ).expect("DB Error : idty joined_on invalid !"), + penultimate_renewed_on: Blockstamp::from_string( + row[6] + .as_string() + .expect("DB Error : idty penultimate_renewed_on invalid !"), + ).expect( + "DB Error : idty penultimate_renewed_on invalid (2) !", + ), + last_renewed_on: Blockstamp::from_string( + row[7] + .as_string() + .expect("get_identity: fail to parse last_renewed_on"), + ).expect("get_identity: fail to parse last_renewed_on (2)"), + expires_on: row[8] + .as_integer() + .expect("get_identity: fail to parse expires_on") + as u64, + revokes_on: row[9] + .as_integer() + .expect("get_identity: fail to parse revokes_on") + as u64, + expired_on, + revoked_on, + idty_doc, + }) + } else { + None + } + } +} diff --git a/dal/lib.rs b/dal/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..7c1541dcc352dbe86ff8262f27ae691df90a9b81 --- /dev/null +++ b/dal/lib.rs @@ -0,0 +1,266 @@ +// Copyright (C) 2018 The Duniter Project Developers. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! Defined the few global types used by all modules, +//! as well as the DuniterModule trait that all modules must implement. + +#![cfg_attr(feature = "strict", deny(warnings))] +#![cfg_attr(feature = "cargo-clippy", allow(implicit_hasher))] +#![cfg_attr(feature = "exp", allow(warnings))] +#![deny( + missing_debug_implementations, missing_copy_implementations, trivial_casts, + trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces +)] + +#[macro_use] +extern crate log; +#[macro_use] +extern crate serde_json; + +extern crate duniter_crypto; +extern crate duniter_documents; +extern crate duniter_wotb; +extern crate serde; +extern crate sqlite; + +pub mod block; +pub mod constants; +pub mod dal_event; +pub mod dal_requests; +pub mod identity; +pub mod parsers; +pub mod tools; +pub mod writers; + +use duniter_crypto::keys::{PublicKey, Signature}; +use duniter_documents::blockchain::v10::documents::BlockDocument; +use duniter_documents::{BlockHash, BlockId, Blockstamp, Hash}; +use duniter_wotb::NodeId; +use std::fmt::Debug; +use std::marker; +use std::path::PathBuf; + +use self::block::DALBlock; + +pub struct DuniterDB(pub sqlite::Connection); + +impl Debug for DuniterDB { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "DuniterDB {{ }}") + } +} + +pub trait FromJsonValue +where + Self: marker::Sized, +{ + fn from_json_value(value: &serde_json::Value) -> Option<Self>; +} + +pub trait WriteToDuniterDB { + fn write(&self, db: &DuniterDB, written_blockstamp: Blockstamp, written_timestamp: u64); +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ForkState { + Free(), + Full(), + Isolate(), +} + +#[derive(Debug, Clone)] +pub struct WotState { + pub block_number: u32, + pub block_hash: String, + pub sentries_count: usize, + pub average_density: usize, + pub average_distance: usize, + pub distances: Vec<usize>, + pub average_connectivity: usize, + pub connectivities: Vec<usize>, + pub average_centrality: usize, + pub centralities: Vec<u64>, +} + +fn _use_json_macro() -> serde_json::Value { + json!({}) +} + +pub fn open_db(db_path: &PathBuf, memory_mode: bool) -> Result<DuniterDB, sqlite::Error> { + let conn: sqlite::Connection; + if memory_mode || !db_path.as_path().exists() { + if memory_mode { + conn = sqlite::open(":memory:")?; + } else { + conn = sqlite::open(db_path.as_path())?; + } + //conn.execute("PRAGMA synchronous = 0;") + //.expect("Fail to configure SQLite DB (PRAGMA) !"); + conn.execute( + " + CREATE TABLE wot_history (block_number INTEGER, block_hash TEXT, sentries_count INTEGER, + average_density INTEGER, average_distance INTEGER, + distances TEXT, average_connectivity INTEGER, connectivities TEXT, + average_centrality INTEGER, centralities TEXT); + CREATE TABLE blocks (fork INTEGER, isolate INTEGER, version INTEGER, nonce INTEGER, number INTEGER, + pow_min INTEGER, time INTEGER, median_time INTEGER, members_count INTEGER, + monetary_mass INTEGER, unit_base INTEGER, issuers_count INTEGER, issuers_frame INTEGER, + issuers_frame_var INTEGER, median_frame INTEGER, second_tiercile_frame INTEGER, + currency TEXT, issuer TEXT, signature TEXT, hash TEXT, previous_hash TEXT, inner_hash TEXT, dividend INTEGER, identities TEXT, joiners TEXT, + actives TEXT, leavers TEXT, revoked TEXT, excluded TEXT, certifications TEXT, + transactions TEXT); + CREATE TABLE identities (wotb_id INTEGER, uid TEXT, pubkey TEXT, hash TEXT, sig TEXT, + state INTEGER, created_on TEXT, joined_on TEXT, penultimate_renewed_on TEXT, last_renewed_on TEXT, + expires_on INTEGER, revokes_on INTEGER, expired_on TEXT, revoked_on TEXT); + CREATE TABLE certifications (pubkey_from TEXT, pubkey_to TEXT, created_on TEXT, + signature TEXT, written_on TEXT, expires_on INTEGER, chainable_on INTEGER); + ", + )?; + } else { + conn = sqlite::open(db_path.as_path())?; + } + Ok(DuniterDB(conn)) +} + +pub fn close_db(db: &DuniterDB) { + db.0 + .execute("PRAGMA optimize;") + .expect("Fail to optimize SQLite DB !"); +} + +pub fn get_uid(db: &DuniterDB, wotb_id: NodeId) -> Option<String> { + let mut cursor: sqlite::Cursor = db + .0 + .prepare("SELECT uid FROM identities WHERE wotb_id=? AND state=0 LIMIT 1;") + .expect("Request SQL get_current_block is wrong !") + .cursor(); + cursor + .bind(&[sqlite::Value::Integer(wotb_id.0 as i64)]) + .expect("0"); + if let Some(row) = cursor.next().expect("fait to get_uid() : cursor error") { + Some(String::from( + row[0] + .as_string() + .expect("get_uid: Fail to parse uid field in str !"), + )) + } else { + None + } +} + +pub fn new_get_current_block(db: &DuniterDB) -> Option<BlockDocument> { + let mut cursor: sqlite::Cursor = db.0 + .prepare( + "SELECT version, nonce, number, pow_min, time, median_time, members_count, monetary_mass, unit_base, issuers_count, issuers_frame, issuers_frame_var, median_frame, second_tiercile_frame, currency, issuer, signature, hash, dividend, joiners, actives, leavers, revoked, excluded, certifications, transactions FROM blocks + WHERE fork=0 ORDER BY median_time DESC LIMIT ?;", + ) + .expect("Request SQL get_current_block is wrong !") + .cursor(); + + cursor.bind(&[sqlite::Value::Integer(1)]).expect("0"); + if let Some(row) = cursor.next().expect("1") { + let dividend = row[18].as_integer().expect("dividend"); + let dividend = if dividend > 0 { + Some(dividend as usize) + } else { + None + }; + return Some(BlockDocument { + nonce: row[1].as_integer().expect("nonce") as u64, + number: BlockId(row[2].as_integer().expect("2") as u32), + pow_min: row[3].as_integer().expect("version") as usize, + time: row[4].as_integer().expect("time") as u64, + median_time: row[5].as_integer().expect("median_time") as u64, + members_count: row[6].as_integer().expect("7") as usize, + monetary_mass: row[7].as_integer().expect("8") as usize, + unit_base: row[8].as_integer().expect("unit_base") as usize, + issuers_count: row[9].as_integer().expect("issuers_count") as usize, + issuers_frame: row[10].as_integer().expect("issuers_frame") as isize, + issuers_frame_var: row[11].as_integer().expect("issuers_frame_var") as isize, + currency: row[14].as_string().expect("currency").to_string(), + issuers: vec![PublicKey::from_base58(row[15].as_string().expect("issuer")).unwrap()], + signatures: vec![ + Signature::from_base64(row[16].as_string().expect("signature")).unwrap(), + ], + hash: Some(BlockHash( + Hash::from_hex(row[17].as_string().expect("hash")).unwrap(), + )), + parameters: None, + previous_hash: Hash::default(), + previous_issuer: None, + inner_hash: None, + dividend, + identities: Vec::with_capacity(0), + joiners: Vec::with_capacity(0), + actives: Vec::with_capacity(0), + leavers: Vec::with_capacity(0), + revoked: Vec::with_capacity(0), + excluded: Vec::with_capacity(0), + certifications: Vec::with_capacity(0), + transactions: Vec::with_capacity(0), + inner_hash_and_nonce_str: String::new(), + }); + } + None +} + +pub fn get_current_block(currency: &str, db: &DuniterDB) -> Option<DALBlock> { + let mut cursor: sqlite::Cursor = db + .0 + .prepare("SELECT number, hash FROM blocks WHERE fork=0 ORDER BY median_time DESC LIMIT ?;") + .expect("Request SQL get_current_block is wrong !") + .cursor(); + + cursor.bind(&[sqlite::Value::Integer(1)]).expect("0"); + + if let Some(row) = cursor.next().unwrap() { + let blockstamp = Blockstamp { + id: BlockId(row[0].as_integer().unwrap() as u32), + hash: BlockHash(Hash::from_hex(row[1].as_string().unwrap()).unwrap()), + }; + DALBlock::get_block(currency, db, &blockstamp) + } else { + None + } +} + +pub fn register_wot_state(db: &DuniterDB, wot_state: &WotState) { + if wot_state.block_number != 1 { + db.0 + .execute(format!( + "INSERT INTO wot_history (block_number, block_hash, sentries_count, + average_density, average_distance, distances, + average_connectivity, connectivities, average_centrality, centralities) + VALUES ({}, '{}', {}, {}, {}, '{}', {}, '{}', {}, '{}');", + wot_state.block_number, + wot_state.block_hash, + wot_state.sentries_count, + wot_state.average_density, + wot_state.average_distance, + serde_json::to_string(&wot_state.distances).unwrap(), + wot_state.average_connectivity, + serde_json::to_string(&wot_state.connectivities).unwrap(), + wot_state.average_centrality, + serde_json::to_string(&wot_state.centralities).unwrap(), + )) + .unwrap(); + } +} + +#[derive(Debug, Copy, Clone)] +pub enum BlockchainError { + UnexpectedBlockNumber(), + UnknowError(), +} diff --git a/dal/parsers/blocks.rs b/dal/parsers/blocks.rs new file mode 100644 index 0000000000000000000000000000000000000000..780ff728f3cc4057a26ff2123f7f3f85eef6ff0b --- /dev/null +++ b/dal/parsers/blocks.rs @@ -0,0 +1,144 @@ +extern crate duniter_crypto; +extern crate duniter_documents; +extern crate duniter_network; +extern crate serde_json; + +use self::duniter_network::{NetworkBlock, NetworkBlockV10}; +use super::excluded::parse_exclusions_from_json_value; +use super::identities::parse_compact_identity; +use super::transactions::parse_transaction; +use duniter_crypto::keys::{PublicKey, Signature}; +use duniter_documents::blockchain::v10::documents::membership::MembershipType; +use duniter_documents::blockchain::v10::documents::BlockDocument; +use duniter_documents::{BlockHash, BlockId, Hash}; + +pub fn parse_json_block(source: &serde_json::Value) -> Option<NetworkBlock> { + let number = BlockId(source.get("number")?.as_u64()? as u32); + let currency = source.get("currency")?.as_str()?.to_string(); + let issuer = match PublicKey::from_base58(source.get("issuer")?.as_str()?) { + Ok(pubkey) => pubkey, + Err(_) => return None, + }; + let sig = match Signature::from_base64(source.get("signature")?.as_str()?) { + Ok(sig) => sig, + Err(_) => return None, + }; + let hash = match Hash::from_hex(source.get("hash")?.as_str()?) { + Ok(hash) => hash, + Err(_) => return None, + }; + let previous_hash = match source.get("previousHash")?.as_str() { + Some(hash_str) => match Hash::from_hex(hash_str) { + Ok(hash) => hash, + Err(_) => return None, + }, + None => if number.0 > 0 { + return None; + } else { + Hash::default() + }, + }; + let previous_issuer = match source.get("previousIssuer")?.as_str() { + Some(pubkey_str) => match PublicKey::from_base58(pubkey_str) { + Ok(pubkey) => Some(pubkey), + Err(_) => return None, + }, + None => if number.0 > 0 { + return None; + } else { + None + }, + }; + let inner_hash = match Hash::from_hex(source.get("inner_hash")?.as_str()?) { + Ok(hash) => Some(hash), + Err(_) => return None, + }; + let dividend = match source.get("dividend")?.as_u64() { + Some(dividend) => Some(dividend as usize), + None => None, + }; + let mut identities = Vec::new(); + for raw_idty in source.get("identities")?.as_array()? { + identities.push(parse_compact_identity(¤cy, &raw_idty)?); + } + let mut joiners = Vec::new(); + for joiner in super::memberships::parse_memberships_from_json_value( + ¤cy, + MembershipType::In(), + &source.get("joiners")?.as_array()?, + ) { + if let Ok(joiner) = joiner { + joiners.push(joiner); + } else { + return None; + } + } + let mut actives = Vec::new(); + for active in super::memberships::parse_memberships_from_json_value( + ¤cy, + MembershipType::In(), + &source.get("actives")?.as_array()?, + ) { + if let Ok(active) = active { + actives.push(active); + } else { + return None; + } + } + let mut leavers = Vec::new(); + for leaver in super::memberships::parse_memberships_from_json_value( + ¤cy, + MembershipType::Out(), + &source.get("leavers")?.as_array()?, + ) { + if let Ok(leaver) = leaver { + leavers.push(leaver); + } else { + return None; + } + } + let mut transactions = Vec::new(); + for json_tx in source.get("transactions")?.as_array()? { + transactions.push(parse_transaction("g1", &json_tx)?); + } + let block_doc = BlockDocument { + nonce: source.get("nonce")?.as_i64()? as u64, + number: BlockId(source.get("number")?.as_u64()? as u32), + pow_min: source.get("powMin")?.as_u64()? as usize, + time: source.get("time")?.as_u64()?, + median_time: source.get("medianTime")?.as_u64()?, + members_count: source.get("membersCount")?.as_u64()? as usize, + monetary_mass: source.get("monetaryMass")?.as_u64()? as usize, + unit_base: source.get("unitbase")?.as_u64()? as usize, + issuers_count: source.get("issuersCount")?.as_u64()? as usize, + issuers_frame: source.get("issuersFrame")?.as_i64()? as isize, + issuers_frame_var: source.get("issuersFrameVar")?.as_i64()? as isize, + currency, + issuers: vec![issuer], + signatures: vec![sig], + hash: Some(BlockHash(hash)), + parameters: None, + previous_hash, + previous_issuer, + inner_hash, + dividend, + identities, + joiners, + actives, + leavers, + revoked: Vec::with_capacity(0), + excluded: parse_exclusions_from_json_value(&source.get("excluded")?.as_array()?), + certifications: Vec::with_capacity(0), + transactions, + inner_hash_and_nonce_str: format!( + "InnerHash: {}\nNonce: {}\n", + inner_hash.unwrap().to_hex(), + source.get("nonce")?.as_u64()? + ), + }; + Some(NetworkBlock::V10(Box::new(NetworkBlockV10 { + uncompleted_block_doc: block_doc, + revoked: source.get("revoked")?.as_array()?.clone(), + certifications: source.get("certifications")?.as_array()?.clone(), + }))) +} diff --git a/dal/parsers/certifications.rs b/dal/parsers/certifications.rs new file mode 100644 index 0000000000000000000000000000000000000000..a62b0a4ad73c69b00f27bff8ee42d97574b61048 --- /dev/null +++ b/dal/parsers/certifications.rs @@ -0,0 +1,131 @@ +extern crate serde; +extern crate serde_json; +extern crate sqlite; + +use super::super::block::DALBlock; +use super::super::identity::DALIdentity; +use super::super::DuniterDB; +use duniter_crypto::keys::{ed25519, PublicKey, Signature}; +use duniter_documents::blockchain::v10::documents::certification::{ + CertificationDocumentBuilder, CompactCertificationDocument, +}; +use duniter_documents::blockchain::v10::documents::{ + CertificationDocument, IdentityDocument, TextDocumentFormat, +}; +use duniter_documents::blockchain::{Document, DocumentBuilder}; +use duniter_documents::{BlockHash, BlockId, Blockstamp, Hash}; +use std::collections::HashMap; + +pub fn parse_certifications_into_compact( + json_certs: &Vec<serde_json::Value>, +) -> Vec<TextDocumentFormat<CertificationDocument>> { + let mut certifications: Vec<TextDocumentFormat<CertificationDocument>> = Vec::new(); + for certification in json_certs.iter() { + let certifications_datas: Vec<&str> = certification + .as_str() + .expect("Receive block in wrong format : fail to split cert !") + .split(':') + .collect(); + if certifications_datas.len() == 4 { + certifications.push(TextDocumentFormat::Compact(CompactCertificationDocument { + issuer: PublicKey::from_base58(certifications_datas[0]) + .expect("Receive block in wrong format : fail to parse issuer !"), + target: PublicKey::from_base58(certifications_datas[1]) + .expect("Receive block in wrong format : fail to parse target !"), + block_number: BlockId( + certifications_datas[2] + .parse() + .expect("Receive block in wrong format : fail to parse block number !"), + ), + signature: Signature::from_base64(certifications_datas[3]) + .expect("Receive block in wrong format : fail to parse signature !"), + })); + } + } + certifications +} + +pub fn parse_certifications_from_json_value( + currency: &str, + db: &DuniterDB, + block_identities: &HashMap<ed25519::PublicKey, IdentityDocument>, + array_certifications: &[serde_json::Value], +) -> Vec<TextDocumentFormat<CertificationDocument>> { + let mut certifications: Vec<TextDocumentFormat<CertificationDocument>> = Vec::new(); + for certification in array_certifications.iter() { + let certification_datas: Vec<&str> = certification + .as_str() + .expect("Fail to parse certs : json isn't str !") + .split(':') + .collect(); + if certification_datas.len() == 4 { + let target = PublicKey::from_base58(certification_datas[1]) + .expect("Fail to parse cert target !"); + let target_idty_doc: IdentityDocument = match block_identities.get(&target) { + Some(idty_doc) => idty_doc.clone(), + None => { + let dal_idty = DALIdentity::get_identity(currency, db, &target) + .expect("target identity not found in bdd !"); + dal_idty.idty_doc + } + }; + let cert_blockstamp_id = BlockId( + certification_datas[2] + .parse() + .expect("Fail to parse cert blockstamp !"), + ); + let cert_builder = + CertificationDocumentBuilder { + currency, + issuer: &PublicKey::from_base58(certification_datas[0]) + .expect("Fail to parse cert issuer !"), + blockstamp: &Blockstamp { + id: cert_blockstamp_id, + hash: if cert_blockstamp_id == BlockId(0) { + BlockHash(Hash::from_hex( + "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855", + ).expect("Fail to parse cert : invalid genesis hash")) + } else { + DALBlock::get_block_hash(db, &cert_blockstamp_id).expect(&format!( + "Fatal Error : Block {} not found in bdd !", + cert_blockstamp_id + )) + }, + }, + target: &target, + identity_username: target_idty_doc.username(), + identity_blockstamp: &target_idty_doc.blockstamp(), + identity_sig: &target_idty_doc.signatures()[0], + }; + let cert_sig = + Signature::from_base64(certification_datas[3]).expect("Fail to parse cert sig !"); + certifications.push(TextDocumentFormat::Complete( + cert_builder.build_with_signature(vec![cert_sig]), + )); + } + } + certifications +} + +pub fn parse_certifications( + currency: &str, + db: &DuniterDB, + block_identities: &HashMap<ed25519::PublicKey, IdentityDocument>, + json_datas: &str, +) -> Option<Vec<TextDocumentFormat<CertificationDocument>>> { + let raw_certifications: serde_json::Value = + serde_json::from_str(json_datas).expect("Fail to parse certs: str isn't json !"); + + if raw_certifications.is_array() { + Some(parse_certifications_from_json_value( + currency, + db, + block_identities, + raw_certifications + .as_array() + .expect("Fail to parse certs: json datas must be an array !"), + )) + } else { + None + } +} diff --git a/dal/parsers/excluded.rs b/dal/parsers/excluded.rs new file mode 100644 index 0000000000000000000000000000000000000000..329261da4fe5a876d6d751945b53ef1d3bd8d505 --- /dev/null +++ b/dal/parsers/excluded.rs @@ -0,0 +1,26 @@ +extern crate serde; +extern crate serde_json; + +use duniter_crypto::keys::{ed25519, PublicKey}; + +pub fn parse_exclusions(json_datas: &str) -> Option<Vec<ed25519::PublicKey>> { + let raw_exclusions: serde_json::Value = serde_json::from_str(json_datas).unwrap(); + + if raw_exclusions.is_array() { + Some(parse_exclusions_from_json_value( + raw_exclusions.as_array().unwrap(), + )) + } else { + None + } +} + +pub fn parse_exclusions_from_json_value( + array_exclusions: &[serde_json::Value], +) -> Vec<ed25519::PublicKey> { + let mut exclusions: Vec<ed25519::PublicKey> = Vec::new(); + for exclusion in array_exclusions.iter() { + exclusions.push(PublicKey::from_base58(exclusion.as_str().unwrap()).unwrap()); + } + exclusions +} diff --git a/dal/parsers/identities.rs b/dal/parsers/identities.rs new file mode 100644 index 0000000000000000000000000000000000000000..9791d4c0f5045d08ce499709ee79857c1676f6c2 --- /dev/null +++ b/dal/parsers/identities.rs @@ -0,0 +1,102 @@ +extern crate serde_json; +extern crate sqlite; + +use duniter_crypto::keys::{PublicKey, Signature}; +use duniter_documents::blockchain::v10::documents::identity::IdentityDocumentBuilder; +use duniter_documents::blockchain::v10::documents::IdentityDocument; +use duniter_documents::blockchain::DocumentBuilder; +use duniter_documents::Blockstamp; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum IdentityParseError { + WrongFormat(), +} + +pub fn parse_identities(currency: &str, json_datas: &str) -> Option<Vec<IdentityDocument>> { + let raw_idties: serde_json::Value = serde_json::from_str(json_datas).unwrap(); + if raw_idties.is_array() { + return Some( + parse_identities_from_json_value(currency, raw_idties.as_array().unwrap()) + .iter() + .map(|i| { + i.clone() + .expect("Fatal error : Fail to parse identity from local DB !") + }) + .collect(), + ); + } + None +} + +pub fn parse_identities_from_json_value( + currency: &str, + array_identities: &[serde_json::Value], +) -> Vec<Result<IdentityDocument, IdentityParseError>> { + array_identities + .iter() + .map(|idty| { + let idty_datas: Vec<&str> = idty.as_str().unwrap().split(':').collect(); + if idty_datas.len() == 4 { + let idty_doc_builder = IdentityDocumentBuilder { + currency, + issuer: &PublicKey::from_base58(idty_datas[0]).unwrap(), + blockstamp: &Blockstamp::from_string(idty_datas[2]).unwrap(), + username: idty_datas[3], + }; + let idty_sig = Signature::from_base64(idty_datas[1]).unwrap(); + //memberships.push(membership_doc_builder.build_with_signature(vec![membership_sig])); + Ok(idty_doc_builder.build_with_signature(vec![idty_sig])) + } else { + Err(IdentityParseError::WrongFormat()) + } + }) + .collect() + + /*for membership in array_memberships.iter() { + let membership_datas: Vec<&str> = membership.as_str().unwrap().split(':').collect(); + if membership_datas.len() == 5 { + let membership_doc_builder = IdentityDocumentBuilder { + currency, + issuer: &PublicKey::from_base58(membership_datas[0]).unwrap(), + blockstamp: &Blockstamp::from_string(membership_datas[2]).unwrap(), + membership: membership_type, + identity_username: membership_datas[4], + identity_blockstamp: &Blockstamp::from_string(membership_datas[3]).unwrap(), + }; + let membership_sig = Signature::from_base64(membership_datas[1]).unwrap(); + memberships.push(membership_doc_builder.build_with_signature(vec![membership_sig])); + } + } + memberships*/ +} + +pub fn parse_compact_identity( + currency: &str, + source: &serde_json::Value, +) -> Option<IdentityDocument> { + if source.is_string() { + let idty_elements: Vec<&str> = source.as_str().unwrap().split(':').collect(); + let issuer = match PublicKey::from_base58(idty_elements[0]) { + Ok(pubkey) => pubkey, + Err(_) => return None, + }; + let signature = match Signature::from_base64(idty_elements[1]) { + Ok(sig) => sig, + Err(_) => return None, + }; + let blockstamp = match Blockstamp::from_string(idty_elements[2]) { + Ok(blockstamp) => blockstamp, + Err(_) => return None, + }; + let username = idty_elements[3]; + let idty_doc_builder = IdentityDocumentBuilder { + currency, + username, + blockstamp: &blockstamp, + issuer: &issuer, + }; + Some(idty_doc_builder.build_with_signature(vec![signature])) + } else { + None + } +} diff --git a/dal/parsers/memberships.rs b/dal/parsers/memberships.rs new file mode 100644 index 0000000000000000000000000000000000000000..8db1a052ec09720219c62fefef64f77f6529444a --- /dev/null +++ b/dal/parsers/memberships.rs @@ -0,0 +1,66 @@ +extern crate serde_json; +extern crate sqlite; + +use duniter_crypto::keys::{PublicKey, Signature}; +use duniter_documents::blockchain::v10::documents::membership::{ + MembershipDocumentBuilder, MembershipType, +}; +use duniter_documents::blockchain::v10::documents::MembershipDocument; +use duniter_documents::blockchain::DocumentBuilder; +use duniter_documents::Blockstamp; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum MembershipParseError { + WrongFormat(), +} + +pub fn parse_memberships( + currency: &str, + membership_type: MembershipType, + json_datas: &str, +) -> Option<Vec<MembershipDocument>> { + let raw_memberships: serde_json::Value = serde_json::from_str(json_datas).unwrap(); + if raw_memberships.is_array() { + return Some( + parse_memberships_from_json_value( + currency, + membership_type, + raw_memberships.as_array().unwrap(), + ).iter() + .map(|m| { + m.clone() + .expect("Fatal error : Fail to parse membership from local DB !") + }) + .collect(), + ); + } + None +} + +pub fn parse_memberships_from_json_value( + currency: &str, + membership_type: MembershipType, + array_memberships: &[serde_json::Value], +) -> Vec<Result<MembershipDocument, MembershipParseError>> { + //let memberships: Vec<MembershipDocument> = Vec::new(); + array_memberships + .iter() + .map(|membership| { + let membership_datas: Vec<&str> = membership.as_str().unwrap().split(':').collect(); + if membership_datas.len() == 5 { + let membership_doc_builder = MembershipDocumentBuilder { + currency, + issuer: &PublicKey::from_base58(membership_datas[0]).unwrap(), + blockstamp: &Blockstamp::from_string(membership_datas[2]).unwrap(), + membership: membership_type, + identity_username: membership_datas[4], + identity_blockstamp: &Blockstamp::from_string(membership_datas[3]).unwrap(), + }; + let membership_sig = Signature::from_base64(membership_datas[1]).unwrap(); + Ok(membership_doc_builder.build_with_signature(vec![membership_sig])) + } else { + Err(MembershipParseError::WrongFormat()) + } + }) + .collect() +} diff --git a/dal/parsers/mod.rs b/dal/parsers/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..49a98a57d01502f01c3412b2cbd6e5643ef8e049 --- /dev/null +++ b/dal/parsers/mod.rs @@ -0,0 +1,132 @@ +pub mod blocks; +pub mod certifications; +pub mod excluded; +pub mod identities; +pub mod memberships; +pub mod revoked; +pub mod transactions; + +#[cfg(test)] +mod tests { + use super::transactions::*; + use duniter_crypto::keys::{PublicKey, Signature}; + use duniter_documents::blockchain::v10::documents::transaction::*; + use duniter_documents::blockchain::DocumentBuilder; + use duniter_documents::Blockstamp; + + #[test] + fn parse_json_tx() { + let tx_json = json!({ + "version": 10, + "currency": "g1", + "locktime": 0, + "hash": "3424206EF64C69E5F8C3906AAE571E378A498FCDAE0B85E9405A5205D7148EFE", + "blockstamp": "112533-000002150F2E805E604D9B31212D079570AAD8D3A4D8BB75F2C15A94A345B6B1", + "blockstampTime": 0, + "issuers": [ + "51EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2" + ], + "inputs": [ + "1000:0:D:51EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2:46496" + ], + "outputs": [ + "1000:0:SIG(2yN8BRSkARcqE8NCxKMBiHfTpx1EvwULFn56Myf6qRmy)" + ], + "unlocks": [ + "0:SIG(0)" + ], + "signatures": [ + "5olrjFylTCsVq8I5Yr7FpXeviynICyvIwe1yG5N0RJF+VZb+bCFBnLAMpmMCU2qzUvK7z41UXOrMRybXiLa2Dw==" + ], + "comment": "Merci pour la calligraphie ;) de Liam" + }); + + let tx_builder = TransactionDocumentBuilder { + currency: "g1", + blockstamp: &Blockstamp::from_string( + "112533-000002150F2E805E604D9B31212D079570AAD8D3A4D8BB75F2C15A94A345B6B1", + ).unwrap(), + locktime: &0, + issuers: &vec![ + PublicKey::from_base58("51EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2").unwrap(), + ], + inputs: &vec![ + TransactionInput::parse_from_str( + "1000:0:D:51EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2:46496", + ).unwrap(), + ], + outputs: &vec![ + TransactionOutput::parse_from_str( + "1000:0:SIG(2yN8BRSkARcqE8NCxKMBiHfTpx1EvwULFn56Myf6qRmy)", + ).unwrap(), + ], + unlocks: &vec![TransactionInputUnlocks::parse_from_str("0:SIG(0)").unwrap()], + comment: "Merci pour la calligraphie ;) de Liam", + }; + + assert_eq!( + parse_transaction("g1", &tx_json).expect("Fail to parse transaction !"), + tx_builder.build_with_signature(vec![Signature::from_base64("5olrjFylTCsVq8I5Yr7FpXeviynICyvIwe1yG5N0RJF+VZb+bCFBnLAMpmMCU2qzUvK7z41UXOrMRybXiLa2Dw==").unwrap()]) + ); + } + + #[test] + fn parse_json_tx2() { + let tx_json = json!({ + "version": 10, + "currency": "g1", + "locktime": 0, + "hash": "F98BF7A8BF82E76F5B69E70CEF0A07A08BFDB03561955EC57B254DB1E958529C", + "blockstamp": "58-00005B9167EBA1E32C6EAD42AE7F72D8F14B765D3C9E47D233B553D47C5AEE0C", + "blockstampTime": 1488990541, + "issuers": [ + "FVUFRrk1K5TQGsY7PRLwqHgdHRoHrwb1hcucp4C2N5tD" + ], + "inputs": [ + "1000:0:D:FVUFRrk1K5TQGsY7PRLwqHgdHRoHrwb1hcucp4C2N5tD:1" + ], + "outputs": [ + "3:0:SIG(7vU9BMDhN6fBuRa2iK3JRbC6pqQKb4qDMGsFcQuT5cz)", + "997:0:SIG(FVUFRrk1K5TQGsY7PRLwqHgdHRoHrwb1hcucp4C2N5tD)" + ], + "unlocks": [ + "0:SIG(0)" + ], + "signatures": [ + "VWbvsiybM4L2X5+o+6lIiuKNw5KrD1yGZqmV+lHtA28XoRUFzochSIgfoUqBsTAaYEHY45vSX917LDXudTEzBg==" + ], + "comment": "Un petit cafe ;-)" + }); + + let tx_builder = TransactionDocumentBuilder { + currency: "g1", + blockstamp: &Blockstamp::from_string( + "58-00005B9167EBA1E32C6EAD42AE7F72D8F14B765D3C9E47D233B553D47C5AEE0C", + ).unwrap(), + locktime: &0, + issuers: &vec![ + PublicKey::from_base58("FVUFRrk1K5TQGsY7PRLwqHgdHRoHrwb1hcucp4C2N5tD").unwrap(), + ], + inputs: &vec![ + TransactionInput::parse_from_str( + "1000:0:D:FVUFRrk1K5TQGsY7PRLwqHgdHRoHrwb1hcucp4C2N5tD:1", + ).unwrap(), + ], + outputs: &vec![ + TransactionOutput::parse_from_str( + "3:0:SIG(7vU9BMDhN6fBuRa2iK3JRbC6pqQKb4qDMGsFcQuT5cz)", + ).unwrap(), + TransactionOutput::parse_from_str( + "997:0:SIG(FVUFRrk1K5TQGsY7PRLwqHgdHRoHrwb1hcucp4C2N5tD)", + ).unwrap(), + ], + unlocks: &vec![TransactionInputUnlocks::parse_from_str("0:SIG(0)").unwrap()], + comment: "Un petit cafe ;-)", + }; + + assert_eq!( + parse_transaction("g1", &tx_json).expect("Fail to parse transaction !"), + tx_builder.build_with_signature(vec![Signature::from_base64("VWbvsiybM4L2X5+o+6lIiuKNw5KrD1yGZqmV+lHtA28XoRUFzochSIgfoUqBsTAaYEHY45vSX917LDXudTEzBg==").unwrap()]) + ); + } +} diff --git a/dal/parsers/revoked.rs b/dal/parsers/revoked.rs new file mode 100644 index 0000000000000000000000000000000000000000..28f12568284da1ef35e2e2583101000737babde5 --- /dev/null +++ b/dal/parsers/revoked.rs @@ -0,0 +1,92 @@ +extern crate serde_json; + +use duniter_crypto::keys::{ed25519, PublicKey, Signature}; +use duniter_documents::blockchain::v10::documents::revocation::{ + CompactRevocationDocument, RevocationDocumentBuilder, +}; +use duniter_documents::blockchain::v10::documents::{ + IdentityDocument, RevocationDocument, TextDocumentFormat, +}; +use duniter_documents::blockchain::{Document, DocumentBuilder}; + +use super::super::identity::DALIdentity; +use super::super::DuniterDB; + +use std::collections::HashMap; + +pub fn parse_revocations_into_compact( + json_recocations: &Vec<serde_json::Value>, +) -> Vec<TextDocumentFormat<RevocationDocument>> { + let mut revocations: Vec<TextDocumentFormat<RevocationDocument>> = Vec::new(); + for revocation in json_recocations.iter() { + let revocations_datas: Vec<&str> = revocation + .as_str() + .expect("Receive block in wrong format !") + .split(':') + .collect(); + if revocations_datas.len() == 2 { + revocations.push(TextDocumentFormat::Compact(CompactRevocationDocument { + issuer: PublicKey::from_base58(revocations_datas[0]) + .expect("Receive block in wrong format !"), + signature: Signature::from_base64(revocations_datas[1]) + .expect("Receive block in wrong format !"), + })); + } + } + revocations +} + +pub fn parse_revocations( + currency: &str, + db: &DuniterDB, + block_identities: &HashMap<ed25519::PublicKey, IdentityDocument>, + json_datas: &str, +) -> Option<Vec<TextDocumentFormat<RevocationDocument>>> { + let raw_revocations: serde_json::Value = serde_json::from_str(json_datas).unwrap(); + + if raw_revocations.is_array() { + Some(parse_revocations_from_json_value( + currency, + db, + block_identities, + raw_revocations.as_array().unwrap(), + )) + } else { + None + } +} + +pub fn parse_revocations_from_json_value( + currency: &str, + db: &DuniterDB, + block_identities: &HashMap<ed25519::PublicKey, IdentityDocument>, + array_revocations: &[serde_json::Value], +) -> Vec<TextDocumentFormat<RevocationDocument>> { + let mut revocations: Vec<TextDocumentFormat<RevocationDocument>> = Vec::new(); + for revocation in array_revocations.iter() { + let revocations_datas: Vec<&str> = revocation.as_str().unwrap().split(':').collect(); + if revocations_datas.len() == 2 { + let idty_pubkey: ed25519::PublicKey = + PublicKey::from_base58(revocations_datas[0]).unwrap(); + let idty_doc: IdentityDocument = match block_identities.get(&idty_pubkey) { + Some(idty_doc) => idty_doc.clone(), + None => { + let dal_idty = DALIdentity::get_identity(currency, db, &idty_pubkey).unwrap(); + dal_idty.idty_doc + } + }; + let revoc_doc_builder = RevocationDocumentBuilder { + currency, + issuer: &idty_pubkey, + identity_username: idty_doc.username(), + identity_blockstamp: &idty_doc.blockstamp(), + identity_sig: &idty_doc.signatures()[0], + }; + let revoc_sig = Signature::from_base64(revocations_datas[1]).unwrap(); + revocations.push(TextDocumentFormat::Complete( + revoc_doc_builder.build_with_signature(vec![revoc_sig]), + )); + } + } + revocations +} diff --git a/dal/parsers/transactions.rs b/dal/parsers/transactions.rs new file mode 100644 index 0000000000000000000000000000000000000000..75146df4b1f32e6b842c6024616c1dd35dfbcb3d --- /dev/null +++ b/dal/parsers/transactions.rs @@ -0,0 +1,233 @@ +extern crate serde; +extern crate serde_json; + +use duniter_crypto::keys::{PublicKey, Signature}; +use duniter_documents::blockchain::v10::documents::transaction::{ + TransactionDocument, TransactionDocumentBuilder, TransactionInput, TransactionInputUnlocks, + TransactionOutput, +}; +use duniter_documents::blockchain::DocumentBuilder; +use duniter_documents::Blockstamp; + +pub fn parse_compact_transactions( + currency: &str, + json_datas: &str, +) -> Option<Vec<TransactionDocument>> { + let raw_transactions: serde_json::Value = + serde_json::from_str(json_datas).expect("Fatal error : fail to jsonifie tx from DB !"); + + if raw_transactions.is_array() { + let mut transactions = Vec::new(); + for transaction in raw_transactions.as_array().unwrap() { + let transaction_lines: Vec<&str> = transaction + .as_str() + .expect("Fail to parse tx from DB !") + .split('$') + .collect(); + let tx_headers: Vec<&str> = transaction_lines[0].split(':').collect(); + let issuers_count = tx_headers[2] + .parse() + .expect("Fail to parse tx header NB_ISSUERS !"); + let inputs_count = tx_headers[3] + .parse() + .expect("Fail to parse tx header NB_INPUTS !"); + let unlocks_count = tx_headers[4] + .parse() + .expect("Fail to parse tx header NB_UNLOCKS !"); + let outputs_count = tx_headers[5] + .parse() + .expect("Fail to parse tx header NB_OUTPUTS !"); + let has_comment: usize = tx_headers[6] + .parse() + .expect("Fail to parse tx header HAS_COMMENT !"); + let locktime = tx_headers[7] + .parse() + .expect("Fail to parse tx header LOCKTIME !"); + let blockstamp = Blockstamp::from_string(transaction_lines[1]) + .expect("Fail to parse tx BLOCKSTAMP !"); + let mut line = 2; + let mut issuers = Vec::new(); + for _ in 0..issuers_count { + issuers.push( + PublicKey::from_base58(transaction_lines[line]) + .expect("Fail to parse tx issuer !"), + ); + line += 1; + } + let mut inputs = Vec::new(); + for _ in 0..inputs_count { + inputs.push( + TransactionInput::parse_from_str(transaction_lines[line]) + .expect("Fail to parse tx issuer !"), + ); + line += 1; + } + let mut unlocks = Vec::new(); + for _ in 0..unlocks_count { + unlocks.push( + TransactionInputUnlocks::parse_from_str(transaction_lines[line]) + .expect("Fail to parse tx issuer !"), + ); + line += 1; + } + let mut outputs = Vec::new(); + for _ in 0..outputs_count { + outputs.push( + TransactionOutput::parse_from_str(transaction_lines[line]) + .expect("Fail to parse tx issuer !"), + ); + line += 1; + } + let mut comment = ""; + if has_comment == 1 { + comment = transaction_lines[line]; + line += 1; + } + let mut signatures = Vec::new(); + for _ in 0..issuers_count { + signatures.push( + Signature::from_base64(transaction_lines[line]) + .expect("Fail to parse tx signature !"), + ); + line += 1; + } + let tx_doc_builder = TransactionDocumentBuilder { + currency, + blockstamp: &blockstamp, + locktime: &locktime, + issuers: &issuers, + inputs: &inputs, + unlocks: &unlocks, + outputs: &outputs, + comment, + }; + transactions.push(tx_doc_builder.build_with_signature(signatures)); + } + Some(transactions) + } else { + None + } +} + +pub fn parse_transaction( + currency: &str, + source: &serde_json::Value, +) -> Option<TransactionDocument> { + //debug!("transaction={:#?}", source); + let blockstamp = match Blockstamp::from_string(source.get("blockstamp")?.as_str()?) { + Ok(blockstamp) => blockstamp, + Err(_) => { + return None; + } + }; + let locktime = source.get("locktime")?.as_i64()? as u64; + let issuers_array = source.get("issuers")?.as_array()?; + let mut issuers = Vec::with_capacity(issuers_array.len()); + for issuer in issuers_array { + match PublicKey::from_base58(issuer.as_str()?) { + Ok(pubkey) => issuers.push(pubkey), + Err(_) => { + return None; + } + } + } + let inputs_array = source.get("inputs")?.as_array()?; + let mut inputs = Vec::with_capacity(inputs_array.len()); + for input in inputs_array { + let input_str = input.as_str()?; + match TransactionInput::parse_from_str(input_str) { + Ok(input) => inputs.push(input), + Err(_) => { + return None; + } + } + } + let unlocks_array = source.get("unlocks")?.as_array()?; + let mut unlocks = Vec::with_capacity(unlocks_array.len()); + for unlock in unlocks_array { + match TransactionInputUnlocks::parse_from_str(unlock.as_str()?) { + Ok(unlock) => unlocks.push(unlock), + Err(_) => { + return None; + } + } + } + let outputs_array = source.get("outputs")?.as_array()?; + let mut outputs = Vec::with_capacity(outputs_array.len()); + for output in outputs_array { + match TransactionOutput::parse_from_str(output.as_str()?) { + Ok(output) => outputs.push(output), + Err(_) => { + return None; + } + } + } + let signatures_array = source.get("signatures")?.as_array()?; + let mut signatures = Vec::with_capacity(signatures_array.len()); + for signature in signatures_array { + match Signature::from_base64(signature.as_str()?) { + Ok(signature) => signatures.push(signature), + Err(_) => { + return None; + } + } + } + let comment = source.get("comment")?.as_str()?; + + let tx_doc_builder = TransactionDocumentBuilder { + currency, + blockstamp: &blockstamp, + locktime: &locktime, + issuers: &issuers, + inputs: &inputs, + unlocks: &unlocks, + outputs: &outputs, + comment, + }; + Some(tx_doc_builder.build_with_signature(signatures)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_compact_tx() { + let compact_txs = "[\"TX:10:1:1:1:1:1:0$\ +112533-000002150F2E805E604D9B31212D079570AAD8D3A4D8BB75F2C15A94A345B6B1$\ +51EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2$\ +1000:0:D:51EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2:46496$\ +0:SIG(0)$\ +1000:0:SIG(2yN8BRSkARcqE8NCxKMBiHfTpx1EvwULFn56Myf6qRmy)$\ +Merci pour la calligraphie ;) de Liam$\ +5olrjFylTCsVq8I5Yr7FpXeviynICyvIwe1yG5N0RJF+VZb+bCFBnLAMpmMCU2qzUvK7z41UXOrMRybXiLa2Dw==\"]"; + + let tx_builder = TransactionDocumentBuilder { + currency: "g1", + blockstamp: &Blockstamp::from_string( + "112533-000002150F2E805E604D9B31212D079570AAD8D3A4D8BB75F2C15A94A345B6B1", + ).unwrap(), + locktime: &0, + issuers: &vec![ + PublicKey::from_base58("51EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2").unwrap(), + ], + inputs: &vec![ + TransactionInput::parse_from_str( + "1000:0:D:51EFVNZwpfmTXU7BSLpeh3PZFgfdmm5hq5MzCDopdH2:46496", + ).unwrap(), + ], + outputs: &vec![ + TransactionOutput::parse_from_str( + "1000:0:SIG(2yN8BRSkARcqE8NCxKMBiHfTpx1EvwULFn56Myf6qRmy)", + ).unwrap(), + ], + unlocks: &vec![TransactionInputUnlocks::parse_from_str("0:SIG(0)").unwrap()], + comment: "Merci pour la calligraphie ;) de Liam", + }; + + assert_eq!( + parse_compact_transactions("g1", compact_txs).expect("Fail to parse compact transactions !"), + vec![tx_builder.build_with_signature(vec![Signature::from_base64("5olrjFylTCsVq8I5Yr7FpXeviynICyvIwe1yG5N0RJF+VZb+bCFBnLAMpmMCU2qzUvK7z41UXOrMRybXiLa2Dw==").unwrap()])] + ); + } +} diff --git a/dal/test.db b/dal/test.db new file mode 100644 index 0000000000000000000000000000000000000000..ab254abc64c4f280bdde96b863cdc6420fddff67 Binary files /dev/null and b/dal/test.db differ diff --git a/dal/tools.rs b/dal/tools.rs new file mode 100644 index 0000000000000000000000000000000000000000..9a4ecee0aa5f649b94ccd172c0eee2d326659533 --- /dev/null +++ b/dal/tools.rs @@ -0,0 +1,137 @@ +extern crate duniter_wotb; + +use duniter_wotb::operations::centrality::{ + CentralitiesCalculator, UlrikBrandesCentralityCalculator, +}; +use duniter_wotb::operations::distance::{ + DistanceCalculator, RustyDistanceCalculator, WotDistance, WotDistanceParameters, +}; +use duniter_wotb::operations::path::{PathFinder, RustyPathFinder}; +use duniter_wotb::{NodeId, WebOfTrust}; + +pub static CENTRALITY_CALCULATOR: UlrikBrandesCentralityCalculator = + UlrikBrandesCentralityCalculator {}; +pub static DISTANCE_CALCULATOR: RustyDistanceCalculator = RustyDistanceCalculator {}; +pub static PATH_FINDER: RustyPathFinder = RustyPathFinder {}; + +pub fn get_sentry_requirement(members_count: usize, step_max: u32) -> u32 { + match step_max { + 5 => { + if members_count < 33 { + 2 + } else if members_count < 244 { + 3 + } else if members_count < 1025 { + 4 + } else if members_count < 3126 { + 5 + } else if members_count < 7777 { + 6 + } else { + panic!("get_sentry_requirement not define for members_count greater than 7777 !"); + } + } + _ => panic!("get_sentry_requirement not define for step_max != 5 !"), + } +} + +pub fn calculate_average_density<T: WebOfTrust>(wot: &T) -> usize { + let enabled_members = wot.get_enabled(); + let enabled_members_count = enabled_members.len(); + let mut count_actives_links: usize = 0; + for member in &enabled_members { + count_actives_links += wot.issued_count(*member).unwrap(); + } + ((count_actives_links as f32 / enabled_members_count as f32) * 1_000.0) as usize +} + +pub fn compute_distances<T: WebOfTrust + Sync>( + wot: &T, + sentry_requirement: u32, + step_max: u32, + x_percent: f64, +) -> (usize, Vec<usize>, usize, Vec<usize>) { + let members_count = wot.get_enabled().len(); + let mut distances = Vec::new(); + let mut average_distance: usize = 0; + let mut connectivities = Vec::new(); + let mut average_connectivity: usize = 0; + for i in 0..wot.size() { + let distance_datas: WotDistance = DISTANCE_CALCULATOR + .compute_distance( + wot, + WotDistanceParameters { + node: NodeId(i), + sentry_requirement, + step_max, + x_percent, + }, + ) + .expect("Fatal Error: compute_distance return None !"); + let mut distance = ((f64::from(distance_datas.success) + / (x_percent * f64::from(distance_datas.sentries))) * 100.0) + as usize; + distances.push(distance); + average_distance += distance; + let mut connectivity = + ((f64::from(distance_datas.success - distance_datas.success_at_border) + / (x_percent * f64::from(distance_datas.sentries))) * 100.0) as usize; + connectivities.push(connectivity); + average_connectivity += connectivity; + } + average_distance /= members_count; + average_connectivity /= members_count; + ( + average_distance, + distances, + average_connectivity, + connectivities, + ) +} + +pub fn calculate_distance_stress_centralities<T: WebOfTrust>(wot: &T, step_max: u32) -> Vec<u64> { + CENTRALITY_CALCULATOR.distance_stress_centralities(wot, step_max as usize) +} + +pub fn calculate_centralities_degree<T: WebOfTrust>(wot: &T, step_max: u32) -> Vec<usize> { + let wot_size = wot.size(); + let members_count = wot.get_enabled().len() as u64; + let oriented_couples_count: u64 = members_count * (members_count - 1); + let mut centralities: Vec<u64> = vec![0; wot_size]; + for i in 0..wot_size { + for j in 0..wot_size { + let mut paths = PATH_FINDER.find_paths(wot, NodeId(i), NodeId(j), step_max); + if paths.is_empty() { + break; + } + //paths.sort_unstable_by(|a, b| a.len().cmp(&b.len())); + let shortest_path_len = paths[0].len(); + let mut intermediate_members: Vec<NodeId> = Vec::new(); + if shortest_path_len > 2 { + for path in paths { + //if path.len() == shortest_path_len { + for node_id in &path { + if !intermediate_members.contains(node_id) { + intermediate_members.push(*node_id); + } + } + /*} else { + break; + }*/ + } + } + let centralities_copy = centralities.clone(); + for node_id in intermediate_members { + let centrality = ¢ralities_copy[node_id.0]; + if let Some(tmp) = centralities.get_mut(node_id.0) { + *tmp = *centrality + 1; + } + } + } + } + let mut relative_centralities = Vec::with_capacity(wot_size); + for centrality in centralities { + relative_centralities.push((centrality * 100_000 / oriented_couples_count) as usize); + } + relative_centralities +} diff --git a/dal/writers/block.rs b/dal/writers/block.rs new file mode 100644 index 0000000000000000000000000000000000000000..aeb4d4d87415c38b6330c57d415032b79f03505b --- /dev/null +++ b/dal/writers/block.rs @@ -0,0 +1,74 @@ +extern crate duniter_crypto; +extern crate duniter_documents; +extern crate duniter_wotb; +extern crate serde; +extern crate serde_json; +extern crate sqlite; + +use self::duniter_documents::blockchain::v10::documents::BlockDocument; +use self::duniter_documents::blockchain::Document; +use super::super::block::DALBlock; +use super::super::DuniterDB; + +pub fn write_network_block( + db: &DuniterDB, + block: &BlockDocument, + fork: usize, + isolate: bool, + revoked: &Vec<serde_json::Value>, + certifications: &Vec<serde_json::Value>, +) { + db.0 + .execute( + format!("INSERT INTO blocks (fork, isolate, version, nonce, number, pow_min, time, median_time, members_count, monetary_mass, unit_base, issuers_count, issuers_frame, issuers_frame_var, currency, issuer, signature, hash, previous_hash, inner_hash, dividend, identities, joiners, actives, leavers, revoked, excluded, certifications, transactions) VALUES ({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, '{}', '{}', '{}', '{}', '{}', '{}', {}, '{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}');", + fork, if isolate { 1 } else { 0 }, 10, + block.nonce, block.number, block.pow_min, block.time, block.median_time, + block.members_count, block.monetary_mass, block.unit_base, block.issuers_count, + block.issuers_frame, block.issuers_frame_var, block.currency, block.issuers[0], + block.signatures[0].to_string(), block.hash.unwrap().0.to_string(), + block.previous_hash.to_string(), block.inner_hash.unwrap().to_string(), + block.dividend.unwrap_or(0), + serde_json::to_string(&block.identities).unwrap(), + serde_json::to_string(&block.joiners).unwrap(), serde_json::to_string(&block.actives).unwrap(), + serde_json::to_string(&block.leavers).unwrap(), serde_json::to_string(revoked).unwrap(), + serde_json::to_string(&block.excluded).unwrap(), serde_json::to_string(certifications).unwrap(), + serde_json::to_string(&block.transactions).unwrap() + )) + .unwrap(); +} + +pub fn write(db: &DuniterDB, block: &BlockDocument, fork: usize, isolate: bool) { + let mut insert = true; + if fork == 0 { + if let Some(_fork) = DALBlock::get_block_fork(db, &block.blockstamp()) { + insert = false; + db.0 + .execute(format!( + "UPDATE blocks SET fork=0 WHERE number={} AND hash='{}';", + block.number, + block.hash.unwrap().0.to_string() + )) + .unwrap(); + } + } + + if insert { + db.0 + .execute( + format!("INSERT INTO blocks (fork, isolate, version, nonce, number, pow_min, time, median_time, members_count, monetary_mass, unit_base, issuers_count, issuers_frame, issuers_frame_var, currency, issuer, signature, hash, previous_hash, inner_hash, dividend, identities, joiners, actives, leavers, revoked, excluded, certifications, transactions) VALUES ({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, '{}', '{}', '{}', '{}', '{}', '{}', {}, '{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}');", + fork, if isolate { 1 } else { 0 }, 10, + block.nonce, block.number, block.pow_min, block.time, block.median_time, + block.members_count, block.monetary_mass, block.unit_base, block.issuers_count, + block.issuers_frame, block.issuers_frame_var, block.currency, block.issuers[0], + block.signatures[0].to_string(), block.hash.unwrap().0.to_string(), + block.previous_hash.to_string(), block.inner_hash.unwrap().to_string(), + block.dividend.unwrap_or(0), serde_json::to_string(&block.identities).unwrap(), + serde_json::to_string(&block.joiners).unwrap(), serde_json::to_string(&block.actives).unwrap(), + serde_json::to_string(&block.leavers).unwrap(), serde_json::to_string(&block.revoked).unwrap(), + serde_json::to_string(&block.excluded).unwrap(), serde_json::to_string(&block.certifications).unwrap(), + serde_json::to_string(&block.transactions).unwrap() + ), + ) + .unwrap(); + } +} diff --git a/dal/writers/certification.rs b/dal/writers/certification.rs new file mode 100644 index 0000000000000000000000000000000000000000..03ec2db95ec3c77b0a9cdb7fb7acf4148b08342a --- /dev/null +++ b/dal/writers/certification.rs @@ -0,0 +1,54 @@ +extern crate serde; +extern crate serde_json; +extern crate sqlite; + +use super::super::DuniterDB; +use duniter_crypto::keys::ed25519; +use duniter_documents::blockchain::v10::documents::certification::CompactCertificationDocument; +use duniter_documents::Blockstamp; + +pub fn write_certification( + cert: &CompactCertificationDocument, + db: &DuniterDB, + written_blockstamp: Blockstamp, + written_timestamp: u64, +) { + let mut cursor = db + .0 + .prepare("SELECT median_time FROM blocks WHERE number=? AND fork=0 LIMIT 1;") + .expect("invalid write_certification sql request") + .cursor(); + + cursor + .bind(&[sqlite::Value::Integer(cert.block_number.0 as i64)]) + .expect("convert blockstamp to timestamp failure at step 1 !"); + + let mut created_timestamp: i64 = 0; + if let Some(row) = cursor + .next() + .expect("convert blockstamp to timestamp failure at step 2 !") + { + created_timestamp = row[0] + .as_integer() + .expect("Fail to write cert, impossible to get created_timestamp !"); + } + + db.0 + .execute( + format!("INSERT INTO certifications (pubkey_from, pubkey_to, created_on, signature, written_on, expires_on, chainable_on) VALUES ('{}', '{}', '{}', '{}', '{}', {}, {});", + cert.issuer, cert.target, cert.block_number.0, cert.signature, + written_blockstamp.to_string(), + created_timestamp+super::super::constants::G1_PARAMS.sig_validity, + written_timestamp+super::super::constants::G1_PARAMS.sig_period + )) + .expect("Fail to execute INSERT certification !"); +} + +pub fn remove_certification(from: ed25519::PublicKey, to: ed25519::PublicKey, db: &DuniterDB) { + db.0 + .execute(format!( + "DELETE FROM certifications WHERE pubkey_from={} AND pubkey_to={}", + from, to + )) + .expect("Fail to execute DELETE certification !"); +} diff --git a/dal/writers/identity.rs b/dal/writers/identity.rs new file mode 100644 index 0000000000000000000000000000000000000000..ea106fce344f2489d635bf26b09c49446fc67c4e --- /dev/null +++ b/dal/writers/identity.rs @@ -0,0 +1,37 @@ +extern crate duniter_wotb; +extern crate sqlite; + +use super::super::identity::DALIdentity; +use super::super::DuniterDB; +use duniter_documents::blockchain::Document; +use duniter_documents::Blockstamp; +use duniter_wotb::NodeId; + +pub fn write( + idty: &DALIdentity, + wotb_id: &NodeId, + db: &DuniterDB, + _written_blockstamp: Blockstamp, + _written_timestamp: u64, +) { + let expired_on = match idty.expired_on { + Some(ref tmp) => tmp.to_string(), + None => String::from(""), + }; + let revoked_on = match idty.revoked_on { + Some(ref tmp) => tmp.to_string(), + None => String::from(""), + }; + db.0 + .execute( + format!("INSERT INTO identities (wotb_id, uid, pubkey, hash, sig, state, created_on, joined_on, penultimate_renewed_on, last_renewed_on, expires_on, revokes_on, expired_on, revoked_on) VALUES ({}, '{}', '{}', '{}', '{}', {}, '{}', '{}', '{}', '{}', {}, {}, '{}', '{}');", + (*wotb_id).0, idty.idty_doc.username(), idty.idty_doc.issuers()[0], idty.hash, + idty.idty_doc.signatures()[0], idty.state, + idty.idty_doc.blockstamp().to_string(), + idty.joined_on.to_string(), + idty.penultimate_renewed_on.to_string(), + idty.last_renewed_on.to_string(), + idty.expires_on, idty.revokes_on, expired_on, revoked_on + )) + .unwrap(); +} diff --git a/dal/writers/mod.rs b/dal/writers/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..aba7f80035e06d003fbf4387999ea70411845262 --- /dev/null +++ b/dal/writers/mod.rs @@ -0,0 +1,4 @@ +pub mod block; +pub mod certification; +pub mod identity; +pub mod requests; diff --git a/dal/writers/requests.rs b/dal/writers/requests.rs new file mode 100644 index 0000000000000000000000000000000000000000..c48b36a3e385cd6cc6929fb42bcb248c235ce492 --- /dev/null +++ b/dal/writers/requests.rs @@ -0,0 +1,83 @@ +extern crate duniter_crypto; +extern crate duniter_documents; +extern crate duniter_wotb; +extern crate serde; +extern crate serde_json; +extern crate sqlite; + +use self::duniter_crypto::keys::ed25519; +use self::duniter_documents::blockchain::v10::documents::certification::CompactCertificationDocument; +use self::duniter_documents::blockchain::v10::documents::identity::IdentityDocument; +use self::duniter_documents::Blockstamp; +use self::duniter_wotb::NodeId; +use super::super::block::DALBlock; +use super::super::identity::DALIdentity; +use super::super::DuniterDB; + +#[derive(Debug)] +/// Contain a pending write request for blockchain database +pub enum DBWriteRequest { + /// Newcomer + CreateIdentity(NodeId, Blockstamp, u64, IdentityDocument), + /// Active + RenewalIdentity(ed25519::PublicKey, Blockstamp, u64), + /// Excluded + ExcludeIdentity(NodeId, Blockstamp, u64), + /// Revoked + RevokeIdentity(NodeId, Blockstamp, u64), + /// Certification + CreateCert(Blockstamp, u64, CompactCertificationDocument), + /// Certification expiry + CertExpiry(NodeId, NodeId, Blockstamp, u64), + /// Write block + WriteBlock(DALBlock), + /// Revert block + RevertBlock(DALBlock), +} + +impl DBWriteRequest { + pub fn apply(&self, currency: &str, db: &DuniterDB) { + match *self { + DBWriteRequest::CreateIdentity( + ref wotb_id, + ref blockstamp, + ref median_time, + ref idty_doc, + ) => { + trace!("DBWriteRequest::CreateIdentity..."); + let idty = DALIdentity::create_identity(db, idty_doc, blockstamp.clone()); + super::identity::write(&idty, wotb_id, db, blockstamp.clone(), *median_time); + trace!("DBWriteRequest::CreateIdentity...finish."); + } + DBWriteRequest::RenewalIdentity(ref pubkey, ref blockstamp, ref median_time) => { + trace!("DBWriteRequest::RenewalIdentity..."); + let mut idty = DALIdentity::get_identity(currency, db, pubkey) + .expect("Fatal error : impossible ton renewal an identidy that don't exist !"); + idty.renewal_identity(db, pubkey, blockstamp, *median_time, false); + trace!("DBWriteRequest::RenewalIdentity..."); + } + DBWriteRequest::ExcludeIdentity(ref wotb_id, ref blockstamp, ref _median_time) => { + DALIdentity::exclude_identity(db, *wotb_id, *blockstamp, false); + } + DBWriteRequest::RevokeIdentity(ref wotb_id, ref blockstamp, ref _median_time) => { + DALIdentity::revoke_identity(db, *wotb_id, blockstamp, false); + } + DBWriteRequest::CreateCert(ref blockstamp, ref median_time, ref compact_cert) => { + trace!("DBWriteRequest::CreateCert..."); + super::certification::write_certification( + compact_cert, + db, + blockstamp.clone(), + *median_time, + ); + trace!("DBWriteRequest::CreateCert...finish"); + } + DBWriteRequest::WriteBlock(ref dal_block) => { + trace!("DBWriteRequest::WriteBlock..."); + super::block::write(db, &dal_block.block, dal_block.fork, dal_block.isolate); + trace!("DBWriteRequest::WriteBlock...finish"); + } + _ => {} + } + } +} diff --git a/documents/blockchain/v10/documents/block.rs b/documents/blockchain/v10/documents/block.rs index 0305af99761abe685cafcc3d3c8d1f2c839398b5..1b51c4689331f4b1bbf849d348a3ef7428577222 100644 --- a/documents/blockchain/v10/documents/block.rs +++ b/documents/blockchain/v10/documents/block.rs @@ -24,7 +24,7 @@ use blockchain::v10::documents::identity::IdentityDocument; use blockchain::v10::documents::membership::MembershipDocument; use blockchain::v10::documents::revocation::RevocationDocument; use blockchain::v10::documents::transaction::TransactionDocument; -use blockchain::v10::documents::{TextDocument, V10Document}; +use blockchain::v10::documents::*; use blockchain::{BlockchainProtocol, Document, IntoSpecializedDocument}; use {BlockHash, BlockId, Blockstamp, Hash}; @@ -130,11 +130,11 @@ pub struct BlockDocument { /// Leavers pub leavers: Vec<MembershipDocument>, /// Revokeds - pub revoked: Vec<RevocationDocument>, + pub revoked: Vec<TextDocumentFormat<RevocationDocument>>, /// Excludeds pub excluded: Vec<ed25519::PublicKey>, /// Certifications - pub certifications: Vec<CertificationDocument>, + pub certifications: Vec<TextDocumentFormat<CertificationDocument>>, /// Transactions pub transactions: Vec<TransactionDocument>, /// Part to sign @@ -146,9 +146,6 @@ impl BlockDocument { pub fn compute_inner_hash(&mut self) { let mut sha256 = Sha256::new(); let inner_text = self.generate_compact_inner_text(); - /*if self.number.0 == 46473 { - println!("#46473 raw_format=\"{}\"", inner_text); - }*/ sha256.input_str(&inner_text); self.inner_hash = Some(Hash::from_hex(&sha256.result_str()).unwrap()); } @@ -176,7 +173,8 @@ impl BlockDocument { )); self.hash = Some(BlockHash(Hash::from_hex(&sha256.result_str()).unwrap())); } - fn generate_compact_inner_text(&self) -> String { + /// Generate compact inner text (for compute inner_hash) + pub fn generate_compact_inner_text(&self) -> String { let mut identities_str = String::from(""); for identity in self.identities.clone() { identities_str.push_str("\n"); @@ -205,7 +203,7 @@ impl BlockDocument { let mut revokeds_str = String::from(""); for revocation in self.revoked.clone() { revokeds_str.push_str("\n"); - revokeds_str.push_str(&revocation.generate_compact_text()); + revokeds_str.push_str(&revocation.as_compact_text()); } let mut excludeds_str = String::from(""); for exclusion in self.excluded.clone() { @@ -215,7 +213,7 @@ impl BlockDocument { let mut certifications_str = String::from(""); for certification in self.certifications.clone() { certifications_str.push_str("\n"); - certifications_str.push_str(&certification.generate_compact_text()); + certifications_str.push_str(&certification.as_compact_text()); } let mut transactions_str = String::from(""); for transaction in self.transactions.clone() { @@ -313,12 +311,8 @@ impl Document for BlockDocument { } } -impl TextDocument for BlockDocument { - fn as_text(&self) -> &str { - panic!(); - } - - fn generate_compact_text(&self) -> String { +impl CompactTextDocument for BlockDocument { + fn as_compact_text(&self) -> String { let compact_inner_text = self.generate_compact_inner_text(); format!( "{}InnerHash: {}\nNonce: ", @@ -328,6 +322,18 @@ impl TextDocument for BlockDocument { } } +impl TextDocument for BlockDocument { + type CompactTextDocument_ = BlockDocument; + + fn as_text(&self) -> &str { + panic!(); + } + + fn to_compact_document(&self) -> Self::CompactTextDocument_ { + self.clone() + } +} + impl IntoSpecializedDocument<BlockchainProtocol> for BlockDocument { fn into_specialized(self) -> BlockchainProtocol { BlockchainProtocol::V10(Box::new(V10Document::Block(Box::new(self)))) @@ -506,7 +512,7 @@ a9PHPuSfw7jW8FRQHXFsGi/bnLjbtDnTYvEVgUC9u0WlR7GVofa+Xb+l5iy6NwuEXiwvueAkf08wPVY8 leavers: Vec::new(), revoked: Vec::new(), excluded: Vec::new(), - certifications: vec![cert1], + certifications: vec![TextDocumentFormat::Complete(cert1)], transactions: vec![tx1, tx2], inner_hash_and_nonce_str: String::new(), }; diff --git a/documents/blockchain/v10/documents/certification.rs b/documents/blockchain/v10/documents/certification.rs index b3e8857ccbe90c902b1e7d8bc307cb61e780aec3..842591838c7eb796bb28cfc5a76bd2e816fffffc 100644 --- a/documents/blockchain/v10/documents/certification.rs +++ b/documents/blockchain/v10/documents/certification.rs @@ -21,11 +21,9 @@ use self::serde::ser::{Serialize, Serializer}; use duniter_crypto::keys::{ed25519, PublicKey, Signature}; use regex::Regex; -use blockchain::v10::documents::{ - StandardTextDocumentParser, TextDocument, TextDocumentBuilder, V10Document, - V10DocumentParsingError, -}; +use blockchain::v10::documents::*; use blockchain::{BlockchainProtocol, Document, DocumentBuilder, IntoSpecializedDocument}; +use BlockId; use Blockstamp; lazy_static! { @@ -34,6 +32,31 @@ lazy_static! { ).unwrap(); } +#[derive(Debug, Copy, Clone)] +/// Wrap an Compact Revocation document (in block content) +pub struct CompactCertificationDocument { + /// Issuer + pub issuer: ed25519::PublicKey, + /// Target + pub target: ed25519::PublicKey, + /// Blockstamp + pub block_number: BlockId, + /// Signature + pub signature: ed25519::Signature, +} + +impl CompactTextDocument for CompactCertificationDocument { + fn as_compact_text(&self) -> String { + format!( + "{issuer}:{target}:{block_number}:{signature}", + issuer = self.issuer, + target = self.target, + block_number = self.block_number.0, + signature = self.signature, + ) + } +} + /// Wrap an Certification document. /// /// Must be created by parsing a text document or using a builder. @@ -109,27 +132,28 @@ impl Document for CertificationDocument { } impl TextDocument for CertificationDocument { + type CompactTextDocument_ = CompactCertificationDocument; + fn as_text(&self) -> &str { &self.text } - fn generate_compact_text(&self) -> String { - format!( - "{issuer}:{target}:{block_number}:{signature}", - issuer = self.issuers[0], - target = self.target, - block_number = self.blockstamp.id.0, - signature = self.signatures[0], - ) + fn to_compact_document(&self) -> Self::CompactTextDocument_ { + CompactCertificationDocument { + issuer: self.issuers[0], + target: self.target, + block_number: self.blockstamp().id, + signature: self.signatures()[0], + } } } -impl Serialize for CertificationDocument { +impl Serialize for TextDocumentFormat<CertificationDocument> { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer, { - serializer.serialize_str(&self.generate_compact_text()) + serializer.serialize_str(&self.as_compact_text()) } } diff --git a/documents/blockchain/v10/documents/identity.rs b/documents/blockchain/v10/documents/identity.rs index 6e986565b04dc04222ff93ed31b3509b7fb4b7be..40bd02bb3c7dc5c1c7d52938028b8f25e14998e0 100644 --- a/documents/blockchain/v10/documents/identity.rs +++ b/documents/blockchain/v10/documents/identity.rs @@ -21,10 +21,7 @@ use self::serde::ser::{Serialize, Serializer}; use duniter_crypto::keys::{ed25519, PublicKey}; use regex::Regex; -use blockchain::v10::documents::{ - StandardTextDocumentParser, TextDocument, TextDocumentBuilder, V10Document, - V10DocumentParsingError, -}; +use blockchain::v10::documents::*; use blockchain::{BlockchainProtocol, Document, DocumentBuilder, IntoSpecializedDocument}; use Blockstamp; @@ -93,12 +90,8 @@ impl Document for IdentityDocument { } } -impl TextDocument for IdentityDocument { - fn as_text(&self) -> &str { - &self.text - } - - fn generate_compact_text(&self) -> String { +impl CompactTextDocument for IdentityDocument { + fn as_compact_text(&self) -> String { format!( "{issuer}:{signature}:{blockstamp}:{username}", issuer = self.issuers[0], @@ -109,6 +102,18 @@ impl TextDocument for IdentityDocument { } } +impl TextDocument for IdentityDocument { + type CompactTextDocument_ = IdentityDocument; + + fn as_text(&self) -> &str { + &self.text + } + + fn to_compact_document(&self) -> Self::CompactTextDocument_ { + self.clone() + } +} + impl Serialize for IdentityDocument { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where diff --git a/documents/blockchain/v10/documents/membership.rs b/documents/blockchain/v10/documents/membership.rs index 0cc53d51b5521471a4aa2719963e94a18704dbdf..2e313601e9f938d431ba33800582234a9ab15382 100644 --- a/documents/blockchain/v10/documents/membership.rs +++ b/documents/blockchain/v10/documents/membership.rs @@ -21,10 +21,7 @@ use self::serde::ser::{Serialize, Serializer}; use duniter_crypto::keys::{ed25519, PublicKey}; use regex::Regex; -use blockchain::v10::documents::{ - StandardTextDocumentParser, TextDocument, TextDocumentBuilder, V10Document, - V10DocumentParsingError, -}; +use blockchain::v10::documents::*; use blockchain::{BlockchainProtocol, Document, DocumentBuilder, IntoSpecializedDocument}; use Blockstamp; @@ -114,12 +111,8 @@ impl Document for MembershipDocument { } } -impl TextDocument for MembershipDocument { - fn as_text(&self) -> &str { - &self.text - } - - fn generate_compact_text(&self) -> String { +impl CompactTextDocument for MembershipDocument { + fn as_compact_text(&self) -> String { format!( "{issuer}:{signature}:{blockstamp}:{idty_blockstamp}:{username}", issuer = self.issuers[0], @@ -131,6 +124,18 @@ impl TextDocument for MembershipDocument { } } +impl TextDocument for MembershipDocument { + type CompactTextDocument_ = MembershipDocument; + + fn as_text(&self) -> &str { + &self.text + } + + fn to_compact_document(&self) -> Self::CompactTextDocument_ { + self.clone() + } +} + impl Serialize for MembershipDocument { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where diff --git a/documents/blockchain/v10/documents/mod.rs b/documents/blockchain/v10/documents/mod.rs index 3557543aabb1be765eb61f36933a895a55c5eba1..495190dd55e0d22a5e671b0c16f1cc8e6e0f2405 100644 --- a/documents/blockchain/v10/documents/mod.rs +++ b/documents/blockchain/v10/documents/mod.rs @@ -42,6 +42,8 @@ pub use blockchain::v10::documents::transaction::{ TransactionDocument, TransactionDocumentBuilder, TransactionDocumentParser, }; +use std::marker::Sized; + // Use of lazy_static so the regex is only compiled at first use. lazy_static! { static ref DOCUMENT_REGEX: Regex = Regex::new( @@ -54,6 +56,25 @@ lazy_static! { static ref SIGNATURES_REGEX: Regex = Regex::new("[[:alnum:]+/=]+\n?").unwrap(); } +#[derive(Debug, Clone)] +/// Contains a document in full or compact format +pub enum TextDocumentFormat<D: TextDocument> { + /// Complete format (Allows to check the validity of the signature) + Complete(D), + /// Format present in the blocks (does not always allow to verify the signature) + Compact(D::CompactTextDocument_), +} + +impl<D: TextDocument> TextDocumentFormat<D> { + /// To compact document + pub fn to_compact_document(&self) -> D::CompactTextDocument_ { + match *self { + TextDocumentFormat::Complete(ref doc) => doc.to_compact_document(), + TextDocumentFormat::Compact(ref compact_doc) => (*compact_doc).clone(), + } + } +} + /// List of wrapped document types. /// /// > TODO Add wrapped types in enum variants. @@ -78,8 +99,30 @@ pub enum V10Document { Revocation(Box<RevocationDocument>), } +/// Trait for a compact V10 document. +pub trait CompactTextDocument: Sized + Clone { + /// Generate document compact text. + /// the compact format is the one used in the blocks. + /// + /// - Don't contains leading signatures + /// - Contains line breaks on all line. + fn as_compact_text(&self) -> String; +} + +impl<D: TextDocument> CompactTextDocument for TextDocumentFormat<D> { + fn as_compact_text(&self) -> String { + match *self { + TextDocumentFormat::Complete(ref doc) => doc.generate_compact_text(), + TextDocumentFormat::Compact(ref doc) => doc.as_compact_text(), + } + } +} + /// Trait for a V10 document. pub trait TextDocument: Document<PublicKey = ed25519::PublicKey, CurrencyType = str> { + /// Type of associated compact document. + type CompactTextDocument_: CompactTextDocument; + /// Return document as text. fn as_text(&self) -> &str; @@ -100,12 +143,19 @@ pub trait TextDocument: Document<PublicKey = ed25519::PublicKey, CurrencyType = text } + /// Generate compact document. + /// the compact format is the one used in the blocks. + /// - Don't contains leading signatures + fn to_compact_document(&self) -> Self::CompactTextDocument_; + /// Generate document compact text. /// the compact format is the one used in the blocks. /// /// - Don't contains leading signatures /// - Contains line breaks on all line. - fn generate_compact_text(&self) -> String; + fn generate_compact_text(&self) -> String { + self.to_compact_document().as_compact_text() + } } /// Trait for a V10 document builder. diff --git a/documents/blockchain/v10/documents/revocation.rs b/documents/blockchain/v10/documents/revocation.rs index b954ae13b2ead8be53710732e66daadf3ae3eb26..d40d97046fe03fb391ae42598985aa8c143ffcde 100644 --- a/documents/blockchain/v10/documents/revocation.rs +++ b/documents/blockchain/v10/documents/revocation.rs @@ -21,10 +21,7 @@ use self::serde::ser::{Serialize, Serializer}; use duniter_crypto::keys::{ed25519, PublicKey, Signature}; use regex::Regex; -use blockchain::v10::documents::{ - StandardTextDocumentParser, TextDocument, TextDocumentBuilder, V10Document, - V10DocumentParsingError, -}; +use blockchain::v10::documents::*; use blockchain::{BlockchainProtocol, Document, DocumentBuilder, IntoSpecializedDocument}; use Blockstamp; @@ -37,6 +34,25 @@ lazy_static! { ).unwrap(); } +#[derive(Debug, Copy, Clone)] +/// Wrap an Compact Revocation document (in block content) +pub struct CompactRevocationDocument { + /// Issuer + pub issuer: ed25519::PublicKey, + /// Signature + pub signature: ed25519::Signature, +} + +impl CompactTextDocument for CompactRevocationDocument { + fn as_compact_text(&self) -> String { + format!( + "{issuer}:{signature}", + issuer = self.issuer, + signature = self.signature, + ) + } +} + /// Wrap an Revocation document. /// /// Must be created by parsing a text document or using a builder. @@ -98,25 +114,26 @@ impl Document for RevocationDocument { } impl TextDocument for RevocationDocument { + type CompactTextDocument_ = CompactRevocationDocument; + fn as_text(&self) -> &str { &self.text } - fn generate_compact_text(&self) -> String { - format!( - "{issuer}:{signature}", - issuer = self.issuers[0], - signature = self.signatures[0], - ) + fn to_compact_document(&self) -> Self::CompactTextDocument_ { + CompactRevocationDocument { + issuer: self.issuers[0], + signature: self.signatures[0], + } } } -impl Serialize for RevocationDocument { +impl Serialize for TextDocumentFormat<RevocationDocument> { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer, { - serializer.serialize_str(&self.generate_compact_text()) + serializer.serialize_str(&self.as_compact_text()) } } diff --git a/documents/blockchain/v10/documents/transaction.rs b/documents/blockchain/v10/documents/transaction.rs index efcf04a7e1252862277bde9898b73b450bf415ee..992640c8a6ce937f77e9e703803fa1791f6a8835 100644 --- a/documents/blockchain/v10/documents/transaction.rs +++ b/documents/blockchain/v10/documents/transaction.rs @@ -25,10 +25,7 @@ use regex::RegexBuilder; use self::serde::ser::{Serialize, Serializer}; -use blockchain::v10::documents::{ - StandardTextDocumentParser, TextDocument, TextDocumentBuilder, V10Document, - V10DocumentParsingError, -}; +use blockchain::v10::documents::*; use blockchain::{BlockchainProtocol, Document, DocumentBuilder, IntoSpecializedDocument}; use Blockstamp; @@ -442,12 +439,8 @@ impl Document for TransactionDocument { } } -impl TextDocument for TransactionDocument { - fn as_text(&self) -> &str { - &self.text - } - - fn generate_compact_text(&self) -> String { +impl CompactTextDocument for TransactionDocument { + fn as_compact_text(&self) -> String { let mut issuers_str = String::from(""); for issuer in self.issuers.clone() { issuers_str.push_str("\n"); @@ -499,12 +492,24 @@ impl TextDocument for TransactionDocument { } } +impl TextDocument for TransactionDocument { + type CompactTextDocument_ = TransactionDocument; + + fn as_text(&self) -> &str { + &self.text + } + + fn to_compact_document(&self) -> Self::CompactTextDocument_ { + self.clone() + } +} + impl Serialize for TransactionDocument { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer, { - let compact_text = self.generate_compact_text(); + let compact_text = self.to_compact_document().generate_compact_text(); serializer.serialize_str(&compact_text.replace("\n", "$")) } } diff --git a/message/Cargo.toml b/message/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..eb558403f94cc1389f09b552ba2ff9b04dfc36c8 --- /dev/null +++ b/message/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "duniter-message" +version = "0.1.0" +authors = ["librelois <elois@duniter.org>"] +description = "message model for the Duniter project." +license = "AGPL-3.0" + +[lib] +path = "lib.rs" + +[dependencies] +duniter-crypto = { path = "../crypto" } +duniter-dal = { path = "../dal" } +duniter-documents = { path = "../documents" } +duniter-module = { path = "../module" } +duniter-network = { path = "../network" } +serde = "1.0.24" +serde_derive = "1.0.24" +serde_json = "1.0.9" + +[features] +# Treat warnings as a build error. +strict = [] \ No newline at end of file diff --git a/message/lib.rs b/message/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..f99560f56c18f75c029bdec43ac648b762d5fdd1 --- /dev/null +++ b/message/lib.rs @@ -0,0 +1,75 @@ +// Copyright (C) 2018 The Duniter Project Developers. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! Defined the few global types used by all modules, +//! as well as the DuniterModule trait that all modules must implement. + +#![cfg_attr(feature = "strict", deny(warnings))] +#![deny( + missing_docs, missing_debug_implementations, missing_copy_implementations, trivial_casts, + trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces, + unused_qualifications +)] + +extern crate duniter_crypto; +extern crate duniter_dal; +extern crate duniter_documents; +extern crate duniter_module; +extern crate duniter_network; +extern crate serde; +extern crate serde_json; + +use std::sync::mpsc; + +use duniter_crypto::keys::ed25519; +use duniter_dal::dal_event::DALEvent; +use duniter_dal::dal_requests::{DALRequest, DALResponse}; +use duniter_documents::blockchain::BlockchainProtocol; +use duniter_documents::{BlockId, Hash}; +use duniter_module::ModuleMessage; +use duniter_network::{NetworkEvent, NetworkRequest}; + +#[derive(Debug, Clone)] +/// Message exchanged between Duniter-rs modules +pub enum DuniterMessage { + /// Brut text message + Text(String), + /// Brut json message + Json(serde_json::Value), + /// Brut binary message + Binary(Vec<u8>), + /// Subscriptions to the module feed + Followers(Vec<mpsc::Sender<DuniterMessage>>), + /// Blockchain datas request + DALRequest(DALRequest), + /// Response of DALRequest + DALResponse(DALResponse), + /// Blockchain event + DALEvent(DALEvent), + /// Request to the network module + NetworkRequest(NetworkRequest), + /// Network event + NetworkEvent(NetworkEvent), + /// Request to the pow module + ProverRequest(BlockId, Hash), + /// Pow module response + ProverResponse(BlockId, ed25519::Signature, u64), + /// Client API event + ReceiveDocsFromClient(Vec<BlockchainProtocol>), + /// Stop signal + Stop(), +} + +impl ModuleMessage for DuniterMessage {} diff --git a/module/Cargo.toml b/module/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..bfc08e52a41fa92145972b86a5c87fddcc2a3151 --- /dev/null +++ b/module/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "duniter-module" +version = "0.1.0" +authors = ["librelois <elois@duniter.org>"] +description = "Modules model for the Duniter project." +license = "AGPL-3.0" + +[lib] +path = "lib.rs" + +[dependencies] +duniter-crypto = { path = "../crypto" } +duniter-documents = { path = "../documents" } +serde = "1.0.24" +serde_derive = "1.0.24" +serde_json = "1.0.9" + +[features] +# Treat warnings as a build error. +strict = [] \ No newline at end of file diff --git a/module/lib.rs b/module/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..75640b34d81fc9b912b0e5a2f77947954434cd9a --- /dev/null +++ b/module/lib.rs @@ -0,0 +1,274 @@ +// Copyright (C) 2018 The Duniter Project Developers. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! Defined the few global types used by all modules, +//! as well as the DuniterModule trait that all modules must implement. + +#![cfg_attr(feature = "strict", deny(warnings))] +#![deny( + missing_docs, missing_debug_implementations, missing_copy_implementations, trivial_casts, + trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces, + unused_qualifications +)] + +extern crate duniter_crypto; +extern crate serde; +extern crate serde_json; + +use duniter_crypto::keys::KeyPair; +use serde::ser::{Serialize, SerializeStruct, Serializer}; +use std::fmt::Debug; +use std::sync::mpsc; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +/// Store Currency +pub enum Currency { + /// Currency in string format + Str(String), + /// Currency in binary format + Bin([u8; 2]), +} + +impl ToString for Currency { + fn to_string(&self) -> String { + match *self { + Currency::Str(ref currency_str) => currency_str.clone(), + Currency::Bin(_) => panic!("Currency binary format is not implemented !"), + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +/// Store module identifier +pub enum ModuleId { + /// Module in static str format because module name must be know at compile time + Str(&'static str), + /// Module in binary format + Bin([u8; 2]), +} + +impl ToString for ModuleId { + fn to_string(&self) -> String { + match *self { + ModuleId::Str(module_id_str) => String::from(module_id_str), + ModuleId::Bin(_) => panic!("ModuleId binary format is not implemented !"), + } + } +} + +impl Serialize for ModuleId { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let module_id_string = match *self { + ModuleId::Str(module_id_str) => String::from(module_id_str), + ModuleId::Bin(_) => panic!("ModuleId binary format is not implemented !"), + }; + serializer.serialize_str(module_id_string.as_str()) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +/// Identifier of an inter-module request +pub struct ModuleReqId(pub u32); + +impl Serialize for ModuleReqId { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serializer.serialize_str(&format!("{:x}", self.0)) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +/// Several modules can simultaneously send requests with the same identifier. +/// To identify each request in a unique way, we must therefore also take into account the identifier of the module performing the request. +pub struct ModuleReqFullId(pub ModuleId, pub ModuleReqId); + +impl ToString for ModuleReqFullId { + fn to_string(&self) -> String { + format!("{}-{}", self.0.to_string(), (self.1).0) + } +} + +/*impl Serialize for ModuleReqFullId { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serializer.serialize_str(&format!("{}-{}", self.0.to_string(), (self.1).0)) + } +}*/ + +#[derive(Debug, Clone, PartialEq)] +/// Duniter configuration v1 +pub struct DuniterConfV1 { + /// Name of datas folder in ~/.config/durs/ + pub profile: String, + /// Currency + pub currency: Currency, + /// Duniter node unique identifier + pub my_node_id: u32, + /// Configuration of modules in json format (obtained from the conf.json file) + pub modules: serde_json::Value, +} + +impl Serialize for DuniterConfV1 { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let mut state = serializer.serialize_struct("DuniterConfV1", 3)?; + + // Currency + state.serialize_field("currency", self.currency.to_string().as_str())?; + + // Node id + state.serialize_field("node_id", &format!("{:x}", self.my_node_id))?; + + // Modules + state.serialize_field("modules", &self.modules)?; + + // End + state.end() + } +} + +#[derive(Debug, Clone, PartialEq)] +/// Duniter node configuration +pub enum DuniterConf { + /// Duniter node configuration v1 + V1(DuniterConfV1), + /// Duniter node configuration v2 + V2(), +} + +impl DuniterConf { + /// Get profile + pub fn profile(&self) -> String { + match *self { + DuniterConf::V1(ref conf_v1) => conf_v1.profile.clone(), + _ => panic!("Fail to load duniter conf : conf version not supported !"), + } + } + /// Get currency + pub fn currency(&self) -> Currency { + match *self { + DuniterConf::V1(ref conf_v1) => conf_v1.currency.clone(), + _ => panic!("Fail to load duniter conf : conf version not supported !"), + } + } + /// Get node id + pub fn my_node_id(&self) -> u32 { + match *self { + DuniterConf::V1(ref conf_v1) => conf_v1.my_node_id, + _ => panic!("Fail to load duniter conf : conf version not supported !"), + } + } + /// Get modules conf + pub fn modules(&self) -> serde_json::Value { + match *self { + DuniterConf::V1(ref conf_v1) => conf_v1.modules.clone(), + _ => panic!("Fail to load duniter conf : conf version not supported !"), + } + } +} + +/// The different modules of Duniter-rs can exchange messages with the type of their choice, +/// provided that this type implements the ModuleMessage trait. +pub trait ModuleMessage: Debug + Clone {} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +/// Type returned by module initialization function +pub enum ModuleInitError { + /// Fail to load configuration + FailToLoadConf(), + /// Unknow error + UnknowError(), +} + +#[derive(Debug, Clone)] +/// Type sent by each module to the rooter during initialization +pub enum RooterThreadMessage<M: ModuleMessage> { + /// Channel on which the module listens + ModuleSender(mpsc::Sender<M>), + /// When the number of plugged modules is known, the rooter thread must be informed of the number of modules it must connect between them. + ModulesCount(usize), +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +/// Indicates which keys the module needs to operate +pub enum RequiredKeys { + /// The module needs the member keypair (private key included). + MemberKeyPair(), + /// The module only needs the member public key. + MemberPublicKey(), + /// The module needs the network keypair (private key included). + NetworkKeyPair(), + /// The module only needs the network public key. + NetworkPublicKey(), + /// The module does not need any key + None(), +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +/// Contains the keys the module needs +pub enum RequiredKeysContent<K: KeyPair> { + /// Contains the member keypair (private key included). + MemberKeyPair(Option<K>), + /// Contains the member public key. + MemberPublicKey(Option<K::PublicKey>), + /// Contains the network keypair (private key included). + NetworkKeyPair(K), + /// Contains the network public key. + NetworkPublicKey(K::PublicKey), + /// Does not contain any keys + None(), +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +/// Defined the priority level of the module +pub enum ModulePriority { + /// This module is necessary for Duniter-Rs to work properly, impossible to disable it. + Essential(), + /// This module is recommended but it's not essential, it's enabled by default but can be disabled by the user. + Recommended(), + /// This module is disabled by default, it must be explicitly enabled by the user. + Optional(), +} + +/// All Duniter-rs modules must implement this trait. +pub trait DuniterModule<K: KeyPair, M: ModuleMessage> { + /// Returns the module identifier + fn id() -> ModuleId; + /// Returns the module priority + fn priority() -> ModulePriority; + /// Indicates which keys the module needs + fn ask_required_keys() -> RequiredKeys; + /// Provides the default module configuration + fn default_conf() -> serde_json::Value; + /// Launch the module + fn start( + soft_name: &str, + soft_version: &str, + keys: RequiredKeysContent<K>, + conf: &DuniterConf, + module_conf: &serde_json::Value, + main_sender: mpsc::Sender<RooterThreadMessage<M>>, + load_conf_only: bool, + ) -> Result<(), ModuleInitError>; +} diff --git a/network/Cargo.toml b/network/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..ab7bd3aca3cb068cc4c3c9de0682521334e86598 --- /dev/null +++ b/network/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "duniter-network" +version = "0.1.0" +authors = ["librelois <elois@duniter.org>"] +description = "network model for the Duniter project." +license = "AGPL-3.0" + +[lib] +path = "lib.rs" + +[dependencies] +duniter-crypto = { path = "../crypto" } +duniter-documents = { path = "../documents" } +duniter-module = { path = "../module" } +lazy_static = "1.0.0" +regex = "0.2.6" +rust-crypto = "0.2.36" +serde = "1.0.24" +serde_derive = "1.0.24" +serde_json = "1.0.9" + +[features] +# Treat warnings as a build error. +strict = [] \ No newline at end of file diff --git a/network/lib.rs b/network/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..9b477cb2a3549deb248f757ccf3bf7ea1d621eca --- /dev/null +++ b/network/lib.rs @@ -0,0 +1,338 @@ +// Copyright (C) 2018 The Duniter Project Developers. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! Defined all aspects of the inter-node network that concern all modules and are therefore independent of one implementation or another of this network layer. + +#![cfg_attr(feature = "strict", deny(warnings))] +#![deny( + missing_docs, missing_debug_implementations, missing_copy_implementations, trivial_casts, + trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces, + unused_qualifications +)] + +#[macro_use] +extern crate lazy_static; + +extern crate crypto; +extern crate duniter_crypto; +extern crate duniter_documents; +extern crate duniter_module; +extern crate serde; +extern crate serde_json; + +pub mod network_endpoint; +pub mod network_head; +pub mod network_peer; + +use self::network_head::NetworkHead; +use self::network_peer::NetworkPeer; +use crypto::digest::Digest; +use crypto::sha2::Sha256; +use duniter_crypto::keys::{ed25519, PublicKey}; +use duniter_documents::blockchain::v10::documents::{ + BlockDocument, CertificationDocument, IdentityDocument, MembershipDocument, RevocationDocument, + TransactionDocument, +}; +use duniter_documents::blockchain::Document; +use duniter_documents::{BlockHash, BlockId, Blockstamp, Hash}; +use duniter_module::{ModuleReqFullId, ModuleReqId}; +use std::fmt::{Debug, Display, Error, Formatter}; +use std::ops::Deref; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +/// Random identifier with which several Duniter nodes with the same network keypair can be differentiated +pub struct NodeUUID(pub u32); + +impl Default for NodeUUID { + fn default() -> NodeUUID { + NodeUUID(0) + } +} + +impl Display for NodeUUID { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!(f, "{:x}", self.0) + } +} + +impl<'a> From<&'a str> for NodeUUID { + fn from(source: &'a str) -> NodeUUID { + NodeUUID(u32::from_str_radix(source, 16).expect("Fail to parse NodeUUID")) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +/// Complete identifier of a duniter node. +pub struct NodeFullId(pub NodeUUID, pub ed25519::PublicKey); + +impl Default for NodeFullId { + fn default() -> NodeFullId { + NodeFullId( + NodeUUID::default(), + PublicKey::from_base58("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").unwrap(), + ) + } +} + +impl Display for NodeFullId { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!(f, "{}-{}", self.0, self.1) + } +} + +impl NodeFullId { + /// Compute sha256 hash + pub fn sha256(&self) -> Hash { + let mut sha256 = Sha256::new(); + sha256.input_str(&format!("{}", self)); + Hash::from_hex(&sha256.result_str()).unwrap() + } + /// To human string + pub fn to_human_string(&self) -> String { + let mut pubkey_string = self.1.to_string(); + pubkey_string.truncate(8); + format!("{:8x}-{:8}", (self.0).0, pubkey_string) + } +} + +/// Trait to be implemented by the configuration object of the module managing the inter-node network. +pub trait NetworkConf: Debug + Copy + Clone + PartialEq {} + +#[derive(Debug, Clone)] +/// Block v10 in network format (Some events require a blockchain access to reconstitute the corresponding document) +pub struct NetworkBlockV10 { + /// Uncompleted block document + pub uncompleted_block_doc: BlockDocument, + /// revoked + pub revoked: Vec<serde_json::Value>, + /// certifications + pub certifications: Vec<serde_json::Value>, +} + +#[derive(Debug, Clone)] +/// Block in network format (Some events require a blockchain access to reconstitute the corresponding document) +pub enum NetworkBlock { + /// Block V10 + V10(Box<NetworkBlockV10>), + /// Block V11 + V11(), +} + +impl NetworkBlock { + /// Return blockstamp + pub fn blockstamp(&self) -> Blockstamp { + match *self { + NetworkBlock::V10(ref network_block_v10) => { + network_block_v10.deref().uncompleted_block_doc.blockstamp() + } + _ => panic!("Block version not supported !"), + } + } + /// Return previous blockstamp + pub fn previous_blockstamp(&self) -> Blockstamp { + match *self { + NetworkBlock::V10(ref network_block_v10) => Blockstamp { + id: BlockId(network_block_v10.deref().uncompleted_block_doc.number.0 - 1), + hash: BlockHash( + network_block_v10 + .deref() + .uncompleted_block_doc + .previous_hash, + ), + }, + _ => panic!("Block version not supported !"), + } + } +} + +#[derive(Debug, Clone)] +/// Network Document +pub enum NetworkDocument { + /// Network Block + Block(NetworkBlock), + /// Identity Document + Identity(Box<IdentityDocument>), + /// Membership Document + Membership(Box<MembershipDocument>), + /// Certification Document + Certification(Box<CertificationDocument>), + /// Revocation Document + Revocation(Box<RevocationDocument>), + /// Transaction Document + Transaction(Box<TransactionDocument>), +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +/// Type returned when the network module fails to determine the current network consensus +pub enum NetworkConsensusError { + /// The network module does not have enough data to determine consensus + InsufficientData(usize), + /// The network module does not determine consensus, there is most likely a fork + Fork(), +} + +#[derive(Debug, Copy, Clone)] +/// Type containing a request addressed to the network module +pub enum NetworkRequest { + /// Get a current block of a specific node + GetCurrent(ModuleReqFullId, NodeFullId), + //GetBlock(ModuleReqFullId, NodeFullId, u64), + /// Get a blocks chunk from specified node + GetBlocks(ModuleReqFullId, NodeFullId, u32, u32), + /// Get pending wot documents from specified node + GetRequirementsPending(ModuleReqFullId, NodeFullId, u32), + /// Obtain the current network consensus + GetConsensus(ModuleReqFullId), + /// Getting the heads cache + GetHeadsCache(ModuleReqFullId), + /// Get a list of known endpoints + GetEndpoints(ModuleReqFullId), +} + +impl NetworkRequest { + /// Get request full identitifier + pub fn get_req_full_id(&self) -> ModuleReqFullId { + match *self { + NetworkRequest::GetCurrent(ref req_id, _) + | NetworkRequest::GetBlocks(ref req_id, _, _, _) + | NetworkRequest::GetRequirementsPending(ref req_id, _, _) + | NetworkRequest::GetConsensus(ref req_id) + | NetworkRequest::GetHeadsCache(ref req_id) + | NetworkRequest::GetEndpoints(ref req_id) => *req_id, + } + } + /// Get request identitifier + pub fn get_req_id(&self) -> ModuleReqId { + self.get_req_full_id().1 + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +/// Type returned when the network module does not get a satisfying answer to a request +pub enum NetworkRequestError { + /// Receiving an invalid format response + WrongFormat(), + /// Unknow error + UnknowError(), + /// No response received + NoResponse(), + /// Unable to reach the target node + ReceiverUnreachable(), +} + +#[derive(Debug, Clone)] +/// Type containing the response to a network request +pub enum NetworkResponse { + /// CurrentBlock + CurrentBlock(ModuleReqFullId, NodeFullId, Box<NetworkBlock>), + /// Block + Block(ModuleReqFullId, NodeFullId, Box<NetworkBlock>), + /// Chunk + Chunk(ModuleReqFullId, NodeFullId, Vec<Box<NetworkBlock>>), + /// PendingDocuments + PendingDocuments(ModuleReqFullId, Vec<NetworkDocument>), + /// Consensus + Consensus(ModuleReqFullId, Result<Blockstamp, NetworkConsensusError>), + /// HeadsCache + HeadsCache(ModuleReqFullId, Box<NetworkHead>), +} + +impl NetworkResponse { + /// Get request full identifier + pub fn get_req_full_id(&self) -> ModuleReqFullId { + match *self { + NetworkResponse::CurrentBlock(ref req_id, _, _) + | NetworkResponse::Block(ref req_id, _, _) + | NetworkResponse::Chunk(ref req_id, _, _) + | NetworkResponse::PendingDocuments(ref req_id, _) + | NetworkResponse::Consensus(ref req_id, _) + | NetworkResponse::HeadsCache(ref req_id, _) => *req_id, + } + } + /// Get request identifier + pub fn get_req_id(&self) -> ModuleReqId { + self.get_req_full_id().1 + } +} + +#[derive(Debug, Clone)] +/// Type containing a network event, each time a network event occurs it's relayed to all modules +pub enum NetworkEvent { + /// Receiving a response to a network request + ReqResponse(Box<NetworkResponse>), + /// A connection has changed state(`u32` is the new state, `Option<String>` est l'uid du noeud) + ConnectionStateChange(NodeFullId, u32, Option<String>, String), + /// Receiving Pending Documents + ReceiveDocuments(Vec<NetworkDocument>), + /// Receipt of peer cards + ReceivePeers(Vec<NetworkPeer>), + /// Receiving heads + ReceiveHeads(Vec<NetworkHead>), +} + +#[cfg(test)] +mod tests { + + use super::network_endpoint::*; + use super::*; + + #[test] + fn parse_endpoint() { + let issuer = + PublicKey::from_base58("D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx").unwrap(); + let node_id = NodeUUID(u32::from_str_radix("c1c39a0a", 16).unwrap()); + let full_id = NodeFullId(node_id, issuer); + assert_eq!( + NetworkEndpoint::parse_from_raw("WS2P c1c39a0a i3.ifee.fr 80 /ws2p", issuer, 0, 0), + Some(NetworkEndpoint::V1(NetworkEndpointV1 { + version: 1, + issuer, + api: NetworkEndpointApi(String::from("WS2P")), + node_id: Some(node_id), + hash_full_id: Some(full_id.sha256()), + host: String::from("i3.ifee.fr"), + port: 80, + path: Some(String::from("ws2p")), + raw_endpoint: String::from("WS2P c1c39a0a i3.ifee.fr 80 /ws2p"), + last_check: 0, + status: 0, + },)) + ); + } + + #[test] + fn parse_endpoint2() { + let issuer = + PublicKey::from_base58("5gJYnQp8v7bWwk7EWRoL8vCLof1r3y9c6VDdnGSM1GLv").unwrap(); + let node_id = NodeUUID(u32::from_str_radix("cb06a19b", 16).unwrap()); + let full_id = NodeFullId(node_id, issuer); + assert_eq!( + NetworkEndpoint::parse_from_raw("WS2P cb06a19b g1.imirhil.fr 53012 /", issuer, 0, 0), + Some(NetworkEndpoint::V1(NetworkEndpointV1 { + version: 1, + issuer, + api: NetworkEndpointApi(String::from("WS2P")), + node_id: Some(node_id), + hash_full_id: Some(full_id.sha256()), + host: String::from("g1.imirhil.fr"), + port: 53012, + path: None, + raw_endpoint: String::from("WS2P cb06a19b g1.imirhil.fr 53012 /"), + last_check: 0, + status: 0, + },)) + ); + } +} diff --git a/network/network_endpoint.rs b/network/network_endpoint.rs new file mode 100644 index 0000000000000000000000000000000000000000..53e3ae82947ebecdfca1c757d1164e39096e9498 --- /dev/null +++ b/network/network_endpoint.rs @@ -0,0 +1,217 @@ +// Copyright (C) 2017 The Duniter Project Developers. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! Module defining the format of network endpoints and how to handle them. + +extern crate crypto; +extern crate duniter_crypto; +extern crate duniter_documents; +extern crate duniter_module; +extern crate regex; +extern crate serde; +extern crate serde_json; + +use self::regex::Regex; +use super::{NodeFullId, NodeUUID}; +use duniter_crypto::keys::ed25519; +use duniter_documents::Hash; + +lazy_static! { + #[derive(Debug)] + /// Regex match all endpoint in V1 format (works for all api) + pub static ref ENDPOINT_V1_REGEX: Regex = Regex::new( + r"^(?P<api>[A-Z0-9_]+) (?P<version>[1-9][0-9]*)? ?(?P<uuid>[a-f0-9]{6,8})? ?(?P<host>[a-z_][a-z0-9-_.]*|[0-9.]+|[0-9a-f:]+) (?P<port>[0-9]+)(?: /?(?P<path>.+)?)? *$" + ).unwrap(); +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +/// Identifies the API of an endpoint +pub struct NetworkEndpointApi(pub String); + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +/// Endpoint v1 +pub struct NetworkEndpointV1 { + /// API version + pub version: usize, + /// API Name + pub api: NetworkEndpointApi, + /// Node unique identifier + pub node_id: Option<NodeUUID>, + /// Public key of the node declaring this endpoint + pub issuer: ed25519::PublicKey, + /// NodeFullID hash + pub hash_full_id: Option<Hash>, + /// hostname + pub host: String, + /// port number + pub port: usize, + /// Optional path + pub path: Option<String>, + /// Endpoint in raw format (as it appears on the peer card) + pub raw_endpoint: String, + /// Accessibility status of this endpoint (updated regularly) + pub status: u32, + /// Timestamp of the last connection attempt to this endpoint + pub last_check: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +/// Endpoint +pub enum NetworkEndpoint { + /// Endpoint v1 + V1(NetworkEndpointV1), + /// Endpoint v2 + V2(), +} + +impl ToString for NetworkEndpoint { + fn to_string(&self) -> String { + match *self { + NetworkEndpoint::V1(ref ep) => ep.raw_endpoint.clone(), + _ => panic!("Endpoint version is not supported !"), + } + } +} + +impl NetworkEndpoint { + /// Accessors providing API name + pub fn api(&self) -> NetworkEndpointApi { + match *self { + NetworkEndpoint::V1(ref ep) => ep.api.clone(), + _ => panic!("Endpoint version is not supported !"), + } + } + /// Accessors providing node unique identifier + pub fn node_uuid(&self) -> Option<NodeUUID> { + match *self { + NetworkEndpoint::V1(ref ep) => ep.node_id, + _ => panic!("Endpoint version is not supported !"), + } + } + /// Accessors providing node public key + pub fn pubkey(&self) -> ed25519::PublicKey { + match *self { + NetworkEndpoint::V1(ref ep) => ep.issuer, + _ => panic!("Endpoint version is not supported !"), + } + } + /// Accessors providing node full identifier + pub fn node_full_id(&self) -> Option<NodeFullId> { + match self.node_uuid() { + Some(node_id) => Some(NodeFullId(node_id, self.pubkey())), + None => None, + } + } + /// Accessors providing port number + pub fn port(&self) -> usize { + match *self { + NetworkEndpoint::V1(ref ep) => ep.port, + _ => panic!("Endpoint version is not supported !"), + } + } + /// Accessors providing raw format + pub fn raw(&self) -> String { + match *self { + NetworkEndpoint::V1(ref ep) => ep.raw_endpoint.clone(), + _ => panic!("Endpoint version is not supported !"), + } + } + /// Accessors providing endpoint accessibility status + pub fn status(&self) -> u32 { + match *self { + NetworkEndpoint::V1(ref ep) => ep.status, + _ => panic!("Endpoint version is not supported !"), + } + } + /// Set status + pub fn set_status(&mut self, new_status: u32) { + match *self { + NetworkEndpoint::V1(ref mut ep) => ep.status = new_status, + _ => panic!("Endpoint version is not supported !"), + } + } + /// Set last_check + pub fn set_last_check(&mut self, new_last_check: u64) { + match *self { + NetworkEndpoint::V1(ref mut ep) => ep.last_check = new_last_check, + _ => panic!("Endpoint version is not supported !"), + } + } + /// Generate endpoint url + pub fn get_url(&self, get_protocol: bool) -> String { + match *self { + NetworkEndpoint::V1(ref ep) => { + let protocol = match &ep.api.0[..] { + "WS2P" | "WS2PTOR" => "ws", + _ => "http", + }; + let tls = match ep.port { + 443 => "s", + _ => "", + }; + let path = match ep.path { + Some(ref path_string) => path_string.clone(), + None => String::new(), + }; + if get_protocol { + format!("{}{}://{}:{}/{}", protocol, tls, ep.host, ep.port, path) + } else { + format!("{}:{}/{}", ep.host, ep.port, path) + } + } + _ => panic!("Endpoint version is not supported !"), + } + } + /// Parse Endpoint from rax format + pub fn parse_from_raw( + raw_endpoint: &str, + issuer: ed25519::PublicKey, + status: u32, + last_check: u64, + ) -> Option<NetworkEndpoint> { + match ENDPOINT_V1_REGEX.captures(raw_endpoint) { + Some(caps) => { + let node_id = match caps.name("uuid") { + Some(caps_node_id) => match u32::from_str_radix(caps_node_id.as_str(), 16) { + Ok(node_id) => Some(NodeUUID(node_id)), + Err(_) => None, + }, + None => None, + }; + let hash_full_id = match node_id { + Some(node_id_) => Some(NodeFullId(node_id_, issuer).sha256()), + None => None, + }; + Some(NetworkEndpoint::V1(NetworkEndpointV1 { + version: 1, + issuer, + api: NetworkEndpointApi(String::from(&caps["api"])), + node_id, + hash_full_id, + host: String::from(&caps["host"]), + port: caps["port"].parse().unwrap_or(80), + path: match caps.name("path") { + Some(m) => Some(m.as_str().to_string()), + None => None, + }, + raw_endpoint: String::from(raw_endpoint), + status, + last_check, + })) + } + None => None, + } + } +} diff --git a/network/network_head.rs b/network/network_head.rs new file mode 100644 index 0000000000000000000000000000000000000000..3e248386dd0528e25d7aef123a4ac9c62eb3f220 --- /dev/null +++ b/network/network_head.rs @@ -0,0 +1,430 @@ +// Copyright (C) 2017 The Duniter Project Developers. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! Module defining the format of network heads and how to handle them. + +extern crate duniter_crypto; +extern crate duniter_documents; +extern crate serde_json; + +use super::{NodeFullId, NodeUUID}; +use duniter_crypto::keys::{ed25519, PublicKey, Signature}; +use duniter_documents::Blockstamp; +use std::cmp::Ordering; +use std::collections::HashMap; +use std::ops::Deref; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +/// Head Message V2 +pub struct NetworkHeadMessageV2 { + /// API details + pub api: String, + /// Head version + pub version: usize, + /// Head pubkey + pub pubkey: ed25519::PublicKey, + /// Head blockstamp + pub blockstamp: Blockstamp, + /// Head node id + pub node_uuid: NodeUUID, + /// Issuer node software + pub software: String, + /// Issuer node soft version + pub soft_version: String, + /// Issuer node prefix + pub prefix: usize, + /// Issuer node free member room + pub free_member_room: Option<usize>, + /// Issuer node free mirror room + pub free_mirror_room: Option<usize>, +} + +impl PartialOrd for NetworkHeadMessageV2 { + fn partial_cmp(&self, other: &NetworkHeadMessageV2) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl Ord for NetworkHeadMessageV2 { + fn cmp(&self, other: &NetworkHeadMessageV2) -> Ordering { + self.blockstamp.cmp(&other.blockstamp) + } +} + +impl NetworkHeadMessageV2 { + /// To human readable string + pub fn to_human_string(&self, max_len: usize, uid: Option<String>) -> String { + let short_api = &self.api[4..]; + + if max_len > 80 && uid.is_some() { + format!( + "{node_id:8}-{pubkey:.8} {blockstamp:.16} {soft:7}:{ver:7} {pre:3} [{api:5}] {mer:02}:{mir:02} {uid}", + node_id = self.node_uuid.to_string(), + pubkey = self.pubkey.to_string(), + blockstamp = self.blockstamp.to_string(), + soft = self.software, + ver = self.soft_version, + pre = self.prefix, + api = short_api, + mer = self.free_member_room.unwrap_or(0), + mir = self.free_mirror_room.unwrap_or(0), + uid = uid.unwrap(), + ) + } else if max_len > 67 { + format!( + "{node_id:8}-{pubkey:.8} {blockstamp:.16} {soft:7}:{ver:7} {pre:3} [{api:5}] {mer:02}:{mir:02}", + node_id = self.node_uuid.to_string(), + pubkey = self.pubkey.to_string(), + blockstamp = self.blockstamp.to_string(), + soft = self.software, + ver = self.soft_version, + pre = self.prefix, + api = short_api, + mer = self.free_member_room.unwrap_or(0), + mir = self.free_mirror_room.unwrap_or(0), + ) + } else if max_len > 63 { + format!( + "{node_id:8}-{pubkey:.8} {blockstamp:.16} {soft:7}:{ver:7} [{api:5}] {mer:02}:{mir:02}", + node_id = self.node_uuid.to_string(), + pubkey = self.pubkey.to_string(), + blockstamp = self.blockstamp.to_string(), + soft = self.software, + ver = self.soft_version, + api = short_api, + mer = self.free_member_room.unwrap_or(0), + mir = self.free_mirror_room.unwrap_or(0), + ) + } else if max_len > 47 { + format!( + "{node_id:8}-{pubkey:.8} {blockstamp:.16} [{api:5}] {mer:02}:{mir:02}", + node_id = self.node_uuid.to_string(), + pubkey = self.pubkey.to_string(), + blockstamp = self.blockstamp.to_string(), + api = short_api, + mer = self.free_member_room.unwrap_or(0), + mir = self.free_mirror_room.unwrap_or(0), + ) + } else if max_len > 41 { + format!( + "{node_id:8}-{pubkey:.8} {blockstamp:.16} [{api:5}]", + node_id = self.node_uuid.to_string(), + pubkey = self.pubkey.to_string(), + blockstamp = self.blockstamp.to_string(), + api = short_api, + ) + } else { + String::from("Term width insufficient") + } + } +} + +#[derive(Debug, Clone, Eq, Ord, PartialOrd, PartialEq, Hash)] +/// Head Message +pub enum NetworkHeadMessage { + /// Head Message V2 + V2(NetworkHeadMessageV2), + /// Head Message V3 + V3(), +} + +impl NetworkHeadMessage { + /// To human readable string + pub fn to_human_string(&self, max_len: usize, uid: Option<String>) -> String { + match *self { + NetworkHeadMessage::V2(ref mess_v2) => mess_v2.deref().to_human_string(max_len, uid), + _ => panic!("NetworkHead version not supported !"), + } + } + /// Parse head from string + fn from_str(source: &str) -> Option<NetworkHeadMessage> { + let source_array: Vec<&str> = source.split(':').collect(); + if let Ok(pubkey) = PublicKey::from_base58(&source_array[3].to_string()) { + Some(NetworkHeadMessage::V2(NetworkHeadMessageV2 { + api: source_array[0].to_string(), + version: source_array[2].parse().unwrap(), + pubkey, + blockstamp: Blockstamp::from_string(source_array[4]).unwrap(), + node_uuid: NodeUUID(u32::from_str_radix(source_array[5], 16).unwrap()), + software: source_array[6].to_string(), + soft_version: source_array[7].to_string(), + prefix: source_array[8].parse().unwrap(), + free_member_room: if let Some(field) = source_array.get(9) { + Some(field.parse().unwrap()) + } else { + None + }, + free_mirror_room: if let Some(field) = source_array.get(10) { + Some(field.parse().unwrap()) + } else { + None + }, + })) + } else { + None + } + } + /// Get head blockcstamp + fn blockstamp(&self) -> Blockstamp { + match *self { + NetworkHeadMessage::V2(ref head_message_v2) => head_message_v2.blockstamp, + _ => panic!("This HEAD version is not supported !"), + } + } + /// Get head node id + fn node_uuid(&self) -> NodeUUID { + match *self { + NetworkHeadMessage::V2(ref head_message_v2) => head_message_v2.node_uuid, + _ => panic!("This HEAD version is not supported !"), + } + } + /// Get head issuer public key + fn _pubkey(&self) -> ed25519::PublicKey { + match *self { + NetworkHeadMessage::V2(ref head_message_v2) => head_message_v2.pubkey, + _ => panic!("This HEAD version is not supported !"), + } + } +} + +impl ToString for NetworkHeadMessageV2 { + fn to_string(&self) -> String { + match self.version { + 1 => format!( + "{}:HEAD:1:{}:{}:{}:{}:{}:{}", + self.api, + self.pubkey, + self.blockstamp, + self.node_uuid, + self.software, + self.soft_version, + self.prefix + ), + 2 => format!( + "{}:HEAD:2:{}:{}:{}:{}:{}:{}:{}:{}", + self.api, + self.pubkey, + self.blockstamp, + self.node_uuid, + self.software, + self.soft_version, + self.prefix, + self.free_member_room.unwrap(), + self.free_mirror_room.unwrap() + ), + _ => panic!("NetworkHeadMessage is wrongly parsed !"), + } + } +} + +impl ToString for NetworkHeadMessage { + fn to_string(&self) -> String { + match *self { + NetworkHeadMessage::V2(ref head_message) => head_message.to_string(), + _ => panic!("This HEADMessage version is not supported !"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +/// Head V2 +pub struct NetworkHeadV2 { + /// Head V1 Message + pub message: NetworkHeadMessage, + /// signature of V1 Message + pub sig: ed25519::Signature, + /// Head V2 Message + pub message_v2: NetworkHeadMessage, + /// signature of V2 Message + pub sig_v2: ed25519::Signature, + /// Head step + pub step: u32, + /// Head issuer uid + pub uid: Option<String>, +} + +impl ToString for NetworkHeadV2 { + fn to_string(&self) -> String { + self.message_v2.to_string() + } +} + +impl PartialOrd for NetworkHeadV2 { + fn partial_cmp(&self, other: &NetworkHeadV2) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl Ord for NetworkHeadV2 { + fn cmp(&self, other: &NetworkHeadV2) -> Ordering { + self.message.cmp(&other.message) + } +} + +impl NetworkHeadV2 { + /// To human readable string + pub fn to_human_string(&self, max_len: usize) -> String { + if max_len > 2 { + format!( + "{} {}", + self.step, + self.message_v2 + .to_human_string(max_len - 2, self.uid.clone()) + ) + } else { + String::from(".") + } + } + /// Get uid of head issuer + pub fn uid(&self) -> Option<String> { + self.uid.clone() + } +} + +#[derive(Debug, Clone, Eq, Ord, PartialOrd, PartialEq)] +/// Network Head : Set of information on the current state of a node, the central information being the blockstamp of its current block (= the head of its blockchain). +pub enum NetworkHead { + /// Head V2 + V2(Box<NetworkHeadV2>), + /// head V3 + V3(), +} + +impl ToString for NetworkHead { + fn to_string(&self) -> String { + match *self { + NetworkHead::V2(ref head_v2) => head_v2.deref().to_string(), + _ => panic!("NetworkHead version not supported !"), + } + } +} + +impl NetworkHead { + /// Get HEAD version + pub fn version(&self) -> u32 { + match *self { + NetworkHead::V2(_) => 2, + _ => panic!("This HEAD version is not supported !"), + } + } + /// Get HEAD blockstamp + pub fn blockstamp(&self) -> Blockstamp { + match *self { + NetworkHead::V2(ref head_v2) => head_v2.message_v2.blockstamp(), + _ => panic!("This HEAD version is not supported !"), + } + } + /// Get pubkey of head issuer + pub fn pubkey(&self) -> ed25519::PublicKey { + match *self { + NetworkHead::V2(ref head_v2) => match head_v2.message_v2 { + NetworkHeadMessage::V2(ref head_message_v2) => head_message_v2.pubkey, + _ => panic!("This HEAD message version is not supported !"), + }, + _ => panic!("This HEAD version is not supported !"), + } + } + /// Get uid of head issuer + pub fn uid(&self) -> Option<String> { + match *self { + NetworkHead::V2(ref head_v2) => head_v2.uid(), + _ => panic!("This HEAD version is not supported !"), + } + } + /// Change uid of head issuer + pub fn set_uid(&mut self, uid: &str) { + match *self { + NetworkHead::V2(ref mut head_v2) => head_v2.uid = Some(String::from(uid)), + _ => panic!("This HEAD version is not supported !"), + } + } + /// return the HEAD Step + pub fn step(&self) -> u32 { + match *self { + NetworkHead::V2(ref head_v2) => head_v2.step, + _ => panic!("This HEAD version is not supported !"), + } + } + /// Checks the validity of all head signatures + pub fn verify(&self) -> bool { + match *self { + NetworkHead::V2(ref head_v2) => { + let pubkey: ed25519::PublicKey = + PublicKey::from_base58(&self.pubkey().to_string()).unwrap(); + pubkey.verify(head_v2.message.to_string().as_bytes(), &head_v2.sig) + && pubkey.verify(head_v2.message_v2.to_string().as_bytes(), &head_v2.sig_v2) + } + _ => panic!("This HEAD version is not supported !"), + } + } + /// Returns issuer node id + pub fn node_uuid(&self) -> NodeUUID { + match *self { + NetworkHead::V2(ref head_v2) => head_v2.message_v2.node_uuid(), + _ => panic!("This HEAD version is not supported !"), + } + } + /// Returns issuer node full identifier + pub fn node_full_id(&self) -> NodeFullId { + NodeFullId(self.node_uuid(), self.pubkey()) + } + /// Returns true only if this head is to replace the old head of the same issuer in the head cache (or if it's the 1st head of this issuer) + pub fn apply(&self, heads_cache: &mut HashMap<NodeFullId, NetworkHead>) -> bool { + let heads_cache_copy = heads_cache.clone(); + if let Some(head) = heads_cache_copy.get(&self.node_full_id()) { + if self.blockstamp().id.0 > head.blockstamp().id.0 + || (self.blockstamp().id.0 == head.blockstamp().id.0 + && self.version() >= head.version() + && self.step() < head.step()) + { + if let Some(head_mut) = heads_cache.get_mut(&self.node_full_id()) { + *head_mut = self.clone(); + true + } else { + false + } + } else { + false + } + } else { + heads_cache.insert(self.node_full_id(), self.clone()); + true + } + } + /// Parse Json Head + pub fn from_json_value(source: &serde_json::Value) -> Option<NetworkHead> { + let message = NetworkHeadMessage::from_str(source.get("message")?.as_str().unwrap())?; + match message { + NetworkHeadMessage::V2(_) => Some(NetworkHead::V2(Box::new(NetworkHeadV2 { + message, + sig: Signature::from_base64(source.get("sig")?.as_str().unwrap()).unwrap(), + message_v2: NetworkHeadMessage::from_str( + source.get("messageV2")?.as_str().unwrap(), + )?, + sig_v2: Signature::from_base64(source.get("sigV2")?.as_str().unwrap()).unwrap(), + step: source.get("step")?.as_u64().unwrap() as u32, + uid: None, + }))), + _ => None, + } + } + /// To human readable string + pub fn to_human_string(&self, max_len: usize) -> String { + match *self { + NetworkHead::V2(ref head_v2) => head_v2.deref().to_human_string(max_len), + _ => panic!("NetworkHead version not supported !"), + } + } +} diff --git a/network/network_peer.rs b/network/network_peer.rs new file mode 100644 index 0000000000000000000000000000000000000000..5efe5ed188f3e4e939ac08ffdc67eb681c3d97b7 --- /dev/null +++ b/network/network_peer.rs @@ -0,0 +1,79 @@ +// Copyright (C) 2017 The Duniter Project Developers. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! Module defining the format of network peer cards and how to handle them. + +extern crate crypto; +extern crate duniter_crypto; +extern crate duniter_documents; +extern crate duniter_module; +extern crate serde; +extern crate serde_json; + +use super::network_endpoint::NetworkEndpoint; +use duniter_crypto::keys::ed25519; +use duniter_documents::Blockstamp; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +/// Peer card V10 +pub struct NetworkPeerV10 { + /// Peer card Blockstamp + pub blockstamp: Blockstamp, + /// Peer card issuer + pub issuer: ed25519::PublicKey, + /// Peer card endpoints list + pub endpoints: Vec<NetworkEndpoint>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +/// Peer card +pub enum NetworkPeer { + /// Peer card V10 + V10(NetworkPeerV10), + /// Peer card V11 + V11(), +} + +impl NetworkPeer { + /// Get peer card version + pub fn version(&self) -> u32 { + match *self { + NetworkPeer::V10(ref _peer_v10) => 10, + _ => panic!("Peer version is not supported !"), + } + } + /// Get peer card blockstamp + pub fn blockstamp(&self) -> Blockstamp { + match *self { + NetworkPeer::V10(ref peer_v10) => peer_v10.blockstamp, + _ => panic!("Peer version is not supported !"), + } + } + /// Get peer card issuer + pub fn issuer(&self) -> ed25519::PublicKey { + match *self { + NetworkPeer::V10(ref peer_v10) => peer_v10.issuer, + _ => panic!("Peer version is not supported !"), + } + } + /// Verify validity of peer card signature + pub fn verify(&self) -> bool { + false + } + /// Get peer card endpoint + pub fn get_endpoints(&self) -> Vec<NetworkEndpoint> { + Vec::with_capacity(0) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000000000000000000000000000000000000..c9e127fb73aeea7c141dcba4ecfa7250f1f68531 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,50 @@ +// Copyright (C) 2018 The Duniter Project Developers. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! Main function for classic duniter-rust nodes (no specialization). + +#![cfg_attr(feature = "strict", deny(warnings))] +#![deny( + missing_docs, missing_debug_implementations, missing_copy_implementations, trivial_casts, + trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces, + unused_qualifications +)] + +extern crate duniter_core; +extern crate duniter_tui; +extern crate duniter_ws2p; + +pub use duniter_core::DuniterCore; +pub use duniter_tui::TuiModule; +pub use duniter_ws2p::WS2PModule; + +/// Main function +fn main() { + // Get software name and version + let soft_name = env!("CARGO_PKG_NAME"); + let soft_version = env!("CARGO_PKG_VERSION"); + + // Run duniter core + if let Some(mut duniter_core) = DuniterCore::new(soft_name, soft_version) { + duniter_core.plug::<WS2PModule>(); + duniter_core.plug::<TuiModule>(); + //duniter_core.plug::<PoolModule>(); + //duniter_core.plug::<PowModule>(); + //duniter_core.plug::<GvaModule>(); + //duniter_core.plug::<DasaModule>(); + //duniter_core.plug::<GuiModule>(); + duniter_core.start_blockchain(); + }; +} diff --git a/tui/Cargo.toml b/tui/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..c3fcd4211ebc7963964a49113b379f98c80bed4a --- /dev/null +++ b/tui/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "duniter-tui" +version = "0.1.0" +authors = ["librelois <elois@ifee.fr>"] +description = "Terminal user interface for Duniter-Rs." +license = "AGPL-3.0" + +[lib] +path = "lib.rs" + +[dependencies] +duniter-conf = { path = "../conf" } +duniter-crypto = { path = "../crypto" } +duniter-dal = { path = "../dal" } +duniter-documents = { path = "../documents" } +duniter-message = { path = "../message" } +duniter-module = { path = "../module" } +duniter-network = { path = "../network" } +log = "0.4.1" +serde_json = "1.0.9" +termion = "1.5.1" + +[features] +# Treat warnings as a build error. +strict = [] \ No newline at end of file diff --git a/tui/lib.rs b/tui/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..deaf579464f81fff9f404a9626f0a36c40d351fa --- /dev/null +++ b/tui/lib.rs @@ -0,0 +1,624 @@ +// Copyright (C) 2018 The Duniter Project Developers. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! Defined the few global types used by all modules, +//! as well as the DuniterModule trait that all modules must implement. + +#![cfg_attr(feature = "strict", deny(warnings))] +#![deny( + missing_docs, missing_debug_implementations, missing_copy_implementations, trivial_casts, + trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces, + unused_qualifications +)] + +#[macro_use] +extern crate log; + +extern crate duniter_conf; +extern crate duniter_crypto; +extern crate duniter_dal; +extern crate duniter_documents; +extern crate duniter_message; +extern crate duniter_module; +extern crate duniter_network; +extern crate serde_json; +extern crate termion; + +use duniter_crypto::keys::ed25519; +use duniter_dal::dal_event::DALEvent; +use duniter_message::DuniterMessage; +use duniter_module::*; +use duniter_network::network_head::NetworkHead; +use duniter_network::{NetworkEvent, NodeFullId}; +use std::collections::HashMap; +use std::io::{stdout, Write}; +use std::sync::mpsc; +use std::thread; +use std::time::{Duration, SystemTime}; +use termion::event::*; +use termion::input::{MouseTerminal, TermRead}; +use termion::raw::{IntoRawMode, RawTerminal}; +use termion::{clear, color, cursor, style}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +/// Tui Module Configuration (For future use) +pub struct TuiConf {} + +#[derive(Debug, Clone)] +/// Format of messages received by the tui module +pub enum TuiMess { + /// Message from another module + DuniterMessage(DuniterMessage), + /// Message from stdin (user event) + TermionEvent(Event), +} + +#[derive(Debug, Copy, Clone)] +/// Tui module +pub struct TuiModule {} + +#[derive(Debug, Clone)] +/// Network connexion (data to display) +pub struct Connection { + /// Connexion status + status: u32, + /// Endpoint url + url: String, + /// Node uid at the other end of the connection (member nodes only) + uid: Option<String>, +} + +#[derive(Debug, Clone)] +/// Data that the Tui module needs to cache +pub struct TuiModuleDatas { + /// Sender of all other modules + pub followers: Vec<mpsc::Sender<DuniterMessage>>, + /// HEADs cache content + pub heads_cache: HashMap<NodeFullId, NetworkHead>, + /// Position of the 1st head displayed on the screen + pub heads_index: usize, + /// Connections cache content + pub connections_status: HashMap<NodeFullId, Connection>, + /// Number of connections in `Established` status + pub established_conns_count: usize, +} + +impl TuiModuleDatas { + /// Parse tui configuration + fn parse_tui_conf(_json_conf: &serde_json::Value) -> TuiConf { + TuiConf {} + } + /// Draw terminal + fn draw_term<W: Write>( + &self, + stdout: &mut RawTerminal<W>, + start_time: SystemTime, + heads_cache: &HashMap<NodeFullId, NetworkHead>, + heads_index: usize, + out_connections_status: &HashMap<NodeFullId, Connection>, + _in_connections_status: &HashMap<NodeFullId, Connection>, + ) { + // Get Terminal size + let (w, h) = termion::terminal_size().expect("Fail to get terminal size !"); + + // Prepare connections screen + let mut out_never_try_conns_count = 0; + let mut out_unreachable_conns_count = 0; + let mut out_trying_conns_count = 0; + let mut out_denial_conns_count = 0; + let mut out_disconnected_conns_count = 0; + let mut out_established_conns = Vec::new(); + for (node_full_id, connection) in out_connections_status { + match connection.status { + 0 => out_never_try_conns_count += 1, + 2 | 4 => out_unreachable_conns_count += 1, + 1 | 3 | 5 | 7 | 8 | 9 => out_trying_conns_count += 1, + 10 => out_denial_conns_count += 1, + 11 => out_disconnected_conns_count += 1, + 12 => out_established_conns.push(( + node_full_id, + connection.uid.clone(), + connection.url.clone(), + )), + _ => {} + } + } + + // Prepare HEADs screen + let mut heads = heads_cache.values().collect::<Vec<&NetworkHead>>(); + heads.sort_unstable_by(|a, b| b.cmp(a)); + let heads_index_max = if heads.len() > (h - 14) as usize { + heads.len() - (h - 14) as usize + } else { + 0 + }; + + // Clear term and reset background color + write!( + stdout, + "{}{}{}", + color::Bg(color::Black), + clear::All, + cursor::Goto(1, 1) + ).unwrap(); + + // Draw headers + let mut line = 1; + write!( + stdout, + "{}{}{} established connections : ", + cursor::Goto(1, line), + color::Fg(color::White), + out_established_conns.len() + ).unwrap(); + line += 1; + write!( + stdout, + "{}{}{} NodeId-PubKey", + cursor::Goto(1, line), + color::Fg(color::White), + style::Italic, + ).unwrap(); + + // Draw inter-nodes established connections + if out_established_conns.is_empty() { + line += 1; + write!( + stdout, + "{}{}{}No established connections !", + cursor::Goto(2, line), + color::Fg(color::Red), + style::Bold, + ).unwrap(); + } else { + for (ref node_full_id, ref uid, ref url) in out_established_conns { + line += 1; + let mut uid_string = uid.clone().unwrap_or(String::from("----------------")); + uid_string.truncate(16); + write!( + stdout, + "{}{} {} {:16} {}", + cursor::Goto(2, line), + color::Fg(color::Green), + node_full_id.to_human_string(), + uid_string, + url, + ).unwrap(); + } + } + + // Draw number of conns per state + line += 1; + write!( + stdout, + "{}{}{} know endpoints : {} Never try, {} Unreach, {} on trial, {} Denial, {} Close.", + cursor::Goto(2, line), + color::Fg(color::Rgb(128, 128, 128)), + out_connections_status.len(), + out_never_try_conns_count, + out_unreachable_conns_count, + out_trying_conns_count, + out_denial_conns_count, + out_disconnected_conns_count, + ).unwrap(); + + // Draw separated line + line += 1; + let mut separated_line = String::with_capacity(w as usize); + for _ in 0..w as usize { + separated_line.push('-'); + } + write!( + stdout, + "{}{}{}", + cursor::Goto(1, line), + color::Fg(color::White), + separated_line, + ).unwrap(); + + // Draw HEADs + line += 1; + write!( + stdout, + "{}{}{} HEADs :", + cursor::Goto(1, line), + color::Fg(color::White), + heads.len() + ).unwrap(); + line += 1; + if heads_index > 0 { + write!( + stdout, + "{}{}/\\", + cursor::Goto(35, line), + color::Fg(color::Green), + ).unwrap(); + } else { + write!( + stdout, + "{}{}/\\", + cursor::Goto(35, line), + color::Fg(color::Black), + ).unwrap(); + } + line += 1; + write!( + stdout, + "{}{}Step NodeId-Pubkey BlockId-BlockHash Soft:Ver Pre [ Api ] MeR:MiR uid", + cursor::Goto(1, line), + color::Fg(color::White) + ).unwrap(); + for head in &heads[heads_index..] { + if line < (h - 2) { + line += 1; + if head.step() == 0 { + write!( + stdout, + "{}{}{}", + cursor::Goto(1, line), + color::Fg(color::Blue), + head.to_human_string(w as usize), + ).unwrap(); + } else { + write!( + stdout, + "{}{}{}", + cursor::Goto(1, line), + color::Fg(color::Green), + head.to_human_string(w as usize), + ).unwrap(); + } + } else { + break; + } + } + line += 1; + if heads_index < heads_index_max { + write!( + stdout, + "{}{}\\/", + cursor::Goto(35, line), + color::Fg(color::Green), + ).unwrap(); + } else { + write!( + stdout, + "{}{}\\/", + cursor::Goto(35, line), + color::Fg(color::Black), + ).unwrap(); + } + + // Draw footer + let mut runtime_in_secs = SystemTime::now() + .duration_since(start_time) + .expect("Fail to get runtime") + .as_secs(); + let runtime_hours = runtime_in_secs / 3600; + runtime_in_secs -= runtime_hours * 3600; + let runtime_mins = runtime_in_secs / 60; + let runtime_secs = runtime_in_secs % 60; + let runtime_str = format!( + "{:02}:{:02}:{:02}", + runtime_hours, runtime_mins, runtime_secs + ); + /*DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(runtime_in_secs, 0), Utc) + .format("%H:%M:%S") + .to_string();*/ + write!( + stdout, + "{}{}{}runtime : {}", + cursor::Goto(1, h), + color::Bg(color::Blue), + color::Fg(color::White), + runtime_str, + ).unwrap(); + write!( + stdout, + "{}{}{}q : quit{}", + cursor::Goto(w - 7, h), + color::Bg(color::Blue), + color::Fg(color::White), + cursor::Hide, + ).unwrap(); + + // Flush stdout (i.e. make the output appear). + stdout.flush().unwrap(); + } +} + +impl Default for TuiModule { + fn default() -> TuiModule { + TuiModule {} + } +} + +impl DuniterModule<ed25519::KeyPair, DuniterMessage> for TuiModule { + fn id() -> ModuleId { + ModuleId::Str("tui") + } + fn priority() -> ModulePriority { + ModulePriority::Recommended() + } + fn ask_required_keys() -> RequiredKeys { + RequiredKeys::None() + } + fn default_conf() -> serde_json::Value { + serde_json::Value::default() + } + fn start( + _soft_name: &str, + _soft_version: &str, + _keys: RequiredKeysContent<ed25519::KeyPair>, + _conf: &DuniterConf, + module_conf: &serde_json::Value, + main_sender: mpsc::Sender<RooterThreadMessage<DuniterMessage>>, + load_conf_only: bool, + ) -> Result<(), ModuleInitError> { + let start_time = SystemTime::now(); //: DateTime<Utc> = Utc::now(); + + // load conf + let _conf = TuiModuleDatas::parse_tui_conf(module_conf); + if load_conf_only { + return Ok(()); + } + + // Instanciate Tui module datas + let mut tui = TuiModuleDatas { + followers: Vec::new(), + heads_cache: HashMap::new(), + heads_index: 0, + connections_status: HashMap::new(), + established_conns_count: 0, + }; + + // Create tui main thread channel + let (tui_sender, tui_receiver): (mpsc::Sender<TuiMess>, mpsc::Receiver<TuiMess>) = + mpsc::channel(); + + // Create proxy channel + let (proxy_sender, proxy_receiver): ( + mpsc::Sender<DuniterMessage>, + mpsc::Receiver<DuniterMessage>, + ) = mpsc::channel(); + + // Launch a proxy thread that transform DuniterMessage() to TuiMess::DuniterMessage(DuniterMessage()) + let tui_sender_clone = tui_sender.clone(); + thread::spawn(move || { + // Send proxy sender to main + match main_sender.send(RooterThreadMessage::ModuleSender(proxy_sender)) { + Ok(_) => { + debug!("Send tui sender to main thread."); + } + Err(_) => panic!("Fatal error : tui module fail to send is sender channel !"), + } + loop { + match proxy_receiver.recv() { + Ok(message) => { + match tui_sender_clone.send(TuiMess::DuniterMessage(message.clone())) { + Ok(_) => { + if let DuniterMessage::Stop() = message { + break; + }; + } + Err(_) => debug!( + "tui proxy : fail to relay DuniterMessage to tui main thread !" + ), + } + } + Err(e) => { + warn!("{}", e); + break; + } + } + } + }); + + // Enter raw mode. + //let mut stdout = stdout().into_raw_mode().unwrap(); + let mut stdout = MouseTerminal::from(stdout().into_raw_mode().unwrap()); + + // Initial draw + let mut last_draw = SystemTime::now(); + tui.draw_term( + &mut stdout, + start_time, + &tui.heads_cache, + tui.heads_index, + &tui.connections_status, + &HashMap::with_capacity(0), + ); + + // Launch stdin thread + let _stdin_thread = thread::spawn(move || { + // Get the standard input stream. + let stdin = std::io::stdin(); + // Get stdin events + for c in stdin.events() { + match tui_sender.send(TuiMess::TermionEvent( + c.expect("error to read stdin event !"), + )) { + Ok(_) => { + trace!("Send stdin event to tui main thread."); + } + Err(_) => { + panic!("Fatal error : tui stdin thread module fail to send message !") + } + } + } + }); + + // ui main loop + loop { + let mut user_event = false; + // Get messages + match tui_receiver.recv_timeout(Duration::from_millis(250)) { + Ok(ref message) => match message { + &TuiMess::DuniterMessage(ref duniter_message) => match duniter_message { + &DuniterMessage::Stop() => { + writeln!( + stdout, + "{}{}{}{}{}", + color::Fg(color::Reset), + cursor::Goto(1, 1), + color::Bg(color::Reset), + cursor::Show, + clear::All, + ).unwrap(); + let _result_stop_propagation: Result< + (), + mpsc::SendError<DuniterMessage>, + > = tui + .followers + .iter() + .map(|f| f.send(DuniterMessage::Stop())) + .collect(); + break; + } + &DuniterMessage::Followers(ref new_followers) => { + info!("Tui module receive followers !"); + for new_follower in new_followers { + debug!("TuiModule : push one follower."); + tui.followers.push(new_follower.clone()); + } + } + &DuniterMessage::DALEvent(ref dal_event) => match dal_event { + &DALEvent::StackUpValidBlock(ref _block) => {} + &DALEvent::RevertBlocks(ref _blocks) => {} + _ => {} + }, + &DuniterMessage::NetworkEvent(ref network_event) => match network_event { + &NetworkEvent::ConnectionStateChange( + ref node_full_id, + ref status, + ref uid, + ref url, + ) => { + if let Some(conn) = tui.connections_status.get(&node_full_id) { + if *status == 12 && (*conn).status != 12 { + tui.established_conns_count += 1; + } else if *status != 12 + && (*conn).status == 12 + && tui.established_conns_count > 0 + { + tui.established_conns_count -= 1; + } + }; + tui.connections_status.insert( + *node_full_id, + Connection { + status: *status, + url: url.clone(), + uid: uid.clone(), + }, + ); + } + &NetworkEvent::ReceiveHeads(ref heads) => { + heads + .iter() + .map(|h| tui.heads_cache.insert(h.node_full_id(), h.clone())) + .collect::<Vec<Option<NetworkHead>>>(); + } + _ => {} + }, + _ => {} + }, + &TuiMess::TermionEvent(ref term_event) => match term_event { + &Event::Key(Key::Char('q')) => { + // Exit + writeln!( + stdout, + "{}{}{}{}{}", + color::Fg(color::Reset), + cursor::Goto(1, 1), + color::Bg(color::Reset), + cursor::Show, + clear::All, + ).unwrap(); + let _result_stop_propagation: Result< + (), + mpsc::SendError<DuniterMessage>, + > = tui + .followers + .iter() + .map(|f| f.send(DuniterMessage::Stop())) + .collect(); + break; + } + &Event::Mouse(ref me) => match me { + &MouseEvent::Press(ref button, ref _a, ref _b) => match button { + &MouseButton::WheelDown => { + // Get Terminal size + let (_w, h) = termion::terminal_size() + .expect("Fail to get terminal size !"); + // heads_index + if h > 16 { + let heads_index_max = + if tui.heads_cache.len() > (h - 16) as usize { + tui.heads_cache.len() - (h - 16) as usize + } else { + 0 + }; + if tui.heads_index < heads_index_max { + tui.heads_index += 1; + user_event = true; + } else { + tui.heads_index = heads_index_max; + } + } + } + &MouseButton::WheelUp => { + // heads_index + if tui.heads_index > 0 { + tui.heads_index -= 1; + user_event = true; + } + } + _ => {} + }, + &MouseEvent::Release(ref _a, ref _b) + | &MouseEvent::Hold(ref _a, ref _b) => {} + }, + _ => {} + }, + }, + Err(e) => match e { + mpsc::RecvTimeoutError::Disconnected => { + panic!("Disconnected tui module !"); + } + mpsc::RecvTimeoutError::Timeout => {} + }, + } + let now = SystemTime::now(); + if user_event + || now + .duration_since(last_draw) + .expect("Tui : Fatal error : fail to get duration since last draw !") + .subsec_nanos() > 250_000_000 + { + last_draw = now; + tui.draw_term( + &mut stdout, + start_time, + &tui.heads_cache, + tui.heads_index, + &tui.connections_status, + &HashMap::with_capacity(0), + ); + } + } + Ok(()) + } +} diff --git a/wotb/data/legacy.rs b/wotb/data/legacy.rs index 202807a26632302fe6f4cb7eda4c5d2a6d5521e7..6a38cf5d6d0b0edeb95c90366351f4d4bb4f3204 100644 --- a/wotb/data/legacy.rs +++ b/wotb/data/legacy.rs @@ -268,7 +268,8 @@ impl WebOfTrust for LegacyWebOfTrust { let node = &self.nodes[node.0]; Some( - node.enabled && node.issued_count() >= sentry_requirement + node.enabled + && node.issued_count() >= sentry_requirement && node.links_iter().count() >= sentry_requirement, ) } @@ -277,7 +278,8 @@ impl WebOfTrust for LegacyWebOfTrust { self.nodes .iter() .filter(|x| { - x.enabled && x.issued_count() >= sentry_requirement + x.enabled + && x.issued_count() >= sentry_requirement && x.links_iter().count() >= sentry_requirement }) .map(|x| x.id()) diff --git a/wotb/data/rusty.rs b/wotb/data/rusty.rs index e55fefaf88b5445c4e0fbdac482eefe0317a256e..85eefdd324172d7242085965eaa7dbd615c1a08d 100644 --- a/wotb/data/rusty.rs +++ b/wotb/data/rusty.rs @@ -176,7 +176,8 @@ impl WebOfTrust for RustyWebOfTrust { let node = &self.nodes[node.0]; Some( - node.enabled && node.issued_count >= sentry_requirement + node.enabled + && node.issued_count >= sentry_requirement && node.links_source.len() >= sentry_requirement, ) } @@ -186,7 +187,8 @@ impl WebOfTrust for RustyWebOfTrust { .par_iter() .enumerate() .filter(|&(_, n)| { - n.enabled && n.issued_count >= sentry_requirement + n.enabled + && n.issued_count >= sentry_requirement && n.links_source.len() >= sentry_requirement }) .map(|(i, _)| NodeId(i)) diff --git a/ws2p/Cargo.toml b/ws2p/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..955f268c5a60cc526dfc1280b56fd190bb860d51 --- /dev/null +++ b/ws2p/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "duniter-ws2p" +version = "0.1.0" +authors = ["librelois <elois@ifee.fr>"] +description = "WebSocketToPeer API for the Duniter project." +license = "AGPL-3.0" + +[lib] +path = "lib.rs" + +[dependencies] +duniter-conf = { path = "../conf" } +duniter-crypto = { path = "../crypto" } +duniter-dal = { path = "../dal" } +duniter-documents = { path = "../documents" } +duniter-message = { path = "../message" } +duniter-module = { path = "../module" } +duniter-network = { path = "../network" } +duniter-wotb = { path = "../wotb" } +lazy_static = "1.0.0" +log = "0.4.1" +rand = "0.4.2" +regex = "0.2.6" +rust-crypto = "0.2.36" +sqlite = "0.23.9" +serde = "1.0.24" +serde_derive = "1.0.24" +serde_json = "1.0.9" +websocket = "0.20.2" + +[features] +# Treat warnings as a build error. +strict = [] \ No newline at end of file diff --git a/ws2p/ack_message.rs b/ws2p/ack_message.rs new file mode 100644 index 0000000000000000000000000000000000000000..07d4b4a2233e57f41cbf7a635194bb7846b54a7a --- /dev/null +++ b/ws2p/ack_message.rs @@ -0,0 +1,67 @@ +extern crate duniter_crypto; +extern crate serde; +extern crate serde_json; + +use self::serde::ser::{Serialize, SerializeStruct, Serializer}; +use super::WS2PMessage; +use duniter_crypto::keys::ed25519::PublicKey as ed25519PublicKey; +use duniter_crypto::keys::PublicKey; + +#[derive(Debug, Clone)] +pub struct WS2PAckMessageV1 { + pub currency: String, + pub pubkey: ed25519PublicKey, + pub challenge: String, + pub signature: Option<duniter_crypto::keys::ed25519::Signature>, +} + +impl WS2PMessage for WS2PAckMessageV1 { + fn parse(v: &serde_json::Value, currency: String) -> Option<Self> { + let pubkey = match v.get("pub") { + Some(pubkey) => pubkey.as_str().unwrap().to_string(), + None => return None, + }; + let signature = match v.get("sig") { + Some(signature) => signature.as_str().unwrap().to_string(), + None => return None, + }; + let pubkey: ed25519PublicKey = ed25519PublicKey::from_base58(&pubkey).unwrap(); + let signature: Option<duniter_crypto::keys::ed25519::Signature> = + Some(duniter_crypto::keys::Signature::from_base64(&signature).unwrap()); + Some(WS2PAckMessageV1 { + currency, + pubkey, + challenge: "".to_string(), + signature, + }) + } + fn to_raw(&self) -> String { + format!( + "WS2P:ACK:{}:{}:{}", + self.currency, self.pubkey, self.challenge + ) + } + fn verify(&self) -> bool { + self.pubkey + .verify(self.to_raw().as_bytes(), &self.signature.unwrap()) + } +} + +impl Serialize for WS2PAckMessageV1 { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let mut connect_message_in_json = serializer.serialize_struct("message", 3)?; + connect_message_in_json.serialize_field("auth", "ACK")?; + connect_message_in_json.serialize_field("pub", &self.pubkey.to_string())?; + connect_message_in_json.serialize_field( + "sig", + &self + .signature + .expect("Fail to serialize ACK message : the signature field is set to None !") + .to_string(), + )?; + connect_message_in_json.end() + } +} diff --git a/ws2p/connect_message.rs b/ws2p/connect_message.rs new file mode 100644 index 0000000000000000000000000000000000000000..4b048143a38d8dd22d88dbcb95abb4989d893ea4 --- /dev/null +++ b/ws2p/connect_message.rs @@ -0,0 +1,87 @@ +extern crate duniter_crypto; +extern crate serde; +extern crate serde_json; + +use self::serde::ser::{Serialize, SerializeStruct, Serializer}; +use super::WS2PMessage; +use duniter_crypto::keys::ed25519::PublicKey as ed25519PublicKey; +use duniter_crypto::keys::PublicKey; + +#[derive(Debug, Clone)] +pub struct WS2PConnectMessageV1 { + pub currency: String, + pub pubkey: ed25519PublicKey, + pub challenge: String, + pub signature: Option<duniter_crypto::keys::ed25519::Signature>, +} + +impl WS2PMessage for WS2PConnectMessageV1 { + fn parse(v: &serde_json::Value, currency: String) -> Option<Self> { + let pubkey = match v.get("pub") { + Some(pubkey) => pubkey.as_str().unwrap().to_string(), + None => return None, + }; + let challenge = match v.get("challenge") { + Some(challenge) => challenge.as_str().unwrap().to_string(), + None => return None, + }; + let signature = match v.get("sig") { + Some(signature) => signature.as_str().unwrap().to_string(), + None => return None, + }; + let pubkey: ed25519PublicKey = ed25519PublicKey::from_base58(&pubkey).unwrap(); + let signature: Option<duniter_crypto::keys::ed25519::Signature> = + Some(duniter_crypto::keys::Signature::from_base64(&signature).unwrap()); + Some(WS2PConnectMessageV1 { + currency, + pubkey, + challenge, + signature, + }) + } + fn to_raw(&self) -> String { + format!( + "WS2P:CONNECT:{}:{}:{}", + self.currency, self.pubkey, self.challenge + ) + } + fn verify(&self) -> bool { + self.pubkey + .verify(self.to_raw().as_bytes(), &self.signature.unwrap()) + } + /*fn parse_and_verify(v: serde_json::Value, currency: String) -> bool { + let pubkey = match v.get("pub") { + Some(pubkey) => pubkey.as_str().unwrap().to_string(), + None => return false, + }; + let challenge = match v.get("pub") { + Some(challenge) => challenge.as_str().unwrap().to_string(), + None => return false, + }; + let signature = match v.get("pub") { + Some(signature) => signature.as_str().unwrap().to_string(), + None => return false, + }; + ed25519PublicKey::from_base58(&pubkey).unwrap().verify(format!("WS2P:CONNECT:{}:{}:{}", currency, pubkey, challenge),&duniter_keys::Signature::from_base64(&signature).unwrap()) + }*/ +} + +impl Serialize for WS2PConnectMessageV1 { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let mut connect_message_in_json = serializer.serialize_struct("message", 4)?; + connect_message_in_json.serialize_field("auth", "CONNECT")?; + connect_message_in_json.serialize_field("pub", &self.pubkey.to_string())?; + connect_message_in_json.serialize_field("challenge", &self.challenge)?; + connect_message_in_json.serialize_field( + "sig", + &self + .signature + .expect("Fail to serialize CONNECT message : the signature field is set to None !") + .to_string(), + )?; + connect_message_in_json.end() + } +} diff --git a/ws2p/constants.rs b/ws2p/constants.rs new file mode 100644 index 0000000000000000000000000000000000000000..7d33b80de435b96c4fc4d93a3c79637aa932b5c7 --- /dev/null +++ b/ws2p/constants.rs @@ -0,0 +1,22 @@ +extern crate regex; + +use self::regex::Regex; + +lazy_static! { + #[derive(Debug)] + pub static ref WS2P_V1_ENDPOINT_REGEX: Regex = Regex::new( + "^WS2P (?P<version>[1-9][0-9]* )?(?P<uuid>[a-f0-9]{6,8}) (?P<host>[a-z_][a-z0-9-_.]*|[0-9.]+|[0-9a-f:]+) (?P<port>[0-9]+)(?: /?(?P<path>.+)?)? *$" + ).unwrap(); +} +pub static WS2P_OUTCOMING_INTERVAL_AT_STARTUP: &'static u64 = &75; +pub static WS2P_OUTCOMING_INTERVAL: &'static u64 = &300; +pub static WS2P_DEFAULT_OUTCOMING_QUOTA: &'static usize = &10; +pub static WS2P_NEGOTIATION_TIMEOUT: &'static u64 = &15; +//pub static WS2P_REQUEST_TIMEOUT : &'static u64 = &30; +pub static WS2P_CONNECTION_TIMEOUT: &'static u64 = &120; +pub static WS2P_SPAM_INTERVAL_IN_MILLI_SECS: &'static u64 = &80; +pub static WS2P_SPAM_LIMIT: &'static u64 = &6; +pub static WS2P_SPAM_SLEEP_TIME_IN_SEC: &'static u64 = &100; +pub static DURATION_BEFORE_RECORDING_ENDPOINT: &'static u64 = &180; +pub static BLOCKS_REQUEST_INTERVAL: &'static u64 = &60; +pub static PENDING_IDENTITIES_REQUEST_INTERVAL: &'static u64 = &40; diff --git a/ws2p/lib.rs b/ws2p/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..91f8bf76def013126dc172bf0a11dadaaf4ff15a --- /dev/null +++ b/ws2p/lib.rs @@ -0,0 +1,1961 @@ +// Copyright (C) 2018 The Duniter Project Developers. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! Crate containing Duniter-rust core. + +#![cfg_attr(feature = "strict", deny(warnings))] +#![deny( + missing_debug_implementations, missing_copy_implementations, trivial_casts, unsafe_code, + unstable_features, unused_import_braces, unused_qualifications +)] +#![recursion_limit = "256"] + +#[macro_use] +extern crate lazy_static; + +#[macro_use] +extern crate log; + +#[macro_use] +extern crate serde_json; + +extern crate duniter_conf; +extern crate duniter_crypto; +extern crate duniter_dal; +extern crate duniter_documents; +extern crate duniter_message; +extern crate duniter_module; +extern crate duniter_network; +extern crate rand; +extern crate sqlite; +extern crate websocket; + +use std::collections::HashMap; +use std::ops::Deref; +use std::path::PathBuf; +use std::str::from_utf8; +use std::sync::mpsc; +use std::thread; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use duniter_crypto::keys::ed25519::Signature; +use duniter_crypto::keys::{ed25519, KeyPair, PrivateKey, PublicKey}; +use duniter_dal::dal_event::DALEvent; +use duniter_dal::dal_requests::{DALReqBlockchain, DALRequest, DALResBlockchain, DALResponse}; +use duniter_dal::parsers::blocks::parse_json_block; +use duniter_documents::blockchain::Document; +use duniter_documents::Blockstamp; +use duniter_message::DuniterMessage; +use duniter_module::*; +use duniter_network::network_endpoint::*; +use duniter_network::network_head::*; +use duniter_network::*; + +use websocket::{ClientBuilder, Message}; + +mod ack_message; +mod connect_message; +pub mod constants; +mod ok_message; +pub mod serializer; +pub mod ws2p_connection; +pub mod ws2p_db; +pub mod ws2p_requests; + +use self::ack_message::WS2PAckMessageV1; +use self::connect_message::WS2PConnectMessageV1; +use self::constants::*; +use self::ok_message::WS2POkMessageV1; +use self::rand::Rng; +use self::ws2p_connection::*; +use self::ws2p_requests::network_request_to_json; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct WS2PConf { + pub node_id: NodeUUID, + pub outcoming_quota: usize, + pub sync_endpoints: Vec<NetworkEndpoint>, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum WS2PConfParseError { + UnknowError(), +} + +#[derive(Debug)] +pub enum WS2PSignal { + WSError(NodeFullId), + ConnectionEstablished(NodeFullId), + NegociationTimeout(NodeFullId), + Timeout(NodeFullId), + DalRequest(NodeFullId, ModuleReqId, serde_json::Value), + PeerCard(NodeFullId, serde_json::Value, Vec<NetworkEndpoint>), + Heads(NodeFullId, Vec<NetworkHead>), + Document(NodeFullId, NetworkDocument), + ReqResponse(ModuleReqId, NetworkRequest, NodeFullId, serde_json::Value), + Empty, + NoConnection, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum NetworkConsensusError { + InsufficientData(usize), + Fork, +} + +#[derive(Debug)] +pub enum SendRequestError { + RequestTypeMustNotBeTransmitted(), + WSError(usize, Vec<websocket::WebSocketError>), +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct WS2PModule {} + +#[derive(Debug)] +pub struct WS2PModuleDatas { + pub followers: Vec<mpsc::Sender<DuniterMessage>>, + pub currency: Option<String>, + pub key_pair: Option<ed25519::KeyPair>, + pub conf: Option<WS2PConf>, + pub main_thread_channel: ( + mpsc::Sender<WS2PThreadSignal>, + mpsc::Receiver<WS2PThreadSignal>, + ), + pub ws2p_endpoints: HashMap<NodeFullId, (NetworkEndpoint, WS2PConnectionState)>, + pub connections_meta_datas: HashMap<NodeFullId, WS2PConnectionMetaData>, + pub websockets: HashMap<NodeFullId, WebsocketSender>, + pub threads_senders_channels: HashMap<NodeFullId, mpsc::Sender<WS2POrderForListeningThread>>, + pub requests_awaiting_response: HashMap<ModuleReqId, (NetworkRequest, NodeFullId, SystemTime)>, + pub heads_cache: HashMap<NodeFullId, NetworkHead>, + pub my_head: Option<NetworkHead>, + pub uids_cache: HashMap<ed25519::PublicKey, String>, +} + +#[derive(Debug)] +pub enum WS2PThreadSignal { + DuniterMessage(DuniterMessage), + WS2PConnectionMessage(WS2PConnectionMessage), +} + +pub trait WS2PMessage: Sized { + fn parse(v: &serde_json::Value, currency: String) -> Option<Self>; + fn to_raw(&self) -> String; + fn sign(&self, key_pair: ed25519::KeyPair) -> Signature { + key_pair.sign(self.to_raw().as_bytes()) + } + fn verify(&self) -> bool; + //fn parse_and_verify(v: serde_json::Value, currency: String) -> bool; +} + +pub fn get_random_connection( + connections: &HashMap<NodeFullId, (NetworkEndpoint, WS2PConnectionState)>, +) -> NodeFullId { + let mut rng = rand::thread_rng(); + let mut loop_count = 0; + loop { + for (ws2p_full_id, (_ep, state)) in connections.clone() { + if loop_count > 10 { + return ws2p_full_id; + } + if let WS2PConnectionState::Established = state { + if rng.gen::<bool>() { + return ws2p_full_id; + } + } + } + loop_count += 1; + } +} + +impl Default for WS2PModule { + fn default() -> WS2PModule { + WS2PModule {} + } +} + +impl DuniterModule<ed25519::KeyPair, DuniterMessage> for WS2PModule { + fn id() -> ModuleId { + ModuleId::Str("ws2p") + } + fn priority() -> ModulePriority { + ModulePriority::Essential() + } + fn ask_required_keys() -> RequiredKeys { + RequiredKeys::NetworkKeyPair() + } + fn default_conf() -> serde_json::Value { + json!({ + "sync_peers": [{ + "pubkey": "7v2J4badvfWQ6qwRdCwhhJfAsmKwoxRUNpJHiJHj7zef", + "ws2p_endpoints": ["WS2P b48824f0 g1.monnaielibreoccitanie.org 80 /ws2p"] + }] + }) + } + fn start( + soft_name: &str, + soft_version: &str, + keys: RequiredKeysContent<ed25519::KeyPair>, + duniter_conf: &DuniterConf, + module_conf: &serde_json::Value, + rooter_sender: mpsc::Sender<RooterThreadMessage<DuniterMessage>>, + load_conf_only: bool, + ) -> Result<(), ModuleInitError> { + let start_time = SystemTime::now(); + let mut ws2p_module = WS2PModuleDatas { + followers: Vec::new(), + key_pair: None, + currency: None, + conf: None, + main_thread_channel: mpsc::channel(), + ws2p_endpoints: HashMap::new(), + connections_meta_datas: HashMap::new(), + websockets: HashMap::new(), + threads_senders_channels: HashMap::new(), + requests_awaiting_response: HashMap::new(), + heads_cache: HashMap::new(), + my_head: None, + uids_cache: HashMap::new(), + }; + + // load conf + let key_pair = match keys { + RequiredKeysContent::NetworkKeyPair(key_pair) => key_pair, + _ => panic!("WS2PModule fatal error at load_conf() : keys != NetworkKeyPair"), + }; + let conf = WS2PModuleDatas::parse_ws2p_conf(duniter_conf, module_conf); + let mut ws2p_endpoints = HashMap::new(); + for ep in conf.sync_endpoints.clone() { + ws2p_endpoints.insert( + ep.node_full_id().unwrap(), + (ep.clone(), WS2PConnectionState::Close), + ); + info!("Load sync endpoint {}", ep.raw()); + } + ws2p_module.key_pair = Some(key_pair.clone()); + ws2p_module.currency = Some(duniter_conf.currency().to_string()); + ws2p_module.conf = Some(conf.clone()); + ws2p_module.ws2p_endpoints = ws2p_endpoints; + + // Create ws2p main thread channel + let ws2p_sender_clone = ws2p_module.main_thread_channel.0.clone(); + + // Create proxy channel + let (proxy_sender, proxy_receiver): ( + mpsc::Sender<DuniterMessage>, + mpsc::Receiver<DuniterMessage>, + ) = mpsc::channel(); + let proxy_sender_clone = proxy_sender.clone(); + + // Launch a proxy thread that transform DuniterMessage to WS2PThreadSignal(DuniterMessage) + thread::spawn(move || { + // Send proxy sender to main + match rooter_sender.send(RooterThreadMessage::ModuleSender(proxy_sender_clone)) { + Ok(_) => { + debug!("Send ws2p sender to main thread."); + } + Err(_) => panic!("Fatal error : ws2p module fail to send is sender channel !"), + } + //drop(rooter_sender); + loop { + match proxy_receiver.recv() { + Ok(message) => match ws2p_sender_clone + .send(WS2PThreadSignal::DuniterMessage(message.clone())) + { + Ok(_) => { + if let DuniterMessage::Stop() = message { + break; + }; + } + Err(_) => panic!( + "Fatal error : fail to relay DuniterMessage to ws2p main thread !" + ), + }, + Err(e) => panic!(format!("{}", e)), + } + } + }); + + // open ws2p bdd + let mut db_path = + duniter_conf::datas_path(duniter_conf.profile().as_str(), &duniter_conf.currency()); + db_path.push("ws2p.db"); + let db = WS2PModuleDatas::open_db(db_path).expect("Fatal error : fail to open WS2P DB !"); + + // Get ws2p endpoints in BDD + let mut count = 0; + let dal_enpoints = + ws2p_db::get_endpoints_for_api(&db, NetworkEndpointApi(String::from("WS2P"))); + for ep in dal_enpoints { + if ep.api() == NetworkEndpointApi(String::from("WS2P")) && ep.port() != 443 { + count += 1; + ws2p_module.ws2p_endpoints.insert( + ep.node_full_id().unwrap(), + (ep.clone(), WS2PConnectionState::from(ep.status())), + ); + } + } + info!("Load {} endpoints from bdd !", count); + + // Stop here in load_conf_only mode + if load_conf_only { + return Ok(()); + } + + // Initialize variables + let mut last_ws2p_connecting_wave = SystemTime::now(); + let mut last_ws2p_connections_print = SystemTime::now(); + let mut endpoints_to_update_status: HashMap<NodeFullId, SystemTime> = HashMap::new(); + let mut last_identities_request = UNIX_EPOCH; + let mut current_blockstamp = Blockstamp::default(); + let mut next_receiver = 0; + + // Start + ws2p_module.connect_to_know_endpoints(); + loop { + match ws2p_module + .main_thread_channel + .1 + .recv_timeout(Duration::from_millis(200)) + { + Ok(message) => match message { + WS2PThreadSignal::DuniterMessage(ref duniter_mesage) => { + match duniter_mesage { + &DuniterMessage::Stop() => break, + &DuniterMessage::Followers(ref new_followers) => { + info!("WS2P module receive followers !"); + for new_follower in new_followers { + debug!("WS2PModule : push one follower."); + ws2p_module.followers.push(new_follower.clone()); + if current_blockstamp == Blockstamp::default() { + // Request local current blockstamp + ws2p_module.send_dal_request( + &DALRequest::BlockchainRequest( + DALReqBlockchain::CurrentBlock(ModuleReqFullId( + WS2PModule::id(), + ModuleReqId(0), + )), + ), + ); + } else { + if ws2p_module.my_head.is_none() { + ws2p_module.my_head = + Some(WS2PModuleDatas::generate_my_head( + &key_pair.clone(), + &conf.clone(), + soft_name, + soft_version, + ¤t_blockstamp, + None, + )); + } + ws2p_module.send_network_event( + &NetworkEvent::ReceiveHeads(vec![ + ws2p_module.my_head.clone().unwrap(), + ]), + ); + } + } + } + &DuniterMessage::NetworkRequest(ref request) => match request { + &NetworkRequest::GetBlocks( + ref req_id, + ref receiver, + ref count, + ref from, + ) => { + if *receiver == NodeFullId::default() { + let mut receiver_index = 0; + let mut real_receiver = NodeFullId::default(); + for (ws2p_full_id, (_ep, state)) in + ws2p_module.ws2p_endpoints.clone() + { + if let WS2PConnectionState::Established = state { + if receiver_index == next_receiver { + real_receiver = ws2p_full_id; + break; + } + receiver_index += 1; + } + } + if real_receiver == NodeFullId::default() { + next_receiver = 0; + for (ws2p_full_id, (_ep, state)) in + ws2p_module.ws2p_endpoints.clone() + { + if let WS2PConnectionState::Established = state { + real_receiver = ws2p_full_id; + break; + } + } + } else { + next_receiver += 1; + } + if real_receiver != NodeFullId::default() { + let _blocks_request_result = ws2p_module + .send_request_to_specific_node( + &real_receiver, + &NetworkRequest::GetBlocks( + req_id.clone(), + receiver.clone(), + count.clone(), + from.clone(), + ), + ); + } else { + warn!("WS2PModule : No WS2P connections !"); + } + } else { + let _blocks_request_result = ws2p_module + .send_request_to_specific_node( + &receiver, + &NetworkRequest::GetBlocks( + req_id.clone(), + receiver.clone(), + count.clone(), + from.clone(), + ), + ); + } + } + _ => {} + }, + &DuniterMessage::DALEvent(ref dal_event) => match dal_event { + &DALEvent::StackUpValidBlock(ref block) => { + current_blockstamp = block.deref().blockstamp(); + debug!( + "WS2PModule : current_blockstamp = {}", + current_blockstamp + ); + ws2p_module.my_head = Some(WS2PModuleDatas::generate_my_head( + &key_pair.clone(), + &conf.clone(), + soft_name, + soft_version, + ¤t_blockstamp, + None, + )); + ws2p_module.send_network_event(&NetworkEvent::ReceiveHeads( + vec![ws2p_module.my_head.clone().unwrap()], + )); + // Send my head to all connections + let my_json_head = serializer::serialize_head( + ws2p_module.my_head.clone().unwrap(), + ); + trace!("Send my HEAD: {:#?}", my_json_head); + let _results: Result< + (), + websocket::WebSocketError, + > = ws2p_module + .websockets + .iter_mut() + .map(|ws| { + (ws.1).0.send_message(&Message::text( + json!({ + "name": "HEAD", + "body": { + "heads": [my_json_head] + } + }).to_string(), + )) + }) + .collect(); + } + _ => {} + }, + &DuniterMessage::DALResponse(ref dal_res) => match dal_res { + &DALResponse::Blockchain(ref dal_res_bc) => match dal_res_bc { + &DALResBlockchain::CurrentBlock( + ref _requester_full_id, + ref current_block, + ) => { + debug!( + "WS2PModule : receive DALResBc::CurrentBlock({})", + current_block.blockstamp() + ); + current_blockstamp = current_block.blockstamp(); + if ws2p_module.my_head.is_none() { + ws2p_module.my_head = + Some(WS2PModuleDatas::generate_my_head( + &key_pair.clone(), + &conf.clone(), + soft_name, + soft_version, + ¤t_blockstamp, + None, + )); + } + ws2p_module.send_network_event( + &NetworkEvent::ReceiveHeads(vec![ + ws2p_module.my_head.clone().unwrap(), + ]), + ); + } + &DALResBlockchain::UIDs(ref uids) => { + // Add uids to heads + for (_, head) in ws2p_module.heads_cache.iter_mut() { + if let Some(uid_option) = uids.get(&head.pubkey()) { + if let &Some(ref uid) = uid_option { + head.set_uid(uid); + ws2p_module + .uids_cache + .insert(head.pubkey(), uid.to_string()); + } else { + ws2p_module.uids_cache.remove(&head.pubkey()); + } + } + } + // Resent heads to other modules + ws2p_module.send_network_event( + &NetworkEvent::ReceiveHeads( + ws2p_module + .heads_cache + .values() + .map(|h| h.clone()) + .collect(), + ), + ); + // Resent to other modules connections that match receive uids + for (node_full_id, (ep, conn_state)) in + ws2p_module.ws2p_endpoints.clone() + { + if let Some(uid_option) = uids.get(&node_full_id.1) { + ws2p_module.send_network_event( + &NetworkEvent::ConnectionStateChange( + node_full_id, + conn_state as u32, + uid_option.clone(), + ep.get_url(false), + ), + ); + } + } + } + _ => {} + }, + _ => {} + }, + _ => {} + } + } + WS2PThreadSignal::WS2PConnectionMessage(ws2p_conn_message) => match ws2p_module + .ws2p_conn_message_pretreatment(ws2p_conn_message) + { + WS2PSignal::NoConnection => { + warn!("WS2PSignal::NoConnection"); + last_ws2p_connecting_wave = SystemTime::now(); + ws2p_module.connect_to_know_endpoints(); + } + WS2PSignal::ConnectionEstablished(ws2p_full_id) => { + let req_id = + ModuleReqId(ws2p_module.requests_awaiting_response.len() as u32); + let module_id = WS2PModule::id(); + let _current_request_result = ws2p_module + .send_request_to_specific_node( + &ws2p_full_id, + &NetworkRequest::GetCurrent( + ModuleReqFullId(module_id, req_id), + ws2p_full_id, + ), + ); + ws2p_module.send_network_event(&NetworkEvent::ConnectionStateChange( + ws2p_full_id, + WS2PConnectionState::Established as u32, + ws2p_module.uids_cache.get(&ws2p_full_id.1).cloned(), + ws2p_module + .ws2p_endpoints + .get(&ws2p_full_id) + .unwrap() + .0 + .get_url(false), + )); + } + WS2PSignal::WSError(ws2p_full_id) => { + endpoints_to_update_status.insert(ws2p_full_id, SystemTime::now()); + ws2p_module.send_network_event(&NetworkEvent::ConnectionStateChange( + ws2p_full_id, + WS2PConnectionState::WSError as u32, + ws2p_module.uids_cache.get(&ws2p_full_id.1).cloned(), + ws2p_module + .ws2p_endpoints + .get(&ws2p_full_id) + .unwrap() + .0 + .get_url(false), + )); + } + WS2PSignal::NegociationTimeout(ws2p_full_id) => { + endpoints_to_update_status.insert(ws2p_full_id, SystemTime::now()); + ws2p_module.send_network_event(&NetworkEvent::ConnectionStateChange( + ws2p_full_id, + WS2PConnectionState::Denial as u32, + ws2p_module.uids_cache.get(&ws2p_full_id.1).cloned(), + ws2p_module + .ws2p_endpoints + .get(&ws2p_full_id) + .unwrap() + .0 + .get_url(false), + )); + } + WS2PSignal::Timeout(ws2p_full_id) => { + endpoints_to_update_status.insert(ws2p_full_id, SystemTime::now()); + ws2p_module.send_network_event(&NetworkEvent::ConnectionStateChange( + ws2p_full_id, + WS2PConnectionState::Close as u32, + ws2p_module.uids_cache.get(&ws2p_full_id.1).cloned(), + ws2p_module + .ws2p_endpoints + .get(&ws2p_full_id) + .unwrap() + .0 + .get_url(false), + )); + } + WS2PSignal::PeerCard(_ws2p_full_id, _peer_card, ws2p_endpoints) => { + //trace!("WS2PSignal::PeerCard({})", ws2p_full_id); + //ws2p_module.send_network_event(NetworkEvent::ReceivePeers(_)); + for ep in ws2p_endpoints { + if ep.port() != 443 { + match ws2p_module + .ws2p_endpoints + .get(&ep.node_full_id().unwrap()) + { + Some(_) => {} + None => { + if let Some(_api) = + ws2p_db::string_to_api(&ep.api().0.clone()) + { + endpoints_to_update_status.insert( + ep.node_full_id().unwrap(), + SystemTime::now(), + ); + } + ws2p_module.connect_to(ep); + } + }; + } + } + } + WS2PSignal::Heads(ws2p_full_id, heads) => { + trace!("WS2PSignal::Heads({}, {:?})", ws2p_full_id, heads.len()); + ws2p_module.send_dal_request(&DALRequest::BlockchainRequest( + DALReqBlockchain::UIDs(heads.iter().map(|h| h.pubkey()).collect()), + )); + ws2p_module.send_network_event(&NetworkEvent::ReceiveHeads( + heads + .iter() + .map(|head| { + let mut new_head = head.clone(); + if let Some(uid) = + ws2p_module.uids_cache.get(&head.pubkey()) + { + new_head.set_uid(uid); + } + new_head + }) + .collect(), + )); + } + WS2PSignal::Document(ws2p_full_id, network_doc) => { + trace!("WS2PSignal::Document({})", ws2p_full_id); + ws2p_module.send_network_event(&NetworkEvent::ReceiveDocuments(vec![ + network_doc, + ])); + } + WS2PSignal::ReqResponse(req_id, req, recipient_full_id, response) => { + match req { + NetworkRequest::GetCurrent(ref _req_id, _receiver) => { + info!("WS2PSignal::ReceiveCurrent({}, {:?})", req_id.0, req); + if let Some(block) = parse_json_block(&response) { + ws2p_module.send_network_event(&NetworkEvent::ReqResponse( + Box::new(NetworkResponse::CurrentBlock( + ModuleReqFullId(WS2PModule::id(), req_id), + recipient_full_id, + Box::new(block), + )), + )); + } + /*if let Some(block) = BlockV10::from_json_value(&response) { + ws2p_module + .connections_meta_datas + .get_mut(&recipient_full_id) + .unwrap() + .current_blockstamp = Some((block.id, block.hash)); + }*/ + } + NetworkRequest::GetBlocks(ref _req_id, _receiver, _count, from) => { + info!("WS2PSignal::ReceiveChunk({}, {:?})", req_id.0, req); + if response.is_array() { + let mut chunk = Vec::new(); + for json_block in response.as_array().unwrap() { + if let Some(block) = parse_json_block(json_block) { + chunk.push(NetworkDocument::Block(block)); + } else { + warn!("WS2PModule: Error : fail to parse one json block !"); + } + } + debug!("Send chunk to followers : {}", from); + ws2p_module.send_network_event( + &NetworkEvent::ReceiveDocuments(chunk), + ); + } + } + NetworkRequest::GetRequirementsPending( + _req_id, + _receiver, + min_cert, + ) => { + info!( + "WS2PSignal::ReceiveRequirementsPending({}, {})", + req_id.0, min_cert + ); + debug!("----------------------------------------"); + debug!("- BEGIN IDENTITIES PENDING -"); + debug!("----------------------------------------"); + debug!("{:#?}", response); + debug!("----------------------------------------"); + debug!("- END IDENTITIES PENDING -"); + debug!("----------------------------------------"); + } + _ => {} + } + } + WS2PSignal::Empty => {} + _ => {} + }, + }, + Err(e) => match e { + mpsc::RecvTimeoutError::Disconnected => { + panic!("Disconnected ws2p module !"); + } + mpsc::RecvTimeoutError::Timeout => {} + }, + } + if SystemTime::now() + .duration_since(last_ws2p_connections_print) + .unwrap() > Duration::new(5, 0) + { + last_ws2p_connections_print = SystemTime::now(); + let mut connected_nodes = Vec::new(); + let mut denial_nodes = Vec::new(); + let mut disconnected_nodes = Vec::new(); + let mut unreachable_nodes = Vec::new(); + let mut ws_error_nodes = Vec::new(); + for (k, (_ep, state)) in ws2p_module.ws2p_endpoints.clone() { + match state { + WS2PConnectionState::NeverTry => { + //writeln!("Never try : {}", k); + } + WS2PConnectionState::TryToOpenWS => {}//writeln!("TryToOpenWS : {}", k), + WS2PConnectionState::WSError => { + ws_error_nodes.push(k); + } + WS2PConnectionState::TryToSendConnectMess => { + //writeln!("TryToSendConnectMess : {}", k) + } + WS2PConnectionState::Unreachable => { + unreachable_nodes.push(k); + } + WS2PConnectionState::WaitingConnectMess => { + //writeln!("WaitingConnectMess : {}", k) + } + WS2PConnectionState::NoResponse => {}//writeln!("NoResponse : {}", k), + WS2PConnectionState::AckMessOk + | WS2PConnectionState::ConnectMessOk + | WS2PConnectionState::OkMessOkWaitingAckMess => { + //writeln!("Ongoing negotiations : {}", k) + } + WS2PConnectionState::Denial => { + denial_nodes.push(k); + } + WS2PConnectionState::Established => { + connected_nodes.push(k); + } + WS2PConnectionState::Close => { + disconnected_nodes.push(k); + } + } + } + /*writeln!( + "Connected with {} nodes. (Denial : {}, Disconnected : {}, Unreachable: {}, WSError : {})", + connected_nodes.len(), + denial_nodes.len(), + disconnected_nodes.len(), + unreachable_nodes.len(), + ws_error_nodes.len() + );*/ + for _node in connected_nodes.clone() { + //writeln!("Connection established : {}", node); + } + for _node in denial_nodes { + //writeln!("Denial : {}", node); + } + for _node in disconnected_nodes { + //writeln!("Disconnected : {}", node); + } + for _node in unreachable_nodes { + //writeln!("Unreachable : {}", node); + } + // Print network consensus + match ws2p_module.get_network_consensus() { + Ok(consensus_blockstamp) => { + debug!( + "WS2PModule : get_network_consensus() = {:?}", + consensus_blockstamp + ); + if current_blockstamp.id.0 < (consensus_blockstamp.id.0 + 2) { + warn!("We probably are in a fork branch !"); + } + } + Err(e) => warn!("{:?}", e), + } + // Print current_blockstamp + info!( + "WS2PModule : current_blockstamp() = {:?}", + current_blockstamp + ); + // New WS2P connection wave + if connected_nodes.len() < ws2p_module.conf.clone().unwrap().outcoming_quota + && (SystemTime::now() + .duration_since(last_ws2p_connecting_wave) + .unwrap() + > Duration::new(*WS2P_OUTCOMING_INTERVAL, 0) + || (SystemTime::now() + .duration_since(last_ws2p_connecting_wave) + .unwrap() + > Duration::new(*WS2P_OUTCOMING_INTERVAL_AT_STARTUP, 0) + && SystemTime::now().duration_since(start_time).unwrap() + < Duration::new(*WS2P_OUTCOMING_INTERVAL, 0))) + { + last_ws2p_connecting_wave = SystemTime::now(); + info!("Connected to know endpoints..."); + ws2p_module.connect_to_know_endpoints(); + } + /*// Request blocks from network + if SystemTime::now() + .duration_since(last_blocks_request) + .unwrap() > Duration::new(*BLOCKS_REQUEST_INTERVAL, 0) + && SystemTime::now().duration_since(start_time).unwrap() > Duration::new(10, 0) + { + let mut request_blocks_from = current_blockstamp.id.0; + if request_blocks_from > 0 { + request_blocks_from += 1; + } + info!("get chunks from all connections..."); + let module_id = WS2PModule::id(); + let _blocks_request_result = + ws2p_module.send_request_to_all_connections(&NetworkRequest::GetBlocks( + ModuleReqFullId(module_id, ModuleReqId(0 as u32)), + NodeFullId::default(), + 50, + request_blocks_from, + )); + last_blocks_request = SystemTime::now(); + }*/ + // Request pending_identities from network + if SystemTime::now() + .duration_since(last_identities_request) + .unwrap() + > Duration::new(*PENDING_IDENTITIES_REQUEST_INTERVAL, 0) + && SystemTime::now().duration_since(start_time).unwrap() > Duration::new(10, 0) + { + /*info!("get pending_identities from all connections..."); + let _blocks_request_result = ws2p_module.send_request_to_all_connections( + &NetworkRequest::GetRequirementsPending(ModuleReqId(0 as u32), 5), + );*/ + last_identities_request = SystemTime::now(); + } + // Write pending endpoints + for (ep_full_id, received_time) in endpoints_to_update_status.clone() { + if SystemTime::now().duration_since(received_time).unwrap() + > Duration::new(*DURATION_BEFORE_RECORDING_ENDPOINT, 0) + { + if let Some(&(ref ep, ref state)) = + ws2p_module.ws2p_endpoints.get(&ep_full_id) + { + /*let dal_endpoint = duniter_dal::endpoint::DALEndpoint::new( + state.clone() as u32, + ep.node_uuid().unwrap().0, + ep.pubkey(), + duniter_dal::endpoint::string_to_api(&ep.api().0).unwrap(), + 1, + ep.to_string(), + received_time.duration_since(UNIX_EPOCH).unwrap(), + );*/ + ws2p_db::write_endpoint( + &db, + &ep, + state.to_u32(), + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + ); + } + endpoints_to_update_status.remove(&ep_full_id); + } else { + info!( + "Write {} endpoint in {} secs.", + ep_full_id, + *DURATION_BEFORE_RECORDING_ENDPOINT + - SystemTime::now() + .duration_since(received_time) + .unwrap() + .as_secs() + ); + } + } + // .. + } + } + Ok(()) + } +} + +impl WS2PModuleDatas { + fn open_db(db_path: PathBuf) -> Result<sqlite::Connection, sqlite::Error> { + let conn: sqlite::Connection; + if !db_path.as_path().exists() { + conn = sqlite::open(db_path.as_path())?; + conn.execute( + "CREATE TABLE endpoints (hash_full_id TEXT, status INTEGER, node_id INTEGER, pubkey TEXT, + api INTEGER, version INTEGER, endpoint TEXT, last_check INTEGER);", + )?; + } else { + conn = sqlite::open(db_path.as_path())?; + } + Ok(conn) + } + pub fn parse_ws2p_conf( + duniter_conf: &DuniterConf, + ws2p_json_conf: &serde_json::Value, + ) -> WS2PConf { + let mut sync_endpoints = Vec::new(); + match ws2p_json_conf.get("sync_peers") { + Some(peers) => { + let array_peers = peers.as_array().expect("Conf: Fail to parse conf file !"); + for peer in array_peers { + let pubkey = match peer.get("pubkey") { + Some(pubkey) => { + PublicKey::from_base58( + pubkey + .as_str() + .expect("WS2PConf Error : fail to parse sync endpoint pubkey"), + ).expect("WS2PConf Error : fail to parse sync endpoint pubkey") + } + None => panic!( + "Fail to load ws2p conf : \ + WrongFormat : not found pubkey field !" + ), + }; + match peer.get("ws2p_endpoints") { + Some(endpoints) => { + let array_endpoints = endpoints + .as_array() + .expect("Conf: Fail to parse conf file !"); + for endpoint in array_endpoints { + sync_endpoints.push( + NetworkEndpoint::parse_from_raw( + endpoint.as_str().unwrap(), + pubkey, + 0, + 0, + ).expect(&format!( + "WS2PConf Error : fail to parse sync Endpoint = {:?}", + endpoint.as_str().unwrap() + )), + ); + } + } + None => panic!( + "Fail to load conf : \ + WrongFormat : not found ws2p_endpoints field !" + ), + }; + } + } + None => panic!( + "Configuration Error : \ + You must declare at least one node on which to synchronize !" + ), + }; + WS2PConf { + outcoming_quota: *WS2P_DEFAULT_OUTCOMING_QUOTA, + node_id: NodeUUID(duniter_conf.my_node_id()), + sync_endpoints, + } + } + pub fn send_dal_request(&self, req: &DALRequest) { + for follower in &self.followers { + match follower.send(DuniterMessage::DALRequest(req.clone())) { + Ok(_) => {} + Err(_) => {} + } + } + } + pub fn send_network_event(&self, event: &NetworkEvent) { + for follower in &self.followers { + match follower.send(DuniterMessage::NetworkEvent(event.clone())) { + Ok(_) => { + debug!("Send NetworkEvent to one follower."); + } + Err(_) => { + warn!("Fail to send NetworkEvent to one follower !"); + } + } + } + } + pub fn generate_my_head( + network_keypair: &ed25519::KeyPair, + conf: &WS2PConf, + soft_name: &str, + soft_version: &str, + my_current_blockstamp: &Blockstamp, + my_uid: Option<String>, + ) -> NetworkHead { + let message = NetworkHeadMessage::V2(NetworkHeadMessageV2 { + api: String::from("WS2POCA"), + version: 1, + pubkey: network_keypair.pubkey, + blockstamp: my_current_blockstamp.clone(), + node_uuid: conf.node_id, + software: String::from(soft_name), + soft_version: String::from(soft_version), + prefix: 1, + free_member_room: None, + free_mirror_room: None, + }); + let message_v2 = NetworkHeadMessage::V2(NetworkHeadMessageV2 { + api: String::from("WS2POCA"), + version: 2, + pubkey: network_keypair.pubkey, + blockstamp: my_current_blockstamp.clone(), + node_uuid: conf.node_id, + software: String::from(soft_name), + soft_version: String::from(soft_version), + prefix: 1, + free_member_room: Some(0), + free_mirror_room: Some(0), + }); + NetworkHead::V2(Box::new(NetworkHeadV2 { + message: message.clone(), + sig: network_keypair.privkey.sign(message.to_string().as_bytes()), + message_v2: message_v2.clone(), + sig_v2: network_keypair + .privkey + .sign(message_v2.to_string().as_bytes()), + step: 0, + uid: my_uid, + })) + } + pub fn get_network_consensus(&self) -> Result<Blockstamp, NetworkConsensusError> { + let mut count_known_blockstamps = 0; + let mut farthest_blockstamp = Blockstamp::default(); + let mut blockstamps_occurences: HashMap<Blockstamp, usize> = + HashMap::with_capacity(*WS2P_DEFAULT_OUTCOMING_QUOTA); + let mut dominant_blockstamp = Blockstamp::default(); + let mut dominant_blockstamp_occurences = 0; + for (_ws2p_full_id, head) in self.heads_cache.clone() { + count_known_blockstamps += 1; + let blockstamps_occurences_copy = blockstamps_occurences.clone(); + match blockstamps_occurences_copy.get(&head.blockstamp()) { + Some(occurences) => { + let mut occurences_mut = + blockstamps_occurences.get_mut(&head.blockstamp()).unwrap(); + *occurences_mut += 1; + if *occurences > dominant_blockstamp_occurences { + dominant_blockstamp_occurences = *occurences; + dominant_blockstamp = head.blockstamp().clone(); + } + } + None => { + blockstamps_occurences.insert(head.blockstamp().clone(), 0); + } + } + if head.blockstamp().id.0 > farthest_blockstamp.id.0 { + farthest_blockstamp = head.blockstamp().clone(); + } + } + if count_known_blockstamps < 5 { + return Err(NetworkConsensusError::InsufficientData( + count_known_blockstamps, + )); + } else if farthest_blockstamp == dominant_blockstamp { + return Ok(dominant_blockstamp); + } + Err(NetworkConsensusError::Fork) + } + fn count_established_connections(&self) -> usize { + let mut count_established_connections = 0; + for (_ws2p_full_id, (_ep, state)) in self.ws2p_endpoints.clone() { + if let WS2PConnectionState::Established = state { + count_established_connections += 1; + } + } + count_established_connections + } + pub fn connect_to_know_endpoints(&mut self) -> () { + let mut count_established_connections = 0; + let mut reachable_endpoints = Vec::new(); + let mut unreachable_endpoints = Vec::new(); + for (_ws2p_full_id, (ep, state)) in self.ws2p_endpoints.clone() { + match state { + WS2PConnectionState::Established => count_established_connections += 1, + WS2PConnectionState::NeverTry + | WS2PConnectionState::Close + | WS2PConnectionState::Denial => reachable_endpoints.push(ep), + _ => unreachable_endpoints.push(ep), + } + } + let mut free_outcoming_rooms = + self.conf.clone().unwrap().outcoming_quota - count_established_connections; + while free_outcoming_rooms > 0 { + let ep = if !reachable_endpoints.is_empty() { + reachable_endpoints.pop().unwrap() + } else if !unreachable_endpoints.is_empty() { + unreachable_endpoints.pop().unwrap() + } else { + break; + }; + self.connect_to_without_checking_quotas(ep); + free_outcoming_rooms -= 1; + } + } + pub fn connect_to(&mut self, endpoint: NetworkEndpoint) -> () { + // Add endpoint to endpoints list (if there isn't already) + match self.ws2p_endpoints.get(&endpoint.node_full_id().unwrap()) { + Some(_) => { + self.ws2p_endpoints + .get_mut(&endpoint.node_full_id().unwrap()) + .unwrap() + .1 = WS2PConnectionState::NeverTry; + } + None => { + self.ws2p_endpoints.insert( + endpoint.node_full_id().unwrap(), + (endpoint.clone(), WS2PConnectionState::NeverTry), + ); + } + }; + if self.conf.clone().unwrap().outcoming_quota > self.count_established_connections() { + self.connect_to_without_checking_quotas(endpoint); + } + } + fn close_connection(&mut self, ws2p_full_id: &NodeFullId, reason: WS2PCloseConnectionReason) { + match reason { + WS2PCloseConnectionReason::NegociationTimeout => {} + WS2PCloseConnectionReason::AuthMessInvalidSig + | WS2PCloseConnectionReason::Timeout + | WS2PCloseConnectionReason::Unknow => { + self.ws2p_endpoints + .get_mut(ws2p_full_id) + .expect("Failure : attempt to delete a non-existent connection !") + .1 = WS2PConnectionState::Close + } + } + self.connections_meta_datas.remove(ws2p_full_id); + self.websockets.remove(ws2p_full_id).expect(&format!( + "Fatal error : no websocket for {} !", + ws2p_full_id + )); + self.threads_senders_channels.remove(ws2p_full_id); + } + pub fn ws2p_conn_message_pretreatment(&mut self, message: WS2PConnectionMessage) -> WS2PSignal { + let connections_count = self.connections_meta_datas.len(); + if connections_count == 0 { + return WS2PSignal::NoConnection; + } + let ws2p_full_id = message.0; + match message.1 { + WS2PConnectionMessagePayload::WrongUrl + | WS2PConnectionMessagePayload::FailOpenWS + | WS2PConnectionMessagePayload::FailToSplitWS => { + self.ws2p_endpoints.get_mut(&ws2p_full_id).unwrap().1 = + WS2PConnectionState::WSError; + return WS2PSignal::WSError(ws2p_full_id); + } + WS2PConnectionMessagePayload::TryToSendConnectMess => { + self.ws2p_endpoints.get_mut(&ws2p_full_id).unwrap().1 = + WS2PConnectionState::TryToSendConnectMess; + } + WS2PConnectionMessagePayload::FailSendConnectMess => { + self.ws2p_endpoints.get_mut(&ws2p_full_id).unwrap().1 = + WS2PConnectionState::Unreachable; + } + WS2PConnectionMessagePayload::WebsocketOk(sender) => { + self.websockets.insert(ws2p_full_id, sender); + } + WS2PConnectionMessagePayload::ValidConnectMessage(response, new_con_state) => { + self.ws2p_endpoints.get_mut(&ws2p_full_id).unwrap().1 = new_con_state; + if let WS2PConnectionState::ConnectMessOk = self.ws2p_endpoints[&ws2p_full_id].1 { + trace!("Send: {:#?}", response); + self.websockets + .get_mut(&ws2p_full_id) + .expect(&format!( + "Fatal error : no websocket for {} !", + ws2p_full_id + )) + .0 + .send_message(&Message::text(response)) + .unwrap(); + } + } + WS2PConnectionMessagePayload::ValidAckMessage(r, new_con_state) => { + self.ws2p_endpoints.get_mut(&ws2p_full_id).unwrap().1 = new_con_state; + if let WS2PConnectionState::AckMessOk = self.ws2p_endpoints[&ws2p_full_id].1 { + trace!("DEBUG : Send: {:#?}", r); + self.websockets + .get_mut(&ws2p_full_id) + .expect(&format!( + "Fatal error : no websocket for {} !", + ws2p_full_id + )) + .0 + .send_message(&Message::text(r)) + .unwrap(); + } + } + WS2PConnectionMessagePayload::ValidOk(new_con_state) => { + self.ws2p_endpoints.get_mut(&ws2p_full_id).unwrap().1 = new_con_state; + match self.ws2p_endpoints[&ws2p_full_id].1 { + WS2PConnectionState::OkMessOkWaitingAckMess => {} + WS2PConnectionState::Established => { + return WS2PSignal::ConnectionEstablished(ws2p_full_id) + } + _ => { + self.threads_senders_channels[&ws2p_full_id] + .send(WS2POrderForListeningThread::Close) + .unwrap(); + self.close_connection(&ws2p_full_id, WS2PCloseConnectionReason::Unknow); + return WS2PSignal::Empty; + } + } + } + WS2PConnectionMessagePayload::DalRequest(req_id, req_body) => { + return WS2PSignal::DalRequest(ws2p_full_id, req_id, req_body); + } + WS2PConnectionMessagePayload::PeerCard(body, ws2p_endpoints) => { + return WS2PSignal::PeerCard(ws2p_full_id, body, ws2p_endpoints); + } + WS2PConnectionMessagePayload::Heads(heads) => { + let mut applied_heads = Vec::with_capacity(heads.len()); + for head in heads { + if let Some(head) = NetworkHead::from_json_value(&head) { + if head.verify() { + if (self.my_head.is_none() + || head.node_full_id() + != self.my_head.clone().unwrap().node_full_id()) + && head.apply(&mut self.heads_cache) + { + applied_heads.push(head); + } + } + } + } + return WS2PSignal::Heads(ws2p_full_id, applied_heads); + } + WS2PConnectionMessagePayload::Document(network_doc) => { + return WS2PSignal::Document(ws2p_full_id, network_doc); + } + WS2PConnectionMessagePayload::ReqResponse(req_id, response) => { + if self.requests_awaiting_response.len() > req_id.0 as usize { + if let Some((ref ws2p_request, ref recipient_fulld_id, ref _timestamp)) = + self.requests_awaiting_response.remove(&req_id) + { + return WS2PSignal::ReqResponse( + req_id, + ws2p_request.clone(), + *recipient_fulld_id, + response, + ); + } + } + } + WS2PConnectionMessagePayload::NegociationTimeout => { + match self.ws2p_endpoints[&ws2p_full_id].1 { + WS2PConnectionState::AckMessOk | WS2PConnectionState::ConnectMessOk => { + self.ws2p_endpoints.get_mut(&ws2p_full_id).unwrap().1 = + WS2PConnectionState::Denial + } + WS2PConnectionState::WaitingConnectMess => { + self.ws2p_endpoints.get_mut(&ws2p_full_id).unwrap().1 = + WS2PConnectionState::NoResponse + } + _ => { + self.ws2p_endpoints.get_mut(&ws2p_full_id).unwrap().1 = + WS2PConnectionState::Unreachable + } + } + self.close_connection(&ws2p_full_id, WS2PCloseConnectionReason::NegociationTimeout); + return WS2PSignal::NegociationTimeout(ws2p_full_id); + } + WS2PConnectionMessagePayload::Timeout => { + self.close_connection(&ws2p_full_id, WS2PCloseConnectionReason::Timeout); + return WS2PSignal::Timeout(ws2p_full_id); + } + WS2PConnectionMessagePayload::UnknowMessage => warn!( + "WS2P : Receive Unknow Message from {}.", + &self.connections_meta_datas[&ws2p_full_id] + .remote_pubkey + .unwrap() + ), + WS2PConnectionMessagePayload::WrongFormatMessage => warn!( + "WS2P : Receive Wrong Format Message from {}.", + &self.connections_meta_datas[&ws2p_full_id] + .remote_pubkey + .unwrap() + ), + WS2PConnectionMessagePayload::InvalidMessage => return WS2PSignal::Empty, + WS2PConnectionMessagePayload::Close => { + self.close_connection(&ws2p_full_id, WS2PCloseConnectionReason::AuthMessInvalidSig) + } + } + // Detect timeout requests + let mut requests_timeout = Vec::new(); + for &(ref req, ref _ws2p_full_id, ref timestamp) in + self.requests_awaiting_response.clone().values() + { + if SystemTime::now().duration_since(*timestamp).unwrap() > Duration::new(20, 0) { + requests_timeout.push(req.get_req_full_id()); + warn!("request timeout : {:?}", req); + } + } + // Delete (and resend) timeout requests + for req_id in requests_timeout { + //let ws2p_endpoints = self.ws2p_endpoints.clone(); + let _request_option = self.requests_awaiting_response.remove(&req_id.1); + /*if let Some((request, _, _)) = request_option { + let _request_result = self.send_request_to_specific_node( + &get_random_connection(&ws2p_endpoints), + &request, + ); + }*/ + } + WS2PSignal::Empty + } + + pub fn send_request_to_all_connections( + &mut self, + ws2p_request: &NetworkRequest, + ) -> Result<(), SendRequestError> { + let mut count_successful_sending: usize = 0; + let mut errors: Vec<websocket::WebSocketError> = Vec::new(); + match ws2p_request.clone() { + NetworkRequest::GetCurrent(req_full_id, _receiver) => { + for (ws2p_full_id, (_ep, state)) in self.ws2p_endpoints.clone() { + if let WS2PConnectionState::Established = state { + let ws2p_request = NetworkRequest::GetCurrent( + ModuleReqFullId( + req_full_id.clone().0, + ModuleReqId( + (self.requests_awaiting_response.len() + + count_successful_sending) + as u32, + ), + ), + ws2p_full_id, + ); + match self.send_request_to_specific_node(&ws2p_full_id, &ws2p_request) { + Ok(_) => count_successful_sending += 1, + Err(e) => errors.push(e), + }; + } + } + } + /* NetworkRequest::GetBlock(req_full_id, number) => {} */ + NetworkRequest::GetBlocks(_req_full_id, _receiver, _count, _from_number) => {} + NetworkRequest::GetRequirementsPending(req_full_id, _receiver, min_cert) => { + for (ws2p_full_id, (_ep, state)) in self.ws2p_endpoints.clone() { + if let WS2PConnectionState::Established = state { + let ws2p_request = NetworkRequest::GetRequirementsPending( + ModuleReqFullId( + req_full_id.clone().0, + ModuleReqId(self.requests_awaiting_response.len() as u32), + ), + ws2p_full_id, + min_cert, + ); + match self.send_request_to_specific_node(&ws2p_full_id, &ws2p_request) { + Ok(_) => count_successful_sending += 1, + Err(e) => errors.push(e), + }; + } + } + } + _ => { + return Err(SendRequestError::RequestTypeMustNotBeTransmitted()); + } + } + debug!("count_successful_sending = {}", count_successful_sending); + if !errors.is_empty() { + return Err(SendRequestError::WSError(count_successful_sending, errors)); + } + Ok(()) + } + + pub fn send_request_to_specific_node( + &mut self, + receiver_ws2p_full_id: &NodeFullId, + ws2p_request: &NetworkRequest, + ) -> Result<(), websocket::WebSocketError> { + self.websockets + .get_mut(receiver_ws2p_full_id) + .unwrap() + .0 + .send_message(&Message::text( + network_request_to_json(ws2p_request).to_string(), + ))?; + self.requests_awaiting_response.insert( + ws2p_request.get_req_id(), + ( + ws2p_request.clone(), + *receiver_ws2p_full_id, + SystemTime::now(), + ), + ); + debug!( + "send request {} to {}", + network_request_to_json(ws2p_request).to_string(), + receiver_ws2p_full_id + ); + Ok(()) + } + + fn connect_to_without_checking_quotas(&mut self, endpoint: NetworkEndpoint) -> () { + // update connection state + self.ws2p_endpoints + .get_mut(&endpoint.node_full_id().unwrap()) + .expect("Fatal error: try to connect to unlisted endpoint ! ") + .1 = WS2PConnectionState::TryToOpenWS; + + // get endpoint url + let ws_url = endpoint.get_url(true); + + // Create WS2PConnection + let mut conn_meta_datas = WS2PConnectionMetaData::new( + "b60a14fd-0826-4ae0-83eb-1a92cd59fd5308535fd3-78f2-4678-9315-cd6e3b7871b1".to_string(), + ); + conn_meta_datas.remote_pubkey = Some(endpoint.pubkey()); + conn_meta_datas.remote_uuid = Some(endpoint.node_uuid().unwrap()); + + // Prepare datas for listening thread + let mut datas_for_listening_thread = WS2PDatasForListeningThread { + conn_meta_datas: conn_meta_datas.clone(), + currency: self.currency.clone().unwrap(), + key_pair: self.key_pair.unwrap(), + }; + + // Create CONNECT Message + let mut connect_message = WS2PConnectMessageV1 { + currency: self.currency.clone().unwrap(), + pubkey: self.key_pair.unwrap().pubkey, + challenge: conn_meta_datas.challenge.clone(), + signature: None, + }; + connect_message.signature = Some(connect_message.sign(self.key_pair.unwrap())); + let json_connect_message = + serde_json::to_string(&connect_message).expect("Fail to serialize CONNECT message !"); + + // Log + trace!("Try connection to {} ...", ws_url); + + // Listen incoming messages into a thread + let sender_to_main_thread: mpsc::Sender<WS2PThreadSignal> = + mpsc::Sender::clone(&self.main_thread_channel.0); + let (tx2, rx2) = mpsc::channel(); + self.connections_meta_datas + .insert(conn_meta_datas.node_full_id(), conn_meta_datas.clone()); + self.threads_senders_channels + .insert(conn_meta_datas.node_full_id(), tx2); + thread::spawn(move || { + // Open websocket + let open_ws_time = SystemTime::now(); + let client = match ClientBuilder::new(&ws_url) { + Ok(mut client_builder) => match client_builder.connect_insecure() { + Ok(c) => c, + Err(_) => { + debug!("WS2PConnectResult::FailOpenWS"); + sender_to_main_thread + .send(WS2PThreadSignal::WS2PConnectionMessage( + WS2PConnectionMessage( + datas_for_listening_thread.conn_meta_datas.node_full_id(), + WS2PConnectionMessagePayload::FailOpenWS, + ), + )) + .unwrap_or(()); + return (); + } + }, + Err(_) => { + warn!("WS2PConnectResult::WrongUrl : {}", ws_url); + sender_to_main_thread + .send(WS2PThreadSignal::WS2PConnectionMessage( + WS2PConnectionMessage( + datas_for_listening_thread.conn_meta_datas.node_full_id(), + WS2PConnectionMessagePayload::WrongUrl, + ), + )) + .unwrap_or(()); + + return (); + } + }; + let (mut receiver, mut sender) = match client.split() { + Ok((mut r, mut s)) => (r, s), + Err(_) => { + sender_to_main_thread + .send(WS2PThreadSignal::WS2PConnectionMessage( + WS2PConnectionMessage( + datas_for_listening_thread.conn_meta_datas.node_full_id(), + WS2PConnectionMessagePayload::FailToSplitWS, + ), + )) + .unwrap_or(()); + return (); + } + }; + + // Send CONNECT Message + sender_to_main_thread + .send(WS2PThreadSignal::WS2PConnectionMessage( + WS2PConnectionMessage( + datas_for_listening_thread.conn_meta_datas.node_full_id(), + WS2PConnectionMessagePayload::TryToSendConnectMess, + ), + )) + .unwrap_or(()); + match sender.send_message(&Message::text(json_connect_message)) { + Ok(_) => { + sender_to_main_thread + .send(WS2PThreadSignal::WS2PConnectionMessage( + WS2PConnectionMessage( + datas_for_listening_thread.conn_meta_datas.node_full_id(), + WS2PConnectionMessagePayload::WebsocketOk(WebsocketSender(sender)), + ), + )) + .unwrap_or(()); + } + Err(_) => { + receiver.shutdown_all().unwrap_or(()); + sender_to_main_thread + .send(WS2PThreadSignal::WS2PConnectionMessage( + WS2PConnectionMessage( + datas_for_listening_thread.conn_meta_datas.node_full_id(), + WS2PConnectionMessagePayload::FailSendConnectMess, + ), + )) + .unwrap_or(()); + return (); + } + } + + let mut last_mess_time = SystemTime::now(); + let mut spam_interval = false; + let mut spam_counter = 0; + for incoming_message in receiver.incoming_messages() { + // Spam ? + if SystemTime::now().duration_since(last_mess_time).unwrap() + > Duration::new(*WS2P_SPAM_INTERVAL_IN_MILLI_SECS, 0) + { + if spam_interval { + spam_counter += 1; + } else { + spam_interval = true; + spam_counter = 2; + } + } else { + spam_interval = false; + spam_counter = 0; + } + // Spam ? + if spam_counter >= *WS2P_SPAM_LIMIT { + thread::sleep(Duration::from_millis(*WS2P_SPAM_SLEEP_TIME_IN_SEC)); + last_mess_time = SystemTime::now(); + } else { + // Negociation timeout ? + if datas_for_listening_thread.conn_meta_datas.state + != WS2PConnectionState::Established + && SystemTime::now().duration_since(open_ws_time).unwrap() + > Duration::new(*WS2P_NEGOTIATION_TIMEOUT, 0) + { + sender_to_main_thread + .send(WS2PThreadSignal::WS2PConnectionMessage( + WS2PConnectionMessage( + datas_for_listening_thread.conn_meta_datas.node_full_id(), + WS2PConnectionMessagePayload::NegociationTimeout, + ), + )) + .unwrap_or(()); + break; + } + // Connection timeout ? + else if SystemTime::now().duration_since(last_mess_time).unwrap() + > Duration::new(*WS2P_CONNECTION_TIMEOUT, 0) + { + sender_to_main_thread + .send(WS2PThreadSignal::WS2PConnectionMessage( + WS2PConnectionMessage( + datas_for_listening_thread.conn_meta_datas.node_full_id(), + WS2PConnectionMessagePayload::Timeout, + ), + )) + .unwrap_or(()); + break; + } + last_mess_time = SystemTime::now(); + match rx2.recv_timeout(Duration::from_millis(40)) { + Ok(s) => match s { + WS2POrderForListeningThread::Close => break, + }, + Err(e) => { + match e { + mpsc::RecvTimeoutError::Timeout => { + match incoming_message { + Ok(message) => { + if message.is_close() { + if sender_to_main_thread + .send(WS2PThreadSignal::WS2PConnectionMessage( + WS2PConnectionMessage( + datas_for_listening_thread + .conn_meta_datas + .node_full_id(), + WS2PConnectionMessagePayload::Close, + ), + )) + .is_ok() + { + break; + } + } else if message.is_data() { + // Parse message + let m = Message::from(message); + let s: String = from_utf8(&m.payload) + .unwrap() + .to_string(); + let message: serde_json::Value = + serde_json::from_str(&s) + .unwrap(); + let result = sender_to_main_thread.send( + WS2PThreadSignal::WS2PConnectionMessage( + WS2PConnectionMessage( + datas_for_listening_thread + .conn_meta_datas + .node_full_id(), + datas_for_listening_thread + .conn_meta_datas + .parse_and_check_incoming_message( + &datas_for_listening_thread + .currency, + datas_for_listening_thread + .key_pair, + &message, + ), + ), + ), + ); + if result.is_err() { + debug!("Close ws2p connection because ws2p main thread is unrechable !"); + break; + } + } + } + Err(e) => { + warn!("WebSocketError : {} ! Close ", e); + //receiver.shutdown_all().unwrap_or(()); + break; + } + }; + } + mpsc::RecvTimeoutError::Disconnected => { + break; + } + } + } + }; + } + } + }); + } +} + +#[cfg(test)] +mod tests { + extern crate duniter_conf; + extern crate duniter_crypto; + extern crate duniter_dal; + extern crate duniter_documents; + extern crate duniter_message; + extern crate duniter_module; + extern crate duniter_network; + + use self::duniter_crypto::keys::ed25519; + use self::duniter_crypto::keys::PublicKey; + use self::duniter_dal::parsers::blocks::parse_json_block; + use self::duniter_documents::blockchain::v10::documents::BlockDocument; + use self::duniter_module::DuniterModule; + use self::duniter_network::network_endpoint::{NetworkEndpoint, NetworkEndpointApi}; + use self::duniter_network::NetworkBlock; + use super::*; + use std::fs; + use std::path::PathBuf; + use std::time::{SystemTime, UNIX_EPOCH}; + + #[test] + fn test_parse_json_block() { + let json_block = json!({ + "fork": false, + "version": 10, + "nonce": 10500000059239 as u64, + "number": 109966, + "powMin": 88, + "time": 1523300656, + "medianTime": 1523295259, + "membersCount": 933, + "monetaryMass": 146881563, + "unitbase": 0, + "issuersCount": 44, + "issuersFrame": 221, + "issuersFrameVar": 0, + "currency": "g1", + "issuer": "GRBPV3Y7PQnB9LaZhSGuS3BqBJbSHyibzYq65kTh1nQ4", + "signature": "GCg2Lti3TdxWlhA8JF8pRI+dRQ0XZVtcC4BqO/COTpjTQFdWG6qmUNVvdeYCtR/lu1JQe3N/IhrbyV6L/6I+Cg==", + "hash": "000000EF5B2AA849F4C3AF3D35E1284EA1F34A9F617EA806CE8371619023DC74", + "parameters": "", + "previousHash": "000004C00602F8A27AE078DE6351C0DDA1EA0974A78D2BEFA7DFBE7B7C3146FD", + "previousIssuer": "5SwfQubSat5SunNafCsunEGTY93nVM4kLSsuprNqQb6S", + "inner_hash": "61F02B1A6AE2E4B9A1FD66CE673258B4B21C0076795571EE3C9DC440DD06C46C", + "dividend": null, + "identities": [], + "joiners": [], + "actives": [], + "leavers": [], + "revoked": [], + "excluded": [], + "certifications": [ + "Hm5qjaNuHogNRdGZ4vgnLA9DMZVUu5YWzVup5mubuxCc:8AmdBsimcLziXaCS4AcVUfPx7rkjeic7482dLbBkuZw6:109964:yHKBGMeuxyIqFb295gVNK6neRC+U0tmsX1Zed3TLjS3ZZHYYycE1piLcYsTKll4ifNVp6rm+hd/CLdHYB+29CA==", + "BncjgJeFpGsMCCsUfzNLEexjsbuX3V2mg9P67ov2LkwK:DyBUBNpzpfvjtwYYSaVMM6ST6t2DNg3NCE9CU9bRQFhF:105864:cJEGW9WxJwlMA2+4LNAK4YieyseUy1WIkFh1YLYD+JJtJEoCSnIQRXzhiAoRpGaj0bRz8sTpwI6PRkuVoDJJDQ==" + ], + "transactions": [ + { + "version": 10, + "currency": "g1", + "locktime": 0, + "hash": "80FE1E83DC4D0B722CA5F8363EFC6A3E29071032EBB71C1E0DF8D4FEA589C698", + "blockstamp": "109964-00000168105D4A8A8BC8C0DC70033F45ABE472782C75A7F2074D0F4D4A3B7B2B", + "blockstampTime": 0, + "issuers": [ + "6PiqcuUWhyiBF3Lgcht8c1yfk6gMfQzcUc46CqrJfeLT" + ], + "inputs": [ + "1001:0:D:6PiqcuUWhyiBF3Lgcht8c1yfk6gMfQzcUc46CqrJfeLT:98284", + "1001:0:D:6PiqcuUWhyiBF3Lgcht8c1yfk6gMfQzcUc46CqrJfeLT:98519", + "1001:0:D:6PiqcuUWhyiBF3Lgcht8c1yfk6gMfQzcUc46CqrJfeLT:98779", + "1001:0:D:6PiqcuUWhyiBF3Lgcht8c1yfk6gMfQzcUc46CqrJfeLT:99054", + "1001:0:D:6PiqcuUWhyiBF3Lgcht8c1yfk6gMfQzcUc46CqrJfeLT:99326", + "1001:0:D:6PiqcuUWhyiBF3Lgcht8c1yfk6gMfQzcUc46CqrJfeLT:99599", + "1001:0:D:6PiqcuUWhyiBF3Lgcht8c1yfk6gMfQzcUc46CqrJfeLT:99884", + "1001:0:D:6PiqcuUWhyiBF3Lgcht8c1yfk6gMfQzcUc46CqrJfeLT:100174", + "1001:0:D:6PiqcuUWhyiBF3Lgcht8c1yfk6gMfQzcUc46CqrJfeLT:100469", + "1001:0:D:6PiqcuUWhyiBF3Lgcht8c1yfk6gMfQzcUc46CqrJfeLT:100746", + "1001:0:D:6PiqcuUWhyiBF3Lgcht8c1yfk6gMfQzcUc46CqrJfeLT:101036", + "1001:0:D:6PiqcuUWhyiBF3Lgcht8c1yfk6gMfQzcUc46CqrJfeLT:101327" + ], + "outputs": [ + "12000:0:SIG(HmH5beJqKGMeotcQUrSW7Wo5tKvAksHmfYXfiSQ9EbWz)", + "12:0:SIG(6PiqcuUWhyiBF3Lgcht8c1yfk6gMfQzcUc46CqrJfeLT)" + ], + "unlocks": [ + "0:SIG(0)", + "1:SIG(0)", + "2:SIG(0)", + "3:SIG(0)", + "4:SIG(0)", + "5:SIG(0)", + "6:SIG(0)", + "7:SIG(0)", + "8:SIG(0)", + "9:SIG(0)", + "10:SIG(0)", + "11:SIG(0)" + ], + "signatures": [ + "MZxoKxYgwufh/s5mwLCsYEZXtIsP1hEKCyAzLipJsvCbR9xj7wXUw0C/ahwvZfBtR7+QVPIfLmwYEol1JcHjDw==" + ], + "comment": "Adhesion 2018" + }, + { + "version": 10, + "currency": "g1", + "locktime": 0, + "hash": "B80507412B35BD5EB437AE0D3EB97E60E3A4974F5CDEA1AF7E2127C0E943481F", + "blockstamp": "109964-00000168105D4A8A8BC8C0DC70033F45ABE472782C75A7F2074D0F4D4A3B7B2B", + "blockstampTime": 0, + "issuers": [ + "8gundJEbfm73Kx3jjw8YivJyz8qD2igjf6baCBLFCxPU" + ], + "inputs": [ + "1001:0:D:8gundJEbfm73Kx3jjw8YivJyz8qD2igjf6baCBLFCxPU:91560", + "1001:0:D:8gundJEbfm73Kx3jjw8YivJyz8qD2igjf6baCBLFCxPU:91850", + "1001:0:D:8gundJEbfm73Kx3jjw8YivJyz8qD2igjf6baCBLFCxPU:92111", + "1001:0:D:8gundJEbfm73Kx3jjw8YivJyz8qD2igjf6baCBLFCxPU:92385", + "1001:0:D:8gundJEbfm73Kx3jjw8YivJyz8qD2igjf6baCBLFCxPU:92635" + ], + "outputs": [ + "5000:0:SIG(BzHnbec1Gov7dLSt1EzJS7vikoQCECeuvZs4wamZAcT1)", + "5:0:SIG(8gundJEbfm73Kx3jjw8YivJyz8qD2igjf6baCBLFCxPU)" + ], + "unlocks": [ + "0:SIG(0)", + "1:SIG(0)", + "2:SIG(0)", + "3:SIG(0)", + "4:SIG(0)" + ], + "signatures": [ + "A+ukwRvLWs1gZQ0KAqAnknEgmRQHdrnOvNuBx/WZqje17BAPrVxSxKpqwU6MiajU+ppigsYp6Bu0FdPf/tGnCQ==" + ], + "comment": "" + }, + { + "version": 10, + "currency": "g1", + "locktime": 0, + "hash": "D8970E6629C0381A78534EEDD86803E9215A7EC4C494BAEA79EB19425F9B4D31", + "blockstamp": "109964-00000168105D4A8A8BC8C0DC70033F45ABE472782C75A7F2074D0F4D4A3B7B2B", + "blockstampTime": 0, + "issuers": [ + "FnSXE7QyBfs4ozoYAt5NEewWhHEPorf38cNXu3kX9xsg" + ], + "inputs": [ + "1000:0:D:FnSXE7QyBfs4ozoYAt5NEewWhHEPorf38cNXu3kX9xsg:36597", + "1000:0:D:FnSXE7QyBfs4ozoYAt5NEewWhHEPorf38cNXu3kX9xsg:36880", + "1000:0:D:FnSXE7QyBfs4ozoYAt5NEewWhHEPorf38cNXu3kX9xsg:37082" + ], + "outputs": [ + "3000:0:SIG(BBC8Rnh4CWN1wBrPLevK7GRFFVDVw7Lu24YNMUmhqoHU)" + ], + "unlocks": [ + "0:SIG(0)", + "1:SIG(0)", + "2:SIG(0)" + ], + "signatures": [ + "OpiF/oQfIigOeAtsteukU0w9FPSELE+BVTxhmsQ8bEeYGlwovG2VF8ZFiJkLLPi6vFuKgwzULJfjNGd97twZCw==" + ], + "comment": "1 billet pour une seance.pour un chouette film" + } + ], + }); + let mut block: BlockDocument = + match parse_json_block(&json_block).expect("Fail to parse test json block !") { + NetworkBlock::V10(network_block_v10) => network_block_v10.uncompleted_block_doc, + _ => { + panic!("Test block must be a v10 block !"); + } + }; + assert_eq!( + block.inner_hash.unwrap().to_hex(), + "61F02B1A6AE2E4B9A1FD66CE673258B4B21C0076795571EE3C9DC440DD06C46C" + ); + block.compute_hash(); + assert_eq!( + block.hash.unwrap().0.to_hex(), + "000000EF5B2AA849F4C3AF3D35E1284EA1F34A9F617EA806CE8371619023DC74" + ); + } + + #[test] + fn endpoint_db_tests() { + let test_db_path = PathBuf::from("test.db"); + if test_db_path.as_path().exists() { + fs::remove_file(&test_db_path).unwrap(); + } + let db = WS2PModuleDatas::open_db(test_db_path).unwrap(); + + let current_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + + let mut endpoint = NetworkEndpoint::parse_from_raw( + "WS2P cb06a19b g1.imirhil.fr 53012 /", + ed25519::PublicKey::from_base58("5gJYnQp8v7bWwk7EWRoL8vCLof1r3y9c6VDdnGSM1GLv") + .unwrap(), + 1, + current_time.as_secs(), + ).expect("Failt to parse test endpoint !"); + + ws2p_db::write_endpoint(&db, &endpoint, 1, current_time.as_secs()); + let mut written_endpoints = + ws2p_db::get_endpoints_for_api(&db, NetworkEndpointApi(String::from("WS2P"))); + assert_eq!(endpoint, written_endpoints.pop().unwrap()); + + // Test status update + endpoint.set_status(3); + ws2p_db::write_endpoint(&db, &endpoint, 3, current_time.as_secs()); + let mut written_endpoints = + ws2p_db::get_endpoints_for_api(&db, NetworkEndpointApi(String::from("WS2P"))); + assert_eq!(endpoint, written_endpoints.pop().unwrap()); + } + + #[test] + fn ws2p_requests() { + let module_id = WS2PModule::id(); + let request = NetworkRequest::GetBlocks( + ModuleReqFullId(module_id, ModuleReqId(58)), + NodeFullId::default(), + 50, + 0, + ); + assert_eq!( + network_request_to_json(&request), + json!({ + "reqId": format!("{:x}", 58), + "body": { + "name": "BLOCKS_CHUNK", + "params": { + "count": 50, + "fromNumber": 0 + } + } + }) + ); + assert_eq!( + network_request_to_json(&request).to_string(), + "{\"body\":{\"name\":\"BLOCKS_CHUNK\",\"params\":{\"count\":50,\"fromNumber\":0}},\"reqId\":\"3a\"}" + ); + } + + #[test] + fn ws2p_parse_head() { + let head = json!({ + "message": "WS2POTMIC:HEAD:1:D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx:104512-0000051B9CE9C1CA89F269375A6751FB88B9E88DE47A36506057E5BFBCFBB276:c1c39a0a:duniter:1.6.21:3", + "sig": "trtK9GXvTdfND995ohWEderpO3NkIqi1X6mBeVvMcaHckq+lIGqjWvJ9t9Vccz5t+VGaSmGUihDl4q6eldIYBw==", + "messageV2": "WS2POTMIC:HEAD:2:D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx:104512-0000051B9CE9C1CA89F269375A6751FB88B9E88DE47A36506057E5BFBCFBB276:c1c39a0a:duniter:1.6.21:3:25:22", + "sigV2": "x6ehPMuYjGY+z7wEGnJGyMBxMKUdu01RWaF0b0XCtoVjg67cCvT4H0V/Qcxn4bAGqzy5ux2fA7NiI+81bBnqDw==", + "step": 0 + }); + let mut heads_count = 0; + if let Some(head) = NetworkHead::from_json_value(&head) { + if let NetworkHead::V2(ref head_v2) = head { + heads_count += 1; + assert_eq!( + head_v2.message.to_string(), + String::from("WS2POTMIC:HEAD:1:D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx:104512-0000051B9CE9C1CA89F269375A6751FB88B9E88DE47A36506057E5BFBCFBB276:c1c39a0a:duniter:1.6.21:3") + ); + } + assert_eq!(head.verify(), true); + } + assert_eq!(heads_count, 1); + } +} diff --git a/ws2p/ok_message.rs b/ws2p/ok_message.rs new file mode 100644 index 0000000000000000000000000000000000000000..9572c822c9761930a622a9cf0eca9ceff631acc4 --- /dev/null +++ b/ws2p/ok_message.rs @@ -0,0 +1,69 @@ +extern crate duniter_crypto; +extern crate serde; +extern crate serde_json; + +use self::serde::ser::{Serialize, SerializeStruct, Serializer}; +use super::WS2PMessage; +use duniter_crypto::keys::ed25519::PublicKey as ed25519PublicKey; +use duniter_crypto::keys::PublicKey; + +#[derive(Debug, Clone)] +pub struct WS2POkMessageV1 { + pub currency: String, + pub pubkey: ed25519PublicKey, + pub challenge: String, + pub signature: Option<duniter_crypto::keys::ed25519::Signature>, +} + +impl WS2PMessage for WS2POkMessageV1 { + fn parse(v: &serde_json::Value, currency: String) -> Option<Self> { + let signature = match v.get("sig") { + Some(signature) => signature + .as_str() + .expect("Parsing of OK message : fail to convert sig to str") + .to_string(), + None => return None, + }; + let pubkey: ed25519PublicKey = ed25519PublicKey::from_base58( + "969qRJs8KhsnkyzqarpL4RKZGMdVKNbZgu8fhsigM7Lj", + ).expect("fail to create default pubkey !"); + let signature: Option<duniter_crypto::keys::ed25519::Signature> = Some( + duniter_crypto::keys::Signature::from_base64(&signature) + .expect("fail to parse signature of OK message !"), + ); + Some(WS2POkMessageV1 { + currency, + pubkey, + challenge: "".to_string(), + signature, + }) + } + fn to_raw(&self) -> String { + format!( + "WS2P:OK:{}:{}:{}", + self.currency, self.pubkey, self.challenge + ) + } + fn verify(&self) -> bool { + self.pubkey + .verify(self.to_raw().as_bytes(), &self.signature.unwrap()) + } +} + +impl Serialize for WS2POkMessageV1 { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let mut connect_message_in_json = serializer.serialize_struct("message", 2)?; + connect_message_in_json.serialize_field("auth", "OK")?; + connect_message_in_json.serialize_field( + "sig", + &self + .signature + .expect("Fail to serialize OK message : the signature field is set to None !") + .to_string(), + )?; + connect_message_in_json.end() + } +} diff --git a/ws2p/serializer.rs b/ws2p/serializer.rs new file mode 100644 index 0000000000000000000000000000000000000000..f780006811a746eb6e6c93426abd9ed65d7a0f1f --- /dev/null +++ b/ws2p/serializer.rs @@ -0,0 +1,21 @@ +extern crate serde_json; + +use duniter_network::network_head::*; + +use std::ops::Deref; + +pub fn serialize_head(head: NetworkHead) -> serde_json::Value { + match head { + NetworkHead::V2(box_head_v2) => { + let head_v2 = box_head_v2.deref(); + json!({ + "message": head_v2.message.to_string(), + "sig": head_v2.sig.to_string(), + "messageV2": head_v2.message_v2.to_string(), + "sigV2": head_v2.sig_v2.to_string(), + "step": head_v2.step + 1 + }) + } + _ => panic!("HEAD version not supported !"), + } +} diff --git a/ws2p/test.db b/ws2p/test.db new file mode 100644 index 0000000000000000000000000000000000000000..e14c9a19404e0e792e709cf7079a7e068f3cfb13 Binary files /dev/null and b/ws2p/test.db differ diff --git a/ws2p/ws2p_connection.rs b/ws2p/ws2p_connection.rs new file mode 100644 index 0000000000000000000000000000000000000000..a36f820971f15c46b5774c90dd338cc2bc570ad3 --- /dev/null +++ b/ws2p/ws2p_connection.rs @@ -0,0 +1,403 @@ +extern crate serde_json; +extern crate websocket; + +use duniter_crypto::keys::ed25519; +use duniter_crypto::keys::PublicKey; +use duniter_dal::parsers::blocks::parse_json_block; +use duniter_module::ModuleReqId; +use duniter_network::network_endpoint::{NetworkEndpoint, NetworkEndpointApi}; +use duniter_network::{NetworkDocument, NodeUUID}; +use std::fmt::Debug; +use std::net::TcpStream; + +use super::{NodeFullId, WS2PAckMessageV1, WS2PConnectMessageV1, WS2PMessage, WS2POkMessageV1}; + +#[derive(Debug, Copy, Clone)] +pub enum WS2POrderForListeningThread { + Close, +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum WS2PConnectionState { + NeverTry = 0, + TryToOpenWS = 1, + WSError = 2, + TryToSendConnectMess = 3, + Unreachable = 4, + WaitingConnectMess = 5, + NoResponse = 6, + ConnectMessOk = 7, + OkMessOkWaitingAckMess = 8, + AckMessOk = 9, + Denial = 10, + Close = 11, + Established = 12, +} + +impl From<u32> for WS2PConnectionState { + fn from(integer: u32) -> Self { + match integer { + 1 | 2 => WS2PConnectionState::WSError, + 3 | 4 => WS2PConnectionState::Unreachable, + 5 | 6 => WS2PConnectionState::NoResponse, + 7 | 8 | 9 | 10 => WS2PConnectionState::Denial, + 11 | 12 => WS2PConnectionState::Close, + _ => WS2PConnectionState::NeverTry, + } + } +} + +impl WS2PConnectionState { + pub fn from_u32(integer: u32, from_db: bool) -> Self { + if from_db { + WS2PConnectionState::from(integer) + } else { + match integer { + 1 => WS2PConnectionState::TryToOpenWS, + 2 => WS2PConnectionState::WSError, + 3 | 4 => WS2PConnectionState::Unreachable, + 5 | 6 => WS2PConnectionState::NoResponse, + 7 => WS2PConnectionState::ConnectMessOk, + 8 => WS2PConnectionState::OkMessOkWaitingAckMess, + 9 => WS2PConnectionState::AckMessOk, + 10 => WS2PConnectionState::Denial, + 11 => WS2PConnectionState::Close, + 12 => WS2PConnectionState::Established, + _ => WS2PConnectionState::NeverTry, + } + } + } + pub fn to_u32(&self) -> u32 { + match *self { + WS2PConnectionState::NeverTry => 0, + _ => 1, + } + } +} + +pub struct WebsocketSender(pub websocket::sender::Writer<TcpStream>); + +impl Debug for WebsocketSender { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "WebsocketSender {{ }}") + } +} + +#[derive(Debug)] +pub enum WS2PConnectionMessagePayload { + FailOpenWS, + WrongUrl, + FailToSplitWS, + TryToSendConnectMess, + FailSendConnectMess, + WebsocketOk(WebsocketSender), + NegociationTimeout, + ValidConnectMessage(String, WS2PConnectionState), + ValidAckMessage(String, WS2PConnectionState), + ValidOk(WS2PConnectionState), + DalRequest(ModuleReqId, serde_json::Value), + PeerCard(serde_json::Value, Vec<NetworkEndpoint>), + Heads(Vec<serde_json::Value>), + Document(NetworkDocument), + ReqResponse(ModuleReqId, serde_json::Value), + InvalidMessage, + WrongFormatMessage, + UnknowMessage, + Timeout, + Close, +} + +#[derive(Debug)] +pub struct WS2PConnectionMessage(pub NodeFullId, pub WS2PConnectionMessagePayload); + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum WS2PCloseConnectionReason { + AuthMessInvalidSig, + NegociationTimeout, + Timeout, + Unknow, +} + +#[derive(Debug, Clone)] +pub struct WS2PConnectionMetaData { + pub state: WS2PConnectionState, + pub remote_uuid: Option<NodeUUID>, + pub remote_pubkey: Option<ed25519::PublicKey>, + pub challenge: String, + pub remote_challenge: String, + pub current_blockstamp: Option<(u32, String)>, +} + +#[derive(Debug, Clone)] +pub struct WS2PDatasForListeningThread { + pub conn_meta_datas: WS2PConnectionMetaData, + pub currency: String, + pub key_pair: ed25519::KeyPair, +} + +impl WS2PConnectionMetaData { + pub fn new(challenge: String) -> Self { + WS2PConnectionMetaData { + state: WS2PConnectionState::WaitingConnectMess, + remote_uuid: None, + remote_pubkey: None, + challenge, + remote_challenge: "".to_string(), + current_blockstamp: None, + } + } + + pub fn node_full_id(&self) -> NodeFullId { + NodeFullId( + self.clone() + .remote_uuid + .expect("Fail to get NodeFullId : remote_uuid is None !"), + self.remote_pubkey + .expect("Fail to get NodeFullId : remote_pubkey is None !"), + ) + } + pub fn parse_and_check_incoming_message( + &mut self, + currency: &str, + key_pair: ed25519::KeyPair, + m: &serde_json::Value, + ) -> WS2PConnectionMessagePayload { + if let Some(s) = m.get("auth") { + if s.is_string() { + match s.as_str().unwrap() { + "CONNECT" => { + let message = WS2PConnectMessageV1::parse(m, currency.to_string()) + .expect("Failed to parsing CONNECT Message !"); + if message.verify() && message.pubkey == self.remote_pubkey.unwrap() { + match self.state { + WS2PConnectionState::WaitingConnectMess => { + trace!("CONNECT sig is valid."); + self.state = WS2PConnectionState::ConnectMessOk; + self.remote_challenge = message.challenge.clone(); + let mut response = WS2PAckMessageV1 { + currency: currency.to_string(), + pubkey: key_pair.pubkey, + challenge: self.remote_challenge.clone(), + signature: None, + }; + response.signature = Some(response.sign(key_pair)); + return WS2PConnectionMessagePayload::ValidConnectMessage( + serde_json::to_string(&response).unwrap(), + self.state.clone(), + ); + } + _ => return WS2PConnectionMessagePayload::InvalidMessage, + } + } else { + warn!("The signature of message CONNECT is invalid !") + } + } + "ACK" => { + let mut message = WS2PAckMessageV1::parse(m, currency.to_string()) + .expect("Failed to parsing ACK Message !"); + message.challenge = self.challenge.to_string(); + if message.verify() { + trace!("ACK sig is valid."); + self.state = match self.state { + WS2PConnectionState::ConnectMessOk => { + WS2PConnectionState::AckMessOk + } + WS2PConnectionState::OkMessOkWaitingAckMess => { + WS2PConnectionState::Established + } + _ => return WS2PConnectionMessagePayload::InvalidMessage, + }; + let mut response = WS2POkMessageV1 { + currency: currency.to_string(), + pubkey: key_pair.pubkey, + challenge: self.challenge.to_string(), + signature: None, + }; + response.signature = Some(response.sign(key_pair)); + return WS2PConnectionMessagePayload::ValidAckMessage( + serde_json::to_string(&response).unwrap(), + self.state.clone(), + ); + } else { + warn!("The signature of message ACK is invalid !") + } + } + "OK" => { + let mut message = WS2POkMessageV1::parse(m, currency.to_string()) + .expect("Failed to parsing OK Message !"); + trace!("Received OK"); + message.challenge = self.remote_challenge.to_string(); + message.pubkey = self.remote_pubkey.expect("fail to get remote pubkey !"); + if message.verify() { + trace!("OK sig is valid."); + match self.state { + WS2PConnectionState::ConnectMessOk => { + self.state = WS2PConnectionState::OkMessOkWaitingAckMess; + return WS2PConnectionMessagePayload::ValidOk( + self.state.clone(), + ); + } + WS2PConnectionState::AckMessOk => { + info!( + "WS2P Connection established with the key {}", + self.remote_pubkey.expect("fail to get remote pubkey !") + ); + self.state = WS2PConnectionState::Established; + return WS2PConnectionMessagePayload::ValidOk( + self.state.clone(), + ); + } + _ => { + warn!("WS2P Error : OK message not expected !"); + return WS2PConnectionMessagePayload::InvalidMessage; + } + } + } else { + warn!("The signature of message OK is invalid !"); + return WS2PConnectionMessagePayload::InvalidMessage; + } + } + &_ => debug!("unknow message"), + }; + } + }; + if let Some(req_id) = m.get("reqId") { + match req_id.as_str() { + Some(req_id) => match m.get("body") { + Some(body) => { + trace!("WS2P : Receive DAL Request from {}.", self.node_full_id()); + match u32::from_str_radix(req_id, 16) { + Ok(req_id) => { + return WS2PConnectionMessagePayload::DalRequest( + ModuleReqId(req_id), + body.clone(), + ); + } + Err(_) => return WS2PConnectionMessagePayload::WrongFormatMessage, + } + } + None => { + warn!("WS2P Error : invalid format : Request must contain a field body !"); + return WS2PConnectionMessagePayload::WrongFormatMessage; + } + }, + None => { + warn!("WS2P Error : invalid format : Request must contain a field body !"); + return WS2PConnectionMessagePayload::WrongFormatMessage; + } + } + } + if let Some(req_id) = m.get("resId") { + match req_id.as_str() { + Some(req_id_str) => match m.get("body") { + Some(body) => match u32::from_str_radix(req_id_str, 16) { + Ok(req_id) => { + return WS2PConnectionMessagePayload::ReqResponse( + ModuleReqId(req_id), + body.clone(), + ) + } + Err(_) => return WS2PConnectionMessagePayload::WrongFormatMessage, + }, + None => match m.get("err") { + Some(err) => warn!("Error in req : {:?}", err), + None => return WS2PConnectionMessagePayload::WrongFormatMessage, + }, + }, + None => return WS2PConnectionMessagePayload::WrongFormatMessage, + } + } + if let Some(body) = m.get("body") { + match body.get("name") { + Some(s) => if s.is_string() { + match s.as_str().unwrap() { + "BLOCK" => match body.get("block") { + Some(block) => { + if let Some(network_block) = parse_json_block(&block) { + return WS2PConnectionMessagePayload::Document( + NetworkDocument::Block(network_block), + ); + } else { + info!("WS2PSignal: receive invalid block (wrong format)."); + }; + } + None => return WS2PConnectionMessagePayload::WrongFormatMessage, + }, + "HEAD" => match body.get("heads") { + Some(heads) => match heads.as_array() { + Some(heads_array) => { + return WS2PConnectionMessagePayload::Heads(heads_array.clone()) + } + None => return WS2PConnectionMessagePayload::WrongFormatMessage, + }, + None => return WS2PConnectionMessagePayload::WrongFormatMessage, + }, + "PEER" => return self.parse_and_check_peer_message(body), + "CERTIFICATION" => { + trace!("WS2P : Receive CERTIFICATION from {}.", self.node_full_id()); + /*return WS2PConnectionMessagePayload::Document( + NetworkDocument::Certification(_) + );*/ + } + _ => { + /*trace!( + "WS2P : Receive Unknow Message from {}.", + self.node_full_id() + );*/ + return WS2PConnectionMessagePayload::UnknowMessage; + } + }; + }, + None => { + warn!("WS2P Error : invalid format : Body must contain a field name !"); + return WS2PConnectionMessagePayload::WrongFormatMessage; + } + } + }; + WS2PConnectionMessagePayload::UnknowMessage + } + + pub fn parse_and_check_peer_message( + &mut self, + body: &serde_json::Value, + ) -> WS2PConnectionMessagePayload { + match body.get("peer") { + Some(peer) => match peer.get("pubkey") { + Some(raw_pubkey) => match PublicKey::from_base58(raw_pubkey.as_str().unwrap_or("")) + { + Ok(pubkey) => { + let mut ws2p_endpoints: Vec<NetworkEndpoint> = Vec::new(); + match peer.get("endpoints") { + Some(endpoints) => match endpoints.as_array() { + Some(array_endpoints) => { + for endpoint in array_endpoints { + if let Some(ep) = NetworkEndpoint::parse_from_raw( + endpoint.as_str().unwrap_or(""), + pubkey, + 0, + 0, + ) { + if ep.api() == NetworkEndpointApi(String::from("WS2P")) + { + ws2p_endpoints.push(ep); + } + } + } + WS2PConnectionMessagePayload::PeerCard( + body.clone(), + ws2p_endpoints, + ) + } + None => WS2PConnectionMessagePayload::WrongFormatMessage, + }, + None => WS2PConnectionMessagePayload::WrongFormatMessage, + } + } + Err(_) => WS2PConnectionMessagePayload::WrongFormatMessage, + }, + None => WS2PConnectionMessagePayload::WrongFormatMessage, + }, + None => WS2PConnectionMessagePayload::WrongFormatMessage, + } + } +} diff --git a/ws2p/ws2p_db.rs b/ws2p/ws2p_db.rs new file mode 100644 index 0000000000000000000000000000000000000000..f487710d74802ccdec80588d990b7285d9fb9697 --- /dev/null +++ b/ws2p/ws2p_db.rs @@ -0,0 +1,138 @@ +extern crate duniter_crypto; +extern crate duniter_documents; +extern crate duniter_message; +extern crate duniter_module; +extern crate duniter_network; +extern crate serde_json; +extern crate sqlite; +extern crate websocket; + +use duniter_crypto::keys::{ed25519, PublicKey}; +use duniter_network::network_endpoint::{NetworkEndpoint, NetworkEndpointApi}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum EndpointApi { + WS2P, + //WS2PS, + //WS2PTOR, + //DASA, + //BMA, + //BMAS, +} + +impl From<u32> for EndpointApi { + fn from(integer: u32) -> Self { + match integer { + _ => EndpointApi::WS2P, + } + } +} + +pub fn string_to_api(api: &str) -> Option<EndpointApi> { + match api { + "WS2P" => Some(EndpointApi::WS2P), + //"WS2PS" => Some(EndpointApi::WS2PS), + //"WS2PTOR" => Some(EndpointApi::WS2PTOR), + //"DASA" => Some(EndpointApi::DASA), + //"BASIC_MERKLED_API" => Some(EndpointApi::BMA), + //"BMAS" => Some(EndpointApi::BMAS), + &_ => None, + } +} + +pub fn api_to_integer(api: &NetworkEndpointApi) -> i64 { + match api.0.as_str() { + "WS2P" => 1, + //EndpointApi::WS2PS => 2, + //EndpointApi::WS2PTOR => 3, + //EndpointApi::DASA => 4, + //EndpointApi::BMA => 5, + //EndpointApi::BMAS => 6, + _ => 0, + } +} + +pub fn get_endpoints_for_api( + db: &sqlite::Connection, + api: NetworkEndpointApi, +) -> Vec<NetworkEndpoint> { + let mut cursor:sqlite::Cursor = db + .prepare("SELECT hash_full_id, status, node_id, pubkey, api, version, endpoint, last_check FROM endpoints WHERE api=? ORDER BY status DESC;") + .expect("get_endpoints_for_api() : Error in SQL request !") + .cursor(); + + cursor + .bind(&[sqlite::Value::Integer(api_to_integer(&api))]) + .expect("get_endpoints_for_api() : Error in cursor binding !"); + let mut endpoints = Vec::new(); + while let Some(row) = cursor + .next() + .expect("get_endpoints_for_api() : Error in cursor.next()") + { + let raw_ep = row[6].as_string().unwrap().to_string(); + let ep_issuer = ed25519::PublicKey::from_base58(row[3].as_string().unwrap()).unwrap(); + let mut ep = match NetworkEndpoint::parse_from_raw( + &raw_ep, + ep_issuer, + row[1].as_integer().unwrap() as u32, + row[7].as_integer().unwrap() as u64, + ) { + Some(ep) => ep, + None => panic!(format!("Fail to parse endpoint : {}", raw_ep)), + }; + ep.set_status(row[1].as_integer().unwrap() as u32); + ep.set_last_check(row[7].as_integer().unwrap() as u64); + + endpoints.push(ep); + } + endpoints +} + +pub fn write_endpoint( + db: &sqlite::Connection, + endpoint: &NetworkEndpoint, + new_status: u32, + new_last_check: u64, +) { + let hash_full_id = endpoint + .node_full_id() + .expect("Fail to write endpoint : node_full_id() return None !") + .sha256(); + // Check if endpoint it's already written + let mut cursor: sqlite::Cursor = db + .prepare("SELECT status FROM endpoints WHERE hash_full_id=? ORDER BY status DESC;") + .expect("write_endpoint() : Error in SQL request !") + .cursor(); + cursor + .bind(&[sqlite::Value::String(hash_full_id.to_string())]) + .expect("write_endpoint() : Error in cursor binding !"); + + // If endpoint it's already written, update status + if let Some(row) = cursor + .next() + .expect("write_endpoint() : Error in cursor.next()") + { + if row[0].as_integer().expect("fail to read ep status !") as u32 != endpoint.status() { + db.execute(format!( + "UPDATE endpoints SET status={} WHERE hash_full_id='{}'", + endpoint.status(), + hash_full_id + )).expect("Fail to parse SQL request update endpoint status !"); + } + } else { + if let &NetworkEndpoint::V1(ref ep_v1) = endpoint { + db + .execute( + format!( + "INSERT INTO endpoints (hash_full_id, status, node_id, pubkey, api, version, endpoint, last_check) VALUES ('{}', {}, {}, '{}', {}, {}, '{}', {});", + ep_v1.hash_full_id.expect("ep_v1.hash_full_id = None"), new_status, ep_v1.node_id.expect("ep_v1.node_id = None").0, + ep_v1.issuer.to_string(), api_to_integer(&ep_v1.api), + ep_v1.version, ep_v1.raw_endpoint, new_last_check + ) + ) + .expect("Fail to parse SQL request INSERT endpoint !"); + } else { + panic!("write_endpoint() : Endpoint version is not supported !") + } + } +} diff --git a/ws2p/ws2p_requests.rs b/ws2p/ws2p_requests.rs new file mode 100644 index 0000000000000000000000000000000000000000..722b0154b8a156a119774775671c41ab1a6f7eb6 --- /dev/null +++ b/ws2p/ws2p_requests.rs @@ -0,0 +1,44 @@ +extern crate duniter_crypto; +extern crate duniter_network; +extern crate serde; +extern crate serde_json; + +use duniter_network::NetworkRequest; + +pub fn network_request_to_json(request: &NetworkRequest) -> serde_json::Value { + let (request_id, request_type, request_params) = match *request { + NetworkRequest::GetCurrent(ref req_full_id, _receiver) => { + (req_full_id.1, "CURRENT", json!({})) + } + NetworkRequest::GetBlocks(ref req_full_id, _receiver, count, from_mumber) => ( + req_full_id.1, + "BLOCKS_CHUNK", + json!({ + "count": count, + "fromNumber": from_mumber + }), + ), + NetworkRequest::GetRequirementsPending(ref req_full_id, _receiver, min_cert) => ( + req_full_id.1, + "WOT_REQUIREMENTS_OF_PENDING", + json!({ "minCert": min_cert }), + ), + NetworkRequest::GetConsensus(_) => { + panic!("GetConsensus() request must be not convert to json !"); + } + NetworkRequest::GetHeadsCache(_) => { + panic!("GetHeadsCache() request must be not convert to json !"); + } + NetworkRequest::GetEndpoints(_) => { + panic!("GetEndpoints() request must be not convert to json !"); + } + }; + + json!({ + "reqId": request_id, + "body" : { + "name": request_type, + "params": request_params + } + }) +}