diff --git a/Cargo.lock b/Cargo.lock index c5e1713363..a8f87ab5b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,15 +13,15 @@ dependencies = [ "eth2_ssz", "eth2_ssz_derive", "eth2_testnet_config", - "futures 0.1.29", - "hex 0.3.2", + "futures 0.3.4", + "hex 0.4.2", "libc", "rayon", "slog", "slog-async", "slog-term", "tempdir", - "tokio 0.1.22", + "tokio 0.2.20", "types", "validator_client", "web3", @@ -160,8 +160,8 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d0864d84b8e07b145449be9a8537db86bf9de5ce03b913214694643b4743502" dependencies = [ - "quote 1.0.4", - "syn 1.0.18", + "quote", + "syn", ] [[package]] @@ -266,15 +266,6 @@ dependencies = [ "safemem", ] -[[package]] -name = "base64" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" -dependencies = [ - "byteorder 1.3.4", -] - [[package]] name = "base64" version = "0.11.0" @@ -283,14 +274,16 @@ checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" [[package]] name = "base64" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5ca2cd0adc3f48f9e9ea5a6bbdf9ccc0bfade884847e484d452414c7ccffb3" +checksum = "53d1ccbaf7d9ec9537465a97bf19edc1a4e158ecb49fc16178202238c569cc42" [[package]] name = "beacon_chain" version = "0.2.0" dependencies = [ + "bitvec", + "bls", "environment", "eth1", "eth2_config", @@ -307,7 +300,7 @@ dependencies = [ "lru", "merkle_proof", "operation_pool", - "parking_lot 0.9.0", + "parking_lot 0.10.2", "proto_array_fork_choice", "rand 0.7.3", "rayon", @@ -322,7 +315,7 @@ dependencies = [ "state_processing", "store", "tempfile", - "tokio 0.1.22", + "tokio 0.2.20", "tree_hash", "types", "websocket_server", @@ -338,14 +331,14 @@ dependencies = [ "client", "ctrlc", "dirs", - "env_logger 0.7.1", + "env_logger", "environment", "eth2-libp2p", "eth2_config", "eth2_ssz", "eth2_testnet_config", - "exit-future 0.1.4", - "futures 0.1.29", + "exit-future", + "futures 0.3.4", "genesis", "logging", "node_test_rig", @@ -355,7 +348,7 @@ dependencies = [ "slog-async", "slog-term", "store", - "tokio 0.1.22", + "tokio 0.2.20", "tokio-timer 0.2.13", "toml", "types", @@ -474,7 +467,7 @@ dependencies = [ "eth2_hashing", "eth2_ssz", "eth2_ssz_types", - "hex 0.3.2", + "hex 0.4.2", "milagro_bls", "rand 0.7.3", "serde", @@ -633,7 +626,7 @@ dependencies = [ "dirs", "eth2_ssz", "eth2_testnet_config", - "hex 0.3.2", + "hex 0.4.2", "types", ] @@ -658,14 +651,14 @@ dependencies = [ "eth2-libp2p", "eth2_config", "eth2_ssz", - "futures 0.1.29", + "futures 0.3.4", "genesis", "lazy_static", "lighthouse_metrics", "network", - "parking_lot 0.9.0", + "parking_lot 0.10.2", "prometheus", - "reqwest 0.9.24", + "reqwest", "rest_api", "serde", "serde_derive", @@ -676,7 +669,7 @@ dependencies = [ "slot_clock", "store", "timer", - "tokio 0.1.22", + "tokio 0.2.20", "toml", "tree_hash", "types", @@ -715,8 +708,8 @@ dependencies = [ name = "compare_fields_derive" version = "0.2.0" dependencies = [ - "quote 0.6.13", - "syn 0.15.44", + "quote", + "syn", ] [[package]] @@ -755,34 +748,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" -[[package]] -name = "cookie" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "888604f00b3db336d2af898ec3c1d5d0ddf5e6d462220f2ededc33a87ac4bbd5" -dependencies = [ - "time", - "url 1.7.2", -] - -[[package]] -name = "cookie_store" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46750b3f362965f197996c4448e4a0935e791bf7d6631bfce9ee0af3d24c919c" -dependencies = [ - "cookie", - "failure", - "idna 0.1.5", - "log 0.4.8", - "publicsuffix", - "serde", - "serde_json", - "time", - "try_from", - "url 1.7.2", -] - [[package]] name = "core-foundation" version = "0.7.0" @@ -843,12 +808,6 @@ dependencies = [ "itertools 0.9.0", ] -[[package]] -name = "crossbeam" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd66663db5a988098a89599d4857919b3acf7f61402e61365acfd3919857b9be" - [[package]] name = "crossbeam" version = "0.7.3" @@ -896,7 +855,7 @@ dependencies = [ "lazy_static", "maybe-uninit", "memoffset", - "scopeguard 1.1.0", + "scopeguard", ] [[package]] @@ -1025,7 +984,7 @@ version = "0.2.0" dependencies = [ "eth2_ssz", "ethabi", - "reqwest 0.9.24", + "reqwest", "serde_json", "tree_hash", "types", @@ -1033,13 +992,13 @@ dependencies = [ [[package]] name = "derivative" -version = "1.0.4" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6d883546668a3e2011b6a716a7330b82eabb0151b138217f632c8243e17135" +checksum = "cb582b60359da160a9477ee80f15c8d784c477e69c217ef2cdd4169c24ea380f" dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", - "syn 0.15.44", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1048,9 +1007,9 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cee758ebd1c79a9c6fb95f242dcc30bdbf555c28369ae908d21fdaf81537496" dependencies = [ - "proc-macro2 1.0.12", - "quote 1.0.4", - "syn 1.0.18", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1059,9 +1018,9 @@ version = "0.99.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2323f3f47db9a0e77ce7a300605d8d2098597fc451ed1a97bb1f6411bb550a7" dependencies = [ - "proc-macro2 1.0.12", - "quote 1.0.4", - "syn 1.0.18", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1159,7 +1118,7 @@ dependencies = [ "eth2_ssz", "eth2_ssz_derive", "ethereum-types", - "hex 0.3.2", + "hex 0.4.2", "rayon", "serde", "serde_derive", @@ -1194,7 +1153,7 @@ version = "0.1.0-alpha.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5d4c8e39a1c41e3ffd5048e0617888cdc1316c87720ede64c83f3914056a58e" dependencies = [ - "base64 0.12.0", + "base64 0.12.1", "bs58", "ed25519-dalek", "hex 0.4.2", @@ -1207,19 +1166,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "env_logger" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafcde04e90a5226a6443b7aabdb016ba2f8307c847d524724bd9b346dd1a2d3" -dependencies = [ - "atty", - "humantime", - "log 0.4.8", - "regex", - "termcolor", -] - [[package]] name = "env_logger" version = "0.7.1" @@ -1240,18 +1186,18 @@ dependencies = [ "beacon_node", "clap", "ctrlc", - "env_logger 0.6.2", + "env_logger", "eth2_config", "eth2_testnet_config", - "futures 0.1.29", + "futures 0.3.4", "logging", - "parking_lot 0.7.1", + "parking_lot 0.10.2", "slog", "slog-async", "slog-json", "slog-term", "sloggers", - "tokio 0.1.22", + "tokio 0.2.20", "types", ] @@ -1275,13 +1221,13 @@ dependencies = [ "eth2_ssz", "eth2_ssz_derive", "futures 0.3.4", - "hex 0.3.2", + "hex 0.4.2", "lazy_static", "libflate", "lighthouse_metrics", "merkle_proof", - "parking_lot 0.7.1", - "reqwest 0.10.4", + "parking_lot 0.10.2", + "reqwest", "serde", "serde_json", "slog", @@ -1310,7 +1256,7 @@ dependencies = [ name = "eth2-libp2p" version = "0.2.0" dependencies = [ - "base64 0.12.0", + "base64 0.12.1", "dirs", "discv5", "error-chain", @@ -1331,7 +1277,7 @@ dependencies = [ "sha2", "slog", "slog-async", - "slog-stdlog 4.0.0", + "slog-stdlog", "slog-term", "smallvec 1.4.0", "snap", @@ -1371,9 +1317,9 @@ dependencies = [ name = "eth2_interop_keypairs" version = "0.2.0" dependencies = [ - "base64 0.11.0", + "base64 0.12.1", "eth2_hashing", - "hex 0.3.2", + "hex 0.4.2", "lazy_static", "milagro_bls", "num-bigint", @@ -1395,8 +1341,8 @@ dependencies = [ name = "eth2_ssz_derive" version = "0.1.0" dependencies = [ - "quote 0.6.13", - "syn 0.15.44", + "quote", + "syn", ] [[package]] @@ -1420,7 +1366,7 @@ version = "0.2.0" dependencies = [ "eth2-libp2p", "eth2_ssz", - "reqwest 0.9.24", + "reqwest", "serde", "serde_yaml", "tempdir", @@ -1468,16 +1414,6 @@ dependencies = [ "uint", ] -[[package]] -name = "exit-future" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8013f441e38e31c670e7f34ec8f1d5d3a2bd9d303c1ff83976ca886005e8f48" -dependencies = [ - "futures 0.1.29", - "parking_lot 0.7.1", -] - [[package]] name = "exit-future" version = "0.2.0" @@ -1487,28 +1423,6 @@ dependencies = [ "futures 0.3.4", ] -[[package]] -name = "failure" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" -dependencies = [ - "backtrace", - "failure_derive", -] - -[[package]] -name = "failure_derive" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" -dependencies = [ - "proc-macro2 1.0.12", - "quote 1.0.4", - "syn 1.0.18", - "synstructure", -] - [[package]] name = "fake-simd" version = "0.1.2" @@ -1662,9 +1576,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a5081aa3de1f7542a794a397cde100ed903b0630152d0973479018fd85423a7" dependencies = [ "proc-macro-hack", - "proc-macro2 1.0.12", - "quote 1.0.4", - "syn 1.0.18", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1747,11 +1661,11 @@ dependencies = [ "eth1_test_rig", "eth2_hashing", "eth2_ssz", - "exit-future 0.2.0", + "exit-future", "futures 0.3.4", "int_to_bytes", "merkle_proof", - "parking_lot 0.7.1", + "parking_lot 0.10.2", "rayon", "serde", "serde_derive", @@ -1815,9 +1729,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "377038bf3c89d18d6ca1431e7a5027194fbd724ca10592b9487ede5e8e144f42" +checksum = "79b7246d7e4b979c03fa093da39cfb3617a96bbeee6310af63991668d7e843ff" dependencies = [ "bytes 0.5.4", "fnv", @@ -2029,7 +1943,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.2.4", + "h2 0.2.5", "http 0.2.1", "http-body 0.3.1", "httparse", @@ -2066,7 +1980,7 @@ dependencies = [ "hyper 0.13.5", "native-tls", "tokio 0.2.20", - "tokio-tls 0.3.0", + "tokio-tls 0.3.1", ] [[package]] @@ -2131,8 +2045,8 @@ dependencies = [ name = "int_to_bytes" version = "0.2.0" dependencies = [ - "bytes 0.4.12", - "hex 0.3.2", + "bytes 0.5.4", + "hex 0.4.2", "yaml-rust", ] @@ -2261,7 +2175,7 @@ dependencies = [ "eth2_testnet_config", "futures 0.3.4", "genesis", - "hex 0.3.2", + "hex 0.4.2", "log 0.4.8", "regex", "serde", @@ -2302,16 +2216,22 @@ checksum = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" [[package]] name = "libflate" -version = "0.1.27" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9135df43b1f5d0e333385cb6e7897ecd1a43d7d11b91ac003f4d2c2d2401fdd" +checksum = "a1fbe6b967a94346446d37ace319ae85be7eca261bb8149325811ac435d35d64" dependencies = [ "adler32", "crc32fast", + "libflate_lz77", "rle-decode-fast", - "take_mut", ] +[[package]] +name = "libflate_lz77" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3286f09f7d4926fc486334f28d8d2e6ebe4f7f9994494b6dab27ddfad2c9b11b" + [[package]] name = "libp2p" version = "0.18.1" @@ -2390,8 +2310,8 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "329127858e4728db5ab60c33d5ae352a999325fdf190ed022ec7d3a4685ae2e6" dependencies = [ - "quote 1.0.4", - "syn 1.0.18", + "quote", + "syn", ] [[package]] @@ -2763,15 +2683,16 @@ dependencies = [ "beacon_node", "clap", "clap_utils", - "env_logger 0.6.2", + "env_logger", "environment", - "futures 0.1.29", + "eth2_testnet_config", + "futures 0.3.4", "logging", "slog", "slog-async", "slog-term", "sloggers", - "tokio 0.1.22", + "tokio 0.2.20", "types", "validator_client", ] @@ -2786,19 +2707,9 @@ dependencies = [ [[package]] name = "linked-hash-map" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83" - -[[package]] -name = "lock_api" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ebf1391f6acad60e5c8b43706dde4582df75c06698ab44511d15016bc2442c" -dependencies = [ - "owning_ref", - "scopeguard 0.3.3", -] +checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" [[package]] name = "lock_api" @@ -2806,7 +2717,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" dependencies = [ - "scopeguard 1.1.0", + "scopeguard", ] [[package]] @@ -3080,11 +2991,11 @@ dependencies = [ "eth2-libp2p", "eth2_ssz", "fnv", - "futures 0.1.29", + "futures 0.3.4", "genesis", "hashset_delay", - "hex 0.3.2", - "parking_lot 0.9.0", + "hex 0.4.2", + "parking_lot 0.10.2", "rand 0.7.3", "rest_types", "rlp", @@ -3094,7 +3005,7 @@ dependencies = [ "smallvec 1.4.0", "store", "tempdir", - "tokio 0.1.22", + "tokio 0.2.20", "tree_hash", "types", ] @@ -3119,14 +3030,14 @@ dependencies = [ "beacon_node", "environment", "eth2_config", - "futures 0.1.29", + "futures 0.3.4", "genesis", "remote_beacon_node", - "reqwest 0.9.24", + "reqwest", "serde", "tempdir", "types", - "url 1.7.2", + "url 2.1.1", "validator_client", ] @@ -3240,7 +3151,7 @@ dependencies = [ "eth2_ssz", "eth2_ssz_derive", "int_to_bytes", - "parking_lot 0.9.0", + "parking_lot 0.10.2", "rand 0.7.3", "serde", "serde_derive", @@ -3249,15 +3160,6 @@ dependencies = [ "types", ] -[[package]] -name = "owning_ref" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff55baddef9e4ad00f88b6c743a2a8062d4c6ade126c2a528644b8e444d52ce" -dependencies = [ - "stable_deref_trait", -] - [[package]] name = "parity-multiaddr" version = "0.8.0" @@ -3294,23 +3196,13 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9777aa91b8ad9dd5aaa04a9b6bcb02c7f1deb952fca5a66034d5e63afc5c6f" -[[package]] -name = "parking_lot" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab41b4aed082705d1056416ae4468b6ea99d52599ecf3169b00088d43113e337" -dependencies = [ - "lock_api 0.1.5", - "parking_lot_core 0.4.0", -] - [[package]] name = "parking_lot" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" dependencies = [ - "lock_api 0.3.4", + "lock_api", "parking_lot_core 0.6.2", "rustc_version", ] @@ -3321,23 +3213,10 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" dependencies = [ - "lock_api 0.3.4", + "lock_api", "parking_lot_core 0.7.2", ] -[[package]] -name = "parking_lot_core" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94c8c7923936b28d546dfd14d4472eaf34c99b14e1c973a32b3e6d4eb04298c9" -dependencies = [ - "libc", - "rand 0.6.5", - "rustc_version", - "smallvec 0.6.13", - "winapi 0.3.8", -] - [[package]] name = "parking_lot_core" version = "0.6.2" @@ -3404,16 +3283,16 @@ version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4d7346ac577ff1296e06a418e7618e22655bae834d4970cb6e39d6da8119969" dependencies = [ - "proc-macro2 1.0.12", - "quote 1.0.4", - "syn 1.0.18", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "pin-project-lite" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "237844750cfbb86f67afe27eee600dfbbcb6188d734139b534cbfbf4f96792ae" +checksum = "f7505eeebd78492e0f6108f7171c4948dbb120ee8119d9d77d0afa5469bef67f" [[package]] name = "pin-utils" @@ -3470,36 +3349,27 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e946095f9d3ed29ec38de908c22f95d9ac008e424c7bcae54c75a79c527c694" -[[package]] -name = "proc-macro2" -version = "0.4.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" -dependencies = [ - "unicode-xid 0.1.0", -] - [[package]] name = "proc-macro2" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8872cf6f48eee44265156c111456a700ab3483686b3f96df4cf5481c89157319" dependencies = [ - "unicode-xid 0.2.0", + "unicode-xid", ] [[package]] name = "prometheus" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5567486d5778e2c6455b1b90ff1c558f29e751fc018130fa182e15828e728af1" +checksum = "b0575e258dab62268e7236d7307caa38848acbda7ec7ab87bd9093791e999d20" dependencies = [ "cfg-if", "fnv", "lazy_static", "protobuf", - "quick-error", "spin", + "thiserror", ] [[package]] @@ -3538,9 +3408,9 @@ checksum = "537aa19b95acde10a12fec4301466386f757403de4cd4e5b4fa78fb5ecb18f72" dependencies = [ "anyhow", "itertools 0.8.2", - "proc-macro2 1.0.12", - "quote 1.0.4", - "syn 1.0.18", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -3559,8 +3429,8 @@ version = "0.2.0" dependencies = [ "eth2_ssz", "eth2_ssz_derive", - "itertools 0.8.2", - "parking_lot 0.9.0", + "itertools 0.9.0", + "parking_lot 0.10.2", "serde", "serde_derive", "serde_yaml", @@ -3573,19 +3443,6 @@ version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e86d370532557ae7573551a1ec8235a0f8d6cb276c7c9e6aa490b511c447485" -[[package]] -name = "publicsuffix" -version = "1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bbaa49075179162b49acac1c6aa45fb4dafb5f13cf6794276d77bc7fd95757b" -dependencies = [ - "error-chain", - "idna 0.2.0", - "lazy_static", - "regex", - "url 2.1.1", -] - [[package]] name = "quick-error" version = "1.2.3" @@ -3598,7 +3455,7 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44883e74aa97ad63db83c4bf8ca490f02b2fc02f92575e720c8551e843c945f" dependencies = [ - "env_logger 0.7.1", + "env_logger", "log 0.4.8", "rand 0.7.3", "rand_core 0.5.1", @@ -3606,13 +3463,13 @@ dependencies = [ [[package]] name = "quickcheck_macros" -version = "0.8.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7dfc1c4a1e048f5cc7d36a4c4118dfcf31d217c79f4b9a61bad65d68185752c" +checksum = "608c156fd8e97febc07dc9c2e2c80bf74cfc6ef26893eae3daf8bc2bc94a4b7f" dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", - "syn 0.15.44", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -3626,22 +3483,13 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "quote" -version = "0.6.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" -dependencies = [ - "proc-macro2 0.4.30", -] - [[package]] name = "quote" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c1f4b0efa5fc5e8ceb705136bfee52cfdb6a4e3509f770b478cd6ed434232a7" dependencies = [ - "proc-macro2 1.0.12", + "proc-macro2", ] [[package]] @@ -3686,25 +3534,6 @@ dependencies = [ "winapi 0.3.8", ] -[[package]] -name = "rand" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" -dependencies = [ - "autocfg 0.1.7", - "libc", - "rand_chacha 0.1.1", - "rand_core 0.4.2", - "rand_hc 0.1.0", - "rand_isaac", - "rand_jitter", - "rand_os", - "rand_pcg", - "rand_xorshift 0.1.1", - "winapi 0.3.8", -] - [[package]] name = "rand" version = "0.7.3" @@ -3713,19 +3542,9 @@ checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ "getrandom", "libc", - "rand_chacha 0.2.2", + "rand_chacha", "rand_core 0.5.1", - "rand_hc 0.2.0", -] - -[[package]] -name = "rand_chacha" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" -dependencies = [ - "autocfg 0.1.7", - "rand_core 0.3.1", + "rand_hc", ] [[package]] @@ -3762,15 +3581,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "rand_hc" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" -dependencies = [ - "rand_core 0.3.1", -] - [[package]] name = "rand_hc" version = "0.2.0" @@ -3780,59 +3590,6 @@ dependencies = [ "rand_core 0.5.1", ] -[[package]] -name = "rand_isaac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" -dependencies = [ - "rand_core 0.3.1", -] - -[[package]] -name = "rand_jitter" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" -dependencies = [ - "libc", - "rand_core 0.4.2", - "winapi 0.3.8", -] - -[[package]] -name = "rand_os" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" -dependencies = [ - "cloudabi", - "fuchsia-cprng", - "libc", - "rand_core 0.4.2", - "rdrand", - "winapi 0.3.8", -] - -[[package]] -name = "rand_pcg" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" -dependencies = [ - "autocfg 0.1.7", - "rand_core 0.4.2", -] - -[[package]] -name = "rand_xorshift" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" -dependencies = [ - "rand_core 0.3.1", -] - [[package]] name = "rand_xorshift" version = "0.2.0" @@ -3926,15 +3683,15 @@ dependencies = [ "eth2_config", "eth2_ssz", "futures 0.3.4", - "hex 0.3.2", + "hex 0.4.2", "operation_pool", "proto_array_fork_choice", - "reqwest 0.10.4", + "reqwest", "rest_types", "serde", "serde_json", "types", - "url 1.7.2", + "url 2.1.1", ] [[package]] @@ -3946,40 +3703,6 @@ dependencies = [ "winapi 0.3.8", ] -[[package]] -name = "reqwest" -version = "0.9.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f88643aea3c1343c804950d7bf983bd2067f5ab59db6d613a08e05572f2714ab" -dependencies = [ - "base64 0.10.1", - "bytes 0.4.12", - "cookie", - "cookie_store", - "encoding_rs", - "flate2", - "futures 0.1.29", - "http 0.1.21", - "hyper 0.12.35", - "hyper-tls 0.3.2", - "log 0.4.8", - "mime 0.3.16", - "mime_guess", - "native-tls", - "serde", - "serde_json", - "serde_urlencoded 0.5.5", - "time", - "tokio 0.1.22", - "tokio-executor", - "tokio-io", - "tokio-threadpool", - "tokio-timer 0.2.13", - "url 1.7.2", - "uuid", - "winreg", -] - [[package]] name = "reqwest" version = "0.10.4" @@ -4005,10 +3728,10 @@ dependencies = [ "pin-project-lite", "serde", "serde_json", - "serde_urlencoded 0.6.1", + "serde_urlencoded", "time", "tokio 0.2.20", - "tokio-tls 0.3.0", + "tokio-tls 0.3.1", "url 2.1.1", "wasm-bindgen", "wasm-bindgen-futures", @@ -4026,16 +3749,16 @@ dependencies = [ "eth2_config", "eth2_ssz", "eth2_ssz_derive", - "futures 0.1.29", - "hex 0.3.2", - "http 0.1.21", - "hyper 0.12.35", + "futures 0.3.4", + "hex 0.4.2", + "http 0.2.1", + "hyper 0.13.5", "lazy_static", "lighthouse_metrics", "network", "node_test_rig", "operation_pool", - "parking_lot 0.9.0", + "parking_lot 0.10.2", "rayon", "remote_beacon_node", "rest_types", @@ -4048,7 +3771,7 @@ dependencies = [ "slot_clock", "state_processing", "store", - "tokio 0.1.22", + "tokio 0.2.20", "tree_hash", "types", "url 2.1.1", @@ -4224,12 +3947,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" -[[package]] -name = "scopeguard" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27" - [[package]] name = "scopeguard" version = "1.1.0" @@ -4323,16 +4040,16 @@ version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e549e3abf4fb8621bd1609f11dfc9f5e50320802273b12f3811a67e6716ea6c" dependencies = [ - "proc-macro2 1.0.12", - "quote 1.0.4", - "syn 1.0.18", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "serde_hex" version = "0.2.0" dependencies = [ - "hex 0.3.2", + "hex 0.4.2", "serde", ] @@ -4353,21 +4070,9 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd02c7587ec314570041b2754829f84d873ced14a96d1fd1823531e11db40573" dependencies = [ - "proc-macro2 1.0.12", - "quote 1.0.4", - "syn 1.0.18", -] - -[[package]] -name = "serde_urlencoded" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "642dd69105886af2efd227f75a520ec9b44a820d65bc133a9131f7d229fd165a" -dependencies = [ - "dtoa", - "itoa", - "serde", - "url 1.7.2", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -4465,12 +4170,12 @@ name = "simulator" version = "0.2.0" dependencies = [ "clap", - "env_logger 0.7.1", + "env_logger", "eth1_test_rig", - "futures 0.1.29", + "futures 0.3.4", "node_test_rig", - "parking_lot 0.9.0", - "tokio 0.1.22", + "parking_lot 0.10.2", + "tokio 0.2.20", "types", "validator_client", ] @@ -4538,25 +4243,13 @@ dependencies = [ "slog", ] -[[package]] -name = "slog-stdlog" -version = "3.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1c469573d1e3f36f9eee66cd132206caf47b50c94b1f6c6e7b4d8235e9ecf01" -dependencies = [ - "crossbeam 0.2.12", - "log 0.3.9", - "slog", - "slog-scope", -] - [[package]] name = "slog-stdlog" version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4d87903baf655da2d82bc3ac3f7ef43868c58bf712b3a661fda72009304c23" dependencies = [ - "crossbeam 0.7.3", + "crossbeam", "log 0.4.8", "slog", "slog-scope", @@ -4577,20 +4270,19 @@ dependencies = [ [[package]] name = "sloggers" -version = "0.3.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31bef221d42166d6708aa1e9b0182324b37a0a7517ff590ec201dbfe1cfa46ef" +checksum = "a17da19b392c9b9e830188839ca3cbb3affeff28606aa30440cb72ca72aa1684" dependencies = [ "chrono", "libflate", "regex", "serde", - "serde_derive", "slog", "slog-async", "slog-kvfilter", "slog-scope", - "slog-stdlog 3.0.5", + "slog-stdlog", "slog-term", "trackable", ] @@ -4601,7 +4293,7 @@ version = "0.2.0" dependencies = [ "lazy_static", "lighthouse_metrics", - "parking_lot 0.9.0", + "parking_lot 0.10.2", "types", ] @@ -4682,12 +4374,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" -[[package]] -name = "stable_deref_trait" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" - [[package]] name = "state_processing" version = "0.2.0" @@ -4696,13 +4382,13 @@ dependencies = [ "beacon_chain", "bls", "criterion", - "env_logger 0.7.1", + "env_logger", "eth2_hashing", "eth2_ssz", "eth2_ssz_types", "int_to_bytes", "integer-sqrt", - "itertools 0.8.2", + "itertools 0.9.0", "lazy_static", "log 0.4.8", "merkle_proof", @@ -4731,12 +4417,12 @@ dependencies = [ "db-key", "eth2_ssz", "eth2_ssz_derive", - "itertools 0.8.2", + "itertools 0.9.0", "lazy_static", "leveldb", "lighthouse_metrics", "lru", - "parking_lot 0.9.0", + "parking_lot 0.10.2", "rayon", "serde", "serde_derive", @@ -4791,30 +4477,19 @@ dependencies = [ "criterion", "eth2_hashing", "ethereum-types", - "hex 0.3.2", + "hex 0.4.2", "yaml-rust", ] [[package]] name = "syn" -version = "0.15.44" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" +checksum = "e8e5aa70697bb26ee62214ae3288465ecec0000f05182f039b477001f08f5ae7" dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", - "unicode-xid 0.1.0", -] - -[[package]] -name = "syn" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "410a7488c0a728c7ceb4ad59b9567eb4053d02e8cc7f5c0e0eeeb39518369213" -dependencies = [ - "proc-macro2 1.0.12", - "quote 1.0.4", - "unicode-xid 0.2.0", + "proc-macro2", + "quote", + "unicode-xid", ] [[package]] @@ -4823,10 +4498,10 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" dependencies = [ - "proc-macro2 1.0.12", - "quote 1.0.4", - "syn 1.0.18", - "unicode-xid 0.2.0", + "proc-macro2", + "quote", + "syn", + "unicode-xid", ] [[package]] @@ -4888,8 +4563,8 @@ dependencies = [ name = "test_random_derive" version = "0.2.0" dependencies = [ - "quote 0.6.13", - "syn 0.15.44", + "quote", + "syn", ] [[package]] @@ -4916,9 +4591,9 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f34e0c1caaa462fd840ec6b768946ea1e7842620d94fe29d5b847138f521269" dependencies = [ - "proc-macro2 1.0.12", - "quote 1.0.4", - "syn 1.0.18", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -5128,9 +4803,9 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0c3acc6aa564495a0f2e1d59fab677cd7f81a19994cfc7f3ad0e64301560389" dependencies = [ - "proc-macro2 1.0.12", - "quote 1.0.4", - "syn 1.0.18", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -5228,9 +4903,9 @@ dependencies = [ [[package]] name = "tokio-tls" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bde02a3a5291395f59b06ec6945a3077602fac2b07eeeaf0dee2122f3619828" +checksum = "9a70f4fcd7b3b24fb194f837560168208f669ca8cb70d0c4b862944452396343" dependencies = [ "native-tls", "tokio 0.2.20", @@ -5318,21 +4993,21 @@ checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" [[package]] name = "trackable" -version = "0.2.23" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11475c3c53b075360eac9794965822cb053996046545f91cf61d90e00b72efa5" +checksum = "30fb6e13d129dd92c501458f64d56c708e3685e3fd307e878ec5f934c5c5bdb0" dependencies = [ "trackable_derive", ] [[package]] name = "trackable_derive" -version = "0.1.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edcf0b9b2caa5f4804ef77aeee1b929629853d806117c48258f402b69737e65c" +checksum = "ebeb235c5847e2f82cfe0f07eb971d1e5f6804b18dac2ae16349cc604380f82f" dependencies = [ - "quote 1.0.4", - "syn 1.0.18", + "quote", + "syn", ] [[package]] @@ -5359,8 +5034,8 @@ dependencies = [ name = "tree_hash_derive" version = "0.2.0" dependencies = [ - "quote 0.6.13", - "syn 0.15.44", + "quote", + "syn", ] [[package]] @@ -5369,15 +5044,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" -[[package]] -name = "try_from" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "283d3b89e1368717881a9d51dad843cc435380d8109c9e47d38780a324698d8b" -dependencies = [ - "cfg-if", -] - [[package]] name = "twofish" version = "0.2.0" @@ -5413,19 +5079,19 @@ dependencies = [ "criterion", "derivative", "dirs", - "env_logger 0.7.1", + "env_logger", "eth2_hashing", "eth2_interop_keypairs", "eth2_ssz", "eth2_ssz_derive", "eth2_ssz_types", "ethereum-types", - "hex 0.3.2", + "hex 0.4.2", "int_to_bytes", "log 0.4.8", "merkle_proof", "rand 0.7.3", - "rand_xorshift 0.2.0", + "rand_xorshift", "rayon", "safe_arith", "serde", @@ -5501,12 +5167,6 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" -[[package]] -name = "unicode-xid" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" - [[package]] name = "unicode-xid" version = "0.2.0" @@ -5560,15 +5220,6 @@ dependencies = [ "percent-encoding 2.1.0", ] -[[package]] -name = "uuid" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a" -dependencies = [ - "rand 0.6.5", -] - [[package]] name = "validator_client" version = "0.2.0" @@ -5584,12 +5235,12 @@ dependencies = [ "eth2_interop_keypairs", "eth2_ssz", "eth2_ssz_derive", - "exit-future 0.1.4", - "futures 0.1.29", - "hex 0.3.2", + "exit-future", + "futures 0.3.4", + "hex 0.4.2", "libc", "logging", - "parking_lot 0.7.1", + "parking_lot 0.10.2", "rayon", "remote_beacon_node", "rest_types", @@ -5601,8 +5252,7 @@ dependencies = [ "slog-term", "slot_clock", "tempdir", - "tokio 0.1.22", - "tokio-timer 0.2.13", + "tokio 0.2.20", "tree_hash", "types", "web3", @@ -5616,9 +5266,9 @@ checksum = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168" [[package]] name = "vec_map" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "version" @@ -5704,9 +5354,9 @@ dependencies = [ "bumpalo", "lazy_static", "log 0.4.8", - "proc-macro2 1.0.12", - "quote 1.0.4", - "syn 1.0.18", + "proc-macro2", + "quote", + "syn", "wasm-bindgen-shared", ] @@ -5728,7 +5378,7 @@ version = "0.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cd85aa2c579e8892442954685f0d801f9129de24fa2136b2c6a539c76b65776" dependencies = [ - "quote 1.0.4", + "quote", "wasm-bindgen-macro-support", ] @@ -5738,9 +5388,9 @@ version = "0.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eb197bd3a47553334907ffd2f16507b4f4f01bbec3ac921a7719e0decdfe72a" dependencies = [ - "proc-macro2 1.0.12", - "quote 1.0.4", - "syn 1.0.18", + "proc-macro2", + "quote", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5771,8 +5421,8 @@ version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c358c8d2507c1bae25efa069e62ea907aa28700b25c8c33dafb0b15ba4603627" dependencies = [ - "proc-macro2 1.0.12", - "quote 1.0.4", + "proc-macro2", + "quote", ] [[package]] @@ -5807,7 +5457,7 @@ version = "0.10.0" source = "git+https://github.com/tomusdrw/rust-web3#b6c81f978ede4e5c250b2d6f93399f31f7ac2a48" dependencies = [ "arrayvec 0.5.1", - "base64 0.12.0", + "base64 0.12.1", "derive_more", "ethabi", "ethereum-types", @@ -6035,8 +5685,8 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de251eec69fc7c1bc3923403d18ececb929380e016afe103da75f396704f8ca2" dependencies = [ - "proc-macro2 1.0.12", - "quote 1.0.4", - "syn 1.0.18", + "proc-macro2", + "quote", + "syn", "synstructure", ] diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index 9588767faf..cd231552a9 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -5,26 +5,26 @@ authors = ["Paul Hauner ", "Luke Anderson for Error { + fn from(e: BeaconChainError) -> Self { + Error::BeaconChainError(e) + } +} + +/// Wraps a `SignedAggregateAndProof` that has been verified for propagation on the gossip network. +pub struct VerifiedAggregatedAttestation { + signed_aggregate: SignedAggregateAndProof, + indexed_attestation: IndexedAttestation, +} + +/// Wraps an `Attestation` that has been verified for propagation on the gossip network. +pub struct VerifiedUnaggregatedAttestation { + attestation: Attestation, + indexed_attestation: IndexedAttestation, +} + +/// Custom `Clone` implementation is to avoid the restrictive trait bounds applied by the usual derive +/// macro. +impl Clone for VerifiedUnaggregatedAttestation { + fn clone(&self) -> Self { + Self { + attestation: self.attestation.clone(), + indexed_attestation: self.indexed_attestation.clone(), + } + } +} + +/// Wraps an `indexed_attestation` that is valid for application to fork choice. The +/// `indexed_attestation` will have been generated via the `VerifiedAggregatedAttestation` or +/// `VerifiedUnaggregatedAttestation` wrappers. +pub struct ForkChoiceVerifiedAttestation<'a, T: BeaconChainTypes> { + indexed_attestation: &'a IndexedAttestation, +} + +/// A helper trait implemented on wrapper types that can be progressed to a state where they can be +/// verified for application to fork choice. +pub trait IntoForkChoiceVerifiedAttestation<'a, T: BeaconChainTypes> { + fn into_fork_choice_verified_attestation( + &'a self, + chain: &BeaconChain, + ) -> Result, Error>; +} + +impl<'a, T: BeaconChainTypes> IntoForkChoiceVerifiedAttestation<'a, T> + for VerifiedAggregatedAttestation +{ + /// Progresses the `VerifiedAggregatedAttestation` to a stage where it is valid for application + /// to the fork-choice rule (or not). + fn into_fork_choice_verified_attestation( + &'a self, + chain: &BeaconChain, + ) -> Result, Error> { + ForkChoiceVerifiedAttestation::from_signature_verified_components( + &self.indexed_attestation, + chain, + ) + } +} + +impl<'a, T: BeaconChainTypes> IntoForkChoiceVerifiedAttestation<'a, T> + for VerifiedUnaggregatedAttestation +{ + /// Progresses the `Attestation` to a stage where it is valid for application to the + /// fork-choice rule (or not). + fn into_fork_choice_verified_attestation( + &'a self, + chain: &BeaconChain, + ) -> Result, Error> { + ForkChoiceVerifiedAttestation::from_signature_verified_components( + &self.indexed_attestation, + chain, + ) + } +} + +impl<'a, T: BeaconChainTypes> IntoForkChoiceVerifiedAttestation<'a, T> + for ForkChoiceVerifiedAttestation<'a, T> +{ + /// Simply returns itself. + fn into_fork_choice_verified_attestation( + &'a self, + _: &BeaconChain, + ) -> Result, Error> { + Ok(Self { + indexed_attestation: self.indexed_attestation, + }) + } +} + +impl VerifiedAggregatedAttestation { + /// Returns `Ok(Self)` if the `signed_aggregate` is valid to be (re)published on the gossip + /// network. + pub fn verify( + signed_aggregate: SignedAggregateAndProof, + chain: &BeaconChain, + ) -> Result { + let attestation = &signed_aggregate.message.aggregate; + + // Ensure attestation is within the last ATTESTATION_PROPAGATION_SLOT_RANGE slots (within a + // MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance). + // + // We do not queue future attestations for later processing. + verify_propagation_slot_range(chain, attestation)?; + + // Ensure the aggregated attestation has not already been seen locally. + // + // TODO: this part of the code is not technically to spec, however I have raised a PR to + // change it: + // + // https://github.com/ethereum/eth2.0-specs/pull/1749 + let attestation_root = attestation.tree_hash_root(); + if chain + .observed_attestations + .is_known(attestation, attestation_root) + .map_err(|e| Error::BeaconChainError(e.into()))? + { + return Err(Error::AttestationAlreadyKnown(attestation_root)); + } + + let aggregator_index = signed_aggregate.message.aggregator_index; + + // Ensure there has been no other observed aggregate for the given `aggregator_index`. + // + // Note: do not observe yet, only observe once the attestation has been verfied. + match chain + .observed_aggregators + .validator_has_been_observed(attestation, aggregator_index as usize) + { + Ok(true) => Err(Error::AggregatorAlreadyKnown(aggregator_index)), + Ok(false) => Ok(()), + Err(ObservedAttestersError::ValidatorIndexTooHigh(i)) => { + Err(Error::ValidatorIndexTooHigh(i)) + } + Err(e) => Err(BeaconChainError::from(e).into()), + }?; + + // Ensure the block being voted for (attestation.data.beacon_block_root) passes validation. + // + // This indirectly checks to see if the `attestation.data.beacon_block_root` is in our fork + // choice. Any known, non-finalized, processed block should be in fork choice, so this + // check immediately filters out attestations that attest to a block that has not been + // processed. + // + // Attestations must be for a known block. If the block is unknown, we simply drop the + // attestation and do not delay consideration for later. + verify_head_block_is_known(chain, &attestation)?; + + let indexed_attestation = map_attestation_committee(chain, attestation, |committee| { + // Note: this clones the signature which is known to be a relatively slow operation. + // + // Future optimizations should remove this clone. + let selection_proof = + SelectionProof::from(signed_aggregate.message.selection_proof.clone()); + + if !selection_proof + .is_aggregator(committee.committee.len(), &chain.spec) + .map_err(|e| Error::BeaconChainError(e.into()))? + { + return Err(Error::InvalidSelectionProof { aggregator_index }); + } + + /* + * I have raised a PR that will likely get merged in v0.12.0: + * + * https://github.com/ethereum/eth2.0-specs/pull/1732 + * + * If this PR gets merged, uncomment this code and remove the code below. + * + if !committee + .committee + .iter() + .any(|validator_index| *validator_index as u64 == aggregator_index) + { + return Err(Error::AggregatorNotInCommittee { aggregator_index }); + } + */ + + get_indexed_attestation(committee.committee, &attestation) + .map_err(|e| BeaconChainError::from(e).into()) + })?; + + // Ensure the aggregator is in the attestation. + // + // I've raised an issue with this here: + // + // https://github.com/ethereum/eth2.0-specs/pull/1732 + // + // I suspect PR my will get merged in v0.12 and we'll need to delete this code and + // uncomment the code above. + if !indexed_attestation + .attesting_indices + .iter() + .any(|validator_index| *validator_index as u64 == aggregator_index) + { + return Err(Error::AggregatorNotInCommittee { aggregator_index }); + } + + if !verify_signed_aggregate_signatures(chain, &signed_aggregate, &indexed_attestation)? { + return Err(Error::InvalidSignature); + } + + // Observe the valid attestation so we do not re-process it. + // + // It's important to double check that the attestation is not already known, otherwise two + // attestations processed at the same time could be published. + if let ObserveOutcome::AlreadyKnown = chain + .observed_attestations + .observe_attestation(attestation, Some(attestation_root)) + .map_err(|e| Error::BeaconChainError(e.into()))? + { + return Err(Error::AttestationAlreadyKnown(attestation_root)); + } + + // Observe the aggregator so we don't process another aggregate from them. + // + // It's important to double check that the attestation is not already known, otherwise two + // attestations processed at the same time could be published. + if chain + .observed_aggregators + .observe_validator(&attestation, aggregator_index as usize) + .map_err(|e| BeaconChainError::from(e))? + { + return Err(Error::PriorAttestationKnown { + validator_index: aggregator_index, + epoch: attestation.data.target.epoch, + }); + } + + Ok(VerifiedAggregatedAttestation { + signed_aggregate, + indexed_attestation, + }) + } + + /// A helper function to add this aggregate to `beacon_chain.op_pool`. + pub fn add_to_pool(self, chain: &BeaconChain) -> Result { + chain.add_to_block_inclusion_pool(self) + } + + /// A helper function to add this aggregate to `beacon_chain.fork_choice`. + pub fn add_to_fork_choice( + &self, + chain: &BeaconChain, + ) -> Result, Error> { + chain.apply_attestation_to_fork_choice(self) + } + + /// Returns the underlying `attestation` for the `signed_aggregate`. + pub fn attestation(&self) -> &Attestation { + &self.signed_aggregate.message.aggregate + } +} + +impl VerifiedUnaggregatedAttestation { + /// Returns `Ok(Self)` if the `attestation` is valid to be (re)published on the gossip + /// network. + pub fn verify( + attestation: Attestation, + chain: &BeaconChain, + ) -> Result { + // Ensure attestation is within the last ATTESTATION_PROPAGATION_SLOT_RANGE slots (within a + // MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance). + // + // We do not queue future attestations for later processing. + verify_propagation_slot_range(chain, &attestation)?; + + // Check to ensure that the attestation is "unaggregated". I.e., it has exactly one + // aggregation bit set. + let num_aggreagtion_bits = attestation.aggregation_bits.num_set_bits(); + if num_aggreagtion_bits != 1 { + return Err(Error::NotExactlyOneAggregationBitSet(num_aggreagtion_bits)); + } + + // Attestations must be for a known block. If the block is unknown, we simply drop the + // attestation and do not delay consideration for later. + verify_head_block_is_known(chain, &attestation)?; + + let indexed_attestation = obtain_indexed_attestation(chain, &attestation)?; + + let validator_index = *indexed_attestation + .attesting_indices + .first() + .ok_or_else(|| Error::NotExactlyOneAggregationBitSet(0))?; + + /* + * The attestation is the first valid attestation received for the participating validator + * for the slot, attestation.data.slot. + */ + if chain + .observed_attesters + .validator_has_been_observed(&attestation, validator_index as usize) + .map_err(|e| BeaconChainError::from(e))? + { + return Err(Error::PriorAttestationKnown { + validator_index, + epoch: attestation.data.target.epoch, + }); + } + + // The aggregate signature of the attestation is valid. + verify_attestation_signature(chain, &indexed_attestation)?; + + // Now that the attestation has been fully verified, store that we have received a valid + // attestation from this validator. + // + // It's important to double check that the attestation still hasn't been observed, since + // there can be a race-condition if we receive two attestations at the same time and + // process them in different threads. + if chain + .observed_attesters + .observe_validator(&attestation, validator_index as usize) + .map_err(|e| BeaconChainError::from(e))? + { + return Err(Error::PriorAttestationKnown { + validator_index, + epoch: attestation.data.target.epoch, + }); + } + + Ok(Self { + attestation, + indexed_attestation, + }) + } + + /// A helper function to add this attestation to `beacon_chain.naive_aggregation_pool`. + pub fn add_to_pool(self, chain: &BeaconChain) -> Result { + chain.add_to_naive_aggregation_pool(self) + } + + /// Returns the wrapped `attestation`. + pub fn attestation(&self) -> &Attestation { + &self.attestation + } + + /// Returns a mutable reference to the underlying attestation. + /// + /// Only use during testing since modifying the `IndexedAttestation` can cause the attestation + /// to no-longer be valid. + pub fn __indexed_attestation_mut(&mut self) -> &mut IndexedAttestation { + &mut self.indexed_attestation + } +} + +impl<'a, T: BeaconChainTypes> ForkChoiceVerifiedAttestation<'a, T> { + /// Returns `Ok(Self)` if the `attestation` is valid to be applied to the beacon chain fork + /// choice. + /// + /// The supplied `indexed_attestation` MUST have a valid signature, this function WILL NOT + /// CHECK THE SIGNATURE. Use the `VerifiedAggregatedAttestation` or + /// `VerifiedUnaggregatedAttestation` structs to do signature verification. + fn from_signature_verified_components( + indexed_attestation: &'a IndexedAttestation, + chain: &BeaconChain, + ) -> Result { + // There is no point in processing an attestation with an empty bitfield. Reject + // it immediately. + // + // This is not in the specification, however it should be transparent to other nodes. We + // return early here to avoid wasting precious resources verifying the rest of it. + if indexed_attestation.attesting_indices.len() == 0 { + return Err(Error::EmptyAggregationBitfield); + } + + let slot_now = chain.slot()?; + let epoch_now = slot_now.epoch(T::EthSpec::slots_per_epoch()); + let target = indexed_attestation.data.target.clone(); + + // Attestation must be from the current or previous epoch. + if target.epoch > epoch_now { + return Err(Error::FutureEpoch { + attestation_epoch: target.epoch, + current_epoch: epoch_now, + }); + } else if target.epoch + 1 < epoch_now { + return Err(Error::PastEpoch { + attestation_epoch: target.epoch, + current_epoch: epoch_now, + }); + } + + if target.epoch + != indexed_attestation + .data + .slot + .epoch(T::EthSpec::slots_per_epoch()) + { + return Err(Error::BadTargetEpoch); + } + + // Attestation target must be for a known block. + if !chain.fork_choice.contains_block(&target.root) { + return Err(Error::UnknownTargetRoot(target.root)); + } + + // TODO: we're not testing an assert from the spec: + // + // `assert get_current_slot(store) >= compute_start_slot_at_epoch(target.epoch)` + // + // I think this check is redundant and I've raised an issue here: + // + // https://github.com/ethereum/eth2.0-specs/pull/1755 + // + // To resolve this todo, observe the outcome of the above PR. + + // Load the slot and state root for `attestation.data.beacon_block_root`. + // + // This indirectly checks to see if the `attestation.data.beacon_block_root` is in our fork + // choice. Any known, non-finalized block should be in fork choice, so this check + // immediately filters out attestations that attest to a block that has not been processed. + // + // Attestations must be for a known block. If the block is unknown, we simply drop the + // attestation and do not delay consideration for later. + let (block_slot, _state_root) = chain + .fork_choice + .block_slot_and_state_root(&indexed_attestation.data.beacon_block_root) + .ok_or_else(|| Error::UnknownHeadBlock { + beacon_block_root: indexed_attestation.data.beacon_block_root, + })?; + + // TODO: currently we do not check the FFG source/target. This is what the spec dictates + // but it seems wrong. + // + // I have opened an issue on the specs repo for this: + // + // https://github.com/ethereum/eth2.0-specs/issues/1636 + // + // We should revisit this code once that issue has been resolved. + + // Attestations must not be for blocks in the future. If this is the case, the attestation + // should not be considered. + if block_slot > indexed_attestation.data.slot { + return Err(Error::AttestsToFutureBlock { + block: block_slot, + attestation: indexed_attestation.data.slot, + }); + } + + // Note: we're not checking the "attestations can only affect the fork choice of subsequent + // slots" part of the spec, we do this upstream. + + Ok(Self { + indexed_attestation, + }) + } + + /// Returns the wrapped `IndexedAttestation`. + pub fn indexed_attestation(&self) -> &IndexedAttestation { + &self.indexed_attestation + } +} + +/// Returns `Ok(())` if the `attestation.data.beacon_block_root` is known to this chain. +/// +/// The block root may not be known for two reasons: +/// +/// 1. The block has never been verified by our application. +/// 2. The block is prior to the latest finalized block. +/// +/// Case (1) is the exact thing we're trying to detect. However case (2) is a little different, but +/// it's still fine to reject here because there's no need for us to handle attestations that are +/// already finalized. +fn verify_head_block_is_known( + chain: &BeaconChain, + attestation: &Attestation, +) -> Result<(), Error> { + if chain + .fork_choice + .contains_block(&attestation.data.beacon_block_root) + { + Ok(()) + } else { + Err(Error::UnknownHeadBlock { + beacon_block_root: attestation.data.beacon_block_root, + }) + } +} + +/// Verify that the `attestation` is within the acceptable gossip propagation range, with reference +/// to the current slot of the `chain`. +/// +/// Accounts for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`. +pub fn verify_propagation_slot_range( + chain: &BeaconChain, + attestation: &Attestation, +) -> Result<(), Error> { + let attestation_slot = attestation.data.slot; + + let latest_permissible_slot = chain + .slot_clock + .now_with_future_tolerance(MAXIMUM_GOSSIP_CLOCK_DISPARITY) + .ok_or_else(|| BeaconChainError::UnableToReadSlot)?; + if attestation_slot > latest_permissible_slot { + return Err(Error::FutureSlot { + attestation_slot, + latest_permissible_slot, + }); + } + + // Taking advantage of saturating subtraction on `Slot`. + let earliest_permissible_slot = chain + .slot_clock + .now_with_past_tolerance(MAXIMUM_GOSSIP_CLOCK_DISPARITY) + .ok_or_else(|| BeaconChainError::UnableToReadSlot)? + - T::EthSpec::slots_per_epoch(); + if attestation_slot < earliest_permissible_slot { + return Err(Error::PastSlot { + attestation_slot, + earliest_permissible_slot, + }); + } + + Ok(()) +} + +/// Verifies that the signature of the `indexed_attestation` is valid. +pub fn verify_attestation_signature( + chain: &BeaconChain, + indexed_attestation: &IndexedAttestation, +) -> Result<(), Error> { + let signature_setup_timer = + metrics::start_timer(&metrics::ATTESTATION_PROCESSING_SIGNATURE_SETUP_TIMES); + + let pubkey_cache = chain + .validator_pubkey_cache + .try_read_for(VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT) + .ok_or_else(|| BeaconChainError::ValidatorPubkeyCacheLockTimeout)?; + + let fork = chain + .canonical_head + .try_read_for(HEAD_LOCK_TIMEOUT) + .ok_or_else(|| BeaconChainError::CanonicalHeadLockTimeout) + .map(|head| head.beacon_state.fork.clone())?; + + let signature_set = indexed_attestation_signature_set_from_pubkeys( + |validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed), + &indexed_attestation.signature, + &indexed_attestation, + &fork, + chain.genesis_validators_root, + &chain.spec, + ) + .map_err(BeaconChainError::SignatureSetError)?; + + metrics::stop_timer(signature_setup_timer); + + let _signature_verification_timer = + metrics::start_timer(&metrics::ATTESTATION_PROCESSING_SIGNATURE_TIMES); + + if signature_set.is_valid() { + Ok(()) + } else { + Err(Error::InvalidSignature) + } +} + +/// Verifies all the signatures in a `SignedAggregateAndProof` using BLS batch verification. This +/// includes three signatures: +/// +/// - `signed_aggregate.signature` +/// - `signed_aggregate.signature.message.selection proof` +/// - `signed_aggregate.signature.message.aggregate.signature` +/// +/// # Returns +/// +/// - `Ok(true)`: if all signatures are valid. +/// - `Ok(false)`: if one or more signatures are invalid. +/// - `Err(e)`: if there was an error preventing signature verification. +pub fn verify_signed_aggregate_signatures( + chain: &BeaconChain, + signed_aggregate: &SignedAggregateAndProof, + indexed_attestation: &IndexedAttestation, +) -> Result { + let pubkey_cache = chain + .validator_pubkey_cache + .try_read_for(VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT) + .ok_or_else(|| BeaconChainError::ValidatorPubkeyCacheLockTimeout)?; + + let aggregator_index = signed_aggregate.message.aggregator_index; + if aggregator_index >= pubkey_cache.len() as u64 { + return Err(Error::AggregatorPubkeyUnknown(aggregator_index)); + } + + let fork = chain + .canonical_head + .try_read_for(HEAD_LOCK_TIMEOUT) + .ok_or_else(|| BeaconChainError::CanonicalHeadLockTimeout) + .map(|head| head.beacon_state.fork.clone())?; + + let signature_sets = vec![ + signed_aggregate_selection_proof_signature_set( + |validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed), + &signed_aggregate, + &fork, + chain.genesis_validators_root, + &chain.spec, + ) + .map_err(BeaconChainError::SignatureSetError)?, + signed_aggregate_signature_set( + |validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed), + &signed_aggregate, + &fork, + chain.genesis_validators_root, + &chain.spec, + ) + .map_err(BeaconChainError::SignatureSetError)?, + indexed_attestation_signature_set_from_pubkeys( + |validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed), + &indexed_attestation.signature, + &indexed_attestation, + &fork, + chain.genesis_validators_root, + &chain.spec, + ) + .map_err(BeaconChainError::SignatureSetError)?, + ]; + + Ok(verify_signature_sets(signature_sets)) +} + +/// Returns the `indexed_attestation` for the `attestation` using the public keys cached in the +/// `chain`. +pub fn obtain_indexed_attestation( + chain: &BeaconChain, + attestation: &Attestation, +) -> Result, Error> { + map_attestation_committee(chain, attestation, |committee| { + get_indexed_attestation(committee.committee, &attestation) + .map_err(|e| BeaconChainError::from(e).into()) + }) +} + +/// Runs the `map_fn` with the committee for the given `attestation`. +/// +/// This function exists in this odd "map" pattern because efficiently obtaining the committee for +/// an attestation can be complex. It might involve reading straight from the +/// `beacon_chain.shuffling_cache` or it might involve reading it from a state from the DB. Due to +/// the complexities of `RwLock`s on the shuffling cache, a simple `Cow` isn't suitable here. +/// +/// If the committee for `attestation` isn't found in the `shuffling_cache`, we will read a state +/// from disk and then update the `shuffling_cache`. +pub fn map_attestation_committee<'a, T, F, R>( + chain: &'a BeaconChain, + attestation: &Attestation, + map_fn: F, +) -> Result +where + T: BeaconChainTypes, + F: Fn(BeaconCommittee) -> Result, +{ + let attestation_epoch = attestation.data.slot.epoch(T::EthSpec::slots_per_epoch()); + let target = &attestation.data.target; + + // Attestation target must be for a known block. + // + // We use fork choice to find the target root, which means that we reject any attestation + // that has a `target.root` earlier than our latest finalized root. There's no point in + // processing an attestation that does not include our latest finalized block in its chain. + // + // We do not delay consideration for later, we simply drop the attestation. + let (target_block_slot, target_block_state_root) = chain + .fork_choice + .block_slot_and_state_root(&target.root) + .ok_or_else(|| Error::UnknownTargetRoot(target.root))?; + + // Obtain the shuffling cache, timing how long we wait. + let cache_wait_timer = + metrics::start_timer(&metrics::ATTESTATION_PROCESSING_SHUFFLING_CACHE_WAIT_TIMES); + + let mut shuffling_cache = chain + .shuffling_cache + .try_write_for(ATTESTATION_CACHE_LOCK_TIMEOUT) + .ok_or_else(|| BeaconChainError::AttestationCacheLockTimeout)?; + + metrics::stop_timer(cache_wait_timer); + + if let Some(committee_cache) = shuffling_cache.get(attestation_epoch, target.root) { + committee_cache + .get_beacon_committee(attestation.data.slot, attestation.data.index) + .map(map_fn) + .unwrap_or_else(|| { + Err(Error::NoCommitteeForSlotAndIndex { + slot: attestation.data.slot, + index: attestation.data.index, + }) + }) + } else { + // Drop the shuffling cache to avoid holding the lock for any longer than + // required. + drop(shuffling_cache); + + debug!( + chain.log, + "Attestation processing cache miss"; + "attn_epoch" => attestation_epoch.as_u64(), + "target_block_epoch" => target_block_slot.epoch(T::EthSpec::slots_per_epoch()).as_u64(), + ); + + let state_read_timer = + metrics::start_timer(&metrics::ATTESTATION_PROCESSING_STATE_READ_TIMES); + + let mut state = chain + .get_state(&target_block_state_root, Some(target_block_slot))? + .ok_or_else(|| BeaconChainError::MissingBeaconState(target_block_state_root))?; + + metrics::stop_timer(state_read_timer); + let state_skip_timer = + metrics::start_timer(&metrics::ATTESTATION_PROCESSING_STATE_SKIP_TIMES); + + while state.current_epoch() + 1 < attestation_epoch { + // Here we tell `per_slot_processing` to skip hashing the state and just + // use the zero hash instead. + // + // The state roots are not useful for the shuffling, so there's no need to + // compute them. + per_slot_processing(&mut state, Some(Hash256::zero()), &chain.spec) + .map_err(|e| BeaconChainError::from(e))? + } + + metrics::stop_timer(state_skip_timer); + let committee_building_timer = + metrics::start_timer(&metrics::ATTESTATION_PROCESSING_COMMITTEE_BUILDING_TIMES); + + let relative_epoch = RelativeEpoch::from_epoch(state.current_epoch(), attestation_epoch) + .map_err(BeaconChainError::IncorrectStateForAttestation)?; + + state + .build_committee_cache(relative_epoch, &chain.spec) + .map_err(|e| BeaconChainError::from(e))?; + + let committee_cache = state + .committee_cache(relative_epoch) + .map_err(|e| BeaconChainError::from(e))?; + + chain + .shuffling_cache + .try_write_for(ATTESTATION_CACHE_LOCK_TIMEOUT) + .ok_or_else(|| BeaconChainError::AttestationCacheLockTimeout)? + .insert(attestation_epoch, target.root, committee_cache); + + metrics::stop_timer(committee_building_timer); + + committee_cache + .get_beacon_committee(attestation.data.slot, attestation.data.index) + .map(map_fn) + .unwrap_or_else(|| { + Err(Error::NoCommitteeForSlotAndIndex { + slot: attestation.data.slot, + index: attestation.data.index, + }) + }) + } +} diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 8b4db6f4d7..9991b75c60 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1,3 +1,7 @@ +use crate::attestation_verification::{ + Error as AttestationError, ForkChoiceVerifiedAttestation, IntoForkChoiceVerifiedAttestation, + VerifiedAggregatedAttestation, VerifiedUnaggregatedAttestation, +}; use crate::block_verification::{ check_block_relevancy, get_block_root, signature_verify_chain_segment, BlockError, FullyVerifiedBlock, GossipVerifiedBlock, IntoFullyVerifiedBlock, @@ -10,6 +14,9 @@ use crate::head_tracker::HeadTracker; use crate::metrics; use crate::migrate::Migrate; use crate::naive_aggregation_pool::{Error as NaiveAggregationError, NaiveAggregationPool}; +use crate::observed_attestations::{Error as AttestationObservationError, ObservedAttestations}; +use crate::observed_attesters::{ObservedAggregators, ObservedAttesters}; +use crate::observed_block_producers::ObservedBlockProducers; use crate::persisted_beacon_chain::PersistedBeaconChain; use crate::shuffling_cache::ShufflingCache; use crate::snapshot_cache::SnapshotCache; @@ -23,10 +30,7 @@ use state_processing::per_block_processing::errors::{ AttestationValidationError, AttesterSlashingValidationError, ExitValidationError, ProposerSlashingValidationError, }; -use state_processing::{ - common::get_indexed_attestation, per_block_processing, per_slot_processing, - signature_sets::indexed_attestation_signature_set_from_pubkeys, BlockSignatureStrategy, -}; +use state_processing::{per_block_processing, per_slot_processing, BlockSignatureStrategy}; use std::borrow::Cow; use std::cmp::Ordering; use std::collections::HashMap; @@ -49,14 +53,14 @@ pub const GRAFFITI: &str = "sigp/lighthouse-0.2.0-prerelease"; /// The time-out before failure during an operation to take a read/write RwLock on the canonical /// head. -const HEAD_LOCK_TIMEOUT: Duration = Duration::from_secs(1); +pub const HEAD_LOCK_TIMEOUT: Duration = Duration::from_secs(1); /// The time-out before failure during an operation to take a read/write RwLock on the block /// processing cache. pub const BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT: Duration = Duration::from_secs(1); /// The time-out before failure during an operation to take a read/write RwLock on the /// attestation cache. -const ATTESTATION_CACHE_LOCK_TIMEOUT: Duration = Duration::from_secs(1); +pub const ATTESTATION_CACHE_LOCK_TIMEOUT: Duration = Duration::from_secs(1); /// The time-out before failure during an operation to take a read/write RwLock on the /// validator pubkey cache. @@ -67,23 +71,6 @@ pub const OP_POOL_DB_KEY: [u8; 32] = [0; 32]; pub const ETH1_CACHE_DB_KEY: [u8; 32] = [0; 32]; pub const FORK_CHOICE_DB_KEY: [u8; 32] = [0; 32]; -#[derive(Debug, PartialEq)] -pub enum AttestationType { - /// An attestation with a single-signature that has been published in accordance with the naive - /// aggregation strategy. - /// - /// These attestations may have come from a `committee_index{subnet_id}_beacon_attestation` - /// gossip subnet or they have have come directly from a validator attached to our API. - /// - /// If `should_store == true`, the attestation will be added to the `NaiveAggregationPool`. - Unaggregated { should_store: bool }, - /// An attestation with one more more signatures that has passed through the aggregation phase - /// of the naive aggregation scheme. - /// - /// These attestations must have come from the `beacon_aggregate_and_proof` gossip subnet. - Aggregated, -} - /// The result of a chain segment processing. #[derive(Debug)] pub enum ChainSegmentResult { @@ -190,6 +177,15 @@ pub struct BeaconChain { /// This pool accepts `Attestation` objects that only have one aggregation bit set and provides /// a method to get an aggregated `Attestation` for some `AttestationData`. pub naive_aggregation_pool: NaiveAggregationPool, + /// Contains a store of attestations which have been observed by the beacon chain. + pub observed_attestations: ObservedAttestations, + /// Maintains a record of which validators have been seen to attest in recent epochs. + pub observed_attesters: ObservedAttesters, + /// Maintains a record of which validators have been seen to create `SignedAggregateAndProofs` + /// in recent epochs. + pub observed_aggregators: ObservedAggregators, + /// Maintains a record of which validators have proposed blocks for each slot. + pub observed_block_producers: ObservedBlockProducers, /// Provides information from the Ethereum 1 (PoW) chain. pub eth1_chain: Option>, /// Stores a "snapshot" of the chain at the time the head-of-the-chain block was received. @@ -742,10 +738,13 @@ impl BeaconChain { self.naive_aggregation_pool.get(data).map_err(Into::into) } - /// Produce a raw unsigned `Attestation` that is valid for the given `slot` and `index`. + /// Produce an unaggregated `Attestation` that is valid for the given `slot` and `index`. + /// + /// The produced `Attestation` will not be valid until it has been signed by exactly one + /// validator that is in the committee for `slot` and `index` in the canonical chain. /// /// Always attests to the canonical chain. - pub fn produce_attestation( + pub fn produce_unaggregated_attestation( &self, slot: Slot, index: CommitteeIndex, @@ -758,7 +757,7 @@ impl BeaconChain { .ok_or_else(|| Error::CanonicalHeadLockTimeout)?; if slot >= head.beacon_block.slot() { - self.produce_attestation_for_block( + self.produce_unaggregated_attestation_for_block( slot, index, head.beacon_block_root, @@ -790,15 +789,24 @@ impl BeaconChain { state.build_committee_cache(RelativeEpoch::Current, &self.spec)?; - self.produce_attestation_for_block(slot, index, beacon_block_root, Cow::Owned(state)) + self.produce_unaggregated_attestation_for_block( + slot, + index, + beacon_block_root, + Cow::Owned(state), + ) } } - /// Produce an `AttestationData` that attests to the chain denoted by `block_root` and `state`. + /// Produces an "unaggregated" attestation for the given `slot` and `index` that attests to + /// `beacon_block_root`. The provided `state` should match the `block.state_root` for the + /// `block` identified by `beacon_block_root`. /// - /// Permits attesting to any arbitrary chain. Generally, the `produce_attestation_data` - /// function should be used as it attests to the canonical chain. - pub fn produce_attestation_for_block( + /// The attestation doesn't _really_ have anything about it that makes it unaggregated per say, + /// however this function is only required in the context of forming an unaggregated + /// attestation. It would be an (undetectable) violation of the protocol to create a + /// `SignedAggregateAndProof` based upon the output of this function. + pub fn produce_unaggregated_attestation_for_block( &self, slot: Slot, index: CommitteeIndex, @@ -846,393 +854,125 @@ impl BeaconChain { }) } - /// Accept a new, potentially invalid attestation from the network. + /// Accepts some `Attestation` from the network and attempts to verify it, returning `Ok(_)` if + /// it is valid to be (re)broadcast on the gossip network. /// - /// If valid, the attestation is added to `self.op_pool` and `self.fork_choice`. - /// - /// Returns an `Ok(AttestationProcessingOutcome)` if the chain was able to make a determination - /// about the `attestation` (whether it was invalid or not). Returns an `Err` if there was an - /// error during this process and no determination was able to be made. - /// - /// ## Notes - /// - /// - Whilst the `attestation` is added to fork choice, the head is not updated. That must be - /// done separately. - /// - /// The `store_raw` parameter determines if this attestation is to be stored in the operation - /// pool. `None` indicates the attestation is not stored in the operation pool (we don't have a - /// validator required to aggregate these attestations). `Some(true)` indicates we are storing a - /// raw un-aggregated attestation from a subnet into the `op_pool` which is short-lived and `Some(false)` - /// indicates that we are storing an aggregate attestation in the `op_pool`. - pub fn process_attestation( + /// The attestation must be "unaggregated", that is it must have exactly one + /// aggregation bit set. + pub fn verify_unaggregated_attestation_for_gossip( &self, attestation: Attestation, - attestation_type: AttestationType, - ) -> Result { - metrics::inc_counter(&metrics::ATTESTATION_PROCESSING_REQUESTS); - let timer = metrics::start_timer(&metrics::ATTESTATION_PROCESSING_TIMES); - - let outcome = self.process_attestation_internal(attestation.clone(), attestation_type); - - match &outcome { - Ok(outcome) => match outcome { - AttestationProcessingOutcome::Processed => { - metrics::inc_counter(&metrics::ATTESTATION_PROCESSING_SUCCESSES); - trace!( - self.log, - "Beacon attestation imported"; - "target_epoch" => attestation.data.target.epoch, - "index" => attestation.data.index, - ); - let _ = self - .event_handler - .register(EventKind::BeaconAttestationImported { - attestation: Box::new(attestation), - }); - } - other => { - trace!( - self.log, - "Beacon attestation rejected"; - "reason" => format!("{:?}", other), - ); - let _ = self - .event_handler - .register(EventKind::BeaconAttestationRejected { - reason: format!("Invalid attestation: {:?}", other), - attestation: Box::new(attestation), - }); - } - }, - Err(e) => { - error!( - self.log, - "Beacon attestation processing error"; - "error" => format!("{:?}", e), - ); - let _ = self - .event_handler - .register(EventKind::BeaconAttestationRejected { - reason: format!("Internal error: {:?}", e), - attestation: Box::new(attestation), - }); - } - } - - metrics::stop_timer(timer); - outcome + ) -> Result, AttestationError> { + VerifiedUnaggregatedAttestation::verify(attestation, self) } - pub fn process_attestation_internal( + /// Accepts some `SignedAggregateAndProof` from the network and attempts to verify it, + /// returning `Ok(_)` if it is valid to be (re)broadcast on the gossip network. + pub fn verify_aggregated_attestation_for_gossip( &self, - attestation: Attestation, - attestation_type: AttestationType, - ) -> Result { - let initial_validation_timer = - metrics::start_timer(&metrics::ATTESTATION_PROCESSING_INITIAL_VALIDATION_TIMES); + signed_aggregate: SignedAggregateAndProof, + ) -> Result, AttestationError> { + VerifiedAggregatedAttestation::verify(signed_aggregate, self) + } - // There is no point in processing an attestation with an empty bitfield. Reject - // it immediately. - if attestation.aggregation_bits.num_set_bits() == 0 { - return Ok(AttestationProcessingOutcome::EmptyAggregationBitfield); - } + /// Accepts some attestation-type object and attempts to verify it in the context of fork + /// choice. If it is valid it is applied to `self.fork_choice`. + /// + /// Common items that implement `IntoForkChoiceVerifiedAttestation`: + /// + /// - `VerifiedUnaggregatedAttestation` + /// - `VerifiedAggregatedAttestation` + /// - `ForkChoiceVerifiedAttestation` + pub fn apply_attestation_to_fork_choice<'a>( + &self, + unverified_attestation: &'a impl IntoForkChoiceVerifiedAttestation<'a, T>, + ) -> Result, AttestationError> { + let verified = unverified_attestation.into_fork_choice_verified_attestation(self)?; + let indexed_attestation = verified.indexed_attestation(); + self.fork_choice + .process_indexed_attestation(indexed_attestation) + .map_err(|e| Error::from(e))?; + Ok(verified) + } - let attestation_epoch = attestation.data.slot.epoch(T::EthSpec::slots_per_epoch()); - let epoch_now = self.epoch()?; - let target = attestation.data.target.clone(); + /// Accepts an `VerifiedUnaggregatedAttestation` and attempts to apply it to the "naive + /// aggregation pool". + /// + /// The naive aggregation pool is used by local validators to produce + /// `SignedAggregateAndProof`. + /// + /// If the attestation is too old (low slot) to be included in the pool it is simply dropped + /// and no error is returned. + pub fn add_to_naive_aggregation_pool( + &self, + unaggregated_attestation: VerifiedUnaggregatedAttestation, + ) -> Result, AttestationError> { + let attestation = unaggregated_attestation.attestation(); - // Attestation must be from the current or previous epoch. - if attestation_epoch > epoch_now { - return Ok(AttestationProcessingOutcome::FutureEpoch { - attestation_epoch, - current_epoch: epoch_now, - }); - } else if attestation_epoch + 1 < epoch_now { - return Ok(AttestationProcessingOutcome::PastEpoch { - attestation_epoch, - current_epoch: epoch_now, - }); - } - - if target.epoch != attestation.data.slot.epoch(T::EthSpec::slots_per_epoch()) { - return Ok(AttestationProcessingOutcome::BadTargetEpoch); - } - - // Attestation target must be for a known block. - // - // We use fork choice to find the target root, which means that we reject any attestation - // that has a `target.root` earlier than our latest finalized root. There's no point in - // processing an attestation that does not include our latest finalized block in its chain. - // - // We do not delay consideration for later, we simply drop the attestation. - let (target_block_slot, target_block_state_root) = if let Some((slot, state_root)) = - self.fork_choice.block_slot_and_state_root(&target.root) - { - (slot, state_root) - } else { - return Ok(AttestationProcessingOutcome::UnknownTargetRoot(target.root)); - }; - - // Load the slot and state root for `attestation.data.beacon_block_root`. - // - // This indirectly checks to see if the `attestation.data.beacon_block_root` is in our fork - // choice. Any known, non-finalized block should be in fork choice, so this check - // immediately filters out attestations that attest to a block that has not been processed. - // - // Attestations must be for a known block. If the block is unknown, we simply drop the - // attestation and do not delay consideration for later. - let block_slot = if let Some((slot, _state_root)) = self - .fork_choice - .block_slot_and_state_root(&attestation.data.beacon_block_root) - { - slot - } else { - return Ok(AttestationProcessingOutcome::UnknownHeadBlock { - beacon_block_root: attestation.data.beacon_block_root, - }); - }; - - // TODO: currently we do not check the FFG source/target. This is what the spec dictates - // but it seems wrong. - // - // I have opened an issue on the specs repo for this: - // - // https://github.com/ethereum/eth2.0-specs/issues/1636 - // - // We should revisit this code once that issue has been resolved. - - // Attestations must not be for blocks in the future. If this is the case, the attestation - // should not be considered. - if block_slot > attestation.data.slot { - return Ok(AttestationProcessingOutcome::AttestsToFutureBlock { - block: block_slot, - attestation: attestation.data.slot, - }); - } - - metrics::stop_timer(initial_validation_timer); - - let cache_wait_timer = - metrics::start_timer(&metrics::ATTESTATION_PROCESSING_SHUFFLING_CACHE_WAIT_TIMES); - - let mut shuffling_cache = self - .shuffling_cache - .try_write_for(ATTESTATION_CACHE_LOCK_TIMEOUT) - .ok_or_else(|| Error::AttestationCacheLockTimeout)?; - - metrics::stop_timer(cache_wait_timer); - - let indexed_attestation = - if let Some(committee_cache) = shuffling_cache.get(attestation_epoch, target.root) { - if let Some(committee) = committee_cache - .get_beacon_committee(attestation.data.slot, attestation.data.index) - { - let indexed_attestation = - get_indexed_attestation(committee.committee, &attestation)?; - - // Drop the shuffling cache to avoid holding the lock for any longer than - // required. - drop(shuffling_cache); - - indexed_attestation - } else { - return Ok(AttestationProcessingOutcome::NoCommitteeForSlotAndIndex { - slot: attestation.data.slot, - index: attestation.data.index, - }); - } - } else { - // Drop the shuffling cache to avoid holding the lock for any longer than - // required. - drop(shuffling_cache); - - debug!( + match self.naive_aggregation_pool.insert(attestation) { + Ok(outcome) => trace!( + self.log, + "Stored unaggregated attestation"; + "outcome" => format!("{:?}", outcome), + "index" => attestation.data.index, + "slot" => attestation.data.slot.as_u64(), + ), + Err(NaiveAggregationError::SlotTooLow { + slot, + lowest_permissible_slot, + }) => { + trace!( self.log, - "Attestation processing cache miss"; - "attn_epoch" => attestation_epoch.as_u64(), - "head_block_epoch" => block_slot.epoch(T::EthSpec::slots_per_epoch()).as_u64(), + "Refused to store unaggregated attestation"; + "lowest_permissible_slot" => lowest_permissible_slot.as_u64(), + "slot" => slot.as_u64(), ); - - let state_read_timer = - metrics::start_timer(&metrics::ATTESTATION_PROCESSING_STATE_READ_TIMES); - - let mut state = self - .get_state(&target_block_state_root, Some(target_block_slot))? - .ok_or_else(|| Error::MissingBeaconState(target_block_state_root))?; - - metrics::stop_timer(state_read_timer); - let state_skip_timer = - metrics::start_timer(&metrics::ATTESTATION_PROCESSING_STATE_SKIP_TIMES); - - while state.current_epoch() + 1 < attestation_epoch { - // Here we tell `per_slot_processing` to skip hashing the state and just - // use the zero hash instead. - // - // The state roots are not useful for the shuffling, so there's no need to - // compute them. - per_slot_processing(&mut state, Some(Hash256::zero()), &self.spec)? - } - - metrics::stop_timer(state_skip_timer); - let committee_building_timer = - metrics::start_timer(&metrics::ATTESTATION_PROCESSING_COMMITTEE_BUILDING_TIMES); - - let relative_epoch = - RelativeEpoch::from_epoch(state.current_epoch(), attestation_epoch) - .map_err(Error::IncorrectStateForAttestation)?; - - state.build_committee_cache(relative_epoch, &self.spec)?; - - let committee_cache = state.committee_cache(relative_epoch)?; - - self.shuffling_cache - .try_write_for(ATTESTATION_CACHE_LOCK_TIMEOUT) - .ok_or_else(|| Error::AttestationCacheLockTimeout)? - .insert(attestation_epoch, target.root, committee_cache); - - metrics::stop_timer(committee_building_timer); - - if let Some(committee) = committee_cache - .get_beacon_committee(attestation.data.slot, attestation.data.index) - { - get_indexed_attestation(committee.committee, &attestation)? - } else { - return Ok(AttestationProcessingOutcome::NoCommitteeForSlotAndIndex { - slot: attestation.data.slot, - index: attestation.data.index, - }); - } - }; - - let signature_setup_timer = - metrics::start_timer(&metrics::ATTESTATION_PROCESSING_SIGNATURE_SETUP_TIMES); - - let pubkey_cache = self - .validator_pubkey_cache - .try_read_for(VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT) - .ok_or_else(|| Error::ValidatorPubkeyCacheLockTimeout)?; - - let (fork, genesis_validators_root) = self - .canonical_head - .try_read_for(HEAD_LOCK_TIMEOUT) - .ok_or_else(|| Error::CanonicalHeadLockTimeout) - .map(|head| { - ( - head.beacon_state.fork.clone(), - head.beacon_state.genesis_validators_root, - ) - })?; - - let signature_set = indexed_attestation_signature_set_from_pubkeys( - |validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed), - &attestation.signature, - &indexed_attestation, - &fork, - genesis_validators_root, - &self.spec, - ) - .map_err(Error::SignatureSetError)?; - - metrics::stop_timer(signature_setup_timer); - - let signature_verification_timer = - metrics::start_timer(&metrics::ATTESTATION_PROCESSING_SIGNATURE_TIMES); - - let signature_is_valid = signature_set.is_valid(); - - metrics::stop_timer(signature_verification_timer); - - drop(pubkey_cache); - - if signature_is_valid { - // Provide the attestation to fork choice, updating the validator latest messages but - // _without_ finding and updating the head. - if let Err(e) = self - .fork_choice - .process_indexed_attestation(&indexed_attestation) - { - error!( - self.log, - "Add attestation to fork choice failed"; - "beacon_block_root" => format!("{}", attestation.data.beacon_block_root), - "error" => format!("{:?}", e) - ); - return Err(e.into()); } - - // Provide the valid attestation to op pool, which may choose to retain the - // attestation for inclusion in a future block. If we receive an attestation from a - // subnet without a validator responsible for aggregating it, we don't store it in the - // op pool. - if self.eth1_chain.is_some() { - match attestation_type { - AttestationType::Unaggregated { should_store } if should_store => { - match self.naive_aggregation_pool.insert(&attestation) { - Ok(outcome) => trace!( - self.log, - "Stored unaggregated attestation"; - "outcome" => format!("{:?}", outcome), - "index" => attestation.data.index, - "slot" => attestation.data.slot.as_u64(), - ), - Err(NaiveAggregationError::SlotTooLow { - slot, - lowest_permissible_slot, - }) => { - trace!( - self.log, - "Refused to store unaggregated attestation"; - "lowest_permissible_slot" => lowest_permissible_slot.as_u64(), - "slot" => slot.as_u64(), - ); - } - Err(e) => error!( - self.log, - "Failed to store unaggregated attestation"; - "error" => format!("{:?}", e), - "index" => attestation.data.index, - "slot" => attestation.data.slot.as_u64(), - ), - } - } - AttestationType::Unaggregated { .. } => trace!( + Err(e) => { + error!( self.log, - "Did not store unaggregated attestation"; + "Failed to store unaggregated attestation"; + "error" => format!("{:?}", e), "index" => attestation.data.index, "slot" => attestation.data.slot.as_u64(), - ), - AttestationType::Aggregated => { - let index = attestation.data.index; - let slot = attestation.data.slot; - - match self.op_pool.insert_attestation( - attestation, - &fork, - genesis_validators_root, - &self.spec, - ) { - Ok(_) => {} - Err(e) => { - error!( - self.log, - "Failed to add attestation to op pool"; - "error" => format!("{:?}", e), - "index" => index, - "slot" => slot.as_u64(), - ); - } - } - } - } + ); + return Err(Error::from(e).into()); } + }; - // Update the metrics. - metrics::inc_counter(&metrics::ATTESTATION_PROCESSING_SUCCESSES); + Ok(unaggregated_attestation) + } - Ok(AttestationProcessingOutcome::Processed) - } else { - Ok(AttestationProcessingOutcome::InvalidSignature) + /// Accepts a `VerifiedAggregatedAttestation` and attempts to apply it to `self.op_pool`. + /// + /// The op pool is used by local block producers to pack blocks with operations. + pub fn add_to_block_inclusion_pool( + &self, + signed_aggregate: VerifiedAggregatedAttestation, + ) -> Result, AttestationError> { + // If there's no eth1 chain then it's impossible to produce blocks and therefore + // useless to put things in the op pool. + if self.eth1_chain.is_some() { + let fork = self + .canonical_head + .try_read_for(HEAD_LOCK_TIMEOUT) + .ok_or_else(|| Error::CanonicalHeadLockTimeout)? + .beacon_state + .fork + .clone(); + + self.op_pool + .insert_attestation( + // TODO: address this clone. + signed_aggregate.attestation().clone(), + &fork, + self.genesis_validators_root, + &self.spec, + ) + .map_err(Error::from)?; } + + Ok(signed_aggregate) } /// Check that the shuffling at `block_root` is equal to one of the shufflings of `state`. @@ -1671,6 +1411,24 @@ impl BeaconChain { let parent_block = fully_verified_block.parent_block; let intermediate_states = fully_verified_block.intermediate_states; + let attestation_observation_timer = + metrics::start_timer(&metrics::BLOCK_PROCESSING_ATTESTATION_OBSERVATION); + + // Iterate through the attestations in the block and register them as an "observed + // attestation". This will stop us from propagating them on the gossip network. + for a in &block.body.attestations { + match self.observed_attestations.observe_attestation(a, None) { + // If the observation was successful or if the slot for the attestation was too + // low, continue. + // + // We ignore `SlotTooLow` since this will be very common whilst syncing. + Ok(_) | Err(AttestationObservationError::SlotTooLow { .. }) => {} + Err(e) => return Err(BlockError::BeaconChainError(e.into())), + } + } + + metrics::stop_timer(attestation_observation_timer); + let fork_choice_register_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_FORK_CHOICE_REGISTER); @@ -2104,6 +1862,9 @@ impl BeaconChain { } else { self.fork_choice.prune()?; + self.observed_block_producers + .prune(new_finalized_epoch.start_slot(T::EthSpec::slots_per_epoch())); + self.snapshot_cache .try_write_for(BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT) .map(|mut snapshot_cache| { diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 886835c6d0..63ca502f10 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -104,10 +104,18 @@ pub enum BlockError { }, /// Block is already known, no need to re-import. BlockIsAlreadyKnown, + /// A block for this proposer and slot has already been observed. + RepeatProposal { proposer: u64, slot: Slot }, /// The block slot exceeds the MAXIMUM_BLOCK_SLOT_NUMBER. BlockSlotLimitReached, + /// The `BeaconBlock` has a `proposer_index` that does not match the index we computed locally. + /// + /// The block is invalid. + IncorrectBlockProposer { block: u64, local_shuffling: u64 }, /// The proposal signature in invalid. ProposalSignatureInvalid, + /// The `block.proposal_index` is not known. + UnknownValidator(u64), /// A signature in the block is invalid (exactly which is unknown). InvalidSignature, /// The provided block is from an earlier slot than its parent. @@ -126,7 +134,18 @@ pub enum BlockError { impl From for BlockError { fn from(e: BlockSignatureVerifierError) -> Self { - BlockError::BeaconChainError(BeaconChainError::BlockSignatureVerifierError(e)) + match e { + // Make a special distinction for `IncorrectBlockProposer` since it indicates an + // invalid block, not an internal error. + BlockSignatureVerifierError::IncorrectBlockProposer { + block, + local_shuffling, + } => BlockError::IncorrectBlockProposer { + block, + local_shuffling, + }, + e => BlockError::BeaconChainError(BeaconChainError::BlockSignatureVerifierError(e)), + } } } @@ -286,7 +305,17 @@ impl GossipVerifiedBlock { // Do not gossip a block from a finalized slot. check_block_against_finalized_slot(&block.message, chain)?; - // TODO: add check for the `(block.proposer_index, block.slot)` tuple once we have v0.11.0 + // Check that we have not already received a block with a valid signature for this slot. + if chain + .observed_block_producers + .proposer_has_been_observed(&block.message) + .map_err(|e| BlockError::BeaconChainError(e.into()))? + { + return Err(BlockError::RepeatProposal { + proposer: block.message.proposer_index, + slot: block.message.slot, + }); + } let mut parent = load_parent(&block.message, chain)?; let block_root = get_block_root(&block); @@ -297,21 +326,54 @@ impl GossipVerifiedBlock { &chain.spec, )?; - let pubkey_cache = get_validator_pubkey_cache(chain)?; + let signature_is_valid = { + let pubkey_cache = get_validator_pubkey_cache(chain)?; + let pubkey = pubkey_cache + .get(block.message.proposer_index as usize) + .ok_or_else(|| BlockError::UnknownValidator(block.message.proposer_index))?; + block.verify_signature( + Some(block_root), + pubkey, + &state.fork, + chain.genesis_validators_root, + &chain.spec, + ) + }; - let mut signature_verifier = get_signature_verifier(&state, &pubkey_cache, &chain.spec); - - signature_verifier.include_block_proposal(&block, Some(block_root))?; - - if signature_verifier.verify().is_ok() { - Ok(Self { - block, - block_root, - parent, - }) - } else { - Err(BlockError::ProposalSignatureInvalid) + if !signature_is_valid { + return Err(BlockError::ProposalSignatureInvalid); } + + // Now the signature is valid, store the proposal so we don't accept another from this + // validator and slot. + // + // It's important to double-check that the proposer still hasn't been observed so we don't + // have a race-condition when verifying two blocks simultaneously. + if chain + .observed_block_producers + .observe_proposer(&block.message) + .map_err(|e| BlockError::BeaconChainError(e.into()))? + { + return Err(BlockError::RepeatProposal { + proposer: block.message.proposer_index, + slot: block.message.slot, + }); + } + + let expected_proposer = + state.get_beacon_proposer_index(block.message.slot, &chain.spec)? as u64; + if block.message.proposer_index != expected_proposer { + return Err(BlockError::IncorrectBlockProposer { + block: block.message.proposer_index, + local_shuffling: expected_proposer, + }); + } + + Ok(Self { + block, + block_root, + parent, + }) } pub fn block_root(&self) -> Hash256 { diff --git a/beacon_node/beacon_chain/src/block_verification/block_processing_outcome.rs b/beacon_node/beacon_chain/src/block_verification/block_processing_outcome.rs index fe0f71a50e..8e38af4734 100644 --- a/beacon_node/beacon_chain/src/block_verification/block_processing_outcome.rs +++ b/beacon_node/beacon_chain/src/block_verification/block_processing_outcome.rs @@ -14,6 +14,8 @@ pub enum BlockProcessingOutcome { InvalidSignature, /// The proposal signature in invalid. ProposalSignatureInvalid, + /// The `block.proposal_index` is not known. + UnknownValidator(u64), /// The parent block was unknown. ParentUnknown(Hash256), /// The block slot is greater than the present slot. @@ -35,6 +37,11 @@ pub enum BlockProcessingOutcome { }, /// Block is already known, no need to re-import. BlockIsAlreadyKnown, + /// A block for this proposer and slot has already been observed. + RepeatProposal { + proposer: u64, + slot: Slot, + }, /// The block slot exceeds the MAXIMUM_BLOCK_SLOT_NUMBER. BlockSlotLimitReached, /// The provided block is from an earlier slot than its parent. @@ -42,6 +49,13 @@ pub enum BlockProcessingOutcome { block_slot: Slot, state_slot: Slot, }, + /// The `BeaconBlock` has a `proposer_index` that does not match the index we computed locally. + /// + /// The block is invalid. + IncorrectBlockProposer { + block: u64, + local_shuffling: u64, + }, /// At least one block in the chain segement did not have it's parent root set to the root of /// the prior block. NonLinearParentRoots, @@ -78,12 +92,16 @@ impl BlockProcessingOutcome { finalized_slot, }), Err(BlockError::BlockIsAlreadyKnown) => Ok(BlockProcessingOutcome::BlockIsAlreadyKnown), + Err(BlockError::RepeatProposal { proposer, slot }) => { + Ok(BlockProcessingOutcome::RepeatProposal { proposer, slot }) + } Err(BlockError::BlockSlotLimitReached) => { Ok(BlockProcessingOutcome::BlockSlotLimitReached) } Err(BlockError::ProposalSignatureInvalid) => { Ok(BlockProcessingOutcome::ProposalSignatureInvalid) } + Err(BlockError::UnknownValidator(i)) => Ok(BlockProcessingOutcome::UnknownValidator(i)), Err(BlockError::InvalidSignature) => Ok(BlockProcessingOutcome::InvalidSignature), Err(BlockError::BlockIsNotLaterThanParent { block_slot, @@ -92,6 +110,13 @@ impl BlockProcessingOutcome { block_slot, state_slot, }), + Err(BlockError::IncorrectBlockProposer { + block, + local_shuffling, + }) => Ok(BlockProcessingOutcome::IncorrectBlockProposer { + block, + local_shuffling, + }), Err(BlockError::NonLinearParentRoots) => { Ok(BlockProcessingOutcome::NonLinearParentRoots) } diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 7a433aab4e..a6b49177fb 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -431,6 +431,14 @@ where .ok_or_else(|| "Cannot build without op pool".to_string())?, // TODO: allow for persisting and loading the pool from disk. naive_aggregation_pool: <_>::default(), + // TODO: allow for persisting and loading the pool from disk. + observed_attestations: <_>::default(), + // TODO: allow for persisting and loading the pool from disk. + observed_attesters: <_>::default(), + // TODO: allow for persisting and loading the pool from disk. + observed_aggregators: <_>::default(), + // TODO: allow for persisting and loading the pool from disk. + observed_block_producers: <_>::default(), eth1_chain: self.eth1_chain, genesis_validators_root: canonical_head.beacon_state.genesis_validators_root, canonical_head: TimeoutRwLock::new(canonical_head.clone()), diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index ffe32f3408..17ef7c23f8 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -1,7 +1,11 @@ use crate::eth1_chain::Error as Eth1ChainError; use crate::fork_choice::Error as ForkChoiceError; use crate::naive_aggregation_pool::Error as NaiveAggregationError; +use crate::observed_attestations::Error as ObservedAttestationsError; +use crate::observed_attesters::Error as ObservedAttestersError; +use crate::observed_block_producers::Error as ObservedBlockProducersError; use operation_pool::OpPoolError; +use safe_arith::ArithError; use ssz::DecodeError; use ssz_types::Error as SszTypesError; use state_processing::{ @@ -66,6 +70,10 @@ pub enum BeaconChainError { ValidatorPubkeyCacheFileError(String), OpPoolError(OpPoolError), NaiveAggregationError(NaiveAggregationError), + ObservedAttestationsError(ObservedAttestationsError), + ObservedAttestersError(ObservedAttestersError), + ObservedBlockProducersError(ObservedBlockProducersError), + ArithError(ArithError), } easy_from_to!(SlotProcessingError, BeaconChainError); @@ -73,7 +81,11 @@ easy_from_to!(AttestationValidationError, BeaconChainError); easy_from_to!(SszTypesError, BeaconChainError); easy_from_to!(OpPoolError, BeaconChainError); easy_from_to!(NaiveAggregationError, BeaconChainError); +easy_from_to!(ObservedAttestationsError, BeaconChainError); +easy_from_to!(ObservedAttestersError, BeaconChainError); +easy_from_to!(ObservedBlockProducersError, BeaconChainError); easy_from_to!(BlockSignatureVerifierError, BeaconChainError); +easy_from_to!(ArithError, BeaconChainError); #[derive(Debug, PartialEq)] pub enum BlockProductionError { diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 0ab3594ae5..e20f66d3b4 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -2,6 +2,7 @@ #[macro_use] extern crate lazy_static; +pub mod attestation_verification; mod beacon_chain; mod beacon_snapshot; mod block_verification; @@ -14,6 +15,9 @@ mod head_tracker; mod metrics; pub mod migrate; mod naive_aggregation_pool; +mod observed_attestations; +mod observed_attesters; +mod observed_block_producers; mod persisted_beacon_chain; mod shuffling_cache; mod snapshot_cache; @@ -22,11 +26,12 @@ mod timeout_rw_lock; mod validator_pubkey_cache; pub use self::beacon_chain::{ - AttestationProcessingOutcome, AttestationType, BeaconChain, BeaconChainTypes, - ChainSegmentResult, StateSkipConfig, + AttestationProcessingOutcome, BeaconChain, BeaconChainTypes, ChainSegmentResult, + StateSkipConfig, }; pub use self::beacon_snapshot::BeaconSnapshot; pub use self::errors::{BeaconChainError, BlockProductionError}; +pub use attestation_verification::Error as AttestationError; pub use block_verification::{BlockError, BlockProcessingOutcome, GossipVerifiedBlock}; pub use eth1_chain::{Eth1Chain, Eth1ChainBackend}; pub use events::EventHandler; diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index 39cd234fb1..c6e3afebcf 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -52,6 +52,10 @@ lazy_static! { "beacon_block_processing_fork_choice_register_seconds", "Time spent registering the new block with fork choice (but not finding head)" ); + pub static ref BLOCK_PROCESSING_ATTESTATION_OBSERVATION: Result = try_create_histogram( + "beacon_block_processing_attestation_observation_seconds", + "Time spent hashing and remembering all the attestations in the block" + ); /* * Block Production diff --git a/beacon_node/beacon_chain/src/naive_aggregation_pool.rs b/beacon_node/beacon_chain/src/naive_aggregation_pool.rs index 6718fd2f3e..4f63f724e8 100644 --- a/beacon_node/beacon_chain/src/naive_aggregation_pool.rs +++ b/beacon_node/beacon_chain/src/naive_aggregation_pool.rs @@ -46,8 +46,6 @@ pub enum Error { /// stored. This indicates a fairly serious error somewhere in the code that called this /// function. InconsistentBitfieldLengths, - /// The function to obtain a map index failed, this is an internal error. - InvalidMapIndex(usize), /// The given `attestation` was for the incorrect slot. This is an internal error. IncorrectSlot { expected: Slot, attestation: Slot }, } @@ -56,30 +54,20 @@ pub enum Error { /// `attestation` are from the same slot. struct AggregatedAttestationMap { map: HashMap>, - slot: Slot, } impl AggregatedAttestationMap { - /// Create an empty collection that will only contain attestation for the given `slot`. - pub fn new(slot: Slot) -> Self { + /// Create an empty collection with the given `initial_capacity`. + pub fn new(initial_capacity: usize) -> Self { Self { - slot, - map: <_>::default(), + map: HashMap::with_capacity(initial_capacity), } } /// Insert an attestation into `self`, aggregating it into the pool. /// - /// The given attestation (`a`) must only have one signature and be from the slot that `self` - /// was initialized with. + /// The given attestation (`a`) must only have one signature. pub fn insert(&mut self, a: &Attestation) -> Result { - if a.data.slot != self.slot { - return Err(Error::IncorrectSlot { - expected: self.slot, - attestation: a.data.slot, - }); - } - let set_bits = a .aggregation_bits .iter() @@ -124,15 +112,12 @@ impl AggregatedAttestationMap { /// /// The given `a.data.slot` must match the slot that `self` was initialized with. pub fn get(&self, data: &AttestationData) -> Result>, Error> { - if data.slot != self.slot { - return Err(Error::IncorrectSlot { - expected: self.slot, - attestation: data.slot, - }); - } - Ok(self.map.get(data).cloned()) } + + pub fn len(&self) -> usize { + self.map.len() + } } /// A pool of `Attestation` that is specially designed to store "unaggregated" attestations from @@ -158,14 +143,14 @@ impl AggregatedAttestationMap { /// receives and it can be triggered manually. pub struct NaiveAggregationPool { lowest_permissible_slot: RwLock, - maps: RwLock>>, + maps: RwLock>>, } impl Default for NaiveAggregationPool { fn default() -> Self { Self { lowest_permissible_slot: RwLock::new(Slot::new(0)), - maps: RwLock::new(vec![]), + maps: RwLock::new(HashMap::new()), } } } @@ -179,28 +164,46 @@ impl NaiveAggregationPool { /// The pool may be pruned if the given `attestation.data` has a slot higher than any /// previously seen. pub fn insert(&self, attestation: &Attestation) -> Result { + let slot = attestation.data.slot; let lowest_permissible_slot = *self.lowest_permissible_slot.read(); // Reject any attestations that are too old. - if attestation.data.slot < lowest_permissible_slot { + if slot < lowest_permissible_slot { return Err(Error::SlotTooLow { - slot: attestation.data.slot, + slot, lowest_permissible_slot, }); } - // Prune the pool if this attestation indicates that the current slot has advanced. - if (lowest_permissible_slot + SLOTS_RETAINED as u64) < attestation.data.slot + 1 { - self.prune(attestation.data.slot) - } + let mut maps = self.maps.write(); - let index = self.get_map_index(attestation.data.slot); + let outcome = if let Some(map) = maps.get_mut(&slot) { + map.insert(attestation) + } else { + // To avoid re-allocations, try and determine a rough initial capacity for the new item + // by obtaining the mean size of all items in earlier epoch. + let (count, sum) = maps + .iter() + // Only include epochs that are less than the given slot in the average. This should + // generally avoid including recent epochs that are still "filling up". + .filter(|(map_slot, _item)| **map_slot < slot) + .map(|(_slot, map)| map.len()) + .fold((0, 0), |(count, sum), len| (count + 1, sum + len)); - self.maps - .write() - .get_mut(index) - .ok_or_else(|| Error::InvalidMapIndex(index))? - .insert(attestation) + // Use the mainnet default committee size if we can't determine an average. + let initial_capacity = sum.checked_div(count).unwrap_or(128); + + let mut item = AggregatedAttestationMap::new(initial_capacity); + let outcome = item.insert(attestation); + maps.insert(slot, item); + + outcome + }; + + drop(maps); + self.prune(slot); + + outcome } /// Returns an aggregated `Attestation` with the given `data`, if any. @@ -208,8 +211,8 @@ impl NaiveAggregationPool { self.maps .read() .iter() - .find(|map| map.slot == data.slot) - .map(|map| map.get(data)) + .find(|(slot, _map)| **slot == data.slot) + .map(|(_slot, map)| map.get(data)) .unwrap_or_else(|| Ok(None)) } @@ -218,41 +221,26 @@ impl NaiveAggregationPool { pub fn prune(&self, current_slot: Slot) { // Taking advantage of saturating subtraction on `Slot`. let lowest_permissible_slot = current_slot - Slot::from(SLOTS_RETAINED); - - self.maps - .write() - .retain(|map| map.slot >= lowest_permissible_slot); - *self.lowest_permissible_slot.write() = lowest_permissible_slot; - } - - /// Returns the index of `self.maps` that matches `slot`. - /// - /// If there is no existing map for this slot one will be created. If `self.maps.len() >= - /// SLOTS_RETAINED`, the map with the lowest slot will be replaced. - fn get_map_index(&self, slot: Slot) -> usize { let mut maps = self.maps.write(); - if let Some(index) = maps.iter().position(|map| map.slot == slot) { - return index; + // Remove any maps that are definitely expired. + maps.retain(|slot, _map| *slot >= lowest_permissible_slot); + + // If we have too many maps, remove the lowest amount to ensure we only have + // `SLOTS_RETAINED` left. + if maps.len() > SLOTS_RETAINED { + let mut slots = maps.iter().map(|(slot, _map)| *slot).collect::>(); + // Sort is generally pretty slow, however `SLOTS_RETAINED` is quite low so it should be + // negligible. + slots.sort_unstable(); + slots + .into_iter() + .take(maps.len().saturating_sub(SLOTS_RETAINED)) + .for_each(|slot| { + maps.remove(&slot); + }) } - - if maps.len() < SLOTS_RETAINED || maps.is_empty() { - let index = maps.len(); - maps.push(AggregatedAttestationMap::new(slot)); - return index; - } - - let index = maps - .iter() - .enumerate() - .min_by_key(|(_i, map)| map.slot) - .map(|(i, _map)| i) - .expect("maps cannot be empty due to previous .is_empty() check"); - - maps[index] = AggregatedAttestationMap::new(slot); - - index } } @@ -432,7 +420,7 @@ mod tests { .maps .read() .iter() - .map(|map| map.slot) + .map(|(slot, _map)| *slot) .collect::>(); pool_slots.sort_unstable(); diff --git a/beacon_node/beacon_chain/src/observed_attestations.rs b/beacon_node/beacon_chain/src/observed_attestations.rs new file mode 100644 index 0000000000..a9896f1377 --- /dev/null +++ b/beacon_node/beacon_chain/src/observed_attestations.rs @@ -0,0 +1,442 @@ +//! Provides an `ObservedAttestations` struct which allows us to reject aggregated attestations if +//! we've already seen the aggregated attestation. + +use parking_lot::RwLock; +use std::collections::HashSet; +use std::marker::PhantomData; +use tree_hash::TreeHash; +use types::{Attestation, EthSpec, Hash256, Slot}; + +/// As a DoS protection measure, the maximum number of distinct `Attestations` that will be +/// recorded for each slot. +/// +/// Currently this is set to ~524k. If we say that each entry is 40 bytes (Hash256 (32 bytes) + an +/// 8 byte hash) then this comes to about 20mb per slot. If we're storing 34 of these slots, then +/// we're at 680mb. This is a lot of memory usage, but probably not a show-stopper for most +/// reasonable hardware. +/// +/// Upstream conditions should strongly restrict the amount of attestations that can show up in +/// this pool. The maximum size with respect to upstream restrictions is more likely on the order +/// of the number of validators. +const MAX_OBSERVATIONS_PER_SLOT: usize = 1 << 19; // 524,288 + +#[derive(Debug, PartialEq)] +pub enum ObserveOutcome { + /// This attestation was already known. + AlreadyKnown, + /// This was the first time this attestation was observed. + New, +} + +#[derive(Debug, PartialEq)] +pub enum Error { + SlotTooLow { + slot: Slot, + lowest_permissible_slot: Slot, + }, + /// The function to obtain a set index failed, this is an internal error. + InvalidSetIndex(usize), + /// We have reached the maximum number of unique `Attestation` that can be observed in a slot. + /// This is a DoS protection function. + ReachedMaxObservationsPerSlot(usize), + IncorrectSlot { + expected: Slot, + attestation: Slot, + }, +} + +/// A `HashSet` that contains entries related to some `Slot`. +struct SlotHashSet { + set: HashSet, + slot: Slot, +} + +impl SlotHashSet { + pub fn new(slot: Slot, initial_capacity: usize) -> Self { + Self { + slot, + set: HashSet::with_capacity(initial_capacity), + } + } + + /// Store the attestation in self so future observations recognise its existence. + pub fn observe_attestation( + &mut self, + a: &Attestation, + root: Hash256, + ) -> Result { + if a.data.slot != self.slot { + return Err(Error::IncorrectSlot { + expected: self.slot, + attestation: a.data.slot, + }); + } + + if self.set.contains(&root) { + Ok(ObserveOutcome::AlreadyKnown) + } else { + // Here we check to see if this slot has reached the maximum observation count. + // + // The resulting behaviour is that we are no longer able to successfully observe new + // attestations, however we will continue to return `is_known` values. We could also + // disable `is_known`, however then we would stop forwarding attestations across the + // gossip network and I think that this is a worse case than sending some invalid ones. + // The underlying libp2p network is responsible for removing duplicate messages, so + // this doesn't risk a broadcast loop. + if self.set.len() >= MAX_OBSERVATIONS_PER_SLOT { + return Err(Error::ReachedMaxObservationsPerSlot( + MAX_OBSERVATIONS_PER_SLOT, + )); + } + + self.set.insert(root); + + Ok(ObserveOutcome::New) + } + } + + /// Indicates if `a` has been observed before. + pub fn is_known(&self, a: &Attestation, root: Hash256) -> Result { + if a.data.slot != self.slot { + return Err(Error::IncorrectSlot { + expected: self.slot, + attestation: a.data.slot, + }); + } + + Ok(self.set.contains(&root)) + } + + /// The number of observed attestations in `self`. + pub fn len(&self) -> usize { + self.set.len() + } +} + +/// Stores the roots of `Attestation` objects for some number of `Slots`, so we can determine if +/// these have previously been seen on the network. +pub struct ObservedAttestations { + lowest_permissible_slot: RwLock, + sets: RwLock>, + _phantom: PhantomData, +} + +impl Default for ObservedAttestations { + fn default() -> Self { + Self { + lowest_permissible_slot: RwLock::new(Slot::new(0)), + sets: RwLock::new(vec![]), + _phantom: PhantomData, + } + } +} + +impl ObservedAttestations { + /// Store the root of `a` in `self`. + /// + /// `root` must equal `a.tree_hash_root()`. + pub fn observe_attestation( + &self, + a: &Attestation, + root_opt: Option, + ) -> Result { + let index = self.get_set_index(a.data.slot)?; + let root = root_opt.unwrap_or_else(|| a.tree_hash_root()); + + self.sets + .write() + .get_mut(index) + .ok_or_else(|| Error::InvalidSetIndex(index)) + .and_then(|set| set.observe_attestation(a, root)) + } + + /// Check to see if the `root` of `a` is in self. + /// + /// `root` must equal `a.tree_hash_root()`. + pub fn is_known(&self, a: &Attestation, root: Hash256) -> Result { + let index = self.get_set_index(a.data.slot)?; + + self.sets + .read() + .get(index) + .ok_or_else(|| Error::InvalidSetIndex(index)) + .and_then(|set| set.is_known(a, root)) + } + + /// The maximum number of slots that attestations are stored for. + fn max_capacity(&self) -> u64 { + // We add `2` in order to account for one slot either side of the range due to + // `MAXIMUM_GOSSIP_CLOCK_DISPARITY`. + E::slots_per_epoch() + 2 + } + + /// Removes any attestations with a slot lower than `current_slot` and bars any future + /// attestations with a slot lower than `current_slot - SLOTS_RETAINED`. + pub fn prune(&self, current_slot: Slot) { + // Taking advantage of saturating subtraction on `Slot`. + let lowest_permissible_slot = current_slot - (self.max_capacity() - 1); + + self.sets + .write() + .retain(|set| set.slot >= lowest_permissible_slot); + + *self.lowest_permissible_slot.write() = lowest_permissible_slot; + } + + /// Returns the index of `self.set` that matches `slot`. + /// + /// If there is no existing set for this slot one will be created. If `self.sets.len() >= + /// Self::max_capacity()`, the set with the lowest slot will be replaced. + fn get_set_index(&self, slot: Slot) -> Result { + let lowest_permissible_slot: Slot = *self.lowest_permissible_slot.read(); + + if slot < lowest_permissible_slot { + return Err(Error::SlotTooLow { + slot, + lowest_permissible_slot, + }); + } + + // Prune the pool if this attestation indicates that the current slot has advanced. + if lowest_permissible_slot + self.max_capacity() < slot + 1 { + self.prune(slot) + } + + let mut sets = self.sets.write(); + + if let Some(index) = sets.iter().position(|set| set.slot == slot) { + return Ok(index); + } + + // To avoid re-allocations, try and determine a rough initial capacity for the new set + // by obtaining the mean size of all items in earlier epoch. + let (count, sum) = sets + .iter() + // Only include slots that are less than the given slot in the average. This should + // generally avoid including recent slots that are still "filling up". + .filter(|set| set.slot < slot) + .map(|set| set.len()) + .fold((0, 0), |(count, sum), len| (count + 1, sum + len)); + // If we are unable to determine an average, just use 128 as it's the target committee + // size for the mainnet spec. This is perhaps a little wasteful for the minimal spec, + // but considering it's approx. 128 * 32 bytes we're not wasting much. + let initial_capacity = sum.checked_div(count).unwrap_or(128); + + if sets.len() < self.max_capacity() as usize || sets.is_empty() { + let index = sets.len(); + sets.push(SlotHashSet::new(slot, initial_capacity)); + return Ok(index); + } + + let index = sets + .iter() + .enumerate() + .min_by_key(|(_i, set)| set.slot) + .map(|(i, _set)| i) + .expect("sets cannot be empty due to previous .is_empty() check"); + + sets[index] = SlotHashSet::new(slot, initial_capacity); + + Ok(index) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tree_hash::TreeHash; + use types::{test_utils::test_random_instance, Hash256}; + + type E = types::MainnetEthSpec; + + const NUM_ELEMENTS: usize = 8; + + fn get_attestation(slot: Slot, beacon_block_root: u64) -> Attestation { + let mut a: Attestation = test_random_instance(); + a.data.slot = slot; + a.data.beacon_block_root = Hash256::from_low_u64_be(beacon_block_root); + a + } + + fn single_slot_test(store: &ObservedAttestations, slot: Slot) { + let attestations = (0..NUM_ELEMENTS as u64) + .map(|i| get_attestation(slot, i)) + .collect::>(); + + for a in &attestations { + assert_eq!( + store.is_known(a, a.tree_hash_root()), + Ok(false), + "should indicate an unknown attestation is unknown" + ); + assert_eq!( + store.observe_attestation(a, None), + Ok(ObserveOutcome::New), + "should observe new attestation" + ); + } + + for a in &attestations { + assert_eq!( + store.is_known(a, a.tree_hash_root()), + Ok(true), + "should indicate a known attestation is known" + ); + assert_eq!( + store.observe_attestation(a, Some(a.tree_hash_root())), + Ok(ObserveOutcome::AlreadyKnown), + "should acknowledge an existing attestation" + ); + } + } + + #[test] + fn single_slot() { + let store = ObservedAttestations::default(); + + single_slot_test(&store, Slot::new(0)); + + assert_eq!( + store.sets.read().len(), + 1, + "should have a single set stored" + ); + assert_eq!( + store.sets.read()[0].len(), + NUM_ELEMENTS, + "set should have NUM_ELEMENTS elements" + ); + } + + #[test] + fn mulitple_contiguous_slots() { + let store = ObservedAttestations::default(); + let max_cap = store.max_capacity(); + + for i in 0..max_cap * 3 { + let slot = Slot::new(i); + + single_slot_test(&store, slot); + + /* + * Ensure that the number of sets is correct. + */ + + if i < max_cap { + assert_eq!( + store.sets.read().len(), + i as usize + 1, + "should have a {} sets stored", + i + 1 + ); + } else { + assert_eq!( + store.sets.read().len(), + max_cap as usize, + "should have max_capacity sets stored" + ); + } + + /* + * Ensure that each set contains the correct number of elements. + */ + + for set in &store.sets.read()[..] { + assert_eq!( + set.len(), + NUM_ELEMENTS, + "each store should have NUM_ELEMENTS elements" + ) + } + + /* + * Ensure that all the sets have the expected slots + */ + + let mut store_slots = store + .sets + .read() + .iter() + .map(|set| set.slot) + .collect::>(); + + assert!( + store_slots.len() <= store.max_capacity() as usize, + "store size should not exceed max" + ); + + store_slots.sort_unstable(); + + let expected_slots = (i.saturating_sub(max_cap - 1)..=i) + .map(Slot::new) + .collect::>(); + + assert_eq!(expected_slots, store_slots, "should have expected slots"); + } + } + + #[test] + fn mulitple_non_contiguous_slots() { + let store = ObservedAttestations::default(); + let max_cap = store.max_capacity(); + + let to_skip = vec![1_u64, 2, 3, 5, 6, 29, 30, 31, 32, 64]; + let slots = (0..max_cap * 3) + .into_iter() + .filter(|i| !to_skip.contains(i)) + .collect::>(); + + for &i in &slots { + if to_skip.contains(&i) { + continue; + } + + let slot = Slot::from(i); + + single_slot_test(&store, slot); + + /* + * Ensure that each set contains the correct number of elements. + */ + + for set in &store.sets.read()[..] { + assert_eq!( + set.len(), + NUM_ELEMENTS, + "each store should have NUM_ELEMENTS elements" + ) + } + + /* + * Ensure that all the sets have the expected slots + */ + + let mut store_slots = store + .sets + .read() + .iter() + .map(|set| set.slot) + .collect::>(); + + store_slots.sort_unstable(); + + assert!( + store_slots.len() <= store.max_capacity() as usize, + "store size should not exceed max" + ); + + let lowest = store.lowest_permissible_slot.read().as_u64(); + let highest = slot.as_u64(); + let expected_slots = (lowest..=highest) + .filter(|i| !to_skip.contains(i)) + .map(Slot::new) + .collect::>(); + + assert_eq!( + expected_slots, + &store_slots[..], + "should have expected slots" + ); + } + } +} diff --git a/beacon_node/beacon_chain/src/observed_attesters.rs b/beacon_node/beacon_chain/src/observed_attesters.rs new file mode 100644 index 0000000000..8be39853ff --- /dev/null +++ b/beacon_node/beacon_chain/src/observed_attesters.rs @@ -0,0 +1,441 @@ +//! Provides two structs that help us filter out attestation gossip from validators that have +//! already published attestations: +//! +//! - `ObservedAttesters`: allows filtering unaggregated attestations from the same validator in +//! the same epoch. +//! - `ObservedAggregators`: allows filtering aggregated attestations from the same aggregators in +//! the same epoch + +use bitvec::vec::BitVec; +use parking_lot::RwLock; +use std::collections::{HashMap, HashSet}; +use std::marker::PhantomData; +use types::{Attestation, Epoch, EthSpec, Unsigned}; + +pub type ObservedAttesters = AutoPruningContainer; +pub type ObservedAggregators = AutoPruningContainer; + +#[derive(Debug, PartialEq)] +pub enum Error { + EpochTooLow { + epoch: Epoch, + lowest_permissible_epoch: Epoch, + }, + /// We have reached the maximum number of unique `Attestation` that can be observed in a slot. + /// This is a DoS protection function. + ReachedMaxObservationsPerSlot(usize), + /// The function to obtain a set index failed, this is an internal error. + ValidatorIndexTooHigh(usize), +} + +/// Implemented on an item in an `AutoPruningContainer`. +pub trait Item { + /// Instantiate `Self` with the given `capacity`. + fn with_capacity(capacity: usize) -> Self; + + /// The default capacity for self. Used when we can't guess a reasonable size. + fn default_capacity() -> usize; + + /// Returns the number of validator indices stored in `self`. + fn len(&self) -> usize; + + /// Store `validator_index` in `self`. + fn insert(&mut self, validator_index: usize) -> bool; + + /// Returns `true` if `validator_index` has been stored in `self`. + fn contains(&self, validator_index: usize) -> bool; +} + +/// Stores a `BitVec` that represents which validator indices have attested during an epoch. +pub struct EpochBitfield { + bitfield: BitVec, +} + +impl Item for EpochBitfield { + fn with_capacity(capacity: usize) -> Self { + Self { + bitfield: BitVec::with_capacity(capacity), + } + } + + /// Uses a default size that equals the number of genesis validators. + fn default_capacity() -> usize { + 16_384 + } + + fn len(&self) -> usize { + self.bitfield.len() + } + + fn insert(&mut self, validator_index: usize) -> bool { + self.bitfield + .get_mut(validator_index) + .map(|mut bit| { + if *bit { + true + } else { + *bit = true; + false + } + }) + .unwrap_or_else(|| { + self.bitfield + .resize(validator_index.saturating_add(1), false); + self.bitfield + .get_mut(validator_index) + .map(|mut bit| *bit = true); + false + }) + } + + fn contains(&self, validator_index: usize) -> bool { + self.bitfield.get(validator_index).map_or(false, |bit| *bit) + } +} + +/// Stores a `HashSet` of which validator indices have created an aggregate attestation during an +/// epoch. +pub struct EpochHashSet { + set: HashSet, +} + +impl Item for EpochHashSet { + fn with_capacity(capacity: usize) -> Self { + Self { + set: HashSet::with_capacity(capacity), + } + } + + /// Defaults to the target number of aggregators per committee (16) multiplied by the expected + /// max committee count (64). + fn default_capacity() -> usize { + 16 * 64 + } + + fn len(&self) -> usize { + self.set.len() + } + + /// Inserts the `validator_index` in the set. Returns `true` if the `validator_index` was + /// already in the set. + fn insert(&mut self, validator_index: usize) -> bool { + !self.set.insert(validator_index) + } + + /// Returns `true` if the `validator_index` is in the set. + fn contains(&self, validator_index: usize) -> bool { + self.set.contains(&validator_index) + } +} + +/// A container that stores some number of `T` items. +/// +/// This container is "auto-pruning" since it gets an idea of the current slot by which +/// attestations are provided to it and prunes old entries based upon that. For example, if +/// `Self::max_capacity == 32` and an attestation with `a.data.target.epoch` is supplied, then all +/// attestations with an epoch prior to `a.data.target.epoch - 32` will be cleared from the cache. +/// +/// `T` should be set to a `EpochBitfield` or `EpochHashSet`. +pub struct AutoPruningContainer { + lowest_permissible_epoch: RwLock, + items: RwLock>, + _phantom: PhantomData, +} + +impl Default for AutoPruningContainer { + fn default() -> Self { + Self { + lowest_permissible_epoch: RwLock::new(Epoch::new(0)), + items: RwLock::new(HashMap::new()), + _phantom: PhantomData, + } + } +} + +impl AutoPruningContainer { + /// Observe that `validator_index` has produced attestation `a`. Returns `Ok(true)` if `a` has + /// previously been observed for `validator_index`. + /// + /// ## Errors + /// + /// - `validator_index` is higher than `VALIDATOR_REGISTRY_LIMIT`. + /// - `a.data.target.slot` is earlier than `self.earliest_permissible_slot`. + pub fn observe_validator( + &self, + a: &Attestation, + validator_index: usize, + ) -> Result { + self.sanitize_request(a, validator_index)?; + + let epoch = a.data.target.epoch; + + self.prune(epoch); + + let mut items = self.items.write(); + + if let Some(item) = items.get_mut(&epoch) { + Ok(item.insert(validator_index)) + } else { + // To avoid re-allocations, try and determine a rough initial capacity for the new item + // by obtaining the mean size of all items in earlier epoch. + let (count, sum) = items + .iter() + // Only include epochs that are less than the given slot in the average. This should + // generally avoid including recent epochs that are still "filling up". + .filter(|(item_epoch, _item)| **item_epoch < epoch) + .map(|(_epoch, item)| item.len()) + .fold((0, 0), |(count, sum), len| (count + 1, sum + len)); + + let initial_capacity = sum.checked_div(count).unwrap_or(T::default_capacity()); + + let mut item = T::with_capacity(initial_capacity); + item.insert(validator_index); + items.insert(epoch, item); + + Ok(false) + } + } + + /// Returns `Ok(true)` if the `validator_index` has produced an attestation conflicting with + /// `a`. + /// + /// ## Errors + /// + /// - `validator_index` is higher than `VALIDATOR_REGISTRY_LIMIT`. + /// - `a.data.target.slot` is earlier than `self.earliest_permissible_slot`. + pub fn validator_has_been_observed( + &self, + a: &Attestation, + validator_index: usize, + ) -> Result { + self.sanitize_request(a, validator_index)?; + + let exists = self + .items + .read() + .get(&a.data.target.epoch) + .map_or(false, |item| item.contains(validator_index)); + + Ok(exists) + } + + fn sanitize_request(&self, a: &Attestation, validator_index: usize) -> Result<(), Error> { + if validator_index > E::ValidatorRegistryLimit::to_usize() { + return Err(Error::ValidatorIndexTooHigh(validator_index)); + } + + let epoch = a.data.target.epoch; + let lowest_permissible_epoch: Epoch = *self.lowest_permissible_epoch.read(); + if epoch < lowest_permissible_epoch { + return Err(Error::EpochTooLow { + epoch, + lowest_permissible_epoch, + }); + } + + Ok(()) + } + + /// The maximum number of epochs stored in `self`. + fn max_capacity(&self) -> u64 { + // The current epoch and the previous epoch. This is sufficient whilst + // GOSSIP_CLOCK_DISPARITY is 1/2 a slot or less: + // + // https://github.com/ethereum/eth2.0-specs/pull/1706#issuecomment-610151808 + 2 + } + + /// Updates `self` with the current epoch, removing all attestations that become expired + /// relative to `Self::max_capacity`. + /// + /// Also sets `self.lowest_permissible_epoch` with relation to `current_epoch` and + /// `Self::max_capacity`. + pub fn prune(&self, current_epoch: Epoch) { + // Taking advantage of saturating subtraction on `Slot`. + let lowest_permissible_epoch = current_epoch - (self.max_capacity().saturating_sub(1)); + + *self.lowest_permissible_epoch.write() = lowest_permissible_epoch; + + self.items + .write() + .retain(|epoch, _item| *epoch >= lowest_permissible_epoch); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! test_suite { + ($mod_name: ident, $type: ident) => { + #[cfg(test)] + mod $mod_name { + use super::*; + use types::test_utils::test_random_instance; + + type E = types::MainnetEthSpec; + + fn get_attestation(epoch: Epoch) -> Attestation { + let mut a: Attestation = test_random_instance(); + a.data.target.epoch = epoch; + a + } + + fn single_epoch_test(store: &$type, epoch: Epoch) { + let attesters = [0, 1, 2, 3, 5, 6, 7, 18, 22]; + let a = &get_attestation(epoch); + + for &i in &attesters { + assert_eq!( + store.validator_has_been_observed(a, i), + Ok(false), + "should indicate an unknown attestation is unknown" + ); + assert_eq!( + store.observe_validator(a, i), + Ok(false), + "should observe new attestation" + ); + } + + for &i in &attesters { + assert_eq!( + store.validator_has_been_observed(a, i), + Ok(true), + "should indicate a known attestation is known" + ); + assert_eq!( + store.observe_validator(a, i), + Ok(true), + "should acknowledge an existing attestation" + ); + } + } + + #[test] + fn single_epoch() { + let store = $type::default(); + + single_epoch_test(&store, Epoch::new(0)); + + assert_eq!( + store.items.read().len(), + 1, + "should have a single bitfield stored" + ); + } + + #[test] + fn mulitple_contiguous_epochs() { + let store = $type::default(); + let max_cap = store.max_capacity(); + + for i in 0..max_cap * 3 { + let epoch = Epoch::new(i); + + single_epoch_test(&store, epoch); + + /* + * Ensure that the number of sets is correct. + */ + + if i < max_cap { + assert_eq!( + store.items.read().len(), + i as usize + 1, + "should have a {} items stored", + i + 1 + ); + } else { + assert_eq!( + store.items.read().len(), + max_cap as usize, + "should have max_capacity items stored" + ); + } + + /* + * Ensure that all the sets have the expected slots + */ + + let mut store_epochs = store + .items + .read() + .iter() + .map(|(epoch, _set)| *epoch) + .collect::>(); + + assert!( + store_epochs.len() <= store.max_capacity() as usize, + "store size should not exceed max" + ); + + store_epochs.sort_unstable(); + + let expected_epochs = (i.saturating_sub(max_cap - 1)..=i) + .map(Epoch::new) + .collect::>(); + + assert_eq!(expected_epochs, store_epochs, "should have expected slots"); + } + } + + #[test] + fn mulitple_non_contiguous_epochs() { + let store = $type::default(); + let max_cap = store.max_capacity(); + + let to_skip = vec![1_u64, 3, 4, 5]; + let epochs = (0..max_cap * 3) + .into_iter() + .filter(|i| !to_skip.contains(i)) + .collect::>(); + + for &i in &epochs { + if to_skip.contains(&i) { + continue; + } + + let epoch = Epoch::from(i); + + single_epoch_test(&store, epoch); + + /* + * Ensure that all the sets have the expected slots + */ + + let mut store_epochs = store + .items + .read() + .iter() + .map(|(epoch, _)| *epoch) + .collect::>(); + + store_epochs.sort_unstable(); + + assert!( + store_epochs.len() <= store.max_capacity() as usize, + "store size should not exceed max" + ); + + let lowest = store.lowest_permissible_epoch.read().as_u64(); + let highest = epoch.as_u64(); + let expected_epochs = (lowest..=highest) + .filter(|i| !to_skip.contains(i)) + .map(Epoch::new) + .collect::>(); + + assert_eq!( + expected_epochs, + &store_epochs[..], + "should have expected epochs" + ); + } + } + } + }; + } + + test_suite!(observed_attesters, ObservedAttesters); + test_suite!(observed_aggregators, ObservedAggregators); +} diff --git a/beacon_node/beacon_chain/src/observed_block_producers.rs b/beacon_node/beacon_chain/src/observed_block_producers.rs new file mode 100644 index 0000000000..b2d281adf9 --- /dev/null +++ b/beacon_node/beacon_chain/src/observed_block_producers.rs @@ -0,0 +1,428 @@ +//! Provides the `ObservedBlockProducers` struct which allows for rejecting gossip blocks from +//! validators that have already produced a block. + +use parking_lot::RwLock; +use std::collections::{HashMap, HashSet}; +use std::marker::PhantomData; +use types::{BeaconBlock, EthSpec, Slot, Unsigned}; + +#[derive(Debug, PartialEq)] +pub enum Error { + /// The slot of the provided block is prior to finalization and should not have been provided + /// to this function. This is an internal error. + FinalizedBlock { slot: Slot, finalized_slot: Slot }, + /// The function to obtain a set index failed, this is an internal error. + ValidatorIndexTooHigh(u64), +} + +/// Maintains a cache of observed `(block.slot, block.proposer)`. +/// +/// The cache supports pruning based upon the finalized epoch. It does not automatically prune, you +/// must call `Self::prune` manually. +/// +/// The maximum size of the cache is determined by `slots_since_finality * +/// VALIDATOR_REGISTRY_LIMIT`. This is quite a large size, so it's important that upstream +/// functions only use this cache for blocks with a valid signature. Only allowing valid signed +/// blocks reduces the theoretical maximum size of this cache to `slots_since_finality * +/// active_validator_count`, however in reality that is more like `slots_since_finality * +/// known_distinct_shufflings` which is much smaller. +pub struct ObservedBlockProducers { + finalized_slot: RwLock, + items: RwLock>>, + _phantom: PhantomData, +} + +impl Default for ObservedBlockProducers { + /// Instantiates `Self` with `finalized_slot == 0`. + fn default() -> Self { + Self { + finalized_slot: RwLock::new(Slot::new(0)), + items: RwLock::new(HashMap::new()), + _phantom: PhantomData, + } + } +} + +impl ObservedBlockProducers { + /// Observe that the `block` was produced by `block.proposer_index` at `block.slot`. This will + /// update `self` so future calls to it indicate that this block is known. + /// + /// The supplied `block` **MUST** be signature verified (see struct-level documentation). + /// + /// ## Errors + /// + /// - `block.proposer_index` is greater than `VALIDATOR_REGISTRY_LIMIT`. + /// - `block.slot` is equal to or less than the latest pruned `finalized_slot`. + pub fn observe_proposer(&self, block: &BeaconBlock) -> Result { + self.sanitize_block(block)?; + + let did_not_exist = self + .items + .write() + .entry(block.slot) + .or_insert_with(|| HashSet::with_capacity(E::SlotsPerEpoch::to_usize())) + .insert(block.proposer_index); + + Ok(!did_not_exist) + } + + /// Returns `Ok(true)` if the `block` has been observed before, `Ok(false)` if not. Does not + /// update the cache, so calling this function multiple times will continue to return + /// `Ok(false)`, until `Self::observe_proposer` is called. + /// + /// ## Errors + /// + /// - `block.proposer_index` is greater than `VALIDATOR_REGISTRY_LIMIT`. + /// - `block.slot` is equal to or less than the latest pruned `finalized_slot`. + pub fn proposer_has_been_observed(&self, block: &BeaconBlock) -> Result { + self.sanitize_block(block)?; + + let exists = self + .items + .read() + .get(&block.slot) + .map_or(false, |set| set.contains(&block.proposer_index)); + + Ok(exists) + } + + /// Returns `Ok(())` if the given `block` is sane. + fn sanitize_block(&self, block: &BeaconBlock) -> Result<(), Error> { + if block.proposer_index > E::ValidatorRegistryLimit::to_u64() { + return Err(Error::ValidatorIndexTooHigh(block.proposer_index)); + } + + let finalized_slot = *self.finalized_slot.read(); + if finalized_slot > 0 && block.slot <= finalized_slot { + return Err(Error::FinalizedBlock { + slot: block.slot, + finalized_slot, + }); + } + + Ok(()) + } + + /// Removes all observations of blocks equal to or earlier than `finalized_slot`. + /// + /// Stores `finalized_slot` in `self`, so that `self` will reject any block that has a slot + /// equal to or less than `finalized_slot`. + /// + /// No-op if `finalized_slot == 0`. + pub fn prune(&self, finalized_slot: Slot) { + if finalized_slot == 0 { + return; + } + + *self.finalized_slot.write() = finalized_slot; + self.items + .write() + .retain(|slot, _set| *slot > finalized_slot); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use types::MainnetEthSpec; + + type E = MainnetEthSpec; + + fn get_block(slot: u64, proposer: u64) -> BeaconBlock { + let mut block = BeaconBlock::empty(&E::default_spec()); + block.slot = slot.into(); + block.proposer_index = proposer; + block + } + + #[test] + fn pruning() { + let cache = ObservedBlockProducers::default(); + + assert_eq!(*cache.finalized_slot.read(), 0, "finalized slot is zero"); + assert_eq!(cache.items.read().len(), 0, "no slots should be present"); + + // Slot 0, proposer 0 + let block_a = &get_block(0, 0); + + assert_eq!( + cache.observe_proposer(block_a), + Ok(false), + "can observe proposer, indicates proposer unobserved" + ); + + /* + * Preconditions. + */ + + assert_eq!(*cache.finalized_slot.read(), 0, "finalized slot is zero"); + assert_eq!( + cache.items.read().len(), + 1, + "only one slot should be present" + ); + assert_eq!( + cache + .items + .read() + .get(&Slot::new(0)) + .expect("slot zero should be present") + .len(), + 1, + "only one proposer should be present" + ); + + /* + * Check that a prune at the genesis slot does nothing. + */ + + cache.prune(Slot::new(0)); + + assert_eq!(*cache.finalized_slot.read(), 0, "finalized slot is zero"); + assert_eq!( + cache.items.read().len(), + 1, + "only one slot should be present" + ); + assert_eq!( + cache + .items + .read() + .get(&Slot::new(0)) + .expect("slot zero should be present") + .len(), + 1, + "only one proposer should be present" + ); + + /* + * Check that a prune empties the cache + */ + + cache.prune(E::slots_per_epoch().into()); + assert_eq!( + *cache.finalized_slot.read(), + Slot::from(E::slots_per_epoch()), + "finalized slot is updated" + ); + assert_eq!(cache.items.read().len(), 0, "no items left"); + + /* + * Check that we can't insert a finalized block + */ + + // First slot of finalized epoch, proposer 0 + let block_b = &get_block(E::slots_per_epoch(), 0); + + assert_eq!( + cache.observe_proposer(block_b), + Err(Error::FinalizedBlock { + slot: E::slots_per_epoch().into(), + finalized_slot: E::slots_per_epoch().into(), + }), + "cant insert finalized block" + ); + + assert_eq!(cache.items.read().len(), 0, "block was not added"); + + /* + * Check that we _can_ insert a non-finalized block + */ + + let three_epochs = E::slots_per_epoch() * 3; + + // First slot of finalized epoch, proposer 0 + let block_b = &get_block(three_epochs, 0); + + assert_eq!( + cache.observe_proposer(block_b), + Ok(false), + "can insert non-finalized block" + ); + + assert_eq!( + cache.items.read().len(), + 1, + "only one slot should be present" + ); + assert_eq!( + cache + .items + .read() + .get(&Slot::new(three_epochs)) + .expect("the three epochs slot should be present") + .len(), + 1, + "only one proposer should be present" + ); + + /* + * Check that a prune doesnt wipe later blocks + */ + + let two_epochs = E::slots_per_epoch() * 2; + cache.prune(two_epochs.into()); + + assert_eq!( + *cache.finalized_slot.read(), + Slot::from(two_epochs), + "finalized slot is updated" + ); + + assert_eq!( + cache.items.read().len(), + 1, + "only one slot should be present" + ); + assert_eq!( + cache + .items + .read() + .get(&Slot::new(three_epochs)) + .expect("the three epochs slot should be present") + .len(), + 1, + "only one proposer should be present" + ); + } + + #[test] + fn simple_observations() { + let cache = ObservedBlockProducers::default(); + + // Slot 0, proposer 0 + let block_a = &get_block(0, 0); + + assert_eq!( + cache.proposer_has_been_observed(block_a), + Ok(false), + "no observation in empty cache" + ); + assert_eq!( + cache.observe_proposer(block_a), + Ok(false), + "can observe proposer, indicates proposer unobserved" + ); + assert_eq!( + cache.proposer_has_been_observed(block_a), + Ok(true), + "observed block is indicated as true" + ); + assert_eq!( + cache.observe_proposer(block_a), + Ok(true), + "observing again indicates true" + ); + + assert_eq!(*cache.finalized_slot.read(), 0, "finalized slot is zero"); + assert_eq!( + cache.items.read().len(), + 1, + "only one slot should be present" + ); + assert_eq!( + cache + .items + .read() + .get(&Slot::new(0)) + .expect("slot zero should be present") + .len(), + 1, + "only one proposer should be present" + ); + + // Slot 1, proposer 0 + let block_b = &get_block(1, 0); + + assert_eq!( + cache.proposer_has_been_observed(block_b), + Ok(false), + "no observation for new slot" + ); + assert_eq!( + cache.observe_proposer(block_b), + Ok(false), + "can observe proposer for new slot, indicates proposer unobserved" + ); + assert_eq!( + cache.proposer_has_been_observed(block_b), + Ok(true), + "observed block in slot 1 is indicated as true" + ); + assert_eq!( + cache.observe_proposer(block_b), + Ok(true), + "observing slot 1 again indicates true" + ); + + assert_eq!(*cache.finalized_slot.read(), 0, "finalized slot is zero"); + assert_eq!(cache.items.read().len(), 2, "two slots should be present"); + assert_eq!( + cache + .items + .read() + .get(&Slot::new(0)) + .expect("slot zero should be present") + .len(), + 1, + "only one proposer should be present in slot 0" + ); + assert_eq!( + cache + .items + .read() + .get(&Slot::new(1)) + .expect("slot zero should be present") + .len(), + 1, + "only one proposer should be present in slot 1" + ); + + // Slot 0, proposer 1 + let block_c = &get_block(0, 1); + + assert_eq!( + cache.proposer_has_been_observed(block_c), + Ok(false), + "no observation for new proposer" + ); + assert_eq!( + cache.observe_proposer(block_c), + Ok(false), + "can observe new proposer, indicates proposer unobserved" + ); + assert_eq!( + cache.proposer_has_been_observed(block_c), + Ok(true), + "observed new proposer block is indicated as true" + ); + assert_eq!( + cache.observe_proposer(block_c), + Ok(true), + "observing new proposer again indicates true" + ); + + assert_eq!(*cache.finalized_slot.read(), 0, "finalized slot is zero"); + assert_eq!(cache.items.read().len(), 2, "two slots should be present"); + assert_eq!( + cache + .items + .read() + .get(&Slot::new(0)) + .expect("slot zero should be present") + .len(), + 2, + "two proposers should be present in slot 0" + ); + assert_eq!( + cache + .items + .read() + .get(&Slot::new(1)) + .expect("slot zero should be present") + .len(), + 1, + "only one proposer should be present in slot 1" + ); + } +} diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 91f3a833c4..2940fa235b 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -7,7 +7,7 @@ use crate::{ builder::{BeaconChainBuilder, Witness}, eth1_chain::CachingEth1Backend, events::NullEventHandler, - AttestationProcessingOutcome, AttestationType, BeaconChain, BeaconChainTypes, StateSkipConfig, + BeaconChain, BeaconChainTypes, StateSkipConfig, }; use genesis::interop_genesis_state; use rayon::prelude::*; @@ -23,8 +23,8 @@ use tempfile::{tempdir, TempDir}; use tree_hash::TreeHash; use types::{ AggregateSignature, Attestation, BeaconState, BeaconStateHash, ChainSpec, Domain, EthSpec, - Hash256, Keypair, SecretKey, Signature, SignedBeaconBlock, SignedBeaconBlockHash, SignedRoot, - Slot, + Hash256, Keypair, SecretKey, SelectionProof, Signature, SignedAggregateAndProof, + SignedBeaconBlock, SignedBeaconBlockHash, SignedRoot, Slot, }; pub use types::test_utils::generate_deterministic_keypairs; @@ -85,8 +85,24 @@ pub struct BeaconChainHarness { impl BeaconChainHarness> { /// Instantiate a new harness with `validator_count` initial validators. pub fn new(eth_spec_instance: E, keypairs: Vec) -> Self { + // Setting the target aggregators to really high means that _all_ validators in the + // committee are required to produce an aggregate. This is overkill, however with small + // validator counts it's the only way to be certain there is _at least one_ aggregator per + // committee. + Self::new_with_target_aggregators(eth_spec_instance, keypairs, 1 << 32) + } + + /// Instantiate a new harness with `validator_count` initial validators and a custom + /// `target_aggregators_per_committee` spec value + pub fn new_with_target_aggregators( + eth_spec_instance: E, + keypairs: Vec, + target_aggregators_per_committee: u64, + ) -> Self { let data_dir = tempdir().expect("should create temporary data_dir"); - let spec = E::default_spec(); + let mut spec = E::default_spec(); + + spec.target_aggregators_per_committee = target_aggregators_per_committee; let log = NullLoggerBuilder.build().expect("logger should build"); @@ -268,9 +284,9 @@ where .expect("should not error during block processing"); self.chain.fork_choice().expect("should find head"); - head_block_root = Some(block_root); - self.add_free_attestations(&attestation_strategy, &new_state, block_root, slot); + + self.add_attestations_for_slot(&attestation_strategy, &new_state, block_root, slot); state = new_state; slot += 1; @@ -312,7 +328,7 @@ where self.chain.fork_choice().expect("should find head"); let attestation_strategy = AttestationStrategy::SomeValidators(validators.to_vec()); - self.add_free_attestations(&attestation_strategy, &new_state, block_root, slot); + self.add_attestations_for_slot(&attestation_strategy, &new_state, block_root, slot); (block_root.into(), new_state) } @@ -448,114 +464,183 @@ where (signed_block, state) } - /// Adds attestations to the `BeaconChain` operations pool and fork choice. + /// A list of attestations for each committee for the given slot. /// - /// The `attestation_strategy` dictates which validators should attest. - fn add_free_attestations( + /// The first layer of the Vec is organised per committee. For example, if the return value is + /// called `all_attestations`, then all attestations in `all_attestations[0]` will be for + /// committee 0, whilst all in `all_attestations[1]` will be for committee 1. + pub fn get_unaggregated_attestations( + &self, + attestation_strategy: &AttestationStrategy, + state: &BeaconState, + head_block_root: Hash256, + attestation_slot: Slot, + ) -> Vec>> { + let spec = &self.spec; + let fork = &state.fork; + + let attesting_validators = self.get_attesting_validators(attestation_strategy); + + state + .get_beacon_committees_at_slot(state.slot) + .expect("should get committees") + .iter() + .map(|bc| { + bc.committee + .par_iter() + .enumerate() + .filter_map(|(i, validator_index)| { + if !attesting_validators.contains(validator_index) { + return None; + } + let mut attestation = self + .chain + .produce_unaggregated_attestation_for_block( + attestation_slot, + bc.index, + head_block_root, + Cow::Borrowed(state), + ) + .expect("should produce attestation"); + + attestation + .aggregation_bits + .set(i, true) + .expect("should be able to set aggregation bits"); + + attestation.signature = { + let domain = spec.get_domain( + attestation.data.target.epoch, + Domain::BeaconAttester, + fork, + state.genesis_validators_root, + ); + + let message = attestation.data.signing_root(domain); + + let mut agg_sig = AggregateSignature::new(); + + agg_sig.add(&Signature::new( + message.as_bytes(), + self.get_sk(*validator_index), + )); + + agg_sig + }; + + Some(attestation) + }) + .collect() + }) + .collect() + } + + fn get_attesting_validators(&self, attestation_strategy: &AttestationStrategy) -> Vec { + match attestation_strategy { + AttestationStrategy::AllValidators => (0..self.keypairs.len()).collect(), + AttestationStrategy::SomeValidators(vec) => vec.clone(), + } + } + + /// Generates a `Vec` for some attestation strategy and head_block. + pub fn add_attestations_for_slot( &self, attestation_strategy: &AttestationStrategy, state: &BeaconState, head_block_root: Hash256, head_block_slot: Slot, ) { - self.get_free_attestations( + // These attestations will not be accepted by the chain so no need to generate them. + if state.slot + E::slots_per_epoch() < self.chain.slot().expect("should get slot") { + return; + } + + let spec = &self.spec; + let fork = &state.fork; + + let attesting_validators = self.get_attesting_validators(attestation_strategy); + + let unaggregated_attestations = self.get_unaggregated_attestations( attestation_strategy, state, head_block_root, head_block_slot, - ) - .into_iter() - .for_each(|attestation| { - match self - .chain - .process_attestation(attestation, AttestationType::Aggregated) - .expect("should not error during attestation processing") - { - // PastEpoch can occur if we fork over several epochs - AttestationProcessingOutcome::Processed - | AttestationProcessingOutcome::PastEpoch { .. } => (), - other => panic!("did not successfully process attestation: {:?}", other), - } - }); - } + ); - /// Generates a `Vec` for some attestation strategy and head_block. - pub fn get_free_attestations( - &self, - attestation_strategy: &AttestationStrategy, - state: &BeaconState, - head_block_root: Hash256, - head_block_slot: Slot, - ) -> Vec> { - let spec = &self.spec; - let fork = &state.fork; + // Loop through all unaggregated attestations, submit them to the chain and also submit a + // single aggregate. + unaggregated_attestations + .into_iter() + .for_each(|committee_attestations| { + // Submit each unaggregated attestation to the chain. + for attestation in &committee_attestations { + self.chain + .verify_unaggregated_attestation_for_gossip(attestation.clone()) + .expect("should not error during attestation processing") + .add_to_pool(&self.chain) + .expect("should add attestation to naive pool"); + } - let attesting_validators: Vec = match attestation_strategy { - AttestationStrategy::AllValidators => (0..self.keypairs.len()).collect(), - AttestationStrategy::SomeValidators(vec) => vec.clone(), - }; + // If there are any attestations in this committee, create an aggregate. + if let Some(attestation) = committee_attestations.first() { + let bc = state.get_beacon_committee(attestation.data.slot, attestation.data.index) + .expect("should get committee"); - let mut attestations = vec![]; + let aggregator_index = bc.committee + .iter() + .find(|&validator_index| { + if !attesting_validators.contains(validator_index) { + return false + } - state - .get_beacon_committees_at_slot(state.slot) - .expect("should get committees") - .iter() - .for_each(|bc| { - let mut local_attestations: Vec> = bc - .committee - .par_iter() - .enumerate() - .filter_map(|(i, validator_index)| { - // Note: searching this array is worst-case `O(n)`. A hashset could be a better - // alternative. - if attesting_validators.contains(validator_index) { - let mut attestation = self - .chain - .produce_attestation_for_block( - head_block_slot, - bc.index, - head_block_root, - Cow::Borrowed(state), - ) - .expect("should produce attestation"); + let selection_proof = SelectionProof::new::( + state.slot, + self.get_sk(*validator_index), + fork, + state.genesis_validators_root, + spec, + ); - attestation - .aggregation_bits - .set(i, true) - .expect("should be able to set aggregation bits"); + selection_proof.is_aggregator(bc.committee.len(), spec).unwrap_or(false) + }) + .copied() + .expect(&format!( + "Committee {} at slot {} with {} attesting validators does not have any aggregators", + bc.index, state.slot, bc.committee.len() + )); - attestation.signature = { - let domain = spec.get_domain( - attestation.data.target.epoch, - Domain::BeaconAttester, - fork, - state.genesis_validators_root, - ); + // If the chain is able to produce an aggregate, use that. Otherwise, build an + // aggregate locally. + let aggregate = self + .chain + .get_aggregated_attestation(&attestation.data) + .expect("should not error whilst finding aggregate") + .unwrap_or_else(|| { + committee_attestations.iter().skip(1).fold(attestation.clone(), |mut agg, att| { + agg.aggregate(att); + agg + }) + }); - let message = attestation.data.signing_root(domain); + let signed_aggregate = SignedAggregateAndProof::from_aggregate( + aggregator_index as u64, + aggregate, + None, + self.get_sk(aggregator_index), + fork, + state.genesis_validators_root, + spec, + ); - let mut agg_sig = AggregateSignature::new(); - - agg_sig.add(&Signature::new( - message.as_bytes(), - self.get_sk(*validator_index), - )); - - agg_sig - }; - - Some(attestation) - } else { - None - } - }) - .collect(); - - attestations.append(&mut local_attestations); + self.chain + .verify_aggregated_attestation_for_gossip(signed_aggregate) + .expect("should not error during attestation processing") + .add_to_pool(&self.chain) + .expect("should add attestation to naive aggregation pool") + .add_to_fork_choice(&self.chain) + .expect("should add attestation to fork choice"); + } }); - - attestations } /// Creates two forks: diff --git a/beacon_node/beacon_chain/src/validator_pubkey_cache.rs b/beacon_node/beacon_chain/src/validator_pubkey_cache.rs index 732f4085f4..111eb0e156 100644 --- a/beacon_node/beacon_chain/src/validator_pubkey_cache.rs +++ b/beacon_node/beacon_chain/src/validator_pubkey_cache.rs @@ -115,6 +115,11 @@ impl ValidatorPubkeyCache { pub fn get_index(&self, pubkey: &PublicKeyBytes) -> Option { self.indices.get(pubkey).copied() } + + /// Returns the number of validators in the cache. + pub fn len(&self) -> usize { + self.indices.len() + } } /// Allows for maintaining an on-disk copy of the `ValidatorPubkeyCache`. The file is raw SSZ bytes diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index 5a305e7ccc..1258c1ca63 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -90,7 +90,7 @@ fn produces_attestations() { .len(); let attestation = chain - .produce_attestation(slot, index) + .produce_unaggregated_attestation(slot, index) .expect("should produce attestation"); let data = &attestation.data; diff --git a/beacon_node/beacon_chain/tests/attestation_tests.rs b/beacon_node/beacon_chain/tests/attestation_tests.rs deleted file mode 100644 index 137746c7fc..0000000000 --- a/beacon_node/beacon_chain/tests/attestation_tests.rs +++ /dev/null @@ -1,268 +0,0 @@ -#![cfg(not(debug_assertions))] - -#[macro_use] -extern crate lazy_static; - -use beacon_chain::test_utils::{ - AttestationStrategy, BeaconChainHarness, BlockStrategy, HarnessType, -}; -use beacon_chain::{AttestationProcessingOutcome, AttestationType}; -use state_processing::per_slot_processing; -use types::{ - test_utils::generate_deterministic_keypair, AggregateSignature, BitList, EthSpec, Hash256, - Keypair, MainnetEthSpec, Signature, -}; - -pub const VALIDATOR_COUNT: usize = 128; - -lazy_static! { - /// A cached set of keys. - static ref KEYPAIRS: Vec = types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT); -} - -fn get_harness(validator_count: usize) -> BeaconChainHarness> { - let harness = BeaconChainHarness::new(MainnetEthSpec, KEYPAIRS[0..validator_count].to_vec()); - - harness.advance_slot(); - - harness -} - -#[test] -fn attestation_validity() { - let harness = get_harness(VALIDATOR_COUNT); - let chain = &harness.chain; - - // Extend the chain out a few epochs so we have some chain depth to play with. - harness.extend_chain( - MainnetEthSpec::slots_per_epoch() as usize * 3 + 1, - BlockStrategy::OnCanonicalHead, - AttestationStrategy::AllValidators, - ); - - let head = chain.head().expect("should get head"); - let current_slot = chain.slot().expect("should get slot"); - let current_epoch = chain.epoch().expect("should get epoch"); - - let valid_attestation = harness - .get_free_attestations( - &AttestationStrategy::AllValidators, - &head.beacon_state, - head.beacon_block_root, - head.beacon_block.slot(), - ) - .first() - .cloned() - .expect("should get at least one attestation"); - - assert_eq!( - chain.process_attestation(valid_attestation.clone(), AttestationType::Aggregated), - Ok(AttestationProcessingOutcome::Processed), - "should accept valid attestation" - ); - - /* - * Should reject attestations if the slot does not match the target epoch. - */ - - let mut epoch_mismatch_attestation = valid_attestation.clone(); - epoch_mismatch_attestation.data.target.epoch = current_epoch + 1; - - assert_eq!( - harness - .chain - .process_attestation(epoch_mismatch_attestation, AttestationType::Aggregated), - Ok(AttestationProcessingOutcome::BadTargetEpoch), - "should not accept attestation where the slot is not in the same epoch as the target" - ); - - /* - * Should reject attestations from future epochs. - */ - - let mut early_attestation = valid_attestation.clone(); - early_attestation.data.target.epoch = current_epoch + 1; - early_attestation.data.slot = (current_epoch + 1).start_slot(MainnetEthSpec::slots_per_epoch()); - - assert_eq!( - harness - .chain - .process_attestation(early_attestation, AttestationType::Aggregated), - Ok(AttestationProcessingOutcome::FutureEpoch { - attestation_epoch: current_epoch + 1, - current_epoch - }), - "should not accept early attestation" - ); - - /* - * Should reject attestations from epochs prior to the previous epoch. - */ - - let late_slot = (current_epoch - 2).start_slot(MainnetEthSpec::slots_per_epoch()); - let late_block = chain - .block_at_slot(late_slot) - .expect("should not error getting block at slot") - .expect("should find block at slot"); - let late_state = chain - .get_state(&late_block.state_root(), Some(late_slot)) - .expect("should not error getting state") - .expect("should find state"); - let late_attestation = harness - .get_free_attestations( - &AttestationStrategy::AllValidators, - &late_state, - late_block.canonical_root(), - late_slot, - ) - .first() - .cloned() - .expect("should get at least one late attestation"); - - assert_eq!( - harness - .chain - .process_attestation(late_attestation, AttestationType::Aggregated), - Ok(AttestationProcessingOutcome::PastEpoch { - attestation_epoch: current_epoch - 2, - current_epoch - }), - "should not accept late attestation" - ); - - /* - * Should reject attestations if the target is unknown. - */ - - let mut bad_target_attestation = valid_attestation.clone(); - bad_target_attestation.data.target.root = Hash256::from_low_u64_be(42); - - assert_eq!( - harness - .chain - .process_attestation(bad_target_attestation, AttestationType::Aggregated), - Ok(AttestationProcessingOutcome::UnknownTargetRoot( - Hash256::from_low_u64_be(42) - )), - "should not accept bad_target attestation" - ); - - /* - * Should reject attestations if the target is unknown. - */ - - let mut future_block_attestation = valid_attestation.clone(); - future_block_attestation.data.slot -= 1; - - assert_eq!( - harness - .chain - .process_attestation(future_block_attestation, AttestationType::Aggregated), - Ok(AttestationProcessingOutcome::AttestsToFutureBlock { - block: current_slot, - attestation: current_slot - 1 - }), - "should not accept future_block attestation" - ); - - /* - * Should reject attestations if the target is unknown. - */ - - let mut bad_head_attestation = valid_attestation.clone(); - bad_head_attestation.data.beacon_block_root = Hash256::from_low_u64_be(42); - - assert_eq!( - harness - .chain - .process_attestation(bad_head_attestation, AttestationType::Aggregated), - Ok(AttestationProcessingOutcome::UnknownHeadBlock { - beacon_block_root: Hash256::from_low_u64_be(42) - }), - "should not accept bad_head attestation" - ); - - /* - * Should reject attestations with a bad signature. - */ - - let mut bad_signature_attestation = valid_attestation.clone(); - let kp = generate_deterministic_keypair(0); - let mut agg_sig = AggregateSignature::new(); - agg_sig.add(&Signature::new(&[42, 42], &kp.sk)); - bad_signature_attestation.signature = agg_sig; - - assert_eq!( - harness - .chain - .process_attestation(bad_signature_attestation, AttestationType::Aggregated), - Ok(AttestationProcessingOutcome::InvalidSignature), - "should not accept bad_signature attestation" - ); - - /* - * Should reject attestations with an empty bitfield. - */ - - let mut empty_bitfield_attestation = valid_attestation.clone(); - empty_bitfield_attestation.aggregation_bits = - BitList::with_capacity(1).expect("should build bitfield"); - - assert_eq!( - harness - .chain - .process_attestation(empty_bitfield_attestation, AttestationType::Aggregated), - Ok(AttestationProcessingOutcome::EmptyAggregationBitfield), - "should not accept empty_bitfield attestation" - ); -} - -#[test] -fn attestation_that_skips_epochs() { - let harness = get_harness(VALIDATOR_COUNT); - let chain = &harness.chain; - - // Extend the chain out a few epochs so we have some chain depth to play with. - harness.extend_chain( - MainnetEthSpec::slots_per_epoch() as usize * 3 + 1, - BlockStrategy::OnCanonicalHead, - AttestationStrategy::AllValidators, - ); - - let current_slot = chain.slot().expect("should get slot"); - let current_epoch = chain.epoch().expect("should get epoch"); - - let earlier_slot = (current_epoch - 2).start_slot(MainnetEthSpec::slots_per_epoch()); - let earlier_block = chain - .block_at_slot(earlier_slot) - .expect("should not error getting block at slot") - .expect("should find block at slot"); - - let mut state = chain - .get_state(&earlier_block.state_root(), Some(earlier_slot)) - .expect("should not error getting state") - .expect("should find state"); - - while state.slot < current_slot { - per_slot_processing(&mut state, None, &harness.spec).expect("should process slot"); - } - - let attestation = harness - .get_free_attestations( - &AttestationStrategy::AllValidators, - &state, - earlier_block.canonical_root(), - current_slot, - ) - .first() - .cloned() - .expect("should get at least one attestation"); - - assert_eq!( - harness - .chain - .process_attestation(attestation, AttestationType::Aggregated), - Ok(AttestationProcessingOutcome::Processed), - "should process attestation that skips slots" - ); -} diff --git a/beacon_node/beacon_chain/tests/attestation_verification.rs b/beacon_node/beacon_chain/tests/attestation_verification.rs new file mode 100644 index 0000000000..8f4d2da167 --- /dev/null +++ b/beacon_node/beacon_chain/tests/attestation_verification.rs @@ -0,0 +1,990 @@ +#![cfg(not(debug_assertions))] + +#[macro_use] +extern crate lazy_static; + +use beacon_chain::{ + attestation_verification::Error as AttnError, + test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy, HarnessType}, + BeaconChain, BeaconChainTypes, +}; +use state_processing::per_slot_processing; +use store::Store; +use tree_hash::TreeHash; +use types::{ + test_utils::generate_deterministic_keypair, AggregateSignature, Attestation, EthSpec, Hash256, + Keypair, MainnetEthSpec, SecretKey, SelectionProof, Signature, SignedAggregateAndProof, + SignedBeaconBlock, Unsigned, +}; + +pub type E = MainnetEthSpec; + +/// The validator count needs to be relatively high compared to other tests to ensure that we can +/// have committees where _some_ validators are aggregators but not _all_. +pub const VALIDATOR_COUNT: usize = 256; + +lazy_static! { + /// A cached set of keys. + static ref KEYPAIRS: Vec = types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT); +} + +/// Returns a beacon chain harness. +fn get_harness(validator_count: usize) -> BeaconChainHarness> { + let harness = BeaconChainHarness::new_with_target_aggregators( + MainnetEthSpec, + KEYPAIRS[0..validator_count].to_vec(), + // A kind-of arbitrary number that ensures that _some_ validators are aggregators, but + // not all. + 4, + ); + + harness.advance_slot(); + + harness +} + +/// Returns an attestation that is valid for some slot in the given `chain`. +/// +/// Also returns some info about who created it. +fn get_valid_unaggregated_attestation( + chain: &BeaconChain, +) -> (Attestation, usize, usize, SecretKey) { + let head = chain.head().expect("should get head"); + let current_slot = chain.slot().expect("should get slot"); + + let mut valid_attestation = chain + .produce_unaggregated_attestation(current_slot, 0) + .expect("should not error while producing attestation"); + + let validator_committee_index = 0; + let validator_index = *head + .beacon_state + .get_beacon_committee(current_slot, valid_attestation.data.index) + .expect("should get committees") + .committee + .get(validator_committee_index) + .expect("there should be an attesting validator"); + + let validator_sk = generate_deterministic_keypair(validator_index).sk; + + valid_attestation + .sign( + &validator_sk, + validator_committee_index, + &head.beacon_state.fork, + chain.genesis_validators_root, + &chain.spec, + ) + .expect("should sign attestation"); + + ( + valid_attestation, + validator_index, + validator_committee_index, + validator_sk, + ) +} + +fn get_valid_aggregated_attestation( + chain: &BeaconChain, + aggregate: Attestation, +) -> (SignedAggregateAndProof, usize, SecretKey) { + let state = &chain.head().expect("should get head").beacon_state; + let current_slot = chain.slot().expect("should get slot"); + + let committee = state + .get_beacon_committee(current_slot, aggregate.data.index) + .expect("should get committees"); + let committee_len = committee.committee.len(); + + let (aggregator_index, aggregator_sk) = committee + .committee + .iter() + .find_map(|&val_index| { + let aggregator_sk = generate_deterministic_keypair(val_index).sk; + + let proof = SelectionProof::new::( + aggregate.data.slot, + &aggregator_sk, + &state.fork, + chain.genesis_validators_root, + &chain.spec, + ); + + if proof.is_aggregator(committee_len, &chain.spec).unwrap() { + Some((val_index, aggregator_sk)) + } else { + None + } + }) + .expect("should find aggregator for committee"); + + let signed_aggregate = SignedAggregateAndProof::from_aggregate( + aggregator_index as u64, + aggregate, + None, + &aggregator_sk, + &state.fork, + chain.genesis_validators_root, + &chain.spec, + ); + + (signed_aggregate, aggregator_index, aggregator_sk) +} + +/// Returns a proof and index for a validator that is **not** an aggregator for the given +/// attestation. +fn get_non_aggregator( + chain: &BeaconChain, + aggregate: &Attestation, +) -> (usize, SecretKey) { + let state = &chain.head().expect("should get head").beacon_state; + let current_slot = chain.slot().expect("should get slot"); + + let committee = state + .get_beacon_committee(current_slot, aggregate.data.index) + .expect("should get committees"); + let committee_len = committee.committee.len(); + + committee + .committee + .iter() + .find_map(|&val_index| { + let aggregator_sk = generate_deterministic_keypair(val_index).sk; + + let proof = SelectionProof::new::( + aggregate.data.slot, + &aggregator_sk, + &state.fork, + chain.genesis_validators_root, + &chain.spec, + ); + + if proof.is_aggregator(committee_len, &chain.spec).unwrap() { + None + } else { + Some((val_index, aggregator_sk)) + } + }) + .expect("should find non-aggregator for committee") +} + +/// Tests verification of `SignedAggregateAndProof` from the gossip network. +#[test] +fn aggregated_gossip_verification() { + let harness = get_harness(VALIDATOR_COUNT); + let chain = &harness.chain; + + // Extend the chain out a few epochs so we have some chain depth to play with. + harness.extend_chain( + MainnetEthSpec::slots_per_epoch() as usize * 3 - 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ); + + // Advance into a slot where there have not been blocks or attestations produced. + harness.advance_slot(); + + let current_slot = chain.slot().expect("should get slot"); + + assert_eq!( + current_slot % E::slots_per_epoch(), + 0, + "the test requires a new epoch to avoid already-seen errors" + ); + + let (valid_attestation, _attester_index, _attester_committee_index, validator_sk) = + get_valid_unaggregated_attestation(&harness.chain); + let (valid_aggregate, aggregator_index, aggregator_sk) = + get_valid_aggregated_attestation(&harness.chain, valid_attestation); + + macro_rules! assert_invalid { + ($desc: tt, $attn_getter: expr, $error: expr) => { + assert_eq!( + harness + .chain + .verify_aggregated_attestation_for_gossip($attn_getter) + .err() + .expect(&format!( + "{} should error during verify_aggregated_attestation_for_gossip", + $desc + )), + $error, + "case: {}", + $desc, + ); + }; + } + + /* + * The following two tests ensure: + * + * Spec v0.11.2 + * + * aggregate.data.slot is within the last ATTESTATION_PROPAGATION_SLOT_RANGE slots (with a + * MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) -- i.e. aggregate.data.slot + + * ATTESTATION_PROPAGATION_SLOT_RANGE >= current_slot >= aggregate.data.slot (a client MAY + * queue future aggregates for processing at the appropriate slot). + */ + + let future_slot = current_slot + 1; + assert_invalid!( + "aggregate from future slot", + { + let mut a = valid_aggregate.clone(); + a.message.aggregate.data.slot = future_slot; + a + }, + AttnError::FutureSlot { + attestation_slot: future_slot, + latest_permissible_slot: current_slot, + } + ); + + let early_slot = current_slot + .as_u64() + .checked_sub(E::slots_per_epoch() + 2) + .expect("chain is not sufficiently deep for test") + .into(); + assert_invalid!( + "aggregate from past slot", + { + let mut a = valid_aggregate.clone(); + a.message.aggregate.data.slot = early_slot; + a + }, + AttnError::PastSlot { + attestation_slot: early_slot, + // Subtract an additional slot since the harness will be exactly on the start of the + // slot and the propagation tolerance will allow an extra slot. + earliest_permissible_slot: current_slot - E::slots_per_epoch() - 1, + } + ); + + /* + * The following test ensures: + * + * Spec v0.11.2 + * + * The block being voted for (aggregate.data.beacon_block_root) passes validation. + */ + + let unknown_root = Hash256::from_low_u64_le(424242); + assert_invalid!( + "aggregate with unknown head block", + { + let mut a = valid_aggregate.clone(); + a.message.aggregate.data.beacon_block_root = unknown_root; + a + }, + AttnError::UnknownHeadBlock { + beacon_block_root: unknown_root + } + ); + + /* + * This test ensures: + * + * Spec v0.11.2 + * + * The aggregator signature, signed_aggregate_and_proof.signature, is valid. + */ + + assert_invalid!( + "aggregate with bad signature", + { + let mut a = valid_aggregate.clone(); + + a.signature = Signature::new(&[42, 42], &validator_sk); + + a + }, + AttnError::InvalidSignature + ); + + /* + * The following test ensures: + * + * Spec v0.11.2 + * + * The aggregate_and_proof.selection_proof is a valid signature of the aggregate.data.slot by + * the validator with index aggregate_and_proof.aggregator_index. + */ + + let committee_len = harness + .chain + .head() + .unwrap() + .beacon_state + .get_beacon_committee( + harness.chain.slot().unwrap(), + valid_aggregate.message.aggregate.data.index, + ) + .expect("should get committees") + .committee + .len(); + assert_invalid!( + "aggregate with bad selection proof signature", + { + let mut a = valid_aggregate.clone(); + + // Generate some random signature until happens to be a valid selection proof. We need + // this in order to reach the signature verification code. + // + // Could run for ever, but that seems _really_ improbable. + let mut i: u64 = 0; + a.message.selection_proof = loop { + i += 1; + let proof: SelectionProof = Signature::new(&i.to_le_bytes(), &validator_sk).into(); + if proof + .is_aggregator(committee_len, &harness.chain.spec) + .unwrap() + { + break proof.into(); + } + }; + + a + }, + AttnError::InvalidSignature + ); + + /* + * The following test ensures: + * + * Spec v0.11.2 + * + * The signature of aggregate is valid. + */ + + assert_invalid!( + "aggregate with bad aggregate signature", + { + let mut a = valid_aggregate.clone(); + + let mut agg_sig = AggregateSignature::new(); + agg_sig.add(&Signature::new(&[42, 42], &aggregator_sk)); + a.message.aggregate.signature = agg_sig; + + a + }, + AttnError::InvalidSignature + ); + + let too_high_index = ::ValidatorRegistryLimit::to_u64() + 1; + assert_invalid!( + "aggregate with too-high aggregator index", + { + let mut a = valid_aggregate.clone(); + a.message.aggregator_index = too_high_index; + a + }, + AttnError::ValidatorIndexTooHigh(too_high_index as usize) + ); + + /* + * The following test ensures: + * + * Spec v0.11.2 + * + * The aggregator's validator index is within the aggregate's committee -- i.e. + * aggregate_and_proof.aggregator_index in get_attesting_indices(state, aggregate.data, + * aggregate.aggregation_bits). + */ + + let unknown_validator = VALIDATOR_COUNT as u64; + assert_invalid!( + "aggregate with unknown aggregator index", + { + let mut a = valid_aggregate.clone(); + a.message.aggregator_index = unknown_validator; + a + }, + // Naively we should think this condition would trigger this error: + // + // AttnError::AggregatorPubkeyUnknown(unknown_validator) + // + // However the following error is triggered first: + AttnError::AggregatorNotInCommittee { + aggregator_index: unknown_validator + } + ); + + /* + * The following test ensures: + * + * Spec v0.11.2 + * + * aggregate_and_proof.selection_proof selects the validator as an aggregator for the slot -- + * i.e. is_aggregator(state, aggregate.data.slot, aggregate.data.index, + * aggregate_and_proof.selection_proof) returns True. + */ + + let (non_aggregator_index, non_aggregator_sk) = + get_non_aggregator(&harness.chain, &valid_aggregate.message.aggregate); + assert_invalid!( + "aggregate with from non-aggregator", + { + SignedAggregateAndProof::from_aggregate( + non_aggregator_index as u64, + valid_aggregate.message.aggregate.clone(), + None, + &non_aggregator_sk, + &harness.chain.head_info().unwrap().fork, + harness.chain.genesis_validators_root, + &harness.chain.spec, + ) + }, + AttnError::InvalidSelectionProof { + aggregator_index: non_aggregator_index as u64 + } + ); + + assert!( + harness + .chain + .verify_aggregated_attestation_for_gossip(valid_aggregate.clone()) + .is_ok(), + "valid aggregate should be verified" + ); + + /* + * The following tests ensures: + * + * NOTE: this is a slight deviation from the spec, see: + * https://github.com/ethereum/eth2.0-specs/pull/1749 + * + * Spec v0.11.2 + * + * The aggregate attestation defined by hash_tree_root(aggregate) has not already been seen + * (via aggregate gossip, within a block, or through the creation of an equivalent aggregate + * locally). + */ + + assert_invalid!( + "aggregate with that has already been seen", + valid_aggregate.clone(), + AttnError::AttestationAlreadyKnown(valid_aggregate.message.aggregate.tree_hash_root()) + ); + + /* + * The following test ensures: + * + * Spec v0.11.2 + * + * The aggregate is the first valid aggregate received for the aggregator with index + * aggregate_and_proof.aggregator_index for the epoch aggregate.data.target.epoch. + */ + + assert_invalid!( + "aggregate from aggregator that has already been seen", + { + let mut a = valid_aggregate.clone(); + a.message.aggregate.data.beacon_block_root = Hash256::from_low_u64_le(42); + a + }, + AttnError::AggregatorAlreadyKnown(aggregator_index as u64) + ); +} + +/// Tests the verification conditions for an unaggregated attestation on the gossip network. +#[test] +fn unaggregated_gossip_verification() { + let harness = get_harness(VALIDATOR_COUNT); + let chain = &harness.chain; + + // Extend the chain out a few epochs so we have some chain depth to play with. + harness.extend_chain( + MainnetEthSpec::slots_per_epoch() as usize * 3 - 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ); + + // Advance into a slot where there have not been blocks or attestations produced. + harness.advance_slot(); + + let current_slot = chain.slot().expect("should get slot"); + let current_epoch = chain.epoch().expect("should get epoch"); + + assert_eq!( + current_slot % E::slots_per_epoch(), + 0, + "the test requires a new epoch to avoid already-seen errors" + ); + + let (valid_attestation, validator_index, validator_committee_index, validator_sk) = + get_valid_unaggregated_attestation(&harness.chain); + + macro_rules! assert_invalid { + ($desc: tt, $attn_getter: expr, $error: expr) => { + assert_eq!( + harness + .chain + .verify_unaggregated_attestation_for_gossip($attn_getter) + .err() + .expect(&format!( + "{} should error during verify_unaggregated_attestation_for_gossip", + $desc + )), + $error, + "case: {}", + $desc, + ); + }; + } + + /* + * The following two tests ensure: + * + * Spec v0.11.2 + * + * attestation.data.slot is within the last ATTESTATION_PROPAGATION_SLOT_RANGE slots (within a + * MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) -- i.e. attestation.data.slot + + * ATTESTATION_PROPAGATION_SLOT_RANGE >= current_slot >= attestation.data.slot (a client MAY + * queue future attestations for processing at the appropriate slot). + */ + + let future_slot = current_slot + 1; + assert_invalid!( + "attestation from future slot", + { + let mut a = valid_attestation.clone(); + a.data.slot = future_slot; + a + }, + AttnError::FutureSlot { + attestation_slot: future_slot, + latest_permissible_slot: current_slot, + } + ); + + let early_slot = current_slot + .as_u64() + .checked_sub(E::slots_per_epoch() + 2) + .expect("chain is not sufficiently deep for test") + .into(); + assert_invalid!( + "attestation from past slot", + { + let mut a = valid_attestation.clone(); + a.data.slot = early_slot; + a + }, + AttnError::PastSlot { + attestation_slot: early_slot, + // Subtract an additional slot since the harness will be exactly on the start of the + // slot and the propagation tolerance will allow an extra slot. + earliest_permissible_slot: current_slot - E::slots_per_epoch() - 1, + } + ); + + /* + * The following two tests ensure: + * + * Spec v0.11.2 + * + * The attestation is unaggregated -- that is, it has exactly one participating validator + * (len([bit for bit in attestation.aggregation_bits if bit == 0b1]) == 1). + */ + + assert_invalid!( + "attestation without any aggregation bits set", + { + let mut a = valid_attestation.clone(); + a.aggregation_bits + .set(validator_committee_index, false) + .expect("should unset aggregation bit"); + assert_eq!( + a.aggregation_bits.num_set_bits(), + 0, + "test requires no set bits" + ); + a + }, + AttnError::NotExactlyOneAggregationBitSet(0) + ); + + assert_invalid!( + "attestation with two aggregation bits set", + { + let mut a = valid_attestation.clone(); + a.aggregation_bits + .set(validator_committee_index + 1, true) + .expect("should set second aggregation bit"); + a + }, + AttnError::NotExactlyOneAggregationBitSet(2) + ); + + /* + * The following test ensures that: + * + * Spec v0.11.2 + * + * The block being voted for (attestation.data.beacon_block_root) passes validation. + */ + + let unknown_root = Hash256::from_low_u64_le(424242); // No one wants one of these + assert_invalid!( + "attestation with unknown head block", + { + let mut a = valid_attestation.clone(); + a.data.beacon_block_root = unknown_root; + a + }, + AttnError::UnknownHeadBlock { + beacon_block_root: unknown_root + } + ); + + /* + * The following test ensures that: + * + * Spec v0.11.2 + * + * The signature of attestation is valid. + */ + + assert_invalid!( + "attestation with bad signature", + { + let mut a = valid_attestation.clone(); + + let mut agg_sig = AggregateSignature::new(); + agg_sig.add(&Signature::new(&[42, 42], &validator_sk)); + a.signature = agg_sig; + + a + }, + AttnError::InvalidSignature + ); + + assert!( + harness + .chain + .verify_unaggregated_attestation_for_gossip(valid_attestation.clone()) + .is_ok(), + "valid attestation should be verified" + ); + + /* + * The following test ensures that: + * + * Spec v0.11.2 + * + * + * There has been no other valid attestation seen on an attestation subnet that has an + * identical attestation.data.target.epoch and participating validator index. + */ + + assert_invalid!( + "attestation that has already been seen", + valid_attestation.clone(), + AttnError::PriorAttestationKnown { + validator_index: validator_index as u64, + epoch: current_epoch + } + ); +} + +/// Tests the verification conditions for an unaggregated attestation on the gossip network. +#[test] +fn fork_choice_verification() { + let harness = get_harness(VALIDATOR_COUNT); + let chain = &harness.chain; + + // Extend the chain out a few epochs so we have some chain depth to play with. + harness.extend_chain( + MainnetEthSpec::slots_per_epoch() as usize * 3 - 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ); + + // Advance into a slot where there have not been blocks or attestations produced. + harness.advance_slot(); + + // We're going to produce the attestations at the first slot of the epoch. + let (valid_attestation, _validator_index, _validator_committee_index, _validator_sk) = + get_valid_unaggregated_attestation(&harness.chain); + + // Extend the chain two more blocks, but without any attestations so we don't trigger the + // "already seen" caches. + // + // Because of this, the attestation we're dealing with was made one slot prior to the current + // slot. This allows us to test the `AttestsToFutureBlock` condition. + harness.extend_chain( + 2, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::SomeValidators(vec![]), + ); + + let current_slot = chain.slot().expect("should get slot"); + let current_epoch = chain.epoch().expect("should get epoch"); + + let attestation = harness + .chain + .verify_unaggregated_attestation_for_gossip(valid_attestation.clone()) + .expect("precondition: should gossip verify attestation"); + + macro_rules! assert_invalid { + ($desc: tt, $attn_getter: expr, $error: expr) => { + assert_eq!( + harness + .chain + .apply_attestation_to_fork_choice(&$attn_getter) + .err() + .expect(&format!( + "{} should error during apply_attestation_to_fork_choice", + $desc + )), + $error, + "case: {}", + $desc, + ); + }; + } + + assert_invalid!( + "attestation without any aggregation bits set", + { + let mut a = attestation.clone(); + a.__indexed_attestation_mut().attesting_indices = vec![].into(); + a + }, + AttnError::EmptyAggregationBitfield + ); + + /* + * The following two tests ensure that: + * + * Spec v0.11.2 + * + * assert target.epoch in [current_epoch, previous_epoch] + */ + + let future_epoch = current_epoch + 1; + assert_invalid!( + "attestation from future epoch", + { + let mut a = attestation.clone(); + a.__indexed_attestation_mut().data.target.epoch = future_epoch; + a + }, + AttnError::FutureEpoch { + attestation_epoch: future_epoch, + current_epoch + } + ); + + assert!( + current_epoch > 1, + "precondition: must be able to have a past epoch" + ); + + let past_epoch = current_epoch - 2; + assert_invalid!( + "attestation from past epoch", + { + let mut a = attestation.clone(); + a.__indexed_attestation_mut().data.target.epoch = past_epoch; + a + }, + AttnError::PastEpoch { + attestation_epoch: past_epoch, + current_epoch + } + ); + + /* + * This test ensures that: + * + * Spec v0.11.2 + * + * assert target.epoch == compute_epoch_at_slot(attestation.data.slot) + */ + + assert_invalid!( + "attestation with bad target epoch", + { + let mut a = attestation.clone(); + + let indexed = a.__indexed_attestation_mut(); + indexed.data.target.epoch = indexed.data.slot.epoch(E::slots_per_epoch()) - 1; + a + }, + AttnError::BadTargetEpoch + ); + + /* + * This test ensures that: + * + * Spec v0.11.2 + * + * Attestations target be for a known block. If target block is unknown, delay consideration + * until the block is found + * + * assert target.root in store.blocks + */ + + let unknown_root = Hash256::from_low_u64_le(42); + assert_invalid!( + "attestation with unknown target root", + { + let mut a = attestation.clone(); + + let indexed = a.__indexed_attestation_mut(); + indexed.data.target.root = unknown_root; + a + }, + AttnError::UnknownTargetRoot(unknown_root) + ); + + // NOTE: we're not testing an assert from the spec: + // + // `assert get_current_slot(store) >= compute_start_slot_at_epoch(target.epoch)` + // + // I think this check is redundant and I've raised an issue here: + // + // https://github.com/ethereum/eth2.0-specs/pull/1755 + + /* + * This test asserts that: + * + * Spec v0.11.2 + * + * # Attestations must be for a known block. If block is unknown, delay consideration until the + * block is found + * + * assert attestation.data.beacon_block_root in store.blocks + */ + + assert_invalid!( + "attestation with unknown beacon block root", + { + let mut a = attestation.clone(); + + let indexed = a.__indexed_attestation_mut(); + indexed.data.beacon_block_root = unknown_root; + a + }, + AttnError::UnknownHeadBlock { + beacon_block_root: unknown_root + } + ); + + let future_block = harness + .chain + .block_at_slot(current_slot) + .expect("should not error getting block") + .expect("should find block at current slot"); + assert_invalid!( + "attestation to future block", + { + let mut a = attestation.clone(); + + let indexed = a.__indexed_attestation_mut(); + + assert!( + future_block.slot() > indexed.data.slot, + "precondition: the attestation must attest to the future" + ); + + indexed.data.beacon_block_root = future_block.canonical_root(); + a + }, + AttnError::AttestsToFutureBlock { + block: current_slot, + attestation: current_slot - 1 + } + ); + + // Note: we're not checking the "attestations can only affect the fork choice of subsequent + // slots" part of the spec, we do this upstream. + + assert!( + harness + .chain + .apply_attestation_to_fork_choice(&attestation.clone()) + .is_ok(), + "should verify valid attestation" + ); + + // There's nothing stopping fork choice from accepting the same attestation twice. + assert!( + harness + .chain + .apply_attestation_to_fork_choice(&attestation) + .is_ok(), + "should verify valid attestation a second time" + ); +} + +/// Ensures that an attestation that skips epochs can still be processed. +/// +/// This also checks that we can do a state lookup if we don't get a hit from the shuffling cache. +#[test] +fn attestation_that_skips_epochs() { + let harness = get_harness(VALIDATOR_COUNT); + let chain = &harness.chain; + + // Extend the chain out a few epochs so we have some chain depth to play with. + harness.extend_chain( + MainnetEthSpec::slots_per_epoch() as usize * 3 + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ); + + let current_slot = chain.slot().expect("should get slot"); + let current_epoch = chain.epoch().expect("should get epoch"); + + let earlier_slot = (current_epoch - 2).start_slot(MainnetEthSpec::slots_per_epoch()); + let earlier_block = chain + .block_at_slot(earlier_slot) + .expect("should not error getting block at slot") + .expect("should find block at slot"); + + let mut state = chain + .get_state(&earlier_block.state_root(), Some(earlier_slot)) + .expect("should not error getting state") + .expect("should find state"); + + while state.slot < current_slot { + per_slot_processing(&mut state, None, &harness.spec).expect("should process slot"); + } + + let attestation = harness + .get_unaggregated_attestations( + &AttestationStrategy::AllValidators, + &state, + earlier_block.canonical_root(), + current_slot, + ) + .first() + .expect("should have at least one committee") + .first() + .cloned() + .expect("should have at least one attestation in committee"); + + let block_root = attestation.data.beacon_block_root; + let block_slot = harness + .chain + .store + .get::>(&block_root) + .expect("should not error getting block") + .expect("should find attestation block") + .message + .slot; + + assert!( + attestation.data.slot - block_slot > E::slots_per_epoch() * 2, + "the attestation must skip more than two epochs" + ); + + assert!( + harness + .chain + .verify_unaggregated_attestation_for_gossip(attestation) + .is_ok(), + "should gossip verify attestation that skips slots" + ); +} diff --git a/beacon_node/beacon_chain/tests/import_chain_segment_tests.rs b/beacon_node/beacon_chain/tests/block_verification.rs similarity index 81% rename from beacon_node/beacon_chain/tests/import_chain_segment_tests.rs rename to beacon_node/beacon_chain/tests/block_verification.rs index bbefe5be32..0aac2e4d4b 100644 --- a/beacon_node/beacon_chain/tests/import_chain_segment_tests.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -314,16 +314,12 @@ fn invalid_signatures() { item ); - let gossip_verified = harness - .chain - .verify_block_for_gossip(snapshots[block_index].beacon_block.clone()) - .expect("should obtain gossip verified block"); - assert_eq!( - harness.chain.process_block(gossip_verified), - Err(BlockError::InvalidSignature), - "should not import gossip verified block with an invalid {} signature", - item - ); + // NOTE: we choose not to check gossip verification here. It only checks one signature + // (proposal) and that is already tested elsewhere in this file. + // + // It's not trivial to just check gossip verification since it will start refusing + // blocks as soon as it has seen one valid proposal signature for a given (validator, + // slot) tuple. }; /* @@ -513,7 +509,7 @@ fn unwrap_err(result: Result) -> E { } #[test] -fn gossip_verification() { +fn block_gossip_verification() { let harness = get_harness(VALIDATOR_COUNT); let block_index = CHAIN_SEGMENT_LENGTH - 2; @@ -537,19 +533,13 @@ fn gossip_verification() { } /* - * Block with invalid signature - */ - - let mut block = CHAIN_SEGMENT[block_index].beacon_block.clone(); - block.signature = junk_signature(); - assert_eq!( - unwrap_err(harness.chain.verify_block_for_gossip(block)), - BlockError::ProposalSignatureInvalid, - "should not import a block with an invalid proposal signature" - ); - - /* - * Block from a future slot. + * This test ensures that: + * + * Spec v0.11.2 + * + * The block is not from a future slot (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) -- + * i.e. validate that signed_beacon_block.message.slot <= current_slot (a client MAY queue + * future blocks for processing at the appropriate slot). */ let mut block = CHAIN_SEGMENT[block_index].beacon_block.clone(); @@ -565,7 +555,15 @@ fn gossip_verification() { ); /* - * Block from a finalized slot. + * This test ensure that: + * + * Spec v0.11.2 + * + * The block is from a slot greater than the latest finalized slot -- i.e. validate that + * signed_beacon_block.message.slot > + * compute_start_slot_at_epoch(state.finalized_checkpoint.epoch) (a client MAY choose to + * validate and store such blocks for additional purposes -- e.g. slashing detection, archive + * nodes, etc). */ let mut block = CHAIN_SEGMENT[block_index].beacon_block.clone(); @@ -585,4 +583,92 @@ fn gossip_verification() { }, "should not import a block with a finalized slot" ); + + /* + * This test ensures that: + * + * Spec v0.11.2 + * + * The proposer signature, signed_beacon_block.signature, is valid with respect to the + * proposer_index pubkey. + */ + + let mut block = CHAIN_SEGMENT[block_index].beacon_block.clone(); + block.signature = junk_signature(); + assert_eq!( + unwrap_err(harness.chain.verify_block_for_gossip(block)), + BlockError::ProposalSignatureInvalid, + "should not import a block with an invalid proposal signature" + ); + + /* + * This test ensures that: + * + * Spec v0.11.2 + * + * The block is proposed by the expected proposer_index for the block's slot in the context of + * the current shuffling (defined by parent_root/slot). If the proposer_index cannot + * immediately be verified against the expected shuffling, the block MAY be queued for later + * processing while proposers for the block's branch are calculated. + */ + + let mut block = CHAIN_SEGMENT[block_index].beacon_block.clone(); + let expected_proposer = block.message.proposer_index; + let other_proposer = (0..VALIDATOR_COUNT as u64) + .into_iter() + .find(|i| *i != block.message.proposer_index) + .expect("there must be more than one validator in this test"); + block.message.proposer_index = other_proposer; + let block = block.message.clone().sign( + &generate_deterministic_keypair(other_proposer as usize).sk, + &harness.chain.head_info().unwrap().fork, + harness.chain.genesis_validators_root, + &harness.chain.spec, + ); + assert_eq!( + unwrap_err(harness.chain.verify_block_for_gossip(block.clone())), + BlockError::IncorrectBlockProposer { + block: other_proposer, + local_shuffling: expected_proposer + }, + "should not import a block with the wrong proposer index" + ); + // Check to ensure that we registered this is a valid block from this proposer. + assert_eq!( + unwrap_err(harness.chain.verify_block_for_gossip(block.clone())), + BlockError::RepeatProposal { + proposer: other_proposer, + slot: block.message.slot + }, + "should register any valid signature against the proposer, even if the block failed later verification" + ); + + let block = CHAIN_SEGMENT[block_index].beacon_block.clone(); + assert!( + harness.chain.verify_block_for_gossip(block).is_ok(), + "the valid block should be processed" + ); + + /* + * This test ensures that: + * + * Spec v0.11.2 + * + * The block is the first block with valid signature received for the proposer for the slot, + * signed_beacon_block.message.slot. + */ + + let block = CHAIN_SEGMENT[block_index].beacon_block.clone(); + assert_eq!( + harness + .chain + .verify_block_for_gossip(block.clone()) + .err() + .expect("should error when processing known block"), + BlockError::RepeatProposal { + proposer: block.message.proposer_index, + slot: block.message.slot, + }, + "the second proposal by this validator should be rejected" + ); } diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index c5a855fc06..1e6f9e1ea5 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -1,13 +1,14 @@ -#![cfg(not(debug_assertions))] +// #![cfg(not(debug_assertions))] #[macro_use] extern crate lazy_static; +use beacon_chain::attestation_verification::Error as AttnError; use beacon_chain::test_utils::{ AttestationStrategy, BeaconChainHarness, BlockStrategy, DiskHarnessType, }; use beacon_chain::BeaconSnapshot; -use beacon_chain::{AttestationProcessingOutcome, AttestationType, StateSkipConfig}; +use beacon_chain::StateSkipConfig; use rand::Rng; use sloggers::{null::NullLoggerBuilder, Build}; use std::collections::HashMap; @@ -272,7 +273,7 @@ fn epoch_boundary_state_attestation_processing() { ); let head = harness.chain.head().expect("head ok"); - late_attestations.extend(harness.get_free_attestations( + late_attestations.extend(harness.get_unaggregated_attestations( &AttestationStrategy::SomeValidators(late_validators.clone()), &head.beacon_state, head.beacon_block_root, @@ -289,7 +290,7 @@ fn epoch_boundary_state_attestation_processing() { let mut checked_pre_fin = false; - for attestation in late_attestations { + for attestation in late_attestations.into_iter().flatten() { // load_epoch_boundary_state is idempotent! let block_root = attestation.data.beacon_block_root; let block = store.get_block(&block_root).unwrap().expect("block exists"); @@ -310,26 +311,29 @@ fn epoch_boundary_state_attestation_processing() { .expect("head ok") .finalized_checkpoint .epoch; + let res = harness .chain - .process_attestation_internal(attestation.clone(), AttestationType::Aggregated); + .verify_unaggregated_attestation_for_gossip(attestation.clone()); - let current_epoch = harness.chain.epoch().expect("should get epoch"); - let attestation_epoch = attestation.data.target.epoch; + let current_slot = harness.chain.slot().expect("should get slot"); + let attestation_slot = attestation.data.slot; + // Extra -1 to handle gossip clock disparity. + let earliest_permissible_slot = current_slot - E::slots_per_epoch() - 1; - if attestation.data.slot <= finalized_epoch.start_slot(E::slots_per_epoch()) - || attestation_epoch + 1 < current_epoch + if attestation_slot <= finalized_epoch.start_slot(E::slots_per_epoch()) + || attestation_slot < earliest_permissible_slot { checked_pre_fin = true; assert_eq!( - res, - Ok(AttestationProcessingOutcome::PastEpoch { - attestation_epoch, - current_epoch, - }) + res.err().unwrap(), + AttnError::PastSlot { + attestation_slot, + earliest_permissible_slot, + } ); } else { - assert_eq!(res, Ok(AttestationProcessingOutcome::Processed)); + res.expect("should have verified attetation"); } } assert!(checked_pre_fin); @@ -1069,9 +1073,20 @@ fn prunes_fork_running_past_finalized_checkpoint() { let (canonical_blocks_second_epoch, _, _, _, _) = harness.add_canonical_chain_blocks( canonical_state, canonical_slot, - slots_per_epoch * 4, + slots_per_epoch * 6, &honest_validators, ); + assert_ne!( + harness + .chain + .head() + .unwrap() + .beacon_state + .finalized_checkpoint + .epoch, + 0, + "chain should have finalized" + ); // Postconditions let canonical_blocks: HashMap = canonical_blocks_zeroth_epoch @@ -1087,8 +1102,8 @@ fn prunes_fork_running_past_finalized_checkpoint() { finalized_blocks, vec![ Hash256::zero().into(), - canonical_blocks[&Slot::new(slots_per_epoch as u64)], - canonical_blocks[&Slot::new((slots_per_epoch * 2) as u64)], + canonical_blocks[&Slot::new(slots_per_epoch as u64 * 3)], + canonical_blocks[&Slot::new(slots_per_epoch as u64 * 4)], ] .into_iter() .collect() @@ -1203,9 +1218,20 @@ fn prunes_skipped_slots_states() { let (canonical_blocks_post_finalization, _, _, _, _) = harness.add_canonical_chain_blocks( canonical_state, canonical_slot, - slots_per_epoch * 5, + slots_per_epoch * 6, &honest_validators, ); + assert_eq!( + harness + .chain + .head() + .unwrap() + .beacon_state + .finalized_checkpoint + .epoch, + 2, + "chain should have finalized" + ); // Postconditions let chain_dump = harness.chain.chain_dump().unwrap(); @@ -1218,7 +1244,7 @@ fn prunes_skipped_slots_states() { finalized_blocks, vec![ Hash256::zero().into(), - canonical_blocks[&Slot::new(slots_per_epoch as u64)], + canonical_blocks[&Slot::new(slots_per_epoch as u64 * 2)], ] .into_iter() .collect() @@ -1240,10 +1266,12 @@ fn prunes_skipped_slots_states() { assert!( harness .chain - .get_state(&state_hash, Some(slot)) + .get_state(&state_hash, None) .unwrap() .is_none(), - "skipped slot states should have been pruned" + "skipped slot {} state {} should have been pruned", + slot, + state_hash ); } } diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs index aecddd2dcb..c46e211d1b 100644 --- a/beacon_node/beacon_chain/tests/tests.rs +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -3,10 +3,12 @@ #[macro_use] extern crate lazy_static; -use beacon_chain::test_utils::{ - AttestationStrategy, BeaconChainHarness, BlockStrategy, HarnessType, OP_POOL_DB_KEY, +use beacon_chain::{ + attestation_verification::Error as AttnError, + test_utils::{ + AttestationStrategy, BeaconChainHarness, BlockStrategy, HarnessType, OP_POOL_DB_KEY, + }, }; -use beacon_chain::{AttestationProcessingOutcome, AttestationType}; use operation_pool::PersistedOperationPool; use state_processing::{ per_slot_processing, per_slot_processing::Error as SlotProcessingError, EpochProcessingError, @@ -361,7 +363,7 @@ fn roundtrip_operation_pool() { } #[test] -fn free_attestations_added_to_fork_choice_some_none() { +fn unaggregated_attestations_added_to_fork_choice_some_none() { let num_blocks_produced = MinimalEthSpec::slots_per_epoch() / 2; let harness = get_harness(VALIDATOR_COUNT); @@ -425,7 +427,7 @@ fn attestations_with_increasing_slots() { ); attestations.append( - &mut harness.get_free_attestations( + &mut harness.get_unaggregated_attestations( &AttestationStrategy::AllValidators, &harness.chain.head().expect("should get head").beacon_state, harness @@ -445,30 +447,31 @@ fn attestations_with_increasing_slots() { harness.advance_slot(); } - let current_epoch = harness.chain.epoch().expect("should get epoch"); - - for attestation in attestations { - let attestation_epoch = attestation.data.target.epoch; + for attestation in attestations.into_iter().flatten() { let res = harness .chain - .process_attestation(attestation, AttestationType::Aggregated); + .verify_unaggregated_attestation_for_gossip(attestation.clone()); - if attestation_epoch + 1 < current_epoch { + let current_slot = harness.chain.slot().expect("should get slot"); + let attestation_slot = attestation.data.slot; + let earliest_permissible_slot = current_slot - MinimalEthSpec::slots_per_epoch() - 1; + + if attestation_slot < earliest_permissible_slot { assert_eq!( - res, - Ok(AttestationProcessingOutcome::PastEpoch { - attestation_epoch, - current_epoch, - }) + res.err().unwrap(), + AttnError::PastSlot { + attestation_slot, + earliest_permissible_slot, + } ) } else { - assert_eq!(res, Ok(AttestationProcessingOutcome::Processed)) + res.expect("should process attestation"); } } } #[test] -fn free_attestations_added_to_fork_choice_all_updated() { +fn unaggregated_attestations_added_to_fork_choice_all_updated() { let num_blocks_produced = MinimalEthSpec::slots_per_epoch() * 2 - 1; let harness = get_harness(VALIDATOR_COUNT); diff --git a/beacon_node/client/Cargo.toml b/beacon_node/client/Cargo.toml index a6a2421616..8bd8966ac4 100644 --- a/beacon_node/client/Cargo.toml +++ b/beacon_node/client/Cargo.toml @@ -5,8 +5,8 @@ authors = ["Age Manning "] edition = "2018" [dev-dependencies] -sloggers = "0.3.4" -toml = "^0.5" +sloggers = "1.0.0" +toml = "0.5.6" [dependencies] beacon_chain = { path = "../beacon_chain" } @@ -15,27 +15,27 @@ network = { path = "../network" } timer = { path = "../timer" } eth2-libp2p = { path = "../eth2-libp2p" } rest_api = { path = "../rest_api" } -parking_lot = "0.9.0" +parking_lot = "0.10.2" websocket_server = { path = "../websocket_server" } -prometheus = "0.7.0" +prometheus = "0.8.0" types = { path = "../../eth2/types" } tree_hash = "0.1.0" eth2_config = { path = "../../eth2/utils/eth2_config" } slot_clock = { path = "../../eth2/utils/slot_clock" } -serde = "1.0.102" -serde_derive = "1.0.102" -error-chain = "0.12.1" +serde = "1.0.106" +serde_derive = "1.0.106" +error-chain = "0.12.2" serde_yaml = "0.8.11" slog = { version = "2.5.2", features = ["max_level_trace"] } -slog-async = "2.3.0" -tokio = "0.1.22" +slog-async = "2.5.0" +tokio = "0.2.20" dirs = "2.0.2" -futures = "0.1.29" -reqwest = "0.9.22" -url = "2.1.0" +futures = "0.3.4" +reqwest = "0.10.4" +url = "2.1.1" eth1 = { path = "../eth1" } genesis = { path = "../genesis" } environment = { path = "../../lighthouse/environment" } -eth2_ssz = { path = "../../eth2/utils/ssz" } +eth2_ssz = "0.1.2" lazy_static = "1.4.0" lighthouse_metrics = { path = "../../eth2/utils/lighthouse_metrics" } diff --git a/beacon_node/eth1/Cargo.toml b/beacon_node/eth1/Cargo.toml index 4e95602aba..9cb9e45d1f 100644 --- a/beacon_node/eth1/Cargo.toml +++ b/beacon_node/eth1/Cargo.toml @@ -7,26 +7,26 @@ edition = "2018" [dev-dependencies] eth1_test_rig = { path = "../../tests/eth1_test_rig" } environment = { path = "../../lighthouse/environment" } -toml = "^0.5" +toml = "0.5.6" web3 = "0.10.0" -sloggers = "0.3.4" +sloggers = "1.0.0" [dependencies] -reqwest = "0.10" -futures = {version = "0.3", features = ["compat"]} -serde_json = "1.0" -serde = { version = "1.0", features = ["derive"] } -hex = "0.3" +reqwest = "0.10.4" +futures = { version = "0.3.4", features = ["compat"] } +serde_json = "1.0.52" +serde = { version = "1.0.106", features = ["derive"] } +hex = "0.4.2" types = { path = "../../eth2/types"} merkle_proof = { path = "../../eth2/utils/merkle_proof"} -eth2_ssz = { path = "../../eth2/utils/ssz"} +eth2_ssz = "0.1.2" eth2_ssz_derive = "0.1.0" -tree_hash = { path = "../../eth2/utils/tree_hash"} -eth2_hashing = { path = "../../eth2/utils/eth2_hashing"} -parking_lot = "0.7" -slog = "^2.2.3" -tokio = { version = "0.2", features = ["full"] } +tree_hash = "0.1.0" +eth2_hashing = "0.1.0" +parking_lot = "0.10.2" +slog = "2.5.2" +tokio = { version = "0.2.20", features = ["full"] } state_processing = { path = "../../eth2/state_processing" } -libflate = "0.1" +libflate = "1.0.0" lighthouse_metrics = { path = "../../eth2/utils/lighthouse_metrics"} lazy_static = "1.4.0" diff --git a/beacon_node/eth2-libp2p/src/discovery/enr_ext.rs b/beacon_node/eth2-libp2p/src/discovery/enr_ext.rs index 8de09196ff..ba0af4d1e3 100644 --- a/beacon_node/eth2-libp2p/src/discovery/enr_ext.rs +++ b/beacon_node/eth2-libp2p/src/discovery/enr_ext.rs @@ -1,7 +1,7 @@ //! ENR extension trait to support libp2p integration. use crate::{Enr, Multiaddr, PeerId}; use discv5::enr::{CombinedKey, CombinedPublicKey}; -use libp2p::core::{identity::Keypair, multiaddr::Protocol}; +use libp2p::core::{identity::Keypair, identity::PublicKey, multiaddr::Protocol}; use tiny_keccak::{Hasher, Keccak}; /// Extend ENR for libp2p types. @@ -113,55 +113,78 @@ impl CombinedKeyExt for CombinedKey { } } -// helper function to convert a peer_id to a node_id. This is only possible for secp256k1 libp2p +// helper function to convert a peer_id to a node_id. This is only possible for secp256k1/ed25519 libp2p // peer_ids -fn peer_id_to_node_id(peer_id: &PeerId) -> Option { - let bytes = peer_id.as_bytes(); - // must be the identity hash - /* To be updated - if bytes.len() == 34 && bytes[0] == 0x00 { - // left over is potentially secp256k1 key +pub fn peer_id_to_node_id(peer_id: &PeerId) -> Result { + // A libp2p peer id byte representation should be 2 length bytes + 4 protobuf bytes + compressed pk bytes + // if generated from a PublicKey with Identity multihash. + let pk_bytes = &peer_id.as_bytes()[2..]; - if let Ok(key) = discv5::enr::secp256k1::PublicKey::parse(&bytes[1..]) { - let uncompressed_key_bytes = key.serialize(); + match PublicKey::from_protobuf_encoding(pk_bytes).map_err(|e| { + format!( + " Cannot parse libp2p public key public key from peer id: {}", + e + ) + })? { + PublicKey::Secp256k1(pk) => { + let uncompressed_key_bytes = &pk.encode_uncompressed()[1..]; let mut output = [0_u8; 32]; let mut hasher = Keccak::v256(); hasher.update(&uncompressed_key_bytes); hasher.finalize(&mut output); - return Some(discv5::enr::NodeId::parse(&output).expect("Must be correct length")); + return Ok(discv5::enr::NodeId::parse(&output).expect("Must be correct length")); } + PublicKey::Ed25519(pk) => { + let uncompressed_key_bytes = pk.encode(); + let mut output = [0_u8; 32]; + let mut hasher = Keccak::v256(); + hasher.update(&uncompressed_key_bytes); + hasher.finalize(&mut output); + return Ok(discv5::enr::NodeId::parse(&output).expect("Must be correct length")); + } + _ => return Err("Unsupported public key".into()), } - */ - None } -/* +#[cfg(test)] mod tests { use super::*; - use std::convert::TryInto; #[test] - fn test_peer_id_conversion() { - let key = discv5::enr::secp256k1::SecretKey::parse_slice( - &hex::decode("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") - .unwrap(), - ) - .unwrap(); + fn test_secp256k1_peer_id_conversion() { + let sk_hex = "df94a73d528434ce2309abb19c16aedb535322797dbd59c157b1e04095900f48"; + let sk_bytes = hex::decode(sk_hex).unwrap(); + let secret_key = discv5::enr::secp256k1::SecretKey::parse_slice(&sk_bytes).unwrap(); - let peer_id: PeerId = - hex::decode("1220dd86cd1b9414f4b9b42a1b1258390ee9097298126df92a61789483ac90801ed6") - .unwrap() - .try_into() - .unwrap(); + let libp2p_sk = libp2p::identity::secp256k1::SecretKey::from_bytes(sk_bytes).unwrap(); + let secp256k1_kp: libp2p::identity::secp256k1::Keypair = libp2p_sk.into(); + let libp2p_kp = Keypair::Secp256k1(secp256k1_kp); + let peer_id = libp2p_kp.public().into_peer_id(); + let enr = discv5::enr::EnrBuilder::new("v4") + .build(&secret_key) + .unwrap(); let node_id = peer_id_to_node_id(&peer_id).unwrap(); - let enr = { - let mut builder = discv5::enr::EnrBuilder::new("v4"); - builder.build(&key).unwrap() - }; + assert_eq!(enr.node_id(), node_id); + } + + #[test] + fn test_ed25519_peer_conversion() { + let sk_hex = "4dea8a5072119927e9d243a7d953f2f4bc95b70f110978e2f9bc7a9000e4b261"; + let sk_bytes = hex::decode(sk_hex).unwrap(); + let secret = discv5::enr::ed25519_dalek::SecretKey::from_bytes(&sk_bytes).unwrap(); + let public = discv5::enr::ed25519_dalek::PublicKey::from(&secret); + let keypair = discv5::enr::ed25519_dalek::Keypair { public, secret }; + + let libp2p_sk = libp2p::identity::ed25519::SecretKey::from_bytes(sk_bytes).unwrap(); + let ed25519_kp: libp2p::identity::ed25519::Keypair = libp2p_sk.into(); + let libp2p_kp = Keypair::Ed25519(ed25519_kp); + let peer_id = libp2p_kp.public().into_peer_id(); + + let enr = discv5::enr::EnrBuilder::new("v4").build(&keypair).unwrap(); + let node_id = peer_id_to_node_id(&peer_id).unwrap(); assert_eq!(enr.node_id(), node_id); } } -*/ diff --git a/beacon_node/eth2-libp2p/src/discovery/mod.rs b/beacon_node/eth2-libp2p/src/discovery/mod.rs index bc43f62597..fe0f07003d 100644 --- a/beacon_node/eth2-libp2p/src/discovery/mod.rs +++ b/beacon_node/eth2-libp2p/src/discovery/mod.rs @@ -4,7 +4,7 @@ pub mod enr_ext; // Allow external use of the lighthouse ENR builder pub use enr::{build_enr, CombinedKey, Keypair}; -use enr_ext::{CombinedKeyExt, EnrExt}; +pub use enr_ext::{CombinedKeyExt, EnrExt}; use crate::metrics; use crate::{error, Enr, NetworkConfig, NetworkGlobals}; @@ -196,15 +196,13 @@ impl Discovery { return Some(enr.clone()); } // not in the local cache, look in the routing table - /* TODO: Correct this function - if let Some(node_id) = peer_id_to_node_id(peer_id) { - // TODO: Need to update discv5 - // self.discovery.find_enr(&node_id) + if let Ok(_node_id) = enr_ext::peer_id_to_node_id(peer_id) { + // TODO: Need to update discv5 + // self.discovery.find_enr(&node_id) + return None; } else { - None + return None; } - */ - None } /// Adds/Removes a subnet from the ENR Bitfield diff --git a/beacon_node/genesis/Cargo.toml b/beacon_node/genesis/Cargo.toml index 106846342c..aae39bd5fd 100644 --- a/beacon_node/genesis/Cargo.toml +++ b/beacon_node/genesis/Cargo.toml @@ -8,20 +8,20 @@ edition = "2018" eth1_test_rig = { path = "../../tests/eth1_test_rig" } [dependencies] -futures = "0.3" +futures = "0.3.4" types = { path = "../../eth2/types"} environment = { path = "../../lighthouse/environment"} eth1 = { path = "../eth1"} -rayon = "1.0" +rayon = "1.3.0" state_processing = { path = "../../eth2/state_processing" } merkle_proof = { path = "../../eth2/utils/merkle_proof" } -eth2_ssz = "0.1" -eth2_hashing = { path = "../../eth2/utils/eth2_hashing" } -tree_hash = "0.1" -tokio = {version = "0.2", features = ["full"]} -parking_lot = "0.7" -slog = "^2.2.3" -exit-future = "0.2" -serde = "1.0" -serde_derive = "1.0" +eth2_ssz = "0.1.2" +eth2_hashing = "0.1.0" +tree_hash = "0.1.0" +tokio = { version = "0.2.20", features = ["full"] } +parking_lot = "0.10.2" +slog = "2.5.2" +exit-future = "0.2.0" +serde = "1.0.106" +serde_derive = "1.0.106" int_to_bytes = { path = "../../eth2/utils/int_to_bytes" } diff --git a/beacon_node/network/Cargo.toml b/beacon_node/network/Cargo.toml index 33b5582470..079a6d2d66 100644 --- a/beacon_node/network/Cargo.toml +++ b/beacon_node/network/Cargo.toml @@ -5,9 +5,9 @@ authors = ["Age Manning "] edition = "2018" [dev-dependencies] -sloggers = "0.3.4" +sloggers = "1.0.0" genesis = { path = "../genesis" } -tempdir = "0.3" +tempdir = "0.3.7" [dependencies] beacon_chain = { path = "../beacon_chain" } @@ -18,15 +18,15 @@ rest_types = { path = "../../eth2/utils/rest_types" } types = { path = "../../eth2/types" } slot_clock = { path = "../../eth2/utils/slot_clock" } slog = { version = "2.5.2", features = ["max_level_trace"] } -hex = "0.3" +hex = "0.4.2" eth2_ssz = "0.1.2" tree_hash = "0.1.0" -futures = "0.1.29" -error-chain = "0.12.1" -tokio = "0.1.22" -parking_lot = "0.9.0" -smallvec = "1.0.0" +futures = "0.3.4" +error-chain = "0.12.2" +tokio = { version = "0.2.20", features = ["full"] } +parking_lot = "0.10.2" +smallvec = "1.4.0" # TODO: Remove rand crate for mainnet -rand = "0.7.2" +rand = "0.7.3" fnv = "1.0.6" -rlp = "0.4.3" +rlp = "0.4.5" diff --git a/beacon_node/network/src/router/mod.rs b/beacon_node/network/src/router/mod.rs index 8615d55863..d6a222e20c 100644 --- a/beacon_node/network/src/router/mod.rs +++ b/beacon_node/network/src/router/mod.rs @@ -8,7 +8,7 @@ pub mod processor; use crate::error; use crate::service::NetworkMessage; -use beacon_chain::{AttestationType, BeaconChain, BeaconChainTypes, BlockError}; +use beacon_chain::{BeaconChain, BeaconChainTypes, BlockError}; use eth2_libp2p::{ rpc::{ RPCCodedResponse, RPCError, RPCRequest, RPCResponse, RPCResponseErrorCode, RequestId, @@ -252,30 +252,28 @@ impl Router { match gossip_message { // Attestations should never reach the router. PubsubMessage::AggregateAndProofAttestation(aggregate_and_proof) => { - if self - .processor - .should_forward_aggregate_attestation(&aggregate_and_proof) + if let Some(gossip_verified) = + self.processor.verify_aggregated_attestation_for_gossip( + peer_id.clone(), + *aggregate_and_proof.clone(), + ) { self.propagate_message(id, peer_id.clone()); + self.processor + .import_aggregated_attestation(peer_id, gossip_verified); } - self.processor.process_attestation_gossip( - peer_id, - aggregate_and_proof.message.aggregate, - AttestationType::Aggregated, - ); } PubsubMessage::Attestation(subnet_attestation) => { - if self - .processor - .should_forward_attestation(&subnet_attestation.1) + if let Some(gossip_verified) = + self.processor.verify_unaggregated_attestation_for_gossip( + peer_id.clone(), + subnet_attestation.1.clone(), + ) { self.propagate_message(id, peer_id.clone()); + self.processor + .import_unaggregated_attestation(peer_id, gossip_verified); } - self.processor.process_attestation_gossip( - peer_id, - subnet_attestation.1, - AttestationType::Unaggregated { should_store: true }, - ); } PubsubMessage::BeaconBlock(block) => { match self.processor.should_forward_block(&peer_id, block) { diff --git a/beacon_node/network/src/router/processor.rs b/beacon_node/network/src/router/processor.rs index fc1f6b6fb5..79e4e1bcb7 100644 --- a/beacon_node/network/src/router/processor.rs +++ b/beacon_node/network/src/router/processor.rs @@ -1,8 +1,11 @@ use crate::service::NetworkMessage; use crate::sync::{PeerSyncInfo, SyncMessage}; use beacon_chain::{ - AttestationProcessingOutcome, AttestationType, BeaconChain, BeaconChainTypes, BlockError, - BlockProcessingOutcome, GossipVerifiedBlock, + attestation_verification::{ + Error as AttnError, IntoForkChoiceVerifiedAttestation, VerifiedAggregatedAttestation, + VerifiedUnaggregatedAttestation, + }, + BeaconChain, BeaconChainTypes, BlockError, BlockProcessingOutcome, GossipVerifiedBlock, }; use eth2_libp2p::rpc::methods::*; use eth2_libp2p::rpc::{RPCCodedResponse, RPCEvent, RPCRequest, RPCResponse, RequestId}; @@ -548,77 +551,322 @@ impl Processor { true } - /// Verifies the Aggregate attestation before propagating. - pub fn should_forward_aggregate_attestation( - &self, - _aggregate_and_proof: &Box>, - ) -> bool { - // TODO: Implement - true - } - - /// Verifies the attestation before propagating. - pub fn should_forward_attestation(&self, _aggregate: &Attestation) -> bool { - // TODO: Implement - true - } - - /// Process a new attestation received from gossipsub. - pub fn process_attestation_gossip( + /// Handle an error whilst verifying an `Attestation` or `SignedAggregateAndProof` from the + /// network. + pub fn handle_attestation_verification_failure( &mut self, peer_id: PeerId, - msg: Attestation, - attestation_type: AttestationType, + beacon_block_root: Hash256, + attestation_type: &str, + error: AttnError, ) { - match self - .chain - .process_attestation(msg.clone(), attestation_type) - { - Ok(outcome) => match outcome { - AttestationProcessingOutcome::Processed => { - debug!( - self.log, - "Processed attestation"; - "source" => "gossip", - "peer" => format!("{:?}",peer_id), - "block_root" => format!("{}", msg.data.beacon_block_root), - "slot" => format!("{}", msg.data.slot), - ); - } - AttestationProcessingOutcome::UnknownHeadBlock { beacon_block_root } => { - // TODO: Maintain this attestation and re-process once sync completes - debug!( + debug!( + self.log, + "Invalid attestation from network"; + "block" => format!("{}", beacon_block_root), + "peer_id" => format!("{:?}", peer_id), + "type" => format!("{:?}", attestation_type), + ); + + match error { + AttnError::FutureEpoch { .. } + | AttnError::PastEpoch { .. } + | AttnError::FutureSlot { .. } + | AttnError::PastSlot { .. } => { + /* + * These errors can be triggered by a mismatch between our slot and the peer. + * + * + * The peer has published an invalid consensus message, _only_ if we trust our own clock. + */ + } + AttnError::InvalidSelectionProof { .. } | AttnError::InvalidSignature => { + /* + * These errors are caused by invalid signatures. + * + * The peer has published an invalid consensus message. + */ + } + AttnError::EmptyAggregationBitfield => { + /* + * The aggregate had no signatures and is therefore worthless. + * + * Whilst we don't gossip this attestation, this act is **not** a clear + * violation of the spec nor indication of fault. + * + * This may change soon. Reference: + * + * https://github.com/ethereum/eth2.0-specs/pull/1732 + */ + } + AttnError::AggregatorPubkeyUnknown(_) => { + /* + * The aggregator index was higher than any known validator index. This is + * possible in two cases: + * + * 1. The attestation is malformed + * 2. The attestation attests to a beacon_block_root that we do not know. + * + * It should be impossible to reach (2) without triggering + * `AttnError::UnknownHeadBlock`, so we can safely assume the peer is + * faulty. + * + * The peer has published an invalid consensus message. + */ + } + AttnError::AggregatorNotInCommittee { .. } => { + /* + * The aggregator index was higher than any known validator index. This is + * possible in two cases: + * + * 1. The attestation is malformed + * 2. The attestation attests to a beacon_block_root that we do not know. + * + * It should be impossible to reach (2) without triggering + * `AttnError::UnknownHeadBlock`, so we can safely assume the peer is + * faulty. + * + * The peer has published an invalid consensus message. + */ + } + AttnError::AttestationAlreadyKnown { .. } => { + /* + * The aggregate attestation has already been observed on the network or in + * a block. + * + * The peer is not necessarily faulty. + */ + } + AttnError::AggregatorAlreadyKnown(_) => { + /* + * There has already been an aggregate attestation seen from this + * aggregator index. + * + * The peer is not necessarily faulty. + */ + } + AttnError::PriorAttestationKnown { .. } => { + /* + * We have already seen an attestation from this validator for this epoch. + * + * The peer is not necessarily faulty. + */ + } + AttnError::ValidatorIndexTooHigh(_) => { + /* + * The aggregator index (or similar field) was higher than the maximum + * possible number of validators. + * + * The peer has published an invalid consensus message. + */ + } + AttnError::UnknownHeadBlock { beacon_block_root } => { + // Note: its a little bit unclear as to whether or not this block is unknown or + // just old. See: + // + // https://github.com/sigp/lighthouse/issues/1039 + + // TODO: Maintain this attestation and re-process once sync completes + debug!( self.log, "Attestation for unknown block"; "peer_id" => format!("{:?}", peer_id), "block" => format!("{}", beacon_block_root) - ); - // we don't know the block, get the sync manager to handle the block lookup - self.send_to_sync(SyncMessage::UnknownBlockHash(peer_id, beacon_block_root)); - } - AttestationProcessingOutcome::FutureEpoch { .. } - | AttestationProcessingOutcome::PastEpoch { .. } - | AttestationProcessingOutcome::UnknownTargetRoot { .. } - | AttestationProcessingOutcome::FinalizedSlot { .. } => {} // ignore the attestation - AttestationProcessingOutcome::Invalid { .. } - | AttestationProcessingOutcome::EmptyAggregationBitfield { .. } - | AttestationProcessingOutcome::AttestsToFutureBlock { .. } - | AttestationProcessingOutcome::InvalidSignature - | AttestationProcessingOutcome::NoCommitteeForSlotAndIndex { .. } - | AttestationProcessingOutcome::BadTargetEpoch { .. } => { - // the peer has sent a bad attestation. Remove them. - self.network.disconnect(peer_id, GoodbyeReason::Fault); - } - }, - Err(_) => { - // error is logged during the processing therefore no error is logged here - trace!( + ); + // we don't know the block, get the sync manager to handle the block lookup + self.send_to_sync(SyncMessage::UnknownBlockHash(peer_id, beacon_block_root)); + } + AttnError::UnknownTargetRoot(_) => { + /* + * The block indicated by the target root is not known to us. + * + * We should always get `AttnError::UnknwonHeadBlock` before we get this + * error, so this means we can get this error if: + * + * 1. The target root does not represent a valid block. + * 2. We do not have the target root in our DB. + * + * 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> + * + * The peer has published an invalid consensus message. + */ + } + AttnError::BadTargetEpoch => { + /* + * The aggregator index (or similar field) was higher than the maximum + * possible number of validators. + * + * The peer has published an invalid consensus message. + */ + } + AttnError::NoCommitteeForSlotAndIndex { .. } => { + /* + * It is not possible to attest this the given committee in the given slot. + * + * The peer has published an invalid consensus message. + */ + } + AttnError::NotExactlyOneAggregationBitSet(_) => { + /* + * The unaggregated attestation doesn't have only one signature. + * + * The peer has published an invalid consensus message. + */ + } + AttnError::AttestsToFutureBlock { .. } => { + /* + * The beacon_block_root is from a higher slot than the attestation. + * + * The peer has published an invalid consensus message. + */ + } + AttnError::Invalid(_) => { + /* + * The attestation failed the state_processing verification. + * + * The peer has published an invalid consensus message. + */ + } + AttnError::BeaconChainError(e) => { + /* + * Lighthouse hit an unexpected error whilst processing the attestation. It + * should be impossible to trigger a `BeaconChainError` from the network, + * so we have a bug. + * + * It's not clear if the message is invalid/malicious. + */ + error!( self.log, - "Erroneous gossip attestation ssz"; - "ssz" => format!("0x{}", hex::encode(msg.as_ssz_bytes())), + "Unable to validate aggregate"; + "peer_id" => format!("{:?}", peer_id), + "error" => format!("{:?}", e), ); } - }; + } + } + + pub fn verify_aggregated_attestation_for_gossip( + &mut self, + peer_id: PeerId, + aggregate_and_proof: SignedAggregateAndProof, + ) -> Option> { + // This is provided to the error handling function to assist with debugging. + let beacon_block_root = aggregate_and_proof.message.aggregate.data.beacon_block_root; + + self.chain + .verify_aggregated_attestation_for_gossip(aggregate_and_proof) + .map_err(|e| { + self.handle_attestation_verification_failure( + peer_id, + beacon_block_root, + "aggregated", + e, + ) + }) + .ok() + } + + pub fn import_aggregated_attestation( + &mut self, + peer_id: PeerId, + verified_attestation: VerifiedAggregatedAttestation, + ) { + // This is provided to the error handling function to assist with debugging. + let beacon_block_root = verified_attestation.attestation().data.beacon_block_root; + + self.apply_attestation_to_fork_choice( + peer_id.clone(), + beacon_block_root, + &verified_attestation, + ); + + if let Err(e) = self.chain.add_to_block_inclusion_pool(verified_attestation) { + debug!( + self.log, + "Attestation invalid for op pool"; + "reason" => format!("{:?}", e), + "peer" => format!("{:?}", peer_id), + "beacon_block_root" => format!("{:?}", beacon_block_root) + ) + } + } + + pub fn verify_unaggregated_attestation_for_gossip( + &mut self, + peer_id: PeerId, + unaggregated_attestation: Attestation, + ) -> Option> { + // This is provided to the error handling function to assist with debugging. + let beacon_block_root = unaggregated_attestation.data.beacon_block_root; + + self.chain + .verify_unaggregated_attestation_for_gossip(unaggregated_attestation) + .map_err(|e| { + self.handle_attestation_verification_failure( + peer_id, + beacon_block_root, + "unaggregated", + e, + ) + }) + .ok() + } + + pub fn import_unaggregated_attestation( + &mut self, + peer_id: PeerId, + verified_attestation: VerifiedUnaggregatedAttestation, + ) { + // This is provided to the error handling function to assist with debugging. + let beacon_block_root = verified_attestation.attestation().data.beacon_block_root; + + self.apply_attestation_to_fork_choice( + peer_id.clone(), + beacon_block_root, + &verified_attestation, + ); + + if let Err(e) = self + .chain + .add_to_naive_aggregation_pool(verified_attestation) + { + debug!( + self.log, + "Attestation invalid for agg pool"; + "reason" => format!("{:?}", e), + "peer" => format!("{:?}", peer_id), + "beacon_block_root" => format!("{:?}", beacon_block_root) + ) + } + } + + /// Apply the attestation to fork choice, suppressing errors. + /// + /// We suppress the errors when adding an attestation to fork choice since the spec + /// permits gossiping attestations that are invalid to be applied to fork choice. + /// + /// An attestation that is invalid for fork choice can still be included in a block. + /// + /// Reference: + /// https://github.com/ethereum/eth2.0-specs/issues/1408#issuecomment-617599260 + fn apply_attestation_to_fork_choice<'a>( + &self, + peer_id: PeerId, + beacon_block_root: Hash256, + attestation: &'a impl IntoForkChoiceVerifiedAttestation<'a, T>, + ) { + if let Err(e) = self.chain.apply_attestation_to_fork_choice(attestation) { + debug!( + self.log, + "Attestation invalid for fork choice"; + "reason" => format!("{:?}", e), + "peer" => format!("{:?}", peer_id), + "beacon_block_root" => format!("{:?}", beacon_block_root) + ) + } } } diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index 5eff3654e8..43db6c50fc 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -15,9 +15,9 @@ use rest_types::ValidatorSubscription; use slog::{debug, error, info, trace}; use std::sync::Arc; use std::time::{Duration, Instant}; -use tokio::runtime::TaskExecutor; +use tokio::runtime::Handle; use tokio::sync::{mpsc, oneshot}; -use tokio::timer::Delay; +use tokio::time::{delay_for, Delay}; use types::EthSpec; mod tests; @@ -56,7 +56,7 @@ impl NetworkService { pub fn start( beacon_chain: Arc>, config: &NetworkConfig, - executor: &TaskExecutor, + runtime_handle: &Handle, network_log: slog::Logger, ) -> error::Result<( Arc>, @@ -95,7 +95,7 @@ impl NetworkService { beacon_chain.clone(), network_globals.clone(), network_send.clone(), - executor, + runtime_handle, network_log.clone(), )?; @@ -118,7 +118,7 @@ impl NetworkService { propagation_percentage, }; - let network_exit = spawn_service(network_service, &executor)?; + let network_exit = runtime_handle.enter(|| spawn_service(network_service))?; Ok((network_globals, network_send, network_exit)) } @@ -126,48 +126,45 @@ impl NetworkService { fn spawn_service( mut service: NetworkService, - executor: &TaskExecutor, ) -> error::Result> { let (network_exit, mut exit_rx) = tokio::sync::oneshot::channel(); // spawn on the current executor - executor.spawn( - futures::future::poll_fn(move || -> Result<_, ()> { - + tokio::spawn(futures::future::poll_fn(move || -> Result<_, ()> { let log = &service.log; // handles any logic which requires an initial delay if !service.initial_delay.is_elapsed() { if let Ok(Async::Ready(_)) = service.initial_delay.poll() { - let multi_addrs = Swarm::listeners(&service.libp2p.swarm).cloned().collect(); - *service.network_globals.listen_multiaddrs.write() = multi_addrs; + let multi_addrs = Swarm::listeners(&service.libp2p.swarm).cloned().collect(); + *service.network_globals.listen_multiaddrs.write() = multi_addrs; } } // perform termination tasks when the network is being shutdown if let Ok(Async::Ready(_)) | Err(_) = exit_rx.poll() { - // network thread is terminating - let enrs: Vec = service.libp2p.swarm.enr_entries().cloned().collect(); - debug!( - log, - "Persisting DHT to store"; - "Number of peers" => format!("{}", enrs.len()), - ); + // network thread is terminating + let enrs: Vec = service.libp2p.swarm.enr_entries().cloned().collect(); + debug!( + log, + "Persisting DHT to store"; + "Number of peers" => format!("{}", enrs.len()), + ); - match persist_dht::(service.store.clone(), enrs) { - Err(e) => error!( - log, - "Failed to persist DHT on drop"; - "error" => format!("{:?}", e) - ), - Ok(_) => info!( - log, - "Saved DHT state"; - ), - } + match persist_dht::(service.store.clone(), enrs) { + Err(e) => error!( + log, + "Failed to persist DHT on drop"; + "error" => format!("{:?}", e) + ), + Ok(_) => info!( + log, + "Saved DHT state"; + ), + } - info!(log.clone(), "Network service shutdown"); - return Ok(Async::Ready(())); + info!(log.clone(), "Network service shutdown"); + return Ok(Async::Ready(())); } // processes the network channel before processing the libp2p swarm @@ -201,7 +198,8 @@ fn spawn_service( "propagation_peer" => format!("{:?}", propagation_source), "message_id" => message_id.to_string(), ); - service.libp2p + service + .libp2p .swarm .propagate_message(&propagation_source, message_id); } @@ -223,10 +221,10 @@ fn spawn_service( } else { let mut topic_kinds = Vec::new(); for message in &messages { - if !topic_kinds.contains(&message.kind()) { - topic_kinds.push(message.kind()); - } + if !topic_kinds.contains(&message.kind()) { + topic_kinds.push(message.kind()); } + } debug!(log, "Sending pubsub messages"; "count" => messages.len(), "topics" => format!("{:?}", topic_kinds)); service.libp2p.swarm.publish(messages); } @@ -237,10 +235,11 @@ fn spawn_service( std::time::Duration::from_secs(BAN_PEER_TIMEOUT), ); } - NetworkMessage::Subscribe { subscriptions } => - { - // the result is dropped as it used solely for ergonomics - let _ = service.attestation_service.validator_subscriptions(subscriptions); + NetworkMessage::Subscribe { subscriptions } => { + // the result is dropped as it used solely for ergonomics + let _ = service + .attestation_service + .validator_subscriptions(subscriptions); } }, Ok(Async::NotReady) => break, @@ -258,24 +257,26 @@ fn spawn_service( // process any attestation service events // NOTE: This must come after the network message processing as that may trigger events in // the attestation service. - while let Ok(Async::Ready(Some(attestation_service_message))) = service.attestation_service.poll() { + while let Ok(Async::Ready(Some(attestation_service_message))) = + service.attestation_service.poll() + { match attestation_service_message { // TODO: Implement AttServiceMessage::Subscribe(subnet_id) => { service.libp2p.swarm.subscribe_to_subnet(subnet_id); - }, + } AttServiceMessage::Unsubscribe(subnet_id) => { service.libp2p.swarm.subscribe_to_subnet(subnet_id); - }, + } AttServiceMessage::EnrAdd(subnet_id) => { service.libp2p.swarm.update_enr_subnet(subnet_id, true); - }, + } AttServiceMessage::EnrRemove(subnet_id) => { service.libp2p.swarm.update_enr_subnet(subnet_id, false); - }, + } AttServiceMessage::DiscoverPeers(subnet_id) => { service.libp2p.swarm.peers_request(subnet_id); - }, + } } } @@ -289,26 +290,38 @@ fn spawn_service( if let RPCEvent::Request(_, RPCRequest::Goodbye(_)) = rpc_event { peers_to_ban.push(peer_id.clone()); }; - service.router_send + service + .router_send .try_send(RouterMessage::RPC(peer_id, rpc_event)) - .map_err(|_| { debug!(log, "Failed to send RPC to router");} )?; + .map_err(|_| { + debug!(log, "Failed to send RPC to router"); + })?; } BehaviourEvent::PeerDialed(peer_id) => { debug!(log, "Peer Dialed"; "peer_id" => format!("{}", peer_id)); - service.router_send + service + .router_send .try_send(RouterMessage::PeerDialed(peer_id)) - .map_err(|_| { debug!(log, "Failed to send peer dialed to router");})?; + .map_err(|_| { + debug!(log, "Failed to send peer dialed to router"); + })?; } BehaviourEvent::PeerDisconnected(peer_id) => { debug!(log, "Peer Disconnected"; "peer_id" => format!("{}", peer_id)); - service.router_send + service + .router_send .try_send(RouterMessage::PeerDisconnected(peer_id)) - .map_err(|_| { debug!(log, "Failed to send peer disconnect to router");})?; + .map_err(|_| { + debug!(log, "Failed to send peer disconnect to router"); + })?; } BehaviourEvent::StatusPeer(peer_id) => { - service.router_send + service + .router_send .try_send(RouterMessage::StatusPeer(peer_id)) - .map_err(|_| { debug!(log, "Failed to send re-status peer to router");})?; + .map_err(|_| { + debug!(log, "Failed to send re-status peer to router"); + })?; } BehaviourEvent::PubsubMessage { id, @@ -316,7 +329,6 @@ fn spawn_service( message, .. } => { - match message { // attestation information gets processed in the attestation service PubsubMessage::Attestation(ref subnet_and_attestation) => { @@ -324,17 +336,28 @@ fn spawn_service( let attestation = &subnet_and_attestation.1; // checks if we have an aggregator for the slot. If so, we process // the attestation - if service.attestation_service.should_process_attestation(&id, &source, subnet, attestation) { - service.router_send - .try_send(RouterMessage::PubsubMessage(id, source, message)) - .map_err(|_| { debug!(log, "Failed to send pubsub message to router");})?; - } + if service.attestation_service.should_process_attestation( + &id, + &source, + subnet, + attestation, + ) { + service + .router_send + .try_send(RouterMessage::PubsubMessage(id, source, message)) + .map_err(|_| { + debug!(log, "Failed to send pubsub message to router"); + })?; + } } _ => { // all else is sent to the router - service.router_send - .try_send(RouterMessage::PubsubMessage(id, source, message)) - .map_err(|_| { debug!(log, "Failed to send pubsub message to router");})?; + service + .router_send + .try_send(RouterMessage::PubsubMessage(id, source, message)) + .map_err(|_| { + debug!(log, "Failed to send pubsub message to router"); + })?; } } } @@ -355,19 +378,20 @@ fn spawn_service( } // if we have just forked, update inform the libp2p layer - if let Some(mut update_fork_delay) = service.next_fork_update.take() { + if let Some(mut update_fork_delay) = service.next_fork_update.take() { if !update_fork_delay.is_elapsed() { if let Ok(Async::Ready(_)) = update_fork_delay.poll() { - service.libp2p.swarm.update_fork_version(service.beacon_chain.enr_fork_id()); - service.next_fork_update = next_fork_delay(&service.beacon_chain); + service + .libp2p + .swarm + .update_fork_version(service.beacon_chain.enr_fork_id()); + service.next_fork_update = next_fork_delay(&service.beacon_chain); } } } Ok(Async::NotReady) - }) - - ); + })); Ok(network_exit) } diff --git a/beacon_node/rest_api/Cargo.toml b/beacon_node/rest_api/Cargo.toml index c754ba4814..5049ae6eba 100644 --- a/beacon_node/rest_api/Cargo.toml +++ b/beacon_node/rest_api/Cargo.toml @@ -13,31 +13,31 @@ network = { path = "../network" } eth2-libp2p = { path = "../eth2-libp2p" } store = { path = "../store" } version = { path = "../version" } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -serde_yaml = "0.8" -slog = "2.5" -slog-term = "2.4" -slog-async = "2.3" -eth2_ssz = { path = "../../eth2/utils/ssz" } -eth2_ssz_derive = { path = "../../eth2/utils/ssz_derive" } +serde = { version = "1.0.106", features = ["derive"] } +serde_json = "1.0.52" +serde_yaml = "0.8.11" +slog = "2.5.2" +slog-term = "2.5.0" +slog-async = "2.5.0" +eth2_ssz = "0.1.2" +eth2_ssz_derive = "0.1.0" state_processing = { path = "../../eth2/state_processing" } types = { path = "../../eth2/types" } -http = "0.1" -hyper = "0.12" -tokio = "0.1.22" -url = "2.1" -lazy_static = "1.3.0" +http = "0.2.1" +hyper = "0.13.5" +tokio = "0.2.20" +url = "2.1.1" +lazy_static = "1.4.0" eth2_config = { path = "../../eth2/utils/eth2_config" } lighthouse_metrics = { path = "../../eth2/utils/lighthouse_metrics" } slot_clock = { path = "../../eth2/utils/slot_clock" } -hex = "0.3" -parking_lot = "0.9" -futures = "0.1.29" +hex = "0.4.2" +parking_lot = "0.10.2" +futures = "0.3.4" operation_pool = { path = "../../eth2/operation_pool" } rayon = "1.3.0" [dev-dependencies] remote_beacon_node = { path = "../../eth2/utils/remote_beacon_node" } node_test_rig = { path = "../../tests/node_test_rig" } -tree_hash = { path = "../../eth2/utils/tree_hash" } +tree_hash = "0.1.0" diff --git a/beacon_node/rest_api/src/helpers.rs b/beacon_node/rest_api/src/helpers.rs index 661b561c87..171a10d246 100644 --- a/beacon_node/rest_api/src/helpers.rs +++ b/beacon_node/rest_api/src/helpers.rs @@ -9,8 +9,7 @@ use network::NetworkMessage; use ssz::Decode; use store::{iter::AncestorIter, Store}; use types::{ - Attestation, BeaconState, ChainSpec, CommitteeIndex, Epoch, EthSpec, Hash256, RelativeEpoch, - SignedAggregateAndProof, SignedBeaconBlock, Slot, + BeaconState, CommitteeIndex, Epoch, EthSpec, Hash256, RelativeEpoch, SignedBeaconBlock, Slot, }; /// Parse a slot. @@ -247,59 +246,6 @@ pub fn publish_beacon_block_to_network( Ok(()) } -/// Publishes a raw un-aggregated attestation to the network. -pub fn publish_raw_attestations_to_network( - mut chan: NetworkChannel, - attestations: Vec>, - spec: &ChainSpec, -) -> Result<(), ApiError> { - let messages = attestations - .into_iter() - .map(|attestation| { - // create the gossip message to send to the network - let subnet_id = attestation - .subnet_id(spec) - .map_err(|e| ApiError::ServerError(format!("Unable to get subnet id: {:?}", e)))?; - - Ok(PubsubMessage::Attestation(Box::new(( - subnet_id, - attestation, - )))) - }) - .collect::, ApiError>>()?; - - // Publish the attestations to the p2p network via gossipsub. - if let Err(e) = chan.try_send(NetworkMessage::Publish { messages }) { - return Err(ApiError::ServerError(format!( - "Unable to send new attestation to network: {:?}", - e - ))); - } - - Ok(()) -} - -/// Publishes an aggregated attestation to the network. -pub fn publish_aggregate_attestations_to_network( - mut chan: NetworkChannel, - signed_proofs: Vec>, -) -> Result<(), ApiError> { - let messages = signed_proofs - .into_iter() - .map(|signed_proof| PubsubMessage::AggregateAndProofAttestation(Box::new(signed_proof))) - .collect::>(); - - // Publish the attestations to the p2p network via gossipsub. - if let Err(e) = chan.try_send(NetworkMessage::Publish { messages }) { - return Err(ApiError::ServerError(format!( - "Unable to send new attestation to network: {:?}", - e - ))); - } - - Ok(()) -} - #[cfg(test)] mod test { use super::*; diff --git a/beacon_node/rest_api/src/validator.rs b/beacon_node/rest_api/src/validator.rs index 609a52e647..18bd628c0c 100644 --- a/beacon_node/rest_api/src/validator.rs +++ b/beacon_node/rest_api/src/validator.rs @@ -1,25 +1,23 @@ -use crate::helpers::{ - check_content_type_for_json, publish_aggregate_attestations_to_network, - publish_beacon_block_to_network, publish_raw_attestations_to_network, -}; +use crate::helpers::{check_content_type_for_json, publish_beacon_block_to_network}; use crate::response_builder::ResponseBuilder; use crate::{ApiError, ApiResult, BoxFut, NetworkChannel, UrlQuery}; use beacon_chain::{ - AttestationProcessingOutcome, AttestationType, BeaconChain, BeaconChainTypes, BlockError, + attestation_verification::Error as AttnError, BeaconChain, BeaconChainTypes, BlockError, StateSkipConfig, }; use bls::PublicKeyBytes; +use eth2_libp2p::PubsubMessage; use futures::{Future, Stream}; use hyper::{Body, Request}; use network::NetworkMessage; use rayon::prelude::*; use rest_types::{ValidatorDutiesRequest, ValidatorDutyBytes, ValidatorSubscription}; -use slog::{error, info, warn, Logger}; +use slog::{error, info, trace, warn, Logger}; use std::sync::Arc; use types::beacon_state::EthSpec; use types::{ - Attestation, BeaconState, Epoch, RelativeEpoch, SignedAggregateAndProof, SignedBeaconBlock, - Slot, + Attestation, AttestationData, BeaconState, Epoch, RelativeEpoch, SelectionProof, + SignedAggregateAndProof, SignedBeaconBlock, Slot, }; /// HTTP Handler to retrieve the duties for a set of validators during a particular epoch. This @@ -226,14 +224,12 @@ fn return_validator_duties( )) })?; - // Obtain the aggregator modulo - let aggregator_modulo = duties.map(|d| { - std::cmp::max( - 1, - d.committee_len as u64 - / &beacon_chain.spec.target_aggregators_per_committee, - ) - }); + let aggregator_modulo = duties + .map(|duties| SelectionProof::modulo(duties.committee_len, &beacon_chain.spec)) + .transpose() + .map_err(|e| { + ApiError::ServerError(format!("Unable to find modulo: {:?}", e)) + })?; let block_proposal_slots = validator_proposers .iter() @@ -400,7 +396,7 @@ pub fn get_new_attestation( let index = query.committee_index()?; let attestation = beacon_chain - .produce_attestation(slot, index) + .produce_unaggregated_attestation(slot, index) .map_err(|e| ApiError::BadRequest(format!("Unable to produce attestation: {:?}", e)))?; ResponseBuilder::new(&req)?.body(&attestation) @@ -450,73 +446,101 @@ pub fn publish_attestations( )) }) }) - .and_then(move |attestations: Vec>| { - // Note: This is a new attestation from a validator. We want to process this and - // inform the validator whether the attestation was valid. In doing so, we store - // this un-aggregated raw attestation in the op_pool by default. This is - // sub-optimal as if we have no validators needing to aggregate, these don't need - // to be stored in the op-pool. This is minimal however as the op_pool gets pruned - // every slot - attestations.par_iter().try_for_each(|attestation| { - // In accordance with the naive aggregation strategy, the validator client should - // only publish attestations to this endpoint with a single signature. - if attestation.aggregation_bits.num_set_bits() != 1 { - return Err(ApiError::BadRequest(format!("Attestation should have exactly one aggregation bit set"))) - } - - // TODO: we only need to store these attestations if we're aggregating for the - // given subnet. - let attestation_type = AttestationType::Unaggregated { should_store: true }; - - match beacon_chain.process_attestation(attestation.clone(), attestation_type) { - Ok(AttestationProcessingOutcome::Processed) => { - // Block was processed, publish via gossipsub - info!( - log, - "Attestation from local validator"; - "target" => attestation.data.source.epoch, - "source" => attestation.data.source.epoch, - "index" => attestation.data.index, - "slot" => attestation.data.slot, - ); - Ok(()) - } - Ok(outcome) => { - warn!( - log, - "Invalid attestation from local validator"; - "outcome" => format!("{:?}", outcome) - ); - - Err(ApiError::ProcessingError(format!( - "An Attestation could not be processed and has not been published: {:?}", - outcome - ))) - } - Err(e) => { - error!( - log, - "Error whilst processing attestation"; - "error" => format!("{:?}", e) - ); - - Err(ApiError::ServerError(format!( - "Error while processing attestation: {:?}", - e - ))) - } - } - })?; - - Ok((attestations, beacon_chain)) + // Process all of the aggregates _without_ exiting early if one fails. + .map(move |attestations: Vec>| { + attestations + .into_par_iter() + .enumerate() + .map(|(i, attestation)| { + process_unaggregated_attestation( + &beacon_chain, + network_chan.clone(), + attestation, + i, + &log, + ) + }) + .collect::>>() }) - .and_then(|(attestations, beacon_chain)| { - publish_raw_attestations_to_network::(network_chan, attestations, &beacon_chain.spec) + // Iterate through all the results and return on the first `Err`. + // + // Note: this will only provide info about the _first_ failure, not all failures. + .and_then(|processing_results| { + processing_results.into_iter().try_for_each(|result| result) }) .and_then(|_| response_builder?.body_no_ssz(&())), ) } +/// Processes an unaggregrated attestation that was included in a list of attestations with the +/// index `i`. +fn process_unaggregated_attestation( + beacon_chain: &BeaconChain, + mut network_chan: NetworkChannel, + attestation: Attestation, + i: usize, + log: &Logger, +) -> Result<(), ApiError> { + let data = &attestation.data.clone(); + + // Verify that the attestation is valid to included on the gossip network. + let verified_attestation = beacon_chain + .verify_unaggregated_attestation_for_gossip(attestation.clone()) + .map_err(|e| { + handle_attestation_error( + e, + &format!("unaggregated attestation {} failed gossip verification", i), + data, + log, + ) + })?; + + // Publish the attestation to the network + if let Err(e) = network_chan.try_send(NetworkMessage::Publish { + messages: vec![PubsubMessage::Attestation(Box::new(( + attestation + .subnet_id(&beacon_chain.spec) + .map_err(|e| ApiError::ServerError(format!("Unable to get subnet id: {:?}", e)))?, + attestation, + )))], + }) { + return Err(ApiError::ServerError(format!( + "Unable to send unaggregated attestation {} to network: {:?}", + i, e + ))); + } + + beacon_chain + .apply_attestation_to_fork_choice(&verified_attestation) + .map_err(|e| { + handle_attestation_error( + e, + &format!( + "unaggregated attestation {} was unable to be added to fork choice", + i + ), + data, + log, + ) + })?; + + beacon_chain + .add_to_naive_aggregation_pool(verified_attestation) + .map_err(|e| { + handle_attestation_error( + e, + &format!( + "unaggregated attestation {} was unable to be added to aggregation pool", + i + ), + data, + log, + ) + })?; + + Ok(()) +} + /// HTTP Handler to publish an Attestation, which has been signed by a validator. pub fn publish_aggregate_and_proofs( req: Request, @@ -540,90 +564,168 @@ pub fn publish_aggregate_and_proofs( )) }) }) - .and_then(move |signed_proofs: Vec>| { - // Verify the signatures for the aggregate and proof and if valid process the - // aggregate - // TODO: Double check speed and logic consistency of handling current fork vs - // validator fork for signatures. - // TODO: More efficient way of getting a fork? - let fork = &beacon_chain.head()?.beacon_state.fork; - - // TODO: Update to shift this task to dedicated task using await - signed_proofs.par_iter().try_for_each(|signed_proof| { - let agg_proof = &signed_proof.message; - let validator_pubkey = &beacon_chain.validator_pubkey(agg_proof.aggregator_index as usize)?.ok_or_else(|| { - warn!( - log, - "Unknown validator from local validator client"; - ); - - ApiError::ProcessingError(format!("The validator is known")) - })?; - - /* - * TODO: checking that `signed_proof.is_valid()` is not sufficient. It - * is also necessary to check that the validator is actually designated as an - * aggregator for this attestation. - * - * I (Paul H) will pick this up in a future PR. - */ - - if signed_proof.is_valid(validator_pubkey, fork, beacon_chain.genesis_validators_root, &beacon_chain.spec) { - let attestation = &agg_proof.aggregate; - - match beacon_chain.process_attestation(attestation.clone(), AttestationType::Aggregated) { - Ok(AttestationProcessingOutcome::Processed) => { - // Block was processed, publish via gossipsub - info!( - log, - "Attestation from local validator"; - "target" => attestation.data.source.epoch, - "source" => attestation.data.source.epoch, - "index" => attestation.data.index, - "slot" => attestation.data.slot, - ); - Ok(()) - } - Ok(outcome) => { - warn!( - log, - "Invalid attestation from local validator"; - "outcome" => format!("{:?}", outcome) - ); - - Err(ApiError::ProcessingError(format!( - "The Attestation could not be processed and has not been published: {:?}", - outcome - ))) - } - Err(e) => { - error!( - log, - "Error whilst processing attestation"; - "error" => format!("{:?}", e) - ); - - Err(ApiError::ServerError(format!( - "Error while processing attestation: {:?}", - e - ))) - } - } - } else { - error!( - log, - "Invalid AggregateAndProof Signature" - ); - Err(ApiError::ServerError(format!( - "Invalid AggregateAndProof Signature" - ))) - } - })?; - Ok(signed_proofs) - }) - .and_then(move |signed_proofs| { - publish_aggregate_attestations_to_network::(network_chan, signed_proofs) + // Process all of the aggregates _without_ exiting early if one fails. + .map( + move |signed_aggregates: Vec>| { + signed_aggregates + .into_par_iter() + .enumerate() + .map(|(i, signed_aggregate)| { + process_aggregated_attestation( + &beacon_chain, + network_chan.clone(), + signed_aggregate, + i, + &log, + ) + }) + .collect::>>() + }, + ) + // Iterate through all the results and return on the first `Err`. + // + // Note: this will only provide info about the _first_ failure, not all failures. + .and_then(|processing_results| { + processing_results.into_iter().try_for_each(|result| result) }) .and_then(|_| response_builder?.body_no_ssz(&())), ) } + +/// Processes an aggregrated attestation that was included in a list of attestations with the index +/// `i`. +fn process_aggregated_attestation( + beacon_chain: &BeaconChain, + mut network_chan: NetworkChannel, + signed_aggregate: SignedAggregateAndProof, + i: usize, + log: &Logger, +) -> Result<(), ApiError> { + let data = &signed_aggregate.message.aggregate.data.clone(); + + // Verify that the attestation is valid to be included on the gossip network. + // + // Using this gossip check for local validators is not necessarily ideal, there will be some + // attestations that we reject that could possibly be included in a block (e.g., attestations + // that late by more than 1 epoch but less than 2). We can come pick this back up if we notice + // that it's materially affecting validator profits. Until then, I'm hesitant to introduce yet + // _another_ attestation verification path. + let verified_attestation = + match beacon_chain.verify_aggregated_attestation_for_gossip(signed_aggregate.clone()) { + Ok(verified_attestation) => verified_attestation, + Err(AttnError::AttestationAlreadyKnown(attestation_root)) => { + trace!( + log, + "Ignored known attn from local validator"; + "attn_root" => format!("{}", attestation_root) + ); + + // Exit early with success for a known attestation, there's no need to re-process + // an aggregate we already know. + return Ok(()); + } + /* + * It's worth noting that we don't check for `Error::AggregatorAlreadyKnown` since (at + * the time of writing) we check for `AttestationAlreadyKnown` first. + * + * Given this, it's impossible to hit `Error::AggregatorAlreadyKnown` without that + * aggregator having already produced a conflicting aggregation. This is not slashable + * but I think it's still the sort of condition we should error on, at least for now. + */ + Err(e) => { + return Err(handle_attestation_error( + e, + &format!("aggregated attestation {} failed gossip verification", i), + data, + log, + )) + } + }; + + // Publish the attestation to the network + if let Err(e) = network_chan.try_send(NetworkMessage::Publish { + messages: vec![PubsubMessage::AggregateAndProofAttestation(Box::new( + signed_aggregate, + ))], + }) { + return Err(ApiError::ServerError(format!( + "Unable to send aggregated attestation {} to network: {:?}", + i, e + ))); + } + + beacon_chain + .apply_attestation_to_fork_choice(&verified_attestation) + .map_err(|e| { + handle_attestation_error( + e, + &format!( + "aggregated attestation {} was unable to be added to fork choice", + i + ), + data, + log, + ) + })?; + + beacon_chain + .add_to_block_inclusion_pool(verified_attestation) + .map_err(|e| { + handle_attestation_error( + e, + &format!( + "aggregated attestation {} was unable to be added to op pool", + i + ), + data, + log, + ) + })?; + + Ok(()) +} + +/// Common handler for `AttnError` during attestation verification. +fn handle_attestation_error( + e: AttnError, + detail: &str, + data: &AttestationData, + log: &Logger, +) -> ApiError { + match e { + AttnError::BeaconChainError(e) => { + error!( + log, + "Internal error verifying local attestation"; + "detail" => detail, + "error" => format!("{:?}", e), + "target" => data.target.epoch, + "source" => data.source.epoch, + "index" => data.index, + "slot" => data.slot, + ); + + ApiError::ServerError(format!( + "Internal error verifying local attestation. Error: {:?}. Detail: {}", + e, detail + )) + } + e => { + error!( + log, + "Invalid local attestation"; + "detail" => detail, + "reason" => format!("{:?}", e), + "target" => data.target.epoch, + "source" => data.source.epoch, + "index" => data.index, + "slot" => data.slot, + ); + + ApiError::ProcessingError(format!( + "Invalid local attestation. Error: {:?} Detail: {}", + e, detail + )) + } + } +} diff --git a/beacon_node/rest_api/tests/test.rs b/beacon_node/rest_api/tests/test.rs index 9f34dc61d9..16561ba1f5 100644 --- a/beacon_node/rest_api/tests/test.rs +++ b/beacon_node/rest_api/tests/test.rs @@ -93,11 +93,20 @@ fn validator_produce_attestation() { let genesis_validators_root = beacon_chain.genesis_validators_root; let state = beacon_chain.head().expect("should get head").beacon_state; - let validator_index = 0; - let duties = state - .get_attestation_duties(validator_index, RelativeEpoch::Current) - .expect("should have attestation duties cache") - .expect("should have attestation duties"); + // Find a validator that has duties in the current slot of the chain. + let mut validator_index = 0; + let duties = loop { + let duties = state + .get_attestation_duties(validator_index, RelativeEpoch::Current) + .expect("should have attestation duties cache") + .expect("should have attestation duties"); + + if duties.slot == node.client.beacon_chain().unwrap().slot().unwrap() { + break duties; + } else { + validator_index += 1 + } + }; let mut attestation = env .runtime() @@ -134,15 +143,18 @@ fn validator_produce_attestation() { // Try publishing the attestation without a signature or a committee bit set, ensure it is // raises an error. - let publish_result = env.runtime().block_on( - remote_node - .http - .validator() - .publish_attestations(vec![attestation.clone()]), - ); + let publish_status = env + .runtime() + .block_on( + remote_node + .http + .validator() + .publish_attestations(vec![attestation.clone()]), + ) + .expect("should publish unsigned attestation"); assert!( - publish_result.is_err(), - "the unsigned published attestation should return error" + !publish_status.is_valid(), + "the unsigned published attestation should be invalid" ); // Set the aggregation bit. @@ -224,6 +236,7 @@ fn validator_produce_attestation() { let signed_aggregate_and_proof = SignedAggregateAndProof::from_aggregate( validator_index as u64, aggregated_attestation, + None, &keypair.sk, &state.fork, genesis_validators_root, diff --git a/beacon_node/store/Cargo.toml b/beacon_node/store/Cargo.toml index 36a34738a4..aa9d25bc25 100644 --- a/beacon_node/store/Cargo.toml +++ b/beacon_node/store/Cargo.toml @@ -10,23 +10,23 @@ harness = false [dev-dependencies] tempfile = "3.1.0" -sloggers = "0.3.2" -criterion = "0.3.0" -rayon = "1.2.0" +sloggers = "1.0.0" +criterion = "0.3.2" +rayon = "1.3.0" [dependencies] db-key = "0.0.5" leveldb = "0.8.4" -parking_lot = "0.9.0" -itertools = "0.8" +parking_lot = "0.10.2" +itertools = "0.9.0" eth2_ssz = "0.1.2" eth2_ssz_derive = "0.1.0" tree_hash = "0.1.0" types = { path = "../../eth2/types" } state_processing = { path = "../../eth2/state_processing" } -slog = "2.2.3" -serde = "1.0" -serde_derive = "1.0.102" +slog = "2.5.2" +serde = "1.0.106" +serde_derive = "1.0.106" lazy_static = "1.4.0" lighthouse_metrics = { path = "../../eth2/utils/lighthouse_metrics" } lru = "0.4.3" diff --git a/beacon_node/timer/Cargo.toml b/beacon_node/timer/Cargo.toml index 7a2e2ec4a2..e36f45b07c 100644 --- a/beacon_node/timer/Cargo.toml +++ b/beacon_node/timer/Cargo.toml @@ -8,7 +8,7 @@ edition = "2018" beacon_chain = { path = "../beacon_chain" } types = { path = "../../eth2/types" } slot_clock = { path = "../../eth2/utils/slot_clock" } -tokio = { version = "0.2", features = ["full"] } +tokio = { version = "0.2.20", features = ["full"] } slog = "2.5.2" -parking_lot = "0.10.0" -futures = "0.3" +parking_lot = "0.10.2" +futures = "0.3.4" diff --git a/beacon_node/websocket_server/Cargo.toml b/beacon_node/websocket_server/Cargo.toml index e53e0a917f..0a5beafb2d 100644 --- a/beacon_node/websocket_server/Cargo.toml +++ b/beacon_node/websocket_server/Cargo.toml @@ -7,11 +7,11 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -futures = "0.3" -serde = "1.0.102" -serde_derive = "1.0.102" -serde_json = "1.0.41" +futures = "0.3.4" +serde = "1.0.106" +serde_derive = "1.0.106" +serde_json = "1.0.52" slog = "2.5.2" -tokio = { version = "0.2", features = ["full"] } +tokio = { version = "0.2.20", features = ["full"] } types = { path = "../../eth2/types" } ws = "0.9.1" diff --git a/eth2/operation_pool/Cargo.toml b/eth2/operation_pool/Cargo.toml index 7bfc86063a..32dbe377de 100644 --- a/eth2/operation_pool/Cargo.toml +++ b/eth2/operation_pool/Cargo.toml @@ -6,14 +6,14 @@ edition = "2018" [dependencies] int_to_bytes = { path = "../utils/int_to_bytes" } -parking_lot = "0.9.0" +parking_lot = "0.10.2" types = { path = "../types" } state_processing = { path = "../state_processing" } eth2_ssz = "0.1.2" eth2_ssz_derive = "0.1.0" -serde = "1.0.102" -serde_derive = "1.0.102" +serde = "1.0.106" +serde_derive = "1.0.106" store = { path = "../../beacon_node/store" } [dev-dependencies] -rand = "0.7.2" +rand = "0.7.3" diff --git a/eth2/proto_array_fork_choice/Cargo.toml b/eth2/proto_array_fork_choice/Cargo.toml index f17515acc3..bc1de8fe57 100644 --- a/eth2/proto_array_fork_choice/Cargo.toml +++ b/eth2/proto_array_fork_choice/Cargo.toml @@ -9,11 +9,11 @@ name = "proto_array_fork_choice" path = "src/bin.rs" [dependencies] -parking_lot = "0.9.0" +parking_lot = "0.10.2" types = { path = "../types" } -itertools = "0.8.1" +itertools = "0.9.0" eth2_ssz = "0.1.2" eth2_ssz_derive = "0.1.0" -serde = "1.0.102" -serde_derive = "1.0.102" +serde = "1.0.106" +serde_derive = "1.0.106" serde_yaml = "0.8.11" diff --git a/eth2/state_processing/Cargo.toml b/eth2/state_processing/Cargo.toml index a13e25f123..1f2c2a66da 100644 --- a/eth2/state_processing/Cargo.toml +++ b/eth2/state_processing/Cargo.toml @@ -9,10 +9,10 @@ name = "benches" harness = false [dev-dependencies] -criterion = "0.3.0" +criterion = "0.3.2" env_logger = "0.7.1" -serde = "1.0.102" -serde_derive = "1.0.102" +serde = "1.0.106" +serde_derive = "1.0.106" lazy_static = "1.4.0" serde_yaml = "0.8.11" beacon_chain = { path = "../../beacon_node/beacon_chain" } @@ -21,20 +21,20 @@ store = { path = "../../beacon_node/store" } [dependencies] bls = { path = "../utils/bls" } -integer-sqrt = "0.1.2" -itertools = "0.8.1" +integer-sqrt = "0.1.3" +itertools = "0.9.0" eth2_ssz = "0.1.2" eth2_ssz_types = { path = "../utils/ssz_types" } merkle_proof = { path = "../utils/merkle_proof" } log = "0.4.8" safe_arith = { path = "../utils/safe_arith" } tree_hash = "0.1.0" -tree_hash_derive = "0.2" +tree_hash_derive = "0.2.0" types = { path = "../types" } -rayon = "1.2.0" -eth2_hashing = { path = "../utils/eth2_hashing" } +rayon = "1.3.0" +eth2_hashing = "0.1.0" int_to_bytes = { path = "../utils/int_to_bytes" } -arbitrary = { version = "0.4.3", features = ["derive"], optional = true } +arbitrary = { version = "0.4.4", features = ["derive"], optional = true } [features] fake_crypto = ["bls/fake_crypto"] diff --git a/eth2/state_processing/src/per_block_processing/block_signature_verifier.rs b/eth2/state_processing/src/per_block_processing/block_signature_verifier.rs index 13f6d099d2..b7721b5331 100644 --- a/eth2/state_processing/src/per_block_processing/block_signature_verifier.rs +++ b/eth2/state_processing/src/per_block_processing/block_signature_verifier.rs @@ -22,6 +22,10 @@ pub enum Error { /// There was an error attempting to read from a `BeaconState`. Block /// validity was not determined. BeaconStateError(BeaconStateError), + /// The `BeaconBlock` has a `proposer_index` that does not match the index we computed locally. + /// + /// The block is invalid. + IncorrectBlockProposer { block: u64, local_shuffling: u64 }, /// Failed to load a signature set. The block may be invalid or we failed to process it. SignatureSetError(SignatureSetError), } @@ -34,7 +38,18 @@ impl From for Error { impl From for Error { fn from(e: SignatureSetError) -> Error { - Error::SignatureSetError(e) + match e { + // Make a special distinction for `IncorrectBlockProposer` since it indicates an + // invalid block, not an internal error. + SignatureSetError::IncorrectBlockProposer { + block, + local_shuffling, + } => Error::IncorrectBlockProposer { + block, + local_shuffling, + }, + e => Error::SignatureSetError(e), + } } } diff --git a/eth2/state_processing/src/per_block_processing/signature_sets.rs b/eth2/state_processing/src/per_block_processing/signature_sets.rs index 20dba7de3d..5f050cd2d6 100644 --- a/eth2/state_processing/src/per_block_processing/signature_sets.rs +++ b/eth2/state_processing/src/per_block_processing/signature_sets.rs @@ -10,8 +10,8 @@ use tree_hash::TreeHash; use types::{ AggregateSignature, AttesterSlashing, BeaconBlock, BeaconState, BeaconStateError, ChainSpec, DepositData, Domain, EthSpec, Fork, Hash256, IndexedAttestation, ProposerSlashing, PublicKey, - Signature, SignedBeaconBlock, SignedBeaconBlockHeader, SignedRoot, SignedVoluntaryExit, - SigningRoot, + Signature, SignedAggregateAndProof, SignedBeaconBlock, SignedBeaconBlockHeader, SignedRoot, + SignedVoluntaryExit, SigningRoot, }; pub type Result = std::result::Result; @@ -26,6 +26,10 @@ pub enum Error { /// Attempted to find the public key of a validator that does not exist. You cannot distinguish /// between an error and an invalid block in this case. ValidatorUnknown(u64), + /// The `BeaconBlock` has a `proposer_index` that does not match the index we computed locally. + /// + /// The block is invalid. + IncorrectBlockProposer { block: u64, local_shuffling: u64 }, /// The public keys supplied do not match the number of objects requiring keys. Block validity /// was not determined. MismatchedPublicKeyLen { pubkey_len: usize, other_len: usize }, @@ -73,6 +77,13 @@ where let block = &signed_block.message; let proposer_index = state.get_beacon_proposer_index(block.slot, spec)?; + if proposer_index as u64 != block.proposer_index { + return Err(Error::IncorrectBlockProposer { + block: block.proposer_index, + local_shuffling: proposer_index as u64, + }); + } + let domain = spec.get_domain( block.slot.epoch(T::slots_per_epoch()), Domain::BeaconProposer, @@ -343,3 +354,74 @@ where message, )) } + +pub fn signed_aggregate_selection_proof_signature_set<'a, T, F>( + get_pubkey: F, + signed_aggregate_and_proof: &'a SignedAggregateAndProof, + fork: &Fork, + genesis_validators_root: Hash256, + spec: &'a ChainSpec, +) -> Result +where + T: EthSpec, + F: Fn(usize) -> Option>, +{ + let slot = signed_aggregate_and_proof.message.aggregate.data.slot; + + let domain = spec.get_domain( + slot.epoch(T::slots_per_epoch()), + Domain::SelectionProof, + fork, + genesis_validators_root, + ); + let message = slot.signing_root(domain).as_bytes().to_vec(); + let signature = &signed_aggregate_and_proof.message.selection_proof; + let validator_index = signed_aggregate_and_proof.message.aggregator_index; + + Ok(SignatureSet::single( + signature, + get_pubkey(validator_index as usize) + .ok_or_else(|| Error::ValidatorUnknown(validator_index))?, + message, + )) +} + +pub fn signed_aggregate_signature_set<'a, T, F>( + get_pubkey: F, + signed_aggregate_and_proof: &'a SignedAggregateAndProof, + fork: &Fork, + genesis_validators_root: Hash256, + spec: &'a ChainSpec, +) -> Result +where + T: EthSpec, + F: Fn(usize) -> Option>, +{ + let target_epoch = signed_aggregate_and_proof + .message + .aggregate + .data + .target + .epoch; + + let domain = spec.get_domain( + target_epoch, + Domain::AggregateAndProof, + fork, + genesis_validators_root, + ); + let message = signed_aggregate_and_proof + .message + .signing_root(domain) + .as_bytes() + .to_vec(); + let signature = &signed_aggregate_and_proof.signature; + let validator_index = signed_aggregate_and_proof.message.aggregator_index; + + Ok(SignatureSet::single( + signature, + get_pubkey(validator_index as usize) + .ok_or_else(|| Error::ValidatorUnknown(validator_index))?, + message, + )) +} diff --git a/eth2/state_processing/src/per_block_processing/tests.rs b/eth2/state_processing/src/per_block_processing/tests.rs index e7254db9d8..dce2d352ce 100644 --- a/eth2/state_processing/src/per_block_processing/tests.rs +++ b/eth2/state_processing/src/per_block_processing/tests.rs @@ -1039,7 +1039,12 @@ fn invalid_proposer_slashing_duplicate_slashing() { let slashing = block.message.body.proposer_slashings[0].clone(); let slashed_proposer = slashing.signed_header_1.message.proposer_index; - block.message.body.proposer_slashings.push(slashing); + block + .message + .body + .proposer_slashings + .push(slashing) + .expect("should push slashing"); let result = per_block_processing( &mut state, diff --git a/eth2/types/Cargo.toml b/eth2/types/Cargo.toml index 31db5cfd86..c681760194 100644 --- a/eth2/types/Cargo.toml +++ b/eth2/types/Cargo.toml @@ -13,19 +13,19 @@ bls = { path = "../utils/bls" } compare_fields = { path = "../utils/compare_fields" } compare_fields_derive = { path = "../utils/compare_fields_derive" } dirs = "2.0.2" -derivative = "1.0.3" +derivative = "2.1.1" eth2_interop_keypairs = { path = "../utils/eth2_interop_keypairs" } ethereum-types = "0.9.1" eth2_hashing = "0.1.0" -hex = "0.3" +hex = "0.4.2" int_to_bytes = { path = "../utils/int_to_bytes" } log = "0.4.8" merkle_proof = { path = "../utils/merkle_proof" } -rayon = "1.2.0" -rand = "0.7.2" +rayon = "1.3.0" +rand = "0.7.3" safe_arith = { path = "../utils/safe_arith" } -serde = "1.0.102" -serde_derive = "1.0.102" +serde = "1.0.106" +serde_derive = "1.0.106" slog = "2.5.2" eth2_ssz = "0.1.2" eth2_ssz_derive = "0.1.0" @@ -33,17 +33,17 @@ eth2_ssz_types = { path = "../utils/ssz_types" } swap_or_not_shuffle = { path = "../utils/swap_or_not_shuffle" } test_random_derive = { path = "../utils/test_random_derive" } tree_hash = "0.1.0" -tree_hash_derive = "0.2" +tree_hash_derive = "0.2.0" rand_xorshift = "0.2.0" cached_tree_hash = { path = "../utils/cached_tree_hash" } serde_yaml = "0.8.11" tempfile = "3.1.0" -arbitrary = { version = "0.4", features = ["derive"], optional = true } +arbitrary = { version = "0.4.4", features = ["derive"], optional = true } [dev-dependencies] env_logger = "0.7.1" -serde_json = "1.0.41" -criterion = "0.3.0" +serde_json = "1.0.52" +criterion = "0.3.2" [features] arbitrary-fuzz = [ diff --git a/eth2/types/src/aggregate_and_proof.rs b/eth2/types/src/aggregate_and_proof.rs index 2cf1d2a512..979d108530 100644 --- a/eth2/types/src/aggregate_and_proof.rs +++ b/eth2/types/src/aggregate_and_proof.rs @@ -27,22 +27,28 @@ pub struct AggregateAndProof { impl AggregateAndProof { /// Produces a new `AggregateAndProof` with a `selection_proof` generated by signing /// `aggregate.data.slot` with `secret_key`. + /// + /// If `selection_proof.is_none()` it will be computed locally. pub fn from_aggregate( aggregator_index: u64, aggregate: Attestation, + selection_proof: Option, secret_key: &SecretKey, fork: &Fork, genesis_validators_root: Hash256, spec: &ChainSpec, ) -> Self { - let selection_proof = SelectionProof::new::( - aggregate.data.slot, - secret_key, - fork, - genesis_validators_root, - spec, - ) - .into(); + let selection_proof = selection_proof + .unwrap_or_else(|| { + SelectionProof::new::( + aggregate.data.slot, + secret_key, + fork, + genesis_validators_root, + spec, + ) + }) + .into(); Self { aggregator_index, diff --git a/eth2/types/src/selection_proof.rs b/eth2/types/src/selection_proof.rs index df8c330f92..8167dad557 100644 --- a/eth2/types/src/selection_proof.rs +++ b/eth2/types/src/selection_proof.rs @@ -1,5 +1,8 @@ -use crate::{ChainSpec, Domain, EthSpec, Fork, Hash256, SecretKey, Signature, SignedRoot, Slot}; +use crate::{ + ChainSpec, Domain, EthSpec, Fork, Hash256, PublicKey, SecretKey, Signature, SignedRoot, Slot, +}; use safe_arith::{ArithError, SafeArith}; +use std::cmp; use std::convert::TryInto; use tree_hash::TreeHash; @@ -26,7 +29,23 @@ impl SelectionProof { Self(Signature::new(message.as_bytes(), secret_key)) } - pub fn is_aggregator(&self, modulo: u64) -> Result { + /// Returns the "modulo" used for determining if a `SelectionProof` elects an aggregator. + pub fn modulo(committee_len: usize, spec: &ChainSpec) -> Result { + Ok(cmp::max( + 1, + (committee_len as u64).safe_div(spec.target_aggregators_per_committee)?, + )) + } + + pub fn is_aggregator( + &self, + committee_len: usize, + spec: &ChainSpec, + ) -> Result { + self.is_aggregator_from_modulo(Self::modulo(committee_len, spec)?) + } + + pub fn is_aggregator_from_modulo(&self, modulo: u64) -> Result { let signature_hash = self.0.tree_hash_root(); let signature_hash_int = u64::from_le_bytes( signature_hash[0..8] @@ -37,6 +56,25 @@ impl SelectionProof { signature_hash_int.safe_rem(modulo).map(|rem| rem == 0) } + + pub fn verify( + &self, + slot: Slot, + pubkey: &PublicKey, + fork: &Fork, + genesis_validators_root: Hash256, + spec: &ChainSpec, + ) -> bool { + let domain = spec.get_domain( + slot.epoch(T::slots_per_epoch()), + Domain::SelectionProof, + fork, + genesis_validators_root, + ); + let message = slot.signing_root(domain); + + self.0.verify(message.as_bytes(), pubkey) + } } impl Into for SelectionProof { @@ -44,3 +82,9 @@ impl Into for SelectionProof { self.0 } } + +impl From for SelectionProof { + fn from(sig: Signature) -> Self { + Self(sig) + } +} diff --git a/eth2/types/src/signed_aggregate_and_proof.rs b/eth2/types/src/signed_aggregate_and_proof.rs index 1d9c587511..fa339e3324 100644 --- a/eth2/types/src/signed_aggregate_and_proof.rs +++ b/eth2/types/src/signed_aggregate_and_proof.rs @@ -1,6 +1,6 @@ use super::{ AggregateAndProof, Attestation, ChainSpec, Domain, EthSpec, Fork, Hash256, PublicKey, - SecretKey, Signature, SignedRoot, + SecretKey, SelectionProof, Signature, SignedRoot, }; use crate::test_utils::TestRandom; use serde_derive::{Deserialize, Serialize}; @@ -25,9 +25,12 @@ pub struct SignedAggregateAndProof { impl SignedAggregateAndProof { /// Produces a new `SignedAggregateAndProof` with a `selection_proof` generated by signing /// `aggregate.data.slot` with `secret_key`. + /// + /// If `selection_proof.is_none()` it will be computed locally. pub fn from_aggregate( aggregator_index: u64, aggregate: Attestation, + selection_proof: Option, secret_key: &SecretKey, fork: &Fork, genesis_validators_root: Hash256, @@ -36,6 +39,7 @@ impl SignedAggregateAndProof { let message = AggregateAndProof::from_aggregate( aggregator_index, aggregate, + selection_proof, secret_key, fork, genesis_validators_root, diff --git a/eth2/types/src/signed_beacon_block.rs b/eth2/types/src/signed_beacon_block.rs index 733d3e06aa..a43ade048e 100644 --- a/eth2/types/src/signed_beacon_block.rs +++ b/eth2/types/src/signed_beacon_block.rs @@ -1,9 +1,11 @@ -use crate::{test_utils::TestRandom, BeaconBlock, EthSpec, Hash256, Slot}; -use std::fmt; - +use crate::{ + test_utils::TestRandom, BeaconBlock, ChainSpec, Domain, EthSpec, Fork, Hash256, PublicKey, + SignedRoot, SigningRoot, Slot, +}; use bls::Signature; use serde_derive::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; +use std::fmt; use test_random_derive::TestRandom; use tree_hash::TreeHash; @@ -47,6 +49,38 @@ pub struct SignedBeaconBlock { } impl SignedBeaconBlock { + /// Verify `self.signature`. + /// + /// If the root of `block.message` is already known it can be passed in via `object_root_opt`. + /// Otherwise, it will be computed locally. + pub fn verify_signature( + &self, + object_root_opt: Option, + pubkey: &PublicKey, + fork: &Fork, + genesis_validators_root: Hash256, + spec: &ChainSpec, + ) -> bool { + let domain = spec.get_domain( + self.message.slot.epoch(E::slots_per_epoch()), + Domain::BeaconProposer, + fork, + genesis_validators_root, + ); + + let message = if let Some(object_root) = object_root_opt { + SigningRoot { + object_root, + domain, + } + .tree_hash_root() + } else { + self.message.signing_root(domain) + }; + + self.signature.verify(message.as_bytes(), pubkey) + } + /// Convenience accessor for the block's slot. pub fn slot(&self) -> Slot { self.message.slot diff --git a/eth2/utils/bls/Cargo.toml b/eth2/utils/bls/Cargo.toml index 93df124921..15e93b190d 100644 --- a/eth2/utils/bls/Cargo.toml +++ b/eth2/utils/bls/Cargo.toml @@ -7,15 +7,15 @@ edition = "2018" [dependencies] milagro_bls = { git = "https://github.com/sigp/milagro_bls", tag = "v1.0.1" } eth2_hashing = "0.1.0" -hex = "0.3" -rand = "0.7.2" -serde = "1.0.102" -serde_derive = "1.0.102" +hex = "0.4.2" +rand = "0.7.3" +serde = "1.0.106" +serde_derive = "1.0.106" serde_hex = { path = "../serde_hex" } eth2_ssz = "0.1.2" eth2_ssz_types = { path = "../ssz_types" } tree_hash = "0.1.0" -arbitrary = { version = "0.4", features = ["derive"], optional = true } +arbitrary = { version = "0.4.4", features = ["derive"], optional = true } [features] fake_crypto = [] diff --git a/eth2/utils/cached_tree_hash/Cargo.toml b/eth2/utils/cached_tree_hash/Cargo.toml index 5ac4d5e81d..1bb86f0c30 100644 --- a/eth2/utils/cached_tree_hash/Cargo.toml +++ b/eth2/utils/cached_tree_hash/Cargo.toml @@ -5,17 +5,17 @@ authors = ["Michael Sproul "] edition = "2018" [dependencies] -ethereum-types = "0.9" +ethereum-types = "0.9.1" eth2_ssz_types = { path = "../ssz_types" } -eth2_hashing = "0.1" +eth2_hashing = "0.1.0" eth2_ssz_derive = "0.1.0" eth2_ssz = "0.1.2" -tree_hash = "0.1" -smallvec = "1.2.0" +tree_hash = "0.1.0" +smallvec = "1.4.0" [dev-dependencies] -quickcheck = "0.9" -quickcheck_macros = "0.8" +quickcheck = "0.9.2" +quickcheck_macros = "0.9.1" [features] arbitrary = ["ethereum-types/arbitrary"] diff --git a/eth2/utils/clap_utils/Cargo.toml b/eth2/utils/clap_utils/Cargo.toml index f1916c4ba4..b7c8cdfa47 100644 --- a/eth2/utils/clap_utils/Cargo.toml +++ b/eth2/utils/clap_utils/Cargo.toml @@ -8,8 +8,8 @@ edition = "2018" [dependencies] clap = "2.33.0" -hex = "0.3" -dirs = "2.0" +hex = "0.4.2" +dirs = "2.0.2" types = { path = "../../types" } eth2_testnet_config = { path = "../eth2_testnet_config" } -eth2_ssz = { path = "../ssz" } +eth2_ssz = "0.1.2" diff --git a/eth2/utils/compare_fields_derive/Cargo.toml b/eth2/utils/compare_fields_derive/Cargo.toml index 485b2708db..550615b146 100644 --- a/eth2/utils/compare_fields_derive/Cargo.toml +++ b/eth2/utils/compare_fields_derive/Cargo.toml @@ -8,5 +8,5 @@ edition = "2018" proc-macro = true [dependencies] -syn = "0.15" -quote = "0.6" +syn = "1.0.18" +quote = "1.0.4" diff --git a/eth2/utils/compare_fields_derive/src/lib.rs b/eth2/utils/compare_fields_derive/src/lib.rs index 15137efa37..a8f95a1bd9 100644 --- a/eth2/utils/compare_fields_derive/src/lib.rs +++ b/eth2/utils/compare_fields_derive/src/lib.rs @@ -8,7 +8,7 @@ use syn::{parse_macro_input, DeriveInput}; fn is_slice(field: &syn::Field) -> bool { field.attrs.iter().any(|attr| { attr.path.is_ident("compare_fields") - && attr.tts.to_string().replace(" ", "") == "(as_slice)" + && attr.tokens.to_string().replace(" ", "") == "(as_slice)" }) } diff --git a/eth2/utils/deposit_contract/Cargo.toml b/eth2/utils/deposit_contract/Cargo.toml index 0c3abacf55..14dafda2df 100644 --- a/eth2/utils/deposit_contract/Cargo.toml +++ b/eth2/utils/deposit_contract/Cargo.toml @@ -7,11 +7,11 @@ edition = "2018" build = "build.rs" [build-dependencies] -reqwest = "0.9.20" -serde_json = "1.0" +reqwest = { version = "0.10.4", features = ["blocking", "json"] } +serde_json = "1.0.52" [dependencies] types = { path = "../../types"} -eth2_ssz = { path = "../ssz"} -tree_hash = { path = "../tree_hash"} -ethabi = "12.0" +eth2_ssz = "0.1.2" +tree_hash = "0.1.0" +ethabi = "12.0.0" diff --git a/eth2/utils/deposit_contract/build.rs b/eth2/utils/deposit_contract/build.rs index 752b2e5bb8..096b3f7337 100644 --- a/eth2/utils/deposit_contract/build.rs +++ b/eth2/utils/deposit_contract/build.rs @@ -56,8 +56,8 @@ pub fn download_deposit_contract( if abi_file.exists() { // Nothing to do. } else { - match reqwest::get(url) { - Ok(mut response) => { + match reqwest::blocking::get(url) { + Ok(response) => { let mut abi_file = File::create(abi_file) .map_err(|e| format!("Failed to create local abi file: {:?}", e))?; let mut bytecode_file = File::create(bytecode_file) diff --git a/eth2/utils/eth2_config/Cargo.toml b/eth2/utils/eth2_config/Cargo.toml index c9af2fef3b..7f4cf6e0e3 100644 --- a/eth2/utils/eth2_config/Cargo.toml +++ b/eth2/utils/eth2_config/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Paul Hauner "] edition = "2018" [dependencies] -serde = "1.0.102" -serde_derive = "1.0.102" -toml = "0.5.4" +serde = "1.0.106" +serde_derive = "1.0.106" +toml = "0.5.6" types = { path = "../../types" } diff --git a/eth2/utils/eth2_hashing/Cargo.toml b/eth2/utils/eth2_hashing/Cargo.toml index 3047a7a4df..2ffb37c9e4 100644 --- a/eth2/utils/eth2_hashing/Cargo.toml +++ b/eth2/utils/eth2_hashing/Cargo.toml @@ -13,13 +13,13 @@ lazy_static = { version = "1.4.0", optional = true } ring = "0.16.9" [target.'cfg(target_arch = "wasm32")'.dependencies] -sha2 = "0.8.0" +sha2 = "0.8.1" [dev-dependencies] -rustc-hex = "2.0.1" +rustc-hex = "2.1.0" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] -wasm-bindgen-test = "0.3.2" +wasm-bindgen-test = "0.3.12" [features] default = ["zero_hash_cache"] diff --git a/eth2/utils/eth2_interop_keypairs/Cargo.toml b/eth2/utils/eth2_interop_keypairs/Cargo.toml index 30509a8f97..2cf4064a6f 100644 --- a/eth2/utils/eth2_interop_keypairs/Cargo.toml +++ b/eth2/utils/eth2_interop_keypairs/Cargo.toml @@ -8,13 +8,13 @@ edition = "2018" [dependencies] lazy_static = "1.4.0" -num-bigint = "0.2.3" +num-bigint = "0.2.6" eth2_hashing = "0.1.0" -hex = "0.3" +hex = "0.4.2" milagro_bls = { git = "https://github.com/sigp/milagro_bls", tag = "v1.0.1" } serde_yaml = "0.8.11" -serde = "1.0.102" -serde_derive = "1.0.102" +serde = "1.0.106" +serde_derive = "1.0.106" [dev-dependencies] -base64 = "0.11.0" +base64 = "0.12.0" diff --git a/eth2/utils/eth2_testnet_config/.gitignore b/eth2/utils/eth2_testnet_config/.gitignore index b407ad27e5..0f72c8a612 100644 --- a/eth2/utils/eth2_testnet_config/.gitignore +++ b/eth2/utils/eth2_testnet_config/.gitignore @@ -1 +1,2 @@ testnet* +schlesi-* diff --git a/eth2/utils/eth2_testnet_config/Cargo.toml b/eth2/utils/eth2_testnet_config/Cargo.toml index 3df6897a49..872da2ac71 100644 --- a/eth2/utils/eth2_testnet_config/Cargo.toml +++ b/eth2/utils/eth2_testnet_config/Cargo.toml @@ -7,15 +7,14 @@ edition = "2018" build = "build.rs" [build-dependencies] -reqwest = "0.9.20" +reqwest = { version = "0.10.4", features = ["blocking"] } [dev-dependencies] -tempdir = "0.3" -reqwest = "0.9.20" +tempdir = "0.3.7" [dependencies] -serde = "1.0" -serde_yaml = "0.8" +serde = "1.0.106" +serde_yaml = "0.8.11" types = { path = "../../types"} eth2-libp2p = { path = "../../../beacon_node/eth2-libp2p"} -eth2_ssz = { path = "../ssz"} +eth2_ssz = "0.1.2" diff --git a/eth2/utils/eth2_testnet_config/build.rs b/eth2/utils/eth2_testnet_config/build.rs index e2f7d71b39..6a01b00057 100644 --- a/eth2/utils/eth2_testnet_config/build.rs +++ b/eth2/utils/eth2_testnet_config/build.rs @@ -1,11 +1,12 @@ -/// Pulls down the latest Lighthouse testnet from https://github.com/eth2-clients/eth2-testnets +//! Downloads a testnet configuration from Github. + use reqwest; use std::env; use std::fs::File; use std::io::Write; use std::path::PathBuf; -const TESTNET_ID: &str = "testnet5"; +const TESTNET_ID: &str = "schlesi-v0-11"; fn main() { if !base_dir().exists() { @@ -37,19 +38,26 @@ pub fn get_all_files() -> Result<(), String> { pub fn get_file(filename: &str) -> Result<(), String> { let url = format!( - "https://raw.githubusercontent.com/eth2-clients/eth2-testnets/master/lighthouse/{}/{}", - TESTNET_ID, filename + "https://raw.githubusercontent.com/goerli/schlesi/839866fe29a1b4df3a87bfe2ff1257c8a58671c9/light/{}", + filename ); let path = base_dir().join(filename); let mut file = File::create(path).map_err(|e| format!("Failed to create {}: {:?}", filename, e))?; - let mut response = - reqwest::get(&url).map_err(|e| format!("Failed to download {}: {}", filename, e))?; - let mut contents: Vec = vec![]; - response - .copy_to(&mut contents) + let request = reqwest::blocking::Client::builder() + .build() + .map_err(|_| "Could not build request client".to_string())? + .get(&url) + .timeout(std::time::Duration::from_secs(120)); + + let contents = request + .send() + .map_err(|e| format!("Failed to download {}: {}", filename, e))? + .error_for_status() + .map_err(|e| format!("Error downloading {}: {}", filename, e))? + .bytes() .map_err(|e| format!("Failed to read {} response bytes: {}", filename, e))?; file.write(&contents) diff --git a/eth2/utils/eth2_testnet_config/src/lib.rs b/eth2/utils/eth2_testnet_config/src/lib.rs index 0b3d476749..0237295dbf 100644 --- a/eth2/utils/eth2_testnet_config/src/lib.rs +++ b/eth2/utils/eth2_testnet_config/src/lib.rs @@ -20,11 +20,14 @@ pub const BOOT_ENR_FILE: &str = "boot_enr.yaml"; pub const GENESIS_STATE_FILE: &str = "genesis.ssz"; pub const YAML_CONFIG_FILE: &str = "config.yaml"; -pub const HARDCODED_YAML_CONFIG: &[u8] = include_bytes!("../testnet5/config.yaml"); -pub const HARDCODED_DEPLOY_BLOCK: &[u8] = include_bytes!("../testnet5/deploy_block.txt"); -pub const HARDCODED_DEPOSIT_CONTRACT: &[u8] = include_bytes!("../testnet5/deposit_contract.txt"); -pub const HARDCODED_GENESIS_STATE: &[u8] = include_bytes!("../testnet5/genesis.ssz"); -pub const HARDCODED_BOOT_ENR: &[u8] = include_bytes!("../testnet5/boot_enr.yaml"); +pub const HARDCODED_TESTNET: &str = "schlesi-v0-11"; + +pub const HARDCODED_YAML_CONFIG: &[u8] = include_bytes!("../schlesi-v0-11/config.yaml"); +pub const HARDCODED_DEPLOY_BLOCK: &[u8] = include_bytes!("../schlesi-v0-11/deploy_block.txt"); +pub const HARDCODED_DEPOSIT_CONTRACT: &[u8] = + include_bytes!("../schlesi-v0-11/deposit_contract.txt"); +pub const HARDCODED_GENESIS_STATE: &[u8] = include_bytes!("../schlesi-v0-11/genesis.ssz"); +pub const HARDCODED_BOOT_ENR: &[u8] = include_bytes!("../schlesi-v0-11/boot_enr.yaml"); /// Specifies an Eth2 testnet. /// diff --git a/eth2/utils/hashset_delay/Cargo.toml b/eth2/utils/hashset_delay/Cargo.toml index 3a424a79e7..48681d225b 100644 --- a/eth2/utils/hashset_delay/Cargo.toml +++ b/eth2/utils/hashset_delay/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" [dependencies] futures = "0.3.4" -tokio = { version = "0.2.19", features = ["time"] } +tokio = { version = "0.2.20", features = ["time"] } [dev-dependencies] -tokio = { version = "0.2.19", features = ["time", "rt-threaded", "macros"] } +tokio = { version = "0.2.20", features = ["time", "rt-threaded", "macros"] } diff --git a/eth2/utils/int_to_bytes/Cargo.toml b/eth2/utils/int_to_bytes/Cargo.toml index c24f657c67..87839ccaa9 100644 --- a/eth2/utils/int_to_bytes/Cargo.toml +++ b/eth2/utils/int_to_bytes/Cargo.toml @@ -5,8 +5,8 @@ authors = ["Paul Hauner "] edition = "2018" [dependencies] -bytes = "0.4.12" +bytes = "0.5.4" [dev-dependencies] yaml-rust = "0.4.3" -hex = "0.3" +hex = "0.4.2" diff --git a/eth2/utils/lighthouse_metrics/Cargo.toml b/eth2/utils/lighthouse_metrics/Cargo.toml index ed4b492533..1e804c8d3b 100644 --- a/eth2/utils/lighthouse_metrics/Cargo.toml +++ b/eth2/utils/lighthouse_metrics/Cargo.toml @@ -8,4 +8,4 @@ edition = "2018" [dependencies] lazy_static = "1.4.0" -prometheus = "0.7.0" +prometheus = "0.8.0" diff --git a/eth2/utils/logging/Cargo.toml b/eth2/utils/logging/Cargo.toml index c7ccd3617f..b3c50c6d6d 100644 --- a/eth2/utils/logging/Cargo.toml +++ b/eth2/utils/logging/Cargo.toml @@ -6,6 +6,6 @@ edition = "2018" [dependencies] slog = "2.5.2" -slog-term = "2.4.2" +slog-term = "2.5.0" lighthouse_metrics = { path = "../lighthouse_metrics" } lazy_static = "1.4.0" diff --git a/eth2/utils/merkle_proof/Cargo.toml b/eth2/utils/merkle_proof/Cargo.toml index e56fab7f31..84745d224b 100644 --- a/eth2/utils/merkle_proof/Cargo.toml +++ b/eth2/utils/merkle_proof/Cargo.toml @@ -5,14 +5,14 @@ authors = ["Michael Sproul "] edition = "2018" [dependencies] -ethereum-types = "0.9" +ethereum-types = "0.9.1" eth2_hashing = "0.1.0" lazy_static = "1.4.0" safe_arith = { path = "../safe_arith" } [dev-dependencies] -quickcheck = "0.9.0" -quickcheck_macros = "0.8.0" +quickcheck = "0.9.2" +quickcheck_macros = "0.9.1" [features] arbitrary = ["ethereum-types/arbitrary"] diff --git a/eth2/utils/remote_beacon_node/Cargo.toml b/eth2/utils/remote_beacon_node/Cargo.toml index 73d2bb8924..fcb981f83e 100644 --- a/eth2/utils/remote_beacon_node/Cargo.toml +++ b/eth2/utils/remote_beacon_node/Cargo.toml @@ -7,15 +7,15 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -reqwest = { version = "0.10", features = ["json"] } -url = "1.2" -serde = "1.0" +reqwest = { version = "0.10.4", features = ["json"] } +url = "2.1.1" +serde = "1.0.106" futures = "0.3.4" types = { path = "../../../eth2/types" } rest_types = { path = "../rest_types" } -hex = "0.3" -eth2_ssz = { path = "../../../eth2/utils/ssz" } -serde_json = "^1.0" +hex = "0.4.2" +eth2_ssz = "0.1.2" +serde_json = "1.0.52" eth2_config = { path = "../../../eth2/utils/eth2_config" } proto_array_fork_choice = { path = "../../../eth2/proto_array_fork_choice" } operation_pool = { path = "../../../eth2/operation_pool" } diff --git a/eth2/utils/rest_types/Cargo.toml b/eth2/utils/rest_types/Cargo.toml index c2745c0603..b037f90470 100644 --- a/eth2/utils/rest_types/Cargo.toml +++ b/eth2/utils/rest_types/Cargo.toml @@ -6,11 +6,11 @@ edition = "2018" [dependencies] types = { path = "../../types" } -eth2_ssz_derive = { path = "../ssz_derive" } -eth2_ssz = { path = "../ssz" } -eth2_hashing = { path = "../eth2_hashing" } -tree_hash = { path = "../tree_hash" } +eth2_ssz_derive = "0.1.0" +eth2_ssz = "0.1.2" +eth2_hashing = "0.1.0" +tree_hash = "0.1.0" state_processing = { path = "../../state_processing" } bls = { path = "../bls" } -serde = { version = "1.0.102", features = ["derive"] } +serde = { version = "1.0.106", features = ["derive"] } rayon = "1.3.0" diff --git a/eth2/utils/serde_hex/Cargo.toml b/eth2/utils/serde_hex/Cargo.toml index b4d7bf619a..05998c8eea 100644 --- a/eth2/utils/serde_hex/Cargo.toml +++ b/eth2/utils/serde_hex/Cargo.toml @@ -5,5 +5,5 @@ authors = ["Paul Hauner "] edition = "2018" [dependencies] -serde = "1.0.102" -hex = "0.3" +serde = "1.0.106" +hex = "0.4.2" diff --git a/eth2/utils/serde_hex/src/lib.rs b/eth2/utils/serde_hex/src/lib.rs index 7b254cf88c..db84222757 100644 --- a/eth2/utils/serde_hex/src/lib.rs +++ b/eth2/utils/serde_hex/src/lib.rs @@ -1,17 +1,10 @@ -use hex::ToHex; use serde::de::{self, Visitor}; use std::fmt; pub fn encode>(data: T) -> String { - let mut hex = String::with_capacity(data.as_ref().len() * 2); - - // Writing to a string never errors, so we can unwrap here. - data.write_hex(&mut hex).unwrap(); - + let hex = hex::encode(data); let mut s = "0x".to_string(); - s.push_str(hex.as_str()); - s } diff --git a/eth2/utils/slot_clock/Cargo.toml b/eth2/utils/slot_clock/Cargo.toml index 81a7b57a9d..40da33af2d 100644 --- a/eth2/utils/slot_clock/Cargo.toml +++ b/eth2/utils/slot_clock/Cargo.toml @@ -8,4 +8,4 @@ edition = "2018" types = { path = "../../types" } lazy_static = "1.4.0" lighthouse_metrics = { path = "../lighthouse_metrics" } -parking_lot = "0.9.0" +parking_lot = "0.10.2" diff --git a/eth2/utils/ssz/Cargo.toml b/eth2/utils/ssz/Cargo.toml index 162c91ae13..e9220cdc64 100644 --- a/eth2/utils/ssz/Cargo.toml +++ b/eth2/utils/ssz/Cargo.toml @@ -14,7 +14,7 @@ eth2_ssz_derive = "0.1.0" [dependencies] ethereum-types = "0.9.1" -smallvec = "1.2.0" +smallvec = "1.4.0" [features] arbitrary = ["ethereum-types/arbitrary"] diff --git a/eth2/utils/ssz_derive/Cargo.toml b/eth2/utils/ssz_derive/Cargo.toml index db25e16be0..e074f001a8 100644 --- a/eth2/utils/ssz_derive/Cargo.toml +++ b/eth2/utils/ssz_derive/Cargo.toml @@ -11,5 +11,5 @@ name = "ssz_derive" proc-macro = true [dependencies] -syn = "0.15" -quote = "0.6" +syn = "1.0.18" +quote = "1.0.4" diff --git a/eth2/utils/ssz_derive/src/lib.rs b/eth2/utils/ssz_derive/src/lib.rs index 04ef8b9826..ae350a1cbb 100644 --- a/eth2/utils/ssz_derive/src/lib.rs +++ b/eth2/utils/ssz_derive/src/lib.rs @@ -54,7 +54,8 @@ fn get_serializable_field_types<'a>(struct_data: &'a syn::DataStruct) -> Vec<&'a /// The field attribute is: `#[ssz(skip_serializing)]` fn should_skip_serializing(field: &syn::Field) -> bool { field.attrs.iter().any(|attr| { - attr.path.is_ident("ssz") && attr.tts.to_string().replace(" ", "") == "(skip_serializing)" + attr.path.is_ident("ssz") + && attr.tokens.to_string().replace(" ", "") == "(skip_serializing)" }) } @@ -148,7 +149,8 @@ pub fn ssz_encode_derive(input: TokenStream) -> TokenStream { /// The field attribute is: `#[ssz(skip_deserializing)]` fn should_skip_deserializing(field: &syn::Field) -> bool { field.attrs.iter().any(|attr| { - attr.path.is_ident("ssz") && attr.tts.to_string().replace(" ", "") == "(skip_deserializing)" + attr.path.is_ident("ssz") + && attr.tokens.to_string().replace(" ", "") == "(skip_deserializing)" }) } diff --git a/eth2/utils/ssz_types/Cargo.toml b/eth2/utils/ssz_types/Cargo.toml index eb7ffb4835..9680ce6f69 100644 --- a/eth2/utils/ssz_types/Cargo.toml +++ b/eth2/utils/ssz_types/Cargo.toml @@ -9,13 +9,13 @@ name = "ssz_types" [dependencies] tree_hash = "0.1.0" -serde = "1.0.102" -serde_derive = "1.0.102" +serde = "1.0.106" +serde_derive = "1.0.106" serde_hex = { path = "../serde_hex" } eth2_ssz = "0.1.2" -typenum = "1.11.2" -arbitrary = { version = "0.4", features = ["derive"], optional = true } +typenum = "1.12.0" +arbitrary = { version = "0.4.4", features = ["derive"], optional = true } [dev-dependencies] serde_yaml = "0.8.11" -tree_hash_derive = "0.2" +tree_hash_derive = "0.2.0" diff --git a/eth2/utils/swap_or_not_shuffle/Cargo.toml b/eth2/utils/swap_or_not_shuffle/Cargo.toml index e624964168..da2b72664d 100644 --- a/eth2/utils/swap_or_not_shuffle/Cargo.toml +++ b/eth2/utils/swap_or_not_shuffle/Cargo.toml @@ -9,9 +9,9 @@ name = "benches" harness = false [dev-dependencies] -criterion = "0.3.0" +criterion = "0.3.2" yaml-rust = "0.4.3" -hex = "0.3" +hex = "0.4.2" [dependencies] eth2_hashing = "0.1.0" diff --git a/eth2/utils/test_random_derive/Cargo.toml b/eth2/utils/test_random_derive/Cargo.toml index 494e9d8ebf..a02cb7fdad 100644 --- a/eth2/utils/test_random_derive/Cargo.toml +++ b/eth2/utils/test_random_derive/Cargo.toml @@ -9,5 +9,5 @@ description = "Procedural derive macros for implementation of TestRandom trait" proc-macro = true [dependencies] -syn = "0.15" -quote = "0.6" +syn = "1.0.18" +quote = "1.0.4" diff --git a/eth2/utils/test_random_derive/src/lib.rs b/eth2/utils/test_random_derive/src/lib.rs index d6e3a0f950..fabc61c7fd 100644 --- a/eth2/utils/test_random_derive/src/lib.rs +++ b/eth2/utils/test_random_derive/src/lib.rs @@ -10,7 +10,7 @@ use syn::{parse_macro_input, DeriveInput}; /// The field attribute is: `#[test_random(default)]` fn should_use_default(field: &syn::Field) -> bool { field.attrs.iter().any(|attr| { - attr.path.is_ident("test_random") && attr.tts.to_string().replace(" ", "") == "(default)" + attr.path.is_ident("test_random") && attr.tokens.to_string().replace(" ", "") == "(default)" }) } diff --git a/eth2/utils/tree_hash/Cargo.toml b/eth2/utils/tree_hash/Cargo.toml index db207ab4ac..ff84952e6f 100644 --- a/eth2/utils/tree_hash/Cargo.toml +++ b/eth2/utils/tree_hash/Cargo.toml @@ -11,16 +11,16 @@ name = "benches" harness = false [dev-dependencies] -criterion = "0.3.0" -rand = "0.7.2" -tree_hash_derive = "0.2" +criterion = "0.3.2" +rand = "0.7.3" +tree_hash_derive = "0.2.0" types = { path = "../../types" } lazy_static = "1.4.0" [dependencies] -ethereum-types = "0.9" +ethereum-types = "0.9.1" eth2_hashing = "0.1.0" -smallvec = "1.2.0" +smallvec = "1.4.0" [features] arbitrary = ["ethereum-types/arbitrary"] diff --git a/eth2/utils/tree_hash_derive/Cargo.toml b/eth2/utils/tree_hash_derive/Cargo.toml index 9c3050e57f..11caabe076 100644 --- a/eth2/utils/tree_hash_derive/Cargo.toml +++ b/eth2/utils/tree_hash_derive/Cargo.toml @@ -10,5 +10,5 @@ license = "Apache-2.0" proc-macro = true [dependencies] -syn = "0.15" -quote = "0.6" +syn = "1.0.18" +quote = "1.0.4" diff --git a/eth2/utils/tree_hash_derive/src/lib.rs b/eth2/utils/tree_hash_derive/src/lib.rs index e233e4ed57..d57903654d 100644 --- a/eth2/utils/tree_hash_derive/src/lib.rs +++ b/eth2/utils/tree_hash_derive/src/lib.rs @@ -51,7 +51,7 @@ fn get_cache_field_for(field: &syn::Field) -> Option { let parsed_attrs = cached_tree_hash_attr_metas(&field.attrs); if let [Meta::List(MetaList { nested, .. })] = &parsed_attrs[..] { nested.iter().find_map(|x| match x { - NestedMeta::Meta(Meta::Word(cache_field_ident)) => Some(cache_field_ident.clone()), + NestedMeta::Meta(Meta::Path(path)) => path.get_ident().cloned(), _ => None, }) } else { @@ -73,7 +73,8 @@ fn cached_tree_hash_attr_metas(attrs: &[Attribute]) -> Vec { /// The field attribute is: `#[tree_hash(skip_hashing)]` fn should_skip_hashing(field: &syn::Field) -> bool { field.attrs.iter().any(|attr| { - attr.path.is_ident("tree_hash") && attr.tts.to_string().replace(" ", "") == "(skip_hashing)" + attr.path.is_ident("tree_hash") + && attr.tokens.to_string().replace(" ", "") == "(skip_hashing)" }) } diff --git a/lcli/Cargo.toml b/lcli/Cargo.toml index 7a423d8caa..fa3a758981 100644 --- a/lcli/Cargo.toml +++ b/lcli/Cargo.toml @@ -9,24 +9,24 @@ edition = "2018" [dependencies] clap = "2.33.0" -hex = "0.3" +hex = "0.4.2" log = "0.4.8" -serde = "1.0.102" +serde = "1.0.106" serde_yaml = "0.8.11" -simple_logger = "1.3.0" +simple_logger = "1.6.0" types = { path = "../eth2/types" } state_processing = { path = "../eth2/state_processing" } eth2_ssz = "0.1.2" -regex = "1.3.1" +regex = "1.3.7" eth1_test_rig = { path = "../tests/eth1_test_rig" } -futures = {version = "0.3", features = ["compat"]} +futures = { version = "0.3.4", features = ["compat"] } environment = { path = "../lighthouse/environment" } web3 = "0.10.0" eth2_testnet_config = { path = "../eth2/utils/eth2_testnet_config" } -dirs = "2.0" +dirs = "2.0.2" genesis = { path = "../beacon_node/genesis" } deposit_contract = { path = "../eth2/utils/deposit_contract" } -tree_hash = { path = "../eth2/utils/tree_hash" } -tokio = { version = "0.2", features = ["full"] } +tree_hash = "0.1.0" +tokio = { version = "0.2.20", features = ["full"] } clap_utils = { path = "../eth2/utils/clap_utils" } eth2-libp2p = { path = "../beacon_node/eth2-libp2p" } diff --git a/lcli/src/generate_bootnode_enr.rs b/lcli/src/generate_bootnode_enr.rs index 2d6e685a62..27f5dda278 100644 --- a/lcli/src/generate_bootnode_enr.rs +++ b/lcli/src/generate_bootnode_enr.rs @@ -1,6 +1,6 @@ use clap::ArgMatches; use eth2_libp2p::{ - discovery::{build_enr, CombinedKey, Keypair, ENR_FILENAME}, + discovery::{build_enr, CombinedKey, CombinedKeyExt, Keypair, ENR_FILENAME}, NetworkConfig, NETWORK_KEY_FILENAME, }; use std::convert::TryInto; @@ -30,10 +30,7 @@ pub fn run(matches: &ArgMatches) -> Result<(), String> { config.enr_tcp_port = Some(tcp_port); let local_keypair = Keypair::generate_secp256k1(); - let enr_key: CombinedKey = local_keypair - .clone() - .try_into() - .map_err(|e| format!("Unable to convert keypair: {:?}", e))?; + let enr_key = CombinedKey::from_libp2p(&local_keypair)?; let enr = build_enr::(&enr_key, &config, EnrForkId::default()) .map_err(|e| format!("Unable to create ENR: {:?}", e))?; diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index e93dd01926..b0db254e78 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -9,17 +9,18 @@ write_ssz_files = ["beacon_node/write_ssz_files"] # Writes debugging .ssz files [dependencies] beacon_node = { "path" = "../beacon_node" } -tokio = "0.1.22" -slog = { version = "^2.2.3" , features = ["max_level_trace"] } -sloggers = "0.3.4" +tokio = "0.2.20" +slog = { version = "2.5.2", features = ["max_level_trace"] } +sloggers = "1.0.0" types = { "path" = "../eth2/types" } -clap = "2.32.0" -env_logger = "0.6.1" +clap = "2.33.0" +env_logger = "0.7.1" logging = { path = "../eth2/utils/logging" } -slog-term = "^2.4.0" -slog-async = "^2.3.0" +slog-term = "2.5.0" +slog-async = "2.5.0" environment = { path = "./environment" } -futures = "0.1.25" +futures = "0.3.4" validator_client = { "path" = "../validator_client" } account_manager = { "path" = "../account_manager" } clap_utils = { path = "../eth2/utils/clap_utils" } +eth2_testnet_config = { path = "../eth2/utils/eth2_testnet_config" } diff --git a/lighthouse/environment/Cargo.toml b/lighthouse/environment/Cargo.toml index 7f4c250945..c34f2f9d1a 100644 --- a/lighthouse/environment/Cargo.toml +++ b/lighthouse/environment/Cargo.toml @@ -6,19 +6,19 @@ edition = "2018" [dependencies] clap = "2.33.0" -tokio = "0.1.22" -slog = { version = "^2.2.3" , features = ["max_level_trace"] } -sloggers = "0.3.4" +tokio = "0.2.20" +slog = { version = "2.5.2", features = ["max_level_trace"] } +sloggers = "1.0.0" types = { "path" = "../../eth2/types" } eth2_config = { "path" = "../../eth2/utils/eth2_config" } eth2_testnet_config = { path = "../../eth2/utils/eth2_testnet_config" } -env_logger = "0.6.1" +env_logger = "0.7.1" logging = { path = "../../eth2/utils/logging" } -slog-term = "^2.4.0" -slog-async = "^2.3.0" -ctrlc = { version = "3.1.1", features = ["termination"] } -futures = "0.1.25" -parking_lot = "0.7" +slog-term = "2.5.0" +slog-async = "2.5.0" +ctrlc = { version = "3.1.4", features = ["termination"] } +futures = "0.3.4" +parking_lot = "0.10.2" slog-json = "2.3.0" [dev-dependencies] diff --git a/lighthouse/environment/src/lib.rs b/lighthouse/environment/src/lib.rs index 20e54b0979..1ed249cba9 100644 --- a/lighthouse/environment/src/lib.rs +++ b/lighthouse/environment/src/lib.rs @@ -9,7 +9,7 @@ use eth2_config::Eth2Config; use eth2_testnet_config::Eth2TestnetConfig; -use futures::{sync::oneshot, Future}; +use futures::channel::oneshot; use slog::{info, o, Drain, Level, Logger}; use sloggers::{null::NullLoggerBuilder, Build}; use std::cell::RefCell; @@ -17,7 +17,7 @@ use std::ffi::OsStr; use std::fs::{rename as FsRename, OpenOptions}; use std::path::PathBuf; use std::time::{SystemTime, UNIX_EPOCH}; -use tokio::runtime::{Builder as RuntimeBuilder, Runtime, TaskExecutor}; +use tokio::runtime::{Builder as RuntimeBuilder, Handle, Runtime}; use types::{EthSpec, InteropEthSpec, MainnetEthSpec, MinimalEthSpec}; pub const ETH2_CONFIG_FILENAME: &str = "eth2-spec.toml"; @@ -183,10 +183,10 @@ impl EnvironmentBuilder { /// An execution context that can be used by a service. /// /// Distinct from an `Environment` because a `Context` is not able to give a mutable reference to a -/// `Runtime`, instead it only has access to a `TaskExecutor`. +/// `Runtime`, instead it only has access to a `Runtime`. #[derive(Clone)] pub struct RuntimeContext { - pub executor: TaskExecutor, + pub runtime_handle: Handle, pub log: Logger, pub eth_spec_instance: E, pub eth2_config: Eth2Config, @@ -198,7 +198,7 @@ impl RuntimeContext { /// The generated service will have the `service_name` in all it's logs. pub fn service_context(&self, service_name: String) -> Self { Self { - executor: self.executor.clone(), + runtime_handle: self.runtime_handle.clone(), log: self.log.new(o!("service" => service_name)), eth_spec_instance: self.eth_spec_instance.clone(), eth2_config: self.eth2_config.clone(), @@ -233,7 +233,7 @@ impl Environment { /// Returns a `Context` where no "service" has been added to the logger output. pub fn core_context(&mut self) -> RuntimeContext { RuntimeContext { - executor: self.runtime.executor(), + runtime_handle: self.runtime.handle().clone(), log: self.log.clone(), eth_spec_instance: self.eth_spec_instance.clone(), eth2_config: self.eth2_config.clone(), @@ -243,7 +243,7 @@ impl Environment { /// Returns a `Context` where the `service_name` is added to the logger output. pub fn service_context(&mut self, service_name: String) -> RuntimeContext { RuntimeContext { - executor: self.runtime.executor(), + runtime_handle: self.runtime.handle().clone(), log: self.log.new(o!("service" => service_name)), eth_spec_instance: self.eth_spec_instance.clone(), eth2_config: self.eth2_config.clone(), @@ -268,11 +268,9 @@ impl Environment { } /// Shutdown the `tokio` runtime when all tasks are idle. - pub fn shutdown_on_idle(self) -> Result<(), String> { + pub fn shutdown_on_idle(self) { self.runtime - .shutdown_on_idle() - .wait() - .map_err(|e| format!("Tokio runtime shutdown returned an error: {:?}", e)) + .shutdown_timeout(std::time::Duration::from_secs(5)) } /// Sets the logger (and all child loggers) to log to a file. diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index dbb3c90395..b98e8e8336 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -6,6 +6,7 @@ use clap::{App, Arg, ArgMatches}; use clap_utils; use env_logger::{Builder, Env}; use environment::EnvironmentBuilder; +use eth2_testnet_config::HARDCODED_TESTNET; use slog::{crit, info, warn}; use std::path::PathBuf; use std::process::exit; @@ -156,6 +157,14 @@ fn run( "Ethereum 2.0 is pre-release. This software is experimental." ); + if !matches.is_present("testnet-dir") { + info!( + log, + "Using default testnet"; + "default" => HARDCODED_TESTNET + ) + } + // Note: the current code technically allows for starting a beacon node _and_ a validator // client at the same time. // diff --git a/tests/ef_tests/Cargo.toml b/tests/ef_tests/Cargo.toml index 252cf1f90d..83178b48d5 100644 --- a/tests/ef_tests/Cargo.toml +++ b/tests/ef_tests/Cargo.toml @@ -12,19 +12,19 @@ fake_crypto = ["bls/fake_crypto"] [dependencies] bls = { path = "../../eth2/utils/bls" } compare_fields = { path = "../../eth2/utils/compare_fields" } -ethereum-types = "0.9" -hex = "0.3" -rayon = "1.2.0" -serde = "1.0.102" -serde_derive = "1.0.102" +ethereum-types = "0.9.1" +hex = "0.4.2" +rayon = "1.3.0" +serde = "1.0.106" +serde_derive = "1.0.106" serde_repr = "0.1.5" serde_yaml = "0.8.11" eth2_ssz = "0.1.2" eth2_ssz_derive = "0.1.0" tree_hash = "0.1.0" -tree_hash_derive = "0.2" +tree_hash_derive = "0.2.0" cached_tree_hash = { path = "../../eth2/utils/cached_tree_hash" } state_processing = { path = "../../eth2/state_processing" } swap_or_not_shuffle = { path = "../../eth2/utils/swap_or_not_shuffle" } types = { path = "../../eth2/types" } -walkdir = "2.2.9" +walkdir = "2.3.1" diff --git a/tests/eth1_test_rig/Cargo.toml b/tests/eth1_test_rig/Cargo.toml index 90dc58436f..ab0a8bcf00 100644 --- a/tests/eth1_test_rig/Cargo.toml +++ b/tests/eth1_test_rig/Cargo.toml @@ -6,8 +6,8 @@ edition = "2018" [dependencies] web3 = "0.10.0" -tokio = { version = "0.2", features = ["time"] } -futures = { version = "0.3", features = ["compat"] } +tokio = { version = "0.2.20", features = ["time"] } +futures = { version = "0.3.4", features = ["compat"] } types = { path = "../../eth2/types"} -serde_json = "1.0" +serde_json = "1.0.52" deposit_contract = { path = "../../eth2/utils/deposit_contract"} diff --git a/tests/node_test_rig/Cargo.toml b/tests/node_test_rig/Cargo.toml index 013cbe0274..472378279b 100644 --- a/tests/node_test_rig/Cargo.toml +++ b/tests/node_test_rig/Cargo.toml @@ -9,11 +9,11 @@ environment = { path = "../../lighthouse/environment" } beacon_node = { path = "../../beacon_node" } types = { path = "../../eth2/types" } eth2_config = { path = "../../eth2/utils/eth2_config" } -tempdir = "0.3" -reqwest = "0.9" -url = "1.2" -serde = "1.0" -futures = "0.1.25" +tempdir = "0.3.7" +reqwest = "0.10.4" +url = "2.1.1" +serde = "1.0.106" +futures = "0.3.4" genesis = { path = "../../beacon_node/genesis" } remote_beacon_node = { path = "../../eth2/utils/remote_beacon_node" } validator_client = { path = "../../validator_client" } diff --git a/tests/simulator/Cargo.toml b/tests/simulator/Cargo.toml index 65b6614705..f5d1565fb8 100644 --- a/tests/simulator/Cargo.toml +++ b/tests/simulator/Cargo.toml @@ -10,9 +10,9 @@ edition = "2018" node_test_rig = { path = "../node_test_rig" } types = { path = "../../eth2/types" } validator_client = { path = "../../validator_client" } -parking_lot = "0.9.0" -futures = "0.1.29" -tokio = "0.1.22" +parking_lot = "0.10.2" +futures = "0.3.4" +tokio = "0.2.20" eth1_test_rig = { path = "../eth1_test_rig" } env_logger = "0.7.1" clap = "2.33.0" diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index 68dfc906d0..657726adda 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -17,28 +17,27 @@ eth2_interop_keypairs = { path = "../eth2/utils/eth2_interop_keypairs" } slot_clock = { path = "../eth2/utils/slot_clock" } rest_types = { path = "../eth2/utils/rest_types" } types = { path = "../eth2/types" } -serde = "1.0.102" -serde_derive = "1.0.102" -serde_json = "1.0.41" +serde = "1.0.106" +serde_derive = "1.0.106" +serde_json = "1.0.52" slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_trace"] } -slog-async = "2.3.0" -slog-term = "2.4.2" -tokio = "0.1.22" -tokio-timer = "0.2.12" -error-chain = "0.12.1" -bincode = "1.2.0" -futures = "0.1.29" +slog-async = "2.5.0" +slog-term = "2.5.0" +tokio = {version = "0.2.20", features = ["time"]} +error-chain = "0.12.2" +bincode = "1.2.1" +futures = {version ="0.3.4", features = ["compat"]} dirs = "2.0.2" logging = { path = "../eth2/utils/logging" } environment = { path = "../lighthouse/environment" } -parking_lot = "0.7" -exit-future = "0.1.4" -libc = "0.2.65" -eth2_ssz_derive = { path = "../eth2/utils/ssz_derive" } -hex = "0.3" +parking_lot = "0.10.2" +exit-future = "0.2.0" +libc = "0.2.69" +eth2_ssz_derive = "0.1.0" +hex = "0.4.2" deposit_contract = { path = "../eth2/utils/deposit_contract" } bls = { path = "../eth2/utils/bls" } remote_beacon_node = { path = "../eth2/utils/remote_beacon_node" } -tempdir = "0.3" -rayon = "1.2.0" +tempdir = "0.3.7" +rayon = "1.3.0" web3 = "0.10.0" diff --git a/validator_client/src/attestation_service.rs b/validator_client/src/attestation_service.rs index c1842519b5..5ef22b6c07 100644 --- a/validator_client/src/attestation_service.rs +++ b/validator_client/src/attestation_service.rs @@ -1,19 +1,17 @@ use crate::{ - duties_service::{DutiesService, DutyAndState}, + duties_service::{DutiesService, DutyAndProof}, validator_store::ValidatorStore, }; use environment::RuntimeContext; use exit_future::Signal; -use futures::{future, Future, Stream}; +use futures::{FutureExt, StreamExt}; use remote_beacon_node::{PublishStatus, RemoteBeaconNode}; -use rest_types::ValidatorSubscription; use slog::{crit, debug, info, trace}; use slot_clock::SlotClock; use std::collections::HashMap; use std::ops::Deref; use std::sync::Arc; -use std::time::{Duration, Instant}; -use tokio::timer::{Delay, Interval}; +use tokio::time::{delay_until, interval_at, Duration, Instant}; use types::{Attestation, ChainSpec, CommitteeIndex, EthSpec, Slot}; /// Builds an `AttestationService`. @@ -131,7 +129,8 @@ impl AttestationService { .ok_or_else(|| "Unable to determine duration to next slot".to_string())?; let interval = { - Interval::new( + // Note: `interval_at` panics if `slot_duration` is 0 + interval_at( Instant::now() + duration_to_next_slot + slot_duration / 3, slot_duration, ) @@ -141,38 +140,28 @@ impl AttestationService { let service = self.clone(); let log_1 = log.clone(); let log_2 = log.clone(); - let log_3 = log.clone(); - context.executor.spawn( - exit_fut - .until( - interval - .map_err(move |e| { - crit! { - log_1, - "Timer thread failed"; - "error" => format!("{}", e) - } - }) - .for_each(move |_| { - if let Err(e) = service.spawn_attestation_tasks(slot_duration) { - crit!( - log_2, - "Failed to spawn attestation tasks"; - "error" => e - ) - } else { - trace!( - log_2, - "Spawned attestation tasks"; - ) - } - - Ok(()) - }), + let interval_fut = interval.for_each(move |_| { + if let Err(e) = service.spawn_attestation_tasks(slot_duration) { + crit!( + log_1, + "Failed to spawn attestation tasks"; + "error" => e ) - .map(move |_| info!(log_3, "Shutdown complete")), + } else { + trace!( + log_1, + "Spawned attestation tasks"; + ) + } + futures::future::ready(()) + }); + + let future = futures::future::select( + interval_fut, + exit_fut.map(move |_| info!(log_2, "Shutdown complete")), ); + tokio::task::spawn(future); Ok(exit_signal) } @@ -182,11 +171,11 @@ impl AttestationService { fn spawn_attestation_tasks(&self, slot_duration: Duration) -> Result<(), String> { let service = self.clone(); - let slot = service + let slot = self .slot_clock .now() .ok_or_else(|| "Failed to read slot clock".to_string())?; - let duration_to_next_slot = service + let duration_to_next_slot = self .slot_clock .duration_to_next_slot() .ok_or_else(|| "Unable to determine duration to next slot".to_string())?; @@ -198,31 +187,15 @@ impl AttestationService { .checked_sub(slot_duration / 3) .unwrap_or_else(|| Duration::from_secs(0)); - let epoch = slot.epoch(E::slots_per_epoch()); - // Check if any attestation subscriptions are required. If there a new attestation duties for - // this epoch or the next, send them to the beacon node - let mut duties_to_subscribe = service.duties_service.unsubscribed_epoch_duties(&epoch); - duties_to_subscribe.append( - &mut service - .duties_service - .unsubscribed_epoch_duties(&(epoch + 1)), - ); - - // spawn a task to subscribe all the duties - service - .context - .executor - .spawn(self.clone().send_subscriptions(duties_to_subscribe)); - - let duties_by_committee_index: HashMap> = service + let duties_by_committee_index: HashMap> = self .duties_service .attesters(slot) .into_iter() - .fold(HashMap::new(), |mut map, duty_and_state| { - if let Some(committee_index) = duty_and_state.duty.attestation_committee_index { + .fold(HashMap::new(), |mut map, duty_and_proof| { + if let Some(committee_index) = duty_and_proof.duty.attestation_committee_index { let validator_duties = map.entry(committee_index).or_insert_with(|| vec![]); - validator_duties.push(duty_and_state); + validator_duties.push(duty_and_proof); } map @@ -236,95 +209,17 @@ impl AttestationService { .into_iter() .for_each(|(committee_index, validator_duties)| { // Spawn a separate task for each attestation. - service - .context - .executor - .spawn(self.clone().publish_attestations_and_aggregates( - slot, - committee_index, - validator_duties, - aggregate_production_instant, - )); + tokio::task::spawn(service.clone().publish_attestations_and_aggregates( + slot, + committee_index, + validator_duties, + aggregate_production_instant, + )); }); Ok(()) } - /// Subscribes any required validators to the beacon node for a particular slot. - /// - /// This informs the beacon node that the validator has a duty on a particular - /// slot allowing the beacon node to connect to the required subnet and determine - /// if attestations need to be aggregated. - fn send_subscriptions(&self, duties: Vec) -> impl Future { - let service_1 = self.clone(); - let num_duties = duties.len(); - - let log_1 = self.context.log.clone(); - let log_2 = self.context.log.clone(); - - let (validator_subscriptions, successful_duties): (Vec<_>, Vec<_>) = duties - .into_iter() - .filter_map(|duty| { - let (slot, attestation_committee_index, _, validator_index) = - duty.attestation_duties()?; - let selection_proof = self - .validator_store - .produce_selection_proof(duty.validator_pubkey(), slot)?; - let modulo = duty.duty.aggregator_modulo?; - let subscription = ValidatorSubscription { - validator_index, - attestation_committee_index, - slot, - is_aggregator: selection_proof - .is_aggregator(modulo) - .map_err(|e| crit!(log_1, "Unable to determine aggregator: {:?}", e)) - .ok()?, - }; - - Some((subscription, (duty, selection_proof))) - }) - .unzip(); - - let num_failed_duties = num_duties - successful_duties.len(); - - self.beacon_node - .http - .validator() - .subscribe(validator_subscriptions) - .map_err(|e| format!("Failed to subscribe validators: {:?}", e)) - .map(move |publish_status| match publish_status { - PublishStatus::Valid => info!( - log_1, - "Successfully subscribed validators"; - "validators" => num_duties, - "failed_validators" => num_failed_duties, - ), - PublishStatus::Invalid(msg) => crit!( - log_1, - "Validator Subscription was invalid"; - "message" => msg, - ), - PublishStatus::Unknown => { - crit!(log_1, "Unknown condition when publishing attestation") - } - }) - .and_then(move |_| { - for (duty, selection_proof) in successful_duties { - service_1 - .duties_service - .subscribe_duty(&duty.duty, selection_proof); - } - Ok(()) - }) - .map_err(move |e| { - crit!( - log_2, - "Error during attestation production"; - "error" => e - ) - }) - } - /// Performs the first step of the attesting process: downloading `Attestation` objects, /// signing them and returning them to the validator. /// @@ -334,75 +229,68 @@ impl AttestationService { /// /// The given `validator_duties` should already be filtered to only contain those that match /// `slot` and `committee_index`. Critical errors will be logged if this is not the case. - fn publish_attestations_and_aggregates( - &self, + async fn publish_attestations_and_aggregates( + self, slot: Slot, committee_index: CommitteeIndex, - validator_duties: Vec, + validator_duties: Vec, aggregate_production_instant: Instant, - ) -> Box + Send> { + ) -> Result<(), ()> { // There's not need to produce `Attestation` or `SignedAggregateAndProof` if we do not have // any validators for the given `slot` and `committee_index`. if validator_duties.is_empty() { - return Box::new(future::ok(())); + return Ok(()); } - let service_1 = self.clone(); let log_1 = self.context.log.clone(); + let log_2 = self.context.log.clone(); let validator_duties_1 = Arc::new(validator_duties); let validator_duties_2 = validator_duties_1.clone(); - Box::new( - // Step 1. - // - // Download, sign and publish an `Attestation` for each validator. - self.produce_and_publish_attestations(slot, committee_index, validator_duties_1) - .and_then::<_, Box + Send>>( - move |attestation_opt| { - if let Some(attestation) = attestation_opt { - Box::new( - // Step 2. (Only if step 1 produced an attestation) - // - // First, wait until the `aggregation_production_instant` (2/3rds - // of the way though the slot). As verified in the - // `delay_triggers_when_in_the_past` test, this code will still run - // even if the instant has already elapsed. - // - // Then download, sign and publish a `SignedAggregateAndProof` for each - // validator that is elected to aggregate for this `slot` and - // `committee_index`. - Delay::new(aggregate_production_instant) - .map_err(|e| { - format!( - "Unable to create aggregate production delay: {:?}", - e - ) - }) - .and_then(move |()| { - service_1.produce_and_publish_aggregates( - attestation, - validator_duties_2, - ) - }), - ) - } else { - // If `produce_and_publish_attestations` did not download any - // attestations then there is no need to produce any - // `SignedAggregateAndProof`. - Box::new(future::ok(())) - } - }, + // Step 1. + // + // Download, sign and publish an `Attestation` for each validator. + let attestation_opt = self + .produce_and_publish_attestations(slot, committee_index, validator_duties_1) + .await + .map_err(move |e| { + crit!( + log_1, + "Error during attestation routine"; + "error" => format!("{:?}", e), + "committee_index" => committee_index, + "slot" => slot.as_u64(), ) - .map_err(move |e| { - crit!( - log_1, - "Error during attestation routine"; - "error" => format!("{:?}", e), - "committee_index" => committee_index, - "slot" => slot.as_u64(), - ) - }), - ) + })?; + if let Some(attestation) = attestation_opt { + // Step 2. (Only if step 1 produced an attestation) + // + // First, wait until the `aggregation_production_instant` (2/3rds + // of the way though the slot). As verified in the + // `delay_triggers_when_in_the_past` test, this code will still run + // even if the instant has already elapsed. + // + // Then download, sign and publish a `SignedAggregateAndProof` for each + // validator that is elected to aggregate for this `slot` and + // `committee_index`. + delay_until(aggregate_production_instant).await; + self.produce_and_publish_aggregates(attestation, validator_duties_2) + .await + } else { + // If `produce_and_publish_attestations` did not download any + // attestations then there is no need to produce any + // `SignedAggregateAndProof`. + Ok(()) + } + .map_err(move |e| { + crit!( + log_2, + "Error during attestation routine"; + "error" => format!("{:?}", e), + "committee_index" => committee_index, + "slot" => slot.as_u64(), + ) + }) } /// Performs the first step of the attesting process: downloading `Attestation` objects, @@ -417,136 +305,132 @@ impl AttestationService { /// /// Only one `Attestation` is downloaded from the BN. It is then cloned and signed by each /// validator and the list of individually-signed `Attestation` objects is returned to the BN. - fn produce_and_publish_attestations( + async fn produce_and_publish_attestations( &self, slot: Slot, committee_index: CommitteeIndex, - validator_duties: Arc>, - ) -> Box>, Error = String> + Send> { + validator_duties: Arc>, + ) -> Result>, String> { if validator_duties.is_empty() { - return Box::new(future::ok(None)); + return Ok(None); } - let service = self.clone(); + let attestation = self + .beacon_node + .http + .validator() + .produce_attestation(slot, committee_index) + .await + .map_err(|e| format!("Failed to produce attestation: {:?}", e))?; + + let log = self.context.log.clone(); + + // For each validator in `validator_duties`, clone the `attestation` and add + // their signature. + // + // If any validator is unable to sign, they are simply skipped. + let signed_attestations = validator_duties + .iter() + .filter_map(|duty| { + let log = self.context.log.clone(); + // Ensure that all required fields are present in the validator duty. + let (duty_slot, duty_committee_index, validator_committee_position, _) = + if let Some(tuple) = duty.attestation_duties() { + tuple + } else { + crit!( + log, + "Missing validator duties when signing"; + "duties" => format!("{:?}", duty) + ); + return None; + }; + + // Ensure that the attestation matches the duties. + if duty_slot != attestation.data.slot + || duty_committee_index != attestation.data.index + { + crit!( + log, + "Inconsistent validator duties during signing"; + "validator" => format!("{:?}", duty.validator_pubkey()), + "duty_slot" => duty_slot, + "attestation_slot" => attestation.data.slot, + "duty_index" => duty_committee_index, + "attestation_index" => attestation.data.index, + ); + return None; + } + + let mut attestation = attestation.clone(); + + if self + .validator_store + .sign_attestation( + duty.validator_pubkey(), + validator_committee_position, + &mut attestation, + ) + .is_none() + { + crit!( + log, + "Attestation signing refused"; + "validator" => format!("{:?}", duty.validator_pubkey()), + "slot" => attestation.data.slot, + "index" => attestation.data.index, + ); + None + } else { + Some(attestation) + } + }) + .collect::>(); + + // If there are any signed attestations, publish them to the BN. Otherwise, + // just return early. + if let Some(attestation) = signed_attestations.first().cloned() { + let num_attestations = signed_attestations.len(); + let beacon_block_root = attestation.data.beacon_block_root; - Box::new( self.beacon_node .http .validator() - .produce_attestation(slot, committee_index) - .map_err(|e| format!("Failed to produce attestation: {:?}", e)) - .and_then::<_, Box + Send>>(move |attestation| { - let log = service.context.log.clone(); - - // For each validator in `validator_duties`, clone the `attestation` and add - // their signature. - // - // If any validator is unable to sign, they are simply skipped. - let signed_attestations = validator_duties - .iter() - .filter_map(|duty| { - let log = service.context.log.clone(); - - // Ensure that all required fields are present in the validator duty. - let (duty_slot, duty_committee_index, validator_committee_position, _) = - if let Some(tuple) = duty.attestation_duties() { - tuple - } else { - crit!( - log, - "Missing validator duties when signing"; - "duties" => format!("{:?}", duty) - ); - return None; - }; - - // Ensure that the attestation matches the duties. - if duty_slot != attestation.data.slot - || duty_committee_index != attestation.data.index - { - crit!( - log, - "Inconsistent validator duties during signing"; - "validator" => format!("{:?}", duty.validator_pubkey()), - "duty_slot" => duty_slot, - "attestation_slot" => attestation.data.slot, - "duty_index" => duty_committee_index, - "attestation_index" => attestation.data.index, - ); - return None; - } - - let mut attestation = attestation.clone(); - - if service - .validator_store - .sign_attestation( - duty.validator_pubkey(), - validator_committee_position, - &mut attestation, - ) - .is_none() - { - crit!( - log, - "Attestation signing refused"; - "validator" => format!("{:?}", duty.validator_pubkey()), - "slot" => attestation.data.slot, - "index" => attestation.data.index, - ); - None - } else { - Some(attestation) - } - }) - .collect::>(); - - // If there are any signed attestations, publish them to the BN. Otherwise, - // just return early. - if let Some(attestation) = signed_attestations.first().cloned() { - let num_attestations = signed_attestations.len(); - let beacon_block_root = attestation.data.beacon_block_root; - - Box::new( - service - .beacon_node - .http - .validator() - .publish_attestations(signed_attestations) - .map_err(|e| format!("Failed to publish attestation: {:?}", e)) - .map(move |publish_status| match publish_status { - PublishStatus::Valid => info!( - log, - "Successfully published attestations"; - "count" => num_attestations, - "head_block" => format!("{:?}", beacon_block_root), - "committee_index" => committee_index, - "slot" => slot.as_u64(), - ), - PublishStatus::Invalid(msg) => crit!( - log, - "Published attestation was invalid"; - "message" => msg, - "committee_index" => committee_index, - "slot" => slot.as_u64(), - ), - PublishStatus::Unknown => { - crit!(log, "Unknown condition when publishing attestation") - } - }) - .map(|()| Some(attestation)), - ) - } else { - debug!( - log, - "No attestations to publish"; - "committee_index" => committee_index, - "slot" => slot.as_u64(), - ); - Box::new(future::ok(None)) + .publish_attestations(signed_attestations) + .await + .map_err(|e| format!("Failed to publish attestation: {:?}", e)) + .map(move |publish_status| match publish_status { + PublishStatus::Valid => info!( + log, + "Successfully published attestations"; + "count" => num_attestations, + "head_block" => format!("{:?}", beacon_block_root), + "committee_index" => committee_index, + "slot" => slot.as_u64(), + "type" => "unaggregated", + ), + PublishStatus::Invalid(msg) => crit!( + log, + "Published attestation was invalid"; + "message" => msg, + "committee_index" => committee_index, + "slot" => slot.as_u64(), + "type" => "unaggregated", + ), + PublishStatus::Unknown => { + crit!(log, "Unknown condition when publishing unagg. attestation") } - }), - ) + }) + .map(|()| Some(attestation)) + } else { + debug!( + log, + "No attestations to publish"; + "committee_index" => committee_index, + "slot" => slot.as_u64(), + ); + return Ok(None); + } } /// Performs the second step of the attesting process: downloading an aggregated `Attestation`, @@ -562,109 +446,105 @@ impl AttestationService { /// Only one aggregated `Attestation` is downloaded from the BN. It is then cloned and signed /// by each validator and the list of individually-signed `SignedAggregateAndProof` objects is /// returned to the BN. - fn produce_and_publish_aggregates( + async fn produce_and_publish_aggregates( &self, attestation: Attestation, - validator_duties: Arc>, - ) -> impl Future { - let service_1 = self.clone(); + validator_duties: Arc>, + ) -> Result<(), String> { let log_1 = self.context.log.clone(); - self.beacon_node + let aggregated_attestation = self + .beacon_node .http .validator() .produce_aggregate_attestation(&attestation.data) - .map_err(|e| format!("Failed to produce an aggregate attestation: {:?}", e)) - .and_then::<_, Box + Send>>( - move |aggregated_attestation| { - // For each validator, clone the `aggregated_attestation` and convert it into - // a `SignedAggregateAndProof` - let signed_aggregate_and_proofs = validator_duties - .iter() - .filter_map(|duty_and_state| { - // Do not produce a signed aggregator for validators that are not - // subscribed aggregators. - // - // Note: this function returns `false` if the validator is required to - // be an aggregator but has not yet subscribed. - if !duty_and_state.is_aggregator() { - return None; - } + .await + .map_err(|e| format!("Failed to produce an aggregate attestation: {:?}", e))?; - let (duty_slot, duty_committee_index, _, validator_index) = - duty_and_state.attestation_duties().or_else(|| { - crit!(log_1, "Missing duties when signing aggregate"); - None - })?; + // For each validator, clone the `aggregated_attestation` and convert it into + // a `SignedAggregateAndProof` + let signed_aggregate_and_proofs = validator_duties + .iter() + .filter_map(|duty_and_proof| { + // Do not produce a signed aggregator for validators that are not + // subscribed aggregators. + let selection_proof = duty_and_proof.selection_proof.as_ref()?.clone(); - let pubkey = &duty_and_state.duty.validator_pubkey; - let slot = attestation.data.slot; - let committee_index = attestation.data.index; + let (duty_slot, duty_committee_index, _, validator_index) = + duty_and_proof.attestation_duties().or_else(|| { + crit!(log_1, "Missing duties when signing aggregate"); + None + })?; - if duty_slot != slot || duty_committee_index != committee_index { - crit!(log_1, "Inconsistent validator duties during signing"); - return None; - } + let pubkey = &duty_and_proof.duty.validator_pubkey; + let slot = attestation.data.slot; + let committee_index = attestation.data.index; - if let Some(signed_aggregate_and_proof) = service_1 - .validator_store - .produce_signed_aggregate_and_proof( - pubkey, - validator_index, - aggregated_attestation.clone(), - ) - { - Some(signed_aggregate_and_proof) - } else { - crit!(log_1, "Failed to sign attestation"); - None - } - }) - .collect::>(); + if duty_slot != slot || duty_committee_index != committee_index { + crit!(log_1, "Inconsistent validator duties during signing"); + return None; + } - // If there any signed aggregates and proofs were produced, publish them to the - // BN. - if let Some(first) = signed_aggregate_and_proofs.first().cloned() { - let attestation = first.message.aggregate; + if let Some(signed_aggregate_and_proof) = + self.validator_store.produce_signed_aggregate_and_proof( + pubkey, + validator_index, + aggregated_attestation.clone(), + selection_proof, + ) + { + Some(signed_aggregate_and_proof) + } else { + crit!(log_1, "Failed to sign attestation"); + None + } + }) + .collect::>(); - Box::new(service_1 - .beacon_node - .http - .validator() - .publish_aggregate_and_proof(signed_aggregate_and_proofs) - .map(|publish_status| (attestation, publish_status)) - .map_err(|e| format!("Failed to publish aggregate and proofs: {:?}", e)) - .map(move |(attestation, publish_status)| match publish_status { - PublishStatus::Valid => info!( - log_1, - "Successfully published aggregate attestations"; - "signatures" => attestation.aggregation_bits.num_set_bits(), - "head_block" => format!("{}", attestation.data.beacon_block_root), - "committee_index" => attestation.data.index, - "slot" => attestation.data.slot.as_u64(), - ), - PublishStatus::Invalid(msg) => crit!( - log_1, - "Published attestation was invalid"; - "message" => msg, - "committee_index" => attestation.data.index, - "slot" => attestation.data.slot.as_u64(), - ), - PublishStatus::Unknown => { - crit!(log_1, "Unknown condition when publishing attestation") - } - })) - } else { - debug!( - log_1, - "No signed aggregates to publish"; - "committee_index" => attestation.data.index, - "slot" => attestation.data.slot.as_u64(), - ); - Box::new(future::ok(())) - } - }, - ) + // If there any signed aggregates and proofs were produced, publish them to the + // BN. + if let Some(first) = signed_aggregate_and_proofs.first().cloned() { + let attestation = first.message.aggregate; + + let publish_status = self + .beacon_node + .http + .validator() + .publish_aggregate_and_proof(signed_aggregate_and_proofs) + .await + .map_err(|e| format!("Failed to publish aggregate and proofs: {:?}", e))?; + match publish_status { + PublishStatus::Valid => info!( + log_1, + "Successfully published attestations"; + "signatures" => attestation.aggregation_bits.num_set_bits(), + "head_block" => format!("{:?}", attestation.data.beacon_block_root), + "committee_index" => attestation.data.index, + "slot" => attestation.data.slot.as_u64(), + "type" => "aggregated", + ), + PublishStatus::Invalid(msg) => crit!( + log_1, + "Published attestation was invalid"; + "message" => msg, + "committee_index" => attestation.data.index, + "slot" => attestation.data.slot.as_u64(), + "type" => "aggregated", + ), + PublishStatus::Unknown => { + crit!(log_1, "Unknown condition when publishing agg. attestation") + } + }; + Ok(()) + } else { + debug!( + log_1, + "No signed aggregates to publish"; + "committee_index" => attestation.data.index, + "slot" => attestation.data.slot.as_u64(), + ); + Ok(()) + } } } @@ -682,16 +562,14 @@ mod tests { let state_1 = Arc::new(RwLock::new(in_the_past)); let state_2 = state_1.clone(); - let future = Delay::new(in_the_past) - .map_err(|_| panic!("Failed to create duration")) - .map(move |()| *state_1.write() = Instant::now()); + let future = delay_until(in_the_past).map(move |()| *state_1.write() = Instant::now()); let mut runtime = RuntimeBuilder::new() .core_threads(1) .build() .expect("failed to start runtime"); - runtime.block_on(future).expect("failed to complete future"); + runtime.block_on(future); assert!( *state_2.read() > in_the_past, diff --git a/validator_client/src/block_service.rs b/validator_client/src/block_service.rs index 14c7043a73..5e9daa6ff2 100644 --- a/validator_client/src/block_service.rs +++ b/validator_client/src/block_service.rs @@ -1,15 +1,14 @@ use crate::{duties_service::DutiesService, validator_store::ValidatorStore}; use environment::RuntimeContext; use exit_future::Signal; -use futures::{stream, Future, IntoFuture, Stream}; +use futures::{FutureExt, StreamExt}; use remote_beacon_node::{PublishStatus, RemoteBeaconNode}; use slog::{crit, error, info, trace}; use slot_clock::SlotClock; use std::ops::Deref; use std::sync::Arc; -use std::time::{Duration, Instant}; -use tokio::timer::Interval; -use types::{ChainSpec, EthSpec}; +use tokio::time::{interval_at, Duration, Instant}; +use types::{ChainSpec, EthSpec, PublicKey, Slot}; /// Delay this period of time after the slot starts. This allows the node to process the new slot. const TIME_DELAY_FROM_SLOT: Duration = Duration::from_millis(100); @@ -124,7 +123,8 @@ impl BlockService { let interval = { let slot_duration = Duration::from_millis(spec.milliseconds_per_slot); - Interval::new( + // Note: interval_at panics if slot_duration = 0 + interval_at( Instant::now() + duration_to_next_slot + TIME_DELAY_FROM_SLOT, slot_duration, ) @@ -132,135 +132,102 @@ impl BlockService { let (exit_signal, exit_fut) = exit_future::signal(); let service = self.clone(); - let log_1 = log.clone(); - let log_2 = log.clone(); + let interval_fut = interval.for_each(move |_| { + let _ = service.clone().do_update(); + futures::future::ready(()) + }); - self.context.executor.spawn( - exit_fut - .until( - interval - .map_err(move |e| { - crit! { - log_1, - "Timer thread failed"; - "error" => format!("{}", e) - } - }) - .for_each(move |_| service.clone().do_update().then(|_| Ok(()))), - ) - .map(move |_| info!(log_2, "Shutdown complete")), + let future = futures::future::select( + interval_fut, + exit_fut.map(move |_| info!(log, "Shutdown complete")), ); + tokio::task::spawn(future); Ok(exit_signal) } /// Attempt to produce a block for any block producers in the `ValidatorStore`. - fn do_update(self) -> impl Future { - let service = self.clone(); + fn do_update(&self) -> Result<(), ()> { let log_1 = self.context.log.clone(); let log_2 = self.context.log.clone(); - self.slot_clock - .now() - .ok_or_else(move || { - crit!(log_1, "Duties manager failed to read slot clock"); - }) - .into_future() - .and_then(move |slot| { - let iter = service.duties_service.block_producers(slot).into_iter(); + let slot = self.slot_clock.now().ok_or_else(move || { + crit!(log_1, "Duties manager failed to read slot clock"); + })?; + let iter = self.duties_service.block_producers(slot).into_iter(); - if iter.len() == 0 { - trace!( - log_2, - "No local block proposers for this slot"; - "slot" => slot.as_u64() - ) - } else if iter.len() > 1 { - error!( - log_2, - "Multiple block proposers for this slot"; - "action" => "producing blocks for all proposers", - "num_proposers" => iter.len(), - "slot" => slot.as_u64(), - ) - } + if iter.len() == 0 { + trace!( + log_2, + "No local block proposers for this slot"; + "slot" => slot.as_u64() + ) + } else if iter.len() > 1 { + error!( + log_2, + "Multiple block proposers for this slot"; + "action" => "producing blocks for all proposers", + "num_proposers" => iter.len(), + "slot" => slot.as_u64(), + ) + } - stream::unfold(iter, move |mut block_producers| { - let log_1 = service.context.log.clone(); - let log_2 = service.context.log.clone(); - let service_1 = service.clone(); - let service_2 = service.clone(); - let service_3 = service.clone(); + // TODO: check if the logic is same as stream::unfold version + let _ = futures::stream::iter(iter).for_each(|validator_pubkey| async { + match self.publish_block(slot, validator_pubkey).await { + Ok(()) => (), + Err(e) => crit!( + log_2, + "Error whilst producing block"; + "message" => e + ), + } + }); - block_producers.next().map(move |validator_pubkey| { - service_1 - .validator_store - .randao_reveal(&validator_pubkey, slot.epoch(E::slots_per_epoch())) - .ok_or_else(|| "Unable to produce randao reveal".to_string()) - .into_future() - .and_then(move |randao_reveal| { - service_1 - .beacon_node - .http - .validator() - .produce_block(slot, randao_reveal) - .map_err(|e| { - format!( - "Error from beacon node when producing block: {:?}", - e - ) - }) - }) - .and_then(move |block| { - service_2 - .validator_store - .sign_block(&validator_pubkey, block) - .ok_or_else(|| "Unable to sign block".to_string()) - }) - .and_then(move |block| { - service_3 - .beacon_node - .http - .validator() - .publish_block(block.clone()) - .map(|publish_status| (block, publish_status)) - .map_err(|e| { - format!( - "Error from beacon node when publishing block: {:?}", - e - ) - }) - }) - .map(move |(block, publish_status)| match publish_status { - PublishStatus::Valid => info!( - log_1, - "Successfully published block"; - "deposits" => block.message.body.deposits.len(), - "attestations" => block.message.body.attestations.len(), - "slot" => block.slot().as_u64(), - ), - PublishStatus::Invalid(msg) => crit!( - log_1, - "Published block was invalid"; - "message" => msg, - "slot" => block.slot().as_u64(), - ), - PublishStatus::Unknown => { - crit!(log_1, "Unknown condition when publishing block") - } - }) - .map_err(move |e| { - crit!( - log_2, - "Error whilst producing block"; - "message" => e - ) - }) - .then(|_| Ok(((), block_producers))) - }) - }) - .collect() - .map(|_| ()) - }) + Ok(()) + } + + /// Produce a block at the given slot for validator_pubkey + async fn publish_block(&self, slot: Slot, validator_pubkey: PublicKey) -> Result<(), String> { + let log_1 = self.context.log.clone(); + let randao_reveal = self + .validator_store + .randao_reveal(&validator_pubkey, slot.epoch(E::slots_per_epoch())) + .ok_or_else(|| "Unable to produce randao reveal".to_string())?; + let block = self + .beacon_node + .http + .validator() + .produce_block(slot, randao_reveal) + .await + .map_err(|e| format!("Error from beacon node when producing block: {:?}", e))?; + let signed_block = self + .validator_store + .sign_block(&validator_pubkey, block) + .ok_or_else(|| "Unable to sign block".to_string())?; + let publish_status = self + .beacon_node + .http + .validator() + .publish_block(signed_block.clone()) + .await + .map_err(|e| format!("Error from beacon node when publishing block: {:?}", e))?; + match publish_status { + PublishStatus::Valid => info!( + log_1, + "Successfully published block"; + "deposits" => signed_block.message.body.deposits.len(), + "attestations" => signed_block.message.body.attestations.len(), + "slot" => signed_block.slot().as_u64(), + ), + PublishStatus::Invalid(msg) => crit!( + log_1, + "Published block was invalid"; + "message" => msg, + "slot" => signed_block.slot().as_u64(), + ), + PublishStatus::Unknown => crit!(log_1, "Unknown condition when publishing block"), + } + Ok(()) } } diff --git a/validator_client/src/duties_service.rs b/validator_client/src/duties_service.rs index 3a9f73790f..e16f45a1fc 100644 --- a/validator_client/src/duties_service.rs +++ b/validator_client/src/duties_service.rs @@ -1,18 +1,17 @@ use crate::validator_store::ValidatorStore; use environment::RuntimeContext; use exit_future::Signal; -use futures::{future, Future, IntoFuture, Stream}; +use futures::{FutureExt, StreamExt}; use parking_lot::RwLock; -use remote_beacon_node::RemoteBeaconNode; -use rest_types::{ValidatorDuty, ValidatorDutyBytes}; -use slog::{crit, debug, error, info, trace, warn}; +use remote_beacon_node::{PublishStatus, RemoteBeaconNode}; +use rest_types::{ValidatorDuty, ValidatorDutyBytes, ValidatorSubscription}; +use slog::{debug, error, info, trace, warn}; use slot_clock::SlotClock; use std::collections::HashMap; use std::convert::TryInto; use std::ops::Deref; use std::sync::Arc; -use std::time::{Duration, Instant}; -use tokio::timer::Interval; +use tokio::time::{interval_at, Duration, Instant}; use types::{ChainSpec, CommitteeIndex, Epoch, EthSpec, PublicKey, SelectionProof, Slot}; /// Delay this period of time after the slot starts. This allows the node to process the new slot. @@ -21,49 +20,73 @@ const TIME_DELAY_FROM_SLOT: Duration = Duration::from_millis(100); /// Remove any duties where the `duties_epoch < current_epoch - PRUNE_DEPTH`. const PRUNE_DEPTH: u64 = 4; -type BaseHashMap = HashMap>; +type BaseHashMap = HashMap>; #[derive(Debug, Clone)] -pub enum DutyState { - /// This duty has not been subscribed to the beacon node. - NotSubscribed, - /// The duty has been subscribed and the validator is an aggregator for this duty. The - /// selection proof is provided to construct the `AggregateAndProof` struct. - SubscribedAggregator(SelectionProof), -} - -#[derive(Debug, Clone)] -pub struct DutyAndState { +pub struct DutyAndProof { /// The validator duty. pub duty: ValidatorDuty, - /// The current state of the validator duty. - state: DutyState, + /// Stores the selection proof if the duty elects the validator to be an aggregator. + pub selection_proof: Option, } -impl DutyAndState { - /// Returns true if the duty is an aggregation duty (the validator must aggregate all - /// attestations. - pub fn is_aggregator(&self) -> bool { - match self.state { - DutyState::NotSubscribed => false, - DutyState::SubscribedAggregator(_) => true, - } +impl DutyAndProof { + /// Computes the selection proof for `self.validator_pubkey` and `self.duty.attestation_slot`, + /// storing it in `self.selection_proof` _if_ the validator is an aggregator. If the validator + /// is not an aggregator, `self.selection_proof` is set to `None`. + /// + /// ## Errors + /// + /// - `self.validator_pubkey` is not known in `validator_store`. + /// - There's an arith error during computation. + pub fn compute_selection_proof( + &mut self, + validator_store: &ValidatorStore, + ) -> Result<(), String> { + let (modulo, slot) = if let (Some(modulo), Some(slot)) = + (self.duty.aggregator_modulo, self.duty.attestation_slot) + { + (modulo, slot) + } else { + // If there is no modulo or for the aggregator we assume they are not activated and + // therefore not an aggregator. + self.selection_proof = None; + return Ok(()); + }; + + let selection_proof = validator_store + .produce_selection_proof(&self.duty.validator_pubkey, slot) + .ok_or_else(|| "Validator pubkey missing from store".to_string())?; + + self.selection_proof = selection_proof + .is_aggregator_from_modulo(modulo) + .map_err(|e| format!("Invalid modulo: {:?}", e)) + .map(|is_aggregator| { + if is_aggregator { + Some(selection_proof) + } else { + None + } + })?; + + Ok(()) } - /// Returns the selection proof if the duty is an aggregation duty. - pub fn selection_proof(&self) -> Option { - match &self.state { - DutyState::SubscribedAggregator(proof) => Some(proof.clone()), - _ => None, - } + /// Returns `true` if the two `Self` instances would result in the same beacon subscription. + pub fn subscription_eq(&self, other: &Self) -> bool { + self.selection_proof_eq(other) + && self.duty.validator_index == other.duty.validator_index + && self.duty.attestation_committee_index == other.duty.attestation_committee_index + && self.duty.attestation_slot == other.duty.attestation_slot } - /// Returns true if the this duty has been subscribed with the beacon node. - pub fn is_subscribed(&self) -> bool { - match self.state { - DutyState::NotSubscribed => false, - DutyState::SubscribedAggregator(_) => true, - } + /// Returns `true` if the selection proof between `self` and `other` _should_ be equal. + /// + /// It's important to note that this doesn't actually check `self.selection_proof`, instead it + /// checks to see if the inputs to computing the selection proof are equal. + fn selection_proof_eq(&self, other: &Self) -> bool { + self.duty.aggregator_modulo == other.duty.aggregator_modulo + && self.duty.attestation_slot == other.duty.attestation_slot } /// Returns the information required for an attesting validator, if they are scheduled to @@ -82,10 +105,10 @@ impl DutyAndState { } } -impl TryInto for ValidatorDutyBytes { +impl TryInto for ValidatorDutyBytes { type Error = String; - fn try_into(self) -> Result { + fn try_into(self) -> Result { let duty = ValidatorDuty { validator_pubkey: (&self.validator_pubkey) .try_into() @@ -97,9 +120,9 @@ impl TryInto for ValidatorDutyBytes { block_proposal_slots: self.block_proposal_slots, aggregator_modulo: self.aggregator_modulo, }; - Ok(DutyAndState { + Ok(DutyAndProof { duty, - state: DutyState::NotSubscribed, + selection_proof: None, }) } } @@ -114,11 +137,24 @@ enum InsertOutcome { Identical, /// There were duties for this validator and epoch in the store that were different to the ones /// provided. The existing duties were replaced. - Replaced, + Replaced { should_resubscribe: bool }, /// The given duties were invalid. Invalid, } +impl InsertOutcome { + /// Returns `true` if the outcome indicates that the validator _might_ require a subscription. + pub fn is_subscription_candidate(self) -> bool { + match self { + InsertOutcome::Replaced { should_resubscribe } => should_resubscribe, + InsertOutcome::NewValidator => true, + InsertOutcome::NewEpoch => true, + InsertOutcome::Identical => false, + InsertOutcome::Invalid => false, + } + } +} + #[derive(Default)] pub struct DutiesStore { store: RwLock, @@ -173,49 +209,7 @@ impl DutiesStore { .collect() } - /// Gets a list of validator duties for an epoch that have not yet been subscribed - /// to the beacon node. - // Note: Potentially we should modify the data structure to store the unsubscribed epoch duties for validator clients with a large number of validators. This currently adds an O(N) search each slot. - fn unsubscribed_epoch_duties(&self, epoch: &Epoch) -> Vec { - self.store - .read() - .iter() - .filter_map(|(_validator_pubkey, validator_map)| { - validator_map.get(epoch).and_then(|duty_and_state| { - if !duty_and_state.is_subscribed() { - Some(duty_and_state) - } else { - None - } - }) - }) - .cloned() - .collect() - } - - /// Marks a duty as being subscribed to the beacon node. This is called by the attestation - /// service once it has been sent. - fn set_duty_state( - &self, - validator: &PublicKey, - slot: Slot, - state: DutyState, - slots_per_epoch: u64, - ) { - let epoch = slot.epoch(slots_per_epoch); - - let mut store = self.store.write(); - if let Some(map) = store.get_mut(validator) { - if let Some(duty) = map.get_mut(&epoch) { - if duty.duty.attestation_slot == Some(slot) { - // set the duty state - duty.state = state; - } - } - } - } - - fn attesters(&self, slot: Slot, slots_per_epoch: u64) -> Vec { + fn attesters(&self, slot: Slot, slots_per_epoch: u64) -> Vec { self.store .read() .iter() @@ -236,27 +230,49 @@ impl DutiesStore { .collect() } - fn insert(&self, epoch: Epoch, duties: DutyAndState, slots_per_epoch: u64) -> InsertOutcome { + fn insert( + &self, + epoch: Epoch, + mut duties: DutyAndProof, + slots_per_epoch: u64, + validator_store: &ValidatorStore, + ) -> Result { let mut store = self.store.write(); if !duties_match_epoch(&duties.duty, epoch, slots_per_epoch) { - return InsertOutcome::Invalid; + return Ok(InsertOutcome::Invalid); } + // TODO: refactor with Entry. + if let Some(validator_map) = store.get_mut(&duties.duty.validator_pubkey) { if let Some(known_duties) = validator_map.get_mut(&epoch) { if known_duties.duty == duties.duty { - InsertOutcome::Identical + Ok(InsertOutcome::Identical) } else { + // Compute the selection proof. + duties.compute_selection_proof(validator_store)?; + + // Determine if a re-subscription is required. + let should_resubscribe = duties.subscription_eq(known_duties); + + // Replace the existing duties. *known_duties = duties; - InsertOutcome::Replaced + + Ok(InsertOutcome::Replaced { should_resubscribe }) } } else { + // Compute the selection proof. + duties.compute_selection_proof(validator_store)?; + validator_map.insert(epoch, duties); - InsertOutcome::NewEpoch + Ok(InsertOutcome::NewEpoch) } } else { + // Compute the selection proof. + duties.compute_selection_proof(validator_store)?; + let validator_pubkey = duties.duty.validator_pubkey.clone(); let mut validator_map = HashMap::new(); @@ -264,7 +280,7 @@ impl DutiesStore { store.insert(validator_pubkey, validator_map); - InsertOutcome::NewValidator + Ok(InsertOutcome::NewValidator) } } @@ -408,29 +424,10 @@ impl DutiesService { } /// Returns all `ValidatorDuty` for the given `slot`. - pub fn attesters(&self, slot: Slot) -> Vec { + pub fn attesters(&self, slot: Slot) -> Vec { self.store.attesters(slot, E::slots_per_epoch()) } - /// Returns all `ValidatorDuty` that have not been registered with the beacon node. - pub fn unsubscribed_epoch_duties(&self, epoch: &Epoch) -> Vec { - self.store.unsubscribed_epoch_duties(epoch) - } - - /// Marks the duty as being subscribed to the beacon node. - /// - /// If the duty is to be marked as an aggregator duty, a selection proof is also provided. - pub fn subscribe_duty(&self, duty: &ValidatorDuty, proof: SelectionProof) { - if let Some(slot) = duty.attestation_slot { - self.store.set_duty_state( - &duty.validator_pubkey, - slot, - DutyState::SubscribedAggregator(proof), - E::slots_per_epoch(), - ) - } - } - /// Start the service that periodically polls the beacon node for validator duties. pub fn start_update_service(&self, spec: &ChainSpec) -> Result { let log = self.context.log.clone(); @@ -442,54 +439,53 @@ impl DutiesService { let interval = { let slot_duration = Duration::from_millis(spec.milliseconds_per_slot); - Interval::new( + // Note: `interval_at` panics if `slot_duration` is 0 + interval_at( Instant::now() + duration_to_next_slot + TIME_DELAY_FROM_SLOT, slot_duration, ) }; let (exit_signal, exit_fut) = exit_future::signal(); - let service = self.clone(); - let log_1 = log.clone(); - let log_2 = log.clone(); + let service_1 = self.clone(); + let service_2 = self.clone(); // Run an immediate update before starting the updater service. - self.context.executor.spawn(service.clone().do_update()); + tokio::task::spawn(service_1.do_update()); - self.context.executor.spawn( - exit_fut - .until( - interval - .map_err(move |e| { - crit! { - log_1, - "Timer thread failed"; - "error" => format!("{}", e) - } - }) - .for_each(move |_| service.clone().do_update().then(|_| Ok(()))), - ) - .map(move |_| info!(log_2, "Shutdown complete")), + let interval_fut = interval.for_each(move |_| { + let _ = service_2.clone().do_update(); + futures::future::ready(()) + }); + + let future = futures::future::select( + interval_fut, + exit_fut.map(move |_| info!(log, "Shutdown complete")), ); + tokio::task::spawn(future); Ok(exit_signal) } /// Attempt to download the duties of all managed validators for this epoch and the next. - fn do_update(&self) -> impl Future { + async fn do_update(self) -> Result<(), ()> { + let log_1 = self.context.log.clone(); + let log_2 = self.context.log.clone(); + let log_3 = self.context.log.clone(); + let log = self.context.log.clone(); + let service_1 = self.clone(); let service_2 = self.clone(); let service_3 = self.clone(); let service_4 = self.clone(); - let log_1 = self.context.log.clone(); - let log_2 = self.context.log.clone(); + let service_5 = self.clone(); - self.slot_clock + let current_epoch = service_1 + .slot_clock .now() .ok_or_else(move || { error!(log_1, "Duties manager failed to read slot clock"); }) - .into_future() .map(move |slot| { let epoch = slot.epoch(E::slots_per_epoch()); @@ -503,146 +499,203 @@ impl DutiesService { "current_epoch" => epoch.as_u64(), ); - service_1.store.prune(prune_below); + self.store.prune(prune_below); } epoch - }) - .and_then(move |epoch| { - let log = service_2.context.log.clone(); + })?; - service_2 - .beacon_node - .http - .beacon() - .get_head() - .map(move |head| (epoch, head.slot.epoch(E::slots_per_epoch()))) - .map_err(move |e| { - error!( - log, - "Failed to contact beacon node"; - "error" => format!("{:?}", e) - ) - }) - }) - .and_then(move |(current_epoch, beacon_head_epoch)| { - let log = service_3.context.log.clone(); + let beacon_head_epoch = service_2 + .beacon_node + .http + .beacon() + .get_head() + .await + .map(|head| head.slot.epoch(E::slots_per_epoch())) + .map_err(move |e| { + error!( + log_3, + "Failed to contact beacon node"; + "error" => format!("{:?}", e) + ) + })?; - let future: Box + Send> = if beacon_head_epoch + 1 - < current_epoch - && !service_3.allow_unsynced_beacon_node - { + if beacon_head_epoch + 1 < current_epoch && !service_3.allow_unsynced_beacon_node { + error!( + log, + "Beacon node is not synced"; + "node_head_epoch" => format!("{}", beacon_head_epoch), + "current_epoch" => format!("{}", current_epoch), + ); + } else { + let result = service_4.clone().update_epoch(current_epoch).await; + if let Err(e) = result { + error!( + log, + "Failed to get current epoch duties"; + "http_error" => format!("{:?}", e) + ); + } + + service_5 + .clone() + .update_epoch(current_epoch + 1) + .await + .map_err(move |e| { error!( log, - "Beacon node is not synced"; - "node_head_epoch" => format!("{}", beacon_head_epoch), - "current_epoch" => format!("{}", current_epoch), + "Failed to get next epoch duties"; + "http_error" => format!("{:?}", e) ); - - Box::new(future::ok(())) - } else { - Box::new(service_3.update_epoch(current_epoch).then(move |result| { - if let Err(e) = result { - error!( - log, - "Failed to get current epoch duties"; - "http_error" => format!("{:?}", e) - ); - } - - let log = service_4.context.log.clone(); - service_4.update_epoch(current_epoch + 1).map_err(move |e| { - error!( - log, - "Failed to get next epoch duties"; - "http_error" => format!("{:?}", e) - ); - }) - })) - }; - - future - }) - .map(|_| ()) + })?; + }; + Ok(()) } /// Attempt to download the duties of all managed validators for the given `epoch`. - fn update_epoch(self, epoch: Epoch) -> impl Future { - let service_1 = self.clone(); - let service_2 = self; - - let pubkeys = service_1.validator_store.voting_pubkeys(); - service_1 + async fn update_epoch(self, epoch: Epoch) -> Result<(), String> { + let pubkeys = self.validator_store.voting_pubkeys(); + let all_duties = self .beacon_node .http .validator() .get_duties(epoch, pubkeys.as_slice()) - .map(move |all_duties| (epoch, all_duties)) - .map_err(move |e| format!("Failed to get duties for epoch {}: {:?}", epoch, e)) - .and_then(move |(epoch, all_duties)| { - let log = service_2.context.log.clone(); + .await + .map_err(move |e| format!("Failed to get duties for epoch {}: {:?}", epoch, e))?; - let mut new_validator = 0; - let mut new_epoch = 0; - let mut identical = 0; - let mut replaced = 0; - let mut invalid = 0; + let log = self.context.log.clone(); - all_duties.into_iter().try_for_each::<_, Result<_, String>>(|remote_duties| { - let duties: DutyAndState = remote_duties.try_into()?; + let mut new_validator = 0; + let mut new_epoch = 0; + let mut identical = 0; + let mut replaced = 0; + let mut invalid = 0; - match service_2 - .store - .insert(epoch, duties.clone(), E::slots_per_epoch()) - { - InsertOutcome::NewValidator => { - debug!( - log, - "First duty assignment for validator"; - "proposal_slots" => format!("{:?}", &duties.duty.block_proposal_slots), - "attestation_slot" => format!("{:?}", &duties.duty.attestation_slot), - "validator" => format!("{:?}", &duties.duty.validator_pubkey) - ); - new_validator += 1 - } - InsertOutcome::NewEpoch => new_epoch += 1, - InsertOutcome::Identical => identical += 1, - InsertOutcome::Replaced => replaced += 1, - InsertOutcome::Invalid => invalid += 1, - }; + // For each of the duties, attempt to insert them into our local store and build a + // list of new or changed selections proofs for any aggregating validators. + let validator_subscriptions = all_duties + .into_iter() + .filter_map(|remote_duties| { + // Convert the remote duties into our local representation. + let duties: DutyAndProof = remote_duties + .try_into() + .map_err(|e| { + error!( + log, + "Unable to convert remote duties"; + "error" => e + ) + }) + .ok()?; - Ok(()) - })?; - - if invalid > 0 { - error!( - log, - "Received invalid duties from beacon node"; - "bad_duty_count" => invalid, - "info" => "Duties are from wrong epoch." + // Attempt to update our local store. + let outcome = self + .store + .insert( + epoch, + duties.clone(), + E::slots_per_epoch(), + &self.validator_store, ) + .map_err(|e| { + error!( + log, + "Unable to store duties"; + "error" => e + ) + }) + .ok()?; + + match &outcome { + InsertOutcome::NewValidator => { + debug!( + log, + "First duty assignment for validator"; + "proposal_slots" => format!("{:?}", &duties.duty.block_proposal_slots), + "attestation_slot" => format!("{:?}", &duties.duty.attestation_slot), + "validator" => format!("{:?}", &duties.duty.validator_pubkey) + ); + new_validator += 1; + } + InsertOutcome::NewEpoch => new_epoch += 1, + InsertOutcome::Identical => identical += 1, + InsertOutcome::Replaced { .. } => replaced += 1, + InsertOutcome::Invalid => invalid += 1, + }; + + if outcome.is_subscription_candidate() { + Some(ValidatorSubscription { + validator_index: duties.duty.validator_index?, + attestation_committee_index: duties.duty.attestation_committee_index?, + slot: duties.duty.attestation_slot?, + is_aggregator: duties.selection_proof.is_some(), + }) + } else { + None } - - trace!( - log, - "Performed duties update"; - "identical" => identical, - "new_epoch" => new_epoch, - "new_validator" => new_validator, - "replaced" => replaced, - "epoch" => format!("{}", epoch) - ); - - if replaced > 0 { - warn!( - log, - "Duties changed during routine update"; - "info" => "Chain re-org likely occurred." - ) - } - - Ok(()) }) + .collect::>(); + + if invalid > 0 { + error!( + log, + "Received invalid duties from beacon node"; + "bad_duty_count" => invalid, + "info" => "Duties are from wrong epoch." + ) + } + + trace!( + log, + "Performed duties update"; + "identical" => identical, + "new_epoch" => new_epoch, + "new_validator" => new_validator, + "replaced" => replaced, + "epoch" => format!("{}", epoch) + ); + + if replaced > 0 { + warn!( + log, + "Duties changed during routine update"; + "info" => "Chain re-org likely occurred." + ) + } + + let log = self.context.log.clone(); + let count = validator_subscriptions.len(); + + if count == 0 { + debug!(log, "No new subscriptions required"); + + Ok(()) + } else { + self.beacon_node + .http + .validator() + .subscribe(validator_subscriptions) + .await + .map_err(|e| format!("Failed to subscribe validators: {:?}", e)) + .map(move |status| { + match status { + PublishStatus::Valid => debug!( + log, + "Successfully subscribed validators"; + "count" => count + ), + PublishStatus::Unknown => error!( + log, + "Unknown response from subscription"; + ), + PublishStatus::Invalid(e) => error!( + log, + "Failed to subscribe validator"; + "error" => e + ), + }; + }) + } } } diff --git a/validator_client/src/fork_service.rs b/validator_client/src/fork_service.rs index 9ff3a0bf56..412c0272fe 100644 --- a/validator_client/src/fork_service.rs +++ b/validator_client/src/fork_service.rs @@ -1,14 +1,13 @@ use environment::RuntimeContext; use exit_future::Signal; -use futures::{Future, Stream}; +use futures::{FutureExt, StreamExt}; use parking_lot::RwLock; use remote_beacon_node::RemoteBeaconNode; -use slog::{crit, info, trace}; +use slog::{info, trace}; use slot_clock::SlotClock; use std::ops::Deref; use std::sync::Arc; -use std::time::{Duration, Instant}; -use tokio::timer::Interval; +use tokio::time::{interval_at, Duration, Instant}; use types::{ChainSpec, EthSpec, Fork}; /// Delay this period of time after the slot starts. This allows the node to process the new slot. @@ -111,51 +110,46 @@ impl ForkService { let interval = { let slot_duration = Duration::from_millis(spec.milliseconds_per_slot); - Interval::new( + // Note: interval_at panics if `slot_duration * E::slots_per_epoch()` = 0 + interval_at( Instant::now() + duration_to_next_epoch + TIME_DELAY_FROM_SLOT, slot_duration * E::slots_per_epoch() as u32, ) }; let (exit_signal, exit_fut) = exit_future::signal(); - let service = self.clone(); - let log_1 = log.clone(); - let log_2 = log.clone(); // Run an immediate update before starting the updater service. - self.context.executor.spawn(service.clone().do_update()); + let service_1 = self.clone(); + let service_2 = self.clone(); + tokio::task::spawn(service_1.do_update()); - self.context.executor.spawn( - exit_fut - .until( - interval - .map_err(move |e| { - crit! { - log_1, - "Timer thread failed"; - "error" => format!("{}", e) - } - }) - .for_each(move |_| service.do_update().then(|_| Ok(()))), - ) - .map(move |_| info!(log_2, "Shutdown complete")), + let interval_fut = interval.for_each(move |_| { + let _ = service_2.clone().do_update(); + futures::future::ready(()) + }); + + let future = futures::future::select( + interval_fut, + exit_fut.map(move |_| info!(log, "Shutdown complete")), ); + tokio::task::spawn(future); Ok(exit_signal) } /// Attempts to download the `Fork` from the server. - fn do_update(&self) -> impl Future { - let service_1 = self.clone(); - let log_1 = service_1.context.log.clone(); - let log_2 = service_1.context.log.clone(); - - self.inner + async fn do_update(self) -> Result<(), ()> { + let log_1 = self.context.log.clone(); + let log_2 = self.context.log.clone(); + let _ = self + .inner .beacon_node .http .beacon() .get_fork() - .map(move |fork| *(service_1.fork.write()) = Some(fork)) + .await + .map(move |fork| *(self.fork.write()) = Some(fork)) .map(move |_| trace!(log_1, "Fork update success")) .map_err(move |e| { trace!( @@ -163,9 +157,9 @@ impl ForkService { "Fork update failed"; "error" => format!("Error retrieving fork: {:?}", e) ) - }) - // Returning an error will stop the interval. This is not desired, a single failure - // should not stop all future attempts. - .then(|_| Ok(())) + }); + // Returning an error will stop the interval. This is not desired, a single failure + // should not stop all future attempts. + Ok(()) } } diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index ec7e2a743d..d4332dcbcf 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -19,18 +19,13 @@ use duties_service::{DutiesService, DutiesServiceBuilder}; use environment::RuntimeContext; use exit_future::Signal; use fork_service::{ForkService, ForkServiceBuilder}; -use futures::{ - future::{self, loop_fn, Loop}, - Future, IntoFuture, -}; use notifier::spawn_notifier; use remote_beacon_node::RemoteBeaconNode; use slog::{error, info, Logger}; use slot_clock::SlotClock; use slot_clock::SystemTimeSlotClock; -use std::time::{Duration, Instant}; use std::time::{SystemTime, UNIX_EPOCH}; -use tokio::timer::Delay; +use tokio::time::{delay_for, Duration}; use types::EthSpec; use validator_store::ValidatorStore; @@ -52,22 +47,18 @@ pub struct ProductionValidatorClient { impl ProductionValidatorClient { /// Instantiates the validator client, _without_ starting the timers to trigger block /// and attestation production. - pub fn new_from_cli( + pub async fn new_from_cli( context: RuntimeContext, - cli_args: &ArgMatches, - ) -> impl Future { - Config::from_cli(&cli_args) - .into_future() - .map_err(|e| format!("Unable to initialize config: {}", e)) - .and_then(|config| Self::new(context, config)) + cli_args: &ArgMatches<'_>, + ) -> Result { + let config = Config::from_cli(&cli_args) + .map_err(|e| format!("Unable to initialize config: {}", e))?; + Self::new(context, config).await } /// Instantiates the validator client, _without_ starting the timers to trigger block /// and attestation production. - pub fn new( - mut context: RuntimeContext, - config: Config, - ) -> impl Future { + pub async fn new(mut context: RuntimeContext, config: Config) -> Result { let log_1 = context.log.clone(); let log_2 = context.log.clone(); let log_3 = context.log.clone(); @@ -80,184 +71,153 @@ impl ProductionValidatorClient { "datadir" => format!("{:?}", config.data_dir), ); - RemoteBeaconNode::new_with_timeout(config.http_server.clone(), HTTP_TIMEOUT) - .map_err(|e| format!("Unable to init beacon node http client: {}", e)) - .into_future() - .and_then(move |beacon_node| wait_for_node(beacon_node, log_2)) - .and_then(|beacon_node| { - beacon_node - .http - .spec() - .get_eth2_config() - .map(|eth2_config| (beacon_node, eth2_config)) - .map_err(|e| format!("Unable to read eth2 config from beacon node: {:?}", e)) - }) - .and_then(|(beacon_node, eth2_config)| { - beacon_node - .http - .beacon() - .get_genesis_time() - .map(|genesis_time| (beacon_node, eth2_config, genesis_time)) - .map_err(|e| format!("Unable to read genesis time from beacon node: {:?}", e)) - }) - .and_then(move |(beacon_node, remote_eth2_config, genesis_time)| { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .into_future() - .map_err(|e| format!("Unable to read system time: {:?}", e)) - .and_then::<_, Box + Send>>(move |now| { - let log = log_3.clone(); - let genesis = Duration::from_secs(genesis_time); + let beacon_node = + RemoteBeaconNode::new_with_timeout(config.http_server.clone(), HTTP_TIMEOUT) + .map_err(|e| format!("Unable to init beacon node http client: {}", e))?; - // If the time now is less than (prior to) genesis, then delay until the - // genesis instant. - // - // If the validator client starts before genesis, it will get errors from - // the slot clock. - if now < genesis { - info!( - log, - "Starting node prior to genesis"; - "seconds_to_wait" => (genesis - now).as_secs() - ); + // TODO: check if all logs in wait_for_node are produed while awaiting + let beacon_node = wait_for_node(beacon_node, log_2).await?; + let eth2_config = beacon_node + .http + .spec() + .get_eth2_config() + .await + .map_err(|e| format!("Unable to read eth2 config from beacon node: {:?}", e))?; + let genesis_time = beacon_node + .http + .beacon() + .get_genesis_time() + .await + .map_err(|e| format!("Unable to read genesis time from beacon node: {:?}", e))?; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|e| format!("Unable to read system time: {:?}", e))?; + let log = log_3.clone(); + let genesis = Duration::from_secs(genesis_time); - Box::new( - Delay::new(Instant::now() + (genesis - now)) - .map_err(|e| { - format!("Unable to create genesis wait delay: {:?}", e) - }) - .map(move |_| (beacon_node, remote_eth2_config, genesis_time)), - ) - } else { - info!( - log, - "Genesis has already occurred"; - "seconds_ago" => (now - genesis).as_secs() - ); + // If the time now is less than (prior to) genesis, then delay until the + // genesis instant. + // + // If the validator client starts before genesis, it will get errors from + // the slot clock. + if now < genesis { + info!( + log, + "Starting node prior to genesis"; + "seconds_to_wait" => (genesis - now).as_secs() + ); - Box::new(future::ok((beacon_node, remote_eth2_config, genesis_time))) - } - }) - }) - .and_then(|(beacon_node, eth2_config, genesis_time)| { - beacon_node - .http - .beacon() - .get_genesis_validators_root() - .map(move |genesis_validators_root| { - ( - beacon_node, - eth2_config, - genesis_time, - genesis_validators_root, - ) - }) - .map_err(|e| { - format!( - "Unable to read genesis validators root from beacon node: {:?}", - e - ) - }) - }) - .and_then( - move |(beacon_node, remote_eth2_config, genesis_time, genesis_validators_root)| { - let log = log_4.clone(); + delay_for(genesis - now).await + } else { + info!( + log, + "Genesis has already occurred"; + "seconds_ago" => (now - genesis).as_secs() + ); + } + let genesis_validators_root = beacon_node + .http + .beacon() + .get_genesis_validators_root() + .await + .map_err(|e| { + format!( + "Unable to read genesis validators root from beacon node: {:?}", + e + ) + })?; + let log = log_4.clone(); - // Do not permit a connection to a beacon node using different spec constants. - if context.eth2_config.spec_constants != remote_eth2_config.spec_constants { - return Err(format!( - "Beacon node is using an incompatible spec. Got {}, expected {}", - remote_eth2_config.spec_constants, context.eth2_config.spec_constants - )); - } + // Do not permit a connection to a beacon node using different spec constants. + if context.eth2_config.spec_constants != eth2_config.spec_constants { + return Err(format!( + "Beacon node is using an incompatible spec. Got {}, expected {}", + eth2_config.spec_constants, context.eth2_config.spec_constants + )); + } - // Note: here we just assume the spec variables of the remote node. This is very useful - // for testnets, but perhaps a security issue when it comes to mainnet. - // - // A damaging attack would be for a beacon node to convince the validator client of a - // different `SLOTS_PER_EPOCH` variable. This could result in slashable messages being - // produced. We are safe from this because `SLOTS_PER_EPOCH` is a type-level constant - // for Lighthouse. - context.eth2_config = remote_eth2_config; + // Note: here we just assume the spec variables of the remote node. This is very useful + // for testnets, but perhaps a security issue when it comes to mainnet. + // + // A damaging attack would be for a beacon node to convince the validator client of a + // different `SLOTS_PER_EPOCH` variable. This could result in slashable messages being + // produced. We are safe from this because `SLOTS_PER_EPOCH` is a type-level constant + // for Lighthouse. + context.eth2_config = eth2_config; - let slot_clock = SystemTimeSlotClock::new( - context.eth2_config.spec.genesis_slot, - Duration::from_secs(genesis_time), - Duration::from_millis(context.eth2_config.spec.milliseconds_per_slot), - ); + let slot_clock = SystemTimeSlotClock::new( + context.eth2_config.spec.genesis_slot, + Duration::from_secs(genesis_time), + Duration::from_millis(context.eth2_config.spec.milliseconds_per_slot), + ); - let fork_service = ForkServiceBuilder::new() - .slot_clock(slot_clock.clone()) - .beacon_node(beacon_node.clone()) - .runtime_context(context.service_context("fork".into())) - .build()?; + let fork_service = ForkServiceBuilder::new() + .slot_clock(slot_clock.clone()) + .beacon_node(beacon_node.clone()) + .runtime_context(context.service_context("fork".into())) + .build()?; - let validator_store: ValidatorStore = - match &config.key_source { - // Load pre-existing validators from the data dir. - // - // Use the `account_manager` to generate these files. - KeySource::Disk => ValidatorStore::load_from_disk( - config.data_dir.clone(), - genesis_validators_root, - context.eth2_config.spec.clone(), - fork_service.clone(), - log.clone(), - )?, - // Generate ephemeral insecure keypairs for testing purposes. - // - // Do not use in production. - KeySource::InsecureKeypairs(indices) => { - ValidatorStore::insecure_ephemeral_validators( - &indices, - genesis_validators_root, - context.eth2_config.spec.clone(), - fork_service.clone(), - log.clone(), - )? - } - }; + let validator_store: ValidatorStore = match &config.key_source { + // Load pre-existing validators from the data dir. + // + // Use the `account_manager` to generate these files. + KeySource::Disk => ValidatorStore::load_from_disk( + config.data_dir.clone(), + genesis_validators_root, + context.eth2_config.spec.clone(), + fork_service.clone(), + log.clone(), + )?, + // Generate ephemeral insecure keypairs for testing purposes. + // + // Do not use in production. + KeySource::InsecureKeypairs(indices) => ValidatorStore::insecure_ephemeral_validators( + &indices, + genesis_validators_root, + context.eth2_config.spec.clone(), + fork_service.clone(), + log.clone(), + )?, + }; - info!( - log, - "Loaded validator keypair store"; - "voting_validators" => validator_store.num_voting_validators() - ); + info!( + log, + "Loaded validator keypair store"; + "voting_validators" => validator_store.num_voting_validators() + ); - let duties_service = DutiesServiceBuilder::new() - .slot_clock(slot_clock.clone()) - .validator_store(validator_store.clone()) - .beacon_node(beacon_node.clone()) - .runtime_context(context.service_context("duties".into())) - .allow_unsynced_beacon_node(config.allow_unsynced_beacon_node) - .build()?; + let duties_service = DutiesServiceBuilder::new() + .slot_clock(slot_clock.clone()) + .validator_store(validator_store.clone()) + .beacon_node(beacon_node.clone()) + .runtime_context(context.service_context("duties".into())) + .allow_unsynced_beacon_node(config.allow_unsynced_beacon_node) + .build()?; - let block_service = BlockServiceBuilder::new() - .duties_service(duties_service.clone()) - .slot_clock(slot_clock.clone()) - .validator_store(validator_store.clone()) - .beacon_node(beacon_node.clone()) - .runtime_context(context.service_context("block".into())) - .build()?; + let block_service = BlockServiceBuilder::new() + .duties_service(duties_service.clone()) + .slot_clock(slot_clock.clone()) + .validator_store(validator_store.clone()) + .beacon_node(beacon_node.clone()) + .runtime_context(context.service_context("block".into())) + .build()?; - let attestation_service = AttestationServiceBuilder::new() - .duties_service(duties_service.clone()) - .slot_clock(slot_clock) - .validator_store(validator_store) - .beacon_node(beacon_node) - .runtime_context(context.service_context("attestation".into())) - .build()?; + let attestation_service = AttestationServiceBuilder::new() + .duties_service(duties_service.clone()) + .slot_clock(slot_clock) + .validator_store(validator_store) + .beacon_node(beacon_node) + .runtime_context(context.service_context("attestation".into())) + .build()?; - Ok(Self { - context, - duties_service, - fork_service, - block_service, - attestation_service, - exit_signals: vec![], - }) - }, - ) + Ok(Self { + context, + duties_service, + fork_service, + block_service, + attestation_service, + exit_signals: vec![], + }) } pub fn start_service(&mut self) -> Result<(), String> { @@ -298,48 +258,39 @@ impl ProductionValidatorClient { /// Request the version from the node, looping back and trying again on failure. Exit once the node /// has been contacted. -fn wait_for_node( +async fn wait_for_node( beacon_node: RemoteBeaconNode, log: Logger, -) -> impl Future, Error = String> { +) -> Result, String> { // Try to get the version string from the node, looping until success is returned. - loop_fn(beacon_node.clone(), move |beacon_node| { + loop { let log = log.clone(); - beacon_node + let result = beacon_node .clone() .http .node() .get_version() - .map_err(|e| format!("{:?}", e)) - .then(move |result| { - let future: Box, Error = String> + Send> = match result - { - Ok(version) => { - info!( - log, - "Connected to beacon node"; - "version" => version, - ); + .await + .map_err(|e| format!("{:?}", e)); - Box::new(future::ok(Loop::Break(beacon_node))) - } - Err(e) => { - error!( - log, - "Unable to connect to beacon node"; - "error" => format!("{:?}", e), - ); + match result { + Ok(version) => { + info!( + log, + "Connected to beacon node"; + "version" => version, + ); - Box::new( - Delay::new(Instant::now() + RETRY_DELAY) - .map_err(|e| format!("Failed to trigger delay: {:?}", e)) - .and_then(|_| future::ok(Loop::Continue(beacon_node))), - ) - } - }; - - future - }) - }) - .map(|_| beacon_node) + return Ok(beacon_node); + } + Err(e) => { + error!( + log, + "Unable to connect to beacon node"; + "error" => format!("{:?}", e), + ); + delay_for(RETRY_DELAY).await; + } + } + } } diff --git a/validator_client/src/notifier.rs b/validator_client/src/notifier.rs index e60bf8cc62..9b611a4c30 100644 --- a/validator_client/src/notifier.rs +++ b/validator_client/src/notifier.rs @@ -1,10 +1,9 @@ use crate::ProductionValidatorClient; use exit_future::Signal; -use futures::{Future, Stream}; +use futures::{FutureExt, StreamExt}; use slog::{error, info}; use slot_clock::SlotClock; -use std::time::{Duration, Instant}; -use tokio::timer::Interval; +use tokio::time::{interval_at, Duration, Instant}; use types::EthSpec; /// Spawns a notifier service which periodically logs information about the node. @@ -26,66 +25,63 @@ pub fn spawn_notifier(client: &ProductionValidatorClient) -> Resu let duties_service = client.duties_service.clone(); let log_1 = context.log.clone(); - let log_2 = context.log.clone(); - let interval_future = Interval::new(start_instant, interval_duration) - .map_err( - move |e| error!(log_1, "Slot notifier timer failed"; "error" => format!("{:?}", e)), - ) - .for_each(move |_| { - let log = log_2.clone(); + // Note: interval_at panics if `interval_duration` is 0 + let interval_fut = interval_at(start_instant, interval_duration).for_each(move |_| { + let log = log_1.clone(); - if let Some(slot) = duties_service.slot_clock.now() { - let epoch = slot.epoch(T::slots_per_epoch()); + if let Some(slot) = duties_service.slot_clock.now() { + let epoch = slot.epoch(T::slots_per_epoch()); - let total_validators = duties_service.total_validator_count(); - let proposing_validators = duties_service.proposer_count(epoch); - let attesting_validators = duties_service.attester_count(epoch); + let total_validators = duties_service.total_validator_count(); + let proposing_validators = duties_service.proposer_count(epoch); + let attesting_validators = duties_service.attester_count(epoch); - if total_validators == 0 { - error!(log, "No validators present") - } else if total_validators == attesting_validators { - info!( - log_2, - "All validators active"; - "proposers" => proposing_validators, - "active_validators" => attesting_validators, - "total_validators" => total_validators, - "epoch" => format!("{}", epoch), - "slot" => format!("{}", slot), - ); - } else if attesting_validators > 0 { - info!( - log_2, - "Some validators active"; - "proposers" => proposing_validators, - "active_validators" => attesting_validators, - "total_validators" => total_validators, - "epoch" => format!("{}", epoch), - "slot" => format!("{}", slot), - ); - } else { - info!( - log_2, - "Awaiting activation"; - "validators" => total_validators, - "epoch" => format!("{}", epoch), - "slot" => format!("{}", slot), - ); - } + if total_validators == 0 { + error!(log, "No validators present") + } else if total_validators == attesting_validators { + info!( + log_1, + "All validators active"; + "proposers" => proposing_validators, + "active_validators" => attesting_validators, + "total_validators" => total_validators, + "epoch" => format!("{}", epoch), + "slot" => format!("{}", slot), + ); + } else if attesting_validators > 0 { + info!( + log_1, + "Some validators active"; + "proposers" => proposing_validators, + "active_validators" => attesting_validators, + "total_validators" => total_validators, + "epoch" => format!("{}", epoch), + "slot" => format!("{}", slot), + ); } else { - error!(log, "Unable to read slot clock"); + info!( + log_1, + "Awaiting activation"; + "validators" => total_validators, + "epoch" => format!("{}", epoch), + "slot" => format!("{}", slot), + ); } + } else { + error!(log, "Unable to read slot clock"); + } - Ok(()) - }); + futures::future::ready(()) + }); let (exit_signal, exit) = exit_future::signal(); let log = context.log.clone(); - client.context.executor.spawn( - exit.until(interval_future) - .map(move |_| info!(log, "Shutdown complete")), + let future = futures::future::select( + interval_fut, + exit.map(move |_| info!(log, "Shutdown complete")), ); + tokio::task::spawn(future); Ok(exit_signal) } diff --git a/validator_client/src/validator_directory.rs b/validator_client/src/validator_directory.rs index 197e1cb44e..35bf580d89 100644 --- a/validator_client/src/validator_directory.rs +++ b/validator_client/src/validator_directory.rs @@ -1,6 +1,6 @@ use bls::get_withdrawal_credentials; use deposit_contract::{encode_eth1_tx_data, DEPOSIT_GAS}; -use futures::{Future, IntoFuture}; +use futures::compat::Future01CompatExt; use hex; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; @@ -303,29 +303,27 @@ impl ValidatorDirectoryBuilder { Ok(self) } - pub fn submit_eth1_deposit( - self, + pub async fn submit_eth1_deposit( + &self, web3: Web3, from: Address, deposit_contract: Address, - ) -> impl Future { - self.get_deposit_data() - .into_future() - .and_then(move |(deposit_data, deposit_amount)| { - web3.eth() - .send_transaction(TransactionRequest { - from, - to: Some(deposit_contract), - gas: Some(DEPOSIT_GAS.into()), - gas_price: None, - value: Some(from_gwei(deposit_amount)), - data: Some(deposit_data.into()), - nonce: None, - condition: None, - }) - .map_err(|e| format!("Failed to send transaction: {:?}", e)) + ) -> Result { + let (deposit_data, deposit_amount) = self.get_deposit_data()?; + web3.eth() + .send_transaction(TransactionRequest { + from, + to: Some(deposit_contract), + gas: Some(DEPOSIT_GAS.into()), + gas_price: None, + value: Some(from_gwei(deposit_amount)), + data: Some(deposit_data.into()), + nonce: None, + condition: None, }) - .map(|tx| (self, tx)) + .compat() + .await + .map_err(|e| format!("Failed to send transaction: {:?}", e)) } pub fn build(self) -> Result { diff --git a/validator_client/src/validator_store.rs b/validator_client/src/validator_store.rs index 5846e73632..6b62fb02e8 100644 --- a/validator_client/src/validator_store.rs +++ b/validator_client/src/validator_store.rs @@ -224,6 +224,7 @@ impl ValidatorStore { validator_pubkey: &PublicKey, validator_index: u64, aggregate: Attestation, + selection_proof: SelectionProof, ) -> Option> { let validators = self.validators.read(); let voting_keypair = validators.get(validator_pubkey)?.voting_keypair.as_ref()?; @@ -231,6 +232,7 @@ impl ValidatorStore { Some(SignedAggregateAndProof::from_aggregate( validator_index, aggregate, + Some(selection_proof), &voting_keypair.sk, &self.fork()?, self.genesis_validators_root,