diff --git a/.github/workflows/local-testnet.yml b/.github/workflows/local-testnet.yml index 7f367821c3..13c1af7ab6 100644 --- a/.github/workflows/local-testnet.yml +++ b/.github/workflows/local-testnet.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v1 - name: Install ganache - run: npm install ganache-cli@latest --global + run: npm install ganache@latest --global # https://github.com/actions/cache/blob/main/examples.md#rust---cargo - uses: actions/cache@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9ce1991ac5..341e53354c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,9 +46,9 @@ jobs: - arch: x86_64-apple-darwin-portable platform: macos-latest - arch: x86_64-windows - platform: windows-latest + platform: windows-2019 - arch: x86_64-windows-portable - platform: windows-latest + platform: windows-2019 runs-on: ${{ matrix.platform }} needs: extract-version diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 919bdbac2a..57ccbdaa14 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -21,6 +21,18 @@ jobs: steps: - name: Check that the pull request is not targeting the stable branch run: test ${{ github.base_ref }} != "stable" + extract-msrv: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Extract Minimum Supported Rust Version (MSRV) + run: | + metadata=$(cargo metadata --no-deps --format-version 1) + msrv=$(echo $metadata | jq -r '.packages | map(select(.name == "lighthouse")) | .[0].rust_version') + echo "::set-output name=MSRV::$msrv" + id: extract_msrv + outputs: + MSRV: ${{ steps.extract_msrv.outputs.MSRV }} cargo-fmt: name: cargo-fmt runs-on: ubuntu-latest @@ -38,8 +50,8 @@ jobs: - uses: actions/checkout@v1 - name: Get latest version of stable Rust run: rustup update stable - - name: Install ganache-cli - run: sudo npm install -g ganache-cli + - name: Install ganache + run: sudo npm install -g ganache - name: Run tests in release run: make test-release release-tests-windows: @@ -50,8 +62,16 @@ jobs: - uses: actions/checkout@v1 - name: Get latest version of stable Rust run: rustup update stable - - name: Install ganache-cli - run: npm install -g ganache-cli + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: '14' + - name: Install windows build tools + run: | + choco install python visualstudio2019-workload-vctools -y + npm config set msvs_version 2019 + - name: Install ganache + run: npm install -g ganache --loglevel verbose - name: Install make run: choco install -y make - uses: KyleMayes/install-llvm-action@v1 @@ -90,8 +110,8 @@ jobs: - uses: actions/checkout@v1 - name: Get latest version of stable Rust run: rustup update stable - - name: Install ganache-cli - run: sudo npm install -g ganache-cli + - name: Install ganache + run: sudo npm install -g ganache - name: Run tests in debug run: make test-debug state-transition-vectors-ubuntu: @@ -134,8 +154,8 @@ jobs: - uses: actions/checkout@v1 - name: Get latest version of stable Rust run: rustup update stable - - name: Install ganache-cli - run: sudo npm install -g ganache-cli + - name: Install ganache + run: sudo npm install -g ganache - name: Run the beacon chain sim that starts from an eth1 contract run: cargo run --release --bin simulator eth1-sim no-eth1-simulator-ubuntu: @@ -146,8 +166,8 @@ jobs: - uses: actions/checkout@v1 - name: Get latest version of stable Rust run: rustup update stable - - name: Install ganache-cli - run: sudo npm install -g ganache-cli + - name: Install ganache + run: sudo npm install -g ganache - name: Run the beacon chain sim without an eth1 connection run: cargo run --release --bin simulator no-eth1-sim syncing-simulator-ubuntu: @@ -158,8 +178,8 @@ jobs: - uses: actions/checkout@v1 - name: Get latest version of stable Rust run: rustup update stable - - name: Install ganache-cli - run: sudo npm install -g ganache-cli + - name: Install ganache + run: sudo npm install -g ganache - name: Run the syncing simulator run: cargo run --release --bin simulator syncing-sim doppelganger-protection-test: @@ -170,8 +190,8 @@ jobs: - uses: actions/checkout@v1 - name: Get latest version of stable Rust run: rustup update stable - - name: Install ganache-cli - run: sudo npm install -g ganache-cli + - name: Install ganache + run: sudo npm install -g ganache - name: Install lighthouse and lcli run: | make @@ -190,6 +210,12 @@ jobs: needs: cargo-fmt steps: - uses: actions/checkout@v1 + - uses: actions/setup-go@v2 + with: + go-version: '1.17' + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: '6.0.201' - name: Get latest version of stable Rust run: rustup update stable - name: Run exec engine integration tests in release @@ -226,6 +252,16 @@ jobs: run: make lint - name: Certify Cargo.lock freshness run: git diff --exit-code Cargo.lock + check-msrv: + name: check-msrv + runs-on: ubuntu-latest + needs: [cargo-fmt, extract-msrv] + steps: + - uses: actions/checkout@v1 + - name: Install Rust @ MSRV (${{ needs.extract-msrv.outputs.MSRV }}) + run: rustup override set ${{ needs.extract-msrv.outputs.MSRV }} + - name: Run cargo check + run: cargo check --workspace arbitrary-check: name: arbitrary-check runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index 5bb73bdf45..4b07ef6749 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,7 +111,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom 0.2.4", + "getrandom 0.2.5", "once_cell", "version_check", ] @@ -141,15 +141,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.53" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0" +checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" [[package]] name = "arbitrary" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "510c76ecefdceada737ea728f4f9a84bd2e1ef29f1ba555e560940fe279954de" +checksum = "c38b6b6b79f671c25e1a3e785b7b82d7562ffc9cd3efdc98627e5668a2472490" dependencies = [ "derive_arbitrary", ] @@ -317,7 +317,7 @@ dependencies = [ "maplit", "merkle_proof", "operation_pool", - "parking_lot", + "parking_lot 0.11.2", "proto_array", "rand 0.7.3", "rayon", @@ -325,6 +325,7 @@ dependencies = [ "sensitive_url", "serde", "serde_derive", + "serde_json", "slasher", "slog", "sloggers", @@ -343,7 +344,7 @@ dependencies = [ [[package]] name = "beacon_node" -version = "2.1.3" +version = "2.1.5" dependencies = [ "beacon_chain", "clap", @@ -354,6 +355,7 @@ dependencies = [ "environment", "eth2_config", "eth2_network_config", + "execution_layer", "futures", "genesis", "hex", @@ -443,11 +445,11 @@ dependencies = [ [[package]] name = "blake2" -version = "0.10.2" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94ba84325db59637ffc528bbe8c7f86c02c57cff5c0e2b9b00f9a851f42f309" +checksum = "b9cf849ee05b2ee5fba5e36f97ff8ec2533916700fc0758d40d92136a42f3388" dependencies = [ - "digest 0.10.1", + "digest 0.10.3", ] [[package]] @@ -508,7 +510,7 @@ dependencies = [ [[package]] name = "boot_node" -version = "2.1.3" +version = "2.1.5" dependencies = [ "beacon_node", "clap", @@ -522,6 +524,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "serde_yaml", "slog", "slog-async", "slog-scope", @@ -573,9 +576,9 @@ checksum = "b0a5e3906bcbf133e33c1d4d95afc664ad37fbdb9f6568d8043e7ea8c27d93d3" [[package]] name = "byte-slice-cast" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d30c751592b77c499e7bce34d99d67c2c11bdc0574e9a488ddade14150a4698" +checksum = "87c5fdd0166095e1d463fc6cc01aa8ce547ad77a4e84d42eb6762b084e28067e" [[package]] name = "byteorder" @@ -619,7 +622,7 @@ dependencies = [ "eth2_ssz_derive 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "eth2_ssz_types", "ethereum-types 0.12.1", - "quickcheck", + "quickcheck 0.9.2", "quickcheck_macros", "smallvec", "tree_hash", @@ -741,6 +744,10 @@ dependencies = [ "eth2_ssz", "ethereum-types 0.12.1", "hex", + "serde", + "serde_json", + "serde_yaml", + "types", ] [[package]] @@ -764,7 +771,7 @@ dependencies = [ "lighthouse_network", "monitoring_api", "network", - "parking_lot", + "parking_lot 0.11.2", "sensitive_url", "serde", "serde_derive", @@ -967,11 +974,12 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-common" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d6b536309245c849479fba3da410962a43ed8e51c26b729208ec0ac2798d0" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" dependencies = [ "generic-array", + "typenum", ] [[package]] @@ -1178,9 +1186,9 @@ dependencies = [ [[package]] name = "derive_arbitrary" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b24629208e87a2d8b396ff43b15c4afb0a69cea3fbbaa9ed9b92b7c02f0aed73" +checksum = "98e23c06c035dac87bd802d98f368df73a7f2cb05a66ffbd1f377e821fac4af9" dependencies = [ "proc-macro2", "quote", @@ -1211,13 +1219,12 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b697d66081d42af4fba142d56918a3cb21dc8eb63372c6b85d14f44fb9c5979b" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" dependencies = [ "block-buffer 0.10.2", "crypto-common", - "generic-array", "subtle", ] @@ -1280,7 +1287,7 @@ dependencies = [ "aes", "aes-gcm", "arrayvec 0.7.2", - "digest 0.10.1", + "digest 0.10.3", "enr", "fnv", "futures", @@ -1290,8 +1297,8 @@ dependencies = [ "lazy_static", "libp2p-core 0.30.2", "lru", - "parking_lot", - "rand 0.8.4", + "parking_lot 0.11.2", + "rand 0.8.5", "rlp 0.5.1", "sha2 0.9.9", "smallvec", @@ -1324,9 +1331,9 @@ dependencies = [ [[package]] name = "ed25519" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74e1069e39f1454367eb2de793ed062fac4c35c2934b76a81d90dd9abcd28816" +checksum = "eed12bbf7b5312f8da1c2722bc06d8c6b12c2d86a7fb35a194c7f3e6fc2bbe39" dependencies = [ "signature", ] @@ -1421,7 +1428,7 @@ dependencies = [ "hex", "k256", "log", - "rand 0.8.4", + "rand 0.8.5", "rlp 0.5.1", "serde", "sha3", @@ -1430,11 +1437,11 @@ dependencies = [ [[package]] name = "enum-as-inner" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595" +checksum = "570d109b813e904becc80d8d5da38376818a143348413f7149f1340fe04754d4" dependencies = [ - "heck 0.3.3", + "heck 0.4.0", "proc-macro2", "quote", "syn", @@ -1508,7 +1515,7 @@ dependencies = [ "lazy_static", "lighthouse_metrics", "merkle_proof", - "parking_lot", + "parking_lot 0.11.2", "reqwest", "sensitive_url", "serde", @@ -1871,8 +1878,12 @@ dependencies = [ "exit-future", "futures", "hex", + "jsonwebtoken", + "lazy_static", + "lighthouse_metrics", "lru", - "parking_lot", + "parking_lot 0.11.2", + "rand 0.7.3", "reqwest", "sensitive_url", "serde", @@ -1880,11 +1891,13 @@ dependencies = [ "slog", "slot_clock", "task_executor", + "tempfile", "tokio", "tree_hash", "tree_hash_derive", "types", "warp 0.3.0", + "zeroize", ] [[package]] @@ -1978,7 +1991,7 @@ source = "git+https://github.com/paritytech/parity-common?rev=df638ab0885293d21d dependencies = [ "arbitrary", "byteorder", - "rand 0.8.4", + "rand 0.8.5", "rustc-hex", "static_assertions", ] @@ -2134,7 +2147,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d383f0425d991a05e564c2f3ec150bd6dde863179c131dd60d8aa73a05434461" dependencies = [ "futures-io", - "rustls 0.20.2", + "rustls 0.20.4", "webpki 0.22.0", ] @@ -2220,9 +2233,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" +checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" dependencies = [ "cfg-if", "libc", @@ -2286,9 +2299,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f1f717ddc7b2ba36df7e871fd88db79326551d3d6f1fc406fbfd28b582ff8e" +checksum = "62eeb471aa3e3c9197aa4bfeabfe02982f6dc96f750486c0bb0009ac58b26d2b" dependencies = [ "bytes", "fnv", @@ -2338,9 +2351,9 @@ dependencies = [ [[package]] name = "headers" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c84c647447a07ca16f5fbd05b633e535cc41a08d2d74ab1e08648df53be9cb89" +checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" dependencies = [ "base64 0.13.0", "bitflags", @@ -2349,7 +2362,7 @@ dependencies = [ "http", "httpdate", "mime", - "sha-1", + "sha-1 0.10.0", ] [[package]] @@ -2488,7 +2501,7 @@ dependencies = [ "lighthouse_network", "lighthouse_version", "network", - "parking_lot", + "parking_lot 0.11.2", "safe_arith", "sensitive_url", "serde", @@ -2545,9 +2558,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.16" +version = "0.14.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55" +checksum = "043f0e083e9901b6cc658a77d1eb86f4fc650bbb977a4337dd63192826aa85dd" dependencies = [ "bytes", "futures-channel", @@ -2558,7 +2571,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 0.4.8", + "itoa 1.0.1", "pin-project-lite 0.2.8", "socket2 0.4.4", "tokio", @@ -2757,9 +2770,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" +checksum = "35e70ee094dc02fd9c13fdad4940090f22dbd6ac7c9e7094a46cf0232a50bc7c" [[package]] name = "itertools" @@ -2836,6 +2849,20 @@ dependencies = [ "serde_json", ] +[[package]] +name = "jsonwebtoken" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "012bb02250fdd38faa5feee63235f7a459974440b9b57593822414c31f92839e" +dependencies = [ + "base64 0.13.0", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "k256" version = "0.8.1" @@ -2871,7 +2898,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lcli" -version = "2.1.3" +version = "2.1.5" dependencies = [ "account_utils", "bls", @@ -2928,9 +2955,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.117" +version = "0.2.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c" +checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" [[package]] name = "libflate" @@ -2980,20 +3007,21 @@ dependencies = [ "indexmap", "libc", "mdbx-sys", - "parking_lot", + "parking_lot 0.11.2", "thiserror", ] [[package]] name = "libp2p" version = "0.43.0" -source = "git+https://github.com/sigp/rust-libp2p?rev=e213703e616eaba3c482d7714775e0d37c4ae8e5#e213703e616eaba3c482d7714775e0d37c4ae8e5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8570e25fa03d4385405dbeaf540ba00e3ee50942f03d84e1a8928a029f35f9" dependencies = [ "atomic", "bytes", "futures", "futures-timer", - "getrandom 0.2.4", + "getrandom 0.2.5", "instant", "lazy_static", "libp2p-core 0.32.0", @@ -3010,7 +3038,7 @@ dependencies = [ "libp2p-websocket", "libp2p-yamux", "multiaddr 0.14.0", - "parking_lot", + "parking_lot 0.12.0", "pin-project 1.0.10", "rand 0.7.3", "smallvec", @@ -3036,11 +3064,11 @@ dependencies = [ "multiaddr 0.13.0", "multihash 0.14.0", "multistream-select 0.10.4", - "parking_lot", + "parking_lot 0.11.2", "pin-project 1.0.10", "prost", "prost-build", - "rand 0.8.4", + "rand 0.8.5", "ring", "rw-stream-sink", "sha2 0.9.9", @@ -3054,7 +3082,8 @@ dependencies = [ [[package]] name = "libp2p-core" version = "0.32.0" -source = "git+https://github.com/sigp/rust-libp2p?rev=e213703e616eaba3c482d7714775e0d37c4ae8e5#e213703e616eaba3c482d7714775e0d37c4ae8e5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9164ec41455856e8187addc870bb4fe1ea2ee28e1a9244831d449a2429b32c1a" dependencies = [ "asn1_der", "bs58", @@ -3070,14 +3099,14 @@ dependencies = [ "multiaddr 0.14.0", "multihash 0.16.1", "multistream-select 0.11.0", - "parking_lot", + "parking_lot 0.12.0", "pin-project 1.0.10", "prost", "prost-build", - "rand 0.8.4", + "rand 0.8.5", "ring", "rw-stream-sink", - "sha2 0.10.1", + "sha2 0.10.2", "smallvec", "thiserror", "unsigned-varint 0.7.1", @@ -3088,7 +3117,8 @@ dependencies = [ [[package]] name = "libp2p-dns" version = "0.32.0" -source = "git+https://github.com/sigp/rust-libp2p?rev=e213703e616eaba3c482d7714775e0d37c4ae8e5#e213703e616eaba3c482d7714775e0d37c4ae8e5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7838647d33978b77f943687412f4a39e74234c8342cbfdad14282b465b272cb4" dependencies = [ "futures", "libp2p-core 0.32.0", @@ -3100,7 +3130,8 @@ dependencies = [ [[package]] name = "libp2p-gossipsub" version = "0.36.0" -source = "git+https://github.com/sigp/rust-libp2p?rev=e213703e616eaba3c482d7714775e0d37c4ae8e5#e213703e616eaba3c482d7714775e0d37c4ae8e5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f62943fba0b0dae02b87868620c52a581c54ec9fb04b5e195cf20313fc510c3" dependencies = [ "asynchronous-codec", "base64 0.13.0", @@ -3118,16 +3149,17 @@ dependencies = [ "prost-build", "rand 0.7.3", "regex", - "sha2 0.10.1", + "sha2 0.10.2", "smallvec", - "tokio", "unsigned-varint 0.7.1", + "wasm-timer", ] [[package]] name = "libp2p-identify" version = "0.34.0" -source = "git+https://github.com/sigp/rust-libp2p?rev=e213703e616eaba3c482d7714775e0d37c4ae8e5#e213703e616eaba3c482d7714775e0d37c4ae8e5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f219b4d4660fe3a04bf5fe6b5970902b7c1918e25b2536be8c70efc480f88f8" dependencies = [ "futures", "futures-timer", @@ -3143,7 +3175,8 @@ dependencies = [ [[package]] name = "libp2p-metrics" version = "0.4.0" -source = "git+https://github.com/sigp/rust-libp2p?rev=e213703e616eaba3c482d7714775e0d37c4ae8e5#e213703e616eaba3c482d7714775e0d37c4ae8e5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29e4e5e4c5aa567fe1ee3133afe088dc2d2fd104e20c5c2c5c2649f75129677" dependencies = [ "libp2p-core 0.32.0", "libp2p-gossipsub", @@ -3155,7 +3188,8 @@ dependencies = [ [[package]] name = "libp2p-mplex" version = "0.32.0" -source = "git+https://github.com/sigp/rust-libp2p?rev=e213703e616eaba3c482d7714775e0d37c4ae8e5#e213703e616eaba3c482d7714775e0d37c4ae8e5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442eb0c9fff0bf22a34f015724b4143ce01877e079ed0963c722d94c07c72160" dependencies = [ "asynchronous-codec", "bytes", @@ -3163,7 +3197,7 @@ dependencies = [ "libp2p-core 0.32.0", "log", "nohash-hasher", - "parking_lot", + "parking_lot 0.12.0", "rand 0.7.3", "smallvec", "unsigned-varint 0.7.1", @@ -3172,7 +3206,8 @@ dependencies = [ [[package]] name = "libp2p-noise" version = "0.35.0" -source = "git+https://github.com/sigp/rust-libp2p?rev=e213703e616eaba3c482d7714775e0d37c4ae8e5#e213703e616eaba3c482d7714775e0d37c4ae8e5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd7e0c94051cda67123be68cf6b65211ba3dde7277be9068412de3e7ffd63ef" dependencies = [ "bytes", "curve25519-dalek 3.2.0", @@ -3182,8 +3217,8 @@ dependencies = [ "log", "prost", "prost-build", - "rand 0.8.4", - "sha2 0.10.1", + "rand 0.8.5", + "sha2 0.10.2", "snow", "static_assertions", "x25519-dalek", @@ -3193,7 +3228,8 @@ dependencies = [ [[package]] name = "libp2p-plaintext" version = "0.32.0" -source = "git+https://github.com/sigp/rust-libp2p?rev=e213703e616eaba3c482d7714775e0d37c4ae8e5#e213703e616eaba3c482d7714775e0d37c4ae8e5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "962c0fb0e7212fb96a69b87f2d09bcefd317935239bdc79cda900e7a8897a3fe" dependencies = [ "asynchronous-codec", "bytes", @@ -3209,23 +3245,28 @@ dependencies = [ [[package]] name = "libp2p-swarm" version = "0.34.0" -source = "git+https://github.com/sigp/rust-libp2p?rev=e213703e616eaba3c482d7714775e0d37c4ae8e5#e213703e616eaba3c482d7714775e0d37c4ae8e5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53ab2d4eb8ef2966b10fdf859245cdd231026df76d3c6ed2cf9e418a8f688ec9" dependencies = [ "either", + "fnv", "futures", "futures-timer", "instant", "libp2p-core 0.32.0", "log", + "pin-project 1.0.10", "rand 0.7.3", "smallvec", + "thiserror", "void", ] [[package]] name = "libp2p-swarm-derive" -version = "0.26.1" -source = "git+https://github.com/sigp/rust-libp2p?rev=e213703e616eaba3c482d7714775e0d37c4ae8e5#e213703e616eaba3c482d7714775e0d37c4ae8e5" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8153a6472e84ec888ef2bf21deafe8d4214e811f0f162abbf07156c27f8fa8" dependencies = [ "quote", "syn", @@ -3234,7 +3275,8 @@ dependencies = [ [[package]] name = "libp2p-tcp" version = "0.32.0" -source = "git+https://github.com/sigp/rust-libp2p?rev=e213703e616eaba3c482d7714775e0d37c4ae8e5#e213703e616eaba3c482d7714775e0d37c4ae8e5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193447aa729c85aac2376828df76d171c1a589c9e6b58fcc7f9d9a020734122c" dependencies = [ "futures", "futures-timer", @@ -3250,7 +3292,8 @@ dependencies = [ [[package]] name = "libp2p-websocket" version = "0.34.0" -source = "git+https://github.com/sigp/rust-libp2p?rev=e213703e616eaba3c482d7714775e0d37c4ae8e5#e213703e616eaba3c482d7714775e0d37c4ae8e5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c932834c3754501c368d1bf3d0fb458487a642b90fc25df082a3a2f3d3b32e37" dependencies = [ "either", "futures", @@ -3267,11 +3310,12 @@ dependencies = [ [[package]] name = "libp2p-yamux" version = "0.36.0" -source = "git+https://github.com/sigp/rust-libp2p?rev=e213703e616eaba3c482d7714775e0d37c4ae8e5#e213703e616eaba3c482d7714775e0d37c4ae8e5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be902ebd89193cd020e89e89107726a38cfc0d16d18f613f4a37d046e92c7517" dependencies = [ "futures", "libp2p-core 0.32.0", - "parking_lot", + "parking_lot 0.12.0", "thiserror", "yamux", ] @@ -3308,7 +3352,7 @@ dependencies = [ "libsecp256k1-core 0.3.0", "libsecp256k1-gen-ecmult 0.3.0", "libsecp256k1-gen-genmult 0.3.0", - "rand 0.8.4", + "rand 0.8.5", "serde", "sha2 0.9.9", "typenum", @@ -3385,9 +3429,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66" +checksum = "6f35facd4a5673cb5a48822be2be1d4236c1c99cb4113cab7061ac720d5bf859" dependencies = [ "cc", "pkg-config", @@ -3396,7 +3440,7 @@ dependencies = [ [[package]] name = "lighthouse" -version = "2.1.3" +version = "2.1.5" dependencies = [ "account_manager", "account_utils", @@ -3417,8 +3461,10 @@ dependencies = [ "lighthouse_network", "lighthouse_version", "malloc_utils", + "sensitive_url", "serde", "serde_json", + "serde_yaml", "slashing_protection", "slog", "sloggers", @@ -3460,7 +3506,7 @@ dependencies = [ "lighthouse_metrics", "lighthouse_version", "lru", - "parking_lot", + "parking_lot 0.11.2", "prometheus-client", "rand 0.7.3", "regex", @@ -3540,9 +3586,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "274353858935c992b13c0ca408752e2121da852d07dec7ce5f108c77dfa14d1f" +checksum = "fcb87f3080f6d1d69e8c564c0fcfde1d7aa8cc451ce40cae89479111f03bc0eb" dependencies = [ "hashbrown", ] @@ -3581,7 +3627,7 @@ dependencies = [ "lazy_static", "libc", "lighthouse_metrics", - "parking_lot", + "parking_lot 0.11.2", ] [[package]] @@ -3645,7 +3691,7 @@ dependencies = [ "eth2_hashing 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "ethereum-types 0.12.1", "lazy_static", - "quickcheck", + "quickcheck 0.9.2", "quickcheck_macros", "safe_arith", ] @@ -3671,7 +3717,7 @@ dependencies = [ "eth2_ssz", "eth2_ssz_derive 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "itertools", - "parking_lot", + "parking_lot 0.11.2", "rayon", "serde", "tree_hash", @@ -3687,9 +3733,9 @@ checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" [[package]] name = "mime_guess" -version = "2.0.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" dependencies = [ "mime", "unicase", @@ -3713,9 +3759,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.7.14" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" +checksum = "ba272f85fa0b41fc91872be579b3bbe0f56b792aa361a380eb669469f68dafb2" dependencies = [ "libc", "log", @@ -3809,9 +3855,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7392bffd88bc0c4f8297e36a777ab9f80b7127409c4a1acb8fee99c9f27addcd" dependencies = [ "core2", - "digest 0.10.1", + "digest 0.10.3", "multihash-derive 0.8.0", - "sha2 0.10.1", + "sha2 0.10.2", "unsigned-varint 0.7.1", ] @@ -3879,7 +3925,7 @@ dependencies = [ "mime", "mime_guess", "quick-error", - "rand 0.8.4", + "rand 0.8.5", "safemem", "tempfile", "twoway", @@ -3902,7 +3948,8 @@ dependencies = [ [[package]] name = "multistream-select" version = "0.11.0" -source = "git+https://github.com/sigp/rust-libp2p?rev=e213703e616eaba3c482d7714775e0d37c4ae8e5#e213703e616eaba3c482d7714775e0d37c4ae8e5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "363a84be6453a70e63513660f4894ef815daf88e3356bffcda9ca27d810ce83b" dependencies = [ "bytes", "futures", @@ -4037,9 +4084,9 @@ dependencies = [ [[package]] name = "ntapi" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" dependencies = [ "winapi", ] @@ -4134,9 +4181,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" [[package]] name = "oorandom" @@ -4172,9 +4219,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "111.17.0+1.1.1m" +version = "111.18.0+1.1.1n" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d6a336abd10814198f66e2a91ccd7336611f30334119ca8ce300536666fcf4" +checksum = "7897a926e1e8d00219127dc020130eca4292e5ca666dd592480d72c3eca2ff6c" dependencies = [ "cc", ] @@ -4204,7 +4251,7 @@ dependencies = [ "itertools", "lazy_static", "lighthouse_metrics", - "parking_lot", + "parking_lot 0.11.2", "rayon", "serde", "serde_derive", @@ -4242,7 +4289,7 @@ checksum = "373b1a4c1338d9cd3d1fa53b3a11bdab5ab6bd80a20f7f7becd76953ae2be909" dependencies = [ "arrayvec 0.7.2", "bitvec 0.20.4", - "byte-slice-cast 1.2.0", + "byte-slice-cast 1.2.1", "impl-trait-for-tuples", "parity-scale-codec-derive", "serde", @@ -4268,7 +4315,17 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", - "parking_lot_core", + "parking_lot_core 0.8.5", +] + +[[package]] +name = "parking_lot" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.1", ] [[package]] @@ -4285,6 +4342,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "parking_lot_core" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28141e0cc4143da2443301914478dc976a61ffdb3f043058310c70df2fed8954" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + [[package]] name = "paste" version = "1.0.6" @@ -4315,6 +4385,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" +[[package]] +name = "pem" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9a3b09a20e374558580a4914d3b7d89bd61b954a5a5e1dcbea98753addb1947" +dependencies = [ + "base64 0.13.0", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -4518,9 +4597,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.1.0" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" +checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" dependencies = [ "thiserror", "toml", @@ -4587,7 +4666,7 @@ dependencies = [ "fnv", "lazy_static", "memchr", - "parking_lot", + "parking_lot 0.11.2", "protobuf", "thiserror", ] @@ -4722,6 +4801,15 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "quickcheck" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" +dependencies = [ + "rand 0.8.5", +] + [[package]] name = "quickcheck_macros" version = "0.9.1" @@ -4760,7 +4848,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "545c5bc2b880973c9c10e4067418407a0ccaa3091781d1671d46eb35107cb26f" dependencies = [ "log", - "parking_lot", + "parking_lot 0.11.2", "scheduled-thread-pool", ] @@ -4802,20 +4890,19 @@ dependencies = [ "libc", "rand_chacha 0.2.2", "rand_core 0.5.1", - "rand_hc 0.2.0", + "rand_hc", "rand_pcg", ] [[package]] name = "rand" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", "rand_core 0.6.3", - "rand_hc 0.3.1", ] [[package]] @@ -4853,7 +4940,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ - "getrandom 0.2.4", + "getrandom 0.2.5", ] [[package]] @@ -4865,15 +4952,6 @@ 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_pcg" version = "0.2.1" @@ -4919,9 +4997,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +checksum = "8380fe0152551244f0747b1bf41737e0f8a74f97a14ccefd1148187271634f3c" dependencies = [ "bitflags", ] @@ -4932,15 +5010,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" dependencies = [ - "getrandom 0.2.4", + "getrandom 0.2.5", "redox_syscall", ] [[package]] name = "regex" -version = "1.5.4" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" dependencies = [ "aho-corasick", "memchr", @@ -5134,7 +5212,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.5", + "semver 1.0.6", ] [[package]] @@ -5152,9 +5230,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.20.2" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d37e5e2290f3e040b594b1a9e04377c2c671f1a1cfd9bfdef82106ac1c113f84" +checksum = "4fbfeb8d0ddb84706bc597a5574ab8912817c52a397f819e5b614e2265206921" dependencies = [ "log", "ring", @@ -5229,7 +5307,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc6f74fd1204073fa02d5d5d68bec8021be4c38690b61264b2fdb48083d0e7d7" dependencies = [ - "parking_lot", + "parking_lot 0.11.2", ] [[package]] @@ -5337,9 +5415,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0486718e92ec9a68fbed73bb5ef687d71103b142595b406835649bebd33f72c7" +checksum = "a4a3381e03edd24287172047536f20cabde766e2cd3e65e6b00fb3af51c4f38d" [[package]] name = "semver-parser" @@ -5406,9 +5484,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085" +checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" dependencies = [ "itoa 1.0.1", "ryu", @@ -5463,6 +5541,17 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "sha-1" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.1", + "digest 0.10.3", +] + [[package]] name = "sha2" version = "0.9.9" @@ -5478,13 +5567,13 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99c3bd8169c58782adad9290a9af5939994036b76187f7b4f0e6de91dbbfc0ec" +checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" dependencies = [ "cfg-if", "cpufeatures 0.2.1", - "digest 0.10.1", + "digest 0.10.3", ] [[package]] @@ -5533,6 +5622,18 @@ dependencies = [ "rand_core 0.6.3", ] +[[package]] +name = "simple_asn1" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a762b1c38b9b990c694b9c2f8abe3372ce6a9ceaae6bca39cfc46e054f45745" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time 0.3.7", +] + [[package]] name = "simulator" version = "0.2.0" @@ -5543,7 +5644,7 @@ dependencies = [ "eth1_test_rig", "futures", "node_test_rig", - "parking_lot", + "parking_lot 0.11.2", "rayon", "sensitive_url", "tokio", @@ -5572,7 +5673,7 @@ dependencies = [ "logging", "lru", "maplit", - "parking_lot", + "parking_lot 0.11.2", "rand 0.7.3", "rayon", "safe_arith", @@ -5642,14 +5743,14 @@ dependencies = [ [[package]] name = "slog-json" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f7f7a952ce80fca9da17bf0a53895d11f8aa1ba063668ca53fc72e7869329e9" +checksum = "70f825ce7346f40aa318111df5d3a94945a7fdca9081584cb9b05692fb3dfcb4" dependencies = [ - "chrono", "serde", "serde_json", "slog", + "time 0.3.7", ] [[package]] @@ -5686,15 +5787,15 @@ dependencies = [ [[package]] name = "slog-term" -version = "2.8.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3668dd2252f4381d64de0c79e6c8dc6bd509d1cab3535b35a3fc9bafd1241d5" +checksum = "87d29185c55b7b258b4f120eab00f48557d4d9bc814f41713f449d35b0f8977c" dependencies = [ "atty", - "chrono", "slog", "term", "thread_local", + "time 0.3.7", ] [[package]] @@ -5727,7 +5828,7 @@ version = "0.2.0" dependencies = [ "lazy_static", "lighthouse_metrics", - "parking_lot", + "parking_lot 0.11.2", "types", ] @@ -5756,7 +5857,7 @@ dependencies = [ "rand_core 0.6.3", "ring", "rustc_version 0.4.0", - "sha2 0.10.1", + "sha2 0.10.2", "subtle", ] @@ -5792,8 +5893,8 @@ dependencies = [ "futures", "httparse", "log", - "rand 0.8.4", - "sha-1", + "rand 0.8.5", + "sha-1 0.9.8", ] [[package]] @@ -5808,8 +5909,8 @@ dependencies = [ "futures", "httparse", "log", - "rand 0.8.4", - "sha-1", + "rand 0.8.5", + "sha-1 0.9.8", ] [[package]] @@ -5889,7 +5990,7 @@ dependencies = [ "leveldb", "lighthouse_metrics", "lru", - "parking_lot", + "parking_lot 0.11.2", "safe_arith", "serde", "serde_derive", @@ -6080,9 +6181,9 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" dependencies = [ "winapi-util", ] @@ -6166,10 +6267,19 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d" dependencies = [ + "itoa 1.0.1", "libc", "num_threads", + "quickcheck 1.0.3", + "time-macros", ] +[[package]] +name = "time-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6" + [[package]] name = "timer" version = "0.2.0" @@ -6245,9 +6355,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.16.1" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a" +checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" dependencies = [ "bytes", "libc", @@ -6255,9 +6365,10 @@ dependencies = [ "mio", "num_cpus", "once_cell", - "parking_lot", + "parking_lot 0.12.0", "pin-project-lite 0.2.8", "signal-hook-registry", + "socket2 0.4.4", "tokio-macros", "winapi", ] @@ -6375,9 +6486,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.30" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d8d93354fe2a8e50d5953f5ae2e47a3fc2ef03292e7ea46e3cc38f549525fb9" +checksum = "4a1bdf54a7c28a2bbf701e1d2233f6c77f473486b94bee4f9678da5a148dca7f" dependencies = [ "cfg-if", "log", @@ -6388,9 +6499,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8276d9a4a3a558d7b7ad5303ad50b53d58264641b82914b7ada36bd762e7a716" +checksum = "2e65ce065b4b5c53e73bb28912318cb8c9e9ad3921f1d669eb0e68b4c8143a2b" dependencies = [ "proc-macro2", "quote", @@ -6399,9 +6510,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03cfcb51380632a72d3111cb8d3447a8d908e577d31beeac006f836383d29a23" +checksum = "aa31669fa42c09c34d94d8165dd2012e8ff3c66aca50f3bb226b68f216f2706c" dependencies = [ "lazy_static", "valuable", @@ -6430,9 +6541,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74786ce43333fcf51efe947aed9718fbe46d5c7328ec3f1029e818083966d9aa" +checksum = "9e0ab7bdc962035a87fba73f3acca9b8a8d0034c2e6f60b84aeaaddddc155dce" dependencies = [ "ansi_term", "lazy_static", @@ -6517,7 +6628,7 @@ dependencies = [ "ipnet", "lazy_static", "log", - "rand 0.8.4", + "rand 0.8.5", "smallvec", "thiserror", "tinyvec", @@ -6537,7 +6648,7 @@ dependencies = [ "lazy_static", "log", "lru-cache", - "parking_lot", + "parking_lot 0.11.2", "resolv-conf", "smallvec", "thiserror", @@ -6564,8 +6675,8 @@ dependencies = [ "httparse", "input_buffer", "log", - "rand 0.8.4", - "sha-1", + "rand 0.8.5", + "sha-1 0.9.8", "url", "utf-8", ] @@ -6582,8 +6693,8 @@ dependencies = [ "http", "httparse", "log", - "rand 0.8.4", - "sha-1", + "rand 0.8.5", + "sha-1 0.9.8", "thiserror", "url", "utf-8", @@ -6629,7 +6740,7 @@ dependencies = [ "lazy_static", "log", "milhouse", - "parking_lot", + "parking_lot 0.11.2", "rand 0.7.3", "rand_xorshift", "rayon", @@ -6795,7 +6906,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom 0.2.4", + "getrandom 0.2.5", "serde", ] @@ -6828,7 +6939,7 @@ dependencies = [ "lockfile", "logging", "monitoring_api", - "parking_lot", + "parking_lot 0.11.2", "rand 0.7.3", "reqwest", "ring", @@ -7099,6 +7210,21 @@ dependencies = [ "quote", ] +[[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.56" @@ -7127,7 +7253,7 @@ dependencies = [ "hex", "jsonrpc-core", "log", - "parking_lot", + "parking_lot 0.11.2", "pin-project 1.0.10", "reqwest", "rlp 0.5.1", @@ -7268,6 +7394,49 @@ dependencies = [ "winapi", ] +[[package]] +name = "windows-sys" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df6e476185f92a12c072be4a189a0210dcdcf512a1891d6dff9edb874deadc6" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" + +[[package]] +name = "windows_i686_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" + +[[package]] +name = "windows_i686_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" + [[package]] name = "winreg" version = "0.6.2" @@ -7336,8 +7505,8 @@ dependencies = [ "futures", "log", "nohash-hasher", - "parking_lot", - "rand 0.8.4", + "parking_lot 0.11.2", + "rand 0.8.5", "static_assertions", ] @@ -7352,9 +7521,9 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.3.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81e8f13fef10b63c06356d65d416b070798ddabcadc10d3ece0c5be9b3c7eddb" +checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" dependencies = [ "proc-macro2", "quote", diff --git a/Makefile b/Makefile index 3a4e10d475..e4af6e6283 100644 --- a/Makefile +++ b/Makefile @@ -161,7 +161,7 @@ arbitrary-fuzz: # Runs cargo audit (Audit Cargo.lock files for crates with security vulnerabilities reported to the RustSec Advisory Database) audit: cargo install --force cargo-audit - cargo audit --ignore RUSTSEC-2020-0071 --ignore RUSTSEC-2020-0159 --ignore RUSTSEC-2022-0009 + cargo audit --ignore RUSTSEC-2020-0071 --ignore RUSTSEC-2020-0159 # Runs `cargo vendor` to make sure dependencies can be vendored for packaging, reproducibility and archival purpose. vendor: diff --git a/README.md b/README.md index 00900b8c3d..acf5f5926d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Lighthouse: Ethereum 2.0 +# Lighthouse: Ethereum consensus client -An open-source Ethereum 2.0 client, written in Rust and maintained by Sigma Prime. +An open-source Ethereum consensus client, written in Rust and maintained by Sigma Prime. [![Build Status]][Build Link] [![Book Status]][Book Link] [![Chat Badge]][Chat Link] @@ -22,7 +22,7 @@ An open-source Ethereum 2.0 client, written in Rust and maintained by Sigma Prim Lighthouse is: -- Ready for use on Eth2 mainnet. +- Ready for use on Ethereum consensus mainnet. - Fully open-source, licensed under Apache 2.0. - Security-focused. Fuzzing techniques have been continuously applied and several external security reviews have been performed. - Built in [Rust](https://www.rust-lang.org), a modern language providing unique safety guarantees and @@ -30,13 +30,13 @@ Lighthouse is: - Funded by various organisations, including Sigma Prime, the Ethereum Foundation, ConsenSys, the Decentralization Foundation and private individuals. - Actively involved in the specification and security analysis of the - Ethereum 2.0 specification. + Ethereum proof-of-stake consensus specification. -## Eth2 Deposit Contract +## Staking Deposit Contract The Lighthouse team acknowledges [`0x00000000219ab540356cBB839Cbe05303d7705Fa`](https://etherscan.io/address/0x00000000219ab540356cbb839cbe05303d7705fa) -as the canonical Eth2 deposit contract address. +as the canonical staking deposit contract address. ## Documentation @@ -66,7 +66,7 @@ of the Lighthouse book. ## Contact The best place for discussion is the [Lighthouse Discord -server](https://discord.gg/cyAszAh). +server](https://discord.gg/cyAszAh). Sign up to the [Lighthouse Development Updates](http://eepurl.com/dh9Lvb) mailing list for email notifications about releases, network status and other important information. diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index fb7c73e46d..4ac0ae9a22 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "beacon_node" -version = "2.1.3" +version = "2.1.5" authors = ["Paul Hauner ", "Age Manning VerifiedUnaggregatedAttestation<'a, T> { } /// Returns `Ok(())` if the `attestation.data.beacon_block_root` is known to this chain. -/// You can use this `shuffling_id` to read from the shuffling cache. /// /// The block root may not be known for two reasons: /// diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 8d9be671bf..d8ff6a52e1 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -4,6 +4,7 @@ use crate::attestation_verification::{ VerifiedUnaggregatedAttestation, }; use crate::attester_cache::{AttesterCache, AttesterCacheKey}; +use crate::beacon_proposer_cache::compute_proposer_duties_from_head; use crate::beacon_proposer_cache::BeaconProposerCache; use crate::block_times_cache::BlockTimesCache; use crate::block_verification::{ @@ -35,6 +36,7 @@ use crate::observed_operations::{ObservationOutcome, ObservedOperations}; use crate::persisted_beacon_chain::{PersistedBeaconChain, DUMMY_CANONICAL_HEAD_BLOCK_ROOT}; use crate::persisted_fork_choice::PersistedForkChoice; use crate::pre_finalization_cache::PreFinalizationBlockCache; +use crate::proposer_prep_service::PAYLOAD_PREPARATION_LOOKAHEAD_FACTOR; use crate::shuffling_cache::{BlockShufflingIds, ShufflingCache}; use crate::sync_committee_verification::{ Error as SyncCommitteeError, VerifiedSyncCommitteeMessage, VerifiedSyncContribution, @@ -51,8 +53,8 @@ use crate::{metrics, BeaconChainError}; use eth2::types::{ EventKind, SseBlock, SseChainReorg, SseFinalizedCheckpoint, SseHead, SseLateHead, SyncDuty, }; -use execution_layer::{ExecutionLayer, PayloadStatus}; -use fork_choice::{AttestationFromBlock, ForkChoice}; +use execution_layer::{ExecutionLayer, PayloadAttributes, PayloadStatus}; +use fork_choice::{AttestationFromBlock, ForkChoice, InvalidationOperation}; use futures::channel::mpsc::Sender; use itertools::process_results; use itertools::Itertools; @@ -108,6 +110,11 @@ pub const FORK_CHOICE_DB_KEY: Hash256 = Hash256::zero(); /// Defines how old a block can be before it's no longer a candidate for the early attester cache. const EARLY_ATTESTER_CACHE_HISTORIC_SLOTS: u64 = 4; +/// Defines a distance between the head block slot and the current slot. +/// +/// If the head block is older than this value, don't bother preparing beacon proposers. +const PREPARE_PROPOSER_HISTORIC_EPOCHS: u64 = 4; + /// Reported to the user when the justified block has an invalid execution payload. pub const INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON: &str = "Justified block has an invalid execution payload."; @@ -202,6 +209,7 @@ pub struct HeadInfo { pub proposer_shuffling_decision_root: Hash256, pub is_merge_transition_complete: bool, pub execution_payload_block_hash: Option, + pub random: Hash256, } pub trait BeaconChainTypes: Send + Sync + 'static { @@ -1110,6 +1118,10 @@ impl BeaconChain { .beacon_state .proposer_shuffling_decision_root(head.beacon_block_root)?; + // The `random` value is used whilst producing an `ExecutionPayload` atop the head. + let current_epoch = head.beacon_state.current_epoch(); + let random = *head.beacon_state.get_randao_mix(current_epoch)?; + Ok(HeadInfo { slot: head.beacon_block.slot(), block_root: head.beacon_block_root, @@ -1128,6 +1140,7 @@ impl BeaconChain { .execution_payload() .ok() .map(|ep| ep.block_hash), + random, }) }) } @@ -3135,49 +3148,29 @@ impl BeaconChain { /// This method must be called whenever an execution engine indicates that a payload is /// invalid. /// - /// If the `latest_root` is known to fork-choice it will be invalidated. If it is not known, an - /// error will be returned. + /// Fork choice will be run after the invalidation. The client may be shut down if the `op` + /// results in the justified checkpoint being invalidated. /// - /// If `latest_valid_hash` is `None` or references a block unknown to fork choice, no other - /// blocks will be invalidated. If `latest_valid_hash` is a block known to fork choice, all - /// blocks between the `latest_root` and the `latest_valid_hash` will be invalidated (which may - /// cause further, second-order invalidations). - /// - /// ## Notes - /// - /// Use these rules to set `latest_root`: - /// - /// - When `forkchoiceUpdated` indicates an invalid block, set `latest_root` to be the - /// block root that was the head of the chain when `forkchoiceUpdated` was called. - /// - When `executePayload` returns an invalid block *during* block import, set - /// `latest_root` to be the parent of the beacon block containing the invalid - /// payload (because the block containing the payload is not present in fork choice). - /// - When `executePayload` returns an invalid block *after* block import, set - /// `latest_root` to be root of the beacon block containing the invalid payload. + /// See the documentation of `InvalidationOperation` for information about defining `op`. pub fn process_invalid_execution_payload( &self, - latest_root: Hash256, - latest_valid_hash: Option, + op: &InvalidationOperation, ) -> Result<(), Error> { debug!( self.log, "Invalid execution payload in block"; - "latest_valid_hash" => ?latest_valid_hash, - "latest_root" => ?latest_root, + "latest_valid_ancestor" => ?op.latest_valid_ancestor(), + "block_root" => ?op.block_root(), ); // Update fork choice. - if let Err(e) = self - .fork_choice - .write() - .on_invalid_execution_payload(latest_root, latest_valid_hash) - { + if let Err(e) = self.fork_choice.write().on_invalid_execution_payload(op) { crit!( self.log, "Failed to process invalid payload"; "error" => ?e, - "latest_valid_hash" => ?latest_valid_hash, - "latest_root" => ?latest_root, + "latest_valid_ancestor" => ?op.latest_valid_ancestor(), + "block_root" => ?op.block_root(), ); } @@ -3395,13 +3388,6 @@ impl BeaconChain { .attester_shuffling_decision_root(self.genesis_block_root, RelativeEpoch::Current); // Used later for the execution engine. - let new_head_execution_block_hash_opt = new_head - .beacon_block - .message() - .body() - .execution_payload() - .ok() - .map(|ep| ep.block_hash); let is_merge_transition_complete = is_merge_transition_complete(&new_head.beacon_state); drop(lag_timer); @@ -3597,24 +3583,260 @@ impl BeaconChain { } // If this is a post-merge block, update the execution layer. - if let Some(new_head_execution_block_hash) = new_head_execution_block_hash_opt { - if is_merge_transition_complete { - let finalized_execution_block_hash = finalized_block - .execution_status - .block_hash() - .unwrap_or_else(ExecutionBlockHash::zero); - if let Err(e) = self.update_execution_engine_forkchoice_blocking( - finalized_execution_block_hash, - beacon_block_root, - new_head_execution_block_hash, - ) { - crit!( - self.log, - "Failed to update execution head"; - "error" => ?e - ); - } + if is_merge_transition_complete { + let current_slot = self.slot()?; + + if let Err(e) = self.update_execution_engine_forkchoice_blocking(current_slot) { + crit!( + self.log, + "Failed to update execution head"; + "error" => ?e + ); } + + // Performing this call immediately after + // `update_execution_engine_forkchoice_blocking` might result in two calls to fork + // choice updated, one *without* payload attributes and then a second *with* + // payload attributes. + // + // This seems OK. It's not a significant waste of EL<>CL bandwidth or resources, as + // far as I know. + if let Err(e) = self.prepare_beacon_proposer_blocking() { + crit!( + self.log, + "Failed to prepare proposers after fork choice"; + "error" => ?e + ); + } + } + + Ok(()) + } + + pub fn prepare_beacon_proposer_blocking(&self) -> Result<(), Error> { + let execution_layer = self + .execution_layer + .as_ref() + .ok_or(Error::ExecutionLayerMissing)?; + + execution_layer + .block_on_generic(|_| self.prepare_beacon_proposer_async()) + .map_err(Error::PrepareProposerBlockingFailed)? + } + + /// Determines the beacon proposer for the next slot. If that proposer is registered in the + /// `execution_layer`, provide the `execution_layer` with the necessary information to produce + /// `PayloadAttributes` for future calls to fork choice. + /// + /// The `PayloadAttributes` are used by the EL to give it a look-ahead for preparing an optimal + /// set of transactions for a new `ExecutionPayload`. + /// + /// This function will result in a call to `forkchoiceUpdated` on the EL if: + /// + /// 1. We're in the tail-end of the slot (as defined by PAYLOAD_PREPARATION_LOOKAHEAD_FACTOR) + /// 2. The head block is one slot (or less) behind the prepare slot (e.g., we're preparing for + /// the next slot and the block at the current slot is already known). + pub async fn prepare_beacon_proposer_async(&self) -> Result<(), Error> { + let execution_layer = self + .execution_layer + .clone() + .ok_or(Error::ExecutionLayerMissing)?; + + // Nothing to do if there are no proposers registered with the EL, exit early to avoid + // wasting cycles. + if !execution_layer.has_any_proposer_preparation_data().await { + return Ok(()); + } + + let head = self.head_info()?; + let current_slot = self.slot()?; + + // Don't bother with proposer prep if the head is more than + // `PREPARE_PROPOSER_HISTORIC_EPOCHS` prior to the current slot. + // + // This prevents the routine from running during sync. + if head.slot + T::EthSpec::slots_per_epoch() * PREPARE_PROPOSER_HISTORIC_EPOCHS + < current_slot + { + debug!( + self.log, + "Head too old for proposer prep"; + "head_slot" => head.slot, + "current_slot" => current_slot, + ); + return Ok(()); + } + + // We only start to push preparation data for some chain *after* the transition block + // has been imported. + // + // There is no payload preparation for the transition block (i.e., the first block with + // execution enabled in some chain). + if head.execution_payload_block_hash.is_none() { + return Ok(()); + }; + + let head_epoch = head.slot.epoch(T::EthSpec::slots_per_epoch()); + let prepare_slot = current_slot + 1; + let prepare_epoch = prepare_slot.epoch(T::EthSpec::slots_per_epoch()); + + // Ensure that the shuffling decision root is correct relative to the epoch we wish to + // query. + let shuffling_decision_root = if head_epoch == prepare_epoch { + head.proposer_shuffling_decision_root + } else { + head.block_root + }; + + // Read the proposer from the proposer cache. + let cached_proposer = self + .beacon_proposer_cache + .lock() + .get_slot::(shuffling_decision_root, prepare_slot); + let proposer = if let Some(proposer) = cached_proposer { + proposer.index + } else { + if head_epoch + 2 < prepare_epoch { + warn!( + self.log, + "Skipping proposer preparation"; + "msg" => "this is a non-critical issue that can happen on unhealthy nodes or \ + networks.", + "prepare_epoch" => prepare_epoch, + "head_epoch" => head_epoch, + ); + + // Don't skip the head forward more than two epochs. This avoids burdening an + // unhealthy node. + // + // Although this node might miss out on preparing for a proposal, they should still + // be able to propose. This will prioritise beacon chain health over efficient + // packing of execution blocks. + return Ok(()); + } + + let (proposers, decision_root, fork) = + compute_proposer_duties_from_head(prepare_epoch, self)?; + + let proposer_index = prepare_slot.as_usize() % (T::EthSpec::slots_per_epoch() as usize); + let proposer = *proposers + .get(proposer_index) + .ok_or(BeaconChainError::NoProposerForSlot(prepare_slot))?; + + self.beacon_proposer_cache.lock().insert( + prepare_epoch, + decision_root, + proposers, + fork, + )?; + + // It's possible that the head changes whilst computing these duties. If so, abandon + // this routine since the change of head would have also spawned another instance of + // this routine. + // + // Exit now, after updating the cache. + if decision_root != shuffling_decision_root { + warn!( + self.log, + "Head changed during proposer preparation"; + ); + return Ok(()); + } + + proposer + }; + + // If the execution layer doesn't have any proposer data for this validator then we assume + // it's not connected to this BN and no action is required. + if !execution_layer + .has_proposer_preparation_data(proposer as u64) + .await + { + return Ok(()); + } + + let payload_attributes = PayloadAttributes { + timestamp: self + .slot_clock + .start_of(prepare_slot) + .ok_or(Error::InvalidSlot(prepare_slot))? + .as_secs(), + prev_randao: head.random, + suggested_fee_recipient: execution_layer + .get_suggested_fee_recipient(proposer as u64) + .await, + }; + + debug!( + self.log, + "Preparing beacon proposer"; + "payload_attributes" => ?payload_attributes, + "head_root" => ?head.block_root, + "prepare_slot" => prepare_slot, + "validator" => proposer, + ); + + let already_known = execution_layer + .insert_proposer( + prepare_slot, + head.block_root, + proposer as u64, + payload_attributes, + ) + .await; + // Only push a log to the user if this is the first time we've seen this proposer for this + // slot. + if !already_known { + info!( + self.log, + "Prepared beacon proposer"; + "already_known" => already_known, + "prepare_slot" => prepare_slot, + "validator" => proposer, + ); + } + + let till_prepare_slot = + if let Some(duration) = self.slot_clock.duration_to_slot(prepare_slot) { + duration + } else { + // `SlotClock::duration_to_slot` will return `None` when we are past the start + // of `prepare_slot`. Don't bother sending a `forkchoiceUpdated` in that case, + // it's too late. + // + // This scenario might occur on an overloaded/under-resourced node. + warn!( + self.log, + "Delayed proposer preparation"; + "prepare_slot" => prepare_slot, + "validator" => proposer, + ); + return Ok(()); + }; + + // If either of the following are true, send a fork-choice update message to the + // EL: + // + // 1. We're in the tail-end of the slot (as defined by + // PAYLOAD_PREPARATION_LOOKAHEAD_FACTOR) + // 2. The head block is one slot (or less) behind the prepare slot (e.g., we're + // preparing for the next slot and the block at the current slot is already + // known). + if till_prepare_slot + <= self.slot_clock.slot_duration() / PAYLOAD_PREPARATION_LOOKAHEAD_FACTOR + || head.slot + 1 >= prepare_slot + { + debug!( + self.log, + "Pushing update to prepare proposer"; + "till_prepare_slot" => ?till_prepare_slot, + "prepare_slot" => prepare_slot + ); + + // Use the blocking method here so that we don't form a queue of these functions when + // routinely calling them. + self.update_execution_engine_forkchoice_async(current_slot) + .await?; } Ok(()) @@ -3622,9 +3844,7 @@ impl BeaconChain { pub fn update_execution_engine_forkchoice_blocking( &self, - finalized_execution_block_hash: ExecutionBlockHash, - head_block_root: Hash256, - head_execution_block_hash: ExecutionBlockHash, + current_slot: Slot, ) -> Result<(), Error> { let execution_layer = self .execution_layer @@ -3632,34 +3852,76 @@ impl BeaconChain { .ok_or(Error::ExecutionLayerMissing)?; execution_layer - .block_on_generic(|_| { - self.update_execution_engine_forkchoice_async( - finalized_execution_block_hash, - head_block_root, - head_execution_block_hash, - ) - }) + .block_on_generic(|_| self.update_execution_engine_forkchoice_async(current_slot)) .map_err(Error::ForkchoiceUpdate)? } pub async fn update_execution_engine_forkchoice_async( &self, - finalized_execution_block_hash: ExecutionBlockHash, - head_block_root: Hash256, - head_execution_block_hash: ExecutionBlockHash, + current_slot: Slot, ) -> Result<(), Error> { + let execution_layer = self + .execution_layer + .as_ref() + .ok_or(Error::ExecutionLayerMissing)?; + + // Take the global lock for updating the execution engine fork choice. + // + // Whilst holding this lock we must: + // + // 1. Read the canonical head. + // 2. Issue a forkchoiceUpdated call to the execution engine. + // + // This will allow us to ensure that we provide the execution layer with an *ordered* view + // of the head. I.e., we will never communicate a past head after communicating a later + // one. + // + // There is a "deadlock warning" in this function. The downside of this nice ordering is the + // potential for deadlock. I would advise against any other use of + // `execution_engine_forkchoice_lock` apart from the one here. + let forkchoice_lock = execution_layer.execution_engine_forkchoice_lock().await; + + // Deadlock warning: + // + // We are taking the `self.fork_choice` lock whilst holding the `forkchoice_lock`. This + // is intentional, since it allows us to ensure a consistent ordering of messages to the + // execution layer. + let (head_block_root, head_hash, finalized_hash) = + if let Some(params) = self.fork_choice.read().get_forkchoice_update_parameters() { + if let Some(head_hash) = params.head_hash { + ( + params.head_root, + head_hash, + params + .finalized_hash + .unwrap_or_else(ExecutionBlockHash::zero), + ) + } else { + // The head block does not have an execution block hash, there is no need to + // send an update to the EL. + return Ok(()); + } + } else { + warn!( + self.log, + "Missing forkchoice params"; + "msg" => "please report this non-critical bug" + ); + return Ok(()); + }; + let forkchoice_updated_response = self .execution_layer .as_ref() .ok_or(Error::ExecutionLayerMissing)? - .notify_forkchoice_updated( - head_execution_block_hash, - finalized_execution_block_hash, - None, - ) + .notify_forkchoice_updated(head_hash, finalized_hash, current_slot, head_block_root) .await .map_err(Error::ExecutionForkChoiceUpdateFailed); + // The head has been read and the execution layer has been updated. It is now valid to send + // another fork choice update. + drop(forkchoice_lock); + match forkchoice_updated_response { Ok(status) => match &status { PayloadStatus::Valid | PayloadStatus::Syncing => Ok(()), @@ -3687,8 +3949,11 @@ impl BeaconChain { // The execution engine has stated that all blocks between the // `head_execution_block_hash` and `latest_valid_hash` are invalid. self.process_invalid_execution_payload( - head_block_root, - Some(*latest_valid_hash), + &InvalidationOperation::InvalidateMany { + head_block_root, + always_invalidate_head: true, + latest_valid_ancestor: *latest_valid_hash, + }, )?; Err(BeaconChainError::ExecutionForkChoiceUpdateInvalid { status }) @@ -3705,7 +3970,11 @@ impl BeaconChain { // // Using a `None` latest valid ancestor will result in only the head block // being invalidated (no ancestors). - self.process_invalid_execution_payload(head_block_root, None)?; + self.process_invalid_execution_payload( + &InvalidationOperation::InvalidateOne { + block_root: head_block_root, + }, + )?; Err(BeaconChainError::ExecutionForkChoiceUpdateInvalid { status }) } diff --git a/beacon_node/beacon_chain/src/beacon_proposer_cache.rs b/beacon_node/beacon_chain/src/beacon_proposer_cache.rs index 646884b606..d645201a58 100644 --- a/beacon_node/beacon_chain/src/beacon_proposer_cache.rs +++ b/beacon_node/beacon_chain/src/beacon_proposer_cache.rs @@ -8,9 +8,14 @@ //! very simple to reason about, but it might store values that are useless due to finalization. The //! values it stores are very small, so this should not be an issue. +use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; use lru::LruCache; use smallvec::SmallVec; -use types::{BeaconStateError, Epoch, EthSpec, Fork, Hash256, Slot, Unsigned}; +use state_processing::state_advance::partial_state_advance; +use std::cmp::Ordering; +use types::{ + BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, Fork, Hash256, Slot, Unsigned, +}; /// The number of sets of proposer indices that should be cached. const CACHE_SIZE: usize = 16; @@ -125,3 +130,56 @@ impl BeaconProposerCache { Ok(()) } } + +/// Compute the proposer duties using the head state without cache. +pub fn compute_proposer_duties_from_head( + current_epoch: Epoch, + chain: &BeaconChain, +) -> Result<(Vec, Hash256, Fork), BeaconChainError> { + // Take a copy of the head of the chain. + let head = chain.head()?; + let mut state = head.beacon_state; + let head_state_root = head.beacon_block.state_root(); + + // Advance the state into the requested epoch. + ensure_state_is_in_epoch(&mut state, head_state_root, current_epoch, &chain.spec)?; + + let indices = state + .get_beacon_proposer_indices(&chain.spec) + .map_err(BeaconChainError::from)?; + + let dependent_root = state + // The only block which decides its own shuffling is the genesis block. + .proposer_shuffling_decision_root(chain.genesis_block_root) + .map_err(BeaconChainError::from)?; + + Ok((indices, dependent_root, state.fork())) +} + +/// If required, advance `state` to `target_epoch`. +/// +/// ## Details +/// +/// - Returns an error if `state.current_epoch() > target_epoch`. +/// - No-op if `state.current_epoch() == target_epoch`. +/// - It must be the case that `state.canonical_root() == state_root`, but this function will not +/// check that. +pub fn ensure_state_is_in_epoch( + state: &mut BeaconState, + state_root: Hash256, + target_epoch: Epoch, + spec: &ChainSpec, +) -> Result<(), BeaconChainError> { + match state.current_epoch().cmp(&target_epoch) { + // Protects against an inconsistent slot clock. + Ordering::Greater => Err(BeaconStateError::SlotOutOfBounds.into()), + // The state needs to be advanced. + Ordering::Less => { + let target_slot = target_epoch.start_slot(E::slots_per_epoch()); + partial_state_advance(state, Some(state_root), target_slot, spec) + .map_err(BeaconChainError::from) + } + // The state is suitable, nothing to do. + Ordering::Equal => Ok(()), + } +} diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index d8e15e644d..b758d4e4b3 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -56,7 +56,7 @@ use fork_choice::{ForkChoice, ForkChoiceStore, PayloadVerificationStatus}; use parking_lot::RwLockReadGuard; use proto_array::Block as ProtoBlock; use safe_arith::ArithError; -use slog::{debug, error, Logger}; +use slog::{debug, error, info, Logger}; use slot_clock::SlotClock; use ssz::Encode; use state_processing::per_block_processing::is_merge_transition_block; @@ -78,6 +78,30 @@ use types::{ SignedBeaconBlock, SignedBeaconBlockHeader, Slot, }; +const POS_PANDA_BANNER: &str = r#" + ,,, ,,, ,,, ,,, + ;" ^; ;' ", ;" ^; ;' ", + ; s$$$$$$$s ; ; s$$$$$$$s ; + , ss$$$$$$$$$$s ,' ooooooooo. .oooooo. .oooooo..o , ss$$$$$$$$$$s ,' + ;s$$$$$$$$$$$$$$$ `888 `Y88. d8P' `Y8b d8P' `Y8 ;s$$$$$$$$$$$$$$$ + $$$$$$$$$$$$$$$$$$ 888 .d88'888 888Y88bo. $$$$$$$$$$$$$$$$$$ + $$$$P""Y$$$Y""W$$$$$ 888ooo88P' 888 888 `"Y8888o. $$$$P""Y$$$Y""W$$$$$ + $$$$ p"LFG"q $$$$$ 888 888 888 `"Y88b $$$$ p"LFG"q $$$$$ + $$$$ .$$$$$. $$$$ 888 `88b d88'oo .d8P $$$$ .$$$$$. $$$$ + $$DcaU$$$$$$$$$$ o888o `Y8bood8P' 8""88888P' $$DcaU$$$$$$$$$$ + "Y$$$"*"$$$Y" "Y$$$"*"$$$Y" + "$b.$$" "$b.$$" + + .o. . o8o . .o8 + .888. .o8 `"' .o8 "888 + .8"888. .ooooo. .o888oooooo oooo ooo .oooo. .o888oo .ooooo. .oooo888 + .8' `888. d88' `"Y8 888 `888 `88. .8' `P )88b 888 d88' `88bd88' `888 + .88ooo8888. 888 888 888 `88..8' .oP"888 888 888ooo888888 888 + .8' `888. 888 .o8 888 . 888 `888' d8( 888 888 .888 .o888 888 + o88o o8888o`Y8bod8P' "888"o888o `8' `Y888""8o "888"`Y8bod8P'`Y8bod88P" + +"#; + /// Maximum block slot number. Block with slots bigger than this constant will NOT be processed. const MAXIMUM_BLOCK_SLOT_NUMBER: u64 = 4_294_967_296; // 2^32 @@ -309,6 +333,8 @@ pub enum ExecutionPayloadError { /// /// The peer is not necessarily invalid. PoWParentMissing(ExecutionBlockHash), + /// The execution node is syncing but we fail the conditions for optimistic sync + UnverifiedNonOptimisticCandidate, } impl From for ExecutionPayloadError { @@ -1114,9 +1140,13 @@ impl<'a, T: BeaconChainTypes> FullyVerifiedBlock<'a, T> { // early. // - Doing the check here means we can keep our fork-choice implementation "pure". I.e., no // calls to remote servers. - if is_merge_transition_block(&state, block.message().body()) { - validate_merge_block(chain, block.message())? - } + let valid_merge_transition_block = + if is_merge_transition_block(&state, block.message().body()) { + validate_merge_block(chain, block.message())?; + true + } else { + false + }; // The specification declares that this should be run *inside* `per_block_processing`, // however we run it here to keep `per_block_processing` pure (i.e., no calls to external @@ -1126,6 +1156,29 @@ impl<'a, T: BeaconChainTypes> FullyVerifiedBlock<'a, T> { // `randao` may change. let payload_verification_status = notify_new_payload(chain, &state, block.message())?; + // If the payload did not validate or invalidate the block, check to see if this block is + // valid for optimistic import. + if payload_verification_status == PayloadVerificationStatus::NotVerified { + let current_slot = chain + .slot_clock + .now() + .ok_or(BeaconChainError::UnableToReadSlot)?; + + if !chain + .fork_choice + .read() + .is_optimistic_candidate_block( + current_slot, + block.slot(), + &block.parent_root(), + &chain.spec, + ) + .map_err(BeaconChainError::from)? + { + return Err(ExecutionPayloadError::UnverifiedNonOptimisticCandidate.into()); + } + } + // If the block is sufficiently recent, notify the validator monitor. if let Some(slot) = chain.slot_clock.now() { let epoch = slot.epoch(T::EthSpec::slots_per_epoch()); @@ -1236,6 +1289,14 @@ impl<'a, T: BeaconChainTypes> FullyVerifiedBlock<'a, T> { }); } + if valid_merge_transition_block { + info!(chain.log, "{}", POS_PANDA_BANNER); + info!(chain.log, "Proof of Stake Activated"; "slot" => block.slot()); + info!(chain.log, ""; "Terminal POW Block Hash" => ?block.message().execution_payload()?.parent_hash.into_root()); + info!(chain.log, ""; "Merge Transition Block Root" => ?block.message().tree_hash_root()); + info!(chain.log, ""; "Merge Transition Execution Hash" => ?block.message().execution_payload()?.block_hash.into_root()); + } + Ok(Self { block, block_root, diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index e8cc157ce4..79f7346ca2 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -138,6 +138,7 @@ pub enum BeaconChainError { AltairForkDisabled, ExecutionLayerMissing, ExecutionForkChoiceUpdateFailed(execution_layer::Error), + PrepareProposerBlockingFailed(execution_layer::Error), ExecutionForkChoiceUpdateInvalid { status: PayloadStatus, }, @@ -160,6 +161,7 @@ pub enum BeaconChainError { head_state: Checkpoint, fork_choice: Hash256, }, + InvalidSlot(Slot), } easy_from_to!(SlotProcessingError, BeaconChainError); diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index 30a0d2b198..0ee9e4b876 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -12,7 +12,7 @@ use crate::{ ExecutionPayloadError, }; use execution_layer::PayloadStatus; -use fork_choice::PayloadVerificationStatus; +use fork_choice::{InvalidationOperation, PayloadVerificationStatus}; use proto_array::{Block as ProtoBlock, ExecutionStatus}; use slog::debug; use slot_clock::SlotClock; @@ -68,7 +68,13 @@ pub fn notify_new_payload( // This block has not yet been applied to fork choice, so the latest block that was // imported to fork choice was the parent. let latest_root = block.parent_root(); - chain.process_invalid_execution_payload(latest_root, Some(latest_valid_hash))?; + chain.process_invalid_execution_payload( + &InvalidationOperation::InvalidateMany { + head_block_root: latest_root, + always_invalidate_head: false, + latest_valid_ancestor: latest_valid_hash, + }, + )?; Err(ExecutionPayloadError::RejectedByExecutionEngine { status }.into()) } @@ -141,13 +147,33 @@ pub fn validate_merge_block( } .into()), None => { - debug!( - chain.log, - "Optimistically accepting terminal block"; - "block_hash" => ?execution_payload.parent_hash, - "msg" => "the terminal block/parent was unavailable" - ); - Ok(()) + let current_slot = chain + .slot_clock + .now() + .ok_or(BeaconChainError::UnableToReadSlot)?; + + // Ensure the block is a candidate for optimistic import. + if chain + .fork_choice + .read() + .is_optimistic_candidate_block( + current_slot, + block.slot(), + &block.parent_root(), + &chain.spec, + ) + .map_err(BeaconChainError::from)? + { + debug!( + chain.log, + "Optimistically accepting terminal block"; + "block_hash" => ?execution_payload.parent_hash, + "msg" => "the terminal block/parent was unavailable" + ); + Ok(()) + } else { + Err(ExecutionPayloadError::UnverifiedNonOptimisticCandidate.into()) + } } } } diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 9e53fd251f..063fa3b23e 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -3,7 +3,7 @@ pub mod attestation_verification; mod attester_cache; mod beacon_chain; mod beacon_fork_choice_store; -mod beacon_proposer_cache; +pub mod beacon_proposer_cache; mod beacon_snapshot; pub mod block_reward; mod block_times_cache; @@ -28,6 +28,7 @@ pub mod observed_operations; mod persisted_beacon_chain; mod persisted_fork_choice; mod pre_finalization_cache; +pub mod proposer_prep_service; pub mod schema_change; mod shuffling_cache; pub mod state_advance_timer; diff --git a/beacon_node/beacon_chain/src/proposer_prep_service.rs b/beacon_node/beacon_chain/src/proposer_prep_service.rs new file mode 100644 index 0000000000..59977f02c8 --- /dev/null +++ b/beacon_node/beacon_chain/src/proposer_prep_service.rs @@ -0,0 +1,74 @@ +use crate::{BeaconChain, BeaconChainTypes}; +use slog::{debug, error}; +use slot_clock::SlotClock; +use std::sync::Arc; +use task_executor::TaskExecutor; +use tokio::time::sleep; + +/// At 12s slot times, the means that the payload preparation routine will run 4s before the start +/// of each slot (`12 / 3 = 4`). +pub const PAYLOAD_PREPARATION_LOOKAHEAD_FACTOR: u32 = 3; + +/// Spawns a routine which ensures the EL is provided advance notice of any block producers. +/// +/// This routine will run once per slot, at `slot_duration / PAYLOAD_PREPARATION_LOOKAHEAD_FACTOR` +/// before the start of each slot. +/// +/// The service will not be started if there is no `execution_layer` on the `chain`. +pub fn start_proposer_prep_service( + executor: TaskExecutor, + chain: Arc>, +) { + // Avoid spawning the service if there's no EL, it'll just error anyway. + if chain.execution_layer.is_some() { + executor.clone().spawn( + async move { proposer_prep_service(executor, chain).await }, + "proposer_prep_service", + ); + } +} + +/// Loop indefinitely, calling `BeaconChain::prepare_beacon_proposer_async` at an interval. +async fn proposer_prep_service( + executor: TaskExecutor, + chain: Arc>, +) { + let slot_duration = chain.slot_clock.slot_duration(); + + loop { + match chain.slot_clock.duration_to_next_slot() { + Some(duration) => { + let additional_delay = slot_duration + - chain.slot_clock.slot_duration() / PAYLOAD_PREPARATION_LOOKAHEAD_FACTOR; + sleep(duration + additional_delay).await; + + debug!( + chain.log, + "Proposer prepare routine firing"; + ); + + let inner_chain = chain.clone(); + executor.spawn( + async move { + if let Err(e) = inner_chain.prepare_beacon_proposer_async().await { + error!( + inner_chain.log, + "Proposer prepare routine failed"; + "error" => ?e + ); + } + }, + "proposer_prep_update", + ); + + continue; + } + None => { + error!(chain.log, "Failed to read slot clock"); + // If we can't read the slot clock, just wait another slot. + sleep(slot_duration).await; + continue; + } + }; + } +} diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 76d73d4772..722e725737 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -337,14 +337,20 @@ where let el_runtime = ExecutionLayerRuntime::default(); - let urls = urls + let urls: Vec = urls .iter() .map(|s| SensitiveUrl::parse(*s)) .collect::>() .unwrap(); - let execution_layer = ExecutionLayer::from_urls( - urls, - Some(Address::repeat_byte(42)), + + let config = execution_layer::Config { + execution_endpoints: urls, + secret_files: vec![], + suggested_fee_recipient: Some(Address::repeat_byte(42)), + ..Default::default() + }; + let execution_layer = ExecutionLayer::from_config( + config, el_runtime.task_executor.clone(), el_runtime.log.clone(), ) diff --git a/beacon_node/beacon_chain/src/validator_pubkey_cache.rs b/beacon_node/beacon_chain/src/validator_pubkey_cache.rs index da55e73e82..f97c3e4103 100644 --- a/beacon_node/beacon_chain/src/validator_pubkey_cache.rs +++ b/beacon_node/beacon_chain/src/validator_pubkey_cache.rs @@ -3,7 +3,7 @@ use crate::{BeaconChainTypes, BeaconStore}; use ssz::{Decode, DecodeError, Encode}; use std::collections::HashMap; use std::convert::TryInto; -use std::fs::{File, OpenOptions}; +use std::fs::File; use std::io::{self, Read, Write}; use std::path::Path; use store::{DBColumn, Error as StoreError, StoreItem}; @@ -257,7 +257,7 @@ impl From for BeaconChainError { impl ValidatorPubkeyCacheFile { /// Opens an existing file for reading and writing. pub fn open>(path: P) -> Result { - OpenOptions::new() + File::options() .read(true) .write(true) .create(false) @@ -455,7 +455,7 @@ mod test { let cache = ValidatorPubkeyCache::::load_from_file(&path).expect("should open cache"); drop(cache); - let mut file = OpenOptions::new() + let mut file = File::options() .write(true) .append(true) .open(&path) diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index 198f674157..4d2dfccac2 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -5,7 +5,11 @@ use beacon_chain::{ BeaconChainError, BlockError, ExecutionPayloadError, HeadInfo, StateSkipConfig, WhenSlotSkipped, INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON, }; +use execution_layer::{ + json_structures::JsonPayloadAttributesV1, ExecutionLayer, PayloadAttributes, +}; use proto_array::ExecutionStatus; +use slot_clock::SlotClock; use task_executor::ShutdownReason; use types::*; @@ -54,6 +58,10 @@ impl InvalidPayloadRig { self } + fn execution_layer(&self) -> ExecutionLayer { + self.harness.chain.execution_layer.clone().unwrap() + } + fn block_hash(&self, block_root: Hash256) -> ExecutionBlockHash { self.harness .chain @@ -85,6 +93,19 @@ impl InvalidPayloadRig { self.harness.chain.head_info().unwrap() } + fn previous_payload_attributes(&self) -> PayloadAttributes { + let mock_execution_layer = self.harness.mock_execution_layer.as_ref().unwrap(); + let json = mock_execution_layer + .server + .take_previous_request() + .expect("no previous request"); + let params = json.get("params").expect("no params"); + let payload_param_json = params.get(1).expect("no payload param"); + let attributes: JsonPayloadAttributesV1 = + serde_json::from_value(payload_param_json.clone()).unwrap(); + attributes.into() + } + fn move_to_terminal_block(&self) { let mock_execution_layer = self.harness.mock_execution_layer.as_ref().unwrap(); mock_execution_layer @@ -231,8 +252,10 @@ fn valid_invalid_syncing() { /// `latest_valid_hash`. #[test] fn invalid_payload_invalidates_parent() { - let mut rig = InvalidPayloadRig::new(); + let mut rig = InvalidPayloadRig::new().enable_attestations(); rig.move_to_terminal_block(); + rig.import_block(Payload::Valid); // Import a valid transition block. + rig.move_to_first_justification(Payload::Syncing); let roots = vec![ rig.import_block(Payload::Syncing), @@ -258,6 +281,7 @@ fn invalid_payload_invalidates_parent() { fn justified_checkpoint_becomes_invalid() { let mut rig = InvalidPayloadRig::new().enable_attestations(); rig.move_to_terminal_block(); + rig.import_block(Payload::Valid); // Import a valid transition block. rig.move_to_first_justification(Payload::Syncing); let justified_checkpoint = rig.head_info().current_justified_checkpoint; @@ -305,7 +329,9 @@ fn pre_finalized_latest_valid_hash() { let mut rig = InvalidPayloadRig::new().enable_attestations(); rig.move_to_terminal_block(); - let blocks = rig.build_blocks(num_blocks, Payload::Syncing); + let mut blocks = vec![]; + blocks.push(rig.import_block(Payload::Valid)); // Import a valid transition block. + blocks.extend(rig.build_blocks(num_blocks - 1, Payload::Syncing)); assert_eq!(rig.head_info().finalized_checkpoint.epoch, finalized_epoch); @@ -330,7 +356,11 @@ fn pre_finalized_latest_valid_hash() { for i in E::slots_per_epoch() * finalized_epoch..num_blocks { let slot = Slot::new(i); let root = rig.block_root_at_slot(slot).unwrap(); - assert!(rig.execution_status(root).is_not_verified()); + if slot == 1 { + assert!(rig.execution_status(root).is_valid()); + } else { + assert!(rig.execution_status(root).is_not_verified()); + } } } @@ -344,7 +374,10 @@ fn latest_valid_hash_will_validate() { let mut rig = InvalidPayloadRig::new().enable_attestations(); rig.move_to_terminal_block(); - let blocks = rig.build_blocks(4, Payload::Syncing); + + let mut blocks = vec![]; + blocks.push(rig.import_block(Payload::Valid)); // Import a valid transition block. + blocks.extend(rig.build_blocks(4, Payload::Syncing)); let latest_valid_root = rig .block_root_at_slot(Slot::new(LATEST_VALID_SLOT)) @@ -357,7 +390,7 @@ fn latest_valid_hash_will_validate() { assert_eq!(rig.head_info().slot, LATEST_VALID_SLOT); - for slot in 0..=4 { + for slot in 0..=5 { let slot = Slot::new(slot); let root = if slot > 0 { // If not the genesis slot, check the blocks we just produced. @@ -386,7 +419,9 @@ fn latest_valid_hash_is_junk() { let mut rig = InvalidPayloadRig::new().enable_attestations(); rig.move_to_terminal_block(); - let blocks = rig.build_blocks(num_blocks, Payload::Syncing); + let mut blocks = vec![]; + blocks.push(rig.import_block(Payload::Valid)); // Import a valid transition block. + blocks.extend(rig.build_blocks(num_blocks, Payload::Syncing)); assert_eq!(rig.head_info().finalized_checkpoint.epoch, finalized_epoch); @@ -408,7 +443,11 @@ fn latest_valid_hash_is_junk() { for i in E::slots_per_epoch() * finalized_epoch..num_blocks { let slot = Slot::new(i); let root = rig.block_root_at_slot(slot).unwrap(); - assert!(rig.execution_status(root).is_not_verified()); + if slot == 1 { + assert!(rig.execution_status(root).is_valid()); + } else { + assert!(rig.execution_status(root).is_not_verified()); + } } } @@ -421,6 +460,7 @@ fn invalidates_all_descendants() { let mut rig = InvalidPayloadRig::new().enable_attestations(); rig.move_to_terminal_block(); + rig.import_block(Payload::Valid); // Import a valid transition block. let blocks = rig.build_blocks(num_blocks, Payload::Syncing); assert_eq!(rig.head_info().finalized_checkpoint.epoch, finalized_epoch); @@ -493,6 +533,7 @@ fn switches_heads() { let mut rig = InvalidPayloadRig::new().enable_attestations(); rig.move_to_terminal_block(); + rig.import_block(Payload::Valid); // Import a valid transition block. let blocks = rig.build_blocks(num_blocks, Payload::Syncing); assert_eq!(rig.head_info().finalized_checkpoint.epoch, finalized_epoch); @@ -571,8 +612,9 @@ fn invalid_during_processing() { #[test] fn invalid_after_optimistic_sync() { - let mut rig = InvalidPayloadRig::new(); + let mut rig = InvalidPayloadRig::new().enable_attestations(); rig.move_to_terminal_block(); + rig.import_block(Payload::Valid); // Import a valid transition block. let mut roots = vec![ rig.import_block(Payload::Syncing), @@ -599,3 +641,55 @@ fn invalid_after_optimistic_sync() { let head = rig.harness.chain.head_info().unwrap(); assert_eq!(head.block_root, roots[1]); } + +#[test] +fn payload_preparation() { + let mut rig = InvalidPayloadRig::new(); + rig.move_to_terminal_block(); + rig.import_block(Payload::Valid); + + let el = rig.execution_layer(); + let head = rig.harness.chain.head().unwrap(); + let current_slot = rig.harness.chain.slot().unwrap(); + assert_eq!(head.beacon_state.slot(), 1); + assert_eq!(current_slot, 1); + + let next_slot = current_slot + 1; + let proposer = head + .beacon_state + .get_beacon_proposer_index(next_slot, &rig.harness.chain.spec) + .unwrap(); + + let fee_recipient = Address::repeat_byte(99); + + // Provide preparation data to the EL for `proposer`. + el.update_proposer_preparation_blocking( + Epoch::new(1), + &[ProposerPreparationData { + validator_index: proposer as u64, + fee_recipient, + }], + ) + .unwrap(); + + rig.harness + .chain + .prepare_beacon_proposer_blocking() + .unwrap(); + + let payload_attributes = PayloadAttributes { + timestamp: rig + .harness + .chain + .slot_clock + .start_of(next_slot) + .unwrap() + .as_secs(), + prev_randao: *head + .beacon_state + .get_randao_mix(head.beacon_state.current_epoch()) + .unwrap(), + suggested_fee_recipient: fee_recipient, + }; + assert_eq!(rig.previous_payload_attributes(), payload_attributes); +} diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index f8c21b8623..353b174a02 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -1,6 +1,7 @@ use crate::config::{ClientGenesis, Config as ClientConfig}; use crate::notifier::spawn_notifier; use crate::Client; +use beacon_chain::proposer_prep_service::start_proposer_prep_service; use beacon_chain::schema_change::migrate_schema; use beacon_chain::{ builder::{BeaconChainBuilder, Witness}, @@ -149,11 +150,10 @@ where None }; - let execution_layer = if let Some(execution_endpoints) = config.execution_endpoints { + let execution_layer = if let Some(config) = config.execution_layer { let context = runtime_context.service_context("exec".into()); - let execution_layer = ExecutionLayer::from_urls( - execution_endpoints, - config.suggested_fee_recipient, + let execution_layer = ExecutionLayer::from_config( + config, context.executor.clone(), context.log().clone(), ) @@ -662,68 +662,58 @@ where ); if let Some(execution_layer) = beacon_chain.execution_layer.as_ref() { - let head = beacon_chain - .head_info() - .map_err(|e| format!("Unable to read beacon chain head: {:?}", e))?; + // Only send a head update *after* genesis. + if let Ok(current_slot) = beacon_chain.slot() { + let head = beacon_chain + .head_info() + .map_err(|e| format!("Unable to read beacon chain head: {:?}", e))?; - // Issue the head to the execution engine on startup. This ensures it can start - // syncing. - if let Some(block_hash) = head.execution_payload_block_hash { - let finalized_root = head.finalized_checkpoint.root; - let finalized_block = beacon_chain - .store - .get_block(&finalized_root) - .map_err(|e| format!("Failed to read finalized block from DB: {:?}", e))? - .ok_or(format!( - "Finalized block missing from store: {:?}", - finalized_root - ))?; - let finalized_execution_block_hash = finalized_block - .message() - .body() - .execution_payload() - .ok() - .map(|ep| ep.block_hash) - .unwrap_or_else(ExecutionBlockHash::zero); + // Issue the head to the execution engine on startup. This ensures it can start + // syncing. + if head + .execution_payload_block_hash + .map_or(false, |h| h != ExecutionBlockHash::zero()) + { + // Spawn a new task using the "async" fork choice update method, rather than + // using the "blocking" method. + // + // Using the blocking method may cause a panic if this code is run inside an + // async context. + let inner_chain = beacon_chain.clone(); + runtime_context.executor.spawn( + async move { + let result = inner_chain + .update_execution_engine_forkchoice_async(current_slot) + .await; - // Spawn a new task using the "async" fork choice update method, rather than - // using the "blocking" method. - // - // Using the blocking method may cause a panic if this code is run inside an - // async context. - let inner_chain = beacon_chain.clone(); - runtime_context.executor.spawn( - async move { - let result = inner_chain - .update_execution_engine_forkchoice_async( - finalized_execution_block_hash, - head.block_root, - block_hash, - ) - .await; + // No need to exit early if setting the head fails. It will be set again if/when the + // node comes online. + if let Err(e) = result { + warn!( + log, + "Failed to update head on execution engines"; + "error" => ?e + ); + } + }, + "el_fork_choice_update", + ); + } - // No need to exit early if setting the head fails. It will be set again if/when the - // node comes online. - if let Err(e) = result { - warn!( - log, - "Failed to update head on execution engines"; - "error" => ?e - ); - } - }, - "el_fork_choice_update", + // Spawn a routine that tracks the status of the execution engines. + execution_layer.spawn_watchdog_routine(beacon_chain.slot_clock.clone()); + + // Spawn a routine that removes expired proposer preparations. + execution_layer.spawn_clean_proposer_caches_routine::( + beacon_chain.slot_clock.clone(), ); + + // Spawns a routine that polls the `exchange_transition_configuration` endpoint. + execution_layer.spawn_transition_configuration_poll(beacon_chain.spec.clone()); } - - // Spawn a routine that tracks the status of the execution engines. - execution_layer.spawn_watchdog_routine(beacon_chain.slot_clock.clone()); - - // Spawn a routine that removes expired proposer preparations. - execution_layer.spawn_clean_proposer_preparation_routine::( - beacon_chain.slot_clock.clone(), - ); } + + start_proposer_prep_service(runtime_context.executor.clone(), beacon_chain.clone()); } Ok(Client { diff --git a/beacon_node/client/src/config.rs b/beacon_node/client/src/config.rs index fc29198405..bb9e196f7e 100644 --- a/beacon_node/client/src/config.rs +++ b/beacon_node/client/src/config.rs @@ -4,7 +4,7 @@ use sensitive_url::SensitiveUrl; use serde_derive::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; -use types::{Address, Graffiti, PublicKeyBytes}; +use types::{Graffiti, PublicKeyBytes}; /// Default directory name for the freezer database under the top-level data dir. const DEFAULT_FREEZER_DB_DIR: &str = "freezer_db"; @@ -72,8 +72,7 @@ pub struct Config { pub network: network::NetworkConfig, pub chain: beacon_chain::ChainConfig, pub eth1: eth1::Config, - pub execution_endpoints: Option>, - pub suggested_fee_recipient: Option
, + pub execution_layer: Option, pub http_api: http_api::Config, pub http_metrics: http_metrics::Config, pub monitoring_api: Option, @@ -94,8 +93,7 @@ impl Default for Config { dummy_eth1_backend: false, sync_eth1_chain: false, eth1: <_>::default(), - execution_endpoints: None, - suggested_fee_recipient: None, + execution_layer: None, graffiti: Graffiti::default(), http_api: <_>::default(), http_metrics: <_>::default(), diff --git a/beacon_node/execution_layer/Cargo.toml b/beacon_node/execution_layer/Cargo.toml index 7dbb326a67..b12d30ea2c 100644 --- a/beacon_node/execution_layer/Cargo.toml +++ b/beacon_node/execution_layer/Cargo.toml @@ -18,6 +18,7 @@ serde_json = "1.0.58" serde = { version = "1.0.116", features = ["derive"] } eth1 = { path = "../eth1" } warp = { git = "https://github.com/macladson/warp", rev ="dfa259e", features = ["tls"] } +jsonwebtoken = "8" environment = { path = "../../lighthouse/environment" } bytes = "1.1.0" task_executor = { path = "../../common/task_executor" } @@ -29,3 +30,8 @@ tree_hash = "0.4.1" tree_hash_derive = { path = "../../consensus/tree_hash_derive"} parking_lot = "0.11.0" slot_clock = { path = "../../common/slot_clock" } +tempfile = "3.1.0" +rand = "0.7.3" +zeroize = { version = "1.4.2", features = ["zeroize_derive"] } +lighthouse_metrics = { path = "../../common/lighthouse_metrics" } +lazy_static = "1.4.0" \ No newline at end of file diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index 605679dd7e..51c689ac62 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -1,12 +1,15 @@ use async_trait::async_trait; use eth1::http::RpcError; +use reqwest::StatusCode; use serde::{Deserialize, Serialize}; pub const LATEST_TAG: &str = "latest"; use crate::engines::ForkChoiceState; +pub use json_structures::TransitionConfigurationV1; pub use types::{Address, EthSpec, ExecutionBlockHash, ExecutionPayload, Hash256, Uint256}; +pub mod auth; pub mod http; pub mod json_structures; @@ -15,6 +18,7 @@ pub type PayloadId = [u8; 8]; #[derive(Debug)] pub enum Error { Reqwest(reqwest::Error), + Auth(auth::Error), BadResponse(String), RequestFailed(String), InvalidExecutePayloadResponse(&'static str), @@ -27,11 +31,19 @@ pub enum Error { ExecutionHeadBlockNotFound, ParentHashEqualsBlockHash(ExecutionBlockHash), PayloadIdUnavailable, + TransitionConfigurationMismatch, } impl From for Error { fn from(e: reqwest::Error) -> Self { - Error::Reqwest(e) + if matches!( + e.status(), + Some(StatusCode::UNAUTHORIZED) | Some(StatusCode::FORBIDDEN) + ) { + Error::Auth(auth::Error::InvalidToken) + } else { + Error::Reqwest(e) + } } } @@ -41,6 +53,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: auth::Error) -> Self { + Error::Auth(e) + } +} + /// A generic interface for an execution engine API. #[async_trait] pub trait EngineApi { @@ -71,6 +89,11 @@ pub trait EngineApi { forkchoice_state: ForkChoiceState, payload_attributes: Option, ) -> Result; + + async fn exchange_transition_configuration_v1( + &self, + transition_configuration: TransitionConfigurationV1, + ) -> Result; } #[derive(Clone, Copy, Debug, PartialEq)] @@ -107,10 +130,10 @@ pub struct ExecutionBlock { pub total_difficulty: Uint256, } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct PayloadAttributes { pub timestamp: u64, - pub random: Hash256, + pub prev_randao: Hash256, pub suggested_fee_recipient: Address, } diff --git a/beacon_node/execution_layer/src/engine_api/auth.rs b/beacon_node/execution_layer/src/engine_api/auth.rs new file mode 100644 index 0000000000..a4050a25c0 --- /dev/null +++ b/beacon_node/execution_layer/src/engine_api/auth.rs @@ -0,0 +1,148 @@ +use jsonwebtoken::{encode, get_current_timestamp, Algorithm, EncodingKey, Header}; +use rand::Rng; +use serde::{Deserialize, Serialize}; +use zeroize::Zeroize; + +/// Default algorithm used for JWT token signing. +const DEFAULT_ALGORITHM: Algorithm = Algorithm::HS256; + +/// JWT secret length in bytes. +pub const JWT_SECRET_LENGTH: usize = 32; + +#[derive(Debug)] +pub enum Error { + JWT(jsonwebtoken::errors::Error), + InvalidToken, +} + +impl From for Error { + fn from(e: jsonwebtoken::errors::Error) -> Self { + Error::JWT(e) + } +} + +/// Provides wrapper around `[u8; JWT_SECRET_LENGTH]` that implements `Zeroize`. +#[derive(Zeroize)] +#[zeroize(drop)] +pub struct JwtKey([u8; JWT_SECRET_LENGTH as usize]); + +impl JwtKey { + /// Wrap given slice in `Self`. Returns an error if slice.len() != `JWT_SECRET_LENGTH`. + pub fn from_slice(key: &[u8]) -> Result { + if key.len() != JWT_SECRET_LENGTH { + return Err(format!( + "Invalid key length. Expected {} got {}", + JWT_SECRET_LENGTH, + key.len() + )); + } + let mut res = [0; JWT_SECRET_LENGTH]; + res.copy_from_slice(key); + Ok(Self(res)) + } + + /// Generate a random secret. + pub fn random() -> Self { + Self(rand::thread_rng().gen::<[u8; JWT_SECRET_LENGTH]>()) + } + + /// Returns a reference to the underlying byte array. + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + /// Returns the hex encoded `String` for the secret. + pub fn hex_string(&self) -> String { + hex::encode(self.0) + } +} + +/// Contains the JWT secret and claims parameters. +pub struct Auth { + key: EncodingKey, + id: Option, + clv: Option, +} + +impl Auth { + pub fn new(secret: JwtKey, id: Option, clv: Option) -> Self { + Self { + key: EncodingKey::from_secret(secret.as_bytes()), + id, + clv, + } + } + + /// Generate a JWT token with `claims.iat` set to current time. + pub fn generate_token(&self) -> Result { + let claims = self.generate_claims_at_timestamp(); + self.generate_token_with_claims(&claims) + } + + /// Generate a JWT token with the given claims. + fn generate_token_with_claims(&self, claims: &Claims) -> Result { + let header = Header::new(DEFAULT_ALGORITHM); + Ok(encode(&header, claims, &self.key)?) + } + + /// Generate a `Claims` struct with `iat` set to current time + fn generate_claims_at_timestamp(&self) -> Claims { + Claims { + iat: get_current_timestamp(), + id: self.id.clone(), + clv: self.clv.clone(), + } + } + + /// Validate a JWT token given the secret key and return the originally signed `TokenData`. + pub fn validate_token( + token: &str, + secret: &JwtKey, + ) -> Result, Error> { + let mut validation = jsonwebtoken::Validation::new(DEFAULT_ALGORITHM); + validation.validate_exp = false; + validation.required_spec_claims.remove("exp"); + + jsonwebtoken::decode::( + token, + &jsonwebtoken::DecodingKey::from_secret(secret.as_bytes()), + &validation, + ) + .map_err(Into::into) + } +} + +/// Claims struct as defined in https://github.com/ethereum/execution-apis/blob/main/src/engine/authentication.md#jwt-claims +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct Claims { + /// issued-at claim. Represented as seconds passed since UNIX_EPOCH. + iat: u64, + /// Optional unique identifier for the CL node. + id: Option, + /// Optional client version for the CL node. + clv: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::JWT_SECRET; + + #[test] + fn test_roundtrip() { + let auth = Auth::new( + JwtKey::from_slice(&JWT_SECRET).unwrap(), + Some("42".into()), + Some("Lighthouse".into()), + ); + let claims = auth.generate_claims_at_timestamp(); + let token = auth.generate_token_with_claims(&claims).unwrap(); + + assert_eq!( + Auth::validate_token(&token, &JwtKey::from_slice(&JWT_SECRET).unwrap()) + .unwrap() + .claims, + claims + ); + } +} diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index 8d82b8d311..9880304d69 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -1,6 +1,7 @@ //! Contains an implementation of `EngineAPI` using the JSON-RPC API via HTTP. use super::*; +use crate::auth::Auth; use crate::json_structures::*; use async_trait::async_trait; use eth1::http::EIP155_ERROR_STR; @@ -36,9 +37,15 @@ pub const ENGINE_GET_PAYLOAD_TIMEOUT: Duration = Duration::from_secs(2); pub const ENGINE_FORKCHOICE_UPDATED_V1: &str = "engine_forkchoiceUpdatedV1"; pub const ENGINE_FORKCHOICE_UPDATED_TIMEOUT: Duration = Duration::from_millis(500); +pub const ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1: &str = + "engine_exchangeTransitionConfigurationV1"; +pub const ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1_TIMEOUT: Duration = + Duration::from_millis(500); + pub struct HttpJsonRpc { pub client: Client, pub url: SensitiveUrl, + auth: Option, } impl HttpJsonRpc { @@ -46,6 +53,15 @@ impl HttpJsonRpc { Ok(Self { client: Client::builder().build()?, url, + auth: None, + }) + } + + pub fn new_with_auth(url: SensitiveUrl, auth: Auth) -> Result { + Ok(Self { + client: Client::builder().build()?, + url, + auth: Some(auth), }) } @@ -62,17 +78,19 @@ impl HttpJsonRpc { id: STATIC_ID, }; - let body: JsonResponseBody = self + let mut request = self .client .post(self.url.full.clone()) .timeout(timeout) .header(CONTENT_TYPE, "application/json") - .json(&body) - .send() - .await? - .error_for_status()? - .json() - .await?; + .json(&body); + + // Generate and add a jwt token to the header if auth is defined. + if let Some(auth) = &self.auth { + request = request.bearer_auth(auth.generate_token()?); + }; + + let body: JsonResponseBody = request.send().await?.error_for_status()?.json().await?; match (body.result, body.error) { (result, None) => serde_json::from_value(result).map_err(Into::into), @@ -179,12 +197,30 @@ impl EngineApi for HttpJsonRpc { Ok(response.into()) } + + async fn exchange_transition_configuration_v1( + &self, + transition_configuration: TransitionConfigurationV1, + ) -> Result { + let params = json!([transition_configuration]); + + let response = self + .rpc_request( + ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1, + params, + ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1_TIMEOUT, + ) + .await?; + + Ok(response) + } } #[cfg(test)] mod test { + use super::auth::JwtKey; use super::*; - use crate::test_utils::MockServer; + use crate::test_utils::{MockServer, JWT_SECRET}; use std::future::Future; use std::str::FromStr; use std::sync::Arc; @@ -197,14 +233,25 @@ mod test { } impl Tester { - pub fn new() -> Self { + pub fn new(with_auth: bool) -> Self { let server = MockServer::unit_testing(); let rpc_url = SensitiveUrl::parse(&server.url()).unwrap(); - let rpc_client = Arc::new(HttpJsonRpc::new(rpc_url).unwrap()); - let echo_url = SensitiveUrl::parse(&format!("{}/echo", server.url())).unwrap(); - let echo_client = Arc::new(HttpJsonRpc::new(echo_url).unwrap()); + // Create rpc clients that include JWT auth headers if `with_auth` is true. + let (rpc_client, echo_client) = if with_auth { + let rpc_auth = Auth::new(JwtKey::from_slice(&JWT_SECRET).unwrap(), None, None); + let echo_auth = Auth::new(JwtKey::from_slice(&JWT_SECRET).unwrap(), None, None); + ( + Arc::new(HttpJsonRpc::new_with_auth(rpc_url, rpc_auth).unwrap()), + Arc::new(HttpJsonRpc::new_with_auth(echo_url, echo_auth).unwrap()), + ) + } else { + ( + Arc::new(HttpJsonRpc::new(rpc_url).unwrap()), + Arc::new(HttpJsonRpc::new(echo_url).unwrap()), + ) + }; Self { server, @@ -235,6 +282,22 @@ mod test { self } + pub async fn assert_auth_failure(self, request_func: R) -> Self + where + R: Fn(Arc) -> F, + F: Future>, + T: std::fmt::Debug, + { + let res = request_func(self.echo_client.clone()).await; + if !matches!(res, Err(Error::Auth(_))) { + panic!( + "No authentication provided, rpc call should have failed.\nResult: {:?}", + res + ) + } + self + } + pub async fn with_preloaded_responses( self, preloaded_responses: Vec, @@ -288,7 +351,7 @@ mod test { "stateRoot": HASH_01, "receiptsRoot": HASH_00, "logsBloom": LOGS_BLOOM_01, - "random": HASH_01, + "prevRandao": HASH_01, "blockNumber": "0x0", "gasLimit": "0x1", "gasUsed": "0x2", @@ -391,7 +454,7 @@ mod test { #[tokio::test] async fn get_block_by_number_request() { - Tester::new() + Tester::new(true) .assert_request_equals( |client| async move { let _ = client @@ -406,11 +469,19 @@ mod test { }), ) .await; + + Tester::new(false) + .assert_auth_failure(|client| async move { + client + .get_block_by_number(BlockByNumberQuery::Tag(LATEST_TAG)) + .await + }) + .await; } #[tokio::test] async fn get_block_by_hash_request() { - Tester::new() + Tester::new(true) .assert_request_equals( |client| async move { let _ = client @@ -425,11 +496,19 @@ mod test { }), ) .await; + + Tester::new(false) + .assert_auth_failure(|client| async move { + client + .get_block_by_hash(ExecutionBlockHash::repeat_byte(1)) + .await + }) + .await; } #[tokio::test] async fn forkchoice_updated_v1_with_payload_attributes_request() { - Tester::new() + Tester::new(true) .assert_request_equals( |client| async move { let _ = client @@ -441,7 +520,7 @@ mod test { }, Some(PayloadAttributes { timestamp: 5, - random: Hash256::zero(), + prev_randao: Hash256::zero(), suggested_fee_recipient: Address::repeat_byte(0), }), ) @@ -458,17 +537,36 @@ mod test { }, { "timestamp":"0x5", - "random": HASH_00, + "prevRandao": HASH_00, "suggestedFeeRecipient": ADDRESS_00 }] }), ) .await; + + Tester::new(false) + .assert_auth_failure(|client| async move { + client + .forkchoice_updated_v1( + ForkChoiceState { + head_block_hash: ExecutionBlockHash::repeat_byte(1), + safe_block_hash: ExecutionBlockHash::repeat_byte(1), + finalized_block_hash: ExecutionBlockHash::zero(), + }, + Some(PayloadAttributes { + timestamp: 5, + prev_randao: Hash256::zero(), + suggested_fee_recipient: Address::repeat_byte(0), + }), + ) + .await + }) + .await; } #[tokio::test] async fn get_payload_v1_request() { - Tester::new() + Tester::new(true) .assert_request_equals( |client| async move { let _ = client.get_payload_v1::([42; 8]).await; @@ -481,11 +579,17 @@ mod test { }), ) .await; + + Tester::new(false) + .assert_auth_failure(|client| async move { + client.get_payload_v1::([42; 8]).await + }) + .await; } #[tokio::test] async fn new_payload_v1_request() { - Tester::new() + Tester::new(true) .assert_request_equals( |client| async move { let _ = client @@ -495,7 +599,7 @@ mod test { state_root: Hash256::repeat_byte(1), receipts_root: Hash256::repeat_byte(0), logs_bloom: vec![1; 256].into(), - random: Hash256::repeat_byte(1), + prev_randao: Hash256::repeat_byte(1), block_number: 0, gas_limit: 1, gas_used: 2, @@ -517,7 +621,7 @@ mod test { "stateRoot": HASH_01, "receiptsRoot": HASH_00, "logsBloom": LOGS_BLOOM_01, - "random": HASH_01, + "prevRandao": HASH_01, "blockNumber": "0x0", "gasLimit": "0x1", "gasUsed": "0x2", @@ -530,11 +634,34 @@ mod test { }), ) .await; + + Tester::new(false) + .assert_auth_failure(|client| async move { + client + .new_payload_v1::(ExecutionPayload { + parent_hash: ExecutionBlockHash::repeat_byte(0), + fee_recipient: Address::repeat_byte(1), + state_root: Hash256::repeat_byte(1), + receipts_root: Hash256::repeat_byte(0), + logs_bloom: vec![1; 256].into(), + prev_randao: Hash256::repeat_byte(1), + block_number: 0, + gas_limit: 1, + gas_used: 2, + timestamp: 42, + extra_data: vec![].into(), + base_fee_per_gas: Uint256::from(1), + block_hash: ExecutionBlockHash::repeat_byte(1), + transactions: vec![].into(), + }) + .await + }) + .await; } #[tokio::test] async fn forkchoice_updated_v1_request() { - Tester::new() + Tester::new(true) .assert_request_equals( |client| async move { let _ = client @@ -560,6 +687,21 @@ mod test { }), ) .await; + + Tester::new(false) + .assert_auth_failure(|client| async move { + client + .forkchoice_updated_v1( + ForkChoiceState { + head_block_hash: ExecutionBlockHash::repeat_byte(0), + safe_block_hash: ExecutionBlockHash::repeat_byte(0), + finalized_block_hash: ExecutionBlockHash::repeat_byte(1), + }, + None, + ) + .await + }) + .await; } fn str_to_payload_id(s: &str) -> PayloadId { @@ -583,7 +725,7 @@ mod test { /// The `id` field has been modified on these vectors to match the one we use. #[tokio::test] async fn geth_test_vectors() { - Tester::new() + Tester::new(true) .assert_request_equals( // engine_forkchoiceUpdatedV1 (prepare payload) REQUEST validation |client| async move { @@ -596,7 +738,7 @@ mod test { }, Some(PayloadAttributes { timestamp: 5, - random: Hash256::zero(), + prev_randao: Hash256::zero(), suggested_fee_recipient: Address::from_str("0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(), }) ) @@ -613,7 +755,7 @@ mod test { }, { "timestamp":"0x5", - "random": HASH_00, + "prevRandao": HASH_00, "suggestedFeeRecipient":"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" }] }) @@ -643,7 +785,7 @@ mod test { }, Some(PayloadAttributes { timestamp: 5, - random: Hash256::zero(), + prev_randao: Hash256::zero(), suggested_fee_recipient: Address::from_str("0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(), }) ) @@ -687,7 +829,7 @@ mod test { "stateRoot":"0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf45", "receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", "logsBloom": LOGS_BLOOM_00, - "random": HASH_00, + "prevRandao": HASH_00, "blockNumber":"0x1", "gasLimit":"0x1c95111", "gasUsed":"0x0", @@ -710,7 +852,7 @@ mod test { state_root: Hash256::from_str("0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf45").unwrap(), receipts_root: Hash256::from_str("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").unwrap(), logs_bloom: vec![0; 256].into(), - random: Hash256::zero(), + prev_randao: Hash256::zero(), block_number: 1, gas_limit: u64::from_str_radix("1c95111",16).unwrap(), gas_used: 0, @@ -735,7 +877,7 @@ mod test { state_root: Hash256::from_str("0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf45").unwrap(), receipts_root: Hash256::from_str("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").unwrap(), logs_bloom: vec![0; 256].into(), - random: Hash256::zero(), + prev_randao: Hash256::zero(), block_number: 1, gas_limit: u64::from_str_radix("1c9c380",16).unwrap(), gas_used: 0, @@ -757,7 +899,7 @@ mod test { "stateRoot":"0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf45", "receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", "logsBloom": LOGS_BLOOM_00, - "random": HASH_00, + "prevRandao": HASH_00, "blockNumber":"0x1", "gasLimit":"0x1c9c380", "gasUsed":"0x0", diff --git a/beacon_node/execution_layer/src/engine_api/json_structures.rs b/beacon_node/execution_layer/src/engine_api/json_structures.rs index 79742a198c..600e359bb1 100644 --- a/beacon_node/execution_layer/src/engine_api/json_structures.rs +++ b/beacon_node/execution_layer/src/engine_api/json_structures.rs @@ -65,7 +65,7 @@ pub struct JsonExecutionPayloadV1 { pub receipts_root: Hash256, #[serde(with = "serde_logs_bloom")] pub logs_bloom: FixedVector, - pub random: Hash256, + pub prev_randao: Hash256, #[serde(with = "eth2_serde_utils::u64_hex_be")] pub block_number: u64, #[serde(with = "eth2_serde_utils::u64_hex_be")] @@ -92,7 +92,7 @@ impl From> for JsonExecutionPayloadV1 { state_root, receipts_root, logs_bloom, - random, + prev_randao, block_number, gas_limit, gas_used, @@ -109,7 +109,7 @@ impl From> for JsonExecutionPayloadV1 { state_root, receipts_root, logs_bloom, - random, + prev_randao, block_number, gas_limit, gas_used, @@ -131,7 +131,7 @@ impl From> for ExecutionPayload { state_root, receipts_root, logs_bloom, - random, + prev_randao, block_number, gas_limit, gas_used, @@ -148,7 +148,7 @@ impl From> for ExecutionPayload { state_root, receipts_root, logs_bloom, - random, + prev_randao, block_number, gas_limit, gas_used, @@ -166,7 +166,7 @@ impl From> for ExecutionPayload { pub struct JsonPayloadAttributesV1 { #[serde(with = "eth2_serde_utils::u64_hex_be")] pub timestamp: u64, - pub random: Hash256, + pub prev_randao: Hash256, pub suggested_fee_recipient: Address, } @@ -175,13 +175,13 @@ impl From for JsonPayloadAttributesV1 { // Use this verbose deconstruction pattern to ensure no field is left unused. let PayloadAttributes { timestamp, - random, + prev_randao, suggested_fee_recipient, } = p; Self { timestamp, - random, + prev_randao, suggested_fee_recipient, } } @@ -192,13 +192,13 @@ impl From for PayloadAttributes { // Use this verbose deconstruction pattern to ensure no field is left unused. let JsonPayloadAttributesV1 { timestamp, - random, + prev_randao, suggested_fee_recipient, } = j; Self { timestamp, - random, + prev_randao, suggested_fee_recipient, } } @@ -364,6 +364,15 @@ impl From for JsonForkchoiceUpdatedV1Response { } } +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransitionConfigurationV1 { + pub terminal_total_difficulty: Uint256, + pub terminal_block_hash: ExecutionBlockHash, + #[serde(with = "eth2_serde_utils::u64_hex_be")] + pub terminal_block_number: u64, +} + /// Serializes the `logs_bloom` field of an `ExecutionPayload`. pub mod serde_logs_bloom { use super::*; diff --git a/beacon_node/execution_layer/src/engines.rs b/beacon_node/execution_layer/src/engines.rs index d8e19baae1..a2c40ceb33 100644 --- a/beacon_node/execution_layer/src/engines.rs +++ b/beacon_node/execution_layer/src/engines.rs @@ -21,6 +21,7 @@ enum EngineState { Synced, Offline, Syncing, + AuthFailed, } #[derive(Copy, Clone, PartialEq, Debug)] @@ -50,7 +51,7 @@ impl Logging { struct PayloadIdCacheKey { pub head_block_hash: ExecutionBlockHash, pub timestamp: u64, - pub random: Hash256, + pub prev_randao: Hash256, pub suggested_fee_recipient: Address, } @@ -77,7 +78,7 @@ impl Engine { &self, head_block_hash: ExecutionBlockHash, timestamp: u64, - random: Hash256, + prev_randao: Hash256, suggested_fee_recipient: Address, ) -> Option { self.payload_id_cache @@ -86,7 +87,7 @@ impl Engine { .get(&PayloadIdCacheKey { head_block_hash, timestamp, - random, + prev_randao, suggested_fee_recipient, }) .cloned() @@ -135,6 +136,7 @@ pub struct Engines { pub enum EngineError { Offline { id: String }, Api { id: String, error: EngineApiError }, + Auth { id: String }, } impl Engines { @@ -150,6 +152,16 @@ impl Engines { let latest_forkchoice_state = self.get_latest_forkchoice_state().await; if let Some(forkchoice_state) = latest_forkchoice_state { + if forkchoice_state.head_block_hash == ExecutionBlockHash::zero() { + debug!( + self.log, + "No need to call forkchoiceUpdated"; + "msg" => "head does not have execution enabled", + "id" => &engine.id, + ); + return; + } + info!( self.log, "Issuing forkchoiceUpdated"; @@ -226,6 +238,18 @@ impl Engines { *state_lock = EngineState::Syncing } + Err(EngineApiError::Auth(err)) => { + if logging.is_enabled() { + warn!( + self.log, + "Failed jwt authorization"; + "error" => ?err, + "id" => &engine.id + ); + } + + *state_lock = EngineState::AuthFailed + } Err(e) => { if logging.is_enabled() { warn!( @@ -295,7 +319,13 @@ impl Engines { let mut errors = vec![]; for engine in &self.engines { - let engine_synced = *engine.state.read().await == EngineState::Synced; + let (engine_synced, engine_auth_failed) = { + let state = engine.state.read().await; + ( + *state == EngineState::Synced, + *state == EngineState::AuthFailed, + ) + }; if engine_synced { match func(engine).await { Ok(result) => return Ok(result), @@ -313,6 +343,10 @@ impl Engines { }) } } + } else if engine_auth_failed { + errors.push(EngineError::Auth { + id: engine.id.clone(), + }) } else { errors.push(EngineError::Offline { id: engine.id.clone(), @@ -393,7 +427,7 @@ impl PayloadIdCacheKey { Self { head_block_hash: state.head_block_hash, timestamp: attributes.timestamp, - random: attributes.random, + prev_randao: attributes.prev_randao, suggested_fee_recipient: attributes.suggested_fee_recipient, } } diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 326c46f870..ba4208d88a 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -4,32 +4,42 @@ //! This crate only provides useful functionality for "The Merge", it does not provide any of the //! deposit-contract functionality that the `beacon_node/eth1` crate already provides. +use auth::{Auth, JwtKey}; use engine_api::{Error as ApiError, *}; use engines::{Engine, EngineError, Engines, ForkChoiceState, Logging}; use lru::LruCache; use payload_status::process_multiple_payload_statuses; use sensitive_url::SensitiveUrl; -use slog::{crit, debug, error, info, Logger}; +use serde::{Deserialize, Serialize}; +use slog::{crit, debug, error, info, trace, Logger}; use slot_clock::SlotClock; use std::collections::HashMap; use std::future::Future; +use std::io::Write; +use std::path::PathBuf; use std::sync::Arc; -use std::time::Duration; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use task_executor::TaskExecutor; use tokio::{ - sync::{Mutex, MutexGuard}, + sync::{Mutex, MutexGuard, RwLock}, time::{sleep, sleep_until, Instant}, }; -use types::{ChainSpec, Epoch, ExecutionBlockHash, ProposerPreparationData}; +use types::{ChainSpec, Epoch, ExecutionBlockHash, ProposerPreparationData, Slot}; -pub use engine_api::{http::HttpJsonRpc, PayloadAttributes, PayloadStatusV1Status}; +pub use engine_api::{ + http::HttpJsonRpc, json_structures, PayloadAttributes, PayloadStatusV1Status, +}; pub use payload_status::PayloadStatus; mod engine_api; mod engines; +mod metrics; mod payload_status; pub mod test_utils; +/// Name for the default file used for the jwt secret. +pub const DEFAULT_JWT_FILE: &str = "jwt.hex"; + /// Each time the `ExecutionLayer` retrieves a block from an execution node, it stores that block /// in an LRU cache to avoid redundant lookups. This is the size of that cache. const EXECUTION_BLOCKS_LRU_CACHE_SIZE: usize = 128; @@ -44,6 +54,8 @@ const EXECUTION_BLOCKS_LRU_CACHE_SIZE: usize = 128; const DEFAULT_SUGGESTED_FEE_RECIPIENT: [u8; 20] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]; +const CONFIG_POLL_INTERVAL: Duration = Duration::from_secs(60); + #[derive(Debug)] pub enum Error { NoEngines, @@ -54,6 +66,7 @@ pub enum Error { FeeRecipientUnspecified, ConsensusFailure, MissingLatestValidHash, + InvalidJWTSecret(String), } impl From for Error { @@ -62,21 +75,60 @@ impl From for Error { } } -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub struct ProposerPreparationDataEntry { update_epoch: Epoch, preparation_data: ProposerPreparationData, } +#[derive(Hash, PartialEq, Eq)] +pub struct ProposerKey { + slot: Slot, + head_block_root: Hash256, +} + +#[derive(PartialEq, Clone)] +pub struct Proposer { + validator_index: u64, + payload_attributes: PayloadAttributes, +} + struct Inner { engines: Engines, + execution_engine_forkchoice_lock: Mutex<()>, suggested_fee_recipient: Option
, proposer_preparation_data: Mutex>, execution_blocks: Mutex>, + proposers: RwLock>, executor: TaskExecutor, log: Logger, } +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct Config { + /// Endpoint urls for EL nodes that are running the engine api. + pub execution_endpoints: Vec, + /// JWT secrets for the above endpoints running the engine api. + pub secret_files: Vec, + /// The default fee recipient to use on the beacon node if none if provided from + /// the validator client during block preparation. + pub suggested_fee_recipient: Option
, + /// An optional id for the beacon node that will be passed to the EL in the JWT token claim. + pub jwt_id: Option, + /// An optional client version for the beacon node that will be passed to the EL in the JWT token claim. + pub jwt_version: Option, + /// Default directory for the jwt secret if not provided through cli. + pub default_datadir: PathBuf, +} + +fn strip_prefix(s: &str) -> &str { + if let Some(stripped) = s.strip_prefix("0x") { + stripped + } else { + s + } +} + /// Provides access to one or more execution engines and provides a neat interface for consumption /// by the `BeaconChain`. /// @@ -92,22 +144,73 @@ pub struct ExecutionLayer { } impl ExecutionLayer { - /// Instantiate `Self` with `urls.len()` engines, all using the JSON-RPC via HTTP. - pub fn from_urls( - urls: Vec, - suggested_fee_recipient: Option
, - executor: TaskExecutor, - log: Logger, - ) -> Result { + /// Instantiate `Self` with Execution engines specified using `Config`, all using the JSON-RPC via HTTP. + pub fn from_config(config: Config, executor: TaskExecutor, log: Logger) -> Result { + let Config { + execution_endpoints: urls, + mut secret_files, + suggested_fee_recipient, + jwt_id, + jwt_version, + default_datadir, + } = config; + if urls.is_empty() { return Err(Error::NoEngines); } - let engines = urls + // Extend the jwt secret files with the default jwt secret path if not provided via cli. + // This ensures that we have a jwt secret for every EL. + secret_files.extend(vec![ + default_datadir.join(DEFAULT_JWT_FILE); + urls.len().saturating_sub(secret_files.len()) + ]); + + let secrets: Vec<(JwtKey, PathBuf)> = secret_files + .iter() + .map(|p| { + // Read secret from file if it already exists + if p.exists() { + std::fs::read_to_string(p) + .map_err(|e| { + format!("Failed to read JWT secret file {:?}, error: {:?}", p, e) + }) + .and_then(|ref s| { + let secret = JwtKey::from_slice( + &hex::decode(strip_prefix(s)) + .map_err(|e| format!("Invalid hex string: {:?}", e))?, + )?; + Ok((secret, p.to_path_buf())) + }) + } else { + // Create a new file and write a randomly generated secret to it if file does not exist + std::fs::File::options() + .write(true) + .create_new(true) + .open(p) + .map_err(|e| { + format!("Failed to open JWT secret file {:?}, error: {:?}", p, e) + }) + .and_then(|mut f| { + let secret = auth::JwtKey::random(); + f.write_all(secret.hex_string().as_bytes()).map_err(|e| { + format!("Failed to write to JWT secret file: {:?}", e) + })?; + Ok((secret, p.to_path_buf())) + }) + } + }) + .collect::>() + .map_err(Error::InvalidJWTSecret)?; + + let engines: Vec> = urls .into_iter() - .map(|url| { + .zip(secrets.into_iter()) + .map(|(url, (secret, path))| { let id = url.to_string(); - let api = HttpJsonRpc::new(url)?; + let auth = Auth::new(secret, jwt_id.clone(), jwt_version.clone()); + debug!(log, "Loaded execution endpoint"; "endpoint" => %id, "jwt_path" => ?path); + let api = HttpJsonRpc::new_with_auth(url, auth)?; Ok(Engine::new(id, api)) }) .collect::>()?; @@ -118,8 +221,10 @@ impl ExecutionLayer { latest_forkchoice_state: <_>::default(), log: log.clone(), }, + execution_engine_forkchoice_lock: <_>::default(), suggested_fee_recipient, proposer_preparation_data: Mutex::new(HashMap::new()), + proposers: RwLock::new(HashMap::new()), execution_blocks: Mutex::new(LruCache::new(EXECUTION_BLOCKS_LRU_CACHE_SIZE)), executor, log, @@ -154,10 +259,18 @@ impl ExecutionLayer { self.inner.proposer_preparation_data.lock().await } + fn proposers(&self) -> &RwLock> { + &self.inner.proposers + } + fn log(&self) -> &Logger { &self.inner.log } + pub async fn execution_engine_forkchoice_lock(&self) -> MutexGuard<'_, ()> { + self.inner.execution_engine_forkchoice_lock.lock().await + } + /// Convenience function to allow calling async functions in a non-async context. pub fn block_on<'a, T, U, V>(&'a self, generate_future: T) -> Result where @@ -261,8 +374,8 @@ impl ExecutionLayer { self.engines().upcheck_not_synced(Logging::Disabled).await; } - /// Spawns a routine which cleans the cached proposer preparations periodically. - pub fn spawn_clean_proposer_preparation_routine( + /// Spawns a routine which cleans the cached proposer data periodically. + pub fn spawn_clean_proposer_caches_routine( &self, slot_clock: S, ) { @@ -280,7 +393,7 @@ impl ExecutionLayer { .map(|slot| slot.epoch(T::slots_per_epoch())) { Some(current_epoch) => el - .clean_proposer_preparation(current_epoch) + .clean_proposer_caches::(current_epoch) .await .map_err(|e| { error!( @@ -303,6 +416,24 @@ impl ExecutionLayer { self.spawn(preparation_cleaner, "exec_preparation_cleanup"); } + /// Spawns a routine that polls the `exchange_transition_configuration` endpoint. + pub fn spawn_transition_configuration_poll(&self, spec: ChainSpec) { + let routine = |el: ExecutionLayer| async move { + loop { + if let Err(e) = el.exchange_transition_configuration(&spec).await { + error!( + el.log(), + "Failed to check transition config"; + "error" => ?e + ); + } + sleep(CONFIG_POLL_INTERVAL).await; + } + }; + + self.spawn(routine, "exec_config_poll"); + } + /// Returns `true` if there is at least one synced and reachable engine. pub async fn is_synced(&self) -> bool { self.engines().any_synced().await @@ -317,7 +448,7 @@ impl ExecutionLayer { self.block_on_generic(|_| async move { self.update_proposer_preparation(update_epoch, preparation_data) .await - })? + }) } /// Updates the proposer preparation data provided by validators @@ -325,23 +456,25 @@ impl ExecutionLayer { &self, update_epoch: Epoch, preparation_data: &[ProposerPreparationData], - ) -> Result<(), Error> { + ) { let mut proposer_preparation_data = self.proposer_preparation_data().await; for preparation_entry in preparation_data { - proposer_preparation_data.insert( - preparation_entry.validator_index, - ProposerPreparationDataEntry { - update_epoch, - preparation_data: preparation_entry.clone(), - }, - ); - } + let new = ProposerPreparationDataEntry { + update_epoch, + preparation_data: preparation_entry.clone(), + }; - Ok(()) + let existing = + proposer_preparation_data.insert(preparation_entry.validator_index, new.clone()); + + if existing != Some(new) { + metrics::inc_counter(&metrics::EXECUTION_LAYER_PROPOSER_DATA_UPDATED); + } + } } - /// Removes expired entries from cached proposer preparations - async fn clean_proposer_preparation(&self, current_epoch: Epoch) -> Result<(), Error> { + /// Removes expired entries from proposer_preparation_data and proposers caches + async fn clean_proposer_caches(&self, current_epoch: Epoch) -> Result<(), Error> { let mut proposer_preparation_data = self.proposer_preparation_data().await; // Keep all entries that have been updated in the last 2 epochs @@ -349,12 +482,33 @@ impl ExecutionLayer { proposer_preparation_data.retain(|_validator_index, preparation_entry| { preparation_entry.update_epoch >= retain_epoch }); + drop(proposer_preparation_data); + + let retain_slot = retain_epoch.start_slot(T::slots_per_epoch()); + self.proposers() + .write() + .await + .retain(|proposer_key, _proposer| proposer_key.slot >= retain_slot); Ok(()) } + /// Returns `true` if there have been any validators registered via + /// `Self::update_proposer_preparation`. + pub async fn has_any_proposer_preparation_data(&self) -> bool { + !self.proposer_preparation_data().await.is_empty() + } + + /// Returns `true` if the `proposer_index` has registered as a local validator via + /// `Self::update_proposer_preparation`. + pub async fn has_proposer_preparation_data(&self, proposer_index: u64) -> bool { + self.proposer_preparation_data() + .await + .contains_key(&proposer_index) + } + /// Returns the fee-recipient address that should be used to build a block - async fn get_suggested_fee_recipient(&self, proposer_index: u64) -> Address { + pub async fn get_suggested_fee_recipient(&self, proposer_index: u64) -> Address { if let Some(preparation_data_entry) = self.proposer_preparation_data().await.get(&proposer_index) { @@ -392,27 +546,36 @@ impl ExecutionLayer { &self, parent_hash: ExecutionBlockHash, timestamp: u64, - random: Hash256, + prev_randao: Hash256, finalized_block_hash: ExecutionBlockHash, proposer_index: u64, ) -> Result, Error> { + let _timer = metrics::start_timer_vec( + &metrics::EXECUTION_LAYER_REQUEST_TIMES, + &[metrics::GET_PAYLOAD], + ); + let suggested_fee_recipient = self.get_suggested_fee_recipient(proposer_index).await; debug!( self.log(), "Issuing engine_getPayload"; "suggested_fee_recipient" => ?suggested_fee_recipient, - "random" => ?random, + "prev_randao" => ?prev_randao, "timestamp" => timestamp, "parent_hash" => ?parent_hash, ); self.engines() .first_success(|engine| async move { let payload_id = if let Some(id) = engine - .get_payload_id(parent_hash, timestamp, random, suggested_fee_recipient) + .get_payload_id(parent_hash, timestamp, prev_randao, suggested_fee_recipient) .await { // The payload id has been cached for this engine. + metrics::inc_counter_vec( + &metrics::EXECUTION_LAYER_PRE_PREPARED_PAYLOAD_ID, + &[metrics::HIT], + ); id } else { // The payload id has *not* been cached for this engine. Trigger an artificial @@ -421,6 +584,10 @@ impl ExecutionLayer { // TODO(merge): a better algorithm might try to favour a node that already had a // cached payload id, since a payload that has had more time to produce is // likely to be more profitable. + metrics::inc_counter_vec( + &metrics::EXECUTION_LAYER_PRE_PREPARED_PAYLOAD_ID, + &[metrics::MISS], + ); let fork_choice_state = ForkChoiceState { head_block_hash: parent_hash, safe_block_hash: parent_hash, @@ -428,7 +595,7 @@ impl ExecutionLayer { }; let payload_attributes = PayloadAttributes { timestamp, - random, + prev_randao, suggested_fee_recipient, }; @@ -475,7 +642,12 @@ impl ExecutionLayer { &self, execution_payload: &ExecutionPayload, ) -> Result { - debug!( + let _timer = metrics::start_timer_vec( + &metrics::EXECUTION_LAYER_REQUEST_TIMES, + &[metrics::NEW_PAYLOAD], + ); + + trace!( self.log(), "Issuing engine_newPayload"; "parent_hash" => ?execution_payload.parent_hash, @@ -495,6 +667,64 @@ impl ExecutionLayer { ) } + /// Register that the given `validator_index` is going to produce a block at `slot`. + /// + /// The block will be built atop `head_block_root` and the EL will need to prepare an + /// `ExecutionPayload` as defined by the given `payload_attributes`. + pub async fn insert_proposer( + &self, + slot: Slot, + head_block_root: Hash256, + validator_index: u64, + payload_attributes: PayloadAttributes, + ) -> bool { + let proposers_key = ProposerKey { + slot, + head_block_root, + }; + + let existing = self.proposers().write().await.insert( + proposers_key, + Proposer { + validator_index, + payload_attributes, + }, + ); + + if existing.is_none() { + metrics::inc_counter(&metrics::EXECUTION_LAYER_PROPOSER_INSERTED); + } + + existing.is_some() + } + + /// If there has been a proposer registered via `Self::insert_proposer` with a matching `slot` + /// `head_block_root`, then return the appropriate `PayloadAttributes` for inclusion in + /// `forkchoiceUpdated` calls. + pub async fn payload_attributes( + &self, + current_slot: Slot, + head_block_root: Hash256, + ) -> Option { + let proposers_key = ProposerKey { + slot: current_slot, + head_block_root, + }; + + let proposer = self.proposers().read().await.get(&proposers_key).cloned()?; + + debug!( + self.log(), + "Beacon proposer found"; + "payload_attributes" => ?proposer.payload_attributes, + "head_block_root" => ?head_block_root, + "slot" => current_slot, + "validator_index" => proposer.validator_index, + ); + + Some(proposer.payload_attributes) + } + /// Maps to the `engine_consensusValidated` JSON-RPC call. /// /// ## Fallback Behaviour @@ -512,15 +742,44 @@ impl ExecutionLayer { &self, head_block_hash: ExecutionBlockHash, finalized_block_hash: ExecutionBlockHash, - payload_attributes: Option, + current_slot: Slot, + head_block_root: Hash256, ) -> Result { - debug!( + let _timer = metrics::start_timer_vec( + &metrics::EXECUTION_LAYER_REQUEST_TIMES, + &[metrics::FORKCHOICE_UPDATED], + ); + + trace!( self.log(), "Issuing engine_forkchoiceUpdated"; "finalized_block_hash" => ?finalized_block_hash, "head_block_hash" => ?head_block_hash, ); + let next_slot = current_slot + 1; + let payload_attributes = self.payload_attributes(next_slot, head_block_root).await; + + // Compute the "lookahead", the time between when the payload will be produced and now. + if let Some(payload_attributes) = payload_attributes { + if let Ok(now) = SystemTime::now().duration_since(UNIX_EPOCH) { + let timestamp = Duration::from_secs(payload_attributes.timestamp); + if let Some(lookahead) = timestamp.checked_sub(now) { + metrics::observe_duration( + &metrics::EXECUTION_LAYER_PAYLOAD_ATTRIBUTES_LOOKAHEAD, + lookahead, + ); + } else { + debug!( + self.log(), + "Late payload attributes"; + "timestamp" => ?timestamp, + "now" => ?now, + ) + } + } + } + // see https://hackmd.io/@n0ble/kintsugi-spec#Engine-API // for now, we must set safe_block_hash = head_block_hash let forkchoice_state = ForkChoiceState { @@ -551,6 +810,65 @@ impl ExecutionLayer { ) } + pub async fn exchange_transition_configuration(&self, spec: &ChainSpec) -> Result<(), Error> { + let local = TransitionConfigurationV1 { + terminal_total_difficulty: spec.terminal_total_difficulty, + terminal_block_hash: spec.terminal_block_hash, + terminal_block_number: 0, + }; + + let broadcast_results = self + .engines() + .broadcast(|engine| engine.api.exchange_transition_configuration_v1(local)) + .await; + + let mut errors = vec![]; + for (i, result) in broadcast_results.into_iter().enumerate() { + match result { + Ok(remote) => { + if local.terminal_total_difficulty != remote.terminal_total_difficulty + || local.terminal_block_hash != remote.terminal_block_hash + { + error!( + self.log(), + "Execution client config mismatch"; + "msg" => "ensure lighthouse and the execution client are up-to-date and \ + configured consistently", + "execution_endpoint" => i, + "remote" => ?remote, + "local" => ?local, + ); + errors.push(EngineError::Api { + id: i.to_string(), + error: ApiError::TransitionConfigurationMismatch, + }); + } else { + debug!( + self.log(), + "Execution client config is OK"; + "execution_endpoint" => i + ); + } + } + Err(e) => { + error!( + self.log(), + "Unable to get transition config"; + "error" => ?e, + "execution_endpoint" => i, + ); + errors.push(e); + } + } + } + + if errors.is_empty() { + Ok(()) + } else { + Err(Error::EngineErrors(errors)) + } + } + /// Used during block production to determine if the merge has been triggered. /// /// ## Specification @@ -562,6 +880,11 @@ impl ExecutionLayer { &self, spec: &ChainSpec, ) -> Result, Error> { + let _timer = metrics::start_timer_vec( + &metrics::EXECUTION_LAYER_REQUEST_TIMES, + &[metrics::GET_TERMINAL_POW_BLOCK_HASH], + ); + let hash_opt = self .engines() .first_success(|engine| async move { @@ -673,6 +996,11 @@ impl ExecutionLayer { block_hash: ExecutionBlockHash, spec: &ChainSpec, ) -> Result, Error> { + let _timer = metrics::start_timer_vec( + &metrics::EXECUTION_LAYER_REQUEST_TIMES, + &[metrics::IS_VALID_TERMINAL_POW_BLOCK_HASH], + ); + let broadcast_results = self .engines() .broadcast(|engine| async move { @@ -793,6 +1121,7 @@ mod test { MockExecutionLayer::default_params() .move_to_block_prior_to_terminal_block() .with_terminal_block(|spec, el, _| async move { + el.engines().upcheck_not_synced(Logging::Disabled).await; assert_eq!(el.get_terminal_pow_block_hash(&spec).await.unwrap(), None) }) .await @@ -811,6 +1140,7 @@ mod test { MockExecutionLayer::default_params() .move_to_terminal_block() .with_terminal_block(|spec, el, terminal_block| async move { + el.engines().upcheck_not_synced(Logging::Disabled).await; assert_eq!( el.is_valid_terminal_pow_block_hash(terminal_block.unwrap().block_hash, &spec) .await @@ -826,6 +1156,7 @@ mod test { MockExecutionLayer::default_params() .move_to_terminal_block() .with_terminal_block(|spec, el, terminal_block| async move { + el.engines().upcheck_not_synced(Logging::Disabled).await; let invalid_terminal_block = terminal_block.unwrap().parent_hash; assert_eq!( @@ -843,6 +1174,7 @@ mod test { MockExecutionLayer::default_params() .move_to_terminal_block() .with_terminal_block(|spec, el, _| async move { + el.engines().upcheck_not_synced(Logging::Disabled).await; let missing_terminal_block = ExecutionBlockHash::repeat_byte(42); assert_eq!( diff --git a/beacon_node/execution_layer/src/metrics.rs b/beacon_node/execution_layer/src/metrics.rs new file mode 100644 index 0000000000..4a761c8e46 --- /dev/null +++ b/beacon_node/execution_layer/src/metrics.rs @@ -0,0 +1,34 @@ +pub use lighthouse_metrics::*; + +pub const HIT: &str = "hit"; +pub const MISS: &str = "miss"; +pub const GET_PAYLOAD: &str = "get_payload"; +pub const NEW_PAYLOAD: &str = "new_payload"; +pub const FORKCHOICE_UPDATED: &str = "forkchoice_updated"; +pub const GET_TERMINAL_POW_BLOCK_HASH: &str = "get_terminal_pow_block_hash"; +pub const IS_VALID_TERMINAL_POW_BLOCK_HASH: &str = "is_valid_terminal_pow_block_hash"; + +lazy_static::lazy_static! { + pub static ref EXECUTION_LAYER_PROPOSER_INSERTED: Result = try_create_int_counter( + "execution_layer_proposer_inserted", + "Count of times a new proposer is known", + ); + pub static ref EXECUTION_LAYER_PROPOSER_DATA_UPDATED: Result = try_create_int_counter( + "execution_layer_proposer_data_updated", + "Count of times new proposer data is supplied", + ); + pub static ref EXECUTION_LAYER_REQUEST_TIMES: Result = try_create_histogram_vec( + "execution_layer_request_times", + "Duration of calls to ELs", + &["method"] + ); + pub static ref EXECUTION_LAYER_PAYLOAD_ATTRIBUTES_LOOKAHEAD: Result = try_create_histogram( + "execution_layer_payload_attributes_lookahead", + "Duration between an fcU call with PayloadAttributes and when the block should be produced", + ); + pub static ref EXECUTION_LAYER_PRE_PREPARED_PAYLOAD_ID: Result = try_create_int_counter_vec( + "execution_layer_pre_prepared_payload_id", + "Indicates hits or misses for already having prepared a payload id before payload production", + &["event"] + ); +} diff --git a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs index 52accad3a1..b61092cf0e 100644 --- a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs +++ b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs @@ -326,7 +326,7 @@ impl ExecutionBlockGenerator { receipts_root: Hash256::repeat_byte(42), state_root: Hash256::repeat_byte(43), logs_bloom: vec![0; 256].into(), - random: attributes.random, + prev_randao: attributes.prev_randao, block_number: parent.block_number() + 1, gas_limit: GAS_LIMIT, gas_used: GAS_USED, diff --git a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs index 1ee29ce7a9..61038f40af 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -10,6 +10,8 @@ pub async fn handle_rpc( body: JsonValue, ctx: Arc>, ) -> Result { + *ctx.previous_request.lock() = Some(body.clone()); + let method = body .get("method") .and_then(JsonValue::as_str) diff --git a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs index a15ab25254..cf8c8516f6 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs @@ -1,11 +1,12 @@ use crate::{ - test_utils::{MockServer, DEFAULT_TERMINAL_BLOCK, DEFAULT_TERMINAL_DIFFICULTY}, - *, + test_utils::{MockServer, DEFAULT_TERMINAL_BLOCK, DEFAULT_TERMINAL_DIFFICULTY, JWT_SECRET}, + Config, *, }; use environment::null_logger; use sensitive_url::SensitiveUrl; use std::sync::Arc; use task_executor::TaskExecutor; +use tempfile::NamedTempFile; use types::{Address, ChainSpec, Epoch, EthSpec, Hash256, Uint256}; pub struct ExecutionLayerRuntime { @@ -85,10 +86,19 @@ impl MockExecutionLayer { ); let url = SensitiveUrl::parse(&server.url()).unwrap(); + let file = NamedTempFile::new().unwrap(); - let el = ExecutionLayer::from_urls( - vec![url], - Some(Address::repeat_byte(42)), + let path = file.path().into(); + std::fs::write(&path, hex::encode(JWT_SECRET)).unwrap(); + + let config = Config { + execution_endpoints: vec![url], + secret_files: vec![path], + suggested_fee_recipient: Some(Address::repeat_byte(42)), + ..Default::default() + }; + let el = ExecutionLayer::from_config( + config, el_runtime.task_executor.clone(), el_runtime.log.clone(), ) @@ -111,18 +121,32 @@ impl MockExecutionLayer { let parent_hash = latest_execution_block.block_hash(); let block_number = latest_execution_block.block_number() + 1; let timestamp = block_number; - let random = Hash256::from_low_u64_be(block_number); + let prev_randao = Hash256::from_low_u64_be(block_number); let finalized_block_hash = parent_hash; + // Insert a proposer to ensure the fork choice updated command works. + let slot = Slot::new(0); + let head_block_root = Hash256::repeat_byte(42); + let validator_index = 0; + self.el + .insert_proposer( + slot, + head_block_root, + validator_index, + PayloadAttributes { + timestamp, + prev_randao, + suggested_fee_recipient: Address::repeat_byte(42), + }, + ) + .await; + self.el .notify_forkchoice_updated( parent_hash, ExecutionBlockHash::zero(), - Some(PayloadAttributes { - timestamp, - random, - suggested_fee_recipient: Address::repeat_byte(42), - }), + slot, + head_block_root, ) .await .unwrap(); @@ -133,7 +157,7 @@ impl MockExecutionLayer { .get_payload::( parent_hash, timestamp, - random, + prev_randao, finalized_block_hash, validator_index, ) @@ -143,13 +167,21 @@ impl MockExecutionLayer { assert_eq!(payload.parent_hash, parent_hash); assert_eq!(payload.block_number, block_number); assert_eq!(payload.timestamp, timestamp); - assert_eq!(payload.random, random); + assert_eq!(payload.prev_randao, prev_randao); let status = self.el.notify_new_payload(&payload).await.unwrap(); assert_eq!(status, PayloadStatus::Valid); + // Use junk values for slot/head-root to ensure there is no payload supplied. + let slot = Slot::new(0); + let head_block_root = Hash256::repeat_byte(13); self.el - .notify_forkchoice_updated(block_hash, ExecutionBlockHash::zero(), None) + .notify_forkchoice_updated( + block_hash, + ExecutionBlockHash::zero(), + slot, + head_block_root, + ) .await .unwrap(); diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index 9d6eb5cf04..99adfa6558 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -1,6 +1,9 @@ //! Provides a mock execution engine HTTP JSON-RPC API for use in testing. -use crate::engine_api::{http::JSONRPC_VERSION, PayloadStatusV1, PayloadStatusV1Status}; +use crate::engine_api::auth::JwtKey; +use crate::engine_api::{ + auth::Auth, http::JSONRPC_VERSION, PayloadStatusV1, PayloadStatusV1Status, +}; use bytes::Bytes; use environment::null_logger; use execution_block_generator::{Block, PoWBlock}; @@ -9,19 +12,21 @@ use parking_lot::{Mutex, RwLock, RwLockWriteGuard}; use serde::{Deserialize, Serialize}; use serde_json::json; use slog::{info, Logger}; +use std::convert::Infallible; use std::future::Future; use std::marker::PhantomData; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; use std::sync::Arc; use tokio::{runtime, sync::oneshot}; use types::{EthSpec, ExecutionBlockHash, Uint256}; -use warp::Filter; +use warp::{http::StatusCode, Filter, Rejection}; pub use execution_block_generator::{generate_pow_block, ExecutionBlockGenerator}; pub use mock_execution_layer::{ExecutionLayerRuntime, MockExecutionLayer}; pub const DEFAULT_TERMINAL_DIFFICULTY: u64 = 6400; pub const DEFAULT_TERMINAL_BLOCK: u64 = 64; +pub const JWT_SECRET: [u8; 32] = [42; 32]; mod execution_block_generator; mod handle_rpc; @@ -60,6 +65,7 @@ impl MockServer { log: null_logger().unwrap(), last_echo_request: last_echo_request.clone(), execution_block_generator: RwLock::new(execution_block_generator), + previous_request: <_>::default(), preloaded_responses, static_new_payload_response: <_>::default(), _phantom: PhantomData, @@ -115,6 +121,10 @@ impl MockServer { self.ctx.preloaded_responses.lock().push(response) } + pub fn take_previous_request(&self) -> Option { + self.ctx.previous_request.lock().take() + } + pub fn all_payloads_valid(&self) { let response = StaticNewPayloadResponse { status: PayloadStatusV1 { @@ -222,6 +232,10 @@ pub struct StaticNewPayloadResponse { status: PayloadStatusV1, should_import: bool, } +#[derive(Debug)] +struct AuthError(String); + +impl warp::reject::Reject for AuthError {} /// A wrapper around all the items required to spawn the HTTP server. /// @@ -232,6 +246,7 @@ pub struct Context { pub last_echo_request: Arc>>, pub execution_block_generator: RwLock>, pub preloaded_responses: Arc>>, + pub previous_request: Arc>>, pub static_new_payload_response: Arc>>, pub _phantom: PhantomData, } @@ -252,6 +267,66 @@ impl Default for Config { } } +/// An API error serializable to JSON. +#[derive(Serialize)] +struct ErrorMessage { + code: u16, + message: String, +} + +/// Returns a `warp` header which filters out request that has a missing or incorrectly +/// signed JWT token. +fn auth_header_filter() -> warp::filters::BoxedFilter<()> { + warp::any() + .and(warp::filters::header::optional("Authorization")) + .and_then(move |authorization: Option| async move { + match authorization { + None => Err(warp::reject::custom(AuthError( + "auth absent from request".to_string(), + ))), + Some(auth) => { + if let Some(token) = auth.strip_prefix("Bearer ") { + let secret = JwtKey::from_slice(&JWT_SECRET).unwrap(); + match Auth::validate_token(token, &secret) { + Ok(_) => Ok(()), + Err(e) => Err(warp::reject::custom(AuthError(format!( + "Auth failure: {:?}", + e + )))), + } + } else { + Err(warp::reject::custom(AuthError( + "Bearer token not present in auth header".to_string(), + ))) + } + } + } + }) + .untuple_one() + .boxed() +} +/// This function receives a `Rejection` and tries to return a custom +/// value on invalid auth, otherwise simply passes the rejection along. +async fn handle_rejection(err: Rejection) -> Result { + let code; + let message; + + if let Some(e) = err.find::() { + message = format!("Authorization error: {:?}", e); + code = StatusCode::UNAUTHORIZED; + } else { + message = "BAD_REQUEST".to_string(); + code = StatusCode::BAD_REQUEST; + } + + let json = warp::reply::json(&ErrorMessage { + code: code.as_u16(), + message, + }); + + Ok(warp::reply::with_status(json, code)) +} + /// Creates a server that will serve requests using information from `ctx`. /// /// The server will shut down gracefully when the `shutdown` future resolves. @@ -288,7 +363,6 @@ pub fn serve( .get("id") .and_then(serde_json::Value::as_u64) .ok_or_else(|| warp::reject::custom(MissingIdField))?; - let preloaded_response = { let mut preloaded_responses = ctx.preloaded_responses.lock(); if !preloaded_responses.is_empty() { @@ -339,7 +413,9 @@ pub fn serve( }); let routes = warp::post() + .and(auth_header_filter()) .and(root.or(echo)) + .recover(handle_rejection) // Add a `Server` header. .map(|reply| warp::reply::with_header(reply, "Server", "lighthouse-mock-execution-client")); diff --git a/beacon_node/genesis/tests/tests.rs b/beacon_node/genesis/tests/tests.rs index 0fbe15871e..8b77c89471 100644 --- a/beacon_node/genesis/tests/tests.rs +++ b/beacon_node/genesis/tests/tests.rs @@ -1,4 +1,4 @@ -//! NOTE: These tests will not pass unless ganache-cli is running on `ENDPOINT` (see below). +//! NOTE: These tests will not pass unless ganache is running on `ENDPOINT` (see below). //! //! You can start a suitable instance using the `ganache_test_node.sh` script in the `scripts` //! dir in the root of the `lighthouse` repo. diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 3a3f408a39..6c25f36120 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -38,7 +38,7 @@ use state_id::StateId; use std::borrow::Cow; use std::convert::TryInto; use std::future::Future; -use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; use std::pin::Pin; use std::sync::Arc; @@ -98,7 +98,7 @@ pub struct Context { #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] pub struct Config { pub enabled: bool, - pub listen_addr: Ipv4Addr, + pub listen_addr: IpAddr, pub listen_port: u16, pub allow_origin: Option, pub serve_legacy_spec: bool, @@ -110,7 +110,7 @@ impl Default for Config { fn default() -> Self { Self { enabled: false, - listen_addr: Ipv4Addr::new(127, 0, 0, 1), + listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), listen_port: 5052, allow_origin: None, serve_legacy_spec: true, @@ -954,7 +954,6 @@ pub fn serve( delay, ); - match chain.process_block(block.clone()) { Ok(root) => { info!( @@ -2229,6 +2228,13 @@ pub fn serve( ) })?; + chain.prepare_beacon_proposer_blocking().map_err(|e| { + warp_utils::reject::custom_bad_request(format!( + "error updating proposer preparations: {:?}", + e + )) + })?; + Ok(()) }) }, @@ -2784,7 +2790,7 @@ pub fn serve( .map(|reply| warp::reply::with_header(reply, "Server", &version_with_platform())) .with(cors_builder.build()); - let http_socket: SocketAddrV4 = SocketAddrV4::new(config.listen_addr, config.listen_port); + let http_socket: SocketAddr = SocketAddr::new(config.listen_addr, config.listen_port); let http_server: HttpServer = match config.tls_config { Some(tls_config) => { let (socket, server) = warp::serve(routes) diff --git a/beacon_node/http_api/src/proposer_duties.rs b/beacon_node/http_api/src/proposer_duties.rs index d817c9f653..b040eec779 100644 --- a/beacon_node/http_api/src/proposer_duties.rs +++ b/beacon_node/http_api/src/proposer_duties.rs @@ -2,15 +2,15 @@ use crate::state_id::StateId; use beacon_chain::{ + beacon_proposer_cache::{compute_proposer_duties_from_head, ensure_state_is_in_epoch}, BeaconChain, BeaconChainError, BeaconChainTypes, MAXIMUM_GOSSIP_CLOCK_DISPARITY, }; use eth2::types::{self as api_types}; use safe_arith::SafeArith; use slog::{debug, Logger}; use slot_clock::SlotClock; -use state_processing::state_advance::partial_state_advance; use std::cmp::Ordering; -use types::{BeaconState, ChainSpec, CloneConfig, Epoch, EthSpec, Fork, Hash256, Slot}; +use types::{CloneConfig, Epoch, EthSpec, Hash256, Slot}; /// The struct that is returned to the requesting HTTP client. type ApiDuties = api_types::DutiesResponse>; @@ -55,7 +55,9 @@ pub fn proposer_duties( .safe_add(1) .map_err(warp_utils::reject::arith_error)? { - let (proposers, dependent_root, _) = compute_proposer_duties(request_epoch, chain)?; + let (proposers, dependent_root, _) = + compute_proposer_duties_from_head(request_epoch, chain) + .map_err(warp_utils::reject::beacon_chain_error)?; convert_to_api_response(chain, request_epoch, dependent_root, proposers) } else if request_epoch > current_epoch @@ -130,7 +132,8 @@ fn compute_and_cache_proposer_duties( current_epoch: Epoch, chain: &BeaconChain, ) -> Result { - let (indices, dependent_root, fork) = compute_proposer_duties(current_epoch, chain)?; + let (indices, dependent_root, fork) = compute_proposer_duties_from_head(current_epoch, chain) + .map_err(warp_utils::reject::beacon_chain_error)?; // Prime the proposer shuffling cache with the newly-learned value. chain @@ -143,35 +146,6 @@ fn compute_and_cache_proposer_duties( convert_to_api_response(chain, current_epoch, dependent_root, indices) } -/// Compute the proposer duties using the head state without cache. -fn compute_proposer_duties( - current_epoch: Epoch, - chain: &BeaconChain, -) -> Result<(Vec, Hash256, Fork), warp::reject::Rejection> { - // Take a copy of the head of the chain. - let head = chain - .head() - .map_err(warp_utils::reject::beacon_chain_error)?; - let mut state = head.beacon_state; - let head_state_root = head.beacon_block.state_root(); - - // Advance the state into the requested epoch. - ensure_state_is_in_epoch(&mut state, head_state_root, current_epoch, &chain.spec)?; - - let indices = state - .get_beacon_proposer_indices(&chain.spec) - .map_err(BeaconChainError::from) - .map_err(warp_utils::reject::beacon_chain_error)?; - - let dependent_root = state - // The only block which decides its own shuffling is the genesis block. - .proposer_shuffling_decision_root(chain.genesis_block_root) - .map_err(BeaconChainError::from) - .map_err(warp_utils::reject::beacon_chain_error)?; - - Ok((indices, dependent_root, state.fork())) -} - /// Compute some proposer duties by reading a `BeaconState` from disk, completely ignoring the /// `beacon_proposer_cache`. fn compute_historic_proposer_duties( @@ -198,7 +172,8 @@ fn compute_historic_proposer_duties( let state = if let Some((state_root, mut state)) = state_opt { // If we've loaded the head state it might be from a previous epoch, ensure it's in a // suitable epoch. - ensure_state_is_in_epoch(&mut state, state_root, epoch, &chain.spec)?; + ensure_state_is_in_epoch(&mut state, state_root, epoch, &chain.spec) + .map_err(warp_utils::reject::beacon_chain_error)?; state } else { StateId::slot(epoch.start_slot(T::EthSpec::slots_per_epoch())).state(chain)? @@ -228,39 +203,6 @@ fn compute_historic_proposer_duties( convert_to_api_response(chain, epoch, dependent_root, indices) } -/// If required, advance `state` to `target_epoch`. -/// -/// ## Details -/// -/// - Returns an error if `state.current_epoch() > target_epoch`. -/// - No-op if `state.current_epoch() == target_epoch`. -/// - It must be the case that `state.canonical_root() == state_root`, but this function will not -/// check that. -fn ensure_state_is_in_epoch( - state: &mut BeaconState, - state_root: Hash256, - target_epoch: Epoch, - spec: &ChainSpec, -) -> Result<(), warp::reject::Rejection> { - match state.current_epoch().cmp(&target_epoch) { - // Protects against an inconsistent slot clock. - Ordering::Greater => Err(warp_utils::reject::custom_server_error(format!( - "state epoch {} is later than target epoch {}", - state.current_epoch(), - target_epoch - ))), - // The state needs to be advanced. - Ordering::Less => { - let target_slot = target_epoch.start_slot(E::slots_per_epoch()); - partial_state_advance(state, Some(state_root), target_slot, spec) - .map_err(BeaconChainError::from) - .map_err(warp_utils::reject::beacon_chain_error) - } - // The state is suitable, nothing to do. - Ordering::Equal => Ok(()), - } -} - /// Converts the internal representation of proposer duties into one that is compatible with the /// standard API. fn convert_to_api_response( diff --git a/beacon_node/http_api/tests/common.rs b/beacon_node/http_api/tests/common.rs index cf20e2f686..06466c43bb 100644 --- a/beacon_node/http_api/tests/common.rs +++ b/beacon_node/http_api/tests/common.rs @@ -15,7 +15,7 @@ use network::NetworkMessage; use sensitive_url::SensitiveUrl; use slog::Logger; use std::future::Future; -use std::net::{Ipv4Addr, SocketAddr}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::Arc; use std::time::Duration; use tokio::sync::{mpsc, oneshot}; @@ -128,7 +128,7 @@ pub async fn create_api_server( let context = Arc::new(Context { config: Config { enabled: true, - listen_addr: Ipv4Addr::new(127, 0, 0, 1), + listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), listen_port: 0, allow_origin: None, serve_legacy_spec: true, diff --git a/beacon_node/http_metrics/src/lib.rs b/beacon_node/http_metrics/src/lib.rs index 729d92ee9e..dfdb8f7ff1 100644 --- a/beacon_node/http_metrics/src/lib.rs +++ b/beacon_node/http_metrics/src/lib.rs @@ -9,7 +9,7 @@ use lighthouse_version::version_with_platform; use serde::{Deserialize, Serialize}; use slog::{crit, info, Logger}; use std::future::Future; -use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; use std::sync::Arc; use warp::{http::Response, Filter}; @@ -48,7 +48,7 @@ pub struct Context { #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] pub struct Config { pub enabled: bool, - pub listen_addr: Ipv4Addr, + pub listen_addr: IpAddr, pub listen_port: u16, pub allow_origin: Option, pub allocator_metrics_enabled: bool, @@ -58,7 +58,7 @@ impl Default for Config { fn default() -> Self { Self { enabled: false, - listen_addr: Ipv4Addr::new(127, 0, 0, 1), + listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), listen_port: 5054, allow_origin: None, allocator_metrics_enabled: true, @@ -131,7 +131,7 @@ pub fn serve( .with(cors_builder.build()); let (listening_socket, server) = warp::serve(routes).try_bind_with_graceful_shutdown( - SocketAddrV4::new(config.listen_addr, config.listen_port), + SocketAddr::new(config.listen_addr, config.listen_port), async { shutdown.await; }, diff --git a/beacon_node/http_metrics/tests/tests.rs b/beacon_node/http_metrics/tests/tests.rs index fd8733cfe5..b3e02d4cb6 100644 --- a/beacon_node/http_metrics/tests/tests.rs +++ b/beacon_node/http_metrics/tests/tests.rs @@ -2,7 +2,7 @@ use beacon_chain::test_utils::EphemeralHarnessType; use environment::null_logger; use http_metrics::Config; use reqwest::StatusCode; -use std::net::Ipv4Addr; +use std::net::{IpAddr, Ipv4Addr}; use std::sync::Arc; use tokio::sync::oneshot; use types::MainnetEthSpec; @@ -17,7 +17,7 @@ async fn returns_200_ok() { let context = Arc::new(Context { config: Config { enabled: true, - listen_addr: Ipv4Addr::new(127, 0, 0, 1), + listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), listen_port: 0, allow_origin: None, allocator_metrics_enabled: true, diff --git a/beacon_node/lighthouse_network/Cargo.toml b/beacon_node/lighthouse_network/Cargo.toml index 70a50c02d5..6f746705d6 100644 --- a/beacon_node/lighthouse_network/Cargo.toml +++ b/beacon_node/lighthouse_network/Cargo.toml @@ -35,16 +35,14 @@ tiny-keccak = "2.0.2" task_executor = { path = "../../common/task_executor" } rand = "0.7.3" directory = { path = "../../common/directory" } -regex = "1.3.9" +regex = "1.5.5" strum = { version = "0.21.0", features = ["derive"] } superstruct = "0.4.0" prometheus-client = "0.15.0" unused_port = { path = "../../common/unused_port" } [dependencies.libp2p] -git = "https://github.com/sigp/rust-libp2p" -# branch libp2p-gossipsub-interval-hotfix -rev = "e213703e616eaba3c482d7714775e0d37c4ae8e5" +version = "0.43.0" default-features = false features = ["websocket", "identify", "mplex", "yamux", "noise", "gossipsub", "dns-tokio", "tcp-tokio", "plaintext", "secp256k1"] diff --git a/beacon_node/lighthouse_network/src/behaviour/mod.rs b/beacon_node/lighthouse_network/src/behaviour/mod.rs index 155e01d789..b5b0049cbd 100644 --- a/beacon_node/lighthouse_network/src/behaviour/mod.rs +++ b/beacon_node/lighthouse_network/src/behaviour/mod.rs @@ -70,9 +70,16 @@ 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 { +pub enum BehaviourEvent { /// We have successfully dialed and connected to a peer. PeerConnectedOutgoing(PeerId), /// A peer has successfully dialed and connected to us. @@ -86,7 +93,7 @@ pub enum BehaviourEvent { /// An RPC Request that was sent failed. RPCFailed { /// The id of the failed request. - id: RequestId, + id: AppReqId, /// The peer to which this request was sent. peer_id: PeerId, }, @@ -102,7 +109,7 @@ pub enum BehaviourEvent { /// Peer that sent the response. peer_id: PeerId, /// Id of the request to which the peer is responding. - id: RequestId, + id: AppReqId, /// Response the peer sent. response: Response, }, @@ -134,16 +141,16 @@ enum InternalBehaviourMessage { /// behaviours. #[derive(NetworkBehaviour)] #[behaviour( - out_event = "BehaviourEvent", + out_event = "BehaviourEvent", poll_method = "poll", event_process = true )] -pub struct Behaviour { +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, + eth2_rpc: RPC, TSpec>, /// Discv5 Discovery protocol. discovery: Discovery, /// Keep regular connection to peers and disconnect if absent. @@ -156,7 +163,7 @@ pub struct Behaviour { /* Auxiliary Fields */ /// The output events generated by this behaviour to be consumed in the swarm poll. #[behaviour(ignore)] - events: VecDeque>, + 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)] @@ -192,7 +199,7 @@ pub struct Behaviour { } /// Implements the combined behaviour for the libp2p service. -impl Behaviour { +impl Behaviour { pub async fn new( local_key: &Keypair, ctx: ServiceContext<'_>, @@ -562,9 +569,9 @@ impl Behaviour { /* Eth2 RPC behaviour functions */ /// Send a request to a peer over RPC. - pub fn send_request(&mut self, peer_id: PeerId, request_id: RequestId, request: Request) { + pub fn send_request(&mut self, peer_id: PeerId, request_id: AppReqId, request: Request) { self.eth2_rpc - .send_request(peer_id, request_id, request.into()) + .send_request(peer_id, RequestId::Application(request_id), request.into()) } /// Send a successful response to a peer over RPC. @@ -718,12 +725,12 @@ impl Behaviour { } /// Sends a Ping request to the peer. - fn ping(&mut self, id: RequestId, peer_id: PeerId) { + fn ping(&mut self, peer_id: PeerId) { let ping = crate::rpc::Ping { data: *self.network_globals.local_metadata.read().seq_number(), }; - trace!(self.log, "Sending Ping"; "request_id" => id, "peer_id" => %peer_id); - + trace!(self.log, "Sending Ping"; "peer_id" => %peer_id); + let id = RequestId::Behaviour; self.eth2_rpc .send_request(peer_id, id, OutboundRequest::Ping(ping)); } @@ -761,13 +768,19 @@ impl Behaviour { // 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) { - if !matches!(id, RequestId::Behaviour) { - self.add_event(BehaviourEvent::ResponseReceived { + 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 => {} } } @@ -793,7 +806,7 @@ impl Behaviour { } /// Adds an event to the queue waking the current task to process it. - fn add_event(&mut self, event: BehaviourEvent) { + fn add_event(&mut self, event: BehaviourEvent) { self.events.push_back(event); if let Some(waker) = &self.waker { waker.wake_by_ref(); @@ -869,7 +882,11 @@ impl Behaviour { */ // Gossipsub -impl NetworkBehaviourEventProcess for Behaviour { +impl NetworkBehaviourEventProcess for Behaviour +where + AppReqId: ReqId, + TSpec: EthSpec, +{ fn inject_event(&mut self, event: GossipsubEvent) { match event { GossipsubEvent::Message { @@ -961,8 +978,13 @@ impl NetworkBehaviourEventProcess for Behaviour< } // RPC -impl NetworkBehaviourEventProcess> for Behaviour { - fn inject_event(&mut self, event: RPCMessage) { +impl NetworkBehaviourEventProcess, TSpec>> + for Behaviour +where + AppReqId: ReqId, + TSpec: EthSpec, +{ + fn inject_event(&mut self, event: RPCMessage, TSpec>) { let peer_id = event.peer_id; if !self.peer_manager.is_connected(&peer_id) { @@ -1006,7 +1028,7 @@ impl NetworkBehaviourEventProcess> for Behavio ConnectionDirection::Outgoing, ); // inform failures of requests comming outside the behaviour - if !matches!(id, RequestId::Behaviour) { + if let RequestId::Application(id) = id { self.add_event(BehaviourEvent::RPCFailed { peer_id, id }); } } @@ -1090,7 +1112,11 @@ impl NetworkBehaviourEventProcess> for Behavio } // Discovery -impl NetworkBehaviourEventProcess for Behaviour { +impl NetworkBehaviourEventProcess for Behaviour +where + AppReqId: ReqId, + TSpec: EthSpec, +{ fn inject_event(&mut self, event: DiscoveryEvent) { match event { DiscoveryEvent::SocketUpdated(socket_addr) => { @@ -1119,7 +1145,11 @@ impl NetworkBehaviourEventProcess for Behaviour< } // Identify -impl NetworkBehaviourEventProcess for Behaviour { +impl NetworkBehaviourEventProcess for Behaviour +where + TSpec: EthSpec, + AppReqId: ReqId, +{ fn inject_event(&mut self, event: IdentifyEvent) { match event { IdentifyEvent::Received { peer_id, mut info } => { @@ -1140,15 +1170,20 @@ impl NetworkBehaviourEventProcess for Behaviour Behaviour { +type BehaviourHandler = + as NetworkBehaviour>::ConnectionHandler; + +impl Behaviour +where + TSpec: EthSpec, + AppReqId: ReqId, +{ /// Consumes the events list and drives the Lighthouse global NetworkBehaviour. fn poll( &mut self, cx: &mut Context, _: &mut impl PollParameters, - ) -> Poll< - NBAction, as NetworkBehaviour>::ProtocolsHandler>, - > { + ) -> Poll, BehaviourHandler>> { if let Some(waker) = &self.waker { if waker.will_wake(cx.waker()) { self.waker = Some(cx.waker().clone()); @@ -1207,7 +1242,9 @@ impl Behaviour { } } -impl NetworkBehaviourEventProcess for Behaviour { +impl NetworkBehaviourEventProcess + for Behaviour +{ fn inject_event(&mut self, event: PeerManagerEvent) { match event { PeerManagerEvent::PeerConnectedIncoming(peer_id) => { @@ -1242,7 +1279,7 @@ impl NetworkBehaviourEventProcess for Behaviou } PeerManagerEvent::Ping(peer_id) => { // send a ping request to this peer - self.ping(RequestId::Behaviour, peer_id); + self.ping(peer_id); } PeerManagerEvent::MetaData(peer_id) => { self.send_meta_data_request(peer_id); @@ -1251,7 +1288,8 @@ impl NetworkBehaviourEventProcess for Behaviou debug!(self.log, "Peer Manager disconnecting peer"; "peer_id" => %peer_id, "reason" => %reason); // send one goodbye - self.eth2_rpc.shutdown(peer_id, reason); + self.eth2_rpc + .shutdown(peer_id, RequestId::Behaviour, reason); } } } @@ -1335,3 +1373,19 @@ pub fn save_metadata_to_disk(dir: &Path, metadata: MetaData, log: } } } + +impl slog::Value for RequestId { + fn serialize( + &self, + record: &slog::Record, + key: slog::Key, + serializer: &mut dyn slog::Serializer, + ) -> slog::Result { + match self { + RequestId::Behaviour => slog::Value::serialize("Behaviour", record, key, serializer), + RequestId::Application(ref id) => { + slog::Value::serialize(&format_args!("{:?}", id), record, key, serializer) + } + } + } +} diff --git a/beacon_node/lighthouse_network/src/config.rs b/beacon_node/lighthouse_network/src/config.rs index 7a2ba61997..263ef0c7cb 100644 --- a/beacon_node/lighthouse_network/src/config.rs +++ b/beacon_node/lighthouse_network/src/config.rs @@ -176,6 +176,7 @@ impl Default for Config { .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(); @@ -347,3 +348,28 @@ pub fn gossipsub_config(network_load: u8, fork_context: Arc) -> Gos .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/beacon_node/lighthouse_network/src/discovery/mod.rs b/beacon_node/lighthouse_network/src/discovery/mod.rs index ab4d54a1e7..23b8895cf3 100644 --- a/beacon_node/lighthouse_network/src/discovery/mod.rs +++ b/beacon_node/lighthouse_network/src/discovery/mod.rs @@ -24,7 +24,7 @@ use futures::stream::FuturesUnordered; pub use libp2p::{ core::{connection::ConnectionId, ConnectedPoint, Multiaddr, PeerId}, swarm::{ - protocols_handler::ProtocolsHandler, DialError, NetworkBehaviour, + handler::ConnectionHandler, DialError, NetworkBehaviour, NetworkBehaviourAction as NBAction, NotifyHandler, PollParameters, SubstreamProtocol, }, }; @@ -908,11 +908,11 @@ impl Discovery { impl NetworkBehaviour for Discovery { // Discovery is not a real NetworkBehaviour... - type ProtocolsHandler = libp2p::swarm::protocols_handler::DummyProtocolsHandler; + type ConnectionHandler = libp2p::swarm::handler::DummyConnectionHandler; type OutEvent = DiscoveryEvent; - fn new_handler(&mut self) -> Self::ProtocolsHandler { - libp2p::swarm::protocols_handler::DummyProtocolsHandler::default() + 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. @@ -932,14 +932,14 @@ impl NetworkBehaviour for Discovery { &mut self, _: PeerId, _: ConnectionId, - _: ::OutEvent, + _: ::OutEvent, ) { } fn inject_dial_failure( &mut self, peer_id: Option, - _handler: Self::ProtocolsHandler, + _handler: Self::ConnectionHandler, error: &DialError, ) { if let Some(peer_id) = peer_id { @@ -967,7 +967,7 @@ impl NetworkBehaviour for Discovery { &mut self, cx: &mut Context, _: &mut impl PollParameters, - ) -> Poll> { + ) -> Poll> { if !self.started { return Poll::Pending; } diff --git a/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs b/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs index d4eef65b0c..3bda64f0b1 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs @@ -3,9 +3,9 @@ use std::task::{Context, Poll}; use futures::StreamExt; use libp2p::core::connection::ConnectionId; use libp2p::core::ConnectedPoint; -use libp2p::swarm::protocols_handler::DummyProtocolsHandler; +use libp2p::swarm::handler::DummyConnectionHandler; use libp2p::swarm::{ - DialError, NetworkBehaviour, NetworkBehaviourAction, PollParameters, ProtocolsHandler, + ConnectionHandler, DialError, NetworkBehaviour, NetworkBehaviourAction, PollParameters, }; use libp2p::{Multiaddr, PeerId}; use slog::{debug, error}; @@ -19,21 +19,21 @@ use super::peerdb::BanResult; use super::{PeerManager, PeerManagerEvent, ReportSource}; impl NetworkBehaviour for PeerManager { - type ProtocolsHandler = DummyProtocolsHandler; + type ConnectionHandler = DummyConnectionHandler; type OutEvent = PeerManagerEvent; /* Required trait members */ - fn new_handler(&mut self) -> Self::ProtocolsHandler { - DummyProtocolsHandler::default() + fn new_handler(&mut self) -> Self::ConnectionHandler { + DummyConnectionHandler::default() } fn inject_event( &mut self, _: PeerId, _: ConnectionId, - _: ::OutEvent, + _: ::OutEvent, ) { unreachable!("Dummy handler does not emit events") } @@ -42,7 +42,7 @@ impl NetworkBehaviour for PeerManager { &mut self, cx: &mut Context<'_>, _params: &mut impl PollParameters, - ) -> Poll> { + ) -> Poll> { // perform the heartbeat when necessary while self.heartbeat.poll_tick(cx).is_ready() { self.heartbeat(); @@ -110,9 +110,13 @@ impl NetworkBehaviour for PeerManager { _connection_id: &ConnectionId, endpoint: &ConnectedPoint, _failed_addresses: Option<&Vec>, - _other_established: usize, + other_established: usize, ) { debug!(self.log, "Connection established"; "peer_id" => %peer_id, "connection" => ?endpoint.to_endpoint()); + 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(); @@ -178,7 +182,7 @@ impl NetworkBehaviour for PeerManager { peer_id: &PeerId, _: &ConnectionId, _: &ConnectedPoint, - _: DummyProtocolsHandler, + _: DummyConnectionHandler, remaining_established: usize, ) { if remaining_established > 0 { @@ -243,7 +247,7 @@ impl NetworkBehaviour for PeerManager { fn inject_dial_failure( &mut self, peer_id: Option, - _handler: DummyProtocolsHandler, + _handler: DummyConnectionHandler, _error: &DialError, ) { if let Some(peer_id) = peer_id { diff --git a/beacon_node/lighthouse_network/src/rpc/handler.rs b/beacon_node/lighthouse_network/src/rpc/handler.rs index 37724e028a..2b9e7c4902 100644 --- a/beacon_node/lighthouse_network/src/rpc/handler.rs +++ b/beacon_node/lighthouse_network/src/rpc/handler.rs @@ -1,12 +1,10 @@ #![allow(clippy::type_complexity)] #![allow(clippy::cognitive_complexity)] -use super::methods::{ - GoodbyeReason, RPCCodedResponse, RPCResponseErrorCode, RequestId, ResponseTermination, -}; +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}; +use super::{RPCReceived, RPCSend, ReqId}; use crate::rpc::outbound::{OutboundFramed, OutboundRequest}; use crate::rpc::protocol::InboundFramed; use fnv::FnvHashMap; @@ -15,8 +13,9 @@ use futures::{Sink, SinkExt}; use libp2p::core::upgrade::{ InboundUpgrade, NegotiationError, OutboundUpgrade, ProtocolError, UpgradeError, }; -use libp2p::swarm::protocols_handler::{ - KeepAlive, ProtocolsHandler, ProtocolsHandlerEvent, ProtocolsHandlerUpgrErr, SubstreamProtocol, +use libp2p::swarm::handler::{ + ConnectionHandler, ConnectionHandlerEvent, ConnectionHandlerUpgrErr, KeepAlive, + SubstreamProtocol, }; use libp2p::swarm::NegotiatedSubstream; use slog::{crit, debug, trace, warn}; @@ -48,11 +47,11 @@ pub struct SubstreamId(usize); type InboundSubstream = InboundFramed; /// Events the handler emits to the behaviour. -type HandlerEvent = Result, HandlerErr>; +pub type HandlerEvent = Result, HandlerErr>; /// An error encountered by the handler. #[derive(Debug)] -pub enum HandlerErr { +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 { @@ -68,7 +67,7 @@ pub enum HandlerErr { /// indicates an error. Outbound { /// Application-given Id of the request for which an error occurred. - id: RequestId, + id: Id, /// Information of the protocol. proto: Protocol, /// The error that occurred. @@ -76,8 +75,8 @@ pub enum HandlerErr { }, } -/// Implementation of `ProtocolsHandler` for the RPC protocol. -pub struct RPCHandler +/// Implementation of `ConnectionHandler` for the RPC protocol. +pub struct RPCHandler where TSpec: EthSpec, { @@ -85,10 +84,10 @@ where listen_protocol: SubstreamProtocol, ()>, /// Queue of events to produce in `poll()`. - events_out: SmallVec<[HandlerEvent; 4]>, + events_out: SmallVec<[HandlerEvent; 4]>, /// Queue of outbound substreams to open. - dial_queue: SmallVec<[(RequestId, OutboundRequest); 4]>, + dial_queue: SmallVec<[(Id, OutboundRequest); 4]>, /// Current number of concurrent outbound substreams being opened. dial_negotiated: u32, @@ -100,7 +99,7 @@ where inbound_substreams_delay: DelayQueue, /// Map of outbound substreams that need to be driven to completion. - outbound_substreams: FnvHashMap>, + outbound_substreams: FnvHashMap>, /// Inbound substream `DelayQueue` which keeps track of when an inbound substream will timeout. outbound_substreams_delay: DelayQueue, @@ -162,7 +161,7 @@ struct InboundInfo { } /// Contains the information the handler keeps on established outbound substreams. -struct OutboundInfo { +struct OutboundInfo { /// State of the substream. state: OutboundSubstreamState, /// Key to keep track of the substream's timeout via `self.outbound_substreams_delay`. @@ -171,8 +170,8 @@ struct OutboundInfo { proto: Protocol, /// Number of chunks to be seen from the peer's response. remaining_chunks: Option, - /// `RequestId` as given by the application that sent the request. - req_id: RequestId, + /// `Id` as given by the application that sent the request. + req_id: Id, } /// State of an inbound substream connection. @@ -203,7 +202,7 @@ pub enum OutboundSubstreamState { Poisoned, } -impl RPCHandler +impl RPCHandler where TSpec: EthSpec, { @@ -234,7 +233,7 @@ where /// Initiates the handler's shutdown process, sending an optional Goodbye message to the /// peer. - fn shutdown(&mut self, goodbye_reason: Option) { + fn shutdown(&mut self, goodbye_reason: Option<(Id, GoodbyeReason)>) { if matches!(self.state, HandlerState::Active) { if !self.dial_queue.is_empty() { debug!(self.log, "Starting handler shutdown"; "unsent_queued_requests" => self.dial_queue.len()); @@ -249,9 +248,8 @@ where } // Queue our goodbye message. - if let Some(reason) = goodbye_reason { - self.dial_queue - .push((RequestId::Router, OutboundRequest::Goodbye(reason))); + if let Some((id, reason)) = goodbye_reason { + self.dial_queue.push((id, OutboundRequest::Goodbye(reason))); } self.state = HandlerState::ShuttingDown(Box::new(sleep_until( @@ -261,7 +259,7 @@ where } /// Opens an outbound substream with a request. - fn send_request(&mut self, id: RequestId, req: OutboundRequest) { + fn send_request(&mut self, id: Id, req: OutboundRequest) { match self.state { HandlerState::Active => { self.dial_queue.push((id, req)); @@ -309,16 +307,17 @@ where } } -impl ProtocolsHandler for RPCHandler +impl ConnectionHandler for RPCHandler where TSpec: EthSpec, + Id: ReqId, { - type InEvent = RPCSend; - type OutEvent = HandlerEvent; + type InEvent = RPCSend; + type OutEvent = HandlerEvent; type Error = RPCError; type InboundProtocol = RPCProtocol; type OutboundProtocol = OutboundRequestContainer; - type OutboundOpenInfo = (RequestId, OutboundRequest); // Keep track of the id and the request + type OutboundOpenInfo = (Id, OutboundRequest); // Keep track of the id and the request type InboundOpenInfo = (); fn listen_protocol(&self) -> SubstreamProtocol { @@ -431,7 +430,7 @@ where 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(reason) => self.shutdown(Some(reason)), + 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 { @@ -442,12 +441,13 @@ where fn inject_dial_upgrade_error( &mut self, request_info: Self::OutboundOpenInfo, - error: ProtocolsHandlerUpgrErr< + error: ConnectionHandlerUpgrErr< >::Error, >, ) { let (id, req) = request_info; - if let ProtocolsHandlerUpgrErr::Upgrade(UpgradeError::Apply(RPCError::IoError(_))) = error { + 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); @@ -461,13 +461,13 @@ where self.outbound_io_error_retries = 0; // map the error let error = match error { - ProtocolsHandlerUpgrErr::Timer => RPCError::InternalError("Timer failed"), - ProtocolsHandlerUpgrErr::Timeout => RPCError::NegotiationTimeout, - ProtocolsHandlerUpgrErr::Upgrade(UpgradeError::Apply(e)) => e, - ProtocolsHandlerUpgrErr::Upgrade(UpgradeError::Select(NegotiationError::Failed)) => { + ConnectionHandlerUpgrErr::Timer => RPCError::InternalError("Timer failed"), + ConnectionHandlerUpgrErr::Timeout => RPCError::NegotiationTimeout, + ConnectionHandlerUpgrErr::Upgrade(UpgradeError::Apply(e)) => e, + ConnectionHandlerUpgrErr::Upgrade(UpgradeError::Select(NegotiationError::Failed)) => { RPCError::UnsupportedProtocol } - ProtocolsHandlerUpgrErr::Upgrade(UpgradeError::Select( + ConnectionHandlerUpgrErr::Upgrade(UpgradeError::Select( NegotiationError::ProtocolError(e), )) => match e { ProtocolError::IoError(io_err) => RPCError::IoError(io_err.to_string()), @@ -517,7 +517,7 @@ where &mut self, cx: &mut Context<'_>, ) -> Poll< - ProtocolsHandlerEvent< + ConnectionHandlerEvent< Self::OutboundProtocol, Self::OutboundOpenInfo, Self::OutEvent, @@ -533,7 +533,7 @@ where } // return any events that need to be reported if !self.events_out.is_empty() { - return Poll::Ready(ProtocolsHandlerEvent::Custom(self.events_out.remove(0))); + return Poll::Ready(ConnectionHandlerEvent::Custom(self.events_out.remove(0))); } else { self.events_out.shrink_to_fit(); } @@ -543,7 +543,7 @@ where if delay.is_elapsed() { self.state = HandlerState::Deactivated; debug!(self.log, "Handler deactivated"); - return Poll::Ready(ProtocolsHandlerEvent::Close(RPCError::InternalError( + return Poll::Ready(ConnectionHandlerEvent::Close(RPCError::InternalError( "Shutdown timeout", ))); } @@ -575,7 +575,7 @@ where Poll::Ready(Some(Err(e))) => { warn!(self.log, "Inbound substream poll failed"; "error" => ?e); // drops the peer if we cannot read the delay queue - return Poll::Ready(ProtocolsHandlerEvent::Close(RPCError::InternalError( + return Poll::Ready(ConnectionHandlerEvent::Close(RPCError::InternalError( "Could not poll inbound stream timer", ))); } @@ -596,14 +596,14 @@ where error: RPCError::StreamTimeout, }; // notify the user - return Poll::Ready(ProtocolsHandlerEvent::Custom(Err(outbound_err))); + return Poll::Ready(ConnectionHandlerEvent::Custom(Err(outbound_err))); } else { crit!(self.log, "timed out substream not in the books"; "stream_id" => outbound_id.get_ref()); } } Poll::Ready(Some(Err(e))) => { warn!(self.log, "Outbound substream poll failed"; "error" => ?e); - return Poll::Ready(ProtocolsHandlerEvent::Close(RPCError::InternalError( + return Poll::Ready(ConnectionHandlerEvent::Close(RPCError::InternalError( "Could not poll outbound stream timer", ))); } @@ -856,7 +856,7 @@ where }), }; - return Poll::Ready(ProtocolsHandlerEvent::Custom(received)); + return Poll::Ready(ConnectionHandlerEvent::Custom(received)); } Poll::Ready(None) => { // stream closed @@ -871,7 +871,7 @@ where // notify the application error if request.expected_responses() > 1 { // return an end of stream result - return Poll::Ready(ProtocolsHandlerEvent::Custom(Ok( + return Poll::Ready(ConnectionHandlerEvent::Custom(Ok( RPCReceived::EndOfStream(request_id, request.stream_termination()), ))); } @@ -882,7 +882,7 @@ where proto: request.protocol(), error: RPCError::IncompleteStream, }; - return Poll::Ready(ProtocolsHandlerEvent::Custom(Err(outbound_err))); + return Poll::Ready(ConnectionHandlerEvent::Custom(Err(outbound_err))); } Poll::Pending => { entry.get_mut().state = @@ -898,7 +898,7 @@ where error: e, }; entry.remove_entry(); - return Poll::Ready(ProtocolsHandlerEvent::Custom(Err(outbound_err))); + return Poll::Ready(ConnectionHandlerEvent::Custom(Err(outbound_err))); } }, OutboundSubstreamState::Closing(mut substream) => { @@ -924,7 +924,7 @@ where }; if let Some(termination) = termination { - return Poll::Ready(ProtocolsHandlerEvent::Custom(Ok( + return Poll::Ready(ConnectionHandlerEvent::Custom(Ok( RPCReceived::EndOfStream(request_id, termination), ))); } @@ -946,7 +946,7 @@ where self.dial_negotiated += 1; let (id, req) = self.dial_queue.remove(0); self.dial_queue.shrink_to_fit(); - return Poll::Ready(ProtocolsHandlerEvent::OutboundSubstreamRequest { + return Poll::Ready(ConnectionHandlerEvent::OutboundSubstreamRequest { protocol: SubstreamProtocol::new( OutboundRequestContainer { req: req.clone(), @@ -967,7 +967,7 @@ where && self.events_out.is_empty() && self.dial_negotiated == 0 { - return Poll::Ready(ProtocolsHandlerEvent::Close(RPCError::Disconnected)); + return Poll::Ready(ConnectionHandlerEvent::Close(RPCError::Disconnected)); } } diff --git a/beacon_node/lighthouse_network/src/rpc/methods.rs b/beacon_node/lighthouse_network/src/rpc/methods.rs index 2d43ecbf07..087f8e5336 100644 --- a/beacon_node/lighthouse_network/src/rpc/methods.rs +++ b/beacon_node/lighthouse_network/src/rpc/methods.rs @@ -56,17 +56,6 @@ impl ToString for ErrorType { /* Requests */ -/// Identifier of a request. -/// -// NOTE: The handler stores the `RequestId` to inform back of responses and errors, but it's execution -// is independent of the contents on this type. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RequestId { - Router, - Sync(usize), - Behaviour, -} - /// The STATUS request/response handshake message. #[derive(Encode, Decode, Clone, Debug, PartialEq)] pub struct StatusMessage { @@ -432,18 +421,3 @@ impl slog::KV for StatusMessage { slog::Result::Ok(()) } } - -impl slog::Value for RequestId { - fn serialize( - &self, - record: &slog::Record, - key: slog::Key, - serializer: &mut dyn slog::Serializer, - ) -> slog::Result { - match self { - RequestId::Behaviour => slog::Value::serialize("Behaviour", record, key, serializer), - RequestId::Router => slog::Value::serialize("Router", record, key, serializer), - RequestId::Sync(ref id) => slog::Value::serialize(id, record, key, serializer), - } - } -} diff --git a/beacon_node/lighthouse_network/src/rpc/mod.rs b/beacon_node/lighthouse_network/src/rpc/mod.rs index 8c4a0b14c7..884acd9bcf 100644 --- a/beacon_node/lighthouse_network/src/rpc/mod.rs +++ b/beacon_node/lighthouse_network/src/rpc/mod.rs @@ -5,13 +5,13 @@ //! syncing. use futures::future::FutureExt; -use handler::RPCHandler; -use libp2p::core::{connection::ConnectionId, ConnectedPoint}; +use handler::{HandlerEvent, RPCHandler}; +use libp2p::core::connection::ConnectionId; use libp2p::swarm::{ - protocols_handler::ProtocolsHandler, NetworkBehaviour, NetworkBehaviourAction, NotifyHandler, + handler::ConnectionHandler, NetworkBehaviour, NetworkBehaviourAction, NotifyHandler, PollParameters, SubstreamProtocol, }; -use libp2p::{Multiaddr, PeerId}; +use libp2p::PeerId; use rate_limiter::{RPCRateLimiter as RateLimiter, RPCRateLimiterBuilder, RateLimitedErr}; use slog::{crit, debug, o}; use std::marker::PhantomData; @@ -27,7 +27,7 @@ pub(crate) use protocol::{InboundRequest, RPCProtocol}; pub use handler::SubstreamId; pub use methods::{ BlocksByRangeRequest, BlocksByRootRequest, GoodbyeReason, MaxRequestBlocks, - RPCResponseErrorCode, RequestId, ResponseTermination, StatusMessage, MAX_REQUEST_BLOCKS, + RPCResponseErrorCode, ResponseTermination, StatusMessage, MAX_REQUEST_BLOCKS, }; pub(crate) use outbound::OutboundRequest; pub use protocol::{max_rpc_size, Protocol, RPCError}; @@ -39,14 +39,18 @@ 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, Clone)] -pub enum RPCSend { +pub enum RPCSend { /// A request sent from Lighthouse. /// - /// The `RequestId` is given by the application making the request. These + /// The `Id` is given by the application making the request. These /// go over *outbound* connections. - Request(RequestId, OutboundRequest), + Request(Id, OutboundRequest), /// A response sent from Lighthouse. /// /// The `SubstreamId` must correspond to the RPC-given ID of the original request received from the @@ -54,12 +58,12 @@ pub enum RPCSend { /// connections. Response(SubstreamId, RPCCodedResponse), /// Lighthouse has requested to terminate the connection with a goodbye message. - Shutdown(GoodbyeReason), + Shutdown(Id, GoodbyeReason), } /// RPC events received from outside Lighthouse. #[derive(Debug, Clone)] -pub enum RPCReceived { +pub enum RPCReceived { /// A request received from the outside. /// /// The `SubstreamId` is given by the `RPCHandler` as it identifies this request with the @@ -67,47 +71,47 @@ pub enum RPCReceived { Request(SubstreamId, InboundRequest), /// A response received from the outside. /// - /// The `RequestId` corresponds to the application given ID of the original request sent to the + /// 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(RequestId, RPCResponse), + Response(Id, RPCResponse), /// Marks a request as completed - EndOfStream(RequestId, ResponseTermination), + EndOfStream(Id, ResponseTermination), } -impl std::fmt::Display for RPCSend { +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(reason) => write!(f, "Sending Goodbye: {}", reason), + RPCSend::Shutdown(_id, reason) => write!(f, "Sending Goodbye: {}", reason), } } } /// Messages sent to the user from the RPC protocol. -pub struct RPCMessage { +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: as ProtocolsHandler>::OutEvent, + pub event: HandlerEvent, } /// Implements the libp2p `NetworkBehaviour` trait and therefore manages network-level /// logic. -pub struct RPC { +pub struct RPC { /// Rate limiter limiter: RateLimiter, /// Queue of events to be processed. - events: Vec, RPCHandler>>, + events: Vec, RPCHandler>>, fork_context: Arc, /// Slog logger for RPC behaviour. log: slog::Logger, } -impl RPC { +impl RPC { pub fn new(fork_context: Arc, log: slog::Logger) -> Self { let log = log.new(o!("service" => "libp2p_rpc")); let limiter = RPCRateLimiterBuilder::new() @@ -150,12 +154,7 @@ impl RPC { /// Submits an RPC request. /// /// The peer must be connected for this to succeed. - pub fn send_request( - &mut self, - peer_id: PeerId, - request_id: RequestId, - event: OutboundRequest, - ) { + pub fn send_request(&mut self, peer_id: PeerId, request_id: Id, event: OutboundRequest) { self.events.push(NetworkBehaviourAction::NotifyHandler { peer_id, handler: NotifyHandler::Any, @@ -165,23 +164,24 @@ impl RPC { /// 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, reason: GoodbyeReason) { + 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(reason), + event: RPCSend::Shutdown(id, reason), }); } } -impl NetworkBehaviour for RPC +impl NetworkBehaviour for RPC where TSpec: EthSpec, + Id: ReqId, { - type ProtocolsHandler = RPCHandler; - type OutEvent = RPCMessage; + type ConnectionHandler = RPCHandler; + type OutEvent = RPCMessage; - fn new_handler(&mut self) -> Self::ProtocolsHandler { + fn new_handler(&mut self) -> Self::ConnectionHandler { RPCHandler::new( SubstreamProtocol::new( RPCProtocol { @@ -196,38 +196,11 @@ where ) } - // handled by discovery - fn addresses_of_peer(&mut self, _peer_id: &PeerId) -> Vec { - Vec::new() - } - - // Use connection established/closed instead of these currently - fn inject_connection_established( - &mut self, - peer_id: &PeerId, - _connection_id: &ConnectionId, - _endpoint: &ConnectedPoint, - _failed_addresses: Option<&Vec>, - other_established: usize, - ) { - if other_established == 0 { - // find the peer's meta-data - debug!(self.log, "Requesting new peer's metadata"; "peer_id" => %peer_id); - let rpc_event = - RPCSend::Request(RequestId::Behaviour, OutboundRequest::MetaData(PhantomData)); - self.events.push(NetworkBehaviourAction::NotifyHandler { - peer_id: *peer_id, - handler: NotifyHandler::Any, - event: rpc_event, - }); - } - } - fn inject_event( &mut self, peer_id: PeerId, conn_id: ConnectionId, - event: ::OutEvent, + event: ::OutEvent, ) { if let Ok(RPCReceived::Request(ref id, ref req)) = event { // check if the request is conformant to the quota @@ -289,7 +262,7 @@ where &mut self, cx: &mut Context, _: &mut impl PollParameters, - ) -> Poll> { + ) -> Poll> { // let the rate limiter prune let _ = self.limiter.poll_unpin(cx); if !self.events.is_empty() { diff --git a/beacon_node/lighthouse_network/src/service.rs b/beacon_node/lighthouse_network/src/service.rs index 7dcee51870..bcd546fb00 100644 --- a/beacon_node/lighthouse_network/src/service.rs +++ b/beacon_node/lighthouse_network/src/service.rs @@ -4,21 +4,18 @@ use crate::behaviour::{ use crate::config::NetworkLoad; use crate::discovery::enr; use crate::multiaddr::Protocol; -use crate::rpc::{ - GoodbyeReason, MetaData, MetaDataV1, MetaDataV2, RPCResponseErrorCode, RequestId, -}; +use crate::rpc::{GoodbyeReason, MetaData, MetaDataV1, MetaDataV2, RPCResponseErrorCode, ReqId}; use crate::types::{error, EnrAttestationBitfield, EnrSyncCommitteeBitfield, GossipKind}; use crate::EnrExt; use crate::{NetworkConfig, NetworkGlobals, PeerAction, ReportSource}; use futures::prelude::*; use libp2p::core::{ - connection::ConnectionLimits, identity::Keypair, multiaddr::Multiaddr, muxing::StreamMuxerBox, - transport::Boxed, + identity::Keypair, multiaddr::Multiaddr, muxing::StreamMuxerBox, transport::Boxed, }; use libp2p::{ bandwidth::{BandwidthLogging, BandwidthSinks}, core, noise, - swarm::{SwarmBuilder, SwarmEvent}, + swarm::{ConnectionLimits, SwarmBuilder, SwarmEvent}, PeerId, Swarm, Transport, }; use prometheus_client::registry::Registry; @@ -43,9 +40,9 @@ pub const METADATA_FILENAME: &str = "metadata"; /// /// This is a subset of the events that a libp2p swarm emits. #[derive(Debug)] -pub enum Libp2pEvent { +pub enum Libp2pEvent { /// A behaviour event - Behaviour(BehaviourEvent), + Behaviour(BehaviourEvent), /// A new listening address has been established. NewListenAddr(Multiaddr), /// We reached zero listening addresses. @@ -53,9 +50,9 @@ pub enum Libp2pEvent { } /// The configuration and state of the libp2p components for the beacon node. -pub struct Service { +pub struct Service { /// The libp2p Swarm handler. - pub swarm: Swarm>, + pub swarm: Swarm>, /// The bandwidth logger for the underlying libp2p transport. pub bandwidth: Arc, /// This node's PeerId. @@ -72,7 +69,7 @@ pub struct Context<'a> { pub gossipsub_registry: Option<&'a mut Registry>, } -impl Service { +impl Service { pub async fn new( executor: task_executor::TaskExecutor, ctx: Context<'_>, @@ -261,7 +258,7 @@ impl Service { } /// Sends a request to a peer, with a given Id. - pub fn send_request(&mut self, peer_id: PeerId, request_id: RequestId, request: Request) { + 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); @@ -308,7 +305,7 @@ impl Service { .send_successful_response(peer_id, id, response); } - pub async fn next_event(&mut self) -> Libp2pEvent { + pub async fn next_event(&mut self) -> Libp2pEvent { loop { match self.swarm.select_next_some().await { SwarmEvent::Behaviour(behaviour) => { diff --git a/beacon_node/lighthouse_network/tests/common/behaviour.rs b/beacon_node/lighthouse_network/tests/common/behaviour.rs index 75797e63d1..76eecfcbc5 100644 --- a/beacon_node/lighthouse_network/tests/common/behaviour.rs +++ b/beacon_node/lighthouse_network/tests/common/behaviour.rs @@ -24,9 +24,7 @@ use std::collections::HashMap; use std::task::{Context, Poll}; use libp2p::core::connection::{ConnectedPoint, ConnectionId, ListenerId}; -use libp2p::swarm::protocols_handler::{ - DummyProtocolsHandler, IntoProtocolsHandler, ProtocolsHandler, -}; +use libp2p::swarm::handler::{ConnectionHandler, DummyConnectionHandler, IntoConnectionHandler}; use libp2p::swarm::{DialError, NetworkBehaviour, NetworkBehaviourAction, PollParameters}; use libp2p::{Multiaddr, PeerId}; @@ -34,10 +32,10 @@ use libp2p::{Multiaddr, PeerId}; /// the instrumentation of return values, without keeping /// any further state. pub struct MockBehaviour< - THandler = DummyProtocolsHandler, - TOutEvent = ::OutEvent, + THandler = DummyConnectionHandler, + TOutEvent = ::OutEvent, > where - THandler: ProtocolsHandler, + THandler: ConnectionHandler, { /// The prototype protocols handler that is cloned for every /// invocation of `new_handler`. @@ -52,7 +50,7 @@ pub struct MockBehaviour< impl MockBehaviour where - THandler: ProtocolsHandler, + THandler: ConnectionHandler, { pub fn new(handler_proto: THandler) -> Self { MockBehaviour { @@ -65,14 +63,14 @@ where impl NetworkBehaviour for MockBehaviour where - THandler: ProtocolsHandler + Clone, + THandler: ConnectionHandler + Clone, THandler::OutEvent: Clone, TOutEvent: Send + 'static, { - type ProtocolsHandler = THandler; + type ConnectionHandler = THandler; type OutEvent = TOutEvent; - fn new_handler(&mut self) -> Self::ProtocolsHandler { + fn new_handler(&mut self) -> Self::ConnectionHandler { self.handler_proto.clone() } @@ -86,7 +84,7 @@ where &mut self, _: &mut Context, _: &mut impl PollParameters, - ) -> Poll> { + ) -> Poll> { Option::take(&mut self.next_action).map_or(Poll::Pending, Poll::Ready) } } @@ -105,7 +103,7 @@ where pub inject_event: Vec<( PeerId, ConnectionId, - <::Handler as ProtocolsHandler>::OutEvent, + <::Handler as ConnectionHandler>::OutEvent, )>, pub inject_dial_failure: Vec>, pub inject_new_listener: Vec, @@ -212,13 +210,13 @@ where impl NetworkBehaviour for CallTraceBehaviour where TInner: NetworkBehaviour, - <::Handler as ProtocolsHandler>::OutEvent: + <::Handler as ConnectionHandler>::OutEvent: Clone, { - type ProtocolsHandler = TInner::ProtocolsHandler; + type ConnectionHandler = TInner::ConnectionHandler; type OutEvent = TInner::OutEvent; - fn new_handler(&mut self) -> Self::ProtocolsHandler { + fn new_handler(&mut self) -> Self::ConnectionHandler { self.inner.new_handler() } @@ -273,7 +271,7 @@ where p: &PeerId, c: &ConnectionId, e: &ConnectedPoint, - handler: ::Handler, + handler: ::Handler, remaining_established: usize, ) { let mut other_closed_connections = self @@ -320,7 +318,7 @@ where &mut self, p: PeerId, c: ConnectionId, - e: <::Handler as ProtocolsHandler>::OutEvent, + e: <::Handler as ConnectionHandler>::OutEvent, ) { assert!( self.inject_connection_established @@ -343,7 +341,7 @@ where fn inject_dial_failure( &mut self, p: Option, - handler: Self::ProtocolsHandler, + handler: Self::ConnectionHandler, error: &DialError, ) { self.inject_dial_failure.push(p); @@ -389,7 +387,7 @@ where &mut self, cx: &mut Context, args: &mut impl PollParameters, - ) -> Poll> { + ) -> Poll> { self.poll += 1; self.inner.poll(cx, args) } diff --git a/beacon_node/lighthouse_network/tests/common/mod.rs b/beacon_node/lighthouse_network/tests/common/mod.rs index 5656cf0789..e79fdf464d 100644 --- a/beacon_node/lighthouse_network/tests/common/mod.rs +++ b/beacon_node/lighthouse_network/tests/common/mod.rs @@ -21,6 +21,8 @@ pub mod behaviour; pub mod swarm; type E = MinimalEthSpec; +type ReqId = usize; + use tempfile::Builder as TempBuilder; /// Returns a dummy fork context @@ -33,10 +35,10 @@ pub fn fork_context() -> ForkContext { ForkContext::new::(types::Slot::new(0), Hash256::zero(), &chain_spec) } -pub struct Libp2pInstance(LibP2PService, exit_future::Signal); +pub struct Libp2pInstance(LibP2PService, exit_future::Signal); impl std::ops::Deref for Libp2pInstance { - type Target = LibP2PService; + type Target = LibP2PService; fn deref(&self) -> &Self::Target { &self.0 } @@ -113,7 +115,7 @@ pub async fn build_libp2p_instance( } #[allow(dead_code)] -pub fn get_enr(node: &LibP2PService) -> Enr { +pub fn get_enr(node: &LibP2PService) -> Enr { node.swarm.behaviour().local_enr() } diff --git a/beacon_node/lighthouse_network/tests/common/swarm.rs b/beacon_node/lighthouse_network/tests/common/swarm.rs index 2930e2e4da..aa41a5c066 100644 --- a/beacon_node/lighthouse_network/tests/common/swarm.rs +++ b/beacon_node/lighthouse_network/tests/common/swarm.rs @@ -5,8 +5,8 @@ use super::behaviour::{CallTraceBehaviour, MockBehaviour}; use futures::stream::Stream; use futures::task::{Context, Poll}; -use libp2p::swarm::protocols_handler::ProtocolsHandler; -use libp2p::swarm::{IntoProtocolsHandler, NetworkBehaviour, Swarm, SwarmBuilder, SwarmEvent}; +use libp2p::swarm::handler::ConnectionHandler; +use libp2p::swarm::{IntoConnectionHandler, NetworkBehaviour, Swarm, SwarmBuilder, SwarmEvent}; use libp2p::{PeerId, Transport}; use futures::StreamExt; @@ -82,10 +82,10 @@ impl SwarmPool { impl Stream for SwarmPool where B: NetworkBehaviour, - ::ProtocolsHandler: ProtocolsHandler, + ::ConnectionHandler: ConnectionHandler, { type Item = (PeerId, - SwarmEvent<::OutEvent, <<::ProtocolsHandler as IntoProtocolsHandler>::Handler as ProtocolsHandler>::Error>); + SwarmEvent<::OutEvent, <<::ConnectionHandler as IntoConnectionHandler>::Handler as ConnectionHandler>::Error>); fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let mut polls = self diff --git a/beacon_node/lighthouse_network/tests/pm_tests.rs b/beacon_node/lighthouse_network/tests/pm_tests.rs index 9b26e4939f..96767204db 100644 --- a/beacon_node/lighthouse_network/tests/pm_tests.rs +++ b/beacon_node/lighthouse_network/tests/pm_tests.rs @@ -20,7 +20,7 @@ use futures::StreamExt; use libp2p::{ core::either::EitherError, swarm::SwarmEvent, - swarm::{protocols_handler::DummyProtocolsHandler, DummyBehaviour, KeepAlive, Swarm}, + swarm::{handler::DummyConnectionHandler, DummyBehaviour, KeepAlive, Swarm}, NetworkBehaviour, }; @@ -77,7 +77,7 @@ impl Behaviour { fn new(pm: PeerManager) -> Self { Behaviour { pm_call_trace: CallTraceBehaviour::new(pm), - sibling: MockBehaviour::new(DummyProtocolsHandler { + sibling: MockBehaviour::new(DummyConnectionHandler { // The peer manager votes No, so we make sure the combined handler stays alive this // way. keep_alive: KeepAlive::Yes, diff --git a/beacon_node/lighthouse_network/tests/rpc_tests.rs b/beacon_node/lighthouse_network/tests/rpc_tests.rs index b270765f8c..6f32e65263 100644 --- a/beacon_node/lighthouse_network/tests/rpc_tests.rs +++ b/beacon_node/lighthouse_network/tests/rpc_tests.rs @@ -88,15 +88,14 @@ fn test_status_rpc() { Libp2pEvent::Behaviour(BehaviourEvent::PeerConnectedOutgoing(peer_id)) => { // Send a STATUS message debug!(log, "Sending RPC"); - sender.swarm.behaviour_mut().send_request( - peer_id, - RequestId::Sync(10), - rpc_request.clone(), - ); + sender + .swarm + .behaviour_mut() + .send_request(peer_id, 10, rpc_request.clone()); } Libp2pEvent::Behaviour(BehaviourEvent::ResponseReceived { peer_id: _, - id: RequestId::Sync(10), + id: 10, response, }) => { // Should receive the RPC response @@ -186,7 +185,7 @@ fn test_blocks_by_range_chunked_rpc() { // keep count of the number of messages received let mut messages_received = 0; - let request_id = RequestId::Sync(messages_to_send as usize); + let request_id = messages_to_send as usize; // build the sender future let sender_future = async { loop { @@ -313,7 +312,7 @@ fn test_blocks_by_range_over_limit() { let signed_full_block = SignedBeaconBlock::from_block(full_block, Signature::empty()); let rpc_response_merge_large = Response::BlocksByRange(Some(Box::new(signed_full_block))); - let request_id = RequestId::Sync(messages_to_send as usize); + let request_id = messages_to_send as usize; // build the sender future let sender_future = async { loop { @@ -413,7 +412,7 @@ fn test_blocks_by_range_chunked_rpc_terminates_correctly() { // keep count of the number of messages received let mut messages_received: u64 = 0; - let request_id = RequestId::Sync(messages_to_send as usize); + let request_id = messages_to_send as usize; // build the sender future let sender_future = async { loop { @@ -553,15 +552,14 @@ fn test_blocks_by_range_single_empty_rpc() { Libp2pEvent::Behaviour(BehaviourEvent::PeerConnectedOutgoing(peer_id)) => { // Send a STATUS message debug!(log, "Sending RPC"); - sender.swarm.behaviour_mut().send_request( - peer_id, - RequestId::Sync(10), - rpc_request.clone(), - ); + sender + .swarm + .behaviour_mut() + .send_request(peer_id, 10, rpc_request.clone()); } Libp2pEvent::Behaviour(BehaviourEvent::ResponseReceived { peer_id: _, - id: RequestId::Sync(10), + id: 10, response, }) => match response { Response::BlocksByRange(Some(_)) => { @@ -679,15 +677,14 @@ fn test_blocks_by_root_chunked_rpc() { Libp2pEvent::Behaviour(BehaviourEvent::PeerConnectedOutgoing(peer_id)) => { // Send a STATUS message debug!(log, "Sending RPC"); - sender.swarm.behaviour_mut().send_request( - peer_id, - RequestId::Sync(6), - rpc_request.clone(), - ); + sender + .swarm + .behaviour_mut() + .send_request(peer_id, 6, rpc_request.clone()); } Libp2pEvent::Behaviour(BehaviourEvent::ResponseReceived { peer_id: _, - id: RequestId::Sync(6), + id: 6, response, }) => match response { Response::BlocksByRoot(Some(_)) => { @@ -814,15 +811,14 @@ fn test_blocks_by_root_chunked_rpc_terminates_correctly() { Libp2pEvent::Behaviour(BehaviourEvent::PeerConnectedOutgoing(peer_id)) => { // Send a STATUS message debug!(log, "Sending RPC"); - sender.swarm.behaviour_mut().send_request( - peer_id, - RequestId::Sync(10), - rpc_request.clone(), - ); + sender + .swarm + .behaviour_mut() + .send_request(peer_id, 10, rpc_request.clone()); } Libp2pEvent::Behaviour(BehaviourEvent::ResponseReceived { peer_id: _, - id: RequestId::Sync(10), + id: 10, response, }) => { debug!(log, "Sender received a response"); diff --git a/beacon_node/network/Cargo.toml b/beacon_node/network/Cargo.toml index 1c7506483e..96458da0a8 100644 --- a/beacon_node/network/Cargo.toml +++ b/beacon_node/network/Cargo.toml @@ -11,7 +11,6 @@ matches = "0.1.8" exit-future = "0.2.0" slog-term = "2.6.0" slog-async = "2.5.0" -logging = { path = "../../common/logging" } environment = { path = "../../lighthouse/environment" } [dependencies] @@ -35,6 +34,7 @@ fnv = "1.0.7" rlp = "0.5.0" lazy_static = "1.4.0" lighthouse_metrics = { path = "../../common/lighthouse_metrics" } +logging = { path = "../../common/logging" } task_executor = { path = "../../common/task_executor" } igd = "0.11.1" itertools = "0.10.0" diff --git a/beacon_node/network/src/beacon_processor/mod.rs b/beacon_node/network/src/beacon_processor/mod.rs index 7c3d482fa5..eb40be960d 100644 --- a/beacon_node/network/src/beacon_processor/mod.rs +++ b/beacon_node/network/src/beacon_processor/mod.rs @@ -38,25 +38,27 @@ //! checks the queues to see if there are more parcels of work that can be spawned in a new worker //! task. +use crate::sync::manager::BlockProcessType; use crate::{metrics, service::NetworkMessage, sync::SyncMessage}; use beacon_chain::parking_lot::Mutex; -use beacon_chain::{BeaconChain, BeaconChainTypes, BlockError, GossipVerifiedBlock}; +use beacon_chain::{BeaconChain, BeaconChainTypes, GossipVerifiedBlock}; use futures::stream::{Stream, StreamExt}; use futures::task::Poll; use lighthouse_network::{ rpc::{BlocksByRangeRequest, BlocksByRootRequest, StatusMessage}, Client, MessageId, NetworkGlobals, PeerId, PeerRequestId, }; +use logging::TimeLatch; use slog::{crit, debug, error, trace, warn, Logger}; use std::collections::VecDeque; use std::fmt; use std::pin::Pin; use std::sync::{Arc, Weak}; use std::task::Context; -use std::time::{Duration, Instant}; +use std::time::Duration; use std::{cmp, collections::HashSet}; use task_executor::TaskExecutor; -use tokio::sync::{mpsc, oneshot}; +use tokio::sync::mpsc; use types::{ Attestation, AttesterSlashing, Hash256, ProposerSlashing, SignedAggregateAndProof, SignedBeaconBlock, SignedContributionAndProof, SignedVoluntaryExit, SubnetId, @@ -73,7 +75,7 @@ mod work_reprocessing_queue; mod worker; use crate::beacon_processor::work_reprocessing_queue::QueuedBlock; -pub use worker::{GossipAggregatePackage, GossipAttestationPackage, ProcessId}; +pub use worker::{ChainSegmentProcessId, GossipAggregatePackage, GossipAttestationPackage}; /// The maximum size of the channel for work events to the `BeaconProcessor`. /// @@ -159,9 +161,6 @@ const MANAGER_TASK_NAME: &str = "beacon_processor_manager"; /// The name of the worker tokio tasks. const WORKER_TASK_NAME: &str = "beacon_processor_worker"; -/// The minimum interval between log messages indicating that a queue is full. -const LOG_DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30); - /// The `MAX_..._BATCH_SIZE` variables define how many attestations can be included in a single /// batch. /// @@ -198,10 +197,6 @@ pub const BLOCKS_BY_ROOTS_REQUEST: &str = "blocks_by_roots_request"; pub const UNKNOWN_BLOCK_ATTESTATION: &str = "unknown_block_attestation"; pub const UNKNOWN_BLOCK_AGGREGATE: &str = "unknown_block_aggregate"; -/// Used to send/receive results from a rpc block import in a blocking task. -pub type BlockResultSender = oneshot::Sender>>; -pub type BlockResultReceiver = oneshot::Receiver>>; - /// A simple first-in-first-out queue with a maximum length. struct FifoQueue { queue: VecDeque, @@ -498,18 +493,22 @@ impl WorkEvent { /// sent to the other side of `result_tx`. pub fn rpc_beacon_block( block: Box>, - ) -> (Self, BlockResultReceiver) { - let (result_tx, result_rx) = oneshot::channel(); - let event = Self { + seen_timestamp: Duration, + process_type: BlockProcessType, + ) -> Self { + Self { drop_during_sync: false, - work: Work::RpcBlock { block, result_tx }, - }; - (event, result_rx) + work: Work::RpcBlock { + block, + seen_timestamp, + process_type, + }, + } } /// Create a new work event to import `blocks` as a beacon chain segment. pub fn chain_segment( - process_id: ProcessId, + process_id: ChainSegmentProcessId, blocks: Vec>, ) -> Self { Self { @@ -694,10 +693,11 @@ pub enum Work { }, RpcBlock { block: Box>, - result_tx: BlockResultSender, + seen_timestamp: Duration, + process_type: BlockProcessType, }, ChainSegment { - process_id: ProcessId, + process_id: ChainSegmentProcessId, blocks: Vec>, }, Status { @@ -742,25 +742,6 @@ impl Work { } } -/// Provides de-bounce functionality for logging. -#[derive(Default)] -struct TimeLatch(Option); - -impl TimeLatch { - /// Only returns true once every `LOG_DEBOUNCE_INTERVAL`. - fn elapsed(&mut self) -> bool { - let now = Instant::now(); - - let is_elapsed = self.0.map_or(false, |elapse_time| now > elapse_time); - - if is_elapsed || self.0.is_none() { - self.0 = Some(now + LOG_DEBOUNCE_INTERVAL); - } - - is_elapsed - } -} - /// Unifies all the messages processed by the `BeaconProcessor`. enum InboundEvent { /// A worker has completed a task and is free. @@ -1509,10 +1490,15 @@ impl BeaconProcessor { /* * Verification for beacon blocks received during syncing via RPC. */ - Work::RpcBlock { block, result_tx } => { + Work::RpcBlock { + block, + seen_timestamp, + process_type, + } => { worker.process_rpc_block( *block, - result_tx, + seen_timestamp, + process_type, work_reprocessing_tx.clone(), duplicate_cache, ); diff --git a/beacon_node/network/src/beacon_processor/tests.rs b/beacon_node/network/src/beacon_processor/tests.rs index 7c060472bb..0f97bc7944 100644 --- a/beacon_node/network/src/beacon_processor/tests.rs +++ b/beacon_node/network/src/beacon_processor/tests.rs @@ -240,7 +240,13 @@ impl TestRig { } pub fn enqueue_rpc_block(&self) { - let (event, _rx) = WorkEvent::rpc_beacon_block(Box::new(self.next_block.clone())); + let event = WorkEvent::rpc_beacon_block( + Box::new(self.next_block.clone()), + std::time::Duration::default(), + BlockProcessType::ParentLookup { + chain_hash: Hash256::random(), + }, + ); self.beacon_processor_tx.try_send(event).unwrap(); } diff --git a/beacon_node/network/src/beacon_processor/work_reprocessing_queue.rs b/beacon_node/network/src/beacon_processor/work_reprocessing_queue.rs index 299e71c8d5..33c15cf06b 100644 --- a/beacon_node/network/src/beacon_processor/work_reprocessing_queue.rs +++ b/beacon_node/network/src/beacon_processor/work_reprocessing_queue.rs @@ -17,6 +17,7 @@ use fnv::FnvHashMap; use futures::task::Poll; use futures::{Stream, StreamExt}; use lighthouse_network::{MessageId, PeerId}; +use logging::TimeLatch; use slog::{crit, debug, error, warn, Logger}; use slot_clock::SlotClock; use std::collections::{HashMap, HashSet}; @@ -133,6 +134,8 @@ struct ReprocessQueue { /* Aux */ /// Next attestation id, used for both aggregated and unaggregated attestations next_attestation: usize, + early_block_debounce: TimeLatch, + attestation_delay_debounce: TimeLatch, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -223,6 +226,8 @@ pub fn spawn_reprocess_scheduler( queued_unaggregates: FnvHashMap::default(), awaiting_attestations_per_root: HashMap::new(), next_attestation: 0, + early_block_debounce: TimeLatch::default(), + attestation_delay_debounce: TimeLatch::default(), }; executor.spawn( @@ -261,12 +266,14 @@ impl ReprocessQueue { if let Some(duration_till_slot) = slot_clock.duration_to_slot(block_slot) { // Check to ensure this won't over-fill the queue. if self.queued_block_roots.len() >= MAXIMUM_QUEUED_BLOCKS { - warn!( - log, - "Early blocks queue is full"; - "queue_size" => MAXIMUM_QUEUED_BLOCKS, - "msg" => "check system clock" - ); + if self.early_block_debounce.elapsed() { + warn!( + log, + "Early blocks queue is full"; + "queue_size" => MAXIMUM_QUEUED_BLOCKS, + "msg" => "check system clock" + ); + } // Drop the block. return; } @@ -306,12 +313,14 @@ impl ReprocessQueue { } InboundEvent::Msg(UnknownBlockAggregate(queued_aggregate)) => { if self.attestations_delay_queue.len() >= MAXIMUM_QUEUED_ATTESTATIONS { - error!( - log, - "Aggregate attestation delay queue is full"; - "queue_size" => MAXIMUM_QUEUED_ATTESTATIONS, - "msg" => "check system clock" - ); + if self.attestation_delay_debounce.elapsed() { + error!( + log, + "Aggregate attestation delay queue is full"; + "queue_size" => MAXIMUM_QUEUED_ATTESTATIONS, + "msg" => "check system clock" + ); + } // Drop the attestation. return; } @@ -337,12 +346,14 @@ impl ReprocessQueue { } InboundEvent::Msg(UnknownBlockUnaggregate(queued_unaggregate)) => { if self.attestations_delay_queue.len() >= MAXIMUM_QUEUED_ATTESTATIONS { - error!( - log, - "Attestation delay queue is full"; - "queue_size" => MAXIMUM_QUEUED_ATTESTATIONS, - "msg" => "check system clock" - ); + if self.attestation_delay_debounce.elapsed() { + error!( + log, + "Attestation delay queue is full"; + "queue_size" => MAXIMUM_QUEUED_ATTESTATIONS, + "msg" => "check system clock" + ); + } // Drop the attestation. return; } diff --git a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs b/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs index 72cb3a7ee1..b367f7f6d2 100644 --- a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs +++ b/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs @@ -772,6 +772,7 @@ impl Worker { } // TODO(merge): reconsider peer scoring for this event. Err(e @BlockError::ExecutionPayloadError(ExecutionPayloadError::RequestFailed(_))) + | Err(e @ BlockError::ExecutionPayloadError(ExecutionPayloadError::UnverifiedNonOptimisticCandidate)) | Err(e @BlockError::ExecutionPayloadError(ExecutionPayloadError::NoExecutionConnection)) => { debug!(self.log, "Could not verify block for gossip, ignoring the block"; "error" => %e); @@ -1556,7 +1557,7 @@ impl Worker { /* * The block indicated by the target root is not known to us. * - * We should always get `AttnError::UnknwonHeadBlock` before we get this + * We should always get `AttnError::UnknownHeadBlock` before we get this * error, so this means we can get this error if: * * 1. The target root does not represent a valid block. @@ -1565,7 +1566,7 @@ impl Worker { * For (2), we should only be processing attestations when we should have * all the available information. Note: if we do a weak-subjectivity sync * it's possible that this situation could occur, but I think it's - * unlikely. For now, we will declare this to be an invalid message> + * unlikely. For now, we will declare this to be an invalid message. * * The peer has published an invalid consensus message. */ @@ -1712,14 +1713,12 @@ impl Worker { AttnError::HeadBlockFinalized { beacon_block_root } => { debug!( self.log, - "Rejected attestation to finalized block"; + "Ignored attestation to finalized block"; "block_root" => ?beacon_block_root, "attestation_slot" => failed_att.attestation().data.slot, ); - // We have to reject the message as it isn't a descendant of the finalized - // checkpoint. - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); // The peer that sent us this could be a lagger, or a spammer, or this failure could // be due to us processing attestations extremely slowly. Don't be too harsh. diff --git a/beacon_node/network/src/beacon_processor/worker/mod.rs b/beacon_node/network/src/beacon_processor/worker/mod.rs index b9d78900b2..f907c49b7d 100644 --- a/beacon_node/network/src/beacon_processor/worker/mod.rs +++ b/beacon_node/network/src/beacon_processor/worker/mod.rs @@ -10,7 +10,7 @@ mod rpc_methods; mod sync_methods; pub use gossip_methods::{GossipAggregatePackage, GossipAttestationPackage}; -pub use sync_methods::ProcessId; +pub use sync_methods::ChainSegmentProcessId; pub(crate) const FUTURE_SLOT_TOLERANCE: u64 = 1; diff --git a/beacon_node/network/src/beacon_processor/worker/sync_methods.rs b/beacon_node/network/src/beacon_processor/worker/sync_methods.rs index 27e0a6711d..082808f88e 100644 --- a/beacon_node/network/src/beacon_processor/worker/sync_methods.rs +++ b/beacon_node/network/src/beacon_processor/worker/sync_methods.rs @@ -1,26 +1,28 @@ +use std::time::Duration; + use super::{super::work_reprocessing_queue::ReprocessQueueMessage, Worker}; use crate::beacon_processor::worker::FUTURE_SLOT_TOLERANCE; -use crate::beacon_processor::{BlockResultSender, DuplicateCache}; +use crate::beacon_processor::DuplicateCache; use crate::metrics; -use crate::sync::manager::{SyncMessage, SyncRequestType}; +use crate::sync::manager::{BlockProcessType, SyncMessage}; use crate::sync::{BatchProcessResult, ChainId}; use beacon_chain::{ BeaconChainError, BeaconChainTypes, BlockError, ChainSegmentResult, HistoricalBlockError, }; -use lighthouse_network::{PeerAction, PeerId}; -use slog::{crit, debug, error, info, trace, warn}; +use lighthouse_network::PeerAction; +use slog::{debug, error, info, trace, warn}; use tokio::sync::mpsc; use types::{Epoch, Hash256, SignedBeaconBlock}; -/// Id associated to a block processing request, either a batch or a single block. +/// Id associated to a batch processing request, either a sync batch or a parent lookup. #[derive(Clone, Debug, PartialEq)] -pub enum ProcessId { +pub enum ChainSegmentProcessId { /// Processing Id of a range syncing batch. RangeBatchId(ChainId, Epoch), /// Processing ID for a backfill syncing batch. BackSyncBatchId(Epoch), /// Processing Id of the parent lookup of a block. - ParentLookup(PeerId, Hash256), + ParentLookup(Hash256), } /// Returned when a chain segment import fails. @@ -32,88 +34,77 @@ struct ChainSegmentFailed { } impl Worker { - /// Attempt to process a block received from a direct RPC request, returning the processing - /// result on the `result_tx` channel. - /// - /// Raises a log if there are errors publishing the result to the channel. + /// Attempt to process a block received from a direct RPC request. pub fn process_rpc_block( self, block: SignedBeaconBlock, - result_tx: BlockResultSender, + seen_timestamp: Duration, + process_type: BlockProcessType, reprocess_tx: mpsc::Sender>, duplicate_cache: DuplicateCache, ) { - let block_root = block.canonical_root(); - // Checks if the block is already being imported through another source - if let Some(handle) = duplicate_cache.check_and_insert(block_root) { - let slot = block.slot(); - let block_result = self.chain.process_block(block); + // Check if the block is already being imported through another source + let handle = match duplicate_cache.check_and_insert(block.canonical_root()) { + Some(handle) => handle, + None => { + // Sync handles these results + self.send_sync_message(SyncMessage::BlockProcessed { + process_type, + result: Err(BlockError::BlockIsAlreadyKnown), + }); + return; + } + }; + let slot = block.slot(); + let result = self.chain.process_block(block); - metrics::inc_counter(&metrics::BEACON_PROCESSOR_RPC_BLOCK_IMPORTED_TOTAL); + metrics::inc_counter(&metrics::BEACON_PROCESSOR_RPC_BLOCK_IMPORTED_TOTAL); - if let Ok(root) = &block_result { - info!( - self.log, - "New RPC block received"; - "slot" => slot, - "hash" => %root + // RPC block imported, regardless of process type + if let &Ok(hash) = &result { + info!(self.log, "New RPC block received"; "slot" => slot, "hash" => %hash); + + // Trigger processing for work referencing this block. + let reprocess_msg = ReprocessQueueMessage::BlockImported(hash); + if reprocess_tx.try_send(reprocess_msg).is_err() { + error!(self.log, "Failed to inform block import"; "source" => "rpc", "block_root" => %hash) + }; + if matches!(process_type, BlockProcessType::SingleBlock { .. }) { + self.chain.block_times_cache.write().set_time_observed( + hash, + slot, + seen_timestamp, + None, + None, ); - - if reprocess_tx - .try_send(ReprocessQueueMessage::BlockImported(*root)) - .is_err() - { - error!( - self.log, - "Failed to inform block import"; - "source" => "rpc", - "block_root" => %root, - ) - }; - } - - if result_tx.send(block_result).is_err() { - crit!(self.log, "Failed return sync block result"); - } - // Drop the handle to remove the entry from the cache - drop(handle); - } else { - debug!( - self.log, - "Gossip block is being imported"; - "block_root" => %block_root, - ); - // The gossip block that is being imported should eventually - // trigger reprocessing of queued attestations once it is imported. - // If the gossip block fails import, then it will be downscored - // appropriately in `process_gossip_block`. - - // Here, we assume that the block will eventually be imported and - // send a `BlockIsAlreadyKnown` message to sync. - if result_tx - .send(Err(BlockError::BlockIsAlreadyKnown)) - .is_err() - { - crit!(self.log, "Failed return sync block result"); + self.run_fork_choice() } } + // Sync handles these results + self.send_sync_message(SyncMessage::BlockProcessed { + process_type, + result: result.map(|_| ()), + }); + + // Drop the handle to remove the entry from the cache + drop(handle); } /// Attempt to import the chain segment (`blocks`) to the beacon chain, informing the sync /// thread if more blocks are needed to process it. pub fn process_chain_segment( &self, - process_id: ProcessId, + sync_type: ChainSegmentProcessId, downloaded_blocks: Vec>, ) { - match process_id { + let result = match sync_type { // this a request from the range sync - ProcessId::RangeBatchId(chain_id, epoch) => { + ChainSegmentProcessId::RangeBatchId(chain_id, epoch) => { let start_slot = downloaded_blocks.first().map(|b| b.slot().as_u64()); let end_slot = downloaded_blocks.last().map(|b| b.slot().as_u64()); let sent_blocks = downloaded_blocks.len(); - let result = match self.process_blocks(downloaded_blocks.iter()) { + match self.process_blocks(downloaded_blocks.iter()) { (_, Ok(_)) => { debug!(self.log, "Batch processed"; "batch_epoch" => epoch, @@ -139,19 +130,15 @@ impl Worker { peer_action: e.peer_action, } } - }; - - let sync_type = SyncRequestType::RangeSync(epoch, chain_id); - - self.send_sync_message(SyncMessage::BatchProcessed { sync_type, result }); + } } // this a request from the Backfill sync - ProcessId::BackSyncBatchId(epoch) => { + ChainSegmentProcessId::BackSyncBatchId(epoch) => { let start_slot = downloaded_blocks.first().map(|b| b.slot().as_u64()); let end_slot = downloaded_blocks.last().map(|b| b.slot().as_u64()); let sent_blocks = downloaded_blocks.len(); - let result = match self.process_backfill_blocks(&downloaded_blocks) { + match self.process_backfill_blocks(&downloaded_blocks) { (_, Ok(_)) => { debug!(self.log, "Backfill batch processed"; "batch_epoch" => epoch, @@ -173,35 +160,34 @@ impl Worker { peer_action: e.peer_action, } } - }; - - let sync_type = SyncRequestType::BackFillSync(epoch); - - self.send_sync_message(SyncMessage::BatchProcessed { sync_type, result }); + } } // this is a parent lookup request from the sync manager - ProcessId::ParentLookup(peer_id, chain_head) => { + ChainSegmentProcessId::ParentLookup(chain_head) => { debug!( self.log, "Processing parent lookup"; - "last_peer_id" => %peer_id, + "chain_hash" => %chain_head, "blocks" => downloaded_blocks.len() ); // parent blocks are ordered from highest slot to lowest, so we need to process in // reverse match self.process_blocks(downloaded_blocks.iter().rev()) { - (_, Err(e)) => { - debug!(self.log, "Parent lookup failed"; "last_peer_id" => %peer_id, "error" => %e.message); - self.send_sync_message(SyncMessage::ParentLookupFailed { - peer_id, - chain_head, - }) + (imported_blocks, Err(e)) => { + debug!(self.log, "Parent lookup failed"; "error" => %e.message); + BatchProcessResult::Failed { + imported_blocks: imported_blocks > 0, + peer_action: e.peer_action, + } } - (_, Ok(_)) => { + (imported_blocks, Ok(_)) => { debug!(self.log, "Parent lookup processed successfully"); + BatchProcessResult::Success(imported_blocks > 0) } } } - } + }; + + self.send_sync_message(SyncMessage::BatchProcessed { sync_type, result }); } /// Helper function to process blocks batches which only consumes the chain and blocks to process. diff --git a/beacon_node/network/src/metrics.rs b/beacon_node/network/src/metrics.rs index 446aa0a033..04aa514721 100644 --- a/beacon_node/network/src/metrics.rs +++ b/beacon_node/network/src/metrics.rs @@ -286,6 +286,14 @@ lazy_static! { "Number of Syncing chains in range, per range type", &["range_type"] ); + pub static ref SYNC_SINGLE_BLOCK_LOOKUPS: Result = try_create_int_gauge( + "sync_single_block_lookups", + "Number of single block lookups underway" + ); + pub static ref SYNC_PARENT_BLOCK_LOOKUPS: Result = try_create_int_gauge( + "sync_parent_block_lookups", + "Number of parent block lookups underway" + ); /* * Block Delay Metrics diff --git a/beacon_node/network/src/router/mod.rs b/beacon_node/network/src/router/mod.rs index 8d639c5ee6..03b877506f 100644 --- a/beacon_node/network/src/router/mod.rs +++ b/beacon_node/network/src/router/mod.rs @@ -8,12 +8,11 @@ mod processor; use crate::error; -use crate::service::NetworkMessage; +use crate::service::{NetworkMessage, RequestId}; use beacon_chain::{BeaconChain, BeaconChainTypes}; use futures::prelude::*; use lighthouse_network::{ - rpc::RequestId, MessageId, NetworkGlobals, PeerId, PeerRequestId, PubsubMessage, Request, - Response, + MessageId, NetworkGlobals, PeerId, PeerRequestId, PubsubMessage, Request, Response, }; use processor::Processor; use slog::{debug, o, trace}; diff --git a/beacon_node/network/src/router/processor.rs b/beacon_node/network/src/router/processor.rs index 04589d1405..b8db9c17f8 100644 --- a/beacon_node/network/src/router/processor.rs +++ b/beacon_node/network/src/router/processor.rs @@ -1,7 +1,8 @@ use crate::beacon_processor::{ BeaconProcessor, WorkEvent as BeaconWorkEvent, MAX_WORK_EVENT_QUEUE_LEN, }; -use crate::service::NetworkMessage; +use crate::service::{NetworkMessage, RequestId}; +use crate::sync::manager::RequestId as SyncId; use crate::sync::SyncMessage; use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes}; use lighthouse_network::rpc::*; @@ -100,8 +101,11 @@ impl Processor { /// this function notifies the sync manager of the error. pub fn on_rpc_error(&mut self, peer_id: PeerId, request_id: RequestId) { // Check if the failed RPC belongs to sync - if let RequestId::Sync(id) = request_id { - self.send_to_sync(SyncMessage::RPCError(peer_id, id)); + if let RequestId::Sync(request_id) = request_id { + self.send_to_sync(SyncMessage::RpcError { + peer_id, + request_id, + }); } } @@ -176,24 +180,28 @@ impl Processor { request_id: RequestId, beacon_block: Option>>, ) { + let request_id = match request_id { + RequestId::Sync(sync_id) => match sync_id { + SyncId::SingleBlock { .. } | SyncId::ParentLookup { .. } => { + unreachable!("Block lookups do not request BBRange requests") + } + id @ (SyncId::BackFillSync { .. } | SyncId::RangeSync { .. }) => id, + }, + RequestId::Router => unreachable!("All BBRange requests belong to sync"), + }; + trace!( self.log, "Received BlocksByRange Response"; "peer" => %peer_id, ); - if let RequestId::Sync(id) = request_id { - self.send_to_sync(SyncMessage::BlocksByRangeResponse { - peer_id, - request_id: id, - beacon_block, - }); - } else { - debug!( - self.log, - "All blocks by range responses should belong to sync" - ); - } + self.send_to_sync(SyncMessage::RpcBlock { + peer_id, + request_id, + beacon_block, + seen_timestamp: timestamp_now(), + }); } /// Handle a `BlocksByRoot` response from the peer. @@ -203,25 +211,27 @@ impl Processor { request_id: RequestId, beacon_block: Option>>, ) { + let request_id = match request_id { + RequestId::Sync(sync_id) => match sync_id { + id @ (SyncId::SingleBlock { .. } | SyncId::ParentLookup { .. }) => id, + SyncId::BackFillSync { .. } | SyncId::RangeSync { .. } => { + unreachable!("Batch syncing do not request BBRoot requests") + } + }, + RequestId::Router => unreachable!("All BBRoot requests belong to sync"), + }; + trace!( self.log, "Received BlocksByRoot Response"; "peer" => %peer_id, ); - - if let RequestId::Sync(id) = request_id { - self.send_to_sync(SyncMessage::BlocksByRootResponse { - peer_id, - request_id: id, - beacon_block, - seen_timestamp: timestamp_now(), - }); - } else { - debug!( - self.log, - "All Blocks by Root responses should belong to sync" - ) - } + self.send_to_sync(SyncMessage::RpcBlock { + peer_id, + request_id, + beacon_block, + seen_timestamp: timestamp_now(), + }); } /// Process a gossip message declaring a new block. diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index a16c2c677c..a8995de2e5 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -1,3 +1,4 @@ +use super::sync::manager::RequestId as SyncId; use crate::persisted_dht::{clear_dht, load_dht, persist_dht}; use crate::router::{Router, RouterMessage}; use crate::subnet_service::SyncCommitteeService; @@ -14,7 +15,7 @@ use lighthouse_network::{ prometheus_client::registry::Registry, MessageAcceptance, Service as LibP2PService, }; use lighthouse_network::{ - rpc::{GoodbyeReason, RPCResponseErrorCode, RequestId}, + rpc::{GoodbyeReason, RPCResponseErrorCode}, Context, Libp2pEvent, PeerAction, PeerRequestId, PubsubMessage, ReportSource, Request, Response, Subnet, }; @@ -42,6 +43,13 @@ const SUBSCRIBE_DELAY_SLOTS: u64 = 2; /// Delay after a fork where we unsubscribe from pre-fork topics. const UNSUBSCRIBE_DELAY_EPOCHS: u64 = 2; +/// Application level requests sent to the network. +#[derive(Debug, Clone, Copy)] +pub enum RequestId { + Sync(SyncId), + Router, +} + /// Types of messages that the network service can receive. #[derive(Debug)] pub enum NetworkMessage { @@ -112,7 +120,7 @@ pub struct NetworkService { /// A reference to the underlying beacon chain. beacon_chain: Arc>, /// The underlying libp2p service that drives all the network interactions. - libp2p: LibP2PService, + libp2p: LibP2PService, /// An attestation and subnet manager service. attestation_service: AttestationService, /// A sync committeee subnet manager service. @@ -389,7 +397,7 @@ impl NetworkService { /// Handle an event received from the network. async fn on_libp2p_event( &mut self, - ev: Libp2pEvent, + ev: Libp2pEvent, shutdown_sender: &mut Sender, ) { match ev { diff --git a/beacon_node/network/src/service/tests.rs b/beacon_node/network/src/service/tests.rs index d78b1fe4f8..f0dd0e75ff 100644 --- a/beacon_node/network/src/service/tests.rs +++ b/beacon_node/network/src/service/tests.rs @@ -59,6 +59,7 @@ mod tests { ); let mut config = NetworkConfig::default(); + config.discv5_config.table_filter = |_| true; // Do not ignore local IPs config.libp2p_port = 21212; config.upnp_enabled = false; config.discovery_port = 21212; diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index 0c34eef274..e76c037dad 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -8,9 +8,8 @@ //! If a batch fails, the backfill sync cannot progress. In this scenario, we mark the backfill //! sync as failed, log an error and attempt to retry once a new peer joins the node. -use super::RequestId; -use crate::beacon_processor::{ProcessId, WorkEvent as BeaconWorkEvent}; -use crate::sync::manager::BatchProcessResult; +use crate::beacon_processor::{ChainSegmentProcessId, WorkEvent as BeaconWorkEvent}; +use crate::sync::manager::{BatchProcessResult, Id}; use crate::sync::network_context::SyncNetworkContext; use crate::sync::range_sync::{BatchConfig, BatchId, BatchInfo, BatchState}; use beacon_chain::{BeaconChain, BeaconChainTypes}; @@ -357,7 +356,7 @@ impl BackFillSync { network: &mut SyncNetworkContext, batch_id: BatchId, peer_id: &PeerId, - request_id: RequestId, + request_id: Id, ) -> Result<(), BackFillError> { if let Some(batch) = self.batches.get_mut(&batch_id) { // A batch could be retried without the peer failing the request (disconnecting/ @@ -392,7 +391,7 @@ impl BackFillSync { network: &mut SyncNetworkContext, batch_id: BatchId, peer_id: &PeerId, - request_id: RequestId, + request_id: Id, beacon_block: Option>, ) -> Result { // check if we have this batch @@ -535,7 +534,7 @@ impl BackFillSync { Ok(v) => v, }; - let process_id = ProcessId::BackSyncBatchId(batch_id); + let process_id = ChainSegmentProcessId::BackSyncBatchId(batch_id); self.current_processing_batch = Some(batch_id); if let Err(e) = self diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs new file mode 100644 index 0000000000..b11dc1c7af --- /dev/null +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -0,0 +1,637 @@ +use std::collections::hash_map::Entry; +use std::time::Duration; + +use beacon_chain::{BeaconChainTypes, BlockError}; +use fnv::FnvHashMap; +use lighthouse_network::{PeerAction, PeerId}; +use lru_cache::LRUCache; +use slog::{crit, debug, error, trace, warn, Logger}; +use smallvec::SmallVec; +use store::{Hash256, SignedBeaconBlock}; +use strum::AsStaticRef; +use tokio::sync::mpsc; + +use crate::beacon_processor::{ChainSegmentProcessId, WorkEvent}; +use crate::metrics; + +use self::{ + parent_lookup::{ParentLookup, VerifyError}, + single_block_lookup::SingleBlockRequest, +}; + +use super::BatchProcessResult; +use super::{ + manager::{BlockProcessType, Id}, + network_context::SyncNetworkContext, +}; + +mod parent_lookup; +mod single_block_lookup; +#[cfg(test)] +mod tests; + +const FAILED_CHAINS_CACHE_SIZE: usize = 500; +const SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS: u8 = 3; + +pub(crate) struct BlockLookups { + /// A collection of parent block lookups. + parent_queue: SmallVec<[ParentLookup; 3]>, + + /// A cache of failed chain lookups to prevent duplicate searches. + failed_chains: LRUCache, + + /// A collection of block hashes being searched for and a flag indicating if a result has been + /// received or not. + /// + /// The flag allows us to determine if the peer returned data or sent us nothing. + single_block_lookups: FnvHashMap>, + + /// A multi-threaded, non-blocking processor for applying messages to the beacon chain. + beacon_processor_send: mpsc::Sender>, + + /// The logger for the import manager. + log: Logger, +} + +impl BlockLookups { + pub fn new(beacon_processor_send: mpsc::Sender>, log: Logger) -> Self { + Self { + parent_queue: Default::default(), + failed_chains: LRUCache::new(FAILED_CHAINS_CACHE_SIZE), + single_block_lookups: Default::default(), + beacon_processor_send, + log, + } + } + + /* Lookup requests */ + + pub fn search_block( + &mut self, + hash: Hash256, + peer_id: PeerId, + cx: &mut SyncNetworkContext, + ) { + // Do not re-request a block that is already being requested + if self + .single_block_lookups + .values_mut() + .any(|single_block_request| single_block_request.add_peer(&hash, &peer_id)) + { + return; + } + + debug!( + self.log, + "Searching for block"; + "peer_id" => %peer_id, + "block" => %hash + ); + + let mut single_block_request = SingleBlockRequest::new(hash, peer_id); + + let (peer_id, request) = single_block_request.request_block().unwrap(); + if let Ok(request_id) = cx.single_block_lookup_request(peer_id, request) { + self.single_block_lookups + .insert(request_id, single_block_request); + + metrics::set_gauge( + &metrics::SYNC_SINGLE_BLOCK_LOOKUPS, + self.single_block_lookups.len() as i64, + ); + } + } + + pub fn search_parent( + &mut self, + block: Box>, + peer_id: PeerId, + cx: &mut SyncNetworkContext, + ) { + let block_root = block.canonical_root(); + let parent_root = block.parent_root(); + // If this block or it's parent is part of a known failed chain, ignore it. + if self.failed_chains.contains(&parent_root) || self.failed_chains.contains(&block_root) { + debug!(self.log, "Block is from a past failed chain. Dropping"; + "block_root" => ?block_root, "block_slot" => block.slot()); + return; + } + + // Make sure this block is not already downloaded, and that neither it or its parent is + // being searched for. + if self.parent_queue.iter_mut().any(|parent_req| { + parent_req.contains_block(&block) + || parent_req.add_peer(&block_root, &peer_id) + || parent_req.add_peer(&parent_root, &peer_id) + }) { + // we are already searching for this block, ignore it + return; + } + + let parent_lookup = ParentLookup::new(*block, peer_id); + self.request_parent(parent_lookup, cx); + } + + /* Lookup responses */ + + pub fn single_block_lookup_response( + &mut self, + id: Id, + peer_id: PeerId, + block: Option>>, + seen_timestamp: Duration, + cx: &mut SyncNetworkContext, + ) { + let mut request = match self.single_block_lookups.entry(id) { + Entry::Occupied(req) => req, + Entry::Vacant(_) => { + if block.is_some() { + crit!( + self.log, + "Block returned for single block lookup not present" + ); + #[cfg(debug_assertions)] + panic!("block returned for single block lookup not present"); + } + return; + } + }; + + match request.get_mut().verify_block(block) { + Ok(Some(block)) => { + // This is the correct block, send it for processing + if self + .send_block_for_processing( + block, + seen_timestamp, + BlockProcessType::SingleBlock { id }, + ) + .is_err() + { + // Remove to avoid inconsistencies + self.single_block_lookups.remove(&id); + } + } + Ok(None) => { + // request finished correctly, it will be removed after the block is processed. + } + Err(error) => { + let msg: &str = error.as_static(); + cx.report_peer(peer_id, PeerAction::LowToleranceError, msg); + // Remove the request, if it can be retried it will be added with a new id. + let mut req = request.remove(); + + debug!(self.log, "Single block lookup failed"; + "peer_id" => %peer_id, "error" => msg, "block_root" => %req.hash); + // try the request again if possible + if let Ok((peer_id, request)) = req.request_block() { + if let Ok(id) = cx.single_block_lookup_request(peer_id, request) { + self.single_block_lookups.insert(id, req); + } + } + } + } + + metrics::set_gauge( + &metrics::SYNC_SINGLE_BLOCK_LOOKUPS, + self.single_block_lookups.len() as i64, + ); + } + + pub fn parent_lookup_response( + &mut self, + id: Id, + peer_id: PeerId, + block: Option>>, + seen_timestamp: Duration, + cx: &mut SyncNetworkContext, + ) { + let mut parent_lookup = if let Some(pos) = self + .parent_queue + .iter() + .position(|request| request.pending_response(id)) + { + self.parent_queue.remove(pos) + } else { + if block.is_some() { + debug!(self.log, "Response for a parent lookup request that was not found"; "peer_id" => %peer_id); + } + return; + }; + + match parent_lookup.verify_block(block, &self.failed_chains) { + Ok(Some(block)) => { + // Block is correct, send to the beacon processor. + let chain_hash = parent_lookup.chain_hash(); + if self + .send_block_for_processing( + block, + seen_timestamp, + BlockProcessType::ParentLookup { chain_hash }, + ) + .is_ok() + { + self.parent_queue.push(parent_lookup) + } + } + Ok(None) => { + // Request finished successfully, nothing else to do. It will be removed after the + // processing result arrives. + self.parent_queue.push(parent_lookup); + } + Err(e) => match e { + VerifyError::RootMismatch + | VerifyError::NoBlockReturned + | VerifyError::ExtraBlocksReturned => { + let e = e.as_static(); + warn!(self.log, "Peer sent invalid response to parent request."; + "peer_id" => %peer_id, "reason" => e); + + // We do not tolerate these kinds of errors. We will accept a few but these are signs + // of a faulty peer. + cx.report_peer(peer_id, PeerAction::LowToleranceError, e); + + // We try again if possible. + self.request_parent(parent_lookup, cx); + } + VerifyError::PreviousFailure { parent_root } => { + self.failed_chains.insert(parent_lookup.chain_hash()); + debug!( + self.log, + "Parent chain ignored due to past failure"; + "block" => %parent_root, + ); + // Add the root block to failed chains + self.failed_chains.insert(parent_lookup.chain_hash()); + + cx.report_peer( + peer_id, + PeerAction::MidToleranceError, + "bbroot_failed_chains", + ); + } + }, + }; + + metrics::set_gauge( + &metrics::SYNC_PARENT_BLOCK_LOOKUPS, + self.parent_queue.len() as i64, + ); + } + + /* Error responses */ + + #[allow(clippy::needless_collect)] // false positive + pub fn peer_disconnected(&mut self, peer_id: &PeerId, cx: &mut SyncNetworkContext) { + /* Check disconnection for single block lookups */ + // better written after https://github.com/rust-lang/rust/issues/59618 + let remove_retry_ids: Vec = self + .single_block_lookups + .iter_mut() + .filter_map(|(id, req)| { + if req.check_peer_disconnected(peer_id).is_err() { + Some(*id) + } else { + None + } + }) + .collect(); + + for mut req in remove_retry_ids + .into_iter() + .map(|id| self.single_block_lookups.remove(&id).unwrap()) + .collect::>() + { + // retry the request + match req.request_block() { + Ok((peer_id, block_request)) => { + if let Ok(request_id) = cx.single_block_lookup_request(peer_id, block_request) { + self.single_block_lookups.insert(request_id, req); + } + } + Err(e) => { + trace!(self.log, "Single block request failed on peer disconnection"; + "block_root" => %req.hash, "peer_id" => %peer_id, "reason" => e.as_static()); + } + } + } + + /* Check disconnection for parent lookups */ + while let Some(pos) = self + .parent_queue + .iter_mut() + .position(|req| req.check_peer_disconnected(peer_id).is_err()) + { + let parent_lookup = self.parent_queue.remove(pos); + trace!(self.log, "Parent lookup's peer disconnected"; &parent_lookup); + self.request_parent(parent_lookup, cx); + } + } + + pub fn parent_lookup_failed( + &mut self, + id: Id, + peer_id: PeerId, + cx: &mut SyncNetworkContext, + ) { + if let Some(pos) = self + .parent_queue + .iter() + .position(|request| request.pending_response(id)) + { + let mut parent_lookup = self.parent_queue.remove(pos); + parent_lookup.download_failed(); + trace!(self.log, "Parent lookup request failed"; &parent_lookup); + self.request_parent(parent_lookup, cx); + } else { + return debug!(self.log, "RPC failure for a parent lookup request that was not found"; "peer_id" => %peer_id); + }; + metrics::set_gauge( + &metrics::SYNC_PARENT_BLOCK_LOOKUPS, + self.parent_queue.len() as i64, + ); + } + + pub fn single_block_lookup_failed(&mut self, id: Id, cx: &mut SyncNetworkContext) { + if let Some(mut request) = self.single_block_lookups.remove(&id) { + request.register_failure(); + trace!(self.log, "Single block lookup failed"; "block" => %request.hash); + if let Ok((peer_id, block_request)) = request.request_block() { + if let Ok(request_id) = cx.single_block_lookup_request(peer_id, block_request) { + self.single_block_lookups.insert(request_id, request); + } + } + } + + metrics::set_gauge( + &metrics::SYNC_SINGLE_BLOCK_LOOKUPS, + self.single_block_lookups.len() as i64, + ); + } + + /* Processing responses */ + + pub fn single_block_processed( + &mut self, + id: Id, + result: Result<(), BlockError>, + cx: &mut SyncNetworkContext, + ) { + let mut req = match self.single_block_lookups.remove(&id) { + Some(req) => req, + None => { + #[cfg(debug_assertions)] + panic!("block processed for single block lookup not present"); + #[cfg(not(debug_assertions))] + return crit!( + self.log, + "Block processed for single block lookup not present" + ); + } + }; + + let root = req.hash; + let peer_id = match req.processing_peer() { + Ok(peer) => peer, + Err(_) => return, + }; + + if let Err(e) = &result { + trace!(self.log, "Single block processing failed"; "block" => %root, "error" => %e); + } else { + trace!(self.log, "Single block processing succeeded"; "block" => %root); + } + + match result { + Err(e) => match e { + BlockError::BlockIsAlreadyKnown => { + // No error here + } + BlockError::BeaconChainError(e) => { + // Internal error + error!(self.log, "Beacon chain error processing single block"; "block_root" => %root, "error" => ?e); + } + BlockError::ParentUnknown(block) => { + self.search_parent(block, peer_id, cx); + } + other => { + warn!(self.log, "Peer sent invalid block in single block lookup"; "root" => %root, "error" => ?other, "peer_id" => %peer_id); + cx.report_peer( + peer_id, + PeerAction::MidToleranceError, + "single_block_failure", + ); + + // Try it again if possible. + req.register_failure(); + if let Ok((peer_id, request)) = req.request_block() { + if let Ok(request_id) = cx.single_block_lookup_request(peer_id, request) { + // insert with the new id + self.single_block_lookups.insert(request_id, req); + } + } + } + }, + Ok(()) => { + // No error here + } + } + + metrics::set_gauge( + &metrics::SYNC_SINGLE_BLOCK_LOOKUPS, + self.single_block_lookups.len() as i64, + ); + } + + pub fn parent_block_processed( + &mut self, + chain_hash: Hash256, + result: Result<(), BlockError>, + cx: &mut SyncNetworkContext, + ) { + let (mut parent_lookup, peer_id) = if let Some((pos, peer)) = self + .parent_queue + .iter() + .enumerate() + .find_map(|(pos, request)| { + request + .get_processing_peer(chain_hash) + .map(|peer| (pos, peer)) + }) { + (self.parent_queue.remove(pos), peer) + } else { + #[cfg(debug_assertions)] + panic!( + "Process response for a parent lookup request that was not found. Chain_hash: {}", + chain_hash + ); + #[cfg(not(debug_assertions))] + return crit!(self.log, "Process response for a parent lookup request that was not found"; "chain_hash" => %chain_hash); + }; + + if let Err(e) = &result { + trace!(self.log, "Parent block processing failed"; &parent_lookup, "error" => %e); + } else { + trace!(self.log, "Parent block processing succeeded"; &parent_lookup); + } + + match result { + Err(BlockError::ParentUnknown(block)) => { + // need to keep looking for parents + // add the block back to the queue and continue the search + parent_lookup.add_block(*block); + self.request_parent(parent_lookup, cx); + } + Ok(_) | Err(BlockError::BlockIsAlreadyKnown { .. }) => { + let chain_hash = parent_lookup.chain_hash(); + let blocks = parent_lookup.chain_blocks(); + let process_id = ChainSegmentProcessId::ParentLookup(chain_hash); + + match self + .beacon_processor_send + .try_send(WorkEvent::chain_segment(process_id, blocks)) + { + Ok(_) => { + self.parent_queue.push(parent_lookup); + } + Err(e) => { + error!( + self.log, + "Failed to send chain segment to processor"; + "error" => ?e + ); + } + } + } + Err(outcome) => { + // all else we consider the chain a failure and downvote the peer that sent + // us the last block + warn!( + self.log, "Invalid parent chain"; + "score_adjustment" => %PeerAction::MidToleranceError, + "outcome" => ?outcome, + "last_peer" => %peer_id, + ); + + // Add this chain to cache of failed chains + self.failed_chains.insert(chain_hash); + + // This currently can be a host of errors. We permit this due to the partial + // ambiguity. + cx.report_peer(peer_id, PeerAction::MidToleranceError, "parent_request_err"); + } + } + + metrics::set_gauge( + &metrics::SYNC_PARENT_BLOCK_LOOKUPS, + self.parent_queue.len() as i64, + ); + } + + pub fn parent_chain_processed( + &mut self, + chain_hash: Hash256, + result: BatchProcessResult, + cx: &mut SyncNetworkContext, + ) { + let parent_lookup = if let Some(pos) = self + .parent_queue + .iter() + .position(|request| request.chain_hash() == chain_hash) + { + self.parent_queue.remove(pos) + } else { + #[cfg(debug_assertions)] + panic!( + "Chain process response for a parent lookup request that was not found. Chain_hash: {}", + chain_hash + ); + #[cfg(not(debug_assertions))] + return crit!(self.log, "Chain process response for a parent lookup request that was not found"; "chain_hash" => %chain_hash); + }; + + debug!(self.log, "Parent chain processed"; "chain_hash" => %chain_hash, "result" => ?result); + match result { + BatchProcessResult::Success(_) => { + // nothing to do. + } + BatchProcessResult::Failed { + imported_blocks: _, + peer_action, + } => { + self.failed_chains.insert(parent_lookup.chain_hash()); + if let Some(peer_action) = peer_action { + for &peer_id in parent_lookup.used_peers() { + cx.report_peer(peer_id, peer_action, "parent_chain_failure") + } + } + } + } + + metrics::set_gauge( + &metrics::SYNC_PARENT_BLOCK_LOOKUPS, + self.parent_queue.len() as i64, + ); + } + + /* Helper functions */ + + fn send_block_for_processing( + &mut self, + block: Box>, + duration: Duration, + process_type: BlockProcessType, + ) -> Result<(), ()> { + trace!(self.log, "Sending block for processing"; "block" => %block.canonical_root(), "process" => ?process_type); + let event = WorkEvent::rpc_beacon_block(block, duration, process_type); + if let Err(e) = self.beacon_processor_send.try_send(event) { + error!( + self.log, + "Failed to send sync block to processor"; + "error" => ?e + ); + return Err(()); + } + + Ok(()) + } + + fn request_parent( + &mut self, + mut parent_lookup: ParentLookup, + cx: &mut SyncNetworkContext, + ) { + match parent_lookup.request_parent(cx) { + Err(e) => { + debug!(self.log, "Failed to request parent"; &parent_lookup, "error" => e.as_static()); + match e { + parent_lookup::RequestError::SendFailed(_) => { + // Probably shutting down, nothing to do here. Drop the request + } + parent_lookup::RequestError::ChainTooLong + | parent_lookup::RequestError::TooManyAttempts => { + self.failed_chains.insert(parent_lookup.chain_hash()); + // This indicates faulty peers. + for &peer_id in parent_lookup.used_peers() { + cx.report_peer(peer_id, PeerAction::LowToleranceError, e.as_static()) + } + } + parent_lookup::RequestError::NoPeers => { + // This happens if the peer disconnects while the block is being + // processed. Drop the request without extra penalty + } + } + } + Ok(_) => { + debug!(self.log, "Requesting parent"; &parent_lookup); + self.parent_queue.push(parent_lookup) + } + } + + // We remove and add back again requests so we want this updated regardless of outcome. + metrics::set_gauge( + &metrics::SYNC_PARENT_BLOCK_LOOKUPS, + self.parent_queue.len() as i64, + ); + } +} diff --git a/beacon_node/network/src/sync/block_lookups/parent_lookup.rs b/beacon_node/network/src/sync/block_lookups/parent_lookup.rs new file mode 100644 index 0000000000..eb8d61ab9e --- /dev/null +++ b/beacon_node/network/src/sync/block_lookups/parent_lookup.rs @@ -0,0 +1,201 @@ +use lighthouse_network::PeerId; +use store::{EthSpec, Hash256, SignedBeaconBlock}; +use strum::AsStaticStr; + +use crate::sync::{ + manager::{Id, SLOT_IMPORT_TOLERANCE}, + network_context::SyncNetworkContext, +}; + +use super::single_block_lookup::{self, SingleBlockRequest}; + +/// How many attempts we try to find a parent of a block before we give up trying . +pub(crate) const PARENT_FAIL_TOLERANCE: u8 = 5; +/// The maximum depth we will search for a parent block. In principle we should have sync'd any +/// canonical chain to its head once the peer connects. A chain should not appear where it's depth +/// is further back than the most recent head slot. +pub(crate) const PARENT_DEPTH_TOLERANCE: usize = SLOT_IMPORT_TOLERANCE * 2; + +/// Maintains a sequential list of parents to lookup and the lookup's current state. +pub(crate) struct ParentLookup { + /// The root of the block triggering this parent request. + chain_hash: Hash256, + /// The blocks that have currently been downloaded. + downloaded_blocks: Vec>, + /// Request of the last parent. + current_parent_request: SingleBlockRequest, + /// Id of the last parent request. + current_parent_request_id: Option, +} + +#[derive(Debug, PartialEq, Eq, AsStaticStr)] +pub enum VerifyError { + RootMismatch, + NoBlockReturned, + ExtraBlocksReturned, + PreviousFailure { parent_root: Hash256 }, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum RequestError { + SendFailed(&'static str), + ChainTooLong, + TooManyAttempts, + NoPeers, +} + +impl ParentLookup { + pub fn contains_block(&self, block: &SignedBeaconBlock) -> bool { + self.downloaded_blocks + .iter() + .any(|d_block| d_block == block) + } + + pub fn new(block: SignedBeaconBlock, peer_id: PeerId) -> Self { + let current_parent_request = SingleBlockRequest::new(block.parent_root(), peer_id); + + Self { + chain_hash: block.canonical_root(), + downloaded_blocks: vec![block], + current_parent_request, + current_parent_request_id: None, + } + } + + /// Attempts to request the next unknown parent. If the request fails, it should be removed. + pub fn request_parent(&mut self, cx: &mut SyncNetworkContext) -> Result<(), RequestError> { + // check to make sure this request hasn't failed + if self.downloaded_blocks.len() >= PARENT_DEPTH_TOLERANCE { + return Err(RequestError::ChainTooLong); + } + + let (peer_id, request) = self.current_parent_request.request_block()?; + match cx.parent_lookup_request(peer_id, request) { + Ok(request_id) => { + self.current_parent_request_id = Some(request_id); + Ok(()) + } + Err(reason) => { + self.current_parent_request_id = None; + Err(RequestError::SendFailed(reason)) + } + } + } + + pub fn check_peer_disconnected(&mut self, peer_id: &PeerId) -> Result<(), ()> { + self.current_parent_request.check_peer_disconnected(peer_id) + } + + pub fn add_block(&mut self, block: SignedBeaconBlock) { + let next_parent = block.parent_root(); + self.downloaded_blocks.push(block); + self.current_parent_request.hash = next_parent; + self.current_parent_request.state = single_block_lookup::State::AwaitingDownload; + self.current_parent_request_id = None; + } + + pub fn pending_response(&self, req_id: Id) -> bool { + self.current_parent_request_id == Some(req_id) + } + + /// Get the parent lookup's chain hash. + pub fn chain_hash(&self) -> Hash256 { + self.chain_hash + } + + pub fn download_failed(&mut self) { + self.current_parent_request.register_failure(); + self.current_parent_request_id = None; + } + + pub fn chain_blocks(&mut self) -> Vec> { + std::mem::take(&mut self.downloaded_blocks) + } + + /// Verifies that the received block is what we requested. If so, parent lookup now waits for + /// the processing result of the block. + pub fn verify_block( + &mut self, + block: Option>>, + failed_chains: &lru_cache::LRUCache, + ) -> Result>>, VerifyError> { + let block = self.current_parent_request.verify_block(block)?; + + // check if the parent of this block isn't in the failed cache. If it is, this chain should + // be dropped and the peer downscored. + if let Some(parent_root) = block.as_ref().map(|block| block.parent_root()) { + if failed_chains.contains(&parent_root) { + self.current_parent_request.register_failure(); + self.current_parent_request_id = None; + return Err(VerifyError::PreviousFailure { parent_root }); + } + } + + Ok(block) + } + + pub fn get_processing_peer(&self, chain_hash: Hash256) -> Option { + if self.chain_hash == chain_hash { + return self.current_parent_request.processing_peer().ok(); + } + None + } + + #[cfg(test)] + pub fn failed_attempts(&self) -> u8 { + self.current_parent_request.failed_attempts + } + + pub fn add_peer(&mut self, block_root: &Hash256, peer_id: &PeerId) -> bool { + self.current_parent_request.add_peer(block_root, peer_id) + } + + pub fn used_peers(&self) -> impl Iterator + '_ { + self.current_parent_request.used_peers.iter() + } +} + +impl From for VerifyError { + fn from(e: super::single_block_lookup::VerifyError) -> Self { + use super::single_block_lookup::VerifyError as E; + match e { + E::RootMismatch => VerifyError::RootMismatch, + E::NoBlockReturned => VerifyError::NoBlockReturned, + E::ExtraBlocksReturned => VerifyError::ExtraBlocksReturned, + } + } +} + +impl From for RequestError { + fn from(e: super::single_block_lookup::LookupRequestError) -> Self { + use super::single_block_lookup::LookupRequestError as E; + match e { + E::TooManyAttempts => RequestError::TooManyAttempts, + E::NoPeers => RequestError::NoPeers, + } + } +} + +impl slog::KV for ParentLookup { + fn serialize( + &self, + record: &slog::Record, + serializer: &mut dyn slog::Serializer, + ) -> slog::Result { + serializer.emit_arguments("chain_hash", &format_args!("{}", self.chain_hash))?; + slog::Value::serialize(&self.current_parent_request, record, "parent", serializer)?; + serializer.emit_usize("downloaded_blocks", self.downloaded_blocks.len())?; + slog::Result::Ok(()) + } +} + +impl RequestError { + pub fn as_static(&self) -> &'static str { + match self { + RequestError::SendFailed(e) => e, + RequestError::ChainTooLong => "chain_too_long", + RequestError::TooManyAttempts => "too_many_attempts", + RequestError::NoPeers => "no_peers", + } + } +} diff --git a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs new file mode 100644 index 0000000000..a4df616cbb --- /dev/null +++ b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs @@ -0,0 +1,209 @@ +use std::collections::HashSet; + +use lighthouse_network::{rpc::BlocksByRootRequest, PeerId}; +use rand::seq::IteratorRandom; +use ssz_types::VariableList; +use store::{EthSpec, Hash256, SignedBeaconBlock}; +use strum::AsStaticStr; + +/// Object representing a single block lookup request. +#[derive(PartialEq, Eq)] +pub struct SingleBlockRequest { + /// The hash of the requested block. + pub hash: Hash256, + /// State of this request. + pub state: State, + /// Peers that should have this block. + pub available_peers: HashSet, + /// Peers from which we have requested this block. + pub used_peers: HashSet, + /// How many times have we attempted this block. + pub failed_attempts: u8, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum State { + AwaitingDownload, + Downloading { peer_id: PeerId }, + Processing { peer_id: PeerId }, +} + +#[derive(Debug, PartialEq, Eq, AsStaticStr)] +pub enum VerifyError { + RootMismatch, + NoBlockReturned, + ExtraBlocksReturned, +} + +#[derive(Debug, PartialEq, Eq, AsStaticStr)] +pub enum LookupRequestError { + TooManyAttempts, + NoPeers, +} + +impl SingleBlockRequest { + pub fn new(hash: Hash256, peer_id: PeerId) -> Self { + Self { + hash, + state: State::AwaitingDownload, + available_peers: HashSet::from([peer_id]), + used_peers: HashSet::default(), + failed_attempts: 0, + } + } + + pub fn register_failure(&mut self) { + self.failed_attempts += 1; + self.state = State::AwaitingDownload; + } + + pub fn add_peer(&mut self, hash: &Hash256, peer_id: &PeerId) -> bool { + let is_useful = &self.hash == hash; + if is_useful { + self.available_peers.insert(*peer_id); + } + is_useful + } + + /// If a peer disconnects, this request could be failed. If so, an error is returned + pub fn check_peer_disconnected(&mut self, dc_peer_id: &PeerId) -> Result<(), ()> { + self.available_peers.remove(dc_peer_id); + if let State::Downloading { peer_id } = &self.state { + if peer_id == dc_peer_id { + // Peer disconnected before providing a block + self.register_failure(); + return Err(()); + } + } + Ok(()) + } + + /// Verifies if the received block matches the requested one. + /// Returns the block for processing if the response is what we expected. + pub fn verify_block( + &mut self, + block: Option>>, + ) -> Result>>, VerifyError> { + match self.state { + State::AwaitingDownload => { + self.register_failure(); + Err(VerifyError::ExtraBlocksReturned) + } + State::Downloading { peer_id } => match block { + Some(block) => { + if block.canonical_root() != self.hash { + // return an error and drop the block + self.register_failure(); + Err(VerifyError::RootMismatch) + } else { + // Return the block for processing. + self.state = State::Processing { peer_id }; + Ok(Some(block)) + } + } + None => { + self.register_failure(); + Err(VerifyError::NoBlockReturned) + } + }, + State::Processing { peer_id: _ } => match block { + Some(_) => { + // We sent the block for processing and received an extra block. + self.register_failure(); + Err(VerifyError::ExtraBlocksReturned) + } + None => { + // This is simply the stream termination and we are already processing the + // block + Ok(None) + } + }, + } + } + + pub fn request_block(&mut self) -> Result<(PeerId, BlocksByRootRequest), LookupRequestError> { + debug_assert!(matches!(self.state, State::AwaitingDownload)); + if self.failed_attempts <= MAX_ATTEMPTS { + if let Some(&peer_id) = self.available_peers.iter().choose(&mut rand::thread_rng()) { + let request = BlocksByRootRequest { + block_roots: VariableList::from(vec![self.hash]), + }; + self.state = State::Downloading { peer_id }; + self.used_peers.insert(peer_id); + Ok((peer_id, request)) + } else { + Err(LookupRequestError::NoPeers) + } + } else { + Err(LookupRequestError::TooManyAttempts) + } + } + + pub fn processing_peer(&self) -> Result { + if let State::Processing { peer_id } = &self.state { + Ok(*peer_id) + } else { + Err(()) + } + } +} + +impl slog::Value for SingleBlockRequest { + fn serialize( + &self, + record: &slog::Record, + key: slog::Key, + serializer: &mut dyn slog::Serializer, + ) -> slog::Result { + serializer.emit_str("request", key)?; + serializer.emit_arguments("hash", &format_args!("{}", self.hash))?; + match &self.state { + State::AwaitingDownload => { + "awaiting_download".serialize(record, "state", serializer)? + } + State::Downloading { peer_id } => { + serializer.emit_arguments("downloading_peer", &format_args!("{}", peer_id))? + } + State::Processing { peer_id } => { + serializer.emit_arguments("processing_peer", &format_args!("{}", peer_id))? + } + } + slog::Result::Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use types::test_utils::{SeedableRng, TestRandom, XorShiftRng}; + use types::MinimalEthSpec as E; + + fn rand_block() -> SignedBeaconBlock { + let mut rng = XorShiftRng::from_seed([42; 16]); + SignedBeaconBlock::from_block( + types::BeaconBlock::Base(types::BeaconBlockBase { + ..<_>::random_for_test(&mut rng) + }), + types::Signature::random_for_test(&mut rng), + ) + } + + #[test] + fn test_happy_path() { + let peer_id = PeerId::random(); + let block = rand_block(); + + let mut sl = SingleBlockRequest::<4>::new(block.canonical_root(), peer_id); + sl.request_block().unwrap(); + sl.verify_block(Some(Box::new(block))).unwrap().unwrap(); + } + + #[test] + fn test_max_attempts() { + let peer_id = PeerId::random(); + let block = rand_block(); + + let mut sl = SingleBlockRequest::<4>::new(block.canonical_root(), peer_id); + sl.register_failure(); + } +} diff --git a/beacon_node/network/src/sync/block_lookups/tests.rs b/beacon_node/network/src/sync/block_lookups/tests.rs new file mode 100644 index 0000000000..dde7d49953 --- /dev/null +++ b/beacon_node/network/src/sync/block_lookups/tests.rs @@ -0,0 +1,460 @@ +use std::sync::Arc; + +use crate::service::RequestId; +use crate::sync::manager::RequestId as SyncId; +use crate::NetworkMessage; + +use super::*; + +use beacon_chain::builder::Witness; +use beacon_chain::eth1_chain::CachingEth1Backend; +use lighthouse_network::{NetworkGlobals, Request}; +use slog::{Drain, Level}; +use slot_clock::SystemTimeSlotClock; +use store::MemoryStore; +use types::test_utils::{SeedableRng, TestRandom, XorShiftRng}; +use types::MinimalEthSpec as E; + +type T = Witness, E, MemoryStore, MemoryStore>; + +struct TestRig { + beacon_processor_rx: mpsc::Receiver>, + network_rx: mpsc::UnboundedReceiver>, + rng: XorShiftRng, +} + +const D: Duration = Duration::new(0, 0); + +impl TestRig { + fn test_setup(log_level: Option) -> (BlockLookups, SyncNetworkContext, Self) { + let log = { + let decorator = slog_term::TermDecorator::new().build(); + let drain = slog_term::FullFormat::new(decorator).build().fuse(); + let drain = slog_async::Async::new(drain).build().fuse(); + + if let Some(log_level) = log_level { + slog::Logger::root(drain.filter_level(log_level).fuse(), slog::o!()) + } else { + slog::Logger::root(drain.filter(|_| false).fuse(), slog::o!()) + } + }; + + let (beacon_processor_tx, beacon_processor_rx) = mpsc::channel(100); + let (network_tx, network_rx) = mpsc::unbounded_channel(); + let rng = XorShiftRng::from_seed([42; 16]); + let rig = TestRig { + beacon_processor_rx, + network_rx, + rng, + }; + let bl = BlockLookups::new( + beacon_processor_tx, + log.new(slog::o!("component" => "block_lookups")), + ); + let cx = { + let globals = Arc::new(NetworkGlobals::new_test_globals(&log)); + SyncNetworkContext::new( + network_tx, + globals, + log.new(slog::o!("component" => "network_context")), + ) + }; + + (bl, cx, rig) + } + + fn rand_block(&mut self) -> SignedBeaconBlock { + SignedBeaconBlock::from_block( + types::BeaconBlock::Base(types::BeaconBlockBase { + ..<_>::random_for_test(&mut self.rng) + }), + types::Signature::random_for_test(&mut self.rng), + ) + } + + #[track_caller] + fn expect_block_request(&mut self) -> Id { + match self.network_rx.try_recv() { + Ok(NetworkMessage::SendRequest { + peer_id: _, + request: Request::BlocksByRoot(_request), + request_id: RequestId::Sync(SyncId::SingleBlock { id }), + }) => id, + other => { + panic!("Expected block request, found {:?}", other); + } + } + } + + #[track_caller] + fn expect_parent_request(&mut self) -> Id { + match self.network_rx.try_recv() { + Ok(NetworkMessage::SendRequest { + peer_id: _, + request: Request::BlocksByRoot(_request), + request_id: RequestId::Sync(SyncId::ParentLookup { id }), + }) => id, + other => panic!("Expected parent request, found {:?}", other), + } + } + + #[track_caller] + fn expect_block_process(&mut self) { + match self.beacon_processor_rx.try_recv() { + Ok(work) => { + assert_eq!(work.work_type(), crate::beacon_processor::RPC_BLOCK); + } + other => panic!("Expected block process, found {:?}", other), + } + } + + #[track_caller] + fn expect_parent_chain_process(&mut self) { + match self.beacon_processor_rx.try_recv() { + Ok(work) => { + assert_eq!(work.work_type(), crate::beacon_processor::CHAIN_SEGMENT); + } + other => panic!("Expected chain segment process, found {:?}", other), + } + } + + #[track_caller] + fn expect_empty_network(&mut self) { + assert_eq!( + self.network_rx.try_recv().expect_err("must err"), + mpsc::error::TryRecvError::Empty + ); + } + + #[track_caller] + pub fn expect_penalty(&mut self) { + match self.network_rx.try_recv() { + Ok(NetworkMessage::ReportPeer { .. }) => {} + other => panic!("Expected peer penalty, found {:?}", other), + } + } + + pub fn block_with_parent(&mut self, parent_root: Hash256) -> SignedBeaconBlock { + SignedBeaconBlock::from_block( + types::BeaconBlock::Base(types::BeaconBlockBase { + parent_root, + ..<_>::random_for_test(&mut self.rng) + }), + types::Signature::random_for_test(&mut self.rng), + ) + } +} + +#[test] +fn test_single_block_lookup_happy_path() { + let (mut bl, mut cx, mut rig) = TestRig::test_setup(None); + + let block = rig.rand_block(); + let peer_id = PeerId::random(); + + // Trigger the request + bl.search_block(block.canonical_root(), peer_id, &mut cx); + let id = rig.expect_block_request(); + + // The peer provides the correct block, should not be penalized. Now the block should be sent + // for processing. + bl.single_block_lookup_response(id, peer_id, Some(Box::new(block)), D, &mut cx); + rig.expect_empty_network(); + rig.expect_block_process(); + + // The request should still be active. + assert_eq!(bl.single_block_lookups.len(), 1); + + // Send the stream termination. Peer should have not been penalized, and the request removed + // after processing. + bl.single_block_lookup_response(id, peer_id, None, D, &mut cx); + bl.single_block_processed(id, Ok(()), &mut cx); + rig.expect_empty_network(); + assert_eq!(bl.single_block_lookups.len(), 0); +} + +#[test] +fn test_single_block_lookup_empty_response() { + let (mut bl, mut cx, mut rig) = TestRig::test_setup(None); + + let block_hash = Hash256::random(); + let peer_id = PeerId::random(); + + // Trigger the request + bl.search_block(block_hash, peer_id, &mut cx); + let id = rig.expect_block_request(); + + // The peer does not have the block. It should be penalized. + bl.single_block_lookup_response(id, peer_id, None, D, &mut cx); + rig.expect_penalty(); + + rig.expect_block_request(); // it should be retried +} + +#[test] +fn test_single_block_lookup_wrong_response() { + let (mut bl, mut cx, mut rig) = TestRig::test_setup(None); + + let block_hash = Hash256::random(); + let peer_id = PeerId::random(); + + // Trigger the request + bl.search_block(block_hash, peer_id, &mut cx); + let id = rig.expect_block_request(); + + // Peer sends something else. It should be penalized. + let bad_block = rig.rand_block(); + bl.single_block_lookup_response(id, peer_id, Some(Box::new(bad_block)), D, &mut cx); + rig.expect_penalty(); + rig.expect_block_request(); // should be retried + + // Send the stream termination. This should not produce an additional penalty. + bl.single_block_lookup_response(id, peer_id, None, D, &mut cx); + rig.expect_empty_network(); +} + +#[test] +fn test_single_block_lookup_failure() { + let (mut bl, mut cx, mut rig) = TestRig::test_setup(None); + + let block_hash = Hash256::random(); + let peer_id = PeerId::random(); + + // Trigger the request + bl.search_block(block_hash, peer_id, &mut cx); + let id = rig.expect_block_request(); + + // The request fails. RPC failures are handled elsewhere so we should not penalize the peer. + bl.single_block_lookup_failed(id, &mut cx); + rig.expect_block_request(); + rig.expect_empty_network(); +} + +#[test] +fn test_single_block_lookup_becomes_parent_request() { + let (mut bl, mut cx, mut rig) = TestRig::test_setup(None); + + let block = rig.rand_block(); + let peer_id = PeerId::random(); + + // Trigger the request + bl.search_block(block.canonical_root(), peer_id, &mut cx); + let id = rig.expect_block_request(); + + // The peer provides the correct block, should not be penalized. Now the block should be sent + // for processing. + bl.single_block_lookup_response(id, peer_id, Some(Box::new(block.clone())), D, &mut cx); + rig.expect_empty_network(); + rig.expect_block_process(); + + // The request should still be active. + assert_eq!(bl.single_block_lookups.len(), 1); + + // Send the stream termination. Peer should have not been penalized, and the request moved to a + // parent request after processing. + bl.single_block_processed(id, Err(BlockError::ParentUnknown(Box::new(block))), &mut cx); + assert_eq!(bl.single_block_lookups.len(), 0); + rig.expect_parent_request(); + rig.expect_empty_network(); + assert_eq!(bl.parent_queue.len(), 1); +} + +#[test] +fn test_parent_lookup_happy_path() { + let (mut bl, mut cx, mut rig) = TestRig::test_setup(None); + + let parent = rig.rand_block(); + let block = rig.block_with_parent(parent.canonical_root()); + let chain_hash = block.canonical_root(); + let peer_id = PeerId::random(); + + // Trigger the request + bl.search_parent(Box::new(block), peer_id, &mut cx); + let id = rig.expect_parent_request(); + + // Peer sends the right block, it should be sent for processing. Peer should not be penalized. + bl.parent_lookup_response(id, peer_id, Some(Box::new(parent)), D, &mut cx); + rig.expect_block_process(); + rig.expect_empty_network(); + + // Processing succeeds, now the rest of the chain should be sent for processing. + bl.parent_block_processed(chain_hash, Err(BlockError::BlockIsAlreadyKnown), &mut cx); + rig.expect_parent_chain_process(); + bl.parent_chain_processed(chain_hash, BatchProcessResult::Success(true), &mut cx); + assert_eq!(bl.parent_queue.len(), 0); +} + +#[test] +fn test_parent_lookup_wrong_response() { + let (mut bl, mut cx, mut rig) = TestRig::test_setup(None); + + let parent = rig.rand_block(); + let block = rig.block_with_parent(parent.canonical_root()); + let chain_hash = block.canonical_root(); + let peer_id = PeerId::random(); + + // Trigger the request + bl.search_parent(Box::new(block), peer_id, &mut cx); + let id1 = rig.expect_parent_request(); + + // Peer sends the wrong block, peer should be penalized and the block re-requested. + let bad_block = rig.rand_block(); + bl.parent_lookup_response(id1, peer_id, Some(Box::new(bad_block)), D, &mut cx); + rig.expect_penalty(); + let id2 = rig.expect_parent_request(); + + // Send the stream termination for the first request. This should not produce extra penalties. + bl.parent_lookup_response(id1, peer_id, None, D, &mut cx); + rig.expect_empty_network(); + + // Send the right block this time. + bl.parent_lookup_response(id2, peer_id, Some(Box::new(parent)), D, &mut cx); + rig.expect_block_process(); + + // Processing succeeds, now the rest of the chain should be sent for processing. + bl.parent_block_processed(chain_hash, Ok(()), &mut cx); + rig.expect_parent_chain_process(); + bl.parent_chain_processed(chain_hash, BatchProcessResult::Success(true), &mut cx); + assert_eq!(bl.parent_queue.len(), 0); +} + +#[test] +fn test_parent_lookup_empty_response() { + let (mut bl, mut cx, mut rig) = TestRig::test_setup(None); + + let parent = rig.rand_block(); + let block = rig.block_with_parent(parent.canonical_root()); + let chain_hash = block.canonical_root(); + let peer_id = PeerId::random(); + + // Trigger the request + bl.search_parent(Box::new(block), peer_id, &mut cx); + let id1 = rig.expect_parent_request(); + + // Peer sends an empty response, peer should be penalized and the block re-requested. + bl.parent_lookup_response(id1, peer_id, None, D, &mut cx); + rig.expect_penalty(); + let id2 = rig.expect_parent_request(); + + // Send the right block this time. + bl.parent_lookup_response(id2, peer_id, Some(Box::new(parent)), D, &mut cx); + rig.expect_block_process(); + + // Processing succeeds, now the rest of the chain should be sent for processing. + bl.parent_block_processed(chain_hash, Ok(()), &mut cx); + rig.expect_parent_chain_process(); + bl.parent_chain_processed(chain_hash, BatchProcessResult::Success(true), &mut cx); + assert_eq!(bl.parent_queue.len(), 0); +} + +#[test] +fn test_parent_lookup_rpc_failure() { + let (mut bl, mut cx, mut rig) = TestRig::test_setup(None); + + let parent = rig.rand_block(); + let block = rig.block_with_parent(parent.canonical_root()); + let chain_hash = block.canonical_root(); + let peer_id = PeerId::random(); + + // Trigger the request + bl.search_parent(Box::new(block), peer_id, &mut cx); + let id1 = rig.expect_parent_request(); + + // The request fails. It should be tried again. + bl.parent_lookup_failed(id1, peer_id, &mut cx); + let id2 = rig.expect_parent_request(); + + // Send the right block this time. + bl.parent_lookup_response(id2, peer_id, Some(Box::new(parent)), D, &mut cx); + rig.expect_block_process(); + + // Processing succeeds, now the rest of the chain should be sent for processing. + bl.parent_block_processed(chain_hash, Ok(()), &mut cx); + rig.expect_parent_chain_process(); + bl.parent_chain_processed(chain_hash, BatchProcessResult::Success(true), &mut cx); + assert_eq!(bl.parent_queue.len(), 0); +} + +#[test] +fn test_parent_lookup_too_many_attempts() { + let (mut bl, mut cx, mut rig) = TestRig::test_setup(None); + + let parent = rig.rand_block(); + let block = rig.block_with_parent(parent.canonical_root()); + let chain_hash = block.canonical_root(); + let peer_id = PeerId::random(); + + // Trigger the request + bl.search_parent(Box::new(block), peer_id, &mut cx); + for i in 1..=parent_lookup::PARENT_FAIL_TOLERANCE + 1 { + let id = rig.expect_parent_request(); + match i % 2 { + // make sure every error is accounted for + 0 => { + // The request fails. It should be tried again. + bl.parent_lookup_failed(id, peer_id, &mut cx); + } + _ => { + // Send a bad block this time. It should be tried again. + let bad_block = rig.rand_block(); + bl.parent_lookup_response(id, peer_id, Some(Box::new(bad_block)), D, &mut cx); + rig.expect_penalty(); + } + } + if i < parent_lookup::PARENT_FAIL_TOLERANCE { + assert_eq!(bl.parent_queue[0].failed_attempts(), dbg!(i)); + } + } + + assert_eq!(bl.parent_queue.len(), 0); + assert!(bl.failed_chains.contains(&chain_hash)); +} + +#[test] +fn test_parent_lookup_too_deep() { + let (mut bl, mut cx, mut rig) = TestRig::test_setup(None); + let mut blocks = + Vec::>::with_capacity(parent_lookup::PARENT_DEPTH_TOLERANCE); + while blocks.len() < parent_lookup::PARENT_DEPTH_TOLERANCE { + let parent = blocks + .last() + .map(|b| b.canonical_root()) + .unwrap_or_else(Hash256::random); + let block = rig.block_with_parent(parent); + blocks.push(block); + } + + let peer_id = PeerId::random(); + let trigger_block = blocks.pop().unwrap(); + let chain_hash = trigger_block.canonical_root(); + bl.search_parent(Box::new(trigger_block), peer_id, &mut cx); + + for block in blocks.into_iter().rev() { + let id = rig.expect_parent_request(); + // the block + bl.parent_lookup_response(id, peer_id, Some(Box::new(block.clone())), D, &mut cx); + // the stream termination + bl.parent_lookup_response(id, peer_id, None, D, &mut cx); + // the processing request + rig.expect_block_process(); + // the processing result + bl.parent_block_processed( + chain_hash, + Err(BlockError::ParentUnknown(Box::new(block))), + &mut cx, + ) + } + + rig.expect_penalty(); + assert!(bl.failed_chains.contains(&chain_hash)); +} + +#[test] +fn test_parent_lookup_disconnection() { + let (mut bl, mut cx, mut rig) = TestRig::test_setup(None); + let peer_id = PeerId::random(); + let trigger_block = rig.rand_block(); + bl.search_parent(Box::new(trigger_block), peer_id, &mut cx); + bl.peer_disconnected(&peer_id, &mut cx); + assert!(bl.parent_queue.is_empty()); +} diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 960dd12afc..53480db88e 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -34,29 +34,25 @@ //! search for the block and subsequently search for parents if needed. use super::backfill_sync::{BackFillSync, ProcessResult, SyncStart}; +use super::block_lookups::BlockLookups; use super::network_context::SyncNetworkContext; use super::peer_sync_info::{remote_sync_type, PeerSyncType}; -use super::range_sync::{ChainId, RangeSync, RangeSyncType, EPOCHS_PER_BATCH}; -use super::RequestId; -use crate::beacon_processor::{ProcessId, WorkEvent as BeaconWorkEvent}; +use super::range_sync::{RangeSync, RangeSyncType, EPOCHS_PER_BATCH}; +use crate::beacon_processor::{ChainSegmentProcessId, WorkEvent as BeaconWorkEvent}; use crate::service::NetworkMessage; use crate::status::ToStatusMessage; use beacon_chain::{BeaconChain, BeaconChainTypes, BlockError}; -use fnv::FnvHashMap; -use lighthouse_network::rpc::{methods::MAX_REQUEST_BLOCKS, BlocksByRootRequest, GoodbyeReason}; +use lighthouse_network::rpc::methods::MAX_REQUEST_BLOCKS; use lighthouse_network::types::{NetworkGlobals, SyncState}; use lighthouse_network::SyncInfo; use lighthouse_network::{PeerAction, PeerId}; -use lru_cache::LRUCache; -use slog::{crit, debug, error, info, trace, warn, Logger}; -use smallvec::SmallVec; -use ssz_types::VariableList; +use slog::{crit, debug, error, info, trace, Logger}; use std::boxed::Box; use std::ops::Sub; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; -use types::{Epoch, EthSpec, Hash256, SignedBeaconBlock, Slot}; +use types::{EthSpec, Hash256, SignedBeaconBlock, Slot}; /// The number of slots ahead of us that is allowed before requesting a long-range (batch) Sync /// from a peer. If a peer is within this tolerance (forwards or backwards), it is treated as a @@ -66,12 +62,21 @@ use types::{Epoch, EthSpec, Hash256, SignedBeaconBlock, Slot}; /// gossip if no peers are further than this range ahead of us that we have not already downloaded /// blocks for. pub const SLOT_IMPORT_TOLERANCE: usize = 32; -/// How many attempts we try to find a parent of a block before we give up trying . -const PARENT_FAIL_TOLERANCE: usize = 5; -/// The maximum depth we will search for a parent block. In principle we should have sync'd any -/// canonical chain to its head once the peer connects. A chain should not appear where it's depth -/// is further back than the most recent head slot. -const PARENT_DEPTH_TOLERANCE: usize = SLOT_IMPORT_TOLERANCE * 2; + +pub type Id = u32; + +/// Id of rpc requests sent by sync to the network. +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub enum RequestId { + /// Request searching for a block given a hash. + SingleBlock { id: Id }, + /// Request searching for a block's parent. The id is the chain + ParentLookup { id: Id }, + /// Request was from the backfill sync algorithm. + BackFillSync { id: Id }, + /// The request was from a chain in the range sync algorithm. + RangeSync { id: Id }, +} #[derive(Debug)] /// A message than can be sent to the sync manager thread. @@ -79,17 +84,10 @@ pub enum SyncMessage { /// A useful peer has been discovered. AddPeer(PeerId, SyncInfo), - /// A [`BlocksByRange`] response has been received. - BlocksByRangeResponse { - peer_id: PeerId, + /// A block has been received from the RPC. + RpcBlock { request_id: RequestId, - beacon_block: Option>>, - }, - - /// A [`BlocksByRoot`] response has been received. - BlocksByRootResponse { peer_id: PeerId, - request_id: RequestId, beacon_block: Option>>, seen_timestamp: Duration, }, @@ -105,30 +103,29 @@ pub enum SyncMessage { Disconnect(PeerId), /// An RPC Error has occurred on a request. - RPCError(PeerId, RequestId), + RpcError { + peer_id: PeerId, + request_id: RequestId, + }, /// A batch has been processed by the block processor thread. BatchProcessed { - sync_type: SyncRequestType, + sync_type: ChainSegmentProcessId, result: BatchProcessResult, }, - /// A parent lookup has failed. - ParentLookupFailed { - /// The head of the chain of blocks that failed to process. - chain_head: Hash256, - /// The peer that instigated the chain lookup. - peer_id: PeerId, + /// Block processed + BlockProcessed { + process_type: BlockProcessType, + result: Result<(), BlockError>, }, } -/// The type of sync request made +/// The type of processing specified for a received block. #[derive(Debug, Clone)] -pub enum SyncRequestType { - /// Request was from the backfill sync algorithm. - BackFillSync(Epoch), - /// The request was from a chain in the range sync algorithm. - RangeSync(Epoch, ChainId), +pub enum BlockProcessType { + SingleBlock { id: Id }, + ParentLookup { chain_hash: Hash256 }, } /// The result of processing multiple blocks (a chain segment). @@ -143,23 +140,6 @@ pub enum BatchProcessResult { }, } -/// Maintains a sequential list of parents to lookup and the lookup's current state. -struct ParentRequests { - /// The blocks that have currently been downloaded. - downloaded_blocks: Vec>, - - /// The number of failed attempts to retrieve a parent block. If too many attempts occur, this - /// lookup is failed and rejected. - failed_attempts: usize, - - /// The peer who last submitted a block. If the chain ends or fails, this is the peer that is - /// penalized. - last_submitted_peer: PeerId, - - /// The request ID of this lookup is in progress. - pending: Option, -} - /// The primary object for handling and driving all the current syncing logic. It maintains the /// current state of the syncing process, the number of useful peers, downloaded blocks and /// controls the logic behind both the long-range (batch) sync and the on-going potential parent @@ -183,42 +163,12 @@ pub struct SyncManager { /// Backfill syncing. backfill_sync: BackFillSync, - /// A collection of parent block lookups. - parent_queue: SmallVec<[ParentRequests; 3]>, - - /// A cache of failed chain lookups to prevent duplicate searches. - failed_chains: LRUCache, - - /// A collection of block hashes being searched for and a flag indicating if a result has been - /// received or not. - /// - /// The flag allows us to determine if the peer returned data or sent us nothing. - single_block_lookups: FnvHashMap, - - /// A multi-threaded, non-blocking processor for applying messages to the beacon chain. - beacon_processor_send: mpsc::Sender>, + block_lookups: BlockLookups, /// The logger for the import manager. log: Logger, } -/// Object representing a single block lookup request. -struct SingleBlockRequest { - /// The hash of the requested block. - pub hash: Hash256, - /// Whether a block was received from this request, or the peer returned an empty response. - pub block_returned: bool, -} - -impl SingleBlockRequest { - pub fn new(hash: Hash256) -> Self { - Self { - hash, - block_returned: false, - } - } -} - /// Spawns a new `SyncManager` thread which has a weak reference to underlying beacon /// chain. This allows the chain to be /// dropped during the syncing process which will gracefully end the `SyncManager`. @@ -239,25 +189,22 @@ pub fn spawn( // create an instance of the SyncManager let mut sync_manager = SyncManager { + chain: beacon_chain.clone(), + network_globals: network_globals.clone(), + input_channel: sync_recv, + network: SyncNetworkContext::new(network_send, network_globals.clone(), log.clone()), range_sync: RangeSync::new( beacon_chain.clone(), beacon_processor_send.clone(), log.clone(), ), backfill_sync: BackFillSync::new( - beacon_chain.clone(), - network_globals.clone(), + beacon_chain, + network_globals, beacon_processor_send.clone(), log.clone(), ), - network: SyncNetworkContext::new(network_send, network_globals.clone(), log.clone()), - chain: beacon_chain, - network_globals, - input_channel: sync_recv, - parent_queue: SmallVec::new(), - failed_chains: LRUCache::new(500), - single_block_lookups: FnvHashMap::default(), - beacon_processor_send, + block_lookups: BlockLookups::new(beacon_processor_send, log.clone()), log: log.clone(), }; @@ -307,345 +254,48 @@ impl SyncManager { self.update_sync_state(); } - /// The response to a `BlocksByRoot` request. - /// The current implementation takes one block at a time. As blocks are streamed, any - /// subsequent blocks will simply be ignored. - /// There are two reasons we could have received a BlocksByRoot response - /// - We requested a single hash and have received a response for the single_block_lookup - /// - We are looking up parent blocks in parent lookup search - async fn blocks_by_root_response( - &mut self, - peer_id: PeerId, - request_id: RequestId, - block: Option>, - seen_timestamp: Duration, - ) { - match block { - Some(block) => { - // data was returned, not just a stream termination - - // check if this is a single block lookup - i.e we were searching for a specific hash - let mut single_block_hash = None; - if let Some(block_request) = self.single_block_lookups.get_mut(&request_id) { - // update the state of the lookup indicating a block was received from the peer - block_request.block_returned = true; - single_block_hash = Some(block_request.hash); - } - if let Some(block_hash) = single_block_hash { - self.single_block_lookup_response(peer_id, block, block_hash, seen_timestamp) - .await; - return; - } - - // This wasn't a single block lookup request, it must be a response to a parent request search - // find the request - let mut parent_request = match self - .parent_queue - .iter() - .position(|request| request.pending == Some(request_id)) - { - // we remove from the queue and process it. It will get re-added if required - Some(pos) => self.parent_queue.remove(pos), - None => { - // No pending request, invalid request_id or coding error - warn!(self.log, "BlocksByRoot response unknown"; "request_id" => request_id); - return; - } - }; - - // check if the parent of this block isn't in our failed cache. If it is, this - // chain should be dropped and the peer downscored. - if self.failed_chains.contains(&block.message().parent_root()) { - debug!( - self.log, - "Parent chain ignored due to past failure"; - "block" => ?block.message().parent_root(), - "slot" => block.slot() - ); - if !parent_request.downloaded_blocks.is_empty() { - // Add the root block to failed chains - self.failed_chains - .insert(parent_request.downloaded_blocks[0].canonical_root()); - } else { - crit!(self.log, "Parent chain has no blocks"); - } - self.network.report_peer( - peer_id, - PeerAction::MidToleranceError, - "bbroot_failed_chains", - ); - return; - } - // add the block to response - parent_request.downloaded_blocks.push(block); - // queue for processing - self.process_parent_request(parent_request).await; - } - None => { - // this is a stream termination - - // stream termination for a single block lookup, remove the key - if let Some(single_block_request) = self.single_block_lookups.remove(&request_id) { - // The peer didn't respond with a block that it referenced. - // This can be allowed as some clients may implement pruning. We mildly - // tolerate this behaviour. - if !single_block_request.block_returned { - warn!(self.log, "Peer didn't respond with a block it referenced"; "referenced_block_hash" => %single_block_request.hash, "peer_id" => %peer_id); - self.network.report_peer( - peer_id, - PeerAction::MidToleranceError, - "bbroot_no_block", - ); - } - return; - } - - // This wasn't a single block lookup request, it must be a response to a parent request search - // find the request and remove it - let mut parent_request = match self - .parent_queue - .iter() - .position(|request| request.pending == Some(request_id)) - { - Some(pos) => self.parent_queue.remove(pos), - None => { - // No pending request, the parent request has been processed and this is - // the resulting stream termination. - return; - } - }; - // An empty response has been returned to a parent request - // if an empty response is given, the peer didn't have the requested block, try again - parent_request.failed_attempts += 1; - parent_request.last_submitted_peer = peer_id; - self.request_parent(parent_request); - } - } - } - - async fn process_block_async( - &mut self, - block: SignedBeaconBlock, - ) -> Option>> { - let (event, rx) = BeaconWorkEvent::rpc_beacon_block(Box::new(block)); - match self.beacon_processor_send.try_send(event) { - Ok(_) => {} - Err(e) => { - error!( - self.log, - "Failed to send sync block to processor"; - "error" => ?e - ); - return None; - } - } - - match rx.await { - Ok(block_result) => Some(block_result), - Err(_) => { - warn!( - self.log, - "Sync block not processed"; - "msg" => "likely due to system resource exhaustion" - ); - None - } - } - } - - /// Processes the response obtained from a single block lookup search. If the block is - /// processed or errors, the search ends. If the blocks parent is unknown, a block parent - /// lookup search is started. - async fn single_block_lookup_response( - &mut self, - peer_id: PeerId, - block: SignedBeaconBlock, - expected_block_hash: Hash256, - seen_timestamp: Duration, - ) { - // verify the hash is correct and try and process the block - if expected_block_hash != block.canonical_root() { - // The peer that sent this, sent us the wrong block. - // We do not tolerate this behaviour. The peer is instantly disconnected and banned. - warn!(self.log, "Peer sent incorrect block for single block lookup"; "peer_id" => %peer_id); - self.network.goodbye_peer(peer_id, GoodbyeReason::Fault); - return; - } - - let block_result = match self.process_block_async(block.clone()).await { - Some(block_result) => block_result, - None => return, - }; - - // we have the correct block, try and process it - match block_result { - Ok(block_root) => { - // Block has been processed, so write the block time to the cache. - self.chain.block_times_cache.write().set_time_observed( - block_root, - block.slot(), - seen_timestamp, - None, - None, - ); - info!(self.log, "Processed block"; "block" => %block_root); - - match self.chain.fork_choice() { - Ok(()) => trace!( - self.log, - "Fork choice success"; - "location" => "single block" - ), - Err(e) => error!( - self.log, - "Fork choice failed"; - "error" => ?e, - "location" => "single block" - ), - } - } - Err(BlockError::ParentUnknown { .. }) => { - // We don't know of the blocks parent, begin a parent lookup search - self.add_unknown_block(peer_id, block); - } - Err(BlockError::BlockIsAlreadyKnown) => { - trace!(self.log, "Single block lookup already known"); - } - Err(BlockError::BeaconChainError(e)) => { - warn!(self.log, "Unexpected block processing error"; "error" => ?e); - } - outcome => { - warn!(self.log, "Single block lookup failed"; "outcome" => ?outcome); - // This could be a range of errors. But we couldn't process the block. - // For now we consider this a mid tolerance error. - self.network.report_peer( - peer_id, - PeerAction::MidToleranceError, - "single_block_lookup_failed", - ); - } - } - } - - /// A block has been sent to us that has an unknown parent. This begins a parent lookup search - /// to find the parent or chain of parents that match our current chain. - fn add_unknown_block(&mut self, peer_id: PeerId, block: SignedBeaconBlock) { - // If we are not synced or within SLOT_IMPORT_TOLERANCE of the block, ignore - if !self.network_globals.sync_state.read().is_synced() { - let head_slot = self - .chain - .head_info() - .map(|info| info.slot) - .unwrap_or_else(|_| Slot::from(0u64)); - let unknown_block_slot = block.slot(); - - // if the block is far in the future, ignore it. If its within the slot tolerance of - // our current head, regardless of the syncing state, fetch it. - if (head_slot >= unknown_block_slot - && head_slot.sub(unknown_block_slot).as_usize() > SLOT_IMPORT_TOLERANCE) - || (head_slot < unknown_block_slot - && unknown_block_slot.sub(head_slot).as_usize() > SLOT_IMPORT_TOLERANCE) - { - return; - } - } - - let block_root = block.canonical_root(); - // If this block or it's parent is part of a known failed chain, ignore it. - if self.failed_chains.contains(&block.message().parent_root()) - || self.failed_chains.contains(&block_root) - { - debug!(self.log, "Block is from a past failed chain. Dropping"; "block_root" => ?block_root, "block_slot" => block.slot()); - return; - } - - // Make sure this block is not already being searched for - // NOTE: Potentially store a hashset of blocks for O(1) lookups - for parent_req in self.parent_queue.iter() { - if parent_req - .downloaded_blocks - .iter() - .any(|d_block| d_block == &block) - { - // we are already searching for this block, ignore it - return; - } - } - - debug!(self.log, "Unknown block received. Starting a parent lookup"; "block_slot" => block.slot(), "block_hash" => %block.canonical_root()); - - let parent_request = ParentRequests { - downloaded_blocks: vec![block], - failed_attempts: 0, - last_submitted_peer: peer_id, - pending: None, - }; - - self.request_parent(parent_request) - } - - /// A request to search for a block hash has been received. This function begins a BlocksByRoot - /// request to find the requested block. - fn search_for_block(&mut self, peer_id: PeerId, block_hash: Hash256) { - // If we are not synced, ignore this block - if !self.network_globals.sync_state.read().is_synced() { - return; - } - - // Do not re-request a block that is already being requested - if self - .single_block_lookups - .values() - .any(|single_block_request| single_block_request.hash == block_hash) - { - return; - } - - debug!( - self.log, - "Searching for block"; - "peer_id" => %peer_id, - "block" => %block_hash - ); - - let request = BlocksByRootRequest { - block_roots: VariableList::from(vec![block_hash]), - }; - - if let Ok(request_id) = self.network.blocks_by_root_request(peer_id, request) { - self.single_block_lookups - .insert(request_id, SingleBlockRequest::new(block_hash)); - } - } - /// Handles RPC errors related to requests that were emitted from the sync manager. fn inject_error(&mut self, peer_id: PeerId, request_id: RequestId) { trace!(self.log, "Sync manager received a failed RPC"); - // remove any single block lookups - if self.single_block_lookups.remove(&request_id).is_some() { - // this was a single block request lookup, look no further - return; + match request_id { + RequestId::SingleBlock { id } => { + self.block_lookups + .single_block_lookup_failed(id, &mut self.network); + } + RequestId::ParentLookup { id } => { + self.block_lookups + .parent_lookup_failed(id, peer_id, &mut self.network); + } + RequestId::BackFillSync { id } => { + if let Some(batch_id) = self.network.backfill_sync_response(id, true) { + match self + .backfill_sync + .inject_error(&mut self.network, batch_id, &peer_id, id) + { + Ok(_) => {} + Err(_) => self.update_sync_state(), + } + } + } + RequestId::RangeSync { id } => { + if let Some((chain_id, batch_id)) = self.network.range_sync_response(id, true) { + self.range_sync.inject_error( + &mut self.network, + peer_id, + batch_id, + chain_id, + id, + ); + self.update_sync_state() + } + } } - - // increment the failure of a parent lookup if the request matches a parent search - if let Some(pos) = self - .parent_queue - .iter() - .position(|request| request.pending == Some(request_id)) - { - let mut parent_request = self.parent_queue.remove(pos); - parent_request.failed_attempts += 1; - parent_request.last_submitted_peer = peer_id; - self.request_parent(parent_request); - return; - } - - // Otherwise this error matches no known request. - trace!(self.log, "Response/Error for non registered request"; "request_id" => request_id) } fn peer_disconnect(&mut self, peer_id: &PeerId) { self.range_sync.peer_disconnect(&mut self.network, peer_id); + self.block_lookups + .peer_disconnected(peer_id, &mut self.network); // Regardless of the outcome, we update the sync status. let _ = self .backfill_sync @@ -804,187 +454,6 @@ impl SyncManager { } } - /* Processing State Functions */ - // These functions are called in the main poll function to transition the state of the sync - // manager - - /// A new block has been received for a parent lookup query, process it. - async fn process_parent_request(&mut self, mut parent_request: ParentRequests) { - // verify the last added block is the parent of the last requested block - - if parent_request.downloaded_blocks.len() < 2 { - crit!( - self.log, - "There must be at least two blocks in a parent request lookup at all times" - ); - panic!("There must be at least two blocks in parent request lookup at all times"); - // fail loudly - } - let previous_index = parent_request.downloaded_blocks.len() - 2; - let expected_hash = parent_request.downloaded_blocks[previous_index].parent_root(); - - // Note: the length must be greater than 2 so this cannot panic. - let block_hash = parent_request - .downloaded_blocks - .last() - .expect("Complete batch cannot be empty") - .canonical_root(); - if block_hash != expected_hash { - // The sent block is not the correct block, remove the head block and downvote - // the peer - let _ = parent_request.downloaded_blocks.pop(); - let peer = parent_request.last_submitted_peer; - - warn!(self.log, "Peer sent invalid parent."; - "peer_id" => %peer, - "received_block" => %block_hash, - "expected_parent" => %expected_hash, - ); - - // We try again, but downvote the peer. - self.request_parent(parent_request); - // We do not tolerate these kinds of errors. We will accept a few but these are signs - // of a faulty peer. - self.network.report_peer( - peer, - PeerAction::LowToleranceError, - "parent_request_bad_hash", - ); - } else { - // The last block in the queue is the only one that has not attempted to be processed yet. - // - // The logic here attempts to process the last block. If it can be processed, the rest - // of the blocks must have known parents. If any of them cannot be processed, we - // consider the entire chain corrupt and drop it, notifying the user. - // - // If the last block in the queue cannot be processed, we also drop the entire queue. - // If the last block in the queue has an unknown parent, we continue the parent - // lookup-search. - - let chain_block_hash = parent_request.downloaded_blocks[0].canonical_root(); - - let newest_block = parent_request - .downloaded_blocks - .pop() - .expect("There is always at least one block in the queue"); - - let block_result = match self.process_block_async(newest_block.clone()).await { - Some(block_result) => block_result, - None => return, - }; - - match block_result { - Err(BlockError::ParentUnknown { .. }) => { - // need to keep looking for parents - // add the block back to the queue and continue the search - parent_request.downloaded_blocks.push(newest_block); - self.request_parent(parent_request); - } - Ok(_) | Err(BlockError::BlockIsAlreadyKnown { .. }) => { - let process_id = ProcessId::ParentLookup( - parent_request.last_submitted_peer, - chain_block_hash, - ); - let blocks = parent_request.downloaded_blocks; - - match self - .beacon_processor_send - .try_send(BeaconWorkEvent::chain_segment(process_id, blocks)) - { - Ok(_) => {} - Err(e) => { - error!( - self.log, - "Failed to send chain segment to processor"; - "error" => ?e - ); - } - } - } - Err(outcome) => { - // all else we consider the chain a failure and downvote the peer that sent - // us the last block - warn!( - self.log, "Invalid parent chain"; - "score_adjustment" => %PeerAction::MidToleranceError, - "outcome" => ?outcome, - "last_peer" => %parent_request.last_submitted_peer, - ); - - // Add this chain to cache of failed chains - self.failed_chains.insert(chain_block_hash); - - // This currently can be a host of errors. We permit this due to the partial - // ambiguity. - self.network.report_peer( - parent_request.last_submitted_peer, - PeerAction::MidToleranceError, - "parent_request_err", - ); - } - } - } - } - - /// Progresses a parent request query. - /// - /// This checks to ensure there a peers to progress the query, checks for failures and - /// initiates requests. - fn request_parent(&mut self, mut parent_request: ParentRequests) { - // check to make sure this request hasn't failed - if parent_request.failed_attempts >= PARENT_FAIL_TOLERANCE - || parent_request.downloaded_blocks.len() >= PARENT_DEPTH_TOLERANCE - { - let error = if parent_request.failed_attempts >= PARENT_FAIL_TOLERANCE { - // This is a peer-specific error and the chain could be continued with another - // peer. We don't consider this chain a failure and prevent retries with another - // peer. - "too many failed attempts" - } else if !parent_request.downloaded_blocks.is_empty() { - self.failed_chains - .insert(parent_request.downloaded_blocks[0].canonical_root()); - "reached maximum lookup-depth" - } else { - crit!(self.log, "Parent lookup has no blocks"); - "no blocks" - }; - - debug!(self.log, "Parent import failed"; - "block" => ?parent_request.downloaded_blocks[0].canonical_root(), - "ancestors_found" => parent_request.downloaded_blocks.len(), - "reason" => error - ); - // Downscore the peer. - self.network.report_peer( - parent_request.last_submitted_peer, - PeerAction::LowToleranceError, - "request_parent_import_failed", - ); - return; // drop the request - } - - let parent_hash = if let Some(block) = parent_request.downloaded_blocks.last() { - block.parent_root() - } else { - crit!(self.log, "Parent queue is empty. This should never happen"); - return; - }; - - let request = BlocksByRootRequest { - block_roots: VariableList::from(vec![parent_hash]), - }; - - // We continue to search for the chain of blocks from the same peer. Other peers are not - // guaranteed to have this chain of blocks. - let peer_id = parent_request.last_submitted_peer; - - if let Ok(request_id) = self.network.blocks_by_root_request(peer_id, request) { - // if the request was successful add the queue back into self - parent_request.pending = Some(request_id); - self.parent_queue.push(parent_request); - } - } - /// The main driving future for the sync manager. async fn main(&mut self) { // process any inbound messages @@ -994,107 +463,70 @@ impl SyncManager { SyncMessage::AddPeer(peer_id, info) => { self.add_peer(peer_id, info); } - SyncMessage::BlocksByRangeResponse { - peer_id, + SyncMessage::RpcBlock { request_id, - beacon_block, - } => { - let beacon_block = beacon_block.map(|b| *b); - // Obtain which sync requested these blocks and divert accordingly. - match self - .network - .blocks_by_range_response(request_id, beacon_block.is_none()) - { - Some(SyncRequestType::RangeSync(batch_id, chain_id)) => { - self.range_sync.blocks_by_range_response( - &mut self.network, - peer_id, - chain_id, - batch_id, - request_id, - beacon_block, - ); - self.update_sync_state(); - } - Some(SyncRequestType::BackFillSync(batch_id)) => { - match self.backfill_sync.on_block_response( - &mut self.network, - batch_id, - &peer_id, - request_id, - beacon_block, - ) { - Ok(ProcessResult::SyncCompleted) => self.update_sync_state(), - Ok(ProcessResult::Successful) => {} - Err(_error) => { - // The backfill sync has failed, errors are reported - // within. - self.update_sync_state(); - } - } - } - None => { - trace!(self.log, "Response/Error for non registered request"; "request_id" => request_id) - } - } - } - SyncMessage::BlocksByRootResponse { peer_id, - request_id, beacon_block, seen_timestamp, } => { - self.blocks_by_root_response( - peer_id, - request_id, - beacon_block.map(|b| *b), - seen_timestamp, - ) - .await; + self.rpc_block_received(request_id, peer_id, beacon_block, seen_timestamp); } SyncMessage::UnknownBlock(peer_id, block) => { - self.add_unknown_block(peer_id, *block); + // If we are not synced or within SLOT_IMPORT_TOLERANCE of the block, ignore + if !self.network_globals.sync_state.read().is_synced() { + let head_slot = self + .chain + .head_info() + .map(|info| info.slot) + .unwrap_or_else(|_| Slot::from(0u64)); + let unknown_block_slot = block.slot(); + + // if the block is far in the future, ignore it. If its within the slot tolerance of + // our current head, regardless of the syncing state, fetch it. + if (head_slot >= unknown_block_slot + && head_slot.sub(unknown_block_slot).as_usize() + > SLOT_IMPORT_TOLERANCE) + || (head_slot < unknown_block_slot + && unknown_block_slot.sub(head_slot).as_usize() + > SLOT_IMPORT_TOLERANCE) + { + continue; + } + } + if self.network_globals.peers.read().is_connected(&peer_id) { + self.block_lookups + .search_parent(block, peer_id, &mut self.network); + } } SyncMessage::UnknownBlockHash(peer_id, block_hash) => { - self.search_for_block(peer_id, block_hash); + // If we are not synced, ignore this block. + if self.network_globals.sync_state.read().is_synced() + && self.network_globals.peers.read().is_connected(&peer_id) + { + self.block_lookups + .search_block(block_hash, peer_id, &mut self.network); + } } SyncMessage::Disconnect(peer_id) => { self.peer_disconnect(&peer_id); } - SyncMessage::RPCError(peer_id, request_id) => { - // Redirect to a sync mechanism if the error is related to one of their - // requests. - match self.network.blocks_by_range_response(request_id, true) { - Some(SyncRequestType::RangeSync(batch_id, chain_id)) => { - self.range_sync.inject_error( - &mut self.network, - peer_id, - batch_id, - chain_id, - request_id, - ); - self.update_sync_state(); - } - Some(SyncRequestType::BackFillSync(batch_id)) => { - match self.backfill_sync.inject_error( - &mut self.network, - batch_id, - &peer_id, - request_id, - ) { - Ok(_) => {} - Err(_) => self.update_sync_state(), - } - } - None => { - // This is a request not belonging to a sync algorithm. - // Process internally. - self.inject_error(peer_id, request_id); - } - } - } + SyncMessage::RpcError { + peer_id, + request_id, + } => self.inject_error(peer_id, request_id), + SyncMessage::BlockProcessed { + process_type, + result, + } => match process_type { + BlockProcessType::SingleBlock { id } => self + .block_lookups + .single_block_processed(id, result, &mut self.network), + BlockProcessType::ParentLookup { chain_hash } => self + .block_lookups + .parent_block_processed(chain_hash, result, &mut self.network), + }, SyncMessage::BatchProcessed { sync_type, result } => match sync_type { - SyncRequestType::RangeSync(epoch, chain_id) => { + ChainSegmentProcessId::RangeBatchId(chain_id, epoch) => { self.range_sync.handle_block_process_result( &mut self.network, chain_id, @@ -1103,7 +535,7 @@ impl SyncManager { ); self.update_sync_state(); } - SyncRequestType::BackFillSync(epoch) => { + ChainSegmentProcessId::BackSyncBatchId(epoch) => { match self.backfill_sync.on_batch_process_result( &mut self.network, epoch, @@ -1118,22 +550,74 @@ impl SyncManager { } } } + ChainSegmentProcessId::ParentLookup(chain_hash) => self + .block_lookups + .parent_chain_processed(chain_hash, result, &mut self.network), }, - SyncMessage::ParentLookupFailed { - chain_head, - peer_id, - } => { - // A peer sent an object (block or attestation) that referenced a parent. - // The processing of this chain failed. - self.failed_chains.insert(chain_head); - self.network.report_peer( - peer_id, - PeerAction::MidToleranceError, - "parent_lookup_failed", - ); + } + } + } + } + + fn rpc_block_received( + &mut self, + request_id: RequestId, + peer_id: PeerId, + beacon_block: Option>>, + seen_timestamp: Duration, + ) { + match request_id { + RequestId::SingleBlock { id } => self.block_lookups.single_block_lookup_response( + id, + peer_id, + beacon_block, + seen_timestamp, + &mut self.network, + ), + RequestId::ParentLookup { id } => self.block_lookups.parent_lookup_response( + id, + peer_id, + beacon_block, + seen_timestamp, + &mut self.network, + ), + RequestId::BackFillSync { id } => { + if let Some(batch_id) = self + .network + .backfill_sync_response(id, beacon_block.is_none()) + { + match self.backfill_sync.on_block_response( + &mut self.network, + batch_id, + &peer_id, + id, + beacon_block.map(|b| *b), + ) { + Ok(ProcessResult::SyncCompleted) => self.update_sync_state(), + Ok(ProcessResult::Successful) => {} + Err(_error) => { + // The backfill sync has failed, errors are reported + // within. + self.update_sync_state(); + } } } } + RequestId::RangeSync { id } => { + if let Some((chain_id, batch_id)) = + self.network.range_sync_response(id, beacon_block.is_none()) + { + self.range_sync.blocks_by_range_response( + &mut self.network, + peer_id, + chain_id, + batch_id, + id, + beacon_block.map(|b| *b), + ); + self.update_sync_state(); + } + } } } } diff --git a/beacon_node/network/src/sync/mod.rs b/beacon_node/network/src/sync/mod.rs index 1755b13e37..7a891de728 100644 --- a/beacon_node/network/src/sync/mod.rs +++ b/beacon_node/network/src/sync/mod.rs @@ -2,6 +2,7 @@ //! //! Stores the various syncing methods for the beacon chain. mod backfill_sync; +mod block_lookups; pub mod manager; mod network_context; mod peer_sync_info; @@ -9,6 +10,3 @@ mod range_sync; pub use manager::{BatchProcessResult, SyncMessage}; pub use range_sync::ChainId; - -/// Type of id of rpc requests sent by sync -pub type RequestId = usize; diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 9415f21002..96bdc533f8 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -1,15 +1,12 @@ //! Provides network functionality for the Syncing thread. This fundamentally wraps a network //! channel and stores a global RPC ID to perform requests. -use super::manager::SyncRequestType; +use super::manager::{Id, RequestId as SyncRequestId}; use super::range_sync::{BatchId, ChainId}; -use super::RequestId as SyncRequestId; -use crate::service::NetworkMessage; +use crate::service::{NetworkMessage, RequestId}; use crate::status::ToStatusMessage; use fnv::FnvHashMap; -use lighthouse_network::rpc::{ - BlocksByRangeRequest, BlocksByRootRequest, GoodbyeReason, RequestId, -}; +use lighthouse_network::rpc::{BlocksByRangeRequest, BlocksByRootRequest, GoodbyeReason}; use lighthouse_network::{Client, NetworkGlobals, PeerAction, PeerId, ReportSource, Request}; use slog::{debug, trace, warn}; use std::sync::Arc; @@ -26,10 +23,12 @@ pub struct SyncNetworkContext { network_globals: Arc>, /// A sequential ID for all RPC requests. - request_id: SyncRequestId, + request_id: Id, - /// BlocksByRange requests made by syncing algorithms. - range_requests: FnvHashMap, + /// BlocksByRange requests made by the range syncing algorithm. + range_requests: FnvHashMap, + + backfill_requests: FnvHashMap, /// Logger for the `SyncNetworkContext`. log: slog::Logger, @@ -46,6 +45,7 @@ impl SyncNetworkContext { network_globals, request_id: 1, range_requests: FnvHashMap::default(), + backfill_requests: FnvHashMap::default(), log, } } @@ -78,7 +78,13 @@ impl SyncNetworkContext { "head_slot" => %status_message.head_slot, ); - let _ = self.send_rpc_request(peer_id, Request::Status(status_message.clone())); + let request = Request::Status(status_message.clone()); + let request_id = RequestId::Router; + let _ = self.send_network_msg(NetworkMessage::SendRequest { + peer_id, + request, + request_id, + }); } } } @@ -90,7 +96,7 @@ impl SyncNetworkContext { request: BlocksByRangeRequest, chain_id: ChainId, batch_id: BatchId, - ) -> Result { + ) -> Result { trace!( self.log, "Sending BlocksByRange Request"; @@ -98,10 +104,16 @@ impl SyncNetworkContext { "count" => request.count, "peer" => %peer_id, ); - let req_id = self.send_rpc_request(peer_id, Request::BlocksByRange(request))?; - self.range_requests - .insert(req_id, SyncRequestType::RangeSync(batch_id, chain_id)); - Ok(req_id) + let request = Request::BlocksByRange(request); + let id = self.next_id(); + let request_id = RequestId::Sync(SyncRequestId::RangeSync { id }); + self.send_network_msg(NetworkMessage::SendRequest { + peer_id, + request, + request_id, + })?; + self.range_requests.insert(id, (chain_id, batch_id)); + Ok(id) } /// A blocks by range request sent by the backfill sync algorithm @@ -110,7 +122,7 @@ impl SyncNetworkContext { peer_id: PeerId, request: BlocksByRangeRequest, batch_id: BatchId, - ) -> Result { + ) -> Result { trace!( self.log, "Sending backfill BlocksByRange Request"; @@ -118,21 +130,24 @@ impl SyncNetworkContext { "count" => request.count, "peer" => %peer_id, ); - let req_id = self.send_rpc_request(peer_id, Request::BlocksByRange(request))?; - self.range_requests - .insert(req_id, SyncRequestType::BackFillSync(batch_id)); - Ok(req_id) + let request = Request::BlocksByRange(request); + let id = self.next_id(); + let request_id = RequestId::Sync(SyncRequestId::BackFillSync { id }); + self.send_network_msg(NetworkMessage::SendRequest { + peer_id, + request, + request_id, + })?; + self.backfill_requests.insert(id, batch_id); + Ok(id) } /// Received a blocks by range response. - pub fn blocks_by_range_response( + pub fn range_sync_response( &mut self, - request_id: usize, + request_id: Id, remove: bool, - ) -> Option { - // NOTE: we can't guarantee that the request must be registered as it could receive more - // than an error, and be removed after receiving the first one. - // FIXME: https://github.com/sigp/lighthouse/issues/1634 + ) -> Option<(ChainId, BatchId)> { if remove { self.range_requests.remove(&request_id) } else { @@ -140,12 +155,21 @@ impl SyncNetworkContext { } } - /// Sends a blocks by root request. - pub fn blocks_by_root_request( + /// Received a blocks by range response. + pub fn backfill_sync_response(&mut self, request_id: Id, remove: bool) -> Option { + if remove { + self.backfill_requests.remove(&request_id) + } else { + self.backfill_requests.get(&request_id).cloned() + } + } + + /// Sends a blocks by root request for a single block lookup. + pub fn single_block_lookup_request( &mut self, peer_id: PeerId, request: BlocksByRootRequest, - ) -> Result { + ) -> Result { trace!( self.log, "Sending BlocksByRoot Request"; @@ -153,7 +177,39 @@ impl SyncNetworkContext { "count" => request.block_roots.len(), "peer" => %peer_id ); - self.send_rpc_request(peer_id, Request::BlocksByRoot(request)) + let request = Request::BlocksByRoot(request); + let id = self.next_id(); + let request_id = RequestId::Sync(SyncRequestId::SingleBlock { id }); + self.send_network_msg(NetworkMessage::SendRequest { + peer_id, + request, + request_id, + })?; + Ok(id) + } + + /// Sends a blocks by root request for a parent request. + pub fn parent_lookup_request( + &mut self, + peer_id: PeerId, + request: BlocksByRootRequest, + ) -> Result { + trace!( + self.log, + "Sending BlocksByRoot Request"; + "method" => "BlocksByRoot", + "count" => request.block_roots.len(), + "peer" => %peer_id + ); + let request = Request::BlocksByRoot(request); + let id = self.next_id(); + let request_id = RequestId::Sync(SyncRequestId::ParentLookup { id }); + self.send_network_msg(NetworkMessage::SendRequest { + peer_id, + request, + request_id, + })?; + Ok(id) } /// Terminates the connection with the peer and bans them. @@ -184,22 +240,6 @@ impl SyncNetworkContext { }); } - /// Sends an RPC request. - fn send_rpc_request( - &mut self, - peer_id: PeerId, - request: Request, - ) -> Result { - let request_id = self.request_id; - self.request_id += 1; - self.send_network_msg(NetworkMessage::SendRequest { - peer_id, - request_id: RequestId::Sync(request_id), - request, - })?; - Ok(request_id) - } - /// Subscribes to core topics. pub fn subscribe_core_topics(&mut self) { self.network_send @@ -216,4 +256,10 @@ impl SyncNetworkContext { "Network channel send Failed" }) } + + fn next_id(&mut self) -> Id { + let id = self.request_id; + self.request_id += 1; + id + } } diff --git a/beacon_node/network/src/sync/range_sync/batch.rs b/beacon_node/network/src/sync/range_sync/batch.rs index 7239081ad1..614bf57dd0 100644 --- a/beacon_node/network/src/sync/range_sync/batch.rs +++ b/beacon_node/network/src/sync/range_sync/batch.rs @@ -1,4 +1,4 @@ -use crate::sync::RequestId; +use crate::sync::manager::Id; use lighthouse_network::rpc::methods::BlocksByRangeRequest; use lighthouse_network::PeerId; use std::collections::HashSet; @@ -93,7 +93,7 @@ pub enum BatchState { /// The batch has failed either downloading or processing, but can be requested again. AwaitingDownload, /// The batch is being downloaded. - Downloading(PeerId, Vec>, RequestId), + Downloading(PeerId, Vec>, Id), /// The batch has been completely downloaded and is ready for processing. AwaitingProcessing(PeerId, Vec>), /// The batch is being processed. @@ -167,7 +167,7 @@ impl BatchInfo { } /// Verifies if an incomming block belongs to this batch. - pub fn is_expecting_block(&self, peer_id: &PeerId, request_id: &RequestId) -> bool { + pub fn is_expecting_block(&self, peer_id: &PeerId, request_id: &Id) -> bool { if let BatchState::Downloading(expected_peer, _, expected_id) = &self.state { return peer_id == expected_peer && expected_id == request_id; } @@ -312,7 +312,7 @@ impl BatchInfo { pub fn start_downloading_from_peer( &mut self, peer: PeerId, - request_id: RequestId, + request_id: Id, ) -> Result<(), WrongState> { match self.state.poison() { BatchState::AwaitingDownload => { diff --git a/beacon_node/network/src/sync/range_sync/chain.rs b/beacon_node/network/src/sync/range_sync/chain.rs index 4474f1cc34..3f81647217 100644 --- a/beacon_node/network/src/sync/range_sync/chain.rs +++ b/beacon_node/network/src/sync/range_sync/chain.rs @@ -1,7 +1,7 @@ use super::batch::{BatchInfo, BatchState}; -use crate::beacon_processor::ProcessId; +use crate::beacon_processor::ChainSegmentProcessId; use crate::beacon_processor::WorkEvent as BeaconWorkEvent; -use crate::sync::{network_context::SyncNetworkContext, BatchProcessResult, RequestId}; +use crate::sync::{manager::Id, network_context::SyncNetworkContext, BatchProcessResult}; use beacon_chain::BeaconChainTypes; use fnv::FnvHashMap; use lighthouse_network::{PeerAction, PeerId}; @@ -214,7 +214,7 @@ impl SyncingChain { network: &mut SyncNetworkContext, batch_id: BatchId, peer_id: &PeerId, - request_id: RequestId, + request_id: Id, beacon_block: Option>, ) -> ProcessingResult { // check if we have this batch @@ -300,7 +300,7 @@ impl SyncingChain { // for removing chains and checking completion is in the callback. let blocks = batch.start_processing()?; - let process_id = ProcessId::RangeBatchId(self.id, batch_id); + let process_id = ChainSegmentProcessId::RangeBatchId(self.id, batch_id); self.current_processing_batch = Some(batch_id); if let Err(e) = self @@ -807,7 +807,7 @@ impl SyncingChain { network: &mut SyncNetworkContext, batch_id: BatchId, peer_id: &PeerId, - request_id: RequestId, + request_id: Id, ) -> ProcessingResult { if let Some(batch) = self.batches.get_mut(&batch_id) { // A batch could be retried without the peer failing the request (disconnecting/ diff --git a/beacon_node/network/src/sync/range_sync/range.rs b/beacon_node/network/src/sync/range_sync/range.rs index ffe74ea985..185fc204ac 100644 --- a/beacon_node/network/src/sync/range_sync/range.rs +++ b/beacon_node/network/src/sync/range_sync/range.rs @@ -45,8 +45,9 @@ use super::chain_collection::ChainCollection; use super::sync_type::RangeSyncType; use crate::beacon_processor::WorkEvent as BeaconWorkEvent; use crate::status::ToStatusMessage; +use crate::sync::manager::Id; use crate::sync::network_context::SyncNetworkContext; -use crate::sync::{BatchProcessResult, RequestId}; +use crate::sync::BatchProcessResult; use beacon_chain::{BeaconChain, BeaconChainTypes}; use lighthouse_network::PeerId; use lighthouse_network::SyncInfo; @@ -201,7 +202,7 @@ where peer_id: PeerId, chain_id: ChainId, batch_id: BatchId, - request_id: RequestId, + request_id: Id, beacon_block: Option>, ) { // check if this chunk removes the chain @@ -300,7 +301,7 @@ where peer_id: PeerId, batch_id: BatchId, chain_id: ChainId, - request_id: RequestId, + request_id: Id, ) { // check that this request is pending match self.chains.call_by_id(chain_id, |chain| { @@ -364,6 +365,7 @@ where #[cfg(test)] mod tests { + use crate::service::RequestId; use crate::NetworkMessage; use super::*; @@ -494,10 +496,7 @@ mod tests { } /// Reads an BlocksByRange request to a given peer from the network receiver channel. - fn grab_request( - &mut self, - expected_peer: &PeerId, - ) -> (lighthouse_network::rpc::RequestId, BlocksByRangeRequest) { + fn grab_request(&mut self, expected_peer: &PeerId) -> (RequestId, BlocksByRangeRequest) { if let Some(NetworkMessage::SendRequest { peer_id, request: Request::BlocksByRange(request), diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 9d80070e96..a1d9512346 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -93,7 +93,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { Arg::with_name("target-peers") .long("target-peers") .help("The target number of peers.") - .default_value("50") + .default_value("80") .takes_value(true), ) .arg( @@ -187,6 +187,12 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .help("One or more comma-delimited trusted peer ids which always have the highest score according to the peer scoring system.") .takes_value(true), ) + .arg( + Arg::with_name("enable-private-discovery") + .long("enable-private-discovery") + .help("Lighthouse by default does not discover private IP addresses. Set this flag to enable connection attempts to local addresses.") + .takes_value(false), + ) /* REST API related arguments */ .arg( Arg::with_name("http") @@ -392,6 +398,35 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { will be used. Defaults to http://127.0.0.1:8545.") .takes_value(true) ) + .arg( + Arg::with_name("jwt-secrets") + .long("jwt-secrets") + .value_name("JWT-SECRETS") + .help("One or more comma-delimited file paths which contain the corresponding hex-encoded \ + JWT secrets for each execution endpoint provided in the --execution-endpoints flag. \ + The number of paths should be in the same order and strictly equal to the number \ + of execution endpoints provided.") + .takes_value(true) + .requires("execution-endpoints") + ) + .arg( + Arg::with_name("jwt-id") + .long("jwt-id") + .value_name("JWT-ID") + .help("Used by the beacon node to communicate a unique identifier to execution nodes \ + during JWT authentication. It corresponds to the 'id' field in the JWT claims object.\ + Set to empty by deafult") + .takes_value(true) + ) + .arg( + Arg::with_name("jwt-version") + .long("jwt-version") + .value_name("JWT-VERSION") + .help("Used by the beacon node to communicate a client version to execution nodes \ + during JWT authentication. It corresponds to the 'clv' field in the JWT claims object.\ + Set to empty by deafult") + .takes_value(true) + ) .arg( Arg::with_name("suggested-fee-recipient") .long("suggested-fee-recipient") diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 52a1919114..8c30dcfddb 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -93,8 +93,8 @@ pub fn get_config( if let Some(address) = cli_args.value_of("http-address") { client_config.http_api.listen_addr = address - .parse::() - .map_err(|_| "http-address is not a valid IPv4 address.")?; + .parse::() + .map_err(|_| "http-address is not a valid IP address.")?; } if let Some(port) = cli_args.value_of("http-port") { @@ -145,8 +145,8 @@ pub fn get_config( if let Some(address) = cli_args.value_of("metrics-address") { client_config.http_metrics.listen_addr = address - .parse::() - .map_err(|_| "metrics-address is not a valid IPv4 address.")?; + .parse::() + .map_err(|_| "metrics-address is not a valid IP address.")?; } if let Some(port) = cli_args.value_of("metrics-port") { @@ -236,20 +236,41 @@ pub fn get_config( client_config.eth1.purge_cache = true; } - if let Some(endpoints) = cli_args.value_of("execution-endpoints") { - client_config.sync_eth1_chain = true; - client_config.execution_endpoints = endpoints - .split(',') - .map(SensitiveUrl::parse) - .collect::>() - .map(Some) - .map_err(|e| format!("execution-endpoints contains an invalid URL {:?}", e))?; - } else if cli_args.is_present("merge") { - client_config.execution_endpoints = Some(client_config.eth1.endpoints.clone()); - } + if cli_args.is_present("merge") || cli_args.is_present("execution-endpoints") { + let mut el_config = execution_layer::Config::default(); - client_config.suggested_fee_recipient = - clap_utils::parse_optional(cli_args, "suggested-fee-recipient")?; + if let Some(endpoints) = cli_args.value_of("execution-endpoints") { + client_config.sync_eth1_chain = true; + el_config.execution_endpoints = endpoints + .split(',') + .map(SensitiveUrl::parse) + .collect::>() + .map_err(|e| format!("execution-endpoints contains an invalid URL {:?}", e))?; + } else if cli_args.is_present("merge") { + el_config.execution_endpoints = client_config.eth1.endpoints.clone(); + } + + if let Some(secrets) = cli_args.value_of("jwt-secrets") { + let secret_files: Vec<_> = secrets.split(',').map(PathBuf::from).collect(); + if !secret_files.is_empty() && secret_files.len() != el_config.execution_endpoints.len() + { + return Err(format!( + "{} execution-endpoints supplied with {} jwt-secrets. Lengths \ + must match or jwt-secrets must be empty.", + el_config.execution_endpoints.len(), + secret_files.len(), + )); + } + el_config.secret_files = secret_files; + } + + el_config.suggested_fee_recipient = + clap_utils::parse_optional(cli_args, "suggested-fee-recipient")?; + el_config.jwt_id = clap_utils::parse_optional(cli_args, "jwt-id")?; + el_config.jwt_version = clap_utils::parse_optional(cli_args, "jwt-version")?; + el_config.default_datadir = client_config.data_dir.clone(); + client_config.execution_layer = Some(el_config); + } if let Some(freezer_dir) = cli_args.value_of("freezer-dir") { client_config.freezer_db_path = Some(PathBuf::from(freezer_dir)); @@ -760,6 +781,10 @@ pub fn set_network_config( config.metrics_enabled = true; } + if cli_args.is_present("enable-private-discovery") { + config.discv5_config.table_filter = |_| true; + } + Ok(()) } diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index f719a3a2b1..22d279d8b7 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -17,7 +17,7 @@ * [Create a validator](./validator-create.md) * [Key recovery](./key-recovery.md) * [Validator Management](./validator-management.md) - * [Importing from the Eth2 Launchpad](./validator-import-launchpad.md) + * [Importing from the Staking Launchpad](./validator-import-launchpad.md) * [Slashing Protection](./slashing-protection.md) * [Voluntary Exits](./voluntary-exit.md) * [Validator Monitoring](./validator-monitoring.md) diff --git a/book/src/advanced_database.md b/book/src/advanced_database.md index b562b31dd6..02a344c74a 100644 --- a/book/src/advanced_database.md +++ b/book/src/advanced_database.md @@ -36,8 +36,8 @@ is 32, and the maximum is 8192. The values shown in the table are approximate, calculated using a simple heuristic: each `BeaconState` consumes around 18MB of disk space, and each block replayed takes around 5ms. The **Yearly Disk Usage** column shows the approx size of the freezer DB _alone_ (hot DB not included), -and the **Load Historical State** time is the worst-case load time for a state in the last slot of -an epoch. +and the **Load Historical State** time is the worst-case load time for a state in the last slot +before a restore point. To configure your Lighthouse node's database with a non-default SPRP, run your Beacon Node with the `--slots-per-restore-point` flag: diff --git a/book/src/advanced_networking.md b/book/src/advanced_networking.md index 461f33f220..c79ddab01f 100644 --- a/book/src/advanced_networking.md +++ b/book/src/advanced_networking.md @@ -46,7 +46,7 @@ still function if it is behind a NAT without any port mappings. Although Lighthouse still functions, we recommend that some mechanism is used to ensure that your Lighthouse node is publicly accessible. This will typically improve your peer count, allow the scoring system to find the best/most favourable -peers for your node and overall improve the eth2 network. +peers for your node and overall improve the Ethereum consensus network. Lighthouse currently supports UPnP. If UPnP is enabled on your router, Lighthouse will automatically establish the port mappings for you (the beacon @@ -63,7 +63,7 @@ settings allow you construct your initial ENR. Their primary intention is for setting up boot-like nodes and having a contactable ENR on boot. On normal operation of a Lighthouse node, none of these flags need to be set. Setting these flags incorrectly can lead to your node being incorrectly added to the -global DHT which will degrades the discovery process for all Eth2 peers. +global DHT which will degrades the discovery process for all Ethereum consensus peers. The ENR of a Lighthouse node is initially set to be non-contactable. The in-built discovery mechanism can determine if you node is publicly accessible, diff --git a/book/src/api-bn.md b/book/src/api-bn.md index b82a8da1d0..481c001694 100644 --- a/book/src/api-bn.md +++ b/book/src/api-bn.md @@ -1,6 +1,6 @@ # Beacon Node API -Lighthouse implements the standard [Eth2 Beacon Node API +Lighthouse implements the standard [Beacon Node API specification][OpenAPI]. Please follow that link for a full description of each API endpoint. ## Starting the server @@ -22,7 +22,7 @@ The following CLI flags control the HTTP server: - `--http-tls-cert`: specify the path to the certificate file for Lighthouse to use. - `--http-tls-key`: specify the path to the private key file for Lighthouse to use. -The schema of the API aligns with the standard Eth2 Beacon Node API as defined +The schema of the API aligns with the standard Beacon Node API as defined at [github.com/ethereum/beacon-APIs](https://github.com/ethereum/beacon-APIs). An interactive specification is available [here][OpenAPI]. @@ -64,7 +64,7 @@ lighthouse bn --http ## HTTP Request/Response Examples This section contains some simple examples of using the HTTP API via `curl`. -All endpoints are documented in the [Eth2 Beacon Node API +All endpoints are documented in the [Beacon Node API specification][OpenAPI]. ### View the head of the beacon chain @@ -137,7 +137,7 @@ However, even when serving the HTTP API server over TLS, it should not be exposed publicly without one of the security measures suggested in the [Security](#security) section. -Below is an simple example serving the HTTP API over TLS using a +Below is a simple example serving the HTTP API over TLS using a self-signed certificate on Linux: ### Enabling TLS on a beacon node diff --git a/book/src/api-lighthouse.md b/book/src/api-lighthouse.md index 77800b5396..ea282cf2bc 100644 --- a/book/src/api-lighthouse.md +++ b/book/src/api-lighthouse.md @@ -199,23 +199,23 @@ See [Validator Inclusion APIs](./validator-inclusion.md). ### `/lighthouse/eth1/syncing` -Returns information regarding the Eth1 network, as it is required for use in -Eth2 +Returns information regarding execution layer, as it is required for use in +consensus layer #### Fields - `head_block_number`, `head_block_timestamp`: the block number and timestamp -from the very head of the Eth1 chain. Useful for understanding the immediate -health of the Eth1 node that the beacon node is connected to. +from the very head of the execution chain. Useful for understanding the immediate +health of the execution node that the beacon node is connected to. - `latest_cached_block_number` & `latest_cached_block_timestamp`: the block number and timestamp of the latest block we have in our block cache. - - For correct Eth1 voting this timestamp should be later than the + - For correct execution client voting this timestamp should be later than the `voting_period_start_timestamp`. -- `voting_target_timestamp`: The latest timestamp allowed for an eth1 block in this voting period. +- `voting_target_timestamp`: The latest timestamp allowed for an execution layer block in this voting period. - `eth1_node_sync_status_percentage` (float): An estimate of how far the head of the - Eth1 node is from the head of the Eth1 chain. - - `100.0` indicates a fully synced Eth1 node. - - `0.0` indicates an Eth1 node that has not verified any blocks past the + execution node is from the head of the execution chain. + - `100.0` indicates a fully synced execution node. + - `0.0` indicates an execution node that has not verified any blocks past the genesis block. - `lighthouse_is_cached_and_ready`: Is set to `true` if the caches in the beacon node are ready for block production. @@ -248,7 +248,7 @@ curl -X GET "http://localhost:5052/lighthouse/eth1/syncing" -H "accept: applica ### `/lighthouse/eth1/block_cache` -Returns a list of all the Eth1 blocks in the Eth1 voting cache. +Returns a list of all the execution layer blocks in the execution client voting cache. #### Example @@ -320,7 +320,7 @@ curl -X GET "http://localhost:5052/lighthouse/eth1/deposit_cache" -H "accept: a Obtains a `BeaconState` in SSZ bytes. Useful for obtaining a genesis state. -The `state_id` parameter is identical to that used in the [Standard Eth2.0 Beacon Node API +The `state_id` parameter is identical to that used in the [Standard Beacon Node API `beacon/state` routes](https://ethereum.github.io/beacon-APIs/#/Beacon/getStateRoot). diff --git a/book/src/api-vc-endpoints.md b/book/src/api-vc-endpoints.md index 1066d5ef3a..ae091130f3 100644 --- a/book/src/api-vc-endpoints.md +++ b/book/src/api-vc-endpoints.md @@ -6,7 +6,7 @@ HTTP Path | Description | | --- | -- | [`GET /lighthouse/version`](#get-lighthouseversion) | Get the Lighthouse software version. [`GET /lighthouse/health`](#get-lighthousehealth) | Get information about the host machine. -[`GET /lighthouse/spec`](#get-lighthousespec) | Get the Eth2 specification used by the validator. +[`GET /lighthouse/spec`](#get-lighthousespec) | Get the Ethereum proof-of-stake consensus specification used by the validator. [`GET /lighthouse/auth`](#get-lighthouseauth) | Get the location of the authorization token. [`GET /lighthouse/validators`](#get-lighthousevalidators) | List all validators. [`GET /lighthouse/validators/:voting_pubkey`](#get-lighthousevalidatorsvoting_pubkey) | Get a specific validator. @@ -79,7 +79,7 @@ Typical Responses | 200 ## `GET /lighthouse/spec` -Returns the Eth2 specification loaded for this validator. +Returns the Ethereum proof-of-stake consensus specification loaded for this validator. ### HTTP Specification diff --git a/book/src/api.md b/book/src/api.md index 56c1ff5ce0..f8c54ad9a9 100644 --- a/book/src/api.md +++ b/book/src/api.md @@ -1,6 +1,6 @@ # APIs -Lighthouse allows users to query the state of Eth2.0 using web-standard, +Lighthouse allows users to query the state of Ethereum consensus using web-standard, RESTful HTTP/JSON APIs. There are two APIs served by Lighthouse: diff --git a/book/src/cli.md b/book/src/cli.md index 60c87d5dcc..6540d3fc3a 100644 --- a/book/src/cli.md +++ b/book/src/cli.md @@ -1,6 +1,6 @@ # Command-Line Interface (CLI) -The `lighthouse` binary provides all necessary Ethereum 2.0 functionality. It +The `lighthouse` binary provides all necessary Ethereum consensus client functionality. It has two primary sub-commands: - `$ lighthouse beacon_node`: the largest and most fundamental component which connects to @@ -48,7 +48,7 @@ maintained by Sigma Prime. However, for developers, testnets can be created by following the instructions outlined in [testnets](./testnets.md). The steps listed here will create a -local database specified to a new testnet. +local database specified to a new testnet. ## Resuming from an existing database diff --git a/book/src/contributing.md b/book/src/contributing.md index da18439c86..9204ff8463 100644 --- a/book/src/contributing.md +++ b/book/src/contributing.md @@ -35,10 +35,10 @@ Lighthouse maintains two permanent branches: - [`unstable`][unstable]: Used for development, contains the latest PRs. - Developers should base thier PRs on this branch. -## Ethereum 2.0 +## Ethereum consensus client -Lighthouse is an implementation of the Ethereum 2.0 specification, as defined -in the [ethereum/eth2.0-specs](https://github.com/ethereum/eth2.0-specs) +Lighthouse is an implementation of the Ethereum proof-of-stake consensus specification, as defined +in the [ethereum/consensus-specs](https://github.com/ethereum/consensus-specs) repository. We recommend reading Danny Ryan's (incomplete) [Phase 0 for diff --git a/book/src/faq.md b/book/src/faq.md index 02a4bfea66..e14947fb05 100644 --- a/book/src/faq.md +++ b/book/src/faq.md @@ -12,68 +12,68 @@ ### Why does it take so long for a validator to be activated? -After validators create their Eth1 deposit transaction there are two waiting +After validators create their execution layer deposit transaction there are two waiting periods before they can start producing blocks and attestations: -1. Waiting for the beacon chain to recognise the Eth1 block containing the +1. Waiting for the beacon chain to recognise the execution layer block containing the deposit (generally 4 to 7.4 hours). 1. Waiting in the queue for validator activation (generally 6.4 minutes for every 4 validators in the queue). Detailed answers below: -#### 1. Waiting for the beacon chain to detect the Eth1 deposit +#### 1. Waiting for the beacon chain to detect the execution layer deposit -Since the beacon chain uses Eth1 for validator on-boarding, beacon chain +Since the beacon chain uses the execution layer for validator on-boarding, beacon chain validators must listen to event logs from the deposit contract. Since the -latest blocks of the Eth1 chain are vulnerable to re-orgs due to minor network -partitions, beacon nodes follow the Eth1 chain at a distance of 1,024 blocks +latest blocks of the execution chain are vulnerable to re-orgs due to minor network +partitions, beacon nodes follow the execution chain at a distance of 1,024 blocks (~4 hours) (see -[`ETH1_FOLLOW_DISTANCE`](https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#misc)). +[`ETH1_FOLLOW_DISTANCE`](https://github.com/ethereum/consensus-specs/blob/v0.12.1/specs/phase0/validator.md#misc)). This follow distance protects the beacon chain from on-boarding validators that -are likely to be removed due to an Eth1 re-org. +are likely to be removed due to an execution chain re-org. Now we know there's a 4 hours delay before the beacon nodes even _consider_ an -Eth1 block. Once they _are_ considering these blocks, there's a voting period -where beacon validators vote on which Eth1 to include in the beacon chain. This +execution layer block. Once they _are_ considering these blocks, there's a voting period +where beacon validators vote on which execution block hash to include in the beacon chain. This period is defined as 32 epochs (~3.4 hours, see -[`ETH1_VOTING_PERIOD`](https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#time-parameters)). +[`ETH1_VOTING_PERIOD`](https://github.com/ethereum/consensus-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#time-parameters)). During this voting period, each beacon block producer includes an -[`Eth1Data`](https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#eth1data) +[`Eth1Data`](https://github.com/ethereum/consensus-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#eth1data) in their block which counts as a vote towards what that validator considers to -be the head of the Eth1 chain at the start of the voting period (with respect +be the head of the execution chain at the start of the voting period (with respect to `ETH1_FOLLOW_DISTANCE`, of course). You can see the exact voting logic -[here](https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#eth1-data). +[here](https://github.com/ethereum/consensus-specs/blob/v0.12.1/specs/phase0/validator.md#eth1-data). -These two delays combined represent the time between an Eth1 deposit being -included in an Eth1 data vote and that validator appearing in the beacon chain. +These two delays combined represent the time between an execution layer deposit being +included in an execution data vote and that validator appearing in the beacon chain. The `ETH1_FOLLOW_DISTANCE` delay causes a minimum delay of ~4 hours and `ETH1_VOTING_PERIOD` means that if a validator deposit happens just _before_ the start of a new voting period then they might not notice this delay at all. However, if the validator deposit happens just _after_ the start of the new voting period the validator might have to wait ~3.4 hours for next voting period. In times of very, very severe network issues, the network may even fail -to vote in new Eth1 blocks, stopping all new validator deposits! +to vote in new execution layer blocks, stopping all new validator deposits! #### 2. Waiting for a validator to be activated If a validator has provided an invalid public key or signature, they will _never_ be activated. They will simply be forgotten by the beacon chain! But, if those parameters were -correct, once the Eth1 delays have elapsed and the validator appears in the +correct, once the execution layer delays have elapsed and the validator appears in the beacon chain, there's _another_ delay before the validator becomes "active" (canonical definition -[here](https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#is_active_validator)) and can start producing blocks and attestations. +[here](https://github.com/ethereum/consensus-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#is_active_validator)) and can start producing blocks and attestations. Firstly, the validator won't become active until their beacon chain balance is equal to or greater than -[`MAX_EFFECTIVE_BALANCE`](https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#gwei-values) +[`MAX_EFFECTIVE_BALANCE`](https://github.com/ethereum/consensus-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#gwei-values) (32 ETH on mainnet, usually 3.2 ETH on testnets). Once this balance is reached, the validator must wait until the start of the next epoch (up to 6.4 minutes) for the -[`process_registry_updates`](https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#registry-updates) +[`process_registry_updates`](https://github.com/ethereum/consensus-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#registry-updates) routine to run. This routine activates validators with respect to a [churn -limit](https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#get_validator_churn_limit); +limit](https://github.com/ethereum/consensus-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#get_validator_churn_limit); it will only allow the number of validators to increase (churn) by a certain amount. Up until there are about 330,000 validators this churn limit is set to 4 and it starts to very slowly increase as the number of validators increases @@ -161,7 +161,7 @@ Nov 30 21:04:28.268 WARN Syncing eth1 block cache est_blocks_remaining: initia ``` This log indicates that your beacon node is downloading blocks and deposits -from your eth1 node. When the `est_blocks_remaining` is +from your execution node. When the `est_blocks_remaining` is `initializing_deposits`, your node is downloading deposit logs. It may stay in this stage for several minutes. Once the deposits logs are finished downloading, the `est_blocks_remaining` value will start decreasing. @@ -170,7 +170,7 @@ It is perfectly normal to see this log when starting a node for the first time or after being off for more than several minutes. If this log continues appearing sporadically during operation, there may be an -issue with your eth1 endpoint. +issue with your execution client endpoint. ### Can I use redundancy in my staking setup? diff --git a/book/src/http.md b/book/src/http.md index 0f9cd124d6..82a688586b 100644 --- a/book/src/http.md +++ b/book/src/http.md @@ -13,7 +13,7 @@ The following CLI flags control the HTTP server: - `--http-port`: specify the listen port of the server. - `--http-address`: specify the listen address of the server. -The schema of the API aligns with the standard Eth2 Beacon Node API as defined +The schema of the API aligns with the standard Ethereum Beacon Node API as defined at [github.com/ethereum/beacon-APIs](https://github.com/ethereum/beacon-APIs). It is an easy-to-use RESTful HTTP/JSON API. An interactive specification is available [here](https://ethereum.github.io/beacon-APIs/). diff --git a/book/src/intro.md b/book/src/intro.md index b31deeef88..fca075892b 100644 --- a/book/src/intro.md +++ b/book/src/intro.md @@ -7,11 +7,11 @@ _Documentation for Lighthouse users and developers._ [Chat Badge]: https://img.shields.io/badge/chat-discord-%237289da [Chat Link]: https://discord.gg/cyAszAh -Lighthouse is an **Ethereum 2.0 client** that connects to other Ethereum 2.0 +Lighthouse is an **Ethereum consensus client** that connects to other Ethereum consensus clients to form a resilient and decentralized proof-of-stake blockchain. We implement the specification as defined in the -[ethereum/eth2.0-specs](https://github.com/ethereum/eth2.0-specs) repository. +[ethereum/consensus-specs](https://github.com/ethereum/consensus-specs) repository. ## Topics diff --git a/book/src/key-management.md b/book/src/key-management.md index 23d11d550c..30d649f346 100644 --- a/book/src/key-management.md +++ b/book/src/key-management.md @@ -3,7 +3,7 @@ [launchpad]: https://launchpad.ethereum.org/ > -> **Note: we recommend using the [Eth2 launchpad][launchpad] to create validators.** +> **Note: we recommend using the [Staking launchpad][launchpad] to create validators.** Lighthouse uses a _hierarchical_ key management system for producing validator keys. It is hierarchical because each validator key can be _derived_ from a @@ -92,7 +92,7 @@ leaking private key data. ### Withdrawal Keypairs -In Eth2 Phase 0, withdrawal keypairs do not serve any immediate purpose. +In Ethereum consensus Phase 0, withdrawal keypairs do not serve any immediate purpose. However, they become very important _after_ Phase 0: they will provide the ultimate control of the ETH of withdrawn validators. diff --git a/book/src/key-recovery.md b/book/src/key-recovery.md index e45887c29c..2474d123ca 100644 --- a/book/src/key-recovery.md +++ b/book/src/key-recovery.md @@ -48,7 +48,7 @@ which contains all the information necessary to run a validator using the `lighthouse vc` command. The password to this new keystore will be placed in the `--secrets-dir` (default `~/.lighthouse/{network}/secrets`). -where `network` is the name of the Eth2 network passed in the `--network` parameter (default is `mainnet`). +where `network` is the name of the consensus layer network passed in the `--network` parameter (default is `mainnet`). ## Recover a EIP-2386 wallet diff --git a/book/src/mainnet-validator.md b/book/src/mainnet-validator.md index c5881c6066..0f91b8e272 100644 --- a/book/src/mainnet-validator.md +++ b/book/src/mainnet-validator.md @@ -1,4 +1,4 @@ -# Become an Eth2 Mainnet Validator +# Become an Ethereum Consensus Mainnet Validator [launchpad]: https://launchpad.ethereum.org/ [lh-book]: https://lighthouse-book.sigmaprime.io/ @@ -8,18 +8,18 @@ [slashing]: ./slashing-protection.md [discord]: https://discord.gg/cyAszAh -Becoming an Eth2 validator is rewarding, but it's not for the faint of heart. You'll need to be +Becoming an Ethereum consensus validator is rewarding, but it's not for the faint of heart. You'll need to be familiar with the rules of staking (e.g., rewards, penalties, etc.) and also configuring and managing servers. You'll also need at least 32 ETH! -For those with an understanding of Eth2 and server maintenance, you'll find that running Lighthouse +For those with an understanding of Ethereum consensus and server maintenance, you'll find that running Lighthouse is easy. Install it, start it, monitor it and keep it updated. You shouldn't need to interact with it on a day-to-day basis. Being educated is critical to validator success. Before submitting your mainnet deposit, we recommend: -- Thoroughly exploring the [Eth2 Launchpad][launchpad] website +- Thoroughly exploring the [Staking Launchpad][launchpad] website - Try running through the deposit process *without* actually submitting a deposit. - Reading through this documentation, especially the [Slashing Protection][slashing] section. - Running a [testnet validator][testnet-validator]. @@ -37,7 +37,7 @@ Remember, if you get stuck you can always reach out on our [Discord][discord]. > occured through the use of Lighthouse. We have an experienced internal security team and have > undergone multiple third-party security-reviews, however the possibility of bugs or malicious > interference remains a real and constant threat. Validators should be prepared to lose some rewards -> due to the actions of other actors on the Eth2 network or software bugs. See the +> due to the actions of other actors on the consensus layer or software bugs. See the > [software license][license] for more detail on liability. ## Using Lighthouse for Mainnet @@ -57,7 +57,7 @@ provide a `--network` flag instead of relying on the default. There are five primary steps to become a testnet validator: 1. Create validator keys and submit deposits. -1. Start an Eth1 client. +1. Start an execution client. 1. Install Lighthouse. 1. Import the validator keys into Lighthouse. 1. Start Lighthouse. @@ -68,10 +68,10 @@ setting aside one or two hours for this process. ### Step 1. Create validator keys -The Ethereum Foundation provides an "Eth2 launch pad" for creating validator keypairs and submitting +The Ethereum Foundation provides a "Staking Launchpad" for creating validator keypairs and submitting deposits: -- [Eth2 Launchpad][launchpad] +- [Staking Launchpad][launchpad] Please follow the steps on the launch pad site to generate validator keys and submit deposits. Make sure you select "Lighthouse" as your client. @@ -79,10 +79,10 @@ sure you select "Lighthouse" as your client. Move to the next step once you have completed the steps on the launch pad, including generating keys via the Python CLI and submitting gETH/ETH deposits. -### Step 2. Start an Eth1 client +### Step 2. Start an execution client -Since Eth2 relies upon the Eth1 chain for validator on-boarding, all Eth2 validators must have a -connection to an Eth1 node. +Since the consensus chain relies upon the execution chain for validator on-boarding, all consensus validators must have a +connection to an execution client. We provide instructions for using Geth, but you could use any client that implements the JSON RPC via HTTP. A fast-synced node is sufficient. @@ -95,7 +95,7 @@ geth. Otherwise [see here](https://github.com/ethereum/go-ethereum/wiki/Installi #### Starting Geth -Once you have geth installed, use this command to start your Eth1 node: +Once you have geth installed, use this command to start your execution node: ```bash geth --http @@ -116,7 +116,7 @@ its `--version` info. ### Step 4. Import validator keys to Lighthouse -When Lighthouse is installed, follow the [Importing from the Ethereum 2.0 Launch +When Lighthouse is installed, follow the [Importing from the Ethereum Staking Launch pad](./validator-import-launchpad.md) instructions so the validator client can perform your validator duties. diff --git a/book/src/redundancy.md b/book/src/redundancy.md index b01a01dd26..3409effb36 100644 --- a/book/src/redundancy.md +++ b/book/src/redundancy.md @@ -5,7 +5,7 @@ There are three places in Lighthouse where redundancy is notable: 1. ✅ GOOD: Using a redundant Beacon node in `lighthouse vc --beacon-nodes` -1. ✅ GOOD: Using a redundant Eth1 node in `lighthouse bn --eth1-endpoints` +1. ✅ GOOD: Using a redundant execution node in `lighthouse bn --eth1-endpoints` 1. ☠️ BAD: Running redundant `lighthouse vc` instances with overlapping keypairs. I mention (3) since it is unsafe and should not be confused with the other two @@ -55,7 +55,7 @@ In our previous example we listed `http://192.168.1.1:5052` as a redundant node. Apart from having sufficient resources, the backup node should have the following flags: -- `--staking`: starts the HTTP API server and ensures the Eth1 chain is synced. +- `--staking`: starts the HTTP API server and ensures the execution chain is synced. - `--http-address 0.0.0.0`: this allows *any* external IP address to access the HTTP server (a firewall should be configured to deny unauthorized access to port `5052`). This is only required if your backup node is on a different host. @@ -92,23 +92,23 @@ There are 64 subnets and each validator will result in a subscription to *at least* one subnet. So, using the two aforementioned flags will result in resource consumption akin to running 64+ validators. -## Redundant Eth1 nodes +## Redundant execution nodes -Compared to redundancy in beacon nodes (see above), using redundant Eth1 nodes +Compared to redundancy in beacon nodes (see above), using redundant execution nodes is very straight-forward: 1. `lighthouse bn --eth1-endpoints http://localhost:8545` 1. `lighthouse bn --eth1-endpoints http://localhost:8545,http://192.168.0.1:8545` In the case of (1), any failure on `http://localhost:8545` will result in a -failure to update the Eth1 cache in the beacon node. Consistent failure over a +failure to update the execution client cache in the beacon node. Consistent failure over a period of hours may result in a failure in block production. -However, in the case of (2), the `http://192.168.0.1:8545` Eth1 endpoint will -be tried each time the first fails. Eth1 endpoints will be tried from first to +However, in the case of (2), the `http://192.168.0.1:8545` execution client endpoint will +be tried each time the first fails. Execution client endpoints will be tried from first to last in the list, until a successful response is obtained. -There is no need for special configuration on the Eth1 endpoint, all endpoints can (probably should) +There is no need for special configuration on the execution client endpoint, all endpoints can (probably should) be configured identically. > Note: When supplying multiple endpoints the `http://localhost:8545` address must be explicitly diff --git a/book/src/setup.md b/book/src/setup.md index e4e1d92ff5..dfff9290e6 100644 --- a/book/src/setup.md +++ b/book/src/setup.md @@ -9,9 +9,9 @@ particularly useful for development but still a good way to ensure you have the base dependencies. The additional requirements for developers are: -- [`ganache-cli`](https://github.com/trufflesuite/ganache-cli). This is used to - simulate the Eth1 chain during tests. You'll get failures during tests if you - don't have `ganache-cli` available on your `PATH`. +- [`ganache v7`](https://github.com/trufflesuite/ganache). This is used to + simulate the execution chain during tests. You'll get failures during tests if you + don't have `ganache` available on your `PATH` or if ganache is older than v7. - [`cmake`](https://cmake.org/cmake/help/latest/command/install.html). Used by some dependencies. See [`Installation Guide`](./installation.md) for more info. - [`java 11 runtime`](https://openjdk.java.net/projects/jdk/). 11 is the minimum, diff --git a/book/src/slashing-protection.md b/book/src/slashing-protection.md index a9bd316453..9ae6c102e3 100644 --- a/book/src/slashing-protection.md +++ b/book/src/slashing-protection.md @@ -1,6 +1,6 @@ # Slashing Protection -The security of Ethereum 2.0's proof of stake protocol depends on penalties for misbehaviour, known +The security of the Ethereum proof-of-stake protocol depends on penalties for misbehaviour, known as _slashings_. Validators that sign conflicting messages (blocks or attestations), can be slashed by other validators through the inclusion of a `ProposerSlashing` or `AttesterSlashing` on chain. diff --git a/book/src/suggested-fee-recipient.md b/book/src/suggested-fee-recipient.md index 6513495fe4..3ff71ec7d6 100644 --- a/book/src/suggested-fee-recipient.md +++ b/book/src/suggested-fee-recipient.md @@ -12,7 +12,7 @@ There is no guarantee that an execution node will use the `suggested_fee_recipie it may use any address it chooses. It is assumed that an honest execution node *will* use the `suggested_fee_recipient`, but users should note this trust assumption. -The `suggested_fee_recipient` can be provided to the VC, who will transmit it to the BN. The also BN +The `suggested_fee_recipient` can be provided to the VC, who will transmit it to the BN. The BN also has a choice regarding the fee recipient it passes to the execution node, creating another noteworthy trust assumption. diff --git a/book/src/testnet-validator.md b/book/src/testnet-validator.md index 0bcd58c9ac..98ba66c244 100644 --- a/book/src/testnet-validator.md +++ b/book/src/testnet-validator.md @@ -3,17 +3,17 @@ [mainnet-validator]: ./mainnet-validator.md [prater-launchpad]: https://prater.launchpad.ethereum.org/ -Joining an Eth2 testnet is a great way to get familiar with staking in Phase 0. All users should +Joining an Ethereum consensus testnet is a great way to get familiar with staking in Phase 0. All users should experiment with a testnet prior to staking mainnet ETH. -To join a testnet, you can follow the [Become an Eth2 Mainnet Validator][mainnet-validator] +To join a testnet, you can follow the [Become an Ethereum consensus Mainnet Validator][mainnet-validator] instructions but with a few differences: -1. Use the appropriate Eth2 launchpad website: +1. Use the appropriate Staking launchpad website: - [Prater][prater-launchpad] 1. Instead of `--network mainnet`, use the appropriate network flag: - `--network prater`: Prater. -1. Use a Goerli Eth1 node instead of a mainnet one: +1. Use a Goerli execution node instead of a mainnet one: - For Geth, this means using `geth --goerli --http`. 1. Notice that Lighthouse will store its files in a different directory by default: - `~/.lighthouse/prater`: Prater. diff --git a/book/src/validator-create.md b/book/src/validator-create.md index e7c316a95f..f13c449b9f 100644 --- a/book/src/validator-create.md +++ b/book/src/validator-create.md @@ -3,7 +3,7 @@ [launchpad]: https://launchpad.ethereum.org/ > -> **Note: we recommend using the [Eth2 launchpad][launchpad] to create validators.** +> **Note: we recommend using the [Staking launchpad][launchpad] to create validators.** Validators are fundamentally represented by a BLS keypair. In Lighthouse, we use a [wallet](./wallet-create.md) to generate these keypairs. Once a wallet diff --git a/book/src/validator-doppelganger.md b/book/src/validator-doppelganger.md index 3fa29043d3..d880cce0ae 100644 --- a/book/src/validator-doppelganger.md +++ b/book/src/validator-doppelganger.md @@ -6,7 +6,7 @@ From Lighthouse `v1.5.0`, the *Doppelganger Protection* feature is available for the Validator Client. Taken from the German *[doppelgänger]*, which translates literally to "double-walker", a -"doppelganger" in Eth2 refers to another instance of a validator running in a separate validator +"doppelganger" in the context of Ethereum proof-of-stake refers to another instance of a validator running in a separate validator process. As detailed in [Slashing Protection], running the same validator twice will inevitably result in slashing. diff --git a/book/src/validator-import-launchpad.md b/book/src/validator-import-launchpad.md index 6bde24732a..aee9ac7b96 100644 --- a/book/src/validator-import-launchpad.md +++ b/book/src/validator-import-launchpad.md @@ -1,9 +1,9 @@ -# Importing from the Ethereum 2.0 Launch pad +# Importing from the Ethereum Staking Launch pad -The [Eth2 Lauchpad](https://github.com/ethereum/eth2.0-deposit) is a website +The [Staking Lauchpad](https://github.com/ethereum/eth2.0-deposit) is a website from the Ethereum Foundation which guides users how to use the [`eth2.0-deposit-cli`](https://github.com/ethereum/eth2.0-deposit-cli) -command-line program to generate Eth2 validator keys. +command-line program to generate consensus validator keys. The keys that are generated from `eth2.0-deposit-cli` can be easily loaded into a Lighthouse validator client (`lighthouse vc`). In fact, both of these @@ -20,7 +20,7 @@ Whilst following the steps on the website, users are instructed to download the repository. This `eth2-deposit-cli` script will generate the validator BLS keys into a `validator_keys` directory. We assume that the user's present-working-directory is the `eth2-deposit-cli` repository (this is where -you will be if you just ran the `./deposit.sh` script from the Eth2 Launch pad +you will be if you just ran the `./deposit.sh` script from the Staking Launch pad website). If this is not the case, simply change the `--directory` to point to the `validator_keys` directory. @@ -38,7 +38,7 @@ section, all other users can use: lighthouse --network mainnet account validator import --directory validator_keys ``` -Note: The user must specify the Eth2 network that they are importing the keys for using the `--network` flag. +Note: The user must specify the consensus client network that they are importing the keys for using the `--network` flag. After which they will be prompted for a password for each keystore discovered: diff --git a/book/src/validator-inclusion.md b/book/src/validator-inclusion.md index 67e17fecad..e6fbc0f16f 100644 --- a/book/src/validator-inclusion.md +++ b/book/src/validator-inclusion.md @@ -4,7 +4,7 @@ The `/lighthouse/validator_inclusion` API endpoints provide information on results of the proof-of-stake voting process used for finality/justification under Casper FFG. -These endpoints are not stable or included in the Eth2 standard API. As such, +These endpoints are not stable or included in the Ethereum consensus standard API. As such, they are subject to change or removal without a change in major release version. diff --git a/book/src/validator-web3signer.md b/book/src/validator-web3signer.md index e655040950..2de641d48b 100644 --- a/book/src/validator-web3signer.md +++ b/book/src/validator-web3signer.md @@ -52,5 +52,5 @@ filesystem of the VC) to encrypt the communications between the VC and Web3Signe > The `request_timeout_ms` key can also be specified. Use this key to override the default timeout > with a new timeout in milliseconds. This is the timeout before requests to Web3Signer are -> considered to be failures. Setting a value that it too-long may create contention and late duties +> considered to be failures. Setting a value that is too long may create contention and late duties > in the VC. Setting it too short will result in failed signatures and therefore missed duties. diff --git a/book/src/voluntary-exit.md b/book/src/voluntary-exit.md index 593bc9969b..69c2d7598b 100644 --- a/book/src/voluntary-exit.md +++ b/book/src/voluntary-exit.md @@ -15,7 +15,7 @@ This number can be much higher depending on how many other validators are queued Even though users can perform a voluntary exit in phase 0, they **cannot withdraw their exited funds at this point in time**. This implies that the staked funds are effectively **frozen** until withdrawals are enabled in future phases. -To understand the phased rollout strategy for Eth2, please visit . +To understand the phased rollout strategy for Ethereum upgrades, please visit . @@ -25,7 +25,7 @@ In order to initiate an exit, users can use the `lighthouse account validator ex - The `--keystore` flag is used to specify the path to the EIP-2335 voting keystore for the validator. -- The `--beacon-node` flag is used to specify a beacon chain HTTP endpoint that confirms to the [Eth2.0 Standard Beacon Node API](https://ethereum.github.io/beacon-APIs/) specifications. That beacon node will be used to validate and propagate the voluntary exit. The default value for this flag is `http://localhost:5052`. +- The `--beacon-node` flag is used to specify a beacon chain HTTP endpoint that confirms to the [Beacon Node API](https://ethereum.github.io/beacon-APIs/) specifications. That beacon node will be used to validate and propagate the voluntary exit. The default value for this flag is `http://localhost:5052`. - The `--network` flag is used to specify a particular Eth2 network (default is `mainnet`). diff --git a/book/src/wallet-create.md b/book/src/wallet-create.md index 17cac248b9..0ebb449177 100644 --- a/book/src/wallet-create.md +++ b/book/src/wallet-create.md @@ -3,7 +3,7 @@ [launchpad]: https://launchpad.ethereum.org/ > -> **Note: we recommend using the [Eth2 launchpad][launchpad] to create validators.** +> **Note: we recommend using the [Staking launchpad][launchpad] to create validators.** A wallet allows for generating practically unlimited validators from an easy-to-remember 24-word string (a mnemonic). As long as that mnemonic is diff --git a/boot_node/Cargo.toml b/boot_node/Cargo.toml index 8c89ab2e4e..b1a2e08d3d 100644 --- a/boot_node/Cargo.toml +++ b/boot_node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "boot_node" -version = "2.1.3" +version = "2.1.5" authors = ["Sigma Prime "] edition = "2021" @@ -23,4 +23,5 @@ hex = "0.4.2" serde = "1.0.116" serde_derive = "1.0.116" serde_json = "1.0.66" +serde_yaml = "0.8.13" eth2_network_config = { path = "../common/eth2_network_config" } diff --git a/boot_node/src/lib.rs b/boot_node/src/lib.rs index 6b933013fc..f4391f987a 100644 --- a/boot_node/src/lib.rs +++ b/boot_node/src/lib.rs @@ -3,8 +3,6 @@ use clap::ArgMatches; use slog::{o, Drain, Level, Logger}; use eth2_network_config::Eth2NetworkConfig; -use std::fs::File; -use std::path::PathBuf; mod cli; pub mod config; mod server; @@ -86,15 +84,13 @@ fn main( // parse the CLI args into a useable config let config: BootNodeConfig = BootNodeConfig::new(bn_matches, eth2_network_config)?; - // Dump config if `dump-config` flag is set - let dump_config = clap_utils::parse_optional::(lh_matches, "dump-config")?; - if let Some(dump_path) = dump_config { - let config_sz = BootNodeConfigSerialization::from_config_ref(&config); - let mut file = File::create(dump_path) - .map_err(|e| format!("Failed to create dumped config: {:?}", e))?; - serde_json::to_writer(&mut file, &config_sz) - .map_err(|e| format!("Error serializing config: {:?}", e))?; - } + // Dump configs if `dump-config` or `dump-chain-config` flags are set + let config_sz = BootNodeConfigSerialization::from_config_ref(&config); + clap_utils::check_dump_configs::<_, T>( + lh_matches, + &config_sz, + ð2_network_config.chain_spec::()?, + )?; // Run the boot node if !lh_matches.is_present("immediate-shutdown") { diff --git a/bors.toml b/bors.toml index 3c6826a9e8..d7d1e98762 100644 --- a/bors.toml +++ b/bors.toml @@ -13,7 +13,13 @@ status = [ "clippy", "arbitrary-check", "cargo-audit", - "cargo-udeps" + "cargo-udeps", + "beacon-chain-tests", + "op-pool-tests", + "doppelganger-protection-test", + "execution-engine-integration-ubuntu", + "cargo-vendor", + "check-msrv" ] use_squash_merge = true timeout_sec = 10800 diff --git a/common/account_utils/Cargo.toml b/common/account_utils/Cargo.toml index 4b42e039df..02333ff599 100644 --- a/common/account_utils/Cargo.toml +++ b/common/account_utils/Cargo.toml @@ -18,6 +18,6 @@ serde_yaml = "0.8.13" slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_trace"] } types = { path = "../../consensus/types" } validator_dir = { path = "../validator_dir" } -regex = "1.3.9" +regex = "1.5.5" rpassword = "5.0.0" directory = { path = "../directory" } diff --git a/common/account_utils/src/validator_definitions.rs b/common/account_utils/src/validator_definitions.rs index 6a3f5a6099..4652370c38 100644 --- a/common/account_utils/src/validator_definitions.rs +++ b/common/account_utils/src/validator_definitions.rs @@ -10,7 +10,7 @@ use regex::Regex; use serde_derive::{Deserialize, Serialize}; use slog::{error, Logger}; use std::collections::HashSet; -use std::fs::{self, OpenOptions}; +use std::fs::{self, File}; use std::io; use std::path::{Path, PathBuf}; use types::{graffiti::GraffitiString, Address, PublicKey}; @@ -162,7 +162,7 @@ impl ValidatorDefinitions { /// Open an existing file, returning an error if the file does not exist. pub fn open>(validators_dir: P) -> Result { let config_path = validators_dir.as_ref().join(CONFIG_FILENAME); - let file = OpenOptions::new() + let file = File::options() .write(true) .read(true) .create_new(false) @@ -219,7 +219,7 @@ impl ValidatorDefinitions { return None; } - let keystore_result = OpenOptions::new() + let keystore_result = File::options() .read(true) .create(false) .open(&voting_keystore_path) diff --git a/common/clap_utils/Cargo.toml b/common/clap_utils/Cargo.toml index 0aa35b2333..b370eb0825 100644 --- a/common/clap_utils/Cargo.toml +++ b/common/clap_utils/Cargo.toml @@ -13,3 +13,7 @@ dirs = "3.0.1" eth2_network_config = { path = "../eth2_network_config" } eth2_ssz = "0.4.1" ethereum-types = "0.12.1" +serde = "1.0.116" +serde_json = "1.0.59" +serde_yaml = "0.8.13" +types = { path = "../../consensus/types"} diff --git a/common/clap_utils/src/lib.rs b/common/clap_utils/src/lib.rs index 3dd42f2a99..1ebd2b1740 100644 --- a/common/clap_utils/src/lib.rs +++ b/common/clap_utils/src/lib.rs @@ -6,6 +6,7 @@ use ethereum_types::U256 as Uint256; use ssz::Decode; use std::path::PathBuf; use std::str::FromStr; +use types::{ChainSpec, Config, EthSpec}; pub mod flags; @@ -52,6 +53,12 @@ pub fn get_eth2_network_config(cli_args: &ArgMatches) -> Result( }) .transpose() } + +/// Writes configs to file if `dump-config` or `dump-chain-config` flags are set +pub fn check_dump_configs( + matches: &ArgMatches, + config: S, + spec: &ChainSpec, +) -> Result<(), String> +where + S: serde::Serialize, + E: EthSpec, +{ + if let Some(dump_path) = parse_optional::(matches, "dump-config")? { + let mut file = std::fs::File::create(dump_path) + .map_err(|e| format!("Failed to open file for writing config: {:?}", e))?; + serde_json::to_writer(&mut file, &config) + .map_err(|e| format!("Error serializing config: {:?}", e))?; + } + if let Some(dump_path) = parse_optional::(matches, "dump-chain-config")? { + let chain_config = Config::from_chain_spec::(spec); + let mut file = std::fs::File::create(dump_path) + .map_err(|e| format!("Failed to open file for writing chain config: {:?}", e))?; + serde_yaml::to_writer(&mut file, &chain_config) + .map_err(|e| format!("Error serializing config: {:?}", e))?; + } + Ok(()) +} diff --git a/common/eth2_config/src/lib.rs b/common/eth2_config/src/lib.rs index 7c1f5810fd..7992955dc4 100644 --- a/common/eth2_config/src/lib.rs +++ b/common/eth2_config/src/lib.rs @@ -236,5 +236,6 @@ macro_rules! define_hardcoded_nets { define_hardcoded_nets!( (mainnet, "mainnet", GENESIS_STATE_IS_KNOWN), (prater, "prater", GENESIS_STATE_IS_KNOWN), - (gnosis, "gnosis", GENESIS_STATE_IS_KNOWN) + (gnosis, "gnosis", GENESIS_STATE_IS_KNOWN), + (kiln, "kiln", GENESIS_STATE_IS_KNOWN) ); diff --git a/common/eth2_network_config/built_in_network_configs/kiln/boot_enr.yaml b/common/eth2_network_config/built_in_network_configs/kiln/boot_enr.yaml new file mode 100644 index 0000000000..4c03b0f19e --- /dev/null +++ b/common/eth2_network_config/built_in_network_configs/kiln/boot_enr.yaml @@ -0,0 +1,3 @@ +- enr:-Iq4QMCTfIMXnow27baRUb35Q8iiFHSIDBJh6hQM5Axohhf4b6Kr_cOCu0htQ5WvVqKvFgY28893DHAg8gnBAXsAVqmGAX53x8JggmlkgnY0gmlwhLKAlv6Jc2VjcDI1NmsxoQK6S-Cii_KmfFdUJL2TANL3ksaKUnNXvTCv1tLwXs0QgIN1ZHCCIyk +- enr:-KG4QFkPJUFWuONp5grM94OJvNht9wX6N36sA4wqucm6Z02ECWBQRmh6AzndaLVGYBHWre67mjK-E0uKt2CIbWrsZ_8DhGV0aDKQc6pfXHAAAHAyAAAAAAAAAIJpZIJ2NIJpcISl6LTmiXNlY3AyNTZrMaEDHlSNOgYrNWP8_l_WXqDMRvjv6gUAvHKizfqDDVc8feaDdGNwgiMog3VkcIIjKA +- enr:-MK4QI-wkVW1PxL4ksUM4H_hMgTTwxKMzvvDMfoiwPBuRxcsGkrGPLo4Kho3Ri1DEtJG4B6pjXddbzA9iF2gVctxv42GAX9v5WG5h2F0dG5ldHOIAAAAAAAAAACEZXRoMpBzql9ccAAAcDIAAAAAAAAAgmlkgnY0gmlwhKRcjMiJc2VjcDI1NmsxoQK1fc46pmVHKq8HNYLkSVaUv4uK2UBsGgjjGWU6AAhAY4hzeW5jbmV0cwCDdGNwgiMog3VkcIIjKA diff --git a/common/eth2_network_config/built_in_network_configs/kiln/config.yaml b/common/eth2_network_config/built_in_network_configs/kiln/config.yaml new file mode 100644 index 0000000000..797c0672c3 --- /dev/null +++ b/common/eth2_network_config/built_in_network_configs/kiln/config.yaml @@ -0,0 +1,69 @@ +# Extends the mainnet preset +CONFIG_NAME: 'kiln' +PRESET_BASE: 'mainnet' +# Genesis +# --------------------------------------------------------------- +MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 95000 +# Mar 11th, 2022, 14:00 UTC +MIN_GENESIS_TIME: 1647007200 +# Gensis fork +GENESIS_FORK_VERSION: 0x70000069 +# 300 seconds (5 min) +GENESIS_DELAY: 300 + + +# Forking +# --------------------------------------------------------------- +# Some forks are disabled for now: +# - These may be re-assigned to another fork-version later +# - Temporarily set to max uint64 value: 2**64 - 1 + +# Altair +ALTAIR_FORK_VERSION: 0x70000070 +ALTAIR_FORK_EPOCH: 50 +# Bellatrix +BELLATRIX_FORK_VERSION: 0x70000071 +BELLATRIX_FORK_EPOCH: 150 +TERMINAL_TOTAL_DIFFICULTY: 20000000000000 +TERMINAL_BLOCK_HASH: 0x0000000000000000000000000000000000000000000000000000000000000000 +TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH: 18446744073709551615 + +# Sharding +SHARDING_FORK_VERSION: 0x03000000 +SHARDING_FORK_EPOCH: 18446744073709551615 + + +# Time parameters +# --------------------------------------------------------------- +# 12 seconds +SECONDS_PER_SLOT: 12 +# 14 (estimate from Eth1 mainnet) +SECONDS_PER_ETH1_BLOCK: 14 +# 2**8 (= 256) epochs ~27 hours +MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256 +# 2**8 (= 256) epochs ~27 hours +SHARD_COMMITTEE_PERIOD: 256 +# 16 blocks is ~190s +ETH1_FOLLOW_DISTANCE: 16 + + +# Validator cycle +# --------------------------------------------------------------- +# 2**2 (= 4) +INACTIVITY_SCORE_BIAS: 4 +# 2**4 (= 16) +INACTIVITY_SCORE_RECOVERY_RATE: 16 +# 2**4 * 10**9 (= 16,000,000,000) Gwei +EJECTION_BALANCE: 16000000000 +# 2**2 (= 4) +MIN_PER_EPOCH_CHURN_LIMIT: 4 +# 2**16 (= 65,536) +CHURN_LIMIT_QUOTIENT: 65536 + + +# Deposit contract +# --------------------------------------------------------------- +# Custom Ethereum testnet +DEPOSIT_CHAIN_ID: 1337802 +DEPOSIT_NETWORK_ID: 1337802 +DEPOSIT_CONTRACT_ADDRESS: 0x4242424242424242424242424242424242424242 diff --git a/common/eth2_network_config/built_in_network_configs/kiln/deploy_block.txt b/common/eth2_network_config/built_in_network_configs/kiln/deploy_block.txt new file mode 100644 index 0000000000..c227083464 --- /dev/null +++ b/common/eth2_network_config/built_in_network_configs/kiln/deploy_block.txt @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/common/eth2_network_config/built_in_network_configs/kiln/genesis.ssz.zip b/common/eth2_network_config/built_in_network_configs/kiln/genesis.ssz.zip new file mode 100644 index 0000000000..309b323a5b Binary files /dev/null and b/common/eth2_network_config/built_in_network_configs/kiln/genesis.ssz.zip differ diff --git a/common/eth2_wallet_manager/src/filesystem.rs b/common/eth2_wallet_manager/src/filesystem.rs index 7c43199751..131b218c7c 100644 --- a/common/eth2_wallet_manager/src/filesystem.rs +++ b/common/eth2_wallet_manager/src/filesystem.rs @@ -2,7 +2,7 @@ use eth2_wallet::Error as WalletError; use eth2_wallet::{Uuid, Wallet}; -use std::fs::{copy as copy_file, remove_file, OpenOptions}; +use std::fs::{copy as copy_file, remove_file, File}; use std::io; use std::path::{Path, PathBuf}; @@ -27,7 +27,7 @@ pub fn read>(wallet_dir: P, uuid: &Uuid) -> Result if !json_path.exists() { Err(Error::WalletDoesNotExist(json_path)) } else { - OpenOptions::new() + File::options() .read(true) .create(false) .open(json_path) @@ -79,7 +79,7 @@ pub fn create>(wallet_dir: P, wallet: &Wallet) -> Result<(), Erro if json_path.exists() { Err(Error::WalletAlreadyExists(json_path)) } else { - OpenOptions::new() + File::options() .write(true) .create_new(true) .open(json_path) diff --git a/common/eth2_wallet_manager/src/wallet_manager.rs b/common/eth2_wallet_manager/src/wallet_manager.rs index 57773595b4..3dd419a48b 100644 --- a/common/eth2_wallet_manager/src/wallet_manager.rs +++ b/common/eth2_wallet_manager/src/wallet_manager.rs @@ -6,7 +6,7 @@ use eth2_wallet::{bip39::Mnemonic, Error as WalletError, Uuid, Wallet, WalletBui use lockfile::LockfileError; use std::collections::HashMap; use std::ffi::OsString; -use std::fs::{create_dir_all, read_dir, OpenOptions}; +use std::fs::{create_dir_all, read_dir, File}; use std::io; use std::path::{Path, PathBuf}; @@ -172,7 +172,7 @@ impl WalletManager { // Ignore any paths that don't parse as a UUID. if let Ok(uuid) = Uuid::parse_str(&file_name) { let wallet_path = f.path().join(format!("{}", uuid)); - let wallet = OpenOptions::new() + let wallet = File::options() .read(true) .create(false) .open(wallet_path) diff --git a/common/lighthouse_version/Cargo.toml b/common/lighthouse_version/Cargo.toml index 782344e9ca..96ac266476 100644 --- a/common/lighthouse_version/Cargo.toml +++ b/common/lighthouse_version/Cargo.toml @@ -11,4 +11,4 @@ git-version = "0.3.4" target_info = "0.1.0" [dev-dependencies] -regex = "1" +regex = "1.5.5" diff --git a/common/lighthouse_version/src/lib.rs b/common/lighthouse_version/src/lib.rs index 5bf4e547e8..80bf56a725 100644 --- a/common/lighthouse_version/src/lib.rs +++ b/common/lighthouse_version/src/lib.rs @@ -16,7 +16,7 @@ pub const VERSION: &str = git_version!( // NOTE: using --match instead of --exclude for compatibility with old Git "--match=thiswillnevermatchlol" ], - prefix = "Lighthouse/v2.1.3-", + prefix = "Lighthouse/v2.1.5-", fallback = "unknown" ); diff --git a/common/lockfile/src/lib.rs b/common/lockfile/src/lib.rs index adb8be7bb7..cc622e0fb7 100644 --- a/common/lockfile/src/lib.rs +++ b/common/lockfile/src/lib.rs @@ -1,5 +1,5 @@ use fs2::FileExt; -use std::fs::{self, File, OpenOptions}; +use std::fs::{self, File}; use std::io::{self, ErrorKind}; use std::path::{Path, PathBuf}; @@ -30,7 +30,7 @@ impl Lockfile { let file = if file_existed { File::open(&path) } else { - OpenOptions::new() + File::options() .read(true) .write(true) .create_new(true) diff --git a/common/logging/src/lib.rs b/common/logging/src/lib.rs index eab8e326b6..85c4255744 100644 --- a/common/logging/src/lib.rs +++ b/common/logging/src/lib.rs @@ -7,9 +7,13 @@ use lighthouse_metrics::{ use slog::Logger; use slog_term::Decorator; use std::io::{Result, Write}; +use std::time::{Duration, Instant}; pub const MAX_MESSAGE_WIDTH: usize = 40; +/// The minimum interval between log messages indicating that a queue is full. +const LOG_DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30); + lazy_static! { pub static ref INFOS_TOTAL: MetricsResult = try_create_int_counter("info_total", "Count of infos logged"); @@ -187,6 +191,25 @@ fn is_ascii_control(character: &u8) -> bool { ) } +/// Provides de-bounce functionality for logging. +#[derive(Default)] +pub struct TimeLatch(Option); + +impl TimeLatch { + /// Only returns true once every `LOG_DEBOUNCE_INTERVAL`. + pub fn elapsed(&mut self) -> bool { + let now = Instant::now(); + + let is_elapsed = self.0.map_or(false, |elapse_time| now > elapse_time); + + if is_elapsed || self.0.is_none() { + self.0 = Some(now + LOG_DEBOUNCE_INTERVAL); + } + + is_elapsed + } +} + /// Return a logger suitable for test usage. /// /// By default no logs will be printed, but they can be enabled via diff --git a/common/monitoring_api/Cargo.toml b/common/monitoring_api/Cargo.toml index 4196f8ccea..bfb5e72042 100644 --- a/common/monitoring_api/Cargo.toml +++ b/common/monitoring_api/Cargo.toml @@ -19,5 +19,5 @@ lighthouse_metrics = { path = "../lighthouse_metrics" } slog = "2.5.2" store = { path = "../../beacon_node/store" } lazy_static = "1.4.0" -regex = "1" +regex = "1.5.5" sensitive_url = { path = "../sensitive_url" } diff --git a/common/sensitive_url/src/lib.rs b/common/sensitive_url/src/lib.rs index b7e620485a..7a3cbae20c 100644 --- a/common/sensitive_url/src/lib.rs +++ b/common/sensitive_url/src/lib.rs @@ -10,7 +10,7 @@ pub enum SensitiveError { } // Wrapper around Url which provides a custom `Display` implementation to protect user secrets. -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub struct SensitiveUrl { pub full: Url, pub redacted: String, diff --git a/common/validator_dir/src/builder.rs b/common/validator_dir/src/builder.rs index 861a6afe96..596c918b3f 100644 --- a/common/validator_dir/src/builder.rs +++ b/common/validator_dir/src/builder.rs @@ -4,7 +4,7 @@ use deposit_contract::{encode_eth1_tx_data, Error as DepositError}; use eth2_keystore::{Error as KeystoreError, Keystore, KeystoreBuilder, PlainText}; use filesystem::create_with_600_perms; use rand::{distributions::Alphanumeric, Rng}; -use std::fs::{create_dir_all, OpenOptions}; +use std::fs::{create_dir_all, File}; use std::io::{self, Write}; use std::path::{Path, PathBuf}; use types::{ChainSpec, DepositData, Hash256, Keypair, Signature}; @@ -197,7 +197,7 @@ impl<'a> Builder<'a> { return Err(Error::DepositDataAlreadyExists(path)); } else { let hex = format!("0x{}", hex::encode(&deposit_data)); - OpenOptions::new() + File::options() .write(true) .read(true) .create(true) @@ -214,7 +214,7 @@ impl<'a> Builder<'a> { if path.exists() { return Err(Error::DepositAmountAlreadyExists(path)); } else { - OpenOptions::new() + File::options() .write(true) .read(true) .create(true) @@ -267,7 +267,7 @@ fn write_keystore_to_file(path: PathBuf, keystore: &Keystore) -> Result<(), Erro if path.exists() { Err(Error::KeystoreAlreadyExists(path)) } else { - let file = OpenOptions::new() + let file = File::options() .write(true) .read(true) .create_new(true) diff --git a/common/validator_dir/src/validator_dir.rs b/common/validator_dir/src/validator_dir.rs index 2fabebc743..cb1ddde24a 100644 --- a/common/validator_dir/src/validator_dir.rs +++ b/common/validator_dir/src/validator_dir.rs @@ -6,7 +6,7 @@ use deposit_contract::decode_eth1_tx_data; use derivative::Derivative; use eth2_keystore::{Error as KeystoreError, Keystore, PlainText}; use lockfile::{Lockfile, LockfileError}; -use std::fs::{read, write, OpenOptions}; +use std::fs::{read, write, File}; use std::io; use std::path::{Path, PathBuf}; use tree_hash::TreeHash; @@ -211,7 +211,7 @@ pub fn unlock_keypair>( password_dir: P, ) -> Result { let keystore = Keystore::from_json_reader( - &mut OpenOptions::new() + &mut File::options() .read(true) .create(false) .open(keystore_path) @@ -236,7 +236,7 @@ pub fn unlock_keypair_from_password_path( password_path: &Path, ) -> Result { let keystore = Keystore::from_json_reader( - &mut OpenOptions::new() + &mut File::options() .read(true) .create(false) .open(keystore_path) diff --git a/common/warp_utils/src/cors.rs b/common/warp_utils/src/cors.rs index 006424b505..314ea9c8f7 100644 --- a/common/warp_utils/src/cors.rs +++ b/common/warp_utils/src/cors.rs @@ -1,4 +1,4 @@ -use std::net::Ipv4Addr; +use std::net::IpAddr; use warp::filters::cors::Builder; /// Configure a `cors::Builder`. @@ -7,7 +7,7 @@ use warp::filters::cors::Builder; pub fn set_builder_origins( builder: Builder, allow_origin: Option<&str>, - default_origin: (Ipv4Addr, u16), + default_origin: (IpAddr, u16), ) -> Result { if let Some(allow_origin) = allow_origin { let origins = allow_origin @@ -16,7 +16,10 @@ pub fn set_builder_origins( .collect::, _>>()?; Ok(builder.allow_origins(origins)) } else { - let origin = format!("http://{}:{}", default_origin.0, default_origin.1); + let origin = match default_origin.0 { + IpAddr::V4(_) => format!("http://{}:{}", default_origin.0, default_origin.1), + IpAddr::V6(_) => format!("http://[{}]:{}", default_origin.0, default_origin.1), + }; verify_cors_origin_str(&origin)?; Ok(builder.allow_origin(origin.as_str())) @@ -65,6 +68,8 @@ mod test { verify_cors_origin_str("http://localhost").unwrap(); verify_cors_origin_str("http://127.0.0.1:8000").unwrap(); verify_cors_origin_str("http://localhost:8000").unwrap(); + verify_cors_origin_str("http://[::1]").unwrap(); + verify_cors_origin_str("http://[::1]:8000").unwrap(); } #[test] @@ -72,5 +77,6 @@ mod test { verify_cors_origin_str(".*").unwrap_err(); verify_cors_origin_str("127.0.0.1").unwrap_err(); verify_cors_origin_str("localhost").unwrap_err(); + verify_cors_origin_str("[::1]").unwrap_err(); } } diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 149c9404c8..dfa922e5dd 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -1,4 +1,4 @@ -use crate::ForkChoiceStore; +use crate::{ForkChoiceStore, InvalidationOperation}; use proto_array::{Block as ProtoBlock, ExecutionStatus, ProtoArrayForkChoice}; use ssz_derive::{Decode, Encode}; use std::cmp::Ordering; @@ -241,6 +241,14 @@ pub enum AttestationFromBlock { False, } +/// Parameters which are cached between calls to `Self::get_head`. +#[derive(Clone, Copy)] +pub struct ForkchoiceUpdateParameters { + pub head_root: Hash256, + pub head_hash: Option, + pub finalized_hash: Option, +} + /// Provides an implementation of "Ethereum 2.0 Phase 0 -- Beacon Chain Fork Choice": /// /// https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/fork-choice.md#ethereum-20-phase-0----beacon-chain-fork-choice @@ -258,6 +266,8 @@ pub struct ForkChoice { proto_array: ProtoArrayForkChoice, /// Attestations that arrived at the current slot and must be queued for later processing. queued_attestations: Vec, + /// Stores a cache of the values required to be sent to the execution layer. + forkchoice_update_parameters: Option, _phantom: PhantomData, } @@ -332,6 +342,7 @@ where fc_store, proto_array, queued_attestations: vec![], + forkchoice_update_parameters: None, _phantom: PhantomData, }) } @@ -349,10 +360,20 @@ where fc_store, proto_array, queued_attestations, + forkchoice_update_parameters: None, _phantom: PhantomData, } } + /// Returns cached information that can be used to issue a `forkchoiceUpdated` message to an + /// execution engine. + /// + /// These values are updated each time `Self::get_head` is called. May return `None` if + /// `Self::get_head` has not yet been called. + pub fn get_forkchoice_update_parameters(&self) -> Option { + self.forkchoice_update_parameters + } + /// Returns the block root of an ancestor of `block_root` at the given `slot`. (Note: `slot` refers /// to the block that is *returned*, not the one that is supplied.) /// @@ -414,18 +435,29 @@ where let store = &mut self.fc_store; - // FIXME(sproul): plumb VList through fork choice - let justified_balances = store.justified_balances().to_vec(); + let head_root = self.proto_array.find_head::( + *store.justified_checkpoint(), + *store.finalized_checkpoint(), + store.justified_balances(), + store.proposer_boost_root(), + spec, + )?; - self.proto_array - .find_head::( - *store.justified_checkpoint(), - *store.finalized_checkpoint(), - &justified_balances, - store.proposer_boost_root(), - spec, - ) - .map_err(Into::into) + // Cache some values for the next forkchoiceUpdate call to the execution layer. + let head_hash = self + .get_block(&head_root) + .and_then(|b| b.execution_status.block_hash()); + let finalized_root = self.finalized_checkpoint().root; + let finalized_hash = self + .get_block(&finalized_root) + .and_then(|b| b.execution_status.block_hash()); + self.forkchoice_update_parameters = Some(ForkchoiceUpdateParameters { + head_root, + head_hash, + finalized_hash, + }); + + Ok(head_root) } /// Returns `true` if the given `store` should be updated to set @@ -483,11 +515,10 @@ where /// See `ProtoArrayForkChoice::process_execution_payload_invalidation` for documentation. pub fn on_invalid_execution_payload( &mut self, - head_block_root: Hash256, - latest_valid_ancestor_root: Option, + op: &InvalidationOperation, ) -> Result<(), Error> { self.proto_array - .process_execution_payload_invalidation(head_block_root, latest_valid_ancestor_root) + .process_execution_payload_invalidation(op) .map_err(Error::FailedToProcessInvalidExecutionPayload) } @@ -931,6 +962,54 @@ where .is_descendant(self.fc_store.finalized_checkpoint().root, block_root) } + /// Returns `Ok(false)` if a block is not viable to be imported optimistically. + /// + /// ## Notes + /// + /// Equivalent to the function with the same name in the optimistic sync specs: + /// + /// https://github.com/ethereum/consensus-specs/blob/dev/sync/optimistic.md#helpers + pub fn is_optimistic_candidate_block( + &self, + current_slot: Slot, + block_slot: Slot, + block_parent_root: &Hash256, + spec: &ChainSpec, + ) -> Result> { + // If the block is sufficiently old, import it. + if block_slot + spec.safe_slots_to_import_optimistically <= current_slot { + return Ok(true); + } + + // If the justified block has execution enabled, then optimistically import any block. + if self + .get_justified_block()? + .execution_status + .is_execution_enabled() + { + return Ok(true); + } + + // If the parent block has execution enabled, always import the block. + // + // TODO(bellatrix): this condition has not yet been merged into the spec. + // + // See: + // + // https://github.com/ethereum/consensus-specs/pull/2844 + if self + .proto_array + .get_block(block_parent_root) + .map_or(false, |parent| { + parent.execution_status.is_execution_enabled() + }) + { + return Ok(true); + } + + Ok(false) + } + /// Return the current finalized checkpoint. pub fn finalized_checkpoint(&self) -> Checkpoint { *self.fc_store.finalized_checkpoint() @@ -1005,6 +1084,7 @@ where fc_store, proto_array, queued_attestations: persisted.queued_attestations, + forkchoice_update_parameters: None, _phantom: PhantomData, }) } diff --git a/consensus/fork_choice/src/lib.rs b/consensus/fork_choice/src/lib.rs index ba031cdf7f..d4a95994e0 100644 --- a/consensus/fork_choice/src/lib.rs +++ b/consensus/fork_choice/src/lib.rs @@ -6,4 +6,4 @@ pub use crate::fork_choice::{ PayloadVerificationStatus, PersistedForkChoice, QueuedAttestation, }; pub use fork_choice_store::ForkChoiceStore; -pub use proto_array::Block as ProtoBlock; +pub use proto_array::{Block as ProtoBlock, InvalidationOperation}; diff --git a/consensus/proto_array/src/fork_choice_test_definition.rs b/consensus/proto_array/src/fork_choice_test_definition.rs index fd90d53903..f2b51c1fd4 100644 --- a/consensus/proto_array/src/fork_choice_test_definition.rs +++ b/consensus/proto_array/src/fork_choice_test_definition.rs @@ -4,6 +4,7 @@ mod no_votes; mod votes; use crate::proto_array_fork_choice::{Block, ExecutionStatus, ProtoArrayForkChoice}; +use crate::InvalidationOperation; use serde_derive::{Deserialize, Serialize}; use types::{ AttestationShufflingId, Checkpoint, Epoch, EthSpec, ExecutionBlockHash, Hash256, @@ -238,12 +239,22 @@ impl ForkChoiceTestDefinition { Operation::InvalidatePayload { head_block_root, latest_valid_ancestor_root, - } => fork_choice - .process_execution_payload_invalidation( - head_block_root, - latest_valid_ancestor_root, - ) - .unwrap(), + } => { + let op = if let Some(latest_valid_ancestor) = latest_valid_ancestor_root { + InvalidationOperation::InvalidateMany { + head_block_root, + always_invalidate_head: true, + latest_valid_ancestor, + } + } else { + InvalidationOperation::InvalidateOne { + block_root: head_block_root, + } + }; + fork_choice + .process_execution_payload_invalidation(&op) + .unwrap() + } Operation::AssertWeight { block_root, weight } => assert_eq!( fork_choice.get_weight(&block_root).unwrap(), weight, diff --git a/consensus/proto_array/src/lib.rs b/consensus/proto_array/src/lib.rs index 216d189fb2..d6f614b7c3 100644 --- a/consensus/proto_array/src/lib.rs +++ b/consensus/proto_array/src/lib.rs @@ -4,6 +4,7 @@ mod proto_array; mod proto_array_fork_choice; mod ssz_container; +pub use crate::proto_array::InvalidationOperation; pub use crate::proto_array_fork_choice::{Block, ExecutionStatus, ProtoArrayForkChoice}; pub use error::Error; diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index b0e8991a78..fb086a96e9 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -15,6 +15,56 @@ use types::{ four_byte_option_impl!(four_byte_option_usize, usize); four_byte_option_impl!(four_byte_option_checkpoint, Checkpoint); +/// Defines an operation which may invalidate the `execution_status` of some nodes. +pub enum InvalidationOperation { + /// Invalidate only `block_root` and it's descendants. Don't invalidate any ancestors. + InvalidateOne { block_root: Hash256 }, + /// Invalidate blocks between `head_block_root` and `latest_valid_ancestor`. + /// + /// If the `latest_valid_ancestor` is known to fork choice, invalidate all blocks between + /// `head_block_root` and `latest_valid_ancestor`. The `head_block_root` will be invalidated, + /// whilst the `latest_valid_ancestor` will not. + /// + /// If `latest_valid_ancestor` is *not* known to fork choice, only invalidate the + /// `head_block_root` if `always_invalidate_head == true`. + InvalidateMany { + head_block_root: Hash256, + always_invalidate_head: bool, + latest_valid_ancestor: ExecutionBlockHash, + }, +} + +impl InvalidationOperation { + pub fn block_root(&self) -> Hash256 { + match self { + InvalidationOperation::InvalidateOne { block_root } => *block_root, + InvalidationOperation::InvalidateMany { + head_block_root, .. + } => *head_block_root, + } + } + + pub fn latest_valid_ancestor(&self) -> Option { + match self { + InvalidationOperation::InvalidateOne { .. } => None, + InvalidationOperation::InvalidateMany { + latest_valid_ancestor, + .. + } => Some(*latest_valid_ancestor), + } + } + + pub fn invalidate_block_root(&self) -> bool { + match self { + InvalidationOperation::InvalidateOne { .. } => true, + InvalidationOperation::InvalidateMany { + always_invalidate_head, + .. + } => *always_invalidate_head, + } + } +} + #[derive(Clone, PartialEq, Debug, Encode, Decode, Serialize, Deserialize)] pub struct ProtoNode { /// The `slot` is not necessary for `ProtoArray`, it just exists so external components can @@ -328,43 +378,15 @@ impl ProtoArray { } } - /// Invalidate the relevant ancestors and descendants of a block with an invalid execution - /// payload. + /// Invalidate zero or more blocks, as specified by the `InvalidationOperation`. /// - /// The `head_block_root` should be the beacon block root of the block with the invalid - /// execution payload, _or_ its parent where the block with the invalid payload has not yet - /// been applied to `self`. - /// - /// The `latest_valid_hash` should be the hash of most recent *valid* execution payload - /// contained in an ancestor block of `head_block_root`. - /// - /// This function will invalidate: - /// - /// * The block matching `head_block_root` _unless_ that block has a payload matching `latest_valid_hash`. - /// * All ancestors of `head_block_root` back to the block with payload matching - /// `latest_valid_hash` (endpoint > exclusive). In the case where the `head_block_root` is the parent - /// of the invalid block and itself matches `latest_valid_hash`, no ancestors will be invalidated. - /// * All descendants of `latest_valid_hash` if supplied and consistent with `head_block_root`, - /// or else all descendants of `head_block_root`. - /// - /// ## Details - /// - /// If `head_block_root` is not known to fork choice, an error is returned. - /// - /// If `latest_valid_hash` is `Some(hash)` where `hash` is either not known to fork choice - /// (perhaps it's junk or pre-finalization), then only the `head_block_root` block will be - /// invalidated (no ancestors). No error will be returned in this case. - /// - /// If `latest_valid_hash` is `Some(hash)` where `hash` is a known ancestor of - /// `head_block_root`, then all blocks between `head_block_root` and `latest_valid_hash` will - /// be invalidated. Additionally, all blocks that descend from a newly-invalidated block will - /// also be invalidated. + /// See the documentation of `InvalidationOperation` for usage. pub fn propagate_execution_payload_invalidation( &mut self, - head_block_root: Hash256, - latest_valid_ancestor_hash: Option, + op: &InvalidationOperation, ) -> Result<(), Error> { let mut invalidated_indices: HashSet = <_>::default(); + let head_block_root = op.block_root(); /* * Step 1: @@ -379,7 +401,8 @@ impl ProtoArray { .ok_or(Error::NodeUnknown(head_block_root))?; // Try to map the ancestor payload *hash* to an ancestor beacon block *root*. - let latest_valid_ancestor_root = latest_valid_ancestor_hash + let latest_valid_ancestor_root = op + .latest_valid_ancestor() .and_then(|hash| self.execution_block_hash_to_beacon_block_root(&hash)); // Set to `true` if both conditions are satisfied: @@ -414,7 +437,7 @@ impl ProtoArray { // an invalid justified checkpoint. if !latest_valid_ancestor_is_descendant && node.root != head_block_root { break; - } else if Some(hash) == latest_valid_ancestor_hash { + } else if op.latest_valid_ancestor() == Some(hash) { // If the `best_child` or `best_descendant` of the latest valid hash was // invalidated, set those fields to `None`. // @@ -444,36 +467,44 @@ impl ProtoArray { ExecutionStatus::Irrelevant(_) => break, } - match &node.execution_status { - // It's illegal for an execution client to declare that some previously-valid block - // is now invalid. This is a consensus failure on their behalf. - ExecutionStatus::Valid(hash) => { - return Err(Error::ValidExecutionStatusBecameInvalid { - block_root: node.root, - payload_block_hash: *hash, - }) - } - ExecutionStatus::Unknown(hash) => { - node.execution_status = ExecutionStatus::Invalid(*hash); + // Only invalidate the head block if either: + // + // - The head block was specifically indicated to be invalidated. + // - The latest valid hash is a known ancestor. + if node.root != head_block_root + || op.invalidate_block_root() + || latest_valid_ancestor_is_descendant + { + match &node.execution_status { + // It's illegal for an execution client to declare that some previously-valid block + // is now invalid. This is a consensus failure on their behalf. + ExecutionStatus::Valid(hash) => { + return Err(Error::ValidExecutionStatusBecameInvalid { + block_root: node.root, + payload_block_hash: *hash, + }) + } + ExecutionStatus::Unknown(hash) => { + invalidated_indices.insert(index); + node.execution_status = ExecutionStatus::Invalid(*hash); - // It's impossible for an invalid block to lead to a "best" block, so set these - // fields to `None`. - // - // Failing to set these values will result in `Self::node_leads_to_viable_head` - // returning `false` for *valid* ancestors of invalid blocks. - node.best_child = None; - node.best_descendant = None; + // It's impossible for an invalid block to lead to a "best" block, so set these + // fields to `None`. + // + // Failing to set these values will result in `Self::node_leads_to_viable_head` + // returning `false` for *valid* ancestors of invalid blocks. + node.best_child = None; + node.best_descendant = None; + } + // The block is already invalid, but keep going backwards to ensure all ancestors + // are updated. + ExecutionStatus::Invalid(_) => (), + // This block is pre-merge, therefore it has no execution status. Nor do its + // ancestors. + ExecutionStatus::Irrelevant(_) => break, } - // The block is already invalid, but keep going backwards to ensure all ancestors - // are updated. - ExecutionStatus::Invalid(_) => (), - // This block is pre-merge, therefore it has no execution status. Nor do its - // ancestors. - ExecutionStatus::Irrelevant(_) => break, } - invalidated_indices.insert(index); - if let Some(parent_index) = node.parent { index = parent_index } else { diff --git a/consensus/proto_array/src/proto_array_fork_choice.rs b/consensus/proto_array/src/proto_array_fork_choice.rs index 1f5b997f67..007f262fdd 100644 --- a/consensus/proto_array/src/proto_array_fork_choice.rs +++ b/consensus/proto_array/src/proto_array_fork_choice.rs @@ -1,5 +1,5 @@ use crate::error::Error; -use crate::proto_array::{ProposerBoost, ProtoArray}; +use crate::proto_array::{InvalidationOperation, Iter, ProposerBoost, ProtoArray}; use crate::ssz_container::SszContainer; use serde_derive::{Deserialize, Serialize}; use ssz::{Decode, Encode}; @@ -40,6 +40,10 @@ pub enum ExecutionStatus { } impl ExecutionStatus { + pub fn is_execution_enabled(&self) -> bool { + !matches!(self, ExecutionStatus::Irrelevant(_)) + } + pub fn irrelevant() -> Self { ExecutionStatus::Irrelevant(false) } @@ -187,11 +191,10 @@ impl ProtoArrayForkChoice { /// See `ProtoArray::propagate_execution_payload_invalidation` for documentation. pub fn process_execution_payload_invalidation( &mut self, - head_block_root: Hash256, - latest_valid_ancestor_root: Option, + op: &InvalidationOperation, ) -> Result<(), String> { self.proto_array - .propagate_execution_payload_invalidation(head_block_root, latest_valid_ancestor_root) + .propagate_execution_payload_invalidation(op) .map_err(|e| format!("Failed to process invalid payload: {:?}", e)) } @@ -341,6 +344,11 @@ impl ProtoArrayForkChoice { } } + /// See `ProtoArray::iter_nodes` + pub fn iter_nodes<'a>(&'a self, block_root: &Hash256) -> Iter<'a> { + self.proto_array.iter_nodes(block_root) + } + pub fn as_bytes(&self) -> Vec { SszContainer::from(self).as_ssz_bytes() } diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index 46d1937cb4..bdad5d93ac 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -333,10 +333,10 @@ pub fn partially_verify_execution_payload( ); } block_verify!( - payload.random == *state.get_randao_mix(state.current_epoch())?, + payload.prev_randao == *state.get_randao_mix(state.current_epoch())?, BlockProcessingError::ExecutionRandaoMismatch { expected: *state.get_randao_mix(state.current_epoch())?, - found: payload.random, + found: payload.prev_randao, } ); @@ -372,7 +372,7 @@ pub fn process_execution_payload( state_root: payload.state_root, receipts_root: payload.receipts_root, logs_bloom: payload.logs_bloom.clone(), - random: payload.random, + prev_randao: payload.prev_randao, block_number: payload.block_number, gas_limit: payload.gas_limit, gas_used: payload.gas_used, diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index d048d5f653..82d6345a8e 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -39,7 +39,7 @@ derivative = "2.1.1" rusqlite = { version = "0.25.3", features = ["bundled"], optional = true } arbitrary = { version = "1.0", features = ["derive"], optional = true } eth2_serde_utils = "0.1.1" -regex = "1.3.9" +regex = "1.5.5" lazy_static = "1.4.0" parking_lot = "0.11.1" itertools = "0.10.0" diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index 7631f1f5cc..1472cabbad 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -146,6 +146,7 @@ pub struct ChainSpec { pub terminal_total_difficulty: Uint256, pub terminal_block_hash: ExecutionBlockHash, pub terminal_block_hash_activation_epoch: Epoch, + pub safe_slots_to_import_optimistically: u64, /* * Networking @@ -558,6 +559,7 @@ impl ChainSpec { .expect("addition does not overflow"), terminal_block_hash: ExecutionBlockHash::zero(), terminal_block_hash_activation_epoch: Epoch::new(u64::MAX), + safe_slots_to_import_optimistically: 128u64, /* * Network specific @@ -755,6 +757,7 @@ impl ChainSpec { .expect("addition does not overflow"), terminal_block_hash: ExecutionBlockHash::zero(), terminal_block_hash_activation_epoch: Epoch::new(u64::MAX), + safe_slots_to_import_optimistically: 128u64, /* * Network specific @@ -798,6 +801,10 @@ pub struct Config { // TODO(merge): remove this default #[serde(default = "default_terminal_block_hash_activation_epoch")] pub terminal_block_hash_activation_epoch: Epoch, + // TODO(merge): remove this default + #[serde(default = "default_safe_slots_to_import_optimistically")] + #[serde(with = "eth2_serde_utils::quoted_u64")] + pub safe_slots_to_import_optimistically: u64, #[serde(with = "eth2_serde_utils::quoted_u64")] min_genesis_active_validator_count: u64, @@ -885,6 +892,10 @@ fn default_terminal_block_hash_activation_epoch() -> Epoch { Epoch::new(u64::MAX) } +fn default_safe_slots_to_import_optimistically() -> u64 { + 128u64 +} + impl Default for Config { fn default() -> Self { let chain_spec = MainnetEthSpec::default_spec(); @@ -942,6 +953,7 @@ impl Config { terminal_total_difficulty: spec.terminal_total_difficulty, terminal_block_hash: spec.terminal_block_hash, terminal_block_hash_activation_epoch: spec.terminal_block_hash_activation_epoch, + safe_slots_to_import_optimistically: spec.safe_slots_to_import_optimistically, min_genesis_active_validator_count: spec.min_genesis_active_validator_count, min_genesis_time: spec.min_genesis_time, @@ -992,6 +1004,7 @@ impl Config { terminal_total_difficulty, terminal_block_hash, terminal_block_hash_activation_epoch, + safe_slots_to_import_optimistically, min_genesis_active_validator_count, min_genesis_time, genesis_fork_version, @@ -1047,6 +1060,7 @@ impl Config { terminal_total_difficulty, terminal_block_hash, terminal_block_hash_activation_epoch, + safe_slots_to_import_optimistically, ..chain_spec.clone() }) } @@ -1157,14 +1171,13 @@ mod tests { #[cfg(test)] mod yaml_tests { use super::*; - use std::fs::OpenOptions; use tempfile::NamedTempFile; #[test] fn minimal_round_trip() { // create temp file let tmp_file = NamedTempFile::new().expect("failed to create temp file"); - let writer = OpenOptions::new() + let writer = File::options() .read(false) .write(true) .open(tmp_file.as_ref()) @@ -1175,7 +1188,7 @@ mod yaml_tests { // write fresh minimal config to file serde_yaml::to_writer(writer, &yamlconfig).expect("failed to write or serialize"); - let reader = OpenOptions::new() + let reader = File::options() .read(true) .write(false) .open(tmp_file.as_ref()) @@ -1188,7 +1201,7 @@ mod yaml_tests { #[test] fn mainnet_round_trip() { let tmp_file = NamedTempFile::new().expect("failed to create temp file"); - let writer = OpenOptions::new() + let writer = File::options() .read(false) .write(true) .open(tmp_file.as_ref()) @@ -1197,7 +1210,7 @@ mod yaml_tests { let yamlconfig = Config::from_chain_spec::(&mainnet_spec); serde_yaml::to_writer(writer, &yamlconfig).expect("failed to write or serialize"); - let reader = OpenOptions::new() + let reader = File::options() .read(true) .write(false) .open(tmp_file.as_ref()) @@ -1234,6 +1247,7 @@ mod yaml_tests { #TERMINAL_TOTAL_DIFFICULTY: 115792089237316195423570985008687907853269984665640564039457584007913129638911 #TERMINAL_BLOCK_HASH: 0x0000000000000000000000000000000000000000000000000000000000000001 #TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH: 18446744073709551614 + #SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY: 2 MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 16384 MIN_GENESIS_TIME: 1606824000 GENESIS_FORK_VERSION: 0x00000000 @@ -1273,6 +1287,10 @@ mod yaml_tests { chain_spec.terminal_block_hash_activation_epoch, default_terminal_block_hash_activation_epoch() ); + assert_eq!( + chain_spec.safe_slots_to_import_optimistically, + default_safe_slots_to_import_optimistically() + ); assert_eq!( chain_spec.bellatrix_fork_epoch, diff --git a/consensus/types/src/config_and_preset.rs b/consensus/types/src/config_and_preset.rs index d782f4d8b1..f721e6c3bb 100644 --- a/consensus/types/src/config_and_preset.rs +++ b/consensus/types/src/config_and_preset.rs @@ -92,13 +92,13 @@ impl ConfigAndPreset { mod test { use super::*; use crate::MainnetEthSpec; - use std::fs::OpenOptions; + use std::fs::File; use tempfile::NamedTempFile; #[test] fn extra_fields_round_trip() { let tmp_file = NamedTempFile::new().expect("failed to create temp file"); - let writer = OpenOptions::new() + let writer = File::options() .read(false) .write(true) .open(tmp_file.as_ref()) @@ -116,7 +116,7 @@ mod test { serde_yaml::to_writer(writer, &yamlconfig).expect("failed to write or serialize"); - let reader = OpenOptions::new() + let reader = File::options() .read(true) .write(false) .open(tmp_file.as_ref()) diff --git a/consensus/types/src/execution_payload.rs b/consensus/types/src/execution_payload.rs index 3a9f95fa9c..d934a6ca00 100644 --- a/consensus/types/src/execution_payload.rs +++ b/consensus/types/src/execution_payload.rs @@ -23,7 +23,7 @@ pub struct ExecutionPayload { pub receipts_root: Hash256, #[serde(with = "ssz_types::serde_utils::hex_fixed_vec")] pub logs_bloom: FixedVector, - pub random: Hash256, + pub prev_randao: Hash256, #[serde(with = "eth2_serde_utils::quoted_u64")] pub block_number: u64, #[serde(with = "eth2_serde_utils::quoted_u64")] diff --git a/consensus/types/src/execution_payload_header.rs b/consensus/types/src/execution_payload_header.rs index d01e0d89a5..53f2caaa22 100644 --- a/consensus/types/src/execution_payload_header.rs +++ b/consensus/types/src/execution_payload_header.rs @@ -17,7 +17,7 @@ pub struct ExecutionPayloadHeader { pub receipts_root: Hash256, #[serde(with = "ssz_types::serde_utils::hex_fixed_vec")] pub logs_bloom: FixedVector, - pub random: Hash256, + pub prev_randao: Hash256, #[serde(with = "eth2_serde_utils::quoted_u64")] pub block_number: u64, #[serde(with = "eth2_serde_utils::quoted_u64")] diff --git a/consensus/types/src/proposer_preparation_data.rs b/consensus/types/src/proposer_preparation_data.rs index f2ea967114..6179828a95 100644 --- a/consensus/types/src/proposer_preparation_data.rs +++ b/consensus/types/src/proposer_preparation_data.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; #[derive(PartialEq, Debug, Serialize, Deserialize, Clone)] pub struct ProposerPreparationData { /// The validators index. + #[serde(with = "eth2_serde_utils::quoted_u64")] pub validator_index: u64, /// The fee-recipient address. pub fee_recipient: Address, diff --git a/crypto/eth2_keystore/src/keystore.rs b/crypto/eth2_keystore/src/keystore.rs index 801e8fbd7a..2049518cd4 100644 --- a/crypto/eth2_keystore/src/keystore.rs +++ b/crypto/eth2_keystore/src/keystore.rs @@ -21,7 +21,7 @@ use scrypt::{ }; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use std::fs::OpenOptions; +use std::fs::File; use std::io::{Read, Write}; use std::iter::FromIterator; use std::path::Path; @@ -329,7 +329,7 @@ impl Keystore { /// Instantiates `self` by reading a JSON file at `path`. pub fn from_json_file>(path: P) -> Result { - OpenOptions::new() + File::options() .read(true) .write(false) .create(false) diff --git a/crypto/eth2_keystore/tests/tests.rs b/crypto/eth2_keystore/tests/tests.rs index 3472ee6f1f..0df884b8a2 100644 --- a/crypto/eth2_keystore/tests/tests.rs +++ b/crypto/eth2_keystore/tests/tests.rs @@ -7,7 +7,7 @@ use eth2_keystore::{ json_keystore::{Kdf, Pbkdf2, Prf, Scrypt}, Error, Keystore, KeystoreBuilder, DKLEN, }; -use std::fs::OpenOptions; +use std::fs::File; use tempfile::tempdir; const GOOD_PASSWORD: &[u8] = &[42, 42, 42]; @@ -55,7 +55,7 @@ fn file() { let path = dir.path().join("keystore.json"); let get_file = || { - OpenOptions::new() + File::options() .write(true) .read(true) .create(true) diff --git a/crypto/eth2_wallet/tests/tests.rs b/crypto/eth2_wallet/tests/tests.rs index eb683e72d6..fe4565e0db 100644 --- a/crypto/eth2_wallet/tests/tests.rs +++ b/crypto/eth2_wallet/tests/tests.rs @@ -4,7 +4,7 @@ use eth2_wallet::{ bip39::{Language, Mnemonic, Seed}, recover_validator_secret, DerivedKey, Error, KeyType, KeystoreError, Wallet, WalletBuilder, }; -use std::fs::OpenOptions; +use std::fs::File; use tempfile::tempdir; const NAME: &str = "Wallet McWalletface"; @@ -133,7 +133,7 @@ fn file_round_trip() { let path = dir.path().join("keystore.json"); let get_file = || { - OpenOptions::new() + File::options() .write(true) .read(true) .create(true) diff --git a/lcli/Cargo.toml b/lcli/Cargo.toml index f13b4762fb..423887cf65 100644 --- a/lcli/Cargo.toml +++ b/lcli/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "lcli" description = "Lighthouse CLI (modeled after zcli)" -version = "2.1.3" +version = "2.1.5" authors = ["Paul Hauner "] edition = "2021" diff --git a/lcli/src/create_payload_header.rs b/lcli/src/create_payload_header.rs index 04122d0e6b..9e91f425a7 100644 --- a/lcli/src/create_payload_header.rs +++ b/lcli/src/create_payload_header.rs @@ -23,7 +23,7 @@ pub fn run(matches: &ArgMatches) -> Result<(), String> { base_fee_per_gas, timestamp: genesis_time, block_hash: eth1_block_hash, - random: eth1_block_hash.into_root(), + prev_randao: eth1_block_hash.into_root(), ..ExecutionPayloadHeader::default() }; let mut file = File::create(file_name).map_err(|_| "Unable to create file".to_string())?; diff --git a/lcli/src/etl/block_efficiency.rs b/lcli/src/etl/block_efficiency.rs index 1446428100..1c7ba1fe61 100644 --- a/lcli/src/etl/block_efficiency.rs +++ b/lcli/src/etl/block_efficiency.rs @@ -4,7 +4,7 @@ use eth2::{BeaconNodeHttpClient, Timeouts}; use log::{error, info}; use sensitive_url::SensitiveUrl; use std::collections::{HashMap, HashSet}; -use std::fs::OpenOptions; +use std::fs::File; use std::io::Write; use std::path::PathBuf; use std::time::Duration; @@ -158,7 +158,7 @@ pub async fn run(matches: &ArgMatches<'_>) -> Result<(), String> { let mut proposer_map: HashMap = HashMap::new(); - let mut file = OpenOptions::new() + let mut file = File::options() .read(true) .write(true) .create(true) diff --git a/lcli/src/parse_ssz.rs b/lcli/src/parse_ssz.rs index 5b3c575166..3f272780db 100644 --- a/lcli/src/parse_ssz.rs +++ b/lcli/src/parse_ssz.rs @@ -41,10 +41,13 @@ pub fn run_parse_ssz(matches: &ArgMatches) -> Result<(), String> { match type_str { "signed_block_base" => decode_and_print::>(&bytes, format)?, "signed_block_altair" => decode_and_print::>(&bytes, format)?, + "signed_block_merge" => decode_and_print::>(&bytes, format)?, "block_base" => decode_and_print::>(&bytes, format)?, "block_altair" => decode_and_print::>(&bytes, format)?, + "block_merge" => decode_and_print::>(&bytes, format)?, "state_base" => decode_and_print::>(&bytes, format)?, "state_altair" => decode_and_print::>(&bytes, format)?, + "state_merge" => decode_and_print::>(&bytes, format)?, other => return Err(format!("Unknown type: {}", other)), }; diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index fd11823c0e..a875027b5c 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -1,9 +1,10 @@ [package] name = "lighthouse" -version = "2.1.3" +version = "2.1.5" authors = ["Sigma Prime "] edition = "2021" autotests = false +rust-version = "1.58" [features] default = ["tree-states"] @@ -44,6 +45,7 @@ lighthouse_metrics = { path = "../common/lighthouse_metrics" } lazy_static = "1.4.0" serde = { version = "1.0.116", features = ["derive"] } serde_json = "1.0.59" +serde_yaml = "0.8.13" task_executor = { path = "../common/task_executor" } malloc_utils = { path = "../common/malloc_utils" } directory = { path = "../common/directory" } @@ -56,6 +58,7 @@ tempfile = "3.1.0" validator_dir = { path = "../common/validator_dir" } slashing_protection = { path = "../validator_client/slashing_protection" } lighthouse_network = { path = "../beacon_node/lighthouse_network" } +sensitive_url = { path = "../common/sensitive_url" } [[test]] name = "lighthouse_tests" diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index d1c6774b20..254acd4075 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -13,7 +13,6 @@ use eth2_network_config::{Eth2NetworkConfig, DEFAULT_HARDCODED_NETWORK, HARDCODE use lighthouse_version::VERSION; use malloc_utils::configure_memory_allocator; use slog::{crit, info, warn}; -use std::fs::File; use std::path::PathBuf; use std::process::exit; use task_executor::ShutdownReason; @@ -193,6 +192,14 @@ fn main() { .takes_value(true) .global(true) ) + .arg( + Arg::with_name("dump-chain-config") + .long("dump-chain-config") + .hidden(true) + .help("Dumps the chain config to a desired location. Used for testing only.") + .takes_value(true) + .global(true) + ) .arg( Arg::with_name("immediate-shutdown") .long("immediate-shutdown") @@ -251,6 +258,19 @@ fn main() { .takes_value(true) .global(true) ) + .arg( + Arg::with_name("safe-slots-to-import-optimistically") + .long("safe-slots-to-import-optimistically") + .value_name("INTEGER") + .help("Used to coordinate manual overrides of the SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY \ + parameter. This flag should only be used if the user has a clear understanding \ + that the broad Ethereum community has elected to override this parameter in the event \ + of an attack at the PoS transition block. Incorrect use of this flag can cause your \ + node to possibly accept an invalid chain or sync more slowly. Be extremely careful with \ + this flag.") + .takes_value(true) + .global(true) + ) .subcommand(beacon_node::cli_app()) .subcommand(boot_node::cli_app()) .subcommand(validator_client::cli_app()) @@ -491,14 +511,8 @@ fn run( let executor = context.executor.clone(); let config = beacon_node::get_config::(matches, &context)?; let shutdown_flag = matches.is_present("immediate-shutdown"); - if let Some(dump_path) = clap_utils::parse_optional::(matches, "dump-config")? - { - let mut file = File::create(dump_path) - .map_err(|e| format!("Failed to create dumped config: {:?}", e))?; - serde_json::to_writer(&mut file, &config) - .map_err(|e| format!("Error serializing config: {:?}", e))?; - }; - + // Dump configs if `dump-config` or `dump-chain-config` flags are set + clap_utils::check_dump_configs::<_, E>(matches, &config, &context.eth2_config.spec)?; executor.clone().spawn( async move { if let Err(e) = ProductionBeaconNode::new(context.clone(), config).await { @@ -524,13 +538,8 @@ fn run( let config = validator_client::Config::from_cli(matches, context.log()) .map_err(|e| format!("Unable to initialize validator config: {}", e))?; let shutdown_flag = matches.is_present("immediate-shutdown"); - if let Some(dump_path) = clap_utils::parse_optional::(matches, "dump-config")? - { - let mut file = File::create(dump_path) - .map_err(|e| format!("Failed to create dumped config: {:?}", e))?; - serde_json::to_writer(&mut file, &config) - .map_err(|e| format!("Error serializing config: {:?}", e))?; - }; + // Dump configs if `dump-config` or `dump-chain-config` flags are set + clap_utils::check_dump_configs::<_, E>(matches, &config, &context.eth2_config.spec)?; if !shutdown_flag { executor.clone().spawn( async move { diff --git a/lighthouse/tests/account_manager.rs b/lighthouse/tests/account_manager.rs index fcc1d2aee2..06b0303c69 100644 --- a/lighthouse/tests/account_manager.rs +++ b/lighthouse/tests/account_manager.rs @@ -30,7 +30,7 @@ use validator_dir::ValidatorDir; // TODO: create tests for the `lighthouse account validator deposit` command. This involves getting // access to an IPC endpoint during testing or adding support for deposit submission via HTTP and -// using ganache-cli. +// using ganache. /// Returns the `lighthouse account` command. fn account_cmd() -> Command { diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 37c4359453..4d596ce703 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -4,13 +4,13 @@ use crate::exec::{CommandLineTestExec, CompletedTest}; use lighthouse_network::PeerId; use std::fs::File; use std::io::Write; -use std::net::{IpAddr, Ipv4Addr}; +use std::net::IpAddr; use std::path::PathBuf; use std::process::Command; use std::str::FromStr; use std::string::ToString; use tempfile::TempDir; -use types::{Address, Checkpoint, Epoch, Hash256}; +use types::{Address, Checkpoint, Epoch, ExecutionBlockHash, Hash256, MainnetEthSpec}; use unused_port::{unused_tcp_port, unused_udp_port}; const DEFAULT_ETH1_ENDPOINT: &str = "http://localhost:8545/"; @@ -206,7 +206,61 @@ fn eth1_purge_cache_flag() { .with_config(|config| assert!(config.eth1.purge_cache)); } -// Tests for Merge flags. +// Tests for Bellatrix flags. +#[test] +fn merge_flag() { + CommandLineTest::new() + .flag("merge", None) + .run_with_zero_port() + .with_config(|config| assert!(config.execution_layer.is_some())); +} +#[test] +fn merge_execution_endpoints_flag() { + use sensitive_url::SensitiveUrl; + let urls = vec!["http://sigp.io/no-way:1337", "http://infura.not_real:4242"]; + let endpoints = urls + .iter() + .map(|s| SensitiveUrl::parse(s).unwrap()) + .collect::>(); + let mut endpoint_arg = urls[0].to_string(); + for url in urls.into_iter().skip(1) { + endpoint_arg.push(','); + endpoint_arg.push_str(url); + } + // this is way better but intersperse is still a nightly feature :/ + // let endpoint_arg: String = urls.into_iter().intersperse(",").collect(); + CommandLineTest::new() + .flag("merge", None) + .flag("execution-endpoints", Some(&endpoint_arg)) + .run_with_zero_port() + .with_config(|config| { + let config = config.execution_layer.as_ref().unwrap(); + assert_eq!(config.execution_endpoints, endpoints) + }); +} +#[test] +fn merge_jwt_secrets_flag() { + let dir = TempDir::new().expect("Unable to create temporary directory"); + let mut file = File::create(dir.path().join("jwtsecrets")).expect("Unable to create file"); + file.write_all(b"0x3cbc11b0d8fa16f3344eacfd6ff6430b9d30734450e8adcf5400f88d327dcb33") + .expect("Unable to write to file"); + CommandLineTest::new() + .flag("merge", None) + .flag("execution-endpoints", Some("http://localhost:8551/")) + .flag( + "jwt-secrets", + dir.path().join("jwt-file").as_os_str().to_str(), + ) + .run_with_zero_port() + .with_config(|config| { + let config = config.execution_layer.as_ref().unwrap(); + assert_eq!( + config.execution_endpoints[0].full.to_string(), + "http://localhost:8551/" + ); + assert_eq!(config.secret_files[0], dir.path().join("jwt-file")); + }); +} #[test] fn merge_fee_recipient_flag() { CommandLineTest::new() @@ -217,10 +271,80 @@ fn merge_fee_recipient_flag() { ) .run_with_zero_port() .with_config(|config| { + let config = config.execution_layer.as_ref().unwrap(); assert_eq!( config.suggested_fee_recipient, Some(Address::from_str("0x00000000219ab540356cbb839cbe05303d7705fa").unwrap()) - ) + ); + }); +} +#[test] +fn jwt_optional_flags() { + CommandLineTest::new() + .flag("merge", None) + .flag("jwt-id", Some("bn-1")) + .flag("jwt-version", Some("Lighthouse-v2.1.3")) + .run_with_zero_port() + .with_config(|config| { + let config = config.execution_layer.as_ref().unwrap(); + assert_eq!(config.jwt_id, Some("bn-1".to_string())); + assert_eq!(config.jwt_version, Some("Lighthouse-v2.1.3".to_string())); + }); +} +#[test] +fn terminal_total_difficulty_override_flag() { + use beacon_node::beacon_chain::types::Uint256; + CommandLineTest::new() + .flag("terminal-total-difficulty-override", Some("1337424242")) + .run_with_zero_port() + .with_spec::(|spec| { + assert_eq!(spec.terminal_total_difficulty, Uint256::from(1337424242)) + }); +} +#[test] +fn terminal_block_hash_and_activation_epoch_override_flags() { + CommandLineTest::new() + .flag("terminal-block-hash-epoch-override", Some("1337")) + .flag( + "terminal-block-hash-override", + Some("0x4242424242424242424242424242424242424242424242424242424242424242"), + ) + .run_with_zero_port() + .with_spec::(|spec| { + assert_eq!( + spec.terminal_block_hash, + ExecutionBlockHash::from_str( + "0x4242424242424242424242424242424242424242424242424242424242424242" + ) + .unwrap() + ); + assert_eq!(spec.terminal_block_hash_activation_epoch, 1337); + }); +} +#[test] +#[should_panic] +fn terminal_block_hash_missing_activation_epoch() { + CommandLineTest::new() + .flag( + "terminal-block-hash-override", + Some("0x4242424242424242424242424242424242424242424242424242424242424242"), + ) + .run_with_zero_port(); +} +#[test] +#[should_panic] +fn epoch_override_missing_terminal_block_hash() { + CommandLineTest::new() + .flag("terminal-block-hash-epoch-override", Some("1337")) + .run_with_zero_port(); +} +#[test] +fn safe_slots_to_import_optimistically_flag() { + CommandLineTest::new() + .flag("safe-slots-to-import-optimistically", Some("421337")) + .run_with_zero_port() + .with_spec::(|spec| { + assert_eq!(spec.safe_slots_to_import_optimistically, 421337) }); } @@ -271,7 +395,7 @@ fn network_shutdown_after_sync_disabled_flag() { } #[test] fn network_listen_address_flag() { - let addr = "127.0.0.2".parse::().unwrap(); + let addr = "127.0.0.2".parse::().unwrap(); CommandLineTest::new() .flag("listen-address", Some("127.0.0.2")) .run_with_zero_port() @@ -410,6 +534,15 @@ fn zero_ports_flag() { assert_eq!(config.http_metrics.listen_port, 0); }); } +#[test] +fn network_load_flag() { + CommandLineTest::new() + .flag("network-load", Some("4")) + .run_with_zero_port() + .with_config(|config| { + assert_eq!(config.network.network_load, 4); + }); +} // Tests for ENR flags. #[test] @@ -494,13 +627,21 @@ fn http_flag() { } #[test] fn http_address_flag() { - let addr = "127.0.0.99".parse::().unwrap(); + let addr = "127.0.0.99".parse::().unwrap(); CommandLineTest::new() .flag("http-address", Some("127.0.0.99")) .run_with_zero_port() .with_config(|config| assert_eq!(config.http_api.listen_addr, addr)); } #[test] +fn http_address_ipv6_flag() { + let addr = "::1".parse::().unwrap(); + CommandLineTest::new() + .flag("http-address", Some("::1")) + .run_with_zero_port() + .with_config(|config| assert_eq!(config.http_api.listen_addr, addr)); +} +#[test] fn http_port_flag() { let port1 = unused_tcp_port().expect("Unable to find unused port."); let port2 = unused_tcp_port().expect("Unable to find unused port."); @@ -527,6 +668,13 @@ fn http_allow_origin_all_flag() { .with_config(|config| assert_eq!(config.http_api.allow_origin, Some("*".to_string()))); } #[test] +fn http_allow_sync_stalled_flag() { + CommandLineTest::new() + .flag("http-allow-sync-stalled", None) + .run_with_zero_port() + .with_config(|config| assert_eq!(config.http_api.allow_sync_stalled, true)); +} +#[test] fn http_tls_flags() { let dir = TempDir::new().expect("Unable to create temporary directory"); CommandLineTest::new() @@ -564,7 +712,7 @@ fn metrics_flag() { } #[test] fn metrics_address_flag() { - let addr = "127.0.0.99".parse::().unwrap(); + let addr = "127.0.0.99".parse::().unwrap(); CommandLineTest::new() .flag("metrics", None) .flag("metrics-address", Some("127.0.0.99")) @@ -572,6 +720,15 @@ fn metrics_address_flag() { .with_config(|config| assert_eq!(config.http_metrics.listen_addr, addr)); } #[test] +fn metrics_address_ipv6_flag() { + let addr = "::1".parse::().unwrap(); + CommandLineTest::new() + .flag("metrics", None) + .flag("metrics-address", Some("::1")) + .run_with_zero_port() + .with_config(|config| assert_eq!(config.http_metrics.listen_addr, addr)); +} +#[test] fn metrics_port_flag() { let port1 = unused_tcp_port().expect("Unable to find unused port."); let port2 = unused_tcp_port().expect("Unable to find unused port."); diff --git a/lighthouse/tests/exec.rs b/lighthouse/tests/exec.rs index 9526a1caf8..61e0677ca8 100644 --- a/lighthouse/tests/exec.rs +++ b/lighthouse/tests/exec.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; use std::process::{Command, Output}; use std::str::from_utf8; use tempfile::TempDir; +use types::{ChainSpec, Config, EthSpec}; pub trait CommandLineTestExec { type Config: DeserializeOwned; @@ -23,19 +24,22 @@ pub trait CommandLineTestExec { /// Executes the `Command` returned by `Self::cmd_mut` with temporary data directory, dumps /// the configuration and shuts down immediately. /// - /// Options `--datadir`, `--dump-config` and `--immediate-shutdown` must not be set on the - /// command. + /// Options `--datadir`, `--dump-config`, `--dump-chain-config` and `--immediate-shutdown` must + /// not be set on the command. fn run(&mut self) -> CompletedTest { // Setup temp directory. let tmp_dir = TempDir::new().expect("Unable to create temporary directory"); let tmp_config_path: PathBuf = tmp_dir.path().join("config.json"); + let tmp_chain_config_path: PathBuf = tmp_dir.path().join("chain_spec.yaml"); - // Add args --datadir --dump-config --immediate-shutdown + // Add args --datadir --dump-config --dump-chain-config --immediate-shutdown let cmd = self.cmd_mut(); cmd.arg("--datadir") .arg(tmp_dir.path().as_os_str()) - .arg(format!("--{}", "--dump-config")) + .arg(format!("--{}", "dump-config")) .arg(tmp_config_path.as_os_str()) + .arg(format!("--{}", "dump-chain-config")) + .arg(tmp_chain_config_path.as_os_str()) .arg("--immediate-shutdown"); // Run the command. @@ -47,23 +51,32 @@ pub trait CommandLineTestExec { // Grab the config. let config_file = File::open(tmp_config_path).expect("Unable to open dumped config"); let config: Self::Config = from_reader(config_file).expect("Unable to deserialize config"); + // Grab the chain config. + let spec_file = + File::open(tmp_chain_config_path).expect("Unable to open dumped chain spec"); + let chain_config: Config = + serde_yaml::from_reader(spec_file).expect("Unable to deserialize config"); - CompletedTest::new(config, tmp_dir) + CompletedTest::new(config, chain_config, tmp_dir) } /// Executes the `Command` returned by `Self::cmd_mut` dumps the configuration and /// shuts down immediately. /// - /// Options `--dump-config` and `--immediate-shutdown` must not be set on the command. + /// Options `--dump-config`, `--dump-chain-config` and `--immediate-shutdown` must not be set on + /// the command. fn run_with_no_datadir(&mut self) -> CompletedTest { // Setup temp directory. let tmp_dir = TempDir::new().expect("Unable to create temporary directory"); let tmp_config_path: PathBuf = tmp_dir.path().join("config.json"); + let tmp_chain_config_path: PathBuf = tmp_dir.path().join("chain_spec.yaml"); - // Add args --datadir --dump-config --immediate-shutdown + // Add args --datadir --dump-config --dump-chain-config --immediate-shutdown let cmd = self.cmd_mut(); - cmd.arg(format!("--{}", "--dump-config")) + cmd.arg(format!("--{}", "dump-config")) .arg(tmp_config_path.as_os_str()) + .arg(format!("--{}", "dump-chain-config")) + .arg(tmp_chain_config_path.as_os_str()) .arg("--immediate-shutdown"); // Run the command. @@ -75,8 +88,13 @@ pub trait CommandLineTestExec { // Grab the config. let config_file = File::open(tmp_config_path).expect("Unable to open dumped config"); let config: Self::Config = from_reader(config_file).expect("Unable to deserialize config"); + // Grab the chain config. + let spec_file = + File::open(tmp_chain_config_path).expect("Unable to open dumped chain spec"); + let chain_config: Config = + serde_yaml::from_reader(spec_file).expect("Unable to deserialize config"); - CompletedTest::new(config, tmp_dir) + CompletedTest::new(config, chain_config, tmp_dir) } } @@ -95,19 +113,35 @@ fn output_result(cmd: &mut Command) -> Result { pub struct CompletedTest { config: C, + chain_config: Config, dir: TempDir, } impl CompletedTest { - fn new(config: C, dir: TempDir) -> Self { - CompletedTest { config, dir } + fn new(config: C, chain_config: Config, dir: TempDir) -> Self { + CompletedTest { + config, + chain_config, + dir, + } } pub fn with_config(self, func: F) { func(&self.config); } + pub fn with_spec(self, func: F) { + let spec = ChainSpec::from_config::(&self.chain_config).unwrap(); + func(spec); + } + pub fn with_config_and_dir(self, func: F) { func(&self.config, &self.dir); } + + #[allow(dead_code)] + pub fn with_config_and_spec(self, func: F) { + let spec = ChainSpec::from_config::(&self.chain_config).unwrap(); + func(&self.config, spec); + } } diff --git a/lighthouse/tests/validator_client.rs b/lighthouse/tests/validator_client.rs index 76315daaa9..c14f5d27ba 100644 --- a/lighthouse/tests/validator_client.rs +++ b/lighthouse/tests/validator_client.rs @@ -4,7 +4,7 @@ use crate::exec::CommandLineTestExec; use bls::{Keypair, PublicKeyBytes}; use std::fs::File; use std::io::Write; -use std::net::Ipv4Addr; +use std::net::IpAddr; use std::path::PathBuf; use std::process::Command; use std::str::FromStr; @@ -320,7 +320,7 @@ fn http_flag() { } #[test] fn http_address_flag() { - let addr = "127.0.0.99".parse::().unwrap(); + let addr = "127.0.0.99".parse::().unwrap(); CommandLineTest::new() .flag("http-address", Some("127.0.0.99")) .flag("unencrypted-http-transport", None) @@ -328,9 +328,18 @@ fn http_address_flag() { .with_config(|config| assert_eq!(config.http_api.listen_addr, addr)); } #[test] +fn http_address_ipv6_flag() { + let addr = "::1".parse::().unwrap(); + CommandLineTest::new() + .flag("http-address", Some("::1")) + .flag("unencrypted-http-transport", None) + .run() + .with_config(|config| assert_eq!(config.http_api.listen_addr, addr)); +} +#[test] #[should_panic] fn missing_unencrypted_http_transport_flag() { - let addr = "127.0.0.99".parse::().unwrap(); + let addr = "127.0.0.99".parse::().unwrap(); CommandLineTest::new() .flag("http-address", Some("127.0.0.99")) .run() @@ -373,13 +382,21 @@ fn metrics_flag() { } #[test] fn metrics_address_flag() { - let addr = "127.0.0.99".parse::().unwrap(); + let addr = "127.0.0.99".parse::().unwrap(); CommandLineTest::new() .flag("metrics-address", Some("127.0.0.99")) .run() .with_config(|config| assert_eq!(config.http_metrics.listen_addr, addr)); } #[test] +fn metrics_address_ipv6_flag() { + let addr = "::1".parse::().unwrap(); + CommandLineTest::new() + .flag("metrics-address", Some("::1")) + .run() + .with_config(|config| assert_eq!(config.http_metrics.listen_addr, addr)); +} +#[test] fn metrics_port_flag() { CommandLineTest::new() .flag("metrics-port", Some("9090")) diff --git a/scripts/local_testnet/beacon_node.sh b/scripts/local_testnet/beacon_node.sh index 8151aac249..ac61b54dfb 100755 --- a/scripts/local_testnet/beacon_node.sh +++ b/scripts/local_testnet/beacon_node.sh @@ -46,6 +46,7 @@ exec lighthouse \ $SUBSCRIBE_ALL_SUBNETS \ --datadir $data_dir \ --testnet-dir $TESTNET_DIR \ + --enable-private-discovery \ --staking \ --enr-address 127.0.0.1 \ --enr-udp-port $network_port \ diff --git a/scripts/local_testnet/ganache_test_node.sh b/scripts/local_testnet/ganache_test_node.sh index 69edc1e770..7d97f2196a 100755 --- a/scripts/local_testnet/ganache_test_node.sh +++ b/scripts/local_testnet/ganache_test_node.sh @@ -4,7 +4,7 @@ set -Eeuo pipefail source ./vars.env -exec ganache-cli \ +exec ganache \ --defaultBalanceEther 1000000000 \ --gasLimit 1000000000 \ --accounts 10 \ @@ -12,4 +12,4 @@ exec ganache-cli \ --port 8545 \ --blockTime $SECONDS_PER_ETH1_BLOCK \ --networkId "$NETWORK_ID" \ - --chainId "$NETWORK_ID" + --chain.chainId "$NETWORK_ID" diff --git a/scripts/local_testnet/start_local_testnet.sh b/scripts/local_testnet/start_local_testnet.sh index 69d55660fa..a5c6c0b5eb 100755 --- a/scripts/local_testnet/start_local_testnet.sh +++ b/scripts/local_testnet/start_local_testnet.sh @@ -86,10 +86,10 @@ execute_command_add_PID() { echo "$!" >> $PID_FILE } -# Start ganache-cli, setup things up and start the bootnode. +# Start ganache, setup things up and start the bootnode. # The delays are necessary, hopefully there is a better way :( -# Delay to let ganache-cli to get started +# Delay to let ganache to get started execute_command_add_PID ganache_test_node.log ./ganache_test_node.sh sleeping 10 diff --git a/scripts/tests/doppelganger_protection.sh b/scripts/tests/doppelganger_protection.sh index d10249bdca..b0f9ce8265 100755 --- a/scripts/tests/doppelganger_protection.sh +++ b/scripts/tests/doppelganger_protection.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Requires `lighthouse`, ``lcli`, `ganache-cli`, `curl`, `jq` +# Requires `lighthouse`, ``lcli`, `ganache`, `curl`, `jq` BEHAVIOR=$1 diff --git a/testing/ef_tests/Makefile b/testing/ef_tests/Makefile index 816651bb45..13d8f631cc 100644 --- a/testing/ef_tests/Makefile +++ b/testing/ef_tests/Makefile @@ -1,4 +1,4 @@ -TESTS_TAG := v1.1.9 +TESTS_TAG := v1.1.10 TESTS = general minimal mainnet TARBALLS = $(patsubst %,%-$(TESTS_TAG).tar.gz,$(TESTS)) diff --git a/testing/ef_tests/src/decode.rs b/testing/ef_tests/src/decode.rs index d0c9e0d969..b5c0da53a0 100644 --- a/testing/ef_tests/src/decode.rs +++ b/testing/ef_tests/src/decode.rs @@ -18,7 +18,7 @@ pub fn log_file_access>(file_accessed: P) { let passed_test_list_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(ACCESSED_FILE_LOG_FILENAME); - let mut file = fs::OpenOptions::new() + let mut file = fs::File::options() .append(true) .create(true) .open(passed_test_list_path) diff --git a/testing/eth1_test_rig/src/ganache.rs b/testing/eth1_test_rig/src/ganache.rs index 505c010437..c82277dc75 100644 --- a/testing/eth1_test_rig/src/ganache.rs +++ b/testing/eth1_test_rig/src/ganache.rs @@ -36,14 +36,14 @@ impl GanacheInstance { loop { if start + Duration::from_millis(GANACHE_STARTUP_TIMEOUT_MILLIS) <= Instant::now() { break Err( - "Timed out waiting for ganache to start. Is ganache-cli installed?".to_string(), + "Timed out waiting for ganache to start. Is ganache installed?".to_string(), ); } let mut line = String::new(); if let Err(e) = reader.read_line(&mut line) { break Err(format!("Failed to read line from ganache process: {:?}", e)); - } else if line.starts_with("Listening on") { + } else if line.starts_with("RPC Listening on") { break Ok(()); } else { continue; @@ -69,13 +69,13 @@ impl GanacheInstance { }) } - /// Start a new `ganache-cli` process, waiting until it indicates that it is ready to accept + /// Start a new `ganache` process, waiting until it indicates that it is ready to accept /// RPC connections. pub fn new(network_id: u64, chain_id: u64) -> Result { let port = unused_tcp_port()?; let binary = match cfg!(windows) { - true => "ganache-cli.cmd", - false => "ganache-cli", + true => "ganache.cmd", + false => "ganache", }; let child = Command::new(binary) .stdout(Stdio::piped()) @@ -85,15 +85,13 @@ impl GanacheInstance { .arg("1000000000") .arg("--accounts") .arg("10") - .arg("--keepAliveTimeout") - .arg("0") .arg("--port") .arg(format!("{}", port)) .arg("--mnemonic") .arg("\"vast thought differ pull jewel broom cook wrist tribe word before omit\"") .arg("--networkId") .arg(format!("{}", network_id)) - .arg("--chainId") + .arg("--chain.chainId") .arg(format!("{}", chain_id)) .spawn() .map_err(|e| { @@ -110,8 +108,8 @@ impl GanacheInstance { pub fn fork(&self) -> Result { let port = unused_tcp_port()?; let binary = match cfg!(windows) { - true => "ganache-cli.cmd", - false => "ganache-cli", + true => "ganache.cmd", + false => "ganache", }; let child = Command::new(binary) .stdout(Stdio::piped()) @@ -119,9 +117,7 @@ impl GanacheInstance { .arg(self.endpoint()) .arg("--port") .arg(format!("{}", port)) - .arg("--keepAliveTimeout") - .arg("0") - .arg("--chainId") + .arg("--chain.chainId") .arg(format!("{}", self.chain_id)) .spawn() .map_err(|e| { @@ -178,8 +174,7 @@ impl GanacheInstance { .await .map(|_| ()) .map_err(|_| { - "utils should mine new block with evm_mine (only works with ganache-cli!)" - .to_string() + "utils should mine new block with evm_mine (only works with ganache!)".to_string() }) } } diff --git a/testing/eth1_test_rig/src/lib.rs b/testing/eth1_test_rig/src/lib.rs index fd4fc8ad59..1b80097cc8 100644 --- a/testing/eth1_test_rig/src/lib.rs +++ b/testing/eth1_test_rig/src/lib.rs @@ -1,6 +1,6 @@ //! Provides utilities for deploying and manipulating the eth2 deposit contract on the eth1 chain. //! -//! Presently used with [`ganache-cli`](https://github.com/trufflesuite/ganache-cli) to simulate +//! Presently used with [`ganache`](https://github.com/trufflesuite/ganache) to simulate //! the deposit contract for testing beacon node eth1 integration. //! //! Not tested to work with actual clients (e.g., geth). It should work fine, however there may be @@ -23,7 +23,7 @@ use web3::Web3; pub const DEPLOYER_ACCOUNTS_INDEX: usize = 0; pub const DEPOSIT_ACCOUNTS_INDEX: usize = 0; -/// Provides a dedicated ganache-cli instance with the deposit contract already deployed. +/// Provides a dedicated ganache instance with the deposit contract already deployed. pub struct GanacheEth1Instance { pub ganache: GanacheInstance, pub deposit_contract: DepositContract, diff --git a/testing/execution_engine_integration/Cargo.toml b/testing/execution_engine_integration/Cargo.toml index cd9836dd6c..fc8230c7a2 100644 --- a/testing/execution_engine_integration/Cargo.toml +++ b/testing/execution_engine_integration/Cargo.toml @@ -3,8 +3,6 @@ name = "execution_engine_integration" version = "0.1.0" edition = "2021" -build = "build.rs" - [dependencies] tempfile = "3.1.0" serde_json = "1.0.58" diff --git a/testing/execution_engine_integration/Makefile b/testing/execution_engine_integration/Makefile index 8bb2b59233..7062065066 100644 --- a/testing/execution_engine_integration/Makefile +++ b/testing/execution_engine_integration/Makefile @@ -1,5 +1,5 @@ test: - cargo test --release --locked + cargo run --release --locked clean: rm -rf execution_clients diff --git a/testing/execution_engine_integration/build.rs b/testing/execution_engine_integration/build.rs deleted file mode 100644 index bedf74fbd1..0000000000 --- a/testing/execution_engine_integration/build.rs +++ /dev/null @@ -1,62 +0,0 @@ -use std::env; -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::Command; - -const GETH_BRANCH: &str = "merge-kiln"; -const GETH_REPO_URL: &str = "https://github.com/MariusVanDerWijden/go-ethereum"; - -fn main() { - let manifest_dir: PathBuf = env::var("CARGO_MANIFEST_DIR").unwrap().into(); - let execution_clients_dir = manifest_dir.join("execution_clients"); - - if !execution_clients_dir.exists() { - fs::create_dir(&execution_clients_dir).unwrap(); - } - - build_geth(&execution_clients_dir); -} - -fn build_geth(execution_clients_dir: &Path) { - let repo_dir = execution_clients_dir.join("go-ethereum"); - - if !repo_dir.exists() { - // Clone the repo - assert!(Command::new("git") - .arg("clone") - .arg(GETH_REPO_URL) - .current_dir(&execution_clients_dir) - .output() - .expect("failed to clone geth repo") - .status - .success()); - } - - // Checkout the correct branch - assert!(Command::new("git") - .arg("checkout") - .arg(GETH_BRANCH) - .current_dir(&repo_dir) - .output() - .expect("failed to checkout geth branch") - .status - .success()); - - // Update the branch - assert!(Command::new("git") - .arg("pull") - .current_dir(&repo_dir) - .output() - .expect("failed to update geth branch") - .status - .success()); - - // Build geth - assert!(Command::new("make") - .arg("geth") - .current_dir(&repo_dir) - .output() - .expect("failed to make geth") - .status - .success()); -} diff --git a/testing/execution_engine_integration/src/build_utils.rs b/testing/execution_engine_integration/src/build_utils.rs new file mode 100644 index 0000000000..4d4a7bf1ce --- /dev/null +++ b/testing/execution_engine_integration/src/build_utils.rs @@ -0,0 +1,73 @@ +use crate::SUPPRESS_LOGS; +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output, Stdio}; + +pub fn prepare_dir() -> PathBuf { + let manifest_dir: PathBuf = env::var("CARGO_MANIFEST_DIR").unwrap().into(); + let execution_clients_dir = manifest_dir.join("execution_clients"); + + if !execution_clients_dir.exists() { + fs::create_dir(&execution_clients_dir).unwrap(); + } + + execution_clients_dir +} + +pub fn clone_repo(repo_dir: &Path, repo_url: &str) -> bool { + Command::new("git") + .arg("clone") + .arg(repo_url) + .arg("--recursive") + .current_dir(repo_dir) + .output() + .unwrap_or_else(|_| panic!("failed to clone repo at {}", repo_url)) + .status + .success() +} + +pub fn checkout_branch(repo_dir: &Path, branch_name: &str) -> bool { + Command::new("git") + .arg("checkout") + .arg(branch_name) + .current_dir(repo_dir) + .output() + .unwrap_or_else(|_| { + panic!( + "failed to checkout branch at {:?}/{}", + repo_dir, branch_name, + ) + }) + .status + .success() +} + +pub fn update_branch(repo_dir: &Path, branch_name: &str) -> bool { + Command::new("git") + .arg("pull") + .current_dir(repo_dir) + .output() + .unwrap_or_else(|_| panic!("failed to update branch at {:?}/{}", repo_dir, branch_name)) + .status + .success() +} + +pub fn check_command_output(output: Output, failure_msg: &'static str) { + if !output.status.success() { + if !SUPPRESS_LOGS { + dbg!(String::from_utf8_lossy(&output.stdout)); + dbg!(String::from_utf8_lossy(&output.stderr)); + } + panic!("{}", failure_msg); + } +} + +/// Builds the stdout/stderr handler for commands which might output to the terminal. +pub fn build_stdio() -> Stdio { + if SUPPRESS_LOGS { + Stdio::null() + } else { + Stdio::inherit() + } +} diff --git a/testing/execution_engine_integration/src/execution_engine.rs b/testing/execution_engine_integration/src/execution_engine.rs index cff36a025b..dd5d03be89 100644 --- a/testing/execution_engine_integration/src/execution_engine.rs +++ b/testing/execution_engine_integration/src/execution_engine.rs @@ -1,15 +1,19 @@ -use crate::{genesis_json::geth_genesis_json, SUPPRESS_LOGS}; +use execution_layer::DEFAULT_JWT_FILE; use sensitive_url::SensitiveUrl; use std::path::PathBuf; -use std::process::{Child, Command, Output, Stdio}; -use std::{env, fs::File}; +use std::process::Child; use tempfile::TempDir; use unused_port::unused_tcp_port; /// Defined for each EE type (e.g., Geth, Nethermind, etc). pub trait GenericExecutionEngine: Clone { fn init_datadir() -> TempDir; - fn start_client(datadir: &TempDir, http_port: u16) -> Child; + fn start_client( + datadir: &TempDir, + http_port: u16, + http_auth_port: u16, + jwt_secret_path: PathBuf, + ) -> Child; } /// Holds handle to a running EE process, plus some other metadata. @@ -19,6 +23,7 @@ pub struct ExecutionEngine { #[allow(dead_code)] datadir: TempDir, http_port: u16, + http_auth_port: u16, child: Child, } @@ -34,12 +39,15 @@ impl Drop for ExecutionEngine { impl ExecutionEngine { pub fn new(engine: E) -> Self { let datadir = E::init_datadir(); + let jwt_secret_path = datadir.path().join(DEFAULT_JWT_FILE); let http_port = unused_tcp_port().unwrap(); - let child = E::start_client(&datadir, http_port); + let http_auth_port = unused_tcp_port().unwrap(); + let child = E::start_client(&datadir, http_port, http_auth_port, jwt_secret_path); Self { engine, datadir, http_port, + http_auth_port, child, } } @@ -47,85 +55,12 @@ impl ExecutionEngine { pub fn http_url(&self) -> SensitiveUrl { SensitiveUrl::parse(&format!("http://127.0.0.1:{}", self.http_port)).unwrap() } -} -/* - * Geth-specific Implementation - */ - -#[derive(Clone)] -pub struct Geth; - -impl Geth { - fn binary_path() -> PathBuf { - let manifest_dir: PathBuf = env::var("CARGO_MANIFEST_DIR").unwrap().into(); - manifest_dir - .join("execution_clients") - .join("go-ethereum") - .join("build") - .join("bin") - .join("geth") - } -} - -impl GenericExecutionEngine for Geth { - fn init_datadir() -> TempDir { - let datadir = TempDir::new().unwrap(); - - let genesis_json_path = datadir.path().join("genesis.json"); - let mut file = File::create(&genesis_json_path).unwrap(); - let json = geth_genesis_json(); - serde_json::to_writer(&mut file, &json).unwrap(); - - let output = Command::new(Self::binary_path()) - .arg("--datadir") - .arg(datadir.path().to_str().unwrap()) - .arg("init") - .arg(genesis_json_path.to_str().unwrap()) - .output() - .expect("failed to init geth"); - - check_command_output(output, "geth init failed"); - - datadir + pub fn http_auth_url(&self) -> SensitiveUrl { + SensitiveUrl::parse(&format!("http://127.0.0.1:{}", self.http_auth_port)).unwrap() } - fn start_client(datadir: &TempDir, http_port: u16) -> Child { - let network_port = unused_tcp_port().unwrap(); - - Command::new(Self::binary_path()) - .arg("--datadir") - .arg(datadir.path().to_str().unwrap()) - .arg("--http") - .arg("--http.api") - .arg("engine,eth") - .arg("--http.port") - .arg(http_port.to_string()) - .arg("--port") - .arg(network_port.to_string()) - .stdout(build_stdio()) - .stderr(build_stdio()) - .spawn() - .expect("failed to start beacon node") - } -} - -fn check_command_output(output: Output, failure_msg: &'static str) { - if !output.status.success() { - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - - dbg!(stdout); - dbg!(stderr); - panic!("{}", failure_msg); - } -} - -/// Builds the stdout/stderr handler for commands which might output to the terminal. -fn build_stdio() -> Stdio { - if SUPPRESS_LOGS { - Stdio::null() - } else { - Stdio::inherit() + pub fn datadir(&self) -> PathBuf { + self.datadir.path().to_path_buf() } } diff --git a/testing/execution_engine_integration/src/genesis_json.rs b/testing/execution_engine_integration/src/genesis_json.rs index 87fdaec14a..0de56fba3c 100644 --- a/testing/execution_engine_integration/src/genesis_json.rs +++ b/testing/execution_engine_integration/src/genesis_json.rs @@ -40,3 +40,77 @@ pub fn geth_genesis_json() -> Value { "baseFeePerGas":"0x7" }) } + +/// Sourced from: +/// +/// https://github.com/NethermindEth/nethermind/blob/themerge_kintsugi/src/Nethermind/Chains/themerge_kintsugi_m2.json +pub fn nethermind_genesis_json() -> Value { + json!({ + "name": "TheMerge_Devnet", + "engine": { + "clique": { + "params": { + "period": 5, + "epoch": 30000 + } + } + }, + "params": { + "gasLimitBoundDivisor": "0x400", + "accountStartNonce": "0x0", + "maximumExtraDataSize": "0x20", + "minGasLimit": "0x1388", + "networkID": 1, + "eip150Transition": "0x0", + "eip155Transition": "0x0", + "eip158Transition": "0x0", + "eip160Transition": "0x0", + "eip161abcTransition": "0x0", + "eip161dTransition": "0x0", + "eip140Transition": "0x0", + "eip211Transition": "0x0", + "eip214Transition": "0x0", + "eip658Transition": "0x0", + "eip145Transition": "0x0", + "eip1014Transition": "0x0", + "eip1052Transition": "0x0", + "eip1283Transition": "0x0", + "eip1283DisableTransition": "0x0", + "eip152Transition": "0x0", + "eip1108Transition": "0x0", + "eip1344Transition": "0x0", + "eip1884Transition": "0x0", + "eip2028Transition": "0x0", + "eip2200Transition": "0x0", + "eip2565Transition": "0x0", + "eip2929Transition": "0x0", + "eip2930Transition": "0x0", + "eip1559Transition": "0x0", + "eip3198Transition": "0x0", + "eip3529Transition": "0x0", + "eip3541Transition": "0x0" + }, + "genesis": { + "seal": { + "ethereum": { + "nonce": "0x42", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + }, + "difficulty": "0x000000000", + "author": "0x0000000000000000000000000000000000000000", + "timestamp": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "extraData":"0x0000000000000000000000000000000000000000000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "gasLimit":"0x1C9C380", + "author": "0x0000000000000000000000000000000000000000", + "parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "baseFeePerGas":"0x7" + }, + "accounts": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance":"0x6d6172697573766477000000" + } + } + }) +} diff --git a/testing/execution_engine_integration/src/geth.rs b/testing/execution_engine_integration/src/geth.rs new file mode 100644 index 0000000000..bc07cea343 --- /dev/null +++ b/testing/execution_engine_integration/src/geth.rs @@ -0,0 +1,110 @@ +use crate::build_utils; +use crate::execution_engine::GenericExecutionEngine; +use crate::genesis_json::geth_genesis_json; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, Output}; +use std::{env, fs::File}; +use tempfile::TempDir; +use unused_port::unused_tcp_port; + +const GETH_BRANCH: &str = "merge-kiln-v2"; +const GETH_REPO_URL: &str = "https://github.com/MariusVanDerWijden/go-ethereum"; + +pub fn build_result(repo_dir: &Path) -> Output { + Command::new("make") + .arg("geth") + .current_dir(&repo_dir) + .output() + .expect("failed to make geth") +} + +pub fn build(execution_clients_dir: &Path) { + let repo_dir = execution_clients_dir.join("go-ethereum"); + + if !repo_dir.exists() { + // Clone the repo + assert!(build_utils::clone_repo( + execution_clients_dir, + GETH_REPO_URL + )); + } + + // Checkout the correct branch + assert!(build_utils::checkout_branch(&repo_dir, GETH_BRANCH)); + + // Update the branch + assert!(build_utils::update_branch(&repo_dir, GETH_BRANCH)); + + // Build geth + build_utils::check_command_output(build_result(&repo_dir), "make failed"); +} + +/* + * Geth-specific Implementation for GenericExecutionEngine + */ + +#[derive(Clone)] +pub struct GethEngine; + +impl GethEngine { + fn binary_path() -> PathBuf { + let manifest_dir: PathBuf = env::var("CARGO_MANIFEST_DIR").unwrap().into(); + manifest_dir + .join("execution_clients") + .join("go-ethereum") + .join("build") + .join("bin") + .join("geth") + } +} + +impl GenericExecutionEngine for GethEngine { + fn init_datadir() -> TempDir { + let datadir = TempDir::new().unwrap(); + + let genesis_json_path = datadir.path().join("genesis.json"); + let mut file = File::create(&genesis_json_path).unwrap(); + let json = geth_genesis_json(); + serde_json::to_writer(&mut file, &json).unwrap(); + + let output = Command::new(Self::binary_path()) + .arg("--datadir") + .arg(datadir.path().to_str().unwrap()) + .arg("init") + .arg(genesis_json_path.to_str().unwrap()) + .output() + .expect("failed to init geth"); + + build_utils::check_command_output(output, "geth init failed"); + + datadir + } + + fn start_client( + datadir: &TempDir, + http_port: u16, + http_auth_port: u16, + jwt_secret_path: PathBuf, + ) -> Child { + let network_port = unused_tcp_port().unwrap(); + + Command::new(Self::binary_path()) + .arg("--datadir") + .arg(datadir.path().to_str().unwrap()) + .arg("--http") + .arg("--http.api") + .arg("engine,eth") + .arg("--http.port") + .arg(http_port.to_string()) + .arg("--authrpc.port") + .arg(http_auth_port.to_string()) + .arg("--port") + .arg(network_port.to_string()) + .arg("--authrpc.jwtsecret") + .arg(jwt_secret_path.as_path().to_str().unwrap()) + .stdout(build_utils::build_stdio()) + .stderr(build_utils::build_stdio()) + .spawn() + .expect("failed to start geth") + } +} diff --git a/testing/execution_engine_integration/src/lib.rs b/testing/execution_engine_integration/src/lib.rs deleted file mode 100644 index 19a73e6bf2..0000000000 --- a/testing/execution_engine_integration/src/lib.rs +++ /dev/null @@ -1,12 +0,0 @@ -/// This library provides integration testing between Lighthouse and other execution engines. -/// -/// See the `tests/tests.rs` file to run tests. -mod execution_engine; -mod genesis_json; -mod test_rig; - -pub use execution_engine::Geth; -pub use test_rig::TestRig; - -/// Set to `false` to send logs to the console during tests. Logs are useful when debugging. -const SUPPRESS_LOGS: bool = true; diff --git a/testing/execution_engine_integration/src/main.rs b/testing/execution_engine_integration/src/main.rs new file mode 100644 index 0000000000..30c8132b7c --- /dev/null +++ b/testing/execution_engine_integration/src/main.rs @@ -0,0 +1,39 @@ +/// This binary runs integration tests between Lighthouse and execution engines. +/// +/// It will first attempt to build any supported integration clients, then it will run tests. +/// +/// A return code of `0` indicates the tests succeeded. +mod build_utils; +mod execution_engine; +mod genesis_json; +mod geth; +mod nethermind; +mod test_rig; + +use geth::GethEngine; +use nethermind::NethermindEngine; +use test_rig::TestRig; + +/// Set to `false` to send logs to the console during tests. Logs are useful when debugging. +const SUPPRESS_LOGS: bool = true; + +fn main() { + if cfg!(windows) { + panic!("windows is not supported, only linux"); + } + + test_geth(); + test_nethermind(); +} + +fn test_geth() { + let test_dir = build_utils::prepare_dir(); + geth::build(&test_dir); + TestRig::new(GethEngine).perform_tests_blocking(); +} + +fn test_nethermind() { + let test_dir = build_utils::prepare_dir(); + nethermind::build(&test_dir); + TestRig::new(NethermindEngine).perform_tests_blocking(); +} diff --git a/testing/execution_engine_integration/src/nethermind.rs b/testing/execution_engine_integration/src/nethermind.rs new file mode 100644 index 0000000000..833409c69e --- /dev/null +++ b/testing/execution_engine_integration/src/nethermind.rs @@ -0,0 +1,117 @@ +use crate::build_utils; +use crate::execution_engine::GenericExecutionEngine; +use crate::genesis_json::nethermind_genesis_json; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, Output}; +use std::{env, fs::File}; +use tempfile::TempDir; +use unused_port::unused_tcp_port; + +const NETHERMIND_BRANCH: &str = "kiln"; +const NETHERMIND_REPO_URL: &str = "https://github.com/NethermindEth/nethermind"; + +fn build_result(repo_dir: &Path) -> Output { + Command::new("dotnet") + .arg("build") + .arg("src/Nethermind/Nethermind.sln") + .arg("-c") + .arg("Release") + .current_dir(repo_dir) + .output() + .expect("failed to make nethermind") +} + +pub fn build(execution_clients_dir: &Path) { + let repo_dir = execution_clients_dir.join("nethermind"); + + if !repo_dir.exists() { + // Clone the repo + assert!(build_utils::clone_repo( + execution_clients_dir, + NETHERMIND_REPO_URL + )); + } + + // Checkout the correct branch + assert!(build_utils::checkout_branch(&repo_dir, NETHERMIND_BRANCH)); + + // Update the branch + assert!(build_utils::update_branch(&repo_dir, NETHERMIND_BRANCH)); + + // Build nethermind + build_utils::check_command_output(build_result(&repo_dir), "dotnet build failed"); + + // Build nethermind a second time to enable Merge-related features. + // Not sure why this is necessary. + build_utils::check_command_output(build_result(&repo_dir), "dotnet build failed"); +} + +/* + * Nethermind-specific Implementation for GenericExecutionEngine + */ + +#[derive(Clone)] +pub struct NethermindEngine; + +impl NethermindEngine { + fn binary_path() -> PathBuf { + let manifest_dir: PathBuf = env::var("CARGO_MANIFEST_DIR").unwrap().into(); + manifest_dir + .join("execution_clients") + .join("nethermind") + .join("src") + .join("Nethermind") + .join("Nethermind.Runner") + .join("bin") + .join("Release") + .join("net6.0") + .join("Nethermind.Runner") + } +} + +impl GenericExecutionEngine for NethermindEngine { + fn init_datadir() -> TempDir { + let datadir = TempDir::new().unwrap(); + + let genesis_json_path = datadir.path().join("genesis.json"); + let mut file = File::create(&genesis_json_path).unwrap(); + let json = nethermind_genesis_json(); + serde_json::to_writer(&mut file, &json).unwrap(); + + datadir + } + + fn start_client( + datadir: &TempDir, + http_port: u16, + http_auth_port: u16, + jwt_secret_path: PathBuf, + ) -> Child { + let network_port = unused_tcp_port().unwrap(); + let genesis_json_path = datadir.path().join("genesis.json"); + + Command::new(Self::binary_path()) + .arg("--datadir") + .arg(datadir.path().to_str().unwrap()) + .arg("--config") + .arg("themerge_kiln_testvectors") + .arg("--Init.ChainSpecPath") + .arg(genesis_json_path.to_str().unwrap()) + .arg("--JsonRpc.AdditionalRpcUrls") + .arg(format!("http://localhost:{}|http;ws|net;eth;subscribe;engine;web3;client|no-auth,http://localhost:{}|http;ws|net;eth;subscribe;engine;web3;client", http_port, http_auth_port)) + .arg("--JsonRpc.EnabledModules") + .arg("net,eth,subscribe,web3,admin,engine") + .arg("--JsonRpc.Port") + .arg(http_port.to_string()) + .arg("--Network.DiscoveryPort") + .arg(network_port.to_string()) + .arg("--Network.P2PPort") + .arg(network_port.to_string()) + .arg("--JsonRpc.JwtSecretFile") + .arg(jwt_secret_path.as_path().to_str().unwrap()) + .stdout(build_utils::build_stdio()) + .stderr(build_utils::build_stdio()) + .spawn() + .expect("failed to start nethermind") + } +} diff --git a/testing/execution_engine_integration/src/test_rig.rs b/testing/execution_engine_integration/src/test_rig.rs index 26dbc1bfdd..b788a7565c 100644 --- a/testing/execution_engine_integration/src/test_rig.rs +++ b/testing/execution_engine_integration/src/test_rig.rs @@ -4,7 +4,9 @@ use std::sync::Arc; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use task_executor::TaskExecutor; use tokio::time::sleep; -use types::{Address, ChainSpec, EthSpec, ExecutionBlockHash, Hash256, MainnetEthSpec, Uint256}; +use types::{ + Address, ChainSpec, EthSpec, ExecutionBlockHash, Hash256, MainnetEthSpec, Slot, Uint256, +}; const EXECUTION_ENGINE_START_TIMEOUT: Duration = Duration::from_secs(10); @@ -46,10 +48,17 @@ impl TestRig { let ee_a = { let execution_engine = ExecutionEngine::new(generic_engine.clone()); - let urls = vec![execution_engine.http_url()]; + let urls = vec![execution_engine.http_auth_url()]; + + let config = execution_layer::Config { + execution_endpoints: urls, + secret_files: vec![], + suggested_fee_recipient: Some(Address::repeat_byte(42)), + default_datadir: execution_engine.datadir(), + ..Default::default() + }; let execution_layer = - ExecutionLayer::from_urls(urls, fee_recipient, executor.clone(), log.clone()) - .unwrap(); + ExecutionLayer::from_config(config, executor.clone(), log.clone()).unwrap(); ExecutionPair { execution_engine, execution_layer, @@ -59,8 +68,16 @@ impl TestRig { let ee_b = { let execution_engine = ExecutionEngine::new(generic_engine); let urls = vec![execution_engine.http_url()]; + + let config = execution_layer::Config { + execution_endpoints: urls, + secret_files: vec![], + suggested_fee_recipient: fee_recipient, + default_datadir: execution_engine.datadir(), + ..Default::default() + }; let execution_layer = - ExecutionLayer::from_urls(urls, fee_recipient, executor, log).unwrap(); + ExecutionLayer::from_config(config, executor, log.clone()).unwrap(); ExecutionPair { execution_engine, execution_layer, @@ -108,6 +125,16 @@ impl TestRig { pub async fn perform_tests(&self) { self.wait_until_synced().await; + /* + * Check the transition config endpoint. + */ + for ee in [&self.ee_a, &self.ee_b] { + ee.execution_layer + .exchange_transition_configuration(&self.spec) + .await + .unwrap(); + } + /* * Read the terminal block hash from both pairs, check it's equal. */ @@ -138,7 +165,7 @@ impl TestRig { let parent_hash = terminal_pow_block_hash; let timestamp = timestamp_now(); - let random = Hash256::zero(); + let prev_randao = Hash256::zero(); let finalized_block_hash = ExecutionBlockHash::zero(); let proposer_index = 0; let valid_payload = self @@ -147,7 +174,7 @@ impl TestRig { .get_payload::( parent_hash, timestamp, - random, + prev_randao, finalized_block_hash, proposer_index, ) @@ -162,11 +189,12 @@ impl TestRig { */ let head_block_hash = valid_payload.block_hash; let finalized_block_hash = ExecutionBlockHash::zero(); - let payload_attributes = None; + let slot = Slot::new(42); + let head_block_root = Hash256::repeat_byte(42); let status = self .ee_a .execution_layer - .notify_forkchoice_updated(head_block_hash, finalized_block_hash, payload_attributes) + .notify_forkchoice_updated(head_block_hash, finalized_block_hash, slot, head_block_root) .await .unwrap(); assert_eq!(status, PayloadStatus::Syncing); @@ -194,11 +222,12 @@ impl TestRig { */ let head_block_hash = valid_payload.block_hash; let finalized_block_hash = ExecutionBlockHash::zero(); - let payload_attributes = None; + let slot = Slot::new(42); + let head_block_root = Hash256::repeat_byte(42); let status = self .ee_a .execution_layer - .notify_forkchoice_updated(head_block_hash, finalized_block_hash, payload_attributes) + .notify_forkchoice_updated(head_block_hash, finalized_block_hash, slot, head_block_root) .await .unwrap(); assert_eq!(status, PayloadStatus::Valid); @@ -210,7 +239,7 @@ impl TestRig { */ let mut invalid_payload = valid_payload.clone(); - invalid_payload.random = Hash256::from_low_u64_be(42); + invalid_payload.prev_randao = Hash256::from_low_u64_be(42); let status = self .ee_a .execution_layer @@ -227,7 +256,7 @@ impl TestRig { let parent_hash = valid_payload.block_hash; let timestamp = valid_payload.timestamp + 1; - let random = Hash256::zero(); + let prev_randao = Hash256::zero(); let finalized_block_hash = ExecutionBlockHash::zero(); let proposer_index = 0; let second_payload = self @@ -236,7 +265,7 @@ impl TestRig { .get_payload::( parent_hash, timestamp, - random, + prev_randao, finalized_block_hash, proposer_index, ) @@ -264,15 +293,22 @@ impl TestRig { */ let head_block_hash = valid_payload.block_hash; let finalized_block_hash = ExecutionBlockHash::zero(); - let payload_attributes = Some(PayloadAttributes { + let payload_attributes = PayloadAttributes { timestamp: second_payload.timestamp + 1, - random: Hash256::zero(), + prev_randao: Hash256::zero(), suggested_fee_recipient: Address::zero(), - }); + }; + let slot = Slot::new(42); + let head_block_root = Hash256::repeat_byte(100); + let validator_index = 0; + self.ee_a + .execution_layer + .insert_proposer(slot, head_block_root, validator_index, payload_attributes) + .await; let status = self .ee_a .execution_layer - .notify_forkchoice_updated(head_block_hash, finalized_block_hash, payload_attributes) + .notify_forkchoice_updated(head_block_hash, finalized_block_hash, slot, head_block_root) .await .unwrap(); assert_eq!(status, PayloadStatus::Valid); @@ -297,11 +333,12 @@ impl TestRig { */ let head_block_hash = second_payload.block_hash; let finalized_block_hash = ExecutionBlockHash::zero(); - let payload_attributes = None; + let slot = Slot::new(42); + let head_block_root = Hash256::repeat_byte(42); let status = self .ee_b .execution_layer - .notify_forkchoice_updated(head_block_hash, finalized_block_hash, payload_attributes) + .notify_forkchoice_updated(head_block_hash, finalized_block_hash, slot, head_block_root) .await .unwrap(); assert_eq!(status, PayloadStatus::Syncing); @@ -340,11 +377,12 @@ impl TestRig { */ let head_block_hash = second_payload.block_hash; let finalized_block_hash = ExecutionBlockHash::zero(); - let payload_attributes = None; + let slot = Slot::new(42); + let head_block_root = Hash256::repeat_byte(42); let status = self .ee_b .execution_layer - .notify_forkchoice_updated(head_block_hash, finalized_block_hash, payload_attributes) + .notify_forkchoice_updated(head_block_hash, finalized_block_hash, slot, head_block_root) .await .unwrap(); assert_eq!(status, PayloadStatus::Valid); diff --git a/testing/execution_engine_integration/tests/tests.rs b/testing/execution_engine_integration/tests/tests.rs deleted file mode 100644 index d4fcb29dca..0000000000 --- a/testing/execution_engine_integration/tests/tests.rs +++ /dev/null @@ -1,16 +0,0 @@ -#[cfg(not(target_family = "windows"))] -mod not_windows { - use execution_engine_integration::{Geth, TestRig}; - #[test] - fn geth() { - TestRig::new(Geth).perform_tests_blocking() - } -} - -#[cfg(target_family = "windows")] -mod windows { - #[test] - fn all_tests_skipped_on_windows() { - // - } -} diff --git a/testing/simulator/src/cli.rs b/testing/simulator/src/cli.rs index e0b2a37268..28f1a25627 100644 --- a/testing/simulator/src/cli.rs +++ b/testing/simulator/src/cli.rs @@ -10,7 +10,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .about( "Lighthouse Beacon Chain Simulator creates `n` beacon node and validator clients, \ each with `v` validators. A deposit contract is deployed at the start of the \ - simulation using a local `ganache-cli` instance (you must have `ganache-cli` \ + simulation using a local `ganache` instance (you must have `ganache` \ installed and avaliable on your path). All beacon nodes independently listen \ for genesis from the deposit contract, then start operating. \ \ diff --git a/testing/simulator/src/local_network.rs b/testing/simulator/src/local_network.rs index 50604040b7..3668cf0064 100644 --- a/testing/simulator/src/local_network.rs +++ b/testing/simulator/src/local_network.rs @@ -55,6 +55,7 @@ impl LocalNetwork { beacon_config.network.libp2p_port = BOOTNODE_PORT; beacon_config.network.enr_udp_port = Some(BOOTNODE_PORT); beacon_config.network.enr_tcp_port = Some(BOOTNODE_PORT); + beacon_config.network.discv5_config.table_filter = |_| true; let beacon_node = LocalBeaconNode::production(context.service_context("boot_node".into()), beacon_config) .await?; @@ -103,6 +104,7 @@ impl LocalNetwork { beacon_config.network.libp2p_port = BOOTNODE_PORT + count; beacon_config.network.enr_udp_port = Some(BOOTNODE_PORT + count); beacon_config.network.enr_tcp_port = Some(BOOTNODE_PORT + count); + beacon_config.network.discv5_config.table_filter = |_| true; } let mut write_lock = self_1.beacon_nodes.write(); diff --git a/testing/simulator/src/main.rs b/testing/simulator/src/main.rs index 4fb9a235b1..9e05a539cf 100644 --- a/testing/simulator/src/main.rs +++ b/testing/simulator/src/main.rs @@ -2,7 +2,7 @@ //! This crate provides a simluation that creates `n` beacon node and validator clients, each with //! `v` validators. A deposit contract is deployed at the start of the simulation using a local -//! `ganache-cli` instance (you must have `ganache-cli` installed and avaliable on your path). All +//! `ganache` instance (you must have `ganache` installed and avaliable on your path). All //! beacon nodes independently listen for genesis from the deposit contract, then start operating. //! //! As the simulation runs, there are checks made to ensure that all components are running diff --git a/validator_client/slashing_protection/src/slashing_database.rs b/validator_client/slashing_protection/src/slashing_database.rs index 9a743ee184..bd5f97f4d8 100644 --- a/validator_client/slashing_protection/src/slashing_database.rs +++ b/validator_client/slashing_protection/src/slashing_database.rs @@ -8,7 +8,7 @@ use crate::{signing_root_from_row, NotSafe, Safe, SignedAttestation, SignedBlock use filesystem::restrict_file_permissions; use r2d2_sqlite::SqliteConnectionManager; use rusqlite::{params, OptionalExtension, Transaction, TransactionBehavior}; -use std::fs::OpenOptions; +use std::fs::File; use std::path::Path; use std::time::Duration; use types::{AttestationData, BeaconBlockHeader, Epoch, Hash256, PublicKeyBytes, SignedRoot, Slot}; @@ -50,7 +50,7 @@ impl SlashingDatabase { /// /// Error if a database (or any file) already exists at `path`. pub fn create(path: &Path) -> Result { - let _file = OpenOptions::new() + let _file = File::options() .write(true) .read(true) .create_new(true) diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index cb9f80eab5..232526bac5 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -12,7 +12,7 @@ use sensitive_url::SensitiveUrl; use serde_derive::{Deserialize, Serialize}; use slog::{info, warn, Logger}; use std::fs; -use std::net::Ipv4Addr; +use std::net::IpAddr; use std::path::PathBuf; use types::{Address, GRAFFITI_BYTES_LEN}; @@ -238,8 +238,8 @@ impl Config { if let Some(address) = cli_args.value_of("http-address") { if cli_args.is_present("unencrypted-http-transport") { config.http_api.listen_addr = address - .parse::() - .map_err(|_| "http-address is not a valid IPv4 address.")?; + .parse::() + .map_err(|_| "http-address is not a valid IP address.")?; } else { return Err( "While using `--http-address`, you must also use `--unencrypted-http-transport`." @@ -273,8 +273,8 @@ impl Config { if let Some(address) = cli_args.value_of("metrics-address") { config.http_metrics.listen_addr = address - .parse::() - .map_err(|_| "metrics-address is not a valid IPv4 address.")?; + .parse::() + .map_err(|_| "metrics-address is not a valid IP address.")?; } if let Some(port) = cli_args.value_of("metrics-port") { diff --git a/validator_client/src/duties_service/sync.rs b/validator_client/src/duties_service/sync.rs index 02f45ebc45..9857561c48 100644 --- a/validator_client/src/duties_service/sync.rs +++ b/validator_client/src/duties_service/sync.rs @@ -403,7 +403,7 @@ pub async fn poll_sync_committee_duties_for_period sync_committee_period, ); return Ok(()); diff --git a/validator_client/src/http_api/mod.rs b/validator_client/src/http_api/mod.rs index 590bfc208a..8e1f5a7390 100644 --- a/validator_client/src/http_api/mod.rs +++ b/validator_client/src/http_api/mod.rs @@ -16,7 +16,7 @@ use slog::{crit, info, warn, Logger}; use slot_clock::SlotClock; use std::future::Future; use std::marker::PhantomData; -use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; use std::sync::{Arc, Weak}; use tokio::runtime::Runtime; @@ -69,7 +69,7 @@ pub struct Context { #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] pub struct Config { pub enabled: bool, - pub listen_addr: Ipv4Addr, + pub listen_addr: IpAddr, pub listen_port: u16, pub allow_origin: Option, } @@ -78,7 +78,7 @@ impl Default for Config { fn default() -> Self { Self { enabled: false, - listen_addr: Ipv4Addr::new(127, 0, 0, 1), + listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), listen_port: 5062, allow_origin: None, } @@ -609,7 +609,7 @@ pub fn serve( .with(cors_builder.build()); let (listening_socket, server) = warp::serve(routes).try_bind_with_graceful_shutdown( - SocketAddrV4::new(config.listen_addr, config.listen_port), + SocketAddr::new(config.listen_addr, config.listen_port), async { shutdown.await; }, diff --git a/validator_client/src/http_api/tests.rs b/validator_client/src/http_api/tests.rs index 788ca28ffb..eef76eb363 100644 --- a/validator_client/src/http_api/tests.rs +++ b/validator_client/src/http_api/tests.rs @@ -27,7 +27,7 @@ use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; use slot_clock::{SlotClock, TestingSlotClock}; use std::future::Future; use std::marker::PhantomData; -use std::net::Ipv4Addr; +use std::net::{IpAddr, Ipv4Addr}; use std::sync::Arc; use std::time::Duration; use task_executor::TaskExecutor; @@ -120,7 +120,7 @@ impl ApiTester { spec: E::default_spec(), config: HttpConfig { enabled: true, - listen_addr: Ipv4Addr::new(127, 0, 0, 1), + listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), listen_port: 0, allow_origin: None, }, diff --git a/validator_client/src/http_metrics/mod.rs b/validator_client/src/http_metrics/mod.rs index fcf98987ad..51a2d3f8a5 100644 --- a/validator_client/src/http_metrics/mod.rs +++ b/validator_client/src/http_metrics/mod.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use slog::{crit, info, Logger}; use slot_clock::SystemTimeSlotClock; use std::future::Future; -use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::Arc; use types::EthSpec; use warp::{http::Response, Filter}; @@ -53,7 +53,7 @@ pub struct Context { #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] pub struct Config { pub enabled: bool, - pub listen_addr: Ipv4Addr, + pub listen_addr: IpAddr, pub listen_port: u16, pub allow_origin: Option, } @@ -62,7 +62,7 @@ impl Default for Config { fn default() -> Self { Self { enabled: false, - listen_addr: Ipv4Addr::new(127, 0, 0, 1), + listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), listen_port: 5064, allow_origin: None, } @@ -134,7 +134,7 @@ pub fn serve( .with(cors_builder.build()); let (listening_socket, server) = warp::serve(routes).try_bind_with_graceful_shutdown( - SocketAddrV4::new(config.listen_addr, config.listen_port), + SocketAddr::new(config.listen_addr, config.listen_port), async { shutdown.await; }, diff --git a/validator_client/src/key_cache.rs b/validator_client/src/key_cache.rs index 5947621023..2088aa6831 100644 --- a/validator_client/src/key_cache.rs +++ b/validator_client/src/key_cache.rs @@ -11,7 +11,7 @@ use eth2_keystore::{ use rand::prelude::*; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use std::fs::OpenOptions; +use std::fs::File; use std::io; use std::path::{Path, PathBuf}; @@ -101,7 +101,7 @@ impl KeyCache { /// Open an existing file, returning an error if the file does not exist. pub fn open>(validators_dir: P) -> Result { let cache_path = validators_dir.as_ref().join(CACHE_FILENAME); - let file = OpenOptions::new() + let file = File::options() .read(true) .create_new(false) .open(&cache_path) diff --git a/validator_client/src/notifier.rs b/validator_client/src/notifier.rs index f521f2fd30..6157027cb1 100644 --- a/validator_client/src/notifier.rs +++ b/validator_client/src/notifier.rs @@ -89,7 +89,7 @@ async fn notify( info!( log, "All validators active"; - "proposers" => proposing_validators, + "current_epoch_proposers" => proposing_validators, "active_validators" => attesting_validators, "total_validators" => total_validators, "epoch" => format!("{}", epoch), @@ -99,7 +99,7 @@ async fn notify( info!( log, "Some validators active"; - "proposers" => proposing_validators, + "current_epoch_proposers" => proposing_validators, "active_validators" => attesting_validators, "total_validators" => total_validators, "epoch" => format!("{}", epoch), diff --git a/validator_client/src/preparation_service.rs b/validator_client/src/preparation_service.rs index e532bd2461..ad04717cc2 100644 --- a/validator_client/src/preparation_service.rs +++ b/validator_client/src/preparation_service.rs @@ -11,6 +11,9 @@ use std::sync::Arc; use tokio::time::{sleep, Duration}; use types::{Address, ChainSpec, EthSpec, ProposerPreparationData}; +/// Number of epochs before the Bellatrix hard fork to begin posting proposer preparations. +const PROPOSER_PREPARATION_LOOKAHEAD_EPOCHS: u64 = 2; + /// Builds an `PreparationService`. pub struct PreparationServiceBuilder { validator_store: Option>>, @@ -122,15 +125,9 @@ impl PreparationService { let log = self.context.log().clone(); let slot_duration = Duration::from_secs(spec.seconds_per_slot); - let duration_to_next_epoch = self - .slot_clock - .duration_to_next_epoch(E::slots_per_epoch()) - .ok_or("Unable to determine duration to next epoch")?; - info!( log, "Proposer preparation service started"; - "next_update_millis" => duration_to_next_epoch.as_millis() ); let executor = self.context.executor.clone(); @@ -138,17 +135,19 @@ impl PreparationService { let interval_fut = async move { loop { - // Poll the endpoint immediately to ensure fee recipients are received. - self.prepare_proposers_and_publish(&spec) - .await - .map_err(|e| { - error!( - log, - "Error during proposer preparation"; - "error" => format!("{:?}", e), - ) - }) - .unwrap_or(()); + if self.should_publish_at_current_slot(&spec) { + // Poll the endpoint immediately to ensure fee recipients are received. + self.prepare_proposers_and_publish(&spec) + .await + .map_err(|e| { + error!( + log, + "Error during proposer preparation"; + "error" => ?e, + ) + }) + .unwrap_or(()); + } if let Some(duration_to_next_slot) = self.slot_clock.duration_to_next_slot() { sleep(duration_to_next_slot).await; @@ -164,6 +163,20 @@ impl PreparationService { Ok(()) } + /// Return `true` if the current slot is close to or past the Bellatrix fork epoch. + /// + /// This avoids spamming the BN with preparations before the Bellatrix fork epoch, which may + /// cause errors if it doesn't support the preparation API. + fn should_publish_at_current_slot(&self, spec: &ChainSpec) -> bool { + let current_epoch = self + .slot_clock + .now() + .map_or(E::genesis_epoch(), |slot| slot.epoch(E::slots_per_epoch())); + spec.bellatrix_fork_epoch.map_or(false, |fork_epoch| { + current_epoch + PROPOSER_PREPARATION_LOOKAHEAD_EPOCHS >= fork_epoch + }) + } + /// Prepare proposer preparations and send to beacon node async fn prepare_proposers_and_publish(&self, spec: &ChainSpec) -> Result<(), String> { let preparation_data = self.collect_preparation_data(spec); diff --git a/validator_client/src/sync_committee_service.rs b/validator_client/src/sync_committee_service.rs index a349cb75f5..105bf7d27f 100644 --- a/validator_client/src/sync_committee_service.rs +++ b/validator_client/src/sync_committee_service.rs @@ -4,7 +4,7 @@ use environment::RuntimeContext; use eth2::types::BlockId; use futures::future::join_all; use futures::future::FutureExt; -use slog::{crit, debug, error, info, trace, warn}; +use slog::{crit, debug, error, info, trace}; use slot_clock::SlotClock; use std::collections::HashMap; use std::ops::Deref; @@ -154,11 +154,16 @@ impl SyncCommitteeService { .checked_sub(slot_duration / 3) .unwrap_or_else(|| Duration::from_secs(0)); - let slot_duties = self + let slot_duties = if let Some(duties) = self .duties_service .sync_duties .get_duties_for_slot::(slot, &self.duties_service.spec) - .ok_or_else(|| format!("Error fetching duties for slot {}", slot))?; + { + duties + } else { + debug!(log, "No duties known for slot {}", slot); + return Ok(()); + }; if slot_duties.duties.is_empty() { debug!( @@ -490,9 +495,9 @@ impl SyncCommitteeService { spec, )), None => { - warn!( + debug!( log, - "Missing duties for subscription"; + "No duties for subscription"; "slot" => duty_slot, ); all_succeeded = false; @@ -500,6 +505,15 @@ impl SyncCommitteeService { } } + if subscriptions.is_empty() { + debug!( + log, + "No sync subscriptions to send"; + "slot" => slot, + ); + return Ok(()); + } + // Post subscriptions to BN. debug!( log,