commit 9058467be1246e4693cb0098a62b18d01b498700 Author: Peilun Li Date: Wed Jan 3 18:24:52 2024 +0800 Initial commit diff --git a/.github/actions-rs/grcov.yml b/.github/actions-rs/grcov.yml new file mode 100644 index 0000000..5cfdff8 --- /dev/null +++ b/.github/actions-rs/grcov.yml @@ -0,0 +1,3 @@ +branch: false +ignore-not-existing: true +llvm: true \ No newline at end of file diff --git a/.github/workflows/cc.yml b/.github/workflows/cc.yml new file mode 100644 index 0000000..f824d0e --- /dev/null +++ b/.github/workflows/cc.yml @@ -0,0 +1,53 @@ +name: code-coverage + +on: + push: + branches: [ "main"] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + unittest-cov: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Cargo cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }} + + - name: Install 1.71.0 toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: 1.71.0 + override: true + + - name: Run unittest + run: cargo test --all-features --no-fail-fast + env: + CARGO_INCREMENTAL: '0' + RUSTC_BOOTSTRAP: '1' + RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Copt-level=0 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests' + RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Copt-level=0 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests' + + - id: coverage + uses: actions-rs/grcov@v0.1 + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ${{ steps.coverage.outputs.report }} + fail_ci_if_error: true \ No newline at end of file diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..792c2c1 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,100 @@ +# Based on https://github.com/actions-rs/meta/blob/master/recipes/quickstart.md +# +# While our "example" application has the platform-specific code, +# for simplicity we are compiling and testing everything on the Ubuntu environment only. +# For multi-OS testing see the `cross.yml` workflow. + +name: rust + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + CARGO_TERM_COLOR: always + +jobs: + check: + name: check + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + + - name: Run cargo check + uses: actions-rs/cargo@v1 + with: + command: check + + test: + name: test + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Cargo cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + + - name: Run cargo test + uses: actions-rs/cargo@v1 + with: + command: test + args: --release + + lints: + name: lints + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: rustfmt, clippy + + - name: Run cargo fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + - name: Run cargo clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: -- -D warnings \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..7dc76f1 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,50 @@ +name: functional-test + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Cargo cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Build + run: cargo build --release --verbose + + - name: Set up Python 3.9 + uses: actions/setup-python@v4 + with: + python-version: '3.9' + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + + - name: Run tests + run: | + cd tests + python test_all.py \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a2e98aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +/target +# src/config.rs +/.idea +tests/**/__pycache__ +tests/tmp/** +.vscode/*.json diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f8df220 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "zerog-storage-contracts"] + path = zerog-storage-contracts + url = git@github.com:zero-gravity-labs/zerog-storage-contracts.git +[submodule "zerog-storage-client"] + path = zerog-storage-client + url = git@github.com:zero-gravity-labs/zerog-storage-client.git diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..072c7ba --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,8082 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array", +] + +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if", + "cipher 0.3.0", + "cpufeatures 0.2.9", + "ctr 0.8.0", + "opaque-debug", +] + +[[package]] +name = "aes" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures 0.2.9", +] + +[[package]] +name = "aes-gcm" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6" +dependencies = [ + "aead", + "aes 0.7.5", + "cipher 0.3.0", + "ctr 0.8.0", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.10", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anyhow" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" +dependencies = [ + "backtrace", +] + +[[package]] +name = "append_merkle" +version = "0.1.0" +dependencies = [ + "anyhow", + "eth2_ssz", + "eth2_ssz_derive", + "ethereum-types 0.14.1", + "lazy_static", + "serde", + "tiny-keccak", + "tracing", +] + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + +[[package]] +name = "asn1_der" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "155a5a185e42c6b77ac7b88a15143d930a9e9727a5b7b77eed417404ab15c247" + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand 1.9.0", + "futures-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite", + "log", + "parking", + "polling", + "rustix 0.37.23", + "slab", + "socket2 0.4.9", + "waker-fn", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-process" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9" +dependencies = [ + "async-io", + "async-lock", + "autocfg", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 0.37.23", + "signal-hook", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-channel", + "async-global-executor", + "async-io", + "async-lock", + "async-process", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite 0.2.12", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-std-resolver" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2f8a4a203be3325981310ab243a28e6e4ea55b6519bffce05d41ab60e09ad8" +dependencies = [ + "async-std", + "async-trait", + "futures-io", + "futures-util", + "pin-utils", + "socket2 0.4.9", + "trust-dns-resolver", +] + +[[package]] +name = "async-task" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" + +[[package]] +name = "async-trait" +version = "0.1.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "async_io_stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" +dependencies = [ + "futures", + "pharos", + "rustc_version", +] + +[[package]] +name = "asynchronous-codec" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057f2c32adbb2fc158e22fb38433c8e9bbf76b75a4732c7c0cbaf695fb65568" +dependencies = [ + "bytes", + "futures-sink", + "futures-util", + "memchr", + "pin-project-lite 0.2.12", +] + +[[package]] +name = "atomic-waker" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" + +[[package]] +name = "attohttpc" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb8867f378f33f78a811a8eb9bf108ad99430d7aad43315dd9319c827ef6247" +dependencies = [ + "http", + "log", + "url", + "wildmatch", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "auto_impl" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fee3da8ef1276b0bee5dd1c7258010d8fffd31801447323115a25560e1327b89" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bech32" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dabbe35f96fb9507f7330793dc490461b2962659ac5d427181e451a623751d1" + +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" +dependencies = [ + "serde", +] + +[[package]] +name = "bimap" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bindgen" +version = "0.65.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.28", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + +[[package]] +name = "bitmaps" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703642b98a00b3b90513279a8ede3fcfa479c126c5fb46e78f3051522f021403" + +[[package]] +name = "bitvec" +version = "0.17.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41262f11d771fd4a61aa3ce019fca363b4b6c282fca9da2a31186d3965a47a5c" +dependencies = [ + "either", + "radium 0.3.0", +] + +[[package]] +name = "bitvec" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7774144344a4faa177370406a7ff5f1da24303817368584c6206c8303eb07848" +dependencies = [ + "funty 1.1.0", + "radium 0.6.2", + "tap", + "wyz 0.2.0", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty 2.0.0", + "radium 0.7.0", + "tap", + "wyz 0.5.1", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "atomic-waker", + "fastrand 1.9.0", + "futures-lite", + "log", +] + +[[package]] +name = "bs58" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" +dependencies = [ + "sha2 0.9.9", +] + +[[package]] +name = "bstr" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "byte-slice-cast" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +dependencies = [ + "serde", +] + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "camino" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cfa25e60aea747ec7e1124f238816749faa93759c6ff5b31f1ccdda137f4479" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chacha20" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c80e5460aa66fe3b91d40bcbdab953a597b60053e34d684ac6903f863b680a6" +dependencies = [ + "cfg-if", + "cipher 0.3.0", + "cpufeatures 0.2.9", + "zeroize", +] + +[[package]] +name = "chacha20poly1305" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18446b09be63d457bbec447509e85f662f32952b035ce892290396bc0b0cff5" +dependencies = [ + "aead", + "chacha20", + "cipher 0.3.0", + "poly1305", + "zeroize", +] + +[[package]] +name = "channel" +version = "0.1.0" +dependencies = [ + "tokio", +] + +[[package]] +name = "chrono" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "time 0.1.45", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "chunk_pool" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-lock", + "hashlink 0.8.3", + "log_entry_sync", + "network", + "shared_types", + "storage-async", + "tokio", + "tracing", +] + +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clang-sys" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_lex", + "indexmap 1.9.3", + "once_cell", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + +[[package]] +name = "coins-bip32" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30a84aab436fcb256a2ab3c80663d8aec686e6bae12827bb05fef3e1e439c9f" +dependencies = [ + "bincode", + "bs58", + "coins-core", + "digest 0.10.7", + "getrandom 0.2.10", + "hmac 0.12.1", + "k256 0.13.1", + "lazy_static", + "serde", + "sha2 0.10.7", + "thiserror", +] + +[[package]] +name = "coins-bip39" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84f4d04ee18e58356accd644896aeb2094ddeafb6a713e056cef0c0a8e468c15" +dependencies = [ + "bitvec 0.17.4", + "coins-bip32", + "getrandom 0.2.10", + "hmac 0.12.1", + "once_cell", + "pbkdf2 0.12.2", + "rand 0.8.5", + "sha2 0.10.7", + "thiserror", +] + +[[package]] +name = "coins-core" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b949a1c63fb7eb591eb7ba438746326aedf0ae843e51ec92ba6bec5bb382c4f" +dependencies = [ + "base64 0.21.2", + "bech32", + "bs58", + "digest 0.10.7", + "generic-array", + "hex", + "ripemd", + "serde", + "serde_derive", + "sha2 0.10.7", + "sha3", + "thiserror", +] + +[[package]] +name = "concurrent-queue" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "config" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" +dependencies = [ + "async-trait", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml 0.5.11", + "yaml-rust", +] + +[[package]] +name = "const-oid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "contract-interface" +version = "0.1.0" +dependencies = [ + "ethers", + "serde_json", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + +[[package]] +name = "cpufeatures" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" +dependencies = [ + "anes", + "atty", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools", + "lazy_static", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-bigint" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4c2f4e1afd912bc40bfd6fed5d9dc1f288e0ba01bfcc835cc5bc3eb13efe15" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "ctr" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" +dependencies = [ + "cipher 0.3.0", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "ctrlc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a011bbe2c35ce9c1f143b7af6f94f29a167beb4cd1d29e6740ce836f723120e" +dependencies = [ + "nix 0.26.2", + "windows-sys 0.48.0", +] + +[[package]] +name = "cuckoofilter" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b810a8449931679f64cd7eef1bbd0fa315801b6d5d9cdc1ace2804d6529eee18" +dependencies = [ + "byteorder", + "fnv", + "rand 0.7.3", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "4.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d4ba9852b42210c7538b75484f9daa0655e9a3ac04f693747bb0f02cf3cfe16" +dependencies = [ + "cfg-if", + "fiat-crypto", + "packed_simd_2", + "platforms", + "subtle", + "zeroize", +] + +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "data-encoding" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" + +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929" + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "directory" +version = "0.1.0" + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "discv5" +version = "0.1.0-beta.13" +dependencies = [ + "aes 0.7.5", + "aes-gcm", + "arrayvec", + "digest 0.10.7", + "enr 0.6.2", + "fnv", + "futures", + "hashlink 0.7.0", + "hex", + "hkdf", + "lazy_static", + "libp2p-core 0.30.2", + "lru", + "parking_lot 0.11.2", + "rand 0.8.5", + "rlp", + "sha2 0.10.7", + "smallvec", + "tokio", + "tokio-stream", + "tokio-util 0.6.10", + "tracing", + "tracing-subscriber", + "uint", + "zeroize", +] + +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + +[[package]] +name = "dns-parser" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4d33be9473d06f75f58220f71f7a9317aca647dc061dbd3c361b0bef505fbea" +dependencies = [ + "byteorder", + "quick-error", +] + +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + +[[package]] +name = "duration-str" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f037c488d179e21c87ef5fa9c331e8e62f5dddfa84618b41bb197da03edff1" +dependencies = [ + "chrono", + "nom", + "rust_decimal", + "serde", + "thiserror", + "time 0.3.25", +] + +[[package]] +name = "ecdsa" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +dependencies = [ + "der 0.6.1", + "elliptic-curve 0.12.3", + "rfc6979 0.3.1", + "signature 1.6.4", +] + +[[package]] +name = "ecdsa" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" +dependencies = [ + "der 0.7.8", + "digest 0.10.7", + "elliptic-curve 0.13.5", + "rfc6979 0.4.0", + "signature 2.1.0", + "spki 0.7.2", +] + +[[package]] +name = "ed25519" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +dependencies = [ + "signature 1.6.4", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek 3.2.0", + "ed25519", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct 0.1.1", + "crypto-bigint 0.4.9", + "der 0.6.1", + "digest 0.10.7", + "ff 0.12.1", + "generic-array", + "group 0.12.1", + "pkcs8 0.9.0", + "rand_core 0.6.4", + "sec1 0.3.0", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b" +dependencies = [ + "base16ct 0.2.0", + "crypto-bigint 0.5.2", + "digest 0.10.7", + "ff 0.13.0", + "generic-array", + "group 0.13.0", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1 0.7.3", + "subtle", + "zeroize", +] + +[[package]] +name = "ena" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c533630cf40e9caa44bd91aadc88a75d75a4c3a12b4cfde353cbed41daa1e1f1" +dependencies = [ + "log", +] + +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enr" +version = "0.6.2" +dependencies = [ + "base64 0.13.1", + "bs58", + "bytes", + "ed25519-dalek", + "hex", + "k256 0.11.6", + "log", + "rand 0.8.5", + "rlp", + "serde", + "sha3", + "zeroize", +] + +[[package]] +name = "enr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf56acd72bb22d2824e66ae8e9e5ada4d0de17a69c7fd35569dde2ada8ec9116" +dependencies = [ + "base64 0.13.1", + "bytes", + "hex", + "k256 0.13.1", + "log", + "rand 0.8.5", + "rlp", + "serde", + "sha3", + "zeroize", +] + +[[package]] +name = "enum-as-inner" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21cdad81446a7f7dc43f6a77409efeb9733d2fa65553efef6018ef257c959b73" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "backtrace", + "version_check", +] + +[[package]] +name = "eth-keystore" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fda3bf123be441da5260717e0661c25a2fd9cb2b2c1d20bf2e05580047158ab" +dependencies = [ + "aes 0.8.3", + "ctr 0.9.2", + "digest 0.10.7", + "hex", + "hmac 0.12.1", + "pbkdf2 0.11.0", + "rand 0.8.5", + "scrypt", + "serde", + "serde_json", + "sha2 0.10.7", + "sha3", + "thiserror", + "uuid", +] + +[[package]] +name = "eth2_hashing" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b67737df7e3769e823d9d583eb5d60bcc4b2ef97ca674d1964ef287a02f8517" +dependencies = [ + "cpufeatures 0.1.5", + "lazy_static", + "ring", + "sha2 0.9.9", +] + +[[package]] +name = "eth2_serde_utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "477fffc25490dfc866288273f96344c6879676a1337187fc39245cd422e10825" +dependencies = [ + "hex", + "serde", + "serde_derive", +] + +[[package]] +name = "eth2_ssz" +version = "0.4.0" +dependencies = [ + "ethereum-types 0.14.1", + "smallvec", +] + +[[package]] +name = "eth2_ssz_derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "635b86d2c941bb71e7419a571e1763d65c93e51a1bafc400352e3bef6ff59fc9" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "eth2_ssz_types" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9423ac7fb37037f828a32b724cdfa65ea62290055811731402a90fb8a5bcbb1" +dependencies = [ + "eth2_serde_utils", + "eth2_ssz", + "serde", + "serde_derive", + "tree_hash", + "typenum", +] + +[[package]] +name = "ethabi" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7413c5f74cc903ea37386a8965a936cbeb334bd270862fdece542c1b2dcbc898" +dependencies = [ + "ethereum-types 0.14.1", + "hex", + "once_cell", + "regex", + "serde", + "serde_json", + "sha3", + "thiserror", + "uint", +] + +[[package]] +name = "ethbloom" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb684ac8fa8f6c5759f788862bb22ec6fe3cb392f6bfd08e3c64b603661e3f8" +dependencies = [ + "crunchy", + "fixed-hash 0.7.0", + "impl-rlp", + "impl-serde 0.3.2", + "tiny-keccak", +] + +[[package]] +name = "ethbloom" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c22d4b5885b6aa2fe5e8b9329fb8d232bf739e434e6b87347c63bdd00c120f60" +dependencies = [ + "crunchy", + "fixed-hash 0.8.0", + "impl-codec 0.6.0", + "impl-rlp", + "impl-serde 0.4.0", + "scale-info", + "tiny-keccak", +] + +[[package]] +name = "ethereum-types" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05136f7057fe789f06e6d41d07b34e6f70d8c86e5693b60f97aaa6553553bdaf" +dependencies = [ + "ethbloom 0.11.1", + "fixed-hash 0.7.0", + "impl-rlp", + "impl-serde 0.3.2", + "primitive-types 0.10.1", + "uint", +] + +[[package]] +name = "ethereum-types" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d215cbf040552efcbe99a38372fe80ab9d00268e20012b79fcd0f073edd8ee" +dependencies = [ + "ethbloom 0.13.0", + "fixed-hash 0.8.0", + "impl-codec 0.6.0", + "impl-rlp", + "impl-serde 0.4.0", + "primitive-types 0.12.1", + "scale-info", + "uint", +] + +[[package]] +name = "ethers" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b4026b97da8281276744741fac7eb385da905f6093c583331fa2953fdd4253" +dependencies = [ + "ethers-addressbook", + "ethers-contract", + "ethers-core", + "ethers-etherscan", + "ethers-middleware", + "ethers-providers", + "ethers-signers", + "ethers-solc", +] + +[[package]] +name = "ethers-addressbook" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcb6ffefc230d8c42874c51b28dc11dbb8de50b27a8fdf92648439d6baa68dc" +dependencies = [ + "ethers-core", + "once_cell", + "serde", + "serde_json", +] + +[[package]] +name = "ethers-contract" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4719a44c3d37ab07c6dea99ab174068d8c35e441b60b6c20ce4e48357273e8" +dependencies = [ + "ethers-contract-abigen", + "ethers-contract-derive", + "ethers-core", + "ethers-providers", + "ethers-signers", + "futures-util", + "hex", + "once_cell", + "pin-project 1.1.3", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "ethers-contract-abigen" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "155ea1b84d169d231317ed86e307af6f2bed6b40dd17e5e94bc84da21cadb21c" +dependencies = [ + "Inflector", + "dunce", + "ethers-core", + "ethers-etherscan", + "eyre", + "hex", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "reqwest", + "serde", + "serde_json", + "syn 2.0.28", + "toml 0.7.6", + "walkdir", +] + +[[package]] +name = "ethers-contract-derive" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8567ff196c4a37c1a8c90ec73bda0ad2062e191e4f0a6dc4d943e2ec4830fc88" +dependencies = [ + "Inflector", + "ethers-contract-abigen", + "ethers-core", + "hex", + "proc-macro2", + "quote", + "serde_json", + "syn 2.0.28", +] + +[[package]] +name = "ethers-core" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60ca2514feb98918a0a31de7e1983c29f2267ebf61b2dc5d4294f91e5b866623" +dependencies = [ + "arrayvec", + "bytes", + "cargo_metadata", + "chrono", + "elliptic-curve 0.13.5", + "ethabi", + "generic-array", + "hex", + "k256 0.13.1", + "num_enum", + "once_cell", + "open-fastrlp", + "rand 0.8.5", + "rlp", + "serde", + "serde_json", + "strum 0.25.0", + "syn 2.0.28", + "tempfile", + "thiserror", + "tiny-keccak", + "unicode-xid", +] + +[[package]] +name = "ethers-etherscan" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22b3a8269d3df0ed6364bc05b4735b95f4bf830ce3aef87d5e760fb0e93e5b91" +dependencies = [ + "ethers-core", + "reqwest", + "semver", + "serde", + "serde_json", + "thiserror", + "tracing", +] + +[[package]] +name = "ethers-middleware" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c339aad74ae5c451d27e0e49c7a3c7d22620b119b4f9291d7aa21f72d7f366" +dependencies = [ + "async-trait", + "auto_impl", + "ethers-contract", + "ethers-core", + "ethers-etherscan", + "ethers-providers", + "ethers-signers", + "futures-channel", + "futures-locks", + "futures-util", + "instant", + "reqwest", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "tracing-futures", + "url", +] + +[[package]] +name = "ethers-providers" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b411b119f1cf0efb69e2190883dee731251882bb21270f893ee9513b3a697c48" +dependencies = [ + "async-trait", + "auto_impl", + "base64 0.21.2", + "bytes", + "enr 0.8.1", + "ethers-core", + "futures-channel", + "futures-core", + "futures-timer", + "futures-util", + "hashers", + "hex", + "http", + "instant", + "once_cell", + "pin-project 1.1.3", + "reqwest", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-tungstenite", + "tracing", + "tracing-futures", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "ws_stream_wasm", +] + +[[package]] +name = "ethers-signers" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4864d387456a9c09a1157fa10e1528b29d90f1d859443acf06a1b23365fb518c" +dependencies = [ + "async-trait", + "coins-bip32", + "coins-bip39", + "elliptic-curve 0.13.5", + "eth-keystore", + "ethers-core", + "hex", + "rand 0.8.5", + "sha2 0.10.7", + "thiserror", + "tracing", +] + +[[package]] +name = "ethers-solc" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6c2b9625a2c639d46625f88acc2092a3cb35786c37f7c2128b3ca20f639b3c" +dependencies = [ + "cfg-if", + "dunce", + "ethers-core", + "glob", + "hex", + "home", + "md-5", + "num_cpus", + "once_cell", + "path-slash", + "rayon", + "regex", + "semver", + "serde", + "serde_json", + "solang-parser", + "svm-rs", + "thiserror", + "tiny-keccak", + "tokio", + "tracing", + "walkdir", + "yansi", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "exit-future" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e43f2f1833d64e33f15592464d6fdd70f349dda7b1a53088eb83cd94014008c5" +dependencies = [ + "futures", +] + +[[package]] +name = "eyre" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" + +[[package]] +name = "file_location_cache" +version = "0.1.0" +dependencies = [ + "hashlink 0.8.3", + "network", + "parking_lot 0.12.1", + "priority-queue", + "rand 0.8.5", + "shared_types", + "tracing", +] + +[[package]] +name = "fixed-hash" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" +dependencies = [ + "byteorder", + "rand 0.8.5", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand 0.8.5", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" +dependencies = [ + "crc32fast", + "libz-sys", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", + "num_cpus", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite 0.2.12", + "waker-fn", +] + +[[package]] +name = "futures-locks" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ec6fe3675af967e67c5536c0b9d44e34e6c52f86bedc4ea49c5317b8e94d06" +dependencies = [ + "futures-channel", + "futures-task", +] + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "futures-rustls" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2411eed028cdf8c8034eaf21f9915f956b6c3abec4d4c7949ee67f0721127bd" +dependencies = [ + "futures-io", + "rustls 0.20.8", + "webpki", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" +dependencies = [ + "gloo-timers", + "send_wrapper 0.4.0", +] + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite 0.2.12", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + +[[package]] +name = "git-version" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b0decc02f4636b9ccad390dcbe77b722a77efedfa393caf8379a51d5c61899" +dependencies = [ + "git-version-macro", + "proc-macro-hack", +] + +[[package]] +name = "git-version-macro" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe69f1cbdb6e28af2bac214e943b99ce8a0a06b447d15d3e61161b0423139f3f" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "globset" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "gloo-net" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9902a044653b26b99f7e3693a42f171312d9be8b26b5697bd1e43ad1f8a35e10" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "js-sys", + "pin-project 1.1.3", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff 0.12.1", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff 0.13.0", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "h2" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 1.9.3", + "slab", + "tokio", + "tokio-util 0.7.8", + "tracing", +] + +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash 0.7.6", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.6", +] + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +dependencies = [ + "ahash 0.8.3", + "allocator-api2", +] + +[[package]] +name = "hashers" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2bca93b15ea5a746f220e56587f71e73c6165eab783df9e26590069953e3c30" +dependencies = [ + "fxhash", +] + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown 0.11.2", +] + +[[package]] +name = "hashlink" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312f66718a2d7789ffef4f4b7b213138ed9f1eb3aa1d0d82fc99f88fb3ffd26f" +dependencies = [ + "hashbrown 0.14.0", +] + +[[package]] +name = "hashset_delay" +version = "0.2.0" +dependencies = [ + "futures", + "tokio", + "tokio-util 0.6.10", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" + +[[package]] +name = "hex_fmt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" + +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac 0.12.1", +] + +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac-drbg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +dependencies = [ + "digest 0.9.0", + "generic-array", + "hmac 0.8.1", +] + +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite 0.2.12", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite 0.2.12", + "socket2 0.4.9", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +dependencies = [ + "http", + "hyper", + "log", + "rustls 0.20.8", + "rustls-native-certs", + "tokio", + "tokio-rustls 0.23.4", + "webpki-roots 0.22.6", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls 0.21.6", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows 0.48.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "if-addrs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc0fa01ffc752e9dbc72818cdb072cd028b86be5e09dd04c5a643704fe101a9" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "if-addrs" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cfc4a06638d2fd0dda83b01126fefd38ef9f04f54d2fc717a938df68b83a68d" +dependencies = [ + "libc", + "windows-sys 0.45.0", +] + +[[package]] +name = "if-watch" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "015a7df1eb6dda30df37f34b63ada9b7b352984b0e84de2a20ed526345000791" +dependencies = [ + "async-io", + "core-foundation", + "fnv", + "futures", + "if-addrs 0.7.0", + "ipnet", + "log", + "rtnetlink", + "system-configuration", + "windows 0.34.0", +] + +[[package]] +name = "igd" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556b5a75cd4adb7c4ea21c64af1c48cefb2ce7d43dc4352c720a1fe47c21f355" +dependencies = [ + "attohttpc", + "log", + "rand 0.8.5", + "url", + "xmltree", +] + +[[package]] +name = "impl-codec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161ebdfec3c8e3b52bf61c4f3550a1eea4f9579d10dc1b936f3171ebdcd6c443" +dependencies = [ + "parity-scale-codec 2.3.1", +] + +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec 3.6.4", +] + +[[package]] +name = "impl-rlp" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" +dependencies = [ + "rlp", +] + +[[package]] +name = "impl-serde" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4551f042f3438e64dbd6226b20527fc84a6e1fe65688b58746a2f53623f25f5c" +dependencies = [ + "serde", +] + +[[package]] +name = "impl-serde" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc88fc67028ae3db0c853baa36269d398d5f45b6982f95549ff5def78c935cd" +dependencies = [ + "serde", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.2", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.3", + "widestring", + "windows-sys 0.48.0", + "winreg 0.50.0", +] + +[[package]] +name = "ipnet" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi 0.3.2", + "rustix 0.38.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "jsonrpsee" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11e017217fcd18da0a25296d3693153dd19c8a6aadab330b3595285d075385d1" +dependencies = [ + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-http-client", + "jsonrpsee-http-server", + "jsonrpsee-proc-macros", + "jsonrpsee-types", + "jsonrpsee-wasm-client", + "jsonrpsee-ws-client", + "jsonrpsee-ws-server", + "tracing", +] + +[[package]] +name = "jsonrpsee-client-transport" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce395539a14d3ad4ec1256fde105abd36a2da25d578a291cabe98f45adfdb111" +dependencies = [ + "anyhow", + "futures-channel", + "futures-timer", + "futures-util", + "gloo-net", + "http", + "jsonrpsee-core", + "jsonrpsee-types", + "pin-project 1.1.3", + "rustls-native-certs", + "soketto", + "thiserror", + "tokio", + "tokio-rustls 0.23.4", + "tokio-util 0.7.8", + "tracing", + "webpki-roots 0.22.6", +] + +[[package]] +name = "jsonrpsee-core" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16efcd4477de857d4a2195a45769b2fe9ebb54f3ef5a4221d3b014a4fe33ec0b" +dependencies = [ + "anyhow", + "arrayvec", + "async-lock", + "async-trait", + "beef", + "futures-channel", + "futures-timer", + "futures-util", + "globset", + "hyper", + "jsonrpsee-types", + "lazy_static", + "parking_lot 0.12.1", + "rand 0.8.5", + "rustc-hash", + "serde", + "serde_json", + "soketto", + "thiserror", + "tokio", + "tracing", + "unicase", + "wasm-bindgen-futures", +] + +[[package]] +name = "jsonrpsee-http-client" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc1d8c0e4f455c47df21f8a29f4bbbcb75eb71bfee919b92e92502b48358392" +dependencies = [ + "async-trait", + "hyper", + "hyper-rustls 0.23.2", + "jsonrpsee-core", + "jsonrpsee-types", + "rustc-hash", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "jsonrpsee-http-server" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd69efeb3ce2cba767f126872f4eeb4624038a29098e75d77608b2b4345ad03" +dependencies = [ + "futures-channel", + "futures-util", + "hyper", + "jsonrpsee-core", + "jsonrpsee-types", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "jsonrpsee-proc-macros" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874cf3f6a027cebf36cae767feca9aa2e8a8f799880e49eb5540819fcbd8eada" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "jsonrpsee-types" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bcf76cd316f5d3ad48138085af1f45e2c58c98e02f0779783dbb034d43f7c86" +dependencies = [ + "anyhow", + "beef", + "serde", + "serde_json", + "thiserror", + "tracing", +] + +[[package]] +name = "jsonrpsee-wasm-client" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1fcb257e9b60de22ec3c151106ad1e089ab1c0691d25e3282f1cc7a0c7ba651" +dependencies = [ + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-types", +] + +[[package]] +name = "jsonrpsee-ws-client" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee043cb5dd0d51d3eb93432e998d5bae797691a7b10ec4a325e036bcdb48c48a" +dependencies = [ + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-types", +] + +[[package]] +name = "jsonrpsee-ws-server" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bd2e4d266774a671f8def3794255b28eddd09b18d76e0b913fa439f34588c0a" +dependencies = [ + "futures-channel", + "futures-util", + "jsonrpsee-core", + "jsonrpsee-types", + "serde_json", + "soketto", + "tokio", + "tokio-stream", + "tokio-util 0.7.8", + "tracing", +] + +[[package]] +name = "k256" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c1e0b51e7ec0a97369623508396067a486bd0cbed95a2659a4b863d28cfc8b" +dependencies = [ + "cfg-if", + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", + "sha2 0.10.7", +] + +[[package]] +name = "k256" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +dependencies = [ + "cfg-if", + "ecdsa 0.16.8", + "elliptic-curve 0.13.5", + "once_cell", + "sha2 0.10.7", + "signature 2.1.0", +] + +[[package]] +name = "keccak" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" +dependencies = [ + "cpufeatures 0.2.9", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "kvdb" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7d770dcb02bf6835887c3a979b5107a04ff4bbde97a5f0928d27404a155add9" +dependencies = [ + "smallvec", +] + +[[package]] +name = "kvdb-memorydb" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7a85fe66f9ff9cd74e169fdd2c94c6e1e74c412c99a73b4df3200b5d3760b2" +dependencies = [ + "kvdb", + "parking_lot 0.12.1", +] + +[[package]] +name = "kvdb-rocksdb" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b644c70b92285f66bfc2032922a79000ea30af7bc2ab31902992a5dcb9b434f6" +dependencies = [ + "kvdb", + "num_cpus", + "parking_lot 0.12.1", + "regex", + "rocksdb", + "smallvec", +] + +[[package]] +name = "lalrpop" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da4081d44f4611b66c6dd725e6de3169f9f63905421e8626fcb86b6a898998b8" +dependencies = [ + "ascii-canvas", + "bit-set", + "diff", + "ena", + "is-terminal", + "itertools", + "lalrpop-util", + "petgraph", + "regex", + "regex-syntax 0.7.4", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", +] + +[[package]] +name = "lalrpop-util" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f35c735096c0293d313e8f2a641627472b83d01b937177fe76e5e2708d31e0d" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libm" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" + +[[package]] +name = "libp2p" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41726ee8f662563fafba2d2d484b14037cc8ecb8c953fbfc8439d4ce3a0a9029" +dependencies = [ + "bytes", + "futures", + "futures-timer", + "getrandom 0.2.10", + "instant", + "lazy_static", + "libp2p-autonat", + "libp2p-core 0.33.0", + "libp2p-deflate", + "libp2p-dns", + "libp2p-floodsub", + "libp2p-gossipsub", + "libp2p-identify", + "libp2p-kad", + "libp2p-mdns", + "libp2p-metrics", + "libp2p-mplex", + "libp2p-noise", + "libp2p-ping", + "libp2p-plaintext", + "libp2p-pnet", + "libp2p-relay", + "libp2p-rendezvous", + "libp2p-request-response", + "libp2p-swarm", + "libp2p-swarm-derive", + "libp2p-tcp", + "libp2p-uds", + "libp2p-wasm-ext", + "libp2p-websocket", + "libp2p-yamux", + "multiaddr 0.14.0", + "parking_lot 0.12.1", + "pin-project 1.1.3", + "rand 0.7.3", + "smallvec", +] + +[[package]] +name = "libp2p-autonat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d45945fd2f96c4b133c23d5c28a8b7fc8d7138e6dd8d5a8cd492dd384f888e3" +dependencies = [ + "async-trait", + "futures", + "futures-timer", + "instant", + "libp2p-core 0.33.0", + "libp2p-request-response", + "libp2p-swarm", + "log", + "prost 0.10.4", + "prost-build 0.10.4", + "rand 0.8.5", +] + +[[package]] +name = "libp2p-core" +version = "0.30.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86aad7d54df283db817becded03e611137698a6509d4237a96881976a162340c" +dependencies = [ + "asn1_der", + "bs58", + "ed25519-dalek", + "either", + "fnv", + "futures", + "futures-timer", + "instant", + "lazy_static", + "libsecp256k1", + "log", + "multiaddr 0.13.0", + "multihash 0.14.0", + "multistream-select 0.10.4", + "parking_lot 0.11.2", + "pin-project 1.1.3", + "prost 0.9.0", + "prost-build 0.9.0", + "rand 0.8.5", + "ring", + "rw-stream-sink 0.2.1", + "sha2 0.9.9", + "smallvec", + "thiserror", + "unsigned-varint", + "void", + "zeroize", +] + +[[package]] +name = "libp2p-core" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b02602099fb75cb2d16f9ea860a320d6eb82ce41e95ab680912c454805cd5" +dependencies = [ + "asn1_der", + "bs58", + "ed25519-dalek", + "either", + "fnv", + "futures", + "futures-timer", + "instant", + "lazy_static", + "log", + "multiaddr 0.14.0", + "multihash 0.16.3", + "multistream-select 0.11.0", + "parking_lot 0.12.1", + "pin-project 1.1.3", + "prost 0.9.0", + "prost-build 0.9.0", + "rand 0.8.5", + "ring", + "rw-stream-sink 0.2.1", + "sha2 0.10.7", + "smallvec", + "thiserror", + "unsigned-varint", + "void", + "zeroize", +] + +[[package]] +name = "libp2p-core" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d46fca305dee6757022e2f5a4f6c023315084d0ed7441c3ab244e76666d979" +dependencies = [ + "asn1_der", + "bs58", + "ed25519-dalek", + "either", + "fnv", + "futures", + "futures-timer", + "instant", + "lazy_static", + "libsecp256k1", + "log", + "multiaddr 0.14.0", + "multihash 0.16.3", + "multistream-select 0.11.0", + "parking_lot 0.12.1", + "pin-project 1.1.3", + "prost 0.10.4", + "prost-build 0.10.4", + "rand 0.8.5", + "ring", + "rw-stream-sink 0.3.0", + "sha2 0.10.7", + "smallvec", + "thiserror", + "unsigned-varint", + "void", + "zeroize", +] + +[[package]] +name = "libp2p-deflate" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86adefc55ea4ed8201149f052fb441210727481dff1fb0b8318460206a79f5fb" +dependencies = [ + "flate2", + "futures", + "libp2p-core 0.33.0", +] + +[[package]] +name = "libp2p-dns" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbb462ec3a51fab457b4b44ac295e8b0a4b04dc175127e615cf996b1f0f1a268" +dependencies = [ + "async-std-resolver", + "futures", + "libp2p-core 0.33.0", + "log", + "parking_lot 0.12.1", + "smallvec", + "trust-dns-resolver", +] + +[[package]] +name = "libp2p-floodsub" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a505d0c6f851cbf2919535150198e530825def8bd3757477f13dc3a57f46cbcc" +dependencies = [ + "cuckoofilter", + "fnv", + "futures", + "libp2p-core 0.33.0", + "libp2p-swarm", + "log", + "prost 0.10.4", + "prost-build 0.10.4", + "rand 0.7.3", + "smallvec", +] + +[[package]] +name = "libp2p-gossipsub" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e064ba4d7832e01c738626c6b274ae100baba05f5ffcc7b265c2a3ed398108" +dependencies = [ + "asynchronous-codec", + "base64 0.13.1", + "byteorder", + "bytes", + "fnv", + "futures", + "hex_fmt", + "instant", + "libp2p-core 0.33.0", + "libp2p-swarm", + "log", + "prometheus-client", + "prost 0.10.4", + "prost-build 0.10.4", + "rand 0.7.3", + "regex", + "sha2 0.10.7", + "smallvec", + "unsigned-varint", + "wasm-timer", +] + +[[package]] +name = "libp2p-identify" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84b53490442d086db1fa5375670c9666e79143dccadef3f7c74a4346899a984" +dependencies = [ + "asynchronous-codec", + "futures", + "futures-timer", + "libp2p-core 0.33.0", + "libp2p-swarm", + "log", + "lru", + "prost 0.10.4", + "prost-build 0.10.4", + "prost-codec", + "smallvec", + "thiserror", + "void", +] + +[[package]] +name = "libp2p-kad" +version = "0.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6b5d4de90fcd35feb65ea6223fd78f3b747a64ca4b65e0813fbe66a27d56aa" +dependencies = [ + "arrayvec", + "asynchronous-codec", + "bytes", + "either", + "fnv", + "futures", + "futures-timer", + "instant", + "libp2p-core 0.33.0", + "libp2p-swarm", + "log", + "prost 0.10.4", + "prost-build 0.10.4", + "rand 0.7.3", + "sha2 0.10.7", + "smallvec", + "thiserror", + "uint", + "unsigned-varint", + "void", +] + +[[package]] +name = "libp2p-mdns" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4783f8cf00c7b6c1ff0f1870b4fcf50b042b45533d2e13b6fb464caf447a6951" +dependencies = [ + "async-io", + "data-encoding", + "dns-parser", + "futures", + "if-watch", + "lazy_static", + "libp2p-core 0.33.0", + "libp2p-swarm", + "log", + "rand 0.8.5", + "smallvec", + "socket2 0.4.9", + "void", +] + +[[package]] +name = "libp2p-metrics" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "564a7e5284d7d9b3140fdfc3cb6567bc32555e86a21de5604c2ec85da05cf384" +dependencies = [ + "libp2p-core 0.33.0", + "libp2p-gossipsub", + "libp2p-identify", + "libp2p-kad", + "libp2p-ping", + "libp2p-relay", + "libp2p-swarm", + "prometheus-client", +] + +[[package]] +name = "libp2p-mplex" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ff9c893f2367631a711301d703c47432af898c9bb8253bea0e2c051a13f7640" +dependencies = [ + "asynchronous-codec", + "bytes", + "futures", + "libp2p-core 0.33.0", + "log", + "nohash-hasher", + "parking_lot 0.12.1", + "rand 0.7.3", + "smallvec", + "unsigned-varint", +] + +[[package]] +name = "libp2p-noise" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2cee1dad1c83325bbd182a8e94555778699cec8a9da00086efb7522c4c15ad" +dependencies = [ + "bytes", + "curve25519-dalek 3.2.0", + "futures", + "lazy_static", + "libp2p-core 0.33.0", + "log", + "prost 0.10.4", + "prost-build 0.10.4", + "rand 0.8.5", + "sha2 0.10.7", + "snow", + "static_assertions", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "libp2p-ping" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d41516c82fe8dd148ec925eead0c5ec08a0628f7913597e93e126e4dfb4e0787" +dependencies = [ + "futures", + "futures-timer", + "instant", + "libp2p-core 0.33.0", + "libp2p-swarm", + "log", + "rand 0.7.3", + "void", +] + +[[package]] +name = "libp2p-plaintext" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db007e737adc5d28b2e03223b0210164928ad742591127130796a72aa8eaf54f" +dependencies = [ + "asynchronous-codec", + "bytes", + "futures", + "libp2p-core 0.33.0", + "log", + "prost 0.10.4", + "prost-build 0.10.4", + "unsigned-varint", + "void", +] + +[[package]] +name = "libp2p-pnet" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6468f382568da936b4fa1cff273ce59b1debf873ff5f4ca412c3b91d0b37442c" +dependencies = [ + "futures", + "log", + "pin-project 1.1.3", + "rand 0.8.5", + "salsa20", + "sha3", +] + +[[package]] +name = "libp2p-relay" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624ead3406f64437a0d4567c31bd128a9a0b8226d5f16c074038f5d0fc32f650" +dependencies = [ + "asynchronous-codec", + "bytes", + "either", + "futures", + "futures-timer", + "instant", + "libp2p-core 0.33.0", + "libp2p-swarm", + "log", + "pin-project 1.1.3", + "prost 0.10.4", + "prost-build 0.10.4", + "prost-codec", + "rand 0.8.5", + "smallvec", + "static_assertions", + "thiserror", + "void", +] + +[[package]] +name = "libp2p-rendezvous" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59967ea2db2c7560f641aa58ac05982d42131863fcd3dd6dcf0dd1daf81c60c" +dependencies = [ + "asynchronous-codec", + "bimap", + "futures", + "futures-timer", + "instant", + "libp2p-core 0.33.0", + "libp2p-swarm", + "log", + "prost 0.10.4", + "prost-build 0.10.4", + "rand 0.8.5", + "sha2 0.10.7", + "thiserror", + "unsigned-varint", + "void", +] + +[[package]] +name = "libp2p-request-response" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b02e0acb725e5a757d77c96b95298fd73a7394fe82ba7b8bbeea510719cbe441" +dependencies = [ + "async-trait", + "bytes", + "futures", + "instant", + "libp2p-core 0.33.0", + "libp2p-swarm", + "log", + "rand 0.7.3", + "smallvec", + "unsigned-varint", +] + +[[package]] +name = "libp2p-swarm" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4bb21c5abadbf00360c734f16bf87f1712ed4f23cd46148f625d2ddb867346" +dependencies = [ + "either", + "fnv", + "futures", + "futures-timer", + "instant", + "libp2p-core 0.33.0", + "log", + "pin-project 1.1.3", + "rand 0.7.3", + "smallvec", + "thiserror", + "void", +] + +[[package]] +name = "libp2p-swarm-derive" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f693c8c68213034d472cbb93a379c63f4f307d97c06f1c41e4985de481687a5" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "libp2p-tcp" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4933e38ef21b50698aefc87799c24f2a365c9d3f6cf50471f3f6a0bc410892" +dependencies = [ + "async-io", + "futures", + "futures-timer", + "if-addrs 0.7.0", + "if-watch", + "ipnet", + "libc", + "libp2p-core 0.33.0", + "log", + "socket2 0.4.9", + "tokio", +] + +[[package]] +name = "libp2p-uds" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24bdab114f7f2701757d6541266e1131b429bbae382008f207f2114ee4222dcb" +dependencies = [ + "async-std", + "futures", + "libp2p-core 0.32.1", + "log", +] + +[[package]] +name = "libp2p-wasm-ext" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f066f2b8b1a1d64793f05da2256e6842ecd0293d6735ca2e9bda89831a1bdc06" +dependencies = [ + "futures", + "js-sys", + "libp2p-core 0.33.0", + "parity-send-wrapper", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "libp2p-websocket" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39d398fbb29f432c4128fabdaac2ed155c3bcaf1b9bd40eeeb10a471eefacbf5" +dependencies = [ + "either", + "futures", + "futures-rustls", + "libp2p-core 0.33.0", + "log", + "parking_lot 0.12.1", + "quicksink", + "rw-stream-sink 0.3.0", + "soketto", + "url", + "webpki-roots 0.22.6", +] + +[[package]] +name = "libp2p-yamux" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fe653639ad74877c759720febb0cbcbf4caa221adde4eed2d3126ce5c6f381f" +dependencies = [ + "futures", + "libp2p-core 0.33.0", + "parking_lot 0.12.1", + "thiserror", + "yamux", +] + +[[package]] +name = "librocksdb-sys" +version = "0.11.0+8.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3386f101bcb4bd252d8e9d2fb41ec3b0862a15a62b478c355b2982efa469e3e" +dependencies = [ + "bindgen", + "bzip2-sys", + "cc", + "glob", + "libc", + "libz-sys", + "tikv-jemalloc-sys", +] + +[[package]] +name = "libsecp256k1" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b09eff1b35ed3b33b877ced3a691fc7a481919c7e29c53c906226fcf55e2a1" +dependencies = [ + "arrayref", + "base64 0.13.1", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand 0.8.5", + "serde", + "sha2 0.9.9", + "typenum", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libz-sys" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "lighthouse_metrics" +version = "0.2.0" +dependencies = [ + "lazy_static", + "prometheus", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +dependencies = [ + "value-bag", +] + +[[package]] +name = "log_entry_sync" +version = "0.1.0" +dependencies = [ + "anyhow", + "append_merkle", + "async-trait", + "contract-interface", + "ethereum-types 0.14.1", + "ethers", + "futures", + "futures-core", + "futures-util", + "jsonrpsee", + "serde_json", + "shared_types", + "storage", + "task_executor", + "thiserror", + "tokio", +] + +[[package]] +name = "lru" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a" +dependencies = [ + "hashbrown 0.12.3", +] + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "md-5" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "merkle_light" +version = "0.4.0" +dependencies = [ + "rayon", +] + +[[package]] +name = "merkle_tree" +version = "0.1.0" +dependencies = [ + "hex", + "merkle_light", + "tiny-keccak", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miner" +version = "0.1.0" +dependencies = [ + "async-trait", + "blake2", + "contract-interface", + "ethereum-types 0.14.1", + "ethers", + "lazy_static", + "network", + "rand 0.8.5", + "shared_types", + "storage", + "task_executor", + "tiny-keccak", + "tokio", + "tracing", + "zgs_seal", + "zgs_spec", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "multiaddr" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ee4ea82141951ac6379f964f71b20876d43712bea8faf6dd1a375e08a46499" +dependencies = [ + "arrayref", + "bs58", + "byteorder", + "data-encoding", + "multihash 0.14.0", + "percent-encoding", + "serde", + "static_assertions", + "unsigned-varint", + "url", +] + +[[package]] +name = "multiaddr" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c580bfdd8803cce319b047d239559a22f809094aaea4ac13902a1fdcfcd4261" +dependencies = [ + "arrayref", + "bs58", + "byteorder", + "data-encoding", + "multihash 0.16.3", + "percent-encoding", + "serde", + "static_assertions", + "unsigned-varint", + "url", +] + +[[package]] +name = "multihash" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "752a61cd890ff691b4411423d23816d5866dd5621e4d1c5687a53b94b5a979d8" +dependencies = [ + "digest 0.9.0", + "generic-array", + "multihash-derive 0.7.2", + "sha2 0.9.9", + "unsigned-varint", +] + +[[package]] +name = "multihash" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c346cf9999c631f002d8f977c4eaeaa0e6386f16007202308d0b3757522c2cc" +dependencies = [ + "core2", + "digest 0.10.7", + "multihash-derive 0.8.1", + "sha2 0.10.7", + "unsigned-varint", +] + +[[package]] +name = "multihash-derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "424f6e86263cd5294cbd7f1e95746b95aca0e0d66bff31e5a40d6baa87b4aa99" +dependencies = [ + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure", +] + +[[package]] +name = "multihash-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6d4752e6230d8ef7adf7bd5d8c4b1f6561c1014c5ba9a37445ccefe18aa1db" +dependencies = [ + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure", +] + +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + +[[package]] +name = "multistream-select" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56a336acba8bc87c8876f6425407dbbe6c417bf478b22015f8fb0994ef3bc0ab" +dependencies = [ + "bytes", + "futures", + "log", + "pin-project 1.1.3", + "smallvec", + "unsigned-varint", +] + +[[package]] +name = "multistream-select" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "363a84be6453a70e63513660f4894ef815daf88e3356bffcda9ca27d810ce83b" +dependencies = [ + "bytes", + "futures", + "log", + "pin-project 1.1.3", + "smallvec", + "unsigned-varint", +] + +[[package]] +name = "netlink-packet-core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345b8ab5bd4e71a2986663e88c56856699d060e78e152e6e9d7966fcd5491297" +dependencies = [ + "anyhow", + "byteorder", + "libc", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9ea4302b9759a7a88242299225ea3688e63c85ea136371bb6cf94fd674efaab" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "byteorder", + "libc", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-utils" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" +dependencies = [ + "anyhow", + "byteorder", + "paste", + "thiserror", +] + +[[package]] +name = "netlink-proto" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65b4b14489ab424703c092062176d52ba55485a89c076b4f9db05092b7223aa6" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror", + "tokio", +] + +[[package]] +name = "netlink-sys" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6471bf08e7ac0135876a9581bf3217ef0333c191c128d34878079f42ee150411" +dependencies = [ + "async-io", + "bytes", + "futures", + "libc", + "log", +] + +[[package]] +name = "network" +version = "0.2.0" +dependencies = [ + "directory", + "dirs", + "discv5", + "error-chain", + "eth2_ssz", + "eth2_ssz_derive", + "eth2_ssz_types", + "ethereum-types 0.14.1", + "exit-future", + "fnv", + "futures", + "hashset_delay", + "hex", + "if-addrs 0.10.1", + "igd", + "lazy_static", + "libp2p", + "lighthouse_metrics", + "lru", + "parking_lot 0.12.1", + "prometheus-client", + "rand 0.8.5", + "regex", + "serde", + "serde_derive", + "sha2 0.10.7", + "shared_types", + "slog", + "smallvec", + "snap", + "strum 0.24.1", + "task_executor", + "tempfile", + "tiny-keccak", + "tokio", + "tokio-io-timeout", + "tokio-util 0.6.10", + "tracing", + "tracing-test", + "unsigned-varint", + "unused_port", + "void", + "zgs_version", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + +[[package]] +name = "nix" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "static_assertions", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.2", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "object" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "open-fastrlp" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "786393f80485445794f6043fd3138854dd109cc6c4bd1a6383db304c9ce9b9ce" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", + "ethereum-types 0.14.1", + "open-fastrlp-derive", +] + +[[package]] +name = "open-fastrlp-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "003b2be5c6c53c1cfeb0a238b8a1c3915cd410feb684457a36c10038f764bb1c" +dependencies = [ + "bytes", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown 0.12.3", +] + +[[package]] +name = "os_str_bytes" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "owning_ref" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff55baddef9e4ad00f88b6c743a2a8062d4c6ade126c2a528644b8e444d52ce" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "packed_simd_2" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1914cd452d8fccd6f9db48147b29fd4ae05bea9dc5d9ad578509f72415de282" +dependencies = [ + "cfg-if", + "libm", +] + +[[package]] +name = "parity-scale-codec" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373b1a4c1338d9cd3d1fa53b3a11bdab5ab6bd80a20f7f7becd76953ae2be909" +dependencies = [ + "arrayvec", + "bitvec 0.20.4", + "byte-slice-cast", + "impl-trait-for-tuples", + "parity-scale-codec-derive 2.3.1", + "serde", +] + +[[package]] +name = "parity-scale-codec" +version = "3.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8e946cc0cc711189c0b0249fb8b599cbeeab9784d83c415719368bb8d4ac64" +dependencies = [ + "arrayvec", + "bitvec 1.0.1", + "byte-slice-cast", + "impl-trait-for-tuples", + "parity-scale-codec-derive 3.6.4", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1557010476e0595c9b568d16dcfb81b93cdeb157612726f5170d31aa707bed27" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a296c3079b5fefbc499e1de58dc26c09b1b9a5952d26694ee89f04a43ebbb3e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "parity-send-wrapper" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9777aa91b8ad9dd5aaa04a9b6bcb02c7f1deb952fca5a66034d5e63afc5c6f" + +[[package]] +name = "parking" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.8", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.3.5", + "smallvec", + "windows-targets 0.48.2", +] + +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "path-slash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42" + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", + "password-hash", + "sha2 0.10.7", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pest" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1acb4a4365a13f749a93f1a094a7805e5cfa0955373a9de860d962eaa3a5fe5a" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666d00490d4ac815001da55838c500eafb0320019bbaa44444137c48b443a853" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ca01446f50dbda87c1786af8770d535423fa8a53aec03b8f4e3d7eb10e0929" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "pest_meta" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56af0a30af74d0445c0bf6d9d051c979b516a1a5af790d251daee76005420a48" +dependencies = [ + "once_cell", + "pest", + "sha2 0.10.7", +] + +[[package]] +name = "petgraph" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" +dependencies = [ + "fixedbitset", + "indexmap 1.9.3", +] + +[[package]] +name = "pharos" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" +dependencies = [ + "futures", + "rustc_version", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef0f924a5ee7ea9cbcea77529dba45f8a9ba9f622419fe3386ca581a3ae9d5a" +dependencies = [ + "pin-project-internal 0.4.30", +] + +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal 1.1.3", +] + +[[package]] +name = "pin-project-internal" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "851c8d0ce9bebe43790dedfc86614c23494ac9f423dd618d3a61fc693eafe61e" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" + +[[package]] +name = "pin-project-lite" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der 0.6.1", + "spki 0.6.0", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.8", + "spki 0.7.2", +] + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "platforms" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630" + +[[package]] +name = "plotters" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" + +[[package]] +name = "plotters-svg" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite 0.2.12", + "windows-sys 0.48.0", +] + +[[package]] +name = "poly1305" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" +dependencies = [ + "cpufeatures 0.2.9", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.9", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62" +dependencies = [ + "proc-macro2", + "syn 2.0.28", +] + +[[package]] +name = "primitive-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05e4722c697a58a99d5d06a08c30821d7c082a4632198de1eaa5a6c22ef42373" +dependencies = [ + "fixed-hash 0.7.0", + "impl-codec 0.5.1", + "impl-rlp", + "impl-serde 0.3.2", + "uint", +] + +[[package]] +name = "primitive-types" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f3486ccba82358b11a77516035647c34ba167dfa53312630de83b12bd4f3d66" +dependencies = [ + "fixed-hash 0.8.0", + "impl-codec 0.6.0", + "impl-rlp", + "impl-serde 0.4.0", + "scale-info", + "uint", +] + +[[package]] +name = "priority-queue" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff39edfcaec0d64e8d0da38564fad195d2d51b680940295fcc307366e101e61" +dependencies = [ + "autocfg", + "indexmap 1.9.3", +] + +[[package]] +name = "proc-macro-crate" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" +dependencies = [ + "thiserror", + "toml 0.5.11", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prometheus" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "449811d15fbdf5ceb5c1144416066429cf82316e2ec8ce0c1f6f8a02e7bbcf8c" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "memchr", + "parking_lot 0.12.1", + "protobuf", + "thiserror", +] + +[[package]] +name = "prometheus-client" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1abe0255c04d15f571427a2d1e00099016506cf3297b53853acd2b7eb87825" +dependencies = [ + "dtoa", + "itoa", + "owning_ref", + "prometheus-client-derive-text-encode", +] + +[[package]] +name = "prometheus-client-derive-text-encode" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e12d01b9d66ad9eb4529c57666b6263fc1993cb30261d83ead658fdd932652" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "prost" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" +dependencies = [ + "bytes", + "prost-derive 0.9.0", +] + +[[package]] +name = "prost" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71adf41db68aa0daaefc69bb30bcd68ded9b9abaad5d1fbb6304c4fb390e083e" +dependencies = [ + "bytes", + "prost-derive 0.10.1", +] + +[[package]] +name = "prost-build" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" +dependencies = [ + "bytes", + "heck 0.3.3", + "itertools", + "lazy_static", + "log", + "multimap", + "petgraph", + "prost 0.9.0", + "prost-types 0.9.0", + "regex", + "tempfile", + "which", +] + +[[package]] +name = "prost-build" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae5a4388762d5815a9fc0dea33c56b021cdc8dde0c55e0c9ca57197254b0cab" +dependencies = [ + "bytes", + "cfg-if", + "cmake", + "heck 0.4.1", + "itertools", + "lazy_static", + "log", + "multimap", + "petgraph", + "prost 0.10.4", + "prost-types 0.10.1", + "regex", + "tempfile", + "which", +] + +[[package]] +name = "prost-codec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00af1e92c33b4813cc79fda3f2dbf56af5169709be0202df730e9ebc3e4cd007" +dependencies = [ + "asynchronous-codec", + "bytes", + "prost 0.10.4", + "thiserror", + "unsigned-varint", +] + +[[package]] +name = "prost-derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "prost-derive" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b670f45da57fb8542ebdbb6105a925fe571b67f9e7ed9f47a06a84e72b4e7cc" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "prost-types" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" +dependencies = [ + "bytes", + "prost 0.9.0", +] + +[[package]] +name = "prost-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d0a014229361011dc8e69c8a1ec6c2e8d0f2af7c91e3ea3f5b2170298461e68" +dependencies = [ + "bytes", + "prost 0.10.4", +] + +[[package]] +name = "protobuf" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quicksink" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77de3c815e5a160b1539c6592796801df2043ae35e123b46d73380cfa57af858" +dependencies = [ + "futures-core", + "futures-sink", + "pin-project-lite 0.1.12", +] + +[[package]] +name = "quote" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def50a86306165861203e7f84ecffbbdfdea79f0e51039b33de1e952358c47ac" + +[[package]] +name = "radium" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.10", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom 0.2.10", + "redox_syscall 0.2.16", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.3.6", + "regex-syntax 0.7.4", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.4", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "reqwest" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +dependencies = [ + "base64 0.21.2", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls 0.24.1", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite 0.2.12", + "rustls 0.21.6", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-rustls 0.24.1", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 0.22.6", + "winreg 0.10.1", +] + +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", +] + +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint 0.4.9", + "hmac 0.12.1", + "zeroize", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes", + "rlp-derive", + "rustc-hex", +] + +[[package]] +name = "rlp-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e33d7b2abe0c340d8797fe2907d3f20d3b5ea5908683618bfe80df7f621f672a" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rocksdb" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6f170a4041d50a0ce04b0d2e14916d6ca863ea2e422689a5b694395d299ffe" +dependencies = [ + "libc", + "librocksdb-sys", +] + +[[package]] +name = "ron" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +dependencies = [ + "base64 0.13.1", + "bitflags 1.3.2", + "serde", +] + +[[package]] +name = "router" +version = "0.1.0" +dependencies = [ + "channel", + "chrono", + "error-chain", + "file_location_cache", + "futures", + "lazy_static", + "miner", + "network", + "rand 0.8.5", + "shared_types", + "storage", + "storage-async", + "sync", + "task_executor", + "tokio", + "tracing", +] + +[[package]] +name = "rpc" +version = "0.1.0" +dependencies = [ + "append_merkle", + "base64 0.13.1", + "chunk_pool", + "futures", + "futures-channel", + "jsonrpsee", + "merkle_light", + "merkle_tree", + "miner", + "network", + "serde", + "serde_json", + "shared_types", + "storage", + "storage-async", + "sync", + "task_executor", + "tokio", + "tracing", +] + +[[package]] +name = "rtnetlink" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322c53fd76a18698f1c27381d58091de3a043d356aa5bd0d510608b565f469a0" +dependencies = [ + "async-global-executor", + "futures", + "log", + "netlink-packet-route", + "netlink-proto", + "nix 0.24.3", + "thiserror", +] + +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rust_decimal" +version = "1.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a2ab0025103a60ecaaf3abf24db1db240a4e1c15837090d2c32f625ac98abea" +dependencies = [ + "arrayvec", + "num-traits", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.37.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys 0.4.5", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustls" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls" +version = "0.21.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1feddffcfcc0b33f5c6ce9a29e341e4cd59c3f78e7ee45f4a40c038b1d6cbb" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.3", + "sct", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.2", +] + +[[package]] +name = "rustls-webpki" +version = "0.100.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261e9e0888cba427c3316e6322805653c9425240b6fd96cee7cb671ab70ab8d0" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "rw-stream-sink" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4da5fcb054c46f5a5dff833b129285a93d3f0179531735e6c866e8cc307d2020" +dependencies = [ + "futures", + "pin-project 0.4.30", + "static_assertions", +] + +[[package]] +name = "rw-stream-sink" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26338f5e09bb721b85b135ea05af7767c90b52f6de4f087d4f4a3a9d64e7dc04" +dependencies = [ + "futures", + "pin-project 1.1.3", + "static_assertions", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scale-info" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0a159d0c45c12b20c5a844feb1fe4bea86e28f17b92a5f0c42193634d3782" +dependencies = [ + "cfg-if", + "derive_more", + "parity-scale-codec 3.6.4", + "scale-info-derive", +] + +[[package]] +name = "scale-info-derive" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "912e55f6d20e0e80d63733872b40e1227c0bce1e1ab81ba67d696339bfd7fd29" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scrypt" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f9e24d2b632954ded8ab2ef9fea0a0c769ea56ea98bddbafbad22caeeadf45d" +dependencies = [ + "hmac 0.12.1", + "pbkdf2 0.11.0", + "salsa20", + "sha2 0.10.7", +] + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct 0.1.1", + "der 0.6.1", + "generic-array", + "pkcs8 0.9.0", + "subtle", + "zeroize", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct 0.2.0", + "der 0.7.8", + "generic-array", + "pkcs8 0.10.2", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +dependencies = [ + "serde", +] + +[[package]] +name = "send_wrapper" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + +[[package]] +name = "serde" +version = "1.0.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "serde_json" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha-1" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures 0.2.9", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.9", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures 0.2.9", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.9", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shared_types" +version = "0.1.0" +dependencies = [ + "anyhow", + "append_merkle", + "chrono", + "eth2_ssz", + "eth2_ssz_derive", + "eth2_ssz_types", + "ethereum-types 0.14.1", + "merkle_light", + "merkle_tree", + "serde", + "tiny-keccak", + "tracing", + "typenum", + "zgs_spec", +] + +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slog" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "snap" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e9f0ab6ef7eb7353d9119c170a436d1bf248eea575ac42d19d12f4e34130831" + +[[package]] +name = "snow" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ccba027ba85743e09d15c03296797cad56395089b832b48b5a5217880f57733" +dependencies = [ + "aes-gcm", + "blake2", + "chacha20poly1305", + "curve25519-dalek 4.0.0-rc.1", + "rand_core 0.6.4", + "ring", + "rustc_version", + "sha2 0.10.7", + "subtle", +] + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "soketto" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d1c5305e39e09653383c2c7244f2f78b3bcae37cf50c64cb4789c9f5096ec2" +dependencies = [ + "base64 0.13.1", + "bytes", + "flate2", + "futures", + "httparse", + "log", + "rand 0.8.5", + "sha-1", +] + +[[package]] +name = "solang-parser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c792fe9fae2a2f716846f214ca10d5a1e21133e0bf36cef34bcc4a852467b21" +dependencies = [ + "itertools", + "lalrpop", + "lalrpop-util", + "phf", + "thiserror", + "unicode-xid", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der 0.6.1", +] + +[[package]] +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der 0.7.8", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "storage" +version = "0.1.0" +dependencies = [ + "anyhow", + "append_merkle", + "bitmaps", + "criterion", + "eth2_ssz", + "eth2_ssz_derive", + "ethereum-types 0.14.1", + "hex", + "hex-literal", + "itertools", + "kvdb", + "kvdb-memorydb", + "kvdb-rocksdb", + "merkle_light", + "merkle_tree", + "rand 0.8.5", + "rayon", + "shared_types", + "static_assertions", + "tempdir", + "tiny-keccak", + "tracing", + "typenum", + "zgs_seal", + "zgs_spec", +] + +[[package]] +name = "storage-async" +version = "0.1.0" +dependencies = [ + "anyhow", + "shared_types", + "storage", + "task_executor", + "tokio", + "tracing", +] + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot 0.12.1", + "phf_shared 0.10.0", + "precomputed-hash", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +dependencies = [ + "strum_macros 0.24.3", +] + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros 0.25.2", +] + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", +] + +[[package]] +name = "strum_macros" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.28", +] + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "svm-rs" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a04fc4f5cd35c700153b233f5575ccb3237e0f941fa5049d9e98254d10bf2fe" +dependencies = [ + "fs2", + "hex", + "home", + "once_cell", + "reqwest", + "semver", + "serde", + "serde_json", + "sha2 0.10.7", + "thiserror", + "url", + "zip", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync" +version = "0.1.0" +dependencies = [ + "anyhow", + "append_merkle", + "channel", + "duration-str", + "eth2_ssz", + "file_location_cache", + "libp2p", + "log_entry_sync", + "merkle_light", + "network", + "rand 0.8.5", + "serde", + "shared_types", + "storage", + "storage-async", + "task_executor", + "tokio", + "tracing", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target_info" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c63f48baada5c52e65a29eef93ab4f8982681b67f9e8d29c7b05abcfec2b9ffe" + +[[package]] +name = "task_executor" +version = "0.1.0" +dependencies = [ + "exit-future", + "futures", + "lazy_static", + "lighthouse_metrics", + "tokio", + "tracing", +] + +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand 0.4.6", + "remove_dir_all", +] + +[[package]] +name = "tempfile" +version = "3.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc02fddf48964c42031a0b3fe0428320ecf3a73c401040fc0096f97794310651" +dependencies = [ + "cfg-if", + "fastrand 2.0.0", + "redox_syscall 0.3.5", + "rustix 0.38.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + +[[package]] +name = "thiserror" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9207952ae1a003f42d3d5e892dac3c6ba42aa6ac0c79a6a91a2b5cb4253e75c" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1728216d3244de4f14f14f8c15c79be1a7c67867d28d69b719690e2a19fb445" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tikv-jemalloc-sys" +version = "0.5.4+5.3.0-patched" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9402443cb8fd499b6f327e40565234ff34dbda27460c5b47db0db77443dd85d1" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" +dependencies = [ + "deranged", + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" +dependencies = [ + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40de3a2ba249dcb097e01be5e67a5ff53cf250397715a071a81543e8a832a920" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot 0.12.1", + "pin-project-lite 0.2.12", + "signal-hook-registry", + "socket2 0.5.3", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite 0.2.12", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls 0.20.8", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.6", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite 0.2.12", + "tokio", + "tokio-util 0.7.8", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec509ac96e9a0c43427c74f003127d953a265737636129424288d27cb5c4b12c" +dependencies = [ + "futures-util", + "log", + "rustls 0.21.6", + "tokio", + "tokio-rustls 0.24.1", + "tungstenite", + "webpki-roots 0.23.1", +] + +[[package]] +name = "tokio-util" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "log", + "pin-project-lite 0.2.12", + "slab", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite 0.2.12", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +dependencies = [ + "indexmap 2.0.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite 0.2.12", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e" +dependencies = [ + "crossbeam-channel", + "time 0.3.25", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project 1.1.3", + "tracing", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-test" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a2c0ff408fe918a94c428a3f2ad04e4afd5c95bbc08fcf868eff750c15728a4" +dependencies = [ + "lazy_static", + "tracing-core", + "tracing-subscriber", + "tracing-test-macro", +] + +[[package]] +name = "tracing-test-macro" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258bc1c4f8e2e73a977812ab339d503e6feeb92700f6d07a6de4d321522d5c08" +dependencies = [ + "lazy_static", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tree_hash" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9c8a86fad3169a65aad2265d3c6a8bc119d0b771046af3c1b2fb0e9b12182b" +dependencies = [ + "eth2_hashing", + "ethereum-types 0.12.1", + "smallvec", +] + +[[package]] +name = "trust-dns-proto" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c31f240f59877c3d4bb3b3ea0ec5a6a0cff07323580ff8c7a605cd7d08b255d" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.2.3", + "ipnet", + "lazy_static", + "log", + "rand 0.8.5", + "smallvec", + "thiserror", + "tinyvec", + "tokio", + "url", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ba72c2ea84515690c9fcef4c6c660bb9df3036ed1051686de84605b74fd558" +dependencies = [ + "cfg-if", + "futures-util", + "ipconfig", + "lazy_static", + "log", + "lru-cache", + "parking_lot 0.12.1", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "trust-dns-proto", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "tungstenite" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15fba1a6d6bb030745759a9a2a588bfe8490fc8b4751a277db3a0be1c9ebbf67" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.8.5", + "rustls 0.21.6", + "sha1", + "thiserror", + "url", + "utf-8", + "webpki", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "universal-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "unsigned-varint" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86a8dc7f45e4c1b0d30e43038c38f274e77af056aa5f74b93c2cf9eb3c1c836" +dependencies = [ + "asynchronous-codec", + "bytes", + "futures-io", + "futures-util", + "tokio-util 0.6.10", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "unused_port" +version = "0.1.0" + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna 0.4.0", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.10", + "serde", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "value-bag" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] +name = "walkdir" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.28", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "wasm-timer" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.11.2", + "pin-utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + +[[package]] +name = "webpki-roots" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" +dependencies = [ + "rustls-webpki 0.100.1", +] + +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + +[[package]] +name = "widestring" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" + +[[package]] +name = "wildmatch" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f44b95f62d34113cf558c93511ac93027e03e9c29a60dd0fd70e6e025c7270a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45296b64204227616fdbf2614cefa4c236b98ee64dfaaaa435207ed99fe7829f" +dependencies = [ + "windows_aarch64_msvc 0.34.0", + "windows_i686_gnu 0.34.0", + "windows_i686_msvc 0.34.0", + "windows_x86_64_gnu 0.34.0", + "windows_x86_64_msvc 0.34.0", +] + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.2", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.2", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1eeca1c172a285ee6c2c84c341ccea837e7c01b12fbb2d0fe3c9e550ce49ec8" +dependencies = [ + "windows_aarch64_gnullvm 0.48.2", + "windows_aarch64_msvc 0.48.2", + "windows_i686_gnu 0.48.2", + "windows_i686_msvc 0.48.2", + "windows_x86_64_gnu 0.48.2", + "windows_x86_64_gnullvm 0.48.2", + "windows_x86_64_msvc 0.48.2", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b10d0c968ba7f6166195e13d593af609ec2e3d24f916f081690695cf5eaffb2f" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "571d8d4e62f26d4932099a9efe89660e8bd5087775a2ab5cdd8b747b811f1058" + +[[package]] +name = "windows_i686_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2229ad223e178db5fbbc8bd8d3835e51e566b8474bfca58d2e6150c48bb723cd" + +[[package]] +name = "windows_i686_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "600956e2d840c194eedfc5d18f8242bc2e17c7775b6684488af3a9fff6fe3287" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea99ff3f8b49fb7a8e0d305e5aec485bd068c2ba691b6e277d29eaeac945868a" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1a05a1ece9a7a0d5a7ccf30ba2c33e3a61a30e042ffd247567d1de1d94120d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d419259aba16b663966e29e6d7c6ecfa0bb8425818bb96f6f1f3c3eb71a6e7b9" + +[[package]] +name = "winnow" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e461589e194280efaa97236b73623445efa195aa633fd7004f39805707a9d53" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "ws_stream_wasm" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7999f5f4217fe3818726b66257a4475f71e74ffd190776ad053fa159e50737f5" +dependencies = [ + "async_io_stream", + "futures", + "js-sys", + "log", + "pharos", + "rustc_version", + "send_wrapper 0.6.0", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x25519-dalek" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f" +dependencies = [ + "curve25519-dalek 3.2.0", + "rand_core 0.5.1", + "zeroize", +] + +[[package]] +name = "xml-rs" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47430998a7b5d499ccee752b41567bc3afc57e1327dc855b1a2aa44ce29b5fa1" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yamux" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d9ba232399af1783a58d8eb26f6b5006fbefe2dc9ef36bd283324792d03ea5" +dependencies = [ + "futures", + "log", + "nohash-hasher", + "parking_lot 0.12.1", + "rand 0.8.5", + "static_assertions", +] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "zgs_node" +version = "0.1.0" +dependencies = [ + "anyhow", + "chunk_pool", + "clap", + "config", + "ctrlc", + "duration-str", + "error-chain", + "ethereum-types 0.14.1", + "exit-future", + "file_location_cache", + "futures", + "itertools", + "libp2p", + "log_entry_sync", + "miner", + "network", + "router", + "rpc", + "serde", + "shared_types", + "storage", + "storage-async", + "sync", + "task_executor", + "tokio", + "tokio-stream", + "toml 0.5.11", + "tracing", + "tracing-appender", + "tracing-subscriber", + "zgs_version", +] + +[[package]] +name = "zgs_seal" +version = "0.1.0" +dependencies = [ + "ethereum-types 0.14.1", + "tiny-keccak", + "zgs_spec", +] + +[[package]] +name = "zgs_spec" +version = "0.1.0" + +[[package]] +name = "zgs_version" +version = "0.1.0" +dependencies = [ + "git-version", + "regex", + "target_info", +] + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes 0.8.3", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac 0.12.1", + "pbkdf2 0.11.0", + "sha1", + "time 0.3.25", + "zstd", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.8+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +dependencies = [ + "cc", + "libc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9d15452 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,32 @@ +[workspace] + +members = [ + "common/channel", + "common/directory", + "common/hashset_delay", + "common/lighthouse_metrics", + "common/merkle_tree", + "common/task_executor", + "common/zgs_version", + "common/unused_port", + "common/append_merkle", + + "node", + "node/chunk_pool", + "node/file_location_cache", + "node/log_entry_sync", + "node/miner", + "node/network", + "node/router", + "node/rpc", + "node/shared_types", + "node/storage", + "node/storage-async", + "node/sync", +] +resolver = "2" + +[patch.crates-io] +discv5 = { path = "version-meld/discv5" } +eth2_ssz = { path = "version-meld/eth2_ssz" } +enr = { path = "version-meld/enr" } \ No newline at end of file diff --git a/LICENSE.lighthouse b/LICENSE.lighthouse new file mode 100644 index 0000000..98016b5 --- /dev/null +++ b/LICENSE.lighthouse @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018 Sigma Prime Pty Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..15ed7b8 --- /dev/null +++ b/NOTICE @@ -0,0 +1,6 @@ +ZeroGStorage +Copyright 2023 ZeroGStorage + +The Initial Developer of some parts of the framework, which are copied from, derived from, or +inspired by Lighthouse, is Sigma Prime Pty Ltd (https://sigmaprime.io). +Copyright 2018 - 2022 Sigma Prime Pty Ltd. \ No newline at end of file diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..a793f59 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,8 @@ +coverage: + status: + project: + default: + informational: true + patch: + default: + informational: true \ No newline at end of file diff --git a/common/append_merkle/Cargo.toml b/common/append_merkle/Cargo.toml new file mode 100644 index 0000000..37c0b3d --- /dev/null +++ b/common/append_merkle/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "append_merkle" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { version = "=1.0.58", features = ["backtrace"] } +ethereum-types = "0.14" +tiny-keccak = { version = "2.0.2", features = ["keccak"] } +eth2_ssz = "0.4.0" +eth2_ssz_derive = "0.3.0" +serde = { version = "1.0.137", features = ["derive"] } +lazy_static = "1.4.0" +tracing = "0.1.36" \ No newline at end of file diff --git a/common/append_merkle/src/lib.rs b/common/append_merkle/src/lib.rs new file mode 100644 index 0000000..72743fb --- /dev/null +++ b/common/append_merkle/src/lib.rs @@ -0,0 +1,641 @@ +mod merkle_tree; +mod proof; +mod sha3; + +use anyhow::{anyhow, bail, Result}; +use std::cmp::Ordering; +use std::collections::HashMap; +use std::fmt::Debug; +use std::marker::PhantomData; +use tracing::warn; + +pub use crate::merkle_tree::{Algorithm, HashElement, MerkleTreeInitialData, MerkleTreeRead}; +pub use proof::{Proof, RangeProof}; +pub use sha3::Sha3Algorithm; + +pub struct AppendMerkleTree> { + /// Keep all the nodes in the latest version. `layers[0]` is the layer of leaves. + layers: Vec>, + /// Keep the delta nodes that can be used to construct a history tree. + /// The key is the root node of that version. + delta_nodes_map: HashMap>, + root_to_tx_seq_map: HashMap, + + /// For `last_chunk_merkle` after the first chunk, this is set to `Some(10)` so that + /// `revert_to` can reset the state correctly when needed. + min_depth: Option, + /// Used to compute the correct padding hash. + /// 0 for `pora_chunk_merkle` and 10 for not-first `last_chunk_merkle`. + leaf_height: usize, + _a: PhantomData, +} + +impl> AppendMerkleTree { + pub fn new(leaves: Vec, leaf_height: usize, start_tx_seq: Option) -> Self { + let mut merkle = Self { + layers: vec![leaves], + delta_nodes_map: HashMap::new(), + root_to_tx_seq_map: HashMap::new(), + min_depth: None, + leaf_height, + _a: Default::default(), + }; + if merkle.leaves() == 0 { + if let Some(seq) = start_tx_seq { + merkle.delta_nodes_map.insert( + seq, + DeltaNodes { + right_most_nodes: vec![], + }, + ); + } + return merkle; + } + // Reconstruct the whole tree. + merkle.recompute(0, 0, None); + // Commit the first version in memory. + // TODO(zz): Check when the roots become available. + merkle.commit(start_tx_seq); + merkle + } + + pub fn new_with_subtrees( + initial_data: MerkleTreeInitialData, + leaf_height: usize, + start_tx_seq: Option, + ) -> Result { + let mut merkle = Self { + layers: vec![vec![]], + delta_nodes_map: HashMap::new(), + root_to_tx_seq_map: HashMap::new(), + min_depth: None, + leaf_height, + _a: Default::default(), + }; + if initial_data.subtree_list.is_empty() { + if let Some(seq) = start_tx_seq { + merkle.delta_nodes_map.insert( + seq, + DeltaNodes { + right_most_nodes: vec![], + }, + ); + } + return Ok(merkle); + } + merkle.append_subtree_list(initial_data.subtree_list)?; + merkle.commit(start_tx_seq); + for (index, h) in initial_data.known_leaves { + merkle.fill_leaf(index, h); + } + Ok(merkle) + } + + /// This is only used for the last chunk, so `leaf_height` is always 0 so far. + pub fn new_with_depth(leaves: Vec, depth: usize, start_tx_seq: Option) -> Self { + if leaves.is_empty() { + // Create an empty merkle tree with `depth`. + let mut merkle = Self { + layers: vec![vec![]; depth], + delta_nodes_map: HashMap::new(), + root_to_tx_seq_map: HashMap::new(), + min_depth: Some(depth), + leaf_height: 0, + _a: Default::default(), + }; + if let Some(seq) = start_tx_seq { + merkle.delta_nodes_map.insert( + seq, + DeltaNodes { + right_most_nodes: vec![], + }, + ); + } + merkle + } else { + let mut layers = vec![vec![]; depth]; + layers[0] = leaves; + let mut merkle = Self { + layers, + delta_nodes_map: HashMap::new(), + root_to_tx_seq_map: HashMap::new(), + min_depth: Some(depth), + leaf_height: 0, + _a: Default::default(), + }; + // Reconstruct the whole tree. + merkle.recompute(0, 0, None); + // Commit the first version in memory. + merkle.commit(start_tx_seq); + merkle + } + } + + /// Return the new merkle root. + pub fn append(&mut self, new_leaf: E) { + self.layers[0].push(new_leaf); + self.recompute_after_append_leaves(self.leaves() - 1); + } + + pub fn append_list(&mut self, mut leaf_list: Vec) { + let start_index = self.leaves(); + self.layers[0].append(&mut leaf_list); + self.recompute_after_append_leaves(start_index); + } + + /// Append a leaf list by providing their intermediate node hash. + /// The appended subtree must be aligned. And it's up to the caller to + /// append the padding nodes for alignment. + /// Other nodes in the subtree will be set to `null` nodes. + /// TODO: Optimize to avoid storing the `null` nodes? + pub fn append_subtree(&mut self, subtree_depth: usize, subtree_root: E) -> Result<()> { + let start_index = self.leaves(); + self.append_subtree_inner(subtree_depth, subtree_root)?; + self.recompute_after_append_subtree(start_index, subtree_depth - 1); + Ok(()) + } + + pub fn append_subtree_list(&mut self, subtree_list: Vec<(usize, E)>) -> Result<()> { + for (subtree_depth, subtree_root) in subtree_list { + let start_index = self.leaves(); + self.append_subtree_inner(subtree_depth, subtree_root)?; + self.recompute_after_append_subtree(start_index, subtree_depth - 1); + } + Ok(()) + } + + /// Change the value of the last leaf and return the new merkle root. + /// This is needed if our merkle-tree in memory only keeps intermediate nodes instead of real leaves. + pub fn update_last(&mut self, updated_leaf: E) { + if self.layers[0].is_empty() { + // Special case for the first data. + self.layers[0].push(updated_leaf); + } else { + *self.layers[0].last_mut().unwrap() = updated_leaf; + } + self.recompute_after_append_leaves(self.leaves() - 1); + } + + /// Fill an unknown `null` leaf with its real value. + /// Panics if the leaf changes the merkle root or the index is out of range. + /// TODO: Batch computing intermediate nodes. + pub fn fill_leaf(&mut self, index: usize, leaf: E) { + if self.layers[0][index] == E::null() { + self.layers[0][index] = leaf; + self.recompute_after_fill_leaves(index, index + 1); + } else if self.layers[0][index] != leaf { + panic!("Fill with invalid leaf") + } + } + + pub fn gen_range_proof(&self, start_index: usize, end_index: usize) -> Result> { + if end_index <= start_index { + bail!( + "invalid proof range: start={} end={}", + start_index, + end_index + ); + } + // TODO(zz): Optimize range proof. + let left_proof = self.gen_proof(start_index)?; + let right_proof = self.gen_proof(end_index - 1)?; + Ok(RangeProof { + left_proof, + right_proof, + }) + } + + pub fn check_root(&self, root: &E) -> bool { + self.root_to_tx_seq_map.contains_key(root) + } + + pub fn leaf_at(&self, position: usize) -> Result> { + if position >= self.leaves() { + bail!("Out of bound: position={} end={}", position, self.leaves()); + } + if self.layers[0][position] != E::null() { + Ok(Some(self.layers[0][position].clone())) + } else { + // The leaf hash is unknown. + Ok(None) + } + } + + /// Return a list of subtrees that can be used to rebuild the tree. + pub fn get_subtrees(&self) -> Vec<(usize, E)> { + let mut next_index = 0; + let mut subtree_list: Vec<(usize, E)> = Vec::new(); + while next_index < self.leaves() { + let root_tuple = self.first_known_root_at(next_index); + let subtree_size = 1 << (root_tuple.0 - 1); + let root_start_index = next_index / subtree_size * subtree_size; + + // Previous subtrees are included within the new subtree. + // Pop them out and replace with the new one. + if root_start_index < next_index { + while let Some(last) = subtree_list.pop() { + next_index -= 1 << (last.0 - 1); + if next_index == root_start_index { + break; + } + } + } + next_index += subtree_size; + subtree_list.push(root_tuple); + } + subtree_list + } +} + +impl> AppendMerkleTree { + pub fn commit(&mut self, tx_seq: Option) { + if let Some(tx_seq) = tx_seq { + if self.leaves() == 0 { + // The state is empty, so we just save the root as `null`. + // Note that this root should not be used. + self.delta_nodes_map.insert( + tx_seq, + DeltaNodes { + right_most_nodes: vec![], + }, + ); + return; + } + let mut right_most_nodes = Vec::new(); + for layer in &self.layers { + right_most_nodes.push((layer.len() - 1, layer.last().unwrap().clone())); + } + let root = self.root().clone(); + assert_eq!(root, right_most_nodes.last().unwrap().1); + self.delta_nodes_map + .insert(tx_seq, DeltaNodes::new(right_most_nodes)); + self.root_to_tx_seq_map.insert(root, tx_seq); + } + } + + fn before_extend_layer(&mut self, height: usize) { + if height == self.layers.len() { + self.layers.push(Vec::new()); + } + } + + fn recompute_after_append_leaves(&mut self, start_index: usize) { + self.recompute(start_index, 0, None) + } + + fn recompute_after_append_subtree(&mut self, start_index: usize, height: usize) { + self.recompute(start_index, height, None) + } + + fn recompute_after_fill_leaves(&mut self, start_index: usize, end_index: usize) { + self.recompute(start_index, 0, Some(end_index)) + } + + /// Given a range of changed leaf nodes and recompute the tree. + /// Since this tree is append-only, we always compute to the end. + fn recompute( + &mut self, + mut start_index: usize, + mut height: usize, + mut maybe_end_index: Option, + ) { + start_index >>= height; + maybe_end_index = maybe_end_index.map(|end| end >> height); + // Loop until we compute the new root and reach `tree_depth`. + while self.layers[height].len() > 1 || height < self.layers.len() - 1 { + let next_layer_start_index = start_index >> 1; + if start_index % 2 == 1 { + start_index -= 1; + } + + let mut end_index = maybe_end_index.unwrap_or(self.layers[height].len()); + if end_index % 2 == 1 && end_index != self.layers[height].len() { + end_index += 1; + } + let mut i = 0; + let mut iter = self.layers[height][start_index..end_index].chunks_exact(2); + // We cannot modify the parent layer while iterating the child layer, + // so just keep the changes and update them later. + let mut parent_update = Vec::new(); + while let Some([left, right]) = iter.next() { + // If either left or right is null (unknown), we cannot compute the parent hash. + // Note that if we are recompute a range of an existing tree, + // we do not need to keep these possibly null parent. This is only saved + // for the case of constructing a new tree from the leaves. + let parent = if *left == E::null() || *right == E::null() { + E::null() + } else { + A::parent(left, right) + }; + parent_update.push((next_layer_start_index + i, parent)); + i += 1; + } + if let [r] = iter.remainder() { + // Same as above. + let parent = if *r == E::null() { + E::null() + } else { + A::parent_single(r, height + self.leaf_height) + }; + parent_update.push((next_layer_start_index + i, parent)); + } + if !parent_update.is_empty() { + self.before_extend_layer(height + 1); + } + // `parent_update` is in increasing order by `parent_index`, so + // we can just overwrite `last_changed_parent_index` with new values. + let mut last_changed_parent_index = None; + for (parent_index, parent) in parent_update { + match parent_index.cmp(&self.layers[height + 1].len()) { + Ordering::Less => { + // We do not overwrite with null. + if parent != E::null() { + if self.layers[height + 1][parent_index] == E::null() + // The last node in a layer can be updated. + || (self.layers[height + 1][parent_index] != parent + && parent_index == self.layers[height + 1].len() - 1) + { + self.layers[height + 1][parent_index] = parent; + last_changed_parent_index = Some(parent_index); + } else if self.layers[height + 1][parent_index] != parent { + // Recompute changes a node in the middle. This should be impossible + // if the inputs are valid. + panic!("Invalid append merkle tree! height={} index={} expected={:?} get={:?}", + height + 1, parent_index, self.layers[height + 1][parent_index], parent); + } + } + } + Ordering::Equal => { + self.layers[height + 1].push(parent); + last_changed_parent_index = Some(parent_index); + } + Ordering::Greater => { + unreachable!("depth={}, parent_index={}", height, parent_index); + } + } + } + if last_changed_parent_index.is_none() { + break; + } + maybe_end_index = last_changed_parent_index.map(|i| i + 1); + height += 1; + start_index = next_layer_start_index; + } + } + + fn append_subtree_inner(&mut self, subtree_depth: usize, subtree_root: E) -> Result<()> { + if subtree_depth == 0 { + bail!("Subtree depth should not be zero!"); + } + if self.leaves() % (1 << (subtree_depth - 1)) != 0 { + warn!( + "The current leaves count is not aligned with the merged subtree, \ + this is only possible during recovery, leaves={}", + self.leaves() + ); + } + for height in 0..(subtree_depth - 1) { + self.before_extend_layer(height); + let subtree_layer_size = 1 << (subtree_depth - 1 - height); + self.layers[height].append(&mut vec![E::null(); subtree_layer_size]); + } + self.before_extend_layer(subtree_depth - 1); + self.layers[subtree_depth - 1].push(subtree_root); + Ok(()) + } + + #[cfg(test)] + pub fn validate(&self, proof: &Proof, leaf: &E, position: usize) -> Result { + proof.validate::(leaf, position)?; + Ok(self.root_to_tx_seq_map.contains_key(&proof.root())) + } + + pub fn revert_to(&mut self, tx_seq: u64) -> Result<()> { + if self.layers[0].is_empty() { + // Any previous state of an empty tree is always empty. + return Ok(()); + } + let delta_nodes = self + .delta_nodes_map + .get(&tx_seq) + .ok_or_else(|| anyhow!("tx_seq unavailable, root={:?}", tx_seq))?; + // Dropping the upper layers that are not in the old merkle tree. + self.layers.truncate(delta_nodes.right_most_nodes.len()); + for (height, (last_index, right_most_node)) in + delta_nodes.right_most_nodes.iter().enumerate() + { + self.layers[height].truncate(*last_index + 1); + self.layers[height][*last_index] = right_most_node.clone(); + } + self.clear_after(tx_seq); + Ok(()) + } + + pub fn at_root_version(&self, root_hash: &E) -> Result> { + let tx_seq = self + .root_to_tx_seq_map + .get(root_hash) + .ok_or_else(|| anyhow!("old root unavailable, root={:?}", root_hash))?; + let delta_nodes = self + .delta_nodes_map + .get(tx_seq) + .ok_or_else(|| anyhow!("tx_seq unavailable, tx_seq={:?}", tx_seq))?; + if delta_nodes.height() == 0 { + bail!("empty tree"); + } + Ok(HistoryTree { + layers: &self.layers, + delta_nodes, + leaf_height: self.leaf_height, + }) + } + + pub fn reset(&mut self) { + self.layers = match self.min_depth { + None => vec![vec![]], + Some(depth) => vec![vec![]; depth], + }; + } + + fn clear_after(&mut self, tx_seq: u64) { + let mut tx_seq = tx_seq + 1; + while self.delta_nodes_map.contains_key(&tx_seq) { + if let Some(nodes) = self.delta_nodes_map.remove(&tx_seq) { + if nodes.height() != 0 { + self.root_to_tx_seq_map.remove(nodes.root()); + } + } + tx_seq += 1; + } + } + + /// Return the height and the root hash of the first available node from the leaf to the root. + /// The caller should ensure that `index` is within range. + fn first_known_root_at(&self, index: usize) -> (usize, E) { + let mut height = 0; + let mut index_in_layer = index; + while height < self.layers.len() { + let node = self.node(height, index_in_layer); + if !node.is_null() { + return (height + 1, node.clone()); + } + height += 1; + index_in_layer /= 2; + } + unreachable!("root is always available") + } +} + +#[derive(Clone, Debug)] +struct DeltaNodes { + /// The right most nodes in a layer and its position. + right_most_nodes: Vec<(usize, E)>, +} + +impl DeltaNodes { + fn new(right_most_nodes: Vec<(usize, E)>) -> Self { + Self { right_most_nodes } + } + + fn get(&self, height: usize, position: usize) -> Result> { + if height >= self.right_most_nodes.len() || position > self.right_most_nodes[height].0 { + Err(anyhow!("position out of tree range")) + } else if position == self.right_most_nodes[height].0 { + Ok(Some(&self.right_most_nodes[height].1)) + } else { + Ok(None) + } + } + + fn layer_len(&self, height: usize) -> usize { + self.right_most_nodes[height].0 + 1 + } + + fn height(&self) -> usize { + self.right_most_nodes.len() + } + + fn root(&self) -> &E { + &self.right_most_nodes.last().unwrap().1 + } +} + +pub struct HistoryTree<'m, E: HashElement> { + /// A reference to the global tree nodes. + layers: &'m Vec>, + /// The delta nodes that are difference from `layers`. + /// This could be a reference, we just take ownership for convenience. + delta_nodes: &'m DeltaNodes, + + leaf_height: usize, +} + +impl> MerkleTreeRead for AppendMerkleTree { + type E = E; + + fn node(&self, layer: usize, index: usize) -> &Self::E { + &self.layers[layer][index] + } + + fn height(&self) -> usize { + self.layers.len() + } + + fn layer_len(&self, layer_height: usize) -> usize { + self.layers[layer_height].len() + } + + fn padding_node(&self, height: usize) -> Self::E { + E::end_pad(height + self.leaf_height) + } +} + +impl<'a, E: HashElement> MerkleTreeRead for HistoryTree<'a, E> { + type E = E; + fn node(&self, layer: usize, index: usize) -> &Self::E { + match self.delta_nodes.get(layer, index).expect("range checked") { + Some(node) => node, + None => &self.layers[layer][index], + } + } + + fn height(&self) -> usize { + self.delta_nodes.height() + } + + fn layer_len(&self, layer_height: usize) -> usize { + self.delta_nodes.layer_len(layer_height) + } + + fn padding_node(&self, height: usize) -> Self::E { + E::end_pad(height + self.leaf_height) + } +} + +#[macro_export] +macro_rules! ensure_eq { + ($given:expr, $expected:expr) => { + ensure!( + $given == $expected, + format!( + "equal check fails! {}:{}: {}={:?}, {}={:?}", + file!(), + line!(), + stringify!($given), + $given, + stringify!($expected), + $expected, + ) + ); + }; +} + +#[cfg(test)] +mod tests { + use crate::merkle_tree::MerkleTreeRead; + use crate::sha3::Sha3Algorithm; + use crate::AppendMerkleTree; + use ethereum_types::H256; + + #[test] + fn test_proof() { + let n = [1, 2, 6, 1025]; + for entry_len in n { + let mut data = Vec::new(); + for _ in 0..entry_len { + data.push(H256::random()); + } + let mut merkle = + AppendMerkleTree::::new(vec![H256::zero()], 0, None); + merkle.append_list(data.clone()); + merkle.commit(Some(0)); + verify(&data, &merkle); + + data.push(H256::random()); + merkle.append(*data.last().unwrap()); + merkle.commit(Some(1)); + verify(&data, &merkle); + + for _ in 0..6 { + data.push(H256::random()); + } + merkle.append_list(data[data.len() - 6..].to_vec()); + merkle.commit(Some(2)); + verify(&data, &merkle); + } + } + + fn verify(data: &Vec, merkle: &AppendMerkleTree) { + for i in 0..data.len() { + let proof = merkle.gen_proof(i + 1).unwrap(); + let r = merkle.validate(&proof, &data[i], i + 1); + assert!(matches!(r, Ok(true)), "{:?}", r); + } + for i in (0..data.len()).step_by(6) { + let end = std::cmp::min(i + 3, data.len()); + let range_proof = merkle.gen_range_proof(i + 1, end + 1).unwrap(); + let r = range_proof.validate::(&data[i..end], i + 1); + assert!(r.is_ok(), "{:?}", r); + } + } +} diff --git a/common/append_merkle/src/merkle_tree.rs b/common/append_merkle/src/merkle_tree.rs new file mode 100644 index 0000000..5c822fb --- /dev/null +++ b/common/append_merkle/src/merkle_tree.rs @@ -0,0 +1,131 @@ +use crate::sha3::Sha3Algorithm; +use crate::Proof; +use anyhow::{bail, Result}; +use ethereum_types::H256; +use lazy_static::lazy_static; +use ssz::{Decode, Encode}; +use std::fmt::Debug; +use std::hash::Hash; +use tracing::trace; + +pub trait HashElement: + Clone + Debug + Eq + Hash + AsRef<[u8]> + AsMut<[u8]> + Decode + Encode + Send + Sync +{ + fn end_pad(height: usize) -> Self; + fn null() -> Self; + fn is_null(&self) -> bool { + self == &Self::null() + } +} + +impl HashElement for H256 { + fn end_pad(height: usize) -> Self { + ZERO_HASHES[height] + } + + fn null() -> Self { + H256::repeat_byte(1) + } +} + +lazy_static! { + static ref ZERO_HASHES: [H256; 64] = { + let leaf_zero_hash: H256 = Sha3Algorithm::leaf(&[0u8; 256]); + let mut list = [H256::zero(); 64]; + list[0] = leaf_zero_hash; + for i in 1..list.len() { + list[i] = Sha3Algorithm::parent(&list[i - 1], &list[i - 1]); + } + list + }; +} + +pub trait Algorithm { + fn parent(left: &E, right: &E) -> E; + fn parent_single(r: &E, height: usize) -> E { + Self::parent(r, &E::end_pad(height)) + } + fn leaf(data: &[u8]) -> E; +} + +pub trait MerkleTreeRead { + type E: HashElement; + fn node(&self, layer: usize, index: usize) -> &Self::E; + fn height(&self) -> usize; + fn layer_len(&self, layer_height: usize) -> usize; + fn padding_node(&self, height: usize) -> Self::E; + + fn leaves(&self) -> usize { + self.layer_len(0) + } + + fn root(&self) -> &Self::E { + self.node(self.height() - 1, 0) + } + + fn gen_proof(&self, leaf_index: usize) -> Result> { + if leaf_index >= self.leaves() { + bail!( + "leaf index out of bound: leaf_index={} total_leaves={}", + leaf_index, + self.leaves() + ); + } + if self.node(0, leaf_index) == &Self::E::null() { + bail!("Not ready to generate proof for leaf_index={}", leaf_index); + } + if self.height() == 1 { + return Ok(Proof::new( + vec![self.root().clone(), self.root().clone()], + vec![], + )); + } + let mut lemma: Vec = Vec::with_capacity(self.height()); // path + root + let mut path: Vec = Vec::with_capacity(self.height() - 2); // path - 1 + let mut index_in_layer = leaf_index; + lemma.push(self.node(0, leaf_index).clone()); + for height in 0..(self.height() - 1) { + trace!( + "gen_proof: height={} index={} hash={:?}", + height, + index_in_layer, + self.node(height, index_in_layer) + ); + if index_in_layer % 2 == 0 { + path.push(true); + if index_in_layer + 1 == self.layer_len(height) { + // TODO: This can be skipped if the tree size is available in validation. + lemma.push(self.padding_node(height)); + } else { + lemma.push(self.node(height, index_in_layer + 1).clone()); + } + } else { + path.push(false); + lemma.push(self.node(height, index_in_layer - 1).clone()); + } + index_in_layer >>= 1; + } + lemma.push(self.root().clone()); + Ok(Proof::new(lemma, path)) + } +} + +/// This includes the data to reconstruct an `AppendMerkleTree` root where some nodes +/// are `null`. Other intermediate nodes will be computed based on these known nodes. +pub struct MerkleTreeInitialData { + /// A list of `(subtree_depth, root)`. + /// The subtrees are continuous so we can compute the tree root with these subtree roots. + pub subtree_list: Vec<(usize, E)>, + /// A list of `(index, leaf_hash)`. + /// These leaves are in some large subtrees of `subtree_list`. 1-node subtrees are also leaves, + /// but they will not be duplicated in `known_leaves`. + pub known_leaves: Vec<(usize, E)>, +} + +impl MerkleTreeInitialData { + pub fn leaves(&self) -> usize { + self.subtree_list.iter().fold(0, |acc, (subtree_depth, _)| { + acc + (1 << (subtree_depth - 1)) + }) + } +} diff --git a/common/append_merkle/src/proof.rs b/common/append_merkle/src/proof.rs new file mode 100644 index 0000000..3c08f5f --- /dev/null +++ b/common/append_merkle/src/proof.rs @@ -0,0 +1,167 @@ +use crate::{ensure_eq, Algorithm, HashElement}; +use anyhow::{bail, ensure, Result}; +use serde::{Deserialize, Serialize}; +use ssz_derive::{Decode as DeriveDecode, Encode as DeriveEncode}; + +#[derive(Clone, Debug, Eq, PartialEq, DeriveEncode, DeriveDecode, Deserialize, Serialize)] +pub struct Proof { + lemma: Vec, + path: Vec, +} + +impl Proof { + /// Creates new MT inclusion proof + pub fn new(hash: Vec, path: Vec) -> Proof { + assert_eq!(hash.len() - 2, path.len()); + Proof { lemma: hash, path } + } + + pub fn new_empty() -> Proof { + Proof { + lemma: vec![], + path: vec![], + } + } + + /// Return proof target leaf + pub fn item(&self) -> T { + self.lemma.first().unwrap().clone() + } + + /// Return tree root + pub fn root(&self) -> T { + self.lemma.last().unwrap().clone() + } + + /// Verifies MT inclusion proof + fn validate_integrity>(&self) -> bool { + let size = self.lemma.len(); + + if size < 2 { + return false; + } + let mut h = self.item(); + + for i in 1..size - 1 { + h = if self.path[i - 1] { + A::parent(&h, &self.lemma[i]) + } else { + A::parent(&self.lemma[i], &h) + }; + } + + h == self.root() + } + + pub fn validate>(&self, item: &T, position: usize) -> Result<()> { + if !self.validate_integrity::() { + bail!("Invalid proof"); + } + if *item != self.item() { + bail!("Proof item unmatch"); + } + if position != self.position() { + bail!("Proof position unmatch"); + } + Ok(()) + } + + /// Returns the path of this proof. + pub fn path(&self) -> &[bool] { + &self.path + } + + /// Returns the lemma of this proof. + pub fn lemma(&self) -> &[T] { + &self.lemma + } + + pub fn position(&self) -> usize { + let mut pos = 0; + for (i, is_left) in self.path.iter().enumerate() { + if !is_left { + pos += 1 << i; + } + } + pos + } +} + +#[derive(Clone, Debug, Eq, PartialEq, DeriveEncode, DeriveDecode, Deserialize, Serialize)] +pub struct RangeProof { + pub left_proof: Proof, + pub right_proof: Proof, +} + +impl RangeProof { + pub fn new_empty() -> Self { + Self { + left_proof: Proof::new_empty(), + right_proof: Proof::new_empty(), + } + } + + fn validate_integrity>(&self) -> bool { + self.left_proof.validate_integrity::() + && self.right_proof.validate_integrity::() + && self.left_proof.root() == self.right_proof.root() + && self.left_proof.path().len() == self.right_proof.path().len() + } + + pub fn root(&self) -> E { + self.left_proof.root() + } + + pub fn validate>( + &self, + range_leaves: &[E], + start_position: usize, + ) -> Result<()> { + if !self.validate_integrity::() { + bail!("Invalid range proof"); + } + if range_leaves.is_empty() { + bail!("Empty range"); + } + let end_position = start_position + range_leaves.len() - 1; + ensure_eq!(self.left_proof.item(), range_leaves[0]); + ensure_eq!( + self.right_proof.item(), + *range_leaves.last().expect("not empty") + ); + ensure_eq!(self.left_proof.position(), start_position); + ensure_eq!(self.right_proof.position(), end_position); + let tree_depth = self.left_proof.path().len() + 1; + // TODO: We can avoid copying the first layer. + let mut children_layer = range_leaves.to_vec(); + for height in 0..(tree_depth - 1) { + let mut parent_layer = Vec::new(); + let start_index = if !self.left_proof.path()[height] { + // If the left-most node is the right child, its sibling is not within the data range and should be retrieved from the proof. + let parent = A::parent(&self.left_proof.lemma()[height + 1], &children_layer[0]); + parent_layer.push(parent); + 1 + } else { + // The left-most node is the left child, its sibling is just the next child. + 0 + }; + let mut iter = children_layer[start_index..].chunks_exact(2); + while let Some([left, right]) = iter.next() { + parent_layer.push(A::parent(left, right)) + } + if let [right_most] = iter.remainder() { + if self.right_proof.path()[height] { + parent_layer.push(A::parent(right_most, &self.right_proof.lemma()[height + 1])); + } else { + bail!("Unexpected error"); + } + } + children_layer = parent_layer; + } + assert_eq!(children_layer.len(), 1); + let computed_root = children_layer.pop().unwrap(); + ensure_eq!(computed_root, self.root()); + + Ok(()) + } +} diff --git a/common/append_merkle/src/sha3.rs b/common/append_merkle/src/sha3.rs new file mode 100644 index 0000000..f7d983c --- /dev/null +++ b/common/append_merkle/src/sha3.rs @@ -0,0 +1,23 @@ +use crate::{Algorithm, HashElement}; +use tiny_keccak::{Hasher, Keccak}; + +pub struct Sha3Algorithm {} + +impl Algorithm for Sha3Algorithm { + fn parent(left: &E, right: &E) -> E { + let mut h = Keccak::v256(); + let mut e = E::null(); + h.update(left.as_ref()); + h.update(right.as_ref()); + h.finalize(e.as_mut()); + e + } + + fn leaf(data: &[u8]) -> E { + let mut h = Keccak::v256(); + let mut e = E::null(); + h.update(data.as_ref()); + h.finalize(e.as_mut()); + e + } +} diff --git a/common/channel/Cargo.toml b/common/channel/Cargo.toml new file mode 100644 index 0000000..cbdc369 --- /dev/null +++ b/common/channel/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "channel" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { version = "1.19.2", features = ["sync", "time"] } diff --git a/common/channel/src/channel.rs b/common/channel/src/channel.rs new file mode 100644 index 0000000..2f5206a --- /dev/null +++ b/common/channel/src/channel.rs @@ -0,0 +1,112 @@ +use crate::error::Error; +use std::time::Duration; +use tokio::sync::mpsc::error::TryRecvError; +use tokio::sync::{mpsc, oneshot}; +use tokio::time::timeout; + +const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(3); + +pub type ResponseSender = oneshot::Sender; + +#[derive(Debug)] +pub enum Message { + Notification(N), + Request(Req, ResponseSender), +} + +pub struct Channel { + _phantom: std::marker::PhantomData<(N, Req, Res)>, +} + +impl Channel { + pub fn unbounded() -> (Sender, Receiver) { + let (sender, receiver) = mpsc::unbounded_channel(); + (Sender { chan: sender }, Receiver { chan: receiver }) + } +} + +pub struct Sender { + chan: mpsc::UnboundedSender>, +} + +impl Clone for Sender { + fn clone(&self) -> Self { + Sender { + chan: self.chan.clone(), + } + } +} + +impl Sender { + pub fn notify(&self, msg: N) -> Result<(), Error> { + self.chan + .send(Message::Notification(msg)) + .map_err(|e| Error::SendError(e)) + } + + pub async fn request(&self, request: Req) -> Result> { + let (sender, receiver) = oneshot::channel(); + + self.chan + .send(Message::Request(request, sender)) + .map_err(|e| Error::SendError(e))?; + + timeout(DEFAULT_REQUEST_TIMEOUT, receiver) + .await + .map_err(|_| Error::TimeoutError)? + .map_err(|e| Error::RecvError(e)) + } +} + +pub struct Receiver { + chan: mpsc::UnboundedReceiver>, +} + +impl Receiver { + pub async fn recv(&mut self) -> Option> { + self.chan.recv().await + } + + pub fn try_recv(&mut self) -> Result, TryRecvError> { + self.chan.try_recv() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Debug)] + enum Notification {} + + #[derive(Debug)] + enum Request { + GetNumber, + } + + #[derive(Debug, PartialEq, Eq)] + enum Response { + GetNumber(u32), + } + + #[tokio::test] + async fn request_response() { + let (tx, mut rx) = Channel::::unbounded(); + + let task1 = async move { + match rx.recv().await.expect("not dropped") { + Message::Notification(_) => {} + Message::Request(Request::GetNumber, sender) => { + sender.send(Response::GetNumber(42)).expect("not dropped"); + } + } + }; + + let task2 = async move { + let result = tx.request(Request::GetNumber).await.expect("not dropped"); + assert_eq!(result, Response::GetNumber(42)); + }; + + tokio::join!(task1, task2); + } +} diff --git a/common/channel/src/error.rs b/common/channel/src/error.rs new file mode 100644 index 0000000..2a76572 --- /dev/null +++ b/common/channel/src/error.rs @@ -0,0 +1,18 @@ +use crate::Message; +use std::fmt::{Debug, Display, Formatter}; +use tokio::sync::{mpsc, oneshot}; + +#[derive(Debug)] +pub enum Error { + SendError(mpsc::error::SendError>), + RecvError(oneshot::error::RecvError), + TimeoutError, +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "ChannelError: {:?}", self) + } +} + +impl std::error::Error for Error {} diff --git a/common/channel/src/lib.rs b/common/channel/src/lib.rs new file mode 100644 index 0000000..f78f284 --- /dev/null +++ b/common/channel/src/lib.rs @@ -0,0 +1,5 @@ +mod channel; +pub mod error; +pub mod test_util; + +pub use crate::channel::{Channel, Message, Receiver, ResponseSender, Sender}; diff --git a/common/channel/src/test_util.rs b/common/channel/src/test_util.rs new file mode 100644 index 0000000..c796c0c --- /dev/null +++ b/common/channel/src/test_util.rs @@ -0,0 +1,55 @@ +use std::{ + fmt::Debug, + ops::{Deref, DerefMut}, + time::Duration, +}; + +use tokio::time::timeout; + +use crate::{Message, Receiver}; + +pub struct TestReceiver { + recv: Receiver, +} + +impl From> for TestReceiver { + fn from(recv: Receiver) -> Self { + Self { recv } + } +} + +impl Deref for TestReceiver { + type Target = Receiver; + + fn deref(&self) -> &Self::Target { + &self.recv + } +} + +impl DerefMut for TestReceiver { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.recv + } +} + +impl TestReceiver { + pub async fn expect_response(&mut self, response: Res) { + let request = timeout(Duration::from_secs(3), self.recv()) + .await + .expect("Timeout to receive request") + .expect("Channel closed"); + + match request { + Message::Notification(..) => panic!("Unexpected message type"), + Message::Request(_, resp_sender) => { + resp_sender.send(response).expect("Channel closed"); + } + } + } + + pub async fn expect_responses(&mut self, responses: Vec) { + for resp in responses { + self.expect_response(resp).await; + } + } +} diff --git a/common/contract-interface/Cargo.toml b/common/contract-interface/Cargo.toml new file mode 100644 index 0000000..f9a1564 --- /dev/null +++ b/common/contract-interface/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "contract-interface" +version = "0.1.0" +edition = "2021" +build = "build.rs" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +ethers = "^2" +serde_json = "1.0.82" + +[features] +compile-contracts = [] \ No newline at end of file diff --git a/common/contract-interface/build.rs b/common/contract-interface/build.rs new file mode 100644 index 0000000..0d405ff --- /dev/null +++ b/common/contract-interface/build.rs @@ -0,0 +1,30 @@ +use std::process::Command; + +const INSTALL_ERROR_MESSAGE: &str = + "Install dependencies for contract fail, try to run `yarn` in folder 'zerog-storage-contracts'"; +const COMPILE_ERROR_MESSAGE: &str = + "Compile solidity contracts fail, try to run `yarn compile` in folder 'zerog-storage-contracts'"; + +fn main() { + if cfg!(feature = "compile-contracts") { + println!("cargo:rerun-if-changed=../../zerog-storage-contracts/contracts/"); + println!("cargo:rerun-if-changed=../../zerog-storage-contracts/hardhat.config.ts"); + + let output = Command::new("yarn") + .arg("--cwd") + .arg("../../zerog-storage-contracts") + .status() + .expect(INSTALL_ERROR_MESSAGE); + assert!(output.success(), "{}", INSTALL_ERROR_MESSAGE); + + let output = Command::new("yarn") + .arg("--cwd") + .arg("../../zerog-storage-contracts") + .arg("compile") + .status() + .expect(COMPILE_ERROR_MESSAGE); + assert!(output.success(), "{}", COMPILE_ERROR_MESSAGE); + } else { + println!("cargo:rerun-if-changed=../../zerog-storage-contracts/artifacts/"); + } +} diff --git a/common/contract-interface/src/lib.rs b/common/contract-interface/src/lib.rs new file mode 100644 index 0000000..8750816 --- /dev/null +++ b/common/contract-interface/src/lib.rs @@ -0,0 +1,13 @@ +use ethers::prelude::abigen; + +// run `cargo doc -p contract-interface --open` to read struct definition + +abigen!( + ZgsFlow, + "../../zerog-storage-contracts/artifacts/contracts/dataFlow/Flow.sol/Flow.json" +); + +abigen!( + PoraMine, + "../../zerog-storage-contracts/artifacts/contracts/test/PoraMineTest.sol/PoraMineTest.json" +); diff --git a/common/directory/Cargo.toml b/common/directory/Cargo.toml new file mode 100644 index 0000000..3f95c1d --- /dev/null +++ b/common/directory/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "directory" +version = "0.1.0" +authors = ["pawan "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] \ No newline at end of file diff --git a/common/directory/src/lib.rs b/common/directory/src/lib.rs new file mode 100644 index 0000000..41acc3c --- /dev/null +++ b/common/directory/src/lib.rs @@ -0,0 +1,12 @@ +/// Names for the default directories. +pub const DEFAULT_ROOT_DIR: &str = ".lighthouse"; +pub const DEFAULT_BEACON_NODE_DIR: &str = "beacon"; +pub const DEFAULT_NETWORK_DIR: &str = "network"; +pub const DEFAULT_VALIDATOR_DIR: &str = "validators"; +pub const DEFAULT_SECRET_DIR: &str = "secrets"; +pub const DEFAULT_WALLET_DIR: &str = "wallets"; + +pub const DEFAULT_HARDCODED_NETWORK: &str = "mainnet"; + +/// Base directory name for unnamed testnets passed through the --testnet-dir flag +pub const CUSTOM_TESTNET_DIR: &str = "custom"; diff --git a/common/hashset_delay/Cargo.toml b/common/hashset_delay/Cargo.toml new file mode 100644 index 0000000..dcc2d7c --- /dev/null +++ b/common/hashset_delay/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "hashset_delay" +version = "0.2.0" +authors = ["Sigma Prime "] +edition = "2021" + +[dependencies] +futures = "0.3.21" +tokio-util = { version = "0.6.10", features = ["time"] } + +[dev-dependencies] +tokio = { version = "1.19.2", features = ["time", "rt-multi-thread", "macros"] } diff --git a/common/hashset_delay/src/hashset_delay.rs b/common/hashset_delay/src/hashset_delay.rs new file mode 100644 index 0000000..052d71f --- /dev/null +++ b/common/hashset_delay/src/hashset_delay.rs @@ -0,0 +1,197 @@ +//NOTE: This is just a specific case of a HashMapDelay. +// The code has been copied to make unique `insert` and `insert_at` functions. + +/// The default delay for entries, in seconds. This is only used when `insert()` is used to add +/// entries. +const DEFAULT_DELAY: u64 = 30; + +use futures::prelude::*; +use std::{ + collections::HashMap, + pin::Pin, + task::{Context, Poll}, + time::{Duration, Instant}, +}; +use tokio_util::time::delay_queue::{self, DelayQueue}; + +pub struct HashSetDelay +where + K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin, +{ + /// The given entries. + entries: HashMap, + /// A queue holding the timeouts of each entry. + expirations: DelayQueue, + /// The default expiration timeout of an entry. + default_entry_timeout: Duration, +} + +/// A wrapping around entries that adds the link to the entry's expiration, via a `delay_queue` key. +struct MapEntry { + /// The expiration key for the entry. + key: delay_queue::Key, + /// The actual entry. + value: Instant, +} + +impl Default for HashSetDelay +where + K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin, +{ + fn default() -> Self { + HashSetDelay::new(Duration::from_secs(DEFAULT_DELAY)) + } +} + +impl HashSetDelay +where + K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin, +{ + /// Creates a new instance of `HashSetDelay`. + pub fn new(default_entry_timeout: Duration) -> Self { + HashSetDelay { + entries: HashMap::new(), + expirations: DelayQueue::new(), + default_entry_timeout, + } + } + + /// Insert an entry into the mapping. Entries will expire after the `default_entry_timeout`. + pub fn insert(&mut self, key: K) { + self.insert_at(key, self.default_entry_timeout); + } + + /// Inserts an entry that will expire at a given instant. If the entry already exists, the + /// timeout is updated. + pub fn insert_at(&mut self, key: K, entry_duration: Duration) { + if self.contains(&key) { + // update the timeout + self.update_timeout(&key, entry_duration); + } else { + let delay_key = self.expirations.insert(key.clone(), entry_duration); + let entry = MapEntry { + key: delay_key, + value: Instant::now() + entry_duration, + }; + self.entries.insert(key, entry); + } + } + + /// Gets a reference to an entry if it exists. + /// + /// Returns None if the entry does not exist. + pub fn get(&self, key: &K) -> Option<&Instant> { + self.entries.get(key).map(|entry| &entry.value) + } + + /// Returns true if the key exists, false otherwise. + pub fn contains(&self, key: &K) -> bool { + self.entries.contains_key(key) + } + + /// Returns the length of the mapping. + pub fn len(&self) -> usize { + self.entries.len() + } + + /// Checks if the mapping is empty. + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } + + /// Updates the timeout for a given key. Returns true if the key existed, false otherwise. + /// + /// Panics if the duration is too far in the future. + pub fn update_timeout(&mut self, key: &K, timeout: Duration) -> bool { + if let Some(entry) = self.entries.get(key) { + self.expirations.reset(&entry.key, timeout); + true + } else { + false + } + } + + /// Removes a key from the map returning the value associated with the key that was in the map. + /// + /// Return false if the key was not in the map. + pub fn remove(&mut self, key: &K) -> bool { + if let Some(entry) = self.entries.remove(key) { + self.expirations.remove(&entry.key); + return true; + } + false + } + + /// Retains only the elements specified by the predicate. + /// + /// In other words, remove all pairs `(k, v)` such that `f(&k,&mut v)` returns false. + pub fn retain bool>(&mut self, mut f: F) { + let expiration = &mut self.expirations; + self.entries.retain(|key, entry| { + let result = f(key); + if !result { + expiration.remove(&entry.key); + } + result + }) + } + + /// Removes all entries from the map. + pub fn clear(&mut self) { + self.entries.clear(); + self.expirations.clear(); + } + + /// Returns a vector of referencing all keys in the map. + pub fn keys(&self) -> impl Iterator { + self.entries.keys() + } +} + +impl Stream for HashSetDelay +where + K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin, +{ + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + match self.expirations.poll_expired(cx) { + Poll::Ready(Some(Ok(key))) => match self.entries.remove(key.get_ref()) { + Some(_) => Poll::Ready(Some(Ok(key.into_inner()))), + None => Poll::Ready(Some(Err("Value no longer exists in expirations".into()))), + }, + Poll::Ready(Some(Err(e))) => { + Poll::Ready(Some(Err(format!("delay queue error: {:?}", e)))) + } + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + } + } +} + +#[cfg(test)] + +mod tests { + use super::*; + + #[tokio::test] + async fn should_not_panic() { + let key = 2u8; + + let mut map = HashSetDelay::default(); + + map.insert(key); + map.update_timeout(&key, Duration::from_secs(100)); + + let fut = |cx: &mut Context| { + let _ = map.poll_next_unpin(cx); + let _ = map.poll_next_unpin(cx); + Poll::Ready(()) + }; + + future::poll_fn(fut).await; + + map.insert(key); + map.update_timeout(&key, Duration::from_secs(100)); + } +} diff --git a/common/hashset_delay/src/lib.rs b/common/hashset_delay/src/lib.rs new file mode 100644 index 0000000..175ad72 --- /dev/null +++ b/common/hashset_delay/src/lib.rs @@ -0,0 +1,12 @@ +//! This crate provides a single type (its counter-part HashMapDelay has been removed as it +//! currently is not in use in lighthouse): +//! - `HashSetDelay` +//! +//! # HashSetDelay +//! +//! This is similar to a `HashMapDelay` except the mapping maps to the expiry time. This +//! allows users to add objects and check their expiry deadlines before the `Stream` +//! consumes them. + +mod hashset_delay; +pub use crate::hashset_delay::HashSetDelay; diff --git a/common/lighthouse_metrics/Cargo.toml b/common/lighthouse_metrics/Cargo.toml new file mode 100644 index 0000000..9e1e88c --- /dev/null +++ b/common/lighthouse_metrics/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "lighthouse_metrics" +version = "0.2.0" +authors = ["Paul Hauner "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +lazy_static = "1.4.0" +prometheus = "0.13.1" diff --git a/common/lighthouse_metrics/src/lib.rs b/common/lighthouse_metrics/src/lib.rs new file mode 100644 index 0000000..98973de --- /dev/null +++ b/common/lighthouse_metrics/src/lib.rs @@ -0,0 +1,359 @@ +#![allow(clippy::needless_doctest_main)] +//! A wrapper around the `prometheus` crate that provides a global, `lazy_static` metrics registry +//! and functions to add and use the following components (more info at +//! [Prometheus docs](https://prometheus.io/docs/concepts/metric_types/)): +//! +//! - `Histogram`: used with `start_timer(..)` and `stop_timer(..)` to record durations (e.g., +//! block processing time). +//! - `IncCounter`: used to represent an ideally ever-growing, never-shrinking integer (e.g., +//! number of block processing requests). +//! - `IntGauge`: used to represent an varying integer (e.g., number of attestations per block). +//! +//! ## Important +//! +//! Metrics will fail if two items have the same `name`. All metrics must have a unique `name`. +//! Because we use a global registry there is no namespace per crate, it's one big global space. +//! +//! See the [Prometheus naming best practices](https://prometheus.io/docs/practices/naming/) when +//! choosing metric names. +//! +//! ## Example +//! +//! ```rust +//! #[macro_use] +//! extern crate lazy_static; +//! use lighthouse_metrics::*; +//! +//! // These metrics are "magically" linked to the global registry defined in `lighthouse_metrics`. +//! lazy_static! { +//! pub static ref RUN_COUNT: Result = try_create_int_counter( +//! "runs_total", +//! "Total number of runs" +//! ); +//! pub static ref CURRENT_VALUE: Result = try_create_int_gauge( +//! "current_value", +//! "The current value" +//! ); +//! pub static ref RUN_TIME: Result = +//! try_create_histogram("run_seconds", "Time taken (measured to high precision)"); +//! } +//! +//! +//! fn main() { +//! for i in 0..100 { +//! inc_counter(&RUN_COUNT); +//! let timer = start_timer(&RUN_TIME); +//! +//! for j in 0..10 { +//! set_gauge(&CURRENT_VALUE, j); +//! println!("Howdy partner"); +//! } +//! +//! stop_timer(timer); +//! } +//! } +//! ``` + +use prometheus::{HistogramOpts, Opts}; +use std::time::Duration; + +use prometheus::core::{Atomic, GenericGauge, GenericGaugeVec}; +pub use prometheus::{ + proto::{Metric, MetricFamily, MetricType}, + Encoder, Gauge, GaugeVec, Histogram, HistogramTimer, HistogramVec, IntCounter, IntCounterVec, + IntGauge, IntGaugeVec, Result, TextEncoder, +}; + +/// Collect all the metrics for reporting. +pub fn gather() -> Vec { + prometheus::gather() +} + +/// Attempts to create an `IntCounter`, returning `Err` if the registry does not accept the counter +/// (potentially due to naming conflict). +pub fn try_create_int_counter(name: &str, help: &str) -> Result { + let opts = Opts::new(name, help); + let counter = IntCounter::with_opts(opts)?; + prometheus::register(Box::new(counter.clone()))?; + Ok(counter) +} + +/// Attempts to create an `IntGauge`, returning `Err` if the registry does not accept the counter +/// (potentially due to naming conflict). +pub fn try_create_int_gauge(name: &str, help: &str) -> Result { + let opts = Opts::new(name, help); + let gauge = IntGauge::with_opts(opts)?; + prometheus::register(Box::new(gauge.clone()))?; + Ok(gauge) +} + +/// Attempts to create a `Gauge`, returning `Err` if the registry does not accept the counter +/// (potentially due to naming conflict). +pub fn try_create_float_gauge(name: &str, help: &str) -> Result { + let opts = Opts::new(name, help); + let gauge = Gauge::with_opts(opts)?; + prometheus::register(Box::new(gauge.clone()))?; + Ok(gauge) +} + +/// Attempts to create a `Histogram`, returning `Err` if the registry does not accept the counter +/// (potentially due to naming conflict). +pub fn try_create_histogram(name: &str, help: &str) -> Result { + let opts = HistogramOpts::new(name, help); + let histogram = Histogram::with_opts(opts)?; + prometheus::register(Box::new(histogram.clone()))?; + Ok(histogram) +} + +/// Attempts to create a `HistogramVec`, returning `Err` if the registry does not accept the counter +/// (potentially due to naming conflict). +pub fn try_create_histogram_vec( + name: &str, + help: &str, + label_names: &[&str], +) -> Result { + let opts = HistogramOpts::new(name, help); + let histogram_vec = HistogramVec::new(opts, label_names)?; + prometheus::register(Box::new(histogram_vec.clone()))?; + Ok(histogram_vec) +} + +/// Attempts to create a `IntGaugeVec`, returning `Err` if the registry does not accept the gauge +/// (potentially due to naming conflict). +pub fn try_create_int_gauge_vec( + name: &str, + help: &str, + label_names: &[&str], +) -> Result { + let opts = Opts::new(name, help); + let counter_vec = IntGaugeVec::new(opts, label_names)?; + prometheus::register(Box::new(counter_vec.clone()))?; + Ok(counter_vec) +} + +/// Attempts to create a `GaugeVec`, returning `Err` if the registry does not accept the gauge +/// (potentially due to naming conflict). +pub fn try_create_float_gauge_vec( + name: &str, + help: &str, + label_names: &[&str], +) -> Result { + let opts = Opts::new(name, help); + let counter_vec = GaugeVec::new(opts, label_names)?; + prometheus::register(Box::new(counter_vec.clone()))?; + Ok(counter_vec) +} + +/// Attempts to create a `IntCounterVec`, returning `Err` if the registry does not accept the gauge +/// (potentially due to naming conflict). +pub fn try_create_int_counter_vec( + name: &str, + help: &str, + label_names: &[&str], +) -> Result { + let opts = Opts::new(name, help); + let counter_vec = IntCounterVec::new(opts, label_names)?; + prometheus::register(Box::new(counter_vec.clone()))?; + Ok(counter_vec) +} + +/// If `int_gauge_vec.is_ok()`, returns a gauge with the given `name`. +pub fn get_int_gauge(int_gauge_vec: &Result, name: &[&str]) -> Option { + if let Ok(int_gauge_vec) = int_gauge_vec { + Some(int_gauge_vec.get_metric_with_label_values(name).ok()?) + } else { + None + } +} + +pub fn get_gauge( + gauge_vec: &Result>, + name: &[&str], +) -> Option> { + if let Ok(gauge_vec) = gauge_vec { + Some(gauge_vec.get_metric_with_label_values(name).ok()?) + } else { + None + } +} + +pub fn set_gauge_entry( + gauge_vec: &Result>, + name: &[&str], + value: P::T, +) { + if let Some(v) = get_gauge(gauge_vec, name) { + v.set(value) + }; +} + +/// If `int_gauge_vec.is_ok()`, sets the gauge with the given `name` to the given `value` +/// otherwise returns false. +pub fn set_int_gauge(int_gauge_vec: &Result, name: &[&str], value: i64) -> bool { + if let Ok(int_gauge_vec) = int_gauge_vec { + int_gauge_vec + .get_metric_with_label_values(name) + .map(|v| { + v.set(value); + true + }) + .unwrap_or_else(|_| false) + } else { + false + } +} + +/// If `int_counter_vec.is_ok()`, returns a counter with the given `name`. +pub fn get_int_counter( + int_counter_vec: &Result, + name: &[&str], +) -> Option { + if let Ok(int_counter_vec) = int_counter_vec { + Some(int_counter_vec.get_metric_with_label_values(name).ok()?) + } else { + None + } +} + +/// Increments the `int_counter_vec` with the given `name`. +pub fn inc_counter_vec(int_counter_vec: &Result, name: &[&str]) { + if let Some(counter) = get_int_counter(int_counter_vec, name) { + counter.inc() + } +} + +pub fn inc_counter_vec_by(int_counter_vec: &Result, name: &[&str], amount: u64) { + if let Some(counter) = get_int_counter(int_counter_vec, name) { + counter.inc_by(amount); + } +} + +/// If `histogram_vec.is_ok()`, returns a histogram with the given `name`. +pub fn get_histogram(histogram_vec: &Result, name: &[&str]) -> Option { + if let Ok(histogram_vec) = histogram_vec { + Some(histogram_vec.get_metric_with_label_values(name).ok()?) + } else { + None + } +} + +/// Starts a timer on `vec` with the given `name`. +pub fn start_timer_vec(vec: &Result, name: &[&str]) -> Option { + get_histogram(vec, name).map(|h| h.start_timer()) +} + +/// Starts a timer for the given `Histogram`, stopping when it gets dropped or given to `stop_timer(..)`. +pub fn start_timer(histogram: &Result) -> Option { + if let Ok(histogram) = histogram { + Some(histogram.start_timer()) + } else { + None + } +} + +/// Starts a timer on `vec` with the given `name`. +pub fn observe_timer_vec(vec: &Result, name: &[&str], duration: Duration) { + if let Some(h) = get_histogram(vec, name) { + h.observe(duration_to_f64(duration)) + } +} + +/// Stops a timer created with `start_timer(..)`. +pub fn stop_timer(timer: Option) { + if let Some(t) = timer { + t.observe_duration() + } +} + +pub fn inc_counter(counter: &Result) { + if let Ok(counter) = counter { + counter.inc(); + } +} + +pub fn inc_counter_by(counter: &Result, value: u64) { + if let Ok(counter) = counter { + counter.inc_by(value); + } +} + +pub fn set_gauge_vec(int_gauge_vec: &Result, name: &[&str], value: i64) { + if let Some(gauge) = get_int_gauge(int_gauge_vec, name) { + gauge.set(value); + } +} + +pub fn inc_gauge_vec(int_gauge_vec: &Result, name: &[&str]) { + if let Some(gauge) = get_int_gauge(int_gauge_vec, name) { + gauge.inc(); + } +} + +pub fn dec_gauge_vec(int_gauge_vec: &Result, name: &[&str]) { + if let Some(gauge) = get_int_gauge(int_gauge_vec, name) { + gauge.dec(); + } +} + +pub fn set_gauge(gauge: &Result, value: i64) { + if let Ok(gauge) = gauge { + gauge.set(value); + } +} + +pub fn set_float_gauge(gauge: &Result, value: f64) { + if let Ok(gauge) = gauge { + gauge.set(value); + } +} + +pub fn set_float_gauge_vec(gauge_vec: &Result, name: &[&str], value: f64) { + if let Some(gauge) = get_gauge(gauge_vec, name) { + gauge.set(value); + } +} + +pub fn inc_gauge(gauge: &Result) { + if let Ok(gauge) = gauge { + gauge.inc(); + } +} + +pub fn dec_gauge(gauge: &Result) { + if let Ok(gauge) = gauge { + gauge.dec(); + } +} + +pub fn maybe_set_gauge(gauge: &Result, value_opt: Option) { + if let Some(value) = value_opt { + set_gauge(gauge, value) + } +} + +pub fn maybe_set_float_gauge(gauge: &Result, value_opt: Option) { + if let Some(value) = value_opt { + set_float_gauge(gauge, value) + } +} + +/// Sets the value of a `Histogram` manually. +pub fn observe(histogram: &Result, value: f64) { + if let Ok(histogram) = histogram { + histogram.observe(value); + } +} + +pub fn observe_duration(histogram: &Result, duration: Duration) { + if let Ok(histogram) = histogram { + histogram.observe(duration_to_f64(duration)) + } +} + +fn duration_to_f64(duration: Duration) -> f64 { + // This conversion was taken from here: + // + // https://docs.rs/prometheus/0.5.0/src/prometheus/histogram.rs.html#550-555 + let nanos = f64::from(duration.subsec_nanos()) / 1e9; + duration.as_secs() as f64 + nanos +} diff --git a/common/merkle_light/Cargo.toml b/common/merkle_light/Cargo.toml new file mode 100644 index 0000000..aa4946c --- /dev/null +++ b/common/merkle_light/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "merkle_light" +version = "0.4.0" +authors = [ + "Ivan Prisyazhnyy " +] +edition = "2021" + +description = "Light merkle tree implementation with SPV support and dependency agnostic." +license = "BSD-3-Clause" +homepage = "https://github.com/sitano/merkle_light" +repository = "https://github.com/sitano/merkle_light" +documentation = "https://sitano.github.io/merkle_light/merkle_light/index.html" +readme = "README.md" +keywords = ["merkle", "merkle-tree", "no_std"] +categories = ["data-structures", "cryptography"] + +[dependencies] + +rayon = "1.5.3" + +[dev-dependencies] + +[features] +default = ["std"] +std = [] + +[package.metadata.release] +sign-commit = true +upload-doc = true +doc-branch = "gh-pages" +pre-release-commit-message = "Release version {{version}}." +pro-release-commit-message = "Start next development iteration {{version}}." +tag-message = "Release version {{version}}." +doc-commit-message = "Update documentation." +dev-version-ext = "pre" diff --git a/common/merkle_light/README.md b/common/merkle_light/README.md new file mode 100644 index 0000000..ae6597f --- /dev/null +++ b/common/merkle_light/README.md @@ -0,0 +1,113 @@ +# merkle + +[![Build Status](https://travis-ci.org/sitano/merkle_light.svg?branch=master&style=flat)](https://travis-ci.org/sitano/merkle_light) +[![Issues](http://img.shields.io/github/issues/sitano/merkle.svg?style=flat)](https://github.com/sitano/merkle_light/issues) +![License](https://img.shields.io/badge/license-bsd3-brightgreen.svg?style=flat) +[![Crates.io](https://img.shields.io/crates/v/merkle_light.svg)](https://crates.io/crates/merkle_light) + +*merkle* is a lightweight Rust implementation of a [Merkle tree](https://en.wikipedia.org/wiki/Merkle_tree). + +## Features + +- external dependency agnostic +- `core::hash::Hasher` compatibility +- standard types hasher implementations +- `#[derive(Hashable)]` support for simple struct +- customizable merkle leaf/node hashing algorithm +- support for custom hash types (e.g. [u8; 16], [u64; 4], [u128; 2], struct) +- customizable hashing algorithm +- linear memory layout, no nodes on heap +- buildable from iterator, objects or hashes +- certificate transparency style merkle hashing support +- SPV included + +## Documentation + +Documentation is [available](https://sitano.github.io/merkle_light/merkle_light/index.html). + +# Examples + +* `test_sip.rs`: algorithm implementation example for std sip hasher, u64 hash items +* `test_xor128.rs`: custom hash example xor128 +* `test_cmh.rs`: custom merkle hasher implementation example +* `crypto_bitcoin_mt.rs`: bitcoin merkle tree using crypto lib +* `crypto_chaincore_mt.rs`: chain core merkle tree using crypto lib +* `ring_bitcoin_mt.rs`: bitcoin merkle tree using ring lib + +# Quick start + +``` +extern crate crypto; +extern crate merkle_light; + +use std::fmt; +use std::hash::Hasher; +use std::iter::FromIterator; +use crypto::sha3::{Sha3, Sha3Mode}; +use crypto::digest::Digest; +use merkle_light::hash::{Algorithm, Hashable}; +use merkle_light::merkle::MerkleTree; + +pub struct ExampleAlgorithm(Sha3); + +impl ExampleAlgorithm { + pub fn new() -> ExampleAlgorithm { + ExampleAlgorithm(Sha3::new(Sha3Mode::Sha3_256)) + } +} + +impl Default for ExampleAlgorithm { + fn default() -> ExampleAlgorithm { + ExampleAlgorithm::new() + } +} + +impl Hasher for ExampleAlgorithm { + #[inline] + fn write(&mut self, msg: &[u8]) { + self.0.input(msg) + } + + #[inline] + fn finish(&self) -> u64 { + unimplemented!() + } +} + +impl Algorithm<[u8; 32]> for ExampleAlgorithm { + #[inline] + fn hash(&mut self) -> [u8; 32] { + let mut h = [0u8; 32]; + self.0.result(&mut h); + h + } + + #[inline] + fn reset(&mut self) { + self.0.reset(); + } +} + +fn main() { + let mut h1 = [0u8; 32]; + let mut h2 = [0u8; 32]; + let mut h3 = [0u8; 32]; + h1[0] = 0x11; + h2[0] = 0x22; + h3[0] = 0x33; + + let t: MerkleTree<[u8; 32], ExampleAlgorithm> = MerkleTree::from_iter(vec![h1, h2, h3]); + println!("{:?}", t.root()); +} +``` + +## Bug Reporting + +Please report bugs either as pull requests or as issues in [the issue +tracker](https://github.com/sitano/merkle_light). *merkle* has a +**full disclosure** vulnerability policy. **Please do NOT attempt to report +any security vulnerability in this code privately to anybody.** + +## License + +See [LICENSE](LICENSE). diff --git a/common/merkle_light/benches/crypto_sha512.rs b/common/merkle_light/benches/crypto_sha512.rs new file mode 100644 index 0000000..23aa2a5 --- /dev/null +++ b/common/merkle_light/benches/crypto_sha512.rs @@ -0,0 +1,170 @@ +//! cargo bench --features "crypto_bench" --verbose +#![cfg(feature = "crypto_bench")] +#![feature(test)] +#![feature(rand)] +#![feature(crypto)] + +mod hash512; + +extern crate crypto; +extern crate merkle_light; +extern crate rand; +extern crate test; + +use crypto::digest::Digest; +use crypto::sha2::Sha512; +use hash512::Hash512; +use merkle_light::hash::{Algorithm, Hashable}; +use merkle_light::merkle::MerkleTree; +use rand::Rng; +use std::hash::Hasher; +use std::iter::FromIterator; +use test::Bencher; + +#[derive(Copy, Clone)] +struct A(Sha512); + +impl A { + fn new() -> A { + A(Sha512::new()) + } +} + +impl Default for A { + fn default() -> Self { + A::new() + } +} + +impl Hasher for A { + #[inline] + fn write(&mut self, msg: &[u8]) { + self.0.input(msg) + } + + #[inline] + fn finish(&self) -> u64 { + unimplemented!() + } +} + +impl Algorithm for A { + #[inline] + fn hash(&mut self) -> Hash512 { + let mut h = [0u8; 64]; + self.0.result(&mut h); + Hash512(h) + } + + #[inline] + fn reset(&mut self) { + self.0.reset(); + } +} + +fn tree_5() -> Vec { + ["one", "two", "three", "four"] + .iter() + .map(|x| { + let mut a = A::new(); + Hashable::hash(x, &mut a); + a.hash() + }) + .collect::>() +} + +fn tree_160() -> Vec { + let mut values = vec![vec![0u8; 256]; 160]; + let mut rng = rand::IsaacRng::new_unseeded(); + + for mut v in &mut values { + rng.fill_bytes(&mut v); + } + + values + .iter() + .map(|x| { + let mut a = A::new(); + a.write(x.as_ref()); + a.hash() + }) + .collect::>() +} + +#[bench] +fn bench_crypto_sha512(b: &mut Bencher) { + let mut h = [0u8; 64]; + b.iter(|| { + let mut x = Sha512::new(); + x.input("12345".as_ref()); + x.result(&mut h); + }); +} + +#[bench] +fn bench_crypto_sha512_from_data_5(b: &mut Bencher) { + let values = tree_5(); + b.iter(|| MerkleTree::::from_iter(values.clone())); +} + +#[bench] +fn bench_crypto_sha512_from_data_5_proof(b: &mut Bencher) { + let values = tree_5(); + let tree: MerkleTree = MerkleTree::from_iter(values.clone()); + + b.iter(|| { + for i in 0..values.len() { + let proof = tree.gen_proof(i); + test::black_box(proof); + } + }); +} + +#[bench] +fn bench_crypto_sha512_from_data_5_proof_check(b: &mut Bencher) { + let values = tree_5(); + let tree: MerkleTree = MerkleTree::from_iter(values.clone()); + let proofs = (0..values.len()) + .map(|i| tree.gen_proof(i)) + .collect::>(); + + b.iter(|| { + for proof in &proofs { + test::black_box(proof.validate::()); + } + }); +} + +#[bench] +fn bench_crypto_sha512_from_data_160(b: &mut Bencher) { + let values = tree_160(); + b.iter(|| MerkleTree::::from_iter(values.clone())); +} + +#[bench] +fn bench_crypto_sha512_from_data_160_proof(b: &mut Bencher) { + let values = tree_160(); + let tree: MerkleTree = MerkleTree::from_iter(values.clone()); + + b.iter(|| { + for i in 0..values.len() { + let proof = tree.gen_proof(i); + test::black_box(proof); + } + }); +} + +#[bench] +fn bench_crypto_sha512_from_data_160_proof_check(b: &mut Bencher) { + let values = tree_160(); + let tree: MerkleTree = MerkleTree::from_iter(values.clone()); + let proofs = (0..values.len()) + .map(|i| tree.gen_proof(i)) + .collect::>(); + + b.iter(|| { + for proof in &proofs { + test::black_box(proof.validate::()); + } + }); +} diff --git a/common/merkle_light/benches/hash512/hash512.rs b/common/merkle_light/benches/hash512/hash512.rs new file mode 100644 index 0000000..90d3e02 --- /dev/null +++ b/common/merkle_light/benches/hash512/hash512.rs @@ -0,0 +1,58 @@ +use std::cmp::Ordering; + +#[derive(Copy, Clone)] +pub struct Hash512(pub [u8; 64]); + +impl Default for Hash512 { + fn default() -> Self { + Hash512([0u8; 64]) + } +} + +impl AsRef<[u8]> for Hash512 { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl PartialOrd for Hash512 { + #[inline] + fn partial_cmp(&self, other: &Hash512) -> Option { + PartialOrd::partial_cmp(&&self.0[..], &&other.0[..]) + } + + #[inline] + fn lt(&self, other: &Hash512) -> bool { + PartialOrd::lt(&&self.0[..], &&other.0[..]) + } + + #[inline] + fn le(&self, other: &Hash512) -> bool { + PartialOrd::le(&&self.0[..], &&other.0[..]) + } + + #[inline] + fn ge(&self, other: &Hash512) -> bool { + PartialOrd::ge(&&self.0[..], &&other.0[..]) + } + + #[inline] + fn gt(&self, other: &Hash512) -> bool { + PartialOrd::gt(&&self.0[..], &&other.0[..]) + } +} + +impl Ord for Hash512 { + #[inline] + fn cmp(&self, other: &Hash512) -> Ordering { + Ord::cmp(&&self.0[..], &&other.0[..]) + } +} + +impl PartialEq for Hash512 { + fn eq(&self, other: &Hash512) -> bool { + self.0.as_ref() == other.0.as_ref() + } +} + +impl Eq for Hash512 {} diff --git a/common/merkle_light/benches/hash512/mod.rs b/common/merkle_light/benches/hash512/mod.rs new file mode 100644 index 0000000..deefc8c --- /dev/null +++ b/common/merkle_light/benches/hash512/mod.rs @@ -0,0 +1,3 @@ +mod hash512; + +pub use hash512::hash512::Hash512; diff --git a/common/merkle_light/benches/ring_sha512.rs b/common/merkle_light/benches/ring_sha512.rs new file mode 100644 index 0000000..eac2c49 --- /dev/null +++ b/common/merkle_light/benches/ring_sha512.rs @@ -0,0 +1,168 @@ +//! cargo bench --features "crypto_bench" --verbose +#![cfg(feature = "crypto_bench")] +#![feature(test)] +#![feature(rand)] + +mod hash512; +mod ringx; + +extern crate merkle_light; +extern crate rand; +extern crate test; + +use hash512::Hash512; +use merkle_light::hash::{Algorithm, Hashable}; +use merkle_light::merkle::MerkleTree; +use rand::Rng; +use ringx::Context; +use ringx::SHA512; +use std::hash::Hasher; +use std::iter::FromIterator; +use test::Bencher; + +#[derive(Clone)] +struct B(Context); + +impl B { + fn new() -> B { + B(Context::new(&SHA512)) + } +} + +impl Default for B { + fn default() -> Self { + B::new() + } +} + +impl Hasher for B { + #[inline] + fn write(&mut self, msg: &[u8]) { + self.0.update(msg) + } + + #[inline] + fn finish(&self) -> u64 { + unimplemented!() + } +} + +impl Algorithm for B { + #[inline] + fn hash(&mut self) -> Hash512 { + let mut h = [0u8; 64]; + h.copy_from_slice(self.0.finish().as_ref()); + Hash512(h) + } + + #[inline] + fn reset(&mut self) { + self.0.reset(); + } +} + +fn tree_5() -> Vec { + ["one", "two", "three", "four"] + .iter() + .map(|x| { + let mut a = B::new(); + Hashable::hash(x, &mut a); + a.hash() + }) + .collect::>() +} + +fn tree_160() -> Vec { + let mut values = vec![vec![0u8; 256]; 160]; + let mut rng = rand::IsaacRng::new_unseeded(); + + for mut v in &mut values { + rng.fill_bytes(&mut v); + } + + values + .iter() + .map(|x| { + let mut a = B::new(); + a.write(x.as_ref()); + a.hash() + }) + .collect::>() +} + +#[bench] +fn bench_ringx_sha512(b: &mut Bencher) { + b.iter(|| { + let mut x = Context::new(&SHA512); + x.update("12345".as_ref()); + x.finish(); + }); +} + +#[bench] +fn bench_ringx_sha512_from_data_5(b: &mut Bencher) { + let values = tree_5(); + b.iter(|| MerkleTree::::from_iter(values.clone())); +} + +#[bench] +fn bench_ringx_sha512_from_data_5_proof(b: &mut Bencher) { + let values = tree_5(); + let tree: MerkleTree = MerkleTree::from_iter(values.clone()); + + b.iter(|| { + for i in 0..values.len() { + let proof = tree.gen_proof(i); + test::black_box(proof); + } + }); +} + +#[bench] +fn bench_ringx_sha512_from_data_5_proof_check(b: &mut Bencher) { + let values = tree_5(); + let tree: MerkleTree = MerkleTree::from_iter(values.clone()); + let proofs = (0..values.len()) + .map(|i| tree.gen_proof(i)) + .collect::>(); + + b.iter(|| { + for proof in &proofs { + test::black_box(proof.validate::()); + } + }); +} + +#[bench] +fn bench_ringx_sha512_from_data_160(b: &mut Bencher) { + let values = tree_160(); + b.iter(|| MerkleTree::::from_iter(values.clone())); +} + +#[bench] +fn bench_ringx_sha512_from_data_160_proof(b: &mut Bencher) { + let values = tree_160(); + let tree: MerkleTree = MerkleTree::from_iter(values.clone()); + + b.iter(|| { + for i in 0..values.len() { + let proof = tree.gen_proof(i); + test::black_box(proof); + } + }); +} + +#[bench] +fn bench_ringx_sha512_from_data_160_proof_check(b: &mut Bencher) { + let values = tree_160(); + let tree: MerkleTree = MerkleTree::from_iter(values.clone()); + let proofs = (0..values.len()) + .map(|i| tree.gen_proof(i)) + .collect::>(); + + b.iter(|| { + for proof in &proofs { + test::black_box(proof.validate::()); + } + }); +} diff --git a/common/merkle_light/benches/ringx/init.rs b/common/merkle_light/benches/ringx/init.rs new file mode 100644 index 0000000..0f70564 --- /dev/null +++ b/common/merkle_light/benches/ringx/init.rs @@ -0,0 +1,26 @@ +// Copyright 2016 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +#[inline(always)] +pub fn init_once() { + #[cfg(not(target_os = "ios"))] + { + use std; + extern "C" { + fn GFp_cpuid_setup(); + } + static INIT: std::sync::Once = std::sync::ONCE_INIT; + INIT.call_once(|| unsafe { GFp_cpuid_setup() }); + } +} diff --git a/common/merkle_light/benches/ringx/mod.rs b/common/merkle_light/benches/ringx/mod.rs new file mode 100644 index 0000000..b36a8c3 --- /dev/null +++ b/common/merkle_light/benches/ringx/mod.rs @@ -0,0 +1,567 @@ +//! Improvement version of [`ring::digest::Context`]. +//! +//! [`Context.finish`] now has `(&mut self)` instead of `(mut self)`. +//! [`Context`] acquired state `reset` thing. + +#![cfg(feature = "crypto_bench")] +#![allow(dead_code)] + +extern crate rand; +extern crate ring; +extern crate test; + +mod init; + +use std::fmt; + +// XXX: Replace with `const fn` when `const fn` is stable: +// https://github.com/rust-lang/rust/issues/24111 +#[cfg(target_endian = "little")] +macro_rules! u32x2 { + ( $first:expr, $second:expr ) => { + ((($second as u64) << 32) | ($first as u64)) + }; +} + +/// A context for multi-step (Init-Update-Finish) digest calculations. +/// +/// C analog: `EVP_MD_CTX`. +pub struct Context { + state: State, + + // Note that SHA-512 has a 128-bit input bit counter, but this + // implementation only supports up to 2^64-1 input bits for all algorithms, + // so a 64-bit counter is more than sufficient. + completed_data_blocks: u64, + + // TODO: More explicitly force 64-bit alignment for |pending|. + pending: [u8; MAX_BLOCK_LEN], + num_pending: usize, + + /// The context's algorithm. + pub algorithm: &'static Algorithm, +} + +impl Context { + /// Constructs a new context. + /// + /// C analogs: `EVP_DigestInit`, `EVP_DigestInit_ex` + pub fn new(algorithm: &'static Algorithm) -> Context { + init::init_once(); + + Context { + algorithm, + state: algorithm.initial_state, + completed_data_blocks: 0, + pending: [0u8; MAX_BLOCK_LEN], + num_pending: 0, + } + } + + /// Updates the digest with all the data in `data`. `update` may be called + /// zero or more times until `finish` is called. It must not be called + /// after `finish` has been called. + /// + /// C analog: `EVP_DigestUpdate` + pub fn update(&mut self, data: &[u8]) { + if data.len() < self.algorithm.block_len - self.num_pending { + self.pending[self.num_pending..(self.num_pending + data.len())].copy_from_slice(data); + self.num_pending += data.len(); + return; + } + + let mut remaining = data; + if self.num_pending > 0 { + let to_copy = self.algorithm.block_len - self.num_pending; + self.pending[self.num_pending..self.algorithm.block_len] + .copy_from_slice(&data[..to_copy]); + + unsafe { + (self.algorithm.block_data_order)(&mut self.state, self.pending.as_ptr(), 1); + } + self.completed_data_blocks = self.completed_data_blocks.checked_add(1).unwrap(); + + remaining = &remaining[to_copy..]; + self.num_pending = 0; + } + + let num_blocks = remaining.len() / self.algorithm.block_len; + let num_to_save_for_later = remaining.len() % self.algorithm.block_len; + if num_blocks > 0 { + unsafe { + (self.algorithm.block_data_order)(&mut self.state, remaining.as_ptr(), num_blocks); + } + self.completed_data_blocks = self + .completed_data_blocks + .checked_add(polyfill::slice::u64_from_usize(num_blocks)) + .unwrap(); + } + if num_to_save_for_later > 0 { + self.pending[..num_to_save_for_later] + .copy_from_slice(&remaining[(remaining.len() - num_to_save_for_later)..]); + self.num_pending = num_to_save_for_later; + } + } + + /// Finalizes the digest calculation and returns the digest value. `finish` + /// consumes the context so it cannot be (mis-)used after `finish` has been + /// called. + /// + /// C analogs: `EVP_DigestFinal`, `EVP_DigestFinal_ex` + pub fn finish(&mut self) -> Digest { + // We know |num_pending < self.algorithm.block_len|, because we would + // have processed the block otherwise. + + let mut padding_pos = self.num_pending; + self.pending[padding_pos] = 0x80; + padding_pos += 1; + + if padding_pos > self.algorithm.block_len - self.algorithm.len_len { + polyfill::slice::fill(&mut self.pending[padding_pos..self.algorithm.block_len], 0); + unsafe { + (self.algorithm.block_data_order)(&mut self.state, self.pending.as_ptr(), 1); + } + // We don't increase |self.completed_data_blocks| because the + // padding isn't data, and so it isn't included in the data length. + padding_pos = 0; + } + + polyfill::slice::fill( + &mut self.pending[padding_pos..(self.algorithm.block_len - 8)], + 0, + ); + + // Output the length, in bits, in big endian order. + let mut completed_data_bits: u64 = self + .completed_data_blocks + .checked_mul(polyfill::slice::u64_from_usize(self.algorithm.block_len)) + .unwrap() + .checked_add(polyfill::slice::u64_from_usize(self.num_pending)) + .unwrap() + .checked_mul(8) + .unwrap(); + + for b in (&mut self.pending[(self.algorithm.block_len - 8)..self.algorithm.block_len]) + .into_iter() + .rev() + { + *b = completed_data_bits as u8; + completed_data_bits /= 0x100; + } + unsafe { + (self.algorithm.block_data_order)(&mut self.state, self.pending.as_ptr(), 1); + } + + Digest { + algorithm: self.algorithm, + value: (self.algorithm.format_output)(&self.state), + } + } + + /// The algorithm that this context is using. + #[inline(always)] + pub fn algorithm(&self) -> &'static Algorithm { + self.algorithm + } + + /// Reset context state. + pub fn reset(&mut self) { + self.state = self.algorithm.initial_state; + self.pending = [0u8; MAX_BLOCK_LEN]; + self.completed_data_blocks = 0; + self.num_pending = 0; + } +} + +// XXX: This should just be `#[derive(Clone)]` but that doesn't work because +// `[u8; 128]` doesn't implement `Clone`. +impl Clone for Context { + fn clone(&self) -> Context { + Context { + state: self.state, + pending: self.pending, + completed_data_blocks: self.completed_data_blocks, + num_pending: self.num_pending, + algorithm: self.algorithm, + } + } +} + +/// Returns the digest of `data` using the given digest algorithm. +/// +/// C analog: `EVP_Digest` +/// +/// # Examples: +/// +/// ``` +/// # #[cfg(feature = "use_heap")] +/// # fn main() { +/// use ring::{digest, test}; +/// +/// let expected_hex = +/// "09ca7e4eaa6e8ae9c7d261167129184883644d07dfba7cbfbc4c8a2e08360d5b"; +/// let expected: Vec = test::from_hex(expected_hex).unwrap(); +/// let actual = digest::digest(&digest::SHA256, b"hello, world"); +/// +/// assert_eq!(&expected, &actual.as_ref()); +/// # } +/// +/// # #[cfg(not(feature = "use_heap"))] +/// # fn main() { } +/// ``` +pub fn digest(algorithm: &'static Algorithm, data: &[u8]) -> Digest { + let mut ctx = Context::new(algorithm); + ctx.update(data); + ctx.finish() +} + +/// A calculated digest value. +/// +/// Use `as_ref` to get the value as a `&[u8]`. +#[derive(Clone, Copy)] +pub struct Digest { + value: Output, + algorithm: &'static Algorithm, +} + +impl Digest { + /// The algorithm that was used to calculate the digest value. + #[inline(always)] + pub fn algorithm(&self) -> &'static Algorithm { + self.algorithm + } +} + +impl AsRef<[u8]> for Digest { + #[inline(always)] + fn as_ref(&self) -> &[u8] { + &(polyfill::slice::u64_as_u8(&self.value))[..self.algorithm.output_len] + } +} + +impl fmt::Debug for Digest { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!(fmt, "{:?}:", self.algorithm)?; + for byte in self.as_ref() { + write!(fmt, "{:02x}", byte)?; + } + Ok(()) + } +} + +/// A digest algorithm. +/// +/// C analog: `EVP_MD` +pub struct Algorithm { + /// C analog: `EVP_MD_size` + pub output_len: usize, + + /// The size of the chaining value of the digest function, in bytes. For + /// non-truncated algorithms (SHA-1, SHA-256, SHA-512), this is equal to + /// `output_len`. For truncated algorithms (e.g. SHA-384, SHA-512/256), + /// this is equal to the length before truncation. This is mostly helpful + /// for determining the size of an HMAC key that is appropriate for the + /// digest algorithm. + pub chaining_len: usize, + + /// C analog: `EVP_MD_block_size` + pub block_len: usize, + + /// The length of the length in the padding. + pub len_len: usize, + + pub block_data_order: unsafe extern "C" fn(state: &mut State, data: *const u8, num: usize), + pub format_output: fn(input: &State) -> Output, + + pub initial_state: State, + + pub id: AlgorithmID, +} + +#[derive(Eq, PartialEq)] +#[allow(non_camel_case_types)] +pub enum AlgorithmID { + SHA256, + SHA512, + SHA512_256, +} + +impl PartialEq for Algorithm { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for Algorithm {} + +impl fmt::Debug for Algorithm { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + // This would have to change if/when we add other algorithms with the + // same lengths. + let (n, suffix) = + if self.output_len == SHA512_256_OUTPUT_LEN && self.block_len == SHA512_BLOCK_LEN { + (512, "_256") + } else if self.output_len == 20 { + (1, "") + } else { + (self.output_len * 8, "") + }; + write!(fmt, "SHA{}{}", n, suffix) + } +} + +/// SHA-256 as specified in [FIPS 180-4]. +/// +/// [FIPS 180-4]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf +pub static SHA256: Algorithm = Algorithm { + output_len: SHA256_OUTPUT_LEN, + chaining_len: SHA256_OUTPUT_LEN, + block_len: 512 / 8, + len_len: 64 / 8, + block_data_order: GFp_sha256_block_data_order, + format_output: sha256_format_output, + initial_state: [ + u32x2!(0x6a09e667u32, 0xbb67ae85u32), + u32x2!(0x3c6ef372u32, 0xa54ff53au32), + u32x2!(0x510e527fu32, 0x9b05688cu32), + u32x2!(0x1f83d9abu32, 0x5be0cd19u32), + 0, + 0, + 0, + 0, + ], + id: AlgorithmID::SHA256, +}; + +/// SHA-512 as specified in [FIPS 180-4]. +/// +/// [FIPS 180-4]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf +pub static SHA512: Algorithm = Algorithm { + output_len: SHA512_OUTPUT_LEN, + chaining_len: SHA512_OUTPUT_LEN, + block_len: SHA512_BLOCK_LEN, + len_len: SHA512_LEN_LEN, + block_data_order: GFp_sha512_block_data_order, + format_output: sha512_format_output, + initial_state: [ + 0x6a09e667f3bcc908, + 0xbb67ae8584caa73b, + 0x3c6ef372fe94f82b, + 0xa54ff53a5f1d36f1, + 0x510e527fade682d1, + 0x9b05688c2b3e6c1f, + 0x1f83d9abfb41bd6b, + 0x5be0cd19137e2179, + ], + id: AlgorithmID::SHA512, +}; + +/// SHA-512/256 as specified in [FIPS 180-4]. +/// +/// This is *not* the same as just truncating the output of SHA-512, as +/// SHA-512/256 has its own initial state distinct from SHA-512's initial +/// state. +/// +/// [FIPS 180-4]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf +pub static SHA512_256: Algorithm = Algorithm { + output_len: SHA512_256_OUTPUT_LEN, + chaining_len: SHA512_OUTPUT_LEN, + block_len: SHA512_BLOCK_LEN, + len_len: SHA512_LEN_LEN, + block_data_order: GFp_sha512_block_data_order, + format_output: sha512_format_output, + initial_state: [ + 0x22312194fc2bf72c, + 0x9f555fa3c84c64c2, + 0x2393b86b6f53b151, + 0x963877195940eabd, + 0x96283ee2a88effe3, + 0xbe5e1e2553863992, + 0x2b0199fc2c85b8aa, + 0x0eb72ddc81c52ca2, + ], + id: AlgorithmID::SHA512_256, +}; + +// We use u64 to try to ensure 64-bit alignment/padding. +pub type State = [u64; MAX_CHAINING_LEN / 8]; + +pub type Output = [u64; MAX_OUTPUT_LEN / 8]; + +/// The maximum block length (`Algorithm::block_len`) of all the algorithms in +/// this module. +pub const MAX_BLOCK_LEN: usize = 1024 / 8; + +/// The maximum output length (`Algorithm::output_len`) of all the algorithms +/// in this module. +pub const MAX_OUTPUT_LEN: usize = 512 / 8; + +/// The maximum chaining length (`Algorithm::chaining_len`) of all the +/// algorithms in this module. +pub const MAX_CHAINING_LEN: usize = MAX_OUTPUT_LEN; + +mod polyfill { + pub mod slice { + use std::slice::from_raw_parts; + + // https://internals.rust-lang.org/t/ + // safe-trasnsmute-for-slices-e-g-u64-u32-particularly-simd-types/2871 + #[inline(always)] + pub fn u64_as_u32(src: &[u64]) -> &[u32] { + unsafe { from_raw_parts(src.as_ptr() as *const u32, src.len() * 2) } + } + + #[inline(always)] + pub fn u64_from_usize(x: usize) -> u64 { + x as u64 + } + + // https://internals.rust-lang.org/t/ + // stabilizing-basic-functions-on-arrays-and-slices/2868 + #[inline(always)] + pub fn fill(dest: &mut [u8], value: u8) { + for d in dest { + *d = value; + } + } + + // https://internals.rust-lang.org/t/ + // safe-trasnsmute-for-slices-e-g-u64-u32-particularly-simd-types/2871 + #[inline(always)] + pub fn u64_as_u8(src: &[u64]) -> &[u8] { + unsafe { from_raw_parts(src.as_ptr() as *const u8, src.len() * 8) } + } + } +} + +pub fn sha256_format_output(input: &State) -> Output { + let input = &polyfill::slice::u64_as_u32(input)[..8]; + [ + u32x2!(input[0].to_be(), input[1].to_be()), + u32x2!(input[2].to_be(), input[3].to_be()), + u32x2!(input[4].to_be(), input[5].to_be()), + u32x2!(input[6].to_be(), input[7].to_be()), + 0, + 0, + 0, + 0, + ] +} + +pub fn sha512_format_output(input: &State) -> Output { + [ + input[0].to_be(), + input[1].to_be(), + input[2].to_be(), + input[3].to_be(), + input[4].to_be(), + input[5].to_be(), + input[6].to_be(), + input[7].to_be(), + ] +} + +/// The length of the output of SHA-256, in bytes. +pub const SHA256_OUTPUT_LEN: usize = 256 / 8; + +/// The length of the output of SHA-512, in bytes. +pub const SHA512_OUTPUT_LEN: usize = 512 / 8; + +/// The length of the output of SHA-512/256, in bytes. +pub const SHA512_256_OUTPUT_LEN: usize = 256 / 8; + +/// The length of a block for SHA-512-based algorithms, in bytes. +const SHA512_BLOCK_LEN: usize = 1024 / 8; + +/// The length of the length field for SHA-512-based algorithms, in bytes. +const SHA512_LEN_LEN: usize = 128 / 8; + +extern "C" { + fn GFp_sha256_block_data_order(state: &mut State, data: *const u8, num: usize); + fn GFp_sha512_block_data_order(state: &mut State, data: *const u8, num: usize); +} + +#[cfg(test)] +pub mod test_util { + use super::*; + + pub static ALL_ALGORITHMS: [&'static Algorithm; 3] = [&SHA256, &SHA512, &SHA512_256]; +} + +/* +#[cfg(test)] +mod tests { + + mod max_input { + use super::super::super::digest; + + macro_rules! max_input_tests { + ( $algorithm_name:ident ) => { + #[allow(non_snake_case)] + mod $algorithm_name { + use super::super::super::super::digest; + + #[test] + fn max_input_test() { + super::max_input_test(&digest::$algorithm_name); + } + + #[test] + #[should_panic] + fn too_long_input_test_block() { + super::too_long_input_test_block( + &digest::$algorithm_name); + } + + #[test] + #[should_panic] + fn too_long_input_test_byte() { + super::too_long_input_test_byte( + &digest::$algorithm_name); + } + } + } + } + + fn max_input_test(alg: &'static digest::Algorithm) { + let mut context = nearly_full_context(alg); + let next_input = vec![0u8; alg.block_len - 1]; + context.update(&next_input); + let _ = context.finish(); // no panic + } + + fn too_long_input_test_block(alg: &'static digest::Algorithm) { + let mut context = nearly_full_context(alg); + let next_input = vec![0u8; alg.block_len]; + context.update(&next_input); + let _ = context.finish(); // should panic + } + + fn too_long_input_test_byte(alg: &'static digest::Algorithm) { + let mut context = nearly_full_context(alg); + let next_input = vec![0u8; alg.block_len - 1]; + context.update(&next_input); // no panic + context.update(&[0]); + let _ = context.finish(); // should panic + } + + fn nearly_full_context(alg: &'static digest::Algorithm) + -> digest::Context { + // All implementations currently support up to 2^64-1 bits + // of input; according to the spec, SHA-384 and SHA-512 + // support up to 2^128-1, but that's not implemented yet. + let max_bytes = 1u64 << (64 - 3); + let max_blocks = max_bytes / (alg.block_len as u64); + digest::Context { + algorithm: alg, + state: alg.initial_state, + completed_data_blocks: max_blocks - 1, + pending: [0u8; digest::MAX_BLOCK_LEN], + num_pending: 0, + } + } + + max_input_tests!(SHA256); + max_input_tests!(SHA512); + } +}*/ diff --git a/common/merkle_light/clippy.toml b/common/merkle_light/clippy.toml new file mode 100644 index 0000000..c7b1f02 --- /dev/null +++ b/common/merkle_light/clippy.toml @@ -0,0 +1,3 @@ +disallowed-names = [ + "unreadable_literal" +] diff --git a/common/merkle_light/examples/no_std.rs b/common/merkle_light/examples/no_std.rs new file mode 100644 index 0000000..3835462 --- /dev/null +++ b/common/merkle_light/examples/no_std.rs @@ -0,0 +1,4 @@ +// This is just for no_std testing +#[allow(unused_imports)] + +fn main() {} diff --git a/common/merkle_light/src/hash.rs b/common/merkle_light/src/hash.rs new file mode 100644 index 0000000..3984f12 --- /dev/null +++ b/common/merkle_light/src/hash.rs @@ -0,0 +1,135 @@ +//! Hash infrastructure for items in Merkle Tree. + +use core::hash::Hasher; + +/// A hashable type. +/// +/// Types implementing `Hashable` are able to be [`hash`]ed with an instance of +/// [`Hasher`]. +/// +/// ## Implementing `Hashable` +/// +/// You can derive `Hashable` with `#[derive(Hashable)]` if all fields implement `Hashable`. +/// The resulting hash will be the combination of the values from calling +/// [`hash`] on each field. +/// +/// ```text +/// #[macro_use] +/// extern crate merkle_light_derive; +/// extern crate merkle_light; +/// +/// use merkle_light::hash::Hashable; +/// +/// fn main() { +/// #[derive(Hashable)] +/// struct Foo { +/// name: String, +/// country: String, +/// } +/// } +/// ``` +/// +/// If you need more control over how a value is hashed, you can of course +/// implement the `Hashable` trait yourself: +/// +/// ``` +/// extern crate merkle_light; +/// +/// use merkle_light::hash::Hashable; +/// use std::hash::Hasher; +/// use std::collections::hash_map::DefaultHasher; +/// +/// fn main() { +/// struct Person { +/// id: u32, +/// name: String, +/// phone: u64, +/// } +/// +/// impl Hashable for Person { +/// fn hash(&self, state: &mut H) { +/// self.id.hash(state); +/// self.name.hash(state); +/// self.phone.hash(state); +/// } +/// } +/// +/// let foo = Person{ +/// id: 1, +/// name: String::from("blah"), +/// phone: 2, +/// }; +/// +/// let mut hr = DefaultHasher::new(); +/// foo.hash(&mut hr); +/// assert_eq!(hr.finish(), 7101638158313343130) +/// } +/// ``` +/// +/// ## `Hashable` and `Eq` +/// +/// When implementing both `Hashable` and [`Eq`], it is important that the following +/// property holds: +/// +/// ```text +/// k1 == k2 -> hash(k1) == hash(k2) +/// ``` +/// +/// In other words, if two keys are equal, their hashes must also be equal. +pub trait Hashable { + /// Feeds this value into the given [`Hasher`]. + /// + /// [`Hasher`]: trait.Hasher.html + fn hash(&self, state: &mut H); + + /// Feeds a slice of this type into the given [`Hasher`]. + /// + /// [`Hasher`]: trait.Hasher.html + fn hash_slice(data: &[Self], state: &mut H) + where + Self: Sized, + { + for piece in data { + piece.hash(state); + } + } +} + +/// A trait for hashing an arbitrary stream of bytes for calculating merkle tree +/// nodes. +/// +/// T is a hash item must be of known size at compile time, globally ordered, with +/// default value as a neutral element of the hash space. Neutral element is +/// interpreted as 0 or nil and required for evaluation of merkle tree. +/// +/// [`Algorithm`] breaks the [`Hasher`] contract at `finish()`, but that is intended. +/// This trait extends [`Hasher`] with `hash -> T` and `reset` state methods, +/// plus implements default behavior of evaluation of MT interior nodes. +pub trait Algorithm: Hasher + Default +where + T: Clone + AsRef<[u8]>, +{ + /// Returns the hash value for the data stream written so far. + fn hash(&mut self) -> T; + + /// Reset Hasher state. + #[inline] + fn reset(&mut self) { + *self = Self::default(); + } + + /// Returns hash value for MT leaf (prefix 0x00). + #[inline] + fn leaf(&mut self, leaf: T) -> T { + self.write(leaf.as_ref()); + self.hash() + } + + /// Returns hash value for MT interior node (prefix 0x01). + #[inline] + fn node(&mut self, left: T, right: T) -> T { + self.write(left.as_ref()); + self.write(right.as_ref()); + self.hash() + } +} diff --git a/common/merkle_light/src/hash_impl.rs b/common/merkle_light/src/hash_impl.rs new file mode 100644 index 0000000..253253c --- /dev/null +++ b/common/merkle_light/src/hash_impl.rs @@ -0,0 +1,188 @@ +extern crate alloc; + +use crate::hash::Hashable; +use alloc::string::String; +use alloc::vec::Vec; +use core::hash::Hasher; +use core::mem; +use core::slice; + +macro_rules! impl_write { + ($(($ty:ident, $meth:ident),)*) => {$( + impl Hashable for $ty { + fn hash(&self, state: &mut H) { + state.$meth(*self) + } + + #[allow(trivial_casts, unsafe_code)] + fn hash_slice(data: &[$ty], state: &mut H) { + let newlen = data.len() * mem::size_of::<$ty>(); + let ptr = data.as_ptr() as *const u8; + state.write(unsafe { slice::from_raw_parts(ptr, newlen) }) + } + } + )*} +} + +impl_write! { + (u8, write_u8), + (u16, write_u16), + (u32, write_u32), + (u64, write_u64), + (usize, write_usize), + (i8, write_i8), + (i16, write_i16), + (i32, write_i32), + (i64, write_i64), + (isize, write_isize), + // unstable: (u128, write_u128), + // unstable: (i128, write_i128), +} + +macro_rules! impl_array { + ($ty:ident $($N:expr)+) => {$( + impl Hashable for [$ty; $N] { + fn hash(&self, state: &mut H) { + state.write(self.as_ref()) + } + + #[allow(trivial_casts, unsafe_code)] + fn hash_slice(data: &[[$ty; $N]], state: &mut H) { + let newlen = data.len() * mem::size_of::<[$ty; $N]>(); + let ptr = data.as_ptr() as *const u8; + state.write(unsafe { slice::from_raw_parts(ptr, newlen) }) + } + } + )*} +} + +impl_array! { u8 + 1 2 3 4 5 6 7 8 9 10 + 11 12 13 14 15 16 17 18 19 20 + 21 22 23 24 25 26 27 28 29 30 + 31 32 33 34 35 36 37 38 39 40 + 41 42 43 44 45 46 47 48 49 50 + 51 52 53 54 55 56 57 58 59 60 + 61 62 63 64 +} + +impl Hashable for Vec { + fn hash(&self, state: &mut H) { + state.write(self.as_ref()); + } +} + +impl Hashable for bool { + fn hash(&self, state: &mut H) { + state.write_u8(*self as u8) + } +} + +impl Hashable for char { + fn hash(&self, state: &mut H) { + state.write_u32(*self as u32) + } +} + +impl Hashable for str { + fn hash(&self, state: &mut H) { + state.write(self.as_bytes()); + // empty str nope: state.write_u8(0xff) + } +} + +impl Hashable for String { + fn hash(&self, state: &mut H) { + state.write(self.as_bytes()); + // empty str nope: state.write_u8(0xff) + } +} + +macro_rules! impl_hash_tuple { + () => ( + impl Hashable for () { + fn hash(&self, _: &mut H) {} + } + ); + + ( $($name:ident)+) => ( + impl),*> Hashable for ($($name,)*) + where + last_type!($($name,)+): ?Sized + { + #[allow(non_snake_case)] + fn hash(&self, state: &mut Z) { + let ($(ref $name,)*) = *self; + $($name.hash(state);)* + } + } + ); +} + +macro_rules! last_type { + ($a:ident,) => { $a }; + ($a:ident, $($rest_a:ident,)+) => { last_type!($($rest_a,)+) }; +} + +impl_hash_tuple! {} +impl_hash_tuple! { A } +impl_hash_tuple! { A B } +impl_hash_tuple! { A B C } +impl_hash_tuple! { A B C D } +impl_hash_tuple! { A B C D E } +impl_hash_tuple! { A B C D E F } +impl_hash_tuple! { A B C D E F G } +impl_hash_tuple! { A B C D E F G H } +impl_hash_tuple! { A B C D E F G H I } +impl_hash_tuple! { A B C D E F G H I J } +impl_hash_tuple! { A B C D E F G H I J K } +impl_hash_tuple! { A B C D E F G H I J K L } + +impl> Hashable for [T] { + fn hash(&self, state: &mut H) { + self.len().hash(state); + Hashable::hash_slice(self, state) + } +} + +impl<'a, H: Hasher, T: ?Sized + Hashable> Hashable for &'a T { + fn hash(&self, state: &mut H) { + (**self).hash(state); + } +} + +impl<'a, H: Hasher, T: ?Sized + Hashable> Hashable for &'a mut T { + fn hash(&self, state: &mut H) { + (**self).hash(state); + } +} + +impl Hashable for *const T { + #[allow(trivial_casts, unsafe_code)] + fn hash(&self, state: &mut H) { + if mem::size_of::() == mem::size_of::() { + // Thin pointer + state.write_usize(*self as *const () as usize); + } else { + // Fat pointer + let (a, b) = unsafe { *(self as *const Self as *const (usize, usize)) }; + state.write_usize(a); + state.write_usize(b); + } + } +} + +impl Hashable for *mut T { + #[allow(trivial_casts, unsafe_code)] + fn hash(&self, state: &mut H) { + if mem::size_of::() == mem::size_of::() { + // Thin pointer + state.write_usize(*self as *const () as usize); + } else { + // Fat pointer + let (a, b) = unsafe { *(self as *const Self as *const (usize, usize)) }; + state.write_usize(a); + state.write_usize(b); + } + } +} diff --git a/common/merkle_light/src/lib.rs b/common/merkle_light/src/lib.rs new file mode 100644 index 0000000..1147981 --- /dev/null +++ b/common/merkle_light/src/lib.rs @@ -0,0 +1,178 @@ +//! light _Merkle Tree_ implementation. +//! +//! Merkle tree (MT) implemented as a full binary tree allocated as a vec +//! of statically sized hashes to give hashes more locality. MT specialized +//! to the extent of hashing algorithm and hash item. [`Hashable`] trait is +//! compatible to the `std::hash::Hasher` and supports custom hash algorithms. +//! Implementation does not depend on any external crypto libraries, and tries +//! to be as performant as possible. +//! +//! This tree implementation uses encoding scheme as in _Certificate Transparency_ +//! by default. Encoding scheme for leafs and nodes can be overridden though. +//! [RFC 6962](https://tools.ietf.org/html/rfc6962): +//! +//! ```text +//! MTH({d(0)}) = ALG(0x00 || d(0)). +//! For n > 1, let k be the largest power of two smaller than n (i.e., +//! k < n <= 2k). The Merkle tree Hash of an n-element list D[n] is then +//! defined recursively as +//! MTH(D[n]) = ALG(0x01 || MTH(D[0:k]) || MTH(D[k:n])), +//! ``` +//! +//! Link: [](https://en.wikipedia.org/wiki/Merkle_tree) +//! +//! # Implementation choices +//! +//! Main idea is the whole code must obtain specialization at compile time with +//! minimum allocations calls, hashes must be of fixed size arrays known at +//! compile time, hash algorithm must be a trait and must not depend on any +//! external cryptographic libraries and the lib itself must somehow mimic std Rust api. +//! +//! Standard way in Rust is to hash objects with a `std::hash::Hasher`, and mainly +//! that is the reason behind the choice of the abstractions: +//! +//! `Object : Hashable -> Hasher + Algorithm <- Merkle Tree` +//! +//! Custom [`merkle::hash::Hashable`] trait allows implementations differ +//! from [`std::collection`] related hashes, different implementations for +//! different hashing algorithms / schemas and conforms object-safety trait rules. +//! +//! [`Algorithm`] complements [`Hasher`] to be reusable and follows the idea +//! that the result hash is a mapping of the data stream. +//! +//! [`Algorithm.hash`] had to change its signature to be `&mut self` (`&self`) because +//! most of the cryptographic digest algorithms breaks current state on finalization +//! into unusable. `ring` libra tho contains interfaces incompatible to +//! `start-update-finish-reset` lifecycle. It requires either `cloning()` its state +//! on finalization, or `Cell`-ing via unsafe. +//! +//! Turning back to having [`Algorithm.write(&mut self, &[u8])`] instead of +//! `write(T)` allows to relax [`Algorithm`] trait [`Hasher`] constraint, even tho +//! works together well still. +//! +//! # Interface +//! +//! ```text +//! - build_tree (items) -> tree +//! - get_root -> hash +//! - gen_proof -> proof +//! - validate_proof (proof, leaf, root) -> bool +//! ``` +//! +//! # Examples +//! +//! [`test_cmh.rs`]: custom merkle hasher implementation example +//! [`crypto_bitcoin_mt.rs`]: bitcoin merkle tree using crypto lib +//! [`crypto_chaincore_mt.rs`]: chain core merkle tree using crypto lib +//! [`ring_bitcoin_mt.rs`]: bitcoin merkle tree using ring lib +//! +//! # Quick start +//! +//! ``` +//! #[cfg(feature = "chaincore")] +//! extern crate crypto; +//! extern crate merkle_light; +//! +//! #[cfg(feature = "chaincore")] +//! mod example { +//! use std::fmt; +//! use std::hash::Hasher; +//! use std::iter::FromIterator; +//! use crypto::sha3::{Sha3, Sha3Mode}; +//! use crypto::digest::Digest; +//! use merkle_light::hash::{Algorithm, Hashable}; +//! +//! pub struct ExampleAlgorithm(Sha3); +//! +//! impl ExampleAlgorithm { +//! pub fn new() -> ExampleAlgorithm { +//! ExampleAlgorithm(Sha3::new(Sha3Mode::Sha3_256)) +//! } +//! } +//! +//! impl Default for ExampleAlgorithm { +//! fn default() -> ExampleAlgorithm { +//! ExampleAlgorithm::new() +//! } +//! } +//! +//! impl Hasher for ExampleAlgorithm { +//! #[inline] +//! fn write(&mut self, msg: &[u8]) { +//! self.0.input(msg) +//! } +//! +//! #[inline] +//! fn finish(&self) -> u64 { +//! unimplemented!() +//! } +//! } +//! +//! impl Algorithm<[u8; 32]> for ExampleAlgorithm { +//! #[inline] +//! fn hash(&mut self) -> [u8; 32] { +//! let mut h = [0u8; 32]; +//! self.0.result(&mut h); +//! h +//! } +//! +//! #[inline] +//! fn reset(&mut self) { +//! self.0.reset(); +//! } +//! } +//! } +//! +//! fn main() { +//! #[cfg(feature = "chaincore")] +//! { +//! use example::ExampleAlgorithm; +//! use merkle_light::merkle::MerkleTree; +//! use std::iter::FromIterator; +//! +//! let mut h1 = [0u8; 32]; +//! let mut h2 = [0u8; 32]; +//! let mut h3 = [0u8; 32]; +//! h1[0] = 0x11; +//! h2[0] = 0x22; +//! h3[0] = 0x33; +//! +//! let t: MerkleTree<[u8; 32], ExampleAlgorithm> = MerkleTree::from_iter(vec![h1, h2, h3]); +//! println!("{:?}", t.root()); +//! } +//! } +//! ``` + +#![deny( + missing_docs, + unused_qualifications, + missing_debug_implementations, + missing_copy_implementations, + trivial_casts, + trivial_numeric_casts, + unsafe_code, + unstable_features, + unused_import_braces +)] +#![cfg_attr(not(feature = "std"), no_std)] +#![cfg_attr(feature = "nightly", allow(unstable_features))] + +/// Hash infrastructure for items in Merkle tree. +pub mod hash; + +/// Common implementations for [`Hashable`]. +mod hash_impl; + +/// Merkle tree inclusion proof +pub mod proof; + +/// Merkle tree abstractions, implementation and algorithms. +pub mod merkle; + +/// Tests data. +#[cfg(test)] +mod test_item; + +/// Tests for Merkle Hasher Customization +#[cfg(test)] +mod test_cmh; diff --git a/common/merkle_light/src/merkle.rs b/common/merkle_light/src/merkle.rs new file mode 100644 index 0000000..abd9711 --- /dev/null +++ b/common/merkle_light/src/merkle.rs @@ -0,0 +1,305 @@ +extern crate alloc; + +use crate::hash::{Algorithm, Hashable}; +use crate::proof::Proof; +use alloc::vec::Vec; +use core::iter::FromIterator; +use core::marker::PhantomData; +use core::ops; +use rayon::prelude::*; +use std::collections::BTreeMap; +use std::fmt::Debug; + +/// Merkle Tree. +/// +/// All leafs and nodes are stored in a linear array (vec). +/// +/// A merkle tree is a tree in which every non-leaf node is the hash of its +/// children nodes. A diagram depicting how it works: +/// +/// ```text +/// root = h1234 = h(h12 + h34) +/// / \ +/// h12 = h(h1 + h2) h34 = h(h3 + h4) +/// / \ / \ +/// h1 = h(tx1) h2 = h(tx2) h3 = h(tx3) h4 = h(tx4) +/// ``` +/// +/// In memory layout: +/// +/// ```text +/// [h1 h2 h3 h4 h12 h34 root] +/// ``` +/// +/// Merkle root is always the last element in the array. +/// +/// The number of inputs is not always a power of two which results in a +/// balanced tree structure as above. In that case, parent nodes with no +/// children are also zero and parent nodes with only a single left node +/// are calculated by concatenating the left node with itself before hashing. +/// Since this function uses nodes that are pointers to the hashes, empty nodes +/// will be nil. +/// +/// TODO: Ord +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct MerkleTree + Sync + Send, A: Algorithm> { + data: Vec, + leafs: usize, + height: usize, + link_map: BTreeMap, + _a: PhantomData, +} + +impl + Sync + Send, A: Algorithm> + MerkleTree +{ + /// Creates new merkle from a sequence of hashes. + pub fn new>(data: I) -> MerkleTree { + Self::from_iter(data) + } + + /// Creates new merkle tree from a list of hashable objects. + pub fn from_data, I: IntoIterator>(data: I) -> MerkleTree { + let mut a = A::default(); + Self::from_iter(data.into_iter().map(|x| { + a.reset(); + x.hash(&mut a); + a.hash() + })) + } + + fn build(&mut self) { + let mut width = self.leafs; + + // build tree + let mut layer_start: usize = 0; + let mut layer_end: usize = width; + while width > 1 { + // if there is odd num of elements, fill in a NULL. + if width & 1 == 1 { + self.data.push(Self::null_node()); + width += 1; + layer_end += 1; + } + + let layer: Vec<_> = (layer_start..layer_end) + .into_par_iter() + .step_by(2) + .map(|i| { + let mut a = A::default(); + // If the right child is not NULL, the left child is ensured to be not NULL. + let mut link_map_update = None; + let h = if self.data[i + 1] != Self::null_node() { + a.node(self.data[i].clone(), self.data[i + 1].clone()) + } else { + // If a child is NULL, the parent should be a linking node to the actual node hash. + let parent_index = (i - layer_start) / 2 + layer_end; + if self.data[i] == Self::null_node() { + // If both are NULL, the left child must be a linking node. + let linked_to = *self.link_map.get(&i).unwrap(); + link_map_update = Some((parent_index, linked_to, Some(i))); + Self::null_node() + } else { + match self.link_map.get(&(i + 1)) { + // Right child is linked to a hash, so we just compute the parent hash. + Some(index) => { + assert_ne!(self.data[*index], Self::null_node()); + a.node(self.data[i].clone(), self.data[*index].clone()) + } + // Right child is NULL, so link the parent to the left child which has a hash stored. + None => { + link_map_update = Some((parent_index, i, None)); + Self::null_node() + } + } + } + }; + (h, link_map_update) + }) + .collect(); + for (node, maybe_link_map_update) in layer { + self.data.push(node); + if let Some((from, to, maybe_remove)) = maybe_link_map_update { + self.link_map.insert(from, to); + if let Some(remove) = maybe_remove { + self.link_map.remove(&remove); + } + } + } + + layer_start = layer_end; + width >>= 1; + layer_end += width; + } + } + + /// Generate merkle tree inclusion proof for leaf `i` + pub fn gen_proof(&self, i: usize) -> Proof { + if self.leafs == 1 { + assert_eq!(i, 0); + return Proof::new(vec![self.root()], vec![]); + } + + assert!(i < self.leafs); // i in [0 .. self.leafs) + + let mut lemma: Vec = Vec::with_capacity(self.height + 1); // path + root + let mut path: Vec = Vec::with_capacity(self.height - 1); // path - 1 + + let mut base = 0; + let mut j = i; + + // level 1 width + let mut width = self.leafs; + if width & 1 == 1 { + width += 1; + } + + lemma.push(self.data[j].clone()); + while base + 1 < self.len() { + let proof_hash_index = if j & 1 == 0 { + // j is left + let right_index = base + j + 1; + if self.data[right_index] == Self::null_node() { + match self.link_map.get(&right_index) { + // A link node, so the proof uses the linked hash. + Some(index) => { + assert_ne!(self.data[*index], Self::null_node()); + Some(*index) + } + // A NULL node, just skip. + None => None, + } + } else { + Some(right_index) + } + } else { + // j is right + Some(base + j - 1) + }; + if let Some(index) = proof_hash_index { + lemma.push(self.data[index].clone()); + path.push(j & 1 == 0); + } + + base += width; + width >>= 1; + if width & 1 == 1 { + width += 1; + } + j >>= 1; + } + + // root is final + lemma.push(self.root()); + Proof::new(lemma, path) + } + + /// Returns merkle root + pub fn root(&self) -> T { + self.data[self.data.len() - 1].clone() + } + + /// Returns number of elements in the tree. + pub fn len(&self) -> usize { + self.data.len() + } + + /// Returns `true` if the vector contains no elements. + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } + + /// Returns height of the tree + pub fn height(&self) -> usize { + self.height + } + + /// Returns original number of elements the tree was built upon. + pub fn leafs(&self) -> usize { + self.leafs + } + + /// Extracts a slice containing the entire vector. + /// + /// Equivalent to `&s[..]`. + pub fn as_slice(&self) -> &[T] { + self + } + + fn null_node() -> T { + T::default() + } +} + +impl + Sync + Send, A: Algorithm> FromIterator + for MerkleTree +{ + /// Creates new merkle tree from an iterator over hashable objects. + fn from_iter>(into: I) -> Self { + let iter = into.into_iter(); + let mut data: Vec = match iter.size_hint().1 { + Some(e) => { + let pow = next_pow2(e); + let size = 2 * pow - 1; + Vec::with_capacity(size) + } + None => Vec::new(), + }; + + // leafs + let mut a = A::default(); + for item in iter { + a.reset(); + data.push(a.leaf(item)); + } + + let leafs = data.len(); + let pow = next_pow2(leafs); + let size = 2 * pow - 1; + + // assert!(leafs > 1); + + let mut mt: MerkleTree = MerkleTree { + data, + leafs, + height: log2_pow2(size + 1), + link_map: Default::default(), + _a: PhantomData, + }; + + mt.build(); + mt + } +} + +impl + Sync + Send, A: Algorithm> ops::Deref for MerkleTree { + type Target = [T]; + + fn deref(&self) -> &[T] { + self.data.deref() + } +} + +/// `next_pow2` returns next highest power of two from a given number if +/// it is not already a power of two. +/// +/// [](http://locklessinc.com/articles/next_pow2/) +/// [](https://stackoverflow.com/questions/466204/rounding-up-to-next-power-of-2/466242#466242) +pub fn next_pow2(mut n: usize) -> usize { + n -= 1; + n |= n >> 1; + n |= n >> 2; + n |= n >> 4; + n |= n >> 8; + n |= n >> 16; + #[cfg(target_pointer_width = "64")] + { + n |= n >> 32; + } + n + 1 +} + +/// find power of 2 of a number which is power of 2 +pub fn log2_pow2(n: usize) -> usize { + n.trailing_zeros() as usize +} diff --git a/common/merkle_light/src/proof.rs b/common/merkle_light/src/proof.rs new file mode 100644 index 0000000..3c5f702 --- /dev/null +++ b/common/merkle_light/src/proof.rs @@ -0,0 +1,80 @@ +extern crate alloc; + +use crate::hash::Algorithm; +use alloc::vec::Vec; + +/// Merkle tree inclusion proof for data element, for which item = Leaf(Hash(Data Item)). +/// +/// Lemma layout: +/// +/// ```text +/// [ item h1x h2y h3z ... root ] +/// ``` +/// +/// Proof validation is positioned hash against lemma path to match root hash. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Proof> { + lemma: Vec, + path: Vec, +} + +impl> Proof { + /// Creates new MT inclusion proof + pub fn new(hash: Vec, path: Vec) -> Proof { + if hash.len() > 2 { + assert_eq!(hash.len() - 2, path.len()); + } else { + assert_eq!(hash.len(), 1); + assert_eq!(path, vec![]); + } + Proof { lemma: hash, path } + } + + /// Return proof target leaf + pub fn item(&self) -> T { + self.lemma.first().unwrap().clone() + } + + /// Return tree root + pub fn root(&self) -> T { + self.lemma.last().unwrap().clone() + } + + /// Verifies MT inclusion proof + pub fn validate>(&self) -> bool { + let size = self.lemma.len(); + + // Special case for a single node. + if size == 1 && self.path == vec![] { + return true; + } + + if size < 2 { + return false; + } + + let mut h = self.item(); + let mut a = A::default(); + + for i in 1..size - 1 { + a.reset(); + h = if self.path[i - 1] { + a.node(h, self.lemma[i].clone()) + } else { + a.node(self.lemma[i].clone(), h) + }; + } + + h == self.root() + } + + /// Returns the path of this proof. + pub fn path(&self) -> &[bool] { + &self.path + } + + /// Returns the lemma of this proof. + pub fn lemma(&self) -> &[T] { + &self.lemma + } +} diff --git a/common/merkle_light/src/test_cmh.rs b/common/merkle_light/src/test_cmh.rs new file mode 100644 index 0000000..925662d --- /dev/null +++ b/common/merkle_light/src/test_cmh.rs @@ -0,0 +1,76 @@ +#![cfg(test)] + +use crate::hash::{Algorithm, Hashable}; +use crate::merkle::MerkleTree; +use crate::test_item::Item; +use std::collections::hash_map::DefaultHasher; +use std::hash::Hasher; +use std::iter::FromIterator; + +/// Custom merkle hash util test +#[derive(Debug, Clone, Default)] +struct CMH(DefaultHasher); + +impl CMH { + pub fn new() -> CMH { + CMH(DefaultHasher::new()) + } +} + +impl Hasher for CMH { + #[inline] + fn write(&mut self, msg: &[u8]) { + self.0.write(msg) + } + + #[inline] + fn finish(&self) -> u64 { + self.0.finish() + } +} + +impl Algorithm for CMH { + #[inline] + fn hash(&mut self) -> Item { + Item(self.finish()) + } + + #[inline] + fn reset(&mut self) { + *self = CMH::default() + } + + #[inline] + fn leaf(&mut self, leaf: Item) -> Item { + Item(leaf.0 & 0xff) + } + + #[inline] + fn node(&mut self, left: Item, right: Item) -> Item { + self.write(&[1u8]); + self.write(left.as_ref()); + self.write(&[2u8]); + self.write(right.as_ref()); + Item(self.hash().0 & 0xffff) + } +} + +#[test] +fn test_custom_merkle_hasher() { + let mut a = CMH::new(); + let mt: MerkleTree = MerkleTree::from_iter([1, 2, 3, 4, 5].iter().map(|x| { + a.reset(); + x.hash(&mut a); + a.hash() + })); + + assert_eq!( + mt.as_slice() + .iter() + .take(mt.leafs()) + .filter(|&&x| x.0 > 255) + .count(), + 0 + ); + assert_eq!(mt.as_slice().iter().filter(|&&x| x.0 > 65535).count(), 0); +} diff --git a/common/merkle_light/src/test_item.rs b/common/merkle_light/src/test_item.rs new file mode 100644 index 0000000..8e8c62b --- /dev/null +++ b/common/merkle_light/src/test_item.rs @@ -0,0 +1,39 @@ +#![cfg(test)] +#![allow(unsafe_code)] + +use crate::hash::{Algorithm, Hashable}; +use std::mem; +use std::slice; + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Debug)] +pub struct Item(pub u64); + +impl AsRef<[u8]> for Item { + fn as_ref(&self) -> &[u8] { + unsafe { slice::from_raw_parts(mem::transmute(&self.0), 8) } + } +} + +impl PartialEq for Item { + fn eq(&self, other: &u64) -> bool { + self.0 == *other + } +} + +impl From for Item { + fn from(x: u64) -> Self { + Item(x) + } +} + +impl From for u64 { + fn from(val: Item) -> Self { + val.0 + } +} + +impl> Hashable for Item { + fn hash(&self, state: &mut A) { + state.write_u64(self.0) + } +} diff --git a/common/merkle_tree/Cargo.toml b/common/merkle_tree/Cargo.toml new file mode 100644 index 0000000..463bb63 --- /dev/null +++ b/common/merkle_tree/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "merkle_tree" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +merkle_light = { path = "../../common/merkle_light" } +tiny-keccak = { version = "2.0.2", features = ["keccak"] } + +[dev-dependencies] +hex = "0.4.3" \ No newline at end of file diff --git a/common/merkle_tree/src/lib.rs b/common/merkle_tree/src/lib.rs new file mode 100644 index 0000000..db984eb --- /dev/null +++ b/common/merkle_tree/src/lib.rs @@ -0,0 +1,88 @@ +use merkle_light::hash::Algorithm; +use std::hash::Hasher; +use tiny_keccak::{Hasher as KeccakHasher, Keccak}; + +// TODO: Option here is only used for compatibility with `tiny_keccak` and `merkle_light`. +#[derive(Clone)] +pub struct RawLeafSha3Algorithm(Option); + +impl RawLeafSha3Algorithm { + fn new() -> RawLeafSha3Algorithm { + RawLeafSha3Algorithm(Some(Keccak::v256())) + } +} + +impl Default for RawLeafSha3Algorithm { + fn default() -> RawLeafSha3Algorithm { + RawLeafSha3Algorithm::new() + } +} + +impl Hasher for RawLeafSha3Algorithm { + #[inline] + fn write(&mut self, msg: &[u8]) { + self.0.as_mut().unwrap().update(msg) + } + + #[inline] + fn finish(&self) -> u64 { + unimplemented!() + } +} + +pub type CryptoSHA256Hash = [u8; 32]; + +impl Algorithm for RawLeafSha3Algorithm { + #[inline] + fn hash(&mut self) -> CryptoSHA256Hash { + let mut h = [0u8; 32]; + self.0.take().unwrap().finalize(&mut h); + h + } + + fn leaf(&mut self, leaf: CryptoSHA256Hash) -> CryptoSHA256Hash { + // Leave the leaf node untouched so we can save the subtree root + // just as the leaf node for the top tree. + // `LEAF` is prepended for `Chunk` hash computation. + leaf + } + + #[inline] + fn node(&mut self, left: CryptoSHA256Hash, right: CryptoSHA256Hash) -> CryptoSHA256Hash { + self.write(left.as_ref()); + self.write(right.as_ref()); + self.hash() + } +} + +#[cfg(test)] +mod tests { + use crate::RawLeafSha3Algorithm; + use merkle_light::{hash::Algorithm, merkle::MerkleTree}; + use std::hash::Hasher; + + #[test] + fn test_root() { + let results = [ + [ + 86, 124, 71, 168, 121, 121, 77, 212, 137, 162, 16, 222, 193, 125, 49, 204, 89, 25, + 188, 66, 125, 19, 141, 113, 106, 129, 7, 224, 37, 226, 219, 203, + ], + [ + 41, 66, 83, 171, 49, 203, 249, 13, 187, 190, 247, 85, 167, 95, 241, 96, 29, 167, + 144, 227, 92, 54, 95, 83, 14, 124, 26, 28, 169, 4, 220, 248, + ], + ]; + for (test_index, n_chunk) in [6, 7].into_iter().enumerate() { + let mut data = Vec::with_capacity(n_chunk); + for _ in 0..n_chunk { + let mut a = RawLeafSha3Algorithm::default(); + a.write(&[0; 256]); + data.push(a.hash()); + } + let mt = MerkleTree::<_, RawLeafSha3Algorithm>::new(data); + println!("{:?} {}", mt.root(), hex::encode(mt.root())); + assert_eq!(results[test_index], mt.root()); + } + } +} diff --git a/common/spec/Cargo.toml b/common/spec/Cargo.toml new file mode 100644 index 0000000..18e6ec8 --- /dev/null +++ b/common/spec/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "zgs_spec" +version = "0.1.0" +edition = "2021" + +[dependencies] \ No newline at end of file diff --git a/common/spec/src/lib.rs b/common/spec/src/lib.rs new file mode 100644 index 0000000..ed3ca68 --- /dev/null +++ b/common/spec/src/lib.rs @@ -0,0 +1,18 @@ +pub const KB: usize = 1024; +pub const MB: usize = 1024 * KB; +pub const GB: usize = 1024 * MB; +pub const TB: usize = 1024 * GB; + +pub const BYTES_PER_SECTOR: usize = 256; +pub const BYTES_PER_SEAL: usize = 4 * KB; +pub const BYTES_PER_SCRATCHPAD: usize = 64 * KB; +pub const BYTES_PER_LOAD: usize = 256 * KB; +pub const BYTES_PER_PRICING: usize = 8 * GB; +pub const BYTES_PER_MAX_MINING_RANGE: usize = 8 * TB; + +pub const SECTORS_PER_LOAD: usize = BYTES_PER_LOAD / BYTES_PER_SECTOR; +pub const SECTORS_PER_SEAL: usize = BYTES_PER_SEAL / BYTES_PER_SECTOR; +pub const SECTORS_PER_PRICING: usize = BYTES_PER_PRICING / BYTES_PER_SECTOR; +pub const SECTORS_PER_MAX_MINING_RANGE: usize = BYTES_PER_MAX_MINING_RANGE / BYTES_PER_SECTOR; + +pub const SEALS_PER_LOAD: usize = BYTES_PER_LOAD / BYTES_PER_SEAL; diff --git a/common/task_executor/Cargo.toml b/common/task_executor/Cargo.toml new file mode 100644 index 0000000..c4c0b44 --- /dev/null +++ b/common/task_executor/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "task_executor" +version = "0.1.0" +authors = ["Sigma Prime "] +edition = "2021" + +[dependencies] +exit-future = "0.2.0" +futures = "0.3.21" +lazy_static = "1.4.0" +lighthouse_metrics = { path = "../lighthouse_metrics" } +tokio = { version = "1.19.2", features = ["rt"] } +tracing = "0.1.35" diff --git a/common/task_executor/src/lib.rs b/common/task_executor/src/lib.rs new file mode 100644 index 0000000..1a7490c --- /dev/null +++ b/common/task_executor/src/lib.rs @@ -0,0 +1,369 @@ +#[macro_use] +extern crate tracing; + +mod metrics; +pub mod test_utils; + +use futures::channel::mpsc::Sender; +use futures::prelude::*; +use std::sync::Weak; +use tokio::runtime::{Handle, Runtime}; + +pub use tokio::task::JoinHandle; + +/// Provides a reason when node is shut down. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum ShutdownReason { + /// The node shut down successfully. + Success(&'static str), + /// The node shut down due to an error condition. + Failure(&'static str), +} + +impl ShutdownReason { + pub fn message(&self) -> &'static str { + match self { + ShutdownReason::Success(msg) => msg, + ShutdownReason::Failure(msg) => msg, + } + } +} + +/// Provides a `Handle` by either: +/// +/// 1. Holding a `Weak` and calling `Runtime::handle`. +/// 2. Directly holding a `Handle` and cloning it. +/// +/// This enum allows the `TaskExecutor` to work in production where a `Weak` is directly +/// accessible and in testing where the `Runtime` is hidden outside our scope. +#[derive(Clone)] +pub enum HandleProvider { + Runtime(Weak), + Handle(Handle), +} + +impl From for HandleProvider { + fn from(handle: Handle) -> Self { + HandleProvider::Handle(handle) + } +} + +impl From> for HandleProvider { + fn from(weak_runtime: Weak) -> Self { + HandleProvider::Runtime(weak_runtime) + } +} + +impl HandleProvider { + /// Returns a `Handle` to a `Runtime`. + /// + /// May return `None` if the weak reference to the `Runtime` has been dropped (this generally + /// means Lighthouse is shutting down). + pub fn handle(&self) -> Option { + match self { + HandleProvider::Runtime(weak_runtime) => weak_runtime + .upgrade() + .map(|runtime| runtime.handle().clone()), + HandleProvider::Handle(handle) => Some(handle.clone()), + } + } +} + +/// A wrapper over a runtime handle which can spawn async and blocking tasks. +#[derive(Clone)] +pub struct TaskExecutor { + /// The handle to the runtime on which tasks are spawned + handle_provider: HandleProvider, + /// The receiver exit future which on receiving shuts down the task + exit: exit_future::Exit, + /// Sender given to tasks, so that if they encounter a state in which execution cannot + /// continue they can request that everything shuts down. + /// + /// The task must provide a reason for shutting down. + signal_tx: Sender, +} + +impl TaskExecutor { + /// Create a new task executor. + /// + /// ## Note + /// + /// This function should only be used during testing. In production, prefer to obtain an + /// instance of `Self` via a `environment::RuntimeContext` (see the `lighthouse/environment` + /// crate). + pub fn new>( + handle: T, + exit: exit_future::Exit, + signal_tx: Sender, + ) -> Self { + Self { + handle_provider: handle.into(), + exit, + signal_tx, + } + } + + /// Clones the task executor adding a service name. + pub fn clone_with_name(&self) -> Self { + TaskExecutor { + handle_provider: self.handle_provider.clone(), + exit: self.exit.clone(), + signal_tx: self.signal_tx.clone(), + } + } + + /// A convenience wrapper for `Self::spawn` which ignores a `Result` as long as both `Ok`/`Err` + /// are of type `()`. + /// + /// The purpose of this function is to create a compile error if some function which previously + /// returned `()` starts returning something else. Such a case may otherwise result in + /// accidental error suppression. + pub fn spawn_ignoring_error( + &self, + task: impl Future> + Send + 'static, + name: &'static str, + ) { + self.spawn(task.map(|_| ()), name) + } + + /// Spawn a task to monitor the completion of another task. + /// + /// If the other task exits by panicking, then the monitor task will shut down the executor. + fn spawn_monitor( + &self, + task_handle: impl Future> + Send + 'static, + name: &'static str, + ) { + let mut shutdown_sender = self.shutdown_sender(); + + if let Some(handle) = self.handle() { + handle.spawn(async move { + let timer = metrics::start_timer_vec(&metrics::TASKS_HISTOGRAM, &[name]); + if let Err(join_error) = task_handle.await { + if let Ok(panic) = join_error.try_into_panic() { + let message = panic.downcast_ref::<&str>().unwrap_or(&""); + + error!( + task_name = name, + message = message, + advice = "Please check above for a backtrace and notify \ + the developers", + "Task panic. This is a bug!", + ); + let _ = shutdown_sender + .try_send(ShutdownReason::Failure("Panic (fatal error)")); + } + } + drop(timer); + }); + } else { + debug!("Couldn't spawn monitor task. Runtime shutting down") + } + } + + /// Spawn a future on the tokio runtime. + /// + /// The future is wrapped in an `exit_future::Exit`. The task is cancelled when the corresponding + /// exit_future `Signal` is fired/dropped. + /// + /// The future is monitored via another spawned future to ensure that it doesn't panic. In case + /// of a panic, the executor will be shut down via `self.signal_tx`. + /// + /// This function generates prometheus metrics on number of tasks and task duration. + pub fn spawn(&self, task: impl Future + Send + 'static, name: &'static str) { + if let Some(task_handle) = self.spawn_handle(task, name) { + self.spawn_monitor(task_handle, name) + } + } + + /// Spawn a future on the tokio runtime. This function does not wrap the task in an `exit_future::Exit` + /// like [spawn](#method.spawn). + /// The caller of this function is responsible for wrapping up the task with an `exit_future::Exit` to + /// ensure that the task gets canceled appropriately. + /// This function generates prometheus metrics on number of tasks and task duration. + /// + /// This is useful in cases where the future to be spawned needs to do additional cleanup work when + /// the task is completed/canceled (e.g. writing local variables to disk) or the task is created from + /// some framework which does its own cleanup (e.g. a hyper server). + pub fn spawn_without_exit( + &self, + task: impl Future + Send + 'static, + name: &'static str, + ) { + if let Some(int_gauge) = metrics::get_int_gauge(&metrics::ASYNC_TASKS_COUNT, &[name]) { + let int_gauge_1 = int_gauge.clone(); + let future = task.then(move |_| { + int_gauge_1.dec(); + futures::future::ready(()) + }); + + int_gauge.inc(); + if let Some(handle) = self.handle() { + handle.spawn(future); + } else { + debug!("Couldn't spawn task. Runtime shutting down"); + } + } + } + + /// Spawn a blocking task on a dedicated tokio thread pool wrapped in an exit future. + /// This function generates prometheus metrics on number of tasks and task duration. + pub fn spawn_blocking(&self, task: F, name: &'static str) + where + F: FnOnce() + Send + 'static, + { + if let Some(task_handle) = self.spawn_blocking_handle(task, name) { + self.spawn_monitor(task_handle, name) + } + } + + /// Spawn a future on the tokio runtime wrapped in an `exit_future::Exit` returning an optional + /// join handle to the future. + /// The task is canceled when the corresponding exit_future `Signal` is fired/dropped. + /// + /// This function generates prometheus metrics on number of tasks and task duration. + pub fn spawn_handle( + &self, + task: impl Future + Send + 'static, + name: &'static str, + ) -> Option>> { + let exit = self.exit.clone(); + + if let Some(int_gauge) = metrics::get_int_gauge(&metrics::ASYNC_TASKS_COUNT, &[name]) { + // Task is shutdown before it completes if `exit` receives + let int_gauge_1 = int_gauge.clone(); + let future = future::select(Box::pin(task), exit).then(move |either| { + let result = match either { + future::Either::Left((value, _)) => { + trace!(task = name, "Async task completed"); + Some(value) + } + future::Either::Right(_) => { + debug!(task = name, "Async task shutdown, exit received"); + None + } + }; + int_gauge_1.dec(); + futures::future::ready(result) + }); + + int_gauge.inc(); + if let Some(handle) = self.handle() { + Some(handle.spawn(future)) + } else { + debug!("Couldn't spawn task. Runtime shutting down"); + None + } + } else { + None + } + } + + /// Spawn a blocking task on a dedicated tokio thread pool wrapped in an exit future returning + /// a join handle to the future. + /// If the runtime doesn't exist, this will return None. + /// The Future returned behaves like the standard JoinHandle which can return an error if the + /// task failed. + /// This function generates prometheus metrics on number of tasks and task duration. + pub fn spawn_blocking_handle( + &self, + task: F, + name: &'static str, + ) -> Option>> + where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, + { + let timer = metrics::start_timer_vec(&metrics::BLOCKING_TASKS_HISTOGRAM, &[name]); + metrics::inc_gauge_vec(&metrics::BLOCKING_TASKS_COUNT, &[name]); + + let join_handle = if let Some(handle) = self.handle() { + handle.spawn_blocking(task) + } else { + debug!("Couldn't spawn task. Runtime shutting down"); + return None; + }; + + let future = async move { + let result = match join_handle.await { + Ok(result) => { + trace!(task = name, "Blocking task completed"); + Ok(result) + } + Err(e) => { + debug!(error = %e, "Blocking task ended unexpectedly"); + Err(e) + } + }; + drop(timer); + metrics::dec_gauge_vec(&metrics::BLOCKING_TASKS_COUNT, &[name]); + result + }; + + Some(future) + } + + /// Block the current (non-async) thread on the completion of some future. + /// + /// ## Warning + /// + /// This method is "dangerous" since calling it from an async thread will result in a panic! Any + /// use of this outside of testing should be very deeply considered as Lighthouse has been + /// burned by this function in the past. + /// + /// Determining what is an "async thread" is rather challenging; just because a function isn't + /// marked as `async` doesn't mean it's not being called from an `async` function or there isn't + /// a `tokio` context present in the thread-local storage due to some `rayon` funkiness. Talk to + /// @paulhauner if you plan to use this function in production. He has put metrics in here to + /// track any use of it, so don't think you can pull a sneaky one on him. + pub fn block_on_dangerous( + &self, + future: F, + name: &'static str, + ) -> Option { + let timer = metrics::start_timer_vec(&metrics::BLOCK_ON_TASKS_HISTOGRAM, &[name]); + metrics::inc_gauge_vec(&metrics::BLOCK_ON_TASKS_COUNT, &[name]); + let handle = self.handle()?; + let exit = self.exit.clone(); + + debug!(name = "name", "Starting block_on task"); + + handle.block_on(async { + let output = tokio::select! { + output = future => { + debug!( + name = "name", + "Completed block_on task" + ); + Some(output) + }, + _ = exit => { + debug!( + name = "name", + "Cancelled block_on task" + + ); + None + } + }; + metrics::dec_gauge_vec(&metrics::BLOCK_ON_TASKS_COUNT, &[name]); + drop(timer); + output + }) + } + + /// Returns a `Handle` to the current runtime. + pub fn handle(&self) -> Option { + self.handle_provider.handle() + } + + /// Returns a copy of the `exit_future::Exit`. + pub fn exit(&self) -> exit_future::Exit { + self.exit.clone() + } + + /// Get a channel to request shutting down. + pub fn shutdown_sender(&self) -> Sender { + self.signal_tx.clone() + } +} diff --git a/common/task_executor/src/metrics.rs b/common/task_executor/src/metrics.rs new file mode 100644 index 0000000..6ecea86 --- /dev/null +++ b/common/task_executor/src/metrics.rs @@ -0,0 +1,36 @@ +/// Handles async task metrics +use lazy_static::lazy_static; +pub use lighthouse_metrics::*; + +lazy_static! { + pub static ref ASYNC_TASKS_COUNT: Result = try_create_int_gauge_vec( + "async_tasks_count", + "Total number of async tasks spawned using spawn", + &["async_task_count"] + ); + pub static ref BLOCKING_TASKS_COUNT: Result = try_create_int_gauge_vec( + "blocking_tasks_count", + "Total number of async tasks spawned using spawn_blocking", + &["blocking_task_count"] + ); + pub static ref BLOCKING_TASKS_HISTOGRAM: Result = try_create_histogram_vec( + "blocking_tasks_histogram", + "Time taken by blocking tasks", + &["blocking_task_hist"] + ); + pub static ref BLOCK_ON_TASKS_COUNT: Result = try_create_int_gauge_vec( + "block_on_tasks_count", + "Total number of block_on_dangerous tasks spawned", + &["name"] + ); + pub static ref BLOCK_ON_TASKS_HISTOGRAM: Result = try_create_histogram_vec( + "block_on_tasks_histogram", + "Time taken by block_on_dangerous tasks", + &["name"] + ); + pub static ref TASKS_HISTOGRAM: Result = try_create_histogram_vec( + "async_tasks_time_histogram", + "Time taken by async tasks", + &["async_task_hist"] + ); +} diff --git a/common/task_executor/src/test_utils.rs b/common/task_executor/src/test_utils.rs new file mode 100644 index 0000000..137e2c1 --- /dev/null +++ b/common/task_executor/src/test_utils.rs @@ -0,0 +1,51 @@ +use crate::TaskExecutor; +use std::sync::Arc; +use tokio::runtime; + +/// Whilst the `TestRuntime` is not necessarily useful in itself, it provides the necessary +/// components for creating a `TaskExecutor` during tests. +/// +/// May create its own runtime or use an existing one. +/// +/// ## Warning +/// +/// This struct should never be used in production, only testing. +pub struct TestRuntime { + runtime: Option>, + _runtime_shutdown: exit_future::Signal, + pub task_executor: TaskExecutor, +} + +impl Default for TestRuntime { + /// If called *inside* an existing runtime, instantiates `Self` using a handle to that runtime. If + /// called *outside* any existing runtime, create a new `Runtime` and keep it alive until the + /// `Self` is dropped. + fn default() -> Self { + let (runtime_shutdown, exit) = exit_future::signal(); + let (shutdown_tx, _) = futures::channel::mpsc::channel(1); + + let runtime = Arc::new( + runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(), + ); + let handle = runtime.handle().clone(); + + let task_executor = TaskExecutor::new(handle, exit, shutdown_tx); + + Self { + runtime: Some(runtime), + _runtime_shutdown: runtime_shutdown, + task_executor, + } + } +} + +impl Drop for TestRuntime { + fn drop(&mut self) { + if let Some(runtime) = self.runtime.take() { + Arc::try_unwrap(runtime).unwrap().shutdown_background() + } + } +} diff --git a/common/unused_port/Cargo.toml b/common/unused_port/Cargo.toml new file mode 100644 index 0000000..06c1ca8 --- /dev/null +++ b/common/unused_port/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "unused_port" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/common/unused_port/src/lib.rs b/common/unused_port/src/lib.rs new file mode 100644 index 0000000..4a8cf17 --- /dev/null +++ b/common/unused_port/src/lib.rs @@ -0,0 +1,55 @@ +use std::net::{TcpListener, UdpSocket}; + +#[derive(Copy, Clone)] +pub enum Transport { + Tcp, + Udp, +} + +/// A convenience function for `unused_port(Transport::Tcp)`. +pub fn unused_tcp_port() -> Result { + unused_port(Transport::Tcp) +} + +/// A convenience function for `unused_port(Transport::Tcp)`. +pub fn unused_udp_port() -> Result { + unused_port(Transport::Udp) +} + +/// A bit of hack to find an unused port. +/// +/// Does not guarantee that the given port is unused after the function exits, just that it was +/// unused before the function started (i.e., it does not reserve a port). +/// +/// ## Notes +/// +/// It is possible that users are unable to bind to the ports returned by this function as the OS +/// has a buffer period where it doesn't allow binding to the same port even after the socket is +/// closed. We might have to use SO_REUSEADDR socket option from `std::net2` crate in that case. +pub fn unused_port(transport: Transport) -> Result { + let local_addr = match transport { + Transport::Tcp => { + let listener = TcpListener::bind("127.0.0.1:0").map_err(|e| { + format!("Failed to create TCP listener to find unused port: {:?}", e) + })?; + listener.local_addr().map_err(|e| { + format!( + "Failed to read TCP listener local_addr to find unused port: {:?}", + e + ) + })? + } + Transport::Udp => { + let socket = UdpSocket::bind("127.0.0.1:0") + .map_err(|e| format!("Failed to create UDP socket to find unused port: {:?}", e))?; + socket.local_addr().map_err(|e| { + format!( + "Failed to read UDP socket local_addr to find unused port: {:?}", + e + ) + })? + } + }; + + Ok(local_addr.port()) +} diff --git a/common/zgs_seal/Cargo.toml b/common/zgs_seal/Cargo.toml new file mode 100644 index 0000000..f2d1960 --- /dev/null +++ b/common/zgs_seal/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "zgs_seal" +version = "0.1.0" +edition = "2021" + +[dependencies] +ethereum-types = "0.14" +zgs_spec = {path = "../spec"} +tiny-keccak = { version = "2.0.2", features = ["keccak"] } \ No newline at end of file diff --git a/common/zgs_seal/src/lib.rs b/common/zgs_seal/src/lib.rs new file mode 100644 index 0000000..a33ee42 --- /dev/null +++ b/common/zgs_seal/src/lib.rs @@ -0,0 +1,62 @@ +use ethereum_types::H256; +use tiny_keccak::{Hasher, Keccak}; + +pub fn compute_first_mask_seed( + miner_id: &H256, + context_digest: &H256, + start_sector: u64, +) -> [u8; 96] { + let mut output = [0u8; 96]; + output[0..32].copy_from_slice(&miner_id.0); + output[32..64].copy_from_slice(&context_digest.0); + output[88..96].clone_from_slice(&start_sector.to_be_bytes()); + output +} + +fn keccak(input: impl AsRef<[u8]>) -> [u8; 32] { + let mut hasher = Keccak::v256(); + let mut output = [0u8; 32]; + hasher.update(input.as_ref()); + hasher.finalize(&mut output); + output +} + +pub fn seal_with_mask(data: &mut [u8], first_mask: [u8; 32]) { + assert!(data.len() % 32 == 0); + let mut mask = first_mask; + for word in data.chunks_mut(32) { + word.iter_mut().zip(mask.iter()).for_each(|(x, y)| *x ^= *y); + mask = keccak(&*word); + } +} + +pub fn unseal_with_mask(data: &mut [u8], first_mask: [u8; 32]) { + assert!(data.len() % 32 == 0); + + let mut mask = first_mask; + data.chunks_exact_mut(32).for_each(|x| { + let next_mask = keccak(&*x); + x.iter_mut() + .zip(mask.iter()) + .for_each(|(x, mask)| *x ^= *mask); + mask = next_mask; + }) +} + +pub fn seal_with_mask_seed(data: &mut [u8], first_mask_seed: impl AsRef<[u8]>) { + seal_with_mask(data, keccak(first_mask_seed)) +} + +pub fn unseal_with_mask_seed(data: &mut [u8], first_mask_seed: impl AsRef<[u8]>) { + unseal_with_mask(data, keccak(first_mask_seed)) +} + +pub fn seal(data: &mut [u8], miner_id: &H256, context_digest: &H256, start_sector: u64) { + let first_mask_seed = compute_first_mask_seed(miner_id, context_digest, start_sector); + seal_with_mask_seed(data, first_mask_seed) +} + +pub fn unseal(data: &mut [u8], miner_id: &H256, context_digest: &H256, start_sector: u64) { + let first_mask_seed = compute_first_mask_seed(miner_id, context_digest, start_sector); + unseal_with_mask_seed(data, first_mask_seed) +} diff --git a/common/zgs_version/Cargo.toml b/common/zgs_version/Cargo.toml new file mode 100644 index 0000000..f3f2049 --- /dev/null +++ b/common/zgs_version/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "zgs_version" +version = "0.1.0" +authors = ["Paul Hauner "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +git-version = "0.3.5" +target_info = "0.1.0" + +[dev-dependencies] +regex = "1.5.6" diff --git a/common/zgs_version/src/lib.rs b/common/zgs_version/src/lib.rs new file mode 100644 index 0000000..e0c7d5e --- /dev/null +++ b/common/zgs_version/src/lib.rs @@ -0,0 +1,47 @@ +use git_version::git_version; +use target_info::Target; + +/// Returns the current version of this build of Lighthouse. +/// +/// A plus-sign (`+`) is appended to the git commit if the tree is dirty. +/// +/// ## Example +/// +/// `Lighthouse/v1.5.1-67da032+` +pub const VERSION: &str = git_version!( + args = [ + "--always", + "--dirty=+", + "--abbrev=7", + // NOTE: using --match instead of --exclude for compatibility with old Git + "--match=thiswillnevermatchlol" + ], + prefix = "zgs/v0.0.1-", + fallback = "unknown" +); + +/// Returns `VERSION`, but with platform information appended to the end. +/// +/// ## Example +/// +/// `zgs/v0.0.1-67da032+/x86_64-linux` +pub fn version_with_platform() -> String { + format!("{}/{}-{}", VERSION, Target::arch(), Target::os()) +} + +#[cfg(test)] +mod test { + use super::*; + use regex::Regex; + + #[test] + fn version_formatting() { + let re = + Regex::new(r"^zgs/v[0-9]+\.[0-9]+\.[0-9]+(-rc.[0-9])?-[[:xdigit:]]{7}\+?$").unwrap(); + assert!( + re.is_match(VERSION), + "version doesn't match regex: {}", + VERSION + ); + } +} diff --git a/doc/install.md b/doc/install.md new file mode 100644 index 0000000..d1207ac --- /dev/null +++ b/doc/install.md @@ -0,0 +1,70 @@ +# Install +ZeroGStorage requires Rust 1.71.0 and Go to build. + +## Install Rust + +We recommend installing Rust through [rustup](https://www.rustup.rs/). + +* Linux + + Install Rust + ```shell + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + rustup install 1.65.0 + ``` + + Other dependencies + * Ubuntu + ```shell + sudo apt-get install clang cmake build-essential + ``` + +* Mac + + Install Rust + ```shell + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + rustup install 1.65.0 + ``` + + ```shell + brew install llvm cmake + ``` + +* Windows + Download and run the rustup installer from [this link](https://static.rust-lang.org/rustup/dist/i686-pc-windows-gnu/rustup-init.exe). + Install LLVM, pre-built binaries can be downloaded from [this link](https://releases.llvm.org/download.html). + +## Install Go +* Linux + ```shell + # Download the Go installer + wget https://go.dev/dl/go1.19.3.linux-amd64.tar.gz + + # Extract the archive + sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.19.3.linux-amd64.tar.gz + + # Add /usr/local/go/bin to the PATH environment variable by adding the following line to your ~/.profile. + export PATH=$PATH:/usr/local/go/bin + ``` + +* Mac + + Download the Go installer from https://go.dev/dl/go1.19.3.darwin-amd64.pkg. + Open the package file you downloaded and follow the prompts to install Go. + +* Windows + Download the Go installer from https://go.dev/dl/go1.19.3.windows-amd64.msi. + Open the MSI file you downloaded and follow the prompts to install Go. + + +## Build from source +```shell +# Download code +$ git clone https://github.com/zero-gravity-labs/zerog-storage-rust.git +$ cd zerog-storage-rust +$ git submodule update --init + +# Build in release mode +$ cargo build --release +``` diff --git a/doc/run.md b/doc/run.md new file mode 100644 index 0000000..3c2e4d7 --- /dev/null +++ b/doc/run.md @@ -0,0 +1,91 @@ +# Run + +## Deploy contract: Token, Flow and Mine contracts + +### Setup Environment + +Install dependencies Node.js, yarn, hardhat. + +* Linux + * Ubuntu + ```shell + # node >=12.18 + sudo apt install npm + sudo npm install --global yarn + sudo npm install --global hardhat + ``` + +* Mac + ```shell + brew install node + sudo npm install --global yarn + sudo npm install --global hardhat + ``` + +* Windows +Download and install node from [here](https://nodejs.org/en/download/) + ```shell + npm install --global yarn + npm install --global hardhat + ``` + +### Download contract source code +```shell +git clone https://github.com/zero-gravity-labs/zerog-storage-contracts.git +cd zerog-storage-contracts +``` + +Add target network to your hardhat.config.js, i.e. +```shell +# example +networks: { + targetNetwork: { + url: "******", + accounts: [ + "******", + ], + }, + }, +``` + +### Compile +```shell +yarn +yarn compile +``` + +### Deploy contract +```shell +npx hardhat run scripts/deploy.ts --network targetnetwork +``` + +Keep contracts addresses + +## Run ZeroGStorage +Update coinfig run/config.toml as required: + +```shell +# p2p port +network_libp2p_port + +# rpc endpoint +rpc_listen_address + +# peer nodes +network_libp2p_nodes + +# flow contract address +log_contract_address + +# mine contract address +mine_contract_address + +# layer one blockchain rpc endpoint +blockchain_rpc_endpoint +``` + +Run node +```shell +cd run +../target/release/zgs_node --config config.toml +``` diff --git a/node/.gitignore b/node/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/node/.gitignore @@ -0,0 +1 @@ +/target diff --git a/node/Cargo.toml b/node/Cargo.toml new file mode 100644 index 0000000..e7bc4d1 --- /dev/null +++ b/node/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "zgs_node" +version = "0.1.0" +edition = "2021" +build = "build.rs" + +[dependencies] +anyhow = { version = "=1.0.58", features = ["backtrace"] } +clap = { version = "3.2.5", features = ["cargo"] } +ctrlc = "3.2.2" +error-chain = "0.12.4" +ethereum-types = "0.14" +exit-future = "0.2.0" +futures = "0.3.21" +file_location_cache = { path = "file_location_cache" } +zgs_version = { path = "../common/zgs_version" } +log_entry_sync = { path = "./log_entry_sync" } +miner = { path = "./miner" } +network = { path = "./network" } +router = { path = "./router" } +rpc = { path = "./rpc" } +shared_types = { path = "./shared_types" } +storage = { path = "./storage" } +storage-async = { path = "./storage-async" } +sync = { path = "./sync" } +task_executor = { path = "../common/task_executor" } +tokio = { version = "1.19.2", features = ["full"] } +tokio-stream = { version = "0.1.9", features = ["sync"] } +toml = "0.5.9" +tracing = "0.1.35" +tracing-subscriber = { version = "0.3.11", features = ["env-filter"] } +tracing-appender = { version = "0.2.2" } +chunk_pool = { path = "./chunk_pool" } +itertools = "0.10.5" +serde = { version = "1.0.137", features = ["derive"] } +duration-str = "0.5.1" +config = "0.13.1" + +[dependencies.libp2p] +version = "0.45.1" +default-features = true +features = ["websocket", "identify", "mplex", "yamux", "noise", "gossipsub", "dns-tokio", "tcp-tokio", "plaintext", "secp256k1"] diff --git a/node/build.rs b/node/build.rs new file mode 100644 index 0000000..d6f58bb --- /dev/null +++ b/node/build.rs @@ -0,0 +1,13 @@ +use std::process::Command; + +fn main() { + println!("cargo:rerun-if-changed=../zerog-storage-client"); + + let status = Command::new("go") + .current_dir("../zerog-storage-client") + .args(vec!["build", "-o", "../target"]) + .status() + .unwrap(); + + println!("build zerog-storage-client with status {}", status); +} diff --git a/node/chunk_pool/Cargo.toml b/node/chunk_pool/Cargo.toml new file mode 100644 index 0000000..ab18769 --- /dev/null +++ b/node/chunk_pool/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "chunk_pool" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { version = "=1.0.58", features = ["backtrace"] } +shared_types = { path = "../shared_types" } +storage-async = { path = "../storage-async" } +log_entry_sync = { path = "../log_entry_sync" } +network = { path = "../network" } +tokio = { version = "1.19.2", features = ["sync"] } +async-lock = "2.5.0" +hashlink = "0.8.0" +tracing = "0.1.35" diff --git a/node/chunk_pool/src/handler.rs b/node/chunk_pool/src/handler.rs new file mode 100644 index 0000000..1966e7f --- /dev/null +++ b/node/chunk_pool/src/handler.rs @@ -0,0 +1,93 @@ +use super::mem_pool::MemoryChunkPool; +use crate::mem_pool::FileID; +use anyhow::Result; +use network::NetworkMessage; +use shared_types::ChunkArray; +use std::sync::Arc; +use storage_async::Store; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; + +/// Handle the cached file when uploaded completely and verified from blockchain. +/// Generally, the file will be persisted into log store. +pub struct ChunkPoolHandler { + receiver: UnboundedReceiver, + mem_pool: Arc, + log_store: Store, + sender: UnboundedSender, +} + +impl ChunkPoolHandler { + pub(crate) fn new( + receiver: UnboundedReceiver, + mem_pool: Arc, + log_store: Store, + sender: UnboundedSender, + ) -> Self { + ChunkPoolHandler { + receiver, + mem_pool, + log_store, + sender, + } + } + + /// Writes memory cached chunks into store and finalize transaction. + /// Note, a separate thread should be spawned to call this method. + pub async fn handle(&mut self) -> Result { + let id = match self.receiver.recv().await { + Some(id) => id, + None => return Ok(false), + }; + + debug!(?id, "Received task to finalize transaction"); + + // TODO(qhz): remove from memory pool after transaction finalized, + // when store support to write chunks with reference. + if let Some(file) = self.mem_pool.remove_cached_file(&id.root).await { + // If there is still cache of chunks, write them into store + let mut segments: Vec = file.segments.into_values().collect(); + while let Some(seg) = segments.pop() { + if !self + .log_store + .put_chunks_with_tx_hash(id.tx_id.seq, id.tx_id.hash, seg) + .await? + { + return Ok(false); + } + } + } + + if !self + .log_store + .finalize_tx_with_hash(id.tx_id.seq, id.tx_id.hash) + .await? + { + return Ok(false); + } + + debug!(?id, "Transaction finalized"); + + // always remove file from pool after transaction finalized + self.mem_pool.remove_file(&id.root).await; + + let msg = NetworkMessage::AnnounceLocalFile { tx_id: id.tx_id }; + if let Err(e) = self.sender.send(msg) { + error!( + "Failed to send NetworkMessage::AnnounceLocalFile message, tx_seq={}, err={}", + id.tx_id.seq, e + ); + } + + Ok(true) + } + + pub async fn run(mut self) { + info!("Worker started to finalize transactions"); + + loop { + if let Err(e) = self.handle().await { + warn!("Failed to write chunks or finalize transaction, {:?}", e); + } + } + } +} diff --git a/node/chunk_pool/src/lib.rs b/node/chunk_pool/src/lib.rs new file mode 100644 index 0000000..1e92d8b --- /dev/null +++ b/node/chunk_pool/src/lib.rs @@ -0,0 +1,38 @@ +#[macro_use] +extern crate tracing; + +mod handler; +mod mem_pool; + +pub use handler::ChunkPoolHandler; +pub use mem_pool::{FileID, MemoryChunkPool, SegmentInfo}; + +use std::sync::Arc; +use std::time::Duration; + +#[derive(Clone, Copy, Debug)] +pub struct Config { + pub write_window_size: usize, + pub max_cached_chunks_all: usize, + pub max_writings: usize, + pub expiration_time_secs: u64, +} + +impl Config { + pub fn expiration_time(&self) -> Duration { + Duration::from_secs(self.expiration_time_secs) + } +} + +pub fn unbounded( + config: Config, + log_store: storage_async::Store, + network_send: tokio::sync::mpsc::UnboundedSender, +) -> (Arc, ChunkPoolHandler) { + let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); + + let mem_pool = Arc::new(MemoryChunkPool::new(config, log_store.clone(), sender)); + let handler = ChunkPoolHandler::new(receiver, mem_pool.clone(), log_store, network_send); + + (mem_pool, handler) +} diff --git a/node/chunk_pool/src/mem_pool/chunk_cache.rs b/node/chunk_pool/src/mem_pool/chunk_cache.rs new file mode 100644 index 0000000..5e525e9 --- /dev/null +++ b/node/chunk_pool/src/mem_pool/chunk_cache.rs @@ -0,0 +1,158 @@ +use super::FileID; +use crate::{Config, SegmentInfo}; +use anyhow::{bail, Result}; +use hashlink::LinkedHashMap; +use shared_types::{bytes_to_chunks, ChunkArray, DataRoot, Transaction, CHUNK_SIZE}; +use std::collections::HashMap; +use std::ops::Add; +use std::time::{Duration, Instant}; + +/// Used to cache chunks in memory pool and persist into db once log entry +/// retrieved from blockchain. +pub struct MemoryCachedFile { + pub id: FileID, + /// Window to control the cache of each file + pub segments: HashMap, + /// Total number of chunks for the cache file, which is updated from log entry. + pub total_chunks: usize, + /// Used for garbage collection. It is updated when new segment uploaded. + expired_at: Instant, + /// Number of chunks that's currently cached for this file + pub cached_chunk_num: usize, +} + +impl MemoryCachedFile { + fn new(root: DataRoot, timeout: Duration) -> Self { + MemoryCachedFile { + id: FileID { + root, + tx_id: Default::default(), + }, + segments: HashMap::default(), + total_chunks: 0, + expired_at: Instant::now().add(timeout), + cached_chunk_num: 0, + } + } + + /// Updates file with transaction once log entry retrieved from blockchain. + /// So that, write memory cached segments into database. + pub fn update_with_tx(&mut self, tx: &Transaction) { + self.total_chunks = bytes_to_chunks(tx.size as usize); + self.id.tx_id = tx.id(); + } + + fn update_expiration_time(&mut self, timeout: Duration) { + self.expired_at = Instant::now().add(timeout); + } + + #[allow(unused)] + fn is_completed(&self) -> bool { + self.total_chunks > 0 && self.cached_chunk_num >= self.total_chunks + } + + fn should_flush(&self) -> bool { + self.total_chunks > 0 && self.cached_chunk_num > 0 + } +} + +/// ChunkPoolCache is used to cache small files that log entry not retrieved +/// from L1 blockchain yet. +pub struct ChunkPoolCache { + config: Config, + /// All cached files. + /// Note, file root is used as key instead of `tx_seq`, since log entry + /// not retrieved yet. + files: LinkedHashMap, + /// Total number of chunks that cached in the memory pool. + pub total_chunks: usize, +} + +impl ChunkPoolCache { + pub fn new(config: Config) -> Self { + ChunkPoolCache { + config, + files: LinkedHashMap::default(), + total_chunks: 0, + } + } + + pub fn get_file(&self, root: &DataRoot) -> Option<&MemoryCachedFile> { + self.files.get(root) + } + + pub fn get_file_mut(&mut self, root: &DataRoot) -> Option<&mut MemoryCachedFile> { + self.files.get_mut(root) + } + + pub fn remove_file(&mut self, root: &DataRoot) -> Option { + let file = self.files.remove(root)?; + self.update_total_chunks_when_remove_file(&file); + Some(file) + } + + /// Remove files that no new segment uploaded for a long time. + /// + /// Note, when log sync delayed, files may be also garbage collected if the + /// entire file uploaded. Because, it is hard to check if log sync delayed + /// or user upload an invalid file, e.g. for attack purpose. + /// + /// Once garbage collected, user could simply upload the entire file again, + /// which is fast enough due to small file size. + fn garbage_collect(&mut self) { + let now = Instant::now(); + + while let Some((_, file)) = self.files.front() { + if file.expired_at > now { + return; + } + + if let Some((r, f)) = self.files.pop_front() { + self.update_total_chunks_when_remove_file(&f); + debug!("Garbage collected for file {}", r); + } + } + } + + fn update_total_chunks_when_remove_file(&mut self, file: &MemoryCachedFile) { + assert!(self.total_chunks >= file.cached_chunk_num); + self.total_chunks -= file.cached_chunk_num; + } + + /// Caches the specified segment in memory. + /// + /// Returns if there are cached segments and log entry also retrieved. + pub fn cache_segment(&mut self, seg_info: SegmentInfo) -> Result { + // always GC at first + self.garbage_collect(); + + let file = self + .files + .entry(seg_info.root) + .or_insert_with(|| MemoryCachedFile::new(seg_info.root, self.config.expiration_time())); + + // Segment already cached in memory. Directly return OK + if file.segments.contains_key(&seg_info.seg_index) { + return Ok(file.should_flush()); + } + + // Otherwise, just cache segment in memory + let num_chunks = seg_info.seg_data.len() / CHUNK_SIZE; + + // Limits the cached chunks in the memory pool. + if self.total_chunks + num_chunks > self.config.max_cached_chunks_all { + bail!( + "exceeds the maximum cached chunks of whole pool: {}", + self.config.max_cached_chunks_all + ); + } + + // Cache segment and update the counter for cached chunks. + self.total_chunks += num_chunks; + file.cached_chunk_num += num_chunks; + file.update_expiration_time(self.config.expiration_time()); + file.segments.insert(seg_info.seg_index, seg_info.into()); + + Ok(file.should_flush()) + } +} diff --git a/node/chunk_pool/src/mem_pool/chunk_pool_inner.rs b/node/chunk_pool/src/mem_pool/chunk_pool_inner.rs new file mode 100644 index 0000000..de8c37b --- /dev/null +++ b/node/chunk_pool/src/mem_pool/chunk_pool_inner.rs @@ -0,0 +1,332 @@ +use super::chunk_cache::{ChunkPoolCache, MemoryCachedFile}; +use super::chunk_write_control::ChunkPoolWriteCtrl; +use super::FileID; +use crate::Config; +use anyhow::{bail, Result}; +use async_lock::Mutex; +use log_entry_sync::LogSyncEvent; +use shared_types::{ + bytes_to_chunks, compute_segment_size, ChunkArray, DataRoot, Transaction, CHUNK_SIZE, +}; +use std::sync::Arc; +use storage_async::Store; +use tokio::sync::broadcast::{error::RecvError, Receiver}; +use tokio::sync::mpsc::UnboundedSender; + +struct Inner { + config: Config, + segment_cache: ChunkPoolCache, + write_control: ChunkPoolWriteCtrl, +} + +impl Inner { + fn new(config: Config) -> Self { + Inner { + config, + segment_cache: ChunkPoolCache::new(config), + write_control: ChunkPoolWriteCtrl::new(config), + } + } + + fn after_flush_cache(&mut self) { + assert!(self.write_control.total_writings > 0); + self.write_control.total_writings -= 1; + } + + /// Return the tx seq and all segments that belong to the root. + fn get_all_cached_segments_to_write( + &mut self, + root: &DataRoot, + ) -> Result<(FileID, Vec)> { + // Limits the number of writing threads. + if self.write_control.total_writings >= self.config.max_writings { + bail!("too many data writing: {}", self.config.max_writings); + } + + let file = match self.segment_cache.remove_file(root) { + Some(f) => f, + None => bail!("file not found to write into store {:?}", root), + }; + let id = file.id; + let segs = file.segments.into_values().collect(); + + self.write_control.total_writings += 1; + + Ok((id, segs)) + } +} + +pub struct SegmentInfo { + pub root: DataRoot, + pub seg_data: Vec, + pub seg_index: usize, + pub chunks_per_segment: usize, +} + +impl From for ChunkArray { + fn from(seg_info: SegmentInfo) -> Self { + let start_index = seg_info.seg_index * seg_info.chunks_per_segment; + ChunkArray { + data: seg_info.seg_data, + start_index: start_index as u64, + } + } +} + +/// Caches data chunks in memory before the entire file uploaded to storage node +/// and data root verified on blockchain. +pub struct MemoryChunkPool { + inner: Mutex, + log_store: Store, + sender: UnboundedSender, +} + +impl MemoryChunkPool { + pub(crate) fn new(config: Config, log_store: Store, sender: UnboundedSender) -> Self { + MemoryChunkPool { + inner: Mutex::new(Inner::new(config)), + log_store, + sender, + } + } + + pub fn validate_segment_size(&self, segment: &Vec) -> Result<()> { + if segment.is_empty() { + bail!("data is empty"); + } + + if segment.len() % CHUNK_SIZE != 0 { + bail!("invalid data length"); + } + + Ok(()) + } + + pub async fn cache_chunks(&self, seg_info: SegmentInfo) -> Result<()> { + let root = seg_info.root; + debug!("cache_chunks, root={:?} index={}", root, seg_info.seg_index); + let should_flush = self + .inner + .lock() + .await + .segment_cache + .cache_segment(seg_info)?; + + // store and finalize the cached file if completed + if should_flush { + debug!("cache_chunk: flush cached chunks"); + self.write_all_cached_chunks_and_finalize(root).await?; + } + + Ok(()) + } + + pub async fn write_chunks( + &self, + seg_info: SegmentInfo, + file_id: FileID, + file_size: usize, + ) -> Result<()> { + let total_chunks = bytes_to_chunks(file_size); + + debug!( + "Begin to write segment, root={}, segment_size={}, segment_index={}", + seg_info.root, + seg_info.seg_data.len(), + seg_info.seg_index, + ); + + //Write the segment in window + let (total_segments, _) = compute_segment_size(total_chunks, seg_info.chunks_per_segment); + self.inner.lock().await.write_control.write_segment( + file_id, + seg_info.seg_index, + total_segments, + )?; + + // Write memory cached segments into store. + // TODO(qhz): error handling + // 1. Push the failed segment back to front. (enhance store to return Err(ChunkArray)) + // 2. Put the incompleted segments back to memory pool. + let seg = ChunkArray { + data: seg_info.seg_data, + start_index: (seg_info.seg_index * seg_info.chunks_per_segment) as u64, + }; + + match self + .log_store + .put_chunks_with_tx_hash(file_id.tx_id.seq, file_id.tx_id.hash, seg) + .await + { + Ok(true) => {} + Ok(false) => { + self.inner + .lock() + .await + .write_control + .on_write_failed(&seg_info.root, seg_info.seg_index); + // remove the file if transaction reverted + self.inner + .lock() + .await + .write_control + .remove_file(&seg_info.root); + bail!("Transaction reverted, please upload again"); + } + Err(e) => { + self.inner + .lock() + .await + .write_control + .on_write_failed(&seg_info.root, seg_info.seg_index); + return Err(e); + } + } + + let all_uploaded = self + .inner + .lock() + .await + .write_control + .on_write_succeeded(&seg_info.root, seg_info.seg_index); + + // Notify to finalize transaction asynchronously. + if all_uploaded { + if let Err(e) = self.sender.send(file_id) { + // Channel receiver will not be dropped until program exit. + bail!("channel send error: {}", e); + } + debug!("Queue to finalize transaction for file {}", seg_info.root); + } + + Ok(()) + } + + /// Updates the cached file info when log entry retrieved from blockchain. + pub async fn update_file_info(&self, tx: &Transaction) -> Result { + let mut inner = self.inner.lock().await; + + // Do nothing if file not uploaded yet. + let file = match inner.segment_cache.get_file_mut(&tx.data_merkle_root) { + Some(f) => f, + None => return Ok(false), + }; + + // Update the file info with transaction. + file.update_with_tx(tx); + + // File partially uploaded and it's up to user thread + // to write chunks into store and finalize transaction. + if file.cached_chunk_num < file.total_chunks { + return Ok(true); + } + + // Otherwise, notify to write all memory cached chunks and finalize transaction. + let file_id = FileID { + root: tx.data_merkle_root, + tx_id: tx.id(), + }; + if let Err(e) = self.sender.send(file_id) { + // Channel receiver will not be dropped until program exit. + bail!("channel send error: {}", e); + } + + Ok(true) + } + + pub async fn monitor_log_entry(chunk_pool: Arc, mut receiver: Receiver) { + info!("Start to monitor log entry"); + + loop { + match receiver.recv().await { + Ok(LogSyncEvent::ReorgDetected { .. }) => {} + Ok(LogSyncEvent::Reverted { .. }) => {} + Ok(LogSyncEvent::TxSynced { tx }) => { + if let Err(_e) = chunk_pool.update_file_info(&tx).await { + error!( + "Failed to update file info. tx seq={}, tx_root={}", + tx.seq, tx.data_merkle_root + ); + } + } + Err(RecvError::Closed) => { + // program terminated + info!("Completed to monitor log entry"); + return; + } + Err(RecvError::Lagged(lagged)) => { + error!(%lagged, "Lagged messages: (Lagged)"); + } + } + } + } + + pub(crate) async fn remove_cached_file(&self, root: &DataRoot) -> Option { + self.inner.lock().await.segment_cache.remove_file(root) + } + + pub(crate) async fn remove_file(&self, root: &DataRoot) -> bool { + let mut inner = self.inner.lock().await; + inner.segment_cache.remove_file(root).is_some() + || inner.write_control.remove_file(root).is_some() + } + + pub async fn check_already_has_cache(&self, root: &DataRoot) -> bool { + self.inner + .lock() + .await + .segment_cache + .get_file(root) + .is_some() + } + + async fn write_all_cached_chunks_and_finalize(&self, root: DataRoot) -> Result<()> { + let (file, mut segments) = self + .inner + .lock() + .await + .get_all_cached_segments_to_write(&root)?; + + while let Some(seg) = segments.pop() { + // TODO(qhz): error handling + // 1. Push the failed segment back to front. (enhance store to return Err(ChunkArray)) + // 2. Put the incompleted segments back to memory pool. + match self + .log_store + .put_chunks_with_tx_hash(file.tx_id.seq, file.tx_id.hash, seg) + .await + { + Ok(true) => {} + Ok(false) => { + self.inner.lock().await.after_flush_cache(); + bail!("Transaction reverted, please upload again"); + } + Err(e) => { + self.inner.lock().await.after_flush_cache(); + return Err(e); + } + } + } + + self.inner.lock().await.after_flush_cache(); + + if let Err(e) = self.sender.send(file) { + // Channel receiver will not be dropped until program exit. + bail!("channel send error: {}", e); + } + + Ok(()) + } + + pub async fn get_uploaded_seg_num(&self, root: &DataRoot) -> (usize, bool) { + let inner = self.inner.lock().await; + + if let Some(file) = inner.segment_cache.get_file(root) { + (file.cached_chunk_num, true) + } else if let Some(file) = inner.write_control.get_file(root) { + (file.uploaded_seg_num(), false) + } else { + (0, false) + } + } +} diff --git a/node/chunk_pool/src/mem_pool/chunk_write_control.rs b/node/chunk_pool/src/mem_pool/chunk_write_control.rs new file mode 100644 index 0000000..6712236 --- /dev/null +++ b/node/chunk_pool/src/mem_pool/chunk_write_control.rs @@ -0,0 +1,200 @@ +use super::FileID; +use crate::Config; +use anyhow::{bail, Result}; +use shared_types::DataRoot; +use std::collections::HashMap; + +/// The segment status in sliding window +#[derive(PartialEq, Eq, Debug)] +enum SlotStatus { + Writing, // segment in writing + Finished, // segment uploaded in store +} + +/// Sliding window is used to control the concurrent uploading process of a file. +/// Bounded window allows segments to be uploaded concurrenly, while having a capacity +/// limit on writing threads per file. Meanwhile, the left_boundary field records +/// how many segments have been uploaded. +struct CtrlWindow { + size: usize, + left_boundary: usize, + slots: HashMap, +} + +impl CtrlWindow { + fn new(size: usize) -> Self { + CtrlWindow { + size, + left_boundary: 0, + slots: HashMap::default(), + } + } + + /// Check if the specified slot by `index` has been already uploaded. + /// Note, this function do not check about the right boundary. + fn check_duplicate(&self, index: usize) -> bool { + index < self.left_boundary || self.slots.contains_key(&index) + } + + /// Should call check_duplicate and handle the duplicated case before calling this function. + /// This function assumes that there are no duplicate slots in the window. + fn start_writing(&mut self, index: usize) -> Result<()> { + assert!(index >= self.left_boundary); + + if index >= self.left_boundary + self.size { + bail!( + "index exceeds window limit, index = {}, left_boundary = {}, window_size = {}", + index, + self.left_boundary, + self.size + ); + } + + assert!(!self.slots.contains_key(&index)); + self.slots.insert(index, SlotStatus::Writing); + + Ok(()) + } + + fn rollback_writing(&mut self, index: usize) { + let slot_status = self.slots.remove(&index); + assert_eq!(slot_status, Some(SlotStatus::Writing)); + } + + fn finish_writing(&mut self, index: usize) { + let old_status = self.slots.insert(index, SlotStatus::Finished); + assert_eq!(old_status, Some(SlotStatus::Writing)); + + // move forward if leftmost slot completed + let mut left_boundary = self.left_boundary; + while let Some(&SlotStatus::Finished) = self.slots.get(&left_boundary) { + self.slots.remove(&left_boundary); + left_boundary += 1; + } + + self.left_boundary = left_boundary; + } +} + +/// To track the file uploading progress. +pub struct FileWriteCtrl { + pub id: FileID, + total_segments: usize, + window: CtrlWindow, +} + +impl FileWriteCtrl { + fn new(id: FileID, total_segments: usize, window_size: usize) -> Self { + FileWriteCtrl { + id, + total_segments, + window: CtrlWindow::new(window_size), + } + } + + pub fn uploaded_seg_num(&self) -> usize { + self.window.left_boundary + } +} + +/// ChunkPoolWriteCtrl is used to track uploading progress for all files, +/// and limits the maximum number of threads to write segments into store. +pub struct ChunkPoolWriteCtrl { + config: Config, + /// Windows to control writing processes of files + files: HashMap, + /// Total number of threads that are writing chunks into store. + pub total_writings: usize, +} + +impl ChunkPoolWriteCtrl { + pub fn new(config: Config) -> Self { + ChunkPoolWriteCtrl { + files: HashMap::default(), + total_writings: 0, + config, + } + } + + pub fn get_file(&self, root: &DataRoot) -> Option<&FileWriteCtrl> { + self.files.get(root) + } + + pub fn remove_file(&mut self, root: &DataRoot) -> Option { + self.files.remove(root) + } + + pub fn write_segment( + &mut self, + id: FileID, + seg_index: usize, + total_segments: usize, + ) -> Result<()> { + let file_ctrl = self.files.entry(id.root).or_insert_with(|| { + FileWriteCtrl::new(id, total_segments, self.config.write_window_size) + }); + + // ensure the tx_id not changed during file uploading + if file_ctrl.id != id { + self.files.remove(&id.root); + bail!("Transaction reverted when uploading segments, please try again"); + } + + if file_ctrl.total_segments != total_segments { + bail!( + "file size in segment doesn't match with file size declared in previous segment. Previous total segments:{}, current total segments:{}s", + file_ctrl.total_segments, + total_segments + ); + } + + // Segment already uploaded. + if file_ctrl.window.check_duplicate(seg_index) { + bail!("segment has already been uploaded or is being uploaded"); + } + + // Limits the number of writing threads. + if self.total_writings >= self.config.max_writings { + bail!("too many data writing: {}", self.config.max_writings); + } + + file_ctrl.window.start_writing(seg_index)?; + + self.total_writings += 1; + + Ok(()) + } + + pub fn on_write_succeeded(&mut self, root: &DataRoot, seg_index: usize) -> bool { + let file_ctrl = match self.files.get_mut(root) { + Some(w) => w, + None => return false, + }; + + file_ctrl.window.finish_writing(seg_index); + + assert!(self.total_writings > 0); + self.total_writings -= 1; + + debug!( + "Succeeded to write segment, root={}, seg_index={}, total_writings={}", + root, seg_index, self.total_writings + ); + + // All chunks of file written into store. + file_ctrl.window.left_boundary >= file_ctrl.total_segments + } + + pub fn on_write_failed(&mut self, root: &DataRoot, seg_index: usize) { + let file_ctrl = match self.files.get_mut(root) { + Some(w) => w, + None => return, + }; + + //Rollback the segment status if failed + file_ctrl.window.rollback_writing(seg_index); + + assert!(self.total_writings > 0); + self.total_writings -= 1; + } +} diff --git a/node/chunk_pool/src/mem_pool/mod.rs b/node/chunk_pool/src/mem_pool/mod.rs new file mode 100644 index 0000000..0fc1299 --- /dev/null +++ b/node/chunk_pool/src/mem_pool/mod.rs @@ -0,0 +1,15 @@ +mod chunk_cache; +mod chunk_pool_inner; +mod chunk_write_control; + +pub use chunk_pool_inner::MemoryChunkPool; +pub use chunk_pool_inner::SegmentInfo; + +use shared_types::DataRoot; +use shared_types::TxID; + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct FileID { + pub root: DataRoot, + pub tx_id: TxID, +} diff --git a/node/file_location_cache/Cargo.toml b/node/file_location_cache/Cargo.toml new file mode 100644 index 0000000..2e46d01 --- /dev/null +++ b/node/file_location_cache/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "file_location_cache" +version = "0.1.0" +edition = "2021" + +[dependencies] +hashlink = "0.8.0" +network = { path = "../network" } +parking_lot = "0.12.1" +rand = "0.8.5" +tracing = "0.1.35" +priority-queue = "1.2.3" +shared_types = { path = "../shared_types" } diff --git a/node/file_location_cache/src/file_location_cache.rs b/node/file_location_cache/src/file_location_cache.rs new file mode 100644 index 0000000..f12fe33 --- /dev/null +++ b/node/file_location_cache/src/file_location_cache.rs @@ -0,0 +1,532 @@ +use crate::Config; +use network::types::SignedAnnounceFile; +use network::PeerId; +use parking_lot::Mutex; +use priority_queue::PriorityQueue; +use rand::seq::IteratorRandom; +use shared_types::{timestamp_now, TxID}; +use std::cmp::Reverse; +use std::collections::HashMap; + +/// Caches limited announcements of specified file from different peers. +struct AnnouncementCache { + /// Maximum number of announcements in cache. + capacity: usize, + + /// Timeout in seconds to expire the cached announcement. + /// This is because file may be removed from the announced + /// storage node. + timeout_secs: u32, + + /// All cached announcements that mapped from peer id to announcement. + /// Note, only cache the latest announcement for each peer. + items: HashMap, + + /// All announcements are prioritized by timestamp. + /// The top element is the oldest announcement. + priorities: PriorityQueue>, +} + +impl AnnouncementCache { + fn new(capacity: usize, timeout_secs: u32) -> Self { + assert!(capacity > 0); + + AnnouncementCache { + capacity, + timeout_secs, + items: Default::default(), + priorities: Default::default(), + } + } + + /// Returns the priority of the oldest announcement if any. + fn peek_priority(&self) -> Option> { + let (_, &Reverse(ts)) = self.priorities.peek()?; + Some(Reverse(ts)) + } + + /// Removes the oldest announcement if any. + fn pop(&mut self) -> Option { + let (peer_id, _) = self.priorities.pop()?; + self.items.remove(&peer_id) + } + + fn len(&self) -> usize { + self.items.len() + } + + /// Garbage collects expired announcements. + fn garbage_collect(&mut self) -> usize { + let mut collected = 0; + let now = timestamp_now(); + + while let Some((_, &Reverse(ts))) = self.priorities.peek() { + if ts + self.timeout_secs > now { + break; + } + + self.pop(); + collected += 1; + } + + collected + } + + fn do_insert_or_update(&mut self, announcement: SignedAnnounceFile) { + let peer_id = announcement.peer_id.clone().into(); + self.priorities + .push(peer_id, Reverse(announcement.timestamp)); + self.items.insert(peer_id, announcement); + } + + /// Insert the specified `announcement` into cache. + fn insert(&mut self, announcement: SignedAnnounceFile) { + self.garbage_collect(); + + let peer_id = announcement.peer_id.clone().into(); + + if let Some(existing) = self.items.get(&peer_id) { + // ignore older announcement + if announcement.timestamp <= existing.timestamp { + return; + } + } + + // insert or update + self.do_insert_or_update(announcement); + + // remove oldest one if capacity exceeded + if self.items.len() > self.capacity { + self.pop(); + } + } + + /// Randomly pick an announcement if any. + fn random(&mut self) -> (Option, usize) { + let collected = self.garbage_collect(); + + // all announcements garbage collected + if self.items.is_empty() { + return (None, collected); + } + + let choosed = self + .items + .iter() + .choose(&mut rand::thread_rng()) + .map(|(_, item)| item.clone()); + + (choosed, collected) + } + + /// Returns all announcements. + fn all(&mut self) -> (Vec, usize) { + let collected = self.garbage_collect(); + let result = self.items.values().cloned().collect(); + (result, collected) + } +} + +/// Caches announcements for different files. +struct FileCache { + /// Cache configuration. + config: Config, + + /// Total number of announcements cached for all files. + total_announcements: usize, + + /// All cached files that mapped from `tx_id` to `AnnouncementCache`. + files: HashMap, + + /// All files are prioritized by timestamp. + /// The top element is the `AnnouncementCache` that has the oldest announcement. + priorities: PriorityQueue>, +} + +impl FileCache { + fn new(config: Config) -> Self { + FileCache { + config, + total_announcements: 0, + files: Default::default(), + priorities: Default::default(), + } + } + + /// Insert the specified `announcement` into cache. + fn insert(&mut self, announcement: SignedAnnounceFile) { + let tx_id = announcement.tx_id; + + let item = self.files.entry(tx_id).or_insert_with(|| { + AnnouncementCache::new( + self.config.max_entries_per_file, + self.config.entry_expiration_time_secs, + ) + }); + + assert!(self.total_announcements >= item.len()); + self.total_announcements -= item.len(); + + item.insert(announcement); + + if let Some(priority) = item.peek_priority() { + self.priorities.push(tx_id, priority); + } + + self.total_announcements += item.len(); + if self.total_announcements > self.config.max_entries_total { + self.pop(); + } + } + + /// Removes the oldest file announcement. + fn pop(&mut self) -> Option { + let (&tx_id, _) = self.priorities.peek()?; + let item = self.files.get_mut(&tx_id)?; + + let result = item.pop()?; + + self.update_on_announcement_cache_changed(&tx_id, 1); + + Some(result) + } + + /// Randomly pick a announcement of specified file by `tx_id`. + fn random(&mut self, tx_id: TxID) -> Option { + let item = self.files.get_mut(&tx_id)?; + let (result, collected) = item.random(); + self.update_on_announcement_cache_changed(&tx_id, collected); + result + } + + fn update_on_announcement_cache_changed(&mut self, tx_id: &TxID, removed: usize) { + if removed == 0 { + return; + } + + assert!(self.total_announcements >= removed); + self.total_announcements -= removed; + + let item = match self.files.get_mut(tx_id) { + Some(v) => v, + None => return, + }; + + if let Some(priority) = item.peek_priority() { + // update priority if changed + self.priorities.change_priority(tx_id, priority); + } else { + // remove entry if empty + self.files.remove(tx_id); + self.priorities.remove(tx_id); + } + } + + /// Returns all the announcements of specified file by `tx_id`. + fn all(&mut self, tx_id: TxID) -> Option> { + let item = self.files.get_mut(&tx_id)?; + let (result, collected) = item.all(); + self.update_on_announcement_cache_changed(&tx_id, collected); + Some(result) + } +} + +pub struct FileLocationCache { + cache: Mutex, +} + +impl Default for FileLocationCache { + fn default() -> Self { + FileLocationCache { + cache: Mutex::new(FileCache::new(Default::default())), + } + } +} + +impl FileLocationCache { + pub fn insert(&self, announcement: SignedAnnounceFile) { + self.cache.lock().insert(announcement); + } + + pub fn get_one(&self, tx_id: TxID) -> Option { + self.cache.lock().random(tx_id) + } + + pub fn get_all(&self, tx_id: TxID) -> Vec { + self.cache.lock().all(tx_id).unwrap_or_default() + } +} + +#[cfg(test)] +mod tests { + use std::cmp::Reverse; + + use network::{types::SignedAnnounceFile, PeerId}; + use shared_types::{timestamp_now, TxID}; + + use crate::{test_util::AnnounceFileBuilder, Config}; + + use super::{AnnouncementCache, FileCache}; + + fn create_file(peer_id: Option, timestamp: u32) -> SignedAnnounceFile { + let builder = AnnounceFileBuilder::default().with_timestamp(timestamp); + if let Some(id) = peer_id { + builder.with_peer_id(id).build() + } else { + builder.build() + } + } + + #[test] + fn test_annoucement_cache_peek_priority() { + let mut cache = AnnouncementCache::new(100, 3600); + let now = timestamp_now(); + + // empty by default + assert_eq!(cache.peek_priority(), None); + + // one file with timestamp `now - 5` + let t1 = now - 5; + cache.insert(create_file(None, t1)); + assert_eq!(cache.peek_priority(), Some(Reverse(t1))); + + // newly file with timestamp `now - 4` + let t2 = now - 4; + cache.insert(create_file(None, t2)); + assert_eq!(cache.peek_priority(), Some(Reverse(t1))); + + // old file with timestamp `now - 6` + let t3 = now - 6; + cache.insert(create_file(None, t3)); + assert_eq!(cache.peek_priority(), Some(Reverse(t3))); + } + + #[test] + fn test_annoucement_cache_pop_len() { + let mut cache = AnnouncementCache::new(100, 3600); + let now = timestamp_now(); + + // empty by default + assert_eq!(cache.pop(), None); + assert_eq!(cache.len(), 0); + + cache.insert(create_file(None, now - 2)); + cache.insert(create_file(None, now - 3)); + cache.insert(create_file(None, now - 1)); + assert_eq!(cache.len(), 3); + + // pop from oldest to newest + assert_eq!(cache.pop().unwrap().timestamp, now - 3); + assert_eq!(cache.pop().unwrap().timestamp, now - 2); + assert_eq!(cache.pop().unwrap().timestamp, now - 1); + assert_eq!(cache.pop(), None); + assert_eq!(cache.len(), 0); + } + + #[test] + fn test_annoucement_cache_garbage_collect() { + let mut cache = AnnouncementCache::new(100, 3600); + let now = timestamp_now(); + + assert_eq!(cache.garbage_collect(), 0); + + cache.do_insert_or_update(create_file(None, now - 5000)); + cache.do_insert_or_update(create_file(None, now - 5001)); + cache.do_insert_or_update(create_file(None, now - 2000)); + cache.do_insert_or_update(create_file(None, now + 10)); + + // gc for expired only + assert_eq!(cache.garbage_collect(), 2); + assert_eq!(cache.len(), 2); + assert_eq!(cache.peek_priority(), Some(Reverse(now - 2000))); + } + + #[test] + fn test_annoucement_cache_insert_gc() { + let mut cache = AnnouncementCache::new(100, 3600); + let now = timestamp_now(); + + // prepare expired items + cache.do_insert_or_update(create_file(None, now - 5000)); + cache.do_insert_or_update(create_file(None, now - 5001)); + + // insert with gc + cache.insert(create_file(None, now - 1)); + + assert_eq!(cache.len(), 1); + assert_eq!(cache.peek_priority(), Some(Reverse(now - 1))); + } + + #[test] + fn test_annoucement_cache_insert_ignore_older() { + let mut cache = AnnouncementCache::new(100, 3600); + let now = timestamp_now(); + + let peer_id = PeerId::random(); + + // insert `now - 2` + cache.insert(create_file(Some(peer_id), now - 2)); + assert_eq!(cache.peek_priority(), Some(Reverse(now - 2))); + assert_eq!(cache.len(), 1); + + // ignore for older announcement of the same peer + cache.insert(create_file(Some(peer_id), now - 3)); + assert_eq!(cache.peek_priority(), Some(Reverse(now - 2))); + assert_eq!(cache.len(), 1); + + // however, older announcement allowed from other peer + cache.insert(create_file(None, now - 3)); + assert_eq!(cache.peek_priority(), Some(Reverse(now - 3))); + assert_eq!(cache.len(), 2); + } + + #[test] + fn test_annoucement_cache_insert_overwrite() { + let mut cache = AnnouncementCache::new(100, 3600); + let now = timestamp_now(); + + let peer_id = PeerId::random(); + + // insert `now - 2` + cache.insert(create_file(Some(peer_id), now - 2)); + assert_eq!(cache.peek_priority(), Some(Reverse(now - 2))); + assert_eq!(cache.len(), 1); + + // overwrite with newly item + cache.insert(create_file(Some(peer_id), now - 1)); + assert_eq!(cache.peek_priority(), Some(Reverse(now - 1))); + assert_eq!(cache.len(), 1); + } + + #[test] + fn test_annoucement_cache_insert_cap_exceeded() { + let mut cache = AnnouncementCache::new(3, 3600); + let now = timestamp_now(); + + cache.insert(create_file(None, now - 2)); + cache.insert(create_file(None, now - 3)); + cache.insert(create_file(None, now - 4)); + + // oldest `now - 5` will be removed + cache.insert(create_file(None, now - 5)); + assert_eq!(cache.peek_priority(), Some(Reverse(now - 4))); + assert_eq!(cache.len(), 3); + + // oldest `now - 4` will be removed + cache.insert(create_file(None, now - 1)); + assert_eq!(cache.peek_priority(), Some(Reverse(now - 3))); + assert_eq!(cache.len(), 3); + } + + #[test] + fn test_annoucement_cache_random() { + let mut cache = AnnouncementCache::new(100, 3600); + let now = timestamp_now(); + + assert_eq!(cache.random().0, None); + + cache.insert(create_file(None, now - 1)); + assert_eq!(cache.random().0.unwrap().timestamp, now - 1); + + cache.insert(create_file(None, now - 2)); + cache.insert(create_file(None, now - 3)); + let picked = cache.random().0.unwrap().timestamp; + assert!(picked >= now - 3 && picked < now); + } + + #[test] + fn test_annoucement_cache_all() { + let mut cache = AnnouncementCache::new(100, 3600); + let now = timestamp_now(); + + assert_eq!(cache.all().0, vec![]); + + cache.insert(create_file(None, now - 1)); + cache.insert(create_file(None, now - 2)); + cache.insert(create_file(None, now - 3)); + + assert_all_files(cache.all().0, vec![now - 3, now - 2, now - 1]) + } + + fn assert_all_files(files: Vec, sorted_timestamps: Vec) { + let mut timestamps: Vec = files.iter().map(|f| f.timestamp).collect(); + timestamps.sort(); + assert_eq!(timestamps, sorted_timestamps); + } + + fn create_file_cache(total_entries: usize, file_entries: usize, timeout: u32) -> FileCache { + FileCache::new(Config { + max_entries_total: total_entries, + max_entries_per_file: file_entries, + entry_expiration_time_secs: timeout, + }) + } + + fn create_file_2(tx_id: TxID, peer_id: PeerId, timestamp: u32) -> SignedAnnounceFile { + AnnounceFileBuilder::default() + .with_tx_id(tx_id) + .with_peer_id(peer_id) + .with_timestamp(timestamp) + .build() + } + + fn assert_file(file: &SignedAnnounceFile, tx_id: TxID, peer_id: PeerId, timestamp: u32) { + assert_eq!(file.tx_id, tx_id); + assert_eq!(PeerId::from(file.peer_id.clone()), peer_id); + assert_eq!(file.timestamp, timestamp); + } + + #[test] + fn test_file_cache_insert_pop() { + let mut cache = create_file_cache(100, 3, 3600); + let now = timestamp_now(); + + assert_eq!(cache.total_announcements, 0); + + let peer1 = PeerId::random(); + let peer2 = PeerId::random(); + let tx1 = TxID::random_hash(1); + let tx2 = TxID::random_hash(2); + + cache.insert(create_file_2(tx1, peer1, now - 1)); + assert_eq!(cache.total_announcements, 1); + cache.insert(create_file_2(tx2, peer1, now - 2)); + assert_eq!(cache.total_announcements, 2); + cache.insert(create_file_2(tx1, peer2, now - 3)); + assert_eq!(cache.total_announcements, 3); + + assert_file(&cache.pop().unwrap(), tx1, peer2, now - 3); + assert_eq!(cache.total_announcements, 2); + assert_file(&cache.pop().unwrap(), tx2, peer1, now - 2); + assert_eq!(cache.total_announcements, 1); + assert_file(&cache.pop().unwrap(), tx1, peer1, now - 1); + assert_eq!(cache.total_announcements, 0); + assert_eq!(cache.pop(), None); + } + + #[test] + fn test_file_cache_insert_cap_exceeded() { + let mut cache = create_file_cache(3, 3, 3600); + let now = timestamp_now(); + + let tx1 = TxID::random_hash(1); + cache.insert(create_file_2(tx1, PeerId::random(), now - 7)); + cache.insert(create_file_2(tx1, PeerId::random(), now - 8)); + cache.insert(create_file_2(tx1, PeerId::random(), now - 9)); + assert_eq!(cache.total_announcements, 3); + + // insert more files and cause to max entries limited + let tx2 = TxID::random_hash(2); + cache.insert(create_file_2(tx2, PeerId::random(), now - 1)); + assert_all_files(cache.all(tx1).unwrap_or_default(), vec![now - 8, now - 7]); + cache.insert(create_file_2(tx2, PeerId::random(), now - 2)); + assert_all_files(cache.all(tx1).unwrap_or_default(), vec![now - 7]); + cache.insert(create_file_2(tx2, PeerId::random(), now - 3)); + assert_all_files(cache.all(tx1).unwrap_or_default(), vec![]); + + assert_all_files( + cache.all(tx2).unwrap_or_default(), + vec![now - 3, now - 2, now - 1], + ); + } +} diff --git a/node/file_location_cache/src/lib.rs b/node/file_location_cache/src/lib.rs new file mode 100644 index 0000000..f36b9d0 --- /dev/null +++ b/node/file_location_cache/src/lib.rs @@ -0,0 +1,20 @@ +mod file_location_cache; +pub mod test_util; + +pub use crate::file_location_cache::FileLocationCache; + +pub struct Config { + pub max_entries_total: usize, + pub max_entries_per_file: usize, + pub entry_expiration_time_secs: u32, +} + +impl Default for Config { + fn default() -> Self { + Config { + max_entries_total: 4096, + max_entries_per_file: 4, + entry_expiration_time_secs: 3600, + } + } +} diff --git a/node/file_location_cache/src/test_util.rs b/node/file_location_cache/src/test_util.rs new file mode 100644 index 0000000..adf20ed --- /dev/null +++ b/node/file_location_cache/src/test_util.rs @@ -0,0 +1,47 @@ +use network::{ + libp2p::identity, + types::{AnnounceFile, SignedAnnounceFile}, + Multiaddr, PeerId, +}; +use shared_types::{timestamp_now, TxID}; + +#[derive(Default)] +pub struct AnnounceFileBuilder { + tx_id: Option, + peer_id: Option, + timestamp: Option, +} + +impl AnnounceFileBuilder { + pub fn with_tx_id(mut self, tx_id: TxID) -> Self { + self.tx_id = Some(tx_id); + self + } + + pub fn with_peer_id(mut self, peer_id: PeerId) -> Self { + self.peer_id = Some(peer_id); + self + } + + pub fn with_timestamp(mut self, timestamp: u32) -> Self { + self.timestamp = Some(timestamp); + self + } + + pub fn build(self) -> SignedAnnounceFile { + let tx_id = self.tx_id.unwrap_or_else(|| TxID::random_hash(0)); + let peer_id = self.peer_id.unwrap_or_else(PeerId::random); + let at: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + let timestamp = self.timestamp.unwrap_or_else(timestamp_now); + + let msg = AnnounceFile { + tx_id, + peer_id: peer_id.into(), + at: at.into(), + timestamp, + }; + + let keypair = identity::Keypair::generate_secp256k1(); + msg.into_signed(&keypair).unwrap() + } +} diff --git a/node/log_entry_sync/Cargo.toml b/node/log_entry_sync/Cargo.toml new file mode 100644 index 0000000..dce7bff --- /dev/null +++ b/node/log_entry_sync/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "log_entry_sync" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = { version = "=1.0.58", features = ["backtrace"] } +append_merkle = { path = "../../common/append_merkle" } +async-trait = "0.1.56" +ethereum-types = "0.14" +futures = "0.3.21" +jsonrpsee = { version = "0.14.0", features = ["full"] } +shared_types = { path = "../shared_types" } +task_executor = { path = "../../common/task_executor" } +tokio = "1.19.2" +ethers = { version = "^2", features = ["ws"] } +serde_json = "1.0.82" +storage = { path = "../storage" } +contract-interface = { path = "../../common/contract-interface" } +futures-core = "0.3.28" +futures-util = "0.3.28" +thiserror = "1.0.44" \ No newline at end of file diff --git a/node/log_entry_sync/src/lib.rs b/node/log_entry_sync/src/lib.rs new file mode 100644 index 0000000..9f27bd2 --- /dev/null +++ b/node/log_entry_sync/src/lib.rs @@ -0,0 +1,10 @@ +extern crate core; + +pub(crate) mod rpc_proxy; +mod sync_manager; + +pub use rpc_proxy::ContractAddress; +pub use sync_manager::{ + config::{CacheConfig, LogSyncConfig}, + LogSyncEvent, LogSyncManager, +}; diff --git a/node/log_entry_sync/src/rpc_proxy/eth.rs b/node/log_entry_sync/src/rpc_proxy/eth.rs new file mode 100644 index 0000000..25566fb --- /dev/null +++ b/node/log_entry_sync/src/rpc_proxy/eth.rs @@ -0,0 +1,30 @@ +use crate::rpc_proxy::{ContractAddress, EvmRpcProxy, SubEvent, SubFilter}; +use async_trait::async_trait; +use ethers::prelude::{Bytes, Middleware, Provider}; +use ethers::providers::Ws; +use ethers::types::TransactionRequest; +use jsonrpsee::core::client::Subscription; + +pub struct EthClient { + client: Provider, +} + +impl EthClient { + #[allow(unused)] + pub async fn new(url: &str) -> anyhow::Result { + let client = Provider::new(Ws::connect(url).await?); + Ok(Self { client }) + } +} + +#[async_trait] +impl EvmRpcProxy for EthClient { + async fn call(&self, to: ContractAddress, data: Bytes) -> anyhow::Result { + let request = TransactionRequest::new().to(to).data(data); + Ok(self.client.call(&request.into(), None).await?) + } + + async fn sub_events(&self, _filter: SubFilter) -> Subscription { + todo!() + } +} diff --git a/node/log_entry_sync/src/rpc_proxy/mod.rs b/node/log_entry_sync/src/rpc_proxy/mod.rs new file mode 100644 index 0000000..a1efe63 --- /dev/null +++ b/node/log_entry_sync/src/rpc_proxy/mod.rs @@ -0,0 +1,38 @@ +use anyhow::Result; +use async_trait::async_trait; +use ethereum_types::{H160, H256}; +use ethers::prelude::Bytes; +use jsonrpsee::core::client::Subscription; + +// TODO: Define accounts/filter/events as associated types? +// TODO: Define an abstraction suitable for other chains. +#[async_trait] +pub trait EvmRpcProxy { + async fn call(&self, to: ContractAddress, data: Bytes) -> Result; + + async fn sub_events(&self, filter: SubFilter) -> Subscription; +} + +pub type ContractAddress = H160; + +pub type Topic = H256; + +#[allow(unused)] +pub struct SubFilter { + to: Option, + topics: Vec, +} + +#[allow(unused)] +pub struct SubEvent { + /// Address + pub address: ContractAddress, + + /// Topics + pub topics: Vec, + + /// Data + pub data: Bytes, +} + +pub(crate) mod eth; diff --git a/node/log_entry_sync/src/sync_manager/config.rs b/node/log_entry_sync/src/sync_manager/config.rs new file mode 100644 index 0000000..fae6e7b --- /dev/null +++ b/node/log_entry_sync/src/sync_manager/config.rs @@ -0,0 +1,65 @@ +use crate::rpc_proxy::ContractAddress; + +pub struct LogSyncConfig { + pub rpc_endpoint_url: String, + pub contract_address: ContractAddress, + pub cache_config: CacheConfig, + + /// The block number where we start to sync data. + /// This is usually the block number when Zgs contract is deployed. + pub start_block_number: u64, + /// The number of blocks needed for confirmation on the blockchain. + /// This is used to rollback to a stable height if reorg happens during node restart. + /// TODO(zz): Some blockchains have better confirmation/finalization mechanisms. + pub confirmation_block_count: u64, + /// Maximum number of event logs to poll at a time. + pub log_page_size: u64, + + // blockchain provider retry params + // the number of retries after a connection times out + pub rate_limit_retries: u32, + // the nubmer of retries for rate limited responses + pub timeout_retries: u32, + // the duration to wait before retry, in ms + pub initial_backoff: u64, + // the duration between each paginated getLogs RPC call, in ms. + // This is set to avoid triggering the throttling mechanism in the RPC server. + pub recover_query_delay: u64, +} + +#[derive(Clone)] +pub struct CacheConfig { + /// The data with a size larger than this will not be cached. + /// This is reasonable because uploading + pub max_data_size: usize, + pub tx_seq_ttl: usize, +} + +impl LogSyncConfig { + #[allow(clippy::too_many_arguments)] + pub fn new( + rpc_endpoint_url: String, + contract_address: ContractAddress, + start_block_number: u64, + confirmation_block_count: u64, + cache_config: CacheConfig, + log_page_size: u64, + rate_limit_retries: u32, + timeout_retries: u32, + initial_backoff: u64, + recover_query_delay: u64, + ) -> Self { + Self { + rpc_endpoint_url, + contract_address, + cache_config, + start_block_number, + confirmation_block_count, + log_page_size, + rate_limit_retries, + timeout_retries, + initial_backoff, + recover_query_delay, + } + } +} diff --git a/node/log_entry_sync/src/sync_manager/data_cache.rs b/node/log_entry_sync/src/sync_manager/data_cache.rs new file mode 100644 index 0000000..00397d6 --- /dev/null +++ b/node/log_entry_sync/src/sync_manager/data_cache.rs @@ -0,0 +1,58 @@ +use crate::sync_manager::config::CacheConfig; +use shared_types::DataRoot; +use std::cmp; +use std::collections::HashMap; + +struct CachedData { + /// Used for garbage collection. + last_seen_tx_seq: u64, + /// Complete data for a given DataRoot. + data: Vec, +} + +pub struct DataCache { + root_to_data: HashMap, + config: CacheConfig, +} + +impl DataCache { + pub fn new(config: CacheConfig) -> Self { + Self { + root_to_data: HashMap::new(), + config, + } + } + + pub fn add_data(&mut self, root: DataRoot, tx_seq: u64, data: Vec) -> bool { + if data.len() > self.config.max_data_size { + // large data are not cached. + return false; + } + // TODO: insert partial data and merge here. + self.root_to_data + .entry(root) + .and_modify(|cached| { + cached.last_seen_tx_seq = cmp::max(tx_seq, cached.last_seen_tx_seq) + }) + .or_insert(CachedData { + last_seen_tx_seq: tx_seq, + data, + }); + true + } + + /// Remove and return the data of a given `DataRoot`. + /// If two completed reverted transactions have the same root and both appear later, + /// the second one will have its data copied in `put_tx`. + pub fn pop_data(&mut self, root: &DataRoot) -> Option> { + self.root_to_data.remove(root).map(|e| e.data) + } + + /// Remove timeout data entries according to TTL. + pub fn garbage_collect(&mut self, latest_tx_seq: u64) { + // We won't keep too many data, so it's okay to just iterate here. + self.root_to_data.retain(|_, cached| { + cached.last_seen_tx_seq + self.config.tx_seq_ttl as u64 >= latest_tx_seq + }) + } +} diff --git a/node/log_entry_sync/src/sync_manager/log_entry_fetcher.rs b/node/log_entry_sync/src/sync_manager/log_entry_fetcher.rs new file mode 100644 index 0000000..eebf938 --- /dev/null +++ b/node/log_entry_sync/src/sync_manager/log_entry_fetcher.rs @@ -0,0 +1,356 @@ +use crate::rpc_proxy::ContractAddress; +use crate::sync_manager::log_query::LogQuery; +use crate::sync_manager::{repeat_run_and_log, RETRY_WAIT_MS}; +use anyhow::{anyhow, bail, Result}; +use append_merkle::{Algorithm, Sha3Algorithm}; +use contract_interface::{ZgsFlow, SubmissionNode, SubmitFilter}; +use ethers::abi::RawLog; +use ethers::prelude::{BlockNumber, EthLogDecode, Http, Log, Middleware, Provider, U256}; +use ethers::providers::{FilterKind, HttpRateLimitRetryPolicy, RetryClient, RetryClientBuilder}; +use ethers::types::H256; +use futures::StreamExt; +use jsonrpsee::tracing::{debug, error, info}; +use shared_types::{DataRoot, Transaction}; +use std::collections::{BTreeMap, VecDeque}; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; +use task_executor::TaskExecutor; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; + +pub struct LogEntryFetcher { + contract_address: ContractAddress, + log_page_size: u64, + provider: Arc>>, + + confirmation_delay: u64, +} + +impl LogEntryFetcher { + pub async fn new( + url: &str, + contract_address: ContractAddress, + log_page_size: u64, + confirmation_delay: u64, + rate_limit_retries: u32, + timeout_retries: u32, + initial_backoff: u64, + ) -> Result { + let provider = Arc::new(Provider::new( + RetryClientBuilder::default() + .rate_limit_retries(rate_limit_retries) + .timeout_retries(timeout_retries) + .initial_backoff(Duration::from_millis(initial_backoff)) + .build(Http::from_str(url)?, Box::new(HttpRateLimitRetryPolicy)), + )); + // TODO: `error` types are removed from the ABI json file. + Ok(Self { + contract_address, + provider, + log_page_size, + confirmation_delay, + }) + } + + pub fn start_recover( + &self, + start_block_number: u64, + end_block_number: u64, + executor: &TaskExecutor, + log_query_delay: Duration, + ) -> UnboundedReceiver { + let provider = self.provider.clone(); + let (recover_tx, recover_rx) = tokio::sync::mpsc::unbounded_channel(); + let contract = ZgsFlow::new(self.contract_address, provider.clone()); + let log_page_size = self.log_page_size; + + executor.spawn( + async move { + let mut progress = start_block_number; + let mut filter = contract + .submit_filter() + .from_block(progress) + .to_block(end_block_number) + .filter; + let mut stream = LogQuery::new(&provider, &filter, log_query_delay) + .with_page_size(log_page_size); + debug!( + "start_recover starts, start={} end={}", + start_block_number, end_block_number + ); + while let Some(maybe_log) = stream.next().await { + match maybe_log { + Ok(log) => { + let sync_progress = + if log.block_hash.is_some() && log.block_number.is_some() { + let synced_block = LogFetchProgress::SyncedBlock(( + log.block_number.unwrap().as_u64(), + log.block_hash.unwrap(), + )); + progress = log.block_number.unwrap().as_u64(); + Some(synced_block) + } else { + None + }; + debug!("recover: progress={:?}", sync_progress); + + match SubmitFilter::decode_log(&RawLog { + topics: log.topics, + data: log.data.to_vec(), + }) { + Ok(event) => { + if let Err(e) = recover_tx + .send(submission_event_to_transaction(event)) + .and_then(|_| match sync_progress { + Some(b) => recover_tx.send(b), + None => Ok(()), + }) + { + error!("send error: e={:?}", e); + } + } + Err(e) => { + error!("log decode error: e={:?}", e); + } + } + } + Err(e) => { + error!("log query error: e={:?}", e); + filter = filter.from_block(progress); + stream = LogQuery::new(&provider, &filter, log_query_delay) + .with_page_size(log_page_size); + tokio::time::sleep(Duration::from_millis(RETRY_WAIT_MS)).await; + } + } + } + }, + "log recover", + ); + recover_rx + } + + pub fn start_watch( + &self, + start_block_number: u64, + executor: &TaskExecutor, + ) -> UnboundedReceiver { + let (watch_tx, watch_rx) = tokio::sync::mpsc::unbounded_channel(); + let contract = ZgsFlow::new(self.contract_address, self.provider.clone()); + let provider = self.provider.clone(); + let mut log_confirmation_queue = LogConfirmationQueue::new(self.confirmation_delay); + executor.spawn( + async move { + let mut filter = contract + .submit_filter() + .from_block(start_block_number) + .filter; + debug!("start_watch starts, start={}", start_block_number); + let mut filter_id = + repeat_run_and_log(|| provider.new_filter(FilterKind::Logs(&filter))).await; + let mut progress = start_block_number; + + loop { + match Self::watch_loop( + provider.as_ref(), + filter_id, + &watch_tx, + &mut log_confirmation_queue, + ) + .await + { + Err(e) => { + error!("log sync watch error: e={:?}", e); + filter = filter.from_block(progress); + filter_id = repeat_run_and_log(|| { + provider.new_filter(FilterKind::Logs(&filter)) + }) + .await; + } + Ok(Some(p)) => { + progress = p; + info!("log sync to block number {:?}", progress); + } + Ok(None) => { + error!( + "log sync gets entries without progress? old_progress={}", + progress + ) + } + } + tokio::time::sleep(Duration::from_millis(RETRY_WAIT_MS)).await; + } + }, + "log watch", + ); + watch_rx + } + + async fn watch_loop( + provider: &Provider>, + filter_id: U256, + watch_tx: &UnboundedSender, + log_confirmation_queue: &mut LogConfirmationQueue, + ) -> Result> { + debug!("get block"); + let latest_block = provider + .get_block(BlockNumber::Latest) + .await? + .ok_or_else(|| anyhow!("None for latest block"))?; + debug!("get filter changes"); + let logs: Vec = provider.get_filter_changes(filter_id).await?; + if let Some(reverted) = log_confirmation_queue.push(logs)? { + watch_tx.send(LogFetchProgress::Reverted(reverted))?; + } + debug!("get filter end"); + for log in log_confirmation_queue.confirm_logs(latest_block.number.unwrap().as_u64()) { + assert!(!log.removed.unwrap_or(false)); + // TODO(zz): Log parse error means logs might be lost here. + let tx = SubmitFilter::decode_log(&RawLog { + topics: log.topics, + data: log.data.to_vec(), + })?; + watch_tx.send(submission_event_to_transaction(tx))?; + } + let progress = if latest_block.hash.is_some() && latest_block.number.is_some() { + Some(( + latest_block.number.unwrap().as_u64(), + latest_block.hash.unwrap(), + )) + } else { + None + }; + if let Some(p) = &progress { + watch_tx.send(LogFetchProgress::SyncedBlock(*p))?; + } + Ok(progress.map(|p| p.0)) + } + + pub fn provider(&self) -> &Provider> { + self.provider.as_ref() + } +} + +struct LogConfirmationQueue { + /// Keep the unconfirmed new logs. + /// The key is the block number and the value is the set of needed logs in that block. + queue: VecDeque<(u64, Vec)>, + + latest_block_number: u64, + confirmation_delay: u64, +} + +impl LogConfirmationQueue { + fn new(confirmation_delay: u64) -> Self { + Self { + queue: VecDeque::new(), + latest_block_number: 0, + confirmation_delay, + } + } + /// Push a set of new logs. + /// We assumes that these logs are in order, and removed logs are returned first. + /// + /// Return `Ok(Some(tx_seq))` of the first reverted tx_seq if chain reorg happens. + /// `Err` is returned if assumptions are violated (like the log have missing fields). + fn push(&mut self, logs: Vec) -> Result> { + let mut revert_to = None; + // First merge logs according to the block number. + let mut block_logs: BTreeMap> = BTreeMap::new(); + let mut removed_block_logs = BTreeMap::new(); + for log in logs { + let set = if log.removed.unwrap_or(false) { + &mut removed_block_logs + } else { + &mut block_logs + }; + let block_number = log + .block_number + .ok_or_else(|| anyhow!("block number missing"))? + .as_u64(); + set.entry(block_number).or_default().push(log); + } + + // Handle revert if it happens. + for (block_number, removed_logs) in &removed_block_logs { + if revert_to.is_none() { + let reverted_index = match self.queue.binary_search_by_key(block_number, |e| e.0) { + Ok(x) => x, + Err(x) => x, + }; + self.queue.truncate(reverted_index); + let first = removed_logs.first().expect("not empty"); + let first_reverted_tx_seq = SubmitFilter::decode_log(&RawLog { + topics: first.topics.clone(), + data: first.data.to_vec(), + })? + .submission_index + .as_u64(); + revert_to = Some(first_reverted_tx_seq); + } else { + // Other removed logs should have larger tx seq, so no need to process them. + break; + } + } + + // Add new logs to the queue. + for (block_number, new_logs) in block_logs { + if block_number <= self.queue.back().map(|e| e.0).unwrap_or(0) { + bail!("reverted without being notified"); + } + self.queue.push_back((block_number, new_logs)); + } + + Ok(revert_to) + } + + /// Pass in the latest block number and return the confirmed logs. + fn confirm_logs(&mut self, latest_block_number: u64) -> Vec { + self.latest_block_number = latest_block_number; + let mut confirmed_logs = Vec::new(); + while let Some((block_number, _)) = self.queue.front() { + if *block_number + > self + .latest_block_number + .wrapping_sub(self.confirmation_delay) + { + break; + } + let (_, mut logs) = self.queue.pop_front().unwrap(); + confirmed_logs.append(&mut logs); + } + confirmed_logs + } +} + +#[derive(Debug)] +pub enum LogFetchProgress { + SyncedBlock((u64, H256)), + Transaction(Transaction), + Reverted(u64), +} + +fn submission_event_to_transaction(e: SubmitFilter) -> LogFetchProgress { + LogFetchProgress::Transaction(Transaction { + stream_ids: vec![], + data: vec![], + data_merkle_root: nodes_to_root(&e.submission.nodes), + merkle_nodes: e + .submission + .nodes + .iter() + // the submission height is the height of the root node starting from height 0. + .map(|SubmissionNode { root, height }| (height.as_usize() + 1, root.into())) + .collect(), + start_entry_index: e.start_pos.as_u64(), + size: e.submission.length.as_u64(), + seq: e.submission_index.as_u64(), + }) +} + +fn nodes_to_root(node_list: &Vec) -> DataRoot { + let mut root: DataRoot = node_list.last().expect("not empty").root.into(); + for next_node in node_list[..node_list.len() - 1].iter().rev() { + root = Sha3Algorithm::parent(&next_node.root.into(), &root); + } + root +} diff --git a/node/log_entry_sync/src/sync_manager/log_query.rs b/node/log_entry_sync/src/sync_manager/log_query.rs new file mode 100644 index 0000000..ee7f412 --- /dev/null +++ b/node/log_entry_sync/src/sync_manager/log_query.rs @@ -0,0 +1,183 @@ +use ethers::prelude::{Filter, JsonRpcClient, Log, Middleware, Provider, ProviderError, U64}; +use futures_core::stream::Stream; +use jsonrpsee::tracing::trace; +use std::future::Future; +use std::time::Duration; +use std::{ + collections::VecDeque, + pin::Pin, + task::{Context, Poll}, +}; +use thiserror::Error; + +pub(crate) type PinBoxFut<'a, T> = + Pin> + Send + 'a>>; + +/// A log query provides streaming access to historical logs via a paginated +/// request. For streaming access to future logs, use [`Middleware::watch`] or +/// [`Middleware::subscribe_logs`] +pub struct LogQuery<'a, P> { + provider: &'a Provider

, + filter: Filter, + from_block: Option, + page_size: u64, + current_logs: VecDeque, + last_block: Option, + state: LogQueryState<'a>, + delay: Duration, +} + +enum LogQueryState<'a> { + Initial, + LoadLastBlock(PinBoxFut<'a, U64>), + LoadLogs(PinBoxFut<'a, Vec>), + Consume, +} + +impl<'a, P> LogQuery<'a, P> +where + P: JsonRpcClient, +{ + /// Instantiate a new `LogQuery` + pub fn new(provider: &'a Provider

, filter: &Filter, delay: Duration) -> Self { + Self { + provider, + filter: filter.clone(), + from_block: filter.get_from_block(), + page_size: 10000, + current_logs: VecDeque::new(), + last_block: None, + state: LogQueryState::Initial, + delay, + } + } + + /// set page size for pagination + pub fn with_page_size(mut self, page_size: u64) -> Self { + self.page_size = page_size; + self + } +} + +macro_rules! rewake_with_new_state { + ($ctx:ident, $this:ident, $new_state:expr) => { + $this.state = $new_state; + $ctx.waker().wake_by_ref(); + return Poll::Pending + }; +} + +/// Errors while querying for logs +#[derive(Error, Debug)] +pub enum LogQueryError { + /// Error loading latest block + #[error(transparent)] + LoadLastBlockError(E), + /// Error loading logs from block range + #[error(transparent)] + LoadLogsError(E), +} + +impl<'a, P> Stream for LogQuery<'a, P> +where + P: JsonRpcClient, +{ + type Item = Result>; + + fn poll_next(mut self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll> { + let delay = self.delay; + match &mut self.state { + LogQueryState::Initial => { + if !self.filter.is_paginatable() { + // if not paginatable, load logs and consume + let filter = self.filter.clone(); + let provider = self.provider; + #[allow(clippy::redundant_async_block)] + let fut = Box::pin(async move { + tokio::time::sleep(delay).await; + provider.get_logs(&filter).await + }); + rewake_with_new_state!(ctx, self, LogQueryState::LoadLogs(fut)); + } else { + // if paginatable, load last block + let fut = self.provider.get_block_number(); + rewake_with_new_state!(ctx, self, LogQueryState::LoadLastBlock(fut)); + } + } + LogQueryState::LoadLastBlock(fut) => { + match futures_util::ready!(fut.as_mut().poll(ctx)) { + Ok(last_block) => { + trace!("log_query: last_block={}", last_block); + self.last_block = Some(last_block); + + // this is okay because we will only enter this state when the filter is + // paginatable i.e. from block is set + let from_block = self.filter.get_from_block().unwrap(); + let to_block = from_block + self.page_size; + self.from_block = Some(to_block + 1); + + let filter = self + .filter + .clone() + .from_block(from_block) + .to_block(to_block); + let provider = self.provider; + // load first page of logs + #[allow(clippy::redundant_async_block)] + let fut = Box::pin(async move { + tokio::time::sleep(delay).await; + provider.get_logs(&filter).await + }); + rewake_with_new_state!(ctx, self, LogQueryState::LoadLogs(fut)); + } + Err(err) => Poll::Ready(Some(Err(LogQueryError::LoadLastBlockError(err)))), + } + } + LogQueryState::LoadLogs(fut) => match futures_util::ready!(fut.as_mut().poll(ctx)) { + Ok(logs) => { + self.current_logs = VecDeque::from(logs); + rewake_with_new_state!(ctx, self, LogQueryState::Consume); + } + Err(err) => Poll::Ready(Some(Err(LogQueryError::LoadLogsError(err)))), + }, + LogQueryState::Consume => { + let log = self.current_logs.pop_front(); + if log.is_none() { + // consumed all the logs + if !self.filter.is_paginatable() { + Poll::Ready(None) + } else { + // load new logs if there are still more pages to go through + // can safely assume this will always be set in this state + let from_block = self.from_block.unwrap(); + let to_block = from_block + self.page_size; + + // no more pages to load, and everything is consumed + // can safely assume this will always be set in this state + if from_block > self.last_block.unwrap() { + return Poll::Ready(None); + } + // load next page + self.from_block = Some(to_block + 1); + + let filter = self + .filter + .clone() + .from_block(from_block) + .to_block(to_block); + let provider = self.provider; + #[allow(clippy::redundant_async_block)] + let fut = Box::pin(async move { + tokio::time::sleep(delay).await; + provider.get_logs(&filter).await + }); + + rewake_with_new_state!(ctx, self, LogQueryState::LoadLogs(fut)); + } + } else { + Poll::Ready(log.map(Ok)) + } + } + } + } +} diff --git a/node/log_entry_sync/src/sync_manager/mod.rs b/node/log_entry_sync/src/sync_manager/mod.rs new file mode 100644 index 0000000..1d68328 --- /dev/null +++ b/node/log_entry_sync/src/sync_manager/mod.rs @@ -0,0 +1,319 @@ +use crate::sync_manager::config::LogSyncConfig; +use crate::sync_manager::data_cache::DataCache; +use crate::sync_manager::log_entry_fetcher::{LogEntryFetcher, LogFetchProgress}; +use anyhow::{bail, Result}; +use ethers::prelude::Middleware; +use futures::FutureExt; +use jsonrpsee::tracing::{debug, error, trace, warn}; +use shared_types::{ChunkArray, Transaction}; +use std::fmt::Debug; +use std::future::Future; +use std::sync::Arc; +use std::time::Duration; +use storage::log_store::Store; +use task_executor::{ShutdownReason, TaskExecutor}; +use tokio::sync::broadcast; +use tokio::sync::mpsc::UnboundedReceiver; +use tokio::sync::RwLock; + +const RETRY_WAIT_MS: u64 = 500; +const BROADCAST_CHANNEL_CAPACITY: usize = 16; + +#[derive(Clone, Debug)] +pub enum LogSyncEvent { + /// Chain reorg detected without any operation yet. + ReorgDetected { tx_seq: u64 }, + /// Transaction reverted in storage. + Reverted { tx_seq: u64 }, + /// Synced a transaction from blockchain + TxSynced { tx: Transaction }, +} + +pub struct LogSyncManager { + config: LogSyncConfig, + log_fetcher: LogEntryFetcher, + store: Arc>, + data_cache: DataCache, + + next_tx_seq: u64, + + /// To broadcast events to handle in advance. + event_send: broadcast::Sender, +} + +impl LogSyncManager { + pub async fn spawn( + config: LogSyncConfig, + executor: TaskExecutor, + store: Arc>, + ) -> Result> { + let next_tx_seq = store.read().await.next_tx_seq(); + + let executor_clone = executor.clone(); + let mut shutdown_sender = executor.shutdown_sender(); + + let (event_send, _) = broadcast::channel(BROADCAST_CHANNEL_CAPACITY); + let event_send_cloned = event_send.clone(); + + // Spawn the task to sync log entries from the blockchain. + executor.spawn( + run_and_log( + move || { + shutdown_sender + .try_send(ShutdownReason::Failure("log sync failure")) + .expect("shutdown send error") + }, + async move { + let log_fetcher = LogEntryFetcher::new( + &config.rpc_endpoint_url, + config.contract_address, + config.log_page_size, + config.confirmation_block_count, + config.rate_limit_retries, + config.timeout_retries, + config.initial_backoff, + ) + .await?; + let data_cache = DataCache::new(config.cache_config.clone()); + let mut log_sync_manager = Self { + config, + log_fetcher, + next_tx_seq, + store, + data_cache, + event_send, + }; + + // Load previous progress from db and check if chain reorg happens after restart. + // TODO(zz): Handle reorg instead of return. + let start_block_number = + match log_sync_manager.store.read().await.get_sync_progress()? { + // No previous progress, so just use config. + None => log_sync_manager.config.start_block_number, + Some((block_number, block_hash)) => { + match log_sync_manager + .log_fetcher + .provider() + .get_block(block_number) + .await + { + Ok(Some(b)) => { + if b.hash == Some(block_hash) { + block_number + } else { + warn!( + "log sync progress check hash fails, \ + block_number={:?} expect={:?} get={:?}", + block_number, block_hash, b.hash + ); + // Assume the blocks before this are not reverted. + block_number.saturating_sub( + log_sync_manager.config.confirmation_block_count, + ) + } + } + e => { + error!("log sync progress check rpc fails, e={:?}", e); + bail!("log sync start error"); + } + } + } + }; + let latest_block_number = log_sync_manager + .log_fetcher + .provider() + .get_block_number() + .await? + .as_u64(); + + // Start watching before recovery to ensure that no log is skipped. + // TODO(zz): Rate limit to avoid OOM during recovery. + let watch_rx = log_sync_manager + .log_fetcher + .start_watch(latest_block_number, &executor_clone); + let recover_rx = log_sync_manager.log_fetcher.start_recover( + start_block_number, + // -1 so the recover and watch ranges do not overlap. + latest_block_number.wrapping_sub(1), + &executor_clone, + Duration::from_millis(log_sync_manager.config.recover_query_delay), + ); + log_sync_manager.handle_data(recover_rx).await?; + // Syncing `watch_rx` is supposed to block forever. + log_sync_manager.handle_data(watch_rx).await?; + Ok(()) + }, + ) + .map(|_| ()), + "log_sync", + ); + Ok(event_send_cloned) + } + + async fn put_tx(&mut self, tx: Transaction) -> bool { + // We call this after process chain reorg, so the sequence number should match. + match tx.seq.cmp(&self.next_tx_seq) { + std::cmp::Ordering::Less => true, + std::cmp::Ordering::Equal => { + debug!("log entry sync get entry: {:?}", tx); + self.put_tx_inner(tx).await + } + std::cmp::Ordering::Greater => { + error!( + "Unexpected transaction seq: next={} get={}", + self.next_tx_seq, tx.seq + ); + false + } + } + } + + /// `tx_seq` is the first reverted tx seq. + async fn process_reverted(&mut self, tx_seq: u64) { + warn!("revert for chain reorg: seq={}", tx_seq); + { + let store = self.store.read().await; + for seq in tx_seq..self.next_tx_seq { + if matches!(store.check_tx_completed(seq), Ok(true)) { + if let Ok(Some(tx)) = store.get_tx_by_seq_number(seq) { + // TODO(zz): Skip reading the rear padding data? + if let Ok(Some(data)) = + store.get_chunks_by_tx_and_index_range(seq, 0, tx.num_entries()) + { + if !self + .data_cache + .add_data(tx.data_merkle_root, seq, data.data) + { + // TODO(zz): Data too large. Save to disk? + warn!("large reverted data dropped for tx={:?}", tx); + } + } + } + } + } + } + + let _ = self.event_send.send(LogSyncEvent::ReorgDetected { tx_seq }); + + // TODO(zz): `wrapping_sub` here is a hack to handle the case of tx_seq=0. + if let Err(e) = self.store.write().await.revert_to(tx_seq.wrapping_sub(1)) { + error!("revert_to fails: e={:?}", e); + return; + } + self.next_tx_seq = tx_seq; + + let _ = self.event_send.send(LogSyncEvent::Reverted { tx_seq }); + } + + async fn handle_data(&mut self, mut rx: UnboundedReceiver) -> Result<()> { + while let Some(data) = rx.recv().await { + trace!("handle_data: data={:?}", data); + match data { + LogFetchProgress::SyncedBlock(progress) => { + match self + .log_fetcher + .provider() + .get_block( + progress + .0 + .saturating_sub(self.config.confirmation_block_count), + ) + .await + { + Ok(Some(b)) => { + if let (Some(block_number), Some(block_hash)) = (b.number, b.hash) { + self.store + .write() + .await + .put_sync_progress((block_number.as_u64(), block_hash))?; + } + } + e => { + error!("log put progress check rpc fails, e={:?}", e); + } + } + } + LogFetchProgress::Transaction(tx) => { + if !self.put_tx(tx.clone()).await { + // Unexpected error. + error!("log sync write error"); + break; + } + if let Err(e) = self.event_send.send(LogSyncEvent::TxSynced { tx }) { + error!("log sync broadcast error, error={:?}", e); + break; + } + } + LogFetchProgress::Reverted(reverted) => { + self.process_reverted(reverted).await; + } + } + } + Ok(()) + } + + async fn put_tx_inner(&mut self, tx: Transaction) -> bool { + if let Err(e) = self.store.write().await.put_tx(tx.clone()) { + error!("put_tx error: e={:?}", e); + false + } else { + if let Some(data) = self.data_cache.pop_data(&tx.data_merkle_root) { + let mut store = self.store.write().await; + // We are holding a mutable reference of LogSyncManager, so no chain reorg is + // possible after put_tx. + if let Err(e) = store + .put_chunks_with_tx_hash( + tx.seq, + tx.hash(), + ChunkArray { + data, + start_index: 0, + }, + ) + .and_then(|_| store.finalize_tx_with_hash(tx.seq, tx.hash())) + { + error!("put_tx data error: e={:?}", e); + return false; + } + } + self.data_cache.garbage_collect(self.next_tx_seq); + self.next_tx_seq += 1; + true + } + } +} + +async fn run_and_log( + mut on_error: impl FnMut(), + f: impl Future> + Send, +) -> Option +where + E: Debug, +{ + match f.await { + Err(e) => { + error!("log sync failure: e={:?}", e); + on_error(); + None + } + Ok(r) => Some(r), + } +} + +async fn repeat_run_and_log(f: impl Fn() -> F) -> R +where + E: Debug, + F: Future> + Send, +{ + loop { + if let Some(r) = run_and_log(|| {}, f()).await { + break r; + } + tokio::time::sleep(Duration::from_millis(RETRY_WAIT_MS)).await; + } +} + +pub(crate) mod config; +mod data_cache; +mod log_entry_fetcher; +mod log_query; diff --git a/node/miner/Cargo.toml b/node/miner/Cargo.toml new file mode 100644 index 0000000..58d9e3a --- /dev/null +++ b/node/miner/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "miner" +version = "0.1.0" +edition = "2021" + +[dependencies] +network = { path = "../network" } +storage = { path = "../storage" } +zgs_spec = { path = "../../common/spec" } +zgs_seal = { path = "../../common/zgs_seal" } +task_executor = { path = "../../common/task_executor" } +contract-interface = { path = "../../common/contract-interface" } +ethereum-types = "0.14" +tokio = { version = "1.19.2", features = ["full"] } +tracing = "0.1.35" +blake2 = "0.10" +tiny-keccak = {version="2.0",features = ["keccak"]} +rand = "^0.8" +ethers = "^2" +lazy_static = "1.4" +async-trait = "0.1.56" +shared_types = { path = "../shared_types" } diff --git a/node/miner/src/config.rs b/node/miner/src/config.rs new file mode 100644 index 0000000..8531c4f --- /dev/null +++ b/node/miner/src/config.rs @@ -0,0 +1,53 @@ +use ethereum_types::{Address, H256}; +use ethers::core::k256::SecretKey; +use ethers::middleware::SignerMiddleware; +use ethers::providers::Http; +use ethers::providers::Middleware; +use ethers::providers::Provider; +use ethers::signers::LocalWallet; +use ethers::signers::Signer; + +pub struct MinerConfig { + pub(crate) miner_id: H256, + pub(crate) miner_key: H256, + pub(crate) rpc_endpoint_url: String, + pub(crate) mine_address: Address, + pub(crate) flow_address: Address, +} + +pub type MineServiceMiddleware = SignerMiddleware, LocalWallet>; + +impl MinerConfig { + pub fn new( + miner_id: Option, + miner_key: Option, + rpc_endpoint_url: String, + mine_address: Address, + flow_address: Address, + ) -> Option { + match (miner_id, miner_key) { + (Some(miner_id), Some(miner_key)) => Some(MinerConfig { + miner_id, + miner_key, + rpc_endpoint_url, + mine_address, + flow_address, + }), + _ => None, + } + } + + pub(crate) async fn make_provider(&self) -> Result { + let provider = Provider::::try_from(&self.rpc_endpoint_url) + .map_err(|e| format!("Can not parse blockchain endpoint: {:?}", e))?; + let chain_id = provider + .get_chainid() + .await + .map_err(|e| format!("Unable to get chain_id: {:?}", e))?; + let secret_key = SecretKey::from_bytes(self.miner_key.as_ref().into()) + .map_err(|e| format!("Cannot parse private key: {:?}", e))?; + let signer = LocalWallet::from(secret_key).with_chain_id(chain_id.as_u64()); + let middleware = SignerMiddleware::new(provider, signer); + Ok(middleware) + } +} diff --git a/node/miner/src/lib.rs b/node/miner/src/lib.rs new file mode 100644 index 0000000..0c6804c --- /dev/null +++ b/node/miner/src/lib.rs @@ -0,0 +1,19 @@ +#[macro_use] +extern crate tracing; +extern crate contract_interface; +#[macro_use] +extern crate lazy_static; + +mod config; +mod loader; +mod mine; +pub mod pora; +mod sealer; +mod service; +mod submitter; +mod watcher; + +pub use config::MinerConfig; +pub use loader::PoraLoader; +pub use mine::CustomMineRange; +pub use service::{MineService, MinerMessage}; diff --git a/node/miner/src/loader.rs b/node/miner/src/loader.rs new file mode 100644 index 0000000..92b7397 --- /dev/null +++ b/node/miner/src/loader.rs @@ -0,0 +1,20 @@ +use async_trait::async_trait; +use std::sync::Arc; +use storage::log_store::{MineLoadChunk, Store}; +use tokio::sync::RwLock; + +#[async_trait] +pub trait PoraLoader: Send + Sync { + async fn load_sealed_data(&self, index: u64) -> Option; +} + +#[async_trait] +impl PoraLoader for Arc> { + async fn load_sealed_data(&self, chunk_index: u64) -> Option { + let store = &*self.read().await; + match store.flow().load_sealed_data(chunk_index) { + Ok(Some(chunk)) => Some(chunk), + _ => None, + } + } +} diff --git a/node/miner/src/mine.rs b/node/miner/src/mine.rs new file mode 100644 index 0000000..8751a3d --- /dev/null +++ b/node/miner/src/mine.rs @@ -0,0 +1,173 @@ +use contract_interface::zgs_flow::MineContext; +use ethereum_types::{H256, U256}; +use rand::{self, Rng}; +use task_executor::TaskExecutor; +use tokio::sync::{broadcast, mpsc}; + +use zgs_spec::{SECTORS_PER_LOAD, SECTORS_PER_MAX_MINING_RANGE, SECTORS_PER_PRICING}; + +use crate::{ + pora::{AnswerWithoutProof, Miner}, + watcher::MineContextMessage, + MinerConfig, MinerMessage, PoraLoader, +}; + +use std::sync::Arc; + +pub struct PoraService { + mine_context_receiver: mpsc::UnboundedReceiver, + mine_answer_sender: mpsc::UnboundedSender, + msg_recv: broadcast::Receiver, + loader: Arc, + + puzzle: Option, + mine_range: CustomMineRange, + miner_id: H256, +} + +struct PoraPuzzle { + context: MineContext, + target_quality: U256, +} +#[derive(Clone, Copy, Debug, Default)] +pub struct CustomMineRange { + start_position: Option, + end_position: Option, +} + +impl CustomMineRange { + #[inline] + fn to_valid_range(self, context: &MineContext) -> Option<(u64, u64)> { + let self_start_position = self.start_position?; + let self_end_position = self.end_position?; + + if self_start_position >= self_end_position { + return None; + } + let minable_length = + (context.flow_length.as_u64() / SECTORS_PER_LOAD as u64) * SECTORS_PER_LOAD as u64; + + let mining_length = std::cmp::min(minable_length, SECTORS_PER_MAX_MINING_RANGE as u64); + + let start_position = std::cmp::min(self_start_position, minable_length - mining_length); + let start_position = + (start_position / SECTORS_PER_PRICING as u64) * SECTORS_PER_PRICING as u64; + Some((start_position, mining_length)) + } + + #[inline] + pub(crate) fn is_covered(&self, recall_position: u64) -> Option { + let self_start_position = self.start_position?; + let self_end_position = self.end_position?; + + if self.start_position >= self.end_position { + return Some(false); + } + Some( + self_start_position <= recall_position + SECTORS_PER_LOAD as u64 + || self_end_position > recall_position, + ) + } +} + +impl PoraService { + pub fn spawn( + executor: TaskExecutor, + msg_recv: broadcast::Receiver, + mine_context_receiver: mpsc::UnboundedReceiver, + loader: Arc, + config: &MinerConfig, + ) -> mpsc::UnboundedReceiver { + let (mine_answer_sender, mine_answer_receiver) = + mpsc::unbounded_channel::(); + let mine_range = CustomMineRange { + start_position: Some(0), + end_position: Some(u64::MAX), + }; + let pora = PoraService { + mine_context_receiver, + mine_answer_sender, + msg_recv, + puzzle: None, + mine_range, + miner_id: config.miner_id, + loader, + }; + executor.spawn(async move { Box::pin(pora.start()).await }, "pora_master"); + mine_answer_receiver + } + + async fn start(mut self) { + let mut mining_enabled = true; + let mut channel_opened = true; + loop { + tokio::select! { + biased; + + v = self.msg_recv.recv(), if channel_opened => { + trace!("PoraService receives msg={:?}", v); + match v { + Ok(MinerMessage::ToggleMining(enable)) => { + info!("Toggle mining: {}", if enable { "on" } else { "off" }); + mining_enabled = enable; + } + Ok(MinerMessage::SetStartPosition(pos)) => { + info!("Change start position to: {:?}", pos); + self.mine_range.start_position = pos; + } + Ok(MinerMessage::SetEndPosition(pos)) => { + info!("Change end position to: {:?}", pos); + self.mine_range.end_position = pos; + } + Err(broadcast::error::RecvError::Closed)=>{ + warn!("Unexpected: Mine service config channel closed."); + channel_opened = false; + } + Err(_)=>{ + + } + } + } + + maybe_msg = self.mine_context_receiver.recv() => { + trace!("PoraService receives context={:?}", maybe_msg); + if let Some(msg) = maybe_msg { + debug!("Update mine service: {:?}", msg); + self.puzzle = msg.map(|(context, target_quality)| PoraPuzzle { + context, target_quality + }); + } + } + + _ = async {}, if mining_enabled && self.as_miner().is_some() => { + let nonce = H256(rand::thread_rng().gen()); + let miner = self.as_miner().unwrap(); + if let Some(answer) = miner.iteration(nonce).await{ + debug!("Hit Pora answer {:?}", answer); + if self.mine_answer_sender.send(answer).is_err() { + warn!("Mine submitter channel closed"); + } + } + } + } + } + } + + #[inline] + fn as_miner(&self) -> Option { + match self.puzzle.as_ref() { + Some(puzzle) => self.mine_range.to_valid_range(&puzzle.context).map( + |(start_position, mining_length)| Miner { + start_position, + mining_length, + miner_id: &self.miner_id, + custom_mine_range: &self.mine_range, + context: &puzzle.context, + target_quality: &puzzle.target_quality, + loader: &*self.loader, + }, + ), + _ => None, + } + } +} diff --git a/node/miner/src/pora.rs b/node/miner/src/pora.rs new file mode 100644 index 0000000..bdb1924 --- /dev/null +++ b/node/miner/src/pora.rs @@ -0,0 +1,165 @@ +use crate::{CustomMineRange, PoraLoader}; +use blake2::{Blake2b512, Digest}; +use contract_interface::zgs_flow::MineContext; +use ethereum_types::{H256, U256}; +use zgs_spec::{BYTES_PER_SCRATCHPAD, BYTES_PER_SEAL, SECTORS_PER_LOAD, SECTORS_PER_SEAL}; +use storage::log_store::MineLoadChunk; +use tiny_keccak::{Hasher, Keccak}; + +pub const BLAKE2B_OUTPUT_BYTES: usize = 64; +pub const KECCAK256_OUTPUT_BYTES: usize = 32; + +fn keccak(input: impl AsRef<[u8]>) -> [u8; KECCAK256_OUTPUT_BYTES] { + let mut hasher = Keccak::v256(); + let mut output = [0u8; 32]; + hasher.update(input.as_ref()); + hasher.finalize(&mut output); + output +} + +pub(crate) struct Miner<'a> { + pub start_position: u64, + pub mining_length: u64, + pub miner_id: &'a H256, + pub context: &'a MineContext, + pub target_quality: &'a U256, + pub loader: &'a dyn PoraLoader, + pub custom_mine_range: &'a CustomMineRange, +} +#[derive(Debug)] +pub struct AnswerWithoutProof { + pub context_digest: H256, + pub context_flow_root: H256, + pub nonce: H256, + pub miner_id: H256, + pub start_position: u64, + pub mining_length: u64, + pub recall_position: u64, + pub seal_offset: usize, + pub sealed_data: [u8; BYTES_PER_SEAL], +} + +impl<'a> Miner<'a> { + pub async fn iteration(&self, nonce: H256) -> Option { + let (scratch_pad, recall_seed) = self.make_scratch_pad(&nonce); + + let (_, recall_offset) = U256::from_big_endian(&recall_seed) + .div_mod(U256::from((self.mining_length as usize) / SECTORS_PER_LOAD)); + let recall_offset = recall_offset.as_u64(); + if !self + .custom_mine_range + .is_covered(self.start_position + recall_offset * SECTORS_PER_LOAD as u64) + .unwrap() + { + trace!( + "recall offset not in range: recall_offset={}, range={:?}", + recall_offset, + self.custom_mine_range + ); + return None; + } + + let MineLoadChunk { + loaded_chunk, + avalibilities, + } = self + .loader + .load_sealed_data(self.start_position / SECTORS_PER_LOAD as u64 + recall_offset) + .await?; + + let scratch_pad: [[u8; BYTES_PER_SEAL]; BYTES_PER_SCRATCHPAD / BYTES_PER_SEAL] = + unsafe { std::mem::transmute(scratch_pad) }; + + for ((idx, mut sealed_data), scratch_pad) in loaded_chunk + .into_iter() + .enumerate() + .zip(scratch_pad.iter().cycle()) + .zip(avalibilities.into_iter()) + .filter_map(|(data, avaliable)| avaliable.then_some(data)) + { + // Rust can optimize this loop well. + for (x, y) in sealed_data.iter_mut().zip(scratch_pad.iter()) { + *x ^= y; + } + + let quality = self.pora(idx, &nonce, &sealed_data); + if &quality <= self.target_quality { + debug!("Find a PoRA valid answer, quality: {}", quality); + // Undo mix data when find a valid solition + for (x, y) in sealed_data.iter_mut().zip(scratch_pad.iter()) { + *x ^= y; + } + return Some(AnswerWithoutProof { + context_digest: H256::from(self.context.digest), + context_flow_root: self.context.flow_root.into(), + nonce, + miner_id: *self.miner_id, + start_position: self.start_position, + mining_length: self.mining_length, + recall_position: self.start_position + + recall_offset * SECTORS_PER_LOAD as u64 + + idx as u64 * SECTORS_PER_SEAL as u64, + seal_offset: idx, + sealed_data, + }); + } + } + None + } + + fn make_scratch_pad( + &self, + nonce: &H256, + ) -> ([u8; BYTES_PER_SCRATCHPAD], [u8; KECCAK256_OUTPUT_BYTES]) { + let mut digest: [u8; BLAKE2B_OUTPUT_BYTES] = { + let mut hasher = Blake2b512::new(); + hasher.update(self.miner_id); + hasher.update(nonce); + hasher.update(self.context.digest); + + hasher.update([0u8; 24]); + hasher.update(self.start_position.to_be_bytes()); + + hasher.update([0u8; 24]); + hasher.update(self.mining_length.to_be_bytes()); + + hasher.finalize().into() + }; + + let mut scratch_pad = + [[0u8; BLAKE2B_OUTPUT_BYTES]; BYTES_PER_SCRATCHPAD / BLAKE2B_OUTPUT_BYTES]; + for scratch_pad_cell in scratch_pad.iter_mut() { + digest = Blake2b512::new().chain_update(digest).finalize().into(); + *scratch_pad_cell = digest; + } + + let scratch_pad: [u8; BYTES_PER_SCRATCHPAD] = unsafe { std::mem::transmute(scratch_pad) }; + let recall_seed: [u8; KECCAK256_OUTPUT_BYTES] = keccak(digest); + + (scratch_pad, recall_seed) + } + + #[inline] + fn pora(&self, seal_index: usize, nonce: &H256, mixed_data: &[u8; BYTES_PER_SEAL]) -> U256 { + let mut hasher = Blake2b512::new(); + hasher.update([0u8; 24]); + hasher.update(seal_index.to_be_bytes()); + + hasher.update(self.miner_id); + hasher.update(nonce); + hasher.update(self.context.digest); + + hasher.update([0u8; 24]); + hasher.update(self.start_position.to_be_bytes()); + + hasher.update([0u8; 24]); + hasher.update(self.mining_length.to_be_bytes()); + + hasher.update([0u8; 64]); + hasher.update(mixed_data); + + let digest = hasher.finalize(); + + U256::from_big_endian(&digest[0..32]) + } +} diff --git a/node/miner/src/sealer.rs b/node/miner/src/sealer.rs new file mode 100644 index 0000000..c8de81d --- /dev/null +++ b/node/miner/src/sealer.rs @@ -0,0 +1,213 @@ +use std::{collections::BTreeMap, sync::Arc}; + +use ethereum_types::H256; +use tokio::{ + sync::RwLock, + time::{sleep, Duration, Instant}, +}; + +use contract_interface::{EpochRangeWithContextDigest, ZgsFlow}; +use zgs_spec::SECTORS_PER_SEAL; +use storage::{ + error::Result, + log_store::{SealAnswer, SealTask, Store}, +}; +use task_executor::TaskExecutor; + +use crate::config::{MineServiceMiddleware, MinerConfig}; + +const DB_QUERY_PERIOD_ON_NO_TASK: u64 = 1; +const DB_QUERY_PERIOD_ON_ERROR: u64 = 5; +const CHAIN_STATUS_QUERY_PERIOD: u64 = 5; + +pub struct Sealer { + flow_contract: ZgsFlow, + store: Arc>, + context_cache: BTreeMap, + last_context_flow_length: u64, + miner_id: H256, +} + +impl Sealer { + pub fn spawn( + executor: TaskExecutor, + provider: Arc, + store: Arc>, + config: &MinerConfig, + ) { + let flow_contract = ZgsFlow::new(config.flow_address, provider); + let sealer = Sealer { + flow_contract, + store, + context_cache: Default::default(), + last_context_flow_length: 0, + miner_id: config.miner_id, + }; + + executor.spawn(async move { Box::pin(sealer.start()).await }, "data_sealer"); + } + + async fn start(mut self) { + let db_checker_throttle = sleep(Duration::from_secs(0)); + tokio::pin!(db_checker_throttle); + + let contract_checker_throttle = sleep(Duration::from_secs(0)); + tokio::pin!(contract_checker_throttle); + + loop { + tokio::select! { + biased; + + () = &mut contract_checker_throttle, if !contract_checker_throttle.is_elapsed() => { + } + + () = &mut db_checker_throttle, if !db_checker_throttle.is_elapsed() => { + } + + _ = async {}, if contract_checker_throttle.is_elapsed() => { + if let Err(err) = self.update_flow_length().await{ + warn!("Fetch onchain context failed {:?}", err); + } + contract_checker_throttle.as_mut().reset(Instant::now() + Duration::from_secs(CHAIN_STATUS_QUERY_PERIOD)); + } + + _ = async {}, if db_checker_throttle.is_elapsed() => { + match self.seal_iteration().await { + Ok(true) => {}, + Ok(false) => {db_checker_throttle.as_mut().reset(Instant::now() + Duration::from_secs(DB_QUERY_PERIOD_ON_NO_TASK));} + Err(err) => { + warn!("Seal iteration failed {:?}", err); + db_checker_throttle.as_mut().reset(Instant::now() + Duration::from_secs(DB_QUERY_PERIOD_ON_ERROR)); + } + } + } + } + } + } + + async fn update_flow_length(&mut self) -> Result<()> { + let recent_context = self.flow_contract.make_context_with_result().call().await?; + debug!(target: "seal", "Recent context is {:?}", recent_context); + + let recent_flow_length = recent_context.flow_length.as_u64(); + if self.last_context_flow_length < recent_flow_length { + let epoch_range = self + .flow_contract + .get_epoch_range(recent_context.digest) + .call() + .await?; + self.context_cache.insert( + epoch_range.start, + EpochRangeWithContextDigest { + start: epoch_range.start, + end: epoch_range.end, + digest: recent_context.digest, + }, + ); + self.last_context_flow_length = recent_flow_length; + info!(target: "seal", "Update sealable flow length: {}", recent_flow_length) + } + Ok(()) + } + + async fn fetch_context(&mut self, seal_index: u64) -> Result> { + let last_entry = ((seal_index as usize + 1) * SECTORS_PER_SEAL - 1) as u128; + if self.last_context_flow_length <= last_entry as u64 { + return Ok(None); + } + + if let Some((_, context)) = self.context_cache.range(..=last_entry).next_back() { + if context.start <= last_entry && context.end > last_entry { + return Ok(Some(( + H256(context.digest), + context.end as u64 / SECTORS_PER_SEAL as u64, + ))); + } + } + + let context = match self + .flow_contract + .query_context_at_position(last_entry) + .call() + .await + { + Ok(context) => context, + Err(err) => { + info!("Error when fetch entries {:?}", err); + return Ok(None); + } + }; + info!( + target:"seal", "Fetch new context: range {} -> {}", + context.start, context.end + ); + self.context_cache.insert(context.start, context.clone()); + + Ok(Some(( + H256(context.digest), + context.end as u64 / SECTORS_PER_SEAL as u64, + ))) + } + + async fn fetch_task(&self) -> Result>> { + let seal_index_max = self.last_context_flow_length as usize / SECTORS_PER_SEAL; + self.store + .read() + .await + .flow() + .pull_seal_chunk(seal_index_max) + } + + async fn submit_answer(&self, answers: Vec) -> Result<()> { + self.store + .write() + .await + .flow_mut() + .submit_seal_result(answers) + } + + async fn seal_iteration(&mut self) -> Result { + let tasks = match self.fetch_task().await? { + Some(tasks) if !tasks.is_empty() => tasks, + _ => { + return Ok(false); + } + }; + + debug!( + "Get seal tasks at seal index {:?}", + tasks.iter().map(|x| x.seal_index).collect::>() + ); + + let mut answers = Vec::with_capacity(tasks.len()); + + for task in tasks { + let (context_digest, end_seal) = + if let Some(context) = self.fetch_context(task.seal_index).await? { + context + } else { + trace!(target: "seal", "Index {} is not ready for seal", task.seal_index); + continue; + }; + let mut data = task.non_sealed_data; + zgs_seal::seal( + &mut data, + &self.miner_id, + &context_digest, + task.seal_index * SECTORS_PER_SEAL as u64, + ); + answers.push(SealAnswer { + seal_index: task.seal_index, + version: task.version, + sealed_data: data, + miner_id: self.miner_id, + seal_context: context_digest, + context_end_seal: end_seal, + }); + } + + self.submit_answer(answers).await?; + + Ok(true) + } +} diff --git a/node/miner/src/service.rs b/node/miner/src/service.rs new file mode 100644 index 0000000..5575597 --- /dev/null +++ b/node/miner/src/service.rs @@ -0,0 +1,62 @@ +use crate::sealer::Sealer; +use crate::submitter::Submitter; +use crate::{config::MinerConfig, mine::PoraService, watcher::MineContextWatcher}; +use network::NetworkMessage; +use std::sync::Arc; +use storage::log_store::Store; +use tokio::sync::mpsc; +use tokio::sync::{broadcast, RwLock}; + +#[derive(Clone, Debug)] +pub enum MinerMessage { + /// Enable / Disable Mining + ToggleMining(bool), + + /// Change mining range + SetStartPosition(Option), + SetEndPosition(Option), +} + +pub struct MineService; + +impl MineService { + pub async fn spawn( + executor: task_executor::TaskExecutor, + _network_send: mpsc::UnboundedSender, + config: MinerConfig, + store: Arc>, + ) -> Result, String> { + let provider = Arc::new(config.make_provider().await?); + + let (msg_send, msg_recv) = broadcast::channel(1024); + + let mine_context_receiver = MineContextWatcher::spawn( + executor.clone(), + msg_recv.resubscribe(), + provider.clone(), + &config, + ); + + let mine_answer_receiver = PoraService::spawn( + executor.clone(), + msg_recv.resubscribe(), + mine_context_receiver, + Arc::new(store.clone()), + &config, + ); + + Submitter::spawn( + executor.clone(), + mine_answer_receiver, + provider.clone(), + store.clone(), + &config, + ); + + Sealer::spawn(executor, provider, store, &config); + + debug!("Starting miner service"); + + Ok(msg_send) + } +} diff --git a/node/miner/src/submitter.rs b/node/miner/src/submitter.rs new file mode 100644 index 0000000..958915f --- /dev/null +++ b/node/miner/src/submitter.rs @@ -0,0 +1,128 @@ +use contract_interface::PoraAnswer; +use contract_interface::{ZgsFlow, PoraMine}; +use ethers::providers::PendingTransaction; +use shared_types::FlowRangeProof; +use std::sync::Arc; +use storage::log_store::Store; +use task_executor::TaskExecutor; +use tokio::sync::{mpsc, RwLock}; + +use crate::config::{MineServiceMiddleware, MinerConfig}; +use crate::pora::AnswerWithoutProof; + +use zgs_spec::SECTORS_PER_SEAL; + +const SUBMISSION_RETIES: usize = 3; + +pub struct Submitter { + mine_answer_receiver: mpsc::UnboundedReceiver, + mine_contract: PoraMine, + flow_contract: ZgsFlow, + store: Arc>, +} + +impl Submitter { + pub fn spawn( + executor: TaskExecutor, + mine_answer_receiver: mpsc::UnboundedReceiver, + provider: Arc, + store: Arc>, + config: &MinerConfig, + ) { + let mine_contract = PoraMine::new(config.mine_address, provider.clone()); + let flow_contract = ZgsFlow::new(config.flow_address, provider); + + let submitter = Submitter { + mine_answer_receiver, + mine_contract, + flow_contract, + store, + }; + executor.spawn( + async move { Box::pin(submitter.start()).await }, + "mine_answer_submitter", + ); + } + + async fn start(mut self) { + loop { + match self.mine_answer_receiver.recv().await { + Some(answer) => { + if let Err(e) = self.submit_answer(answer).await { + warn!(e) + } + } + None => { + warn!("Mine submitter stopped because mine answer channel is closed."); + break; + } + }; + } + } + + async fn submit_answer(&mut self, mine_answer: AnswerWithoutProof) -> Result<(), String> { + debug!("submit answer: {:?}", mine_answer); + let sealed_context_digest = self + .flow_contract + .query_context_at_position( + (mine_answer.recall_position + SECTORS_PER_SEAL as u64 - 1) as u128, + ) + .call() + .await + .map_err(|e| format!("Failed to fetch sealed contest digest: {:?}", e))?; + debug!("Fetch sealed context: {:?}", sealed_context_digest); + + let flow_proof = self + .store + .read() + .await + .get_proof_at_root( + &mine_answer.context_flow_root, + mine_answer.recall_position, + SECTORS_PER_SEAL as u64, + ) + .map_err(|e| e.to_string())?; + + let answer = PoraAnswer { + context_digest: mine_answer.context_digest.0, + nonce: mine_answer.nonce.0, + miner_id: mine_answer.miner_id.0, + start_position: mine_answer.start_position.into(), + mine_length: mine_answer.mining_length.into(), + recall_position: mine_answer.recall_position.into(), + seal_offset: mine_answer.seal_offset.into(), + sealed_context_digest: sealed_context_digest.digest, // TODO(kevin): wait for implementation of data sealing. + sealed_data: unsafe { std::mem::transmute(mine_answer.sealed_data) }, + merkle_proof: flow_proof_to_pora_merkle_proof(flow_proof), + }; + trace!("submit_answer: answer={:?}", answer); + + let submission_call = self.mine_contract.submit(answer).legacy(); + let pending_transaction: PendingTransaction<'_, _> = submission_call + .send() + .await + .map_err(|e| format!("Fail to send mine answer transaction: {:?}", e))?; + + let receipt = pending_transaction + .retries(SUBMISSION_RETIES) + .await + .map_err(|e| format!("Fail to execute mine answer transaction: {:?}", e))? + .ok_or(format!( + "Mine answer transaction dropped after {} retires", + SUBMISSION_RETIES + ))?; + + info!("Submit PoRA sucess"); + debug!("Receipt: {:?}", receipt); + + Ok(()) + } +} + +// TODO: The conversion will be simpler if we optimize range proof structure. +fn flow_proof_to_pora_merkle_proof(flow_proof: FlowRangeProof) -> Vec<[u8; 32]> { + let depth_in_sealed_data = SECTORS_PER_SEAL.trailing_zeros() as usize; + let full_proof: Vec<[u8; 32]> = flow_proof.left_proof.lemma().iter().map(|h| h.0).collect(); + // Exclude `item`, the nodes in the sealed data subtree, and `root`. + full_proof[depth_in_sealed_data + 1..full_proof.len() - 1].to_vec() +} diff --git a/node/miner/src/watcher.rs b/node/miner/src/watcher.rs new file mode 100644 index 0000000..b4dcf97 --- /dev/null +++ b/node/miner/src/watcher.rs @@ -0,0 +1,142 @@ +#![allow(unused)] + +use contract_interface::{zgs_flow::MineContext, ZgsFlow, PoraMine}; +use ethereum_types::{Address, H256, U256}; +use ethers::{ + contract::Contract, + providers::{JsonRpcClient, Middleware, Provider, StreamExt}, + types::BlockId, +}; +use task_executor::TaskExecutor; +use tokio::{ + sync::{broadcast, mpsc}, + time::{sleep, Instant, Sleep}, + try_join, +}; + +use std::pin::Pin; +use std::sync::Arc; +use std::time::Duration; +use std::{ops::DerefMut, str::FromStr}; + +use crate::{config::MineServiceMiddleware, MinerConfig, MinerMessage}; + +pub type MineContextMessage = Option<(MineContext, U256)>; + +lazy_static! { + pub static ref EMPTY_HASH: H256 = + H256::from_str("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470").unwrap(); +} + +pub struct MineContextWatcher { + provider: Arc, + flow_contract: ZgsFlow, + mine_contract: PoraMine, + + mine_context_sender: mpsc::UnboundedSender, + last_report: MineContextMessage, + + msg_recv: broadcast::Receiver, +} + +impl MineContextWatcher { + pub fn spawn( + executor: TaskExecutor, + msg_recv: broadcast::Receiver, + provider: Arc, + config: &MinerConfig, + ) -> mpsc::UnboundedReceiver { + let provider = provider; + + let mine_contract = PoraMine::new(config.mine_address, provider.clone()); + let flow_contract = ZgsFlow::new(config.flow_address, provider.clone()); + + let (mine_context_sender, mine_context_receiver) = + mpsc::unbounded_channel::(); + let watcher = MineContextWatcher { + provider, + flow_contract, + mine_contract, + mine_context_sender, + msg_recv, + last_report: None, + }; + executor.spawn( + async move { Box::pin(watcher.start()).await }, + "mine_context_watcher", + ); + mine_context_receiver + } + + async fn start(mut self) { + let mut mining_enabled = true; + let mut channel_opened = true; + + let mut mining_throttle = sleep(Duration::from_secs(0)); + tokio::pin!(mining_throttle); + + loop { + tokio::select! { + biased; + + v = self.msg_recv.recv(), if channel_opened => { + match v { + Ok(MinerMessage::ToggleMining(enable)) => { + mining_enabled = enable; + } + Err(broadcast::error::RecvError::Closed) => { + channel_opened = false; + } + _ => {} + } + } + + () = &mut mining_throttle, if !mining_throttle.is_elapsed() => { + } + + _ = async {}, if mining_enabled && mining_throttle.is_elapsed() => { + mining_throttle.as_mut().reset(Instant::now() + Duration::from_secs(1)); + if let Err(err) = self.query_recent_context().await { + warn!(err); + } + } + } + } + } + + async fn query_recent_context(&mut self) -> Result<(), String> { + // let mut watcher = self + // .provider + // .watch_blocks() + // .await + // .expect("should success") + // .stream(); + // watcher.next().await + let context_call = self.flow_contract.make_context_with_result(); + let epoch_call = self.mine_contract.last_mined_epoch(); + let quality_call = self.mine_contract.target_quality(); + + let (context, epoch, quality) = + try_join!(context_call.call(), epoch_call.call(), quality_call.call()) + .map_err(|e| format!("Failed to query mining context: {:?}", e))?; + let report = if context.epoch > epoch && context.digest != EMPTY_HASH.0 { + if context.block_digest == [0; 32] { + warn!("Mine Context is not updated on time."); + None + } else { + Some((context, quality)) + } + } else { + None + }; + + if report != self.last_report { + self.mine_context_sender + .send(report.clone()) + .map_err(|e| format!("Failed to send out the most recent mine context: {:?}", e))?; + } + self.last_report = report; + + Ok(()) + } +} diff --git a/node/network/Cargo.toml b/node/network/Cargo.toml new file mode 100644 index 0000000..3cc7052 --- /dev/null +++ b/node/network/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "network" +version = "0.2.0" +edition = "2021" + +[dependencies] +directory = { path = "../../common/directory" } +dirs = "4.0.0" +discv5 = { version = "0.1.0-beta.13", features = ["libp2p"] } +error-chain = "0.12.4" +eth2_ssz = "0.4.0" +eth2_ssz_derive = "0.3.0" +eth2_ssz_types = "0.2.1" +ethereum-types = "0.14" +fnv = "1.0.7" +futures = "0.3.21" +hashset_delay = { path = "../../common/hashset_delay" } +hex = "0.4.3" +zgs_version = { path = "../../common/zgs_version" } +lazy_static = "1.4.0" +lighthouse_metrics = { path = "../../common/lighthouse_metrics" } +lru = "0.7.7" +parking_lot = "0.12.1" +prometheus-client = "0.16.0" +rand = "0.8.5" +regex = "1.5.6" +serde = { version = "1.0.137", features = ["derive"] } +serde_derive = "1.0.137" +sha2 = "0.10.2" +shared_types = { path = "../shared_types" } +smallvec = "1.8.0" +snap = "1.0.5" +strum = { version = "0.24.1", features = ["derive"] } +task_executor = { path = "../../common/task_executor" } +tiny-keccak = "2.0.2" +tokio = { version = "1.19.2", features = ["time", "macros"] } +tokio-io-timeout = "1.2.0" +tokio-util = { version = "0.6.10", features = ["codec", "compat", "time"] } +tracing = "0.1.35" +unsigned-varint = { version = "0.7.1", features = ["codec"] } +if-addrs = "0.10.1" +slog = "2.7.0" +igd = "0.12.1" + +[dependencies.libp2p] +version = "0.45.1" +default-features = false +features = ["websocket", "identify", "mplex", "yamux", "noise", "gossipsub", "dns-tokio", "tcp-tokio", "plaintext", "secp256k1"] + +[dev-dependencies] +exit-future = "0.2.0" +tempfile = "3.3.0" +tracing-test = "0.2.2" +unused_port = { path = "../../common/unused_port" } +void = "1.0.2" diff --git a/node/network/src/behaviour/gossip_cache.rs b/node/network/src/behaviour/gossip_cache.rs new file mode 100644 index 0000000..4ea3cad --- /dev/null +++ b/node/network/src/behaviour/gossip_cache.rs @@ -0,0 +1,183 @@ +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::Duration; + +use crate::types::GossipKind; +use crate::GossipTopic; + +use tokio_util::time::delay_queue::{DelayQueue, Key}; + +/// Store of gossip messages that we failed to publish and will try again later. By default, all +/// messages are ignored. This behaviour can be changed using `GossipCacheBuilder::default_timeout` +/// to apply the same delay to every kind. Individual timeouts for specific kinds can be set and +/// will overwrite the default_timeout if present. +pub struct GossipCache { + /// Expire timeouts for each topic-msg pair. + expirations: DelayQueue<(GossipTopic, Vec)>, + /// Messages cached for each topic. + topic_msgs: HashMap, Key>>, + /// Timeout for Example messages. + example: Option, + /// Timeout for FindFile messages. + find_file: Option, + /// Timeout for AnnounceFile. + announce_file: Option, +} + +#[derive(Default)] +pub struct GossipCacheBuilder { + default_timeout: Option, + /// Timeout for Example messages. + example: Option, + /// Timeout for blocks FindFile messages. + find_file: Option, + /// Timeout for AnnounceFile messages. + announce_file: Option, +} + +#[allow(dead_code)] +impl GossipCacheBuilder { + /// By default, all timeouts all disabled. Setting a default timeout will enable all timeout + /// that are not already set. + pub fn default_timeout(mut self, timeout: Duration) -> Self { + self.default_timeout = Some(timeout); + self + } + + /// Timeout for Example messages. + pub fn example_timeout(mut self, timeout: Duration) -> Self { + self.example = Some(timeout); + self + } + + /// Timeout for FindFile messages. + pub fn find_file_timeout(mut self, timeout: Duration) -> Self { + self.find_file = Some(timeout); + self + } + + /// Timeout for AnnounceFile messages. + pub fn announce_file_timeout(mut self, timeout: Duration) -> Self { + self.announce_file = Some(timeout); + self + } + + pub fn build(self) -> GossipCache { + let GossipCacheBuilder { + default_timeout, + example, + find_file, + announce_file, + } = self; + + GossipCache { + expirations: DelayQueue::default(), + topic_msgs: HashMap::default(), + example: example.or(default_timeout), + find_file: find_file.or(default_timeout), + announce_file: announce_file.or(default_timeout), + } + } +} + +impl GossipCache { + /// Get a builder of a `GossipCache`. Topic kinds for which no timeout is defined will be + /// ignored if added in `insert`. + pub fn builder() -> GossipCacheBuilder { + GossipCacheBuilder::default() + } + + // Insert a message to be sent later. + pub fn insert(&mut self, topic: GossipTopic, data: Vec) { + let expire_timeout = match topic.kind() { + GossipKind::Example => self.example, + GossipKind::FindFile => self.find_file, + GossipKind::AnnounceFile => self.announce_file, + }; + + let expire_timeout = match expire_timeout { + Some(expire_timeout) => expire_timeout, + None => return, + }; + + match self + .topic_msgs + .entry(topic.clone()) + .or_default() + .entry(data.clone()) + { + Entry::Occupied(key) => self.expirations.reset(key.get(), expire_timeout), + Entry::Vacant(entry) => { + let key = self.expirations.insert((topic, data), expire_timeout); + entry.insert(key); + } + } + } + + // Get the registered messages for this topic. + pub fn retrieve(&mut self, topic: &GossipTopic) -> Option> + '_> { + if let Some(msgs) = self.topic_msgs.remove(topic) { + for (_, key) in msgs.iter() { + self.expirations.remove(key); + } + Some(msgs.into_keys()) + } else { + None + } + } +} + +impl futures::stream::Stream for GossipCache { + type Item = Result; // We don't care to retrieve the expired data. + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.expirations.poll_expired(cx) { + Poll::Ready(Some(Ok(expired))) => { + let expected_key = expired.key(); + let (topic, data) = expired.into_inner(); + match self.topic_msgs.get_mut(&topic) { + Some(msgs) => { + let key = msgs.remove(&data); + debug_assert_eq!(key, Some(expected_key)); + if msgs.is_empty() { + // no more messages for this topic. + self.topic_msgs.remove(&topic); + } + } + None => { + #[cfg(debug_assertions)] + panic!("Topic for registered message is not present.") + } + } + Poll::Ready(Some(Ok(topic))) + } + Poll::Ready(Some(Err(x))) => Poll::Ready(Some(Err(x.to_string()))), + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + } + } +} + +#[cfg(test)] +mod tests { + use crate::types::GossipKind; + + use super::*; + use futures::stream::StreamExt; + + #[tokio::test] + async fn test_stream() { + let mut cache = GossipCache::builder() + .default_timeout(Duration::from_millis(300)) + .build(); + let test_topic = + GossipTopic::new(GossipKind::Example, crate::types::GossipEncoding::SSZSnappy); + cache.insert(test_topic, vec![]); + tokio::time::sleep(Duration::from_millis(300)).await; + while cache.next().await.is_some() {} + assert!(cache.expirations.is_empty()); + assert!(cache.topic_msgs.is_empty()); + } +} diff --git a/node/network/src/behaviour/gossipsub_scoring_parameters.rs b/node/network/src/behaviour/gossipsub_scoring_parameters.rs new file mode 100644 index 0000000..dbe6e68 --- /dev/null +++ b/node/network/src/behaviour/gossipsub_scoring_parameters.rs @@ -0,0 +1,354 @@ +// use crate::error; +use libp2p::gossipsub::PeerScoreThresholds; +// use std::cmp::max; +// use std::collections::HashMap; +// use std::time::Duration; + +// const MAX_IN_MESH_SCORE: f64 = 10.0; +// const MAX_FIRST_MESSAGE_DELIVERIES_SCORE: f64 = 40.0; +// const BEACON_BLOCK_WEIGHT: f64 = 0.5; +// const BEACON_AGGREGATE_PROOF_WEIGHT: f64 = 0.5; +// const VOLUNTARY_EXIT_WEIGHT: f64 = 0.05; +// const PROPOSER_SLASHING_WEIGHT: f64 = 0.05; +// const ATTESTER_SLASHING_WEIGHT: f64 = 0.05; + +/// The time window (seconds) that we expect messages to be forwarded to us in the mesh. +// const MESH_MESSAGE_DELIVERIES_WINDOW: u64 = 2; + +// Const as this is used in the peer manager to prevent gossip from disconnecting peers. +pub const GREYLIST_THRESHOLD: f64 = -16000.0; + +/// Builds the peer score thresholds. +pub fn lighthouse_gossip_thresholds() -> PeerScoreThresholds { + PeerScoreThresholds { + gossip_threshold: -4000.0, + publish_threshold: -8000.0, + graylist_threshold: GREYLIST_THRESHOLD, + accept_px_threshold: 100.0, + opportunistic_graft_threshold: 5.0, + } +} + +// pub struct PeerScoreSettings { +// // slot: Duration, +// epoch: Duration, + +// // beacon_attestation_subnet_weight: f64, +// max_positive_score: f64, + +// decay_interval: Duration, +// decay_to_zero: f64, + +// // mesh_n: usize, +// // max_committees_per_slot: usize, +// // target_committee_size: usize, +// // target_aggregators_per_committee: usize, +// attestation_subnet_count: u64, +// } + +// impl PeerScoreSettings { +// pub fn new(chain_spec: &ChainSpec) -> PeerScoreSettings { +// let slot = Duration::from_secs(chain_spec.seconds_per_slot); +// let beacon_attestation_subnet_weight = 1.0 / chain_spec.attestation_subnet_count as f64; +// let max_positive_score = (MAX_IN_MESH_SCORE + MAX_FIRST_MESSAGE_DELIVERIES_SCORE) +// * (BEACON_BLOCK_WEIGHT +// + BEACON_AGGREGATE_PROOF_WEIGHT +// + beacon_attestation_subnet_weight * chain_spec.attestation_subnet_count as f64 +// + VOLUNTARY_EXIT_WEIGHT +// + PROPOSER_SLASHING_WEIGHT +// + ATTESTER_SLASHING_WEIGHT); + +// PeerScoreSettings { +// // slot, +// // epoch: slot * TSpec::slots_per_epoch() as u32, +// epoch: slot * 32 as u32, +// // beacon_attestation_subnet_weight, +// max_positive_score, +// decay_interval: max(Duration::from_secs(1), slot), +// decay_to_zero: 0.01, +// // mesh_n: gs_config.mesh_n(), +// // max_committees_per_slot: chain_spec.max_committees_per_slot, +// // target_committee_size: chain_spec.target_committee_size, +// // target_aggregators_per_committee: chain_spec.target_aggregators_per_committee as usize, +// attestation_subnet_count: chain_spec.attestation_subnet_count, +// } +// } + +// pub fn get_peer_score_params( +// &self, +// thresholds: &PeerScoreThresholds, +// ) -> error::Result { +// let mut params = PeerScoreParams { +// decay_interval: self.decay_interval, +// decay_to_zero: self.decay_to_zero, +// retain_score: self.epoch * 100, +// app_specific_weight: 1.0, +// ip_colocation_factor_threshold: 8.0, // Allow up to 8 nodes per IP +// behaviour_penalty_threshold: 6.0, +// behaviour_penalty_decay: self.score_parameter_decay(self.epoch * 10), +// ..Default::default() +// }; + +// let target_value = Self::decay_convergence( +// params.behaviour_penalty_decay, +// // 10.0 / TSpec::slots_per_epoch() as f64, +// 10.0 / 32 as f64, +// ) - params.behaviour_penalty_threshold; +// params.behaviour_penalty_weight = thresholds.gossip_threshold / target_value.powi(2); + +// params.topic_score_cap = self.max_positive_score * 0.5; +// params.ip_colocation_factor_weight = -params.topic_score_cap; + +// params.topics = HashMap::new(); + +// // let get_hash = |kind: GossipKind| -> TopicHash { +// // let topic: Topic = +// // GossipTopic::new(kind, GossipEncoding::default()).into(); +// // topic.hash() +// // }; + +// // //first all fixed topics +// // params.topics.insert( +// // get_hash(GossipKind::VoluntaryExit), +// // Self::get_topic_params( +// // self, +// // VOLUNTARY_EXIT_WEIGHT, +// // // 4.0 / TSpec::slots_per_epoch() as f64, +// // 4.0 / 32 as f64, +// // self.epoch * 100, +// // None, +// // ), +// // ); +// // params.topics.insert( +// // get_hash(GossipKind::AttesterSlashing), +// // Self::get_topic_params( +// // self, +// // ATTESTER_SLASHING_WEIGHT, +// // // 1.0 / 5.0 / TSpec::slots_per_epoch() as f64, +// // 1.0 / 5.0 / 32 as f64, +// // self.epoch * 100, +// // None, +// // ), +// // ); +// // params.topics.insert( +// // get_hash(GossipKind::ProposerSlashing), +// // Self::get_topic_params( +// // self, +// // PROPOSER_SLASHING_WEIGHT, +// // // 1.0 / 5.0 / TSpec::slots_per_epoch() as f64, +// // 1.0 / 5.0 / 32 as f64, +// // self.epoch * 100, +// // None, +// // ), +// // ); + +// //dynamic topics +// // let (beacon_block_params, beacon_aggregate_proof_params, beacon_attestation_subnet_params) = +// // self.get_dynamic_topic_params(active_validators, current_slot)?; + +// // params +// // .topics +// // .insert(get_hash(GossipKind::BeaconBlock), beacon_block_params); + +// // params.topics.insert( +// // get_hash(GossipKind::BeaconAggregateAndProof), +// // beacon_aggregate_proof_params, +// // ); + +// // for i in 0..self.attestation_subnet_count { +// // params.topics.insert( +// // get_hash(GossipKind::Attestation(SubnetId::new(i))), +// // beacon_attestation_subnet_params.clone(), +// // ); +// // } + +// Ok(params) +// } + +// // pub fn get_dynamic_topic_params( +// // &self, +// // active_validators: usize, +// // current_slot: Slot, +// // ) -> error::Result<(TopicScoreParams, TopicScoreParams, TopicScoreParams)> { +// // let (aggregators_per_slot, committees_per_slot) = +// // self.expected_aggregator_count_per_slot(active_validators)?; +// // let multiple_bursts_per_subnet_per_epoch = committees_per_slot as u64 +// // >= 2 * self.attestation_subnet_count / TSpec::slots_per_epoch(); + +// // let beacon_block_params = Self::get_topic_params( +// // self, +// // BEACON_BLOCK_WEIGHT, +// // 1.0, +// // self.epoch * 20, +// // Some((TSpec::slots_per_epoch() * 5, 3.0, self.epoch, current_slot)), +// // ); + +// // let beacon_aggregate_proof_params = Self::get_topic_params( +// // self, +// // BEACON_AGGREGATE_PROOF_WEIGHT, +// // aggregators_per_slot, +// // self.epoch, +// // Some((TSpec::slots_per_epoch() * 2, 4.0, self.epoch, current_slot)), +// // ); +// // let beacon_attestation_subnet_params = Self::get_topic_params( +// // self, +// // self.beacon_attestation_subnet_weight, +// // active_validators as f64 +// // / self.attestation_subnet_count as f64 +// // / TSpec::slots_per_epoch() as f64, +// // self.epoch +// // * (if multiple_bursts_per_subnet_per_epoch { +// // 1 +// // } else { +// // 4 +// // }), +// // Some(( +// // TSpec::slots_per_epoch() +// // * (if multiple_bursts_per_subnet_per_epoch { +// // 4 +// // } else { +// // 16 +// // }), +// // 16.0, +// // if multiple_bursts_per_subnet_per_epoch { +// // self.slot * (TSpec::slots_per_epoch() as u32 / 2 + 1) +// // } else { +// // self.epoch * 3 +// // }, +// // current_slot, +// // )), +// // ); + +// // Ok(( +// // beacon_block_params, +// // beacon_aggregate_proof_params, +// // beacon_attestation_subnet_params, +// // )) +// // } + +// pub fn attestation_subnet_count(&self) -> u64 { +// self.attestation_subnet_count +// } + +// fn score_parameter_decay_with_base( +// decay_time: Duration, +// decay_interval: Duration, +// decay_to_zero: f64, +// ) -> f64 { +// let ticks = decay_time.as_secs_f64() / decay_interval.as_secs_f64(); +// decay_to_zero.powf(1.0 / ticks) +// } + +// fn decay_convergence(decay: f64, rate: f64) -> f64 { +// rate / (1.0 - decay) +// } + +// // fn threshold(decay: f64, rate: f64) -> f64 { +// // Self::decay_convergence(decay, rate) * decay +// // } + +// // fn expected_aggregator_count_per_slot( +// // &self, +// // active_validators: usize, +// // ) -> error::Result<(f64, usize)> { +// // let committees_per_slot = TSpec::get_committee_count_per_slot_with( +// // active_validators, +// // self.max_committees_per_slot, +// // self.target_committee_size, +// // ) +// // .map_err(|e| format!("Could not get committee count from spec: {:?}", e))?; + +// // let committees = committees_per_slot * TSpec::slots_per_epoch() as usize; + +// // let smaller_committee_size = active_validators / committees; +// // let num_larger_committees = active_validators - smaller_committee_size * committees; + +// // let modulo_smaller = max( +// // 1, +// // smaller_committee_size / self.target_aggregators_per_committee as usize, +// // ); +// // let modulo_larger = max( +// // 1, +// // (smaller_committee_size + 1) / self.target_aggregators_per_committee as usize, +// // ); + +// // Ok(( +// // (((committees - num_larger_committees) * smaller_committee_size) as f64 +// // / modulo_smaller as f64 +// // + (num_larger_committees * (smaller_committee_size + 1)) as f64 +// // / modulo_larger as f64) +// // / TSpec::slots_per_epoch() as f64, +// // committees_per_slot, +// // )) +// // } + +// fn score_parameter_decay(&self, decay_time: Duration) -> f64 { +// Self::score_parameter_decay_with_base(decay_time, self.decay_interval, self.decay_to_zero) +// } + +// // fn get_topic_params( +// // &self, +// // topic_weight: f64, +// // expected_message_rate: f64, +// // first_message_decay_time: Duration, +// // // decay slots (decay time in slots), cap factor, activation window, current slot +// // mesh_message_info: Option<(u64, f64, Duration, Slot)>, +// // ) -> TopicScoreParams { +// // let mut t_params = TopicScoreParams::default(); + +// // t_params.topic_weight = topic_weight; + +// // t_params.time_in_mesh_quantum = self.slot; +// // t_params.time_in_mesh_cap = 3600.0 / t_params.time_in_mesh_quantum.as_secs_f64(); +// // t_params.time_in_mesh_weight = 10.0 / t_params.time_in_mesh_cap; + +// // t_params.first_message_deliveries_decay = +// // self.score_parameter_decay(first_message_decay_time); +// // t_params.first_message_deliveries_cap = Self::decay_convergence( +// // t_params.first_message_deliveries_decay, +// // 2.0 * expected_message_rate / self.mesh_n as f64, +// // ); +// // t_params.first_message_deliveries_weight = 40.0 / t_params.first_message_deliveries_cap; + +// // if let Some((decay_slots, cap_factor, activation_window, current_slot)) = mesh_message_info +// // { +// // let decay_time = self.slot * decay_slots as u32; +// // t_params.mesh_message_deliveries_decay = self.score_parameter_decay(decay_time); +// // t_params.mesh_message_deliveries_threshold = Self::threshold( +// // t_params.mesh_message_deliveries_decay, +// // expected_message_rate / 50.0, +// // ); +// // t_params.mesh_message_deliveries_cap = +// // if cap_factor * t_params.mesh_message_deliveries_threshold < 2.0 { +// // 2.0 +// // } else { +// // cap_factor * t_params.mesh_message_deliveries_threshold +// // }; +// // t_params.mesh_message_deliveries_activation = activation_window; +// // t_params.mesh_message_deliveries_window = +// // Duration::from_secs(MESH_MESSAGE_DELIVERIES_WINDOW); +// // t_params.mesh_failure_penalty_decay = t_params.mesh_message_deliveries_decay; +// // t_params.mesh_message_deliveries_weight = -t_params.topic_weight; +// // t_params.mesh_failure_penalty_weight = t_params.mesh_message_deliveries_weight; +// // if decay_slots >= current_slot.as_u64() { +// // t_params.mesh_message_deliveries_threshold = 0.0; +// // t_params.mesh_message_deliveries_weight = 0.0; +// // } +// // } else { +// // t_params.mesh_message_deliveries_weight = 0.0; +// // t_params.mesh_message_deliveries_threshold = 0.0; +// // t_params.mesh_message_deliveries_decay = 0.0; +// // t_params.mesh_message_deliveries_cap = 0.0; +// // t_params.mesh_message_deliveries_window = Duration::from_secs(0); +// // t_params.mesh_message_deliveries_activation = Duration::from_secs(0); +// // t_params.mesh_failure_penalty_decay = 0.0; +// // t_params.mesh_failure_penalty_weight = 0.0; +// // } + +// // t_params.invalid_message_deliveries_weight = +// // -self.max_positive_score / t_params.topic_weight; +// // t_params.invalid_message_deliveries_decay = self.score_parameter_decay(self.epoch * 50); + +// // t_params +// // } +// } diff --git a/node/network/src/behaviour/mod.rs b/node/network/src/behaviour/mod.rs new file mode 100644 index 0000000..c035d44 --- /dev/null +++ b/node/network/src/behaviour/mod.rs @@ -0,0 +1,989 @@ +use crate::behaviour::gossipsub_scoring_parameters::lighthouse_gossip_thresholds; +use crate::config::gossipsub_config; +use crate::discovery::{Discovery, DiscoveryEvent, FIND_NODE_QUERY_CLOSEST_PEERS}; +use crate::peer_manager::{ + config::Config as PeerManagerCfg, peerdb::score::PeerAction, peerdb::score::ReportSource, + ConnectionDirection, PeerManager, PeerManagerEvent, +}; +use crate::rpc::methods::DataByHashRequest; +use crate::rpc::methods::GetChunksRequest; +use crate::rpc::*; +use crate::service::Context as ServiceContext; +use crate::types::{GossipEncoding, GossipKind, GossipTopic, SnappyTransform}; +use crate::{error, metrics, Enr, NetworkGlobals, PubsubMessage, TopicHash}; +use futures::stream::StreamExt; +use libp2p::gossipsub::error::PublishError; +use libp2p::{ + core::{ + connection::ConnectionId, identity::Keypair, multiaddr::Protocol as MProtocol, Multiaddr, + }, + gossipsub::{ + subscription_filter::{AllowAllSubscriptionFilter, MaxCountSubscriptionFilter}, + Gossipsub as BaseGossipsub, GossipsubEvent, IdentTopic as Topic, MessageAcceptance, + MessageAuthenticity, MessageId, + }, + identify::{Identify, IdentifyConfig, IdentifyEvent}, + swarm::{ + dial_opts::{DialOpts, PeerCondition}, + AddressScore, NetworkBehaviour, NetworkBehaviourAction as NBAction, + NetworkBehaviourEventProcess, PollParameters, + }, + NetworkBehaviour, PeerId, +}; +use shared_types::ChunkArrayWithProof; +use std::{ + collections::VecDeque, + sync::Arc, + task::{Context, Poll}, +}; + +use self::gossip_cache::GossipCache; + +mod gossip_cache; +pub mod gossipsub_scoring_parameters; + +/// The number of peers we target per subnet for discovery queries. +pub const TARGET_SUBNET_PEERS: usize = 6; + +const MAX_IDENTIFY_ADDRESSES: usize = 10; + +/// Identifier of requests sent by a peer. +pub type PeerRequestId = (ConnectionId, SubstreamId); + +pub type SubscriptionFilter = MaxCountSubscriptionFilter; +pub type Gossipsub = BaseGossipsub; + +/// Identifier of a request. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RequestId { + Application(AppReqId), + Behaviour, +} + +/// The types of events than can be obtained from polling the behaviour. +#[derive(Debug)] +pub enum BehaviourEvent { + /// We have successfully dialed and connected to a peer. + PeerConnectedOutgoing(PeerId), + /// A peer has successfully dialed and connected to us. + PeerConnectedIncoming(PeerId), + /// A peer has disconnected. + PeerDisconnected(PeerId), + /// The peer needs to be banned. + PeerBanned(PeerId), + /// The peer has been unbanned. + PeerUnbanned(PeerId), + /// An RPC Request that was sent failed. + RPCFailed { + /// The id of the failed request. + id: AppReqId, + /// The peer to which this request was sent. + peer_id: PeerId, + }, + RequestReceived { + /// The peer that sent the request. + peer_id: PeerId, + /// Identifier of the request. All responses to this request must use this id. + id: PeerRequestId, + /// Request the peer sent. + request: Request, + }, + ResponseReceived { + /// Peer that sent the response. + peer_id: PeerId, + /// Id of the request to which the peer is responding. + id: AppReqId, + /// Response the peer sent. + response: Response, + }, + PubsubMessage { + /// The gossipsub message id. Used when propagating blocks after validation. + id: MessageId, + /// The peer from which we received this message, not the peer that published it. + propagation_source: PeerId, + /// The peer who published and signed this message. + source: PeerId, + /// The topic that this message was sent on. + topic: TopicHash, + /// The message itself. + message: PubsubMessage, + }, + /// Inform the network to send a Status to this peer. + StatusPeer(PeerId), +} + +/// Internal type to pass messages from sub-behaviours to the poll of the global behaviour to be +/// specified as an NBAction. +enum InternalBehaviourMessage { + /// Dial a Peer. + DialPeer(PeerId), + /// The socket has been updated. + SocketUpdated(Multiaddr), +} + +/// Builds the network behaviour that manages the core protocols of eth2. +/// This core behaviour is managed by `Behaviour` which adds peer management to all core +/// behaviours. +#[derive(NetworkBehaviour)] +#[behaviour( + out_event = "BehaviourEvent", + poll_method = "poll", + event_process = true +)] +pub struct Behaviour { + /* Sub-Behaviours */ + /// The routing pub-sub mechanism for eth2. + gossipsub: Gossipsub, + /// The Eth2 RPC specified in the wire-0 protocol. + eth2_rpc: RPC>, + /// Discv5 Discovery protocol. + discovery: Discovery, + /// Keep regular connection to peers and disconnect if absent. + // NOTE: The id protocol is used for initial interop. This will be removed by mainnet. + /// Provides IP addresses and peer information. + identify: Identify, + /// The peer manager that keeps track of peer's reputation and status. + peer_manager: PeerManager, + + /* Auxiliary Fields */ + /// The output events generated by this behaviour to be consumed in the swarm poll. + #[behaviour(ignore)] + events: VecDeque>, + /// Internal behaviour events, the NBAction type is composed of sub-behaviours, so we use a + /// custom type here to avoid having to specify the concrete type. + #[behaviour(ignore)] + internal_events: VecDeque, + /// A collections of variables accessible outside the network service. + #[behaviour(ignore)] + network_globals: Arc, + /// The waker for the current task. This is used to wake the task when events are added to the + /// queue. + #[behaviour(ignore)] + waker: Option, + /// The interval for updating gossipsub scores + #[behaviour(ignore)] + update_gossipsub_scores: tokio::time::Interval, + #[behaviour(ignore)] + gossip_cache: GossipCache, +} + +/// Implements the combined behaviour for the libp2p service. +impl Behaviour { + pub async fn new( + local_key: &Keypair, + ctx: ServiceContext<'_>, + network_globals: Arc, + ) -> error::Result { + let mut config = ctx.config.clone(); + + // Set up the Identify Behaviour + let identify_config = if config.private { + IdentifyConfig::new( + "".into(), + local_key.public(), // Still send legitimate public key + ) + .with_cache_size(0) + } else { + IdentifyConfig::new("eth2/1.0.0".into(), local_key.public()) + .with_agent_version(zgs_version::version_with_platform()) + .with_cache_size(0) + }; + + // Build and start the discovery sub-behaviour + let mut discovery = Discovery::new(local_key, &config, network_globals.clone()).await?; + // start searching for peers + discovery.discover_peers(FIND_NODE_QUERY_CLOSEST_PEERS); + + let filter = MaxCountSubscriptionFilter { + filter: AllowAllSubscriptionFilter {}, + max_subscribed_topics: 200, + max_subscriptions_per_request: 150, // 148 in theory = (64 attestation + 4 sync committee + 6 core topics) * 2 + }; + + config.gs_config = gossipsub_config(config.network_load); + + // If metrics are enabled for gossipsub build the configuration + let snappy_transform = SnappyTransform::new(config.gs_config.max_transmit_size()); + let mut gossipsub = Gossipsub::new_with_subscription_filter_and_transform( + MessageAuthenticity::Signed(local_key.clone()), + config.gs_config.clone(), + None, // gossipsub_metrics + filter, + snappy_transform, + ) + .map_err(|e| format!("Could not construct gossipsub: {:?}", e))?; + + // Construct a set of gossipsub peer scoring parameters + // We don't know the number of active validators and the current slot yet + let thresholds = lighthouse_gossip_thresholds(); + + // let score_settings = PeerScoreSettings::new(ctx.chain_spec); + + // // Prepare scoring parameters + // let params = score_settings.get_peer_score_params( + // &thresholds, + // )?; + + // trace!(behaviour_log, "Using peer score params"; "params" => ?params); + + let params = libp2p::gossipsub::PeerScoreParams::default(); + + // Set up a scoring update interval + let update_gossipsub_scores = tokio::time::interval(params.decay_interval); + + gossipsub + .with_peer_score(params, thresholds) + .expect("Valid score params and thresholds"); + + let peer_manager_cfg = PeerManagerCfg { + discovery_enabled: !config.disable_discovery, + metrics_enabled: config.metrics_enabled, + target_peer_count: config.target_peers, + ..Default::default() + }; + + let slot_duration = std::time::Duration::from_secs(12); + // let slot_duration = std::time::Duration::from_secs(ctx.chain_spec.seconds_per_slot); + + let gossip_cache = GossipCache::builder() + .example_timeout(slot_duration) // TODO + .build(); + + Ok(Behaviour { + // Sub-behaviours + gossipsub, + eth2_rpc: RPC::new(), + discovery, + identify: Identify::new(identify_config), + // Auxiliary fields + peer_manager: PeerManager::new(peer_manager_cfg, network_globals.clone()).await?, + events: VecDeque::new(), + internal_events: VecDeque::new(), + network_globals, + waker: None, + gossip_cache, + update_gossipsub_scores, + }) + } + + /* Public Accessible Functions to interact with the behaviour */ + + /// Get a mutable reference to the underlying discovery sub-behaviour. + pub fn discovery_mut(&mut self) -> &mut Discovery { + &mut self.discovery + } + + /// Get a mutable reference to the peer manager. + pub fn peer_manager_mut(&mut self) -> &mut PeerManager { + &mut self.peer_manager + } + + /// Returns the local ENR of the node. + pub fn local_enr(&self) -> Enr { + self.network_globals.local_enr() + } + + /// Obtain a reference to the gossipsub protocol. + pub fn gs(&self) -> &Gossipsub { + &self.gossipsub + } + + /* Pubsub behaviour functions */ + + /// Subscribes to a gossipsub topic kind, letting the network service determine the + /// encoding and fork version. + pub fn subscribe_kind(&mut self, kind: GossipKind) -> bool { + let gossip_topic = GossipTopic::new(kind, GossipEncoding::default()); + + self.subscribe(gossip_topic) + } + + /// Unsubscribes from a gossipsub topic kind, letting the network service determine the + /// encoding and fork version. + pub fn unsubscribe_kind(&mut self, kind: GossipKind) -> bool { + let gossip_topic = GossipTopic::new(kind, GossipEncoding::default()); + self.unsubscribe(gossip_topic) + } + + /// Subscribes to a gossipsub topic. + /// + /// Returns `true` if the subscription was successful and `false` otherwise. + pub fn subscribe(&mut self, topic: GossipTopic) -> bool { + // update the network globals + self.network_globals + .gossipsub_subscriptions + .write() + .insert(topic.clone()); + + let topic: Topic = topic.into(); + + match self.gossipsub.subscribe(&topic) { + Err(e) => { + warn!(%topic, error = ?e, "Failed to subscribe to topic"); + false + } + Ok(_) => { + debug!(%topic, "Subscribed to topic"); + true + } + } + } + + /// Unsubscribe from a gossipsub topic. + pub fn unsubscribe(&mut self, topic: GossipTopic) -> bool { + // update the network globals + self.network_globals + .gossipsub_subscriptions + .write() + .remove(&topic); + + // unsubscribe from the topic + let libp2p_topic: Topic = topic.clone().into(); + + match self.gossipsub.unsubscribe(&libp2p_topic) { + Err(_) => { + warn!(topic = %libp2p_topic, "Failed to unsubscribe from topic"); + false + } + Ok(v) => { + // Inform the network + debug!(%topic, "Unsubscribed to topic"); + v + } + } + } + + /// Publishes a list of messages on the pubsub (gossipsub) behaviour, choosing the encoding. + pub fn publish(&mut self, messages: Vec) { + for message in messages { + for topic in message.topics(GossipEncoding::default()) { + let message_data = message.encode(GossipEncoding::default()); + if let Err(e) = self + .gossipsub + .publish(topic.clone().into(), message_data.clone()) + { + warn!(error = ?e, "Could not publish message"); + + // add to metrics + if let Some(v) = metrics::get_int_gauge( + &metrics::FAILED_PUBLISHES_PER_MAIN_TOPIC, + &[&format!("{:?}", topic.kind())], + ) { + v.inc() + }; + + if let PublishError::InsufficientPeers = e { + self.gossip_cache.insert(topic, message_data); + } + } + } + } + } + + /// Informs the gossipsub about the result of a message validation. + /// If the message is valid it will get propagated by gossipsub. + pub fn report_message_validation_result( + &mut self, + propagation_source: &PeerId, + message_id: MessageId, + validation_result: MessageAcceptance, + ) { + if let Some(result) = match validation_result { + MessageAcceptance::Accept => None, + MessageAcceptance::Ignore => Some("ignore"), + MessageAcceptance::Reject => Some("reject"), + } { + if let Some(client) = self + .network_globals + .peers + .read() + .peer_info(propagation_source) + .map(|info| info.client().kind.as_ref()) + { + metrics::inc_counter_vec( + &metrics::GOSSIP_UNACCEPTED_MESSAGES_PER_CLIENT, + &[client, result], + ) + } + } + + if let Err(e) = self.gossipsub.report_message_validation_result( + &message_id, + propagation_source, + validation_result, + ) { + warn!( + %message_id, + peer_id = %propagation_source, + error = ?e, + "Failed to report message validation", + ); + } + } + + /* Eth2 RPC behaviour functions */ + + /// Send a request to a peer over RPC. + pub fn send_request(&mut self, peer_id: PeerId, request_id: AppReqId, request: Request) { + self.eth2_rpc + .send_request(peer_id, RequestId::Application(request_id), request.into()) + } + + /// Send a successful response to a peer over RPC. + pub fn send_successful_response( + &mut self, + peer_id: PeerId, + id: PeerRequestId, + response: Response, + ) { + self.eth2_rpc.send_response(peer_id, id, response.into()) + } + + /// Inform the peer that their request produced an error. + pub fn send_error_reponse( + &mut self, + peer_id: PeerId, + id: PeerRequestId, + error: RPCResponseErrorCode, + reason: String, + ) { + self.eth2_rpc + .send_response(peer_id, id, RPCCodedResponse::Error(error, reason.into())) + } + + /* Peer management functions */ + + /// Disconnects from a peer providing a reason. + /// + /// This will send a goodbye, disconnect and then ban the peer. + /// This is fatal for a peer, and should be used in unrecoverable circumstances. + pub fn goodbye_peer(&mut self, peer_id: &PeerId, reason: GoodbyeReason, source: ReportSource) { + self.peer_manager.goodbye_peer(peer_id, reason, source); + } + + /// Returns an iterator over all enr entries in the DHT. + pub fn enr_entries(&mut self) -> Vec { + self.discovery.table_entries_enr() + } + + /// Add an ENR to the routing table of the discovery mechanism. + pub fn add_enr(&mut self, enr: Enr) { + self.discovery.add_enr(enr); + } + + /* Private internal functions */ + + /// Sends a Ping request to the peer. + fn ping(&mut self, peer_id: PeerId) { + let ping = crate::rpc::Ping { data: 1 }; + trace!(%peer_id, "Sending Ping"); + let id = RequestId::Behaviour; + self.eth2_rpc + .send_request(peer_id, id, OutboundRequest::Ping(ping)); + } + + /// Sends a Pong response to the peer. + fn pong(&mut self, id: PeerRequestId, peer_id: PeerId) { + let ping = crate::rpc::Ping { data: 1 }; + trace!(request_id = ?id.1, %peer_id, "Sending Pong"); + let event = RPCCodedResponse::Success(RPCResponse::Pong(ping)); + self.eth2_rpc.send_response(peer_id, id, event); + } + + /// Returns a reference to the peer manager to allow the swarm to notify the manager of peer + /// status + pub fn peer_manager(&mut self) -> &mut PeerManager { + &mut self.peer_manager + } + + // RPC Propagation methods + /// Queues the response to be sent upwards as long at it was requested outside the Behaviour. + fn propagate_response(&mut self, id: RequestId, peer_id: PeerId, response: Response) { + match id { + RequestId::Application(id) => self.add_event(BehaviourEvent::ResponseReceived { + peer_id, + id, + response, + }), + RequestId::Behaviour => {} + } + } + + /// Convenience function to propagate a request. + fn propagate_request(&mut self, id: PeerRequestId, peer_id: PeerId, request: Request) { + // Increment metrics + match &request { + Request::Status(_) => { + metrics::inc_counter_vec(&metrics::TOTAL_RPC_REQUESTS, &["status"]) + } + Request::DataByHash { .. } => { + metrics::inc_counter_vec(&metrics::TOTAL_RPC_REQUESTS, &["data_by_hash"]) + } + Request::GetChunks { .. } => { + metrics::inc_counter_vec(&metrics::TOTAL_RPC_REQUESTS, &["get_chunks"]) + } + } + self.add_event(BehaviourEvent::RequestReceived { + peer_id, + id, + request, + }); + } + + /// Adds an event to the queue waking the current task to process it. + fn add_event(&mut self, event: BehaviourEvent) { + self.events.push_back(event); + if let Some(waker) = &self.waker { + waker.wake_by_ref(); + } + } +} + +/* Behaviour Event Process Implementations + * + * These implementations dictate how to process each event that is emitted from each + * sub-behaviour. + */ + +// Gossipsub +impl NetworkBehaviourEventProcess for Behaviour +where + AppReqId: ReqId, +{ + fn inject_event(&mut self, event: GossipsubEvent) { + match event { + GossipsubEvent::Message { + propagation_source, + message_id: id, + message: gs_msg, + } => { + // Note: We are keeping track here of the peer that sent us the message, not the + // peer that originally published the message. + match PubsubMessage::decode(&gs_msg.topic, &gs_msg.data) { + Err(e) => { + debug!(topic = ?gs_msg.topic, error = ?e, "Could not decode gossipsub message"); + //reject the message + if let Err(e) = self.gossipsub.report_message_validation_result( + &id, + &propagation_source, + MessageAcceptance::Reject, + ) { + warn!(message_id = %id, peer_id = %propagation_source, error = ?e, "Failed to report message validation"); + } + } + Ok(msg) => { + // Notify the network + self.add_event(BehaviourEvent::PubsubMessage { + id, + propagation_source, + source: gs_msg.source.expect("message is signed"), // TODO: is this guaranteed? + topic: gs_msg.topic, + message: msg, + }); + } + } + } + GossipsubEvent::Subscribed { peer_id: _, topic } => { + if let Ok(topic) = GossipTopic::decode(topic.as_str()) { + // if let Some(subnet_id) = topic.subnet_id() { + // self.network_globals + // .peers + // .write() + // .add_subscription(&peer_id, subnet_id); + // } + // Try to send the cached messages for this topic + if let Some(msgs) = self.gossip_cache.retrieve(&topic) { + for data in msgs { + let topic_str: &str = topic.kind().as_ref(); + match self.gossipsub.publish(topic.clone().into(), data) { + Ok(_) => { + warn!(topic = topic_str, "Gossip message published on retry"); + if let Some(v) = metrics::get_int_counter( + &metrics::GOSSIP_LATE_PUBLISH_PER_TOPIC_KIND, + &[topic_str], + ) { + v.inc() + }; + } + Err(e) => { + warn!(topic = topic_str, error = %e, "Gossip message publish failed on retry"); + if let Some(v) = metrics::get_int_counter( + &metrics::GOSSIP_FAILED_LATE_PUBLISH_PER_TOPIC_KIND, + &[topic_str], + ) { + v.inc() + }; + } + } + } + } + } + } + GossipsubEvent::Unsubscribed { + peer_id: _, + topic: _, + } => { + // if let Some(subnet_id) = subnet_from_topic_hash(&topic) { + // self.network_globals + // .peers + // .write() + // .remove_subscription(&peer_id, &subnet_id); + // } + } + GossipsubEvent::GossipsubNotSupported { peer_id } => { + debug!(%peer_id, "Peer does not support gossipsub"); + self.peer_manager.report_peer( + &peer_id, + PeerAction::LowToleranceError, + ReportSource::Gossipsub, + Some(GoodbyeReason::Unknown), + "does_not_support_gossipsub", + ); + } + } + } +} + +// RPC +impl NetworkBehaviourEventProcess>> for Behaviour +where + AppReqId: ReqId, +{ + fn inject_event(&mut self, event: RPCMessage>) { + let peer_id = event.peer_id; + + if !self.peer_manager.is_connected(&peer_id) { + debug!( + peer = %peer_id, + "Ignoring rpc message of disconnecting peer" + ); + return; + } + + let handler_id = event.conn_id; + // The METADATA and PING RPC responses are handled within the behaviour and not propagated + match event.event { + Err(handler_err) => { + match handler_err { + HandlerErr::Inbound { + id: _, + proto, + error, + } => { + if matches!(error, RPCError::HandlerRejected) { + // this peer's request got canceled + } + // Inform the peer manager of the error. + // An inbound error here means we sent an error to the peer, or the stream + // timed out. + self.peer_manager.handle_rpc_error( + &peer_id, + proto, + &error, + ConnectionDirection::Incoming, + ); + } + HandlerErr::Outbound { id, proto, error } => { + // Inform the peer manager that a request we sent to the peer failed + self.peer_manager.handle_rpc_error( + &peer_id, + proto, + &error, + ConnectionDirection::Outgoing, + ); + // inform failures of requests comming outside the behaviour + if let RequestId::Application(id) = id { + self.add_event(BehaviourEvent::RPCFailed { peer_id, id }); + } + } + } + } + Ok(RPCReceived::Request(id, request)) => { + let peer_request_id = (handler_id, id); + match request { + /* Behaviour managed protocols: Ping and Metadata */ + InboundRequest::Ping(ping) => { + // inform the peer manager and send the response + self.peer_manager.ping_request(&peer_id, ping.data); + // send a ping response + self.pong(peer_request_id, peer_id); + } + InboundRequest::Goodbye(reason) => { + // queue for disconnection without a goodbye message + debug!( + %peer_id, + %reason, + client = %self.network_globals.client(&peer_id), + "Peer sent Goodbye" + ); + // NOTE: We currently do not inform the application that we are + // disconnecting here. The RPC handler will automatically + // disconnect for us. + // The actual disconnection event will be relayed to the application. + } + /* Protocols propagated to the Network */ + InboundRequest::Status(msg) => { + // inform the peer manager that we have received a status from a peer + self.peer_manager.peer_statusd(&peer_id); + // propagate the STATUS message upwards + self.propagate_request(peer_request_id, peer_id, Request::Status(msg)) + } + InboundRequest::DataByHash(req) => { + self.propagate_request(peer_request_id, peer_id, Request::DataByHash(req)) + } + InboundRequest::GetChunks(req) => { + self.propagate_request(peer_request_id, peer_id, Request::GetChunks(req)) + } + } + } + Ok(RPCReceived::Response(id, resp)) => { + match resp { + /* Behaviour managed protocols */ + RPCResponse::Pong(ping) => self.peer_manager.pong_response(&peer_id, ping.data), + /* Network propagated protocols */ + RPCResponse::Status(msg) => { + // inform the peer manager that we have received a status from a peer + self.peer_manager.peer_statusd(&peer_id); + // propagate the STATUS message upwards + self.propagate_response(id, peer_id, Response::Status(msg)); + } + RPCResponse::DataByHash(resp) => { + self.propagate_response(id, peer_id, Response::DataByHash(Some(resp))) + } + RPCResponse::Chunks(resp) => { + self.propagate_response(id, peer_id, Response::Chunks(resp)) + } + } + } + Ok(RPCReceived::EndOfStream(id, termination)) => { + let response = match termination { + ResponseTermination::DataByHash => Response::DataByHash(None), + }; + self.propagate_response(id, peer_id, response); + } + } + } +} + +// Discovery +impl NetworkBehaviourEventProcess for Behaviour +where + AppReqId: ReqId, +{ + fn inject_event(&mut self, event: DiscoveryEvent) { + match event { + DiscoveryEvent::SocketUpdated(socket_addr) => { + // A new UDP socket has been detected. + // Build a multiaddr to report to libp2p + let mut multiaddr = Multiaddr::from(socket_addr.ip()); + // NOTE: This doesn't actually track the external TCP port. More sophisticated NAT handling + // should handle this. + multiaddr.push(MProtocol::Tcp(self.network_globals.listen_port_tcp())); + self.internal_events + .push_back(InternalBehaviourMessage::SocketUpdated(multiaddr)); + } + DiscoveryEvent::QueryResult(results) => { + let to_dial_peers = self.peer_manager.peers_discovered(results); + for peer_id in to_dial_peers { + debug!(%peer_id, "Dialing discovered peer"); + // For any dial event, inform the peer manager + let enr = self.discovery_mut().enr_of_peer(&peer_id); + self.peer_manager.inject_dialing(&peer_id, enr); + self.internal_events + .push_back(InternalBehaviourMessage::DialPeer(peer_id)); + } + } + } + } +} + +// Identify +impl NetworkBehaviourEventProcess for Behaviour +where + AppReqId: ReqId, +{ + fn inject_event(&mut self, event: IdentifyEvent) { + match event { + IdentifyEvent::Received { peer_id, mut info } => { + if info.listen_addrs.len() > MAX_IDENTIFY_ADDRESSES { + debug!("More than 10 addresses have been identified, truncating"); + info.listen_addrs.truncate(MAX_IDENTIFY_ADDRESSES); + } + // send peer info to the peer manager. + self.peer_manager.identify(&peer_id, &info); + } + IdentifyEvent::Sent { .. } => {} + IdentifyEvent::Error { .. } => {} + IdentifyEvent::Pushed { .. } => {} + } + } +} + +type BehaviourHandler = as NetworkBehaviour>::ConnectionHandler; + +impl Behaviour +where + AppReqId: ReqId, +{ + /// Consumes the events list and drives the Lighthouse global NetworkBehaviour. + fn poll( + &mut self, + cx: &mut Context, + _: &mut impl PollParameters, + ) -> Poll, BehaviourHandler>> { + if let Some(waker) = &self.waker { + if waker.will_wake(cx.waker()) { + self.waker = Some(cx.waker().clone()); + } + } else { + self.waker = Some(cx.waker().clone()); + } + + // Handle internal events first + if let Some(event) = self.internal_events.pop_front() { + match event { + InternalBehaviourMessage::DialPeer(peer_id) => { + // Submit the event + let handler = self.new_handler(); + return Poll::Ready(NBAction::Dial { + opts: DialOpts::peer_id(peer_id) + .condition(PeerCondition::Disconnected) + .build(), + handler, + }); + } + InternalBehaviourMessage::SocketUpdated(address) => { + return Poll::Ready(NBAction::ReportObservedAddr { + address, + score: AddressScore::Finite(1), + }); + } + } + } + + if let Some(event) = self.events.pop_front() { + return Poll::Ready(NBAction::GenerateEvent(event)); + } + + // perform gossipsub score updates when necessary + while self.update_gossipsub_scores.poll_tick(cx).is_ready() { + self.peer_manager.update_gossipsub_scores(&self.gossipsub); + } + + // poll the gossipsub cache to clear expired messages + while let Poll::Ready(Some(result)) = self.gossip_cache.poll_next_unpin(cx) { + match result { + Err(e) => warn!(error = ?e, "Gossip cache error"), + Ok(expired_topic) => { + if let Some(v) = metrics::get_int_counter( + &metrics::GOSSIP_EXPIRED_LATE_PUBLISH_PER_TOPIC_KIND, + &[expired_topic.kind().as_ref()], + ) { + v.inc() + }; + } + } + } + + Poll::Pending + } +} + +impl NetworkBehaviourEventProcess for Behaviour { + fn inject_event(&mut self, event: PeerManagerEvent) { + match event { + PeerManagerEvent::PeerConnectedIncoming(peer_id) => { + self.add_event(BehaviourEvent::PeerConnectedIncoming(peer_id)); + } + PeerManagerEvent::PeerConnectedOutgoing(peer_id) => { + self.add_event(BehaviourEvent::PeerConnectedOutgoing(peer_id)); + } + PeerManagerEvent::PeerDisconnected(peer_id) => { + self.add_event(BehaviourEvent::PeerDisconnected(peer_id)); + } + PeerManagerEvent::Banned(peer_id, associated_ips) => { + self.discovery.ban_peer(&peer_id, associated_ips); + self.add_event(BehaviourEvent::PeerBanned(peer_id)); + } + PeerManagerEvent::UnBanned(peer_id, associated_ips) => { + self.discovery.unban_peer(&peer_id, associated_ips); + self.add_event(BehaviourEvent::PeerUnbanned(peer_id)); + } + PeerManagerEvent::Status(peer_id) => { + // it's time to status. We don't keep a beacon chain reference here, so we inform + // the network to send a status to this peer + self.add_event(BehaviourEvent::StatusPeer(peer_id)); + } + PeerManagerEvent::DiscoverPeers(peers_to_find) => { + // Peer manager has requested a discovery query for more peers. + self.discovery.discover_peers(peers_to_find); + } + PeerManagerEvent::Ping(peer_id) => { + // send a ping request to this peer + self.ping(peer_id); + } + PeerManagerEvent::DisconnectPeer(peer_id, reason) => { + debug!(%peer_id, %reason, "Peer Manager disconnecting peer"); + // send one goodbye + self.eth2_rpc + .shutdown(peer_id, RequestId::Behaviour, reason); + } + } + } +} + +/* Public API types */ + +/// The type of RPC requests the Behaviour informs it has received and allows for sending. +/// +// NOTE: This is an application-level wrapper over the lower network level requests that can be +// sent. The main difference is the absence of the Ping, Metadata and Goodbye protocols, which don't +// leave the Behaviour. For all protocols managed by RPC see `RPCRequest`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Request { + /// A Status message. + Status(StatusMessage), + /// A data by hash request. + DataByHash(DataByHashRequest), + /// A GetChunks request. + GetChunks(GetChunksRequest), +} + +impl std::convert::From for OutboundRequest { + fn from(req: Request) -> OutboundRequest { + match req { + Request::Status(s) => OutboundRequest::Status(s), + Request::DataByHash(r) => OutboundRequest::DataByHash(r), + Request::GetChunks(r) => OutboundRequest::GetChunks(r), + } + } +} + +/// The type of RPC responses the Behaviour informs it has received, and allows for sending. +/// +// NOTE: This is an application-level wrapper over the lower network level responses that can be +// sent. The main difference is the absense of Pong and Metadata, which don't leave the +// Behaviour. For all protocol reponses managed by RPC see `RPCResponse` and +// `RPCCodedResponse`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Response { + /// A Status message. + Status(StatusMessage), + /// A response to a get DATA_BY_HASH request. A None response signals the end of the batch. + DataByHash(Option>), + /// A response to a GET_CHUNKS request. + Chunks(ChunkArrayWithProof), +} + +impl std::convert::From for RPCCodedResponse { + fn from(resp: Response) -> RPCCodedResponse { + match resp { + Response::Status(s) => RPCCodedResponse::Success(RPCResponse::Status(s)), + Response::DataByHash(r) => match r { + Some(b) => RPCCodedResponse::Success(RPCResponse::DataByHash(b)), + None => RPCCodedResponse::StreamTermination(ResponseTermination::DataByHash), + }, + Response::Chunks(c) => RPCCodedResponse::Success(RPCResponse::Chunks(c)), + } + } +} diff --git a/node/network/src/config.rs b/node/network/src/config.rs new file mode 100644 index 0000000..e93ac53 --- /dev/null +++ b/node/network/src/config.rs @@ -0,0 +1,351 @@ +use crate::types::GossipKind; +use crate::{Enr, PeerIdSerialized}; +use directory::{ + DEFAULT_BEACON_NODE_DIR, DEFAULT_HARDCODED_NETWORK, DEFAULT_NETWORK_DIR, DEFAULT_ROOT_DIR, +}; +use discv5::{Discv5Config, Discv5ConfigBuilder}; +use libp2p::gossipsub::{ + FastMessageId, GossipsubConfig, GossipsubConfigBuilder, GossipsubMessage, MessageId, + RawGossipsubMessage, ValidationMode, +}; +use libp2p::Multiaddr; +use serde_derive::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::path::PathBuf; +use std::time::Duration; + +/// The maximum transmit size of gossip messages in bytes post-merge. +const GOSSIP_MAX_SIZE_POST_MERGE: usize = 10 * 1_048_576; // 10M + +/// The cache time is set to accommodate the circulation time of an attestation. +/// +/// The p2p spec declares that we accept attestations within the following range: +/// +/// ```ignore +/// ATTESTATION_PROPAGATION_SLOT_RANGE = 32 +/// attestation.data.slot + ATTESTATION_PROPAGATION_SLOT_RANGE >= current_slot >= attestation.data.slot +/// ``` +/// +/// Therefore, we must accept attestations across a span of 33 slots (where each slot is 12 +/// seconds). We add an additional second to account for the 500ms gossip clock disparity, and +/// another 500ms for "fudge factor". +pub const DUPLICATE_CACHE_TIME: Duration = Duration::from_secs(33 * 12 + 1); + +// We treat uncompressed messages as invalid and never use the INVALID_SNAPPY_DOMAIN as in the +// specification. We leave it here for posterity. +// const MESSAGE_DOMAIN_INVALID_SNAPPY: [u8; 4] = [0, 0, 0, 0]; +const MESSAGE_DOMAIN_VALID_SNAPPY: [u8; 4] = [1, 0, 0, 0]; + +/// The maximum size of gossip messages. +pub fn gossip_max_size() -> usize { + GOSSIP_MAX_SIZE_POST_MERGE +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(default)] +/// Network configuration for lighthouse. +pub struct Config { + /// Data directory where node's keyfile is stored + pub network_dir: PathBuf, + + /// IP address to listen on. + pub listen_address: std::net::IpAddr, + + /// The TCP port that libp2p listens on. + pub libp2p_port: u16, + + /// UDP port that discovery listens on. + pub discovery_port: u16, + + /// The address to broadcast to peers about which address we are listening on. None indicates + /// that no discovery address has been set in the CLI args. + pub enr_address: Option, + + /// The udp port to broadcast to peers in order to reach back for discovery. + pub enr_udp_port: Option, + + /// The tcp port to broadcast to peers in order to reach back for libp2p services. + pub enr_tcp_port: Option, + + /// Target number of connected peers. + pub target_peers: usize, + + /// Gossipsub configuration parameters. + #[serde(skip)] + pub gs_config: GossipsubConfig, + + /// Discv5 configuration parameters. + #[serde(skip)] + pub discv5_config: Discv5Config, + + /// List of nodes to initially connect to. + pub boot_nodes_enr: Vec, + + /// List of nodes to initially connect to, on Multiaddr format. + pub boot_nodes_multiaddr: Vec, + + /// List of libp2p nodes to initially connect to. + pub libp2p_nodes: Vec, + + /// List of trusted libp2p nodes which are not scored. + pub trusted_peers: Vec, + + /// Client version + pub client_version: String, + + /// Disables the discovery protocol from starting. + pub disable_discovery: bool, + + /// Attempt to construct external port mappings with UPnP. + pub upnp_enabled: bool, + + /// Subscribe to all subnets for the duration of the runtime. + pub subscribe_all_subnets: bool, + + /// Import/aggregate all attestations recieved on subscribed subnets for the duration of the + /// runtime. + pub import_all_attestations: bool, + + /// A setting specifying a range of values that tune the network parameters of lighthouse. The + /// lower the value the less bandwidth used, but the slower messages will be received. + pub network_load: u8, + + /// Indicates if the user has set the network to be in private mode. Currently this + /// prevents sending client identifying information over identify. + pub private: bool, + + /// Shutdown beacon node after sync is completed. + pub shutdown_after_sync: bool, + + /// List of extra topics to initially subscribe to as strings. + pub topics: Vec, + + /// Whether metrics are enabled. + pub metrics_enabled: bool, +} + +impl Default for Config { + /// Generate a default network configuration. + fn default() -> Self { + // WARNING: this directory default should be always overwritten with parameters + // from cli for specific networks. + let network_dir = dirs::home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join(DEFAULT_ROOT_DIR) + .join(DEFAULT_HARDCODED_NETWORK) + .join(DEFAULT_BEACON_NODE_DIR) + .join(DEFAULT_NETWORK_DIR); + + // Note: Using the default config here. Use `gossipsub_config` function for getting + // Lighthouse specific configuration for gossipsub. + let gs_config = GossipsubConfigBuilder::default() + .build() + .expect("valid gossipsub configuration"); + + // Discv5 Unsolicited Packet Rate Limiter + let filter_rate_limiter = Some( + discv5::RateLimiterBuilder::new() + .total_n_every(10, Duration::from_secs(1)) // Allow bursts, average 10 per second + .ip_n_every(9, Duration::from_secs(1)) // Allow bursts, average 9 per second + .node_n_every(8, Duration::from_secs(1)) // Allow bursts, average 8 per second + .build() + .expect("The total rate limit has been specified"), + ); + + // discv5 configuration + let discv5_config = Discv5ConfigBuilder::new() + .enable_packet_filter() + .session_cache_capacity(5000) + .request_timeout(Duration::from_secs(1)) + .query_peer_timeout(Duration::from_secs(2)) + .query_timeout(Duration::from_secs(30)) + .request_retries(1) + .enr_peer_update_min(10) + .query_parallelism(5) + .disable_report_discovered_peers() + .ip_limit() // limits /24 IP's in buckets. + .incoming_bucket_limit(8) // half the bucket size + .filter_rate_limiter(filter_rate_limiter) + .filter_max_bans_per_ip(Some(5)) + .filter_max_nodes_per_ip(Some(10)) + .table_filter(|enr| enr.ip().map_or(false, |ip| is_global(&ip))) // Filter non-global IPs + .ban_duration(Some(Duration::from_secs(3600))) + .ping_interval(Duration::from_secs(300)) + .build(); + + // NOTE: Some of these get overridden by the corresponding CLI default values. + Config { + network_dir, + listen_address: "0.0.0.0".parse().expect("valid ip address"), + libp2p_port: 9000, + discovery_port: 9000, + enr_address: None, + enr_udp_port: None, + enr_tcp_port: None, + target_peers: 50, + gs_config, + discv5_config, + boot_nodes_enr: vec![], + boot_nodes_multiaddr: vec![], + libp2p_nodes: vec![], + trusted_peers: vec![], + client_version: zgs_version::version_with_platform(), + disable_discovery: false, + upnp_enabled: true, + network_load: 3, + private: false, + subscribe_all_subnets: false, + import_all_attestations: false, + shutdown_after_sync: false, + topics: Vec::new(), + metrics_enabled: false, + } + } +} + +/// Controls sizes of gossipsub meshes to tune a Lighthouse node's bandwidth/performance. +pub struct NetworkLoad { + pub name: &'static str, + pub mesh_n_low: usize, + pub outbound_min: usize, + pub mesh_n: usize, + pub mesh_n_high: usize, + pub gossip_lazy: usize, + pub history_gossip: usize, + pub heartbeat_interval: Duration, +} + +impl From for NetworkLoad { + fn from(load: u8) -> NetworkLoad { + match load { + 1 => NetworkLoad { + name: "Low", + mesh_n_low: 1, + outbound_min: 1, + mesh_n: 3, + mesh_n_high: 4, + gossip_lazy: 3, + history_gossip: 3, + heartbeat_interval: Duration::from_millis(1200), + }, + 2 => NetworkLoad { + name: "Low", + mesh_n_low: 2, + outbound_min: 2, + mesh_n: 4, + mesh_n_high: 8, + gossip_lazy: 3, + history_gossip: 3, + heartbeat_interval: Duration::from_millis(1000), + }, + 3 => NetworkLoad { + name: "Average", + mesh_n_low: 3, + outbound_min: 2, + mesh_n: 5, + mesh_n_high: 10, + gossip_lazy: 3, + history_gossip: 3, + heartbeat_interval: Duration::from_millis(700), + }, + 4 => NetworkLoad { + name: "Average", + mesh_n_low: 4, + outbound_min: 3, + mesh_n: 8, + mesh_n_high: 12, + gossip_lazy: 3, + history_gossip: 3, + heartbeat_interval: Duration::from_millis(700), + }, + // 5 and above + _ => NetworkLoad { + name: "High", + mesh_n_low: 5, + outbound_min: 3, + mesh_n: 10, + mesh_n_high: 15, + gossip_lazy: 5, + history_gossip: 6, + heartbeat_interval: Duration::from_millis(500), + }, + } + } +} + +/// Return a Lighthouse specific `GossipsubConfig` where the `message_id_fn` depends on the current fork. +pub fn gossipsub_config(network_load: u8) -> GossipsubConfig { + // The function used to generate a gossipsub message id + // We use the first 8 bytes of SHA256(data) for content addressing + let fast_gossip_message_id = + |message: &RawGossipsubMessage| FastMessageId::from(&Sha256::digest(&message.data)[..8]); + fn prefix(prefix: [u8; 4], message: &GossipsubMessage) -> Vec { + let topic_bytes = message.topic.as_str().as_bytes(); + + // according to: https://github.com/ethereum/consensus-specs/blob/dev/specs/merge/p2p-interface.md#the-gossip-domain-gossipsub + // the derivation of the message-id remains the same in the merge + let topic_len_bytes = topic_bytes.len().to_le_bytes(); + let mut vec = Vec::with_capacity( + prefix.len() + topic_len_bytes.len() + topic_bytes.len() + message.data.len(), + ); + vec.extend_from_slice(&prefix); + vec.extend_from_slice(&topic_len_bytes); + vec.extend_from_slice(topic_bytes); + vec.extend_from_slice(&message.data); + vec + } + + let gossip_message_id = move |message: &GossipsubMessage| { + MessageId::from( + &Sha256::digest(prefix(MESSAGE_DOMAIN_VALID_SNAPPY, message).as_slice())[..20], + ) + }; + + let load = NetworkLoad::from(network_load); + + GossipsubConfigBuilder::default() + .max_transmit_size(gossip_max_size()) + .heartbeat_interval(load.heartbeat_interval) + .mesh_n(load.mesh_n) + .mesh_n_low(load.mesh_n_low) + .mesh_outbound_min(load.outbound_min) + .mesh_n_high(load.mesh_n_high) + .gossip_lazy(load.gossip_lazy) + .fanout_ttl(Duration::from_secs(60)) + .history_length(12) + .max_messages_per_rpc(Some(500)) // Responses to IWANT can be quite large + .history_gossip(load.history_gossip) + .validate_messages() // require validation before propagation + .validation_mode(ValidationMode::Strict) + .duplicate_cache_time(DUPLICATE_CACHE_TIME) + .message_id_fn(gossip_message_id) + .fast_message_id_fn(fast_gossip_message_id) + .allow_self_origin(true) + .build() + .expect("valid gossipsub configuration") +} + +/// Helper function to determine if the IpAddr is a global address or not. The `is_global()` +/// function is not yet stable on IpAddr. +#[allow(clippy::nonminimal_bool)] +fn is_global(addr: &std::net::Ipv4Addr) -> bool { + // check if this address is 192.0.0.9 or 192.0.0.10. These addresses are the only two + // globally routable addresses in the 192.0.0.0/24 range. + if u32::from_be_bytes(addr.octets()) == 0xc0000009 + || u32::from_be_bytes(addr.octets()) == 0xc000000a + { + return true; + } + !addr.is_private() + && !addr.is_loopback() + && !addr.is_link_local() + && !addr.is_broadcast() + && !addr.is_documentation() + // shared + && !(addr.octets()[0] == 100 && (addr.octets()[1] & 0b1100_0000 == 0b0100_0000)) &&!(addr.octets()[0] & 240 == 240 && !addr.is_broadcast()) + // addresses reserved for future protocols (`192.0.0.0/24`) + // reserved + && !(addr.octets()[0] == 192 && addr.octets()[1] == 0 && addr.octets()[2] == 0) + // Make sure the address is not in 0.0.0.0/8 + && addr.octets()[0] != 0 +} diff --git a/node/network/src/discovery/enr.rs b/node/network/src/discovery/enr.rs new file mode 100644 index 0000000..1e661c3 --- /dev/null +++ b/node/network/src/discovery/enr.rs @@ -0,0 +1,152 @@ +//! Helper functions and an extension trait for Ethereum 2 ENRs. + +pub use discv5::enr::{self, CombinedKey, EnrBuilder}; + +use super::enr_ext::CombinedKeyExt; +use super::ENR_FILENAME; +use crate::types::Enr; +use crate::NetworkConfig; +use discv5::enr::EnrKey; +use libp2p::core::identity::Keypair; +use std::fs::File; +use std::io::prelude::*; +use std::path::Path; +use std::str::FromStr; + +/// Either use the given ENR or load an ENR from file if it exists and matches the current NodeId +/// and sequence number. +/// If an ENR exists, with the same NodeId, this function checks to see if the loaded ENR from +/// disk is suitable to use, otherwise we increment the given ENR's sequence number. +pub fn use_or_load_enr( + enr_key: &CombinedKey, + local_enr: &mut Enr, + config: &NetworkConfig, +) -> Result<(), String> { + let enr_f = config.network_dir.join(ENR_FILENAME); + if let Ok(mut enr_file) = File::open(enr_f.clone()) { + let mut enr_string = String::new(); + match enr_file.read_to_string(&mut enr_string) { + Err(_) => debug!("Could not read ENR from file"), + Ok(_) => { + match Enr::from_str(&enr_string) { + Ok(disk_enr) => { + // if the same node id, then we may need to update our sequence number + if local_enr.node_id() == disk_enr.node_id() { + if compare_enr(local_enr, &disk_enr) { + debug!(file = ?enr_f, "ENR loaded from disk"); + // the stored ENR has the same configuration, use it + *local_enr = disk_enr; + return Ok(()); + } + + // same node id, different configuration - update the sequence number + // Note: local_enr is generated with default(0) attnets value, + // so a non default value in persisted enr will also update sequence number. + let new_seq_no = disk_enr.seq().checked_add(1).ok_or("ENR sequence number on file is too large. Remove it to generate a new NodeId")?; + local_enr.set_seq(new_seq_no, enr_key).map_err(|e| { + format!("Could not update ENR sequence number: {:?}", e) + })?; + debug!(seq = new_seq_no, "ENR sequence number increased"); + } + } + Err(e) => { + warn!(error = ?e, "ENR from file could not be decoded"); + } + } + } + } + } + + save_enr_to_disk(&config.network_dir, local_enr); + + Ok(()) +} + +/// Loads an ENR from file if it exists and matches the current NodeId and sequence number. If none +/// exists, generates a new one. +/// +/// If an ENR exists, with the same NodeId, this function checks to see if the loaded ENR from +/// disk is suitable to use, otherwise we increment our newly generated ENR's sequence number. +pub fn build_or_load_enr(local_key: Keypair, config: &NetworkConfig) -> Result { + // Build the local ENR. + // Note: Discovery should update the ENR record's IP to the external IP as seen by the + // majority of our peers, if the CLI doesn't expressly forbid it. + let enr_key = CombinedKey::from_libp2p(&local_key)?; + let mut local_enr = build_enr(&enr_key, config)?; + + use_or_load_enr(&enr_key, &mut local_enr, config)?; + Ok(local_enr) +} + +pub fn create_enr_builder_from_config( + config: &NetworkConfig, + enable_tcp: bool, +) -> EnrBuilder { + let mut builder = EnrBuilder::new("v4"); + if let Some(enr_address) = config.enr_address { + builder.ip(enr_address); + } + if let Some(udp_port) = config.enr_udp_port { + builder.udp(udp_port); + } + // we always give it our listening tcp port + if enable_tcp { + let tcp_port = config.enr_tcp_port.unwrap_or(config.libp2p_port); + builder.tcp(tcp_port); + } + builder +} + +/// Builds a lighthouse ENR given a `NetworkConfig`. +pub fn build_enr(enr_key: &CombinedKey, config: &NetworkConfig) -> Result { + let mut builder = create_enr_builder_from_config(config, true); + + builder + .build(enr_key) + .map_err(|e| format!("Could not build Local ENR: {:?}", e)) +} + +/// Defines the conditions under which we use the locally built ENR or the one stored on disk. +/// If this function returns true, we use the `disk_enr`. +fn compare_enr(local_enr: &Enr, disk_enr: &Enr) -> bool { + // take preference over disk_enr address if one is not specified + (local_enr.ip().is_none() || local_enr.ip() == disk_enr.ip()) + // tcp ports must match + && local_enr.tcp() == disk_enr.tcp() + // take preference over disk udp port if one is not specified + && (local_enr.udp().is_none() || local_enr.udp() == disk_enr.udp()) +} + +/// Loads enr from the given directory +pub fn load_enr_from_disk(dir: &Path) -> Result { + let enr_f = dir.join(ENR_FILENAME); + let mut enr_file = + File::open(enr_f).map_err(|e| format!("Failed to open enr file: {:?}", e))?; + let mut enr_string = String::new(); + match enr_file.read_to_string(&mut enr_string) { + Err(_) => Err("Could not read ENR from file".to_string()), + Ok(_) => match Enr::from_str(&enr_string) { + Ok(disk_enr) => Ok(disk_enr), + Err(e) => Err(format!("ENR from file could not be decoded: {:?}", e)), + }, + } +} + +/// Saves an ENR to disk +pub fn save_enr_to_disk(dir: &Path, enr: &Enr) { + let _ = std::fs::create_dir_all(dir); + match File::create(dir.join(Path::new(ENR_FILENAME))) + .and_then(|mut f| f.write_all(enr.to_base64().as_bytes())) + { + Ok(_) => { + debug!("ENR written to disk"); + } + Err(e) => { + warn!( + file = %format!("{:?}{:?}",dir, ENR_FILENAME), + error = %e, + "Could not write ENR to file", + ); + } + } +} diff --git a/node/network/src/discovery/enr_ext.rs b/node/network/src/discovery/enr_ext.rs new file mode 100644 index 0000000..81eaaaf --- /dev/null +++ b/node/network/src/discovery/enr_ext.rs @@ -0,0 +1,314 @@ +//! ENR extension trait to support libp2p integration. +use crate::{Enr, Multiaddr, PeerId}; +use discv5::enr::{CombinedKey, CombinedPublicKey}; +use libp2p::core::{identity::Keypair, identity::PublicKey, multiaddr::Protocol}; +use tiny_keccak::{Hasher, Keccak}; + +/// Extend ENR for libp2p types. +pub trait EnrExt { + /// The libp2p `PeerId` for the record. + fn peer_id(&self) -> PeerId; + + /// Returns a list of multiaddrs if the ENR has an `ip` and either a `tcp` or `udp` key **or** an `ip6` and either a `tcp6` or `udp6`. + /// The vector remains empty if these fields are not defined. + fn multiaddr(&self) -> Vec; + + /// Returns a list of multiaddrs with the `PeerId` prepended. + fn multiaddr_p2p(&self) -> Vec; + + /// Returns any multiaddrs that contain the TCP protocol with the `PeerId` prepended. + fn multiaddr_p2p_tcp(&self) -> Vec; + + /// Returns any multiaddrs that contain the UDP protocol with the `PeerId` prepended. + fn multiaddr_p2p_udp(&self) -> Vec; + + /// Returns any multiaddrs that contain the TCP protocol. + fn multiaddr_tcp(&self) -> Vec; +} + +/// Extend ENR CombinedPublicKey for libp2p types. +pub trait CombinedKeyPublicExt { + /// Converts the publickey into a peer id, without consuming the key. + fn as_peer_id(&self) -> PeerId; +} + +/// Extend ENR CombinedKey for conversion to libp2p keys. +pub trait CombinedKeyExt { + /// Converts a libp2p key into an ENR combined key. + fn from_libp2p(key: &libp2p::core::identity::Keypair) -> Result; +} + +impl EnrExt for Enr { + /// The libp2p `PeerId` for the record. + fn peer_id(&self) -> PeerId { + self.public_key().as_peer_id() + } + + /// Returns a list of multiaddrs if the ENR has an `ip` and either a `tcp` or `udp` key **or** an `ip6` and either a `tcp6` or `udp6`. + /// The vector remains empty if these fields are not defined. + fn multiaddr(&self) -> Vec { + let mut multiaddrs: Vec = Vec::new(); + if let Some(ip) = self.ip() { + if let Some(udp) = self.udp() { + let mut multiaddr: Multiaddr = ip.into(); + multiaddr.push(Protocol::Udp(udp)); + multiaddrs.push(multiaddr); + } + + if let Some(tcp) = self.tcp() { + let mut multiaddr: Multiaddr = ip.into(); + multiaddr.push(Protocol::Tcp(tcp)); + multiaddrs.push(multiaddr); + } + } + if let Some(ip6) = self.ip6() { + if let Some(udp6) = self.udp6() { + let mut multiaddr: Multiaddr = ip6.into(); + multiaddr.push(Protocol::Udp(udp6)); + multiaddrs.push(multiaddr); + } + + if let Some(tcp6) = self.tcp6() { + let mut multiaddr: Multiaddr = ip6.into(); + multiaddr.push(Protocol::Tcp(tcp6)); + multiaddrs.push(multiaddr); + } + } + multiaddrs + } + + /// Returns a list of multiaddrs if the ENR has an `ip` and either a `tcp` or `udp` key **or** an `ip6` and either a `tcp6` or `udp6`. + /// The vector remains empty if these fields are not defined. + /// + /// This also prepends the `PeerId` into each multiaddr with the `P2p` protocol. + fn multiaddr_p2p(&self) -> Vec { + let peer_id = self.peer_id(); + let mut multiaddrs: Vec = Vec::new(); + if let Some(ip) = self.ip() { + if let Some(udp) = self.udp() { + let mut multiaddr: Multiaddr = ip.into(); + multiaddr.push(Protocol::Udp(udp)); + multiaddr.push(Protocol::P2p(peer_id.into())); + multiaddrs.push(multiaddr); + } + + if let Some(tcp) = self.tcp() { + let mut multiaddr: Multiaddr = ip.into(); + multiaddr.push(Protocol::Tcp(tcp)); + multiaddr.push(Protocol::P2p(peer_id.into())); + multiaddrs.push(multiaddr); + } + } + if let Some(ip6) = self.ip6() { + if let Some(udp6) = self.udp6() { + let mut multiaddr: Multiaddr = ip6.into(); + multiaddr.push(Protocol::Udp(udp6)); + multiaddr.push(Protocol::P2p(peer_id.into())); + multiaddrs.push(multiaddr); + } + + if let Some(tcp6) = self.tcp6() { + let mut multiaddr: Multiaddr = ip6.into(); + multiaddr.push(Protocol::Tcp(tcp6)); + multiaddr.push(Protocol::P2p(peer_id.into())); + multiaddrs.push(multiaddr); + } + } + multiaddrs + } + + /// Returns a list of multiaddrs if the ENR has an `ip` and a `tcp` key **or** an `ip6` and a `tcp6`. + /// The vector remains empty if these fields are not defined. + /// + /// This also prepends the `PeerId` into each multiaddr with the `P2p` protocol. + fn multiaddr_p2p_tcp(&self) -> Vec { + let peer_id = self.peer_id(); + let mut multiaddrs: Vec = Vec::new(); + if let Some(ip) = self.ip() { + if let Some(tcp) = self.tcp() { + let mut multiaddr: Multiaddr = ip.into(); + multiaddr.push(Protocol::Tcp(tcp)); + multiaddr.push(Protocol::P2p(peer_id.into())); + multiaddrs.push(multiaddr); + } + } + if let Some(ip6) = self.ip6() { + if let Some(tcp6) = self.tcp6() { + let mut multiaddr: Multiaddr = ip6.into(); + multiaddr.push(Protocol::Tcp(tcp6)); + multiaddr.push(Protocol::P2p(peer_id.into())); + multiaddrs.push(multiaddr); + } + } + multiaddrs + } + + /// Returns a list of multiaddrs if the ENR has an `ip` and a `udp` key **or** an `ip6` and a `udp6`. + /// The vector remains empty if these fields are not defined. + /// + /// This also prepends the `PeerId` into each multiaddr with the `P2p` protocol. + fn multiaddr_p2p_udp(&self) -> Vec { + let peer_id = self.peer_id(); + let mut multiaddrs: Vec = Vec::new(); + if let Some(ip) = self.ip() { + if let Some(udp) = self.udp() { + let mut multiaddr: Multiaddr = ip.into(); + multiaddr.push(Protocol::Udp(udp)); + multiaddr.push(Protocol::P2p(peer_id.into())); + multiaddrs.push(multiaddr); + } + } + if let Some(ip6) = self.ip6() { + if let Some(udp6) = self.udp6() { + let mut multiaddr: Multiaddr = ip6.into(); + multiaddr.push(Protocol::Udp(udp6)); + multiaddr.push(Protocol::P2p(peer_id.into())); + multiaddrs.push(multiaddr); + } + } + multiaddrs + } + + /// Returns a list of multiaddrs if the ENR has an `ip` and either a `tcp` or `udp` key **or** an `ip6` and either a `tcp6` or `udp6`. + /// The vector remains empty if these fields are not defined. + fn multiaddr_tcp(&self) -> Vec { + let mut multiaddrs: Vec = Vec::new(); + if let Some(ip) = self.ip() { + if let Some(tcp) = self.tcp() { + let mut multiaddr: Multiaddr = ip.into(); + multiaddr.push(Protocol::Tcp(tcp)); + multiaddrs.push(multiaddr); + } + } + if let Some(ip6) = self.ip6() { + if let Some(tcp6) = self.tcp6() { + let mut multiaddr: Multiaddr = ip6.into(); + multiaddr.push(Protocol::Tcp(tcp6)); + multiaddrs.push(multiaddr); + } + } + multiaddrs + } +} + +impl CombinedKeyPublicExt for CombinedPublicKey { + /// Converts the publickey into a peer id, without consuming the key. + /// + /// This is only available with the `libp2p` feature flag. + fn as_peer_id(&self) -> PeerId { + match self { + Self::Secp256k1(pk) => { + let pk_bytes = pk.to_bytes(); + let libp2p_pk = libp2p::core::PublicKey::Secp256k1( + libp2p::core::identity::secp256k1::PublicKey::decode(&pk_bytes) + .expect("valid public key"), + ); + PeerId::from_public_key(&libp2p_pk) + } + Self::Ed25519(pk) => { + let pk_bytes = pk.to_bytes(); + let libp2p_pk = libp2p::core::PublicKey::Ed25519( + libp2p::core::identity::ed25519::PublicKey::decode(&pk_bytes) + .expect("valid public key"), + ); + PeerId::from_public_key(&libp2p_pk) + } + } + } +} + +impl CombinedKeyExt for CombinedKey { + fn from_libp2p(key: &libp2p::core::identity::Keypair) -> Result { + match key { + Keypair::Secp256k1(key) => { + let secret = + discv5::enr::k256::ecdsa::SigningKey::from_bytes(&key.secret().to_bytes()) + .expect("libp2p key must be valid"); + Ok(CombinedKey::Secp256k1(secret)) + } + Keypair::Ed25519(key) => { + let ed_keypair = + discv5::enr::ed25519_dalek::SecretKey::from_bytes(&key.encode()[..32]) + .expect("libp2p key must be valid"); + Ok(CombinedKey::from(ed_keypair)) + } + _ => Err("ENR: Unsupported libp2p key type"), + } + } +} + +// helper function to convert a peer_id to a node_id. This is only possible for secp256k1/ed25519 libp2p +// peer_ids +pub fn peer_id_to_node_id(peer_id: &PeerId) -> Result { + // A libp2p peer id byte representation should be 2 length bytes + 4 protobuf bytes + compressed pk bytes + // if generated from a PublicKey with Identity multihash. + let pk_bytes = &peer_id.to_bytes()[2..]; + + match PublicKey::from_protobuf_encoding(pk_bytes).map_err(|e| { + format!( + " Cannot parse libp2p public key public key from peer id: {}", + e + ) + })? { + PublicKey::Secp256k1(pk) => { + let uncompressed_key_bytes = &pk.encode_uncompressed()[1..]; + let mut output = [0_u8; 32]; + let mut hasher = Keccak::v256(); + hasher.update(uncompressed_key_bytes); + hasher.finalize(&mut output); + Ok(discv5::enr::NodeId::parse(&output).expect("Must be correct length")) + } + PublicKey::Ed25519(pk) => { + let uncompressed_key_bytes = pk.encode(); + let mut output = [0_u8; 32]; + let mut hasher = Keccak::v256(); + hasher.update(&uncompressed_key_bytes); + hasher.finalize(&mut output); + Ok(discv5::enr::NodeId::parse(&output).expect("Must be correct length")) + } + _ => Err("Unsupported public key".into()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_secp256k1_peer_id_conversion() { + let sk_hex = "df94a73d528434ce2309abb19c16aedb535322797dbd59c157b1e04095900f48"; + let sk_bytes = hex::decode(sk_hex).unwrap(); + let secret_key = discv5::enr::k256::ecdsa::SigningKey::from_bytes(&sk_bytes).unwrap(); + + let libp2p_sk = libp2p::identity::secp256k1::SecretKey::from_bytes(sk_bytes).unwrap(); + let secp256k1_kp: libp2p::identity::secp256k1::Keypair = libp2p_sk.into(); + let libp2p_kp = Keypair::Secp256k1(secp256k1_kp); + let peer_id = libp2p_kp.public().to_peer_id(); + + let enr = discv5::enr::EnrBuilder::new("v4") + .build(&secret_key) + .unwrap(); + let node_id = peer_id_to_node_id(&peer_id).unwrap(); + + assert_eq!(enr.node_id(), node_id); + } + + #[test] + fn test_ed25519_peer_conversion() { + let sk_hex = "4dea8a5072119927e9d243a7d953f2f4bc95b70f110978e2f9bc7a9000e4b261"; + let sk_bytes = hex::decode(sk_hex).unwrap(); + let secret = discv5::enr::ed25519_dalek::SecretKey::from_bytes(&sk_bytes).unwrap(); + let public = discv5::enr::ed25519_dalek::PublicKey::from(&secret); + let keypair = discv5::enr::ed25519_dalek::Keypair { secret, public }; + + let libp2p_sk = libp2p::identity::ed25519::SecretKey::from_bytes(sk_bytes).unwrap(); + let ed25519_kp: libp2p::identity::ed25519::Keypair = libp2p_sk.into(); + let libp2p_kp = Keypair::Ed25519(ed25519_kp); + let peer_id = libp2p_kp.public().to_peer_id(); + + let enr = discv5::enr::EnrBuilder::new("v4").build(&keypair).unwrap(); + let node_id = peer_id_to_node_id(&peer_id).unwrap(); + + assert_eq!(enr.node_id(), node_id); + } +} diff --git a/node/network/src/discovery/mod.rs b/node/network/src/discovery/mod.rs new file mode 100644 index 0000000..307377c --- /dev/null +++ b/node/network/src/discovery/mod.rs @@ -0,0 +1,632 @@ +//! The discovery sub-behaviour of Lighthouse. +//! +//! This module creates a libp2p dummy-behaviour built around the discv5 protocol. It handles +//! queries and manages access to the discovery routing table. + +pub(crate) mod enr; +pub mod enr_ext; + +use crate::metrics; +use crate::{error, Enr, NetworkConfig, NetworkGlobals}; +use discv5::{enr::NodeId, Discv5, Discv5Event}; +pub use enr::{ + build_enr, create_enr_builder_from_config, load_enr_from_disk, use_or_load_enr, CombinedKey, +}; +pub use enr_ext::{peer_id_to_node_id, CombinedKeyExt, EnrExt}; +pub use libp2p::core::identity::{Keypair, PublicKey}; + +use futures::prelude::*; +use futures::stream::FuturesUnordered; +pub use libp2p::{ + core::{connection::ConnectionId, ConnectedPoint, Multiaddr, PeerId}, + swarm::{ + handler::ConnectionHandler, DialError, NetworkBehaviour, + NetworkBehaviourAction as NBAction, NotifyHandler, PollParameters, SubstreamProtocol, + }, +}; +use lru::LruCache; +use std::{ + collections::HashMap, + net::{IpAddr, SocketAddr}, + path::Path, + pin::Pin, + sync::Arc, + task::{Context, Poll}, + time::Instant, +}; +use tokio::sync::mpsc; + +/// Local ENR storage filename. +pub const ENR_FILENAME: &str = "enr.dat"; +/// The number of closest peers to search for when doing a regular peer search. +/// +/// We could reduce this constant to speed up queries however at the cost of security. It will +/// make it easier to peers to eclipse this node. Kademlia suggests a value of 16. +pub const FIND_NODE_QUERY_CLOSEST_PEERS: usize = 16; +/// The threshold for updating `min_ttl` on a connected peer. + +/// The events emitted by polling discovery. +pub enum DiscoveryEvent { + /// A query has completed. This result contains a mapping of discovered peer IDs to the `min_ttl` + /// of the peer if it is specified. + QueryResult(HashMap>), + /// This indicates that our local UDP socketaddr has been updated and we should inform libp2p. + SocketUpdated(SocketAddr), +} + +#[derive(Debug, Clone, PartialEq)] +enum QueryType { + /// We are searching for more peers without ENR or time constraints. + FindPeers, +} + +/// The result of a query. +struct QueryResult { + query_type: QueryType, + result: Result, discv5::QueryError>, +} + +// Awaiting the event stream future +enum EventStream { + /// Awaiting an event stream to be generated. This is required due to the poll nature of + /// `Discovery` + Awaiting( + Pin< + Box< + dyn Future, discv5::Discv5Error>> + + Send, + >, + >, + ), + /// The future has completed. + Present(mpsc::Receiver), + // The future has failed or discv5 has been disabled. There are no events from discv5. + InActive, +} + +/// The main discovery service. This can be disabled via CLI arguements. When disabled the +/// underlying processes are not started, but this struct still maintains our current ENR. +pub struct Discovery { + /// A collection of seen live ENRs for quick lookup and to map peer-id's to ENRs. + cached_enrs: LruCache, + + /// The directory where the ENR is stored. + enr_dir: String, + + /// The handle for the underlying discv5 Server. + /// + /// This is behind a Reference counter to allow for futures to be spawned and polled with a + /// static lifetime. + discv5: Discv5, + + /// A collection of network constants that can be read from other threads. + network_globals: Arc, + + /// Indicates if we are actively searching for peers. We only allow a single FindPeers query at + /// a time, regardless of the query concurrency. + find_peer_active: bool, + + /// Active discovery queries. + active_queries: FuturesUnordered + Send>>>, + + /// The discv5 event stream. + event_stream: EventStream, + + /// Indicates if the discovery service has been started. When the service is disabled, this is + /// always false. + pub started: bool, +} + +impl Discovery { + /// NOTE: Creating discovery requires running within a tokio execution environment. + pub async fn new( + local_key: &Keypair, + config: &NetworkConfig, + network_globals: Arc, + ) -> error::Result { + let enr_dir = match config.network_dir.to_str() { + Some(path) => String::from(path), + None => String::from(""), + }; + + let local_enr = network_globals.local_enr.read().clone(); + + info!( + enr = %local_enr.to_base64(), + seq = %local_enr.seq(), + id = %local_enr.node_id(), + ip = ?local_enr.ip(), + udp = ?local_enr.udp(), + tcp = ?local_enr.tcp(), + udp4_socket = ?local_enr.udp_socket(), + "ENR Initialised", + ); + + let listen_socket = SocketAddr::new(config.listen_address, config.discovery_port); + + // convert the keypair into an ENR key + let enr_key: CombinedKey = CombinedKey::from_libp2p(local_key)?; + + let mut discv5 = Discv5::new(local_enr, enr_key, config.discv5_config.clone()) + .map_err(|e| format!("Discv5 service failed. Error: {:?}", e))?; + + // Add bootnodes to routing table + for bootnode_enr in config.boot_nodes_enr.clone() { + debug!( + node_id = %bootnode_enr.node_id(), + peer_id = %bootnode_enr.peer_id(), + ip = ?bootnode_enr.ip(), + udp = ?bootnode_enr.udp(), + tcp = ?bootnode_enr.tcp(), + "Adding node to routing table", + ); + let repr = bootnode_enr.to_string(); + let _ = discv5.add_enr(bootnode_enr).map_err(|e| { + error!( + addr = %repr, + error = %e.to_string(), + "Could not add peer to the local routing table", + ) + }); + } + + // Start the discv5 service and obtain an event stream + let event_stream = if !config.disable_discovery { + discv5 + .start(listen_socket) + .map_err(|e| e.to_string()) + .await?; + debug!("Discovery service started"); + EventStream::Awaiting(Box::pin(discv5.event_stream())) + } else { + EventStream::InActive + }; + + if !config.boot_nodes_multiaddr.is_empty() { + info!("Contacting Multiaddr boot-nodes for their ENR"); + } + + // get futures for requesting the Enrs associated to these multiaddr and wait for their + // completion + let mut fut_coll = config + .boot_nodes_multiaddr + .iter() + .map(|addr| addr.to_string()) + // request the ENR for this multiaddr and keep the original for logging + .map(|addr| { + futures::future::join( + discv5.request_enr(addr.clone()), + futures::future::ready(addr), + ) + }) + .collect::>(); + + while let Some((result, original_addr)) = fut_coll.next().await { + match result { + Ok(enr) => { + debug!( + node_id = %enr.node_id(), + peer_id = %enr.peer_id(), + ip = ?enr.ip(), + udp = ?enr.udp(), + tcp = ?enr.tcp(), + "Adding node to routing table", + ); + let _ = discv5.add_enr(enr).map_err(|e| { + error!( + addr = %original_addr.to_string(), + error = %e.to_string(), + "Could not add peer to the local routing table", + ) + }); + } + Err(e) => { + error!( + multiaddr = %original_addr.to_string(), + error = %e.to_string(), + "Error getting mapping to ENR", + ) + } + } + } + + Ok(Self { + cached_enrs: LruCache::new(50), + network_globals, + find_peer_active: false, + active_queries: FuturesUnordered::new(), + discv5, + event_stream, + started: !config.disable_discovery, + enr_dir, + }) + } + + /// Return the nodes local ENR. + pub fn local_enr(&self) -> Enr { + self.discv5.local_enr() + } + + /// Return the cached enrs. + pub fn cached_enrs(&self) -> impl Iterator { + self.cached_enrs.iter() + } + + /// Removes a cached ENR from the list. + pub fn remove_cached_enr(&mut self, peer_id: &PeerId) -> Option { + self.cached_enrs.pop(peer_id) + } + + /// This adds a new `FindPeers` query to the queue if one doesn't already exist. + /// The `target_peers` parameter informs discovery to end the query once the target is found. + /// The maximum this can be is 16. + pub fn discover_peers(&mut self, target_peers: usize) { + // If the discv5 service isn't running or we are in the process of a query, don't bother queuing a new one. + if !self.started || self.find_peer_active { + return; + } + // Immediately start a FindNode query + let target_peers = std::cmp::min(FIND_NODE_QUERY_CLOSEST_PEERS, target_peers); + debug!(%target_peers, "Starting a peer discovery request"); + self.find_peer_active = true; + self.start_query(QueryType::FindPeers, target_peers); + } + + /// Add an ENR to the routing table of the discovery mechanism. + pub fn add_enr(&mut self, enr: Enr) { + // add the enr to seen caches + self.cached_enrs.put(enr.peer_id(), enr.clone()); + + if let Err(e) = self.discv5.add_enr(enr) { + debug!( + error = %e, + "Could not add peer to the local routing table", + ) + } + } + + /// Returns an iterator over all enr entries in the DHT. + pub fn table_entries_enr(&mut self) -> Vec { + self.discv5.table_entries_enr() + } + + /// Returns the ENR of a known peer if it exists. + pub fn enr_of_peer(&mut self, peer_id: &PeerId) -> Option { + // first search the local cache + if let Some(enr) = self.cached_enrs.get(peer_id) { + return Some(enr.clone()); + } + // not in the local cache, look in the routing table + if let Ok(node_id) = enr_ext::peer_id_to_node_id(peer_id) { + self.discv5.find_enr(&node_id) + } else { + None + } + } + + /// Updates the local ENR TCP port. + /// There currently isn't a case to update the address here. We opt for discovery to + /// automatically update the external address. + /// + /// If the external address needs to be modified, use `update_enr_udp_socket. + pub fn update_enr_tcp_port(&mut self, port: u16) -> Result<(), String> { + self.discv5 + .enr_insert("tcp", &port.to_be_bytes()) + .map_err(|e| format!("{:?}", e))?; + + // replace the global version + *self.network_globals.local_enr.write() = self.discv5.local_enr(); + // persist modified enr to disk + enr::save_enr_to_disk(Path::new(&self.enr_dir), &self.local_enr()); + Ok(()) + } + + /// Updates the local ENR UDP socket. + /// + /// This is with caution. Discovery should automatically maintain this. This should only be + /// used when automatic discovery is disabled. + pub fn update_enr_udp_socket(&mut self, socket_addr: SocketAddr) -> Result<(), String> { + match socket_addr { + SocketAddr::V4(socket) => { + self.discv5 + .enr_insert("ip", &socket.ip().octets()) + .map_err(|e| format!("{:?}", e))?; + self.discv5 + .enr_insert("udp", &socket.port().to_be_bytes()) + .map_err(|e| format!("{:?}", e))?; + } + SocketAddr::V6(socket) => { + self.discv5 + .enr_insert("ip6", &socket.ip().octets()) + .map_err(|e| format!("{:?}", e))?; + self.discv5 + .enr_insert("udp6", &socket.port().to_be_bytes()) + .map_err(|e| format!("{:?}", e))?; + } + } + + // replace the global version + *self.network_globals.local_enr.write() = self.discv5.local_enr(); + // persist modified enr to disk + enr::save_enr_to_disk(Path::new(&self.enr_dir), &self.local_enr()); + Ok(()) + } + + // Bans a peer and it's associated seen IP addresses. + pub fn ban_peer(&mut self, peer_id: &PeerId, ip_addresses: Vec) { + // first try and convert the peer_id to a node_id. + if let Ok(node_id) = peer_id_to_node_id(peer_id) { + // If we could convert this peer id, remove it from the DHT and ban it from discovery. + self.discv5.ban_node(&node_id, None); + // Remove the node from the routing table. + self.discv5.remove_node(&node_id); + } + + for ip_address in ip_addresses { + self.discv5.ban_ip(ip_address, None); + } + } + + /// Unbans the peer in discovery. + pub fn unban_peer(&mut self, peer_id: &PeerId, ip_addresses: Vec) { + // first try and convert the peer_id to a node_id. + if let Ok(node_id) = peer_id_to_node_id(peer_id) { + self.discv5.ban_node_remove(&node_id); + } + + for ip_address in ip_addresses { + self.discv5.ban_ip_remove(&ip_address); + } + } + + /// Marks node as disconnected in the DHT, freeing up space for other nodes, this also removes + /// nodes from the cached ENR list. + pub fn disconnect_peer(&mut self, peer_id: &PeerId) { + if let Ok(node_id) = peer_id_to_node_id(peer_id) { + self.discv5.disconnect_node(&node_id); + } + // Remove the peer from the cached list, to prevent redialing disconnected + // peers. + self.cached_enrs.pop(peer_id); + } + + /* Internal Functions */ + + /// Search for a specified number of new peers using the underlying discovery mechanism. + /// + /// This can optionally search for peers for a given predicate. Regardless of the predicate + /// given, this will only search for peers on the same enr_fork_id as specified in the local + /// ENR. + fn start_query(&mut self, query: QueryType, target_peers: usize) { + // Generate a random target node id. + let random_node = NodeId::random(); + + // Build the future + let query_future = self + .discv5 + .find_node_predicate(random_node, Box::new(|_| true), target_peers) + .map(|v| QueryResult { + query_type: query, + result: v, + }); + + // Add the future to active queries, to be executed. + self.active_queries.push(Box::pin(query_future)); + } + + /// Process the completed QueryResult returned from discv5. + fn process_completed_queries( + &mut self, + query: QueryResult, + ) -> Option>> { + match query.query_type { + QueryType::FindPeers => { + self.find_peer_active = false; + match query.result { + Ok(r) if r.is_empty() => { + debug!("Discovery query yielded no results."); + } + Ok(r) => { + debug!(peers_found = r.len(), "Discovery query completed"); + let mut results: HashMap<_, Option> = HashMap::new(); + r.iter().for_each(|enr| { + // cache the found ENR's + self.cached_enrs.put(enr.peer_id(), enr.clone()); + results.insert(enr.peer_id(), None); + }); + return Some(results); + } + Err(e) => { + warn!(error = %e, "Discovery query failed"); + } + } + } + } + + None + } + + /// Drives the queries returning any results from completed queries. + fn poll_queries(&mut self, cx: &mut Context) -> Option>> { + while let Poll::Ready(Some(query_result)) = self.active_queries.poll_next_unpin(cx) { + let result = self.process_completed_queries(query_result); + if result.is_some() { + return result; + } + } + None + } +} + +/* NetworkBehaviour Implementation */ + +impl NetworkBehaviour for Discovery { + // Discovery is not a real NetworkBehaviour... + type ConnectionHandler = libp2p::swarm::handler::DummyConnectionHandler; + type OutEvent = DiscoveryEvent; + + fn new_handler(&mut self) -> Self::ConnectionHandler { + libp2p::swarm::handler::DummyConnectionHandler::default() + } + + // Handles the libp2p request to obtain multiaddrs for peer_id's in order to dial them. + fn addresses_of_peer(&mut self, peer_id: &PeerId) -> Vec { + if let Some(enr) = self.enr_of_peer(peer_id) { + // ENR's may have multiple Multiaddrs. The multi-addr associated with the UDP + // port is removed, which is assumed to be associated with the discv5 protocol (and + // therefore irrelevant for other libp2p components). + enr.multiaddr_tcp() + } else { + // PeerId is not known + Vec::new() + } + } + + fn inject_event( + &mut self, + _: PeerId, + _: ConnectionId, + _: ::OutEvent, + ) { + } + + fn inject_dial_failure( + &mut self, + peer_id: Option, + _handler: Self::ConnectionHandler, + error: &DialError, + ) { + if let Some(peer_id) = peer_id { + match error { + DialError::Banned + | DialError::LocalPeerId + | DialError::InvalidPeerId(_) + | DialError::ConnectionIo(_) + | DialError::NoAddresses + | DialError::Transport(_) + | DialError::WrongPeerId { .. } => { + // set peer as disconnected in discovery DHT + debug!(peer_id = %peer_id, "Marking peer disconnected in DHT"); + self.disconnect_peer(&peer_id); + } + DialError::ConnectionLimit(_) + | DialError::DialPeerConditionFalse(_) + | DialError::Aborted => {} + } + } + } + + // Main execution loop to drive the behaviour + fn poll( + &mut self, + cx: &mut Context, + _: &mut impl PollParameters, + ) -> Poll> { + if !self.started { + return Poll::Pending; + } + + // Drive the queries and return any results from completed queries + if let Some(results) = self.poll_queries(cx) { + // return the result to the peer manager + return Poll::Ready(NBAction::GenerateEvent(DiscoveryEvent::QueryResult( + results, + ))); + } + + // Process the server event stream + match self.event_stream { + EventStream::Awaiting(ref mut fut) => { + // Still awaiting the event stream, poll it + if let Poll::Ready(event_stream) = fut.poll_unpin(cx) { + match event_stream { + Ok(stream) => { + debug!("Discv5 event stream ready"); + self.event_stream = EventStream::Present(stream); + } + Err(e) => { + error!(error = %e, "Discv5 event stream failed"); + self.event_stream = EventStream::InActive; + } + } + } + } + EventStream::InActive => {} // ignore checking the stream + EventStream::Present(ref mut stream) => { + while let Poll::Ready(Some(event)) = stream.poll_recv(cx) { + match event { + // We filter out unwanted discv5 events here and only propagate useful results to + // the peer manager. + Discv5Event::Discovered(_enr) => { + // Peers that get discovered during a query but are not contactable or + // don't match a predicate can end up here. For debugging purposes we + // log these to see if we are unnecessarily dropping discovered peers + /* + if enr.eth2() == self.local_enr().eth2() { + trace!(self.log, "Peer found in process of query"; "peer_id" => format!("{}", enr.peer_id()), "tcp_socket" => enr.tcp_socket()); + } else { + // this is temporary warning for debugging the DHT + warn!(self.log, "Found peer during discovery not on correct fork"; "peer_id" => format!("{}", enr.peer_id()), "tcp_socket" => enr.tcp_socket()); + } + */ + } + Discv5Event::SocketUpdated(socket) => { + info!(ip = %socket.ip(), udp_port = %socket.port(), "Address updated"); + metrics::inc_counter(&metrics::ADDRESS_UPDATE_COUNT); + metrics::check_nat(); + // Discv5 will have updated our local ENR. We save the updated version + // to disk. + let enr = self.discv5.local_enr(); + enr::save_enr_to_disk(Path::new(&self.enr_dir), &enr); + // update network globals + *self.network_globals.local_enr.write() = enr; + return Poll::Ready(NBAction::GenerateEvent( + DiscoveryEvent::SocketUpdated(socket), + )); + } + Discv5Event::EnrAdded { .. } + | Discv5Event::TalkRequest(_) + | Discv5Event::NodeInserted { .. } => {} // Ignore all other discv5 server events + } + } + } + } + Poll::Pending + } +} + +// #[cfg(test)] +// mod tests { +// use super::*; +// use enr::EnrBuilder; +// use unused_port::unused_udp_port; + +// async fn build_discovery() -> Discovery { +// let keypair = libp2p::identity::Keypair::generate_secp256k1(); +// let config = NetworkConfig { +// discovery_port: unused_udp_port().unwrap(), +// ..Default::default() +// }; + +// let enr_key: CombinedKey = CombinedKey::from_libp2p(&keypair).unwrap(); +// let enr: Enr = build_enr(&enr_key, &config).unwrap(); + +// let globals = NetworkGlobals::new( +// enr, +// 9000, +// 9000, +// vec![], +// ); + +// Discovery::new(&keypair, &config, Arc::new(globals)) +// .await +// .unwrap() +// } + +// fn make_enr(subnet_ids: Vec) -> Enr { +// let mut builder = EnrBuilder::new("v4"); +// let keypair = libp2p::identity::Keypair::generate_secp256k1(); +// let enr_key: CombinedKey = CombinedKey::from_libp2p(&keypair).unwrap(); +// builder.build(&enr_key).unwrap() +// } +// } diff --git a/node/network/src/lib.rs b/node/network/src/lib.rs new file mode 100644 index 0000000..6eb318b --- /dev/null +++ b/node/network/src/lib.rs @@ -0,0 +1,155 @@ +/// This crate contains the main link for lighthouse to rust-libp2p. It therefore re-exports +/// all required libp2p functionality. +/// +/// This crate builds and manages the libp2p services required by the beacon node. +#[macro_use] +extern crate lazy_static; + +#[macro_use] +extern crate tracing; + +pub mod behaviour; +mod config; + +#[allow(clippy::mutable_key_type)] +// PeerId in hashmaps are no longer permitted by clippy +pub mod discovery; +pub mod metrics; +pub mod nat; +pub mod peer_manager; +pub mod rpc; +mod service; +pub mod types; + +pub use config::gossip_max_size; +use std::net::SocketAddr; + +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use shared_types::TxID; +use std::str::FromStr; + +/// Wrapper over a libp2p `PeerId` which implements `Serialize` and `Deserialize` +#[derive(Clone, Debug)] +pub struct PeerIdSerialized(libp2p::PeerId); + +impl From for PeerId { + fn from(peer_id: PeerIdSerialized) -> Self { + peer_id.0 + } +} + +impl FromStr for PeerIdSerialized { + type Err = String; + + fn from_str(s: &str) -> Result { + Ok(Self( + PeerId::from_str(s).map_err(|e| format!("Invalid peer id: {}", e))?, + )) + } +} + +impl Serialize for PeerIdSerialized { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.0.to_string()) + } +} + +impl<'de> Deserialize<'de> for PeerIdSerialized { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s: String = Deserialize::deserialize(deserializer)?; + Ok(Self(PeerId::from_str(&s).map_err(|e| { + de::Error::custom(format!("Failed to deserialise peer id: {:?}", e)) + })?)) + } +} + +pub use crate::types::{error, Enr, GossipTopic, NetworkGlobals, PubsubMessage}; + +pub use prometheus_client; + +pub use behaviour::{BehaviourEvent, Gossipsub, PeerRequestId, Request, Response}; +pub use config::Config as NetworkConfig; +pub use discovery::{CombinedKeyExt, EnrExt}; +pub use discv5; +pub use libp2p; +pub use libp2p::bandwidth::BandwidthSinks; +pub use libp2p::core::identity::{error::SigningError, Keypair, PublicKey}; +pub use libp2p::gossipsub::{IdentTopic, MessageAcceptance, MessageId, Topic, TopicHash}; +pub use libp2p::{core::ConnectedPoint, PeerId, Swarm}; +pub use libp2p::{multiaddr, Multiaddr}; +pub use metrics::scrape_discovery_metrics; +pub use peer_manager::{ + peerdb::client::Client, + peerdb::score::{PeerAction, ReportSource}, + peerdb::PeerDB, + ConnectionDirection, PeerConnectionStatus, PeerInfo, PeerManager, SyncInfo, SyncStatus, +}; +pub use service::{load_private_key, Context, Libp2pEvent, Service, NETWORK_KEY_FILENAME}; + +/// Application level requests sent to the network. +#[derive(Debug, Clone, Copy)] +pub enum RequestId { + Router, + Sync(SyncId), +} + +#[derive(Debug, Clone, Copy)] +pub enum SyncId { + SerialSync { tx_id: TxID }, +} + +/// Types of messages that the network service can receive. +#[derive(Debug)] +pub enum NetworkMessage { + /// Send an RPC request to the libp2p service. + SendRequest { + peer_id: PeerId, + request: Request, + request_id: RequestId, + }, + /// Send a successful Response to the libp2p service. + SendResponse { + peer_id: PeerId, + response: Response, + id: PeerRequestId, + }, + /// Send an error response to an RPC request. + SendErrorResponse { + peer_id: PeerId, + error: rpc::RPCResponseErrorCode, + reason: String, + id: PeerRequestId, + }, + /// Publish a list of messages to the gossipsub protocol. + Publish { messages: Vec }, + /// Reports a peer to the peer manager for performing an action. + ReportPeer { + peer_id: PeerId, + action: PeerAction, + source: ReportSource, + msg: &'static str, + }, + /// Disconnect an ban a peer, providing a reason. + GoodbyePeer { + peer_id: PeerId, + reason: rpc::GoodbyeReason, + source: ReportSource, + }, + /// Start dialing a new peer. + DialPeer { address: Multiaddr, peer_id: PeerId }, + /// Notify that new file stored in db. + AnnounceLocalFile { tx_id: TxID }, + /// Called if a known external TCP socket address has been updated. + UPnPMappingEstablished { + /// The external TCP address has been updated. + tcp_socket: Option, + /// The external UDP address has been updated. + udp_socket: Option, + }, +} diff --git a/node/network/src/metrics.rs b/node/network/src/metrics.rs new file mode 100644 index 0000000..66d7a1f --- /dev/null +++ b/node/network/src/metrics.rs @@ -0,0 +1,162 @@ +pub use lighthouse_metrics::*; + +lazy_static! { + pub static ref NAT_OPEN: Result = try_create_int_counter( + "nat_open", + "An estimate indicating if the local node is exposed to the internet." + ); + pub static ref ADDRESS_UPDATE_COUNT: Result = try_create_int_counter( + "libp2p_address_update_total", + "Count of libp2p socked updated events (when our view of our IP address has changed)" + ); + pub static ref PEERS_CONNECTED: Result = try_create_int_gauge( + "libp2p_peers", + "Count of libp2p peers currently connected" + ); + + pub static ref PEER_CONNECT_EVENT_COUNT: Result = try_create_int_counter( + "libp2p_peer_connect_event_total", + "Count of libp2p peer connect events (not the current number of connected peers)" + ); + pub static ref PEER_DISCONNECT_EVENT_COUNT: Result = try_create_int_counter( + "libp2p_peer_disconnect_event_total", + "Count of libp2p peer disconnect events" + ); + pub static ref DISCOVERY_SENT_BYTES: Result = try_create_int_gauge( + "discovery_sent_bytes", + "The number of bytes sent in discovery" + ); + pub static ref DISCOVERY_RECV_BYTES: Result = try_create_int_gauge( + "discovery_recv_bytes", + "The number of bytes received in discovery" + ); + pub static ref DISCOVERY_QUEUE: Result = try_create_int_gauge( + "discovery_queue_size", + "The number of discovery queries awaiting execution" + ); + pub static ref DISCOVERY_REQS: Result = try_create_float_gauge( + "discovery_requests", + "The number of unsolicited discovery requests per second" + ); + pub static ref DISCOVERY_SESSIONS: Result = try_create_int_gauge( + "discovery_sessions", + "The number of active discovery sessions with peers" + ); + + pub static ref PEERS_PER_CLIENT: Result = try_create_int_gauge_vec( + "libp2p_peers_per_client", + "The connected peers via client implementation", + &["Client"] + ); + pub static ref FAILED_ATTESTATION_PUBLISHES_PER_SUBNET: Result = + try_create_int_gauge_vec( + "gossipsub_failed_attestation_publishes_per_subnet", + "Failed attestation publishes per subnet", + &["subnet"] + ); + pub static ref FAILED_PUBLISHES_PER_MAIN_TOPIC: Result = try_create_int_gauge_vec( + "gossipsub_failed_publishes_per_main_topic", + "Failed gossip publishes", + &["topic_hash"] + ); + pub static ref TOTAL_RPC_ERRORS_PER_CLIENT: Result = try_create_int_counter_vec( + "libp2p_rpc_errors_per_client", + "RPC errors per client", + &["client", "rpc_error", "direction"] + ); + pub static ref TOTAL_RPC_REQUESTS: Result = try_create_int_counter_vec( + "libp2p_rpc_requests_total", + "RPC requests total", + &["type"] + ); + pub static ref PEER_ACTION_EVENTS_PER_CLIENT: Result = + try_create_int_counter_vec( + "libp2p_peer_actions_per_client", + "Score reports per client", + &["client", "action", "source"] + ); + pub static ref GOSSIP_UNACCEPTED_MESSAGES_PER_CLIENT: Result = + try_create_int_counter_vec( + "gossipsub_unaccepted_messages_per_client", + "Gossipsub messages that we did not accept, per client", + &["client", "validation_result"] + ); + pub static ref GOSSIP_LATE_PUBLISH_PER_TOPIC_KIND: Result = + try_create_int_counter_vec( + "gossipsub_late_publish_per_topic_kind", + "Messages published late to gossipsub per topic kind.", + &["topic_kind"] + ); + pub static ref GOSSIP_EXPIRED_LATE_PUBLISH_PER_TOPIC_KIND: Result = + try_create_int_counter_vec( + "gossipsub_expired_late_publish_per_topic_kind", + "Messages that expired waiting to be published on retry to gossipsub per topic kind.", + &["topic_kind"] + ); + pub static ref GOSSIP_FAILED_LATE_PUBLISH_PER_TOPIC_KIND: Result = + try_create_int_counter_vec( + "gossipsub_failed_late_publish_per_topic_kind", + "Messages that failed to be published on retry to gossipsub per topic kind.", + &["topic_kind"] + ); + pub static ref PEER_SCORE_DISTRIBUTION: Result = + try_create_int_gauge_vec( + "peer_score_distribution", + "The distribution of connected peer scores", + &["position"] + ); + pub static ref PEER_SCORE_PER_CLIENT: Result = + try_create_float_gauge_vec( + "peer_score_per_client", + "Average score per client", + &["client"] + ); + + /* + * Inbound/Outbound peers + */ + /// The number of peers that dialed us. + pub static ref NETWORK_INBOUND_PEERS: Result = + try_create_int_gauge("network_inbound_peers","The number of peers that are currently connected that have dialed us."); + + /// The number of peers that we dialed us. + pub static ref NETWORK_OUTBOUND_PEERS: Result = + try_create_int_gauge("network_outbound_peers","The number of peers that are currently connected that we dialed."); + + /* + * Peer Reporting + */ + pub static ref REPORT_PEER_MSGS: Result = try_create_int_counter_vec( + "libp2p_report_peer_msgs_total", + "Number of peer reports per msg", + &["msg"] + ); +} + +/// Checks if we consider the NAT open. +/// +/// Conditions for an open NAT: +/// 1. We have 1 or more SOCKET_UPDATED messages. This occurs when discovery has a majority of +/// users reporting an external port and our ENR gets updated. +/// 2. We have 0 SOCKET_UPDATED messages (can be true if the port was correct on boot), then we +/// rely on whether we have any inbound messages. If we have no socket update messages, but +/// manage to get at least one inbound peer, we are exposed correctly. +pub fn check_nat() { + // NAT is already deemed open. + if NAT_OPEN.as_ref().map(|v| v.get()).unwrap_or(0) != 0 { + return; + } + if ADDRESS_UPDATE_COUNT.as_ref().map(|v| v.get()).unwrap_or(0) == 0 + || NETWORK_INBOUND_PEERS.as_ref().map(|v| v.get()).unwrap_or(0) != 0_i64 + { + inc_counter(&NAT_OPEN); + } +} + +pub fn scrape_discovery_metrics() { + let metrics = discv5::metrics::Metrics::from(discv5::Discv5::raw_metrics()); + set_float_gauge(&DISCOVERY_REQS, metrics.unsolicited_requests_per_second); + set_gauge(&DISCOVERY_SESSIONS, metrics.active_sessions as i64); + set_gauge(&DISCOVERY_SENT_BYTES, metrics.bytes_sent as i64); + set_gauge(&DISCOVERY_RECV_BYTES, metrics.bytes_recv as i64); +} diff --git a/node/network/src/nat.rs b/node/network/src/nat.rs new file mode 100644 index 0000000..5914cc1 --- /dev/null +++ b/node/network/src/nat.rs @@ -0,0 +1,231 @@ +//! This houses various NAT hole punching strategies. +//! +//! Currently supported strategies: +//! - UPnP + +use crate::{NetworkConfig, NetworkMessage}; +use if_addrs::get_if_addrs; +use std::net::{IpAddr, SocketAddr, SocketAddrV4}; +use tokio::sync::mpsc; + +/// Configuration required to construct the UPnP port mappings. +pub struct UPnPConfig { + /// The local tcp port. + tcp_port: u16, + /// The local udp port. + udp_port: u16, + /// Whether discovery is enabled or not. + disable_discovery: bool, +} + +impl UPnPConfig { + pub fn from_config(config: &NetworkConfig) -> Option { + if config.enr_tcp_port.is_some() + || config.enr_udp_port.is_some() + || config.enr_address.is_some() + { + None + } else { + Some(UPnPConfig { + tcp_port: config.libp2p_port, + udp_port: config.discovery_port, + disable_discovery: config.disable_discovery, + }) + } + } +} + +/// Attempts to construct external port mappings with UPnP. +pub fn construct_upnp_mappings( + config: UPnPConfig, + network_send: mpsc::UnboundedSender, +) { + info!("UPnP Attempting to initialise routes"); + match igd::search_gateway(Default::default()) { + Err(e) => info!(error = %e, "UPnP not available"), + Ok(gateway) => { + // Need to find the local listening address matched with the router subnet + let interfaces = match get_if_addrs() { + Ok(v) => v, + Err(e) => { + info!(error = %e, "UPnP failed to get local interfaces"); + return; + } + }; + let local_ip = interfaces.iter().find_map(|interface| { + // Just use the first IP of the first interface that is not a loopback and not an + // ipv6 address. + if !interface.is_loopback() { + interface.ip().is_ipv4().then(|| interface.ip()) + } else { + None + } + }); + + let local_ip = match local_ip { + None => { + info!("UPnP failed to find local IP address"); + return; + } + Some(v) => v, + }; + + debug!(ip = %local_ip, "UPnP Local IP Discovered"); + + match local_ip { + IpAddr::V4(address) => { + let libp2p_socket = SocketAddrV4::new(address, config.tcp_port); + let external_ip = gateway.get_external_ip(); + // We add specific port mappings rather than getting the router to arbitrary assign + // one. + // I've found this to be more reliable. If multiple users are behind a single + // router, they should ideally try to set different port numbers. + let tcp_socket = add_port_mapping( + &gateway, + igd::PortMappingProtocol::TCP, + libp2p_socket, + "tcp", + ) + .and_then(|_| { + let external_socket = external_ip + .as_ref() + .map(|ip| SocketAddr::new((*ip).into(), config.tcp_port)) + .map_err(|_| ()); + info!( + external_socket = format!( + "{}:{}", + external_socket + .as_ref() + .map(|ip| ip.to_string()) + .unwrap_or_else(|_| "".into()), + config.tcp_port + ) + ); + external_socket + }) + .ok(); + + let udp_socket = if !config.disable_discovery { + let discovery_socket = SocketAddrV4::new(address, config.udp_port); + add_port_mapping( + &gateway, + igd::PortMappingProtocol::UDP, + discovery_socket, + "udp", + ) + .and_then(|_| { + let external_socket = external_ip + .map(|ip| SocketAddr::new(ip.into(), config.udp_port)) + .map_err(|_| ()); + info!( + external_socket = format!( + "{}:{}", + external_socket + .as_ref() + .map(|ip| ip.to_string()) + .unwrap_or_else(|_| "".into()), + config.udp_port + ), + "UPnP UDP route established" + ); + external_socket + }) + .ok() + } else { + None + }; + + // report any updates to the network service. + network_send + .send(NetworkMessage::UPnPMappingEstablished { + tcp_socket, + udp_socket, + }) + .unwrap_or_else( + |e| debug!(error = %e, "Could not send message to the network service"), + ); + } + _ => debug!("UPnP no routes constructed. IPv6 not supported"), + } + } + }; +} + +/// Sets up a port mapping for a protocol returning the mapped port if successful. +fn add_port_mapping( + gateway: &igd::Gateway, + protocol: igd::PortMappingProtocol, + socket: SocketAddrV4, + protocol_string: &'static str, +) -> Result<(), ()> { + // We add specific port mappings rather than getting the router to arbitrary assign + // one. + // I've found this to be more reliable. If multiple users are behind a single + // router, they should ideally try to set different port numbers. + let mapping_string = &format!("lighthouse-{}", protocol_string); + for _ in 0..2 { + match gateway.add_port(protocol, socket.port(), socket, 0, mapping_string) { + Err(e) => { + match e { + igd::AddPortError::PortInUse => { + // Try and remove and re-create + debug!( + protocol = protocol_string, + port = socket.port(), + "UPnP port in use, attempting to remap" + ); + match gateway.remove_port(protocol, socket.port()) { + Ok(()) => { + debug!( + protocol = protocol_string, + port = socket.port(), + "UPnP Removed port mapping" + ) + } + Err(e) => { + debug!(protocol = protocol_string, port = socket.port(), error = %e, "UPnP Port remove failure"); + return Err(()); + } + } + } + e => { + info!(error = %e, "UPnP TCP route not set"); + return Err(()); + } + } + } + Ok(_) => { + return Ok(()); + } + } + } + Err(()) +} + +/// Removes the specified TCP and UDP port mappings. +pub fn remove_mappings(tcp_port: Option, udp_port: Option) { + if tcp_port.is_some() || udp_port.is_some() { + debug!("Removing UPnP port mappings"); + match igd::search_gateway(Default::default()) { + Ok(gateway) => { + if let Some(tcp_port) = tcp_port { + match gateway.remove_port(igd::PortMappingProtocol::TCP, tcp_port) { + Ok(()) => debug!(port = tcp_port, "UPnP Removed TCP port mapping"), + Err(e) => { + debug!(port = tcp_port, error = %e, "UPnP Failed to remove TCP port mapping") + } + } + } + if let Some(udp_port) = udp_port { + match gateway.remove_port(igd::PortMappingProtocol::UDP, udp_port) { + Ok(()) => debug!(port = tcp_port, "UPnP Removed UDP port mapping"), + Err(e) => { + debug!(port = tcp_port, error = %e, "UPnP Failed to remove UDP port mapping") + } + } + } + } + Err(e) => debug!(error = %e, "UPnP failed to remove mappings"), + } + } +} diff --git a/node/network/src/peer_manager/config.rs b/node/network/src/peer_manager/config.rs new file mode 100644 index 0000000..6c5523d --- /dev/null +++ b/node/network/src/peer_manager/config.rs @@ -0,0 +1,46 @@ +/// The time in seconds between re-status's peers. +pub const DEFAULT_STATUS_INTERVAL: u64 = 300; + +/// Default ping interval for outbound connections, in seconds. +pub const DEFAULT_PING_INTERVAL_OUTBOUND: u64 = 15; + +/// Default interval for inbound connections. +pub const DEFAULT_PING_INTERVAL_INBOUND: u64 = 20; + +/// Default number of peers to connect to. +pub const DEFAULT_TARGET_PEERS: usize = 50; + +/// Configurations for the PeerManager. +#[derive(Debug)] +pub struct Config { + /* Peer count related configurations */ + /// Whether discovery is enabled. + pub discovery_enabled: bool, + /// Whether metrics are enabled. + pub metrics_enabled: bool, + /// Target number of peers to connect to. + pub target_peer_count: usize, + + /* RPC related configurations */ + /// Time in seconds between status requests sent to peers. + pub status_interval: u64, + /// The time in seconds between PING events. We do not send a ping if the other peer has PING'd + /// us within this time frame (Seconds). This is asymmetric to avoid simultaneous pings. This + /// interval applies to inbound connections: those in which we are not the dialer. + pub ping_interval_inbound: u64, + /// Interval between PING events for peers dialed by us. + pub ping_interval_outbound: u64, +} + +impl Default for Config { + fn default() -> Self { + Config { + discovery_enabled: true, + metrics_enabled: false, + target_peer_count: DEFAULT_TARGET_PEERS, + status_interval: DEFAULT_STATUS_INTERVAL, + ping_interval_inbound: DEFAULT_PING_INTERVAL_INBOUND, + ping_interval_outbound: DEFAULT_PING_INTERVAL_OUTBOUND, + } + } +} diff --git a/node/network/src/peer_manager/mod.rs b/node/network/src/peer_manager/mod.rs new file mode 100644 index 0000000..f7b0f76 --- /dev/null +++ b/node/network/src/peer_manager/mod.rs @@ -0,0 +1,1329 @@ +//! Implementation of Lighthouse's peer management system. + +use crate::rpc::{GoodbyeReason, Protocol, RPCError, RPCResponseErrorCode}; +use crate::{error, metrics, Gossipsub}; +use crate::{NetworkGlobals, PeerId}; +use discv5::Enr; +use hashset_delay::HashSetDelay; +use libp2p::identify::IdentifyInfo; +use peerdb::{client::ClientKind, BanOperation, BanResult, ScoreUpdateResult}; +use smallvec::SmallVec; +use std::{ + sync::Arc, + time::{Duration, Instant}, +}; +use strum::IntoEnumIterator; + +pub use libp2p::core::{identity::Keypair, Multiaddr}; + +#[allow(clippy::mutable_key_type)] +// PeerId in hashmaps are no longer permitted by clippy +pub mod peerdb; + +pub use peerdb::peer_info::{ + ConnectionDirection, PeerConnectionStatus, PeerConnectionStatus::*, PeerInfo, +}; +use peerdb::score::{PeerAction, ReportSource}; +pub use peerdb::sync_status::{SyncInfo, SyncStatus}; +use std::collections::HashMap; +use std::net::IpAddr; +pub mod config; +mod network_behaviour; + +/// The heartbeat performs regular updates such as updating reputations and performing discovery +/// requests. This defines the interval in seconds. +const HEARTBEAT_INTERVAL: u64 = 30; + +/// This is used in the pruning logic. We avoid pruning peers on sync-committees if doing so would +/// lower our peer count below this number. Instead we favour a non-uniform distribution of subnet +/// peers. +pub const MIN_SYNC_COMMITTEE_PEERS: u64 = 2; +/// A fraction of `PeerManager::target_peers` that we allow to connect to us in excess of +/// `PeerManager::target_peers`. For clarity, if `PeerManager::target_peers` is 50 and +/// PEER_EXCESS_FACTOR = 0.1 we allow 10% more nodes, i.e 55. +pub const PEER_EXCESS_FACTOR: f32 = 0.1; +/// A fraction of `PeerManager::target_peers` that we want to be outbound-only connections. +pub const TARGET_OUTBOUND_ONLY_FACTOR: f32 = 0.3; +/// A fraction of `PeerManager::target_peers` that if we get below, we start a discovery query to +/// reach our target. MIN_OUTBOUND_ONLY_FACTOR must be < TARGET_OUTBOUND_ONLY_FACTOR. +pub const MIN_OUTBOUND_ONLY_FACTOR: f32 = 0.2; +/// The fraction of extra peers beyond the PEER_EXCESS_FACTOR that we allow us to dial for when +/// requiring subnet peers. More specifically, if our target peer limit is 50, and our excess peer +/// limit is 55, and we are at 55 peers, the following parameter provisions a few more slots of +/// dialing priority peers we need for validator duties. +pub const PRIORITY_PEER_EXCESS: f32 = 0.2; + +/// The main struct that handles peer's reputation and connection status. +pub struct PeerManager { + /// Storage of network globals to access the `PeerDB`. + network_globals: Arc, + /// A queue of events that the `PeerManager` is waiting to produce. + events: SmallVec<[PeerManagerEvent; 16]>, + /// A collection of inbound-connected peers awaiting to be Ping'd. + inbound_ping_peers: HashSetDelay, + /// A collection of outbound-connected peers awaiting to be Ping'd. + outbound_ping_peers: HashSetDelay, + /// A collection of peers awaiting to be Status'd. + status_peers: HashSetDelay, + /// The target number of peers we would like to connect to. + target_peers: usize, + /// The heartbeat interval to perform routine maintenance. + heartbeat: tokio::time::Interval, + /// Keeps track of whether the discovery service is enabled or not. + discovery_enabled: bool, + /// Keeps track if the current instance is reporting metrics or not. + metrics_enabled: bool, +} + +/// The events that the `PeerManager` outputs (requests). +#[derive(Debug)] +pub enum PeerManagerEvent { + /// A peer has dialed us. + PeerConnectedIncoming(PeerId), + /// A peer has been dialed. + PeerConnectedOutgoing(PeerId), + /// A peer has disconnected. + PeerDisconnected(PeerId), + /// Sends a STATUS to a peer. + Status(PeerId), + /// Sends a PING to a peer. + Ping(PeerId), + /// The peer should be disconnected. + DisconnectPeer(PeerId, GoodbyeReason), + /// Inform the behaviour to ban this peer and associated ip addresses. + Banned(PeerId, Vec), + /// The peer should be unbanned with the associated ip addresses. + UnBanned(PeerId, Vec), + /// Request the behaviour to discover more peers and the amount of peers to discover. + DiscoverPeers(usize), +} + +impl PeerManager { + // NOTE: Must be run inside a tokio executor. + pub async fn new( + cfg: config::Config, + network_globals: Arc, + ) -> error::Result { + let config::Config { + discovery_enabled, + metrics_enabled, + target_peer_count, + status_interval, + ping_interval_inbound, + ping_interval_outbound, + } = cfg; + + // Set up the peer manager heartbeat interval + let heartbeat = tokio::time::interval(tokio::time::Duration::from_secs(HEARTBEAT_INTERVAL)); + + Ok(PeerManager { + network_globals, + events: SmallVec::new(), + inbound_ping_peers: HashSetDelay::new(Duration::from_secs(ping_interval_inbound)), + outbound_ping_peers: HashSetDelay::new(Duration::from_secs(ping_interval_outbound)), + status_peers: HashSetDelay::new(Duration::from_secs(status_interval)), + target_peers: target_peer_count, + heartbeat, + discovery_enabled, + metrics_enabled, + }) + } + + /* Public accessible functions */ + + /// The application layer wants to disconnect from a peer for a particular reason. + /// + /// All instant disconnections are fatal and we ban the associated peer. + /// + /// This will send a goodbye and disconnect the peer if it is connected or dialing. + pub fn goodbye_peer(&mut self, peer_id: &PeerId, reason: GoodbyeReason, source: ReportSource) { + // Update the sync status if required + if let Some(info) = self.network_globals.peers.write().peer_info_mut(peer_id) { + debug!(peer_id = %peer_id, reason = %reason, score = %info.score(), "Sending goodbye to peer"); + if matches!(reason, GoodbyeReason::IrrelevantNetwork) { + info.update_sync_status(SyncStatus::IrrelevantPeer); + } + } + + self.report_peer( + peer_id, + PeerAction::Fatal, + source, + Some(reason), + "goodbye_peer", + ); + } + + /// Reports a peer for some action. + /// + /// If the peer doesn't exist, log a warning and insert defaults. + pub fn report_peer( + &mut self, + peer_id: &PeerId, + action: PeerAction, + source: ReportSource, + reason: Option, + msg: &'static str, + ) { + let action = self + .network_globals + .peers + .write() + .report_peer(peer_id, action, source, msg); + self.handle_score_action(peer_id, action, reason); + } + + /// Upon adjusting a Peer's score, there are times the peer manager must pass messages up to + /// libp2p. This function handles the conditional logic associated with each score update + /// result. + fn handle_score_action( + &mut self, + peer_id: &PeerId, + action: ScoreUpdateResult, + reason: Option, + ) { + match action { + ScoreUpdateResult::Ban(ban_operation) => { + // The peer has been banned and we need to handle the banning operation + // NOTE: When we ban a peer, its IP address can be banned. We do not recursively search + // through all our connected peers banning all other peers that are using this IP address. + // If these peers are behaving fine, we permit their current connections. However, if any new + // nodes or current nodes try to reconnect on a banned IP, they will be instantly banned + // and disconnected. + self.handle_ban_operation(peer_id, ban_operation, reason); + } + ScoreUpdateResult::Disconnect => { + // The peer has transitioned to a disconnect state and has been marked as such in + // the peer db. We must inform libp2p to disconnect this peer. + self.events.push(PeerManagerEvent::DisconnectPeer( + *peer_id, + GoodbyeReason::BadScore, + )); + } + ScoreUpdateResult::NoAction => { + // The report had no effect on the peer and there is nothing to do. + } + ScoreUpdateResult::Unbanned(unbanned_ips) => { + // Inform the Swarm to unban the peer + self.events + .push(PeerManagerEvent::UnBanned(*peer_id, unbanned_ips)); + } + } + } + + /// If a peer is being banned, this handles the banning operation. + fn handle_ban_operation( + &mut self, + peer_id: &PeerId, + ban_operation: BanOperation, + reason: Option, + ) { + match ban_operation { + BanOperation::DisconnectThePeer => { + // The peer was currently connected, so we start a disconnection. + // Once the peer has disconnected, its connection state will transition to a + // banned state. + self.events.push(PeerManagerEvent::DisconnectPeer( + *peer_id, + reason.unwrap_or(GoodbyeReason::BadScore), + )); + } + BanOperation::PeerDisconnecting => { + // The peer is currently being disconnected and will be banned once the + // disconnection completes. + } + BanOperation::ReadyToBan(banned_ips) => { + // The peer is not currently connected, we can safely ban it at the swarm + // level. + // Inform the Swarm to ban the peer + self.events + .push(PeerManagerEvent::Banned(*peer_id, banned_ips)); + } + } + } + + /// Peers that have been returned by discovery requests that are suitable for dialing are + /// returned here. + /// + /// NOTE: By dialing `PeerId`s and not multiaddrs, libp2p requests the multiaddr associated + /// with a new `PeerId` which involves a discovery routing table lookup. We could dial the + /// multiaddr here, however this could relate to duplicate PeerId's etc. If the lookup + /// proves resource constraining, we should switch to multiaddr dialling here. + #[allow(clippy::mutable_key_type)] + pub fn peers_discovered(&mut self, results: HashMap>) -> Vec { + let mut to_dial_peers = Vec::new(); + + let connected_or_dialing = self.network_globals.connected_or_dialing_peers(); + for (peer_id, min_ttl) in results { + // There are two conditions in deciding whether to dial this peer. + // 1. If we are less than our max connections. Discovery queries are executed to reach + // our target peers, so its fine to dial up to our max peers (which will get pruned + // in the next heartbeat down to our target). + // 2. If the peer is one our validators require for a specific subnet, then it is + // considered a priority. We have pre-allocated some extra priority slots for these + // peers as specified by PRIORITY_PEER_EXCESS. Therefore we dial these peers, even + // if we are already at our max_peer limit. + if (min_ttl.is_some() + && connected_or_dialing + to_dial_peers.len() < self.max_priority_peers() + || connected_or_dialing + to_dial_peers.len() < self.max_peers()) + && self.network_globals.peers.read().should_dial(&peer_id) + { + // This should be updated with the peer dialing. In fact created once the peer is + // dialed + if let Some(min_ttl) = min_ttl { + self.network_globals + .peers + .write() + .update_min_ttl(&peer_id, min_ttl); + } + to_dial_peers.push(peer_id); + } + } + + // Queue another discovery if we need to + self.maintain_peer_count(to_dial_peers.len()); + + to_dial_peers + } + + /// A STATUS message has been received from a peer. This resets the status timer. + pub fn peer_statusd(&mut self, peer_id: &PeerId) { + self.status_peers.insert(*peer_id); + } + + /// The maximum number of peers we allow to connect to us. This is `target_peers` * (1 + + /// PEER_EXCESS_FACTOR) + fn max_peers(&self) -> usize { + (self.target_peers as f32 * (1.0 + PEER_EXCESS_FACTOR)).ceil() as usize + } + + /// The maximum number of peers we allow when dialing a priority peer (i.e a peer that is + /// subscribed to subnets that our validator requires. This is `target_peers` * (1 + + /// PEER_EXCESS_FACTOR + PRIORITY_PEER_EXCESS) + fn max_priority_peers(&self) -> usize { + (self.target_peers as f32 * (1.0 + PEER_EXCESS_FACTOR + PRIORITY_PEER_EXCESS)).ceil() + as usize + } + + /// The minimum number of outbound peers that we reach before we start another discovery query. + fn min_outbound_only_peers(&self) -> usize { + (self.target_peers as f32 * MIN_OUTBOUND_ONLY_FACTOR).ceil() as usize + } + + /// The minimum number of outbound peers that we reach before we start another discovery query. + fn target_outbound_peers(&self) -> usize { + (self.target_peers as f32 * TARGET_OUTBOUND_ONLY_FACTOR).ceil() as usize + } + + /// The maximum number of peers that are connected or dialing before we refuse to do another + /// discovery search for more outbound peers. We can use up to half the priority peer excess allocation. + fn max_outbound_dialing_peers(&self) -> usize { + (self.target_peers as f32 * (1.0 + PEER_EXCESS_FACTOR + PRIORITY_PEER_EXCESS / 2.0)).ceil() + as usize + } + + /* Notifications from the Swarm */ + + // A peer is being dialed. + pub fn inject_dialing(&mut self, peer_id: &PeerId, enr: Option) { + self.inject_peer_connection(peer_id, ConnectingType::Dialing, enr); + } + + /// Reports if a peer is banned or not. + /// + /// This is used to determine if we should accept incoming connections. + pub fn ban_status(&self, peer_id: &PeerId) -> BanResult { + self.network_globals.peers.read().ban_status(peer_id) + } + + pub fn is_connected(&self, peer_id: &PeerId) -> bool { + self.network_globals.peers.read().is_connected(peer_id) + } + + /// Reports whether the peer limit is reached in which case we stop allowing new incoming + /// connections. + pub fn peer_limit_reached(&self, count_dialing: bool) -> bool { + if count_dialing { + // This is an incoming connection so limit by the standard max peers + self.network_globals.connected_or_dialing_peers() >= self.max_peers() + } else { + // We dialed this peer, allow up to max_outbound_dialing_peers + self.network_globals.connected_peers() >= self.max_outbound_dialing_peers() + } + } + + /// Updates `PeerInfo` with `identify` information. + pub fn identify(&mut self, peer_id: &PeerId, info: &IdentifyInfo) { + if let Some(peer_info) = self.network_globals.peers.write().peer_info_mut(peer_id) { + let previous_kind = peer_info.client().kind; + let previous_listening_addresses = + peer_info.set_listening_addresses(info.listen_addrs.clone()); + peer_info.set_client(peerdb::client::Client::from_identify_info(info)); + + if previous_kind != peer_info.client().kind + || *peer_info.listening_addresses() != previous_listening_addresses + { + debug!( + peer = %peer_id, + protocol_version = %info.protocol_version, + agent_version = %info.agent_version, + // listening_ addresses = ?info.listen_addrs, + observed_address = ?info.observed_addr, + protocols = ?info.protocols, + "Identified Peer", + ); + + // update the peer client kind metric if the peer is connected + if matches!( + peer_info.connection_status(), + PeerConnectionStatus::Connected { .. } + | PeerConnectionStatus::Disconnecting { .. } + ) { + metrics::inc_gauge_vec( + &metrics::PEERS_PER_CLIENT, + &[peer_info.client().kind.as_ref()], + ); + metrics::dec_gauge_vec(&metrics::PEERS_PER_CLIENT, &[previous_kind.as_ref()]); + } + } + } else { + error!(%peer_id, "Received an Identify response from an unknown peer"); + } + } + + /// An error has occurred in the RPC. + /// + /// This adjusts a peer's score based on the error. + pub fn handle_rpc_error( + &mut self, + peer_id: &PeerId, + protocol: Protocol, + err: &RPCError, + direction: ConnectionDirection, + ) { + let client = self.network_globals.client(peer_id); + let score = self.network_globals.peers.read().score(peer_id); + debug!(%protocol, %err, %client, %peer_id, %score, ?direction, "RPC Error"); + + metrics::inc_counter_vec( + &metrics::TOTAL_RPC_ERRORS_PER_CLIENT, + &[ + client.kind.as_ref(), + err.as_static_str(), + direction.as_ref(), + ], + ); + + // Map this error to a `PeerAction` (if any) + let peer_action = match err { + RPCError::IncompleteStream => { + // They closed early, this could mean poor connection + PeerAction::MidToleranceError + } + RPCError::InternalError(e) => { + debug!(error = %e, %peer_id, "Internal RPC Error"); + return; + } + RPCError::HandlerRejected => { + // Our fault. Do nothing + return; + } + RPCError::InvalidData(_) => { + // Peer is not complying with the protocol. This is considered a malicious action + PeerAction::Fatal + } + RPCError::IoError(_e) => { + // this could their fault or ours, so we tolerate this + PeerAction::HighToleranceError + } + RPCError::ErrorResponse(code, _) => match code { + RPCResponseErrorCode::Unknown => PeerAction::HighToleranceError, + RPCResponseErrorCode::ResourceUnavailable => { + // NOTE: This error only makes sense for the `BlocksByRange` and `BlocksByRoot` + // protocols. + // + // If we are syncing, there is no point keeping these peers around and + // continually failing to request blocks. We instantly ban them and hope that + // by the time the ban lifts, the peers will have completed their backfill + // sync. + // + // TODO: Potentially a more graceful way of handling such peers, would be to + // implement a new sync type which tracks these peers and prevents the sync + // algorithms from requesting blocks from them (at least for a set period of + // time, multiple failures would then lead to a ban). + PeerAction::Fatal + } + RPCResponseErrorCode::ServerError => PeerAction::MidToleranceError, + RPCResponseErrorCode::InvalidRequest => PeerAction::HighToleranceError, + RPCResponseErrorCode::RateLimited => match protocol { + Protocol::Ping => PeerAction::MidToleranceError, + Protocol::Goodbye => PeerAction::LowToleranceError, + Protocol::Status => PeerAction::LowToleranceError, + Protocol::DataByHash => PeerAction::MidToleranceError, + Protocol::GetChunks => PeerAction::MidToleranceError, + }, + }, + RPCError::SSZDecodeError(_) => PeerAction::Fatal, + RPCError::UnsupportedProtocol => { + // Not supporting a protocol shouldn't be considered a malicious action, but + // it is an action that in some cases will make the peer unfit to continue + // communicating. + + match protocol { + Protocol::Ping => PeerAction::Fatal, + Protocol::Goodbye => return, + Protocol::Status => PeerAction::LowToleranceError, + Protocol::DataByHash => return, + Protocol::GetChunks => return, + } + } + RPCError::StreamTimeout => match direction { + ConnectionDirection::Incoming => { + // There was a timeout responding to a peer. + debug!(%peer_id, "Timed out responding to RPC Request"); + return; + } + ConnectionDirection::Outgoing => match protocol { + Protocol::Ping => PeerAction::LowToleranceError, + Protocol::Goodbye => return, + Protocol::Status => return, + Protocol::DataByHash => PeerAction::MidToleranceError, + Protocol::GetChunks => PeerAction::MidToleranceError, + }, + }, + RPCError::NegotiationTimeout => PeerAction::LowToleranceError, + RPCError::Disconnected => return, // No penalty for a graceful disconnection + }; + + self.report_peer( + peer_id, + peer_action, + ReportSource::RPC, + None, + "handle_rpc_error", + ); + } + + /// A ping request has been received. + // NOTE: The behaviour responds with a PONG automatically + pub fn ping_request(&mut self, peer_id: &PeerId, seq: u64) { + if let Some(peer_info) = self.network_globals.peers.read().peer_info(peer_id) { + // received a ping + // reset the to-ping timer for this peer + trace!(%peer_id, seq_no = %seq, "Received a ping request"); + match peer_info.connection_direction() { + Some(ConnectionDirection::Incoming) => { + self.inbound_ping_peers.insert(*peer_id); + } + Some(ConnectionDirection::Outgoing) => { + self.outbound_ping_peers.insert(*peer_id); + } + None => { + warn!(%peer_id, "Received a ping from a peer with an unknown connection direction"); + } + } + } else { + error!(%peer_id, "Received a PING from an unknown peer"); + } + } + + /// A PONG has been returned from a peer. + pub fn pong_response(&mut self, peer_id: &PeerId, _seq: u64) { + if let Some(_peer_info) = self.network_globals.peers.read().peer_info(peer_id) { + // received a pong + } else { + error!(%peer_id, "Received a PONG from an unknown peer"); + } + } + + /// Updates the gossipsub scores for all known peers in gossipsub. + pub(crate) fn update_gossipsub_scores(&mut self, gossipsub: &Gossipsub) { + let actions = self + .network_globals + .peers + .write() + .update_gossipsub_scores(self.target_peers, gossipsub); + + for (peer_id, score_action) in actions { + self.handle_score_action(&peer_id, score_action, None); + } + } + + // This function updates metrics for all connected peers. + fn update_connected_peer_metrics(&self) { + // Do nothing if we don't have metrics enabled. + if !self.metrics_enabled { + return; + } + + let mut connected_peer_count = 0; + let mut inbound_connected_peers = 0; + let mut outbound_connected_peers = 0; + let mut clients_per_peer = HashMap::new(); + + for (_peer, peer_info) in self.network_globals.peers.read().connected_peers() { + connected_peer_count += 1; + if let PeerConnectionStatus::Connected { n_in, .. } = peer_info.connection_status() { + if *n_in > 0 { + inbound_connected_peers += 1; + } else { + outbound_connected_peers += 1; + } + } + *clients_per_peer + .entry(peer_info.client().kind.to_string()) + .or_default() += 1; + } + + metrics::set_gauge(&metrics::PEERS_CONNECTED, connected_peer_count); + metrics::set_gauge(&metrics::NETWORK_INBOUND_PEERS, inbound_connected_peers); + metrics::set_gauge(&metrics::NETWORK_OUTBOUND_PEERS, outbound_connected_peers); + + for client_kind in ClientKind::iter() { + let value = clients_per_peer.get(&client_kind.to_string()).unwrap_or(&0); + metrics::set_gauge_vec( + &metrics::PEERS_PER_CLIENT, + &[client_kind.as_ref()], + *value as i64, + ); + } + } + + /* Internal functions */ + + /// Sets a peer as connected as long as their reputation allows it + /// Informs if the peer was accepted + fn inject_connect_ingoing( + &mut self, + peer_id: &PeerId, + multiaddr: Multiaddr, + enr: Option, + ) -> bool { + self.inject_peer_connection(peer_id, ConnectingType::IngoingConnected { multiaddr }, enr) + } + + /// Sets a peer as connected as long as their reputation allows it + /// Informs if the peer was accepted + fn inject_connect_outgoing( + &mut self, + peer_id: &PeerId, + multiaddr: Multiaddr, + enr: Option, + ) -> bool { + self.inject_peer_connection( + peer_id, + ConnectingType::OutgoingConnected { multiaddr }, + enr, + ) + } + + /// Updates the state of the peer as disconnected. + /// + /// This is also called when dialing a peer fails. + fn inject_disconnect(&mut self, peer_id: &PeerId) { + let (ban_operation, purged_peers) = self + .network_globals + .peers + .write() + .inject_disconnect(peer_id); + + if let Some(ban_operation) = ban_operation { + // The peer was awaiting a ban, continue to ban the peer. + self.handle_ban_operation(peer_id, ban_operation, None); + } + + // Remove the ping and status timer for the peer + self.inbound_ping_peers.remove(peer_id); + self.outbound_ping_peers.remove(peer_id); + self.status_peers.remove(peer_id); + self.events.extend( + purged_peers + .into_iter() + .map(|(peer_id, unbanned_ips)| PeerManagerEvent::UnBanned(peer_id, unbanned_ips)), + ); + } + + /// Registers a peer as connected. The `ingoing` parameter determines if the peer is being + /// dialed or connecting to us. + /// + /// This is called by `connect_ingoing` and `connect_outgoing`. + /// + /// Informs if the peer was accepted in to the db or not. + fn inject_peer_connection( + &mut self, + peer_id: &PeerId, + connection: ConnectingType, + enr: Option, + ) -> bool { + { + let mut peerdb = self.network_globals.peers.write(); + if !matches!(peerdb.ban_status(peer_id), BanResult::NotBanned) { + // don't connect if the peer is banned + error!(%peer_id, "Connection has been allowed to a banned peer"); + } + + match connection { + ConnectingType::Dialing => { + peerdb.dialing_peer(peer_id, enr); + return true; + } + ConnectingType::IngoingConnected { multiaddr } => { + peerdb.connect_ingoing(peer_id, multiaddr, enr); + // start a timer to ping inbound peers. + self.inbound_ping_peers.insert(*peer_id); + } + ConnectingType::OutgoingConnected { multiaddr } => { + peerdb.connect_outgoing(peer_id, multiaddr, enr); + // start a timer for to ping outbound peers. + self.outbound_ping_peers.insert(*peer_id); + } + } + } + + // start a ping and status timer for the peer + self.status_peers.insert(*peer_id); + + let connected_peers = self.network_globals.connected_peers() as i64; + + // increment prometheus metrics + metrics::inc_counter(&metrics::PEER_CONNECT_EVENT_COUNT); + metrics::set_gauge(&metrics::PEERS_CONNECTED, connected_peers); + + true + } + + // Gracefully disconnects a peer without banning them. + fn disconnect_peer(&mut self, peer_id: PeerId, reason: GoodbyeReason) { + self.events + .push(PeerManagerEvent::DisconnectPeer(peer_id, reason)); + self.network_globals + .peers + .write() + .notify_disconnecting(&peer_id, false); + } + + /// This function checks the status of our current peers and optionally requests a discovery + /// query if we need to find more peers to maintain the current number of peers + fn maintain_peer_count(&mut self, dialing_peers: usize) { + // Check if we need to do a discovery lookup + if self.discovery_enabled { + let peer_count = self.network_globals.connected_or_dialing_peers(); + let outbound_only_peer_count = self.network_globals.connected_outbound_only_peers(); + let wanted_peers = if peer_count < self.target_peers.saturating_sub(dialing_peers) { + // We need more peers in general. + // The maximum discovery query is for 16 peers, but we can search for less if + // needed. + std::cmp::min( + self.target_peers.saturating_sub(dialing_peers) - peer_count, + 16, + ) + } else if outbound_only_peer_count < self.min_outbound_only_peers() + && peer_count < self.max_outbound_dialing_peers() + { + std::cmp::min( + self.max_outbound_dialing_peers() + .saturating_sub(dialing_peers) + - peer_count, + 16, + ) + } else { + 0 + }; + + if wanted_peers != 0 { + // We need more peers, re-queue a discovery lookup. + debug!( + connected = peer_count, + target = self.target_peers, + outbound = outbound_only_peer_count, + wanted = wanted_peers, + "Starting a new peer discovery query", + ); + self.events + .push(PeerManagerEvent::DiscoverPeers(wanted_peers)); + } + } + } + + /// Remove excess peers back down to our target values. + /// This prioritises peers with a good score and uniform distribution of peers across + /// subnets. + /// + /// The logic for the peer pruning is as follows: + /// + /// Global rules: + /// - Always maintain peers we need for a validator duty. + /// - Do not prune outbound peers to exceed our outbound target. + /// - Do not prune more peers than our target peer count. + /// - If we have an option to remove a number of peers, remove ones that have the least + /// long-lived subnets. + /// - When pruning peers based on subnet count. If multiple peers can be chosen, choose a peer + /// that is not subscribed to a long-lived sync committee subnet. + /// - When pruning peers based on subnet count, do not prune a peer that would lower us below the + /// MIN_SYNC_COMMITTEE_PEERS peer count. To keep it simple, we favour a minimum number of sync-committee-peers over + /// uniformity subnet peers. NOTE: We could apply more sophisticated logic, but the code is + /// simpler and easier to maintain if we take this approach. If we are pruning subnet peers + /// below the MIN_SYNC_COMMITTEE_PEERS and maintaining the sync committee peers, this should be + /// fine as subnet peers are more likely to be found than sync-committee-peers. Also, we're + /// in a bit of trouble anyway if we have so few peers on subnets. The + /// MIN_SYNC_COMMITTEE_PEERS + /// number should be set low as an absolute lower bound to maintain peers on the sync + /// committees. + /// + /// Prune peers in the following order: + /// 1. Remove worst scoring peers + /// 2. Remove peers that are not subscribed to a subnet (they have less value) + /// 3. Remove peers that we have many on any particular subnet + /// 4. Randomly remove peers if all the above are satisfied + /// + fn prune_excess_peers(&mut self) { + // The current number of connected peers. + let connected_peer_count = self.network_globals.connected_peers(); + if connected_peer_count <= self.target_peers { + // No need to prune peers + return; + } + + // Keep a list of peers we are pruning. + let mut peers_to_prune = std::collections::HashSet::new(); + let connected_outbound_peer_count = self.network_globals.connected_outbound_only_peers(); + + // Keep track of the number of outbound peers we are pruning. + let mut outbound_peers_pruned = 0; + + macro_rules! prune_peers { + ($filter: expr) => { + for (peer_id, info) in self + .network_globals + .peers + .read() + .worst_connected_peers() + .iter() + .filter(|(_, info)| !info.has_future_duty() && $filter(*info)) + { + if peers_to_prune.len() + >= connected_peer_count.saturating_sub(self.target_peers) + { + // We have found all the peers we need to drop, end. + break; + } + if peers_to_prune.contains(*peer_id) { + continue; + } + // Only remove up to the target outbound peer count. + if info.is_outbound_only() { + if self.target_outbound_peers() + outbound_peers_pruned + < connected_outbound_peer_count + { + outbound_peers_pruned += 1; + } else { + continue; + } + } + peers_to_prune.insert(**peer_id); + } + }; + } + + // 1. Look through peers that have the worst score (ignoring non-penalized scored peers). + prune_peers!(|info: &PeerInfo| { info.score().score() < 0.0 }); + + // 2. Attempt to remove peers that are not subscribed to a subnet, if we still need to + // prune more. + if peers_to_prune.len() < connected_peer_count.saturating_sub(self.target_peers) { + // prune_peers!(|info: &PeerInfo| { !info.has_long_lived_subnet() }); + prune_peers!(|_info: &PeerInfo| { true }); + } + + // 3. and 4. Remove peers that are too grouped on any given subnet. If all subnets are + // uniformly distributed, remove random peers. + // if peers_to_prune.len() < connected_peer_count.saturating_sub(self.target_peers) { + // // Of our connected peers, build a map from subnet_id -> Vec<(PeerId, PeerInfo)> + // let mut subnet_to_peer: HashMap> = + // HashMap::new(); + // // These variables are used to track if a peer is in a long-lived sync-committee as we + // // may wish to retain this peer over others when pruning. + // let mut sync_committee_peer_count: HashMap = HashMap::new(); + // let peer_to_sync_committee: HashMap< + // PeerId, + // std::collections::HashSet, + // > = HashMap::new(); + + // for (peer_id, _info) in self.network_globals.peers.read().connected_peers() { + // // Ignore peers we are already pruning + // if peers_to_prune.contains(peer_id) { + // continue; + // } + + // // Count based on long-lived subnets not short-lived subnets + // // NOTE: There are only 4 sync committees. These are likely to be denser than the + // // subnets, so our priority here to make the subnet peer count uniform, ignoring + // // the dense sync committees. + // for subnet in info.long_lived_subnets() { + // match subnet { + // Subnet::Attestation(_) => { + // subnet_to_peer + // .entry(subnet) + // .or_insert_with(Vec::new) + // .push((*peer_id, info.clone())); + // } + // Subnet::SyncCommittee(id) => { + // *sync_committee_peer_count.entry(id).or_default() += 1; + // peer_to_sync_committee + // .entry(*peer_id) + // .or_default() + // .insert(id); + // } + // } + // } + // } + + // // Add to the peers to prune mapping + // while peers_to_prune.len() < connected_peer_count.saturating_sub(self.target_peers) { + // if let Some((_, peers_on_subnet)) = subnet_to_peer + // .iter_mut() + // .max_by_key(|(_, peers)| peers.len()) + // { + // // and the subnet still contains peers + // if !peers_on_subnet.is_empty() { + // // Order the peers by the number of subnets they are long-lived + // // subscribed too, shuffle equal peers. + // peers_on_subnet.shuffle(&mut rand::thread_rng()); + // // peers_on_subnet.sort_by_key(|(_, info)| info.long_lived_subnet_count()); + + // // Try and find a candidate peer to remove from the subnet. + // // We ignore peers that would put us below our target outbound peers + // // and we currently ignore peers that would put us below our + // // sync-committee threshold, if we can avoid it. + + // let mut removed_peer_index = None; + // for (index, (candidate_peer, info)) in peers_on_subnet.iter().enumerate() { + // // Ensure we don't remove too many outbound peers + // if info.is_outbound_only() { + // if self.target_outbound_peers() + // < connected_outbound_peer_count + // .saturating_sub(outbound_peers_pruned) + // { + // outbound_peers_pruned += 1; + // } else { + // // Restart the main loop with the outbound peer removed from + // // the list. This will lower the peers per subnet count and + // // potentially a new subnet may be chosen to remove peers. This + // // can occur recursively until we have no peers left to choose + // // from. + // continue; + // } + // } + + // // Check the sync committee + // if let Some(subnets) = peer_to_sync_committee.get(candidate_peer) { + // // The peer is subscribed to some long-lived sync-committees + // // Of all the subnets this peer is subscribed too, the minimum + // // peer count of all of them is min_subnet_count + // if let Some(min_subnet_count) = subnets + // .iter() + // .filter_map(|v| sync_committee_peer_count.get(v).copied()) + // .min() + // { + // // If the minimum count is our target or lower, we + // // shouldn't remove this peer, because it drops us lower + // // than our target + // if min_subnet_count <= MIN_SYNC_COMMITTEE_PEERS { + // // Do not drop this peer in this pruning interval + // continue; + // } + // } + // } + + // // This peer is suitable to be pruned + // removed_peer_index = Some(index); + // break; + // } + + // // If we have successfully found a candidate peer to prune, prune it, + // // otherwise all peers on this subnet should not be removed due to our + // // outbound limit or min_subnet_count. In this case, we remove all + // // peers from the pruning logic and try another subnet. + // if let Some(index) = removed_peer_index { + // let (candidate_peer, _) = peers_on_subnet.remove(index); + // // Remove pruned peers from other subnet counts + // for subnet_peers in subnet_to_peer.values_mut() { + // subnet_peers.retain(|(peer_id, _)| peer_id != &candidate_peer); + // } + // // Remove pruned peers from all sync-committee counts + // if let Some(known_sync_committes) = + // peer_to_sync_committee.get(&candidate_peer) + // { + // for sync_committee in known_sync_committes { + // if let Some(sync_committee_count) = + // sync_committee_peer_count.get_mut(sync_committee) + // { + // *sync_committee_count = + // sync_committee_count.saturating_sub(1); + // } + // } + // } + // peers_to_prune.insert(candidate_peer); + // } else { + // peers_on_subnet.clear(); + // } + // continue; + // } + // } + // // If there are no peers left to prune exit. + // break; + // } + // } + + // Disconnect the pruned peers. + for peer_id in peers_to_prune { + self.disconnect_peer(peer_id, GoodbyeReason::TooManyPeers); + } + } + + /// The Peer manager's heartbeat maintains the peer count and maintains peer reputations. + /// + /// It will request discovery queries if the peer count has not reached the desired number of + /// overall peers, as well as the desired number of outbound-only peers. + /// + /// NOTE: Discovery will only add a new query if one isn't already queued. + fn heartbeat(&mut self) { + info!( + connected = self.network_globals.connected_or_dialing_peers(), + target = self.target_peers, + outbound = self.network_globals.connected_outbound_only_peers(), + "Peer statistics", + ); + + // Optionally run a discovery query if we need more peers. + self.maintain_peer_count(0); + + // Cleans up the connection state of dialing peers. + // Libp2p dials peer-ids, but sometimes the response is from another peer-id or libp2p + // returns dial errors without a peer-id attached. This function reverts peers that have a + // dialing status long than DIAL_TIMEOUT seconds to a disconnected status. This is important because + // we count the number of dialing peers in our inbound connections. + self.network_globals.peers.write().cleanup_dialing_peers(); + + // Updates peer's scores and unban any peers if required. + let actions = self.network_globals.peers.write().update_scores(); + for (peer_id, action) in actions { + self.handle_score_action(&peer_id, action, None); + } + + // Update peer score metrics; + self.update_peer_score_metrics(); + + // Prune any excess peers back to our target in such a way that incentivises good scores and + // a uniform distribution of subnets. + self.prune_excess_peers(); + } + + // Update metrics related to peer scoring. + fn update_peer_score_metrics(&self) { + if !self.metrics_enabled { + return; + } + // reset the gauges + let _ = metrics::PEER_SCORE_DISTRIBUTION + .as_ref() + .map(|gauge| gauge.reset()); + let _ = metrics::PEER_SCORE_PER_CLIENT + .as_ref() + .map(|gauge| gauge.reset()); + + let mut avg_score_per_client: HashMap = HashMap::with_capacity(5); + { + let peers_db_read_lock = self.network_globals.peers.read(); + let connected_peers = peers_db_read_lock.best_peers_by_status(PeerInfo::is_connected); + let total_peers = connected_peers.len(); + for (id, (_peer, peer_info)) in connected_peers.into_iter().enumerate() { + // First quartile + if id == 0 { + metrics::set_gauge_vec( + &metrics::PEER_SCORE_DISTRIBUTION, + &["1st"], + peer_info.score().score() as i64, + ); + } else if id == (total_peers * 3 / 4).saturating_sub(1) { + metrics::set_gauge_vec( + &metrics::PEER_SCORE_DISTRIBUTION, + &["3/4"], + peer_info.score().score() as i64, + ); + } else if id == (total_peers / 2).saturating_sub(1) { + metrics::set_gauge_vec( + &metrics::PEER_SCORE_DISTRIBUTION, + &["1/2"], + peer_info.score().score() as i64, + ); + } else if id == (total_peers / 4).saturating_sub(1) { + metrics::set_gauge_vec( + &metrics::PEER_SCORE_DISTRIBUTION, + &["1/4"], + peer_info.score().score() as i64, + ); + } else if id == total_peers.saturating_sub(1) { + metrics::set_gauge_vec( + &metrics::PEER_SCORE_DISTRIBUTION, + &["last"], + peer_info.score().score() as i64, + ); + } + + let score_peers: &mut (f64, usize) = avg_score_per_client + .entry(peer_info.client().kind.to_string()) + .or_default(); + score_peers.0 += peer_info.score().score(); + score_peers.1 += 1; + } + } // read lock ended + + for (client, (score, peers)) in avg_score_per_client { + metrics::set_float_gauge_vec( + &metrics::PEER_SCORE_PER_CLIENT, + &[&client.to_string()], + score / (peers as f64), + ); + } + } +} + +enum ConnectingType { + /// We are in the process of dialing this peer. + Dialing, + /// A peer has dialed us. + IngoingConnected { + // The multiaddr the peer connected to us on. + multiaddr: Multiaddr, + }, + /// We have successfully dialed a peer. + OutgoingConnected { + /// The multiaddr we dialed to reach the peer. + multiaddr: Multiaddr, + }, +} + +#[cfg(test)] +mod tests { + use super::*; + + async fn build_peer_manager(target_peer_count: usize) -> PeerManager { + let config = config::Config { + target_peer_count, + discovery_enabled: false, + ..Default::default() + }; + let globals = NetworkGlobals::new_test_globals(); + PeerManager::new(config, Arc::new(globals)).await.unwrap() + } + + #[tokio::test] + async fn test_peer_manager_disconnects_correctly_during_heartbeat() { + let mut peer_manager = build_peer_manager(3).await; + + // Create 5 peers to connect to. + // 2 will be outbound-only, and have the lowest score. + let peer0 = PeerId::random(); + let peer1 = PeerId::random(); + let peer2 = PeerId::random(); + let outbound_only_peer1 = PeerId::random(); + let outbound_only_peer2 = PeerId::random(); + + peer_manager.inject_connect_ingoing(&peer0, "/ip4/0.0.0.0".parse().unwrap(), None); + peer_manager.inject_connect_ingoing(&peer1, "/ip4/0.0.0.0".parse().unwrap(), None); + peer_manager.inject_connect_ingoing(&peer2, "/ip4/0.0.0.0".parse().unwrap(), None); + peer_manager.inject_connect_outgoing( + &outbound_only_peer1, + "/ip4/0.0.0.0".parse().unwrap(), + None, + ); + peer_manager.inject_connect_outgoing( + &outbound_only_peer2, + "/ip4/0.0.0.0".parse().unwrap(), + None, + ); + + // Set the outbound-only peers to have the lowest score. + peer_manager + .network_globals + .peers + .write() + .peer_info_mut(&outbound_only_peer1) + .unwrap() + .add_to_score(-1.0); + + peer_manager + .network_globals + .peers + .write() + .peer_info_mut(&outbound_only_peer2) + .unwrap() + .add_to_score(-2.0); + + // Check initial connected peers. + assert_eq!(peer_manager.network_globals.connected_or_dialing_peers(), 5); + + peer_manager.heartbeat(); + + // Check that we disconnected from two peers. + // Check that one outbound-only peer was removed because it had the worst score + // and that we did not disconnect the other outbound peer due to the minimum outbound quota. + assert_eq!(peer_manager.network_globals.connected_or_dialing_peers(), 3); + assert!(peer_manager + .network_globals + .peers + .read() + .is_connected(&outbound_only_peer1)); + assert!(!peer_manager + .network_globals + .peers + .read() + .is_connected(&outbound_only_peer2)); + + peer_manager.heartbeat(); + + // Check that if we are at target number of peers, we do not disconnect any. + assert_eq!(peer_manager.network_globals.connected_or_dialing_peers(), 3); + } + + #[tokio::test] + async fn test_peer_manager_not_enough_outbound_peers_no_panic_during_heartbeat() { + let mut peer_manager = build_peer_manager(20).await; + + // Connect to 20 ingoing-only peers. + for _i in 0..19 { + let peer = PeerId::random(); + peer_manager.inject_connect_ingoing(&peer, "/ip4/0.0.0.0".parse().unwrap(), None); + } + + // Connect an outbound-only peer. + // Give it the lowest score so that it is evaluated first in the disconnect list iterator. + let outbound_only_peer = PeerId::random(); + peer_manager.inject_connect_ingoing( + &outbound_only_peer, + "/ip4/0.0.0.0".parse().unwrap(), + None, + ); + peer_manager + .network_globals + .peers + .write() + .peer_info_mut(&(outbound_only_peer)) + .unwrap() + .add_to_score(-1.0); + // After heartbeat, we will have removed one peer. + // Having less outbound-only peers than minimum won't cause panic when the outbound-only peer is being considered for disconnection. + peer_manager.heartbeat(); + assert_eq!( + peer_manager.network_globals.connected_or_dialing_peers(), + 20 + ); + } + + #[tokio::test] + async fn test_peer_manager_remove_unhealthy_peers_brings_peers_below_target() { + let mut peer_manager = build_peer_manager(3).await; + + // Create 4 peers to connect to. + // One pair will be unhealthy inbound only and outbound only peers. + let peer0 = PeerId::random(); + let peer1 = PeerId::random(); + let inbound_only_peer1 = PeerId::random(); + let outbound_only_peer1 = PeerId::random(); + + peer_manager.inject_connect_ingoing(&peer0, "/ip4/0.0.0.0/tcp/8000".parse().unwrap(), None); + peer_manager.inject_connect_ingoing(&peer1, "/ip4/0.0.0.0/tcp/8000".parse().unwrap(), None); + + // Connect to two peers that are on the threshold of being disconnected. + peer_manager.inject_connect_ingoing( + &inbound_only_peer1, + "/ip4/0.0.0.0/tcp/8000".parse().unwrap(), + None, + ); + peer_manager.inject_connect_outgoing( + &outbound_only_peer1, + "/ip4/0.0.0.0/tcp/8000".parse().unwrap(), + None, + ); + peer_manager + .network_globals + .peers + .write() + .peer_info_mut(&(inbound_only_peer1)) + .unwrap() + .add_to_score(-19.8); + peer_manager + .network_globals + .peers + .write() + .peer_info_mut(&(outbound_only_peer1)) + .unwrap() + .add_to_score(-19.8); + peer_manager + .network_globals + .peers + .write() + .peer_info_mut(&(inbound_only_peer1)) + .unwrap() + .set_gossipsub_score(-85.0); + peer_manager + .network_globals + .peers + .write() + .peer_info_mut(&(outbound_only_peer1)) + .unwrap() + .set_gossipsub_score(-85.0); + peer_manager.heartbeat(); + // Tests that when we are over the target peer limit, after disconnecting one unhealthy peer, + // the loop to check for disconnecting peers will stop because we have removed enough peers (only needed to remove 1 to reach target). + assert_eq!(peer_manager.network_globals.connected_or_dialing_peers(), 3); + } + + #[tokio::test] + async fn test_peer_manager_removes_enough_peers_when_one_is_unhealthy() { + let mut peer_manager = build_peer_manager(3).await; + + // Create 5 peers to connect to. + // One will be unhealthy inbound only and outbound only peers. + let peer0 = PeerId::random(); + let peer1 = PeerId::random(); + let peer2 = PeerId::random(); + let inbound_only_peer1 = PeerId::random(); + let outbound_only_peer1 = PeerId::random(); + + peer_manager.inject_connect_ingoing(&peer0, "/ip4/0.0.0.0".parse().unwrap(), None); + peer_manager.inject_connect_ingoing(&peer1, "/ip4/0.0.0.0".parse().unwrap(), None); + peer_manager.inject_connect_ingoing(&peer2, "/ip4/0.0.0.0".parse().unwrap(), None); + peer_manager.inject_connect_outgoing( + &outbound_only_peer1, + "/ip4/0.0.0.0".parse().unwrap(), + None, + ); + // Have one peer be on the verge of disconnection. + peer_manager.inject_connect_ingoing( + &inbound_only_peer1, + "/ip4/0.0.0.0".parse().unwrap(), + None, + ); + peer_manager + .network_globals + .peers + .write() + .peer_info_mut(&(inbound_only_peer1)) + .unwrap() + .add_to_score(-19.9); + peer_manager + .network_globals + .peers + .write() + .peer_info_mut(&(inbound_only_peer1)) + .unwrap() + .set_gossipsub_score(-85.0); + + peer_manager.heartbeat(); + // Tests that when we are over the target peer limit, after disconnecting an unhealthy peer, + // the number of connected peers updates and we will not remove too many peers. + assert_eq!(peer_manager.network_globals.connected_or_dialing_peers(), 3); + } +} diff --git a/node/network/src/peer_manager/network_behaviour.rs b/node/network/src/peer_manager/network_behaviour.rs new file mode 100644 index 0000000..da5c0a3 --- /dev/null +++ b/node/network/src/peer_manager/network_behaviour.rs @@ -0,0 +1,251 @@ +use std::task::{Context, Poll}; + +use futures::StreamExt; +use libp2p::core::connection::ConnectionId; +use libp2p::core::ConnectedPoint; +use libp2p::swarm::handler::DummyConnectionHandler; +use libp2p::swarm::{ + ConnectionHandler, DialError, NetworkBehaviour, NetworkBehaviourAction, PollParameters, +}; +use libp2p::{Multiaddr, PeerId}; + +use crate::metrics; +use crate::rpc::GoodbyeReason; + +use super::peerdb::BanResult; +use super::{PeerManager, PeerManagerEvent, ReportSource}; + +impl NetworkBehaviour for PeerManager { + type ConnectionHandler = DummyConnectionHandler; + + type OutEvent = PeerManagerEvent; + + /* Required trait members */ + + fn new_handler(&mut self) -> Self::ConnectionHandler { + DummyConnectionHandler::default() + } + + fn inject_event( + &mut self, + _: PeerId, + _: ConnectionId, + _: ::OutEvent, + ) { + unreachable!("Dummy handler does not emit events") + } + + fn poll( + &mut self, + cx: &mut Context<'_>, + _params: &mut impl PollParameters, + ) -> Poll> { + // perform the heartbeat when necessary + while self.heartbeat.poll_tick(cx).is_ready() { + self.heartbeat(); + } + + // poll the timeouts for pings and status' + loop { + match self.inbound_ping_peers.poll_next_unpin(cx) { + Poll::Ready(Some(Ok(peer_id))) => { + self.inbound_ping_peers.insert(peer_id); + self.events.push(PeerManagerEvent::Ping(peer_id)); + } + Poll::Ready(Some(Err(e))) => { + error!(error = %e.to_string(), "Failed to check for inbound peers to ping") + } + Poll::Ready(None) | Poll::Pending => break, + } + } + + loop { + match self.outbound_ping_peers.poll_next_unpin(cx) { + Poll::Ready(Some(Ok(peer_id))) => { + self.outbound_ping_peers.insert(peer_id); + self.events.push(PeerManagerEvent::Ping(peer_id)); + } + Poll::Ready(Some(Err(e))) => { + error!(error = %e.to_string(), "Failed to check for outbound peers to ping") + } + Poll::Ready(None) | Poll::Pending => break, + } + } + + loop { + match self.status_peers.poll_next_unpin(cx) { + Poll::Ready(Some(Ok(peer_id))) => { + self.status_peers.insert(peer_id); + self.events.push(PeerManagerEvent::Status(peer_id)) + } + Poll::Ready(Some(Err(e))) => { + error!(error = %e.to_string(), "Failed to check for peers to ping") + } + Poll::Ready(None) | Poll::Pending => break, + } + } + + if !self.events.is_empty() { + return Poll::Ready(NetworkBehaviourAction::GenerateEvent(self.events.remove(0))); + } else { + self.events.shrink_to_fit(); + } + + Poll::Pending + } + + /* Overwritten trait members */ + + fn inject_connection_established( + &mut self, + peer_id: &PeerId, + _connection_id: &ConnectionId, + endpoint: &ConnectedPoint, + _failed_addresses: Option<&Vec>, + other_established: usize, + ) { + debug!(%peer_id, connection = ?endpoint.to_endpoint(), "Connection established"); + if other_established == 0 { + // self.events.push(PeerManagerEvent::MetaData(*peer_id)); + } + + // Check NAT if metrics are enabled + if self.network_globals.local_enr.read().udp().is_some() { + metrics::check_nat(); + } + + // Check to make sure the peer is not supposed to be banned + match self.ban_status(peer_id) { + // TODO: directly emit the ban event? + BanResult::BadScore => { + // This is a faulty state + error!(%peer_id, "Connected to a banned peer, re-banning"); + // Reban the peer + self.goodbye_peer(peer_id, GoodbyeReason::Banned, ReportSource::PeerManager); + return; + } + BanResult::BannedIp(ip_addr) => { + // A good peer has connected to us via a banned IP address. We ban the peer and + // prevent future connections. + debug!(%peer_id, banned_ip = %ip_addr, "Peer connected via banned IP. Banning"); + self.goodbye_peer(peer_id, GoodbyeReason::BannedIP, ReportSource::PeerManager); + return; + } + BanResult::NotBanned => {} + } + + // Count dialing peers in the limit if the peer dialied us. + let count_dialing = endpoint.is_listener(); + // Check the connection limits + if self.peer_limit_reached(count_dialing) + && self + .network_globals + .peers + .read() + .peer_info(peer_id) + .map_or(true, |peer| !peer.has_future_duty()) + { + // Gracefully disconnect the peer. + self.disconnect_peer(*peer_id, GoodbyeReason::TooManyPeers); + return; + } + + // NOTE: We don't register peers that we are disconnecting immediately. The network service + // does not need to know about these peers. + match endpoint { + ConnectedPoint::Listener { send_back_addr, .. } => { + self.inject_connect_ingoing(peer_id, send_back_addr.clone(), None); + self.events + .push(PeerManagerEvent::PeerConnectedIncoming(*peer_id)); + } + ConnectedPoint::Dialer { address, .. } => { + self.inject_connect_outgoing(peer_id, address.clone(), None); + self.events + .push(PeerManagerEvent::PeerConnectedOutgoing(*peer_id)); + } + } + + // increment prometheus metrics + self.update_connected_peer_metrics(); + metrics::inc_counter(&metrics::PEER_CONNECT_EVENT_COUNT); + } + fn inject_connection_closed( + &mut self, + peer_id: &PeerId, + _: &ConnectionId, + _: &ConnectedPoint, + _: DummyConnectionHandler, + remaining_established: usize, + ) { + if remaining_established > 0 { + return; + } + + // There are no more connections + if self + .network_globals + .peers + .read() + .is_connected_or_disconnecting(peer_id) + { + // We are disconnecting the peer or the peer has already been connected. + // Both these cases, the peer has been previously registered by the peer manager and + // potentially the application layer. + // Inform the application. + self.events + .push(PeerManagerEvent::PeerDisconnected(*peer_id)); + debug!(%peer_id, "Peer disconnected"); + } + + // NOTE: It may be the case that a rejected node, due to too many peers is disconnected + // here and the peer manager has no knowledge of its connection. We insert it here for + // reference so that peer manager can track this peer. + self.inject_disconnect(peer_id); + + // Update the prometheus metrics + self.update_connected_peer_metrics(); + metrics::inc_counter(&metrics::PEER_DISCONNECT_EVENT_COUNT); + } + + fn inject_address_change( + &mut self, + _peer_id: &PeerId, + _connection_id: &ConnectionId, + old: &ConnectedPoint, + new: &ConnectedPoint, + ) { + debug_assert!( + matches!( + (old, new), + ( + // inbound remains inbound + ConnectedPoint::Listener { .. }, + ConnectedPoint::Listener { .. } + ) | ( + // outbound remains outbound + ConnectedPoint::Dialer { .. }, + ConnectedPoint::Dialer { .. } + ) + ), + "A peer has changed between inbound and outbound" + ) + } + + /// A dial attempt has failed. + /// + /// NOTE: It can be the case that we are dialing a peer and during the dialing process the peer + /// connects and the dial attempt later fails. To handle this, we only update the peer_db if + /// the peer is not already connected. + fn inject_dial_failure( + &mut self, + peer_id: Option, + _handler: DummyConnectionHandler, + _error: &DialError, + ) { + if let Some(peer_id) = peer_id { + if !self.network_globals.peers.read().is_connected(&peer_id) { + self.inject_disconnect(&peer_id); + } + } + } +} diff --git a/node/network/src/peer_manager/peerdb.rs b/node/network/src/peer_manager/peerdb.rs new file mode 100644 index 0000000..ef82990 --- /dev/null +++ b/node/network/src/peer_manager/peerdb.rs @@ -0,0 +1,1930 @@ +use crate::{ + metrics, + multiaddr::{Multiaddr, Protocol}, + Enr, Gossipsub, PeerId, +}; +use peer_info::{ConnectionDirection, PeerConnectionStatus, PeerInfo}; +use rand::seq::SliceRandom; +use score::{PeerAction, ReportSource, Score, ScoreState}; +use std::cmp::Ordering; +use std::collections::{HashMap, HashSet}; +use std::net::{IpAddr, SocketAddr}; +use std::time::Instant; +use sync_status::SyncStatus; + +pub mod client; +pub mod peer_info; +pub mod score; +pub mod sync_status; + +/// Max number of disconnected nodes to remember. +const MAX_DC_PEERS: usize = 500; +/// The maximum number of banned nodes to remember. +pub const MAX_BANNED_PEERS: usize = 1000; +/// We ban an IP if there are more than `BANNED_PEERS_PER_IP_THRESHOLD` banned peers with this IP. +const BANNED_PEERS_PER_IP_THRESHOLD: usize = 5; +/// Relative factor of peers that are allowed to have a negative gossipsub score without penalizing +/// them in lighthouse. +const ALLOWED_NEGATIVE_GOSSIPSUB_FACTOR: f32 = 0.1; +/// The time we allow peers to be in the dialing state in our PeerDb before we revert them to a +/// disconnected state. +const DIAL_TIMEOUT: u64 = 15; + +/// Storage of known peers, their reputation and information +pub struct PeerDB { + /// The collection of known connected peers, their status and reputation + peers: HashMap, + /// The number of disconnected nodes in the database. + disconnected_peers: usize, + /// Counts banned peers in total and per ip + banned_peers_count: BannedPeersCount, +} + +impl PeerDB { + pub fn new(trusted_peers: Vec) -> Self { + // Initialize the peers hashmap with trusted peers + let peers = trusted_peers + .into_iter() + .map(|peer_id| (peer_id, PeerInfo::trusted_peer_info())) + .collect(); + Self { + disconnected_peers: 0, + banned_peers_count: BannedPeersCount::default(), + peers, + } + } + + /* Getters */ + + /// Gives the score of a peer, or default score if it is unknown. + pub fn score(&self, peer_id: &PeerId) -> f64 { + self.peers + .get(peer_id) + .map_or(&Score::default(), |info| info.score()) + .score() + } + + /// Returns an iterator over all peers in the db. + pub fn peers(&self) -> impl Iterator { + self.peers.iter() + } + + /// Gives the ids of all known peers. + pub fn peer_ids(&self) -> impl Iterator { + self.peers.keys() + } + + /// Returns a peer's info, if known. + pub fn peer_info(&self, peer_id: &PeerId) -> Option<&PeerInfo> { + self.peers.get(peer_id) + } + + /// Returns a mutable reference to a peer's info if known. + // VISIBILITY: The peer manager is able to modify some elements of the peer info, such as sync + // status. + pub(super) fn peer_info_mut(&mut self, peer_id: &PeerId) -> Option<&mut PeerInfo> { + self.peers.get_mut(peer_id) + } + + /// Returns if the peer is already connected. + pub fn is_connected(&self, peer_id: &PeerId) -> bool { + matches!( + self.connection_status(peer_id), + Some(PeerConnectionStatus::Connected { .. }) + ) + } + + /// If we are connected or currently dialing the peer returns true. + pub fn is_connected_or_dialing(&self, peer_id: &PeerId) -> bool { + matches!( + self.connection_status(peer_id), + Some(PeerConnectionStatus::Connected { .. }) + | Some(PeerConnectionStatus::Dialing { .. }) + ) + } + + /// If we are connected or in the process of disconnecting + pub fn is_connected_or_disconnecting(&self, peer_id: &PeerId) -> bool { + matches!( + self.connection_status(peer_id), + Some(PeerConnectionStatus::Connected { .. }) + | Some(PeerConnectionStatus::Disconnecting { .. }) + ) + } + + /// Returns true if the peer should be dialed. This checks the connection state and the + /// score state and determines if the peer manager should dial this peer. + pub fn should_dial(&self, peer_id: &PeerId) -> bool { + matches!( + self.connection_status(peer_id), + Some(PeerConnectionStatus::Disconnected { .. }) + | Some(PeerConnectionStatus::Unknown { .. }) + | None + ) && !self.score_state_banned_or_disconnected(peer_id) + } + + /// Returns true if the peer is synced at least to our current head. + pub fn is_synced(&self, peer_id: &PeerId) -> bool { + match self.peers.get(peer_id).map(|info| info.sync_status()) { + Some(SyncStatus::Synced { .. }) => true, + Some(_) => false, + None => false, + } + } + + /// Returns the current [`BanResult`] of the peer. This doesn't check the connection state, rather the + /// underlying score of the peer. A peer may be banned but still in the connected state + /// temporarily. + /// + /// This is used to determine if we should accept incoming connections or not. + pub fn ban_status(&self, peer_id: &PeerId) -> BanResult { + if let Some(peer) = self.peers.get(peer_id) { + match peer.score_state() { + ScoreState::Banned => BanResult::BadScore, + _ => { + if let Some(ip) = self.ip_is_banned(peer) { + BanResult::BannedIp(ip) + } else { + BanResult::NotBanned + } + } + } + } else { + BanResult::NotBanned + } + } + + /// Checks if the peer's known addresses are currently banned. + fn ip_is_banned(&self, peer: &PeerInfo) -> Option { + peer.seen_ip_addresses() + .find(|ip| self.banned_peers_count.ip_is_banned(ip)) + } + + /// Returns true if the IP is banned. + pub fn is_ip_banned(&self, ip: &IpAddr) -> bool { + self.banned_peers_count.ip_is_banned(ip) + } + + /// Returns true if the Peer is either banned or in the disconnected state. + fn score_state_banned_or_disconnected(&self, peer_id: &PeerId) -> bool { + if let Some(peer) = self.peers.get(peer_id) { + match peer.score_state() { + ScoreState::Banned | ScoreState::Disconnected => true, + _ => self.ip_is_banned(peer).is_some(), + } + } else { + false + } + } + + /// Gives the ids and info of all known connected peers. + pub fn connected_peers(&self) -> impl Iterator { + self.peers.iter().filter(|(_, info)| info.is_connected()) + } + + /// Gives the ids of all known connected peers. + pub fn connected_peer_ids(&self) -> impl Iterator { + self.peers + .iter() + .filter(|(_, info)| info.is_connected()) + .map(|(peer_id, _)| peer_id) + } + + /// Connected or dialing peers + pub fn connected_or_dialing_peers(&self) -> impl Iterator { + self.peers + .iter() + .filter(|(_, info)| info.is_connected() || info.is_dialing()) + .map(|(peer_id, _)| peer_id) + } + + /// Connected outbound-only peers + pub fn connected_outbound_only_peers(&self) -> impl Iterator { + self.peers + .iter() + .filter(|(_, info)| info.is_outbound_only()) + .map(|(peer_id, _)| peer_id) + } + + /// Gives the `peer_id` of all known connected and synced peers. + pub fn synced_peers(&self) -> impl Iterator { + self.peers + .iter() + .filter(|(_, info)| { + if info.sync_status().is_synced() || info.sync_status().is_advanced() { + return info.is_connected(); + } + false + }) + .map(|(peer_id, _)| peer_id) + } + + /// Gives the `peer_id` of all known connected and advanced peers. + pub fn advanced_peers(&self) -> impl Iterator { + self.peers + .iter() + .filter(|(_, info)| { + if info.sync_status().is_advanced() { + return info.is_connected(); + } + false + }) + .map(|(peer_id, _)| peer_id) + } + + /// Gives the ids of all known disconnected peers. + pub fn disconnected_peers(&self) -> impl Iterator { + self.peers + .iter() + .filter(|(_, info)| info.is_disconnected()) + .map(|(peer_id, _)| peer_id) + } + + /// Returns the ids of all known banned peers. + pub fn banned_peers(&self) -> impl Iterator { + self.peers + .iter() + .filter(|(_, info)| info.is_banned()) + .map(|(peer_id, _)| peer_id) + } + + /// Gives the ids of all known banned peers. + pub fn banned_peers_by_score(&self) -> impl Iterator { + self.peers + .iter() + .filter(|(_, info)| info.score_is_banned()) + .map(|(peer_id, _)| peer_id) + } + + /// Returns a vector of all connected peers sorted by score beginning with the worst scores. + /// Ties get broken randomly. + pub fn worst_connected_peers(&self) -> Vec<(&PeerId, &PeerInfo)> { + let mut connected = self + .peers + .iter() + .filter(|(_, info)| info.is_connected()) + .collect::>(); + + connected.shuffle(&mut rand::thread_rng()); + connected.sort_by_key(|(_, info)| info.score()); + connected + } + + /// Returns a vector containing peers (their ids and info), sorted by + /// score from highest to lowest, and filtered using `is_status` + pub fn best_peers_by_status(&self, is_status: F) -> Vec<(&PeerId, &PeerInfo)> + where + F: Fn(&PeerInfo) -> bool, + { + let mut by_status = self + .peers + .iter() + .filter(|(_, info)| is_status(info)) + .collect::>(); + by_status.sort_by_key(|(_, info)| info.score()); + by_status.into_iter().rev().collect() + } + + /// Returns the peer with highest reputation that satisfies `is_status` + pub fn best_by_status(&self, is_status: F) -> Option<&PeerId> + where + F: Fn(&PeerInfo) -> bool, + { + self.peers + .iter() + .filter(|(_, info)| is_status(info)) + .max_by_key(|(_, info)| info.score()) + .map(|(id, _)| id) + } + + /// Returns the peer's connection status. Returns unknown if the peer is not in the DB. + pub fn connection_status(&self, peer_id: &PeerId) -> Option { + self.peer_info(peer_id) + .map(|info| info.connection_status().clone()) + } + + /* Mutability */ + + /// Cleans up the connection state of dialing peers. + // Libp2p dial's peerids, but sometimes the response is from another peer-id or libp2p + // returns dial errors without a peer-id attached. This function reverts peers that have a + // dialing status longer than DIAL_TIMEOUT seconds to a disconnected status. This is important because + // we count the number of dialing peers in our inbound connections. + pub fn cleanup_dialing_peers(&mut self) { + let peers_to_disconnect: Vec<_> = self + .peers + .iter() + .filter_map(|(peer_id, info)| { + if let PeerConnectionStatus::Dialing { since } = info.connection_status() { + if (*since) + std::time::Duration::from_secs(DIAL_TIMEOUT) + < std::time::Instant::now() + { + return Some(*peer_id); + } + } + None + }) + .collect(); + + for peer_id in peers_to_disconnect { + self.update_connection_state(&peer_id, NewConnectionState::Disconnected); + } + } + + /// Allows the sync module to update sync status' of peers. Returns None, if the peer doesn't + /// exist and returns Some(bool) representing if the sync state was modified. + pub fn update_sync_status( + &mut self, + peer_id: &PeerId, + sync_status: SyncStatus, + ) -> Option { + let info = self.peers.get_mut(peer_id)?; + Some(info.update_sync_status(sync_status)) + } + + /// Updates the scores of known peers according to their connection status and the time that + /// has passed. This function returns a list of peers that have been unbanned. + /// NOTE: Peer scores cannot be penalized during the update, they can only increase. Therefore + /// it not possible to ban peers when updating scores. + #[must_use = "The unbanned peers must be sent to libp2p"] + pub(super) fn update_scores(&mut self) -> Vec<(PeerId, ScoreUpdateResult)> { + // Peer can be unbanned in this process. + // We return the result, such that the peer manager can inform the swarm to lift the libp2p + // ban on these peers. + let mut peers_to_unban = Vec::new(); + let mut result = Vec::new(); + + for (peer_id, info) in self.peers.iter_mut() { + let previous_state = info.score_state(); + // Update scores + info.score_update(); + + match Self::handle_score_transition(previous_state, peer_id, info) { + // A peer should not be able to be banned from a score update. + ScoreTransitionResult::Banned => { + error!(peer_id = %peer_id, "Peer has been banned in an update") + } + // A peer should not be able to transition to a disconnected state from a healthy + // state in a score update. + ScoreTransitionResult::Disconnected => { + error!(peer_id = %peer_id, "Peer has been disconnected in an update") + } + ScoreTransitionResult::Unbanned => { + peers_to_unban.push(*peer_id); + } + ScoreTransitionResult::NoAction => {} + } + } + + // Update the state in the peerdb + for unbanned_peer in peers_to_unban { + self.update_connection_state(&unbanned_peer, NewConnectionState::Unbanned); + let seen_ip_addresses = self + .peers + .get(&unbanned_peer) + .map(|info| { + info.seen_ip_addresses() + .filter(|ip| !self.is_ip_banned(ip)) + .collect::>() + }) + .unwrap_or_default(); + result.push(( + unbanned_peer, + ScoreUpdateResult::Unbanned(seen_ip_addresses), + )); + } + // Return the list so that the peer manager can update libp2p + result + } + + /// Updates gossipsub scores for all peers. + #[must_use = "Score updates need to be reported to libp2p"] + pub(super) fn update_gossipsub_scores( + &mut self, + target_peers: usize, + gossipsub: &Gossipsub, + ) -> Vec<(PeerId, ScoreUpdateResult)> { + let mut actions = Vec::new(); + let mut results = Vec::new(); + + let mut peers: Vec<_> = self + .peers + .iter_mut() + .filter(|(_peer_id, info)| info.is_connected()) + .filter_map(|(peer_id, info)| { + gossipsub + .peer_score(peer_id) + .map(|score| (peer_id, info, score)) + }) + .collect(); + + // sort descending by score + peers.sort_unstable_by(|(.., s1), (.., s2)| s2.partial_cmp(s1).unwrap_or(Ordering::Equal)); + + let mut to_ignore_negative_peers = + (target_peers as f32 * ALLOWED_NEGATIVE_GOSSIPSUB_FACTOR).ceil() as usize; + + for (peer_id, info, score) in peers { + let previous_state = info.score_state(); + info.update_gossipsub_score( + score, + if score < 0.0 && to_ignore_negative_peers > 0 { + to_ignore_negative_peers -= 1; + // We ignore the negative score for the best negative peers so that their + // gossipsub score can recover without getting disconnected. + true + } else { + false + }, + ); + + actions.push(( + *peer_id, + Self::handle_score_transition(previous_state, peer_id, info), + )); + } + + for (peer_id, action) in actions { + let result = match action { + ScoreTransitionResult::Banned => { + // The peer was banned as a result of this action. + self.update_connection_state(&peer_id, NewConnectionState::Banned) + .into() + } + ScoreTransitionResult::Disconnected => { + // The peer needs to be disconnected + + // Update the state + self.update_connection_state( + &peer_id, + NewConnectionState::Disconnecting { to_ban: false }, + ); + ScoreUpdateResult::Disconnect + } + ScoreTransitionResult::NoAction => ScoreUpdateResult::NoAction, + ScoreTransitionResult::Unbanned => { + self.update_connection_state(&peer_id, NewConnectionState::Unbanned); + let seen_ip_addresses = self + .peers + .get(&peer_id) + .map(|info| { + info.seen_ip_addresses() + .filter(|ip| !self.is_ip_banned(ip)) + .collect::>() + }) + .unwrap_or_default(); + + ScoreUpdateResult::Unbanned(seen_ip_addresses) + } + }; + + // Actions to be handled by the peer manager for each peer id + if !matches!(result, ScoreUpdateResult::NoAction) { + results.push((peer_id, result)); + } + } + results + } + + /// Reports a peer for some action. + /// + /// The action can only cause a negative effect. This can lead to disconnecting or banning a + /// specific peer. Therefore the result of this function returns if the peer needs to be banned + /// or disconnected. + /// + /// If the peer doesn't exist, log a warning and insert defaults. + #[must_use = "Banned and disconnected peers need to be handled in libp2p"] + pub(super) fn report_peer( + &mut self, + peer_id: &PeerId, + action: PeerAction, + source: ReportSource, + msg: &'static str, + ) -> ScoreUpdateResult { + metrics::inc_counter_vec(&metrics::REPORT_PEER_MSGS, &[msg]); + + match self.peers.get_mut(peer_id) { + Some(info) => { + let previous_state = info.score_state(); + info.apply_peer_action_to_score(action); + metrics::inc_counter_vec( + &metrics::PEER_ACTION_EVENTS_PER_CLIENT, + &[info.client().kind.as_ref(), action.as_ref(), source.into()], + ); + let result = Self::handle_score_transition(previous_state, peer_id, info); + if previous_state == info.score_state() { + debug!( + msg = %msg, + peer_id = %peer_id, + score = %info.score(), + "Peer score adjusted" + ); + } + match result { + ScoreTransitionResult::Banned => { + // The peer was banned as a result of this action. + self.update_connection_state(peer_id, NewConnectionState::Banned) + .into() + } + ScoreTransitionResult::Disconnected => { + // The peer needs to be disconnected + + // Update the state + self.update_connection_state( + peer_id, + NewConnectionState::Disconnecting { to_ban: false }, + ); + ScoreUpdateResult::Disconnect + } + ScoreTransitionResult::NoAction => ScoreUpdateResult::NoAction, + ScoreTransitionResult::Unbanned => { + error!( + msg = %msg, + peer_id = %peer_id, + "Report peer action lead to an unbanning", + ); + ScoreUpdateResult::NoAction + } + } + } + None => { + debug!( + msg = %msg, + peer_id =%peer_id, + "Reporting a peer that doesn't exist", + ); + ScoreUpdateResult::NoAction + } + } + } + + /// Update min ttl of a peer. + // VISIBILITY: Only the peer manager can update the min_ttl + pub(super) fn update_min_ttl(&mut self, peer_id: &PeerId, min_ttl: Instant) { + let info = self.peers.entry(*peer_id).or_default(); + + // only update if the ttl is longer + if info.min_ttl().is_none() || Some(&min_ttl) > info.min_ttl() { + info.set_min_ttl(min_ttl); + + let min_ttl_secs = min_ttl + .checked_duration_since(Instant::now()) + .map(|duration| duration.as_secs()) + .unwrap_or_else(|| 0); + + debug!( + peer_id = %peer_id, + future_min_ttl_secs = min_ttl_secs, + "Updating the time a peer is required for", + ); + } + } + + /// A peer is being dialed. + // VISIBILITY: Only the peer manager can adjust the connection state + pub(super) fn dialing_peer(&mut self, peer_id: &PeerId, enr: Option) { + self.update_connection_state(peer_id, NewConnectionState::Dialing { enr }); + } + + /// Sets a peer as connected with an ingoing connection. + // VISIBILITY: Only the peer manager can adjust the connection state. + pub(super) fn connect_ingoing( + &mut self, + peer_id: &PeerId, + seen_address: Multiaddr, + enr: Option, + ) { + self.update_connection_state( + peer_id, + NewConnectionState::Connected { + enr, + seen_address, + direction: ConnectionDirection::Incoming, + }, + ); + } + + /// Sets a peer as connected with an outgoing connection. + // VISIBILITY: Only the peer manager can adjust the connection state. + pub(super) fn connect_outgoing( + &mut self, + peer_id: &PeerId, + seen_address: Multiaddr, + enr: Option, + ) { + self.update_connection_state( + peer_id, + NewConnectionState::Connected { + enr, + seen_address, + direction: ConnectionDirection::Outgoing, + }, + ); + } + + /// The connection state of the peer has been changed. Modify the peer in the db to ensure all + /// variables are in sync with libp2p. + /// Updating the state can lead to a `BanOperation` which needs to be processed via the peer + /// manager and should be handled in the peer manager. + // NOTE: This function is vital in keeping the connection state, and thus the peerdb size in + // check and up to date with libp2p. + fn update_connection_state( + &mut self, + peer_id: &PeerId, + new_state: NewConnectionState, + ) -> Option { + let info = self.peers.entry(*peer_id).or_insert_with(|| { + // If we are not creating a new connection (or dropping a current inbound connection) log a warning indicating we are updating a + // connection state for an unknown peer. + if !matches!( + new_state, + NewConnectionState::Connected { .. } // We have established a new connection (peer may not have been seen before) + | NewConnectionState::Disconnecting { .. }// We are disconnecting from a peer that may not have been registered before + | NewConnectionState::Dialing { .. } // We are dialing a potentially new peer + | NewConnectionState::Disconnected { .. } // Dialing a peer that responds by a different ID can be immediately + // disconnected without having being stored in the db before + ) { + warn!(peer_id = %peer_id, new_state = ?new_state, "Updating state of unknown peer"); + } + PeerInfo::default() + }); + + // Ban the peer if the score is not already low enough. + if matches!(new_state, NewConnectionState::Banned) { + match info.score_state() { + ScoreState::Banned => {} + _ => { + // If score isn't low enough to ban, this function has been called incorrectly. + error!(peer_id = %peer_id, "Banning a peer with a good score"); + info.apply_peer_action_to_score(score::PeerAction::Fatal); + } + } + } + + // Handle all the possible state changes + match (info.connection_status().clone(), new_state) { + /* CONNECTED + * + * + * Handles the transition to a connected state + */ + ( + current_state, + NewConnectionState::Connected { + enr, + direction, + seen_address, + }, + ) => { + // Update the ENR if one exists + if let Some(enr) = enr { + info.set_enr(enr); + } + + match current_state { + PeerConnectionStatus::Disconnected { .. } => { + self.disconnected_peers = self.disconnected_peers.saturating_sub(1); + } + PeerConnectionStatus::Banned { .. } => { + error!(peer_id = %peer_id, "Accepted a connection from a banned peer"); + // TODO: check if this happens and report the unban back + self.banned_peers_count + .remove_banned_peer(info.seen_ip_addresses()); + } + PeerConnectionStatus::Disconnecting { .. } => { + warn!(peer_id = %peer_id, "Connected to a disconnecting peer") + } + PeerConnectionStatus::Unknown + | PeerConnectionStatus::Connected { .. } + | PeerConnectionStatus::Dialing { .. } => {} + } + + // Add the seen ip address and port to the peer's info + let socket_addr = match seen_address.iter().fold( + (None, None), + |(found_ip, found_port), protocol| match protocol { + Protocol::Ip4(ip) => (Some(ip.into()), found_port), + Protocol::Ip6(ip) => (Some(ip.into()), found_port), + Protocol::Tcp(port) => (found_ip, Some(port)), + _ => (found_ip, found_port), + }, + ) { + (Some(ip), Some(port)) => Some(SocketAddr::new(ip, port)), + (Some(_ip), None) => { + error!(peer_id = %peer_id, "Connected peer has an IP but no TCP port"); + None + } + _ => None, + }; + + // Update the connection state + match direction { + ConnectionDirection::Incoming => info.connect_ingoing(socket_addr), + ConnectionDirection::Outgoing => info.connect_outgoing(socket_addr), + } + } + + /* DIALING + * + * + * Handles the transition to a dialing state + */ + (old_state, NewConnectionState::Dialing { enr }) => { + match old_state { + PeerConnectionStatus::Banned { .. } => { + warn!(peer_id = %peer_id, "Dialing a banned peer"); + self.banned_peers_count + .remove_banned_peer(info.seen_ip_addresses()); + } + PeerConnectionStatus::Disconnected { .. } => { + self.disconnected_peers = self.disconnected_peers.saturating_sub(1); + } + PeerConnectionStatus::Connected { .. } => { + warn!(peer_id = %peer_id, "Dialing an already connected peer") + } + PeerConnectionStatus::Dialing { .. } => { + warn!(peer_id = %peer_id, "Dialing an already dialing peer") + } + PeerConnectionStatus::Disconnecting { .. } => { + warn!(peer_id = %peer_id, "Dialing a disconnecting peer") + } + PeerConnectionStatus::Unknown => {} // default behaviour + } + // Update the ENR if one is known. + if let Some(enr) = enr { + info.set_enr(enr); + } + + if let Err(e) = info.set_dialing_peer() { + error!(peer_id = %peer_id, "{}", e); + } + } + + /* DISCONNECTED + * + * + * Handle the transition to the disconnected state + */ + (old_state, NewConnectionState::Disconnected) => { + // Remove all subnets for disconnected peers. + // info.clear_subnets(); + + match old_state { + PeerConnectionStatus::Banned { .. } => {} + PeerConnectionStatus::Disconnected { .. } => {} + PeerConnectionStatus::Disconnecting { to_ban } if to_ban => { + // Update the status. + info.set_connection_status(PeerConnectionStatus::Banned { + since: Instant::now(), + }); + self.banned_peers_count + .add_banned_peer(info.seen_ip_addresses()); + let known_banned_ips = self.banned_peers_count.banned_ips(); + let banned_ips = info + .seen_ip_addresses() + .filter(|ip| known_banned_ips.contains(ip)) + .collect::>(); + return Some(BanOperation::ReadyToBan(banned_ips)); + } + PeerConnectionStatus::Disconnecting { .. } + | PeerConnectionStatus::Unknown + | PeerConnectionStatus::Connected { .. } + | PeerConnectionStatus::Dialing { .. } => { + self.disconnected_peers += 1; + info.set_connection_status(PeerConnectionStatus::Disconnected { + since: Instant::now(), + }); + } + } + } + + /* DISCONNECTING + * + * + * Handles the transition to a disconnecting state + */ + (PeerConnectionStatus::Banned { .. }, NewConnectionState::Disconnecting { to_ban }) => { + error!(peer_id = %peer_id, "Disconnecting from a banned peer"); + info.set_connection_status(PeerConnectionStatus::Disconnecting { to_ban }); + } + ( + PeerConnectionStatus::Disconnected { .. }, + NewConnectionState::Disconnecting { to_ban }, + ) => { + // If the peer was previously disconnected and is now being disconnected, decrease + // the disconnected_peers counter. + self.disconnected_peers = self.disconnected_peers.saturating_sub(1); + info.set_connection_status(PeerConnectionStatus::Disconnecting { to_ban }); + } + (_, NewConnectionState::Disconnecting { to_ban }) => { + // We overwrite all states and set this peer to be disconnecting. + // NOTE: A peer can be in the disconnected state and transition straight to a + // disconnected state. This occurs when a disconnected peer dials us, we have too + // many peers and we transition them straight to the disconnecting state. + info.set_connection_status(PeerConnectionStatus::Disconnecting { to_ban }); + } + + /* BANNED + * + * + * Handles the transition to a banned state + */ + (PeerConnectionStatus::Disconnected { .. }, NewConnectionState::Banned) => { + // It is possible to ban a peer that is currently disconnected. This can occur when + // there are many events that score it poorly and are processed after it has disconnected. + info.set_connection_status(PeerConnectionStatus::Banned { + since: Instant::now(), + }); + self.banned_peers_count + .add_banned_peer(info.seen_ip_addresses()); + self.disconnected_peers = self.disconnected_peers.saturating_sub(1); + let known_banned_ips = self.banned_peers_count.banned_ips(); + let banned_ips = info + .seen_ip_addresses() + .filter(|ip| known_banned_ips.contains(ip)) + .collect::>(); + return Some(BanOperation::ReadyToBan(banned_ips)); + } + (PeerConnectionStatus::Disconnecting { .. }, NewConnectionState::Banned) => { + // NOTE: This can occur due a rapid downscore of a peer. It goes through the + // disconnection phase and straight into banning in a short time-frame. + debug!(peer_id = %peer_id, "Banning peer that is currently disconnecting"); + // Ban the peer once the disconnection process completes. + info.set_connection_status(PeerConnectionStatus::Disconnecting { to_ban: true }); + return Some(BanOperation::PeerDisconnecting); + } + (PeerConnectionStatus::Banned { .. }, NewConnectionState::Banned) => { + error!(peer_id = %peer_id, "Banning already banned peer"); + let known_banned_ips = self.banned_peers_count.banned_ips(); + let banned_ips = info + .seen_ip_addresses() + .filter(|ip| known_banned_ips.contains(ip)) + .collect::>(); + return Some(BanOperation::ReadyToBan(banned_ips)); + } + ( + PeerConnectionStatus::Connected { .. } | PeerConnectionStatus::Dialing { .. }, + NewConnectionState::Banned, + ) => { + // update the state + info.set_connection_status(PeerConnectionStatus::Disconnecting { to_ban: true }); + return Some(BanOperation::DisconnectThePeer); + } + (PeerConnectionStatus::Unknown, NewConnectionState::Banned) => { + // shift the peer straight to banned + warn!(peer_id = %peer_id, "Banning a peer of unknown connection state"); + self.banned_peers_count + .add_banned_peer(info.seen_ip_addresses()); + info.set_connection_status(PeerConnectionStatus::Banned { + since: Instant::now(), + }); + let known_banned_ips = self.banned_peers_count.banned_ips(); + let banned_ips = info + .seen_ip_addresses() + .filter(|ip| known_banned_ips.contains(ip)) + .collect::>(); + return Some(BanOperation::ReadyToBan(banned_ips)); + } + + /* UNBANNED + * + * + * Handles the transition to an unbanned state + */ + (old_state, NewConnectionState::Unbanned) => { + if matches!(info.score_state(), ScoreState::Banned) { + error!(peer_id = %peer_id, "Unbanning a banned peer"); + } + match old_state { + PeerConnectionStatus::Unknown | PeerConnectionStatus::Connected { .. } => { + error!(peer_id = %peer_id, "Unbanning a connected peer"); + } + PeerConnectionStatus::Disconnected { .. } + | PeerConnectionStatus::Disconnecting { .. } => { + debug!(peer_id = %peer_id, "Unbanning disconnected or disconnecting peer"); + } // These are odd but fine. + PeerConnectionStatus::Dialing { .. } => {} // Also odd but acceptable + PeerConnectionStatus::Banned { since } => { + info.set_connection_status(PeerConnectionStatus::Disconnected { since }); + + // Increment the disconnected count and reduce the banned count + self.banned_peers_count + .remove_banned_peer(info.seen_ip_addresses()); + self.disconnected_peers = self.disconnected_peers.saturating_add(1); + } + } + } + } + None + } + + /// Sets the peer as disconnected. A banned peer remains banned. If the node has become banned, + /// this returns true, otherwise this is false. + // VISIBILITY: Only the peer manager can adjust the connection state. + pub(super) fn inject_disconnect( + &mut self, + peer_id: &PeerId, + ) -> (Option, Vec<(PeerId, Vec)>) { + // A peer can be banned for disconnecting. Thus another peer could be purged + let maybe_ban_op = self.update_connection_state(peer_id, NewConnectionState::Disconnected); + let purged_peers = self.shrink_to_fit(); + (maybe_ban_op, purged_peers) + } + + /// The peer manager has notified us that the peer is undergoing a normal disconnect. Optionally tag + /// the peer to be banned after the disconnect. + // VISIBILITY: Only the peer manager can adjust the connection state. + pub(super) fn notify_disconnecting(&mut self, peer_id: &PeerId, to_ban: bool) { + self.update_connection_state(peer_id, NewConnectionState::Disconnecting { to_ban }); + } + + /// Removes banned and disconnected peers from the DB if we have reached any of our limits. + /// Drops the peers with the lowest reputation so that the number of disconnected peers is less + /// than MAX_DC_PEERS + #[must_use = "Unbanned peers need to be reported to libp2p."] + fn shrink_to_fit(&mut self) -> Vec<(PeerId, Vec)> { + let excess_peers = self + .banned_peers_count + .banned_peers() + .saturating_sub(MAX_BANNED_PEERS); + let mut unbanned_peers = Vec::with_capacity(excess_peers); + + // Remove excess banned peers + while self.banned_peers_count.banned_peers() > MAX_BANNED_PEERS { + if let Some((to_drop, unbanned_ips)) = if let Some((id, info, _)) = self + .peers + .iter() + .filter_map(|(id, info)| match info.connection_status() { + PeerConnectionStatus::Banned { since } => Some((id, info, since)), + _ => None, + }) + .min_by_key(|(_, _, since)| *since) + { + self.banned_peers_count + .remove_banned_peer(info.seen_ip_addresses()); + let unbanned_ips = info + .seen_ip_addresses() + .filter(|ip| !self.is_ip_banned(ip)) + .collect::>(); + + Some((*id, unbanned_ips)) + } else { + // If there is no minimum, this is a coding error. + error!("banned_peers > MAX_BANNED_PEERS despite no banned peers in db!"); + // reset banned_peers this will also exit the loop + self.banned_peers_count = BannedPeersCount::default(); + None + } { + debug!(peer_id = %to_drop, "Removing old banned peer"); + self.peers.remove(&to_drop); + unbanned_peers.push((to_drop, unbanned_ips)) + } + } + + // Remove excess disconnected peers + while self.disconnected_peers > MAX_DC_PEERS { + if let Some(to_drop) = self + .peers + .iter() + .filter(|(_, info)| info.is_disconnected()) + .filter_map(|(id, info)| match info.connection_status() { + PeerConnectionStatus::Disconnected { since } => Some((id, since)), + _ => None, + }) + .min_by_key(|(_, age)| *age) + .map(|(id, _)| *id) + { + debug!(peer_id = %to_drop, disconnected_size = self.disconnected_peers.saturating_sub(1), "Removing old disconnected peer"); + self.peers.remove(&to_drop); + } + // If there is no minimum, this is a coding error. For safety we decrease + // the count to avoid a potential infinite loop. + self.disconnected_peers = self.disconnected_peers.saturating_sub(1); + } + + unbanned_peers + } + + /// This handles score transitions between states. It transitions peers states from + /// disconnected/banned/connected. + fn handle_score_transition( + previous_state: ScoreState, + peer_id: &PeerId, + info: &PeerInfo, + ) -> ScoreTransitionResult { + match (info.score_state(), previous_state) { + (ScoreState::Banned, ScoreState::Healthy | ScoreState::Disconnected) => { + debug!(peer_id = %peer_id, score = %info.score(), "Peer has been banned"); + ScoreTransitionResult::Banned + } + (ScoreState::Disconnected, ScoreState::Banned | ScoreState::Healthy) => { + debug!(peer_id = %peer_id, score = %info.score(), past_state = %previous_state, "Peer transitioned to disconnect state"); + // disconnect the peer if it's currently connected or dialing + if info.is_connected_or_dialing() { + ScoreTransitionResult::Disconnected + } else if previous_state == ScoreState::Banned { + ScoreTransitionResult::Unbanned + } else { + // The peer was healthy, but is already disconnected, so there is no action to + // take. + ScoreTransitionResult::NoAction + } + } + (ScoreState::Healthy, ScoreState::Disconnected) => { + debug!(peer_id = %peer_id, score = %info.score(), past_state = %previous_state, "Peer transitioned to healthy state"); + ScoreTransitionResult::NoAction + } + (ScoreState::Healthy, ScoreState::Banned) => { + debug!(peer_id = %peer_id, score = %info.score(), past_state = %previous_state, "Peer transitioned to healthy state"); + // unban the peer if it was previously banned. + ScoreTransitionResult::Unbanned + } + // Explicitly ignore states that haven't transitioned. + (ScoreState::Healthy, ScoreState::Healthy) => ScoreTransitionResult::NoAction, + (ScoreState::Disconnected, ScoreState::Disconnected) => ScoreTransitionResult::NoAction, + + (ScoreState::Banned, ScoreState::Banned) => ScoreTransitionResult::NoAction, + } + } +} + +/// Internal enum for managing connection state transitions. +#[derive(Debug)] +enum NewConnectionState { + /// A peer has connected to us. + Connected { + /// An optional known ENR if the peer was dialed. + enr: Option, + /// The seen socket address associated with the connection. + seen_address: Multiaddr, + /// The direction, incoming/outgoing. + direction: ConnectionDirection, + }, + /// The peer is in the process of being disconnected. + Disconnecting { + /// Whether the peer should be banned after the disconnect occurs. + to_ban: bool, + }, + /// We are dialing this peer. + Dialing { + /// An optional known ENR for the peer we are dialing. + enr: Option, + }, + /// The peer has been disconnected from our local node. + Disconnected, + /// The peer has been banned and actions to shift the peer to the banned state should be + /// undertaken + Banned, + /// The peer has been unbanned and the connection state should be updated to reflect this. + Unbanned, +} + +/// The result of applying a score transition to a peer. +enum ScoreTransitionResult { + /// The peer has become disconnected. + Disconnected, + /// The peer has been banned. + Banned, + /// The peer has been unbanned. + Unbanned, + /// No state change occurred. + NoAction, +} + +/// The type of results that can happen from executing the `report_peer` function. +pub enum ScoreUpdateResult { + /// The reported peer must be banned. + Ban(BanOperation), + /// The reported peer transitioned to the disconnected state and must be disconnected. + Disconnect, + /// The peer has been unbanned and this needs to be propagated to libp2p. The list of unbanned + /// IP addresses are sent along with it. + Unbanned(Vec), + /// The report requires no further action. + NoAction, +} + +impl From> for ScoreUpdateResult { + fn from(ban_operation: Option) -> Self { + match ban_operation { + None => ScoreUpdateResult::NoAction, + Some(bo) => ScoreUpdateResult::Ban(bo), + } + } +} + +/// When attempting to ban a peer provides the peer manager with the operation that must be taken. +pub enum BanOperation { + // The peer is currently connected. Perform a graceful disconnect before banning at the swarm + // level. + DisconnectThePeer, + // The peer is disconnected, it has now been banned and can be banned at the swarm level. It + // stores a collection of banned IP addresses to inform the swarm. + ReadyToBan(Vec), + // The peer is currently being disconnected, nothing to do. + PeerDisconnecting, +} + +/// When checking if a peer is banned, it can be banned for multiple reasons. +pub enum BanResult { + /// The peer's score is too low causing it to be banned. + BadScore, + /// The peer should be banned because it is connecting from a banned IP address. + BannedIp(IpAddr), + /// The peer is not banned. + NotBanned, +} + +// Helper function for unit tests +#[cfg(test)] +impl BanResult { + pub fn is_banned(&self) -> bool { + !matches!(self, BanResult::NotBanned) + } +} + +#[derive(Default)] +pub struct BannedPeersCount { + /// The number of banned peers in the database. + banned_peers: usize, + /// maps ips to number of banned peers with this ip + banned_peers_per_ip: HashMap, +} + +impl BannedPeersCount { + /// Removes the peer from the counts if it is banned. Returns true if the peer was banned and + /// false otherwise. + pub fn remove_banned_peer(&mut self, ip_addresses: impl Iterator) { + self.banned_peers = self.banned_peers.saturating_sub(1); + for address in ip_addresses { + if let Some(count) = self.banned_peers_per_ip.get_mut(&address) { + *count = count.saturating_sub(1); + } + } + } + + pub fn add_banned_peer(&mut self, ip_addresses: impl Iterator) { + self.banned_peers = self.banned_peers.saturating_add(1); + for address in ip_addresses { + *self.banned_peers_per_ip.entry(address).or_insert(0) += 1; + } + } + + pub fn banned_peers(&self) -> usize { + self.banned_peers + } + + pub fn banned_ips(&self) -> HashSet { + self.banned_peers_per_ip + .iter() + .filter(|(_ip, count)| **count > BANNED_PEERS_PER_IP_THRESHOLD) + .map(|(ip, _count)| *ip) + .collect() + } + + /// An IP is considered banned if more than BANNED_PEERS_PER_IP_THRESHOLD banned peers + /// exist with this IP + pub fn ip_is_banned(&self, ip: &IpAddr) -> bool { + self.banned_peers_per_ip + .get(ip) + .map_or(false, |count| *count > BANNED_PEERS_PER_IP_THRESHOLD) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use libp2p::core::Multiaddr; + use std::net::{Ipv4Addr, Ipv6Addr}; + + fn add_score(db: &mut PeerDB, peer_id: &PeerId, score: f64) { + if let Some(info) = db.peer_info_mut(peer_id) { + info.add_to_score(score); + } + } + + fn reset_score(db: &mut PeerDB, peer_id: &PeerId) { + if let Some(info) = db.peer_info_mut(peer_id) { + info.reset_score(); + } + } + + fn get_db() -> PeerDB { + PeerDB::new(vec![]) + } + + #[test] + #[allow(clippy::float_cmp)] + fn test_peer_connected_successfully() { + let mut pdb = get_db(); + let random_peer = PeerId::random(); + + let (n_in, n_out) = (10, 20); + for _ in 0..n_in { + pdb.connect_ingoing(&random_peer, "/ip4/0.0.0.0".parse().unwrap(), None); + } + for _ in 0..n_out { + pdb.connect_outgoing(&random_peer, "/ip4/0.0.0.0".parse().unwrap(), None); + } + + // the peer is known + let peer_info = pdb.peer_info(&random_peer); + assert!(peer_info.is_some()); + // this is the only peer + assert_eq!(pdb.peers().count(), 1); + // the peer has the default reputation + assert_eq!(pdb.score(&random_peer), Score::default().score()); + // it should be connected, and therefore not counted as disconnected + assert_eq!(pdb.disconnected_peers, 0); + assert!(peer_info.unwrap().is_connected()); + assert_eq!(peer_info.unwrap().connections(), (n_in, n_out)); + } + + #[test] + fn test_outbound_only_peers_counted_correctly() { + let mut pdb = get_db(); + let p0 = PeerId::random(); + let p1 = PeerId::random(); + let p2 = PeerId::random(); + // Create peer with no connections. + let _p3 = PeerId::random(); + + pdb.connect_ingoing(&p0, "/ip4/0.0.0.0".parse().unwrap(), None); + pdb.connect_ingoing(&p1, "/ip4/0.0.0.0".parse().unwrap(), None); + pdb.connect_outgoing(&p1, "/ip4/0.0.0.0".parse().unwrap(), None); + pdb.connect_outgoing(&p2, "/ip4/0.0.0.0".parse().unwrap(), None); + + // We should only have one outbound-only peer (p2). + // Peers that are inbound-only, have both types of connections, or no connections should not be counted. + assert_eq!(pdb.connected_outbound_only_peers().count(), 1); + } + + #[test] + fn test_disconnected_removed_in_correct_order() { + let mut pdb = get_db(); + + use std::collections::BTreeMap; + let mut peer_list = BTreeMap::new(); + for id in 0..MAX_DC_PEERS + 1 { + let new_peer = PeerId::random(); + pdb.connect_ingoing(&new_peer, "/ip4/0.0.0.0".parse().unwrap(), None); + peer_list.insert(id, new_peer); + } + assert_eq!(pdb.disconnected_peers, 0); + + for (_, p) in peer_list.iter() { + pdb.inject_disconnect(p); + // Allow the timing to update correctly + } + assert_eq!(pdb.disconnected_peers, MAX_DC_PEERS); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + + // Only the oldest peer should have been removed + for (id, peer_id) in peer_list.iter().rev().take(MAX_DC_PEERS) { + println!("Testing id {}", id); + assert!( + pdb.peer_info(peer_id).is_some(), + "Latest peer should not be pruned" + ); + } + + assert!( + pdb.peer_info(peer_list.iter().next().unwrap().1).is_none(), + "First peer should be removed" + ); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + } + + #[test] + fn new_connection_should_remain() { + let mut pdb = get_db(); + + use std::collections::BTreeMap; + let mut peer_list = BTreeMap::new(); + for id in 0..MAX_DC_PEERS + 20 { + let new_peer = PeerId::random(); + pdb.connect_ingoing(&new_peer, "/ip4/0.0.0.0".parse().unwrap(), None); + peer_list.insert(id, new_peer); + } + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + for (_, p) in peer_list.iter() { + pdb.inject_disconnect(p); + } + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + println!("{}", pdb.disconnected_peers); + + peer_list.clear(); + for id in 0..MAX_DC_PEERS + 20 { + let new_peer = PeerId::random(); + pdb.connect_ingoing(&new_peer, "/ip4/0.0.0.0".parse().unwrap(), None); + peer_list.insert(id, new_peer); + } + + let new_peer = PeerId::random(); + // New peer gets its min_ttl updated because it exists on a subnet + let min_ttl = Instant::now() + std::time::Duration::from_secs(12); + + pdb.update_min_ttl(&new_peer, min_ttl); + // Peer then gets dialed + pdb.dialing_peer(&new_peer, None); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + // Dialing fails, remove the peer + pdb.inject_disconnect(&new_peer); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + + assert!( + pdb.peer_info(&new_peer).is_some(), + "Peer should exist as disconnected" + ); + + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + println!("{}", pdb.disconnected_peers); + } + + #[test] + fn test_disconnected_are_bounded() { + let mut pdb = get_db(); + + for _ in 0..MAX_DC_PEERS + 1 { + let p = PeerId::random(); + pdb.connect_ingoing(&p, "/ip4/0.0.0.0".parse().unwrap(), None); + } + assert_eq!(pdb.disconnected_peers, 0); + + for p in pdb.connected_peer_ids().cloned().collect::>() { + pdb.inject_disconnect(&p); + } + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + + assert_eq!(pdb.disconnected_peers, MAX_DC_PEERS); + } + + #[test] + fn test_banned_are_bounded() { + let mut pdb = get_db(); + + for _ in 0..MAX_BANNED_PEERS + 1 { + let p = PeerId::random(); + pdb.connect_ingoing(&p, "/ip4/0.0.0.0".parse().unwrap(), None); + } + assert_eq!(pdb.banned_peers_count.banned_peers(), 0); + + for p in pdb.connected_peer_ids().cloned().collect::>() { + let _ = pdb.report_peer(&p, PeerAction::Fatal, ReportSource::PeerManager, ""); + pdb.inject_disconnect(&p); + } + + assert_eq!(pdb.banned_peers_count.banned_peers(), MAX_BANNED_PEERS); + } + + #[test] + fn test_best_peers() { + let mut pdb = get_db(); + + let p0 = PeerId::random(); + let p1 = PeerId::random(); + let p2 = PeerId::random(); + pdb.connect_ingoing(&p0, "/ip4/0.0.0.0".parse().unwrap(), None); + pdb.connect_ingoing(&p1, "/ip4/0.0.0.0".parse().unwrap(), None); + pdb.connect_ingoing(&p2, "/ip4/0.0.0.0".parse().unwrap(), None); + add_score(&mut pdb, &p0, 70.0); + add_score(&mut pdb, &p1, 100.0); + add_score(&mut pdb, &p2, 50.0); + + let best_peers: Vec<&PeerId> = pdb + .best_peers_by_status(PeerInfo::is_connected) + .iter() + .map(|p| p.0) + .collect(); + assert_eq!(vec![&p1, &p0, &p2], best_peers); + } + + #[test] + fn test_the_best_peer() { + let mut pdb = get_db(); + + let p0 = PeerId::random(); + let p1 = PeerId::random(); + let p2 = PeerId::random(); + pdb.connect_ingoing(&p0, "/ip4/0.0.0.0".parse().unwrap(), None); + pdb.connect_ingoing(&p1, "/ip4/0.0.0.0".parse().unwrap(), None); + pdb.connect_ingoing(&p2, "/ip4/0.0.0.0".parse().unwrap(), None); + add_score(&mut pdb, &p0, 70.0); + add_score(&mut pdb, &p1, 100.0); + add_score(&mut pdb, &p2, 50.0); + + let the_best = pdb.best_by_status(PeerInfo::is_connected); + assert!(the_best.is_some()); + // Consistency check + let best_peers = pdb.best_peers_by_status(PeerInfo::is_connected); + assert_eq!(the_best.unwrap(), best_peers.get(0).unwrap().0); + } + + #[test] + fn test_disconnected_consistency() { + let mut pdb = get_db(); + + let random_peer = PeerId::random(); + + pdb.connect_ingoing(&random_peer, "/ip4/0.0.0.0".parse().unwrap(), None); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + + pdb.connect_ingoing(&random_peer, "/ip4/0.0.0.0".parse().unwrap(), None); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + pdb.inject_disconnect(&random_peer); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + + pdb.connect_outgoing(&random_peer, "/ip4/0.0.0.0".parse().unwrap(), None); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + pdb.inject_disconnect(&random_peer); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + + let _ = pdb.report_peer( + &random_peer, + PeerAction::Fatal, + ReportSource::PeerManager, + "", + ); + pdb.inject_disconnect(&random_peer); + let _ = pdb.report_peer( + &random_peer, + PeerAction::Fatal, + ReportSource::PeerManager, + "", + ); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + pdb.inject_disconnect(&random_peer); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + + pdb.inject_disconnect(&random_peer); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + pdb.inject_disconnect(&random_peer); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + } + + #[test] + fn test_disconnected_ban_consistency() { + let mut pdb = get_db(); + let mut multiaddr = Multiaddr::empty(); + multiaddr.push(Protocol::Tcp(9000)); + multiaddr.push(Protocol::Ip4("0.0.0.0".parse().unwrap())); + + let random_peer = PeerId::random(); + let random_peer1 = PeerId::random(); + let random_peer2 = PeerId::random(); + let random_peer3 = PeerId::random(); + println!("{}", random_peer); + println!("{}", random_peer1); + println!("{}", random_peer2); + println!("{}", random_peer3); + + // All 4 peers connected on the same IP + pdb.connect_ingoing(&random_peer, multiaddr.clone(), None); + pdb.connect_ingoing(&random_peer1, multiaddr.clone(), None); + pdb.connect_ingoing(&random_peer2, multiaddr.clone(), None); + pdb.connect_ingoing(&random_peer3, multiaddr.clone(), None); + + // Should be no disconnected or banned peers + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + assert_eq!( + pdb.banned_peers_count.banned_peers(), + pdb.banned_peers_by_score().count() + ); + + // Should be no disconnected peers + println!( + "1:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + // Disconnect one peer + pdb.inject_disconnect(&random_peer1); + // Should be 1 disconnected peer + println!( + "2:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + // Disconnect and ban peer 2 + let _ = pdb.report_peer( + &random_peer2, + PeerAction::Fatal, + ReportSource::PeerManager, + "", + ); + // Should be 1 disconnected peer and one peer in the process of being disconnected + println!( + "3:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + // The peer is now disconnected and banned + pdb.inject_disconnect(&random_peer2); + // There should be 2 disconnected peers. + println!( + "4:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + // Now that the peer is disconnected, register the ban. + let _ = pdb.report_peer( + &random_peer2, + PeerAction::Fatal, + ReportSource::PeerManager, + "", + ); + // There should be 1 disconnected peer and one banned peer. + println!( + "5:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + // Re-connect peer3, should have no effect + pdb.connect_ingoing(&random_peer3, multiaddr.clone(), None); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + assert_eq!( + pdb.banned_peers_count.banned_peers(), + pdb.banned_peers().count() + ); + // Now ban peer 1. + let _ = pdb.report_peer( + &random_peer1, + PeerAction::Fatal, + ReportSource::PeerManager, + "", + ); + // There should be no disconnected peers and 2 banned peers + println!( + "6:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + // This should have no effect + pdb.inject_disconnect(&random_peer1); + // Should still be no disconnected peers and 2 banned peers + println!( + "7:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + // Same thing here. + let _ = pdb.report_peer( + &random_peer1, + PeerAction::Fatal, + ReportSource::PeerManager, + "", + ); + println!( + "8:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + println!( + "{}, {:?}, {}", + pdb.disconnected_peers, + pdb.disconnected_peers().collect::>(), + pdb.banned_peers_count.banned_peers + ); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + assert_eq!( + pdb.banned_peers_count.banned_peers(), + pdb.banned_peers().count() + ); + + // Try and reconnect banned peer 2. + pdb.connect_outgoing(&random_peer2, multiaddr.clone(), None); + pdb.peer_info_mut(&random_peer2) + .unwrap() + .add_to_score(100.0); + // This removes the banned peer and should give us 0 disconnected, 1 banned peer + // (peer1) + println!( + "9:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + assert_eq!( + pdb.banned_peers_count.banned_peers(), + pdb.banned_peers().count() + ); + + // Ban peer 3 + let _ = pdb.report_peer( + &random_peer3, + PeerAction::Fatal, + ReportSource::PeerManager, + "", + ); + pdb.inject_disconnect(&random_peer3); + + // This should add a new banned peer, there should be 0 disconnected and 2 banned + // peers (peer1 and peer3) + println!( + "10:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + assert_eq!( + pdb.banned_peers_count.banned_peers(), + pdb.banned_peers().count() + ); + + // Ban peer 3 + let _ = pdb.report_peer( + &random_peer3, + PeerAction::Fatal, + ReportSource::PeerManager, + "", + ); + pdb.inject_disconnect(&random_peer3); + + // Should still have 2 banned peers + println!( + "11:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + + // Unban peer 1 + pdb.connect_ingoing(&random_peer1, multiaddr.clone(), None); + pdb.peer_info_mut(&random_peer1) + .unwrap() + .add_to_score(100.0); + // Should have 1 banned peer (peer 3) + println!( + "12:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + + // Disconnect peer 2 + pdb.inject_disconnect(&random_peer2); + + // Should have 1 disconnect (peer 2) and one banned (peer 3) + println!( + "12:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + + // Ban peer 3 + let _ = pdb.report_peer( + &random_peer3, + PeerAction::Fatal, + ReportSource::PeerManager, + "", + ); + pdb.inject_disconnect(&random_peer3); + + // Should have 1 disconnect (peer 2) and one banned (peer 3) + println!( + "13:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + + // Add peer 0 + pdb.connect_ingoing(&random_peer, multiaddr, None); + pdb.peer_info_mut(&random_peer).unwrap().add_to_score(100.0); + + // Should have 1 disconnect (peer 2) and one banned (peer 3) + println!( + "14:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + assert_eq!( + pdb.banned_peers_count.banned_peers(), + pdb.banned_peers().count() + ); + + // Disconnect peer 0 + pdb.inject_disconnect(&random_peer); + // Should have 2 disconnect (peer 0, peer 2) and one banned (peer 3) + println!( + "15:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + assert_eq!( + pdb.banned_peers_count.banned_peers(), + pdb.banned_peers().count() + ); + + // Disconnect peer 0 + pdb.inject_disconnect(&random_peer); + // Should have 2 disconnect (peer 0, peer 2) and one banned (peer 3) + println!( + "16:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + assert_eq!( + pdb.banned_peers_count.banned_peers(), + pdb.banned_peers().count() + ); + + // Ban peer 0 + let _ = pdb.report_peer( + &random_peer, + PeerAction::Fatal, + ReportSource::PeerManager, + "", + ); + pdb.inject_disconnect(&random_peer); + + // Should have 1 disconnect ( peer 2) and two banned (peer0, peer 3) + println!( + "17:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + } + + fn connect_peer_with_ips(pdb: &mut PeerDB, ips: Vec) -> PeerId { + let p = PeerId::random(); + + for ip in ips { + let mut addr = Multiaddr::empty(); + addr.push(Protocol::from(ip)); + addr.push(Protocol::Tcp(9000)); + pdb.connect_ingoing(&p, addr, None); + } + p + } + + #[test] + fn test_ban_address() { + let mut pdb = get_db(); + + let ip1 = Ipv4Addr::new(1, 2, 3, 4).into(); + let ip2 = Ipv6Addr::new(1, 2, 3, 4, 5, 6, 7, 8).into(); + let ip3 = Ipv4Addr::new(1, 2, 3, 5).into(); + let ip4 = Ipv6Addr::new(1, 2, 3, 4, 5, 6, 7, 9).into(); + let ip5 = Ipv4Addr::new(2, 2, 3, 4).into(); + + let mut peers = Vec::new(); + for i in 0..BANNED_PEERS_PER_IP_THRESHOLD + 2 { + peers.push(connect_peer_with_ips( + &mut pdb, + if i == 0 { + vec![ip1, ip2] + } else { + vec![ip1, ip2, ip3, ip4] + }, + )); + } + + let p1 = connect_peer_with_ips(&mut pdb, vec![ip1]); + let p2 = connect_peer_with_ips(&mut pdb, vec![ip2, ip5]); + let p3 = connect_peer_with_ips(&mut pdb, vec![ip3, ip5]); + let p4 = connect_peer_with_ips(&mut pdb, vec![ip5, ip4]); + let p5 = connect_peer_with_ips(&mut pdb, vec![ip5]); + + for p in &peers[..BANNED_PEERS_PER_IP_THRESHOLD + 1] { + let _ = pdb.report_peer(p, PeerAction::Fatal, ReportSource::PeerManager, ""); + pdb.inject_disconnect(p); + } + + //check that ip1 and ip2 are banned but ip3-5 not + assert!(pdb.ban_status(&p1).is_banned()); + assert!(pdb.ban_status(&p2).is_banned()); + assert!(!pdb.ban_status(&p3).is_banned()); + assert!(!pdb.ban_status(&p4).is_banned()); + assert!(!pdb.ban_status(&p5).is_banned()); + + //ban also the last peer in peers + let _ = pdb.report_peer( + &peers[BANNED_PEERS_PER_IP_THRESHOLD + 1], + PeerAction::Fatal, + ReportSource::PeerManager, + "", + ); + pdb.inject_disconnect(&peers[BANNED_PEERS_PER_IP_THRESHOLD + 1]); + + //check that ip1-ip4 are banned but ip5 not + assert!(pdb.ban_status(&p1).is_banned()); + assert!(pdb.ban_status(&p2).is_banned()); + assert!(pdb.ban_status(&p3).is_banned()); + assert!(pdb.ban_status(&p4).is_banned()); + assert!(!pdb.ban_status(&p5).is_banned()); + + //peers[0] gets unbanned + reset_score(&mut pdb, &peers[0]); + pdb.update_connection_state(&peers[0], NewConnectionState::Unbanned); + let _ = pdb.shrink_to_fit(); + + //nothing changed + assert!(pdb.ban_status(&p1).is_banned()); + assert!(pdb.ban_status(&p2).is_banned()); + assert!(pdb.ban_status(&p3).is_banned()); + assert!(pdb.ban_status(&p4).is_banned()); + assert!(!pdb.ban_status(&p5).is_banned()); + + //peers[1] gets unbanned + reset_score(&mut pdb, &peers[1]); + pdb.update_connection_state(&peers[1], NewConnectionState::Unbanned); + let _ = pdb.shrink_to_fit(); + + //all ips are unbanned + assert!(!pdb.ban_status(&p1).is_banned()); + assert!(!pdb.ban_status(&p2).is_banned()); + assert!(!pdb.ban_status(&p3).is_banned()); + assert!(!pdb.ban_status(&p4).is_banned()); + assert!(!pdb.ban_status(&p5).is_banned()); + } + + #[test] + fn test_banned_ip_consistent_after_changing_ips() { + let mut pdb = get_db(); + + let ip1: IpAddr = Ipv4Addr::new(1, 2, 3, 4).into(); + let ip2: IpAddr = Ipv6Addr::new(1, 2, 3, 4, 5, 6, 7, 8).into(); + + let mut peers = Vec::new(); + for _ in 0..BANNED_PEERS_PER_IP_THRESHOLD + 1 { + peers.push(connect_peer_with_ips(&mut pdb, vec![ip1])); + } + + let p1 = connect_peer_with_ips(&mut pdb, vec![ip1]); + let p2 = connect_peer_with_ips(&mut pdb, vec![ip2]); + + // ban all peers + for p in &peers { + let _ = pdb.report_peer(p, PeerAction::Fatal, ReportSource::PeerManager, ""); + pdb.inject_disconnect(p); + } + + // check ip is banned + assert!(pdb.ban_status(&p1).is_banned()); + assert!(!pdb.ban_status(&p2).is_banned()); + + // unban a peer + reset_score(&mut pdb, &peers[0]); + pdb.update_connection_state(&peers[0], NewConnectionState::Unbanned); + let _ = pdb.shrink_to_fit(); + + // check not banned anymore + assert!(!pdb.ban_status(&p1).is_banned()); + assert!(!pdb.ban_status(&p2).is_banned()); + + // unban all peers + for p in &peers { + reset_score(&mut pdb, p); + pdb.update_connection_state(p, NewConnectionState::Unbanned); + let _ = pdb.shrink_to_fit(); + } + + // add ip2 to all peers and ban them. + let mut socker_addr = Multiaddr::from(ip2); + socker_addr.push(Protocol::Tcp(8080)); + for p in &peers { + pdb.connect_ingoing(p, socker_addr.clone(), None); + let _ = pdb.report_peer(p, PeerAction::Fatal, ReportSource::PeerManager, ""); + pdb.inject_disconnect(p); + } + + // both IP's are now banned + assert!(pdb.ban_status(&p1).is_banned()); + assert!(pdb.ban_status(&p2).is_banned()); + + // unban all peers + for p in &peers { + reset_score(&mut pdb, p); + pdb.update_connection_state(p, NewConnectionState::Unbanned); + let _ = pdb.shrink_to_fit(); + } + + // reban every peer except one + for p in &peers[1..] { + let _ = pdb.report_peer(p, PeerAction::Fatal, ReportSource::PeerManager, ""); + pdb.inject_disconnect(p); + } + + // nothing is banned + assert!(!pdb.ban_status(&p1).is_banned()); + assert!(!pdb.ban_status(&p2).is_banned()); + + // reban last peer + let _ = pdb.report_peer(&peers[0], PeerAction::Fatal, ReportSource::PeerManager, ""); + pdb.inject_disconnect(&peers[0]); + + //Ip's are banned again + assert!(pdb.ban_status(&p1).is_banned()); + assert!(pdb.ban_status(&p2).is_banned()); + } + + #[test] + #[allow(clippy::float_cmp)] + fn test_trusted_peers_score() { + let trusted_peer = PeerId::random(); + let mut pdb: PeerDB = PeerDB::new(vec![trusted_peer]); + + pdb.connect_ingoing(&trusted_peer, "/ip4/0.0.0.0".parse().unwrap(), None); + + // Check trusted status and score + assert!(pdb.peer_info(&trusted_peer).unwrap().is_trusted()); + assert_eq!( + pdb.peer_info(&trusted_peer).unwrap().score().score(), + Score::max_score().score() + ); + + // Adding/Subtracting score should have no effect on a trusted peer + add_score(&mut pdb, &trusted_peer, -50.0); + + assert_eq!( + pdb.peer_info(&trusted_peer).unwrap().score().score(), + Score::max_score().score() + ); + } +} diff --git a/node/network/src/peer_manager/peerdb/client.rs b/node/network/src/peer_manager/peerdb/client.rs new file mode 100644 index 0000000..aaacbcc --- /dev/null +++ b/node/network/src/peer_manager/peerdb/client.rs @@ -0,0 +1,102 @@ +use libp2p::identify::IdentifyInfo; +use serde::Serialize; +use strum::{AsRefStr, EnumIter, IntoStaticStr}; + +/// Various client and protocol information related to a node. +#[derive(Clone, Debug, Serialize)] +pub struct Client { + /// The client's name. + pub kind: ClientKind, + /// The client's version. + pub version: String, + /// The OS version of the client. + pub os_version: String, + /// The libp2p protocol version. + pub protocol_version: String, + /// Identify agent string + pub agent_string: Option, +} + +#[derive(Clone, Copy, Debug, Serialize, PartialEq, Eq, AsRefStr, IntoStaticStr, EnumIter)] +pub enum ClientKind { + /// An Zgs node. + Zgs, + /// An unknown client. + Unknown, +} + +impl Default for Client { + fn default() -> Self { + Client { + kind: ClientKind::Unknown, + version: "unknown".into(), + os_version: "unknown".into(), + protocol_version: "unknown".into(), + agent_string: None, + } + } +} + +impl Client { + /// Builds a `Client` from `IdentifyInfo`. + pub fn from_identify_info(info: &IdentifyInfo) -> Self { + let (kind, version, os_version) = client_from_agent_version(&info.agent_version); + + Client { + kind, + version, + os_version, + protocol_version: info.protocol_version.clone(), + agent_string: Some(info.agent_version.clone()), + } + } +} + +impl std::fmt::Display for Client { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.kind { + ClientKind::Zgs => write!( + f, + "zgs: version: {}, os_version: {}", + self.version, self.os_version + ), + ClientKind::Unknown => { + if let Some(agent_string) = &self.agent_string { + write!(f, "Unknown: {}", agent_string) + } else { + write!(f, "Unknown") + } + } + } + } +} + +impl std::fmt::Display for ClientKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_ref()) + } +} + +// helper function to identify clients from their agent_version. Returns the client +// kind and it's associated version and the OS kind. +fn client_from_agent_version(agent_version: &str) -> (ClientKind, String, String) { + let mut agent_split = agent_version.split('/'); + match agent_split.next() { + Some("zgs") => { + let kind = ClientKind::Zgs; + let mut version = String::from("unknown"); + let mut os_version = version.clone(); + if let Some(agent_version) = agent_split.next() { + version = agent_version.into(); + if let Some(agent_os_version) = agent_split.next() { + os_version = agent_os_version.into(); + } + } + (kind, version, os_version) + } + _ => { + let unknown = String::from("unknown"); + (ClientKind::Unknown, unknown.clone(), unknown) + } + } +} diff --git a/node/network/src/peer_manager/peerdb/peer_info.rs b/node/network/src/peer_manager/peerdb/peer_info.rs new file mode 100644 index 0000000..ae75458 --- /dev/null +++ b/node/network/src/peer_manager/peerdb/peer_info.rs @@ -0,0 +1,424 @@ +use super::client::Client; +use super::score::{PeerAction, Score, ScoreState}; +use super::sync_status::SyncStatus; +use crate::Multiaddr; +use discv5::Enr; +use serde::{ + ser::{SerializeStruct, Serializer}, + Serialize, +}; +use std::collections::HashSet; +use std::net::{IpAddr, SocketAddr}; +use std::time::Instant; +use strum::AsRefStr; +use PeerConnectionStatus::*; + +/// Information about a given connected peer. +#[derive(Clone, Debug, Serialize)] +pub struct PeerInfo { + /// The peers reputation + score: Score, + /// Client managing this peer + client: Client, + /// Connection status of this peer + connection_status: PeerConnectionStatus, + /// The known listening addresses of this peer. This is given by identify and can be arbitrary + /// (including local IPs). + listening_addresses: Vec, + /// This is addresses we have physically seen and this is what we use for banning/un-banning + /// peers. + seen_addresses: HashSet, + /// The current syncing state of the peer. The state may be determined after it's initial + /// connection. + sync_status: SyncStatus, + /// The time we would like to retain this peer. After this time, the peer is no longer + /// necessary. + #[serde(skip)] + min_ttl: Option, + /// Is the peer a trusted peer. + is_trusted: bool, + /// Direction of the first connection of the last (or current) connected session with this peer. + /// None if this peer was never connected. + connection_direction: Option, + /// The enr of the peer, if known. + enr: Option, +} + +impl Default for PeerInfo { + fn default() -> PeerInfo { + PeerInfo { + score: Score::default(), + client: Client::default(), + connection_status: Default::default(), + listening_addresses: Vec::new(), + seen_addresses: HashSet::new(), + sync_status: SyncStatus::Unknown, + min_ttl: None, + is_trusted: false, + connection_direction: None, + enr: None, + } + } +} + +impl PeerInfo { + /// Return a PeerInfo struct for a trusted peer. + pub fn trusted_peer_info() -> Self { + PeerInfo { + score: Score::max_score(), + is_trusted: true, + ..Default::default() + } + } + + /// Obtains the client of the peer. + pub fn client(&self) -> &Client { + &self.client + } + + /// Returns the listening addresses of the Peer. + pub fn listening_addresses(&self) -> &Vec { + &self.listening_addresses + } + + /// Returns the connection direction for the peer. + pub fn connection_direction(&self) -> Option<&ConnectionDirection> { + self.connection_direction.as_ref() + } + + /// Returns the sync status of the peer. + pub fn sync_status(&self) -> &SyncStatus { + &self.sync_status + } + + /// Returns whether the peer is a trusted peer or not. + pub fn is_trusted(&self) -> bool { + self.is_trusted + } + + /// The time a peer is expected to be useful until for an attached validator. If this is set to + /// None, the peer is not required for any upcoming duty. + pub fn min_ttl(&self) -> Option<&Instant> { + self.min_ttl.as_ref() + } + + /// The ENR of the peer if it is known. + pub fn enr(&self) -> Option<&Enr> { + self.enr.as_ref() + } + + /// Returns the seen addresses of the peer. + pub fn seen_addresses(&self) -> impl Iterator + '_ { + self.seen_addresses.iter() + } + + /// Returns a list of seen IP addresses for the peer. + pub fn seen_ip_addresses(&self) -> impl Iterator + '_ { + self.seen_addresses + .iter() + .map(|socket_addr| socket_addr.ip()) + } + + /// Returns the connection status of the peer. + pub fn connection_status(&self) -> &PeerConnectionStatus { + &self.connection_status + } + + /// Reports if this peer has some future validator duty in which case it is valuable to keep it. + pub fn has_future_duty(&self) -> bool { + self.min_ttl.map_or(false, |i| i >= Instant::now()) + } + + /// Returns score of the peer. + pub fn score(&self) -> &Score { + &self.score + } + + /// Returns the state of the peer based on the score. + pub(crate) fn score_state(&self) -> ScoreState { + self.score.state() + } + + /// Returns true if the gossipsub score is sufficient. + pub fn is_good_gossipsub_peer(&self) -> bool { + self.score.is_good_gossipsub_peer() + } + + /* Peer connection status API */ + + /// Checks if the status is connected. + pub fn is_connected(&self) -> bool { + matches!( + self.connection_status, + PeerConnectionStatus::Connected { .. } + ) + } + + /// Checks if the status is connected. + pub fn is_dialing(&self) -> bool { + matches!(self.connection_status, PeerConnectionStatus::Dialing { .. }) + } + + /// The peer is either connected or in the process of being dialed. + pub fn is_connected_or_dialing(&self) -> bool { + self.is_connected() || self.is_dialing() + } + + /// Checks if the connection status is banned. This can lag behind the score state + /// temporarily. + pub fn is_banned(&self) -> bool { + matches!(self.connection_status, PeerConnectionStatus::Banned { .. }) + } + + /// Checks if the peer's score is banned. + pub fn score_is_banned(&self) -> bool { + matches!(self.score.state(), ScoreState::Banned) + } + + /// Checks if the status is disconnected. + pub fn is_disconnected(&self) -> bool { + matches!(self.connection_status, Disconnected { .. }) + } + + /// Checks if the peer is outbound-only + pub fn is_outbound_only(&self) -> bool { + matches!(self.connection_status, Connected {n_in, n_out} if n_in == 0 && n_out > 0) + } + + /// Returns the number of connections with this peer. + pub fn connections(&self) -> (u8, u8) { + match self.connection_status { + Connected { n_in, n_out } => (n_in, n_out), + _ => (0, 0), + } + } + + /* Mutable Functions */ + + /// Updates the sync status. Returns true if the status was changed. + // VISIBILITY: Both the peer manager the network sync is able to update the sync state of a peer + pub fn update_sync_status(&mut self, sync_status: SyncStatus) -> bool { + self.sync_status.update(sync_status) + } + + /// Sets the client of the peer. + // VISIBILITY: The peer manager is able to set the client + pub(in crate::peer_manager) fn set_client(&mut self, client: Client) { + self.client = client + } + + /// Replaces the current listening addresses with those specified, returning the current + /// listening addresses. + // VISIBILITY: The peer manager is able to set the listening addresses + pub(in crate::peer_manager) fn set_listening_addresses( + &mut self, + listening_addresses: Vec, + ) -> Vec { + std::mem::replace(&mut self.listening_addresses, listening_addresses) + } + + /// Sets the connection status of the peer. + pub(super) fn set_connection_status(&mut self, connection_status: PeerConnectionStatus) { + self.connection_status = connection_status + } + + /// Sets the ENR of the peer if one is known. + pub(super) fn set_enr(&mut self, enr: Enr) { + self.enr = Some(enr) + } + + /// Sets the time that the peer is expected to be needed until for an attached validator duty. + pub(super) fn set_min_ttl(&mut self, min_ttl: Instant) { + self.min_ttl = Some(min_ttl) + } + + /// Applies decay rates to a non-trusted peer's score. + pub(super) fn score_update(&mut self) { + if !self.is_trusted { + self.score.update() + } + } + + /// Apply peer action to a non-trusted peer's score. + // VISIBILITY: The peer manager is able to modify the score of a peer. + pub(in crate::peer_manager) fn apply_peer_action_to_score(&mut self, peer_action: PeerAction) { + if !self.is_trusted { + self.score.apply_peer_action(peer_action) + } + } + + /// Updates the gossipsub score with a new score. Optionally ignore the gossipsub score. + pub(super) fn update_gossipsub_score(&mut self, new_score: f64, ignore: bool) { + self.score.update_gossipsub_score(new_score, ignore); + } + + #[cfg(test)] + /// Resets the peers score. + pub fn reset_score(&mut self) { + self.score.test_reset(); + } + + /// Modifies the status to Dialing + /// Returns an error if the current state is unexpected. + pub(super) fn set_dialing_peer(&mut self) -> Result<(), &'static str> { + match &mut self.connection_status { + Connected { .. } => return Err("Dialing connected peer"), + Dialing { .. } => return Err("Dialing an already dialing peer"), + Disconnecting { .. } => return Err("Dialing a disconnecting peer"), + Disconnected { .. } | Banned { .. } | Unknown => {} + } + self.connection_status = Dialing { + since: Instant::now(), + }; + Ok(()) + } + + /// Modifies the status to Connected and increases the number of ingoing + /// connections by one + pub(super) fn connect_ingoing(&mut self, seen_address: Option) { + match &mut self.connection_status { + Connected { n_in, .. } => *n_in += 1, + Disconnected { .. } + | Banned { .. } + | Dialing { .. } + | Disconnecting { .. } + | Unknown => { + self.connection_status = Connected { n_in: 1, n_out: 0 }; + self.connection_direction = Some(ConnectionDirection::Incoming); + } + } + + if let Some(socket_addr) = seen_address { + self.seen_addresses.insert(socket_addr); + } + } + + /// Modifies the status to Connected and increases the number of outgoing + /// connections by one + pub(super) fn connect_outgoing(&mut self, seen_address: Option) { + match &mut self.connection_status { + Connected { n_out, .. } => *n_out += 1, + Disconnected { .. } + | Banned { .. } + | Dialing { .. } + | Disconnecting { .. } + | Unknown => { + self.connection_status = Connected { n_in: 0, n_out: 1 }; + self.connection_direction = Some(ConnectionDirection::Outgoing); + } + } + if let Some(ip_addr) = seen_address { + self.seen_addresses.insert(ip_addr); + } + } + + #[cfg(test)] + /// Add an f64 to a non-trusted peer's score abiding by the limits. + pub fn add_to_score(&mut self, score: f64) { + if !self.is_trusted { + self.score.test_add(score) + } + } + + #[cfg(test)] + pub fn set_gossipsub_score(&mut self, score: f64) { + self.score.set_gossipsub_score(score); + } +} + +/// Connection Direction of connection. +#[derive(Debug, Clone, Serialize, AsRefStr)] +#[strum(serialize_all = "snake_case")] +pub enum ConnectionDirection { + /// The connection was established by a peer dialing us. + Incoming, + /// The connection was established by us dialing a peer. + Outgoing, +} + +/// Connection Status of the peer. +#[derive(Debug, Clone, Default)] +pub enum PeerConnectionStatus { + /// The peer is connected. + Connected { + /// number of ingoing connections. + n_in: u8, + /// number of outgoing connections. + n_out: u8, + }, + /// The peer is being disconnected. + Disconnecting { + // After the disconnection the peer will be considered banned. + to_ban: bool, + }, + /// The peer has disconnected. + Disconnected { + /// last time the peer was connected or discovered. + since: Instant, + }, + /// The peer has been banned and is disconnected. + Banned { + /// moment when the peer was banned. + since: Instant, + }, + /// We are currently dialing this peer. + Dialing { + /// time since we last communicated with the peer. + since: Instant, + }, + /// The connection status has not been specified. + #[default] + Unknown, +} + +/// Serialization for http requests. +impl Serialize for PeerConnectionStatus { + fn serialize(&self, serializer: S) -> Result { + let mut s = serializer.serialize_struct("connection_status", 6)?; + match self { + Connected { n_in, n_out } => { + s.serialize_field("status", "connected")?; + s.serialize_field("connections_in", n_in)?; + s.serialize_field("connections_out", n_out)?; + s.serialize_field("last_seen", &0)?; + s.end() + } + Disconnecting { .. } => { + s.serialize_field("status", "disconnecting")?; + s.serialize_field("connections_in", &0)?; + s.serialize_field("connections_out", &0)?; + s.serialize_field("last_seen", &0)?; + s.end() + } + Disconnected { since } => { + s.serialize_field("status", "disconnected")?; + s.serialize_field("connections_in", &0)?; + s.serialize_field("connections_out", &0)?; + s.serialize_field("last_seen", &since.elapsed().as_secs())?; + s.serialize_field("banned_ips", &Vec::::new())?; + s.end() + } + Banned { since } => { + s.serialize_field("status", "banned")?; + s.serialize_field("connections_in", &0)?; + s.serialize_field("connections_out", &0)?; + s.serialize_field("last_seen", &since.elapsed().as_secs())?; + s.end() + } + Dialing { since } => { + s.serialize_field("status", "dialing")?; + s.serialize_field("connections_in", &0)?; + s.serialize_field("connections_out", &0)?; + s.serialize_field("last_seen", &since.elapsed().as_secs())?; + s.end() + } + Unknown => { + s.serialize_field("status", "unknown")?; + s.serialize_field("connections_in", &0)?; + s.serialize_field("connections_out", &0)?; + s.serialize_field("last_seen", &0)?; + s.end() + } + } + } +} diff --git a/node/network/src/peer_manager/peerdb/score.rs b/node/network/src/peer_manager/peerdb/score.rs new file mode 100644 index 0000000..3b67c44 --- /dev/null +++ b/node/network/src/peer_manager/peerdb/score.rs @@ -0,0 +1,425 @@ +//! This contains the scoring logic for peers. +//! +//! A peer's score is a rational number in the range [-100, 100]. +//! +//! As the logic develops this documentation will advance. +//! +//! The scoring algorithms are currently experimental. +use crate::behaviour::gossipsub_scoring_parameters::GREYLIST_THRESHOLD as GOSSIPSUB_GREYLIST_THRESHOLD; +use serde::Serialize; +use std::time::Instant; +use strum::AsRefStr; +use tokio::time::Duration; + +lazy_static! { + static ref HALFLIFE_DECAY: f64 = -(2.0f64.ln()) / SCORE_HALFLIFE; +} + +/// The default score for new peers. +pub(crate) const DEFAULT_SCORE: f64 = 0.0; +/// The minimum reputation before a peer is disconnected. +const MIN_SCORE_BEFORE_DISCONNECT: f64 = -20.0; +/// The minimum reputation before a peer is banned. +const MIN_SCORE_BEFORE_BAN: f64 = -50.0; +/// If a peer has a lighthouse score below this constant all other score parts will get ignored and +/// the peer will get banned regardless of the other parts. +const MIN_LIGHTHOUSE_SCORE_BEFORE_BAN: f64 = -60.0; +/// The maximum score a peer can obtain. +const MAX_SCORE: f64 = 100.0; +/// The minimum score a peer can obtain. +const MIN_SCORE: f64 = -100.0; +/// The halflife of a peer's score. I.e the number of seconds it takes for the score to decay to half its value. +const SCORE_HALFLIFE: f64 = 600.0; +/// The number of seconds we ban a peer for before their score begins to decay. +const BANNED_BEFORE_DECAY: Duration = Duration::from_secs(12 * 3600); // 12 hours + +/// We weight negative gossipsub scores in such a way that they never result in a disconnect by +/// themselves. This "solves" the problem of non-decaying gossipsub scores for disconnected peers. +const GOSSIPSUB_NEGATIVE_SCORE_WEIGHT: f64 = + (MIN_SCORE_BEFORE_DISCONNECT + 1.0) / GOSSIPSUB_GREYLIST_THRESHOLD; +const GOSSIPSUB_POSITIVE_SCORE_WEIGHT: f64 = GOSSIPSUB_NEGATIVE_SCORE_WEIGHT; + +/// A collection of actions a peer can perform which will adjust its score. +/// Each variant has an associated score change. +// To easily assess the behaviour of scores changes the number of variants should stay low, and +// somewhat generic. +#[derive(Debug, Clone, Copy, AsRefStr)] +#[strum(serialize_all = "snake_case")] +pub enum PeerAction { + /// We should not communicate more with this peer. + /// This action will cause the peer to get banned. + Fatal, + /// This peer's action is not malicious but will not be tolerated. A few occurrences will cause + /// the peer to get kicked. + /// NOTE: ~5 occurrences will get the peer banned + LowToleranceError, + /// An error occurred with this peer but it is not necessarily malicious. + /// We have high tolerance for this actions: several occurrences are needed for a peer to get + /// kicked. + /// NOTE: ~10 occurrences will get the peer banned + MidToleranceError, + /// An error occurred with this peer but it is not necessarily malicious. + /// We have high tolerance for this actions: several occurrences are needed for a peer to get + /// kicked. + /// NOTE: ~50 occurrences will get the peer banned + HighToleranceError, +} + +/// Service reporting a `PeerAction` for a peer. +#[derive(Debug)] +pub enum ReportSource { + Gossipsub, + RPC, + Processor, + SyncService, + PeerManager, +} + +impl From for &'static str { + fn from(report_source: ReportSource) -> &'static str { + match report_source { + ReportSource::Gossipsub => "gossipsub", + ReportSource::RPC => "rpc_error", + ReportSource::Processor => "processor", + ReportSource::SyncService => "sync", + ReportSource::PeerManager => "peer_manager", + } + } +} + +impl std::fmt::Display for PeerAction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PeerAction::Fatal => write!(f, "Fatal"), + PeerAction::LowToleranceError => write!(f, "Low Tolerance Error"), + PeerAction::MidToleranceError => write!(f, "Mid Tolerance Error"), + PeerAction::HighToleranceError => write!(f, "High Tolerance Error"), + } + } +} + +/// The expected state of the peer given the peer's score. +#[derive(Debug, PartialEq, Clone, Copy)] +pub(crate) enum ScoreState { + /// We are content with the peers performance. We permit connections and messages. + Healthy, + /// The peer should be disconnected. We allow re-connections if the peer is persistent. + Disconnected, + /// The peer is banned. We disallow new connections until it's score has decayed into a + /// tolerable threshold. + Banned, +} + +impl std::fmt::Display for ScoreState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ScoreState::Healthy => write!(f, "Healthy"), + ScoreState::Banned => write!(f, "Banned"), + ScoreState::Disconnected => write!(f, "Disconnected"), + } + } +} + +/// A peer's score (perceived potential usefulness). +/// +/// This simplistic version consists of a global score per peer which decays to 0 over time. The +/// decay rate applies equally to positive and negative scores. +#[derive(PartialEq, Clone, Debug, Serialize)] +pub struct RealScore { + /// The global score. + // NOTE: In the future we may separate this into sub-scores involving the RPC, Gossipsub and + // lighthouse. + lighthouse_score: f64, + gossipsub_score: f64, + /// We ignore the negative gossipsub scores of some peers to allow decaying without + /// disconnecting. + ignore_negative_gossipsub_score: bool, + score: f64, + /// The time the score was last updated to perform time-based adjustments such as score-decay. + #[serde(skip)] + last_updated: Instant, +} + +impl Default for RealScore { + fn default() -> Self { + RealScore { + lighthouse_score: DEFAULT_SCORE, + gossipsub_score: DEFAULT_SCORE, + score: DEFAULT_SCORE, + last_updated: Instant::now(), + ignore_negative_gossipsub_score: false, + } + } +} + +impl RealScore { + /// Access to the underlying score. + fn recompute_score(&mut self) { + self.score = self.lighthouse_score; + if self.lighthouse_score <= MIN_LIGHTHOUSE_SCORE_BEFORE_BAN { + //ignore all other scores, i.e. do nothing here + } else if self.gossipsub_score >= 0.0 { + self.score += self.gossipsub_score * GOSSIPSUB_POSITIVE_SCORE_WEIGHT; + } else if !self.ignore_negative_gossipsub_score { + self.score += self.gossipsub_score * GOSSIPSUB_NEGATIVE_SCORE_WEIGHT; + } + } + + fn score(&self) -> f64 { + self.score + } + + /// Modifies the score based on a peer's action. + pub fn apply_peer_action(&mut self, peer_action: PeerAction) { + match peer_action { + PeerAction::Fatal => self.set_lighthouse_score(MIN_SCORE), // The worst possible score + PeerAction::LowToleranceError => self.add(-10.0), + PeerAction::MidToleranceError => self.add(-5.0), + PeerAction::HighToleranceError => self.add(-1.0), + } + } + + fn set_lighthouse_score(&mut self, new_score: f64) { + self.lighthouse_score = new_score; + self.update_state(); + } + + /// Add an f64 to the score abiding by the limits. + fn add(&mut self, score: f64) { + let mut new_score = self.lighthouse_score + score; + if new_score > MAX_SCORE { + new_score = MAX_SCORE; + } + if new_score < MIN_SCORE { + new_score = MIN_SCORE; + } + + self.set_lighthouse_score(new_score); + } + + fn update_state(&mut self) { + let was_not_banned = self.score > MIN_SCORE_BEFORE_BAN; + self.recompute_score(); + if was_not_banned && self.score <= MIN_SCORE_BEFORE_BAN { + //we ban this peer for at least BANNED_BEFORE_DECAY seconds + self.last_updated += BANNED_BEFORE_DECAY; + } + } + + /// Add an f64 to the score abiding by the limits. + #[cfg(test)] + pub fn test_add(&mut self, score: f64) { + self.add(score); + } + + #[cfg(test)] + // reset the score + pub fn test_reset(&mut self) { + self.set_lighthouse_score(0f64); + } + + // Set the gossipsub_score to a specific f64. + // Used in testing to induce score status changes during a heartbeat. + #[cfg(test)] + pub fn set_gossipsub_score(&mut self, score: f64) { + self.gossipsub_score = score; + } + + /// Applies time-based logic such as decay rates to the score. + /// This function should be called periodically. + pub fn update(&mut self) { + self.update_at(Instant::now()) + } + + /// Applies time-based logic such as decay rates to the score with the given now value. + /// This private sub function is mainly used for testing. + fn update_at(&mut self, now: Instant) { + // Decay the current score + // Using exponential decay based on a constant half life. + + // It is important that we use here `checked_duration_since` instead of elapsed, since + // we set last_updated to the future when banning peers. Therefore `checked_duration_since` + // will return None in this case and the score does not get decayed. + if let Some(secs_since_update) = now + .checked_duration_since(self.last_updated) + .map(|d| d.as_secs()) + { + // e^(-ln(2)/HL*t) + let decay_factor = (*HALFLIFE_DECAY * secs_since_update as f64).exp(); + self.lighthouse_score *= decay_factor; + self.last_updated = now; + self.update_state(); + } + } + + pub fn update_gossipsub_score(&mut self, new_score: f64, ignore: bool) { + // we only update gossipsub if last_updated is in the past which means either the peer is + // not banned or the BANNED_BEFORE_DECAY time is over. + if self.last_updated <= Instant::now() { + self.gossipsub_score = new_score; + self.ignore_negative_gossipsub_score = ignore; + self.update_state(); + } + } + + pub fn is_good_gossipsub_peer(&self) -> bool { + self.gossipsub_score >= 0.0 + } +} + +#[derive(PartialEq, Clone, Debug, Serialize)] +pub enum Score { + Max, + Real(RealScore), +} + +impl Default for Score { + fn default() -> Self { + Self::Real(RealScore::default()) + } +} + +macro_rules! apply { + ( $method:ident $(, $param_name: ident: $param_type: ty)*) => { + impl Score { + pub fn $method( + &mut self, $($param_name: $param_type, )* + ) { + if let Self::Real(score) = self { + score.$method($($param_name, )*); + } + } + } + }; +} + +apply!(apply_peer_action, peer_action: PeerAction); +apply!(update); +apply!(update_gossipsub_score, new_score: f64, ignore: bool); +#[cfg(test)] +apply!(test_add, score: f64); +#[cfg(test)] +apply!(test_reset); +#[cfg(test)] +apply!(set_gossipsub_score, score: f64); + +impl Score { + pub fn score(&self) -> f64 { + match self { + Self::Max => f64::INFINITY, + Self::Real(score) => score.score(), + } + } + + pub fn max_score() -> Self { + Self::Max + } + + /// Returns the expected state of the peer given it's score. + pub(crate) fn state(&self) -> ScoreState { + match self.score() { + x if x <= MIN_SCORE_BEFORE_BAN => ScoreState::Banned, + x if x <= MIN_SCORE_BEFORE_DISCONNECT => ScoreState::Disconnected, + _ => ScoreState::Healthy, + } + } + + pub fn is_good_gossipsub_peer(&self) -> bool { + match self { + Self::Max => true, + Self::Real(score) => score.is_good_gossipsub_peer(), + } + } +} + +impl Eq for Score {} + +impl PartialOrd for Score { + fn partial_cmp(&self, other: &Score) -> Option { + self.score().partial_cmp(&other.score()) + } +} + +impl Ord for Score { + fn cmp(&self, other: &Score) -> std::cmp::Ordering { + self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal) + } +} + +impl std::fmt::Display for Score { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:.2}", self.score()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[allow(clippy::float_cmp)] + fn test_reputation_change() { + let mut score = Score::default(); + + // 0 change does not change de reputation + // + let change = 0.0; + score.test_add(change); + assert_eq!(score.score(), DEFAULT_SCORE); + + // underflowing change is capped + let mut score = Score::default(); + let change = MIN_SCORE - 50.0; + score.test_add(change); + assert_eq!(score.score(), MIN_SCORE); + + // overflowing change is capped + let mut score = Score::default(); + let change = MAX_SCORE + 50.0; + score.test_add(change); + assert_eq!(score.score(), MAX_SCORE); + + // Score adjusts + let mut score = Score::default(); + let change = 1.32; + score.test_add(change); + assert_eq!(score.score(), DEFAULT_SCORE + change); + } + + #[test] + #[allow(clippy::float_cmp)] + fn test_ban_time() { + let mut score = RealScore::default(); + let now = Instant::now(); + + let change = MIN_SCORE_BEFORE_BAN; + score.test_add(change); + assert_eq!(score.score(), MIN_SCORE_BEFORE_BAN); + + score.update_at(now + BANNED_BEFORE_DECAY); + assert_eq!(score.score(), MIN_SCORE_BEFORE_BAN); + + score.update_at(now + BANNED_BEFORE_DECAY + Duration::from_secs(1)); + assert!(score.score() > MIN_SCORE_BEFORE_BAN); + } + + #[test] + fn test_very_negative_gossipsub_score() { + let mut score = Score::default(); + score.update_gossipsub_score(GOSSIPSUB_GREYLIST_THRESHOLD, false); + assert!(!score.is_good_gossipsub_peer()); + assert!(score.score() < 0.0); + assert_eq!(score.state(), ScoreState::Healthy); + score.test_add(-1.0001); + assert_eq!(score.state(), ScoreState::Disconnected); + } + + #[test] + #[allow(clippy::float_cmp)] + fn test_ignored_gossipsub_score() { + let mut score = Score::default(); + score.update_gossipsub_score(GOSSIPSUB_GREYLIST_THRESHOLD, true); + assert!(!score.is_good_gossipsub_peer()); + assert_eq!(score.score(), 0.0); + } +} diff --git a/node/network/src/peer_manager/peerdb/sync_status.rs b/node/network/src/peer_manager/peerdb/sync_status.rs new file mode 100644 index 0000000..b23cf57 --- /dev/null +++ b/node/network/src/peer_manager/peerdb/sync_status.rs @@ -0,0 +1,82 @@ +//! Handles individual sync status for peers. + +use ethereum_types::H256; +use serde::Serialize; + +#[derive(Clone, Debug, Serialize)] +/// The current sync status of the peer. +pub enum SyncStatus { + /// At the current state as our node or ahead of us. + Synced { info: SyncInfo }, + /// The peer has greater knowledge about the canonical chain than we do. + Advanced { info: SyncInfo }, + /// Is behind our current head and not useful for block downloads. + Behind { info: SyncInfo }, + /// This peer is in an incompatible network. + IrrelevantPeer, + /// Not currently known as a STATUS handshake has not occurred. + Unknown, +} + +/// A relevant peer's sync information. +#[derive(Clone, Debug, Serialize)] +pub struct SyncInfo { + pub head_root: H256, + pub finalized_root: H256, +} + +impl std::cmp::PartialEq for SyncStatus { + fn eq(&self, other: &Self) -> bool { + matches!( + (self, other), + (SyncStatus::Synced { .. }, SyncStatus::Synced { .. }) + | (SyncStatus::Advanced { .. }, SyncStatus::Advanced { .. }) + | (SyncStatus::Behind { .. }, SyncStatus::Behind { .. }) + | (SyncStatus::IrrelevantPeer, SyncStatus::IrrelevantPeer) + | (SyncStatus::Unknown, SyncStatus::Unknown) + ) + } +} + +impl SyncStatus { + /// Returns true if the peer has advanced knowledge of the chain. + pub fn is_advanced(&self) -> bool { + matches!(self, SyncStatus::Advanced { .. }) + } + + /// Returns true if the peer is up to date with the current chain. + pub fn is_synced(&self) -> bool { + matches!(self, SyncStatus::Synced { .. }) + } + + /// Returns true if the peer is behind the current chain. + pub fn is_behind(&self) -> bool { + matches!(self, SyncStatus::Behind { .. }) + } + + /// Updates the peer's sync status, returning whether the status transitioned. + /// + /// E.g. returns `true` if the state changed from `Synced` to `Advanced`, but not if + /// the status remained `Synced` with different `SyncInfo` within. + pub fn update(&mut self, new_state: SyncStatus) -> bool { + let changed_status = *self != new_state; + *self = new_state; + changed_status + } + + pub fn as_str(&self) -> &'static str { + match self { + SyncStatus::Advanced { .. } => "Advanced", + SyncStatus::Behind { .. } => "Behind", + SyncStatus::Synced { .. } => "Synced", + SyncStatus::Unknown => "Unknown", + SyncStatus::IrrelevantPeer => "Irrelevant", + } + } +} + +impl std::fmt::Display for SyncStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} diff --git a/node/network/src/rpc/codec/base.rs b/node/network/src/rpc/codec/base.rs new file mode 100644 index 0000000..7aa9e2c --- /dev/null +++ b/node/network/src/rpc/codec/base.rs @@ -0,0 +1,262 @@ +//! This handles the various supported encoding mechanism for the Eth 2.0 RPC. + +use crate::rpc::methods::ErrorType; +use crate::rpc::{InboundRequest, OutboundRequest, RPCCodedResponse, RPCResponse}; +use libp2p::bytes::BufMut; +use libp2p::bytes::BytesMut; +use tokio_util::codec::{Decoder, Encoder}; + +pub trait OutboundCodec: Encoder + Decoder { + type CodecErrorType; + + fn decode_error( + &mut self, + src: &mut BytesMut, + ) -> Result, ::Error>; +} + +/* Global Inbound Codec */ +// This deals with Decoding RPC Requests from other peers and encoding our responses + +pub struct BaseInboundCodec +where + TCodec: Encoder + Decoder, +{ + /// Inner codec for handling various encodings + inner: TCodec, +} + +impl BaseInboundCodec +where + TCodec: Encoder + Decoder, +{ + pub fn new(codec: TCodec) -> Self { + BaseInboundCodec { inner: codec } + } +} + +/* Global Outbound Codec */ +// This deals with Decoding RPC Responses from other peers and encoding our requests +pub struct BaseOutboundCodec +where + TOutboundCodec: OutboundCodec, +{ + /// Inner codec for handling various encodings. + inner: TOutboundCodec, + /// Keeps track of the current response code for a chunk. + current_response_code: Option, +} + +impl BaseOutboundCodec +where + TOutboundCodec: OutboundCodec, +{ + pub fn new(codec: TOutboundCodec) -> Self { + BaseOutboundCodec { + inner: codec, + current_response_code: None, + } + } +} + +/* Implementation of the Encoding/Decoding for the global codecs */ + +/* Base Inbound Codec */ + +// This Encodes RPC Responses sent to external peers +impl Encoder for BaseInboundCodec +where + TCodec: Decoder + Encoder, +{ + type Error = >::Error; + + fn encode(&mut self, item: RPCCodedResponse, dst: &mut BytesMut) -> Result<(), Self::Error> { + dst.clear(); + dst.reserve(1); + dst.put_u8( + item.as_u8() + .expect("Should never encode a stream termination"), + ); + self.inner.encode(item, dst) + } +} + +// This Decodes RPC Requests from external peers +impl Decoder for BaseInboundCodec +where + TCodec: Encoder + Decoder, +{ + type Item = InboundRequest; + type Error = ::Error; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + self.inner.decode(src) + } +} + +/* Base Outbound Codec */ + +// This Encodes RPC Requests sent to external peers +impl Encoder for BaseOutboundCodec +where + TCodec: OutboundCodec + Encoder, +{ + type Error = >::Error; + + fn encode(&mut self, item: OutboundRequest, dst: &mut BytesMut) -> Result<(), Self::Error> { + self.inner.encode(item, dst) + } +} + +// This decodes RPC Responses received from external peers +impl Decoder for BaseOutboundCodec +where + TCodec: + OutboundCodec + Decoder, +{ + type Item = RPCCodedResponse; + type Error = ::Error; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + // if we have only received the response code, wait for more bytes + if src.len() <= 1 { + return Ok(None); + } + // using the response code determine which kind of payload needs to be decoded. + let response_code = self.current_response_code.unwrap_or_else(|| { + let resp_code = src.split_to(1)[0]; + self.current_response_code = Some(resp_code); + resp_code + }); + + let inner_result = { + if RPCCodedResponse::is_response(response_code) { + // decode an actual response and mutates the buffer if enough bytes have been read + // returning the result. + self.inner + .decode(src) + .map(|r| r.map(RPCCodedResponse::Success)) + } else { + // decode an error + self.inner + .decode_error(src) + .map(|r| r.map(|resp| RPCCodedResponse::from_error(response_code, resp))) + } + }; + // if the inner decoder was capable of decoding a chunk, we need to reset the current + // response code for the next chunk + if let Ok(Some(_)) = inner_result { + self.current_response_code = None; + } + // return the result + inner_result + } +} + +#[cfg(test)] +mod tests { + use super::super::ssz_snappy::*; + use super::*; + use crate::rpc::protocol::*; + + use unsigned_varint::codec::Uvi; + + // #[test] + // fn test_decode_status_message() { + // let message = hex::decode("0054ff060000734e615070590032000006e71e7b54989925efd6c9cbcb8ceb9b5f71216f5137282bf6a1e3b50f64e42d6c7fb347abe07eb0db8200000005029e2800").unwrap(); + // let mut buf = BytesMut::new(); + // buf.extend_from_slice(&message); + + // let snappy_protocol_id = + // ProtocolId::new(Protocol::Status, Version::V1, Encoding::SSZSnappy); + + // let mut snappy_outbound_codec = SSZSnappyOutboundCodec::new( + // snappy_protocol_id, + // max_rpc_size(), + // ); + + // // remove response code + // let mut snappy_buf = buf.clone(); + // let _ = snappy_buf.split_to(1); + + // // decode message just as snappy message + // let _snappy_decoded_message = snappy_outbound_codec.decode(&mut snappy_buf).unwrap(); + + // // build codecs for entire chunk + // let mut snappy_base_outbound_codec = BaseOutboundCodec::new(snappy_outbound_codec); + + // // decode message as ssz snappy chunk + // let _snappy_decoded_chunk = snappy_base_outbound_codec.decode(&mut buf).unwrap(); + // } + + #[test] + fn test_invalid_length_prefix() { + let mut uvi_codec: Uvi = Uvi::default(); + let mut dst = BytesMut::with_capacity(1024); + + // Smallest > 10 byte varint + let len: u128 = 2u128.pow(70); + + // Insert length-prefix + uvi_codec.encode(len, &mut dst).unwrap(); + + let snappy_protocol_id = + ProtocolId::new(Protocol::Status, Version::V1, Encoding::SSZSnappy); + + let mut snappy_outbound_codec = + SSZSnappyOutboundCodec::new(snappy_protocol_id, max_rpc_size()); + + let snappy_decoded_message = snappy_outbound_codec.decode(&mut dst).unwrap_err(); + + assert_eq!( + snappy_decoded_message, + RPCError::IoError("input bytes exceed maximum".to_string()), + "length-prefix of > 10 bytes is invalid" + ); + } + + #[test] + fn test_length_limits() { + fn encode_len(len: usize) -> BytesMut { + let mut uvi_codec: Uvi = Uvi::default(); + let mut dst = BytesMut::with_capacity(1024); + uvi_codec.encode(len, &mut dst).unwrap(); + dst + } + + let protocol_id = ProtocolId::new(Protocol::DataByHash, Version::V1, Encoding::SSZSnappy); + + // Response limits + let max_rpc_size = max_rpc_size(); + let limit = protocol_id.rpc_response_limits(); + let mut max = encode_len(limit.max + 1); + let mut codec = SSZSnappyOutboundCodec::new(protocol_id.clone(), max_rpc_size); + assert!(matches!( + codec.decode(&mut max).unwrap_err(), + RPCError::InvalidData(_) + )); + + let mut min = encode_len(limit.min - 1); + let mut codec = SSZSnappyOutboundCodec::new(protocol_id.clone(), max_rpc_size); + assert!(matches!( + codec.decode(&mut min).unwrap_err(), + RPCError::InvalidData(_) + )); + + // Request limits + let limit = protocol_id.rpc_request_limits(); + let mut max = encode_len(limit.max + 1); + let mut codec = SSZSnappyOutboundCodec::new(protocol_id.clone(), max_rpc_size); + assert!(matches!( + codec.decode(&mut max).unwrap_err(), + RPCError::InvalidData(_) + )); + + let mut min = encode_len(limit.min - 1); + let mut codec = SSZSnappyOutboundCodec::new(protocol_id, max_rpc_size); + assert!(matches!( + codec.decode(&mut min).unwrap_err(), + RPCError::InvalidData(_) + )); + } +} diff --git a/node/network/src/rpc/codec/mod.rs b/node/network/src/rpc/codec/mod.rs new file mode 100644 index 0000000..34ccb3d --- /dev/null +++ b/node/network/src/rpc/codec/mod.rs @@ -0,0 +1,60 @@ +pub(crate) mod base; +pub(crate) mod ssz_snappy; + +use self::base::{BaseInboundCodec, BaseOutboundCodec}; +use self::ssz_snappy::{SSZSnappyInboundCodec, SSZSnappyOutboundCodec}; +use crate::rpc::protocol::RPCError; +use crate::rpc::{InboundRequest, OutboundRequest, RPCCodedResponse}; +use libp2p::bytes::BytesMut; +use tokio_util::codec::{Decoder, Encoder}; + +// Known types of codecs +pub enum InboundCodec { + SSZSnappy(BaseInboundCodec), +} + +pub enum OutboundCodec { + SSZSnappy(BaseOutboundCodec), +} + +impl Encoder for InboundCodec { + type Error = RPCError; + + fn encode(&mut self, item: RPCCodedResponse, dst: &mut BytesMut) -> Result<(), Self::Error> { + match self { + InboundCodec::SSZSnappy(codec) => codec.encode(item, dst), + } + } +} + +impl Decoder for InboundCodec { + type Item = InboundRequest; + type Error = RPCError; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + match self { + InboundCodec::SSZSnappy(codec) => codec.decode(src), + } + } +} + +impl Encoder for OutboundCodec { + type Error = RPCError; + + fn encode(&mut self, item: OutboundRequest, dst: &mut BytesMut) -> Result<(), Self::Error> { + match self { + OutboundCodec::SSZSnappy(codec) => codec.encode(item, dst), + } + } +} + +impl Decoder for OutboundCodec { + type Item = RPCCodedResponse; + type Error = RPCError; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + match self { + OutboundCodec::SSZSnappy(codec) => codec.decode(src), + } + } +} diff --git a/node/network/src/rpc/codec/ssz_snappy.rs b/node/network/src/rpc/codec/ssz_snappy.rs new file mode 100644 index 0000000..604f0ba --- /dev/null +++ b/node/network/src/rpc/codec/ssz_snappy.rs @@ -0,0 +1,585 @@ +use crate::rpc::methods::*; +use crate::rpc::{ + codec::base::OutboundCodec, + protocol::{Encoding, Protocol, ProtocolId, RPCError, Version, ERROR_TYPE_MAX, ERROR_TYPE_MIN}, +}; +use crate::rpc::{InboundRequest, OutboundRequest, RPCCodedResponse, RPCResponse}; +use libp2p::bytes::BytesMut; +use shared_types::ChunkArrayWithProof; +use snap::read::FrameDecoder; +use snap::write::FrameEncoder; +use ssz::{Decode, Encode}; +use ssz_types::VariableList; +use std::io::Cursor; +use std::io::ErrorKind; +use std::io::{Read, Write}; +use tokio_util::codec::{Decoder, Encoder}; +use unsigned_varint::codec::Uvi; + +/* Inbound Codec */ + +pub struct SSZSnappyInboundCodec { + protocol: ProtocolId, + inner: Uvi, + len: Option, + /// Maximum bytes that can be sent in one req/resp chunked responses. + max_packet_size: usize, +} + +impl SSZSnappyInboundCodec { + pub fn new(protocol: ProtocolId, max_packet_size: usize) -> Self { + let uvi_codec = Uvi::default(); + // this encoding only applies to ssz_snappy. + debug_assert_eq!(protocol.encoding, Encoding::SSZSnappy); + + SSZSnappyInboundCodec { + inner: uvi_codec, + protocol, + len: None, + max_packet_size, + } + } +} + +// Encoder for inbound streams: Encodes RPC Responses sent to peers. +impl Encoder for SSZSnappyInboundCodec { + type Error = RPCError; + + fn encode(&mut self, item: RPCCodedResponse, dst: &mut BytesMut) -> Result<(), Self::Error> { + let bytes = match &item { + RPCCodedResponse::Success(resp) => match &resp { + RPCResponse::Status(res) => res.as_ssz_bytes(), + RPCResponse::Pong(res) => res.data.as_ssz_bytes(), + RPCResponse::DataByHash(res) => res.as_ssz_bytes(), + RPCResponse::Chunks(res) => res.as_ssz_bytes(), + }, + RPCCodedResponse::Error(_, err) => err.as_ssz_bytes(), + RPCCodedResponse::StreamTermination(_) => { + unreachable!("Code error - attempting to encode a stream termination") + } + }; + // SSZ encoded bytes should be within `max_packet_size` + if bytes.len() > self.max_packet_size { + return Err(RPCError::InternalError( + "attempting to encode data > max_packet_size", + )); + } + + // Inserts the length prefix of the uncompressed bytes into dst + // encoded as a unsigned varint + self.inner + .encode(bytes.len(), dst) + .map_err(RPCError::from)?; + + let mut writer = FrameEncoder::new(Vec::new()); + writer.write_all(&bytes).map_err(RPCError::from)?; + writer.flush().map_err(RPCError::from)?; + + // Write compressed bytes to `dst` + dst.extend_from_slice(writer.get_ref()); + Ok(()) + } +} + +// Decoder for inbound streams: Decodes RPC requests from peers +impl Decoder for SSZSnappyInboundCodec { + type Item = InboundRequest; + type Error = RPCError; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + let length = match handle_length(&mut self.inner, &mut self.len, src)? { + Some(len) => len, + None => return Ok(None), + }; + + // Should not attempt to decode rpc chunks with `length > max_packet_size` or not within bounds of + // packet size for ssz container corresponding to `self.protocol`. + let ssz_limits = self.protocol.rpc_request_limits(); + if ssz_limits.is_out_of_bounds(length, self.max_packet_size) { + return Err(RPCError::InvalidData(format!( + "RPC request length is out of bounds, length {} bounds = {:?}, protocol = {:?}", + length, ssz_limits, self.protocol, + ))); + } + // Calculate worst case compression length for given uncompressed length + let max_compressed_len = snap::raw::max_compress_len(length) as u64; + + // Create a limit reader as a wrapper that reads only upto `max_compressed_len` from `src`. + let limit_reader = Cursor::new(src.as_ref()).take(max_compressed_len); + let mut reader = FrameDecoder::new(limit_reader); + let mut decoded_buffer = vec![0; length]; + + match reader.read_exact(&mut decoded_buffer) { + Ok(()) => { + // `n` is how many bytes the reader read in the compressed stream + let n = reader.get_ref().get_ref().position(); + self.len = None; + let _read_bytes = src.split_to(n as usize); + + match self.protocol.version { + Version::V1 => handle_v1_request(self.protocol.message_name, &decoded_buffer), + } + } + Err(e) => handle_error(e, reader.get_ref().get_ref().position(), max_compressed_len), + } + } +} + +/* Outbound Codec: Codec for initiating RPC requests */ +pub struct SSZSnappyOutboundCodec { + inner: Uvi, + len: Option, + protocol: ProtocolId, + /// Maximum bytes that can be sent in one req/resp chunked responses. + max_packet_size: usize, +} + +impl SSZSnappyOutboundCodec { + pub fn new(protocol: ProtocolId, max_packet_size: usize) -> Self { + let uvi_codec = Uvi::default(); + // this encoding only applies to ssz_snappy. + debug_assert_eq!(protocol.encoding, Encoding::SSZSnappy); + + SSZSnappyOutboundCodec { + inner: uvi_codec, + protocol, + max_packet_size, + len: None, + } + } +} + +// Encoder for outbound streams: Encodes RPC Requests to peers +impl Encoder for SSZSnappyOutboundCodec { + type Error = RPCError; + + fn encode(&mut self, item: OutboundRequest, dst: &mut BytesMut) -> Result<(), Self::Error> { + let bytes = match item { + OutboundRequest::Status(req) => req.as_ssz_bytes(), + OutboundRequest::Goodbye(req) => req.as_ssz_bytes(), + OutboundRequest::Ping(req) => req.as_ssz_bytes(), + OutboundRequest::DataByHash(req) => req.hashes.as_ssz_bytes(), + OutboundRequest::GetChunks(req) => req.as_ssz_bytes(), + }; + // SSZ encoded bytes should be within `max_packet_size` + if bytes.len() > self.max_packet_size { + return Err(RPCError::InternalError( + "attempting to encode data > max_packet_size", + )); + } + + // Inserts the length prefix of the uncompressed bytes into dst + // encoded as a unsigned varint + self.inner + .encode(bytes.len(), dst) + .map_err(RPCError::from)?; + + let mut writer = FrameEncoder::new(Vec::new()); + writer.write_all(&bytes).map_err(RPCError::from)?; + writer.flush().map_err(RPCError::from)?; + + // Write compressed bytes to `dst` + dst.extend_from_slice(writer.get_ref()); + Ok(()) + } +} + +// Decoder for outbound streams: Decodes RPC responses from peers. +// +// The majority of the decoding has now been pushed upstream due to the changing specification. +// We prefer to decode blocks and attestations with extra knowledge about the chain to perform +// faster verification checks before decoding entire blocks/attestations. +impl Decoder for SSZSnappyOutboundCodec { + type Item = RPCResponse; + type Error = RPCError; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + let length = match handle_length(&mut self.inner, &mut self.len, src)? { + Some(len) => len, + None => return Ok(None), + }; + + // Should not attempt to decode rpc chunks with `length > max_packet_size` or not within bounds of + // packet size for ssz container corresponding to `self.protocol`. + let ssz_limits = self.protocol.rpc_response_limits(); + + if ssz_limits.is_out_of_bounds(length, self.max_packet_size) { + return Err(RPCError::InvalidData(format!( + "RPC response length is out of bounds, length {}", + length, + ))); + } + // Calculate worst case compression length for given uncompressed length + let max_compressed_len = snap::raw::max_compress_len(length) as u64; + // Create a limit reader as a wrapper that reads only upto `max_compressed_len` from `src`. + let limit_reader = Cursor::new(src.as_ref()).take(max_compressed_len); + let mut reader = FrameDecoder::new(limit_reader); + + let mut decoded_buffer = vec![0; length]; + + match reader.read_exact(&mut decoded_buffer) { + Ok(()) => { + // `n` is how many bytes the reader read in the compressed stream + let n = reader.get_ref().get_ref().position(); + self.len = None; + let _read_bytes = src.split_to(n as usize); + + match self.protocol.version { + Version::V1 => handle_v1_response(self.protocol.message_name, &decoded_buffer), + } + } + Err(e) => handle_error(e, reader.get_ref().get_ref().position(), max_compressed_len), + } + } +} + +impl OutboundCodec for SSZSnappyOutboundCodec { + type CodecErrorType = ErrorType; + + fn decode_error( + &mut self, + src: &mut BytesMut, + ) -> Result, RPCError> { + let length = match handle_length(&mut self.inner, &mut self.len, src)? { + Some(len) => len, + None => return Ok(None), + }; + + // Should not attempt to decode rpc chunks with `length > max_packet_size` or not within bounds of + // packet size for ssz container corresponding to `ErrorType`. + if length > self.max_packet_size || length > *ERROR_TYPE_MAX || length < *ERROR_TYPE_MIN { + return Err(RPCError::InvalidData(format!( + "RPC Error length is out of bounds, length {}", + length + ))); + } + + // Calculate worst case compression length for given uncompressed length + let max_compressed_len = snap::raw::max_compress_len(length) as u64; + // Create a limit reader as a wrapper that reads only upto `max_compressed_len` from `src`. + let limit_reader = Cursor::new(src.as_ref()).take(max_compressed_len); + let mut reader = FrameDecoder::new(limit_reader); + let mut decoded_buffer = vec![0; length]; + match reader.read_exact(&mut decoded_buffer) { + Ok(()) => { + // `n` is how many bytes the reader read in the compressed stream + let n = reader.get_ref().get_ref().position(); + self.len = None; + let _read_bytes = src.split_to(n as usize); + Ok(Some(ErrorType(VariableList::from_ssz_bytes( + &decoded_buffer, + )?))) + } + Err(e) => handle_error(e, reader.get_ref().get_ref().position(), max_compressed_len), + } + } +} + +/// Handle errors that we get from decoding an RPC message from the stream. +/// `num_bytes_read` is the number of bytes the snappy decoder has read from the underlying stream. +/// `max_compressed_len` is the maximum compressed size for a given uncompressed size. +fn handle_error( + err: std::io::Error, + num_bytes: u64, + max_compressed_len: u64, +) -> Result, RPCError> { + match err.kind() { + ErrorKind::UnexpectedEof => { + // If snappy has read `max_compressed_len` from underlying stream and still can't fill buffer, we have a malicious message. + // Report as `InvalidData` so that malicious peer gets banned. + if num_bytes >= max_compressed_len { + Err(RPCError::InvalidData(format!( + "Received malicious snappy message, num_bytes {}, max_compressed_len {}", + num_bytes, max_compressed_len + ))) + } else { + // Haven't received enough bytes to decode yet, wait for more + Ok(None) + } + } + _ => Err(err).map_err(RPCError::from), + } +} + +/// Decodes the length-prefix from the bytes as an unsigned protobuf varint. +/// +/// Returns `Ok(Some(length))` by decoding the bytes if required. +/// Returns `Ok(None)` if more bytes are needed to decode the length-prefix. +/// Returns an `RPCError` for a decoding error. +fn handle_length( + uvi_codec: &mut Uvi, + len: &mut Option, + bytes: &mut BytesMut, +) -> Result, RPCError> { + if let Some(length) = len { + Ok(Some(*length)) + } else { + // Decode the length of the uncompressed bytes from an unsigned varint + // Note: length-prefix of > 10 bytes(uint64) would be a decoding error + match uvi_codec.decode(bytes).map_err(RPCError::from)? { + Some(length) => { + *len = Some(length); + Ok(Some(length)) + } + None => Ok(None), // need more bytes to decode length + } + } +} + +/// Decodes a `Version::V1` `InboundRequest` from the byte stream. +/// `decoded_buffer` should be an ssz-encoded bytestream with +// length = length-prefix received in the beginning of the stream. +fn handle_v1_request( + protocol: Protocol, + decoded_buffer: &[u8], +) -> Result, RPCError> { + match protocol { + Protocol::Status => Ok(Some(InboundRequest::Status(StatusMessage::from_ssz_bytes( + decoded_buffer, + )?))), + Protocol::Goodbye => Ok(Some(InboundRequest::Goodbye( + GoodbyeReason::from_ssz_bytes(decoded_buffer)?, + ))), + Protocol::Ping => Ok(Some(InboundRequest::Ping(Ping { + data: u64::from_ssz_bytes(decoded_buffer)?, + }))), + Protocol::DataByHash => Ok(Some(InboundRequest::DataByHash(DataByHashRequest { + hashes: VariableList::from_ssz_bytes(decoded_buffer)?, + }))), + Protocol::GetChunks => Ok(Some(InboundRequest::GetChunks( + GetChunksRequest::from_ssz_bytes(decoded_buffer)?, + ))), + } +} + +/// Decodes a `Version::V1` `RPCResponse` from the byte stream. +/// `decoded_buffer` should be an ssz-encoded bytestream with +// length = length-prefix received in the beginning of the stream. +fn handle_v1_response( + protocol: Protocol, + decoded_buffer: &[u8], +) -> Result, RPCError> { + match protocol { + Protocol::Status => Ok(Some(RPCResponse::Status(StatusMessage::from_ssz_bytes( + decoded_buffer, + )?))), + // This case should be unreachable as `Goodbye` has no response. + Protocol::Goodbye => Err(RPCError::InvalidData( + "Goodbye RPC message has no valid response".to_string(), + )), + Protocol::Ping => Ok(Some(RPCResponse::Pong(Ping { + data: u64::from_ssz_bytes(decoded_buffer)?, + }))), + Protocol::DataByHash => Ok(Some(RPCResponse::DataByHash(Box::new( + ZgsData::from_ssz_bytes(decoded_buffer)?, + )))), + Protocol::GetChunks => Ok(Some(RPCResponse::Chunks( + ChunkArrayWithProof::from_ssz_bytes(decoded_buffer)?, + ))), + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::rpc::protocol::*; + use crate::rpc::{methods::StatusMessage, Ping}; + + use snap::write::FrameEncoder; + use ssz::Encode; + use std::io::Write; + + fn status_message() -> StatusMessage { + StatusMessage { data: 1 } + } + + fn ping_message() -> Ping { + Ping { data: 1 } + } + + /// Encodes the given protocol response as bytes. + fn encode( + protocol: Protocol, + version: Version, + message: RPCCodedResponse, + ) -> Result { + let snappy_protocol_id = ProtocolId::new(protocol, version, Encoding::SSZSnappy); + let max_packet_size = max_rpc_size(); + + let mut buf = BytesMut::new(); + let mut snappy_inbound_codec = + SSZSnappyInboundCodec::new(snappy_protocol_id, max_packet_size); + + snappy_inbound_codec.encode(message, &mut buf)?; + Ok(buf) + } + + // fn encode_without_length_checks( + // bytes: Vec, + // ) -> Result { + // let mut dst = BytesMut::new(); + + // let mut uvi_codec: Uvi = Uvi::default(); + + // // Inserts the length prefix of the uncompressed bytes into dst + // // encoded as a unsigned varint + // uvi_codec + // .encode(bytes.len(), &mut dst) + // .map_err(RPCError::from)?; + + // let mut writer = FrameEncoder::new(Vec::new()); + // writer.write_all(&bytes).map_err(RPCError::from)?; + // writer.flush().map_err(RPCError::from)?; + + // // Write compressed bytes to `dst` + // dst.extend_from_slice(writer.get_ref()); + + // Ok(dst) + // } + + /// Attempts to decode the given protocol bytes as an rpc response + fn decode( + protocol: Protocol, + version: Version, + message: &mut BytesMut, + ) -> Result, RPCError> { + let snappy_protocol_id = ProtocolId::new(protocol, version, Encoding::SSZSnappy); + let max_packet_size = max_rpc_size(); + let mut snappy_outbound_codec = + SSZSnappyOutboundCodec::new(snappy_protocol_id, max_packet_size); + // decode message just as snappy message + snappy_outbound_codec.decode(message) + } + + /// Encodes the provided protocol message as bytes and tries to decode the encoding bytes. + fn encode_then_decode( + protocol: Protocol, + version: Version, + message: RPCCodedResponse, + ) -> Result, RPCError> { + let mut encoded = encode(protocol, version.clone(), message)?; + decode(protocol, version, &mut encoded) + } + + // Test RPCResponse encoding/decoding for V1 messages + #[test] + fn test_encode_then_decode_v1() { + assert_eq!( + encode_then_decode( + Protocol::Status, + Version::V1, + RPCCodedResponse::Success(RPCResponse::Status(status_message())), + ), + Ok(Some(RPCResponse::Status(status_message()))) + ); + + assert_eq!( + encode_then_decode( + Protocol::Ping, + Version::V1, + RPCCodedResponse::Success(RPCResponse::Pong(ping_message())), + ), + Ok(Some(RPCResponse::Pong(ping_message()))) + ); + + assert_eq!( + encode_then_decode( + Protocol::DataByHash, + Version::V1, + RPCCodedResponse::Success(RPCResponse::DataByHash(Box::new(ZgsData { + hash: Default::default() + }))), + ), + Ok(Some(RPCResponse::DataByHash(Box::new(ZgsData { + hash: Default::default() + })))) + ); + + // TODO: add tests for outbound requests + } + + // /// Test a malicious snappy encoding for a V1 `Status` message where the attacker + // /// sends a valid message filled with a stream of useless padding before the actual message. + // #[test] + // fn test_decode_malicious_v1_message() { + // // 10 byte snappy stream identifier + // let stream_identifier: &'static [u8] = b"\xFF\x06\x00\x00sNaPpY"; + + // assert_eq!(stream_identifier.len(), 10); + + // // byte 0(0xFE) is padding chunk type identifier for snappy messages + // // byte 1,2,3 are chunk length (little endian) + // let malicious_padding: &'static [u8] = b"\xFE\x00\x00\x00"; + + // // Status message is 84 bytes uncompressed. `max_compressed_len` is 32 + 84 + 84/6 = 130. + // let status_message_bytes = StatusMessage { + // data: 1 + // } + // .as_ssz_bytes(); + + // assert_eq!(status_message_bytes.len(), 8); + // assert_eq!(snap::raw::max_compress_len(status_message_bytes.len()), 41); + + // let mut uvi_codec: Uvi = Uvi::default(); + // let mut dst = BytesMut::with_capacity(1024); + + // // Insert length-prefix + // uvi_codec + // .encode(status_message_bytes.len(), &mut dst) + // .unwrap(); + + // // Insert snappy stream identifier + // dst.extend_from_slice(stream_identifier); + + // // Insert malicious padding of 80 bytes. + // for _ in 0..20 { + // dst.extend_from_slice(malicious_padding); + // } + + // // Insert payload (42 bytes compressed) + // let mut writer = FrameEncoder::new(Vec::new()); + // writer.write_all(&status_message_bytes).unwrap(); + // writer.flush().unwrap(); + // assert_eq!(writer.get_ref().len(), 42); + // dst.extend_from_slice(writer.get_ref()); + + // // 10 (for stream identifier) + 80 + 42 = 132 > `max_compressed_len`. Hence, decoding should fail with `InvalidData`. + // assert!(matches!( + // decode(Protocol::Status, Version::V1, &mut dst).unwrap_err(), + // RPCError::InvalidData(_) + // )); + // } + + /// Test sending a message with encoded length prefix > max_rpc_size. + #[test] + fn test_decode_invalid_length() { + // 10 byte snappy stream identifier + let stream_identifier: &'static [u8] = b"\xFF\x06\x00\x00sNaPpY"; + + assert_eq!(stream_identifier.len(), 10); + + // Status message is 84 bytes uncompressed. `max_compressed_len` is 32 + 84 + 84/6 = 130. + let status_message_bytes = StatusMessage { data: 1 }.as_ssz_bytes(); + + let mut uvi_codec: Uvi = Uvi::default(); + let mut dst = BytesMut::with_capacity(1024); + + // Insert length-prefix + uvi_codec.encode(MAX_RPC_SIZE + 1, &mut dst).unwrap(); + + // Insert snappy stream identifier + dst.extend_from_slice(stream_identifier); + + // Insert payload + let mut writer = FrameEncoder::new(Vec::new()); + writer.write_all(&status_message_bytes).unwrap(); + writer.flush().unwrap(); + dst.extend_from_slice(writer.get_ref()); + + assert!(matches!( + decode(Protocol::Status, Version::V1, &mut dst).unwrap_err(), + RPCError::InvalidData(_) + )); + } +} diff --git a/node/network/src/rpc/handler.rs b/node/network/src/rpc/handler.rs new file mode 100644 index 0000000..fef268b --- /dev/null +++ b/node/network/src/rpc/handler.rs @@ -0,0 +1,983 @@ +#![allow(clippy::type_complexity)] +#![allow(clippy::cognitive_complexity)] + +use super::methods::{GoodbyeReason, RPCCodedResponse, RPCResponseErrorCode, ResponseTermination}; +use super::outbound::OutboundRequestContainer; +use super::protocol::{max_rpc_size, InboundRequest, Protocol, RPCError, RPCProtocol}; +use super::{RPCReceived, RPCSend, ReqId}; +use crate::rpc::outbound::{OutboundFramed, OutboundRequest}; +use crate::rpc::protocol::InboundFramed; +use fnv::FnvHashMap; +use futures::prelude::*; +use futures::{Sink, SinkExt}; +use libp2p::core::upgrade::{ + InboundUpgrade, NegotiationError, OutboundUpgrade, ProtocolError, UpgradeError, +}; +use libp2p::swarm::handler::{ + ConnectionHandler, ConnectionHandlerEvent, ConnectionHandlerUpgrErr, KeepAlive, + SubstreamProtocol, +}; +use libp2p::swarm::NegotiatedSubstream; +use smallvec::SmallVec; +use std::{ + collections::{hash_map::Entry, VecDeque}, + pin::Pin, + task::{Context, Poll}, + time::{Duration, Instant}, +}; +use tokio::time::{sleep_until, Instant as TInstant, Sleep}; +use tokio_util::time::{delay_queue, DelayQueue}; + +/// The time (in seconds) before a substream that is awaiting a response from the user times out. +pub const RESPONSE_TIMEOUT: u64 = 30; + +/// The number of times to retry an outbound upgrade in the case of IO errors. +const IO_ERROR_RETRIES: u8 = 3; + +/// Maximum time given to the handler to perform shutdown operations. +const SHUTDOWN_TIMEOUT_SECS: u8 = 45; + +/// Identifier of inbound and outbound substreams from the handler's perspective. +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] +pub struct SubstreamId(pub usize); + +type InboundSubstream = InboundFramed; + +/// Events the handler emits to the behaviour. +pub type HandlerEvent = Result, HandlerErr>; + +/// An error encountered by the handler. +#[derive(Debug)] +pub enum HandlerErr { + /// An error occurred for this peer's request. This can occur during protocol negotiation, + /// message passing, or if the handler identifies that we are sending an error response to the peer. + Inbound { + /// Id of the peer's request for which an error occurred. + id: SubstreamId, + /// Information of the negotiated protocol. + proto: Protocol, + /// The error that occurred. + error: RPCError, + }, + /// An error occurred for this request. Such error can occur during protocol negotiation, + /// message passing, or if we successfully received a response from the peer, but this response + /// indicates an error. + Outbound { + /// Application-given Id of the request for which an error occurred. + id: Id, + /// Information of the protocol. + proto: Protocol, + /// The error that occurred. + error: RPCError, + }, +} + +/// Implementation of `ConnectionHandler` for the RPC protocol. +pub struct RPCHandler { + /// The upgrade for inbound substreams. + listen_protocol: SubstreamProtocol, + + /// Queue of events to produce in `poll()`. + events_out: SmallVec<[HandlerEvent; 4]>, + + /// Queue of outbound substreams to open. + dial_queue: SmallVec<[(Id, OutboundRequest); 4]>, + + /// Current number of concurrent outbound substreams being opened. + dial_negotiated: u32, + + /// Current inbound substreams awaiting processing. + inbound_substreams: FnvHashMap, + + /// Inbound substream `DelayQueue` which keeps track of when an inbound substream will timeout. + inbound_substreams_delay: DelayQueue, + + /// Map of outbound substreams that need to be driven to completion. + outbound_substreams: FnvHashMap>, + + /// Inbound substream `DelayQueue` which keeps track of when an inbound substream will timeout. + outbound_substreams_delay: DelayQueue, + + /// Sequential ID for waiting substreams. For inbound substreams, this is also the inbound request ID. + current_inbound_substream_id: SubstreamId, + + /// Sequential ID for outbound substreams. + current_outbound_substream_id: SubstreamId, + + /// Maximum number of concurrent outbound substreams being opened. Value is never modified. + max_dial_negotiated: u32, + + /// State of the handler. + state: HandlerState, + + /// Try to negotiate the outbound upgrade a few times if there is an IO error before reporting the request as failed. + /// This keeps track of the number of attempts. + outbound_io_error_retries: u8, + + /// Waker, to be sure the handler gets polled when needed. + waker: Option, +} + +enum HandlerState { + /// The handler is active. All messages are sent and received. + Active, + /// The handler is shutting_down. + /// + /// While in this state the handler rejects new requests but tries to finish existing ones. + /// Once the timer expires, all messages are killed. + ShuttingDown(Box), + /// The handler is deactivated. A goodbye has been sent and no more messages are sent or + /// received. + Deactivated, +} + +/// Contains the information the handler keeps on established inbound substreams. +struct InboundInfo { + /// State of the substream. + state: InboundState, + /// Responses queued for sending. + pending_items: VecDeque, + /// Protocol of the original request we received from the peer. + protocol: Protocol, + /// Responses that the peer is still expecting from us. + remaining_chunks: u64, + /// Useful to timing how long each request took to process. Currently only used by + /// BlocksByRange. + request_start_time: Instant, + /// Key to keep track of the substream's timeout via `self.inbound_substreams_delay`. + delay_key: Option, +} + +/// Contains the information the handler keeps on established outbound substreams. +struct OutboundInfo { + /// State of the substream. + state: OutboundSubstreamState, + /// Key to keep track of the substream's timeout via `self.outbound_substreams_delay`. + delay_key: delay_queue::Key, + /// Info over the protocol this substream is handling. + proto: Protocol, + /// Number of chunks to be seen from the peer's response. + remaining_chunks: Option, + /// `Id` as given by the application that sent the request. + req_id: Id, +} + +/// State of an inbound substream connection. +enum InboundState { + /// The underlying substream is not being used. + Idle(InboundSubstream), + /// The underlying substream is processing responses. + /// The return value of the future is (substream, stream_was_closed). The stream_was_closed boolean + /// indicates if the stream was closed due to an error or successfully completing a response. + Busy(Pin> + Send>>), + /// Temporary state during processing + Poisoned, +} + +/// State of an outbound substream. Either waiting for a response, or in the process of sending. +pub enum OutboundSubstreamState { + /// A request has been sent, and we are awaiting a response. This future is driven in the + /// handler because GOODBYE requests can be handled and responses dropped instantly. + RequestPendingResponse { + /// The framed negotiated substream. + substream: Box>, + /// Keeps track of the actual request sent. + request: OutboundRequest, + }, + /// Closing an outbound substream> + Closing(Box>), + /// Temporary state during processing + Poisoned, +} + +impl RPCHandler { + pub fn new(listen_protocol: SubstreamProtocol) -> Self { + RPCHandler { + listen_protocol, + events_out: SmallVec::new(), + dial_queue: SmallVec::new(), + dial_negotiated: 0, + inbound_substreams: FnvHashMap::default(), + outbound_substreams: FnvHashMap::default(), + inbound_substreams_delay: DelayQueue::new(), + outbound_substreams_delay: DelayQueue::new(), + current_inbound_substream_id: SubstreamId(0), + current_outbound_substream_id: SubstreamId(0), + state: HandlerState::Active, + max_dial_negotiated: 8, + outbound_io_error_retries: 0, + waker: None, + } + } + + /// Initiates the handler's shutdown process, sending an optional Goodbye message to the + /// peer. + fn shutdown(&mut self, goodbye_reason: Option<(Id, GoodbyeReason)>) { + if matches!(self.state, HandlerState::Active) { + if !self.dial_queue.is_empty() { + debug!(unsent_queued_requests = %self.dial_queue.len(), "Starting handler shutdown"); + } + // We now drive to completion communications already dialed/established + while let Some((id, req)) = self.dial_queue.pop() { + self.events_out.push(Err(HandlerErr::Outbound { + error: RPCError::HandlerRejected, + proto: req.protocol(), + id, + })); + } + + // Queue our goodbye message. + if let Some((id, reason)) = goodbye_reason { + self.dial_queue.push((id, OutboundRequest::Goodbye(reason))); + } + + self.state = HandlerState::ShuttingDown(Box::new(sleep_until( + TInstant::now() + Duration::from_secs(SHUTDOWN_TIMEOUT_SECS as u64), + ))); + } + } + + /// Opens an outbound substream with a request. + fn send_request(&mut self, id: Id, req: OutboundRequest) { + match self.state { + HandlerState::Active => { + self.dial_queue.push((id, req)); + } + _ => self.events_out.push(Err(HandlerErr::Outbound { + error: RPCError::HandlerRejected, + proto: req.protocol(), + id, + })), + } + } + + /// Sends a response to a peer's request. + // NOTE: If the substream has closed due to inactivity, or the substream is in the + // wrong state a response will fail silently. + fn send_response(&mut self, inbound_id: SubstreamId, response: RPCCodedResponse) { + // check if the stream matching the response still exists + let inbound_info = if let Some(info) = self.inbound_substreams.get_mut(&inbound_id) { + info + } else { + if !matches!(response, RPCCodedResponse::StreamTermination(..)) { + // the stream is closed after sending the expected number of responses + trace!(response = %response, id = ?inbound_id, "Inbound stream has expired, response not sent"); + } + return; + }; + + // If the response we are sending is an error, report back for handling + if let RPCCodedResponse::Error(ref code, ref reason) = response { + self.events_out.push(Err(HandlerErr::Inbound { + error: RPCError::ErrorResponse(*code, reason.to_string()), + proto: inbound_info.protocol, + id: inbound_id, + })); + } + + if matches!(self.state, HandlerState::Deactivated) { + // we no longer send responses after the handler is deactivated + debug!(response = %response, id = ?inbound_id, "Response not sent. Deactivated handler"); + return; + } + inbound_info.pending_items.push_back(response); + } +} + +impl ConnectionHandler for RPCHandler +where + Id: ReqId, +{ + type InEvent = RPCSend; + type OutEvent = HandlerEvent; + type Error = RPCError; + type InboundProtocol = RPCProtocol; + type OutboundProtocol = OutboundRequestContainer; + type OutboundOpenInfo = (Id, OutboundRequest); // Keep track of the id and the request + type InboundOpenInfo = (); + + fn listen_protocol(&self) -> SubstreamProtocol { + self.listen_protocol.clone() + } + + fn inject_fully_negotiated_inbound( + &mut self, + substream: >::Output, + _info: Self::InboundOpenInfo, + ) { + // only accept new peer requests when active + if !matches!(self.state, HandlerState::Active) { + return; + } + + let (req, substream) = substream; + let expected_responses = req.expected_responses(); + + // store requests that expect responses + if expected_responses > 0 { + // Store the stream and tag the output. + let delay_key = self.inbound_substreams_delay.insert( + self.current_inbound_substream_id, + Duration::from_secs(RESPONSE_TIMEOUT), + ); + let awaiting_stream = InboundState::Idle(substream); + self.inbound_substreams.insert( + self.current_inbound_substream_id, + InboundInfo { + state: awaiting_stream, + pending_items: VecDeque::with_capacity(expected_responses as usize), + delay_key: Some(delay_key), + protocol: req.protocol(), + request_start_time: Instant::now(), + remaining_chunks: expected_responses, + }, + ); + } + + // If we received a goodbye, shutdown the connection. + if let InboundRequest::Goodbye(_) = req { + self.shutdown(None); + } + + self.events_out.push(Ok(RPCReceived::Request( + self.current_inbound_substream_id, + req, + ))); + self.current_inbound_substream_id.0 += 1; + } + + fn inject_fully_negotiated_outbound( + &mut self, + + out: >::Output, + request_info: Self::OutboundOpenInfo, + ) { + self.dial_negotiated -= 1; + let (id, request) = request_info; + let proto = request.protocol(); + + // accept outbound connections only if the handler is not deactivated + if matches!(self.state, HandlerState::Deactivated) { + self.events_out.push(Err(HandlerErr::Outbound { + error: RPCError::HandlerRejected, + proto, + id, + })); + } + + // add the stream to substreams if we expect a response, otherwise drop the stream. + let expected_responses = request.expected_responses(); + if expected_responses > 0 { + // new outbound request. Store the stream and tag the output. + let delay_key = self.outbound_substreams_delay.insert( + self.current_outbound_substream_id, + Duration::from_secs(RESPONSE_TIMEOUT), + ); + let awaiting_stream = OutboundSubstreamState::RequestPendingResponse { + substream: Box::new(out), + request, + }; + let expected_responses = if expected_responses > 1 { + // Currently enforced only for multiple responses + Some(expected_responses) + } else { + None + }; + if self + .outbound_substreams + .insert( + self.current_outbound_substream_id, + OutboundInfo { + state: awaiting_stream, + delay_key, + proto, + remaining_chunks: expected_responses, + req_id: id, + }, + ) + .is_some() + { + error!(id = ?self.current_outbound_substream_id, "Duplicate outbound substream id"); + } + self.current_outbound_substream_id.0 += 1; + } + } + + fn inject_event(&mut self, rpc_event: Self::InEvent) { + match rpc_event { + RPCSend::Request(id, req) => self.send_request(id, req), + RPCSend::Response(inbound_id, response) => self.send_response(inbound_id, response), + RPCSend::Shutdown(id, reason) => self.shutdown(Some((id, reason))), + } + // In any case, we need the handler to process the event. + if let Some(waker) = &self.waker { + waker.wake_by_ref(); + } + } + + fn inject_dial_upgrade_error( + &mut self, + request_info: Self::OutboundOpenInfo, + error: ConnectionHandlerUpgrErr< + >::Error, + >, + ) { + let (id, req) = request_info; + if let ConnectionHandlerUpgrErr::Upgrade(UpgradeError::Apply(RPCError::IoError(_))) = error + { + self.outbound_io_error_retries += 1; + if self.outbound_io_error_retries < IO_ERROR_RETRIES { + self.send_request(id, req); + return; + } + } + + // This dialing is now considered failed + self.dial_negotiated -= 1; + + self.outbound_io_error_retries = 0; + // map the error + let error = match error { + ConnectionHandlerUpgrErr::Timer => RPCError::InternalError("Timer failed"), + ConnectionHandlerUpgrErr::Timeout => RPCError::NegotiationTimeout, + ConnectionHandlerUpgrErr::Upgrade(UpgradeError::Apply(e)) => e, + ConnectionHandlerUpgrErr::Upgrade(UpgradeError::Select(NegotiationError::Failed)) => { + RPCError::UnsupportedProtocol + } + ConnectionHandlerUpgrErr::Upgrade(UpgradeError::Select( + NegotiationError::ProtocolError(e), + )) => match e { + ProtocolError::IoError(io_err) => RPCError::IoError(io_err.to_string()), + ProtocolError::InvalidProtocol => { + RPCError::InternalError("Protocol was deemed invalid") + } + ProtocolError::InvalidMessage | ProtocolError::TooManyProtocols => { + // Peer is sending invalid data during the negotiation phase, not + // participating in the protocol + RPCError::InvalidData("Invalid message during negotiation".to_string()) + } + }, + }; + self.events_out.push(Err(HandlerErr::Outbound { + error, + proto: req.protocol(), + id, + })); + } + + fn connection_keep_alive(&self) -> KeepAlive { + // Check that we don't have outbound items pending for dialing, nor dialing, nor + // established. Also check that there are no established inbound substreams. + // Errors and events need to be reported back, so check those too. + let should_shutdown = match self.state { + HandlerState::ShuttingDown(_) => { + self.dial_queue.is_empty() + && self.outbound_substreams.is_empty() + && self.inbound_substreams.is_empty() + && self.events_out.is_empty() + && self.dial_negotiated == 0 + } + HandlerState::Deactivated => { + // Regardless of events, the timeout has expired. Force the disconnect. + true + } + _ => false, + }; + if should_shutdown { + KeepAlive::No + } else { + KeepAlive::Yes + } + } + + fn poll( + &mut self, + cx: &mut Context<'_>, + ) -> Poll< + ConnectionHandlerEvent< + Self::OutboundProtocol, + Self::OutboundOpenInfo, + Self::OutEvent, + Self::Error, + >, + > { + if let Some(waker) = &self.waker { + if waker.will_wake(cx.waker()) { + self.waker = Some(cx.waker().clone()); + } + } else { + self.waker = Some(cx.waker().clone()); + } + // return any events that need to be reported + if !self.events_out.is_empty() { + return Poll::Ready(ConnectionHandlerEvent::Custom(self.events_out.remove(0))); + } else { + self.events_out.shrink_to_fit(); + } + + // Check if we are shutting down, and if the timer ran out + if let HandlerState::ShuttingDown(delay) = &self.state { + if delay.is_elapsed() { + self.state = HandlerState::Deactivated; + debug!("Handler deactivated"); + return Poll::Ready(ConnectionHandlerEvent::Close(RPCError::InternalError( + "Shutdown timeout", + ))); + } + } + + // purge expired inbound substreams and send an error + loop { + match self.inbound_substreams_delay.poll_expired(cx) { + Poll::Ready(Some(Ok(inbound_id))) => { + // handle a stream timeout for various states + if let Some(info) = self.inbound_substreams.get_mut(inbound_id.get_ref()) { + // the delay has been removed + info.delay_key = None; + self.events_out.push(Err(HandlerErr::Inbound { + error: RPCError::StreamTimeout, + proto: info.protocol, + id: *inbound_id.get_ref(), + })); + + if info.pending_items.back().map(|l| l.close_after()) == Some(false) { + // if the last chunk does not close the stream, append an error + info.pending_items.push_back(RPCCodedResponse::Error( + RPCResponseErrorCode::ServerError, + "Request timed out".into(), + )); + } + } + } + Poll::Ready(Some(Err(e))) => { + warn!(error = ?e, "Inbound substream poll failed"); + // drops the peer if we cannot read the delay queue + return Poll::Ready(ConnectionHandlerEvent::Close(RPCError::InternalError( + "Could not poll inbound stream timer", + ))); + } + Poll::Pending | Poll::Ready(None) => break, + } + } + + // purge expired outbound substreams + loop { + match self.outbound_substreams_delay.poll_expired(cx) { + Poll::Ready(Some(Ok(outbound_id))) => { + if let Some(OutboundInfo { proto, req_id, .. }) = + self.outbound_substreams.remove(outbound_id.get_ref()) + { + let outbound_err = HandlerErr::Outbound { + id: req_id, + proto, + error: RPCError::StreamTimeout, + }; + // notify the user + return Poll::Ready(ConnectionHandlerEvent::Custom(Err(outbound_err))); + } else { + error!(stream_id = ?outbound_id.get_ref(), "timed out substream not in the books"); + } + } + Poll::Ready(Some(Err(e))) => { + warn!(error = ?e, "Outbound substream poll failed"); + return Poll::Ready(ConnectionHandlerEvent::Close(RPCError::InternalError( + "Could not poll outbound stream timer", + ))); + } + Poll::Pending | Poll::Ready(None) => break, + } + } + + // when deactivated, close all streams + let deactivated = matches!(self.state, HandlerState::Deactivated); + + // drive inbound streams that need to be processed + let mut substreams_to_remove = Vec::new(); // Closed substreams that need to be removed + for (id, info) in self.inbound_substreams.iter_mut() { + loop { + match std::mem::replace(&mut info.state, InboundState::Poisoned) { + // This state indicates that we are not currently sending any messages to the + // peer. We need to check if there are messages to send, if so, start the + // sending process. + InboundState::Idle(substream) if !deactivated => { + // Process one more message if one exists. + if let Some(message) = info.pending_items.pop_front() { + // If this is the last chunk, terminate the stream. + let last_chunk = info.remaining_chunks <= 1; + let fut = + send_message_to_inbound_substream(substream, message, last_chunk) + .boxed(); + // Update the state and try to process this further. + info.state = InboundState::Busy(Box::pin(fut)); + } else { + // There is nothing left to process. Set the stream to idle and + // move on to the next one. + info.state = InboundState::Idle(substream); + break; + } + } + // This state indicates we are not sending at the moment, and the handler is in + // the process of closing the connection to the peer. + InboundState::Idle(mut substream) => { + // Handler is deactivated, close the stream and mark it for removal + match substream.close().poll_unpin(cx) { + // if we can't close right now, put the substream back and try again + // immediately, continue to do this until we close the substream. + Poll::Pending => info.state = InboundState::Idle(substream), + Poll::Ready(res) => { + // The substream closed, we remove it from the mapping and remove + // the timeout + substreams_to_remove.push(*id); + if let Some(ref delay_key) = info.delay_key { + self.inbound_substreams_delay.remove(delay_key); + } + // If there was an error in shutting down the substream report the + // error + if let Err(error) = res { + self.events_out.push(Err(HandlerErr::Inbound { + error, + proto: info.protocol, + id: *id, + })); + } + // If there are still requests to send, report that we are in the + // process of closing a connection to the peer and that we are not + // processing these excess requests. + if info.pending_items.back().map(|l| l.close_after()) == Some(false) + { + // if the request was still active, report back to cancel it + self.events_out.push(Err(HandlerErr::Inbound { + error: RPCError::HandlerRejected, + proto: info.protocol, + id: *id, + })); + } + } + } + break; + } + // This state indicates that there are messages to send back to the peer. + // The future here is built by the `process_inbound_substream` function. The + // output returns a substream and whether it was closed in this operation. + InboundState::Busy(mut fut) => { + // Check if the future has completed (i.e we have completed sending all our + // pending items) + match fut.poll_unpin(cx) { + // The pending messages have been sent successfully + Poll::Ready(Ok((substream, substream_was_closed))) + if !substream_was_closed => + { + // The substream is still active, decrement the remaining + // chunks expected. + info.remaining_chunks = info.remaining_chunks.saturating_sub(1); + + // If this substream has not ended, we reset the timer. + // Each chunk is allowed RESPONSE_TIMEOUT to be sent. + if let Some(ref delay_key) = info.delay_key { + self.inbound_substreams_delay + .reset(delay_key, Duration::from_secs(RESPONSE_TIMEOUT)); + } + + // The stream may be currently idle. Attempt to process more + // elements + if !deactivated && !info.pending_items.is_empty() { + // Process one more message if one exists. + if let Some(message) = info.pending_items.pop_front() { + // If this is the last chunk, terminate the stream. + let last_chunk = info.remaining_chunks <= 1; + let fut = send_message_to_inbound_substream( + substream, message, last_chunk, + ) + .boxed(); + // Update the state and try to process this further. + info.state = InboundState::Busy(Box::pin(fut)); + } + } else { + // There is nothing left to process. Set the stream to idle and + // move on to the next one. + info.state = InboundState::Idle(substream); + break; + } + } + // The pending messages have been sent successfully and the stream has + // terminated + Poll::Ready(Ok((_substream, _substream_was_closed))) => { + // The substream has closed. Remove the timeout related to the + // substream. + substreams_to_remove.push(*id); + if let Some(ref delay_key) = info.delay_key { + self.inbound_substreams_delay.remove(delay_key); + } + + // BlocksByRange is the one that typically consumes the most time. + // Its useful to log when the request was completed. + if matches!(info.protocol, Protocol::DataByHash) { + debug!(duration = %Instant::now().duration_since(info.request_start_time).as_secs(), "DataByHash Response sent"); + } + + // There is nothing more to process on this substream as it has + // been closed. Move on to the next one. + break; + } + // An error occurred when trying to send a response. + // This means we terminate the substream. + Poll::Ready(Err(error)) => { + // Remove the stream timeout from the mapping + substreams_to_remove.push(*id); + if let Some(ref delay_key) = info.delay_key { + self.inbound_substreams_delay.remove(delay_key); + } + // Report the error that occurred during the send process + self.events_out.push(Err(HandlerErr::Inbound { + error, + proto: info.protocol, + id: *id, + })); + + if matches!(info.protocol, Protocol::DataByHash) { + debug!(duration = %info.request_start_time.elapsed().as_secs(), "DataByHash Response failed"); + } + break; + } + // The sending future has not completed. Leave the state as busy and + // try to progress later. + Poll::Pending => { + info.state = InboundState::Busy(fut); + break; + } + }; + } + InboundState::Poisoned => { + unreachable!("Poisoned inbound substream") + } + } + } + } + + // Remove closed substreams + for inbound_id in substreams_to_remove { + self.inbound_substreams.remove(&inbound_id); + } + + // drive outbound streams that need to be processed + for outbound_id in self.outbound_substreams.keys().copied().collect::>() { + // get the state and mark it as poisoned + let (mut entry, state) = match self.outbound_substreams.entry(outbound_id) { + Entry::Occupied(mut entry) => { + let state = std::mem::replace( + &mut entry.get_mut().state, + OutboundSubstreamState::Poisoned, + ); + (entry, state) + } + Entry::Vacant(_) => unreachable!(), + }; + + match state { + OutboundSubstreamState::RequestPendingResponse { + substream, + request: _, + } if deactivated => { + // the handler is deactivated. Close the stream + entry.get_mut().state = OutboundSubstreamState::Closing(substream); + self.events_out.push(Err(HandlerErr::Outbound { + error: RPCError::HandlerRejected, + proto: entry.get().proto, + id: entry.get().req_id, + })) + } + OutboundSubstreamState::RequestPendingResponse { + mut substream, + request, + } => match substream.poll_next_unpin(cx) { + Poll::Ready(Some(Ok(response))) => { + if request.expected_responses() > 1 && !response.close_after() { + let substream_entry = entry.get_mut(); + let delay_key = &substream_entry.delay_key; + // chunks left after this one + let remaining_chunks = substream_entry + .remaining_chunks + .map(|count| count.saturating_sub(1)) + .unwrap_or_else(|| 0); + if remaining_chunks == 0 { + // this is the last expected message, close the stream as all expected chunks have been received + substream_entry.state = OutboundSubstreamState::Closing(substream); + } else { + // If the response chunk was expected update the remaining number of chunks expected and reset the Timeout + substream_entry.state = + OutboundSubstreamState::RequestPendingResponse { + substream, + request, + }; + substream_entry.remaining_chunks = Some(remaining_chunks); + self.outbound_substreams_delay + .reset(delay_key, Duration::from_secs(RESPONSE_TIMEOUT)); + } + } else { + // either this is a single response request or this response closes the + // stream + entry.get_mut().state = OutboundSubstreamState::Closing(substream); + } + + // Check what type of response we got and report it accordingly + let id = entry.get().req_id; + let proto = entry.get().proto; + + let received = match response { + RPCCodedResponse::StreamTermination(t) => { + Ok(RPCReceived::EndOfStream(id, t)) + } + RPCCodedResponse::Success(resp) => Ok(RPCReceived::Response(id, resp)), + RPCCodedResponse::Error(ref code, ref r) => Err(HandlerErr::Outbound { + id, + proto, + error: RPCError::ErrorResponse(*code, r.to_string()), + }), + }; + + return Poll::Ready(ConnectionHandlerEvent::Custom(received)); + } + Poll::Ready(None) => { + // stream closed + // if we expected multiple streams send a stream termination, + // else report the stream terminating only. + //trace!(self.log, "RPC Response - stream closed by remote"); + // drop the stream + let delay_key = &entry.get().delay_key; + let request_id = entry.get().req_id; + self.outbound_substreams_delay.remove(delay_key); + entry.remove_entry(); + // notify the application error + if request.expected_responses() > 1 { + // return an end of stream result + return Poll::Ready(ConnectionHandlerEvent::Custom(Ok( + RPCReceived::EndOfStream(request_id, request.stream_termination()), + ))); + } + + // else we return an error, stream should not have closed early. + let outbound_err = HandlerErr::Outbound { + id: request_id, + proto: request.protocol(), + error: RPCError::IncompleteStream, + }; + return Poll::Ready(ConnectionHandlerEvent::Custom(Err(outbound_err))); + } + Poll::Pending => { + entry.get_mut().state = + OutboundSubstreamState::RequestPendingResponse { substream, request } + } + Poll::Ready(Some(Err(e))) => { + // drop the stream + let delay_key = &entry.get().delay_key; + self.outbound_substreams_delay.remove(delay_key); + let outbound_err = HandlerErr::Outbound { + id: entry.get().req_id, + proto: entry.get().proto, + error: e, + }; + entry.remove_entry(); + return Poll::Ready(ConnectionHandlerEvent::Custom(Err(outbound_err))); + } + }, + OutboundSubstreamState::Closing(mut substream) => { + match Sink::poll_close(Pin::new(&mut substream), cx) { + Poll::Ready(_) => { + // drop the stream and its corresponding timeout + let delay_key = &entry.get().delay_key; + let protocol = entry.get().proto; + let request_id = entry.get().req_id; + self.outbound_substreams_delay.remove(delay_key); + entry.remove_entry(); + + // report the stream termination to the user + // + // Streams can be terminated here if a responder tries to + // continue sending responses beyond what we would expect. Here + // we simply terminate the stream and report a stream + // termination to the application + let termination = match protocol { + Protocol::DataByHash => Some(ResponseTermination::DataByHash), + _ => None, // all other protocols are do not have multiple responses and we do not inform the user, we simply drop the stream. + }; + + if let Some(termination) = termination { + return Poll::Ready(ConnectionHandlerEvent::Custom(Ok( + RPCReceived::EndOfStream(request_id, termination), + ))); + } + } + Poll::Pending => { + entry.get_mut().state = OutboundSubstreamState::Closing(substream); + } + } + } + OutboundSubstreamState::Poisoned => { + error!("Poisoned outbound substream"); + unreachable!("Coding Error: Outbound substream is poisoned") + } + } + } + + // establish outbound substreams + if !self.dial_queue.is_empty() && self.dial_negotiated < self.max_dial_negotiated { + self.dial_negotiated += 1; + let (id, req) = self.dial_queue.remove(0); + self.dial_queue.shrink_to_fit(); + return Poll::Ready(ConnectionHandlerEvent::OutboundSubstreamRequest { + protocol: SubstreamProtocol::new( + OutboundRequestContainer { + req: req.clone(), + max_rpc_size: max_rpc_size(), + }, + (), + ) + .map_info(|()| (id, req)), + }); + } + + // Check if we have completed sending a goodbye, disconnect. + if let HandlerState::ShuttingDown(_) = self.state { + if self.dial_queue.is_empty() + && self.outbound_substreams.is_empty() + && self.inbound_substreams.is_empty() + && self.events_out.is_empty() + && self.dial_negotiated == 0 + { + return Poll::Ready(ConnectionHandlerEvent::Close(RPCError::Disconnected)); + } + } + + Poll::Pending + } +} + +/// Creates a future that can be polled that will send any queued message to the peer. +/// +/// This function returns the given substream, along with whether it has been closed or not. Any +/// error that occurred with sending a message is reported also. +async fn send_message_to_inbound_substream( + mut substream: InboundSubstream, + message: RPCCodedResponse, + last_chunk: bool, +) -> Result<(InboundSubstream, bool), RPCError> { + if matches!(message, RPCCodedResponse::StreamTermination(_)) { + substream.close().await.map(|_| (substream, true)) + } else { + // chunks that are not stream terminations get sent, and the stream is closed if + // the response is an error + let is_error = matches!(message, RPCCodedResponse::Error(..)); + + let send_result = substream.send(message).await; + + // If we need to close the substream, do so and return the result. + if last_chunk || is_error || send_result.is_err() { + let close_result = substream.close().await.map(|_| (substream, true)); + // If there was an error in sending, return this error, otherwise, return the + // result of closing the substream. + return send_result.and(close_result); + } + // Everything worked as expected return the result. + send_result.map(|_| (substream, false)) + } +} diff --git a/node/network/src/rpc/methods.rs b/node/network/src/rpc/methods.rs new file mode 100644 index 0000000..c2f2777 --- /dev/null +++ b/node/network/src/rpc/methods.rs @@ -0,0 +1,367 @@ +//! Available RPC methods types and ids. + +use regex::bytes::Regex; +use ssz_derive::{Decode, Encode}; +use ssz_types::{ + typenum::{U1024, U256}, + VariableList, +}; +use std::ops::Deref; +use strum::IntoStaticStr; +pub type Hash256 = ethereum_types::H256; +use shared_types::{ChunkArrayWithProof, TxID}; + +pub use ssz_types::{typenum, typenum::Unsigned, BitList, BitVector, FixedVector}; + +/// Maximum number of blocks in a single request. +pub type MaxRequestBlocks = U1024; +pub const MAX_REQUEST_BLOCKS: u64 = 1024; + +/// Maximum length of error message. +pub type MaxErrorLen = U256; +pub const MAX_ERROR_LEN: u64 = 256; + +/// Maximum length of data message. +pub type MaxDataLen = U256; +pub const MAX_DATA_LEN: u64 = 256; + +// Maximum length of GetChunksResponse chunk data. +pub const MAX_CHUNKS_LENGTH: usize = 10 * 1024 * 1024; // 10M + +#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)] +pub struct ZgsData { + pub hash: Hash256, +} + +/// Wrapper over SSZ List to represent error message in rpc responses. +#[derive(Debug, Clone)] +pub struct ErrorType(pub VariableList); + +impl From for ErrorType { + fn from(s: String) -> Self { + Self(VariableList::from(s.as_bytes().to_vec())) + } +} + +impl From<&str> for ErrorType { + fn from(s: &str) -> Self { + Self(VariableList::from(s.as_bytes().to_vec())) + } +} + +impl Deref for ErrorType { + type Target = VariableList; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ToString for ErrorType { + fn to_string(&self) -> String { + #[allow(clippy::invalid_regex)] + let re = Regex::new("\\p{C}").expect("Regex is valid"); + String::from_utf8_lossy(&re.replace_all(self.0.deref(), &b""[..])).to_string() + } +} + +/* Request/Response data structures for RPC methods */ + +/* Requests */ + +/// The STATUS request/response handshake message. +#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)] +pub struct StatusMessage { + pub data: u64, +} + +/// The PING request/response message. +#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)] +pub struct Ping { + /// The metadata sequence number. + pub data: u64, +} + +/// The reason given for a `Goodbye` message. +/// +/// Note: any unknown `u64::into(n)` will resolve to `Goodbye::Unknown` for any unknown `n`, +/// however `GoodbyeReason::Unknown.into()` will go into `0_u64`. Therefore de-serializing then +/// re-serializing may not return the same bytes. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GoodbyeReason { + /// This node has shutdown. + ClientShutdown = 1, + + /// Incompatible networks. + IrrelevantNetwork = 2, + + /// Error/fault in the RPC. + Fault = 3, + + /// Teku uses this code for not being able to verify a network. + UnableToVerifyNetwork = 128, + + /// The node has too many connected peers. + TooManyPeers = 129, + + /// Scored poorly. + BadScore = 250, + + /// The peer is banned + Banned = 251, + + /// The IP address the peer is using is banned. + BannedIP = 252, + + /// Unknown reason. + Unknown = 0, +} + +impl From for GoodbyeReason { + fn from(id: u64) -> GoodbyeReason { + match id { + 1 => GoodbyeReason::ClientShutdown, + 2 => GoodbyeReason::IrrelevantNetwork, + 3 => GoodbyeReason::Fault, + 128 => GoodbyeReason::UnableToVerifyNetwork, + 129 => GoodbyeReason::TooManyPeers, + 250 => GoodbyeReason::BadScore, + 251 => GoodbyeReason::Banned, + 252 => GoodbyeReason::BannedIP, + _ => GoodbyeReason::Unknown, + } + } +} + +impl From for u64 { + fn from(reason: GoodbyeReason) -> u64 { + reason as u64 + } +} + +impl ssz::Encode for GoodbyeReason { + fn is_ssz_fixed_len() -> bool { + ::is_ssz_fixed_len() + } + + fn ssz_fixed_len() -> usize { + ::ssz_fixed_len() + } + + fn ssz_bytes_len(&self) -> usize { + 0_u64.ssz_bytes_len() + } + + fn ssz_append(&self, buf: &mut Vec) { + let conv: u64 = self.clone().into(); + conv.ssz_append(buf) + } +} + +impl ssz::Decode for GoodbyeReason { + fn is_ssz_fixed_len() -> bool { + ::is_ssz_fixed_len() + } + + fn ssz_fixed_len() -> usize { + ::ssz_fixed_len() + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + u64::from_ssz_bytes(bytes).map(|n| n.into()) + } +} + +/// Request a number of beacon block bodies from a peer. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DataByHashRequest { + /// The list of beacon block bodies being requested. + pub hashes: VariableList, +} + +/// Request a chunk array from a peer. +#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)] +pub struct GetChunksRequest { + pub tx_id: TxID, + pub index_start: u64, + pub index_end: u64, +} + +/* RPC Handling and Grouping */ +// Collection of enums and structs used by the Codecs to encode/decode RPC messages + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RPCResponse { + /// A HELLO message. + Status(StatusMessage), + + /// A PONG response to a PING request. + Pong(Ping), + + /// A response to a get DATA_BY_HASH request. + DataByHash(Box), + + /// A response to a GET_CHUNKS request. + Chunks(ChunkArrayWithProof), +} + +/// Indicates which response is being terminated by a stream termination response. +#[derive(Debug, Clone)] +pub enum ResponseTermination { + /// Data by hash stream termination. + DataByHash, +} + +/// The structured response containing a result/code indicating success or failure +/// and the contents of the response +#[derive(Debug)] +pub enum RPCCodedResponse { + /// The response is a successful. + Success(RPCResponse), + + Error(RPCResponseErrorCode, ErrorType), + + /// Received a stream termination indicating which response is being terminated. + StreamTermination(ResponseTermination), +} + +/// The code assigned to an erroneous `RPCResponse`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoStaticStr)] +#[strum(serialize_all = "snake_case")] +pub enum RPCResponseErrorCode { + RateLimited, + InvalidRequest, + ServerError, + /// Error spec'd to indicate that a peer does not have blocks on a requested range. + ResourceUnavailable, + Unknown, +} + +impl RPCCodedResponse { + /// Used to encode the response in the codec. + pub fn as_u8(&self) -> Option { + match self { + RPCCodedResponse::Success(_) => Some(0), + RPCCodedResponse::Error(code, _) => Some(code.as_u8()), + RPCCodedResponse::StreamTermination(_) => None, + } + } + + /// Tells the codec whether to decode as an RPCResponse or an error. + pub fn is_response(response_code: u8) -> bool { + matches!(response_code, 0) + } + + /// Builds an RPCCodedResponse from a response code and an ErrorMessage + pub fn from_error(response_code: u8, err: ErrorType) -> Self { + let code = match response_code { + 1 => RPCResponseErrorCode::InvalidRequest, + 2 => RPCResponseErrorCode::ServerError, + 3 => RPCResponseErrorCode::ResourceUnavailable, + 139 => RPCResponseErrorCode::RateLimited, + _ => RPCResponseErrorCode::Unknown, + }; + RPCCodedResponse::Error(code, err) + } + + /// Specifies which response allows for multiple chunks for the stream handler. + pub fn multiple_responses(&self) -> bool { + match self { + RPCCodedResponse::Success(resp) => match resp { + RPCResponse::Status(_) => false, + RPCResponse::Pong(_) => false, + RPCResponse::DataByHash(_) => true, + RPCResponse::Chunks(_) => false, + }, + RPCCodedResponse::Error(_, _) => true, + // Stream terminations are part of responses that have chunks + RPCCodedResponse::StreamTermination(_) => true, + } + } + + /// Returns true if this response always terminates the stream. + pub fn close_after(&self) -> bool { + !matches!(self, RPCCodedResponse::Success(_)) + } +} + +impl RPCResponseErrorCode { + fn as_u8(&self) -> u8 { + match self { + RPCResponseErrorCode::InvalidRequest => 1, + RPCResponseErrorCode::ServerError => 2, + RPCResponseErrorCode::ResourceUnavailable => 3, + RPCResponseErrorCode::Unknown => 255, + RPCResponseErrorCode::RateLimited => 139, + } + } +} + +impl std::fmt::Display for RPCResponseErrorCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let repr = match self { + RPCResponseErrorCode::InvalidRequest => "The request was invalid", + RPCResponseErrorCode::ResourceUnavailable => "Resource unavailable", + RPCResponseErrorCode::ServerError => "Server error occurred", + RPCResponseErrorCode::Unknown => "Unknown error occurred", + RPCResponseErrorCode::RateLimited => "Rate limited", + }; + f.write_str(repr) + } +} + +impl std::fmt::Display for StatusMessage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Status Message: Data: {:?}", self.data) + } +} + +impl std::fmt::Display for RPCResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RPCResponse::Status(status) => write!(f, "{}", status), + RPCResponse::Pong(ping) => write!(f, "Pong: {}", ping.data), + RPCResponse::DataByHash(data) => { + write!(f, "DataByHash: Hash: {:?}", data.hash) + } + RPCResponse::Chunks(data) => { + write!( + f, + "Chunks Response, data length: {}", + data.chunks.data.len() + ) + } + } + } +} + +impl std::fmt::Display for RPCCodedResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RPCCodedResponse::Success(res) => write!(f, "{}", res), + RPCCodedResponse::Error(code, err) => { + write!(f, "{}: {}", code, err.to_string()) + } + RPCCodedResponse::StreamTermination(_) => { + write!(f, "Stream Termination") + } + } + } +} + +impl std::fmt::Display for GoodbyeReason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GoodbyeReason::ClientShutdown => write!(f, "Client Shutdown"), + GoodbyeReason::IrrelevantNetwork => write!(f, "Irrelevant Network"), + GoodbyeReason::Fault => write!(f, "Fault"), + GoodbyeReason::UnableToVerifyNetwork => { + write!(f, "Unable to verify network") + } + GoodbyeReason::TooManyPeers => write!(f, "Too many peers"), + GoodbyeReason::BadScore => write!(f, "Bad Score"), + GoodbyeReason::Banned => write!(f, "Banned"), + GoodbyeReason::BannedIP => write!(f, "BannedIP"), + GoodbyeReason::Unknown => write!(f, "Unknown Reason"), + } + } +} diff --git a/node/network/src/rpc/mod.rs b/node/network/src/rpc/mod.rs new file mode 100644 index 0000000..51552d4 --- /dev/null +++ b/node/network/src/rpc/mod.rs @@ -0,0 +1,260 @@ +//! The Ethereum 2.0 Wire Protocol +//! +//! This protocol is a purpose built Ethereum 2.0 libp2p protocol. It's role is to facilitate +//! direct peer-to-peer communication primarily for sending/receiving chain information for +//! syncing. + +use futures::future::FutureExt; +use handler::{HandlerEvent, RPCHandler}; +use libp2p::core::connection::ConnectionId; +use libp2p::swarm::{ + handler::ConnectionHandler, NetworkBehaviour, NetworkBehaviourAction, NotifyHandler, + PollParameters, SubstreamProtocol, +}; +use libp2p::PeerId; +use rate_limiter::{RPCRateLimiter as RateLimiter, RPCRateLimiterBuilder, RateLimitedErr}; +use std::task::{Context, Poll}; +use std::time::Duration; + +pub(crate) use handler::HandlerErr; +pub(crate) use methods::{Ping, RPCCodedResponse, RPCResponse}; +pub(crate) use protocol::{InboundRequest, RPCProtocol}; + +pub use handler::SubstreamId; +pub use methods::{ + DataByHashRequest, GetChunksRequest, GoodbyeReason, MaxRequestBlocks, ZgsData, + RPCResponseErrorCode, ResponseTermination, StatusMessage, MAX_REQUEST_BLOCKS, +}; +pub(crate) use outbound::OutboundRequest; +pub use protocol::{max_rpc_size, Protocol, RPCError}; + +pub(crate) mod codec; +mod handler; +pub mod methods; +mod outbound; +mod protocol; +mod rate_limiter; + +/// Composite trait for a request id. +pub trait ReqId: Send + 'static + std::fmt::Debug + Copy + Clone {} +impl ReqId for T where T: Send + 'static + std::fmt::Debug + Copy + Clone {} + +/// RPC events sent from Lighthouse. +#[derive(Debug)] +pub enum RPCSend { + /// A request sent from Lighthouse. + /// + /// The `Id` is given by the application making the request. These + /// go over *outbound* connections. + Request(Id, OutboundRequest), + /// A response sent from Lighthouse. + /// + /// The `SubstreamId` must correspond to the RPC-given ID of the original request received from the + /// peer. The second parameter is a single chunk of a response. These go over *inbound* + /// connections. + Response(SubstreamId, RPCCodedResponse), + /// Lighthouse has requested to terminate the connection with a goodbye message. + Shutdown(Id, GoodbyeReason), +} + +/// RPC events received from outside Lighthouse. +#[derive(Debug)] +pub enum RPCReceived { + /// A request received from the outside. + /// + /// The `SubstreamId` is given by the `RPCHandler` as it identifies this request with the + /// *inbound* substream over which it is managed. + Request(SubstreamId, InboundRequest), + /// A response received from the outside. + /// + /// The `Id` corresponds to the application given ID of the original request sent to the + /// peer. The second parameter is a single chunk of a response. These go over *outbound* + /// connections. + Response(Id, RPCResponse), + /// Marks a request as completed + EndOfStream(Id, ResponseTermination), +} + +impl std::fmt::Display for RPCSend { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RPCSend::Request(id, req) => { + write!(f, "RPC Request(id: {:?}, {})", id, req) + } + RPCSend::Response(id, res) => { + write!(f, "RPC Response(id: {:?}, {})", id, res) + } + RPCSend::Shutdown(_id, reason) => { + write!(f, "Sending Goodbye: {}", reason) + } + } + } +} + +/// Messages sent to the user from the RPC protocol. +pub struct RPCMessage { + /// The peer that sent the message. + pub peer_id: PeerId, + /// Handler managing this message. + pub conn_id: ConnectionId, + /// The message that was sent. + pub event: HandlerEvent, +} + +/// Implements the libp2p `NetworkBehaviour` trait and therefore manages network-level +/// logic. +pub struct RPC { + /// Rate limiter + limiter: RateLimiter, + /// Queue of events to be processed. + events: Vec, RPCHandler>>, +} + +impl RPC { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + let limiter = RPCRateLimiterBuilder::new() + .n_every(Protocol::Ping, 2, Duration::from_secs(10)) + .n_every(Protocol::Status, 5, Duration::from_secs(15)) + .one_every(Protocol::Goodbye, Duration::from_secs(10)) + .n_every(Protocol::DataByHash, 128, Duration::from_secs(10)) + .n_every(Protocol::GetChunks, 4096, Duration::from_secs(10)) + .build() + .expect("Configuration parameters are valid"); + RPC { + limiter, + events: Vec::new(), + } + } + + /// Sends an RPC response. + /// + /// The peer must be connected for this to succeed. + pub fn send_response( + &mut self, + peer_id: PeerId, + id: (ConnectionId, SubstreamId), + event: RPCCodedResponse, + ) { + self.events.push(NetworkBehaviourAction::NotifyHandler { + peer_id, + handler: NotifyHandler::One(id.0), + event: RPCSend::Response(id.1, event), + }); + } + + /// Submits an RPC request. + /// + /// The peer must be connected for this to succeed. + pub fn send_request(&mut self, peer_id: PeerId, request_id: Id, event: OutboundRequest) { + self.events.push(NetworkBehaviourAction::NotifyHandler { + peer_id, + handler: NotifyHandler::Any, + event: RPCSend::Request(request_id, event), + }); + } + + /// Lighthouse wishes to disconnect from this peer by sending a Goodbye message. This + /// gracefully terminates the RPC behaviour with a goodbye message. + pub fn shutdown(&mut self, peer_id: PeerId, id: Id, reason: GoodbyeReason) { + self.events.push(NetworkBehaviourAction::NotifyHandler { + peer_id, + handler: NotifyHandler::Any, + event: RPCSend::Shutdown(id, reason), + }); + } +} + +impl NetworkBehaviour for RPC +where + Id: ReqId, +{ + type ConnectionHandler = RPCHandler; + type OutEvent = RPCMessage; + + fn new_handler(&mut self) -> Self::ConnectionHandler { + RPCHandler::new(SubstreamProtocol::new( + RPCProtocol { + max_rpc_size: max_rpc_size(), + }, + (), + )) + } + + fn inject_event( + &mut self, + peer_id: PeerId, + conn_id: ConnectionId, + event: ::OutEvent, + ) { + if let Ok(RPCReceived::Request(ref id, ref req)) = event { + // check if the request is conformant to the quota + match self.limiter.allows(&peer_id, req) { + Ok(()) => { + // send the event to the user + self.events + .push(NetworkBehaviourAction::GenerateEvent(RPCMessage { + peer_id, + conn_id, + event, + })) + } + Err(RateLimitedErr::TooLarge) => { + // we set the batch sizes, so this is a coding/config err for most protocols + let protocol = req.protocol(); + error!(%protocol, "Request size too large to ever be processed"); + + // send an error code to the peer. + // the handler upon receiving the error code will send it back to the behaviour + self.send_response( + peer_id, + (conn_id, *id), + RPCCodedResponse::Error( + RPCResponseErrorCode::RateLimited, + "Rate limited. Request too large".into(), + ), + ); + } + Err(RateLimitedErr::TooSoon(wait_time)) => { + debug!( + request = %req, + %peer_id, + wait_time_ms = %wait_time.as_millis(), + "Request exceeds the rate limit", + ); + + // send an error code to the peer. + // the handler upon receiving the error code will send it back to the behaviour + self.send_response( + peer_id, + (conn_id, *id), + RPCCodedResponse::Error( + RPCResponseErrorCode::RateLimited, + format!("Wait {:?}", wait_time).into(), + ), + ); + } + } + } else { + self.events + .push(NetworkBehaviourAction::GenerateEvent(RPCMessage { + peer_id, + conn_id, + event, + })); + } + } + + fn poll( + &mut self, + cx: &mut Context, + _: &mut impl PollParameters, + ) -> Poll> { + // let the rate limiter prune + let _ = self.limiter.poll_unpin(cx); + if !self.events.is_empty() { + return Poll::Ready(self.events.remove(0)); + } + Poll::Pending + } +} diff --git a/node/network/src/rpc/outbound.rs b/node/network/src/rpc/outbound.rs new file mode 100644 index 0000000..30a6bdc --- /dev/null +++ b/node/network/src/rpc/outbound.rs @@ -0,0 +1,178 @@ +use super::methods::*; +use super::protocol::Protocol; +use super::protocol::ProtocolId; +use super::RPCError; +use crate::rpc::protocol::Encoding; +use crate::rpc::protocol::Version; +use crate::rpc::{ + codec::{base::BaseOutboundCodec, ssz_snappy::SSZSnappyOutboundCodec, OutboundCodec}, + methods::ResponseTermination, +}; +use futures::future::BoxFuture; +use futures::prelude::{AsyncRead, AsyncWrite}; +use futures::{FutureExt, SinkExt}; +use libp2p::core::{OutboundUpgrade, UpgradeInfo}; +use tokio_util::{ + codec::Framed, + compat::{Compat, FuturesAsyncReadCompatExt}, +}; + +/* Outbound request */ + +// Combines all the RPC requests into a single enum to implement `UpgradeInfo` and +// `OutboundUpgrade` + +#[derive(Debug, Clone)] +pub struct OutboundRequestContainer { + pub req: OutboundRequest, + pub max_rpc_size: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum OutboundRequest { + Status(StatusMessage), + Goodbye(GoodbyeReason), + Ping(Ping), + DataByHash(DataByHashRequest), + GetChunks(GetChunksRequest), +} + +impl UpgradeInfo for OutboundRequestContainer { + type Info = ProtocolId; + type InfoIter = Vec; + + // add further protocols as we support more encodings/versions + fn protocol_info(&self) -> Self::InfoIter { + self.req.supported_protocols() + } +} + +/// Implements the encoding per supported protocol for `RPCRequest`. +impl OutboundRequest { + pub fn supported_protocols(&self) -> Vec { + match self { + // add more protocols when versions/encodings are supported + OutboundRequest::Status(_) => vec![ProtocolId::new( + Protocol::Status, + Version::V1, + Encoding::SSZSnappy, + )], + OutboundRequest::Goodbye(_) => vec![ProtocolId::new( + Protocol::Goodbye, + Version::V1, + Encoding::SSZSnappy, + )], + OutboundRequest::Ping(_) => vec![ProtocolId::new( + Protocol::Ping, + Version::V1, + Encoding::SSZSnappy, + )], + OutboundRequest::DataByHash(_) => vec![ProtocolId::new( + Protocol::DataByHash, + Version::V1, + Encoding::SSZSnappy, + )], + OutboundRequest::GetChunks(_) => vec![ProtocolId::new( + Protocol::GetChunks, + Version::V1, + Encoding::SSZSnappy, + )], + } + } + + /* These functions are used in the handler for stream management */ + + /// Number of responses expected for this request. + pub fn expected_responses(&self) -> u64 { + match self { + OutboundRequest::Status(_) => 1, + OutboundRequest::Goodbye(_) => 0, + OutboundRequest::Ping(_) => 1, + OutboundRequest::DataByHash(req) => req.hashes.len() as u64, + OutboundRequest::GetChunks(_) => 1, + } + } + + /// Gives the corresponding `Protocol` to this request. + pub fn protocol(&self) -> Protocol { + match self { + OutboundRequest::Status(_) => Protocol::Status, + OutboundRequest::Goodbye(_) => Protocol::Goodbye, + OutboundRequest::Ping(_) => Protocol::Ping, + OutboundRequest::DataByHash(_) => Protocol::DataByHash, + OutboundRequest::GetChunks(_) => Protocol::GetChunks, + } + } + + /// Returns the `ResponseTermination` type associated with the request if a stream gets + /// terminated. + pub fn stream_termination(&self) -> ResponseTermination { + match self { + // this only gets called after `multiple_responses()` returns true. Therefore, only + // variants that have `multiple_responses()` can have values. + OutboundRequest::DataByHash(_) => ResponseTermination::DataByHash, + OutboundRequest::Status(_) => unreachable!(), + OutboundRequest::Goodbye(_) => unreachable!(), + OutboundRequest::Ping(_) => unreachable!(), + OutboundRequest::GetChunks(_) => unreachable!(), + } + } +} + +/* RPC Response type - used for outbound upgrades */ + +/* Outbound upgrades */ + +pub type OutboundFramed = Framed, OutboundCodec>; + +impl OutboundUpgrade for OutboundRequestContainer +where + TSocket: AsyncRead + AsyncWrite + Unpin + Send + 'static, +{ + type Output = OutboundFramed; + type Error = RPCError; + type Future = BoxFuture<'static, Result>; + + fn upgrade_outbound(self, socket: TSocket, protocol: Self::Info) -> Self::Future { + // convert to a tokio compatible socket + let socket = socket.compat(); + let codec = match protocol.encoding { + Encoding::SSZSnappy => { + let ssz_snappy_codec = BaseOutboundCodec::new(SSZSnappyOutboundCodec::new( + protocol, + self.max_rpc_size, + )); + OutboundCodec::SSZSnappy(ssz_snappy_codec) + } + }; + + let mut socket = Framed::new(socket, codec); + + async { + socket.send(self.req).await?; + socket.close().await?; + Ok(socket) + } + .boxed() + } +} + +impl std::fmt::Display for OutboundRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + OutboundRequest::Status(status) => { + write!(f, "Status Message: {}", status) + } + OutboundRequest::Goodbye(reason) => { + write!(f, "Goodbye: {}", reason) + } + OutboundRequest::Ping(ping) => write!(f, "Ping: {}", ping.data), + OutboundRequest::DataByHash(req) => { + write!(f, "Data by hash: {:?}", req) + } + OutboundRequest::GetChunks(req) => { + write!(f, "GetChunks: {:?}", req) + } + } + } +} diff --git a/node/network/src/rpc/protocol.rs b/node/network/src/rpc/protocol.rs new file mode 100644 index 0000000..53de8df --- /dev/null +++ b/node/network/src/rpc/protocol.rs @@ -0,0 +1,542 @@ +use super::methods::*; +use crate::rpc::{ + codec::{base::BaseInboundCodec, ssz_snappy::SSZSnappyInboundCodec, InboundCodec}, + methods::{MaxErrorLen, ResponseTermination, MAX_CHUNKS_LENGTH, MAX_ERROR_LEN}, + MaxRequestBlocks, MAX_REQUEST_BLOCKS, +}; +use futures::future::BoxFuture; +use futures::prelude::{AsyncRead, AsyncWrite}; +use futures::{FutureExt, StreamExt}; +use libp2p::core::{InboundUpgrade, ProtocolName, UpgradeInfo}; +use shared_types::{ChunkArray, ChunkArrayWithProof, FlowRangeProof}; +use ssz::Encode; +use ssz_types::VariableList; +use std::io; +use std::time::Duration; +use strum::IntoStaticStr; +use tokio_io_timeout::TimeoutStream; +use tokio_util::{ + codec::Framed, + compat::{Compat, FuturesAsyncReadCompatExt}, +}; + +pub type Hash256 = ethereum_types::H256; + +lazy_static! { + pub static ref DATA_BY_HASH_REQUEST_MIN: usize = + VariableList::::from(Vec::::new()) + .as_ssz_bytes() + .len(); + pub static ref DATA_BY_HASH_REQUEST_MAX: usize = + VariableList::::from(vec![ + Hash256::zero(); + MAX_REQUEST_BLOCKS as usize + ]) + .as_ssz_bytes() + .len(); + pub static ref ERROR_TYPE_MIN: usize = VariableList::::from(Vec::::new()) + .as_ssz_bytes() + .len(); + pub static ref ERROR_TYPE_MAX: usize = + VariableList::::from(vec![0u8; MAX_ERROR_LEN as usize]) + .as_ssz_bytes() + .len(); + pub static ref CHUNKS_RESPONSE_MIN: usize = ChunkArrayWithProof { + chunks: ChunkArray { + data: vec![], + start_index: 0, + }, + proof: FlowRangeProof::new_empty(), + } + .as_ssz_bytes() + .len(); + // TODO + pub static ref CHUNKS_RESPONSE_MAX: usize = ChunkArrayWithProof { + chunks: ChunkArray { + data: vec![0u8; MAX_CHUNKS_LENGTH], + start_index: 0, + }, + proof: FlowRangeProof::new_empty(), + } + .as_ssz_bytes() + .len(); +} + +// /// The maximum bytes that can be sent across the RPC pre-merge. +// pub(crate) const MAX_RPC_SIZE: usize = 1_048_576; // 1M +/// The maximum bytes that can be sent across the RPC post-merge. +pub(crate) const MAX_RPC_SIZE: usize = 10 * 1_048_576; // 10M +/// The protocol prefix the RPC protocol id. +const PROTOCOL_PREFIX: &str = "/zgs/req"; +/// Time allowed for the first byte of a request to arrive before we time out (Time To First Byte). +const TTFB_TIMEOUT: u64 = 5; +/// The number of seconds to wait for the first bytes of a request once a protocol has been +/// established before the stream is terminated. +const REQUEST_TIMEOUT: u64 = 15; + +/// Returns the maximum bytes that can be sent across the RPC. +pub fn max_rpc_size() -> usize { + MAX_RPC_SIZE +} + +/// Protocol names to be used. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Protocol { + /// The Status protocol name. + Status, + /// The Goodbye protocol name. + Goodbye, + /// The `Ping` protocol name. + Ping, + /// TODO + DataByHash, + + /// The Chunk sync protocol. + GetChunks, +} + +/// RPC Versions +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Version { + /// Version 1 of RPC + V1, +} + +/// RPC Encondings supported. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Encoding { + SSZSnappy, +} + +impl std::fmt::Display for Protocol { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let repr = match self { + Protocol::Status => "status", + Protocol::Goodbye => "goodbye", + Protocol::Ping => "ping", + Protocol::DataByHash => "data_by_hash", + Protocol::GetChunks => "get_chunks", + }; + f.write_str(repr) + } +} + +impl std::fmt::Display for Encoding { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let repr = match self { + Encoding::SSZSnappy => "ssz_snappy", + }; + f.write_str(repr) + } +} + +impl std::fmt::Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let repr = match self { + Version::V1 => "1", + }; + f.write_str(repr) + } +} + +#[derive(Debug, Clone)] +pub struct RPCProtocol { + pub max_rpc_size: usize, +} + +impl UpgradeInfo for RPCProtocol { + type Info = ProtocolId; + type InfoIter = Vec; + + /// The list of supported RPC protocols for Lighthouse. + fn protocol_info(&self) -> Self::InfoIter { + vec![ + ProtocolId::new(Protocol::Status, Version::V1, Encoding::SSZSnappy), + ProtocolId::new(Protocol::Goodbye, Version::V1, Encoding::SSZSnappy), + ProtocolId::new(Protocol::Ping, Version::V1, Encoding::SSZSnappy), + ProtocolId::new(Protocol::DataByHash, Version::V1, Encoding::SSZSnappy), + ProtocolId::new(Protocol::GetChunks, Version::V1, Encoding::SSZSnappy), + ] + } +} + +/// Represents the ssz length bounds for RPC messages. +#[derive(Debug, PartialEq, Eq)] +pub struct RpcLimits { + pub min: usize, + pub max: usize, +} + +impl RpcLimits { + pub fn new(min: usize, max: usize) -> Self { + Self { min, max } + } + + /// Returns true if the given length is greater than `max_rpc_size` or out of + /// bounds for the given ssz type, returns false otherwise. + pub fn is_out_of_bounds(&self, length: usize, max_rpc_size: usize) -> bool { + length > std::cmp::min(self.max, max_rpc_size) || length < self.min + } +} + +/// Tracks the types in a protocol id. +#[derive(Clone, Debug)] +pub struct ProtocolId { + /// The RPC message type/name. + pub message_name: Protocol, + + /// The version of the RPC. + pub version: Version, + + /// The encoding of the RPC. + pub encoding: Encoding, + + /// The protocol id that is formed from the above fields. + protocol_id: String, +} + +impl ProtocolId { + /// Returns min and max size for messages of given protocol id requests. + pub fn rpc_request_limits(&self) -> RpcLimits { + match self.message_name { + Protocol::Status => RpcLimits::new( + ::ssz_fixed_len(), + ::ssz_fixed_len(), + ), + Protocol::Goodbye => RpcLimits::new( + ::ssz_fixed_len(), + ::ssz_fixed_len(), + ), + Protocol::Ping => RpcLimits::new( + ::ssz_fixed_len(), + ::ssz_fixed_len(), + ), + Protocol::DataByHash => { + // RpcLimits::new(*DATA_BY_HASH_REQUEST_MIN, *DATA_BY_HASH_REQUEST_MAX) + // TODO + RpcLimits::new(1, *DATA_BY_HASH_REQUEST_MAX) + } + Protocol::GetChunks => RpcLimits::new( + ::ssz_fixed_len(), + ::ssz_fixed_len(), + ), + } + } + + /// Returns min and max size for messages of given protocol id responses. + pub fn rpc_response_limits(&self) -> RpcLimits { + match self.message_name { + Protocol::Status => RpcLimits::new( + ::ssz_fixed_len(), + ::ssz_fixed_len(), + ), + Protocol::Goodbye => RpcLimits::new(0, 0), // Goodbye request has no response + + Protocol::Ping => RpcLimits::new( + ::ssz_fixed_len(), + ::ssz_fixed_len(), + ), + + // TODO: not fixed size? + Protocol::DataByHash => RpcLimits::new( + ::ssz_fixed_len(), + ::ssz_fixed_len(), + ), + + Protocol::GetChunks => RpcLimits::new(*CHUNKS_RESPONSE_MIN, *CHUNKS_RESPONSE_MAX), + } + } +} + +/// An RPC protocol ID. +impl ProtocolId { + pub fn new(message_name: Protocol, version: Version, encoding: Encoding) -> Self { + let protocol_id = format!( + "{}/{}/{}/{}", + PROTOCOL_PREFIX, message_name, version, encoding + ); + + ProtocolId { + message_name, + version, + encoding, + protocol_id, + } + } +} + +impl ProtocolName for ProtocolId { + fn protocol_name(&self) -> &[u8] { + self.protocol_id.as_bytes() + } +} + +/* Inbound upgrade */ + +// The inbound protocol reads the request, decodes it and returns the stream to the protocol +// handler to respond to once ready. + +pub type InboundOutput = (InboundRequest, InboundFramed); +pub type InboundFramed = + Framed>>>, InboundCodec>; + +impl InboundUpgrade for RPCProtocol +where + TSocket: AsyncRead + AsyncWrite + Unpin + Send + 'static, +{ + type Output = InboundOutput; + type Error = RPCError; + type Future = BoxFuture<'static, Result>; + + fn upgrade_inbound(self, socket: TSocket, protocol: ProtocolId) -> Self::Future { + async move { + // convert the socket to tokio compatible socket + let socket = socket.compat(); + let codec = match protocol.encoding { + Encoding::SSZSnappy => { + let ssz_snappy_codec = BaseInboundCodec::new(SSZSnappyInboundCodec::new( + protocol, + self.max_rpc_size, + )); + InboundCodec::SSZSnappy(ssz_snappy_codec) + } + }; + let mut timed_socket = TimeoutStream::new(socket); + timed_socket.set_read_timeout(Some(Duration::from_secs(TTFB_TIMEOUT))); + + let socket = Framed::new(Box::pin(timed_socket), codec); + + match tokio::time::timeout(Duration::from_secs(REQUEST_TIMEOUT), socket.into_future()) + .await + { + Err(e) => Err(RPCError::from(e)), + Ok((Some(Ok(request)), stream)) => Ok((request, stream)), + Ok((Some(Err(e)), _)) => Err(e), + Ok((None, _)) => Err(RPCError::IncompleteStream), + } + } + .boxed() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum InboundRequest { + Status(StatusMessage), + Goodbye(GoodbyeReason), + Ping(Ping), + DataByHash(DataByHashRequest), + GetChunks(GetChunksRequest), +} + +impl UpgradeInfo for InboundRequest { + type Info = ProtocolId; + type InfoIter = Vec; + + // add further protocols as we support more encodings/versions + fn protocol_info(&self) -> Self::InfoIter { + self.supported_protocols() + } +} + +/// Implements the encoding per supported protocol for `RPCRequest`. +impl InboundRequest { + pub fn supported_protocols(&self) -> Vec { + match self { + // add more protocols when versions/encodings are supported + InboundRequest::Status(_) => vec![ProtocolId::new( + Protocol::Status, + Version::V1, + Encoding::SSZSnappy, + )], + InboundRequest::Goodbye(_) => vec![ProtocolId::new( + Protocol::Goodbye, + Version::V1, + Encoding::SSZSnappy, + )], + InboundRequest::Ping(_) => vec![ProtocolId::new( + Protocol::Ping, + Version::V1, + Encoding::SSZSnappy, + )], + InboundRequest::DataByHash(_) => vec![ProtocolId::new( + Protocol::DataByHash, + Version::V1, + Encoding::SSZSnappy, + )], + InboundRequest::GetChunks(_) => vec![ProtocolId::new( + Protocol::GetChunks, + Version::V1, + Encoding::SSZSnappy, + )], + } + } + + /* These functions are used in the handler for stream management */ + + /// Number of responses expected for this request. + pub fn expected_responses(&self) -> u64 { + match self { + InboundRequest::Status(_) => 1, + InboundRequest::Goodbye(_) => 0, + InboundRequest::DataByHash(req) => req.hashes.len() as u64, + InboundRequest::Ping(_) => 1, + InboundRequest::GetChunks(_) => 1, + } + } + + /// Gives the corresponding `Protocol` to this request. + pub fn protocol(&self) -> Protocol { + match self { + InboundRequest::Status(_) => Protocol::Status, + InboundRequest::Goodbye(_) => Protocol::Goodbye, + InboundRequest::Ping(_) => Protocol::Ping, + InboundRequest::DataByHash(_) => Protocol::DataByHash, + InboundRequest::GetChunks(_) => Protocol::GetChunks, + } + } + + /// Returns the `ResponseTermination` type associated with the request if a stream gets + /// terminated. + pub fn stream_termination(&self) -> ResponseTermination { + match self { + // this only gets called after `multiple_responses()` returns true. Therefore, only + // variants that have `multiple_responses()` can have values. + InboundRequest::DataByHash(_) => ResponseTermination::DataByHash, + InboundRequest::Status(_) => unreachable!(), + InboundRequest::Goodbye(_) => unreachable!(), + InboundRequest::Ping(_) => unreachable!(), + InboundRequest::GetChunks(_) => unreachable!(), + } + } +} + +/// Error in RPC Encoding/Decoding. +#[derive(Debug, Clone, PartialEq, IntoStaticStr)] +#[strum(serialize_all = "snake_case")] +pub enum RPCError { + /// Error when decoding the raw buffer from ssz. + // NOTE: in the future a ssz::DecodeError should map to an InvalidData error + #[strum(serialize = "decode_error")] + SSZDecodeError(ssz::DecodeError), + /// IO Error. + IoError(String), + /// The peer returned a valid response but the response indicated an error. + ErrorResponse(RPCResponseErrorCode, String), + /// Timed out waiting for a response. + StreamTimeout, + /// Peer does not support the protocol. + UnsupportedProtocol, + /// Stream ended unexpectedly. + IncompleteStream, + /// Peer sent invalid data. + InvalidData(String), + /// An error occurred due to internal reasons. Ex: timer failure. + InternalError(&'static str), + /// Negotiation with this peer timed out. + NegotiationTimeout, + /// Handler rejected this request. + HandlerRejected, + /// We have intentionally disconnected. + Disconnected, +} + +impl From for RPCError { + #[inline] + fn from(err: ssz::DecodeError) -> Self { + RPCError::SSZDecodeError(err) + } +} +impl From for RPCError { + fn from(_: tokio::time::error::Elapsed) -> Self { + RPCError::StreamTimeout + } +} + +impl From for RPCError { + fn from(err: io::Error) -> Self { + RPCError::IoError(err.to_string()) + } +} + +// Error trait is required for `ProtocolsHandler` +impl std::fmt::Display for RPCError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + RPCError::SSZDecodeError(ref err) => { + write!(f, "Error while decoding ssz: {:?}", err) + } + RPCError::InvalidData(ref err) => { + write!(f, "Peer sent unexpected data: {}", err) + } + RPCError::IoError(ref err) => write!(f, "IO Error: {}", err), + RPCError::ErrorResponse(ref code, ref reason) => write!( + f, + "RPC response was an error: {} with reason: {}", + code, reason + ), + RPCError::StreamTimeout => write!(f, "Stream Timeout"), + RPCError::UnsupportedProtocol => { + write!(f, "Peer does not support the protocol") + } + RPCError::IncompleteStream => { + write!(f, "Stream ended unexpectedly") + } + RPCError::InternalError(ref err) => { + write!(f, "Internal error: {}", err) + } + RPCError::NegotiationTimeout => write!(f, "Negotiation timeout"), + RPCError::HandlerRejected => { + write!(f, "Handler rejected the request") + } + RPCError::Disconnected => write!(f, "Gracefully Disconnected"), + } + } +} + +impl std::error::Error for RPCError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match *self { + // NOTE: this does have a source + RPCError::SSZDecodeError(_) => None, + RPCError::IoError(_) => None, + RPCError::StreamTimeout => None, + RPCError::UnsupportedProtocol => None, + RPCError::IncompleteStream => None, + RPCError::InvalidData(_) => None, + RPCError::InternalError(_) => None, + RPCError::ErrorResponse(_, _) => None, + RPCError::NegotiationTimeout => None, + RPCError::HandlerRejected => None, + RPCError::Disconnected => None, + } + } +} + +impl std::fmt::Display for InboundRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + InboundRequest::Status(status) => { + write!(f, "Status Message: {}", status) + } + InboundRequest::Goodbye(reason) => write!(f, "Goodbye: {}", reason), + InboundRequest::Ping(ping) => write!(f, "Ping: {}", ping.data), + InboundRequest::DataByHash(req) => { + write!(f, "Data by hash: {:?}", req) + } + InboundRequest::GetChunks(req) => { + write!(f, "Get Chunks: {:?}", req) + } + } + } +} + +impl RPCError { + /// Get a `str` representation of the error. + /// Used for metrics. + pub fn as_static_str(&self) -> &'static str { + match self { + RPCError::ErrorResponse(ref code, ..) => code.into(), + e => e.into(), + } + } +} diff --git a/node/network/src/rpc/rate_limiter.rs b/node/network/src/rpc/rate_limiter.rs new file mode 100644 index 0000000..c213146 --- /dev/null +++ b/node/network/src/rpc/rate_limiter.rs @@ -0,0 +1,388 @@ +use crate::rpc::{InboundRequest, Protocol}; +use fnv::FnvHashMap; +use libp2p::PeerId; +use std::convert::TryInto; +use std::future::Future; +use std::hash::Hash; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::{Duration, Instant}; +use tokio::time::Interval; + +/// Nanoseconds since a given time. +// Maintained as u64 to reduce footprint +// NOTE: this also implies that the rate limiter will manage checking if a batch is allowed for at +// most + u64::MAX nanosecs, ~500 years. So it is realistic to assume this is fine. +type Nanosecs = u64; + +/// User-friendly rate limiting parameters of the GCRA. +/// +/// A quota of `max_tokens` tokens every `replenish_all_every` units of time means that: +/// 1. One token is replenished every `replenish_all_every`/`max_tokens` units of time. +/// 2. Instantaneous bursts (batches) of up to `max_tokens` tokens are allowed. +/// +/// The above implies that if `max_tokens` is greater than 1, the perceived rate may be higher (but +/// bounded) than the defined rate when instantaneous bursts occur. For instance, for a rate of +/// 4T/2s a first burst of 4T is allowed with subsequent requests of 1T every 0.5s forever, +/// producing a perceived rate over the window of the first 2s of 8T. However, subsequent sliding +/// windows of 2s keep the limit. +/// +/// In this scenario using the same rate as above, the sender is always maxing out their tokens, +/// except at seconds 1.5, 3, 3.5 and 4 +/// +/// ```ignore +/// x +/// used x +/// tokens x x x +/// at a x x x x x x +/// given +--+--+--o--+--+--o--o--o--> seconds +/// time | | | | | | | | | +/// 0 1 2 3 4 +/// +/// 4 1 1 1 2 1 1 2 3 <= available tokens when the batch is received +/// ``` +/// +/// For a sender to request a batch of `n`T, they would need to wait at least +/// n*`replenish_all_every`/`max_tokens` units of time since their last request. +/// +/// To produce hard limits, set `max_tokens` to 1. +pub struct Quota { + /// How often are `max_tokens` fully replenished. + replenish_all_every: Duration, + /// Token limit. This translates on how large can an instantaneous batch of + /// tokens be. + max_tokens: u64, +} + +/// Manages rate limiting of requests per peer, with differentiated rates per protocol. +pub struct RPCRateLimiter { + /// Interval to prune peers for which their timer ran out. + prune_interval: Interval, + /// Creation time of the rate limiter. + init_time: Instant, + /// Goodbye rate limiter. + goodbye_rl: Limiter, + /// Ping rate limiter. + ping_rl: Limiter, + /// Status rate limiter. + status_rl: Limiter, + /// DataByHash rate limiter. + data_by_hash_rl: Limiter, + /// GetChunks rate limiter. + get_chunks_rl: Limiter, +} + +/// Error type for non conformant requests +pub enum RateLimitedErr { + /// Required tokens for this request exceed the maximum + TooLarge, + /// Request does not fit in the quota. Gives the earliest time the request could be accepted. + TooSoon(Duration), +} + +/// User-friendly builder of a `RPCRateLimiter` +#[derive(Default)] +pub struct RPCRateLimiterBuilder { + /// Quota for the Goodbye protocol. + goodbye_quota: Option, + /// Quota for the Ping protocol. + ping_quota: Option, + /// Quota for the Status protocol. + status_quota: Option, + /// Quota for the DataByHash protocol. + data_by_hash_quota: Option, + /// Quota for the GetChunks protocol. + get_chunks_quota: Option, +} + +impl RPCRateLimiterBuilder { + /// Get an empty `RPCRateLimiterBuilder`. + pub fn new() -> Self { + Default::default() + } + + /// Set a quota for a protocol. + fn set_quota(mut self, protocol: Protocol, quota: Quota) -> Self { + let q = Some(quota); + match protocol { + Protocol::Ping => self.ping_quota = q, + Protocol::Status => self.status_quota = q, + Protocol::Goodbye => self.goodbye_quota = q, + Protocol::DataByHash => self.data_by_hash_quota = q, + Protocol::GetChunks => self.get_chunks_quota = q, + } + self + } + + /// Allow one token every `time_period` to be used for this `protocol`. + /// This produces a hard limit. + pub fn one_every(self, protocol: Protocol, time_period: Duration) -> Self { + self.set_quota( + protocol, + Quota { + replenish_all_every: time_period, + max_tokens: 1, + }, + ) + } + + /// Allow `n` tokens to be use used every `time_period` for this `protocol`. + pub fn n_every(self, protocol: Protocol, n: u64, time_period: Duration) -> Self { + self.set_quota( + protocol, + Quota { + max_tokens: n, + replenish_all_every: time_period, + }, + ) + } + + pub fn build(self) -> Result { + // get our quotas + let ping_quota = self.ping_quota.ok_or("Ping quota not specified")?; + let status_quota = self.status_quota.ok_or("Status quota not specified")?; + let goodbye_quota = self.goodbye_quota.ok_or("Goodbye quota not specified")?; + let data_by_hash_quota = self + .data_by_hash_quota + .ok_or("DataByHash quota not specified")?; + let get_chunks_quota = self + .get_chunks_quota + .ok_or("GetChunks quota not specified")?; + + // create the rate limiters + let ping_rl = Limiter::from_quota(ping_quota)?; + let status_rl = Limiter::from_quota(status_quota)?; + let goodbye_rl = Limiter::from_quota(goodbye_quota)?; + let data_by_hash_rl = Limiter::from_quota(data_by_hash_quota)?; + let get_chunks_rl = Limiter::from_quota(get_chunks_quota)?; + + // check for peers to prune every 30 seconds, starting in 30 seconds + let prune_every = tokio::time::Duration::from_secs(30); + let prune_start = tokio::time::Instant::now() + prune_every; + let prune_interval = tokio::time::interval_at(prune_start, prune_every); + Ok(RPCRateLimiter { + prune_interval, + ping_rl, + status_rl, + goodbye_rl, + data_by_hash_rl, + get_chunks_rl, + init_time: Instant::now(), + }) + } +} + +impl RPCRateLimiter { + pub fn allows( + &mut self, + peer_id: &PeerId, + request: &InboundRequest, + ) -> Result<(), RateLimitedErr> { + let time_since_start = self.init_time.elapsed(); + let tokens = request.expected_responses().max(1); + + // Increase the rate limit for blocks by range requests with large step counts. + // We count to tokens as a quadratic increase with step size. + // Using (step_size/5)^2 + 1 as penalty factor allows step sizes of 1-4 to have no penalty + // but step sizes higher than this add a quadratic penalty. + // Penalty's go: + // Step size | Penalty Factor + // 1 | 1 + // 2 | 1 + // 3 | 1 + // 4 | 1 + // 5 | 2 + // 6 | 2 + // 7 | 2 + // 8 | 3 + // 9 | 4 + // 10 | 5 + + // if let InboundRequest::BlocksByRange(bbr_req) = request { + // let penalty_factor = (bbr_req.step as f64 / 5.0).powi(2) as u64 + 1; + // tokens *= penalty_factor; + // } + + let check = + |limiter: &mut Limiter| limiter.allows(time_since_start, peer_id, tokens); + let limiter = match request.protocol() { + Protocol::Ping => &mut self.ping_rl, + Protocol::Status => &mut self.status_rl, + Protocol::Goodbye => &mut self.goodbye_rl, + Protocol::DataByHash => &mut self.data_by_hash_rl, + Protocol::GetChunks => &mut self.get_chunks_rl, + }; + check(limiter) + } + + pub fn prune(&mut self) { + let time_since_start = self.init_time.elapsed(); + self.ping_rl.prune(time_since_start); + self.status_rl.prune(time_since_start); + self.goodbye_rl.prune(time_since_start); + self.data_by_hash_rl.prune(time_since_start); + self.get_chunks_rl.prune(time_since_start); + } +} + +impl Future for RPCRateLimiter { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { + while self.prune_interval.poll_tick(cx).is_ready() { + self.prune(); + } + + Poll::Pending + } +} + +/// Per key rate limiter using the token bucket / leaky bucket as a meter rate limiting algorithm, +/// with the GCRA implementation. +pub struct Limiter { + /// After how long is the bucket considered full via replenishing 1T every `t`. + tau: Nanosecs, + /// How often is 1T replenished. + t: Nanosecs, + /// Time when the bucket will be full for each peer. TAT (theoretical arrival time) from GCRA. + tat_per_key: FnvHashMap, +} + +impl Limiter { + pub fn from_quota(quota: Quota) -> Result { + if quota.max_tokens == 0 { + return Err("Max number of tokens should be positive"); + } + let tau = quota.replenish_all_every.as_nanos(); + if tau == 0 { + return Err("Replenish time must be positive"); + } + let t = (tau / quota.max_tokens as u128) + .try_into() + .map_err(|_| "total replenish time is too long")?; + let tau = tau + .try_into() + .map_err(|_| "total replenish time is too long")?; + Ok(Limiter { + tau, + t, + tat_per_key: FnvHashMap::default(), + }) + } + + pub fn allows( + &mut self, + time_since_start: Duration, + key: &Key, + tokens: u64, + ) -> Result<(), RateLimitedErr> { + let time_since_start = time_since_start.as_nanos() as u64; + let tau = self.tau; + let t = self.t; + // how long does it take to replenish these tokens + let additional_time = t * tokens; + if additional_time > tau { + // the time required to process this amount of tokens is longer than the time that + // makes the bucket full. So, this batch can _never_ be processed + return Err(RateLimitedErr::TooLarge); + } + // If the key is new, we consider their bucket full (which means, their request will be + // allowed) + let tat = self + .tat_per_key + .entry(key.clone()) + .or_insert(time_since_start); + // check how soon could the request be made + let earliest_time = (*tat + additional_time).saturating_sub(tau); + // earliest_time is in the future + if time_since_start < earliest_time { + Err(RateLimitedErr::TooSoon(Duration::from_nanos( + /* time they need to wait, i.e. how soon were they */ + earliest_time - time_since_start, + ))) + } else { + // calculate the new TAT + *tat = time_since_start.max(*tat) + additional_time; + Ok(()) + } + } + + /// Removes keys for which their bucket is full by `time_limit` + pub fn prune(&mut self, time_limit: Duration) { + let lim = &mut (time_limit.as_nanos() as u64); + // remove those for which tat < lim + self.tat_per_key.retain(|_k, tat| tat >= lim) + } +} + +#[cfg(test)] +mod tests { + use crate::rpc::rate_limiter::{Limiter, Quota}; + use std::time::Duration; + + #[test] + fn it_works_a() { + let mut limiter = Limiter::from_quota(Quota { + replenish_all_every: Duration::from_secs(2), + max_tokens: 4, + }) + .unwrap(); + let key = 10; + // x + // used x + // tokens x x + // x x x x + // +--+--+--+--+----> seconds + // | | | | | + // 0 1 2 + + assert!(limiter + .allows(Duration::from_secs_f32(0.0), &key, 4) + .is_ok()); + limiter.prune(Duration::from_secs_f32(0.1)); + assert!(limiter + .allows(Duration::from_secs_f32(0.1), &key, 1) + .is_err()); + assert!(limiter + .allows(Duration::from_secs_f32(0.5), &key, 1) + .is_ok()); + assert!(limiter + .allows(Duration::from_secs_f32(1.0), &key, 1) + .is_ok()); + assert!(limiter + .allows(Duration::from_secs_f32(1.4), &key, 1) + .is_err()); + assert!(limiter + .allows(Duration::from_secs_f32(2.0), &key, 2) + .is_ok()); + } + + #[test] + fn it_works_b() { + let mut limiter = Limiter::from_quota(Quota { + replenish_all_every: Duration::from_secs(2), + max_tokens: 4, + }) + .unwrap(); + let key = 10; + // if we limit to 4T per 2s, check that 4 requests worth 1 token can be sent before the + // first half second, when one token will be available again. Check also that before + // regaining a token, another request is rejected + + assert!(limiter + .allows(Duration::from_secs_f32(0.0), &key, 1) + .is_ok()); + assert!(limiter + .allows(Duration::from_secs_f32(0.1), &key, 1) + .is_ok()); + assert!(limiter + .allows(Duration::from_secs_f32(0.2), &key, 1) + .is_ok()); + assert!(limiter + .allows(Duration::from_secs_f32(0.3), &key, 1) + .is_ok()); + assert!(limiter + .allows(Duration::from_secs_f32(0.4), &key, 1) + .is_err()); + } +} diff --git a/node/network/src/service.rs b/node/network/src/service.rs new file mode 100644 index 0000000..80bc986 --- /dev/null +++ b/node/network/src/service.rs @@ -0,0 +1,508 @@ +use crate::behaviour::{Behaviour, BehaviourEvent, PeerRequestId, Request, Response}; +use crate::config::NetworkLoad; +use crate::discovery::enr; +use crate::multiaddr::Protocol; +use crate::rpc::{GoodbyeReason, RPCResponseErrorCode, ReqId}; +use crate::types::{error, GossipKind}; +use crate::{EnrExt, NetworkMessage}; +use crate::{NetworkConfig, NetworkGlobals, PeerAction, ReportSource}; +use futures::prelude::*; +use libp2p::core::{ + identity::Keypair, multiaddr::Multiaddr, muxing::StreamMuxerBox, transport::Boxed, +}; +use libp2p::{ + bandwidth::{BandwidthLogging, BandwidthSinks}, + core, noise, + swarm::{ConnectionLimits, SwarmBuilder, SwarmEvent}, + PeerId, Swarm, Transport, +}; +use std::fs::File; +use std::io::prelude::*; +use std::pin::Pin; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::mpsc::UnboundedSender; + +use crate::peer_manager::{MIN_OUTBOUND_ONLY_FACTOR, PEER_EXCESS_FACTOR, PRIORITY_PEER_EXCESS}; + +pub const NETWORK_KEY_FILENAME: &str = "key"; +/// The maximum simultaneous libp2p connections per peer. +const MAX_CONNECTIONS_PER_PEER: u32 = 1; + +/// The types of events than can be obtained from polling the libp2p service. +/// +/// This is a subset of the events that a libp2p swarm emits. +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +pub enum Libp2pEvent { + /// A behaviour event + Behaviour(BehaviourEvent), + /// A new listening address has been established. + NewListenAddr(Multiaddr), + /// We reached zero listening addresses. + ZeroListeners, +} + +/// The configuration and state of the libp2p components for the beacon node. +pub struct Service { + /// The libp2p Swarm handler. + pub swarm: Swarm>, + /// The bandwidth logger for the underlying libp2p transport. + pub bandwidth: Arc, + /// This node's PeerId. + pub local_peer_id: PeerId, +} + +pub struct Context<'a> { + pub config: &'a NetworkConfig, +} + +impl Service { + pub async fn new( + executor: task_executor::TaskExecutor, + network_sender: UnboundedSender, + ctx: Context<'_>, + ) -> error::Result<(Arc, Keypair, Self)> { + trace!("Libp2p Service starting"); + + let config = ctx.config; + // initialise the node's ID + let local_keypair = load_private_key(config); + + // Create an ENR or load from disk if appropriate + let enr = enr::build_or_load_enr(local_keypair.clone(), config)?; + + let local_peer_id = enr.peer_id(); + + // set up a collection of variables accessible outside of the network crate + let network_globals = Arc::new(NetworkGlobals::new( + enr.clone(), + config.libp2p_port, + config.discovery_port, + config + .trusted_peers + .iter() + .map(|x| PeerId::from(x.clone())) + .collect(), + )); + + // try and construct UPnP port mappings if required. + if let Some(upnp_config) = crate::nat::UPnPConfig::from_config(config) { + if config.upnp_enabled { + executor.spawn_blocking( + move || crate::nat::construct_upnp_mappings(upnp_config, network_sender), + "UPnP", + ); + } + } + + info!( + peer_id = %enr.peer_id(), + bandwidth_config = %format!("{}-{}", config.network_load, NetworkLoad::from(config.network_load).name), + "Libp2p Starting", + ); + + let discovery_string = if config.disable_discovery { + "None".into() + } else { + config.discovery_port.to_string() + }; + + debug!( + address = ?config.listen_address, + tcp_port = %config.libp2p_port, + udp_port = %discovery_string, + "Attempting to open listening ports", + ); + + let (mut swarm, bandwidth) = { + // Set up the transport - tcp/ws with noise and mplex + let (transport, bandwidth) = build_transport(local_keypair.clone()) + .map_err(|e| format!("Failed to build transport: {:?}", e))?; + + // Lighthouse network behaviour + let behaviour = + Behaviour::new(&local_keypair.clone(), ctx, network_globals.clone()).await?; + + // use the executor for libp2p + struct Executor(task_executor::TaskExecutor); + impl libp2p::core::Executor for Executor { + fn exec(&self, f: Pin + Send>>) { + self.0.spawn(f, "libp2p"); + } + } + + // sets up the libp2p connection limits + let limits = ConnectionLimits::default() + .with_max_pending_incoming(Some(5)) + .with_max_pending_outgoing(Some(16)) + .with_max_established_incoming(Some( + (config.target_peers as f32 + * (1.0 + PEER_EXCESS_FACTOR - MIN_OUTBOUND_ONLY_FACTOR)) + .ceil() as u32, + )) + .with_max_established_outgoing(Some( + (config.target_peers as f32 * (1.0 + PEER_EXCESS_FACTOR)).ceil() as u32, + )) + .with_max_established(Some( + (config.target_peers as f32 * (1.0 + PEER_EXCESS_FACTOR + PRIORITY_PEER_EXCESS)) + .ceil() as u32, + )) + .with_max_established_per_peer(Some(MAX_CONNECTIONS_PER_PEER)); + + ( + SwarmBuilder::new(transport, behaviour, local_peer_id) + .notify_handler_buffer_size(std::num::NonZeroUsize::new(7).expect("Not zero")) + .connection_event_buffer_size(64) + .connection_limits(limits) + .executor(Box::new(Executor(executor))) + .build(), + bandwidth, + ) + }; + + // listen on the specified address + let listen_multiaddr = { + let mut m = Multiaddr::from(config.listen_address); + m.push(Protocol::Tcp(config.libp2p_port)); + m + }; + + match Swarm::listen_on(&mut swarm, listen_multiaddr.clone()) { + Ok(_) => { + let mut log_address = listen_multiaddr; + log_address.push(Protocol::P2p(local_peer_id.into())); + info!(address = %log_address, "Listening established"); + } + Err(err) => { + error!( + error = ?err, + listen_multiaddr = %listen_multiaddr, + "Unable to listen on libp2p address", + ); + return Err("Libp2p was unable to listen on the given listen address.".into()); + } + }; + + // helper closure for dialing peers + let mut dial = |multiaddr: Multiaddr| { + // strip the p2p protocol if it exists + match Swarm::dial(&mut swarm, multiaddr.clone()) { + Ok(()) => debug!(address = %multiaddr, "Dialing libp2p peer"), + Err(err) => { + debug!(address = %multiaddr, error = ?err, "Could not connect to peer") + } + }; + }; + + // attempt to connect to user-input libp2p nodes + for multiaddr in &config.libp2p_nodes { + dial(multiaddr.clone()); + } + + // attempt to connect to any specified boot-nodes + let mut boot_nodes = config.boot_nodes_enr.clone(); + boot_nodes.dedup(); + + for bootnode_enr in boot_nodes { + for multiaddr in &bootnode_enr.multiaddr() { + // ignore udp multiaddr if it exists + let components = multiaddr.iter().collect::>(); + if let Protocol::Udp(_) = components[1] { + continue; + } + + if !network_globals + .peers + .read() + .is_connected_or_dialing(&bootnode_enr.peer_id()) + { + dial(multiaddr.clone()); + } + } + } + + for multiaddr in &config.boot_nodes_multiaddr { + // check TCP support for dialing + if multiaddr + .iter() + .any(|proto| matches!(proto, Protocol::Tcp(_))) + { + dial(multiaddr.clone()); + } + } + + let mut subscribed_topics: Vec = vec![]; + + // for topic_kind in &config.topics { + // if swarm.behaviour_mut().subscribe_kind(topic_kind.clone()) { + // subscribed_topics.push(topic_kind.clone()); + // } else { + // warn!(topic = ?topic_kind, "Could not subscribe to topic"); + // } + // } + + for topic_kind in &crate::types::CORE_TOPICS { + if swarm.behaviour_mut().subscribe_kind(topic_kind.clone()) { + subscribed_topics.push(topic_kind.clone()); + } else { + warn!(topic = ?topic_kind, "Could not subscribe to topic"); + } + } + + if !subscribed_topics.is_empty() { + info!(topics = ?subscribed_topics, "Subscribed to topics"); + } + + let service = Service { + swarm, + bandwidth, + local_peer_id, + }; + + Ok((network_globals, local_keypair, service)) + } + + /// Sends a request to a peer, with a given Id. + pub fn send_request(&mut self, peer_id: PeerId, request_id: AppReqId, request: Request) { + self.swarm + .behaviour_mut() + .send_request(peer_id, request_id, request); + } + + /// Informs the peer that their request failed. + pub fn respond_with_error( + &mut self, + peer_id: PeerId, + id: PeerRequestId, + error: RPCResponseErrorCode, + reason: String, + ) { + self.swarm + .behaviour_mut() + .send_error_reponse(peer_id, id, error, reason); + } + + /// Report a peer's action. + pub fn report_peer( + &mut self, + peer_id: &PeerId, + action: PeerAction, + source: ReportSource, + msg: &'static str, + ) { + self.swarm + .behaviour_mut() + .peer_manager_mut() + .report_peer(peer_id, action, source, None, msg); + } + + /// Disconnect and ban a peer, providing a reason. + pub fn goodbye_peer(&mut self, peer_id: &PeerId, reason: GoodbyeReason, source: ReportSource) { + self.swarm + .behaviour_mut() + .goodbye_peer(peer_id, reason, source); + } + + /// Sends a response to a peer's request. + pub fn send_response(&mut self, peer_id: PeerId, id: PeerRequestId, response: Response) { + self.swarm + .behaviour_mut() + .send_successful_response(peer_id, id, response); + } + + pub async fn next_event(&mut self) -> Libp2pEvent { + loop { + match self.swarm.select_next_some().await { + SwarmEvent::Behaviour(behaviour) => { + // Handle banning here + match &behaviour { + BehaviourEvent::PeerBanned(peer_id) => { + self.swarm.ban_peer_id(*peer_id); + } + BehaviourEvent::PeerUnbanned(peer_id) => { + self.swarm.unban_peer_id(*peer_id); + } + _ => {} + } + return Libp2pEvent::Behaviour(behaviour); + } + SwarmEvent::ConnectionEstablished { + peer_id: _, + endpoint: _, + num_established: _, + concurrent_dial_errors: _, + } => {} + SwarmEvent::ConnectionClosed { + peer_id: _, + cause: _, + endpoint: _, + num_established: _, + } => {} + SwarmEvent::NewListenAddr { address, .. } => { + return Libp2pEvent::NewListenAddr(address) + } + SwarmEvent::IncomingConnection { + local_addr, + send_back_addr, + } => { + trace!(our_addr = %local_addr, from = %send_back_addr, "Incoming connection") + } + SwarmEvent::IncomingConnectionError { + local_addr, + send_back_addr, + error, + } => { + debug!(our_addr = %local_addr, from = %send_back_addr, error = %error, "Failed incoming connection"); + } + SwarmEvent::BannedPeer { peer_id, .. } => { + debug!(peer_id = %peer_id, "Banned peer connection rejected"); + } + SwarmEvent::OutgoingConnectionError { peer_id, error } => { + debug!(peer_id = ?peer_id, error = %error, "Failed to dial address"); + } + SwarmEvent::ExpiredListenAddr { address, .. } => { + debug!(address = %address, "Listen address expired") + } + SwarmEvent::ListenerClosed { + addresses, reason, .. + } => { + error!(addresses = ?addresses, reason = ?reason, "Listener closed"); + if Swarm::listeners(&self.swarm).count() == 0 { + return Libp2pEvent::ZeroListeners; + } + } + SwarmEvent::ListenerError { error, .. } => { + // this is non fatal, but we still check + warn!(error = ?error, "Listener error"); + if Swarm::listeners(&self.swarm).count() == 0 { + return Libp2pEvent::ZeroListeners; + } + } + SwarmEvent::Dialing(_peer_id) => {} + } + } + } +} + +type BoxedTransport = Boxed<(PeerId, StreamMuxerBox)>; + +/// The implementation supports TCP/IP, WebSockets over TCP/IP, noise as the encryption layer, and +/// mplex as the multiplexing layer. +fn build_transport( + local_private_key: Keypair, +) -> std::io::Result<(BoxedTransport, Arc)> { + let tcp = libp2p::tcp::TokioTcpConfig::new().nodelay(true); + let transport = libp2p::dns::TokioDnsConfig::system(tcp)?; + #[cfg(feature = "libp2p-websocket")] + let transport = { + let trans_clone = transport.clone(); + transport.or_transport(libp2p::websocket::WsConfig::new(trans_clone)) + }; + + let (transport, bandwidth) = BandwidthLogging::new(transport); + + // mplex config + let mut mplex_config = libp2p::mplex::MplexConfig::new(); + mplex_config.set_max_buffer_size(256); + mplex_config.set_max_buffer_behaviour(libp2p::mplex::MaxBufferBehaviour::Block); + + // yamux config + let mut yamux_config = libp2p::yamux::YamuxConfig::default(); + yamux_config.set_window_update_mode(libp2p::yamux::WindowUpdateMode::on_read()); + + // Authentication + Ok(( + transport + .upgrade(core::upgrade::Version::V1) + .authenticate(generate_noise_config(&local_private_key)) + .multiplex(core::upgrade::SelectUpgrade::new( + yamux_config, + mplex_config, + )) + .timeout(Duration::from_secs(10)) + .boxed(), + bandwidth, + )) +} + +// Useful helper functions for debugging. Currently not used in the client. +#[allow(dead_code)] +fn keypair_from_hex(hex_bytes: &str) -> error::Result { + let hex_bytes = if let Some(stripped) = hex_bytes.strip_prefix("0x") { + stripped.to_string() + } else { + hex_bytes.to_string() + }; + + hex::decode(hex_bytes) + .map_err(|e| format!("Failed to parse p2p secret key bytes: {:?}", e).into()) + .and_then(keypair_from_bytes) +} + +#[allow(dead_code)] +fn keypair_from_bytes(mut bytes: Vec) -> error::Result { + libp2p::core::identity::secp256k1::SecretKey::from_bytes(&mut bytes) + .map(|secret| { + let keypair: libp2p::core::identity::secp256k1::Keypair = secret.into(); + Keypair::Secp256k1(keypair) + }) + .map_err(|e| format!("Unable to parse p2p secret key: {:?}", e).into()) +} + +/// Loads a private key from disk. If this fails, a new key is +/// generated and is then saved to disk. +/// +/// Currently only secp256k1 keys are allowed, as these are the only keys supported by discv5. +pub fn load_private_key(config: &NetworkConfig) -> Keypair { + // check for key from disk + let network_key_f = config.network_dir.join(NETWORK_KEY_FILENAME); + if let Ok(mut network_key_file) = File::open(network_key_f.clone()) { + let mut key_bytes: Vec = Vec::with_capacity(36); + match network_key_file.read_to_end(&mut key_bytes) { + Err(_) => debug!("Could not read network key file"), + Ok(_) => { + // only accept secp256k1 keys for now + if let Ok(secret_key) = + libp2p::core::identity::secp256k1::SecretKey::from_bytes(&mut key_bytes) + { + let kp: libp2p::core::identity::secp256k1::Keypair = secret_key.into(); + debug!("Loaded network key from disk."); + return Keypair::Secp256k1(kp); + } else { + debug!("Network key file is not a valid secp256k1 key"); + } + } + } + } + + // if a key could not be loaded from disk, generate a new one and save it + let local_private_key = Keypair::generate_secp256k1(); + if let Keypair::Secp256k1(key) = local_private_key.clone() { + let _ = std::fs::create_dir_all(&config.network_dir); + match File::create(network_key_f.clone()) + .and_then(|mut f| f.write_all(&key.secret().to_bytes())) + { + Ok(_) => { + debug!("New network key generated and written to disk"); + } + Err(e) => { + warn!( + "Could not write node key to file: {:?}. error: {}", + network_key_f, e + ); + } + } + } + local_private_key +} + +/// Generate authenticated XX Noise config from identity keys +fn generate_noise_config( + identity_keypair: &Keypair, +) -> noise::NoiseAuthenticated { + let static_dh_keys = noise::Keypair::::new() + .into_authentic(identity_keypair) + .expect("signing can fail only once during starting a node"); + noise::NoiseConfig::xx(static_dh_keys).into_authenticated() +} diff --git a/node/network/src/types/error.rs b/node/network/src/types/error.rs new file mode 100644 index 0000000..a291e8f --- /dev/null +++ b/node/network/src/types/error.rs @@ -0,0 +1,5 @@ +// generates error types + +use error_chain::error_chain; + +error_chain! {} diff --git a/node/network/src/types/globals.rs b/node/network/src/types/globals.rs new file mode 100644 index 0000000..3875560 --- /dev/null +++ b/node/network/src/types/globals.rs @@ -0,0 +1,100 @@ +//! A collection of variables that are accessible outside of the network thread itself. +use crate::peer_manager::peerdb::PeerDB; +use crate::Client; +use crate::EnrExt; +use crate::{Enr, GossipTopic, Multiaddr, PeerId}; +use parking_lot::RwLock; +use std::collections::HashSet; +use std::sync::atomic::{AtomicU16, Ordering}; + +pub struct NetworkGlobals { + /// The current local ENR. + pub local_enr: RwLock, + /// The local peer_id. + pub peer_id: RwLock, + /// Listening multiaddrs. + pub listen_multiaddrs: RwLock>, + /// The TCP port that the libp2p service is listening on + pub listen_port_tcp: AtomicU16, + /// The UDP port that the discovery service is listening on + pub listen_port_udp: AtomicU16, + /// The collection of known peers. + pub peers: RwLock, + /// The current gossipsub topic subscriptions. + pub gossipsub_subscriptions: RwLock>, +} + +impl NetworkGlobals { + pub fn new(enr: Enr, tcp_port: u16, udp_port: u16, trusted_peers: Vec) -> Self { + NetworkGlobals { + local_enr: RwLock::new(enr.clone()), + peer_id: RwLock::new(enr.peer_id()), + listen_multiaddrs: RwLock::new(Vec::new()), + listen_port_tcp: AtomicU16::new(tcp_port), + listen_port_udp: AtomicU16::new(udp_port), + peers: RwLock::new(PeerDB::new(trusted_peers)), + gossipsub_subscriptions: RwLock::new(HashSet::new()), + } + } + + /// Returns the local ENR from the underlying Discv5 behaviour that external peers may connect + /// to. + pub fn local_enr(&self) -> Enr { + self.local_enr.read().clone() + } + + /// Returns the local libp2p PeerID. + pub fn local_peer_id(&self) -> PeerId { + *self.peer_id.read() + } + + /// Returns the list of `Multiaddr` that the underlying libp2p instance is listening on. + pub fn listen_multiaddrs(&self) -> Vec { + self.listen_multiaddrs.read().clone() + } + + /// Returns the libp2p TCP port that this node has been configured to listen on. + pub fn listen_port_tcp(&self) -> u16 { + self.listen_port_tcp.load(Ordering::Relaxed) + } + + /// Returns the UDP discovery port that this node has been configured to listen on. + pub fn listen_port_udp(&self) -> u16 { + self.listen_port_udp.load(Ordering::Relaxed) + } + + /// Returns the number of libp2p connected peers. + pub fn connected_peers(&self) -> usize { + self.peers.read().connected_peer_ids().count() + } + + /// Returns the number of libp2p connected peers with outbound-only connections. + pub fn connected_outbound_only_peers(&self) -> usize { + self.peers.read().connected_outbound_only_peers().count() + } + + /// Returns the number of libp2p peers that are either connected or being dialed. + pub fn connected_or_dialing_peers(&self) -> usize { + self.peers.read().connected_or_dialing_peers().count() + } + + /// Returns a `Client` type if one is known for the `PeerId`. + pub fn client(&self, peer_id: &PeerId) -> Client { + self.peers + .read() + .peer_info(peer_id) + .map(|info| info.client().clone()) + .unwrap_or_default() + } + + /// TESTING ONLY. Build a dummy NetworkGlobals instance. + #[allow(dead_code)] + pub fn new_test_globals() -> NetworkGlobals { + use crate::CombinedKeyExt; + let keypair = libp2p::identity::Keypair::generate_secp256k1(); + let enr_key: discv5::enr::CombinedKey = + discv5::enr::CombinedKey::from_libp2p(&keypair).unwrap(); + let enr = discv5::enr::EnrBuilder::new("v4").build(&enr_key).unwrap(); + NetworkGlobals::new(enr, 9000, 9000, vec![]) + } +} diff --git a/node/network/src/types/mod.rs b/node/network/src/types/mod.rs new file mode 100644 index 0000000..0269cf4 --- /dev/null +++ b/node/network/src/types/mod.rs @@ -0,0 +1,10 @@ +pub mod error; +mod globals; +mod pubsub; +mod topics; + +pub type Enr = discv5::enr::Enr; + +pub use globals::NetworkGlobals; +pub use pubsub::{AnnounceFile, FindFile, PubsubMessage, SignedAnnounceFile, SnappyTransform}; +pub use topics::{GossipEncoding, GossipKind, GossipTopic, CORE_TOPICS}; diff --git a/node/network/src/types/pubsub.rs b/node/network/src/types/pubsub.rs new file mode 100644 index 0000000..81cb830 --- /dev/null +++ b/node/network/src/types/pubsub.rs @@ -0,0 +1,307 @@ +//! Handles the encoding and decoding of pubsub messages. + +use crate::types::{GossipEncoding, GossipKind, GossipTopic}; +use crate::{Keypair, PublicKey, SigningError, TopicHash}; +use libp2p::{ + gossipsub::{DataTransform, GossipsubMessage, RawGossipsubMessage}, + Multiaddr, PeerId, +}; +use shared_types::TxID; +use snap::raw::{decompress_len, Decoder, Encoder}; +use ssz::{Decode, Encode}; +use ssz_derive::{Decode, Encode}; +use std::{ + io::{Error, ErrorKind}, + ops::Deref, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct WrappedMultiaddr(Multiaddr); + +impl From for WrappedMultiaddr { + fn from(addr: Multiaddr) -> Self { + WrappedMultiaddr(addr) + } +} + +impl From for Multiaddr { + fn from(addr: WrappedMultiaddr) -> Self { + addr.0 + } +} + +impl ssz::Encode for WrappedMultiaddr { + fn is_ssz_fixed_len() -> bool { + false + } + + fn ssz_bytes_len(&self) -> usize { + self.0.len() + } + + fn ssz_append(&self, buf: &mut Vec) { + self.0.to_vec().ssz_append(buf) + } +} + +impl ssz::Decode for WrappedMultiaddr { + fn is_ssz_fixed_len() -> bool { + false + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + // TODO: limit length + match Multiaddr::try_from(bytes.to_vec()) { + Ok(addr) => Ok(WrappedMultiaddr(addr)), + Err(_) => Err(ssz::DecodeError::BytesInvalid( + "Cannot parse multiaddr".into(), + )), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct WrappedPeerId(PeerId); + +impl Deref for WrappedPeerId { + type Target = PeerId; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for WrappedPeerId { + fn from(addr: PeerId) -> Self { + WrappedPeerId(addr) + } +} + +impl From for PeerId { + fn from(addr: WrappedPeerId) -> Self { + addr.0 + } +} + +impl ssz::Encode for WrappedPeerId { + fn is_ssz_fixed_len() -> bool { + // TODO: we can probably encode PeerId as fixed-length + false + } + + fn ssz_bytes_len(&self) -> usize { + self.0.to_bytes().len() + } + + fn ssz_append(&self, buf: &mut Vec) { + self.0.to_bytes().ssz_append(buf) + } +} + +impl ssz::Decode for WrappedPeerId { + fn is_ssz_fixed_len() -> bool { + false + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + // TODO: limit length + match PeerId::from_bytes(bytes) { + Ok(addr) => Ok(WrappedPeerId(addr)), + Err(_) => Err(ssz::DecodeError::BytesInvalid( + "Cannot parse peer id".into(), + )), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct FindFile { + pub tx_id: TxID, + pub timestamp: u32, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Encode, Decode)] +pub struct AnnounceFile { + pub tx_id: TxID, + pub peer_id: WrappedPeerId, + pub at: WrappedMultiaddr, + pub timestamp: u32, +} + +impl AnnounceFile { + pub fn into_signed(self, keypair: &Keypair) -> Result { + let raw = self.as_ssz_bytes(); + let signature = keypair.sign(&raw)?; + + Ok(SignedAnnounceFile { + inner: self, + signature, + resend_timestamp: 0, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Encode, Decode)] +pub struct SignedAnnounceFile { + pub inner: AnnounceFile, + pub signature: Vec, + pub resend_timestamp: u32, +} + +impl SignedAnnounceFile { + pub fn verify_signature(&self, public_key: &PublicKey) -> bool { + let raw = self.inner.as_ssz_bytes(); + public_key.verify(&raw, &self.signature) + } +} + +impl Deref for SignedAnnounceFile { + type Target = AnnounceFile; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PubsubMessage { + ExampleMessage(u64), + FindFile(FindFile), + AnnounceFile(SignedAnnounceFile), +} + +// Implements the `DataTransform` trait of gossipsub to employ snappy compression +pub struct SnappyTransform { + /// Sets the maximum size we allow gossipsub messages to decompress to. + max_size_per_message: usize, +} + +impl SnappyTransform { + pub fn new(max_size_per_message: usize) -> Self { + SnappyTransform { + max_size_per_message, + } + } +} + +impl DataTransform for SnappyTransform { + // Provides the snappy decompression from RawGossipsubMessages + fn inbound_transform( + &self, + raw_message: RawGossipsubMessage, + ) -> Result { + // check the length of the raw bytes + let len = decompress_len(&raw_message.data)?; + if len > self.max_size_per_message { + return Err(Error::new( + ErrorKind::InvalidData, + "ssz_snappy decoded data > GOSSIP_MAX_SIZE", + )); + } + + let mut decoder = Decoder::new(); + let decompressed_data = decoder.decompress_vec(&raw_message.data)?; + + // Build the GossipsubMessage struct + Ok(GossipsubMessage { + source: raw_message.source, + data: decompressed_data, + sequence_number: raw_message.sequence_number, + topic: raw_message.topic, + }) + } + + /// Provides the snappy compression logic to gossipsub. + fn outbound_transform( + &self, + _topic: &TopicHash, + data: Vec, + ) -> Result, std::io::Error> { + // Currently we are not employing topic-based compression. Everything is expected to be + // snappy compressed. + if data.len() > self.max_size_per_message { + return Err(Error::new( + ErrorKind::InvalidData, + "ssz_snappy Encoded data > GOSSIP_MAX_SIZE", + )); + } + let mut encoder = Encoder::new(); + encoder.compress_vec(&data).map_err(Into::into) + } +} + +impl PubsubMessage { + /// Returns the topics that each pubsub message will be sent across, given a supported + /// gossipsub encoding and fork version. + pub fn topics(&self, encoding: GossipEncoding) -> Vec { + vec![GossipTopic::new(self.kind(), encoding)] + } + + /// Returns the kind of gossipsub topic associated with the message. + pub fn kind(&self) -> GossipKind { + match self { + PubsubMessage::ExampleMessage(_) => GossipKind::Example, + PubsubMessage::FindFile(_) => GossipKind::FindFile, + PubsubMessage::AnnounceFile(_) => GossipKind::AnnounceFile, + } + } + + /// This decodes `data` into a `PubsubMessage` given a topic. + /* Note: This is assuming we are not hashing topics. If we choose to hash topics, these will + * need to be modified. + */ + pub fn decode(topic: &TopicHash, data: &[u8]) -> Result { + match GossipTopic::decode(topic.as_str()) { + Err(_) => Err(format!("Unknown gossipsub topic: {:?}", topic)), + Ok(gossip_topic) => { + // All topics are currently expected to be compressed and decompressed with snappy. + // This is done in the `SnappyTransform` struct. + // Therefore compression has already been handled for us by the time we are + // decoding the objects here. + + // the ssz decoders + match gossip_topic.kind() { + GossipKind::Example => Ok(PubsubMessage::ExampleMessage( + u64::from_ssz_bytes(data).map_err(|e| format!("{:?}", e))?, + )), + GossipKind::FindFile => Ok(PubsubMessage::FindFile( + FindFile::from_ssz_bytes(data).map_err(|e| format!("{:?}", e))?, + )), + GossipKind::AnnounceFile => Ok(PubsubMessage::AnnounceFile( + SignedAnnounceFile::from_ssz_bytes(data).map_err(|e| format!("{:?}", e))?, + )), + } + } + } + } + + /// Encodes a `PubsubMessage` based on the topic encodings. The first known encoding is used. If + /// no encoding is known, and error is returned. + pub fn encode(&self, _encoding: GossipEncoding) -> Vec { + // Currently do not employ encoding strategies based on the topic. All messages are ssz + // encoded. + // Also note, that the compression is handled by the `SnappyTransform` struct. Gossipsub will compress the + // messages for us. + match &self { + PubsubMessage::ExampleMessage(data) => data.as_ssz_bytes(), + PubsubMessage::FindFile(data) => data.as_ssz_bytes(), + PubsubMessage::AnnounceFile(data) => data.as_ssz_bytes(), + } + } +} + +impl std::fmt::Display for PubsubMessage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PubsubMessage::ExampleMessage(msg) => { + write!(f, "Example message: {}", msg) + } + PubsubMessage::FindFile(msg) => { + write!(f, "FindFile message: {:?}", msg) + } + PubsubMessage::AnnounceFile(msg) => { + write!(f, "AnnounceFile message: {:?}", msg) + } + } + } +} diff --git a/node/network/src/types/topics.rs b/node/network/src/types/topics.rs new file mode 100644 index 0000000..5836781 --- /dev/null +++ b/node/network/src/types/topics.rs @@ -0,0 +1,177 @@ +use libp2p::gossipsub::IdentTopic as Topic; +use serde_derive::{Deserialize, Serialize}; +use strum::AsRefStr; + +/// The gossipsub topic names. +// These constants form a topic name of the form /TOPIC_PREFIX/TOPIC/ENCODING_POSTFIX +// For example /eth2/beacon_block/ssz +pub const TOPIC_PREFIX: &str = "eth2"; +pub const SSZ_SNAPPY_ENCODING_POSTFIX: &str = "ssz_snappy"; +pub const EXAMPLE_TOPIC: &str = "example"; +pub const FIND_FILE_TOPIC: &str = "find_file"; +pub const ANNOUNCE_FILE_TOPIC: &str = "announce_file"; + +pub const CORE_TOPICS: [GossipKind; 2] = [GossipKind::FindFile, GossipKind::AnnounceFile]; + +/// A gossipsub topic which encapsulates the type of messages that should be sent and received over +/// the pubsub protocol and the way the messages should be encoded. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct GossipTopic { + /// The encoding of the topic. + encoding: GossipEncoding, + /// The kind of topic. + kind: GossipKind, +} + +/// Enum that brings these topics into the rust type system. +// NOTE: There is intentionally no unknown type here. We only allow known gossipsub topics. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, AsRefStr)] +#[strum(serialize_all = "snake_case")] +pub enum GossipKind { + Example, + FindFile, + AnnounceFile, +} + +/// The known encoding types for gossipsub messages. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Default)] +pub enum GossipEncoding { + /// Messages are encoded with SSZSnappy. + #[default] + SSZSnappy, +} + +impl GossipTopic { + pub fn new(kind: GossipKind, encoding: GossipEncoding) -> Self { + GossipTopic { encoding, kind } + } + + /// Returns the encoding type for the gossipsub topic. + pub fn encoding(&self) -> &GossipEncoding { + &self.encoding + } + + /// Returns the kind of message expected on the gossipsub topic. + pub fn kind(&self) -> &GossipKind { + &self.kind + } + + pub fn decode(topic: &str) -> Result { + let topic_parts: Vec<&str> = topic.split('/').collect(); + if topic_parts.len() == 4 && topic_parts[1] == TOPIC_PREFIX { + let encoding = match topic_parts[3] { + SSZ_SNAPPY_ENCODING_POSTFIX => GossipEncoding::SSZSnappy, + _ => return Err(format!("Unknown encoding: {}", topic)), + }; + + let kind = match topic_parts[2] { + EXAMPLE_TOPIC => GossipKind::Example, + FIND_FILE_TOPIC => GossipKind::FindFile, + ANNOUNCE_FILE_TOPIC => GossipKind::AnnounceFile, + _ => return Err(format!("Unknown topic: {}", topic)), + }; + + return Ok(GossipTopic { encoding, kind }); + } + + Err(format!("Unknown topic: {}", topic)) + } +} + +impl From for Topic { + fn from(topic: GossipTopic) -> Topic { + Topic::new(topic) + } +} + +impl From for String { + fn from(topic: GossipTopic) -> String { + let encoding = match topic.encoding { + GossipEncoding::SSZSnappy => SSZ_SNAPPY_ENCODING_POSTFIX, + }; + + let kind = match topic.kind { + GossipKind::Example => EXAMPLE_TOPIC, + GossipKind::FindFile => FIND_FILE_TOPIC, + GossipKind::AnnounceFile => ANNOUNCE_FILE_TOPIC, + }; + + format!("/{}/{}/{}", TOPIC_PREFIX, kind, encoding) + } +} + +impl std::fmt::Display for GossipTopic { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let encoding = match self.encoding { + GossipEncoding::SSZSnappy => SSZ_SNAPPY_ENCODING_POSTFIX, + }; + + let kind = match self.kind { + GossipKind::Example => EXAMPLE_TOPIC, + GossipKind::FindFile => FIND_FILE_TOPIC, + GossipKind::AnnounceFile => ANNOUNCE_FILE_TOPIC, + }; + + write!(f, "/{}/{}/{}", TOPIC_PREFIX, kind, encoding) + } +} + +#[cfg(test)] +mod tests { + use super::GossipKind::*; + use super::*; + + const BAD_PREFIX: &str = "tezos"; + const BAD_ENCODING: &str = "rlp"; + const BAD_KIND: &str = "blocks"; + + fn topics() -> Vec { + let mut topics = Vec::new(); + + for encoding in [GossipEncoding::SSZSnappy].iter() { + { + let kind = &Example; + topics.push(GossipTopic::new(kind.clone(), encoding.clone()).into()); + } + } + topics + } + + fn create_topic(prefix: &str, kind: &str, encoding: &str) -> String { + format!("/{}/{}/{}", prefix, kind, encoding) + } + + #[test] + fn test_decode() { + for topic in topics().iter() { + assert!(GossipTopic::decode(topic.as_str()).is_ok()); + } + } + + #[test] + fn test_decode_malicious() { + let bad_prefix_str = create_topic(BAD_PREFIX, EXAMPLE_TOPIC, SSZ_SNAPPY_ENCODING_POSTFIX); + assert!(GossipTopic::decode(bad_prefix_str.as_str()).is_err()); + + let bad_kind_str = create_topic(TOPIC_PREFIX, BAD_KIND, SSZ_SNAPPY_ENCODING_POSTFIX); + assert!(GossipTopic::decode(bad_kind_str.as_str()).is_err()); + + let bad_encoding_str = create_topic(TOPIC_PREFIX, EXAMPLE_TOPIC, BAD_ENCODING); + assert!(GossipTopic::decode(bad_encoding_str.as_str()).is_err()); + + // Extra parts + assert!( + GossipTopic::decode("/eth2/beacon_block/ssz_snappy/yolo").is_err(), + "should have exactly 4 parts" + ); + // Empty string + assert!(GossipTopic::decode("").is_err()); + // Empty parts + assert!(GossipTopic::decode("////").is_err()); + } + + #[test] + fn test_as_str_ref() { + assert_eq!("example", Example.as_ref()); + } +} diff --git a/node/network/tests/common/behaviour.rs b/node/network/tests/common/behaviour.rs new file mode 100644 index 0000000..76eecfc --- /dev/null +++ b/node/network/tests/common/behaviour.rs @@ -0,0 +1,394 @@ +// NOTE: Taken from libp2p's swarm's testing utils. +// +// Copyright 2020 Parity Technologies (UK) Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +use std::collections::HashMap; +use std::task::{Context, Poll}; + +use libp2p::core::connection::{ConnectedPoint, ConnectionId, ListenerId}; +use libp2p::swarm::handler::{ConnectionHandler, DummyConnectionHandler, IntoConnectionHandler}; +use libp2p::swarm::{DialError, NetworkBehaviour, NetworkBehaviourAction, PollParameters}; +use libp2p::{Multiaddr, PeerId}; + +/// A `MockBehaviour` is a `NetworkBehaviour` that allows for +/// the instrumentation of return values, without keeping +/// any further state. +pub struct MockBehaviour< + THandler = DummyConnectionHandler, + TOutEvent = ::OutEvent, +> where + THandler: ConnectionHandler, +{ + /// The prototype protocols handler that is cloned for every + /// invocation of `new_handler`. + pub handler_proto: THandler, + /// The addresses to return from `addresses_of_peer`. + pub addresses: HashMap>, + /// The next action to return from `poll`. + /// + /// An action is only returned once. + pub next_action: Option>, +} + +impl MockBehaviour +where + THandler: ConnectionHandler, +{ + pub fn new(handler_proto: THandler) -> Self { + MockBehaviour { + handler_proto, + addresses: HashMap::new(), + next_action: None, + } + } +} + +impl NetworkBehaviour for MockBehaviour +where + THandler: ConnectionHandler + Clone, + THandler::OutEvent: Clone, + TOutEvent: Send + 'static, +{ + type ConnectionHandler = THandler; + type OutEvent = TOutEvent; + + fn new_handler(&mut self) -> Self::ConnectionHandler { + self.handler_proto.clone() + } + + fn addresses_of_peer(&mut self, p: &PeerId) -> Vec { + self.addresses.get(p).map_or(Vec::new(), |v| v.clone()) + } + + fn inject_event(&mut self, _: PeerId, _: ConnectionId, _: THandler::OutEvent) {} + + fn poll( + &mut self, + _: &mut Context, + _: &mut impl PollParameters, + ) -> Poll> { + Option::take(&mut self.next_action).map_or(Poll::Pending, Poll::Ready) + } +} + +/// A `CallTraceBehaviour` is a `NetworkBehaviour` that tracks invocations of callback methods and +/// their arguments, wrapping around an inner behaviour. It ensures certain invariants are met. +pub struct CallTraceBehaviour +where + TInner: NetworkBehaviour, +{ + inner: TInner, + + pub addresses_of_peer: Vec, + pub inject_connection_established: Vec<(PeerId, ConnectionId, ConnectedPoint, usize)>, + pub inject_connection_closed: Vec<(PeerId, ConnectionId, ConnectedPoint, usize)>, + pub inject_event: Vec<( + PeerId, + ConnectionId, + <::Handler as ConnectionHandler>::OutEvent, + )>, + pub inject_dial_failure: Vec>, + pub inject_new_listener: Vec, + pub inject_new_listen_addr: Vec<(ListenerId, Multiaddr)>, + pub inject_new_external_addr: Vec, + pub inject_expired_listen_addr: Vec<(ListenerId, Multiaddr)>, + pub inject_expired_external_addr: Vec, + pub inject_listener_error: Vec, + pub inject_listener_closed: Vec<(ListenerId, bool)>, + pub poll: usize, +} + +impl CallTraceBehaviour +where + TInner: NetworkBehaviour, +{ + pub fn new(inner: TInner) -> Self { + Self { + inner, + addresses_of_peer: Vec::new(), + inject_connection_established: Vec::new(), + inject_connection_closed: Vec::new(), + inject_event: Vec::new(), + inject_dial_failure: Vec::new(), + inject_new_listener: Vec::new(), + inject_new_listen_addr: Vec::new(), + inject_new_external_addr: Vec::new(), + inject_expired_listen_addr: Vec::new(), + inject_expired_external_addr: Vec::new(), + inject_listener_error: Vec::new(), + inject_listener_closed: Vec::new(), + poll: 0, + } + } + + #[allow(dead_code)] + pub fn reset(&mut self) { + self.addresses_of_peer = Vec::new(); + self.inject_connection_established = Vec::new(); + self.inject_connection_closed = Vec::new(); + self.inject_event = Vec::new(); + self.inject_dial_failure = Vec::new(); + self.inject_new_listen_addr = Vec::new(); + self.inject_new_external_addr = Vec::new(); + self.inject_expired_listen_addr = Vec::new(); + self.inject_listener_error = Vec::new(); + self.inject_listener_closed = Vec::new(); + self.poll = 0; + } + + pub fn inner(&mut self) -> &mut TInner { + &mut self.inner + } + + /// Checks that when the expected number of closed connection notifications are received, a + /// given number of expected disconnections have been received as well. + /// + /// Returns if the first condition is met. + pub fn assert_disconnected( + &self, + expected_closed_connections: usize, + expected_disconnections: usize, + ) -> bool { + if self.inject_connection_closed.len() == expected_closed_connections { + assert_eq!( + self.inject_connection_closed + .iter() + .filter(|(.., remaining_established)| { *remaining_established == 0 }) + .count(), + expected_disconnections + ); + return true; + } + + false + } + + /// Checks that when the expected number of established connection notifications are received, + /// a given number of expected connections have been received as well. + /// + /// Returns if the first condition is met. + pub fn assert_connected( + &self, + expected_established_connections: usize, + expected_connections: usize, + ) -> bool { + if self.inject_connection_established.len() == expected_established_connections { + assert_eq!( + self.inject_connection_established + .iter() + .filter(|(.., reported_aditional_connections)| { + *reported_aditional_connections == 0 + }) + .count(), + expected_connections + ); + return true; + } + + false + } +} + +impl NetworkBehaviour for CallTraceBehaviour +where + TInner: NetworkBehaviour, + <::Handler as ConnectionHandler>::OutEvent: + Clone, +{ + type ConnectionHandler = TInner::ConnectionHandler; + type OutEvent = TInner::OutEvent; + + fn new_handler(&mut self) -> Self::ConnectionHandler { + self.inner.new_handler() + } + + fn addresses_of_peer(&mut self, p: &PeerId) -> Vec { + self.addresses_of_peer.push(*p); + self.inner.addresses_of_peer(p) + } + + fn inject_connection_established( + &mut self, + p: &PeerId, + c: &ConnectionId, + e: &ConnectedPoint, + errors: Option<&Vec>, + other_established: usize, + ) { + let mut other_peer_connections = self + .inject_connection_established + .iter() + .rev() // take last to first + .filter_map(|(peer, .., other_established)| { + if p == peer { + Some(other_established) + } else { + None + } + }) + .take(other_established); + + // We are informed that there are `other_established` additional connections. Ensure that the + // number of previous connections is consistent with this + if let Some(&prev) = other_peer_connections.next() { + if prev < other_established { + assert_eq!( + prev, + other_established - 1, + "Inconsistent connection reporting" + ) + } + assert_eq!(other_peer_connections.count(), other_established - 1); + } else { + assert_eq!(other_established, 0) + } + self.inject_connection_established + .push((*p, *c, e.clone(), other_established)); + self.inner + .inject_connection_established(p, c, e, errors, other_established); + } + + fn inject_connection_closed( + &mut self, + p: &PeerId, + c: &ConnectionId, + e: &ConnectedPoint, + handler: ::Handler, + remaining_established: usize, + ) { + let mut other_closed_connections = self + .inject_connection_established + .iter() + .rev() // take last to first + .filter_map(|(peer, .., remaining_established)| { + if p == peer { + Some(remaining_established) + } else { + None + } + }) + .take(remaining_established); + + // We are informed that there are `other_established` additional connections. Ensure that the + // number of previous connections is consistent with this + if let Some(&prev) = other_closed_connections.next() { + if prev < remaining_established { + assert_eq!( + prev, + remaining_established - 1, + "Inconsistent closed connection reporting" + ) + } + assert_eq!(other_closed_connections.count(), remaining_established - 1); + } else { + assert_eq!(remaining_established, 0) + } + assert!( + self.inject_connection_established + .iter() + .any(|(peer, conn_id, endpoint, _)| (peer, conn_id, endpoint) == (p, c, e)), + "`inject_connection_closed` is called only for connections for \ + which `inject_connection_established` was called first." + ); + self.inject_connection_closed + .push((*p, *c, e.clone(), remaining_established)); + self.inner + .inject_connection_closed(p, c, e, handler, remaining_established); + } + + fn inject_event( + &mut self, + p: PeerId, + c: ConnectionId, + e: <::Handler as ConnectionHandler>::OutEvent, + ) { + assert!( + self.inject_connection_established + .iter() + .any(|(peer_id, conn_id, ..)| *peer_id == p && c == *conn_id), + "`inject_event` is called for reported connections." + ); + assert!( + !self + .inject_connection_closed + .iter() + .any(|(peer_id, conn_id, ..)| *peer_id == p && c == *conn_id), + "`inject_event` is never called for closed connections." + ); + + self.inject_event.push((p, c, e.clone())); + self.inner.inject_event(p, c, e); + } + + fn inject_dial_failure( + &mut self, + p: Option, + handler: Self::ConnectionHandler, + error: &DialError, + ) { + self.inject_dial_failure.push(p); + self.inner.inject_dial_failure(p, handler, error); + } + + fn inject_new_listener(&mut self, id: ListenerId) { + self.inject_new_listener.push(id); + self.inner.inject_new_listener(id); + } + + fn inject_new_listen_addr(&mut self, id: ListenerId, a: &Multiaddr) { + self.inject_new_listen_addr.push((id, a.clone())); + self.inner.inject_new_listen_addr(id, a); + } + + fn inject_expired_listen_addr(&mut self, id: ListenerId, a: &Multiaddr) { + self.inject_expired_listen_addr.push((id, a.clone())); + self.inner.inject_expired_listen_addr(id, a); + } + + fn inject_new_external_addr(&mut self, a: &Multiaddr) { + self.inject_new_external_addr.push(a.clone()); + self.inner.inject_new_external_addr(a); + } + + fn inject_expired_external_addr(&mut self, a: &Multiaddr) { + self.inject_expired_external_addr.push(a.clone()); + self.inner.inject_expired_external_addr(a); + } + + fn inject_listener_error(&mut self, l: ListenerId, e: &(dyn std::error::Error + 'static)) { + self.inject_listener_error.push(l); + self.inner.inject_listener_error(l, e); + } + + fn inject_listener_closed(&mut self, l: ListenerId, r: Result<(), &std::io::Error>) { + self.inject_listener_closed.push((l, r.is_ok())); + self.inner.inject_listener_closed(l, r); + } + + fn poll( + &mut self, + cx: &mut Context, + args: &mut impl PollParameters, + ) -> Poll> { + self.poll += 1; + self.inner.poll(cx, args) + } +} diff --git a/node/network/tests/common/mod.rs b/node/network/tests/common/mod.rs new file mode 100644 index 0000000..ac7f019 --- /dev/null +++ b/node/network/tests/common/mod.rs @@ -0,0 +1,175 @@ +#![cfg(test)] + +use libp2p::gossipsub::GossipsubConfigBuilder; +use network::Enr; +use network::EnrExt; +use network::Multiaddr; +use network::Service as LibP2PService; +use network::{Libp2pEvent, NetworkConfig}; +use std::sync::Weak; +use std::time::Duration; +use tokio::runtime::Runtime; +use tracing::{debug, error}; +use unused_port::unused_tcp_port; + +#[allow(clippy::type_complexity)] +#[allow(unused)] +pub mod behaviour; +#[allow(clippy::type_complexity)] +#[allow(unused)] +pub mod swarm; + +type ReqId = usize; + +use tempfile::Builder as TempBuilder; +use tokio::sync::mpsc::unbounded_channel; + +pub struct Libp2pInstance(LibP2PService, exit_future::Signal); + +impl std::ops::Deref for Libp2pInstance { + type Target = LibP2PService; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for Libp2pInstance { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +pub fn build_config(port: u16, mut boot_nodes: Vec) -> NetworkConfig { + let mut config = NetworkConfig::default(); + let path = TempBuilder::new() + .prefix(&format!("libp2p_test{}", port)) + .tempdir() + .unwrap(); + + config.libp2p_port = port; // tcp port + config.discovery_port = port; // udp port + config.enr_tcp_port = Some(port); + config.enr_udp_port = Some(port); + config.enr_address = Some("127.0.0.1".parse().unwrap()); + config.boot_nodes_enr.append(&mut boot_nodes); + config.network_dir = path.into_path(); + // Reduce gossipsub heartbeat parameters + config.gs_config = GossipsubConfigBuilder::from(config.gs_config) + .heartbeat_initial_delay(Duration::from_millis(500)) + .heartbeat_interval(Duration::from_millis(500)) + .build() + .unwrap(); + config +} + +pub async fn build_libp2p_instance(rt: Weak, boot_nodes: Vec) -> Libp2pInstance { + let port = unused_tcp_port().unwrap(); + let config = build_config(port, boot_nodes); + // launch libp2p service + + let (signal, exit) = exit_future::signal(); + let (shutdown_tx, _) = futures::channel::mpsc::channel(1); + let executor = task_executor::TaskExecutor::new(rt, exit, shutdown_tx); + let libp2p_context = network::Context { config: &config }; + let (sender, _) = unbounded_channel(); + Libp2pInstance( + LibP2PService::new(executor, sender, libp2p_context) + .await + .expect("should build libp2p instance") + .2, + signal, + ) +} + +#[allow(dead_code)] +pub fn get_enr(node: &LibP2PService) -> Enr { + node.swarm.behaviour().local_enr() +} + +// Returns `n` libp2p peers in fully connected topology. +#[allow(dead_code)] +pub async fn build_full_mesh(rt: Weak, n: usize) -> Vec { + let mut nodes = Vec::with_capacity(n); + for _ in 0..n { + nodes.push(build_libp2p_instance(rt.clone(), vec![]).await); + } + let multiaddrs: Vec = nodes + .iter() + .map(|x| get_enr(x).multiaddr()[1].clone()) + .collect(); + + for (i, node) in nodes.iter_mut().enumerate().take(n) { + for (j, multiaddr) in multiaddrs.iter().enumerate().skip(i) { + if i != j { + match libp2p::Swarm::dial(&mut node.swarm, multiaddr.clone()) { + Ok(()) => debug!("Connected"), + Err(_) => error!("Failed to connect"), + }; + } + } + } + nodes +} + +// Constructs a pair of nodes with separate loggers. The sender dials the receiver. +// This returns a (sender, receiver) pair. +#[allow(dead_code)] +pub async fn build_node_pair(rt: Weak) -> (Libp2pInstance, Libp2pInstance) { + let mut sender = build_libp2p_instance(rt.clone(), vec![]).await; + let mut receiver = build_libp2p_instance(rt, vec![]).await; + + let receiver_multiaddr = receiver.swarm.behaviour_mut().local_enr().multiaddr()[1].clone(); + + // let the two nodes set up listeners + let sender_fut = async { + loop { + if let Libp2pEvent::NewListenAddr(_) = sender.next_event().await { + return; + } + } + }; + let receiver_fut = async { + loop { + if let Libp2pEvent::NewListenAddr(_) = receiver.next_event().await { + return; + } + } + }; + + let joined = futures::future::join(sender_fut, receiver_fut); + + // wait for either both nodes to listen or a timeout + tokio::select! { + _ = tokio::time::sleep(Duration::from_millis(500)) => {} + _ = joined => {} + } + + match libp2p::Swarm::dial(&mut sender.swarm, receiver_multiaddr.clone()) { + Ok(()) => { + debug!(address = %format!("{:?}", receiver_multiaddr), "Sender dialed receiver") + } + Err(_) => error!("Dialing failed"), + }; + (sender, receiver) +} + +// Returns `n` peers in a linear topology +#[allow(dead_code)] +pub async fn build_linear(rt: Weak, n: usize) -> Vec { + let mut nodes = Vec::with_capacity(n); + for _ in 0..n { + nodes.push(build_libp2p_instance(rt.clone(), vec![]).await); + } + + let multiaddrs: Vec = nodes + .iter() + .map(|x| get_enr(x).multiaddr()[1].clone()) + .collect(); + for i in 0..n - 1 { + match libp2p::Swarm::dial(&mut nodes[i].swarm, multiaddrs[i + 1].clone()) { + Ok(()) => debug!("Connected"), + Err(_) => error!("Failed to connect"), + }; + } + nodes +} diff --git a/node/network/tests/common/swarm.rs b/node/network/tests/common/swarm.rs new file mode 100644 index 0000000..224718d --- /dev/null +++ b/node/network/tests/common/swarm.rs @@ -0,0 +1,99 @@ +use std::collections::HashMap; +use std::pin::Pin; + +use super::behaviour::{CallTraceBehaviour, MockBehaviour}; + +use futures::stream::Stream; +use futures::task::{Context, Poll}; +use libp2p::swarm::handler::ConnectionHandler; +use libp2p::swarm::{IntoConnectionHandler, NetworkBehaviour, Swarm, SwarmBuilder, SwarmEvent}; +use libp2p::{PeerId, Transport}; + +use futures::StreamExt; + +pub fn new_test_swarm(behaviour: B) -> Swarm +where + B: NetworkBehaviour, +{ + let id_keys = libp2p::identity::Keypair::generate_ed25519(); + let local_public_key = id_keys.public(); + let transport = libp2p::core::transport::MemoryTransport + .upgrade(libp2p::core::upgrade::Version::V1) + .authenticate(libp2p::plaintext::PlainText2Config { + local_public_key: local_public_key.clone(), + }) + .multiplex(libp2p::yamux::YamuxConfig::default()) + .boxed(); + SwarmBuilder::new(transport, behaviour, local_public_key.into()).build() +} + +pub fn random_multiaddr() -> libp2p::multiaddr::Multiaddr { + libp2p::multiaddr::Protocol::Memory(rand::random::()).into() +} + +/// Bind a memory multiaddr to a compatible swarm. +pub async fn bind_listener( + swarm: &mut Swarm, +) -> libp2p::multiaddr::Multiaddr { + swarm.listen_on(random_multiaddr()).unwrap(); + match swarm.select_next_some().await { + SwarmEvent::NewListenAddr { + listener_id: _, + address, + } => address, + _ => panic!("Testing swarm's first event should be a new listener"), + } +} + +#[derive(Default)] +pub struct SwarmPool { + swarms: HashMap>, +} + +impl SwarmPool { + pub fn with_capacity(capacity: usize) -> Self { + Self { + swarms: HashMap::with_capacity(capacity), + } + } + pub fn insert(&mut self, swarm: Swarm) -> PeerId { + let peer_id = *swarm.local_peer_id(); + self.swarms.insert(peer_id, swarm); + peer_id + } + + pub fn remove(&mut self, peer_id: &PeerId) { + self.swarms.remove(peer_id); + } + + pub fn get_mut(&mut self, peer_id: &PeerId) -> Option<&mut Swarm> { + self.swarms.get_mut(peer_id) + } + + pub fn swarms(&self) -> &HashMap> { + &self.swarms + } + + pub fn swarms_mut(&mut self) -> &mut HashMap> { + &mut self.swarms + } +} + +impl Stream for SwarmPool +where + B: NetworkBehaviour, + ::ConnectionHandler: ConnectionHandler, +{ + type Item = (PeerId, + SwarmEvent<::OutEvent, <<::ConnectionHandler as IntoConnectionHandler>::Handler as ConnectionHandler>::Error>); + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mut polls = self + .get_mut() + .swarms + .iter_mut() + .map(|(&peer_id, swarm)| swarm.map(move |ev| (peer_id, ev))) + .collect::>(); + polls.poll_next_unpin(cx) + } +} diff --git a/node/network/tests/gossipsub_tests.rs b/node/network/tests/gossipsub_tests.rs new file mode 100644 index 0000000..c5b661c --- /dev/null +++ b/node/network/tests/gossipsub_tests.rs @@ -0,0 +1,171 @@ +/* These are temporarily disabled due to their non-deterministic behaviour and impending update to + * gossipsub 1.1. We leave these here as a template for future test upgrades + + +#![cfg(test)] +use crate::types::GossipEncoding; +use ::types::{BeaconBlock, EthSpec, MinimalEthSpec, Signature, SignedBeaconBlock}; +use lighthouse_network::*; +use slog::{debug, Level}; + +type E = MinimalEthSpec; + +mod common; + +/* Gossipsub tests */ +// Note: The aim of these tests is not to test the robustness of the gossip network +// but to check if the gossipsub implementation is behaving according to the specifications. + +// Test if gossipsub message are forwarded by nodes with a simple linear topology. +// +// Topology used in test +// +// node1 <-> node2 <-> node3 ..... <-> node(n-1) <-> node(n) + +#[tokio::test] +async fn test_gossipsub_forward() { + // set up the logging. The level and enabled or not + let log = common::build_log(Level::Info, false); + + let num_nodes = 20; + let mut nodes = common::build_linear(log.clone(), num_nodes); + let mut received_count = 0; + let spec = E::default_spec(); + let empty_block = BeaconBlock::empty(&spec); + let signed_block = SignedBeaconBlock { + message: empty_block, + signature: Signature::empty_signature(), + }; + let pubsub_message = PubsubMessage::BeaconBlock(Box::new(signed_block)); + let publishing_topic: String = pubsub_message + .topics(GossipEncoding::default(), [0, 0, 0, 0]) + .first() + .unwrap() + .clone() + .into(); + let mut subscribed_count = 0; + let fut = async move { + for node in nodes.iter_mut() { + loop { + match node.next_event().await { + Libp2pEvent::Behaviour(b) => match b { + BehaviourEvent::PubsubMessage { + topics, + message, + source, + id, + } => { + assert_eq!(topics.len(), 1); + // Assert topic is the published topic + assert_eq!( + topics.first().unwrap(), + &TopicHash::from_raw(publishing_topic.clone()) + ); + // Assert message received is the correct one + assert_eq!(message, pubsub_message.clone()); + received_count += 1; + // Since `propagate_message` is false, need to propagate manually + node.swarm.propagate_message(&source, id); + // Test should succeed if all nodes except the publisher receive the message + if received_count == num_nodes - 1 { + debug!(log.clone(), "Received message at {} nodes", num_nodes - 1); + return; + } + } + BehaviourEvent::PeerSubscribed(_, topic) => { + // Publish on beacon block topic + if topic == TopicHash::from_raw(publishing_topic.clone()) { + subscribed_count += 1; + // Every node except the corner nodes are connected to 2 nodes. + if subscribed_count == (num_nodes * 2) - 2 { + node.swarm.publish(vec![pubsub_message.clone()]); + } + } + } + _ => break, + }, + _ => break, + } + } + } + }; + + tokio::select! { + _ = fut => {} + _ = tokio::time::delay_for(tokio::time::Duration::from_millis(800)) => { + panic!("Future timed out"); + } + } +} + +// Test publishing of a message with a full mesh for the topic +// Not very useful but this is the bare minimum functionality. +#[tokio::test] +async fn test_gossipsub_full_mesh_publish() { + // set up the logging. The level and enabled or not + let log = common::build_log(Level::Debug, false); + + // Note: This test does not propagate gossipsub messages. + // Having `num_nodes` > `mesh_n_high` may give inconsistent results + // as nodes may get pruned out of the mesh before the gossipsub message + // is published to them. + let num_nodes = 12; + let mut nodes = common::build_full_mesh(log, num_nodes); + let mut publishing_node = nodes.pop().unwrap(); + let spec = E::default_spec(); + let empty_block = BeaconBlock::empty(&spec); + let signed_block = SignedBeaconBlock { + message: empty_block, + signature: Signature::empty_signature(), + }; + let pubsub_message = PubsubMessage::BeaconBlock(Box::new(signed_block)); + let publishing_topic: String = pubsub_message + .topics(GossipEncoding::default(), [0, 0, 0, 0]) + .first() + .unwrap() + .clone() + .into(); + let mut subscribed_count = 0; + let mut received_count = 0; + let fut = async move { + for node in nodes.iter_mut() { + while let Libp2pEvent::Behaviour(BehaviourEvent::PubsubMessage { + topics, + message, + .. + }) = node.next_event().await + { + assert_eq!(topics.len(), 1); + // Assert topic is the published topic + assert_eq!( + topics.first().unwrap(), + &TopicHash::from_raw(publishing_topic.clone()) + ); + // Assert message received is the correct one + assert_eq!(message, pubsub_message.clone()); + received_count += 1; + if received_count == num_nodes - 1 { + return; + } + } + } + while let Libp2pEvent::Behaviour(BehaviourEvent::PeerSubscribed(_, topic)) = + publishing_node.next_event().await + { + // Publish on beacon block topic + if topic == TopicHash::from_raw(publishing_topic.clone()) { + subscribed_count += 1; + if subscribed_count == num_nodes - 1 { + publishing_node.swarm.publish(vec![pubsub_message.clone()]); + } + } + } + }; + tokio::select! { + _ = fut => {} + _ = tokio::time::delay_for(tokio::time::Duration::from_millis(800)) => { + panic!("Future timed out"); + } + } +} +*/ diff --git a/node/network/tests/pm_tests.rs b/node/network/tests/pm_tests.rs new file mode 100644 index 0000000..414e1aa --- /dev/null +++ b/node/network/tests/pm_tests.rs @@ -0,0 +1,197 @@ +mod common; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; + +use common::{ + behaviour::{CallTraceBehaviour, MockBehaviour}, + swarm, +}; +use network::{ + peer_manager::{self, config::Config, PeerManagerEvent}, + NetworkGlobals, PeerAction, PeerInfo, PeerManager, ReportSource, +}; + +use futures::StreamExt; +use libp2p::{ + core::either::EitherError, + swarm::SwarmEvent, + swarm::{handler::DummyConnectionHandler, DummyBehaviour, KeepAlive, Swarm}, + NetworkBehaviour, +}; + +use tracing::debug; + +/// Struct that mimics the lighthouse_network::Service with respect to handling peer manager +/// events. +// TODO: make this a real struct for more accurate testing. +struct Service { + swarm: Swarm, +} + +impl Service { + async fn select_next_some(&mut self) -> SwarmEvent> { + let ev = self.swarm.select_next_some().await; + match &ev { + SwarmEvent::Behaviour(Ev(PeerManagerEvent::Banned(peer_id, _addr_vec))) => { + self.swarm.ban_peer_id(*peer_id); + } + SwarmEvent::Behaviour(Ev(PeerManagerEvent::UnBanned(peer_id, _addr_vec))) => { + self.swarm.unban_peer_id(*peer_id); + } + SwarmEvent::Behaviour(Ev(PeerManagerEvent::DisconnectPeer(peer_id, _reason))) => { + // directly disconnect here. + let _ = self.swarm.disconnect_peer_id(*peer_id); + } + _ => {} + } + ev + } +} + +#[derive(Debug)] +struct Ev(PeerManagerEvent); +impl From for Ev { + fn from(_: void::Void) -> Self { + unreachable!("No events are emmited") + } +} +impl From for Ev { + fn from(ev: PeerManagerEvent) -> Self { + Ev(ev) + } +} + +#[derive(NetworkBehaviour)] +#[behaviour(out_event = "Ev")] +struct Behaviour { + pm_call_trace: CallTraceBehaviour, + sibling: MockBehaviour, +} + +impl Behaviour { + fn new(pm: PeerManager) -> Self { + Behaviour { + pm_call_trace: CallTraceBehaviour::new(pm), + sibling: MockBehaviour::new(DummyConnectionHandler { + // The peer manager votes No, so we make sure the combined handler stays alive this + // way. + keep_alive: KeepAlive::Yes, + }), + } + } +} + +#[tokio::test] +async fn banned_peers_consistency() { + let globals: Arc = Arc::new(NetworkGlobals::new_test_globals()); + + // Build the peer manager. + let (mut pm_service, pm_addr) = { + let pm_config = Config { + discovery_enabled: false, + ..Default::default() + }; + let pm = PeerManager::new(pm_config, globals.clone()).await.unwrap(); + let mut pm_swarm = swarm::new_test_swarm(Behaviour::new(pm)); + let pm_addr = swarm::bind_listener(&mut pm_swarm).await; + let service = Service { swarm: pm_swarm }; + (service, pm_addr) + }; + + let excess_banned_peers = 15; + let peers_to_ban = peer_manager::peerdb::MAX_BANNED_PEERS + excess_banned_peers; + + // Build all the dummy peers needed. + let (mut swarm_pool, peers) = { + let mut pool = swarm::SwarmPool::with_capacity(peers_to_ban); + let mut peers = HashSet::with_capacity(peers_to_ban); + for _ in 0..peers_to_ban { + let mut peer_swarm = + swarm::new_test_swarm(DummyBehaviour::with_keep_alive(KeepAlive::Yes)); + let _peer_addr = swarm::bind_listener(&mut peer_swarm).await; + // It is ok to dial all at the same time since the swarm handles an event at a time. + peer_swarm.dial(pm_addr.clone()).unwrap(); + let peer_id = pool.insert(peer_swarm); + peers.insert(peer_id); + } + (pool, peers) + }; + + // we track banned peers at the swarm level here since there is no access to that info. + let mut swarm_banned_peers = HashMap::with_capacity(peers_to_ban); + let mut peers_unbanned = 0; + let timeout = tokio::time::sleep(tokio::time::Duration::from_secs(30)); + futures::pin_mut!(timeout); + + loop { + // poll the pm and dummy swarms. + tokio::select! { + pm_event = pm_service.select_next_some() => { + debug!("[PM] {:?}", pm_event); + match pm_event { + SwarmEvent::Behaviour(Ev(ev)) => match ev { + PeerManagerEvent::Banned(peer_id, _) => { + let has_been_unbanned = false; + swarm_banned_peers.insert(peer_id, has_been_unbanned); + } + PeerManagerEvent::UnBanned(peer_id, _) => { + *swarm_banned_peers.get_mut(&peer_id).expect("Unbanned peer must be banned first") = true; + peers_unbanned += 1; + } + _ => {} + } + SwarmEvent::ConnectionEstablished { + peer_id, + endpoint: _, + num_established: _, + concurrent_dial_errors: _, + } => { + assert!(peers.contains(&peer_id)); + // now we report the peer as banned. + pm_service + .swarm + .behaviour_mut() + .pm_call_trace + .inner() + .report_peer( + &peer_id, + PeerAction::Fatal, + ReportSource::Processor, + None, + "" + ); + }, + _ => {} + } + } + Some((_peer_id, _peer_ev)) = swarm_pool.next() => { + // we need to poll the swarms to keep the peers going + } + _ = timeout.as_mut() => { + panic!("Test timeout.") + } + } + + if peers_unbanned == excess_banned_peers { + let pdb = globals.peers.read(); + let inconsistencies = swarm_banned_peers + .into_iter() + .map(|(peer_id, was_unbanned)| { + was_unbanned + != pdb.peer_info(&peer_id).map_or( + false, /* We forgot about a banned peer */ + PeerInfo::is_banned, + ) + }); + assert_eq!( + inconsistencies + .filter(|is_consistent| *is_consistent) + .count(), + peers_to_ban + ); + return; + } + } +} diff --git a/node/network/tests/rpc_tests.rs b/node/network/tests/rpc_tests.rs new file mode 100644 index 0000000..f931cca --- /dev/null +++ b/node/network/tests/rpc_tests.rs @@ -0,0 +1,393 @@ +#![cfg(test)] +use network::rpc::methods::*; +use network::{BehaviourEvent, Libp2pEvent, ReportSource, Request, Response}; +use ssz_types::VariableList; +use std::sync::Arc; +use std::time::Duration; +use tokio::runtime::Runtime; +use tokio::time::sleep; +use tracing::{debug, warn}; +use tracing_test::traced_test; + +mod common; + +// Tests the STATUS RPC message +#[test] +#[traced_test] +#[allow(clippy::single_match)] +fn test_status_rpc() { + let rt = Arc::new(Runtime::new().unwrap()); + + rt.block_on(async { + // get sender/receiver + let (mut sender, mut receiver) = common::build_node_pair(Arc::downgrade(&rt)).await; + + // Dummy STATUS RPC message + let rpc_request = Request::Status(StatusMessage { data: 2 }); + + // Dummy STATUS RPC message + let rpc_response = Response::Status(StatusMessage { data: 3 }); + + // build the sender future + let sender_future = async { + loop { + match sender.next_event().await { + Libp2pEvent::Behaviour(BehaviourEvent::PeerConnectedOutgoing(peer_id)) => { + // Send a STATUS message + debug!("Sending RPC"); + sender + .swarm + .behaviour_mut() + .send_request(peer_id, 10, rpc_request.clone()); + } + Libp2pEvent::Behaviour(BehaviourEvent::ResponseReceived { + peer_id: _, + id: 10, + response, + }) => { + // Should receive the RPC response + debug!("Sender Received"); + assert_eq!(response, rpc_response.clone()); + debug!("Sender Completed"); + return; + } + _ => {} + } + } + }; + + // build the receiver future + let receiver_future = async { + loop { + match receiver.next_event().await { + Libp2pEvent::Behaviour(BehaviourEvent::RequestReceived { + peer_id, + id, + request, + }) => { + if request == rpc_request { + // send the response + debug!("Receiver Received"); + receiver.swarm.behaviour_mut().send_successful_response( + peer_id, + id, + rpc_response.clone(), + ); + } + } + _ => {} // Ignore other events + } + } + }; + + tokio::select! { + _ = sender_future => {} + _ = receiver_future => {} + _ = sleep(Duration::from_secs(30)) => { + panic!("Future timed out"); + } + } + }) +} + +// Tests a streamed DataByHash RPC Message +#[test] +#[traced_test] +#[allow(clippy::single_match)] +fn test_data_by_hash_chunked_rpc() { + let messages_to_send = 6; + let rt = Arc::new(Runtime::new().unwrap()); + + rt.block_on(async { + // get sender/receiver + let (mut sender, mut receiver) = common::build_node_pair(Arc::downgrade(&rt)).await; + + // DataByHash Request + let rpc_request = Request::DataByHash(DataByHashRequest { + hashes: VariableList::from(vec![ + Hash256::from_low_u64_be(0), + Hash256::from_low_u64_be(0), + Hash256::from_low_u64_be(0), + Hash256::from_low_u64_be(0), + Hash256::from_low_u64_be(0), + Hash256::from_low_u64_be(0), + ]), + }); + + // DataByHash Response + let data = ZgsData { + hash: Hash256::from_low_u64_be(0), + }; + let rpc_response = Response::DataByHash(Some(Box::new(data))); + + // keep count of the number of messages received + let mut messages_received = 0; + let request_id = messages_to_send as usize; + + // build the sender future + let sender_future = async { + loop { + match sender.next_event().await { + Libp2pEvent::Behaviour(BehaviourEvent::PeerConnectedOutgoing(peer_id)) => { + // Send a DATA_BY_HASH message + debug!("Sending RPC"); + sender.swarm.behaviour_mut().send_request( + peer_id, + request_id, + rpc_request.clone(), + ); + } + Libp2pEvent::Behaviour(BehaviourEvent::ResponseReceived { + peer_id: _, + id: _, + response, + }) => { + warn!("Sender received a response: {:?}", &response); + match response { + Response::DataByHash(Some(_)) => { + assert_eq!(response, rpc_response.clone()); + messages_received += 1; + warn!("Chunk received"); + } + Response::DataByHash(None) => { + // should be exactly `messages_to_send` messages before terminating + assert_eq!(messages_received, messages_to_send); + // end the test + return; + } + _ => panic!("Invalid RPC received"), + } + } + _ => {} // Ignore other behaviour events + } + } + }; + + // build the receiver future + let receiver_future = async { + loop { + match receiver.next_event().await { + Libp2pEvent::Behaviour(BehaviourEvent::RequestReceived { + peer_id, + id, + request, + }) => { + if request == rpc_request { + // send the response + warn!("Receiver got request"); + for _ in 0..messages_to_send { + let rpc_response = rpc_response.clone(); + + receiver.swarm.behaviour_mut().send_successful_response( + peer_id, + id, + rpc_response.clone(), + ); + } + // send the stream termination + receiver.swarm.behaviour_mut().send_successful_response( + peer_id, + id, + Response::DataByHash(None), + ); + } + } + _ => {} // Ignore other events + } + } + }; + + tokio::select! { + _ = sender_future => {} + _ = receiver_future => {} + _ = sleep(Duration::from_secs(30)) => { + panic!("Future timed out"); + } + } + }) +} + +// Tests that a streamed DataByHash RPC Message terminates when all expected chunks were received +#[test] +#[traced_test] +fn test_data_by_hash_chunked_rpc_terminates_correctly() { + let messages_to_send = 10; + let extra_messages_to_send = 10; + let rt = Arc::new(Runtime::new().unwrap()); + + rt.block_on(async { + // get sender/receiver + let (mut sender, mut receiver) = common::build_node_pair(Arc::downgrade(&rt)).await; + + // DataByHash Request + let rpc_request = Request::DataByHash(DataByHashRequest { + hashes: VariableList::from( + (0..messages_to_send) + .map(|_| Hash256::from_low_u64_be(0)) + .collect::>(), + ), + }); + + // DataByHash Response + let data = ZgsData { + hash: Hash256::from_low_u64_be(0), + }; + let rpc_response = Response::DataByHash(Some(Box::new(data))); + + // keep count of the number of messages received + let mut messages_received: u64 = 0; + let request_id = messages_to_send as usize; + // build the sender future + let sender_future = async { + loop { + match sender.next_event().await { + Libp2pEvent::Behaviour(BehaviourEvent::PeerConnectedOutgoing(peer_id)) => { + // Send a STATUS message + debug!("Sending RPC"); + sender.swarm.behaviour_mut().send_request( + peer_id, + request_id, + rpc_request.clone(), + ); + } + Libp2pEvent::Behaviour(BehaviourEvent::ResponseReceived { + peer_id: _, + id: _, + response, + }) => + // Should receive the RPC response + { + debug!("Sender received a response"); + match response { + Response::DataByHash(Some(_)) => { + assert_eq!(response, rpc_response.clone()); + messages_received += 1; + } + Response::DataByHash(None) => { + // should be exactly 10 messages, as requested + assert_eq!(messages_received, messages_to_send); + } + _ => panic!("Invalid RPC received"), + } + } + + _ => {} // Ignore other behaviour events + } + } + }; + + // determine messages to send (PeerId, RequestId). If some, indicates we still need to send + // messages + let mut message_info = None; + // the number of messages we've sent + let mut messages_sent = 0; + let receiver_future = async { + loop { + // this future either drives the sending/receiving or times out allowing messages to be + // sent in the timeout + match futures::future::select( + Box::pin(receiver.next_event()), + Box::pin(tokio::time::sleep(Duration::from_secs(1))), + ) + .await + { + futures::future::Either::Left(( + Libp2pEvent::Behaviour(BehaviourEvent::RequestReceived { + peer_id, + id, + request, + }), + _, + )) => { + if request == rpc_request { + // send the response + warn!("Receiver got request"); + message_info = Some((peer_id, id)); + } + } + futures::future::Either::Right((_, _)) => {} // The timeout hit, send messages if required + _ => continue, + } + + // if we need to send messages send them here. This will happen after a delay + if message_info.is_some() { + messages_sent += 1; + let (peer_id, stream_id) = message_info.as_ref().unwrap(); + receiver.swarm.behaviour_mut().send_successful_response( + *peer_id, + *stream_id, + rpc_response.clone(), + ); + debug!("Sending message {}", messages_sent); + if messages_sent == messages_to_send + extra_messages_to_send { + // stop sending messages + return; + } + } + } + }; + + tokio::select! { + _ = sender_future => {} + _ = receiver_future => {} + _ = sleep(Duration::from_secs(30)) => { + panic!("Future timed out"); + } + } + }) +} + +// Tests a Goodbye RPC message +#[test] +#[traced_test] +#[allow(clippy::single_match)] +fn test_goodbye_rpc() { + let rt = Arc::new(Runtime::new().unwrap()); + + rt.block_on(async { + let (mut sender, mut receiver) = common::build_node_pair(Arc::downgrade(&rt)).await; + + // build the sender future + let sender_future = async { + loop { + match sender.next_event().await { + Libp2pEvent::Behaviour(BehaviourEvent::PeerConnectedOutgoing(peer_id)) => { + // Send a goodbye and disconnect + debug!("Sending RPC"); + sender.swarm.behaviour_mut().goodbye_peer( + &peer_id, + GoodbyeReason::IrrelevantNetwork, + ReportSource::SyncService, + ); + } + Libp2pEvent::Behaviour(BehaviourEvent::PeerDisconnected(_)) => { + return; + } + _ => {} // Ignore other RPC messages + } + } + }; + + // build the receiver future + let receiver_future = async { + loop { + match receiver.next_event().await { + Libp2pEvent::Behaviour(BehaviourEvent::PeerDisconnected(_)) => { + // Should receive sent RPC request + return; + } + _ => {} // Ignore other events + } + } + }; + + let total_future = futures::future::join(sender_future, receiver_future); + + tokio::select! { + _ = total_future => {} + _ = sleep(Duration::from_secs(30)) => { + panic!("Future timed out"); + } + } + }) +} diff --git a/node/router/Cargo.toml b/node/router/Cargo.toml new file mode 100644 index 0000000..b9c89be --- /dev/null +++ b/node/router/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "router" +version = "0.1.0" +edition = "2021" + +[dependencies] +chrono = "0.4.19" +error-chain = "0.12.4" +futures = "0.3.21" +file_location_cache = { path = "../file_location_cache" } +lazy_static = "1.4.0" +miner = { path = "../miner" } +network = { path = "../network" } +shared_types = { path = "../shared_types" } +storage = { path = "../storage" } +storage-async = { path = "../storage-async" } +sync = { path = "../sync" } +task_executor = { path = "../../common/task_executor" } +tokio = { version = "1.19.2", features = ["full"] } +tracing = "0.1.35" +rand = "0.8.5" + +[dev-dependencies] +channel = { path = "../../common/channel" } diff --git a/node/router/src/lib.rs b/node/router/src/lib.rs new file mode 100644 index 0000000..8b619d8 --- /dev/null +++ b/node/router/src/lib.rs @@ -0,0 +1,32 @@ +#[macro_use] +extern crate tracing; + +mod libp2p_event_handler; +mod peer_manager; +mod service; + +use network::Multiaddr; + +pub use crate::service::RouterService; + +#[derive(Debug, Clone)] +pub struct Config { + pub heartbeat_interval_secs: u64, + + pub idle_time_secs: u64, + pub max_idle_incoming_peers: usize, + pub max_idle_outgoing_peers: usize, + pub libp2p_nodes: Vec, +} + +impl Default for Config { + fn default() -> Self { + Self { + heartbeat_interval_secs: 5, + idle_time_secs: 180, + max_idle_incoming_peers: 12, + max_idle_outgoing_peers: 20, + libp2p_nodes: vec![], + } + } +} diff --git a/node/router/src/libp2p_event_handler.rs b/node/router/src/libp2p_event_handler.rs new file mode 100644 index 0000000..0b6c25f --- /dev/null +++ b/node/router/src/libp2p_event_handler.rs @@ -0,0 +1,786 @@ +use std::{ops::Neg, sync::Arc}; + +use file_location_cache::FileLocationCache; +use network::{ + rpc::StatusMessage, + types::{AnnounceFile, FindFile, SignedAnnounceFile}, + Keypair, MessageAcceptance, MessageId, NetworkGlobals, NetworkMessage, PeerId, PeerRequestId, + PublicKey, PubsubMessage, Request, RequestId, Response, +}; +use shared_types::{timestamp_now, TxID}; +use storage_async::Store; +use sync::{SyncMessage, SyncSender}; +use tokio::sync::{mpsc, RwLock}; + +use crate::peer_manager::PeerManager; + +lazy_static::lazy_static! { + pub static ref FIND_FILE_TIMEOUT: chrono::Duration = chrono::Duration::minutes(2); + pub static ref ANNOUNCE_FILE_TIMEOUT: chrono::Duration = chrono::Duration::minutes(2); + pub static ref TOLERABLE_DRIFT: chrono::Duration = chrono::Duration::seconds(5); +} + +fn duration_since(timestamp: u32) -> chrono::Duration { + let timestamp = i64::try_from(timestamp).expect("Should fit"); + let timestamp = chrono::NaiveDateTime::from_timestamp_opt(timestamp, 0).expect("should fit"); + let now = chrono::Utc::now().naive_utc(); + now.signed_duration_since(timestamp) +} + +fn peer_id_to_public_key(peer_id: &PeerId) -> Result { + // A libp2p peer id byte representation should be 2 length bytes + 4 protobuf bytes + compressed pk bytes + // if generated from a PublicKey with Identity multihash. + let pk_bytes = &peer_id.to_bytes()[2..]; + + PublicKey::from_protobuf_encoding(pk_bytes).map_err(|e| { + format!( + " Cannot parse libp2p public key public key from peer id: {}", + e + ) + }) +} + +pub struct Libp2pEventHandler { + /// A collection of global variables, accessible outside of the network service. + network_globals: Arc, + /// A channel to the router service. + network_send: mpsc::UnboundedSender, + /// A channel to the syncing service. + sync_send: SyncSender, + /// Node keypair for signing messages. + local_keypair: Keypair, + /// Log and transaction storage. + store: Store, + /// Cache for storing and serving gossip messages. + file_location_cache: Arc, + /// All connected peers. + peers: Arc>, +} + +impl Libp2pEventHandler { + pub fn new( + network_globals: Arc, + network_send: mpsc::UnboundedSender, + sync_send: SyncSender, + local_keypair: Keypair, + store: Store, + file_location_cache: Arc, + peers: Arc>, + ) -> Self { + Self { + network_globals, + network_send, + sync_send, + local_keypair, + store, + file_location_cache, + peers, + } + } + + fn send_to_network(&self, message: NetworkMessage) { + self.network_send.send(message).unwrap_or_else(|err| { + warn!(%err, "Could not send message to the network service"); + }); + } + + pub fn send_to_sync(&self, message: SyncMessage) { + self.sync_send.notify(message).unwrap_or_else(|err| { + warn!(%err, "Could not send message to the sync service"); + }); + } + + pub fn publish(&self, msg: PubsubMessage) { + self.send_to_network(NetworkMessage::Publish { + messages: vec![msg], + }); + } + + pub fn send_status(&self, peer_id: PeerId) { + let status_message = StatusMessage { data: 123 }; // dummy status message + debug!(%peer_id, ?status_message, "Sending Status request"); + + self.send_to_network(NetworkMessage::SendRequest { + peer_id, + request_id: RequestId::Router, + request: Request::Status(status_message), + }); + } + + pub async fn on_peer_connected(&self, peer_id: PeerId, outgoing: bool) { + self.peers.write().await.add(peer_id, outgoing); + + if outgoing { + self.send_status(peer_id); + self.send_to_sync(SyncMessage::PeerConnected { peer_id }); + } + } + + pub async fn on_peer_disconnected(&self, peer_id: PeerId) { + self.peers.write().await.remove(&peer_id); + self.send_to_sync(SyncMessage::PeerDisconnected { peer_id }); + } + + pub async fn on_rpc_request( + &self, + peer_id: PeerId, + request_id: PeerRequestId, + request: Request, + ) { + self.peers.write().await.update(&peer_id); + + match request { + Request::Status(status) => { + self.on_status_request(peer_id, request_id, status); + } + Request::GetChunks(request) => { + self.send_to_sync(SyncMessage::RequestChunks { + peer_id, + request_id, + request, + }); + } + Request::DataByHash(_) => { + // ignore + } + } + } + + fn on_status_request(&self, peer_id: PeerId, request_id: PeerRequestId, status: StatusMessage) { + debug!(%peer_id, ?status, "Received Status request"); + + let status_message = StatusMessage { data: 456 }; // dummy status message + debug!(%peer_id, ?status_message, "Sending Status response"); + + self.send_to_network(NetworkMessage::SendResponse { + peer_id, + id: request_id, + response: Response::Status(status_message), + }); + } + + pub async fn on_rpc_response( + &self, + peer_id: PeerId, + request_id: RequestId, + response: Response, + ) { + self.peers.write().await.update(&peer_id); + + match response { + Response::Status(status_message) => { + debug!(%peer_id, ?status_message, "Received Status response"); + } + Response::Chunks(response) => { + let request_id = match request_id { + RequestId::Sync(sync_id) => sync_id, + _ => unreachable!("All Chunks responses belong to sync"), + }; + + self.send_to_sync(SyncMessage::ChunksResponse { + peer_id, + request_id, + response, + }); + } + Response::DataByHash(_) => { + // ignore + } + } + } + + pub async fn on_rpc_error(&self, peer_id: PeerId, request_id: RequestId) { + self.peers.write().await.update(&peer_id); + + // Check if the failed RPC belongs to sync + if let RequestId::Sync(request_id) = request_id { + self.send_to_sync(SyncMessage::RpcError { + peer_id, + request_id, + }); + } + } + + pub async fn on_pubsub_message( + &self, + propagation_source: PeerId, + source: PeerId, + id: &MessageId, + message: PubsubMessage, + ) -> MessageAcceptance { + info!(?message, %propagation_source, %source, %id, "Received pubsub message"); + + match message { + PubsubMessage::ExampleMessage(_) => MessageAcceptance::Ignore, + PubsubMessage::FindFile(msg) => self.on_find_file(msg).await, + PubsubMessage::AnnounceFile(msg) => self.on_announce_file(propagation_source, msg), + } + } + + pub fn construct_announce_file_message(&self, tx_id: TxID) -> Option { + let peer_id = *self.network_globals.peer_id.read(); + + let addr = match self.network_globals.listen_multiaddrs.read().first() { + Some(addr) => addr.clone(), + None => { + error!("No listen address available"); + return None; + } + }; + + let timestamp = timestamp_now(); + + let msg = AnnounceFile { + tx_id, + peer_id: peer_id.into(), + at: addr.into(), + timestamp, + }; + + let mut signed = match msg.into_signed(&self.local_keypair) { + Ok(signed) => signed, + Err(e) => { + error!(%tx_id.seq, %e, "Failed to sign AnnounceFile message"); + return None; + } + }; + + signed.resend_timestamp = timestamp; + + Some(PubsubMessage::AnnounceFile(signed)) + } + + async fn on_find_file(&self, msg: FindFile) -> MessageAcceptance { + let FindFile { tx_id, timestamp } = msg; + + // verify timestamp + let d = duration_since(timestamp); + if d < TOLERABLE_DRIFT.neg() || d > *FIND_FILE_TIMEOUT { + debug!(%timestamp, "Invalid timestamp, ignoring FindFile message"); + return MessageAcceptance::Ignore; + } + + // check if we have it + if matches!(self.store.check_tx_completed(tx_id.seq).await, Ok(true)) { + if let Ok(Some(tx)) = self.store.get_tx_by_seq_number(tx_id.seq).await { + if tx.id() == tx_id { + debug!(?tx_id, "Found file locally, responding to FindFile query"); + + return match self.construct_announce_file_message(tx_id) { + Some(msg) => { + self.publish(msg); + MessageAcceptance::Ignore + } + // propagate FindFile query to other nodes + None => MessageAcceptance::Accept, + }; + } + } + } + + // try from cache + if let Some(mut msg) = self.file_location_cache.get_one(tx_id) { + debug!(?tx_id, "Found file in cache, responding to FindFile query"); + + msg.resend_timestamp = timestamp_now(); + self.publish(PubsubMessage::AnnounceFile(msg)); + + return MessageAcceptance::Ignore; + } + + // propagate FindFile query to other nodes + MessageAcceptance::Accept + } + + fn on_announce_file( + &self, + propagation_source: PeerId, + msg: SignedAnnounceFile, + ) -> MessageAcceptance { + // verify message signature + let pk = match peer_id_to_public_key(&msg.peer_id) { + Ok(pk) => pk, + Err(e) => { + error!( + "Failed to convert peer id {:?} to public key: {:?}", + msg.peer_id, e + ); + return MessageAcceptance::Reject; + } + }; + + if !msg.verify_signature(&pk) { + warn!( + "Received message with invalid signature from peer {:?}", + propagation_source + ); + return MessageAcceptance::Reject; + } + + // propagate gossip to peers + let d = duration_since(msg.resend_timestamp); + if d < TOLERABLE_DRIFT.neg() || d > *ANNOUNCE_FILE_TIMEOUT { + debug!(%msg.resend_timestamp, "Invalid resend timestamp, ignoring AnnounceFile message"); + return MessageAcceptance::Ignore; + } + + // notify sync layer + self.send_to_sync(SyncMessage::AnnounceFileGossip { + tx_id: msg.tx_id, + peer_id: msg.peer_id.clone().into(), + addr: msg.at.clone().into(), + }); + + // insert message to cache + self.file_location_cache.insert(msg); + + MessageAcceptance::Accept + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use channel::Message::*; + use file_location_cache::{test_util::AnnounceFileBuilder, FileLocationCache}; + use network::{ + discovery::{CombinedKey, ConnectionId}, + discv5::enr::EnrBuilder, + rpc::{GetChunksRequest, StatusMessage, SubstreamId}, + types::FindFile, + CombinedKeyExt, Keypair, MessageAcceptance, MessageId, Multiaddr, NetworkGlobals, + NetworkMessage, PeerId, PubsubMessage, Request, RequestId, Response, SyncId, + }; + use shared_types::{timestamp_now, ChunkArray, ChunkArrayWithProof, FlowRangeProof, TxID}; + use storage::{ + log_store::{log_manager::LogConfig, Store}, + LogManager, + }; + use sync::{test_util::create_2_store, SyncMessage, SyncReceiver, SyncSender}; + use task_executor::test_utils::TestRuntime; + use tokio::sync::{ + mpsc::{self, error::TryRecvError}, + RwLock, + }; + + use crate::{peer_manager::PeerManager, Config}; + + use super::*; + + struct Context { + runtime: TestRuntime, + network_globals: Arc, + keypair: Keypair, + network_send: mpsc::UnboundedSender, + network_recv: mpsc::UnboundedReceiver, + sync_send: SyncSender, + sync_recv: SyncReceiver, + store: Arc>, + file_location_cache: Arc, + peers: Arc>, + } + + impl Default for Context { + fn default() -> Self { + let runtime = TestRuntime::default(); + let (network_globals, keypair) = Context::new_network_globals(); + let (network_send, network_recv) = mpsc::unbounded_channel(); + let (sync_send, sync_recv) = channel::Channel::unbounded(); + let store = LogManager::memorydb(LogConfig::default()).unwrap(); + Self { + runtime, + network_globals: Arc::new(network_globals), + keypair, + network_send, + network_recv, + sync_send, + sync_recv, + store: Arc::new(RwLock::new(store)), + file_location_cache: Arc::new(FileLocationCache::default()), + peers: Arc::new(RwLock::new(PeerManager::new(Config::default()))), + } + } + } + + impl Context { + fn new_handler(&self) -> Libp2pEventHandler { + Libp2pEventHandler::new( + self.network_globals.clone(), + self.network_send.clone(), + self.sync_send.clone(), + self.keypair.clone(), + storage_async::Store::new(self.store.clone(), self.runtime.task_executor.clone()), + self.file_location_cache.clone(), + self.peers.clone(), + ) + } + + fn new_network_globals() -> (NetworkGlobals, Keypair) { + let keypair = Keypair::generate_secp256k1(); + let enr_key = CombinedKey::from_libp2p(&keypair).unwrap(); + let enr = EnrBuilder::new("v4").build(&enr_key).unwrap(); + let network_globals = NetworkGlobals::new(enr, 30000, 30000, vec![]); + + let listen_addr: Multiaddr = "/ip4/127.0.0.1/tcp/30000".parse().unwrap(); + network_globals.listen_multiaddrs.write().push(listen_addr); + + (network_globals, keypair) + } + + fn assert_status_request(&mut self, expected_peer_id: PeerId) { + match self.network_recv.try_recv() { + Ok(NetworkMessage::SendRequest { + peer_id, + request, + request_id, + }) => { + assert_eq!(peer_id, expected_peer_id); + assert!(matches!(request, Request::Status(..))); + assert!(matches!(request_id, RequestId::Router)) + } + Ok(_) => panic!("Unexpected network message type received"), + Err(e) => panic!("No network message received: {:?}", e), + } + } + + fn assert_file_announcement_published(&mut self, expected_tx_id: TxID) { + match self.network_recv.try_recv() { + Ok(NetworkMessage::Publish { messages }) => { + assert_eq!(messages.len(), 1); + assert!( + matches!(&messages[0], PubsubMessage::AnnounceFile(file) if file.tx_id == expected_tx_id) + ); + } + Ok(_) => panic!("Unexpected network message type received"), + Err(e) => panic!("No network message received: {:?}", e), + } + } + } + + #[test] + fn test_send_status() { + let mut ctx = Context::default(); + let handler = ctx.new_handler(); + + assert!(matches!( + ctx.network_recv.try_recv(), + Err(TryRecvError::Empty) + )); + + let alice = PeerId::random(); + handler.send_status(alice); + + ctx.assert_status_request(alice); + } + + #[tokio::test] + async fn test_on_peer_connected_incoming() { + let mut ctx = Context::default(); + let handler = ctx.new_handler(); + + assert_eq!(handler.peers.read().await.size(), 0); + + let alice = PeerId::random(); + handler.on_peer_connected(alice, false).await; + + assert_eq!(handler.peers.read().await.size(), 1); + assert!(matches!( + ctx.network_recv.try_recv(), + Err(TryRecvError::Empty) + )); + assert!(matches!(ctx.sync_recv.try_recv(), Err(TryRecvError::Empty))); + } + + #[tokio::test] + async fn test_on_peer_connected_outgoing() { + let mut ctx = Context::default(); + let handler = ctx.new_handler(); + + assert_eq!(handler.peers.read().await.size(), 0); + + let alice = PeerId::random(); + handler.on_peer_connected(alice, true).await; + + assert_eq!(handler.peers.read().await.size(), 1); + ctx.assert_status_request(alice); + assert!(matches!( + ctx.sync_recv.try_recv(), + Ok(Notification(SyncMessage::PeerConnected {peer_id})) if peer_id == alice + )); + } + + #[tokio::test] + async fn test_on_peer_disconnected() { + let mut ctx = Context::default(); + let handler = ctx.new_handler(); + + let alice = PeerId::random(); + handler.on_peer_connected(alice, false).await; + assert_eq!(handler.peers.read().await.size(), 1); + + handler.on_peer_disconnected(alice).await; + assert_eq!(handler.peers.read().await.size(), 0); + assert!(matches!( + ctx.sync_recv.try_recv(), + Ok(Notification(SyncMessage::PeerDisconnected {peer_id})) if peer_id == alice + )); + } + + #[tokio::test] + async fn test_on_rpc_request_status() { + let mut ctx = Context::default(); + let handler = ctx.new_handler(); + + let alice = PeerId::random(); + let req_id = (ConnectionId::new(4), SubstreamId(12)); + let request = Request::Status(StatusMessage { data: 412 }); + handler.on_rpc_request(alice, req_id, request).await; + + match ctx.network_recv.try_recv() { + Ok(NetworkMessage::SendResponse { + peer_id, + response, + id, + }) => { + assert_eq!(peer_id, alice); + assert!(matches!(response, Response::Status(..))); + assert_eq!(id, req_id); + } + Ok(_) => panic!("Unexpected network message type received"), + Err(e) => panic!("No network message received: {:?}", e), + } + } + + #[tokio::test] + async fn test_on_rpc_request_get_chunks() { + let mut ctx = Context::default(); + let handler = ctx.new_handler(); + + let alice = PeerId::random(); + let id = (ConnectionId::new(4), SubstreamId(12)); + let raw_request = GetChunksRequest { + tx_id: TxID::random_hash(7), + index_start: 66, + index_end: 99, + }; + handler + .on_rpc_request(alice, id, Request::GetChunks(raw_request.clone())) + .await; + + match ctx.sync_recv.try_recv() { + Ok(Notification(SyncMessage::RequestChunks { + peer_id, + request_id, + request, + })) => { + assert_eq!(peer_id, alice); + assert_eq!(request_id, id); + assert_eq!(request, raw_request); + } + Ok(_) => panic!("Unexpected sync message type received"), + Err(e) => panic!("No sync message received: {:?}", e), + } + } + + #[tokio::test] + async fn test_on_rpc_response() { + let mut ctx = Context::default(); + let handler = ctx.new_handler(); + + let alice = PeerId::random(); + let id = TxID::random_hash(555); + let data = ChunkArrayWithProof { + chunks: ChunkArray { + data: vec![1, 2, 3, 4], + start_index: 16, + }, + proof: FlowRangeProof::new_empty(), + }; + handler + .on_rpc_response( + alice, + RequestId::Sync(SyncId::SerialSync { tx_id: id }), + Response::Chunks(data.clone()), + ) + .await; + + match ctx.sync_recv.try_recv() { + Ok(Notification(SyncMessage::ChunksResponse { + peer_id, + request_id, + response, + })) => { + assert_eq!(peer_id, alice); + assert!(matches!(request_id, SyncId::SerialSync { tx_id } if tx_id == id )); + assert_eq!(response, data); + } + Ok(_) => panic!("Unexpected sync message type received"), + Err(e) => panic!("No sync message received: {:?}", e), + } + } + + #[tokio::test] + async fn test_on_rpc_error() { + let mut ctx = Context::default(); + let handler = ctx.new_handler(); + + let alice = PeerId::random(); + let id = TxID::random_hash(555); + handler + .on_rpc_error(alice, RequestId::Sync(SyncId::SerialSync { tx_id: id })) + .await; + + match ctx.sync_recv.try_recv() { + Ok(Notification(SyncMessage::RpcError { + peer_id, + request_id, + })) => { + assert_eq!(peer_id, alice); + assert!(matches!(request_id, SyncId::SerialSync { tx_id } if tx_id == id )); + } + Ok(_) => panic!("Unexpected sync message type received"), + Err(e) => panic!("No sync message received: {:?}", e), + } + } + + async fn handle_find_file_msg( + handler: &Libp2pEventHandler, + tx_id: TxID, + timestamp: u32, + ) -> MessageAcceptance { + let (alice, bob) = (PeerId::random(), PeerId::random()); + let id = MessageId::new(b"dummy message"); + let message = PubsubMessage::FindFile(FindFile { tx_id, timestamp }); + handler.on_pubsub_message(alice, bob, &id, message).await + } + + #[tokio::test] + async fn test_on_pubsub_find_file_invalid_timestamp() { + let ctx = Context::default(); + let handler = ctx.new_handler(); + + // message too future + let result = handle_find_file_msg( + &handler, + TxID::random_hash(412), + timestamp_now() + 10 + TOLERABLE_DRIFT.num_seconds() as u32, + ) + .await; + assert!(matches!(result, MessageAcceptance::Ignore)); + + // message too old + let result = handle_find_file_msg( + &handler, + TxID::random_hash(412), + timestamp_now() - 10 - FIND_FILE_TIMEOUT.num_seconds() as u32, + ) + .await; + assert!(matches!(result, MessageAcceptance::Ignore)); + } + + #[tokio::test] + async fn test_on_pubsub_find_file_not_found() { + let ctx = Context::default(); + let handler = ctx.new_handler(); + + let result = handle_find_file_msg(&handler, TxID::random_hash(412), timestamp_now()).await; + assert!(matches!(result, MessageAcceptance::Accept)); + } + + #[tokio::test] + async fn test_on_pubsub_find_file_in_store() { + let mut ctx = Context::default(); + + // prepare store with txs + let (_, store, txs, _) = create_2_store(vec![1314]); + ctx.store = store; + + let handler = ctx.new_handler(); + + // receive find file request + let result = handle_find_file_msg(&handler, txs[0].id(), timestamp_now()).await; + assert!(matches!(result, MessageAcceptance::Ignore)); + ctx.assert_file_announcement_published(txs[0].id()); + } + + #[tokio::test] + async fn test_on_pubsub_find_file_in_cache() { + let mut ctx = Context::default(); + let handler = ctx.new_handler(); + + // prepare tx in cache + let tx_id = TxID::random_hash(412); + let signed = AnnounceFileBuilder::default() + .with_tx_id(tx_id) + .with_timestamp(timestamp_now() - 5) + .build(); + ctx.file_location_cache.insert(signed); + + // receive find file request + let result = handle_find_file_msg(&handler, tx_id, timestamp_now()).await; + assert!(matches!(result, MessageAcceptance::Ignore)); + ctx.assert_file_announcement_published(tx_id); + } + + #[tokio::test] + async fn test_on_pubsub_announce_file_invalid_sig() { + let ctx = Context::default(); + let handler = ctx.new_handler(); + + let (alice, bob) = (PeerId::random(), PeerId::random()); + let id = MessageId::new(b"dummy message"); + let tx_id = TxID::random_hash(412); + + // change signed message + let message = match handler.construct_announce_file_message(tx_id).unwrap() { + PubsubMessage::AnnounceFile(mut file) => { + let malicious_addr: Multiaddr = "/ip4/127.0.0.38/tcp/30000".parse().unwrap(); + file.inner.at = malicious_addr.into(); + PubsubMessage::AnnounceFile(file) + } + _ => panic!("Unexpected pubsub message type"), + }; + + // failed to verify signature + let result = handler.on_pubsub_message(alice, bob, &id, message).await; + assert!(matches!(result, MessageAcceptance::Reject)); + } + + #[tokio::test] + async fn test_on_pubsub_announce_file() { + let mut ctx = Context::default(); + let handler = ctx.new_handler(); + + // prepare message + let (alice, bob) = (PeerId::random(), PeerId::random()); + let id = MessageId::new(b"dummy message"); + let tx = TxID::random_hash(412); + let message = handler.construct_announce_file_message(tx).unwrap(); + + // succeeded to handle + let result = handler.on_pubsub_message(alice, bob, &id, message).await; + assert!(matches!(result, MessageAcceptance::Accept)); + + // ensure notify to sync layer + match ctx.sync_recv.try_recv() { + Ok(Notification(SyncMessage::AnnounceFileGossip { + tx_id, + peer_id, + addr, + })) => { + assert_eq!(tx_id, tx); + assert_eq!(peer_id, *ctx.network_globals.peer_id.read()); + assert_eq!( + addr, + *ctx.network_globals.listen_multiaddrs.read().get(0).unwrap() + ); + } + Ok(_) => panic!("Unexpected sync message type received"), + Err(e) => panic!("No sync message received: {:?}", e), + } + + // ensure cache updated + assert_eq!(ctx.file_location_cache.get_all(tx).len(), 1); + } +} diff --git a/node/router/src/peer_manager.rs b/node/router/src/peer_manager.rs new file mode 100644 index 0000000..243e3ac --- /dev/null +++ b/node/router/src/peer_manager.rs @@ -0,0 +1,221 @@ +use crate::Config; +use network::PeerId; +use rand::seq::IteratorRandom; +use std::collections::HashMap; +use std::time::Instant; + +/// Connected peer info. +struct PeerInfo { + /// Outgoing or incoming connection. + outgoing: bool, + /// Last update time. + since: Instant, +} + +impl PeerInfo { + fn new(outgoing: bool) -> Self { + Self { + outgoing, + since: Instant::now(), + } + } + + fn elapsed_secs(&self) -> u64 { + self.since.elapsed().as_secs() + } +} + +/// Manages connected outgoing and incoming peers. +/// +/// Basically, records the last update time for RPC requests/responses, +/// and disconnect some peers periodically if too many idle ones. So that, +/// there are enough incoming connections available for other peers to +/// sync file from this peer. +/// +/// On the other hand, pub-sub message propagation rely on peer connections, +/// so a peer should have enough peers connected to broadcast pub-sub messages. +#[derive(Default)] +pub struct PeerManager { + peers: HashMap, + config: Config, +} + +impl PeerManager { + pub fn new(config: Config) -> Self { + Self { + peers: Default::default(), + config, + } + } + + pub fn add(&mut self, peer_id: PeerId, outgoing: bool) -> bool { + let old = self.peers.insert(peer_id, PeerInfo::new(outgoing)); + if old.is_none() { + debug!(%peer_id, %outgoing, "New peer added"); + true + } else { + // peer should not be connected multiple times + error!(%peer_id, %outgoing, "Peer already exists"); + false + } + } + + pub fn remove(&mut self, peer_id: &PeerId) -> bool { + if self.peers.remove(peer_id).is_some() { + debug!(%peer_id, "Peer removed"); + true + } else { + error!(%peer_id, "Peer not found to remove"); + false + } + } + + /// Updates the timestamp of specified peer if any. + pub fn update(&mut self, peer_id: &PeerId) -> bool { + match self.peers.get_mut(peer_id) { + Some(peer) => { + peer.since = Instant::now(); + trace!(%peer_id, "Peer updated"); + true + } + None => { + error!(%peer_id, "Peer not found to update"); + false + } + } + } + + /// Finds idle peers for garbage collection in advance. + pub fn expired_peers(&self) -> Vec { + let mut expired_outgoing = self.expired(true, self.config.max_idle_outgoing_peers); + let mut expired_incoming = self.expired(false, self.config.max_idle_incoming_peers); + expired_outgoing.append(&mut expired_incoming); + expired_outgoing + } + + fn expired(&self, outgoing: bool, max_idle: usize) -> Vec { + let expired: Vec = self + .peers + .iter() + .filter(|(_, peer)| { + peer.outgoing == outgoing && peer.elapsed_secs() >= self.config.idle_time_secs + }) + .map(|(peer_id, _)| *peer_id) + .collect(); + + if expired.len() <= max_idle { + return vec![]; + } + + let num_expired = expired.len() - max_idle; + expired + .into_iter() + .choose_multiple(&mut rand::thread_rng(), num_expired) + } +} + +#[cfg(test)] +mod tests { + use std::{ + ops::Sub, + time::{Duration, Instant}, + }; + + use network::PeerId; + + use crate::Config; + + use super::PeerManager; + + impl PeerManager { + pub fn size(&self) -> usize { + self.peers.len() + } + } + + #[test] + fn test_add() { + let mut manager = PeerManager::new(Config::default()); + + let peer1 = PeerId::random(); + assert!(manager.add(peer1, false)); + assert!(!manager.add(peer1, false)); + assert!(!manager.add(peer1, true)); + assert_eq!(manager.size(), 1); + + let peer2 = PeerId::random(); + assert!(manager.add(peer2, false)); + assert_eq!(manager.size(), 2); + } + + #[test] + fn test_remove() { + let mut manager = PeerManager::new(Config::default()); + + let peer1 = PeerId::random(); + assert!(manager.add(peer1, false)); + let peer2 = PeerId::random(); + assert!(manager.add(peer2, true)); + + let peer3 = PeerId::random(); + assert!(!manager.remove(&peer3)); + assert!(manager.remove(&peer1)); + assert!(manager.remove(&peer2)); + assert_eq!(manager.size(), 0); + } + + #[test] + fn test_update() { + let mut manager = PeerManager::new(Config::default()); + + let peer1 = PeerId::random(); + assert!(manager.add(peer1, false)); + let ts1 = manager.peers.get(&peer1).unwrap().since; + + let peer2 = PeerId::random(); + assert!(!manager.update(&peer2)); + assert!(manager.update(&peer1)); + + let ts2 = manager.peers.get(&peer1).unwrap().since; + assert!(ts2 > ts1); + } + + #[test] + fn test_expired() { + let config = Config::default(); + let mut manager = PeerManager::new(config.clone()); + + let mut peers = vec![]; + + // setup incoming peers: max + 3 + for _ in 0..(config.max_idle_incoming_peers + 3) { + let peer_id = PeerId::random(); + peers.push(peer_id); + assert!(manager.add(peer_id, false)); + } + + // setup outgoing peers: max + 2 + for _ in 0..(config.max_idle_outgoing_peers + 2) { + let peer_id = PeerId::random(); + peers.push(peer_id); + assert!(manager.add(peer_id, true)); + } + + // change timestamp for all peers + let idle_timeout = Duration::from_secs(config.idle_time_secs); + for peer_id in peers.iter() { + let peer = manager.peers.get_mut(peer_id).unwrap(); + peer.since = Instant::now().sub(idle_timeout); + } + + assert_eq!( + manager.expired(false, config.max_idle_incoming_peers).len(), + 3 + ); + assert_eq!( + manager.expired(true, config.max_idle_outgoing_peers).len(), + 2 + ); + assert_eq!(manager.expired_peers().len(), 5); + } +} diff --git a/node/router/src/service.rs b/node/router/src/service.rs new file mode 100644 index 0000000..2621f15 --- /dev/null +++ b/node/router/src/service.rs @@ -0,0 +1,347 @@ +use crate::Config; +use crate::{libp2p_event_handler::Libp2pEventHandler, peer_manager::PeerManager}; +use file_location_cache::FileLocationCache; +use futures::{channel::mpsc::Sender, prelude::*}; +use miner::MinerMessage; +use network::{ + BehaviourEvent, Keypair, Libp2pEvent, NetworkGlobals, NetworkMessage, RequestId, + Service as LibP2PService, Swarm, +}; +use std::sync::Arc; +use std::time::Duration; +use storage::log_store::Store as LogStore; +use storage_async::Store; +use sync::{SyncMessage, SyncSender}; +use task_executor::ShutdownReason; +use tokio::sync::{broadcast, mpsc, RwLock}; +use tokio::time::interval; + +/// Service that handles communication between internal services and the libp2p service. +pub struct RouterService { + config: Config, + + /// The underlying libp2p service that drives all the network interactions. + libp2p: LibP2PService, + + /// A collection of global variables, accessible outside of the network service. + network_globals: Arc, + + /// The receiver channel for Zgs to communicate with the network service. + network_recv: mpsc::UnboundedReceiver, + + /// All connected peers. + peers: Arc>, + + /// Handler for libp2p events. + libp2p_event_handler: Libp2pEventHandler, + + /// Stores potentially created UPnP mappings to be removed on shutdown. (TCP port and UDP + /// port). + upnp_mappings: (Option, Option), +} + +impl RouterService { + #[allow(clippy::too_many_arguments)] + pub fn spawn( + executor: task_executor::TaskExecutor, + libp2p: LibP2PService, + network_globals: Arc, + network_recv: mpsc::UnboundedReceiver, + network_send: mpsc::UnboundedSender, + sync_send: SyncSender, + _miner_send: Option>, + store: Arc>, + file_location_cache: Arc, + local_keypair: Keypair, + config: Config, + ) { + let store = Store::new(store, executor.clone()); + let peers = Arc::new(RwLock::new(PeerManager::new(config.clone()))); + + // create the network service and spawn the task + let router = RouterService { + config, + libp2p, + network_globals: network_globals.clone(), + network_recv, + peers: peers.clone(), + libp2p_event_handler: Libp2pEventHandler::new( + network_globals, + network_send, + sync_send, + local_keypair, + store, + file_location_cache, + peers, + ), + upnp_mappings: (None, None), + }; + + // spawn service + let shutdown_sender = executor.shutdown_sender(); + + executor.spawn(router.main(shutdown_sender), "router"); + } + + async fn main(mut self, mut shutdown_sender: Sender) { + let mut heartbeat = interval(Duration::from_secs(self.config.heartbeat_interval_secs)); + + loop { + tokio::select! { + // handle a message sent to the network + Some(msg) = self.network_recv.recv() => self.on_network_msg(msg, &mut shutdown_sender).await, + + // handle event coming from the network + event = self.libp2p.next_event() => self.on_libp2p_event(event, &mut shutdown_sender).await, + + // heartbeat + _ = heartbeat.tick() => self.on_heartbeat().await, + } + } + } + + /// Handle an event received from the network. + async fn on_libp2p_event( + &mut self, + ev: Libp2pEvent, + shutdown_sender: &mut Sender, + ) { + debug!(?ev, "Received new event from libp2p"); + + match ev { + Libp2pEvent::Behaviour(event) => match event { + BehaviourEvent::PeerConnectedOutgoing(peer_id) => { + self.libp2p_event_handler + .on_peer_connected(peer_id, true) + .await; + } + BehaviourEvent::PeerConnectedIncoming(peer_id) => { + self.libp2p_event_handler + .on_peer_connected(peer_id, false) + .await; + } + BehaviourEvent::PeerBanned(_) | BehaviourEvent::PeerUnbanned(_) => { + // No action required for these events. + } + BehaviourEvent::PeerDisconnected(peer_id) => { + self.libp2p_event_handler + .on_peer_disconnected(peer_id) + .await; + } + BehaviourEvent::RequestReceived { + peer_id, + id, + request, + } => { + if self.network_globals.peers.read().is_connected(&peer_id) { + self.libp2p_event_handler + .on_rpc_request(peer_id, id, request) + .await; + } else { + debug!(%peer_id, ?request, "Dropping request of disconnected peer"); + } + } + BehaviourEvent::ResponseReceived { + peer_id, + id, + response, + } => { + self.libp2p_event_handler + .on_rpc_response(peer_id, id, response) + .await; + } + BehaviourEvent::RPCFailed { id, peer_id } => { + self.libp2p_event_handler.on_rpc_error(peer_id, id).await; + } + BehaviourEvent::StatusPeer(peer_id) => { + self.libp2p_event_handler.send_status(peer_id); + } + BehaviourEvent::PubsubMessage { + id, + propagation_source, + source, + message, + .. + } => { + let result = self + .libp2p_event_handler + .on_pubsub_message(propagation_source, source, &id, message) + .await; + + self.libp2p + .swarm + .behaviour_mut() + .report_message_validation_result(&propagation_source, id, result); + } + }, + Libp2pEvent::NewListenAddr(multiaddr) => { + self.network_globals + .listen_multiaddrs + .write() + .push(multiaddr); + } + Libp2pEvent::ZeroListeners => { + let _ = shutdown_sender + .send(ShutdownReason::Failure( + "All listeners are closed. Unable to listen", + )) + .await + .map_err(|e| { + warn!( + error = %e, + "failed to send a shutdown signal", + ) + }); + } + } + } + + /// Handle a message sent to the network service. + async fn on_network_msg( + &mut self, + msg: NetworkMessage, + _shutdown_sender: &mut Sender, + ) { + debug!(?msg, "Received new message"); + + match msg { + NetworkMessage::SendRequest { + peer_id, + request, + request_id, + } => { + self.libp2p.send_request(peer_id, request_id, request); + } + NetworkMessage::SendResponse { + peer_id, + response, + id, + } => { + self.libp2p.send_response(peer_id, id, response); + } + NetworkMessage::SendErrorResponse { + peer_id, + error, + id, + reason, + } => { + self.libp2p.respond_with_error(peer_id, id, error, reason); + } + NetworkMessage::Publish { messages } => { + if self.libp2p.swarm.connected_peers().next().is_none() { + // this is a boardcast message, when current node doesn't have any peers connected, try to connect any peer in config + for multiaddr in &self.config.libp2p_nodes { + match Swarm::dial(&mut self.libp2p.swarm, multiaddr.clone()) { + Ok(()) => { + debug!(address = %multiaddr, "Dialing libp2p peer"); + break; + } + Err(err) => { + debug!(address = %multiaddr, error = ?err, "Could not connect to peer") + } + }; + } + } + + let mut topic_kinds = Vec::new(); + for message in &messages { + if !topic_kinds.contains(&message.kind()) { + topic_kinds.push(message.kind()); + } + } + debug!( + count = messages.len(), + topics = ?topic_kinds, + "Sending pubsub messages", + ); + self.libp2p.swarm.behaviour_mut().publish(messages); + } + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => self.libp2p.report_peer(&peer_id, action, source, msg), + NetworkMessage::GoodbyePeer { + peer_id, + reason, + source, + } => self.libp2p.goodbye_peer(&peer_id, reason, source), + NetworkMessage::DialPeer { address, peer_id } => { + if self.libp2p.swarm.is_connected(&peer_id) { + self.libp2p_event_handler + .send_to_sync(SyncMessage::PeerConnected { peer_id }); + } else { + match Swarm::dial(&mut self.libp2p.swarm, address.clone()) { + Ok(()) => debug!(%address, "Dialing libp2p peer"), + Err(err) => { + info!(%address, error = ?err, "Failed to dial peer"); + self.libp2p_event_handler + .send_to_sync(SyncMessage::DailFailed { peer_id, err }); + } + }; + } + } + NetworkMessage::AnnounceLocalFile { tx_id } => { + if let Some(msg) = self + .libp2p_event_handler + .construct_announce_file_message(tx_id) + { + self.libp2p_event_handler.publish(msg); + } + } + NetworkMessage::UPnPMappingEstablished { + tcp_socket, + udp_socket, + } => { + self.upnp_mappings = (tcp_socket.map(|s| s.port()), udp_socket.map(|s| s.port())); + // If there is an external TCP port update, modify our local ENR. + if let Some(tcp_socket) = tcp_socket { + if let Err(e) = self + .libp2p + .swarm + .behaviour_mut() + .discovery_mut() + .update_enr_tcp_port(tcp_socket.port()) + { + warn!(error = %e, "Failed to update ENR"); + } + } + if let Some(udp_socket) = udp_socket { + if let Err(e) = self + .libp2p + .swarm + .behaviour_mut() + .discovery_mut() + .update_enr_udp_socket(udp_socket) + { + warn!(error = %e, "Failed to update ENR"); + } + } + } + } + } + + async fn on_heartbeat(&mut self) { + let expired_peers = self.peers.write().await.expired_peers(); + + trace!("heartbeat, expired peers = {:?}", expired_peers.len()); + + for peer_id in expired_peers { + // async operation, once peer disconnected, swarm event `PeerDisconnected` + // will be polled to handle in advance. + match self.libp2p.swarm.disconnect_peer_id(peer_id) { + Ok(_) => debug!(%peer_id, "Peer expired and disconnect it"), + Err(_) => error!(%peer_id, "Peer expired but failed to disconnect"), + } + } + } +} + +impl Drop for RouterService { + fn drop(&mut self) { + info!("Router service shutdown"); + // attempt to remove port mappings + network::nat::remove_mappings(self.upnp_mappings.0, self.upnp_mappings.1); + } +} diff --git a/node/rpc/Cargo.toml b/node/rpc/Cargo.toml new file mode 100644 index 0000000..0a26652 --- /dev/null +++ b/node/rpc/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "rpc" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +append_merkle = { path = "../../common/append_merkle" } +miner = {path = "../miner"} +futures = "0.3.21" +jsonrpsee = { version = "0.14.0", features = ["full"] } +network = { path = "../network" } +serde = { version = "1.0.137", features = ["derive"] } +base64 = "0.13.0" +shared_types = { path = "../shared_types" } +sync = { path = "../sync" } +task_executor = { path = "../../common/task_executor" } +tokio = { version = "1.19.2", features = ["macros", "sync"] } +tracing = "0.1.35" +chunk_pool = { path = "../chunk_pool" } +storage = { path = "../storage" } +storage-async = { path = "../storage-async" } +merkle_light = { path = "../../common/merkle_light" } +merkle_tree = { path = "../../common/merkle_tree"} +futures-channel = "^0.3" + +[dev-dependencies] +serde_json = "1.0.82" diff --git a/node/rpc/src/admin/api.rs b/node/rpc/src/admin/api.rs new file mode 100644 index 0000000..d4bc63d --- /dev/null +++ b/node/rpc/src/admin/api.rs @@ -0,0 +1,23 @@ +use crate::types::NetworkInfo; +use jsonrpsee::core::RpcResult; +use jsonrpsee::proc_macros::rpc; +use std::collections::HashMap; +use sync::FileSyncInfo; + +#[rpc(server, client, namespace = "admin")] +pub trait Rpc { + #[method(name = "shutdown")] + async fn shutdown(&self) -> RpcResult<()>; + + #[method(name = "startSyncFile")] + async fn start_sync_file(&self, tx_seq: u64) -> RpcResult<()>; + + #[method(name = "getSyncStatus")] + async fn get_sync_status(&self, tx_seq: u64) -> RpcResult; + + #[method(name = "getSyncInfo")] + async fn get_sync_info(&self, tx_seq: Option) -> RpcResult>; + + #[method(name = "getNetworkInfo")] + async fn get_network_info(&self) -> RpcResult; +} diff --git a/node/rpc/src/admin/impl.rs b/node/rpc/src/admin/impl.rs new file mode 100644 index 0000000..0406c62 --- /dev/null +++ b/node/rpc/src/admin/impl.rs @@ -0,0 +1,101 @@ +use super::api::RpcServer; +use crate::types::NetworkInfo; +use crate::{error, Context}; +use futures::prelude::*; +use jsonrpsee::core::async_trait; +use jsonrpsee::core::RpcResult; +use std::collections::HashMap; +use sync::{FileSyncInfo, SyncRequest, SyncResponse}; +use task_executor::ShutdownReason; + +pub struct RpcServerImpl { + pub ctx: Context, +} + +#[async_trait] +impl RpcServer for RpcServerImpl { + #[tracing::instrument(skip(self), err)] + async fn shutdown(&self) -> RpcResult<()> { + info!("admin_shutdown()"); + + self.ctx + .shutdown_sender + .clone() + .send(ShutdownReason::Success("Shutdown by admin")) + .await + .map_err(|e| error::internal_error(format!("Failed to send shutdown command: {:?}", e))) + } + + #[tracing::instrument(skip(self), err)] + async fn start_sync_file(&self, tx_seq: u64) -> RpcResult<()> { + info!("admin_startSyncFile({tx_seq})"); + + let response = self + .ctx + .request_sync(SyncRequest::SyncFile { tx_seq }) + .await?; + + match response { + SyncResponse::SyncFile { err } => { + if err.is_empty() { + Ok(()) + } else { + Err(error::internal_error(err)) + } + } + _ => Err(error::internal_error("unexpected response type")), + } + } + + #[tracing::instrument(skip(self), err)] + async fn get_sync_status(&self, tx_seq: u64) -> RpcResult { + info!("admin_getSyncStatus({tx_seq})"); + + let response = self + .ctx + .request_sync(SyncRequest::SyncStatus { tx_seq }) + .await?; + + match response { + SyncResponse::SyncStatus { status } => Ok(status + .map(|x| format!("{:?}", x)) + .unwrap_or_else(|| "unknown".into())), + _ => Err(error::internal_error("unexpected response type")), + } + } + + #[tracing::instrument(skip(self), err)] + async fn get_sync_info(&self, tx_seq: Option) -> RpcResult> { + info!(?tx_seq, "admin_getSyncInfo()"); + + let response = self + .ctx + .request_sync(SyncRequest::FileSyncInfo { tx_seq }) + .await?; + + match response { + SyncResponse::FileSyncInfo { result } => Ok(result), + _ => Err(error::internal_error("unexpected response type")), + } + } + + #[tracing::instrument(skip(self), err)] + async fn get_network_info(&self) -> RpcResult { + info!("admin_getNetworkInfo()"); + + let db = self.ctx.network_globals.peers.read(); + + let connected_peers = db.connected_peers().count(); + let connected_outgoing_peers = db.connected_outbound_only_peers().count(); + + Ok(NetworkInfo { + peer_id: self.ctx.network_globals.local_peer_id().to_base58(), + total_peers: db.peers().count(), + banned_peers: db.banned_peers().count(), + disconnected_peers: db.disconnected_peers().count(), + connected_peers, + connected_outgoing_peers, + connected_incoming_peers: connected_peers - connected_outgoing_peers, + }) + } +} diff --git a/node/rpc/src/admin/mod.rs b/node/rpc/src/admin/mod.rs new file mode 100644 index 0000000..2882f83 --- /dev/null +++ b/node/rpc/src/admin/mod.rs @@ -0,0 +1,5 @@ +mod api; +mod r#impl; + +pub use api::RpcServer; +pub use r#impl::RpcServerImpl; diff --git a/node/rpc/src/config.rs b/node/rpc/src/config.rs new file mode 100644 index 0000000..c21a369 --- /dev/null +++ b/node/rpc/src/config.rs @@ -0,0 +1,9 @@ +use std::net::SocketAddr; + +#[derive(Clone)] +pub struct Config { + pub enabled: bool, + pub listen_address: SocketAddr, + pub chunks_per_segment: usize, + pub max_cache_file_size: usize, +} diff --git a/node/rpc/src/error.rs b/node/rpc/src/error.rs new file mode 100644 index 0000000..9369a4a --- /dev/null +++ b/node/rpc/src/error.rs @@ -0,0 +1,30 @@ +#![allow(dead_code)] + +use jsonrpsee::core::Error; +use jsonrpsee::types::error::{CallError, ErrorCode, ErrorObject}; + +pub fn not_supported() -> Error { + Error::Call(CallError::Custom(ErrorObject::borrowed( + ErrorCode::MethodNotFound.code(), + &"Not supported", + None, + ))) +} + +pub fn internal_error(msg: impl std::convert::AsRef) -> Error { + Error::Call(CallError::Custom(ErrorObject::owned( + ErrorCode::InternalError.code(), + "Internal error", + Some(msg.as_ref()), + ))) +} + +pub fn invalid_params(param: &str, msg: impl std::convert::AsRef) -> Error { + let error = &format!("Invalid params: {:}", param); + + Error::Call(CallError::Custom(ErrorObject::owned( + ErrorCode::InvalidParams.code(), + error, + Some(msg.as_ref()), + ))) +} diff --git a/node/rpc/src/lib.rs b/node/rpc/src/lib.rs new file mode 100644 index 0000000..b7ec0a0 --- /dev/null +++ b/node/rpc/src/lib.rs @@ -0,0 +1,82 @@ +#[macro_use] +extern crate tracing; + +extern crate miner as zgs_miner; + +mod admin; +mod config; +mod error; +mod miner; +mod zgs; +mod types; + +use crate::miner::RpcServer as MinerRpcServer; +use admin::RpcServer as AdminRpcServer; +use chunk_pool::MemoryChunkPool; +use futures::channel::mpsc::Sender; +use jsonrpsee::core::RpcResult; +use jsonrpsee::http_server::{HttpServerBuilder, HttpServerHandle}; +use network::NetworkGlobals; +use network::NetworkMessage; +use zgs::RpcServer as ZgsRpcServer; +use zgs_miner::MinerMessage; +use std::error::Error; +use std::sync::Arc; +use storage_async::Store; +use sync::{SyncRequest, SyncResponse, SyncSender}; +use task_executor::ShutdownReason; +use tokio::sync::broadcast; +use tokio::sync::mpsc::UnboundedSender; + +pub use config::Config as RPCConfig; + +/// A wrapper around all the items required to spawn the HTTP server. +/// +/// The server will gracefully handle the case where any fields are `None`. +#[derive(Clone)] +pub struct Context { + pub config: RPCConfig, + pub network_globals: Arc, + pub network_send: UnboundedSender, + pub sync_send: SyncSender, + pub chunk_pool: Arc, + pub log_store: Store, + pub shutdown_sender: Sender, + pub mine_service_sender: Option>, +} + +impl Context { + pub fn send_network(&self, msg: NetworkMessage) -> RpcResult<()> { + self.network_send + .send(msg) + .map_err(|e| error::internal_error(format!("Failed to send network message: {:?}", e))) + } + + pub async fn request_sync(&self, request: SyncRequest) -> RpcResult { + self.sync_send + .request(request) + .await + .map_err(|e| error::internal_error(format!("Failed to send sync request: {:?}", e))) + } +} + +pub async fn run_server(ctx: Context) -> Result> { + let server = HttpServerBuilder::default() + .build(ctx.config.listen_address) + .await?; + + let mut zgs = (zgs::RpcServerImpl { ctx: ctx.clone() }).into_rpc(); + let admin = (admin::RpcServerImpl { ctx: ctx.clone() }).into_rpc(); + zgs.merge(admin)?; + + if ctx.mine_service_sender.is_some() { + let mine = (miner::RpcServerImpl { ctx }).into_rpc(); + zgs.merge(mine)?; + } + + let addr = server.local_addr()?; + let handle = server.start(zgs)?; + info!("Server started http://{}", addr); + + Ok(handle) +} diff --git a/node/rpc/src/miner/api.rs b/node/rpc/src/miner/api.rs new file mode 100644 index 0000000..8d4417f --- /dev/null +++ b/node/rpc/src/miner/api.rs @@ -0,0 +1,14 @@ +use jsonrpsee::core::RpcResult; +use jsonrpsee::proc_macros::rpc; + +#[rpc(server, client, namespace = "miner")] +pub trait Rpc { + #[method(name = "start")] + async fn start(&self) -> RpcResult; + + #[method(name = "stop")] + async fn stop(&self) -> RpcResult; + + #[method(name = "setStartPosition")] + async fn set_start_position(&self, index: u64) -> RpcResult; +} diff --git a/node/rpc/src/miner/impl.rs b/node/rpc/src/miner/impl.rs new file mode 100644 index 0000000..5eed003 --- /dev/null +++ b/node/rpc/src/miner/impl.rs @@ -0,0 +1,49 @@ +use super::api::RpcServer; +use crate::Context; +use futures::prelude::*; +use jsonrpsee::core::async_trait; +use jsonrpsee::core::{Error, RpcResult}; +use miner::MinerMessage; +use tokio::sync::broadcast; + +pub struct RpcServerImpl { + pub ctx: Context, +} + +impl RpcServerImpl { + fn mine_service_sender(&self) -> &broadcast::Sender { + self.ctx.mine_service_sender.as_ref().unwrap() + } +} + +#[async_trait] +impl RpcServer for RpcServerImpl { + async fn start(&self) -> RpcResult { + info!("mine_start()"); + let success = self + .mine_service_sender() + .send(MinerMessage::ToggleMining(true)) + .is_ok(); + Ok(success) + } + + async fn stop(&self) -> RpcResult { + info!("mine_stop()"); + + let success = self + .mine_service_sender() + .send(MinerMessage::ToggleMining(false)) + .is_ok(); + Ok(success) + } + + async fn set_start_position(&self, index: u64) -> RpcResult { + info!("mine_setStartPosition({})", index); + + let success = self + .mine_service_sender() + .send(MinerMessage::SetStartPosition(Some(index))) + .is_ok(); + Ok(success) + } +} diff --git a/node/rpc/src/miner/mod.rs b/node/rpc/src/miner/mod.rs new file mode 100644 index 0000000..2640fc7 --- /dev/null +++ b/node/rpc/src/miner/mod.rs @@ -0,0 +1,6 @@ +#![allow(unused)] +mod api; +mod r#impl; + +pub use api::RpcServer; +pub use r#impl::RpcServerImpl; diff --git a/node/rpc/src/types.rs b/node/rpc/src/types.rs new file mode 100644 index 0000000..91ca92d --- /dev/null +++ b/node/rpc/src/types.rs @@ -0,0 +1,226 @@ +use crate::error; +use jsonrpsee::core::RpcResult; +use merkle_light::hash::Algorithm; +use merkle_light::merkle::MerkleTree; +use merkle_tree::RawLeafSha3Algorithm; +use serde::{Deserialize, Serialize}; +use shared_types::{ + compute_padded_chunk_size, compute_segment_size, DataRoot, FileProof, Transaction, CHUNK_SIZE, +}; +use std::hash::Hasher; +use storage::H256; + +const ZERO_HASH: [u8; 32] = [ + 0xd3, 0x97, 0xb3, 0xb0, 0x43, 0xd8, 0x7f, 0xcd, 0x6f, 0xad, 0x12, 0x91, 0xff, 0xb, 0xfd, 0x16, + 0x40, 0x1c, 0x27, 0x48, 0x96, 0xd8, 0xc6, 0x3a, 0x92, 0x37, 0x27, 0xf0, 0x77, 0xb8, 0xe0, 0xb5, +]; + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Status { + pub connected_peers: usize, + pub log_sync_height: u64, + pub log_sync_block: H256, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NetworkInfo { + pub peer_id: String, + pub total_peers: usize, + pub banned_peers: usize, + pub disconnected_peers: usize, + pub connected_peers: usize, + pub connected_outgoing_peers: usize, + pub connected_incoming_peers: usize, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FileInfo { + pub tx: Transaction, + pub finalized: bool, + pub is_cached: bool, + pub uploaded_seg_num: usize, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Segment(#[serde(with = "base64")] pub Vec); + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SegmentWithProof { + /// File merkle root. + pub root: DataRoot, + #[serde(with = "base64")] + /// With fixed data size except the last segment. + pub data: Vec, + /// Segment index. + pub index: usize, + /// File merkle proof whose leaf node is segment root. + pub proof: FileProof, + /// File size + pub file_size: usize, +} + +impl SegmentWithProof { + /// Splits file into segments and returns the total number of segments and the last segment size. + pub fn split_file_into_segments( + file_size: usize, + chunks_per_segment: usize, + ) -> RpcResult<(usize, usize)> { + if file_size == 0 { + return Err(error::invalid_params("file_size", "file is empty")); + } + + let segment_size = chunks_per_segment * CHUNK_SIZE; + let remaining_size = file_size % segment_size; + let mut num_segments = file_size / segment_size; + + if remaining_size == 0 { + return Ok((num_segments, segment_size)); + } + + // Otherwise, the last segment is not full. + num_segments += 1; + + let last_chunk_size = remaining_size % CHUNK_SIZE; + if last_chunk_size == 0 { + Ok((num_segments, remaining_size)) + } else { + // Padding last chunk with zeros. + let last_segment_size = remaining_size - last_chunk_size + CHUNK_SIZE; + Ok((num_segments, last_segment_size)) + } + } + + fn validate_data_size_and_index( + &self, + file_size: usize, + chunks_per_segment: usize, + ) -> RpcResult { + let (num_segments, last_segment_size) = + SegmentWithProof::split_file_into_segments(file_size, chunks_per_segment)?; + + if self.index >= num_segments { + return Err(error::invalid_params("index", "index out of bound")); + } + + let data_size = if self.index == num_segments - 1 { + last_segment_size + } else { + chunks_per_segment * CHUNK_SIZE + }; + + if self.data.len() != data_size { + return Err(error::invalid_params("data", "invalid data length")); + } + + Ok(num_segments) + } + + fn calculate_segment_merkle_root(&self, extend_chunk_length: usize) -> [u8; 32] { + let mut a = RawLeafSha3Algorithm::default(); + let hashes = self.data.chunks_exact(CHUNK_SIZE).map(|x| { + a.reset(); + a.write(x); + a.hash() + }); + let mut hash_data = hashes.collect::>(); + hash_data.append(&mut vec![ZERO_HASH; extend_chunk_length]); + + MerkleTree::<_, RawLeafSha3Algorithm>::new(hash_data).root() + } + + fn validate_proof(&self, num_segments: usize, expected_data_length: usize) -> RpcResult<()> { + // Validate proof data format at first. + if self.proof.path.is_empty() { + if self.proof.lemma.len() != 1 { + return Err(error::invalid_params("proof", "invalid proof")); + } + } else if self.proof.lemma.len() != self.proof.path.len() + 2 { + return Err(error::invalid_params("proof", "invalid proof")); + } + + // Calculate segment merkle root to verify proof. + let extend_chunk_length = if expected_data_length > self.data.len() { + let extend_data_length = expected_data_length - self.data.len(); + if extend_data_length % CHUNK_SIZE != 0 { + return Err(error::invalid_params("proof", "invalid data len")); + } + + extend_data_length / CHUNK_SIZE + } else { + 0 + }; + + let segment_root = self.calculate_segment_merkle_root(extend_chunk_length); + if !self + .proof + .validate(&segment_root, &self.root, self.index, num_segments)? + { + return Err(error::invalid_params("proof", "validation failed")); + } + + Ok(()) + } + + /// Validates the segment data size and proof. + pub fn validate(&self, chunks_per_segment: usize) -> RpcResult<()> { + self.validate_data_size_and_index(self.file_size, chunks_per_segment)?; + + let (chunks, _) = compute_padded_chunk_size(self.file_size); + let (segments_for_proof, last_segment_size) = + compute_segment_size(chunks, chunks_per_segment); + + let expected_data_length = if self.index == segments_for_proof - 1 { + last_segment_size * CHUNK_SIZE + } else { + chunks_per_segment * CHUNK_SIZE + }; + + debug!( + "data len: {}, expected len: {}", + self.data.len(), + expected_data_length + ); + + self.validate_proof(segments_for_proof, expected_data_length)?; + Ok(()) + } + + /// Returns the index of first chunk in the segment. + #[allow(dead_code)] + pub fn chunk_index(&self, chunks_per_segment: usize) -> usize { + self.index * chunks_per_segment + } +} + +mod base64 { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn serialize(v: &Vec, s: S) -> Result { + let base64 = base64::encode(v); + String::serialize(&base64, s) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { + let base64 = String::deserialize(d)?; + base64::decode(base64.as_bytes()).map_err(serde::de::Error::custom) + } +} + +#[cfg(test)] +mod tests { + use super::Segment; + + #[test] + fn test_segment_serde() { + let seg = Segment("hello, world".as_bytes().to_vec()); + let result = serde_json::to_string(&seg).unwrap(); + assert_eq!(result.as_str(), "\"aGVsbG8sIHdvcmxk\""); + + let seg2: Segment = serde_json::from_str("\"aGVsbG8sIHdvcmxk\"").unwrap(); + assert_eq!(String::from_utf8(seg2.0).unwrap().as_str(), "hello, world"); + } +} diff --git a/node/rpc/src/zgs/api.rs b/node/rpc/src/zgs/api.rs new file mode 100644 index 0000000..f1f6c75 --- /dev/null +++ b/node/rpc/src/zgs/api.rs @@ -0,0 +1,34 @@ +use crate::types::{FileInfo, Segment, SegmentWithProof, Status}; +use jsonrpsee::core::RpcResult; +use jsonrpsee::proc_macros::rpc; +use shared_types::DataRoot; + +#[rpc(server, client, namespace = "zgs")] +pub trait Rpc { + #[method(name = "getStatus")] + async fn get_status(&self) -> RpcResult; + + #[method(name = "uploadSegment")] + async fn upload_segment(&self, segment: SegmentWithProof) -> RpcResult<()>; + + #[method(name = "downloadSegment")] + async fn download_segment( + &self, + data_root: DataRoot, + start_index: usize, + end_index: usize, + ) -> RpcResult>; + + #[method(name = "downloadSegmentWithProof")] + async fn download_segment_with_proof( + &self, + data_root: DataRoot, + index: usize, + ) -> RpcResult>; + + #[method(name = "getFileInfo")] + async fn get_file_info(&self, data_root: DataRoot) -> RpcResult>; + + #[method(name = "getFileInfoByTxSeq")] + async fn get_file_info_by_tx_seq(&self, tx_seq: u64) -> RpcResult>; +} diff --git a/node/rpc/src/zgs/impl.rs b/node/rpc/src/zgs/impl.rs new file mode 100644 index 0000000..1408855 --- /dev/null +++ b/node/rpc/src/zgs/impl.rs @@ -0,0 +1,238 @@ +use super::api::RpcServer; +use crate::error; +use crate::types::{FileInfo, Segment, SegmentWithProof, Status}; +use crate::Context; +use chunk_pool::{FileID, SegmentInfo}; +use jsonrpsee::core::async_trait; +use jsonrpsee::core::RpcResult; +use shared_types::{DataRoot, Transaction, CHUNK_SIZE}; +use storage::try_option; + +pub struct RpcServerImpl { + pub ctx: Context, +} + +#[async_trait] +impl RpcServer for RpcServerImpl { + #[tracing::instrument(skip(self), err)] + async fn get_status(&self) -> RpcResult { + info!("zgs_getStatus()"); + let sync_progress = self + .ctx + .log_store + .get_store() + .read() + .await + .get_sync_progress()? + .unwrap_or_default(); + + Ok(Status { + connected_peers: self.ctx.network_globals.connected_peers(), + log_sync_height: sync_progress.0, + log_sync_block: sync_progress.1, + }) + } + + async fn upload_segment(&self, segment: SegmentWithProof) -> RpcResult<()> { + debug!(root = %segment.root, index = %segment.index, "zgs_uploadSegment"); + + let _ = self.ctx.chunk_pool.validate_segment_size(&segment.data)?; + + let maybe_tx = self + .ctx + .log_store + .get_tx_by_data_root(&segment.root) + .await?; + let mut need_cache = false; + + if self + .ctx + .chunk_pool + .check_already_has_cache(&segment.root) + .await + { + need_cache = true; + } + + if !need_cache { + need_cache = self.check_need_cache(&maybe_tx, segment.file_size).await?; + } + + segment.validate(self.ctx.config.chunks_per_segment)?; + + let seg_info = SegmentInfo { + root: segment.root, + seg_data: segment.data, + seg_index: segment.index, + chunks_per_segment: self.ctx.config.chunks_per_segment, + }; + + if need_cache { + self.ctx.chunk_pool.cache_chunks(seg_info).await?; + } else { + let file_id = FileID { + root: seg_info.root, + tx_id: maybe_tx.unwrap().id(), + }; + self.ctx + .chunk_pool + .write_chunks(seg_info, file_id, segment.file_size) + .await?; + } + + Ok(()) + } + + async fn download_segment( + &self, + data_root: DataRoot, + start_index: usize, + end_index: usize, + ) -> RpcResult> { + debug!(%data_root, %start_index, %end_index, "zgs_downloadSegment"); + + if start_index >= end_index { + return Err(error::invalid_params("end_index", "invalid chunk index")); + } + + if end_index - start_index > self.ctx.config.chunks_per_segment { + return Err(error::invalid_params( + "end_index", + format!( + "exceeds maximum chunks {}", + self.ctx.config.chunks_per_segment + ), + )); + } + + let tx_seq = try_option!( + self.ctx + .log_store + .get_tx_seq_by_data_root(&data_root) + .await? + ); + let segment = try_option!( + self.ctx + .log_store + .get_chunks_by_tx_and_index_range(tx_seq, start_index, end_index) + .await? + ); + + Ok(Some(Segment(segment.data))) + } + + async fn download_segment_with_proof( + &self, + data_root: DataRoot, + index: usize, + ) -> RpcResult> { + debug!(%data_root, %index, "zgs_downloadSegmentWithProof"); + + let tx = try_option!(self.ctx.log_store.get_tx_by_data_root(&data_root).await?); + + // validate index + let chunks_per_segment = self.ctx.config.chunks_per_segment; + let (num_segments, last_segment_size) = + SegmentWithProof::split_file_into_segments(tx.size as usize, chunks_per_segment)?; + + if index >= num_segments { + return Err(error::invalid_params("index", "index out of bound")); + } + + // calculate chunk start and end index + let start_index = index * chunks_per_segment; + let end_index = if index == num_segments - 1 { + // last segment without padding chunks by flow + start_index + last_segment_size / CHUNK_SIZE + } else { + start_index + chunks_per_segment + }; + + let segment = try_option!( + self.ctx + .log_store + .get_chunks_with_proof_by_tx_and_index_range(tx.seq, start_index, end_index) + .await? + ); + + let proof = tx.compute_segment_proof(&segment, chunks_per_segment)?; + + Ok(Some(SegmentWithProof { + root: data_root, + data: segment.chunks.data, + index, + proof, + file_size: tx.size as usize, + })) + } + + async fn get_file_info(&self, data_root: DataRoot) -> RpcResult> { + debug!(%data_root, "zgs_getFileInfo"); + + let tx = try_option!(self.ctx.log_store.get_tx_by_data_root(&data_root).await?); + + Ok(Some(self.get_file_info_by_tx(tx).await?)) + } + + async fn get_file_info_by_tx_seq(&self, tx_seq: u64) -> RpcResult> { + debug!(%tx_seq, "zgs_getFileInfoByTxSeq"); + + let tx = try_option!(self.ctx.log_store.get_tx_by_seq_number(tx_seq).await?); + + Ok(Some(self.get_file_info_by_tx(tx).await?)) + } +} + +impl RpcServerImpl { + async fn check_need_cache( + &self, + maybe_tx: &Option, + file_size: usize, + ) -> RpcResult { + if let Some(tx) = maybe_tx { + if tx.size != file_size as u64 { + return Err(error::invalid_params( + "file_size", + "segment file size not matched with tx file size", + )); + } + + // Transaction already finalized for the specified file data root. + if self.ctx.log_store.check_tx_completed(tx.seq).await? { + return Err(error::invalid_params( + "root", + "already uploaded and finalized", + )); + } + + Ok(false) + } else { + //Check whether file is small enough to cache in the system + if file_size > self.ctx.config.max_cache_file_size { + return Err(error::invalid_params( + "file_size", + "caching of large file when tx is unavailable is not supported", + )); + } + + Ok(true) + } + } + + async fn get_file_info_by_tx(&self, tx: Transaction) -> RpcResult { + let (uploaded_seg_num, is_cached) = self + .ctx + .chunk_pool + .get_uploaded_seg_num(&tx.data_merkle_root) + .await; + + let finalized = self.ctx.log_store.check_tx_completed(tx.seq).await?; + + Ok(FileInfo { + tx, + finalized, + is_cached, + uploaded_seg_num, + }) + } +} diff --git a/node/rpc/src/zgs/mod.rs b/node/rpc/src/zgs/mod.rs new file mode 100644 index 0000000..2882f83 --- /dev/null +++ b/node/rpc/src/zgs/mod.rs @@ -0,0 +1,5 @@ +mod api; +mod r#impl; + +pub use api::RpcServer; +pub use r#impl::RpcServerImpl; diff --git a/node/shared_types/Cargo.toml b/node/shared_types/Cargo.toml new file mode 100644 index 0000000..bc30a7c --- /dev/null +++ b/node/shared_types/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "shared_types" +version = "0.1.0" +edition = "2021" + +[dependencies] +append_merkle = { path = "../../common/append_merkle"} +merkle_tree = { path = "../../common/merkle_tree"} +zgs_spec = { path = "../../common/spec"} +anyhow = { version = "=1.0.58", features = ["backtrace"] } +ethereum-types = "0.14" +eth2_ssz = "0.4.0" +eth2_ssz_derive = "0.3.0" +eth2_ssz_types = "0.2.1" +merkle_light = { path = "../../common/merkle_light" } +tiny-keccak = "2.0.2" +tracing = "0.1.35" +typenum = "1.15.0" +serde = { version = "1.0.137", features = ["derive"] } +chrono = "0.4.19" diff --git a/node/shared_types/src/lib.rs b/node/shared_types/src/lib.rs new file mode 100644 index 0000000..7a7b1ab --- /dev/null +++ b/node/shared_types/src/lib.rs @@ -0,0 +1,348 @@ +mod proof; + +use anyhow::bail; +use append_merkle::{ + AppendMerkleTree, Proof as RawProof, RangeProof as RawRangeProof, Sha3Algorithm, +}; +use ethereum_types::{H256, U256}; +use merkle_light::merkle::MerkleTree; +use merkle_light::proof::Proof as RawFileProof; +use merkle_light::{hash::Algorithm, merkle::next_pow2}; +use merkle_tree::RawLeafSha3Algorithm; +use serde::{Deserialize, Serialize}; +use ssz::Encode; +use ssz_derive::{Decode as DeriveDecode, Encode as DeriveEncode}; +use std::fmt; +use std::hash::Hasher; +use tiny_keccak::{Hasher as KeccakHasher, Keccak}; +use tracing::debug; + +const ZERO_HASH: [u8; 32] = [ + 0xd3, 0x97, 0xb3, 0xb0, 0x43, 0xd8, 0x7f, 0xcd, 0x6f, 0xad, 0x12, 0x91, 0xff, 0xb, 0xfd, 0x16, + 0x40, 0x1c, 0x27, 0x48, 0x96, 0xd8, 0xc6, 0x3a, 0x92, 0x37, 0x27, 0xf0, 0x77, 0xb8, 0xe0, 0xb5, +]; + +/// Application level requests sent to the network. +#[derive(Debug, Clone, Copy)] +pub enum RequestId { + Router, +} + +pub type DataRoot = H256; + +pub type FlowProof = RawProof; +pub type FlowRangeProof = RawRangeProof; +pub type Merkle = AppendMerkleTree; + +// Each chunk is 32 bytes. +pub const CHUNK_SIZE: usize = 256; + +pub fn bytes_to_chunks(size_bytes: usize) -> usize { + if size_bytes % CHUNK_SIZE == 0 { + size_bytes / CHUNK_SIZE + } else { + size_bytes / CHUNK_SIZE + 1 + } +} + +pub fn compute_padded_chunk_size(size_bytes: usize) -> (usize, usize) { + let chunk_len = bytes_to_chunks(size_bytes); + let chunks_next_pow2 = next_pow2(chunk_len); + + if chunks_next_pow2 == chunk_len { + return (chunks_next_pow2, chunks_next_pow2); + } + + let min_chunk = if chunks_next_pow2 < 16 { + 1 + } else { + chunks_next_pow2 >> 4 + }; + + // chunk_len will be always greater than 0, size_byte comes from tx.size which is file size, the flow contract doesn't allowy upload 0-size file + let padded_chunks = ((chunk_len - 1) / min_chunk + 1) * min_chunk; + + (padded_chunks, chunks_next_pow2) +} + +pub fn compute_segment_size(chunks: usize, chunks_per_segment: usize) -> (usize, usize) { + if chunks % chunks_per_segment == 0 { + (chunks / chunks_per_segment, chunks_per_segment) + } else { + (chunks / chunks_per_segment + 1, chunks % chunks_per_segment) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Chunk(pub [u8; CHUNK_SIZE]); + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, DeriveDecode, DeriveEncode)] +pub struct TxID { + pub seq: u64, + pub hash: H256, +} + +impl TxID { + pub fn random_hash(seq: u64) -> Self { + Self { + seq, + hash: H256::random(), + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq, DeriveDecode, DeriveEncode, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Transaction { + pub stream_ids: Vec, + /// In-place data. + pub data: Vec, + pub data_merkle_root: DataRoot, + /// `(subtree_depth, subtree_root)` + pub merkle_nodes: Vec<(usize, DataRoot)>, + + pub start_entry_index: u64, + pub size: u64, + pub seq: u64, +} + +impl Transaction { + pub fn num_entries_of_node(depth: usize) -> usize { + 1 << (depth - 1) + } + + pub fn num_entries(&self) -> usize { + self.merkle_nodes.iter().fold(0, |size, &(depth, _)| { + size + Transaction::num_entries_of_node(depth) + }) + } + + pub fn hash(&self) -> H256 { + let bytes = self.as_ssz_bytes(); + let mut h = Keccak::v256(); + let mut e = H256::zero(); + h.update(&bytes); + h.finalize(e.as_mut()); + e + } + + pub fn id(&self) -> TxID { + TxID { + seq: self.seq, + hash: self.hash(), + } + } +} + +pub struct ChunkWithProof { + pub chunk: Chunk, + pub proof: FlowProof, +} + +#[derive(Debug, Clone, PartialEq, Eq, DeriveEncode, DeriveDecode)] +pub struct ChunkArrayWithProof { + pub chunks: ChunkArray, + // TODO: The top levels of the two proofs can be merged. + pub proof: FlowRangeProof, +} + +#[derive(Clone, Eq, PartialEq, DeriveEncode, DeriveDecode)] +pub struct ChunkArray { + // The length is exactly a multiple of `CHUNK_SIZE` + pub data: Vec, + pub start_index: u64, +} + +impl fmt::Debug for ChunkArray { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "ChunkArray: start_index={} data_len={}", + self.start_index, + self.data.len() + ) + } +} + +impl ChunkArray { + pub fn first_chunk(&self) -> Option { + self.chunk_at(self.start_index as usize) + } + + pub fn last_chunk(&self) -> Option { + let last_index = + (self.start_index as usize + self.data.len() / CHUNK_SIZE).checked_sub(1)?; + self.chunk_at(last_index) + } + + pub fn chunk_at(&self, index: usize) -> Option { + if index >= self.data.len() / CHUNK_SIZE + self.start_index as usize + || index < self.start_index as usize + { + return None; + } + let offset = (index - self.start_index as usize) * CHUNK_SIZE; + Some(Chunk( + self.data[offset..offset + CHUNK_SIZE] + .try_into() + .expect("length match"), + )) + } + + pub fn sub_array(&self, start: u64, end: u64) -> Option { + if start >= (self.data.len() / CHUNK_SIZE) as u64 + self.start_index + || start < self.start_index + || end > (self.data.len() / CHUNK_SIZE) as u64 + self.start_index + || end <= self.start_index + || end <= start + { + return None; + } + let start_offset = (start - self.start_index) as usize * CHUNK_SIZE; + let end_offset = (end - self.start_index) as usize * CHUNK_SIZE; + Some(ChunkArray { + data: self.data[start_offset..end_offset].to_vec(), + start_index: start, + }) + } +} + +impl std::fmt::Display for ChunkArray { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "ChunkArray {{ chunks = {}, start_index = {} }}", + self.data.len() / CHUNK_SIZE, + self.start_index + ) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEncode, DeriveDecode, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FileProof { + pub lemma: Vec, + pub path: Vec, +} +impl FileProof { + pub fn new(mut lemma: Vec, path: Vec) -> Self { + if path.is_empty() { + lemma.truncate(1); + } + + FileProof { lemma, path } + } + + pub fn validate( + &self, + leaf_hash: &[u8; 32], + root: &DataRoot, + position: usize, + leaf_count: usize, + ) -> anyhow::Result { + let proof_position = self.position(leaf_count)?; + if proof_position != position { + bail!( + "wrong position: proof_pos={} provided={}", + proof_position, + position + ); + } + + let proof: RawFileProof<[u8; 32]> = self.try_into()?; + + if !proof.validate::() { + debug!("Proof validate fails"); + return Ok(false); + } + + if proof.root() != root.0 { + bail!( + "root mismatch, proof_root={:?} provided={:?}", + proof.root(), + root.0 + ); + } + + if proof.item() != *leaf_hash { + bail!( + "data hash mismatch: leaf_hash={:?} proof_item={:?}", + leaf_hash, + proof.item(), + ); + } + + Ok(true) + } + + fn position(&self, total_chunk_count: usize) -> anyhow::Result { + let mut left_chunk_count = total_chunk_count; + let mut proof_position = 0; + // TODO: After the first `is_left == true`, the tree depth is fixed. + for is_left in self.path.iter().rev() { + if left_chunk_count <= 1 { + bail!( + "Proof path too long for a tree size: path={:?}, size={}", + self.path, + total_chunk_count + ); + } + let subtree_size = next_pow2(left_chunk_count) >> 1; + if !is_left { + proof_position += subtree_size; + left_chunk_count -= subtree_size; + } else { + left_chunk_count = subtree_size; + } + } + if left_chunk_count != 1 { + bail!( + "Proof path too short for a tree size: path={:?}, size={}", + self.path, + total_chunk_count + ); + } + Ok(proof_position) + } +} + +impl TryFrom<&FileProof> for RawFileProof<[u8; 32]> { + type Error = anyhow::Error; + + fn try_from(value: &FileProof) -> std::result::Result { + if (value.lemma.len() == 1 && value.path.is_empty()) + || (value.lemma.len() > 2 && value.lemma.len() == value.path.len() + 2) + { + Ok(RawFileProof::<[u8; 32]>::new( + value.lemma.iter().map(|e| e.0).collect(), + value.path.clone(), + )) + } else { + bail!("Invalid proof: proof={:?}", value) + } + } +} + +pub fn timestamp_now() -> u32 { + let timestamp = chrono::Utc::now().timestamp(); + u32::try_from(timestamp).expect("The year is between 1970 and 2106") +} + +pub fn compute_segment_merkle_root(data: &[u8], segment_chunks: usize) -> [u8; 32] { + let mut a = RawLeafSha3Algorithm::default(); + let mut hashes: Vec<[u8; 32]> = data + .chunks_exact(CHUNK_SIZE) + .map(|x| { + a.reset(); + a.write(x); + a.hash() + }) + .collect(); + + let num_chunks = data.len() / CHUNK_SIZE; + if num_chunks < segment_chunks { + hashes.append(&mut vec![ZERO_HASH; segment_chunks - num_chunks]); + } + + MerkleTree::<_, RawLeafSha3Algorithm>::new(hashes).root() +} diff --git a/node/shared_types/src/proof.rs b/node/shared_types/src/proof.rs new file mode 100644 index 0000000..e2c8ea3 --- /dev/null +++ b/node/shared_types/src/proof.rs @@ -0,0 +1,220 @@ +use crate::{compute_segment_merkle_root, ChunkArrayWithProof, FileProof, Transaction, CHUNK_SIZE}; +use anyhow::{bail, Result}; +use append_merkle::{Algorithm, Sha3Algorithm}; +use ethereum_types::H256; +use merkle_light::merkle::log2_pow2; + +enum NodeProofLeaf { + /// Segment in a single sub-tree. + Full { node_depth: usize }, + + /// Segment is not full and is made up of one or multiple sub-trees + /// that smaller than a full segment. + Partial, +} + +/// NodeProof represents a merkle proof from submission node to file root. +struct NodeProof { + proof: FileProof, + leaf: NodeProofLeaf, +} + +impl NodeProof { + fn compute_segment_proof( + self, + segment: &ChunkArrayWithProof, + chunks_per_segment: usize, + ) -> Result { + let mut node_depth = match self.leaf { + // In this case, some proof pathes missed between segment + // root and submission node, which could be retrieved from + // the flow proof. + NodeProofLeaf::Full { node_depth } => node_depth, + + // Client could compute the segment root via returned + // segment data, and use the NodeProof to validate. + NodeProofLeaf::Partial => return Ok(self.proof), + }; + + // node depth in Transaction is by number of nodes, + // here we use depth by number of edges. + node_depth -= 1; + + self.compute_segment_proof_full(node_depth, segment, chunks_per_segment) + } + + fn compute_segment_proof_full( + self, + node_depth: usize, // node depth by edge + segment: &ChunkArrayWithProof, + chunks_per_segment: usize, + ) -> Result { + // when segment equals to sub-tree, just return the proof directly + let segment_depth = log2_pow2(chunks_per_segment); + if node_depth == segment_depth { + return Ok(self.proof); + } + + // otherwise, segment should be smaller than sub-tree + assert!(node_depth > segment_depth); + + // flow tree should not be smaller than any sub-tree + let flow_proof_path_len = segment.proof.left_proof.path().len(); + if flow_proof_path_len < node_depth { + bail!( + "flow proof path too small, path_len = {}, node_depth = {}", + flow_proof_path_len, + node_depth + ); + } + + // segment root as proof leaf + let segment_root: H256 = + compute_segment_merkle_root(&segment.chunks.data, chunks_per_segment).into(); + let mut lemma = vec![segment_root]; + let mut path = vec![]; + + // copy from flow proof + for i in segment_depth..node_depth { + lemma.push(segment.proof.left_proof.lemma()[i + 1]); + path.push(segment.proof.left_proof.path()[i]); + } + + // combine with node proof + if self.proof.path.is_empty() { + // append file root only + lemma.push(self.proof.lemma[0]); + } else { + // append lemma/path and file root, and ignore the sub-tree root + // which could be constructed via proof. + lemma.extend_from_slice(&self.proof.lemma[1..]); + path.extend_from_slice(&self.proof.path); + } + + Ok(FileProof::new(lemma, path)) + } +} + +impl Transaction { + /// Computes file merkle proof for the specified segment. + /// + /// The leaf of proof is segment root, and root of proof is file merkle root. + pub fn compute_segment_proof( + &self, + segment: &ChunkArrayWithProof, + chunks_per_segment: usize, + ) -> Result { + // validate index + let chunk_start_index = segment.chunks.start_index as usize; + if chunk_start_index % chunks_per_segment != 0 { + bail!("start index not aligned"); + } + + let total_entries = self.num_entries(); + if chunk_start_index >= total_entries { + bail!("start index out of bound"); + } + + let data_len = segment.chunks.data.len(); + if chunk_start_index + data_len / CHUNK_SIZE > total_entries { + bail!("end index out of bound"); + } + + // compute merkle proof from node root to file root + let node_proof = self.compute_node_proof(chunk_start_index, chunks_per_segment); + + node_proof.compute_segment_proof(segment, chunks_per_segment) + } + + fn compute_node_proof(&self, chunk_start_index: usize, chunks_per_segment: usize) -> NodeProof { + // construct `lemma` in the order: root -> interier nodes -> leaf, + // and reverse the `lemma` and `path` to create proof. + let mut lemma = vec![self.data_merkle_root]; + let mut path = vec![]; + + // Basically, a segment should be in a sub-tree except the last segment. + // As for the last segment, it should be also in a sub-tree if the smallest + // sub-tree is bigger than a segment. Otherwise, segment is made up of + // all sub-trees that smaller than a segment. + + // try to find a single node for segment + let mut node = None; + let mut total_chunks = 0; + for (depth, root) in self.merkle_nodes.iter().cloned() { + let node_chunks = Transaction::num_entries_of_node(depth); + + // ignore nodes that smaller than a segment + if node_chunks < chunks_per_segment { + break; + } + + total_chunks += node_chunks; + + // segment in a single node + if chunk_start_index < total_chunks { + node = Some((depth, root)); + break; + } + + // otherwise, segment not in current node + lemma.push(root); + path.push(false); + } + + let leaf = match node { + // segment in a single node + Some((depth, root)) => { + // append lemma and path if right sibling exists + if let Some(right_root) = self.compute_merkle_root(depth) { + lemma.push(right_root); + path.push(true); + } + + // append leaf + lemma.push(root); + + NodeProofLeaf::Full { node_depth: depth } + } + + // segment is made up of multiple nodes + None => { + let segment_depth = log2_pow2(chunks_per_segment) + 1; + let right_root = self + .compute_merkle_root(segment_depth) + .expect("merkle root should be exists"); + + // append leaf + lemma.push(right_root); + + NodeProofLeaf::Partial + } + }; + + // change to bottom-top order: leaf -> (interiers) -> root + lemma.reverse(); + path.reverse(); + + NodeProof { + proof: FileProof::new(lemma, path), + leaf, + } + } + + /// Computes the merkle root of nodes that of depth less than `max_depth_exclusive`. + fn compute_merkle_root(&self, max_depth_exclusive: usize) -> Option { + let (depth, mut root) = self.merkle_nodes.last().cloned()?; + if depth >= max_depth_exclusive { + return None; + } + + for (node_depth, node_root) in self.merkle_nodes.iter().rev().skip(1) { + if *node_depth >= max_depth_exclusive { + break; + } + + root = Sha3Algorithm::parent(node_root, &root); + } + + Some(root) + } +} diff --git a/node/src/cli/mod.rs b/node/src/cli/mod.rs new file mode 100644 index 0000000..804ec59 --- /dev/null +++ b/node/src/cli/mod.rs @@ -0,0 +1,7 @@ +use clap::{arg, command, Command}; + +pub fn cli_app<'a>() -> Command<'a> { + command!() + .arg(arg!(-c --config "Sets a custom config file")) + .allow_external_subcommands(true) +} diff --git a/node/src/client/builder.rs b/node/src/client/builder.rs new file mode 100644 index 0000000..15aea1f --- /dev/null +++ b/node/src/client/builder.rs @@ -0,0 +1,273 @@ +use super::{Client, RuntimeContext}; +use chunk_pool::{Config as ChunkPoolConfig, MemoryChunkPool}; +use file_location_cache::FileLocationCache; +use log_entry_sync::{LogSyncConfig, LogSyncEvent, LogSyncManager}; +use miner::{MineService, MinerConfig, MinerMessage}; +use network::{ + self, Keypair, NetworkConfig, NetworkGlobals, NetworkMessage, RequestId, + Service as LibP2PService, +}; +use router::RouterService; +use rpc::RPCConfig; +use std::sync::Arc; +use storage::log_store::log_manager::LogConfig; +use storage::log_store::Store; +use storage::{LogManager, StorageConfig}; +use sync::{SyncSender, SyncService}; +use tokio::sync::{broadcast, mpsc, RwLock}; + +macro_rules! require { + ($component:expr, $self:ident, $e:ident) => { + $self + .$e + .as_ref() + .ok_or(format!("{} requires {}", $component, std::stringify!($e)))? + }; +} + +struct NetworkComponents { + send: mpsc::UnboundedSender, + globals: Arc, + keypair: Keypair, + + // note: these will be owned by the router service + owned: Option<( + LibP2PService, + mpsc::UnboundedReceiver, + )>, +} + +struct SyncComponents { + send: SyncSender, +} + +struct MinerComponents { + send: broadcast::Sender, +} + +struct LogSyncComponents { + send: broadcast::Sender, +} + +/// Builds a `Client` instance. +/// +/// ## Notes +/// +/// The builder may start some services (e.g.., libp2p, http server) immediately after they are +/// initialized, _before_ the `self.build(..)` method has been called. +#[derive(Default)] +pub struct ClientBuilder { + runtime_context: Option, + store: Option>>, + async_store: Option, + file_location_cache: Option>, + network: Option, + sync: Option, + miner: Option, + log_sync: Option, +} + +impl ClientBuilder { + /// Specifies the runtime context (tokio executor, logger, etc) for client services. + pub fn with_runtime_context(mut self, context: RuntimeContext) -> Self { + self.runtime_context = Some(context); + self + } + + /// Initializes in-memory storage. + pub fn with_memory_store(mut self) -> Result { + // TODO(zz): Set config. + let store = Arc::new(RwLock::new( + LogManager::memorydb(LogConfig::default()) + .map_err(|e| format!("Unable to start in-memory store: {:?}", e))?, + )); + + self.store = Some(store.clone()); + + if let Some(ctx) = self.runtime_context.as_ref() { + self.async_store = Some(storage_async::Store::new(store, ctx.executor.clone())); + } + + Ok(self) + } + + /// Initializes RocksDB storage. + pub fn with_rocksdb_store(mut self, config: &StorageConfig) -> Result { + let store = Arc::new(RwLock::new( + LogManager::rocksdb(LogConfig::default(), &config.db_dir) + .map_err(|e| format!("Unable to start RocksDB store: {:?}", e))?, + )); + + self.store = Some(store.clone()); + + if let Some(ctx) = self.runtime_context.as_ref() { + self.async_store = Some(storage_async::Store::new(store, ctx.executor.clone())); + } + + Ok(self) + } + + pub fn with_file_location_cache(mut self) -> Self { + let file_location_cache = Default::default(); + self.file_location_cache = Some(Arc::new(file_location_cache)); + self + } + + /// Starts the networking stack. + pub async fn with_network(mut self, config: &NetworkConfig) -> Result { + let executor = require!("network", self, runtime_context).clone().executor; + + // construct the libp2p service context + let service_context = network::Context { config }; + + // construct communication channel + let (send, recv) = mpsc::unbounded_channel::(); + + // launch libp2p service + let (globals, keypair, libp2p) = + LibP2PService::new(executor, send.clone(), service_context) + .await + .map_err(|e| format!("Failed to start network service: {:?}", e))?; + + self.network = Some(NetworkComponents { + send, + globals, + keypair, + owned: Some((libp2p, recv)), + }); + + Ok(self) + } + + pub async fn with_sync(mut self, config: sync::Config) -> Result { + let executor = require!("sync", self, runtime_context).clone().executor; + let store = require!("sync", self, store).clone(); + let file_location_cache = require!("sync", self, file_location_cache).clone(); + let network_send = require!("sync", self, network).send.clone(); + let event_recv = require!("sync", self, log_sync).send.subscribe(); + + let send = SyncService::spawn_with_config( + config, + executor, + network_send, + store, + file_location_cache, + event_recv, + ) + .await + .map_err(|e| format!("Failed to start sync service: {:?}", e))?; + self.sync = Some(SyncComponents { send }); + + Ok(self) + } + + pub async fn with_miner(mut self, config: Option) -> Result { + if let Some(config) = config { + let executor = require!("miner", self, runtime_context).clone().executor; + let network_send = require!("miner", self, network).send.clone(); + let store = self.store.as_ref().unwrap().clone(); + + let send = MineService::spawn(executor, network_send, config, store).await?; + self.miner = Some(MinerComponents { send }); + } + + Ok(self) + } + + /// Starts the networking stack. + pub fn with_router(mut self, router_config: router::Config) -> Result { + let executor = require!("router", self, runtime_context).clone().executor; + let sync_send = require!("router", self, sync).send.clone(); // note: we can make this optional in the future + let miner_send = self.miner.as_ref().map(|x| x.send.clone()); + let store = require!("router", self, store).clone(); + let file_location_cache = require!("router", self, file_location_cache).clone(); + + let network = self.network.as_mut().ok_or("router requires a network")?; + + let (libp2p, network_recv) = network + .owned + .take() // router takes ownership of libp2p and network_recv + .ok_or("router requires a network")?; + + RouterService::spawn( + executor, + libp2p, + network.globals.clone(), + network_recv, + network.send.clone(), + sync_send, + miner_send, + store, + file_location_cache, + network.keypair.clone(), + router_config, + ); + + Ok(self) + } + + pub async fn with_rpc( + self, + rpc_config: RPCConfig, + chunk_pool_config: ChunkPoolConfig, + ) -> Result { + if !rpc_config.enabled { + return Ok(self); + } + + let executor = require!("rpc", self, runtime_context).clone().executor; + let async_store = require!("rpc", self, async_store).clone(); + let network_send = require!("rpc", self, network).send.clone(); + let mine_send = self.miner.as_ref().map(|x| x.send.clone()); + let synced_tx_recv = require!("sync", self, log_sync).send.subscribe(); + + let (chunk_pool, chunk_pool_handler) = + chunk_pool::unbounded(chunk_pool_config, async_store.clone(), network_send.clone()); + + let chunk_pool_clone = chunk_pool.clone(); + let ctx = rpc::Context { + config: rpc_config, + network_globals: require!("rpc", self, network).globals.clone(), + network_send, + sync_send: require!("rpc", self, sync).send.clone(), + log_store: async_store, + chunk_pool, + shutdown_sender: executor.shutdown_sender(), + mine_service_sender: mine_send, + }; + + let rpc_handle = rpc::run_server(ctx) + .await + .map_err(|e| format!("Unable to start HTTP RPC server: {:?}", e))?; + + executor.spawn(rpc_handle, "rpc"); + executor.spawn(chunk_pool_handler.run(), "chunk_pool_handler"); + executor.spawn( + MemoryChunkPool::monitor_log_entry(chunk_pool_clone, synced_tx_recv), + "chunk_pool_log_monitor", + ); + + Ok(self) + } + + pub async fn with_log_sync(mut self, config: LogSyncConfig) -> Result { + let executor = require!("log_sync", self, runtime_context).clone().executor; + let store = require!("log_sync", self, store).clone(); + let send = LogSyncManager::spawn(config, executor, store) + .await + .map_err(|e| e.to_string())?; + + self.log_sync = Some(LogSyncComponents { send }); + Ok(self) + } + + /// Consumes the builder, returning a `Client` if all necessary components have been + /// specified. + pub fn build(self) -> Result { + require!("client", self, runtime_context); + + Ok(Client { + network_globals: self.network.as_ref().map(|network| network.globals.clone()), + }) + } +} diff --git a/node/src/client/environment.rs b/node/src/client/environment.rs new file mode 100644 index 0000000..a484a98 --- /dev/null +++ b/node/src/client/environment.rs @@ -0,0 +1,253 @@ +//! This crate aims to provide a common set of tools that can be used to create a "environment" to +//! run Zgs services. This allows for the unification of creating tokio runtimes, etc. +//! +//! The idea is that the main thread creates an `Environment`, which is then used to spawn a +//! `Context` which can be handed to any service that wishes to start async tasks. + +use futures::channel::mpsc::{channel, Receiver, Sender}; +use futures::{future, StreamExt}; +use std::sync::Arc; +use task_executor::{ShutdownReason, TaskExecutor}; +use tokio::runtime::{Builder as RuntimeBuilder, Runtime}; + +#[cfg(target_family = "unix")] +use { + futures::Future, + std::{pin::Pin, task::Context, task::Poll}, + tokio::signal::unix::{signal, Signal, SignalKind}, +}; + +#[cfg(not(target_family = "unix"))] +use {futures::channel::oneshot, std::cell::RefCell}; + +/// The maximum time in seconds the client will wait for all internal tasks to shutdown. +const MAXIMUM_SHUTDOWN_TIME: u64 = 15; + +/// Builds an `Environment`. +pub struct EnvironmentBuilder { + runtime: Option>, +} + +impl EnvironmentBuilder { + pub fn new() -> Self { + Self { runtime: None } + } +} + +impl EnvironmentBuilder { + /// Specifies that a multi-threaded tokio runtime should be used. Ideal for production uses. + /// + /// The `Runtime` used is just the standard tokio runtime. + pub fn multi_threaded_tokio_runtime(mut self) -> Result { + self.runtime = Some(Arc::new( + RuntimeBuilder::new_multi_thread() + .enable_all() + .build() + .map_err(|e| format!("Failed to start runtime: {:?}", e))?, + )); + Ok(self) + } + + /// Consumes the builder, returning an `Environment`. + pub fn build(self) -> Result { + let (signal, exit) = exit_future::signal(); + let (signal_tx, signal_rx) = channel(1); + Ok(Environment { + runtime: self + .runtime + .ok_or("Cannot build environment without runtime")?, + signal_tx, + signal_rx: Some(signal_rx), + signal: Some(signal), + exit, + }) + } +} + +/// An execution context that can be used by a service. +/// +/// Distinct from an `Environment` because a `Context` is not able to give a mutable reference to a +/// `Runtime`, instead it only has access to a `Runtime`. +#[derive(Clone)] +pub struct RuntimeContext { + pub executor: TaskExecutor, +} + +/// An environment where Zgs services can run. +pub struct Environment { + runtime: Arc, + + /// Receiver side of an internal shutdown signal. + signal_rx: Option>, + + /// Sender to request shutting down. + signal_tx: Sender, + signal: Option, + exit: exit_future::Exit, +} + +impl Environment { + /// Returns a mutable reference to the `tokio` runtime. + /// + /// Useful in the rare scenarios where it's necessary to block the current thread until a task + /// is finished (e.g., during testing). + pub fn runtime(&self) -> &Arc { + &self.runtime + } + + /// Returns a `Context`. + pub fn core_context(&mut self) -> RuntimeContext { + RuntimeContext { + executor: TaskExecutor::new( + self.runtime().handle().clone(), + self.exit.clone(), + self.signal_tx.clone(), + ), + } + } + + /// Block the current thread until a shutdown signal is received. + /// + /// This can be either the user Ctrl-C'ing or a task requesting to shutdown. + #[cfg(target_family = "unix")] + pub fn block_until_shutdown_requested(&mut self) -> Result { + // future of a task requesting to shutdown + let mut rx = self + .signal_rx + .take() + .ok_or("Inner shutdown already received")?; + let inner_shutdown = + async move { rx.next().await.ok_or("Internal shutdown channel exhausted") }; + futures::pin_mut!(inner_shutdown); + + match self.runtime().block_on(async { + let mut handles = vec![]; + + // setup for handling SIGTERM + match signal(SignalKind::terminate()) { + Ok(terminate_stream) => { + let terminate = SignalFuture::new(terminate_stream, "Received SIGTERM"); + handles.push(terminate); + } + Err(e) => error!(error = %e, "Could not register SIGTERM handler"), + }; + + // setup for handling SIGINT + match signal(SignalKind::interrupt()) { + Ok(interrupt_stream) => { + let interrupt = SignalFuture::new(interrupt_stream, "Received SIGINT"); + handles.push(interrupt); + } + Err(e) => error!(error = %e, "Could not register SIGINT handler"), + } + + // setup for handling a SIGHUP + match signal(SignalKind::hangup()) { + Ok(hup_stream) => { + let hup = SignalFuture::new(hup_stream, "Received SIGHUP"); + handles.push(hup); + } + Err(e) => error!(error = %e, "Could not register SIGHUP handler"), + } + + future::select(inner_shutdown, future::select_all(handles.into_iter())).await + }) { + future::Either::Left((Ok(reason), _)) => { + info!(reason = reason.message(), "Internal shutdown received"); + Ok(reason) + } + future::Either::Left((Err(e), _)) => Err(e.into()), + future::Either::Right(((res, _, _), _)) => { + res.ok_or_else(|| "Handler channel closed".to_string()) + } + } + } + + /// Block the current thread until a shutdown signal is received. + /// + /// This can be either the user Ctrl-C'ing or a task requesting to shutdown. + #[cfg(not(target_family = "unix"))] + pub fn block_until_shutdown_requested(&mut self) -> Result { + // future of a task requesting to shutdown + let mut rx = self + .signal_rx + .take() + .ok_or("Inner shutdown already received")?; + let inner_shutdown = + async move { rx.next().await.ok_or("Internal shutdown channel exhausted") }; + futures::pin_mut!(inner_shutdown); + + // setup for handling a Ctrl-C + let (ctrlc_send, ctrlc_oneshot) = oneshot::channel(); + let ctrlc_send_c = RefCell::new(Some(ctrlc_send)); + ctrlc::set_handler(move || { + if let Some(ctrlc_send) = ctrlc_send_c.try_borrow_mut().unwrap().take() { + if let Err(e) = ctrlc_send.send(()) { + error!("Error sending ctrl-c message: {:?}", e); + } + } + }) + .map_err(|e| format!("Could not set ctrlc handler: {:?}", e))?; + + // Block this thread until a shutdown signal is received. + match self + .runtime() + .block_on(future::select(inner_shutdown, ctrlc_oneshot)) + { + future::Either::Left((Ok(reason), _)) => { + info!(reasion = reason.message(), "Internal shutdown received"); + Ok(reason) + } + future::Either::Left((Err(e), _)) => Err(e.into()), + future::Either::Right((x, _)) => x + .map(|()| ShutdownReason::Success("Received Ctrl+C")) + .map_err(|e| format!("Ctrlc oneshot failed: {}", e)), + } + } + + /// Shutdown the `tokio` runtime when all tasks are idle. + pub fn shutdown_on_idle(self) { + match Arc::try_unwrap(self.runtime) { + Ok(runtime) => { + runtime.shutdown_timeout(std::time::Duration::from_secs(MAXIMUM_SHUTDOWN_TIME)) + } + Err(e) => warn!( + error = ?e, + "Failed to obtain runtime access to shutdown gracefully", + ), + } + } + + /// Fire exit signal which shuts down all spawned services + pub fn fire_signal(&mut self) { + if let Some(signal) = self.signal.take() { + let _ = signal.fire(); + } + } +} + +#[cfg(target_family = "unix")] +struct SignalFuture { + signal: Signal, + message: &'static str, +} + +#[cfg(target_family = "unix")] +impl SignalFuture { + pub fn new(signal: Signal, message: &'static str) -> SignalFuture { + SignalFuture { signal, message } + } +} + +#[cfg(target_family = "unix")] +impl Future for SignalFuture { + type Output = Option; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match self.signal.poll_recv(cx) { + Poll::Pending => Poll::Pending, + Poll::Ready(Some(_)) => Poll::Ready(Some(ShutdownReason::Success(self.message))), + Poll::Ready(None) => Poll::Ready(None), + } + } +} diff --git a/node/src/client/mod.rs b/node/src/client/mod.rs new file mode 100644 index 0000000..4074b04 --- /dev/null +++ b/node/src/client/mod.rs @@ -0,0 +1,34 @@ +#![allow(dead_code)] + +mod builder; +mod environment; + +use network::{Enr, Multiaddr, NetworkGlobals}; +use std::sync::Arc; + +pub use builder::ClientBuilder; +pub use environment::{Environment, EnvironmentBuilder, RuntimeContext}; + +/// The core Zgs client. +/// +/// Holds references to running services, cleanly shutting them down when dropped. +pub struct Client { + network_globals: Option>, +} + +impl Client { + /// Returns the port of the client's libp2p stack, if it was started. + pub fn libp2p_listen_port(&self) -> Option { + self.network_globals.as_ref().map(|n| n.listen_port_tcp()) + } + + /// Returns the list of libp2p addresses the client is listening to. + pub fn libp2p_listen_addresses(&self) -> Option> { + self.network_globals.as_ref().map(|n| n.listen_multiaddrs()) + } + + /// Returns the local libp2p ENR of this node, for network discovery. + pub fn enr(&self) -> Option { + self.network_globals.as_ref().map(|n| n.local_enr()) + } +} diff --git a/node/src/config/config_macro.rs b/node/src/config/config_macro.rs new file mode 100644 index 0000000..99eb278 --- /dev/null +++ b/node/src/config/config_macro.rs @@ -0,0 +1,86 @@ +// Copyright 2019 Conflux Foundation. All rights reserved. +// Conflux is free software and distributed under GNU General Public License. +// See http://www.gnu.org/licenses/ + +macro_rules! if_option { + (Option<$type:ty>, THEN {$($then:tt)*} ELSE {$($otherwise:tt)*}) => ( + $($then)* + ); + ($type:ty, THEN {$($then:tt)*} ELSE {$($otherwise:tt)*}) => ( + $($otherwise)* + ); +} + +macro_rules! if_not_vector { + (Vec<$type:ty>, THEN {$($then:tt)*}) => ( + {} + ); + ($type:ty, THEN {$($then:tt)*}) => ( + $($then)* + ); +} + +macro_rules! underscore_to_hyphen { + ($e:expr) => { + str::replace($e, "_", "-") + }; +} + +macro_rules! build_config{ + ($(($name:ident, ($($type:tt)+), $default:expr))*) => { + #[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize)] + pub struct RawConfiguration { + $(pub $name: $($type)+,)* + } + + impl Default for RawConfiguration { + fn default() -> Self { + RawConfiguration { + $($name: $default,)* + } + } + } + + impl RawConfiguration { + // First parse arguments from config file, and then parse them from command line. + // Replace the ones from config file with the ones from commandline for duplicates. + pub fn parse(matches: &clap::ArgMatches) -> Result { + let mut config = RawConfiguration::default(); + + // read from config file + if let Some(config_file) = matches.value_of("config") { + let config_value = std::fs::read_to_string(config_file) + .map_err(|e| format!("failed to read configuration file: {:?}", e))? + .parse::() + .map_err(|e| format!("failed to parse configuration file: {:?}", e))?; + + $( + if let Some(value) = config_value.get(stringify!($name)) { + config.$name = if_option!($($type)+, + THEN { Some(value.clone().try_into().map_err(|e| format!("Invalid {}: err={:?}", stringify!($name), e).to_owned())?) } + ELSE { value.clone().try_into().map_err(|e| format!("Invalid {}: err={:?}", stringify!($name), e).to_owned())? } + ); + } + )* + } + + // read from command line + $( + #[allow(unused_variables)] + if let Some(value) = matches.value_of(underscore_to_hyphen!(stringify!($name))) { + if_not_vector!($($type)+, THEN { + config.$name = if_option!($($type)+, + THEN{ Some(value.parse().map_err(|_| concat!("Invalid ", stringify!($name)).to_owned())?) } + ELSE{ value.parse().map_err(|_| concat!("Invalid ", stringify!($name)).to_owned())? } + )} + ) + } + )* + + Ok(config) + } + } + } +} + +pub(crate) use {build_config, if_not_vector, if_option, underscore_to_hyphen}; diff --git a/node/src/config/convert.rs b/node/src/config/convert.rs new file mode 100644 index 0000000..57be42c --- /dev/null +++ b/node/src/config/convert.rs @@ -0,0 +1,158 @@ +#![allow(clippy::field_reassign_with_default)] + +use crate::ZgsConfig; +use ethereum_types::H256; +use log_entry_sync::{CacheConfig, ContractAddress, LogSyncConfig}; +use miner::MinerConfig; +use network::NetworkConfig; +use rpc::RPCConfig; +use std::time::Duration; +use storage::StorageConfig; + +impl ZgsConfig { + pub fn network_config(&self) -> Result { + let mut network_config = NetworkConfig::default(); + + network_config.listen_address = self + .network_listen_address + .parse::() + .map_err(|e| format!("Unable to parse network_listen_address: {:?}", e))?; + + network_config.network_dir = self.network_dir.clone().into(); + network_config.libp2p_port = self.network_libp2p_port; + network_config.disable_discovery = self.network_disable_discovery; + network_config.discovery_port = self.network_discovery_port; + network_config.enr_tcp_port = self.network_enr_tcp_port; + network_config.enr_udp_port = self.network_enr_udp_port; + network_config.enr_address = self + .network_enr_address + .as_ref() + .map(|x| x.parse::().unwrap()); + + network_config.boot_nodes_multiaddr = self + .network_boot_nodes + .iter() + .map(|addr| addr.parse::()) + .collect::>() + .map_err(|e| format!("Unable to parse network_boot_nodes: {:?}", e))?; + + network_config.libp2p_nodes = self + .network_libp2p_nodes + .iter() + .map(|addr| addr.parse::()) + .collect::>() + .map_err(|e| format!("Unable to parse network_libp2p_nodes: {:?}", e))?; + + network_config.discv5_config.table_filter = |_| true; + + // TODO + network_config.target_peers = self.network_target_peers; + network_config.private = self.network_private; + + Ok(network_config) + } + + pub fn storage_config(&self) -> Result { + Ok(StorageConfig { + db_dir: self.db_dir.clone().into(), + }) + } + + pub fn rpc_config(&self) -> Result { + let listen_address = self + .rpc_listen_address + .parse::() + .map_err(|e| format!("Unable to parse rpc_listen_address: {:?}", e))?; + + Ok(RPCConfig { + enabled: self.rpc_enabled, + listen_address, + chunks_per_segment: self.rpc_chunks_per_segment, + max_cache_file_size: self.rpc_max_cache_file_size, + }) + } + + pub fn log_sync_config(&self) -> Result { + let contract_address = self + .log_contract_address + .parse::() + .map_err(|e| format!("Unable to parse log_contract_address: {:?}", e))?; + let cache_config = CacheConfig { + // 100 MB. + max_data_size: self.max_cache_data_size, + // This should be enough if we have about one Zgs tx per block. + tx_seq_ttl: self.cache_tx_seq_ttl, + }; + Ok(LogSyncConfig::new( + self.blockchain_rpc_endpoint.clone(), + contract_address, + self.log_sync_start_block_number, + self.confirmation_block_count, + cache_config, + self.log_page_size, + self.rate_limit_retries, + self.timeout_retries, + self.initial_backoff, + self.recover_query_delay, + )) + } + + pub fn mine_config(&self) -> Result, String> { + let flow_address = self + .log_contract_address + .parse::() + .map_err(|e| format!("Unable to parse log_contract_address: {:?}", e))?; + let mine_address = self + .mine_contract_address + .parse::() + .map_err(|e| format!("Unable to parse mine_address: {:?}", e))?; + + let miner_id = if let Some(ref miner_id) = self.miner_id { + Some( + miner_id + .parse::() + .map_err(|e| format!("Unable to parse miner_id: {:?}", e))?, + ) + } else { + None + }; + let miner_key = if let Some(ref miner_key) = self.miner_key { + Some( + miner_key + .parse::() + .map_err(|e| format!("Unable to parse miner_key: {:?}", e))?, + ) + } else { + None + }; + Ok(MinerConfig::new( + miner_id, + miner_key, + self.blockchain_rpc_endpoint.clone(), + mine_address, + flow_address, + )) + } + + pub fn chunk_pool_config(&self) -> chunk_pool::Config { + chunk_pool::Config { + write_window_size: self.chunk_pool_write_window_size, + max_cached_chunks_all: self.chunk_pool_max_cached_chunks_all, + max_writings: self.chunk_pool_max_writings, + expiration_time_secs: self.chunk_pool_expiration_time_secs, + } + } + + pub fn router_config(&self, network_config: &NetworkConfig) -> Result { + let mut router_config = router::Config::default(); + router_config.libp2p_nodes = network_config.libp2p_nodes.to_vec(); + Ok(router_config) + } + + pub fn sync_config(&self) -> sync::Config { + let mut config = self.sync.clone(); + config.find_peer_timeout = Duration::from_secs(self.find_peer_timeout_secs); + config.enable_chunk_request = self.enable_chunk_request; + config + } +} diff --git a/node/src/config/mod.rs b/node/src/config/mod.rs new file mode 100644 index 0000000..5ecfba6 --- /dev/null +++ b/node/src/config/mod.rs @@ -0,0 +1,100 @@ +mod config_macro; + +mod convert; +use config_macro::*; +use serde::Deserialize; +use std::ops::Deref; + +build_config! { + // network + (network_dir, (String), "network".to_string()) + (network_listen_address, (String), "0.0.0.0".to_string()) + (network_enr_address, (Option), None) + (network_enr_tcp_port, (Option), None) + (network_enr_udp_port, (Option), None) + (network_libp2p_port, (u16), 1234) + (network_discovery_port, (u16), 1234) + (network_target_peers, (usize), 3) + (network_boot_nodes, (Vec), vec![]) + (network_libp2p_nodes, (Vec), vec![]) + (network_private, (bool), false) + (network_disable_discovery, (bool), false) + + // log sync + (blockchain_rpc_endpoint, (String), "http://127.0.0.1:8545".to_string()) + (log_contract_address, (String), "".to_string()) + (log_sync_start_block_number, (u64), 0) + (confirmation_block_count, (u64), 12) + (log_page_size, (u64), 1000) + (max_cache_data_size, (usize), 100 * 1024 * 1024) // 100 MB + (cache_tx_seq_ttl, (usize), 500) + + (rate_limit_retries, (u32), 100) + (timeout_retries, (u32), 100) + (initial_backoff, (u64), 500) + (recover_query_delay, (u64), 50) + + // rpc + (rpc_enabled, (bool), true) + (rpc_listen_address, (String), "127.0.0.1:5678".to_string()) + (rpc_chunks_per_segment, (usize), 1024) + (rpc_max_cache_file_size, (usize), 10*1024*1024) //10MB + + // chunk pool + (chunk_pool_write_window_size, (usize), 4) + (chunk_pool_max_cached_chunks_all, (usize), 4*1024*1024) // 1G + (chunk_pool_max_writings, (usize), 16) + (chunk_pool_expiration_time_secs, (u64), 300) // 5 minutes + + // db + (db_dir, (String), "db".to_string()) + + // misc + (log_config_file, (String), "log_config".to_string()) + (log_directory, (String), "log".to_string()) + + // mine + (mine_contract_address, (String), "".to_string()) + (miner_id, (Option), None) + (miner_key, (Option), None) + + // sync + (find_peer_timeout_secs, (u64), 30) + (enable_chunk_request, (bool), true) +} + +#[derive(Debug, Default, Deserialize)] +#[serde(default)] +pub struct ZgsConfig { + pub raw_conf: RawConfiguration, + + pub sync: sync::Config, +} + +impl Deref for ZgsConfig { + type Target = RawConfiguration; + + fn deref(&self) -> &Self::Target { + &self.raw_conf + } +} + +impl ZgsConfig { + pub fn parse(matches: &clap::ArgMatches) -> Result { + let config_file = match matches.get_one::("config") { + Some(file) => file.as_str(), + None => return Err("config file not specified".to_string()), + }; + + let mut config = config::Config::builder() + .add_source(config::File::with_name(config_file)) + .build() + .map_err(|e| format!("Failed to build config: {:?}", e))? + .try_deserialize::() + .map_err(|e| format!("Failed to deserialize config: {:?}", e))?; + + config.raw_conf = RawConfiguration::parse(matches)?; + + Ok(config) + } +} diff --git a/node/src/log.rs b/node/src/log.rs new file mode 100644 index 0000000..5f1b526 --- /dev/null +++ b/node/src/log.rs @@ -0,0 +1,61 @@ +use task_executor::TaskExecutor; +use tracing::Level; +use tracing_subscriber::EnvFilter; + +const LOG_RELOAD_PERIOD_SEC: u64 = 30; + +pub fn configure(log_level_file: &str, log_directory: &str, executor: TaskExecutor) { + let file_appender = tracing_appender::rolling::daily(log_directory, "zgs.log"); + let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); + let builder = tracing_subscriber::fmt() + .with_max_level(Level::TRACE) + .with_env_filter(EnvFilter::default()) + .with_writer(non_blocking) + .with_ansi(false) + // .with_file(true) + // .with_line_number(true) + // .with_thread_names(true) + .with_filter_reloading(); + + let handle = builder.reload_handle(); + builder.init(); + + let level_file = log_level_file.to_string(); + + // load config synchronously + let mut config = std::fs::read_to_string(&level_file).unwrap_or_default(); + let _ = handle.reload(&config); + + // periodically check for config changes + executor.spawn( + async move { + // move the log writer guard so that it's not dropped. + let _moved_guard = guard; + let mut interval = + tokio::time::interval(std::time::Duration::from_secs(LOG_RELOAD_PERIOD_SEC)); + + loop { + interval.tick().await; + + let new_config = match tokio::fs::read_to_string(&level_file).await { + Ok(c) if c == config => continue, + Ok(c) => c, + Err(e) => { + println!("Unable to read log file {}: {:?}", level_file, e); + continue; + } + }; + + println!("Updating log config to {:?}", new_config); + + match handle.reload(&new_config) { + Ok(()) => config = new_config, + Err(e) => { + println!("Failed to load new config: {:?}", e); + } + } + } + }, + "log_reload", + ); +} diff --git a/node/src/main.rs b/node/src/main.rs new file mode 100644 index 0000000..dade5ee --- /dev/null +++ b/node/src/main.rs @@ -0,0 +1,101 @@ +#[macro_use] +extern crate tracing; + +mod cli; +mod client; +mod config; +mod log; + +use crate::config::ZgsConfig; +use client::{Client, ClientBuilder, RuntimeContext}; +use std::error::Error; + +async fn start_node(context: RuntimeContext, config: ZgsConfig) -> Result { + let network_config = config.network_config()?; + let storage_config = config.storage_config()?; + let rpc_config = config.rpc_config()?; + let log_sync_config = config.log_sync_config()?; + let miner_config = config.mine_config()?; + let router_config = config.router_config(&network_config)?; + let sync_config = config.sync_config(); + + ClientBuilder::default() + .with_runtime_context(context) + .with_rocksdb_store(&storage_config)? + .with_log_sync(log_sync_config) + .await? + .with_file_location_cache() + .with_network(&network_config) + .await? + .with_sync(sync_config) + .await? + .with_miner(miner_config) + .await? + .with_router(router_config)? + .with_rpc(rpc_config, config.chunk_pool_config()) + .await? + .build() +} + +fn main() -> Result<(), Box> { + // Only allow 64-bit targets for compilation, since there are many + // type conversions between `usize` and `u64`, or even use `usize` + // as file size or chunk index. + #[cfg(not(target_pointer_width = "64"))] + compile_error!("compilation is only allowed for 64-bit targets"); + + // enable backtraces + std::env::set_var("RUST_BACKTRACE", "1"); + + // runtime environment + let mut environment = client::EnvironmentBuilder::new() + .multi_threaded_tokio_runtime()? + .build()?; + + let context = environment.core_context(); + let executor = context.executor.clone(); + + // CLI, config, and logs + let matches = cli::cli_app().get_matches(); + let config = ZgsConfig::parse(&matches)?; + log::configure( + &config.log_config_file, + &config.log_directory, + executor.clone(), + ); + + // start services + executor.clone().spawn( + async move { + info!("Starting services..."); + if let Err(e) = start_node(context.clone(), config).await { + error!(reason = %e, "Failed to start zgs node"); + // Ignore the error since it always occurs during normal operation when + // shutting down. + let _ = + executor + .shutdown_sender() + .try_send(task_executor::ShutdownReason::Failure( + "Failed to start zgs node", + )); + } else { + info!("Services started"); + } + }, + "zgs_node", + ); + + // Block this thread until we get a ctrl-c or a task sends a shutdown signal. + let shutdown_reason = environment.block_until_shutdown_requested()?; + info!(reason = ?shutdown_reason, "Shutting down..."); + + environment.fire_signal(); + + // Shutdown the environment once all tasks have completed. + environment.shutdown_on_idle(); + + match shutdown_reason { + task_executor::ShutdownReason::Success(_) => Ok(()), + task_executor::ShutdownReason::Failure(msg) => Err(msg.to_string().into()), + } +} diff --git a/node/storage-async/Cargo.toml b/node/storage-async/Cargo.toml new file mode 100644 index 0000000..2d3b1c6 --- /dev/null +++ b/node/storage-async/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "storage-async" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { version = "=1.0.58", features = ["backtrace"] } +shared_types = { path = "../shared_types" } +storage = { path = "../storage" } +task_executor = { path = "../../common/task_executor" } +tokio = { version = "1.19.2", features = ["sync"] } +tracing = "0.1.35" \ No newline at end of file diff --git a/node/storage-async/src/lib.rs b/node/storage-async/src/lib.rs new file mode 100644 index 0000000..546b290 --- /dev/null +++ b/node/storage-async/src/lib.rs @@ -0,0 +1,91 @@ +#[macro_use] +extern crate tracing; + +use anyhow::bail; +use shared_types::{Chunk, ChunkArray, ChunkArrayWithProof, DataRoot, Transaction}; +use std::sync::Arc; +use storage::{error, error::Result, log_store::Store as LogStore, H256}; +use task_executor::TaskExecutor; +use tokio::sync::{oneshot, RwLock}; + +/// The name of the worker tokio tasks. +const WORKER_TASK_NAME: &str = "async_storage_worker"; + +macro_rules! delegate { + (fn $name:tt($($v:ident: $t:ty),*)) => { + delegate!($name($($v: $t),*) -> ()); + }; + + (fn $name:tt($($v:ident: $t:ty),*) -> $ret:ty) => { + pub async fn $name(&self, $($v: $t),*) -> $ret { + self.spawn(move |store| store.$name($($v),*)).await + } + }; +} + +#[derive(Clone)] +pub struct Store { + /// Log and transaction storage. + store: Arc>, + + /// Tokio executor for spawning worker tasks. + executor: TaskExecutor, +} + +impl Store { + pub fn new(store: Arc>, executor: TaskExecutor) -> Self { + Store { store, executor } + } + + delegate!(fn check_tx_completed(tx_seq: u64) -> Result); + delegate!(fn get_chunk_by_tx_and_index(tx_seq: u64, index: usize) -> Result>); + delegate!(fn get_chunks_by_tx_and_index_range(tx_seq: u64, index_start: usize, index_end: usize) -> Result>); + delegate!(fn get_chunks_with_proof_by_tx_and_index_range(tx_seq: u64, index_start: usize, index_end: usize) -> Result>); + delegate!(fn get_tx_by_seq_number(seq: u64) -> Result>); + delegate!(fn put_chunks(tx_seq: u64, chunks: ChunkArray) -> Result<()>); + delegate!(fn put_chunks_with_tx_hash(tx_seq: u64, tx_hash: H256, chunks: ChunkArray) -> Result); + delegate!(fn get_chunk_by_flow_index(index: u64, length: u64) -> Result>); + delegate!(fn finalize_tx(tx_seq: u64) -> Result<()>); + delegate!(fn finalize_tx_with_hash(tx_seq: u64, tx_hash: H256) -> Result); + + pub async fn get_tx_seq_by_data_root(&self, data_root: &DataRoot) -> Result> { + let root = *data_root; + self.spawn(move |store| store.get_tx_seq_by_data_root(&root)) + .await + } + + pub async fn get_tx_by_data_root(&self, data_root: &DataRoot) -> Result> { + let root = *data_root; + self.spawn(move |store| store.get_tx_by_data_root(&root)) + .await + } + + async fn spawn(&self, f: F) -> Result + where + F: FnOnce(&mut dyn LogStore) -> Result + Send + 'static, + T: Send + 'static, + { + let store = self.store.clone(); + let (tx, rx) = oneshot::channel(); + + self.executor.spawn( + async move { + // FIXME(zz): Not all functions need `write`. Refactor store usage. + let res = f(&mut *store.write().await); + + if tx.send(res).is_err() { + error!("Unable to complete async storage operation: the receiver dropped"); + } + }, + WORKER_TASK_NAME, + ); + + rx.await + .unwrap_or_else(|_| bail!(error::Error::Custom("Receiver error".to_string()))) + } + + // FIXME(zz): Refactor the lock and async call here. + pub fn get_store(&self) -> &RwLock { + self.store.as_ref() + } +} diff --git a/node/storage/Cargo.toml b/node/storage/Cargo.toml new file mode 100644 index 0000000..765f4d6 --- /dev/null +++ b/node/storage/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "storage" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { version = "=1.0.58", features = ["backtrace"] } +append_merkle = {path = "../../common/append_merkle"} +zgs_spec = {path = "../../common/spec"} +zgs_seal = {path = "../../common/zgs_seal"} +eth2_ssz = "0.4.0" +eth2_ssz_derive = "0.3.0" +ethereum-types = "0.14" +hex = "0.4.3" +kvdb = "0.13.0" +kvdb-memorydb = "0.13.0" +kvdb-rocksdb = "0.19.0" +#merkle_light = {git = "https://github.com/sitano/merkle_light.git", rev = "fe31d4e" } +merkle_light = { path = "../../common/merkle_light" } +merkle_tree = { path = "../../common/merkle_tree"} +rayon = "1.5.3" +shared_types = { path = "../shared_types" } +tracing = "0.1.35" +typenum = "1.15.0" +bitmaps = "^3.2" +static_assertions = "1.1" +tiny-keccak = "*" +itertools = "0.10.5" + +[dev-dependencies] +tempdir = "0.3.7" +rand = "0.8.5" +hex-literal = "0.3.4" +criterion = "0.4" + +[[bench]] +name = "benchmark" +harness = false + +[profile.bench] +debug = true \ No newline at end of file diff --git a/node/storage/benches/benchmark.rs b/node/storage/benches/benchmark.rs new file mode 100644 index 0000000..8aae9f7 --- /dev/null +++ b/node/storage/benches/benchmark.rs @@ -0,0 +1,184 @@ +use std::{ + fs, + path::Path, + sync::{Arc, RwLock}, +}; + +use criterion::{criterion_group, criterion_main, Criterion}; +use rand::{random, Rng}; +use shared_types::{ChunkArray, Transaction, CHUNK_SIZE}; +use storage::{ + log_store::{ + log_manager::{sub_merkle_tree, tx_subtree_root_list_padded, LogConfig}, + Store, + }, + LogManager, +}; + +fn write_performance(c: &mut Criterion) { + if Path::new("db_write").exists() { + fs::remove_dir_all("db_write").unwrap(); + } + + let store: Arc> = Arc::new(RwLock::new( + LogManager::rocksdb(LogConfig::default(), "db_write") + .map_err(|e| format!("Unable to start RocksDB store: {:?}", e)) + .unwrap(), + )); + + let chunk_count = 2048; + let data_size = CHUNK_SIZE * chunk_count; + let (chunk_size_padded, _) = shared_types::compute_padded_chunk_size(data_size); + + let mut data_vec = vec![]; + let mut merkel_nodes_vec = vec![]; + let mut first_tree_size_vec = vec![]; + let mut merkel_root_vec = vec![]; + + for _ in 0..5000 { + let mut data = vec![0; data_size]; + for i in 0..data_size { + data[i] = random(); + } + + let merkel_nodes = tx_subtree_root_list_padded(&data[..]); + let first_tree_size = 1 << (merkel_nodes[0].0 - 1); + + let merkle = sub_merkle_tree(&data).unwrap(); + let merkel_root = merkle.root().into(); + + data_vec.push(data); + merkel_nodes_vec.push(merkel_nodes); + first_tree_size_vec.push(first_tree_size); + merkel_root_vec.push(merkel_root); + } + + let mut seq = 0; + let mut offset = 1; + + let mut group = c.benchmark_group("write performance"); + group.sample_size(10); + group.bench_function("write performance", move |b| { + b.iter(|| { + let first_tree_size = first_tree_size_vec[seq as usize]; + let data = data_vec[seq as usize].clone(); + let merkel_root = merkel_root_vec[seq as usize]; + let merkel_nodes = merkel_nodes_vec[seq as usize].clone(); + + let start_offset = if offset % first_tree_size == 0 { + offset + } else { + (offset / first_tree_size + 1) * first_tree_size + }; + + let chunks = ChunkArray { + data: data.to_vec(), + start_index: 0, + }; + + let tx = Transaction { + stream_ids: vec![], + size: data_size as u64, + data_merkle_root: merkel_root, + seq, + data: vec![], + start_entry_index: start_offset, + merkle_nodes: merkel_nodes, + }; + + store.write().unwrap().put_tx(tx).unwrap(); + store + .write() + .unwrap() + .put_chunks(seq, chunks.clone()) + .unwrap(); + store.write().unwrap().finalize_tx(seq).unwrap(); + + offset = start_offset + chunk_size_padded as u64; + seq += 1; + }) + }); +} + +fn read_performance(c: &mut Criterion) { + if Path::new("db_read").exists() { + fs::remove_dir_all("db_read").unwrap(); + } + + let store: Arc> = Arc::new(RwLock::new( + LogManager::rocksdb(LogConfig::default(), "db_read") + .map_err(|e| format!("Unable to start RocksDB store: {:?}", e)) + .unwrap(), + )); + + let tx_size = 1000; + let chunk_count = 4096; + let data_size = CHUNK_SIZE * chunk_count; + let mut offset = 1; + let (chunk_size_padded, _) = shared_types::compute_padded_chunk_size(data_size); + + for seq in 0..tx_size { + let mut data = vec![0; data_size]; + for i in 0..data_size { + data[i] = random(); + } + + let merkel_nodes = tx_subtree_root_list_padded(&data[..]); + let first_tree_size = 1 << (merkel_nodes[0].0 - 1); + + let merkle = sub_merkle_tree(&data).unwrap(); + let merkel_root = merkle.root().into(); + + let start_offset = if offset % first_tree_size == 0 { + offset + } else { + (offset / first_tree_size + 1) * first_tree_size + }; + + let chunks = ChunkArray { + data: data.to_vec(), + start_index: 0, + }; + + let tx = Transaction { + stream_ids: vec![], + size: data_size as u64, + data_merkle_root: merkel_root, + seq, + data: vec![], + start_entry_index: start_offset, + merkle_nodes: merkel_nodes, + }; + + store.write().unwrap().put_tx(tx).unwrap(); + store + .write() + .unwrap() + .put_chunks(seq, chunks.clone()) + .unwrap(); + store.write().unwrap().finalize_tx(seq).unwrap(); + + offset = start_offset + chunk_size_padded as u64; + } + + let mut rng = rand::thread_rng(); + + let mut group = c.benchmark_group("read performance"); + group.sample_size(100); + group.bench_function("read performance", move |b| { + b.iter(|| { + let tx_seq = rng.gen_range(0..tx_size); + let index_start = rng.gen_range(0..=chunk_count); + let index_end = rng.gen_range((index_start + 1)..=(chunk_count + 1)); + + store + .read() + .unwrap() + .get_chunks_with_proof_by_tx_and_index_range(tx_seq, index_start, index_end) + .unwrap(); + }) + }); +} + +criterion_group!(benches, write_performance, read_performance); +criterion_main!(benches); diff --git a/node/storage/src/config.rs b/node/storage/src/config.rs new file mode 100644 index 0000000..1f537d3 --- /dev/null +++ b/node/storage/src/config.rs @@ -0,0 +1,6 @@ +use std::path::PathBuf; + +#[derive(Clone)] +pub struct Config { + pub db_dir: PathBuf, +} diff --git a/node/storage/src/error.rs b/node/storage/src/error.rs new file mode 100644 index 0000000..3a7082e --- /dev/null +++ b/node/storage/src/error.rs @@ -0,0 +1,42 @@ +use anyhow; +use ssz::DecodeError; +use std::error::Error as ErrorTrait; +use std::fmt::{Debug, Display, Formatter}; +use std::io::Error as IoError; + +pub type Result = anyhow::Result; + +#[derive(Debug)] +pub enum Error { + Io(IoError), + /// A partial chunk batch is written. + InvalidBatchBoundary, + ValueDecodingError(DecodeError), + Custom(String), +} + +impl From for Error { + fn from(e: IoError) -> Self { + Error::Io(e) + } +} + +impl From for Error { + fn from(e: DecodeError) -> Self { + Error::ValueDecodingError(e) + } +} + +impl From for Error { + fn from(e: anyhow::Error) -> Self { + Error::Custom(e.to_string()) + } +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "StorageError: {:?}", self) + } +} + +impl ErrorTrait for Error {} diff --git a/node/storage/src/lib.rs b/node/storage/src/lib.rs new file mode 100644 index 0000000..dd5ad7b --- /dev/null +++ b/node/storage/src/lib.rs @@ -0,0 +1,32 @@ +use kvdb::KeyValueDB; + +pub mod config; +pub mod error; +pub mod log_store; + +pub use config::Config as StorageConfig; +pub use log_store::log_manager::LogManager; + +pub use ethereum_types::H256; + +pub trait ZgsKeyValueDB: KeyValueDB { + fn put(&self, col: u32, key: &[u8], value: &[u8]) -> std::io::Result<()> { + let mut tx = self.transaction(); + tx.put(col, key, value); + self.write(tx) + } + + fn delete(&self, col: u32, key: &[u8]) -> std::io::Result<()> { + let mut tx = self.transaction(); + tx.delete(col, key); + self.write(tx) + } + + fn delete_with_prefix(&self, col: u32, key_prefix: &[u8]) -> std::io::Result<()> { + let mut tx = self.transaction(); + tx.delete_prefix(col, key_prefix); + self.write(tx) + } +} + +impl ZgsKeyValueDB for T {} diff --git a/node/storage/src/log_store/config.rs b/node/storage/src/log_store/config.rs new file mode 100644 index 0000000..ace314a --- /dev/null +++ b/node/storage/src/log_store/config.rs @@ -0,0 +1,85 @@ +use anyhow::{anyhow, Result}; +use kvdb::{DBKey, DBOp}; +use ssz::{Decode, Encode}; + +use crate::LogManager; + +use super::log_manager::COL_MISC; + +pub trait Configurable { + fn get_config(&self, key: &[u8]) -> Result>>; + fn set_config(&self, key: &[u8], value: &[u8]) -> Result<()>; + fn remove_config(&self, key: &[u8]) -> Result<()>; + + fn exec_configs(&self, tx: ConfigTx) -> Result<()>; +} + +#[derive(Default)] +pub struct ConfigTx { + ops: Vec, +} + +impl ConfigTx { + pub fn append(&mut self, other: &mut Self) { + self.ops.append(&mut other.ops); + } + + pub fn set_config, T: Encode>(&mut self, key: &K, value: &T) { + self.ops.push(DBOp::Insert { + col: COL_MISC, + key: DBKey::from_slice(key.as_ref()), + value: value.as_ssz_bytes(), + }); + } + + pub fn remove_config>(&mut self, key: &K) { + self.ops.push(DBOp::Delete { + col: COL_MISC, + key: DBKey::from_slice(key.as_ref()), + }); + } +} + +pub trait ConfigurableExt: Configurable { + fn get_config_decoded, T: Decode>(&self, key: &K) -> Result> { + match self.get_config(key.as_ref())? { + Some(val) => Ok(Some( + T::from_ssz_bytes(&val).map_err(|e| anyhow!("SSZ decode error: {:?}", e))?, + )), + None => Ok(None), + } + } + + fn set_config_encoded, T: Encode>(&self, key: &K, value: &T) -> Result<()> { + self.set_config(key.as_ref(), &value.as_ssz_bytes()) + } + + fn remove_config_by_key>(&self, key: &K) -> Result<()> { + self.remove_config(key.as_ref()) + } +} + +impl ConfigurableExt for T {} + +impl Configurable for LogManager { + fn get_config(&self, key: &[u8]) -> Result>> { + Ok(self.db.get(COL_MISC, key)?) + } + + fn set_config(&self, key: &[u8], value: &[u8]) -> Result<()> { + self.db.put(COL_MISC, key, value)?; + Ok(()) + } + + fn remove_config(&self, key: &[u8]) -> Result<()> { + Ok(self.db.delete(COL_MISC, key)?) + } + + fn exec_configs(&self, tx: ConfigTx) -> Result<()> { + let mut db_tx = self.db.transaction(); + db_tx.ops = tx.ops; + self.db.write(db_tx)?; + + Ok(()) + } +} diff --git a/node/storage/src/log_store/flow_store.rs b/node/storage/src/log_store/flow_store.rs new file mode 100644 index 0000000..34fbcac --- /dev/null +++ b/node/storage/src/log_store/flow_store.rs @@ -0,0 +1,539 @@ +use super::load_chunk::EntryBatch; +use super::{MineLoadChunk, SealAnswer, SealTask}; +use crate::error::Error; +use crate::log_store::log_manager::{bytes_to_entries, COL_ENTRY_BATCH, COL_ENTRY_BATCH_ROOT}; +use crate::log_store::{FlowRead, FlowSeal, FlowWrite}; +use crate::{try_option, ZgsKeyValueDB}; +use anyhow::{anyhow, bail, Result}; +use append_merkle::{MerkleTreeInitialData, MerkleTreeRead}; +use itertools::Itertools; +use zgs_spec::{BYTES_PER_SECTOR, SEALS_PER_LOAD, SECTORS_PER_LOAD, SECTORS_PER_SEAL}; +use shared_types::{ChunkArray, DataRoot, FlowProof}; +use ssz::{Decode, Encode}; +use ssz_derive::{Decode as DeriveDecode, Encode as DeriveEncode}; +use std::cmp::Ordering; +use std::collections::BTreeMap; +use std::fmt::Debug; +use std::sync::Arc; +use std::{cmp, mem}; +use tracing::{debug, error, trace}; + +pub struct FlowStore { + db: FlowDBStore, + // TODO(kevin): This is an in-memory cache for recording which chunks are ready for sealing. It should be persisted on disk. + to_seal_set: BTreeMap, + // Data sealing is an asynchronized process. + // The sealing service uses the version number to distinguish if revert happens during sealing. + to_seal_version: usize, + config: FlowConfig, +} + +impl FlowStore { + pub fn new(db: Arc, config: FlowConfig) -> Self { + Self { + db: FlowDBStore::new(db), + to_seal_set: Default::default(), + to_seal_version: 0, + config, + } + } + + pub fn put_batch_root_list(&self, root_map: BTreeMap) -> Result<()> { + self.db.put_batch_root_list(root_map) + } + + pub fn insert_subtree_list_for_batch( + &self, + batch_index: usize, + subtree_list: Vec<(usize, usize, DataRoot)>, + ) -> Result<()> { + let mut batch = self + .db + .get_entry_batch(batch_index as u64)? + .unwrap_or_else(|| EntryBatch::new(batch_index as u64)); + batch.set_subtree_list(subtree_list); + self.db.put_entry_raw(vec![(batch_index as u64, batch)])?; + + Ok(()) + } + + pub fn gen_proof_in_batch(&self, batch_index: usize, sector_index: usize) -> Result { + let batch = self + .db + .get_entry_batch(batch_index as u64)? + .ok_or_else(|| anyhow!("batch missing, index={}", batch_index))?; + let merkle = batch.to_merkle_tree(batch_index == 0)?.ok_or_else(|| { + anyhow!( + "batch data incomplete for building a merkle tree, index={}", + batch_index + ) + })?; + merkle.gen_proof(sector_index) + } +} + +#[derive(Clone, Debug)] +pub struct FlowConfig { + pub batch_size: usize, +} + +impl Default for FlowConfig { + fn default() -> Self { + Self { + batch_size: SECTORS_PER_LOAD, + } + } +} + +impl FlowRead for FlowStore { + /// Return `Ok(None)` if only partial data are available. + fn get_entries(&self, index_start: u64, index_end: u64) -> Result> { + if index_end <= index_start { + bail!( + "invalid entry index: start={} end={}", + index_start, + index_end + ); + } + let mut data = Vec::with_capacity((index_end - index_start) as usize * BYTES_PER_SECTOR); + for (start_entry_index, end_entry_index) in + batch_iter(index_start, index_end, self.config.batch_size) + { + let chunk_index = start_entry_index / self.config.batch_size as u64; + let mut offset = start_entry_index - chunk_index * self.config.batch_size as u64; + let mut length = end_entry_index - start_entry_index; + + // Tempfix: for first chunk, its offset is always 1 + if chunk_index == 0 && offset == 0 { + offset = 1; + length -= 1; + } + + let entry_batch = try_option!(self.db.get_entry_batch(chunk_index)?); + let mut entry_batch_data = + try_option!(entry_batch.get_unsealed_data(offset as usize, length as usize)); + data.append(&mut entry_batch_data); + } + Ok(Some(ChunkArray { + data, + start_index: index_start, + })) + } + + fn get_available_entries(&self, index_start: u64, index_end: u64) -> Result> { + // Both `index_start` and `index_end` are at the batch boundaries, so we do not need + // to check if the data is within range when we process each batch. + if index_end <= index_start + || index_start % self.config.batch_size as u64 != 0 + || index_end % self.config.batch_size as u64 != 0 + { + bail!( + "invalid entry index: start={} end={}", + index_start, + index_end + ); + } + let mut entry_list = Vec::::new(); + for (start_entry_index, _) in batch_iter(index_start, index_end, self.config.batch_size) { + let chunk_index = start_entry_index / self.config.batch_size as u64; + + if let Some(mut data_list) = self + .db + .get_entry_batch(chunk_index)? + .map(|b| b.into_data_list(start_entry_index)) + { + if data_list.is_empty() { + continue; + } + // This will not happen for now because we only get entries for the last chunk. + if let Some(last) = entry_list.last_mut() { + if last.start_index + bytes_to_entries(last.data.len() as u64) + == data_list[0].start_index + { + // Merge the first element with the previous one. + last.data.append(&mut data_list.remove(0).data); + } + } + for data in data_list { + entry_list.push(data); + } + } + } + Ok(entry_list) + } + + /// Return the list of all stored chunk roots. + fn get_chunk_root_list(&self) -> Result> { + self.db.get_batch_root_list() + } + + fn load_sealed_data(&self, chunk_index: u64) -> Result> { + let batch = try_option!(self.db.get_entry_batch(chunk_index)?); + let mut mine_chunk = MineLoadChunk::default(); + for (seal_index, (sealed, validity)) in mine_chunk + .loaded_chunk + .iter_mut() + .zip(mine_chunk.avalibilities.iter_mut()) + .enumerate() + { + if let Some(data) = batch.get_sealed_data(seal_index as u16) { + *validity = true; + *sealed = data; + } + } + Ok(Some(mine_chunk)) + } +} + +impl FlowWrite for FlowStore { + /// Return the roots of completed chunks. The order is guaranteed to be increasing + /// by chunk index. + fn append_entries(&mut self, data: ChunkArray) -> Result> { + trace!("append_entries: {} {}", data.start_index, data.data.len()); + if data.data.len() % BYTES_PER_SECTOR != 0 { + bail!("append_entries: invalid data size, len={}", data.data.len()); + } + let mut batch_list = Vec::new(); + for (start_entry_index, end_entry_index) in batch_iter( + data.start_index, + data.start_index + bytes_to_entries(data.data.len() as u64), + self.config.batch_size, + ) { + // TODO: Avoid mem-copy if possible. + let chunk = data + .sub_array(start_entry_index, end_entry_index) + .expect("in range"); + + let chunk_index = chunk.start_index / self.config.batch_size as u64; + + // TODO: Try to avoid loading from db if possible. + let mut batch = self + .db + .get_entry_batch(chunk_index)? + .unwrap_or_else(|| EntryBatch::new(chunk_index)); + let completed_seals = batch.insert_data( + (chunk.start_index % self.config.batch_size as u64) as usize, + chunk.data, + )?; + completed_seals.into_iter().for_each(|x| { + self.to_seal_set.insert( + chunk_index as usize * SEALS_PER_LOAD + x as usize, + self.to_seal_version, + ); + }); + + batch_list.push((chunk_index, batch)); + } + self.db.put_entry_batch_list(batch_list) + } + + fn truncate(&mut self, start_index: u64) -> crate::error::Result<()> { + let to_reseal = self.db.truncate(start_index, self.config.batch_size)?; + + self.to_seal_set + .split_off(&(start_index as usize / SECTORS_PER_SEAL)); + self.to_seal_version += 1; + + to_reseal.into_iter().for_each(|x| { + self.to_seal_set.insert(x, self.to_seal_version); + }); + Ok(()) + } +} + +impl FlowSeal for FlowStore { + fn pull_seal_chunk(&self, seal_index_max: usize) -> Result>> { + let mut to_seal_iter = self.to_seal_set.iter(); + let (&first_index, &first_version) = try_option!(to_seal_iter.next()); + if first_index >= seal_index_max { + return Ok(None); + } + + let mut tasks = Vec::with_capacity(SEALS_PER_LOAD); + + let batch_data = self + .db + .get_entry_batch((first_index / SEALS_PER_LOAD) as u64)? + .expect("Lost data chunk in to_seal_set"); + + for (&seal_index, &version) in + std::iter::once((&first_index, &first_version)).chain(to_seal_iter.filter(|(&x, _)| { + first_index / SEALS_PER_LOAD == x / SEALS_PER_LOAD && x < seal_index_max + })) + { + let seal_index_local = seal_index % SEALS_PER_LOAD; + let non_sealed_data = batch_data + .get_non_sealed_data(seal_index_local as u16) + .expect("Lost seal chunk in to_seal_set"); + tasks.push(SealTask { + seal_index: seal_index as u64, + version, + non_sealed_data, + }) + } + + Ok(Some(tasks)) + } + + fn submit_seal_result(&mut self, answers: Vec) -> Result<()> { + let is_consistent = |answer: &SealAnswer| { + self.to_seal_set + .get(&(answer.seal_index as usize)) + .map_or(false, |cur_ver| cur_ver == &answer.version) + }; + + let mut updated_chunk = vec![]; + let mut removed_seal_index = Vec::new(); + for (load_index, answers_in_chunk) in &answers + .into_iter() + .filter(is_consistent) + .group_by(|answer| answer.seal_index / SEALS_PER_LOAD as u64) + { + let mut batch_chunk = self + .db + .get_entry_batch(load_index)? + .expect("Can not find chunk data"); + for answer in answers_in_chunk { + removed_seal_index.push(answer.seal_index as usize); + batch_chunk.submit_seal_result(answer)?; + } + updated_chunk.push((load_index, batch_chunk)); + } + + debug!("Seal chunks: indices = {:?}", removed_seal_index); + + for idx in removed_seal_index.into_iter() { + self.to_seal_set.remove(&idx); + } + + self.db.put_entry_raw(updated_chunk)?; + + Ok(()) + } +} + +pub struct FlowDBStore { + kvdb: Arc, +} + +impl FlowDBStore { + pub fn new(kvdb: Arc) -> Self { + Self { kvdb } + } + + fn put_entry_batch_list( + &self, + batch_list: Vec<(u64, EntryBatch)>, + ) -> Result> { + let mut completed_batches = Vec::new(); + let mut tx = self.kvdb.transaction(); + for (batch_index, batch) in batch_list { + tx.put( + COL_ENTRY_BATCH, + &batch_index.to_be_bytes(), + &batch.as_ssz_bytes(), + ); + if let Some(root) = batch.build_root(batch_index == 0)? { + trace!("complete batch: index={}", batch_index); + tx.put( + COL_ENTRY_BATCH_ROOT, + // (batch_index, subtree_depth) + &encode_batch_root_key(batch_index as usize, 1), + root.as_bytes(), + ); + completed_batches.push((batch_index, root)); + } + } + self.kvdb.write(tx)?; + Ok(completed_batches) + } + + fn put_entry_raw(&self, batch_list: Vec<(u64, EntryBatch)>) -> Result<()> { + let mut tx = self.kvdb.transaction(); + for (batch_index, batch) in batch_list { + tx.put( + COL_ENTRY_BATCH, + &batch_index.to_be_bytes(), + &batch.as_ssz_bytes(), + ); + } + self.kvdb.write(tx)?; + Ok(()) + } + + fn get_entry_batch(&self, batch_index: u64) -> Result> { + let raw = try_option!(self.kvdb.get(COL_ENTRY_BATCH, &batch_index.to_be_bytes())?); + Ok(Some(EntryBatch::from_ssz_bytes(&raw).map_err(Error::from)?)) + } + + fn put_batch_root_list(&self, root_map: BTreeMap) -> Result<()> { + let mut tx = self.kvdb.transaction(); + for (batch_index, (root, subtree_depth)) in root_map { + tx.put( + COL_ENTRY_BATCH_ROOT, + &encode_batch_root_key(batch_index, subtree_depth), + root.as_bytes(), + ); + } + Ok(self.kvdb.write(tx)?) + } + + fn get_batch_root_list(&self) -> Result> { + let mut range_root = None; + // A list of `BatchRoot` that can reconstruct the whole merkle tree structure. + let mut root_list = Vec::new(); + // A list of leaf `(index, root_hash)` in the subtrees of some nodes in `root_list`, + // and they will be updated in the merkle tree with `fill_leaf` by the caller. + let mut leaf_list = Vec::new(); + let mut expected_index = 0; + for r in self.kvdb.iter(COL_ENTRY_BATCH_ROOT) { + let (index_bytes, root_bytes) = r?; + let (batch_index, subtree_depth) = decode_batch_root_key(index_bytes.as_ref())?; + debug!( + "load root depth={}, index expected={} get={}", + subtree_depth, expected_index, batch_index + ); + let root = DataRoot::from_slice(root_bytes.as_ref()); + if subtree_depth == 1 { + if range_root.is_none() { + // This is expected to be the next leaf. + if batch_index == expected_index { + root_list.push((1, root)); + expected_index += 1; + } else { + bail!( + "unexpected chunk leaf, expected={}, get={}", + expected_index, + batch_index + ); + } + } else { + match batch_index.cmp(&expected_index) { + Ordering::Less => { + // This leaf is within a subtree whose root is known. + leaf_list.push((batch_index, root)); + } + Ordering::Equal => { + // A subtree range ends. + range_root = None; + root_list.push((1, root)); + expected_index += 1; + } + Ordering::Greater => { + bail!( + "unexpected chunk leaf in range, expected={}, get={}, range={:?}", + expected_index, + batch_index, + range_root, + ); + } + } + } + } else if expected_index == batch_index { + range_root = Some(BatchRoot::Multiple((subtree_depth, root))); + root_list.push((subtree_depth, root)); + expected_index += 1 << (subtree_depth - 1); + } else { + bail!( + "unexpected range root: expected={} get={}", + expected_index, + batch_index + ); + } + } + Ok(MerkleTreeInitialData { + subtree_list: root_list, + known_leaves: leaf_list, + }) + } + + fn truncate(&self, start_index: u64, batch_size: usize) -> crate::error::Result> { + let mut tx = self.kvdb.transaction(); + let mut start_batch_index = start_index / batch_size as u64; + let first_batch_offset = start_index as usize % batch_size; + let mut index_to_reseal = Vec::new(); + if first_batch_offset != 0 { + if let Some(mut first_batch) = self.get_entry_batch(start_batch_index)? { + index_to_reseal = first_batch + .truncate(first_batch_offset) + .into_iter() + .map(|x| start_batch_index as usize * SEALS_PER_LOAD + x as usize) + .collect(); + if !first_batch.is_empty() { + tx.put( + COL_ENTRY_BATCH, + &start_batch_index.to_be_bytes(), + &first_batch.as_ssz_bytes(), + ); + } else { + tx.delete(COL_ENTRY_BATCH, &start_batch_index.to_be_bytes()); + } + } + + start_batch_index += 1; + } + // TODO: `kvdb` and `kvdb-rocksdb` does not support `seek_to_last` yet. + // We'll need to fork it or use another wrapper for a better performance in this. + let end = match self.kvdb.iter(COL_ENTRY_BATCH).last() { + Some(Ok((k, _))) => decode_batch_index(k.as_ref())?, + Some(Err(e)) => { + error!("truncate db error: e={:?}", e); + return Err(e.into()); + } + None => { + // The db has no data, so we can just return; + return Ok(index_to_reseal); + } + }; + for batch_index in start_batch_index as usize..=end { + tx.delete(COL_ENTRY_BATCH, &batch_index.to_be_bytes()); + tx.delete_prefix(COL_ENTRY_BATCH_ROOT, &batch_index.to_be_bytes()); + } + self.kvdb.write(tx)?; + Ok(index_to_reseal) + } +} + +#[derive(DeriveEncode, DeriveDecode, Clone, Debug)] +#[ssz(enum_behaviour = "union")] +pub enum BatchRoot { + Single(DataRoot), + Multiple((usize, DataRoot)), +} + +/// Return the batch boundaries `(batch_start_index, batch_end_index)` given the index range. +pub fn batch_iter(start: u64, end: u64, batch_size: usize) -> Vec<(u64, u64)> { + let mut list = Vec::new(); + for i in (start / batch_size as u64 * batch_size as u64..end).step_by(batch_size) { + let batch_start = cmp::max(start, i); + let batch_end = cmp::min(end, i + batch_size as u64); + list.push((batch_start, batch_end)); + } + list +} + +fn try_decode_usize(data: &[u8]) -> Result { + Ok(usize::from_be_bytes( + data.try_into().map_err(|e| anyhow!("{:?}", e))?, + )) +} + +fn decode_batch_index(data: &[u8]) -> Result { + try_decode_usize(data) +} + +/// For the same batch_index, we want to process the larger subtree_depth first in iteration. +fn encode_batch_root_key(batch_index: usize, subtree_depth: usize) -> Vec { + let mut key = batch_index.to_be_bytes().to_vec(); + key.extend_from_slice(&(usize::MAX - subtree_depth).to_be_bytes()); + key +} + +fn decode_batch_root_key(data: &[u8]) -> Result<(usize, usize)> { + if data.len() != mem::size_of::() * 2 { + bail!("invalid data length"); + } + let batch_index = try_decode_usize(&data[..mem::size_of::()])?; + let subtree_depth = usize::MAX - try_decode_usize(&data[mem::size_of::()..])?; + Ok((batch_index, subtree_depth)) +} diff --git a/node/storage/src/log_store/load_chunk/bitmap.rs b/node/storage/src/log_store/load_chunk/bitmap.rs new file mode 100644 index 0000000..0269428 --- /dev/null +++ b/node/storage/src/log_store/load_chunk/bitmap.rs @@ -0,0 +1,175 @@ +use std::ops::{Deref, DerefMut}; + +use ssz::{Decode, DecodeError, Encode}; + +use bitmaps::{Bitmap, Bits, BitsImpl}; + +#[derive(Default, Debug)] +pub struct WrappedBitmap(pub Bitmap) +where + BitsImpl<{ N }>: Bits; +type PrimitveInner = as Bits>::Store; + +impl Encode for WrappedBitmap +where + BitsImpl<{ N }>: Bits, + PrimitveInner<{ N }>: Encode, +{ + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_append(&self, buf: &mut Vec) { + buf.append(&mut self.0.into_value().as_ssz_bytes()) + } + + fn ssz_bytes_len(&self) -> usize { + ::ssz_fixed_len() + } + + fn ssz_fixed_len() -> usize { + as Encode>::ssz_fixed_len() + } +} + +impl Decode for WrappedBitmap +where + BitsImpl<{ N }>: Bits, + PrimitveInner<{ N }>: Decode, +{ + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + as Decode>::ssz_fixed_len() + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + Ok(Self(Bitmap::<{ N }>::from_value( + PrimitveInner::from_ssz_bytes(bytes)?, + ))) + } +} + +impl Deref for WrappedBitmap +where + BitsImpl<{ N }>: Bits, +{ + type Target = Bitmap<{ N }>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for WrappedBitmap +where + BitsImpl<{ N }>: Bits, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +pub trait TruncateBitmap { + fn truncate(&mut self, index: u16); +} + +impl TruncateBitmap for u16 { + fn truncate(&mut self, index: u16) { + let mask: u16 = (1 << index) - 1; + *self &= mask + } +} + +impl TruncateBitmap for u32 { + fn truncate(&mut self, index: u16) { + let mask: u32 = (1 << index) - 1; + *self &= mask + } +} + +impl TruncateBitmap for u64 { + fn truncate(&mut self, index: u16) { + let mask: u64 = (1 << index) - 1; + *self &= mask + } +} + +impl TruncateBitmap for u128 { + fn truncate(&mut self, index: u16) { + let mask: u128 = (1 << index) - 1; + *self &= mask + } +} + +impl TruncateBitmap for [u128; N] { + fn truncate(&mut self, index: u16) { + let blob_index = index as usize / u128::BITS as usize; + let bit_index = index as usize % u128::BITS as usize; + let mask: u128 = (1 << (bit_index as u128)) - 1; + self[blob_index] &= mask; + for blob in &mut self[(blob_index + 1)..N] { + *blob = 0; + } + } +} + +impl WrappedBitmap +where + BitsImpl<{ N }>: Bits, + PrimitveInner<{ N }>: TruncateBitmap, +{ + /// Set the position large or equal to `index` to false + pub fn truncate(&mut self, index: u16) { + let mut current = *self.as_value(); + TruncateBitmap::truncate(&mut current, index); + self.0 = Bitmap::<{ N }>::from_value(current) + } +} + +#[test] +fn bitmap_serde() { + let mut bitmap = WrappedBitmap::<64>::default(); + bitmap.set(10, true); + bitmap.set(29, true); + + let serialized = bitmap.as_ssz_bytes(); + let deserialized = WrappedBitmap::<64>::from_ssz_bytes(&serialized).unwrap(); + assert_eq!(bitmap.into_value(), deserialized.into_value()); +} + +#[test] +fn bitmap_truncate() { + let mut bitmap = WrappedBitmap::<64>::default(); + bitmap.set(10, true); + bitmap.set(29, true); + bitmap.set(30, true); + bitmap.set(55, true); + + bitmap.truncate(30); + + assert!(bitmap.get(10)); + assert!(bitmap.get(29)); + assert!(!bitmap.get(30)); + assert!(!bitmap.get(55)); +} + +#[test] +fn bitmap_big_truncate() { + let mut bitmap = WrappedBitmap::<300>::default(); + bitmap.set(110, true); + bitmap.set(129, true); + bitmap.set(130, true); + bitmap.set(155, true); + bitmap.set(299, true); + + bitmap.truncate(130); + + assert!(bitmap.get(110)); + assert!(bitmap.get(129)); + assert!(!bitmap.get(130)); + assert!(!bitmap.get(155)); + assert!(!bitmap.get(299)); +} diff --git a/node/storage/src/log_store/load_chunk/chunk_data.rs b/node/storage/src/log_store/load_chunk/chunk_data.rs new file mode 100644 index 0000000..8131607 --- /dev/null +++ b/node/storage/src/log_store/load_chunk/chunk_data.rs @@ -0,0 +1,497 @@ +use anyhow::{bail, Result}; +use zgs_spec::{BYTES_PER_LOAD, BYTES_PER_SECTOR, SECTORS_PER_LOAD, SECTORS_PER_SEAL}; +use shared_types::{bytes_to_chunks, DataRoot}; +use ssz_derive::{Decode, Encode}; +use std::fmt::{Debug, Formatter}; +use std::mem; +use tracing::error; + +pub enum EntryBatchData { + Complete(Vec), + /// All `PartialBatch`s are ordered based on `start_index`. + Incomplete(IncompleteData), +} + +#[derive(Default, Debug, Encode, Decode)] +pub struct IncompleteData { + pub subtrees: Vec, + pub known_data: Vec, +} + +impl IncompleteData { + fn truncate(&mut self, sector_index: usize) { + let partial_batch_truncate = match PartialBatch::find(&self.known_data, sector_index) { + Ok(x) => { + let p = &mut self.known_data[x]; + let truncated_byte = (sector_index - p.start_sector) * BYTES_PER_SECTOR; + p.data.truncate(truncated_byte); + if p.data.is_empty() { + x + } else { + x + 1 + } + } + Err(x) => x, + }; + self.known_data.truncate(partial_batch_truncate); + + let subtree_truncate = match Subtree::find(&self.subtrees, sector_index) { + Ok(x) => x, + Err(x) => x, + }; + self.subtrees.truncate(subtree_truncate); + } + + /// This is only called once when the batch is removed from the memory and fully stored in db. + pub fn set_subtree_list(&mut self, subtree_list: Vec) { + self.subtrees = subtree_list; + } + + pub fn get(&self, mut start_byte: usize, length_byte: usize) -> Option<&[u8]> { + let p = &self.known_data + [PartialBatch::find(&self.known_data, start_byte / BYTES_PER_SECTOR).ok()?]; + // Rebase the start_byte and end_byte w.r.t. to hit partial batch. + start_byte -= p.start_sector * BYTES_PER_SECTOR; + p.data.get(start_byte..(start_byte + length_byte)) + } +} + +#[derive(Default, Debug, Encode, Decode)] +pub struct Subtree { + pub start_sector: usize, + pub subtree_height: usize, + pub root: DataRoot, +} + +#[derive(PartialEq, Eq)] +pub struct PartialBatch { + /// Offset in this batch. + pub(super) start_sector: usize, + pub(super) data: Vec, +} + +impl Debug for PartialBatch { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "PartialBatch: start_offset={} data_len={}", + self.start_sector, + self.data.len() + ) + } +} + +trait DataRange: Sized { + fn start_sector(&self) -> usize; + fn size(&self) -> usize; + + fn end_sector(&self) -> usize { + self.start_sector() + self.size() + } + + fn find(data_list: &[Self], sector_index: usize) -> Result { + let possible_index = + match data_list.binary_search_by_key(§or_index, |x| x.start_sector()) { + Ok(x) => x, + Err(0) => { + return Err(0); + } + Err(x) => x - 1, + }; + if sector_index < data_list[possible_index].end_sector() { + Ok(possible_index) + } else { + Err(possible_index + 1) + } + } +} + +impl DataRange for PartialBatch { + fn start_sector(&self) -> usize { + self.start_sector + } + + fn size(&self) -> usize { + bytes_to_chunks(self.data.len()) + } +} + +impl DataRange for Subtree { + fn start_sector(&self) -> usize { + self.start_sector + } + + fn size(&self) -> usize { + 1 << (self.subtree_height - 1) + } +} + +impl EntryBatchData { + pub fn new() -> Self { + EntryBatchData::Incomplete(IncompleteData { + subtrees: vec![], + known_data: vec![], + }) + } + + pub fn is_empty(&self) -> bool { + matches!(self,EntryBatchData::Incomplete(x) if x.known_data.is_empty()) + } + + pub fn get(&self, start_byte: usize, length_byte: usize) -> Option<&[u8]> { + assert!(start_byte + length_byte <= BYTES_PER_LOAD); + + match self { + EntryBatchData::Complete(data) => data.get(start_byte..(start_byte + length_byte)), + EntryBatchData::Incomplete(data) => data.get(start_byte, length_byte), + } + } + + pub fn get_mut(&mut self, mut start_byte: usize, length_byte: usize) -> Option<&mut [u8]> { + assert!(start_byte + length_byte <= BYTES_PER_LOAD); + + match self { + EntryBatchData::Complete(data) => data.get_mut(start_byte..(start_byte + length_byte)), + EntryBatchData::Incomplete(data) => { + let index = + PartialBatch::find(&data.known_data, start_byte / BYTES_PER_SECTOR).ok()?; + let p = &mut data.known_data[index]; + + // Rebase the start_byte and end_byte w.r.t. to hit partial batch. + start_byte -= p.start_sector * BYTES_PER_SECTOR; + + p.data.get_mut(start_byte..(start_byte + length_byte)) + } + } + } + + pub fn truncate(&mut self, truncated_byte: usize) { + assert!(truncated_byte % BYTES_PER_SECTOR == 0); + *self = match self { + EntryBatchData::Complete(data) => { + data.truncate(truncated_byte); + let known_data = vec![PartialBatch { + start_sector: 0, + data: std::mem::take(data), + }]; + EntryBatchData::Incomplete(IncompleteData { + subtrees: vec![], + known_data, + }) + } + EntryBatchData::Incomplete(batch_list) => { + batch_list.truncate(truncated_byte / BYTES_PER_SECTOR); + EntryBatchData::Incomplete(std::mem::take(batch_list)) + } + }; + } + + pub fn insert_data(&mut self, start_byte: usize, mut data: Vec) -> Result> { + assert!(start_byte % BYTES_PER_SECTOR == 0); + assert!(data.len() % BYTES_PER_SECTOR == 0); + + if data.is_empty() || self.get(start_byte, data.len()) == Some(&data) { + // TODO(zz): This assumes the caller has processed chain reorg (truncate flow) before + // inserting new data, and the data of the same file are always inserted with the + // same pattern. + return Ok(vec![]); + } + + // Check if the entry is completed + let (list, subtree_list) = if let EntryBatchData::Incomplete(x) = self { + (&mut x.known_data, &mut x.subtrees) + } else { + bail!( + "overwriting a completed PoRA Chunk with conflict data, start_byte={}", + start_byte + ); + }; + + let start_sector = start_byte / BYTES_PER_SECTOR; + let end_sector = start_sector + data.len() / BYTES_PER_SECTOR; + let length_sector = data.len() / BYTES_PER_SECTOR; + + // Check if the entry is completed + let start_insert_position = match PartialBatch::find(list, start_sector) { + Ok(x) => { + bail!( + "start position overlapped with existing batch: start {}, len {}", + list[x].start_sector, + list[x].data.len() + ); + } + Err(x) => x, + }; + + let end_insert_position = match PartialBatch::find(list, end_sector - 1) { + Ok(x) => { + bail!( + "end position overlapped with existing batch: start {}, len {}", + list[x].start_sector, + list[x].data.len() + ); + } + Err(x) => x, + }; + + let position = if start_insert_position != end_insert_position { + bail!("data overlapped with existing batches"); + } else { + start_insert_position + }; + + let merge_prev = position != 0 && start_sector == list[position - 1].end_sector(); + let merge_next = position != list.len() && end_sector == list[position].start_sector; + + let updated_segment = match (merge_prev, merge_next) { + (false, false) => { + list.insert(position, PartialBatch { start_sector, data }); + &list[position] + } + (true, false) => { + list[position - 1].data.append(&mut data); + &list[position - 1] + } + (false, true) => { + data.append(&mut list[position].data); + list[position] = PartialBatch { start_sector, data }; + &list[position] + } + (true, true) => { + // Merge the new data with the two around partial batches to + // a single one. + list[position - 1].data.append(&mut data); + let mut next = list.remove(position); + list[position - 1].data.append(&mut next.data); + &list[position - 1] + } + }; + + // Remove completed subtrees. + let updated_end_sector = + updated_segment.start_sector + updated_segment.data.len() / BYTES_PER_SECTOR; + let start_subtree_index = match subtree_list + .binary_search_by_key(&updated_segment.start_sector, |tree| tree.start_sector) + { + Ok(x) => x, + Err(x) => x, + }; + for subtree_index in start_subtree_index..subtree_list.len() { + assert!(subtree_list[subtree_index].start_sector >= updated_segment.start_sector); + if subtree_list[subtree_index].end_sector() > updated_end_sector { + subtree_list.drain(start_subtree_index..subtree_index); + break; + } + } + + // Find which seal chunks are made intact by this submission. + // It will be notified to the sealer later. + let intact_seal_idxs = get_intact_sealing_index( + updated_segment.start_sector, + updated_segment.data.len() / BYTES_PER_SECTOR, + ); + + // TODO(zz): Use config here? + if list.len() == 1 + && list[0].start_sector == 0 + && bytes_to_chunks(list[0].data.len()) == SECTORS_PER_LOAD + { + // All data in this batch have been filled. + *self = EntryBatchData::Complete(mem::take(&mut list[0].data)); + } + + let ready_for_seal_idxs: Vec = get_covered_sealing_index(start_sector, length_sector) + .filter(|x| intact_seal_idxs.contains(x)) + .collect(); + + Ok(ready_for_seal_idxs) + } + + pub(super) fn available_range_entries(&self) -> Vec<(usize, usize)> { + match self { + EntryBatchData::Complete(data) => { + vec![(0, data.len() / BYTES_PER_SECTOR)] + } + EntryBatchData::Incomplete(batch_list) => batch_list + .known_data + .iter() + .map(|b| (b.start_sector, b.data.len() / BYTES_PER_SECTOR)) + .collect(), + } + } + + /// This is only called once when the batch is removed from the memory and fully stored in db. + pub fn set_subtree_list(&mut self, subtree_list: Vec<(usize, usize, DataRoot)>) { + let subtree_list: Vec = subtree_list + .into_iter() + .filter(|(start_sector, subtree_height, root)| { + self.get(*start_sector * BYTES_PER_SECTOR, (1 << (*subtree_height - 1)) * BYTES_PER_SECTOR) + .is_none() + // The first sector is regarded as known. + && *root != DataRoot::zero() + }) + .map(|(start_sector, subtree_height, root)| Subtree { + start_sector, + subtree_height, + root, + }) + .collect(); + match self { + EntryBatchData::Complete(_) => { + // This should not happen if the data in memory matches the data in DB. + if !subtree_list.is_empty() { + error!(?subtree_list, "store subtrees for a completed chunk"); + } + } + EntryBatchData::Incomplete(data) => data.set_subtree_list(subtree_list), + } + } + + pub fn get_subtree_list(&self) -> &[Subtree] { + match self { + EntryBatchData::Complete(_) => &[], + EntryBatchData::Incomplete(data) => &data.subtrees, + } + } +} + +fn get_intact_sealing_index(start_sector: usize, length_sector: usize) -> std::ops::Range { + // Inclusive + let start_index = ((start_sector + SECTORS_PER_SEAL - 1) / SECTORS_PER_SEAL) as u16; + // Exclusive + let end_index = ((start_sector + length_sector) / SECTORS_PER_SEAL) as u16; + start_index..end_index +} + +fn get_covered_sealing_index(start_sector: usize, length_sector: usize) -> std::ops::Range { + // Inclusive + let start_index = (start_sector / SECTORS_PER_SEAL) as u16; + // Exclusive + let end_index = + ((start_sector + length_sector + SECTORS_PER_SEAL - 1) / SECTORS_PER_SEAL) as u16; + start_index..end_index +} + +#[cfg(test)] +mod tests { + use crate::log_store::load_chunk::chunk_data::PartialBatch; + + use super::EntryBatchData; + use zgs_spec::{BYTES_PER_LOAD, BYTES_PER_SECTOR, SECTORS_PER_LOAD}; + use rand::{rngs::StdRng, RngCore, SeedableRng}; + + fn test_data() -> Vec { + let mut data = vec![0u8; BYTES_PER_LOAD]; + let mut random = StdRng::seed_from_u64(73); + random.fill_bytes(&mut data); + data + } + + #[test] + fn test_data_chunk_insert() { + let data = test_data(); + let mut chunk_batch = EntryBatchData::new(); + + for i in [2usize, 0, 1, 3].into_iter() { + chunk_batch + .insert_data( + BYTES_PER_LOAD / 4 * i, + data[(BYTES_PER_LOAD / 4) * i..(BYTES_PER_LOAD / 4) * (i + 1)].to_vec(), + ) + .unwrap(); + } + + assert!(matches!(chunk_batch, EntryBatchData::Complete(_))); + } + + #[test] + fn test_data_chunk_truncate() { + let data = test_data(); + let mut chunk_batch = EntryBatchData::new(); + + for i in [3, 1].into_iter() { + chunk_batch + .insert_data( + BYTES_PER_LOAD / 4 * i, + data[(BYTES_PER_LOAD / 4) * i..(BYTES_PER_LOAD / 4) * (i + 1)].to_vec(), + ) + .unwrap(); + } + + chunk_batch.truncate(BYTES_PER_LOAD / 4 * 3 + BYTES_PER_SECTOR); + + let chunks = if let EntryBatchData::Incomplete(chunks) = chunk_batch { + chunks + } else { + unreachable!(); + }; + + assert!(chunks.known_data.len() == 2); + assert!( + chunks.known_data[0] + == PartialBatch { + start_sector: SECTORS_PER_LOAD / 4, + data: data[(BYTES_PER_LOAD / 4)..(BYTES_PER_LOAD / 4) * 2].to_vec() + } + ); + assert!( + chunks.known_data[1] + == PartialBatch { + start_sector: SECTORS_PER_LOAD / 4 * 3, + data: data[BYTES_PER_LOAD / 4 * 3..BYTES_PER_LOAD / 4 * 3 + BYTES_PER_SECTOR] + .to_vec() + } + ); + } + + #[test] + fn test_data_chunk_get_slice() { + let data = test_data(); + let mut chunk_batch = EntryBatchData::new(); + + const N: usize = BYTES_PER_LOAD; + const B: usize = N / 16; + + // Skip batch 5,7,10,11 + for i in [3, 8, 12, 15, 6, 1, 4, 13, 0, 2, 9, 14].into_iter() { + chunk_batch + .insert_data(B * i, data[B * i..B * (i + 1)].to_vec()) + .unwrap(); + assert_eq!( + chunk_batch.get(B * i, B).unwrap(), + &data[B * i..B * (i + 1)] + ); + assert_eq!( + chunk_batch.get_mut(B * i, B).unwrap(), + &data[B * i..B * (i + 1)] + ); + } + + const S: usize = B / BYTES_PER_SECTOR; + assert_eq!( + chunk_batch.available_range_entries(), + vec![(0, 5 * S), (6 * S, S), (8 * S, 2 * S), (12 * S, 4 * S)] + ); + + assert_eq!(chunk_batch.get(B * 8, B * 2).unwrap(), &data[B * 8..B * 10]); + assert_eq!( + chunk_batch.get_mut(B * 8, B * 2).unwrap(), + &data[B * 8..B * 10] + ); + + assert_eq!(chunk_batch.get(0, B * 4).unwrap(), &data[0..B * 4]); + assert_eq!(chunk_batch.get_mut(0, B * 4).unwrap(), &data[0..B * 4]); + + assert!(chunk_batch.get(0, B * 5 + 32).is_none()); + assert!(chunk_batch.get_mut(0, B * 5 + 32).is_none()); + + assert!(chunk_batch.get(B * 7 - 32, B + 32).is_none()); + assert!(chunk_batch.get_mut(B * 7 - 32, B + 32).is_none()); + + assert!(chunk_batch.get(B * 7, B + 32).is_none()); + assert!(chunk_batch.get_mut(B * 7, B + 32).is_none()); + + assert!(chunk_batch.get(B * 12 - 32, B + 32).is_none()); + assert!(chunk_batch.get_mut(B * 12 - 32, B + 32).is_none()); + } +} diff --git a/node/storage/src/log_store/load_chunk/mod.rs b/node/storage/src/log_store/load_chunk/mod.rs new file mode 100644 index 0000000..7bc240f --- /dev/null +++ b/node/storage/src/log_store/load_chunk/mod.rs @@ -0,0 +1,425 @@ +mod bitmap; +mod chunk_data; +mod seal; +mod serde; + +use std::cmp::min; + +use anyhow::Result; +use ethereum_types::H256; +use ssz_derive::{Decode, Encode}; + +use crate::log_store::log_manager::data_to_merkle_leaves; +use crate::try_option; +use append_merkle::{Algorithm, MerkleTreeRead, Sha3Algorithm}; +use zgs_spec::{ + BYTES_PER_LOAD, BYTES_PER_SEAL, BYTES_PER_SECTOR, SEALS_PER_LOAD, SECTORS_PER_LOAD, + SECTORS_PER_SEAL, +}; +use shared_types::{ChunkArray, DataRoot, Merkle}; +use tracing::trace; + +use super::SealAnswer; +use chunk_data::EntryBatchData; +use seal::SealInfo; + +#[derive(Encode, Decode)] +pub struct EntryBatch { + seal: SealInfo, + // the inner data + data: EntryBatchData, +} + +impl EntryBatch { + pub fn new(load_index_global: u64) -> Self { + Self { + seal: SealInfo::new(load_index_global), + data: EntryBatchData::new(), + } + } + + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } +} + +impl EntryBatch { + pub fn get_sealed_data(&self, seal_index: u16) -> Option<[u8; BYTES_PER_SEAL]> { + if self.seal.is_sealed(seal_index) { + let loaded_slice = self + .data + .get(seal_index as usize * BYTES_PER_SEAL, BYTES_PER_SEAL)?; + Some(loaded_slice.try_into().unwrap()) + } else { + None + } + } + + pub fn get_non_sealed_data(&self, seal_index: u16) -> Option<[u8; BYTES_PER_SEAL]> { + if !self.seal.is_sealed(seal_index) { + let loaded_slice = self + .data + .get(seal_index as usize * BYTES_PER_SEAL, BYTES_PER_SEAL)?; + Some(loaded_slice.try_into().unwrap()) + } else { + None + } + } + + /// Get unsealed data + pub fn get_unsealed_data(&self, start_sector: usize, length_sector: usize) -> Option> { + // If the start position is not aligned and is sealed, we need to load one more word (32 bytes) for unsealing + let advanced_by_one = if start_sector % SECTORS_PER_SEAL == 0 { + // If the start position is not aligned, it is no need to load one more word + false + } else { + // otherwise, it depends on if the given offset is seal + self.seal + .is_sealed((start_sector / SECTORS_PER_SEAL) as u16) + }; + + let start_byte = start_sector * BYTES_PER_SECTOR; + let length_byte = length_sector * BYTES_PER_SECTOR; + + // Load data slice with the word for unsealing + let (mut loaded_data, unseal_mask_seed) = if advanced_by_one { + let loaded_data_with_hint = self.data.get(start_byte - 32, length_byte + 32)?; + + // TODO (api stable): use `split_array_ref` instead when this api is stable. + let (unseal_mask_seed, loaded_data) = loaded_data_with_hint.split_at(32); + let unseal_mask_seed = <[u8; 32]>::try_from(unseal_mask_seed).unwrap(); + (loaded_data.to_vec(), Some(unseal_mask_seed)) + } else { + (self.data.get(start_byte, length_byte)?.to_vec(), None) + }; + + let incomplete_seal_chunk_length = (BYTES_PER_LOAD - start_byte) % BYTES_PER_SEAL; + + // Unseal the first incomplete sealing chunk (if exists) + if let Some(unseal_mask_seed) = unseal_mask_seed { + let data_to_unseal = if loaded_data.len() < incomplete_seal_chunk_length { + // The loaded data does not cross sealings + loaded_data.as_mut() + } else { + loaded_data[..incomplete_seal_chunk_length].as_mut() + }; + + zgs_seal::unseal_with_mask_seed(data_to_unseal, unseal_mask_seed); + } + + if loaded_data.len() > incomplete_seal_chunk_length { + let complete_chunks = &mut loaded_data[incomplete_seal_chunk_length..]; + let start_seal = (start_byte + incomplete_seal_chunk_length) / BYTES_PER_SEAL; + + for (seal_index, data_to_unseal) in complete_chunks + .chunks_mut(BYTES_PER_SEAL) + .enumerate() + .map(|(idx, chunk)| (start_seal + idx, chunk)) + { + self.seal.unseal(data_to_unseal, seal_index as u16); + } + } + + Some(loaded_data) + } + + /// Return `Error` if the new data overlaps with old data. + /// Convert `Incomplete` to `Completed` if the chunk is completed after the insertion. + pub fn insert_data(&mut self, offset: usize, data: Vec) -> Result> { + self.data.insert_data(offset * BYTES_PER_SECTOR, data) + } + + pub fn truncate(&mut self, truncated_sector: usize) -> Vec { + assert!(truncated_sector > 0 && truncated_sector < SECTORS_PER_LOAD); + + self.data.truncate(truncated_sector * BYTES_PER_SECTOR); + self.truncate_seal(truncated_sector) + } + + pub fn into_data_list(self, global_start_entry: u64) -> Vec { + self.data + .available_range_entries() + .into_iter() + .map(|(start_entry, length_entry)| ChunkArray { + data: self + .get_unsealed_data(start_entry, length_entry) + .unwrap() + .to_vec(), + start_index: global_start_entry + start_entry as u64, + }) + .collect() + } + + fn truncate_seal(&mut self, truncated_sector: usize) -> Vec { + let reverted_seal_index = (truncated_sector / SECTORS_PER_SEAL) as u16; + + let first_unseal_index = self.seal.truncated_seal_index(reverted_seal_index); + let last_unseal_index = ((truncated_sector - 1) / SECTORS_PER_SEAL) as u16; + + let mut to_reseal_set = Vec::with_capacity(SEALS_PER_LOAD); + + for unseal_index in first_unseal_index..=last_unseal_index { + if !self.seal.is_sealed(unseal_index) { + continue; + } + + let truncated_byte = truncated_sector * BYTES_PER_SECTOR; + let first_unseal_byte = unseal_index as usize * BYTES_PER_SEAL; + let length = min(truncated_byte - first_unseal_byte, BYTES_PER_SEAL); + let to_unseal = self + .data + .get_mut(first_unseal_byte, length) + .expect("Sealed chunk should be complete"); + self.seal.unseal(to_unseal, unseal_index); + + to_reseal_set.push(unseal_index) + } + + // truncate the bitmap + self.seal.truncate(reverted_seal_index); + + to_reseal_set + } + + pub fn build_root(&self, is_first_chunk: bool) -> Result> { + // Fast check if an incomplete chunk is a full chunk. + if let EntryBatchData::Incomplete(d) = &self.data { + if self.get_unsealed_data(SECTORS_PER_LOAD - 1, 1).is_none() { + if let Some(last_subtree) = d.subtrees.last() { + if last_subtree.start_sector + (1 << (last_subtree.subtree_height - 1)) + != SECTORS_PER_LOAD + { + return Ok(None); + } + } else { + return Ok(None); + } + } + } + Ok(Some( + *try_option!(self.to_merkle_tree(is_first_chunk)?).root(), + )) + } + + pub fn submit_seal_result(&mut self, answer: SealAnswer) -> Result<()> { + let local_seal_index = answer.seal_index as usize % SEALS_PER_LOAD; + assert!( + !self.seal.is_sealed(local_seal_index as u16), + "Duplicated sealing" + ); + assert_eq!( + answer.seal_index / SEALS_PER_LOAD as u64, + self.seal.load_index() + ); + + self.seal.set_seal_context( + answer.seal_context, + answer.context_end_seal, + answer.miner_id, + ); + let sealing_segment = self + .data + .get_mut(local_seal_index * BYTES_PER_SEAL, BYTES_PER_SEAL) + .expect("Sealing segment should exist"); + + sealing_segment.copy_from_slice(&answer.sealed_data); + self.seal.mark_sealed(local_seal_index as u16); + + Ok(()) + } + + /// This is only called once when the batch is removed from the memory and fully stored in db. + pub fn set_subtree_list(&mut self, subtree_list: Vec<(usize, usize, DataRoot)>) { + self.data.set_subtree_list(subtree_list) + } + + pub fn to_merkle_tree(&self, is_first_chunk: bool) -> Result> { + let initial_leaves = if is_first_chunk { + vec![H256::zero()] + } else { + vec![] + }; + let mut merkle = Merkle::new(initial_leaves, 0, None); + for subtree in self.data.get_subtree_list() { + trace!(?subtree, "get subtree, leaves={}", merkle.leaves()); + if subtree.start_sector != merkle.leaves() { + let leaf_data = try_option!( + self.get_unsealed_data(merkle.leaves(), subtree.start_sector - merkle.leaves()) + ); + merkle.append_list(data_to_merkle_leaves(&leaf_data).expect("aligned")); + } + merkle.append_subtree(subtree.subtree_height, subtree.root)?; + } + if merkle.leaves() != SECTORS_PER_LOAD { + let leaf_data = try_option!( + self.get_unsealed_data(merkle.leaves(), SECTORS_PER_LOAD - merkle.leaves()) + ); + merkle.append_list(data_to_merkle_leaves(&leaf_data).expect("aligned")); + } + // TODO(zz): Optimize. + for index in 0..merkle.leaves() { + if merkle.leaf_at(index)?.is_none() { + if let Some(leaf_data) = self.get_unsealed_data(index, 1) { + merkle.fill_leaf(index, Sha3Algorithm::leaf(&leaf_data)); + } + } + } + Ok(Some(merkle)) + } +} + +#[cfg(test)] +mod tests { + use super::{EntryBatch, SealAnswer}; + use ethereum_types::H256; + use zgs_spec::{ + BYTES_PER_SEAL, BYTES_PER_SECTOR, SEALS_PER_LOAD, SECTORS_PER_LOAD, SECTORS_PER_SEAL, + }; + const LOAD_INDEX: u64 = 1; + fn seal( + batch: &mut EntryBatch, + seal_index: u16, + context_digest: H256, + context_end_seal_local: u64, + ) { + let miner_id = H256([33u8; 32]); + let mut data = batch.get_non_sealed_data(seal_index).unwrap(); + zgs_seal::seal( + &mut data, + &miner_id, + &context_digest, + LOAD_INDEX * SECTORS_PER_LOAD as u64 + seal_index as u64 * SECTORS_PER_SEAL as u64, + ); + batch + .submit_seal_result(SealAnswer { + seal_index: LOAD_INDEX * SEALS_PER_LOAD as u64 + seal_index as u64, + version: 0, + sealed_data: data, + miner_id, + seal_context: context_digest, + context_end_seal: LOAD_INDEX * SEALS_PER_LOAD as u64 + context_end_seal_local, + }) + .unwrap(); + } + + #[test] + fn test_seal_single() { + let mut batch = EntryBatch::new(LOAD_INDEX); + batch.insert_data(0, vec![11; BYTES_PER_SEAL]).unwrap(); + + const DIGEST: H256 = H256([22u8; 32]); + seal(&mut batch, 0, DIGEST, 1); + + assert_eq!( + batch.get_unsealed_data(0, SECTORS_PER_SEAL).unwrap(), + vec![11; SECTORS_PER_SEAL * BYTES_PER_SECTOR] + ); + assert_eq!( + batch.get_unsealed_data(1, SECTORS_PER_SEAL - 1).unwrap(), + vec![11; (SECTORS_PER_SEAL - 1) * BYTES_PER_SECTOR] + ); + } + + fn check_two_seals(batch: &EntryBatch) { + assert_eq!( + batch.get_unsealed_data(0, SECTORS_PER_SEAL).unwrap(), + vec![11; SECTORS_PER_SEAL * BYTES_PER_SECTOR] + ); + assert_eq!( + batch + .get_unsealed_data(SECTORS_PER_SEAL, SECTORS_PER_SEAL) + .unwrap(), + vec![11; SECTORS_PER_SEAL * BYTES_PER_SECTOR] + ); + assert_eq!( + batch.get_unsealed_data(1, SECTORS_PER_SEAL - 1).unwrap(), + vec![11; (SECTORS_PER_SEAL - 1) * BYTES_PER_SECTOR] + ); + assert_eq!( + batch.get_unsealed_data(1, SECTORS_PER_SEAL).unwrap(), + vec![11; SECTORS_PER_SEAL * BYTES_PER_SECTOR] + ); + assert_eq!( + batch + .get_unsealed_data(1, 2 * SECTORS_PER_SEAL - 1) + .unwrap(), + vec![11; (2 * SECTORS_PER_SEAL - 1) * BYTES_PER_SECTOR] + ); + } + + #[test] + fn test_seal_mono_context() { + let mut batch = EntryBatch::new(LOAD_INDEX); + batch.insert_data(0, vec![11; BYTES_PER_SEAL * 2]).unwrap(); + + const DIGEST: H256 = H256([22u8; 32]); + seal(&mut batch, 0, DIGEST, 2); + seal(&mut batch, 1, DIGEST, 2); + + check_two_seals(&batch); + } + + #[test] + fn test_seal_mono_context_reorder() { + let mut batch = EntryBatch::new(LOAD_INDEX); + batch.insert_data(0, vec![11; BYTES_PER_SEAL * 2]).unwrap(); + + const DIGEST: H256 = H256([22u8; 32]); + seal(&mut batch, 1, DIGEST, 2); + seal(&mut batch, 0, DIGEST, 2); + + check_two_seals(&batch); + } + + #[test] + fn test_seal_mono_context_partial() { + let mut batch = EntryBatch::new(LOAD_INDEX); + batch.insert_data(0, vec![11; BYTES_PER_SEAL * 2]).unwrap(); + + const DIGEST: H256 = H256([22u8; 32]); + seal(&mut batch, 1, DIGEST, 2); + + check_two_seals(&batch); + } + + #[test] + fn test_seal_hete_context() { + let mut batch = EntryBatch::new(LOAD_INDEX); + batch.insert_data(0, vec![11; BYTES_PER_SEAL * 2]).unwrap(); + + const DIGEST0: H256 = H256([22u8; 32]); + const DIGEST1: H256 = H256([33u8; 32]); + + seal(&mut batch, 0, DIGEST0, 1); + seal(&mut batch, 1, DIGEST1, 2); + + check_two_seals(&batch); + } + + #[test] + fn test_seal_hete_context_reord() { + let mut batch = EntryBatch::new(LOAD_INDEX); + batch.insert_data(0, vec![11; BYTES_PER_SEAL * 2]).unwrap(); + + const DIGEST0: H256 = H256([22u8; 32]); + const DIGEST1: H256 = H256([33u8; 32]); + + seal(&mut batch, 1, DIGEST1, 2); + seal(&mut batch, 0, DIGEST0, 1); + + check_two_seals(&batch); + } + + #[test] + fn test_seal_hete_context_partial() { + let mut batch = EntryBatch::new(LOAD_INDEX); + batch.insert_data(0, vec![11; BYTES_PER_SEAL * 2]).unwrap(); + + // const DIGEST0: H256 = H256([22u8; 32]); + const DIGEST1: H256 = H256([33u8; 32]); + + seal(&mut batch, 1, DIGEST1, 2); + + check_two_seals(&batch); + } +} diff --git a/node/storage/src/log_store/load_chunk/seal.rs b/node/storage/src/log_store/load_chunk/seal.rs new file mode 100644 index 0000000..f2c37d2 --- /dev/null +++ b/node/storage/src/log_store/load_chunk/seal.rs @@ -0,0 +1,303 @@ +use ethereum_types::H256; +use ssz_derive::{Decode as DeriveDecode, Encode as DeriveEncode}; +use static_assertions::const_assert; + +use zgs_seal; +use zgs_spec::{SEALS_PER_LOAD, SECTORS_PER_LOAD, SECTORS_PER_SEAL}; + +use super::bitmap::WrappedBitmap; + +#[derive(DeriveEncode, DeriveDecode)] +pub struct SealContextInfo { + /// The context digest for this seal group + context_digest: H256, + /// The end position (exclusive) indexed by sectors + end_seal_index: u16, +} + +type ChunkSealBitmap = WrappedBitmap; +const_assert!(SEALS_PER_LOAD <= u128::BITS as usize); + +#[derive(Default, DeriveEncode, DeriveDecode)] +pub struct SealInfo { + // a bitmap specify which sealing chunks have been sealed + bitmap: ChunkSealBitmap, + // the batch_offset (seal chunks) of the EntryBatch this seal info belongs to + load_index: u64, + // the miner Id for sealing this chunk, zero representing doesn't exists + miner_id: H256, + // seal context information, indexed by u16. Get a position has never been set is undefined behaviour. + seal_contexts: Vec, +} + +// Basic interfaces +impl SealInfo { + pub fn new(load_index: u64) -> Self { + Self { + load_index, + ..Default::default() + } + } + + pub fn is_sealed(&self, seal_index: u16) -> bool { + self.bitmap.get(seal_index as usize) + } + + pub fn mark_sealed(&mut self, seal_index: u16) { + self.bitmap.set(seal_index as usize, true); + } + + pub fn load_index(&self) -> u64 { + self.load_index + } + + pub fn global_seal_sector(&self, index: u16) -> u64 { + (self.load_index as usize * SECTORS_PER_LOAD + index as usize * SECTORS_PER_SEAL) as u64 + } +} + +// Interfaces for maintaining context info +impl SealInfo { + fn context_index(&self, seal_index: u16) -> usize { + match self + .seal_contexts + .binary_search_by_key(&(seal_index + 1), |x| x.end_seal_index) + { + Ok(x) | Err(x) => x, + } + } + + pub fn get_seal_context_digest(&self, seal_index: u16) -> Option { + self.seal_contexts + .get(self.context_index(seal_index)) + .map(|x| x.context_digest) + } + + pub fn set_seal_context( + &mut self, + context_digest: H256, + global_end_seal_index: u64, + miner_id: H256, + ) { + // 1. Check consistency of the miner id. + if self.miner_id.is_zero() { + self.miner_id = miner_id; + } else { + assert!( + self.miner_id == miner_id, + "miner_id setting is inconsistent with db" + ); + } + + // 2. Compute the local end_seal_index + let end_seal_index = global_end_seal_index - self.load_index * SEALS_PER_LOAD as u64; + let end_seal_index = std::cmp::min(end_seal_index as u16, SEALS_PER_LOAD as u16); + let new_context = SealContextInfo { + context_digest, + end_seal_index, + }; + + // 3. Update the seal context array by cases + let insert_position = self.context_index(end_seal_index - 1); + + if let Some(existing_context) = self.seal_contexts.get(insert_position) { + if existing_context.context_digest == new_context.context_digest { + // Case 1: the new context is consistent with existing contexts (nothing to do) + } else { + // Case 2: the new context should be inserted in the middle (may not happen) + self.seal_contexts.insert(insert_position, new_context); + } + } else { + // Case 3: the new context exceeds the upper bound of existing contexts + self.seal_contexts.push(new_context); + } + } +} + +impl SealInfo { + pub fn truncate(&mut self, reverted_seal_index: u16) { + // TODO (kevin): have issue in some cases + let truncated_context_index = self.context_index(reverted_seal_index); + let truncated_seal_index = self.truncated_seal_index(reverted_seal_index); + + self.bitmap.truncate(truncated_seal_index); + self.seal_contexts.truncate(truncated_context_index); + } + + pub fn truncated_seal_index(&self, reverted_seal_index: u16) -> u16 { + let truncated_context = self.context_index(reverted_seal_index); + if truncated_context == 0 { + 0 + } else { + self.seal_contexts + .get(truncated_context - 1) + .unwrap() + .end_seal_index + } + } +} + +impl SealInfo { + pub fn unseal(&self, data: &mut [u8], index: u16) { + if !self.is_sealed(index) { + return; + } + let seal_context = self + .get_seal_context_digest(index) + .expect("cannot unseal non-sealed data"); + zgs_seal::unseal( + data, + &self.miner_id, + &seal_context, + self.global_seal_sector(index), + ); + } + + #[cfg(test)] + pub fn seal(&self, data: &mut [u8], index: u16) { + if self.is_sealed(index) { + return; + } + let seal_context = self + .get_seal_context_digest(index) + .expect("cannot unseal non-sealed data"); + zgs_seal::seal( + data, + &self.miner_id, + &seal_context, + self.global_seal_sector(index), + ); + } +} + +#[cfg(test)] +mod tests { + use ethereum_types::H256; + use hex_literal::hex; + use zgs_seal; + use zgs_spec::BYTES_PER_SEAL; + use rand::{rngs::StdRng, RngCore, SeedableRng}; + + use super::{SealContextInfo, SealInfo}; + + const TEST_MINER_ID: H256 = H256(hex!( + "003d82782c78262bada18a22f5f982d2b43934d5541e236ca3781ddc8c911cb8" + )); + + #[test] + fn get_seal_context() { + let mut random = StdRng::seed_from_u64(149); + + let mut context1 = H256::default(); + let mut context2 = H256::default(); + let mut context3 = H256::default(); + random.fill_bytes(&mut context1.0); + random.fill_bytes(&mut context2.0); + random.fill_bytes(&mut context3.0); + + let mut sealer = SealInfo::new(0); + sealer.seal_contexts.push(SealContextInfo { + context_digest: context1, + end_seal_index: 2, + }); + sealer.seal_contexts.push(SealContextInfo { + context_digest: context2, + end_seal_index: 3, + }); + sealer.seal_contexts.push(SealContextInfo { + context_digest: context3, + end_seal_index: 6, + }); + + assert_eq!(sealer.get_seal_context_digest(0), Some(context1)); + assert_eq!(sealer.get_seal_context_digest(1), Some(context1)); + assert_eq!(sealer.get_seal_context_digest(2), Some(context2)); + assert_eq!(sealer.get_seal_context_digest(3), Some(context3)); + assert_eq!(sealer.get_seal_context_digest(4), Some(context3)); + assert_eq!(sealer.get_seal_context_digest(5), Some(context3)); + assert_eq!(sealer.get_seal_context_digest(6), None); + + assert_eq!(sealer.truncated_seal_index(0), 0); + assert_eq!(sealer.truncated_seal_index(1), 0); + assert_eq!(sealer.truncated_seal_index(2), 2); + assert_eq!(sealer.truncated_seal_index(3), 3); + assert_eq!(sealer.truncated_seal_index(4), 3); + assert_eq!(sealer.truncated_seal_index(5), 3); + assert_eq!(sealer.truncated_seal_index(6), 6); + } + + #[test] + fn unseal_chunks() { + let mut random = StdRng::seed_from_u64(137); + let mut unsealed_data = vec![0u8; BYTES_PER_SEAL * 10]; + random.fill_bytes(&mut unsealed_data); + let mut data = unsealed_data.clone(); + + let mut context1 = H256::default(); + let mut context2 = H256::default(); + let mut context3 = H256::default(); + random.fill_bytes(&mut context1.0); + random.fill_bytes(&mut context2.0); + random.fill_bytes(&mut context3.0); + + let mut sealer = SealInfo::new(100); + sealer.miner_id = TEST_MINER_ID; + + sealer.seal_contexts.push(SealContextInfo { + context_digest: context1, + end_seal_index: 2, + }); + sealer.seal_contexts.push(SealContextInfo { + context_digest: context2, + end_seal_index: 5, + }); + sealer.seal_contexts.push(SealContextInfo { + context_digest: context3, + end_seal_index: 10, + }); + + // skip seal 6, 3, 9 + for idx in [1, 7, 2, 5, 0, 8, 4].into_iter() { + sealer.seal( + &mut data[idx * BYTES_PER_SEAL..(idx + 1) * BYTES_PER_SEAL], + idx as u16, + ); + sealer.bitmap.set(idx, true); + } + + let partial_hint = &data[BYTES_PER_SEAL * 5 + 64..BYTES_PER_SEAL * 5 + 96]; + let mut tmp_data = data.clone(); + zgs_seal::unseal_with_mask_seed( + &mut tmp_data[BYTES_PER_SEAL * 5 + 96..BYTES_PER_SEAL * 6], + partial_hint, + ); + assert_eq!( + &tmp_data[BYTES_PER_SEAL * 5 + 96..BYTES_PER_SEAL * 6], + &unsealed_data[BYTES_PER_SEAL * 5 + 96..BYTES_PER_SEAL * 6] + ); + + let mut tmp_data = data.clone(); + sealer.unseal(&mut tmp_data[BYTES_PER_SEAL * 5..BYTES_PER_SEAL * 6], 5); + assert_eq!( + &tmp_data[BYTES_PER_SEAL * 5..BYTES_PER_SEAL * 6], + &unsealed_data[BYTES_PER_SEAL * 5..BYTES_PER_SEAL * 6] + ); + + let mut tmp_data = data.clone(); + sealer.unseal(&mut tmp_data[BYTES_PER_SEAL * 6..BYTES_PER_SEAL * 7], 6); + assert_eq!( + &tmp_data[BYTES_PER_SEAL * 6..BYTES_PER_SEAL * 7], + &unsealed_data[BYTES_PER_SEAL * 6..BYTES_PER_SEAL * 7] + ); + + let mut tmp_data = data.clone(); + sealer.unseal( + &mut tmp_data[BYTES_PER_SEAL * 7..BYTES_PER_SEAL * 7 + 96], + 7, + ); + assert_eq!( + &tmp_data[BYTES_PER_SEAL * 7..BYTES_PER_SEAL * 7 + 96], + &unsealed_data[BYTES_PER_SEAL * 7..BYTES_PER_SEAL * 7 + 96] + ); + } +} diff --git a/node/storage/src/log_store/load_chunk/serde.rs b/node/storage/src/log_store/load_chunk/serde.rs new file mode 100644 index 0000000..4c7fa50 --- /dev/null +++ b/node/storage/src/log_store/load_chunk/serde.rs @@ -0,0 +1,83 @@ +use super::{chunk_data::PartialBatch, EntryBatchData}; + +use crate::log_store::load_chunk::chunk_data::IncompleteData; +use ssz::{Decode, DecodeError, Encode}; +use std::mem; + +const COMPLETE_BATCH_TYPE: u8 = 0; +const INCOMPLETE_BATCH_TYPE: u8 = 1; + +impl Encode for EntryBatchData { + fn is_ssz_fixed_len() -> bool { + false + } + + fn ssz_append(&self, buf: &mut Vec) { + match &self { + EntryBatchData::Complete(data) => { + buf.extend_from_slice(&[COMPLETE_BATCH_TYPE]); + buf.extend_from_slice(data.as_slice()); + } + EntryBatchData::Incomplete(data_list) => { + buf.extend_from_slice(&[INCOMPLETE_BATCH_TYPE]); + buf.extend_from_slice(&data_list.as_ssz_bytes()); + } + } + } + + fn ssz_bytes_len(&self) -> usize { + match &self { + EntryBatchData::Complete(data) => 1 + data.len(), + EntryBatchData::Incomplete(batch_list) => 1 + batch_list.ssz_bytes_len(), + } + } +} + +impl Decode for EntryBatchData { + fn is_ssz_fixed_len() -> bool { + false + } + + fn from_ssz_bytes(bytes: &[u8]) -> std::result::Result { + match *bytes.first().ok_or(DecodeError::ZeroLengthItem)? { + COMPLETE_BATCH_TYPE => Ok(EntryBatchData::Complete(bytes[1..].to_vec())), + INCOMPLETE_BATCH_TYPE => Ok(EntryBatchData::Incomplete( + IncompleteData::from_ssz_bytes(&bytes[1..])?, + )), + unknown => Err(DecodeError::BytesInvalid(format!( + "Unrecognized EntryBatchData indentifier {}", + unknown + ))), + } + } +} + +impl Encode for PartialBatch { + fn is_ssz_fixed_len() -> bool { + false + } + + fn ssz_append(&self, buf: &mut Vec) { + buf.extend_from_slice(&self.start_sector.to_be_bytes()); + buf.extend_from_slice(&self.data); + } + + fn ssz_bytes_len(&self) -> usize { + 1 + self.data.len() + } +} + +impl Decode for PartialBatch { + fn is_ssz_fixed_len() -> bool { + false + } + + fn from_ssz_bytes(bytes: &[u8]) -> std::result::Result { + Ok(Self { + start_sector: usize::from_be_bytes( + bytes[..mem::size_of::()].try_into().unwrap(), + ), + data: bytes[mem::size_of::()..].to_vec(), + }) + } +} diff --git a/node/storage/src/log_store/log_manager.rs b/node/storage/src/log_store/log_manager.rs new file mode 100644 index 0000000..4f231ce --- /dev/null +++ b/node/storage/src/log_store/log_manager.rs @@ -0,0 +1,1077 @@ +use crate::log_store::flow_store::{batch_iter, FlowConfig, FlowStore}; +use crate::log_store::tx_store::TransactionStore; +use crate::log_store::{ + FlowRead, FlowWrite, LogStoreChunkRead, LogStoreChunkWrite, LogStoreRead, LogStoreWrite, +}; +use crate::{try_option, ZgsKeyValueDB}; +use anyhow::{anyhow, bail, Result}; +use append_merkle::{Algorithm, MerkleTreeRead, Sha3Algorithm}; +use ethereum_types::H256; +use kvdb_rocksdb::{Database, DatabaseConfig}; +use merkle_light::merkle::{log2_pow2, MerkleTree}; +use merkle_tree::RawLeafSha3Algorithm; +use rayon::iter::ParallelIterator; +use rayon::prelude::ParallelSlice; +use shared_types::{ + bytes_to_chunks, compute_padded_chunk_size, compute_segment_size, Chunk, ChunkArray, + ChunkArrayWithProof, ChunkWithProof, DataRoot, FlowProof, FlowRangeProof, Merkle, Transaction, +}; +use std::cmp::Ordering; +use std::collections::BTreeMap; +use std::path::Path; +use std::sync::Arc; +use tracing::{debug, error, info, instrument, trace, warn}; + +use super::LogStoreInner; + +/// 256 Bytes +pub const ENTRY_SIZE: usize = 256; +/// 1024 Entries. +pub const PORA_CHUNK_SIZE: usize = 1024; + +pub const COL_TX: u32 = 0; +pub const COL_ENTRY_BATCH: u32 = 1; +pub const COL_TX_DATA_ROOT_INDEX: u32 = 2; +pub const COL_ENTRY_BATCH_ROOT: u32 = 3; +pub const COL_TX_COMPLETED: u32 = 4; +pub const COL_MISC: u32 = 5; +pub const COL_SEAL_CONTEXT: u32 = 6; +pub const COL_NUM: u32 = 7; + +pub struct LogManager { + pub(crate) db: Arc, + tx_store: TransactionStore, + flow_store: FlowStore, + // TODO(zz): Refactor the in-memory merkle and in-disk storage together. + pora_chunks_merkle: Merkle, + /// The in-memory structure of the sub merkle tree of the last chunk. + /// The size is always less than `PORA_CHUNK_SIZE`. + last_chunk_merkle: Merkle, +} + +#[derive(Clone, Default)] +pub struct LogConfig { + pub flow: FlowConfig, +} + +impl LogStoreInner for LogManager { + fn flow(&self) -> &dyn super::Flow { + &self.flow_store + } + + fn flow_mut(&mut self) -> &mut dyn super::Flow { + &mut self.flow_store + } +} + +impl LogStoreChunkWrite for LogManager { + fn put_chunks(&mut self, tx_seq: u64, chunks: ChunkArray) -> Result<()> { + let tx = self + .tx_store + .get_tx_by_seq_number(tx_seq)? + .ok_or_else(|| anyhow!("put chunks with missing tx: tx_seq={}", tx_seq))?; + let (chunks_for_proof, _) = compute_padded_chunk_size(tx.size as usize); + if chunks.start_index.saturating_mul(ENTRY_SIZE as u64) + chunks.data.len() as u64 + > (chunks_for_proof * ENTRY_SIZE) as u64 + { + bail!( + "put chunks with data out of tx range: tx_seq={} start_index={} data_len={}", + tx_seq, + chunks.start_index, + chunks.data.len() + ); + } + // TODO: Use another struct to avoid confusion. + let mut flow_entry_array = chunks; + flow_entry_array.start_index += tx.start_entry_index; + self.append_entries(flow_entry_array)?; + Ok(()) + } + + fn put_chunks_with_tx_hash( + &mut self, + tx_seq: u64, + tx_hash: H256, + chunks: ChunkArray, + ) -> Result { + let tx = self + .tx_store + .get_tx_by_seq_number(tx_seq)? + .ok_or_else(|| anyhow!("put chunks with missing tx: tx_seq={}", tx_seq))?; + if tx.hash() != tx_hash { + return Ok(false); + } + let (chunks_for_proof, _) = compute_padded_chunk_size(tx.size as usize); + if chunks.start_index.saturating_mul(ENTRY_SIZE as u64) + chunks.data.len() as u64 + > (chunks_for_proof * ENTRY_SIZE) as u64 + { + bail!( + "put chunks with data out of tx range: tx_seq={} start_index={} data_len={}", + tx_seq, + chunks.start_index, + chunks.data.len() + ); + } + // TODO: Use another struct to avoid confusion. + let mut flow_entry_array = chunks; + flow_entry_array.start_index += tx.start_entry_index; + self.append_entries(flow_entry_array)?; + Ok(true) + } + + fn remove_all_chunks(&self, _tx_seq: u64) -> crate::error::Result<()> { + todo!() + } +} + +impl LogStoreWrite for LogManager { + #[instrument(skip(self))] + /// Insert the tx and update the flow store if needed. + /// + /// We assumes that all transactions are inserted in order sequentially. + /// We always write the database in the following order: + /// 1. Insert the tx (the tx and the root to tx_seq map are inserted atomically). + /// 2. Update the flow store(pad data for alignment and copy data in `put_tx`, write data in + /// `put_chunks`, pad rear data in `finalize_tx`). + /// 3. Mark tx as finalized. + /// + /// Step 1 and 3 are both atomic operations. + /// * If a tx has been finalized, the data in flow must + /// have been updated correctly. + /// * If `put_tx` succeeds but not finalized, we rely on the upper layer + /// operations (client/auto-sync) to insert needed data (`put_chunks`) and trigger + /// finalization (`finalize_tx`). + /// * If `put_tx` fails in the middle, the tx is inserted but the flow is not updated correctly. + /// Only the last tx may have this case, so we rerun + /// `put_tx` for the last tx when we restart the node to ensure that it succeeds. + /// + fn put_tx(&mut self, tx: Transaction) -> Result<()> { + debug!("put_tx: tx={:?}", tx); + let expected_seq = self.next_tx_seq(); + if tx.seq != expected_seq { + if tx.seq + 1 == expected_seq && !self.check_tx_completed(tx.seq)? { + // special case for rerun the last tx during recovery. + debug!("recovery with tx_seq={}", tx.seq); + } else { + // This is not supposed to happen since we have checked the tx seq in log entry sync. + error!("tx unmatch, expected={} get={:?}", expected_seq, tx); + bail!("unexpected tx!"); + } + } + let maybe_same_data_tx_seq = self.tx_store.put_tx(tx.clone())?.first().cloned(); + // TODO(zz): Should we validate received tx? + self.append_subtree_list(tx.merkle_nodes.clone())?; + self.commit_merkle(tx.seq)?; + + if let Some(old_tx_seq) = maybe_same_data_tx_seq { + if self.check_tx_completed(old_tx_seq)? { + self.copy_tx_data(old_tx_seq, vec![tx.seq])?; + self.tx_store.finalize_tx(tx.seq)?; + } + } + Ok(()) + } + + fn finalize_tx(&mut self, tx_seq: u64) -> Result<()> { + let tx = self + .tx_store + .get_tx_by_seq_number(tx_seq)? + .ok_or_else(|| anyhow!("finalize_tx with tx missing: tx_seq={}", tx_seq))?; + + self.padding_rear_data(&tx)?; + + let tx_end_index = tx.start_entry_index + bytes_to_entries(tx.size); + // TODO: Check completeness without loading all data in memory. + // TODO: Should we double check the tx merkle root? + if self + .flow_store + .get_entries(tx.start_entry_index, tx_end_index)? + .is_some() + { + let same_root_seq_list = self + .tx_store + .get_tx_seq_list_by_data_root(&tx.data_merkle_root)?; + // Check if there are other same-root transaction not finalized. + if same_root_seq_list.first() == Some(&tx_seq) { + self.copy_tx_data(tx_seq, same_root_seq_list[1..].to_vec())?; + } + self.tx_store.finalize_tx(tx_seq)?; + Ok(()) + } else { + bail!("finalize tx with data missing: tx_seq={}", tx_seq) + } + } + + fn finalize_tx_with_hash(&mut self, tx_seq: u64, tx_hash: H256) -> crate::error::Result { + trace!( + "finalize_tx_with_hash: tx_seq={} tx_hash={:?}", + tx_seq, + tx_hash + ); + let tx = self + .tx_store + .get_tx_by_seq_number(tx_seq)? + .ok_or_else(|| anyhow!("finalize_tx with tx missing: tx_seq={}", tx_seq))?; + debug!("finalize_tx_with_hash: tx={:?}", tx); + if tx.hash() != tx_hash { + return Ok(false); + } + + self.padding_rear_data(&tx)?; + + let tx_end_index = tx.start_entry_index + bytes_to_entries(tx.size); + // TODO: Check completeness without loading all data in memory. + // TODO: Should we double check the tx merkle root? + if self + .flow_store + .get_entries(tx.start_entry_index, tx_end_index)? + .is_some() + { + self.tx_store.finalize_tx(tx_seq)?; + let same_root_seq_list = self + .tx_store + .get_tx_seq_list_by_data_root(&tx.data_merkle_root)?; + // Check if there are other same-root transaction not finalized. + if same_root_seq_list.first() == Some(&tx_seq) { + self.copy_tx_data(tx_seq, same_root_seq_list[1..].to_vec())?; + } + Ok(true) + } else { + bail!("finalize tx with data missing: tx_seq={}", tx_seq) + } + } + + fn put_sync_progress(&self, progress: (u64, H256)) -> Result<()> { + self.tx_store.put_progress(progress) + } + + /// Return the reverted Transactions in order. + /// `tx_seq == u64::MAX` is a special case for reverting all transactions. + fn revert_to(&mut self, tx_seq: u64) -> Result> { + // FIXME(zz): If this revert is triggered by chain reorg after restarts, this will fail. + self.revert_merkle_tree(tx_seq)?; + let start_index = self.last_chunk_start_index() * PORA_CHUNK_SIZE as u64 + + self.last_chunk_merkle.leaves() as u64; + self.flow_store.truncate(start_index)?; + let start = if tx_seq != u64::MAX { tx_seq + 1 } else { 0 }; + self.tx_store.remove_tx_after(start) + } +} + +impl LogStoreChunkRead for LogManager { + fn get_chunk_by_tx_and_index( + &self, + tx_seq: u64, + index: usize, + ) -> crate::error::Result> { + // TODO(zz): This is not needed? + let single_chunk_array = + try_option!(self.get_chunks_by_tx_and_index_range(tx_seq, index, index + 1)?); + Ok(Some(Chunk(single_chunk_array.data.as_slice().try_into()?))) + } + + fn get_chunks_by_tx_and_index_range( + &self, + tx_seq: u64, + index_start: usize, + index_end: usize, + ) -> crate::error::Result> { + let tx = try_option!(self.get_tx_by_seq_number(tx_seq)?); + + if index_end as u64 > bytes_to_entries(tx.size) { + bail!( + "end entry index exceeds tx size: end={} tx size={}", + index_start, + tx.size + ); + } + + let start_flow_index = tx.start_entry_index + index_start as u64; + let end_flow_index = tx.start_entry_index + index_end as u64; + // TODO: Use another struct. + // Set returned chunk start index as the offset in the tx data. + let mut tx_chunk = try_option!(self + .flow_store + .get_entries(start_flow_index, end_flow_index)?); + tx_chunk.start_index -= tx.start_entry_index; + Ok(Some(tx_chunk)) + } + + fn get_chunk_by_data_root_and_index( + &self, + _data_root: &DataRoot, + _index: usize, + ) -> crate::error::Result> { + todo!() + } + + fn get_chunks_by_data_root_and_index_range( + &self, + data_root: &DataRoot, + index_start: usize, + index_end: usize, + ) -> crate::error::Result> { + let tx_seq = try_option!(self.get_tx_seq_by_data_root(data_root)?); + self.get_chunks_by_tx_and_index_range(tx_seq, index_start, index_end) + } + + fn get_chunk_index_list(&self, _tx_seq: u64) -> crate::error::Result> { + todo!() + } + + fn get_chunk_by_flow_index( + &self, + index: u64, + length: u64, + ) -> crate::error::Result> { + let start_flow_index = index; + let end_flow_index = index + length; + self.flow_store + .get_entries(start_flow_index, end_flow_index) + } +} + +impl LogStoreRead for LogManager { + fn get_tx_by_seq_number(&self, seq: u64) -> crate::error::Result> { + self.tx_store.get_tx_by_seq_number(seq) + } + + fn get_tx_seq_by_data_root(&self, data_root: &DataRoot) -> crate::error::Result> { + self.tx_store.get_first_tx_seq_by_data_root(data_root) + } + + fn get_chunk_with_proof_by_tx_and_index( + &self, + tx_seq: u64, + index: usize, + ) -> crate::error::Result> { + // TODO(zz): Optimize for mining. + let single_chunk_array = try_option!(self.get_chunks_with_proof_by_tx_and_index_range( + tx_seq, + index, + index + 1 + )?); + Ok(Some(ChunkWithProof { + chunk: Chunk(single_chunk_array.chunks.data.as_slice().try_into()?), + proof: single_chunk_array.proof.left_proof, + })) + } + + fn get_chunks_with_proof_by_tx_and_index_range( + &self, + tx_seq: u64, + index_start: usize, + index_end: usize, + ) -> crate::error::Result> { + let tx = try_option!(self.tx_store.get_tx_by_seq_number(tx_seq)?); + let chunks = + try_option!(self.get_chunks_by_tx_and_index_range(tx_seq, index_start, index_end)?); + let left_proof = self.gen_proof(tx.start_entry_index + index_start as u64, None)?; + let right_proof = self.gen_proof(tx.start_entry_index + index_end as u64 - 1, None)?; + Ok(Some(ChunkArrayWithProof { + chunks, + proof: FlowRangeProof { + left_proof, + right_proof, + }, + })) + } + + fn check_tx_completed(&self, tx_seq: u64) -> crate::error::Result { + self.tx_store.check_tx_completed(tx_seq) + } + + fn validate_range_proof(&self, tx_seq: u64, data: &ChunkArrayWithProof) -> Result { + let tx = self + .get_tx_by_seq_number(tx_seq)? + .ok_or_else(|| anyhow!("tx missing"))?; + let leaves = data_to_merkle_leaves(&data.chunks.data)?; + data.proof.validate::( + &leaves, + (data.chunks.start_index + tx.start_entry_index) as usize, + )?; + Ok(self.pora_chunks_merkle.check_root(&data.proof.root())) + } + + fn get_sync_progress(&self) -> Result> { + self.tx_store.get_progress() + } + + fn next_tx_seq(&self) -> u64 { + self.tx_store.next_tx_seq() + } + + fn get_proof_at_root( + &self, + root: &DataRoot, + index: u64, + length: u64, + ) -> crate::error::Result { + let left_proof = self.gen_proof(index, Some(*root))?; + let right_proof = self.gen_proof(index + length - 1, Some(*root))?; + Ok(FlowRangeProof { + left_proof, + right_proof, + }) + } + + fn get_context(&self) -> crate::error::Result<(DataRoot, u64)> { + Ok(( + *self.pora_chunks_merkle.root(), + self.last_chunk_start_index() + self.last_chunk_merkle.leaves() as u64, + )) + } +} + +impl LogManager { + pub fn rocksdb(config: LogConfig, path: impl AsRef) -> Result { + let mut db_config = DatabaseConfig::with_columns(COL_NUM); + db_config.enable_statistics = true; + let db = Arc::new(Database::open(&db_config, path)?); + Self::new(db, config) + } + + pub fn memorydb(config: LogConfig) -> Result { + let db = Arc::new(kvdb_memorydb::create(COL_NUM)); + Self::new(db, config) + } + + fn new(db: Arc, config: LogConfig) -> Result { + let tx_store = TransactionStore::new(db.clone())?; + let flow_store = FlowStore::new(db.clone(), config.flow); + let mut initial_data = flow_store.get_chunk_root_list()?; + // If the last tx `put_tx` does not complete, we will revert it in `initial_data.subtree_list` + // first and call `put_tx` later. The known leaves in its data will be saved in `extra_leaves` + // and inserted later. + let mut extra_leaves = Vec::new(); + + let next_tx_seq = tx_store.next_tx_seq(); + let mut start_tx_seq = if next_tx_seq > 0 { + Some(next_tx_seq - 1) + } else { + None + }; + let mut last_tx_to_insert = None; + if let Some(last_tx_seq) = start_tx_seq { + if !tx_store.check_tx_completed(last_tx_seq)? { + // Last tx not finalized, we need to check if its `put_tx` is completed. + let last_tx = tx_store + .get_tx_by_seq_number(last_tx_seq)? + .expect("tx missing"); + let mut current_len = initial_data.leaves(); + let expected_len = (last_tx.start_entry_index + last_tx.num_entries() as u64) + / PORA_CHUNK_SIZE as u64; + match expected_len.cmp(&(current_len as u64)) { + Ordering::Less => { + bail!( + "Unexpected DB: merkle tree larger than the known data size,\ + expected={} get={}", + expected_len, + current_len + ); + } + Ordering::Equal => {} + Ordering::Greater => { + // Flow updates are not complete. + // For simplicity, we build the merkle tree for the previous tx and update + // the flow for the last tx again. + info!("revert last tx: last_tx={:?}", last_tx); + last_tx_to_insert = Some(last_tx); + if last_tx_seq == 0 { + start_tx_seq = None; + } else { + // truncate until we get the pora chunks merkle for the previous tx. + let previous_tx = tx_store + .get_tx_by_seq_number(last_tx_seq - 1)? + .expect("tx missing"); + let expected_len = ((previous_tx.start_entry_index + + previous_tx.num_entries() as u64) + / PORA_CHUNK_SIZE as u64) + as usize; + assert!(current_len > expected_len); + while let Some((subtree_depth, _)) = initial_data.subtree_list.pop() { + current_len -= 1 << (subtree_depth - 1); + if current_len == expected_len { + break; + } + } + assert_eq!(current_len, expected_len); + while let Some((index, h)) = initial_data.known_leaves.pop() { + if index < current_len { + initial_data.known_leaves.push((index, h)); + break; + } else { + extra_leaves.push((index, h)); + } + } + start_tx_seq = Some(last_tx_seq - 1); + }; + } + } + } + } + + let mut pora_chunks_merkle = + Merkle::new_with_subtrees(initial_data, log2_pow2(PORA_CHUNK_SIZE), start_tx_seq)?; + let last_chunk_merkle = match start_tx_seq { + Some(tx_seq) => { + tx_store.rebuild_last_chunk_merkle(pora_chunks_merkle.leaves(), tx_seq)? + } + // Initialize + None => Merkle::new_with_depth(vec![], log2_pow2(PORA_CHUNK_SIZE) + 1, None), + }; + + debug!( + "LogManager::new() with chunk_list_len={} start_tx_seq={:?} last_chunk={}", + pora_chunks_merkle.leaves(), + start_tx_seq, + last_chunk_merkle.leaves(), + ); + if last_chunk_merkle.leaves() != 0 { + pora_chunks_merkle.append(*last_chunk_merkle.root()); + // update the merkle root + pora_chunks_merkle.commit(start_tx_seq); + } + let mut log_manager = Self { + db, + tx_store, + flow_store, + pora_chunks_merkle, + last_chunk_merkle, + }; + + if let Some(tx) = last_tx_to_insert { + log_manager.put_tx(tx)?; + for (index, h) in extra_leaves { + if index < log_manager.pora_chunks_merkle.leaves() { + log_manager.pora_chunks_merkle.fill_leaf(index, h); + } else { + error!("out of range extra leaf: index={} hash={:?}", index, h); + } + } + } else { + assert!(extra_leaves.is_empty()); + } + log_manager.try_initialize()?; + Ok(log_manager) + } + + fn try_initialize(&mut self) -> Result<()> { + if self.pora_chunks_merkle.leaves() == 0 && self.last_chunk_merkle.leaves() == 0 { + self.last_chunk_merkle.append(H256::zero()); + self.pora_chunks_merkle + .update_last(*self.last_chunk_merkle.root()); + } else if self.last_chunk_merkle.leaves() != 0 { + let last_chunk_start_index = self.last_chunk_start_index(); + let last_chunk_data = self.flow_store.get_available_entries( + last_chunk_start_index, + last_chunk_start_index + PORA_CHUNK_SIZE as u64, + )?; + for e in last_chunk_data { + let start_index = e.start_index - last_chunk_start_index; + for i in 0..e.data.len() / ENTRY_SIZE { + let index = i + start_index as usize; + if index >= self.last_chunk_merkle.leaves() { + // We revert the merkle tree before truncate the flow store, + // so last_chunk_data may include data that should have been truncated. + break; + } + self.last_chunk_merkle.fill_leaf( + index, + Sha3Algorithm::leaf(&e.data[i * ENTRY_SIZE..(i + 1) * ENTRY_SIZE]), + ); + } + } + } + Ok(()) + } + + fn gen_proof(&self, flow_index: u64, maybe_root: Option) -> Result { + let chunk_index = flow_index / PORA_CHUNK_SIZE as u64; + let top_proof = match maybe_root { + None => self.pora_chunks_merkle.gen_proof(chunk_index as usize)?, + Some(root) => self + .pora_chunks_merkle + .at_root_version(&root)? + .gen_proof(chunk_index as usize)?, + }; + + // TODO(zz): Maybe we can decide that all proofs are at the PoRA chunk level, so + // we do not need to maintain the proof at the entry level below. + // Condition (self.last_chunk_merkle.leaves() == 0): When last chunk size is exactly PORA_CHUNK_SIZE, proof should be generated from flow data, as last_chunk_merkle.leaves() is zero at this time + // TODO(zz): In the current use cases, `maybe_root` is only `Some` for mining + // and `flow_index` must be within a complete PoRA chunk. For possible future usages, + // we'll need to find the flow length at the given root and load a partial chunk + // if `flow_index` is in the last chunk. + let sub_proof = if chunk_index as usize != self.pora_chunks_merkle.leaves() - 1 + || self.last_chunk_merkle.leaves() == 0 + { + self.flow_store + .gen_proof_in_batch(chunk_index as usize, flow_index as usize % PORA_CHUNK_SIZE)? + } else { + match maybe_root { + None => self + .last_chunk_merkle + .gen_proof(flow_index as usize % PORA_CHUNK_SIZE)?, + Some(root) => self + .last_chunk_merkle + .at_root_version(&root)? + .gen_proof(flow_index as usize % PORA_CHUNK_SIZE)?, + } + }; + entry_proof(&top_proof, &sub_proof) + } + + #[instrument(skip(self))] + fn append_subtree_list(&mut self, merkle_list: Vec<(usize, DataRoot)>) -> Result<()> { + if merkle_list.is_empty() { + return Ok(()); + } + + self.pad_tx(1 << (merkle_list[0].0 - 1))?; + + let mut batch_root_map = BTreeMap::new(); + for (subtree_depth, subtree_root) in merkle_list { + let subtree_size = 1 << (subtree_depth - 1); + if self.last_chunk_merkle.leaves() + subtree_size <= PORA_CHUNK_SIZE { + self.last_chunk_merkle + .append_subtree(subtree_depth, subtree_root)?; + if self.last_chunk_merkle.leaves() == subtree_size { + // `last_chunk_merkle` was empty, so this is a new leaf in the top_tree. + self.pora_chunks_merkle + .append_subtree(1, *self.last_chunk_merkle.root())?; + } else { + self.pora_chunks_merkle + .update_last(*self.last_chunk_merkle.root()); + } + if self.last_chunk_merkle.leaves() == PORA_CHUNK_SIZE { + batch_root_map.insert( + self.pora_chunks_merkle.leaves() - 1, + (*self.last_chunk_merkle.root(), 1), + ); + self.complete_last_chunk_merkle(self.pora_chunks_merkle.leaves() - 1)?; + } + } else { + // `last_chunk_merkle` has been padded here, so a subtree should not be across + // the chunks boundary. + assert_eq!(self.last_chunk_merkle.leaves(), 0); + assert!(subtree_size >= PORA_CHUNK_SIZE); + batch_root_map.insert( + self.pora_chunks_merkle.leaves(), + (subtree_root, subtree_depth - log2_pow2(PORA_CHUNK_SIZE)), + ); + self.pora_chunks_merkle + .append_subtree(subtree_depth - log2_pow2(PORA_CHUNK_SIZE), subtree_root)?; + } + } + self.flow_store.put_batch_root_list(batch_root_map)?; + Ok(()) + } + + #[instrument(skip(self))] + fn pad_tx(&mut self, first_subtree_size: u64) -> Result<()> { + // Check if we need to pad the flow. + let tx_start_flow_index = + self.last_chunk_start_index() + self.last_chunk_merkle.leaves() as u64; + let extra = tx_start_flow_index % first_subtree_size; + trace!( + "before pad_tx {} {}", + self.pora_chunks_merkle.leaves(), + self.last_chunk_merkle.leaves() + ); + if extra != 0 { + let pad_data = Self::padding((first_subtree_size - extra) as usize); + + // Update the in-memory merkle tree. + let mut root_map = BTreeMap::new(); + let last_chunk_pad = if self.last_chunk_merkle.leaves() == 0 { + 0 + } else { + (PORA_CHUNK_SIZE - self.last_chunk_merkle.leaves()) * ENTRY_SIZE + }; + + let mut completed_chunk_index = None; + if pad_data.len() < last_chunk_pad { + self.last_chunk_merkle + .append_list(data_to_merkle_leaves(&pad_data)?); + self.pora_chunks_merkle + .update_last(*self.last_chunk_merkle.root()); + } else { + if last_chunk_pad != 0 { + // Pad the last chunk. + self.last_chunk_merkle + .append_list(data_to_merkle_leaves(&pad_data[..last_chunk_pad])?); + self.pora_chunks_merkle + .update_last(*self.last_chunk_merkle.root()); + root_map.insert( + self.pora_chunks_merkle.leaves() - 1, + (*self.last_chunk_merkle.root(), 1), + ); + completed_chunk_index = Some(self.pora_chunks_merkle.leaves() - 1); + } + + // Pad with more complete chunks. + let mut start_index = last_chunk_pad / ENTRY_SIZE; + while pad_data.len() >= (start_index + PORA_CHUNK_SIZE) * ENTRY_SIZE { + let data = pad_data + [start_index * ENTRY_SIZE..(start_index + PORA_CHUNK_SIZE) * ENTRY_SIZE] + .to_vec(); + let root = *Merkle::new(data_to_merkle_leaves(&data)?, 0, None).root(); + self.pora_chunks_merkle.append(root); + root_map.insert(self.pora_chunks_merkle.leaves() - 1, (root, 1)); + start_index += PORA_CHUNK_SIZE; + } + assert_eq!(pad_data.len(), start_index * ENTRY_SIZE); + } + + // Update the root index. + self.flow_store.put_batch_root_list(root_map)?; + // Update the flow database. + // This should be called before `complete_last_chunk_merkle` so that we do not save + // subtrees with data known. + self.flow_store.append_entries(ChunkArray { + data: pad_data, + start_index: tx_start_flow_index, + })?; + if let Some(index) = completed_chunk_index { + self.complete_last_chunk_merkle(index)?; + } + } + trace!( + "after pad_tx {} {}", + self.pora_chunks_merkle.leaves(), + self.last_chunk_merkle.leaves() + ); + Ok(()) + } + + fn append_entries(&mut self, flow_entry_array: ChunkArray) -> Result<()> { + let last_chunk_start_index = self.last_chunk_start_index(); + if flow_entry_array.start_index + bytes_to_chunks(flow_entry_array.data.len()) as u64 + > last_chunk_start_index + { + // Update `last_chunk_merkle` with real data. + let (chunk_start_index, flow_entry_data_index) = if flow_entry_array.start_index + >= last_chunk_start_index + { + // flow_entry_array only fill last chunk + ( + (flow_entry_array.start_index - last_chunk_start_index) as usize, + 0, + ) + } else { + // flow_entry_array fill both last and last - 1 chunk + ( + 0, + (last_chunk_start_index - flow_entry_array.start_index) as usize * ENTRY_SIZE, + ) + }; + + // Since we always put tx before insert its data. Here `last_chunk_merkle` must + // have included the data range. + for (local_index, entry) in flow_entry_array.data[flow_entry_data_index..] + .chunks_exact(ENTRY_SIZE) + .enumerate() + { + self.last_chunk_merkle + .fill_leaf(chunk_start_index + local_index, Sha3Algorithm::leaf(entry)); + } + } + let chunk_roots = self.flow_store.append_entries(flow_entry_array)?; + for (chunk_index, chunk_root) in chunk_roots { + if chunk_index < self.pora_chunks_merkle.leaves() as u64 { + self.pora_chunks_merkle + .fill_leaf(chunk_index as usize, chunk_root); + } else { + // TODO(zz): This assumption may be false in the future. + unreachable!("We always insert tx nodes before put_chunks"); + } + } + Ok(()) + } + + // FIXME(zz): Implement padding. + pub fn padding(len: usize) -> Vec { + vec![0; len * ENTRY_SIZE] + } + + fn last_chunk_start_index(&self) -> u64 { + if self.pora_chunks_merkle.leaves() == 0 { + 0 + } else { + PORA_CHUNK_SIZE as u64 + * if self.last_chunk_merkle.leaves() == 0 { + // The last chunk is empty and its root hash is not in `pora_chunk_merkle`, + // so all chunks in `pora_chunk_merkle` is complete. + self.pora_chunks_merkle.leaves() + } else { + // The last chunk has data, so we need to exclude it from `pora_chunks_merkle`. + self.pora_chunks_merkle.leaves() - 1 + } as u64 + } + } + + #[instrument(skip(self))] + fn commit_merkle(&mut self, tx_seq: u64) -> Result<()> { + self.pora_chunks_merkle.commit(Some(tx_seq)); + self.last_chunk_merkle.commit(Some(tx_seq)); + Ok(()) + } + + fn revert_merkle_tree(&mut self, tx_seq: u64) -> Result<()> { + // Special case for reverting tx_seq == 0 + if tx_seq == u64::MAX { + self.pora_chunks_merkle.reset(); + self.last_chunk_merkle.reset(); + self.try_initialize()?; + return Ok(()); + } + let old_leaves = self.pora_chunks_merkle.leaves(); + self.pora_chunks_merkle.revert_to(tx_seq)?; + if old_leaves == self.pora_chunks_merkle.leaves() { + self.last_chunk_merkle.revert_to(tx_seq)?; + } else { + // We are reverting to a position before the current last_chunk. + self.last_chunk_merkle = self + .tx_store + .rebuild_last_chunk_merkle(self.pora_chunks_merkle.leaves() - 1, tx_seq)?; + self.try_initialize()?; + assert_eq!( + Some(*self.last_chunk_merkle.root()), + self.pora_chunks_merkle + .leaf_at(self.pora_chunks_merkle.leaves() - 1)? + ); + } + Ok(()) + } + + #[cfg(test)] + pub fn flow_store(&self) -> &FlowStore { + &self.flow_store + } + + fn padding_rear_data(&mut self, tx: &Transaction) -> Result<()> { + let (chunks, _) = compute_padded_chunk_size(tx.size as usize); + let (segments_for_proof, last_segment_size_for_proof) = + compute_segment_size(chunks, PORA_CHUNK_SIZE); + debug!( + "segments_for_proof: {}, last_segment_size_for_proof: {}", + segments_for_proof, last_segment_size_for_proof + ); + + let chunks_for_file = bytes_to_entries(tx.size) as usize; + let (mut segments_for_file, mut last_segment_size_for_file) = + compute_segment_size(chunks_for_file, PORA_CHUNK_SIZE); + debug!( + "segments_for_file: {}, last_segment_size_for_file: {}", + segments_for_file, last_segment_size_for_file + ); + + while segments_for_file <= segments_for_proof { + let padding_size = if segments_for_file == segments_for_proof { + (last_segment_size_for_proof - last_segment_size_for_file) * ENTRY_SIZE + } else { + (PORA_CHUNK_SIZE - last_segment_size_for_file) * ENTRY_SIZE + }; + + debug!("Padding size: {}", padding_size); + if padding_size > 0 { + // This tx hash is guaranteed to be consistent. + self.put_chunks_with_tx_hash( + tx.seq, + tx.hash(), + ChunkArray { + data: vec![0u8; padding_size], + start_index: ((segments_for_file - 1) * PORA_CHUNK_SIZE + + last_segment_size_for_file) + as u64, + }, + )?; + } + + last_segment_size_for_file = 0; + segments_for_file += 1; + } + + Ok(()) + } + + fn copy_tx_data(&mut self, from_tx_seq: u64, to_tx_seq_list: Vec) -> Result<()> { + // We have all the data need for this tx, so just copy them. + let old_tx = self + .get_tx_by_seq_number(from_tx_seq)? + .ok_or_else(|| anyhow!("from tx missing"))?; + let mut to_tx_offset_list = Vec::with_capacity(to_tx_seq_list.len()); + for seq in to_tx_seq_list { + // No need to copy data for completed tx. + if self.check_tx_completed(seq)? { + continue; + } + let tx = self + .get_tx_by_seq_number(seq)? + .ok_or_else(|| anyhow!("to tx missing"))?; + to_tx_offset_list.push((tx.seq, tx.start_entry_index - old_tx.start_entry_index)); + } + if to_tx_offset_list.is_empty() { + return Ok(()); + } + // copy data in batches + // TODO(zz): Do this asynchronously and keep atomicity. + for (batch_start, batch_end) in batch_iter( + old_tx.start_entry_index, + old_tx.start_entry_index + old_tx.num_entries() as u64, + PORA_CHUNK_SIZE, + ) { + let batch_data = self + .get_chunk_by_flow_index(batch_start, batch_end - batch_start)? + .ok_or_else(|| anyhow!("tx data missing"))?; + for (_, offset) in &to_tx_offset_list { + let mut data = batch_data.clone(); + data.start_index += offset; + self.append_entries(data)?; + } + } + // num_entries() includes the rear padding data, so no need for more padding. + + for (seq, _) in to_tx_offset_list { + self.tx_store.finalize_tx(seq)?; + } + Ok(()) + } + + /// Here we persist the subtrees with the incomplete data of the last chunk merkle so that + /// we can still provide proof for known data in it. + /// Another choice is to insert these subtrees earlier in `put_tx`. To insert them here can + /// batch them and avoid inserting for the subtrees with all data known. + fn complete_last_chunk_merkle(&mut self, index: usize) -> Result<()> { + let subtree_list = self.last_chunk_merkle.get_subtrees(); + self.last_chunk_merkle = + Merkle::new_with_depth(vec![], log2_pow2(PORA_CHUNK_SIZE) + 1, None); + + // Only insert non-leave subtrees. The leave data should have been available. + let mut to_insert_subtrees = Vec::new(); + let mut start_index = 0; + for (subtree_height, root) in subtree_list { + to_insert_subtrees.push((start_index, subtree_height, root)); + start_index += 1 << (subtree_height - 1); + } + self.flow_store + .insert_subtree_list_for_batch(index, to_insert_subtrees) + } +} + +/// This represents the subtree of a chunk or the whole data merkle tree. +pub type FileMerkleTree = MerkleTree<[u8; 32], RawLeafSha3Algorithm>; + +#[macro_export] +macro_rules! try_option { + ($r: ident) => { + match $r { + Some(v) => v, + None => return Ok(None), + } + }; + ($e: expr) => { + match $e { + Some(v) => v, + None => return Ok(None), + } + }; +} + +/// This should be called with input checked. +pub fn sub_merkle_tree(leaf_data: &[u8]) -> Result { + Ok(FileMerkleTree::new( + data_to_merkle_leaves(leaf_data)? + .into_iter() + .map(|h| h.0) + .collect::>(), + )) +} + +pub fn data_to_merkle_leaves(leaf_data: &[u8]) -> Result> { + if leaf_data.len() % ENTRY_SIZE != 0 { + bail!("merkle_tree: unmatch data size"); + } + // If the data size is small, using `rayon` would introduce more overhead. + let r = if leaf_data.len() >= ENTRY_SIZE * 8 { + leaf_data + .par_chunks_exact(ENTRY_SIZE) + .map(Sha3Algorithm::leaf) + .collect() + } else { + leaf_data + .chunks_exact(ENTRY_SIZE) + .map(Sha3Algorithm::leaf) + .collect() + }; + Ok(r) +} + +pub fn bytes_to_entries(size_bytes: u64) -> u64 { + if size_bytes % ENTRY_SIZE as u64 == 0 { + size_bytes / ENTRY_SIZE as u64 + } else { + size_bytes / ENTRY_SIZE as u64 + 1 + } +} + +fn entry_proof(top_proof: &FlowProof, sub_proof: &FlowProof) -> Result { + if top_proof.item() != sub_proof.root() { + bail!( + "top tree and sub tree mismatch: top_leaf={:?}, sub_root={:?}", + top_proof.item(), + sub_proof.root() + ); + } + let mut lemma = sub_proof.lemma().to_vec(); + let mut path = sub_proof.path().to_vec(); + assert!(lemma.pop().is_some()); + lemma.extend_from_slice(&top_proof.lemma()[1..]); + path.extend_from_slice(top_proof.path()); + Ok(FlowProof::new(lemma, path)) +} + +pub fn split_nodes(data_size: usize) -> Vec { + let (mut padded_chunks, chunks_next_pow2) = compute_padded_chunk_size(data_size); + let mut next_chunk_size = chunks_next_pow2; + + let mut nodes = vec![]; + while padded_chunks > 0 { + if padded_chunks >= next_chunk_size { + padded_chunks -= next_chunk_size; + nodes.push(next_chunk_size); + } + + next_chunk_size >>= 1; + } + + nodes +} + +pub fn tx_subtree_root_list_padded(data: &[u8]) -> Vec<(usize, DataRoot)> { + let mut root_list = Vec::new(); + let mut start_index = 0; + let nodes = split_nodes(data.len()); + + for &tree_size in nodes.iter() { + let end = start_index + tree_size * ENTRY_SIZE; + + let submerkle_root = if start_index >= data.len() { + sub_merkle_tree(&vec![0u8; tree_size * ENTRY_SIZE]) + .unwrap() + .root() + } else if end > data.len() { + let mut pad_data = data[start_index..].to_vec(); + pad_data.append(&mut vec![0u8; end - data.len()]); + sub_merkle_tree(&pad_data).unwrap().root() + } else { + sub_merkle_tree(&data[start_index..end]).unwrap().root() + }; + + root_list.push((log2_pow2(tree_size) + 1, submerkle_root.into())); + start_index = end; + } + + root_list +} diff --git a/node/storage/src/log_store/mod.rs b/node/storage/src/log_store/mod.rs new file mode 100644 index 0000000..a5a48a5 --- /dev/null +++ b/node/storage/src/log_store/mod.rs @@ -0,0 +1,227 @@ +use append_merkle::MerkleTreeInitialData; +use ethereum_types::H256; +use zgs_spec::{BYTES_PER_SEAL, SEALS_PER_LOAD}; +use shared_types::{ + Chunk, ChunkArray, ChunkArrayWithProof, ChunkWithProof, DataRoot, FlowRangeProof, Transaction, +}; + +use crate::error::Result; + +pub mod config; +mod flow_store; +mod load_chunk; +pub mod log_manager; +#[cfg(test)] +mod tests; +mod tx_store; + +/// The trait to read the transactions already appended to the log. +/// +/// Implementation Rationale: +/// If the stored chunk is large, we can store the proof together with the chunk. +pub trait LogStoreRead: LogStoreChunkRead { + /// Get a transaction by its global log sequence number. + fn get_tx_by_seq_number(&self, seq: u64) -> Result>; + + /// Get a transaction by the data root of its data. + fn get_tx_seq_by_data_root(&self, data_root: &DataRoot) -> Result>; + + fn get_tx_by_data_root(&self, data_root: &DataRoot) -> Result> { + match self.get_tx_seq_by_data_root(data_root)? { + Some(seq) => self.get_tx_by_seq_number(seq), + None => Ok(None), + } + } + + fn get_chunk_with_proof_by_tx_and_index( + &self, + tx_seq: u64, + index: usize, + ) -> Result>; + + fn get_chunks_with_proof_by_tx_and_index_range( + &self, + tx_seq: u64, + index_start: usize, + index_end: usize, + ) -> Result>; + + fn check_tx_completed(&self, tx_seq: u64) -> Result; + + fn next_tx_seq(&self) -> u64; + + fn get_sync_progress(&self) -> Result>; + + fn validate_range_proof(&self, tx_seq: u64, data: &ChunkArrayWithProof) -> Result; + + fn get_proof_at_root(&self, root: &DataRoot, index: u64, length: u64) + -> Result; + + /// Return flow root and length. + fn get_context(&self) -> Result<(DataRoot, u64)>; +} + +pub trait LogStoreChunkRead { + /// Get a data chunk by the transaction sequence number and the chunk offset in the transaction. + /// Accessing a single chunk is mostly used for mining. + fn get_chunk_by_tx_and_index(&self, tx_seq: u64, index: usize) -> Result>; + + /// Get a list of continuous chunks by the transaction sequence number and an index range (`index_end` excluded). + fn get_chunks_by_tx_and_index_range( + &self, + tx_seq: u64, + index_start: usize, + index_end: usize, + ) -> Result>; + + fn get_chunk_by_data_root_and_index( + &self, + data_root: &DataRoot, + index: usize, + ) -> Result>; + + fn get_chunks_by_data_root_and_index_range( + &self, + data_root: &DataRoot, + index_start: usize, + index_end: usize, + ) -> Result>; + + fn get_chunk_index_list(&self, tx_seq: u64) -> Result>; + + /// Accessing chunks by absolute flow index + fn get_chunk_by_flow_index(&self, index: u64, length: u64) -> Result>; +} + +pub trait LogStoreWrite: LogStoreChunkWrite { + /// Store a data entry metadata. + fn put_tx(&mut self, tx: Transaction) -> Result<()>; + + /// Finalize a transaction storage. + /// This will compute and the merkle tree, check the data root, and persist a part of the merkle + /// tree for future queries. + /// + /// This will return error if not all chunks are stored. But since this check can be expensive, + /// the caller is supposed to track chunk statuses and call this after storing all the chunks. + fn finalize_tx(&mut self, tx_seq: u64) -> Result<()>; + fn finalize_tx_with_hash(&mut self, tx_seq: u64, tx_hash: H256) -> Result; + + /// Store the progress of synced block number and its hash. + fn put_sync_progress(&self, progress: (u64, H256)) -> Result<()>; + + /// Revert the log state to a given tx seq. + /// This is needed when transactions are reverted because of chain reorg. + /// + /// Reverted transactions are returned in order. + fn revert_to(&mut self, tx_seq: u64) -> Result>; +} + +pub trait LogStoreChunkWrite { + /// Store data chunks of a data entry. + fn put_chunks(&mut self, tx_seq: u64, chunks: ChunkArray) -> Result<()>; + + fn put_chunks_with_tx_hash( + &mut self, + tx_seq: u64, + tx_hash: H256, + chunks: ChunkArray, + ) -> Result; + + /// Delete all chunks of a tx. + fn remove_all_chunks(&self, tx_seq: u64) -> Result<()>; +} + +pub trait LogChunkStore: LogStoreChunkRead + LogStoreChunkWrite + Send + Sync + 'static {} +impl LogChunkStore for T {} + +pub trait Store: + LogStoreRead + LogStoreWrite + LogStoreInner + config::Configurable + Send + Sync + 'static +{ +} +impl< + T: LogStoreRead + LogStoreWrite + LogStoreInner + config::Configurable + Send + Sync + 'static, + > Store for T +{ +} + +pub trait LogStoreInner { + fn flow(&self) -> &dyn Flow; + fn flow_mut(&mut self) -> &mut dyn Flow; +} + +pub struct MineLoadChunk { + pub loaded_chunk: [[u8; BYTES_PER_SEAL]; SEALS_PER_LOAD], + pub avalibilities: [bool; SEALS_PER_LOAD], +} + +impl Default for MineLoadChunk { + fn default() -> Self { + Self { + loaded_chunk: [[0u8; BYTES_PER_SEAL]; SEALS_PER_LOAD], + avalibilities: [false; SEALS_PER_LOAD], + } + } +} + +pub trait FlowRead { + /// Return the entries in the given range. If some data are missing, `Ok(None)` is returned. + fn get_entries(&self, index_start: u64, index_end: u64) -> Result>; + + /// Return the available entries in the given range. + /// The `ChunkArray` in the returned list are in order and they will not overlap or be adjacent. + /// + /// For simplicity, `index_start` and `index_end` must be at the batch boundaries. + fn get_available_entries(&self, index_start: u64, index_end: u64) -> Result>; + + fn get_chunk_root_list(&self) -> Result>; + + fn load_sealed_data(&self, chunk_index: u64) -> Result>; +} + +pub trait FlowWrite { + /// Append data to the flow. `start_index` is included in `ChunkArray`, so + /// it's possible to append arrays in any place. + /// Return the list of completed chunks. + fn append_entries(&mut self, data: ChunkArray) -> Result>; + + /// Remove all the entries after `start_index`. + /// This is used to remove deprecated data in case of chain reorg. + fn truncate(&mut self, start_index: u64) -> Result<()>; +} + +pub struct SealTask { + /// The index (in seal) of chunks + pub seal_index: u64, + /// An ephemeral version number to distinguish if revert happending + pub version: usize, + /// The data to be sealed + pub non_sealed_data: [u8; BYTES_PER_SEAL], +} + +#[derive(Debug)] +pub struct SealAnswer { + /// The index (in seal) of chunks + pub seal_index: u64, + /// An ephemeral version number to distinguish if revert happending + pub version: usize, + /// The data to be sealed + pub sealed_data: [u8; BYTES_PER_SEAL], + /// The miner Id + pub miner_id: H256, + /// The seal_context for this chunk + pub seal_context: H256, + pub context_end_seal: u64, +} + +pub trait FlowSeal { + /// Pull a seal chunk ready for sealing + /// Return the global index (in sector) and the data + fn pull_seal_chunk(&self, seal_index_max: usize) -> Result>>; + + /// Submit sealing result + + fn submit_seal_result(&mut self, answers: Vec) -> Result<()>; +} + +pub trait Flow: FlowRead + FlowWrite + FlowSeal {} +impl Flow for T {} diff --git a/node/storage/src/log_store/tests.rs b/node/storage/src/log_store/tests.rs new file mode 100644 index 0000000..8e3636b --- /dev/null +++ b/node/storage/src/log_store/tests.rs @@ -0,0 +1,207 @@ +use crate::log_store::log_manager::{ + data_to_merkle_leaves, sub_merkle_tree, tx_subtree_root_list_padded, LogConfig, LogManager, + PORA_CHUNK_SIZE, +}; +use crate::log_store::{LogStoreChunkRead, LogStoreChunkWrite, LogStoreRead, LogStoreWrite}; +use append_merkle::{Algorithm, AppendMerkleTree, MerkleTreeRead, Sha3Algorithm}; +use ethereum_types::H256; +use rand::random; +use shared_types::{compute_padded_chunk_size, ChunkArray, Transaction, CHUNK_SIZE}; +use std::cmp; + +#[test] +fn test_put_get() { + let config = LogConfig::default(); + let mut store = LogManager::memorydb(config.clone()).unwrap(); + let chunk_count = config.flow.batch_size + config.flow.batch_size / 2 - 1; + // Aligned with size. + let start_offset = 1024; + let data_size = CHUNK_SIZE * chunk_count; + let mut data = vec![0u8; data_size]; + for i in 0..chunk_count { + data[i * CHUNK_SIZE] = random(); + } + let (padded_chunks, _) = compute_padded_chunk_size(data_size); + let mut merkle = AppendMerkleTree::::new(vec![H256::zero()], 0, None); + merkle.append_list(data_to_merkle_leaves(&LogManager::padding(start_offset - 1)).unwrap()); + let mut data_padded = data.clone(); + data_padded.append(&mut vec![0u8; CHUNK_SIZE]); + merkle.append_list(data_to_merkle_leaves(&data_padded).unwrap()); + merkle.commit(Some(0)); + let tx_merkle = sub_merkle_tree(&data).unwrap(); + let tx = Transaction { + stream_ids: vec![], + size: data_size as u64, + data_merkle_root: tx_merkle.root().into(), + seq: 0, + data: vec![], + start_entry_index: start_offset as u64, + // TODO: This can come from `tx_merkle`. + merkle_nodes: tx_subtree_root_list_padded(&data), + }; + store.put_tx(tx.clone()).unwrap(); + for start_index in (0..chunk_count).step_by(PORA_CHUNK_SIZE) { + let end = cmp::min((start_index + PORA_CHUNK_SIZE) * CHUNK_SIZE, data.len()); + let chunk_array = ChunkArray { + data: data[start_index * CHUNK_SIZE..end].to_vec(), + start_index: start_index as u64, + }; + store.put_chunks(tx.seq, chunk_array.clone()).unwrap(); + } + store.finalize_tx(tx.seq).unwrap(); + + let chunk_array = ChunkArray { + data, + start_index: 0, + }; + assert_eq!(store.get_tx_by_seq_number(0).unwrap().unwrap(), tx); + for i in 0..chunk_count { + assert_eq!( + store.get_chunk_by_tx_and_index(tx.seq, i).unwrap().unwrap(), + chunk_array.chunk_at(i).unwrap() + ); + } + assert!(store + .get_chunk_by_tx_and_index(tx.seq, padded_chunks) + .is_err()); + + assert_eq!( + store + .get_chunks_by_tx_and_index_range(tx.seq, 0, chunk_count) + .unwrap() + .unwrap(), + chunk_array + ); + for i in 0..chunk_count { + let chunk_with_proof = store + .get_chunk_with_proof_by_tx_and_index(tx.seq, i) + .unwrap() + .unwrap(); + assert_eq!(chunk_with_proof.chunk, chunk_array.chunk_at(i).unwrap()); + assert_eq!( + chunk_with_proof.proof, + merkle.gen_proof(i + start_offset).unwrap() + ); + let r = chunk_with_proof.proof.validate::( + &Sha3Algorithm::leaf(&chunk_with_proof.chunk.0), + i + start_offset, + ); + assert!(r.is_ok(), "proof={:?} \n r={:?}", chunk_with_proof.proof, r); + assert!(merkle.check_root(&chunk_with_proof.proof.root())); + } + for i in (0..chunk_count).step_by(PORA_CHUNK_SIZE / 3) { + let end = std::cmp::min(i + PORA_CHUNK_SIZE, chunk_count); + let chunk_array_with_proof = store + .get_chunks_with_proof_by_tx_and_index_range(tx.seq, i, end) + .unwrap() + .unwrap(); + assert_eq!( + chunk_array_with_proof.chunks, + chunk_array.sub_array(i as u64, end as u64).unwrap() + ); + assert!(chunk_array_with_proof + .proof + .validate::( + &data_to_merkle_leaves(&chunk_array_with_proof.chunks.data).unwrap(), + i + start_offset + ) + .is_ok()); + } +} + +#[test] +fn test_root() { + for depth in 0..12 { + let n_chunk = 1 << depth; + let mut data = vec![0; n_chunk * CHUNK_SIZE]; + for i in 0..n_chunk { + data[i * CHUNK_SIZE] = random(); + } + let mt = sub_merkle_tree(&data).unwrap(); + println!("{:?} {}", mt.root(), hex::encode(mt.root())); + let append_mt = AppendMerkleTree::::new( + data_to_merkle_leaves(&data).unwrap(), + 0, + None, + ); + assert_eq!(mt.root(), append_mt.root().0); + } +} + +#[test] +fn test_multi_tx() { + let mut store = create_store(); + put_tx(&mut store, 3, 0); + put_tx(&mut store, 3, 1); + put_tx(&mut store, 5, 2); +} + +#[test] +fn test_revert() { + let mut store = create_store(); + put_tx(&mut store, 1, 0); + store.revert_to(0u64.wrapping_sub(1)).unwrap(); + put_tx(&mut store, 1, 0); + put_tx(&mut store, 1, 1); + store.revert_to(0).unwrap(); + put_tx(&mut store, 1, 1); + + // Test revert across the last chunk. + put_tx(&mut store, 1024 + 1, 2); + store.revert_to(1).unwrap(); + + // Test with chunk boundary within a tx. + put_tx(&mut store, 256, 2); + put_tx(&mut store, 512 + 1, 3); + put_tx(&mut store, 1, 4); + store.revert_to(1).unwrap(); + put_tx(&mut store, 1, 2); +} + +#[test] +fn test_put_tx() { + for i in 0..12 { + let chunk_count = 0xF << i; + let mut store = create_store(); + put_tx(&mut store, chunk_count, 0); + } +} + +fn create_store() -> LogManager { + let config = LogConfig::default(); + + LogManager::memorydb(config).unwrap() +} + +fn put_tx(store: &mut LogManager, chunk_count: usize, seq: u64) { + let data_size = CHUNK_SIZE * chunk_count; + let mut data = vec![0u8; data_size]; + for i in 0..chunk_count { + data[i * CHUNK_SIZE..(i * CHUNK_SIZE + 8)].copy_from_slice(&(seq + 1).to_be_bytes()); + } + let tx_merkle = sub_merkle_tree(&data).unwrap(); + let merkle_nodes = tx_subtree_root_list_padded(&data); + let flow_len = store.get_context().unwrap().1; + let first_subtree_size = 1 << (merkle_nodes.first().unwrap().0 - 1); + let start_entry_index = ((flow_len - 1) / first_subtree_size + 1) * first_subtree_size; + let tx = Transaction { + stream_ids: vec![], + size: data_size as u64, + data_merkle_root: tx_merkle.root().into(), + seq, + data: vec![], + start_entry_index, + // TODO: This can come from `tx_merkle`. + merkle_nodes, + }; + store.put_tx(tx.clone()).unwrap(); + for start_index in (0..chunk_count).step_by(PORA_CHUNK_SIZE) { + let end = cmp::min((start_index + PORA_CHUNK_SIZE) * CHUNK_SIZE, data.len()); + let chunk_array = ChunkArray { + data: data[start_index * CHUNK_SIZE..end].to_vec(), + start_index: start_index as u64, + }; + store.put_chunks(tx.seq, chunk_array.clone()).unwrap(); + } + store.finalize_tx(tx.seq).unwrap(); +} diff --git a/node/storage/src/log_store/tx_store.rs b/node/storage/src/log_store/tx_store.rs new file mode 100644 index 0000000..1da6731 --- /dev/null +++ b/node/storage/src/log_store/tx_store.rs @@ -0,0 +1,276 @@ +use crate::error::Error; +use crate::log_store::log_manager::{ + data_to_merkle_leaves, sub_merkle_tree, COL_MISC, COL_TX, COL_TX_COMPLETED, + COL_TX_DATA_ROOT_INDEX, ENTRY_SIZE, PORA_CHUNK_SIZE, +}; +use crate::{try_option, LogManager, ZgsKeyValueDB}; +use anyhow::{anyhow, Result}; +use append_merkle::{AppendMerkleTree, MerkleTreeRead, Sha3Algorithm}; +use ethereum_types::H256; +use merkle_light::merkle::log2_pow2; +use shared_types::{DataRoot, Transaction}; +use ssz::{Decode, Encode}; +use std::cmp; +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; +use tracing::{error, instrument}; + +const LOG_SYNC_PROGRESS_KEY: &str = "log_sync_progress"; +const NEXT_TX_KEY: &str = "next_tx_seq"; + +pub struct TransactionStore { + kvdb: Arc, + /// This is always updated before writing the database to ensure no intermediate states. + next_tx_seq: AtomicU64, +} + +impl TransactionStore { + pub fn new(kvdb: Arc) -> Result { + let next_tx_seq = kvdb + .get(COL_TX, NEXT_TX_KEY.as_bytes())? + .map(|a| decode_tx_seq(&a)) + .unwrap_or(Ok(0))?; + Ok(Self { + kvdb, + next_tx_seq: AtomicU64::new(next_tx_seq), + }) + } + + #[instrument(skip(self))] + /// Return `Ok(Some(tx_seq))` if a previous transaction has the same tx root. + pub fn put_tx(&self, mut tx: Transaction) -> Result> { + let mut db_tx = self.kvdb.transaction(); + + if !tx.data.is_empty() { + tx.size = tx.data.len() as u64; + let mut padded_data = tx.data.clone(); + let extra = tx.data.len() % ENTRY_SIZE; + if extra != 0 { + padded_data.append(&mut vec![0u8; ENTRY_SIZE - extra]); + } + let data_root = sub_merkle_tree(&padded_data)?.root(); + tx.data_merkle_root = data_root.into(); + } + + db_tx.put(COL_TX, &tx.seq.to_be_bytes(), &tx.as_ssz_bytes()); + db_tx.put(COL_TX, NEXT_TX_KEY.as_bytes(), &(tx.seq + 1).to_be_bytes()); + let old_tx_seq_list = self.get_tx_seq_list_by_data_root(&tx.data_merkle_root)?; + // The list is sorted, and we always call `put_tx` in order. + assert!(old_tx_seq_list + .last() + .map(|last| *last < tx.seq) + .unwrap_or(true)); + let mut new_tx_seq_list = old_tx_seq_list.clone(); + new_tx_seq_list.push(tx.seq); + db_tx.put( + COL_TX_DATA_ROOT_INDEX, + tx.data_merkle_root.as_bytes(), + &new_tx_seq_list.as_ssz_bytes(), + ); + self.next_tx_seq.store(tx.seq + 1, Ordering::SeqCst); + self.kvdb.write(db_tx)?; + Ok(old_tx_seq_list) + } + + pub fn get_tx_by_seq_number(&self, seq: u64) -> Result> { + if seq >= self.next_tx_seq() { + return Ok(None); + } + let value = try_option!(self.kvdb.get(COL_TX, &seq.to_be_bytes())?); + let tx = Transaction::from_ssz_bytes(&value).map_err(Error::from)?; + Ok(Some(tx)) + } + + pub fn remove_tx_after(&self, min_seq: u64) -> Result> { + let mut removed_txs = Vec::new(); + let max_seq = self.next_tx_seq(); + let mut db_tx = self.kvdb.transaction(); + let mut modified_merkle_root_map = HashMap::new(); + for seq in min_seq..max_seq { + let Some(tx) = self.get_tx_by_seq_number(seq)? else { + error!(?seq, ?max_seq, "Transaction missing before the end"); + break; + }; + db_tx.delete(COL_TX, &seq.to_be_bytes()); + db_tx.delete(COL_TX_COMPLETED, &seq.to_be_bytes()); + // We only remove tx when the blockchain reorgs. + // If a tx is reverted, all data after it will also be reverted, so we call remove + // all indices after it. + let tx_seq_list = match modified_merkle_root_map.entry(tx.data_merkle_root) { + Entry::Occupied(e) => e.into_mut(), + Entry::Vacant(e) => { + e.insert(self.get_tx_seq_list_by_data_root(&tx.data_merkle_root)?) + } + }; + tx_seq_list.retain(|e| *e < seq); + removed_txs.push(tx); + } + for (merkle_root, tx_seq_list) in modified_merkle_root_map { + if tx_seq_list.is_empty() { + db_tx.delete(COL_TX_DATA_ROOT_INDEX, merkle_root.as_bytes()); + } else { + db_tx.put( + COL_TX_DATA_ROOT_INDEX, + merkle_root.as_bytes(), + &tx_seq_list.as_ssz_bytes(), + ); + } + } + db_tx.put(COL_TX, NEXT_TX_KEY.as_bytes(), &min_seq.to_be_bytes()); + self.next_tx_seq.store(min_seq, Ordering::SeqCst); + self.kvdb.write(db_tx)?; + Ok(removed_txs) + } + + pub fn get_tx_seq_list_by_data_root(&self, data_root: &DataRoot) -> Result> { + let value = match self + .kvdb + .get(COL_TX_DATA_ROOT_INDEX, data_root.as_bytes())? + { + Some(v) => v, + None => return Ok(Vec::new()), + }; + Ok(Vec::::from_ssz_bytes(&value).map_err(Error::from)?) + } + + pub fn get_first_tx_seq_by_data_root(&self, data_root: &DataRoot) -> Result> { + let value = try_option!(self + .kvdb + .get(COL_TX_DATA_ROOT_INDEX, data_root.as_bytes())?); + let seq_list = Vec::::from_ssz_bytes(&value).map_err(Error::from)?; + Ok(seq_list.first().cloned()) + } + + #[instrument(skip(self))] + pub fn finalize_tx(&self, tx_seq: u64) -> Result<()> { + Ok(self + .kvdb + .put(COL_TX_COMPLETED, &tx_seq.to_be_bytes(), &[0])?) + } + + pub fn check_tx_completed(&self, tx_seq: u64) -> Result { + Ok(self.kvdb.has_key(COL_TX_COMPLETED, &tx_seq.to_be_bytes())?) + } + + pub fn next_tx_seq(&self) -> u64 { + self.next_tx_seq.load(Ordering::SeqCst) + } + + #[instrument(skip(self))] + pub fn put_progress(&self, progress: (u64, H256)) -> Result<()> { + Ok(self.kvdb.put( + COL_MISC, + LOG_SYNC_PROGRESS_KEY.as_bytes(), + &progress.as_ssz_bytes(), + )?) + } + + #[instrument(skip(self))] + pub fn get_progress(&self) -> Result> { + Ok(Some( + <(u64, H256)>::from_ssz_bytes(&try_option!(self + .kvdb + .get(COL_MISC, LOG_SYNC_PROGRESS_KEY.as_bytes())?)) + .map_err(Error::from)?, + )) + } + + /// Build the merkle tree at `pora_chunk_index` with the data before (including) `tx_seq`. + /// This first rebuild the tree with the tx root nodes lists by repeatedly checking previous + /// until we reach the start of this chunk. + /// + /// Note that this can only be called with the last chunk after some transaction is committed, + /// otherwise the start of this chunk might be within some tx subtree and this will panic. + // TODO(zz): Fill the last chunk with data. + pub fn rebuild_last_chunk_merkle( + &self, + pora_chunk_index: usize, + mut tx_seq: u64, + ) -> Result> { + let last_chunk_start_index = pora_chunk_index as u64 * PORA_CHUNK_SIZE as u64; + let mut tx_list = Vec::new(); + // Find the first tx within the last chunk. + loop { + let tx = self.get_tx_by_seq_number(tx_seq)?.expect("tx not removed"); + match tx.start_entry_index.cmp(&last_chunk_start_index) { + cmp::Ordering::Greater => { + tx_list.push((tx_seq, tx.merkle_nodes)); + } + cmp::Ordering::Equal => { + tx_list.push((tx_seq, tx.merkle_nodes)); + break; + } + cmp::Ordering::Less => { + // The transaction data crosses a chunk, so we need to find the subtrees + // within the last chunk. + let mut start_index = tx.start_entry_index; + let mut first_index = None; + for (i, (depth, _)) in tx.merkle_nodes.iter().enumerate() { + start_index += 1 << (depth - 1); + if start_index == last_chunk_start_index { + first_index = Some(i + 1); + break; + } + } + // Some means some subtree ends at the chunk boundary. + // None means there are padding data between the tx data and the boundary, + // so no data belongs to the last chunk. + if let Some(first_index) = first_index { + if first_index != tx.merkle_nodes.len() { + tx_list.push((tx_seq, tx.merkle_nodes[first_index..].to_vec())); + } else { + // If the last subtree ends at the chunk boundary, we also do not need + // to add data of this tx to the last chunk. + // This is only possible if the last chunk is empty, because otherwise + // we should have entered the `Equal` condition before and + // have broken the loop. + assert!(tx_list.is_empty()); + } + } + break; + } + } + if tx_seq == 0 { + break; + } else { + tx_seq -= 1; + } + } + let mut merkle = if last_chunk_start_index == 0 { + // The first entry hash is initialized as zero. + AppendMerkleTree::::new_with_depth( + vec![H256::zero()], + log2_pow2(PORA_CHUNK_SIZE) + 1, + None, + ) + } else { + AppendMerkleTree::::new_with_depth( + vec![], + log2_pow2(PORA_CHUNK_SIZE) + 1, + None, + ) + }; + for (tx_seq, subtree_list) in tx_list.into_iter().rev() { + // Pad the tx. After the first subtree is padded, other subtrees should be aligned. + let first_subtree = 1 << (subtree_list[0].0 - 1); + if merkle.leaves() % first_subtree != 0 { + let pad_len = + cmp::min(first_subtree, PORA_CHUNK_SIZE) - (merkle.leaves() % first_subtree); + merkle.append_list(data_to_merkle_leaves(&LogManager::padding(pad_len))?); + } + // Since we are building the last merkle with a given last tx_seq, it's ensured + // that appending subtrees will not go beyond the max size. + merkle.append_subtree_list(subtree_list)?; + merkle.commit(Some(tx_seq)); + } + Ok(merkle) + } +} + +fn decode_tx_seq(data: &[u8]) -> Result { + Ok(u64::from_be_bytes( + data.try_into().map_err(|e| anyhow!("{:?}", e))?, + )) +} diff --git a/node/sync/Cargo.toml b/node/sync/Cargo.toml new file mode 100644 index 0000000..d2f800a --- /dev/null +++ b/node/sync/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "sync" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { version = "1.0.58", features = ["backtrace"] } +append_merkle = { path = "../../common/append_merkle" } +channel = { path = "../../common/channel" } +file_location_cache = { path = "../file_location_cache" } +log_entry_sync = { path = "../log_entry_sync" } +network = { path = "../network" } +rand = "0.8.5" +shared_types = { path = "../shared_types" } +storage = { path = "../storage" } +storage-async = { path = "../storage-async" } +task_executor = { path = "../../common/task_executor" } +tokio = { version = "1.19.2", features = ["full"] } +tracing = "0.1.35" +eth2_ssz = "0.4.0" +serde = { version = "1.0.137", features = ["derive"] } +duration-str = "0.5.1" + +[dev-dependencies] +merkle_light = { path = "../../common/merkle_light" } + +[dependencies.libp2p] +version = "0.45.1" +default-features = false +features = ["websocket", "identify", "mplex", "yamux", "noise", "gossipsub", "dns-tokio", "tcp-tokio", "plaintext", "secp256k1"] \ No newline at end of file diff --git a/node/sync/src/auto_sync/manager.rs b/node/sync/src/auto_sync/manager.rs new file mode 100644 index 0000000..4f8e93c --- /dev/null +++ b/node/sync/src/auto_sync/manager.rs @@ -0,0 +1,667 @@ +use super::sync_store::SyncStore; +use crate::{controllers::SyncState, Config, SyncRequest, SyncResponse, SyncSender}; +use anyhow::{bail, Result}; +use log_entry_sync::LogSyncEvent; +use std::sync::{ + atomic::{AtomicU64, Ordering}, + Arc, +}; +use std::time::Duration; +use storage_async::Store; +use task_executor::TaskExecutor; +use tokio::sync::broadcast::{error::RecvError, Receiver}; +use tokio::time::sleep; + +const INTERVAL_CATCHUP: Duration = Duration::from_millis(1); +const INTERVAL: Duration = Duration::from_secs(1); +const INTERVAL_ERROR: Duration = Duration::from_secs(10); + +/// Manager to synchronize files among storage nodes automatically. +/// +/// Generally, most files could be synchronized among storage nodes. However, a few +/// files may be unavailable on all storage nodes, e.g. +/// +/// 1. File not uploaded by user in time. +/// 2. File removed due to blockchain reorg, and user do not upload again. +/// +/// So, there are 2 workers to synchronize files in parallel: +/// +/// 1. Synchronize announced files in sequence. If any file unavailable, store it into db. +/// 2. Synchronize the missed files in sequential synchronization. +#[derive(Clone)] +pub struct Manager { + config: Config, + + /// The next `tx_seq` to sync in sequence. + next_tx_seq: Arc, + + /// The maximum `tx_seq` to sync in sequence, `u64::MAX` means unlimited. + /// Generally, it is updated when file announcement received. + max_tx_seq: Arc, + + /// The last reverted transaction seq, `u64::MAX` means no tx reverted. + /// Generally, it is updated when transaction reverted. + reverted_tx_seq: Arc, + + store: Store, + sync_store: SyncStore, + + /// Used to interact with sync service for the current file in sync. + sync_send: SyncSender, +} + +impl Manager { + pub async fn new(store: Store, sync_send: SyncSender, config: Config) -> Result { + let sync_store = SyncStore::new(store.clone()); + + let (next_tx_seq, max_tx_seq) = sync_store.get_tx_seq_range().await?; + let next_tx_seq = next_tx_seq.unwrap_or(0); + let max_tx_seq = max_tx_seq.unwrap_or(u64::MAX); + + Ok(Self { + config, + next_tx_seq: Arc::new(AtomicU64::new(next_tx_seq)), + max_tx_seq: Arc::new(AtomicU64::new(max_tx_seq)), + reverted_tx_seq: Arc::new(AtomicU64::new(u64::MAX)), + store, + sync_store, + sync_send, + }) + } + + pub fn spwn(&self, executor: &TaskExecutor, receiver: Receiver) { + executor.spawn( + self.clone().monitor_reorg(receiver), + "sync_manager_reorg_monitor", + ); + + executor.spawn(self.clone().start_sync(), "sync_manager_sequential_syncer"); + + executor.spawn( + self.clone().start_sync_pending_txs(), + "sync_manager_pending_syncer", + ); + } + + fn set_reverted(&self, tx_seq: u64) -> bool { + if tx_seq >= self.reverted_tx_seq.load(Ordering::Relaxed) { + return false; + } + + self.reverted_tx_seq.store(tx_seq, Ordering::Relaxed); + + true + } + + fn handle_on_reorg(&self) -> Option { + let reverted_tx_seq = self.reverted_tx_seq.load(Ordering::Relaxed); + + // no reorg happened + if reverted_tx_seq == u64::MAX { + return None; + } + + self.reverted_tx_seq.store(u64::MAX, Ordering::Relaxed); + + // reorg happened, but no impact on file sync + let next_tx_seq = self.next_tx_seq.load(Ordering::Relaxed); + if reverted_tx_seq > next_tx_seq { + return None; + } + + // handles on reorg + info!(%reverted_tx_seq, %next_tx_seq, "Transaction reverted"); + + // re-sync files from the reverted tx seq + self.next_tx_seq.store(reverted_tx_seq, Ordering::Relaxed); + + Some(next_tx_seq) + } + + pub async fn update_on_announcement(&self, announced_tx_seq: u64) { + // new file announced + let max_tx_seq = self.max_tx_seq.load(Ordering::Relaxed); + if max_tx_seq == u64::MAX || announced_tx_seq > max_tx_seq { + match self.sync_store.set_max_tx_seq(announced_tx_seq).await { + Ok(()) => self.max_tx_seq.store(announced_tx_seq, Ordering::Relaxed), + Err(e) => error!(%e, "Failed to set max_tx_seq in store"), + }; + return; + } + + // already wait for sequential sync + if announced_tx_seq >= self.next_tx_seq.load(Ordering::Relaxed) { + return; + } + + // otherwise, mark tx as ready for sync + if let Err(e) = self.sync_store.upgrade_tx_to_ready(announced_tx_seq).await { + error!(%e, "Failed to promote announced tx to ready"); + } + } + + async fn move_forward(&self, pending: bool) -> Result { + let tx_seq = self.next_tx_seq.load(Ordering::Relaxed); + if tx_seq > self.max_tx_seq.load(Ordering::Relaxed) { + return Ok(false); + } + + // put the tx into pending list + if pending && self.sync_store.add_pending_tx(tx_seq).await? { + debug!(%tx_seq, "Pending tx added"); + } + + let next_tx_seq = tx_seq + 1; + self.sync_store.set_next_tx_seq(next_tx_seq).await?; + self.next_tx_seq.store(next_tx_seq, Ordering::Relaxed); + + Ok(true) + } + + /// Returns whether file sync in progress but no peers found + async fn sync_tx(&self, tx_seq: u64) -> Result { + // tx not available yet + if self.store.get_tx_by_seq_number(tx_seq).await?.is_none() { + return Ok(false); + } + + // get sync state to handle in advance + let state = match self + .sync_send + .request(SyncRequest::SyncStatus { tx_seq }) + .await? + { + SyncResponse::SyncStatus { status } => status, + _ => bail!("Invalid sync response type"), + }; + trace!(?tx_seq, ?state, "sync_tx tx status"); + + // notify service to sync file if not started or failed + if matches!(state, None | Some(SyncState::Failed { .. })) { + match self + .sync_send + .request(SyncRequest::SyncFile { tx_seq }) + .await? + { + SyncResponse::SyncFile { err } if err.is_empty() => return Ok(false), + SyncResponse::SyncFile { err } => bail!("Failed to sync file: {:?}", err), + _ => bail!("Invalid sync response type"), + } + } + + if matches!(state, Some(SyncState::FindingPeers { since, .. }) if since.elapsed() > self.config.find_peer_timeout) + { + // no peers found for a long time + self.terminate_file_sync(tx_seq, false).await; + Ok(true) + } else { + // otherwise, continue to wait for file sync that already in progress + Ok(false) + } + } + + async fn terminate_file_sync(&self, tx_seq: u64, is_reverted: bool) { + if let Err(err) = self + .sync_send + .request(SyncRequest::TerminateFileSync { + tx_seq, + is_reverted, + }) + .await + { + // just log and go ahead for any error, e.g. timeout + error!(%err, %tx_seq, "Failed to terminate file sync"); + } + } + + /// Starts to monitor reorg and handle on transaction reverted. + async fn monitor_reorg(self, mut receiver: Receiver) { + info!("Start to monitor reorg"); + + loop { + match receiver.recv().await { + Ok(LogSyncEvent::ReorgDetected { .. }) => {} + Ok(LogSyncEvent::Reverted { tx_seq }) => { + // requires to re-sync files since transaction and files removed in storage + self.set_reverted(tx_seq); + } + Ok(LogSyncEvent::TxSynced { .. }) => {} //No need to handle synced tx in reorg + Err(RecvError::Closed) => { + // program terminated + info!("Completed to monitor reorg"); + return; + } + Err(RecvError::Lagged(lagged)) => { + // Generally, such error should not happen since confirmed block + // reorg rarely happen, and the buffer size of broadcast channel + // is big enough. + error!(%lagged, "Failed to receive reverted tx (Lagged)"); + } + } + } + } + + /// Starts to synchronize files in sequence. + async fn start_sync(self) { + info!( + "Start to sync files periodically, next = {}, max = {}", + self.next_tx_seq.load(Ordering::Relaxed), + self.max_tx_seq.load(Ordering::Relaxed) + ); + + loop { + // handles reorg before file sync + if let Some(tx_seq) = self.handle_on_reorg() { + // request sync service to terminate the file sync immediately + self.terminate_file_sync(tx_seq, true).await; + } + + // sync file + let sync_result = self.sync_once().await; + let next_tx_seq = self.next_tx_seq.load(Ordering::Relaxed); + match sync_result { + Ok(true) => { + debug!(%next_tx_seq, "Completed to sync file"); + sleep(INTERVAL_CATCHUP).await; + } + Ok(false) => { + trace!(%next_tx_seq, "File in sync or log entry unavailable"); + sleep(INTERVAL).await; + } + Err(err) => { + warn!(%err, %next_tx_seq, "Failed to sync file"); + sleep(INTERVAL_ERROR).await; + } + } + } + } + + async fn sync_once(&self) -> Result { + // already sync to the latest file + let next_tx_seq = self.next_tx_seq.load(Ordering::Relaxed); + if next_tx_seq > self.max_tx_seq.load(Ordering::Relaxed) { + return Ok(false); + } + + // already finalized + if self.store.check_tx_completed(next_tx_seq).await? { + self.move_forward(false).await?; + return Ok(true); + } + + // try sync tx + let no_peer_timeout = self.sync_tx(next_tx_seq).await?; + + // put tx to pending list if no peers found for a long time + if no_peer_timeout { + self.move_forward(true).await?; + } + + Ok(no_peer_timeout) + } + + /// Starts to synchronize pending files that unavailable during sequential synchronization. + async fn start_sync_pending_txs(self) { + info!("Start to sync pending files"); + + let mut tx_seq = 0; + let mut next = true; + + loop { + if next { + match self.sync_store.random_tx().await { + Ok(Some(seq)) => tx_seq = seq, + Ok(None) => { + trace!("No pending file to sync"); + sleep(INTERVAL).await; + continue; + } + Err(err) => { + warn!(%err, "Failed to pick pending file to sync"); + sleep(INTERVAL_ERROR).await; + continue; + } + } + } + + match self.sync_pending_tx(tx_seq).await { + Ok(true) => { + debug!(%tx_seq, "Completed to sync pending file"); + sleep(INTERVAL_CATCHUP).await; + next = true; + } + Ok(false) => { + trace!(%tx_seq, "Pending file in sync or tx unavailable"); + sleep(INTERVAL).await; + next = false; + } + Err(err) => { + warn!(%err, %tx_seq, "Failed to sync pending file"); + sleep(INTERVAL_ERROR).await; + next = false; + } + } + } + } + + async fn sync_pending_tx(&self, tx_seq: u64) -> Result { + // already finalized + if self.store.check_tx_completed(tx_seq).await? { + self.sync_store.remove_tx(tx_seq).await?; + return Ok(true); + } + + // try sync tx + let no_peer_timeout = self.sync_tx(tx_seq).await?; + + // downgrade if no peers found for a long time + if no_peer_timeout && self.sync_store.downgrade_tx_to_pending(tx_seq).await? { + debug!(%tx_seq, "No peers found for pending file and downgraded"); + } + + Ok(no_peer_timeout) + } +} + +#[cfg(test)] +mod tests { + use std::{ + ops::Sub, + sync::atomic::Ordering, + time::{Duration, Instant}, + }; + + use channel::{test_util::TestReceiver, Channel}; + use tokio::sync::mpsc::error::TryRecvError; + + use crate::{ + auto_sync::sync_store::SyncStore, + controllers::SyncState, + test_util::{create_2_store, tests::TestStoreRuntime}, + Config, SyncMessage, SyncRequest, SyncResponse, + }; + + use super::Manager; + + async fn new_manager( + runtime: &TestStoreRuntime, + next_tx_seq: u64, + max_tx_seq: u64, + ) -> ( + Manager, + TestReceiver, + ) { + let sync_store = SyncStore::new(runtime.store.clone()); + sync_store.set_next_tx_seq(next_tx_seq).await.unwrap(); + if max_tx_seq < u64::MAX { + sync_store.set_max_tx_seq(max_tx_seq).await.unwrap(); + } + + let (sync_send, sync_recv) = Channel::unbounded(); + let manager = Manager::new(runtime.store.clone(), sync_send, Config::default()) + .await + .unwrap(); + (manager, sync_recv.into()) + } + + #[tokio::test] + async fn test_manager_init_values() { + let runtime = TestStoreRuntime::default(); + let (manager, _sync_recv) = new_manager(&runtime, 4, 12).await; + + assert_eq!(manager.next_tx_seq.load(Ordering::Relaxed), 4); + assert_eq!(manager.max_tx_seq.load(Ordering::Relaxed), 12); + assert_eq!(manager.reverted_tx_seq.load(Ordering::Relaxed), u64::MAX); + } + + #[tokio::test] + async fn test_manager_set_reverted() { + let runtime = TestStoreRuntime::default(); + let (manager, _sync_recv) = new_manager(&runtime, 4, 12).await; + + // reverted to tx 5 + assert!(manager.set_reverted(5)); + assert_eq!(manager.reverted_tx_seq.load(Ordering::Relaxed), 5); + + // no effect if tx 6 reverted again + assert!(!manager.set_reverted(6)); + assert_eq!(manager.reverted_tx_seq.load(Ordering::Relaxed), 5); + + // overwrite tx 5 if tx 3 reverted + assert!(manager.set_reverted(3)); + assert_eq!(manager.reverted_tx_seq.load(Ordering::Relaxed), 3); + } + + #[tokio::test] + async fn test_manager_handle_reorg() { + let runtime = TestStoreRuntime::default(); + let (manager, _sync_recv) = new_manager(&runtime, 4, 12).await; + + // no effect if not reverted + assert_eq!(manager.handle_on_reorg(), None); + assert_eq!(manager.reverted_tx_seq.load(Ordering::Relaxed), u64::MAX); + assert_eq!(manager.next_tx_seq.load(Ordering::Relaxed), 4); + + // tx 5 reverted, but sync in future + assert!(manager.set_reverted(5)); + assert_eq!(manager.handle_on_reorg(), None); + assert_eq!(manager.reverted_tx_seq.load(Ordering::Relaxed), u64::MAX); + assert_eq!(manager.next_tx_seq.load(Ordering::Relaxed), 4); + + // tx 3 reverted, should terminate tx 4 and re-sync files since tx 3 + assert!(manager.set_reverted(3)); + assert_eq!(manager.handle_on_reorg(), Some(4)); + assert_eq!(manager.reverted_tx_seq.load(Ordering::Relaxed), u64::MAX); + assert_eq!(manager.next_tx_seq.load(Ordering::Relaxed), 3); + } + + #[tokio::test] + async fn test_manager_update_on_announcement() { + let runtime = TestStoreRuntime::default(); + let (manager, _sync_recv) = new_manager(&runtime, 4, 12).await; + + // no effect if tx 10 announced + manager.update_on_announcement(10).await; + assert_eq!(manager.next_tx_seq.load(Ordering::Relaxed), 4); + assert_eq!(manager.max_tx_seq.load(Ordering::Relaxed), 12); + + // `max_tx_seq` enlarged if tx 20 announced + manager.update_on_announcement(20).await; + assert_eq!(manager.next_tx_seq.load(Ordering::Relaxed), 4); + assert_eq!(manager.max_tx_seq.load(Ordering::Relaxed), 20); + + // no effect if announced for a non-pending tx + manager.update_on_announcement(2).await; + assert_eq!(manager.next_tx_seq.load(Ordering::Relaxed), 4); + assert_eq!(manager.max_tx_seq.load(Ordering::Relaxed), 20); + assert_eq!(manager.sync_store.random_tx().await.unwrap(), None); + + // pending tx upgraded if announcement received + assert!(manager.sync_store.add_pending_tx(1).await.unwrap()); + assert!(manager.sync_store.add_pending_tx(2).await.unwrap()); + manager.update_on_announcement(2).await; + assert_eq!(manager.sync_store.random_tx().await.unwrap(), Some(2)); + } + + #[tokio::test] + async fn test_manager_move_forward() { + let runtime = TestStoreRuntime::default(); + let (manager, _sync_recv) = new_manager(&runtime, 4, 12).await; + + // move forward from 4 to 5 + assert!(manager.move_forward(false).await.unwrap()); + assert_eq!(manager.next_tx_seq.load(Ordering::Relaxed), 5); + assert_eq!(manager.max_tx_seq.load(Ordering::Relaxed), 12); + assert_eq!( + manager.sync_store.get_tx_seq_range().await.unwrap(), + (Some(5), Some(12)) + ); + + // move forward and add tx 5 to pending list + assert!(manager.move_forward(true).await.unwrap()); + assert_eq!(manager.next_tx_seq.load(Ordering::Relaxed), 6); + assert_eq!(manager.max_tx_seq.load(Ordering::Relaxed), 12); + assert_eq!( + manager.sync_store.get_tx_seq_range().await.unwrap(), + (Some(6), Some(12)) + ); + assert_eq!(manager.sync_store.random_tx().await.unwrap(), Some(5)); + } + + #[tokio::test] + async fn test_manager_move_forward_failed() { + let runtime = TestStoreRuntime::default(); + let (manager, _sync_recv) = new_manager(&runtime, 5, 5).await; + + // 5 -> 6 + assert!(manager.move_forward(false).await.unwrap()); + assert_eq!(manager.next_tx_seq.load(Ordering::Relaxed), 6); + assert_eq!(manager.max_tx_seq.load(Ordering::Relaxed), 5); + + // cannot move forward anymore + assert!(!manager.move_forward(false).await.unwrap()); + assert_eq!(manager.next_tx_seq.load(Ordering::Relaxed), 6); + assert_eq!(manager.max_tx_seq.load(Ordering::Relaxed), 5); + assert_eq!( + manager.sync_store.get_tx_seq_range().await.unwrap(), + (Some(6), Some(5)) + ); + } + + #[tokio::test] + async fn test_manager_sync_tx_unavailable() { + let runtime = TestStoreRuntime::default(); + let (manager, _sync_recv) = new_manager(&runtime, 4, 12).await; + + assert!(!manager.sync_tx(4).await.unwrap()); + } + + #[tokio::test] + async fn test_manager_sync_tx_status_none() { + let (_, store, _, _) = create_2_store(vec![1314, 1324]); + let runtime = TestStoreRuntime::new(store); + let (manager, mut sync_recv) = new_manager(&runtime, 1, 5).await; + + let (_, sync_result) = tokio::join!( + sync_recv.expect_responses(vec![ + SyncResponse::SyncStatus { status: None }, + // cause to file sync started + SyncResponse::SyncFile { err: String::new() }, + ]), + manager.sync_tx(1) + ); + assert!(!sync_result.unwrap()); + assert!(matches!(sync_recv.try_recv(), Err(TryRecvError::Empty))); + } + + #[tokio::test] + async fn test_manager_sync_tx_in_progress() { + let (_, store, _, _) = create_2_store(vec![1314, 1324]); + let runtime = TestStoreRuntime::new(store); + let (manager, mut sync_recv) = new_manager(&runtime, 1, 5).await; + + let (_, sync_result) = tokio::join!( + // unnecessary to start file sync again + sync_recv.expect_response(SyncResponse::SyncStatus { + status: Some(SyncState::ConnectingPeers) + }), + manager.sync_tx(1) + ); + assert!(!sync_result.unwrap()); + assert!(matches!(sync_recv.try_recv(), Err(TryRecvError::Empty))); + } + + async fn expect_no_peer_found( + sync_recv: &mut TestReceiver, + ) { + let responses = vec![ + // no peers for file sync for a long time + SyncResponse::SyncStatus { + status: Some(SyncState::FindingPeers { + since: Instant::now().sub(Duration::from_secs(10000)), + updated: Instant::now(), + }), + }, + // required to terminate the file sync + SyncResponse::TerminateFileSync { count: 1 }, + ]; + sync_recv.expect_responses(responses).await + } + + #[tokio::test] + async fn test_manager_sync_tx_no_peer_found() { + let (_, store, _, _) = create_2_store(vec![1314, 1324]); + let runtime = TestStoreRuntime::new(store); + let (manager, mut sync_recv) = new_manager(&runtime, 1, 5).await; + + let (_, sync_result) = + tokio::join!(expect_no_peer_found(&mut sync_recv), manager.sync_tx(1)); + assert!(sync_result.unwrap()); + assert!(matches!(sync_recv.try_recv(), Err(TryRecvError::Empty))); + } + + #[tokio::test] + async fn test_manager_sync_once_already_latest() { + let runtime = TestStoreRuntime::default(); + let (manager, _sync_recv) = new_manager(&runtime, 6, 5).await; + + assert!(!manager.sync_once().await.unwrap()); + } + + #[tokio::test] + async fn test_manager_sync_once_finalized() { + let (_, store, _, _) = create_2_store(vec![1314, 1324]); + let runtime = TestStoreRuntime::new(store); + let (manager, _sync_recv) = new_manager(&runtime, 1, 5).await; + + assert!(manager.sync_once().await.unwrap()); + assert_eq!(manager.next_tx_seq.load(Ordering::Relaxed), 2); + assert_eq!(manager.sync_store.random_tx().await.unwrap(), None); + } + + #[tokio::test] + async fn test_manager_sync_once_no_peer_found() { + let (store, _, _, _) = create_2_store(vec![1314]); + let runtime = TestStoreRuntime::new(store); + let (manager, mut sync_recv) = new_manager(&runtime, 0, 5).await; + + let (_, sync_result) = + tokio::join!(expect_no_peer_found(&mut sync_recv), manager.sync_once(),); + assert!(sync_result.unwrap()); + assert!(matches!(sync_recv.try_recv(), Err(TryRecvError::Empty))); + assert_eq!(manager.sync_store.random_tx().await.unwrap(), Some(0)); + } + + #[tokio::test] + async fn test_manager_sync_pending_tx_finalized() { + let (_, store, _, _) = create_2_store(vec![1314, 1324]); + let runtime = TestStoreRuntime::new(store); + let (manager, _sync_recv) = new_manager(&runtime, 4, 12).await; + + assert!(manager.sync_store.add_pending_tx(0).await.unwrap()); + assert!(manager.sync_store.add_pending_tx(1).await.unwrap()); + + assert!(manager.sync_pending_tx(1).await.unwrap()); + assert_eq!(manager.sync_store.random_tx().await.unwrap(), Some(0)); + assert!(manager.sync_store.add_pending_tx(1).await.unwrap()); + } + + #[tokio::test] + async fn test_manager_sync_pending_tx_no_peer_found() { + let (store, _, _, _) = create_2_store(vec![1314, 1324]); + let runtime = TestStoreRuntime::new(store); + let (manager, mut sync_recv) = new_manager(&runtime, 4, 12).await; + + assert!(manager.sync_store.add_pending_tx(0).await.unwrap()); + assert!(manager.sync_store.add_pending_tx(1).await.unwrap()); + assert!(manager.sync_store.upgrade_tx_to_ready(1).await.unwrap()); + + let (_, sync_result) = tokio::join!( + expect_no_peer_found(&mut sync_recv), + manager.sync_pending_tx(1), + ); + assert!(sync_result.unwrap()); + assert!(matches!(sync_recv.try_recv(), Err(TryRecvError::Empty))); + assert!(manager.sync_store.upgrade_tx_to_ready(1).await.unwrap()); + } +} diff --git a/node/sync/src/auto_sync/mod.rs b/node/sync/src/auto_sync/mod.rs new file mode 100644 index 0000000..abf577c --- /dev/null +++ b/node/sync/src/auto_sync/mod.rs @@ -0,0 +1,5 @@ +mod manager; +mod sync_store; +mod tx_store; + +pub use manager::Manager as AutoSyncManager; diff --git a/node/sync/src/auto_sync/sync_store.rs b/node/sync/src/auto_sync/sync_store.rs new file mode 100644 index 0000000..7fa8bb5 --- /dev/null +++ b/node/sync/src/auto_sync/sync_store.rs @@ -0,0 +1,264 @@ +use super::tx_store::TxStore; +use anyhow::Result; +use std::ops::Deref; +use storage::log_store::config::{ConfigTx, ConfigurableExt}; +use storage_async::Store; + +const KEY_NEXT_TX_SEQ: &str = "sync.manager.next_tx_seq"; +const KEY_MAX_TX_SEQ: &str = "sync.manager.max_tx_seq"; + +#[derive(Clone)] +pub struct SyncStore { + store: Store, + + /// Pending transactions to sync with low priority. + pending_txs: TxStore, + + /// Ready transactions to sync with high priority since announcement + /// already received from other peers. + ready_txs: TxStore, +} + +impl SyncStore { + pub fn new(store: Store) -> Self { + Self { + store, + pending_txs: TxStore::new("pending"), + ready_txs: TxStore::new("ready"), + } + } + + pub async fn get_tx_seq_range(&self) -> Result<(Option, Option)> { + let store = self.store.get_store().read().await; + + // load next_tx_seq + let next_tx_seq = store.get_config_decoded(&KEY_NEXT_TX_SEQ)?; + + // load max_tx_seq + let max_tx_seq = store.get_config_decoded(&KEY_MAX_TX_SEQ)?; + + Ok((next_tx_seq, max_tx_seq)) + } + + pub async fn set_next_tx_seq(&self, tx_seq: u64) -> Result<()> { + self.store + .get_store() + .write() + .await + .set_config_encoded(&KEY_NEXT_TX_SEQ, &tx_seq) + } + + pub async fn set_max_tx_seq(&self, tx_seq: u64) -> Result<()> { + debug!(%tx_seq, "set_max_tx_seq"); + self.store + .get_store() + .write() + .await + .set_config_encoded(&KEY_MAX_TX_SEQ, &tx_seq) + } + + pub async fn add_pending_tx(&self, tx_seq: u64) -> Result { + let store = self.store.get_store().write().await; + + // already in ready queue + if self.ready_txs.has(store.deref(), tx_seq)? { + return Ok(false); + } + + // always add in pending queue + self.pending_txs.add(store.deref(), None, tx_seq) + } + + pub async fn upgrade_tx_to_ready(&self, tx_seq: u64) -> Result { + let store = self.store.get_store().write().await; + + let mut tx = ConfigTx::default(); + + // not in pending queue + if !self + .pending_txs + .remove(store.deref(), Some(&mut tx), tx_seq)? + { + return Ok(false); + } + + // move from pending to ready queue + let added = self.ready_txs.add(store.deref(), Some(&mut tx), tx_seq)?; + + store.exec_configs(tx)?; + + Ok(added) + } + + pub async fn downgrade_tx_to_pending(&self, tx_seq: u64) -> Result { + let store = self.store.get_store().write().await; + + let mut tx = ConfigTx::default(); + + // not in ready queue + if !self + .ready_txs + .remove(store.deref(), Some(&mut tx), tx_seq)? + { + return Ok(false); + } + + // move from ready to pending queue + let added = self.pending_txs.add(store.deref(), Some(&mut tx), tx_seq)?; + + store.exec_configs(tx)?; + + Ok(added) + } + + pub async fn random_tx(&self) -> Result> { + let store = self.store.get_store().read().await; + + // try to find a tx in ready queue with high priority + if let Some(val) = self.ready_txs.random(store.deref())? { + return Ok(Some(val)); + } + + // otherwise, find tx in pending queue + self.pending_txs.random(store.deref()) + } + + pub async fn remove_tx(&self, tx_seq: u64) -> Result { + let store = self.store.get_store().write().await; + + // removed in ready queue + if self.ready_txs.remove(store.deref(), None, tx_seq)? { + return Ok(true); + } + + // otherwise, try to remove in pending queue + self.pending_txs.remove(store.deref(), None, tx_seq) + } +} + +#[cfg(test)] +mod tests { + use crate::test_util::tests::TestStoreRuntime; + + use super::SyncStore; + + #[tokio::test] + async fn test_tx_seq_range() { + let runtime = TestStoreRuntime::default(); + let store = SyncStore::new(runtime.store.clone()); + + // check values by default + assert_eq!(store.get_tx_seq_range().await.unwrap(), (None, None)); + + // update values + store.set_next_tx_seq(4).await.unwrap(); + store.set_max_tx_seq(12).await.unwrap(); + + // check values again + assert_eq!(store.get_tx_seq_range().await.unwrap(), (Some(4), Some(12))); + } + + #[tokio::test] + async fn test_add_pending_tx() { + let runtime = TestStoreRuntime::default(); + let store = SyncStore::new(runtime.store.clone()); + + // add pending tx 3 + assert!(store.add_pending_tx(3).await.unwrap()); + + // cannot add pending tx 3 again + assert!(!store.add_pending_tx(3).await.unwrap()); + } + + #[tokio::test] + async fn test_upgrade_tx() { + let runtime = TestStoreRuntime::default(); + let store = SyncStore::new(runtime.store.clone()); + + // cannot upgrade by default + assert!(!store.upgrade_tx_to_ready(3).await.unwrap()); + + // add pending tx 3 + assert!(store.add_pending_tx(3).await.unwrap()); + + // can upgrade to ready + assert!(store.upgrade_tx_to_ready(3).await.unwrap()); + + // cannot add pending tx 3 again event upgraded to ready + assert!(!store.add_pending_tx(3).await.unwrap()); + + // cannot upgrade again + assert!(!store.upgrade_tx_to_ready(3).await.unwrap()); + } + + #[tokio::test] + async fn test_downgrade_tx() { + let runtime = TestStoreRuntime::default(); + let store = SyncStore::new(runtime.store.clone()); + + // cannot downgrade by default + assert!(!store.downgrade_tx_to_pending(3).await.unwrap()); + + // add pending tx 3 + assert!(store.add_pending_tx(3).await.unwrap()); + + // cannot downgrade for non-ready tx + assert!(!store.downgrade_tx_to_pending(3).await.unwrap()); + + // upgrade tx 3 to ready + assert!(store.upgrade_tx_to_ready(3).await.unwrap()); + + // can downgrade now + assert!(store.downgrade_tx_to_pending(3).await.unwrap()); + + // cannot downgrade now + assert!(!store.downgrade_tx_to_pending(3).await.unwrap()); + } + + #[tokio::test] + async fn test_random_tx() { + let runtime = TestStoreRuntime::default(); + let store = SyncStore::new(runtime.store.clone()); + + // no tx by default + assert_eq!(store.random_tx().await.unwrap(), None); + + // add pending txs 1, 2, 3 + assert!(store.add_pending_tx(1).await.unwrap()); + assert!(store.add_pending_tx(2).await.unwrap()); + assert!(store.add_pending_tx(3).await.unwrap()); + let tx = store.random_tx().await.unwrap().unwrap(); + assert!((1..=3).contains(&tx)); + + // upgrade tx 1 to ready + assert!(store.upgrade_tx_to_ready(2).await.unwrap()); + assert_eq!(store.random_tx().await.unwrap(), Some(2)); + } + + #[tokio::test] + async fn test_remove_tx() { + let runtime = TestStoreRuntime::default(); + let store = SyncStore::new(runtime.store.clone()); + + // cannot remove by default + assert!(!store.remove_tx(1).await.unwrap()); + + // add pending tx 1, 2 + assert!(store.add_pending_tx(1).await.unwrap()); + assert!(store.add_pending_tx(2).await.unwrap()); + + // upgrade tx 1 to ready + assert!(store.upgrade_tx_to_ready(1).await.unwrap()); + assert_eq!(store.random_tx().await.unwrap(), Some(1)); + + // remove tx 1 + assert!(store.remove_tx(1).await.unwrap()); + assert_eq!(store.random_tx().await.unwrap(), Some(2)); + assert!(!store.remove_tx(1).await.unwrap()); + + // remove tx 2 + assert!(store.remove_tx(2).await.unwrap()); + assert_eq!(store.random_tx().await.unwrap(), None); + assert!(!store.remove_tx(2).await.unwrap()); + } +} diff --git a/node/sync/src/auto_sync/tx_store.rs b/node/sync/src/auto_sync/tx_store.rs new file mode 100644 index 0000000..3c0de45 --- /dev/null +++ b/node/sync/src/auto_sync/tx_store.rs @@ -0,0 +1,242 @@ +use anyhow::Result; +use rand::Rng; +use storage::log_store::config::{ConfigTx, ConfigurableExt}; +use storage::log_store::Store; + +/// TxStore is used to store pending transactions that to be synchronized in advance. +/// +/// Basically, this store maintains an enumerable map data structure for `tx_seq`. +#[derive(Clone)] +pub struct TxStore { + /// To allow multiple `TxStore` with different priority. + name: &'static str, + + /// DB key for `count` value. + key_count: String, +} + +impl TxStore { + pub fn new(name: &'static str) -> Self { + Self { + name, + key_count: format!("sync.manager.txs.{}.count", name), + } + } + + fn key_seq_to_index(&self, tx_seq: u64) -> String { + format!("sync.manager.txs.{}.seq2index.{}", self.name, tx_seq) + } + + fn key_index_to_seq(&self, index: usize) -> String { + format!("sync.manager.txs.{}.index2seq.{}", self.name, index) + } + + fn index_of(&self, store: &dyn Store, tx_seq: u64) -> Result> { + store.get_config_decoded(&self.key_seq_to_index(tx_seq)) + } + + fn at(&self, store: &dyn Store, index: usize) -> Result> { + store.get_config_decoded(&self.key_index_to_seq(index)) + } + + pub fn has(&self, store: &dyn Store, tx_seq: u64) -> Result { + self.index_of(store, tx_seq).map(|idx| idx.is_some()) + } + + pub fn count(&self, store: &dyn Store) -> Result { + store + .get_config_decoded(&self.key_count) + .map(|x| x.unwrap_or(0)) + } + + pub fn add( + &self, + store: &dyn Store, + db_tx: Option<&mut ConfigTx>, + tx_seq: u64, + ) -> Result { + // already exists + if self.has(store, tx_seq)? { + return Ok(false); + } + + let count = self.count(store)?; + + let mut tx = ConfigTx::default(); + tx.set_config(&self.key_index_to_seq(count), &tx_seq); + tx.set_config(&self.key_seq_to_index(tx_seq), &count); + tx.set_config(&self.key_count, &(count + 1)); + + if let Some(db_tx) = db_tx { + db_tx.append(&mut tx); + } else { + store.exec_configs(tx)?; + } + + Ok(true) + } + + pub fn random(&self, store: &dyn Store) -> Result> { + let count = self.count(store)?; + if count == 0 { + return Ok(None); + } + + let index = rand::thread_rng().gen_range(0..count); + let tx_seq = self.at(store, index)?.expect("data corruption"); + + Ok(Some(tx_seq)) + } + + pub fn remove( + &self, + store: &dyn Store, + db_tx: Option<&mut ConfigTx>, + tx_seq: u64, + ) -> Result { + let index = match self.index_of(store, tx_seq)? { + Some(val) => val, + None => return Ok(false), + }; + + let count = self.count(store)?; + assert!(count > 0, "data corruption"); + + let mut tx = ConfigTx::default(); + + // update `count` value + tx.set_config(&self.key_count, &(count - 1)); + + // remove `seq2index` index + tx.remove_config(&self.key_seq_to_index(tx_seq)); + + if index == count - 1 { + // remove `index2seq` index for the last element + tx.remove_config(&self.key_index_to_seq(index)); + } else { + // swap `back` to the `removed` slot + let last_tx = self.at(store, count - 1)?.expect("data corruption"); + + // update the `index2seq` for the removed element + tx.set_config(&self.key_index_to_seq(index), &last_tx); + + // remove the last slot + tx.remove_config(&self.key_index_to_seq(count - 1)); + + // update `seq2index` index for the last tx + tx.set_config(&self.key_seq_to_index(last_tx), &index); + } + + if let Some(db_tx) = db_tx { + db_tx.append(&mut tx); + } else { + store.exec_configs(tx)?; + } + + Ok(true) + } +} + +#[cfg(test)] +mod tests { + use crate::test_util::tests::TestStoreRuntime; + + use super::TxStore; + + #[test] + fn test_add() { + let store = TestStoreRuntime::new_store(); + let tx_store = TxStore::new("foo"); + + // count is 0 by default + assert_eq!(tx_store.count(&store).unwrap(), 0); + + // add 3 txs + assert!(tx_store.add(&store, None, 1).unwrap()); + assert!(tx_store.add(&store, None, 2).unwrap()); + assert!(tx_store.add(&store, None, 3).unwrap()); + + // cannot add again + assert!(!tx_store.add(&store, None, 1).unwrap()); + assert!(!tx_store.add(&store, None, 2).unwrap()); + assert!(!tx_store.add(&store, None, 3).unwrap()); + + // count is 3 after insertion + assert_eq!(tx_store.count(&store).unwrap(), 3); + + // check index of tx + assert_eq!(tx_store.index_of(&store, 1).unwrap(), Some(0)); + assert_eq!(tx_store.index_of(&store, 2).unwrap(), Some(1)); + assert_eq!(tx_store.index_of(&store, 3).unwrap(), Some(2)); + assert_eq!(tx_store.index_of(&store, 4).unwrap(), None); + + // check tx of index + assert_eq!(tx_store.at(&store, 0).unwrap(), Some(1)); + assert_eq!(tx_store.at(&store, 1).unwrap(), Some(2)); + assert_eq!(tx_store.at(&store, 2).unwrap(), Some(3)); + assert_eq!(tx_store.at(&store, 3).unwrap(), None); + } + + #[test] + fn test_random() { + let store = TestStoreRuntime::new_store(); + let tx_store = TxStore::new("foo"); + + assert_eq!(tx_store.random(&store).unwrap(), None); + + assert!(tx_store.add(&store, None, 1).unwrap()); + assert!(tx_store.add(&store, None, 2).unwrap()); + assert!(tx_store.add(&store, None, 3).unwrap()); + + let tx_seq = tx_store + .random(&store) + .unwrap() + .expect("should randomly pick one"); + assert!((1..=3).contains(&tx_seq)); + } + + #[test] + fn test_remove_tail() { + let store = TestStoreRuntime::new_store(); + let tx_store = TxStore::new("foo"); + + assert!(tx_store.add(&store, None, 1).unwrap()); + assert!(tx_store.add(&store, None, 2).unwrap()); + assert!(tx_store.add(&store, None, 3).unwrap()); + + assert!(tx_store.remove(&store, None, 3).unwrap()); + + assert_eq!(tx_store.count(&store).unwrap(), 2); + assert_eq!(tx_store.index_of(&store, 1).unwrap(), Some(0)); + assert_eq!(tx_store.index_of(&store, 2).unwrap(), Some(1)); + assert_eq!(tx_store.index_of(&store, 3).unwrap(), None); + + assert_eq!(tx_store.at(&store, 0).unwrap(), Some(1)); + assert_eq!(tx_store.at(&store, 1).unwrap(), Some(2)); + assert_eq!(tx_store.at(&store, 2).unwrap(), None); + } + + #[test] + fn test_remove_swap() { + let store = TestStoreRuntime::new_store(); + let tx_store = TxStore::new("foo"); + + assert!(tx_store.add(&store, None, 1).unwrap()); + assert!(tx_store.add(&store, None, 2).unwrap()); + assert!(tx_store.add(&store, None, 3).unwrap()); + assert!(tx_store.add(&store, None, 4).unwrap()); + + assert!(tx_store.remove(&store, None, 2).unwrap()); + + assert_eq!(tx_store.count(&store).unwrap(), 3); + assert_eq!(tx_store.index_of(&store, 1).unwrap(), Some(0)); + assert_eq!(tx_store.index_of(&store, 2).unwrap(), None); + assert_eq!(tx_store.index_of(&store, 3).unwrap(), Some(2)); + assert_eq!(tx_store.index_of(&store, 4).unwrap(), Some(1)); + + assert_eq!(tx_store.at(&store, 0).unwrap(), Some(1)); + assert_eq!(tx_store.at(&store, 1).unwrap(), Some(4)); + assert_eq!(tx_store.at(&store, 2).unwrap(), Some(3)); + assert_eq!(tx_store.at(&store, 3).unwrap(), None); + } +} diff --git a/node/sync/src/context.rs b/node/sync/src/context.rs new file mode 100644 index 0000000..9b62cd6 --- /dev/null +++ b/node/sync/src/context.rs @@ -0,0 +1,46 @@ +use network::{NetworkMessage, PeerAction, PeerId, PubsubMessage, ReportSource}; +use tokio::sync::mpsc; + +pub struct SyncNetworkContext { + network_send: mpsc::UnboundedSender, +} + +impl SyncNetworkContext { + pub fn new(network_send: mpsc::UnboundedSender) -> Self { + Self { network_send } + } + + /// Sends an arbitrary network message. + pub fn send(&self, msg: NetworkMessage) { + self.network_send.send(msg).unwrap_or_else(|_| { + warn!("Could not send message to the network service"); + }) + } + + /// Publishes a single message. + pub fn publish(&self, msg: PubsubMessage) { + self.send(NetworkMessage::Publish { + messages: vec![msg], + }); + } + + pub fn report_peer(&self, peer_id: PeerId, action: PeerAction, msg: &'static str) { + debug!(%peer_id, ?action, %msg, "Report peer"); + self.send(NetworkMessage::ReportPeer { + peer_id, + action, + source: ReportSource::SyncService, + msg, + }) + } + + pub fn ban_peer(&self, peer_id: PeerId, msg: &'static str) { + info!(%peer_id, %msg, "Ban peer"); + self.send(NetworkMessage::ReportPeer { + peer_id, + action: PeerAction::Fatal, + source: ReportSource::SyncService, + msg, + }) + } +} diff --git a/node/sync/src/controllers/mod.rs b/node/sync/src/controllers/mod.rs new file mode 100644 index 0000000..9eb4058 --- /dev/null +++ b/node/sync/src/controllers/mod.rs @@ -0,0 +1,16 @@ +mod peers; +mod serial; + +use serde::{Deserialize, Serialize}; + +pub use serial::{FailureReason, SerialSyncController, SyncState}; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FileSyncInfo { + pub elapsed_secs: u64, + pub peers: usize, + pub num_chunks: u64, + pub next_chunks: u64, + pub state: String, +} diff --git a/node/sync/src/controllers/peers.rs b/node/sync/src/controllers/peers.rs new file mode 100644 index 0000000..efac22c --- /dev/null +++ b/node/sync/src/controllers/peers.rs @@ -0,0 +1,306 @@ +use network::{Multiaddr, PeerId}; +use rand::seq::IteratorRandom; +use std::collections::HashMap; +use std::time::{Duration, Instant}; + +const PEER_CONNECT_TIMEOUT: Duration = Duration::from_secs(5); +const PEER_DISCONNECT_TIMEOUT: Duration = Duration::from_secs(5); + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PeerState { + Found, + Connecting, + Connected, + Disconnecting, + Disconnected, +} + +struct PeerInfo { + /// The reported/connected address of the peer. + pub addr: Multiaddr, + + /// The current state of the peer. + pub state: PeerState, + + /// Timestamp of the last state change. + pub since: Instant, +} + +impl PeerInfo { + fn update_state(&mut self, new_state: PeerState) { + self.state = new_state; + self.since = Instant::now(); + } +} + +#[derive(Default)] +pub struct SyncPeers { + peers: HashMap, +} + +impl SyncPeers { + pub fn add_new_peer(&mut self, peer_id: PeerId, addr: Multiaddr) -> bool { + if self.peers.contains_key(&peer_id) { + return false; + } + + self.peers.insert( + peer_id, + PeerInfo { + addr, + state: PeerState::Found, + since: Instant::now(), + }, + ); + + true + } + + pub fn update_state( + &mut self, + peer_id: &PeerId, + from: PeerState, + to: PeerState, + ) -> Option { + let info = self.peers.get_mut(peer_id)?; + + if info.state == from { + info.update_state(to); + Some(true) + } else { + Some(false) + } + } + + pub fn update_state_force(&mut self, peer_id: &PeerId, state: PeerState) -> Option { + let info = self.peers.get_mut(peer_id)?; + let old_state = info.state; + info.state = state; + Some(old_state) + } + + pub fn peer_state(&self, peer_id: &PeerId) -> Option { + self.peers.get(peer_id).map(|info| info.state) + } + + pub fn random_peer(&self, state: PeerState) -> Option<(PeerId, Multiaddr)> { + self.peers + .iter() + .filter(|(_, info)| info.state == state) + .map(|(peer_id, info)| (*peer_id, info.addr.clone())) + .choose(&mut rand::thread_rng()) + } + + pub fn count(&self, states: &[PeerState]) -> usize { + self.peers + .values() + .filter(|info| states.contains(&info.state)) + .count() + } + + pub fn transition(&mut self) { + let mut bad_peers = vec![]; + + for (peer_id, info) in self.peers.iter_mut() { + match info.state { + PeerState::Found | PeerState::Connected => {} + + PeerState::Connecting => { + if info.since.elapsed() >= PEER_CONNECT_TIMEOUT { + info!(%peer_id, %info.addr, "Peer connection timeout"); + bad_peers.push(*peer_id); + } + } + + PeerState::Disconnecting => { + if info.since.elapsed() >= PEER_DISCONNECT_TIMEOUT { + info!(%peer_id, %info.addr, "Peer disconnect timeout"); + bad_peers.push(*peer_id); + } + } + + PeerState::Disconnected => bad_peers.push(*peer_id), + } + } + + for peer_id in bad_peers { + self.peers.remove(&peer_id); + } + } +} + +#[cfg(test)] +mod tests { + use libp2p::identity; + use std::collections::HashSet; + + use super::*; + + #[test] + fn test_add_new_peer() { + let mut sync_peers: SyncPeers = Default::default(); + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let addr: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + + assert!(sync_peers.add_new_peer(peer_id, addr.clone())); + assert!(!sync_peers.add_new_peer(peer_id, addr)); + } + + #[test] + fn test_update_state() { + let mut sync_peers: SyncPeers = Default::default(); + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let addr: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + + assert_eq!( + sync_peers.update_state(&peer_id, PeerState::Found, PeerState::Connecting), + None + ); + assert_eq!(sync_peers.peer_state(&peer_id), None); + + sync_peers.add_new_peer(peer_id, addr); + assert_eq!(sync_peers.peer_state(&peer_id), Some(PeerState::Found)); + + assert_eq!( + sync_peers.update_state(&peer_id, PeerState::Found, PeerState::Connecting), + Some(true) + ); + assert_eq!(sync_peers.peer_state(&peer_id), Some(PeerState::Connecting)); + + assert_eq!( + sync_peers.update_state(&peer_id, PeerState::Found, PeerState::Connected), + Some(false) + ); + assert_eq!(sync_peers.peer_state(&peer_id), Some(PeerState::Connecting)); + } + + #[test] + fn test_update_state_force() { + let mut sync_peers: SyncPeers = Default::default(); + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let addr: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + + assert_eq!( + sync_peers.update_state_force(&peer_id, PeerState::Connecting), + None + ); + assert_eq!(sync_peers.peer_state(&peer_id), None); + + sync_peers.add_new_peer(peer_id, addr); + + assert_eq!( + sync_peers.update_state_force(&peer_id, PeerState::Connecting), + Some(PeerState::Found) + ); + assert_eq!(sync_peers.peer_state(&peer_id), Some(PeerState::Connecting)); + } + + #[test] + fn test_random_peer() { + let count = 10; + let mut sync_peers: SyncPeers = Default::default(); + let addr: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + + let mut peers_found = HashSet::new(); + let mut peers_connecting = HashSet::new(); + + for i in 0..count { + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + sync_peers.add_new_peer(peer_id, addr.clone()); + peers_found.insert(peer_id); + + assert_eq!(sync_peers.count(&[PeerState::Found]), i + 1); + assert_eq!(sync_peers.count(&[PeerState::Connecting]), 0); + assert_eq!( + sync_peers.count(&[PeerState::Found, PeerState::Connecting]), + i + 1 + ); + } + + for i in 0..count { + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + sync_peers.add_new_peer(peer_id, addr.clone()); + sync_peers.update_state_force(&peer_id, PeerState::Connecting); + peers_connecting.insert(peer_id); + + assert_eq!(sync_peers.count(&[PeerState::Found]), count); + assert_eq!(sync_peers.count(&[PeerState::Connecting]), i + 1); + assert_eq!( + sync_peers.count(&[PeerState::Found, PeerState::Connecting]), + count + i + 1 + ); + } + + // random pick + for _ in 0..30 { + let peer = sync_peers.random_peer(PeerState::Found).unwrap(); + assert!(peers_found.contains(&peer.0)); + assert_eq!(peer.1, addr); + let peer = sync_peers.random_peer(PeerState::Connecting).unwrap(); + assert!(peers_connecting.contains(&peer.0)); + assert_eq!(peer.1, addr); + assert!(sync_peers.random_peer(PeerState::Disconnected).is_none()); + } + } + + #[test] + fn test_transition() { + let mut sync_peers: SyncPeers = Default::default(); + let addr: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + sync_peers.add_new_peer(peer_id, addr.clone()); + + let peer_id_connected = identity::Keypair::generate_ed25519().public().to_peer_id(); + sync_peers.add_new_peer(peer_id_connected, addr.clone()); + sync_peers.update_state_force(&peer_id_connected, PeerState::Connected); + + let peer_id_connecting = identity::Keypair::generate_ed25519().public().to_peer_id(); + sync_peers.add_new_peer(peer_id_connecting, addr.clone()); + sync_peers.update_state_force(&peer_id_connecting, PeerState::Connecting); + sync_peers.peers.get_mut(&peer_id_connecting).unwrap().since = + Instant::now() - PEER_CONNECT_TIMEOUT; + + let peer_id_disconnecting = identity::Keypair::generate_ed25519().public().to_peer_id(); + sync_peers.add_new_peer(peer_id_disconnecting, addr.clone()); + sync_peers.update_state_force(&peer_id_disconnecting, PeerState::Disconnecting); + sync_peers + .peers + .get_mut(&peer_id_disconnecting) + .unwrap() + .since = Instant::now() - PEER_DISCONNECT_TIMEOUT; + + let peer_id_disconnected = identity::Keypair::generate_ed25519().public().to_peer_id(); + sync_peers.add_new_peer(peer_id_disconnected, addr); + sync_peers.update_state_force(&peer_id_disconnected, PeerState::Disconnected); + + assert_eq!(sync_peers.peer_state(&peer_id), Some(PeerState::Found)); + assert_eq!( + sync_peers.peer_state(&peer_id_connected), + Some(PeerState::Connected) + ); + assert_eq!( + sync_peers.peer_state(&peer_id_connecting), + Some(PeerState::Connecting) + ); + assert_eq!( + sync_peers.peer_state(&peer_id_disconnecting), + Some(PeerState::Disconnecting) + ); + assert_eq!( + sync_peers.peer_state(&peer_id_disconnected), + Some(PeerState::Disconnected) + ); + + sync_peers.transition(); + + assert_eq!(sync_peers.peer_state(&peer_id), Some(PeerState::Found)); + assert_eq!( + sync_peers.peer_state(&peer_id_connected), + Some(PeerState::Connected) + ); + assert_eq!(sync_peers.peer_state(&peer_id_connecting), None); + assert_eq!(sync_peers.peer_state(&peer_id_disconnecting), None); + assert_eq!(sync_peers.peer_state(&peer_id_disconnected), None); + } +} diff --git a/node/sync/src/controllers/serial.rs b/node/sync/src/controllers/serial.rs new file mode 100644 index 0000000..91d672c --- /dev/null +++ b/node/sync/src/controllers/serial.rs @@ -0,0 +1,1510 @@ +use crate::context::SyncNetworkContext; +use crate::controllers::peers::{PeerState, SyncPeers}; +use crate::controllers::FileSyncInfo; +use file_location_cache::FileLocationCache; +use libp2p::swarm::DialError; +use network::{ + multiaddr::Protocol, rpc::GetChunksRequest, types::FindFile, Multiaddr, NetworkMessage, + PeerAction, PeerId, PubsubMessage, SyncId as RequestId, +}; +use shared_types::{timestamp_now, ChunkArrayWithProof, TxID, CHUNK_SIZE}; +use std::{ + sync::Arc, + time::{Duration, Instant}, +}; +use storage_async::Store; + +const MAX_CHUNKS_TO_REQUEST: u64 = 2 * 1024; +const MAX_REQUEST_FAILURES: usize = 100; +const PEER_REQUEST_TIMEOUT: Duration = Duration::from_secs(5); +const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(5); +const WAIT_OUTGOING_CONNECTION_TIMEOUT: Duration = Duration::from_secs(10); +const NEXT_REQUEST_WAIT_TIME: Duration = Duration::from_secs(3); + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum FailureReason { + DBError(String), + TxReverted(TxID), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SyncState { + Idle, + FindingPeers { + since: Instant, + updated: Instant, + }, + FoundPeers, + ConnectingPeers, + AwaitingOutgoingConnection { + since: Instant, + }, + AwaitingDownload { + since: Instant, + }, + Downloading { + peer_id: PeerId, + from_chunk: u64, + to_chunk: u64, + since: Instant, + }, + Completed, + Failed { + reason: FailureReason, + }, +} + +pub struct SerialSyncController { + // only used for log purpose + tx_seq: u64, + + /// The unique transaction ID. + tx_id: TxID, + + since: Instant, + + /// The size of the file to be synced. + num_chunks: u64, + + /// The next chunk id that we need to retrieve. + next_chunk: u64, + + /// Continuous RPC failures to request chunks. + failures: usize, + + /// Current state of this request. + state: SyncState, + + /// Sync peer manager. + peers: SyncPeers, + + /// A network context to contact the network service. + ctx: Arc, + + /// Log and transaction storage. + store: Store, + + /// Cache for storing and serving gossip messages. + file_location_cache: Arc, +} + +impl SerialSyncController { + pub fn new( + tx_id: TxID, + num_chunks: u64, + ctx: Arc, + store: Store, + file_location_cache: Arc, + ) -> Self { + SerialSyncController { + tx_seq: tx_id.seq, + tx_id, + since: Instant::now(), + num_chunks, + next_chunk: 0, + failures: 0, + state: SyncState::Idle, + peers: Default::default(), + ctx, + store, + file_location_cache, + } + } + + pub fn get_sync_info(&self) -> FileSyncInfo { + FileSyncInfo { + elapsed_secs: self.since.elapsed().as_secs(), + peers: self.peers.count(&[PeerState::Connected]), + num_chunks: self.num_chunks, + next_chunks: self.next_chunk, + state: format!("{:?}", self.state), + } + } + + pub fn get_status(&self) -> &SyncState { + &self.state + } + + /// Resets the status to re-sync file when failed. + pub fn reset(&mut self) { + self.next_chunk = 0; + self.failures = 0; + self.state = SyncState::Idle; + // remove disconnected peers + self.peers.transition(); + } + + fn try_find_peers(&mut self) { + info!(%self.tx_seq, "Finding peers"); + + // try from cache + let mut found_new_peer = false; + + for announcement in self.file_location_cache.get_all(self.tx_id) { + // make sure peer_id is part of the address + let peer_id: PeerId = announcement.peer_id.clone().into(); + let mut addr: Multiaddr = announcement.at.clone().into(); + addr.push(Protocol::P2p(peer_id.into())); + + found_new_peer = self.on_peer_found(peer_id, addr) || found_new_peer; + } + + if !found_new_peer { + self.ctx.publish(PubsubMessage::FindFile(FindFile { + tx_id: self.tx_id, + timestamp: timestamp_now(), + })); + } + + let now = Instant::now(); + + let (since, updated) = match self.state { + SyncState::FindingPeers { since, .. } => (since, now), + _ => (now, now), + }; + + self.state = SyncState::FindingPeers { since, updated }; + } + + fn try_connect(&mut self) { + // select a random peer + let (peer_id, address) = match self.peers.random_peer(PeerState::Found) { + Some((peer_id, address)) => (peer_id, address), + None => { + // peer may be disconnected by remote node and need to find peers again + warn!(%self.tx_seq, "No peers available to connect"); + self.state = SyncState::Idle; + return; + } + }; + + // connect to peer + info!(%peer_id, %address, "Attempting to connect to peer"); + self.ctx.send(NetworkMessage::DialPeer { address, peer_id }); + + self.peers + .update_state(&peer_id, PeerState::Found, PeerState::Connecting); + + self.state = SyncState::ConnectingPeers; + } + + fn try_request_next(&mut self) { + // select a random peer + let peer_id = match self.peers.random_peer(PeerState::Connected) { + Some((peer_id, _)) => peer_id, + None => { + warn!(%self.tx_seq, "No peers available to request chunks"); + self.state = SyncState::Idle; + return; + } + }; + + // request next chunk array + let from_chunk = self.next_chunk; + let to_chunk = std::cmp::min(from_chunk + MAX_CHUNKS_TO_REQUEST, self.num_chunks); + + let request_id = network::RequestId::Sync(RequestId::SerialSync { tx_id: self.tx_id }); + + let request = network::Request::GetChunks(GetChunksRequest { + tx_id: self.tx_id, + index_start: from_chunk, + index_end: to_chunk, + }); + + self.ctx.send(NetworkMessage::SendRequest { + peer_id, + request_id, + request, + }); + + self.state = SyncState::Downloading { + peer_id, + from_chunk, + to_chunk, + since: Instant::now(), + }; + } + + fn ban_peer(&mut self, peer_id: PeerId, reason: &'static str) { + self.ctx.ban_peer(peer_id, reason); + + self.peers + .update_state(&peer_id, PeerState::Connected, PeerState::Disconnecting); + } + + pub fn on_peer_found(&mut self, peer_id: PeerId, addr: Multiaddr) -> bool { + if self.peers.add_new_peer(peer_id, addr.clone()) { + info!(%self.tx_seq, %peer_id, %addr, "Found new peer"); + true + } else { + // e.g. multiple `AnnounceFile` messages propagated + debug!(%self.tx_seq, %peer_id, %addr, "Found an existing peer"); + false + } + } + + pub fn on_dail_failed(&mut self, peer_id: PeerId, err: &DialError) { + match err { + DialError::ConnectionLimit(_) => { + if let Some(true) = + self.peers + .update_state(&peer_id, PeerState::Connecting, PeerState::Found) + { + info!(%self.tx_seq, %peer_id, "Failed to dail peer due to outgoing connection limitation"); + self.state = SyncState::AwaitingOutgoingConnection { + since: Instant::now(), + }; + } + } + _ => { + if let Some(true) = self.peers.update_state( + &peer_id, + PeerState::Connecting, + PeerState::Disconnected, + ) { + info!(%self.tx_seq, %peer_id, "Failed to dail peer"); + self.state = SyncState::Idle; + } + } + } + } + + pub fn on_peer_connected(&mut self, peer_id: PeerId) { + if let Some(true) = + self.peers + .update_state(&peer_id, PeerState::Connecting, PeerState::Connected) + { + info!(%self.tx_seq, %peer_id, "Peer connected"); + } + } + + pub fn on_peer_disconnected(&mut self, peer_id: PeerId) { + match self + .peers + .update_state_force(&peer_id, PeerState::Disconnected) + { + Some(PeerState::Disconnecting) => info!(%self.tx_seq, %peer_id, "Peer disconnected"), + Some(old_state) => { + info!(%self.tx_seq, %peer_id, ?old_state, "Peer disconnected by remote"); + } + None => {} + } + } + + /// Handle the case that got an unexpected response: + /// 1. not in `Downloading` sync state. + /// 2. from unexpected peer. + fn handle_on_response_mismatch(&self, from_peer_id: PeerId) -> bool { + match self.state { + SyncState::Downloading { peer_id, .. } => { + if from_peer_id == peer_id { + return false; + } + + // got response from wrong peer + // this can happen if we get a response for a timeout request + warn!(%self.tx_seq, %from_peer_id, %peer_id, "Got response from unexpected peer"); + self.ctx.report_peer( + from_peer_id, + PeerAction::LowToleranceError, + "Peer id mismatch", + ); + true + } + _ => { + // FIXME(zz). Delayed response can enter this. + warn!(%self.tx_seq, %from_peer_id, ?self.state, "Got response in unexpected state"); + self.ctx.report_peer( + from_peer_id, + PeerAction::HighToleranceError, + "Sync state mismatch", + ); + true + } + } + } + + pub async fn on_response(&mut self, from_peer_id: PeerId, response: ChunkArrayWithProof) { + if self.handle_on_response_mismatch(from_peer_id) { + return; + } + + let (from_chunk, to_chunk) = match self.state { + SyncState::Downloading { + peer_id: _peer_id, + from_chunk, + to_chunk, + .. + } => (from_chunk, to_chunk), + _ => return, + }; + + debug_assert!(from_chunk < to_chunk, "Invalid chunk boundaries"); + + // invalid chunk array size: ban and re-request + let data_len = response.chunks.data.len(); + if data_len == 0 || data_len % CHUNK_SIZE > 0 { + warn!(%from_peer_id, %self.tx_seq, %data_len, "Invalid chunk response data length"); + self.ban_peer(from_peer_id, "Invalid chunk response data length"); + self.state = SyncState::Idle; + return; + } + + // invalid chunk range: ban and re-request + let start_index = response.chunks.start_index; + let end_index = start_index + (data_len / CHUNK_SIZE) as u64; + if start_index != from_chunk || end_index != to_chunk { + // FIXME(zz): Possible for relayed response. + warn!(%self.tx_seq, "Invalid chunk response range, expected={from_chunk}..{to_chunk}, actual={start_index}..{end_index}"); + // self.ban_peer(from_peer_id, "Invalid chunk response range"); + // self.state = SyncState::Idle; + return; + } + + // validate Merkle proofs + let validation_result = self + .store + .get_store() + .read() + .await + .validate_range_proof(self.tx_seq, &response); + + match validation_result { + Ok(true) => {} + Ok(false) => { + info!("Failed to validate chunks response due to no root found"); + self.state = SyncState::AwaitingDownload { + since: Instant::now() + NEXT_REQUEST_WAIT_TIME, + }; + return; + } + Err(err) => { + warn!(%err, "Failed to validate chunks response"); + self.ban_peer(from_peer_id, "Chunk array validation failed"); + self.state = SyncState::Idle; + return; + } + } + + self.failures = 0; + + // store in db + match self + .store + .put_chunks_with_tx_hash(self.tx_id.seq, self.tx_id.hash, response.chunks) + .await + { + Ok(true) => self.next_chunk = to_chunk, + Ok(false) => { + warn!(?self.tx_id, "Transaction reverted while storing chunks"); + self.state = SyncState::Failed { + reason: FailureReason::TxReverted(self.tx_id), + }; + return; + } + Err(err) => { + error!(%err, "Unexpected DB error while storing chunks"); + self.state = SyncState::Failed { + reason: FailureReason::DBError(err.to_string()), + }; + return; + } + } + + // prepare to download next + if self.next_chunk < self.num_chunks { + self.state = SyncState::Idle; + return; + } + + // finalize tx if all chunks downloaded + match self + .store + .finalize_tx_with_hash(self.tx_id.seq, self.tx_id.hash) + .await + { + Ok(true) => self.state = SyncState::Completed, + Ok(false) => { + warn!(?self.tx_id, "Transaction reverted during finalize_tx"); + self.state = SyncState::Failed { + reason: FailureReason::TxReverted(self.tx_id), + }; + } + Err(err) => { + error!(%err, "Unexpected error during finalize_tx"); + self.state = SyncState::Failed { + reason: FailureReason::DBError(err.to_string()), + }; + } + } + } + + pub fn on_request_failed(&mut self, peer_id: PeerId) { + if self.handle_on_response_mismatch(peer_id) { + return; + } + + self.handle_response_failure(peer_id, "RPC Error"); + } + + fn handle_response_failure(&mut self, peer_id: PeerId, reason: &'static str) { + info!(%peer_id, %self.tx_seq, %reason, "Chunks request failed"); + + // ban peer on too many failures + // FIXME(zz): If remote removes a file, we will also get failure here. + // self.ctx + // .report_peer(peer_id, PeerAction::HighToleranceError, reason); + + self.failures += 1; + + if self.failures <= MAX_REQUEST_FAILURES { + // try again + self.state = SyncState::AwaitingDownload { + since: Instant::now() + NEXT_REQUEST_WAIT_TIME, + }; + } else { + // ban and find new peer to download + self.ban_peer(peer_id, reason); + self.state = SyncState::Idle; + } + } + + pub fn transition(&mut self) { + use PeerState::*; + + // update peer connection states + self.peers.transition(); + + loop { + match self.state { + SyncState::Idle => { + if self.peers.count(&[Found, Connecting, Connected]) > 0 { + self.state = SyncState::FoundPeers; + } else { + self.try_find_peers(); + } + } + + SyncState::FindingPeers { updated, .. } => { + if self.peers.count(&[Found, Connecting, Connected]) > 0 { + self.state = SyncState::FoundPeers; + } else { + // storage node may not have the specific file when `FindFile` + // gossip message received. In this case, just broadcast the + // `FindFile` message again. + if updated.elapsed() >= PEER_REQUEST_TIMEOUT { + debug!(%self.tx_seq, "Peer request timeout"); + self.try_find_peers(); + } + + return; + } + } + + SyncState::FoundPeers => { + if self.peers.count(&[Connecting, Connected]) > 0 { + self.state = SyncState::ConnectingPeers; + } else { + self.try_connect(); + } + } + + SyncState::ConnectingPeers => { + if self.peers.count(&[Connected]) > 0 { + self.state = SyncState::AwaitingDownload { + since: Instant::now(), + }; + } else if self.peers.count(&[Connecting]) == 0 { + self.state = SyncState::Idle; + } else { + // peers.transition() will handle the case that peer connecting timeout + return; + } + } + + SyncState::AwaitingOutgoingConnection { since } => { + if since.elapsed() < WAIT_OUTGOING_CONNECTION_TIMEOUT { + return; + } + + self.state = SyncState::Idle; + } + + SyncState::AwaitingDownload { since } => { + if Instant::now() < since { + return; + } + + self.try_request_next(); + } + + SyncState::Downloading { peer_id, since, .. } => { + if !matches!(self.peers.peer_state(&peer_id), Some(PeerState::Connected)) { + // e.g. peer disconnected by remote node + self.state = SyncState::Idle; + } else if since.elapsed() >= DOWNLOAD_TIMEOUT { + self.handle_response_failure(peer_id, "RPC timeout"); + } else { + return; + } + } + + SyncState::Completed | SyncState::Failed { .. } => return, + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_util::create_2_store; + use crate::test_util::tests::create_file_location_cache; + use libp2p::identity; + use network::{ReportSource, Request}; + use storage::log_store::log_manager::LogConfig; + use storage::log_store::log_manager::LogManager; + use storage::log_store::LogStoreRead; + use storage::H256; + use task_executor::{test_utils::TestRuntime, TaskExecutor}; + use tokio::sync::mpsc::{self, UnboundedReceiver}; + use tokio::sync::RwLock; + + #[test] + fn test_status() { + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, _) = create_default_controller(task_executor, None); + + assert_eq!(*controller.get_status(), SyncState::Idle); + controller.state = SyncState::Completed; + assert_eq!(*controller.get_status(), SyncState::Completed); + + controller.reset(); + assert_eq!(*controller.get_status(), SyncState::Idle); + } + + #[tokio::test] + async fn test_find_peers() { + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = create_default_controller(task_executor, None); + + assert_eq!(controller.peers.count(&[PeerState::Found]), 0); + + controller.try_find_peers(); + assert!(matches!( + *controller.get_status(), + SyncState::FindingPeers { .. } + )); + assert_eq!(controller.peers.count(&[PeerState::Found]), 1); + assert!(network_recv.try_recv().is_err()); + + controller.try_find_peers(); + assert_eq!(controller.peers.count(&[PeerState::Found]), 1); + + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::Publish { messages } => { + assert_eq!(messages.len(), 1); + + match &messages[0] { + PubsubMessage::FindFile(data) => { + assert_eq!(data.tx_id, controller.tx_id); + } + _ => { + panic!("Unexpected message type"); + } + } + } + _ => { + panic!("Unexpected message type"); + } + } + } + } + + #[tokio::test] + async fn test_find_peers_not_in_file_cache() { + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = create_default_controller(task_executor, None); + + controller.tx_seq = 1; + controller.tx_id = TxID { + seq: 1, + hash: H256::random(), + }; + controller.try_find_peers(); + + assert_eq!(controller.peers.count(&[PeerState::Found]), 0); + + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::Publish { messages } => { + assert_eq!(messages.len(), 1); + + match &messages[0] { + PubsubMessage::FindFile(data) => { + assert_eq!(data.tx_id, controller.tx_id); + } + _ => { + panic!("Unexpected message type"); + } + } + } + _ => { + panic!("Unexpected message type"); + } + } + } + + assert!(matches!( + *controller.get_status(), + SyncState::FindingPeers { .. } + )); + } + + #[tokio::test] + async fn test_connect_peers() { + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = create_default_controller(task_executor, None); + + controller.state = SyncState::FoundPeers; + controller.try_connect(); + assert_eq!(controller.state, SyncState::Idle); + assert!(network_recv.try_recv().is_err()); + + let new_peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let addr: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + + controller.peers.add_new_peer(new_peer_id, addr.clone()); + controller.try_connect(); + + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::DialPeer { address, peer_id } => { + assert_eq!(address, addr); + assert_eq!(peer_id, new_peer_id); + } + _ => { + panic!("Not expected message: NetworkMessage::DialPeer"); + } + } + } + + assert_eq!(controller.state, SyncState::ConnectingPeers); + } + + #[tokio::test] + async fn test_request_chunks() { + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = create_default_controller(task_executor, None); + + controller.state = SyncState::AwaitingDownload { + since: Instant::now(), + }; + controller.try_request_next(); + assert_eq!(controller.state, SyncState::Idle); + assert!(network_recv.try_recv().is_err()); + + let new_peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let addr: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + + controller.peers.add_new_peer(new_peer_id, addr.clone()); + controller + .peers + .update_state_force(&new_peer_id, PeerState::Connected); + + controller.try_request_next(); + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::SendRequest { + peer_id, + request_id, + request, + } => { + assert_eq!(peer_id, new_peer_id); + assert_eq!( + request, + Request::GetChunks(GetChunksRequest { + tx_id: controller.tx_id, + index_start: 0, + index_end: 123, + }) + ); + + match request_id { + network::RequestId::Sync(sync_id) => match sync_id { + network::SyncId::SerialSync { tx_id } => { + assert_eq!(tx_id, controller.tx_id); + } + }, + _ => { + panic!("Not expected message: network::RequestId::Sync"); + } + } + } + _ => { + panic!("Not expected message: NetworkMessage::SendRequest"); + } + } + } + + assert!(matches!( + *controller.get_status(), + SyncState::Downloading { .. } + )); + } + + #[tokio::test] + async fn test_ban_peer() { + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = create_default_controller(task_executor, None); + + let new_peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + controller.ban_peer(new_peer_id, "unit test"); + + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, new_peer_id); + match action { + PeerAction::Fatal => {} + _ => { + panic!("PeerAction expect Fatal"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + + assert_eq!(msg, "unit test"); + } + _ => { + panic!("Not received expected message: NetworkMessage::ReportPeer"); + } + } + } + } + + #[tokio::test] + async fn test_report_peer() { + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (controller, mut network_recv) = create_default_controller(task_executor, None); + + let new_peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + controller + .ctx + .report_peer(new_peer_id, PeerAction::MidToleranceError, "unit test"); + + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, new_peer_id); + match action { + PeerAction::MidToleranceError => {} + _ => { + panic!("PeerAction expect MidToleranceError"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + + assert_eq!(msg, "unit test"); + } + _ => { + panic!("Not expected message: NetworkMessage::ReportPeer"); + } + } + } + } + + #[test] + fn test_peer_connected() { + let new_peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, _) = create_default_controller(task_executor, Some(new_peer_id)); + + let addr: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + controller.peers.add_new_peer(new_peer_id, addr); + + controller.on_peer_connected(new_peer_id); + assert_eq!( + controller.peers.peer_state(&new_peer_id), + Some(PeerState::Found) + ); + + controller + .peers + .update_state_force(&new_peer_id, PeerState::Connecting); + controller.on_peer_connected(new_peer_id); + assert_eq!( + controller.peers.peer_state(&new_peer_id), + Some(PeerState::Connected) + ); + } + + #[test] + fn test_peer_disconnected() { + let new_peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, _) = create_default_controller(task_executor, Some(new_peer_id)); + + let addr: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + controller.peers.add_new_peer(new_peer_id, addr); + + controller + .peers + .update_state_force(&new_peer_id, PeerState::Disconnecting); + controller.on_peer_disconnected(new_peer_id); + assert_eq!( + controller.peers.peer_state(&new_peer_id), + Some(PeerState::Disconnected) + ); + + controller + .peers + .update_state_force(&new_peer_id, PeerState::Found); + controller.on_peer_disconnected(new_peer_id); + assert_eq!( + controller.peers.peer_state(&new_peer_id), + Some(PeerState::Disconnected) + ); + + let new_peer_id_1 = identity::Keypair::generate_ed25519().public().to_peer_id(); + controller.on_peer_disconnected(new_peer_id_1); + assert_eq!(controller.peers.peer_state(&new_peer_id_1), None); + } + + // FIXME(zz): enable. + // #[tokio::test] + #[allow(unused)] + async fn test_response_mismatch_state_mismatch() { + let init_peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (controller, mut network_recv) = + create_default_controller(task_executor, Some(init_peer_id)); + + assert!(controller.handle_on_response_mismatch(init_peer_id)); + + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, init_peer_id); + match action { + PeerAction::HighToleranceError => {} + _ => { + panic!("PeerAction expect HighToleranceError"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + + assert_eq!(msg, "Sync state mismatch"); + } + _ => { + panic!("Not expected message: NetworkMessage::ReportPeer"); + } + } + } + } + + #[tokio::test] + async fn test_response_mismatch_peer_id_mismatch() { + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = + create_default_controller(task_executor, Some(peer_id)); + + let peer_id_1 = identity::Keypair::generate_ed25519().public().to_peer_id(); + + controller.state = SyncState::Downloading { + peer_id, + from_chunk: 0, + to_chunk: 1, + since: Instant::now(), + }; + assert!(controller.handle_on_response_mismatch(peer_id_1)); + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, peer_id_1); + match action { + PeerAction::LowToleranceError => {} + _ => { + panic!("PeerAction expect MidToleranceError"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + + assert_eq!(msg, "Peer id mismatch"); + } + _ => { + panic!("Not expected message: NetworkMessage::ReportPeer"); + } + } + } + } + + #[tokio::test] + #[should_panic(expected = "Invalid chunk boundaries")] + #[ignore = "only panic in debug mode"] + async fn test_response_panic() { + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + + let tx_seq = 0; + let chunk_count = 123; + let (store, peer_store, txs, _) = create_2_store(vec![chunk_count]); + + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, _) = create_controller( + task_executor, + Some(peer_id), + store, + txs[0].id(), + chunk_count, + ); + + let chunks = peer_store + .read() + .await + .get_chunks_with_proof_by_tx_and_index_range(tx_seq, 0, chunk_count) + .unwrap() + .unwrap(); + + controller.state = SyncState::Downloading { + peer_id, + from_chunk: 0, + to_chunk: 0, + since: Instant::now(), + }; + controller.on_response(peer_id, chunks).await; + } + + #[tokio::test] + async fn test_response_chunk_len_invalid() { + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + + let tx_seq = 0; + let chunk_count = 123; + let (store, peer_store, txs, _) = create_2_store(vec![chunk_count]); + + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = create_controller( + task_executor, + Some(peer_id), + store, + txs[0].id(), + chunk_count, + ); + + let mut chunks = peer_store + .read() + .await + .get_chunks_with_proof_by_tx_and_index_range(tx_seq, 0, chunk_count) + .unwrap() + .unwrap(); + + controller.state = SyncState::Downloading { + peer_id, + from_chunk: 0, + to_chunk: chunk_count as u64, + since: Instant::now(), + }; + + chunks.chunks.data = Vec::new(); + controller.on_response(peer_id, chunks).await; + assert_eq!(*controller.get_status(), SyncState::Idle); + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, peer_id); + match action { + PeerAction::Fatal => {} + _ => { + panic!("PeerAction expect Fatal"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + + assert_eq!(msg, "Invalid chunk response data length"); + } + _ => { + panic!("Not expected message: NetworkMessage::ReportPeer"); + } + } + } + } + + // FIXME(zz): enable. + // #[tokio::test] + #[allow(unused)] + async fn test_response_chunk_index_invalid() { + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + + let tx_seq = 0; + let chunk_count = 123; + let (store, peer_store, txs, _) = create_2_store(vec![chunk_count]); + + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = create_controller( + task_executor, + Some(peer_id), + store, + txs[0].id(), + chunk_count, + ); + + let chunks = peer_store + .read() + .await + .get_chunks_with_proof_by_tx_and_index_range(tx_seq, 0, chunk_count) + .unwrap() + .unwrap(); + + controller.state = SyncState::Downloading { + peer_id, + from_chunk: 1, + to_chunk: chunk_count as u64, + since: Instant::now(), + }; + + controller.on_response(peer_id, chunks).await; + assert_eq!(*controller.get_status(), SyncState::Idle); + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, peer_id); + match action { + PeerAction::Fatal => {} + _ => { + panic!("PeerAction expect Fatal"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + + assert_eq!(msg, "Invalid chunk response range"); + } + _ => { + panic!("Not expected message: NetworkMessage::ReportPeer"); + } + } + } + } + + #[tokio::test] + async fn test_response_validate_failed() { + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + + let tx_seq = 0; + let chunk_count = 123; + let (store, peer_store, txs, _) = create_2_store(vec![chunk_count]); + + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = create_controller( + task_executor, + Some(peer_id), + store, + txs[0].id(), + chunk_count, + ); + + let chunks = peer_store + .read() + .await + .get_chunks_with_proof_by_tx_and_index_range(tx_seq, 0, chunk_count) + .unwrap() + .unwrap(); + + controller.state = SyncState::Downloading { + peer_id, + from_chunk: 0, + to_chunk: chunk_count as u64, + since: Instant::now(), + }; + + controller.tx_seq = 1; + controller.tx_id = TxID { + seq: 1, + hash: H256::random(), + }; + + controller.on_response(peer_id, chunks).await; + assert_eq!(*controller.get_status(), SyncState::Idle); + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, peer_id); + match action { + PeerAction::Fatal => {} + _ => { + panic!("PeerAction expect Fatal"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + + assert_eq!(msg, "Chunk array validation failed"); + } + _ => { + panic!("Not expected message: NetworkMessage::ReportPeer"); + } + } + } + } + + // FIXME(zz): enable. + // #[tokio::test] + #[allow(unused)] + async fn test_response_put_failed() { + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + + let tx_seq = 0; + let chunk_count = 123; + let (_, peer_store, txs, _) = create_2_store(vec![chunk_count]); + + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = create_controller( + task_executor, + Some(peer_id), + peer_store.clone(), + txs[0].id(), + chunk_count, + ); + + let chunks = peer_store + .read() + .await + .get_chunks_with_proof_by_tx_and_index_range(tx_seq, 0, chunk_count) + .unwrap() + .unwrap(); + + controller.state = SyncState::Downloading { + peer_id, + from_chunk: 0, + to_chunk: chunk_count as u64, + since: Instant::now(), + }; + + controller.on_response(peer_id, chunks).await; + match controller.get_status() { + SyncState::Failed { reason } => { + assert!(matches!(reason, FailureReason::DBError(..))); + } + _ => { + panic!("Not expected SyncState"); + } + } + + assert!(network_recv.try_recv().is_err()); + } + + #[tokio::test] + async fn test_response_finalize_failed() { + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + + let tx_seq = 0; + let chunk_count = 2049; + let (store, peer_store, txs, _) = create_2_store(vec![chunk_count]); + + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = create_controller( + task_executor, + Some(peer_id), + store, + txs[0].id(), + chunk_count, + ); + + let chunks = peer_store + .read() + .await + .get_chunks_with_proof_by_tx_and_index_range(tx_seq, 0, 2048) + .unwrap() + .unwrap(); + + controller.state = SyncState::Downloading { + peer_id, + from_chunk: 0, + to_chunk: 2048, + since: Instant::now(), + }; + + controller.num_chunks = 2048; + + controller.on_response(peer_id, chunks).await; + match controller.get_status() { + SyncState::Failed { reason } => { + assert!(matches!(reason, FailureReason::DBError(..))); + } + _ => { + panic!("Not expected SyncState"); + } + } + + assert!(network_recv.try_recv().is_err()); + } + + #[tokio::test] + async fn test_response_success() { + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + + let tx_seq = 0; + let chunk_count = 123; + let (store, peer_store, txs, _) = create_2_store(vec![chunk_count]); + + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = create_controller( + task_executor, + Some(peer_id), + store, + txs[0].id(), + chunk_count, + ); + + let chunks = peer_store + .read() + .await + .get_chunks_with_proof_by_tx_and_index_range(tx_seq, 0, chunk_count) + .unwrap() + .unwrap(); + + controller.state = SyncState::Downloading { + peer_id, + from_chunk: 0, + to_chunk: chunk_count as u64, + since: Instant::now(), + }; + + controller.on_response(peer_id, chunks).await; + assert_eq!(*controller.get_status(), SyncState::Completed); + assert!(network_recv.try_recv().is_err()); + } + + // FIXME(zz): enable. + // #[tokio::test] + #[allow(unused)] + async fn test_handle_response_failure() { + let init_peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + + let chunk_count = 123; + let (store, _, txs, _) = create_2_store(vec![chunk_count]); + + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = create_controller( + task_executor, + Some(init_peer_id), + store, + txs[0].id(), + chunk_count, + ); + + for i in 0..(MAX_REQUEST_FAILURES + 1) { + controller.handle_response_failure(init_peer_id, "unit test"); + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, init_peer_id); + match action { + PeerAction::LowToleranceError => {} + _ => { + panic!("PeerAction expect LowToleranceError"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + + assert_eq!(msg, "unit test"); + } + _ => { + panic!("Not expected message: NetworkMessage::ReportPeer"); + } + } + } + + assert_eq!(controller.failures, i + 1); + if i == MAX_REQUEST_FAILURES { + assert_eq!(*controller.get_status(), SyncState::Idle); + + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, init_peer_id); + match action { + PeerAction::Fatal => {} + _ => { + panic!("PeerAction expect Fatal"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + + assert_eq!(msg, "unit test"); + } + _ => { + panic!("Not expected message: NetworkMessage::ReportPeer"); + } + } + } + } else { + assert!(matches!( + *controller.get_status(), + SyncState::AwaitingDownload { .. } + )); + } + } + } + + fn create_default_controller( + task_executor: TaskExecutor, + peer_id: Option, + ) -> (SerialSyncController, UnboundedReceiver) { + let tx_id = TxID { + seq: 0, + hash: H256::random(), + }; + let num_chunks = 123; + + let config = LogConfig::default(); + let store = Arc::new(RwLock::new(LogManager::memorydb(config).unwrap())); + + create_controller(task_executor, peer_id, store, tx_id, num_chunks) + } + + fn create_controller( + task_executor: TaskExecutor, + peer_id: Option, + store: Arc>, + tx_id: TxID, + num_chunks: usize, + ) -> (SerialSyncController, UnboundedReceiver) { + let (network_send, network_recv) = mpsc::unbounded_channel::(); + let ctx = Arc::new(SyncNetworkContext::new(network_send)); + + let peer_id = match peer_id { + Some(v) => v, + _ => identity::Keypair::generate_ed25519().public().to_peer_id(), + }; + + let file_location_cache = create_file_location_cache(peer_id, vec![tx_id]); + + let controller = SerialSyncController::new( + tx_id, + num_chunks as u64, + ctx, + Store::new(store, task_executor), + file_location_cache, + ); + + (controller, network_recv) + } +} diff --git a/node/sync/src/lib.rs b/node/sync/src/lib.rs new file mode 100644 index 0000000..dd5be31 --- /dev/null +++ b/node/sync/src/lib.rs @@ -0,0 +1,42 @@ +#[macro_use] +extern crate tracing; + +mod auto_sync; +mod context; +mod controllers; +mod service; +pub mod test_util; + +pub use controllers::FileSyncInfo; +use duration_str::deserialize_duration; +use serde::Deserialize; +pub use service::{SyncMessage, SyncReceiver, SyncRequest, SyncResponse, SyncSender, SyncService}; +use std::time::Duration; + +#[derive(Clone, Debug, Deserialize)] +#[serde(default)] +pub struct Config { + pub auto_sync_disabled: bool, + pub max_sync_files: usize, + #[serde(deserialize_with = "deserialize_duration")] + pub find_peer_timeout: Duration, + pub enable_chunk_request: bool, +} + +impl Default for Config { + fn default() -> Self { + Self { + auto_sync_disabled: false, + max_sync_files: 100, + find_peer_timeout: Duration::from_secs(30), + enable_chunk_request: false, + } + } +} + +impl Config { + pub fn disable_auto_sync(mut self) -> Self { + self.auto_sync_disabled = true; + self + } +} diff --git a/node/sync/src/service.rs b/node/sync/src/service.rs new file mode 100644 index 0000000..5a5a740 --- /dev/null +++ b/node/sync/src/service.rs @@ -0,0 +1,1576 @@ +use crate::auto_sync::AutoSyncManager; +use crate::context::SyncNetworkContext; +use crate::controllers::{FailureReason, FileSyncInfo, SerialSyncController, SyncState}; +use crate::Config; +use anyhow::{bail, Result}; +use file_location_cache::FileLocationCache; +use libp2p::swarm::DialError; +use log_entry_sync::LogSyncEvent; +use network::{ + rpc::GetChunksRequest, rpc::RPCResponseErrorCode, Multiaddr, NetworkMessage, PeerId, + PeerRequestId, SyncId as RequestId, +}; +use shared_types::{bytes_to_chunks, ChunkArrayWithProof, TxID}; +use std::{ + collections::{hash_map::Entry, HashMap}, + sync::Arc, +}; +use storage::error::Result as StorageResult; +use storage::log_store::Store as LogStore; +use storage_async::Store; +use tokio::sync::{broadcast, mpsc, RwLock}; + +const HEARTBEAT_INTERVAL_SEC: u64 = 5; + +pub type SyncSender = channel::Sender; +pub type SyncReceiver = channel::Receiver; + +#[derive(Debug)] +pub enum SyncMessage { + DailFailed { + peer_id: PeerId, + err: DialError, + }, + PeerConnected { + peer_id: PeerId, + }, + PeerDisconnected { + peer_id: PeerId, + }, + RequestChunks { + peer_id: PeerId, + request_id: PeerRequestId, + request: GetChunksRequest, + }, + ChunksResponse { + peer_id: PeerId, + request_id: RequestId, + response: ChunkArrayWithProof, + }, + RpcError { + peer_id: PeerId, + request_id: RequestId, + }, + AnnounceFileGossip { + tx_id: TxID, + peer_id: PeerId, + addr: Multiaddr, + }, +} + +#[derive(Debug)] +pub enum SyncRequest { + SyncStatus { tx_seq: u64 }, + SyncFile { tx_seq: u64 }, + FileSyncInfo { tx_seq: Option }, + TerminateFileSync { tx_seq: u64, is_reverted: bool }, +} + +#[derive(Debug)] +pub enum SyncResponse { + SyncStatus { status: Option }, + SyncFile { err: String }, + FileSyncInfo { result: HashMap }, + TerminateFileSync { count: usize }, +} + +pub struct SyncService { + config: Config, + + /// A receiving channel sent by the message processor thread. + msg_recv: channel::Receiver, + + /// A network context to contact the network service. + ctx: Arc, + + /// Log and transaction storage. + store: Store, + + /// Cache for storing and serving gossip messages. + file_location_cache: Arc, + + /// A collection of file sync controllers. + controllers: HashMap, + + /// Heartbeat interval for executing periodic tasks. + heartbeat: tokio::time::Interval, + + manager: AutoSyncManager, +} + +impl SyncService { + pub async fn spawn( + executor: task_executor::TaskExecutor, + network_send: mpsc::UnboundedSender, + store: Arc>, + file_location_cache: Arc, + event_recv: broadcast::Receiver, + ) -> Result { + Self::spawn_with_config( + Config::default(), + executor, + network_send, + store, + file_location_cache, + event_recv, + ) + .await + } + + pub async fn spawn_with_config( + config: Config, + executor: task_executor::TaskExecutor, + network_send: mpsc::UnboundedSender, + store: Arc>, + file_location_cache: Arc, + event_recv: broadcast::Receiver, + ) -> Result { + let (sync_send, sync_recv) = channel::Channel::unbounded(); + + let heartbeat = + tokio::time::interval(tokio::time::Duration::from_secs(HEARTBEAT_INTERVAL_SEC)); + + let store = Store::new(store, executor.clone()); + + let manager = + AutoSyncManager::new(store.clone(), sync_send.clone(), config.clone()).await?; + if !config.auto_sync_disabled { + manager.spwn(&executor, event_recv); + } + + let mut sync = SyncService { + config, + msg_recv: sync_recv, + ctx: Arc::new(SyncNetworkContext::new(network_send)), + store, + file_location_cache, + controllers: Default::default(), + heartbeat, + manager, + }; + + debug!("Starting sync service"); + executor.spawn(async move { Box::pin(sync.main()).await }, "sync"); + + Ok(sync_send) + } + + async fn main(&mut self) { + loop { + tokio::select! { + // received sync message + Some(msg) = self.msg_recv.recv() => { + match msg { + channel::Message::Notification(msg) => self.on_sync_msg(msg).await, + channel::Message::Request(req, sender) => self.on_sync_request(req, sender).await, + } + } + + // heartbeat + _ = self.heartbeat.tick() => self.on_heartbeat(), + } + } + } + + async fn on_sync_msg(&mut self, msg: SyncMessage) { + debug!("Sync received message {:?}", msg); + + match msg { + SyncMessage::DailFailed { peer_id, err } => { + self.on_dail_failed(peer_id, err); + } + SyncMessage::PeerConnected { peer_id } => { + self.on_peer_connected(peer_id); + } + + SyncMessage::PeerDisconnected { peer_id } => { + self.on_peer_disconnected(peer_id); + } + + SyncMessage::RequestChunks { + request_id, + peer_id, + request, + } => { + self.on_get_chunks_request(peer_id, request_id, request) + .await; + } + + SyncMessage::ChunksResponse { + peer_id, + request_id, + response, + } => { + self.on_chunks_response(peer_id, request_id, response).await; + } + + SyncMessage::RpcError { + peer_id, + request_id, + } => { + self.on_rpc_error(peer_id, request_id); + } + + SyncMessage::AnnounceFileGossip { + tx_id, + peer_id, + addr, + } => { + self.on_announce_file_gossip(tx_id, peer_id, addr).await; + } + } + } + + async fn on_sync_request( + &mut self, + req: SyncRequest, + sender: channel::ResponseSender, + ) { + match req { + SyncRequest::SyncStatus { tx_seq } => { + let status = self + .controllers + .get(&tx_seq) + .map(|c| c.get_status().clone()); + + let _ = sender.send(SyncResponse::SyncStatus { status }); + } + + SyncRequest::SyncFile { tx_seq } => { + if !self.controllers.contains_key(&tx_seq) + && self.controllers.len() >= self.config.max_sync_files + { + let _ = sender.send(SyncResponse::SyncFile { + err: format!( + "max sync file limitation reached: {:?}", + self.config.max_sync_files + ), + }); + return; + } + + let err = match self.on_start_sync_file(tx_seq, None).await { + Ok(()) => "".into(), + Err(err) => err.to_string(), + }; + + let _ = sender.send(SyncResponse::SyncFile { err }); + } + + SyncRequest::FileSyncInfo { tx_seq } => { + let mut result = HashMap::default(); + + match tx_seq { + Some(seq) => { + if let Some(controller) = self.controllers.get(&seq) { + result.insert(seq, controller.get_sync_info()); + } + } + None => { + for (seq, controller) in self.controllers.iter() { + result.insert(*seq, controller.get_sync_info()); + } + } + } + + let _ = sender.send(SyncResponse::FileSyncInfo { result }); + } + + SyncRequest::TerminateFileSync { + tx_seq, + is_reverted, + } => { + debug!(?tx_seq, "terminate file sync"); + let count = self.on_terminate_file_sync(tx_seq, is_reverted); + let _ = sender.send(SyncResponse::TerminateFileSync { count }); + } + } + } + + fn on_dail_failed(&mut self, peer_id: PeerId, err: DialError) { + info!(%peer_id, "Dail to peer failed"); + + for controller in self.controllers.values_mut() { + controller.on_dail_failed(peer_id, &err); + controller.transition(); + } + } + + fn on_peer_connected(&mut self, peer_id: PeerId) { + info!(%peer_id, "Peer connected"); + + for controller in self.controllers.values_mut() { + controller.on_peer_connected(peer_id); + controller.transition(); + } + } + + fn on_peer_disconnected(&mut self, peer_id: PeerId) { + info!(%peer_id, "Peer disconnected"); + + for controller in self.controllers.values_mut() { + controller.on_peer_disconnected(peer_id); + controller.transition(); + } + } + + async fn on_get_chunks_request( + &mut self, + peer_id: PeerId, + request_id: PeerRequestId, + request: GetChunksRequest, + ) { + info!(?request, %peer_id, ?request_id, "Received GetChunks request"); + + if let Err(err) = self + .handle_chunks_request_with_db_err(peer_id, request_id, request) + .await + { + error!(%err, "Failed to handle chunks request due to db error"); + self.ctx.send(NetworkMessage::SendErrorResponse { + peer_id, + id: request_id, + error: RPCResponseErrorCode::ServerError, + reason: "DB error".into(), + }); + } + } + + async fn handle_chunks_request_with_db_err( + &mut self, + peer_id: PeerId, + request_id: PeerRequestId, + request: GetChunksRequest, + ) -> StorageResult<()> { + // ban peer for invalid chunk index range + if request.index_start >= request.index_end { + self.ctx.ban_peer(peer_id, "Invalid chunk indices"); + return Ok(()); + } + + // ban peer if invalid tx requested + let tx = match self.store.get_tx_by_seq_number(request.tx_id.seq).await? { + Some(tx) => tx, + None => { + self.ctx.ban_peer(peer_id, "Tx not found"); + return Ok(()); + } + }; + + // Transaction may be reverted during file sync + if tx.id() != request.tx_id { + self.ctx.send(NetworkMessage::SendErrorResponse { + peer_id, + error: RPCResponseErrorCode::InvalidRequest, + reason: "Tx not found (Reverted)".into(), + id: request_id, + }); + } + + // ban peer if chunk index out of bound + let num_chunks = bytes_to_chunks(tx.size as usize); + if request.index_end as usize > num_chunks { + self.ctx.ban_peer(peer_id, "Chunk index out of bound"); + return Ok(()); + } + + // file may be removed, but remote peer still find one from the file location cache + let finalized = self.store.check_tx_completed(request.tx_id.seq).await?; + if !finalized { + info!(%request.tx_id.seq, "Failed to handle chunks request due to tx not finalized"); + // FIXME(zz): If remote removes a file, we will also get failure here. + // self.ctx + // .report_peer(peer_id, PeerAction::HighToleranceError, "Tx not finalized"); + self.ctx.send(NetworkMessage::SendErrorResponse { + peer_id, + error: RPCResponseErrorCode::InvalidRequest, + reason: "Tx not finalized".into(), + id: request_id, + }); + return Ok(()); + } + + let result = self + .store + .get_chunks_with_proof_by_tx_and_index_range( + request.tx_id.seq, + request.index_start as usize, + request.index_end as usize, + ) + .await?; + + match result { + Some(chunks) => { + self.ctx.send(NetworkMessage::SendResponse { + peer_id, + id: request_id, + response: network::Response::Chunks(chunks), + }); + } + None => { + // file may be removed during downloading + warn!(%request.tx_id.seq, "Failed to handle chunks request due to chunks not found"); + self.ctx.send(NetworkMessage::SendErrorResponse { + peer_id, + error: RPCResponseErrorCode::InvalidRequest, + reason: "Chunks not found".into(), + id: request_id, + }); + } + } + + Ok(()) + } + + async fn on_chunks_response( + &mut self, + peer_id: PeerId, + request_id: RequestId, + response: ChunkArrayWithProof, + ) { + info!(%response.chunks, %peer_id, ?request_id, "Received chunks response"); + + let tx_seq = match request_id { + RequestId::SerialSync { tx_id } => tx_id.seq, + }; + + match self.controllers.get_mut(&tx_seq) { + Some(controller) => { + controller.on_response(peer_id, response).await; + controller.transition(); + } + None => { + warn!("Received chunks response for non-existent controller tx_seq={tx_seq}"); + } + } + } + + fn on_rpc_error(&mut self, peer_id: PeerId, request_id: RequestId) { + info!(%peer_id, ?request_id, "Received RPC error"); + + let tx_seq = match request_id { + RequestId::SerialSync { tx_id } => tx_id.seq, + }; + + match self.controllers.get_mut(&tx_seq) { + Some(controller) => { + controller.on_request_failed(peer_id); + controller.transition(); + } + None => { + warn!("Received rpc error for non-existent controller tx_seq={tx_seq}"); + } + } + } + + async fn on_start_sync_file( + &mut self, + tx_seq: u64, + maybe_peer: Option<(PeerId, Multiaddr)>, + ) -> Result<()> { + info!(%tx_seq, "Start to sync file"); + if !self.config.enable_chunk_request { + return Ok(()); + } + + // remove failed entry if caused by tx reverted, so as to re-sync + // file with latest tx_id. + let mut tx_reverted = false; + if let Some(controller) = self.controllers.get(&tx_seq) { + if let SyncState::Failed { + reason: FailureReason::TxReverted(..), + } = controller.get_status() + { + tx_reverted = true; + } + } + + if tx_reverted { + self.controllers.remove(&tx_seq); + } + + let controller = match self.controllers.entry(tx_seq) { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => { + let tx = match self.store.get_tx_by_seq_number(tx_seq).await? { + Some(tx) => tx, + None => bail!("transaction not found"), + }; + + let num_chunks = match usize::try_from(tx.size) { + Ok(size) => bytes_to_chunks(size), + Err(_) => { + error!(%tx_seq, "Unexpected transaction size: {}", tx.size); + bail!("Unexpected transaction size"); + } + }; + + // file already exists + if self.store.check_tx_completed(tx_seq).await? { + bail!("File already exists"); + } + + entry.insert(SerialSyncController::new( + tx.id(), + num_chunks as u64, + self.ctx.clone(), + self.store.clone(), + self.file_location_cache.clone(), + )) + } + }; + + // trigger retry after failure + if let SyncState::Failed { .. } = controller.get_status() { + controller.reset(); + } + + if let Some((peer_id, addr)) = maybe_peer { + controller.on_peer_found(peer_id, addr); + } + + controller.transition(); + + Ok(()) + } + + async fn on_announce_file_gossip(&mut self, tx_id: TxID, peer_id: PeerId, addr: Multiaddr) { + let tx_seq = tx_id.seq; + info!(%tx_seq, %peer_id, %addr, "Received AnnounceFile gossip"); + + self.manager.update_on_announcement(tx_seq).await; + + // File already in sync + if let Some(controller) = self.controllers.get_mut(&tx_seq) { + controller.on_peer_found(peer_id, addr); + controller.transition(); + return; + } + + // File already exists and ignore the AnnounceFile message + match self.store.check_tx_completed(tx_seq).await { + Ok(true) => return, + Ok(false) => {} + Err(err) => { + error!(%tx_seq, %err, "Failed to check if file finalized"); + return; + } + } + + // Now, always sync files among all nodes + if let Err(err) = self.on_start_sync_file(tx_seq, Some((peer_id, addr))).await { + // FIXME(zz): This is possible for tx missing. Is it expected? + error!(%tx_seq, %err, "Failed to sync file"); + } + } + + /// Terminate file sync of `min_tx_seq`. + /// If `is_reverted` is `true` (means confirmed transactions reverted), + /// also terminate `tx_seq` greater than `min_tx_seq` + /// + /// Note, this function should be as fast as possible to avoid + /// message lagged in channel. + fn on_terminate_file_sync(&mut self, min_tx_seq: u64, is_reverted: bool) -> usize { + let mut to_terminate = vec![]; + + if is_reverted { + for (tx_seq, _) in self.controllers.iter() { + if *tx_seq >= min_tx_seq { + to_terminate.push(*tx_seq); + } + } + } else { + to_terminate.push(min_tx_seq); + } + + for tx_seq in to_terminate.iter() { + self.controllers.remove(tx_seq); + } + + to_terminate.len() + } + + fn on_heartbeat(&mut self) { + let mut completed = vec![]; + + for (&tx_seq, controller) in self.controllers.iter_mut() { + controller.transition(); + + if let SyncState::Completed = controller.get_status() { + completed.push(tx_seq); + } + } + + for tx_seq in completed { + self.controllers.remove(&tx_seq); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_util::create_2_store; + use crate::test_util::tests::create_file_location_cache; + use libp2p::identity; + use network::discovery::ConnectionId; + use network::rpc::SubstreamId; + use network::ReportSource; + use shared_types::ChunkArray; + use shared_types::Transaction; + use std::thread; + use std::time::Duration; + use std::time::Instant; + use storage::log_store::log_manager::LogConfig; + use storage::log_store::log_manager::LogManager; + use storage::log_store::LogStoreRead; + use storage::H256; + use task_executor::test_utils::TestRuntime; + use tokio::sync::mpsc::UnboundedReceiver; + use tokio::sync::mpsc::UnboundedSender; + + struct TestSyncRuntime { + runtime: TestRuntime, + + chunk_count: usize, + store: Arc>, + peer_store: Arc>, + txs: Vec, + init_data: Vec, + + init_peer_id: PeerId, + file_location_cache: Arc, + + network_send: UnboundedSender, + network_recv: UnboundedReceiver, + event_send: broadcast::Sender, + } + + impl Default for TestSyncRuntime { + fn default() -> Self { + TestSyncRuntime::new(vec![1535], 1) + } + } + + impl TestSyncRuntime { + fn new(chunk_counts: Vec, seq_size: usize) -> Self { + let chunk_count = chunk_counts[0]; + let (store, peer_store, txs, data) = create_2_store(chunk_counts); + let init_data = data[0].clone(); + let init_peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let (network_send, network_recv) = mpsc::unbounded_channel::(); + let (event_send, _) = broadcast::channel(16); + + let tx_ids = txs.iter().take(seq_size).map(|tx| tx.id()).collect(); + + Self { + runtime: TestRuntime::default(), + chunk_count, + store, + peer_store, + txs, + init_data, + init_peer_id, + file_location_cache: create_file_location_cache(init_peer_id, tx_ids), + network_send, + network_recv, + event_send, + } + } + + async fn spawn_sync_service(&self, with_peer_store: bool) -> SyncSender { + let store = if with_peer_store { + self.peer_store.clone() + } else { + self.store.clone() + }; + + SyncService::spawn_with_config( + Config::default().disable_auto_sync(), + self.runtime.task_executor.clone(), + self.network_send.clone(), + store, + self.file_location_cache.clone(), + self.event_send.subscribe(), + ) + .await + .unwrap() + } + } + + #[tokio::test] + async fn test_peer_connected_not_in_controller() { + let runtime = TestRuntime::default(); + + let chunk_count = 1535; + let (_, store, txs, _) = create_2_store(vec![chunk_count]); + let store = Store::new(store, runtime.task_executor.clone()); + + let init_peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let file_location_cache: Arc = + create_file_location_cache(init_peer_id, vec![txs[0].id()]); + + let (network_send, mut network_recv) = mpsc::unbounded_channel::(); + let (sync_send, sync_recv) = channel::Channel::unbounded(); + + let heartbeat = tokio::time::interval(Duration::from_secs(HEARTBEAT_INTERVAL_SEC)); + let manager = AutoSyncManager::new(store.clone(), sync_send, Config::default()) + .await + .unwrap(); + + let mut sync = SyncService { + config: Config::default().disable_auto_sync(), + msg_recv: sync_recv, + ctx: Arc::new(SyncNetworkContext::new(network_send)), + store, + file_location_cache, + controllers: Default::default(), + heartbeat, + manager, + }; + + sync.on_peer_connected(init_peer_id); + assert!(network_recv.try_recv().is_err()); + } + + #[tokio::test] + async fn test_peer_disconnected_not_in_controller() { + let runtime = TestRuntime::default(); + + let chunk_count = 1535; + let (_, store, txs, _) = create_2_store(vec![chunk_count]); + let store = Store::new(store, runtime.task_executor.clone()); + + let init_peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let file_location_cache: Arc = + create_file_location_cache(init_peer_id, vec![txs[0].id()]); + + let (network_send, mut network_recv) = mpsc::unbounded_channel::(); + let (sync_send, sync_recv) = channel::Channel::unbounded(); + + let heartbeat = tokio::time::interval(Duration::from_secs(HEARTBEAT_INTERVAL_SEC)); + let manager = AutoSyncManager::new(store.clone(), sync_send, Config::default()) + .await + .unwrap(); + + let mut sync = SyncService { + config: Config::default().disable_auto_sync(), + msg_recv: sync_recv, + ctx: Arc::new(SyncNetworkContext::new(network_send)), + store, + file_location_cache, + controllers: Default::default(), + heartbeat, + manager, + }; + + sync.on_peer_disconnected(init_peer_id); + assert!(network_recv.try_recv().is_err()); + } + + #[tokio::test] + async fn test_request_chunks() { + let mut runtime = TestSyncRuntime::default(); + let sync_send = runtime.spawn_sync_service(true).await; + + let request = GetChunksRequest { + tx_id: runtime.txs[0].id(), + index_start: 0, + index_end: runtime.chunk_count as u64, + }; + + sync_send + .notify(SyncMessage::RequestChunks { + request_id: (ConnectionId::new(0), SubstreamId(0)), + peer_id: runtime.init_peer_id, + request, + }) + .unwrap(); + + if let Some(msg) = runtime.network_recv.recv().await { + match msg { + NetworkMessage::SendResponse { + peer_id, + response, + id, + } => match response { + network::Response::Chunks(response) => { + assert_eq!(peer_id, runtime.init_peer_id); + assert_eq!(id.0, ConnectionId::new(0)); + assert_eq!(id.1 .0, 0); + + let data = runtime.init_data.clone(); + let chunk_array = ChunkArray { + data, + start_index: 0, + }; + + assert_eq!( + response.chunks, + chunk_array + .sub_array(0, runtime.chunk_count as u64) + .unwrap() + ); + + runtime + .peer_store + .read() + .await + .validate_range_proof(0, &response) + .expect("validate proof"); + } + _ => { + panic!("Not expected message: Response::Chunks"); + } + }, + _ => { + panic!("Not expected message: NetworkMessage::SendResponse"); + } + } + } + } + + #[tokio::test] + async fn test_request_chunks_invalid_indices() { + let mut runtime = TestSyncRuntime::default(); + let sync_send = runtime.spawn_sync_service(true).await; + + let request = GetChunksRequest { + tx_id: runtime.txs[0].id(), + index_start: 0, + index_end: 0_u64, + }; + + sync_send + .notify(SyncMessage::RequestChunks { + request_id: (ConnectionId::new(0), SubstreamId(0)), + peer_id: runtime.init_peer_id, + request, + }) + .unwrap(); + + if let Some(msg) = runtime.network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, runtime.init_peer_id); + match action { + network::PeerAction::Fatal => {} + _ => { + panic!("PeerAction expect Fatal"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + assert_eq!(msg, "Invalid chunk indices"); + } + _ => { + panic!("Not expected message: NetworkMessage::ReportPeer"); + } + } + } + } + + #[tokio::test] + async fn test_request_chunks_tx_not_exist() { + let mut runtime = TestSyncRuntime::default(); + let sync_send = runtime.spawn_sync_service(true).await; + + let request = GetChunksRequest { + tx_id: TxID { + seq: 1, + hash: H256::random(), + }, + index_start: 0, + index_end: runtime.chunk_count as u64, + }; + + sync_send + .notify(SyncMessage::RequestChunks { + request_id: (ConnectionId::new(0), SubstreamId(0)), + peer_id: runtime.init_peer_id, + request, + }) + .unwrap(); + + if let Some(msg) = runtime.network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, runtime.init_peer_id); + match action { + network::PeerAction::Fatal => {} + _ => { + panic!("PeerAction expect Fatal"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + assert_eq!(msg, "Tx not found"); + } + _ => { + panic!("Not expected message: NetworkMessage::ReportPeer"); + } + } + } + } + + #[tokio::test] + async fn test_request_chunks_index_out_bound() { + let mut runtime = TestSyncRuntime::default(); + let sync_send = runtime.spawn_sync_service(true).await; + + let request = GetChunksRequest { + tx_id: runtime.txs[0].id(), + index_start: 0, + index_end: runtime.chunk_count as u64 + 1, + }; + + sync_send + .notify(SyncMessage::RequestChunks { + request_id: (ConnectionId::new(0), SubstreamId(0)), + peer_id: runtime.init_peer_id, + request, + }) + .unwrap(); + + if let Some(msg) = runtime.network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, runtime.init_peer_id); + match action { + network::PeerAction::Fatal => {} + _ => { + panic!("PeerAction expect Fatal"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + assert_eq!(msg, "Chunk index out of bound"); + } + _ => { + panic!("Not expected message: NetworkMessage::ReportPeer"); + } + } + } + } + + // FIXME(zz): enable. + // #[tokio::test] + #[allow(unused)] + async fn test_request_chunks_tx_not_finalized() { + let mut runtime = TestSyncRuntime::default(); + let sync_send = runtime.spawn_sync_service(false).await; + + let request = GetChunksRequest { + tx_id: runtime.txs[0].id(), + index_start: 0, + index_end: runtime.chunk_count as u64, + }; + + sync_send + .notify(SyncMessage::RequestChunks { + request_id: (ConnectionId::new(0), SubstreamId(0)), + peer_id: runtime.init_peer_id, + request, + }) + .unwrap(); + + if let Some(msg) = runtime.network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, runtime.init_peer_id); + match action { + network::PeerAction::MidToleranceError => {} + _ => { + panic!("PeerAction expect MidToleranceError"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + assert_eq!(msg, "Tx not finalized"); + } + _ => { + panic!("Not expected message: NetworkMessage::ReportPeer"); + } + } + } + + if let Some(msg) = runtime.network_recv.recv().await { + match msg { + NetworkMessage::SendErrorResponse { + peer_id, + id, + error, + reason, + } => { + assert_eq!(peer_id, runtime.init_peer_id); + assert_eq!(id.1 .0, 0); + assert_eq!(error, RPCResponseErrorCode::InvalidRequest); + assert_eq!(reason, "Tx not finalized".to_string()); + } + _ => { + panic!("Not expected message: NetworkMessage::SendResponse"); + } + } + } + } + + #[tokio::test] + // #[traced_test] + async fn test_sync_file_tx_not_exist() { + let runtime = TestRuntime::default(); + + let config = LogConfig::default(); + + let store = Arc::new(RwLock::new(LogManager::memorydb(config.clone()).unwrap())); + + let init_peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let file_location_cache: Arc = + create_file_location_cache(init_peer_id, vec![]); + + let (network_send, mut network_recv) = mpsc::unbounded_channel::(); + let (_event_send, event_recv) = broadcast::channel(16); + let sync_send = SyncService::spawn_with_config( + Config::default().disable_auto_sync(), + runtime.task_executor.clone(), + network_send, + store.clone(), + file_location_cache, + event_recv, + ) + .await + .unwrap(); + + let tx_seq = 0u64; + sync_send + .request(SyncRequest::SyncFile { tx_seq }) + .await + .unwrap(); + + thread::sleep(Duration::from_millis(1000)); + assert_eq!( + store.read().await.get_tx_by_seq_number(tx_seq).unwrap(), + None + ); + assert!(network_recv.try_recv().is_err()); + } + + #[tokio::test] + async fn test_sync_file_exist_in_store() { + let mut runtime = TestSyncRuntime::default(); + let sync_send = runtime.spawn_sync_service(true).await; + + let tx_seq = 0u64; + sync_send + .request(SyncRequest::SyncFile { tx_seq }) + .await + .unwrap(); + + thread::sleep(Duration::from_millis(1000)); + assert!(runtime + .peer_store + .read() + .await + .check_tx_completed(tx_seq) + .unwrap()); + assert!(runtime.network_recv.try_recv().is_err()); + } + + async fn wait_for_tx_finalized(store: Arc>, tx_seq: u64) { + let deadline = Instant::now() + Duration::from_millis(5000); + while !store.read().await.check_tx_completed(tx_seq).unwrap() { + if Instant::now() >= deadline { + panic!("Failed to wait tx completed"); + } + + thread::sleep(Duration::from_millis(300)); + } + } + + #[tokio::test] + async fn test_sync_file_success() { + let mut runtime = TestSyncRuntime::default(); + let sync_send = runtime.spawn_sync_service(false).await; + + let tx_seq = 0u64; + sync_send + .request(SyncRequest::SyncFile { tx_seq }) + .await + .unwrap(); + + receive_dial(&mut runtime, &sync_send).await; + + assert!(!runtime + .store + .read() + .await + .check_tx_completed(tx_seq) + .unwrap()); + + assert!(!matches!( + sync_send + .request(SyncRequest::SyncStatus { tx_seq }) + .await + .unwrap(), + SyncResponse::SyncStatus { status } if status == Some(SyncState::Completed) + )); + + receive_chunk_request( + &mut runtime.network_recv, + &sync_send, + runtime.peer_store.clone(), + runtime.init_peer_id, + tx_seq, + 0, + runtime.chunk_count as u64, + ) + .await; + + wait_for_tx_finalized(runtime.store, tx_seq).await; + + // test heartbeat + let deadline = Instant::now() + Duration::from_secs(HEARTBEAT_INTERVAL_SEC + 1); + while !matches!(sync_send + .request(SyncRequest::SyncStatus { tx_seq }) + .await + .unwrap(), + SyncResponse::SyncStatus {status} if status.is_none() + ) { + if Instant::now() >= deadline { + panic!("Failed to wait heartbeat"); + } + + thread::sleep(Duration::from_millis(300)); + } + } + + #[tokio::test] + async fn test_sync_file_special_size() { + test_sync_file(1).await; + test_sync_file(511).await; + test_sync_file(512).await; + test_sync_file(513).await; + test_sync_file(514).await; + test_sync_file(1023).await; + test_sync_file(1024).await; + test_sync_file(1025).await; + test_sync_file(2047).await; + test_sync_file(2048).await; + } + + #[tokio::test] + async fn test_sync_file_exceed_max_chunks_to_request() { + let mut runtime = TestSyncRuntime::new(vec![2049], 1); + let sync_send = runtime.spawn_sync_service(false).await; + + let tx_seq = 0u64; + sync_send + .request(SyncRequest::SyncFile { tx_seq }) + .await + .unwrap(); + + receive_dial(&mut runtime, &sync_send).await; + + assert!(!runtime + .store + .read() + .await + .check_tx_completed(tx_seq) + .unwrap()); + + receive_chunk_request( + &mut runtime.network_recv, + &sync_send, + runtime.peer_store.clone(), + runtime.init_peer_id, + tx_seq, + 0, + 2048, + ) + .await; + + assert!(!matches!( + sync_send + .request(SyncRequest::SyncStatus { tx_seq }) + .await + .unwrap(), + SyncResponse::SyncStatus { status } if status == Some(SyncState::Completed) + )); + + // next batch + receive_chunk_request( + &mut runtime.network_recv, + &sync_send, + runtime.peer_store.clone(), + runtime.init_peer_id, + tx_seq, + 2048, + runtime.chunk_count as u64, + ) + .await; + + wait_for_tx_finalized(runtime.store, tx_seq).await; + } + + #[tokio::test] + async fn test_sync_file_multi_files() { + let mut runtime = TestSyncRuntime::new(vec![1535, 1535, 1535], 3); + let sync_send = runtime.spawn_sync_service(false).await; + + // second file + let tx_seq = 1u64; + sync_send + .request(SyncRequest::SyncFile { tx_seq }) + .await + .unwrap(); + + receive_dial(&mut runtime, &sync_send).await; + + assert!(!runtime + .store + .read() + .await + .check_tx_completed(tx_seq) + .unwrap()); + assert!(!runtime.store.read().await.check_tx_completed(0).unwrap()); + + receive_chunk_request( + &mut runtime.network_recv, + &sync_send, + runtime.peer_store.clone(), + runtime.init_peer_id, + tx_seq, + 0, + runtime.chunk_count as u64, + ) + .await; + + wait_for_tx_finalized(runtime.store.clone(), tx_seq).await; + + assert!(!runtime.store.read().await.check_tx_completed(0).unwrap()); + + // first file + let tx_seq = 0u64; + sync_send + .request(SyncRequest::SyncFile { tx_seq }) + .await + .unwrap(); + + receive_dial(&mut runtime, &sync_send).await; + + receive_chunk_request( + &mut runtime.network_recv, + &sync_send, + runtime.peer_store.clone(), + runtime.init_peer_id, + tx_seq, + 0, + runtime.chunk_count as u64, + ) + .await; + + wait_for_tx_finalized(runtime.store, tx_seq).await; + + sync_send + .notify(SyncMessage::PeerDisconnected { + peer_id: runtime.init_peer_id, + }) + .unwrap(); + + thread::sleep(Duration::from_millis(1000)); + assert!(runtime.network_recv.try_recv().is_err()); + } + + #[tokio::test] + async fn test_rpc_error() { + let mut runtime = TestSyncRuntime::default(); + let sync_send = runtime.spawn_sync_service(true).await; + + sync_send + .notify(SyncMessage::RpcError { + request_id: network::SyncId::SerialSync { + tx_id: runtime.txs[0].id(), + }, + peer_id: runtime.init_peer_id, + }) + .unwrap(); + + thread::sleep(Duration::from_millis(1000)); + assert!(runtime.network_recv.try_recv().is_err()); + } + + #[tokio::test] + async fn test_announce_file() { + let mut runtime = TestSyncRuntime::new(vec![1535], 0); + let sync_send = runtime.spawn_sync_service(false).await; + + let tx_seq = 0u64; + let address: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + sync_send + .notify(SyncMessage::AnnounceFileGossip { + tx_id: runtime.txs[tx_seq as usize].id(), + peer_id: runtime.init_peer_id, + addr: address, + }) + .unwrap(); + + receive_dial(&mut runtime, &sync_send).await; + + assert!(!runtime + .store + .read() + .await + .check_tx_completed(tx_seq) + .unwrap()); + + receive_chunk_request( + &mut runtime.network_recv, + &sync_send, + runtime.peer_store.clone(), + runtime.init_peer_id, + tx_seq, + 0, + runtime.chunk_count as u64, + ) + .await; + + wait_for_tx_finalized(runtime.store, tx_seq).await; + } + + #[tokio::test] + async fn test_announce_file_in_sync() { + let mut runtime = TestSyncRuntime::default(); + let sync_send = runtime.spawn_sync_service(false).await; + + let tx_seq = 0u64; + sync_send + .request(SyncRequest::SyncFile { tx_seq }) + .await + .unwrap(); + + let address: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + sync_send + .notify(SyncMessage::AnnounceFileGossip { + tx_id: runtime.txs[tx_seq as usize].id(), + peer_id: runtime.init_peer_id, + addr: address, + }) + .unwrap(); + + receive_dial(&mut runtime, &sync_send).await; + + assert!(!runtime + .store + .read() + .await + .check_tx_completed(tx_seq) + .unwrap()); + + receive_chunk_request( + &mut runtime.network_recv, + &sync_send, + runtime.peer_store.clone(), + runtime.init_peer_id, + tx_seq, + 0, + runtime.chunk_count as u64, + ) + .await; + + wait_for_tx_finalized(runtime.store, tx_seq).await; + } + + #[tokio::test] + async fn test_announce_file_already_in_store() { + let mut runtime = TestSyncRuntime::default(); + let sync_send = runtime.spawn_sync_service(true).await; + + let tx_seq = 0u64; + let address: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + sync_send + .notify(SyncMessage::AnnounceFileGossip { + tx_id: runtime.txs[tx_seq as usize].id(), + peer_id: runtime.init_peer_id, + addr: address, + }) + .unwrap(); + + thread::sleep(Duration::from_millis(1000)); + assert!(runtime.network_recv.try_recv().is_err()); + } + + #[tokio::test] + async fn test_sync_status_unknown() { + let runtime = TestSyncRuntime::default(); + let sync_send = runtime.spawn_sync_service(false).await; + + assert!(matches!( + sync_send + .request(SyncRequest::SyncStatus { tx_seq: 0 }) + .await + .unwrap(), + SyncResponse::SyncStatus { status } if status.is_none() + )); + } + + async fn receive_dial(runtime: &mut TestSyncRuntime, sync_send: &SyncSender) { + if let Some(msg) = runtime.network_recv.recv().await { + match msg { + NetworkMessage::DialPeer { + address: _, + peer_id, + } => { + assert_eq!(peer_id, runtime.init_peer_id); + + sync_send + .notify(SyncMessage::PeerConnected { peer_id }) + .unwrap(); + } + _ => { + panic!("Not expected message: NetworkMessage::DialPeer"); + } + } + } + } + + async fn test_sync_file(chunk_count: usize) { + let mut runtime = TestSyncRuntime::new(vec![chunk_count], 1); + let sync_send = runtime.spawn_sync_service(false).await; + + let tx_seq = 0u64; + sync_send + .request(SyncRequest::SyncFile { tx_seq }) + .await + .unwrap(); + + receive_dial(&mut runtime, &sync_send).await; + + assert!(!runtime + .store + .read() + .await + .check_tx_completed(tx_seq) + .unwrap()); + + assert!(!matches!( + sync_send + .request(SyncRequest::SyncStatus { tx_seq }) + .await + .unwrap(), + SyncResponse::SyncStatus { status } if status == Some(SyncState::Completed) )); + + receive_chunk_request( + &mut runtime.network_recv, + &sync_send, + runtime.peer_store.clone(), + runtime.init_peer_id, + tx_seq, + 0, + chunk_count as u64, + ) + .await; + + wait_for_tx_finalized(runtime.store, tx_seq).await; + } + + async fn receive_chunk_request( + network_recv: &mut UnboundedReceiver, + sync_send: &SyncSender, + peer_store: Arc>, + init_peer_id: PeerId, + tx_seq: u64, + index_start: u64, + index_end: u64, + ) { + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::SendRequest { + peer_id, + request, + request_id, + } => { + assert_eq!(peer_id, init_peer_id); + + let req = match request { + network::Request::GetChunks(req) => { + assert_eq!(req.tx_id.seq, tx_seq); + assert_eq!(req.index_start, index_start); + assert_eq!(req.index_end, index_end); + + req + } + _ => { + panic!("Not expected message network::Request::GetChunks"); + } + }; + + let sync_id = match request_id { + network::RequestId::Sync(sync_id) => sync_id, + _ => unreachable!("All Chunks responses belong to sync"), + }; + + let chunks = peer_store + .read() + .await + .get_chunks_with_proof_by_tx_and_index_range( + tx_seq, + req.index_start as usize, + req.index_end as usize, + ) + .unwrap() + .unwrap(); + + sync_send + .notify(SyncMessage::ChunksResponse { + peer_id, + request_id: sync_id, + response: chunks, + }) + .unwrap(); + } + _ => { + panic!("Not expected message: NetworkMessage::SendRequest"); + } + } + } + } +} diff --git a/node/sync/src/test_util.rs b/node/sync/src/test_util.rs new file mode 100644 index 0000000..15b46a6 --- /dev/null +++ b/node/sync/src/test_util.rs @@ -0,0 +1,150 @@ +use rand::random; +use shared_types::{compute_padded_chunk_size, ChunkArray, Transaction, CHUNK_SIZE}; +use std::{cmp, sync::Arc}; +use storage::{ + log_store::{ + log_manager::{sub_merkle_tree, tx_subtree_root_list_padded, LogConfig, PORA_CHUNK_SIZE}, + LogStoreChunkWrite, LogStoreWrite, + }, + LogManager, +}; +use tokio::sync::RwLock; + +/// Creates stores for local node and peers with initialized transaction of specified chunk count. +/// The first store is for local node, and data not stored. The second store is for peers, and all +/// transactions are finalized for file sync. +#[allow(clippy::type_complexity)] +pub fn create_2_store( + chunk_count: Vec, +) -> ( + Arc>, + Arc>, + Vec, + Vec>, +) { + let config = LogConfig::default(); + let mut store = LogManager::memorydb(config.clone()).unwrap(); + let mut peer_store = LogManager::memorydb(config).unwrap(); + + let mut offset = 1; + let mut txs = vec![]; + let mut data = vec![]; + + for (tx_seq, chunks) in chunk_count.iter().enumerate() { + let ret = generate_data(*chunks, &mut store, &mut peer_store, tx_seq as u64, offset); + txs.push(ret.0); + data.push(ret.1); + offset = ret.2; + } + + ( + Arc::new(RwLock::new(store)), + Arc::new(RwLock::new(peer_store)), + txs, + data, + ) +} + +fn generate_data( + chunk_count: usize, + store: &mut LogManager, + peer_store: &mut LogManager, + seq: u64, + offset: u64, +) -> (Transaction, Vec, u64) { + let data_size = CHUNK_SIZE * chunk_count; + let mut data = vec![0u8; data_size]; + + for i in 0..chunk_count { + data[i * CHUNK_SIZE] = random(); + } + + let merkel_nodes = tx_subtree_root_list_padded(&data); + let first_tree_size = 1 << (merkel_nodes[0].0 - 1); + let start_offset = if offset % first_tree_size == 0 { + offset + } else { + (offset / first_tree_size + 1) * first_tree_size + }; + + let merkle = sub_merkle_tree(&data).unwrap(); + let tx = Transaction { + stream_ids: vec![], + size: data_size as u64, + data_merkle_root: merkle.root().into(), + seq, + data: vec![], + start_entry_index: start_offset, + merkle_nodes: merkel_nodes, + }; + store.put_tx(tx.clone()).unwrap(); + peer_store.put_tx(tx.clone()).unwrap(); + for start_index in (0..chunk_count).step_by(PORA_CHUNK_SIZE) { + let end = cmp::min((start_index + PORA_CHUNK_SIZE) * CHUNK_SIZE, data.len()); + let chunk_array = ChunkArray { + data: data[start_index * CHUNK_SIZE..end].to_vec(), + start_index: start_index as u64, + }; + peer_store.put_chunks(tx.seq, chunk_array.clone()).unwrap(); + } + peer_store.finalize_tx(tx.seq).unwrap(); + + let (padded_chunk_count, _) = compute_padded_chunk_size(data_size); + (tx, data, start_offset + padded_chunk_count as u64) +} + +#[cfg(test)] +pub mod tests { + use file_location_cache::{test_util::AnnounceFileBuilder, FileLocationCache}; + use libp2p::PeerId; + use shared_types::TxID; + use std::sync::Arc; + use storage::{ + log_store::{log_manager::LogConfig, Store as LogStore}, + LogManager, + }; + use storage_async::Store; + use task_executor::test_utils::TestRuntime; + use tokio::sync::RwLock; + + pub struct TestStoreRuntime { + pub runtime: TestRuntime, + pub store: Store, + } + + impl Default for TestStoreRuntime { + fn default() -> Self { + let store = Arc::new(RwLock::new(Self::new_store())); + Self::new(store) + } + } + + impl TestStoreRuntime { + pub fn new_store() -> impl LogStore { + LogManager::memorydb(LogConfig::default()).unwrap() + } + + pub fn new(store: Arc>) -> TestStoreRuntime { + let runtime = TestRuntime::default(); + let executor = runtime.task_executor.clone(); + Self { + runtime, + store: Store::new(store, executor), + } + } + } + + pub fn create_file_location_cache(peer_id: PeerId, txs: Vec) -> Arc { + let cache = FileLocationCache::default(); + + for tx_id in txs { + let announcement = AnnounceFileBuilder::default() + .with_tx_id(tx_id) + .with_peer_id(peer_id) + .build(); + cache.insert(announcement); + } + + Arc::new(cache) + } +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1899b27 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +jsonrpcclient +pyyaml +pysha3 +coincurve +eth-utils +py-ecc +web3 +eth_tester +rtoml \ No newline at end of file diff --git a/run/config.toml b/run/config.toml new file mode 100644 index 0000000..293bb69 --- /dev/null +++ b/run/config.toml @@ -0,0 +1,32 @@ +network_dir = "network" +network_listen_address = "0.0.0.0" +# network_enr_address = "" +# network_enr_tcp_port = 1234 +# network_enr_udp_port = 1234 +network_libp2p_port = 1234 +network_discovery_port = 1234 +network_target_peers = 3 +network_boot_nodes = ["/ip4/141.148.134.20/udp/1234/p2p/16Uiu2HAmPxGNWu9eVAQPJww79J32pTJLKGcpjRMb4Qb8xxKkyuG1","/ip4/129.146.156.125/udp/1234/p2p/16Uiu2HAm93Hd5azfhkGBbkx1zero3nYHvfjQYM2NtiW4R3r5bE2g"] +network_libp2p_nodes = [] +network_private = false +network_disable_discovery = false + +db_dir = "db" + +rpc_enabled = true +rpc_listen_address = "0.0.0.0:5678" + +log_config_file = "log_config" + +blockchain_rpc_endpoint = "https://evmtestnet.confluxrpc.com" + +log_contract_address = "0xcb7afff6f048ec1fb033aa7b495ed0dd932c8656" +mine_contract_address = "0x0d0d21020d04621479489f9ebde9a0373efbb838" + +log_sync_start_block_number = 134253180 +log_page_size = 999 + +# [sync] +# auto_sync_disabled = false +# max_sync_files = 8 +# find_peer_timeout = "30s" diff --git a/run/log_config b/run/log_config new file mode 100644 index 0000000..b2b5d0d --- /dev/null +++ b/run/log_config @@ -0,0 +1 @@ +debug \ No newline at end of file diff --git a/rust-toolchain b/rust-toolchain new file mode 100644 index 0000000..5e3a425 --- /dev/null +++ b/rust-toolchain @@ -0,0 +1 @@ +1.73.0 diff --git a/tests/cache_test.py b/tests/cache_test.py new file mode 100755 index 0000000..c56fa14 --- /dev/null +++ b/tests/cache_test.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +from test_framework.test_framework import TestFramework +from utility.submission import create_submission, submit_data, data_to_segments +from utility.utils import wait_until + + +class ExampleTest(TestFramework): + def run_test(self): + client = self.nodes[0] + + chunk_data = b"\x01" * 256 * 1025 + submissions, data_root = create_submission(chunk_data) + + segments = data_to_segments(chunk_data) + client.zgs_upload_segment(segments[0]) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == 1) + wait_until(lambda: client.zgs_get_file_info(data_root) is not None) + wait_until(lambda: client.zgs_get_file_info(data_root)["isCached"]) + client.zgs_upload_segment(segments[1]) + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + + +if __name__ == "__main__": + ExampleTest().main() diff --git a/tests/ccov.sh b/tests/ccov.sh new file mode 100755 index 0000000..74ab4b7 --- /dev/null +++ b/tests/ccov.sh @@ -0,0 +1,40 @@ +#!/bin/bash +set -euo pipefail +ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )" + +cd $ROOT_DIR +echo "If you are running this script for the first time, please clean previous +debug build first by running \`rm -rf target/debug\`." + +# Install dependencies +cargo install grcov + +# Build binary and run unit tests with code coverage. +export CARGO_INCREMENTAL=0 +export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests" +export RUSTC_BOOTSTRAP=1 + +cargo build +cargo test --all + +# Run python integration tests. +export ZGS="`pwd`/target/debug/zgs_node" +./tests/test_all.py + +# Generate code coverage data +if [ -d "ccov" ] +then + rm -dr ccov +fi + +mkdir ccov +zip -0 ccov/ccov.zip `find . \( -name "*.gc*" \) -print` +grcov ccov/ccov.zip -s . -t html --llvm --branch --ignore-not-existing --ignore "/*" \ +--ignore "*target/debug/build/libp2p-*" \ +--ignore "*target/debug/build/clang-sys*" \ +--ignore "*target/debug/build/librocksdb-sys*" \ +--ignore "*target/debug/build/solang*" -o ccov + +echo "Code coverage result is saved to directory 'ccov'. +You can open 'ccov/index.html' with a web brower to start." + diff --git a/tests/cli_submission_test.py b/tests/cli_submission_test.py new file mode 100755 index 0000000..8f1c5b5 --- /dev/null +++ b/tests/cli_submission_test.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 + +import base64 +import random +import tempfile + +from config.node_config import GENESIS_ACCOUNT +from utility.submission import ENTRY_SIZE, bytes_to_entries +from utility.utils import ( + assert_equal, + wait_until, +) +from test_framework.test_framework import TestFramework + + +class CliSubmissionTest(TestFramework): + def setup_params(self): + self.num_blockchain_nodes = 2 + self.num_nodes = 2 + + def run_test(self): + data_size = [ + 2, + 255, + 256, + 257, + 1023, + 1024, + 1025, + 256 * 1023, + 256 * 1024, + 256 * 1025, + 256 * 2048, + 256 * 16385, + 256 * 1024 * 64, + ] + + for i, v in enumerate(data_size): + self.__test_upload_file_with_cli( + v, i + 1, False if v >= 256 * 1024 * 64 else True + ) + + def __test_upload_file_with_cli(self, size, submission_index, rand_data=True): + node_idx = random.randint(0, self.num_nodes - 1) + self.log.info("node index: %d, file size: %d", node_idx, size) + + file_to_upload = tempfile.NamedTemporaryFile(dir=self.root_dir, delete=False) + data = random.randbytes(size) if rand_data else b"\x10" * size + + file_to_upload.write(data) + file_to_upload.close() + + root = self._upload_file_use_cli( + self.blockchain_nodes[0].rpc_url, + self.contract.address(), + GENESIS_ACCOUNT.key, + self.nodes[node_idx].rpc_url, + file_to_upload, + ) + + self.log.info("root: %s", root) + wait_until(lambda: self.contract.num_submissions() == submission_index) + + client = self.nodes[node_idx] + wait_until(lambda: client.zgs_get_file_info(root) is not None) + wait_until(lambda: client.zgs_get_file_info(root)["finalized"]) + + num_of_entris = bytes_to_entries(size) + if num_of_entris > 1: + start_idx = random.randint(0, num_of_entris - 2) + end_idx = min( + random.randint(start_idx + 1, num_of_entris - 1), start_idx + ENTRY_SIZE + ) + + assert_equal( + client.zgs_download_segment(root, start_idx, end_idx), + base64.b64encode( + data[start_idx * ENTRY_SIZE : end_idx * ENTRY_SIZE] + ).decode("utf-8"), + ) + + for i in range(0, self.num_nodes): + if node_idx == i: + continue + + self.log.info("wait node %d", i) + self.nodes[i].admin_start_sync_file(submission_index - 1) + wait_until( + lambda: self.nodes[i].sycn_status_is_completed_or_unknown( + submission_index - 1 + ) + ) + + wait_until(lambda: self.nodes[i].zgs_get_file_info(root) is not None) + wait_until(lambda: self.nodes[i].zgs_get_file_info(root)["finalized"]) + + # start_idx = random.randint(0, num_of_entris - 1) + # end_idx = min( + # random.randint(start_idx + 1, num_of_entris), start_idx + ENTRY_SIZE + # ) + + assert_equal( + client.zgs_download_segment(root, 0, 1), + self.nodes[i].zgs_download_segment(root, 0, 1), + ) + + +if __name__ == "__main__": + CliSubmissionTest().main() diff --git a/tests/config/MockToken.json b/tests/config/MockToken.json new file mode 100644 index 0000000..5d82c15 --- /dev/null +++ b/tests/config/MockToken.json @@ -0,0 +1,286 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "MockToken", + "sourceName": "contracts/token/MockToken.sol", + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x608034620003ac576040906001600160401b039080830182811182821017620002ac5783526007815260209166135bd8dad554d160ca1b8383015283519184830183811083821117620002ac578552600391828452621554d160ea1b85850152815192818411620002ac578054936001938486811c96168015620003a1575b888710146200038b578190601f9687811162000335575b508890878311600114620002ce57600092620002c2575b505060001982841b1c191690841b1781555b8451918211620002ac5760049485548481811c91168015620002a1575b888210146200028c5785811162000241575b508690858411600114620001d657938394918492600095620001ca575b50501b92600019911b1c19161782555b33156200018b57505062000130600254620003b1565b6002553360005260008152816000206200014b8154620003b1565b905560007fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef835192633b9aca0084523393a3516108bc9081620003da8239f35b60649284519262461bcd60e51b845283015260248201527f45524332303a206d696e7420746f20746865207a65726f2061646472657373006044820152fd5b0151935038806200010a565b9190601f198416928760005284896000209460005b8b898383106200022957505050106200020e575b50505050811b0182556200011a565b01519060f884600019921b161c1916905538808080620001ff565b868601518955909701969485019488935001620001eb565b86600052876000208680860160051c8201928a871062000282575b0160051c019085905b82811062000275575050620000ed565b6000815501859062000265565b925081926200025c565b602287634e487b7160e01b6000525260246000fd5b90607f1690620000db565b634e487b7160e01b600052604160045260246000fd5b015190503880620000ac565b90869350601f19831691856000528a6000209260005b8c8282106200031e575050841162000305575b505050811b018155620000be565b015160001983861b60f8161c19169055388080620002f7565b8385015186558a97909501949384019301620002e4565b90915083600052886000208780850160051c8201928b861062000381575b918891869594930160051c01915b8281106200037157505062000095565b6000815585945088910162000361565b9250819262000353565b634e487b7160e01b600052602260045260246000fd5b95607f16956200007e565b600080fd5b90633b9aca008201809211620003c357565b634e487b7160e01b600052601160045260246000fdfe608060408181526004918236101561001657600080fd5b600092833560e01c91826306fdde031461049657508163095ea7b31461046c57816318160ddd1461044d57816323b872dd14610383578163313ce56714610367578163395093511461031757816370a08231146102e057816395d89b41146101c1578163a457c2d71461011957508063a9059cbb146100e95763dd62ed3e1461009e57600080fd5b346100e557806003193601126100e557806020926100ba6105bb565b6100c26105d6565b6001600160a01b0391821683526001865283832091168252845220549051908152f35b5080fd5b50346100e557806003193601126100e5576020906101126101086105bb565b602435903361060f565b5160018152f35b905082346101be57826003193601126101be576101346105bb565b918360243592338152600160205281812060018060a01b038616825260205220549082821061016d576020856101128585038733610784565b608490602086519162461bcd60e51b8352820152602560248201527f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f77604482015264207a65726f60d81b6064820152fd5b80fd5b8383346100e557816003193601126100e557805190828454600181811c908083169283156102d6575b60209384841081146102c3578388529081156102a75750600114610252575b505050829003601f01601f191682019267ffffffffffffffff84118385101761023f575082918261023b925282610572565b0390f35b634e487b7160e01b815260418552602490fd5b8787529192508591837f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b5b8385106102935750505050830101858080610209565b80548886018301529301928490820161027d565b60ff1916878501525050151560051b8401019050858080610209565b634e487b7160e01b895260228a52602489fd5b91607f16916101ea565b5050346100e55760203660031901126100e55760209181906001600160a01b036103086105bb565b16815280845220549051908152f35b5050346100e557806003193601126100e55761011260209261036061033a6105bb565b338352600186528483206001600160a01b038216845286529184902054602435906105ec565b9033610784565b5050346100e557816003193601126100e5576020905160128152f35b839150346100e55760603660031901126100e55761039f6105bb565b6103a76105d6565b91846044359460018060a01b0384168152600160205281812033825260205220549060001982036103e1575b60208661011287878761060f565b84821061040a57509183916103ff6020969561011295033383610784565b9193948193506103d3565b606490602087519162461bcd60e51b8352820152601d60248201527f45524332303a20696e73756666696369656e7420616c6c6f77616e63650000006044820152fd5b5050346100e557816003193601126100e5576020906002549051908152f35b5050346100e557806003193601126100e55760209061011261048c6105bb565b6024359033610784565b8490843461056e578260031936011261056e5782600354600181811c90808316928315610564575b60209384841081146102c3578388529081156102a7575060011461050e57505050829003601f01601f191682019267ffffffffffffffff84118385101761023f575082918261023b925282610572565b600387529192508591837fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b5b8385106105505750505050830101858080610209565b80548886018301529301928490820161053a565b91607f16916104be565b8280fd5b6020808252825181830181905290939260005b8281106105a757505060409293506000838284010152601f8019910116010190565b818101860151848201604001528501610585565b600435906001600160a01b03821682036105d157565b600080fd5b602435906001600160a01b03821682036105d157565b919082018092116105f957565b634e487b7160e01b600052601160045260246000fd5b6001600160a01b0390811691821561073157169182156106e05760008281528060205260408120549180831061068c57604082827fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9587602096528286520382822055868152206106818282546105ec565b9055604051908152a3565b60405162461bcd60e51b815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e7420657863656564732062604482015265616c616e636560d01b6064820152608490fd5b60405162461bcd60e51b815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201526265737360e81b6064820152608490fd5b60405162461bcd60e51b815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f206164604482015264647265737360d81b6064820152608490fd5b6001600160a01b0390811691821561083557169182156107e55760207f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925918360005260018252604060002085600052825280604060002055604051908152a3565b60405162461bcd60e51b815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f206164647265604482015261737360f01b6064820152608490fd5b60405162461bcd60e51b8152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b6064820152608490fdfea26469706673582212200239d5fdc8ba06d4c9278a25c561798906e419c37123e597870f8ad34e28da2864736f6c63430008100033", + "deployedBytecode": "0x608060408181526004918236101561001657600080fd5b600092833560e01c91826306fdde031461049657508163095ea7b31461046c57816318160ddd1461044d57816323b872dd14610383578163313ce56714610367578163395093511461031757816370a08231146102e057816395d89b41146101c1578163a457c2d71461011957508063a9059cbb146100e95763dd62ed3e1461009e57600080fd5b346100e557806003193601126100e557806020926100ba6105bb565b6100c26105d6565b6001600160a01b0391821683526001865283832091168252845220549051908152f35b5080fd5b50346100e557806003193601126100e5576020906101126101086105bb565b602435903361060f565b5160018152f35b905082346101be57826003193601126101be576101346105bb565b918360243592338152600160205281812060018060a01b038616825260205220549082821061016d576020856101128585038733610784565b608490602086519162461bcd60e51b8352820152602560248201527f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f77604482015264207a65726f60d81b6064820152fd5b80fd5b8383346100e557816003193601126100e557805190828454600181811c908083169283156102d6575b60209384841081146102c3578388529081156102a75750600114610252575b505050829003601f01601f191682019267ffffffffffffffff84118385101761023f575082918261023b925282610572565b0390f35b634e487b7160e01b815260418552602490fd5b8787529192508591837f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b5b8385106102935750505050830101858080610209565b80548886018301529301928490820161027d565b60ff1916878501525050151560051b8401019050858080610209565b634e487b7160e01b895260228a52602489fd5b91607f16916101ea565b5050346100e55760203660031901126100e55760209181906001600160a01b036103086105bb565b16815280845220549051908152f35b5050346100e557806003193601126100e55761011260209261036061033a6105bb565b338352600186528483206001600160a01b038216845286529184902054602435906105ec565b9033610784565b5050346100e557816003193601126100e5576020905160128152f35b839150346100e55760603660031901126100e55761039f6105bb565b6103a76105d6565b91846044359460018060a01b0384168152600160205281812033825260205220549060001982036103e1575b60208661011287878761060f565b84821061040a57509183916103ff6020969561011295033383610784565b9193948193506103d3565b606490602087519162461bcd60e51b8352820152601d60248201527f45524332303a20696e73756666696369656e7420616c6c6f77616e63650000006044820152fd5b5050346100e557816003193601126100e5576020906002549051908152f35b5050346100e557806003193601126100e55760209061011261048c6105bb565b6024359033610784565b8490843461056e578260031936011261056e5782600354600181811c90808316928315610564575b60209384841081146102c3578388529081156102a7575060011461050e57505050829003601f01601f191682019267ffffffffffffffff84118385101761023f575082918261023b925282610572565b600387529192508591837fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b5b8385106105505750505050830101858080610209565b80548886018301529301928490820161053a565b91607f16916104be565b8280fd5b6020808252825181830181905290939260005b8281106105a757505060409293506000838284010152601f8019910116010190565b818101860151848201604001528501610585565b600435906001600160a01b03821682036105d157565b600080fd5b602435906001600160a01b03821682036105d157565b919082018092116105f957565b634e487b7160e01b600052601160045260246000fd5b6001600160a01b0390811691821561073157169182156106e05760008281528060205260408120549180831061068c57604082827fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9587602096528286520382822055868152206106818282546105ec565b9055604051908152a3565b60405162461bcd60e51b815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e7420657863656564732062604482015265616c616e636560d01b6064820152608490fd5b60405162461bcd60e51b815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201526265737360e81b6064820152608490fd5b60405162461bcd60e51b815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f206164604482015264647265737360d81b6064820152608490fd5b6001600160a01b0390811691821561083557169182156107e55760207f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925918360005260018252604060002085600052825280604060002055604051908152a3565b60405162461bcd60e51b815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f206164647265604482015261737360f01b6064820152608490fd5b60405162461bcd60e51b8152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b6064820152608490fdfea26469706673582212200239d5fdc8ba06d4c9278a25c561798906e419c37123e597870f8ad34e28da2864736f6c63430008100033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/tests/config/bsc.toml b/tests/config/bsc.toml new file mode 100644 index 0000000..2b5c92b --- /dev/null +++ b/tests/config/bsc.toml @@ -0,0 +1,101 @@ +[Eth] +SyncMode = "snap" +DisablePeerTxBroadcast = false +EthDiscoveryURLs = [] +SnapDiscoveryURLs = [] +TrustDiscoveryURLs = [] +NoPruning = false +NoPrefetch = false +DirectBroadcast = false +DisableSnapProtocol = false +DisableDiffProtocol = false +EnableTrustProtocol = false +DiffSync = false +RangeLimit = false +TxLookupLimit = 2350000 +LightPeers = 100 +UltraLightFraction = 75 +DatabaseCache = 512 +DatabaseFreezer = "" +DatabaseDiff = "" +TrieCleanCache = 154 +TrieCleanCacheJournal = "triecache" +TrieCleanCacheRejournal = 3600000000000 +TrieDirtyCache = 256 +TrieTimeout = 3600000000000 +SnapshotCache = 102 +TriesInMemory = 128 +TriesVerifyMode = "local" +Preimages = false +PersistDiff = false +DiffBlock = 86400 +PruneAncientData = false +EnablePreimageRecording = false +EWASMInterpreter = "" +EVMInterpreter = "" +RPCGasCap = 50000000 +RPCEVMTimeout = 5000000000 +RPCTxFeeCap = 1e+00 + +[Eth.Miner] +DelayLeftOver = 50000000 +GasFloor = 0 +GasCeil = 8000000 +GasPrice = 1000000000 +Recommit = 3000000000 +Noverify = false + +[Eth.Ethash] +CacheDir = "ethash" +CachesInMem = 2 +CachesOnDisk = 3 +CachesLockMmap = false +DatasetDir = "" +DatasetsInMem = 1 +DatasetsOnDisk = 2 +DatasetsLockMmap = false +PowMode = 2 +NotifyFull = false + +[Eth.TxPool] +Locals = [] +NoLocals = false +Journal = "transactions.rlp" +Rejournal = 3600000000000 +PriceLimit = 1 +PriceBump = 10 +AccountSlots = 16 +GlobalSlots = 5120 +AccountQueue = 64 +GlobalQueue = 1024 +Lifetime = 10800000000000 +ReannounceTime = 315360000000000000 + +[Eth.GPO] +Blocks = 20 +Percentile = 60 +MaxHeaderHistory = 0 +MaxBlockHistory = 0 +MaxPrice = 100000000000 +IgnorePrice = 4 +OracleThreshold = 1000 + +[Node] +IPCPath = "geth.ipc" +HTTPHost = "127.0.0.1" +HTTPVirtualHosts = ["localhost"] +HTTPModules = ["personal", "eth", "net", "web3", "admin", "txpool", "miner"] +GraphQLVirtualHosts = ["localhost"] + +[Node.P2P] +MaxPeers = 50 +NoDiscovery = false +StaticNodes = [] +VerifyNodes = [] +TrustedNodes = [] +EnableMsgEvents = false + +[Node.HTTPTimeouts] +ReadTimeout = 30000000000 +WriteTimeout = 30000000000 +IdleTimeout = 120000000000 \ No newline at end of file diff --git a/tests/config/genesis.json b/tests/config/genesis.json new file mode 100644 index 0000000..7112690 --- /dev/null +++ b/tests/config/genesis.json @@ -0,0 +1,32 @@ +{ + "config":{ + "chainId":2001, + "homesteadBlock": 0, + "eip150Block": 0, + "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "muirGlacierBlock": 0, + "ramanujanBlock": 0, + "nielsBlock": 0 + }, + "nonce":"0x0000000000000061", + "timestamp":"0x0", + "parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "gasLimit":"0x8000000", + "difficulty":"0x100", + "mixhash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase":"0x3333333333333333333333333333331234567890", + "alloc":{ + "fbe45681Ac6C53D5a40475F7526baC1FE7590fb8":{ + "balance":"400000000000000000000000000" + }, + "0e768D12395C8ABFDEdF7b1aEB0Dd1D27d5E2A7F":{ + "balance":"400000000000000000000000000" + } + } + } \ No newline at end of file diff --git a/tests/config/node_config.py b/tests/config/node_config.py new file mode 100644 index 0000000..4e02373 --- /dev/null +++ b/tests/config/node_config.py @@ -0,0 +1,50 @@ +from web3 import Web3 + +ZGS_CONFIG = { + "log_config_file": "log_config", +} + +BSC_CONFIG = dict( + NetworkId=1000, + HTTPPort=8545, + HTTPHost="127.0.0.1", + Etherbase="0x7df9a875a174b3bc565e6424a0050ebc1b2d1d82", + DataDir="test/local_ethereum_blockchain/node1", + Port=30303, + Verbosity=5, +) + +CONFLUX_CONFIG = dict( + mode="dev", + chain_id=10, + jsonrpc_http_eth_port=8545, + tcp_port=32323, + log_level="debug", + log_file="./conflux.log", + public_address="127.0.0.1", + poll_lifetime_in_seconds=60, + dev_allow_phase_change_without_peer="true", + dev_block_interval_ms=200, +) + +BLOCK_SIZE_LIMIT = 200 * 1024 +GENESIS_PRIV_KEY = "46b9e861b63d3509c88b7817275a30d22d62c8cd8fa6486ddee35ef0d8e0495f" +MINER_ID = "308a6e102a5829ba35e4ba1da0473c3e8bd45f5d3ffb91e31adb43f25463dddb" +GENESIS_ACCOUNT = Web3().eth.account.from_key(GENESIS_PRIV_KEY) +TX_PARAMS = { + "gasPrice": 10_000_000_000, + "gas": 10_000_000, + "from": GENESIS_ACCOUNT.address, + "gas": 2_000_000, +} + +GENESIS_PRIV_KEY1 = "9a6d3ba2b0c7514b16a006ee605055d71b9edfad183aeb2d9790e9d4ccced471" +GENESIS_ACCOUNT1 = Web3().eth.account.from_key(GENESIS_PRIV_KEY1) +TX_PARAMS1 = { + "gasPrice": 10_000_000_000, + "gas": 10_000_000, + "from": GENESIS_ACCOUNT1.address, +} + +NO_SEAL_FLAG = 0x1 +NO_MERKLE_PROOF_FLAG = 0x2 diff --git a/tests/crash_test.py b/tests/crash_test.py new file mode 100644 index 0000000..ed0bfc1 --- /dev/null +++ b/tests/crash_test.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 + +from test_framework.test_framework import TestFramework +from utility.submission import create_submission, submit_data +from utility.utils import wait_until + + +class CrashTest(TestFramework): + def setup_params(self): + self.num_blockchain_nodes = 1 + self.num_nodes = 4 + + def run_test(self): + # 1. all nodes are running + chunk_data = b"\x01" * 256 + submissions, data_root = create_submission(chunk_data) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == 1) + wait_until(lambda: self.nodes[0].zgs_get_file_info(data_root) is not None) + + segment = submit_data(self.nodes[0], chunk_data) + self.log.info("segment: %s", segment) + + for i in range(self.num_nodes): + self.log.info("wait for node: %s", i) + wait_until( + lambda: self.nodes[i].zgs_get_file_info(data_root) is not None + ) + wait_until( + lambda: self.nodes[i].zgs_get_file_info(data_root)["finalized"] + ) + + # 2: first node runnging, other nodes killed + self.log.info("kill node") + # kill node to simulate node crash + for i in range(1, self.num_nodes): + self.nodes[i].stop(kill=True) + + chunk_data = b"\x02" * 256 * 1024 + submissions, data_root = create_submission(chunk_data) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == 2) + wait_until(lambda: self.nodes[0].zgs_get_file_info(data_root) is not None) + + segment = submit_data(self.nodes[0], chunk_data) + wait_until(lambda: self.nodes[0].zgs_get_file_info(data_root)["finalized"]) + + self.start_storage_node(1) + self.nodes[1].wait_for_rpc_connection() + self.nodes[1].admin_start_sync_file(1) + wait_until(lambda: self.nodes[1].zgs_get_file_info(data_root) is not None) + wait_until(lambda: self.nodes[1].zgs_get_file_info(data_root)["finalized"]) + + for i in range(2, self.num_nodes): + self.start_storage_node(i) + self.nodes[i].wait_for_rpc_connection() + self.nodes[i].admin_start_sync_file(1) + + self.nodes[i].stop(kill=True) + self.start_storage_node(i) + self.nodes[i].wait_for_rpc_connection() + self.nodes[i].admin_start_sync_file(1) + wait_until( + lambda: self.nodes[i].zgs_get_file_info(data_root) is not None + ) + wait_until( + lambda: self.nodes[i].zgs_get_file_info(data_root)["finalized"] + ) + + # 4: node[1..] synced contract entries and killed + self.log.info("kill node 0") + self.nodes[0].stop(kill=True) + self.start_storage_node(0) + self.nodes[0].wait_for_rpc_connection() + + chunk_data = b"\x03" * 256 + submissions, data_root = create_submission(chunk_data) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == 3) + wait_until(lambda: self.nodes[0].zgs_get_file_info(data_root) is not None) + + for i in range(1, self.num_nodes): + self.nodes[i].stop(kill=True) + + segment = submit_data(self.nodes[0], chunk_data) + self.log.info("segment: %s", segment) + wait_until(lambda: self.nodes[0].zgs_get_file_info(data_root)["finalized"]) + + for i in range(1, self.num_nodes): + self.log.info("wait for node: %s", i) + self.start_storage_node(i) + self.nodes[i].wait_for_rpc_connection() + self.nodes[i].admin_start_sync_file(2) + wait_until( + lambda: self.nodes[i].zgs_get_file_info(data_root) is not None + ) + wait_until( + lambda: self.nodes[i].zgs_get_file_info(data_root)["finalized"] + ) + + # 5: node[1..] synced contract entries and killed, sync disorder + self.nodes[0].stop(kill=True) + self.start_storage_node(0) + self.nodes[0].wait_for_rpc_connection() + + chunk_data = b"\x04" * 256 * 1561 + submissions, data_root = create_submission(chunk_data) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == 4) + wait_until(lambda: self.nodes[0].zgs_get_file_info(data_root) is not None) + + for i in range(1, 2): + self.nodes[i].stop(kill=True) + + chunk_data1 = b"\x05" * 256 + submissions1, data_root1 = create_submission(chunk_data1) + self.contract.submit(submissions1) + wait_until(lambda: self.contract.num_submissions() == 5) + wait_until(lambda: self.nodes[0].zgs_get_file_info(data_root1) is not None) + + for i in range(2, self.num_nodes): + self.nodes[i].stop(kill=True) + + segment = submit_data(self.nodes[0], chunk_data) + wait_until(lambda: self.nodes[0].zgs_get_file_info(data_root)["finalized"]) + + segment = submit_data(self.nodes[0], chunk_data1) + wait_until(lambda: self.nodes[0].zgs_get_file_info(data_root1)["finalized"]) + + for i in range(1, self.num_nodes): + self.log.info("wait for node: %s", i) + self.start_storage_node(i) + self.nodes[i].wait_for_rpc_connection() + self.nodes[i].admin_start_sync_file(4) + wait_until( + lambda: self.nodes[i].zgs_get_file_info(data_root1) is not None + ) + wait_until( + lambda: self.nodes[i].zgs_get_file_info(data_root1)["finalized"] + ) + + self.nodes[i].admin_start_sync_file(3) + wait_until( + lambda: self.nodes[i].zgs_get_file_info(data_root) is not None + ) + wait_until( + lambda: self.nodes[i].zgs_get_file_info(data_root)["finalized"] + ) + + +if __name__ == "__main__": + CrashTest().main() diff --git a/tests/dep_pip3.sh b/tests/dep_pip3.sh new file mode 100755 index 0000000..8686bcd --- /dev/null +++ b/tests/dep_pip3.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -e + +function install() { + if [ "`pip3 show ${1%%=*}`" = "" ]; then + pip3 install $1 + fi +} + +install jsonrpcclient +install pyyaml +install pysha3 +install coincurve +install eth_utils +install py-ecc +install web3 +install eth_tester \ No newline at end of file diff --git a/tests/example_test.py b/tests/example_test.py new file mode 100755 index 0000000..25fe3d1 --- /dev/null +++ b/tests/example_test.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +from test_framework.test_framework import TestFramework +from utility.submission import create_submission, submit_data +from utility.utils import wait_until + + +class ExampleTest(TestFramework): + def run_test(self): + client = self.nodes[0] + + chunk_data = b"\x00" * 256 + submissions, data_root = create_submission(chunk_data) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == 1) + wait_until(lambda: client.zgs_get_file_info(data_root) is not None) + + segment = submit_data(client, chunk_data) + self.log.info("segment: %s", segment) + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + + +if __name__ == "__main__": + ExampleTest().main() diff --git a/tests/fuzz_test.py b/tests/fuzz_test.py new file mode 100644 index 0000000..0c17fe7 --- /dev/null +++ b/tests/fuzz_test.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 + +import random +import threading + +from test_framework.test_framework import TestFramework +from config.node_config import TX_PARAMS, TX_PARAMS1 +from utility.submission import create_submission, submit_data +from utility.utils import wait_until + +SAMLL_SIZE = 350 +LARGE_SIZE = 200 +RADMON_SIZE = 300 + + +class FuzzTest(TestFramework): + def setup_params(self): + self.num_blockchain_nodes = 1 + self.num_nodes = 4 + + def run_test(self): + lock = threading.Lock() + nodes_index = [i for i in range(self.num_nodes)] + + account_lock = threading.Lock() + accounts = [TX_PARAMS, TX_PARAMS1] + + def run_chunk_size(sizes, nodes, contract, log): + data_roots = [] + for i, v in enumerate(sizes): + log.info("submission %s, data size: %s", i, v) + + chunk_data = random.randbytes(v) + submissions, data_root = create_submission(chunk_data) + + account_lock.acquire() + account_idx = random.randint(0, len(accounts) - 1) + contract.submit(submissions, tx_prarams=accounts[account_idx]) + account_lock.release() + + lock.acquire() + client_idx = random.randint(0, len(nodes_index) - 1) + idx = nodes_index[client_idx] + nodes_index.pop(client_idx) + lock.release() + + log.info("submit data via client %s", idx) + wait_until( + lambda: nodes[idx].zgs_get_file_info(data_root) is not None + ) + segment = submit_data(nodes[idx], chunk_data) + wait_until( + lambda: nodes[idx].zgs_get_file_info(data_root)["finalized"] + ) + + lock.acquire() + nodes_index.append(idx) + lock.release() + + data_roots.append(data_root) + + for data_root in data_roots: + for idx in range(len(nodes)): + wait_until( + lambda: nodes[idx].zgs_get_file_info(data_root) is not None + ) + + def wait_finalized(): + ret = nodes[idx].zgs_get_file_info(data_root) + if ret["finalized"]: + return True + else: + nodes[idx].admin_start_sync_file(ret['tx']['seq']) + return False + + wait_until( + lambda: wait_finalized(), timeout = 180 + ) + + def run_small_chunk_size(nodes, contract, log): + sizes = [i for i in range(1, SAMLL_SIZE + 1)] + random.shuffle(sizes) + + run_chunk_size(sizes, nodes, contract, log) + + def run_large_chunk_size(nodes, contract, log): + sizes = [i for i in range(256 * 1024 * 256 - LARGE_SIZE, 256 * 1024 * 256 )] + random.shuffle(sizes) + + run_chunk_size(sizes, nodes, contract, log) + + def run_random_chunk_size(nodes, contract, log): + sizes = [] + for i in range(RADMON_SIZE): + sizes.append(random.randint(1, 256 * 1024 * 256)) + + run_chunk_size(sizes, nodes, contract, log) + + t1 = threading.Thread( + target=run_small_chunk_size, args=(self.nodes, self.contract, self.log) + ) + t2 = threading.Thread( + target=run_large_chunk_size, args=(self.nodes, self.contract, self.log) + ) + t3 = threading.Thread( + target=run_random_chunk_size, args=(self.nodes, self.contract, self.log) + ) + + t1.start() + t2.start() + t3.start() + + t1.join() + t2.join() + t3.join() + + +if __name__ == "__main__": + FuzzTest().main() diff --git a/tests/mine_test.py b/tests/mine_test.py new file mode 100755 index 0000000..481727d --- /dev/null +++ b/tests/mine_test.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +from test_framework.blockchain_node import BlockChainNodeType +from test_framework.test_framework import TestFramework +from config.node_config import MINER_ID, GENESIS_PRIV_KEY +from test_framework.blockchain_node import BlockChainNodeType +from utility.submission import create_submission, submit_data +from utility.utils import wait_until + + +class MineTest(TestFramework): + def setup_params(self): + self.num_blockchain_nodes = 1 + self.num_nodes = 1 + self.zgs_node_configs[0] = { + "miner_id": MINER_ID, + "miner_key": GENESIS_PRIV_KEY, + } + + def submit_data(self, item, size): + submissions_before = self.contract.num_submissions() + client = self.nodes[0] + chunk_data = item * 256 * size + submissions, data_root = create_submission(chunk_data) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == submissions_before + 1) + wait_until(lambda: client.zgs_get_file_info(data_root) is not None) + + segment = submit_data(client, chunk_data) + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + + def run_test(self): + blockchain = self.blockchain_nodes[0] + + self.log.info("flow address: %s", self.contract.address()) + self.log.info("mine address: %s", self.mine_contract.address()) + + quality = int(2**256 / 40960) + self.mine_contract.set_quality(quality) + + self.log.info("Submit the first data chunk") + self.submit_data(b"\x11", 2000) + + self.log.info("Wait for the first mine context release") + wait_until(lambda: int(blockchain.eth_blockNumber(), 16) > 100, timeout=180) + + self.log.info("Wait for the first mine answer") + wait_until(lambda: self.mine_contract.last_mined_epoch() == 1) + + self.log.info("Wait for the second mine context release") + wait_until(lambda: int(blockchain.eth_blockNumber(), 16) > 200, timeout=180) + + self.log.info("Wait for the second mine answer") + wait_until(lambda: self.mine_contract.last_mined_epoch() == 2) + + self.nodes[0].miner_stop() + self.log.info("Wait for the third mine context release") + wait_until(lambda: int(blockchain.eth_blockNumber(), 16) > 307, timeout=180) + self.log.info("Submit the second data chunk") + self.submit_data(b"\x22", 2000) + # Now the storage node should have the latest flow, but the mining context is using an old one. + self.nodes[0].miner_start() + + self.log.info("Wait for the third mine answer") + wait_until(lambda: self.mine_contract.last_mined_epoch() == 3) + + +if __name__ == "__main__": + MineTest(blockchain_node_type=BlockChainNodeType.BSC).main() diff --git a/tests/parallel_submission_test.py b/tests/parallel_submission_test.py new file mode 100644 index 0000000..8128ade --- /dev/null +++ b/tests/parallel_submission_test.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 + +import random +import threading + +from utility.submission import create_submission, submit_data +from utility.utils import ( + wait_until, +) +from test_framework.test_framework import TestFramework + + +class ParallelSubmissionTest(TestFramework): + def setup_params(self): + self.num_blockchain_nodes = 2 + self.num_nodes = 2 + + def run_test(self): + size = 245759 + thread_count = 16 + + chunks = self.generate_data(size, thread_count) + for submission, _, _ in chunks: + self.log.info("submission: %s", submission) + self.contract.submit(submission) + + wait_until(lambda: self.contract.num_submissions() == thread_count) + + client = self.nodes[0] + for _, data_root, _ in chunks: + wait_until(lambda: client.zgs_get_file_info(data_root) is not None) + + def submit(client, chunk_data): + submit_data(client, chunk_data) + + threads = [] + for _, _, chunk_data in chunks: + t = threading.Thread(target=submit, args=(client, chunk_data)) + threads.append(t) + + for t in threads[::-1]: + t.start() + + for t in threads: + t.join() + + for _, data_root, _ in chunks: + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + + def generate_data(self, size, num): + res = [] + for _ in range(num): + chunk_data = random.randbytes(size) + submission = create_submission(chunk_data) + res.append((submission[0], submission[1], chunk_data)) + + return res + + +if __name__ == "__main__": + ParallelSubmissionTest().main() diff --git a/tests/partial_submission_test.py b/tests/partial_submission_test.py new file mode 100755 index 0000000..e2fb3df --- /dev/null +++ b/tests/partial_submission_test.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +from test_framework.test_framework import TestFramework +from utility.submission import create_submission, submit_data, data_to_segments +from utility.utils import wait_until + + +class ExampleTest(TestFramework): + def run_test(self): + client = self.nodes[0] + + chunk_data = b"\x01" * 256 * 1025 + submissions, data_root = create_submission(chunk_data) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == 1) + wait_until(lambda: client.zgs_get_file_info(data_root) is not None) + + segments = data_to_segments(chunk_data) + client.zgs_upload_segment(segments[1]) + segment = client.zgs_download_segment(data_root, 1024, 1025) + print(segment) + print(client.rpc.zgs_downloadSegmentWithProof([data_root, 1])) + + +if __name__ == "__main__": + ExampleTest().main() diff --git a/tests/random_test.py b/tests/random_test.py new file mode 100755 index 0000000..c3eea3c --- /dev/null +++ b/tests/random_test.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 + +import random + +from test_framework.blockchain_node import BlockChainNodeType +from test_framework.test_framework import TestFramework +from utility.submission import create_submission +from utility.submission import submit_data +from utility.utils import ( + assert_equal, + wait_until, +) + + +class RandomTest(TestFramework): + def setup_params(self): + self.num_blockchain_nodes = 1 + self.num_nodes = 4 + for i in range(self.num_nodes): + self.zgs_node_configs[i] = {"find_peer_timeout_secs": 1, "confirmation_block_count": 1} + + def run_test(self): + max_size = 256 * 1024 * 64 + small_max_size = 256 * 64 + small_ratio = 0.9 + no_data_ratio = 0.2 + crash_ratio = 0.2 + clean_ratio = 0.3 + + file_list = [] + # The number of files is set to a larger number for local tests. + tx_count = 20 + for i in range(tx_count): + chosen_node = random.randint(0, self.num_nodes - 1) + if random.random() <= small_ratio: + size = random.randint(0, small_max_size) + else: + size = random.randint(0, max_size) + no_data = random.random() <= no_data_ratio + self.log.info(f"choose {chosen_node}, seq={i}, size={size}, no_data={no_data}") + + client = self.nodes[chosen_node] + chunk_data = random.randbytes(size) + submissions, data_root = create_submission(chunk_data) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == i + 1) + wait_until(lambda: client.zgs_get_file_info(data_root) is not None, timeout=120) + if not no_data: + submit_data(client, chunk_data) + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + # Wait until the tx is sent out. + for node_index in range(len(self.nodes)): + if node_index != chosen_node: + self.log.debug(f"check {node_index}") + wait_until(lambda: self.nodes[node_index].zgs_get_file_info(data_root) is not None, timeout=120) + wait_until(lambda: self.nodes[node_index].zgs_get_file_info(data_root)["finalized"], timeout=300) + # TODO(zz): This is a temp solution to trigger auto sync after all nodes started. + if i >= tx_count - 2: + continue + file_list.append((data_root, no_data)) + + if random.random() <= crash_ratio: + # TODO(zz): node 0 is the boot node. + # If it's crashed and cleaned, it will not be connected by others now. + chosen_crash = random.randint(1, self.num_nodes - 1) + clean = random.random() <= clean_ratio + self.log.info(f"crash {chosen_crash}, clean={clean}") + self.stop_storage_node(chosen_crash, clean) + self.start_storage_node(chosen_crash) + self.nodes[chosen_crash].wait_for_rpc_connection() + + for i in range(tx_count): + for node in self.nodes: + status = node.zgs_get_file_info_by_tx_seq(i) + self.log.info(f"check tx seq {i}, {node.index}, status={status}") + for data_root, no_data in file_list: + if not no_data: + for node in self.nodes: + self.log.debug(f"check {data_root}, {node.index}") + wait_until(lambda: node.zgs_get_file_info(data_root)["finalized"], timeout=300) + + +if __name__ == "__main__": + RandomTest(blockchain_node_type=BlockChainNodeType.Conflux).main() diff --git a/tests/recovery_test.py b/tests/recovery_test.py new file mode 100755 index 0000000..fe23c44 --- /dev/null +++ b/tests/recovery_test.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 + +from test_framework.test_framework import TestFramework +from utility.submission import create_submission, submit_data +from utility.utils import wait_until + + +class RecoveryTest(TestFramework): + def run_test(self): + client = self.nodes[0] + + chunk_data = b"\x01" * 256 * 5 + submissions, data_root = create_submission(chunk_data) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == 1) + wait_until(lambda: client.zgs_get_file_info(data_root) is not None) + + segment = submit_data(client, chunk_data) + self.log.info("segment: %s", segment) + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + + self.stop_storage_node(0) + chunk_data = b"\x02" * 256 * 7 + submissions, data_root = create_submission(chunk_data) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == 2) + self.start_storage_node(0) + self.nodes[0].wait_for_rpc_connection() + wait_until(lambda: client.zgs_get_file_info(data_root) is not None) + segment = submit_data(client, chunk_data) + self.log.info("segment: %s", segment) + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + + self.stop_storage_node(0) + self.start_storage_node(0) + self.nodes[0].wait_for_rpc_connection() + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + + # Test with larger data. + chunk_data = b"\x03" * 256 * 1024 * 19 + submissions, data_root = create_submission(chunk_data) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == 3) + wait_until(lambda: client.zgs_get_file_info(data_root) is not None) + self.stop_storage_node(0) + self.start_storage_node(0) + self.nodes[0].wait_for_rpc_connection() + submit_data(client, chunk_data) + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + self.stop_storage_node(0) + self.start_storage_node(0) + self.nodes[0].wait_for_rpc_connection() + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + + +if __name__ == "__main__": + RecoveryTest().main() diff --git a/tests/revert_test.py b/tests/revert_test.py new file mode 100755 index 0000000..f6fad01 --- /dev/null +++ b/tests/revert_test.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +from test_framework.test_framework import TestFramework +from test_framework.blockchain_node import BlockChainNodeType +from test_framework.conflux_node import connect_nodes, disconnect_nodes, sync_blocks +from config.node_config import CONFLUX_CONFIG, TX_PARAMS1 +from utility.submission import create_submission, submit_data +from utility.utils import wait_until + + +class RevertTest(TestFramework): + def setup_params(self): + self.num_blockchain_nodes = 2 + self.num_nodes = 1 + + del CONFLUX_CONFIG["dev_block_interval_ms"] + + def run_test(self): + blockchain_client1 = self.blockchain_nodes[0] + blockchain_client2 = self.blockchain_nodes[1] + + self.log.info("Node 1 epoch {}".format(blockchain_client1.cfx_epochNumber())) + self.log.info("Node 2 epoch {}".format(blockchain_client2.cfx_epochNumber())) + + disconnect_nodes(self.blockchain_nodes, 0, 1) + blockchain_client1.generate_empty_blocks(5) + + self.log.info("Node 1 epoch {}".format(blockchain_client1.cfx_epochNumber())) + self.log.info("Node 2 epoch {}".format(blockchain_client2.cfx_epochNumber())) + + client = self.nodes[0] + chunk_data = b"\x00" * 256 + submissions, data_root = create_submission(chunk_data) + + tx_hash = self.contract.submit(submissions, 0) + self.log.info("tx 1 hash: {}".format(tx_hash.hex())) + wait_until(lambda: self.contract.num_submissions() == 1) + assert client.zgs_get_file_info(data_root) is None + # Generate blocks for confirmation + blockchain_client1.generate_empty_blocks(12) + wait_until(lambda: client.zgs_get_file_info(data_root) is not None) + + segment = submit_data(client, chunk_data) + self.log.info("segment: %s", segment) + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + + self.log.info("Node 1 epoch {}".format(blockchain_client1.cfx_epochNumber())) + self.log.info("Node 2 epoch {}".format(blockchain_client2.cfx_epochNumber())) + + self.log.info("===== submit tx to second node =====") + chunk_data = b"\x10" * 256 + submissions, data_root1 = create_submission(chunk_data) + tx_hash = self.contract.submit(submissions, 1, tx_prarams=TX_PARAMS1) + self.log.info("tx 2 hash: {}".format(tx_hash.hex())) + wait_until(lambda: self.contract.num_submissions(1) == 1) + + blockchain_client2.generate_empty_blocks(30) + + self.log.info("Node 1 epoch {}".format(blockchain_client1.cfx_epochNumber())) + self.log.info("Node 2 epoch {}".format(blockchain_client2.cfx_epochNumber())) + + connect_nodes(self.blockchain_nodes, 0, 1) + sync_blocks(self.blockchain_nodes[0:2]) + + self.log.info("Node 1 epoch {}".format(blockchain_client1.cfx_epochNumber())) + self.log.info("Node 2 epoch {}".format(blockchain_client2.cfx_epochNumber())) + + wait_until(lambda: client.zgs_get_file_info(data_root) is None) + wait_until(lambda: client.zgs_get_file_info(data_root1) is not None) + wait_until(lambda: not client.zgs_get_file_info(data_root1)["finalized"]) + + segment = submit_data(client, chunk_data) + self.log.info("segment: %s", segment) + wait_until(lambda: client.zgs_get_file_info(data_root1)["finalized"]) + + +if __name__ == "__main__": + RevertTest(blockchain_node_type=BlockChainNodeType.Conflux).main() diff --git a/tests/rpc_test.py b/tests/rpc_test.py new file mode 100755 index 0000000..f6edc6b --- /dev/null +++ b/tests/rpc_test.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 + +import tempfile + +from config.node_config import GENESIS_ACCOUNT +from test_framework.test_framework import TestFramework +from utility.submission import create_submission, submit_data +from utility.utils import ( + assert_equal, + wait_until, +) + + +class RpcTest(TestFramework): + def setup_params(self): + self.num_blockchain_nodes = 2 + self.num_nodes = 2 + + def run_test(self): + client1 = self.nodes[0] + client2 = self.nodes[1] + + chunk_data = b"\x00" * 256 + submissions, data_root = create_submission(chunk_data) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == 1) + wait_until(lambda: self.contract.num_submissions(1) == 1) + assert_equal(self.contract.num_submissions(), self.contract.num_submissions(1)) + + wait_until(lambda: client1.zgs_get_file_info(data_root) is not None) + assert_equal(client1.zgs_get_file_info(data_root)["finalized"], False) + + wait_until(lambda: client2.zgs_get_file_info(data_root) is not None) + assert_equal(client2.zgs_get_file_info(data_root)["finalized"], False) + + segment = submit_data(client1, chunk_data) + self.log.info("segment: %s", segment) + + wait_until(lambda: client1.zgs_get_file_info(data_root)["finalized"]) + assert_equal( + client1.zgs_download_segment(data_root, 0, 1), segment[0]["data"] + ) + + client2.admin_start_sync_file(0) + wait_until(lambda: client2.sycn_status_is_completed_or_unknown(0)) + + wait_until(lambda: client2.zgs_get_file_info(data_root)["finalized"]) + assert_equal( + client2.zgs_download_segment(data_root, 0, 1), segment[0]["data"] + ) + + self.__test_upload_file_with_cli(client1) + + client2.shutdown() + wait_until(lambda: client1.zgs_get_status() == 0) + + def __test_upload_file_with_cli(self, client1): + # Test with uploading files with cli + n_files = 1 + for size in [2, 1023, 2051]: + self.log.debug("file size: %d", size) + file_to_upload = tempfile.NamedTemporaryFile( + dir=self.root_dir, delete=False + ) + file_to_upload.write(b"\x10" * 256 * size) + file_to_upload.close() + + root = self._upload_file_use_cli( + self.blockchain_nodes[0].rpc_url, + self.contract.address(), + GENESIS_ACCOUNT.key, + self.nodes[0].rpc_url, + file_to_upload, + ) + + n_files += 1 + wait_until(lambda: self.contract.num_submissions() == n_files) + + wait_until(lambda: client1.zgs_get_file_info(root) is not None) + wait_until(lambda: client1.zgs_get_file_info(root)["finalized"]) + + for i in range(1, self.num_nodes): + self.log.info("wait node %d", i) + self.nodes[i].admin_start_sync_file(n_files - 1) + wait_until( + lambda: self.nodes[i].sycn_status_is_completed_or_unknown( + n_files - 1 + ) + ) + + wait_until(lambda: self.nodes[i].zgs_get_file_info(root) is not None) + wait_until( + lambda: self.nodes[i].zgs_get_file_info(root)["finalized"] + ) + + assert_equal( + client1.zgs_download_segment(root, 0, 2), + self.nodes[i].zgs_download_segment(root, 0, 2), + ) + + +if __name__ == "__main__": + RpcTest().main() diff --git a/tests/same_root_test.py b/tests/same_root_test.py new file mode 100755 index 0000000..f7c293a --- /dev/null +++ b/tests/same_root_test.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 + +import base64 +import random +from test_framework.test_framework import TestFramework +from utility.submission import ENTRY_SIZE, submit_data +from utility.submission import create_submission +from utility.utils import ( + assert_equal, + wait_until, +) + + +class SubmissionTest(TestFramework): + def setup_params(self): + self.num_blockchain_nodes = 1 + self.num_nodes = 1 + + def run_test(self): + data_size = [ + 2, + 256, + 256 * 1023 + 1, + 256 * 1024 * 256, + ] + same_root_tx_count = 2 + + next_tx_seq = 0 + for i, v in enumerate(data_size): + chunk_data = random.randbytes(v) + # Send tx before uploading data + for _ in range(same_root_tx_count): + self.submit_tx_for_data(chunk_data, next_tx_seq) + next_tx_seq += 1 + + # Send tx and upload data. + self.submit_tx_for_data(chunk_data, next_tx_seq) + self.submit_data(chunk_data) + next_tx_seq += 1 + # Check if all transactions are finalized + for tx_offset in range(same_root_tx_count + 1): + tx_seq = next_tx_seq - 1 - tx_offset + status = self.nodes[0].zgs_get_file_info_by_tx_seq(tx_seq) + assert status["finalized"] + + # Send tx after uploading data + for _ in range(same_root_tx_count): + self.submit_tx_for_data(chunk_data, next_tx_seq, data_finalized=True) + next_tx_seq += 1 + + def submit_tx_for_data(self, chunk_data, tx_seq, data_finalized=False, node_idx=0): + submissions, data_root = create_submission(chunk_data) + self.log.info("data root: %s, submissions: %s", data_root, submissions) + self.contract.submit(submissions) + + wait_until(lambda: self.contract.num_submissions() == tx_seq + 1) + + client = self.nodes[node_idx] + wait_until(lambda: client.zgs_get_file_info_by_tx_seq(tx_seq) is not None) + assert_equal(client.zgs_get_file_info_by_tx_seq(tx_seq)["finalized"], data_finalized) + + def submit_data(self, chunk_data, node_idx=0): + _, data_root = create_submission(chunk_data) + client = self.nodes[node_idx] + segments = submit_data(client, chunk_data) + self.log.debug( + "segments: %s", [(s["root"], s["index"], s["proof"]) for s in segments] + ) + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + + +if __name__ == "__main__": + SubmissionTest().main() diff --git a/tests/submission_test.py b/tests/submission_test.py new file mode 100755 index 0000000..376f13f --- /dev/null +++ b/tests/submission_test.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 + +import base64 +import random +from test_framework.test_framework import TestFramework +from utility.submission import ENTRY_SIZE, submit_data +from utility.submission import create_submission +from utility.utils import ( + assert_equal, + wait_until, +) + + +class SubmissionTest(TestFramework): + def setup_params(self): + self.num_blockchain_nodes = 2 + self.num_nodes = 3 + + def run_test(self): + data_size = [ + 2, + 255, + 256, + 257, + 1023, + 1024, + 1025, + 256 * 1023, + 256 * 1023 + 1, + 256 * 1024, + 256 * 1024 + 1, + 256 * 1025, + 256 * 2048 - 1, + 256 * 2048, + 256 * 16385, + 256 * 1024 * 256, + ] + + for i, v in enumerate(data_size): + self.submission_data(v, i + 1, False if v >= 256 * 1024 * 256 else True) + + def submission_data(self, size, submission_index, rand_data=True): + node_idx = random.randint(0, self.num_nodes - 1) + self.log.info("node index: %d, file size: %d", node_idx, size) + chunk_data = random.randbytes(size) if rand_data else b"\x10" * size + + submissions, data_root = create_submission(chunk_data) + self.log.info("data root: %s, submissions: %s", data_root, submissions) + self.contract.submit(submissions) + + wait_until(lambda: self.contract.num_submissions() == submission_index) + + client = self.nodes[node_idx] + wait_until(lambda: client.zgs_get_file_info(data_root) is not None) + assert_equal(client.zgs_get_file_info(data_root)["finalized"], False) + + segments = submit_data(client, chunk_data) + self.log.info( + "segments: %s", [(s["root"], s["index"], s["proof"]) for s in segments] + ) + + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + + first_entry = base64.b64decode(segments[0]["data"].encode("utf-8"))[ + 0:ENTRY_SIZE + ] + + assert_equal( + base64.b64decode( + client.zgs_download_segment(data_root, 0, 1).encode("utf-8") + ), + first_entry, + ) + + for i in range(0, self.num_nodes): + if node_idx == i: + continue + + self.nodes[i].admin_start_sync_file(submission_index - 1) + wait_until( + lambda: self.nodes[i].sycn_status_is_completed_or_unknown( + submission_index - 1 + ) + ) + + wait_until( + lambda: self.nodes[i].zgs_get_file_info(data_root) is not None + ) + wait_until( + lambda: self.nodes[i].zgs_get_file_info(data_root)["finalized"] + ) + + assert_equal( + base64.b64decode( + self.nodes[i] + .zgs_download_segment(data_root, 0, 1) + .encode("utf-8") + ), + first_entry, + ) + + +if __name__ == "__main__": + SubmissionTest().main() diff --git a/tests/sync_test.py b/tests/sync_test.py new file mode 100755 index 0000000..781927b --- /dev/null +++ b/tests/sync_test.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 + +import random + +from test_framework.test_framework import TestFramework +from utility.submission import create_submission +from utility.submission import submit_data +from utility.utils import ( + assert_equal, + wait_until, +) + + +class SyncTest(TestFramework): + def setup_params(self): + self.num_blockchain_nodes = 2 + self.num_nodes = 2 + + def run_test(self): + client1 = self.nodes[0] + client2 = self.nodes[1] + + self.stop_storage_node(1) + + size = 256 * 1024 + chunk_data = random.randbytes(size) + + submissions, data_root = create_submission(chunk_data) + self.log.info("data root: %s, submissions: %s", data_root, submissions) + self.contract.submit(submissions) + + wait_until(lambda: self.contract.num_submissions() == 1) + + wait_until(lambda: client1.zgs_get_file_info(data_root) is not None) + assert_equal(client1.zgs_get_file_info(data_root)["finalized"], False) + + segments = submit_data(client1, chunk_data) + self.log.info( + "segments: %s", [(s["root"], s["index"], s["proof"]) for s in segments] + ) + + wait_until(lambda: client1.zgs_get_file_info(data_root)["finalized"]) + + self.start_storage_node(1) + self.nodes[1].wait_for_rpc_connection() + + client2.admin_start_sync_file(0) + wait_until(lambda: client2.sycn_status_is_completed_or_unknown(0)) + + wait_until(lambda: client2.zgs_get_file_info(data_root)["finalized"]) + assert_equal( + client2.zgs_download_segment(data_root, 0, 1), + client1.zgs_download_segment(data_root, 0, 1), + ) + + +if __name__ == "__main__": + SyncTest().main() diff --git a/tests/test_all.py b/tests/test_all.py new file mode 100755 index 0000000..2507a82 --- /dev/null +++ b/tests/test_all.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +import argparse +import os +import shutil +import stat +import subprocess +import sys + +from concurrent.futures import ProcessPoolExecutor + +from utility.utils import is_windows_platform + +PORT_MIN = 11000 +PORT_MAX = 65535 +PORT_RANGE = 500 + +__file_path__ = os.path.dirname(os.path.realpath(__file__)) + + +def run_single_test(py, script, test_dir, index, port_min, port_max): + try: + # Make sure python thinks it can write unicode to its stdout + "\u2713".encode("utf_8").decode(sys.stdout.encoding) + TICK = "✓ " + CROSS = "✖ " + CIRCLE = "○ " + except UnicodeDecodeError: + TICK = "P " + CROSS = "x " + CIRCLE = "o " + + BOLD, BLUE, RED, GREY = ("", ""), ("", ""), ("", ""), ("", "") + if os.name == "posix": + # primitive formatting on supported + # terminal via ANSI escape sequences: + BOLD = ("\033[0m", "\033[1m") + BLUE = ("\033[0m", "\033[0;34m") + RED = ("\033[0m", "\033[0;31m") + GREY = ("\033[0m", "\033[1;30m") + print("Running " + script) + port_min = port_min + (index * PORT_RANGE) % (port_max - port_min) + color = BLUE + glyph = TICK + try: + subprocess.check_output( + args=[py, script, "--randomseed=1", f"--port-min={port_min}"], + stdin=None, + cwd=test_dir, + ) + except subprocess.CalledProcessError as err: + color = RED + glyph = CROSS + print(color[1] + glyph + " Testcase " + script + color[0]) + print("Output of " + script + "\n" + err.output.decode("utf-8")) + raise err + print(color[1] + glyph + " Testcase " + script + color[0]) + + +def run(): + dir_name = os.path.join(__file_path__, "tmp") + if not os.path.exists(dir_name): + os.makedirs(dir_name, exist_ok=True) + + conflux_path = os.path.join(dir_name, "conflux") + if not os.path.exists(conflux_path): + build_conflux(conflux_path) + + parser = argparse.ArgumentParser(usage="%(prog)s [options]") + parser.add_argument( + "--max-workers", + dest="max_workers", + default=5, + type=int, + ) + parser.add_argument( + "--port-max", + dest="port_max", + default=PORT_MAX, + type=int, + ) + parser.add_argument( + "--port-min", + dest="port_min", + default=PORT_MIN, + type=int, + ) + + options = parser.parse_args() + + TEST_SCRIPTS = [] + + test_dir = os.path.dirname(os.path.realpath(__file__)) + test_subdirs = [ + "", # include test_dir itself + ] + + slow_tests = {} + long_manual_tests = {"fuzz_test.py"} + + for subdir in test_subdirs: + subdir_path = os.path.join(test_dir, subdir) + for file in os.listdir(subdir_path): + if file.endswith("_test.py"): + rel_path = os.path.join(subdir, file) + if rel_path not in slow_tests and rel_path not in long_manual_tests: + TEST_SCRIPTS.append(rel_path) + + executor = ProcessPoolExecutor(max_workers=options.max_workers) + test_results = [] + + py = "python3" + if hasattr(sys, "getwindowsversion"): + py = "python" + + i = 0 + # Start slow tests first to avoid waiting for long-tail jobs + for script in slow_tests: + f = executor.submit( + run_single_test, py, script, test_dir, i, options.port_min, options.port_max + ) + test_results.append((script, f)) + i += 1 + for script in TEST_SCRIPTS: + f = executor.submit( + run_single_test, py, script, test_dir, i, options.port_min, options.port_max + ) + test_results.append((script, f)) + i += 1 + + failed = set() + for script, f in test_results: + try: + f.result() + except subprocess.CalledProcessError as err: + print("CalledProcessError " + repr(err)) + failed.add(script) + + if len(failed) > 0: + print("The following test fails: ") + for c in failed: + print(c) + sys.exit(1) + + +def build_conflux(conflux_path): + destination_path = os.path.join(__file_path__, "tmp", "conflux_tmp") + if os.path.exists(destination_path): + shutil.rmtree(destination_path) + + clone_command = "git clone https://github.com/Conflux-Chain/conflux-rust.git" + clone_with_path = clone_command + " " + destination_path + os.system(clone_with_path) + + origin_path = os.getcwd() + os.chdir(destination_path) + os.system("cargo build --release --bin conflux") + + path = os.path.join(destination_path, "target", "release", "conflux") + shutil.copyfile(path, conflux_path) + + if not is_windows_platform(): + st = os.stat(conflux_path) + os.chmod(conflux_path, st.st_mode | stat.S_IEXEC) + + os.chdir(origin_path) + + +if __name__ == "__main__": + run() diff --git a/tests/test_framework/blockchain_node.py b/tests/test_framework/blockchain_node.py new file mode 100644 index 0000000..bcbb5ca --- /dev/null +++ b/tests/test_framework/blockchain_node.py @@ -0,0 +1,326 @@ +import json +import os +import subprocess +import tempfile +import time + +from eth_utils import decode_hex +from web3 import Web3, HTTPProvider +from web3.middleware import construct_sign_and_send_raw_middleware +from enum import Enum, unique +from config.node_config import ( + GENESIS_PRIV_KEY, + GENESIS_PRIV_KEY1, + TX_PARAMS, + MINER_ID, + NO_MERKLE_PROOF_FLAG, + NO_SEAL_FLAG, + TX_PARAMS1, +) +from utility.simple_rpc_proxy import SimpleRpcProxy +from utility.utils import ( + initialize_config, + wait_until, +) + + +@unique +class BlockChainNodeType(Enum): + Conflux = 0 + BSC = 1 + + +@unique +class NodeType(Enum): + BlockChain = 0 + Zgs = 1 + + +class FailedToStartError(Exception): + """Raised when a node fails to start correctly.""" + + +class TestNode: + def __init__( + self, node_type, index, data_dir, rpc_url, binary, config, log, rpc_timeout=10 + ): + self.node_type = node_type + self.index = index + self.data_dir = data_dir + self.rpc_url = rpc_url + self.config = config + self.rpc_timeout = rpc_timeout + self.process = None + self.stdout = None + self.stderr = None + self.config_file = os.path.join(self.data_dir, "config.toml") + self.args = [binary, "--config", self.config_file] + self.running = False + self.rpc_connected = False + self.rpc = None + self.log = log + + def __del__(self): + if self.process: + self.process.terminate() + + def __getattr__(self, name): + """Dispatches any unrecognised messages to the RPC connection.""" + assert self.rpc_connected and self.rpc is not None, self._node_msg( + "Error: no RPC connection" + ) + return getattr(self.rpc, name) + + def _node_msg(self, msg: str) -> str: + """Return a modified msg that identifies this node by its index as a debugging aid.""" + return "[node %s %d] %s" % (self.node_type, self.index, msg) + + def _raise_assertion_error(self, msg: str): + """Raise an AssertionError with msg modified to identify this node.""" + raise AssertionError(self._node_msg(msg)) + + def setup_config(self): + os.mkdir(self.data_dir) + initialize_config(self.config_file, self.config) + + def start(self, redirect_stderr=False): + my_env = os.environ.copy() + if self.stdout is None: + self.stdout = tempfile.NamedTemporaryFile( + dir=self.data_dir, prefix="stdout", delete=False + ) + if self.stderr is None: + self.stderr = tempfile.NamedTemporaryFile( + dir=self.data_dir, prefix="stderr", delete=False + ) + + if redirect_stderr: + self.process = subprocess.Popen( + self.args, + stdout=self.stdout, + stderr=self.stdout, + cwd=self.data_dir, + env=my_env, + ) + else: + self.process = subprocess.Popen( + self.args, + stdout=self.stdout, + stderr=self.stderr, + cwd=self.data_dir, + env=my_env, + ) + self.running = True + + def wait_for_rpc_connection(self): + raise NotImplementedError + + def _wait_for_rpc_connection(self, check): + """Sets up an RPC connection to the node process. Returns False if unable to connect.""" + # Poll at a rate of four times per second + poll_per_s = 4 + for _ in range(poll_per_s * self.rpc_timeout): + if self.process.poll() is not None: + raise FailedToStartError( + self._node_msg( + "exited with status {} during initialization".format( + self.process.returncode + ) + ) + ) + rpc = SimpleRpcProxy(self.rpc_url, timeout=self.rpc_timeout) + if check(rpc): + self.rpc_connected = True + self.rpc = rpc + return + time.sleep(1.0 / poll_per_s) + self._raise_assertion_error( + "failed to get RPC proxy: index = {}, rpc_url = {}".format( + self.index, self.rpc_url + ) + ) + + def stop(self, expected_stderr="", kill=False, wait=True): + """Stop the node.""" + if not self.running: + return + if kill: + self.process.kill() + else: + self.process.terminate() + if wait: + self.wait_until_stopped() + # Check that stderr is as expected + self.stderr.seek(0) + stderr = self.stderr.read().decode("utf-8").strip() + # TODO: Check how to avoid `pthread lock: Invalid argument`. + if stderr != expected_stderr and stderr != "pthread lock: Invalid argument": + # print process status for debug + if self.return_code is None: + self.log.info("Process is still running") + else: + self.log.info( + "Process has terminated with code {}".format(self.return_code) + ) + + raise AssertionError( + "Unexpected stderr {} != {} from node={}{}".format( + stderr, expected_stderr, self.node_type, self.index + ) + ) + + self.stdout.close() + self.stderr.close() + self.stdout = None + self.stderr = None + + def is_node_stopped(self): + """Checks whether the node has stopped. + + Returns True if the node has stopped. False otherwise. + This method is responsible for freeing resources (self.process).""" + if not self.running: + return True + return_code = self.process.poll() + if return_code is None: + return False + + # process has stopped. Assert that it didn't return an error code. + # assert return_code == 0, self._node_msg( + # "Node returned non-zero exit code (%d) when stopping" % return_code + # ) + self.running = False + self.process = None + self.rpc = None + self.log.debug("Node stopped") + self.return_code = return_code + return True + + def wait_until_stopped(self, timeout=20): + wait_until(self.is_node_stopped, timeout=timeout) + + +class BlockchainNode(TestNode): + def __init__( + self, + index, + data_dir, + rpc_url, + binary, + local_conf, + contract_path, + token_contract_path, + mine_contract_path, + log, + blockchain_node_type, + rpc_timeout=10, + ): + self.contract_path = contract_path + self.token_contract_path = token_contract_path + self.mine_contract_path = mine_contract_path + + self.blockchain_node_type = blockchain_node_type + + super().__init__( + NodeType.BlockChain, + index, + data_dir, + rpc_url, + binary, + local_conf, + log, + rpc_timeout, + ) + + def wait_for_rpc_connection(self): + self._wait_for_rpc_connection(lambda rpc: rpc.eth_syncing() is False) + + def wait_for_start_mining(self): + self._wait_for_rpc_connection(lambda rpc: int(rpc.eth_blockNumber(), 16) > 0) + + def wait_for_transaction_receipt(self, w3, tx_hash, timeout=120, parent_hash=None): + return w3.eth.wait_for_transaction_receipt(tx_hash, timeout) + + def setup_contract(self): + w3 = Web3(HTTPProvider(self.rpc_url)) + + account1 = w3.eth.account.from_key(GENESIS_PRIV_KEY) + account2 = w3.eth.account.from_key(GENESIS_PRIV_KEY1) + w3.middleware_onion.add( + construct_sign_and_send_raw_middleware([account1, account2]) + ) + # account = w3.eth.account.from_key(GENESIS_PRIV_KEY1) + # w3.middleware_onion.add(construct_sign_and_send_raw_middleware(account)) + + def deploy_contract(path, args=None): + if args is None: + args = [] + contract_interface = json.load(open(path, "r")) + contract = w3.eth.contract( + abi=contract_interface["abi"], + bytecode=contract_interface["bytecode"], + ) + tx_params = TX_PARAMS.copy() + del tx_params["gas"] + tx_hash = contract.constructor(*args).transact(tx_params) + tx_receipt = self.wait_for_transaction_receipt(w3, tx_hash) + contract = w3.eth.contract( + address=tx_receipt.contractAddress, + abi=contract_interface["abi"], + ) + return contract, tx_hash + + self.log.debug("Start deploy contracts") + token_contract, _ = deploy_contract(self.token_contract_path) + self.log.debug("ERC20 deployed") + flow_contract, flow_contract_hash = deploy_contract( + self.contract_path, [token_contract.address] + ) + self.log.debug("Flow deployed") + mine_contract, _ = deploy_contract( + self.mine_contract_path, + [flow_contract.address, 0], + ) + self.log.debug("Mine deployed") + self.log.info("All contracts deployed") + + tx_hash = token_contract.functions.approve( + flow_contract.address, int(1e9) + ).transact(TX_PARAMS) + self.wait_for_transaction_receipt(w3, tx_hash) + + # setup second account + amount = int(1e8) + tx_hash = token_contract.functions.transfer(account2.address, amount).transact( + TX_PARAMS + ) + self.wait_for_transaction_receipt(w3, tx_hash) + + tx_hash = token_contract.functions.approve( + flow_contract.address, amount + ).transact(TX_PARAMS1) + self.wait_for_transaction_receipt(w3, tx_hash) + + tx_hash = mine_contract.functions.setMiner(decode_hex(MINER_ID)).transact(TX_PARAMS) + self.wait_for_transaction_receipt(w3, tx_hash) + + return flow_contract, flow_contract_hash, mine_contract + + def get_contract(self, contract_address): + w3 = Web3(HTTPProvider(self.rpc_url)) + + account1 = w3.eth.account.from_key(GENESIS_PRIV_KEY) + account2 = w3.eth.account.from_key(GENESIS_PRIV_KEY1) + w3.middleware_onion.add( + construct_sign_and_send_raw_middleware([account1, account2]) + ) + + contract_interface = json.load(open(self.contract_path, "r")) + return w3.eth.contract(address=contract_address, abi=contract_interface["abi"]) + + def wait_for_transaction(self, tx_hash): + w3 = Web3(HTTPProvider(self.rpc_url)) + w3.eth.wait_for_transaction_receipt(tx_hash) + + def start(self): + super().start(self.blockchain_node_type == BlockChainNodeType.BSC) diff --git a/tests/test_framework/bsc_node.py b/tests/test_framework/bsc_node.py new file mode 100644 index 0000000..886ce80 --- /dev/null +++ b/tests/test_framework/bsc_node.py @@ -0,0 +1,164 @@ +import os +import platform +import requests +import shutil +import stat + +from config.node_config import BSC_CONFIG +from eth_utils import encode_hex +from utility.signature_utils import ec_random_keys, priv_to_addr +from test_framework.blockchain_node import BlockChainNodeType, BlockchainNode +from utility.utils import ( + blockchain_p2p_port, + blockchain_rpc_port, + is_windows_platform, + wait_until, +) + +__file_path__ = os.path.dirname(os.path.realpath(__file__)) + + +class BSCNode(BlockchainNode): + def __init__( + self, + index, + root_dir, + binary, + updated_config, + contract_path, + token_contract_path, + mine_contract_path, + log, + rpc_timeout=10, + ): + local_conf = BSC_CONFIG.copy() + indexed_config = { + "HTTPPort": blockchain_rpc_port(index), + "Port": blockchain_p2p_port(index), + } + # Set configs for this specific node. + local_conf.update(indexed_config) + # Overwrite with personalized configs. + local_conf.update(updated_config) + data_dir = os.path.join(root_dir, "blockchain_node" + str(index)) + rpc_url = "http://" + local_conf["HTTPHost"] + ":" + str(local_conf["HTTPPort"]) + + self.genesis_config = os.path.join( + __file_path__, "..", "config", "genesis.json" + ) + self.binary = binary + + if not os.path.exists(self.binary): + log.info("binary does not exist") + dir_name = os.path.dirname(self.binary) + if not os.path.exists(dir_name): + os.makedirs(dir_name, exist_ok=True) + + try: + with open(f"{self.binary}", "xb") as f: + self.__try_download_node(f, log) + except FileExistsError: + log.info("Binary is alrady under downloading") + + wait_until(lambda: os.access(f"{self.binary}", os.X_OK), timeout=120) + + self.node_id = encode_hex(priv_to_addr(ec_random_keys()[0])) + + super().__init__( + index, + data_dir, + rpc_url, + binary, + local_conf, + contract_path, + token_contract_path, + mine_contract_path, + log, + BlockChainNodeType.BSC, + rpc_timeout, + ) + + def __try_download_node(self, f, log): + url = "https://api.github.com/repos/{}/{}/releases/latest".format( + "bnb-chain", "bsc" + ) + req = requests.get(url) + if req.ok: + asset_name = self.__get_asset_name() + + url = "" + for asset in req.json()["assets"]: + if asset["name"].lower() == asset_name: + url = asset["browser_download_url"] + break + + if url: + log.info("Try to download geth from %s", url) + f.write(requests.get(url).content) + f.close() + + if not is_windows_platform(): + st = os.stat(self.binary) + os.chmod(self.binary, st.st_mode | stat.S_IEXEC) + else: + log.info("Request failed with %s", req) + + def __get_asset_name(self): + sys = platform.system().lower() + if sys == "linux": + return "geth_linux" + elif sys == "windows": + return "geth_windows.exe" + elif sys == "darwin": + return "geth_mac" + else: + raise RuntimeError("Unable to recognize platform") + + def start(self): + self.args = [ + self.binary, + "--datadir", + self.data_dir, + "init", + self.genesis_config, + ] + super().start() + + wait_until(lambda: self.process.poll() is not None) + ret = self.process.poll() + assert ret == 0, "BSC init should be successful" + + self.log.info("BSC node%d init finished with return code %d", self.index, ret) + + config_file = os.path.join(__file_path__, "..", "config", "bsc.toml") + target = os.path.join(self.data_dir, "bsc.toml") + shutil.copyfile(config_file, target) + + self.args = [ + self.binary, + "--datadir", + self.data_dir, + "--port", + str(self.config["Port"]), + "--http", + "--http.api", + "personal,eth,net,web3,admin,txpool,miner", + "--http.port", + str(self.config["HTTPPort"]), + # "--syncmode", + # "full", + # "--mine", + "--miner.threads", + "1", + "--miner.etherbase", + self.node_id, + "--networkid", + str(self.config["NetworkId"]), + "--verbosity", + str(self.config["Verbosity"]), + "--config", + "bsc.toml", + ] + + self.log.info("Start BSC node %d", self.index) + super().start() diff --git a/tests/test_framework/conflux_node.py b/tests/test_framework/conflux_node.py new file mode 100644 index 0000000..3be502a --- /dev/null +++ b/tests/test_framework/conflux_node.py @@ -0,0 +1,335 @@ +import os +import random +import threading +import time + +from config.node_config import BLOCK_SIZE_LIMIT, CONFLUX_CONFIG +import eth_utils +from test_framework.blockchain_node import BlockChainNodeType, BlockchainNode +from utility.signature_utils import ( + encode_int32, + get_nodeid, + sha3, +) +from utility.simple_rpc_proxy import SimpleRpcProxy +from utility.utils import ( + blockchain_p2p_port, + blockchain_rpc_port, + blockchain_rpc_port_core, + wait_until, +) +from web3.exceptions import TransactionNotFound + + +class ConfluxNode(BlockchainNode): + def __init__( + self, + index, + root_dir, + binary, + updated_config, + contract_path, + token_contract_path, + mine_contract_path, + log, + rpc_timeout=10, + ): + local_conf = CONFLUX_CONFIG.copy() + indexed_config = { + "jsonrpc_http_eth_port": blockchain_rpc_port(index), + "jsonrpc_local_http_port": blockchain_rpc_port_core(index), + "tcp_port": blockchain_p2p_port(index), + } + # Set configs for this specific node. + local_conf.update(indexed_config) + # Overwrite with personalized configs. + local_conf.update(updated_config) + data_dir = os.path.join(root_dir, "blockchain_node" + str(index)) + rpc_url = ( + "http://" + + local_conf["public_address"] + + ":" + + str(local_conf["jsonrpc_http_eth_port"]) + ) + self.ip = local_conf["public_address"] + self.port = str(local_conf["tcp_port"]) + + if "dev_block_interval_ms" in local_conf: + self.auto_mining = True + else: + self.auto_mining = False + + # setup core space rpc + core_space_rpc_url = ( + "http://" + + local_conf["public_address"] + + ":" + + str(local_conf["jsonrpc_local_http_port"]) + ) + + self.core_space_rpc = SimpleRpcProxy(core_space_rpc_url, timeout=rpc_timeout) + + super().__init__( + index, + data_dir, + rpc_url, + binary, + local_conf, + contract_path, + token_contract_path, + mine_contract_path, + log, + BlockChainNodeType.Conflux, + rpc_timeout, + ) + + def __getattr__(self, name): + """Dispatches any unrecognised messages to the RPC connection.""" + assert self.rpc_connected and self.rpc is not None, self._node_msg( + "Error: no RPC connection" + ) + if name.startswith("eth_") or name.startswith("parity_"): + return getattr(self.rpc, name) + else: + return getattr(self.core_space_rpc, name) + + def wait_for_transaction_receipt(self, w3, tx_hash, timeout=120, parent_hash=None): + if self.auto_mining: + return super().wait_for_transaction_receipt(w3, tx_hash, timeout) + else: + time_end = time.time() + timeout + while time.time() < time_end: + try: + tx_receipt = w3.eth.get_transaction_receipt(tx_hash) + except TransactionNotFound: + tx_receipt = None + if parent_hash: + parent_hash = self.generatefixedblock( + parent_hash, [], 1, False, None, None + ) + else: + self.generateoneblock(1, BLOCK_SIZE_LIMIT) + time.sleep(0.5) + + if tx_receipt is not None: + return tx_receipt + + raise TransactionNotFound + + def wait_for_nodeid(self): + pubkey, x, y = get_nodeid(self) + self.key = eth_utils.encode_hex(pubkey) + addr_tmp = bytearray(sha3(encode_int32(x) + encode_int32(y))[12:]) + addr_tmp[0] &= 0x0F + addr_tmp[0] |= 0x10 + self.addr = addr_tmp + self.log.debug("Get node {} nodeid {}".format(self.index, self.key)) + + def best_block_hash(self): + return self.core_space_rpc.cfx_getBestBlockHash() + + def cfx_epochNumber(self, epoch_number=None): + return self.core_space_rpc.cfx_epochNumber([epoch_number]) + + def getblockcount(self): + return self.core_space_rpc.getblockcount() + + def addnode(self, key, peer_addr): + return self.core_space_rpc.addnode([key, peer_addr]) + + def removenode(self, key, peer_addr): + return self.core_space_rpc.removenode([key, peer_addr]) + + def addlatency(self, node_id, latency_ms): + return self.core_space_rpc.addlatency([node_id, latency_ms]) + + def getnodeid(self, challenge): + return self.core_space_rpc.getnodeid([challenge]) + + def generate_empty_blocks(self, num_blocks): + return self.core_space_rpc.generate_empty_blocks([num_blocks]) + + def generateoneblock(self, num_txs, block_size_limit): + return self.core_space_rpc.generateoneblock([num_txs, block_size_limit]) + + def generatefixedblock( + self, parent_hash, referee, num_txs, adaptive, difficulty, pos_reference + ): + return self.core_space_rpc.generatefixedblock( + [parent_hash, referee, num_txs, adaptive, difficulty, pos_reference] + ) + + +def check_handshake(from_connection, target_node_id): + """ + Check whether node 'from_connection' has already + added node 'target_node_id' into its peer set. + """ + peers = from_connection.getpeerinfo() + for peer in peers: + if peer["nodeid"] == target_node_id and len(peer["protocols"]) > 0: + return True + return False + + +def get_peer_addr(connection): + return "{}:{}".format(connection.ip, connection.port) + + +def connect_nodes(nodes, a, node_num, timeout=60): + """ + Let node[a] connect to node[node_num] + """ + from_connection = nodes[a] + to_connection = nodes[node_num] + key = nodes[node_num].key + peer_addr = get_peer_addr(to_connection) + from_connection.addnode(key, peer_addr) + # poll until hello handshake complete to avoid race conditions + # with transaction relaying + wait_until( + lambda: check_handshake(from_connection, to_connection.key), timeout=timeout + ) + + +def sync_blocks(rpc_connections, *, sync_count=True, wait=1, timeout=60): + """ + Wait until everybody has the same tip. + + sync_blocks needs to be called with an rpc_connections set that has least + one node already synced to the latest, stable tip, otherwise there's a + chance it might return before all nodes are stably synced. + """ + stop_time = time.time() + timeout + while time.time() <= stop_time: + best_hash = [x.best_block_hash() for x in rpc_connections] + block_count = [x.getblockcount() for x in rpc_connections] + if best_hash.count(best_hash[0]) == len(rpc_connections) and ( + not sync_count or block_count.count(block_count[0]) == len(rpc_connections) + ): + return + time.sleep(wait) + raise AssertionError( + "Block sync timed out:{}".format( + "".join("\n {!r}".format(b) for b in best_hash + block_count) + ) + ) + + +def disconnect_nodes(nodes, from_connection, node_num): + try: + nodes[from_connection].removenode( + nodes[node_num].key, get_peer_addr(nodes[node_num]) + ) + nodes[node_num].removenode( + nodes[from_connection].key, get_peer_addr(nodes[from_connection]) + ) + except Exception as e: + # If this node is disconnected between calculating the peer id + # and issuing the disconnect, don't worry about it. + # This avoids a race condition if we're mass-disconnecting peers. + if e.error["code"] != -29: # RPC_CLIENT_NODE_NOT_CONNECTED + raise + + # wait to disconnect + wait_until( + lambda: [ + peer + for peer in nodes[from_connection].getpeerinfo() + if peer["nodeid"] == nodes[node_num].key + ] + == [], + timeout=5, + ) + wait_until( + lambda: [ + peer + for peer in nodes[node_num].getpeerinfo() + if peer["nodeid"] == nodes[from_connection].key + ] + == [], + timeout=5, + ) + + +def connect_sample_nodes( + nodes, log, sample=3, latency_min=0, latency_max=300, timeout=30 +): + """ + Establish connections among nodes with each node having 'sample' outgoing peers. + It first lets all the nodes link as a loop, then randomly pick 'sample-1' + outgoing peers for each node. + """ + peer = [[] for _ in nodes] + latencies = [{} for _ in nodes] + threads = [] + num_nodes = len(nodes) + sample = min(num_nodes - 1, sample) + + for i in range(num_nodes): + # make sure all nodes are reachable + next = (i + 1) % num_nodes + peer[i].append(next) + lat = random.randint(latency_min, latency_max) + latencies[i][next] = lat + latencies[next][i] = lat + + for _ in range(sample - 1): + while True: + p = random.randint(0, num_nodes - 1) + if p not in peer[i] and not p == i: + peer[i].append(p) + lat = random.randint(latency_min, latency_max) + latencies[i][p] = lat + latencies[p][i] = lat + break + + for i in range(num_nodes): + t = ConnectThread(nodes, i, peer[i], latencies, log, min_peers=sample) + t.start() + threads.append(t) + + for t in threads: + t.join(timeout) + assert ( + not t.is_alive() + ), "Node[{}] connect to other nodes timeout in {} seconds".format(t.a, timeout) + assert not t.failed, "connect_sample_nodes failed." + + +class ConnectThread(threading.Thread): + def __init__(self, nodes, a, peers, latencies, log, min_peers=3, daemon=True): + threading.Thread.__init__(self, daemon=daemon) + self.nodes = nodes + self.a = a + self.peers = peers + self.latencies = latencies + self.log = log + self.min_peers = min_peers + self.failed = False + + def run(self): + try: + while True: + for i in range(len(self.peers)): + p = self.peers[i] + connect_nodes(self.nodes, self.a, p) + for p in self.latencies[self.a]: + self.nodes[self.a].addlatency( + self.nodes[p].key, self.latencies[self.a][p] + ) + if len(self.nodes[self.a].getpeerinfo()) >= self.min_peers: + break + else: + time.sleep(1) + except Exception as e: + node = self.nodes[self.a] + self.log.error( + "Node " + + str(self.a) + + " fails to be connected to " + + str(self.peers) + + ", ip={}, index={}".format(node.ip, node.index) + ) + self.log.error(e) + self.failed = True diff --git a/tests/test_framework/contract_proxy.py b/tests/test_framework/contract_proxy.py new file mode 100644 index 0000000..52c350a --- /dev/null +++ b/tests/test_framework/contract_proxy.py @@ -0,0 +1,66 @@ +from gettext import npgettext +from config.node_config import TX_PARAMS +from utility.utils import assert_equal + + +class ContractProxy: + def __init__(self, contract, blockchain_nodes): + self.contract = contract + self.contract_address = contract.address + self.blockchain_nodes = blockchain_nodes + + def _get_contract(self, node_idx=0): + return ( + self.contract + if node_idx == 0 + else self.blockchain_nodes[node_idx].get_contract(self.contract_address) + ) + + def _call(self, fn_name, node_idx, **args): + assert node_idx < len(self.blockchain_nodes) + + contract = self._get_contract(node_idx) + return getattr(contract.functions, fn_name)(**args).call() + + def _send(self, fn_name, node_idx, **args): + assert node_idx < len(self.blockchain_nodes) + + contract = self._get_contract(node_idx) + return getattr(contract.functions, fn_name)(**args).transact(TX_PARAMS) + + def address(self): + return self.contract_address + + +class FlowContractProxy(ContractProxy): + def submit( + self, submission_nodes, node_idx=0, tx_prarams=TX_PARAMS, parent_hash=None + ): + assert node_idx < len(self.blockchain_nodes) + + contract = self._get_contract(node_idx) + tx_hash = contract.functions.submit(submission_nodes).transact(tx_prarams) + receipt = self.blockchain_nodes[node_idx].wait_for_transaction_receipt( + contract.w3, tx_hash, parent_hash=parent_hash + ) + if receipt["status"] != 1: + print(receipt) + assert_equal(receipt["status"], 1) + return tx_hash + + def num_submissions(self, node_idx=0): + return self._call("numSubmissions", node_idx) + + def first_block(self, node_idx=0): + return self._call("firstBlock", node_idx) + + def epoch(self, node_idx=0): + return self._call("epoch", node_idx) + + +class MineContractProxy(ContractProxy): + def last_mined_epoch(self, node_idx=0): + return self._call("lastMinedEpoch", node_idx) + + def set_quality(self, quality, node_idx=0): + return self._send("setQuality", node_idx, _targetQuality=quality) diff --git a/tests/test_framework/test_framework.py b/tests/test_framework/test_framework.py new file mode 100644 index 0000000..7348639 --- /dev/null +++ b/tests/test_framework/test_framework.py @@ -0,0 +1,461 @@ +import argparse +from enum import Enum +import logging +import os +import pdb +import random +import shutil +import subprocess +import sys +import tempfile +import time +import traceback + +from eth_utils import encode_hex +from test_framework.bsc_node import BSCNode +from test_framework.contract_proxy import FlowContractProxy, MineContractProxy +from test_framework.zgs_node import ZgsNode +from test_framework.blockchain_node import BlockChainNodeType +from test_framework.conflux_node import ConfluxNode, connect_sample_nodes, sync_blocks + +from utility.utils import PortMin, is_windows_platform, wait_until + +__file_path__ = os.path.dirname(os.path.realpath(__file__)) + + +class TestStatus(Enum): + PASSED = 1 + FAILED = 2 + + +TEST_EXIT_PASSED = 0 +TEST_EXIT_FAILED = 1 + + +class TestFramework: + def __init__(self, blockchain_node_type=BlockChainNodeType.Conflux): + self.num_blockchain_nodes = None + self.num_nodes = None + self.blockchain_nodes = [] + self.nodes = [] + self.contract = None + self.blockchain_node_configs = {} + self.zgs_node_configs = {} + self.blockchain_node_type = blockchain_node_type + + def __setup_blockchain_node(self): + for i in range(self.num_blockchain_nodes): + if i in self.blockchain_node_configs: + updated_config = self.blockchain_node_configs[i] + else: + updated_config = {} + + node = None + if self.blockchain_node_type == BlockChainNodeType.BSC: + node = BSCNode( + i, + self.root_dir, + self.blockchain_binary, + updated_config, + self.contract_path, + self.token_contract_path, + self.mine_contract_path, + self.log, + 60, + ) + elif self.blockchain_node_type == BlockChainNodeType.Conflux: + node = ConfluxNode( + i, + self.root_dir, + self.blockchain_binary, + updated_config, + self.contract_path, + self.token_contract_path, + self.mine_contract_path, + self.log, + ) + else: + raise NotImplementedError + + self.blockchain_nodes.append(node) + node.setup_config() + node.start() + + # wait node to start to avoid NewConnectionError + time.sleep(1) + for node in self.blockchain_nodes: + node.wait_for_rpc_connection() + + if self.blockchain_node_type == BlockChainNodeType.BSC: + enodes = set( + [node.admin_nodeInfo()["enode"] for node in self.blockchain_nodes[1:]] + ) + for enode in enodes: + self.blockchain_nodes[0].admin_addPeer([enode]) + + # mine + self.blockchain_nodes[0].miner_start([1]) + + def wait_for_peer(): + peers = self.blockchain_nodes[0].admin_peers() + for peer in peers: + if peer["enode"] in enodes: + enodes.remove(peer["enode"]) + + if enodes: + for enode in enodes: + self.blockchain_nodes[0].admin_addPeer([enode]) + return False + + return True + + wait_until(lambda: wait_for_peer()) + + for node in self.blockchain_nodes: + node.wait_for_start_mining() + elif self.blockchain_node_type == BlockChainNodeType.Conflux: + for node in self.blockchain_nodes: + node.wait_for_nodeid() + + # make nodes full connected + if self.num_blockchain_nodes > 1: + connect_sample_nodes(self.blockchain_nodes, self.log) + sync_blocks(self.blockchain_nodes) + + contract, tx_hash, mine_contract = self.blockchain_nodes[0].setup_contract() + self.contract = FlowContractProxy(contract, self.blockchain_nodes) + self.mine_contract = MineContractProxy(mine_contract, self.blockchain_nodes) + + for node in self.blockchain_nodes[1:]: + node.wait_for_transaction(tx_hash) + + def __setup_zgs_node(self): + for i in range(self.num_nodes): + if i in self.zgs_node_configs: + updated_config = self.zgs_node_configs[i] + else: + updated_config = {} + + assert os.path.exists(self.zgs_binary), ( + "%s should be exist" % self.zgs_binary + ) + node = ZgsNode( + i, + self.root_dir, + self.zgs_binary, + updated_config, + self.contract.address(), + self.mine_contract.address(), + self.log, + ) + self.nodes.append(node) + node.setup_config() + # wait firt node start for connection + if i > 0: + time.sleep(1) + node.start() + + time.sleep(1) + for node in self.nodes: + node.wait_for_rpc_connection() + + def __parse_arguments(self): + parser = argparse.ArgumentParser(usage="%(prog)s [options]") + + parser.add_argument( + "--conflux-binary", + dest="conflux", + default=os.path.join( + __file_path__, + "../tmp/conflux" + (".exe" if is_windows_platform() else ""), + ), + type=str, + ) + + parser.add_argument( + "--bsc-binary", + dest="bsc", + default=os.path.join( + __file_path__, + "../tmp/geth" + (".exe" if is_windows_platform() else ""), + ), + type=str, + ) + + parser.add_argument( + "--zerog-storage-binary", + dest="zerog_storage", + default=os.getenv( + "ZGS", + default=os.path.join( + __file_path__, + "../../target/release/zgs_node" + + (".exe" if is_windows_platform() else ""), + ), + ), + type=str, + ) + + parser.add_argument( + "--zerog-storage-client", + dest="cli", + default=os.path.join( + __file_path__, + "../../target/zerog-storage-client" + + (".exe" if is_windows_platform() else ""), + ), + type=str, + ) + + parser.add_argument( + "--contract-path", + dest="contract", + default=os.path.join( + __file_path__, + "../../zerog-storage-contracts/artifacts/contracts/dataFlow/Flow.sol/Flow.json", + ), + type=str, + ) + + parser.add_argument( + "--token-contract-path", + dest="token_contract", + default=os.path.join( + __file_path__, + "../config/MockToken.json", + ), + type=str, + ) + + parser.add_argument( + "--mine-contract-path", + dest="mine_contract", + default=os.path.join( + __file_path__, + "../../zerog-storage-contracts/artifacts/contracts/test/PoraMineTest.sol/PoraMineTest.json", + ), + type=str, + ) + + parser.add_argument( + "-l", + "--loglevel", + dest="loglevel", + default="INFO", + help="log events at this level and higher to the console. Can be set to DEBUG, INFO, WARNING, ERROR or CRITICAL. Passing --loglevel DEBUG will output all logs to console. Note that logs at all levels are always written to the test_framework.log file in the temporary test directory.", + ) + + parser.add_argument( + "--tmpdir", dest="tmpdir", help="Root directory for datadirs" + ) + + parser.add_argument( + "--randomseed", dest="random_seed", type=int, help="Set a random seed" + ) + + parser.add_argument("--port-min", dest="port_min", default=11000, type=int) + + parser.add_argument( + "--pdbonfailure", + dest="pdbonfailure", + default=False, + action="store_true", + help="Attach a python debugger if test fails", + ) + + self.options = parser.parse_args() + + def __start_logging(self): + # Add logger and logging handlers + self.log = logging.getLogger("TestFramework") + self.log.setLevel(logging.DEBUG) + + # Create file handler to log all messages + fh = logging.FileHandler( + self.options.tmpdir + "/test_framework.log", encoding="utf-8" + ) + fh.setLevel(logging.DEBUG) + + # Create console handler to log messages to stderr. By default this logs only error messages, but can be configured with --loglevel. + ch = logging.StreamHandler(sys.stdout) + # User can provide log level as a number or string (eg DEBUG). loglevel was caught as a string, so try to convert it to an int + ll = ( + int(self.options.loglevel) + if self.options.loglevel.isdigit() + else self.options.loglevel.upper() + ) + ch.setLevel(ll) + + # Format logs the same as bitcoind's debug.log with microprecision (so log files can be concatenated and sorted) + formatter = logging.Formatter( + fmt="%(asctime)s.%(msecs)03d000Z %(name)s (%(levelname)s): %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", + ) + formatter.converter = time.gmtime + fh.setFormatter(formatter) + ch.setFormatter(formatter) + + # add the handlers to the logger + self.log.addHandler(fh) + self.log.addHandler(ch) + + def _upload_file_use_cli( + self, + blockchain_node_rpc_url, + contract_address, + key, + ionion_node_rpc_url, + file_to_upload, + ): + assert os.path.exists(self.cli_binary), "%s should be exist" % self.cli_binary + upload_args = [ + self.cli_binary, + "upload", + "--url", + blockchain_node_rpc_url, + "--contract", + contract_address, + "--key", + encode_hex(key), + "--node", + ionion_node_rpc_url, + "--log-level", + "debug", + "--gas-limit", + "10000000", + "--file", + ] + + proc = subprocess.Popen( + upload_args + [file_to_upload.name], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + proc.wait() + + root = None + lines = proc.stdout.readlines() + for line in lines: + self.log.debug("line: %s", line) + if "root" in line: + index = line.find("root=") + root = line[index + 5 : -1] + self.log.info("root: %s", root) + + assert proc.returncode == 0, "%s upload file failed, log: %s" % (self.cli_binary, lines) + self.log.info("file uploaded") + + return root + + def setup_params(self): + self.num_blockchain_nodes = 1 + self.num_nodes = 1 + + def setup_nodes(self): + self.__setup_blockchain_node() + self.__setup_zgs_node() + + def stop_nodes(self): + # stop ionion nodes first + for node in self.nodes: + node.stop() + + for node in self.blockchain_nodes: + node.stop() + + def stop_storage_node(self, index, clean=False): + self.nodes[index].stop() + if clean: + self.nodes[index].clean_data() + + + def start_storage_node(self, index): + self.nodes[index].start() + + def run_test(self): + raise NotImplementedError + + def main(self): + self.__parse_arguments() + PortMin.n = self.options.port_min + + # Set up temp directory and start logging + if self.options.tmpdir: + self.options.tmpdir = os.path.abspath(self.options.tmpdir) + os.makedirs(self.options.tmpdir, exist_ok=True) + else: + self.options.tmpdir = os.getenv( + "ZGS_TESTS_LOG_DIR", default=tempfile.mkdtemp(prefix="zgs_test_") + ) + + self.root_dir = self.options.tmpdir + + self.__start_logging() + self.log.info("Root dir: %s", self.root_dir) + + if self.blockchain_node_type == BlockChainNodeType.Conflux: + self.blockchain_binary = self.options.conflux + else: + self.blockchain_binary = self.options.bsc + + self.zgs_binary = self.options.zerog_storage + self.cli_binary = self.options.cli + self.contract_path = self.options.contract + self.token_contract_path = self.options.token_contract + self.mine_contract_path = self.options.mine_contract + + assert os.path.exists(self.contract_path), ( + "%s should be exist" % self.contract_path + ) + assert os.path.exists(self.token_contract_path), ( + "%s should be exist" % self.token_contract_path + ) + assert os.path.exists(self.mine_contract_path), ( + "%s should be exist" % self.mine_contract_path + ) + + if self.options.random_seed is not None: + random.seed(self.options.random_seed) + + success = TestStatus.FAILED + try: + self.setup_params() + self.setup_nodes() + self.run_test() + success = TestStatus.PASSED + except AssertionError as e: + self.log.exception("Assertion failed %s", repr(e)) + except KeyboardInterrupt as e: + self.log.warning("Exiting after keyboard interrupt %s", repr(e)) + except Exception as e: + self.log.error("Test exception %s %s", repr(e), traceback.format_exc()) + self.log.error(f"Test data are not deleted: {self.root_dir}") + + if success == TestStatus.FAILED and self.options.pdbonfailure: + print("Testcase failed. Attaching python debugger. Enter ? for help") + pdb.set_trace() + + if success == TestStatus.PASSED: + self.log.info("Tests successful") + exit_code = TEST_EXIT_PASSED + else: + self.log.error( + "Test failed. Test logging available at %s/test_framework.log", + self.options.tmpdir, + ) + exit_code = TEST_EXIT_FAILED + + self.stop_nodes() + + if success == TestStatus.PASSED: + self.log.info("Test success") + shutil.rmtree(self.root_dir) + + handlers = self.log.handlers[:] + for handler in handlers: + self.log.removeHandler(handler) + handler.close() + logging.shutdown() + + sys.exit(exit_code) diff --git a/tests/test_framework/zgs_node.py b/tests/test_framework/zgs_node.py new file mode 100644 index 0000000..a8307a7 --- /dev/null +++ b/tests/test_framework/zgs_node.py @@ -0,0 +1,110 @@ +import os +import shutil +import sys + +from config.node_config import ZGS_CONFIG +from test_framework.blockchain_node import NodeType, TestNode +from config.node_config import MINER_ID +from utility.utils import ( + initialize_toml_config, + p2p_port, + rpc_port, + blockchain_rpc_port, +) + + +class ZgsNode(TestNode): + def __init__( + self, + index, + root_dir, + binary, + updated_config, + log_contract_address, + mine_contract_address, + log, + rpc_timeout=10, + libp2p_nodes=None, + ): + local_conf = ZGS_CONFIG.copy() + if libp2p_nodes is None: + if index == 0: + libp2p_nodes = [] + else: + libp2p_nodes = [] + for i in range(index): + libp2p_nodes.append(f"/ip4/127.0.0.1/tcp/{p2p_port(i)}") + + indexed_config = { + "network_libp2p_port": p2p_port(index), + "network_discovery_port": p2p_port(index), + "rpc_listen_address": f"127.0.0.1:{rpc_port(index)}", + "network_libp2p_nodes": libp2p_nodes, + "log_contract_address": log_contract_address, + "mine_contract_address": mine_contract_address, + "blockchain_rpc_endpoint": f"http://127.0.0.1:{blockchain_rpc_port(0)}", + } + # Set configs for this specific node. + local_conf.update(indexed_config) + # Overwrite with personalized configs. + local_conf.update(updated_config) + data_dir = os.path.join(root_dir, "zgs_node" + str(index)) + rpc_url = "http://" + local_conf["rpc_listen_address"] + super().__init__( + NodeType.Zgs, + index, + data_dir, + rpc_url, + binary, + local_conf, + log, + rpc_timeout, + ) + + def setup_config(self): + os.mkdir(self.data_dir) + log_config_path = os.path.join(self.data_dir, self.config["log_config_file"]) + with open(log_config_path, "w") as f: + f.write("debug") + + initialize_toml_config(self.config_file, self.config) + + def wait_for_rpc_connection(self): + self._wait_for_rpc_connection(lambda rpc: rpc.zgs_getStatus() is not None) + + def start(self): + self.log.info("Start zerog_storage node %d", self.index) + super().start() + + # rpc + def zgs_get_status(self): + return self.rpc.zgs_getStatus()["connectedPeers"] + + def zgs_upload_segment(self, segment): + return self.rpc.zgs_uploadSegment([segment]) + + def zgs_download_segment(self, data_root, start_index, end_index): + return self.rpc.zgs_downloadSegment([data_root, start_index, end_index]) + + def zgs_get_file_info(self, data_root): + return self.rpc.zgs_getFileInfo([data_root]) + + def zgs_get_file_info_by_tx_seq(self, tx_seq): + return self.rpc.zgs_getFileInfoByTxSeq([tx_seq]) + + def shutdown(self): + self.rpc.admin_shutdown() + self.wait_until_stopped() + + def admin_start_sync_file(self, tx_seq): + return self.rpc.admin_startSyncFile([tx_seq]) + + def admin_get_sync_status(self, tx_seq): + return self.rpc.admin_getSyncStatus([tx_seq]) + + def sycn_status_is_completed_or_unknown(self, tx_seq): + status = self.rpc.admin_getSyncStatus([tx_seq]) + return status == "Completed" or status == "unknown" + + def clean_data(self): + shutil.rmtree(os.path.join(self.data_dir, "db")) diff --git a/tests/utility/merkle_tree.py b/tests/utility/merkle_tree.py new file mode 100644 index 0000000..3906aac --- /dev/null +++ b/tests/utility/merkle_tree.py @@ -0,0 +1,238 @@ +import sha3 + +from math import log2 + + +def decompose(num): + powers = [] + while num > 0: + power = int(log2(num)) + powers += [power] + num -= 1 << power + return powers + + +def add_0x_prefix(val): + return "0x" + val + + +class Hasher: + def __init__(self, algorithm="keccak_256", encoding="utf-8", security=False): + self.algorithm = algorithm + self.security = security + self.encoding = encoding + + if security: + self.prefix00 = "\x00".encode(encoding) + self.prefix01 = "\x01".encode(encoding) + else: + self.prefix00 = bytes() + self.prefix01 = bytes() + + def _hasher(self): + if self.algorithm == "keccak_256": + return sha3.keccak_256() + else: + raise NotImplementedError + + def hash_data(self, data): + buff = self.prefix00 + ( + data if isinstance(data, bytes) else data.encode(self.encoding) + ) + + hasher = self._hasher() + hasher.update(buff) + return hasher.hexdigest().encode(self.encoding) + + def hash_pair(self, left, right): + buff = ( + self.prefix01 + + bytes.fromhex(left.decode("utf-8")) + + bytes.fromhex(right.decode("utf-8")) + ) + + hasher = self._hasher() + hasher.update(buff) + return hasher.hexdigest().encode(self.encoding) + + +class Node: + + __slots__ = ("__value", "__parent", "__left", "__right") + + def __init__(self, value, parent=None, left=None, right=None): + self.__value = value + self.__parent = parent + self.__left = left + self.__right = right + + if left: + left.__parent = self + if right: + right.__parent = self + + @property + def value(self): + return self.__value + + @property + def left(self): + return self.__left + + @property + def right(self): + return self.__right + + @property + def parent(self): + return self.__parent + + def set_left(self, node): + self.__left = node + + def set_right(self, node): + self.__right = node + + def set_parent(self, node): + self.__parent = node + + def is_left_child(self): + parent = self.__parent + if not parent: + return False + + return self == parent.left + + def is_right_child(self): + parent = self.__parent + if not parent: + return False + + return self == parent.right + + def is_leaf(self): + return isinstance(self, Leaf) + + @classmethod + def from_children(cls, left, right, hasher): + digest = hasher.hash_pair(left.__value, right.__value) + return cls(value=digest, left=left, right=right, parent=None) + + def ancestor(self, degree): + if degree == 0: + return self + + if not self.__parent: + return + + return self.__parent.ancestor(degree - 1) + + def recalculate_hash(self, hasher): + self.__value = hasher.hash_pair(self.left.value, self.right.value) + + +class Leaf(Node): + def __init__(self, value, leaf=None): + super().__init__(value) + + @classmethod + def from_data(cls, data, hasher): + return cls(hasher.hash_data(data), leaf=None) + + +class MerkleTree: + def __init__(self, encoding="utf-8"): + self.__root = None + self.__leaves = [] + self.encoding = encoding + self.hasher = Hasher(encoding=encoding) + + def __bool__(self): + return len(self.__leaves) != 0 + + def encrypt(self, data): + leaf = Leaf.from_data(data, self.hasher) + self.add_leaf(leaf) + + def add_leaf(self, leaf): + if self: + subroot = self.get_last_subroot() + self._append_leaf(leaf) + + if not subroot.parent: + # Increase height by one + self.__root = Node.from_children(subroot, leaf, self.hasher) + else: + parent = subroot.parent + + # Create bifurcation node + new_node = Node.from_children(subroot, leaf, self.hasher) + + # Interject bifurcation node + parent.set_right(new_node) + new_node.set_parent(parent) + + # Recalculate hashes only at the rightmost branch of the tree + curr = parent + while curr: + curr.recalculate_hash(self.hasher) + curr = curr.parent + else: + self._append_leaf(leaf) + self.__root = leaf + + def get_last_subroot(self): + if not self.__leaves: + raise ValueError + + last_power = decompose(len(self.__leaves))[-1] + return self.get_tail().ancestor(degree=last_power) + + def get_tail(self): + return self.__leaves[-1] + + def _append_leaf(self, leaf): + self.__leaves.append(leaf) + + def get_root_hash(self): + if not self.__root: + return + + return self.__root.value + + def decode_value(self, val): + return val.decode(self.encoding) + + def proof_at(self, i): + if i < 0 or i >= len(self.__leaves): + raise IndexError + + if len(self.__leaves) == 1: + return { + "lemma": [add_0x_prefix(self.decode_value(self.get_root_hash()))], + "path": [], + } + + proof = {"lemma": [], "path": []} + proof["lemma"].append(add_0x_prefix(self.decode_value(self.__leaves[i].value))) + + current = self.__leaves[i] + while current != self.__root: + if current.parent != None and current.parent.left == current: + # add right + proof["lemma"].append( + add_0x_prefix(self.decode_value(current.parent.right.value)) + ) + proof["path"].append(True) + else: + # add left + proof["lemma"].append( + add_0x_prefix(self.decode_value(current.parent.left.value)) + ) + proof["path"].append(False) + + current = current.parent + + # add root + proof["lemma"].append(add_0x_prefix(self.decode_value(self.get_root_hash()))) + return proof diff --git a/tests/utility/signature_utils.py b/tests/utility/signature_utils.py new file mode 100644 index 0000000..84d21d7 --- /dev/null +++ b/tests/utility/signature_utils.py @@ -0,0 +1,615 @@ +import coincurve +import random +import rlp +import sha3 as _sha3 + +from eth_utils import decode_hex, int_to_big_endian, big_endian_to_int +from eth_utils import encode_hex as encode_hex_0x +from py_ecc.secp256k1 import privtopub, ecdsa_raw_sign, ecdsa_raw_recover +from rlp.sedes import big_endian_int, BigEndianInt, Binary +from rlp.utils import ALL_BYTES + + +def sha3_256(x): + return _sha3.keccak_256(x).digest() + + +class Memoize: + def __init__(self, fn): + self.fn = fn + self.memo = {} + + def __call__(self, *args): + if args not in self.memo: + self.memo[args] = self.fn(*args) + return self.memo[args] + + +TT256 = 2**256 +TT256M1 = 2**256 - 1 +TT255 = 2**255 +SECP256K1P = 2**256 - 4294968273 + + +def is_numeric(x): + return isinstance(x, int) + + +def is_string(x): + return isinstance(x, bytes) + + +def to_string(value): + if isinstance(value, bytes): + return value + if isinstance(value, str): + return bytes(value, "utf-8") + if isinstance(value, int): + return bytes(str(value), "utf-8") + + +def int_to_bytes(value): + if isinstance(value, bytes): + return value + return int_to_big_endian(value) + + +def to_string_for_regexp(value): + return str(to_string(value), "utf-8") + + +unicode = str + + +def bytearray_to_bytestr(value): + return bytes(value) + + +def encode_int32(v): + return v.to_bytes(32, byteorder="big") + + +def bytes_to_int(value): + return int.from_bytes(value, byteorder="big") + + +def str_to_bytes(value): + if isinstance(value, bytearray): + value = bytes(value) + if isinstance(value, bytes): + return value + return bytes(value, "utf-8") + + +def ascii_chr(n): + return ALL_BYTES[n] + + +def encode_hex(n): + if isinstance(n, str): + return encode_hex(n.encode("ascii")) + return encode_hex_0x(n)[2:] + + +def ecrecover_to_pub(rawhash, v, r, s): + if coincurve and hasattr(coincurve, "PublicKey"): + try: + pk = coincurve.PublicKey.from_signature_and_message( + zpad(bytearray_to_bytestr(int_to_32bytearray(r)), 32) + + zpad(bytearray_to_bytestr(int_to_32bytearray(s)), 32) + + ascii_chr(v - 27), + rawhash, + hasher=None, + ) + pub = pk.format(compressed=False)[1:] + x, y = pk.point() + except BaseException: + x, y = 0, 0 + pub = b"\x00" * 64 + else: + result = ecdsa_raw_recover(rawhash, (v, r, s)) + if result: + x, y = result + pub = encode_int32(x) + encode_int32(y) + else: + raise ValueError("Invalid VRS") + assert len(pub) == 64 + return pub, x, y + + +def ecsign(rawhash, key): + if coincurve and hasattr(coincurve, "PrivateKey"): + pk = coincurve.PrivateKey(key) + signature = pk.sign_recoverable(rawhash, hasher=None) + v = safe_ord(signature[64]) + 27 + r = big_endian_to_int(signature[0:32]) + s = big_endian_to_int(signature[32:64]) + else: + v, r, s = ecdsa_raw_sign(rawhash, key) + return v, r, s + + +def ec_random_keys(): + priv_key = random.randint(0, 2**256).to_bytes(32, "big") + pub_key = privtopub(priv_key) + return priv_key, pub_key + + +def convert_to_nodeid(signature, challenge): + r = big_endian_to_int(signature[:32]) + s = big_endian_to_int(signature[32:64]) + v = big_endian_to_int(signature[64:]) + 27 + signed = int_to_bytes(challenge) + h_signed = sha3_256(signed) + return ecrecover_to_pub(h_signed, v, r, s) + + +def get_nodeid(node): + challenge = random.randint(0, 2**32 - 1) + signature = node.getnodeid(list(int_to_bytes(challenge))) + return convert_to_nodeid(signature, challenge) + + +def mk_contract_address(sender, nonce): + return sha3(rlp.encode([normalize_address(sender), nonce]))[12:] + + +def mk_metropolis_contract_address(sender, initcode): + return sha3(normalize_address(sender) + initcode)[12:] + + +def safe_ord(value): + if isinstance(value, int): + return value + else: + return ord(value) + + +# decorator + + +def debug(label): + def deb(f): + def inner(*args, **kwargs): + i = random.randrange(1000000) + print(label, i, "start", args) + x = f(*args, **kwargs) + print(label, i, "end", x) + return x + + return inner + + return deb + + +def flatten(li): + o = [] + for l in li: + o.extend(l) + return o + + +def bytearray_to_int(arr): + o = 0 + for a in arr: + o = (o << 8) + a + return o + + +def int_to_32bytearray(i): + o = [0] * 32 + for x in range(32): + o[31 - x] = i & 0xFF + i >>= 8 + return o + + +# sha3_count = [0] + + +def sha3(seed): + return sha3_256(to_string(seed)) + + +assert ( + encode_hex(sha3(b"")) + == "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" +) +assert ( + encode_hex(sha3(b"\x00" * 256)) + == "d397b3b043d87fcd6fad1291ff0bfd16401c274896d8c63a923727f077b8e0b5" +) + + +@Memoize +def priv_to_addr(k): + k = normalize_key(k) + x, y = privtopub(k) + addr = bytearray(sha3(encode_int32(x) + encode_int32(y))[12:]) + addr[0] &= 0x0F + addr[0] |= 0x10 + return bytes(addr) + + +def priv_to_pub(k): + k = normalize_key(k) + x, y = privtopub(k) + return bytes(encode_int32(x) + encode_int32(y)) + + +def pub_to_addr(k): + x = big_endian_to_int(decode_hex(k[2:34])) + y = big_endian_to_int(decode_hex(k[34:66])) + addr = sha3(encode_int32(x) + encode_int32(y))[12:] + addr[0] &= 0x0F + addr[0] |= 0x10 + return bytes(addr) + + +def checksum_encode(addr): # Takes a 20-byte binary address as input + addr = normalize_address(addr) + o = "" + v = big_endian_to_int(sha3(encode_hex(addr))) + for i, c in enumerate(encode_hex(addr)): + if c in "0123456789": + o += c + else: + o += c.upper() if (v & (2 ** (255 - 4 * i))) else c.lower() + return "0x" + o + + +def check_checksum(addr): + return checksum_encode(normalize_address(addr)) == addr + + +def normalize_address(x, allow_blank=False): + if is_numeric(x): + return int_to_addr(x) + if allow_blank and x in {"", b""}: + return b"" + if len(x) in (42, 50) and x[:2] in {"0x", b"0x"}: + x = x[2:] + if len(x) in (40, 48): + x = decode_hex(x) + if len(x) == 24: + assert len(x) == 24 and sha3(x[:20])[:4] == x[-4:] + x = x[:20] + if len(x) != 20: + raise Exception("Invalid address format: %r" % x) + return x + + +def normalize_key(key): + if is_numeric(key): + o = encode_int32(key) + elif len(key) == 32: + o = key + elif len(key) == 64: + o = decode_hex(key) + elif len(key) == 66 and key[:2] == "0x": + o = decode_hex(key[2:]) + else: + raise Exception("Invalid key format: %r" % key) + if o == b"\x00" * 32: + raise Exception("Zero privkey invalid") + return o + + +def zpad(x, l): + """Left zero pad value `x` at least to length `l`. + + >>> zpad('', 1) + '\x00' + >>> zpad('\xca\xfe', 4) + '\x00\x00\xca\xfe' + >>> zpad('\xff', 1) + '\xff' + >>> zpad('\xca\xfe', 2) + '\xca\xfe' + """ + return b"\x00" * max(0, l - len(x)) + x + + +def rzpad(value, total_length): + """Right zero pad value `x` at least to length `l`. + + >>> zpad('', 1) + '\x00' + >>> zpad('\xca\xfe', 4) + '\xca\xfe\x00\x00' + >>> zpad('\xff', 1) + '\xff' + >>> zpad('\xca\xfe', 2) + '\xca\xfe' + """ + return value + b"\x00" * max(0, total_length - len(value)) + + +def int_to_addr(x): + o = [b""] * 20 + for i in range(20): + o[19 - i] = ascii_chr(x & 0xFF) + x >>= 8 + return b"".join(o) + + +def coerce_addr_to_bin(x): + if is_numeric(x): + return encode_hex(zpad(big_endian_int.serialize(x), 20)) + elif len(x) == 40 or len(x) == 0: + return decode_hex(x) + else: + return zpad(x, 20)[-20:] + + +def coerce_addr_to_hex(x): + if is_numeric(x): + return encode_hex(zpad(big_endian_int.serialize(x), 20)) + elif len(x) == 40 or len(x) == 0: + return x + else: + return encode_hex(zpad(x, 20)[-20:]) + + +def coerce_to_int(x): + if is_numeric(x): + return x + elif len(x) == 40: + return big_endian_to_int(decode_hex(x)) + else: + return big_endian_to_int(x) + + +def coerce_to_bytes(x): + if is_numeric(x): + return big_endian_int.serialize(x) + elif len(x) == 40: + return decode_hex(x) + else: + return x + + +def parse_int_or_hex(s): + if is_numeric(s): + return s + elif s[:2] in (b"0x", "0x"): + s = to_string(s) + tail = (b"0" if len(s) % 2 else b"") + s[2:] + return big_endian_to_int(decode_hex(tail)) + else: + return int(s) + + +def ceil32(x): + return x if x % 32 == 0 else x + 32 - (x % 32) + + +def to_signed(i): + return i if i < TT255 else i - TT256 + + +def sha3rlp(x): + return sha3(rlp.encode(x)) + + +# Format encoders/decoders for bin, addr, int + + +def decode_bin(v): + """decodes a bytearray from serialization""" + if not is_string(v): + raise Exception("Value must be binary, not RLP array") + return v + + +def decode_addr(v): + """decodes an address from serialization""" + if len(v) not in [0, 20]: + raise Exception("Serialized addresses must be empty or 20 bytes long!") + return encode_hex(v) + + +def decode_int(v): + """decodes and integer from serialization""" + if len(v) > 0 and (v[0] == b"\x00" or v[0] == 0): + raise Exception("No leading zero bytes allowed for integers") + return big_endian_to_int(v) + + +def decode_int256(v): + return big_endian_to_int(v) + + +def encode_bin(v): + """encodes a bytearray into serialization""" + return v + + +def encode_root(v): + """encodes a trie root into serialization""" + return v + + +def encode_int(v): + """encodes an integer into serialization""" + if not is_numeric(v) or v < 0 or v >= TT256: + raise Exception("Integer invalid or out of range: %r" % v) + return int_to_big_endian(v) + + +def encode_int256(v): + return zpad(int_to_big_endian(v), 256) + + +def scan_bin(v): + if v[:2] in ("0x", b"0x"): + return decode_hex(v[2:]) + else: + return decode_hex(v) + + +def scan_int(v): + if v[:2] in ("0x", b"0x"): + return big_endian_to_int(decode_hex(v[2:])) + else: + return int(v) + + +# Decoding from RLP serialization +decoders = { + "bin": decode_bin, + "addr": decode_addr, + "int": decode_int, + "int256b": decode_int256, +} + +# Encoding to RLP serialization +encoders = { + "bin": encode_bin, + "int": encode_int, + "trie_root": encode_root, + "int256b": encode_int256, +} + +# Encoding to printable format +printers = { + "bin": lambda v: "0x" + encode_hex(v), + "addr": lambda v: v, + "int": lambda v: to_string(v), + "trie_root": lambda v: encode_hex(v), + "int256b": lambda x: encode_hex(zpad(encode_int256(x), 256)), +} + +# Decoding from printable format +scanners = { + "bin": scan_bin, + "addr": lambda x: x[2:] if x[:2] in (b"0x", "0x") else x, + "int": scan_int, + "trie_root": lambda x: scan_bin, + "int256b": lambda x: big_endian_to_int(decode_hex(x)), +} + + +def int_to_hex(x): + o = encode_hex(encode_int(x)) + return "0x" + (o[1:] if (len(o) > 0 and o[0] == b"0") else o) + + +def remove_0x_head(s): + return s[2:] if s[:2] in (b"0x", "0x") else s + + +def parse_as_bin(s): + return decode_hex(s[2:] if s[:2] == "0x" else s) + + +def parse_as_int(s): + return s if is_numeric(s) else int("0" + s[2:], 16) if s[:2] == "0x" else int(s) + + +def print_func_call(ignore_first_arg=False, max_call_number=100): + """utility function to facilitate debug, it will print input args before + function call, and print return value after function call + + usage: + + @print_func_call + def some_func_to_be_debu(): + pass + + :param ignore_first_arg: whether print the first arg or not. + useful when ignore the `self` parameter of an object method call + """ + from functools import wraps + + def display(x): + x = to_string(x) + try: + x.decode("ascii") + except BaseException: + return "NON_PRINTABLE" + return x + + local = {"call_number": 0} + + def inner(f): + @wraps(f) + def wrapper(*args, **kwargs): + local["call_number"] += 1 + tmp_args = args[1:] if ignore_first_arg and len(args) else args + this_call_number = local["call_number"] + print( + ( + "{0}#{1} args: {2}, {3}".format( + f.__name__, + this_call_number, + ", ".join([display(x) for x in tmp_args]), + ", ".join( + display(key) + "=" + to_string(value) + for key, value in kwargs.items() + ), + ) + ) + ) + res = f(*args, **kwargs) + print( + ( + "{0}#{1} return: {2}".format( + f.__name__, this_call_number, display(res) + ) + ) + ) + + if local["call_number"] > 100: + raise Exception("Touch max call number!") + return res + + return wrapper + + return inner + + +def dump_state(trie): + res = "" + for k, v in list(trie.to_dict().items()): + res += "%r:%r\n" % (encode_hex(k), encode_hex(v)) + return res + + +class Denoms: + def __init__(self): + self.wei = 1 + self.babbage = 10**3 + self.ada = 10**3 + self.kwei = 10**3 + self.lovelace = 10**6 + self.mwei = 10**6 + self.shannon = 10**9 + self.gwei = 10**9 + self.szabo = 10**12 + self.finney = 10**15 + self.mether = 10**15 + self.ether = 10**18 + self.turing = 2**256 - 1 + + +denoms = Denoms() + +address = Binary.fixed_length(20, allow_empty=True) +int20 = BigEndianInt(20) +int32 = BigEndianInt(32) +int256 = BigEndianInt(256) +hash32 = Binary.fixed_length(32) +hash20 = Binary.fixed_length(20) +trie_root = Binary.fixed_length(32, allow_empty=True) + + +class bcolors: + HEADER = "\033[95m" + OKBLUE = "\033[94m" + OKGREEN = "\033[92m" + WARNING = "\033[91m" + FAIL = "\033[91m" + ENDC = "\033[0m" + BOLD = "\033[1m" + UNDERLINE = "\033[4m" diff --git a/tests/utility/simple_rpc_proxy.py b/tests/utility/simple_rpc_proxy.py new file mode 100644 index 0000000..26db293 --- /dev/null +++ b/tests/utility/simple_rpc_proxy.py @@ -0,0 +1,31 @@ +from jsonrpcclient import request, parse, Ok +import requests + + +class SimpleRpcProxy: + def __init__(self, url, timeout=60): + self.url = url + self.timeout = timeout + + def __getattr__(self, name): + return RpcCaller(self.url, name, self.timeout) + + +class RpcCaller: + def __init__(self, url, method, timeout): + self.url = url + self.method = method + self.timeout = timeout + + def __call__(self, *args, **argsn): + r = request(self.method, *args) + try: + response = requests.post(self.url, json=r, timeout=self.timeout) + parsed = parse(response.json()) + if isinstance(parsed, Ok): + return parsed.result + else: + print(parsed) + except Exception as ex: + print(ex) + return None diff --git a/tests/utility/submission.py b/tests/utility/submission.py new file mode 100644 index 0000000..bcb363b --- /dev/null +++ b/tests/utility/submission.py @@ -0,0 +1,228 @@ +import base64 + +from eth_utils import encode_hex, decode_hex +from math import log2 +from utility.merkle_tree import add_0x_prefix, Leaf, MerkleTree + + +ENTRY_SIZE = 256 +PORA_CHUNK_SIZE = 1024 + + +def log2_pow2(n): + return int(log2(((n ^ (n - 1)) >> 1) + 1)) + + +def next_pow2(input): + x = input + x -= 1 + x |= x >> 16 + x |= x >> 8 + x |= x >> 4 + x |= x >> 2 + x |= x >> 1 + x += 1 + + return x + + +def bytes_to_entries(size_bytes): + if size_bytes % ENTRY_SIZE == 0: + return size_bytes // ENTRY_SIZE + else: + return size_bytes // ENTRY_SIZE + 1 + + +def create_submission(data): + submission = [] + submission.append(len(data)) + submission.append(b"") + submission.append([]) + + offset = 0 + nodes = [] + for chunks in split_nodes(len(data)): + node_hash = create_node(data, offset, chunks) + nodes.append(node_hash) + + height = int(log2(chunks)) + submission[2].append([decode_hex(node_hash.decode("utf-8")), height]) + offset += chunks * ENTRY_SIZE + + root_hash = nodes[-1] + for i in range(len(nodes) - 2, -1, -1): + tree = MerkleTree() + tree.add_leaf(Leaf(nodes[i])) + tree.add_leaf(Leaf(root_hash)) + root_hash = tree.get_root_hash() + + return submission, add_0x_prefix(root_hash.decode("utf-8")) + + +def split_nodes(data_len): + nodes = [] + + chunks = bytes_to_entries(data_len) + padded_chunks, chunks_next_pow2 = compute_padded_size(chunks) + next_chunk_size = chunks_next_pow2 + + while padded_chunks > 0: + if padded_chunks >= next_chunk_size: + padded_chunks -= next_chunk_size + nodes.append(next_chunk_size) + + next_chunk_size >>= 1 + + return nodes + + +def compute_padded_size(chunk_len): + chunks_next_pow2 = next_pow2(chunk_len) + + if chunks_next_pow2 == chunk_len: + return chunks_next_pow2, chunks_next_pow2 + + min_chunk = 1 if chunks_next_pow2 < 16 else chunks_next_pow2 // 16 + padded_chunks = ((chunk_len - 1) // min_chunk + 1) * min_chunk + + return padded_chunks, chunks_next_pow2 + + +def create_node(data, offset, chunks): + batch = chunks + if chunks > PORA_CHUNK_SIZE: + batch = PORA_CHUNK_SIZE + + return create_segment_node(data, offset, ENTRY_SIZE * batch, ENTRY_SIZE * chunks) + + +def create_segment_node(data, offset, batch, size): + tree = MerkleTree() + i = offset + n = len(data) + while i < offset + size: + start = i + end = min(offset + size, i + batch) + + if start >= n: + tree.add_leaf(Leaf(segment_root(b"\x00" * (end - start)))) + elif end > n: + tree.add_leaf(Leaf(segment_root(data[start:] + b"\x00" * (end - n)))) + else: + tree.add_leaf(Leaf(segment_root(data[start:end]))) + + i += batch + + return tree.get_root_hash() + + +def segment_root(chunks): + data_len = len(chunks) + if data_len == 0: + return b"\x00" * 32 + + tree = MerkleTree() + for i in range(0, data_len, ENTRY_SIZE): + tree.encrypt(chunks[i : i + ENTRY_SIZE]) + + return tree.get_root_hash() + + +def generate_merkle_tree(data): + chunks = bytes_to_entries(len(data)) + padded_chunks, _ = compute_padded_size(chunks) + + tree = MerkleTree() + for i in range(padded_chunks): + if i * ENTRY_SIZE > len(data): + tree.encrypt(b"\x00" * ENTRY_SIZE) + elif (i + 1) * ENTRY_SIZE > len(data): + tree.encrypt( + data[i * ENTRY_SIZE :] + b"\x00" * ((i + 1) * ENTRY_SIZE - len(data)) + ) + else: + tree.encrypt(data[i * ENTRY_SIZE : (i + 1) * ENTRY_SIZE]) + + return tree + + +def generate_merkle_tree_by_batch(data): + chunks = bytes_to_entries(len(data)) + padded_chunks, _ = compute_padded_size(chunks) + + tree = MerkleTree() + for i in range(0, padded_chunks, PORA_CHUNK_SIZE): + if i * ENTRY_SIZE >= len(data): + tree.add_leaf( + Leaf( + segment_root( + b"\x00" * ENTRY_SIZE * min(PORA_CHUNK_SIZE, padded_chunks - i) + ) + ) + ) + elif (i + PORA_CHUNK_SIZE) * ENTRY_SIZE > len(data): + tree.add_leaf( + Leaf( + segment_root( + data[i * ENTRY_SIZE :] + + b"\x00" + * ( + min(padded_chunks, i + PORA_CHUNK_SIZE) * ENTRY_SIZE + - len(data) + ) + ) + ) + ) + else: + tree.add_leaf( + Leaf( + segment_root( + data[i * ENTRY_SIZE : (i + PORA_CHUNK_SIZE) * ENTRY_SIZE] + ) + ) + ) + + return tree, add_0x_prefix(tree.decode_value(tree.get_root_hash())) + + +def submit_data(client, data): + segments = data_to_segments(data) + for segment in segments: + client.zgs_upload_segment(segment) + return segments + + +def data_to_segments(data): + tree, root_hash = generate_merkle_tree_by_batch(data) + chunks = bytes_to_entries(len(data)) + + segments = [] + idx = 0 + while idx * PORA_CHUNK_SIZE < chunks: + proof = tree.proof_at(idx) + + tmp = ( + data[ + idx + * ENTRY_SIZE + * PORA_CHUNK_SIZE : (idx + 1) + * ENTRY_SIZE + * PORA_CHUNK_SIZE + ] + if len(data) >= (idx + 1) * PORA_CHUNK_SIZE * ENTRY_SIZE + else data[idx * ENTRY_SIZE * PORA_CHUNK_SIZE :] + + b"\x00" * (chunks * ENTRY_SIZE - len(data)) + ) + + segment = { + "root": root_hash, + "data": base64.b64encode(tmp).decode("utf-8"), + "index": idx, + "proof": proof, + "fileSize": len(data), + } + + segments.append(segment) + idx += 1 + + return segments \ No newline at end of file diff --git a/tests/utility/utils.py b/tests/utility/utils.py new file mode 100644 index 0000000..95d3084 --- /dev/null +++ b/tests/utility/utils.py @@ -0,0 +1,130 @@ +import base64 +import inspect +import os +import platform +import rtoml +import time + +from config.node_config import ZGS_CONFIG +from eth_utils import encode_hex + + +class PortMin: + # Must be initialized with a unique integer for each process + n = 11000 + + +MAX_NODES = 100 + + +def p2p_port(n): + assert n <= MAX_NODES + return PortMin.n + n + + +def rpc_port(n): + return PortMin.n + MAX_NODES + n + + +def blockchain_p2p_port(n): + return PortMin.n + 2 * MAX_NODES + n + + +def blockchain_rpc_port(n): + return PortMin.n + 3 * MAX_NODES + n + + +def blockchain_rpc_port_core(n): + return PortMin.n + 4 * MAX_NODES + n + + +def wait_until(predicate, *, attempts=float("inf"), timeout=float("inf"), lock=None): + if attempts == float("inf") and timeout == float("inf"): + timeout = 60 + attempt = 0 + time_end = time.time() + timeout + + while attempt < attempts and time.time() < time_end: + if lock: + with lock: + if predicate(): + return + else: + if predicate(): + return + attempt += 1 + time.sleep(0.5) + + # Print the cause of the timeout + predicate_source = inspect.getsourcelines(predicate) + if attempt >= attempts: + raise AssertionError( + "Predicate {} not true after {} attempts".format(predicate_source, attempts) + ) + elif time.time() >= time_end: + raise AssertionError( + "Predicate {} not true after {} seconds".format(predicate_source, timeout) + ) + raise RuntimeError("Unreachable") + + +def is_windows_platform(): + return platform.system().lower() == "windows" + + +def initialize_config(config_path, config_parameters): + with open(config_path, "w") as f: + for k in config_parameters: + value = config_parameters[k] + if isinstance(value, str) and not ( + value.startswith('"') or value.startswith("'") + ): + if value == "true" or value == "false": + value = f"{value}" + else: + value = f'"{value}"' + + f.write(f"{k}={value}\n") + + +def initialize_toml_config(config_path, config_parameters): + with open(config_path, "w") as f: + rtoml.dump(config_parameters, f) + + +def create_proof_and_segment(chunk_data, data_root, index=0): + proof = { + "lemma": [data_root], + "path": [], + } + + segment = { + "root": data_root, + "data": base64.b64encode(chunk_data).decode("utf-8"), + "index": index, + "proof": proof, + } + + return proof, segment + + +def assert_equal(thing1, thing2, *args): + if thing1 != thing2 or any(thing1 != arg for arg in args): + raise AssertionError( + "not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args) + ) + + +def assert_ne(thing1, thing2): + if thing1 == thing2: + raise AssertionError("not(%s)" % " != ".join([thing1, thing2])) + + +def assert_greater_than(thing1, thing2): + if thing1 <= thing2: + raise AssertionError("%s <= %s" % (str(thing1), str(thing2))) + + +def assert_greater_than_or_equal(thing1, thing2): + if thing1 < thing2: + raise AssertionError("%s < %s" % (str(thing1), str(thing2))) diff --git a/version-meld/discv5/.cargo-ok b/version-meld/discv5/.cargo-ok new file mode 100644 index 0000000..b5754e2 --- /dev/null +++ b/version-meld/discv5/.cargo-ok @@ -0,0 +1 @@ +ok \ No newline at end of file diff --git a/version-meld/discv5/.cargo_vcs_info.json b/version-meld/discv5/.cargo_vcs_info.json new file mode 100644 index 0000000..5ce3272 --- /dev/null +++ b/version-meld/discv5/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "00b4b1b1987d443b8f3f25647a0cfe9c5d5f29db" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/version-meld/discv5/Cargo.lock b/version-meld/discv5/Cargo.lock new file mode 100644 index 0000000..9d63d70 --- /dev/null +++ b/version-meld/discv5/Cargo.lock @@ -0,0 +1,2102 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array", +] + +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "ctr", + "opaque-debug", +] + +[[package]] +name = "aes-gcm" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "991984e3fd003e7ba02eb724f87a0f997b78677c46c0e91f8424ad7394c9886a" +dependencies = [ + "getrandom 0.2.3", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "asn1_der" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22d1f4b888c298a027c99dc9048015fac177587de20fc30232a057dfbe24a21" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitvec" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7774144344a4faa177370406a7ff5f1da24303817368584c6206c8303eb07848" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1d36a02058e76b040de25a4464ba1c80935655595b661505c8b39b664828b95" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + +[[package]] +name = "bs58" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" + +[[package]] +name = "bumpalo" +version = "3.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cc" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + +[[package]] +name = "colored" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59" +dependencies = [ + "atty", + "lazy_static", + "winapi", +] + +[[package]] +name = "const-oid" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279bc8fc53f788a75c7804af68237d1fce02cde1e275a886a4b320604dc2aeda" + +[[package]] +name = "cpufeatures" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d6b536309245c849479fba3da410962a43ed8e51c26b729208ec0ac2798d0" +dependencies = [ + "generic-array", +] + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "ctr" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "data-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" + +[[package]] +name = "der" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eeb9d92785d1facb50567852ce75d0858630630e7eabea59cf7eb7474051087" +dependencies = [ + "const-oid", + "typenum", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b697d66081d42af4fba142d56918a3cb21dc8eb63372c6b85d14f44fb9c5979b" +dependencies = [ + "block-buffer 0.10.0", + "crypto-common", + "generic-array", +] + +[[package]] +name = "discv5" +version = "0.1.0-beta.13" +dependencies = [ + "aes", + "aes-gcm", + "arrayvec", + "digest 0.10.1", + "enr", + "env_logger 0.9.0", + "fnv", + "futures", + "hashlink", + "hex", + "hex-literal", + "hkdf", + "lazy_static", + "libp2p-core", + "lru", + "parking_lot", + "quickcheck", + "rand 0.7.3", + "rand 0.8.4", + "rand_core 0.6.3", + "rand_xorshift", + "rlp", + "sha2", + "simple_logger", + "smallvec", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "tracing-subscriber", + "uint", + "zeroize", +] + +[[package]] +name = "ecdsa" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34d33b390ab82f2e1481e331dbd0530895640179d2128ef9a79cc690b78d1eba" +dependencies = [ + "der", + "elliptic-curve", + "hmac 0.11.0", + "signature", +] + +[[package]] +name = "ed25519" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4620d40f6d2601794401d6dd95a5cf69b6c157852539470eeda433a99b3c0efc" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand 0.7.3", + "serde", + "sha2", + "zeroize", +] + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "elliptic-curve" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13e9b0c3c4170dcc2a12783746c4205d98e18957f57854251eea3f9750fe005" +dependencies = [ + "bitvec", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core 0.6.3", + "subtle", + "zeroize", +] + +[[package]] +name = "enr" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809869a1328bfb586b48c9c0f87761c47c41793a85bcb06f66074a87cafc1bcd" +dependencies = [ + "base64", + "bs58", + "bytes", + "ed25519-dalek", + "hex", + "k256", + "log", + "rand 0.8.4", + "rlp", + "serde", + "sha3", + "zeroize", +] + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "ff" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a4d941a5b7c2a75222e2d44fcdf634a67133d9db31e177ae5ff6ecda852bfe" +dependencies = [ + "bitvec", + "rand_core 0.6.3", + "subtle", +] + +[[package]] +name = "fixedbitset" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "398ea4fabe40b9b0d885340a2a991a44c8a645624075ad966d21f88688e2b69e" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + +[[package]] +name = "futures" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7" + +[[package]] +name = "futures-executor" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", + "num_cpus", +] + +[[package]] +name = "futures-io" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2" + +[[package]] +name = "futures-macro" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508" + +[[package]] +name = "futures-task" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72" + +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + +[[package]] +name = "futures-util" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.10.2+wasi-snapshot-preview1", +] + +[[package]] +name = "ghash" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "group" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b3c1e8b4f1ca07e6605ea1be903a5f6956aec5c8a67fd44d56076631675ed8" +dependencies = [ + "ff", + "rand_core 0.6.3", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" + +[[package]] +name = "hkdf" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b" +dependencies = [ + "digest 0.9.0", + "hmac 0.11.0", +] + +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac 0.8.0", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac 0.11.1", + "digest 0.9.0", +] + +[[package]] +name = "hmac-drbg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +dependencies = [ + "digest 0.9.0", + "generic-array", + "hmac 0.8.1", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "716d3d89f35ac6a34fd0eed635395f4c3b76fa889338a4632e5231a8684216bd" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itertools" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "js-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3e8e491ed22bc161583a1c77e42313672c483eba6bd9d7afec0f1131d0b9ce" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "sha2", +] + +[[package]] +name = "keccak" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" + +[[package]] +name = "libp2p-core" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef22d9bba1e8bcb7ec300073e6802943fe8abb8190431842262b5f1c30abba1" +dependencies = [ + "asn1_der", + "bs58", + "ed25519-dalek", + "either", + "fnv", + "futures", + "futures-timer", + "lazy_static", + "libsecp256k1", + "log", + "multiaddr", + "multihash", + "multistream-select", + "parking_lot", + "pin-project 1.0.8", + "prost", + "prost-build", + "rand 0.8.4", + "ring", + "rw-stream-sink", + "sha2", + "smallvec", + "thiserror", + "unsigned-varint", + "void", + "zeroize", +] + +[[package]] +name = "libsecp256k1" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0452aac8bab02242429380e9b2f94ea20cea2b37e2c1777a1358799bbe97f37" +dependencies = [ + "arrayref", + "base64", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand 0.8.4", + "serde", + "sha2", + "typenum", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "lock_api" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "lru" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469898e909a1774d844793b347135a0cd344ca2f69d082013ecb8061a2229a3a" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "mio" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "multiaddr" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ee4ea82141951ac6379f964f71b20876d43712bea8faf6dd1a375e08a46499" +dependencies = [ + "arrayref", + "bs58", + "byteorder", + "data-encoding", + "multihash", + "percent-encoding", + "serde", + "static_assertions", + "unsigned-varint", + "url", +] + +[[package]] +name = "multihash" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "752a61cd890ff691b4411423d23816d5866dd5621e4d1c5687a53b94b5a979d8" +dependencies = [ + "digest 0.9.0", + "generic-array", + "multihash-derive", + "sha2", + "unsigned-varint", +] + +[[package]] +name = "multihash-derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "424f6e86263cd5294cbd7f1e95746b95aca0e0d66bff31e5a40d6baa87b4aa99" +dependencies = [ + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + +[[package]] +name = "multistream-select" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d91ec0a2440aaff5f78ec35631a7027d50386c6163aa975f7caa0d5da4b6ff8" +dependencies = [ + "bytes", + "futures", + "log", + "pin-project 1.0.8", + "smallvec", + "unsigned-varint", +] + +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "petgraph" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a13a2fa9d0b63e5f22328828741e523766fff0ee9e779316902290dff3f824f" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918192b5c59119d51e0cd221f4d49dde9112824ba717369e903c97d076083d0f" +dependencies = [ + "pin-project-internal 0.4.28", +] + +[[package]] +name = "pin-project" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576bc800220cc65dac09e99e97b08b358cfab6e17078de8dc5fee223bd2d0c08" +dependencies = [ + "pin-project-internal 1.0.8", +] + +[[package]] +name = "pin-project-internal" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be26700300be6d9d23264c73211d8190e755b6b5ca7a1b28230025511b52a5e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e8fe8163d14ce7f0cdac2e040116f22eac817edabff0be91e8aff7e9accf389" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9c2f795bc591cb3384cb64082a578b89207ac92bb89c9d98c1ea2ace7cd8110" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "polyval" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "proc-macro-crate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" +dependencies = [ + "thiserror", + "toml", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "prost" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" +dependencies = [ + "bytes", + "heck", + "itertools", + "lazy_static", + "log", + "multimap", + "petgraph", + "prost", + "prost-types", + "regex", + "tempfile", + "which", +] + +[[package]] +name = "prost-derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" +dependencies = [ + "bytes", + "prost", +] + +[[package]] +name = "quickcheck" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44883e74aa97ad63db83c4bf8ca490f02b2fc02f92575e720c8551e843c945f" +dependencies = [ + "env_logger 0.7.1", + "log", + "rand 0.7.3", + "rand_core 0.5.1", +] + +[[package]] +name = "quote" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", +] + +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.3", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core 0.6.3", +] + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core 0.6.3", +] + +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rlp" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "999508abb0ae792aabed2460c45b89106d97fe4adac593bdaef433c2605847b5" +dependencies = [ + "bytes", + "rustc-hex", +] + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rw-stream-sink" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4da5fcb054c46f5a5dff833b129285a93d3f0179531735e6c866e8cc307d2020" +dependencies = [ + "futures", + "pin-project 0.4.28", + "static_assertions", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha2" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha3" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "keccak", + "opaque-debug", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19772be3c4dd2ceaacf03cb41d5885f2a02c4d8804884918e3a258480803335" +dependencies = [ + "digest 0.9.0", + "rand_core 0.6.3", +] + +[[package]] +name = "simple_logger" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45b60258a35dc3cb8a16890b8fd6723349bfa458d7960e25e633f1b1c19d7b5e" +dependencies = [ + "atty", + "colored", + "log", + "time", + "winapi", +] + +[[package]] +name = "slab" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" + +[[package]] +name = "smallvec" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spki" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dae7e047abc519c96350e9484a96c6bf1492348af912fd3446dd2dc323f6268" +dependencies = [ + "der", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +dependencies = [ + "cfg-if", + "libc", + "rand 0.8.4", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" +dependencies = [ + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41effe7cfa8af36f439fac33861b66b049edc6f9a32331e2312660529c1c24ad" +dependencies = [ + "itoa", + "libc", + "time-macros", +] + +[[package]] +name = "time-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6" + +[[package]] +name = "tinyvec" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838" +dependencies = [ + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "slab", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "tracing" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "tracing-log" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245da694cc7fc4729f3f418b304cb57789f1bed2a78c575407ab8a23f53cb4d3" +dependencies = [ + "ansi_term", + "lazy_static", + "matchers", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" + +[[package]] +name = "uint" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6470ab50f482bde894a037a57064480a246dbfdd5960bd65a44824693f08da5f" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "universal-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "unsigned-varint" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f8d425fafb8cd76bc3f22aace4af471d3156301d7508f2107e98fbeae10bc7f" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasm-bindgen" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" + +[[package]] +name = "web-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "which" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea187a8ef279bc014ec368c27a920da2024d2a711109bfbe3440585d5cf27ad9" +dependencies = [ + "either", + "lazy_static", + "libc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + +[[package]] +name = "zeroize" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdff2024a851a322b08f179173ae2ba620445aef1e838f0c196820eade4ae0c7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] diff --git a/version-meld/discv5/Cargo.toml b/version-meld/discv5/Cargo.toml new file mode 100644 index 0000000..40ea4b5 --- /dev/null +++ b/version-meld/discv5/Cargo.toml @@ -0,0 +1,139 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2018" +name = "discv5" +version = "0.1.0-beta.13" +authors = ["Age Manning "] +exclude = [".gitignore", ".github/*"] +description = "Implementation of the p2p discv5 discovery protocol" +readme = "./README.md" +keywords = ["peer-to-peer", "libp2p", "networking", "discovery", "discv5"] +categories = ["network-programming", "asynchronous"] +license = "MIT" +repository = "https://github.com/sigp/discv5" +[dependencies.aes] +version = "0.7.5" +features = ["ctr"] + +[dependencies.aes-gcm] +version = "0.9.4" + +[dependencies.arrayvec] +version = "0.7.2" + +[dependencies.digest] +version = "0.10.1" + +[dependencies.enr] +version = "0.6.2" +features = ["k256", "ed25519"] + +[dependencies.fnv] +version = "1.0.7" + +[dependencies.futures] +version = "0.3.19" + +[dependencies.hashlink] +version = "0.7.0" + +[dependencies.hex] +version = "0.4.3" + +[dependencies.hkdf] +version = "0.12" + +[dependencies.lazy_static] +version = "1.4.0" + +[dependencies.libp2p-core] +version = "0.30.0" +optional = true + +[dependencies.lru] +version = "0.7.1" + +[dependencies.parking_lot] +version = "0.11.2" + +[dependencies.rand] +version = "0.8.4" +package = "rand" + +[dependencies.rlp] +version = "0.5.1" + +[dependencies.sha2] +version = "0.10" + +[dependencies.smallvec] +version = "1.7.0" + +[dependencies.tokio] +version = "1.15.0" +features = ["net", "sync", "macros", "rt"] + +[dependencies.tokio-stream] +version = "0.1.8" + +[dependencies.tokio-util] +version = "0.6.9" +features = ["time"] + +[dependencies.tracing] +version = "0.1.29" +features = ["log"] + +[dependencies.tracing-subscriber] +version = "0.3.3" +features = ["env-filter"] + +[dependencies.uint] +version = "0.9.1" +default-features = false + +[dependencies.zeroize] +version = "1.4.3" +features = ["zeroize_derive"] +[dev-dependencies.env_logger] +version = "0.9.0" + +[dev-dependencies.hex-literal] +version = "0.3.4" + +[dev-dependencies.quickcheck] +version = "0.9.2" + +[dev-dependencies.rand_07] +version = "0.7" +package = "rand" + +[dev-dependencies.rand_core] +version = "0.6.3" + +[dev-dependencies.rand_xorshift] +version = "0.3.0" + +[dev-dependencies.simple_logger] +version = "1.16.0" + +[dev-dependencies.tokio] +version = "1.15.0" +features = ["full"] + +[dev-dependencies.tokio-util] +version = "0.6.9" +features = ["time"] + +[features] +libp2p = ["libp2p-core"] diff --git a/version-meld/discv5/Cargo.toml.orig b/version-meld/discv5/Cargo.toml.orig new file mode 100644 index 0000000..58e6a31 --- /dev/null +++ b/version-meld/discv5/Cargo.toml.orig @@ -0,0 +1,53 @@ +[package] +name = "discv5" +authors = ["Age Manning "] +edition = "2018" +version = "0.1.0-beta.13" +description = "Implementation of the p2p discv5 discovery protocol" +license = "MIT" +repository = "https://github.com/sigp/discv5" +readme = "./README.md" +keywords = ["peer-to-peer", "libp2p", "networking", "discovery", "discv5"] +categories = ["network-programming", "asynchronous"] +exclude = [".gitignore", ".github/*"] + +[dependencies] +enr = { version = "0.5.1", features = ["k256", "ed25519"] } +tokio = { version = "1.15.0", features = ["net", "sync", "macros", "rt"] } +tokio-stream = "0.1.8" +tokio-util = { version = "0.6.9", features = ["time"] } +libp2p-core = { version = "0.30.0", optional = true } +zeroize = { version = "1.4.3", features = ["zeroize_derive"] } +futures = "0.3.19" +uint = { version = "0.9.1", default-features = false } +rlp = "0.5.1" +sha2 = "0.9.5" +hkdf = "0.11.0" +hex = "0.4.3" +fnv = "1.0.7" +arrayvec = "0.7.2" +digest = "0.10.1" +rand = { version = "0.8.4", package = "rand" } +smallvec = "1.7.0" +parking_lot = "0.11.2" +lazy_static = "1.4.0" +aes = { version = "0.7.5", features = ["ctr"] } +aes-gcm = "0.9.4" +tracing = { version = "0.1.29", features = ["log"] } +tracing-subscriber = { version = "0.3.3", features = ["env-filter"] } +lru = "0.7.1" +hashlink = "0.7.0" + +[dev-dependencies] +rand_07 = { package = "rand", version = "0.7" } +quickcheck = "0.9.2" +env_logger = "0.9.0" +hex-literal = "0.3.4" +simple_logger = "1.16.0" +tokio-util = { version = "0.6.9", features = ["time"] } +tokio = { version = "1.15.0", features = ["full"] } +rand_xorshift = "0.3.0" +rand_core = "0.6.3" + +[features] +libp2p = ["libp2p-core"] diff --git a/version-meld/discv5/LICENSE b/version-meld/discv5/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/version-meld/discv5/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/version-meld/discv5/README.md b/version-meld/discv5/README.md new file mode 100644 index 0000000..21934ee --- /dev/null +++ b/version-meld/discv5/README.md @@ -0,0 +1,75 @@ +discv5 +============ + +[![Build Status]][Build Link] [![Doc Status]][Doc Link] [![Crates +Status]][Crates Link] + +[Build Status]: https://github.com/sigp/discv5/workflows/build/badge.svg?branch=master +[Build Link]: https://github.com/sigp/discv5/actions +[Doc Status]: https://docs.rs/discv5/badge.svg +[Doc Link]: https://docs.rs/discv5 +[Crates Status]: https://img.shields.io/crates/v/discv5.svg +[Crates Link]: https://crates.io/crates/discv5 + +[Documentation at docs.rs](https://docs.rs/discv5) + + +# Overview + +This is a rust implementation of the [Discovery v5](https://github.com/ethereum/devp2p/blob/master/discv5/discv5.md) +peer discovery protocol. + +Discovery v5 is a protocol designed for encrypted peer discovery and topic advertisement. Each peer/node +on the network is identified via it's `ENR` ([Ethereum Node +Record](https://eips.ethereum.org/EIPS/eip-778)), which is essentially a signed key-value store +containing the node's public key and optionally IP address and port. + +Discv5 employs a kademlia-like routing table to store and manage discovered peers and topics. The +protocol allows for external IP discovery in NAT environments through regular PING/PONG's with +discovered nodes. Nodes return the external IP address that they have received and a simple +majority is chosen as our external IP address. If an external IP address is updated, this is +produced as an event to notify the swarm (if one is used for this behaviour). + +For a simple CLI discovery service see [discv5-cli](https://github.com/AgeManning/discv5-cli) + +# Usage + +A simple example of creating this service is as follows: + +```rust + use discv5::{enr, enr::{CombinedKey, NodeId}, TokioExecutor, Discv5, Discv5ConfigBuilder}; + use std::net::SocketAddr; + + // listening address and port + let listen_addr = "0.0.0.0:9000".parse::().unwrap(); + + // construct a local ENR + let enr_key = CombinedKey::generate_secp256k1(); + let enr = enr::EnrBuilder::new("v4").build(&enr_key).unwrap(); + + // build the tokio executor + let mut runtime = tokio::runtime::Builder::new_multi_thread() + .thread_name("Discv5-example") + .enable_all() + .build() + .unwrap(); + + // default configuration + let config = Discv5ConfigBuilder::new().build(); + + // construct the discv5 server + let mut discv5 = Discv5::new(enr, enr_key, config).unwrap(); + + // In order to bootstrap the routing table an external ENR should be added + // This can be done via add_enr. I.e.: + // discv5.add_enr() + + // start the discv5 server + runtime.block_on(discv5.start(listen_addr)); + + // run a find_node query + runtime.block_on(async { + let found_nodes = discv5.find_node(NodeId::random()).await.unwrap(); + println!("Found nodes: {:?}", found_nodes); + }); +``` diff --git a/version-meld/discv5/examples/custom_executor.rs b/version-meld/discv5/examples/custom_executor.rs new file mode 100644 index 0000000..9d48c8b --- /dev/null +++ b/version-meld/discv5/examples/custom_executor.rs @@ -0,0 +1,82 @@ +//! Demonstrates how to run a basic Discovery v5 Service with a custom tokio executor. +//! +//! Discv5 requires a Tokio executor with all features. A custom exuector can be passed via the +//! configuration parameters. If none is passed, it will use the current runtime that build the +//! `Discv5` struct. +//! +//! To run this example simply run: +//! ``` +//! $ cargo run --example custom_executor +//! ``` + +use discv5::{enr, enr::CombinedKey, Discv5, Discv5ConfigBuilder, Discv5Event}; +use std::net::SocketAddr; + +fn main() { + // allows detailed logging with the RUST_LOG env variable + let filter_layer = tracing_subscriber::EnvFilter::try_from_default_env() + .or_else(|_| tracing_subscriber::EnvFilter::try_new("info")) + .unwrap(); + let _ = tracing_subscriber::fmt() + .with_env_filter(filter_layer) + .try_init(); + + // listening address and port + let listen_addr = "0.0.0.0:9000".parse::().unwrap(); + + let enr_key = CombinedKey::generate_secp256k1(); + // construct a local ENR + let enr = enr::EnrBuilder::new("v4").build(&enr_key).unwrap(); + + // build the tokio executor + let runtime = tokio::runtime::Builder::new_multi_thread() + .thread_name("Discv5-example") + .enable_all() + .build() + .unwrap(); + + // default configuration - uses the current executor + let config = Discv5ConfigBuilder::new().build(); + + // construct the discv5 server + let mut discv5 = Discv5::new(enr, enr_key, config).unwrap(); + + // if we know of another peer's ENR, add it known peers + if let Some(base64_enr) = std::env::args().nth(1) { + match base64_enr.parse::>() { + Ok(enr) => { + println!( + "ENR Read. ip: {:?}, udp_port {:?}, tcp_port: {:?}", + enr.ip(), + enr.udp(), + enr.tcp() + ); + if let Err(e) = discv5.add_enr(enr) { + println!("ENR was not added: {}", e); + } + } + Err(e) => panic!("Decoding ENR failed: {}", e), + } + } + + runtime.block_on(async { + // start the discv5 service + discv5.start(listen_addr).await.unwrap(); + println!("Server started"); + + // get an event stream + let mut event_stream = discv5.event_stream().await.unwrap(); + + loop { + match event_stream.recv().await { + Some(Discv5Event::SocketUpdated(addr)) => { + println!("Nodes ENR socket address has been updated to: {:?}", addr); + } + Some(Discv5Event::Discovered(enr)) => { + println!("A peer has been discovered: {}", enr.node_id()); + } + _ => {} + } + } + }) +} diff --git a/version-meld/discv5/examples/find_nodes.rs b/version-meld/discv5/examples/find_nodes.rs new file mode 100644 index 0000000..16430c1 --- /dev/null +++ b/version-meld/discv5/examples/find_nodes.rs @@ -0,0 +1,167 @@ +//! Demonstrates how to run a basic Discovery v5 Service. +//! +//! This example creates a discv5 service which searches for peers every 60 seconds. On +//! creation, the local ENR created for this service is displayed in base64. This can be used to +//! allow other instances to connect and join the network. The service can be stopped by pressing +//! Ctrl-C. +//! +//! To add peers to the network, create multiple instances of this service adding the ENR of a +//! participating node in the command line. The nodes should discover each other over a period of +//! time. (It is probabilistic that nodes to find each other on any given query). +//! +//! A single instance listening on a UDP socket `0.0.0.0:9000` (with an ENR that has an empty IP +//! and UDP port) can be created via: +//! +//! ``` +//! sh cargo run --example find_nodes +//! ``` +//! +//! As the associated ENR has no IP/Port it is not displayed, as it cannot be used to connect to. +//! +//! An ENR IP address (to allow another nodes to dial this service), port and ENR node can also be +//! passed as command line options. Therefore, a second instance, in a new terminal, can be run on +//! port 9001 and connected to another node with a valid ENR: +//! +//! ``` +//! sh cargo run --example find_nodes -- 127.0.0.1 9001 +//! ``` +//! Here `127.0.0.1` represents the external IP address that others may connect to this node on. The +//! `9001` represents the external port and the port to listen on. The `` is the base64 +//! ENR given from executing the first node with an IP and port +//! given in the CLI. +//! `` is a boolean (`true` or `false`) specifying if a new key should be generated. +//! These steps can be repeated to add further nodes to the test network. +//! +//! The parameters are optional. +//! +//! For a simple CLI discovery service see [discv5-cli](https://github.com/AgeManning/discv5-cli) + +use discv5::{ + enr, + enr::{k256, CombinedKey}, + Discv5, Discv5ConfigBuilder, +}; +use std::{ + net::{Ipv4Addr, SocketAddr}, + time::Duration, +}; + +#[tokio::main] +async fn main() { + let filter_layer = tracing_subscriber::EnvFilter::try_from_default_env() + .or_else(|_| tracing_subscriber::EnvFilter::try_new("info")) + .unwrap(); + let _ = tracing_subscriber::fmt() + .with_env_filter(filter_layer) + .try_init(); + + // if there is an address specified use it + let address = std::env::args() + .nth(1) + .map(|addr| addr.parse::().unwrap()); + + let port = { + if let Some(udp_port) = std::env::args().nth(2) { + udp_port.parse().unwrap() + } else { + 9000 + } + }; + + // A fixed key for testing + let raw_key = + hex::decode("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291").unwrap(); + let secret_key = k256::ecdsa::SigningKey::from_bytes(&raw_key).unwrap(); + let mut enr_key = CombinedKey::from(secret_key); + + // use a random key if specified + if let Some(generate_key) = std::env::args().nth(3) { + if generate_key.parse::().unwrap() { + enr_key = CombinedKey::generate_secp256k1(); + } + } + + // construct a local ENR + let enr = { + let mut builder = enr::EnrBuilder::new("v4"); + // if an IP was specified, use it + if let Some(external_address) = address { + builder.ip(external_address.into()); + } + // if a port was specified, use it + if std::env::args().nth(2).is_some() { + builder.udp(port); + } + builder.build(&enr_key).unwrap() + }; + + // if the ENR is useful print it + println!("Node Id: {}", enr.node_id()); + if enr.udp_socket().is_some() { + println!("Base64 ENR: {}", enr.to_base64()); + println!("IP: {}, UDP_PORT:{}", enr.ip().unwrap(), enr.udp().unwrap()); + } else { + println!("ENR is not printed as no IP:PORT was specified"); + } + + // default configuration with packet filtering + // let config = Discv5ConfigBuilder::new().enable_packet_filter().build(); + // default configuration without packet filtering + let config = Discv5ConfigBuilder::new().build(); + + // the address to listen on + let socket_addr = SocketAddr::new("0.0.0.0".parse().expect("valid ip"), port); + + // construct the discv5 server + let mut discv5 = Discv5::new(enr, enr_key, config).unwrap(); + + // if we know of another peer's ENR, add it known peers + if let Some(base64_enr) = std::env::args().nth(4) { + match base64_enr.parse::>() { + Ok(enr) => { + println!( + "ENR Read. ip: {:?}, udp_port {:?}, tcp_port: {:?}", + enr.ip(), + enr.udp(), + enr.tcp() + ); + if let Err(e) = discv5.add_enr(enr) { + println!("ENR was not added: {}", e); + } + } + Err(e) => panic!("Decoding ENR failed: {}", e), + } + } + + // start the discv5 service + discv5.start(socket_addr).await.unwrap(); + + // construct a 30 second interval to search for new peers. + let mut query_interval = tokio::time::interval(Duration::from_secs(60)); + + loop { + tokio::select! { + _ = query_interval.tick() => { + // pick a random node target + let target_random_node_id = enr::NodeId::random(); + // get metrics + let metrics = discv5.metrics(); + let connected_peers = discv5.connected_peers(); + println!("Connected peers: {}, Active sessions: {}, Unsolicited requests/s: {:.2}", connected_peers, metrics.active_sessions, metrics.unsolicited_requests_per_second); + println!("Searching for peers..."); + // execute a FINDNODE query + match discv5.find_node(target_random_node_id).await { + Err(e) => println!("Find Node result failed: {:?}", e), + Ok(v) => { + // found a list of ENR's print their NodeIds + let node_ids = v.iter().map(|enr| enr.node_id()).collect::>(); + println!("Nodes found: {}", node_ids.len()); + for node_id in node_ids { + println!("Node: {}", node_id); + } + } + } + } + } + } +} diff --git a/version-meld/discv5/examples/request_enr.rs b/version-meld/discv5/examples/request_enr.rs new file mode 100644 index 0000000..dc010c8 --- /dev/null +++ b/version-meld/discv5/examples/request_enr.rs @@ -0,0 +1,65 @@ +//! Creates a Discv5 Server and requests an ENR of a node given a multiaddr. +//! +//! This is a simple example of how one may connect to a Discv5 peer using a multiaddr/multiaddr +//! string. +//! +//! To run this example execute the following command from the root directory: +//! ```bash +//! $ cargo run --example request_enr +//! ``` +//! +//! The value should be the string form of a multiaddr including the p2p protocol. +//! Currently only secp256k1 and ed25519 keys are supported. +//! +//! This requires the "libp2p" feature. +#[cfg(feature = "libp2p")] +use discv5::{enr, enr::CombinedKey, Discv5, Discv5Config}; +#[cfg(feature = "libp2p")] +use std::net::SocketAddr; + +#[cfg(not(feature = "libp2p"))] +fn main() {} + +#[cfg(feature = "libp2p")] +#[tokio::main] +async fn main() { + let filter_layer = tracing_subscriber::EnvFilter::try_from_default_env() + .or_else(|_| tracing_subscriber::EnvFilter::try_new("info")) + .unwrap(); + let _ = tracing_subscriber::fmt() + .with_env_filter(filter_layer) + .try_init(); + + // listening address and port + let listen_addr = "0.0.0.0:9000".parse::().unwrap(); + + // generate a new enr key + let enr_key = CombinedKey::generate_secp256k1(); + // construct a local ENR + let enr = enr::EnrBuilder::new("v4").build(&enr_key).unwrap(); + + // default discv5 configuration + let config = Discv5Config::default(); + + let multiaddr = std::env::args() + .nth(1) + .expect("A multiaddr must be supplied"); + + // construct the discv5 server + let mut discv5 = Discv5::new(enr, enr_key, config).unwrap(); + + // start the discv5 service + discv5.start(listen_addr).await.unwrap(); + + // search for the ENR + match discv5.request_enr(multiaddr).await { + Ok(enr) => { + println!("ENR Found:"); + println!("Base64:{}", enr.to_base64()); + println!("{}", enr); + } + Err(e) => { + println!("Error:{:?}", e); + } + } +} diff --git a/version-meld/discv5/examples/simple_server.rs b/version-meld/discv5/examples/simple_server.rs new file mode 100644 index 0000000..b61bec4 --- /dev/null +++ b/version-meld/discv5/examples/simple_server.rs @@ -0,0 +1,109 @@ +//! Demonstrates how to run a basic Discovery v5 Service. +//! +//! This example simply starts a discovery server and listens to events that the server emits. +//! +//! +//! It can be bootstrapped to a DHT by providing an ENR to add to its DHT. +//! +//! To run this example simply run: +//! ``` +//! $ cargo run --example simple_server -- +//! ``` + +use discv5::{enr, enr::CombinedKey, Discv5, Discv5Config, Discv5Event}; +use std::net::{Ipv4Addr, SocketAddr}; + +#[tokio::main] +async fn main() { + // allows detailed logging with the RUST_LOG env variable + let filter_layer = tracing_subscriber::EnvFilter::try_from_default_env() + .or_else(|_| tracing_subscriber::EnvFilter::try_new("info")) + .unwrap(); + let _ = tracing_subscriber::fmt() + .with_env_filter(filter_layer) + .try_init(); + + // if there is an address specified use it + let address = std::env::args() + .nth(1) + .map(|addr| addr.parse::().unwrap()); + + let port = { + if let Some(udp_port) = std::env::args().nth(2) { + udp_port.parse().unwrap() + } else { + 9000 + } + }; + + // listening address and port + let listen_addr = "0.0.0.0:9000".parse::().unwrap(); + + let enr_key = CombinedKey::generate_secp256k1(); + + // construct a local ENR + let enr = { + let mut builder = enr::EnrBuilder::new("v4"); + // if an IP was specified, use it + if let Some(external_address) = address { + builder.ip(external_address.into()); + } + // if a port was specified, use it + if std::env::args().nth(2).is_some() { + builder.udp(port); + } + builder.build(&enr_key).unwrap() + }; + + // if the ENR is useful print it + println!("Node Id: {}", enr.node_id()); + if enr.udp_socket().is_some() { + println!("Base64 ENR: {}", enr.to_base64()); + println!("IP: {}, UDP_PORT:{}", enr.ip().unwrap(), enr.udp().unwrap()); + } else { + println!("ENR is not printed as no IP:PORT was specified"); + } + + // default configuration + let config = Discv5Config::default(); + + // construct the discv5 server + let mut discv5 = Discv5::new(enr, enr_key, config).unwrap(); + + // if we know of another peer's ENR, add it known peers + if let Some(base64_enr) = std::env::args().nth(3) { + match base64_enr.parse::>() { + Ok(enr) => { + println!( + "ENR Read. ip: {:?}, udp_port {:?}, tcp_port: {:?}", + enr.ip(), + enr.udp(), + enr.tcp() + ); + if let Err(e) = discv5.add_enr(enr) { + println!("ENR was not added: {}", e); + } + } + Err(e) => panic!("Decoding ENR failed: {}", e), + } + } + + // start the discv5 service + discv5.start(listen_addr).await.unwrap(); + println!("Server started"); + + // get an event stream + let mut event_stream = discv5.event_stream().await.unwrap(); + + loop { + match event_stream.recv().await { + Some(Discv5Event::SocketUpdated(addr)) => { + println!("Nodes ENR socket address has been updated to: {:?}", addr); + } + Some(Discv5Event::Discovered(enr)) => { + println!("A peer has been discovered: {}", enr.node_id()); + } + _ => {} + } + } +} diff --git a/version-meld/discv5/src/config.rs b/version-meld/discv5/src/config.rs new file mode 100644 index 0000000..264e885 --- /dev/null +++ b/version-meld/discv5/src/config.rs @@ -0,0 +1,339 @@ +use crate::{ + kbucket::MAX_NODES_PER_BUCKET, Enr, Executor, PermitBanList, RateLimiter, RateLimiterBuilder, +}; +///! A set of configuration parameters to tune the discovery protocol. +use std::time::Duration; + +/// Configuration parameters that define the performance of the gossipsub network. +#[derive(Clone)] +pub struct Discv5Config { + /// Whether to enable the incoming packet filter. Default: false. + pub enable_packet_filter: bool, + + /// The request timeout for each UDP request. Default: 2 seconds. + pub request_timeout: Duration, + + /// The interval over which votes are remembered when determining our external IP. A lower + /// interval will respond faster to IP changes. Default is 30 seconds. + pub vote_duration: Duration, + + /// The timeout after which a `QueryPeer` in an ongoing query is marked unresponsive. + /// Unresponsive peers don't count towards the parallelism limits for a query. + /// Hence, we may potentially end up making more requests to good peers. Default: 2 seconds. + pub query_peer_timeout: Duration, + + /// The timeout for an entire query. Any peers discovered for this query are returned. Default 60 seconds. + pub query_timeout: Duration, + + /// The number of retries for each UDP request. Default: 1. + pub request_retries: u8, + + /// The session timeout for each node. Default: 1 day. + pub session_timeout: Duration, + + /// The maximum number of established sessions to maintain. Default: 1000. + pub session_cache_capacity: usize, + + /// Updates the local ENR IP and port based on PONG responses from peers. Default: true. + pub enr_update: bool, + + /// The maximum number of nodes we return to a find nodes request. The default is 16. + pub max_nodes_response: usize, + + /// The minimum number of peer's who agree on an external IP port before updating the + /// local ENR. Default: 10. + pub enr_peer_update_min: usize, + + /// The number of peers to request in parallel in a single query. Default: 3. + pub query_parallelism: usize, + + /// Limits the number of IP addresses from the same + /// /24 subnet in the kbuckets table. This is to mitigate eclipse attacks. Default: false. + pub ip_limit: bool, + + /// Sets a maximum limit to the number of incoming nodes (nodes that have dialed us) to exist per-bucket. This cannot be larger + /// than the bucket size (16). By default this is disabled (set to the maximum bucket size, 16). + pub incoming_bucket_limit: usize, + + /// A filter used to decide whether to insert nodes into our local routing table. Nodes can be + /// excluded if they do not pass this filter. The default is to accept all nodes. + pub table_filter: fn(&Enr) -> bool, + + /// The time between pings to ensure connectivity amongst connected nodes. Default: 300 + /// seconds. + pub ping_interval: Duration, + + /// Reports all discovered ENR's when traversing the DHT to the event stream. Default true. + pub report_discovered_peers: bool, + + /// A set of configuration parameters for setting inbound request rate limits. See + /// [`RateLimiterBuilder`] for options. This is only functional if the packet filter is + /// enabled via the `enable_packet_filter` option. See the `Default` implementation for + /// default values. If set to None, inbound requests are not filtered. + pub filter_rate_limiter: Option, + + /// The maximum number of node-ids allowed per IP address before the IP address gets banned. + /// Having this set to None, disables this feature. Default value is 10. This is only + /// applicable if the `enable_packet_filter` option is set. + pub filter_max_nodes_per_ip: Option, + + /// The maximum number of nodes that can be banned by a single IP before that IP gets banned. + /// The default is 5. This is only + /// applicable if the `enable_packet_filter` option is set. + pub filter_max_bans_per_ip: Option, + + /// A set of lists that permit or ban IP's or NodeIds from the server. See + /// `crate::PermitBanList`. + pub permit_ban_list: PermitBanList, + + /// Set the default duration for which nodes are banned for. This timeouts are checked every 5 minutes, + /// so the precision will be to the nearest 5 minutes. If set to `None`, bans from the filter + /// will last indefinitely. Default is 1 hour. + pub ban_duration: Option, + + /// A custom executor which can spawn the discv5 tasks. This must be a tokio runtime, with + /// timing support. By default, the executor that created the discv5 struct will be used. + pub executor: Option>, +} + +impl Default for Discv5Config { + fn default() -> Self { + // This is only applicable if enable_packet_filter is set. + let filter_rate_limiter = Some( + RateLimiterBuilder::new() + .total_n_every(10, Duration::from_secs(1)) // Allow bursts, average 10 per second + .node_n_every(8, Duration::from_secs(1)) // Allow bursts, average 8 per second + .ip_n_every(9, Duration::from_secs(1)) // Allow bursts, average 9 per second + .build() + .expect("The total rate limit has been specified"), + ); + + Self { + enable_packet_filter: false, + request_timeout: Duration::from_secs(1), + vote_duration: Duration::from_secs(30), + query_peer_timeout: Duration::from_secs(2), + query_timeout: Duration::from_secs(60), + request_retries: 1, + session_timeout: Duration::from_secs(86400), + session_cache_capacity: 1000, + enr_update: true, + max_nodes_response: 16, + enr_peer_update_min: 10, + query_parallelism: 3, + ip_limit: false, + incoming_bucket_limit: MAX_NODES_PER_BUCKET, + table_filter: |_| true, + ping_interval: Duration::from_secs(300), + report_discovered_peers: true, + filter_rate_limiter, + filter_max_nodes_per_ip: Some(10), + filter_max_bans_per_ip: Some(5), + permit_ban_list: PermitBanList::default(), + ban_duration: Some(Duration::from_secs(3600)), // 1 hour + executor: None, + } + } +} + +#[derive(Debug, Default)] +pub struct Discv5ConfigBuilder { + config: Discv5Config, +} + +impl Discv5ConfigBuilder { + // set default values + pub fn new() -> Self { + Discv5ConfigBuilder::default() + } + + /// Whether to enable the incoming packet filter. + pub fn enable_packet_filter(&mut self) -> &mut Self { + self.config.enable_packet_filter = true; + self + } + + /// The request timeout for each UDP request. + pub fn request_timeout(&mut self, timeout: Duration) -> &mut Self { + self.config.request_timeout = timeout; + self + } + + /// The interval over which votes are remembered when determining our external IP. A lower + /// interval will respond faster to IP changes. Default is 30 seconds. + pub fn vote_duration(&mut self, vote_duration: Duration) -> &mut Self { + self.config.vote_duration = vote_duration; + self + } + + /// The timeout after which a `QueryPeer` in an ongoing query is marked unresponsive. + /// Unresponsive peers don't count towards the parallelism limits for a query. + /// Hence, we may potentially end up making more requests to good peers. + pub fn query_peer_timeout(&mut self, timeout: Duration) -> &mut Self { + self.config.query_peer_timeout = timeout; + self + } + + /// The timeout for an entire query. Any peers discovered before this timeout are returned. + pub fn query_timeout(&mut self, timeout: Duration) -> &mut Self { + self.config.query_timeout = timeout; + self + } + + /// The number of retries for each UDP request. + pub fn request_retries(&mut self, retries: u8) -> &mut Self { + self.config.request_retries = retries; + self + } + + /// The session timeout for each node. + pub fn session_timeout(&mut self, timeout: Duration) -> &mut Self { + self.config.session_timeout = timeout; + self + } + + /// The maximum number of established sessions to maintain. + pub fn session_cache_capacity(&mut self, capacity: usize) -> &mut Self { + self.config.session_cache_capacity = capacity; + self + } + + /// Disables the auto-update of the local ENR IP and port based on PONG responses from peers. + pub fn disable_enr_update(&mut self) -> &mut Self { + self.config.enr_update = false; + self + } + + /// The maximum number of nodes we response to a find nodes request. + pub fn max_nodes_response(&mut self, max: usize) -> &mut Self { + self.config.max_nodes_response = max; + self + } + + /// The minimum number of peer's who agree on an external IP port before updating the + /// local ENR. + pub fn enr_peer_update_min(&mut self, min: usize) -> &mut Self { + if min < 2 { + panic!("Setting enr_peer_update_min to a value less than 2 will cause issues with discovery with peers behind NAT"); + } + self.config.enr_peer_update_min = min; + self + } + + /// The number of peers to request in parallel in a single query. + pub fn query_parallelism(&mut self, parallelism: usize) -> &mut Self { + self.config.query_parallelism = parallelism; + self + } + + /// Limits the number of IP addresses from the same + /// /24 subnet in the kbuckets table. This is to mitigate eclipse attacks. + pub fn ip_limit(&mut self) -> &mut Self { + self.config.ip_limit = true; + self + } + + /// Sets a maximum limit to the number of incoming nodes (nodes that have dialed us) to exist per-bucket. This cannot be larger + /// than the bucket size (16). By default, half of every bucket (8 positions) is the largest number of nodes that we accept that dial us. + pub fn incoming_bucket_limit(&mut self, limit: usize) -> &mut Self { + self.config.incoming_bucket_limit = limit; + self + } + + /// A filter used to decide whether to insert nodes into our local routing table. Nodes can be + /// excluded if they do not pass this filter. + pub fn table_filter(&mut self, filter: fn(&Enr) -> bool) -> &mut Self { + self.config.table_filter = filter; + self + } + + /// The time between pings to ensure connectivity amongst connected nodes. + pub fn ping_interval(&mut self, interval: Duration) -> &mut Self { + self.config.ping_interval = interval; + self + } + + /// Disables reporting of discovered peers through the event stream. + pub fn disable_report_discovered_peers(&mut self) -> &mut Self { + self.config.report_discovered_peers = false; + self + } + + /// A rate limiter for limiting inbound requests. + pub fn filter_rate_limiter(&mut self, rate_limiter: Option) -> &mut Self { + self.config.filter_rate_limiter = rate_limiter; + self + } + + /// If the filter is enabled, sets the maximum number of nodes per IP before banning + /// the IP. + pub fn filter_max_nodes_per_ip(&mut self, max_nodes_per_ip: Option) -> &mut Self { + self.config.filter_max_nodes_per_ip = max_nodes_per_ip; + self + } + + /// The maximum number of times nodes from a single IP can be banned, before the IP itself + /// gets banned. + pub fn filter_max_bans_per_ip(&mut self, max_bans_per_ip: Option) -> &mut Self { + self.config.filter_max_bans_per_ip = max_bans_per_ip; + self + } + + /// A set of lists that permit or ban IP's or NodeIds from the server. See + /// `crate::PermitBanList`. + pub fn permit_ban_list(&mut self, list: PermitBanList) -> &mut Self { + self.config.permit_ban_list = list; + self + } + + /// Set the default duration for which nodes are banned for. This timeouts are checked every 5 minutes, + /// so the precision will be to the nearest 5 minutes. If set to `None`, bans from the filter + /// will last indefinitely. Default is 1 hour. + pub fn ban_duration(&mut self, ban_duration: Option) -> &mut Self { + self.config.ban_duration = ban_duration; + self + } + + /// A custom executor which can spawn the discv5 tasks. This must be a tokio runtime, with + /// timing support. + pub fn executor(&mut self, executor: Box) -> &mut Self { + self.config.executor = Some(executor); + self + } + + pub fn build(&mut self) -> Discv5Config { + // If an executor is not provided, assume a current tokio runtime is running. + if self.config.executor.is_none() { + self.config.executor = Some(Box::new(crate::executor::TokioExecutor::default())); + }; + + assert!(self.config.incoming_bucket_limit <= MAX_NODES_PER_BUCKET); + + self.config.clone() + } +} + +impl std::fmt::Debug for Discv5Config { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut builder = f.debug_struct("Discv5Config"); + let _ = builder.field("filter_enabled", &self.enable_packet_filter); + let _ = builder.field("request_timeout", &self.request_timeout); + let _ = builder.field("vote_duration", &self.vote_duration); + let _ = builder.field("query_timeout", &self.query_timeout); + let _ = builder.field("query_peer_timeout", &self.query_peer_timeout); + let _ = builder.field("request_retries", &self.request_retries); + let _ = builder.field("session_timeout", &self.session_timeout); + let _ = builder.field("session_cache_capacity", &self.session_cache_capacity); + let _ = builder.field("enr_update", &self.enr_update); + let _ = builder.field("query_parallelism", &self.query_parallelism); + let _ = builder.field("report_discovered_peers", &self.report_discovered_peers); + let _ = builder.field("ip_limit", &self.ip_limit); + let _ = builder.field("filter_max_nodes_per_ip", &self.filter_max_nodes_per_ip); + let _ = builder.field("filter_max_bans_per_ip", &self.filter_max_bans_per_ip); + let _ = builder.field("ip_limit", &self.ip_limit); + let _ = builder.field("incoming_bucket_limit", &self.incoming_bucket_limit); + let _ = builder.field("ping_interval", &self.ping_interval); + let _ = builder.field("ban_duration", &self.ban_duration); + builder.finish() + } +} diff --git a/version-meld/discv5/src/discv5.rs b/version-meld/discv5/src/discv5.rs new file mode 100644 index 0000000..0605219 --- /dev/null +++ b/version-meld/discv5/src/discv5.rs @@ -0,0 +1,592 @@ +//! The Discovery v5 protocol. See the module level docs for further details. +//! +//! This provides the main struct for running and interfacing with a discovery v5 server. +//! +//! A [`Discv5`] struct needs to be created either with an [`crate::executor::Executor`] specified in the +//! [`Discv5Config`] via the [`crate::Discv5ConfigBuilder`] or in the presence of a tokio runtime that has +//! timing and io enabled. +//! +//! Once a [`Discv5`] struct has been created the service is started by running the [`Discv5::start`] +//! functions with a UDP socket. This will start a discv5 server in the background listening on the +//! specified UDP socket. +//! +//! The server can be shutdown using the [`Discv5::shutdown`] function. + +use crate::{ + error::{Discv5Error, QueryError, RequestError}, + kbucket::{ + self, ConnectionDirection, ConnectionState, FailureReason, InsertResult, KBucketsTable, + NodeStatus, UpdateResult, + }, + node_info::NodeContact, + service::{QueryKind, Service, ServiceRequest, TalkRequest}, + Discv5Config, Enr, +}; +use enr::{CombinedKey, EnrError, EnrKey, NodeId}; +use parking_lot::RwLock; +use std::{ + future::Future, + net::SocketAddr, + sync::Arc, + time::{Duration, Instant}, +}; +use tokio::sync::{mpsc, oneshot}; +use tracing::{debug, warn}; + +#[cfg(feature = "libp2p")] +use {libp2p_core::Multiaddr, std::convert::TryFrom}; + +// Create lazy static variable for the global permit/ban list +use crate::metrics::{Metrics, METRICS}; +lazy_static! { + pub static ref PERMIT_BAN_LIST: RwLock = + RwLock::new(crate::PermitBanList::default()); +} + +mod test; + +/// Events that can be produced by the `Discv5` event stream. +#[derive(Debug)] +pub enum Discv5Event { + /// A node has been discovered from a FINDNODES request. + /// + /// The ENR of the node is returned. Various properties can be derived from the ENR. + /// This happen spontaneously through queries as nodes return ENR's. These ENR's are not + /// guaranteed to be live or contactable. + Discovered(Enr), + /// A new ENR was added to the routing table. + EnrAdded { enr: Enr, replaced: Option }, + /// A new node has been added to the routing table. + NodeInserted { + node_id: NodeId, + replaced: Option, + }, + /// Our local ENR IP address has been updated. + SocketUpdated(SocketAddr), + /// A node has initiated a talk request. + TalkRequest(TalkRequest), +} + +/// The main Discv5 Service struct. This provides the user-level API for performing queries and +/// interacting with the underlying service. +pub struct Discv5 { + config: Discv5Config, + /// The channel to make requests from the main service. + service_channel: Option>, + /// The exit channel to shutdown the underlying service. + service_exit: Option>, + /// The routing table of the discv5 service. + kbuckets: Arc>>, + /// The local ENR of the server. + local_enr: Arc>, + /// The key associated with the local ENR, required for updating the local ENR. + enr_key: Arc>, +} + +impl Discv5 { + pub fn new( + local_enr: Enr, + enr_key: CombinedKey, + mut config: Discv5Config, + ) -> Result { + // ensure the keypair matches the one that signed the enr. + if local_enr.public_key() != enr_key.public() { + return Err("Provided keypair does not match the provided ENR"); + } + + // If an executor is not provided, assume a current tokio runtime is running. If not panic. + if config.executor.is_none() { + config.executor = Some(Box::new(crate::executor::TokioExecutor::default())); + }; + + // NOTE: Currently we don't expose custom filter support in the configuration. Users can + // optionally use the IP filter via the ip_limit configuration parameter. In the future, we + // may expose this functionality to the users if there is demand for it. + let (table_filter, bucket_filter) = if config.ip_limit { + ( + Some(Box::new(kbucket::IpTableFilter) as Box>), + Some(Box::new(kbucket::IpBucketFilter) as Box>), + ) + } else { + (None, None) + }; + + let local_enr = Arc::new(RwLock::new(local_enr)); + let enr_key = Arc::new(RwLock::new(enr_key)); + let kbuckets = Arc::new(RwLock::new(KBucketsTable::new( + local_enr.read().node_id().into(), + Duration::from_secs(60), + config.incoming_bucket_limit, + table_filter, + bucket_filter, + ))); + + // Update the PermitBan list based on initial configuration + *PERMIT_BAN_LIST.write() = config.permit_ban_list.clone(); + + Ok(Discv5 { + config, + service_channel: None, + service_exit: None, + kbuckets, + local_enr, + enr_key, + }) + } + + /// Starts the required tasks and begins listening on a given UDP SocketAddr. + pub async fn start(&mut self, listen_socket: SocketAddr) -> Result<(), Discv5Error> { + if self.service_channel.is_some() { + warn!("Service is already started"); + return Err(Discv5Error::ServiceAlreadyStarted); + } + + // create the main service + let (service_exit, service_channel) = Service::spawn( + self.local_enr.clone(), + self.enr_key.clone(), + self.kbuckets.clone(), + self.config.clone(), + listen_socket, + ) + .await?; + self.service_exit = Some(service_exit); + self.service_channel = Some(service_channel); + Ok(()) + } + + /// Terminates the service. + pub fn shutdown(&mut self) { + if let Some(exit) = self.service_exit.take() { + if exit.send(()).is_err() { + debug!("Discv5 service already shutdown"); + } + self.service_channel = None; + } else { + debug!("Service is already shutdown"); + } + } + + /// Adds a known ENR of a peer participating in Service to the + /// routing table. + /// + /// This allows pre-populating the Kademlia routing table with known + /// addresses, so that they can be used immediately in following DHT + /// operations involving one of these peers, without having to dial + /// them upfront. + pub fn add_enr(&self, enr: Enr) -> Result<(), &'static str> { + // only add ENR's that have a valid udp socket. + if enr.udp_socket().is_none() { + warn!("ENR attempted to be added without a UDP socket has been ignored"); + return Err("ENR has no UDP socket to connect to"); + } + + if !(self.config.table_filter)(&enr) { + warn!("ENR attempted to be added which is banned by the configuration table filter."); + return Err("ENR banned by table filter"); + } + + let key = kbucket::Key::from(enr.node_id()); + + match self.kbuckets.write().insert_or_update( + &key, + enr, + NodeStatus { + state: ConnectionState::Disconnected, + direction: ConnectionDirection::Incoming, + }, + ) { + InsertResult::Inserted + | InsertResult::Pending { .. } + | InsertResult::StatusUpdated { .. } + | InsertResult::ValueUpdated + | InsertResult::Updated { .. } + | InsertResult::UpdatedPending => Ok(()), + InsertResult::Failed(FailureReason::BucketFull) => Err("Table full"), + InsertResult::Failed(FailureReason::BucketFilter) => Err("Failed bucket filter"), + InsertResult::Failed(FailureReason::TableFilter) => Err("Failed table filter"), + InsertResult::Failed(FailureReason::InvalidSelfUpdate) => Err("Invalid self update"), + InsertResult::Failed(_) => Err("Failed to insert ENR"), + } + } + + /// Removes a `node_id` from the routing table. + /// + /// This allows applications, for whatever reason, to remove nodes from the local routing + /// table. Returns `true` if the node was in the table and `false` otherwise. + pub fn remove_node(&self, node_id: &NodeId) -> bool { + let key = &kbucket::Key::from(*node_id); + self.kbuckets.write().remove(key) + } + + /// Returns a vector of closest nodes by the given distances. + pub fn nodes_by_distance(&self, mut distances: Vec) -> Vec { + let mut nodes_to_send = Vec::new(); + distances.sort_unstable(); + distances.dedup(); + + if let Some(0) = distances.first() { + // if the distance is 0 send our local ENR + nodes_to_send.push(self.local_enr.read().clone()); + distances.remove(0); + } + + if !distances.is_empty() { + let mut kbuckets = self.kbuckets.write(); + for node in kbuckets + .nodes_by_distances(distances.as_slice(), self.config.max_nodes_response) + .into_iter() + .map(|entry| entry.node.value.clone()) + { + nodes_to_send.push(node); + } + } + nodes_to_send + } + + /// Mark a node in the routing table as `Disconnnected`. + /// + /// A `Disconnected` node will be present in the routing table and will be only + /// used if there are no other `Connected` peers in the bucket. + /// Returns `true` if node was in table and `false` otherwise. + pub fn disconnect_node(&self, node_id: &NodeId) -> bool { + let key = &kbucket::Key::from(*node_id); + !matches!( + self.kbuckets + .write() + .update_node_status(key, ConnectionState::Disconnected, None), + UpdateResult::Failed(_) + ) + } + + /// Returns the number of connected peers that exist in the routing table. + pub fn connected_peers(&self) -> usize { + self.kbuckets + .write() + .iter() + .filter(|entry| entry.status.is_connected()) + .count() + } + + /// Gets the metrics associated with the Server + pub fn metrics(&self) -> Metrics { + Metrics::from(&METRICS) + } + + /// Exposes the raw reference to the underlying internal metrics. + pub fn raw_metrics() -> &'static METRICS { + &METRICS + } + + /// Returns the local ENR of the node. + pub fn local_enr(&self) -> Enr { + self.local_enr.read().clone() + } + + /// Returns an ENR if one is known for the given NodeId. + pub fn find_enr(&self, node_id: &NodeId) -> Option { + // check if we know this node id in our routing table + let key = kbucket::Key::from(*node_id); + if let kbucket::Entry::Present(entry, _) = self.kbuckets.write().entry(&key) { + return Some(entry.value().clone()); + } + None + } + + /// Bans a node from the server. This will remove the node from the routing table if it exists + /// and block all incoming packets from the node until the timeout specified. Setting the + /// timeout to `None` creates a permanent ban. + pub fn ban_node(&self, node_id: &NodeId, duration_of_ban: Option) { + let time_to_unban = duration_of_ban.map(|v| Instant::now() + v); + self.remove_node(node_id); + PERMIT_BAN_LIST + .write() + .ban_nodes + .insert(*node_id, time_to_unban); + } + + /// Removes a banned node from the banned list. + pub fn ban_node_remove(&self, node_id: &NodeId) { + PERMIT_BAN_LIST.write().ban_nodes.remove(node_id); + } + + /// Permits a node, allowing the node to bypass the packet filter. + pub fn permit_node(&self, node_id: &NodeId) { + PERMIT_BAN_LIST.write().permit_nodes.insert(*node_id); + } + + /// Removes a node from the permit list. + pub fn permit_node_remove(&self, node_id: &NodeId) { + PERMIT_BAN_LIST.write().permit_nodes.remove(node_id); + } + + /// Bans an IP from the server. This will block all incoming packets from the IP. + pub fn ban_ip(&self, ip: std::net::IpAddr, duration_of_ban: Option) { + let time_to_unban = duration_of_ban.map(|v| Instant::now() + v); + PERMIT_BAN_LIST.write().ban_ips.insert(ip, time_to_unban); + } + + /// Removes a banned IP from the banned list. + pub fn ban_ip_remove(&self, ip: &std::net::IpAddr) { + PERMIT_BAN_LIST.write().ban_ips.remove(ip); + } + + /// Permits an IP, allowing the all packets from the IP to bypass the packet filter. + pub fn permit_ip(&self, ip: std::net::IpAddr) { + PERMIT_BAN_LIST.write().permit_ips.insert(ip); + } + + /// Removes an IP from the permit list. + pub fn permit_ip_remove(&self, ip: &std::net::IpAddr) { + PERMIT_BAN_LIST.write().permit_ips.remove(ip); + } + + /// Updates the local ENR TCP/UDP socket. + pub fn update_local_enr_socket(&self, socket_addr: SocketAddr, is_tcp: bool) -> bool { + let local_socket = self.local_enr.read().udp_socket(); + if local_socket != Some(socket_addr) { + if is_tcp { + self.local_enr + .write() + .set_tcp_socket(socket_addr, &self.enr_key.read()) + .is_ok() + } else { + self.local_enr + .write() + .set_udp_socket(socket_addr, &self.enr_key.read()) + .is_ok() + } + } else { + false + } + } + + /// Allows application layer to insert an arbitrary field into the local ENR. + pub fn enr_insert(&self, key: &str, value: &[u8]) -> Result>, EnrError> { + self.local_enr + .write() + .insert(key, value, &self.enr_key.read()) + .map(|v| v.map(|v| v.to_vec())) + } + + /// Returns an iterator over all ENR node IDs of nodes currently contained in the routing table. + pub fn table_entries_id(&self) -> Vec { + self.kbuckets + .write() + .iter() + .map(|entry| *entry.node.key.preimage()) + .collect() + } + + /// Returns an iterator over all the ENR's of nodes currently contained in the routing table. + pub fn table_entries_enr(&self) -> Vec { + self.kbuckets + .write() + .iter() + .map(|entry| entry.node.value.clone()) + .collect() + } + + /// Returns an iterator over all the entries in the routing table. + pub fn table_entries(&self) -> Vec<(NodeId, Enr, NodeStatus)> { + self.kbuckets + .write() + .iter() + .map(|entry| { + ( + *entry.node.key.preimage(), + entry.node.value.clone(), + entry.status, + ) + }) + .collect() + } + + /// Requests the ENR of a node corresponding to multiaddr or multi-addr string. + /// + /// Only `ed25519` and `secp256k1` key types are currently supported. + /// + /// Note: The async syntax is forgone here in order to create `'static` futures, where the + /// underlying sending channel is cloned. + #[cfg(feature = "libp2p")] + #[cfg_attr(docsrs, doc(cfg(feature = "libp2p")))] + pub fn request_enr( + &self, + multiaddr: impl std::convert::TryInto + 'static, + ) -> impl Future> + 'static { + let channel = self.clone_channel(); + + async move { + let channel = channel.map_err(|_| RequestError::ServiceNotStarted)?; + // Sanitize the multiaddr + + // The multiaddr must support the udp protocol and be of an appropriate key type. + // The conversion logic is contained in the `TryFrom` implementation of a + // `NodeContact`. + let multiaddr: Multiaddr = multiaddr.try_into().map_err(|_| { + RequestError::InvalidMultiaddr("Could not convert to multiaddr".into()) + })?; + let node_contact: NodeContact = NodeContact::try_from(multiaddr) + .map_err(|e| RequestError::InvalidMultiaddr(e.into()))?; + + let (callback_send, callback_recv) = oneshot::channel(); + + let event = ServiceRequest::FindEnr(node_contact, callback_send); + channel + .send(event) + .await + .map_err(|_| RequestError::ChannelFailed("Service channel closed".into()))?; + callback_recv + .await + .map_err(|e| RequestError::ChannelFailed(e.to_string()))? + } + } + + /// Request a TALK message from a node, identified via the ENR. + pub fn talk_req( + &self, + enr: Enr, + protocol: Vec, + request: Vec, + ) -> impl Future, RequestError>> + 'static { + // convert the ENR to a node_contact. + let node_contact = NodeContact::from(enr); + + // the service will verify if this node is contactable, we just send it and + // await a response. + let (callback_send, callback_recv) = oneshot::channel(); + let channel = self.clone_channel(); + + async move { + let channel = channel.map_err(|_| RequestError::ServiceNotStarted)?; + + let event = ServiceRequest::Talk(node_contact, protocol, request, callback_send); + + // send the request + channel + .send(event) + .await + .map_err(|_| RequestError::ChannelFailed("Service channel closed".into()))?; + // await the response + callback_recv + .await + .map_err(|e| RequestError::ChannelFailed(e.to_string()))? + } + } + + /// Runs an iterative `FIND_NODE` request. + /// + /// This will return peers containing contactable nodes of the DHT closest to the + /// requested `NodeId`. + /// + /// Note: The async syntax is forgone here in order to create `'static` futures, where the + /// underlying sending channel is cloned. + pub fn find_node( + &self, + target_node: NodeId, + ) -> impl Future, QueryError>> + 'static { + let channel = self.clone_channel(); + + async move { + let channel = channel.map_err(|_| QueryError::ServiceNotStarted)?; + let (callback_send, callback_recv) = oneshot::channel(); + + let query_kind = QueryKind::FindNode { target_node }; + + let event = ServiceRequest::StartQuery(query_kind, callback_send); + channel + .send(event) + .await + .map_err(|_| QueryError::ChannelFailed("Service channel closed".into()))?; + + callback_recv + .await + .map_err(|e| QueryError::ChannelFailed(e.to_string())) + } + } + + /// Starts a `FIND_NODE` request. + /// + /// This will return less than or equal to `num_nodes` ENRs which satisfy the + /// `predicate`. + /// + /// The predicate is a boxed function that takes an ENR reference and returns a boolean + /// indicating if the record is applicable to the query or not. + /// + /// Note: The async syntax is forgone here in order to create `'static` futures, where the + /// underlying sending channel is cloned. + /// + /// ### Example + /// ```ignore + /// let predicate = Box::new(|enr: &Enr| enr.ip().is_some()); + /// let target = NodeId::random(); + /// let result = discv5.find_node_predicate(target, predicate, 5).await; + /// ``` + pub fn find_node_predicate( + &self, + target_node: NodeId, + predicate: Box bool + Send>, + target_peer_no: usize, + ) -> impl Future, QueryError>> + 'static { + let channel = self.clone_channel(); + + async move { + let channel = channel.map_err(|_| QueryError::ServiceNotStarted)?; + let (callback_send, callback_recv) = oneshot::channel(); + + let query_kind = QueryKind::Predicate { + target_node, + predicate, + target_peer_no, + }; + + let event = ServiceRequest::StartQuery(query_kind, callback_send); + channel + .send(event) + .await + .map_err(|_| QueryError::ChannelFailed("Service channel closed".into()))?; + + callback_recv + .await + .map_err(|e| QueryError::ChannelFailed(e.to_string())) + } + } + + /// Creates an event stream channel which can be polled to receive Discv5 events. + pub fn event_stream( + &self, + ) -> impl Future, Discv5Error>> + 'static { + let channel = self.clone_channel(); + + async move { + let channel = channel?; + + let (callback_send, callback_recv) = oneshot::channel(); + + let event = ServiceRequest::RequestEventStream(callback_send); + channel + .send(event) + .await + .map_err(|_| Discv5Error::ServiceChannelClosed)?; + + callback_recv + .await + .map_err(|_| Discv5Error::ServiceChannelClosed) + } + } + + /// Internal helper function to send events to the Service. + fn clone_channel(&self) -> Result, Discv5Error> { + if let Some(channel) = self.service_channel.as_ref() { + Ok(channel.clone()) + } else { + Err(Discv5Error::ServiceNotStarted) + } + } +} + +impl Drop for Discv5 { + fn drop(&mut self) { + self.shutdown(); + } +} diff --git a/version-meld/discv5/src/discv5/test.rs b/version-meld/discv5/src/discv5/test.rs new file mode 100644 index 0000000..55e13e5 --- /dev/null +++ b/version-meld/discv5/src/discv5/test.rs @@ -0,0 +1,482 @@ +#![cfg(test)] + +use crate::{kbucket, Discv5, *}; +use enr::{k256, CombinedKey, Enr, EnrBuilder, EnrKey, NodeId}; +use rand_core::{RngCore, SeedableRng}; +use std::{ + collections::HashMap, + net::{IpAddr, Ipv4Addr}, +}; + +fn init() { + let _ = tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .try_init(); +} + +fn update_enr(discv5: &mut Discv5, key: &str, value: &[u8]) -> bool { + discv5.enr_insert(key, value).is_ok() +} + +async fn build_nodes(n: usize, base_port: u16) -> Vec { + let mut nodes = Vec::new(); + let ip: IpAddr = "127.0.0.1".parse().unwrap(); + + for port in base_port..base_port + n as u16 { + let enr_key = CombinedKey::generate_secp256k1(); + let config = Discv5Config::default(); + + let enr = EnrBuilder::new("v4") + .ip(ip) + .udp(port) + .build(&enr_key) + .unwrap(); + // transport for building a swarm + let socket_addr = enr.udp_socket().unwrap(); + let mut discv5 = Discv5::new(enr, enr_key, config).unwrap(); + discv5.start(socket_addr).await.unwrap(); + nodes.push(discv5); + } + nodes +} + +/// Build `n` swarms using passed keypairs. +async fn build_nodes_from_keypairs(keys: Vec, base_port: u16) -> Vec { + let mut nodes = Vec::new(); + let ip: IpAddr = "127.0.0.1".parse().unwrap(); + + for (i, enr_key) in keys.into_iter().enumerate() { + let port = base_port + i as u16; + + let config = Discv5ConfigBuilder::new().build(); + let enr = EnrBuilder::new("v4") + .ip(ip) + .udp(port) + .build(&enr_key) + .unwrap(); + + let socket_addr = enr.udp_socket().unwrap(); + let mut discv5 = Discv5::new(enr, enr_key, config).unwrap(); + discv5.start(socket_addr).await.unwrap(); + nodes.push(discv5); + } + nodes +} + +/// Generate `n` deterministic keypairs from a given seed. +fn generate_deterministic_keypair(n: usize, seed: u64) -> Vec { + let mut keypairs = Vec::new(); + for i in 0..n { + let sk = { + let rng = &mut rand_xorshift::XorShiftRng::seed_from_u64(seed + i as u64); + let mut b = [0; 32]; + loop { + // until a value is given within the curve order + rng.fill_bytes(&mut b); + if let Ok(k) = k256::ecdsa::SigningKey::from_bytes(&b) { + break k; + } + } + }; + let kp = CombinedKey::from(sk); + keypairs.push(kp); + } + keypairs +} + +fn get_distance(node1: NodeId, node2: NodeId) -> Option { + let node1: kbucket::Key = node1.into(); + node1.log2_distance(&node2.into()) +} + +// Simple searching function to find seeds that give node ids for a range of testing and different +// topologies +#[allow(dead_code)] +fn find_seed_same_bucket() { + let mut seed = 1; + 'main: loop { + if seed % 1000 == 0 { + println!("Seed: {}", seed); + } + + let keys = generate_deterministic_keypair(11, seed); + + let node_ids = keys + .into_iter() + .map(|k| NodeId::from(k.public())) + .collect::>(); + + let local = node_ids[0]; + + for &id in node_ids[1..].iter() { + let distance = get_distance(local, id); + if distance != Some(256) { + seed += 1; + continue 'main; + } + } + break; + } + println!("Found Seed: {}", seed); +} + +#[allow(dead_code)] +fn find_seed_spread_bucket() { + let mut buckets; + let mut seed = 1; + loop { + seed += 1; + + let keys = generate_deterministic_keypair(11, seed); + + let node_ids = keys + .into_iter() + .map(|k| NodeId::from(k.public())) + .collect::>(); + + let local = node_ids[0]; + + buckets = HashMap::new(); + + for &id in node_ids[1..].iter() { + let distance = get_distance(local, id); + if let Some(distance) = distance { + *buckets.entry(distance).or_insert_with(|| 0) += 1; + } + } + if buckets.values().find(|v| **v > 2) == None { + break; + } + if seed % 1000 == 0 { + println!("Seed: {}", seed); + } + } + println!("Found Seed: {}", seed); + for (k, v) in buckets.iter() { + println!("{}, {}", k, v); + } +} + +/// This is a smaller version of the star topology test designed to debug issues with queries. +#[tokio::test] +async fn test_discovery_three_peers() { + init(); + let total_nodes = 3; + // Seed is chosen such that all nodes are in the 256th bucket of bootstrap + let seed = 1652; + // Generate `num_nodes` + bootstrap_node and target_node keypairs from given seed + let keypairs = generate_deterministic_keypair(total_nodes + 2, seed); + let mut nodes = build_nodes_from_keypairs(keypairs, 11200).await; + // Last node is bootstrap node in a star topology + let bootstrap_node = nodes.remove(0); + // target_node is not polled. + let target_node = nodes.pop().unwrap(); + println!("Bootstrap node: {}", bootstrap_node.local_enr().node_id()); + println!("Target node: {}", target_node.local_enr().node_id()); + let key: kbucket::Key = target_node.local_enr().node_id().into(); + let distance = key + .log2_distance(&bootstrap_node.local_enr().node_id().into()) + .unwrap(); + println!( + "Distance of target_node {} relative to bootstrap {}: {}", + target_node.local_enr().node_id(), + bootstrap_node.local_enr().node_id(), + distance + ); + for node in nodes.iter_mut() { + let key: kbucket::Key = node.local_enr().node_id().into(); + let distance = key + .log2_distance(&bootstrap_node.local_enr().node_id().into()) + .unwrap(); + println!( + "Distance of node {} relative to bootstrap {}: {}", + node.local_enr().node_id(), + bootstrap_node.local_enr().node_id(), + distance + ); + node.add_enr(bootstrap_node.local_enr().clone()).unwrap(); + bootstrap_node.add_enr(node.local_enr().clone()).unwrap(); + } + + // Start a FINDNODE query of target + let target_random_node_id = target_node.local_enr().node_id(); + nodes.push(bootstrap_node); + let result_nodes = nodes + .first_mut() + .unwrap() + .find_node(target_random_node_id) + .await + .unwrap(); + println!( + "Query found {} peers, Total peers {}", + result_nodes.len(), + total_nodes + ); + assert!(result_nodes.len() == total_nodes); +} + +/// Test for a star topology with `num_nodes` connected to a `bootstrap_node` +/// FINDNODE request is sent from any of the `num_nodes` nodes to a `target_node` +/// which isn't part of the swarm. +/// The seed for the keypair generation is chosen such that all `num_nodes` nodes +/// and the `target_node` are in the 256th k-bucket of the bootstrap node. +/// This ensures that all nodes are found in a single FINDNODE query. +#[tokio::test] +async fn test_discovery_star_topology() { + init(); + let total_nodes = 10; + // Seed is chosen such that all nodes are in the 256th bucket of bootstrap + let seed = 1652; + // Generate `num_nodes` + bootstrap_node and target_node keypairs from given seed + let keypairs = generate_deterministic_keypair(total_nodes + 2, seed); + let mut nodes = build_nodes_from_keypairs(keypairs, 11000).await; + // Last node is bootstrap node in a star topology + let bootstrap_node = nodes.remove(0); + // target_node is not polled. + let target_node = nodes.pop().unwrap(); + println!("Bootstrap node: {}", bootstrap_node.local_enr().node_id()); + let key: kbucket::Key = target_node.local_enr().node_id().into(); + let distance = key + .log2_distance(&bootstrap_node.local_enr().node_id().into()) + .unwrap(); + println!("Target node: {}", target_node.local_enr().node_id()); + println!( + "Distance of target_node {} relative to bootstrap {}: {}", + target_node.local_enr().node_id(), + bootstrap_node.local_enr().node_id(), + distance + ); + for node in nodes.iter_mut() { + let key: kbucket::Key = node.local_enr().node_id().into(); + let distance = key + .log2_distance(&bootstrap_node.local_enr().node_id().into()) + .unwrap(); + println!( + "Distance of node {} relative to bootstrap node {}: {}", + node.local_enr().node_id(), + bootstrap_node.local_enr().node_id(), + distance + ); + node.add_enr(bootstrap_node.local_enr().clone()).unwrap(); + bootstrap_node.add_enr(node.local_enr().clone()).unwrap(); + } + // Start a FINDNODE query of target + let target_random_node_id = target_node.local_enr().node_id(); + nodes.push(bootstrap_node); + let result_nodes = nodes + .first_mut() + .unwrap() + .find_node(target_random_node_id) + .await + .unwrap(); + println!( + "Query found {} peers, Total peers {}", + result_nodes.len(), + total_nodes + ); + assert!(result_nodes.len() == total_nodes); +} + +#[tokio::test] +async fn test_findnode_query() { + init(); + // build a collection of 8 nodes + let total_nodes = 8; + let mut nodes = build_nodes(total_nodes, 30000).await; + let node_enrs: Vec> = nodes.iter().map(|n| n.local_enr()).collect(); + + // link the nodes together + for (node, previous_node_enr) in nodes.iter_mut().skip(1).zip(node_enrs.clone()) { + let key: kbucket::Key = node.local_enr().node_id().into(); + let distance = key + .log2_distance(&previous_node_enr.node_id().into()) + .unwrap(); + println!("Distance of node relative to next: {}", distance); + node.add_enr(previous_node_enr).unwrap(); + } + + // pick a random node target + let target_random_node_id = NodeId::random(); + + // start a query on the last node + let found_nodes = nodes + .last_mut() + .unwrap() + .find_node(target_random_node_id) + .await + .unwrap(); + + // build expectations + let expected_node_ids: Vec = node_enrs + .iter() + .map(|enr| enr.node_id()) + .take(total_nodes - 1) + .collect(); + + // NOTE: The number of peers found is statistical, as we only ask + // peers for specific buckets, there is a chance our node doesn't + // exist if the first few buckets asked for. + println!( + "Query with found {} peers. Total peers were: {}", + found_nodes.len(), + expected_node_ids.len() + ); + assert!(found_nodes.len() <= expected_node_ids.len()); +} + +#[tokio::test] +async fn test_predicate_search() { + init(); + let total_nodes = 10; + // Seed is chosen such that all nodes are in the 256th bucket of bootstrap + let seed = 1652; + // Generate `num_nodes` + bootstrap_node and target_node keypairs from given seed + let keypairs = generate_deterministic_keypair(total_nodes + 2, seed); + let mut nodes = build_nodes_from_keypairs(keypairs, 12000).await; + // Last node is bootstrap node in a star topology + let bootstrap_node = nodes.remove(0); + // target_node is not polled. + let target_node = nodes.pop().unwrap(); + + // Update `num_nodes` with the required attnet value + let num_nodes = total_nodes / 2; + let required_attnet_value = vec![1, 0, 0, 0]; + let unwanted_attnet_value = vec![0, 0, 0, 0]; + println!("Bootstrap node: {}", bootstrap_node.local_enr().node_id()); + println!("Target node: {}", target_node.local_enr().node_id()); + + for (i, swarm) in nodes.iter_mut().enumerate() { + let key: kbucket::Key = swarm.local_enr().node_id().into(); + let distance = key + .log2_distance(&bootstrap_node.local_enr().node_id().into()) + .unwrap(); + println!( + "Distance of node {} relative to node {}: {}", + swarm.local_enr().node_id(), + bootstrap_node.local_enr().node_id(), + distance + ); + swarm.add_enr(bootstrap_node.local_enr().clone()).unwrap(); + if i % 2 == 0 { + update_enr(swarm, "attnets", &unwanted_attnet_value); + } else { + update_enr(swarm, "attnets", &required_attnet_value); + } + bootstrap_node.add_enr(swarm.local_enr().clone()).unwrap(); + } + + // Predicate function for filtering enrs + let predicate = move |enr: &Enr| { + if let Some(v) = enr.get("attnets") { + v == required_attnet_value.as_slice() + } else { + false + } + }; + nodes.push(bootstrap_node); + + // Start a find enr predicate query + let target_random_node_id = target_node.local_enr().node_id(); + let found_nodes = nodes + .first_mut() + .unwrap() + .find_node_predicate(target_random_node_id, Box::new(predicate), total_nodes) + .await + .unwrap(); + + println!( + "Query found {} peers. Total peers were: {}", + found_nodes.len(), + total_nodes, + ); + println!("Nodes expected to pass predicate search {}", num_nodes); + assert!(found_nodes.len() == num_nodes); +} + +// The kbuckets table can have maximum 10 nodes in the same /24 subnet across all buckets +#[tokio::test] +async fn test_table_limits() { + // this seed generates 12 node id's that are distributed accross buckets such that no more than + // 2 exist in a single bucket. + let mut keypairs = generate_deterministic_keypair(12, 9487); + let ip: IpAddr = "127.0.0.1".parse().unwrap(); + let enr_key: CombinedKey = keypairs.remove(0); + let config = Discv5ConfigBuilder::new().ip_limit().build(); + let enr = EnrBuilder::new("v4") + .ip(ip) + .udp(9050) + .build(&enr_key) + .unwrap(); + + // let socket_addr = enr.udp_socket().unwrap(); + let discv5: Discv5 = Discv5::new(enr, enr_key, config).unwrap(); + let table_limit: usize = 10; + // Generate `table_limit + 2` nodes in the same subnet. + let enrs: Vec> = (1..=table_limit + 1) + .map(|i| { + let ip: IpAddr = IpAddr::V4(Ipv4Addr::new(192, 168, 1, i as u8)); + let enr_key: CombinedKey = keypairs.remove(0); + EnrBuilder::new("v4") + .ip(ip) + .udp(9050 + i as u16) + .build(&enr_key) + .unwrap() + }) + .collect(); + for enr in enrs { + let _ = discv5.add_enr(enr.clone()); // we expect some of these to fail the filter. + } + // Number of entries should be `table_limit`, i.e one node got restricted + assert_eq!(discv5.kbuckets.read().iter_ref().count(), table_limit); +} + +// Each bucket can have maximum 2 nodes in the same /24 subnet +#[tokio::test] +async fn test_bucket_limits() { + let enr_key = CombinedKey::generate_secp256k1(); + let ip: IpAddr = "127.0.0.1".parse().unwrap(); + let enr = EnrBuilder::new("v4") + .ip(ip) + .udp(9500) + .build(&enr_key) + .unwrap(); + let bucket_limit: usize = 2; + // Generate `bucket_limit + 1` keypairs that go in `enr` node's 256th bucket. + let keys = { + let mut keys = Vec::new(); + for _ in 0..bucket_limit + 1 { + loop { + let key = CombinedKey::generate_secp256k1(); + let enr_new = EnrBuilder::new("v4").build(&key).unwrap(); + let node_key: kbucket::Key = enr.node_id().into(); + let distance = node_key.log2_distance(&enr_new.node_id().into()).unwrap(); + if distance == 256 { + keys.push(key); + break; + } + } + } + keys + }; + // Generate `bucket_limit + 1` nodes in the same subnet. + let enrs: Vec> = (1..=bucket_limit + 1) + .map(|i| { + let kp = &keys[i - 1]; + let ip: IpAddr = IpAddr::V4(Ipv4Addr::new(192, 168, 1, i as u8)); + EnrBuilder::new("v4") + .ip(ip) + .udp(9500 + i as u16) + .build(kp) + .unwrap() + }) + .collect(); + + let config = Discv5ConfigBuilder::new().ip_limit().build(); + let discv5 = Discv5::new(enr, enr_key, config).unwrap(); + for enr in enrs { + let _ = discv5.add_enr(enr.clone()); // we expect some of these to fail based on the filter. + } + + // Number of entries should be equal to `bucket_limit`. + assert_eq!(discv5.kbuckets.read().iter_ref().count(), bucket_limit); +} diff --git a/version-meld/discv5/src/error.rs b/version-meld/discv5/src/error.rs new file mode 100644 index 0000000..26f0757 --- /dev/null +++ b/version-meld/discv5/src/error.rs @@ -0,0 +1,146 @@ +use crate::handler::Challenge; +use rlp::DecoderError; +use std::fmt; + +#[derive(Debug)] +/// A general error that is used throughout the Discv5 library. +pub enum Discv5Error { + /// An invalid ENR was received. + InvalidEnr, + /// The public key type is known. + UnknownPublicKey, + /// The ENR key used is not supported. + KeyTypeNotSupported(&'static str), + /// Failed to derive an ephemeral public key. + KeyDerivationFailed, + /// The remote's public key was invalid. + InvalidRemotePublicKey, + /// The secret key does not match the provided ENR. + InvalidSecretKey, + /// An invalid signature was received for a challenge. + InvalidChallengeSignature(Challenge), + /// The Service channel has been closed early. + ServiceChannelClosed, + /// The discv5 service is not running. + ServiceNotStarted, + /// The service has is already running. + ServiceAlreadyStarted, + /// A session could not be established with the remote. + SessionNotEstablished, + /// An RLP decoding error occurred. + RLPError(DecoderError), + /// Failed to encrypt a message. + EncryptionFail(String), + /// Failed to decrypt a message. + DecryptionFailed(String), + /// The custom error has occurred. + Custom(&'static str), + /// A generic dynamic error occurred. + Error(String), + /// An IO error occurred. + Io(std::io::Error), +} + +impl From for Discv5Error { + fn from(err: std::io::Error) -> Discv5Error { + Discv5Error::Io(err) + } +} + +#[derive(Debug, Clone, PartialEq)] +/// Types of packet errors. +pub enum PacketError { + /// The packet type is unknown. + UnknownPacket, + /// The packet size was larger than expected. + TooLarge, + /// The packet size was smaller than expected. + TooSmall, + /// The NodeId sent was invalid. + InvalidNodeId, + /// The header has an invalid length. + HeaderLengthInvalid(usize), + /// The header could not be decrypted. + HeaderDecryptionFailed, + /// The authdata size is too large. + InvalidAuthDataSize, + /// The handshake is of an invalid version. + InvalidVersion(u16), + /// The ENR sent was invalid. + InvalidEnr(DecoderError), +} + +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub enum ResponseError { + /// The channel used to send the response has already been closed. + ChannelClosed, +} + +impl fmt::Display for ResponseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ResponseError::ChannelClosed => { + write!(f, "response channel has already been closed") + } + } + } +} + +impl std::error::Error for ResponseError {} + +#[derive(Debug, Clone, PartialEq)] +pub enum RequestError { + /// The request timed out. + Timeout, + /// The discovery service has not been started. + ServiceNotStarted, + /// The request was sent to ourselves. + SelfRequest, + /// The channel to the underlying threads failed. + ChannelFailed(String), + /// An invalid ENR was provided. + InvalidEnr(String), + /// The remote's ENR is invalid. + InvalidRemoteEnr, + /// The remote returned and invalid packet. + InvalidRemotePacket, + /// Failed attempting to encrypt the request. + EncryptionFailed(String), + /// The multiaddr provided is invalid. + InvalidMultiaddr(String), + /// Failure generating random numbers during request. + EntropyFailure(&'static str), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum QueryError { + /// The discv5 service is not currently running. + ServiceNotStarted, + /// The channel to the underlying threads failed. + ChannelFailed(String), + /// The ENR provided was invalid. + InvalidEnr(String), + /// Encrypting the message failed. + EncryptionFailed(String), + /// The multiaddr provided was invalid. + InvalidMultiaddr(String), +} + +impl fmt::Display for Discv5Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl fmt::Display for RequestError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl fmt::Display for QueryError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} diff --git a/version-meld/discv5/src/executor.rs b/version-meld/discv5/src/executor.rs new file mode 100644 index 0000000..f3b6532 --- /dev/null +++ b/version-meld/discv5/src/executor.rs @@ -0,0 +1,42 @@ +///! A simple trait to allow generic executors or wrappers for spawning the discv5 tasks. +use std::future::Future; +use std::pin::Pin; + +pub trait Executor: ExecutorClone { + /// Run the given future in the background until it ends. + fn spawn(&self, future: Pin + Send>>); +} + +pub trait ExecutorClone { + fn clone_box(&self) -> Box; +} + +impl ExecutorClone for T +where + T: 'static + Executor + Clone + Send + Sync, +{ + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +impl Clone for Box { + fn clone(&self) -> Box { + self.clone_box() + } +} + +#[derive(Clone)] +pub struct TokioExecutor; + +impl Executor for TokioExecutor { + fn spawn(&self, future: Pin + Send>>) { + tokio::task::spawn(future); + } +} + +impl Default for TokioExecutor { + fn default() -> Self { + TokioExecutor + } +} diff --git a/version-meld/discv5/src/handler/crypto/ecdh.rs b/version-meld/discv5/src/handler/crypto/ecdh.rs new file mode 100644 index 0000000..bf78000 --- /dev/null +++ b/version-meld/discv5/src/handler/crypto/ecdh.rs @@ -0,0 +1,23 @@ +//! Implements the static ecdh algorithm required by discv5 in terms of the `k256` library. +use super::k256::{ + self, + ecdsa::{SigningKey, VerifyingKey}, + elliptic_curve::sec1::ToEncodedPoint, +}; + +pub fn ecdh(public_key: &VerifyingKey, secret_key: &SigningKey) -> Vec { + k256::PublicKey::from_affine( + (&k256::PublicKey::from_sec1_bytes(public_key.to_bytes().as_ref()) + .unwrap() + .to_projective() + * k256::SecretKey::from_be_bytes(secret_key.to_bytes().as_ref()) + .unwrap() + .to_nonzero_scalar() + .as_ref()) + .to_affine(), + ) + .unwrap() + .to_encoded_point(true) + .as_bytes() + .to_vec() +} diff --git a/version-meld/discv5/src/handler/crypto/mod.rs b/version-meld/discv5/src/handler/crypto/mod.rs new file mode 100644 index 0000000..d6c4544 --- /dev/null +++ b/version-meld/discv5/src/handler/crypto/mod.rs @@ -0,0 +1,405 @@ +//! Implementation for generating session keys in the Discv5 protocol. +//! Currently, Diffie-Hellman key agreement is performed with known public key types. Session keys +//! are then derived using the HKDF (SHA2-256) key derivation function. +//! +//! There is no abstraction in this module as the specification explicitly defines a singular +//! encryption and key-derivation algorithms. Future versions may abstract some of these to allow +//! for different algorithms. +use crate::{ + error::Discv5Error, + node_info::NodeContact, + packet::{ChallengeData, MessageNonce}, +}; +use aes_gcm::{ + aead::{generic_array::GenericArray, Aead, NewAead, Payload}, + Aes128Gcm, +}; +use ecdh::ecdh; +use enr::{ + k256::{ + self, + ecdsa::{ + signature::{DigestSigner, DigestVerifier, Signature as _}, + Signature, + }, + }, + CombinedKey, CombinedPublicKey, NodeId, +}; +use hkdf::Hkdf; +use sha2::{Digest, Sha256}; +use std::convert::TryFrom; + +mod ecdh; + +const NODE_ID_LENGTH: usize = 32; +const INFO_LENGTH: usize = 26 + 2 * NODE_ID_LENGTH; +const KEY_LENGTH: usize = 16; +const KEY_AGREEMENT_STRING: &str = "discovery v5 key agreement"; +const ID_SIGNATURE_TEXT: &str = "discovery v5 identity proof"; + +type Key = [u8; KEY_LENGTH]; + +/* Session key generation */ + +/// Generates session and auth-response keys for a nonce and remote ENR. This currently only +/// supports Secp256k1 signed ENR's. This returns four keys; initiator key, responder key, auth +/// response key and the ephemeral public key. +pub(crate) fn generate_session_keys( + local_id: &NodeId, + contact: &NodeContact, + challenge_data: &ChallengeData, +) -> Result<(Key, Key, Vec), Discv5Error> { + let (secret, ephem_pk) = { + match contact.public_key() { + CombinedPublicKey::Secp256k1(remote_pk) => { + let ephem_sk = k256::ecdsa::SigningKey::random(rand::thread_rng()); + let secret = ecdh(&remote_pk, &ephem_sk); + let ephem_pk = ephem_sk.verifying_key(); + (secret, ephem_pk.to_bytes().to_vec()) + } + CombinedPublicKey::Ed25519(_) => { + return Err(Discv5Error::KeyTypeNotSupported("Ed25519")) + } + } + }; + + let (initiator_key, recipient_key) = + derive_key(&secret, local_id, &contact.node_id(), challenge_data)?; + + Ok((initiator_key, recipient_key, ephem_pk)) +} + +fn derive_key( + secret: &[u8], + first_id: &NodeId, + second_id: &NodeId, + challenge_data: &ChallengeData, +) -> Result<(Key, Key), Discv5Error> { + let mut info = [0u8; INFO_LENGTH]; + info[0..26].copy_from_slice(KEY_AGREEMENT_STRING.as_bytes()); + info[26..26 + NODE_ID_LENGTH].copy_from_slice(&first_id.raw()); + info[26 + NODE_ID_LENGTH..].copy_from_slice(&second_id.raw()); + + let hk = Hkdf::::new(Some(challenge_data.as_ref()), secret); + + let mut okm = [0u8; 2 * KEY_LENGTH]; + hk.expand(&info, &mut okm) + .map_err(|_| Discv5Error::KeyDerivationFailed)?; + + let mut initiator_key: Key = Default::default(); + let mut recipient_key: Key = Default::default(); + initiator_key.copy_from_slice(&okm[0..KEY_LENGTH]); + recipient_key.copy_from_slice(&okm[KEY_LENGTH..2 * KEY_LENGTH]); + + Ok((initiator_key, recipient_key)) +} + +/// Derives the session keys for a public key type that matches the local keypair. +pub(crate) fn derive_keys_from_pubkey( + local_key: &CombinedKey, + local_id: &NodeId, + remote_id: &NodeId, + challenge_data: &ChallengeData, + ephem_pubkey: &[u8], +) -> Result<(Key, Key), Discv5Error> { + let secret = { + match local_key { + CombinedKey::Secp256k1(key) => { + // convert remote pubkey into secp256k1 public key + // the key type should match our own node record + let remote_pubkey = k256::ecdsa::VerifyingKey::from_sec1_bytes(ephem_pubkey) + .map_err(|_| Discv5Error::InvalidRemotePublicKey)?; + ecdh(&remote_pubkey, key) + } + CombinedKey::Ed25519(_) => return Err(Discv5Error::KeyTypeNotSupported("Ed25519")), + } + }; + + derive_key(&secret, remote_id, local_id, challenge_data) +} + +/* Nonce Signing */ + +/// Generates a signature of a nonce given a keypair. This prefixes the `NONCE_PREFIX` to the +/// signature. +pub(crate) fn sign_nonce( + signing_key: &CombinedKey, + challenge_data: &ChallengeData, + ephem_pubkey: &[u8], + dst_id: &NodeId, +) -> Result, Discv5Error> { + let signing_message = generate_signing_nonce(challenge_data, ephem_pubkey, dst_id); + + match signing_key { + CombinedKey::Secp256k1(key) => { + let message = Sha256::new().chain_update(signing_message); + let signature: Signature = key + .try_sign_digest(message) + .map_err(|e| Discv5Error::Error(format!("Failed to sign message: {}", e)))?; + Ok(signature.as_bytes().to_vec()) + } + CombinedKey::Ed25519(_) => Err(Discv5Error::KeyTypeNotSupported("Ed25519")), + } +} + +/// Verifies the authentication header nonce. +pub(crate) fn verify_authentication_nonce( + remote_pubkey: &CombinedPublicKey, + remote_ephem_pubkey: &[u8], + challenge_data: &ChallengeData, + dst_id: &NodeId, + sig: &[u8], +) -> bool { + let signing_nonce = generate_signing_nonce(challenge_data, remote_ephem_pubkey, dst_id); + + match remote_pubkey { + CombinedPublicKey::Secp256k1(key) => { + if let Ok(sig) = k256::ecdsa::Signature::try_from(sig) { + return key + .verify_digest(Sha256::new().chain_update(signing_nonce), &sig) + .is_ok(); + } + false + } + CombinedPublicKey::Ed25519(_) => { + // key not yet supported + false + } + } +} + +/// Builds the signature for a given challenge data. +/// +/// This takes the SHA256 hash of the nonce. +fn generate_signing_nonce( + challenge_data: &ChallengeData, + ephem_pubkey: &[u8], + dst_id: &NodeId, +) -> Vec { + let mut data = ID_SIGNATURE_TEXT.as_bytes().to_vec(); + data.extend_from_slice(challenge_data.as_ref()); + data.extend_from_slice(ephem_pubkey); + data.extend_from_slice(&dst_id.raw().to_vec()); + data +} + +/* Decryption related functions */ + +/// Decrypt messages that are post-fixed with an authenticated MAC. +pub(crate) fn decrypt_message( + key: &Key, + message_nonce: MessageNonce, + msg: &[u8], + aad: &[u8], +) -> Result, Discv5Error> { + if msg.len() < 16 { + return Err(Discv5Error::DecryptionFailed( + "Message not long enough to contain a MAC".into(), + )); + } + + let aead = Aes128Gcm::new(GenericArray::from_slice(key)); + let payload = Payload { msg, aad }; + aead.decrypt(GenericArray::from_slice(&message_nonce), payload) + .map_err(|e| Discv5Error::DecryptionFailed(e.to_string())) +} + +/* Encryption related functions */ + +/// A wrapper around the underlying default AES_GCM implementation. This may be abstracted in the +/// future. +pub(crate) fn encrypt_message( + key: &Key, + message_nonce: MessageNonce, + msg: &[u8], + aad: &[u8], +) -> Result, Discv5Error> { + let aead = Aes128Gcm::new(GenericArray::from_slice(key)); + let payload = Payload { msg, aad }; + aead.encrypt(GenericArray::from_slice(&message_nonce), payload) + .map_err(|e| Discv5Error::DecryptionFailed(e.to_string())) +} + +#[cfg(test)] +mod tests { + use super::*; + use enr::{CombinedKey, EnrBuilder, EnrKey}; + use std::convert::TryInto; + + fn hex_decode(x: &'static str) -> Vec { + hex::decode(x).unwrap() + } + + fn node_key_1() -> CombinedKey { + CombinedKey::secp256k1_from_bytes(&mut hex_decode( + "eef77acb6c6a6eebc5b363a475ac583ec7eccdb42b6481424c60f59aa326547f", + )) + .unwrap() + } + + fn node_key_2() -> CombinedKey { + CombinedKey::secp256k1_from_bytes(&mut hex_decode( + "66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628", + )) + .unwrap() + } + /* This section provides a series of reference tests for the encoding of packets */ + + #[test] + fn ref_test_ecdh() { + let remote_pubkey = + hex::decode("039961e4c2356d61bedb83052c115d311acb3a96f5777296dcf297351130266231") + .unwrap(); + let local_secret_key = + hex::decode("fb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736") + .unwrap(); + + let expected_secret = + hex::decode("033b11a2a1f214567e1537ce5e509ffd9b21373247f2a3ff6841f4976f53165e7e") + .unwrap(); + + let remote_pk = k256::ecdsa::VerifyingKey::from_sec1_bytes(&remote_pubkey).unwrap(); + let local_sk = k256::ecdsa::SigningKey::from_bytes(&local_secret_key).unwrap(); + + let secret = ecdh(&remote_pk, &local_sk); + assert_eq!(secret, expected_secret); + } + + #[test] + fn ref_key_derivation() { + let ephem_key = + hex::decode("fb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736") + .unwrap(); + let dest_pubkey = + hex::decode("0317931e6e0840220642f230037d285d122bc59063221ef3226b1f403ddc69ca91") + .unwrap(); + + let remote_pk = k256::ecdsa::VerifyingKey::from_sec1_bytes(&dest_pubkey).unwrap(); + let local_sk = k256::ecdsa::SigningKey::from_bytes(&ephem_key).unwrap(); + + let secret = ecdh(&remote_pk, &local_sk); + + let first_node_id: NodeId = node_key_1().public().into(); + let second_node_id: NodeId = node_key_2().public().into(); + + let challenge_data: ChallengeData = hex::decode("000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000").unwrap().as_slice().try_into().unwrap(); + + let expected_first_key = hex::decode("dccc82d81bd610f4f76d3ebe97a40571").unwrap(); + let expected_second_key = hex::decode("ac74bb8773749920b0d3a8881c173ec5").unwrap(); + + let (first_key, second_key) = + derive_key(&secret, &first_node_id, &second_node_id, &challenge_data).unwrap(); + + assert_eq!(first_key.to_vec(), expected_first_key); + assert_eq!(second_key.to_vec(), expected_second_key); + } + + #[test] + fn ref_nonce_signing() { + let ephemeral_pubkey = + hex::decode("039961e4c2356d61bedb83052c115d311acb3a96f5777296dcf297351130266231") + .unwrap(); + let local_secret_key = + hex::decode("fb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736") + .unwrap(); + let dst_id: NodeId = node_key_2().public().into(); + + println!("{}", dst_id); + + let expected_sig = hex::decode("94852a1e2318c4e5e9d422c98eaf19d1d90d876b29cd06ca7cb7546d0fff7b484fe86c09a064fe72bdbef73ba8e9c34df0cd2b53e9d65528c2c7f336d5dfc6e6").unwrap(); + + let challenge_data = ChallengeData::try_from(hex::decode("000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000").unwrap().as_slice()).unwrap(); + let key = k256::ecdsa::SigningKey::from_bytes(&local_secret_key).unwrap(); + let sig = sign_nonce(&key.into(), &challenge_data, &ephemeral_pubkey, &dst_id).unwrap(); + + assert_eq!(sig, expected_sig); + } + + #[test] + fn ref_encryption() { + let key_bytes = hex::decode("9f2d77db7004bf8a1a85107ac686990b").unwrap(); + let nonce_bytes = hex::decode("27b5af763c446acd2749fe8e").unwrap(); + let pt = hex::decode("01c20101").unwrap(); + let ad = hex::decode("93a7400fa0d6a694ebc24d5cf570f65d04215b6ac00757875e3f3a5f42107903") + .unwrap(); + let expected_ciphertext = hex::decode("a5d12a2d94b8ccb3ba55558229867dc13bfa3648").unwrap(); + + let mut key = [0u8; 16]; + key.copy_from_slice(&key_bytes); + let mut nonce = [0u8; 12]; + nonce.copy_from_slice(&nonce_bytes); + + let ciphertext = encrypt_message(&key, nonce, &pt, &ad).unwrap(); + + assert_eq!(ciphertext, expected_ciphertext); + } + + /* This section provides functionality testing */ + + #[test] + fn derive_symmetric_keys() { + let node1_key = CombinedKey::generate_secp256k1(); + let node2_key = CombinedKey::generate_secp256k1(); + + let node1_enr = EnrBuilder::new("v4").build(&node1_key).unwrap(); + let node2_enr = EnrBuilder::new("v4").build(&node2_key).unwrap(); + + let challenge_data = vec![1; 63]; + let challenge_data = ChallengeData::try_from(challenge_data.as_slice()).unwrap(); + + let (key1, key2, pk) = generate_session_keys( + &node1_enr.node_id(), + &node2_enr.clone().into(), + &challenge_data, + ) + .unwrap(); + let (key4, key5) = derive_keys_from_pubkey( + &node2_key, + &node2_enr.node_id(), + &node1_enr.node_id(), + &challenge_data, + &pk, + ) + .unwrap(); + + assert_eq!(key1, key4); + assert_eq!(key2, key5); + } + + #[test] + fn encrypt_decrypt() { + // aad + let aad: [u8; 12] = rand::random(); + let msg: Vec = vec![1, 2, 3, 4, 5, 6]; + let key: Key = rand::random(); + let nonce: MessageNonce = rand::random(); + + let cipher = encrypt_message(&key, nonce, &msg, &aad).unwrap(); + let plain_text = decrypt_message(&key, nonce, &cipher, &aad).unwrap(); + + assert_eq!(plain_text, msg); + } + + #[test] + fn decrypt_ref_test_ping() { + let dst_id: NodeId = node_key_2().public().into(); + let encoded_ref_packet = hex::decode("00000000000000000000000000000000088b3d4342774649325f313964a39e55ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d34c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc").unwrap(); + let (_packet, auth_data) = + crate::packet::Packet::decode(&dst_id, &encoded_ref_packet).unwrap(); + + let ciphertext = hex::decode("b84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc").unwrap(); + let read_key = hex::decode("00000000000000000000000000000000").unwrap(); + let mut key = [0u8; 16]; + key.copy_from_slice(&read_key); + let byte_nonce = hex::decode("ffffffffffffffffffffffff").unwrap(); + let mut nonce = [0u8; 12]; + nonce.copy_from_slice(&byte_nonce); + + let message = decrypt_message(&key, nonce, &ciphertext, &auth_data).unwrap(); + dbg!(&message); + dbg!(hex::encode(&message)); + let rpc = crate::rpc::Message::decode(&message).unwrap(); + + println!("{}", rpc); + } +} diff --git a/version-meld/discv5/src/handler/hashmap_delay.rs b/version-meld/discv5/src/handler/hashmap_delay.rs new file mode 100644 index 0000000..3f369d9 --- /dev/null +++ b/version-meld/discv5/src/handler/hashmap_delay.rs @@ -0,0 +1,170 @@ +//! A simple hashmap object coupled with a `delay_queue` which has entries that expire after a +//! fixed time. +//! +//! A `HashMapDelay` implements `Stream` which removes expired items from the map. + +/// The default delay for entries, in seconds. This is only used when `insert()` is used to add +/// entries. +const DEFAULT_DELAY: u64 = 30; + +use futures::prelude::*; +use std::{ + collections::HashMap, + pin::Pin, + task::{Context, Poll}, + time::Duration, +}; +use tokio_util::time::delay_queue::{self, DelayQueue}; + +pub struct HashMapDelay +where + K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin, +{ + /// The given entries. + entries: HashMap>, + /// A queue holding the timeouts of each entry. + expirations: DelayQueue, + /// The default expiration timeout of an entry. + default_entry_timeout: Duration, +} + +/// A wrapping around entries that adds the link to the entry's expiration, via a `delay_queue` key. +struct MapEntry { + /// The expiration key for the entry. + key: delay_queue::Key, + /// The actual entry. + value: V, +} + +impl Default for HashMapDelay +where + K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin, +{ + fn default() -> Self { + HashMapDelay::new(Duration::from_secs(DEFAULT_DELAY)) + } +} + +impl HashMapDelay +where + K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin, +{ + /// Creates a new instance of `HashMapDelay`. + pub fn new(default_entry_timeout: Duration) -> Self { + HashMapDelay { + entries: HashMap::new(), + expirations: DelayQueue::new(), + default_entry_timeout, + } + } + + /// Insert an entry into the mapping. Entries will expire after the `default_entry_timeout`. + pub fn insert(&mut self, key: K, value: V) { + self.insert_at(key, value, self.default_entry_timeout); + } + + /// Inserts an entry that will expire at a given instant. + pub fn insert_at(&mut self, key: K, value: V, entry_duration: Duration) { + if self.contains_key(&key) { + // update the timeout + self.update_timeout(&key, value, entry_duration); + } else { + let delay_key = self.expirations.insert(key.clone(), entry_duration); + let entry = MapEntry { + key: delay_key, + value, + }; + self.entries.insert(key, entry); + } + } + + /// Updates the timeout for a given key. Returns true if the key existed, false otherwise. + /// + /// Panics if the duration is too far in the future. + pub fn update_timeout(&mut self, key: &K, value: V, timeout: Duration) -> bool { + if let Some(entry) = self.entries.get_mut(key) { + entry.value = value; + self.expirations.reset(&entry.key, timeout); + true + } else { + false + } + } + + /// Gets a reference to an entry if it exists. + /// + /// Returns None if the entry does not exist. + pub fn get(&self, key: &K) -> Option<&V> { + self.entries.get(key).map(|entry| &entry.value) + } + + /// Gets a mutable reference to an entry if it exists. + /// + /// Returns None if the entry does not exist. + pub fn _get_mut(&mut self, key: &K) -> Option<&mut V> { + self.entries.get_mut(key).map(|entry| &mut entry.value) + } + + /// Returns true if the key exists, false otherwise. + pub fn contains_key(&self, key: &K) -> bool { + self.entries.contains_key(key) + } + + /// Returns the length of the mapping. + pub fn _len(&self) -> usize { + self.entries.len() + } + + /// Removes a key from the map returning the value associated with the key that was in the map. + /// + /// Return None if the key was not in the map. + pub fn remove(&mut self, key: &K) -> Option { + if let Some(entry) = self.entries.remove(key) { + self.expirations.remove(&entry.key); + return Some(entry.value); + } + None + } + + /// Retains only the elements specified by the predicate. + /// + /// In other words, remove all pairs `(k, v)` such that `f(&k,&mut v)` returns false. + pub fn _retain bool>(&mut self, mut f: F) { + let expiration = &mut self.expirations; + self.entries.retain(|key, entry| { + let result = f(key, &mut entry.value); + if !result { + expiration.remove(&entry.key); + } + result + }) + } + + /// Removes all entries from the map. + pub fn _clear(&mut self) { + self.entries.clear(); + self.expirations.clear(); + } +} + +impl Stream for HashMapDelay +where + K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin, + V: Unpin, +{ + type Item = Result<(K, V), String>; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.expirations.poll_expired(cx) { + Poll::Ready(Some(Ok(key))) => match self.entries.remove(key.get_ref()) { + Some(entry) => Poll::Ready(Some(Ok((key.into_inner(), entry.value)))), + None => Poll::Ready(Some(Err("Value no longer exists in expirations".into()))), + }, + Poll::Ready(Some(Err(e))) => { + Poll::Ready(Some(Err(format!("delay queue error: {:?}", e)))) + } + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + } + } +} diff --git a/version-meld/discv5/src/handler/mod.rs b/version-meld/discv5/src/handler/mod.rs new file mode 100644 index 0000000..2aebd31 --- /dev/null +++ b/version-meld/discv5/src/handler/mod.rs @@ -0,0 +1,1143 @@ +//! Session and packet handling for the Discv5 Discovery service. +//! +//! The [`Handler`] is responsible for establishing and maintaining sessions with +//! connected/discovered nodes. Each node, identified by it's [`NodeId`] is associated with a +//! `Session`. This service drives the handshakes for establishing the sessions and associated +//! logic for sending/requesting initial connections/ENR's to/from unknown peers. +//! +//! The [`Handler`] also manages the timeouts for each request and reports back RPC failures, +//! and received messages. Messages are encrypted and decrypted using the +//! associated `Session` for each node. +//! +//! An ongoing established connection is abstractly represented by a `Session`. A node that provides an ENR with an +//! IP address/port that doesn't match the source, is considered invalid. A node that doesn't know +//! their external contactable addresses should set their ENR IP field to `None`. +//! +//! The Handler also routinely checks the timeouts for banned nodes and removes them from the +//! banned list once their ban expires. +//! +//! # Usage +//! +//! Interacting with a handler is done via channels. A Handler is spawned using the [`Handler::spawn`] +//! function. This returns an exit channel, a sending and receiving channel respectively. If the +//! exit channel is dropped or fired, the handler task gets shutdown. +//! +//! Requests to the handler can be made via the sending channel using a [`HandlerRequest`]. +//! Responses come by the receiving channel in the form of a [`HandlerResponse`]. +use crate::{ + config::Discv5Config, + discv5::PERMIT_BAN_LIST, + error::{Discv5Error, RequestError}, + packet::{ChallengeData, IdNonce, MessageNonce, Packet, PacketKind}, + rpc::{Message, Request, RequestBody, RequestId, Response, ResponseBody}, + socket, + socket::{FilterConfig, Socket}, + Enr, +}; +use enr::{CombinedKey, NodeId}; +use futures::prelude::*; +use parking_lot::RwLock; +use std::{ + collections::HashMap, + convert::TryFrom, + default::Default, + net::SocketAddr, + sync::{atomic::Ordering, Arc}, + time::{Duration, Instant}, +}; +use tokio::sync::{mpsc, oneshot}; +use tracing::{debug, error, trace, warn}; + +mod crypto; +mod hashmap_delay; +mod session; +mod tests; + +pub use crate::node_info::{NodeAddress, NodeContact}; + +use crate::metrics::METRICS; + +use crate::lru_time_cache::LruTimeCache; +use hashmap_delay::HashMapDelay; +use session::Session; + +// The time interval to check banned peer timeouts and unban peers when the timeout has elapsed (in +// seconds). +const BANNED_NODES_CHECK: u64 = 300; // Check every 5 minutes. + +/// Events sent to the handler to be executed. +#[derive(Debug, Clone, PartialEq)] +#[allow(clippy::large_enum_variant)] +pub enum HandlerRequest { + /// Sends a `Request` to a `NodeContact`. A `NodeContact` is an abstract type + /// that allows for either an ENR to be sent or a `Raw` type which represents an `SocketAddr`, + /// `PublicKey` and `NodeId`. This type can be created from MultiAddrs and MultiAddr strings + /// for some keys. + /// + /// This permits us to send messages to nodes without knowing their ENR. In this case their ENR + /// will be requested during the handshake. + /// + /// A Request is flagged and permits responses through the packet filter. + /// + /// Note: To update an ENR for an unknown node, we request a FINDNODE with distance 0 to the + /// `NodeContact` we know of. + Request(NodeContact, Box), + + /// Send a response to a received request to a particular node. + /// + /// The handler does not keep state of requests, so the application layer must send the + /// response back to the `NodeAddress` from which the request was received. + Response(NodeAddress, Box), + + /// A Random packet has been received and we have requested the application layer to inform + /// us what the highest known ENR is for this node. + /// The `WhoAreYouRef` is sent out in the `HandlerResponse::WhoAreYou` event and should + /// be returned here to submit the application's response. + WhoAreYou(WhoAreYouRef, Option), +} + +/// The outputs provided by the `Handler`. +#[derive(Debug, Clone, PartialEq)] +pub enum HandlerResponse { + /// A session has been established with a node. + /// + /// A session is only considered established once we have received a signed ENR from the + /// node and received messages from it's `SocketAddr` matching it's ENR fields. + Established(Enr, ConnectionDirection), + + /// A Request has been received. + Request(NodeAddress, Box), + + /// A Response has been received. + Response(NodeAddress, Box), + + /// An unknown source has requested information from us. Return the reference with the known + /// ENR of this node (if known). See the `HandlerRequest::WhoAreYou` variant. + WhoAreYou(WhoAreYouRef), + + /// An RPC request failed. + /// + /// This returns the request ID and an error indicating why the request failed. + RequestFailed(RequestId, RequestError), +} + +/// How we connected to the node. +#[derive(PartialEq, Eq, Debug, Copy, Clone)] +pub enum ConnectionDirection { + /// The node contacted us. + Incoming, + /// We contacted the node. + Outgoing, +} + +/// A reference for the application layer to send back when the handler requests any known +/// ENR for the NodeContact. +#[derive(Debug, Clone, PartialEq)] +pub struct WhoAreYouRef(pub NodeAddress, MessageNonce); + +#[derive(Debug)] +/// A Challenge (WHOAREYOU) object used to handle and send WHOAREYOU requests. +pub struct Challenge { + /// The challenge data received from the node. + data: ChallengeData, + /// The remote's ENR if we know it. We can receive a challenge from an unknown node. + remote_enr: Option, +} + +/// A request to a node that we are waiting for a response. +#[derive(Debug)] +pub(crate) struct RequestCall { + contact: NodeContact, + /// The raw discv5 packet sent. + packet: Packet, + /// The unencrypted message. Required if need to re-encrypt and re-send. + request: Request, + /// Handshakes attempted. + handshake_sent: bool, + /// The number of times this request has been re-sent. + retries: u8, + /// If we receive a Nodes Response with a total greater than 1. This keeps track of the + /// remaining responses expected. + remaining_responses: Option, + /// Signifies if we are initiating the session with a random packet. This is only used to + /// determine the connection direction of the session. + initiating_session: bool, +} + +impl RequestCall { + fn new( + contact: NodeContact, + packet: Packet, + request: Request, + initiating_session: bool, + ) -> Self { + RequestCall { + contact, + packet, + request, + handshake_sent: false, + retries: 1, + remaining_responses: None, + initiating_session, + } + } + + fn id(&self) -> &RequestId { + &self.request.id + } +} + +/// Process to handle handshakes and sessions established from raw RPC communications between nodes. +pub struct Handler { + /// Configuration for the discv5 service. + request_retries: u8, + /// The local node id to save unnecessary read locks on the ENR. The NodeID should not change + /// during the operation of the server. + node_id: NodeId, + /// The local ENR. + enr: Arc>, + /// The key to sign the ENR and set up encrypted communication with peers. + key: Arc>, + /// Pending raw requests. A list of raw messages we are awaiting a response from the remote. + active_requests: HashMapDelay, + // WHOAREYOU messages do not include the source node id. We therefore maintain another + // mapping of active_requests via message_nonce. This allows us to match WHOAREYOU + // requests with active requests sent. + /// A mapping of all pending active raw requests message nonces to their NodeAddress. + active_requests_nonce_mapping: HashMap, + /// The expected responses by SocketAddr which allows packets to pass the underlying filter. + filter_expected_responses: Arc>>, + /// Requests awaiting a handshake completion. + pending_requests: HashMap>, + /// Currently in-progress handshakes with peers. + active_challenges: LruTimeCache, + /// Established sessions with peers. + sessions: LruTimeCache, + /// The channel that receives requests from the application layer. + inbound_channel: mpsc::UnboundedReceiver, + /// The channel to send responses to the application layer. + outbound_channel: mpsc::Sender, + /// The listening socket to filter out any attempted requests to self. + listen_socket: SocketAddr, + /// The discovery v5 UDP socket tasks. + socket: Socket, + /// Exit channel to shutdown the handler. + exit: oneshot::Receiver<()>, +} + +type HandlerReturn = ( + oneshot::Sender<()>, + mpsc::UnboundedSender, + mpsc::Receiver, +); +impl Handler { + /// A new Session service which instantiates the UDP socket send/recv tasks. + pub async fn spawn( + enr: Arc>, + key: Arc>, + listen_socket: SocketAddr, + config: Discv5Config, + ) -> Result { + let (exit_sender, exit) = oneshot::channel(); + // create the channels to send/receive messages from the application + let (inbound_send, inbound_channel) = mpsc::unbounded_channel(); + let (outbound_channel, outbound_recv) = mpsc::channel(50); + + // Creates a SocketConfig to pass to the underlying UDP socket tasks. + + // Lets the underlying filter know that we are expecting a packet from this source. + let filter_expected_responses = Arc::new(RwLock::new(HashMap::new())); + + // The local node id + let node_id = enr.read().node_id(); + + // enable the packet filter if required + + let filter_config = FilterConfig { + enabled: config.enable_packet_filter, + rate_limiter: config.filter_rate_limiter.clone(), + max_nodes_per_ip: config.filter_max_nodes_per_ip, + max_bans_per_ip: config.filter_max_bans_per_ip, + }; + + let socket_config = socket::SocketConfig { + executor: config.executor.clone().expect("Executor must exist"), + socket_addr: listen_socket, + filter_config, + local_node_id: node_id, + expected_responses: filter_expected_responses.clone(), + ban_duration: config.ban_duration, + }; + + // Attempt to bind to the socket before spinning up the send/recv tasks. + let socket = socket::Socket::new_socket(&socket_config.socket_addr).await?; + + config + .executor + .clone() + .expect("Executor must be present") + .spawn(Box::pin(async move { + let socket = match socket::Socket::new(socket, socket_config) { + Ok(v) => v, + Err(e) => { + error!("Could not bind UDP socket. {}", e); + return; + } + }; + + let mut handler = Handler { + request_retries: config.request_retries, + node_id, + enr, + key, + active_requests: HashMapDelay::new(config.request_timeout), + active_requests_nonce_mapping: HashMap::new(), + pending_requests: HashMap::new(), + filter_expected_responses, + sessions: LruTimeCache::new( + config.session_timeout, + Some(config.session_cache_capacity), + ), + active_challenges: LruTimeCache::new(config.request_timeout * 2, None), + inbound_channel, + outbound_channel, + listen_socket, + socket, + exit, + }; + debug!("Handler Starting"); + handler.start().await; + })); + + Ok((exit_sender, inbound_send, outbound_recv)) + } + + /// The main execution loop for the handler. + async fn start(&mut self) { + let mut banned_nodes_check = tokio::time::interval(Duration::from_secs(BANNED_NODES_CHECK)); + + loop { + tokio::select! { + Some(handler_request) = self.inbound_channel.recv() => { + match handler_request { + HandlerRequest::Request(contact, request) => { + let id = request.id.clone(); + if let Err(request_error) = self.send_request(contact, *request).await { + // If the sending failed report to the application + let _ = self.outbound_channel.send(HandlerResponse::RequestFailed(id, request_error)).await; + } + } + HandlerRequest::Response(dst, response) => self.send_response(dst, *response).await, + HandlerRequest::WhoAreYou(wru_ref, enr) => self.send_challenge(wru_ref, enr).await, + } + } + Some(inbound_packet) = self.socket.recv.recv() => { + self.process_inbound_packet(inbound_packet).await; + } + Some(Ok((node_address, pending_request))) = self.active_requests.next() => { + self.handle_request_timeout(node_address, pending_request).await; + } + _ = banned_nodes_check.tick() => self.unban_nodes_check(), // Unban nodes that are past the timeout + _ = &mut self.exit => { + return; + } + } + } + } + + /// Processes an inbound decoded packet. + async fn process_inbound_packet(&mut self, inbound_packet: socket::InboundPacket) { + let message_nonce = inbound_packet.header.message_nonce; + match inbound_packet.header.kind { + PacketKind::WhoAreYou { enr_seq, .. } => { + let challenge_data = + ChallengeData::try_from(inbound_packet.authenticated_data.as_slice()) + .expect("Must be correct size"); + self.handle_challenge( + inbound_packet.src_address, + message_nonce, + enr_seq, + challenge_data, + ) + .await + } + PacketKind::Handshake { + src_id, + id_nonce_sig, + ephem_pubkey, + enr_record, + } => { + let node_address = NodeAddress { + socket_addr: inbound_packet.src_address, + node_id: src_id, + }; + self.handle_auth_message( + node_address, + message_nonce, + &id_nonce_sig, + &ephem_pubkey, + enr_record, + &inbound_packet.message, + &inbound_packet.authenticated_data, // This is required for authenticated data in decryption. + ) + .await + } + PacketKind::Message { src_id } => { + let node_address = NodeAddress { + socket_addr: inbound_packet.src_address, + node_id: src_id, + }; + self.handle_message( + node_address, + message_nonce, + &inbound_packet.message, + &inbound_packet.authenticated_data, + ) + .await + } + } + } + + fn remove_expected_response(&mut self, socket_addr: SocketAddr) { + if let std::collections::hash_map::Entry::Occupied(mut entry) = + self.filter_expected_responses.write().entry(socket_addr) + { + let count = entry.get_mut(); + *count = count.saturating_sub(1); + if count == &0 { + entry.remove(); + } + } + } + + fn add_expected_response(&mut self, socket_addr: SocketAddr) { + *self + .filter_expected_responses + .write() + .entry(socket_addr) + .or_default() += 1; + } + + /// A request has timed out. + async fn handle_request_timeout( + &mut self, + node_address: NodeAddress, + mut request_call: RequestCall, + ) { + if request_call.retries >= self.request_retries { + trace!("Request timed out with {}", node_address); + // Remove the request from the awaiting packet_filter + // Remove the associated nonce mapping. + self.active_requests_nonce_mapping + .remove(request_call.packet.message_nonce()); + self.remove_expected_response(node_address.socket_addr); + // The request has timed out. We keep any established session for future use. + self.fail_request(request_call, RequestError::Timeout, false) + .await; + } else { + // increment the request retry count and restart the timeout + trace!( + "Resending message: {} to {}", + request_call.request, + node_address + ); + self.send(node_address.clone(), request_call.packet.clone()) + .await; + request_call.retries += 1; + self.active_requests.insert(node_address, request_call); + } + } + + /// Sends a `Request` to a node. + async fn send_request( + &mut self, + contact: NodeContact, + request: Request, + ) -> Result<(), RequestError> { + let node_address = contact + .node_address() + .map_err(|e| RequestError::InvalidEnr(e.into()))?; + + if node_address.socket_addr == self.listen_socket { + debug!("Filtered request to self"); + return Err(RequestError::SelfRequest); + } + + // If there is already an active request for this node, add to pending requests + if self.active_requests.get(&node_address).is_some() { + trace!("Request queued for node: {}", node_address); + self.pending_requests + .entry(node_address) + .or_insert_with(Vec::new) + .push((contact, request)); + return Ok(()); + } + + let (packet, initiating_session) = { + if let Some(session) = self.sessions.get_mut(&node_address) { + // Encrypt the message and send + let packet = session + .encrypt_message(self.node_id, &request.clone().encode()) + .map_err(|e| RequestError::EncryptionFailed(format!("{:?}", e)))?; + (packet, false) + } else { + // No session exists, start a new handshake + trace!( + "Starting session. Sending random packet to: {}", + node_address + ); + let packet = + Packet::new_random(&self.node_id).map_err(RequestError::EntropyFailure)?; + // We are initiating a new session + (packet, true) + } + }; + + let call = RequestCall::new(contact, packet.clone(), request, initiating_session); + // let the filter know we are expecting a response + self.add_expected_response(node_address.socket_addr); + let nonce = *packet.message_nonce(); + self.send(node_address.clone(), packet).await; + + self.active_requests_nonce_mapping + .insert(nonce, node_address.clone()); + self.active_requests.insert(node_address, call); + Ok(()) + } + + /// Sends an RPC Response. + async fn send_response(&mut self, node_address: NodeAddress, response: Response) { + // Check for an established session + if let Some(session) = self.sessions.get_mut(&node_address) { + // Encrypt the message and send + let packet = match session.encrypt_message(self.node_id, &response.encode()) { + Ok(packet) => packet, + Err(e) => { + warn!("Could not encrypt response: {:?}", e); + return; + } + }; + self.send(node_address, packet).await; + } else { + // Either the session is being established or has expired. We simply drop the + // response in this case. + warn!( + "Session is not established. Dropping response {} for node: {}", + response, node_address.node_id + ); + } + } + + /// This is called in response to a `HandlerResponse::WhoAreYou` event. The applications finds the + /// highest known ENR for a node then we respond to the node with a WHOAREYOU packet. + async fn send_challenge(&mut self, wru_ref: WhoAreYouRef, remote_enr: Option) { + let node_address = wru_ref.0; + let message_nonce = wru_ref.1; + + if self.active_challenges.peek(&node_address).is_some() { + warn!("WHOAREYOU already sent. {}", node_address); + return; + } + + // Ignore this request if the session is already established + if self.sessions.get(&node_address).is_some() { + trace!( + "Session already established. WHOAREYOU not sent to {}", + node_address + ); + return; + } + + // It could be the case we have sent an ENR with an active request, however we consider + // these independent as this is in response to an unknown packet. If the ENR it not in our + // table (remote_enr is None) then we re-request the ENR to keep the session up to date. + + // send the challenge + let enr_seq = remote_enr.clone().map_or_else(|| 0, |enr| enr.seq()); + let id_nonce: IdNonce = rand::random(); + let packet = Packet::new_whoareyou(message_nonce, id_nonce, enr_seq); + let challenge_data = ChallengeData::try_from(packet.authenticated_data().as_slice()) + .expect("Must be the correct challenge size"); + debug!("Sending WHOAREYOU to {}", node_address); + self.send(node_address.clone(), packet).await; + self.active_challenges.insert( + node_address, + Challenge { + data: challenge_data, + remote_enr, + }, + ); + } + + /* Packet Handling */ + + /// Handles a WHOAREYOU packet that was received from the network. + async fn handle_challenge( + &mut self, + src_address: SocketAddr, + request_nonce: MessageNonce, + enr_seq: u64, + challenge_data: ChallengeData, + ) { + // Check that this challenge matches a known active request. + // If this message passes all the requisite checks, a request call is returned. + let mut request_call = { + // Check for an active request + let node_address = match self.active_requests_nonce_mapping.remove(&request_nonce) { + Some(addr) => addr, + None => { + trace!("Received a WHOAREYOU packet that references an unknown or expired request. Source {}, message_nonce {}", src_address, hex::encode(request_nonce)); + return; + } + }; + + // Verify that the src_addresses match + if node_address.socket_addr != src_address { + trace!("Received a WHOAREYOU packet for a message with a non-expected source. Source {}, expected_source: {} message_nonce {}", src_address, node_address.socket_addr, hex::encode(request_nonce)); + // add the mapping back + self.active_requests_nonce_mapping + .insert(request_nonce, node_address); + return; + } + + // Obtain the request from the mapping. This must exist, otherwise there is a + // serious coding error. The active_requests_nonce_mapping and active_requests + // mappings should be 1 to 1. + + match self.active_requests.remove(&node_address) { + Some(request_call) => request_call, + None => { + error!("Active request mappings are not in sync. Message_id {}, node_address {} doesn't exist in active request mapping", hex::encode(request_nonce), node_address); + // NOTE: Both mappings are removed in this case. + return; + } + } + }; + + // double check the message nonces match + if request_call.packet.message_nonce() != &request_nonce { + // This could theoretically happen if a peer uses the same node id across + // different connections. + warn!("Received a WHOAREYOU from a non expected source. Source: {}, message_nonce {} , expected_nonce: {}", request_call.contact, hex::encode(request_call.packet.message_nonce()), hex::encode(request_nonce)); + // NOTE: Both mappings are removed in this case. + return; + } + + trace!( + "Received a WHOAREYOU packet response. Source: {}", + request_call.contact + ); + + // We do not allow multiple WHOAREYOU packets for a single challenge request. If we have + // already sent a WHOAREYOU ourselves, we drop sessions who send us a WHOAREYOU in + // response. + if request_call.handshake_sent { + warn!( + "Authentication response already sent. Dropping session. Node: {}", + request_call.contact + ); + self.fail_request(request_call, RequestError::InvalidRemotePacket, true) + .await; + return; + } + + // Encrypt the message with an auth header and respond + + // First if a new version of our ENR is requested, obtain it for the header + let updated_enr = if enr_seq < self.enr.read().seq() { + Some(self.enr.read().clone()) + } else { + None + }; + + // Generate a new session and authentication packet + let (auth_packet, mut session) = match Session::encrypt_with_header( + &request_call.contact, + self.key.clone(), + updated_enr, + &self.node_id, + &challenge_data, + &(request_call.request.clone().encode()), + ) { + Ok(v) => v, + Err(e) => { + error!("Could not generate a session. Error: {:?}", e); + self.fail_request(request_call, RequestError::InvalidRemotePacket, true) + .await; + return; + } + }; + + // There are two quirks with an established session at this point. + // 1. We may not know the ENR if we dialed this node with a NodeContact::Raw. In this case + // we need to set up a request to find the ENR and wait for a response before we + // officially call this node established. + // 2. The challenge here could be to an already established session. If so, we need to + // update the existing session to attempt to decrypt future messages with the new keys + // and update the keys internally upon successful decryption. + // + // We handle both of these cases here. + + // Check if we know the ENR, if not request it and flag the session as awaiting an ENR. + // + // All sent requests must have an associated node_id. Therefore the following + // must not panic. + let node_address = request_call + .contact + .node_address() + .expect("All sent requests must have a node address"); + match request_call.contact.clone() { + NodeContact::Enr(enr) => { + // NOTE: Here we decide if the session is outgoing or ingoing. The condition for an + // outgoing session is that we originally sent a RANDOM packet (signifying we did + // not have a session for a request) and the packet is not a PING (we are not + // trying to update an old session that may have expired. + let connection_direction = { + match (&request_call.initiating_session, &request_call.request.body) { + (true, RequestBody::Ping { .. }) => ConnectionDirection::Incoming, + (true, _) => ConnectionDirection::Outgoing, + (false, _) => ConnectionDirection::Incoming, + } + }; + + // We already know the ENR. Send the handshake response packet + trace!("Sending Authentication response to node: {}", node_address); + request_call.packet = auth_packet.clone(); + request_call.handshake_sent = true; + request_call.initiating_session = false; + // Reinsert the request_call + self.insert_active_request(request_call); + // Send the actual packet to the send task. + self.send(node_address.clone(), auth_packet).await; + + // Notify the application that the session has been established + self.outbound_channel + .send(HandlerResponse::Established(*enr, connection_direction)) + .await + .unwrap_or_else(|e| warn!("Error with sending channel: {}", e)); + } + NodeContact::Raw { .. } => { + // Don't know the ENR. Establish the session, but request an ENR also + + // Send the Auth response + let contact = request_call.contact.clone(); + trace!( + "Sending Authentication response to node: {}", + request_call + .contact + .node_address() + .expect("Sanitized contact") + ); + request_call.packet = auth_packet.clone(); + request_call.handshake_sent = true; + // Reinsert the request_call + self.insert_active_request(request_call); + self.send(node_address.clone(), auth_packet).await; + + let id = RequestId::random(); + let request = Request { + id: id.clone(), + body: RequestBody::FindNode { distances: vec![0] }, + }; + + session.awaiting_enr = Some(id); + let _ = self.send_request(contact, request).await; + } + } + self.new_session(node_address, session); + } + + /// Verifies a Node ENR to it's observed address. If it fails, any associated session is also + /// considered failed. If it succeeds, we notify the application. + fn verify_enr(&self, enr: &Enr, node_address: &NodeAddress) -> bool { + // If the ENR does not match the observed IP addresses, we consider the Session + // failed. + enr.node_id() == node_address.node_id + && (enr.udp_socket().is_none() || enr.udp_socket() == Some(node_address.socket_addr)) + } + + /// Handle a message that contains an authentication header. + #[allow(clippy::too_many_arguments)] + async fn handle_auth_message( + &mut self, + node_address: NodeAddress, + message_nonce: MessageNonce, + id_nonce_sig: &[u8], + ephem_pubkey: &[u8], + enr_record: Option, + message: &[u8], + authenticated_data: &[u8], + ) { + // Needs to match an outgoing challenge packet (so we have the required nonce to be signed). If it doesn't we drop the packet. + // This will lead to future outgoing challenges if they proceed to send further encrypted + // packets. + trace!( + "Received an Authentication header message from: {}", + node_address + ); + + if let Some(challenge) = self.active_challenges.remove(&node_address) { + match Session::establish_from_challenge( + self.key.clone(), + &self.node_id, + &node_address.node_id, + challenge, + id_nonce_sig, + ephem_pubkey, + enr_record, + ) { + Ok((session, enr)) => { + // Receiving an AuthResponse must give us an up-to-date view of the node ENR. + // Verify the ENR is valid + if self.verify_enr(&enr, &node_address) { + // Session is valid + // Notify the application + // The session established here are from WHOAREYOU packets that we sent. + // This occurs when a node established a connection with us. + let _ = self + .outbound_channel + .send(HandlerResponse::Established( + enr, + ConnectionDirection::Incoming, + )) + .await; + self.new_session(node_address.clone(), session); + self.handle_message( + node_address, + message_nonce, + message, + authenticated_data, + ) + .await; + } else { + // IP's or NodeAddress don't match. Drop the session. + warn!( + "Session has invalid ENR. Enr socket: {:?}, {}", + enr.udp_socket(), + node_address + ); + self.fail_session(&node_address, RequestError::InvalidRemoteEnr, true) + .await; + } + } + Err(Discv5Error::InvalidChallengeSignature(challenge)) => { + warn!( + "Authentication header contained invalid signature. Ignoring packet from: {}", + node_address + ); + // insert back the challenge + self.active_challenges.insert(node_address, challenge); + } + Err(e) => { + warn!( + "Invalid Authentication header. Dropping session. Error: {:?}", + e + ); + self.fail_session(&node_address, RequestError::InvalidRemotePacket, true) + .await; + } + } + } else { + warn!( + "Received an authenticated header without a matching WHOAREYOU request. {}", + node_address + ); + } + } + + async fn send_next_request(&mut self, node_address: NodeAddress) { + // ensure we are not over writing any existing requests + + if self.active_requests.get(&node_address).is_none() { + if let std::collections::hash_map::Entry::Occupied(mut entry) = + self.pending_requests.entry(node_address) + { + // If it exists, there must be a request here + let request = entry.get_mut().remove(0); + if entry.get().is_empty() { + entry.remove(); + } + trace!("Sending next awaiting message. Node: {}", request.0); + let _ = self.send_request(request.0, request.1).await; + } + } + } + + /// Handle a standard message that does not contain an authentication header. + #[allow(clippy::single_match)] + async fn handle_message( + &mut self, + node_address: NodeAddress, + message_nonce: MessageNonce, + message: &[u8], + authenticated_data: &[u8], + ) { + // check if we have an available session + if let Some(session) = self.sessions.get_mut(&node_address) { + // attempt to decrypt and process the message. + let message = match session.decrypt_message(message_nonce, message, authenticated_data) + { + Ok(m) => match Message::decode(&m) { + Ok(p) => p, + Err(e) => { + warn!("Failed to decode message. Error: {:?}, {}", e, node_address); + return; + } + }, + Err(e) => { + // We have a session, but the message could not be decrypted. It is likely the node + // sending this message has dropped their session. In this case, this message is a + // Random packet and we should reply with a WHOAREYOU. + // This means we need to drop the current session and re-establish. + trace!("Decryption failed. Error {}", e); + debug!( + "Message from node: {} is not encrypted with known session keys.", + node_address + ); + self.fail_session(&node_address, RequestError::InvalidRemotePacket, true) + .await; + // If we haven't already sent a WhoAreYou, + // spawn a WHOAREYOU event to check for highest known ENR + // Update the cache time and remove expired entries. + if self.active_challenges.peek(&node_address).is_none() { + let whoareyou_ref = WhoAreYouRef(node_address, message_nonce); + let _ = self + .outbound_channel + .send(HandlerResponse::WhoAreYou(whoareyou_ref)) + .await; + } else { + trace!("WHOAREYOU packet already sent: {}", node_address); + } + return; + } + }; + + trace!("Received message from: {}", node_address); + + // Remove any associated request from pending_request + match message { + Message::Request(request) => { + // report the request to the application + let _ = self + .outbound_channel + .send(HandlerResponse::Request(node_address, Box::new(request))) + .await; + } + Message::Response(response) => { + // Sessions could be awaiting an ENR response. Check if this response matches + // these + if let Some(request_id) = session.awaiting_enr.as_ref() { + if &response.id == request_id { + session.awaiting_enr = None; + match response.body { + ResponseBody::Nodes { mut nodes, .. } => { + // Received the requested ENR + if let Some(enr) = nodes.pop() { + if self.verify_enr(&enr, &node_address) { + // Notify the application + // This can occur when we try to dial a node without an + // ENR. In this case we have attempted to establish the + // connection, so this is an outgoing connection. + let _ = self + .outbound_channel + .send(HandlerResponse::Established( + enr, + ConnectionDirection::Outgoing, + )) + .await; + return; + } + } + } + _ => {} + } + debug!("Session failed invalid ENR response"); + self.fail_session(&node_address, RequestError::InvalidRemoteEnr, true) + .await; + return; + } + } + // Handle standard responses + self.handle_response(node_address, response).await; + } + } + } else { + // no session exists + trace!("Received a message without a session. {}", node_address); + trace!("Requesting a WHOAREYOU packet to be sent."); + // spawn a WHOAREYOU event to check for highest known ENR + let whoareyou_ref = WhoAreYouRef(node_address, message_nonce); + let _ = self + .outbound_channel + .send(HandlerResponse::WhoAreYou(whoareyou_ref)) + .await; + } + } + + /// Handles a response to a request. Re-inserts the request call if the response is a multiple + /// Nodes response. + async fn handle_response(&mut self, node_address: NodeAddress, response: Response) { + // Find a matching request, if any + if let Some(mut request_call) = self.active_requests.remove(&node_address) { + if request_call.id() != &response.id { + trace!( + "Received an RPC Response to an unknown request. Likely late response. {}", + node_address + ); + // add the request back and reset the timer + self.active_requests.insert(node_address, request_call); + return; + } + + // The response matches a request + + // Check to see if this is a Nodes response, in which case we may require to wait for + // extra responses + if let ResponseBody::Nodes { total, .. } = response.body { + if total > 1 { + // This is a multi-response Nodes response + if let Some(remaining_responses) = request_call.remaining_responses.as_mut() { + *remaining_responses -= 1; + if remaining_responses != &0 { + // more responses remaining, add back the request and send the response + // add back the request and send the response + self.active_requests + .insert(node_address.clone(), request_call); + let _ = self + .outbound_channel + .send(HandlerResponse::Response(node_address, Box::new(response))) + .await; + return; + } + } else { + // This is the first instance + request_call.remaining_responses = Some(total - 1); + // add back the request and send the response + self.active_requests + .insert(node_address.clone(), request_call); + let _ = self + .outbound_channel + .send(HandlerResponse::Response(node_address, Box::new(response))) + .await; + return; + } + } + } + + // Remove the associated nonce mapping. + self.active_requests_nonce_mapping + .remove(request_call.packet.message_nonce()); + // Remove the expected response + self.remove_expected_response(node_address.socket_addr); + + // The request matches report the response + let _ = self + .outbound_channel + .send(HandlerResponse::Response( + node_address.clone(), + Box::new(response), + )) + .await; + self.send_next_request(node_address).await; + } else { + // This is likely a late response and we have already failed the request. These get + // dropped here. + trace!("Late response from node: {}", node_address); + } + } + + /// Inserts a request and associated auth_tag mapping. + fn insert_active_request(&mut self, request_call: RequestCall) { + let node_address = request_call + .contact + .node_address() + .expect("Can only add requests with a valid destination"); + // adds the mapping of message nonce to node address + self.active_requests_nonce_mapping + .insert(*request_call.packet.message_nonce(), node_address.clone()); + self.active_requests.insert(node_address, request_call); + } + + fn new_session(&mut self, node_address: NodeAddress, session: Session) { + if let Some(current_session) = self.sessions.get_mut(&node_address) { + current_session.update(session); + } else { + self.sessions.insert(node_address, session); + METRICS + .active_sessions + .store(self.sessions.len(), Ordering::Relaxed); + } + } + + /// A request has failed. + async fn fail_request( + &mut self, + request_call: RequestCall, + error: RequestError, + remove_session: bool, + ) { + // The Request has expired, remove the session. + // Remove the associated nonce mapping. + self.active_requests_nonce_mapping + .remove(request_call.packet.message_nonce()); + // Fail the current request + let request_id = request_call.request.id; + let _ = self + .outbound_channel + .send(HandlerResponse::RequestFailed(request_id, error.clone())) + .await; + + let node_address = request_call + .contact + .node_address() + .expect("All Request calls have been sanitized"); + self.fail_session(&node_address, error, remove_session) + .await; + } + + /// Removes a session and updates associated metrics and fields. + async fn fail_session( + &mut self, + node_address: &NodeAddress, + error: RequestError, + remove_session: bool, + ) { + if remove_session { + self.sessions.remove(node_address); + METRICS + .active_sessions + .store(self.sessions.len(), Ordering::Relaxed); + } + for request in self + .pending_requests + .remove(node_address) + .unwrap_or_else(Vec::new) + { + let _ = self + .outbound_channel + .send(HandlerResponse::RequestFailed(request.1.id, error.clone())) + .await; + } + } + + /// Sends a packet to the send handler to be encoded and sent. + async fn send(&mut self, node_address: NodeAddress, packet: Packet) { + let outbound_packet = socket::OutboundPacket { + node_address, + packet, + }; + let _ = self.socket.send.send(outbound_packet).await; + } + + /// Check if any banned nodes have served their time and unban them. + fn unban_nodes_check(&self) { + PERMIT_BAN_LIST + .write() + .ban_ips + .retain(|_, time| time.is_none() || Some(Instant::now()) < *time); + PERMIT_BAN_LIST + .write() + .ban_nodes + .retain(|_, time| time.is_none() || Some(Instant::now()) < *time); + } +} diff --git a/version-meld/discv5/src/handler/session.rs b/version-meld/discv5/src/handler/session.rs new file mode 100644 index 0000000..c7c3ec3 --- /dev/null +++ b/version-meld/discv5/src/handler/session.rs @@ -0,0 +1,253 @@ +use super::*; +use crate::{ + node_info::NodeContact, + packet::{ChallengeData, Packet, PacketHeader, PacketKind, MESSAGE_NONCE_LENGTH}, +}; +use enr::{CombinedKey, NodeId}; +use zeroize::Zeroize; + +#[derive(Zeroize, PartialEq)] +pub(crate) struct Keys { + /// The encryption key. + encryption_key: [u8; 16], + /// The decryption key. + decryption_key: [u8; 16], +} + +/// A Session containing the encryption/decryption keys. These are kept individually for a given +/// node. +pub(crate) struct Session { + /// The current keys used to encrypt/decrypt messages. + keys: Keys, + /// If a new handshake is being established, these keys can be tried to determine if this new + /// set of keys is canon. + awaiting_keys: Option, + /// If we contacted this node without an ENR, i.e. via a multiaddr, during the session + /// establishment we request the nodes ENR. Once the ENR is received and verified, this session + /// becomes established. + /// + /// This field holds the request_id associated with the ENR request. + pub awaiting_enr: Option, + /// Number of messages sent. Used to ensure the nonce used in message encryption is always + /// unique. + counter: u32, +} + +impl Session { + pub fn new(keys: Keys) -> Self { + Session { + keys, + awaiting_keys: None, + awaiting_enr: None, + counter: 0, + } + } + + /// A new session has been established. Update this session based on the new session. + pub fn update(&mut self, new_session: Session) { + // Await the new sessions keys + self.awaiting_keys = Some(new_session.keys); + self.awaiting_enr = new_session.awaiting_enr; + } + + /// Uses the current `Session` to encrypt a message. Encrypt packets with the current session + /// key if we are awaiting a response from AuthMessage. + pub(crate) fn encrypt_message( + &mut self, + src_id: NodeId, + message: &[u8], + ) -> Result { + self.counter += 1; + + // If the message nonce length is ever set below 4 bytes this will explode. The packet + // size constants shouldn't be modified. + let random_nonce: [u8; MESSAGE_NONCE_LENGTH - 4] = rand::random(); + let mut message_nonce: MessageNonce = [0u8; crate::packet::MESSAGE_NONCE_LENGTH]; + message_nonce[..4].copy_from_slice(&self.counter.to_be_bytes()); + message_nonce[4..].copy_from_slice(&random_nonce); + + // the authenticated data is the IV concatenated with the packet header + let iv: u128 = rand::random(); + let header = PacketHeader { + message_nonce, + kind: PacketKind::Message { src_id }, + }; + + let mut authenticated_data = iv.to_be_bytes().to_vec(); + authenticated_data.extend_from_slice(&header.encode()); + + let cipher = crypto::encrypt_message( + &self.keys.encryption_key, + message_nonce, + message, + &authenticated_data, + )?; + + // construct a packet from the header and the cipher text + Ok(Packet { + iv, + header, + message: cipher, + }) + } + + /// Decrypts an encrypted message. If a Session is already established, the original decryption + /// keys are tried first, upon failure, the new keys are attempted. If the new keys succeed, + /// the session keys are updated along with the Session state. + pub(crate) fn decrypt_message( + &mut self, + message_nonce: MessageNonce, + message: &[u8], + aad: &[u8], + ) -> Result, Discv5Error> { + // try with the new keys + if let Some(new_keys) = self.awaiting_keys.take() { + let result = + crypto::decrypt_message(&new_keys.decryption_key, message_nonce, message, aad); + if result.is_ok() { + self.keys = new_keys; + return result; + } + } + // if it failed try with the old keys + crypto::decrypt_message(&self.keys.decryption_key, message_nonce, message, aad) + } + + /* Session Helper Functions */ + + /// Generates session keys from an authentication header. If the IP of the ENR does not match the + /// source IP address, we consider this session untrusted. The output returns a boolean which + /// specifies if the Session is trusted or not. + pub(crate) fn establish_from_challenge( + local_key: Arc>, + local_id: &NodeId, + remote_id: &NodeId, + challenge: Challenge, + id_nonce_sig: &[u8], + ephem_pubkey: &[u8], + enr_record: Option, + ) -> Result<(Session, Enr), Discv5Error> { + // check and verify a potential ENR update + + // Duplicate code here to avoid cloning an ENR + let remote_public_key = { + let enr = match (enr_record.as_ref(), challenge.remote_enr.as_ref()) { + (Some(new_enr), Some(known_enr)) => { + if new_enr.seq() > known_enr.seq() { + new_enr + } else { + known_enr + } + } + (Some(new_enr), None) => new_enr, + (None, Some(known_enr)) => known_enr, + (None, None) => { + warn!( + "Peer did not respond with their ENR. Session could not be established. Node: {}", + remote_id + ); + return Err(Discv5Error::SessionNotEstablished); + } + }; + enr.public_key() + }; + + // verify the auth header nonce + if !crypto::verify_authentication_nonce( + &remote_public_key, + ephem_pubkey, + &challenge.data, + local_id, + id_nonce_sig, + ) { + return Err(Discv5Error::InvalidChallengeSignature(challenge)); + } + + // The keys are derived after the message has been verified to prevent potential extra work + // for invalid messages. + + // generate session keys + let (decryption_key, encryption_key) = crypto::derive_keys_from_pubkey( + &local_key.read(), + local_id, + remote_id, + &challenge.data, + ephem_pubkey, + )?; + + let keys = Keys { + encryption_key, + decryption_key, + }; + + // Takes ownership of the provided ENRs - Slightly annoying code duplication, but avoids + // cloning ENRs + let session_enr = match (enr_record, challenge.remote_enr) { + (Some(new_enr), Some(known_enr)) => { + if new_enr.seq() > known_enr.seq() { + new_enr + } else { + known_enr + } + } + (Some(new_enr), None) => new_enr, + (None, Some(known_enr)) => known_enr, + (None, None) => unreachable!("Checked in the first match above"), + }; + + Ok((Session::new(keys), session_enr)) + } + + /// Encrypts a message and produces an AuthMessage. + pub(crate) fn encrypt_with_header( + remote_contact: &NodeContact, + local_key: Arc>, + updated_enr: Option, + local_node_id: &NodeId, + challenge_data: &ChallengeData, + message: &[u8], + ) -> Result<(Packet, Session), Discv5Error> { + // generate the session keys + let (encryption_key, decryption_key, ephem_pubkey) = + crypto::generate_session_keys(local_node_id, remote_contact, challenge_data)?; + + let keys = Keys { + encryption_key, + decryption_key, + }; + + // construct the nonce signature + let sig = crypto::sign_nonce( + &local_key.read(), + challenge_data, + &ephem_pubkey, + &remote_contact.node_id(), + ) + .map_err(|_| Discv5Error::Custom("Could not sign WHOAREYOU nonce"))?; + + // build an authentication packet + let message_nonce: MessageNonce = rand::random(); + let mut packet = Packet::new_authheader( + *local_node_id, + message_nonce, + sig, + ephem_pubkey, + updated_enr, + ); + + // Create the authenticated data for the new packet. + + let mut authenticated_data = packet.iv.to_be_bytes().to_vec(); + authenticated_data.extend_from_slice(&packet.header.encode()); + + // encrypt the message + let message_ciphertext = + crypto::encrypt_message(&encryption_key, message_nonce, message, &authenticated_data)?; + + packet.message = message_ciphertext; + + let session = Session::new(keys); + + Ok((packet, session)) + } +} diff --git a/version-meld/discv5/src/handler/tests.rs b/version-meld/discv5/src/handler/tests.rs new file mode 100644 index 0000000..2045285 --- /dev/null +++ b/version-meld/discv5/src/handler/tests.rs @@ -0,0 +1,219 @@ +#![cfg(test)] +use super::*; +use crate::{ + rpc::{Request, Response}, + Discv5ConfigBuilder, +}; +use enr::EnrBuilder; +use std::{net::IpAddr, time::Duration}; +use tokio::time::sleep; + +fn init() { + let _ = tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .try_init(); +} + +macro_rules! arc_rw { + ( $x: expr ) => { + Arc::new(RwLock::new($x)) + }; +} + +#[tokio::test] +// Tests the construction and sending of a simple message +async fn simple_session_message() { + init(); + + let sender_port = 5000; + let receiver_port = 5001; + let ip: IpAddr = "127.0.0.1".parse().unwrap(); + + let key1 = CombinedKey::generate_secp256k1(); + let key2 = CombinedKey::generate_secp256k1(); + + let config = Discv5ConfigBuilder::new().enable_packet_filter().build(); + + let sender_enr = EnrBuilder::new("v4") + .ip(ip) + .udp(sender_port) + .build(&key1) + .unwrap(); + let receiver_enr = EnrBuilder::new("v4") + .ip(ip) + .udp(receiver_port) + .build(&key2) + .unwrap(); + + let (_exit_send, sender_send, _sender_recv) = Handler::spawn( + arc_rw!(sender_enr.clone()), + arc_rw!(key1), + sender_enr.udp_socket().unwrap(), + config.clone(), + ) + .await + .unwrap(); + + let (_exit_recv, recv_send, mut receiver_recv) = Handler::spawn( + arc_rw!(receiver_enr.clone()), + arc_rw!(key2), + receiver_enr.udp_socket().unwrap(), + config, + ) + .await + .unwrap(); + + let send_message = Box::new(Request { + id: RequestId(vec![1]), + body: RequestBody::Ping { enr_seq: 1 }, + }); + + let _ = sender_send.send(HandlerRequest::Request( + receiver_enr.into(), + send_message.clone(), + )); + + let receiver = async move { + loop { + if let Some(message) = receiver_recv.recv().await { + match message { + HandlerResponse::WhoAreYou(wru_ref) => { + let _ = recv_send + .send(HandlerRequest::WhoAreYou(wru_ref, Some(sender_enr.clone()))); + } + HandlerResponse::Request(_, request) => { + assert_eq!(request, send_message); + return; + } + _ => {} + } + } + } + }; + + tokio::select! { + _ = receiver => {} + _ = sleep(Duration::from_millis(500)) => { + panic!("Test timed out"); + } + } +} + +#[tokio::test] +// Tests sending multiple messages on an encrypted session +async fn multiple_messages() { + init(); + let sender_port = 5002; + let receiver_port = 5003; + let ip: IpAddr = "127.0.0.1".parse().unwrap(); + let key1 = CombinedKey::generate_secp256k1(); + let key2 = CombinedKey::generate_secp256k1(); + + let config = Discv5ConfigBuilder::new().build(); + let sender_enr = EnrBuilder::new("v4") + .ip(ip) + .udp(sender_port) + .build(&key1) + .unwrap(); + let receiver_enr = EnrBuilder::new("v4") + .ip(ip) + .udp(receiver_port) + .build(&key2) + .unwrap(); + + let (_exit_send, sender_handler, mut sender_handler_recv) = Handler::spawn( + arc_rw!(sender_enr.clone()), + arc_rw!(key1), + sender_enr.udp_socket().unwrap(), + config.clone(), + ) + .await + .unwrap(); + + let (_exit_recv, recv_send, mut receiver_handler) = Handler::spawn( + arc_rw!(receiver_enr.clone()), + arc_rw!(key2), + receiver_enr.udp_socket().unwrap(), + config, + ) + .await + .unwrap(); + + let send_message = Box::new(Request { + id: RequestId(vec![1]), + body: RequestBody::Ping { enr_seq: 1 }, + }); + + // sender to send the first message then await for the session to be established + let _ = sender_handler.send(HandlerRequest::Request( + receiver_enr.clone().into(), + send_message.clone(), + )); + + let pong_response = Response { + id: RequestId(vec![1]), + body: ResponseBody::Pong { + enr_seq: 1, + ip, + port: sender_port, + }, + }; + + let messages_to_send = 5usize; + + let mut message_count = 0usize; + let recv_send_message = send_message.clone(); + + let sender = async move { + loop { + match sender_handler_recv.recv().await { + Some(HandlerResponse::Established(_, _)) => { + // now the session is established, send the rest of the messages + for _ in 0..messages_to_send - 1 { + let _ = sender_handler.send(HandlerRequest::Request( + receiver_enr.clone().into(), + send_message.clone(), + )); + } + } + _ => continue, + }; + } + }; + + let receiver = async move { + loop { + match receiver_handler.recv().await { + Some(HandlerResponse::WhoAreYou(wru_ref)) => { + let _ = recv_send + .send(HandlerRequest::WhoAreYou(wru_ref, Some(sender_enr.clone()))); + } + Some(HandlerResponse::Request(addr, request)) => { + assert_eq!(request, recv_send_message); + message_count += 1; + // required to send a pong response to establish the session + let _ = recv_send.send(HandlerRequest::Response( + addr, + Box::new(pong_response.clone()), + )); + if message_count == messages_to_send { + return; + } + } + _ => { + continue; + } + } + } + }; + + let sleep_future = sleep(Duration::from_millis(100)); + + tokio::select! { + _ = sender => {} + _ = receiver => {} + _ = sleep_future => { + panic!("Test timed out"); + } + } +} diff --git a/version-meld/discv5/src/kbucket.rs b/version-meld/discv5/src/kbucket.rs new file mode 100644 index 0000000..eb308ed --- /dev/null +++ b/version-meld/discv5/src/kbucket.rs @@ -0,0 +1,1027 @@ +// Copyright 2018 Parity Technologies (UK) Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// This basis of this file has been taken from the rust-libp2p codebase: +// https://github.com/libp2p/rust-libp2p + +//! Implementation of a Kademlia routing table as used by a single peer +//! participating in a Kademlia DHT. +//! +//! The entry point for the API of this module is a [`KBucketsTable`]. +//! +//! ## Pending Insertions +//! +//! When the bucket associated with the `Key` of an inserted entry is full +//! but contains disconnected nodes, it accepts a [`PendingEntry`]. +//! Pending entries are inserted lazily when their timeout is found to be expired +//! upon querying the `KBucketsTable`. When that happens, the `KBucketsTable` records +//! an [`AppliedPending`] result which must be consumed by calling [`take_applied_pending`] +//! regularly and / or after performing lookup operations like [`entry`] and [`closest_keys`]. +//! +//! [`entry`]: KBucketsTable::entry +//! [`closest_keys`]: KBucketsTable::closest_keys +//! [`take_applied_pending`]: KBucketsTable::take_applied_pending + +// [Implementation Notes] +// +// 1. Routing Table Layout +// +// The routing table is currently implemented as a fixed-size "array" of +// buckets, ordered by increasing distance relative to a local key +// that identifies the local peer. This is an often-used, simplified +// implementation that approximates the properties of the b-tree (or prefix tree) +// implementation described in the full paper [0], whereby buckets are split on-demand. +// This should be treated as an implementation detail, however, so that the +// implementation may change in the future without breaking the API. +// +// 2. Replacement Cache +// +// In this implementation, the "replacement cache" for unresponsive peers +// consists of a single entry per bucket. Furthermore, this implementation is +// currently tailored to connection-oriented transports, meaning that the +// "LRU"-based ordering of entries in a bucket is actually based on the last reported +// connection status of the corresponding peers, from least-recently (dis)connected to +// most-recently (dis)connected, and controlled through the `Entry` API. As a result, +// the nodes in the buckets are not reordered as a result of RPC activity, but only as a +// result of nodes being marked as connected or disconnected. In particular, +// if a bucket is full and contains only entries for peers that are considered +// connected, no pending entry is accepted. See the `bucket` submodule for +// further details. +// +// [0]: https://pdos.csail.mit.edu/~petar/papers/maymounkov-kademlia-lncs.pdf + +mod bucket; +mod entry; +mod filter; +mod key; + +pub use entry::*; + +pub use crate::handler::ConnectionDirection; +use arrayvec::{self, ArrayVec}; +use bucket::KBucket; +pub use bucket::{ + ConnectionState, FailureReason, InsertResult as BucketInsertResult, UpdateResult, + MAX_NODES_PER_BUCKET, +}; +pub use filter::{Filter, IpBucketFilter, IpTableFilter}; +use std::{ + collections::VecDeque, + time::{Duration, Instant}, +}; + +/// Maximum number of k-buckets. +const NUM_BUCKETS: usize = 256; + +/// Closest Iterator Output Value +pub struct ClosestValue { + pub key: Key, + pub value: TVal, +} + +impl AsRef> for ClosestValue { + fn as_ref(&self) -> &Key { + &self.key + } +} + +/// A key that can be returned from the `closest_keys` function, which indicates if the key matches the +/// predicate or not. +pub struct PredicateKey { + pub key: Key, + pub predicate_match: bool, +} + +impl From> for Key { + fn from(key: PredicateKey) -> Self { + key.key + } +} + +impl From> for PredicateKey { + fn from(value: PredicateValue) -> Self { + PredicateKey { + key: value.key, + predicate_match: value.predicate_match, + } + } +} + +/// A value being returned from a predicate closest iterator. +pub struct PredicateValue { + pub key: Key, + pub predicate_match: bool, + pub value: TVal, +} + +impl AsRef> for PredicateValue { + fn as_ref(&self) -> &Key { + &self.key + } +} + +impl From> for Key { + fn from(key: PredicateValue) -> Self { + key.key + } +} + +/// A `KBucketsTable` represents a Kademlia routing table. +#[derive(Clone)] +pub struct KBucketsTable { + /// The key identifying the local peer that owns the routing table. + local_key: Key, + /// The buckets comprising the routing table. + buckets: Vec>, + /// The list of evicted entries that have been replaced with pending + /// entries since the last call to [`KBucketsTable::take_applied_pending`]. + applied_pending: VecDeque>, + /// Filter to be applied at the table level when adding/updating a node. + table_filter: Option>>, +} + +#[must_use] +#[derive(Debug, Clone)] +/// Informs if the record was inserted. +pub enum InsertResult { + /// The node didn't exist and the new record was inserted. + Inserted, + /// The node was inserted into a pending state. + Pending { + /// The key of the least-recently connected entry that is currently considered + /// disconnected and whose corresponding peer should be checked for connectivity + /// in order to prevent it from being evicted. If connectivity to the peer is + /// re-established, the corresponding entry should be updated with + /// [`bucket::ConnectionState::Connected`]. + disconnected: Key, + }, + /// The node existed and the status was updated. + StatusUpdated { + // Returns true if the status updated promoted a disconnected node to a connected node. + promoted_to_connected: bool, + }, + /// The node existed and the value was updated. + ValueUpdated, + /// Both the status and value were updated. + Updated { + // Returns true if the status updated promoted a disconnected node to a connected node. + promoted_to_connected: bool, + }, + /// The pending slot was updated. + UpdatedPending, + /// The record failed to be inserted. This can happen to not passing table/bucket filters or + /// the bucket was full. + Failed(FailureReason), +} + +/// A (type-safe) index into a `KBucketsTable`, i.e. a non-negative integer in the +/// interval `[0, NUM_BUCKETS)`. +#[derive(Copy, Clone)] +struct BucketIndex(usize); + +impl BucketIndex { + /// Creates a new `BucketIndex` for a `Distance`. + /// + /// The given distance is interpreted as the distance from a `local_key` of + /// a `KBucketsTable`. If the distance is zero, `None` is returned, in + /// recognition of the fact that the only key with distance `0` to a + /// `local_key` is the `local_key` itself, which does not belong in any + /// bucket. + fn new(d: &Distance) -> Option { + (NUM_BUCKETS - d.0.leading_zeros() as usize) + .checked_sub(1) + .map(BucketIndex) + } + + /// Gets the index value as an unsigned integer. + fn get(self) -> usize { + self.0 + } +} + +impl KBucketsTable +where + TNodeId: Clone, + TVal: Eq, +{ + /// Creates a new, empty Kademlia routing table with entries partitioned + /// into buckets as per the Kademlia protocol. + /// + /// The given `pending_timeout` specifies the duration after creation of + /// a [`PendingEntry`] after which it becomes eligible for insertion into + /// a full bucket, replacing the least-recently (dis)connected node. + /// + /// A filter can be applied that limits entries into a bucket based on the buckets contents. + /// Entries that fail the filter, will not be inserted. + pub fn new( + local_key: Key, + pending_timeout: Duration, + max_incoming_per_bucket: usize, + table_filter: Option>>, + bucket_filter: Option>>, + ) -> Self { + KBucketsTable { + local_key, + buckets: (0..NUM_BUCKETS) + .map(|_| { + KBucket::new( + pending_timeout, + max_incoming_per_bucket, + bucket_filter.clone(), + ) + }) + .collect(), + applied_pending: VecDeque::new(), + table_filter, + } + } + + // Updates a node's status if it exists in the table. + // This checks all table and bucket filters before performing the update. + pub fn update_node_status( + &mut self, + key: &Key, + state: ConnectionState, + direction: Option, + ) -> UpdateResult { + let index = BucketIndex::new(&self.local_key.distance(key)); + if let Some(i) = index { + let bucket = &mut self.buckets[i.get()]; + if let Some(applied) = bucket.apply_pending() { + self.applied_pending.push_back(applied) + } + + bucket.update_status(key, state, direction) + } else { + UpdateResult::NotModified // The key refers to our current node. + } + } + + /// Updates a node's value if it exists in the table. + /// + /// Optionally the connection state can be modified. + pub fn update_node( + &mut self, + key: &Key, + value: TVal, + state: Option, + ) -> UpdateResult { + // Apply the table filter + let mut passed_table_filter = true; + if let Some(table_filter) = self.table_filter.as_ref() { + // Check if the value is a duplicate before applying the table filter (optimisation). + let duplicate = { + let index = BucketIndex::new(&self.local_key.distance(key)); + if let Some(i) = index { + let bucket = &mut self.buckets[i.get()]; + if let Some(node) = bucket.get(key) { + node.value == value + } else { + false + } + } else { + false + } + }; + + // If the Value is new, check the table filter + if !duplicate && !table_filter.filter(&value, &mut self.table_iter()) { + passed_table_filter = false; + } + } + + let index = BucketIndex::new(&self.local_key.distance(key)); + if let Some(i) = index { + let bucket = &mut self.buckets[i.get()]; + if let Some(applied) = bucket.apply_pending() { + self.applied_pending.push_back(applied) + } + + if !passed_table_filter { + bucket.remove(key); + return UpdateResult::Failed(FailureReason::TableFilter); + } + + let update_result = bucket.update_value(key, value); + + if let UpdateResult::Failed(_) = &update_result { + return update_result; + } + + // If we need to update the connection state, update it here. + let status_result = if let Some(state) = state { + bucket.update_status(key, state, None) + } else { + UpdateResult::NotModified + }; + + // Return an appropriate value + match (&update_result, &status_result) { + (_, UpdateResult::Failed(_)) => status_result, + (UpdateResult::Failed(_), _) => update_result, + (_, UpdateResult::UpdatedAndPromoted) => UpdateResult::UpdatedAndPromoted, + (UpdateResult::UpdatedPending, _) => UpdateResult::UpdatedPending, + (_, UpdateResult::UpdatedPending) => UpdateResult::UpdatedPending, + (UpdateResult::NotModified, UpdateResult::NotModified) => UpdateResult::NotModified, + (_, _) => UpdateResult::Updated, + } + } else { + UpdateResult::NotModified // The key refers to our current node. + } + } + + // Attempts to insert or update + pub fn insert_or_update( + &mut self, + key: &Key, + value: TVal, + status: NodeStatus, + ) -> InsertResult { + // Check the table filter + let mut passed_table_filter = true; + if let Some(table_filter) = self.table_filter.as_ref() { + // Check if the value is a duplicate before applying the table filter (optimisation). + let duplicate = { + let index = BucketIndex::new(&self.local_key.distance(key)); + if let Some(i) = index { + let bucket = &mut self.buckets[i.get()]; + if let Some(node) = bucket.get(key) { + node.value == value + } else { + false + } + } else { + false + } + }; + + if !duplicate && !table_filter.filter(&value, &mut self.table_iter()) { + passed_table_filter = false; + } + } + + let index = BucketIndex::new(&self.local_key.distance(key)); + if let Some(i) = index { + let bucket = &mut self.buckets[i.get()]; + if let Some(applied) = bucket.apply_pending() { + self.applied_pending.push_back(applied) + } + + if !passed_table_filter { + bucket.remove(key); + return InsertResult::Failed(FailureReason::TableFilter); + } + + // If the node doesn't exist, insert it + if bucket.position(key).is_none() { + let node = Node { + key: key.clone(), + value, + status, + }; + match bucket.insert(node) { + bucket::InsertResult::NodeExists => unreachable!("Node must exist"), + bucket::InsertResult::Full => InsertResult::Failed(FailureReason::BucketFull), + bucket::InsertResult::TooManyIncoming => { + InsertResult::Failed(FailureReason::TooManyIncoming) + } + bucket::InsertResult::FailedFilter => { + InsertResult::Failed(FailureReason::BucketFilter) + } + bucket::InsertResult::Pending { disconnected } => { + InsertResult::Pending { disconnected } + } + bucket::InsertResult::Inserted => InsertResult::Inserted, + } + } else { + // The node exists in the bucket + // Attempt to update the status + let update_status = bucket.update_status(key, status.state, Some(status.direction)); + + if update_status.failed() { + // The node was removed from the table + return InsertResult::Failed(FailureReason::TooManyIncoming); + } + // Attempt to update the value + let update_value = bucket.update_value(key, value); + + match (update_value, update_status) { + (UpdateResult::Updated { .. }, UpdateResult::Updated) => { + InsertResult::Updated { + promoted_to_connected: false, + } + } + (UpdateResult::Updated { .. }, UpdateResult::UpdatedAndPromoted) => { + InsertResult::Updated { + promoted_to_connected: true, + } + } + (UpdateResult::Updated { .. }, UpdateResult::NotModified) + | (UpdateResult::Updated { .. }, UpdateResult::UpdatedPending) => { + InsertResult::ValueUpdated + } + (UpdateResult::NotModified, UpdateResult::Updated) => { + InsertResult::StatusUpdated { + promoted_to_connected: false, + } + } + (UpdateResult::NotModified, UpdateResult::UpdatedAndPromoted) => { + InsertResult::StatusUpdated { + promoted_to_connected: true, + } + } + (UpdateResult::NotModified, UpdateResult::NotModified) => { + InsertResult::Updated { + promoted_to_connected: false, + } + } + (UpdateResult::UpdatedPending, _) | (_, UpdateResult::UpdatedPending) => { + InsertResult::UpdatedPending + } + (UpdateResult::Failed(reason), _) => InsertResult::Failed(reason), + (_, UpdateResult::Failed(_)) => unreachable!("Status failure handled earlier."), + (UpdateResult::UpdatedAndPromoted, _) => { + unreachable!("Value update cannot promote a connection.") + } + } + } + } else { + // Cannot insert our local entry. + InsertResult::Failed(FailureReason::InvalidSelfUpdate) + } + } + + /// Removes a node from the routing table. Returns `true` of the node existed. + pub fn remove(&mut self, key: &Key) -> bool { + let index = BucketIndex::new(&self.local_key.distance(key)); + if let Some(i) = index { + let bucket = &mut self.buckets[i.get()]; + if let Some(applied) = bucket.apply_pending() { + self.applied_pending.push_back(applied) + } + bucket.remove(key) + } else { + false + } + } + + /// Returns an `Entry` for the given key, representing the state of the entry + /// in the routing table. + /// NOTE: This must be used with caution. Modifying values manually can bypass the internal + /// table filters and ingoing/outgoing limits. + pub fn entry<'a>(&'a mut self, key: &'a Key) -> Entry<'a, TNodeId, TVal> { + let index = BucketIndex::new(&self.local_key.distance(key)); + if let Some(i) = index { + let bucket = &mut self.buckets[i.get()]; + if let Some(applied) = bucket.apply_pending() { + self.applied_pending.push_back(applied) + } + Entry::new(bucket, key) + } else { + Entry::SelfEntry + } + } + + /// Returns an iterator over all the entries in the routing table. + pub fn iter(&mut self) -> impl Iterator> { + let applied_pending = &mut self.applied_pending; + self.buckets.iter_mut().flat_map(move |table| { + if let Some(applied) = table.apply_pending() { + applied_pending.push_back(applied) + } + table.iter().map(move |n| EntryRefView { + node: NodeRefView { + key: &n.key, + value: &n.value, + }, + status: n.status, + }) + }) + } + + /// Returns an iterator over all the entries in the routing table to give to a table filter. + /// + /// This differs from the regular iterator as it doesn't take ownership of self and doesn't try + /// to apply any pending nodes. + fn table_iter(&self) -> impl Iterator { + self.buckets + .iter() + .flat_map(move |table| table.iter().map(|n| &n.value)) + } + + /// Returns an iterator over all the entries in the routing table. + /// Does not add pending node to kbucket to get an iterator which + /// takes a reference instead of a mutable reference. + pub fn iter_ref(&self) -> impl Iterator> { + self.buckets.iter().flat_map(move |table| { + table.iter().map(move |n| EntryRefView { + node: NodeRefView { + key: &n.key, + value: &n.value, + }, + status: n.status, + }) + }) + } + + /// Consumes the next applied pending entry, if any. + /// + /// When an entry is attempted to be inserted and the respective bucket is full, + /// it may be recorded as pending insertion after a timeout, see [`InsertResult::Pending`]. + /// + /// If the oldest currently disconnected entry in the respective bucket does not change + /// its status until the timeout of pending entry expires, it is evicted and + /// the pending entry inserted instead. These insertions of pending entries + /// happens lazily, whenever the `KBucketsTable` is accessed, and the corresponding + /// buckets are updated accordingly. The fact that a pending entry was applied is + /// recorded in the `KBucketsTable` in the form of `AppliedPending` results, which must be + /// consumed by calling this function. + pub fn take_applied_pending(&mut self) -> Option> { + self.applied_pending.pop_front() + } + + /// Returns an iterator over the keys that are contained in a kbucket, specified by a log2 distance. + pub fn nodes_by_distances( + &mut self, + log2_distances: &[u64], + max_nodes: usize, + ) -> Vec> { + // Filter log2 distances to only include those in the closed interval [1, 256] + let distances = log2_distances + .iter() + .filter_map(|&d| { + if d > 0 && d <= (NUM_BUCKETS as u64) { + Some(d) + } else { + None + } + }) + .collect::>(); + + // Apply pending nodes + for distance in &distances { + // The log2 distance ranges from 1-256 and is always 1 more than the bucket index. For this + // reason we subtract 1 from log2 distance to get the correct bucket index. + let bucket = &mut self.buckets[(distance - 1) as usize]; + if let Some(applied) = bucket.apply_pending() { + self.applied_pending.push_back(applied) + } + } + + // Find the matching nodes + let mut matching_nodes = Vec::new(); + + // Note we search via distance in order + for distance in distances { + let bucket = &self.buckets[(distance - 1) as usize]; + for node in bucket.iter().map(|n| { + let node = NodeRefView { + key: &n.key, + value: &n.value, + }; + EntryRefView { + node, + status: n.status, + } + }) { + matching_nodes.push(node); + // Exit early if we have found enough nodes + if matching_nodes.len() >= max_nodes { + return matching_nodes; + } + } + } + matching_nodes + } + + /// Returns an iterator over the keys closest to `target`, ordered by + /// increasing distance. + pub fn closest_keys<'a, T>( + &'a mut self, + target: &'a Key, + ) -> impl Iterator> + 'a + where + T: Clone, + { + let distance = self.local_key.distance(target); + ClosestIter { + target, + iter: None, + table: self, + buckets_iter: ClosestBucketsIter::new(distance), + fmap: |b: &KBucket| -> ArrayVec<_, MAX_NODES_PER_BUCKET> { + b.iter().map(|n| n.key.clone()).collect() + }, + } + } + + /// Returns an iterator over the keys closest to `target`, ordered by + /// increasing distance. + pub fn closest_values<'a, T>( + &'a mut self, + target: &'a Key, + ) -> impl Iterator> + 'a + where + T: Clone, + TVal: Clone, + { + let distance = self.local_key.distance(target); + ClosestIter { + target, + iter: None, + table: self, + buckets_iter: ClosestBucketsIter::new(distance), + fmap: |b: &KBucket| -> ArrayVec<_, MAX_NODES_PER_BUCKET> { + b.iter() + .map(|n| ClosestValue { + key: n.key.clone(), + value: n.value.clone(), + }) + .collect() + }, + } + } + + /// Returns an iterator over the keys closest to `target`, ordered by + /// increasing distance specifying which keys agree with a value predicate. + pub fn closest_values_predicate<'a, T, F>( + &'a mut self, + target: &'a Key, + predicate: F, + ) -> impl Iterator> + 'a + where + T: Clone, + F: Fn(&TVal) -> bool + 'a, + TVal: Clone, + { + let distance = self.local_key.distance(target); + ClosestIter { + target, + iter: None, + table: self, + buckets_iter: ClosestBucketsIter::new(distance), + fmap: move |b: &KBucket| -> ArrayVec<_, MAX_NODES_PER_BUCKET> { + b.iter() + .map(|n| PredicateValue { + key: n.key.clone(), + predicate_match: predicate(&n.value), + value: n.value.clone(), + }) + .collect() + }, + } + } + + /// Returns a reference to a bucket given the key. Returns None if bucket does not exist. + pub fn get_bucket<'a>(&'a self, key: &Key) -> Option<&'a KBucket> { + let index = BucketIndex::new(&self.local_key.distance(key)); + if let Some(i) = index { + let bucket = &self.buckets[i.get()]; + Some(bucket) + } else { + None + } + } +} + +/// An iterator over (some projection of) the closest entries in a +/// `KBucketsTable` w.r.t. some target `Key`. +struct ClosestIter<'a, TTarget, TNodeId, TVal: Eq, TMap, TOut> { + /// A reference to the target key whose distance to the local key determines + /// the order in which the buckets are traversed. The resulting + /// array from projecting the entries of each bucket using `fmap` is + /// sorted according to the distance to the target. + target: &'a Key, + /// A reference to all buckets of the `KBucketsTable`. + table: &'a mut KBucketsTable, + /// The iterator over the bucket indices in the order determined by the + /// distance of the local key to the target. + buckets_iter: ClosestBucketsIter, + /// The iterator over the entries in the currently traversed bucket. + iter: Option>, + /// The projection function / mapping applied on each bucket as + /// it is encountered, producing the next `iter`ator. + fmap: TMap, +} + +/// An iterator over the bucket indices, in the order determined by the `Distance` of +/// a target from the `local_key`, such that the entries in the buckets are incrementally +/// further away from the target, starting with the bucket covering the target. +struct ClosestBucketsIter { + /// The distance to the `local_key`. + distance: Distance, + /// The current state of the iterator. + state: ClosestBucketsIterState, +} + +/// Operating states of a `ClosestBucketsIter`. +enum ClosestBucketsIterState { + /// The starting state of the iterator yields the first bucket index and + /// then transitions to `ZoomIn`. + Start(BucketIndex), + /// The iterator "zooms in" to to yield the next bucket containing nodes that + /// are incrementally closer to the local node but further from the `target`. + /// These buckets are identified by a `1` in the corresponding bit position + /// of the distance bit string. When bucket `0` is reached, the iterator + /// transitions to `ZoomOut`. + ZoomIn(BucketIndex), + /// Once bucket `0` has been reached, the iterator starts "zooming out" + /// to buckets containing nodes that are incrementally further away from + /// both the local key and the target. These are identified by a `0` in + /// the corresponding bit position of the distance bit string. When bucket + /// `255` is reached, the iterator transitions to state `Done`. + ZoomOut(BucketIndex), + /// The iterator is in this state once it has visited all buckets. + Done, +} + +impl ClosestBucketsIter { + fn new(distance: Distance) -> Self { + let state = match BucketIndex::new(&distance) { + Some(i) => ClosestBucketsIterState::Start(i), + None => ClosestBucketsIterState::Done, + }; + Self { distance, state } + } + + fn next_in(&self, i: BucketIndex) -> Option { + (0..i.get()).rev().find_map(|i| { + if self.distance.0.bit(i) { + Some(BucketIndex(i)) + } else { + None + } + }) + } + + fn next_out(&self, i: BucketIndex) -> Option { + (i.get() + 1..NUM_BUCKETS).find_map(|i| { + if !self.distance.0.bit(i) { + Some(BucketIndex(i)) + } else { + None + } + }) + } +} + +impl Iterator for ClosestBucketsIter { + type Item = BucketIndex; + + fn next(&mut self) -> Option { + match self.state { + ClosestBucketsIterState::Start(i) => { + self.state = ClosestBucketsIterState::ZoomIn(i); + Some(i) + } + ClosestBucketsIterState::ZoomIn(i) => { + if let Some(i) = self.next_in(i) { + self.state = ClosestBucketsIterState::ZoomIn(i); + Some(i) + } else { + let i = BucketIndex(0); + self.state = ClosestBucketsIterState::ZoomOut(i); + Some(i) + } + } + ClosestBucketsIterState::ZoomOut(i) => { + if let Some(i) = self.next_out(i) { + self.state = ClosestBucketsIterState::ZoomOut(i); + Some(i) + } else { + self.state = ClosestBucketsIterState::Done; + None + } + } + ClosestBucketsIterState::Done => None, + } + } +} + +impl Iterator + for ClosestIter<'_, TTarget, TNodeId, TVal, TMap, TOut> +where + TNodeId: Clone, + TVal: Eq, + TMap: Fn(&KBucket) -> ArrayVec, + TOut: AsRef>, +{ + type Item = TOut; + + fn next(&mut self) -> Option { + loop { + match &mut self.iter { + Some(iter) => match iter.next() { + Some(k) => return Some(k), + None => self.iter = None, + }, + None => { + if let Some(i) = self.buckets_iter.next() { + let bucket = &mut self.table.buckets[i.get()]; + if let Some(applied) = bucket.apply_pending() { + self.table.applied_pending.push_back(applied) + } + let mut v = (self.fmap)(bucket); + v.sort_by(|a, b| { + self.target + .distance(a.as_ref()) + .cmp(&self.target.distance(b.as_ref())) + }); + self.iter = Some(v.into_iter()); + } else { + return None; + } + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::{bucket::InsertResult as BucketInsertResult, *}; + use enr::NodeId; + + fn connected_state() -> NodeStatus { + NodeStatus { + state: ConnectionState::Connected, + direction: ConnectionDirection::Outgoing, + } + } + + fn disconnected_state() -> NodeStatus { + NodeStatus { + state: ConnectionState::Disconnected, + direction: ConnectionDirection::Outgoing, + } + } + + #[test] + fn basic_closest() { + let local_key = Key::from(NodeId::random()); + let other_id = Key::from(NodeId::random()); + + let mut table = KBucketsTable::<_, ()>::new( + local_key, + Duration::from_secs(5), + MAX_NODES_PER_BUCKET, + None, + None, + ); + if let Entry::Absent(entry) = table.entry(&other_id) { + match entry.insert((), connected_state()) { + BucketInsertResult::Inserted => (), + _ => panic!(), + } + } else { + panic!() + } + + let res = table.closest_keys(&other_id).collect::>(); + assert_eq!(res.len(), 1); + assert_eq!(res[0], other_id); + } + + #[test] + fn update_local_id_fails() { + let local_key = Key::from(NodeId::random()); + let mut table = KBucketsTable::<_, ()>::new( + local_key.clone(), + Duration::from_secs(5), + MAX_NODES_PER_BUCKET, + None, + None, + ); + match table.entry(&local_key) { + Entry::SelfEntry => (), + _ => panic!(), + } + } + + #[test] + fn closest() { + let local_key = Key::from(NodeId::random()); + let mut table = KBucketsTable::<_, ()>::new( + local_key, + Duration::from_secs(5), + MAX_NODES_PER_BUCKET, + None, + None, + ); + let mut count = 0; + loop { + if count == 100 { + break; + } + let key = Key::from(NodeId::random()); + if let Entry::Absent(e) = table.entry(&key) { + match e.insert((), connected_state()) { + BucketInsertResult::Inserted => count += 1, + _ => continue, + } + } else { + panic!("entry exists") + } + } + + let mut expected_keys: Vec<_> = table + .buckets + .iter() + .flat_map(|t| t.iter().map(|n| n.key.clone())) + .collect(); + + for _ in 0..10 { + let target_key = Key::from(NodeId::random()); + let keys = table.closest_keys(&target_key).collect::>(); + // The list of keys is expected to match the result of a full-table scan. + expected_keys.sort_by_key(|k| k.distance(&target_key)); + assert_eq!(keys, expected_keys); + } + } + + #[test] + fn applied_pending() { + let local_key = Key::from(NodeId::random()); + let mut table = KBucketsTable::<_, ()>::new( + local_key.clone(), + Duration::from_millis(1), + MAX_NODES_PER_BUCKET, + None, + None, + ); + let expected_applied; + let full_bucket_index; + loop { + let key = Key::from(NodeId::random()); + if let Entry::Absent(e) = table.entry(&key) { + match e.insert((), disconnected_state()) { + BucketInsertResult::Full => { + if let Entry::Absent(e) = table.entry(&key) { + match e.insert((), connected_state()) { + BucketInsertResult::Pending { disconnected } => { + expected_applied = AppliedPending { + inserted: key.clone(), + evicted: Some(Node { + key: disconnected, + value: (), + status: disconnected_state(), + }), + }; + full_bucket_index = BucketIndex::new(&key.distance(&local_key)); + break; + } + _ => panic!(), + } + } else { + panic!() + } + } + _ => continue, + } + } else { + panic!("entry exists") + } + } + + // Expire the timeout for the pending entry on the full bucket.` + let full_bucket = &mut table.buckets[full_bucket_index.unwrap().get()]; + let elapsed = Instant::now() - Duration::from_secs(1); + full_bucket.pending_mut().unwrap().set_ready_at(elapsed); + + match table.entry(&expected_applied.inserted) { + Entry::Present( + _, + NodeStatus { + state: ConnectionState::Connected, + direction: _direction, + }, + ) => {} + x => panic!("Unexpected entry: {:?}", x), + } + + match table.entry(&expected_applied.evicted.as_ref().unwrap().key) { + Entry::Absent(_) => {} + x => panic!("Unexpected entry: {:?}", x), + } + + assert_eq!(Some(expected_applied), table.take_applied_pending()); + assert_eq!(None, table.take_applied_pending()); + } +} diff --git a/version-meld/discv5/src/kbucket/bucket.rs b/version-meld/discv5/src/kbucket/bucket.rs new file mode 100644 index 0000000..94d12e1 --- /dev/null +++ b/version-meld/discv5/src/kbucket/bucket.rs @@ -0,0 +1,1439 @@ +// Copyright 2019 Parity Technologies (UK) Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// This basis of this file has been taken from the rust-libp2p codebase: +// https://github.com/libp2p/rust-libp2p + +//! The internal API for a single `KBucket` in a `KBucketsTable`. +//! +//! > **Note**: Uniqueness of entries w.r.t. a `Key` in a `KBucket` is not +//! > checked in this module. This is an invariant that must hold across all +//! > buckets in a `KBucketsTable` and hence is enforced by the public API +//! > of the `KBucketsTable` and in particular the public `Entry` API. + +#![allow(dead_code)] + +use super::*; +use tracing::{debug, error}; + +/// Maximum number of nodes in a bucket, i.e. the (fixed) `k` parameter. +pub const MAX_NODES_PER_BUCKET: usize = 16; + +/// A `PendingNode` is a `Node` that is pending insertion into a `KBucket`. +#[derive(Debug, Clone)] +pub struct PendingNode { + /// The pending node to insert. + node: Node, + + /// The instant at which the pending node is eligible for insertion into a bucket. + replace: Instant, +} + +/// The status of a node in a bucket. +/// +/// The status of a node in a bucket together with the time of the +/// last status change determines the position of the node in a +/// bucket. +#[derive(PartialEq, Eq, Debug, Copy, Clone)] +pub struct NodeStatus { + /// The direction (incoming or outgoing) for the node. If in the disconnected state, this + /// represents the last connection status. + pub direction: ConnectionDirection, + /// The connection state, connected or disconnected. + pub state: ConnectionState, +} + +/// The connection state of a node. +#[derive(PartialEq, Eq, Debug, Copy, Clone)] +pub enum ConnectionState { + /// The node is connected. + Connected, + /// The node is considered disconnected. + Disconnected, +} + +impl NodeStatus { + pub fn is_connected(&self) -> bool { + match self.state { + ConnectionState::Connected => true, + ConnectionState::Disconnected => false, + } + } + + pub fn is_incoming(&self) -> bool { + match self.direction { + ConnectionDirection::Outgoing => false, + ConnectionDirection::Incoming => true, + } + } +} + +impl PendingNode { + pub fn status(&self) -> NodeStatus { + self.node.status + } + + pub fn value_mut(&mut self) -> &mut TVal { + &mut self.node.value + } + + pub fn set_ready_at(&mut self, t: Instant) { + self.replace = t; + } +} + +/// A `Node` in a bucket, representing a peer participating +/// in the Kademlia DHT together with an associated value (e.g. contact +/// information). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Node { + /// The key of the node, identifying the peer. + pub key: Key, + /// The associated value. + pub value: TVal, + /// The status of the node. + pub status: NodeStatus, +} + +/// The position of a node in a `KBucket`, i.e. a non-negative integer +/// in the range `[0, MAX_NODES_PER_BUCKET)`. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Position(usize); + +/// A `KBucket` is a list of up to `MAX_NODES_PER_BUCKET` `Key`s and associated values, +/// ordered from least-recently connected to most-recently connected. +#[derive(Clone)] +pub struct KBucket { + /// The nodes contained in the bucket. + nodes: ArrayVec, MAX_NODES_PER_BUCKET>, + + /// The position (index) in `nodes` that marks the first connected node. + /// + /// Since the entries in `nodes` are ordered from least-recently connected to + /// most-recently connected, all entries above this index are also considered + /// connected, i.e. the range `[0, first_connected_pos)` marks the sub-list of entries + /// that are considered disconnected and the range + /// `[first_connected_pos, MAX_NODES_PER_BUCKET)` marks sub-list of entries that are + /// considered connected. + /// + /// `None` indicates that there are no connected entries in the bucket, i.e. + /// the bucket is either empty, or contains only entries for peers that are + /// considered disconnected. + first_connected_pos: Option, + + /// A node that is pending to be inserted into a full bucket, should the + /// least-recently connected (and currently disconnected) node not be + /// marked as connected within `unresponsive_timeout`. + pending: Option>, + + /// The timeout window before a new pending node is eligible for insertion, + /// if the least-recently connected node is not updated as being connected + /// in the meantime. + pending_timeout: Duration, + + /// An optional filter that filters new entries given an iterator over current entries in + /// the bucket. + filter: Option>>, + + /// The maximum number of incoming connections allowed per bucket. Setting this to + /// MAX_NODES_PER_BUCKET means there is no restriction on incoming nodes. + max_incoming: usize, +} + +/// The result of inserting an entry into a bucket. +#[must_use] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum InsertResult { + /// The entry has been successfully inserted. + Inserted, + /// The entry is pending insertion because the relevant bucket is currently full. + /// The entry is inserted after a timeout elapsed, if the status of the + /// least-recently connected (and currently disconnected) node in the bucket + /// is not updated before the timeout expires. + Pending { + /// The key of the least-recently connected entry that is currently considered + /// disconnected and whose corresponding peer should be checked for connectivity + /// in order to prevent it from being evicted. If connectivity to the peer is + /// re-established, the corresponding entry should be updated with a connected status. + disconnected: Key, + }, + /// The attempted entry failed to pass the filter. + FailedFilter, + /// There were too many incoming nodes for this bucket. + TooManyIncoming, + /// The entry was not inserted because the relevant bucket is full. + Full, + /// The entry already exists. + NodeExists, +} + +/// The result of performing an update on a kbucket/table. +#[must_use] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum UpdateResult { + /// The node was updated successfully, + Updated, + /// The update promoted the node to a connected state from a disconnected state. + UpdatedAndPromoted, + /// The pending entry was updated. + UpdatedPending, + /// The update removed the node because it would violate the incoming peers condition. + Failed(FailureReason), + /// There were no changes made to the value of the node. + NotModified, +} + +impl UpdateResult { + // The update failed. + pub fn failed(&self) -> bool { + matches!(self, UpdateResult::Failed(_)) + } +} + +/// A reason for failing to update or insert a node into the bucket. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FailureReason { + /// Too many incoming nodes already in the bucket. + TooManyIncoming, + /// The node didn't pass the bucket filter. + BucketFilter, + /// The node didn't pass the table filter. + TableFilter, + /// The node didn't exist. + KeyNonExistant, + /// The bucket was full. + BucketFull, + /// Cannot update self, + InvalidSelfUpdate, +} + +/// The result of applying a pending node to a bucket, possibly +/// replacing an existing node. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AppliedPending { + /// The key of the inserted pending node. + pub inserted: Key, + /// The node that has been evicted from the bucket to make room for the + /// pending node, if any. + pub evicted: Option>, +} + +impl KBucket +where + TNodeId: Clone, + TVal: Eq, +{ + /// Creates a new `KBucket` with the given timeout for pending entries. + pub fn new( + pending_timeout: Duration, + max_incoming: usize, + filter: Option>>, + ) -> Self { + KBucket { + nodes: ArrayVec::new(), + first_connected_pos: None, + pending: None, + pending_timeout, + filter, + max_incoming, + } + } + + /// Returns a reference to the pending node of the bucket, if there is any. + pub fn pending(&self) -> Option<&PendingNode> { + self.pending.as_ref() + } + + /// Returns a mutable reference to the pending node of the bucket, if there is any. + pub fn pending_mut(&mut self) -> Option<&mut PendingNode> { + self.pending.as_mut() + } + + /// Returns a reference to the pending node of the bucket, if there is any + /// with a matching key. + pub fn as_pending(&self, key: &Key) -> Option<&PendingNode> { + self.pending().filter(|p| &p.node.key == key) + } + + /// Returns an iterator over the nodes in the bucket, together with their status. + pub fn iter(&self) -> impl Iterator> { + self.nodes.iter() + } + + /// Inserts the pending node into the bucket, if its timeout has elapsed, + /// replacing the least-recently connected node. + /// + /// If a pending node has been inserted, its key is returned together with + /// the node that was replaced. `None` indicates that the nodes in the + /// bucket remained unchanged. + pub fn apply_pending(&mut self) -> Option> { + if let Some(pending) = self.pending.take() { + if pending.replace <= Instant::now() { + // Check if the bucket is full + if self.nodes.is_full() { + // Apply bucket filters + + if self.nodes[0].status.is_connected() { + // The bucket is full with connected nodes. Drop the pending node. + return None; + } + // Check the custom filter + if let Some(filter) = self.filter.as_ref() { + if !filter.filter( + &pending.node.value, + &mut self.iter().map(|node| &node.value), + ) { + // The pending node doesn't satisfy the bucket filter. Drop the pending + // node. + return None; + } + } + // Check the incoming node restriction + if pending.status().is_connected() && pending.status().is_incoming() { + // Make sure this doesn't violate the incoming conditions + if self.is_max_incoming() { + // The pending node doesn't satisfy the incoming/outgoing limits. Drop + // the pending node. + return None; + } + } + + // The pending node will be inserted. + let inserted = pending.node.key.clone(); + // A connected pending node goes at the end of the list for + // the connected peers, removing the least-recently connected. + if pending.status().is_connected() { + let evicted = Some(self.nodes.remove(0)); + self.first_connected_pos = self + .first_connected_pos + .map_or_else(|| Some(self.nodes.len()), |p| p.checked_sub(1)); + self.nodes.push(pending.node); + return Some(AppliedPending { inserted, evicted }); + } + // A disconnected pending node goes at the end of the list + // for the disconnected peers. + else if let Some(p) = self.first_connected_pos { + if let Some(insert_pos) = p.checked_sub(1) { + let evicted = Some(self.nodes.remove(0)); + self.nodes.insert(insert_pos, pending.node); + return Some(AppliedPending { inserted, evicted }); + } + } else { + // All nodes are disconnected. Insert the new node as the most + // recently disconnected, removing the least-recently disconnected. + let evicted = Some(self.nodes.remove(0)); + self.nodes.push(pending.node); + return Some(AppliedPending { inserted, evicted }); + } + } else { + // There is room in the bucket, so just insert the pending node. + let inserted = pending.node.key.clone(); + match self.insert(pending.node) { + InsertResult::Inserted => { + return Some(AppliedPending { + inserted, + evicted: None, + }) + } + InsertResult::Full => unreachable!("Bucket cannot be full"), + InsertResult::Pending { .. } | InsertResult::NodeExists => { + error!("Bucket is not full or double node") + } + InsertResult::FailedFilter => debug!("Pending node failed filter"), + InsertResult::TooManyIncoming => { + debug!("Pending node failed incoming filter") + } + } + } + } else { + self.pending = Some(pending); + } + } + + None + } + + /// Updates the status of the pending node, if any. + pub fn update_pending(&mut self, status: NodeStatus) { + if let Some(pending) = &mut self.pending { + pending.node.status = status + } + } + + /// Updates the status of the node referred to by the given key, if it is + /// in the bucket. If the node is not in the bucket, or the update would violate a bucket + /// filter or incoming limits, returns an update result indicating the outcome. + /// An optional connection state can be given. If this is omitted the connection state will not + /// be modified. + pub fn update_status( + &mut self, + key: &Key, + state: ConnectionState, + direction: Option, + ) -> UpdateResult { + // Remove the node from its current position and then reinsert it + // with the desired status, which puts it at the end of either the + // prefix list of disconnected nodes or the suffix list of connected + // nodes (i.e. most-recently disconnected or most-recently connected, + // respectively). + if let Some(pos) = self.position(key) { + // Remove the node from its current position. + let mut node = self.nodes.remove(pos.0); + let old_status = node.status; + node.status.state = state; + if let Some(direction) = direction { + node.status.direction = direction; + } + + // Flag indicating if this update modified the entry. + let not_modified = old_status == node.status; + // Flag indicating we are upgrading to a connected status + let is_connected = matches!(state, ConnectionState::Connected); + + // Adjust `first_connected_pos` accordingly. + match old_status.state { + ConnectionState::Connected => { + if self.first_connected_pos.map_or(false, |p| p == pos.0) + && pos.0 == self.nodes.len() + { + // It was the last connected node. + self.first_connected_pos = None + } + } + ConnectionState::Disconnected => { + self.first_connected_pos = + self.first_connected_pos.and_then(|p| p.checked_sub(1)) + } + } + // If the least-recently connected node re-establishes its + // connected status, drop the pending node. + if pos == Position(0) && is_connected { + self.pending = None + } + // Reinsert the node with the desired status. + match self.insert(node) { + InsertResult::Inserted => { + if not_modified { + UpdateResult::NotModified + } else if !old_status.is_connected() && is_connected { + // This means the status was updated from a disconnected state to connected + // state + UpdateResult::UpdatedAndPromoted + } else { + UpdateResult::Updated + } + } + InsertResult::TooManyIncoming => { + UpdateResult::Failed(FailureReason::TooManyIncoming) + } + // Node could not be inserted. None of these should be possible. + InsertResult::FailedFilter => { + // If the filter is non-deterministic, potentially a re-insertion of the same + // node can fail the filter. + UpdateResult::Failed(FailureReason::BucketFilter) + } + InsertResult::NodeExists => { + unreachable!("The node was removed and shouldn't already exist") + } + InsertResult::Full => { + unreachable!("The node was removed so the bucket cannot be full") + } + InsertResult::Pending { .. } => { + unreachable!("The node was removed so can't be added as pending") + } + } + } else if let Some(pending) = &mut self.pending { + if &pending.node.key == key { + pending.node.status.state = state; + if let Some(direction) = direction { + pending.node.status.direction = direction; + } + UpdateResult::UpdatedPending + } else { + UpdateResult::Failed(FailureReason::KeyNonExistant) + } + } else { + UpdateResult::Failed(FailureReason::KeyNonExistant) + } + } + + /// Updates the value of the node referred to by the given key, if it is + /// in the bucket. If the node is not in the bucket, or the update would violate a bucket + /// filter or incoming limits, returns false and removes the node from the bucket. + /// NOTE: This does not update the position of the node in the table. It node will be removed + /// if it fails the filter however. + pub fn update_value(&mut self, key: &Key, value: TVal) -> UpdateResult { + // Remove the node from its current position, check the filter and add it back in. + if let Some(Position(pos)) = self.position(key) { + // Remove the node from its current position. + let mut node = self.nodes.remove(pos); + if node.value == value { + self.nodes.insert(pos, node); + UpdateResult::NotModified + } else { + // Check bucket filter + if let Some(filter) = self.filter.as_ref() { + if !filter.filter(&value, &mut self.iter().map(|node| &node.value)) { + // Node is removed, update the `first_connected_pos` accordingly. + self.update_first_connected_pos_for_removal(pos); + + return UpdateResult::Failed(FailureReason::BucketFilter); + } + } + node.value = value; + self.nodes.insert(pos, node); + UpdateResult::Updated + } + } else if let Some(pending) = &mut self.pending { + if &pending.node.key == key { + pending.node.value = value; + UpdateResult::UpdatedPending + } else { + UpdateResult::Failed(FailureReason::KeyNonExistant) + } + } else { + UpdateResult::Failed(FailureReason::KeyNonExistant) + } + } + + /// Inserts a new node into the bucket with the given status. + /// + /// The status of the node to insert determines the result as follows: + /// + /// * [`ConnectionState::Connected`] for both directions: If the bucket is full and either all nodes are connected + /// or there is already a pending node, insertion fails with [`InsertResult::Full`]. + /// If the bucket is full but at least one node is disconnected and there is no pending + /// node, the new node is inserted as pending, yielding [`InsertResult::Pending`]. + /// Otherwise the bucket has free slots and the new node is added to the end of the + /// bucket as the most-recently connected node. + /// + /// * [`ConnectionState::Disconnected`]: If the bucket is full, insertion fails with + /// [`InsertResult::Full`]. Otherwise the bucket has free slots and the new node + /// is inserted at the position preceding the first connected node, + /// i.e. as the most-recently disconnected node. If there are no connected nodes, + /// the new node is added as the last element of the bucket. + /// + /// The insert can fail if a provided bucket filter does not pass. If a node is attempted + /// to be inserted that doesn't pass the bucket filter, [`InsertResult::FailedFilter`] will be + /// returned. Similarly, if the inserted node would violate the `max_incoming` value, the + /// result will return [`InsertResult::TooManyIncoming`]. + pub fn insert(&mut self, node: Node) -> InsertResult { + // Prevent inserting duplicate nodes. + if self.position(&node.key).is_some() { + return InsertResult::NodeExists; + } + + // check bucket filter + if let Some(filter) = self.filter.as_ref() { + if !filter.filter(&node.value, &mut self.iter().map(|node| &node.value)) { + return InsertResult::FailedFilter; + } + } + + let inserting_pending = self + .pending + .as_ref() + .map(|pending| pending.node.key == node.key) + .unwrap_or_default(); + + let insert_result = match node.status.state { + ConnectionState::Connected => { + if node.status.is_incoming() { + // check the maximum counter + if self.is_max_incoming() { + return InsertResult::TooManyIncoming; + } + } + if self.nodes.is_full() { + if self.first_connected_pos == Some(0) || self.pending.is_some() { + return InsertResult::Full; + } else { + self.pending = Some(PendingNode { + node, + replace: Instant::now() + self.pending_timeout, + }); + return InsertResult::Pending { + disconnected: self.nodes[0].key.clone(), + }; + } + } + + let pos = self.nodes.len(); + self.first_connected_pos = self.first_connected_pos.or(Some(pos)); + self.nodes.push(node); + InsertResult::Inserted + } + ConnectionState::Disconnected => { + if self.nodes.is_full() { + return InsertResult::Full; + } + + if let Some(ref mut first_connected_pos) = self.first_connected_pos { + self.nodes.insert(*first_connected_pos, node); + *first_connected_pos += 1; + } else { + self.nodes.push(node); + } + InsertResult::Inserted + } + }; + + // If we inserted the node, make sure there is no pending node of the same key. This can + // happen when a pending node is inserted, a node gets removed from the bucket, freeing up + // space and then re-inserted here. + if matches!(insert_result, InsertResult::Inserted) && inserting_pending { + self.pending = None + } + insert_result + } + + /// Removes a node from the bucket. + pub fn remove(&mut self, key: &Key) -> bool { + if let Some(Position(position)) = self.position(key) { + self.nodes.remove(position); + self.update_first_connected_pos_for_removal(position); + self.apply_pending(); + true + } else { + false + } + } + + /// Gets the number of entries currently in the bucket. + pub fn num_entries(&self) -> usize { + self.nodes.len() + } + + /// Gets the number of entries in the bucket that are considered connected. + pub fn num_connected(&self) -> usize { + self.first_connected_pos.map_or(0, |i| self.nodes.len() - i) + } + + /// Gets the number of entries in the bucket that are considered disconnected. + pub fn num_disconnected(&self) -> usize { + self.nodes.len() - self.num_connected() + } + + /// Gets the position of an node in the bucket. + pub fn position(&self, key: &Key) -> Option { + self.nodes.iter().position(|p| &p.key == key).map(Position) + } + + /// Returns the state of the node at the given position. + pub fn status(&self, pos: Position) -> NodeStatus { + if let Some(node) = self.nodes.get(pos.0) { + node.status + } else { + // If the node isn't in the bucket, return the worst kind of state. + NodeStatus { + state: ConnectionState::Disconnected, + direction: ConnectionDirection::Incoming, + } + } + } + + /// Gets a mutable reference to the node identified by the given key. + /// + /// Returns `None` if the given key does not refer to an node in the + /// bucket. + fn get_mut(&mut self, key: &Key) -> Option<&mut Node> { + self.nodes.iter_mut().find(move |p| &p.key == key) + } + + /// Gets a reference to the node identified by the given key. + /// + /// Returns `None` if the given key does not refer to an node in the + /// bucket. + pub fn get(&self, key: &Key) -> Option<&Node> { + self.nodes.iter().find(move |p| &p.key == key) + } + + /// Returns whether the bucket has reached its maximum capacity of incoming nodes. This is used + /// to determine if new nodes can be added to the bucket or not. + fn is_max_incoming(&self) -> bool { + self.nodes + .iter() + .filter(|node| node.status.is_connected() && node.status.is_incoming()) + .count() + >= self.max_incoming + } + + /// Update the `first_connected_pos` for the removal of a node at position `removed_pos`. + /// + /// This function should be called *after* removing the node. It has the ability to destroy + /// the bucket's internal consistency invariants if misused. + fn update_first_connected_pos_for_removal(&mut self, removed_pos: usize) { + self.first_connected_pos = self.first_connected_pos.and_then(|fcp| { + if removed_pos < fcp { + // Remove node is before the first connected position, decrement it. + Some(fcp - 1) + } else { + // FCP is unchanged, unless there are no nodes following the removed node. + Some(fcp).filter(|_| fcp < self.nodes.len()) + } + }); + } +} + +impl std::fmt::Debug + for KBucket +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut builder = f.debug_struct("KBucket"); + let _ = builder.field("nodes", &self.nodes); + let _ = builder.field("first_connected_pos", &self.first_connected_pos); + let _ = builder.field("pending", &self.pending); + let _ = builder.field("pending_timeout", &self.pending_timeout); + let _ = builder.field("filter", &self.filter.is_some()); + let _ = builder.field("max_incoming", &self.max_incoming); + builder.finish() + } +} + +impl std::fmt::Display for ConnectionDirection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self { + ConnectionDirection::Incoming => write!(f, "Incoming"), + ConnectionDirection::Outgoing => write!(f, "Outgoing"), + } + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use enr::NodeId; + use quickcheck::*; + use rand_07::Rng; + use std::{ + collections::{HashSet, VecDeque}, + hash::Hash, + }; + + fn connected_state() -> NodeStatus { + NodeStatus { + state: ConnectionState::Connected, + direction: ConnectionDirection::Outgoing, + } + } + + fn disconnected_state() -> NodeStatus { + NodeStatus { + state: ConnectionState::Disconnected, + direction: ConnectionDirection::Outgoing, + } + } + + pub fn arbitrary_node_id(g: &mut G) -> NodeId { + let mut node_id = [0u8; 32]; + g.fill_bytes(&mut node_id); + NodeId::new(&node_id) + } + + impl KBucket + where + V: Eq + std::fmt::Debug, + { + /// Check invariants that must hold on the `KBucket`. + fn check_invariants(&self) { + self.check_first_connected_pos(); + self.check_status_ordering(); + self.check_max_incoming_nodes(); + } + + /// Check that the cached `first_connected_pos` field matches the list of nodes. + fn check_first_connected_pos(&self) { + let first_connected_pos = self + .nodes + .iter() + .position(|node| node.status.is_connected()); + assert_eq!(self.first_connected_pos, first_connected_pos); + } + + /// Check that disconnected nodes are listed first, follow by connected nodes. + fn check_status_ordering(&self) { + let first_connected_pos = self.first_connected_pos.unwrap_or(self.nodes.len()); + assert!(self.nodes[..first_connected_pos] + .iter() + .all(|n| !n.status.is_connected())); + assert!(self.nodes[first_connected_pos..] + .iter() + .all(|n| n.status.is_connected())); + } + + /// Check that the limit on incoming connections is respected. + fn check_max_incoming_nodes(&self) { + let number_of_incoming_nodes = self + .nodes + .iter() + .filter(|n| n.status.is_connected() && n.status.is_incoming()) + .count(); + assert!(number_of_incoming_nodes <= self.max_incoming); + } + } + + impl Arbitrary for KBucket + where + V: Arbitrary + Eq, + { + fn arbitrary(g: &mut G) -> KBucket { + let timeout = Duration::from_secs(g.gen_range(1, g.size() as u64)); + let mut bucket = KBucket::::new(timeout, MAX_NODES_PER_BUCKET, None); + let num_nodes = g.gen_range(1, MAX_NODES_PER_BUCKET + 1); + for _ in 0..num_nodes { + loop { + let node = Node::arbitrary(g); + match bucket.insert(node) { + InsertResult::Inserted => break, + InsertResult::TooManyIncoming => {} + _ => panic!(), + } + } + } + bucket + } + } + + impl Arbitrary for Node + where + V: Arbitrary + Eq, + { + fn arbitrary(g: &mut G) -> Self { + let key = Key::from(arbitrary_node_id(g)); + Node { + key: key.clone(), + value: V::arbitrary(g), + status: NodeStatus::arbitrary(g), + } + } + } + + impl Arbitrary for NodeStatus { + fn arbitrary(g: &mut G) -> NodeStatus { + match g.gen_range(1, 4) { + 1 => NodeStatus { + direction: ConnectionDirection::Incoming, + state: ConnectionState::Connected, + }, + 2 => NodeStatus { + direction: ConnectionDirection::Outgoing, + state: ConnectionState::Connected, + }, + 3 => NodeStatus { + direction: ConnectionDirection::Incoming, + state: ConnectionState::Disconnected, + }, + 4 => NodeStatus { + direction: ConnectionDirection::Outgoing, + state: ConnectionState::Disconnected, + }, + x => unreachable!("Should not generate numbers out of this range {}", x), + } + } + } + + impl Arbitrary for Position { + fn arbitrary(g: &mut G) -> Position { + Position(g.gen_range(0, MAX_NODES_PER_BUCKET)) + } + } + + // Fill a bucket with random nodes with the given status. + fn fill_bucket(bucket: &mut KBucket, status: NodeStatus) { + let num_entries_start = bucket.num_entries(); + for i in 0..MAX_NODES_PER_BUCKET - num_entries_start { + let key = Key::from(NodeId::random()); + let node = Node { + key, + value: (), + status, + }; + assert_eq!(InsertResult::Inserted, bucket.insert(node)); + assert_eq!(bucket.num_entries(), num_entries_start + i + 1); + } + } + + /// Filter for testing that returns true if the value is in `self.set`. + #[derive(Debug, Clone)] + pub struct SetFilter { + set: HashSet, + } + + impl Filter for SetFilter + where + T: Clone + Hash + Eq + Send + Sync + 'static, + { + fn filter(&self, value: &T, _: &mut dyn Iterator) -> bool { + self.set.contains(value) + } + } + + /// Enum encoding mutable method calls on KBucket, implements Arbitrary. + #[derive(Debug, Clone)] + pub enum Action + where + TVal: Eq, + { + Insert(Node), + Remove(usize), + UpdatePending(NodeStatus), + ApplyPending, + UpdateStatus(usize, NodeStatus), + UpdateValue(usize, TVal), + } + + impl Arbitrary for Action + where + V: Arbitrary + Eq, + { + fn arbitrary(g: &mut G) -> Self { + match g.gen_range(0, 6) { + 0 => Action::Insert(<_>::arbitrary(g)), + 1 => Action::Remove(<_>::arbitrary(g)), + 2 => Action::UpdatePending(<_>::arbitrary(g)), + 3 => Action::ApplyPending, + 4 => Action::UpdateStatus(<_>::arbitrary(g), <_>::arbitrary(g)), + 5 => Action::UpdateValue(<_>::arbitrary(g), <_>::arbitrary(g)), + _ => panic!("wrong number of action variants"), + } + } + } + + impl KBucket + where + V: Eq + std::fmt::Debug, + { + fn apply_action(&mut self, action: Action) { + match action { + Action::Insert(node) => { + let _ = self.insert(node); + } + Action::Remove(pos) => { + if let Some(key) = self.key_of_pos(pos) { + self.remove(&key); + } + } + Action::UpdatePending(status) => { + self.update_pending(status); + } + Action::ApplyPending => { + self.apply_pending(); + } + Action::UpdateStatus(pos, status) => { + if let Some(key) = self.key_of_pos(pos) { + let _ = self.update_status(&key, status.state, Some(status.direction)); + } + } + Action::UpdateValue(pos, value) => { + if let Some(key) = self.key_of_pos(pos) { + let _ = self.update_value(&key, value); + } + } + } + } + + fn key_of_pos(&self, pos: usize) -> Option> { + let num_nodes = self.num_entries(); + if num_nodes > 0 { + let pos = pos % num_nodes; + let key = self.nodes[pos].key.clone(); + Some(key) + } else { + None + } + } + } + + #[test] + fn ordering() { + fn prop(status: Vec) -> bool { + let mut bucket = + KBucket::::new(Duration::from_secs(1), MAX_NODES_PER_BUCKET, None); + + // The expected lists of connected and disconnected nodes. + let mut connected = VecDeque::new(); + let mut disconnected = VecDeque::new(); + + // Fill the bucket, thereby populating the expected lists in insertion order. + for status in status { + let key = Key::from(NodeId::random()); + let node = Node { + key: key.clone(), + value: (), + status, + }; + let full = bucket.num_entries() == MAX_NODES_PER_BUCKET; + if let InsertResult::Inserted = bucket.insert(node) { + let vec = if status.is_connected() { + &mut connected + } else { + &mut disconnected + }; + if full { + vec.pop_front(); + } + vec.push_back((status, key.clone())); + } + } + + // Get all nodes from the bucket, together with their status. + let mut nodes = bucket + .iter() + .map(|n| (n.status, n.key.clone())) + .collect::>(); + + // Split the list of nodes at the first connected node. + let first_connected_pos = nodes.iter().position(|(status, _)| status.is_connected()); + assert_eq!(bucket.first_connected_pos, first_connected_pos); + let tail = first_connected_pos.map_or(Vec::new(), |p| nodes.split_off(p)); + + // All nodes before the first connected node must be disconnected and + // in insertion order. Similarly, all remaining nodes must be connected + // and in insertion order. + disconnected == nodes && connected == tail + } + + quickcheck(prop as fn(_) -> _); + } + + #[test] + fn full_bucket() { + let mut bucket = + KBucket::::new(Duration::from_secs(1), MAX_NODES_PER_BUCKET, None); + + let disconnected_status = NodeStatus { + state: ConnectionState::Disconnected, + direction: ConnectionDirection::Outgoing, + }; + // Fill the bucket with disconnected nodes. + fill_bucket(&mut bucket, disconnected_status); + + // Trying to insert another disconnected node fails. + let key = Key::from(NodeId::random()); + let node = Node { + key, + value: (), + status: disconnected_status, + }; + match bucket.insert(node) { + InsertResult::Full => {} + x => panic!("{:?}", x), + } + + // One-by-one fill the bucket with connected nodes, replacing the disconnected ones. + for i in 0..MAX_NODES_PER_BUCKET { + let first = bucket.iter().next().unwrap(); + let first_disconnected = first.clone(); + assert_eq!(first.status, disconnected_status); + + // Add a connected node, which is expected to be pending, scheduled to + // replace the first (i.e. least-recently connected) node. + let key = Key::from(NodeId::random()); + let node = Node { + key: key.clone(), + value: (), + status: connected_state(), + }; + match bucket.insert(node.clone()) { + InsertResult::Pending { disconnected } => { + assert_eq!(disconnected, first_disconnected.key) + } + x => panic!("{:?}", x), + } + + // Trying to insert another connected node fails. + match bucket.insert(node.clone()) { + InsertResult::Full => {} + x => panic!("{:?}", x), + } + + assert!(bucket.pending().is_some()); + + // Apply the pending node. + let pending = bucket.pending_mut().expect("No pending node."); + pending.set_ready_at(Instant::now() - Duration::from_secs(1)); + let result = bucket.apply_pending(); + assert_eq!( + result, + Some(AppliedPending { + inserted: key.clone(), + evicted: Some(first_disconnected) + }) + ); + assert_eq!( + Some(connected_state()), + bucket.iter().map(|v| v.status).last() + ); + assert!(bucket.pending().is_none()); + assert_eq!( + Some(MAX_NODES_PER_BUCKET - (i + 1)), + bucket.first_connected_pos + ); + } + + assert!(bucket.pending().is_none()); + assert_eq!(MAX_NODES_PER_BUCKET, bucket.num_entries()); + + // Trying to insert another connected node fails. + let key = Key::from(NodeId::random()); + let node = Node { + key, + value: (), + status: connected_state(), + }; + match bucket.insert(node) { + InsertResult::Full => {} + x => panic!("{:?}", x), + } + } + + #[test] + fn full_bucket_discard_pending() { + let mut bucket = + KBucket::::new(Duration::from_secs(1), MAX_NODES_PER_BUCKET, None); + fill_bucket(&mut bucket, disconnected_state()); + let first = bucket.iter().next().unwrap(); + let first_disconnected = first.clone(); + + // Add a connected pending node. + let key = Key::from(NodeId::random()); + let node = Node { + key: key.clone(), + value: (), + status: connected_state(), + }; + if let InsertResult::Pending { disconnected } = bucket.insert(node) { + assert_eq!(&disconnected, &first_disconnected.key); + } else { + panic!() + } + assert!(bucket.pending().is_some()); + + // Update the status of the first disconnected node to be connected. + let _ = bucket.update_status(&first_disconnected.key, ConnectionState::Connected, None); + + // The pending node has been discarded. + assert!(bucket.pending().is_none()); + assert!(bucket.iter().all(|n| n.key != key)); + + // The initially disconnected node is now the most-recently connected. + assert_eq!( + Some((&first_disconnected.key, connected_state())), + bucket.iter().map(|v| (&v.key, v.status)).last() + ); + assert_eq!( + bucket.position(&first_disconnected.key).map(|p| p.0), + bucket.first_connected_pos + ); + assert_eq!(1, bucket.num_connected()); + assert_eq!(MAX_NODES_PER_BUCKET - 1, bucket.num_disconnected()); + } + + /// No duplicate nodes can be inserted via the apply_pending function. + #[test] + fn full_bucket_applied_no_duplicates() { + // First fill the bucket with connected nodes. + let mut bucket = + KBucket::::new(Duration::from_secs(1), MAX_NODES_PER_BUCKET, None); + fill_bucket(&mut bucket, connected_state()); + + let first = bucket.iter().next().unwrap().clone(); + + let third = bucket.iter().nth(2).unwrap().clone(); + + // Set the first connected node as disconnected + + assert_eq!( + bucket.update_status(&first.key, ConnectionState::Disconnected, None), + UpdateResult::Updated + ); + + // Add a connected pending node. + let key = Key::from(NodeId::random()); + let node = Node { + key: key.clone(), + value: (), + status: connected_state(), + }; + + // Add a pending node + if let InsertResult::Pending { disconnected } = bucket.insert(node.clone()) { + assert_eq!(&disconnected, &first.key); + } else { + panic!() + } + assert!(bucket.pending().is_some()); + + // A misc node gets dropped, because it may not pass a filter when updating its connection + // status. + bucket.remove(&third.key); + + // The pending nodes status gets updated + // Apply pending gets called within kbuckets, so we mimic here. + // The pending time hasn't elapsed so nothing should occur. + assert_eq!(bucket.apply_pending(), None); + assert_eq!(bucket.insert(node.clone()), InsertResult::Inserted); + assert!(bucket.pending.is_none()); + + // Speed up the pending time + if let Some(pending) = bucket.pending.as_mut() { + pending.replace = Instant::now() - Duration::from_secs(1); + } + + // At some later time apply pending + assert_eq!(bucket.apply_pending(), None); + // And try and update the status of the pending node + assert_eq!( + bucket.update_status(&node.key, ConnectionState::Connected, None), + UpdateResult::NotModified + ); + } + + #[test] + fn bucket_update_status() { + fn prop(mut bucket: KBucket, pos: Position, status: NodeStatus) -> bool { + let num_nodes = bucket.num_entries(); + + // Capture position and key of the random node to update. + let pos = pos.0 % num_nodes; + let key = bucket.nodes[pos].key.clone(); + + // Record the (ordered) list of status of all nodes in the bucket. + let mut expected = bucket + .iter() + .map(|n| (n.key.clone(), n.status)) + .collect::>(); + + // Update the node in the bucket. + let _ = bucket.update_status(&key, status.state, Some(status.direction)); + + // Check that the bucket now contains the node with the new status, + // preserving the status and relative order of all other nodes. + let expected_pos = if status.is_connected() { + num_nodes - 1 + } else { + bucket.first_connected_pos.unwrap_or(num_nodes) - 1 + }; + expected.remove(pos); + expected.insert(expected_pos, (key, status)); + let actual = bucket + .iter() + .map(|n| (n.key.clone(), n.status)) + .collect::>(); + expected == actual + } + + quickcheck(prop as fn(_, _, _) -> _); + } + + #[test] + fn bucket_update_value_with_filtering() { + fn prop( + mut bucket: KBucket, + pos: Position, + value: u8, + value_matches_filter: bool, + ) -> bool { + // Initialise filter. + let filter = SetFilter { + set: value_matches_filter.then(|| value).into_iter().collect(), + }; + bucket.filter = Some(Box::new(filter)); + + let num_nodes = bucket.num_entries(); + + // Capture position and key of the random node to update. + let pos = pos.0 % num_nodes; + let key = bucket.nodes[pos].key.clone(); + + // Record the (ordered) list of values of all nodes in the bucket. + let mut expected = bucket + .iter() + .map(|n| (n.key.clone(), n.value)) + .collect::>(); + + // Update the node in the bucket. + let _ = bucket.update_value(&key, value); + + bucket.check_invariants(); + + // Check that the bucket now contains the node with the new value, or that the node + // has been removed. + if value_matches_filter || expected[pos].1 == value { + expected[pos].1 = value; + } else { + expected.remove(pos); + } + let actual = bucket + .iter() + .map(|n| (n.key.clone(), n.value)) + .collect::>(); + expected == actual + } + + quickcheck(prop as fn(_, _, _, _) -> _); + } + + /// Hammer a bucket with random mutations to ensure invariants are always maintained. + #[test] + fn random_actions_with_filtering() { + fn prop( + initial_nodes: Vec>, + pending_timeout_millis: u64, + max_incoming: usize, + filter_set: HashSet, + actions: Vec>, + ) -> bool { + let filter = SetFilter { set: filter_set }; + let pending_timeout = Duration::from_millis(pending_timeout_millis); + let mut kbucket = + KBucket::::new(pending_timeout, max_incoming, Some(Box::new(filter))); + + for node in initial_nodes { + let _ = kbucket.insert(node); + } + + for action in actions { + kbucket.apply_action(action); + kbucket.check_invariants(); + } + true + } + + quickcheck(prop as fn(_, _, _, _, _) -> _); + } + + #[test] + fn table_update_status_connection() { + let max_incoming = 7; + let mut bucket = KBucket::::new(Duration::from_secs(1), max_incoming, None); + + let mut incoming_connected = 0; + let mut keys = Vec::new(); + for _ in 0..MAX_NODES_PER_BUCKET { + let key = Key::from(NodeId::random()); + keys.push(key.clone()); + incoming_connected += 1; + let direction = if incoming_connected <= max_incoming { + ConnectionDirection::Incoming + } else { + ConnectionDirection::Outgoing + }; + let status = NodeStatus { + state: ConnectionState::Connected, + direction, + }; + let node = Node { + key: key.clone(), + value: (), + status, + }; + assert_eq!(InsertResult::Inserted, bucket.insert(node)); + } + + // Bucket is full + // Attempt to modify a new state + let result = bucket.update_status( + &keys[max_incoming], + ConnectionState::Disconnected, + Some(ConnectionDirection::Incoming), + ); + assert_eq!(result, UpdateResult::Updated); + let result = bucket.update_status( + &keys[max_incoming], + ConnectionState::Connected, + Some(ConnectionDirection::Outgoing), + ); + assert_eq!(result, UpdateResult::UpdatedAndPromoted); + let result = bucket.update_status( + &keys[max_incoming], + ConnectionState::Connected, + Some(ConnectionDirection::Outgoing), + ); + assert_eq!(result, UpdateResult::NotModified); + let result = bucket.update_status( + &keys[max_incoming], + ConnectionState::Connected, + Some(ConnectionDirection::Incoming), + ); + assert_eq!(result, UpdateResult::Failed(FailureReason::TooManyIncoming)); + } + + #[test] + fn bucket_max_incoming_nodes() { + fn prop(status: Vec) -> bool { + let max_incoming_nodes = 5; + let mut bucket = + KBucket::::new(Duration::from_secs(1), max_incoming_nodes, None); + + // The expected lists of connected and disconnected nodes. + let mut connected = VecDeque::new(); + let mut disconnected = VecDeque::new(); + + // Fill the bucket, thereby populating the expected lists in insertion order. + for status in status { + let key = Key::from(NodeId::random()); + let node = Node { + key: key.clone(), + value: (), + status, + }; + let full = bucket.num_entries() == MAX_NODES_PER_BUCKET; + match bucket.insert(node) { + InsertResult::Inserted => { + let vec = if status.is_connected() { + &mut connected + } else { + &mut disconnected + }; + if full { + vec.pop_front(); + } + vec.push_back((status, key.clone())); + } + InsertResult::FailedFilter => break, + _ => {} + } + } + + // Check all invariants. + bucket.check_invariants(); + + // Get all nodes from the bucket, together with their status. + let mut nodes = bucket + .iter() + .map(|n| (n.status, n.key.clone())) + .collect::>(); + + // Split the list of nodes at the first connected node. + let tail = bucket + .first_connected_pos + .map_or(Vec::new(), |p| nodes.split_off(p)); + + // All nodes before the first connected node must be disconnected and + // in insertion order. Similarly, all remaining nodes must be connected + // and in insertion order. + // The number of incoming nodes does not exceed the maximum limit. + disconnected == nodes && connected == tail + } + + quickcheck(prop as fn(_) -> _); + } +} diff --git a/version-meld/discv5/src/kbucket/entry.rs b/version-meld/discv5/src/kbucket/entry.rs new file mode 100644 index 0000000..e978613 --- /dev/null +++ b/version-meld/discv5/src/kbucket/entry.rs @@ -0,0 +1,198 @@ +// Copyright 2019 Parity Technologies (UK) Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// This basis of this file has been taken from the rust-libp2p codebase: +// https://github.com/libp2p/rust-libp2p + +//! The `Entry` API for querying and modifying the entries of a `KBucketsTable` +//! representing the nodes participating in the Kademlia DHT. + +pub use super::{ + bucket::{ + AppliedPending, ConnectionState, InsertResult, Node, NodeStatus, MAX_NODES_PER_BUCKET, + }, + key::*, + ConnectionDirection, +}; + +use super::*; + +/// An immutable by-reference view of a bucket entry. +pub struct EntryRefView<'a, TPeerId, TVal: Eq> { + /// The node represented by the entry. + pub node: NodeRefView<'a, TPeerId, TVal>, + /// The status of the node identified by the key. + pub status: NodeStatus, +} + +/// An immutable by-reference view of a `Node`. +pub struct NodeRefView<'a, TPeerId, TVal: Eq> { + pub key: &'a Key, + pub value: &'a TVal, +} + +/// A cloned, immutable view of an entry that is either present in a bucket +/// or pending insertion. +#[derive(Clone, Debug)] +pub struct EntryView { + /// The node represented by the entry. + pub node: Node, + /// The status of the node. + pub status: NodeStatus, +} + +impl AsRef> for EntryView { + fn as_ref(&self) -> &Key { + &self.node.key + } +} + +/// A reference into a single entry of a `KBucketsTable`. +#[derive(Debug)] +#[allow(clippy::enum_variant_names)] +pub enum Entry<'a, TPeerId, TVal: Eq> { + /// The entry is present in a bucket. + Present(PresentEntry<'a, TPeerId, TVal>, NodeStatus), + /// The entry is pending insertion in a bucket. + Pending(PendingEntry<'a, TPeerId, TVal>, NodeStatus), + /// The entry is absent and may be inserted. + Absent(AbsentEntry<'a, TPeerId, TVal>), + /// The entry represents the local node. + SelfEntry, +} + +/// The internal representation of the different states of an `Entry`, +/// referencing the associated key and bucket. +#[derive(Debug)] +struct EntryRef<'a, TPeerId, TVal: Eq> { + bucket: &'a mut KBucket, + key: &'a Key, +} + +impl<'a, TPeerId, TVal> Entry<'a, TPeerId, TVal> +where + TPeerId: Clone, + TVal: Eq, +{ + /// Creates a new `Entry` for a `Key`, encapsulating access to a bucket. + pub(super) fn new(bucket: &'a mut KBucket, key: &'a Key) -> Self { + if let Some(pos) = bucket.position(key) { + let status = bucket.status(pos); + Entry::Present(PresentEntry::new(bucket, key), status) + } else if let Some(pending) = bucket.as_pending(key) { + let status = pending.status(); + Entry::Pending(PendingEntry::new(bucket, key), status) + } else { + Entry::Absent(AbsentEntry::new(bucket, key)) + } + } +} + +/// An entry present in a bucket. +#[derive(Debug)] +pub struct PresentEntry<'a, TPeerId, TVal: Eq>(EntryRef<'a, TPeerId, TVal>); + +impl<'a, TPeerId, TVal> PresentEntry<'a, TPeerId, TVal> +where + TPeerId: Clone, + TVal: Eq, +{ + fn new(bucket: &'a mut KBucket, key: &'a Key) -> Self { + PresentEntry(EntryRef { bucket, key }) + } + + /// Returns the value associated with the key. + pub fn value(&self) -> &TVal { + &self + .0 + .bucket + .get(self.0.key) + .expect("We can only build a ConnectedEntry if the entry is in the bucket; QED") + .value + } + + /// Sets the status of the entry. + pub fn update(self, state: ConnectionState, direction: Option) -> Self { + let _ = self.0.bucket.update_status(self.0.key, state, direction); + Self::new(self.0.bucket, self.0.key) + } + + /// Removes the entry from the table. + pub fn remove(self) { + self.0.bucket.remove(self.0.key); + } +} + +/// An entry waiting for a slot to be available in a bucket. +#[derive(Debug)] +pub struct PendingEntry<'a, TPeerId, TVal: Eq>(EntryRef<'a, TPeerId, TVal>); + +impl<'a, TPeerId, TVal: Eq> PendingEntry<'a, TPeerId, TVal> +where + TPeerId: Clone, + TVal: Eq, +{ + fn new(bucket: &'a mut KBucket, key: &'a Key) -> Self { + PendingEntry(EntryRef { bucket, key }) + } + + /// Returns the value associated with the key. + pub fn value(&mut self) -> &mut TVal { + self.0 + .bucket + .pending_mut() + .expect("We can only build a ConnectedPendingEntry if the entry is pending; QED") + .value_mut() + } + + /// Updates the status of the pending entry. + pub fn update(self, status: NodeStatus) -> PendingEntry<'a, TPeerId, TVal> { + self.0.bucket.update_pending(status); + PendingEntry::new(self.0.bucket, self.0.key) + } + + /// Removes the entry from the table. + pub fn remove(self) { + self.0.bucket.remove(self.0.key); + } +} + +/// An entry that is not present in any bucket. +#[derive(Debug)] +pub struct AbsentEntry<'a, TPeerId, TVal: Eq>(EntryRef<'a, TPeerId, TVal>); + +impl<'a, TPeerId, TVal> AbsentEntry<'a, TPeerId, TVal> +where + TPeerId: Clone, + TVal: Eq, +{ + fn new(bucket: &'a mut KBucket, key: &'a Key) -> Self { + AbsentEntry(EntryRef { bucket, key }) + } + + /// Attempts to insert the entry into a bucket. + pub fn insert(self, value: TVal, status: NodeStatus) -> InsertResult { + self.0.bucket.insert(Node { + key: self.0.key.clone(), + value, + status, + }) + } +} diff --git a/version-meld/discv5/src/kbucket/filter.rs b/version-meld/discv5/src/kbucket/filter.rs new file mode 100644 index 0000000..36fe7fa --- /dev/null +++ b/version-meld/discv5/src/kbucket/filter.rs @@ -0,0 +1,96 @@ +//! Provides a trait that can be implemented to apply a filter to a table or bucket. + +use crate::Enr; + +pub trait Filter: FilterClone + Send + Sync { + fn filter( + &self, + value_to_be_inserted: &TVal, + other_vals: &mut dyn Iterator, + ) -> bool; +} + +/// Allow the trait objects to be cloneable. +pub trait FilterClone { + fn clone_box(&self) -> Box>; +} + +impl FilterClone for T +where + T: 'static + Filter + Clone, +{ + fn clone_box(&self) -> Box> { + Box::new(self.clone()) + } +} + +impl Clone for Box> { + fn clone(&self) -> Box> { + self.clone_box() + } +} + +// Implementation of an IP filter for buckets and for tables + +/// Number of permitted nodes in the same /24 subnet per table. +const MAX_NODES_PER_SUBNET_TABLE: usize = 10; +/// The number of nodes permitted in the same /24 subnet per bucket. +const MAX_NODES_PER_SUBNET_BUCKET: usize = 2; + +#[derive(Clone)] +pub struct IpTableFilter; + +impl Filter for IpTableFilter { + fn filter( + &self, + value_to_be_inserted: &Enr, + other_vals: &mut dyn Iterator, + ) -> bool { + ip_filter(value_to_be_inserted, other_vals, MAX_NODES_PER_SUBNET_TABLE) + } +} + +#[derive(Clone)] +pub struct IpBucketFilter; + +impl Filter for IpBucketFilter { + fn filter( + &self, + value_to_be_inserted: &Enr, + other_vals: &mut dyn Iterator, + ) -> bool { + ip_filter( + value_to_be_inserted, + other_vals, + MAX_NODES_PER_SUBNET_BUCKET, + ) + } +} + +fn ip_filter( + value_to_be_inserted: &Enr, + other_vals: &mut dyn Iterator, + limit: usize, +) -> bool { + if let Some(ip) = value_to_be_inserted.ip() { + let mut count = 0; + for enr in other_vals { + // Ignore duplicates + if enr == value_to_be_inserted { + continue; + } + + // Count the same /24 subnet + if let Some(other_ip) = enr.ip() { + if other_ip.octets()[0..3] == ip.octets()[0..3] { + count += 1; + } + } + if count >= limit { + return false; + } + } + } + // No IP, so no restrictions + true +} diff --git a/version-meld/discv5/src/kbucket/key.rs b/version-meld/discv5/src/kbucket/key.rs new file mode 100644 index 0000000..f27765a --- /dev/null +++ b/version-meld/discv5/src/kbucket/key.rs @@ -0,0 +1,168 @@ +// Copyright 2018 Parity Technologies (UK) Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// This basis of this file has been taken from the rust-libp2p codebase: +// https://github.com/libp2p/rust-libp2p + +#![allow(clippy::all)] + +use enr::NodeId; +use sha2::digest::generic_array::{typenum::U32, GenericArray}; +use uint::construct_uint; + +construct_uint! { + /// 256-bit unsigned integer. + pub(super) struct U256(4); +} + +/// A `Key` is a cryptographic hash, identifying both the nodes participating in +/// the Kademlia DHT, as well as records stored in the DHT. +/// +/// The set of all `Key`s defines the Kademlia keyspace. +/// +/// `Key`s have an XOR metric as defined in the Kademlia paper, i.e. the bitwise XOR of +/// the hash digests, interpreted as an integer. See [`Key::distance`]. +/// +/// A `Key` preserves the preimage of type `T` of the hash function. See [`Key::preimage`]. +#[derive(Clone, Debug)] +pub struct Key { + preimage: T, + hash: GenericArray, +} + +impl PartialEq for Key { + fn eq(&self, other: &Key) -> bool { + self.hash == other.hash + } +} + +impl Eq for Key {} + +impl AsRef> for Key { + fn as_ref(&self) -> &Key { + self + } +} + +impl Key { + /// Construct a new `Key` by providing the raw 32 byte hash. + pub fn new_raw(preimage: T, hash: GenericArray) -> Key { + Key { preimage, hash } + } + + /// Borrows the preimage of the key. + pub fn preimage(&self) -> &T { + &self.preimage + } + + /// Converts the key into its preimage. + pub fn into_preimage(self) -> T { + self.preimage + } + + /// Computes the distance of the keys according to the XOR metric. + pub fn distance(&self, other: &Key) -> Distance { + let a = U256::from(self.hash.as_slice()); + let b = U256::from(other.hash.as_slice()); + Distance(a ^ b) + } + + // Used in the FINDNODE query outside of the k-bucket implementation. + /// Computes the integer log-2 distance between two keys, assuming a 256-bit + /// key. The output returns None if the key's are identical. The range is 1-256. + pub fn log2_distance(&self, other: &Key) -> Option { + let xor_dist = self.distance(other); + let log_dist = u64::from(256 - xor_dist.0.leading_zeros()); + if log_dist == 0 { + None + } else { + Some(log_dist) + } + } +} + +impl From for Key { + fn from(node_id: NodeId) -> Self { + Key { + preimage: node_id, + hash: *GenericArray::from_slice(&node_id.raw()), + } + } +} + +/// A distance between two `Key`s. +#[derive(Copy, Clone, PartialEq, Eq, Default, PartialOrd, Ord, Debug)] +pub struct Distance(pub(super) U256); + +#[cfg(test)] +mod tests { + use super::*; + use crate::kbucket::bucket::tests::arbitrary_node_id; + use quickcheck::*; + + impl Arbitrary for Key { + fn arbitrary(g: &mut G) -> Key { + Key::from(arbitrary_node_id(g)) + } + } + + #[test] + fn identity() { + fn prop(a: Key) -> bool { + a.distance(&a) == Distance::default() + } + quickcheck(prop as fn(_) -> _) + } + + #[test] + fn symmetry() { + fn prop(a: Key, b: Key) -> bool { + a.distance(&b) == b.distance(&a) + } + quickcheck(prop as fn(_, _) -> _) + } + + #[test] + fn triangle_inequality() { + fn prop(a: Key, b: Key, c: Key) -> TestResult { + let ab = a.distance(&b); + let bc = b.distance(&c); + let (ab_plus_bc, overflow) = ab.0.overflowing_add(bc.0); + if overflow { + TestResult::discard() + } else { + TestResult::from_bool(a.distance(&c) <= Distance(ab_plus_bc)) + } + } + quickcheck(prop as fn(_, _, _) -> _) + } + + #[test] + fn unidirectionality() { + fn prop(a: Key, b: Key) -> bool { + let d = a.distance(&b); + (0..100).all(|_| { + let c = Key::from(NodeId::random()); + a.distance(&c) != d || b == c + }) + } + quickcheck(prop as fn(_, _) -> _) + } +} diff --git a/version-meld/discv5/src/lib.rs b/version-meld/discv5/src/lib.rs new file mode 100644 index 0000000..1aac813 --- /dev/null +++ b/version-meld/discv5/src/lib.rs @@ -0,0 +1,135 @@ +#![warn(rust_2018_idioms)] +#![deny(rustdoc::broken_intra_doc_links)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![allow(clippy::needless_doctest_main)] +//! An implementation of [Discovery V5](https://github.com/ethereum/devp2p/blob/master/discv5/discv5.md). +//! +//! # Overview +//! +//! Discovery v5 is a protocol designed for encrypted peer discovery and topic advertisement. Each peer/node +//! on the network is identified via it's ENR ([Ethereum Name +//! Record](https://eips.ethereum.org/EIPS/eip-778)), which is essentially a signed key-value store +//! containing the node's public key and optionally IP address and port. +//! +//! Discv5 employs a kademlia-like routing table to store and manage discovered peers and topics. The +//! protocol allows for external IP discovery in NAT environments through regular PING/PONG's with +//! discovered nodes. Nodes return the external IP address that they have received and a simple +//! majority is chosen as our external IP address. If an external IP address is updated, this is +//! produced as an event to notify the swarm (if one is used for this behaviour). +//! +//! For a simple CLI discovery service see [discv5-cli](https://github.com/AgeManning/discv5-cli) +//! +//! This protocol is split into four main sections/layers: +//! +//! * Socket - The [`socket`] module is responsible for opening the underlying UDP socket. It +//! creates individual tasks for sending/encoding and receiving/decoding packets from the UDP +//! socket. +//! * Handler - The protocol's communication is encrypted with `AES_GCM`. All node communication +//! undergoes a handshake, which results in a [`Session`]. [`Session`]'s are established when +//! needed and get dropped after a timeout. This section manages the creation and maintenance of +//! sessions between nodes and the encryption/decryption of packets from the socket. It is realised by the [`handler::Handler`] struct and it runs in its own task. +//! * Service - This section contains the protocol-level logic. In particular it manages the +//! routing table of known ENR's, topic registration/advertisement and performs various queries +//! such as peer discovery. This section is realised by the [`Service`] struct. This also runs in +//! it's own thread. +//! * Application - This section is the user-facing API which can start/stop the underlying +//! tasks, initiate queries and obtain metrics about the underlying server. +//! +//! ## Event Stream +//! +//! The [`Discv5`] struct provides access to an event-stream which allows the user to listen to +//! [`Discv5Event`] that get generated from the underlying server. The stream can be obtained +//! from the [`Discv5::event_stream()`] function. +//! +//! ## Runtimes +//! +//! Discv5 requires a tokio runtime with timing and io enabled. An explicit runtime can be given +//! via the configuration. See the [`Discv5ConfigBuilder`] for further details. Such a runtime +//! must implement the [`Executor`] trait. +//! +//! If an explicit runtime is not provided via the configuration parameters, it is assumed that +//! a tokio runtime is present when creating the [`Discv5`] struct. The struct will use the +//! existing runtime for spawning the underlying server tasks. If a runtime is not present, the +//! creation of the [`Discv5`] struct will panic. +//! +//! # Usage +//! +//! A simple example of creating this service is as follows: +//! +//! ```rust +//! use discv5::{enr, enr::{CombinedKey, NodeId}, TokioExecutor, Discv5, Discv5ConfigBuilder}; +//! use std::net::SocketAddr; +//! +//! // listening address and port +//! let listen_addr = "0.0.0.0:9000".parse::().unwrap(); +//! +//! // construct a local ENR +//! let enr_key = CombinedKey::generate_secp256k1(); +//! let enr = enr::EnrBuilder::new("v4").build(&enr_key).unwrap(); +//! +//! // build the tokio executor +//! let mut runtime = tokio::runtime::Builder::new_multi_thread() +//! .thread_name("Discv5-example") +//! .enable_all() +//! .build() +//! .unwrap(); +//! +//! // default configuration +//! let config = Discv5ConfigBuilder::new().build(); +//! +//! // construct the discv5 server +//! let mut discv5 = Discv5::new(enr, enr_key, config).unwrap(); +//! +//! // In order to bootstrap the routing table an external ENR should be added +//! // This can be done via add_enr. I.e.: +//! // discv5.add_enr() +//! +//! // start the discv5 server +//! runtime.block_on(discv5.start(listen_addr)); +//! +//! // run a find_node query +//! runtime.block_on(async { +//! let found_nodes = discv5.find_node(NodeId::random()).await.unwrap(); +//! println!("Found nodes: {:?}", found_nodes); +//! }); +//! ``` +//! +//! [`Discv5`]: struct.Discv5.html +//! [`Discv5Event`]: enum.Discv5Event.html +//! [`Discv5Config`]: config/struct.Discv5Config.html +//! [`Discv5ConfigBuilder`]: config/struct.Discv5ConfigBuilder.html +//! [Packet]: packet/enum.Packet.html +//! [`Service`]: service/struct.Service.html +//! [`Session`]: session/struct.Session.html + +mod config; +mod discv5; +mod error; +mod executor; +pub mod handler; +pub mod kbucket; +mod lru_time_cache; +pub mod metrics; +mod node_info; +pub mod packet; +pub mod permit_ban; +mod query_pool; +pub mod rpc; +pub mod service; +pub mod socket; + +#[macro_use] +extern crate lazy_static; + +pub type Enr = enr::Enr; + +pub use crate::discv5::{Discv5, Discv5Event}; +pub use config::{Discv5Config, Discv5ConfigBuilder}; +pub use error::{Discv5Error, QueryError, RequestError, ResponseError}; +pub use executor::{Executor, TokioExecutor}; +pub use kbucket::{ConnectionDirection, ConnectionState, Key}; +pub use permit_ban::PermitBanList; +pub use service::TalkRequest; +pub use socket::{RateLimiter, RateLimiterBuilder}; +// re-export the ENR crate +pub use enr; diff --git a/version-meld/discv5/src/lru_time_cache.rs b/version-meld/discv5/src/lru_time_cache.rs new file mode 100644 index 0000000..67606c3 --- /dev/null +++ b/version-meld/discv5/src/lru_time_cache.rs @@ -0,0 +1,253 @@ +use hashlink::LinkedHashMap; +use std::{ + hash::Hash, + time::{Duration, Instant}, +}; + +pub struct LruTimeCache { + map: LinkedHashMap, + /// The time elements remain in the cache. + ttl: Duration, + /// The max size of the cache. + capacity: usize, +} + +impl LruTimeCache { + pub fn new(ttl: Duration, capacity: Option) -> LruTimeCache { + let capacity = if let Some(cap) = capacity { + cap + } else { + usize::MAX + }; + LruTimeCache { + map: LinkedHashMap::new(), + ttl, + capacity, + } + } + + /// Inserts a key-value pair into the cache. + pub fn insert(&mut self, key: K, value: V) { + let now = Instant::now(); + self.map.insert(key, (value, now)); + + if self.map.len() > self.capacity { + self.map.pop_front(); + } + } + + /// Retrieves a reference to the value stored under `key`, or `None` if the key doesn't exist. + /// Also removes expired elements and updates the time. + pub fn get(&mut self, key: &K) -> Option<&V> { + self.get_mut(key).map(|value| &*value) + } + + /// Retrieves a mutable reference to the value stored under `key`, or `None` if the key doesn't exist. + /// Also removes expired elements and updates the time. + pub fn get_mut(&mut self, key: &K) -> Option<&mut V> { + let now = Instant::now(); + self.remove_expired_values(now); + + match self.map.raw_entry_mut().from_key(key) { + hashlink::linked_hash_map::RawEntryMut::Occupied(mut occupied) => { + occupied.get_mut().1 = now; + occupied.to_back(); + Some(&mut occupied.into_mut().0) + } + hashlink::linked_hash_map::RawEntryMut::Vacant(_) => None, + } + } + + /// Returns a reference to the value with the given `key`, if present and not expired, without + /// updating the timestamp. + pub fn peek(&self, key: &K) -> Option<&V> { + if let Some((value, time)) = self.map.get(key) { + return if *time + self.ttl >= Instant::now() { + Some(value) + } else { + None + }; + } + + None + } + + /// Returns the size of the cache, i.e. the number of cached non-expired key-value pairs. + pub fn len(&mut self) -> usize { + self.remove_expired_values(Instant::now()); + self.map.len() + } + + /// Removes a key-value pair from the cache, returning the value at the key if the key + /// was previously in the map. + pub fn remove(&mut self, key: &K) -> Option { + self.map.remove(key).map(|v| v.0) + } + + /// Removes expired items from the cache. + fn remove_expired_values(&mut self, now: Instant) { + let mut expired_keys = vec![]; + + for (key, (_, time)) in self.map.iter_mut() { + if *time + self.ttl >= now { + break; + } + expired_keys.push(key.clone()); + } + + for k in expired_keys { + self.map.remove(&k); + } + } +} + +#[cfg(test)] +mod tests { + use crate::lru_time_cache::LruTimeCache; + use std::time::Duration; + + #[test] + fn insert() { + let mut cache = LruTimeCache::new(Duration::from_secs(10), None); + + cache.insert(1, 10); + cache.insert(2, 20); + cache.insert(3, 30); + + assert_eq!(Some(&10), cache.get(&1)); + assert_eq!(Some(&20), cache.get(&2)); + assert_eq!(Some(&30), cache.get(&3)); + } + + #[test] + fn capacity() { + let mut cache = LruTimeCache::new(Duration::from_secs(10), Some(2)); + + cache.insert(1, 10); + cache.insert(2, 20); + assert_eq!(2, cache.len()); + + cache.insert(3, 30); + assert_eq!(2, cache.len()); + assert_eq!(Some(&20), cache.get(&2)); + assert_eq!(Some(&30), cache.get(&3)); + } + + #[test] + fn get() { + let mut cache = LruTimeCache::new(Duration::from_secs(10), Some(2)); + + cache.insert(1, 10); + cache.insert(2, 20); + assert_eq!(Some(&10), cache.get(&1)); + + cache.insert(3, 30); + // `1` is alive as `get()` updates the timestamp. + assert_eq!(Some(&10), cache.get(&1)); + // `2` is removed as `2` is oldest at the time `3` was inserted. + assert_eq!(None, cache.get(&2)); + } + + #[test] + fn get_mut() { + let mut cache = LruTimeCache::new(Duration::from_secs(10), None); + + cache.insert(1, 10); + let v = cache.get_mut(&1).expect("should have value"); + *v = 100; + + assert_eq!(Some(&100), cache.get(&1)); + } + + #[test] + fn peek() { + let mut cache = LruTimeCache::new(Duration::from_secs(10), Some(2)); + + cache.insert(1, 10); + cache.insert(2, 20); + assert_eq!(Some(&10), cache.peek(&1)); + + cache.insert(3, 30); + // `1` is removed as `peek()` does not update the time. + assert_eq!(None, cache.peek(&1)); + assert_eq!(Some(&20), cache.get(&2)); + } + + #[test] + fn len() { + let mut cache = LruTimeCache::new(Duration::from_secs(10), None); + + assert_eq!(0, cache.len()); + + cache.insert(1, 10); + cache.insert(2, 20); + cache.insert(3, 30); + assert_eq!(3, cache.len()); + } + + #[test] + fn remove() { + let mut cache = LruTimeCache::new(Duration::from_secs(10), None); + + cache.insert(1, 10); + assert_eq!(Some(10), cache.remove(&1)); + assert_eq!(None, cache.get(&1)); + assert_eq!(None, cache.remove(&1)); + } + + mod ttl { + use crate::lru_time_cache::LruTimeCache; + use std::{thread::sleep, time::Duration}; + + const TTL: Duration = Duration::from_millis(100); + + #[test] + fn get() { + let mut cache = LruTimeCache::new(TTL, None); + cache.insert(1, 10); + assert_eq!(Some(&10), cache.get(&1)); + + sleep(TTL); + assert_eq!(None, cache.get(&1)); + } + + #[test] + fn peek() { + let mut cache = LruTimeCache::new(TTL, None); + cache.insert(1, 10); + assert_eq!(Some(&10), cache.peek(&1)); + + sleep(TTL); + assert_eq!(None, cache.peek(&1)); + } + + #[test] + fn len() { + let mut cache = LruTimeCache::new(TTL, None); + cache.insert(1, 10); + assert_eq!(1, cache.len()); + + sleep(TTL); + assert_eq!(0, cache.len()); + } + + #[test] + fn ttl() { + let mut cache = LruTimeCache::new(TTL, None); + cache.insert(1, 10); + sleep(TTL / 4); + cache.insert(2, 20); + sleep(TTL / 4); + cache.insert(3, 30); + sleep(TTL / 4); + cache.insert(4, 40); + sleep(TTL / 4); + + assert_eq!(3, cache.len()); + assert_eq!(None, cache.get(&1)); + assert_eq!(Some(&20), cache.get(&2)); + assert_eq!(Some(&30), cache.get(&3)); + assert_eq!(Some(&40), cache.get(&4)); + } + } +} diff --git a/version-meld/discv5/src/metrics.rs b/version-meld/discv5/src/metrics.rs new file mode 100644 index 0000000..2e6f2fc --- /dev/null +++ b/version-meld/discv5/src/metrics.rs @@ -0,0 +1,72 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; + +lazy_static! { + pub static ref METRICS: InternalMetrics = InternalMetrics::default(); +} + +/// A collection of metrics used throughout the server. +pub struct InternalMetrics { + /// The number of active UDP sessions that are currently established. + pub active_sessions: AtomicUsize, + /// The number of seconds to store received packets to taking a moving average over. + pub moving_window: u64, + /// The number of unsolicited requests received per moving window. + pub unsolicited_requests_per_window: AtomicUsize, + /// The number of bytes sent. + pub bytes_sent: AtomicUsize, + /// The number of bytes received. + pub bytes_recv: AtomicUsize, +} + +impl Default for InternalMetrics { + fn default() -> Self { + InternalMetrics { + moving_window: 5, + active_sessions: AtomicUsize::new(0), + unsolicited_requests_per_window: AtomicUsize::new(0), + bytes_sent: AtomicUsize::new(0), + bytes_recv: AtomicUsize::new(0), + } + } +} + +impl InternalMetrics { + pub fn add_recv_bytes(&self, bytes: usize) { + let current_bytes_recv = self.bytes_recv.load(Ordering::Relaxed); + self.bytes_recv + .store(current_bytes_recv.saturating_add(bytes), Ordering::Relaxed); + } + + pub fn add_sent_bytes(&self, bytes: usize) { + let current_bytes_sent = self.bytes_sent.load(Ordering::Relaxed); + self.bytes_sent + .store(current_bytes_sent.saturating_add(bytes), Ordering::Relaxed); + } +} + +#[derive(Clone, Debug)] +/// The publicly accessible metrics that can be obtained from the Discv5 server. +pub struct Metrics { + /// The number of active UDP sessions that are currently established. + pub active_sessions: usize, + /// The number of unsolicited requests received per second (averaged over a moving window). + pub unsolicited_requests_per_second: f64, + /// The number of bytes sent. + pub bytes_sent: usize, + /// The number of bytes received. + pub bytes_recv: usize, +} + +impl From<&METRICS> for Metrics { + fn from(internal_metrics: &METRICS) -> Self { + Metrics { + active_sessions: internal_metrics.active_sessions.load(Ordering::Relaxed), + unsolicited_requests_per_second: internal_metrics + .unsolicited_requests_per_window + .load(Ordering::Relaxed) as f64 + / internal_metrics.moving_window as f64, + bytes_sent: internal_metrics.bytes_sent.load(Ordering::Relaxed), + bytes_recv: internal_metrics.bytes_recv.load(Ordering::Relaxed), + } + } +} diff --git a/version-meld/discv5/src/node_info.rs b/version-meld/discv5/src/node_info.rs new file mode 100644 index 0000000..3bf7010 --- /dev/null +++ b/version-meld/discv5/src/node_info.rs @@ -0,0 +1,190 @@ +use super::*; +use crate::Enr; +use enr::{CombinedPublicKey, NodeId}; +use std::net::SocketAddr; + +#[cfg(feature = "libp2p")] +use libp2p_core::{identity::PublicKey, multiaddr::Protocol, multihash, Multiaddr}; + +/// This type relaxes the requirement of having an ENR to connect to a node, to allow for unsigned +/// connection types, such as multiaddrs. +#[derive(Debug, Clone, PartialEq)] +pub enum NodeContact { + /// We know the ENR of the node we are contacting. + Enr(Box), + /// We don't have an ENR, but have enough information to start a handshake. + /// + /// The handshake will request the ENR at the first opportunity. + /// The public key can be derived from multiaddr's whose keys can be inlined. The `TryFrom` + /// implementation for `String` and `MultiAddr`. This is gated behind the `libp2p` feature. + Raw { + /// An ENR compatible public key, required for handshaking with peers. + public_key: Box, + /// The socket address and `NodeId` of the peer to connect to. + node_address: Box, + }, +} + +impl NodeContact { + pub fn node_id(&self) -> NodeId { + match self { + NodeContact::Enr(enr) => enr.node_id(), + NodeContact::Raw { node_address, .. } => node_address.node_id, + } + } + + pub fn seq_no(&self) -> Option { + match self { + NodeContact::Enr(enr) => Some(enr.seq()), + _ => None, + } + } + + pub fn public_key(&self) -> CombinedPublicKey { + match self { + NodeContact::Enr(ref enr) => enr.public_key(), + NodeContact::Raw { public_key, .. } => *public_key.clone(), + } + } + + pub fn is_enr(&self) -> bool { + matches!(self, NodeContact::Enr(_)) + } + + pub fn udp_socket(&self) -> Result { + match self { + NodeContact::Enr(enr) => enr + .udp_socket() + .ok_or("ENR does not contain an IP and UDP port"), + NodeContact::Raw { node_address, .. } => Ok(node_address.socket_addr), + } + } + + pub fn node_address(&self) -> Result { + let socket_addr = self.udp_socket()?; + let node_id = self.node_id(); + Ok(NodeAddress { + socket_addr, + node_id, + }) + } +} + +impl From for NodeContact { + fn from(enr: Enr) -> Self { + NodeContact::Enr(Box::new(enr)) + } +} + +#[cfg(feature = "libp2p")] +impl std::convert::TryFrom for NodeContact { + type Error = &'static str; + + fn try_from(multiaddr: Multiaddr) -> Result { + // The multiaddr must contain either the ip4 or ip6 protocols, the UDP protocol and the P2P + // protocol with either secp256k1 or ed25519 keys. + + // perform a single pass and try to fill all required protocols from the multiaddr + let mut ip_addr = None; + let mut udp_port = None; + let mut p2p = None; + + for protocol in multiaddr.into_iter() { + match protocol { + Protocol::Udp(port) => udp_port = Some(port), + Protocol::Ip4(addr) => ip_addr = Some(addr.into()), + Protocol::Ip6(addr) => ip_addr = Some(addr.into()), + Protocol::P2p(multihash) => p2p = Some(multihash), + _ => {} + } + } + + let udp_port = udp_port.ok_or("A UDP port must be specified in the multiaddr")?; + let ip_addr = ip_addr.ok_or("An IP address must be specified in the multiaddr")?; + let multihash = p2p.ok_or("The p2p protocol must be specified in the multiaddr")?; + + // verify the correct key type + if multihash.code() != u64::from(multihash::Code::Identity) { + return Err("The key type is unsupported"); + } + + let public_key: CombinedPublicKey = + match PublicKey::from_protobuf_encoding(&multihash.to_bytes()[2..]) + .map_err(|_| "Invalid public key")? + { + PublicKey::Secp256k1(pk) => { + // TODO: Remove libp2p dep to avoid conversion here + enr::k256::ecdsa::VerifyingKey::from_sec1_bytes(&pk.encode_uncompressed()) + .expect("Libp2p key conversion, always valid") + .into() + } + PublicKey::Ed25519(pk) => enr::ed25519_dalek::PublicKey::from_bytes(&pk.encode()) + .expect("Libp2p key conversion, always valid") + .into(), + _ => return Err("The key type is not supported"), + }; + + Ok(NodeContact::Raw { + public_key: Box::new(public_key.clone()), + node_address: Box::new(NodeAddress { + socket_addr: SocketAddr::new(ip_addr, udp_port), + node_id: public_key.into(), + }), + }) + } +} + +impl std::fmt::Display for NodeContact { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + NodeContact::Enr(enr) => { + write!(f, "Node: {}, addr: {:?}", enr.node_id(), enr.udp_socket()) + } + NodeContact::Raw { node_address, .. } => write!(f, "{}", node_address), + } + } +} + +/// A representation of an unsigned contactable node. +#[derive(PartialEq, Hash, Eq, Clone, Debug)] +pub struct NodeAddress { + /// The destination socket address. + pub socket_addr: SocketAddr, + /// The destination Node Id. + pub node_id: NodeId, +} + +impl Ord for NodeAddress { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + let ord = self.node_id.raw().cmp(&other.node_id.raw()); + if ord != std::cmp::Ordering::Equal { + return ord; + } + let ord = self.socket_addr.ip().cmp(&other.socket_addr.ip()); + if ord != std::cmp::Ordering::Equal { + return ord; + } + self.socket_addr.port().cmp(&other.socket_addr.port()) + } +} + +impl PartialOrd for NodeAddress { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl NodeAddress { + pub fn new(socket_addr: SocketAddr, node_id: NodeId) -> Self { + Self { + socket_addr, + node_id, + } + } +} + +impl std::fmt::Display for NodeAddress { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Node: {}, addr: {:?}", self.node_id, self.socket_addr) + } +} diff --git a/version-meld/discv5/src/packet/mod.rs b/version-meld/discv5/src/packet/mod.rs new file mode 100644 index 0000000..240618c --- /dev/null +++ b/version-meld/discv5/src/packet/mod.rs @@ -0,0 +1,899 @@ +//! This module defines the raw UDP message packets for Discovery v5.1. +//! +//! The [discv5 wire specification](https://github.com/ethereum/devp2p/blob/master/discv5/discv5.md) provides further information on UDP message packets as implemented in this module. +//! +//! A [`Packet`] defines all raw UDP message variants and implements the encoding/decoding +//! logic. +//! +//! Note, that all message encryption/decryption is handled outside of this module. +//! +//! [`Packet`]: enum.Packet.html + +use crate::{error::PacketError, Enr}; +use aes::{ + cipher::{generic_array::GenericArray, NewCipher, StreamCipher}, + Aes128Ctr, +}; +use enr::NodeId; +use rand::Rng; +use std::convert::TryInto; +use zeroize::Zeroize; + +/// The packet IV length (u128). +pub const IV_LENGTH: usize = 16; +/// The length of the static header. (6 byte protocol id, 2 bytes version, 1 byte kind, 12 byte +/// message nonce and a 2 byte authdata-size). +pub const STATIC_HEADER_LENGTH: usize = 23; +/// The message nonce length (in bytes). +pub const MESSAGE_NONCE_LENGTH: usize = 12; +/// The Id nonce length (in bytes). +pub const ID_NONCE_LENGTH: usize = 16; + +/// Protocol ID sent with each message. +const PROTOCOL_ID: &str = "discv5"; +/// The version sent with each handshake. +const VERSION: u16 = 0x0001; + +pub(crate) const MAX_PACKET_SIZE: usize = 1280; +// The smallest packet must be at least this large +// The 24 is the smallest auth_data that can be sent (it is by a WHOAREYOU packet) +const MIN_PACKET_SIZE: usize = IV_LENGTH + STATIC_HEADER_LENGTH + 24; + +/// Message Nonce (12 bytes). +pub type MessageNonce = [u8; MESSAGE_NONCE_LENGTH]; +/// The nonce sent in a WHOAREYOU packet. +pub type IdNonce = [u8; ID_NONCE_LENGTH]; + +// This is the WHOAREYOU authenticated data. +pub struct ChallengeData([u8; 63]); + +impl std::fmt::Debug for ChallengeData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex::encode(&self.0)) + } +} + +impl std::convert::TryFrom<&[u8]> for ChallengeData { + type Error = (); + + fn try_from(data: &[u8]) -> Result { + if data.len() != 63 { + return Err(()); + } + let mut result = [0; 63]; + result.copy_from_slice(data); + Ok(ChallengeData(result)) + } +} + +impl AsRef<[u8]> for ChallengeData { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Packet { + /// Random data unique to the packet. + pub iv: u128, + /// Protocol header. + pub header: PacketHeader, + /// The message contents itself. + pub message: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct PacketHeader { + /// The nonce of the associated message + pub message_nonce: MessageNonce, + /// The type of packet this is. + pub kind: PacketKind, +} + +impl PacketHeader { + // Encodes the header to bytes to be included into the `masked-header` of the Packet Encoding. + pub fn encode(&self) -> Vec { + let auth_data = self.kind.encode(); + let mut buf = Vec::with_capacity(auth_data.len() + STATIC_HEADER_LENGTH); + buf.extend_from_slice(PROTOCOL_ID.as_bytes()); + buf.extend_from_slice(&VERSION.to_be_bytes()); + let kind: u8 = (&self.kind).into(); + buf.extend_from_slice(&kind.to_be_bytes()); + buf.extend_from_slice(&self.message_nonce); + buf.extend_from_slice(&(auth_data.len() as u16).to_be_bytes()); + buf.extend_from_slice(&auth_data); + buf + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum PacketKind { + /// An ordinary message. + Message { + /// The sending NodeId. + src_id: NodeId, + }, + /// A WHOAREYOU packet. + WhoAreYou { + /// The ID Nonce to be verified. + id_nonce: IdNonce, + /// The local node's current ENR sequence number. + enr_seq: u64, + }, + /// A handshake message. + Handshake { + /// The sending NodeId + src_id: NodeId, + /// Id-nonce signature that matches the WHOAREYOU request. + id_nonce_sig: Vec, + /// The ephemeral public key of the handshake. + ephem_pubkey: Vec, + /// The ENR record of the node if the WHOAREYOU request is out-dated. + enr_record: Option, + }, +} + +impl From<&PacketKind> for u8 { + fn from(kind: &PacketKind) -> Self { + match kind { + PacketKind::Message { .. } => 0, + PacketKind::WhoAreYou { .. } => 1, + PacketKind::Handshake { .. } => 2, + } + } +} + +impl PacketKind { + /// Encodes the packet type into its corresponding auth_data. + pub fn encode(&self) -> Vec { + match self { + PacketKind::Message { src_id } => src_id.raw().to_vec(), + PacketKind::WhoAreYou { id_nonce, enr_seq } => { + let mut auth_data = Vec::with_capacity(24); + auth_data.extend_from_slice(id_nonce); + auth_data.extend_from_slice(&enr_seq.to_be_bytes()); + debug_assert_eq!(auth_data.len(), 24); + auth_data + } + PacketKind::Handshake { + src_id, + id_nonce_sig, + ephem_pubkey, + enr_record, + } => { + let sig_size = id_nonce_sig.len(); + let pubkey_size = ephem_pubkey.len(); + let node_record = enr_record.as_ref().map(rlp::encode); + let expected_len = 34 + + sig_size + + pubkey_size + + node_record.as_ref().map(|x| x.len()).unwrap_or_default(); + + let mut auth_data = Vec::with_capacity(expected_len); + auth_data.extend_from_slice(&src_id.raw()); + auth_data.extend_from_slice(&(sig_size as u8).to_be_bytes()); + auth_data.extend_from_slice(&(pubkey_size as u8).to_be_bytes()); + auth_data.extend_from_slice(id_nonce_sig); + auth_data.extend_from_slice(ephem_pubkey); + if let Some(node_record) = node_record { + auth_data.extend_from_slice(&node_record); + } + debug_assert_eq!(auth_data.len(), expected_len); + auth_data + } + } + } + + pub fn is_whoareyou(&self) -> bool { + matches!(self, PacketKind::WhoAreYou { .. }) + } + + /// Decodes auth data, given the kind byte. + pub fn decode(kind: u8, auth_data: &[u8]) -> Result { + match kind { + 0 => { + // Decoding a message packet + // This should only contain a 32 byte NodeId. + if auth_data.len() != 32 { + return Err(PacketError::InvalidAuthDataSize); + } + + let src_id = NodeId::parse(auth_data).map_err(|_| PacketError::InvalidNodeId)?; + Ok(PacketKind::Message { src_id }) + } + 1 => { + // Decoding a WHOAREYOU packet authdata + // This must be 24 bytes long. + if auth_data.len() != 24 { + return Err(PacketError::InvalidAuthDataSize); + } + let id_nonce: IdNonce = auth_data[..ID_NONCE_LENGTH] + .try_into() + .expect("ID_NONCE_LENGTH must be the correct size"); + let enr_seq = u64::from_be_bytes( + auth_data[ID_NONCE_LENGTH..] + .try_into() + .expect("The length of the authdata must be 52 bytes"), + ); + + Ok(PacketKind::WhoAreYou { id_nonce, enr_seq }) + } + 2 => { + // Decoding a Handshake packet + // Start by decoding the header + // Length must contain 2 bytes of lengths and the src id (32 bytes) + if auth_data.len() < 34 { + // The auth_data header is too short + return Err(PacketError::InvalidAuthDataSize); + } + + // decode the src_id + let src_id = + NodeId::parse(&auth_data[..32]).map_err(|_| PacketError::InvalidNodeId)?; + + // decode the lengths + let sig_size = auth_data[32] as usize; + let eph_key_size = auth_data[32 + 1] as usize; + + let total_size = sig_size + eph_key_size; + + // verify the auth data length + if auth_data.len() < 34 + total_size { + return Err(PacketError::InvalidAuthDataSize); + } + + let remaining_data = &auth_data[32 + 2..]; + + let id_nonce_sig = remaining_data[0..sig_size as usize].to_vec(); + let ephem_pubkey = remaining_data[sig_size as usize..total_size].to_vec(); + + let enr_record = if remaining_data.len() > total_size { + Some( + rlp::decode::(&remaining_data[total_size..]) + .map_err(PacketError::InvalidEnr)?, + ) + } else { + None + }; + + Ok(PacketKind::Handshake { + src_id, + id_nonce_sig, + ephem_pubkey, + enr_record, + }) + } + _ => Err(PacketError::UnknownPacket), + } + } +} + +/// The implementation of creating, encoding and decoding raw packets in the discv5.1 system. +// +// NOTE: We perform the encryption and decryption when we are encoding/decoding as this is +// performed in its own task in practice. The Handler can create the messages without the overhead +// of encryption/decryption and send them off to the send/recv tasks to perform the +// encryption/decryption. +impl Packet { + /// Creates an ordinary message packet. + pub fn new_message(src_id: NodeId, message_nonce: MessageNonce, ciphertext: Vec) -> Self { + let iv: u128 = rand::random(); + + let header = PacketHeader { + message_nonce, + kind: PacketKind::Message { src_id }, + }; + + Packet { + iv, + header, + message: ciphertext, + } + } + + pub fn new_whoareyou(request_nonce: MessageNonce, id_nonce: IdNonce, enr_seq: u64) -> Self { + let iv: u128 = rand::random(); + + let header = PacketHeader { + message_nonce: request_nonce, + kind: PacketKind::WhoAreYou { id_nonce, enr_seq }, + }; + + Packet { + iv, + header, + message: Vec::new(), + } + } + + pub fn new_authheader( + src_id: NodeId, + message_nonce: MessageNonce, + id_nonce_sig: Vec, + ephem_pubkey: Vec, + enr_record: Option, + ) -> Self { + let iv: u128 = rand::random(); + + let header = PacketHeader { + message_nonce, + kind: PacketKind::Handshake { + src_id, + id_nonce_sig, + ephem_pubkey, + enr_record, + }, + }; + + Packet { + iv, + header, + message: Vec::new(), + } + } + + /// Generates a Packet::Random given a `tag`. + pub fn new_random(src_id: &NodeId) -> Result { + let mut ciphertext = [0u8; 44]; + rand::thread_rng() + .try_fill(&mut ciphertext[..]) + .map_err(|_| "PRNG failed")?; + + let message_nonce: MessageNonce = rand::random(); + + Ok(Self::new_message( + *src_id, + message_nonce, + ciphertext.to_vec(), + )) + } + + /// Returns true if the packet is a WHOAREYOU packet. + pub fn is_whoareyou(&self) -> bool { + match &self.header.kind { + PacketKind::WhoAreYou { .. } => true, + PacketKind::Message { .. } | PacketKind::Handshake { .. } => false, + } + } + + /// Non-challenge (WHOAREYOU) packets contain the src_id of the node. This function returns the + /// src_id in this case. + pub fn src_id(&self) -> Option { + match self.header.kind { + PacketKind::Message { src_id } => Some(src_id), + PacketKind::WhoAreYou { .. } => None, + PacketKind::Handshake { src_id, .. } => Some(src_id), + } + } + + /// Returns the message nonce if one exists. + pub fn message_nonce(&self) -> &MessageNonce { + &self.header.message_nonce + } + + /// Generates the authenticated data for this packet. + pub fn authenticated_data(&self) -> Vec { + let mut authenticated_data = self.iv.to_be_bytes().to_vec(); + authenticated_data.extend_from_slice(&self.header.encode()); + authenticated_data + } + + /// Encodes a packet to bytes and performs the AES-CTR encryption. + pub fn encode(self, dst_id: &NodeId) -> Vec { + let header = self.encrypt_header(dst_id); + let mut buf = Vec::with_capacity(IV_LENGTH + header.len() + self.message.len()); + buf.extend_from_slice(&self.iv.to_be_bytes()); + buf.extend_from_slice(&header); + buf.extend_from_slice(&self.message); + buf + } + + /// Creates the masked header of a packet performing the required AES-CTR encryption. + fn encrypt_header(&self, dst_id: &NodeId) -> Vec { + let mut header_bytes = self.header.encode(); + + /* Encryption is done inline + * + * This was split into its own library, but brought back to allow re-use of the cipher when + * performing decryption + */ + let mut key = GenericArray::clone_from_slice(&dst_id.raw()[..16]); + let mut nonce = GenericArray::clone_from_slice(&self.iv.to_be_bytes()); + + let mut cipher = Aes128Ctr::new(&key, &nonce); + cipher.apply_keystream(&mut header_bytes); + key.zeroize(); + nonce.zeroize(); + header_bytes + } + + /// Decodes a packet (data) given our local source id (src_key). + /// + /// This also returns the authenticated data for further decryption in the handler. + pub fn decode(src_id: &NodeId, data: &[u8]) -> Result<(Self, Vec), PacketError> { + if data.len() > MAX_PACKET_SIZE { + return Err(PacketError::TooLarge); + } + if data.len() < MIN_PACKET_SIZE { + return Err(PacketError::TooSmall); + } + + // attempt to decrypt the static header + let iv = data[..IV_LENGTH].to_vec(); + + /* Decryption is done inline + * + * This was split into its own library, but brought back to allow re-use of the cipher when + * performing the decryption + */ + let key = GenericArray::clone_from_slice(&src_id.raw()[..16]); + let nonce = GenericArray::clone_from_slice(&iv); + let mut cipher = Aes128Ctr::new(&key, &nonce); + + // Take the static header content + let mut static_header = data[IV_LENGTH..IV_LENGTH + STATIC_HEADER_LENGTH].to_vec(); + cipher.apply_keystream(&mut static_header); + + // double check the size + if static_header.len() != STATIC_HEADER_LENGTH { + return Err(PacketError::HeaderLengthInvalid(static_header.len())); + } + + // Check the protocol id + if &static_header[..6] != PROTOCOL_ID.as_bytes() { + return Err(PacketError::HeaderDecryptionFailed); + } + + // Check the version matches + let version = u16::from_be_bytes( + static_header[6..8] + .try_into() + .expect("Must be correct size"), + ); + if version != VERSION { + return Err(PacketError::InvalidVersion(version)); + } + + let flag = static_header[8]; + + // Obtain the message nonce + let message_nonce: MessageNonce = static_header[9..9 + MESSAGE_NONCE_LENGTH] + .try_into() + .expect("Must be correct size"); + + // The decryption was successful, decrypt the remaining header + let auth_data_size = u16::from_be_bytes( + static_header[STATIC_HEADER_LENGTH - 2..] + .try_into() + .expect("Can only be 2 bytes in size"), + ); + + let remaining_data = data[IV_LENGTH + STATIC_HEADER_LENGTH..].to_vec(); + if auth_data_size as usize > remaining_data.len() { + return Err(PacketError::InvalidAuthDataSize); + } + + let mut auth_data = data[IV_LENGTH + STATIC_HEADER_LENGTH + ..IV_LENGTH + STATIC_HEADER_LENGTH + auth_data_size as usize] + .to_vec(); + cipher.apply_keystream(&mut auth_data); + + let kind = PacketKind::decode(flag, &auth_data)?; + + let header = PacketHeader { + message_nonce, + kind, + }; + + // Any remaining bytes are message data + let message = data[IV_LENGTH + STATIC_HEADER_LENGTH + auth_data_size as usize..].to_vec(); + + if !message.is_empty() && header.kind.is_whoareyou() { + // do not allow extra bytes being sent in WHOAREYOU messages + return Err(PacketError::UnknownPacket); + } + + // build the authenticated data + let mut authenticated_data = iv.to_vec(); + authenticated_data.extend_from_slice(&static_header); + authenticated_data.extend_from_slice(&auth_data); + + let packet = Packet { + iv: u128::from_be_bytes(iv[..].try_into().expect("IV_LENGTH must be 16 bytes")), + header, + message, + }; + + Ok((packet, authenticated_data)) + } +} + +impl std::fmt::Display for Packet { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Packet {{ iv: {}, header: {}, message {} }}", + hex::encode(self.iv.to_be_bytes()), + self.header.to_string(), + hex::encode(&self.message) + ) + } +} + +impl std::fmt::Display for PacketHeader { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "PacketHeader {{ message_nonce: {}, kind: {} }}", + hex::encode(self.message_nonce), + self.kind.to_string() + ) + } +} + +impl std::fmt::Display for PacketKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PacketKind::Message { src_id } => write!(f, "Message {{ src_id: {} }}", src_id), + PacketKind::WhoAreYou { id_nonce, enr_seq } => write!( + f, + "WhoAreYou {{ id_nonce: {}, enr_seq: {} }}", + hex::encode(id_nonce), + enr_seq + ), + PacketKind::Handshake { + src_id, + id_nonce_sig, + ephem_pubkey, + enr_record, + } => write!( + f, + "Handshake {{ src_id : {}, id_nonce_sig: {}, ephem_pubkey: {}, enr_record {:?}", + hex::encode(src_id.raw()), + hex::encode(id_nonce_sig), + hex::encode(ephem_pubkey), + enr_record + ), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use enr::{CombinedKey, EnrKey}; + + fn init_log() { + let _ = tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .try_init(); + } + + fn hex_decode(x: &'static str) -> Vec { + hex::decode(x).unwrap() + } + + fn node_key_1() -> CombinedKey { + CombinedKey::secp256k1_from_bytes(&mut hex_decode( + "eef77acb6c6a6eebc5b363a475ac583ec7eccdb42b6481424c60f59aa326547f", + )) + .unwrap() + } + + fn node_key_2() -> CombinedKey { + CombinedKey::secp256k1_from_bytes(&mut hex_decode( + "66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628", + )) + .unwrap() + } + + #[test] + fn packet_encode_random() { + init_log(); + let node_id_a: NodeId = node_key_1().public().into(); + let node_id_b: NodeId = node_key_2().public().into(); + + let expected_result = hex::decode("0000000000000000000000000000000b4f3ab1857252f96f758330a846b5d3d4a954d738dfcd6d1ed118ecc1d54f9b20fbf2be28db87805b23193e03c455d73d63ac71dfa91ffa010101010101010101010101").unwrap(); + let iv = 11u128; + let message_nonce = [12u8; MESSAGE_NONCE_LENGTH]; + let header = PacketHeader { + message_nonce, + kind: PacketKind::Message { src_id: node_id_a }, + }; + let message = [1u8; 12].to_vec(); + let packet = Packet { + iv, + header, + message, + }; + + let encoded = packet.encode(&node_id_b); + dbg!(hex::encode(&encoded)); + assert_eq!(expected_result, encoded); + } + + #[test] + fn packet_ref_test_encode_whoareyou() { + init_log(); + // reference input + let dst_id: NodeId = node_key_2().public().into(); + let request_nonce: MessageNonce = hex_decode("0102030405060708090a0b0c")[..] + .try_into() + .unwrap(); + let id_nonce: IdNonce = hex_decode("0102030405060708090a0b0c0d0e0f10")[..] + .try_into() + .unwrap(); + let enr_seq = 0u64; + let iv = 0u128; + + // expected hex output + let expected_output = hex::decode("00000000000000000000000000000000088b3d434277464933a1ccc59f5967ad1d6035f15e528627dde75cd68292f9e6c27d6b66c8100a873fcbaed4e16b8d").unwrap(); + + let header = PacketHeader { + message_nonce: request_nonce, + kind: PacketKind::WhoAreYou { id_nonce, enr_seq }, + }; + + let packet = Packet { + iv, + header, + message: Vec::new(), + }; + + assert_eq!(packet.encode(&dst_id), expected_output); + } + + #[test] + fn packet_encode_handshake() { + init_log(); + // reference input + let src_id = NodeId::parse(&[3; 32]).unwrap(); + let dst_id = NodeId::parse(&[4; 32]).unwrap(); + let message_nonce: MessageNonce = [52u8; MESSAGE_NONCE_LENGTH]; + let id_nonce_sig = vec![5u8; 64]; + let ephem_pubkey = vec![6u8; 33]; + let enr_record = None; + let iv = 0u128; + + let expected_output = hex::decode("0000000000000000000000000000000035a14bcdb844ae25f36070f07e0b25e765ed72b4d69c99d5fe5a8d438a4b5b518dfead9d80200875c23e31d0acda6f1b2a6124a70e3dc1f2b8b0770f24d8da18605ff3f5b60b090c61515093a88ef4c02186f7d1b5c9a88fdb8cfae239f13e451758751561b439d8044e27cecdf646f2aa1c9ecbd5faf37eb67a4f6337f4b2a885391e631f72deb808c63bf0b0faed23d7117f7a2e1f98c28bd0").unwrap(); + + let header = PacketHeader { + message_nonce, + kind: PacketKind::Handshake { + src_id, + id_nonce_sig, + ephem_pubkey, + enr_record, + }, + }; + + let packet = Packet { + iv, + header, + message: Vec::new(), + }; + let encoded = packet.encode(&dst_id); + assert_eq!(encoded, expected_output); + } + + #[test] + fn packet_encode_handshake_enr() { + // reference input + let node_key_1 = node_key_1(); + let src_id: NodeId = node_key_1.public().into(); + let dst_id = NodeId::parse(&[4; 32]).unwrap(); + let message_nonce: MessageNonce = [52u8; MESSAGE_NONCE_LENGTH]; + let id_nonce_sig = vec![5u8; 64]; + let ephem_pubkey = vec![6u8; 33]; + let enr_record: Option = Some("enr:-IS4QHXuNmr1vGEGVGDcy_sG2BZ7a3A7mbKS812BK_9rToQiF1Lfknsi5o0xKLnGJbTzBssJCzMcIj8SOiu1O9dnfZEBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQMT0UIR4Ch7I2GhYViQqbUhIIBUbQoleuTP-Wz1NJksuYN0Y3CCIyg".parse().unwrap()); + let iv = 0u128; + + let expected_output = hex::decode("0000000000000000000000000000000035a14bcdb844ae25f36070f07e0b25e765ed72b4d69d137c57dd97a97dd558d1d8e6e6b6fed699e55bb02b47d25562e0a6486ff2aba179f2b8b0770f24d8da18605ff3f5b60b090c61515093a88ef4c02186f7d1b5c9a88fdb8cfae239f13e451758751561b439d8044e27cecdf646f2aa1c9ecbd5faf37eb67a4f6337f4b2a885391e631f72deb808c63bf0b0faed23d7117f7a2e1f98c28bd0e908ce8b51cc89e592ed2efa671b8efd49e1ce8fd567fdb06ed308267d31f6bd75827812d21e8aa5a6c025e69b67faea57a15c1c9324d16938c4ebe71dba0bd5d7b00bb6de3e846ed37ef13a9d2e271f25233f5d97bbb026223dbe6595210f6a11cbee54589a0c0c20c7bb7c4c5bea46553480e1b7d4e83b2dd8305aac3b15fd9b1a1e13fda0").unwrap(); + + let header = PacketHeader { + message_nonce, + kind: PacketKind::Handshake { + src_id, + id_nonce_sig, + ephem_pubkey, + enr_record, + }, + }; + + let packet = Packet { + iv, + header, + message: Vec::new(), + }; + let encoded = packet.encode(&dst_id); + assert_eq!(encoded, expected_output); + } + + #[test] + fn packet_ref_test_encode_message() { + // reference input + let src_id: NodeId = node_key_1().public().into(); + let dst_id: NodeId = node_key_2().public().into(); + let iv = 0u128; + + let message_nonce: MessageNonce = [52u8; MESSAGE_NONCE_LENGTH]; + let header = PacketHeader { + message_nonce, + kind: PacketKind::Message { src_id }, + }; + let ciphertext = vec![23; 12]; + + let expected_output = hex::decode("00000000000000000000000000000000088b3d43427746493294faf2af68559e215d0bce6652be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d34c4f53245d08da171717171717171717171717").unwrap(); + + let packet = Packet { + iv, + header, + message: ciphertext, + }; + let encoded = packet.encode(&dst_id); + assert_eq!(encoded, expected_output); + } + + /* This section provides functionality testing of the packets */ + #[test] + fn packet_encode_decode_random() { + let src_id: NodeId = node_key_1().public().into(); + let dst_id: NodeId = node_key_2().public().into(); + + let packet = Packet::new_random(&src_id).unwrap(); + + let encoded_packet = packet.clone().encode(&dst_id); + let (decoded_packet, _authenticated_data) = + Packet::decode(&dst_id, &encoded_packet).unwrap(); + + assert_eq!(decoded_packet, packet); + } + + #[test] + fn packet_encode_decode_whoareyou() { + let dst_id: NodeId = node_key_2().public().into(); + + let message_nonce: MessageNonce = rand::random(); + let id_nonce: IdNonce = rand::random(); + let enr_seq: u64 = rand::random(); + + let packet = Packet::new_whoareyou(message_nonce, id_nonce, enr_seq); + + let encoded_packet = packet.clone().encode(&dst_id); + let (decoded_packet, _authenticated_data) = + Packet::decode(&dst_id, &encoded_packet).unwrap(); + + assert_eq!(decoded_packet, packet); + } + + #[test] + fn encode_decode_auth_packet() { + let src_id: NodeId = node_key_1().public().into(); + let dst_id: NodeId = node_key_2().public().into(); + + let message_nonce: MessageNonce = rand::random(); + let id_nonce_sig = vec![13; 64]; + let pubkey = vec![11; 33]; + let enr_record = None; + + let packet = + Packet::new_authheader(src_id, message_nonce, id_nonce_sig, pubkey, enr_record); + + let encoded_packet = packet.clone().encode(&dst_id); + let (decoded_packet, _authenticated_data) = + Packet::decode(&dst_id, &encoded_packet).unwrap(); + + assert_eq!(decoded_packet, packet); + } + + #[test] + fn packet_decode_ref_ping() { + let src_id: NodeId = node_key_1().public().into(); + let dst_id: NodeId = node_key_2().public().into(); + let message_nonce: MessageNonce = hex_decode("ffffffffffffffffffffffff")[..] + .try_into() + .unwrap(); + let iv = 0u128; + + let header = PacketHeader { + message_nonce, + kind: PacketKind::Message { src_id }, + }; + let ciphertext = hex_decode("b84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc"); + let expected_packet = Packet { + iv, + header, + message: ciphertext, + }; + + let encoded_ref_packet = hex::decode("00000000000000000000000000000000088b3d4342774649325f313964a39e55ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d34c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc").unwrap(); + + let (packet, _auth_data) = Packet::decode(&dst_id, &encoded_ref_packet).unwrap(); + assert_eq!(packet, expected_packet); + } + + #[test] + fn packet_decode_ref_ping_handshake() { + let src_id: NodeId = node_key_1().public().into(); + let dst_id: NodeId = node_key_2().public().into(); + let message_nonce: MessageNonce = hex_decode("ffffffffffffffffffffffff")[..] + .try_into() + .unwrap(); + let id_nonce_sig = hex_decode("c0a04b36f276172afc66a62848eb0769800c670c4edbefab8f26785e7fda6b56506a3f27ca72a75b106edd392a2cbf8a69272f5c1785c36d1de9d98a0894b2db"); + let ephem_pubkey = + hex_decode("039a003ba6517b473fa0cd74aefe99dadfdb34627f90fec6362df85803908f53a5"); + let enr_record = None; + let iv = 0u128; + + let header = PacketHeader { + message_nonce, + kind: PacketKind::Handshake { + src_id, + id_nonce_sig, + ephem_pubkey, + enr_record, + }, + }; + + let message = hex_decode("f1eadf5f0f4126b79336671cbcf7a885b1f8bd2a5d839cf8"); + let expected_packet = Packet { + iv, + header, + message, + }; + + let decoded_ref_packet = hex::decode("00000000000000000000000000000000088b3d4342774649305f313964a39e55ea96c005ad521d8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d34c4f53245d08da4bb252012b2cba3f4f374a90a75cff91f142fa9be3e0a5f3ef268ccb9065aeecfd67a999e7fdc137e062b2ec4a0eb92947f0d9a74bfbf44dfba776b21301f8b65efd5796706adff216ab862a9186875f9494150c4ae06fa4d1f0396c93f215fa4ef524f1eadf5f0f4126b79336671cbcf7a885b1f8bd2a5d839cf8").unwrap(); + + let (packet, _auth_data) = Packet::decode(&dst_id, &decoded_ref_packet).unwrap(); + assert_eq!(packet, expected_packet); + } + + #[test] + fn packet_decode_ref_ping_handshake_enr() { + let src_id: NodeId = node_key_1().public().into(); + let dst_id: NodeId = node_key_2().public().into(); + let message_nonce: MessageNonce = hex_decode("ffffffffffffffffffffffff")[..] + .try_into() + .unwrap(); + let id_nonce_sig = hex_decode("a439e69918e3f53f555d8ca4838fbe8abeab56aa55b056a2ac4d49c157ee719240a93f56c9fccfe7742722a92b3f2dfa27a5452f5aca8adeeab8c4d5d87df555"); + let ephem_pubkey = + hex_decode("039a003ba6517b473fa0cd74aefe99dadfdb34627f90fec6362df85803908f53a5"); + let enr_record = Some("enr:-H24QBfhsHORjaMtZAZCx2LA4ngWmOSXH4qzmnd0atrYPwHnb_yHTFkkgIu-fFCJCILCuKASh6CwgxLR1ToX1Rf16ycBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQMT0UIR4Ch7I2GhYViQqbUhIIBUbQoleuTP-Wz1NJksuQ".parse::().unwrap()); + let iv = 0u128; + + let header = PacketHeader { + message_nonce, + kind: PacketKind::Handshake { + src_id, + id_nonce_sig, + ephem_pubkey, + enr_record, + }, + }; + + let message = hex_decode("08d65093ccab5aa596a34d7511401987662d8cf62b139471"); + let expected_packet = Packet { + iv, + header, + message, + }; + + let encoded_ref_packet = hex::decode("00000000000000000000000000000000088b3d4342774649305f313964a39e55ea96c005ad539c8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d34c4f53245d08da4bb23698868350aaad22e3ab8dd034f548a1c43cd246be98562fafa0a1fa86d8e7a3b95ae78cc2b988ded6a5b59eb83ad58097252188b902b21481e30e5e285f19735796706adff216ab862a9186875f9494150c4ae06fa4d1f0396c93f215fa4ef524e0ed04c3c21e39b1868e1ca8105e585ec17315e755e6cfc4dd6cb7fd8e1a1f55e49b4b5eb024221482105346f3c82b15fdaae36a3bb12a494683b4a3c7f2ae41306252fed84785e2bbff3b022812d0882f06978df84a80d443972213342d04b9048fc3b1d5fcb1df0f822152eced6da4d3f6df27e70e4539717307a0208cd208d65093ccab5aa596a34d7511401987662d8cf62b139471").unwrap(); + + let (packet, _auth_data) = Packet::decode(&dst_id, &encoded_ref_packet).unwrap(); + assert_eq!(packet, expected_packet); + } + + #[test] + fn packet_decode_invalid_packet_size() { + let src_id: NodeId = node_key_1().public().into(); + + let data = [0; MAX_PACKET_SIZE + 1]; + let result = Packet::decode(&src_id, &data); + assert_eq!(result, Err(PacketError::TooLarge)); + + let data = [0; MIN_PACKET_SIZE - 1]; + let result = Packet::decode(&src_id, &data); + assert_eq!(result, Err(PacketError::TooSmall)); + } +} diff --git a/version-meld/discv5/src/permit_ban.rs b/version-meld/discv5/src/permit_ban.rs new file mode 100644 index 0000000..32af5a6 --- /dev/null +++ b/version-meld/discv5/src/permit_ban.rs @@ -0,0 +1,27 @@ +use crate::node_info::NodeAddress; +use enr::NodeId; +use std::{ + collections::{HashMap, HashSet}, + net::IpAddr, + time::Instant, +}; + +#[derive(Debug, Clone, Default)] +pub struct PermitBanList { + /// A set of IPs which pass all filters. + pub permit_ips: HashSet, + /// A set of IPs whose packets get dropped instantly. + pub ban_ips: HashMap>, + /// A set of NodeIds which pass all filters. + pub permit_nodes: HashSet, + /// A set of NodeIds whose packets get dropped instantly. + pub ban_nodes: HashMap>, +} + +impl PermitBanList { + pub fn ban(&mut self, node_address: NodeAddress, time_to_unban: Option) { + self.ban_ips + .insert(node_address.socket_addr.ip(), time_to_unban); + self.ban_nodes.insert(node_address.node_id, time_to_unban); + } +} diff --git a/version-meld/discv5/src/query_pool.rs b/version-meld/discv5/src/query_pool.rs new file mode 100644 index 0000000..ad3ba46 --- /dev/null +++ b/version-meld/discv5/src/query_pool.rs @@ -0,0 +1,293 @@ +// Copyright 2019 Parity Technologies (UK) Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// This basis of this file has been taken from the rust-libp2p codebase: +// https://github.com/libp2p/rust-libp2p + +mod peers; + +pub use peers::QueryState; +pub(crate) use peers::{ + closest::{FindNodeQuery, FindNodeQueryConfig}, + predicate::{PredicateQuery, PredicateQueryConfig}, +}; + +use crate::kbucket::{Key, PredicateKey}; +use fnv::FnvHashMap; +use std::time::{Duration, Instant}; + +pub trait TargetKey { + fn key(&self) -> Key; +} + +/// A `QueryPool` provides an aggregate state machine for driving `Query`s to completion. +/// +/// Internally, a `Query` is in turn driven by an underlying `QueryPeerIter` +/// that determines the peer selection strategy, i.e. the order in which the +/// peers involved in the query should be contacted. +pub struct QueryPool { + next_id: usize, + query_timeout: Duration, + queries: FnvHashMap>, +} + +/// The observable states emitted by [`QueryPool::poll`]. +#[allow(clippy::type_complexity)] +pub enum QueryPoolState<'a, TTarget, TNodeId, TResult> { + /// The pool is idle, i.e. there are no queries to process. + Idle, + /// At least one query is waiting for results. `Some(request)` indicates + /// that a new request is now being waited on. + Waiting(Option<(&'a mut Query, TNodeId)>), + /// A query has finished. + Finished(Query), + /// A query has timed out. + Timeout(Query), +} + +impl QueryPool +where + TTarget: TargetKey, + TNodeId: Into> + Eq + Clone, + TResult: Into + Clone, +{ + /// Creates a new `QueryPool` with the given configuration. + pub fn new(query_timeout: Duration) -> Self { + QueryPool { + next_id: 0, + query_timeout, + queries: Default::default(), + } + } + + /// Returns an iterator over the queries in the pool. + pub fn iter(&self) -> impl Iterator> { + self.queries.values() + } + + /// Adds a query to the pool that iterates towards the closest peers to the target. + pub fn add_findnode_query( + &mut self, + config: FindNodeQueryConfig, + target: TTarget, + peers: I, + ) -> QueryId + where + I: IntoIterator>, + { + let target_key = target.key(); + let findnode_query = FindNodeQuery::with_config(config, target_key, peers); + let peer_iter = QueryPeerIter::FindNode(findnode_query); + self.add(peer_iter, target) + } + + /// Adds a query to the pool that returns peers that satisfy a predicate. + pub(crate) fn add_predicate_query( + &mut self, + config: PredicateQueryConfig, + target: TTarget, + peers: I, + predicate: impl Fn(&TResult) -> bool + Send + 'static, + ) -> QueryId + where + I: IntoIterator>, + { + let target_key = target.key(); + let predicate_query = PredicateQuery::with_config(config, target_key, peers, predicate); + let peer_iter = QueryPeerIter::Predicate(predicate_query); + self.add(peer_iter, target) + } + + fn add(&mut self, peer_iter: QueryPeerIter, target: TTarget) -> QueryId { + let id = QueryId(self.next_id); + self.next_id = self.next_id.wrapping_add(1); + let query = Query::new(id, peer_iter, target); + self.queries.insert(id, query); + id + } + + /// Returns a mutable reference to a query with the given ID, if it is in the pool. + pub fn get_mut(&mut self, id: QueryId) -> Option<&mut Query> { + self.queries.get_mut(&id) + } + + /// Polls the pool to advance the queries. + pub fn poll(&mut self) -> QueryPoolState<'_, TTarget, TNodeId, TResult> { + let now = Instant::now(); + let mut finished = None; + let mut waiting = None; + let mut timeout = None; + + for (&query_id, query) in self.queries.iter_mut() { + query.started = query.started.or(Some(now)); + match query.next(now) { + QueryState::Finished => { + finished = Some(query_id); + break; + } + QueryState::Waiting(Some(return_peer)) => { + waiting = Some((query_id, return_peer)); + break; + } + QueryState::Waiting(None) | QueryState::WaitingAtCapacity => { + let elapsed = now - query.started.unwrap_or(now); + if elapsed >= self.query_timeout { + timeout = Some(query_id); + break; + } + } + } + } + + if let Some((query_id, return_peer)) = waiting { + let query = self.queries.get_mut(&query_id).expect("s.a."); + return QueryPoolState::Waiting(Some((query, return_peer))); + } + + if let Some(query_id) = finished { + let query = self.queries.remove(&query_id).expect("s.a."); + return QueryPoolState::Finished(query); + } + + if let Some(query_id) = timeout { + let query = self.queries.remove(&query_id).expect("s.a."); + return QueryPoolState::Timeout(query); + } + + if self.queries.is_empty() { + QueryPoolState::Idle + } else { + QueryPoolState::Waiting(None) + } + } +} + +/// Unique identifier for an active query. +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +pub struct QueryId(pub usize); + +impl std::ops::Deref for QueryId { + type Target = usize; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +/// A query in a `QueryPool`. +pub struct Query { + /// The unique ID of the query. + id: QueryId, + /// The peer iterator that drives the query state. + peer_iter: QueryPeerIter, + /// The instant when the query started (i.e. began waiting for the first + /// result from a peer). + started: Option, + /// Target we are looking for. + target: TTarget, +} + +/// The peer selection strategies that can be used by queries. +enum QueryPeerIter { + FindNode(FindNodeQuery), + Predicate(PredicateQuery), +} + +impl Query +where + TTarget: TargetKey, + TNodeId: Into> + Eq + Clone, + TResult: Into + Clone, +{ + /// Creates a new query without starting it. + fn new(id: QueryId, peer_iter: QueryPeerIter, target: TTarget) -> Self { + Query { + id, + peer_iter, + target, + started: None, + } + } + + /// Gets the unique ID of the query. + pub fn id(&self) -> QueryId { + self.id + } + + /// Informs the query that the attempt to contact `peer` failed. + pub fn on_failure(&mut self, peer: &TNodeId) { + match &mut self.peer_iter { + QueryPeerIter::FindNode(iter) => iter.on_failure(peer), + QueryPeerIter::Predicate(iter) => iter.on_failure(peer), + } + } + + /// Informs the query that the attempt to contact `peer` succeeded, + /// possibly resulting in new peers that should be incorporated into + /// the query, if applicable. + pub fn on_success<'a>(&mut self, peer: &TNodeId, new_peers: &'a [TResult]) + where + &'a TResult: Into, + { + match &mut self.peer_iter { + QueryPeerIter::FindNode(iter) => { + iter.on_success(peer, new_peers.iter().map(|result| result.into()).collect()) + } + QueryPeerIter::Predicate(iter) => iter.on_success(peer, new_peers), + } + } + + /// Advances the state of the underlying peer iterator. + fn next(&mut self, now: Instant) -> QueryState { + match &mut self.peer_iter { + QueryPeerIter::FindNode(iter) => iter.next(now), + QueryPeerIter::Predicate(iter) => iter.next(now), + } + } + + /// Consumes the query, producing the final `QueryResult`. + pub fn into_result(self) -> QueryResult> { + let peers = match self.peer_iter { + QueryPeerIter::FindNode(iter) => iter.into_result(), + QueryPeerIter::Predicate(iter) => iter.into_result(), + }; + QueryResult { + target: self.target, + closest_peers: peers.into_iter(), + } + } + + /// Returns a reference to the query `target`. + pub fn target(&self) -> &TTarget { + &self.target + } + + /// Returns a mutable reference to the query `target`. + pub fn target_mut(&mut self) -> &mut TTarget { + &mut self.target + } +} + +/// The result of a `Query`. +pub struct QueryResult { + /// The target of the query. + pub target: TTarget, + /// The closest peers to the target found by the query. + pub closest_peers: TClosest, +} diff --git a/version-meld/discv5/src/query_pool/peers.rs b/version-meld/discv5/src/query_pool/peers.rs new file mode 100644 index 0000000..9a9da5b --- /dev/null +++ b/version-meld/discv5/src/query_pool/peers.rs @@ -0,0 +1,68 @@ +// Copyright 2019 Parity Technologies (UK) Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// The basis of this file has been taken from the rust-libp2p codebase: +// https://github.com/libp2p/rust-libp2p + +//! Peer selection strategies for queries in the form of iterator-like state machines. +//! +//! Using a peer iterator in a query involves performing the following steps +//! repeatedly and in an alternating fashion: +//! +//! 1. Calling `next` to observe the next state of the iterator and determine +//! what to do, which is to either issue new requests to peers or continue +//! waiting for responses. +//! +//! 2. When responses are received or requests fail, providing input to the +//! iterator via the `on_success` and `on_failure` callbacks, +//! respectively, followed by repeating step (1). +//! +//! When a call to `next` returns [`Finished`], no more peers can be obtained +//! from the iterator and the results can be obtained from `into_result`. +//! +//! A peer iterator can be finished prematurely at any time through `finish`. +//! +//! [`Finished`]: QueryState::Finished + +pub mod closest; +pub mod predicate; + +/// The state of the query reported by [`closest::FindNodeQuery::next`] or +/// [`predicate::PredicateQuery::next`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum QueryState { + /// The query is waiting for results. + /// + /// `Some(peer)` indicates that the query is now waiting for a result + /// from `peer`, in addition to any other peers for which the query is already + /// waiting for results. + /// + /// `None` indicates that the query is waiting for results and there is no + /// new peer to contact, despite the query not being at capacity w.r.t. + /// the permitted parallelism. + Waiting(Option), + + /// The query is waiting for results and is at capacity w.r.t. the + /// permitted parallelism. + WaitingAtCapacity, + + /// The query finished. + Finished, +} diff --git a/version-meld/discv5/src/query_pool/peers/closest.rs b/version-meld/discv5/src/query_pool/peers/closest.rs new file mode 100644 index 0000000..fd64efa --- /dev/null +++ b/version-meld/discv5/src/query_pool/peers/closest.rs @@ -0,0 +1,707 @@ +// Copyright 2018 Parity Technologies (UK) Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// This basis of this file has been taken from the rust-libp2p codebase: +// https://github.com/libp2p/rust-libp2p +// +use super::*; +use crate::{ + config::Discv5Config, + kbucket::{Distance, Key, MAX_NODES_PER_BUCKET}, +}; +use std::{ + collections::btree_map::{BTreeMap, Entry}, + time::{Duration, Instant}, +}; + +#[derive(Debug, Clone)] +pub struct FindNodeQuery { + /// The target key we are looking for + target_key: Key, + + /// The current state of progress of the query. + progress: QueryProgress, + + /// The closest peers to the target, ordered by increasing distance. + closest_peers: BTreeMap>, + + /// The number of peers for which the query is currently waiting for results. + num_waiting: usize, + + /// The configuration of the query. + config: FindNodeQueryConfig, +} + +/// Configuration for a `Query`. +#[derive(Debug, Clone)] +pub struct FindNodeQueryConfig { + /// Allowed level of parallelism. + /// + /// The `α` parameter in the Kademlia paper. The maximum number of peers that a query + /// is allowed to wait for in parallel while iterating towards the closest + /// nodes to a target. Defaults to `3`. + pub parallelism: usize, + + /// Number of results to produce. + /// + /// The number of closest peers that a query must obtain successful results + /// for before it terminates. Defaults to the maximum number of entries in a + /// single k-bucket, i.e. the `k` parameter in the Kademlia paper. + pub num_results: usize, + + /// The timeout for a single peer. + /// + /// If a successful result is not reported for a peer within this timeout + /// window, the iterator considers the peer unresponsive and will not wait for + /// the peer when evaluating the termination conditions, until and unless a + /// result is delivered. Defaults to `10` seconds. + pub peer_timeout: Duration, +} + +impl FindNodeQueryConfig { + pub fn new_from_config(config: &Discv5Config) -> Self { + Self { + parallelism: config.query_parallelism, + num_results: MAX_NODES_PER_BUCKET, + peer_timeout: config.query_peer_timeout, + } + } +} + +impl FindNodeQuery +where + TNodeId: Into> + Eq + Clone, +{ + /// Creates a new query with the given configuration. + pub fn with_config( + config: FindNodeQueryConfig, + target_key: Key, + known_closest_peers: I, + ) -> Self + where + I: IntoIterator>, + { + // Initialise the closest peers to begin the query with. + let closest_peers = known_closest_peers + .into_iter() + .map(|key| { + let key: Key = key; + let distance = key.distance(&target_key); + let state = QueryPeerState::NotContacted; + (distance, QueryPeer::new(key, state)) + }) + .take(config.num_results) + .collect(); + + // The query initially makes progress by iterating towards the target. + let progress = QueryProgress::Iterating { no_progress: 0 }; + + FindNodeQuery { + config, + target_key, + progress, + closest_peers, + num_waiting: 0, + } + } + + /// Callback for delivering the result of a successful request to a peer + /// that the query is waiting on. + /// + /// Delivering results of requests back to the query allows the query to make + /// progress. The query is said to make progress either when the given + /// `closer_peers` contain a peer closer to the target than any peer seen so far, + /// or when the query did not yet accumulate `num_results` closest peers and + /// `closer_peers` contains a new peer, regardless of its distance to the target. + /// + /// After calling this function, `next` should eventually be called again + /// to advance the state of the query. + /// + /// If the query is finished, the query is not currently waiting for a + /// result from `peer`, or a result for `peer` has already been reported, + /// calling this function has no effect. + pub fn on_success(&mut self, node_id: &TNodeId, closer_peers: Vec) { + if let QueryProgress::Finished = self.progress { + return; + } + + let key: Key = node_id.clone().into(); + let distance = key.distance(&self.target_key); + + // Mark the peer's progress, the total nodes it has returned and it's current iteration. + // If the node returned peers, mark it as succeeded. + match self.closest_peers.entry(distance) { + Entry::Vacant(..) => return, + Entry::Occupied(mut e) => match e.get().state { + QueryPeerState::Waiting(..) => { + debug_assert!(self.num_waiting > 0); + self.num_waiting -= 1; + let peer = e.get_mut(); + peer.peers_returned += closer_peers.len(); + // mark the peer as succeeded + peer.state = QueryPeerState::Succeeded; + } + QueryPeerState::Unresponsive => { + let peer = e.get_mut(); + peer.peers_returned += closer_peers.len(); + // mark the peer as succeeded + peer.state = QueryPeerState::Succeeded; + } + QueryPeerState::NotContacted + | QueryPeerState::Failed + | QueryPeerState::Succeeded => return, + }, + } + + let mut progress = false; + let num_closest = self.closest_peers.len(); + + // Incorporate the reported closer peers into the query. + for peer in closer_peers { + let key: Key = peer.into(); + let distance = self.target_key.distance(&key); + let peer = QueryPeer::new(key, QueryPeerState::NotContacted); + self.closest_peers.entry(distance).or_insert(peer); + // The query makes progress if the new peer is either closer to the target + // than any peer seen so far (i.e. is the first entry), or the query did + // not yet accumulate enough closest peers. + progress = self.closest_peers.keys().next() == Some(&distance) + || num_closest < self.config.num_results; + } + + // Update the query progress. + self.progress = match self.progress { + QueryProgress::Iterating { no_progress } => { + let no_progress = if progress { 0 } else { no_progress + 1 }; + if no_progress >= self.config.parallelism { + QueryProgress::Stalled + } else { + QueryProgress::Iterating { no_progress } + } + } + QueryProgress::Stalled => { + if progress { + QueryProgress::Iterating { no_progress: 0 } + } else { + QueryProgress::Stalled + } + } + QueryProgress::Finished => QueryProgress::Finished, + } + } + + /// Callback for informing the query about a failed request to a peer + /// that the query is waiting on. + /// + /// After calling this function, `next` should eventually be called again + /// to advance the state of the query. + /// + /// If the query is finished, the query is not currently waiting for a + /// result from `peer`, or a result for `peer` has already been reported, + /// calling this function has no effect. + pub fn on_failure(&mut self, peer: &TNodeId) { + if let QueryProgress::Finished = self.progress { + return; + } + + let key: Key = peer.clone().into(); + let distance = key.distance(&self.target_key); + + match self.closest_peers.entry(distance) { + Entry::Vacant(_) => {} + Entry::Occupied(mut e) => match e.get().state { + QueryPeerState::Waiting(..) => { + debug_assert!(self.num_waiting > 0); + self.num_waiting -= 1; + e.get_mut().state = QueryPeerState::Failed + } + QueryPeerState::Unresponsive => e.get_mut().state = QueryPeerState::Failed, + _ => {} + }, + } + } + + /// Advances the state of the query, potentially getting a new peer to contact. + /// + /// See [`QueryState`]. + pub fn next(&mut self, now: Instant) -> QueryState { + if let QueryProgress::Finished = self.progress { + return QueryState::Finished; + } + + // Count the number of peers that returned a result. If there is a + // request in progress to one of the `num_results` closest peers, the + // counter is set to `None` as the query can only finish once + // `num_results` closest peers have responded (or there are no more + // peers to contact, see `active_counter`). + let mut result_counter = Some(0); + + // Check if the query is at capacity w.r.t. the allowed parallelism. + let at_capacity = self.at_capacity(); + + for peer in self.closest_peers.values_mut() { + match peer.state { + QueryPeerState::NotContacted => { + // This peer is waiting to be reiterated. + if !at_capacity { + let timeout = now + self.config.peer_timeout; + peer.state = QueryPeerState::Waiting(timeout); + self.num_waiting += 1; + let peer = peer.key.preimage().clone(); + return QueryState::Waiting(Some(peer)); + } else { + return QueryState::WaitingAtCapacity; + } + } + + QueryPeerState::Waiting(timeout) => { + if now >= timeout { + // Peers that don't respond within timeout are set to `Failed`. + debug_assert!(self.num_waiting > 0); + self.num_waiting -= 1; + peer.state = QueryPeerState::Unresponsive; + } else if at_capacity { + // The query is still waiting for a result from a peer and is + // at capacity w.r.t. the maximum number of peers being waited on. + return QueryState::WaitingAtCapacity; + } else { + // The query is still waiting for a result from a peer and the + // `result_counter` did not yet reach `num_results`. Therefore + // the query is not yet done, regardless of already successful + // queries to peers farther from the target. + result_counter = None; + } + } + + QueryPeerState::Succeeded => { + if let Some(ref mut cnt) = result_counter { + *cnt += 1; + // If `num_results` successful results have been delivered for the + // closest peers, the query is done. + if *cnt >= self.config.num_results { + self.progress = QueryProgress::Finished; + return QueryState::Finished; + } + } + } + + QueryPeerState::Failed | QueryPeerState::Unresponsive => { + // Skip over unresponsive or failed peers. + } + } + } + + if self.num_waiting > 0 { + // The query is still waiting for results and not at capacity w.r.t. + // the allowed parallelism, but there are no new peers to contact + // at the moment. + QueryState::Waiting(None) + } else { + // The query is finished because all available peers have been contacted + // and the query is not waiting for any more results. + self.progress = QueryProgress::Finished; + QueryState::Finished + } + } + + /// Consumes the query, returning the target and the closest peers. + pub fn into_result(self) -> Vec { + self.closest_peers + .into_iter() + .filter_map(|(_, peer)| { + if let QueryPeerState::Succeeded = peer.state { + Some(peer.key.into_preimage()) + } else { + None + } + }) + .take(self.config.num_results) + .collect() + } + + /// Checks if the query is at capacity w.r.t. the permitted parallelism. + /// + /// While the query is stalled, up to `num_results` parallel requests + /// are allowed. This is a slightly more permissive variant of the + /// requirement that the initiator "resends the FIND_NODE to all of the + /// k closest nodes it has not already queried". + fn at_capacity(&self) -> bool { + match self.progress { + QueryProgress::Stalled => self.num_waiting >= self.config.num_results, + QueryProgress::Iterating { .. } => self.num_waiting >= self.config.parallelism, + QueryProgress::Finished => true, + } + } +} + +/// Stage of the query. +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum QueryProgress { + /// The query is making progress by iterating towards `num_results` closest + /// peers to the target with a maximum of `parallelism` peers for which the + /// query is waiting for results at a time. + /// + /// > **Note**: When the query switches back to `Iterating` after being + /// > `Stalled`, it may temporarily be waiting for more than `parallelism` + /// > results from peers, with new peers only being considered once + /// > the number pending results drops below `parallelism`. + Iterating { + /// The number of consecutive results that did not yield a peer closer + /// to the target. When this number reaches `parallelism` and no new + /// peer was discovered or at least `num_results` peers are known to + /// the query, it is considered `Stalled`. + no_progress: usize, + }, + + /// A query is stalled when it did not make progress after `parallelism` + /// consecutive successful results (see `on_success`). + /// + /// While the query is stalled, the maximum allowed parallelism for pending + /// results is increased to `num_results` in an attempt to finish the query. + /// If the query can make progress again upon receiving the remaining + /// results, it switches back to `Iterating`. Otherwise it will be finished. + Stalled, + + /// The query is finished. + /// + /// A query finishes either when it has collected `num_results` results + /// from the closest peers (not counting those that failed or are unresponsive) + /// or because the query ran out of peers that have not yet delivered + /// results (or failed). + Finished, +} + +/// Representation of a peer in the context of a query. +#[derive(Debug, Clone)] +struct QueryPeer { + /// The `KBucket` key used to identify the peer. + key: Key, + + /// The number of peers that have been returned by this peer. + peers_returned: usize, + + /// The current query state of this peer. + state: QueryPeerState, +} + +impl QueryPeer { + pub fn new(key: Key, state: QueryPeerState) -> Self { + QueryPeer { + key, + peers_returned: 0, + state, + } + } +} + +/// The state of `QueryPeer` in the context of a query. +#[derive(Debug, Copy, Clone)] +enum QueryPeerState { + /// The peer has not yet been contacted. + /// + /// This is the starting state for every peer known to, or discovered by, a query. + NotContacted, + + /// The query is waiting for a result from the peer. + Waiting(Instant), + + /// A result was not delivered for the peer within the configured timeout. + /// + /// The peer is not taken into account for the termination conditions + /// of the iterator until and unless it responds. + Unresponsive, + + /// Obtaining a result from the peer has failed. + /// + /// This is a final state, reached as a result of a call to `on_failure`. + Failed, + + /// A successful result from the peer has been delivered. + /// + /// This is a final state, reached as a result of a call to `on_success`. + Succeeded, +} + +#[cfg(test)] +mod tests { + use super::*; + use enr::NodeId; + use quickcheck::*; + use rand_07::{thread_rng, Rng}; + use std::time::Duration; + + type TestQuery = FindNodeQuery; + + fn random_nodes(n: usize) -> impl Iterator + Clone { + (0..n).map(|_| NodeId::random()) + } + + fn random_query(g: &mut G) -> TestQuery { + let known_closest_peers = random_nodes(g.gen_range(1, 60)).map(Key::from); + let target = NodeId::random(); + let config = FindNodeQueryConfig { + parallelism: g.gen_range(1, 10), + num_results: g.gen_range(1, 25), + peer_timeout: Duration::from_secs(g.gen_range(10, 30)), + }; + FindNodeQuery::with_config(config, target.into(), known_closest_peers) + } + + fn sorted(target: &Key, peers: &[Key]) -> bool { + peers + .windows(2) + .all(|w| w[0].distance(&target) < w[1].distance(&target)) + } + + impl Arbitrary for TestQuery { + fn arbitrary(g: &mut G) -> TestQuery { + random_query(g) + } + } + + #[test] + fn new_query() { + let query = random_query(&mut thread_rng()); + let target = query.target_key.clone(); + + let (keys, states): (Vec<_>, Vec<_>) = query + .closest_peers + .values() + .map(|e| (e.key.clone(), &e.state)) + .unzip(); + + let none_contacted = states + .iter() + .all(|s| matches!(s, QueryPeerState::NotContacted)); + + assert!(none_contacted, "Unexpected peer state in new query."); + assert!( + sorted(&target, &keys), + "Closest peers in new query not sorted by distance to target." + ); + assert_eq!( + query.num_waiting, 0, + "Unexpected peers in progress in new query." + ); + assert!( + query.into_result().is_empty(), + "Unexpected closest peers in new query" + ); + } + + #[test] + fn termination_and_parallelism() { + fn prop(mut query: TestQuery) { + let now = Instant::now(); + let mut rng = thread_rng(); + + let mut expected = query + .closest_peers + .values() + .map(|e| e.key.clone()) + .collect::>(); + let num_known = expected.len(); + let max_parallelism = usize::min(query.config.parallelism, num_known); + + let target = query.target_key.clone(); + let mut remaining; + let mut num_failures = 0; + + 'finished: loop { + if expected.is_empty() { + break; + } + // Split off the next up to `parallelism` expected peers. + else if expected.len() < max_parallelism { + remaining = Vec::new(); + } else { + remaining = expected.split_off(max_parallelism); + } + + // Advance the query for maximum parallelism. + for k in expected.iter() { + match query.next(now) { + QueryState::Finished => break 'finished, + QueryState::Waiting(Some(p)) => assert_eq!(&p, k.preimage()), + QueryState::Waiting(None) => panic!("Expected another peer."), + QueryState::WaitingAtCapacity => panic!("Unexpectedly reached capacity."), + } + } + let num_waiting = query.num_waiting; + assert_eq!(num_waiting, expected.len()); + + // Check the bounded parallelism. + if query.at_capacity() { + assert_eq!(query.next(now), QueryState::WaitingAtCapacity) + } + + // Report results back to the query with a random number of "closer" + // peers or an error, thus finishing the "in-flight requests". + for (i, k) in expected.iter().enumerate() { + if rng.gen_bool(0.75) { + let num_closer = rng.gen_range(0, query.config.num_results + 1); + let closer_peers = random_nodes(num_closer).collect::>(); + // let _: () = remaining; + remaining.extend(closer_peers.iter().map(|x| Key::from(*x))); + query.on_success(k.preimage(), closer_peers); + } else { + num_failures += 1; + query.on_failure(k.preimage()); + } + assert_eq!(query.num_waiting, num_waiting - (i + 1)); + } + + // Re-sort the remaining expected peers for the next "round". + remaining.sort_by_key(|k| target.distance(&k)); + + expected = remaining + } + + // The query must be finished. + assert_eq!(query.next(now), QueryState::Finished); + assert_eq!(query.progress, QueryProgress::Finished); + + // Determine if all peers have been contacted by the query. This _must_ be + // the case if the query finished with fewer than the requested number + // of results. + let all_contacted = query.closest_peers.values().all(|e| { + !matches!( + e.state, + QueryPeerState::NotContacted | QueryPeerState::Waiting { .. } + ) + }); + + let target_key = query.target_key.clone(); + let num_results = query.config.num_results; + let result = query.into_result(); + let closest = result.into_iter().map(Key::from).collect::>(); + + // assert_eq!(result.target, target); + assert!(sorted(&target_key, &closest)); + + if closest.len() < num_results { + // The query returned fewer results than requested. Therefore + // either the initial number of known peers must have been + // less than the desired number of results, or there must + // have been failures. + assert!(num_known < num_results || num_failures > 0); + // All peers must have been contacted. + assert!(all_contacted, "Not all peers have been contacted."); + } else { + assert_eq!(num_results, closest.len(), "Too many results."); + } + } + + QuickCheck::new().tests(10).quickcheck(prop as fn(_) -> _) + } + + #[test] + fn no_duplicates() { + fn prop(mut query: TestQuery) -> bool { + let now = Instant::now(); + let closer: Vec = random_nodes(1).collect(); + + // A first peer reports a "closer" peer. + let peer1 = if let QueryState::Waiting(Some(p)) = query.next(now) { + p + } else { + panic!("No peer."); + }; + query.on_success(&peer1, closer.clone()); + // Duplicate result from the same peer. + query.on_success(&peer1, closer.clone()); + + // If there is a second peer, let it also report the same "closer" peer. + match query.next(now) { + QueryState::Waiting(Some(p)) => { + let peer2 = p; + query.on_success(&peer2, closer.clone()) + } + QueryState::Finished => {} + _ => panic!("Unexpectedly query state."), + }; + + // The "closer" peer must only be in the query once. + let n = query + .closest_peers + .values() + .filter(|e| e.key.preimage() == &closer[0]) + .count(); + assert_eq!(n, 1); + + true + } + + QuickCheck::new().tests(10).quickcheck(prop as fn(_) -> _) + } + + #[test] + fn timeout() { + fn prop(mut query: TestQuery) -> bool { + let mut now = Instant::now(); + let peer = query + .closest_peers + .values() + .next() + .unwrap() + .key + .clone() + .into_preimage(); + // Poll the query for the first peer to be in progress. + match query.next(now) { + QueryState::Waiting(Some(id)) => assert_eq!(id, peer), + _ => panic!(), + } + + // Artificially advance the clock. + now += query.config.peer_timeout; + + // Advancing the query again should mark the first peer as unresponsive. + let _ = query.next(now); + match &query.closest_peers.values().next().unwrap() { + QueryPeer { + key, + state: QueryPeerState::Unresponsive, + .. + } => { + assert_eq!(key.preimage(), &peer); + } + QueryPeer { state, .. } => panic!("Unexpected peer state: {:?}", state), + } + + let finished = query.progress == QueryProgress::Finished; + query.on_success(&peer, Vec::::new()); + let closest = query.into_result(); + + if finished { + // Delivering results when the query already finished must have + // no effect. + assert_eq!(Vec::::new(), closest); + } else { + // Unresponsive peers can still deliver results while the iterator + // is not finished. + assert_eq!(vec![peer], closest) + } + true + } + + QuickCheck::new().tests(10).quickcheck(prop as fn(_) -> _) + } +} diff --git a/version-meld/discv5/src/query_pool/peers/predicate.rs b/version-meld/discv5/src/query_pool/peers/predicate.rs new file mode 100644 index 0000000..4768a1c --- /dev/null +++ b/version-meld/discv5/src/query_pool/peers/predicate.rs @@ -0,0 +1,438 @@ +use super::*; +use crate::{ + config::Discv5Config, + kbucket::{Distance, Key, PredicateKey, MAX_NODES_PER_BUCKET}, +}; +use std::{ + collections::btree_map::{BTreeMap, Entry}, + time::{Duration, Instant}, +}; + +pub(crate) struct PredicateQuery { + /// The target key we are looking for + target_key: Key, + + /// The current state of progress of the query. + progress: QueryProgress, + + /// The closest peers to the target, ordered by increasing distance. + closest_peers: BTreeMap>, + + /// The number of peers for which the query is currently waiting for results. + num_waiting: usize, + + /// The predicate function to be applied to filter the ENR's found during the search. + predicate: Box bool + Send + 'static>, + + /// The configuration of the query. + config: PredicateQueryConfig, +} + +/// Configuration for a `Query`. +#[derive(Debug, Clone)] +pub(crate) struct PredicateQueryConfig { + /// Allowed level of parallelism. + /// + /// The `α` parameter in the Kademlia paper. The maximum number of peers that a query + /// is allowed to wait for in parallel while iterating towards the closest + /// nodes to a target. Defaults to `3`. + pub(crate) parallelism: usize, + + /// Number of results to produce. + /// + /// The number of closest peers that a query must obtain successful results + /// for before it terminates. Defaults to the maximum number of entries in a + /// single k-bucket, i.e. the `k` parameter in the Kademlia paper. + pub(crate) num_results: usize, + + /// The timeout for a single peer. + /// + /// If a successful result is not reported for a peer within this timeout + /// window, the iterator considers the peer unresponsive and will not wait for + /// the peer when evaluating the termination conditions, until and unless a + /// result is delivered. Defaults to `10` seconds. + pub(crate) peer_timeout: Duration, +} + +impl PredicateQueryConfig { + pub(crate) fn new_from_config(config: &Discv5Config) -> Self { + Self { + parallelism: config.query_parallelism, + num_results: MAX_NODES_PER_BUCKET, + peer_timeout: config.query_peer_timeout, + } + } +} + +impl PredicateQuery +where + TNodeId: Into> + Eq + Clone, + TResult: Into + Clone, +{ + /// Creates a new query with the given configuration. + pub fn with_config( + config: PredicateQueryConfig, + target_key: Key, + known_closest_peers: I, + predicate: impl Fn(&TResult) -> bool + Send + 'static, + ) -> Self + where + I: IntoIterator>, + { + // Initialise the closest peers to begin the query with. + let closest_peers = known_closest_peers + .into_iter() + .map(|key| { + let predicate_match = key.predicate_match; + let key: Key = key.into(); + let distance = key.distance(&target_key); + let state = QueryPeerState::NotContacted; + + (distance, QueryPeer::new(key, state, predicate_match)) + }) + .take(config.num_results) + .collect(); + + // The query initially makes progress by iterating towards the target. + let progress = QueryProgress::Iterating { no_progress: 0 }; + + PredicateQuery { + config, + target_key, + progress, + closest_peers, + num_waiting: 0, + predicate: Box::new(predicate), + } + } + + /// Callback for delivering the result of a successful request to a peer + /// that the query is waiting on. + /// + /// Delivering results of requests back to the query allows the query to make + /// progress. The query is said to make progress either when the given + /// `closer_peers` contain a peer closer to the target than any peer seen so far, + /// or when the query did not yet accumulate `num_results` closest peers and + /// `closer_peers` contains a new peer, regardless of its distance to the target. + /// + /// After calling this function, `next` should eventually be called again + /// to advance the state of the query. + /// + /// If the query is finished, the query is not currently waiting for a + /// result from `peer`, or a result for `peer` has already been reported, + /// calling this function has no effect. + pub fn on_success<'a>(&mut self, node_id: &TNodeId, closer_peers: &'a [TResult]) + where + &'a TResult: Into, + { + if let QueryProgress::Finished = self.progress { + return; + } + + let key: Key = node_id.clone().into(); + let distance = key.distance(&self.target_key); + + // Mark the peer's progress, the total nodes it has returned and it's current iteration. + // If the node returned peers, mark it as succeeded. + match self.closest_peers.entry(distance) { + Entry::Vacant(..) => return, + Entry::Occupied(mut e) => match e.get().state { + QueryPeerState::Waiting(..) => { + debug_assert!(self.num_waiting > 0); + self.num_waiting -= 1; + let peer = e.get_mut(); + peer.peers_returned += closer_peers.len(); + // mark the peer as succeeded + peer.state = QueryPeerState::Succeeded; + } + QueryPeerState::Unresponsive => { + let peer = e.get_mut(); + peer.peers_returned += closer_peers.len(); + // mark the peer as succeeded + peer.state = QueryPeerState::Succeeded; + } + QueryPeerState::NotContacted + | QueryPeerState::Failed + | QueryPeerState::Succeeded => return, + }, + } + + let mut progress = false; + let num_closest = self.closest_peers.len(); + + // Incorporate the reported closer peers into the query. + for result in closer_peers { + // If ENR satisfies the predicate, add to list of peers that satisfies predicate + let predicate_match = (self.predicate)(result); + let key: TNodeId = result.into(); + let key: Key = key.into(); + let distance = self.target_key.distance(&key); + let peer = QueryPeer::new(key, QueryPeerState::NotContacted, predicate_match); + self.closest_peers.entry(distance).or_insert(peer); + // The query makes progress if the new peer is either closer to the target + // than any peer seen so far (i.e. is the first entry), or the query did + // not yet accumulate enough closest peers. + progress = self.closest_peers.keys().next() == Some(&distance) + || num_closest < self.config.num_results; + } + + // Update the query progress. + self.progress = match self.progress { + QueryProgress::Iterating { no_progress } => { + let no_progress = if progress { 0 } else { no_progress + 1 }; + if no_progress >= self.config.parallelism { + QueryProgress::Stalled + } else { + QueryProgress::Iterating { no_progress } + } + } + QueryProgress::Stalled => { + if progress { + QueryProgress::Iterating { no_progress: 0 } + } else { + QueryProgress::Stalled + } + } + QueryProgress::Finished => QueryProgress::Finished, + } + } + + /// Callback for informing the query about a failed request to a peer + /// that the query is waiting on. + /// + /// After calling this function, `next` should eventually be called again + /// to advance the state of the query. + /// + /// If the query is finished, the query is not currently waiting for a + /// result from `peer`, or a result for `peer` has already been reported, + /// calling this function has no effect. + pub fn on_failure(&mut self, peer: &TNodeId) { + if let QueryProgress::Finished = self.progress { + return; + } + + let key: Key = peer.clone().into(); + let distance = key.distance(&self.target_key); + + if let Entry::Occupied(mut e) = self.closest_peers.entry(distance) { + if let QueryPeerState::Waiting(..) = e.get().state { + debug_assert!(self.num_waiting > 0); + self.num_waiting -= 1; + e.get_mut().state = QueryPeerState::Failed + } + } + } + + /// Advances the state of the query, potentially getting a new peer to contact. + /// + /// See [`QueryState`]. + pub fn next(&mut self, now: Instant) -> QueryState { + if let QueryProgress::Finished = self.progress { + return QueryState::Finished; + } + + // Count the number of peers that returned a result. If there is a + // request in progress to one of the `num_results` closest peers, the + // counter is set to `None` as the query can only finish once + // `num_results` closest peers have responded (or there are no more + // peers to contact, see `active_counter`). + let mut result_counter = Some(0); + + // Check if the query is at capacity w.r.t. the allowed parallelism. + let at_capacity = self.at_capacity(); + + for peer in self.closest_peers.values_mut() { + match peer.state { + QueryPeerState::NotContacted => { + // This peer is waiting to be reiterated. + if !at_capacity { + let timeout = now + self.config.peer_timeout; + peer.state = QueryPeerState::Waiting(timeout); + self.num_waiting += 1; + let return_peer = peer.key.preimage().clone(); + return QueryState::Waiting(Some(return_peer)); + } else { + return QueryState::WaitingAtCapacity; + } + } + + QueryPeerState::Waiting(timeout) => { + if now >= timeout { + // Peers that don't respond within timeout are set to `Failed`. + debug_assert!(self.num_waiting > 0); + self.num_waiting -= 1; + peer.state = QueryPeerState::Unresponsive; + } else if at_capacity { + // The query is still waiting for a result from a peer and is + // at capacity w.r.t. the maximum number of peers being waited on. + return QueryState::WaitingAtCapacity; + } else { + // The query is still waiting for a result from a peer and the + // `result_counter` did not yet reach `num_results`. Therefore + // the query is not yet done, regardless of already successful + // queries to peers farther from the target. + // Only count predicate peers. + if peer.predicate_match { + result_counter = None; + } + } + } + + QueryPeerState::Succeeded => { + if let Some(ref mut cnt) = result_counter { + if peer.predicate_match { + *cnt += 1; + // If `num_results` successful results have been delivered for the + // closest peers, the query is done. + if *cnt >= self.config.num_results { + self.progress = QueryProgress::Finished; + return QueryState::Finished; + } + } + } + } + + QueryPeerState::Failed | QueryPeerState::Unresponsive => { + // Skip over unresponsive or failed peers. + } + } + } + + if self.num_waiting > 0 { + // The query is still waiting for results and not at capacity w.r.t. + // the allowed parallelism, but there are no new peers to contact + // at the moment. + QueryState::Waiting(None) + } else { + // The query is finished because all available peers have been contacted + // and the query is not waiting for any more results. + self.progress = QueryProgress::Finished; + QueryState::Finished + } + } + + /// Consumes the query, returning the peers who match the predicate. + pub fn into_result(self) -> Vec { + self.closest_peers + .into_iter() + .filter_map(|(_, peer)| { + if let QueryPeerState::Succeeded = peer.state { + if peer.predicate_match { + Some(peer.key.into_preimage()) + } else { + None + } + } else { + None + } + }) + .take(self.config.num_results) + .collect() + } + + /// Checks if the query is at capacity w.r.t. the permitted parallelism. + /// + /// While the query is stalled, up to `num_results` parallel requests + /// are allowed. This is a slightly more permissive variant of the + /// requirement that the initiator "resends the FIND_NODE to all of the + /// k closest nodes it has not already queried". + fn at_capacity(&self) -> bool { + match self.progress { + QueryProgress::Stalled => self.num_waiting >= self.config.num_results, + QueryProgress::Iterating { .. } => self.num_waiting >= self.config.parallelism, + QueryProgress::Finished => true, + } + } +} + +/// Stage of the query. +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum QueryProgress { + /// The query is making progress by iterating towards `num_results` closest + /// peers to the target with a maximum of `parallelism` peers for which the + /// query is waiting for results at a time. + /// + /// > **Note**: When the query switches back to `Iterating` after being + /// > `Stalled`, it may temporarily be waiting for more than `parallelism` + /// > results from peers, with new peers only being considered once + /// > the number pending results drops below `parallelism`. + Iterating { + /// The number of consecutive results that did not yield a peer closer + /// to the target. When this number reaches `parallelism` and no new + /// peer was discovered or at least `num_results` peers are known to + /// the query, it is considered `Stalled`. + no_progress: usize, + }, + + /// A query is stalled when it did not make progress after `parallelism` + /// consecutive successful results (see `on_success`). + /// + /// While the query is stalled, the maximum allowed parallelism for pending + /// results is increased to `num_results` in an attempt to finish the query. + /// If the query can make progress again upon receiving the remaining + /// results, it switches back to `Iterating`. Otherwise it will be finished. + Stalled, + + /// The query is finished. + /// + /// A query finishes either when it has collected `num_results` results + /// from the closest peers (not counting those that failed or are unresponsive) + /// or because the query ran out of peers that have not yet delivered + /// results (or failed). + Finished, +} + +/// Representation of a peer in the context of a query. +#[derive(Debug, Clone)] +struct QueryPeer { + /// The `KBucket` key used to identify the peer. + key: Key, + + /// The number of peers that have been returned by this peer. + peers_returned: usize, + + /// Whether the peer has matched the predicate or not. + predicate_match: bool, + + /// The current query state of this peer. + state: QueryPeerState, +} + +impl QueryPeer { + pub fn new(key: Key, state: QueryPeerState, predicate_match: bool) -> Self { + QueryPeer { + key, + peers_returned: 0, + predicate_match, + state, + } + } +} + +/// The state of `QueryPeer` in the context of a query. +#[derive(Debug, Copy, Clone)] +enum QueryPeerState { + /// The peer has not yet been contacted. + /// + /// This is the starting state for every peer known to, or discovered by, a query. + NotContacted, + + /// The query is waiting for a result from the peer. + Waiting(Instant), + + /// A result was not delivered for the peer within the configured timeout. + /// + /// The peer is not taken into account for the termination conditions + /// of the iterator until and unless it responds. + Unresponsive, + + /// Obtaining a result from the peer has failed. + /// + /// This is a final state, reached as a result of a call to `on_failure`. + Failed, + + /// A successful result from the peer has been delivered. + /// + /// This is a final state, reached as a result of a call to `on_success`. + Succeeded, +} diff --git a/version-meld/discv5/src/rpc.rs b/version-meld/discv5/src/rpc.rs new file mode 100644 index 0000000..120578a --- /dev/null +++ b/version-meld/discv5/src/rpc.rs @@ -0,0 +1,1018 @@ +use enr::{CombinedKey, Enr}; +use rlp::{DecoderError, RlpStream}; +use std::net::{IpAddr, Ipv6Addr}; +use tracing::{debug, warn}; + +type TopicHash = [u8; 32]; + +/// Type to manage the request IDs. +#[derive(Debug, Clone, PartialEq, Hash, Eq)] +pub struct RequestId(pub Vec); + +impl From for Vec { + fn from(id: RequestId) -> Self { + id.0 + } +} + +impl RequestId { + /// Decodes the ID from a raw bytes. + pub fn decode(data: Vec) -> Result { + if data.len() > 8 { + return Err(DecoderError::Custom("Invalid ID length")); + } + Ok(RequestId(data)) + } + + pub fn random() -> Self { + let rand: u64 = rand::random(); + RequestId(rand.to_be_bytes().to_vec()) + } + + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } +} + +#[derive(Debug, Clone, PartialEq)] +/// A combined type representing requests and responses. +pub enum Message { + /// A request, which contains its [`RequestId`]. + Request(Request), + /// A Response, which contains the [`RequestId`] of its associated request. + Response(Response), +} + +#[derive(Debug, Clone, PartialEq)] +/// A request sent between nodes. +pub struct Request { + /// The [`RequestId`] of the request. + pub id: RequestId, + /// The body of the request. + pub body: RequestBody, +} + +#[derive(Debug, Clone, PartialEq)] +/// A response sent in response to a [`Request`] +pub struct Response { + /// The [`RequestId`] of the request that triggered this response. + pub id: RequestId, + /// The body of this response. + pub body: ResponseBody, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum RequestBody { + /// A PING request. + Ping { + /// Our current ENR sequence number. + enr_seq: u64, + }, + /// A FINDNODE request. + FindNode { + /// The distance(s) of peers we expect to be returned in the response. + distances: Vec, + }, + /// A Talk request. + Talk { + /// The protocol requesting. + protocol: Vec, + /// The request. + request: Vec, + }, + /// A REGISTERTOPIC request. + RegisterTopic { + topic: Vec, + enr: crate::Enr, + ticket: Vec, + }, + /// A TOPICQUERY request. + TopicQuery { topic: TopicHash }, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ResponseBody { + /// A PONG response. + Pong { + /// The current ENR sequence number of the responder. + enr_seq: u64, + /// Our external IP address as observed by the responder. + ip: IpAddr, + /// Our external UDP port as observed by the responder. + port: u16, + }, + /// A NODES response. + Nodes { + /// The total number of responses that make up this response. + total: u64, + /// A list of ENR's returned by the responder. + nodes: Vec>, + }, + /// The TALK response. + Talk { + /// The response for the talk. + response: Vec, + }, + Ticket { + ticket: Vec, + wait_time: u64, + }, + RegisterConfirmation { + topic: Vec, + }, +} + +impl Request { + pub fn msg_type(&self) -> u8 { + match self.body { + RequestBody::Ping { .. } => 1, + RequestBody::FindNode { .. } => 3, + RequestBody::Talk { .. } => 5, + RequestBody::RegisterTopic { .. } => 7, + RequestBody::TopicQuery { .. } => 10, + } + } + + /// Encodes a Message to RLP-encoded bytes. + pub fn encode(self) -> Vec { + let mut buf = Vec::with_capacity(10); + let msg_type = self.msg_type(); + buf.push(msg_type); + let id = &self.id; + match self.body { + RequestBody::Ping { enr_seq } => { + let mut s = RlpStream::new(); + s.begin_list(2); + s.append(&id.as_bytes()); + s.append(&enr_seq); + buf.extend_from_slice(&s.out()); + buf + } + RequestBody::FindNode { distances } => { + let mut s = RlpStream::new(); + s.begin_list(2); + s.append(&id.as_bytes()); + s.begin_list(distances.len()); + for distance in distances { + s.append(&distance); + } + buf.extend_from_slice(&s.out()); + buf + } + RequestBody::Talk { protocol, request } => { + let mut s = RlpStream::new(); + s.begin_list(3); + s.append(&id.as_bytes()); + s.append(&protocol); + s.append(&request); + buf.extend_from_slice(&s.out()); + buf + } + RequestBody::RegisterTopic { topic, enr, ticket } => { + let mut s = RlpStream::new(); + s.begin_list(4); + s.append(&id.as_bytes()); + s.append(&topic); + s.append(&enr); + s.append(&ticket); + buf.extend_from_slice(&s.out()); + buf + } + RequestBody::TopicQuery { topic } => { + let mut s = RlpStream::new(); + s.begin_list(2); + s.append(&id.as_bytes()); + s.append(&(&topic as &[u8])); + buf.extend_from_slice(&s.out()); + buf + } + } + } +} + +impl Response { + pub fn msg_type(&self) -> u8 { + match &self.body { + ResponseBody::Pong { .. } => 2, + ResponseBody::Nodes { .. } => 4, + ResponseBody::Talk { .. } => 6, + ResponseBody::Ticket { .. } => 8, + ResponseBody::RegisterConfirmation { .. } => 9, + } + } + + /// Determines if the response is a valid response to the given request. + pub fn match_request(&self, req: &RequestBody) -> bool { + match self.body { + ResponseBody::Pong { .. } => matches!(req, RequestBody::Ping { .. }), + ResponseBody::Nodes { .. } => { + matches!( + req, + RequestBody::FindNode { .. } | RequestBody::TopicQuery { .. } + ) + } + ResponseBody::Talk { .. } => matches!(req, RequestBody::Talk { .. }), + ResponseBody::Ticket { .. } => matches!(req, RequestBody::RegisterTopic { .. }), + ResponseBody::RegisterConfirmation { .. } => { + matches!(req, RequestBody::RegisterTopic { .. }) + } + } + } + + /// Encodes a Message to RLP-encoded bytes. + pub fn encode(self) -> Vec { + let mut buf = Vec::with_capacity(10); + let msg_type = self.msg_type(); + buf.push(msg_type); + let id = &self.id; + match self.body { + ResponseBody::Pong { enr_seq, ip, port } => { + let mut s = RlpStream::new(); + s.begin_list(4); + s.append(&id.as_bytes()); + s.append(&enr_seq); + match ip { + IpAddr::V4(addr) => s.append(&(&addr.octets() as &[u8])), + IpAddr::V6(addr) => s.append(&(&addr.octets() as &[u8])), + }; + s.append(&port); + buf.extend_from_slice(&s.out()); + buf + } + ResponseBody::Nodes { total, nodes } => { + let mut s = RlpStream::new(); + s.begin_list(3); + s.append(&id.as_bytes()); + s.append(&total); + + if nodes.is_empty() { + s.begin_list(0); + } else { + s.begin_list(nodes.len()); + for node in nodes { + s.append(&node); + } + } + buf.extend_from_slice(&s.out()); + buf + } + ResponseBody::Talk { response } => { + let mut s = RlpStream::new(); + s.begin_list(2); + s.append(&id.as_bytes()); + s.append(&response); + buf.extend_from_slice(&s.out()); + buf + } + ResponseBody::Ticket { ticket, wait_time } => { + let mut s = RlpStream::new(); + s.begin_list(3); + s.append(&id.as_bytes()); + s.append(&ticket); + s.append(&wait_time); + buf.extend_from_slice(&s.out()); + buf + } + ResponseBody::RegisterConfirmation { topic } => { + let mut s = RlpStream::new(); + s.begin_list(2); + s.append(&id.as_bytes()); + s.append(&topic); + buf.extend_from_slice(&s.out()); + buf + } + } + } +} + +impl std::fmt::Display for RequestId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex::encode(&self.0)) + } +} + +impl std::fmt::Display for Message { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Message::Request(request) => write!(f, "{}", request), + Message::Response(response) => write!(f, "{}", response), + } + } +} + +impl std::fmt::Display for Response { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Response: id: {}: {}", self.id, self.body) + } +} + +impl std::fmt::Display for ResponseBody { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ResponseBody::Pong { enr_seq, ip, port } => write!( + f, + "PONG: Enr-seq: {}, Ip: {:?}, Port: {}", + enr_seq, ip, port + ), + ResponseBody::Nodes { total, nodes } => { + let _ = write!(f, "NODES: total: {}, Nodes: [", total); + let mut first = true; + for id in nodes { + if !first { + write!(f, ", {}", id)?; + } else { + write!(f, "{}", id)?; + } + first = false; + } + + write!(f, "]") + } + ResponseBody::Talk { response } => { + write!(f, "Response: Response {}", hex::encode(response)) + } + ResponseBody::Ticket { ticket, wait_time } => { + write!(f, "TICKET: Ticket: {:?}, Wait time: {}", ticket, wait_time) + } + ResponseBody::RegisterConfirmation { topic } => { + write!(f, "REGTOPIC: Registered: {}", hex::encode(topic)) + } + } + } +} + +impl std::fmt::Display for Request { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Request: id: {}: {}", self.id, self.body) + } +} + +impl std::fmt::Display for RequestBody { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RequestBody::Ping { enr_seq } => write!(f, "PING: enr_seq: {}", enr_seq), + RequestBody::FindNode { distances } => { + write!(f, "FINDNODE Request: distance: {:?}", distances) + } + RequestBody::Talk { protocol, request } => write!( + f, + "TALK: protocol: {}, request: {}", + hex::encode(protocol), + hex::encode(request) + ), + RequestBody::TopicQuery { topic } => write!(f, "TOPICQUERY: topic: {:?}", topic), + RequestBody::RegisterTopic { topic, enr, ticket } => write!( + f, + "RegisterTopic: topic: {}, enr: {}, ticket: {}", + hex::encode(topic), + enr.to_base64(), + hex::encode(ticket) + ), + } + } +} +#[allow(dead_code)] +impl Message { + pub fn encode(self) -> Vec { + match self { + Self::Request(request) => request.encode(), + Self::Response(response) => response.encode(), + } + } + + pub fn decode(data: &[u8]) -> Result { + if data.len() < 3 { + return Err(DecoderError::RlpIsTooShort); + } + + let msg_type = data[0]; + + let rlp = rlp::Rlp::new(&data[1..]); + + let list_len = rlp.item_count().and_then(|size| { + if size < 2 { + Err(DecoderError::RlpIncorrectListLen) + } else { + Ok(size) + } + })?; + + let id = RequestId::decode(rlp.val_at::>(0)?)?; + + let message = match msg_type { + 1 => { + // PingRequest + if list_len != 2 { + debug!( + "Ping Request has an invalid RLP list length. Expected 2, found {}", + list_len + ); + return Err(DecoderError::RlpIncorrectListLen); + } + Message::Request(Request { + id, + body: RequestBody::Ping { + enr_seq: rlp.val_at::(1)?, + }, + }) + } + 2 => { + // PingResponse + if list_len != 4 { + debug!( + "Ping Response has an invalid RLP list length. Expected 4, found {}", + list_len + ); + return Err(DecoderError::RlpIncorrectListLen); + } + let ip_bytes = rlp.val_at::>(2)?; + let ip = match ip_bytes.len() { + 4 => { + let mut ip = [0u8; 4]; + ip.copy_from_slice(&ip_bytes); + IpAddr::from(ip) + } + 16 => { + let mut ip = [0u8; 16]; + ip.copy_from_slice(&ip_bytes); + let ipv6 = Ipv6Addr::from(ip); + // If the ipv6 is ipv4 compatible/mapped, simply return the ipv4. + if let Some(ipv4) = ipv6.to_ipv4() { + IpAddr::V4(ipv4) + } else { + IpAddr::V6(ipv6) + } + } + _ => { + debug!("Ping Response has incorrect byte length for IP"); + return Err(DecoderError::RlpIncorrectListLen); + } + }; + let port = rlp.val_at::(3)?; + Message::Response(Response { + id, + body: ResponseBody::Pong { + enr_seq: rlp.val_at::(1)?, + ip, + port, + }, + }) + } + 3 => { + // FindNodeRequest + if list_len != 2 { + debug!( + "FindNode Request has an invalid RLP list length. Expected 2, found {}", + list_len + ); + return Err(DecoderError::RlpIncorrectListLen); + } + let distances = rlp.list_at::(1)?; + + if distances.len() > 10 { + warn!( + "Rejected FindNode request asking for too many buckets {}, maximum 10", + distances.len() + ); + return Err(DecoderError::Custom("FINDNODE request too large")); + } + for distance in distances.iter() { + if distance > &256u64 { + warn!( + "Rejected FindNode request asking for unknown distance {}, maximum 256", + distance + ); + return Err(DecoderError::Custom("FINDNODE request distance invalid")); + } + } + + Message::Request(Request { + id, + body: RequestBody::FindNode { distances }, + }) + } + 4 => { + // NodesResponse + if list_len != 3 { + debug!( + "Nodes Response has an invalid RLP list length. Expected 3, found {}", + list_len + ); + return Err(DecoderError::RlpIncorrectListLen); + } + + let nodes = { + let enr_list_rlp = rlp.at(2)?; + if enr_list_rlp.is_empty() { + // no records + vec![] + } else { + enr_list_rlp.as_list::>()? + } + }; + Message::Response(Response { + id, + body: ResponseBody::Nodes { + total: rlp.val_at::(1)?, + nodes, + }, + }) + } + 5 => { + // Talk Request + if list_len != 3 { + debug!( + "Talk Request has an invalid RLP list length. Expected 3, found {}", + list_len + ); + return Err(DecoderError::RlpIncorrectListLen); + } + let protocol = rlp.val_at::>(1)?; + let request = rlp.val_at::>(2)?; + Message::Request(Request { + id, + body: RequestBody::Talk { protocol, request }, + }) + } + 6 => { + // Talk Response + if list_len != 2 { + debug!( + "Talk Response has an invalid RLP list length. Expected 2, found {}", + list_len + ); + return Err(DecoderError::RlpIncorrectListLen); + } + let response = rlp.val_at::>(1)?; + Message::Response(Response { + id, + body: ResponseBody::Talk { response }, + }) + } + _ => { + return Err(DecoderError::Custom("Unknown RPC message type")); + } /* + * All other RPC messages are currently not supported as per the 5.1 specification. + + 7 => { + // RegisterTopicRequest + if list_len != 2 { + debug!("RegisterTopic Request has an invalid RLP list length. Expected 2, found {}", list_len); + return Err(DecoderError::RlpIncorrectListLen); + } + let ticket = rlp.val_at::>(1)?; + Message::Request(Request { + id, + body: RequestBody::RegisterTopic { ticket }, + }) + } + 8 => { + // RegisterTopicResponse + if list_len != 2 { + debug!("RegisterTopic Response has an invalid RLP list length. Expected 2, found {}", list_len); + return Err(DecoderError::RlpIncorrectListLen); + } + Message::Response(Response { + id, + body: ResponseBody::RegisterTopic { + registered: rlp.val_at::(1)?, + }, + }) + } + 9 => { + // TopicQueryRequest + if list_len != 2 { + debug!( + "TopicQuery Request has an invalid RLP list length. Expected 2, found {}", + list_len + ); + return Err(DecoderError::RlpIncorrectListLen); + } + let topic = { + let topic_bytes = rlp.val_at::>(1)?; + if topic_bytes.len() > 32 { + debug!("Ticket Request has a topic greater than 32 bytes"); + return Err(DecoderError::RlpIsTooBig); + } + let mut topic = [0u8; 32]; + topic[32 - topic_bytes.len()..].copy_from_slice(&topic_bytes); + topic + }; + Message::Request(Request { + id, + body: RequestBody::TopicQuery { topic }, + }) + } + */ + }; + + Ok(message) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use enr::EnrBuilder; + + #[test] + fn ref_test_encode_request_ping() { + // reference input + let id = RequestId(vec![1]); + let enr_seq = 1; + let message = Message::Request(Request { + id, + body: RequestBody::Ping { enr_seq }, + }); + + // expected hex output + let expected_output = hex::decode("01c20101").unwrap(); + + dbg!(hex::encode(message.clone().encode())); + assert_eq!(message.encode(), expected_output); + } + + #[test] + fn ref_test_encode_request_findnode() { + // reference input + let id = RequestId(vec![1]); + let distances = vec![256]; + let message = Message::Request(Request { + id, + body: RequestBody::FindNode { distances }, + }); + + // expected hex output + let expected_output = hex::decode("03c501c3820100").unwrap(); + dbg!(hex::encode(message.clone().encode())); + + assert_eq!(message.encode(), expected_output); + } + + #[test] + fn ref_test_encode_response_ping() { + // reference input + let id = RequestId(vec![1]); + let enr_seq = 1; + let ip: IpAddr = "127.0.0.1".parse().unwrap(); + let port = 5000; + let message = Message::Response(Response { + id, + body: ResponseBody::Pong { enr_seq, ip, port }, + }); + + // expected hex output + let expected_output = hex::decode("02ca0101847f000001821388").unwrap(); + + dbg!(hex::encode(message.clone().encode())); + assert_eq!(message.encode(), expected_output); + } + + #[test] + fn ref_test_encode_response_nodes_empty() { + // reference input + let id = RequestId(vec![1]); + let total = 1; + + // expected hex output + let expected_output = hex::decode("04c30101c0").unwrap(); + + let message = Message::Response(Response { + id, + body: ResponseBody::Nodes { + total, + nodes: vec![], + }, + }); + assert_eq!(message.encode(), expected_output); + } + + #[test] + fn ref_test_encode_response_nodes() { + // reference input + let id = RequestId(vec![1]); + let total = 1; + + let enr = "-HW4QCjfjuCfSmIJHxqLYfGKrSz-Pq3G81DVJwd_muvFYJiIOkf0bGtJu7kZVCOPnhSTMneyvR4MRbF3G5TNB4wy2ssBgmlkgnY0iXNlY3AyNTZrMaEDymNMrg1JrLQB2KTGtv6MVbcNEVv0AHacwUAPMljNMTg".parse::>().unwrap(); + // expected hex output + let expected_output = hex::decode("04f87b0101f877f875b84028df8ee09f4a62091f1a8b61f18aad2cfe3eadc6f350d527077f9aebc56098883a47f46c6b49bbb91954238f9e14933277b2bd1e0c45b1771b94cd078c32dacb0182696482763489736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138").unwrap(); + + let message = Message::Response(Response { + id, + body: ResponseBody::Nodes { + total, + nodes: vec![enr], + }, + }); + dbg!(hex::encode(message.clone().encode())); + assert_eq!(message.encode(), expected_output); + } + + #[test] + fn ref_test_encode_response_nodes_multiple() { + // reference input + let id = RequestId(vec![1]); + let total = 1; + let enr = "enr:-HW4QBzimRxkmT18hMKaAL3IcZF1UcfTMPyi3Q1pxwZZbcZVRI8DC5infUAB_UauARLOJtYTxaagKoGmIjzQxO2qUygBgmlkgnY0iXNlY3AyNTZrMaEDymNMrg1JrLQB2KTGtv6MVbcNEVv0AHacwUAPMljNMTg".parse::>().unwrap(); + + let enr2 = "enr:-HW4QNfxw543Ypf4HXKXdYxkyzfcxcO-6p9X986WldfVpnVTQX1xlTnWrktEWUbeTZnmgOuAY_KUhbVV1Ft98WoYUBMBgmlkgnY0iXNlY3AyNTZrMaEDDiy3QkHAxPyOgWbxp5oF1bDdlYE6dLCUUp8xfVw50jU".parse::>().unwrap(); + + // expected hex output + let expected_output = hex::decode("04f8f20101f8eef875b8401ce2991c64993d7c84c29a00bdc871917551c7d330fca2dd0d69c706596dc655448f030b98a77d4001fd46ae0112ce26d613c5a6a02a81a6223cd0c4edaa53280182696482763489736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138f875b840d7f1c39e376297f81d7297758c64cb37dcc5c3beea9f57f7ce9695d7d5a67553417d719539d6ae4b445946de4d99e680eb8063f29485b555d45b7df16a1850130182696482763489736563703235366b31a1030e2cb74241c0c4fc8e8166f1a79a05d5b0dd95813a74b094529f317d5c39d235").unwrap(); + + let message = Message::Response(Response { + id, + body: ResponseBody::Nodes { + total, + nodes: vec![enr, enr2], + }, + }); + dbg!(hex::encode(message.clone().encode())); + assert_eq!(message.encode(), expected_output); + } + + #[test] + fn ref_decode_response_nodes_multiple() { + let input = hex::decode("04f8f20101f8eef875b8401ce2991c64993d7c84c29a00bdc871917551c7d330fca2dd0d69c706596dc655448f030b98a77d4001fd46ae0112ce26d613c5a6a02a81a6223cd0c4edaa53280182696482763489736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138f875b840d7f1c39e376297f81d7297758c64cb37dcc5c3beea9f57f7ce9695d7d5a67553417d719539d6ae4b445946de4d99e680eb8063f29485b555d45b7df16a1850130182696482763489736563703235366b31a1030e2cb74241c0c4fc8e8166f1a79a05d5b0dd95813a74b094529f317d5c39d235").unwrap(); + + let expected_enr1 = "enr:-HW4QBzimRxkmT18hMKaAL3IcZF1UcfTMPyi3Q1pxwZZbcZVRI8DC5infUAB_UauARLOJtYTxaagKoGmIjzQxO2qUygBgmlkgnY0iXNlY3AyNTZrMaEDymNMrg1JrLQB2KTGtv6MVbcNEVv0AHacwUAPMljNMTg".parse::>().unwrap(); + let expected_enr2 = "enr:-HW4QNfxw543Ypf4HXKXdYxkyzfcxcO-6p9X986WldfVpnVTQX1xlTnWrktEWUbeTZnmgOuAY_KUhbVV1Ft98WoYUBMBgmlkgnY0iXNlY3AyNTZrMaEDDiy3QkHAxPyOgWbxp5oF1bDdlYE6dLCUUp8xfVw50jU".parse::>().unwrap(); + + let decoded = Message::decode(&input).unwrap(); + + match decoded { + Message::Response(response) => match response.body { + ResponseBody::Nodes { total, nodes } => { + assert_eq!(total, 1); + assert_eq!(nodes[0], expected_enr1); + assert_eq!(nodes[1], expected_enr2); + } + _ => panic!("Invalid decoding"), + }, + _ => panic!("Invalid decoding"), + } + } + + #[test] + fn encode_decode_ping_request() { + let id = RequestId(vec![1]); + let request = Message::Request(Request { + id, + body: RequestBody::Ping { enr_seq: 15 }, + }); + + let encoded = request.clone().encode(); + let decoded = Message::decode(&encoded).unwrap(); + + assert_eq!(request, decoded); + } + + #[test] + fn encode_decode_ping_response() { + let id = RequestId(vec![1]); + let request = Message::Response(Response { + id, + body: ResponseBody::Pong { + enr_seq: 15, + ip: "127.0.0.1".parse().unwrap(), + port: 80, + }, + }); + + let encoded = request.clone().encode(); + let decoded = Message::decode(&encoded).unwrap(); + + assert_eq!(request, decoded); + } + + #[test] + fn encode_decode_find_node_request() { + let id = RequestId(vec![1]); + let request = Message::Request(Request { + id, + body: RequestBody::FindNode { + distances: vec![12], + }, + }); + + let encoded = request.clone().encode(); + let decoded = Message::decode(&encoded).unwrap(); + + assert_eq!(request, decoded); + } + + #[test] + fn encode_decode_nodes_response() { + let key = CombinedKey::generate_secp256k1(); + let enr1 = EnrBuilder::new("v4") + .ip("127.0.0.1".parse().unwrap()) + .udp(500) + .build(&key) + .unwrap(); + let enr2 = EnrBuilder::new("v4") + .ip("10.0.0.1".parse().unwrap()) + .tcp4(8080) + .build(&key) + .unwrap(); + let enr3 = EnrBuilder::new("v4") + .ip("10.4.5.6".parse().unwrap()) + .build(&key) + .unwrap(); + + let enr_list = vec![enr1, enr2, enr3]; + let id = RequestId(vec![1]); + let request = Message::Response(Response { + id, + body: ResponseBody::Nodes { + total: 1, + nodes: enr_list, + }, + }); + + let encoded = request.clone().encode(); + let decoded = Message::decode(&encoded).unwrap(); + + assert_eq!(request, decoded); + } + + #[test] + fn encode_decode_ticket_request() { + let id = RequestId(vec![1]); + let request = Message::Request(Request { + id, + body: RequestBody::Talk { + protocol: vec![17u8; 32], + request: vec![1, 2, 3], + }, + }); + + let encoded = request.clone().encode(); + let decoded = Message::decode(&encoded).unwrap(); + + assert_eq!(request, decoded); + } + + /* + * These RPC messages are not in use yet + * + #[test] + fn ref_test_encode_request_ticket() { + // reference input + let id = 1; + let hash_bytes = + hex::decode("fb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736") + .unwrap(); + + // expected hex output + let expected_output = + hex::decode("05e201a0fb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736") + .unwrap(); + + let mut topic_hash = [0; 32]; + topic_hash.copy_from_slice(&hash_bytes); + + let message = Message::Request(Request { + id, + body: RequestBody::Ticket { topic: topic_hash }, + }); + assert_eq!(message.encode(), expected_output); + } + + #[test] + fn ref_test_encode_request_register_topic() { + // reference input + let id = 1; + let ticket = + hex::decode("fb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736") + .unwrap(); + + // expected hex output + let expected_output = + hex::decode("07e201a0fb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736") + .unwrap(); + + let message = Message::Request(Request { + id, + body: RequestBody::RegisterTopic { ticket }, + }); + assert_eq!(message.encode(), expected_output); + } + + #[test] + fn ref_test_encode_request_topic_query() { + // reference input + let id = 1; + let hash_bytes = + hex::decode("fb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736") + .unwrap(); + + // expected hex output + let expected_output = + hex::decode("09e201a0fb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736") + .unwrap(); + + let mut topic_hash = [0; 32]; + topic_hash.copy_from_slice(&hash_bytes); + + let message = Message::Request(Request { + id, + body: RequestBody::TopicQuery { topic: topic_hash }, + }); + assert_eq!(message.encode(), expected_output); + } + + #[test] + fn ref_test_encode_response_register_topic() { + // reference input + let id = 1; + let registered = true; + + // expected hex output + let expected_output = hex::decode("08c20101").unwrap(); + let message = Message::Response(Response { + id, + body: ResponseBody::RegisterTopic { registered }, + }); + assert_eq!(message.encode(), expected_output); + } + + #[test] + fn encode_decode_register_topic_request() { + let request = Message::Request(Request { + id: 1, + body: RequestBody::RegisterTopic { + topic: vec![1,2,3], + ticket: vec![1, 2, 3, 4, 5], + }, + }); + + let encoded = request.clone().encode(); + let decoded = Message::decode(encoded).unwrap(); + + assert_eq!(request, decoded); + } + + #[test] + fn encode_decode_register_topic_response() { + let request = Message::Response(Response { + id: 0, + body: ResponseBody::RegisterTopic { registered: true }, + }); + + let encoded = request.clone().encode(); + let decoded = Message::decode(encoded).unwrap(); + + assert_eq!(request, decoded); + } + + #[test] + fn encode_decode_topic_query_request() { + let request = Message::Request(Request { + id: 1, + body: RequestBody::TopicQuery { topic: [17u8; 32] }, + }); + + let encoded = request.clone().encode(); + let decoded = Message::decode(encoded).unwrap(); + + assert_eq!(request, decoded); + } + + #[test] + fn ref_test_encode_response_ticket() { + // reference input + let id = 1; + let ticket = [0; 32].to_vec(); // all 0's + let wait_time = 5; + + // expected hex output + let expected_output = hex::decode( + "06e301a0000000000000000000000000000000000000000000000000000000000000000005", + ) + .unwrap(); + + let message = Message::Response(Response { + id, + body: ResponseBody::Ticket { ticket, wait_time }, + }); + assert_eq!(message.encode(), expected_output); + } + + #[test] + fn encode_decode_ticket_response() { + let request = Message::Response(Response { + id: 0, + body: ResponseBody::Ticket { + ticket: vec![1, 2, 3, 4, 5], + wait_time: 5, + }, + }); + + let encoded = request.clone().encode(); + let decoded = Message::decode(encoded).unwrap(); + + assert_eq!(request, decoded); + } + + */ +} diff --git a/version-meld/discv5/src/service.rs b/version-meld/discv5/src/service.rs new file mode 100644 index 0000000..7d5d6e0 --- /dev/null +++ b/version-meld/discv5/src/service.rs @@ -0,0 +1,1434 @@ +//! The Discovery v5 protocol. See `lib.rs` for further details. +//! +//! Note: Discovered ENR's are not automatically added to the routing table. Only established +//! sessions get added, ensuring only valid ENRs are added. Manual additions can be made using the +//! `add_enr()` function. +//! +//! Response to queries return `PeerId`. Only the trusted (a session has been established with) +//! `PeerId`'s are returned, as ENR's for these `PeerId`'s are stored in the routing table and as +//! such should have an address to connect to. Untrusted `PeerId`'s can be obtained from the +//! `Service::Discovered` event, which is fired as peers get discovered. +//! +//! Note that although the ENR crate does support Ed25519 keys, these are currently not +//! supported as the ECDH procedure isn't specified in the specification. Therefore, only +//! secp256k1 keys are supported currently. + +use self::{ + ip_vote::IpVote, + query_info::{QueryInfo, QueryType}, +}; +use crate::{ + error::{RequestError, ResponseError}, + handler::{Handler, HandlerRequest, HandlerResponse}, + kbucket::{ + self, ConnectionDirection, ConnectionState, FailureReason, InsertResult, KBucketsTable, + NodeStatus, UpdateResult, + }, + node_info::{NodeAddress, NodeContact}, + packet::MAX_PACKET_SIZE, + query_pool::{ + FindNodeQueryConfig, PredicateQueryConfig, QueryId, QueryPool, QueryPoolState, TargetKey, + }, + rpc, Discv5Config, Discv5Event, Enr, +}; +use enr::{CombinedKey, NodeId}; +use fnv::FnvHashMap; +use futures::prelude::*; +use hashset_delay::HashSetDelay; +use parking_lot::RwLock; +use rpc::*; +use std::{collections::HashMap, net::SocketAddr, sync::Arc, task::Poll, time::Instant}; +use tokio::sync::{mpsc, oneshot}; +use tracing::{debug, error, info, trace, warn}; + +mod hashset_delay; +mod ip_vote; +mod query_info; +mod test; + +/// Request type for Protocols using `TalkReq` message. +/// +/// Automatically responds with an empty body on drop if +/// [`TalkRequest::respond`] is not called. +#[derive(Debug)] +pub struct TalkRequest { + id: RequestId, + node_address: NodeAddress, + protocol: Vec, + body: Vec, + sender: Option>, +} + +impl Drop for TalkRequest { + fn drop(&mut self) { + let sender = match self.sender.take() { + Some(s) => s, + None => return, + }; + + let response = Response { + id: self.id.clone(), + body: ResponseBody::Talk { response: vec![] }, + }; + + debug!("Sending empty TALK response to {}", self.node_address); + let _ = sender.send(HandlerRequest::Response( + self.node_address.clone(), + Box::new(response), + )); + } +} + +impl TalkRequest { + pub fn id(&self) -> &RequestId { + &self.id + } + + pub fn node_id(&self) -> &NodeId { + &self.node_address.node_id + } + + pub fn protocol(&self) -> &[u8] { + &self.protocol + } + + pub fn body(&self) -> &[u8] { + &self.body + } + + pub fn respond(mut self, response: Vec) -> Result<(), ResponseError> { + debug!("Sending TALK response to {}", self.node_address); + + let response = Response { + id: self.id.clone(), + body: ResponseBody::Talk { response }, + }; + + self.sender + .take() + .unwrap() + .send(HandlerRequest::Response( + self.node_address.clone(), + Box::new(response), + )) + .map_err(|_| ResponseError::ChannelClosed)?; + + Ok(()) + } +} + +/// The number of distances (buckets) we simultaneously request from each peer. +pub(crate) const DISTANCES_TO_REQUEST_PER_PEER: usize = 3; + +/// The types of requests to send to the Discv5 service. +pub enum ServiceRequest { + /// A request to start a query. There are two types of queries: + /// - A FindNode Query - Searches for peers using a random target. + /// - A Predicate Query - Searches for peers closest to a random target that match a specified + /// predicate. + StartQuery(QueryKind, oneshot::Sender>), + /// Find the ENR of a node given its multiaddr. + FindEnr(NodeContact, oneshot::Sender>), + /// The TALK discv5 RPC function. + Talk( + NodeContact, + Vec, + Vec, + oneshot::Sender, RequestError>>, + ), + /// Sets up an event stream where the discv5 server will return various events such as + /// discovered nodes as it traverses the DHT. + RequestEventStream(oneshot::Sender>), +} + +use crate::discv5::PERMIT_BAN_LIST; + +pub struct Service { + /// Configuration parameters. + config: Discv5Config, + + /// The local ENR of the server. + local_enr: Arc>, + + /// The key associated with the local ENR. + enr_key: Arc>, + + /// Storage of the ENR record for each node. + kbuckets: Arc>>, + + /// All the iterative queries we are currently performing. + queries: QueryPool, + + /// RPC requests that have been sent and are awaiting a response. Some requests are linked to a + /// query. + active_requests: FnvHashMap, + + /// Keeps track of the number of responses received from a NODES response. + active_nodes_responses: HashMap, + + /// A map of votes nodes have made about our external IP address. We accept the majority. + ip_votes: Option, + + /// The channel to send messages to the handler. + handler_send: mpsc::UnboundedSender, + + /// The channel to receive messages from the handler. + handler_recv: mpsc::Receiver, + + /// The exit channel to shutdown the handler. + handler_exit: Option>, + + /// The channel of messages sent by the controlling discv5 wrapper. + discv5_recv: mpsc::Receiver, + + /// The exit channel for the service. + exit: oneshot::Receiver<()>, + + /// A queue of peers that require regular ping to check connectivity. + peers_to_ping: HashSetDelay, + + /// A channel that the service emits events on. + event_stream: Option>, +} + +/// Active RPC request awaiting a response from the handler. +struct ActiveRequest { + /// The address the request was sent to. + pub contact: NodeContact, + /// The request that was sent. + pub request_body: RequestBody, + /// The query ID if the request was related to a query. + pub query_id: Option, + /// Channel callback if this request was from a user level request. + pub callback: Option, +} + +/// The kinds of responses we can send back to the discv5 layer. +pub enum CallbackResponse { + /// A response to a requested ENR. + Enr(oneshot::Sender>), + /// A response from a TALK request + Talk(oneshot::Sender, RequestError>>), +} + +/// For multiple responses to a FindNodes request, this keeps track of the request count +/// and the nodes that have been received. +struct NodesResponse { + /// The response count. + count: usize, + /// The filtered nodes that have been received. + received_nodes: Vec, +} + +impl Default for NodesResponse { + fn default() -> Self { + NodesResponse { + count: 1, + received_nodes: Vec::new(), + } + } +} + +impl Service { + /// Builds the `Service` main struct. + /// + /// `local_enr` is the `ENR` representing the local node. This contains node identifying information, such + /// as IP addresses and ports which we wish to broadcast to other nodes via this discovery + /// mechanism. + pub async fn spawn( + local_enr: Arc>, + enr_key: Arc>, + kbuckets: Arc>>, + config: Discv5Config, + listen_socket: SocketAddr, + ) -> Result<(oneshot::Sender<()>, mpsc::Sender), std::io::Error> { + // process behaviour-level configuration parameters + let ip_votes = if config.enr_update { + Some(IpVote::new( + config.enr_peer_update_min, + config.vote_duration, + )) + } else { + None + }; + + // build the session service + let (handler_exit, handler_send, handler_recv) = Handler::spawn( + local_enr.clone(), + enr_key.clone(), + listen_socket, + config.clone(), + ) + .await?; + + // create the required channels + let (discv5_send, discv5_recv) = mpsc::channel(30); + let (exit_send, exit) = oneshot::channel(); + + config + .executor + .clone() + .expect("Executor must be present") + .spawn(Box::pin(async move { + let mut service = Service { + local_enr, + enr_key, + kbuckets, + queries: QueryPool::new(config.query_timeout), + active_requests: Default::default(), + active_nodes_responses: HashMap::new(), + ip_votes, + handler_send, + handler_recv, + handler_exit: Some(handler_exit), + peers_to_ping: HashSetDelay::new(config.ping_interval), + discv5_recv, + event_stream: None, + exit, + config: config.clone(), + }; + + info!("Discv5 Service started"); + service.start().await; + })); + + Ok((exit_send, discv5_send)) + } + + /// The main execution loop of the discv5 serviced. + async fn start(&mut self) { + loop { + tokio::select! { + _ = &mut self.exit => { + if let Some(exit) = self.handler_exit.take() { + let _ = exit.send(()); + info!("Discv5 Service shutdown"); + } + return; + } + Some(service_request) = self.discv5_recv.recv() => { + match service_request { + ServiceRequest::StartQuery(query, callback) => { + match query { + QueryKind::FindNode { target_node } => { + self.start_findnode_query(target_node, callback); + } + QueryKind::Predicate { target_node, target_peer_no, predicate } => { + self.start_predicate_query(target_node, target_peer_no, predicate, callback); + } + } + } + ServiceRequest::FindEnr(node_contact, callback) => { + self.request_enr(node_contact, Some(callback)); + } + ServiceRequest::Talk(node_contact, protocol, request, callback) => { + self.talk_request(node_contact, protocol, request, callback); + } + ServiceRequest::RequestEventStream(callback) => { + // the channel size needs to be large to handle many discovered peers + // if we are reporting them on the event stream. + let channel_size = if self.config.report_discovered_peers { 100 } else { 30 }; + let (event_stream, event_stream_recv) = mpsc::channel(channel_size); + self.event_stream = Some(event_stream); + if callback.send(event_stream_recv).is_err() { + error!("Failed to return the event stream channel"); + } + } + } + } + Some(event) = self.handler_recv.recv() => { + match event { + HandlerResponse::Established(enr, direction) => { + self.inject_session_established(enr,direction); + } + HandlerResponse::Request(node_address, request) => { + self.handle_rpc_request(node_address, *request); + } + HandlerResponse::Response(node_address, response) => { + self.handle_rpc_response(node_address, *response); + } + HandlerResponse::WhoAreYou(whoareyou_ref) => { + // check what our latest known ENR is for this node. + if let Some(known_enr) = self.find_enr(&whoareyou_ref.0.node_id) { + let _ = self.handler_send.send(HandlerRequest::WhoAreYou(whoareyou_ref, Some(known_enr))); + } else { + // do not know of this peer + debug!("NodeId unknown, requesting ENR. {}", whoareyou_ref.0); + let _ = self.handler_send.send(HandlerRequest::WhoAreYou(whoareyou_ref, None)); + } + } + HandlerResponse::RequestFailed(request_id, error) => { + if let RequestError::Timeout = error { + debug!("RPC Request timed out. id: {}", request_id); + } else { + warn!("RPC Request failed: id: {}, error {:?}", request_id, error); + } + self.rpc_failure(request_id, error); + } + } + } + event = Service::bucket_maintenance_poll(&self.kbuckets) => { + self.send_event(event); + } + query_event = Service::query_event_poll(&mut self.queries) => { + match query_event { + QueryEvent::Waiting(query_id, node_id, request_body) => { + self.send_rpc_query(query_id, node_id, request_body); + } + // Note: Currently the distinction between a timed-out query and a finished + // query is superfluous, however it may be useful in future versions. + QueryEvent::Finished(query) | QueryEvent::TimedOut(query) => { + let id = query.id(); + let mut result = query.into_result(); + // obtain the ENR's for the resulting nodes + let mut found_enrs = Vec::new(); + for node_id in result.closest_peers.into_iter() { + if let Some(position) = result.target.untrusted_enrs.iter().position(|enr| enr.node_id() == node_id) { + let enr = result.target.untrusted_enrs.swap_remove(position); + found_enrs.push(enr); + } else if let Some(enr) = self.find_enr(&node_id) { + // look up from the routing table + found_enrs.push(enr); + } + else { + warn!("ENR not present in queries results"); + } + } + if result.target.callback.send(found_enrs).is_err() { + warn!("Callback dropped for query {}. Results dropped", *id); + } + } + } + } + Some(Ok(node_id)) = self.peers_to_ping.next() => { + // If the node is in the routing table, Ping it and re-queue the node. + let key = kbucket::Key::from(node_id); + let enr = { + if let kbucket::Entry::Present(entry, _) = self.kbuckets.write().entry(&key) { + // The peer is in the routing table, ping it and re-queue the ping + self.peers_to_ping.insert(node_id); + Some(entry.value().clone()) + } else { None } + }; + + if let Some(enr) = enr { + self.send_ping(enr); + } + } + } + } + } + + /// Internal function that starts a query. + fn start_findnode_query(&mut self, target_node: NodeId, callback: oneshot::Sender>) { + let mut target = QueryInfo { + query_type: QueryType::FindNode(target_node), + untrusted_enrs: Default::default(), + distances_to_request: DISTANCES_TO_REQUEST_PER_PEER, + callback, + }; + + let target_key: kbucket::Key = target.key(); + let mut known_closest_peers = Vec::new(); + { + let mut kbuckets = self.kbuckets.write(); + for closest in kbuckets.closest_values(&target_key) { + // Add the known ENR's to the untrusted list + target.untrusted_enrs.push(closest.value); + // Add the key to the list for the query + known_closest_peers.push(closest.key); + } + } + + if known_closest_peers.is_empty() { + warn!("No known_closest_peers found. Return empty result without sending query."); + if target.callback.send(vec![]).is_err() { + warn!("Failed to callback"); + } + } else { + let query_config = FindNodeQueryConfig::new_from_config(&self.config); + self.queries + .add_findnode_query(query_config, target, known_closest_peers); + } + } + + /// Internal function that starts a query. + fn start_predicate_query( + &mut self, + target_node: NodeId, + num_nodes: usize, + predicate: Box bool + Send>, + callback: oneshot::Sender>, + ) { + let mut target = QueryInfo { + query_type: QueryType::FindNode(target_node), + untrusted_enrs: Default::default(), + distances_to_request: DISTANCES_TO_REQUEST_PER_PEER, + callback, + }; + + let target_key: kbucket::Key = target.key(); + + // Map the TableEntry to an ENR. + let kbucket_predicate = |e: &Enr| predicate(e); + + let mut known_closest_peers = Vec::>::new(); + { + let mut kbuckets = self.kbuckets.write(); + for closest in kbuckets.closest_values_predicate(&target_key, &kbucket_predicate) { + // Add the known ENR's to the untrusted list + target.untrusted_enrs.push(closest.value.clone()); + // Add the key to the list for the query + known_closest_peers.push(closest.into()); + } + }; + + if known_closest_peers.is_empty() { + warn!("No known_closest_peers found. Return empty result without sending query."); + if target.callback.send(vec![]).is_err() { + warn!("Failed to callback"); + } + } else { + let mut query_config = PredicateQueryConfig::new_from_config(&self.config); + query_config.num_results = num_nodes; + self.queries + .add_predicate_query(query_config, target, known_closest_peers, predicate); + } + } + + /// Returns an ENR if one is known for the given NodeId. + pub fn find_enr(&self, node_id: &NodeId) -> Option { + // check if we know this node id in our routing table + let key = kbucket::Key::from(*node_id); + if let kbucket::Entry::Present(entry, _) = self.kbuckets.write().entry(&key) { + return Some(entry.value().clone()); + } + // check the untrusted addresses for ongoing queries + for query in self.queries.iter() { + if let Some(enr) = query + .target() + .untrusted_enrs + .iter() + .find(|v| v.node_id() == *node_id) + { + return Some(enr.clone()); + } + } + None + } + + /// Processes an RPC request from a peer. Requests respond to the received socket address, + /// rather than the IP of the known ENR. + fn handle_rpc_request(&mut self, node_address: NodeAddress, req: Request) { + let id = req.id; + match req.body { + RequestBody::FindNode { distances } => { + self.send_nodes_response(node_address, id, distances); + } + RequestBody::Ping { enr_seq } => { + // check if we need to update the known ENR + let mut to_request_enr = None; + match self.kbuckets.write().entry(&node_address.node_id.into()) { + kbucket::Entry::Present(ref mut entry, _) => { + if entry.value().seq() < enr_seq { + let enr = entry.value().clone(); + to_request_enr = Some(enr.into()); + } + } + kbucket::Entry::Pending(ref mut entry, _) => { + if entry.value().seq() < enr_seq { + let enr = entry.value().clone(); + to_request_enr = Some(enr.into()); + } + } + // don't know of the ENR, request the update + _ => {} + } + if let Some(enr) = to_request_enr { + self.request_enr(enr, None); + } + + // build the PONG response + let src = node_address.socket_addr; + let response = Response { + id, + body: ResponseBody::Pong { + enr_seq: self.local_enr.read().seq(), + ip: src.ip(), + port: src.port(), + }, + }; + debug!("Sending PONG response to {}", node_address); + let _ = self + .handler_send + .send(HandlerRequest::Response(node_address, Box::new(response))); + } + RequestBody::Talk { protocol, request } => { + let req = TalkRequest { + id, + node_address, + protocol, + body: request, + sender: Some(self.handler_send.clone()), + }; + + self.send_event(Discv5Event::TalkRequest(req)); + } + RequestBody::RegisterTopic { .. } => { + debug!("Received RegisterTopic request which is unimplemented"); + } + RequestBody::TopicQuery { .. } => { + debug!("Received TopicQuery request which is unimplemented"); + } + } + } + + /// Processes an RPC response from a peer. + fn handle_rpc_response(&mut self, node_address: NodeAddress, response: Response) { + // verify we know of the rpc_id + let id = response.id.clone(); + + if let Some(mut active_request) = self.active_requests.remove(&id) { + debug!( + "Received RPC response: {} to request: {} from: {}", + response.body, active_request.request_body, active_request.contact + ); + + // Check that the responder matches the expected request + if let Ok(request_node_address) = active_request.contact.node_address() { + if request_node_address != node_address { + warn!("Received a response from an unexpected address. Expected {}, received {}, request_id {}", request_node_address, node_address, id); + return; + } + } + + let node_id = active_request.contact.node_id(); + if !response.match_request(&active_request.request_body) { + warn!( + "Node gave an incorrect response type. Ignoring response from: {}", + active_request.contact + ); + return; + } + match response.body { + ResponseBody::Nodes { total, mut nodes } => { + // Currently a maximum of DISTANCES_TO_REQUEST_PER_PEER*BUCKET_SIZE peers can be returned. Datagrams have a max + // size of 1280 and ENR's have a max size of 300 bytes. + // + // Bucket sizes should be 16. In this case, there should be no more than 5*DISTANCES_TO_REQUEST_PER_PEER responses, to return all required peers. + if total > 5 * DISTANCES_TO_REQUEST_PER_PEER as u64 { + warn!( + "NodesResponse has a total larger than {}, nodes will be truncated", + DISTANCES_TO_REQUEST_PER_PEER * 5 + ); + } + + // These are sanitized and ordered + let distances_requested = match &active_request.request_body { + RequestBody::FindNode { distances } => distances, + _ => unreachable!(), + }; + + // This could be an ENR request from the outer service. If so respond to the + // callback and End. + if let Some(CallbackResponse::Enr(callback)) = active_request.callback.take() { + // Currently only support requesting for ENR's. Verify this is the case. + if !distances_requested.is_empty() && distances_requested[0] != 0 { + error!("Retrieved a callback request that wasn't for a peer's ENR"); + return; + } + // This must be for asking for an ENR + if nodes.len() > 1 { + warn!( + "Peer returned more than one ENR for itself. {}", + active_request.contact + ); + } + let response = nodes.pop().ok_or_else(|| { + RequestError::InvalidEnr("Peer did not return an ENR".into()) + }); + let _ = callback.send(response); + return; + } + + // Filter out any nodes that are not of the correct distance + let peer_key: kbucket::Key = node_id.into(); + + // The distances we send are sanitized an ordered. + // We never send an ENR request in combination of other requests. + if distances_requested.len() == 1 && distances_requested[0] == 0 { + // we requested an ENR update + if nodes.len() > 1 { + warn!( + "Peer returned more than one ENR for itself. Blacklisting {}", + active_request.contact + ); + } + let ban_timeout = self.config.ban_duration.map(|v| Instant::now() + v); + PERMIT_BAN_LIST.write().ban( + active_request + .contact + .node_address() + .expect("Sanitized request"), + ban_timeout, + ); + nodes.retain(|enr| peer_key.log2_distance(&enr.node_id().into()).is_none()); + } else { + let before_len = nodes.len(); + nodes.retain(|enr| { + peer_key + .log2_distance(&enr.node_id().into()) + .map(|distance| distances_requested.contains(&distance)) + .unwrap_or_else(|| false) + }); + + if nodes.len() < before_len { + // Peer sent invalid ENRs. Blacklist the Node + warn!( + "Peer sent invalid ENR. Blacklisting {}", + active_request.contact + ); + let ban_timeout = self.config.ban_duration.map(|v| Instant::now() + v); + PERMIT_BAN_LIST.write().ban( + active_request + .contact + .node_address() + .expect("Sanitized request"), + ban_timeout, + ); + } + } + + // handle the case that there is more than one response + if total > 1 { + let mut current_response = self + .active_nodes_responses + .remove(&node_id) + .unwrap_or_default(); + + debug!( + "Nodes Response: {} of {} received", + current_response.count, total + ); + // if there are more requests coming, store the nodes and wait for + // another response + // We allow for implementations to send at a minimum 3 nodes per response. + // We allow for the number of nodes to be returned as the maximum we emit. + if current_response.count < self.config.max_nodes_response / 3 + 1 + && (current_response.count as u64) < total + { + current_response.count += 1; + + current_response.received_nodes.append(&mut nodes); + self.active_nodes_responses + .insert(node_id, current_response); + self.active_requests.insert(id, active_request); + return; + } + + // have received all the Nodes responses we are willing to accept + // ignore duplicates here as they will be handled when adding + // to the DHT + current_response.received_nodes.append(&mut nodes); + nodes = current_response.received_nodes; + } + + debug!( + "Received a nodes response of len: {}, total: {}, from: {}", + nodes.len(), + total, + active_request.contact + ); + // note: If a peer sends an initial NODES response with a total > 1 then + // in a later response sends a response with a total of 1, all previous nodes + // will be ignored. + // ensure any mapping is removed in this rare case + self.active_nodes_responses.remove(&node_id); + + self.discovered(&node_id, nodes, active_request.query_id); + } + ResponseBody::Pong { enr_seq, ip, port } => { + let socket = SocketAddr::new(ip, port); + // perform ENR majority-based update if required. + // + // only attempt the majority-update if the peer supplies an ipv4 address to + // mitigate https://github.com/sigp/lighthouse/issues/2215 + // + // Only count votes that from peers we have contacted. + let key: kbucket::Key = node_id.into(); + let should_count = match self.kbuckets.write().entry(&key) { + kbucket::Entry::Present(_, status) + if status.is_connected() && !status.is_incoming() => + { + true + } + _ => false, + }; + + if should_count && socket.is_ipv4() { + let local_socket = self.local_enr.read().udp_socket(); + if let Some(ref mut ip_votes) = self.ip_votes { + ip_votes.insert(node_id, socket); + if let Some(majority_socket) = ip_votes.majority() { + if Some(majority_socket) != local_socket { + info!("Local UDP socket updated to: {}", majority_socket); + self.send_event(Discv5Event::SocketUpdated(majority_socket)); + // Update the UDP socket + if self + .local_enr + .write() + .set_udp_socket(majority_socket, &self.enr_key.read()) + .is_ok() + { + self.ping_connected_peers(); + } + } + } + } + } + + // check if we need to request a new ENR + if let Some(enr) = self.find_enr(&node_id) { + if enr.seq() < enr_seq { + // request an ENR update + debug!("Requesting an ENR update from: {}", active_request.contact); + let request_body = RequestBody::FindNode { distances: vec![0] }; + let active_request = ActiveRequest { + contact: active_request.contact, + request_body, + query_id: None, + callback: None, + }; + self.send_rpc_request(active_request); + } + self.connection_updated(node_id, ConnectionStatus::PongReceived(enr)); + } + } + ResponseBody::Talk { response } => { + // Send the response to the user + match active_request.callback { + Some(CallbackResponse::Talk(callback)) => { + let _ = callback.send(Ok(response)); + } + _ => error!("Invalid callback for response"), + } + } + ResponseBody::Ticket { .. } => { + error!("Received a TICKET response. This is unimplemented and should be unreachable."); + } + ResponseBody::RegisterConfirmation { .. } => { + error!("Received a RegisterConfirmation response. This is unimplemented and should be unreachable."); + } + } + } else { + warn!( + "Received an RPC response which doesn't match a request. Id: {}", + id + ); + } + } + + // Send RPC Requests // + + /// Sends a PING request to a node. + fn send_ping(&mut self, enr: Enr) { + let request_body = RequestBody::Ping { + enr_seq: self.local_enr.read().seq(), + }; + let active_request = ActiveRequest { + contact: enr.into(), + request_body, + query_id: None, + callback: None, + }; + self.send_rpc_request(active_request); + } + + /// Ping all peers that are connected in the routing table. + fn ping_connected_peers(&mut self) { + // maintain the ping interval + let connected_peers = { + let mut kbuckets = self.kbuckets.write(); + kbuckets + .iter() + .filter_map(|entry| { + if entry.status.is_connected() { + Some(entry.node.value.clone()) + } else { + None + } + }) + .collect::>() + }; + + for enr in connected_peers { + self.send_ping(enr.clone()); + } + } + + /// Request an external node's ENR. + fn request_enr( + &mut self, + contact: NodeContact, + callback: Option>>, + ) { + let request_body = RequestBody::FindNode { distances: vec![0] }; + let active_request = ActiveRequest { + contact, + request_body, + query_id: None, + callback: callback.map(CallbackResponse::Enr), + }; + self.send_rpc_request(active_request); + } + + /// Requests a TALK message from the peer. + fn talk_request( + &mut self, + contact: NodeContact, + protocol: Vec, + request: Vec, + callback: oneshot::Sender, RequestError>>, + ) { + let request_body = RequestBody::Talk { protocol, request }; + + let active_request = ActiveRequest { + contact, + request_body, + query_id: None, + callback: Some(CallbackResponse::Talk(callback)), + }; + self.send_rpc_request(active_request); + } + + /// Sends a NODES response, given a list of found ENR's. This function splits the nodes up + /// into multiple responses to ensure the response stays below the maximum packet size. + fn send_nodes_response( + &mut self, + node_address: NodeAddress, + rpc_id: RequestId, + mut distances: Vec, + ) { + // NOTE: At most we only allow 5 distances to be sent (see the decoder). If each of these + // buckets are full, that equates to 80 ENR's to respond with. + + let mut nodes_to_send = Vec::new(); + distances.sort_unstable(); + distances.dedup(); + + if let Some(0) = distances.first() { + // if the distance is 0 send our local ENR + nodes_to_send.push(self.local_enr.read().clone()); + debug!("Sending our ENR to node: {}", node_address); + distances.remove(0); + } + + if !distances.is_empty() { + let mut kbuckets = self.kbuckets.write(); + for node in kbuckets + .nodes_by_distances(distances.as_slice(), self.config.max_nodes_response) + .into_iter() + .filter_map(|entry| { + if entry.node.key.preimage() != &node_address.node_id { + Some(entry.node.value.clone()) + } else { + None + } + }) + { + nodes_to_send.push(node); + } + } + + // if there are no nodes, send an empty response + if nodes_to_send.is_empty() { + let response = Response { + id: rpc_id, + body: ResponseBody::Nodes { + total: 1u64, + nodes: Vec::new(), + }, + }; + trace!( + "Sending empty FINDNODES response to: {}", + node_address.node_id + ); + let _ = self + .handler_send + .send(HandlerRequest::Response(node_address, Box::new(response))); + } else { + // build the NODES response + let mut to_send_nodes: Vec> = Vec::new(); + let mut total_size = 0; + let mut rpc_index = 0; + to_send_nodes.push(Vec::new()); + for enr in nodes_to_send.into_iter() { + let entry_size = rlp::encode(&enr).len(); + // Responses assume that a session is established. Thus, on top of the encoded + // ENR's the packet should be a regular message. A regular message has an IV (16 + // bytes), and a header of 55 bytes. The find-nodes RPC requires 16 bytes for the ID and the + // `total` field. Also there is a 16 byte HMAC for encryption and an extra byte for + // RLP encoding. + // + // We could also be responding via an autheader which can take up to 282 bytes in its + // header. + // As most messages will be normal messages we will try and pack as many ENR's we + // can in and drop the response packet if a user requests an auth message of a very + // packed response. + // + // The estimated total overhead for a regular message is therefore 104 bytes. + if entry_size + total_size < MAX_PACKET_SIZE - 104 { + total_size += entry_size; + trace!( + "Adding ENR {}, size {}, total size {}", + enr, + entry_size, + total_size + ); + to_send_nodes[rpc_index].push(enr); + } else { + total_size = entry_size; + to_send_nodes.push(vec![enr]); + rpc_index += 1; + } + } + + let responses: Vec = to_send_nodes + .into_iter() + .map(|nodes| Response { + id: rpc_id.clone(), + body: ResponseBody::Nodes { + total: (rpc_index + 1) as u64, + nodes, + }, + }) + .collect(); + + for response in responses { + trace!( + "Sending FINDNODES response to: {}. Response: {} ", + node_address, + response + ); + let _ = self.handler_send.send(HandlerRequest::Response( + node_address.clone(), + Box::new(response), + )); + } + } + } + + /// Constructs and sends a request RPC to the session service given a `QueryInfo`. + fn send_rpc_query( + &mut self, + query_id: QueryId, + return_peer: NodeId, + request_body: RequestBody, + ) { + // find the ENR associated with the query + if let Some(enr) = self.find_enr(&return_peer) { + let active_request = ActiveRequest { + contact: enr.into(), + request_body, + query_id: Some(query_id), + callback: None, + }; + self.send_rpc_request(active_request); + } else { + error!("Query {} requested an unknown ENR", *query_id); + } + } + + /// Sends generic RPC requests. Each request gets added to known outputs, awaiting a response. + fn send_rpc_request(&mut self, active_request: ActiveRequest) { + // Generate a random rpc_id which is matched per node id + let id = RequestId::random(); + let request: Request = Request { + id: id.clone(), + body: active_request.request_body.clone(), + }; + let contact = active_request.contact.clone(); + self.active_requests.insert(id, active_request); + debug!("Sending RPC {} to node: {}", request, contact); + + let _ = self + .handler_send + .send(HandlerRequest::Request(contact, Box::new(request))); + } + + fn send_event(&mut self, event: Discv5Event) { + if let Some(stream) = self.event_stream.as_mut() { + if let Err(mpsc::error::TrySendError::Closed(_)) = stream.try_send(event) { + // If the stream has been dropped prevent future attempts to send events + self.event_stream = None; + } + } + } + + /// Processes discovered peers from a query. + fn discovered(&mut self, source: &NodeId, mut enrs: Vec, query_id: Option) { + let local_id = self.local_enr.read().node_id(); + enrs.retain(|enr| { + if enr.node_id() == local_id { + return false; + } + + // If any of the discovered nodes are in the routing table, and there contains an older ENR, update it. + // If there is an event stream send the Discovered event + if self.config.report_discovered_peers { + self.send_event(Discv5Event::Discovered(enr.clone())); + } + + // ignore peers that don't pass the table filter + if (self.config.table_filter)(enr) { + let key = kbucket::Key::from(enr.node_id()); + + // If the ENR exists in the routing table and the discovered ENR has a greater + // sequence number, perform some filter checks before updating the enr. + + let must_update_enr = match self.kbuckets.write().entry(&key) { + kbucket::Entry::Present(entry, _) => entry.value().seq() < enr.seq(), + kbucket::Entry::Pending(mut entry, _) => entry.value().seq() < enr.seq(), + _ => false, + }; + + if must_update_enr { + if let UpdateResult::Failed(reason) = + self.kbuckets.write().update_node(&key, enr.clone(), None) + { + self.peers_to_ping.remove(&enr.node_id()); + debug!( + "Failed to update discovered ENR. Node: {}, Reason: {:?}", + source, reason + ); + + false // Remove this peer from the discovered list + } else { + true // Keep this peer in the list + } + } else { + true // We don't need to update ENR + } + } else { + false // Didn't pass the table filter + } + }); + + // if this is part of a query, update the query + if let Some(query_id) = query_id { + if let Some(query) = self.queries.get_mut(query_id) { + let mut peer_count = 0; + for enr_ref in enrs.iter() { + if !query + .target_mut() + .untrusted_enrs + .iter() + .any(|e| e.node_id() == enr_ref.node_id()) + { + query.target_mut().untrusted_enrs.push(enr_ref.clone()); + } + peer_count += 1; + } + debug!("{} peers found for query id {:?}", peer_count, query_id); + query.on_success(source, &enrs) + } else { + debug!("Response returned for ended query {:?}", query_id) + } + } + } + + /// Update the connection status of a node in the routing table. + /// This tracks whether or not we should be pinging peers. Disconnected peers are removed from + /// the queue and newly added peers to the routing table are added to the queue. + fn connection_updated(&mut self, node_id: NodeId, new_status: ConnectionStatus) { + // Variables to that may require post-processing + let mut ping_peer = None; + let mut event_to_send = None; + + let key = kbucket::Key::from(node_id); + match new_status { + ConnectionStatus::Connected(enr, direction) => { + // attempt to update or insert the new ENR. + let status = NodeStatus { + state: ConnectionState::Connected, + direction, + }; + match self.kbuckets.write().insert_or_update(&key, enr, status) { + InsertResult::Inserted => { + // We added this peer to the table + debug!("New connected node added to routing table: {}", node_id); + self.peers_to_ping.insert(node_id); + let event = Discv5Event::NodeInserted { + node_id, + replaced: None, + }; + event_to_send = Some(event); + } + InsertResult::Pending { disconnected } => { + ping_peer = Some(disconnected); + } + InsertResult::StatusUpdated { + promoted_to_connected, + } + | InsertResult::Updated { + promoted_to_connected, + } => { + // The node was updated + if promoted_to_connected { + debug!("Node promoted to connected: {}", node_id); + self.peers_to_ping.insert(node_id); + } + } + InsertResult::ValueUpdated | InsertResult::UpdatedPending => {} + InsertResult::Failed(reason) => { + self.peers_to_ping.remove(&node_id); + trace!("Could not insert node: {}, reason: {:?}", node_id, reason); + } + } + } + ConnectionStatus::PongReceived(enr) => { + match self + .kbuckets + .write() + .update_node(&key, enr, Some(ConnectionState::Connected)) + { + UpdateResult::Failed(reason) => { + self.peers_to_ping.remove(&node_id); + debug!( + "Could not update ENR from pong. Node: {}, reason: {:?}", + node_id, reason + ); + } + update => { + debug!("Updated {:?}", update) + } // Updated ENR successfully. + } + } + ConnectionStatus::Disconnected => { + // If the node has disconnected, remove any ping timer for the node. + match self.kbuckets.write().update_node_status( + &key, + ConnectionState::Disconnected, + None, + ) { + UpdateResult::Failed(reason) => match reason { + FailureReason::KeyNonExistant => {} + others => { + warn!( + "Could not update node to disconnected. Node: {}, Reason: {:?}", + node_id, others + ); + } + }, + _ => { + debug!("Node set to disconnected: {}", node_id) + } + } + self.peers_to_ping.remove(&node_id); + } + }; + + // Post processing + + if let Some(event) = event_to_send { + self.send_event(event); + } + + if let Some(node_key) = ping_peer { + let optional_enr = { + if let kbucket::Entry::Present(entry, _status) = + self.kbuckets.write().entry(&node_key) + { + // NOTE: We don't check the status of this peer. We try and ping outdated peers. + Some(entry.value().clone()) + } else { + None + } + }; + if let Some(enr) = optional_enr { + self.send_ping(enr) + } + } + } + + /// The equivalent of libp2p `inject_connected()` for a udp session. We have no stream, but a + /// session key-pair has been negotiated. + fn inject_session_established(&mut self, enr: Enr, direction: ConnectionDirection) { + // Ignore sessions with non-contactable ENRs + if enr.udp_socket().is_none() { + return; + } + + let node_id = enr.node_id(); + debug!( + "Session established with Node: {}, direction: {}", + node_id, direction + ); + self.connection_updated(node_id, ConnectionStatus::Connected(enr, direction)); + } + + /// A session could not be established or an RPC request timed-out (after a few retries, if + /// specified). + fn rpc_failure(&mut self, id: RequestId, error: RequestError) { + trace!("RPC Error removing request. Reason: {:?}, id {}", error, id); + if let Some(active_request) = self.active_requests.remove(&id) { + // If this is initiated by the user, return an error on the callback. All callbacks + // support a request error. + match active_request.callback { + Some(CallbackResponse::Enr(callback)) => { + callback + .send(Err(error)) + .unwrap_or_else(|_| debug!("Couldn't send TALK error response to user")); + return; + } + Some(CallbackResponse::Talk(callback)) => { + // return the error + callback + .send(Err(error)) + .unwrap_or_else(|_| debug!("Couldn't send TALK error response to user")); + return; + } + None => { + // no callback to send too + } + } + + let node_id = active_request.contact.node_id(); + match active_request.request_body { + // if a failed FindNodes request, ensure we haven't partially received packets. If + // so, process the partially found nodes + RequestBody::FindNode { .. } => { + if let Some(nodes_response) = self.active_nodes_responses.remove(&node_id) { + if !nodes_response.received_nodes.is_empty() { + warn!( + "NODES Response failed, but was partially processed from: {}", + active_request.contact + ); + // if it's a query mark it as success, to process the partial + // collection of peers + self.discovered( + &node_id, + nodes_response.received_nodes, + active_request.query_id, + ); + } + } else { + // there was no partially downloaded nodes inform the query of the failure + // if it's part of a query + if let Some(query_id) = active_request.query_id { + if let Some(query) = self.queries.get_mut(query_id) { + query.on_failure(&node_id); + } + } else { + debug!( + "Failed RPC request: {}: {} ", + active_request.request_body, active_request.contact + ); + } + } + } + // for all other requests, if any are queries, mark them as failures. + _ => { + if let Some(query_id) = active_request.query_id { + if let Some(query) = self.queries.get_mut(query_id) { + debug!( + "Failed query request: {} for query: {} and {} ", + active_request.request_body, *query_id, active_request.contact + ); + query.on_failure(&node_id); + } + } else { + debug!( + "Failed RPC request: {} for node: {}, reason {:?} ", + active_request.request_body, active_request.contact, error + ); + } + } + } + + self.connection_updated(node_id, ConnectionStatus::Disconnected); + } + } + + /// A future that maintains the routing table and inserts nodes when required. This returns the + /// `Discv5Event::NodeInserted` variant if a new node has been inserted into the routing table. + async fn bucket_maintenance_poll( + kbuckets: &Arc>>, + ) -> Discv5Event { + future::poll_fn(move |_cx| { + // Drain applied pending entries from the routing table. + if let Some(entry) = kbuckets.write().take_applied_pending() { + let event = Discv5Event::NodeInserted { + node_id: entry.inserted.into_preimage(), + replaced: entry.evicted.map(|n| n.key.into_preimage()), + }; + return Poll::Ready(event); + } + Poll::Pending + }) + .await + } + + /// A future the maintains active queries. This returns completed and timed out queries, as + /// well as queries which need to be driven further with extra requests. + async fn query_event_poll(queries: &mut QueryPool) -> QueryEvent { + future::poll_fn(move |_cx| match queries.poll() { + QueryPoolState::Finished(query) => Poll::Ready(QueryEvent::Finished(Box::new(query))), + QueryPoolState::Waiting(Some((query, return_peer))) => { + let node_id = return_peer; + + let request_body = match query.target().rpc_request(return_peer) { + Ok(r) => r, + Err(e) => { + // dst node is local_key, report failure + error!("Send RPC failed: {}", e); + query.on_failure(&node_id); + return Poll::Pending; + } + }; + + Poll::Ready(QueryEvent::Waiting(query.id(), node_id, request_body)) + } + QueryPoolState::Timeout(query) => { + warn!("Query id: {:?} timed out", query.id()); + Poll::Ready(QueryEvent::TimedOut(Box::new(query))) + } + QueryPoolState::Waiting(None) | QueryPoolState::Idle => Poll::Pending, + }) + .await + } +} + +/// The result of the `query_event_poll` indicating an action is required to further progress an +/// active query. +enum QueryEvent { + /// The query is waiting for a peer to be contacted. + Waiting(QueryId, NodeId, RequestBody), + /// The query has timed out, possible returning peers. + TimedOut(Box>), + /// The query has completed successfully. + Finished(Box>), +} + +/// The types of queries that can be made. +pub enum QueryKind { + /// A FindNode query. Searches for peers that are closest to a particular target. + FindNode { target_node: NodeId }, + /// A predicate query. Searches for peers that are close to a target but filtered by a specific + /// predicate and limited by a target peer count. + Predicate { + target_node: NodeId, + target_peer_no: usize, + predicate: Box bool + Send>, + }, +} + +/// Reporting the connection status of a node. +enum ConnectionStatus { + /// A node has started a new connection with us. + Connected(Enr, ConnectionDirection), + /// We received a Pong from a new node. Do not have the connection direction. + PongReceived(Enr), + /// The node has disconnected + Disconnected, +} diff --git a/version-meld/discv5/src/service/hashset_delay.rs b/version-meld/discv5/src/service/hashset_delay.rs new file mode 100644 index 0000000..926642e --- /dev/null +++ b/version-meld/discv5/src/service/hashset_delay.rs @@ -0,0 +1,131 @@ +//! A simple hashset object coupled with a `delay_queue` which has entries that expire after a +//! fixed time. +//! +//! A `HashSetDelay` implements `Stream` which removes expired items from the map. + +/// The default delay for entries, in seconds. This is only used when `insert()` is used to add +/// entries. +const DEFAULT_DELAY: u64 = 30; + +use futures::prelude::*; +use std::{ + collections::HashMap, + pin::Pin, + task::{Context, Poll}, + time::Duration, +}; +use tokio_util::time::delay_queue::{self, DelayQueue}; + +pub struct HashSetDelay +where + K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin, +{ + /// The given entries. + entries: HashMap, + /// A queue holding the timeouts of each entry. + expirations: DelayQueue, + /// The default expiration timeout of an entry. + default_entry_timeout: Duration, +} + +impl Default for HashSetDelay +where + K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin, +{ + fn default() -> Self { + HashSetDelay::new(Duration::from_secs(DEFAULT_DELAY)) + } +} + +impl HashSetDelay +where + K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin, +{ + /// Creates a new instance of `HashSetDelay`. + pub fn new(default_entry_timeout: Duration) -> Self { + HashSetDelay { + entries: HashMap::new(), + expirations: DelayQueue::new(), + default_entry_timeout, + } + } + + /// Insert an entry into the mapping. Entries will expire after the `default_entry_timeout`. + pub fn insert(&mut self, key: K) { + self.insert_at(key, self.default_entry_timeout); + } + + /// Inserts an entry that will expire at a given instant. + pub fn insert_at(&mut self, key: K, entry_duration: Duration) { + if self.contains_key(&key) { + // update the timeout + self.update_timeout(&key, entry_duration); + } else { + let delay_key = self.expirations.insert(key.clone(), entry_duration); + self.entries.insert(key, delay_key); + } + } + + /// Updates the timeout for a given key. Returns true if the key existed, false otherwise. + /// + /// Panics if the duration is too far in the future. + pub fn update_timeout(&mut self, key: &K, timeout: Duration) -> bool { + if let Some(delay_key) = self.entries.get_mut(key) { + self.expirations.reset(delay_key, timeout); + true + } else { + false + } + } + + /// Returns true if the key exists, false otherwise. + pub fn contains_key(&self, key: &K) -> bool { + self.entries.contains_key(key) + } + + /// Returns the length of the mapping. + #[allow(dead_code)] + pub fn len(&self) -> usize { + self.entries.len() + } + + /// Removes a key from the map returning the value associated with the key that was in the map. + /// + /// Return None if the key was not in the map. + pub fn remove(&mut self, key: &K) -> bool { + if let Some(delay_key) = self.entries.remove(key) { + self.expirations.remove(&delay_key); + true + } else { + false + } + } + + /// Removes all entries from the map. + #[allow(dead_code)] + pub fn clear(&mut self) { + self.entries.clear(); + self.expirations.clear(); + } +} + +impl Stream for HashSetDelay +where + K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin, +{ + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.expirations.poll_expired(cx) { + Poll::Ready(Some(Ok(key))) => match self.entries.remove(key.get_ref()) { + Some(_delay_key) => Poll::Ready(Some(Ok(key.into_inner()))), + None => Poll::Ready(Some(Err("Value no longer exists in expirations".into()))), + }, + Poll::Ready(Some(Err(e))) => { + Poll::Ready(Some(Err(format!("delay queue error: {:?}", e)))) + } + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + } + } +} diff --git a/version-meld/discv5/src/service/ip_vote.rs b/version-meld/discv5/src/service/ip_vote.rs new file mode 100644 index 0000000..3d34ffd --- /dev/null +++ b/version-meld/discv5/src/service/ip_vote.rs @@ -0,0 +1,113 @@ +use enr::NodeId; +use fnv::FnvHashMap; +use std::{ + collections::HashMap, + net::SocketAddr, + time::{Duration, Instant}, +}; + +/// A collection of IP:Ports for our node reported from external peers. +pub(crate) struct IpVote { + /// The current collection of IP:Port votes. + votes: HashMap, + /// The minimum number of votes required before an IP/PORT is accepted. + minimum_threshold: usize, + /// The time votes remain valid. + vote_duration: Duration, +} + +impl IpVote { + pub fn new(minimum_threshold: usize, vote_duration: Duration) -> Self { + // do not allow minimum thresholds less than 2 + if minimum_threshold < 2 { + panic!("Setting enr_peer_update_min to a value less than 2 will cause issues with discovery with peers behind NAT"); + } + IpVote { + votes: HashMap::new(), + minimum_threshold, + vote_duration, + } + } + + pub fn insert(&mut self, key: NodeId, socket: SocketAddr) { + self.votes + .insert(key, (socket, Instant::now() + self.vote_duration)); + } + + /// Returns the majority `SocketAddr` if it exists. If there are not enough votes to meet the threshold this returns None. + pub fn majority(&mut self) -> Option { + // remove any expired votes + let instant = Instant::now(); + self.votes.retain(|_, v| v.1 > instant); + + // count votes, take majority + let mut ip_count: FnvHashMap = FnvHashMap::default(); + for (socket, _) in self.votes.values() { + *ip_count.entry(*socket).or_insert_with(|| 0) += 1; + } + + // find the maximum socket addr + ip_count + .into_iter() + .filter(|v| v.1 >= self.minimum_threshold) + .max_by_key(|v| v.1) + .map(|v| v.0) + } +} + +#[cfg(test)] +mod tests { + use super::{Duration, IpVote, NodeId, SocketAddr}; + + #[test] + fn test_three_way_vote_draw() { + let mut votes = IpVote::new(2, Duration::from_secs(10)); + + let socket_1 = SocketAddr::new("127.0.0.1".parse().unwrap(), 1); + let socket_2 = SocketAddr::new("127.0.0.1".parse().unwrap(), 2); + let socket_3 = SocketAddr::new("127.0.0.1".parse().unwrap(), 3); + + // 3 votes for each socket + votes.insert(NodeId::random(), socket_1); + votes.insert(NodeId::random(), socket_1); + votes.insert(NodeId::random(), socket_1); + votes.insert(NodeId::random(), socket_2); + votes.insert(NodeId::random(), socket_2); + votes.insert(NodeId::random(), socket_2); + votes.insert(NodeId::random(), socket_3); + votes.insert(NodeId::random(), socket_3); + votes.insert(NodeId::random(), socket_3); + + assert_eq!(votes.majority(), Some(socket_2)); + } + + #[test] + fn test_majority_vote() { + let mut votes = IpVote::new(2, Duration::from_secs(10)); + let socket_1 = SocketAddr::new("127.0.0.1".parse().unwrap(), 1); + let socket_2 = SocketAddr::new("127.0.0.1".parse().unwrap(), 2); + let socket_3 = SocketAddr::new("127.0.0.1".parse().unwrap(), 3); + + votes.insert(NodeId::random(), socket_1); + votes.insert(NodeId::random(), socket_1); + votes.insert(NodeId::random(), socket_2); + votes.insert(NodeId::random(), socket_3); + + assert_eq!(votes.majority(), Some(socket_1)); + } + + #[test] + fn test_below_threshold() { + let mut votes = IpVote::new(3, Duration::from_secs(10)); + let socket_1 = SocketAddr::new("127.0.0.1".parse().unwrap(), 1); + let socket_2 = SocketAddr::new("127.0.0.1".parse().unwrap(), 2); + let socket_3 = SocketAddr::new("127.0.0.1".parse().unwrap(), 3); + + votes.insert(NodeId::random(), socket_1); + votes.insert(NodeId::random(), socket_1); + votes.insert(NodeId::random(), socket_2); + votes.insert(NodeId::random(), socket_3); + + assert_eq!(votes.majority(), None); + } +} diff --git a/version-meld/discv5/src/service/query_info.rs b/version-meld/discv5/src/service/query_info.rs new file mode 100644 index 0000000..980474e --- /dev/null +++ b/version-meld/discv5/src/service/query_info.rs @@ -0,0 +1,134 @@ +use crate::{kbucket::Key, rpc::RequestBody, Enr}; +use enr::NodeId; +use sha2::digest::generic_array::GenericArray; +use smallvec::SmallVec; +use tokio::sync::oneshot; + +/// Information about a query. +#[derive(Debug)] +pub struct QueryInfo { + /// What we are querying and why. + pub query_type: QueryType, + + /// Temporary ENRs used when trying to reach nodes. + pub untrusted_enrs: SmallVec<[Enr; 16]>, + + /// A callback channel for the service that requested the query. + pub callback: oneshot::Sender>, + + /// The number of distances we request for each peer. + pub distances_to_request: usize, +} + +/// Additional information about the query. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum QueryType { + /// The user requested a `FIND_NODE` query to be performed. It should be reported when finished. + FindNode(NodeId), +} + +impl QueryInfo { + /// Builds an RPC Request, given the QueryInfo + pub(crate) fn rpc_request(&self, peer: NodeId) -> Result { + let request = match self.query_type { + QueryType::FindNode(node_id) => { + let distances = findnode_log2distance(node_id, peer, self.distances_to_request) + .ok_or("Requested a node find itself")?; + RequestBody::FindNode { distances } + } + }; + + Ok(request) + } +} + +impl crate::query_pool::TargetKey for QueryInfo { + fn key(&self) -> Key { + match self.query_type { + QueryType::FindNode(ref node_id) => { + Key::new_raw(*node_id, *GenericArray::from_slice(&node_id.raw())) + } + } + } +} + +/// Calculates the log2 distance for a destination peer given a target and the size (number of +/// distances to request). +/// +/// As the iteration increases, FINDNODE requests adjacent distances from the exact peer distance. +/// +/// As an example, if the target has a distance of 12 from the remote peer, the sequence of distances that are sent for increasing iterations would be [12, 13, 11, 14, 10, .. ]. +fn findnode_log2distance(target: NodeId, peer: NodeId, size: usize) -> Option> { + if size > 127 { + // invoke and endless loop - coding error + panic!("Iterations cannot be greater than 127"); + } + + let dst_key: Key = peer.into(); + let distance = dst_key.log2_distance(&target.into())?; + + let mut result_list = vec![distance]; + let mut difference = 1; + while result_list.len() < size { + if distance + difference <= 256 { + result_list.push(distance + difference); + } + if result_list.len() < size { + if let Some(d) = distance.checked_sub(difference) { + result_list.push(d); + } + } + difference += 1; + } + Some(result_list[..size].to_vec()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_log2distance() { + let target = NodeId::new(&[0u8; 32]); + let mut destination = [0u8; 32]; + destination[10] = 1; // gives a log2 distance of 169 + let destination = NodeId::new(&destination); + + let expected_distances = vec![169, 170, 168, 171, 167, 172, 166, 173, 165]; + + assert_eq!( + findnode_log2distance(target, destination, expected_distances.len()).unwrap(), + expected_distances + ); + } + + #[test] + fn test_log2distance_lower() { + let target = NodeId::new(&[0u8; 32]); + let mut destination = [0u8; 32]; + destination[31] = 8; // gives a log2 distance of 5 + let destination = NodeId::new(&destination); + + let expected_distances = vec![4, 5, 3, 6, 2, 7, 1, 8, 0, 9, 10]; + + assert_eq!( + findnode_log2distance(target, destination, expected_distances.len()).unwrap(), + expected_distances + ); + } + + #[test] + fn test_log2distance_upper() { + let target = NodeId::new(&[0u8; 32]); + let mut destination = [0u8; 32]; + destination[0] = 8; // gives a log2 distance of 252 + let destination = NodeId::new(&destination); + + let expected_distances = vec![252, 253, 251, 254, 250, 255, 249, 256, 248, 247, 246]; + + assert_eq!( + findnode_log2distance(target, destination, expected_distances.len()).unwrap(), + expected_distances + ); + } +} diff --git a/version-meld/discv5/src/service/test.rs b/version-meld/discv5/src/service/test.rs new file mode 100644 index 0000000..29b21b1 --- /dev/null +++ b/version-meld/discv5/src/service/test.rs @@ -0,0 +1,173 @@ +#![cfg(test)] + +use super::*; + +use crate::{ + handler::Handler, + kbucket, + kbucket::{BucketInsertResult, KBucketsTable, NodeStatus}, + node_info::NodeContact, + query_pool::{QueryId, QueryPool}, + rpc, + rpc::RequestId, + service::{ActiveRequest, Service}, + Discv5ConfigBuilder, Enr, +}; +use enr::{CombinedKey, EnrBuilder}; +use parking_lot::RwLock; +use std::{ + collections::HashMap, + net::{IpAddr, SocketAddr}, + sync::Arc, + time::Duration, +}; +use tokio::sync::{mpsc, oneshot}; + +fn _connected_state() -> NodeStatus { + NodeStatus { + state: ConnectionState::Connected, + direction: ConnectionDirection::Outgoing, + } +} + +fn disconnected_state() -> NodeStatus { + NodeStatus { + state: ConnectionState::Disconnected, + direction: ConnectionDirection::Outgoing, + } +} + +fn init() { + let _ = tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .try_init(); +} + +async fn build_service( + local_enr: Arc>, + enr_key: Arc>, + listen_socket: SocketAddr, + filters: bool, +) -> Service { + let config = Discv5ConfigBuilder::new() + .executor(Box::new(crate::executor::TokioExecutor::default())) + .build(); + // build the session service + let (_handler_exit, handler_send, handler_recv) = Handler::spawn( + local_enr.clone(), + enr_key.clone(), + listen_socket, + config.clone(), + ) + .await + .unwrap(); + + let (table_filter, bucket_filter) = if filters { + ( + Some(Box::new(kbucket::IpTableFilter) as Box>), + Some(Box::new(kbucket::IpBucketFilter) as Box>), + ) + } else { + (None, None) + }; + + let kbuckets = Arc::new(RwLock::new(KBucketsTable::new( + local_enr.read().node_id().into(), + Duration::from_secs(60), + config.incoming_bucket_limit, + table_filter, + bucket_filter, + ))); + + // create the required channels + let (_discv5_send, discv5_recv) = mpsc::channel(30); + let (_exit_send, exit) = oneshot::channel(); + + Service { + local_enr, + enr_key, + kbuckets, + queries: QueryPool::new(config.query_timeout), + active_requests: Default::default(), + active_nodes_responses: HashMap::new(), + ip_votes: None, + handler_send, + handler_recv, + handler_exit: Some(_handler_exit), + peers_to_ping: HashSetDelay::new(config.ping_interval), + discv5_recv, + event_stream: None, + exit, + config, + } +} + +#[tokio::test] +async fn test_updating_connection_on_ping() { + init(); + let enr_key1 = CombinedKey::generate_secp256k1(); + let ip: IpAddr = "127.0.0.1".parse().unwrap(); + let enr = EnrBuilder::new("v4") + .ip(ip) + .udp(10001) + .build(&enr_key1) + .unwrap(); + let ip2: IpAddr = "127.0.0.1".parse().unwrap(); + let enr_key2 = CombinedKey::generate_secp256k1(); + let enr2 = EnrBuilder::new("v4") + .ip(ip2) + .udp(10002) + .build(&enr_key2) + .unwrap(); + + let socket_addr = enr.udp_socket().unwrap(); + + let mut service = build_service( + Arc::new(RwLock::new(enr)), + Arc::new(RwLock::new(enr_key1)), + socket_addr, + false, + ) + .await; + // Set up service with one disconnected node + let key = kbucket::Key::from(enr2.node_id()); + if let kbucket::Entry::Absent(entry) = service.kbuckets.write().entry(&key) { + match entry.insert(enr2.clone(), disconnected_state()) { + BucketInsertResult::Inserted => {} + BucketInsertResult::Full => { + panic!("Can't be full"); + } + BucketInsertResult::Pending { .. } => {} + _ => panic!("Could not be inserted"), + } + } + + // Add a fake request + let response = rpc::Response { + id: RequestId(vec![1]), + body: rpc::ResponseBody::Pong { + enr_seq: 2, + ip: ip2, + port: 10002, + }, + }; + + let node_contact = NodeContact::Enr(Box::new(enr2)); + let expected_return_addr = node_contact.node_address().unwrap(); + + service.active_requests.insert( + RequestId(vec![1]), + ActiveRequest { + contact: node_contact, + request_body: rpc::RequestBody::Ping { enr_seq: 2 }, + query_id: Some(QueryId(1)), + callback: None, + }, + ); + + // Handle the ping and expect the disconnected Node to become connected + service.handle_rpc_response(expected_return_addr, response); + let buckets = service.kbuckets.read(); + let node = buckets.iter_ref().next().unwrap(); + assert!(node.status.is_connected()) +} diff --git a/version-meld/discv5/src/socket/filter/cache.rs b/version-meld/discv5/src/socket/filter/cache.rs new file mode 100644 index 0000000..6f996f8 --- /dev/null +++ b/version-meld/discv5/src/socket/filter/cache.rs @@ -0,0 +1,113 @@ +//! This provides a cache to check rate limits as well as store data for metrics. +//! +//! The cache essentially consists of a time-ordered list of elements. The list is split into two +//! sections, within the enforced time and without. +//! +//! +//! | | Enforced Time | +//! \[x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x\] +//! +//! The enforced time represents one seconds worth of elements. The target aims to limit the +//! number of elements that can be inserted within the enforced time. The length of the list is +//! determined by the `time_window` this can be longer than one second and can be used by metrics +//! to average results over large values than one second. + +use std::{ + collections::VecDeque, + time::{Duration, Instant}, +}; + +/// The time window that the size of the cache is enforced for. I.e if the size is 5 and +/// ENFORCED_SIZE_TIME is 1 second, this will allow 5 entries per second. This MUST be less than the +/// cache's `time_window`. +pub const ENFORCED_SIZE_TIME: u64 = 1; + +pub struct ReceivedPacket { + /// The source that sent us the packet. + pub content: T, + /// The time the packet was received. + pub received: Instant, +} + +pub struct ReceivedPacketCache { + /// The target number of entries per ENFORCED_SIZE_TIME before inserting new elements reports + /// failure. The maximum size of the cache is target*time_window + target: usize, + /// The cache stores `time_window` seconds worth of information to calculate a moving average. + /// This variable keeps track the number of elements in the cache within the + /// ENFORCED_SIZE_TIME. + time_window: u64, + /// This stores the current number of messages that are within the `ENFORCED_SIZE_TIME`. + within_enforced_time: usize, + /// The underlying data structure. + inner: VecDeque>, +} + +impl ReceivedPacketCache { + /// Creates a new `ReceivedPacketCache` with a specified size from which no more can enter. + pub fn new(target: usize, time_window: u64) -> Self { + Self { + target, + time_window, + within_enforced_time: 0, + inner: VecDeque::with_capacity(target * time_window as usize), + } + } + + /// Remove expired packets. We only keep, `CACHE_TIME` of data in the cache. + pub fn reset(&mut self) { + while let Some(packet) = self.inner.pop_front() { + if packet.received > Instant::now() - Duration::from_secs(self.time_window) { + // add the packet back and end + self.inner.push_front(packet); + break; + } + } + // update the within_enforced_time + let mut count = 0; + for packet in self.inner.iter().rev() { + if packet.received > Instant::now() - Duration::from_secs(ENFORCED_SIZE_TIME) { + count += 1; + } else { + break; + } + } + self.within_enforced_time = count; + } + + /// Inserts an element into the cache, removing any expired elements. + pub fn cache_insert(&mut self, content: T) -> bool { + self.reset(); + self.internal_insert(content) + } + + /// Inserts an element into the cache without removing expired elements. + fn internal_insert(&mut self, content: T) -> bool { + if self.within_enforced_time >= self.target { + // Reached the target + false + } else { + let received_packet = ReceivedPacket { + content, + received: Instant::now(), + }; + self.inner.push_back(received_packet); + self.within_enforced_time += 1; + true + } + } +} + +impl std::ops::Deref for ReceivedPacketCache { + type Target = VecDeque>; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl std::ops::DerefMut for ReceivedPacketCache { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} diff --git a/version-meld/discv5/src/socket/filter/config.rs b/version-meld/discv5/src/socket/filter/config.rs new file mode 100644 index 0000000..3dd755f --- /dev/null +++ b/version-meld/discv5/src/socket/filter/config.rs @@ -0,0 +1,18 @@ +use super::rate_limiter::RateLimiter; + +#[derive(Debug)] +pub struct FilterConfig { + /// Whether the packet filter is enabled or not. + pub enabled: bool, + /// Set up various rate limits for unsolicited packets. See the + /// [`crate::RateLimiterBuilder`] for + /// further details on constructing rate limits. See the [`Default`] implementation for default + /// values. + pub rate_limiter: Option, + /// The maximum number of node-ids allowed per IP address before the IP address gets banned. + /// Having this set to None, disables this feature. Default value is 10. + pub max_nodes_per_ip: Option, + /// The maximum number of nodes that can be banned by a single IP before that IP gets banned. + /// The default is 5. + pub max_bans_per_ip: Option, +} diff --git a/version-meld/discv5/src/socket/filter/mod.rs b/version-meld/discv5/src/socket/filter/mod.rs new file mode 100644 index 0000000..f755758 --- /dev/null +++ b/version-meld/discv5/src/socket/filter/mod.rs @@ -0,0 +1,223 @@ +//! A filter which decides whether to accept/reject incoming UDP packets. + +use crate::{discv5::PERMIT_BAN_LIST, metrics::METRICS, node_info::NodeAddress, packet::Packet}; +use cache::ReceivedPacketCache; +use enr::NodeId; +use lru::LruCache; +use std::{ + collections::HashSet, + net::{IpAddr, SocketAddr}, + sync::atomic::Ordering, + time::{Duration, Instant}, +}; +use tracing::{debug, warn}; + +mod cache; +mod config; +pub mod rate_limiter; +pub use config::FilterConfig; +use rate_limiter::{LimitKind, RateLimiter}; + +/// The maximum number of IPs to retain when calculating the number of nodes per IP. +const KNOWN_ADDRS_SIZE: usize = 500; +/// The number of IPs to retain at any given time that have banned nodes. +const BANNED_NODES_SIZE: usize = 50; +/// The maximum number of packets to keep record of for metrics if the rate limiter is not +/// specified. +const DEFAULT_PACKETS_PER_SECOND: usize = 20; + +/// The packet filter which decides whether we accept or reject incoming packets. +pub(crate) struct Filter { + /// Whether the filter is enabled or not. + enabled: bool, + /// An optional rate limiter for incoming packets. + rate_limiter: Option, + /// An ordered (by time) collection of recently seen packets by SocketAddr. The packet data is not + /// stored here. This stores 5 seconds of history to calculate a 5 second moving average for + /// the metrics. + raw_packets_received: ReceivedPacketCache, + /// The duration that bans by this filter last. + ban_duration: Option, + /// Keep track of node ids per socket. If someone is using too many node-ids per IP, they can + /// be banned. + known_addrs: LruCache>, + /// Keep track of Ips that have banned nodes. If a single IP has many nodes that get banned, + /// then we ban the IP address. + banned_nodes: LruCache, + /// The maximum number of node-ids allowed per IP address before the IP address gets banned. + /// Having this set to None, disables this feature. Default value is 10. + pub max_nodes_per_ip: Option, + /// The maximum number of nodes that can be banned by a single IP before that IP gets banned. + /// The default is 5. + pub max_bans_per_ip: Option, +} + +impl Filter { + pub fn new(config: FilterConfig, ban_duration: Option) -> Filter { + let expected_packets_per_second = config + .rate_limiter + .as_ref() + .map(|v| v.total_requests_per_second().round() as usize) + .unwrap_or(DEFAULT_PACKETS_PER_SECOND); + + Filter { + enabled: config.enabled, + rate_limiter: config.rate_limiter, + raw_packets_received: ReceivedPacketCache::new( + expected_packets_per_second, + METRICS.moving_window, + ), + known_addrs: LruCache::new(KNOWN_ADDRS_SIZE), + banned_nodes: LruCache::new(BANNED_NODES_SIZE), + ban_duration, + max_nodes_per_ip: config.max_nodes_per_ip, + max_bans_per_ip: config.max_bans_per_ip, + } + } + + /// The first check. This determines if a new UDP packet should be decoded or dropped. + /// Only unsolicited packets arrive here. + pub fn initial_pass(&mut self, src: &SocketAddr) -> bool { + if PERMIT_BAN_LIST.read().permit_ips.get(&src.ip()).is_some() { + return true; + } + + if PERMIT_BAN_LIST.read().ban_ips.get(&src.ip()).is_some() { + debug!("Dropped unsolicited packet from banned src: {:?}", src); + return false; + } + + // Add the un-solicited request to the cache + // If this is over the maximum requests per ENFORCED_SIZE_TIME, it will not be added, we + // leave the rate limiter to enforce the rate limits.. + let _ = self.raw_packets_received.cache_insert(*src); + + // build the metrics + METRICS + .unsolicited_requests_per_window + .store(self.raw_packets_received.len(), Ordering::Relaxed); + + // If the filter isn't enabled, pass the packet + if !self.enabled { + return true; + } + + // Check rate limits + if let Some(rate_limiter) = self.rate_limiter.as_mut() { + if rate_limiter.allows(&LimitKind::Ip(src.ip())).is_err() { + warn!("Banning IP for excessive requests: {:?}", src.ip()); + // Ban the IP address + let ban_timeout = self.ban_duration.map(|v| Instant::now() + v); + PERMIT_BAN_LIST + .write() + .ban_ips + .insert(src.ip(), ban_timeout); + return false; + } + + if rate_limiter.allows(&LimitKind::Total).is_err() { + debug!("Dropped unsolicited packet from RPC limit: {:?}", src.ip()); + return false; + } + } + true + } + + pub fn final_pass(&mut self, node_address: &NodeAddress, _packet: &Packet) -> bool { + if PERMIT_BAN_LIST + .read() + .permit_nodes + .get(&node_address.node_id) + .is_some() + { + return true; + } + + if PERMIT_BAN_LIST + .read() + .ban_nodes + .get(&node_address.node_id) + .is_some() + { + debug!( + "Dropped unsolicited packet from banned node_id: {}", + node_address + ); + return false; + } + + // If the filter isn't enabled, just pass the packet. + if !self.enabled { + return true; + } + + if let Some(rate_limiter) = self.rate_limiter.as_mut() { + if rate_limiter + .allows(&LimitKind::NodeId(node_address.node_id)) + .is_err() + { + warn!( + "Node has exceeded its request limit and is now banned {}", + node_address.node_id + ); + + // The node is being banned + let ban_timeout = self.ban_duration.map(|v| Instant::now() + v); + PERMIT_BAN_LIST + .write() + .ban_nodes + .insert(node_address.node_id, ban_timeout); + + // If we are tracking banned nodes per IP, add to the count. If the count is higher + // than our tolerance, ban the IP. + if let Some(max_bans_per_ip) = self.max_bans_per_ip { + let ip = node_address.socket_addr.ip(); + if let Some(banned_count) = self.banned_nodes.get_mut(&ip) { + *banned_count += 1; + if *banned_count >= max_bans_per_ip { + PERMIT_BAN_LIST.write().ban_ips.insert(ip, ban_timeout); + } + } else { + self.banned_nodes.put(ip, 0); + } + } + + return false; + } + } + + // Check the nodes per IP filter configuration + if let Some(max_nodes_per_ip) = self.max_nodes_per_ip { + // This option is set, store the known nodes per IP. + let ip = node_address.socket_addr.ip(); + let known_nodes = { + if let Some(known_nodes) = self.known_addrs.get_mut(&ip) { + known_nodes.insert(node_address.node_id); + known_nodes.len() + } else { + let mut ids = HashSet::new(); + ids.insert(node_address.node_id); + self.known_addrs.put(ip, ids); + 1 + } + }; + + if known_nodes >= max_nodes_per_ip { + warn!("IP has exceeded its node-id limit and is now banned {}", ip); + // The node is being banned + let ban_timeout = self.ban_duration.map(|v| Instant::now() + v); + PERMIT_BAN_LIST.write().ban_ips.insert(ip, ban_timeout); + self.known_addrs.pop(&ip); + return false; + } + } + + true + } + + pub fn prune_limiter(&mut self) { + if let Some(rate_limiter) = self.rate_limiter.as_mut() { + rate_limiter.prune(); + } + } +} diff --git a/version-meld/discv5/src/socket/filter/rate_limiter.rs b/version-meld/discv5/src/socket/filter/rate_limiter.rs new file mode 100644 index 0000000..0e8af57 --- /dev/null +++ b/version-meld/discv5/src/socket/filter/rate_limiter.rs @@ -0,0 +1,414 @@ +use enr::NodeId; +use fnv::FnvHashMap; +use std::{ + convert::TryInto, + hash::Hash, + net::IpAddr, + time::{Duration, Instant}, +}; + +/// Nanoseconds since a given time. +// Maintained as u64 to reduce footprint +// NOTE: this also implies that the rate limiter will manage checking if a batch is allowed for at +// most + u64::MAX nanosecs, ~500 years. So it is realistic to assume this is fine. +type Nanosecs = u64; + +/// User-friendly rate limiting parameters of the GCRA. +/// +/// A quota of `max_tokens` tokens every `replenish_all_every` units of time means that: +/// 1. One token is replenished every `replenish_all_every`/`max_tokens` units of time. +/// 2. Instantaneous bursts (batches) of up to `max_tokens` tokens are allowed. +/// +/// The above implies that if `max_tokens` is greater than 1, the perceived rate may be higher (but +/// bounded) than the defined rate when instantaneous bursts occur. For instance, for a rate of +/// 4T/2s a first burst of 4T is allowed with subsequent requests of 1T every 0.5s forever, +/// producing a perceived rate over the window of the first 2s of 8T. However, subsequent sliding +/// windows of 2s keep the limit. +/// +/// In this scenario using the same rate as above, the sender is always maxing out their tokens, +/// except at seconds 1.5, 3, 3.5 and 4 +/// +/// ```ignore +/// x +/// used x +/// tokens x x x +/// at a x x x x x x +/// given +--+--+--o--+--+--o--o--o--> seconds +/// time | | | | | | | | | +/// 0 1 2 3 4 +/// +/// 4 1 1 1 2 1 1 2 3 <= available tokens when the batch is received +/// ``` +/// +/// For a sender to request a batch of `n`T, they would need to wait at least +/// n*`replenish_all_every`/`max_tokens` units of time since their last request. +/// +/// To produce hard limits, set `max_tokens` to 1. +#[derive(Clone)] +pub struct Quota { + /// How often are `max_tokens` fully replenished. + replenish_all_every: Duration, + /// Token limit. This translates on how large can an instantaneous batch of + /// tokens be. + max_tokens: u64, +} + +/// Manages rate limiting of requests per peer, with differentiated rates per protocol. +#[derive(Debug, Clone)] +pub struct RateLimiter { + /// An estimate of the maximum requests per second. This is only used for estimating the size + /// of the cache for measuring metrics + total_requests_per_second: f32, + /// Creation time of the rate limiter. + init_time: Instant, + /// Total rate limit. Must be set. + total_rl: Limiter<()>, + /// Rate limit for each node + node_rl: Option>, + /// Rate limit for each ip. + ip_rl: Option>, +} + +/// Error type for non conformant requests +pub enum RateLimitedErr { + /// Required tokens for this request exceed the maximum + TooLarge, + /// Request does not fit in the quota. Gives the earliest time the request could be accepted. + TooSoon(Duration), +} + +pub enum LimitKind { + /// Request counts towards the total limit. + Total, + /// Request counts towards the NodeId limit. + NodeId(NodeId), + /// Request counts toward the ip limit. + Ip(IpAddr), +} + +/// User-friendly builder of a `RateLimiter`. The user can specify three kinds of rate limits but +/// must at least set the total quota. The three types are: +/// 1. Total Quota - Specifies the total number of inbound requests. This must be set. +/// 2. Node Quota - Specifies the number of requests per node id. +/// 3. IP Quota - Specifies the number of requests per IP. +/// +/// Quotas can be set via the X_one_every() functions to set hard limits as described above. Using +/// the `X_n_every()` functions allow for bursts. +#[derive(Default)] +pub struct RateLimiterBuilder { + /// Quota for total received RPCs. + total_quota: Option, + /// Quota for each node-id. + node_quota: Option, + /// Quota for each IP. + ip_quota: Option, +} + +#[allow(dead_code)] +impl RateLimiterBuilder { + /// Get an empty `RateLimiterBuilder`. + pub fn new() -> Self { + Default::default() + } + + /// Set the total quota. + fn total_quota(mut self, quota: Quota) -> Self { + self.total_quota = Some(quota); + self + } + + /// Set the node quota. + fn node_quota(mut self, quota: Quota) -> Self { + self.node_quota = Some(quota); + self + } + + /// Set the IP quota. + fn ip_quota(mut self, quota: Quota) -> Self { + self.ip_quota = Some(quota); + self + } + + /// Allow one token every `time_period` to be used for the total RPC limit. + /// This produces a hard limit. + pub fn total_one_every(self, time_period: Duration) -> Self { + self.total_quota(Quota { + replenish_all_every: time_period, + max_tokens: 1, + }) + } + + /// Allow one token every `time_period` to be used for the node RPC limit. + /// This produces a hard limit. + pub fn node_one_every(self, time_period: Duration) -> Self { + self.node_quota(Quota { + replenish_all_every: time_period, + max_tokens: 1, + }) + } + + /// Allow one token every `time_period` to be used for the total RPC limit per IP. + /// This produces a hard limit. + pub fn ip_one_every(self, time_period: Duration) -> Self { + self.ip_quota(Quota { + replenish_all_every: time_period, + max_tokens: 1, + }) + } + + /// Allow `n` tokens to be use used every `time_period` for the total. + pub fn total_n_every(self, n: u64, time_period: Duration) -> Self { + self.total_quota(Quota { + replenish_all_every: time_period, + max_tokens: n, + }) + } + + /// Allow `n` tokens to be use used every `time_period` for the total. + pub fn node_n_every(self, n: u64, time_period: Duration) -> Self { + self.node_quota(Quota { + replenish_all_every: time_period, + max_tokens: n, + }) + } + + /// Allow `n` tokens to be use used every `time_period` for the total. + pub fn ip_n_every(self, n: u64, time_period: Duration) -> Self { + self.ip_quota(Quota { + replenish_all_every: time_period, + max_tokens: n, + }) + } + + pub fn build(self) -> Result { + // get our quotas + let total_quota = self + .total_quota + .ok_or("Total quota not specified and must be set.")?; + + // create the rate limiters + let total_rl = Limiter::from_quota(total_quota.clone())?; + let node_rl = match self.node_quota { + Some(q) => Some(Limiter::from_quota(q)?), + None => None, + }; + let ip_rl = match self.ip_quota { + Some(q) => Some(Limiter::from_quota(q)?), + None => None, + }; + + let total_requests_per_second = if total_quota.max_tokens == 1 { + (1.0 / total_quota.replenish_all_every.as_secs_f32() + / Duration::from_secs(1).as_secs_f32()) + .round() + } else { + (2.0 * total_quota.max_tokens as f32 // multiply by 2 to account for potential bursts + / total_quota.replenish_all_every.as_secs_f32() + / Duration::from_secs(1).as_secs_f32()) + .round() + }; + + Ok(RateLimiter { + total_requests_per_second, + total_rl, + node_rl, + ip_rl, + init_time: Instant::now(), + }) + } +} + +impl RateLimiter { + /// Indicates whether the request is allowed based on the configured rate limits. + pub fn allows(&mut self, request: &LimitKind) -> Result<(), RateLimitedErr> { + let time_since_start = self.init_time.elapsed(); + let tokens = 1; // Only count each of these as one. + + // Check the limits + match request { + LimitKind::Total => self.total_rl.allows(time_since_start, &(), tokens), + LimitKind::Ip(ip_addr) => { + if let Some(limiter) = self.ip_rl.as_mut() { + limiter.allows(time_since_start, ip_addr, tokens) + } else { + Ok(()) + } + } + LimitKind::NodeId(node_id) => { + if let Some(limiter) = self.node_rl.as_mut() { + limiter.allows(time_since_start, node_id, tokens) + } else { + Ok(()) + } + } + } + } + + /// Returns the expected total requests per second. + pub fn total_requests_per_second(&self) -> f32 { + self.total_requests_per_second + } + + /// Prunes excess entries. Should be called regularly (30 seconds) to remove old entries. + pub fn prune(&mut self) { + let time_since_start = self.init_time.elapsed(); + self.total_rl.prune(time_since_start); + if let Some(v) = self.ip_rl.as_mut() { + v.prune(time_since_start) + }; + if let Some(v) = self.node_rl.as_mut() { + v.prune(time_since_start) + }; + } +} + +/// Per key rate limiter using the token bucket / leaky bucket as a meter rate limiting algorithm, +/// with the GCRA implementation. +#[derive(Debug, Clone)] +pub struct Limiter { + /// After how long is the bucket considered full via replenishing 1T every `t`. + tau: Nanosecs, + /// How often is 1T replenished. + t: Nanosecs, + /// Time when the bucket will be full for each peer. TAT (theoretical arrival time) from GCRA. + tat_per_key: FnvHashMap, +} + +impl Limiter { + pub fn from_quota(quota: Quota) -> Result { + if quota.max_tokens == 0 { + return Err("Max number of tokens should be positive"); + } + let tau = quota.replenish_all_every.as_nanos(); + if tau == 0 { + return Err("Replenish time must be positive"); + } + let t = (tau / quota.max_tokens as u128) + .try_into() + .map_err(|_| "total replenish time is too long")?; + let tau = tau + .try_into() + .map_err(|_| "total replenish time is too long")?; + Ok(Limiter { + tau, + t, + tat_per_key: FnvHashMap::default(), + }) + } + + pub fn allows( + &mut self, + time_since_start: Duration, + key: &Key, + tokens: u64, + ) -> Result<(), RateLimitedErr> { + let time_since_start = time_since_start.as_nanos() as u64; + let tau = self.tau; + let t = self.t; + // how long does it take to replenish these tokens + let additional_time = t * tokens; + if additional_time > tau { + // the time required to process this amount of tokens is longer than the time that + // makes the bucket full. So, this batch can _never_ be processed + return Err(RateLimitedErr::TooLarge); + } + // If the key is new, we consider their bucket full (which means, their request will be + // allowed) + let tat = self + .tat_per_key + .entry(key.clone()) + .or_insert(time_since_start); + // check how soon could the request be made + let earliest_time = (*tat + additional_time).saturating_sub(tau); + // earliest_time is in the future + if time_since_start < earliest_time { + Err(RateLimitedErr::TooSoon(Duration::from_nanos( + /* time they need to wait, i.e. how soon were they */ + earliest_time - time_since_start, + ))) + } else { + // calculate the new TAT + *tat = time_since_start.max(*tat) + additional_time; + Ok(()) + } + } + + /// Removes keys for which their bucket is full by `time_limit` + pub fn prune(&mut self, time_limit: Duration) { + let lim = &mut (time_limit.as_nanos() as u64); + // remove those for which tat < lim + self.tat_per_key.retain(|_k, tat| tat >= lim) + } +} + +#[cfg(test)] +mod tests { + use super::{Limiter, Quota}; + use std::time::Duration; + + #[test] + fn it_works_a() { + let mut limiter = Limiter::from_quota(Quota { + replenish_all_every: Duration::from_secs(2), + max_tokens: 4, + }) + .unwrap(); + let key = 10; + // x + // used x + // tokens x x + // x x x x + // +--+--+--+--+----> seconds + // | | | | | + // 0 1 2 + + assert!(limiter + .allows(Duration::from_secs_f32(0.0), &key, 4) + .is_ok()); + limiter.prune(Duration::from_secs_f32(0.1)); + assert!(limiter + .allows(Duration::from_secs_f32(0.1), &key, 1) + .is_err()); + assert!(limiter + .allows(Duration::from_secs_f32(0.5), &key, 1) + .is_ok()); + assert!(limiter + .allows(Duration::from_secs_f32(1.0), &key, 1) + .is_ok()); + assert!(limiter + .allows(Duration::from_secs_f32(1.4), &key, 1) + .is_err()); + assert!(limiter + .allows(Duration::from_secs_f32(2.0), &key, 2) + .is_ok()); + } + + #[test] + fn it_works_b() { + let mut limiter = Limiter::from_quota(Quota { + replenish_all_every: Duration::from_secs(2), + max_tokens: 4, + }) + .unwrap(); + let key = 10; + // if we limit to 4T per 2s, check that 4 requests worth 1 token can be sent before the + // first half second, when one token will be available again. Check also that before + // regaining a token, another request is rejected + + assert!(limiter + .allows(Duration::from_secs_f32(0.0), &key, 1) + .is_ok()); + assert!(limiter + .allows(Duration::from_secs_f32(0.1), &key, 1) + .is_ok()); + assert!(limiter + .allows(Duration::from_secs_f32(0.2), &key, 1) + .is_ok()); + assert!(limiter + .allows(Duration::from_secs_f32(0.3), &key, 1) + .is_ok()); + assert!(limiter + .allows(Duration::from_secs_f32(0.4), &key, 1) + .is_err()); + } +} diff --git a/version-meld/discv5/src/socket/mod.rs b/version-meld/discv5/src/socket/mod.rs new file mode 100644 index 0000000..307e761 --- /dev/null +++ b/version-meld/discv5/src/socket/mod.rs @@ -0,0 +1,95 @@ +use crate::Executor; +use parking_lot::RwLock; +use recv::*; +use send::*; +use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration}; +use tokio::sync::{mpsc, oneshot}; + +mod filter; +mod recv; +mod send; + +pub use filter::{ + rate_limiter::{RateLimiter, RateLimiterBuilder}, + FilterConfig, +}; +pub use recv::InboundPacket; +pub use send::OutboundPacket; +/// Convenience objects for setting up the recv handler. +pub struct SocketConfig { + /// The executor to spawn the tasks. + pub executor: Box, + /// The listening socket. + pub socket_addr: SocketAddr, + /// Configuration details for the packet filter. + pub filter_config: FilterConfig, + /// If the filter is enabled this sets the default timeout for bans enacted by the filter. + pub ban_duration: Option, + /// The expected responses reference. + pub expected_responses: Arc>>, + /// The local node id used to decrypt messages. + pub local_node_id: enr::NodeId, +} + +/// Creates the UDP socket and handles the exit futures for the send/recv UDP handlers. +pub struct Socket { + pub send: mpsc::Sender, + pub recv: mpsc::Receiver, + sender_exit: Option>, + recv_exit: Option>, +} + +impl Socket { + /// This creates and binds a new UDP socket. + // In general this function can be expanded to handle more advanced socket creation. + pub(crate) async fn new_socket( + socket: &SocketAddr, + ) -> Result { + tokio::net::UdpSocket::bind(socket).await + } + + /// Creates a UDP socket, spawns a send/recv task and returns the channels. + /// If this struct is dropped, the send/recv tasks will shutdown. + /// This needs to be run inside of a tokio executor. + pub(crate) fn new( + socket: tokio::net::UdpSocket, + config: SocketConfig, + ) -> Result { + // Arc the udp socket for the send/recv tasks. + let recv_udp = Arc::new(socket); + let send_udp = recv_udp.clone(); + + // spawn the recv handler + let recv_config = RecvHandlerConfig { + filter_config: config.filter_config, + executor: config.executor.clone(), + recv: recv_udp, + local_node_id: config.local_node_id, + expected_responses: config.expected_responses, + ban_duration: config.ban_duration, + }; + + let (recv, recv_exit) = RecvHandler::spawn(recv_config); + // spawn the sender handler + let (send, sender_exit) = SendHandler::spawn(config.executor.clone(), send_udp); + + Ok(Socket { + send, + recv, + sender_exit: Some(sender_exit), + recv_exit: Some(recv_exit), + }) + } +} + +impl std::ops::Drop for Socket { + // close the send/recv handlers + fn drop(&mut self) { + let _ = self + .sender_exit + .take() + .expect("Exit always exists") + .send(()); + let _ = self.recv_exit.take().expect("Exit always exists").send(()); + } +} diff --git a/version-meld/discv5/src/socket/recv.rs b/version-meld/discv5/src/socket/recv.rs new file mode 100644 index 0000000..b1ba24a --- /dev/null +++ b/version-meld/discv5/src/socket/recv.rs @@ -0,0 +1,160 @@ +//! This is a standalone task that handles UDP packets as they are received. +//! +//! Every UDP packet passes a filter before being processed. + +use super::filter::{Filter, FilterConfig}; +use crate::{metrics::METRICS, node_info::NodeAddress, packet::*, Executor}; +use parking_lot::RwLock; +use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration}; +use tokio::{ + net::UdpSocket, + sync::{mpsc, oneshot}, +}; + +use tracing::{debug, trace, warn}; + +/// The object sent back by the Recv handler. +pub struct InboundPacket { + /// The originating socket addr. + pub src_address: SocketAddr, + /// The packet header. + pub header: PacketHeader, + /// The message of the packet. + pub message: Vec, + /// The authenticated data of the packet. + pub authenticated_data: Vec, +} + +/// Convenience objects for setting up the recv handler. +pub struct RecvHandlerConfig { + pub filter_config: FilterConfig, + /// If the filter is enabled this sets the default timeout for bans enacted by the filter. + pub ban_duration: Option, + pub executor: Box, + pub recv: Arc, + pub local_node_id: enr::NodeId, + pub expected_responses: Arc>>, +} + +/// The main task that handles inbound UDP packets. +pub(crate) struct RecvHandler { + /// The UDP recv socket. + recv: Arc, + /// The list of waiting responses. These are used to allow incoming packets from sources + /// that we are expected a response from bypassing the rate-limit filters. + expected_responses: Arc>>, + /// The packet filter which decides whether to accept or reject inbound packets. + filter: Filter, + /// The buffer to accept inbound datagrams. + recv_buffer: [u8; MAX_PACKET_SIZE], + /// The local node id used to decrypt headers of messages. + node_id: enr::NodeId, + /// The channel to send the packet handler. + handler: mpsc::Sender, + /// Exit channel to shutdown the recv handler. + exit: oneshot::Receiver<()>, +} + +impl RecvHandler { + /// Spawns the `RecvHandler` on a provided executor. + pub(crate) fn spawn( + config: RecvHandlerConfig, + ) -> (mpsc::Receiver, oneshot::Sender<()>) { + let (exit_sender, exit) = oneshot::channel(); + + let filter_enabled = config.filter_config.enabled; + + // create the channel to send decoded packets to the handler + let (handler, handler_recv) = mpsc::channel(30); + + let mut recv_handler = RecvHandler { + recv: config.recv, + filter: Filter::new(config.filter_config, config.ban_duration), + recv_buffer: [0; MAX_PACKET_SIZE], + node_id: config.local_node_id, + expected_responses: config.expected_responses, + handler, + exit, + }; + + // start the handler + config.executor.spawn(Box::pin(async move { + debug!("Recv handler starting"); + recv_handler.start(filter_enabled).await; + })); + (handler_recv, exit_sender) + } + + /// The main future driving the recv handler. This will shutdown when the exit future is fired. + async fn start(&mut self, filter_enabled: bool) { + // Interval to prune to rate limiter. + let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(30)); + + loop { + tokio::select! { + Ok((length, src)) = self.recv.recv_from(&mut self.recv_buffer) => { + METRICS.add_recv_bytes(length); + self.handle_inbound(src, length).await; + } + _ = interval.tick(), if filter_enabled => { + self.filter.prune_limiter(); + }, + _ = &mut self.exit => { + debug!("Recv handler shutdown"); + return; + } + } + } + } + + /// Handles in incoming packet. Passes through the filter, decodes and sends to the packet + /// handler. + async fn handle_inbound(&mut self, src_address: SocketAddr, length: usize) { + // Permit all expected responses + let permitted = self.expected_responses.read().get(&src_address).is_some(); + + // Perform the first run of the filter. This checks for rate limits and black listed IP + // addresses. + if !permitted && !self.filter.initial_pass(&src_address) { + trace!("Packet filtered from source: {:?}", src_address); + return; + } + // Decodes the packet + let (packet, authenticated_data) = + match Packet::decode(&self.node_id, &self.recv_buffer[..length]) { + Ok(p) => p, + Err(e) => { + debug!("Packet decoding failed: {:?}", e); // could not decode the packet, drop it + return; + } + }; + + // If this is not a challenge packet, we immediately know its src_id and so pass it + // through the second filter. + if let Some(node_id) = packet.src_id() { + // Construct the node address + let node_address = NodeAddress { + socket_addr: src_address, + node_id, + }; + + // Perform packet-level filtering + if !permitted && !self.filter.final_pass(&node_address, &packet) { + return; + } + } + + let inbound = InboundPacket { + src_address, + header: packet.header, + message: packet.message, + authenticated_data, + }; + + // send the filtered decoded packet to the handler. + self.handler + .send(inbound) + .await + .unwrap_or_else(|e| warn!("Could not send packet to handler: {}", e)); + } +} diff --git a/version-meld/discv5/src/socket/send.rs b/version-meld/discv5/src/socket/send.rs new file mode 100644 index 0000000..ce43729 --- /dev/null +++ b/version-meld/discv5/src/socket/send.rs @@ -0,0 +1,74 @@ +//! This is a standalone task that encodes and sends Discv5 UDP packets +use crate::{metrics::METRICS, node_info::NodeAddress, packet::*, Executor}; +use std::sync::Arc; +use tokio::{ + net::UdpSocket, + sync::{mpsc, oneshot}, +}; +use tracing::{debug, trace, warn}; + +pub struct OutboundPacket { + /// The destination node address + pub node_address: NodeAddress, + /// The packet to be encoded. + pub packet: Packet, +} + +/// The main task that handles outbound UDP packets. +pub(crate) struct SendHandler { + /// The UDP send socket. + send: Arc, + /// The channel to respond to send requests. + handler_recv: mpsc::Receiver, + /// Exit channel to shutdown the handler. + exit: oneshot::Receiver<()>, +} + +impl SendHandler { + /// Spawns the `SendHandler` on a provided executor. + /// This returns the sending channel to process `OutboundPacket`'s and an exit channel to + /// shutdown the handler. + pub(crate) fn spawn( + executor: Box, + send: Arc, + ) -> (mpsc::Sender, oneshot::Sender<()>) { + let (exit_send, exit) = oneshot::channel(); + let (handler_send, handler_recv) = mpsc::channel(30); + + let mut send_handler = SendHandler { + send, + handler_recv, + exit, + }; + + // start the handler + executor.spawn(Box::pin(async move { + debug!("Send handler starting"); + send_handler.start().await; + })); + (handler_send, exit_send) + } + + /// The main future driving the send handler. This will shutdown when the exit future is fired. + async fn start(&mut self) { + loop { + tokio::select! { + Some(packet) = self.handler_recv.recv() => { + let encoded_packet = packet.packet.encode(&packet.node_address.node_id); + if encoded_packet.len() > MAX_PACKET_SIZE { + warn!("Sending packet larger than max size: {} max: {}", encoded_packet.len(), MAX_PACKET_SIZE); + } + if let Err(e) = self.send.send_to(&encoded_packet, &packet.node_address.socket_addr).await { + trace!("Could not send packet. Error: {:?}", e); + } else { + METRICS.add_sent_bytes(encoded_packet.len()); + } + } + _ = &mut self.exit => { + debug!("Send handler shutdown"); + return; + } + } + } + } +} diff --git a/version-meld/enr/.cargo-ok b/version-meld/enr/.cargo-ok new file mode 100644 index 0000000..b5754e2 --- /dev/null +++ b/version-meld/enr/.cargo-ok @@ -0,0 +1 @@ +ok \ No newline at end of file diff --git a/version-meld/enr/.cargo_vcs_info.json b/version-meld/enr/.cargo_vcs_info.json new file mode 100644 index 0000000..dd50e4a --- /dev/null +++ b/version-meld/enr/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "b09b66ee006e6d16cd0371f9e5ea3a4f14bda8db" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/version-meld/enr/Cargo.toml b/version-meld/enr/Cargo.toml new file mode 100644 index 0000000..ae16b1f --- /dev/null +++ b/version-meld/enr/Cargo.toml @@ -0,0 +1,101 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2018" +name = "enr" +version = "0.6.2" +authors = ["Age Manning "] +exclude = [ + ".gitignore", + ".github/*", +] +description = "Rust implementation of Ethereum Node Record (ENR) EIP778" +readme = "./README.md" +keywords = [ + "ethereum", + "enr", + "record", + "EIP778", + "node", +] +categories = ["cryptography::cryptocurrencies"] +license = "MIT" +repository = "https://github.com/sigp/enr" + +[package.metadata.docs.rs] +all-features = true + +[lib] +name = "enr" +path = "src/lib.rs" + +[dependencies.base64] +version = "0.13" + +[dependencies.bs58] +version = "0.4" + +[dependencies.bytes] +version = "1" + +[dependencies.ed25519-dalek] +version = "1.0.0-pre.4" +optional = true + +[dependencies.hex] +version = "0.4.2" + +[dependencies.k256] +version = "0.11" +features = ["ecdsa"] +optional = true + +[dependencies.log] +version = "0.4.8" + +[dependencies.rand] +version = "0.8" + +[dependencies.rlp] +version = "0.5" + +[dependencies.secp256k1] +version = "0.24" +features = ["global-context"] +optional = true +default-features = false + +[dependencies.serde] +version = "1.0.110" +optional = true + +[dependencies.sha3] +version = "0.10" + +[dependencies.zeroize] +version = "1.1.0" + +[dev-dependencies.rand_07] +version = "0.7" +package = "rand" + +[dev-dependencies.secp256k1] +version = "0.24" +features = ["rand-std"] + +[features] +default = [ + "serde", + "k256", +] +ed25519 = ["ed25519-dalek"] +rust-secp256k1 = ["secp256k1"] diff --git a/version-meld/enr/Cargo.toml.orig b/version-meld/enr/Cargo.toml.orig new file mode 100644 index 0000000..3870448 --- /dev/null +++ b/version-meld/enr/Cargo.toml.orig @@ -0,0 +1,45 @@ +[package] +name = "enr" +authors = ["Age Manning "] +edition = "2018" +version = "0.6.2" +description = "Rust implementation of Ethereum Node Record (ENR) EIP778" +readme = "./README.md" +keywords = ["ethereum", "enr", "record", "EIP778", "node"] +repository = "https://github.com/sigp/enr" +categories = ["cryptography::cryptocurrencies"] +license = "MIT" +exclude = [".gitignore", ".github/*"] + +[dependencies] +base64 = "0.13" +bs58 = "0.4" +bytes = "1" +hex = "0.4.2" +log = "0.4.8" +rand = "0.8" +rlp = "0.5" +zeroize = "1.1.0" +sha3 = "0.10" +k256 = { version = "0.11", features = ["ecdsa"], optional = true } +serde = { version = "1.0.110", optional = true } +ed25519-dalek = { version = "1.0.0-pre.4", optional = true } +secp256k1 = { version = "0.24", optional = true, default-features = false, features = [ + "global-context", +] } + +[dev-dependencies] +rand_07 = { package = "rand", version = "0.7" } +secp256k1 = { features = ["rand-std"], version = "0.24" } + +[features] +default = ["serde", "k256"] +ed25519 = ["ed25519-dalek"] +rust-secp256k1 = ["secp256k1"] + +[lib] +name = "enr" +path = "src/lib.rs" + +[package.metadata.docs.rs] +all-features = true diff --git a/version-meld/enr/LICENSE b/version-meld/enr/LICENSE new file mode 100644 index 0000000..bd1c2c2 --- /dev/null +++ b/version-meld/enr/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Age Manning + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/version-meld/enr/README.md b/version-meld/enr/README.md new file mode 100644 index 0000000..efc017a --- /dev/null +++ b/version-meld/enr/README.md @@ -0,0 +1,170 @@ +enr +============ + +[![Build Status]][Build Link] [![Doc Status]][Doc Link] [![Crates +Status]][Crates Link] + +[Build Status]: https://github.com/AgeManning/enr/workflows/build/badge.svg?branch=master +[Build Link]: https://github.com/AgeManning/enr/actions +[Doc Status]: https://docs.rs/enr/badge.svg +[Doc Link]: https://docs.rs/enr +[Crates Status]: https://img.shields.io/crates/v/enr.svg +[Crates Link]: https://crates.io/crates/enr + +[Documentation at docs.rs](https://docs.rs/enr) + +This crate contains an implementation of an Ethereum Node Record (ENR) as specified by +[EIP-778](https://eips.ethereum.org/EIPS/eip-778) extended to allow for the use of ed25519 keys. + +An ENR is a signed, key-value record which has an associated `NodeId` (a 32-byte identifier). +Updating/modifying an ENR requires an `EnrKey` in order to re-sign the record with the +associated key-pair. + +ENR's are identified by their sequence number. When updating an ENR, the sequence number is +increased. + +Different identity schemes can be used to define the node id and signatures. Currently only the +"v4" identity is supported and is set by default. + +## Signing Algorithms + +User's wishing to implement their own singing algorithms simply need to +implement the `EnrKey` trait and apply it to an `Enr`. + +By default, `k256::SigningKey` implement `EnrKey` and can be used to sign and +verify ENR records. This library also implements `EnrKey` for `ed25519_dalek::Keypair` via the `ed25519` +feature flag. + +Furthermore, a `CombinedKey` is provided if the `ed25519` feature flag is set, which provides an +ENR type that can support both `secp256k1` and `ed25519` signed ENR records. Examples of the +use of each of these key types is given below. + +## Features + +This crate supports a number of features. + +- `serde`: Allows for serde serialization and deserialization for ENRs. +- `ed25519`: Provides support for `ed25519_dalek` keypair types. +- `rust-secp256k1`: Uses `c-secp256k1` for secp256k1 keys. + +These can be enabled via adding the feature flag in your `Cargo.toml` + +```toml +enr = { version = "*", features = ["serde", "ed25519", "rust-secp256k1"] } +``` + +## Examples + +To build an ENR, an `EnrBuilder` is provided. + +#### Building an ENR with the default `k256` key type + +```rust +use enr::{EnrBuilder, k256}; +use std::net::Ipv4Addr; +use rand::thread_rng; + +// generate a random secp256k1 key +let mut rng = thread_rng(); +let key = k256::ecdsa::SigningKey::random(&mut rng); + +let ip = Ipv4Addr::new(192,168,0,1); +let enr = EnrBuilder::new("v4").ip4(ip).tcp4(8000).build(&key).unwrap(); + +assert_eq!(enr.ip4(), Some("192.168.0.1".parse().unwrap())); +assert_eq!(enr.id(), Some("v4".into())); +``` + +#### Building an ENR with the `CombinedKey` type (support for multiple signing algorithms). + +Note the `ed25519` feature flag must be set. This makes use of the +`EnrBuilder` struct. + +```rust +use enr::{EnrBuilder, CombinedKey}; +use std::net::Ipv4Addr; + +// create a new secp256k1 key +let key = CombinedKey::generate_secp256k1(); + +// or create a new ed25519 key +let key = CombinedKey::generate_ed25519(); + +let ip = Ipv4Addr::new(192,168,0,1); +let enr = EnrBuilder::new("v4").ip4(ip).tcp4(8000).build(&key).unwrap(); + +assert_eq!(enr.ip4(), Some("192.168.0.1".parse().unwrap())); +assert_eq!(enr.id(), Some("v4".into())); +``` + +#### Modifying an ENR + +ENR fields can be added and modified using the getters/setters on `Enr`. A custom field +can be added using `insert` and retrieved with `get`. + +```rust +use enr::{EnrBuilder, k256::ecdsa::SigningKey, Enr}; +use std::net::Ipv4Addr; +use rand::thread_rng; + +// specify the type of ENR +type DefaultEnr = Enr; + +// generate a random secp256k1 key +let mut rng = thread_rng(); +let key = SigningKey::random(&mut rng); + +let ip = Ipv4Addr::new(192,168,0,1); +let mut enr = EnrBuilder::new("v4").ip4(ip).tcp4(8000).build(&key).unwrap(); + +enr.set_tcp4(8001, &key); +// set a custom key +enr.insert("custom_key", &vec![0,0,1], &key); + +// encode to base64 +let base_64_string = enr.to_base64(); + +// decode from base64 +let decoded_enr: DefaultEnr = base_64_string.parse().unwrap(); + +assert_eq!(decoded_enr.ip4(), Some("192.168.0.1".parse().unwrap())); +assert_eq!(decoded_enr.id(), Some("v4".into())); +assert_eq!(decoded_enr.tcp4(), Some(8001)); +assert_eq!(decoded_enr.get("custom_key"), Some(vec![0,0,1].as_slice())); +``` + +#### Encoding/Decoding ENR's of various key types + +```rust +use enr::{EnrBuilder, k256::ecdsa::SigningKey, Enr, ed25519_dalek::Keypair, CombinedKey}; +use std::net::Ipv4Addr; +use rand::thread_rng; +use rand::Rng; + +// generate a random secp256k1 key +let mut rng = thread_rng(); +let key = SigningKey::random(&mut rng); +let ip = Ipv4Addr::new(192,168,0,1); +let enr_secp256k1 = EnrBuilder::new("v4").ip4(ip).tcp4(8000).build(&key).unwrap(); + +// encode to base64 +let base64_string_secp256k1 = enr_secp256k1.to_base64(); + +// generate a random ed25519 key +let mut rng = rand_07::thread_rng(); +let key = Keypair::generate(&mut rng); +let enr_ed25519 = EnrBuilder::new("v4").ip4(ip).tcp4(8000).build(&key).unwrap(); + +// encode to base64 +let base64_string_ed25519 = enr_ed25519.to_base64(); + +// decode base64 strings of varying key types +// decode the secp256k1 with default Enr +let decoded_enr_secp256k1: Enr = base64_string_secp256k1.parse().unwrap(); +// decode ed25519 ENRs +let decoded_enr_ed25519: Enr = base64_string_ed25519.parse().unwrap(); + +// use the combined key to be able to decode either +let decoded_enr: Enr = base64_string_secp256k1.parse().unwrap(); +let decoded_enr: Enr = base64_string_ed25519.parse().unwrap(); +``` diff --git a/version-meld/enr/src/builder.rs b/version-meld/enr/src/builder.rs new file mode 100644 index 0000000..aee3d61 --- /dev/null +++ b/version-meld/enr/src/builder.rs @@ -0,0 +1,187 @@ +use crate::{Enr, EnrError, EnrKey, EnrPublicKey, Key, NodeId, MAX_ENR_SIZE}; +use bytes::{Bytes, BytesMut}; +use rlp::RlpStream; +use std::{ + collections::BTreeMap, + marker::PhantomData, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, +}; + +///! The base builder for generating ENR records with arbitrary signing algorithms. +pub struct EnrBuilder { + /// The identity scheme used to build the ENR record. + id: String, + + /// The starting sequence number for the ENR record. + seq: u64, + + /// The key-value pairs for the ENR record. + /// Values are stored as RLP encoded bytes. + content: BTreeMap, + + /// Pins the generic key types. + phantom: PhantomData, +} + +impl EnrBuilder { + /// Constructs a minimal `EnrBuilder` providing only a sequence number. + /// Currently only supports the id v4 scheme and therefore disallows creation of any other + /// scheme. + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + seq: 1, + content: BTreeMap::new(), + phantom: PhantomData, + } + } + + /// Modifies the sequence number of the builder. + pub fn seq(&mut self, seq: u64) -> &mut Self { + self.seq = seq; + self + } + + /// Adds an arbitrary key-value to the `ENRBuilder`. + pub fn add_value(&mut self, key: impl AsRef<[u8]>, value: &[u8]) -> &mut Self { + self.add_value_rlp(key, rlp::encode(&value).freeze()) + } + + /// Adds an arbitrary key-value where the value is raw RLP encoded bytes. + pub fn add_value_rlp(&mut self, key: impl AsRef<[u8]>, rlp: Bytes) -> &mut Self { + self.content.insert(key.as_ref().to_vec(), rlp); + self + } + + /// Adds an `ip`/`ip6` field to the `ENRBuilder`. + pub fn ip(&mut self, ip: IpAddr) -> &mut Self { + match ip { + IpAddr::V4(addr) => self.ip4(addr), + IpAddr::V6(addr) => self.ip6(addr), + } + } + + /// Adds an `ip` field to the `ENRBuilder`. + pub fn ip4(&mut self, ip: Ipv4Addr) -> &mut Self { + self.add_value("ip", &ip.octets()); + self + } + + /// Adds an `ip6` field to the `ENRBuilder`. + pub fn ip6(&mut self, ip: Ipv6Addr) -> &mut Self { + self.add_value("ip6", &ip.octets()); + self + } + + /* + * Removed from the builder as only the v4 scheme is currently supported. + * This is set as default in the builder. + + /// Adds an `Id` field to the `ENRBuilder`. + pub fn id(&mut self, id: &str) -> &mut Self { + self.add_value("id", &id.as_bytes()); + self + } + */ + + pub fn tcp(&mut self, tcp: u16) -> &mut Self { + self.tcp4(tcp) + } + + /// Adds a `tcp` field to the `ENRBuilder`. + pub fn tcp4(&mut self, tcp: u16) -> &mut Self { + self.add_value("tcp", &tcp.to_be_bytes()); + self + } + + /// Adds a `tcp6` field to the `ENRBuilder`. + pub fn tcp6(&mut self, tcp: u16) -> &mut Self { + self.add_value("tcp6", &tcp.to_be_bytes()); + self + } + + pub fn udp(&mut self, udp: u16) -> &mut Self { + self.udp4(udp) + } + + /// Adds a `udp` field to the `ENRBuilder`. + pub fn udp4(&mut self, udp: u16) -> &mut Self { + self.add_value("udp", &udp.to_be_bytes()); + self + } + + /// Adds a `udp6` field to the `ENRBuilder`. + pub fn udp6(&mut self, udp: u16) -> &mut Self { + self.add_value("udp6", &udp.to_be_bytes()); + self + } + + /// Generates the rlp-encoded form of the ENR specified by the builder config. + fn rlp_content(&self) -> BytesMut { + let mut stream = RlpStream::new_with_buffer(BytesMut::with_capacity(MAX_ENR_SIZE)); + stream.begin_list(self.content.len() * 2 + 1); + stream.append(&self.seq); + for (k, v) in &self.content { + stream.append(k); + // The values are stored as raw RLP encoded bytes + stream.append_raw(v, 1); + } + stream.out() + } + + /// Signs record based on the identity scheme. Currently only "v4" is supported. + fn signature(&self, key: &K) -> Result, EnrError> { + match self.id.as_str() { + "v4" => key + .sign_v4(&self.rlp_content()) + .map_err(|_| EnrError::SigningError), + // unsupported identity schemes + _ => Err(EnrError::SigningError), + } + } + + /// Adds a public key to the ENR builder. + fn add_public_key(&mut self, key: &K::PublicKey) { + self.add_value(key.enr_key(), key.encode().as_ref()); + } + + /// Constructs an ENR from the `EnrBuilder`. + /// + /// # Errors + /// Fails if the identity scheme is not supported, or the record size exceeds `MAX_ENR_SIZE`. + pub fn build(&mut self, key: &K) -> Result, EnrError> { + // add the identity scheme to the content + if self.id != "v4" { + return Err(EnrError::UnsupportedIdentityScheme); + } + + // Sanitize all data, ensuring all RLP data is correctly formatted. + for (key, value) in &self.content { + if rlp::Rlp::new(value).data().is_err() { + return Err(EnrError::InvalidRlpData( + String::from_utf8_lossy(key).into(), + )); + } + } + + self.add_value_rlp("id", rlp::encode(&self.id.as_bytes()).freeze()); + + self.add_public_key(&key.public()); + let rlp_content = self.rlp_content(); + + let signature = self.signature(key)?; + + // check the size of the record + if rlp_content.len() + signature.len() + 8 > MAX_ENR_SIZE { + return Err(EnrError::ExceedsMaxSize); + } + + Ok(Enr { + seq: self.seq, + node_id: NodeId::from(key.public()), + content: self.content.clone(), + signature, + phantom: PhantomData, + }) + } +} diff --git a/version-meld/enr/src/error.rs b/version-meld/enr/src/error.rs new file mode 100644 index 0000000..55281c9 --- /dev/null +++ b/version-meld/enr/src/error.rs @@ -0,0 +1,33 @@ +//! The error type emitted for various ENR operations. + +use std::error::Error; +use std::fmt; + +#[derive(Clone, Debug)] +/// An error type for handling various ENR operations. +pub enum EnrError { + /// The ENR is too large. + ExceedsMaxSize, + /// The sequence number is too large. + SequenceNumberTooHigh, + /// There was an error with signing an ENR record. + SigningError, + /// The identity scheme is not supported. + UnsupportedIdentityScheme, + /// The entered RLP data is invalid. + InvalidRlpData(String), +} + +impl fmt::Display for EnrError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ExceedsMaxSize => write!(f, "enr exceeds max size"), + Self::SequenceNumberTooHigh => write!(f, "sequence number too large"), + Self::SigningError => write!(f, "signing error"), + Self::UnsupportedIdentityScheme => write!(f, "unsupported identity scheme"), + Self::InvalidRlpData(_rlp) => write!(f, "invalid rlp data"), + } + } +} + +impl Error for EnrError {} diff --git a/version-meld/enr/src/keys/combined.rs b/version-meld/enr/src/keys/combined.rs new file mode 100644 index 0000000..d69e654 --- /dev/null +++ b/version-meld/enr/src/keys/combined.rs @@ -0,0 +1,184 @@ +//! An implementation that combines the currently supported key types. This +//! facilitates and ENR type than can decode and read ENR's of all supported key types. +//! +//! Currently only `secp256k1` and `ed25519` key types are supported. + +use super::{ed25519_dalek as ed25519, EnrKey, EnrPublicKey, SigningError}; +use bytes::Bytes; +pub use k256; +use rand::RngCore; +use rlp::DecoderError; +use std::collections::BTreeMap; +use zeroize::Zeroize; + +use crate::Key; + +/// A standard implementation of the `EnrKey` trait used to sign and modify ENR records. The variants here represent the currently +/// supported in-built signing schemes. +pub enum CombinedKey { + /// An `secp256k1` keypair. + Secp256k1(k256::ecdsa::SigningKey), + /// An `Ed25519` keypair. + Ed25519(ed25519::Keypair), +} + +impl From for CombinedKey { + fn from(secret_key: k256::ecdsa::SigningKey) -> Self { + Self::Secp256k1(secret_key) + } +} + +impl From for CombinedKey { + fn from(keypair: ed25519_dalek::Keypair) -> Self { + Self::Ed25519(keypair) + } +} + +/// Promote an Ed25519 secret key into a keypair. +impl From for CombinedKey { + fn from(secret: ed25519::SecretKey) -> Self { + let public = ed25519::PublicKey::from(&secret); + Self::Ed25519(ed25519::Keypair { secret, public }) + } +} + +impl EnrKey for CombinedKey { + type PublicKey = CombinedPublicKey; + + /// Performs ENR-specific signing. + /// + /// Note: that this library supports a number of signing algorithms. The ENR specification + /// currently lists the `v4` identity scheme which requires the `secp256k1` signing algorithm. + /// Using `secp256k1` keys follow the `v4` identity scheme, using other types do not, although + /// they are supported. + fn sign_v4(&self, msg: &[u8]) -> Result, SigningError> { + match self { + Self::Secp256k1(ref key) => key.sign_v4(msg), + Self::Ed25519(ref key) => key.sign_v4(msg), + } + } + + /// Returns the public key associated with the private key. + fn public(&self) -> Self::PublicKey { + match self { + Self::Secp256k1(key) => CombinedPublicKey::from(key.public()), + Self::Ed25519(key) => CombinedPublicKey::from(key.public()), + } + } + + /// Decodes the raw bytes of an ENR's content into a public key if possible. + fn enr_to_public(content: &BTreeMap) -> Result { + k256::ecdsa::SigningKey::enr_to_public(content) + .map(CombinedPublicKey::Secp256k1) + .or_else(|_| ed25519::Keypair::enr_to_public(content).map(CombinedPublicKey::from)) + } +} + +impl CombinedKey { + /// Generates a new secp256k1 key. + #[must_use] + pub fn generate_secp256k1() -> Self { + let key = k256::ecdsa::SigningKey::random(rand::thread_rng()); + Self::Secp256k1(key) + } + + /// Generates a new ed25510 key. + #[must_use] + pub fn generate_ed25519() -> Self { + let mut bytes = [0_u8; 32]; + rand::thread_rng().fill_bytes(&mut bytes); + let key = + Self::from(ed25519::SecretKey::from_bytes(&bytes).expect( + "this returns `Err` only if the length is wrong; the length is correct; qed", + )); + bytes.zeroize(); + key + } + + /// Imports a secp256k1 from raw bytes in any format. + pub fn secp256k1_from_bytes(bytes: &mut [u8]) -> Result { + let key = k256::ecdsa::SigningKey::from_bytes(bytes) + .map_err(|_| DecoderError::Custom("Invalid secp256k1 secret key")) + .map(Self::from)?; + bytes.zeroize(); + Ok(key) + } + + /// Imports an ed25519 key from raw 32 bytes. + pub fn ed25519_from_bytes(bytes: &mut [u8]) -> Result { + let key = ed25519::SecretKey::from_bytes(bytes) + .map_err(|_| DecoderError::Custom("Invalid ed25519 secret key")) + .map(Self::from)?; + bytes.zeroize(); + Ok(key) + } + + /// Encodes the `CombinedKey` into compressed (where possible) bytes. + #[must_use] + pub fn encode(&self) -> Vec { + match self { + Self::Secp256k1(key) => key.to_bytes().to_vec(), + Self::Ed25519(key) => key.secret.as_bytes().to_vec(), + } + } +} + +/// A combined implementation of `EnrPublicKey` which has support for `Secp256k1` +/// and `Ed25519` for ENR signature verification. +#[derive(Clone, Debug, PartialEq)] +pub enum CombinedPublicKey { + /// An `Secp256k1` public key. + Secp256k1(k256::ecdsa::VerifyingKey), + /// An `Ed25519` public key. + Ed25519(ed25519::PublicKey), +} + +impl From for CombinedPublicKey { + fn from(public_key: k256::ecdsa::VerifyingKey) -> Self { + Self::Secp256k1(public_key) + } +} + +impl From for CombinedPublicKey { + fn from(public_key: ed25519::PublicKey) -> Self { + Self::Ed25519(public_key) + } +} + +impl EnrPublicKey for CombinedPublicKey { + type Raw = Vec; + type RawUncompressed = Vec; + + /// Verify a raw message, given a public key for the v4 identity scheme. + fn verify_v4(&self, msg: &[u8], sig: &[u8]) -> bool { + match self { + Self::Secp256k1(pk) => pk.verify_v4(msg, sig), + Self::Ed25519(pk) => pk.verify_v4(msg, sig), + } + } + + /// Encodes the public key into compressed form, if possible. + fn encode(&self) -> Vec { + match self { + // serialize in compressed form: 33 bytes + Self::Secp256k1(pk) => pk.encode().to_vec(), + Self::Ed25519(pk) => pk.encode().to_vec(), + } + } + + /// Encodes the public key in uncompressed form. + fn encode_uncompressed(&self) -> Vec { + match self { + Self::Secp256k1(pk) => pk.encode_uncompressed().to_vec(), + Self::Ed25519(pk) => pk.encode_uncompressed().to_vec(), + } + } + + /// Generates the ENR public key string associated with the key type. + fn enr_key(&self) -> Key { + match self { + Self::Secp256k1(key) => key.enr_key(), + Self::Ed25519(key) => key.enr_key(), + } + } +} diff --git a/version-meld/enr/src/keys/ed25519.rs b/version-meld/enr/src/keys/ed25519.rs new file mode 100644 index 0000000..7531b71 --- /dev/null +++ b/version-meld/enr/src/keys/ed25519.rs @@ -0,0 +1,75 @@ +use super::{ + ed25519_dalek::{self as ed25519, Signer as _, Verifier as _}, + EnrKey, EnrKeyUnambiguous, EnrPublicKey, SigningError, +}; +use crate::Key; +use bytes::Bytes; +use rlp::DecoderError; +use std::{collections::BTreeMap, convert::TryFrom}; + +/// The ENR key that stores the public key in the ENR record. +pub const ENR_KEY: &str = "ed25519"; + +impl EnrKey for ed25519::Keypair { + type PublicKey = ed25519::PublicKey; + + /// Performs ENR-specific signing. + /// + /// Using `ed25519` keys do not currently follow the `v4` identity scheme, which dictates + /// `secp256k1` keys should be used. + fn sign_v4(&self, msg: &[u8]) -> Result, SigningError> { + Ok(self.sign(msg).to_bytes().to_vec()) + } + + /// Returns the public key associated with the private key. + fn public(&self) -> Self::PublicKey { + self.public + } + + /// Decodes the raw bytes of an ENR's content into a public key if possible. + fn enr_to_public(content: &BTreeMap) -> Result { + let pubkey_bytes = content + .get(ENR_KEY.as_bytes()) + .ok_or(DecoderError::Custom("Unknown signature"))?; + + // Decode the RLP + let pubkey_bytes = rlp::Rlp::new(pubkey_bytes).data()?; + + Self::decode_public(pubkey_bytes) + } +} + +impl EnrKeyUnambiguous for ed25519::Keypair { + fn decode_public(bytes: &[u8]) -> Result { + ed25519::PublicKey::from_bytes(bytes) + .map_err(|_| DecoderError::Custom("Invalid ed25519 Signature")) + } +} + +impl EnrPublicKey for ed25519::PublicKey { + type Raw = [u8; ed25519::PUBLIC_KEY_LENGTH]; + type RawUncompressed = [u8; ed25519::PUBLIC_KEY_LENGTH]; + + /// Verify a raw message, given a public key for the v4 identity scheme. + fn verify_v4(&self, msg: &[u8], sig: &[u8]) -> bool { + ed25519::Signature::try_from(sig) + .and_then(|s| self.verify(msg, &s)) + .is_ok() + } + + /// Encodes the public key into compressed form, if possible. + fn encode(&self) -> Self::Raw { + self.to_bytes() + } + + /// Encodes the public key in uncompressed form. This is the same as the compressed form for + /// ed25519 keys + fn encode_uncompressed(&self) -> Self::RawUncompressed { + self.encode() + } + + /// Generates the ENR public key string associated with the ed25519 key type. + fn enr_key(&self) -> Key { + ENR_KEY.into() + } +} diff --git a/version-meld/enr/src/keys/k256_key.rs b/version-meld/enr/src/keys/k256_key.rs new file mode 100644 index 0000000..53b38da --- /dev/null +++ b/version-meld/enr/src/keys/k256_key.rs @@ -0,0 +1,106 @@ +//! An implementation for `EnrKey` for `k256::ecdsa::SigningKey` + +use super::{EnrKey, EnrKeyUnambiguous, EnrPublicKey, SigningError}; +use crate::Key; +use bytes::Bytes; +use k256::{ + ecdsa::{ + signature::{DigestVerifier, RandomizedDigestSigner, Signature as _}, + Signature, SigningKey, VerifyingKey, + }, + elliptic_curve::{ + sec1::{Coordinates, ToEncodedPoint}, + subtle::Choice, + DecompressPoint, + }, + AffinePoint, CompressedPoint, EncodedPoint, +}; +use rand::rngs::OsRng; +use rlp::DecoderError; +use sha3::{Digest, Keccak256}; +use std::{collections::BTreeMap, convert::TryFrom}; + +/// The ENR key that stores the public key in the ENR record. +pub const ENR_KEY: &str = "secp256k1"; + +impl EnrKey for SigningKey { + type PublicKey = VerifyingKey; + + fn sign_v4(&self, msg: &[u8]) -> Result, SigningError> { + // take a keccak256 hash then sign. + let digest = Keccak256::new().chain_update(msg); + let signature: Signature = self + .try_sign_digest_with_rng(&mut OsRng, digest) + .map_err(|_| SigningError::new("failed to sign"))?; + + Ok(signature.as_bytes().to_vec()) + } + + fn public(&self) -> Self::PublicKey { + self.verifying_key() + } + + fn enr_to_public(content: &BTreeMap) -> Result { + let pubkey_bytes = content + .get(ENR_KEY.as_bytes()) + .ok_or(DecoderError::Custom("Unknown signature"))?; + + // Decode the RLP + let pubkey_bytes = rlp::Rlp::new(pubkey_bytes).data()?; + + Self::decode_public(pubkey_bytes) + } +} + +impl EnrKeyUnambiguous for SigningKey { + fn decode_public(bytes: &[u8]) -> Result { + // should be encoded in compressed form, i.e 33 byte raw secp256k1 public key + VerifyingKey::from_sec1_bytes(bytes) + .map_err(|_| DecoderError::Custom("Invalid Secp256k1 Signature")) + } +} + +impl EnrPublicKey for VerifyingKey { + type Raw = CompressedPoint; + type RawUncompressed = [u8; 64]; + + fn verify_v4(&self, msg: &[u8], sig: &[u8]) -> bool { + if let Ok(sig) = k256::ecdsa::Signature::try_from(sig) { + return self + .verify_digest(Keccak256::new().chain_update(msg), &sig) + .is_ok(); + } + false + } + + fn encode(&self) -> Self::Raw { + // serialize in compressed form: 33 bytes + self.to_bytes() + } + + fn encode_uncompressed(&self) -> Self::RawUncompressed { + let p = EncodedPoint::from(self); + let (x, y) = match p.coordinates() { + Coordinates::Compact { .. } | Coordinates::Identity => unreachable!(), + Coordinates::Compressed { x, y_is_odd } => ( + x, + *AffinePoint::decompress(x, Choice::from(u8::from(y_is_odd))) + .unwrap() + .to_encoded_point(false) + .y() + .unwrap(), + ), + Coordinates::Uncompressed { x, y } => (x, *y), + }; + + let mut coords = [0; 64]; + coords[..32].copy_from_slice(x); + coords[32..].copy_from_slice(&y); + + coords + } + + fn enr_key(&self) -> Key { + ENR_KEY.into() + } +} diff --git a/version-meld/enr/src/keys/mod.rs b/version-meld/enr/src/keys/mod.rs new file mode 100644 index 0000000..f54a77e --- /dev/null +++ b/version-meld/enr/src/keys/mod.rs @@ -0,0 +1,108 @@ +//! This module provides the [`EnrKey`] and [`EnrPublicKey`] traits. User's wishing to implement their +//! own signing schemes can implement these traits and apply them to a [`Enr`]. +//! +//! [`EnrKey`]: crate::EnrKey +//! [`EnrPublicKey`]: crate::EnrPublicKey +//! [`Enr`]: crate::Enr + +#[cfg(all(feature = "ed25519", feature = "k256"))] +mod combined; +#[cfg(feature = "ed25519")] +mod ed25519; +#[cfg(any(feature = "k256"))] +mod k256_key; +#[cfg(feature = "rust-secp256k1")] +mod rust_secp256k1; + +#[cfg(all(feature = "ed25519", feature = "k256"))] +pub use combined::{CombinedKey, CombinedPublicKey}; +#[cfg(feature = "ed25519")] +pub use ed25519_dalek; +#[cfg(any(feature = "k256"))] +pub use k256; +#[cfg(feature = "rust-secp256k1")] +pub use secp256k1; + +use crate::Key; +use bytes::Bytes; +use rlp::DecoderError; +use std::{ + collections::BTreeMap, + error::Error, + fmt::{self, Debug, Display}, +}; + +/// The trait required for a key to sign and modify an ENR record. +pub trait EnrKey: Send + Sync + Unpin + 'static { + type PublicKey: EnrPublicKey + Clone; + + /// Performs ENR-specific signing for the `v4` identity scheme. + fn sign_v4(&self, msg: &[u8]) -> Result, SigningError>; + + /// Returns the public key associated with current key pair. + fn public(&self) -> Self::PublicKey; + + /// Provides a method to decode a raw public key from an ENR `BTreeMap` to a useable public key. + /// + /// This method allows a key type to decode the raw bytes in an ENR to a useable + /// `EnrPublicKey`. It takes the ENR's `BTreeMap` and returns a public key. + /// + /// Note: This specifies the supported key schemes for an ENR. + fn enr_to_public(content: &BTreeMap) -> Result; +} + +/// Trait for keys that are uniquely represented +pub trait EnrKeyUnambiguous: EnrKey { + /// Decode raw bytes as corresponding public key. + fn decode_public(bytes: &[u8]) -> Result; +} + +/// The trait required for a `PublicKey` to verify an ENR record. +pub trait EnrPublicKey: Clone + Debug + Send + Sync + Unpin + 'static { + type Raw: AsRef<[u8]>; + type RawUncompressed: AsRef<[u8]>; + + /// Verify an ENR signature for the `v4` identity scheme. + fn verify_v4(&self, msg: &[u8], sig: &[u8]) -> bool; + + /// Encodes the public key to bytes in compressed form, if possible. + fn encode(&self) -> Self::Raw; + + /// Encodes the public key in uncompressed form. + // For compatible keys, encode in uncompressed form. Necessary for generating the node-id + fn encode_uncompressed(&self) -> Self::RawUncompressed; + + /// Returns the ENR key identifier for the public key type. For `secp256k1` keys this + /// is `secp256k1`. + fn enr_key(&self) -> Key; +} + +/// An error during signing of a message. +#[derive(Debug)] +pub struct SigningError { + msg: String, + source: Option>, +} + +/// An error during encoding of key material. +#[allow(dead_code)] +impl SigningError { + pub(crate) fn new(msg: S) -> Self { + Self { + msg: msg.to_string(), + source: None, + } + } +} + +impl fmt::Display for SigningError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Key signing error: {}", self.msg) + } +} + +impl Error for SigningError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + self.source.as_ref().map(|s| &**s as &dyn Error) + } +} diff --git a/version-meld/enr/src/keys/rust_secp256k1.rs b/version-meld/enr/src/keys/rust_secp256k1.rs new file mode 100644 index 0000000..28daa1a --- /dev/null +++ b/version-meld/enr/src/keys/rust_secp256k1.rs @@ -0,0 +1,73 @@ +use super::{EnrKey, EnrKeyUnambiguous, EnrPublicKey, SigningError}; +use crate::{digest, Key}; +use bytes::Bytes; +use rlp::DecoderError; +use secp256k1::SECP256K1; +use std::collections::BTreeMap; + +/// The ENR key that stores the public key in the ENR record. +pub const ENR_KEY: &str = "secp256k1"; + +impl EnrKey for secp256k1::SecretKey { + type PublicKey = secp256k1::PublicKey; + + fn sign_v4(&self, msg: &[u8]) -> Result, SigningError> { + // take a keccak256 hash then sign. + let hash = digest(msg); + let m = secp256k1::Message::from_slice(&hash) + .map_err(|_| SigningError::new("failed to parse secp256k1 digest"))?; + // serialize to an uncompressed 64 byte vector + Ok(SECP256K1.sign_ecdsa(&m, self).serialize_compact().to_vec()) + } + + fn public(&self) -> Self::PublicKey { + Self::PublicKey::from_secret_key(SECP256K1, self) + } + + fn enr_to_public(content: &BTreeMap) -> Result { + let pubkey_bytes = content + .get(ENR_KEY.as_bytes()) + .ok_or(DecoderError::Custom("Unknown signature"))?; + // Decode the RLP + let pubkey_bytes = rlp::Rlp::new(pubkey_bytes).data()?; + + Self::decode_public(pubkey_bytes) + } +} + +impl EnrKeyUnambiguous for secp256k1::SecretKey { + fn decode_public(bytes: &[u8]) -> Result { + // should be encoded in compressed form, i.e 33 byte raw secp256k1 public key + secp256k1::PublicKey::from_slice(bytes) + .map_err(|_| DecoderError::Custom("Invalid Secp256k1 Signature")) + } +} + +impl EnrPublicKey for secp256k1::PublicKey { + type Raw = [u8; secp256k1::constants::PUBLIC_KEY_SIZE]; + type RawUncompressed = [u8; secp256k1::constants::UNCOMPRESSED_PUBLIC_KEY_SIZE - 1]; + + fn verify_v4(&self, msg: &[u8], sig: &[u8]) -> bool { + let msg = digest(msg); + if let Ok(sig) = secp256k1::ecdsa::Signature::from_compact(sig) { + if let Ok(msg) = secp256k1::Message::from_slice(&msg) { + return SECP256K1.verify_ecdsa(&msg, &sig, self).is_ok(); + } + } + false + } + + fn encode(&self) -> Self::Raw { + self.serialize() + } + + fn encode_uncompressed(&self) -> Self::RawUncompressed { + let mut out = [0_u8; secp256k1::constants::UNCOMPRESSED_PUBLIC_KEY_SIZE - 1]; + out.copy_from_slice(&self.serialize_uncompressed()[1..]); + out + } + + fn enr_key(&self) -> Key { + ENR_KEY.into() + } +} diff --git a/version-meld/enr/src/lib.rs b/version-meld/enr/src/lib.rs new file mode 100644 index 0000000..7d258e4 --- /dev/null +++ b/version-meld/enr/src/lib.rs @@ -0,0 +1,1268 @@ +//! # Ethereum Node Record (ENR) +//! +//! This crate contains an implementation of an Ethereum Node Record (ENR) as specified by +//! [EIP-778](https://eips.ethereum.org/EIPS/eip-778) extended to allow for the use of ed25519 keys. +//! +//! An ENR is a signed, key-value record which has an associated [`NodeId`] (a 32-byte identifier). +//! Updating/modifying an ENR requires an [`EnrKey`] in order to re-sign the recrd with the +//! associated key-pair. +//! +//! ENR's are identified by their sequence number. When updating an ENR, the sequence number is +//! increased. +//! +//! Different identity schemes can be used to define the node id and signatures. Currently only the +//! "v4" identity is supported and is set by default. +//! +//! ## Signing Algorithms +//! +//! User's wishing to implement their own singing algorithms simply need to +//! implement the [`EnrKey`] trait and apply it to an [`Enr`]. +//! +//! By default, `k256::SigningKey` implement [`EnrKey`] and can be used to sign and +//! verify ENR records. This library also implements [`EnrKey`] for `ed25519_dalek::Keypair` via the `ed25519` +//! feature flag. +//! +//! Furthermore, a [`CombinedKey`] is provided if the `ed25519` feature flag is set, which provides an +//! ENR type that can support both `secp256k1` and `ed25519` signed ENR records. Examples of the +//! use of each of these key types is given below. +//! +//! ## Features +//! +//! This crate supports a number of features. +//! +//! - `serde`: Allows for serde serialization and deserialization for ENRs. +//! - `ed25519`: Provides support for `ed25519_dalek` keypair types. +//! - `k256`: Uses `k256` for secp256k1 keys. +//! - `rust-secp256k1`: Uses `rust-secp256k1` for secp256k1 keys. +//! +//! These can be enabled via adding the feature flag in your `Cargo.toml` +//! +//! ```toml +//! enr = { version = "*", features = ["serde", "ed25519"] } +//! ``` +//! +//! ## Examples +//! +//! To build an ENR, an [`EnrBuilder`] is provided. +//! +//! ### Building an ENR with the default `k256` `secp256k1` key type +//! +//! ```rust +//! use enr::{EnrBuilder, k256}; +//! use std::net::Ipv4Addr; +//! use rand::thread_rng; +//! +//! // generate a random secp256k1 key +//! let mut rng = thread_rng(); +//! let key = k256::ecdsa::SigningKey::random(&mut rng); +//! +//! let ip = Ipv4Addr::new(192,168,0,1); +//! let enr = EnrBuilder::new("v4").ip4(ip).tcp4(8000).build(&key).unwrap(); +//! +//! assert_eq!(enr.ip4(), Some("192.168.0.1".parse().unwrap())); +//! assert_eq!(enr.id(), Some("v4".into())); +//! ``` +//! +//! ### Building an ENR with the `CombinedKey` type (support for multiple signing +//! algorithms). +//! +//! Note the `ed25519` feature flag must be set. This makes use of the +//! [`EnrBuilder`] struct. +//! ```rust +//! # #[cfg(feature = "ed25519")] { +//! use enr::{EnrBuilder, CombinedKey}; +//! use std::net::Ipv4Addr; +//! +//! // create a new secp256k1 key +//! let key = CombinedKey::generate_secp256k1(); +//! +//! // or create a new ed25519 key +//! let key = CombinedKey::generate_ed25519(); +//! +//! let ip = Ipv4Addr::new(192,168,0,1); +//! let enr = EnrBuilder::new("v4").ip4(ip).tcp4(8000).build(&key).unwrap(); +//! +//! assert_eq!(enr.ip4(), Some("192.168.0.1".parse().unwrap())); +//! assert_eq!(enr.id(), Some("v4".into())); +//! # } +//! ``` +//! +//! ### Modifying an [`Enr`] +//! +//! ENR fields can be added and modified using the getters/setters on [`Enr`]. A custom field +//! can be added using [`insert`] and retrieved with [`get`]. +//! +//! ```rust +//! use enr::{EnrBuilder, k256::ecdsa::SigningKey, Enr}; +//! use std::net::Ipv4Addr; +//! use rand::thread_rng; +//! +//! // specify the type of ENR +//! type DefaultEnr = Enr; +//! +//! // generate a random secp256k1 key +//! let mut rng = thread_rng(); +//! let key = SigningKey::random(&mut rng); +//! +//! let ip = Ipv4Addr::new(192,168,0,1); +//! let mut enr = EnrBuilder::new("v4").ip4(ip).tcp4(8000).build(&key).unwrap(); +//! +//! enr.set_tcp4(8001, &key); +//! // set a custom key +//! enr.insert("custom_key", &vec![0,0,1], &key); +//! +//! // encode to base64 +//! let base_64_string = enr.to_base64(); +//! +//! // decode from base64 +//! let decoded_enr: DefaultEnr = base_64_string.parse().unwrap(); +//! +//! assert_eq!(decoded_enr.ip4(), Some("192.168.0.1".parse().unwrap())); +//! assert_eq!(decoded_enr.id(), Some("v4".into())); +//! assert_eq!(decoded_enr.tcp4(), Some(8001)); +//! assert_eq!(decoded_enr.get("custom_key"), Some(vec![0,0,1].as_slice())); +//! ``` +//! +//! ### Encoding/Decoding ENR's of various key types +//! +//! ```rust +//! # #[cfg(feature = "ed25519")] { +//! use enr::{EnrBuilder, k256::ecdsa::SigningKey, Enr, ed25519_dalek::Keypair, CombinedKey}; +//! use std::net::Ipv4Addr; +//! use rand::thread_rng; +//! use rand::Rng; +//! +//! // generate a random secp256k1 key +//! let mut rng = thread_rng(); +//! let key = SigningKey::random(&mut rng); +//! let ip = Ipv4Addr::new(192,168,0,1); +//! let enr_secp256k1 = EnrBuilder::new("v4").ip4(ip).tcp4(8000).build(&key).unwrap(); +//! +//! // encode to base64 +//! let base64_string_secp256k1 = enr_secp256k1.to_base64(); +//! +//! // generate a random ed25519 key +//! # let mut rng = rand_07::thread_rng(); +//! let key = Keypair::generate(&mut rng); +//! let enr_ed25519 = EnrBuilder::new("v4").ip4(ip).tcp4(8000).build(&key).unwrap(); +//! +//! // encode to base64 +//! let base64_string_ed25519 = enr_ed25519.to_base64(); +//! +//! // decode base64 strings of varying key types +//! // decode the secp256k1 with default Enr +//! let decoded_enr_secp256k1: Enr = base64_string_secp256k1.parse().unwrap(); +//! // decode ed25519 ENRs +//! let decoded_enr_ed25519: Enr = base64_string_ed25519.parse().unwrap(); +//! +//! // use the combined key to be able to decode either +//! let decoded_enr: Enr = base64_string_secp256k1.parse().unwrap(); +//! let decoded_enr: Enr = base64_string_ed25519.parse().unwrap(); +//! # } +//! ``` +//! +//! +//! [`CombinedKey`]: enum.CombinedKey.html +//! [`EnrKey`]: trait.EnrKey.html +//! [`Enr`]: struct.EnrBase.html +//! [`EnrBuilder`]: struct.EnrBuilderBase.html +//! [`NodeId`]: struct.NodeId.html +//! [`insert`]: struct.Enr.html#method.insert +//! [`get`]: struct.Enr.html#method.get + +#![warn(clippy::all, clippy::pedantic, clippy::nursery)] +#![allow( + clippy::map_err_ignore, + clippy::missing_errors_doc, + clippy::module_name_repetitions +)] + +mod builder; +mod error; +mod keys; +mod node_id; + +use bytes::{Bytes, BytesMut}; +use log::debug; +use rlp::{DecoderError, Rlp, RlpStream}; +use std::{ + collections::BTreeMap, + net::{SocketAddrV4, SocketAddrV6}, +}; + +#[cfg(feature = "serde")] +use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; +use sha3::{Digest, Keccak256}; +use std::{ + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, + str::FromStr, +}; + +pub use builder::EnrBuilder; +pub use error::EnrError; + +#[cfg(feature = "k256")] +pub use keys::k256; +#[cfg(feature = "rust-secp256k1")] +pub use keys::secp256k1; +#[cfg(all(feature = "ed25519", feature = "k256"))] +pub use keys::{ed25519_dalek, CombinedKey, CombinedPublicKey}; + +pub use keys::{EnrKey, EnrKeyUnambiguous, EnrPublicKey}; +pub use node_id::NodeId; +use std::marker::PhantomData; + +/// The "key" in an ENR record can be arbitrary bytes. +type Key = Vec; + +const MAX_ENR_SIZE: usize = 300; + +/// The ENR, allowing for arbitrary signing algorithms. +/// +/// This struct will always have a valid signature, known public key type, sequence number and `NodeId`. All other parameters are variable/optional. +pub struct Enr { + /// ENR sequence number. + seq: u64, + + /// The `NodeId` of the ENR record. + node_id: NodeId, + + /// Key-value contents of the ENR. A BTreeMap is used to get the keys in sorted order, which is + /// important for verifying the signature of the ENR. + /// Everything is stored as raw RLP bytes. + content: BTreeMap, + + /// The signature of the ENR record, stored as bytes. + signature: Vec, + + /// Marker to pin the generic. + phantom: PhantomData, +} + +impl Enr { + // getters // + + /// The `NodeId` for the record. + #[must_use] + pub const fn node_id(&self) -> NodeId { + self.node_id + } + + /// The current sequence number of the ENR record. + #[must_use] + pub const fn seq(&self) -> u64 { + self.seq + } + + /// Reads a custom key from the record if it exists. + pub fn get(&self, key: impl AsRef<[u8]>) -> Option<&[u8]> { + self.get_raw_rlp(key).map(|rlp_data| { + rlp::Rlp::new(rlp_data) + .data() + .expect("All data is sanitized") + }) + } + + /// Reads a custom key from the record if it exists as raw RLP bytes. + pub fn get_raw_rlp(&self, key: impl AsRef<[u8]>) -> Option<&[u8]> { + self.content.get(key.as_ref()).map(AsRef::as_ref) + } + + /// Returns an iterator over all key/value pairs in the ENR. + pub fn iter(&self) -> impl Iterator { + self.content.iter().map(|(k, v)| (k, v.as_ref())) + } + + /// Returns the IPv4 address of the ENR record if it is defined. + #[must_use] + pub fn ip4(&self) -> Option { + if let Some(ip_bytes) = self.get("ip") { + return match ip_bytes.len() { + 4 => { + let mut ip = [0_u8; 4]; + ip.copy_from_slice(ip_bytes); + Some(Ipv4Addr::from(ip)) + } + _ => None, + }; + } + None + } + + #[must_use] + pub fn ip(&self) -> Option { + self.ip4() + } + + /// Returns the IPv6 address of the ENR record if it is defined. + #[must_use] + pub fn ip6(&self) -> Option { + if let Some(ip_bytes) = self.get("ip6") { + return match ip_bytes.len() { + 16 => { + let mut ip = [0_u8; 16]; + ip.copy_from_slice(ip_bytes); + Some(Ipv6Addr::from(ip)) + } + _ => None, + }; + } + None + } + + /// The `id` of ENR record if it is defined. + #[must_use] + pub fn id(&self) -> Option { + if let Some(id_bytes) = self.get("id") { + return Some(String::from_utf8_lossy(id_bytes).to_string()); + } + None + } + + /// The TCP port of ENR record if it is defined. + #[must_use] + pub fn tcp4(&self) -> Option { + if let Some(tcp_bytes) = self.get("tcp") { + if tcp_bytes.len() <= 2 { + let mut tcp = [0_u8; 2]; + tcp[2 - tcp_bytes.len()..].copy_from_slice(tcp_bytes); + return Some(u16::from_be_bytes(tcp)); + } + } + None + } + + #[must_use] + pub fn tcp(&self) -> Option { + self.tcp4() + } + + /// The IPv6-specific TCP port of ENR record if it is defined. + #[must_use] + pub fn tcp6(&self) -> Option { + if let Some(tcp_bytes) = self.get("tcp6") { + if tcp_bytes.len() <= 2 { + let mut tcp = [0_u8; 2]; + tcp[2 - tcp_bytes.len()..].copy_from_slice(tcp_bytes); + return Some(u16::from_be_bytes(tcp)); + } + } + None + } + + /// The UDP port of ENR record if it is defined. + #[must_use] + pub fn udp4(&self) -> Option { + if let Some(udp_bytes) = self.get("udp") { + if udp_bytes.len() <= 2 { + let mut udp = [0_u8; 2]; + udp[2 - udp_bytes.len()..].copy_from_slice(udp_bytes); + return Some(u16::from_be_bytes(udp)); + } + } + None + } + + #[must_use] + pub fn udp(&self) -> Option { + self.udp4() + } + + /// The IPv6-specific UDP port of ENR record if it is defined. + #[must_use] + pub fn udp6(&self) -> Option { + if let Some(udp_bytes) = self.get("udp6") { + if udp_bytes.len() <= 2 { + let mut udp = [0_u8; 2]; + udp[2 - udp_bytes.len()..].copy_from_slice(udp_bytes); + return Some(u16::from_be_bytes(udp)); + } + } + None + } + + /// Provides a socket (based on the UDP port), if the IPv4 and UDP fields are specified. + #[must_use] + pub fn udp4_socket(&self) -> Option { + if let Some(ip) = self.ip4() { + if let Some(udp) = self.udp4() { + return Some(SocketAddrV4::new(ip, udp)); + } + } + None + } + + /// Provides a socket (based on the UDP port), if the IPv4 and UDP fields are specified. + #[must_use] + pub fn udp6_socket(&self) -> Option { + if let Some(ip6) = self.ip6() { + if let Some(udp6) = self.udp6() { + return Some(SocketAddrV6::new(ip6, udp6, 0, 0)); + } + } + None + } + + #[must_use] + pub fn udp_socket(&self) -> Option { + if let Some(v4)=self.udp4_socket(){ + return Some(SocketAddr::V4(v4)); + } + if let Some(v6)=self.udp6_socket(){ + return Some(SocketAddr::V6(v6)); + } + return None; + } + + /// Provides a socket (based on the TCP port), if the IP and TCP fields are specified. + #[must_use] + pub fn tcp4_socket(&self) -> Option { + if let Some(ip) = self.ip4() { + if let Some(tcp) = self.tcp4() { + return Some(SocketAddrV4::new(ip, tcp)); + } + } + None + } + + /// Provides a socket (based on the TCP port), if the IPv6 and TCP6 fields are specified. + #[must_use] + pub fn tcp6_socket(&self) -> Option { + if let Some(ip6) = self.ip6() { + if let Some(tcp6) = self.tcp6() { + return Some(SocketAddrV6::new(ip6, tcp6, 0, 0)); + } + } + None + } + + /// The signature of the ENR record. + #[must_use] + pub fn signature(&self) -> &[u8] { + &self.signature + } + + /// Returns the public key of the ENR record. + #[must_use] + pub fn public_key(&self) -> K::PublicKey { + K::enr_to_public(&self.content).expect("ENR's can only be created with supported keys") + } + + /// Verify the signature of the ENR record. + #[must_use] + pub fn verify(&self) -> bool { + let pubkey = self.public_key(); + match self.id() { + Some(ref id) if id == "v4" => pubkey.verify_v4(&self.rlp_content(), &self.signature), + // unsupported identity schemes + _ => false, + } + } + + /// Provides the URL-safe base64 encoded "text" version of the ENR prefixed by "enr:". + #[must_use] + pub fn to_base64(&self) -> String { + let hex = base64::encode_config(&rlp::encode(self), base64::URL_SAFE_NO_PAD); + format!("enr:{}", hex) + } + + /// Returns the current size of the ENR. + #[must_use] + pub fn size(&self) -> usize { + self.rlp_content().len() + } + + // Setters // + + /// Allows setting the sequence number to an arbitrary value. + pub fn set_seq(&mut self, seq: u64, key: &K) -> Result<(), EnrError> { + self.seq = seq; + + // sign the record + self.sign(key)?; + + // update the node id + self.node_id = NodeId::from(key.public()); + + // check the size of the record + if self.size() > MAX_ENR_SIZE { + return Err(EnrError::ExceedsMaxSize); + } + + Ok(()) + } + + /// Adds or modifies a key/value to the ENR record. A `EnrKey` is required to re-sign the record once + /// modified. + /// + /// Returns the previous value in the record if it exists. + pub fn insert( + &mut self, + key: impl AsRef<[u8]>, + value: &[u8], + enr_key: &K, + ) -> Result, EnrError> { + self.insert_raw_rlp(key, rlp::encode(&value).freeze(), enr_key) + } + + /// Adds or modifies a key/value to the ENR record. A `EnrKey` is required to re-sign the record once + /// modified. The value here is interpreted as raw RLP data. + /// + /// Returns the previous value in the record if it exists. + pub fn insert_raw_rlp( + &mut self, + key: impl AsRef<[u8]>, + value: Bytes, + enr_key: &K, + ) -> Result, EnrError> { + // currently only support "v4" identity schemes + if key.as_ref() == b"id" && &*value != b"v4" { + return Err(EnrError::UnsupportedIdentityScheme); + } + + let previous_value = self.content.insert(key.as_ref().to_vec(), value); + // add the new public key + let public_key = enr_key.public(); + let previous_key = self.content.insert( + public_key.enr_key(), + rlp::encode(&public_key.encode().as_ref()).freeze(), + ); + + // check the size of the record + if self.size() > MAX_ENR_SIZE { + // if the size of the record is too large, revert and error + // revert the public key + if let Some(key) = previous_key { + self.content.insert(public_key.enr_key(), key); + } else { + self.content.remove(&public_key.enr_key()); + } + // revert the content + if let Some(prev_value) = previous_value { + self.content.insert(key.as_ref().to_vec(), prev_value); + } else { + self.content.remove(key.as_ref()); + } + return Err(EnrError::ExceedsMaxSize); + } + // increment the sequence number + self.seq = self + .seq + .checked_add(1) + .ok_or(EnrError::SequenceNumberTooHigh)?; + + // sign the record + self.sign(enr_key)?; + + // update the node id + self.node_id = NodeId::from(enr_key.public()); + + if self.size() > MAX_ENR_SIZE { + // in case the signature size changes, inform the user the size has exceeded the maximum + return Err(EnrError::ExceedsMaxSize); + } + + Ok(previous_value) + } + + /// Sets the `ip` field of the ENR. Returns any pre-existing IP address in the record. + pub fn set_ip(&mut self, ip: IpAddr, key: &K) -> Result, EnrError> { + match ip { + IpAddr::V4(addr) => { + let prev_value = self.insert("ip", &addr.octets(), key)?; + if let Some(bytes) = prev_value { + if bytes.len() == 4 { + let mut v = [0_u8; 4]; + v.copy_from_slice(&bytes); + return Ok(Some(IpAddr::V4(Ipv4Addr::from(v)))); + } + } + } + IpAddr::V6(addr) => { + let prev_value = self.insert("ip6", &addr.octets(), key)?; + if let Some(bytes) = prev_value { + if bytes.len() == 16 { + let mut v = [0_u8; 16]; + v.copy_from_slice(&bytes); + return Ok(Some(IpAddr::V6(Ipv6Addr::from(v)))); + } + } + } + } + + Ok(None) + } + + /// Sets the `udp` field of the ENR. Returns any pre-existing UDP port in the record. + pub fn set_udp4(&mut self, udp: u16, key: &K) -> Result, EnrError> { + if let Some(udp_bytes) = self.insert("udp", &udp.to_be_bytes(), key)? { + if udp_bytes.len() <= 2 { + let mut v = [0_u8; 2]; + v[2 - udp_bytes.len()..].copy_from_slice(&udp_bytes); + return Ok(Some(u16::from_be_bytes(v))); + } + } + Ok(None) + } + + /// Sets the `udp6` field of the ENR. Returns any pre-existing UDP port in the record. + pub fn set_udp6(&mut self, udp: u16, key: &K) -> Result, EnrError> { + if let Some(udp_bytes) = self.insert("udp6", &udp.to_be_bytes(), key)? { + if udp_bytes.len() <= 2 { + let mut v = [0_u8; 2]; + v[2 - udp_bytes.len()..].copy_from_slice(&udp_bytes); + return Ok(Some(u16::from_be_bytes(v))); + } + } + Ok(None) + } + + /// Sets the `tcp` field of the ENR. Returns any pre-existing tcp port in the record. + pub fn set_tcp4(&mut self, tcp: u16, key: &K) -> Result, EnrError> { + if let Some(tcp_bytes) = self.insert("tcp", &tcp.to_be_bytes(), key)? { + if tcp_bytes.len() <= 2 { + let mut v = [0_u8; 2]; + v[2 - tcp_bytes.len()..].copy_from_slice(&tcp_bytes); + return Ok(Some(u16::from_be_bytes(v))); + } + } + Ok(None) + } + + /// Sets the `tcp6` field of the ENR. Returns any pre-existing tcp6 port in the record. + pub fn set_tcp6(&mut self, tcp: u16, key: &K) -> Result, EnrError> { + if let Some(tcp_bytes) = self.insert("tcp6", &tcp.to_be_bytes(), key)? { + if tcp_bytes.len() <= 2 { + let mut v = [0_u8; 2]; + v[2 - tcp_bytes.len()..].copy_from_slice(&tcp_bytes); + return Ok(Some(u16::from_be_bytes(v))); + } + } + Ok(None) + } + + /// Sets the IP and UDP port in a single update with a single increment in sequence number. + pub fn set_udp_socket(&mut self, socket: SocketAddr, key: &K) -> Result<(), EnrError> { + self.set_socket(socket, key, false) + } + + /// Sets the IP and TCP port in a single update with a single increment in sequence number. + pub fn set_tcp_socket(&mut self, socket: SocketAddr, key: &K) -> Result<(), EnrError> { + self.set_socket(socket, key, true) + } + + /// Helper function for `set_tcp_socket()` and `set_udp_socket`. + fn set_socket(&mut self, socket: SocketAddr, key: &K, is_tcp: bool) -> Result<(), EnrError> { + let (port_string, port_v6_string): (Key, Key) = if is_tcp { + ("tcp".into(), "tcp6".into()) + } else { + ("udp".into(), "udp6".into()) + }; + + let (prev_ip, prev_port) = match socket.ip() { + IpAddr::V4(addr) => ( + self.content.insert( + "ip".into(), + rlp::encode(&(&addr.octets() as &[u8])).freeze(), + ), + self.content.insert( + port_string.clone(), + rlp::encode(&(&socket.port().to_be_bytes() as &[u8])).freeze(), + ), + ), + IpAddr::V6(addr) => ( + self.content.insert( + "ip6".into(), + rlp::encode(&(&addr.octets() as &[u8])).freeze(), + ), + self.content.insert( + port_v6_string.clone(), + rlp::encode(&(&socket.port().to_be_bytes() as &[u8])).freeze(), + ), + ), + }; + + let public_key = key.public(); + let previous_key = self.content.insert( + public_key.enr_key(), + rlp::encode(&public_key.encode().as_ref()).freeze(), + ); + + // check the size and revert on failure + if self.size() > MAX_ENR_SIZE { + // if the size of the record is too large, revert and error + // revert the public key + if let Some(key) = previous_key { + self.content.insert(public_key.enr_key(), key); + } else { + self.content.remove(&public_key.enr_key()); + } + // revert the content + match socket.ip() { + IpAddr::V4(_) => { + if let Some(ip) = prev_ip { + self.content.insert("ip".into(), ip); + } else { + self.content.remove(b"ip".as_ref()); + } + if let Some(udp) = prev_port { + self.content.insert(port_string, udp); + } else { + self.content.remove(&port_string); + } + } + IpAddr::V6(_) => { + if let Some(ip) = prev_ip { + self.content.insert("ip6".into(), ip); + } else { + self.content.remove(b"ip6".as_ref()); + } + if let Some(udp) = prev_port { + self.content.insert(port_v6_string, udp); + } else { + self.content.remove(&port_v6_string); + } + } + } + return Err(EnrError::ExceedsMaxSize); + } + + // increment the sequence number + self.seq = self + .seq + .checked_add(1) + .ok_or(EnrError::SequenceNumberTooHigh)?; + + // sign the record + self.sign(key)?; + + // update the node id + self.node_id = NodeId::from(key.public()); + + Ok(()) + } + + /// Sets a new public key for the record. + pub fn set_public_key(&mut self, public_key: &K::PublicKey, key: &K) -> Result<(), EnrError> { + self.insert(&public_key.enr_key(), public_key.encode().as_ref(), key) + .map(|_| {}) + } + + // Private Functions // + + /// Evaluates the RLP-encoding of the content of the ENR record. + fn rlp_content(&self) -> BytesMut { + let mut stream = RlpStream::new_with_buffer(BytesMut::with_capacity(MAX_ENR_SIZE)); + stream.begin_list(self.content.len() * 2 + 1); + stream.append(&self.seq); + for (k, v) in &self.content { + // Keys are bytes + stream.append(k); + // Values are raw RLP encoded data + stream.append_raw(v, 1); + } + stream.out() + } + + /// Signs the ENR record based on the identity scheme. Currently only "v4" is supported. + fn sign(&mut self, key: &K) -> Result<(), EnrError> { + self.signature = { + match self.id() { + Some(ref id) if id == "v4" => key + .sign_v4(&self.rlp_content()) + .map_err(|_| EnrError::SigningError)?, + // other identity schemes are unsupported + _ => return Err(EnrError::SigningError), + } + }; + Ok(()) + } +} + +// traits // + +impl Clone for Enr { + fn clone(&self) -> Self { + Self { + seq: self.seq, + node_id: self.node_id, + content: self.content.clone(), + signature: self.signature.clone(), + phantom: self.phantom, + } + } +} + +impl std::cmp::Eq for Enr {} + +impl PartialEq for Enr { + fn eq(&self, other: &Self) -> bool { + self.seq == other.seq && self.node_id == other.node_id && self.signature == other.signature + } +} + +impl std::fmt::Display for Enr { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "ENR: NodeId: {}, IpV4 Socket: {:?} IpV6 Socket: {:?}", + self.node_id(), + self.udp4_socket(), + self.udp6_socket() + ) + } +} + +impl std::fmt::Debug for Enr { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.to_base64()) + } +} + +/// Convert a URL-SAFE base64 encoded ENR into an ENR. +impl FromStr for Enr { + type Err = String; + + fn from_str(base64_string: &str) -> Result { + if base64_string.len() < 4 { + return Err("Invalid ENR string".to_string()); + } + // support both enr prefix and not + let mut decode_string = base64_string; + if base64_string.starts_with("enr:") { + decode_string = decode_string + .get(4..) + .ok_or_else(|| "Invalid ENR string".to_string())?; + } + let bytes = base64::decode_config(decode_string, base64::URL_SAFE_NO_PAD) + .map_err(|e| format!("Invalid base64 encoding: {:?}", e))?; + rlp::decode(&bytes).map_err(|e| format!("Invalid ENR: {:?}", e)) + } +} + +#[cfg(any(feature = "serde"))] +impl Serialize for Enr { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_base64()) + } +} + +#[cfg(any(feature = "serde"))] +impl<'de, K: EnrKey> Deserialize<'de> for Enr { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s: String = Deserialize::deserialize(deserializer)?; + Self::from_str(&s).map_err(D::Error::custom) + } +} + +impl rlp::Encodable for Enr { + fn rlp_append(&self, s: &mut RlpStream) { + s.begin_list(self.content.len() * 2 + 2); + s.append(&self.signature); + s.append(&self.seq); + // must use rlp_content to preserve ordering. + for (k, v) in &self.content { + // Keys are byte data + s.append(k); + // Values are raw RLP encoded data + s.append_raw(v, 1); + } + } +} + +impl rlp::Decodable for Enr { + fn decode(rlp: &Rlp) -> Result { + if !rlp.is_list() { + debug!("Failed to decode ENR. Not an RLP list: {}", rlp); + return Err(DecoderError::RlpExpectedToBeList); + } + + let mut rlp_iter = rlp.iter(); + + if rlp_iter.len() == 0 || rlp_iter.len() % 2 != 0 { + debug!("Failed to decode ENR. List size is not a multiple of 2."); + return Err(DecoderError::Custom("List not a multiple of two")); + } + + let signature = rlp_iter + .next() + .ok_or(DecoderError::Custom("List is empty"))? + .data()?; + let seq_bytes = rlp_iter + .next() + .ok_or(DecoderError::Custom("List has only one item"))? + .data()?; + + if seq_bytes.len() > 8 { + debug!("Failed to decode ENR. Sequence number is not a u64."); + return Err(DecoderError::Custom("Invalid Sequence number")); + } + + // build u64 from big endian vec + let mut seq: [u8; 8] = [0; 8]; + seq[8 - seq_bytes.len()..].copy_from_slice(seq_bytes); + let seq = u64::from_be_bytes(seq); + + let mut content = BTreeMap::new(); + let mut prev: Option<&[u8]> = None; + while let Some(key) = rlp_iter.next() { + let key = key.data()?; + let item = rlp_iter + .next() + .ok_or(DecoderError::Custom("List not a multiple of 2"))?; + + // Sanitize the data + let _ = item.data()?; + let value = item.as_raw(); + + if prev.is_some() && prev >= Some(key) { + return Err(DecoderError::Custom("Unsorted keys")); + } + prev = Some(key); + content.insert(key.to_vec(), Bytes::copy_from_slice(value)); + } + + // verify we know the signature type + let public_key = K::enr_to_public(&content)?; + + // calculate the node id + let node_id = NodeId::from(public_key); + + let enr = Self { + seq, + node_id, + signature: signature.into(), + content, + phantom: PhantomData, + }; + + // verify the signature before returning + // if the public key is of an unknown type, this will fail. + // An ENR record will always have a valid public-key and therefore node-id + if !enr.verify() { + return Err(DecoderError::Custom("Invalid Signature")); + } + Ok(enr) + } +} + +pub(crate) fn digest(b: &[u8]) -> [u8; 32] { + let mut output = [0_u8; 32]; + output.copy_from_slice(&Keccak256::digest(b)); + output +} + +#[cfg(test)] +#[cfg(feature = "k256")] +mod tests { + use super::*; + use std::net::Ipv4Addr; + + type DefaultEnr = Enr; + + #[cfg(feature = "k256")] + #[test] + fn test_vector_k256() { + let valid_record = hex::decode("f884b8407098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c01826964827634826970847f00000189736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd31388375647082765f").unwrap(); + let signature = hex::decode("7098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c").unwrap(); + let expected_pubkey = + hex::decode("03ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138") + .unwrap(); + + let enr = rlp::decode::(&valid_record).unwrap(); + + let pubkey = enr.public_key().encode(); + + assert_eq!(enr.ip4(), Some(Ipv4Addr::new(127, 0, 0, 1))); + assert_eq!(enr.id(), Some(String::from("v4"))); + assert_eq!(enr.udp4(), Some(30303)); + assert_eq!(enr.tcp4(), None); + assert_eq!(enr.signature(), &signature[..]); + assert_eq!(pubkey.to_vec(), expected_pubkey); + assert!(enr.verify()); + } + + #[cfg(feature = "k256")] + #[test] + fn test_vector_2() { + let text = "enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8"; + let signature = hex::decode("7098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c").unwrap(); + let expected_pubkey = + hex::decode("03ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138") + .unwrap(); + let expected_node_id = + hex::decode("a448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7") + .unwrap(); + + let enr = text.parse::().unwrap(); + let pubkey = enr.public_key().encode(); + assert_eq!(enr.ip4(), Some(Ipv4Addr::new(127, 0, 0, 1))); + assert_eq!(enr.ip6(), None); + assert_eq!(enr.id(), Some(String::from("v4"))); + assert_eq!(enr.udp4(), Some(30303)); + assert_eq!(enr.udp6(), None); + assert_eq!(enr.tcp4(), None); + assert_eq!(enr.tcp6(), None); + assert_eq!(enr.signature(), &signature[..]); + assert_eq!(pubkey.to_vec(), expected_pubkey); + assert_eq!(enr.node_id().raw().to_vec(), expected_node_id); + + assert!(enr.verify()); + } + + #[cfg(feature = "k256")] + #[test] + fn test_vector_2_k256() { + let text = "enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8"; + let signature = hex::decode("7098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c").unwrap(); + let expected_pubkey = + hex::decode("03ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138") + .unwrap(); + let expected_node_id = + hex::decode("a448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7") + .unwrap(); + + let enr = text.parse::>().unwrap(); + let pubkey = enr.public_key().encode(); + assert_eq!(enr.ip4(), Some(Ipv4Addr::new(127, 0, 0, 1))); + assert_eq!(enr.ip6(), None); + assert_eq!(enr.id(), Some(String::from("v4"))); + assert_eq!(enr.udp4(), Some(30303)); + assert_eq!(enr.udp6(), None); + assert_eq!(enr.tcp4(), None); + assert_eq!(enr.tcp6(), None); + assert_eq!(enr.signature(), &signature[..]); + assert_eq!(pubkey.to_vec(), expected_pubkey); + assert_eq!(enr.node_id().raw().to_vec(), expected_node_id); + + assert!(enr.verify()); + } + + // the values in the content are rlp lists + #[test] + fn test_rlp_list_value() { + let text = "enr:-Je4QH0uN2HkMRmscUp6yvyTOPGtOg9U6lCxBFvCGynyystnDNRJbfz5GhXXY2lcu9tsghMxRiYHoznBwG46GQ7dfm0og2V0aMfGhMvbiDiAgmlkgnY0gmlwhA6hJmuJc2VjcDI1NmsxoQJBP4kg9GNBurV3uVXgR72u1n-XIABibUZLT1WvJLKwvIN0Y3CCdyeDdWRwgncn"; + let signature = hex::decode("7d2e3761e43119ac714a7acafc9338f1ad3a0f54ea50b1045bc21b29f2cacb670cd4496dfcf91a15d763695cbbdb6c821331462607a339c1c06e3a190edd7e6d").unwrap(); + let expected_pubkey = + hex::decode("02413f8920f46341bab577b955e047bdaed67f972000626d464b4f55af24b2b0bc") + .unwrap(); + let enr = text.parse::().unwrap(); + + assert_eq!(enr.ip4(), Some(Ipv4Addr::new(14, 161, 38, 107))); + assert_eq!(enr.id(), Some(String::from("v4"))); + assert_eq!(enr.udp4(), Some(30503)); + assert_eq!(enr.tcp4(), Some(30503)); + assert_eq!(enr.seq(), 40); + assert_eq!(enr.signature(), &signature[..]); + assert_eq!(enr.public_key().encode().to_vec(), expected_pubkey); + + assert!(enr.verify()); + } + + #[cfg(feature = "k256")] + #[test] + fn test_read_enr_no_prefix() { + let text = "-Iu4QM-YJF2RRpMcZkFiWzMf2kRd1A5F1GIekPa4Sfi_v0DCLTDBfOMTMMWJhhawr1YLUPb5008CpnBKrgjY3sstjfgCgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQP8u1uyQFyJYuQUTyA1raXKhSw1HhhxNUQ2VE52LNHWMIN0Y3CCIyiDdWRwgiMo"; + text.parse::().unwrap(); + } + + #[cfg(feature = "k256")] + #[test] + fn test_read_enr_prefix() { + let text = "enr:-Iu4QM-YJF2RRpMcZkFiWzMf2kRd1A5F1GIekPa4Sfi_v0DCLTDBfOMTMMWJhhawr1YLUPb5008CpnBKrgjY3sstjfgCgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQP8u1uyQFyJYuQUTyA1raXKhSw1HhhxNUQ2VE52LNHWMIN0Y3CCIyiDdWRwgiMo"; + text.parse::().unwrap(); + } + + #[cfg(feature = "rust-secp256k1")] + #[test] + fn test_encode_decode_secp256k1() { + let mut rng = secp256k1::rand::thread_rng(); + let key = secp256k1::SecretKey::new(&mut rng); + let ip = Ipv4Addr::new(127, 0, 0, 1); + let tcp = 3000; + + let enr = { + let mut builder = EnrBuilder::new("v4"); + builder.ip4(ip); + builder.tcp4(tcp); + builder.build(&key).unwrap() + }; + + let encoded_enr = rlp::encode(&enr); + + let decoded_enr = rlp::decode::>(&encoded_enr).unwrap(); + + assert_eq!(decoded_enr.id(), Some("v4".into())); + assert_eq!(decoded_enr.ip4(), Some(ip)); + assert_eq!(decoded_enr.tcp4(), Some(tcp)); + // Must compare encoding as the public key itself can be different + assert_eq!(decoded_enr.public_key().encode(), key.public().encode()); + assert!(decoded_enr.verify()); + } + + #[cfg(feature = "k256")] + #[test] + fn test_encode_decode_k256() { + let key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); + let ip = Ipv4Addr::new(127, 0, 0, 1); + let tcp = 3000; + + let enr = { + let mut builder = EnrBuilder::new("v4"); + builder.ip(ip.into()); + builder.tcp4(tcp); + builder.build(&key).unwrap() + }; + + let encoded_enr = rlp::encode(&enr); + + let decoded_enr = rlp::decode::>(&encoded_enr).unwrap(); + + assert_eq!(decoded_enr.id(), Some("v4".into())); + assert_eq!(decoded_enr.ip4(), Some(ip)); + assert_eq!(decoded_enr.tcp4(), Some(tcp)); + // Must compare encoding as the public key itself can be different + assert_eq!(decoded_enr.public_key().encode(), key.public().encode()); + decoded_enr.public_key().encode_uncompressed(); + assert!(decoded_enr.verify()); + } + + #[cfg(all(feature = "ed25519", feature = "k256"))] + #[test] + fn test_encode_decode_ed25519() { + let mut rng = rand_07::thread_rng(); + let key = ed25519_dalek::Keypair::generate(&mut rng); + let ip = Ipv4Addr::new(10, 0, 0, 1); + let tcp = 30303; + + let enr = { + let mut builder = EnrBuilder::new("v4"); + builder.ip4(ip.into()); + builder.tcp4(tcp); + builder.build(&key).unwrap() + }; + + let encoded_enr = rlp::encode(&enr); + let decoded_enr = rlp::decode::>(&encoded_enr).unwrap(); + + assert_eq!(decoded_enr.id(), Some("v4".into())); + assert_eq!(decoded_enr.ip4(), Some(ip)); + assert_eq!(decoded_enr.tcp4(), Some(tcp)); + assert_eq!(decoded_enr.public_key().encode(), key.public().encode()); + assert!(decoded_enr.verify()); + } + + #[test] + fn test_add_key() { + let mut rng = rand::thread_rng(); + let key = k256::ecdsa::SigningKey::random(&mut rng); + let ip = Ipv4Addr::new(10, 0, 0, 1); + let tcp = 30303; + + let mut enr = { + let mut builder = EnrBuilder::new("v4"); + builder.ip(ip.into()); + builder.tcp4(tcp); + builder.build(&key).unwrap() + }; + + enr.insert("random", &Vec::new(), &key).unwrap(); + assert!(enr.verify()); + } + + #[test] + fn test_set_ip() { + let mut rng = rand::thread_rng(); + let key = k256::ecdsa::SigningKey::random(&mut rng); + let tcp = 30303; + let ip = Ipv4Addr::new(10, 0, 0, 1); + + let mut enr = { + let mut builder = EnrBuilder::new("v4"); + builder.tcp4(tcp); + builder.build(&key).unwrap() + }; + + assert!(enr.set_ip(ip.into(), &key).is_ok()); + assert_eq!(enr.id(), Some("v4".into())); + assert_eq!(enr.ip4(), Some(ip)); + assert_eq!(enr.tcp4(), Some(tcp)); + assert!(enr.verify()); + + // Compare the encoding as the key itself can be differnet + assert_eq!(enr.public_key().encode(), key.public().encode(),); + } + + #[test] + fn ip_mutation_static_node_id() { + let mut rng = rand::thread_rng(); + let key = k256::ecdsa::SigningKey::random(&mut rng); + let tcp = 30303; + let udp = 30304; + let ip = Ipv4Addr::new(10, 0, 0, 1); + + let mut enr = { + let mut builder = EnrBuilder::new("v4"); + builder.ip(ip.into()); + builder.tcp4(tcp); + builder.udp4(udp); + builder.build(&key).unwrap() + }; + + let node_id = enr.node_id(); + + enr.set_udp_socket("192.168.0.1:800".parse::().unwrap(), &key) + .unwrap(); + assert_eq!(node_id, enr.node_id()); + assert_eq!( + enr.udp4_socket(), + "192.168.0.1:800".parse::().unwrap().into() + ); + } + + #[cfg(all(feature = "ed25519", feature = "k256"))] + #[test] + fn combined_key_can_decode_all() { + // generate a random secp256k1 key + let key = k256::ecdsa::SigningKey::random(&mut rand::thread_rng()); + let ip = Ipv4Addr::new(192, 168, 0, 1); + let enr_secp256k1 = EnrBuilder::new("v4") + .ip(ip.into()) + .tcp4(8000) + .build(&key) + .unwrap(); + + // encode to base64 + let base64_string_secp256k1 = enr_secp256k1.to_base64(); + + // generate a random ed25519 key + let key = ed25519_dalek::Keypair::generate(&mut rand_07::thread_rng()); + let enr_ed25519 = EnrBuilder::new("v4") + .ip(ip.into()) + .tcp4(8000) + .build(&key) + .unwrap(); + + // encode to base64 + let base64_string_ed25519 = enr_ed25519.to_base64(); + + // decode base64 strings of varying key types + // decode the secp256k1 with default Enr + let _decoded_enr_secp256k1: DefaultEnr = base64_string_secp256k1.parse().unwrap(); + // decode ed25519 ENRs + let _decoded_enr_ed25519: Enr = + base64_string_ed25519.parse().unwrap(); + + // use the combined key to be able to decode either + let _decoded_enr: Enr = base64_string_secp256k1 + .parse() + .expect("Can decode both secp"); + let _decoded_enr: Enr = base64_string_ed25519.parse().unwrap(); + } +} diff --git a/version-meld/enr/src/node_id.rs b/version-meld/enr/src/node_id.rs new file mode 100644 index 0000000..2b64dc7 --- /dev/null +++ b/version-meld/enr/src/node_id.rs @@ -0,0 +1,77 @@ +//! The identifier for an ENR record. This is the keccak256 hash of the public key (for secp256k1 +//! keys this is the uncompressed encoded form of the public key). + +use crate::{digest, keys::EnrPublicKey, Enr, EnrKey}; + +type RawNodeId = [u8; 32]; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +/// The `NodeId` of an ENR (a 32 byte identifier). +pub struct NodeId { + raw: RawNodeId, +} + +impl NodeId { + /// Creates a new node record from 32 bytes. + #[must_use] + pub const fn new(raw_input: &[u8; 32]) -> Self { + Self { raw: *raw_input } + } + + /// Parses a byte slice to form a node Id. This fails if the slice isn't of length 32. + pub fn parse(raw_input: &[u8]) -> Result { + if raw_input.len() > 32 { + return Err("Input too large"); + } + + let mut raw: RawNodeId = [0_u8; 32]; + raw[..std::cmp::min(32, raw_input.len())].copy_from_slice(raw_input); + + Ok(Self { raw }) + } + + /// Generates a random `NodeId`. + #[must_use] + pub fn random() -> Self { + Self { + raw: rand::random(), + } + } + + /// Returns a `RawNodeId` which is a 32 byte list. + #[must_use] + pub const fn raw(&self) -> RawNodeId { + self.raw + } +} + +impl From for NodeId { + fn from(public_key: T) -> Self { + Self::parse(&digest(public_key.encode_uncompressed().as_ref())) + .expect("always of correct length; qed") + } +} + +impl From> for NodeId { + fn from(enr: Enr) -> Self { + enr.node_id() + } +} + +impl From<&Enr> for NodeId { + fn from(enr: &Enr) -> Self { + enr.node_id() + } +} + +impl std::fmt::Display for NodeId { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let hex_encode = hex::encode(self.raw); + write!( + f, + "0x{}..{}", + &hex_encode[0..4], + &hex_encode[hex_encode.len() - 4..] + ) + } +} diff --git a/version-meld/eth2_ssz/.cargo-ok b/version-meld/eth2_ssz/.cargo-ok new file mode 100644 index 0000000..b5754e2 --- /dev/null +++ b/version-meld/eth2_ssz/.cargo-ok @@ -0,0 +1 @@ +ok \ No newline at end of file diff --git a/version-meld/eth2_ssz/.cargo_vcs_info.json b/version-meld/eth2_ssz/.cargo_vcs_info.json new file mode 100644 index 0000000..6292b69 --- /dev/null +++ b/version-meld/eth2_ssz/.cargo_vcs_info.json @@ -0,0 +1,5 @@ +{ + "git": { + "sha1": "0b319d492695daf11cd8fc0712b602b63ee5ed50" + } +} diff --git a/version-meld/eth2_ssz/Cargo.lock b/version-meld/eth2_ssz/Cargo.lock new file mode 100644 index 0000000..f94addf --- /dev/null +++ b/version-meld/eth2_ssz/Cargo.lock @@ -0,0 +1,473 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "arbitrary" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db55d72333851e17d572bec876e390cd3b11eb1ef53ae821dd9f3b653d2b4569" + +[[package]] +name = "arbitrary" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "510c76ecefdceada737ea728f4f9a84bd2e1ef29f1ba555e560940fe279954de" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "bitvec" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7774144344a4faa177370406a7ff5f1da24303817368584c6206c8303eb07848" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "byte-slice-cast" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c751592b77c499e7bce34d99d67c2c11bdc0574e9a488ddade14150a4698" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "darling" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757c0ded2af11d8e739c4daea1ac623dd1624b06c844cf3f5a39f1bdbd99bb12" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c34d8efb62d0c2d7f60ece80f75e5c63c1588ba68032740494b0b9a996466e3" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade7bff147130fe5e6d39f089c6bd49ec0250f35d70b2eebf72afdfc919f15cc" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "eth2_ssz" +version = "0.4.0" +dependencies = [ + "eth2_ssz_derive", + "ethereum-types", + "smallvec", +] + +[[package]] +name = "eth2_ssz_derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "635b86d2c941bb71e7419a571e1763d65c93e51a1bafc400352e3bef6ff59fc9" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ethbloom" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb684ac8fa8f6c5759f788862bb22ec6fe3cb392f6bfd08e3c64b603661e3f8" +dependencies = [ + "crunchy", + "fixed-hash", + "impl-rlp", + "impl-serde", + "tiny-keccak", +] + +[[package]] +name = "ethereum-types" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05136f7057fe789f06e6d41d07b34e6f70d8c86e5693b60f97aaa6553553bdaf" +dependencies = [ + "ethbloom", + "fixed-hash", + "impl-rlp", + "impl-serde", + "primitive-types", + "uint", +] + +[[package]] +name = "fixed-hash" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" +dependencies = [ + "arbitrary 0.4.7", + "byteorder", + "rand", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "impl-codec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161ebdfec3c8e3b52bf61c4f3550a1eea4f9579d10dc1b936f3171ebdcd6c443" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-rlp" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" +dependencies = [ + "rlp", +] + +[[package]] +name = "impl-serde" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4551f042f3438e64dbd6226b20527fc84a6e1fe65688b58746a2f53623f25f5c" +dependencies = [ + "serde", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5dacb10c5b3bb92d46ba347505a9041e676bb20ad220101326bffb0c93031ee" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "libc" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119" + +[[package]] +name = "parity-scale-codec" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373b1a4c1338d9cd3d1fa53b3a11bdab5ab6bd80a20f7f7becd76953ae2be909" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1557010476e0595c9b568d16dcfb81b93cdeb157612726f5170d31aa707bed27" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" + +[[package]] +name = "primitive-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05e4722c697a58a99d5d06a08c30821d7c082a4632198de1eaa5a6c22ef42373" +dependencies = [ + "fixed-hash", + "impl-codec", + "impl-rlp", + "impl-serde", + "uint", +] + +[[package]] +name = "proc-macro-crate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" +dependencies = [ + "thiserror", + "toml", +] + +[[package]] +name = "proc-macro2" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" + +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rlp" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "999508abb0ae792aabed2460c45b89106d97fe4adac593bdaef433c2605847b5" +dependencies = [ + "bytes", + "rustc-hex", +] + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "serde" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" + +[[package]] +name = "smallvec" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "uint" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6470ab50f482bde894a037a57064480a246dbfdd5960bd65a44824693f08da5f" +dependencies = [ + "arbitrary 1.0.3", + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" diff --git a/version-meld/eth2_ssz/Cargo.toml b/version-meld/eth2_ssz/Cargo.toml new file mode 100644 index 0000000..06596b9 --- /dev/null +++ b/version-meld/eth2_ssz/Cargo.toml @@ -0,0 +1,31 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2018" +name = "eth2_ssz" +version = "0.4.0" +authors = ["Paul Hauner "] +description = "SimpleSerialize (SSZ) as used in Ethereum 2.0" +license = "Apache-2.0" + +[lib] +name = "ssz" +[dependencies.ethereum-types] +version = "0.14.1" + +[dependencies.smallvec] +version = "1.6.1" +[dev-dependencies.eth2_ssz_derive] +version = "0.3.0" + +[features] +arbitrary = ["ethereum-types/arbitrary"] diff --git a/version-meld/eth2_ssz/Cargo.toml.orig b/version-meld/eth2_ssz/Cargo.toml.orig new file mode 100644 index 0000000..853fd72 --- /dev/null +++ b/version-meld/eth2_ssz/Cargo.toml.orig @@ -0,0 +1,20 @@ +[package] +name = "eth2_ssz" +version = "0.4.0" +authors = ["Paul Hauner "] +edition = "2018" +description = "SimpleSerialize (SSZ) as used in Ethereum 2.0" +license = "Apache-2.0" + +[lib] +name = "ssz" + +[dev-dependencies] +eth2_ssz_derive = "0.3.0" + +[dependencies] +ethereum-types = "0.12.1" +smallvec = "1.6.1" + +[features] +arbitrary = ["ethereum-types/arbitrary"] diff --git a/version-meld/eth2_ssz/README.md b/version-meld/eth2_ssz/README.md new file mode 100644 index 0000000..04603cd --- /dev/null +++ b/version-meld/eth2_ssz/README.md @@ -0,0 +1,3 @@ +# simpleserialize (ssz) + +[](https://crates.io/crates/eth2_ssz) diff --git a/version-meld/eth2_ssz/examples/large_list.rs b/version-meld/eth2_ssz/examples/large_list.rs new file mode 100644 index 0000000..a1b10ab --- /dev/null +++ b/version-meld/eth2_ssz/examples/large_list.rs @@ -0,0 +1,15 @@ +//! Encode and decode a list many times. +//! +//! Useful for `cargo flamegraph`. + +use ssz::{Decode, Encode}; + +fn main() { + let vec: Vec = vec![4242; 8196]; + + let output: Vec> = (0..40_000) + .map(|_| Vec::from_ssz_bytes(&vec.as_ssz_bytes()).unwrap()) + .collect(); + + println!("{}", output.len()); +} diff --git a/version-meld/eth2_ssz/examples/large_list_of_structs.rs b/version-meld/eth2_ssz/examples/large_list_of_structs.rs new file mode 100644 index 0000000..2aaaf9b --- /dev/null +++ b/version-meld/eth2_ssz/examples/large_list_of_structs.rs @@ -0,0 +1,31 @@ +//! Encode and decode a list many times. +//! +//! Useful for `cargo flamegraph`. + +use ssz::{Decode, Encode}; +use ssz_derive::{Decode, Encode}; + +#[derive(Clone, Copy, Encode, Decode)] +pub struct FixedLen { + a: u64, + b: u64, + c: u64, + d: u64, +} + +fn main() { + let fixed_len = FixedLen { + a: 42, + b: 42, + c: 42, + d: 42, + }; + + let vec: Vec = vec![fixed_len; 8196]; + + let output: Vec> = (0..40_000) + .map(|_| Vec::from_ssz_bytes(&vec.as_ssz_bytes()).unwrap()) + .collect(); + + println!("{}", output.len()); +} diff --git a/version-meld/eth2_ssz/examples/struct_definition.rs b/version-meld/eth2_ssz/examples/struct_definition.rs new file mode 100644 index 0000000..123da12 --- /dev/null +++ b/version-meld/eth2_ssz/examples/struct_definition.rs @@ -0,0 +1,73 @@ +use ssz::{Decode, DecodeError, Encode, SszDecoderBuilder, SszEncoder}; + +#[derive(Debug, PartialEq)] +pub struct Foo { + a: u16, + b: Vec, + c: u16, +} + +impl Encode for Foo { + fn is_ssz_fixed_len() -> bool { + ::is_ssz_fixed_len() && as Encode>::is_ssz_fixed_len() + } + + fn ssz_bytes_len(&self) -> usize { + ::ssz_fixed_len() + + ssz::BYTES_PER_LENGTH_OFFSET + + ::ssz_fixed_len() + + self.b.ssz_bytes_len() + } + + fn ssz_append(&self, buf: &mut Vec) { + let offset = ::ssz_fixed_len() + + as Encode>::ssz_fixed_len() + + ::ssz_fixed_len(); + + let mut encoder = SszEncoder::container(buf, offset); + + encoder.append(&self.a); + encoder.append(&self.b); + encoder.append(&self.c); + + encoder.finalize(); + } +} + +impl Decode for Foo { + fn is_ssz_fixed_len() -> bool { + ::is_ssz_fixed_len() && as Decode>::is_ssz_fixed_len() + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let mut builder = SszDecoderBuilder::new(bytes); + + builder.register_type::()?; + builder.register_type::>()?; + builder.register_type::()?; + + let mut decoder = builder.build()?; + + Ok(Self { + a: decoder.decode_next()?, + b: decoder.decode_next()?, + c: decoder.decode_next()?, + }) + } +} + +fn main() { + let my_foo = Foo { + a: 42, + b: vec![0, 1, 2, 3], + c: 11, + }; + + let bytes = vec![42, 0, 8, 0, 0, 0, 11, 0, 0, 1, 2, 3]; + + assert_eq!(my_foo.as_ssz_bytes(), bytes); + + let decoded_foo = Foo::from_ssz_bytes(&bytes).unwrap(); + + assert_eq!(my_foo, decoded_foo); +} diff --git a/version-meld/eth2_ssz/src/decode.rs b/version-meld/eth2_ssz/src/decode.rs new file mode 100644 index 0000000..1c4c04f --- /dev/null +++ b/version-meld/eth2_ssz/src/decode.rs @@ -0,0 +1,372 @@ +use super::*; +use smallvec::{smallvec, SmallVec}; +use std::cmp::Ordering; + +type SmallVec8 = SmallVec<[T; 8]>; + +pub mod impls; + +/// Returned when SSZ decoding fails. +#[derive(Debug, PartialEq, Clone)] +pub enum DecodeError { + /// The bytes supplied were too short to be decoded into the specified type. + InvalidByteLength { len: usize, expected: usize }, + /// The given bytes were too short to be read as a length prefix. + InvalidLengthPrefix { len: usize, expected: usize }, + /// A length offset pointed to a byte that was out-of-bounds (OOB). + /// + /// A bytes may be OOB for the following reasons: + /// + /// - It is `>= bytes.len()`. + /// - When decoding variable length items, the 1st offset points "backwards" into the fixed + /// length items (i.e., `length[0] < BYTES_PER_LENGTH_OFFSET`). + /// - When decoding variable-length items, the `n`'th offset was less than the `n-1`'th offset. + OutOfBoundsByte { i: usize }, + /// An offset points “backwards” into the fixed-bytes portion of the message, essentially + /// double-decoding bytes that will also be decoded as fixed-length. + /// + /// https://notes.ethereum.org/ruKvDXl6QOW3gnqVYb8ezA?view#1-Offset-into-fixed-portion + OffsetIntoFixedPortion(usize), + /// The first offset does not point to the byte that follows the fixed byte portion, + /// essentially skipping a variable-length byte. + /// + /// https://notes.ethereum.org/ruKvDXl6QOW3gnqVYb8ezA?view#2-Skip-first-variable-byte + OffsetSkipsVariableBytes(usize), + /// An offset points to bytes prior to the previous offset. Depending on how you look at it, + /// this either double-decodes bytes or makes the first offset a negative-length. + /// + /// https://notes.ethereum.org/ruKvDXl6QOW3gnqVYb8ezA?view#3-Offsets-are-decreasing + OffsetsAreDecreasing(usize), + /// An offset references byte indices that do not exist in the source bytes. + /// + /// https://notes.ethereum.org/ruKvDXl6QOW3gnqVYb8ezA?view#4-Offsets-are-out-of-bounds + OffsetOutOfBounds(usize), + /// A variable-length list does not have a fixed portion that is cleanly divisible by + /// `BYTES_PER_LENGTH_OFFSET`. + InvalidListFixedBytesLen(usize), + /// Some item has a `ssz_fixed_len` of zero. This is illegal. + ZeroLengthItem, + /// The given bytes were invalid for some application-level reason. + BytesInvalid(String), + /// The given union selector is out of bounds. + UnionSelectorInvalid(u8), +} + +/// Performs checks on the `offset` based upon the other parameters provided. +/// +/// ## Detail +/// +/// - `offset`: the offset bytes (e.g., result of `read_offset(..)`). +/// - `previous_offset`: unless this is the first offset in the SSZ object, the value of the +/// previously-read offset. Used to ensure offsets are not decreasing. +/// - `num_bytes`: the total number of bytes in the SSZ object. Used to ensure the offset is not +/// out of bounds. +/// - `num_fixed_bytes`: the number of fixed-bytes in the struct, if it is known. Used to ensure +/// that the first offset doesn't skip any variable bytes. +/// +/// ## References +/// +/// The checks here are derived from this document: +/// +/// https://notes.ethereum.org/ruKvDXl6QOW3gnqVYb8ezA?view +pub fn sanitize_offset( + offset: usize, + previous_offset: Option, + num_bytes: usize, + num_fixed_bytes: Option, +) -> Result { + if num_fixed_bytes.map_or(false, |fixed_bytes| offset < fixed_bytes) { + Err(DecodeError::OffsetIntoFixedPortion(offset)) + } else if previous_offset.is_none() + && num_fixed_bytes.map_or(false, |fixed_bytes| offset != fixed_bytes) + { + Err(DecodeError::OffsetSkipsVariableBytes(offset)) + } else if offset > num_bytes { + Err(DecodeError::OffsetOutOfBounds(offset)) + } else if previous_offset.map_or(false, |prev| prev > offset) { + Err(DecodeError::OffsetsAreDecreasing(offset)) + } else { + Ok(offset) + } +} + +/// Provides SSZ decoding (de-serialization) via the `from_ssz_bytes(&bytes)` method. +/// +/// See `examples/` for manual implementations or the crate root for implementations using +/// `#[derive(Decode)]`. +pub trait Decode: Sized { + /// Returns `true` if this object has a fixed-length. + /// + /// I.e., there are no variable length items in this object or any of it's contained objects. + fn is_ssz_fixed_len() -> bool; + + /// The number of bytes this object occupies in the fixed-length portion of the SSZ bytes. + /// + /// By default, this is set to `BYTES_PER_LENGTH_OFFSET` which is suitable for variable length + /// objects, but not fixed-length objects. Fixed-length objects _must_ return a value which + /// represents their length. + fn ssz_fixed_len() -> usize { + BYTES_PER_LENGTH_OFFSET + } + + /// Attempts to decode `Self` from `bytes`, returning a `DecodeError` on failure. + /// + /// The supplied bytes must be the exact length required to decode `Self`, excess bytes will + /// result in an error. + fn from_ssz_bytes(bytes: &[u8]) -> Result; +} + +#[derive(Copy, Clone, Debug)] +pub struct Offset { + position: usize, + offset: usize, +} + +/// Builds an `SszDecoder`. +/// +/// The purpose of this struct is to split some SSZ bytes into individual slices. The builder is +/// then converted into a `SszDecoder` which decodes those values into object instances. +/// +/// See [`SszDecoder`](struct.SszDecoder.html) for usage examples. +pub struct SszDecoderBuilder<'a> { + bytes: &'a [u8], + items: SmallVec8<&'a [u8]>, + offsets: SmallVec8, + items_index: usize, +} + +impl<'a> SszDecoderBuilder<'a> { + /// Instantiate a new builder that should build a `SszDecoder` over the given `bytes` which + /// are assumed to be the SSZ encoding of some object. + pub fn new(bytes: &'a [u8]) -> Self { + Self { + bytes, + items: smallvec![], + offsets: smallvec![], + items_index: 0, + } + } + + /// Registers a variable-length object as the next item in `bytes`, without specifying the + /// actual type. + /// + /// ## Notes + /// + /// Use of this function is generally discouraged since it cannot detect if some type changes + /// from variable to fixed length. + /// + /// Use `Self::register_type` wherever possible. + pub fn register_anonymous_variable_length_item(&mut self) -> Result<(), DecodeError> { + struct Anonymous; + + impl Decode for Anonymous { + fn is_ssz_fixed_len() -> bool { + false + } + + fn from_ssz_bytes(_bytes: &[u8]) -> Result { + unreachable!("Anonymous should never be decoded") + } + } + + self.register_type::() + } + + /// Declares that some type `T` is the next item in `bytes`. + pub fn register_type(&mut self) -> Result<(), DecodeError> { + self.register_type_parameterized(T::is_ssz_fixed_len(), T::ssz_fixed_len()) + } + + /// Declares that a type with the given parameters is the next item in `bytes`. + pub fn register_type_parameterized( + &mut self, + is_ssz_fixed_len: bool, + ssz_fixed_len: usize, + ) -> Result<(), DecodeError> { + if is_ssz_fixed_len { + let start = self.items_index; + self.items_index += ssz_fixed_len; + + let slice = self.bytes.get(start..self.items_index).ok_or_else(|| { + DecodeError::InvalidByteLength { + len: self.bytes.len(), + expected: self.items_index, + } + })?; + + self.items.push(slice); + } else { + self.offsets.push(Offset { + position: self.items.len(), + offset: sanitize_offset( + read_offset(&self.bytes[self.items_index..])?, + self.offsets.last().map(|o| o.offset), + self.bytes.len(), + None, + )?, + }); + + // Push an empty slice into items; it will be replaced later. + self.items.push(&[]); + + self.items_index += BYTES_PER_LENGTH_OFFSET; + } + + Ok(()) + } + + fn finalize(&mut self) -> Result<(), DecodeError> { + if let Some(first_offset) = self.offsets.first().map(|o| o.offset) { + // Check to ensure the first offset points to the byte immediately following the + // fixed-length bytes. + match first_offset.cmp(&self.items_index) { + Ordering::Less => return Err(DecodeError::OffsetIntoFixedPortion(first_offset)), + Ordering::Greater => { + return Err(DecodeError::OffsetSkipsVariableBytes(first_offset)) + } + Ordering::Equal => (), + } + + // Iterate through each pair of offsets, grabbing the slice between each of the offsets. + for pair in self.offsets.windows(2) { + let a = pair[0]; + let b = pair[1]; + + self.items[a.position] = &self.bytes[a.offset..b.offset]; + } + + // Handle the last offset, pushing a slice from it's start through to the end of + // `self.bytes`. + if let Some(last) = self.offsets.last() { + self.items[last.position] = &self.bytes[last.offset..] + } + } else { + // If the container is fixed-length, ensure there are no excess bytes. + if self.items_index != self.bytes.len() { + return Err(DecodeError::InvalidByteLength { + len: self.bytes.len(), + expected: self.items_index, + }); + } + } + + Ok(()) + } + + /// Finalizes the builder, returning a `SszDecoder` that may be used to instantiate objects. + pub fn build(mut self) -> Result, DecodeError> { + self.finalize()?; + + Ok(SszDecoder { items: self.items }) + } +} + +/// Decodes some slices of SSZ into object instances. Should be instantiated using +/// [`SszDecoderBuilder`](struct.SszDecoderBuilder.html). +/// +/// ## Example +/// +/// ```rust +/// use ssz_derive::{Encode, Decode}; +/// use ssz::{Decode, Encode, SszDecoder, SszDecoderBuilder}; +/// +/// #[derive(PartialEq, Debug, Encode, Decode)] +/// struct Foo { +/// a: u64, +/// b: Vec, +/// } +/// +/// fn ssz_decoding_example() { +/// let foo = Foo { +/// a: 42, +/// b: vec![1, 3, 3, 7] +/// }; +/// +/// let bytes = foo.as_ssz_bytes(); +/// +/// let mut builder = SszDecoderBuilder::new(&bytes); +/// +/// builder.register_type::().unwrap(); +/// builder.register_type::>().unwrap(); +/// +/// let mut decoder = builder.build().unwrap(); +/// +/// let decoded_foo = Foo { +/// a: decoder.decode_next().unwrap(), +/// b: decoder.decode_next().unwrap(), +/// }; +/// +/// assert_eq!(foo, decoded_foo); +/// } +/// +/// ``` +pub struct SszDecoder<'a> { + items: SmallVec8<&'a [u8]>, +} + +impl<'a> SszDecoder<'a> { + /// Decodes the next item. + /// + /// # Panics + /// + /// Panics when attempting to decode more items than actually exist. + pub fn decode_next(&mut self) -> Result { + self.decode_next_with(|slice| T::from_ssz_bytes(slice)) + } + + /// Decodes the next item using the provided function. + pub fn decode_next_with(&mut self, f: F) -> Result + where + F: FnOnce(&'a [u8]) -> Result, + { + f(self.items.remove(0)) + } +} + +/// Takes `bytes`, assuming it is the encoding for a SSZ union, and returns the union-selector and +/// the body (trailing bytes). +/// +/// ## Errors +/// +/// Returns an error if: +/// +/// - `bytes` is empty. +/// - the union selector is not a valid value (i.e., larger than the maximum number of variants. +pub fn split_union_bytes(bytes: &[u8]) -> Result<(UnionSelector, &[u8]), DecodeError> { + let selector = bytes + .first() + .copied() + .ok_or(DecodeError::OutOfBoundsByte { i: 0 }) + .and_then(UnionSelector::new)?; + let body = bytes + .get(1..) + .ok_or(DecodeError::OutOfBoundsByte { i: 1 })?; + Ok((selector, body)) +} + +/// Reads a `BYTES_PER_LENGTH_OFFSET`-byte length from `bytes`, where `bytes.len() >= +/// BYTES_PER_LENGTH_OFFSET`. +pub fn read_offset(bytes: &[u8]) -> Result { + decode_offset(bytes.get(0..BYTES_PER_LENGTH_OFFSET).ok_or_else(|| { + DecodeError::InvalidLengthPrefix { + len: bytes.len(), + expected: BYTES_PER_LENGTH_OFFSET, + } + })?) +} + +/// Decode bytes as a little-endian usize, returning an `Err` if `bytes.len() != +/// BYTES_PER_LENGTH_OFFSET`. +fn decode_offset(bytes: &[u8]) -> Result { + let len = bytes.len(); + let expected = BYTES_PER_LENGTH_OFFSET; + + if len != expected { + Err(DecodeError::InvalidLengthPrefix { len, expected }) + } else { + let mut array: [u8; BYTES_PER_LENGTH_OFFSET] = std::default::Default::default(); + array.clone_from_slice(bytes); + + Ok(u32::from_le_bytes(array) as usize) + } +} diff --git a/version-meld/eth2_ssz/src/decode/impls.rs b/version-meld/eth2_ssz/src/decode/impls.rs new file mode 100644 index 0000000..29b2aec --- /dev/null +++ b/version-meld/eth2_ssz/src/decode/impls.rs @@ -0,0 +1,697 @@ +use super::*; +use core::num::NonZeroUsize; +use ethereum_types::{H256, U128, U256}; +use smallvec::SmallVec; +use std::sync::Arc; + +macro_rules! impl_decodable_for_uint { + ($type: ident, $bit_size: expr) => { + impl Decode for $type { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + $bit_size / 8 + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let len = bytes.len(); + let expected = ::ssz_fixed_len(); + + if len != expected { + Err(DecodeError::InvalidByteLength { len, expected }) + } else { + let mut array: [u8; $bit_size / 8] = std::default::Default::default(); + array.clone_from_slice(bytes); + + Ok(Self::from_le_bytes(array)) + } + } + } + }; +} + +impl_decodable_for_uint!(u8, 8); +impl_decodable_for_uint!(u16, 16); +impl_decodable_for_uint!(u32, 32); +impl_decodable_for_uint!(u64, 64); + +#[cfg(target_pointer_width = "32")] +impl_decodable_for_uint!(usize, 32); + +#[cfg(target_pointer_width = "64")] +impl_decodable_for_uint!(usize, 64); + +macro_rules! impl_decode_for_tuples { + ($( + $Tuple:ident { + $(($idx:tt) -> $T:ident)+ + } + )+) => { + $( + impl<$($T: Decode),+> Decode for ($($T,)+) { + fn is_ssz_fixed_len() -> bool { + $( + <$T as Decode>::is_ssz_fixed_len() && + )* + true + } + + fn ssz_fixed_len() -> usize { + if ::is_ssz_fixed_len() { + $( + <$T as Decode>::ssz_fixed_len() + + )* + 0 + } else { + BYTES_PER_LENGTH_OFFSET + } + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let mut builder = SszDecoderBuilder::new(bytes); + + $( + builder.register_type::<$T>()?; + )* + + let mut decoder = builder.build()?; + + Ok(($( + decoder.decode_next::<$T>()?, + )* + )) + } + } + )+ + } +} + +impl_decode_for_tuples! { + Tuple2 { + (0) -> A + (1) -> B + } + Tuple3 { + (0) -> A + (1) -> B + (2) -> C + } + Tuple4 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + } + Tuple5 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + } + Tuple6 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + } + Tuple7 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + } + Tuple8 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + } + Tuple9 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + (8) -> I + } + Tuple10 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + (8) -> I + (9) -> J + } + Tuple11 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + (8) -> I + (9) -> J + (10) -> K + } + Tuple12 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + (8) -> I + (9) -> J + (10) -> K + (11) -> L + } +} + +impl Decode for bool { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 1 + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let len = bytes.len(); + let expected = ::ssz_fixed_len(); + + if len != expected { + Err(DecodeError::InvalidByteLength { len, expected }) + } else { + match bytes[0] { + 0b0000_0000 => Ok(false), + 0b0000_0001 => Ok(true), + _ => Err(DecodeError::BytesInvalid(format!( + "Out-of-range for boolean: {}", + bytes[0] + ))), + } + } + } +} + +impl Decode for NonZeroUsize { + fn is_ssz_fixed_len() -> bool { + ::is_ssz_fixed_len() + } + + fn ssz_fixed_len() -> usize { + ::ssz_fixed_len() + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let x = usize::from_ssz_bytes(bytes)?; + + if x == 0 { + Err(DecodeError::BytesInvalid( + "NonZeroUsize cannot be zero.".to_string(), + )) + } else { + // `unwrap` is safe here as `NonZeroUsize::new()` succeeds if `x > 0` and this path + // never executes when `x == 0`. + Ok(NonZeroUsize::new(x).unwrap()) + } + } +} + +impl Decode for Arc { + fn is_ssz_fixed_len() -> bool { + T::is_ssz_fixed_len() + } + + fn ssz_fixed_len() -> usize { + T::ssz_fixed_len() + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + T::from_ssz_bytes(bytes).map(Arc::new) + } +} + +impl Decode for H256 { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 32 + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let len = bytes.len(); + let expected = ::ssz_fixed_len(); + + if len != expected { + Err(DecodeError::InvalidByteLength { len, expected }) + } else { + Ok(H256::from_slice(bytes)) + } + } +} + +impl Decode for U256 { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 32 + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let len = bytes.len(); + let expected = ::ssz_fixed_len(); + + if len != expected { + Err(DecodeError::InvalidByteLength { len, expected }) + } else { + Ok(U256::from_little_endian(bytes)) + } + } +} + +impl Decode for U128 { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 16 + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let len = bytes.len(); + let expected = ::ssz_fixed_len(); + + if len != expected { + Err(DecodeError::InvalidByteLength { len, expected }) + } else { + Ok(U128::from_little_endian(bytes)) + } + } +} + +macro_rules! impl_decodable_for_u8_array { + ($len: expr) => { + impl Decode for [u8; $len] { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + $len + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let len = bytes.len(); + let expected = ::ssz_fixed_len(); + + if len != expected { + Err(DecodeError::InvalidByteLength { len, expected }) + } else { + let mut array: [u8; $len] = [0; $len]; + array.copy_from_slice(bytes); + + Ok(array) + } + } + } + }; +} + +impl_decodable_for_u8_array!(4); +impl_decodable_for_u8_array!(32); + +macro_rules! impl_for_vec { + ($type: ty, $max_len: expr) => { + impl Decode for $type { + fn is_ssz_fixed_len() -> bool { + false + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + if bytes.is_empty() { + Ok(vec![].into()) + } else if T::is_ssz_fixed_len() { + bytes + .chunks(T::ssz_fixed_len()) + .map(|chunk| T::from_ssz_bytes(chunk)) + .collect() + } else { + decode_list_of_variable_length_items(bytes, $max_len).map(|vec| vec.into()) + } + } + } + }; +} + +impl_for_vec!(Vec, None); +impl_for_vec!(SmallVec<[T; 1]>, Some(1)); +impl_for_vec!(SmallVec<[T; 2]>, Some(2)); +impl_for_vec!(SmallVec<[T; 3]>, Some(3)); +impl_for_vec!(SmallVec<[T; 4]>, Some(4)); +impl_for_vec!(SmallVec<[T; 5]>, Some(5)); +impl_for_vec!(SmallVec<[T; 6]>, Some(6)); +impl_for_vec!(SmallVec<[T; 7]>, Some(7)); +impl_for_vec!(SmallVec<[T; 8]>, Some(8)); + +/// Decodes `bytes` as if it were a list of variable-length items. +/// +/// The `ssz::SszDecoder` can also perform this functionality, however it it significantly faster +/// as it is optimized to read same-typed items whilst `ssz::SszDecoder` supports reading items of +/// differing types. +pub fn decode_list_of_variable_length_items( + bytes: &[u8], + max_len: Option, +) -> Result, DecodeError> { + if bytes.is_empty() { + return Ok(vec![]); + } + + let first_offset = read_offset(bytes)?; + sanitize_offset(first_offset, None, bytes.len(), Some(first_offset))?; + + if first_offset % BYTES_PER_LENGTH_OFFSET != 0 || first_offset < BYTES_PER_LENGTH_OFFSET { + return Err(DecodeError::InvalidListFixedBytesLen(first_offset)); + } + + let num_items = first_offset / BYTES_PER_LENGTH_OFFSET; + + if max_len.map_or(false, |max| num_items > max) { + return Err(DecodeError::BytesInvalid(format!( + "Variable length list of {} items exceeds maximum of {:?}", + num_items, max_len + ))); + } + + // Only initialize the vec with a capacity if a maximum length is provided. + // + // We assume that if a max length is provided then the application is able to handle an + // allocation of this size. + let mut values = if max_len.is_some() { + Vec::with_capacity(num_items) + } else { + vec![] + }; + + let mut offset = first_offset; + for i in 1..=num_items { + let slice_option = if i == num_items { + bytes.get(offset..) + } else { + let start = offset; + + let next_offset = read_offset(&bytes[(i * BYTES_PER_LENGTH_OFFSET)..])?; + offset = sanitize_offset(next_offset, Some(offset), bytes.len(), Some(first_offset))?; + + bytes.get(start..offset) + }; + + let slice = slice_option.ok_or(DecodeError::OutOfBoundsByte { i: offset })?; + + values.push(T::from_ssz_bytes(slice)?); + } + + Ok(values) +} + +#[cfg(test)] +mod tests { + use super::*; + + // Note: decoding of valid bytes is generally tested "indirectly" in the `/tests` dir, by + // encoding then decoding the element. + + #[test] + fn invalid_u8_array_4() { + assert_eq!( + <[u8; 4]>::from_ssz_bytes(&[0; 3]), + Err(DecodeError::InvalidByteLength { + len: 3, + expected: 4 + }) + ); + + assert_eq!( + <[u8; 4]>::from_ssz_bytes(&[0; 5]), + Err(DecodeError::InvalidByteLength { + len: 5, + expected: 4 + }) + ); + } + + #[test] + fn invalid_bool() { + assert_eq!( + bool::from_ssz_bytes(&[0; 2]), + Err(DecodeError::InvalidByteLength { + len: 2, + expected: 1 + }) + ); + + assert_eq!( + bool::from_ssz_bytes(&[]), + Err(DecodeError::InvalidByteLength { + len: 0, + expected: 1 + }) + ); + + if let Err(DecodeError::BytesInvalid(_)) = bool::from_ssz_bytes(&[2]) { + // Success. + } else { + panic!("Did not return error on invalid bool val") + } + } + + #[test] + fn invalid_h256() { + assert_eq!( + H256::from_ssz_bytes(&[0; 33]), + Err(DecodeError::InvalidByteLength { + len: 33, + expected: 32 + }) + ); + + assert_eq!( + H256::from_ssz_bytes(&[0; 31]), + Err(DecodeError::InvalidByteLength { + len: 31, + expected: 32 + }) + ); + } + + #[test] + fn empty_list() { + let vec: Vec> = vec![]; + let bytes = vec.as_ssz_bytes(); + assert!(bytes.is_empty()); + assert_eq!(Vec::from_ssz_bytes(&bytes), Ok(vec),); + } + + #[test] + fn first_length_points_backwards() { + assert_eq!( + >>::from_ssz_bytes(&[0, 0, 0, 0]), + Err(DecodeError::InvalidListFixedBytesLen(0)) + ); + + assert_eq!( + >>::from_ssz_bytes(&[1, 0, 0, 0]), + Err(DecodeError::InvalidListFixedBytesLen(1)) + ); + + assert_eq!( + >>::from_ssz_bytes(&[2, 0, 0, 0]), + Err(DecodeError::InvalidListFixedBytesLen(2)) + ); + + assert_eq!( + >>::from_ssz_bytes(&[3, 0, 0, 0]), + Err(DecodeError::InvalidListFixedBytesLen(3)) + ); + } + + #[test] + fn lengths_are_decreasing() { + assert_eq!( + >>::from_ssz_bytes(&[12, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 1, 0, 1, 0]), + Err(DecodeError::OffsetsAreDecreasing(12)) + ); + } + + #[test] + fn awkward_fixed_length_portion() { + assert_eq!( + >>::from_ssz_bytes(&[10, 0, 0, 0, 10, 0, 0, 0, 0, 0]), + Err(DecodeError::InvalidListFixedBytesLen(10)) + ); + } + + #[test] + fn length_out_of_bounds() { + assert_eq!( + >>::from_ssz_bytes(&[5, 0, 0, 0]), + Err(DecodeError::OffsetOutOfBounds(5)) + ); + assert_eq!( + >>::from_ssz_bytes(&[8, 0, 0, 0, 9, 0, 0, 0]), + Err(DecodeError::OffsetOutOfBounds(9)) + ); + assert_eq!( + >>::from_ssz_bytes(&[8, 0, 0, 0, 16, 0, 0, 0]), + Err(DecodeError::OffsetOutOfBounds(16)) + ); + } + + #[test] + fn vec_of_vec_of_u16() { + assert_eq!( + >>::from_ssz_bytes(&[4, 0, 0, 0]), + Ok(vec![vec![]]) + ); + + assert_eq!( + >::from_ssz_bytes(&[0, 0, 1, 0, 2, 0, 3, 0]), + Ok(vec![0, 1, 2, 3]) + ); + assert_eq!(::from_ssz_bytes(&[16, 0]), Ok(16)); + assert_eq!(::from_ssz_bytes(&[0, 1]), Ok(256)); + assert_eq!(::from_ssz_bytes(&[255, 255]), Ok(65535)); + + assert_eq!( + ::from_ssz_bytes(&[255]), + Err(DecodeError::InvalidByteLength { + len: 1, + expected: 2 + }) + ); + + assert_eq!( + ::from_ssz_bytes(&[]), + Err(DecodeError::InvalidByteLength { + len: 0, + expected: 2 + }) + ); + + assert_eq!( + ::from_ssz_bytes(&[0, 1, 2]), + Err(DecodeError::InvalidByteLength { + len: 3, + expected: 2 + }) + ); + } + + #[test] + fn vec_of_u16() { + assert_eq!(>::from_ssz_bytes(&[0, 0, 0, 0]), Ok(vec![0, 0])); + assert_eq!( + >::from_ssz_bytes(&[0, 0, 1, 0, 2, 0, 3, 0]), + Ok(vec![0, 1, 2, 3]) + ); + assert_eq!(::from_ssz_bytes(&[16, 0]), Ok(16)); + assert_eq!(::from_ssz_bytes(&[0, 1]), Ok(256)); + assert_eq!(::from_ssz_bytes(&[255, 255]), Ok(65535)); + + assert_eq!( + ::from_ssz_bytes(&[255]), + Err(DecodeError::InvalidByteLength { + len: 1, + expected: 2 + }) + ); + + assert_eq!( + ::from_ssz_bytes(&[]), + Err(DecodeError::InvalidByteLength { + len: 0, + expected: 2 + }) + ); + + assert_eq!( + ::from_ssz_bytes(&[0, 1, 2]), + Err(DecodeError::InvalidByteLength { + len: 3, + expected: 2 + }) + ); + } + + #[test] + fn u16() { + assert_eq!(::from_ssz_bytes(&[0, 0]), Ok(0)); + assert_eq!(::from_ssz_bytes(&[16, 0]), Ok(16)); + assert_eq!(::from_ssz_bytes(&[0, 1]), Ok(256)); + assert_eq!(::from_ssz_bytes(&[255, 255]), Ok(65535)); + + assert_eq!( + ::from_ssz_bytes(&[255]), + Err(DecodeError::InvalidByteLength { + len: 1, + expected: 2 + }) + ); + + assert_eq!( + ::from_ssz_bytes(&[]), + Err(DecodeError::InvalidByteLength { + len: 0, + expected: 2 + }) + ); + + assert_eq!( + ::from_ssz_bytes(&[0, 1, 2]), + Err(DecodeError::InvalidByteLength { + len: 3, + expected: 2 + }) + ); + } + + #[test] + fn tuple() { + assert_eq!(<(u16, u16)>::from_ssz_bytes(&[0, 0, 0, 0]), Ok((0, 0))); + assert_eq!(<(u16, u16)>::from_ssz_bytes(&[16, 0, 17, 0]), Ok((16, 17))); + assert_eq!(<(u16, u16)>::from_ssz_bytes(&[0, 1, 2, 0]), Ok((256, 2))); + assert_eq!( + <(u16, u16)>::from_ssz_bytes(&[255, 255, 0, 0]), + Ok((65535, 0)) + ); + } +} diff --git a/version-meld/eth2_ssz/src/encode.rs b/version-meld/eth2_ssz/src/encode.rs new file mode 100644 index 0000000..cecd615 --- /dev/null +++ b/version-meld/eth2_ssz/src/encode.rs @@ -0,0 +1,196 @@ +use super::*; + +mod impls; + +/// Provides SSZ encoding (serialization) via the `as_ssz_bytes(&self)` method. +/// +/// See `examples/` for manual implementations or the crate root for implementations using +/// `#[derive(Encode)]`. +pub trait Encode { + /// Returns `true` if this object has a fixed-length. + /// + /// I.e., there are no variable length items in this object or any of it's contained objects. + fn is_ssz_fixed_len() -> bool; + + /// Append the encoding `self` to `buf`. + /// + /// Note, variable length objects need only to append their "variable length" portion, they do + /// not need to provide their offset. + fn ssz_append(&self, buf: &mut Vec); + + /// The number of bytes this object occupies in the fixed-length portion of the SSZ bytes. + /// + /// By default, this is set to `BYTES_PER_LENGTH_OFFSET` which is suitable for variable length + /// objects, but not fixed-length objects. Fixed-length objects _must_ return a value which + /// represents their length. + fn ssz_fixed_len() -> usize { + BYTES_PER_LENGTH_OFFSET + } + + /// Returns the size (in bytes) when `self` is serialized. + /// + /// Returns the same value as `self.as_ssz_bytes().len()` but this method is significantly more + /// efficient. + fn ssz_bytes_len(&self) -> usize; + + /// Returns the full-form encoding of this object. + /// + /// The default implementation of this method should suffice for most cases. + fn as_ssz_bytes(&self) -> Vec { + let mut buf = vec![]; + + self.ssz_append(&mut buf); + + buf + } +} + +/// Allow for encoding an ordered series of distinct or indistinct objects as SSZ bytes. +/// +/// **You must call `finalize(..)` after the final `append(..)` call** to ensure the bytes are +/// written to `buf`. +/// +/// ## Example +/// +/// Use `SszEncoder` to produce identical output to `foo.as_ssz_bytes()`: +/// +/// ```rust +/// use ssz_derive::{Encode, Decode}; +/// use ssz::{Decode, Encode, SszEncoder}; +/// +/// #[derive(PartialEq, Debug, Encode, Decode)] +/// struct Foo { +/// a: u64, +/// b: Vec, +/// } +/// +/// fn ssz_encode_example() { +/// let foo = Foo { +/// a: 42, +/// b: vec![1, 3, 3, 7] +/// }; +/// +/// let mut buf: Vec = vec![]; +/// let offset = ::ssz_fixed_len() + as Encode>::ssz_fixed_len(); +/// +/// let mut encoder = SszEncoder::container(&mut buf, offset); +/// +/// encoder.append(&foo.a); +/// encoder.append(&foo.b); +/// +/// encoder.finalize(); +/// +/// assert_eq!(foo.as_ssz_bytes(), buf); +/// } +/// +/// ``` +pub struct SszEncoder<'a> { + offset: usize, + buf: &'a mut Vec, + variable_bytes: Vec, +} + +impl<'a> SszEncoder<'a> { + /// Instantiate a new encoder for encoding a SSZ container. + pub fn container(buf: &'a mut Vec, num_fixed_bytes: usize) -> Self { + buf.reserve(num_fixed_bytes); + + Self { + offset: num_fixed_bytes, + buf, + variable_bytes: vec![], + } + } + + /// Append some `item` to the SSZ bytes. + pub fn append(&mut self, item: &T) { + self.append_parameterized(T::is_ssz_fixed_len(), |buf| item.ssz_append(buf)) + } + + /// Uses `ssz_append` to append the encoding of some item to the SSZ bytes. + pub fn append_parameterized(&mut self, is_ssz_fixed_len: bool, ssz_append: F) + where + F: Fn(&mut Vec), + { + if is_ssz_fixed_len { + ssz_append(&mut self.buf); + } else { + self.buf + .extend_from_slice(&encode_length(self.offset + self.variable_bytes.len())); + + ssz_append(&mut self.variable_bytes); + } + } + + /// Write the variable bytes to `self.bytes`. + /// + /// This method must be called after the final `append(..)` call when serializing + /// variable-length items. + pub fn finalize(&mut self) -> &mut Vec { + self.buf.append(&mut self.variable_bytes); + + &mut self.buf + } +} + +/// Encode `len` as a little-endian byte array of `BYTES_PER_LENGTH_OFFSET` length. +/// +/// If `len` is larger than `2 ^ BYTES_PER_LENGTH_OFFSET`, a `debug_assert` is raised. +pub fn encode_length(len: usize) -> [u8; BYTES_PER_LENGTH_OFFSET] { + // Note: it is possible for `len` to be larger than what can be encoded in + // `BYTES_PER_LENGTH_OFFSET` bytes, triggering this debug assertion. + // + // These are the alternatives to using a `debug_assert` here: + // + // 1. Use `assert`. + // 2. Push an error to the caller (e.g., `Option` or `Result`). + // 3. Ignore it completely. + // + // I have avoided (1) because it's basically a choice between "produce invalid SSZ" or "kill + // the entire program". I figure it may be possible for an attacker to trigger this assert and + // take the program down -- I think producing invalid SSZ is a better option than this. + // + // I have avoided (2) because this error will need to be propagated upstream, making encoding a + // function which may fail. I don't think this is ergonomic and the upsides don't outweigh the + // downsides. + // + // I figure a `debug_assertion` is better than (3) as it will give us a change to detect the + // error during testing. + // + // If you have a different opinion, feel free to start an issue and tag @paulhauner. + debug_assert!(len <= MAX_LENGTH_VALUE); + + let mut bytes = [0; BYTES_PER_LENGTH_OFFSET]; + bytes.copy_from_slice(&len.to_le_bytes()[0..BYTES_PER_LENGTH_OFFSET]); + bytes +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_encode_length() { + assert_eq!(encode_length(0), [0; 4]); + + assert_eq!(encode_length(1), [1, 0, 0, 0]); + + assert_eq!( + encode_length(MAX_LENGTH_VALUE), + [255; BYTES_PER_LENGTH_OFFSET] + ); + } + + #[test] + #[should_panic] + #[cfg(debug_assertions)] + fn test_encode_length_above_max_debug_panics() { + encode_length(MAX_LENGTH_VALUE + 1); + } + + #[test] + #[cfg(not(debug_assertions))] + fn test_encode_length_above_max_not_debug_does_not_panic() { + assert_eq!(&encode_length(MAX_LENGTH_VALUE + 1)[..], &[0; 4]); + } +} diff --git a/version-meld/eth2_ssz/src/encode/impls.rs b/version-meld/eth2_ssz/src/encode/impls.rs new file mode 100644 index 0000000..00d3e0a --- /dev/null +++ b/version-meld/eth2_ssz/src/encode/impls.rs @@ -0,0 +1,504 @@ +use super::*; +use core::num::NonZeroUsize; +use ethereum_types::{H256, U128, U256}; +use smallvec::SmallVec; +use std::sync::Arc; + +macro_rules! impl_encodable_for_uint { + ($type: ident, $bit_size: expr) => { + impl Encode for $type { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + $bit_size / 8 + } + + fn ssz_bytes_len(&self) -> usize { + $bit_size / 8 + } + + fn ssz_append(&self, buf: &mut Vec) { + buf.extend_from_slice(&self.to_le_bytes()); + } + } + }; +} + +impl_encodable_for_uint!(u8, 8); +impl_encodable_for_uint!(u16, 16); +impl_encodable_for_uint!(u32, 32); +impl_encodable_for_uint!(u64, 64); + +#[cfg(target_pointer_width = "32")] +impl_encodable_for_uint!(usize, 32); + +#[cfg(target_pointer_width = "64")] +impl_encodable_for_uint!(usize, 64); + +// Based on the `tuple_impls` macro from the standard library. +macro_rules! impl_encode_for_tuples { + ($( + $Tuple:ident { + $(($idx:tt) -> $T:ident)+ + } + )+) => { + $( + impl<$($T: Encode),+> Encode for ($($T,)+) { + fn is_ssz_fixed_len() -> bool { + $( + <$T as Encode>::is_ssz_fixed_len() && + )* + true + } + + fn ssz_fixed_len() -> usize { + if ::is_ssz_fixed_len() { + $( + <$T as Encode>::ssz_fixed_len() + + )* + 0 + } else { + BYTES_PER_LENGTH_OFFSET + } + } + + fn ssz_bytes_len(&self) -> usize { + if ::is_ssz_fixed_len() { + ::ssz_fixed_len() + } else { + let mut len = 0; + $( + len += if <$T as Encode>::is_ssz_fixed_len() { + <$T as Encode>::ssz_fixed_len() + } else { + BYTES_PER_LENGTH_OFFSET + + self.$idx.ssz_bytes_len() + }; + )* + len + } + } + + fn ssz_append(&self, buf: &mut Vec) { + let offset = $( + <$T as Encode>::ssz_fixed_len() + + )* + 0; + + let mut encoder = SszEncoder::container(buf, offset); + + $( + encoder.append(&self.$idx); + )* + + encoder.finalize(); + } + } + )+ + } +} + +impl_encode_for_tuples! { + Tuple2 { + (0) -> A + (1) -> B + } + Tuple3 { + (0) -> A + (1) -> B + (2) -> C + } + Tuple4 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + } + Tuple5 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + } + Tuple6 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + } + Tuple7 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + } + Tuple8 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + } + Tuple9 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + (8) -> I + } + Tuple10 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + (8) -> I + (9) -> J + } + Tuple11 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + (8) -> I + (9) -> J + (10) -> K + } + Tuple12 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + (8) -> I + (9) -> J + (10) -> K + (11) -> L + } +} + +impl Encode for Arc { + fn is_ssz_fixed_len() -> bool { + T::is_ssz_fixed_len() + } + + fn ssz_fixed_len() -> usize { + T::ssz_fixed_len() + } + + fn ssz_append(&self, buf: &mut Vec) { + self.as_ref().ssz_append(buf) + } + + fn ssz_bytes_len(&self) -> usize { + self.as_ref().ssz_bytes_len() + } +} + +macro_rules! impl_for_vec { + ($type: ty) => { + impl Encode for $type { + fn is_ssz_fixed_len() -> bool { + false + } + + fn ssz_bytes_len(&self) -> usize { + if ::is_ssz_fixed_len() { + ::ssz_fixed_len() * self.len() + } else { + let mut len = self.iter().map(|item| item.ssz_bytes_len()).sum(); + len += BYTES_PER_LENGTH_OFFSET * self.len(); + len + } + } + + fn ssz_append(&self, buf: &mut Vec) { + if T::is_ssz_fixed_len() { + buf.reserve(T::ssz_fixed_len() * self.len()); + + for item in self { + item.ssz_append(buf); + } + } else { + let mut encoder = + SszEncoder::container(buf, self.len() * BYTES_PER_LENGTH_OFFSET); + + for item in self { + encoder.append(item); + } + + encoder.finalize(); + } + } + } + }; +} + +impl_for_vec!(Vec); +impl_for_vec!(SmallVec<[T; 1]>); +impl_for_vec!(SmallVec<[T; 2]>); +impl_for_vec!(SmallVec<[T; 3]>); +impl_for_vec!(SmallVec<[T; 4]>); +impl_for_vec!(SmallVec<[T; 5]>); +impl_for_vec!(SmallVec<[T; 6]>); +impl_for_vec!(SmallVec<[T; 7]>); +impl_for_vec!(SmallVec<[T; 8]>); + +impl Encode for bool { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 1 + } + + fn ssz_bytes_len(&self) -> usize { + 1 + } + + fn ssz_append(&self, buf: &mut Vec) { + buf.extend_from_slice(&(*self as u8).to_le_bytes()); + } +} + +impl Encode for NonZeroUsize { + fn is_ssz_fixed_len() -> bool { + ::is_ssz_fixed_len() + } + + fn ssz_fixed_len() -> usize { + ::ssz_fixed_len() + } + + fn ssz_bytes_len(&self) -> usize { + std::mem::size_of::() + } + + fn ssz_append(&self, buf: &mut Vec) { + self.get().ssz_append(buf) + } +} + +impl Encode for H256 { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 32 + } + + fn ssz_bytes_len(&self) -> usize { + 32 + } + + fn ssz_append(&self, buf: &mut Vec) { + buf.extend_from_slice(self.as_bytes()); + } +} + +impl Encode for U256 { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 32 + } + + fn ssz_bytes_len(&self) -> usize { + 32 + } + + fn ssz_append(&self, buf: &mut Vec) { + let n = ::ssz_fixed_len(); + let s = buf.len(); + + buf.resize(s + n, 0); + self.to_little_endian(&mut buf[s..]); + } +} + +impl Encode for U128 { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 16 + } + + fn ssz_bytes_len(&self) -> usize { + 16 + } + + fn ssz_append(&self, buf: &mut Vec) { + let n = ::ssz_fixed_len(); + let s = buf.len(); + + buf.resize(s + n, 0); + self.to_little_endian(&mut buf[s..]); + } +} + +macro_rules! impl_encodable_for_u8_array { + ($len: expr) => { + impl Encode for [u8; $len] { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + $len + } + + fn ssz_bytes_len(&self) -> usize { + $len + } + + fn ssz_append(&self, buf: &mut Vec) { + buf.extend_from_slice(&self[..]); + } + } + }; +} + +impl_encodable_for_u8_array!(4); +impl_encodable_for_u8_array!(32); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn vec_of_u8() { + let vec: Vec = vec![]; + assert_eq!(vec.as_ssz_bytes(), vec![]); + + let vec: Vec = vec![1]; + assert_eq!(vec.as_ssz_bytes(), vec![1]); + + let vec: Vec = vec![0, 1, 2, 3]; + assert_eq!(vec.as_ssz_bytes(), vec![0, 1, 2, 3]); + } + + #[test] + fn vec_of_vec_of_u8() { + let vec: Vec> = vec![]; + assert_eq!(vec.as_ssz_bytes(), vec![]); + + let vec: Vec> = vec![vec![]]; + assert_eq!(vec.as_ssz_bytes(), vec![4, 0, 0, 0]); + + let vec: Vec> = vec![vec![], vec![]]; + assert_eq!(vec.as_ssz_bytes(), vec![8, 0, 0, 0, 8, 0, 0, 0]); + + let vec: Vec> = vec![vec![0, 1, 2], vec![11, 22, 33]]; + assert_eq!( + vec.as_ssz_bytes(), + vec![8, 0, 0, 0, 11, 0, 0, 0, 0, 1, 2, 11, 22, 33] + ); + } + + #[test] + fn ssz_encode_u8() { + assert_eq!(0_u8.as_ssz_bytes(), vec![0]); + assert_eq!(1_u8.as_ssz_bytes(), vec![1]); + assert_eq!(100_u8.as_ssz_bytes(), vec![100]); + assert_eq!(255_u8.as_ssz_bytes(), vec![255]); + } + + #[test] + fn ssz_encode_u16() { + assert_eq!(1_u16.as_ssz_bytes(), vec![1, 0]); + assert_eq!(100_u16.as_ssz_bytes(), vec![100, 0]); + assert_eq!((1_u16 << 8).as_ssz_bytes(), vec![0, 1]); + assert_eq!(65535_u16.as_ssz_bytes(), vec![255, 255]); + } + + #[test] + fn ssz_encode_u32() { + assert_eq!(1_u32.as_ssz_bytes(), vec![1, 0, 0, 0]); + assert_eq!(100_u32.as_ssz_bytes(), vec![100, 0, 0, 0]); + assert_eq!((1_u32 << 16).as_ssz_bytes(), vec![0, 0, 1, 0]); + assert_eq!((1_u32 << 24).as_ssz_bytes(), vec![0, 0, 0, 1]); + assert_eq!((!0_u32).as_ssz_bytes(), vec![255, 255, 255, 255]); + } + + #[test] + fn ssz_encode_u64() { + assert_eq!(1_u64.as_ssz_bytes(), vec![1, 0, 0, 0, 0, 0, 0, 0]); + assert_eq!( + (!0_u64).as_ssz_bytes(), + vec![255, 255, 255, 255, 255, 255, 255, 255] + ); + } + + #[test] + fn ssz_encode_usize() { + assert_eq!(1_usize.as_ssz_bytes(), vec![1, 0, 0, 0, 0, 0, 0, 0]); + assert_eq!( + (!0_usize).as_ssz_bytes(), + vec![255, 255, 255, 255, 255, 255, 255, 255] + ); + } + + #[test] + fn ssz_encode_bool() { + assert_eq!(true.as_ssz_bytes(), vec![1]); + assert_eq!(false.as_ssz_bytes(), vec![0]); + } + + #[test] + fn ssz_encode_h256() { + assert_eq!(H256::from(&[0; 32]).as_ssz_bytes(), vec![0; 32]); + assert_eq!(H256::from(&[1; 32]).as_ssz_bytes(), vec![1; 32]); + + let bytes = vec![ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]; + + assert_eq!(H256::from_slice(&bytes).as_ssz_bytes(), bytes); + } + + #[test] + fn ssz_encode_u8_array_4() { + assert_eq!([0, 0, 0, 0].as_ssz_bytes(), vec![0; 4]); + assert_eq!([1, 0, 0, 0].as_ssz_bytes(), vec![1, 0, 0, 0]); + assert_eq!([1, 2, 3, 4].as_ssz_bytes(), vec![1, 2, 3, 4]); + } + + #[test] + fn tuple() { + assert_eq!((10u8, 11u8).as_ssz_bytes(), vec![10, 11]); + assert_eq!((10u32, 11u8).as_ssz_bytes(), vec![10, 0, 0, 0, 11]); + assert_eq!((10u8, 11u8, 12u8).as_ssz_bytes(), vec![10, 11, 12]); + } +} diff --git a/version-meld/eth2_ssz/src/legacy.rs b/version-meld/eth2_ssz/src/legacy.rs new file mode 100644 index 0000000..4953db0 --- /dev/null +++ b/version-meld/eth2_ssz/src/legacy.rs @@ -0,0 +1,265 @@ +//! Provides a "legacy" version of SSZ encoding for `Option where T: Encode + Decode`. +//! +//! The SSZ specification changed in 2021 to use a 1-byte union selector, instead of a 4-byte one +//! which was used in the Lighthouse database. +//! +//! Users can use the `four_byte_option_impl` macro to define a module that can be used with the +//! `#[ssz(with = "module")]`. +//! +//! ## Example +//! +//! ```rust +//! use ssz_derive::{Encode, Decode}; +//! use ssz::four_byte_option_impl; +//! +//! four_byte_option_impl!(impl_for_u64, u64); +//! +//! #[derive(Encode, Decode)] +//! struct Foo { +//! #[ssz(with = "impl_for_u64")] +//! a: Option, +//! } +//! ``` + +use crate::*; + +#[macro_export] +macro_rules! four_byte_option_impl { + ($mod_name: ident, $type: ty) => { + #[allow(dead_code)] + mod $mod_name { + use super::*; + + pub mod encode { + use super::*; + #[allow(unused_imports)] + use ssz::*; + + pub fn is_ssz_fixed_len() -> bool { + false + } + + pub fn ssz_fixed_len() -> usize { + BYTES_PER_LENGTH_OFFSET + } + + pub fn ssz_bytes_len(opt: &Option<$type>) -> usize { + if let Some(some) = opt { + let len = if <$type as Encode>::is_ssz_fixed_len() { + <$type as Encode>::ssz_fixed_len() + } else { + <$type as Encode>::ssz_bytes_len(some) + }; + len + BYTES_PER_LENGTH_OFFSET + } else { + BYTES_PER_LENGTH_OFFSET + } + } + + pub fn ssz_append(opt: &Option<$type>, buf: &mut Vec) { + match opt { + None => buf.extend_from_slice(&legacy::encode_four_byte_union_selector(0)), + Some(t) => { + buf.extend_from_slice(&legacy::encode_four_byte_union_selector(1)); + t.ssz_append(buf); + } + } + } + + pub fn as_ssz_bytes(opt: &Option<$type>) -> Vec { + let mut buf = vec![]; + + ssz_append(opt, &mut buf); + + buf + } + } + + pub mod decode { + use super::*; + #[allow(unused_imports)] + use ssz::*; + + pub fn is_ssz_fixed_len() -> bool { + false + } + + pub fn ssz_fixed_len() -> usize { + BYTES_PER_LENGTH_OFFSET + } + + pub fn from_ssz_bytes(bytes: &[u8]) -> Result, DecodeError> { + if bytes.len() < BYTES_PER_LENGTH_OFFSET { + return Err(DecodeError::InvalidByteLength { + len: bytes.len(), + expected: BYTES_PER_LENGTH_OFFSET, + }); + } + + let (index_bytes, value_bytes) = bytes.split_at(BYTES_PER_LENGTH_OFFSET); + + let index = legacy::read_four_byte_union_selector(index_bytes)?; + if index == 0 { + Ok(None) + } else if index == 1 { + Ok(Some(<$type as ssz::Decode>::from_ssz_bytes(value_bytes)?)) + } else { + Err(DecodeError::BytesInvalid(format!( + "{} is not a valid union index for Option", + index + ))) + } + } + } + } + }; +} + +pub fn encode_four_byte_union_selector(selector: usize) -> [u8; BYTES_PER_LENGTH_OFFSET] { + encode_length(selector) +} + +pub fn read_four_byte_union_selector(bytes: &[u8]) -> Result { + read_offset(bytes) +} + +#[cfg(test)] +mod test { + use super::*; + use crate as ssz; + use ssz_derive::{Decode, Encode}; + + type VecU16 = Vec; + + four_byte_option_impl!(impl_u16, u16); + four_byte_option_impl!(impl_vec_u16, VecU16); + + #[test] + fn ssz_encode_option_u16() { + let item = Some(65535_u16); + let bytes = vec![1, 0, 0, 0, 255, 255]; + assert_eq!(impl_u16::encode::as_ssz_bytes(&item), bytes); + assert_eq!(impl_u16::decode::from_ssz_bytes(&bytes).unwrap(), item); + + let item = None; + let bytes = vec![0, 0, 0, 0]; + assert_eq!(impl_u16::encode::as_ssz_bytes(&item), bytes); + assert_eq!(impl_u16::decode::from_ssz_bytes(&bytes).unwrap(), None); + } + + #[test] + fn ssz_encode_option_vec_u16() { + let item = Some(vec![0_u16, 1]); + let bytes = vec![1, 0, 0, 0, 0, 0, 1, 0]; + assert_eq!(impl_vec_u16::encode::as_ssz_bytes(&item), bytes); + assert_eq!(impl_vec_u16::decode::from_ssz_bytes(&bytes).unwrap(), item); + + let item = None; + let bytes = vec![0, 0, 0, 0]; + assert_eq!(impl_vec_u16::encode::as_ssz_bytes(&item), bytes); + assert_eq!(impl_vec_u16::decode::from_ssz_bytes(&bytes).unwrap(), item); + } + + fn round_trip(items: Vec) { + for item in items { + let encoded = &item.as_ssz_bytes(); + assert_eq!(item.ssz_bytes_len(), encoded.len()); + assert_eq!(T::from_ssz_bytes(encoded), Ok(item)); + } + } + + #[derive(Debug, PartialEq, Encode, Decode)] + struct TwoVariableLenOptions { + a: u16, + #[ssz(with = "impl_u16")] + b: Option, + #[ssz(with = "impl_vec_u16")] + c: Option>, + #[ssz(with = "impl_vec_u16")] + d: Option>, + } + + #[test] + #[allow(clippy::zero_prefixed_literal)] + fn two_variable_len_options_encoding() { + let s = TwoVariableLenOptions { + a: 42, + b: None, + c: Some(vec![0]), + d: None, + }; + + let bytes = vec![ + // 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 + // | option | offset | offset | option = vec![ + TwoVariableLenOptions { + a: 42, + b: Some(12), + c: Some(vec![0]), + d: Some(vec![1]), + }, + TwoVariableLenOptions { + a: 42, + b: Some(12), + c: Some(vec![0]), + d: None, + }, + TwoVariableLenOptions { + a: 42, + b: None, + c: Some(vec![0]), + d: None, + }, + TwoVariableLenOptions { + a: 42, + b: None, + c: None, + d: None, + }, + ]; + + round_trip(vec); + } + + #[test] + fn tuple_u8_u16() { + let vec: Vec<(u8, u16)> = vec![ + (0, 0), + (0, 1), + (1, 0), + (u8::max_value(), u16::max_value()), + (0, u16::max_value()), + (u8::max_value(), 0), + (42, 12301), + ]; + + round_trip(vec); + } + + #[test] + fn tuple_vec_vec() { + let vec: Vec<(u64, Vec, Vec>)> = vec![ + (0, vec![], vec![vec![]]), + (99, vec![101], vec![vec![], vec![]]), + ( + 42, + vec![12, 13, 14], + vec![vec![99, 98, 97, 96], vec![42, 44, 46, 48, 50]], + ), + ]; + + round_trip(vec); + } +} diff --git a/version-meld/eth2_ssz/src/lib.rs b/version-meld/eth2_ssz/src/lib.rs new file mode 100644 index 0000000..df00c51 --- /dev/null +++ b/version-meld/eth2_ssz/src/lib.rs @@ -0,0 +1,71 @@ +//! Provides encoding (serialization) and decoding (deserialization) in the SimpleSerialize (SSZ) +//! format designed for use in Ethereum 2.0. +//! +//! Adheres to the Ethereum 2.0 [SSZ +//! specification](https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/ssz/simple-serialize.md) +//! at v0.12.1. +//! +//! ## Example +//! +//! ```rust +//! use ssz_derive::{Encode, Decode}; +//! use ssz::{Decode, Encode}; +//! +//! #[derive(PartialEq, Debug, Encode, Decode)] +//! struct Foo { +//! a: u64, +//! b: Vec, +//! } +//! +//! fn ssz_encode_decode_example() { +//! let foo = Foo { +//! a: 42, +//! b: vec![1, 3, 3, 7] +//! }; +//! +//! let ssz_bytes: Vec = foo.as_ssz_bytes(); +//! +//! let decoded_foo = Foo::from_ssz_bytes(&ssz_bytes).unwrap(); +//! +//! assert_eq!(foo, decoded_foo); +//! } +//! +//! ``` +//! +//! See `examples/` for manual implementations of the `Encode` and `Decode` traits. + +mod decode; +mod encode; +pub mod legacy; +mod union_selector; + +pub use decode::{ + impls::decode_list_of_variable_length_items, read_offset, split_union_bytes, Decode, + DecodeError, SszDecoder, SszDecoderBuilder, +}; +pub use encode::{encode_length, Encode, SszEncoder}; +pub use union_selector::UnionSelector; + +/// The number of bytes used to represent an offset. +pub const BYTES_PER_LENGTH_OFFSET: usize = 4; +/// The maximum value that can be represented using `BYTES_PER_LENGTH_OFFSET`. +#[cfg(target_pointer_width = "32")] +pub const MAX_LENGTH_VALUE: usize = (std::u32::MAX >> (8 * (4 - BYTES_PER_LENGTH_OFFSET))) as usize; +#[cfg(target_pointer_width = "64")] +pub const MAX_LENGTH_VALUE: usize = (std::u64::MAX >> (8 * (8 - BYTES_PER_LENGTH_OFFSET))) as usize; + +/// The number of bytes used to indicate the variant of a union. +pub const BYTES_PER_UNION_SELECTOR: usize = 1; +/// The highest possible union selector value (higher values are reserved for backwards compatible +/// extensions). +pub const MAX_UNION_SELECTOR: u8 = 127; + +/// Convenience function to SSZ encode an object supporting ssz::Encode. +/// +/// Equivalent to `val.as_ssz_bytes()`. +pub fn ssz_encode(val: &T) -> Vec +where + T: Encode, +{ + val.as_ssz_bytes() +} diff --git a/version-meld/eth2_ssz/src/union_selector.rs b/version-meld/eth2_ssz/src/union_selector.rs new file mode 100644 index 0000000..18bab09 --- /dev/null +++ b/version-meld/eth2_ssz/src/union_selector.rs @@ -0,0 +1,29 @@ +use crate::*; + +/// Provides the one-byte "selector" from the SSZ union specification: +/// +/// https://github.com/ethereum/consensus-specs/blob/v1.1.0-beta.3/ssz/simple-serialize.md#union +#[derive(Copy, Clone)] +pub struct UnionSelector(u8); + +impl From for u8 { + fn from(union_selector: UnionSelector) -> u8 { + union_selector.0 + } +} + +impl PartialEq for UnionSelector { + fn eq(&self, other: &u8) -> bool { + self.0 == *other + } +} + +impl UnionSelector { + /// Instantiate `self`, returning an error if `selector > MAX_UNION_SELECTOR`. + pub fn new(selector: u8) -> Result { + Some(selector) + .filter(|_| selector <= MAX_UNION_SELECTOR) + .map(Self) + .ok_or(DecodeError::UnionSelectorInvalid(selector)) + } +} diff --git a/version-meld/eth2_ssz/tests/tests.rs b/version-meld/eth2_ssz/tests/tests.rs new file mode 100644 index 0000000..7bd6252 --- /dev/null +++ b/version-meld/eth2_ssz/tests/tests.rs @@ -0,0 +1,466 @@ +use ethereum_types::H256; +use ssz::{Decode, DecodeError, Encode}; +use ssz_derive::{Decode, Encode}; + +mod round_trip { + use super::*; + + fn round_trip(items: Vec) { + for item in items { + let encoded = &item.as_ssz_bytes(); + assert_eq!(item.ssz_bytes_len(), encoded.len()); + assert_eq!(T::from_ssz_bytes(encoded), Ok(item)); + } + } + + #[test] + fn bool() { + let items: Vec = vec![true, false]; + + round_trip(items); + } + + #[test] + fn u8_array_4() { + let items: Vec<[u8; 4]> = vec![[0, 0, 0, 0], [1, 0, 0, 0], [1, 2, 3, 4], [1, 2, 0, 4]]; + + round_trip(items); + } + + #[test] + fn h256() { + let items: Vec = vec![H256::zero(), H256::from([1; 32]), H256::random()]; + + round_trip(items); + } + + #[test] + fn vec_of_h256() { + let items: Vec> = vec![ + vec![], + vec![H256::zero(), H256::from([1; 32]), H256::random()], + ]; + + round_trip(items); + } + + #[test] + fn vec_u16() { + let items: Vec> = vec![ + vec![], + vec![255], + vec![0, 1, 2], + vec![100; 64], + vec![255, 0, 255], + ]; + + round_trip(items); + } + + #[test] + fn vec_of_vec_u16() { + let items: Vec>> = vec![ + vec![], + vec![vec![]], + vec![vec![1, 2, 3]], + vec![vec![], vec![]], + vec![vec![], vec![1, 2, 3]], + vec![vec![1, 2, 3], vec![1, 2, 3]], + vec![vec![1, 2, 3], vec![], vec![1, 2, 3]], + vec![vec![], vec![], vec![1, 2, 3]], + vec![vec![], vec![1], vec![1, 2, 3]], + vec![vec![], vec![1], vec![1, 2, 3]], + ]; + + round_trip(items); + } + + #[derive(Debug, PartialEq, Encode, Decode)] + struct FixedLen { + a: u16, + b: u64, + c: u32, + } + + #[test] + #[allow(clippy::zero_prefixed_literal)] + fn fixed_len_struct_encoding() { + let items: Vec = vec![ + FixedLen { a: 0, b: 0, c: 0 }, + FixedLen { a: 1, b: 1, c: 1 }, + FixedLen { a: 1, b: 0, c: 1 }, + ]; + + let expected_encodings = vec![ + // | u16--| u64----------------------------| u32----------| + vec![00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00], + vec![01, 00, 01, 00, 00, 00, 00, 00, 00, 00, 01, 00, 00, 00], + vec![01, 00, 00, 00, 00, 00, 00, 00, 00, 00, 01, 00, 00, 00], + ]; + + for i in 0..items.len() { + assert_eq!( + items[i].as_ssz_bytes(), + expected_encodings[i], + "Failed on {}", + i + ); + } + } + + #[test] + fn fixed_len_excess_bytes() { + let fixed = FixedLen { a: 1, b: 2, c: 3 }; + + let mut bytes = fixed.as_ssz_bytes(); + bytes.append(&mut vec![0]); + + assert_eq!( + FixedLen::from_ssz_bytes(&bytes), + Err(DecodeError::InvalidByteLength { + len: 15, + expected: 14, + }) + ); + } + + #[test] + fn vec_of_fixed_len_struct() { + let items: Vec = vec![ + FixedLen { a: 0, b: 0, c: 0 }, + FixedLen { a: 1, b: 1, c: 1 }, + FixedLen { a: 1, b: 0, c: 1 }, + ]; + + round_trip(items); + } + + #[derive(Debug, PartialEq, Encode, Decode)] + struct VariableLen { + a: u16, + b: Vec, + c: u32, + } + + #[test] + #[allow(clippy::zero_prefixed_literal)] + fn offset_into_fixed_bytes() { + let bytes = vec![ + // 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + // | offset | u32 | variable + 01, 00, 09, 00, 00, 00, 01, 00, 00, 00, 00, 00, 01, 00, 02, 00, + ]; + + assert_eq!( + VariableLen::from_ssz_bytes(&bytes), + Err(DecodeError::OffsetIntoFixedPortion(9)) + ); + } + + #[test] + fn variable_len_excess_bytes() { + let variable = VariableLen { + a: 1, + b: vec![2], + c: 3, + }; + + let mut bytes = variable.as_ssz_bytes(); + bytes.append(&mut vec![0]); + + // The error message triggered is not so helpful, it's caught by a side-effect. Just + // checking there is _some_ error is fine. + assert!(VariableLen::from_ssz_bytes(&bytes).is_err()); + } + + #[test] + #[allow(clippy::zero_prefixed_literal)] + fn first_offset_skips_byte() { + let bytes = vec![ + // 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + // | offset | u32 | variable + 01, 00, 11, 00, 00, 00, 01, 00, 00, 00, 00, 00, 01, 00, 02, 00, + ]; + + assert_eq!( + VariableLen::from_ssz_bytes(&bytes), + Err(DecodeError::OffsetSkipsVariableBytes(11)) + ); + } + + #[test] + #[allow(clippy::zero_prefixed_literal)] + fn variable_len_struct_encoding() { + let items: Vec = vec![ + VariableLen { + a: 0, + b: vec![], + c: 0, + }, + VariableLen { + a: 1, + b: vec![0], + c: 1, + }, + VariableLen { + a: 1, + b: vec![0, 1, 2], + c: 1, + }, + ]; + + let expected_encodings = vec![ + // 00..................................09 + // | u16--| vec offset-----| u32------------| vec payload --------| + vec![00, 00, 10, 00, 00, 00, 00, 00, 00, 00], + vec![01, 00, 10, 00, 00, 00, 01, 00, 00, 00, 00, 00], + vec![ + 01, 00, 10, 00, 00, 00, 01, 00, 00, 00, 00, 00, 01, 00, 02, 00, + ], + ]; + + for i in 0..items.len() { + assert_eq!( + items[i].as_ssz_bytes(), + expected_encodings[i], + "Failed on {}", + i + ); + } + } + + #[test] + fn vec_of_variable_len_struct() { + let items: Vec = vec![ + VariableLen { + a: 0, + b: vec![], + c: 0, + }, + VariableLen { + a: 255, + b: vec![0, 1, 2, 3], + c: 99, + }, + VariableLen { + a: 255, + b: vec![0], + c: 99, + }, + VariableLen { + a: 50, + b: vec![0], + c: 0, + }, + ]; + + round_trip(items); + } + + #[derive(Debug, PartialEq, Encode, Decode)] + struct ThreeVariableLen { + a: u16, + b: Vec, + c: Vec, + d: Vec, + } + + #[test] + fn three_variable_len() { + let vec: Vec = vec![ThreeVariableLen { + a: 42, + b: vec![0], + c: vec![1], + d: vec![2], + }]; + + round_trip(vec); + } + + #[test] + #[allow(clippy::zero_prefixed_literal)] + fn offsets_decreasing() { + let bytes = vec![ + // 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + // | offset | offset | offset | variable + 01, 00, 14, 00, 00, 00, 15, 00, 00, 00, 14, 00, 00, 00, 00, 00, + ]; + + assert_eq!( + ThreeVariableLen::from_ssz_bytes(&bytes), + Err(DecodeError::OffsetsAreDecreasing(14)) + ); + } + + #[test] + fn tuple_u8_u16() { + let vec: Vec<(u8, u16)> = vec![ + (0, 0), + (0, 1), + (1, 0), + (u8::max_value(), u16::max_value()), + (0, u16::max_value()), + (u8::max_value(), 0), + (42, 12301), + ]; + + round_trip(vec); + } + + #[test] + fn tuple_vec_vec() { + let vec: Vec<(u64, Vec, Vec>)> = vec![ + (0, vec![], vec![vec![]]), + (99, vec![101], vec![vec![], vec![]]), + ( + 42, + vec![12, 13, 14], + vec![vec![99, 98, 97, 96], vec![42, 44, 46, 48, 50]], + ), + ]; + + round_trip(vec); + } +} + +mod derive_macro { + use ssz::{Decode, Encode}; + use ssz_derive::{Decode, Encode}; + use std::fmt::Debug; + + fn assert_encode(item: &T, bytes: &[u8]) { + assert_eq!(item.as_ssz_bytes(), bytes); + } + + fn assert_encode_decode(item: &T, bytes: &[u8]) { + assert_encode(item, bytes); + assert_eq!(T::from_ssz_bytes(bytes).unwrap(), *item); + } + + #[derive(PartialEq, Debug, Encode, Decode)] + #[ssz(enum_behaviour = "union")] + enum TwoFixedUnion { + U8(u8), + U16(u16), + } + + #[derive(PartialEq, Debug, Encode, Decode)] + struct TwoFixedUnionStruct { + a: TwoFixedUnion, + } + + #[test] + fn two_fixed_union() { + let eight = TwoFixedUnion::U8(1); + let sixteen = TwoFixedUnion::U16(1); + + assert_encode_decode(&eight, &[0, 1]); + assert_encode_decode(&sixteen, &[1, 1, 0]); + + assert_encode_decode(&TwoFixedUnionStruct { a: eight }, &[4, 0, 0, 0, 0, 1]); + assert_encode_decode(&TwoFixedUnionStruct { a: sixteen }, &[4, 0, 0, 0, 1, 1, 0]); + } + + #[derive(PartialEq, Debug, Encode, Decode)] + struct VariableA { + a: u8, + b: Vec, + } + + #[derive(PartialEq, Debug, Encode, Decode)] + struct VariableB { + a: Vec, + b: u8, + } + + #[derive(PartialEq, Debug, Encode)] + #[ssz(enum_behaviour = "transparent")] + enum TwoVariableTrans { + A(VariableA), + B(VariableB), + } + + #[derive(PartialEq, Debug, Encode)] + struct TwoVariableTransStruct { + a: TwoVariableTrans, + } + + #[derive(PartialEq, Debug, Encode, Decode)] + #[ssz(enum_behaviour = "union")] + enum TwoVariableUnion { + A(VariableA), + B(VariableB), + } + + #[derive(PartialEq, Debug, Encode, Decode)] + struct TwoVariableUnionStruct { + a: TwoVariableUnion, + } + + #[test] + fn two_variable_trans() { + let trans_a = TwoVariableTrans::A(VariableA { + a: 1, + b: vec![2, 3], + }); + let trans_b = TwoVariableTrans::B(VariableB { + a: vec![1, 2], + b: 3, + }); + + assert_encode(&trans_a, &[1, 5, 0, 0, 0, 2, 3]); + assert_encode(&trans_b, &[5, 0, 0, 0, 3, 1, 2]); + + assert_encode( + &TwoVariableTransStruct { a: trans_a }, + &[4, 0, 0, 0, 1, 5, 0, 0, 0, 2, 3], + ); + assert_encode( + &TwoVariableTransStruct { a: trans_b }, + &[4, 0, 0, 0, 5, 0, 0, 0, 3, 1, 2], + ); + } + + #[test] + fn two_variable_union() { + let union_a = TwoVariableUnion::A(VariableA { + a: 1, + b: vec![2, 3], + }); + let union_b = TwoVariableUnion::B(VariableB { + a: vec![1, 2], + b: 3, + }); + + assert_encode_decode(&union_a, &[0, 1, 5, 0, 0, 0, 2, 3]); + assert_encode_decode(&union_b, &[1, 5, 0, 0, 0, 3, 1, 2]); + + assert_encode_decode( + &TwoVariableUnionStruct { a: union_a }, + &[4, 0, 0, 0, 0, 1, 5, 0, 0, 0, 2, 3], + ); + assert_encode_decode( + &TwoVariableUnionStruct { a: union_b }, + &[4, 0, 0, 0, 1, 5, 0, 0, 0, 3, 1, 2], + ); + } + + #[derive(PartialEq, Debug, Encode, Decode)] + #[ssz(enum_behaviour = "union")] + enum TwoVecUnion { + A(Vec), + B(Vec), + } + + #[test] + fn two_vec_union() { + assert_encode_decode(&TwoVecUnion::A(vec![]), &[0]); + assert_encode_decode(&TwoVecUnion::B(vec![]), &[1]); + + assert_encode_decode(&TwoVecUnion::A(vec![0]), &[0, 0]); + assert_encode_decode(&TwoVecUnion::B(vec![0]), &[1, 0]); + + assert_encode_decode(&TwoVecUnion::A(vec![0, 1]), &[0, 0, 1]); + assert_encode_decode(&TwoVecUnion::B(vec![0, 1]), &[1, 0, 1]); + } +} diff --git a/zerog-storage-client b/zerog-storage-client new file mode 160000 index 0000000..307f5b1 --- /dev/null +++ b/zerog-storage-client @@ -0,0 +1 @@ +Subproject commit 307f5b17d178df643dd981d63495bb2c92455f5f diff --git a/zerog-storage-contracts b/zerog-storage-contracts new file mode 160000 index 0000000..171cebf --- /dev/null +++ b/zerog-storage-contracts @@ -0,0 +1 @@ +Subproject commit 171cebffffdd82bf464dea85539458bcafbd73ef