diff --git a/Cargo.lock b/Cargo.lock index e37e3903dbd..b3ef7c9c88a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,7 +49,7 @@ dependencies = [ "eth2_keystore", "eth2_wallet", "filesystem", - "rand", + "rand 0.8.5", "regex", "rpassword", "serde", @@ -240,7 +240,7 @@ dependencies = [ "k256 0.13.3", "keccak-asm", "proptest", - "rand", + "rand 0.8.5", "ruint", "serde", "tiny-keccak", @@ -313,6 +313,15 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" +[[package]] +name = "archery" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a8da9bc4c4053ee067669762bcaeea6e241841295a2b6c948312dad6ef4cc02" +dependencies = [ + "static_assertions", +] + [[package]] name = "ark-ff" version = "0.3.0" @@ -424,7 +433,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" dependencies = [ "num-traits", - "rand", + "rand 0.8.5", ] [[package]] @@ -434,7 +443,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" dependencies = [ "num-traits", - "rand", + "rand 0.8.5", ] [[package]] @@ -616,6 +625,15 @@ dependencies = [ "syn 2.0.52", ] +[[package]] +name = "autocfg" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" +dependencies = [ + "autocfg 1.1.0", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -734,6 +752,7 @@ version = "0.2.0" dependencies = [ "bitvec 1.0.1", "bls", + "crossbeam-channel", "derivative", "environment", "eth1", @@ -760,8 +779,9 @@ dependencies = [ "oneshot_broadcast", "operation_pool", "parking_lot 0.12.1", + "promise_cache", "proto_array", - "rand", + "rand 0.8.5", "rayon", "safe_arith", "sensitive_url", @@ -790,7 +810,7 @@ dependencies = [ [[package]] name = "beacon_node" -version = "5.1.3" +version = "5.1.222-exp" dependencies = [ "beacon_chain", "clap", @@ -991,12 +1011,13 @@ version = "0.2.0" dependencies = [ "arbitrary", "blst", + "criterion", "ethereum-types 0.14.1", "ethereum_hashing", "ethereum_serde_utils", "ethereum_ssz", "hex", - "rand", + "rand 0.8.5", "serde", "tree_hash", "zeroize", @@ -1026,7 +1047,7 @@ dependencies = [ [[package]] name = "boot_node" -version = "5.1.3" +version = "5.1.222-exp" dependencies = [ "beacon_node", "clap", @@ -1358,6 +1379,15 @@ dependencies = [ "types", ] +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "cmake" version = "0.1.50" @@ -1549,7 +1579,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -1561,7 +1591,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -1573,7 +1603,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "typenum", ] @@ -1770,6 +1800,7 @@ dependencies = [ "clap", "clap_utils", "environment", + "ethereum_ssz", "hex", "logging", "slog", @@ -2027,7 +2058,7 @@ dependencies = [ "lru", "more-asserts", "parking_lot 0.11.2", - "rand", + "rand 0.8.5", "rlp", "smallvec", "socket2 0.4.10", @@ -2104,7 +2135,7 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core", + "rand_core 0.6.4", "serde", "sha2 0.10.8", "subtle", @@ -2165,7 +2196,7 @@ dependencies = [ "ff 0.12.1", "generic-array", "group 0.12.1", - "rand_core", + "rand_core 0.6.4", "sec1 0.3.0", "subtle", "zeroize", @@ -2185,7 +2216,7 @@ dependencies = [ "group 0.13.0", "pem-rfc7468", "pkcs8 0.10.2", - "rand_core", + "rand_core 0.6.4", "sec1 0.7.3", "subtle", "zeroize", @@ -2212,7 +2243,7 @@ dependencies = [ "hex", "k256 0.13.3", "log", - "rand", + "rand 0.8.5", "rlp", "serde", "sha3 0.10.8", @@ -2433,7 +2464,7 @@ dependencies = [ "hex", "hmac 0.11.0", "pbkdf2 0.8.0", - "rand", + "rand 0.8.5", "scrypt", "serde", "serde_json", @@ -2475,7 +2506,7 @@ dependencies = [ "eth2_key_derivation", "eth2_keystore", "hex", - "rand", + "rand 0.8.5", "serde", "serde_json", "serde_repr", @@ -2707,7 +2738,7 @@ dependencies = [ "k256 0.11.6", "once_cell", "open-fastrlp", - "rand", + "rand 0.8.5", "rlp", "rlp-derive", "serde", @@ -2834,7 +2865,7 @@ dependencies = [ "lru", "parking_lot 0.12.1", "pretty_reqwest_error", - "rand", + "rand 0.8.5", "reqwest", "sensitive_url", "serde", @@ -2902,7 +2933,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -2912,7 +2943,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -2959,7 +2990,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" dependencies = [ "byteorder", - "rand", + "rand 0.8.5", "rustc-hex", "static_assertions", ] @@ -2972,7 +3003,7 @@ checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" dependencies = [ "arbitrary", "byteorder", - "rand", + "rand 0.8.5", "rustc-hex", "static_assertions", ] @@ -3043,6 +3074,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "funty" version = "1.1.0" @@ -3320,7 +3357,7 @@ dependencies = [ "quick-protobuf", "quick-protobuf-codec 0.3.1", "quickcheck", - "rand", + "rand 0.8.5", "regex", "serde", "sha2 0.10.8", @@ -3336,7 +3373,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" dependencies = [ "ff 0.12.1", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -3347,7 +3384,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff 0.13.0", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -3535,7 +3572,7 @@ dependencies = [ "idna 0.4.0", "ipnet", "once_cell", - "rand", + "rand 0.8.5", "socket2 0.5.6", "thiserror", "tinyvec", @@ -3557,7 +3594,7 @@ dependencies = [ "lru-cache", "once_cell", "parking_lot 0.12.1", - "rand", + "rand 0.8.5", "resolv-conf", "smallvec", "thiserror", @@ -3954,7 +3991,7 @@ dependencies = [ "http 0.2.11", "hyper 0.14.28", "log", - "rand", + "rand 0.8.5", "tokio", "url", "xmltree", @@ -4028,7 +4065,7 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ - "autocfg", + "autocfg 1.1.0", "hashbrown 0.12.3", ] @@ -4288,7 +4325,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lcli" -version = "5.1.3" +version = "5.1.222-exp" dependencies = [ "account_utils", "beacon_chain", @@ -4486,7 +4523,7 @@ dependencies = [ "parking_lot 0.12.1", "pin-project", "quick-protobuf", - "rand", + "rand 0.8.5", "rw-stream-sink", "smallvec", "thiserror", @@ -4548,7 +4585,7 @@ dependencies = [ "multihash", "p256", "quick-protobuf", - "rand", + "rand 0.8.5", "sec1 0.7.3", "sha2 0.10.8", "thiserror", @@ -4570,7 +4607,7 @@ dependencies = [ "libp2p-core", "libp2p-identity", "libp2p-swarm", - "rand", + "rand 0.8.5", "smallvec", "socket2 0.5.6", "tokio", @@ -4607,7 +4644,7 @@ dependencies = [ "libp2p-identity", "nohash-hasher", "parking_lot 0.12.1", - "rand", + "rand 0.8.5", "smallvec", "tracing", "unsigned-varint 0.7.2", @@ -4629,7 +4666,7 @@ dependencies = [ "multihash", "once_cell", "quick-protobuf", - "rand", + "rand 0.8.5", "sha2 0.10.8", "snow", "static_assertions", @@ -4670,7 +4707,7 @@ dependencies = [ "libp2p-tls", "parking_lot 0.12.1", "quinn", - "rand", + "rand 0.8.5", "ring 0.16.20", "rustls", "socket2 0.5.6", @@ -4695,7 +4732,7 @@ dependencies = [ "libp2p-swarm-derive", "multistream-select", "once_cell", - "rand", + "rand 0.8.5", "smallvec", "tokio", "tracing", @@ -4805,7 +4842,7 @@ dependencies = [ "libsecp256k1-core", "libsecp256k1-gen-ecmult", "libsecp256k1-gen-genmult", - "rand", + "rand 0.8.5", "serde", "sha2 0.9.9", "typenum", @@ -4864,7 +4901,7 @@ dependencies = [ [[package]] name = "lighthouse" -version = "5.1.3" +version = "5.1.222-exp" dependencies = [ "account_manager", "account_utils", @@ -4896,6 +4933,7 @@ dependencies = [ "slashing_protection", "slog", "sloggers", + "store", "task_executor", "tempfile", "tracing-subscriber", @@ -4949,7 +4987,7 @@ dependencies = [ "prometheus-client", "quickcheck", "quickcheck_macros", - "rand", + "rand 0.8.5", "regex", "serde", "sha2 0.9.9", @@ -5030,7 +5068,7 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ - "autocfg", + "autocfg 1.1.0", "scopeguard", ] @@ -5190,7 +5228,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ - "autocfg", + "autocfg 1.1.0", ] [[package]] @@ -5249,6 +5287,28 @@ dependencies = [ "quote", ] +[[package]] +name = "milhouse" +version = "0.1.0" +source = "git+https://github.com/sigp/milhouse?branch=main#40a536490b14dc95834f9ece0001e8e04f7b38d7" +dependencies = [ + "arbitrary", + "derivative", + "ethereum-types 0.14.1", + "ethereum_hashing", + "ethereum_ssz", + "ethereum_ssz_derive", + "itertools", + "parking_lot 0.11.2", + "rayon", + "serde", + "smallvec", + "tree_hash", + "triomphe", + "typenum", + "vec_map", +] + [[package]] name = "mime" version = "0.3.17" @@ -5493,7 +5553,7 @@ dependencies = [ "num_cpus", "operation_pool", "parking_lot 0.12.1", - "rand", + "rand 0.8.5", "rlp", "slog", "slog-async", @@ -5590,7 +5650,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ - "autocfg", + "autocfg 1.1.0", "num-integer", "num-traits", ] @@ -5607,7 +5667,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "serde", "smallvec", "zeroize", @@ -5634,7 +5694,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" dependencies = [ - "autocfg", + "autocfg 1.1.0", "num-integer", "num-traits", ] @@ -5645,7 +5705,7 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ - "autocfg", + "autocfg 1.1.0", "libm", ] @@ -5795,7 +5855,7 @@ dependencies = [ "lighthouse_metrics", "maplit", "parking_lot 0.12.1", - "rand", + "rand 0.8.5", "rayon", "serde", "state_processing", @@ -5935,7 +5995,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" dependencies = [ "base64ct", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -6205,7 +6265,7 @@ dependencies = [ "hmac 0.12.1", "md-5", "memchr", - "rand", + "rand 0.8.5", "sha2 0.10.8", "stringprep", ] @@ -6377,6 +6437,16 @@ dependencies = [ "syn 2.0.52", ] +[[package]] +name = "promise_cache" +version = "0.1.0" +dependencies = [ + "derivative", + "itertools", + "oneshot_broadcast", + "slog", +] + [[package]] name = "proptest" version = "1.4.0" @@ -6388,9 +6458,9 @@ dependencies = [ "bitflags 2.4.2", "lazy_static", "num-traits", - "rand", - "rand_chacha", - "rand_xorshift", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_xorshift 0.3.0", "regex-syntax 0.8.2", "rusty-fork", "tempfile", @@ -6484,7 +6554,7 @@ checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" dependencies = [ "env_logger 0.8.4", "log", - "rand", + "rand 0.8.5", ] [[package]] @@ -6523,7 +6593,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "141bf7dfde2fbc246bfd3fe12f2455aa24b0fbd9af535d8c86c7bd1381ff2b1a" dependencies = [ "bytes", - "rand", + "rand 0.8.5", "ring 0.16.20", "rustc-hash", "rustls", @@ -6588,6 +6658,25 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.8", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift 0.1.1", + "winapi", +] + [[package]] name = "rand" version = "0.8.5" @@ -6595,8 +6684,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.3.1", ] [[package]] @@ -6606,9 +6705,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + [[package]] name = "rand_core" version = "0.6.4" @@ -6618,13 +6732,75 @@ 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_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", +] + +[[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", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.8", + "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.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -6659,6 +6835,15 @@ dependencies = [ "yasna", ] +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -6876,6 +7061,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "rpds" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ef5140bcb576bfd6d56cd2de709a7d17851ac1f3805e67fe9d99e42a11821f" +dependencies = [ + "archery", +] + [[package]] name = "rtnetlink" version = "0.10.1" @@ -6907,7 +7101,7 @@ dependencies = [ "parity-scale-codec 3.6.9", "primitive-types 0.12.2", "proptest", - "rand", + "rand 0.8.5", "rlp", "ruint-macro", "serde", @@ -7489,7 +7683,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" dependencies = [ "digest 0.10.7", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -7499,7 +7693,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest 0.10.7", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -7544,7 +7738,7 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ - "autocfg", + "autocfg 1.1.0", ] [[package]] @@ -7566,7 +7760,7 @@ dependencies = [ "lru", "maplit", "parking_lot 0.12.1", - "rand", + "rand 0.8.5", "rayon", "safe_arith", "serde", @@ -7748,7 +7942,7 @@ dependencies = [ "blake2", "chacha20poly1305", "curve25519-dalek", - "rand_core", + "rand_core 0.6.4", "ring 0.17.8", "rustc_version 0.4.0", "sha2 0.10.8", @@ -7825,6 +8019,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "state_processing" version = "0.2.0" @@ -7850,6 +8050,7 @@ dependencies = [ "tokio", "tree_hash", "types", + "vec_map", ] [[package]] @@ -7875,6 +8076,7 @@ name = "store" version = "0.2.0" dependencies = [ "beacon_chain", + "bls", "db-key", "directory", "ethereum_ssz", @@ -7883,15 +8085,20 @@ dependencies = [ "lazy_static", "leveldb", "lighthouse_metrics", + "logging", "lru", "parking_lot 0.12.1", + "safe_arith", "serde", "slog", "sloggers", + "smallvec", "state_processing", "strum", "tempfile", "types", + "xdelta3", + "zstd", ] [[package]] @@ -7947,9 +8154,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "superstruct" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b9e5728aa1a87141cefd4e7509903fc01fa0dcb108022b1e841a67c5159fc5" +checksum = "6f4e1f478a7728f8855d7e620e9a152cf8932c6614f86564c886f9b8141f3201" dependencies = [ "darling", "itertools", @@ -8089,6 +8296,7 @@ dependencies = [ "futures", "lazy_static", "lighthouse_metrics", + "logging", "slog", "sloggers", "tokio", @@ -8153,7 +8361,7 @@ dependencies = [ "hex", "hmac 0.12.1", "log", - "rand", + "rand 0.8.5", "serde", "serde_json", "sha2 0.10.8", @@ -8259,7 +8467,7 @@ dependencies = [ "hmac 0.12.1", "once_cell", "pbkdf2 0.11.0", - "rand", + "rand 0.8.5", "rustc-hash", "sha2 0.10.8", "thiserror", @@ -8370,7 +8578,7 @@ dependencies = [ "pin-project-lite", "postgres-protocol", "postgres-types", - "rand", + "rand 0.8.5", "socket2 0.5.6", "tokio", "tokio-util 0.7.10", @@ -8647,6 +8855,16 @@ dependencies = [ "rlp", ] +[[package]] +name = "triomphe" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee8098afad3fb0c54a9007aab6804558410503ad676d4633f9c2559a00ac0f" +dependencies = [ + "serde", + "stable_deref_trait", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -8686,12 +8904,14 @@ dependencies = [ "maplit", "merkle_proof", "metastruct", + "milhouse", "parking_lot 0.12.1", "paste", - "rand", - "rand_xorshift", + "rand 0.8.5", + "rand_xorshift 0.3.0", "rayon", "regex", + "rpds", "rusqlite", "safe_arith", "serde", @@ -8908,7 +9128,7 @@ dependencies = [ "malloc_utils", "monitoring_api", "parking_lot 0.12.1", - "rand", + "rand 0.8.5", "reqwest", "ring 0.16.20", "safe_arith", @@ -8945,7 +9165,7 @@ dependencies = [ "filesystem", "hex", "lockfile", - "rand", + "rand 0.8.5", "tempfile", "tree_hash", "types", @@ -9211,7 +9431,7 @@ dependencies = [ "logging", "network", "r2d2", - "rand", + "rand 0.8.5", "reqwest", "serde", "serde_json", @@ -9631,7 +9851,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ "curve25519-dalek", - "rand_core", + "rand_core 0.6.4", "serde", "zeroize", ] @@ -9653,6 +9873,20 @@ dependencies = [ "time", ] +[[package]] +name = "xdelta3" +version = "0.1.5" +source = "git+http://github.com/michaelsproul/xdelta3-rs?rev=ae9a1d2585ef998f4656acdc35cf263ee88e6dfa#ae9a1d2585ef998f4656acdc35cf263ee88e6dfa" +dependencies = [ + "bindgen 0.66.1", + "cc", + "futures-io", + "futures-util", + "libc", + "log", + "rand 0.6.5", +] + [[package]] name = "xml-rs" version = "0.8.19" @@ -9688,7 +9922,7 @@ dependencies = [ "nohash-hasher", "parking_lot 0.12.1", "pin-project", - "rand", + "rand 0.8.5", "static_assertions", ] @@ -9703,7 +9937,7 @@ dependencies = [ "nohash-hasher", "parking_lot 0.12.1", "pin-project", - "rand", + "rand 0.8.5", "static_assertions", ] diff --git a/Cargo.toml b/Cargo.toml index 38018c712d5..61e0d7bb05a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ members = [ "common/malloc_utils", "common/oneshot_broadcast", "common/pretty_reqwest_error", + "common/promise_cache", "common/sensitive_url", "common/slot_clock", "common/system_health", @@ -105,6 +106,7 @@ bytes = "1" clap = "2" compare_fields_derive = { path = "common/compare_fields_derive" } criterion = "0.3" +crossbeam-channel = "0.5.8" delay_map = "0.3" derivative = "2" dirs = "3" @@ -131,6 +133,7 @@ libsecp256k1 = "0.7" log = "0.4" lru = "0.12" maplit = "1" +milhouse = { git = "https://github.com/sigp/milhouse", branch = "main" } num_cpus = "1" parking_lot = "0.12" paste = "1" @@ -157,7 +160,7 @@ smallvec = "1.11.2" snap = "1" ssz_types = "0.5" strum = { version = "0.24", features = ["derive"] } -superstruct = "0.6" +superstruct = "0.7" syn = "1" sysinfo = "0.26" tempfile = "3" @@ -174,8 +177,10 @@ tree_hash_derive = "0.5" url = "2" uuid = { version = "0.8", features = ["serde", "v4"] } warp = { version = "0.3.6", default-features = false, features = ["tls"] } +xdelta3 = { git = "http://github.com/michaelsproul/xdelta3-rs", rev="ae9a1d2585ef998f4656acdc35cf263ee88e6dfa" } zeroize = { version = "1", features = ["zeroize_derive"] } zip = "0.6" +zstd = "0.11.2" # Local crates. account_utils = { path = "common/account_utils" } diff --git a/account_manager/src/validator/exit.rs b/account_manager/src/validator/exit.rs index bc9e0ee1dd6..f5cdd635188 100644 --- a/account_manager/src/validator/exit.rs +++ b/account_manager/src/validator/exit.rs @@ -203,8 +203,8 @@ async fn publish_voluntary_exit( let validator_data = get_validator_data(client, &keypair.pk).await?; match validator_data.status { ValidatorStatus::ActiveExiting => { - let exit_epoch = validator_data.validator.exit_epoch; - let withdrawal_epoch = validator_data.validator.withdrawable_epoch; + let exit_epoch = validator_data.validator.exit_epoch(); + let withdrawal_epoch = validator_data.validator.withdrawable_epoch(); let current_epoch = get_current_epoch::(genesis_data.genesis_time, spec) .ok_or("Failed to get current epoch. Please check your system time")?; eprintln!("Voluntary exit has been accepted into the beacon chain, but not yet finalized. \ @@ -224,7 +224,7 @@ async fn publish_voluntary_exit( ValidatorStatus::ExitedSlashed | ValidatorStatus::ExitedUnslashed => { eprintln!( "Validator has exited on epoch: {}", - validator_data.validator.exit_epoch + validator_data.validator.exit_epoch() ); break; } @@ -250,7 +250,7 @@ async fn get_validator_index_for_exit( ValidatorStatus::ActiveOngoing => { let eligible_epoch = validator_data .validator - .activation_epoch + .activation_epoch() .safe_add(spec.shard_committee_period) .map_err(|e| format!("Failed to calculate eligible epoch, validator activation epoch too high: {:?}", e))?; diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index 7cc6e2b6ae8..d456d0e2038 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "beacon_node" -version = "5.1.3" +version = "5.1.222-exp" authors = [ "Paul Hauner ", "Age Manning BeaconChain { let mut inactivity_penalty = 0i64; if eligible { - let effective_balance = validator.effective_balance; + let effective_balance = validator.effective_balance(); for flag_index in 0..PARTICIPATION_FLAG_WEIGHTS.len() { let (ideal_reward, penalty) = ideal_rewards_hashmap .get(&(flag_index, effective_balance)) .ok_or(BeaconChainError::AttestationRewardsError)?; - let voted_correctly = !validator.slashed + let voted_correctly = !validator.slashed() && previous_epoch_participation_flags.has_flag(flag_index)?; if voted_correctly { if flag_index == TIMELY_HEAD_FLAG_INDEX { diff --git a/beacon_node/beacon_chain/src/beacon_block_reward.rs b/beacon_node/beacon_chain/src/beacon_block_reward.rs index 5b70215d225..9ee5ec41eed 100644 --- a/beacon_node/beacon_chain/src/beacon_block_reward.rs +++ b/beacon_node/beacon_chain/src/beacon_block_reward.rs @@ -135,7 +135,7 @@ impl BeaconChain { proposer_slashing_reward.safe_add_assign( state .get_validator(proposer_slashing.proposer_index() as usize)? - .effective_balance + .effective_balance() .safe_div(self.spec.whistleblower_reward_quotient)?, )?; } @@ -157,7 +157,7 @@ impl BeaconChain { attester_slashing_reward.safe_add_assign( state .get_validator(attester_index as usize)? - .effective_balance + .effective_balance() .safe_div(self.spec.whistleblower_reward_quotient)?, )?; } diff --git a/beacon_node/beacon_chain/src/beacon_block_streamer.rs b/beacon_node/beacon_chain/src/beacon_block_streamer.rs index a1f7c99067e..1bcde3b347f 100644 --- a/beacon_node/beacon_chain/src/beacon_block_streamer.rs +++ b/beacon_node/beacon_chain/src/beacon_block_streamer.rs @@ -429,7 +429,7 @@ impl BeaconBlockStreamer { continue; } - match self.beacon_chain.store.try_get_full_block(&root) { + match self.beacon_chain.store.try_get_full_block(&root, None) { Err(e) => db_blocks.push((root, Err(e.into()))), Ok(opt_block) => db_blocks.push(( root, diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 421bc12ee43..b4a352b33c5 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -54,11 +54,11 @@ use crate::observed_blob_sidecars::ObservedBlobSidecars; use crate::observed_block_producers::ObservedBlockProducers; use crate::observed_operations::{ObservationOutcome, ObservedOperations}; use crate::observed_slashable::ObservedSlashable; +use crate::parallel_state_cache::ParallelStateCache; use crate::persisted_beacon_chain::{PersistedBeaconChain, DUMMY_CANONICAL_HEAD_BLOCK_ROOT}; use crate::persisted_fork_choice::PersistedForkChoice; use crate::pre_finalization_cache::PreFinalizationBlockCache; use crate::shuffling_cache::{BlockShufflingIds, ShufflingCache}; -use crate::snapshot_cache::{BlockProductionPreState, SnapshotCache}; use crate::sync_committee_verification::{ Error as SyncCommitteeError, VerifiedSyncCommitteeMessage, VerifiedSyncContribution, }; @@ -130,9 +130,6 @@ pub type ForkChoiceError = fork_choice::Error; /// Alias to appease clippy. type HashBlockTuple = (Hash256, RpcBlock); -/// 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. pub const ATTESTATION_CACHE_LOCK_TIMEOUT: Duration = Duration::from_secs(1); @@ -176,6 +173,7 @@ pub const INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON: &str = "Finalized merge transition block is invalid."; /// Defines the behaviour when a block/block-root for a skipped slot is requested. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WhenSlotSkipped { /// If the slot is a skip slot, return `None`. /// @@ -447,8 +445,6 @@ pub struct BeaconChain { pub event_handler: Option>, /// Used to track the heads of the beacon chain. pub(crate) head_tracker: Arc, - /// A cache dedicated to block processing. - pub(crate) snapshot_cache: TimeoutRwLock>, /// Caches the attester shuffling for a given epoch and shuffling key root. pub shuffling_cache: TimeoutRwLock, /// A cache of eth1 deposit data at epoch boundaries for deposit finalization @@ -456,7 +452,7 @@ pub struct BeaconChain { /// Caches the beacon block proposer shuffling for a given epoch and shuffling key root. pub beacon_proposer_cache: Arc>, /// Caches a map of `validator_index -> validator_pubkey`. - pub(crate) validator_pubkey_cache: TimeoutRwLock>, + pub(crate) validator_pubkey_cache: Arc>>, /// A cache used when producing attestations. pub(crate) attester_cache: Arc, /// A cache used when producing attestations whilst the head block is still being imported. @@ -465,6 +461,10 @@ pub struct BeaconChain { pub block_times_cache: Arc>, /// A cache used to track pre-finalization block roots for quick rejection. pub pre_finalization_block_cache: PreFinalizationBlockCache, + /// A cache used to de-duplicate HTTP state requests. + /// + /// The cache is keyed by `state_root`. + pub parallel_state_cache: Arc>>, /// A cache used to produce light_client server messages pub light_client_server_cache: LightClientServerCache, /// Sender to signal the light_client server to produce new updates @@ -487,11 +487,6 @@ pub struct BeaconChain { pub data_availability_checker: Arc>, /// The KZG trusted setup used by this chain. pub kzg: Option>, - /// State with complete tree hash cache, ready for block production. - /// - /// NB: We can delete this once we have tree-states. - #[allow(clippy::type_complexity)] - pub block_production_state: Arc)>>>, } pub enum BeaconBlockResponseWrapper { @@ -768,9 +763,8 @@ impl BeaconChain { let iter = self.store.forwards_block_roots_iterator( start_slot, - local_head.beacon_state.clone_with(CloneConfig::none()), + local_head.beacon_state.clone(), local_head.beacon_block_root, - &self.spec, )?; Ok(iter.map(|result| result.map_err(Into::into))) @@ -795,17 +789,11 @@ impl BeaconChain { } self.with_head(move |head| { - let iter = self.store.forwards_block_roots_iterator_until( - start_slot, - end_slot, - || { - Ok(( - head.beacon_state.clone_with_only_committee_caches(), - head.beacon_block_root, - )) - }, - &self.spec, - )?; + let iter = + self.store + .forwards_block_roots_iterator_until(start_slot, end_slot, || { + Ok((head.beacon_state.clone(), head.beacon_block_root)) + })?; Ok(iter .map(|result| result.map_err(Into::into)) .take_while(move |result| { @@ -874,8 +862,7 @@ impl BeaconChain { let iter = self.store.forwards_state_roots_iterator( start_slot, local_head.beacon_state_root(), - local_head.beacon_state.clone_with(CloneConfig::none()), - &self.spec, + local_head.beacon_state.clone(), )?; Ok(iter.map(|result| result.map_err(Into::into))) @@ -892,17 +879,11 @@ impl BeaconChain { end_slot: Slot, ) -> Result> + '_, Error> { self.with_head(move |head| { - let iter = self.store.forwards_state_roots_iterator_until( - start_slot, - end_slot, - || { - Ok(( - head.beacon_state.clone_with_only_committee_caches(), - head.beacon_state_root(), - )) - }, - &self.spec, - )?; + let iter = + self.store + .forwards_state_roots_iterator_until(start_slot, end_slot, || { + Ok((head.beacon_state.clone(), head.beacon_state_root())) + })?; Ok(iter .map(|result| result.map_err(Into::into)) .take_while(move |result| { @@ -925,8 +906,14 @@ impl BeaconChain { ) -> Result>, Error> { let root = self.block_root_at_slot(request_slot, skips)?; + // Only hint the slot if expect a block at this exact slot. + let slot_hint = match skips { + WhenSlotSkipped::Prev => None, + WhenSlotSkipped::None => Some(request_slot), + }; + if let Some(block_root) = root { - Ok(self.store.get_blinded_block(&block_root)?) + Ok(self.store.get_blinded_block(&block_root, slot_hint)?) } else { Ok(None) } @@ -1185,7 +1172,7 @@ impl BeaconChain { ) -> Result>, Error> { // Load block from database, returning immediately if we have the full block w payload // stored. - let blinded_block = match self.store.try_get_full_block(block_root)? { + let blinded_block = match self.store.try_get_full_block(block_root, None)? { Some(DatabaseBlock::Full(block)) => return Ok(Some(block)), Some(DatabaseBlock::Blinded(block)) => block, None => return Ok(None), @@ -1253,7 +1240,7 @@ impl BeaconChain { &self, block_root: &Hash256, ) -> Result>, Error> { - Ok(self.store.get_blinded_block(block_root)?) + Ok(self.store.get_blinded_block(block_root, None)?) } /// Returns the state at the given root, if any. @@ -1476,10 +1463,7 @@ impl BeaconChain { /// /// May return an error if acquiring a read-lock on the `validator_pubkey_cache` times out. pub fn validator_index(&self, pubkey: &PublicKeyBytes) -> Result, Error> { - let pubkey_cache = self - .validator_pubkey_cache - .try_read_for(VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT) - .ok_or(Error::ValidatorPubkeyCacheLockTimeout)?; + let pubkey_cache = self.validator_pubkey_cache.read(); Ok(pubkey_cache.get_index(pubkey)) } @@ -1492,10 +1476,7 @@ impl BeaconChain { &self, validator_pubkeys: impl Iterator, ) -> Result, Error> { - let pubkey_cache = self - .validator_pubkey_cache - .try_read_for(VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT) - .ok_or(Error::ValidatorPubkeyCacheLockTimeout)?; + let pubkey_cache = self.validator_pubkey_cache.read(); validator_pubkeys .map(|pubkey| { @@ -1520,10 +1501,7 @@ impl BeaconChain { /// /// May return an error if acquiring a read-lock on the `validator_pubkey_cache` times out. pub fn validator_pubkey(&self, validator_index: usize) -> Result, Error> { - let pubkey_cache = self - .validator_pubkey_cache - .try_read_for(VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT) - .ok_or(Error::ValidatorPubkeyCacheLockTimeout)?; + let pubkey_cache = self.validator_pubkey_cache.read(); Ok(pubkey_cache.get(validator_index).cloned()) } @@ -1533,11 +1511,7 @@ impl BeaconChain { &self, validator_index: usize, ) -> Result, Error> { - let pubkey_cache = self - .validator_pubkey_cache - .try_read_for(VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT) - .ok_or(Error::ValidatorPubkeyCacheLockTimeout)?; - + let pubkey_cache = self.validator_pubkey_cache.read(); Ok(pubkey_cache.get_pubkey_bytes(validator_index).copied()) } @@ -1550,10 +1524,7 @@ impl BeaconChain { &self, validator_indices: &[usize], ) -> Result, Error> { - let pubkey_cache = self - .validator_pubkey_cache - .try_read_for(VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT) - .ok_or(Error::ValidatorPubkeyCacheLockTimeout)?; + let pubkey_cache = self.validator_pubkey_cache.read(); let mut map = HashMap::with_capacity(validator_indices.len()); for &validator_index in validator_indices { @@ -3320,8 +3291,7 @@ impl BeaconChain { // would be difficult to check that they all lock fork choice first. let mut ops = self .validator_pubkey_cache - .try_write_for(VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT) - .ok_or(Error::ValidatorPubkeyCacheLockTimeout)? + .write() .import_new_pubkeys(&state)?; // Apply the state to the attester cache, only if it is from the previous epoch or later. @@ -3397,6 +3367,13 @@ impl BeaconChain { "Early attester cache insert failed"; "error" => ?e ); + } else { + // Success, record the block as capable of being attested to. + self.block_times_cache.write().set_time_attestable( + block_root, + block.slot(), + timestamp_now(), + ); } } else { warn!( @@ -3542,29 +3519,6 @@ impl BeaconChain { }); } - self.snapshot_cache - .try_write_for(BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT) - .ok_or(Error::SnapshotCacheLockTimeout) - .map(|mut snapshot_cache| { - snapshot_cache.insert( - BeaconSnapshot { - beacon_state: state, - beacon_block: signed_block.clone(), - beacon_block_root: block_root, - }, - None, - &self.spec, - ) - }) - .unwrap_or_else(|e| { - error!( - self.log, - "Failed to insert snapshot"; - "error" => ?e, - "task" => "process block" - ); - }); - self.head_tracker .register_block(block_root, parent_root, slot); @@ -3974,7 +3928,7 @@ impl BeaconChain { self.shuffling_cache .try_write_for(ATTESTATION_CACHE_LOCK_TIMEOUT) .ok_or(Error::AttestationCacheLockTimeout)? - .insert_committee_cache(shuffling_id, committee_cache); + .insert_value(shuffling_id, committee_cache); } } Ok(()) @@ -4145,22 +4099,22 @@ impl BeaconChain { self.wait_for_fork_choice_before_block_production(slot)?; drop(fork_choice_timer); - // Producing a block requires the tree hash cache, so clone a full state corresponding to - // the head from the snapshot cache. Unfortunately we can't move the snapshot out of the - // cache (which would be fast), because we need to re-process the block after it has been - // signed. If we miss the cache or we're producing a block that conflicts with the head, - // fall back to getting the head from `slot - 1`. let state_load_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_STATE_LOAD_TIMES); // Atomically read some values from the head whilst avoiding holding cached head `Arc` any // longer than necessary. - let (head_slot, head_block_root) = { + let (head_slot, head_block_root, head_state_root) = { let head = self.canonical_head.cached_head(); - (head.head_slot(), head.head_block_root()) + ( + head.head_slot(), + head.head_block_root(), + head.head_state_root(), + ) }; let (state, state_root_opt) = if head_slot < slot { // Attempt an aggressive re-org if configured and the conditions are right. - if let Some(re_org_state) = self.get_state_for_re_org(slot, head_slot, head_block_root) + if let Some((re_org_state, re_org_state_root)) = + self.get_state_for_re_org(slot, head_slot, head_block_root) { info!( self.log, @@ -4168,37 +4122,16 @@ impl BeaconChain { "slot" => slot, "head_to_reorg" => %head_block_root, ); - (re_org_state.pre_state, re_org_state.state_root) - } - // Normal case: proposing a block atop the current head using the cache. - else if let Some((_, cached_state)) = - self.get_state_from_block_production_cache(head_block_root) - { - (cached_state.pre_state, cached_state.state_root) - } - // Fall back to a direct read of the snapshot cache. - else if let Some(pre_state) = - self.get_state_from_snapshot_cache_for_block_production(head_block_root) - { - warn!( - self.log, - "Block production cache miss"; - "message" => "falling back to snapshot cache clone", - "slot" => slot - ); - (pre_state.pre_state, pre_state.state_root) + (re_org_state, Some(re_org_state_root)) } else { - warn!( - self.log, - "Block production cache miss"; - "message" => "this block is more likely to be orphaned", - "slot" => slot, - ); - let state = self - .state_at_slot(slot - 1, StateSkipConfig::WithStateRoots) - .map_err(|_| BlockProductionError::UnableToProduceAtSlot(slot))?; - - (state, None) + // Fetch the head state advanced through to `slot`, which should be present in the + // state cache thanks to the state advance timer. + let (state_root, state) = self + .store + .get_advanced_hot_state(head_block_root, slot, head_state_root) + .map_err(BlockProductionError::FailedToLoadState)? + .ok_or(BlockProductionError::UnableToProduceAtSlot(slot))?; + (state, Some(state_root)) } } else { warn!( @@ -4219,40 +4152,6 @@ impl BeaconChain { Ok((state, state_root_opt)) } - /// Get the state cached for block production *if* it matches `head_block_root`. - /// - /// This will clear the cache regardless of whether the block root matches, so only call this if - /// you think the `head_block_root` is likely to match! - fn get_state_from_block_production_cache( - &self, - head_block_root: Hash256, - ) -> Option<(Hash256, BlockProductionPreState)> { - // Take care to drop the lock as quickly as possible. - let mut lock = self.block_production_state.lock(); - let result = lock - .take() - .filter(|(cached_block_root, _)| *cached_block_root == head_block_root); - drop(lock); - result - } - - /// Get a state for block production from the snapshot cache. - fn get_state_from_snapshot_cache_for_block_production( - &self, - head_block_root: Hash256, - ) -> Option> { - if let Some(lock) = self - .snapshot_cache - .try_read_for(BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT) - { - let result = lock.get_state_for_block_production(head_block_root); - drop(lock); - result - } else { - None - } - } - /// Fetch the beacon state to use for producing a block if a 1-slot proposer re-org is viable. /// /// This function will return `None` if proposer re-orgs are disabled. @@ -4261,7 +4160,7 @@ impl BeaconChain { slot: Slot, head_slot: Slot, canonical_head: Hash256, - ) -> Option> { + ) -> Option<(BeaconState, Hash256)> { let re_org_head_threshold = self.config.re_org_head_threshold?; let re_org_parent_threshold = self.config.re_org_parent_threshold?; @@ -4344,30 +4243,20 @@ impl BeaconChain { .ok()?; drop(proposer_head_timer); let re_org_parent_block = proposer_head.parent_node.root; + let re_org_parent_state_root = proposer_head.parent_node.state_root; - // Only attempt a re-org if we hit the block production cache or snapshot cache. - let pre_state = self - .get_state_from_block_production_cache(re_org_parent_block) - .map(|(_, state)| state) - .or_else(|| { + // FIXME(sproul): consider not re-orging if we miss the cache + let (state_root, state) = self + .store + .get_advanced_hot_state(re_org_parent_block, slot, re_org_parent_state_root) + .map_err(|e| { warn!( self.log, - "Block production cache miss"; - "message" => "falling back to snapshot cache during re-org", - "slot" => slot, - "block_root" => ?re_org_parent_block + "Error loading block production state"; + "error" => ?e, ); - self.get_state_from_snapshot_cache_for_block_production(re_org_parent_block) }) - .or_else(|| { - debug!( - self.log, - "Not attempting re-org"; - "reason" => "missed snapshot cache", - "parent_block" => ?re_org_parent_block, - ); - None - })?; + .ok()??; info!( self.log, @@ -4378,7 +4267,7 @@ impl BeaconChain { "threshold_weight" => proposer_head.re_org_head_weight_threshold ); - Some(pre_state) + Some((state, state_root)) } /// Get the proposer index and `prev_randao` value for a proposal at slot `proposal_slot`. @@ -4503,23 +4392,10 @@ impl BeaconChain { let parent_block_root = forkchoice_update_params.head_root; + // FIXME(sproul): optimise this for tree-states let (unadvanced_state, unadvanced_state_root) = if cached_head.head_block_root() == parent_block_root { (Cow::Borrowed(head_state), cached_head.head_state_root()) - } else if let Some(snapshot) = self - .snapshot_cache - .try_read_for(BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT) - .ok_or(Error::SnapshotCacheLockTimeout)? - .get_cloned(parent_block_root, CloneConfig::none()) - { - debug!( - self.log, - "Hit snapshot cache during withdrawals calculation"; - "slot" => proposal_slot, - "parent_block_root" => ?parent_block_root, - ); - let state_root = snapshot.beacon_state_root(); - (Cow::Owned(snapshot.beacon_state), state_root) } else { info!( self.log, @@ -4910,6 +4786,7 @@ impl BeaconChain { drop(slot_timer); state.build_committee_cache(RelativeEpoch::Current, &self.spec)?; + state.apply_pending_mutations()?; let parent_root = if state.slot() > 0 { *state @@ -4924,7 +4801,7 @@ impl BeaconChain { let pubkey = state .validators() .get(proposer_index as usize) - .map(|v| v.pubkey) + .map(|v| *v.pubkey()) .ok_or(BlockProductionError::BeaconChain( BeaconChainError::ValidatorIndexUnknown(proposer_index as usize), ))?; @@ -6286,7 +6163,7 @@ impl BeaconChain { // access. drop(shuffling_cache); - let committee_cache = cache_item.wait()?; + let committee_cache = cache_item.wait().map_err(Error::ShufflingCacheError)?; map_fn(&committee_cache, shuffling_id.shuffling_decision_block) } else { // Create an entry in the cache that "promises" this value will eventually be computed. @@ -6295,7 +6172,9 @@ impl BeaconChain { // // Creating the promise whilst we hold the `shuffling_cache` lock will prevent the same // promise from being created twice. - let sender = shuffling_cache.create_promise(shuffling_id.clone())?; + let sender = shuffling_cache + .create_promise(shuffling_id.clone()) + .map_err(Error::ShufflingCacheError)?; // Drop the shuffling cache to avoid holding the lock for any longer than // required. @@ -6308,6 +6187,17 @@ impl BeaconChain { "head_block_root" => head_block_root.to_string(), ); + // If the block's state will be so far ahead of `shuffling_epoch` that even its + // previous epoch committee cache will be too new, then error. Callers of this function + // shouldn't be requesting such old shufflings for this `head_block_root`. + let head_block_epoch = head_block.slot.epoch(T::EthSpec::slots_per_epoch()); + if head_block_epoch > shuffling_epoch + 1 { + return Err(Error::InvalidStateForShuffling { + state_epoch: head_block_epoch, + shuffling_epoch, + }); + } + let state_read_timer = metrics::start_timer(&metrics::ATTESTATION_PROCESSING_STATE_READ_TIMES); @@ -6318,71 +6208,52 @@ impl BeaconChain { // to copy the head is liable to race-conditions. let head_state_opt = self.with_head(|head| { if head.beacon_block_root == head_block_root { - Ok(Some(( - head.beacon_state - .clone_with(CloneConfig::committee_caches_only()), - head.beacon_state_root(), - ))) + Ok(Some((head.beacon_state.clone(), head.beacon_state_root()))) } else { Ok::<_, Error>(None) } })?; + // Compute the `target_slot` to advance the block's state to. + // + // Since there's a one-epoch look-ahead on the attester shuffling, it suffices to + // only advance into the first slot of the epoch prior to `shuffling_epoch`. + // + // If the `head_block` is already ahead of that slot, then we should load the state + // at that slot, as we've determined above that the `shuffling_epoch` cache will + // not be too far in the past. + let target_slot = std::cmp::max( + shuffling_epoch + .saturating_sub(1_u64) + .start_slot(T::EthSpec::slots_per_epoch()), + head_block.slot, + ); + // If the head state is useful for this request, use it. Otherwise, read a state from - // disk. + // disk that is advanced as close as possible to `target_slot`. let (mut state, state_root) = if let Some((state, state_root)) = head_state_opt { (state, state_root) } else { - let block_state_root = head_block.state_root; - let max_slot = shuffling_epoch.start_slot(T::EthSpec::slots_per_epoch()); let (state_root, state) = self .store - .get_inconsistent_state_for_attestation_verification_only( - &head_block_root, - max_slot, - block_state_root, - )? - .ok_or(Error::MissingBeaconState(block_state_root))?; + .get_advanced_hot_state(head_block_root, target_slot, head_block.state_root)? + .ok_or(Error::MissingBeaconState(head_block.state_root))?; (state, state_root) }; - /* - * IMPORTANT - * - * Since it's possible that - * `Store::get_inconsistent_state_for_attestation_verification_only` was used to obtain - * the state, we cannot rely upon the following fields: - * - * - `state.state_roots` - * - `state.block_roots` - * - * These fields should not be used for the rest of this function. - */ - metrics::stop_timer(state_read_timer); let state_skip_timer = metrics::start_timer(&metrics::ATTESTATION_PROCESSING_STATE_SKIP_TIMES); - // If the state is in an earlier epoch, advance it. If it's from a later epoch, reject - // it. + // If the state is still in an earlier epoch, advance it to the `target_slot` so + // that its next epoch committee cache matches the `shuffling_epoch`. if state.current_epoch() + 1 < shuffling_epoch { - // Since there's a one-epoch look-ahead on the attester shuffling, it suffices to - // only advance into the slot prior to the `shuffling_epoch`. - let target_slot = shuffling_epoch - .saturating_sub(1_u64) - .start_slot(T::EthSpec::slots_per_epoch()); - - // Advance the state into the required slot, using the "partial" method since the state - // roots are not relevant for the shuffling. + // Advance the state into the required slot, using the "partial" method since the + // state roots are not relevant for the shuffling. partial_state_advance(&mut state, Some(state_root), target_slot, &self.spec)?; - } else if state.current_epoch() > shuffling_epoch { - return Err(Error::InvalidStateForShuffling { - state_epoch: state.current_epoch(), - shuffling_epoch, - }); } - metrics::stop_timer(state_skip_timer); + let committee_building_timer = metrics::start_timer(&metrics::ATTESTATION_PROCESSING_COMMITTEE_BUILDING_TIMES); @@ -6391,14 +6262,13 @@ impl BeaconChain { state.build_committee_cache(relative_epoch, &self.spec)?; - let committee_cache = state.take_committee_cache(relative_epoch)?; - let committee_cache = Arc::new(committee_cache); + let committee_cache = state.committee_cache(relative_epoch)?.clone(); let shuffling_decision_block = shuffling_id.shuffling_decision_block; self.shuffling_cache .try_write_for(ATTESTATION_CACHE_LOCK_TIMEOUT) .ok_or(Error::AttestationCacheLockTimeout)? - .insert_committee_cache(shuffling_id, &committee_cache); + .insert_value(shuffling_id, &committee_cache); metrics::stop_timer(committee_building_timer); @@ -6412,56 +6282,60 @@ impl BeaconChain { /// /// This could be a very expensive operation and should only be done in testing/analysis /// activities. + /// + /// This dump function previously used a backwards iterator but has been swapped to a forwards + /// iterator as it allows for MUCH better caching and rebasing. Memory usage of some tests went + /// from 5GB per test to 90MB. #[allow(clippy::type_complexity)] pub fn chain_dump( &self, ) -> Result>>, Error> { let mut dump = vec![]; - let mut last_slot = { - let head = self.canonical_head.cached_head(); - BeaconSnapshot { - beacon_block: Arc::new(head.snapshot.beacon_block.clone_as_blinded()), - beacon_block_root: head.snapshot.beacon_block_root, - beacon_state: head.snapshot.beacon_state.clone(), - } - }; - - dump.push(last_slot.clone()); + let mut prev_block_root = None; + let mut prev_beacon_state = None; - loop { - let beacon_block_root = last_slot.beacon_block.parent_root(); + for res in self.forwards_iter_block_roots(Slot::new(0))? { + let (beacon_block_root, _) = res?; - if beacon_block_root == Hash256::zero() { - break; // Genesis has been reached. + // Do not include snapshots at skipped slots. + if Some(beacon_block_root) == prev_block_root { + continue; } + prev_block_root = Some(beacon_block_root); let beacon_block = self .store - .get_blinded_block(&beacon_block_root)? + .get_blinded_block(&beacon_block_root, None)? .ok_or_else(|| { Error::DBInconsistent(format!("Missing block {}", beacon_block_root)) })?; let beacon_state_root = beacon_block.state_root(); - let beacon_state = self + + let mut beacon_state = self .store .get_state(&beacon_state_root, Some(beacon_block.slot()))? .ok_or_else(|| { Error::DBInconsistent(format!("Missing state {:?}", beacon_state_root)) })?; - let slot = BeaconSnapshot { + // This beacon state might come from the freezer DB, which means it could have pending + // updates or lots of untethered memory. We rebase it on the previous state in order to + // address this. + beacon_state.apply_pending_mutations()?; + if let Some(prev) = prev_beacon_state { + beacon_state.rebase_on(&prev, &self.spec)?; + } + beacon_state.build_caches(&self.spec)?; + prev_beacon_state = Some(beacon_state.clone()); + + let snapshot = BeaconSnapshot { beacon_block: Arc::new(beacon_block), beacon_block_root, beacon_state, }; - - dump.push(slot.clone()); - last_slot = slot; + dump.push(snapshot); } - - dump.reverse(); - Ok(dump) } @@ -6696,6 +6570,10 @@ impl BeaconChain { self.data_availability_checker.data_availability_boundary() } + pub fn logger(&self) -> &Logger { + &self.log + } + /// Gets the `LightClientBootstrap` object for a requested block root. /// /// Returns `None` when the state or block is not found in the database. diff --git a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs index 2a42b49b422..900c6b1d8c3 100644 --- a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs +++ b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs @@ -315,7 +315,7 @@ where metrics::inc_counter(&metrics::BALANCES_CACHE_MISSES); let justified_block = self .store - .get_blinded_block(&self.justified_checkpoint.root) + .get_blinded_block(&self.justified_checkpoint.root, None) .map_err(Error::FailedToReadBlock)? .ok_or(Error::MissingBlock(self.justified_checkpoint.root))? .deconstruct() diff --git a/beacon_node/beacon_chain/src/beacon_proposer_cache.rs b/beacon_node/beacon_chain/src/beacon_proposer_cache.rs index ca390712b13..d10bbfbbc5f 100644 --- a/beacon_node/beacon_chain/src/beacon_proposer_cache.rs +++ b/beacon_node/beacon_chain/src/beacon_proposer_cache.rs @@ -17,8 +17,7 @@ use std::cmp::Ordering; use std::num::NonZeroUsize; use types::non_zero_usize::new_non_zero_usize; use types::{ - BeaconState, BeaconStateError, ChainSpec, CloneConfig, Epoch, EthSpec, Fork, Hash256, Slot, - Unsigned, + BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, Fork, Hash256, Slot, Unsigned, }; /// The number of sets of proposer indices that should be cached. @@ -145,10 +144,7 @@ pub fn compute_proposer_duties_from_head( let (mut state, head_state_root, head_block_root) = { let head = chain.canonical_head.cached_head(); // Take a copy of the head state. - let head_state = head - .snapshot - .beacon_state - .clone_with(CloneConfig::committee_caches_only()); + let head_state = head.snapshot.beacon_state.clone(); let head_state_root = head.head_state_root(); let head_block_root = head.head_block_root(); (head_state, head_state_root, head_block_root) diff --git a/beacon_node/beacon_chain/src/beacon_snapshot.rs b/beacon_node/beacon_chain/src/beacon_snapshot.rs index afb13247766..e9fde48ac67 100644 --- a/beacon_node/beacon_chain/src/beacon_snapshot.rs +++ b/beacon_node/beacon_chain/src/beacon_snapshot.rs @@ -1,8 +1,8 @@ use serde::Serialize; use std::sync::Arc; use types::{ - beacon_state::CloneConfig, AbstractExecPayload, BeaconState, EthSpec, FullPayload, Hash256, - SignedBeaconBlock, + AbstractExecPayload, BeaconState, EthSpec, FullPayload, Hash256, SignedBeaconBlock, + SignedBlindedBeaconBlock, }; /// Represents some block and its associated state. Generally, this will be used for tracking the @@ -14,6 +14,19 @@ pub struct BeaconSnapshot = FullPayl pub beacon_state: BeaconState, } +/// This snapshot is to be used for verifying a child of `self.beacon_block`. +#[derive(Debug)] +pub struct PreProcessingSnapshot { + /// This state is equivalent to the `self.beacon_block.state_root()` state that has been + /// advanced forward one slot using `per_slot_processing`. This state is "primed and ready" for + /// the application of another block. + pub pre_state: BeaconState, + /// This value is only set to `Some` if the `pre_state` was *not* advanced forward. + pub beacon_state_root: Option, + pub beacon_block: SignedBlindedBeaconBlock, + pub beacon_block_root: Hash256, +} + impl> BeaconSnapshot { /// Create a new checkpoint. pub fn new( @@ -48,12 +61,4 @@ impl> BeaconSnapshot { self.beacon_block_root = beacon_block_root; self.beacon_state = beacon_state; } - - pub fn clone_with(&self, clone_config: CloneConfig) -> Self { - Self { - beacon_block: self.beacon_block.clone(), - beacon_block_root: self.beacon_block_root, - beacon_state: self.beacon_state.clone_with(clone_config), - } - } } diff --git a/beacon_node/beacon_chain/src/blob_verification.rs b/beacon_node/beacon_chain/src/blob_verification.rs index a69f2b74524..496a11f93e0 100644 --- a/beacon_node/beacon_chain/src/blob_verification.rs +++ b/beacon_node/beacon_chain/src/blob_verification.rs @@ -2,23 +2,20 @@ use derivative::Derivative; use slot_clock::SlotClock; use std::sync::Arc; -use crate::beacon_chain::{BeaconChain, BeaconChainTypes, BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT}; +use crate::beacon_chain::{BeaconChain, BeaconChainTypes}; use crate::block_verification::{ - cheap_state_advance_to_obtain_committees, get_validator_pubkey_cache, process_block_slash_info, - BlockSlashInfo, + cheap_state_advance_to_obtain_committees, process_block_slash_info, BlockSlashInfo, }; use crate::kzg_utils::{validate_blob, validate_blobs}; use crate::{metrics, BeaconChainError}; use kzg::{Error as KzgError, Kzg, KzgCommitment}; use merkle_proof::MerkleTreeError; -use slog::{debug, warn}; +use slog::debug; use ssz_derive::{Decode, Encode}; use ssz_types::VariableList; use tree_hash::TreeHash; use types::blob_sidecar::BlobIdentifier; -use types::{ - BeaconStateError, BlobSidecar, CloneConfig, EthSpec, Hash256, SignedBeaconBlockHeader, Slot, -}; +use types::{BeaconStateError, BlobSidecar, EthSpec, Hash256, SignedBeaconBlockHeader, Slot}; /// An error occurred while validating a gossip blob. #[derive(Debug)] @@ -485,98 +482,42 @@ pub fn validate_blob_sidecar_for_gossip( "block_root" => %block_root, "index" => %blob_index, ); - if let Some(mut snapshot) = chain - .snapshot_cache - .try_read_for(BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT) - .and_then(|snapshot_cache| { - snapshot_cache.get_cloned(block_parent_root, CloneConfig::committee_caches_only()) - }) - { - if snapshot.beacon_state.slot() == blob_slot { - debug!( - chain.log, - "Cloning snapshot cache state for blob verification"; - "block_root" => %block_root, - "index" => %blob_index, - ); - ( - snapshot - .beacon_state - .get_beacon_proposer_index(blob_slot, &chain.spec)?, - snapshot.beacon_state.fork(), - ) - } else { - debug!( - chain.log, - "Cloning and advancing snapshot cache state for blob verification"; - "block_root" => %block_root, - "index" => %blob_index, - ); - let state = - cheap_state_advance_to_obtain_committees::<_, GossipBlobError>( - &mut snapshot.beacon_state, - Some(snapshot.beacon_block_root), - blob_slot, - &chain.spec, - )?; - ( - state.get_beacon_proposer_index(blob_slot, &chain.spec)?, - state.fork(), - ) - } - } - // Need to advance the state to get the proposer index - else { - warn!( - chain.log, - "Snapshot cache miss for blob verification"; - "block_root" => %block_root, - "index" => %blob_index, - ); - - let parent_block = chain - .get_blinded_block(&block_parent_root) - .map_err(GossipBlobError::BeaconChainError)? - .ok_or_else(|| { - GossipBlobError::from(BeaconChainError::MissingBeaconBlock(block_parent_root)) - })?; - - let mut parent_state = chain - .get_state(&parent_block.state_root(), Some(parent_block.slot()))? - .ok_or_else(|| { - BeaconChainError::DBInconsistent(format!( - "Missing state {:?}", - parent_block.state_root() - )) - })?; - let state = cheap_state_advance_to_obtain_committees::<_, GossipBlobError>( - &mut parent_state, - Some(parent_block.state_root()), - blob_slot, - &chain.spec, - )?; - - let proposers = state.get_beacon_proposer_indices(&chain.spec)?; - let proposer_index = *proposers - .get(blob_slot.as_usize() % T::EthSpec::slots_per_epoch() as usize) - .ok_or_else(|| BeaconChainError::NoProposerForSlot(blob_slot))?; - - let fork = state.fork(); - // Prime the proposer shuffling cache with the newly-learned value. - chain.beacon_proposer_cache.lock().insert( - blob_epoch, - proposer_shuffling_root, - proposers, - fork, - )?; - (proposer_index, fork) - } + let (parent_state_root, mut parent_state) = chain + .store + .get_advanced_hot_state(block_parent_root, blob_slot, parent_block.state_root) + .map_err(|e| GossipBlobError::BeaconChainError(e.into()))? + .ok_or_else(|| { + BeaconChainError::DBInconsistent(format!( + "Missing state for parent block {block_parent_root:?}", + )) + })?; + + let state = cheap_state_advance_to_obtain_committees::<_, GossipBlobError>( + &mut parent_state, + Some(parent_state_root), + blob_slot, + &chain.spec, + )?; + + let proposers = state.get_beacon_proposer_indices(&chain.spec)?; + let proposer_index = *proposers + .get(blob_slot.as_usize() % T::EthSpec::slots_per_epoch() as usize) + .ok_or_else(|| BeaconChainError::NoProposerForSlot(blob_slot))?; + + // Prime the proposer shuffling cache with the newly-learned value. + chain.beacon_proposer_cache.lock().insert( + blob_epoch, + proposer_shuffling_root, + proposers, + state.fork(), + )?; + (proposer_index, state.fork()) }; // Signature verify the signed block header. let signature_is_valid = { - let pubkey_cache = - get_validator_pubkey_cache(chain).map_err(|_| GossipBlobError::PubkeyCacheTimeout)?; + let pubkey_cache = chain.validator_pubkey_cache.read(); + let pubkey = pubkey_cache .get(proposer_index) .ok_or_else(|| GossipBlobError::UnknownValidator(proposer_index as u64))?; diff --git a/beacon_node/beacon_chain/src/block_times_cache.rs b/beacon_node/beacon_chain/src/block_times_cache.rs index c5293bcb0ee..35431c2e635 100644 --- a/beacon_node/beacon_chain/src/block_times_cache.rs +++ b/beacon_node/beacon_chain/src/block_times_cache.rs @@ -18,6 +18,7 @@ type BlockRoot = Hash256; #[derive(Clone, Default)] pub struct Timestamps { pub observed: Option, + pub attestable: Option, pub imported: Option, pub set_as_head: Option, } @@ -26,6 +27,7 @@ pub struct Timestamps { #[derive(Debug, Default)] pub struct BlockDelays { pub observed: Option, + pub attestable: Option, pub imported: Option, pub set_as_head: Option, } @@ -35,6 +37,9 @@ impl BlockDelays { let observed = times .observed .and_then(|observed_time| observed_time.checked_sub(slot_start_time)); + let attestable = times + .attestable + .and_then(|attestable_time| attestable_time.checked_sub(slot_start_time)); let imported = times .imported .and_then(|imported_time| imported_time.checked_sub(times.observed?)); @@ -43,6 +48,7 @@ impl BlockDelays { .and_then(|set_as_head_time| set_as_head_time.checked_sub(times.imported?)); BlockDelays { observed, + attestable, imported, set_as_head, } @@ -109,6 +115,14 @@ impl BlockTimesCache { } } + pub fn set_time_attestable(&mut self, block_root: BlockRoot, slot: Slot, timestamp: Duration) { + let block_times = self + .cache + .entry(block_root) + .or_insert_with(|| BlockTimesCacheValue::new(slot)); + block_times.timestamps.attestable = Some(timestamp); + } + pub fn set_time_imported(&mut self, block_root: BlockRoot, slot: Slot, timestamp: Duration) { let block_times = self .cache diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 3cd8a7f259b..461e54df719 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -48,6 +48,7 @@ // returned alongside. #![allow(clippy::result_large_err)] +use crate::beacon_snapshot::PreProcessingSnapshot; use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob}; use crate::block_verification_types::{ AsBlock, BlockContentsError, BlockImportData, GossipVerifiedBlockContents, RpcBlock, @@ -59,14 +60,10 @@ use crate::execution_payload::{ AllowOptimisticImport, NotifyExecutionLayer, PayloadNotifier, }; use crate::observed_block_producers::SeenBlock; -use crate::snapshot_cache::PreProcessingSnapshot; use crate::validator_monitor::HISTORIC_EPOCHS as VALIDATOR_MONITOR_HISTORIC_EPOCHS; use crate::validator_pubkey_cache::ValidatorPubkeyCache; use crate::{ - beacon_chain::{ - BeaconForkChoice, ForkChoiceError, BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT, - VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT, - }, + beacon_chain::{BeaconForkChoice, ForkChoiceError}, metrics, BeaconChain, BeaconChainError, BeaconChainTypes, }; use derivative::Derivative; @@ -86,7 +83,7 @@ use state_processing::{ block_signature_verifier::{BlockSignatureVerifier, Error as BlockSignatureVerifierError}, per_block_processing, per_slot_processing, state_advance::partial_state_advance, - BlockProcessingError, BlockSignatureStrategy, ConsensusContext, SlotProcessingError, + AllCaches, BlockProcessingError, BlockSignatureStrategy, ConsensusContext, SlotProcessingError, StateProcessingStrategy, VerifyBlockRoot, }; use std::borrow::Cow; @@ -94,14 +91,13 @@ use std::fmt::Debug; use std::fs; use std::io::Write; use std::sync::Arc; -use std::time::Duration; -use store::{Error as DBError, HotStateSummary, KeyValueStore, StoreOp}; +use store::{Error as DBError, KeyValueStore, StoreOp}; use task_executor::JoinHandle; use tree_hash::TreeHash; use types::{ - BeaconBlockRef, BeaconState, BeaconStateError, ChainSpec, CloneConfig, Epoch, EthSpec, - ExecutionBlockHash, Hash256, InconsistentFork, PublicKey, PublicKeyBytes, RelativeEpoch, - SignedBeaconBlock, SignedBeaconBlockHeader, Slot, + BeaconBlockRef, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, ExecutionBlockHash, + Hash256, InconsistentFork, PublicKey, PublicKeyBytes, RelativeEpoch, SignedBeaconBlock, + SignedBeaconBlockHeader, Slot, }; use types::{BlobSidecar, ExecPayload}; @@ -617,7 +613,7 @@ pub fn signature_verify_chain_segment( // verify signatures let pubkey_cache = get_validator_pubkey_cache(chain)?; - let mut signature_verifier = get_signature_verifier(&state, &pubkey_cache, &chain.spec); + let mut signature_verifier = get_signature_verifier::(&state, &pubkey_cache, &chain.spec); for svb in &mut signature_verified_blocks { signature_verifier .include_all_signatures(svb.block.as_block(), &mut svb.consensus_context)?; @@ -1054,7 +1050,8 @@ impl SignatureVerifiedBlock { let pubkey_cache = get_validator_pubkey_cache(chain)?; - let mut signature_verifier = get_signature_verifier(&state, &pubkey_cache, &chain.spec); + let mut signature_verifier = + get_signature_verifier::(&state, &pubkey_cache, &chain.spec); let mut consensus_context = ConsensusContext::new(block.slot()).set_current_block_root(block_root); @@ -1105,7 +1102,8 @@ impl SignatureVerifiedBlock { let pubkey_cache = get_validator_pubkey_cache(chain)?; - let mut signature_verifier = get_signature_verifier(&state, &pubkey_cache, &chain.spec); + let mut signature_verifier = + get_signature_verifier::(&state, &pubkey_cache, &chain.spec); // Gossip verification has already checked the proposer index. Use it to check the RANDAO // signature. @@ -1426,52 +1424,31 @@ impl ExecutionPendingBlock { let distance = block.slot().as_u64().saturating_sub(state.slot().as_u64()); for _ in 0..distance { - let state_root = if parent.beacon_block.slot() == state.slot() { - // If it happens that `pre_state` has *not* already been advanced forward a single - // slot, then there is no need to compute the state root for this - // `per_slot_processing` call since that state root is already stored in the parent - // block. - parent.beacon_block.state_root() - } else { - // This is a new state we've reached, so stage it for storage in the DB. - // Computing the state root here is time-equivalent to computing it during slot - // processing, but we get early access to it. - let state_root = state.update_tree_hash_cache()?; - - // Store the state immediately, marking it as temporary, and staging the deletion - // of its temporary status as part of the larger atomic operation. - let txn_lock = chain.store.hot_db.begin_rw_transaction(); - let state_already_exists = - chain.store.load_hot_state_summary(&state_root)?.is_some(); - - let state_batch = if state_already_exists { - // If the state exists, it could be temporary or permanent, but in neither case - // should we rewrite it or store a new temporary flag for it. We *will* stage - // the temporary flag for deletion because it's OK to double-delete the flag, - // and we don't mind if another thread gets there first. - vec![] + let state_root = + if parent.beacon_block.slot() == state.slot() { + // If it happens that `pre_state` has *not* already been advanced forward a single + // slot, then there is no need to compute the state root for this + // `per_slot_processing` call since that state root is already stored in the parent + // block. + parent.beacon_block.state_root() } else { - vec![ - if state.slot() % T::EthSpec::slots_per_epoch() == 0 { - StoreOp::PutState(state_root, &state) - } else { - StoreOp::PutStateSummary( - state_root, - HotStateSummary::new(&state_root, &state)?, - ) - }, - StoreOp::PutStateTemporaryFlag(state_root), - ] + // This is a new state we've reached, so stage it for storage in the DB. + // Computing the state root here is time-equivalent to computing it during slot + // processing, but we get early access to it. + let state_root = state.update_tree_hash_cache()?; + + // Store the state immediately, marking it as temporary, and staging the deletion + // of its temporary status as part of the larger atomic operation. + let txn_lock = chain.store.hot_db.begin_rw_transaction(); + chain.store.do_atomically_with_block_and_blobs_cache(vec![ + StoreOp::PutState(state_root, &state), + ])?; + drop(txn_lock); + + confirmed_state_roots.push(state_root); + + state_root }; - chain - .store - .do_atomically_with_block_and_blobs_cache(state_batch)?; - drop(txn_lock); - - confirmed_state_roots.push(state_root); - - state_root - }; if let Some(summary) = per_slot_processing(&mut state, Some(state_root), &chain.spec)? { // Expose Prometheus metrics. @@ -1523,8 +1500,7 @@ impl ExecutionPendingBlock { let committee_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_COMMITTEE); - state.build_committee_cache(RelativeEpoch::Previous, &chain.spec)?; - state.build_committee_cache(RelativeEpoch::Current, &chain.spec)?; + state.build_all_committee_caches(&chain.spec)?; metrics::stop_timer(committee_timer); @@ -1840,12 +1816,10 @@ fn verify_parent_block_is_known( /// whilst attempting the operation. #[allow(clippy::type_complexity)] fn load_parent>( - block_root: Hash256, + _block_root: Hash256, block: B, chain: &BeaconChain, ) -> Result<(PreProcessingSnapshot, B), BlockError> { - let spec = &chain.spec; - // Reject any block if its parent is not known to fork choice. // // A block that is not in fork choice is either: @@ -1864,44 +1838,9 @@ fn load_parent>( return Err(BlockError::ParentUnknown(block.into_rpc_block())); } - let block_delay = chain - .block_times_cache - .read() - .get_block_delays( - block_root, - chain - .slot_clock - .start_of(block.slot()) - .unwrap_or_else(|| Duration::from_secs(0)), - ) - .observed; - let db_read_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_DB_READ); - let result = if let Some((snapshot, cloned)) = chain - .snapshot_cache - .try_write_for(BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT) - .and_then(|mut snapshot_cache| { - snapshot_cache.get_state_for_block_processing( - block.parent_root(), - block.slot(), - block_delay, - spec, - ) - }) { - if cloned { - metrics::inc_counter(&metrics::BLOCK_PROCESSING_SNAPSHOT_CACHE_CLONES); - debug!( - chain.log, - "Cloned snapshot for late block/skipped slot"; - "slot" => %block.slot(), - "parent_slot" => %snapshot.beacon_block.slot(), - "parent_root" => ?block.parent_root(), - "block_delay" => ?block_delay, - ); - } - Ok((snapshot, block)) - } else { + let result = { // Load the blocks parent block from the database, returning invalid if that block is not // found. // @@ -1926,7 +1865,7 @@ fn load_parent>( // Retrieve any state that is advanced through to at most `block.slot()`: this is // particularly important if `block` descends from the finalized/split block, but at a slot // prior to the finalized slot (which is invalid and inaccessible in our DB schema). - let (parent_state_root, parent_state) = chain + let (parent_state_root, state) = chain .store .get_advanced_hot_state(root, block.slot(), parent_block.state_root())? .ok_or_else(|| { @@ -1935,22 +1874,46 @@ fn load_parent>( ) })?; - metrics::inc_counter(&metrics::BLOCK_PROCESSING_SNAPSHOT_CACHE_MISSES); - debug!( - chain.log, - "Missed snapshot cache"; - "slot" => block.slot(), - "parent_slot" => parent_block.slot(), - "parent_root" => ?block.parent_root(), - "block_delay" => ?block_delay, - ); + if !state.all_caches_built() { + slog::warn!( + chain.log, + "Parent state lacks built caches"; + "block_slot" => block.slot(), + "state_slot" => state.slot(), + ); + } + + if block.slot() != state.slot() { + slog::warn!( + chain.log, + "Parent state is not advanced"; + "block_slot" => block.slot(), + "state_slot" => state.slot(), + ); + } + + let beacon_state_root = if state.slot() == parent_block.slot() { + // Sanity check. + if parent_state_root != parent_block.state_root() { + return Err(BeaconChainError::DBInconsistent(format!( + "Parent state at slot {} has the wrong state root: {:?} != {:?}", + state.slot(), + parent_state_root, + parent_block.state_root() + )) + .into()); + } + Some(parent_block.state_root()) + } else { + None + }; Ok(( PreProcessingSnapshot { beacon_block: parent_block, beacon_block_root: root, - pre_state: parent_state, - beacon_state_root: Some(parent_state_root), + pre_state: state, + beacon_state_root, }, block, )) @@ -2031,7 +1994,7 @@ pub fn cheap_state_advance_to_obtain_committees<'a, E: EthSpec, Err: BlockBlobEr } else if state.slot() > block_slot { Err(Err::not_later_than_parent_error(block_slot, state.slot())) } else { - let mut state = state.clone_with(CloneConfig::committee_caches_only()); + let mut state = state.clone(); let target_slot = block_epoch.start_slot(E::slots_per_epoch()); // Advance the state into the same epoch as the block. Use the "partial" method since state @@ -2050,10 +2013,7 @@ pub fn cheap_state_advance_to_obtain_committees<'a, E: EthSpec, Err: BlockBlobEr pub fn get_validator_pubkey_cache( chain: &BeaconChain, ) -> Result>, BeaconChainError> { - chain - .validator_pubkey_cache - .try_read_for(VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT) - .ok_or(BeaconChainError::ValidatorPubkeyCacheLockTimeout) + Ok(chain.validator_pubkey_cache.read()) } /// Produces an _empty_ `BlockSignatureVerifier`. diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index c1ebeb68bba..72c2a9c2f53 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -12,10 +12,8 @@ use crate::light_client_server_cache::LightClientServerCache; use crate::migrate::{BackgroundMigrator, MigratorConfig}; use crate::persisted_beacon_chain::PersistedBeaconChain; use crate::shuffling_cache::{BlockShufflingIds, ShufflingCache}; -use crate::snapshot_cache::SnapshotCache; use crate::timeout_rw_lock::TimeoutRwLock; use crate::validator_monitor::{ValidatorMonitor, ValidatorMonitorConfig}; -use crate::validator_pubkey_cache::ValidatorPubkeyCache; use crate::ChainConfig; use crate::{ BeaconChain, BeaconChainTypes, BeaconForkChoiceStore, BeaconSnapshot, Eth1Chain, @@ -28,19 +26,20 @@ use futures::channel::mpsc::Sender; use kzg::{Kzg, TrustedSetup}; use operation_pool::{OperationPool, PersistedOperationPool}; use parking_lot::{Mutex, RwLock}; +use promise_cache::PromiseCache; use proto_array::{DisallowedReOrgOffsets, ReOrgThreshold}; use slasher::Slasher; use slog::{crit, debug, error, info, o, Logger}; use slot_clock::{SlotClock, TestingSlotClock}; -use state_processing::per_slot_processing; +use state_processing::{per_slot_processing, AllCaches}; use std::marker::PhantomData; use std::sync::Arc; use std::time::Duration; use store::{Error as StoreError, HotColdDB, ItemStore, KeyValueStoreOp}; use task_executor::{ShutdownReason, TaskExecutor}; use types::{ - BeaconBlock, BeaconState, BlobSidecarList, ChainSpec, Checkpoint, Epoch, EthSpec, Graffiti, - Hash256, Signature, SignedBeaconBlock, Slot, + BeaconBlock, BeaconState, BlobSidecarList, ChainSpec, Epoch, EthSpec, Graffiti, Hash256, + Signature, SignedBeaconBlock, Slot, }; /// An empty struct used to "witness" all the `BeaconChainTypes` traits. It has no user-facing @@ -92,7 +91,6 @@ pub struct BeaconChainBuilder { shutdown_sender: Option>, light_client_server_tx: Option>>, head_tracker: Option, - validator_pubkey_cache: Option>, spec: ChainSpec, chain_config: ChainConfig, log: Option, @@ -135,7 +133,6 @@ where shutdown_sender: None, light_client_server_tx: None, head_tracker: None, - validator_pubkey_cache: None, spec: E::default_spec(), chain_config: ChainConfig::default(), log: None, @@ -292,7 +289,7 @@ where .ok_or("Fork choice not found in store")?; let genesis_block = store - .get_blinded_block(&chain.genesis_block_root) + .get_blinded_block(&chain.genesis_block_root, Some(Slot::new(0))) .map_err(|e| descriptive_db_error("genesis block", &e))? .ok_or("Genesis block not found in store")?; let genesis_state = store @@ -317,16 +314,12 @@ where .unwrap_or_else(OperationPool::new), ); - let pubkey_cache = ValidatorPubkeyCache::load_from_store(store) - .map_err(|e| format!("Unable to open persisted pubkey cache: {:?}", e))?; - self.genesis_block_root = Some(chain.genesis_block_root); self.genesis_state_root = Some(genesis_block.state_root()); self.head_tracker = Some( HeadTracker::from_ssz_container(&chain.ssz_head_tracker) .map_err(|e| format!("Failed to decode head tracker for database: {:?}", e))?, ); - self.validator_pubkey_cache = Some(pubkey_cache); self.fork_choice = Some(fork_choice); Ok(self) @@ -347,24 +340,38 @@ where .ok_or("set_genesis_state requires a store")?; let beacon_block = genesis_block(&mut beacon_state, &self.spec)?; + let beacon_state_root = beacon_block.message().state_root(); + let beacon_block_root = beacon_block.canonical_root(); + let (blinded_block, payload) = beacon_block.into(); beacon_state .build_caches(&self.spec) .map_err(|e| format!("Failed to build genesis state caches: {:?}", e))?; - let beacon_state_root = beacon_block.message().state_root(); - let beacon_block_root = beacon_block.canonical_root(); - + info!(store.log, "Storing genesis state"; "state_root" => ?beacon_state_root); store .put_state(&beacon_state_root, &beacon_state) .map_err(|e| format!("Failed to store genesis state: {:?}", e))?; store - .put_block(&beacon_block_root, beacon_block.clone()) + .update_finalized_state(beacon_state_root, beacon_block_root, beacon_state.clone()) + .map_err(|e| format!("Failed to set genesis state as finalized state: {:?}", e))?; + + // Store the genesis block's execution payload (if any) in the hot database. + if let Some(execution_payload) = &payload { + store + .put_execution_payload(&beacon_block_root, execution_payload) + .map_err(|e| format!("Failed to store genesis execution payload: {e:?}"))?; + // FIXME(sproul): store it again under the 0x00 root? + } + + // Store the genesis block in the cold database. + store + .put_cold_blinded_block(&beacon_block_root, &blinded_block) .map_err(|e| format!("Failed to store genesis block: {:?}", e))?; // Store the genesis block under the `ZERO_HASH` key. store - .put_block(&Hash256::zero(), beacon_block.clone()) + .put_cold_blinded_block(&Hash256::zero(), &blinded_block) .map_err(|e| { format!( "Failed to store genesis block under 0x00..00 alias: {:?}", @@ -372,6 +379,11 @@ where ) })?; + // Reconstruct full genesis block. + let beacon_block = blinded_block + .try_into_full_block(payload) + .ok_or("Unable to reconstruct genesis block with payload")?; + self.genesis_state_root = Some(beacon_state_root); self.genesis_block_root = Some(beacon_block_root); self.genesis_time = Some(beacon_state.genesis_time()); @@ -462,7 +474,7 @@ where // Prime all caches before storing the state in the database and computing the tree hash // root. weak_subj_state - .build_caches(&self.spec) + .build_all_caches(&self.spec) .map_err(|e| format!("Error building caches on checkpoint state: {e:?}"))?; let weak_subj_state_root = weak_subj_state .update_tree_hash_cache() @@ -520,6 +532,12 @@ where let (_, updated_builder) = self.set_genesis_state(genesis_state)?; self = updated_builder; + // Build the committee caches before storing. The database assumes that states have + // committee caches built before storing. + weak_subj_state + .build_all_committee_caches(&self.spec) + .map_err(|e| format!("Error building caches on checkpoint state: {:?}", e))?; + // Fill in the linear block roots between the checkpoint block's slot and the aligned // state's slot. All slots less than the block's slot will be handled by block backfill, // while states greater or equal to the checkpoint state will be handled by `migrate_db`. @@ -537,6 +555,13 @@ where // Write the state, block and blobs non-atomically, it doesn't matter if they're forgotten // about on a crash restart. + store + .update_finalized_state( + weak_subj_state_root, + weak_subj_block_root, + weak_subj_state.clone(), + ) + .map_err(|e| format!("Failed to set checkpoint state as finalized state: {:?}", e))?; store .put_state(&weak_subj_state_root, &weak_subj_state) .map_err(|e| format!("Failed to store weak subjectivity state: {e:?}"))?; @@ -565,13 +590,6 @@ where .map_err(|e| format!("Failed to initialize blob info: {:?}", e))?, ); - // Store pruning checkpoint to prevent attempting to prune before the anchor state. - self.pending_io_batch - .push(store.pruning_checkpoint_store_op(Checkpoint { - root: weak_subj_block_root, - epoch: weak_subj_state.slot().epoch(E::slots_per_epoch()), - })); - let snapshot = BeaconSnapshot { beacon_block_root: weak_subj_block_root, beacon_block: Arc::new(weak_subj_block), @@ -734,7 +752,7 @@ where // Try to decode the head block according to the current fork, if that fails, try // to backtrack to before the most recent fork. let (head_block_root, head_block, head_reverted) = - match store.get_full_block(&initial_head_block_root) { + match store.get_full_block(&initial_head_block_root, None) { Ok(Some(block)) => (initial_head_block_root, block, false), Ok(None) => return Err("Head block not found in store".into()), Err(StoreError::SszDecodeError(_)) => { @@ -805,10 +823,16 @@ where )); } - let validator_pubkey_cache = self.validator_pubkey_cache.map(Ok).unwrap_or_else(|| { - ValidatorPubkeyCache::new(&head_snapshot.beacon_state, store.clone()) - .map_err(|e| format!("Unable to init validator pubkey cache: {:?}", e)) - })?; + let validator_pubkey_cache = store.immutable_validators.clone(); + + // Update pubkey cache on first start in case we have started from genesis. + let store_ops = validator_pubkey_cache + .write() + .import_new_pubkeys(&head_snapshot.beacon_state) + .map_err(|e| format!("error initializing pubkey cache: {e:?}"))?; + store + .do_atomically_with_block_and_blobs_cache(store_ops) + .map_err(|e| format!("error writing validator store: {e:?}"))?; let migrator_config = self.store_migrator_config.unwrap_or_default(); let store_migrator = BackgroundMigrator::new( @@ -860,10 +884,9 @@ where let genesis_validators_root = head_snapshot.beacon_state.genesis_validators_root(); let genesis_time = head_snapshot.beacon_state.genesis_time(); - let head_for_snapshot_cache = head_snapshot.clone(); let canonical_head = CanonicalHead::new(fork_choice, Arc::new(head_snapshot)); let shuffling_cache_size = self.chain_config.shuffling_cache_size; - let snapshot_cache_size = self.chain_config.snapshot_cache_size; + let parallel_state_cache_size = self.chain_config.parallel_state_cache_size; // Calculate the weak subjectivity point in which to backfill blocks to. let genesis_backfill_slot = if self.chain_config.genesis_backfill { @@ -939,10 +962,6 @@ where fork_choice_signal_rx, event_handler: self.event_handler, head_tracker, - snapshot_cache: TimeoutRwLock::new(SnapshotCache::new( - snapshot_cache_size, - head_for_snapshot_cache, - )), shuffling_cache: TimeoutRwLock::new(ShufflingCache::new( shuffling_cache_size, head_shuffling_ids, @@ -952,7 +971,12 @@ where beacon_proposer_cache, block_times_cache: <_>::default(), pre_finalization_block_cache: <_>::default(), - validator_pubkey_cache: TimeoutRwLock::new(validator_pubkey_cache), + parallel_state_cache: Arc::new(RwLock::new(PromiseCache::new( + parallel_state_cache_size, + Default::default(), + log.clone(), + ))), + validator_pubkey_cache, attester_cache: <_>::default(), early_attester_cache: <_>::default(), light_client_server_cache: LightClientServerCache::new(), @@ -970,7 +994,6 @@ where .map_err(|e| format!("Error initializing DataAvailabiltyChecker: {:?}", e))?, ), kzg, - block_production_state: Arc::new(Mutex::new(None)), }; let head = beacon_chain.head_snapshot(); @@ -1215,7 +1238,7 @@ mod test { assert_eq!( chain .store - .get_blinded_block(&Hash256::zero()) + .get_blinded_block(&Hash256::zero(), None) .expect("should read db") .expect("should find genesis block"), block.clone_as_blinded(), @@ -1270,14 +1293,15 @@ mod test { } for v in state.validators() { - let creds = v.withdrawal_credentials.as_bytes(); + let creds = v.withdrawal_credentials(); + let creds = creds.as_bytes(); assert_eq!( creds[0], spec.bls_withdrawal_prefix_byte, "first byte of withdrawal creds should be bls prefix" ); assert_eq!( &creds[1..], - &hash(&v.pubkey.as_ssz_bytes())[1..], + &hash(&v.pubkey().as_ssz_bytes())[1..], "rest of withdrawal creds should be pubkey hash" ) } diff --git a/beacon_node/beacon_chain/src/canonical_head.rs b/beacon_node/beacon_chain/src/canonical_head.rs index ced4eda05cf..1e924fb5fc6 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -35,10 +35,7 @@ use crate::beacon_chain::ATTESTATION_CACHE_LOCK_TIMEOUT; use crate::persisted_fork_choice::PersistedForkChoice; use crate::shuffling_cache::BlockShufflingIds; use crate::{ - beacon_chain::{ - BeaconForkChoice, BeaconStore, OverrideForkchoiceUpdate, - BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT, FORK_CHOICE_DB_KEY, - }, + beacon_chain::{BeaconForkChoice, BeaconStore, OverrideForkchoiceUpdate, FORK_CHOICE_DB_KEY}, block_times_cache::BlockTimesCache, events::ServerSentEventHandler, metrics, @@ -54,6 +51,7 @@ use itertools::process_results; use parking_lot::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; use slog::{crit, debug, error, warn, Logger}; use slot_clock::SlotClock; +use state_processing::AllCaches; use std::sync::Arc; use std::time::Duration; use store::{iter::StateRootsIterator, KeyValueStoreOp, StoreItem}; @@ -297,7 +295,7 @@ impl CanonicalHead { let fork_choice_view = fork_choice.cached_fork_choice_view(); let beacon_block_root = fork_choice_view.head_block_root; let beacon_block = store - .get_full_block(&beacon_block_root)? + .get_full_block(&beacon_block_root, None)? .ok_or(Error::MissingBeaconBlock(beacon_block_root))?; let current_slot = fork_choice.fc_store().get_current_slot(); let (_, beacon_state) = store @@ -466,9 +464,7 @@ impl BeaconChain { pub fn head_beacon_state_cloned(&self) -> BeaconState { // Don't clone whilst holding the read-lock, take an Arc-clone to reduce lock contention. let snapshot: Arc<_> = self.head_snapshot(); - snapshot - .beacon_state - .clone_with(CloneConfig::committee_caches_only()) + snapshot.beacon_state.clone() } /// Execute the fork choice algorithm and enthrone the result as the canonical head. @@ -652,48 +648,31 @@ impl BeaconChain { let new_cached_head = if new_view.head_block_root != old_view.head_block_root { metrics::inc_counter(&metrics::FORK_CHOICE_CHANGED_HEAD); - // Try and obtain the snapshot for `beacon_block_root` from the snapshot cache, falling - // back to a database read if that fails. - let new_snapshot = self - .snapshot_cache - .try_read_for(BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT) - .and_then(|snapshot_cache| { - snapshot_cache.get_cloned( + let mut new_snapshot = { + let beacon_block = self + .store + .get_full_block(&new_view.head_block_root, None)? + .ok_or(Error::MissingBeaconBlock(new_view.head_block_root))?; + + let (_, beacon_state) = self + .store + .get_advanced_hot_state( new_view.head_block_root, - CloneConfig::committee_caches_only(), - ) - }) - .map::, _>(Ok) - .unwrap_or_else(|| { - let beacon_block = self - .store - .get_full_block(&new_view.head_block_root)? - .ok_or(Error::MissingBeaconBlock(new_view.head_block_root))?; - - let (_, beacon_state) = self - .store - .get_advanced_hot_state( - new_view.head_block_root, - current_slot, - beacon_block.state_root(), - )? - .ok_or(Error::MissingBeaconState(beacon_block.state_root()))?; - - Ok(BeaconSnapshot { - beacon_block: Arc::new(beacon_block), - beacon_block_root: new_view.head_block_root, - beacon_state, - }) - }) - .and_then(|mut snapshot| { - // Regardless of where we got the state from, attempt to build the committee - // caches. - snapshot - .beacon_state - .build_all_committee_caches(&self.spec) - .map_err(Into::into) - .map(|()| snapshot) - })?; + current_slot, + beacon_block.state_root(), + )? + .ok_or(Error::MissingBeaconState(beacon_block.state_root()))?; + + BeaconSnapshot { + beacon_block: Arc::new(beacon_block), + beacon_block_root: new_view.head_block_root, + beacon_state, + } + }; + + // Regardless of where we got the state from, attempt to build all the + // caches except the tree hash cache. + new_snapshot.beacon_state.build_all_caches(&self.spec)?; let new_cached_head = CachedHead { snapshot: Arc::new(new_snapshot), @@ -834,25 +813,6 @@ impl BeaconChain { .beacon_state .attester_shuffling_decision_root(self.genesis_block_root, RelativeEpoch::Current); - // Update the snapshot cache with the latest head value. - // - // This *could* be done inside `recompute_head`, however updating the head on the snapshot - // cache is not critical so we avoid placing it on a critical path. Note that this function - // will not return an error if the update fails, it will just log an error. - self.snapshot_cache - .try_write_for(BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT) - .map(|mut snapshot_cache| { - snapshot_cache.update_head(new_snapshot.beacon_block_root); - }) - .unwrap_or_else(|| { - error!( - self.log, - "Failed to obtain cache write lock"; - "lock" => "snapshot_cache", - "task" => "update head" - ); - }); - match BlockShufflingIds::try_from_head( new_snapshot.beacon_block_root, &new_snapshot.beacon_state, @@ -860,9 +820,7 @@ impl BeaconChain { Ok(head_shuffling_ids) => { self.shuffling_cache .try_write_for(ATTESTATION_CACHE_LOCK_TIMEOUT) - .map(|mut shuffling_cache| { - shuffling_cache.update_head_shuffling_ids(head_shuffling_ids) - }) + .map(|mut shuffling_cache| shuffling_cache.update_protector(head_shuffling_ids)) .unwrap_or_else(|| { error!( self.log, @@ -998,26 +956,6 @@ impl BeaconChain { .start_slot(T::EthSpec::slots_per_epoch()), ); - self.snapshot_cache - .try_write_for(BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT) - .map(|mut snapshot_cache| { - snapshot_cache.prune(new_view.finalized_checkpoint.epoch); - debug!( - self.log, - "Snapshot cache pruned"; - "new_len" => snapshot_cache.len(), - "remaining_roots" => ?snapshot_cache.beacon_block_roots(), - ); - }) - .unwrap_or_else(|| { - error!( - self.log, - "Failed to obtain cache write lock"; - "lock" => "snapshot_cache", - "task" => "prune" - ); - }); - self.attester_cache .prune_below(new_view.finalized_checkpoint.epoch); @@ -1472,6 +1410,7 @@ fn observe_head_block_delays( block_delay: block_delay_total, observed_delay: block_delays.observed, imported_delay: block_delays.imported, + attestable_delay: block_delays.attestable, set_as_head_delay: block_delays.set_as_head, execution_optimistic: head_block_is_optimistic, })); diff --git a/beacon_node/beacon_chain/src/chain_config.rs b/beacon_node/beacon_chain/src/chain_config.rs index 0772aff6710..545bdd20b7b 100644 --- a/beacon_node/beacon_chain/src/chain_config.rs +++ b/beacon_node/beacon_chain/src/chain_config.rs @@ -16,6 +16,9 @@ pub const DEFAULT_PREPARE_PAYLOAD_LOOKAHEAD_FACTOR: u32 = 3; /// Fraction of a slot lookahead for fork choice in the state advance timer (500ms on mainnet). pub const FORK_CHOICE_LOOKAHEAD_FACTOR: u32 = 24; +/// Cache only a small number of states in the parallel cache by default. +pub const DEFAULT_PARALLEL_STATE_CACHE_SIZE: usize = 2; + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] pub struct ChainConfig { /// Maximum number of slots to skip when importing an attestation. @@ -75,8 +78,6 @@ pub struct ChainConfig { pub optimistic_finalized_sync: bool, /// The size of the shuffling cache, pub shuffling_cache_size: usize, - /// The size of the snapshot cache. - pub snapshot_cache_size: usize, /// If using a weak-subjectivity sync, whether we should download blocks all the way back to /// genesis. pub genesis_backfill: bool, @@ -86,6 +87,8 @@ pub struct ChainConfig { pub always_prepare_payload: bool, /// Number of epochs between each migration of data from the hot database to the freezer. pub epochs_per_migration: u64, + /// Size of the promise cache for de-duplicating parallel state requests. + pub parallel_state_cache_size: usize, /// When set to true Light client server computes and caches state proofs for serving updates pub enable_light_client_server: bool, } @@ -116,10 +119,10 @@ impl Default for ChainConfig { // This value isn't actually read except in tests. optimistic_finalized_sync: true, shuffling_cache_size: crate::shuffling_cache::DEFAULT_CACHE_SIZE, - snapshot_cache_size: crate::snapshot_cache::DEFAULT_SNAPSHOT_CACHE_SIZE, genesis_backfill: false, always_prepare_payload: false, epochs_per_migration: crate::migrate::DEFAULT_EPOCHS_PER_MIGRATION, + parallel_state_cache_size: DEFAULT_PARALLEL_STATE_CACHE_SIZE, enable_light_client_server: false, } } diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index 50e07987fdf..9c74db1b933 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -907,7 +907,7 @@ mod test { let chain = &harness.chain; let log = chain.log.clone(); let head = chain.head_snapshot(); - let parent_state = head.beacon_state.clone_with_only_committee_caches(); + let parent_state = head.beacon_state.clone(); let target_slot = chain.slot().expect("should get slot") + 1; let parent_root = head.beacon_block_root; diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 9c82e964cc0..864cc277001 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -91,13 +91,10 @@ pub enum BeaconChainError { ValidatorPubkeyCacheLockTimeout, SnapshotCacheLockTimeout, IncorrectStateForAttestation(RelativeEpochError), - InvalidValidatorPubkeyBytes(bls::Error), ValidatorPubkeyCacheIncomplete(usize), SignatureSetError(SignatureSetError), BlockSignatureVerifierError(state_processing::block_signature_verifier::Error), BlockReplayError(BlockReplayError), - DuplicateValidatorPublicKey, - ValidatorPubkeyCacheError(String), ValidatorIndexUnknown(usize), ValidatorPubkeyUnknown(PublicKeyBytes), OpPoolError(OpPoolError), @@ -214,8 +211,7 @@ pub enum BeaconChainError { }, AttestationHeadNotInForkChoice(Hash256), MissingPersistedForkChoice, - CommitteePromiseFailed(oneshot_broadcast::Error), - MaxCommitteePromises(usize), + ShufflingCacheError(promise_cache::PromiseCacheError), BlsToExecutionPriorToCapella, BlsToExecutionConflictsWithPool, InconsistentFork(InconsistentFork), @@ -279,6 +275,7 @@ pub enum BlockProductionError { TerminalPoWBlockLookupFailed(execution_layer::Error), GetPayloadFailed(execution_layer::Error), FailedToReadFinalizedBlock(store::Error), + FailedToLoadState(store::Error), MissingFinalizedBlock(Hash256), BlockTooLarge(usize), ShuttingDown, diff --git a/beacon_node/beacon_chain/src/eth1_chain.rs b/beacon_node/beacon_chain/src/eth1_chain.rs index 3ec39f9d192..31297244e3e 100644 --- a/beacon_node/beacon_chain/src/eth1_chain.rs +++ b/beacon_node/beacon_chain/src/eth1_chain.rs @@ -1020,6 +1020,7 @@ mod test { mod collect_valid_votes { use super::*; + use types::List; fn get_eth1_data_vec(n: u64, block_number_offset: u64) -> Vec<(Eth1Data, BlockNumber)> { (0..n) @@ -1067,12 +1068,14 @@ mod test { let votes_to_consider = get_eth1_data_vec(slots, 0); - *state.eth1_data_votes_mut() = votes_to_consider[0..slots as usize / 4] - .iter() - .map(|(eth1_data, _)| eth1_data) - .cloned() - .collect::>() - .into(); + *state.eth1_data_votes_mut() = List::new( + votes_to_consider[0..slots as usize / 4] + .iter() + .map(|(eth1_data, _)| eth1_data) + .cloned() + .collect::>(), + ) + .unwrap(); let votes = collect_valid_votes(&state, &votes_to_consider.clone().into_iter().collect()); @@ -1096,12 +1099,14 @@ mod test { .expect("should have some eth1 data") .clone(); - *state.eth1_data_votes_mut() = vec![duplicate_eth1_data.clone(); 4] - .iter() - .map(|(eth1_data, _)| eth1_data) - .cloned() - .collect::>() - .into(); + *state.eth1_data_votes_mut() = List::new( + vec![duplicate_eth1_data.clone(); 4] + .iter() + .map(|(eth1_data, _)| eth1_data) + .cloned() + .collect::>(), + ) + .unwrap(); let votes = collect_valid_votes(&state, &votes_to_consider.into_iter().collect()); assert_votes!( diff --git a/beacon_node/beacon_chain/src/fork_revert.rs b/beacon_node/beacon_chain/src/fork_revert.rs index 084ae95e096..d25cecda55a 100644 --- a/beacon_node/beacon_chain/src/fork_revert.rs +++ b/beacon_node/beacon_chain/src/fork_revert.rs @@ -105,7 +105,7 @@ pub fn reset_fork_choice_to_finalization, Cold: It let finalized_checkpoint = head_state.finalized_checkpoint(); let finalized_block_root = finalized_checkpoint.root; let finalized_block = store - .get_full_block(&finalized_block_root) + .get_full_block(&finalized_block_root, None) .map_err(|e| format!("Error loading finalized block: {:?}", e))? .ok_or_else(|| { format!( diff --git a/beacon_node/beacon_chain/src/head_tracker.rs b/beacon_node/beacon_chain/src/head_tracker.rs index 71e2473cdcf..b7802cbb2e0 100644 --- a/beacon_node/beacon_chain/src/head_tracker.rs +++ b/beacon_node/beacon_chain/src/head_tracker.rs @@ -90,8 +90,8 @@ impl PartialEq for HeadTracker { /// This is used when persisting the state of the `BeaconChain` to disk. #[derive(Encode, Decode, Clone)] pub struct SszHeadTracker { - roots: Vec, - slots: Vec, + pub roots: Vec, + pub slots: Vec, } impl SszHeadTracker { diff --git a/beacon_node/beacon_chain/src/historical_blocks.rs b/beacon_node/beacon_chain/src/historical_blocks.rs index 85208c8ad6f..a2377d6a2e0 100644 --- a/beacon_node/beacon_chain/src/historical_blocks.rs +++ b/beacon_node/beacon_chain/src/historical_blocks.rs @@ -9,7 +9,7 @@ use state_processing::{ use std::borrow::Cow; use std::iter; use std::time::Duration; -use store::{chunked_vector::BlockRoots, AnchorInfo, BlobInfo, ChunkWriter, KeyValueStore}; +use store::{get_key_for_col, AnchorInfo, BlobInfo, DBColumn, KeyValueStore, KeyValueStoreOp}; use types::{Hash256, Slot}; /// Use a longer timeout on the pubkey cache. @@ -97,13 +97,11 @@ impl BeaconChain { let mut expected_block_root = anchor_info.oldest_block_parent; let mut prev_block_slot = anchor_info.oldest_block_slot; - let mut chunk_writer = - ChunkWriter::::new(&self.store.cold_db, prev_block_slot.as_usize())?; + let mut new_oldest_blob_slot = blob_info.oldest_blob_slot; let mut blob_batch = Vec::with_capacity(n_blobs_lists_to_import); let mut cold_batch = Vec::with_capacity(blocks_to_import.len()); - let mut hot_batch = Vec::with_capacity(blocks_to_import.len()); let mut signed_blocks = Vec::with_capacity(blocks_to_import.len()); for available_block in blocks_to_import.into_iter().rev() { @@ -118,9 +116,12 @@ impl BeaconChain { } let blinded_block = block.clone_as_blinded(); - // Store block in the hot database without payload. - self.store - .blinded_block_as_kv_store_ops(&block_root, &blinded_block, &mut hot_batch); + // Store block in the freezer database without payload. + self.store.blinded_block_as_cold_kv_store_ops( + &block_root, + &blinded_block, + &mut cold_batch, + )?; // Store the blobs too if let Some(blobs) = maybe_blobs { new_oldest_blob_slot = Some(block.slot()); @@ -129,8 +130,13 @@ impl BeaconChain { } // Store block roots, including at all skip slots in the freezer DB. - for slot in (block.slot().as_usize()..prev_block_slot.as_usize()).rev() { - chunk_writer.set(slot, block_root, &mut cold_batch)?; + // The block root mapping for `block.slot()` itself was already written when the block + // was stored, above. + for slot in (block.slot().as_usize() + 1..prev_block_slot.as_usize()).rev() { + cold_batch.push(KeyValueStoreOp::PutKeyValue( + get_key_for_col(DBColumn::BeaconBlockRoots.into(), &slot.to_be_bytes()), + block_root.as_bytes().to_vec(), + )); } prev_block_slot = block.slot(); @@ -142,15 +148,17 @@ impl BeaconChain { // completion. if expected_block_root == self.genesis_block_root { let genesis_slot = self.spec.genesis_slot; - for slot in genesis_slot.as_usize()..prev_block_slot.as_usize() { - chunk_writer.set(slot, self.genesis_block_root, &mut cold_batch)?; + for slot in genesis_slot.as_u64()..prev_block_slot.as_u64() { + cold_batch.push(KeyValueStoreOp::PutKeyValue( + get_key_for_col(DBColumn::BeaconBlockRoots.into(), &slot.to_be_bytes()), + self.genesis_block_root.as_bytes().to_vec(), + )); } prev_block_slot = genesis_slot; expected_block_root = Hash256::zero(); break; } } - chunk_writer.write(&mut cold_batch)?; // these were pushed in reverse order so we reverse again signed_blocks.reverse(); @@ -197,12 +205,15 @@ impl BeaconChain { drop(verify_timer); drop(sig_timer); - // Write the I/O batches to disk, writing the blocks themselves first, as it's better - // for the hot DB to contain extra blocks than for the cold DB to point to blocks that - // do not exist. + // Write the I/O batches to disk. + // We fsync after each write because we need the writes to the blob and freezer DB to + // be persisted if the writes to the hot DB are persisted. Without fsync we could end up + // in a situation where the hot DB's anchor is updated but the actual blocks are forgotten + // from disk. self.store.blobs_db.do_atomically(blob_batch)?; - self.store.hot_db.do_atomically(hot_batch)?; + self.store.blobs_db.sync()?; self.store.cold_db.do_atomically(cold_batch)?; + self.store.cold_db.sync()?; let mut anchor_and_blob_batch = Vec::with_capacity(2); @@ -232,6 +243,7 @@ impl BeaconChain { .compare_and_set_anchor_info(Some(anchor_info), Some(new_anchor))?, ); self.store.hot_db.do_atomically(anchor_and_blob_batch)?; + self.store.hot_db.sync()?; // If backfill has completed and the chain is configured to reconstruct historic states, // send a message to the background migrator instructing it to begin reconstruction. diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 7721c9b00ff..7ee18de0351 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -45,20 +45,19 @@ pub mod observed_block_producers; pub mod observed_operations; mod observed_slashable; pub mod otb_verification_service; +mod parallel_state_cache; mod persisted_beacon_chain; mod persisted_fork_choice; mod pre_finalization_cache; pub mod proposer_prep_service; pub mod schema_change; pub mod shuffling_cache; -pub mod snapshot_cache; pub mod state_advance_timer; pub mod sync_committee_rewards; pub mod sync_committee_verification; pub mod test_utils; mod timeout_rw_lock; pub mod validator_monitor; -pub mod validator_pubkey_cache; pub use self::beacon_chain::{ AttestationProcessingOutcome, AvailabilityProcessingStatus, BeaconBlockResponse, @@ -98,3 +97,13 @@ pub use state_processing::per_block_processing::errors::{ pub use store; pub use timeout_rw_lock::TimeoutRwLock; pub use types; + +pub mod validator_pubkey_cache { + use crate::BeaconChainTypes; + + pub type ValidatorPubkeyCache = store::ValidatorPubkeyCache< + ::EthSpec, + ::HotStore, + ::ColdStore, + >; +} diff --git a/beacon_node/beacon_chain/src/light_client_server_cache.rs b/beacon_node/beacon_chain/src/light_client_server_cache.rs index ca029057373..7f8f205fd31 100644 --- a/beacon_node/beacon_chain/src/light_client_server_cache.rs +++ b/beacon_node/beacon_chain/src/light_client_server_cache.rs @@ -84,13 +84,12 @@ impl LightClientServerCache { let signature_slot = block_slot; let attested_block_root = block_parent_root; - let attested_block = - store - .get_full_block(attested_block_root)? - .ok_or(BeaconChainError::DBInconsistent(format!( - "Block not available {:?}", - attested_block_root - )))?; + let attested_block = store.get_full_block(attested_block_root, None)?.ok_or( + BeaconChainError::DBInconsistent(format!( + "Block not available {:?}", + attested_block_root + )), + )?; let cached_parts = self.get_or_compute_prev_block_cache( store.clone(), @@ -130,7 +129,7 @@ impl LightClientServerCache { if is_latest_finality & !cached_parts.finalized_block_root.is_zero() { // Immediately after checkpoint sync the finalized block may not be available yet. if let Some(finalized_block) = - store.get_full_block(&cached_parts.finalized_block_root)? + store.get_full_block(&cached_parts.finalized_block_root, None)? { *self.latest_finality_update.write() = Some(LightClientFinalityUpdate::new( &attested_block, diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index b40f46da221..833d0822494 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -4,12 +4,8 @@ use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; use lazy_static::lazy_static; pub use lighthouse_metrics::*; use slot_clock::SlotClock; -use std::time::Duration; use types::{BeaconState, Epoch, EthSpec, Hash256, Slot}; -/// The maximum time to wait for the snapshot cache lock during a metrics scrape. -const SNAPSHOT_CACHE_TIMEOUT: Duration = Duration::from_millis(100); - // Attestation simulator metrics pub const VALIDATOR_MONITOR_ATTESTATION_SIMULATOR_HEAD_ATTESTER_HIT_TOTAL: &str = "validator_monitor_attestation_simulator_head_attester_hit_total"; @@ -839,7 +835,10 @@ lazy_static! { "Number of attester slashings seen", &["src", "validator"] ); +} +// Fourth lazy-static block is used to account for macro recursion limit. +lazy_static! { /* * Block Delay Metrics */ @@ -860,7 +859,11 @@ lazy_static! { "Duration between the time the block was imported and the time when it was set as head.", // [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5] decimal_buckets(-2,-1) - ); + ); + pub static ref BEACON_BLOCK_HEAD_ATTESTABLE_DELAY_TIME: Result = try_create_histogram( + "beacon_block_head_attestable_delay_time", + "Duration between the start of the slot and the time at which the block could be attested to.", + ); pub static ref BEACON_BLOCK_HEAD_SLOT_START_DELAY_TIME: Result = try_create_histogram_with_buckets( "beacon_block_head_slot_start_delay_time", "Duration between the start of the block's slot and the time when it was set as head.", @@ -872,6 +875,22 @@ lazy_static! { "Triggered when the duration between the start of the block's slot and the current time \ will result in failed attestations.", ); + pub static ref BEACON_BLOCK_HEAD_MISSED_ATT_DEADLINE_LATE: Result = try_create_int_counter( + "beacon_block_head_missed_att_deadline_late", + "Total number of delayed head blocks that arrived late" + ); + pub static ref BEACON_BLOCK_HEAD_MISSED_ATT_DEADLINE_BORDERLINE: Result = try_create_int_counter( + "beacon_block_head_missed_att_deadline_borderline", + "Total number of delayed head blocks that arrived very close to the deadline" + ); + pub static ref BEACON_BLOCK_HEAD_MISSED_ATT_DEADLINE_SLOW: Result = try_create_int_counter( + "beacon_block_head_missed_att_deadline_slow", + "Total number of delayed head blocks that arrived on time but not processed in time" + ); + pub static ref BEACON_BLOCK_HEAD_MISSED_ATT_DEADLINE_OTHER: Result = try_create_int_counter( + "beacon_block_head_missed_att_deadline_other", + "Total number of delayed head blocks that were not late and not slow to process" + ); /* * General block metrics @@ -881,10 +900,7 @@ lazy_static! { "gossip_beacon_block_skipped_slots", "For each gossip blocks, the number of skip slots between it and its parent" ); -} -// Fourth lazy-static block is used to account for macro recursion limit. -lazy_static! { /* * Sync Committee Message Verification */ @@ -1186,15 +1202,10 @@ pub fn scrape_for_metrics(beacon_chain: &BeaconChain) { let attestation_stats = beacon_chain.op_pool.attestation_stats(); - if let Some(snapshot_cache) = beacon_chain - .snapshot_cache - .try_write_for(SNAPSHOT_CACHE_TIMEOUT) - { - set_gauge( - &BLOCK_PROCESSING_SNAPSHOT_CACHE_SIZE, - snapshot_cache.len() as i64, - ) - } + set_gauge_by_usize( + &BLOCK_PROCESSING_SNAPSHOT_CACHE_SIZE, + beacon_chain.store.state_cache_len(), + ); let da_checker_metrics = beacon_chain.data_availability_checker.metrics(); set_gauge_by_usize( @@ -1324,7 +1335,7 @@ fn scrape_head_state(state: &BeaconState, state_root: Hash256) { num_active += 1; } - if v.slashed { + if v.slashed() { num_slashed += 1; } diff --git a/beacon_node/beacon_chain/src/migrate.rs b/beacon_node/beacon_chain/src/migrate.rs index ad597bf92aa..8d66f865e16 100644 --- a/beacon_node/beacon_chain/src/migrate.rs +++ b/beacon_node/beacon_chain/src/migrate.rs @@ -6,7 +6,7 @@ use parking_lot::Mutex; use slog::{debug, error, info, warn, Logger}; use std::collections::{HashMap, HashSet}; use std::mem; -use std::sync::{mpsc, Arc}; +use std::sync::Arc; use std::thread; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use store::hot_cold_store::{migrate_database, HotColdDBError}; @@ -24,6 +24,7 @@ const MAX_COMPACTION_PERIOD_SECONDS: u64 = 604800; const MIN_COMPACTION_PERIOD_SECONDS: u64 = 7200; /// Compact after a large finality gap, if we respect `MIN_COMPACTION_PERIOD_SECONDS`. const COMPACTION_FINALITY_DISTANCE: u64 = 1024; +const BLOCKS_PER_RECONSTRUCTION: usize = 8192 * 4; /// Default number of epochs to wait between finalization migrations. pub const DEFAULT_EPOCHS_PER_MIGRATION: u64 = 1; @@ -34,8 +35,12 @@ pub struct BackgroundMigrator, Cold: ItemStore> db: Arc>, /// Record of when the last migration ran, for enforcing `epochs_per_migration`. prev_migration: Arc>, - #[allow(clippy::type_complexity)] - tx_thread: Option, thread::JoinHandle<()>)>>, + tx_thread: Option< + Mutex<( + crossbeam_channel::Sender, + thread::JoinHandle<()>, + )>, + >, /// Genesis block root, for persisting the `PersistedBeaconChain`. genesis_block_root: Hash256, log: Logger, @@ -72,6 +77,7 @@ impl MigratorConfig { } /// Record of when the last migration ran. +#[derive(Debug)] pub struct PrevMigration { /// The epoch at which the last finalization migration ran. epoch: Epoch, @@ -114,12 +120,14 @@ pub enum PruningError { } /// Message sent to the migration thread containing the information it needs to run. +#[derive(Debug)] pub enum Notification { Finalization(FinalizationNotification), Reconstruction, PruneBlobs(Epoch), } +#[derive(Clone, Debug)] pub struct FinalizationNotification { finalized_state_root: BeaconStateHash, finalized_checkpoint: Checkpoint, @@ -128,6 +136,21 @@ pub struct FinalizationNotification { genesis_block_root: Hash256, } +/* +impl Notification { + pub fn epoch(&self) -> Option { + match self { + Notification::Finalization(FinalizationNotification { + finalized_checkpoint, + .. + }) => Some(finalized_checkpoint.epoch), + Notification::Reconstruction => None, + Notification::PruneBlobs => None, + } + } +} +*/ + impl, Cold: ItemStore> BackgroundMigrator { /// Create a new `BackgroundMigrator` and spawn its thread if necessary. pub fn new( @@ -201,7 +224,7 @@ impl, Cold: ItemStore> BackgroundMigrator>, log: &Logger) { - if let Err(e) = db.reconstruct_historic_states() { + if let Err(e) = db.reconstruct_historic_states(None) { error!( log, "State reconstruction failed"; @@ -287,6 +310,7 @@ impl, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator { debug!( log, - "Database migration postponed, unaligned finalized block"; + "Database migration postponed due to unaligned finalized block"; "slot" => slot.as_u64() ); } @@ -361,7 +385,7 @@ impl, Cold: ItemStore> BackgroundMigrator format!("{:?}", e) + "error" => ?e ); return; } @@ -374,10 +398,14 @@ impl, Cold: ItemStore> BackgroundMigrator format!("{:?}", e)); + warn!(log, "Database compaction failed"; "error" => ?e); } - debug!(log, "Database consolidation complete"); + debug!( + log, + "Database consolidation complete"; + "running_time_ms" => timer.elapsed().as_millis() + ); } /// Spawn a new child thread to run the migration process. @@ -386,48 +414,85 @@ impl, Cold: ItemStore> BackgroundMigrator>, log: Logger, - ) -> (mpsc::Sender, thread::JoinHandle<()>) { - let (tx, rx) = mpsc::channel(); + ) -> ( + crossbeam_channel::Sender, + thread::JoinHandle<()>, + ) { + let (tx, rx) = crossbeam_channel::unbounded(); + let tx_thread = tx.clone(); let thread = thread::spawn(move || { - while let Ok(notif) = rx.recv() { - let mut reconstruction_notif = None; - let mut finalization_notif = None; - let mut prune_blobs_notif = None; - match notif { - Notification::Reconstruction => reconstruction_notif = Some(notif), - Notification::Finalization(fin) => finalization_notif = Some(fin), - Notification::PruneBlobs(dab) => prune_blobs_notif = Some(dab), - } - // Read the rest of the messages in the channel, taking the best of each type. - for notif in rx.try_iter() { - match notif { - Notification::Reconstruction => reconstruction_notif = Some(notif), - Notification::Finalization(fin) => { - if let Some(current) = finalization_notif.as_mut() { - if fin.finalized_checkpoint.epoch - > current.finalized_checkpoint.epoch - { - *current = fin; - } - } else { - finalization_notif = Some(fin); - } + let mut sel = crossbeam_channel::Select::new(); + sel.recv(&rx); + + loop { + // Block until sth is in queue + let _queue_size = sel.ready(); + let queue: Vec = rx.try_iter().collect(); + debug!( + log, + "New worker thread poll"; + "queue" => ?queue + ); + + // Find a reconstruction notification and best finalization notification. + let reconstruction_notif = queue + .iter() + .find(|n| matches!(n, Notification::Reconstruction)); + let migrate_notif = queue + .iter() + .filter_map(|n| match n { + Notification::Finalization(f) => Some(f), + _ => None, + }) + .max_by_key(|f| f.finalized_checkpoint.epoch) + .cloned(); + let prune_blobs_notif = queue + .iter() + .filter_map(|n| match n { + Notification::PruneBlobs(dab) => Some(*dab), + _ => None, + }) + .max(); + + // Do a bit of state reconstruction first if required. + if reconstruction_notif.is_some() { + let timer = std::time::Instant::now(); + + match db.reconstruct_historic_states(Some(BLOCKS_PER_RECONSTRUCTION)) { + Err(Error::StateReconstructionDidNotComplete) => { + info!( + log, + "Finished reconstruction batch"; + "batch_time_ms" => timer.elapsed().as_millis() + ); + // Handle send error + let _ = tx_thread.send(Notification::Reconstruction); + } + Err(e) => { + error!( + log, + "State reconstruction failed"; + "error" => ?e, + ); } - Notification::PruneBlobs(dab) => { - prune_blobs_notif = std::cmp::max(prune_blobs_notif, Some(dab)); + Ok(()) => { + info!( + log, + "Finished state reconstruction"; + "batch_time_ms" => timer.elapsed().as_millis() + ); } } } - // If reconstruction is on-going, ignore finalization migration and blob pruning. - if reconstruction_notif.is_some() { - Self::run_reconstruction(db.clone(), &log); - } else { - if let Some(fin) = finalization_notif { - Self::run_migration(db.clone(), fin, &log); - } - if let Some(dab) = prune_blobs_notif { - Self::run_prune_blobs(db.clone(), dab, &log); - } + + // Do the finalization migration. + if let Some(notif) = migrate_notif { + Self::run_migration(db.clone(), notif, &log); + } + + // Prune blobs. + if let Some(dab) = prune_blobs_notif { + Self::run_prune_blobs(db.clone(), dab, &log); } } }); @@ -447,13 +512,7 @@ impl, Cold: ItemStore> BackgroundMigrator Result { - let old_finalized_checkpoint = - store - .load_pruning_checkpoint()? - .unwrap_or_else(|| Checkpoint { - epoch: Epoch::new(0), - root: Hash256::zero(), - }); + let old_finalized_checkpoint = store.get_pruning_checkpoint(); let old_finalized_slot = old_finalized_checkpoint .epoch @@ -507,18 +566,32 @@ impl, Cold: ItemStore> BackgroundMigrator>()?; + // Quick sanity check. If the canonical block & state roots are incorrect then we could + // incorrectly delete canonical states, which would corrupt the database. + let expected_canonical_block_roots = new_finalized_slot + .saturating_sub(old_finalized_slot) + .as_usize() + .saturating_add(1); + if newly_finalized_chain.len() != expected_canonical_block_roots { + return Err(BeaconChainError::DBInconsistent(format!( + "canonical chain iterator is corrupt; \ + expected {} but got {} block roots", + expected_canonical_block_roots, + newly_finalized_chain.len() + ))); + } + // We don't know which blocks are shared among abandoned chains, so we buffer and delete // everything in one fell swoop. let mut abandoned_blocks: HashSet = HashSet::new(); - let mut abandoned_states: HashSet<(Slot, BeaconStateHash)> = HashSet::new(); let mut abandoned_heads: HashSet = HashSet::new(); let heads = head_tracker.heads(); debug!( log, "Extra pruning information"; - "old_finalized_root" => format!("{:?}", old_finalized_checkpoint.root), - "new_finalized_root" => format!("{:?}", new_finalized_checkpoint.root), + "old_finalized_root" => ?old_finalized_checkpoint.root, + "new_finalized_root" => ?new_finalized_checkpoint.root, "head_count" => heads.len(), ); @@ -527,7 +600,7 @@ impl, Cold: ItemStore> BackgroundMigrator block.state_root(), Ok(None) => { return Err(BeaconStateError::MissingBeaconBlock(head_hash.into()).into()) @@ -546,14 +619,15 @@ impl, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator { if slot > new_finalized_slot { - potentially_abandoned_blocks.push(( - slot, - Some(block_root), - Some(state_root), - )); + potentially_abandoned_blocks.insert(block_root); } else if slot >= old_finalized_slot { return Err(PruningError::MissingInfoForCanonicalChain { slot }.into()); } else { @@ -577,7 +647,7 @@ impl, Cold: ItemStore> BackgroundMigrator format!("{:?}", head_hash), + "head_block_root" => ?head_hash, "head_slot" => head_slot, ); potentially_abandoned_head.take(); @@ -605,26 +675,14 @@ impl, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator format!("{:?}", abandoned_head), + "head_block_root" => ?abandoned_head, "head_slot" => head_slot, ); abandoned_heads.insert(abandoned_head); - abandoned_blocks.extend( - potentially_abandoned_blocks - .iter() - .filter_map(|(_, maybe_block_hash, _)| *maybe_block_hash), - ); - abandoned_states.extend(potentially_abandoned_blocks.iter().filter_map( - |(slot, _, maybe_state_hash)| maybe_state_hash.map(|sr| (*slot, sr)), - )); + abandoned_blocks.extend(potentially_abandoned_blocks); } } @@ -668,6 +719,7 @@ impl, Cold: ItemStore> BackgroundMigrator> = abandoned_blocks .into_iter() .map(Into::into) @@ -678,11 +730,6 @@ impl, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator num_deleted_blocks, + ); + + // Do a separate pass to clean up irrelevant states. + let mut state_delete_batch = vec![]; + for res in store.iter_hot_state_summaries() { + let (state_root, summary) = res?; + + if summary.slot <= new_finalized_slot { + // If state root doesn't match state root from canonical chain, then delete. + // We may also find older states here that should have been deleted by `migrate_db` + // but weren't due to wonky I/O atomicity. + if newly_finalized_chain + .get(&summary.slot) + .map_or(true, |(_, canonical_state_root)| { + state_root != Hash256::from(*canonical_state_root) + }) + { + let reason = if summary.slot < old_finalized_slot { + "old dangling state" + } else { + "non-canonical" + }; + debug!( + log, + "Deleting state"; + "state_root" => ?state_root, + "slot" => summary.slot, + "reason" => reason, + ); + state_delete_batch.push(StoreOp::DeleteState(state_root, Some(summary.slot))); + } + } + } + let num_deleted_states = state_delete_batch.len(); + store.do_atomically_with_block_and_blobs_cache(state_delete_batch)?; + debug!( + log, + "Database state pruning complete"; + "num_deleted_states" => num_deleted_states, + ); Ok(PruningOutcome::Successful { old_finalized_checkpoint, diff --git a/beacon_node/beacon_chain/src/parallel_state_cache.rs b/beacon_node/beacon_chain/src/parallel_state_cache.rs new file mode 100644 index 00000000000..d568d3248cd --- /dev/null +++ b/beacon_node/beacon_chain/src/parallel_state_cache.rs @@ -0,0 +1,22 @@ +use promise_cache::{PromiseCache, Protect}; +use types::{BeaconState, Hash256}; + +#[derive(Debug, Default)] +pub struct ParallelStateProtector; + +impl Protect for ParallelStateProtector { + type SortKey = usize; + + /// Evict in arbitrary (hashmap) order by using the same key for every value. + fn sort_key(&self, _: &Hash256) -> Self::SortKey { + 0 + } + + /// We don't care too much about preventing evictions of particular states here. All the states + /// in this cache should be different from the head state. + fn protect_from_eviction(&self, _: &Hash256) -> bool { + false + } +} + +pub type ParallelStateCache = PromiseCache, ParallelStateProtector>; diff --git a/beacon_node/beacon_chain/src/persisted_fork_choice.rs b/beacon_node/beacon_chain/src/persisted_fork_choice.rs index 8297ea93457..271a587016d 100644 --- a/beacon_node/beacon_chain/src/persisted_fork_choice.rs +++ b/beacon_node/beacon_chain/src/persisted_fork_choice.rs @@ -49,7 +49,7 @@ macro_rules! impl_store_item { self.as_ssz_bytes() } - fn from_store_bytes(bytes: &[u8]) -> std::result::Result { + fn from_store_bytes(bytes: &[u8]) -> Result { Self::from_ssz_bytes(bytes).map_err(Into::into) } } diff --git a/beacon_node/beacon_chain/src/pre_finalization_cache.rs b/beacon_node/beacon_chain/src/pre_finalization_cache.rs index 22b76e026cb..3b337d4228b 100644 --- a/beacon_node/beacon_chain/src/pre_finalization_cache.rs +++ b/beacon_node/beacon_chain/src/pre_finalization_cache.rs @@ -73,7 +73,7 @@ impl BeaconChain { } // 2. Check on disk. - if self.store.get_blinded_block(&block_root)?.is_some() { + if self.store.get_blinded_block(&block_root, None)?.is_some() { cache.block_roots.put(block_root, ()); return Ok(true); } diff --git a/beacon_node/beacon_chain/src/schema_change.rs b/beacon_node/beacon_chain/src/schema_change.rs index 63eb72c43ab..5024b2ab822 100644 --- a/beacon_node/beacon_chain/src/schema_change.rs +++ b/beacon_node/beacon_chain/src/schema_change.rs @@ -2,6 +2,7 @@ mod migration_schema_v17; mod migration_schema_v18; mod migration_schema_v19; +mod migration_schema_v20; use crate::beacon_chain::BeaconChainTypes; use crate::types::ChainSpec; @@ -24,6 +25,14 @@ pub fn migrate_schema( match (from, to) { // Migrating from the current schema version to itself is always OK, a no-op. (_, _) if from == to && to == CURRENT_SCHEMA_VERSION => Ok(()), + // Upgrade for tree-states database changes. + (SchemaVersion(12), SchemaVersion(20)) => { + migration_schema_v20::upgrade_to_v20::(db, log) + } + // Downgrade for tree-states database changes. + (SchemaVersion(20), SchemaVersion(12)) => { + migration_schema_v20::downgrade_from_v20::(db, log) + } // Upgrade across multiple versions by recursively migrating one step at a time. (_, _) if from.as_u64() + 1 < to.as_u64() => { let next = SchemaVersion(from.as_u64() + 1); diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v18.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v18.rs index 04a9da84128..8f67f8a2c6a 100644 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v18.rs +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v18.rs @@ -17,7 +17,7 @@ fn get_slot_clock( log: &Logger, ) -> Result, Error> { let spec = db.get_chain_spec(); - let Some(genesis_block) = db.get_blinded_block(&Hash256::zero())? else { + let Some(genesis_block) = db.get_blinded_block(&Hash256::zero(), Some(Slot::new(0)))? else { error!(log, "Missing genesis block"); return Ok(None); }; @@ -46,10 +46,6 @@ pub fn upgrade_to_v18( db: Arc>, log: Logger, ) -> Result, Error> { - db.heal_freezer_block_roots_at_split()?; - db.heal_freezer_block_roots_at_genesis()?; - info!(log, "Healed freezer block roots"); - // No-op, even if Deneb has already occurred. The database is probably borked in this case, but // *maybe* the fork recovery will revert the minority fork and succeed. if let Some(deneb_fork_epoch) = db.get_chain_spec().deneb_fork_epoch { diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v20.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v20.rs new file mode 100644 index 00000000000..8a6f93f535b --- /dev/null +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v20.rs @@ -0,0 +1,273 @@ +// FIXME(sproul): implement migration +#![allow(unused)] + +use crate::{ + beacon_chain::{BeaconChainTypes, BEACON_CHAIN_DB_KEY}, + persisted_beacon_chain::PersistedBeaconChain, +}; +use slog::{debug, info, Logger}; +use std::collections::HashMap; +use std::sync::Arc; +use store::{ + get_key_for_col, + hot_cold_store::{HotColdDBError, HotStateSummaryV1, HotStateSummaryV10}, + metadata::SchemaVersion, + DBColumn, Error, HotColdDB, KeyValueStoreOp, StoreItem, +}; +use types::{milhouse::Diff, BeaconState, EthSpec, Hash256, Slot}; + +fn get_summary_v1( + db: &HotColdDB, + state_root: Hash256, +) -> Result { + db.get_item(&state_root)? + .ok_or_else(|| HotColdDBError::MissingHotStateSummary(state_root).into()) +} + +fn get_state_by_replay( + db: &HotColdDB, + state_root: Hash256, +) -> Result, Error> { + /* FIXME(sproul): fix migration + // Load state summary. + let HotStateSummaryV1 { + slot, + latest_block_root, + epoch_boundary_state_root, + } = get_summary_v1::(db, state_root)?; + + // Load full state from the epoch boundary. + let (epoch_boundary_state, _) = db.load_hot_state_full(&epoch_boundary_state_root)?; + + // Replay blocks to reach the target state. + let blocks = db.load_blocks_to_replay(epoch_boundary_state.slot(), slot, latest_block_root)?; + + db.replay_blocks(epoch_boundary_state, blocks, slot, std::iter::empty(), None) + */ + panic!() +} + +pub fn upgrade_to_v20( + db: Arc>, + log: Logger, +) -> Result<(), Error> { + /* FIXME(sproul): fix this + let mut ops = vec![]; + + // Translate hot state summaries to new format: + // - Rewrite epoch boundary root to previous epoch boundary root. + // - Add previous state root. + // + // Replace most epoch boundary states by diffs. + let split = db.get_split_info(); + let finalized_slot = split.slot; + let finalized_state_root = split.state_root; + let slots_per_epoch = T::EthSpec::slots_per_epoch(); + + let ssz_head_tracker = db + .get_item::(&BEACON_CHAIN_DB_KEY)? + .ok_or(Error::MissingPersistedBeaconChain)? + .ssz_head_tracker; + + let mut new_summaries = HashMap::new(); + + for (head_block_root, head_state_slot) in ssz_head_tracker + .roots + .into_iter() + .zip(ssz_head_tracker.slots) + { + let block = db + .get_blinded_block(&head_block_root, Some(head_state_slot))? + .ok_or(Error::BlockNotFound(head_block_root))?; + let head_state_root = block.state_root(); + + debug!( + log, + "Re-writing state summaries for head"; + "block_root" => ?head_block_root, + "state_root" => ?head_state_root, + "slot" => head_state_slot + ); + let mut current_state = get_state_by_replay::(&db, head_state_root)?; + let mut current_state_root = head_state_root; + + new_summaries.insert( + head_state_root, + HotStateSummaryV10::new(&head_state_root, ¤t_state)?, + ); + + for slot in (finalized_slot.as_u64()..current_state.slot().as_u64()) + .rev() + .map(Slot::new) + { + let epoch_boundary_slot = (slot - 1) / slots_per_epoch * slots_per_epoch; + + let state_root = *current_state.get_state_root(slot)?; + let latest_block_root = *current_state.get_block_root(slot)?; + let prev_state_root = *current_state.get_state_root(slot - 1)?; + let epoch_boundary_state_root = *current_state.get_state_root(epoch_boundary_slot)?; + + // FIXME(sproul): rename V10 variant + let summary = HotStateSummaryV10 { + slot, + latest_block_root, + epoch_boundary_state_root, + prev_state_root, + }; + + // Stage the updated state summary for storage. + // If we've reached a known segment of chain then we can stop and continue to the next + // head. + if new_summaries.insert(state_root, summary).is_some() { + debug!( + log, + "Finished migrating chain tip"; + "head_block_root" => ?head_block_root, + "reason" => format!("reached common state {:?}", state_root), + ); + break; + } else { + debug!( + log, + "Rewriting hot state summary"; + "state_root" => ?state_root, + "slot" => slot, + "epoch_boundary_state_root" => ?epoch_boundary_state_root, + "prev_state_root" => ?prev_state_root, + ); + } + + // If the state reached is an epoch boundary state, then load it so that we can continue + // backtracking from it and storing diffs. + if slot % slots_per_epoch == 0 { + debug!( + log, + "Loading epoch boundary state"; + "state_root" => ?state_root, + "slot" => slot, + ); + let backtrack_state = get_state_by_replay::(&db, state_root)?; + + // If the current state is an epoch boundary state too then we might need to convert + // it to a diff relative to the backtrack state. + if current_state.slot() % slots_per_epoch == 0 + && !db.is_stored_as_full_state(current_state_root, current_state.slot())? + { + debug!( + log, + "Converting full state to diff"; + "prev_state_root" => ?state_root, + "state_root" => ?current_state_root, + "slot" => current_state.slot(), + ); + + let diff = BeaconStateDiff::compute_diff(&backtrack_state, ¤t_state)?; + + // Store diff. + ops.push(db.state_diff_as_kv_store_op(¤t_state_root, &diff)?); + + // Delete full state. + let state_key = get_key_for_col( + DBColumn::BeaconState.into(), + current_state_root.as_bytes(), + ); + ops.push(KeyValueStoreOp::DeleteKey(state_key)); + } + + current_state = backtrack_state; + current_state_root = state_root; + } + + if slot == finalized_slot { + // FIXME(sproul): remove assert + assert_eq!(finalized_state_root, state_root); + debug!( + log, + "Finished migrating chain tip"; + "head_block_root" => ?head_block_root, + "reason" => format!("reached finalized state {:?}", finalized_state_root), + ); + break; + } + } + } + + ops.reserve(new_summaries.len()); + for (state_root, summary) in new_summaries { + ops.push(summary.as_kv_store_op(state_root)?); + } + + db.store_schema_version_atomically(SchemaVersion(20), ops) + */ + panic!() +} + +pub fn downgrade_from_v20( + db: Arc>, + log: Logger, +) -> Result<(), Error> { + /* FIXME(sproul): broken + let slots_per_epoch = T::EthSpec::slots_per_epoch(); + + // Iterate hot state summaries and re-write them so that: + // + // - The previous state root is removed. + // - The epoch boundary root points to the most recent epoch boundary root rather than the + // previous epoch boundary root. We exploit the fact that they are the same except when the slot + // of the summary itself lies on an epoch boundary. + let mut summaries = db + .iter_hot_state_summaries() + .collect::, _>>()?; + + // Sort by slot ascending so that the state cache has a better chance of hitting. + summaries.sort_unstable_by(|(_, summ1), (_, summ2)| summ1.slot.cmp(&summ2.slot)); + + info!(log, "Rewriting {} state summaries", summaries.len()); + + let mut ops = Vec::with_capacity(summaries.len()); + + for (state_root, summary) in summaries { + let epoch_boundary_state_root = if summary.slot % slots_per_epoch == 0 { + info!( + log, + "Ensuring state is stored as full state"; + "state_root" => ?state_root, + "slot" => summary.slot + ); + let state = db + .get_hot_state(&state_root)? + .ok_or(Error::MissingState(state_root))?; + + // Delete state diff. + let state_key = + get_key_for_col(DBColumn::BeaconStateDiff.into(), state_root.as_bytes()); + ops.push(KeyValueStoreOp::DeleteKey(state_key)); + + // Store full state. + db.store_full_state_in_batch(&state_root, &state, &mut ops)?; + + // This state root is its own most recent epoch boundary root. + state_root + } else { + summary.epoch_boundary_state_root + }; + let summary_v1 = HotStateSummaryV1 { + slot: summary.slot, + latest_block_root: summary.latest_block_root, + epoch_boundary_state_root, + }; + debug!( + log, + "Rewriting state summary"; + "slot" => summary_v1.slot, + "latest_block_root" => ?summary_v1.latest_block_root, + "epoch_boundary_state_root" => ?summary_v1.epoch_boundary_state_root, + ); + + ops.push(summary_v1.as_kv_store_op(state_root)?); + } + + db.store_schema_version_atomically(SchemaVersion(8), ops) + */ + panic!() +} diff --git a/beacon_node/beacon_chain/src/shuffling_cache.rs b/beacon_node/beacon_chain/src/shuffling_cache.rs index b3de6f91c92..7db4e082142 100644 --- a/beacon_node/beacon_chain/src/shuffling_cache.rs +++ b/beacon_node/beacon_chain/src/shuffling_cache.rs @@ -1,240 +1,53 @@ -use std::collections::HashMap; -use std::sync::Arc; - -use itertools::Itertools; +use promise_cache::{PromiseCache, Protect}; use slog::{debug, Logger}; - -use oneshot_broadcast::{oneshot, Receiver, Sender}; use types::{ beacon_state::CommitteeCache, AttestationShufflingId, BeaconState, Epoch, EthSpec, Hash256, RelativeEpoch, }; -use crate::{metrics, BeaconChainError}; - /// The size of the cache that stores committee caches for quicker verification. /// /// Each entry should be `8 + 800,000 = 800,008` bytes in size with 100k validators. (8-byte hash + /// 100k indices). Therefore, this cache should be approx `16 * 800,008 = 12.8 MB`. (Note: this /// ignores a few extra bytes in the caches that should be insignificant compared to the indices). -pub const DEFAULT_CACHE_SIZE: usize = 16; - -/// The maximum number of concurrent committee cache "promises" that can be issued. In effect, this -/// limits the number of concurrent states that can be loaded into memory for the committee cache. -/// This prevents excessive memory usage at the cost of rejecting some attestations. +/// +/// The cache size also determines the maximum number of concurrent committee cache "promises" that +/// can be issued. In effect, this limits the number of concurrent states that can be loaded into +/// memory for the committee cache. This prevents excessive memory usage at the cost of rejecting +/// some attestations. /// /// We set this value to 2 since states can be quite large and have a significant impact on memory /// usage. A healthy network cannot have more than a few committee caches and those caches should /// always be inserted during block import. Unstable networks with a high degree of forking might /// see some attestations dropped due to this concurrency limit, however I propose that this is /// better than low-resource nodes going OOM. -const MAX_CONCURRENT_PROMISES: usize = 2; - -#[derive(Clone)] -pub enum CacheItem { - /// A committee. - Committee(Arc), - /// A promise for a future committee. - Promise(Receiver>), -} - -impl CacheItem { - pub fn is_promise(&self) -> bool { - matches!(self, CacheItem::Promise(_)) - } - - pub fn wait(self) -> Result, BeaconChainError> { - match self { - CacheItem::Committee(cache) => Ok(cache), - CacheItem::Promise(receiver) => receiver - .recv() - .map_err(BeaconChainError::CommitteePromiseFailed), - } - } -} - -/// Provides a cache for `CommitteeCache`. -/// -/// It has been named `ShufflingCache` because `CommitteeCacheCache` is a bit weird and looks like -/// a find/replace error. -pub struct ShufflingCache { - cache: HashMap, - cache_size: usize, - head_shuffling_ids: BlockShufflingIds, - logger: Logger, -} - -impl ShufflingCache { - pub fn new(cache_size: usize, head_shuffling_ids: BlockShufflingIds, logger: Logger) -> Self { - Self { - cache: HashMap::new(), - cache_size, - head_shuffling_ids, - logger, - } - } - - pub fn get(&mut self, key: &AttestationShufflingId) -> Option { - match self.cache.get(key) { - // The cache contained the committee cache, return it. - item @ Some(CacheItem::Committee(_)) => { - metrics::inc_counter(&metrics::SHUFFLING_CACHE_HITS); - item.cloned() - } - // The cache contains a promise for the committee cache. Check to see if the promise has - // already been resolved, without waiting for it. - item @ Some(CacheItem::Promise(receiver)) => match receiver.try_recv() { - // The promise has already been resolved. Replace the entry in the cache with a - // `Committee` entry and then return the committee. - Ok(Some(committee)) => { - metrics::inc_counter(&metrics::SHUFFLING_CACHE_PROMISE_HITS); - metrics::inc_counter(&metrics::SHUFFLING_CACHE_HITS); - let ready = CacheItem::Committee(committee); - self.insert_cache_item(key.clone(), ready.clone()); - Some(ready) - } - // The promise has not yet been resolved. Return the promise so the caller can await - // it. - Ok(None) => { - metrics::inc_counter(&metrics::SHUFFLING_CACHE_PROMISE_HITS); - metrics::inc_counter(&metrics::SHUFFLING_CACHE_HITS); - item.cloned() - } - // The sender has been dropped without sending a committee. There was most likely an - // error computing the committee cache. Drop the key from the cache and return - // `None` so the caller can recompute the committee. - // - // It's worth noting that this is the only place where we removed unresolved - // promises from the cache. This means unresolved promises will only be removed if - // we try to access them again. This is OK, since the promises don't consume much - // memory. We expect that *all* promises should be resolved, unless there is a - // programming or database error. - Err(oneshot_broadcast::Error::SenderDropped) => { - metrics::inc_counter(&metrics::SHUFFLING_CACHE_PROMISE_FAILS); - metrics::inc_counter(&metrics::SHUFFLING_CACHE_MISSES); - self.cache.remove(key); - None - } - }, - // The cache does not have this committee and it's not already promised to be computed. - None => { - metrics::inc_counter(&metrics::SHUFFLING_CACHE_MISSES); - None - } - } - } - - pub fn contains(&self, key: &AttestationShufflingId) -> bool { - self.cache.contains_key(key) - } - - pub fn insert_committee_cache( - &mut self, - key: AttestationShufflingId, - committee_cache: &C, - ) { - if self - .cache - .get(&key) - // Replace the committee if it's not present or if it's a promise. A bird in the hand is - // worth two in the promise-bush! - .map_or(true, CacheItem::is_promise) - { - self.insert_cache_item( - key, - CacheItem::Committee(committee_cache.to_arc_committee_cache()), - ); - } - } - - /// Prunes the cache first before inserting a new cache item. - fn insert_cache_item(&mut self, key: AttestationShufflingId, cache_item: CacheItem) { - self.prune_cache(); - self.cache.insert(key, cache_item); - } - - /// Prunes the `cache` to keep the size below the `cache_size` limit, based on the following - /// preferences: - /// - Entries from more recent epochs are preferred over older ones. - /// - Entries with shuffling ids matching the head's previous, current, and future epochs must - /// not be pruned. - fn prune_cache(&mut self) { - let target_cache_size = self.cache_size.saturating_sub(1); - if let Some(prune_count) = self.cache.len().checked_sub(target_cache_size) { - let shuffling_ids_to_prune = self - .cache - .keys() - .sorted_by_key(|key| key.shuffling_epoch) - .filter(|shuffling_id| { - Some(shuffling_id) - != self - .head_shuffling_ids - .id_for_epoch(shuffling_id.shuffling_epoch) - .as_ref() - .as_ref() - }) - .take(prune_count) - .cloned() - .collect::>(); - - for shuffling_id in shuffling_ids_to_prune.iter() { - debug!( - self.logger, - "Removing old shuffling from cache"; - "shuffling_epoch" => shuffling_id.shuffling_epoch, - "shuffling_decision_block" => ?shuffling_id.shuffling_decision_block - ); - self.cache.remove(shuffling_id); - } - } - } +pub const DEFAULT_CACHE_SIZE: usize = 16; - pub fn create_promise( - &mut self, - key: AttestationShufflingId, - ) -> Result>, BeaconChainError> { - let num_active_promises = self - .cache - .iter() - .filter(|(_, item)| item.is_promise()) - .count(); - if num_active_promises >= MAX_CONCURRENT_PROMISES { - return Err(BeaconChainError::MaxCommitteePromises(num_active_promises)); - } +impl Protect for BlockShufflingIds { + type SortKey = Epoch; - let (sender, receiver) = oneshot(); - self.insert_cache_item(key, CacheItem::Promise(receiver)); - Ok(sender) + fn sort_key(&self, k: &AttestationShufflingId) -> Epoch { + k.shuffling_epoch } - /// Inform the cache that the shuffling decision roots for the head has changed. - /// - /// The shufflings for the head's previous, current, and future epochs will never be ejected from - /// the cache during `Self::insert_cache_item`. - pub fn update_head_shuffling_ids(&mut self, head_shuffling_ids: BlockShufflingIds) { - self.head_shuffling_ids = head_shuffling_ids; + fn protect_from_eviction(&self, shuffling_id: &AttestationShufflingId) -> bool { + Some(shuffling_id) == self.id_for_epoch(shuffling_id.shuffling_epoch).as_ref() } -} -/// A helper trait to allow lazy-cloning of the committee cache when inserting into the cache. -pub trait ToArcCommitteeCache { - fn to_arc_committee_cache(&self) -> Arc; -} - -impl ToArcCommitteeCache for CommitteeCache { - fn to_arc_committee_cache(&self) -> Arc { - Arc::new(self.clone()) + fn notify_eviction(&self, shuffling_id: &AttestationShufflingId, logger: &Logger) { + debug!( + logger, + "Removing old shuffling from cache"; + "shuffling_epoch" => shuffling_id.shuffling_epoch, + "shuffling_decision_block" => ?shuffling_id.shuffling_decision_block + ); } } -impl ToArcCommitteeCache for Arc { - fn to_arc_committee_cache(&self) -> Arc { - self.clone() - } -} +pub type ShufflingCache = PromiseCache; /// Contains the shuffling IDs for a beacon block. -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct BlockShufflingIds { pub current: AttestationShufflingId, pub next: AttestationShufflingId, @@ -294,13 +107,13 @@ impl BlockShufflingIds { #[cfg(not(debug_assertions))] #[cfg(test)] mod test { + use super::*; + use crate::test_utils::EphemeralHarnessType; + use promise_cache::{CacheItem, PromiseCacheError}; + use std::sync::Arc; use task_executor::test_utils::null_logger; use types::*; - use crate::test_utils::EphemeralHarnessType; - - use super::*; - type E = MinimalEthSpec; type TestBeaconChainType = EphemeralHarnessType; type BeaconChainHarness = crate::test_utils::BeaconChainHarness; @@ -339,7 +152,7 @@ mod test { .clone(); let committee_b = state.committee_cache(RelativeEpoch::Next).unwrap().clone(); assert!(committee_a != committee_b); - (Arc::new(committee_a), Arc::new(committee_b)) + (committee_a, committee_b) } /// Builds a deterministic but incoherent shuffling ID from a `u64`. @@ -372,10 +185,10 @@ mod test { // Ensure the promise has been resolved. let item = cache.get(&id_a).unwrap(); assert!( - matches!(item, CacheItem::Committee(committee) if committee == committee_a), + matches!(item, CacheItem::Complete(committee) if committee == committee_a), "the promise should be resolved" ); - assert_eq!(cache.cache.len(), 1, "the cache should have one entry"); + assert_eq!(cache.len(), 1, "the cache should have one entry"); } #[test] @@ -399,7 +212,7 @@ mod test { // Ensure the key now indicates an empty slot. assert!(cache.get(&id_a).is_none(), "the slot should be empty"); - assert!(cache.cache.is_empty(), "the cache should be empty"); + assert!(cache.is_empty(), "the cache should be empty"); } #[test] @@ -433,7 +246,7 @@ mod test { // Ensure promise A has been resolved. let item = cache.get(&id_a).unwrap(); assert!( - matches!(item, CacheItem::Committee(committee) if committee == committee_a), + matches!(item, CacheItem::Complete(committee) if committee == committee_a), "promise A should be resolved" ); @@ -442,41 +255,40 @@ mod test { // Ensure promise B has been resolved. let item = cache.get(&id_b).unwrap(); assert!( - matches!(item, CacheItem::Committee(committee) if committee == committee_b), + matches!(item, CacheItem::Complete(committee) if committee == committee_b), "promise B should be resolved" ); // Check both entries again. assert!( - matches!(cache.get(&id_a).unwrap(), CacheItem::Committee(committee) if committee == committee_a), + matches!(cache.get(&id_a).unwrap(), CacheItem::Complete(committee) if committee == committee_a), "promise A should remain resolved" ); assert!( - matches!(cache.get(&id_b).unwrap(), CacheItem::Committee(committee) if committee == committee_b), + matches!(cache.get(&id_b).unwrap(), CacheItem::Complete(committee) if committee == committee_b), "promise B should remain resolved" ); - assert_eq!(cache.cache.len(), 2, "the cache should have two entries"); + assert_eq!(cache.len(), 2, "the cache should have two entries"); } #[test] fn too_many_promises() { let mut cache = new_shuffling_cache(); - for i in 0..MAX_CONCURRENT_PROMISES { + for i in 0..cache.max_concurrent_promises() { cache.create_promise(shuffling_id(i as u64)).unwrap(); } // Ensure that the next promise returns an error. It is important for the application to // dump his ass when he can't keep his promises, you're a queen and you deserve better. assert!(matches!( - cache.create_promise(shuffling_id(MAX_CONCURRENT_PROMISES as u64)), - Err(BeaconChainError::MaxCommitteePromises( - MAX_CONCURRENT_PROMISES - )) + cache.create_promise(shuffling_id(cache.max_concurrent_promises() as u64)), + Err(PromiseCacheError::MaxConcurrentPromises(n)) + if n == cache.max_concurrent_promises() )); assert_eq!( - cache.cache.len(), - MAX_CONCURRENT_PROMISES, + cache.len(), + cache.max_concurrent_promises(), "the cache should have two entries" ); } @@ -486,9 +298,9 @@ mod test { let mut cache = new_shuffling_cache(); let id_a = shuffling_id(1); let committee_cache_a = Arc::new(CommitteeCache::default()); - cache.insert_committee_cache(id_a.clone(), &committee_cache_a); + cache.insert_value(id_a.clone(), &committee_cache_a); assert!( - matches!(cache.get(&id_a).unwrap(), CacheItem::Committee(committee_cache) if committee_cache == committee_cache_a), + matches!(cache.get(&id_a).unwrap(), CacheItem::Complete(committee_cache) if committee_cache == committee_cache_a), "should insert committee cache" ); } @@ -501,7 +313,7 @@ mod test { .collect::>(); for (shuffling_id, committee_cache) in shuffling_id_and_committee_caches.iter() { - cache.insert_committee_cache(shuffling_id.clone(), committee_cache); + cache.insert_value(shuffling_id.clone(), committee_cache); } for i in 1..(TEST_CACHE_SIZE + 1) { @@ -515,11 +327,7 @@ mod test { !cache.contains(&shuffling_id_and_committee_caches.get(0).unwrap().0), "should not contain oldest epoch shuffling id" ); - assert_eq!( - cache.cache.len(), - cache.cache_size, - "should limit cache size" - ); + assert_eq!(cache.len(), TEST_CACHE_SIZE, "should limit cache size"); } #[test] @@ -534,7 +342,7 @@ mod test { shuffling_epoch: (current_epoch + 1).into(), shuffling_decision_block: Hash256::from_low_u64_be(current_epoch + i as u64), }; - cache.insert_committee_cache(shuffling_id, &committee_cache); + cache.insert_value(shuffling_id, &committee_cache); } // Now, update the head shuffling ids @@ -544,12 +352,12 @@ mod test { previous: Some(shuffling_id(current_epoch - 1)), block_root: Hash256::from_low_u64_le(42), }; - cache.update_head_shuffling_ids(head_shuffling_ids.clone()); + cache.update_protector(head_shuffling_ids.clone()); // Insert head state shuffling ids. Should not be overridden by other shuffling ids. - cache.insert_committee_cache(head_shuffling_ids.current.clone(), &committee_cache); - cache.insert_committee_cache(head_shuffling_ids.next.clone(), &committee_cache); - cache.insert_committee_cache( + cache.insert_value(head_shuffling_ids.current.clone(), &committee_cache); + cache.insert_value(head_shuffling_ids.next.clone(), &committee_cache); + cache.insert_value( head_shuffling_ids.previous.clone().unwrap(), &committee_cache, ); @@ -560,7 +368,7 @@ mod test { shuffling_epoch: Epoch::from(i), shuffling_decision_block: Hash256::from_low_u64_be(i as u64), }; - cache.insert_committee_cache(shuffling_id, &committee_cache); + cache.insert_value(shuffling_id, &committee_cache); } assert!( @@ -575,10 +383,6 @@ mod test { cache.contains(&head_shuffling_ids.previous.unwrap()), "should retain head shuffling id for previous epoch." ); - assert_eq!( - cache.cache.len(), - cache.cache_size, - "should limit cache size" - ); + assert_eq!(cache.len(), TEST_CACHE_SIZE, "should limit cache size"); } } diff --git a/beacon_node/beacon_chain/src/state_advance_timer.rs b/beacon_node/beacon_chain/src/state_advance_timer.rs index 39d35f81113..2049502318c 100644 --- a/beacon_node/beacon_chain/src/state_advance_timer.rs +++ b/beacon_node/beacon_chain/src/state_advance_timer.rs @@ -15,9 +15,7 @@ //! 2. There's a possibility that the head block is never built upon, causing wasted CPU cycles. use crate::validator_monitor::HISTORIC_EPOCHS as VALIDATOR_MONITOR_HISTORIC_EPOCHS; use crate::{ - beacon_chain::{ATTESTATION_CACHE_LOCK_TIMEOUT, BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT}, - chain_config::FORK_CHOICE_LOOKAHEAD_FACTOR, - snapshot_cache::StateAdvance, + beacon_chain::ATTESTATION_CACHE_LOCK_TIMEOUT, chain_config::FORK_CHOICE_LOOKAHEAD_FACTOR, BeaconChain, BeaconChainError, BeaconChainTypes, }; use slog::{debug, error, warn, Logger}; @@ -29,7 +27,7 @@ use std::sync::{ }; use task_executor::TaskExecutor; use tokio::time::{sleep, sleep_until, Instant}; -use types::{AttestationShufflingId, EthSpec, Hash256, RelativeEpoch, Slot}; +use types::{AttestationShufflingId, BeaconStateError, EthSpec, Hash256, RelativeEpoch, Slot}; /// If the head slot is more than `MAX_ADVANCE_DISTANCE` from the current slot, then don't perform /// the state advancement. @@ -45,13 +43,12 @@ const MAX_ADVANCE_DISTANCE: u64 = 4; /// impact whilst having 8 epochs without a block is a comfortable grace period. const MAX_FORK_CHOICE_DISTANCE: u64 = 256; -/// Drop any unused block production state cache after this many slots. -const MAX_BLOCK_PRODUCTION_CACHE_DISTANCE: u64 = 4; - #[derive(Debug)] enum Error { BeaconChain(BeaconChainError), // We don't use the inner value directly, but it's used in the Debug impl. + BeaconState(#[allow(dead_code)] BeaconStateError), + Store(#[allow(dead_code)] store::Error), HeadMissingFromSnapshotCache(#[allow(dead_code)] Hash256), MaxDistanceExceeded { current_slot: Slot, @@ -62,7 +59,7 @@ enum Error { }, BadStateSlot { _state_slot: Slot, - _block_slot: Slot, + _current_slot: Slot, }, } @@ -72,6 +69,18 @@ impl From for Error { } } +impl From for Error { + fn from(e: BeaconStateError) -> Self { + Self::BeaconState(e) + } +} + +impl From for Error { + fn from(e: store::Error) -> Self { + Self::Store(e) + } +} + /// Provides a simple thread-safe lock to be used for task co-ordination. Practically equivalent to /// `Mutex<()>`. #[derive(Clone)] @@ -231,7 +240,7 @@ async fn state_advance_timer( // Prepare proposers so that the node can send payload attributes in the case where // it decides to abandon a proposer boost re-org. - let proposer_head = beacon_chain + beacon_chain .prepare_beacon_proposer(current_slot) .await .unwrap_or_else(|e| { @@ -248,56 +257,6 @@ async fn state_advance_timer( // in `ForkChoiceSignalTx`. beacon_chain.task_executor.clone().spawn_blocking( move || { - // If we're proposing, clone the head state preemptively so that it isn't on - // the hot path of proposing. We can delete this once we have tree-states. - if let Some(proposer_head) = proposer_head { - let mut cache = beacon_chain.block_production_state.lock(); - - // Avoid holding two states in memory. It's OK to hold the lock because - // we always lock the block production cache before the snapshot cache - // and we prefer for block production to wait for the block production - // cache if a clone is in-progress. - if cache - .as_ref() - .map_or(false, |(cached_head, _)| *cached_head != proposer_head) - { - drop(cache.take()); - } - if let Some(proposer_state) = beacon_chain - .snapshot_cache - .try_read_for(BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT) - .and_then(|snapshot_cache| { - snapshot_cache.get_state_for_block_production(proposer_head) - }) - { - *cache = Some((proposer_head, proposer_state)); - debug!( - log, - "Cloned state ready for block production"; - "head_block_root" => ?proposer_head, - "slot" => next_slot - ); - } else { - warn!( - log, - "Block production state missing from snapshot cache"; - "head_block_root" => ?proposer_head, - "slot" => next_slot - ); - } - } else { - // If we aren't proposing, drop any old block production cache to save - // memory. - let mut cache = beacon_chain.block_production_state.lock(); - if let Some((_, state)) = &*cache { - if state.pre_state.slot() + MAX_BLOCK_PRODUCTION_CACHE_DISTANCE - <= current_slot - { - drop(cache.take()); - } - } - } - // Signal block proposal for the next slot (if it happens to be waiting). if let Some(tx) = &beacon_chain.fork_choice_signal_tx { if let Err(e) = tx.notify_fork_choice_complete(next_slot) { @@ -318,11 +277,6 @@ async fn state_advance_timer( } } -/// Reads the `snapshot_cache` from the `beacon_chain` and attempts to take a clone of the -/// `BeaconState` of the head block. If it obtains this clone, the state will be advanced a single -/// slot then placed back in the `snapshot_cache` to be used for block verification. -/// -/// See the module-level documentation for rationale. fn advance_head( beacon_chain: &Arc>, log: &Logger, @@ -345,46 +299,38 @@ fn advance_head( } } - let head_root = beacon_chain.head_beacon_block_root(); - - let (head_slot, head_state_root, mut state) = match beacon_chain - .snapshot_cache - .try_write_for(BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT) - .ok_or(BeaconChainError::SnapshotCacheLockTimeout)? - .get_for_state_advance(head_root) - { - StateAdvance::AlreadyAdvanced => { - return Err(Error::StateAlreadyAdvanced { - block_root: head_root, - }) - } - StateAdvance::BlockNotFound => return Err(Error::HeadMissingFromSnapshotCache(head_root)), - StateAdvance::State { - state, - state_root, - block_slot, - } => (block_slot, state_root, *state), + let (head_block_root, head_block_state_root) = { + let snapshot = beacon_chain.head_snapshot(); + (snapshot.beacon_block_root, snapshot.beacon_state_root()) }; - let initial_slot = state.slot(); - let initial_epoch = state.current_epoch(); + let (head_state_root, mut state) = beacon_chain + .store + .get_advanced_hot_state(head_block_root, current_slot, head_block_state_root)? + .ok_or(Error::HeadMissingFromSnapshotCache(head_block_root))?; - let state_root = if state.slot() == head_slot { - Some(head_state_root) - } else { + if state.slot() == current_slot + 1 { + return Err(Error::StateAlreadyAdvanced { + block_root: head_block_root, + }); + } else if state.slot() != current_slot { // Protect against advancing a state more than a single slot. // // Advancing more than one slot without storing the intermediate state would corrupt the // database. Future works might store temporary, intermediate states inside this function. return Err(Error::BadStateSlot { - _block_slot: head_slot, _state_slot: state.slot(), + _current_slot: current_slot, }); - }; + } + + let initial_slot = state.slot(); + let initial_epoch = state.current_epoch(); // Advance the state a single slot. - if let Some(summary) = per_slot_processing(&mut state, state_root, &beacon_chain.spec) - .map_err(BeaconChainError::from)? + if let Some(summary) = + per_slot_processing(&mut state, Some(head_state_root), &beacon_chain.spec) + .map_err(BeaconChainError::from)? { // Expose Prometheus metrics. if let Err(e) = summary.observe_metrics() { @@ -418,7 +364,7 @@ fn advance_head( debug!( log, "Advanced head state one slot"; - "head_root" => ?head_root, + "head_block_root" => ?head_block_root, "state_slot" => state.slot(), "current_slot" => current_slot, ); @@ -437,14 +383,14 @@ fn advance_head( if initial_epoch < state.current_epoch() { // Update the proposer cache. // - // We supply the `head_root` as the decision block since the prior `if` statement guarantees + // We supply the `head_block_root` as the decision block since the prior `if` statement guarantees // the head root is the latest block from the prior epoch. beacon_chain .beacon_proposer_cache .lock() .insert( state.current_epoch(), - head_root, + head_block_root, state .get_beacon_proposer_indices(&beacon_chain.spec) .map_err(BeaconChainError::from)?, @@ -453,8 +399,9 @@ fn advance_head( .map_err(BeaconChainError::from)?; // Update the attester cache. - let shuffling_id = AttestationShufflingId::new(head_root, &state, RelativeEpoch::Next) - .map_err(BeaconChainError::from)?; + let shuffling_id = + AttestationShufflingId::new(head_block_root, &state, RelativeEpoch::Next) + .map_err(BeaconChainError::from)?; let committee_cache = state .committee_cache(RelativeEpoch::Next) .map_err(BeaconChainError::from)?; @@ -462,12 +409,12 @@ fn advance_head( .shuffling_cache .try_write_for(ATTESTATION_CACHE_LOCK_TIMEOUT) .ok_or(BeaconChainError::AttestationCacheLockTimeout)? - .insert_committee_cache(shuffling_id.clone(), committee_cache); + .insert_value(shuffling_id.clone(), committee_cache); debug!( log, "Primed proposer and attester caches"; - "head_root" => ?head_root, + "head_block_root" => ?head_block_root, "next_epoch_shuffling_root" => ?shuffling_id.shuffling_decision_block, "state_epoch" => state.current_epoch(), "current_epoch" => current_slot.epoch(T::EthSpec::slots_per_epoch()), @@ -477,44 +424,19 @@ fn advance_head( // Apply the state to the attester cache, if the cache deems it interesting. beacon_chain .attester_cache - .maybe_cache_state(&state, head_root, &beacon_chain.spec) + .maybe_cache_state(&state, head_block_root, &beacon_chain.spec) .map_err(BeaconChainError::from)?; let final_slot = state.slot(); - // Insert the advanced state back into the snapshot cache. - beacon_chain - .snapshot_cache - .try_write_for(BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT) - .ok_or(BeaconChainError::SnapshotCacheLockTimeout)? - .update_pre_state(head_root, state) - .ok_or(Error::HeadMissingFromSnapshotCache(head_root))?; - - // If we have moved into the next slot whilst processing the state then this function is going - // to become ineffective and likely become a hindrance as we're stealing the tree hash cache - // from the snapshot cache (which may force the next block to rebuild a new one). - // - // If this warning occurs very frequently on well-resourced machines then we should consider - // starting it earlier in the slot. Otherwise, it's a good indication that the machine is too - // slow/overloaded and will be useful information for the user. - let starting_slot = current_slot; - let current_slot = beacon_chain.slot()?; - if starting_slot < current_slot { - warn!( - log, - "State advance too slow"; - "head_root" => %head_root, - "advanced_slot" => final_slot, - "current_slot" => current_slot, - "starting_slot" => starting_slot, - "msg" => "system resources may be overloaded", - ); - } + // Write the advanced state to the database. + let advanced_state_root = state.update_tree_hash_cache()?; + beacon_chain.store.put_state(&advanced_state_root, &state)?; debug!( log, "Completed state advance"; - "head_root" => ?head_root, + "head_block_root" => ?head_block_root, "advanced_slot" => final_slot, "initial_slot" => initial_slot, ); diff --git a/beacon_node/beacon_chain/src/sync_committee_rewards.rs b/beacon_node/beacon_chain/src/sync_committee_rewards.rs index 2221aa1d5eb..9b35cff9432 100644 --- a/beacon_node/beacon_chain/src/sync_committee_rewards.rs +++ b/beacon_node/beacon_chain/src/sync_committee_rewards.rs @@ -38,9 +38,26 @@ impl BeaconChain { })?; let mut balances = HashMap::::new(); + for &validator_index in &sync_committee_indices { + balances.insert( + validator_index, + *state + .balances() + .get(validator_index) + .ok_or(BeaconChainError::SyncCommitteeRewardsSyncError)?, + ); + } + + let proposer_index = block.proposer_index() as usize; + balances.insert( + proposer_index, + *state + .balances() + .get(proposer_index) + .ok_or(BeaconChainError::SyncCommitteeRewardsSyncError)?, + ); let mut total_proposer_rewards = 0; - let proposer_index = state.get_beacon_proposer_index(block.slot(), spec)?; // Apply rewards to participant balances. Keep track of proposer rewards for (validator_index, participant_bit) in sync_committee_indices @@ -48,15 +65,15 @@ impl BeaconChain { .zip(sync_aggregate.sync_committee_bits.iter()) { let participant_balance = balances - .entry(*validator_index) - .or_insert_with(|| state.balances()[*validator_index]); + .get_mut(validator_index) + .ok_or(BeaconChainError::SyncCommitteeRewardsSyncError)?; if participant_bit { participant_balance.safe_add_assign(participant_reward_value)?; balances - .entry(proposer_index) - .or_insert_with(|| state.balances()[proposer_index]) + .get_mut(&proposer_index) + .ok_or(BeaconChainError::SyncCommitteeRewardsSyncError)? .safe_add_assign(proposer_reward_per_bit)?; total_proposer_rewards.safe_add_assign(proposer_reward_per_bit)?; @@ -67,18 +84,17 @@ impl BeaconChain { Ok(balances .iter() - .filter_map(|(i, new_balance)| { - let reward = if *i != proposer_index { - *new_balance as i64 - state.balances()[*i] as i64 - } else if sync_committee_indices.contains(i) { - *new_balance as i64 - - state.balances()[*i] as i64 - - total_proposer_rewards as i64 + .filter_map(|(&i, &new_balance)| { + let initial_balance = *state.balances().get(i)? as i64; + let reward = if i != proposer_index { + new_balance as i64 - initial_balance + } else if sync_committee_indices.contains(&i) { + new_balance as i64 - initial_balance - total_proposer_rewards as i64 } else { return None; }; Some(SyncCommitteeReward { - validator_index: *i as u64, + validator_index: i as u64, reward, }) }) diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 0a494e1d8a4..efbb4feaf08 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -45,10 +45,7 @@ use slog_async::Async; use slog_term::{FullFormat, TermDecorator}; use slot_clock::{SlotClock, TestingSlotClock}; use state_processing::per_block_processing::compute_timestamp_at_slot; -use state_processing::{ - state_advance::{complete_state_advance, partial_state_advance}, - StateProcessingStrategy, -}; +use state_processing::state_advance::complete_state_advance; use std::borrow::Cow; use std::collections::{HashMap, HashSet}; use std::fmt; @@ -754,10 +751,7 @@ where pub fn get_current_state_and_root(&self) -> (BeaconState, Hash256) { let head = self.chain.head_snapshot(); let state_root = head.beacon_state_root(); - ( - head.beacon_state.clone_with_only_committee_caches(), - state_root, - ) + (head.beacon_state.clone(), state_root) } pub fn head_slot(&self) -> Slot { @@ -800,8 +794,9 @@ where pub fn get_hot_state(&self, state_hash: BeaconStateHash) -> Option> { self.chain .store - .load_hot_state(&state_hash.into(), StateProcessingStrategy::Accurate) + .load_hot_state(&state_hash.into()) .unwrap() + .map(|(state, _)| state) } pub fn get_cold_state(&self, state_hash: BeaconStateHash) -> Option> { @@ -1015,9 +1010,7 @@ where return Err(BeaconChainError::CannotAttestToFutureState); } else if state.current_epoch() < epoch { let mut_state = state.to_mut(); - // Only perform a "partial" state advance since we do not require the state roots to be - // accurate. - partial_state_advance( + complete_state_advance( mut_state, Some(state_root), epoch.start_slot(E::slots_per_epoch()), diff --git a/beacon_node/beacon_chain/src/validator_monitor.rs b/beacon_node/beacon_chain/src/validator_monitor.rs index a63940074b4..e9993fcd397 100644 --- a/beacon_node/beacon_chain/src/validator_monitor.rs +++ b/beacon_node/beacon_chain/src/validator_monitor.rs @@ -493,10 +493,10 @@ impl ValidatorMonitor { .skip(self.indices.len()) .for_each(|(i, validator)| { let i = i as u64; - if let Some(validator) = self.validators.get_mut(&validator.pubkey) { + if let Some(validator) = self.validators.get_mut(validator.pubkey()) { validator.set_index(i) } - self.indices.insert(i, validator.pubkey); + self.indices.insert(i, *validator.pubkey()); }); // Add missed non-finalized blocks for the monitored validators @@ -536,12 +536,12 @@ impl ValidatorMonitor { metrics::set_int_gauge( &metrics::VALIDATOR_MONITOR_EFFECTIVE_BALANCE_GWEI, &[id], - u64_to_i64(validator.effective_balance), + u64_to_i64(validator.effective_balance()), ); metrics::set_int_gauge( &metrics::VALIDATOR_MONITOR_SLASHED, &[id], - i64::from(validator.slashed), + i64::from(validator.slashed()), ); metrics::set_int_gauge( &metrics::VALIDATOR_MONITOR_ACTIVE, @@ -561,22 +561,22 @@ impl ValidatorMonitor { metrics::set_int_gauge( &metrics::VALIDATOR_ACTIVATION_ELIGIBILITY_EPOCH, &[id], - u64_to_i64(validator.activation_eligibility_epoch), + u64_to_i64(validator.activation_eligibility_epoch()), ); metrics::set_int_gauge( &metrics::VALIDATOR_ACTIVATION_EPOCH, &[id], - u64_to_i64(validator.activation_epoch), + u64_to_i64(validator.activation_epoch()), ); metrics::set_int_gauge( &metrics::VALIDATOR_EXIT_EPOCH, &[id], - u64_to_i64(validator.exit_epoch), + u64_to_i64(validator.exit_epoch()), ); metrics::set_int_gauge( &metrics::VALIDATOR_WITHDRAWABLE_EPOCH, &[id], - u64_to_i64(validator.withdrawable_epoch), + u64_to_i64(validator.withdrawable_epoch()), ); } } diff --git a/beacon_node/beacon_chain/tests/attestation_verification.rs b/beacon_node/beacon_chain/tests/attestation_verification.rs index 3432604cc93..0ee98af0e11 100644 --- a/beacon_node/beacon_chain/tests/attestation_verification.rs +++ b/beacon_node/beacon_chain/tests/attestation_verification.rs @@ -1059,7 +1059,7 @@ async fn attestation_that_skips_epochs() { let block_slot = harness .chain .store - .get_blinded_block(&block_root) + .get_blinded_block(&block_root, None) .expect("should not error getting block") .expect("should find attestation block") .message() diff --git a/beacon_node/beacon_chain/tests/op_verification.rs b/beacon_node/beacon_chain/tests/op_verification.rs index 40910b9b9fe..02be7120ca9 100644 --- a/beacon_node/beacon_chain/tests/op_verification.rs +++ b/beacon_node/beacon_chain/tests/op_verification.rs @@ -170,7 +170,7 @@ async fn voluntary_exit_duplicate_in_state() { .validators() .get(exited_validator as usize) .unwrap() - .exit_epoch, + .exit_epoch(), spec.far_future_epoch ); @@ -274,14 +274,12 @@ async fn proposer_slashing_duplicate_in_state() { .await; // Verify validator is actually slashed. - assert!( - harness - .get_current_state() - .validators() - .get(slashed_validator as usize) - .unwrap() - .slashed - ); + assert!(harness + .get_current_state() + .validators() + .get(slashed_validator as usize) + .unwrap() + .slashed()); // Clear the in-memory gossip cache & try to verify the same slashing on gossip. // It should still fail because gossip verification should check the validator's `slashed` field @@ -402,14 +400,12 @@ async fn attester_slashing_duplicate_in_state() { .await; // Verify validator is actually slashed. - assert!( - harness - .get_current_state() - .validators() - .get(slashed_validator as usize) - .unwrap() - .slashed - ); + assert!(harness + .get_current_state() + .validators() + .get(slashed_validator as usize) + .unwrap() + .slashed()); // Clear the in-memory gossip cache & try to verify the same slashing on gossip. // It should still fail because gossip verification should check the validator's `slashed` field diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index f1262596f70..b207ef70c33 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -223,7 +223,7 @@ impl InvalidPayloadRig { let mock_execution_layer = self.harness.mock_execution_layer.as_ref().unwrap(); let head = self.harness.chain.head_snapshot(); - let state = head.beacon_state.clone_with_only_committee_caches(); + let state = head.beacon_state.clone(); let slot = slot_override.unwrap_or(state.slot() + 1); let ((block, blobs), post_state) = self.harness.make_block(state, slot).await; let block_root = block.canonical_root(); @@ -316,7 +316,7 @@ impl InvalidPayloadRig { self.harness .chain .store - .get_full_block(&block_root) + .get_full_block(&block_root, None) .unwrap() .unwrap(), *block, @@ -2048,7 +2048,7 @@ async fn weights_after_resetting_optimistic_status() { .fork_choice_read_lock() .get_block_weight(&head.head_block_root()) .unwrap(), - head.snapshot.beacon_state.validators()[0].effective_balance, + head.snapshot.beacon_state.validators().get(0).unwrap().effective_balance(), "proposer boost should be removed from the head block and the vote of a single validator applied" ); diff --git a/beacon_node/beacon_chain/tests/rewards.rs b/beacon_node/beacon_chain/tests/rewards.rs index a78463ef5d7..1c80525223a 100644 --- a/beacon_node/beacon_chain/tests/rewards.rs +++ b/beacon_node/beacon_chain/tests/rewards.rs @@ -105,8 +105,8 @@ async fn test_sync_committee_rewards() { .get_validator_index(&validator.pubkey) .unwrap() .unwrap(); - let pre_state_balance = parent_state.balances()[validator_index]; - let post_state_balance = state.balances()[validator_index]; + let pre_state_balance = *parent_state.balances().get(validator_index).unwrap(); + let post_state_balance = *state.balances().get(validator_index).unwrap(); let sync_committee_reward = rewards.get(&(validator_index as u64)).unwrap_or(&0); if validator_index == proposer_index { @@ -141,7 +141,7 @@ async fn test_verify_attestation_rewards_base() { ) .await; - let initial_balances: Vec = harness.get_current_state().balances().clone().into(); + let initial_balances: Vec = harness.get_current_state().balances().to_vec(); // extend slots to beginning of epoch N + 2 harness.extend_slots(E::slots_per_epoch() as usize).await; @@ -163,7 +163,7 @@ async fn test_verify_attestation_rewards_base() { let expected_balances = apply_attestation_rewards(&initial_balances, total_rewards); // verify expected balances against actual balances - let balances: Vec = harness.get_current_state().balances().clone().into(); + let balances: Vec = harness.get_current_state().balances().to_vec(); assert_eq!(expected_balances, balances); } @@ -185,7 +185,7 @@ async fn test_verify_attestation_rewards_base_inactivity_leak() { AttestationStrategy::SomeValidators(half_validators.clone()), ) .await; - let initial_balances: Vec = harness.get_current_state().balances().clone().into(); + let initial_balances: Vec = harness.get_current_state().balances().to_vec(); // extend slots to beginning of epoch N + 2 harness.advance_slot(); @@ -215,7 +215,7 @@ async fn test_verify_attestation_rewards_base_inactivity_leak() { let expected_balances = apply_attestation_rewards(&initial_balances, total_rewards); // verify expected balances against actual balances - let balances: Vec = harness.get_current_state().balances().clone().into(); + let balances: Vec = harness.get_current_state().balances().to_vec(); assert_eq!(expected_balances, balances); } @@ -241,7 +241,7 @@ async fn test_verify_attestation_rewards_base_inactivity_leak_justification_epoc // advance to create first justification epoch and get initial balances harness.extend_slots(E::slots_per_epoch() as usize).await; target_epoch += 1; - let initial_balances: Vec = harness.get_current_state().balances().clone().into(); + let initial_balances: Vec = harness.get_current_state().balances().to_vec(); //assert previous_justified_checkpoint matches 0 as we were in inactivity leak from beginning assert_eq!( @@ -284,7 +284,7 @@ async fn test_verify_attestation_rewards_base_inactivity_leak_justification_epoc let expected_balances = apply_attestation_rewards(&initial_balances, total_rewards); // verify expected balances against actual balances - let balances: Vec = harness.get_current_state().balances().clone().into(); + let balances: Vec = harness.get_current_state().balances().to_vec(); assert_eq!(expected_balances, balances); } @@ -298,7 +298,7 @@ async fn test_verify_attestation_rewards_altair() { harness .extend_slots((E::slots_per_epoch() * (target_epoch + 1)) as usize) .await; - let initial_balances: Vec = harness.get_current_state().balances().clone().into(); + let initial_balances: Vec = harness.get_current_state().balances().to_vec(); // advance until epoch N + 2 and build proposal rewards map let mut proposal_rewards_map: HashMap = HashMap::new(); @@ -364,7 +364,7 @@ async fn test_verify_attestation_rewards_altair() { apply_sync_committee_rewards(&sync_committee_rewards_map, expected_balances); // verify expected balances against actual balances - let balances: Vec = harness.get_current_state().balances().clone().into(); + let balances: Vec = harness.get_current_state().balances().to_vec(); assert_eq!(expected_balances, balances); } @@ -386,7 +386,7 @@ async fn test_verify_attestation_rewards_altair_inactivity_leak() { half_validators.clone(), ) .await; - let initial_balances: Vec = harness.get_current_state().balances().clone().into(); + let initial_balances: Vec = harness.get_current_state().balances().to_vec(); // advance until epoch N + 2 and build proposal rewards map let mut proposal_rewards_map: HashMap = HashMap::new(); @@ -458,7 +458,7 @@ async fn test_verify_attestation_rewards_altair_inactivity_leak() { apply_sync_committee_rewards(&sync_committee_rewards_map, expected_balances); // verify expected balances against actual balances - let balances: Vec = harness.get_current_state().balances().clone().into(); + let balances: Vec = harness.get_current_state().balances().to_vec(); assert_eq!(expected_balances, balances); } @@ -492,7 +492,7 @@ async fn test_verify_attestation_rewards_altair_inactivity_leak_justification_ep // advance for first justification epoch and get balances harness.extend_slots(E::slots_per_epoch() as usize).await; target_epoch += 1; - let initial_balances: Vec = harness.get_current_state().balances().clone().into(); + let initial_balances: Vec = harness.get_current_state().balances().to_vec(); // advance until epoch N + 2 and build proposal rewards map let mut proposal_rewards_map: HashMap = HashMap::new(); @@ -568,7 +568,7 @@ async fn test_verify_attestation_rewards_altair_inactivity_leak_justification_ep apply_sync_committee_rewards(&sync_committee_rewards_map, expected_balances); // verify expected balances against actual balances - let balances: Vec = harness.get_current_state().balances().clone().into(); + let balances: Vec = harness.get_current_state().balances().to_vec(); assert_eq!(expected_balances, balances); } diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 66f4138afb4..6b51390af6a 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -4,7 +4,6 @@ use beacon_chain::attestation_verification::Error as AttnError; use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::builder::BeaconChainBuilder; use beacon_chain::data_availability_checker::AvailableBlock; -use beacon_chain::schema_change::migrate_schema; use beacon_chain::test_utils::{ mock_execution_layer_from_parts, test_spec, AttestationStrategy, BeaconChainHarness, BlockStrategy, DiskHarnessType, @@ -27,17 +26,14 @@ use std::collections::HashSet; use std::convert::TryInto; use std::sync::Arc; use std::time::Duration; -use store::chunked_vector::Chunk; -use store::metadata::{SchemaVersion, CURRENT_SCHEMA_VERSION, STATE_UPPER_LIMIT_NO_RETAIN}; +use store::hdiff::HierarchyConfig; +use store::metadata::STATE_UPPER_LIMIT_NO_RETAIN; use store::{ - chunked_vector::{chunk_key, Field}, - get_key_for_col, + config::StoreConfigError, iter::{BlockRootsIterator, StateRootsIterator}, - BlobInfo, DBColumn, HotColdDB, KeyValueStore, KeyValueStoreOp, LevelDB, StoreConfig, + BlobInfo, Error as StoreError, HotColdDB, LevelDB, StoreConfig, }; use tempfile::{tempdir, TempDir}; -use tokio::time::sleep; -use tree_hash::TreeHash; use types::test_utils::{SeedableRng, XorShiftRng}; use types::*; @@ -62,6 +58,21 @@ fn get_store_generic( config: StoreConfig, spec: ChainSpec, ) -> Arc, LevelDB>> { + let config = StoreConfig { + // More frequent snapshots and hdiffs in tests for testing + hierarchy_config: HierarchyConfig { + exponents: vec![1, 3, 5], + }, + ..config + }; + try_get_store_with_spec_and_config(db_path, spec, config).expect("disk store should initialize") +} + +fn try_get_store_with_spec_and_config( + db_path: &TempDir, + spec: ChainSpec, + config: StoreConfig, +) -> Result, LevelDB>>, StoreError> { let hot_path = db_path.path().join("hot_db"); let cold_path = db_path.path().join("cold_db"); let blobs_path = db_path.path().join("blobs_db"); @@ -76,7 +87,6 @@ fn get_store_generic( spec, log, ) - .expect("disk store should initialize") } fn get_harness( @@ -108,253 +118,6 @@ fn get_harness_generic( harness } -/// Tests that `store.heal_freezer_block_roots_at_split` inserts block roots between last restore point -/// slot and the split slot. -#[tokio::test] -async fn heal_freezer_block_roots_at_split() { - // chunk_size is hard-coded to 128 - let num_blocks_produced = E::slots_per_epoch() * 20; - let db_path = tempdir().unwrap(); - let store = get_store_generic( - &db_path, - StoreConfig { - slots_per_restore_point: 2 * E::slots_per_epoch(), - ..Default::default() - }, - test_spec::(), - ); - let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); - - harness - .extend_chain( - num_blocks_produced as usize, - BlockStrategy::OnCanonicalHead, - AttestationStrategy::AllValidators, - ) - .await; - - let split_slot = store.get_split_slot(); - assert_eq!(split_slot, 18 * E::slots_per_epoch()); - - // Do a heal before deleting to make sure that it doesn't break. - let last_restore_point_slot = Slot::new(16 * E::slots_per_epoch()); - store.heal_freezer_block_roots_at_split().unwrap(); - check_freezer_block_roots(&harness, last_restore_point_slot, split_slot); - - // Delete block roots between `last_restore_point_slot` and `split_slot`. - let chunk_index = >::chunk_index( - last_restore_point_slot.as_usize(), - ); - let key_chunk = get_key_for_col(DBColumn::BeaconBlockRoots.as_str(), &chunk_key(chunk_index)); - store - .cold_db - .do_atomically(vec![KeyValueStoreOp::DeleteKey(key_chunk)]) - .unwrap(); - - let block_root_err = store - .forwards_block_roots_iterator_until( - last_restore_point_slot, - last_restore_point_slot + 1, - || unreachable!(), - &harness.chain.spec, - ) - .unwrap() - .next() - .unwrap() - .unwrap_err(); - - assert!(matches!(block_root_err, store::Error::NoContinuationData)); - - // Re-insert block roots - store.heal_freezer_block_roots_at_split().unwrap(); - check_freezer_block_roots(&harness, last_restore_point_slot, split_slot); - - // Run for another two epochs to check that the invariant is maintained. - let additional_blocks_produced = 2 * E::slots_per_epoch(); - harness - .extend_slots(additional_blocks_produced as usize) - .await; - - check_finalization(&harness, num_blocks_produced + additional_blocks_produced); - check_split_slot(&harness, store); - check_chain_dump( - &harness, - num_blocks_produced + additional_blocks_produced + 1, - ); - check_iterators(&harness); -} - -/// Tests that `store.heal_freezer_block_roots` inserts block roots between last restore point -/// slot and the split slot. -#[tokio::test] -async fn heal_freezer_block_roots_with_skip_slots() { - // chunk_size is hard-coded to 128 - let num_blocks_produced = E::slots_per_epoch() * 20; - let db_path = tempdir().unwrap(); - let store = get_store_generic( - &db_path, - StoreConfig { - slots_per_restore_point: 2 * E::slots_per_epoch(), - ..Default::default() - }, - test_spec::(), - ); - let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); - - let current_state = harness.get_current_state(); - let state_root = harness.get_current_state().tree_hash_root(); - let all_validators = &harness.get_all_validators(); - harness - .add_attested_blocks_at_slots( - current_state, - state_root, - &(1..=num_blocks_produced) - .filter(|i| i % 12 != 0) - .map(Slot::new) - .collect::>(), - all_validators, - ) - .await; - - // split slot should be 18 here - let split_slot = store.get_split_slot(); - assert_eq!(split_slot, 18 * E::slots_per_epoch()); - - let last_restore_point_slot = Slot::new(16 * E::slots_per_epoch()); - let chunk_index = >::chunk_index( - last_restore_point_slot.as_usize(), - ); - let key_chunk = get_key_for_col(DBColumn::BeaconBlockRoots.as_str(), &chunk_key(chunk_index)); - store - .cold_db - .do_atomically(vec![KeyValueStoreOp::DeleteKey(key_chunk)]) - .unwrap(); - - let block_root_err = store - .forwards_block_roots_iterator_until( - last_restore_point_slot, - last_restore_point_slot + 1, - || unreachable!(), - &harness.chain.spec, - ) - .unwrap() - .next() - .unwrap() - .unwrap_err(); - - assert!(matches!(block_root_err, store::Error::NoContinuationData)); - - // heal function - store.heal_freezer_block_roots_at_split().unwrap(); - check_freezer_block_roots(&harness, last_restore_point_slot, split_slot); - - // Run for another two epochs to check that the invariant is maintained. - let additional_blocks_produced = 2 * E::slots_per_epoch(); - harness - .extend_slots(additional_blocks_produced as usize) - .await; - - check_finalization(&harness, num_blocks_produced + additional_blocks_produced); - check_split_slot(&harness, store); - check_iterators(&harness); -} - -/// Tests that `store.heal_freezer_block_roots_at_genesis` replaces 0x0 block roots between slot -/// 0 and the first non-skip slot with genesis block root. -#[tokio::test] -async fn heal_freezer_block_roots_at_genesis() { - // Run for a few epochs to ensure we're past finalization. - let num_blocks_produced = E::slots_per_epoch() * 4; - let db_path = tempdir().unwrap(); - let store = get_store(&db_path); - let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); - - // Start with 2 skip slots. - harness.advance_slot(); - harness.advance_slot(); - - harness - .extend_chain( - num_blocks_produced as usize, - BlockStrategy::OnCanonicalHead, - AttestationStrategy::AllValidators, - ) - .await; - - // Do a heal before deleting to make sure that it doesn't break. - store.heal_freezer_block_roots_at_genesis().unwrap(); - check_freezer_block_roots( - &harness, - Slot::new(0), - Epoch::new(1).end_slot(E::slots_per_epoch()), - ); - - // Write 0x0 block roots at slot 1 and slot 2. - let chunk_index = 0; - let chunk_db_key = chunk_key(chunk_index); - let mut chunk = - Chunk::::load(&store.cold_db, DBColumn::BeaconBlockRoots, &chunk_db_key) - .unwrap() - .unwrap(); - - chunk.values[1] = Hash256::zero(); - chunk.values[2] = Hash256::zero(); - - let mut ops = vec![]; - chunk - .store(DBColumn::BeaconBlockRoots, &chunk_db_key, &mut ops) - .unwrap(); - store.cold_db.do_atomically(ops).unwrap(); - - // Ensure the DB is corrupted - let block_roots = store - .forwards_block_roots_iterator_until( - Slot::new(1), - Slot::new(2), - || unreachable!(), - &harness.chain.spec, - ) - .unwrap() - .map(Result::unwrap) - .take(2) - .collect::>(); - assert_eq!( - block_roots, - vec![ - (Hash256::zero(), Slot::new(1)), - (Hash256::zero(), Slot::new(2)) - ] - ); - - // Insert genesis block roots at skip slots before first block slot - store.heal_freezer_block_roots_at_genesis().unwrap(); - check_freezer_block_roots( - &harness, - Slot::new(0), - Epoch::new(1).end_slot(E::slots_per_epoch()), - ); -} - -fn check_freezer_block_roots(harness: &TestHarness, start_slot: Slot, end_slot: Slot) { - for slot in (start_slot.as_u64()..end_slot.as_u64()).map(Slot::new) { - let (block_root, result_slot) = harness - .chain - .store - .forwards_block_roots_iterator_until(slot, slot, || unreachable!(), &harness.chain.spec) - .unwrap() - .next() - .unwrap() - .unwrap(); - assert_eq!(slot, result_slot); - let expected_block_root = harness - .chain - .block_root_at_slot(slot, WhenSlotSkipped::Prev) - .unwrap() - .unwrap(); - assert_eq!(expected_block_root, block_root); - } -} - #[tokio::test] async fn full_participation_no_skips() { let num_blocks_produced = E::slots_per_epoch() * 5; @@ -553,6 +316,9 @@ async fn split_slot_restore() { ) .await; + // Uhmm. FIXME(sproul) + // tokio::time::sleep(std::time::Duration::from_secs(10)).await; + store.get_split_slot() }; assert_ne!(split_slot, Slot::new(0)); @@ -607,22 +373,6 @@ async fn epoch_boundary_state_attestation_processing() { let mut checked_pre_fin = false; for (attestation, subnet_id) in late_attestations.into_iter().flatten() { - // load_epoch_boundary_state is idempotent! - let block_root = attestation.data.beacon_block_root; - let block = store - .get_blinded_block(&block_root) - .unwrap() - .expect("block exists"); - let epoch_boundary_state = store - .load_epoch_boundary_state(&block.state_root()) - .expect("no error") - .expect("epoch boundary state exists"); - let ebs_of_ebs = store - .load_epoch_boundary_state(&epoch_boundary_state.canonical_root()) - .expect("no error") - .expect("ebs of ebs exists"); - assert_eq!(epoch_boundary_state, ebs_of_ebs); - // If the attestation is pre-finalization it should be rejected. let finalized_epoch = harness.finalized_checkpoint().epoch; @@ -682,10 +432,9 @@ async fn forwards_iter_block_and_state_roots_until() { check_finalization(&harness, num_blocks_produced); check_split_slot(&harness, store.clone()); - // The last restore point slot is the point at which the hybrid forwards iterator behaviour - // changes. - let last_restore_point_slot = store.get_latest_restore_point_slot().unwrap(); - assert!(last_restore_point_slot > 0); + // The split slot is the point at which the hybrid forwards iterator behaviour changes. + let split_slot = store.get_split_slot(); + assert!(split_slot > 0); let chain = &harness.chain; let head_state = harness.get_current_state(); @@ -709,15 +458,12 @@ async fn forwards_iter_block_and_state_roots_until() { } }; - let split_slot = store.get_split_slot(); - assert!(split_slot > last_restore_point_slot); - - test_range(Slot::new(0), last_restore_point_slot); - test_range(last_restore_point_slot, last_restore_point_slot); - test_range(last_restore_point_slot - 1, last_restore_point_slot); - test_range(Slot::new(0), last_restore_point_slot - 1); test_range(Slot::new(0), split_slot); - test_range(last_restore_point_slot - 1, split_slot); + test_range(split_slot, split_slot); + test_range(split_slot - 1, split_slot); + test_range(Slot::new(0), split_slot - 1); + test_range(Slot::new(0), split_slot); + test_range(split_slot - 1, split_slot); test_range(Slot::new(0), head_state.slot()); } @@ -797,7 +543,7 @@ async fn block_replayer_hooks() { let mut post_block_slots = vec![]; let mut replay_state = BlockReplayer::::new(state, &chain.spec) - .pre_slot_hook(Box::new(|state| { + .pre_slot_hook(Box::new(|_, state| { pre_slots.push(state.slot()); Ok(()) })) @@ -836,6 +582,8 @@ async fn block_replayer_hooks() { assert_eq!(post_block_slots, block_slots); // States match. + end_state.apply_pending_mutations().unwrap(); + replay_state.apply_pending_mutations().unwrap(); end_state.drop_all_caches().unwrap(); replay_state.drop_all_caches().unwrap(); assert_eq!(end_state, replay_state); @@ -895,7 +643,7 @@ async fn delete_blocks_and_states() { ); let faulty_head_block = store - .get_blinded_block(&faulty_head.into()) + .get_blinded_block(&faulty_head.into(), None) .expect("no errors") .expect("faulty head block exists"); @@ -937,7 +685,7 @@ async fn delete_blocks_and_states() { break; } store.delete_block(&block_root).unwrap(); - assert_eq!(store.get_blinded_block(&block_root).unwrap(), None); + assert_eq!(store.get_blinded_block(&block_root, None).unwrap(), None); } // Deleting frozen states should do nothing @@ -1181,7 +929,7 @@ fn get_state_for_block(harness: &TestHarness, block_root: Hash256) -> BeaconStat let head_block = harness .chain .store - .get_blinded_block(&block_root) + .get_blinded_block(&block_root, None) .unwrap() .unwrap(); harness @@ -1221,9 +969,17 @@ fn check_shuffling_compatible( |committee_cache, _| { let state_cache = head_state.committee_cache(RelativeEpoch::Current).unwrap(); if current_epoch_shuffling_is_compatible { - assert_eq!(committee_cache, state_cache, "block at slot {slot}"); + assert_eq!( + committee_cache, + state_cache.as_ref(), + "block at slot {slot}" + ); } else { - assert_ne!(committee_cache, state_cache, "block at slot {slot}"); + assert_ne!( + committee_cache, + state_cache.as_ref(), + "block at slot {slot}" + ); } Ok(()) }, @@ -1253,9 +1009,9 @@ fn check_shuffling_compatible( |committee_cache, _| { let state_cache = head_state.committee_cache(RelativeEpoch::Previous).unwrap(); if previous_epoch_shuffling_is_compatible { - assert_eq!(committee_cache, state_cache); + assert_eq!(committee_cache, state_cache.as_ref()); } else { - assert_ne!(committee_cache, state_cache); + assert_ne!(committee_cache, state_cache.as_ref()); } Ok(()) }, @@ -2253,6 +2009,7 @@ async fn pruning_test( check_no_blocks_exist(&harness, stray_blocks.values()); } +/* FIXME(sproul): adapt this test for new paradigm #[tokio::test] async fn garbage_collect_temp_states_from_failed_block() { let db_path = tempdir().unwrap(); @@ -2310,6 +2067,7 @@ async fn garbage_collect_temp_states_from_failed_block() { let store = get_store(&db_path); assert_eq!(store.iter_temporary_state_roots().count(), 0); } +*/ #[tokio::test] async fn weak_subjectivity_sync_easy() { @@ -2393,7 +2151,7 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { let wss_block = harness .chain .store - .get_full_block(&wss_block_root) + .get_full_block(&wss_block_root, None) .unwrap() .unwrap(); let wss_blobs_opt = harness.chain.store.get_blobs(&wss_block_root).unwrap(); @@ -2615,7 +2373,7 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { .unwrap() .map(Result::unwrap) { - let block = store.get_blinded_block(&block_root).unwrap().unwrap(); + let block = store.get_blinded_block(&block_root, None).unwrap().unwrap(); if block_root != prev_block_root { assert_eq!(block.slot(), slot); } @@ -2638,7 +2396,7 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { assert_eq!(store.get_anchor_slot(), Some(wss_block.slot())); // Reconstruct states. - store.clone().reconstruct_historic_states().unwrap(); + store.clone().reconstruct_historic_states(None).unwrap(); assert_eq!(store.get_anchor_slot(), None); } @@ -3053,10 +2811,55 @@ async fn revert_minority_fork_on_resume() { assert_eq!(heads.len(), 1); } +#[tokio::test] +async fn should_not_initialize_incompatible_store_config() { + let validator_count = 16; + let spec = MinimalEthSpec::default_spec(); + let db_path = tempdir().unwrap(); + let store_config = StoreConfig::default(); + let store = try_get_store_with_spec_and_config(&db_path, spec.clone(), store_config.clone()) + .expect("disk store should initialize"); + let harness = BeaconChainHarness::builder(MinimalEthSpec) + .spec(spec.clone()) + .deterministic_keypairs(validator_count) + .fresh_disk_store(store.clone()) + .build(); + + // Ensure the store is dropped & closed. + // This test was a bit flaky, with the store not getting dropped before attempting to re-open + // it. Checking the strong_count is an attempt to remedy this. + drop(harness); + for _ in 0..100 { + if Arc::strong_count(&store) == 1 { + break; + } + tokio::time::sleep(std::time::Duration::from_millis(5)).await; + } + drop(store); + + // Resume from disk with a different store config. + let different_store_config = StoreConfig { + linear_blocks: !store_config.linear_blocks, + ..store_config + }; + let maybe_err = + try_get_store_with_spec_and_config(&db_path, spec, different_store_config).err(); + + assert!(matches!( + maybe_err, + Some(StoreError::ConfigError( + StoreConfigError::IncompatibleStoreConfig { .. } + )) + )); +} + // This test checks whether the schema downgrade from the latest version to some minimum supported // version is correct. This is the easiest schema test to write without historic versions of // Lighthouse on-hand, but has the disadvantage that the min version needs to be adjusted manually // as old downgrades are deprecated. +/* FIXME(sproul): broken until DB migration is implemented +use beacon_chain::schema_change::migrate_schema; +use store::metadata::{SchemaVersion, CURRENT_SCHEMA_VERSION}; #[tokio::test] async fn schema_downgrade_to_min_version() { let num_blocks_produced = E::slots_per_epoch() * 4; @@ -3141,6 +2944,7 @@ async fn schema_downgrade_to_min_version() { ) .expect_err("should not downgrade below minimum version"); } +*/ /// Check that blob pruning prunes blobs older than the data availability boundary. #[tokio::test] @@ -3609,16 +3413,16 @@ fn check_split_slot(harness: &TestHarness, store: Arc, L /// Check that all the states in a chain dump have the correct tree hash. fn check_chain_dump(harness: &TestHarness, expected_len: u64) { - let chain_dump = harness.chain.chain_dump().unwrap(); + let mut chain_dump = harness.chain.chain_dump().unwrap(); let split_slot = harness.chain.store.get_split_slot(); assert_eq!(chain_dump.len() as u64, expected_len); - for checkpoint in &chain_dump { + for checkpoint in &mut chain_dump { // Check that the tree hash of the stored state is as expected assert_eq!( checkpoint.beacon_state_root(), - checkpoint.beacon_state.tree_hash_root(), + checkpoint.beacon_state.update_tree_hash_cache().unwrap(), "tree hash of stored state is incorrect" ); diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index a91f5d6a442..d03ba22bfea 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -17,12 +17,13 @@ pub use json_structures::{JsonWithdrawal, TransitionConfigurationV1}; use pretty_reqwest_error::PrettyReqwestError; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; +use ssz_types::FixedVector; use strum::IntoStaticStr; use superstruct::superstruct; pub use types::{ Address, BeaconBlockRef, EthSpec, ExecutionBlockHash, ExecutionPayload, ExecutionPayloadHeader, - ExecutionPayloadRef, FixedVector, ForkName, Hash256, Transactions, Uint256, VariableList, - Withdrawal, Withdrawals, + ExecutionPayloadRef, ForkName, Hash256, Transactions, Uint256, VariableList, Withdrawal, + Withdrawals, }; use types::{ diff --git a/beacon_node/execution_layer/src/engine_api/json_structures.rs b/beacon_node/execution_layer/src/engine_api/json_structures.rs index 9f2387ae314..b7fe41cabed 100644 --- a/beacon_node/execution_layer/src/engine_api/json_structures.rs +++ b/beacon_node/execution_layer/src/engine_api/json_structures.rs @@ -1,10 +1,11 @@ use super::*; use serde::{Deserialize, Serialize}; +use ssz_types::FixedVector; use strum::EnumString; use superstruct::superstruct; use types::beacon_block_body::KzgCommitments; use types::blob_sidecar::BlobsList; -use types::{FixedVector, Unsigned}; +use types::Unsigned; #[derive(Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/beacon_node/genesis/src/interop.rs b/beacon_node/genesis/src/interop.rs index b4753e92f1f..f11eeeac09a 100644 --- a/beacon_node/genesis/src/interop.rs +++ b/beacon_node/genesis/src/interop.rs @@ -178,14 +178,15 @@ mod test { } for v in state.validators() { - let creds = v.withdrawal_credentials.as_bytes(); + let creds = v.withdrawal_credentials(); assert_eq!( - creds[0], spec.bls_withdrawal_prefix_byte, + creds.as_bytes()[0], + spec.bls_withdrawal_prefix_byte, "first byte of withdrawal creds should be bls prefix" ); assert_eq!( - &creds[1..], - &hash(&v.pubkey.as_ssz_bytes())[1..], + &creds.as_bytes()[1..], + &hash(&v.pubkey().as_ssz_bytes())[1..], "rest of withdrawal creds should be pubkey hash" ) } @@ -240,7 +241,8 @@ mod test { } for (index, v) in state.validators().iter().enumerate() { - let creds = v.withdrawal_credentials.as_bytes(); + let withdrawal_credientials = v.withdrawal_credentials(); + let creds = withdrawal_credientials.as_bytes(); if index % 2 == 0 { assert_eq!( creds[0], spec.bls_withdrawal_prefix_byte, diff --git a/beacon_node/http_api/src/attester_duties.rs b/beacon_node/http_api/src/attester_duties.rs index f3242a2b374..6c7dc3348c1 100644 --- a/beacon_node/http_api/src/attester_duties.rs +++ b/beacon_node/http_api/src/attester_duties.rs @@ -5,9 +5,7 @@ use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes}; use eth2::types::{self as api_types}; use slot_clock::SlotClock; use state_processing::state_advance::partial_state_advance; -use types::{ - AttestationDuty, BeaconState, ChainSpec, CloneConfig, Epoch, EthSpec, Hash256, RelativeEpoch, -}; +use types::{AttestationDuty, BeaconState, ChainSpec, Epoch, EthSpec, Hash256, RelativeEpoch}; /// The struct that is returned to the requesting HTTP client. type ApiDuties = api_types::DutiesResponse>; @@ -90,8 +88,7 @@ fn compute_historic_attester_duties( if head.beacon_state.current_epoch() <= request_epoch { Some(( head.beacon_state_root(), - head.beacon_state - .clone_with(CloneConfig::committee_caches_only()), + head.beacon_state.clone(), execution_status.is_optimistic_or_invalid(), )) } else { diff --git a/beacon_node/http_api/src/block_packing_efficiency.rs b/beacon_node/http_api/src/block_packing_efficiency.rs index d78f1f7c66e..f105fdf0a7d 100644 --- a/beacon_node/http_api/src/block_packing_efficiency.rs +++ b/beacon_node/http_api/src/block_packing_efficiency.rs @@ -279,7 +279,7 @@ pub fn get_block_packing_efficiency( )); let pre_slot_hook = - |state: &mut BeaconState| -> Result<(), PackingEfficiencyError> { + |_, state: &mut BeaconState| -> Result<(), PackingEfficiencyError> { // Add attestations to `available_attestations`. handler.lock().add_attestations(state.slot())?; Ok(()) diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 9e6022dc954..42188a6c97c 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -61,7 +61,6 @@ use slog::{crit, debug, error, info, warn, Logger}; use slot_clock::SlotClock; use ssz::Encode; pub use state_id::StateId; -use std::borrow::Cow; use std::future::Future; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; @@ -870,10 +869,10 @@ pub fn serve( None }; - let committee_cache = if let Some(ref shuffling) = + let committee_cache = if let Some(shuffling) = maybe_cached_shuffling { - Cow::Borrowed(&**shuffling) + shuffling } else { let possibly_built_cache = match RelativeEpoch::from_epoch(current_epoch, epoch) { @@ -882,16 +881,13 @@ pub fn serve( relative_epoch, ) => { - state - .committee_cache(relative_epoch) - .map(Cow::Borrowed) + state.committee_cache(relative_epoch).cloned() } _ => CommitteeCache::initialized( state, epoch, &chain.spec, - ) - .map(Cow::Owned), + ), } .map_err(|e| { match e { @@ -937,9 +933,9 @@ pub fn serve( .shuffling_cache .try_write_for(std::time::Duration::from_secs(1)) { - cache_write.insert_committee_cache( + cache_write.insert_value( shuffling_id, - &*possibly_built_cache, + &possibly_built_cache, ); } } diff --git a/beacon_node/http_api/src/proposer_duties.rs b/beacon_node/http_api/src/proposer_duties.rs index c31dd9b1faa..ab8952976c8 100644 --- a/beacon_node/http_api/src/proposer_duties.rs +++ b/beacon_node/http_api/src/proposer_duties.rs @@ -10,7 +10,7 @@ use safe_arith::SafeArith; use slog::{debug, Logger}; use slot_clock::SlotClock; use std::cmp::Ordering; -use types::{CloneConfig, Epoch, EthSpec, Hash256, Slot}; +use types::{Epoch, EthSpec, Hash256, Slot}; /// The struct that is returned to the requesting HTTP client. type ApiDuties = api_types::DutiesResponse>; @@ -192,8 +192,7 @@ fn compute_historic_proposer_duties( if head.beacon_state.current_epoch() <= epoch { Some(( head.beacon_state_root(), - head.beacon_state - .clone_with(CloneConfig::committee_caches_only()), + head.beacon_state.clone(), execution_status.is_optimistic_or_invalid(), )) } else { diff --git a/beacon_node/http_api/src/state_id.rs b/beacon_node/http_api/src/state_id.rs index 1a76333e2d4..c4b721f0411 100644 --- a/beacon_node/http_api/src/state_id.rs +++ b/beacon_node/http_api/src/state_id.rs @@ -1,6 +1,7 @@ use crate::ExecutionOptimistic; use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes}; use eth2::types::StateId as CoreStateId; +use slog::{debug, warn}; use std::fmt; use std::str::FromStr; use types::{BeaconState, Checkpoint, EthSpec, Fork, Hash256, Slot}; @@ -178,10 +179,7 @@ impl StateId { .head_and_execution_status() .map_err(warp_utils::reject::beacon_chain_error)?; return Ok(( - cached_head - .snapshot - .beacon_state - .clone_with_only_committee_caches(), + cached_head.snapshot.beacon_state.clone(), execution_status.is_optimistic_or_invalid(), false, )); @@ -190,6 +188,49 @@ impl StateId { _ => (self.root(chain)?, None), }; + let mut opt_state_cache = Some(chain.parallel_state_cache.write()); + + // Try the cache. + if let Some(cache_item) = opt_state_cache + .as_mut() + .and_then(|cache| cache.get(&state_root)) + { + drop(opt_state_cache.take()); + match cache_item.wait() { + Ok(state) => { + debug!( + chain.logger(), + "HTTP state cache hit"; + "state_root" => ?state_root, + "slot" => state.slot(), + ); + return Ok(((*state).clone(), execution_optimistic, finalized)); + } + Err(e) => { + warn!( + chain.logger(), + "State promise failed"; + "state_root" => ?state_root, + "outcome" => "re-computing", + "error" => ?e, + ); + } + } + } + + // Re-lock only in case of failed promise. + debug!( + chain.logger(), + "HTTP state cache miss"; + "state_root" => ?state_root + ); + let mut state_cache = opt_state_cache.unwrap_or_else(|| chain.parallel_state_cache.write()); + + let sender = state_cache.create_promise(state_root).map_err(|e| { + warp_utils::reject::custom_server_error(format!("too many concurrent requests: {e:?}")) + })?; + drop(state_cache); + let state = chain .get_state(&state_root, slot_opt) .map_err(warp_utils::reject::beacon_chain_error) @@ -202,6 +243,11 @@ impl StateId { }) })?; + // Fulfil promise (and re-lock again). + let mut state_cache = chain.parallel_state_cache.write(); + state_cache.resolve_promise(sender, state_root, &state); + drop(state_cache); + Ok((state, execution_optimistic, finalized)) } diff --git a/beacon_node/http_api/src/validator.rs b/beacon_node/http_api/src/validator.rs index 7f11ddd8f43..f54c6424313 100644 --- a/beacon_node/http_api/src/validator.rs +++ b/beacon_node/http_api/src/validator.rs @@ -14,7 +14,7 @@ pub fn pubkey_to_validator_index( state .validators() .get(index) - .map_or(false, |v| v.pubkey == *pubkey) + .map_or(false, |v| *v.pubkey == *pubkey) }) .map(Result::Ok) .transpose() diff --git a/beacon_node/http_api/src/validator_inclusion.rs b/beacon_node/http_api/src/validator_inclusion.rs index dd4e137ce66..0a257725741 100644 --- a/beacon_node/http_api/src/validator_inclusion.rs +++ b/beacon_node/http_api/src/validator_inclusion.rs @@ -95,13 +95,13 @@ pub fn validator_inclusion_data( let summary = get_epoch_processing_summary(&mut state, &chain.spec)?; Ok(Some(ValidatorInclusionData { - is_slashed: validator.slashed, + is_slashed: validator.slashed(), is_withdrawable_in_current_epoch: validator.is_withdrawable_at(epoch), is_active_unslashed_in_current_epoch: summary .is_active_unslashed_in_current_epoch(validator_index), is_active_unslashed_in_previous_epoch: summary .is_active_unslashed_in_previous_epoch(validator_index), - current_epoch_effective_balance_gwei: validator.effective_balance, + current_epoch_effective_balance_gwei: validator.effective_balance(), is_current_epoch_target_attester: summary .is_current_epoch_target_attester(validator_index) .map_err(convert_cache_error)?, diff --git a/beacon_node/http_api/src/validators.rs b/beacon_node/http_api/src/validators.rs index 20af7a680df..69765d79199 100644 --- a/beacon_node/http_api/src/validators.rs +++ b/beacon_node/http_api/src/validators.rs @@ -29,7 +29,7 @@ pub fn get_beacon_state_validators( .filter(|(index, (validator, _))| { query_ids.as_ref().map_or(true, |ids| { ids.iter().any(|id| match id { - ValidatorId::PublicKey(pubkey) => &validator.pubkey == pubkey, + ValidatorId::PublicKey(pubkey) => validator.pubkey() == pubkey, ValidatorId::Index(param_index) => { *param_index == *index as u64 } @@ -93,7 +93,7 @@ pub fn get_beacon_state_validator_balances( .filter(|(index, (validator, _))| { optional_ids.map_or(true, |ids| { ids.iter().any(|id| match id { - ValidatorId::PublicKey(pubkey) => &validator.pubkey == pubkey, + ValidatorId::PublicKey(pubkey) => validator.pubkey() == pubkey, ValidatorId::Index(param_index) => { *param_index == *index as u64 } diff --git a/beacon_node/http_api/tests/fork_tests.rs b/beacon_node/http_api/tests/fork_tests.rs index 74b26475639..ad32ff1d579 100644 --- a/beacon_node/http_api/tests/fork_tests.rs +++ b/beacon_node/http_api/tests/fork_tests.rs @@ -128,17 +128,18 @@ async fn attestations_across_fork_with_skip_slots() { let all_validators = harness.get_all_validators(); let fork_slot = fork_epoch.start_slot(E::slots_per_epoch()); - let fork_state = harness + let mut fork_state = harness .chain .state_at_slot(fork_slot, StateSkipConfig::WithStateRoots) .unwrap(); + let fork_state_root = fork_state.update_tree_hash_cache().unwrap(); harness.set_current_slot(fork_slot); let attestations = harness.make_attestations( &all_validators, &fork_state, - fork_state.canonical_root(), + fork_state_root, (*fork_state.get_block_root(fork_slot - 1).unwrap()).into(), fork_slot, ); diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index e4580e4ffdb..8536f0265e3 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -806,7 +806,7 @@ impl ApiTester { let state_opt = state_id.state(&self.chain).ok(); let validators: Vec = match state_opt.as_ref() { Some((state, _execution_optimistic, _finalized)) => { - state.validators().clone().into() + state.validators().clone().to_vec() } None => vec![], }; @@ -822,7 +822,7 @@ impl ApiTester { ValidatorId::PublicKey( validators .get(i as usize) - .map_or(PublicKeyBytes::empty(), |val| val.pubkey.clone()), + .map_or(PublicKeyBytes::empty(), |val| *val.pubkey), ) }) .collect::>(); @@ -865,7 +865,7 @@ impl ApiTester { if i < state.balances().len() as u64 { validators.push(ValidatorBalanceData { index: i as u64, - balance: state.balances()[i as usize], + balance: *state.balances().get(i as usize).unwrap(), }); } } @@ -892,7 +892,7 @@ impl ApiTester { .ok() .map(|(state, _execution_optimistic, _finalized)| state); let validators: Vec = match state_opt.as_ref() { - Some(state) => state.validators().clone().into(), + Some(state) => state.validators().to_vec(), None => vec![], }; let validator_index_ids = validator_indices @@ -907,7 +907,7 @@ impl ApiTester { ValidatorId::PublicKey( validators .get(i as usize) - .map_or(PublicKeyBytes::empty(), |val| val.pubkey.clone()), + .map_or(PublicKeyBytes::empty(), |val| *val.pubkey), ) }) .collect::>(); @@ -955,7 +955,7 @@ impl ApiTester { if i >= state.validators().len() as u64 { continue; } - let validator = state.validators()[i as usize].clone(); + let validator = state.validators().get(i as usize).unwrap().clone(); let status = ValidatorStatus::from_validator( &validator, epoch, @@ -967,7 +967,7 @@ impl ApiTester { { validators.push(ValidatorData { index: i as u64, - balance: state.balances()[i as usize], + balance: *state.balances().get(i as usize).unwrap(), status, validator, }); @@ -995,13 +995,13 @@ impl ApiTester { .ok() .map(|(state, _execution_optimistic, _finalized)| state); let validators = match state_opt.as_ref() { - Some(state) => state.validators().clone().into(), + Some(state) => state.validators().to_vec(), None => vec![], }; for (i, validator) in validators.into_iter().enumerate() { let validator_ids = &[ - ValidatorId::PublicKey(validator.pubkey.clone()), + ValidatorId::PublicKey(*validator.pubkey), ValidatorId::Index(i as u64), ]; @@ -1025,7 +1025,7 @@ impl ApiTester { ValidatorData { index: i as u64, - balance: state.balances()[i], + balance: *state.balances().get(i).unwrap(), status: ValidatorStatus::from_validator( &validator, epoch, @@ -2360,7 +2360,7 @@ impl ApiTester { .unwrap() { let expected = AttesterData { - pubkey: state.validators()[i as usize].pubkey.clone().into(), + pubkey: *state.validators().get(i as usize).unwrap().pubkey, validator_index: i, committees_at_slot: duty.committees_at_slot, committee_index: duty.index, @@ -2465,7 +2465,7 @@ impl ApiTester { let index = state .get_beacon_proposer_index(slot, &self.chain.spec) .unwrap(); - let pubkey = state.validators()[index].pubkey.clone().into(); + let pubkey = *state.validators().get(index).unwrap().pubkey; ProposerData { pubkey, diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index f7bba900372..c9f8cb381c9 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -2295,7 +2295,7 @@ impl NetworkBeaconProcessor { debug!(self.log, "Attestation for finalized state"; "peer_id" => % peer_id); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); } - e @ AttnError::BeaconChainError(BeaconChainError::MaxCommitteePromises(_)) => { + AttnError::BeaconChainError(BeaconChainError::ShufflingCacheError(e)) => { debug!( self.log, "Dropping attestation"; diff --git a/beacon_node/operation_pool/src/lib.rs b/beacon_node/operation_pool/src/lib.rs index 03659bcee05..fee8a49c51d 100644 --- a/beacon_node/operation_pool/src/lib.rs +++ b/beacon_node/operation_pool/src/lib.rs @@ -392,7 +392,7 @@ impl OperationPool { && state .validators() .get(slashing.as_inner().signed_header_1.message.proposer_index as usize) - .map_or(false, |validator| !validator.slashed) + .map_or(false, |validator| !validator.slashed()) }, |slashing| slashing.as_inner().clone(), E::MaxProposerSlashings::to_usize(), @@ -451,7 +451,7 @@ impl OperationPool { pub fn prune_proposer_slashings(&self, head_state: &BeaconState) { prune_validator_hash_map( &mut self.proposer_slashings.write(), - |_, validator| validator.exit_epoch <= head_state.finalized_checkpoint().epoch, + |_, validator| validator.exit_epoch() <= head_state.finalized_checkpoint().epoch, head_state, ); } @@ -470,7 +470,7 @@ impl OperationPool { // // We cannot check the `slashed` field since the `head` is not finalized and // a fork could un-slash someone. - validator.exit_epoch > head_state.finalized_checkpoint().epoch + validator.exit_epoch() > head_state.finalized_checkpoint().epoch }) .map_or(false, |indices| !indices.is_empty()); @@ -527,7 +527,7 @@ impl OperationPool { // // We choose simplicity over the gain of pruning more exits since they are small and // should not be seen frequently. - |_, validator| validator.exit_epoch <= head_state.finalized_checkpoint().epoch, + |_, validator| validator.exit_epoch() <= head_state.finalized_checkpoint().epoch, head_state, ); } @@ -1272,7 +1272,12 @@ mod release_tests { // Each validator will have a multiple of 1_000_000_000 wei. // Safe from overflow unless there are about 18B validators (2^64 / 1_000_000_000). for i in 0..state.validators().len() { - state.validators_mut()[i].effective_balance = 1_000_000_000 * i as u64; + state + .validators_mut() + .get_mut(i) + .unwrap() + .mutable + .effective_balance = 1_000_000_000 * i as u64; } let num_validators = num_committees @@ -1530,9 +1535,24 @@ mod release_tests { let spec = &harness.spec; let mut state = harness.get_current_state(); let op_pool = OperationPool::::new(); - state.validators_mut()[1].effective_balance = 17_000_000_000; - state.validators_mut()[2].effective_balance = 17_000_000_000; - state.validators_mut()[3].effective_balance = 17_000_000_000; + state + .validators_mut() + .get_mut(1) + .unwrap() + .mutable + .effective_balance = 17_000_000_000; + state + .validators_mut() + .get_mut(2) + .unwrap() + .mutable + .effective_balance = 17_000_000_000; + state + .validators_mut() + .get_mut(3) + .unwrap() + .mutable + .effective_balance = 17_000_000_000; let slashing_1 = harness.make_attester_slashing(vec![1, 2, 3]); let slashing_2 = harness.make_attester_slashing(vec![4, 5, 6]); diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index d46eb0f403e..15b66612cde 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -592,9 +592,22 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { Arg::with_name("slots-per-restore-point") .long("slots-per-restore-point") .value_name("SLOT_COUNT") - .help("Specifies how often a freezer DB restore point should be stored. \ - Cannot be changed after initialization. \ - [default: 8192 (mainnet) or 64 (minimal)]") + .help("Deprecated.") + .takes_value(true) + ) + .arg( + Arg::with_name("hierarchy-exponents") + .long("hierarchy-exponents") + .value_name("EXPONENTS") + .help("Specifies the frequency for storing full state snapshots and hierarchical \ + diffs in the freezer DB. Accepts a comma-separated list of ascending \ + exponents. Each exponent defines an interval for storing diffs to the layer \ + above. The last exponent defines the interval for full snapshots. \ + For example, a config of '4,8,12' would store a full snapshot every \ + 4096 (2^12) slots, first-level diffs every 256 (2^8) slots, and second-level \ + diffs every 16 (2^4) slots. \ + Cannot be changed after initialization. \ + [default: 5,9,11,13,16,18,21]") .takes_value(true) ) .arg( @@ -722,7 +735,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .takes_value(true) ) /* - * Database purging and compaction. + * Database. */ .arg( Arg::with_name("purge-db") @@ -742,6 +755,29 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .takes_value(true) .default_value("true") ) + .arg( + Arg::with_name("state-cache-size") + .long("state-cache-size") + .value_name("SIZE") + .help("Specifies how many states the database should cache in memory [default: 128]") + .takes_value(true) + ) + .arg( + Arg::with_name("diff-buffer-cache-size") + .long("diff-buffer-cache-size") + .value_name("SIZE") + .help("The maximum number of diff buffers to hold in memory. This cache is used \ + when fetching historic states [default: 16]") + .takes_value(true) + ) + .arg( + Arg::with_name("compression-level") + .long("compression-level") + .value_name("LEVEL") + .help("Compression level (-99 to 22) for zstd compression applied to states on disk \ + [default: 1]. You may change the compression level freely without re-syncing.") + .takes_value(true) + ) .arg( Arg::with_name("prune-payloads") .long("prune-payloads") @@ -751,6 +787,16 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .takes_value(true) .default_value("true") ) + .arg( + Arg::with_name("epochs-per-state-diff") + .long("epochs-per-state-diff") + .value_name("EPOCHS") + .help("Number of epochs between state diffs stored in the database. Lower values \ + result in more writes and more data stored, while higher values result in \ + more block replaying and longer load times in case of cache miss.") + .default_value("16") + .takes_value(true) + ) .arg( Arg::with_name("prune-blobs") .long("prune-blobs") @@ -779,6 +825,17 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .takes_value(true) .default_value("0") ) + .arg( + Arg::with_name("parallel-state-cache-size") + .long("parallel-state-cache-size") + .value_name("N") + .help("Set the size of the cache used to de-duplicate requests for the same \ + state. This cache is additional to other state caches within Lighthouse \ + and should be kept small unless a large number of parallel requests for \ + different states are anticipated.") + .takes_value(true) + .default_value("2") + ) /* * Misc. @@ -1235,6 +1292,12 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .takes_value(true) .possible_values(&["fast", "disabled", "checked", "strict"]) ) + .arg( + Arg::with_name("unsafe-and-dangerous-mode") + .long("unsafe-and-dangerous-mode") + .help("Don't use this flag unless you know what you're doing. Go back and download a \ + stable Lighthouse release") + ) .arg( Arg::with_name("beacon-processor-max-workers") .long("beacon-processor-max-workers") diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 9b0032e3068..e08bfd0c2b5 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -16,7 +16,7 @@ use http_api::TlsConfig; use lighthouse_network::ListenAddress; use lighthouse_network::{multiaddr::Protocol, Enr, Multiaddr, NetworkConfig, PeerIdSerialized}; use sensitive_url::SensitiveUrl; -use slog::{info, warn, Logger}; +use slog::{crit, info, warn, Logger}; use std::cmp; use std::cmp::max; use std::fmt::Debug; @@ -171,9 +171,6 @@ pub fn get_config( if let Some(cache_size) = clap_utils::parse_optional(cli_args, "shuffling-cache-size")? { client_config.chain.shuffling_cache_size = cache_size; } - if let Some(cache_size) = clap_utils::parse_optional(cli_args, "state-cache-size")? { - client_config.chain.snapshot_cache_size = cache_size; - } /* * Prometheus metrics HTTP server @@ -386,18 +383,39 @@ pub fn get_config( client_config.freezer_db_path = Some(PathBuf::from(freezer_dir)); } + if !cli_args.is_present("unsafe-and-dangerous-mode") { + crit!( + log, + "This is an EXPERIMENTAL build of Lighthouse. If you are seeing this message you may \ + have downloaded the wrong version by mistake. If so, go back and download the latest \ + stable release. If you are certain that you want to continue, read the docs for the \ + latest experimental release and continue at your own risk." + ); + return Err("FATAL ERROR, YOU HAVE THE WRONG LIGHTHOUSE BINARY".into()); + } + if let Some(blobs_db_dir) = cli_args.value_of("blobs-dir") { client_config.blobs_db_path = Some(PathBuf::from(blobs_db_dir)); } - let (sprp, sprp_explicit) = get_slots_per_restore_point::(cli_args)?; - client_config.store.slots_per_restore_point = sprp; - client_config.store.slots_per_restore_point_set_explicitly = sprp_explicit; - - if let Some(block_cache_size) = cli_args.value_of("block-cache-size") { - client_config.store.block_cache_size = block_cache_size - .parse() - .map_err(|_| "block-cache-size is not a valid integer".to_string())?; + if let Some(block_cache_size) = clap_utils::parse_optional(cli_args, "block-cache-size")? { + client_config.store.block_cache_size = block_cache_size; + } + if let Some(state_cache_size) = clap_utils::parse_optional(cli_args, "state-cache-size")? { + client_config.store.state_cache_size = state_cache_size; + } + if let Some(parallel_state_cache_size) = + clap_utils::parse_optional(cli_args, "parallel-state-cache-size")? + { + client_config.chain.parallel_state_cache_size = parallel_state_cache_size; + } + if let Some(diff_buffer_cache_size) = + clap_utils::parse_optional(cli_args, "diff-buffer-cache-size")? + { + client_config.store.diff_buffer_cache_size = diff_buffer_cache_size; + } + if let Some(compression_level) = clap_utils::parse_optional(cli_args, "compression-level")? { + client_config.store.compression_level = compression_level; } if let Some(historic_state_cache_size) = cli_args.value_of("historic-state-cache-size") { @@ -417,6 +435,16 @@ pub fn get_config( client_config.store.prune_payloads = prune_payloads; } + if let Some(epochs_per_state_diff) = + clap_utils::parse_optional(cli_args, "epochs-per-state-diff")? + { + client_config.store.epochs_per_state_diff = epochs_per_state_diff; + } + + if let Some(hierarchy_config) = clap_utils::parse_optional(cli_args, "hierarchy-exponents")? { + client_config.store.hierarchy_config = hierarchy_config; + } + if let Some(epochs_per_migration) = clap_utils::parse_optional(cli_args, "epochs-per-migration")? { @@ -1464,25 +1492,6 @@ pub fn get_data_dir(cli_args: &ArgMatches) -> PathBuf { .unwrap_or_else(|| PathBuf::from(".")) } -/// Get the `slots_per_restore_point` value to use for the database. -/// -/// Return `(sprp, set_explicitly)` where `set_explicitly` is `true` if the user provided the value. -pub fn get_slots_per_restore_point( - cli_args: &ArgMatches, -) -> Result<(u64, bool), String> { - if let Some(slots_per_restore_point) = - clap_utils::parse_optional(cli_args, "slots-per-restore-point")? - { - Ok((slots_per_restore_point, true)) - } else { - let default = std::cmp::min( - E::slots_per_historical_root() as u64, - store::config::DEFAULT_SLOTS_PER_RESTORE_POINT, - ); - Ok((default, false)) - } -} - /// Parses the `cli_value` as a comma-separated string of values to be parsed with `parser`. /// /// If there is more than one value, log a warning. If there are no values, return an error. diff --git a/beacon_node/src/lib.rs b/beacon_node/src/lib.rs index ee782c650e2..e6e0799d621 100644 --- a/beacon_node/src/lib.rs +++ b/beacon_node/src/lib.rs @@ -13,7 +13,7 @@ use beacon_chain::{ use clap::ArgMatches; pub use cli::cli_app; pub use client::{Client, ClientBuilder, ClientConfig, ClientGenesis}; -pub use config::{get_config, get_data_dir, get_slots_per_restore_point, set_network_config}; +pub use config::{get_config, get_data_dir, set_network_config}; use environment::RuntimeContext; pub use eth2_config::Eth2Config; use slasher::{DatabaseBackendOverride, Slasher}; diff --git a/beacon_node/store/Cargo.toml b/beacon_node/store/Cargo.toml index 7bf1ef76bef..288d167b419 100644 --- a/beacon_node/store/Cargo.toml +++ b/beacon_node/store/Cargo.toml @@ -25,3 +25,9 @@ lru = { workspace = true } sloggers = { workspace = true } directory = { workspace = true } strum = { workspace = true } +xdelta3 = { workspace = true } +zstd = { workspace = true } +safe_arith = { workspace = true } +bls = { workspace = true } +smallvec = { workspace = true } +logging = { workspace = true } diff --git a/beacon_node/store/src/chunk_writer.rs b/beacon_node/store/src/chunk_writer.rs deleted file mode 100644 index 059b812e74c..00000000000 --- a/beacon_node/store/src/chunk_writer.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::chunked_vector::{chunk_key, Chunk, ChunkError, Field}; -use crate::{Error, KeyValueStore, KeyValueStoreOp}; -use types::EthSpec; - -/// Buffered writer for chunked vectors (block roots mainly). -pub struct ChunkWriter<'a, F, E, S> -where - F: Field, - E: EthSpec, - S: KeyValueStore, -{ - /// Buffered chunk awaiting writing to disk (always dirty). - chunk: Chunk, - /// Chunk index of `chunk`. - index: usize, - store: &'a S, -} - -impl<'a, F, E, S> ChunkWriter<'a, F, E, S> -where - F: Field, - E: EthSpec, - S: KeyValueStore, -{ - pub fn new(store: &'a S, vindex: usize) -> Result { - let chunk_index = F::chunk_index(vindex); - let chunk = Chunk::load(store, F::column(), &chunk_key(chunk_index))? - .unwrap_or_else(|| Chunk::new(vec![F::Value::default(); F::chunk_size()])); - - Ok(Self { - chunk, - index: chunk_index, - store, - }) - } - - /// Set the value at a given vector index, writing the current chunk and moving on if necessary. - pub fn set( - &mut self, - vindex: usize, - value: F::Value, - batch: &mut Vec, - ) -> Result<(), Error> { - let chunk_index = F::chunk_index(vindex); - - // Advance to the next chunk. - if chunk_index != self.index { - self.write(batch)?; - *self = Self::new(self.store, vindex)?; - } - - let i = vindex % F::chunk_size(); - let existing_value = &self.chunk.values[i]; - - if existing_value == &value || existing_value == &F::Value::default() { - self.chunk.values[i] = value; - Ok(()) - } else { - Err(ChunkError::Inconsistent { - field: F::column(), - chunk_index, - existing_value: format!("{:?}", existing_value), - new_value: format!("{:?}", value), - } - .into()) - } - } - - /// Write the current chunk to disk. - /// - /// Should be called before the writer is dropped, in order to write the final chunk to disk. - pub fn write(&self, batch: &mut Vec) -> Result<(), Error> { - self.chunk.store(F::column(), &chunk_key(self.index), batch) - } -} diff --git a/beacon_node/store/src/chunked_iter.rs b/beacon_node/store/src/chunked_iter.rs deleted file mode 100644 index b3322b5225d..00000000000 --- a/beacon_node/store/src/chunked_iter.rs +++ /dev/null @@ -1,123 +0,0 @@ -use crate::chunked_vector::{chunk_key, Chunk, Field}; -use crate::{HotColdDB, ItemStore}; -use slog::error; -use types::{ChainSpec, EthSpec, Slot}; - -/// Iterator over the values of a `BeaconState` vector field (like `block_roots`). -/// -/// Uses the freezer DB's separate table to load the values. -pub struct ChunkedVectorIter<'a, F, E, Hot, Cold> -where - F: Field, - E: EthSpec, - Hot: ItemStore, - Cold: ItemStore, -{ - pub(crate) store: &'a HotColdDB, - current_vindex: usize, - pub(crate) end_vindex: usize, - next_cindex: usize, - current_chunk: Chunk, -} - -impl<'a, F, E, Hot, Cold> ChunkedVectorIter<'a, F, E, Hot, Cold> -where - F: Field, - E: EthSpec, - Hot: ItemStore, - Cold: ItemStore, -{ - /// Create a new iterator which can yield elements from `start_vindex` up to the last - /// index stored by the restore point at `last_restore_point_slot`. - /// - /// The `freezer_upper_limit` slot should be the slot of a recent restore point as obtained from - /// `Root::freezer_upper_limit`. We pass it as a parameter so that the caller can - /// maintain a stable view of the database (see `HybridForwardsBlockRootsIterator`). - pub fn new( - store: &'a HotColdDB, - start_vindex: usize, - freezer_upper_limit: Slot, - spec: &ChainSpec, - ) -> Self { - let (_, end_vindex) = F::start_and_end_vindex(freezer_upper_limit, spec); - - // Set the next chunk to the one containing `start_vindex`. - let next_cindex = start_vindex / F::chunk_size(); - // Set the current chunk to the empty chunk, it will never be read. - let current_chunk = Chunk::default(); - - Self { - store, - current_vindex: start_vindex, - end_vindex, - next_cindex, - current_chunk, - } - } -} - -impl<'a, F, E, Hot, Cold> Iterator for ChunkedVectorIter<'a, F, E, Hot, Cold> -where - F: Field, - E: EthSpec, - Hot: ItemStore, - Cold: ItemStore, -{ - type Item = (usize, F::Value); - - fn next(&mut self) -> Option { - let chunk_size = F::chunk_size(); - - // Range exhausted, return `None` forever. - if self.current_vindex >= self.end_vindex { - None - } - // Value lies in the current chunk, return it. - else if self.current_vindex < self.next_cindex * chunk_size { - let vindex = self.current_vindex; - let val = self - .current_chunk - .values - .get(vindex % chunk_size) - .cloned() - .or_else(|| { - error!( - self.store.log, - "Missing chunk value in forwards iterator"; - "vector index" => vindex - ); - None - })?; - self.current_vindex += 1; - Some((vindex, val)) - } - // Need to load the next chunk, load it and recurse back into the in-range case. - else { - self.current_chunk = Chunk::load( - &self.store.cold_db, - F::column(), - &chunk_key(self.next_cindex), - ) - .map_err(|e| { - error!( - self.store.log, - "Database error in forwards iterator"; - "chunk index" => self.next_cindex, - "error" => format!("{:?}", e) - ); - e - }) - .ok()? - .or_else(|| { - error!( - self.store.log, - "Missing chunk in forwards iterator"; - "chunk index" => self.next_cindex - ); - None - })?; - self.next_cindex += 1; - self.next() - } - } -} diff --git a/beacon_node/store/src/chunked_vector.rs b/beacon_node/store/src/chunked_vector.rs deleted file mode 100644 index a0c50e5a2b5..00000000000 --- a/beacon_node/store/src/chunked_vector.rs +++ /dev/null @@ -1,884 +0,0 @@ -//! Space-efficient storage for `BeaconState` vector fields. -//! -//! This module provides logic for splitting the `FixedVector` fields of a `BeaconState` into -//! chunks, and storing those chunks in contiguous ranges in the on-disk database. The motiviation -//! for doing this is avoiding massive duplication in every on-disk state. For example, rather than -//! storing the whole `historical_roots` vector, which is updated once every couple of thousand -//! slots, at every slot, we instead store all the historical values as a chunked vector on-disk, -//! and fetch only the slice we need when reconstructing the `historical_roots` of a state. -//! -//! ## Terminology -//! -//! * **Chunk size**: the number of vector values stored per on-disk chunk. -//! * **Vector index** (vindex): index into all the historical values, identifying a single element -//! of the vector being stored. -//! * **Chunk index** (cindex): index into the keyspace of the on-disk database, identifying a chunk -//! of elements. To find the chunk index of a vector index: `cindex = vindex / chunk_size`. -use self::UpdatePattern::*; -use crate::*; -use ssz::{Decode, Encode}; -use types::historical_summary::HistoricalSummary; - -/// Description of how a `BeaconState` field is updated during state processing. -/// -/// When storing a state, this allows us to efficiently store only those entries -/// which are not present in the DB already. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum UpdatePattern { - /// The value is updated once per `n` slots. - OncePerNSlots { - n: u64, - /// The slot at which the field begins to accumulate values. - /// - /// The field should not be read or written until `activation_slot` is reached, and the - /// activation slot should act as an offset when converting slots to vector indices. - activation_slot: Option, - /// The slot at which the field ceases to accumulate values. - /// - /// If this is `None` then the field is continually updated. - deactivation_slot: Option, - }, - /// The value is updated once per epoch, for the epoch `current_epoch - lag`. - OncePerEpoch { lag: u64 }, -} - -/// Map a chunk index to bytes that can be used to key the NoSQL database. -/// -/// We shift chunks up by 1 to make room for a genesis chunk that is handled separately. -pub fn chunk_key(cindex: usize) -> [u8; 8] { - (cindex as u64 + 1).to_be_bytes() -} - -/// Return the database key for the genesis value. -fn genesis_value_key() -> [u8; 8] { - 0u64.to_be_bytes() -} - -/// Trait for types representing fields of the `BeaconState`. -/// -/// All of the required methods are type-level, because we do most things with fields at the -/// type-level. We require their value-level witnesses to be `Copy` so that we can avoid the -/// turbofish when calling functions like `store_updated_vector`. -pub trait Field: Copy { - /// The type of value stored in this field: the `T` from `FixedVector`. - /// - /// The `Default` impl will be used to fill extra vector entries. - type Value: Decode + Encode + Default + Clone + PartialEq + std::fmt::Debug; - - /// The length of this field: the `N` from `FixedVector`. - type Length: Unsigned; - - /// The database column where the integer-indexed chunks for this field should be stored. - /// - /// Each field's column **must** be unique. - fn column() -> DBColumn; - - /// Update pattern for this field, so that we can do differential updates. - fn update_pattern(spec: &ChainSpec) -> UpdatePattern; - - /// The number of values to store per chunk on disk. - /// - /// Default is 128 so that we read/write 4K pages when the values are 32 bytes. - // TODO: benchmark and optimise this parameter - fn chunk_size() -> usize { - 128 - } - - /// Convert a v-index (vector index) to a chunk index. - fn chunk_index(vindex: usize) -> usize { - vindex / Self::chunk_size() - } - - /// Get the value of this field at the given vector index, from the state. - fn get_value( - state: &BeaconState, - vindex: u64, - spec: &ChainSpec, - ) -> Result; - - /// True if this is a `FixedLengthField`, false otherwise. - fn is_fixed_length() -> bool; - - /// Compute the start and end vector indices of the slice of history required at `current_slot`. - /// - /// ## Example - /// - /// If we have a field that is updated once per epoch, then the end vindex will be - /// `current_epoch + 1`, because we want to include the value for the current epoch, and the - /// start vindex will be `end_vindex - Self::Length`, because that's how far back we can look. - fn start_and_end_vindex(current_slot: Slot, spec: &ChainSpec) -> (usize, usize) { - // We take advantage of saturating subtraction on slots and epochs - match Self::update_pattern(spec) { - OncePerNSlots { - n, - activation_slot, - deactivation_slot, - } => { - // Per-slot changes exclude the index for the current slot, because - // it won't be set until the slot completes (think of `state_roots`, `block_roots`). - // This also works for the `historical_roots` because at the `n`th slot, the 0th - // entry of the list is created, and before that the list is empty. - // - // To account for the switch from historical roots to historical summaries at - // Capella we also modify the current slot by the activation and deactivation slots. - // The activation slot acts as an offset (subtraction) while the deactivation slot - // acts as a clamp (min). - let slot_with_clamp = deactivation_slot.map_or(current_slot, |deactivation_slot| { - std::cmp::min(current_slot, deactivation_slot) - }); - let slot_with_clamp_and_offset = if let Some(activation_slot) = activation_slot { - slot_with_clamp - activation_slot - } else { - // Return (0, 0) to indicate that the field should not be read/written. - return (0, 0); - }; - let end_vindex = slot_with_clamp_and_offset / n; - let start_vindex = end_vindex - Self::Length::to_u64(); - (start_vindex.as_usize(), end_vindex.as_usize()) - } - OncePerEpoch { lag } => { - // Per-epoch changes include the index for the current epoch, because it - // will have been set at the most recent epoch boundary. - let current_epoch = current_slot.epoch(E::slots_per_epoch()); - let end_epoch = current_epoch + 1 - lag; - let start_epoch = end_epoch + lag - Self::Length::to_u64(); - (start_epoch.as_usize(), end_epoch.as_usize()) - } - } - } - - /// Given an `existing_chunk` stored in the DB, construct an updated chunk to replace it. - fn get_updated_chunk( - existing_chunk: &Chunk, - chunk_index: usize, - start_vindex: usize, - end_vindex: usize, - state: &BeaconState, - spec: &ChainSpec, - ) -> Result, Error> { - let chunk_size = Self::chunk_size(); - let mut new_chunk = Chunk::new(vec![Self::Value::default(); chunk_size]); - - for i in 0..chunk_size { - let vindex = chunk_index * chunk_size + i; - if vindex >= start_vindex && vindex < end_vindex { - let vector_value = Self::get_value(state, vindex as u64, spec)?; - - if let Some(existing_value) = existing_chunk.values.get(i) { - if *existing_value != vector_value && *existing_value != Self::Value::default() - { - return Err(ChunkError::Inconsistent { - field: Self::column(), - chunk_index, - existing_value: format!("{:?}", existing_value), - new_value: format!("{:?}", vector_value), - } - .into()); - } - } - - new_chunk.values[i] = vector_value; - } else { - new_chunk.values[i] = existing_chunk.values.get(i).cloned().unwrap_or_default(); - } - } - - Ok(new_chunk) - } - - /// Determine whether a state at `slot` possesses (or requires) the genesis value. - fn slot_needs_genesis_value(slot: Slot, spec: &ChainSpec) -> bool { - let (_, end_vindex) = Self::start_and_end_vindex(slot, spec); - match Self::update_pattern(spec) { - // If the end_vindex is less than the length of the vector, then the vector - // has not yet been completely filled with non-genesis values, and so the genesis - // value is still required. - OncePerNSlots { .. } => { - Self::is_fixed_length() && end_vindex < Self::Length::to_usize() - } - // If the field has lag, then it takes an extra `lag` vindices beyond the - // `end_vindex` before the vector has been filled with non-genesis values. - OncePerEpoch { lag } => { - Self::is_fixed_length() && end_vindex + (lag as usize) < Self::Length::to_usize() - } - } - } - - /// Load the genesis value for a fixed length field from the store. - /// - /// This genesis value should be used to fill the initial state of the vector. - fn load_genesis_value>(store: &S) -> Result { - let key = &genesis_value_key()[..]; - let chunk = - Chunk::load(store, Self::column(), key)?.ok_or(ChunkError::MissingGenesisValue)?; - chunk - .values - .first() - .cloned() - .ok_or_else(|| ChunkError::MissingGenesisValue.into()) - } - - /// Store the given `value` as the genesis value for this field, unless stored already. - /// - /// Check the existing value (if any) for consistency with the value we intend to store, and - /// return an error if they are inconsistent. - fn check_and_store_genesis_value>( - store: &S, - value: Self::Value, - ops: &mut Vec, - ) -> Result<(), Error> { - let key = &genesis_value_key()[..]; - - if let Some(existing_chunk) = Chunk::::load(store, Self::column(), key)? { - if existing_chunk.values.len() != 1 { - Err(ChunkError::InvalidGenesisChunk { - field: Self::column(), - expected_len: 1, - observed_len: existing_chunk.values.len(), - } - .into()) - } else if existing_chunk.values[0] != value { - Err(ChunkError::InconsistentGenesisValue { - field: Self::column(), - existing_value: format!("{:?}", existing_chunk.values[0]), - new_value: format!("{:?}", value), - } - .into()) - } else { - Ok(()) - } - } else { - let chunk = Chunk::new(vec![value]); - chunk.store(Self::column(), &genesis_value_key()[..], ops)?; - Ok(()) - } - } - - /// Extract the genesis value for a fixed length field from an - /// - /// Will only return a correct value if `slot_needs_genesis_value(state.slot(), spec) == true`. - fn extract_genesis_value( - state: &BeaconState, - spec: &ChainSpec, - ) -> Result { - let (_, end_vindex) = Self::start_and_end_vindex(state.slot(), spec); - match Self::update_pattern(spec) { - // Genesis value is guaranteed to exist at `end_vindex`, as it won't yet have been - // updated - OncePerNSlots { .. } => Ok(Self::get_value(state, end_vindex as u64, spec)?), - // If there's lag, the value of the field at the vindex *without the lag* - // should still be set to the genesis value. - OncePerEpoch { lag } => Ok(Self::get_value(state, end_vindex as u64 + lag, spec)?), - } - } -} - -/// Marker trait for fixed-length fields (`FixedVector`). -pub trait FixedLengthField: Field {} - -/// Marker trait for variable-length fields (`VariableList`). -pub trait VariableLengthField: Field {} - -/// Macro to implement the `Field` trait on a new unit struct type. -macro_rules! field { - ($struct_name:ident, $marker_trait:ident, $value_ty:ty, $length_ty:ty, $column:expr, - $update_pattern:expr, $get_value:expr) => { - #[derive(Clone, Copy)] - pub struct $struct_name; - - impl Field for $struct_name - where - E: EthSpec, - { - type Value = $value_ty; - type Length = $length_ty; - - fn column() -> DBColumn { - $column - } - - fn update_pattern(spec: &ChainSpec) -> UpdatePattern { - let update_pattern = $update_pattern; - update_pattern(spec) - } - - fn get_value( - state: &BeaconState, - vindex: u64, - spec: &ChainSpec, - ) -> Result { - let get_value = $get_value; - get_value(state, vindex, spec) - } - - fn is_fixed_length() -> bool { - stringify!($marker_trait) == "FixedLengthField" - } - } - - impl $marker_trait for $struct_name {} - }; -} - -field!( - BlockRoots, - FixedLengthField, - Hash256, - E::SlotsPerHistoricalRoot, - DBColumn::BeaconBlockRoots, - |_| OncePerNSlots { - n: 1, - activation_slot: Some(Slot::new(0)), - deactivation_slot: None - }, - |state: &BeaconState<_>, index, _| safe_modulo_index(state.block_roots(), index) -); - -field!( - StateRoots, - FixedLengthField, - Hash256, - E::SlotsPerHistoricalRoot, - DBColumn::BeaconStateRoots, - |_| OncePerNSlots { - n: 1, - activation_slot: Some(Slot::new(0)), - deactivation_slot: None, - }, - |state: &BeaconState<_>, index, _| safe_modulo_index(state.state_roots(), index) -); - -field!( - HistoricalRoots, - VariableLengthField, - Hash256, - E::HistoricalRootsLimit, - DBColumn::BeaconHistoricalRoots, - |spec: &ChainSpec| OncePerNSlots { - n: E::SlotsPerHistoricalRoot::to_u64(), - activation_slot: Some(Slot::new(0)), - deactivation_slot: spec - .capella_fork_epoch - .map(|fork_epoch| fork_epoch.start_slot(E::slots_per_epoch())), - }, - |state: &BeaconState<_>, index, _| safe_modulo_index(state.historical_roots(), index) -); - -field!( - RandaoMixes, - FixedLengthField, - Hash256, - E::EpochsPerHistoricalVector, - DBColumn::BeaconRandaoMixes, - |_| OncePerEpoch { lag: 1 }, - |state: &BeaconState<_>, index, _| safe_modulo_index(state.randao_mixes(), index) -); - -field!( - HistoricalSummaries, - VariableLengthField, - HistoricalSummary, - E::HistoricalRootsLimit, - DBColumn::BeaconHistoricalSummaries, - |spec: &ChainSpec| OncePerNSlots { - n: E::SlotsPerHistoricalRoot::to_u64(), - activation_slot: spec - .capella_fork_epoch - .map(|fork_epoch| fork_epoch.start_slot(E::slots_per_epoch())), - deactivation_slot: None, - }, - |state: &BeaconState<_>, index, _| safe_modulo_index( - state - .historical_summaries() - .map_err(|_| ChunkError::InvalidFork)?, - index - ) -); - -pub fn store_updated_vector, E: EthSpec, S: KeyValueStore>( - field: F, - store: &S, - state: &BeaconState, - spec: &ChainSpec, - ops: &mut Vec, -) -> Result<(), Error> { - let chunk_size = F::chunk_size(); - let (start_vindex, end_vindex) = F::start_and_end_vindex(state.slot(), spec); - let start_cindex = start_vindex / chunk_size; - let end_cindex = end_vindex / chunk_size; - - // Store the genesis value if we have access to it, and it hasn't been stored already. - if F::slot_needs_genesis_value(state.slot(), spec) { - let genesis_value = F::extract_genesis_value(state, spec)?; - F::check_and_store_genesis_value(store, genesis_value, ops)?; - } - - // Start by iterating backwards from the last chunk, storing new chunks in the database. - // Stop once a chunk in the database matches what we were about to store, this indicates - // that a previously stored state has already filled-in a portion of the indices covered. - let full_range_checked = store_range( - field, - (start_cindex..=end_cindex).rev(), - start_vindex, - end_vindex, - store, - state, - spec, - ops, - )?; - - // If the previous `store_range` did not check the entire range, it may be the case that the - // state's vector includes elements at low vector indices that are not yet stored in the - // database, so run another `store_range` to ensure these values are also stored. - if !full_range_checked { - store_range( - field, - start_cindex..end_cindex, - start_vindex, - end_vindex, - store, - state, - spec, - ops, - )?; - } - - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -fn store_range( - _: F, - range: I, - start_vindex: usize, - end_vindex: usize, - store: &S, - state: &BeaconState, - spec: &ChainSpec, - ops: &mut Vec, -) -> Result -where - F: Field, - E: EthSpec, - S: KeyValueStore, - I: Iterator, -{ - for chunk_index in range { - let chunk_key = &chunk_key(chunk_index)[..]; - - let existing_chunk = - Chunk::::load(store, F::column(), chunk_key)?.unwrap_or_default(); - - let new_chunk = F::get_updated_chunk( - &existing_chunk, - chunk_index, - start_vindex, - end_vindex, - state, - spec, - )?; - - if new_chunk == existing_chunk { - return Ok(false); - } - - new_chunk.store(F::column(), chunk_key, ops)?; - } - - Ok(true) -} - -// Chunks at the end index are included. -// TODO: could be more efficient with a real range query (perhaps RocksDB) -fn range_query, E: EthSpec, T: Decode + Encode>( - store: &S, - column: DBColumn, - start_index: usize, - end_index: usize, -) -> Result>, Error> { - let range = start_index..=end_index; - let len = range - .end() - // Add one to account for inclusive range. - .saturating_add(1) - .saturating_sub(*range.start()); - let mut result = Vec::with_capacity(len); - - for chunk_index in range { - let key = &chunk_key(chunk_index)[..]; - let chunk = Chunk::load(store, column, key)?.ok_or(ChunkError::Missing { chunk_index })?; - result.push(chunk); - } - - Ok(result) -} - -/// Combine chunks to form a list or vector of all values with vindex in `start_vindex..end_vindex`. -/// -/// The `length` parameter is the length of the vec to construct, with entries set to `default` if -/// they lie outside the vindex range. -fn stitch( - chunks: Vec>, - start_vindex: usize, - end_vindex: usize, - chunk_size: usize, - length: usize, - default: T, -) -> Result, ChunkError> { - if start_vindex + length < end_vindex { - return Err(ChunkError::OversizedRange { - start_vindex, - end_vindex, - length, - }); - } - - let start_cindex = start_vindex / chunk_size; - let end_cindex = end_vindex / chunk_size; - - let mut result = vec![default; length]; - - for (chunk_index, chunk) in (start_cindex..=end_cindex).zip(chunks.into_iter()) { - // All chunks but the last chunk must be full-sized - if chunk_index != end_cindex && chunk.values.len() != chunk_size { - return Err(ChunkError::InvalidSize { - chunk_index, - expected: chunk_size, - actual: chunk.values.len(), - }); - } - - // Copy the chunk entries into the result vector - for (i, value) in chunk.values.into_iter().enumerate() { - let vindex = chunk_index * chunk_size + i; - - if vindex >= start_vindex && vindex < end_vindex { - result[vindex % length] = value; - } - } - } - - Ok(result) -} - -pub fn load_vector_from_db, E: EthSpec, S: KeyValueStore>( - store: &S, - slot: Slot, - spec: &ChainSpec, -) -> Result, Error> { - // Do a range query - let chunk_size = F::chunk_size(); - let (start_vindex, end_vindex) = F::start_and_end_vindex(slot, spec); - let start_cindex = start_vindex / chunk_size; - let end_cindex = end_vindex / chunk_size; - - let chunks = range_query(store, F::column(), start_cindex, end_cindex)?; - - let default = if F::slot_needs_genesis_value(slot, spec) { - F::load_genesis_value(store)? - } else { - F::Value::default() - }; - - let result = stitch( - chunks, - start_vindex, - end_vindex, - chunk_size, - F::Length::to_usize(), - default, - )?; - - Ok(result.into()) -} - -/// The historical roots are stored in vector chunks, despite not actually being a vector. -pub fn load_variable_list_from_db, E: EthSpec, S: KeyValueStore>( - store: &S, - slot: Slot, - spec: &ChainSpec, -) -> Result, Error> { - let chunk_size = F::chunk_size(); - let (start_vindex, end_vindex) = F::start_and_end_vindex(slot, spec); - let start_cindex = start_vindex / chunk_size; - let end_cindex = end_vindex / chunk_size; - - let chunks: Vec> = range_query(store, F::column(), start_cindex, end_cindex)?; - - let mut result = Vec::with_capacity(chunk_size * chunks.len()); - - for (chunk_index, chunk) in chunks.into_iter().enumerate() { - for (i, value) in chunk.values.into_iter().enumerate() { - let vindex = chunk_index * chunk_size + i; - - if vindex >= start_vindex && vindex < end_vindex { - result.push(value); - } - } - } - - Ok(result.into()) -} - -/// Index into a field of the state, avoiding out of bounds and division by 0. -fn safe_modulo_index(values: &[T], index: u64) -> Result { - if values.is_empty() { - Err(ChunkError::ZeroLengthVector) - } else { - Ok(values[index as usize % values.len()]) - } -} - -/// A chunk of a fixed-size vector from the `BeaconState`, stored in the database. -#[derive(Debug, Clone, PartialEq)] -pub struct Chunk { - /// A vector of up-to `chunk_size` values. - pub values: Vec, -} - -impl Default for Chunk -where - T: Decode + Encode, -{ - fn default() -> Self { - Chunk { values: vec![] } - } -} - -impl Chunk -where - T: Decode + Encode, -{ - pub fn new(values: Vec) -> Self { - Chunk { values } - } - - pub fn load, E: EthSpec>( - store: &S, - column: DBColumn, - key: &[u8], - ) -> Result, Error> { - store - .get_bytes(column.into(), key)? - .map(|bytes| Self::decode(&bytes)) - .transpose() - } - - pub fn store( - &self, - column: DBColumn, - key: &[u8], - ops: &mut Vec, - ) -> Result<(), Error> { - let db_key = get_key_for_col(column.into(), key); - ops.push(KeyValueStoreOp::PutKeyValue(db_key, self.encode()?)); - Ok(()) - } - - /// Attempt to decode a single chunk. - pub fn decode(bytes: &[u8]) -> Result { - if !::is_ssz_fixed_len() { - return Err(Error::from(ChunkError::InvalidType)); - } - - let value_size = ::ssz_fixed_len(); - - if value_size == 0 { - return Err(Error::from(ChunkError::InvalidType)); - } - - let values = bytes - .chunks(value_size) - .map(T::from_ssz_bytes) - .collect::>()?; - - Ok(Chunk { values }) - } - - pub fn encoded_size(&self) -> usize { - self.values.len() * ::ssz_fixed_len() - } - - /// Encode a single chunk as bytes. - pub fn encode(&self) -> Result, Error> { - if !::is_ssz_fixed_len() { - return Err(Error::from(ChunkError::InvalidType)); - } - - Ok(self.values.iter().flat_map(T::as_ssz_bytes).collect()) - } -} - -#[derive(Debug, PartialEq)] -pub enum ChunkError { - ZeroLengthVector, - InvalidSize { - chunk_index: usize, - expected: usize, - actual: usize, - }, - Missing { - chunk_index: usize, - }, - MissingGenesisValue, - Inconsistent { - field: DBColumn, - chunk_index: usize, - existing_value: String, - new_value: String, - }, - InconsistentGenesisValue { - field: DBColumn, - existing_value: String, - new_value: String, - }, - InvalidGenesisChunk { - field: DBColumn, - expected_len: usize, - observed_len: usize, - }, - InvalidType, - OversizedRange { - start_vindex: usize, - end_vindex: usize, - length: usize, - }, - InvalidFork, -} - -#[cfg(test)] -mod test { - use super::*; - use types::MainnetEthSpec as TestSpec; - use types::*; - - fn v(i: u64) -> Hash256 { - Hash256::from_low_u64_be(i) - } - - #[test] - fn stitch_default() { - let chunk_size = 4; - - let chunks = vec![ - Chunk::new(vec![0u64, 1, 2, 3]), - Chunk::new(vec![4, 5, 0, 0]), - ]; - - assert_eq!( - stitch(chunks, 2, 6, chunk_size, 12, 99).unwrap(), - vec![99, 99, 2, 3, 4, 5, 99, 99, 99, 99, 99, 99] - ); - } - - #[test] - fn stitch_basic() { - let chunk_size = 4; - let default = v(0); - - let chunks = vec![ - Chunk::new(vec![v(0), v(1), v(2), v(3)]), - Chunk::new(vec![v(4), v(5), v(6), v(7)]), - Chunk::new(vec![v(8), v(9), v(10), v(11)]), - ]; - - assert_eq!( - stitch(chunks.clone(), 0, 12, chunk_size, 12, default).unwrap(), - (0..12).map(v).collect::>() - ); - - assert_eq!( - stitch(chunks, 2, 10, chunk_size, 8, default).unwrap(), - vec![v(8), v(9), v(2), v(3), v(4), v(5), v(6), v(7)] - ); - } - - #[test] - fn stitch_oversized_range() { - let chunk_size = 4; - let default = 0; - - let chunks = vec![Chunk::new(vec![20u64, 21, 22, 23])]; - - // Args (start_vindex, end_vindex, length) - let args = vec![(0, 21, 20), (0, 2048, 1024), (0, 2, 1)]; - - for (start_vindex, end_vindex, length) in args { - assert_eq!( - stitch( - chunks.clone(), - start_vindex, - end_vindex, - chunk_size, - length, - default - ), - Err(ChunkError::OversizedRange { - start_vindex, - end_vindex, - length, - }) - ); - } - } - - #[test] - fn fixed_length_fields() { - fn test_fixed_length>(_: F, expected: bool) { - assert_eq!(F::is_fixed_length(), expected); - } - test_fixed_length(BlockRoots, true); - test_fixed_length(StateRoots, true); - test_fixed_length(HistoricalRoots, false); - test_fixed_length(RandaoMixes, true); - } - - fn needs_genesis_value_once_per_slot>(_: F) { - let spec = &TestSpec::default_spec(); - let max = F::Length::to_u64(); - for i in 0..max { - assert!( - F::slot_needs_genesis_value(Slot::new(i), spec), - "slot {}", - i - ); - } - assert!(!F::slot_needs_genesis_value(Slot::new(max), spec)); - } - - #[test] - fn needs_genesis_value_block_roots() { - needs_genesis_value_once_per_slot(BlockRoots); - } - - #[test] - fn needs_genesis_value_state_roots() { - needs_genesis_value_once_per_slot(StateRoots); - } - - #[test] - fn needs_genesis_value_historical_roots() { - let spec = &TestSpec::default_spec(); - assert!( - !>::slot_needs_genesis_value(Slot::new(0), spec) - ); - } - - fn needs_genesis_value_test_randao>(_: F) { - let spec = &TestSpec::default_spec(); - let max = TestSpec::slots_per_epoch() * (F::Length::to_u64() - 1); - for i in 0..max { - assert!( - F::slot_needs_genesis_value(Slot::new(i), spec), - "slot {}", - i - ); - } - assert!(!F::slot_needs_genesis_value(Slot::new(max), spec)); - } - - #[test] - fn needs_genesis_value_randao() { - needs_genesis_value_test_randao(RandaoMixes); - } -} diff --git a/beacon_node/store/src/config.rs b/beacon_node/store/src/config.rs index 681d424e282..4fef9e16537 100644 --- a/beacon_node/store/src/config.rs +++ b/beacon_node/store/src/config.rs @@ -1,14 +1,21 @@ -use crate::{DBColumn, Error, StoreItem}; +use crate::hdiff::HierarchyConfig; +use crate::{AnchorInfo, DBColumn, Error, Split, StoreItem}; use serde::{Deserialize, Serialize}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; +use std::io::Write; use std::num::NonZeroUsize; use types::non_zero_usize::new_non_zero_usize; -use types::{EthSpec, MinimalEthSpec}; +use types::{EthSpec, Unsigned}; +use zstd::Encoder; -pub const PREV_DEFAULT_SLOTS_PER_RESTORE_POINT: u64 = 2048; -pub const DEFAULT_SLOTS_PER_RESTORE_POINT: u64 = 8192; -pub const DEFAULT_BLOCK_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(5); +// Only used in tests. Mainnet sets a higher default on the CLI. +pub const DEFAULT_EPOCHS_PER_STATE_DIFF: u64 = 8; +pub const DEFAULT_BLOCK_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(64); +pub const DEFAULT_STATE_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(128); +pub const DEFAULT_COMPRESSION_LEVEL: i32 = 1; +pub const DEFAULT_DIFF_BUFFER_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(16); +const EST_COMPRESSION_FACTOR: usize = 2; pub const DEFAULT_HISTORIC_STATE_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(1); pub const DEFAULT_EPOCHS_PER_BLOB_PRUNE: u64 = 1; pub const DEFAULT_BLOB_PUNE_MARGIN_EPOCHS: u64 = 0; @@ -16,12 +23,16 @@ pub const DEFAULT_BLOB_PUNE_MARGIN_EPOCHS: u64 = 0; /// Database configuration parameters. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct StoreConfig { - /// Number of slots to wait between storing restore points in the freezer database. - pub slots_per_restore_point: u64, - /// Flag indicating whether the `slots_per_restore_point` was set explicitly by the user. - pub slots_per_restore_point_set_explicitly: bool, + /// Number of epochs between state diffs in the hot database. + pub epochs_per_state_diff: u64, /// Maximum number of blocks to store in the in-memory block cache. pub block_cache_size: NonZeroUsize, + /// Maximum number of states to store in the in-memory state cache. + pub state_cache_size: NonZeroUsize, + /// Compression level for blocks, state diffs and other compressed values. + pub compression_level: i32, + /// Maximum number of `HDiffBuffer`s to store in memory. + pub diff_buffer_cache_size: NonZeroUsize, /// Maximum number of states from freezer database to store in the in-memory state cache. pub historic_state_cache_size: NonZeroUsize, /// Whether to compact the database on initialization. @@ -30,6 +41,12 @@ pub struct StoreConfig { pub compact_on_prune: bool, /// Whether to prune payloads on initialization and finalization. pub prune_payloads: bool, + /// Whether to store finalized blocks compressed and linearised in the freezer database. + pub linear_blocks: bool, + /// Whether to store finalized states compressed and linearised in the freezer database. + pub linear_restore_points: bool, + /// State diff hierarchy. + pub hierarchy_config: HierarchyConfig, /// Whether to prune blobs older than the blob data availability boundary. pub prune_blobs: bool, /// Frequency of blob pruning in epochs. Default: 1 (every epoch). @@ -41,26 +58,47 @@ pub struct StoreConfig { /// Variant of `StoreConfig` that gets written to disk. Contains immutable configuration params. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +// FIXME(sproul): schema migration pub struct OnDiskStoreConfig { - pub slots_per_restore_point: u64, + pub linear_blocks: bool, + pub hierarchy_config: HierarchyConfig, } #[derive(Debug, Clone)] pub enum StoreConfigError { - MismatchedSlotsPerRestorePoint { config: u64, on_disk: u64 }, + MismatchedSlotsPerRestorePoint { + config: u64, + on_disk: u64, + }, + InvalidCompressionLevel { + level: i32, + }, + IncompatibleStoreConfig { + config: OnDiskStoreConfig, + on_disk: OnDiskStoreConfig, + }, + InvalidEpochsPerStateDiff { + epochs_per_state_diff: u64, + max_supported: u64, + }, + ZeroEpochsPerBlobPrune, } impl Default for StoreConfig { fn default() -> Self { Self { - // Safe default for tests, shouldn't ever be read by a CLI node. - slots_per_restore_point: MinimalEthSpec::slots_per_historical_root() as u64, - slots_per_restore_point_set_explicitly: false, + epochs_per_state_diff: DEFAULT_EPOCHS_PER_STATE_DIFF, block_cache_size: DEFAULT_BLOCK_CACHE_SIZE, + state_cache_size: DEFAULT_STATE_CACHE_SIZE, + diff_buffer_cache_size: DEFAULT_DIFF_BUFFER_CACHE_SIZE, + compression_level: DEFAULT_COMPRESSION_LEVEL, historic_state_cache_size: DEFAULT_HISTORIC_STATE_CACHE_SIZE, compact_on_init: false, compact_on_prune: true, prune_payloads: true, + linear_blocks: true, + linear_restore_points: true, + hierarchy_config: HierarchyConfig::default(), prune_blobs: true, epochs_per_blob_prune: DEFAULT_EPOCHS_PER_BLOB_PRUNE, blob_prune_margin_epochs: DEFAULT_BLOB_PUNE_MARGIN_EPOCHS, @@ -71,21 +109,102 @@ impl Default for StoreConfig { impl StoreConfig { pub fn as_disk_config(&self) -> OnDiskStoreConfig { OnDiskStoreConfig { - slots_per_restore_point: self.slots_per_restore_point, + linear_blocks: self.linear_blocks, + hierarchy_config: self.hierarchy_config.clone(), } } pub fn check_compatibility( &self, on_disk_config: &OnDiskStoreConfig, + split: &Split, + anchor: Option<&AnchorInfo>, ) -> Result<(), StoreConfigError> { - if self.slots_per_restore_point != on_disk_config.slots_per_restore_point { - return Err(StoreConfigError::MismatchedSlotsPerRestorePoint { - config: self.slots_per_restore_point, - on_disk: on_disk_config.slots_per_restore_point, - }); + let db_config = self.as_disk_config(); + // Allow changing the hierarchy exponents if no historic states are stored. + if db_config.linear_blocks == on_disk_config.linear_blocks + && (db_config.hierarchy_config == on_disk_config.hierarchy_config + || anchor.map_or(false, |anchor| anchor.no_historic_states_stored(split.slot))) + { + Ok(()) + } else { + Err(StoreConfigError::IncompatibleStoreConfig { + config: db_config, + on_disk: on_disk_config.clone(), + }) } - Ok(()) + } + + /// Check that the configuration is valid. + pub fn verify(&self) -> Result<(), StoreConfigError> { + self.verify_compression_level()?; + self.verify_epochs_per_blob_prune()?; + self.verify_epochs_per_state_diff::() + } + + /// Check that the compression level is valid. + fn verify_compression_level(&self) -> Result<(), StoreConfigError> { + if zstd::compression_level_range().contains(&self.compression_level) { + Ok(()) + } else { + Err(StoreConfigError::InvalidCompressionLevel { + level: self.compression_level, + }) + } + } + + /// Check that the configuration is valid. + pub fn verify_epochs_per_state_diff(&self) -> Result<(), StoreConfigError> { + // To build state diffs we need to be able to determine the previous state root from the + // state itself, which requires reading back in the state_roots array. + let max_supported = E::SlotsPerHistoricalRoot::to_u64() / E::slots_per_epoch(); + if self.epochs_per_state_diff <= max_supported { + Ok(()) + } else { + Err(StoreConfigError::InvalidEpochsPerStateDiff { + epochs_per_state_diff: self.epochs_per_state_diff, + max_supported, + }) + } + } + + /// Check that epochs_per_blob_prune is at least 1 epoch to avoid attempting to prune the same + /// epochs over and over again. + fn verify_epochs_per_blob_prune(&self) -> Result<(), StoreConfigError> { + if self.epochs_per_blob_prune > 0 { + Ok(()) + } else { + Err(StoreConfigError::ZeroEpochsPerBlobPrune) + } + } + + /// Estimate the size of `len` bytes after compression at the current compression level. + pub fn estimate_compressed_size(&self, len: usize) -> usize { + if self.compression_level == 0 { + len + } else { + len / EST_COMPRESSION_FACTOR + } + } + + /// Estimate the size of `len` compressed bytes after decompression at the current compression + /// level. + pub fn estimate_decompressed_size(&self, len: usize) -> usize { + if self.compression_level == 0 { + len + } else { + len * EST_COMPRESSION_FACTOR + } + } + + pub fn compress_bytes(&self, ssz_bytes: &[u8]) -> Result, Error> { + let mut compressed_value = + Vec::with_capacity(self.estimate_compressed_size(ssz_bytes.len())); + let mut encoder = Encoder::new(&mut compressed_value, self.compression_level) + .map_err(Error::Compression)?; + encoder.write_all(ssz_bytes).map_err(Error::Compression)?; + encoder.finish().map_err(Error::Compression)?; + Ok(compressed_value) } } @@ -102,3 +221,85 @@ impl StoreItem for OnDiskStoreConfig { Ok(Self::from_ssz_bytes(bytes)?) } } + +#[cfg(test)] +mod test { + use super::*; + use crate::{metadata::STATE_UPPER_LIMIT_NO_RETAIN, AnchorInfo, Split}; + use types::{Hash256, Slot}; + + #[test] + fn check_compatibility_ok() { + let store_config = StoreConfig { + linear_blocks: true, + ..Default::default() + }; + let on_disk_config = OnDiskStoreConfig { + linear_blocks: true, + hierarchy_config: store_config.hierarchy_config.clone(), + }; + let split = Split::default(); + assert!(store_config + .check_compatibility(&on_disk_config, &split, None) + .is_ok()); + } + + #[test] + fn check_compatibility_linear_blocks_mismatch() { + let store_config = StoreConfig { + linear_blocks: true, + ..Default::default() + }; + let on_disk_config = OnDiskStoreConfig { + linear_blocks: false, + hierarchy_config: store_config.hierarchy_config.clone(), + }; + let split = Split::default(); + assert!(store_config + .check_compatibility(&on_disk_config, &split, None) + .is_err()); + } + + #[test] + fn check_compatibility_hierarchy_config_incompatible() { + let store_config = StoreConfig { + linear_blocks: true, + ..Default::default() + }; + let on_disk_config = OnDiskStoreConfig { + linear_blocks: true, + hierarchy_config: HierarchyConfig { + exponents: vec![5, 8, 11, 13, 16, 18, 21], + }, + }; + let split = Split::default(); + assert!(store_config + .check_compatibility(&on_disk_config, &split, None) + .is_err()); + } + + #[test] + fn check_compatibility_hierarchy_config_update() { + let store_config = StoreConfig { + linear_blocks: true, + ..Default::default() + }; + let on_disk_config = OnDiskStoreConfig { + linear_blocks: true, + hierarchy_config: HierarchyConfig { + exponents: vec![5, 8, 11, 13, 16, 18, 21], + }, + }; + let split = Split::default(); + let anchor = AnchorInfo { + anchor_slot: Slot::new(0), + oldest_block_slot: Slot::new(0), + oldest_block_parent: Hash256::zero(), + state_upper_limit: STATE_UPPER_LIMIT_NO_RETAIN, + state_lower_limit: Slot::new(0), + }; + assert!(store_config + .check_compatibility(&on_disk_config, &split, Some(&anchor)) + .is_ok()); + } +} diff --git a/beacon_node/store/src/errors.rs b/beacon_node/store/src/errors.rs index 96e02b80ff8..b7eaac054f6 100644 --- a/beacon_node/store/src/errors.rs +++ b/beacon_node/store/src/errors.rs @@ -1,16 +1,15 @@ -use crate::chunked_vector::ChunkError; use crate::config::StoreConfigError; +use crate::hdiff; use crate::hot_cold_store::HotColdDBError; use ssz::DecodeError; use state_processing::BlockReplayError; -use types::{BeaconStateError, Hash256, InconsistentFork, Slot}; +use types::{milhouse, BeaconStateError, Epoch, EpochCacheError, Hash256, InconsistentFork, Slot}; pub type Result = std::result::Result; #[derive(Debug)] pub enum Error { SszDecodeError(DecodeError), - VectorChunkError(ChunkError), BeaconStateError(BeaconStateError), PartialBeaconStateError, HotColdDBError(HotColdDBError), @@ -42,13 +41,40 @@ pub enum Error { expected: Hash256, computed: Hash256, }, + MissingStateRoot(Slot), + MissingState(Hash256), + MissingSnapshot(Slot), + MissingDiff(Epoch), + NoBaseStateFound(Hash256), BlockReplayError(BlockReplayError), + MilhouseError(milhouse::Error), + Compression(std::io::Error), + MissingPersistedBeaconChain, + SlotIsBeforeSplit { + slot: Slot, + }, + FinalizedStateDecreasingSlot, + FinalizedStateUnaligned, + StateForCacheHasPendingUpdates { + state_root: Hash256, + slot: Slot, + }, AddPayloadLogicError, SlotClockUnavailableForMigration, + MissingImmutableValidator(usize), + MissingValidator(usize), + V9MigrationFailure(Hash256), + ValidatorPubkeyCacheError(String), + DuplicateValidatorPublicKey, + InvalidValidatorPubkeyBytes(bls::Error), + ValidatorPubkeyCacheUninitialized, InvalidKey, InvalidBytes, UnableToDowngrade, + Hdiff(hdiff::Error), InconsistentFork(InconsistentFork), + ZeroCacheSize, + CacheBuildError(EpochCacheError), } pub trait HandleUnavailable { @@ -71,12 +97,6 @@ impl From for Error { } } -impl From for Error { - fn from(e: ChunkError) -> Error { - Error::VectorChunkError(e) - } -} - impl From for Error { fn from(e: HotColdDBError) -> Error { Error::HotColdDBError(e) @@ -101,6 +121,18 @@ impl From for Error { } } +impl From for Error { + fn from(e: milhouse::Error) -> Self { + Self::MilhouseError(e) + } +} + +impl From for Error { + fn from(e: hdiff::Error) -> Self { + Self::Hdiff(e) + } +} + impl From for Error { fn from(e: BlockReplayError) -> Error { Error::BlockReplayError(e) @@ -113,6 +145,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: EpochCacheError) -> Error { + Error::CacheBuildError(e) + } +} + #[derive(Debug)] pub struct DBError { pub message: String, diff --git a/beacon_node/store/src/forwards_iter.rs b/beacon_node/store/src/forwards_iter.rs index 1ccf1da1b7c..7b827ad9569 100644 --- a/beacon_node/store/src/forwards_iter.rs +++ b/beacon_node/store/src/forwards_iter.rs @@ -1,37 +1,34 @@ -use crate::chunked_iter::ChunkedVectorIter; -use crate::chunked_vector::{BlockRoots, Field, StateRoots}; use crate::errors::{Error, Result}; use crate::iter::{BlockRootsIterator, StateRootsIterator}; -use crate::{HotColdDB, ItemStore}; +use crate::{ColumnIter, DBColumn, HotColdDB, ItemStore}; use itertools::process_results; -use types::{BeaconState, ChainSpec, EthSpec, Hash256, Slot}; +use std::marker::PhantomData; +use types::{BeaconState, EthSpec, Hash256, Slot}; pub type HybridForwardsBlockRootsIterator<'a, E, Hot, Cold> = - HybridForwardsIterator<'a, E, BlockRoots, Hot, Cold>; + HybridForwardsIterator<'a, E, Hot, Cold>; pub type HybridForwardsStateRootsIterator<'a, E, Hot, Cold> = - HybridForwardsIterator<'a, E, StateRoots, Hot, Cold>; + HybridForwardsIterator<'a, E, Hot, Cold>; -/// Trait unifying `BlockRoots` and `StateRoots` for forward iteration. -pub trait Root: Field { - fn simple_forwards_iterator, Cold: ItemStore>( - store: &HotColdDB, +impl, Cold: ItemStore> HotColdDB { + pub fn simple_forwards_iterator( + &self, + column: DBColumn, start_slot: Slot, end_state: BeaconState, end_root: Hash256, - ) -> Result; - - /// The first slot for which this field is *no longer* stored in the freezer database. - /// - /// If `None`, then this field is not stored in the freezer database at all due to pruning - /// configuration. - fn freezer_upper_limit, Cold: ItemStore>( - store: &HotColdDB, - ) -> Option; -} + ) -> Result { + if column == DBColumn::BeaconBlockRoots { + self.forwards_iter_block_roots_using_state(start_slot, end_state, end_root) + } else if column == DBColumn::BeaconStateRoots { + self.forwards_iter_state_roots_using_state(start_slot, end_state, end_root) + } else { + panic!("FIXME(sproul): better error") + } + } -impl Root for BlockRoots { - fn simple_forwards_iterator, Cold: ItemStore>( - store: &HotColdDB, + pub fn forwards_iter_block_roots_using_state( + &self, start_slot: Slot, end_state: BeaconState, end_block_root: Hash256, @@ -39,7 +36,7 @@ impl Root for BlockRoots { // Iterate backwards from the end state, stopping at the start slot. let values = process_results( std::iter::once(Ok((end_block_root, end_state.slot()))) - .chain(BlockRootsIterator::owned(store, end_state)), + .chain(BlockRootsIterator::owned(self, end_state)), |iter| { iter.take_while(|(_, slot)| *slot >= start_slot) .collect::>() @@ -48,17 +45,8 @@ impl Root for BlockRoots { Ok(SimpleForwardsIterator { values }) } - fn freezer_upper_limit, Cold: ItemStore>( - store: &HotColdDB, - ) -> Option { - // Block roots are stored for all slots up to the split slot (exclusive). - Some(store.get_split_slot()) - } -} - -impl Root for StateRoots { - fn simple_forwards_iterator, Cold: ItemStore>( - store: &HotColdDB, + pub fn forwards_iter_state_roots_using_state( + &self, start_slot: Slot, end_state: BeaconState, end_state_root: Hash256, @@ -66,7 +54,7 @@ impl Root for StateRoots { // Iterate backwards from the end state, stopping at the start slot. let values = process_results( std::iter::once(Ok((end_state_root, end_state.slot()))) - .chain(StateRootsIterator::owned(store, end_state)), + .chain(StateRootsIterator::owned(self, end_state)), |iter| { iter.take_while(|(_, slot)| *slot >= start_slot) .collect::>() @@ -75,51 +63,92 @@ impl Root for StateRoots { Ok(SimpleForwardsIterator { values }) } - fn freezer_upper_limit, Cold: ItemStore>( - store: &HotColdDB, - ) -> Option { - // State roots are stored for all slots up to the latest restore point (exclusive). - // There may not be a latest restore point if state pruning is enabled, in which - // case this function will return `None`. - store.get_latest_restore_point_slot() + fn freezer_upper_limit(&self, column: DBColumn) -> Option { + let split_slot = self.get_split_slot(); + if column == DBColumn::BeaconBlockRoots { + // Block roots are available up to the split slot. + Some(split_slot) + } else if column == DBColumn::BeaconStateRoots { + let anchor_info = self.get_anchor_info(); + // There are no historic states stored if the state upper limit lies in the hot + // database. It hasn't been reached yet, and may never be. + if anchor_info.as_ref().map_or(false, |a| { + a.state_upper_limit >= split_slot && a.state_lower_limit == 0 + }) { + None + } else if let Some(lower_limit) = anchor_info + .map(|a| a.state_lower_limit) + .filter(|limit| *limit > 0) + { + Some(lower_limit) + } else { + // Otherwise if the state upper limit lies in the freezer or all states are + // reconstructed then state roots are available up to the split slot. + Some(split_slot) + } + } else { + None + } } } /// Forwards root iterator that makes use of a flat field table in the freezer DB. -pub struct FrozenForwardsIterator<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> -{ - inner: ChunkedVectorIter<'a, F, E, Hot, Cold>, +pub struct FrozenForwardsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { + inner: ColumnIter<'a, Vec>, + next_slot: Slot, + end_slot: Slot, + _phantom: PhantomData<(E, Hot, Cold)>, } -impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> - FrozenForwardsIterator<'a, E, F, Hot, Cold> +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> + FrozenForwardsIterator<'a, E, Hot, Cold> { + /// `end_slot` is EXCLUSIVE here. pub fn new( store: &'a HotColdDB, + column: DBColumn, start_slot: Slot, - last_restore_point_slot: Slot, - spec: &ChainSpec, + end_slot: Slot, ) -> Self { + if column != DBColumn::BeaconBlockRoots && column != DBColumn::BeaconStateRoots { + panic!("FIXME(sproul): bad column error"); + } + let start = start_slot.as_u64().to_be_bytes(); Self { - inner: ChunkedVectorIter::new( - store, - start_slot.as_usize(), - last_restore_point_slot, - spec, - ), + inner: store.cold_db.iter_column_from(column, &start), + next_slot: start_slot, + end_slot, + _phantom: PhantomData, } } } -impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> Iterator - for FrozenForwardsIterator<'a, E, F, Hot, Cold> +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> Iterator + for FrozenForwardsIterator<'a, E, Hot, Cold> { - type Item = (Hash256, Slot); + type Item = Result<(Hash256, Slot)>; fn next(&mut self) -> Option { + if self.next_slot == self.end_slot { + return None; + } + self.inner - .next() - .map(|(slot, root)| (root, Slot::from(slot))) + .next()? + .and_then(|(slot_bytes, root_bytes)| { + if slot_bytes.len() != 8 || root_bytes.len() != 32 { + Err(Error::InvalidBytes) + } else { + let slot = Slot::new(u64::from_be_bytes(slot_bytes.try_into().unwrap())); + let root = Hash256::from_slice(&root_bytes); + + assert_eq!(slot, self.next_slot); + self.next_slot += 1; + + Ok(Some((root, slot))) + } + }) + .transpose() } } @@ -139,17 +168,20 @@ impl Iterator for SimpleForwardsIterator { } /// Fusion of the above two approaches to forwards iteration. Fast and efficient. -pub enum HybridForwardsIterator<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> { +pub enum HybridForwardsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { PreFinalization { - iter: Box>, + iter: Box>, + store: &'a HotColdDB, end_slot: Option, /// Data required by the `PostFinalization` iterator when we get to it. continuation_data: Option, Hash256)>>, + column: DBColumn, }, PostFinalizationLazy { continuation_data: Option, Hash256)>>, store: &'a HotColdDB, start_slot: Slot, + column: DBColumn, }, PostFinalization { iter: SimpleForwardsIterator, @@ -157,8 +189,8 @@ pub enum HybridForwardsIterator<'a, E: EthSpec, F: Root, Hot: ItemStore, C Finished, } -impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> - HybridForwardsIterator<'a, E, F, Hot, Cold> +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> + HybridForwardsIterator<'a, E, Hot, Cold> { /// Construct a new hybrid iterator. /// @@ -174,23 +206,23 @@ impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> /// function may block for some time while `get_state` runs. pub fn new( store: &'a HotColdDB, + column: DBColumn, start_slot: Slot, end_slot: Option, get_state: impl FnOnce() -> Result<(BeaconState, Hash256)>, - spec: &ChainSpec, ) -> Result { use HybridForwardsIterator::*; // First slot at which this field is *not* available in the freezer. i.e. all slots less // than this slot have their data available in the freezer. - let freezer_upper_limit = F::freezer_upper_limit(store).unwrap_or(Slot::new(0)); + let freezer_upper_limit = store.freezer_upper_limit(column).unwrap_or(Slot::new(0)); let result = if start_slot < freezer_upper_limit { let iter = Box::new(FrozenForwardsIterator::new( store, + column, start_slot, freezer_upper_limit, - spec, )); // No continuation data is needed if the forwards iterator plans to halt before @@ -204,14 +236,17 @@ impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> }; PreFinalization { iter, + store, end_slot, continuation_data, + column, } } else { PostFinalizationLazy { continuation_data: Some(Box::new(get_state()?)), store, start_slot, + column, } }; @@ -224,30 +259,32 @@ impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> match self { PreFinalization { iter, + store, end_slot, continuation_data, + column, } => { match iter.next() { - Some(x) => Ok(Some(x)), + Some(x) => x.map(Some), // Once the pre-finalization iterator is consumed, transition // to a post-finalization iterator beginning from the last slot // of the pre iterator. None => { // If the iterator has an end slot (inclusive) which has already been // covered by the (exclusive) frozen forwards iterator, then we're done! - let iter_end_slot = Slot::from(iter.inner.end_vindex); - if end_slot.map_or(false, |end_slot| iter_end_slot == end_slot + 1) { + if end_slot.map_or(false, |end_slot| iter.end_slot == end_slot + 1) { *self = Finished; return Ok(None); } let continuation_data = continuation_data.take(); - let store = iter.inner.store; - let start_slot = iter_end_slot; + let start_slot = iter.end_slot; + *self = PostFinalizationLazy { continuation_data, store, start_slot, + column: *column, }; self.do_next() @@ -258,11 +295,17 @@ impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> continuation_data, store, start_slot, + column, } => { let (end_state, end_root) = *continuation_data.take().ok_or(Error::NoContinuationData)?; *self = PostFinalization { - iter: F::simple_forwards_iterator(store, *start_slot, end_state, end_root)?, + iter: store.simple_forwards_iterator( + *column, + *start_slot, + end_state, + end_root, + )?, }; self.do_next() } @@ -272,8 +315,8 @@ impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> } } -impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> Iterator - for HybridForwardsIterator<'a, E, F, Hot, Cold> +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> Iterator + for HybridForwardsIterator<'a, E, Hot, Cold> { type Item = Result<(Hash256, Slot)>; diff --git a/beacon_node/store/src/hdiff.rs b/beacon_node/store/src/hdiff.rs new file mode 100644 index 00000000000..962b602a51b --- /dev/null +++ b/beacon_node/store/src/hdiff.rs @@ -0,0 +1,403 @@ +//! Hierarchical diff implementation. +use crate::{DBColumn, StoreItem}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use ssz::{Decode, Encode}; +use ssz_derive::{Decode, Encode}; +use std::io::{Read, Write}; +use std::str::FromStr; +use types::{BeaconState, ChainSpec, EthSpec, List, Slot}; +use zstd::{Decoder, Encoder}; + +#[derive(Debug)] +pub enum Error { + InvalidHierarchy, + U64DiffDeletionsNotSupported, + UnableToComputeDiff, + UnableToApplyDiff, + Compression(std::io::Error), +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)] +pub struct HierarchyConfig { + pub exponents: Vec, +} + +impl FromStr for HierarchyConfig { + type Err = String; + + fn from_str(s: &str) -> Result { + let exponents = s + .split(',') + .map(|s| { + s.parse() + .map_err(|e| format!("invalid hierarchy-exponents: {e:?}")) + }) + .collect::, _>>()?; + + if exponents.windows(2).any(|w| w[0] >= w[1]) { + return Err("hierarchy-exponents must be in ascending order".to_string()); + } + + Ok(HierarchyConfig { exponents }) + } +} + +#[derive(Debug)] +pub struct HierarchyModuli { + moduli: Vec, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum StorageStrategy { + ReplayFrom(Slot), + DiffFrom(Slot), + Snapshot, +} + +/// Hierarchical diff output and working buffer. +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct HDiffBuffer { + state: Vec, + balances: Vec, +} + +/// Hierarchical state diff. +#[derive(Debug, Encode, Decode)] +pub struct HDiff { + state_diff: BytesDiff, + balances_diff: CompressedU64Diff, +} + +#[derive(Debug, Encode, Decode)] +pub struct BytesDiff { + bytes: Vec, +} + +#[derive(Debug, Encode, Decode)] +pub struct CompressedU64Diff { + bytes: Vec, +} + +impl HDiffBuffer { + pub fn from_state(mut beacon_state: BeaconState) -> Self { + let balances_list = std::mem::take(beacon_state.balances_mut()); + + let state = beacon_state.as_ssz_bytes(); + let balances = balances_list.to_vec(); + + HDiffBuffer { state, balances } + } + + pub fn into_state(self, spec: &ChainSpec) -> Result, Error> { + let mut state = BeaconState::from_ssz_bytes(&self.state, spec).unwrap(); + *state.balances_mut() = List::new(self.balances).unwrap(); + Ok(state) + } +} + +impl HDiff { + pub fn compute(source: &HDiffBuffer, target: &HDiffBuffer) -> Result { + let state_diff = BytesDiff::compute(&source.state, &target.state)?; + let balances_diff = CompressedU64Diff::compute(&source.balances, &target.balances)?; + + Ok(Self { + state_diff, + balances_diff, + }) + } + + pub fn apply(&self, source: &mut HDiffBuffer) -> Result<(), Error> { + let source_state = std::mem::take(&mut source.state); + self.state_diff.apply(&source_state, &mut source.state)?; + + self.balances_diff.apply(&mut source.balances)?; + Ok(()) + } + + pub fn state_diff_len(&self) -> usize { + self.state_diff.bytes.len() + } + + pub fn balances_diff_len(&self) -> usize { + self.balances_diff.bytes.len() + } +} + +impl StoreItem for HDiff { + fn db_column() -> DBColumn { + DBColumn::BeaconStateDiff + } + + fn as_store_bytes(&self) -> Vec { + self.as_ssz_bytes() + } + + fn from_store_bytes(bytes: &[u8]) -> Result { + Ok(Self::from_ssz_bytes(bytes)?) + } +} + +impl BytesDiff { + pub fn compute(source: &[u8], target: &[u8]) -> Result { + Self::compute_xdelta(source, target) + } + + pub fn compute_xdelta(source_bytes: &[u8], target_bytes: &[u8]) -> Result { + let bytes = + xdelta3::encode(target_bytes, source_bytes).ok_or(Error::UnableToComputeDiff)?; + Ok(Self { bytes }) + } + + pub fn apply(&self, source: &[u8], target: &mut Vec) -> Result<(), Error> { + self.apply_xdelta(source, target) + } + + pub fn apply_xdelta(&self, source: &[u8], target: &mut Vec) -> Result<(), Error> { + *target = xdelta3::decode(&self.bytes, source).ok_or(Error::UnableToApplyDiff)?; + Ok(()) + } +} + +impl CompressedU64Diff { + pub fn compute(xs: &[u64], ys: &[u64]) -> Result { + if xs.len() > ys.len() { + return Err(Error::U64DiffDeletionsNotSupported); + } + + let uncompressed_bytes: Vec = ys + .iter() + .enumerate() + .flat_map(|(i, y)| { + // Diff from 0 if the entry is new. + let x = xs.get(i).copied().unwrap_or(0); + y.wrapping_sub(x).to_be_bytes() + }) + .collect(); + + // FIXME(sproul): reconsider + let compression_level = 1; + let mut compressed_bytes = Vec::with_capacity(uncompressed_bytes.len() / 2); + let mut encoder = + Encoder::new(&mut compressed_bytes, compression_level).map_err(Error::Compression)?; + encoder + .write_all(&uncompressed_bytes) + .map_err(Error::Compression)?; + encoder.finish().map_err(Error::Compression)?; + + Ok(CompressedU64Diff { + bytes: compressed_bytes, + }) + } + + pub fn apply(&self, xs: &mut Vec) -> Result<(), Error> { + // Decompress balances diff. + let mut balances_diff_bytes = Vec::with_capacity(2 * self.bytes.len()); + let mut decoder = Decoder::new(&*self.bytes).map_err(Error::Compression)?; + decoder + .read_to_end(&mut balances_diff_bytes) + .map_err(Error::Compression)?; + + for (i, diff_bytes) in balances_diff_bytes + .chunks(u64::BITS as usize / 8) + .enumerate() + { + // FIXME(sproul): unwrap + let diff = u64::from_be_bytes(diff_bytes.try_into().unwrap()); + + if let Some(x) = xs.get_mut(i) { + *x = x.wrapping_add(diff); + } else { + xs.push(diff); + } + } + + Ok(()) + } +} + +impl Default for HierarchyConfig { + fn default() -> Self { + HierarchyConfig { + exponents: vec![5, 9, 11, 13, 16, 18, 21], + } + } +} + +impl HierarchyConfig { + pub fn to_moduli(&self) -> Result { + self.validate()?; + let moduli = self.exponents.iter().map(|n| 1 << n).collect(); + Ok(HierarchyModuli { moduli }) + } + + pub fn validate(&self) -> Result<(), Error> { + if self.exponents.len() > 2 + && self + .exponents + .iter() + .tuple_windows() + .all(|(small, big)| small < big && *big < u64::BITS as u8) + { + Ok(()) + } else { + Err(Error::InvalidHierarchy) + } + } +} + +impl HierarchyModuli { + pub fn storage_strategy(&self, slot: Slot) -> Result { + let last = self.moduli.last().copied().ok_or(Error::InvalidHierarchy)?; + let first = self + .moduli + .first() + .copied() + .ok_or(Error::InvalidHierarchy)?; + let replay_from = slot / first * first; + + if slot % last == 0 { + return Ok(StorageStrategy::Snapshot); + } + + let diff_from = self + .moduli + .iter() + .rev() + .tuple_windows() + .find_map(|(&n_big, &n_small)| { + (slot % n_small == 0).then(|| { + // Diff from the previous layer. + slot / n_big * n_big + }) + }); + + Ok(diff_from.map_or( + StorageStrategy::ReplayFrom(replay_from), + StorageStrategy::DiffFrom, + )) + } + + /// Return the smallest slot greater than or equal to `slot` at which a full snapshot should + /// be stored. + pub fn next_snapshot_slot(&self, slot: Slot) -> Result { + let last = self.moduli.last().copied().ok_or(Error::InvalidHierarchy)?; + if slot % last == 0 { + Ok(slot) + } else { + Ok((slot / last + 1) * last) + } + } + + /// Return `true` if the database ops for this slot should be committed immediately. + /// + /// This is the case for all diffs in the 2nd lowest layer and above, which are required by diffs + /// in the 1st layer. + pub fn should_commit_immediately(&self, slot: Slot) -> Result { + // If there's only 1 layer of snapshots, then commit only when writing a snapshot. + self.moduli.get(1).map_or_else( + || Ok(slot == self.next_snapshot_slot(slot)?), + |second_layer_moduli| Ok(slot % *second_layer_moduli == 0), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_storage_strategy() { + let config = HierarchyConfig::default(); + config.validate().unwrap(); + + let moduli = config.to_moduli().unwrap(); + + // Full snapshots at multiples of 2^21. + let snapshot_freq = Slot::new(1 << 21); + assert_eq!( + moduli.storage_strategy(Slot::new(0)).unwrap(), + StorageStrategy::Snapshot + ); + assert_eq!( + moduli.storage_strategy(snapshot_freq).unwrap(), + StorageStrategy::Snapshot + ); + assert_eq!( + moduli.storage_strategy(snapshot_freq * 3).unwrap(), + StorageStrategy::Snapshot + ); + + // Diffs should be from the previous layer (the snapshot in this case), and not the previous diff in the same layer. + let first_layer = Slot::new(1 << 18); + assert_eq!( + moduli.storage_strategy(first_layer * 2).unwrap(), + StorageStrategy::DiffFrom(Slot::new(0)) + ); + + let replay_strategy_slot = first_layer + 1; + assert_eq!( + moduli.storage_strategy(replay_strategy_slot).unwrap(), + StorageStrategy::ReplayFrom(first_layer) + ); + } + + #[test] + fn next_snapshot_slot() { + let config = HierarchyConfig::default(); + config.validate().unwrap(); + + let moduli = config.to_moduli().unwrap(); + let snapshot_freq = Slot::new(1 << 21); + + assert_eq!( + moduli.next_snapshot_slot(snapshot_freq).unwrap(), + snapshot_freq + ); + assert_eq!( + moduli.next_snapshot_slot(snapshot_freq + 1).unwrap(), + snapshot_freq * 2 + ); + assert_eq!( + moduli.next_snapshot_slot(snapshot_freq * 2 - 1).unwrap(), + snapshot_freq * 2 + ); + assert_eq!( + moduli.next_snapshot_slot(snapshot_freq * 2).unwrap(), + snapshot_freq * 2 + ); + assert_eq!( + moduli.next_snapshot_slot(snapshot_freq * 100).unwrap(), + snapshot_freq * 100 + ); + } + + #[test] + fn compressed_u64_vs_bytes_diff() { + let x_values = vec![99u64, 55, 123, 6834857, 0, 12]; + let y_values = vec![98u64, 55, 312, 1, 1, 2, 4, 5]; + + let to_bytes = + |nums: &[u64]| -> Vec { nums.iter().flat_map(|x| x.to_be_bytes()).collect() }; + + let x_bytes = to_bytes(&x_values); + let y_bytes = to_bytes(&y_values); + + let u64_diff = CompressedU64Diff::compute(&x_values, &y_values).unwrap(); + + let mut y_from_u64_diff = x_values; + u64_diff.apply(&mut y_from_u64_diff).unwrap(); + + assert_eq!(y_values, y_from_u64_diff); + + let bytes_diff = BytesDiff::compute(&x_bytes, &y_bytes).unwrap(); + + let mut y_from_bytes = vec![]; + bytes_diff.apply(&x_bytes, &mut y_from_bytes).unwrap(); + + assert_eq!(y_bytes, y_from_bytes); + + // U64 diff wins by more than a factor of 3 + assert!(u64_diff.bytes.len() < 3 * bytes_diff.bytes.len()); + } +} diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 70e02164e08..ac25f746e0f 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -1,44 +1,50 @@ -use crate::chunked_vector::{ - store_updated_vector, BlockRoots, HistoricalRoots, HistoricalSummaries, RandaoMixes, StateRoots, -}; -use crate::config::{ - OnDiskStoreConfig, StoreConfig, DEFAULT_SLOTS_PER_RESTORE_POINT, - PREV_DEFAULT_SLOTS_PER_RESTORE_POINT, -}; +use crate::config::{OnDiskStoreConfig, StoreConfig}; use crate::forwards_iter::{HybridForwardsBlockRootsIterator, HybridForwardsStateRootsIterator}; -use crate::impls::beacon_state::{get_full_state, store_full_state}; +use crate::hdiff::{HDiff, HDiffBuffer, HierarchyModuli, StorageStrategy}; +use crate::hot_state_iter::HotStateRootIter; +use crate::impls::{ + beacon_state::{get_full_state, store_full_state}, + frozen_block_slot::FrozenBlockSlot, +}; use crate::iter::{BlockRootsIterator, ParentRootBlockIterator, RootsIterator}; -use crate::leveldb_store::BytesKey; -use crate::leveldb_store::LevelDB; +use crate::leveldb_store::{BytesKey, LevelDB}; use crate::memory_store::MemoryStore; use crate::metadata::{ - AnchorInfo, BlobInfo, CompactionTimestamp, PruningCheckpoint, SchemaVersion, ANCHOR_INFO_KEY, - BLOB_INFO_KEY, COMPACTION_TIMESTAMP_KEY, CONFIG_KEY, CURRENT_SCHEMA_VERSION, - PRUNING_CHECKPOINT_KEY, SCHEMA_VERSION_KEY, SPLIT_KEY, STATE_UPPER_LIMIT_NO_RETAIN, + AnchorInfo, BlobInfo, CompactionTimestamp, SchemaVersion, ANCHOR_INFO_KEY, BLOB_INFO_KEY, + COMPACTION_TIMESTAMP_KEY, CONFIG_KEY, CURRENT_SCHEMA_VERSION, SCHEMA_VERSION_KEY, SPLIT_KEY, + STATE_UPPER_LIMIT_NO_RETAIN, }; use crate::metrics; +use crate::state_cache::{PutStateOutcome, StateCache}; use crate::{ - get_key_for_col, ChunkWriter, DBColumn, DatabaseBlock, Error, ItemStore, KeyValueStoreOp, - PartialBeaconState, StoreItem, StoreOp, + get_key_for_col, DBColumn, DatabaseBlock, Error, ItemStore, KeyValueStoreOp, StoreItem, + StoreOp, ValidatorPubkeyCache, }; use itertools::process_results; use leveldb::iterator::LevelDBIterator; use lru::LruCache; use parking_lot::{Mutex, RwLock}; +use safe_arith::SafeArith; use serde::{Deserialize, Serialize}; use slog::{debug, error, info, trace, warn, Logger}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use state_processing::{ - BlockProcessingError, BlockReplayer, SlotProcessingError, StateProcessingStrategy, + block_replayer::PreSlotHook, AllCaches, BlockProcessingError, BlockReplayer, + SlotProcessingError, StateProcessingStrategy, }; use std::cmp::min; +use std::collections::VecDeque; +use std::io::{Read, Write}; use std::marker::PhantomData; use std::num::NonZeroUsize; use std::path::Path; use std::sync::Arc; use std::time::Duration; use types::*; +use zstd::{Decoder, Encoder}; + +pub const MAX_PARENT_STATES_TO_CACHE: u64 = 1; /// On-disk database that stores finalized states efficiently. /// @@ -56,6 +62,7 @@ pub struct HotColdDB, Cold: ItemStore> { /// The starting slots for the range of blobs stored in the database. blob_info: RwLock, pub(crate) config: StoreConfig, + pub(crate) hierarchy: HierarchyModuli, /// Cold database containing compact historical data. pub cold_db: Cold, /// Database containing blobs. If None, store falls back to use `cold_db`. @@ -66,12 +73,22 @@ pub struct HotColdDB, Cold: ItemStore> { pub hot_db: Hot, /// LRU cache of deserialized blocks and blobs. Updated whenever a block or blob is loaded. block_cache: Mutex>, + /// Cache of beacon states. + /// + /// LOCK ORDERING: this lock must always be locked *after* the `split` if both are required. + state_cache: Mutex>, + /// Immutable validator cache. + pub immutable_validators: Arc>>, /// LRU cache of replayed states. - state_cache: Mutex>>, + // FIXME(sproul): re-enable historic state cache + #[allow(dead_code)] + historic_state_cache: Mutex>>, + /// Cache of hierarchical diff buffers. + diff_buffer_cache: Mutex>, /// Chain spec. pub(crate) spec: ChainSpec, /// Logger. - pub(crate) log: Logger, + pub log: Logger, /// Mere vessel for E. _phantom: PhantomData, } @@ -128,14 +145,21 @@ pub enum HotColdDBError { }, MissingStateToFreeze(Hash256), MissingRestorePointHash(u64), + MissingRestorePointState(Slot), MissingRestorePoint(Hash256), MissingColdStateSummary(Hash256), MissingHotStateSummary(Hash256), MissingEpochBoundaryState(Hash256), + MissingPrevState(Hash256), MissingSplitState(Hash256, Slot), + MissingStateDiff(Hash256), + MissingHDiff(Slot), MissingExecutionPayload(Hash256), MissingFullBlockExecutionPayloadPruned(Hash256, Slot), MissingAnchorInfo, + MissingFrozenBlockSlot(Hash256), + MissingFrozenBlock(Slot), + MissingPathToBlobsDatabase, BlobsPreviouslyInDefaultStore, HotStateSummaryError(BeaconStateError), RestorePointDecodeError(ssz::DecodeError), @@ -168,7 +192,14 @@ impl HotColdDB, MemoryStore> { spec: ChainSpec, log: Logger, ) -> Result, MemoryStore>, Error> { - Self::verify_config(&config)?; + config.verify::()?; + + let hierarchy = config.hierarchy_config.to_moduli()?; + + let block_cache_size = config.block_cache_size; + let state_cache_size = config.state_cache_size; + let historic_state_cache_size = config.historic_state_cache_size; + let diff_buffer_cache_size = config.diff_buffer_cache_size; let db = HotColdDB { split: RwLock::new(Split::default()), @@ -177,9 +208,13 @@ impl HotColdDB, MemoryStore> { cold_db: MemoryStore::open(), blobs_db: MemoryStore::open(), hot_db: MemoryStore::open(), - block_cache: Mutex::new(BlockCache::new(config.block_cache_size)), - state_cache: Mutex::new(LruCache::new(config.historic_state_cache_size)), + block_cache: Mutex::new(BlockCache::new(block_cache_size)), + state_cache: Mutex::new(StateCache::new(state_cache_size)), + immutable_validators: Arc::new(RwLock::new(Default::default())), + historic_state_cache: Mutex::new(LruCache::new(historic_state_cache_size)), + diff_buffer_cache: Mutex::new(LruCache::new(diff_buffer_cache_size)), config, + hierarchy, spec, log, _phantom: PhantomData, @@ -192,8 +227,6 @@ impl HotColdDB, MemoryStore> { impl HotColdDB, LevelDB> { /// Open a new or existing database, with the given paths to the hot and cold DBs. /// - /// The `slots_per_restore_point` parameter must be a divisor of `SLOTS_PER_HISTORICAL_ROOT`. - /// /// The `migrate_schema` function is passed in so that the parent `BeaconChain` can provide /// context and access `BeaconChain`-level code without creating a circular dependency. pub fn open( @@ -205,42 +238,37 @@ impl HotColdDB, LevelDB> { spec: ChainSpec, log: Logger, ) -> Result, Error> { - Self::verify_slots_per_restore_point(config.slots_per_restore_point)?; + config.verify::()?; + + let hierarchy = config.hierarchy_config.to_moduli()?; + + let block_cache_size = config.block_cache_size; + let state_cache_size = config.state_cache_size; + let historic_state_cache_size = config.historic_state_cache_size; + let diff_buffer_cache_size = config.diff_buffer_cache_size; - let mut db = HotColdDB { + let db = HotColdDB { split: RwLock::new(Split::default()), anchor_info: RwLock::new(None), blob_info: RwLock::new(BlobInfo::default()), cold_db: LevelDB::open(cold_path)?, blobs_db: LevelDB::open(blobs_db_path)?, hot_db: LevelDB::open(hot_path)?, - block_cache: Mutex::new(BlockCache::new(config.block_cache_size)), - state_cache: Mutex::new(LruCache::new(config.historic_state_cache_size)), + block_cache: Mutex::new(BlockCache::new(block_cache_size)), + state_cache: Mutex::new(StateCache::new(state_cache_size)), + immutable_validators: Arc::new(RwLock::new(Default::default())), + historic_state_cache: Mutex::new(LruCache::new(historic_state_cache_size)), + diff_buffer_cache: Mutex::new(LruCache::new(diff_buffer_cache_size)), config, + hierarchy, spec, log, _phantom: PhantomData, }; - // Allow the slots-per-restore-point value to stay at the previous default if the config - // uses the new default. Don't error on a failed read because the config itself may need - // migrating. - if let Ok(Some(disk_config)) = db.load_config() { - if !db.config.slots_per_restore_point_set_explicitly - && disk_config.slots_per_restore_point == PREV_DEFAULT_SLOTS_PER_RESTORE_POINT - && db.config.slots_per_restore_point == DEFAULT_SLOTS_PER_RESTORE_POINT - { - debug!( - db.log, - "Ignoring slots-per-restore-point config in favour of on-disk value"; - "config" => db.config.slots_per_restore_point, - "on_disk" => disk_config.slots_per_restore_point, - ); - - // Mutate the in-memory config so that it's compatible. - db.config.slots_per_restore_point = PREV_DEFAULT_SLOTS_PER_RESTORE_POINT; - } - } + // Load the config from disk but don't error on a failed read because the config itself may + // need migrating. + let _ = db.load_config(); // Load the previous split slot from the database (if any). This ensures we can // stop and restart correctly. This needs to occur *before* running any migrations @@ -257,6 +285,11 @@ impl HotColdDB, LevelDB> { ); } + // Load validator pubkey cache. + // FIXME(sproul): probably breaks migrations, etc + let pubkey_cache = ValidatorPubkeyCache::load_from_store(&db)?; + *db.immutable_validators.write() = pubkey_cache; + // Open separate blobs directory if configured and same configuration was used on previous // run. let blob_info = db.load_blob_info()?; @@ -312,7 +345,20 @@ impl HotColdDB, LevelDB> { // Ensure that any on-disk config is compatible with the supplied config. if let Some(disk_config) = db.load_config()? { - db.config.check_compatibility(&disk_config)?; + let split = db.get_split_info(); + let anchor = db.get_anchor_info(); + db.config + .check_compatibility(&disk_config, &split, anchor.as_ref())?; + + // Inform user if hierarchy config is changing. + if db.config.hierarchy_config != disk_config.hierarchy_config { + info!( + db.log, + "Updating historic state config"; + "previous_config" => ?disk_config.hierarchy_config, + "new_config" => ?db.config.hierarchy_config, + ); + } } db.store_config()?; @@ -352,6 +398,21 @@ impl HotColdDB, LevelDB> { } impl, Cold: ItemStore> HotColdDB { + pub fn update_finalized_state( + &self, + state_root: Hash256, + block_root: Hash256, + state: BeaconState, + ) -> Result<(), Error> { + self.state_cache + .lock() + .update_finalized_state(state_root, block_root, state) + } + + pub fn state_cache_len(&self) -> usize { + self.state_cache.lock().len() + } + /// Store a block and update the LRU cache. pub fn put_block( &self, @@ -360,8 +421,10 @@ impl, Cold: ItemStore> HotColdDB ) -> Result<(), Error> { // Store on disk. let mut ops = Vec::with_capacity(2); + let block = self.block_as_kv_store_ops(block_root, block, &mut ops)?; self.hot_db.do_atomically(ops)?; + // Update cache. self.block_cache.lock().put_block(*block_root, block); Ok(()) @@ -411,6 +474,7 @@ impl, Cold: ItemStore> HotColdDB pub fn try_get_full_block( &self, block_root: &Hash256, + slot: Option, ) -> Result>, Error> { metrics::inc_counter(&metrics::BEACON_BLOCK_GET_COUNT); @@ -421,7 +485,7 @@ impl, Cold: ItemStore> HotColdDB } // Load the blinded block. - let Some(blinded_block) = self.get_blinded_block(block_root)? else { + let Some(blinded_block) = self.get_blinded_block(block_root, slot)? else { return Ok(None); }; @@ -469,8 +533,9 @@ impl, Cold: ItemStore> HotColdDB pub fn get_full_block( &self, block_root: &Hash256, + slot: Option, ) -> Result>, Error> { - match self.try_get_full_block(block_root)? { + match self.try_get_full_block(block_root, slot)? { Some(DatabaseBlock::Full(block)) => Ok(Some(block)), Some(DatabaseBlock::Blinded(block)) => Err( HotColdDBError::MissingFullBlockExecutionPayloadPruned(*block_root, block.slot()) @@ -501,12 +566,115 @@ impl, Cold: ItemStore> HotColdDB pub fn get_blinded_block( &self, block_root: &Hash256, - ) -> Result>>, Error> { + slot: Option, + ) -> Result>, Error> { + let split = self.get_split_info(); + if let Some(slot) = slot { + if (slot < split.slot || slot == 0) && *block_root != split.block_root { + // To the freezer DB. + self.get_cold_blinded_block_by_slot(slot) + } else { + self.get_hot_blinded_block(block_root) + } + } else { + match self.get_hot_blinded_block(block_root)? { + Some(block) => Ok(Some(block)), + None => self.get_cold_blinded_block_by_root(block_root), + } + } + } + + pub fn get_hot_blinded_block( + &self, + block_root: &Hash256, + ) -> Result>, Error> { self.get_block_with(block_root, |bytes| { SignedBeaconBlock::from_ssz_bytes(bytes, &self.spec) }) } + pub fn get_cold_blinded_block_by_root( + &self, + block_root: &Hash256, + ) -> Result>, Error> { + // Load slot. + if let Some(FrozenBlockSlot(block_slot)) = self.cold_db.get(block_root)? { + self.get_cold_blinded_block_by_slot(block_slot) + } else { + Ok(None) + } + } + + pub fn get_cold_blinded_block_by_slot( + &self, + slot: Slot, + ) -> Result>, Error> { + let Some(bytes) = self.cold_db.get_bytes( + DBColumn::BeaconBlockFrozen.into(), + &slot.as_u64().to_be_bytes(), + )? + else { + return Ok(None); + }; + + let mut ssz_bytes = Vec::with_capacity(self.config.estimate_decompressed_size(bytes.len())); + let mut decoder = Decoder::new(&*bytes).map_err(Error::Compression)?; + decoder + .read_to_end(&mut ssz_bytes) + .map_err(Error::Compression)?; + Ok(Some(SignedBeaconBlock::from_ssz_bytes( + &ssz_bytes, &self.spec, + )?)) + } + + pub fn put_cold_blinded_block( + &self, + block_root: &Hash256, + block: &SignedBlindedBeaconBlock, + ) -> Result<(), Error> { + let mut ops = Vec::with_capacity(2); + self.blinded_block_as_cold_kv_store_ops(block_root, block, &mut ops)?; + self.cold_db.do_atomically(ops) + } + + pub fn blinded_block_as_cold_kv_store_ops( + &self, + block_root: &Hash256, + block: &SignedBlindedBeaconBlock, + kv_store_ops: &mut Vec, + ) -> Result<(), Error> { + // Write the block root to slot mapping. + let slot = block.slot(); + kv_store_ops.push(FrozenBlockSlot(slot).as_kv_store_op(*block_root)); + + // Write the slot to block root mapping. + kv_store_ops.push(KeyValueStoreOp::PutKeyValue( + get_key_for_col( + DBColumn::BeaconBlockRoots.into(), + &slot.as_u64().to_be_bytes(), + ), + block_root.as_bytes().to_vec(), + )); + + // Write the block keyed by slot. + let db_key = get_key_for_col( + DBColumn::BeaconBlockFrozen.into(), + &slot.as_u64().to_be_bytes(), + ); + + let ssz_bytes = block.as_ssz_bytes(); + let mut compressed_value = + Vec::with_capacity(self.config.estimate_compressed_size(ssz_bytes.len())); + let mut encoder = Encoder::new(&mut compressed_value, self.config.compression_level) + .map_err(Error::Compression)?; + encoder.write_all(&ssz_bytes).map_err(Error::Compression)?; + encoder.finish().map_err(Error::Compression)?; + + kv_store_ops.push(KeyValueStoreOp::PutKeyValue(db_key, compressed_value)); + + Ok(()) + } + /// Fetch a block from the store, ignoring which fork variant it *should* be for. pub fn get_block_any_variant>( &self, @@ -562,16 +730,30 @@ impl, Cold: ItemStore> HotColdDB .map(|payload| payload.is_some()) } + /// Store an execution payload in the hot database. + pub fn put_execution_payload( + &self, + block_root: &Hash256, + execution_payload: &ExecutionPayload, + ) -> Result<(), Error> { + self.hot_db + .do_atomically(vec![execution_payload.as_kv_store_op(*block_root)]) + } + /// Check if the blobs for a block exists on disk. pub fn blobs_exist(&self, block_root: &Hash256) -> Result { self.blobs_db .key_exists(DBColumn::BeaconBlob.into(), block_root.as_bytes()) } - /// Determine whether a block exists in the database. + /// Determine whether a block exists in the database (hot *or* cold). pub fn block_exists(&self, block_root: &Hash256) -> Result { - self.hot_db - .key_exists(DBColumn::BeaconBlock.into(), block_root.as_bytes()) + Ok(self + .hot_db + .key_exists(DBColumn::BeaconBlock.into(), block_root.as_bytes())? + || self + .cold_db + .key_exists(DBColumn::BeaconBlock.into(), block_root.as_bytes())?) } /// Delete a block from the store and the block cache. @@ -648,19 +830,17 @@ impl, Cold: ItemStore> HotColdDB // chain. This way we avoid returning a state that doesn't match `state_root`. self.load_cold_state(state_root) } else { - self.load_hot_state(state_root, StateProcessingStrategy::Accurate) + self.get_hot_state(state_root) } } else { - match self.load_hot_state(state_root, StateProcessingStrategy::Accurate)? { + match self.get_hot_state(state_root)? { Some(state) => Ok(Some(state)), None => self.load_cold_state(state_root), } } } - /// Fetch a state from the store, but don't compute all of the values when replaying blocks - /// upon that state (e.g., state roots). Additionally, only states from the hot store are - /// returned. + /// Get a state with `latest_block_root == block_root` advanced through to at most `slot`. /// /// See `Self::get_advanced_hot_state` for information about `max_slot`. /// @@ -706,6 +886,13 @@ impl, Cold: ItemStore> HotColdDB max_slot: Slot, state_root: Hash256, ) -> Result)>, Error> { + if let Some(cached) = self + .state_cache + .lock() + .get_by_block_root(block_root, max_slot) + { + return Ok(Some(cached)); + } self.get_advanced_hot_state_with_strategy( block_root, max_slot, @@ -715,12 +902,13 @@ impl, Cold: ItemStore> HotColdDB } /// Same as `get_advanced_hot_state` but taking a `StateProcessingStrategy`. + // FIXME(sproul): delete the state processing strategy stuff again pub fn get_advanced_hot_state_with_strategy( &self, block_root: Hash256, max_slot: Slot, state_root: Hash256, - state_processing_strategy: StateProcessingStrategy, + _state_processing_strategy: StateProcessingStrategy, ) -> Result)>, Error> { // Hold a read lock on the split point so it can't move while we're trying to load the // state. @@ -741,11 +929,11 @@ impl, Cold: ItemStore> HotColdDB } else { state_root }; - let state = self - .load_hot_state(&state_root, state_processing_strategy)? - .map(|state| (state_root, state)); + let opt_state = self + .load_hot_state(&state_root)? + .map(|(state, _block_root)| (state_root, state)); drop(split); - Ok(state) + Ok(opt_state) } /// Delete a state, ensuring it is removed from the LRU cache, as well as from on-disk. @@ -755,17 +943,10 @@ impl, Cold: ItemStore> HotColdDB /// (which are frozen, and won't be deleted), or valid descendents of the finalized checkpoint /// (which will be deleted by this function but shouldn't be). pub fn delete_state(&self, state_root: &Hash256, slot: Slot) -> Result<(), Error> { - // Delete the state summary. - self.hot_db - .key_delete(DBColumn::BeaconStateSummary.into(), state_root.as_bytes())?; - - // Delete the full state if it lies on an epoch boundary. - if slot % E::slots_per_epoch() == 0 { - self.hot_db - .key_delete(DBColumn::BeaconState.into(), state_root.as_bytes())?; - } - - Ok(()) + self.do_atomically_with_block_and_blobs_cache(vec![StoreOp::DeleteState( + *state_root, + Some(slot), + )]) } pub fn forwards_block_roots_iterator( @@ -773,14 +954,13 @@ impl, Cold: ItemStore> HotColdDB start_slot: Slot, end_state: BeaconState, end_block_root: Hash256, - spec: &ChainSpec, ) -> Result> + '_, Error> { HybridForwardsBlockRootsIterator::new( self, + DBColumn::BeaconBlockRoots, start_slot, None, || Ok((end_state, end_block_root)), - spec, ) } @@ -789,9 +969,14 @@ impl, Cold: ItemStore> HotColdDB start_slot: Slot, end_slot: Slot, get_state: impl FnOnce() -> Result<(BeaconState, Hash256), Error>, - spec: &ChainSpec, ) -> Result, Error> { - HybridForwardsBlockRootsIterator::new(self, start_slot, Some(end_slot), get_state, spec) + HybridForwardsBlockRootsIterator::new( + self, + DBColumn::BeaconBlockRoots, + start_slot, + Some(end_slot), + get_state, + ) } pub fn forwards_state_roots_iterator( @@ -799,14 +984,13 @@ impl, Cold: ItemStore> HotColdDB start_slot: Slot, end_state_root: Hash256, end_state: BeaconState, - spec: &ChainSpec, ) -> Result> + '_, Error> { HybridForwardsStateRootsIterator::new( self, + DBColumn::BeaconStateRoots, start_slot, None, || Ok((end_state, end_state_root)), - spec, ) } @@ -815,47 +999,14 @@ impl, Cold: ItemStore> HotColdDB start_slot: Slot, end_slot: Slot, get_state: impl FnOnce() -> Result<(BeaconState, Hash256), Error>, - spec: &ChainSpec, ) -> Result, Error> { - HybridForwardsStateRootsIterator::new(self, start_slot, Some(end_slot), get_state, spec) - } - - /// Load an epoch boundary state by using the hot state summary look-up. - /// - /// Will fall back to the cold DB if a hot state summary is not found. - pub fn load_epoch_boundary_state( - &self, - state_root: &Hash256, - ) -> Result>, Error> { - if let Some(HotStateSummary { - epoch_boundary_state_root, - .. - }) = self.load_hot_state_summary(state_root)? - { - // NOTE: minor inefficiency here because we load an unnecessary hot state summary - // - // `StateProcessingStrategy` should be irrelevant here since we never replay blocks for an epoch - // boundary state in the hot DB. - let state = self - .load_hot_state( - &epoch_boundary_state_root, - StateProcessingStrategy::Accurate, - )? - .ok_or(HotColdDBError::MissingEpochBoundaryState( - epoch_boundary_state_root, - ))?; - Ok(Some(state)) - } else { - // Try the cold DB - match self.load_cold_state_slot(state_root)? { - Some(state_slot) => { - let epoch_boundary_slot = - state_slot / E::slots_per_epoch() * E::slots_per_epoch(); - self.load_cold_state_by_slot(epoch_boundary_slot) - } - None => Ok(None), - } - } + HybridForwardsStateRootsIterator::new( + self, + DBColumn::BeaconStateRoots, + start_slot, + Some(end_slot), + get_state, + ) } pub fn put_item(&self, key: &Hash256, item: &I) -> Result<(), Error> { @@ -886,16 +1037,12 @@ impl, Cold: ItemStore> HotColdDB )?; } - StoreOp::PutState(state_root, state) => { - self.store_hot_state(&state_root, state, &mut key_value_batch)?; - } - StoreOp::PutBlobs(block_root, blobs) => { self.blobs_as_kv_store_ops(&block_root, blobs, &mut key_value_batch); } - StoreOp::PutStateSummary(state_root, summary) => { - key_value_batch.push(summary.as_kv_store_op(state_root)); + StoreOp::PutState(state_root, state) => { + self.store_hot_state(&state_root, state, &mut key_value_batch)?; } StoreOp::PutStateTemporaryFlag(state_root) => { @@ -924,12 +1071,19 @@ impl, Cold: ItemStore> HotColdDB key_value_batch.push(KeyValueStoreOp::DeleteKey(state_summary_key)); if slot.map_or(true, |slot| slot % E::slots_per_epoch() == 0) { + // Delete full state if any. let state_key = get_key_for_col(DBColumn::BeaconState.into(), state_root.as_bytes()); key_value_batch.push(KeyValueStoreOp::DeleteKey(state_key)); + + // Delete diff too. + let diff_key = get_key_for_col( + DBColumn::BeaconStateDiff.into(), + state_root.as_bytes(), + ); + key_value_batch.push(KeyValueStoreOp::DeleteKey(diff_key)); } } - StoreOp::DeleteExecutionPayload(block_root) => { let key = get_key_for_col(DBColumn::ExecPayload.into(), block_root.as_bytes()); key_value_batch.push(KeyValueStoreOp::DeleteKey(key)); @@ -1021,19 +1175,20 @@ impl, Cold: ItemStore> HotColdDB StoreOp::PutState(_, _) => (), - StoreOp::PutStateSummary(_, _) => (), - StoreOp::PutStateTemporaryFlag(_) => (), StoreOp::DeleteStateTemporaryFlag(_) => (), StoreOp::DeleteBlock(block_root) => { guard.delete_block(&block_root); + self.state_cache.lock().delete_block_states(&block_root); } - StoreOp::DeleteBlobs(_) => (), + StoreOp::DeleteState(state_root, _) => { + self.state_cache.lock().delete_state(&state_root) + } - StoreOp::DeleteState(_, _) => (), + StoreOp::DeleteBlobs(_) => (), StoreOp::DeleteExecutionPayload(_) => (), @@ -1070,118 +1225,578 @@ impl, Cold: ItemStore> HotColdDB state: &BeaconState, ops: &mut Vec, ) -> Result<(), Error> { - // On the epoch boundary, store the full state. - if state.slot() % E::slots_per_epoch() == 0 { - trace!( + // Put the state in the cache. + // FIXME(sproul): could optimise out the block root + let block_root = state.get_latest_block_root(*state_root); + + // Avoid storing states in the database if they already exist in the state cache. + // The exception to this is the finalized state, which must exist in the cache before it + // is stored on disk. + if let PutStateOutcome::Duplicate = + self.state_cache + .lock() + .put_state(*state_root, block_root, state)? + { + debug!( self.log, - "Storing full state on epoch boundary"; - "slot" => state.slot().as_u64(), - "state_root" => format!("{:?}", state_root) + "Skipping storage of cached state"; + "slot" => state.slot() ); - store_full_state(state_root, state, ops)?; + return Ok(()); } // Store a summary of the state. // We store one even for the epoch boundary states, as we may need their slots // when doing a look up by state root. - let hot_state_summary = HotStateSummary::new(state_root, state)?; + let diff_base_slot = self.state_diff_slot(state.slot()); + + let hot_state_summary = HotStateSummary::new(state_root, state, diff_base_slot)?; let op = hot_state_summary.as_kv_store_op(*state_root); ops.push(op); + // On an epoch boundary, consider storing: + // + // 1. A full state, if the state is the split state or a fork boundary state. + // 2. A state diff, if the state is a multiple of `epochs_per_state_diff` after the + // split state. + if state.slot() % E::slots_per_epoch() == 0 { + if self.is_stored_as_full_state(*state_root, state.slot())? { + info!( + self.log, + "Storing full state on epoch boundary"; + "slot" => state.slot(), + "state_root" => ?state_root, + ); + self.store_full_state_in_batch(state_root, state, ops)?; + } else if let Some(base_slot) = diff_base_slot { + debug!( + self.log, + "Storing state diff on boundary"; + "slot" => state.slot(), + "base_slot" => base_slot, + "state_root" => ?state_root, + ); + let diff_base_state_root = hot_state_summary.diff_base_state_root; + let diff_base_state = self.get_hot_state(&diff_base_state_root)?.ok_or( + HotColdDBError::MissingEpochBoundaryState(diff_base_state_root), + )?; + + let compute_diff_timer = + metrics::start_timer(&metrics::BEACON_STATE_DIFF_COMPUTE_TIME); + + let base_buffer = HDiffBuffer::from_state(diff_base_state); + let target_buffer = HDiffBuffer::from_state(state.clone()); + let diff = HDiff::compute(&base_buffer, &target_buffer)?; + drop(compute_diff_timer); + ops.push(diff.as_kv_store_op(*state_root)); + } + } + Ok(()) } + pub fn store_full_state( + &self, + state_root: &Hash256, + state: &BeaconState, + ) -> Result<(), Error> { + let mut ops = Vec::with_capacity(4); + self.store_full_state_in_batch(state_root, state, &mut ops)?; + self.hot_db.do_atomically(ops) + } + + pub fn store_full_state_in_batch( + &self, + state_root: &Hash256, + state: &BeaconState, + ops: &mut Vec, + ) -> Result<(), Error> { + store_full_state(state_root, state, ops, &self.config) + } + + /// Get a post-finalization state from the database or store. + pub fn get_hot_state(&self, state_root: &Hash256) -> Result>, Error> { + if let Some(state) = self.state_cache.lock().get_by_state_root(*state_root) { + return Ok(Some(state)); + } + warn!( + self.log, + "State cache missed"; + "state_root" => ?state_root, + ); + + let state_from_disk = self.load_hot_state(state_root)?; + + if let Some((state, block_root)) = state_from_disk { + self.state_cache + .lock() + .put_state(*state_root, block_root, &state)?; + Ok(Some(state)) + } else { + Ok(None) + } + } + /// Load a post-finalization state from the hot database. /// - /// Will replay blocks from the nearest epoch boundary. + /// Use a combination of state diffs and replayed blocks as appropriate. + /// + /// Return the `(state, latest_block_root)` if found. pub fn load_hot_state( &self, state_root: &Hash256, - state_processing_strategy: StateProcessingStrategy, - ) -> Result>, Error> { - metrics::inc_counter(&metrics::BEACON_STATE_HOT_GET_COUNT); + ) -> Result, Hash256)>, Error> { + let _timer = metrics::start_timer(&metrics::BEACON_HOT_STATE_READ_TIMES); + metrics::inc_counter(&metrics::BEACON_STATE_HOT_GET_COUNT); + + // If the state is the finalized state, load it from disk. This should only be necessary + // once during start-up, after which point the finalized state will be cached. + if *state_root == self.get_split_info().state_root { + return self.load_hot_state_full(state_root).map(Some); + } + + let Some(target_summary) = self.load_hot_state_summary(state_root)? else { + return Ok(None); + }; + + let target_slot = target_summary.slot; + let target_latest_block_root = target_summary.latest_block_root; + + // Load the latest block, and use it to confirm the validity of this state. + if self + .get_blinded_block(&target_summary.latest_block_root, None)? + .is_none() + { + // Dangling state, will be deleted fully once finalization advances past it. + debug!( + self.log, + "Ignoring state load for dangling state"; + "state_root" => ?state_root, + "slot" => target_slot, + "latest_block_root" => ?target_summary.latest_block_root, + ); + return Ok(None); + } + + // Take a read lock on the split point while we load data from prior states. We need + // to prevent the finalization migration from deleting the state summaries and state diffs + // that we are iterating back through. + let split_read_lock = self.split.read_recursive(); + + // Backtrack until we reach a state that is in the cache, or in the worst case + // the finalized state (this should only be reachable on first start-up). + let state_summary_iter = HotStateRootIter::new(self, target_slot, *state_root); + + // State and state root of the state upon which blocks and diffs will be replayed. + let mut base_state = None; + + // State diffs to be replayed on top of `base_state`. + // Each element is `(summary, state_root, diff)` such that applying `diff` to the + // state with `summary.diff_base_state_root` yields the state with `state_root`. + let mut state_diffs = VecDeque::new(); + + // State roots for all slots between `base_state` and the `target_slot`. Depending on how + // the diffs fall, some of these roots may not be needed. + let mut state_roots = VecDeque::new(); + + for res in state_summary_iter { + let (prior_state_root, prior_summary) = res?; + + state_roots.push_front(Ok((prior_state_root, prior_summary.slot))); + + // Check if this state is in the cache. + if let Some(state) = self.state_cache.lock().get_by_state_root(prior_state_root) { + debug!( + self.log, + "Found cached base state for replay"; + "base_state_root" => ?prior_state_root, + "base_slot" => prior_summary.slot, + "target_state_root" => ?state_root, + "target_slot" => target_slot, + ); + base_state = Some((prior_state_root, state)); + break; + } + + // If the prior state is the split state and it isn't cached then load it in + // entirety from disk. This should only happen on first start up. + if prior_state_root == split_read_lock.state_root || prior_summary.slot == 0 { + debug!( + self.log, + "Using split state as base state for replay"; + "base_state_root" => ?prior_state_root, + "base_slot" => prior_summary.slot, + "target_state_root" => ?state_root, + "target_slot" => target_slot, + ); + let (split_state, _) = self.load_hot_state_full(&prior_state_root)?; + base_state = Some((prior_state_root, split_state)); + break; + } + + // If there's a state diff stored at this slot, load it and store it for application. + if !prior_summary.diff_base_state_root.is_zero() { + let diff = self.load_hot_state_diff(prior_state_root)?; + state_diffs.push_front((prior_summary, prior_state_root, diff)); + } + } + + let (_, mut state) = base_state.ok_or(Error::NoBaseStateFound(*state_root))?; + + // Finished reading information about prior states, allow the split point to update. + drop(split_read_lock); + + // Construct a mutable iterator for the state roots, which will be iterated through + // consecutive calls to `replay_blocks`. + let mut state_roots_iter = state_roots.into_iter(); + + // This hook caches states from block replay so that they may be reused. + let state_cacher_hook = |opt_state_root: Option, state: &mut BeaconState<_>| { + // Ensure all caches are built before attempting to cache. + state.update_tree_hash_cache()?; + state.build_all_caches(&self.spec)?; + + if let Some(state_root) = opt_state_root { + // Cache + if state.slot() + MAX_PARENT_STATES_TO_CACHE >= target_slot + || state.slot() % E::slots_per_epoch() == 0 + { + let slot = state.slot(); + let latest_block_root = state.get_latest_block_root(state_root); + if let PutStateOutcome::New = + self.state_cache + .lock() + .put_state(state_root, latest_block_root, state)? + { + debug!( + self.log, + "Cached ancestor state"; + "state_root" => ?state_root, + "slot" => slot, + ); + } + } + } else { + debug!( + self.log, + "Block replay state root miss"; + "slot" => state.slot(), + ); + } + Ok(()) + }; + + // Apply the diffs, and replay blocks atop the base state to reach the target state. + while state.slot() < target_slot { + // Drop unncessary diffs. + state_diffs.retain(|(summary, diff_root, _)| { + let keep = summary.diff_base_slot >= state.slot(); + if !keep { + debug!( + self.log, + "Ignoring irrelevant state diff"; + "diff_state_root" => ?diff_root, + "diff_base_slot" => summary.diff_base_slot, + "current_state_slot" => state.slot(), + ); + } + keep + }); + + // Get the next diff that will be applicable, taking the highest slot diff in case of + // multiple diffs which are applicable at the same base slot, which can happen if the + // diff frequency has changed. + let mut next_state_diff: Option<(HotStateSummary, Hash256, HDiff)> = None; + while let Some((summary, _, _)) = state_diffs.front() { + if next_state_diff.as_ref().map_or(true, |(current, _, _)| { + summary.diff_base_slot == current.diff_base_slot + }) { + next_state_diff = state_diffs.pop_front(); + } else { + break; + } + } + + // Replay blocks to get to the next diff's base state, or to the target state if there + // is no next diff to apply. + if next_state_diff + .as_ref() + .map_or(true, |(next_summary, _, _)| { + next_summary.diff_base_slot != state.slot() + }) + { + let (next_slot, latest_block_root) = next_state_diff + .as_ref() + .map(|(summary, _, _)| (summary.diff_base_slot, summary.latest_block_root)) + .unwrap_or_else(|| (target_summary.slot, target_latest_block_root)); + debug!( + self.log, + "Replaying blocks"; + "from_slot" => state.slot(), + "to_slot" => next_slot, + "latest_block_root" => ?latest_block_root, + ); + let blocks = + self.load_blocks_to_replay(state.slot(), next_slot, latest_block_root)?; + + state = self.replay_blocks( + state, + blocks, + next_slot, + &mut state_roots_iter, + Some(Box::new(state_cacher_hook)), + )?; + + state.update_tree_hash_cache()?; + state.build_all_caches(&self.spec)?; + } + + // Apply state diff. Block replay should have ensured that the diff is now applicable. + if let Some((summary, to_root, diff)) = next_state_diff { + let block_root = summary.latest_block_root; + debug!( + self.log, + "Applying state diff"; + "from_root" => ?summary.diff_base_state_root, + "from_slot" => summary.diff_base_slot, + "to_root" => ?to_root, + "to_slot" => summary.slot, + "block_root" => ?block_root, + ); + assert_eq!(summary.diff_base_slot, state.slot()); + + let t = std::time::Instant::now(); + let pre_state = state.clone(); + let mut base_buffer = HDiffBuffer::from_state(pre_state.clone()); + diff.apply(&mut base_buffer)?; + state = base_buffer.into_state(&self.spec)?; + let application_ms = t.elapsed().as_millis(); + + // Rebase state before adding it to the cache, to ensure it uses minimal memory. + let t = std::time::Instant::now(); + state.rebase_on(&pre_state, &self.spec)?; + let rebase_ms = t.elapsed().as_millis(); + + let t = std::time::Instant::now(); + state.update_tree_hash_cache()?; + let tree_hash_ms = t.elapsed().as_millis(); + + let t = std::time::Instant::now(); + state.build_all_caches(&self.spec)?; + let cache_ms = t.elapsed().as_millis(); + + debug!( + self.log, + "State diff applied"; + "application_ms" => application_ms, + "rebase_ms" => rebase_ms, + "tree_hash_ms" => tree_hash_ms, + "cache_ms" => cache_ms, + "slot" => state.slot() + ); + + // Add state to the cache, it is by definition an epoch boundary state and likely + // to be useful. + self.state_cache + .lock() + .put_state(to_root, block_root, &state)?; + } + } + + Ok(Some((state, target_latest_block_root))) + } + + /// Determine if the `state_root` at `slot` should be stored as a full state. + /// + /// This is dependent on the database's current split point, so may change from `false` to + /// `true` after a finalization update. It cannot change from `true` to `false` for a state in + /// the hot database as the split state will be migrated to the freezer. + /// + /// All fork boundary states are also stored as full states. + pub fn is_stored_as_full_state(&self, state_root: Hash256, slot: Slot) -> Result { + let split = self.get_split_info(); + + if slot >= split.slot { + Ok(state_root == split.state_root + || self.spec.fork_activated_at_slot::(slot).is_some() + || slot == 0) + } else { + Err(Error::SlotIsBeforeSplit { slot }) + } + } + + /// Determine if a state diff should be stored at `slot`. + /// + /// If `Some(base_slot)` is returned then a state diff should be constructed for the state + /// at `slot` based on the ancestor state at `base_slot`. The frequency of state diffs stored + /// on disk is determined by the `epochs_per_state_diff` parameter. + pub fn state_diff_slot(&self, slot: Slot) -> Option { + let split = self.get_split_info(); + let slots_per_epoch = E::slots_per_epoch(); + + if slot % slots_per_epoch != 0 { + return None; + } + + let epochs_since_split = slot.saturating_sub(split.slot).epoch(slots_per_epoch); + + (epochs_since_split > 0 && epochs_since_split % self.config.epochs_per_state_diff == 0) + .then(|| slot.saturating_sub(self.config.epochs_per_state_diff * slots_per_epoch)) + } + + pub fn load_hot_state_full( + &self, + state_root: &Hash256, + ) -> Result<(BeaconState, Hash256), Error> { + let pubkey_cache = self.immutable_validators.read(); + let validator_pubkeys = |i: usize| pubkey_cache.get_validator_pubkey(i); + let mut state = get_full_state( + &self.hot_db, + state_root, + validator_pubkeys, + &self.config, + &self.spec, + )? + .ok_or(HotColdDBError::MissingEpochBoundaryState(*state_root))?; - // If the state is marked as temporary, do not return it. It will become visible - // only once its transaction commits and deletes its temporary flag. - if self.load_state_temporary_flag(state_root)?.is_some() { - return Ok(None); - } + // Do a tree hash here so that the cache is fully built. + state.update_tree_hash_cache()?; + state.build_all_caches(&self.spec)?; - if let Some(HotStateSummary { - slot, - latest_block_root, - epoch_boundary_state_root, - }) = self.load_hot_state_summary(state_root)? - { - let boundary_state = - get_full_state(&self.hot_db, &epoch_boundary_state_root, &self.spec)?.ok_or( - HotColdDBError::MissingEpochBoundaryState(epoch_boundary_state_root), - )?; + let latest_block_root = state.get_latest_block_root(*state_root); + Ok((state, latest_block_root)) + } - // Optimization to avoid even *thinking* about replaying blocks if we're already - // on an epoch boundary. - let state = if slot % E::slots_per_epoch() == 0 { - boundary_state - } else { - let blocks = - self.load_blocks_to_replay(boundary_state.slot(), slot, latest_block_root)?; - self.replay_blocks( - boundary_state, - blocks, - slot, - no_state_root_iter(), - state_processing_strategy, - )? - }; + pub fn load_hot_state_diff(&self, state_root: Hash256) -> Result { + self.hot_db + .get(&state_root)? + .ok_or(HotColdDBError::MissingStateDiff(state_root).into()) + } - Ok(Some(state)) - } else { - Ok(None) - } + pub fn store_cold_state_summary( + &self, + state_root: &Hash256, + slot: Slot, + ops: &mut Vec, + ) -> Result<(), Error> { + ops.push(ColdStateSummary { slot }.as_kv_store_op(*state_root)); + ops.push(KeyValueStoreOp::PutKeyValue( + get_key_for_col( + DBColumn::BeaconStateRoots.into(), + &slot.as_u64().to_be_bytes(), + ), + state_root.as_bytes().to_vec(), + )); + Ok(()) } /// Store a pre-finalization state in the freezer database. - /// - /// If the state doesn't lie on a restore point boundary then just its summary will be stored. pub fn store_cold_state( &self, state_root: &Hash256, state: &BeaconState, ops: &mut Vec, ) -> Result<(), Error> { - ops.push(ColdStateSummary { slot: state.slot() }.as_kv_store_op(*state_root)); + self.store_cold_state_summary(state_root, state.slot(), ops)?; - if state.slot() % self.config.slots_per_restore_point != 0 { - return Ok(()); + let slot = state.slot(); + match self.hierarchy.storage_strategy(slot)? { + StorageStrategy::ReplayFrom(from) => { + debug!( + self.log, + "Storing cold state"; + "strategy" => "replay", + "from_slot" => from, + "slot" => state.slot(), + ); + } + StorageStrategy::Snapshot => { + debug!( + self.log, + "Storing cold state"; + "strategy" => "snapshot", + "slot" => state.slot(), + ); + self.store_cold_state_as_snapshot(state, ops)?; + } + StorageStrategy::DiffFrom(from) => { + debug!( + self.log, + "Storing cold state"; + "strategy" => "diff", + "from_slot" => from, + "slot" => state.slot(), + ); + self.store_cold_state_as_diff(state, from, ops)?; + } } - trace!( - self.log, - "Creating restore point"; - "slot" => state.slot(), - "state_root" => format!("{:?}", state_root) + Ok(()) + } + + pub fn store_cold_state_as_snapshot( + &self, + state: &BeaconState, + ops: &mut Vec, + ) -> Result<(), Error> { + let bytes = state.as_ssz_bytes(); + let mut compressed_value = + Vec::with_capacity(self.config.estimate_compressed_size(bytes.len())); + let mut encoder = Encoder::new(&mut compressed_value, self.config.compression_level) + .map_err(Error::Compression)?; + encoder.write_all(&bytes).map_err(Error::Compression)?; + encoder.finish().map_err(Error::Compression)?; + + let key = get_key_for_col( + DBColumn::BeaconStateSnapshot.into(), + &state.slot().as_u64().to_be_bytes(), ); + ops.push(KeyValueStoreOp::PutKeyValue(key, compressed_value)); + Ok(()) + } - // 1. Convert to PartialBeaconState and store that in the DB. - let partial_state = PartialBeaconState::from_state_forgetful(state); - let op = partial_state.as_kv_store_op(*state_root); - ops.push(op); + pub fn load_cold_state_bytes_as_snapshot(&self, slot: Slot) -> Result>, Error> { + match self.cold_db.get_bytes( + DBColumn::BeaconStateSnapshot.into(), + &slot.as_u64().to_be_bytes(), + )? { + Some(bytes) => { + let mut ssz_bytes = + Vec::with_capacity(self.config.estimate_decompressed_size(bytes.len())); + let mut decoder = Decoder::new(&*bytes).map_err(Error::Compression)?; + decoder + .read_to_end(&mut ssz_bytes) + .map_err(Error::Compression)?; + Ok(Some(ssz_bytes)) + } + None => Ok(None), + } + } - // 2. Store updated vector entries. - // Block roots need to be written here as well as by the `ChunkWriter` in `migrate_db` - // because states may require older block roots, and the writer only stores block roots - // between the previous split point and the new split point. - let db = &self.cold_db; - store_updated_vector(BlockRoots, db, state, &self.spec, ops)?; - store_updated_vector(StateRoots, db, state, &self.spec, ops)?; - store_updated_vector(HistoricalRoots, db, state, &self.spec, ops)?; - store_updated_vector(RandaoMixes, db, state, &self.spec, ops)?; - store_updated_vector(HistoricalSummaries, db, state, &self.spec, ops)?; - - // 3. Store restore point. - let restore_point_index = state.slot().as_u64() / self.config.slots_per_restore_point; - self.store_restore_point_hash(restore_point_index, *state_root, ops); + pub fn load_cold_state_as_snapshot(&self, slot: Slot) -> Result>, Error> { + Ok(self + .load_cold_state_bytes_as_snapshot(slot)? + .map(|bytes| BeaconState::from_ssz_bytes(&bytes, &self.spec)) + .transpose()?) + } + pub fn store_cold_state_as_diff( + &self, + state: &BeaconState, + from_slot: Slot, + ops: &mut Vec, + ) -> Result<(), Error> { + // Load diff base state bytes. + let (_, base_buffer) = self.load_hdiff_buffer_for_slot(from_slot)?; + let target_buffer = HDiffBuffer::from_state(state.clone()); + let diff = HDiff::compute(&base_buffer, &target_buffer)?; + let diff_bytes = diff.as_ssz_bytes(); + + let key = get_key_for_col( + DBColumn::BeaconStateDiff.into(), + &state.slot().as_u64().to_be_bytes(), + ); + ops.push(KeyValueStoreOp::PutKeyValue(key, diff_bytes)); Ok(()) } @@ -1199,151 +1814,102 @@ impl, Cold: ItemStore> HotColdDB /// /// Will reconstruct the state if it lies between restore points. pub fn load_cold_state_by_slot(&self, slot: Slot) -> Result>, Error> { - // Guard against fetching states that do not exist due to gaps in the historic state - // database, which can occur due to checkpoint sync or re-indexing. - // See the comments in `get_historic_state_limits` for more information. - let (lower_limit, upper_limit) = self.get_historic_state_limits(); - - if slot <= lower_limit || slot >= upper_limit { - if slot % self.config.slots_per_restore_point == 0 { - let restore_point_idx = slot.as_u64() / self.config.slots_per_restore_point; - self.load_restore_point_by_index(restore_point_idx) - } else { - self.load_cold_intermediate_state(slot) - } - .map(Some) - } else { - Ok(None) - } - } + let (base_slot, hdiff_buffer) = self.load_hdiff_buffer_for_slot(slot)?; + let base_state = hdiff_buffer.into_state(&self.spec)?; + debug_assert_eq!(base_slot, base_state.slot()); - /// Load a restore point state by its `state_root`. - fn load_restore_point(&self, state_root: &Hash256) -> Result, Error> { - let partial_state_bytes = self - .cold_db - .get_bytes(DBColumn::BeaconState.into(), state_root.as_bytes())? - .ok_or(HotColdDBError::MissingRestorePoint(*state_root))?; - let mut partial_state: PartialBeaconState = - PartialBeaconState::from_ssz_bytes(&partial_state_bytes, &self.spec)?; + if base_state.slot() == slot { + return Ok(Some(base_state)); + } - // Fill in the fields of the partial state. - partial_state.load_block_roots(&self.cold_db, &self.spec)?; - partial_state.load_state_roots(&self.cold_db, &self.spec)?; - partial_state.load_historical_roots(&self.cold_db, &self.spec)?; - partial_state.load_randao_mixes(&self.cold_db, &self.spec)?; - partial_state.load_historical_summaries(&self.cold_db, &self.spec)?; + let blocks = self.load_cold_blocks(base_state.slot() + 1, slot)?; - partial_state.try_into() - } + // Include state root for base state as it is required by block processing. + let state_root_iter = + self.forwards_state_roots_iterator_until(base_state.slot(), slot, || { + panic!("FIXME(sproul): unreachable state root iter miss") + })?; - /// Load a restore point state by its `restore_point_index`. - fn load_restore_point_by_index( - &self, - restore_point_index: u64, - ) -> Result, Error> { - let state_root = self.load_restore_point_hash(restore_point_index)?; - self.load_restore_point(&state_root) + self.replay_blocks(base_state, blocks, slot, state_root_iter, None) + .map(Some) } - /// Load a frozen state that lies between restore points. - fn load_cold_intermediate_state(&self, slot: Slot) -> Result, Error> { - if let Some(state) = self.state_cache.lock().get(&slot) { - return Ok(state.clone()); + fn load_hdiff_for_slot(&self, slot: Slot) -> Result { + self.cold_db + .get_bytes( + DBColumn::BeaconStateDiff.into(), + &slot.as_u64().to_be_bytes(), + )? + .map(|bytes| HDiff::from_ssz_bytes(&bytes)) + .ok_or(HotColdDBError::MissingHDiff(slot))? + .map_err(Into::into) + } + + /// Returns `HDiffBuffer` for the specified slot, or `HDiffBuffer` for the `ReplayFrom` slot if + /// the diff for the specified slot is not stored. + fn load_hdiff_buffer_for_slot(&self, slot: Slot) -> Result<(Slot, HDiffBuffer), Error> { + if let Some(buffer) = self.diff_buffer_cache.lock().get(&slot) { + debug!( + self.log, + "Hit diff buffer cache"; + "slot" => slot + ); + return Ok((slot, buffer.clone())); } - // 1. Load the restore points either side of the intermediate state. - let low_restore_point_idx = slot.as_u64() / self.config.slots_per_restore_point; - let high_restore_point_idx = low_restore_point_idx + 1; - - // Use low restore point as the base state. - let mut low_slot: Slot = - Slot::new(low_restore_point_idx * self.config.slots_per_restore_point); - let mut low_state: Option> = None; + // Load buffer for the previous state. + // This amount of recursion (<10 levels) should be OK. + let t = std::time::Instant::now(); + let (_buffer_slot, mut buffer) = match self.hierarchy.storage_strategy(slot)? { + // Base case. + StorageStrategy::Snapshot => { + let state = self + .load_cold_state_as_snapshot(slot)? + .ok_or(Error::MissingSnapshot(slot))?; + let buffer = HDiffBuffer::from_state(state); + + self.diff_buffer_cache.lock().put(slot, buffer.clone()); + debug!( + self.log, + "Added diff buffer to cache"; + "load_time_ms" => t.elapsed().as_millis(), + "slot" => slot + ); - // Try to get a more recent state from the cache to avoid massive blocks replay. - for (s, state) in self.state_cache.lock().iter() { - if s.as_u64() / self.config.slots_per_restore_point == low_restore_point_idx - && *s < slot - && low_slot < *s - { - low_slot = *s; - low_state = Some(state.clone()); + return Ok((slot, buffer)); } - } - - // If low_state is still None, use load_restore_point_by_index to load the state. - let low_state = match low_state { - Some(state) => state, - None => self.load_restore_point_by_index(low_restore_point_idx)?, + // Recursive case. + StorageStrategy::DiffFrom(from) => self.load_hdiff_buffer_for_slot(from)?, + StorageStrategy::ReplayFrom(from) => return self.load_hdiff_buffer_for_slot(from), }; - // Acquire the read lock, so that the split can't change while this is happening. - let split = self.split.read_recursive(); - - let high_restore_point = self.get_restore_point(high_restore_point_idx, &split)?; - - // 2. Load the blocks from the high restore point back to the low point. - let blocks = self.load_blocks_to_replay( - low_slot, - slot, - self.get_high_restore_point_block_root(&high_restore_point, slot)?, - )?; - - // 3. Replay the blocks on top of the low point. - // Use a forwards state root iterator to avoid doing any tree hashing. - // The state root of the high restore point should never be used, so is safely set to 0. - let state_root_iter = self.forwards_state_roots_iterator_until( - low_slot, - slot, - || Ok((high_restore_point, Hash256::zero())), - &self.spec, - )?; - - let state = self.replay_blocks( - low_state, - blocks, - slot, - Some(state_root_iter), - StateProcessingStrategy::Accurate, - )?; - - // If state is not error, put it in the cache. - self.state_cache.lock().put(slot, state.clone()); + // Load diff and apply it to buffer. + let diff = self.load_hdiff_for_slot(slot)?; + diff.apply(&mut buffer)?; - Ok(state) - } + self.diff_buffer_cache.lock().put(slot, buffer.clone()); + debug!( + self.log, + "Added diff buffer to cache"; + "load_time_ms" => t.elapsed().as_millis(), + "slot" => slot + ); - /// Get the restore point with the given index, or if it is out of bounds, the split state. - pub(crate) fn get_restore_point( - &self, - restore_point_idx: u64, - split: &Split, - ) -> Result, Error> { - if restore_point_idx * self.config.slots_per_restore_point >= split.slot.as_u64() { - self.get_state(&split.state_root, Some(split.slot))? - .ok_or(HotColdDBError::MissingSplitState( - split.state_root, - split.slot, - )) - .map_err(Into::into) - } else { - self.load_restore_point_by_index(restore_point_idx) - } + Ok((slot, buffer)) } - /// Get a suitable block root for backtracking from `high_restore_point` to the state at `slot`. - /// - /// Defaults to the block root for `slot`, which *should* be in range. - fn get_high_restore_point_block_root( + /// Load cold blocks between `start_slot` and `end_slot` inclusive. + pub fn load_cold_blocks( &self, - high_restore_point: &BeaconState, - slot: Slot, - ) -> Result { - high_restore_point - .get_block_root(slot) - .or_else(|_| high_restore_point.get_oldest_block_root()) - .copied() - .map_err(HotColdDBError::RestorePointBlockHashError) + start_slot: Slot, + end_slot: Slot, + ) -> Result>, Error> { + process_results( + (start_slot.as_u64()..=end_slot.as_u64()) + .map(Slot::new) + .map(|slot| self.get_cold_blinded_block_by_slot(slot)), + |iter| iter.flatten().collect(), + ) } /// Load the blocks between `start_slot` and `end_slot` by backtracking from `end_block_hash`. @@ -1390,35 +1956,33 @@ impl, Cold: ItemStore> HotColdDB /// /// Will skip slots as necessary. The returned state is not guaranteed /// to have any caches built, beyond those immediately required by block processing. - fn replay_blocks( + pub fn replay_blocks( &self, state: BeaconState, blocks: Vec>>, target_slot: Slot, - state_root_iter: Option>>, - state_processing_strategy: StateProcessingStrategy, + state_root_iter: impl Iterator>, + pre_slot_hook: Option>, ) -> Result, Error> { let mut block_replayer = BlockReplayer::new(state, &self.spec) - .state_processing_strategy(state_processing_strategy) .no_signature_verification() - .minimal_block_root_verification(); + .minimal_block_root_verification() + .state_root_iter(state_root_iter); - let have_state_root_iterator = state_root_iter.is_some(); - if let Some(state_root_iter) = state_root_iter { - block_replayer = block_replayer.state_root_iter(state_root_iter); + if let Some(pre_slot_hook) = pre_slot_hook { + block_replayer = block_replayer.pre_slot_hook(pre_slot_hook); } block_replayer .apply_blocks(blocks, Some(target_slot)) .map(|block_replayer| { - if have_state_root_iterator && block_replayer.state_root_miss() { + if block_replayer.state_root_miss() { warn!( self.log, - "State root iterator miss"; + "State root cache miss during block replay"; "slot" => target_slot, ); } - block_replayer.into_state() }) } @@ -1474,30 +2038,6 @@ impl, Cold: ItemStore> HotColdDB }; } - /// Fetch the slot of the most recently stored restore point (if any). - pub fn get_latest_restore_point_slot(&self) -> Option { - let split_slot = self.get_split_slot(); - let anchor = self.get_anchor_info(); - - // There are no restore points stored if the state upper limit lies in the hot database, - // and the lower limit is zero. It hasn't been reached yet, and may never be. - if anchor.as_ref().map_or(false, |a| { - a.state_upper_limit >= split_slot && a.state_lower_limit == 0 - }) { - None - } else if let Some(lower_limit) = anchor - .map(|a| a.state_lower_limit) - .filter(|limit| *limit > 0) - { - Some(lower_limit) - } else { - Some( - (split_slot - 1) / self.config.slots_per_restore_point - * self.config.slots_per_restore_point, - ) - } - } - /// Load the database schema version from disk. fn load_schema_version(&self) -> Result, Error> { self.hot_db.get(&SCHEMA_VERSION_KEY) @@ -1530,16 +2070,13 @@ impl, Cold: ItemStore> HotColdDB retain_historic_states: bool, ) -> Result { let anchor_slot = block.slot(); - let slots_per_restore_point = self.config.slots_per_restore_point; + // Set the `state_upper_limit` to the slot of the *next* checkpoint. + let next_snapshot_slot = self.hierarchy.next_snapshot_slot(anchor_slot)?; let state_upper_limit = if !retain_historic_states { STATE_UPPER_LIMIT_NO_RETAIN - } else if anchor_slot % slots_per_restore_point == 0 { - anchor_slot } else { - // Set the `state_upper_limit` to the slot of the *next* restore point. - // See `get_state_upper_limit` for rationale. - (anchor_slot / slots_per_restore_point + 1) * slots_per_restore_point + next_snapshot_slot }; let anchor_info = if state_upper_limit == 0 && anchor_slot == 0 { // Genesis archive node: no anchor because we *will* store all states. @@ -1770,6 +2307,7 @@ impl, Cold: ItemStore> HotColdDB } /// Load the state root of a restore point. + #[allow(unused)] fn load_restore_point_hash(&self, restore_point_index: u64) -> Result { let key = Self::restore_point_key(restore_point_index); self.cold_db @@ -1779,18 +2317,21 @@ impl, Cold: ItemStore> HotColdDB } /// Store the state root of a restore point. + #[allow(unused)] fn store_restore_point_hash( &self, restore_point_index: u64, state_root: Hash256, ops: &mut Vec, - ) { + ) -> Result<(), Error> { let value = &RestorePointHash { state_root }; let op = value.as_kv_store_op(Self::restore_point_key(restore_point_index)); ops.push(op); + Ok(()) } /// Convert a `restore_point_index` into a database key. + #[allow(unused)] fn restore_point_key(restore_point_index: u64) -> Hash256 { Hash256::from_low_u64_be(restore_point_index) } @@ -1811,6 +2352,18 @@ impl, Cold: ItemStore> HotColdDB self.hot_db.get(state_root) } + /// Iterate all hot state summaries in the database. + pub fn iter_hot_state_summaries( + &self, + ) -> impl Iterator> + '_ { + self.hot_db + .iter_column(DBColumn::BeaconStateSummary) + .map(|res| { + let (key, value_bytes) = res?; + Ok((key, HotStateSummary::from_store_bytes(&value_bytes)?)) + }) + } + /// Load the temporary flag for a state root, if one exists. /// /// Returns `Some` if the state is temporary, or `None` if the state is permanent or does not @@ -1822,52 +2375,6 @@ impl, Cold: ItemStore> HotColdDB self.hot_db.get(state_root) } - /// Verify that a parsed config is valid. - fn verify_config(config: &StoreConfig) -> Result<(), HotColdDBError> { - Self::verify_slots_per_restore_point(config.slots_per_restore_point)?; - Self::verify_epochs_per_blob_prune(config.epochs_per_blob_prune) - } - - /// Check that the restore point frequency is valid. - /// - /// Specifically, check that it is: - /// (1) A divisor of the number of slots per historical root, and - /// (2) Divisible by the number of slots per epoch - /// - /// - /// (1) ensures that we have at least one restore point within range of our state - /// root history when iterating backwards (and allows for more frequent restore points if - /// desired). - /// - /// (2) ensures that restore points align with hot state summaries, making it - /// quick to migrate hot to cold. - fn verify_slots_per_restore_point(slots_per_restore_point: u64) -> Result<(), HotColdDBError> { - let slots_per_historical_root = E::SlotsPerHistoricalRoot::to_u64(); - let slots_per_epoch = E::slots_per_epoch(); - if slots_per_restore_point > 0 - && slots_per_historical_root % slots_per_restore_point == 0 - && slots_per_restore_point % slots_per_epoch == 0 - { - Ok(()) - } else { - Err(HotColdDBError::InvalidSlotsPerRestorePoint { - slots_per_restore_point, - slots_per_historical_root, - slots_per_epoch, - }) - } - } - - // Check that epochs_per_blob_prune is at least 1 epoch to avoid attempting to prune the same - // epochs over and over again. - fn verify_epochs_per_blob_prune(epochs_per_blob_prune: u64) -> Result<(), HotColdDBError> { - if epochs_per_blob_prune > 0 { - Ok(()) - } else { - Err(HotColdDBError::ZeroEpochsPerBlobPrune) - } - } - /// Run a compaction pass to free up space used by deleted states. pub fn compact(&self) -> Result<(), Error> { self.hot_db.compact()?; @@ -1879,23 +2386,17 @@ impl, Cold: ItemStore> HotColdDB self.config.compact_on_prune } - /// Load the checkpoint to begin pruning from (the "old finalized checkpoint"). - pub fn load_pruning_checkpoint(&self) -> Result, Error> { - Ok(self - .hot_db - .get(&PRUNING_CHECKPOINT_KEY)? - .map(|pc: PruningCheckpoint| pc.checkpoint)) - } - - /// Store the checkpoint to begin pruning from (the "old finalized checkpoint"). - pub fn store_pruning_checkpoint(&self, checkpoint: Checkpoint) -> Result<(), Error> { - self.hot_db - .do_atomically(vec![self.pruning_checkpoint_store_op(checkpoint)]) - } - - /// Create a staged store for the pruning checkpoint. - pub fn pruning_checkpoint_store_op(&self, checkpoint: Checkpoint) -> KeyValueStoreOp { - PruningCheckpoint { checkpoint }.as_kv_store_op(PRUNING_CHECKPOINT_KEY) + /// Get the checkpoint to begin pruning from (the "old finalized checkpoint"). + pub fn get_pruning_checkpoint(&self) -> Checkpoint { + // Since tree-states we infer the pruning checkpoint from the split, as this is simpler & + // safer in the presence of crashes that occur after pruning but before the split is + // updated. + // FIXME(sproul): ensure delete PRUNING_CHECKPOINT_KEY is deleted in DB migration + let split = self.get_split_info(); + Checkpoint { + epoch: split.slot.epoch(E::slots_per_epoch()), + root: split.block_root, + } } /// Load the timestamp of the last compaction as a `Duration` since the UNIX epoch. @@ -1924,12 +2425,12 @@ impl, Cold: ItemStore> HotColdDB block_root: Hash256, ) -> Result, Error> { let mut ops = vec![]; - let mut block_root_writer = - ChunkWriter::::new(&self.cold_db, start_slot.as_usize())?; - for slot in start_slot.as_usize()..end_slot.as_usize() { - block_root_writer.set(slot, block_root, &mut ops)?; + for slot in start_slot.as_u64()..end_slot.as_u64() { + ops.push(KeyValueStoreOp::PutKeyValue( + get_key_for_col(DBColumn::BeaconBlockRoots.into(), &slot.to_be_bytes()), + block_root.as_bytes().to_vec(), + )); } - block_root_writer.write(&mut ops)?; Ok(ops) } @@ -2152,21 +2653,16 @@ impl, Cold: ItemStore> HotColdDB let mut ops = vec![]; let mut last_pruned_block_root = None; - for res in self.forwards_block_roots_iterator_until( - oldest_blob_slot, - end_slot, - || { - let (_, split_state) = self - .get_advanced_hot_state(split.block_root, split.slot, split.state_root)? - .ok_or(HotColdDBError::MissingSplitState( - split.state_root, - split.slot, - ))?; - - Ok((split_state, split.block_root)) - }, - &self.spec, - )? { + for res in self.forwards_block_roots_iterator_until(oldest_blob_slot, end_slot, || { + let (_, split_state) = self + .get_advanced_hot_state(split.block_root, split.slot, split.state_root)? + .ok_or(HotColdDBError::MissingSplitState( + split.state_root, + split.slot, + ))?; + + Ok((split_state, split.block_root)) + })? { let (block_root, slot) = match res { Ok(tuple) => tuple, Err(e) => { @@ -2212,84 +2708,6 @@ impl, Cold: ItemStore> HotColdDB Ok(()) } - /// This function fills in missing block roots between last restore point slot and split - /// slot, if any. - pub fn heal_freezer_block_roots_at_split(&self) -> Result<(), Error> { - let split = self.get_split_info(); - let last_restore_point_slot = (split.slot - 1) / self.config.slots_per_restore_point - * self.config.slots_per_restore_point; - - // Load split state (which has access to block roots). - let (_, split_state) = self - .get_advanced_hot_state(split.block_root, split.slot, split.state_root)? - .ok_or(HotColdDBError::MissingSplitState( - split.state_root, - split.slot, - ))?; - - let mut batch = vec![]; - let mut chunk_writer = ChunkWriter::::new( - &self.cold_db, - last_restore_point_slot.as_usize(), - )?; - - for slot in (last_restore_point_slot.as_u64()..split.slot.as_u64()).map(Slot::new) { - let block_root = *split_state.get_block_root(slot)?; - chunk_writer.set(slot.as_usize(), block_root, &mut batch)?; - } - chunk_writer.write(&mut batch)?; - self.cold_db.do_atomically(batch)?; - - Ok(()) - } - - pub fn heal_freezer_block_roots_at_genesis(&self) -> Result<(), Error> { - let oldest_block_slot = self.get_oldest_block_slot(); - let split_slot = self.get_split_slot(); - - // Check if backfill has been completed AND the freezer db has data in it - if oldest_block_slot != 0 || split_slot == 0 { - return Ok(()); - } - - let mut block_root_iter = self.forwards_block_roots_iterator_until( - Slot::new(0), - split_slot - 1, - || { - Err(Error::DBError { - message: "Should not require end state".to_string(), - }) - }, - &self.spec, - )?; - - let (genesis_block_root, _) = block_root_iter.next().ok_or_else(|| Error::DBError { - message: "Genesis block root missing".to_string(), - })??; - - let slots_to_fix = itertools::process_results(block_root_iter, |iter| { - iter.take_while(|(block_root, _)| block_root.is_zero()) - .map(|(_, slot)| slot) - .collect::>() - })?; - - let Some(first_slot) = slots_to_fix.first() else { - return Ok(()); - }; - - let mut chunk_writer = - ChunkWriter::::new(&self.cold_db, first_slot.as_usize())?; - let mut ops = vec![]; - for slot in slots_to_fix { - chunk_writer.set(slot.as_usize(), genesis_block_root, &mut ops)?; - } - - chunk_writer.write(&mut ops)?; - self.cold_db.do_atomically(ops)?; - - Ok(()) - } - /// Delete *all* states from the freezer database and update the anchor accordingly. /// /// WARNING: this method deletes the genesis state and replaces it with the provided @@ -2301,9 +2719,6 @@ impl, Cold: ItemStore> HotColdDB genesis_state_root: Hash256, genesis_state: &BeaconState, ) -> Result<(), Error> { - // Make sure there is no missing block roots before pruning - self.heal_freezer_block_roots_at_split()?; - // Update the anchor to use the dummy state upper limit and disable historic state storage. let old_anchor = self.get_anchor_info(); let new_anchor = if let Some(old_anchor) = old_anchor.clone() { @@ -2334,6 +2749,7 @@ impl, Cold: ItemStore> HotColdDB let columns = [ DBColumn::BeaconState, DBColumn::BeaconStateSummary, + DBColumn::BeaconStateDiff, DBColumn::BeaconRestorePoint, DBColumn::BeaconStateRoots, DBColumn::BeaconHistoricalRoots, @@ -2413,27 +2829,27 @@ pub fn migrate_database, Cold: ItemStore>( return Err(HotColdDBError::FreezeSlotUnaligned(finalized_state.slot()).into()); } - let mut hot_db_ops = vec![]; - let mut cold_db_ops = vec![]; + // Store the new finalized state as a full state in the database. It would likely previously + // have been stored in memory, or maybe as a diff. + store.store_full_state(&finalized_state_root, finalized_state)?; - // Chunk writer for the linear block roots in the freezer DB. - // Start at the new upper limit because we iterate backwards. - let new_frozen_block_root_upper_limit = finalized_state.slot().as_usize().saturating_sub(1); - let mut block_root_writer = - ChunkWriter::::new(&store.cold_db, new_frozen_block_root_upper_limit)?; + // Copy all of the states between the new finalized state and the split slot, from the hot DB to + // the cold DB. + let mut hot_db_ops: Vec> = Vec::new(); + let mut cold_db_block_ops: Vec = vec![]; - // 1. Copy all of the states between the new finalized state and the split slot, from the hot DB - // to the cold DB. Delete the execution payloads of these now-finalized blocks. - let state_root_iter = RootsIterator::new(&store, finalized_state); - for maybe_tuple in state_root_iter.take_while(|result| match result { - Ok((_, _, slot)) => { - slot >= ¤t_split_slot - && anchor_slot.map_or(true, |anchor_slot| slot >= &anchor_slot) - } - Err(_) => true, - }) { - let (block_root, state_root, slot) = maybe_tuple?; + let state_roots = RootsIterator::new(&store, finalized_state) + .take_while(|result| match result { + Ok((_, _, slot)) => { + slot >= ¤t_split_slot + && anchor_slot.map_or(true, |anchor_slot| slot >= &anchor_slot) + } + Err(_) => true, + }) + .collect::, _>>()?; + // Iterate states in slot ascending order, as they are stored wrt previous states. + for (block_root, state_root, slot) in state_roots.into_iter().rev() { // Delete the execution payload if payload pruning is enabled. At a skipped slot we may // delete the payload for the finalized block itself, but that's OK as we only guarantee // that payloads are present for slots >= the split slot. The payload fetching code is also @@ -2442,12 +2858,32 @@ pub fn migrate_database, Cold: ItemStore>( hot_db_ops.push(StoreOp::DeleteExecutionPayload(block_root)); } + // Move the blinded block from the hot database to the freezer. + // FIXME(sproul): make this load lazy + let blinded_block = store + .get_blinded_block(&block_root, None)? + .ok_or(Error::BlockNotFound(block_root))?; + if blinded_block.slot() == slot || slot == current_split_slot { + store.blinded_block_as_cold_kv_store_ops( + &block_root, + &blinded_block, + &mut cold_db_block_ops, + )?; + hot_db_ops.push(StoreOp::DeleteBlock(block_root)); + } + + // Store the slot to block root mapping. + cold_db_block_ops.push(KeyValueStoreOp::PutKeyValue( + get_key_for_col( + DBColumn::BeaconBlockRoots.into(), + &slot.as_u64().to_be_bytes(), + ), + block_root.as_bytes().to_vec(), + )); + // Delete the old summary, and the full state if we lie on an epoch boundary. hot_db_ops.push(StoreOp::DeleteState(state_root, Some(slot))); - // Store the block root for this slot in the linear array of frozen block roots. - block_root_writer.set(slot.as_usize(), block_root, &mut cold_db_ops)?; - // Do not try to store states if a restore point is yet to be stored, or will never be // stored (see `STATE_UPPER_LIMIT_NO_RETAIN`). Make an exception for the genesis state // which always needs to be copied from the hot DB to the freezer and should not be deleted. @@ -2457,47 +2893,50 @@ pub fn migrate_database, Cold: ItemStore>( .map_or(false, |anchor| slot < anchor.state_upper_limit) { debug!(store.log, "Pruning finalized state"; "slot" => slot); - continue; } - // Store a pointer from this state root to its slot, so we can later reconstruct states - // from their state root alone. - let cold_state_summary = ColdStateSummary { slot }; - let op = cold_state_summary.as_kv_store_op(state_root); - cold_db_ops.push(op); + let mut cold_db_ops: Vec = Vec::new(); - if slot % store.config.slots_per_restore_point == 0 { - let state: BeaconState = get_full_state(&store.hot_db, &state_root, &store.spec)? + // Only store the cold state if it's on a diff boundary + if matches!( + store.hierarchy.storage_strategy(slot)?, + StorageStrategy::ReplayFrom(..) + ) { + // Store slot -> state_root and state_root -> slot mappings. + store.store_cold_state_summary(&state_root, slot, &mut cold_db_ops)?; + } else { + let state: BeaconState = store + .get_hot_state(&state_root)? .ok_or(HotColdDBError::MissingStateToFreeze(state_root))?; store.store_cold_state(&state_root, &state, &mut cold_db_ops)?; - - // Commit the batch of cold DB ops whenever a full state is written. Each state stored - // may read the linear fields of previous states stored. - store - .cold_db - .do_atomically(std::mem::take(&mut cold_db_ops))?; } - } - // Finish writing the block roots and commit the remaining cold DB ops. - block_root_writer.write(&mut cold_db_ops)?; - store.cold_db.do_atomically(cold_db_ops)?; + // Cold states are diffed with respect to each other, so we need to finish writing previous + // states before storing new ones. + store.cold_db.do_atomically(cold_db_ops)?; + } // Warning: Critical section. We have to take care not to put any of the two databases in an // inconsistent state if the OS process dies at any point during the freezing // procedure. // // Since it is pretty much impossible to be atomic across more than one database, we trade - // losing track of states to delete, for consistency. In other words: We should be safe to die - // at any point below but it may happen that some states won't be deleted from the hot database - // and will remain there forever. Since dying in these particular few lines should be an - // exceedingly rare event, this should be an acceptable tradeoff. + // temporarily losing track of blocks to delete, for consistency. In other words: We should be + // safe to die at any point below but it may happen that some blocks won't be deleted from the + // hot database and will remain there forever. We may also temporarily abandon states, but + // they will get picked up by the state pruning that iterates over the whole column. // Flush to disk all the states that have just been migrated to the cold store. + store.cold_db.do_atomically(cold_db_block_ops)?; store.cold_db.sync()?; + // Update the split. + // + // NOTE(sproul): We do this in its own fsync'd transaction mostly for historical reasons, but + // I'm scared to change it, because doing an fsync with *more data* while holding the split + // write lock might have terrible performance implications (jamming the split for 100-500ms+). { let mut split_guard = store.split.write(); let latest_split_slot = split_guard.slot; @@ -2528,15 +2967,22 @@ pub fn migrate_database, Cold: ItemStore>( }; store.hot_db.put_sync(&SPLIT_KEY, &split)?; - // Split point is now persisted in the hot database on disk. The in-memory split point - // hasn't been modified elsewhere since we keep a write lock on it. It's safe to update + // Split point is now persisted in the hot database on disk. The in-memory split point + // hasn't been modified elsewhere since we keep a write lock on it. It's safe to update // the in-memory split point now. *split_guard = split; } - // Delete the states from the hot database if we got this far. + // Delete the blocks and states from the hot database if we got this far. store.do_atomically_with_block_and_blobs_cache(hot_db_ops)?; + // Update the cache's view of the finalized state. + store.update_finalized_state( + finalized_state_root, + finalized_block_root, + finalized_state.clone(), + )?; + debug!( store.log, "Freezer migration complete"; @@ -2575,54 +3021,89 @@ impl StoreItem for Split { } } -/// Type hint. -fn no_state_root_iter() -> Option>> { - None -} - /// Struct for summarising a state in the hot database. /// /// Allows full reconstruction by replaying blocks. -#[derive(Debug, Clone, Copy, Default, Encode, Decode)] +// FIXME(sproul): change to V20 +#[superstruct( + variants(V1, V10), + variant_attributes(derive(Debug, Clone, Copy, Default, Encode, Decode)), + no_enum +)] pub struct HotStateSummary { pub slot: Slot, pub latest_block_root: Hash256, - epoch_boundary_state_root: Hash256, + /// The state root of a state prior to this state with respect to which this state's diff is + /// stored. + /// + /// Set to 0 if this state *is not* stored as a diff. + /// + /// Formerly known as the `epoch_boundary_state_root`. + pub diff_base_state_root: Hash256, + /// The slot of the state with `diff_base_state_root`, or 0 if no diff is stored. + pub diff_base_slot: Slot, + /// The state root of the state at the prior slot. + #[superstruct(only(V10))] + pub prev_state_root: Hash256, } -impl StoreItem for HotStateSummary { - fn db_column() -> DBColumn { - DBColumn::BeaconStateSummary - } +pub type HotStateSummary = HotStateSummaryV10; - fn as_store_bytes(&self) -> Vec { - self.as_ssz_bytes() - } +macro_rules! impl_store_item_summary { + ($t:ty) => { + impl StoreItem for $t { + fn db_column() -> DBColumn { + DBColumn::BeaconStateSummary + } - fn from_store_bytes(bytes: &[u8]) -> Result { - Ok(Self::from_ssz_bytes(bytes)?) - } + fn as_store_bytes(&self) -> Vec { + self.as_ssz_bytes() + } + + fn from_store_bytes(bytes: &[u8]) -> Result { + Ok(Self::from_ssz_bytes(bytes)?) + } + } + }; } +impl_store_item_summary!(HotStateSummaryV1); +impl_store_item_summary!(HotStateSummaryV10); impl HotStateSummary { /// Construct a new summary of the given state. - pub fn new(state_root: &Hash256, state: &BeaconState) -> Result { + pub fn new( + state_root: &Hash256, + state: &BeaconState, + diff_base_slot: Option, + ) -> Result { // Fill in the state root on the latest block header if necessary (this happens on all // slots where there isn't a skip). + let slot = state.slot(); let latest_block_root = state.get_latest_block_root(*state_root); - let epoch_boundary_slot = state.slot() / E::slots_per_epoch() * E::slots_per_epoch(); - let epoch_boundary_state_root = if epoch_boundary_slot == state.slot() { - *state_root + + // Set the diff state root as appropriate. + let diff_base_state_root = if let Some(base_slot) = diff_base_slot { + *state + .get_state_root(base_slot) + .map_err(HotColdDBError::HotStateSummaryError)? } else { + Hash256::zero() + }; + + let prev_state_root = if let Ok(prev_slot) = slot.safe_sub(1) { *state - .get_state_root(epoch_boundary_slot) + .get_state_root(prev_slot) .map_err(HotColdDBError::HotStateSummaryError)? + } else { + Hash256::zero() }; Ok(HotStateSummary { - slot: state.slot(), + slot, latest_block_root, - epoch_boundary_state_root, + diff_base_state_root, + diff_base_slot: diff_base_slot.unwrap_or(Slot::new(0)), + prev_state_root, }) } } diff --git a/beacon_node/store/src/hot_state_iter.rs b/beacon_node/store/src/hot_state_iter.rs new file mode 100644 index 00000000000..22ecf1dadfd --- /dev/null +++ b/beacon_node/store/src/hot_state_iter.rs @@ -0,0 +1,50 @@ +use crate::{hot_cold_store::HotColdDBError, Error, HotColdDB, HotStateSummary, ItemStore}; +use types::{EthSpec, Hash256, Slot}; + +pub struct HotStateRootIter<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { + store: &'a HotColdDB, + next_slot: Slot, + next_state_root: Hash256, +} + +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> HotStateRootIter<'a, E, Hot, Cold> { + pub fn new( + store: &'a HotColdDB, + next_slot: Slot, + next_state_root: Hash256, + ) -> Self { + Self { + store, + next_slot, + next_state_root, + } + } + + fn do_next(&mut self) -> Result, Error> { + if self.next_state_root.is_zero() { + return Ok(None); + } + + let summary = self + .store + .load_hot_state_summary(&self.next_state_root)? + .ok_or(HotColdDBError::MissingHotStateSummary(self.next_state_root))?; + + let state_root = self.next_state_root; + + self.next_state_root = summary.prev_state_root; + self.next_slot -= 1; + + Ok(Some((state_root, summary))) + } +} + +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> Iterator + for HotStateRootIter<'a, E, Hot, Cold> +{ + type Item = Result<(Hash256, HotStateSummary), Error>; + + fn next(&mut self) -> Option { + self.do_next().transpose() + } +} diff --git a/beacon_node/store/src/impls.rs b/beacon_node/store/src/impls.rs index 736585a72aa..b2af9a408ef 100644 --- a/beacon_node/store/src/impls.rs +++ b/beacon_node/store/src/impls.rs @@ -1,2 +1,3 @@ pub mod beacon_state; pub mod execution_payload; +pub mod frozen_block_slot; diff --git a/beacon_node/store/src/impls/beacon_state.rs b/beacon_node/store/src/impls/beacon_state.rs index d08bf564927..2a6fdd482a7 100644 --- a/beacon_node/store/src/impls/beacon_state.rs +++ b/beacon_node/store/src/impls/beacon_state.rs @@ -1,60 +1,81 @@ use crate::*; -use ssz::{DecodeError, Encode}; +use ssz::Encode; use ssz_derive::Encode; +use std::io::{Read, Write}; +use zstd::{Decoder, Encoder}; pub fn store_full_state( state_root: &Hash256, state: &BeaconState, ops: &mut Vec, + config: &StoreConfig, ) -> Result<(), Error> { let bytes = { let _overhead_timer = metrics::start_timer(&metrics::BEACON_STATE_WRITE_OVERHEAD_TIMES); StorageContainer::new(state).as_ssz_bytes() }; - metrics::inc_counter_by(&metrics::BEACON_STATE_WRITE_BYTES, bytes.len() as u64); + let mut compressed_value = Vec::with_capacity(config.estimate_compressed_size(bytes.len())); + let mut encoder = Encoder::new(&mut compressed_value, config.compression_level) + .map_err(Error::Compression)?; + encoder.write_all(&bytes).map_err(Error::Compression)?; + encoder.finish().map_err(Error::Compression)?; + + metrics::inc_counter_by( + &metrics::BEACON_STATE_WRITE_BYTES, + compressed_value.len() as u64, + ); metrics::inc_counter(&metrics::BEACON_STATE_WRITE_COUNT); + let key = get_key_for_col(DBColumn::BeaconState.into(), state_root.as_bytes()); - ops.push(KeyValueStoreOp::PutKeyValue(key, bytes)); + ops.push(KeyValueStoreOp::PutKeyValue(key, compressed_value)); Ok(()) } -pub fn get_full_state, E: EthSpec>( +pub fn get_full_state, E: EthSpec, F>( db: &KV, state_root: &Hash256, + immutable_validators: F, + config: &StoreConfig, spec: &ChainSpec, -) -> Result>, Error> { +) -> Result>, Error> +where + F: Fn(usize) -> Option>, +{ let total_timer = metrics::start_timer(&metrics::BEACON_STATE_READ_TIMES); match db.get_bytes(DBColumn::BeaconState.into(), state_root.as_bytes())? { Some(bytes) => { + let mut ssz_bytes = Vec::with_capacity(config.estimate_decompressed_size(bytes.len())); + let mut decoder = Decoder::new(&*bytes).map_err(Error::Compression)?; + decoder + .read_to_end(&mut ssz_bytes) + .map_err(Error::Compression)?; + let overhead_timer = metrics::start_timer(&metrics::BEACON_STATE_READ_OVERHEAD_TIMES); - let container = StorageContainer::from_ssz_bytes(&bytes, spec)?; + let container = StorageContainer::from_ssz_bytes(&ssz_bytes, spec)?; metrics::stop_timer(overhead_timer); metrics::stop_timer(total_timer); metrics::inc_counter(&metrics::BEACON_STATE_READ_COUNT); metrics::inc_counter_by(&metrics::BEACON_STATE_READ_BYTES, bytes.len() as u64); - Ok(Some(container.try_into()?)) + Ok(Some(container.into_beacon_state(immutable_validators)?)) } None => Ok(None), } } /// A container for storing `BeaconState` components. -// TODO: would be more space efficient with the caches stored separately and referenced by hash #[derive(Encode)] -pub struct StorageContainer { - state: BeaconState, - committee_caches: Vec, +pub struct StorageContainer { + state: CompactBeaconState, } impl StorageContainer { /// Create a new instance for storing a `BeaconState`. pub fn new(state: &BeaconState) -> Self { Self { - state: state.clone_with(CloneConfig::none()), - committee_caches: state.committee_caches().to_vec(), + state: state.clone().into_compact_state(), } } @@ -64,36 +85,20 @@ impl StorageContainer { let mut builder = ssz::SszDecoderBuilder::new(bytes); builder.register_anonymous_variable_length_item()?; - builder.register_type::>()?; let mut decoder = builder.build()?; - let state = decoder.decode_next_with(|bytes| BeaconState::from_ssz_bytes(bytes, spec))?; - let committee_caches = decoder.decode_next()?; + let state = + decoder.decode_next_with(|bytes| CompactBeaconState::from_ssz_bytes(bytes, spec))?; - Ok(Self { - state, - committee_caches, - }) + Ok(Self { state }) } -} - -impl TryInto> for StorageContainer { - type Error = Error; - - fn try_into(mut self) -> Result, Error> { - let mut state = self.state; - - for i in (0..CACHED_EPOCHS).rev() { - if i >= self.committee_caches.len() { - return Err(Error::SszDecodeError(DecodeError::BytesInvalid( - "Insufficient committees for BeaconState".to_string(), - ))); - }; - - state.committee_caches_mut()[i] = self.committee_caches.remove(i); - } + fn into_beacon_state(self, immutable_validators: F) -> Result, Error> + where + F: Fn(usize) -> Option>, + { + let state = self.state.try_into_full_state(immutable_validators)?; Ok(state) } } diff --git a/beacon_node/store/src/impls/frozen_block_slot.rs b/beacon_node/store/src/impls/frozen_block_slot.rs new file mode 100644 index 00000000000..67d11b4f081 --- /dev/null +++ b/beacon_node/store/src/impls/frozen_block_slot.rs @@ -0,0 +1,19 @@ +use crate::{DBColumn, Error, StoreItem}; +use ssz::{Decode, Encode}; +use types::Slot; + +pub struct FrozenBlockSlot(pub Slot); + +impl StoreItem for FrozenBlockSlot { + fn db_column() -> DBColumn { + DBColumn::BeaconBlock + } + + fn as_store_bytes(&self) -> Vec { + self.0.as_ssz_bytes() + } + + fn from_store_bytes(bytes: &[u8]) -> Result { + Ok(FrozenBlockSlot(Slot::from_ssz_bytes(bytes)?)) + } +} diff --git a/beacon_node/store/src/iter.rs b/beacon_node/store/src/iter.rs index e459c1c3575..70058bd7e58 100644 --- a/beacon_node/store/src/iter.rs +++ b/beacon_node/store/src/iter.rs @@ -189,7 +189,7 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> RootsIterator<'a, E, block_hash: Hash256, ) -> Result { let block = store - .get_blinded_block(&block_hash)? + .get_blinded_block(&block_hash, None)? .ok_or_else(|| BeaconStateError::MissingBeaconBlock(block_hash.into()))?; let state = store .get_state(&block.state_root(), Some(block.slot()))? @@ -286,7 +286,7 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> let block = if self.decode_any_variant { self.store.get_block_any_variant(&block_root) } else { - self.store.get_blinded_block(&block_root) + self.store.get_blinded_block(&block_root, None) }? .ok_or(Error::BlockNotFound(block_root))?; self.next_block_root = block.message().parent_root(); @@ -329,7 +329,8 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> BlockIterator<'a, E, fn do_next(&mut self) -> Result>>, Error> { if let Some(result) = self.roots.next() { let (root, _slot) = result?; - self.roots.inner.store.get_blinded_block(&root) + // Don't use slot hint here as it could be a skipped slot. + self.roots.inner.store.get_blinded_block(&root, None) } else { Ok(None) } @@ -378,6 +379,9 @@ fn slot_of_prev_restore_point(current_slot: Slot) -> Slot { (current_slot - 1) / slots_per_historical_root * slots_per_historical_root } +/* FIXME(sproul): these tests are broken because they do not store states in a way that is + * compatible with using state diffs in the hot DB. If we keep state diffs, we should rewrite these + * tests to be less quirky. #[cfg(test)] mod test { use super::*; @@ -412,15 +416,16 @@ mod test { let mut hashes = (0..).map(Hash256::from_low_u64_be); let roots_a = state_a.block_roots_mut(); for i in 0..roots_a.len() { - roots_a[i] = hashes.next().unwrap() + *roots_a.get_mut(i).unwrap() = hashes.next().unwrap(); } let roots_b = state_b.block_roots_mut(); for i in 0..roots_b.len() { - roots_b[i] = hashes.next().unwrap() + *roots_b.get_mut(i).unwrap() = hashes.next().unwrap(); } let state_a_root = hashes.next().unwrap(); - state_b.state_roots_mut()[0] = state_a_root; + *state_b.state_roots_mut().get_mut(0).unwrap() = state_a_root; + state_a.apply_pending_mutations().unwrap(); store.put_state(&state_a_root, &state_a).unwrap(); let iter = BlockRootsIterator::new(&store, &state_b); @@ -446,8 +451,11 @@ mod test { #[test] fn state_root_iter() { let log = NullLoggerBuilder.build().unwrap(); - let store = - HotColdDB::open_ephemeral(Config::default(), ChainSpec::minimal(), log).unwrap(); + let config = Config { + epochs_per_state_diff: 256, + ..Config::default() + }; + let store = HotColdDB::open_ephemeral(config, ChainSpec::minimal(), log).unwrap(); let slots_per_historical_root = MainnetEthSpec::slots_per_historical_root(); let mut state_a: BeaconState = get_state(); @@ -472,6 +480,9 @@ mod test { let state_a_root = Hash256::from_low_u64_be(slots_per_historical_root as u64); let state_b_root = Hash256::from_low_u64_be(slots_per_historical_root as u64 * 2); + state_a.apply_pending_mutations().unwrap(); + state_b.apply_pending_mutations().unwrap(); + store.put_state(&state_a_root, &state_a).unwrap(); store.put_state(&state_b_root, &state_b).unwrap(); @@ -504,3 +515,4 @@ mod test { } } } +*/ diff --git a/beacon_node/store/src/leveldb_store.rs b/beacon_node/store/src/leveldb_store.rs index ffd55c16a04..f3c21cb27b9 100644 --- a/beacon_node/store/src/leveldb_store.rs +++ b/beacon_node/store/src/leveldb_store.rs @@ -25,6 +25,7 @@ impl LevelDB { let mut options = Options::new(); options.create_if_missing = true; + options.write_buffer_size = Some(512 * 1024 * 1024); let db = Database::open(path, options)?; let transaction_mutex = Mutex::new(()); diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index e86689b0cf1..ecfeb891b1f 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -10,30 +10,28 @@ #[macro_use] extern crate lazy_static; -mod chunk_writer; -pub mod chunked_iter; -pub mod chunked_vector; pub mod config; pub mod errors; mod forwards_iter; mod garbage_collection; +pub mod hdiff; pub mod hot_cold_store; +mod hot_state_iter; mod impls; mod leveldb_store; mod memory_store; pub mod metadata; pub mod metrics; -mod partial_beacon_state; pub mod reconstruct; +mod state_cache; +pub mod validator_pubkey_cache; pub mod iter; -pub use self::chunk_writer::ChunkWriter; pub use self::config::StoreConfig; pub use self::hot_cold_store::{HotColdDB, HotStateSummary, Split}; pub use self::leveldb_store::LevelDB; pub use self::memory_store::MemoryStore; -pub use self::partial_beacon_state::PartialBeaconState; pub use crate::metadata::BlobInfo; pub use errors::Error; pub use impls::beacon_state::StorageContainer as BeaconStateStorageContainer; @@ -43,6 +41,7 @@ use parking_lot::MutexGuard; use std::sync::Arc; use strum::{EnumString, IntoStaticStr}; pub use types::*; +pub use validator_pubkey_cache::ValidatorPubkeyCache; pub type ColumnIter<'a, K> = Box), Error>> + 'a>; pub type ColumnKeyIter<'a, K> = Box> + 'a>; @@ -89,6 +88,7 @@ pub trait KeyValueStore: Sync + Send + Sized + 'static { // i.e. entries being created and deleted. for column in [ DBColumn::BeaconState, + DBColumn::BeaconStateDiff, DBColumn::BeaconStateSummary, DBColumn::BeaconBlock, ] { @@ -203,7 +203,6 @@ pub enum StoreOp<'a, E: EthSpec> { PutBlock(Hash256, Arc>), PutState(Hash256, &'a BeaconState), PutBlobs(Hash256, BlobSidecarList), - PutStateSummary(Hash256, HotStateSummary), PutStateTemporaryFlag(Hash256), DeleteStateTemporaryFlag(Hash256), DeleteBlock(Hash256), @@ -219,13 +218,30 @@ pub enum DBColumn { /// For data related to the database itself. #[strum(serialize = "bma")] BeaconMeta, + /// Data related to blocks. + /// + /// - Key: `Hash256` block root. + /// - Value in hot DB: SSZ-encoded blinded block. + /// - Value in cold DB: 8-byte slot of block. #[strum(serialize = "blk")] BeaconBlock, + /// Frozen beacon blocks. + /// + /// - Key: 8-byte slot. + /// - Value: ZSTD-compressed SSZ-encoded blinded block. + #[strum(serialize = "bbf")] + BeaconBlockFrozen, #[strum(serialize = "blb")] BeaconBlob, /// For full `BeaconState`s in the hot database (finalized or fork-boundary states). #[strum(serialize = "ste")] BeaconState, + /// For beacon state snapshots in the freezer DB. + #[strum(serialize = "bsn")] + BeaconStateSnapshot, + /// For compact `BeaconStateDiff`s in the freezer DB. + #[strum(serialize = "bsd")] + BeaconStateDiff, /// For the mapping from state roots to their slots or summaries. #[strum(serialize = "bss")] BeaconStateSummary, @@ -247,7 +263,7 @@ pub enum DBColumn { ForkChoice, #[strum(serialize = "pkc")] PubkeyCache, - /// For the table mapping restore point numbers to state roots. + /// For the legacy table mapping restore point numbers to state roots. #[strum(serialize = "brp")] BeaconRestorePoint, #[strum(serialize = "bbr")] @@ -309,7 +325,10 @@ impl DBColumn { | Self::BeaconStateRoots | Self::BeaconHistoricalRoots | Self::BeaconHistoricalSummaries - | Self::BeaconRandaoMixes => 8, + | Self::BeaconRandaoMixes + | Self::BeaconBlockFrozen + | Self::BeaconStateSnapshot + | Self::BeaconStateDiff => 8, } } } diff --git a/beacon_node/store/src/metadata.rs b/beacon_node/store/src/metadata.rs index 1675051bd80..902a3f24991 100644 --- a/beacon_node/store/src/metadata.rs +++ b/beacon_node/store/src/metadata.rs @@ -4,7 +4,7 @@ use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use types::{Checkpoint, Hash256, Slot}; -pub const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(19); +pub const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(24); // All the keys that get stored under the `BeaconMeta` column. // @@ -108,6 +108,11 @@ impl AnchorInfo { pub fn block_backfill_complete(&self, target_slot: Slot) -> bool { self.oldest_block_slot <= target_slot } + + /// Return true if no historic states other than genesis are stored in the database. + pub fn no_historic_states_stored(&self, split_slot: Slot) -> bool { + self.state_lower_limit == 0 && self.state_upper_limit >= split_slot + } } impl StoreItem for AnchorInfo { diff --git a/beacon_node/store/src/metrics.rs b/beacon_node/store/src/metrics.rs index 2d901fdd932..144e9aa493c 100644 --- a/beacon_node/store/src/metrics.rs +++ b/beacon_node/store/src/metrics.rs @@ -54,17 +54,13 @@ lazy_static! { "store_beacon_state_hot_get_total", "Total number of hot beacon states requested from the store (cache or DB)" ); - pub static ref BEACON_STATE_CACHE_HIT_COUNT: Result = try_create_int_counter( - "store_beacon_state_cache_hit_total", - "Number of hits to the store's state cache" - ); - pub static ref BEACON_STATE_CACHE_CLONE_TIME: Result = try_create_histogram( - "store_beacon_state_cache_clone_time", - "Time to load a beacon block from the block cache" - ); pub static ref BEACON_STATE_READ_TIMES: Result = try_create_histogram( "store_beacon_state_read_seconds", - "Total time required to read a BeaconState from the database" + "Total time required to read a full BeaconState from the database" + ); + pub static ref BEACON_HOT_STATE_READ_TIMES: Result = try_create_histogram( + "store_beacon_hot_state_read_seconds", + "Total time required to read a hot BeaconState from the database" ); pub static ref BEACON_STATE_READ_OVERHEAD_TIMES: Result = try_create_histogram( "store_beacon_state_read_overhead_seconds", @@ -90,6 +86,33 @@ lazy_static! { "store_beacon_state_write_bytes_total", "Total number of beacon state bytes written to the DB" ); + /* + * Beacon state diffs + */ + pub static ref BEACON_STATE_DIFF_WRITE_BYTES: Result = try_create_int_counter( + "store_beacon_state_diff_write_bytes_total", + "Total number of bytes written for beacon state diffs" + ); + pub static ref BEACON_STATE_DIFF_WRITE_COUNT: Result = try_create_int_counter( + "store_beacon_state_diff_write_count_total", + "Total number of beacon state diffs written" + ); + pub static ref BEACON_STATE_DIFF_COMPRESSION_RATIO: Result = try_create_float_gauge( + "store_beacon_state_diff_compression_ratio", + "Compression ratio for beacon state diffs (higher is better)" + ); + pub static ref BEACON_STATE_DIFF_COMPUTE_TIME: Result = try_create_histogram( + "store_beacon_state_diff_compute_time", + "Time to calculate a beacon state diff" + ); + pub static ref BEACON_STATE_DIFF_ENCODE_TIME: Result = try_create_histogram( + "store_beacon_state_diff_encode_time", + "Time to encode a beacon state diff as SSZ" + ); + pub static ref BEACON_STATE_DIFF_COMPRESSION_TIME: Result = try_create_histogram( + "store_beacon_state_diff_compression_time", + "Time to compress beacon state SSZ using Flate2" + ); /* * Beacon Block */ diff --git a/beacon_node/store/src/partial_beacon_state.rs b/beacon_node/store/src/partial_beacon_state.rs deleted file mode 100644 index 4e5a2b8e64b..00000000000 --- a/beacon_node/store/src/partial_beacon_state.rs +++ /dev/null @@ -1,534 +0,0 @@ -use crate::chunked_vector::{ - load_variable_list_from_db, load_vector_from_db, BlockRoots, HistoricalRoots, - HistoricalSummaries, RandaoMixes, StateRoots, -}; -use crate::{get_key_for_col, DBColumn, Error, KeyValueStore, KeyValueStoreOp}; -use ssz::{Decode, DecodeError, Encode}; -use ssz_derive::{Decode, Encode}; -use std::sync::Arc; -use types::historical_summary::HistoricalSummary; -use types::superstruct; -use types::*; - -/// Lightweight variant of the `BeaconState` that is stored in the database. -/// -/// Utilises lazy-loading from separate storage for its vector fields. -#[superstruct( - variants(Base, Altair, Merge, Capella, Deneb, Electra), - variant_attributes(derive(Debug, PartialEq, Clone, Encode, Decode)) -)] -#[derive(Debug, PartialEq, Clone, Encode)] -#[ssz(enum_behaviour = "transparent")] -pub struct PartialBeaconState -where - E: EthSpec, -{ - // Versioning - pub genesis_time: u64, - pub genesis_validators_root: Hash256, - #[superstruct(getter(copy))] - pub slot: Slot, - pub fork: Fork, - - // History - pub latest_block_header: BeaconBlockHeader, - - #[ssz(skip_serializing, skip_deserializing)] - pub block_roots: Option>, - #[ssz(skip_serializing, skip_deserializing)] - pub state_roots: Option>, - - #[ssz(skip_serializing, skip_deserializing)] - pub historical_roots: Option>, - - // Ethereum 1.0 chain data - pub eth1_data: Eth1Data, - pub eth1_data_votes: VariableList, - pub eth1_deposit_index: u64, - - // Registry - pub validators: VariableList, - pub balances: VariableList, - - // Shuffling - /// Randao value from the current slot, for patching into the per-epoch randao vector. - pub latest_randao_value: Hash256, - #[ssz(skip_serializing, skip_deserializing)] - pub randao_mixes: Option>, - - // Slashings - slashings: FixedVector, - - // Attestations (genesis fork only) - #[superstruct(only(Base))] - pub previous_epoch_attestations: VariableList, E::MaxPendingAttestations>, - #[superstruct(only(Base))] - pub current_epoch_attestations: VariableList, E::MaxPendingAttestations>, - - // Participation (Altair and later) - #[superstruct(only(Altair, Merge, Capella, Deneb, Electra))] - pub previous_epoch_participation: VariableList, - #[superstruct(only(Altair, Merge, Capella, Deneb, Electra))] - pub current_epoch_participation: VariableList, - - // Finality - pub justification_bits: BitVector, - pub previous_justified_checkpoint: Checkpoint, - pub current_justified_checkpoint: Checkpoint, - pub finalized_checkpoint: Checkpoint, - - // Inactivity - #[superstruct(only(Altair, Merge, Capella, Deneb, Electra))] - pub inactivity_scores: VariableList, - - // Light-client sync committees - #[superstruct(only(Altair, Merge, Capella, Deneb, Electra))] - pub current_sync_committee: Arc>, - #[superstruct(only(Altair, Merge, Capella, Deneb, Electra))] - pub next_sync_committee: Arc>, - - // Execution - #[superstruct( - only(Merge), - partial_getter(rename = "latest_execution_payload_header_merge") - )] - pub latest_execution_payload_header: ExecutionPayloadHeaderMerge, - #[superstruct( - only(Capella), - partial_getter(rename = "latest_execution_payload_header_capella") - )] - pub latest_execution_payload_header: ExecutionPayloadHeaderCapella, - #[superstruct( - only(Deneb), - partial_getter(rename = "latest_execution_payload_header_deneb") - )] - pub latest_execution_payload_header: ExecutionPayloadHeaderDeneb, - #[superstruct( - only(Electra), - partial_getter(rename = "latest_execution_payload_header_electra") - )] - pub latest_execution_payload_header: ExecutionPayloadHeaderElectra, - - // Capella - #[superstruct(only(Capella, Deneb, Electra))] - pub next_withdrawal_index: u64, - #[superstruct(only(Capella, Deneb, Electra))] - pub next_withdrawal_validator_index: u64, - - #[ssz(skip_serializing, skip_deserializing)] - #[superstruct(only(Capella, Deneb, Electra))] - pub historical_summaries: Option>, -} - -/// Implement the conversion function from BeaconState -> PartialBeaconState. -macro_rules! impl_from_state_forgetful { - ($s:ident, $outer:ident, $variant_name:ident, $struct_name:ident, [$($extra_fields:ident),*], [$($extra_fields_opt:ident),*]) => { - PartialBeaconState::$variant_name($struct_name { - // Versioning - genesis_time: $s.genesis_time, - genesis_validators_root: $s.genesis_validators_root, - slot: $s.slot, - fork: $s.fork, - - // History - latest_block_header: $s.latest_block_header.clone(), - block_roots: None, - state_roots: None, - historical_roots: None, - - // Eth1 - eth1_data: $s.eth1_data.clone(), - eth1_data_votes: $s.eth1_data_votes.clone(), - eth1_deposit_index: $s.eth1_deposit_index, - - // Validator registry - validators: $s.validators.clone(), - balances: $s.balances.clone(), - - // Shuffling - latest_randao_value: *$outer - .get_randao_mix($outer.current_epoch()) - .expect("randao at current epoch is OK"), - randao_mixes: None, - - // Slashings - slashings: $s.slashings.clone(), - - // Finality - justification_bits: $s.justification_bits.clone(), - previous_justified_checkpoint: $s.previous_justified_checkpoint, - current_justified_checkpoint: $s.current_justified_checkpoint, - finalized_checkpoint: $s.finalized_checkpoint, - - // Variant-specific fields - $( - $extra_fields: $s.$extra_fields.clone() - ),*, - - // Variant-specific optional - $( - $extra_fields_opt: None - ),* - }) - } -} - -impl PartialBeaconState { - /// Convert a `BeaconState` to a `PartialBeaconState`, while dropping the optional fields. - pub fn from_state_forgetful(outer: &BeaconState) -> Self { - match outer { - BeaconState::Base(s) => impl_from_state_forgetful!( - s, - outer, - Base, - PartialBeaconStateBase, - [previous_epoch_attestations, current_epoch_attestations], - [] - ), - BeaconState::Altair(s) => impl_from_state_forgetful!( - s, - outer, - Altair, - PartialBeaconStateAltair, - [ - previous_epoch_participation, - current_epoch_participation, - current_sync_committee, - next_sync_committee, - inactivity_scores - ], - [] - ), - BeaconState::Merge(s) => impl_from_state_forgetful!( - s, - outer, - Merge, - PartialBeaconStateMerge, - [ - previous_epoch_participation, - current_epoch_participation, - current_sync_committee, - next_sync_committee, - inactivity_scores, - latest_execution_payload_header - ], - [] - ), - BeaconState::Capella(s) => impl_from_state_forgetful!( - s, - outer, - Capella, - PartialBeaconStateCapella, - [ - previous_epoch_participation, - current_epoch_participation, - current_sync_committee, - next_sync_committee, - inactivity_scores, - latest_execution_payload_header, - next_withdrawal_index, - next_withdrawal_validator_index - ], - [historical_summaries] - ), - BeaconState::Deneb(s) => impl_from_state_forgetful!( - s, - outer, - Deneb, - PartialBeaconStateDeneb, - [ - previous_epoch_participation, - current_epoch_participation, - current_sync_committee, - next_sync_committee, - inactivity_scores, - latest_execution_payload_header, - next_withdrawal_index, - next_withdrawal_validator_index - ], - [historical_summaries] - ), - BeaconState::Electra(s) => impl_from_state_forgetful!( - s, - outer, - Electra, - PartialBeaconStateElectra, - [ - previous_epoch_participation, - current_epoch_participation, - current_sync_committee, - next_sync_committee, - inactivity_scores, - latest_execution_payload_header, - next_withdrawal_index, - next_withdrawal_validator_index - ], - [historical_summaries] - ), - } - } - - /// SSZ decode. - pub fn from_ssz_bytes(bytes: &[u8], spec: &ChainSpec) -> Result { - // Slot is after genesis_time (u64) and genesis_validators_root (Hash256). - let slot_offset = ::ssz_fixed_len() + ::ssz_fixed_len(); - let slot_len = ::ssz_fixed_len(); - let slot_bytes = bytes.get(slot_offset..slot_offset + slot_len).ok_or( - DecodeError::InvalidByteLength { - len: bytes.len(), - expected: slot_offset + slot_len, - }, - )?; - - let slot = Slot::from_ssz_bytes(slot_bytes)?; - let fork_at_slot = spec.fork_name_at_slot::(slot); - - Ok(map_fork_name!( - fork_at_slot, - Self, - <_>::from_ssz_bytes(bytes)? - )) - } - - /// Prepare the partial state for storage in the KV database. - pub fn as_kv_store_op(&self, state_root: Hash256) -> KeyValueStoreOp { - let db_key = get_key_for_col(DBColumn::BeaconState.into(), state_root.as_bytes()); - KeyValueStoreOp::PutKeyValue(db_key, self.as_ssz_bytes()) - } - - pub fn load_block_roots>( - &mut self, - store: &S, - spec: &ChainSpec, - ) -> Result<(), Error> { - if self.block_roots().is_none() { - *self.block_roots_mut() = Some(load_vector_from_db::( - store, - self.slot(), - spec, - )?); - } - Ok(()) - } - - pub fn load_state_roots>( - &mut self, - store: &S, - spec: &ChainSpec, - ) -> Result<(), Error> { - if self.state_roots().is_none() { - *self.state_roots_mut() = Some(load_vector_from_db::( - store, - self.slot(), - spec, - )?); - } - Ok(()) - } - - pub fn load_historical_roots>( - &mut self, - store: &S, - spec: &ChainSpec, - ) -> Result<(), Error> { - if self.historical_roots().is_none() { - *self.historical_roots_mut() = Some( - load_variable_list_from_db::(store, self.slot(), spec)?, - ); - } - Ok(()) - } - - pub fn load_historical_summaries>( - &mut self, - store: &S, - spec: &ChainSpec, - ) -> Result<(), Error> { - let slot = self.slot(); - if let Ok(historical_summaries) = self.historical_summaries_mut() { - if historical_summaries.is_none() { - *historical_summaries = - Some(load_variable_list_from_db::( - store, slot, spec, - )?); - } - } - Ok(()) - } - - pub fn load_randao_mixes>( - &mut self, - store: &S, - spec: &ChainSpec, - ) -> Result<(), Error> { - if self.randao_mixes().is_none() { - // Load the per-epoch values from the database - let mut randao_mixes = - load_vector_from_db::(store, self.slot(), spec)?; - - // Patch the value for the current slot into the index for the current epoch - let current_epoch = self.slot().epoch(E::slots_per_epoch()); - let len = randao_mixes.len(); - randao_mixes[current_epoch.as_usize() % len] = *self.latest_randao_value(); - - *self.randao_mixes_mut() = Some(randao_mixes) - } - Ok(()) - } -} - -/// Implement the conversion from PartialBeaconState -> BeaconState. -macro_rules! impl_try_into_beacon_state { - ($inner:ident, $variant_name:ident, $struct_name:ident, [$($extra_fields:ident),*], [$($extra_opt_fields:ident),*]) => { - BeaconState::$variant_name($struct_name { - // Versioning - genesis_time: $inner.genesis_time, - genesis_validators_root: $inner.genesis_validators_root, - slot: $inner.slot, - fork: $inner.fork, - - // History - latest_block_header: $inner.latest_block_header, - block_roots: unpack_field($inner.block_roots)?, - state_roots: unpack_field($inner.state_roots)?, - historical_roots: unpack_field($inner.historical_roots)?, - - // Eth1 - eth1_data: $inner.eth1_data, - eth1_data_votes: $inner.eth1_data_votes, - eth1_deposit_index: $inner.eth1_deposit_index, - - // Validator registry - validators: $inner.validators, - balances: $inner.balances, - - // Shuffling - randao_mixes: unpack_field($inner.randao_mixes)?, - - // Slashings - slashings: $inner.slashings, - - // Finality - justification_bits: $inner.justification_bits, - previous_justified_checkpoint: $inner.previous_justified_checkpoint, - current_justified_checkpoint: $inner.current_justified_checkpoint, - finalized_checkpoint: $inner.finalized_checkpoint, - - // Caching - total_active_balance: <_>::default(), - progressive_balances_cache: <_>::default(), - committee_caches: <_>::default(), - pubkey_cache: <_>::default(), - exit_cache: <_>::default(), - slashings_cache: <_>::default(), - epoch_cache: <_>::default(), - tree_hash_cache: <_>::default(), - - // Variant-specific fields - $( - $extra_fields: $inner.$extra_fields - ),*, - - // Variant-specific optional fields - $( - $extra_opt_fields: unpack_field($inner.$extra_opt_fields)? - ),* - }) - } -} - -fn unpack_field(x: Option) -> Result { - x.ok_or(Error::PartialBeaconStateError) -} - -impl TryInto> for PartialBeaconState { - type Error = Error; - - fn try_into(self) -> Result, Error> { - let state = match self { - PartialBeaconState::Base(inner) => impl_try_into_beacon_state!( - inner, - Base, - BeaconStateBase, - [previous_epoch_attestations, current_epoch_attestations], - [] - ), - PartialBeaconState::Altair(inner) => impl_try_into_beacon_state!( - inner, - Altair, - BeaconStateAltair, - [ - previous_epoch_participation, - current_epoch_participation, - current_sync_committee, - next_sync_committee, - inactivity_scores - ], - [] - ), - PartialBeaconState::Merge(inner) => impl_try_into_beacon_state!( - inner, - Merge, - BeaconStateMerge, - [ - previous_epoch_participation, - current_epoch_participation, - current_sync_committee, - next_sync_committee, - inactivity_scores, - latest_execution_payload_header - ], - [] - ), - PartialBeaconState::Capella(inner) => impl_try_into_beacon_state!( - inner, - Capella, - BeaconStateCapella, - [ - previous_epoch_participation, - current_epoch_participation, - current_sync_committee, - next_sync_committee, - inactivity_scores, - latest_execution_payload_header, - next_withdrawal_index, - next_withdrawal_validator_index - ], - [historical_summaries] - ), - PartialBeaconState::Deneb(inner) => impl_try_into_beacon_state!( - inner, - Deneb, - BeaconStateDeneb, - [ - previous_epoch_participation, - current_epoch_participation, - current_sync_committee, - next_sync_committee, - inactivity_scores, - latest_execution_payload_header, - next_withdrawal_index, - next_withdrawal_validator_index - ], - [historical_summaries] - ), - PartialBeaconState::Electra(inner) => impl_try_into_beacon_state!( - inner, - Electra, - BeaconStateElectra, - [ - previous_epoch_participation, - current_epoch_participation, - current_sync_committee, - next_sync_committee, - inactivity_scores, - latest_execution_payload_header, - next_withdrawal_index, - next_withdrawal_validator_index - ], - [historical_summaries] - ), - }; - Ok(state) - } -} diff --git a/beacon_node/store/src/reconstruct.rs b/beacon_node/store/src/reconstruct.rs index 8fe13777ac4..744f97b28ce 100644 --- a/beacon_node/store/src/reconstruct.rs +++ b/beacon_node/store/src/reconstruct.rs @@ -8,7 +8,7 @@ use state_processing::{ StateProcessingStrategy, VerifyBlockRoot, }; use std::sync::Arc; -use types::{EthSpec, Hash256}; +use types::EthSpec; impl HotColdDB where @@ -16,7 +16,10 @@ where Hot: ItemStore, Cold: ItemStore, { - pub fn reconstruct_historic_states(self: &Arc) -> Result<(), Error> { + pub fn reconstruct_historic_states( + self: &Arc, + num_blocks: Option, + ) -> Result<(), Error> { let Some(mut anchor) = self.get_anchor_info() else { // Nothing to do, history is complete. return Ok(()); @@ -35,26 +38,17 @@ where "start_slot" => anchor.state_lower_limit, ); - let slots_per_restore_point = self.config.slots_per_restore_point; - // Iterate blocks from the state lower limit to the upper limit. - let lower_limit_slot = anchor.state_lower_limit; let split = self.get_split_info(); - let upper_limit_state = self.get_restore_point( - anchor.state_upper_limit.as_u64() / slots_per_restore_point, - &split, - )?; - let upper_limit_slot = upper_limit_state.slot(); - - // Use a dummy root, as we never read the block for the upper limit state. - let upper_limit_block_root = Hash256::repeat_byte(0xff); - - let block_root_iter = self.forwards_block_roots_iterator( - lower_limit_slot, - upper_limit_state, - upper_limit_block_root, - &self.spec, - )?; + let lower_limit_slot = anchor.state_lower_limit; + let upper_limit_slot = std::cmp::min(split.slot, anchor.state_upper_limit); + + // If `num_blocks` is not specified iterate all blocks. + let block_root_iter = self + .forwards_block_roots_iterator_until(lower_limit_slot, upper_limit_slot - 1, || { + panic!("FIXME(sproul): reconstruction doesn't need this state") + })? + .take(num_blocks.unwrap_or(usize::MAX)); // The state to be advanced. let mut state = self @@ -75,7 +69,7 @@ where None } else { Some( - self.get_blinded_block(&block_root)? + self.get_blinded_block(&block_root, Some(slot))? .ok_or(Error::BlockNotFound(block_root))?, ) }; @@ -112,7 +106,7 @@ where self.store_cold_state(&state_root, &state, &mut io_batch)?; // If the slot lies on an epoch boundary, commit the batch and update the anchor. - if slot % slots_per_restore_point == 0 || slot + 1 == upper_limit_slot { + if self.hierarchy.should_commit_immediately(slot)? || slot + 1 == upper_limit_slot { info!( self.log, "State reconstruction in progress"; diff --git a/beacon_node/store/src/state_cache.rs b/beacon_node/store/src/state_cache.rs new file mode 100644 index 00000000000..1bd73c53f8b --- /dev/null +++ b/beacon_node/store/src/state_cache.rs @@ -0,0 +1,283 @@ +use crate::Error; +use lru::LruCache; +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::num::NonZeroUsize; +use types::{BeaconState, Epoch, EthSpec, Hash256, Slot}; + +/// Fraction of the LRU cache to leave intact during culling. +const CULL_EXEMPT_NUMERATOR: usize = 1; +const CULL_EXEMPT_DENOMINATOR: usize = 10; + +/// States that are less than or equal to this many epochs old *could* become finalized and will not +/// be culled from the cache. +const EPOCH_FINALIZATION_LIMIT: u64 = 4; + +#[derive(Debug)] +pub struct FinalizedState { + state_root: Hash256, + state: BeaconState, +} + +/// Map from block_root -> slot -> state_root. +#[derive(Debug, Default)] +pub struct BlockMap { + blocks: HashMap, +} + +/// Map from slot -> state_root. +#[derive(Debug, Default)] +pub struct SlotMap { + slots: BTreeMap, +} + +#[derive(Debug)] +pub struct StateCache { + finalized_state: Option>, + states: LruCache>, + block_map: BlockMap, + capacity: NonZeroUsize, + max_epoch: Epoch, +} + +#[derive(Debug)] +pub enum PutStateOutcome { + Finalized, + Duplicate, + New, +} + +impl StateCache { + pub fn new(capacity: NonZeroUsize) -> Self { + StateCache { + finalized_state: None, + states: LruCache::new(capacity), + block_map: BlockMap::default(), + capacity, + max_epoch: Epoch::new(0), + } + } + + pub fn len(&self) -> usize { + self.states.len() + } + + pub fn update_finalized_state( + &mut self, + state_root: Hash256, + block_root: Hash256, + state: BeaconState, + ) -> Result<(), Error> { + if state.slot() % E::slots_per_epoch() != 0 { + return Err(Error::FinalizedStateUnaligned); + } + + if self + .finalized_state + .as_ref() + .map_or(false, |finalized_state| { + state.slot() < finalized_state.state.slot() + }) + { + return Err(Error::FinalizedStateDecreasingSlot); + } + + // Add to block map. + self.block_map.insert(block_root, state.slot(), state_root); + + // Prune block map. + let state_roots_to_prune = self.block_map.prune(state.slot()); + + // Delete states. + for state_root in state_roots_to_prune { + self.states.pop(&state_root); + } + + // Update finalized state. + self.finalized_state = Some(FinalizedState { state_root, state }); + Ok(()) + } + + /// Return a status indicating whether the state already existed in the cache. + pub fn put_state( + &mut self, + state_root: Hash256, + block_root: Hash256, + state: &BeaconState, + ) -> Result { + if self + .finalized_state + .as_ref() + .map_or(false, |finalized_state| { + finalized_state.state_root == state_root + }) + { + return Ok(PutStateOutcome::Finalized); + } + + if self.states.peek(&state_root).is_some() { + return Ok(PutStateOutcome::Duplicate); + } + + // Refuse states with pending mutations: we want cached states to be as small as possible + // i.e. stored entirely as a binary merkle tree with no updates overlaid. + if state.has_pending_mutations() { + return Err(Error::StateForCacheHasPendingUpdates { + state_root, + slot: state.slot(), + }); + } + + // Update the cache's idea of the max epoch. + self.max_epoch = std::cmp::max(state.current_epoch(), self.max_epoch); + + // If the cache is full, use the custom cull routine to make room. + if let Some(over_capacity) = self.len().checked_sub(self.capacity.get()) { + self.cull(over_capacity + 1); + } + + // Insert the full state into the cache. + self.states.put(state_root, state.clone()); + + // Record the connection from block root and slot to this state. + let slot = state.slot(); + self.block_map.insert(block_root, slot, state_root); + + Ok(PutStateOutcome::New) + } + + pub fn get_by_state_root(&mut self, state_root: Hash256) -> Option> { + if let Some(ref finalized_state) = self.finalized_state { + if state_root == finalized_state.state_root { + return Some(finalized_state.state.clone()); + } + } + self.states.get(&state_root).cloned() + } + + pub fn get_by_block_root( + &mut self, + block_root: Hash256, + slot: Slot, + ) -> Option<(Hash256, BeaconState)> { + let slot_map = self.block_map.blocks.get(&block_root)?; + + // Find the state at `slot`, or failing that the most recent ancestor. + let state_root = slot_map + .slots + .iter() + .rev() + .find_map(|(ancestor_slot, state_root)| { + (*ancestor_slot <= slot).then_some(*state_root) + })?; + + let state = self.get_by_state_root(state_root)?; + Some((state_root, state)) + } + + pub fn delete_state(&mut self, state_root: &Hash256) { + self.states.pop(state_root); + self.block_map.delete(state_root); + } + + pub fn delete_block_states(&mut self, block_root: &Hash256) { + if let Some(slot_map) = self.block_map.delete_block_states(block_root) { + for state_root in slot_map.slots.values() { + self.states.pop(state_root); + } + } + } + + /// Cull approximately `count` states from the cache. + /// + /// States are culled LRU, with the following extra order imposed: + /// + /// - Advanced states. + /// - Mid-epoch unadvanced states. + /// - Epoch-boundary states that are too old to be finalized. + /// - Epoch-boundary states that could be finalized. + pub fn cull(&mut self, count: usize) { + let cull_exempt = std::cmp::max( + 1, + self.len() * CULL_EXEMPT_NUMERATOR / CULL_EXEMPT_DENOMINATOR, + ); + + // Stage 1: gather states to cull. + let mut advanced_state_roots = vec![]; + let mut mid_epoch_state_roots = vec![]; + let mut old_boundary_state_roots = vec![]; + let mut good_boundary_state_roots = vec![]; + for (&state_root, state) in self.states.iter().skip(cull_exempt) { + let is_advanced = state.slot() > state.latest_block_header().slot; + let is_boundary = state.slot() % E::slots_per_epoch() == 0; + let could_finalize = + (self.max_epoch - state.current_epoch()) <= EPOCH_FINALIZATION_LIMIT; + + if is_boundary { + if could_finalize { + good_boundary_state_roots.push(state_root); + } else { + old_boundary_state_roots.push(state_root); + } + } else if is_advanced { + advanced_state_roots.push(state_root); + } else { + mid_epoch_state_roots.push(state_root); + } + + // Terminate early in the common case where we've already found enough junk to cull. + if advanced_state_roots.len() == count { + break; + } + } + + // Stage 2: delete. + // This could probably be more efficient in how it interacts with the block map. + for state_root in advanced_state_roots + .iter() + .chain(mid_epoch_state_roots.iter()) + .chain(old_boundary_state_roots.iter()) + .chain(good_boundary_state_roots.iter()) + .take(count) + { + self.delete_state(state_root); + } + } +} + +impl BlockMap { + fn insert(&mut self, block_root: Hash256, slot: Slot, state_root: Hash256) { + let slot_map = self.blocks.entry(block_root).or_default(); + slot_map.slots.insert(slot, state_root); + } + + fn prune(&mut self, finalized_slot: Slot) -> HashSet { + let mut pruned_states = HashSet::new(); + + self.blocks.retain(|_, slot_map| { + slot_map.slots.retain(|slot, state_root| { + let keep = *slot >= finalized_slot; + if !keep { + pruned_states.insert(*state_root); + } + keep + }); + + !slot_map.slots.is_empty() + }); + + pruned_states + } + + fn delete(&mut self, state_root_to_delete: &Hash256) { + self.blocks.retain(|_, slot_map| { + slot_map + .slots + .retain(|_, state_root| state_root != state_root_to_delete); + !slot_map.slots.is_empty() + }); + } + + fn delete_block_states(&mut self, block_root: &Hash256) -> Option { + self.blocks.remove(block_root) + } +} diff --git a/beacon_node/beacon_chain/src/validator_pubkey_cache.rs b/beacon_node/store/src/validator_pubkey_cache.rs similarity index 60% rename from beacon_node/beacon_chain/src/validator_pubkey_cache.rs rename to beacon_node/store/src/validator_pubkey_cache.rs index 2cf0c326158..60dceb3934f 100644 --- a/beacon_node/beacon_chain/src/validator_pubkey_cache.rs +++ b/beacon_node/store/src/validator_pubkey_cache.rs @@ -1,10 +1,12 @@ -use crate::errors::BeaconChainError; -use crate::{BeaconChainTypes, BeaconStore}; +use crate::{DBColumn, Error, HotColdDB, ItemStore, StoreItem, StoreOp}; +use bls::PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN; +use smallvec::SmallVec; use ssz::{Decode, Encode}; +use ssz_derive::{Decode, Encode}; use std::collections::HashMap; use std::marker::PhantomData; -use store::{DBColumn, Error as StoreError, StoreItem, StoreOp}; -use types::{BeaconState, Hash256, PublicKey, PublicKeyBytes}; +use std::sync::Arc; +use types::{BeaconState, EthSpec, Hash256, PublicKey, PublicKeyBytes}; /// Provides a mapping of `validator_index -> validator_publickey`. /// @@ -14,25 +16,40 @@ use types::{BeaconState, Hash256, PublicKey, PublicKeyBytes}; /// 2. To reduce the amount of public key _decompression_ required. A `BeaconState` stores public /// keys in compressed form and they are needed in decompressed form for signature verification. /// Decompression is expensive when many keys are involved. -pub struct ValidatorPubkeyCache { +/// +/// The cache has a `backing` that it uses to maintain a persistent, on-disk +/// copy of itself. This allows it to be restored between process invocations. +#[derive(Debug)] +pub struct ValidatorPubkeyCache, Cold: ItemStore> { pubkeys: Vec, indices: HashMap, - pubkey_bytes: Vec, - _phantom: PhantomData, + validators: Vec>, + _phantom: PhantomData<(E, Hot, Cold)>, +} + +// Temp value. +impl, Cold: ItemStore> Default + for ValidatorPubkeyCache +{ + fn default() -> Self { + ValidatorPubkeyCache { + pubkeys: vec![], + indices: HashMap::new(), + validators: vec![], + _phantom: PhantomData, + } + } } -impl ValidatorPubkeyCache { +impl, Cold: ItemStore> ValidatorPubkeyCache { /// Create a new public key cache using the keys in `state.validators`. /// /// The new cache will be updated with the keys from `state` and immediately written to disk. - pub fn new( - state: &BeaconState, - store: BeaconStore, - ) -> Result { + pub fn new(state: &BeaconState, store: &HotColdDB) -> Result { let mut cache = Self { pubkeys: vec![], indices: HashMap::new(), - pubkey_bytes: vec![], + validators: vec![], _phantom: PhantomData, }; @@ -43,20 +60,20 @@ impl ValidatorPubkeyCache { } /// Load the pubkey cache from the given on-disk database. - pub fn load_from_store(store: BeaconStore) -> Result { + pub fn load_from_store(store: &HotColdDB) -> Result { let mut pubkeys = vec![]; let mut indices = HashMap::new(); - let mut pubkey_bytes = vec![]; + let mut validators = vec![]; for validator_index in 0.. { - if let Some(DatabasePubkey(pubkey)) = - store.get_item(&DatabasePubkey::key_for_index(validator_index))? + if let Some(db_validator) = + store.get_item(&DatabaseValidator::key_for_index(validator_index))? { - pubkeys.push((&pubkey).try_into().map_err(|e| { - BeaconChainError::ValidatorPubkeyCacheError(format!("{:?}", e)) - })?); - pubkey_bytes.push(pubkey); - indices.insert(pubkey, validator_index); + let (pubkey, pubkey_bytes) = + DatabaseValidator::into_immutable_validator(&db_validator)?; + pubkeys.push(pubkey); + indices.insert(pubkey_bytes, validator_index); + validators.push(Arc::new(pubkey_bytes)); } else { break; } @@ -65,7 +82,7 @@ impl ValidatorPubkeyCache { Ok(ValidatorPubkeyCache { pubkeys, indices, - pubkey_bytes, + validators, _phantom: PhantomData, }) } @@ -77,13 +94,14 @@ impl ValidatorPubkeyCache { /// NOTE: The caller *must* commit the returned I/O batch as part of the block import process. pub fn import_new_pubkeys( &mut self, - state: &BeaconState, - ) -> Result>, BeaconChainError> { - if state.validators().len() > self.pubkeys.len() { + state: &BeaconState, + ) -> Result>, Error> { + if state.validators().len() > self.validators.len() { self.import( - state.validators()[self.pubkeys.len()..] - .iter() - .map(|v| v.pubkey), + state + .validators() + .iter_from(self.pubkeys.len())? + .map(|v| v.pubkey.clone()), ) } else { Ok(vec![]) @@ -91,41 +109,38 @@ impl ValidatorPubkeyCache { } /// Adds zero or more validators to `self`. - fn import( - &mut self, - validator_keys: I, - ) -> Result>, BeaconChainError> + fn import(&mut self, validator_keys: I) -> Result>, Error> where - I: Iterator + ExactSizeIterator, + I: Iterator> + ExactSizeIterator, { - self.pubkey_bytes.reserve(validator_keys.len()); + self.validators.reserve(validator_keys.len()); self.pubkeys.reserve(validator_keys.len()); self.indices.reserve(validator_keys.len()); let mut store_ops = Vec::with_capacity(validator_keys.len()); - for pubkey in validator_keys { + for pubkey_bytes in validator_keys { let i = self.pubkeys.len(); - if self.indices.contains_key(&pubkey) { - return Err(BeaconChainError::DuplicateValidatorPublicKey); + if self.indices.contains_key(&pubkey_bytes) { + return Err(Error::DuplicateValidatorPublicKey); } + let pubkey = (&*pubkey_bytes) + .try_into() + .map_err(Error::InvalidValidatorPubkeyBytes)?; + // Stage the new validator key for writing to disk. // It will be committed atomically when the block that introduced it is written to disk. // Notably it is NOT written while the write lock on the cache is held. // See: https://github.com/sigp/lighthouse/issues/2327 store_ops.push(StoreOp::KeyValueOp( - DatabasePubkey(pubkey).as_kv_store_op(DatabasePubkey::key_for_index(i)), + DatabaseValidator::from_immutable_validator(&pubkey, &pubkey_bytes) + .as_kv_store_op(DatabaseValidator::key_for_index(i)), )); - self.pubkeys.push( - (&pubkey) - .try_into() - .map_err(BeaconChainError::InvalidValidatorPubkeyBytes)?, - ); - self.pubkey_bytes.push(pubkey); - - self.indices.insert(pubkey, i); + self.pubkeys.push(pubkey); + self.indices.insert(*pubkey_bytes, i); + self.validators.push(pubkey_bytes); } Ok(store_ops) @@ -136,6 +151,11 @@ impl ValidatorPubkeyCache { self.pubkeys.get(i) } + /// Get the immutable validator with index `i`. + pub fn get_validator_pubkey(&self, i: usize) -> Option> { + self.validators.get(i).cloned() + } + /// Get the `PublicKey` for a validator with `PublicKeyBytes`. pub fn get_pubkey_from_pubkey_bytes(&self, pubkey: &PublicKeyBytes) -> Option<&PublicKey> { self.get_index(pubkey).and_then(|index| self.get(index)) @@ -143,7 +163,7 @@ impl ValidatorPubkeyCache { /// Get the public key (in bytes form) for a validator with index `i`. pub fn get_pubkey_bytes(&self, i: usize) -> Option<&PublicKeyBytes> { - self.pubkey_bytes.get(i) + self.validators.get(i).map(|pubkey_bytes| &**pubkey_bytes) } /// Get the index of a validator with `pubkey`. @@ -165,39 +185,57 @@ impl ValidatorPubkeyCache { /// Wrapper for a public key stored in the database. /// /// Keyed by the validator index as `Hash256::from_low_u64_be(index)`. -struct DatabasePubkey(PublicKeyBytes); +#[derive(Encode, Decode)] +struct DatabaseValidator { + pubkey: SmallVec<[u8; PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN]>, +} -impl StoreItem for DatabasePubkey { +impl StoreItem for DatabaseValidator { fn db_column() -> DBColumn { DBColumn::PubkeyCache } fn as_store_bytes(&self) -> Vec { - self.0.as_ssz_bytes() + self.as_ssz_bytes() } - fn from_store_bytes(bytes: &[u8]) -> Result { - Ok(Self(PublicKeyBytes::from_ssz_bytes(bytes)?)) + fn from_store_bytes(bytes: &[u8]) -> Result { + Ok(Self::from_ssz_bytes(bytes)?) } } -impl DatabasePubkey { +impl DatabaseValidator { fn key_for_index(index: usize) -> Hash256 { Hash256::from_low_u64_be(index as u64) } + + // FIXME(sproul): remove param + fn from_immutable_validator(pubkey: &PublicKey, _validator: &PublicKeyBytes) -> Self { + DatabaseValidator { + pubkey: pubkey.serialize_uncompressed().into(), + } + } + + #[allow(clippy::wrong_self_convention)] + fn into_immutable_validator(&self) -> Result<(PublicKey, PublicKeyBytes), Error> { + let pubkey = PublicKey::deserialize_uncompressed(&self.pubkey) + .map_err(Error::InvalidValidatorPubkeyBytes)?; + let pubkey_bytes = pubkey.compress(); + Ok((pubkey, pubkey_bytes)) + } } #[cfg(test)] mod test { use super::*; - use crate::test_utils::{BeaconChainHarness, EphemeralHarnessType}; + use crate::{HotColdDB, MemoryStore}; + use beacon_chain::test_utils::BeaconChainHarness; use logging::test_logger; use std::sync::Arc; - use store::HotColdDB; - use types::{EthSpec, Keypair, MainnetEthSpec}; + use types::{BeaconState, EthSpec, Keypair, MainnetEthSpec}; type E = MainnetEthSpec; - type T = EphemeralHarnessType; + type Store = MemoryStore; fn get_state(validator_count: usize) -> (BeaconState, Vec) { let harness = BeaconChainHarness::builder(MainnetEthSpec) @@ -211,14 +249,14 @@ mod test { (harness.get_current_state(), harness.validator_keypairs) } - fn get_store() -> BeaconStore { + fn get_store() -> Arc> { Arc::new( HotColdDB::open_ephemeral(<_>::default(), E::default_spec(), test_logger()).unwrap(), ) } #[allow(clippy::needless_range_loop)] - fn check_cache_get(cache: &ValidatorPubkeyCache, keypairs: &[Keypair]) { + fn check_cache_get(cache: &ValidatorPubkeyCache, keypairs: &[Keypair]) { let validator_count = keypairs.len(); for i in 0..validator_count + 1 { @@ -251,7 +289,7 @@ mod test { let store = get_store(); - let mut cache = ValidatorPubkeyCache::new(&state, store).expect("should create cache"); + let mut cache = ValidatorPubkeyCache::new(&state, &store).expect("should create cache"); check_cache_get(&cache, &keypairs[..]); @@ -284,13 +322,12 @@ mod test { let store = get_store(); // Create a new cache. - let cache = ValidatorPubkeyCache::new(&state, store.clone()).expect("should create cache"); + let cache = ValidatorPubkeyCache::new(&state, &store).expect("should create cache"); check_cache_get(&cache, &keypairs[..]); drop(cache); // Re-init the cache from the store. - let mut cache = - ValidatorPubkeyCache::load_from_store(store.clone()).expect("should open cache"); + let mut cache = ValidatorPubkeyCache::load_from_store(&store).expect("should open cache"); check_cache_get(&cache, &keypairs[..]); // Add some more keypairs. @@ -303,7 +340,7 @@ mod test { drop(cache); // Re-init the cache from the store. - let cache = ValidatorPubkeyCache::load_from_store(store).expect("should open cache"); + let cache = ValidatorPubkeyCache::load_from_store(&store).expect("should open cache"); check_cache_get(&cache, &keypairs[..]); } } diff --git a/book/src/api-lighthouse.md b/book/src/api-lighthouse.md index ce71450987d..1a3decef96b 100644 --- a/book/src/api-lighthouse.md +++ b/book/src/api-lighthouse.md @@ -541,7 +541,6 @@ reconstruction has yet to be completed. For more information on the specific meanings of these fields see the docs on [Checkpoint Sync](./checkpoint-sync.md#reconstructing-states). - ### `/lighthouse/merge_readiness` Returns the current difficulty and terminal total difficulty of the network. Before [The Merge](https://ethereum.org/en/roadmap/merge/) on 15th September 2022, you will see that the current difficulty is less than the terminal total difficulty, An example is shown below: ```bash diff --git a/book/src/help_bn.md b/book/src/help_bn.md index 996642f0fc7..ac5d831650e 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -113,6 +113,8 @@ FLAGS: --eth1` pre-merge --subscribe-all-subnets Subscribe to all subnets regardless of validator count. This will also advertise the beacon node as being long-lived subscribed to all subnets. + --unsafe-and-dangerous-mode Don't use this flag unless you know what you're doing. Go back and + download a stable Lighthouse release --validator-monitor-auto Enables the automatic detection and monitoring of validators connected to the HTTP API and using the subnet subscription endpoint. This generally has the effect of providing additional logging and metrics for locally @@ -175,6 +177,9 @@ OPTIONS: --checkpoint-sync-url-timeout Set the timeout for checkpoint sync calls to remote beacon node HTTP endpoint. [default: 180] + --compression-level + Compression level (-99 to 22) for zstd compression applied to states on disk [default: 1]. You may change + the compression level freely without re-syncing. -d, --datadir Used to specify a custom root data directory for lighthouse keys and databases. Defaults to $HOME/.lighthouse/{network} where network is the value of the `network` flag Note: Users should specify @@ -182,6 +187,9 @@ OPTIONS: --debug-level Specifies the verbosity level used when emitting logs to the terminal. [default: info] [possible values: info, debug, trace, warn, error, crit] + --diff-buffer-cache-size + The maximum number of diff buffers to hold in memory. This cache is used when fetching historic states + [default: 16] --discovery-port The UDP port that discovery will listen on. Defaults to `port` @@ -218,6 +226,10 @@ OPTIONS: --epochs-per-migration The number of epochs to wait between running the migration of data from the hot DB to the cold DB. Less frequent runs can be useful for minimizing disk writes [default: 1] + --epochs-per-state-diff + Number of epochs between state diffs stored in the database. Lower values result in more writes and more + data stored, while higher values result in more block replaying and longer load times in case of cache miss. + [default: 16] --eth1-blocks-per-log-query Specifies the number of blocks that a deposit log query should span. This will reduce the size of responses from the Eth1 endpoint. [default: 1000] @@ -260,6 +272,13 @@ OPTIONS: --graffiti Specify your custom graffiti to be included in blocks. Defaults to the current version and commit, truncated to fit in 32 bytes. + --hierarchy-exponents + Specifies the frequency for storing full state snapshots and hierarchical diffs in the freezer DB. Accepts a + comma-separated list of ascending exponents. Each exponent defines an interval for storing diffs to the + layer above. The last exponent defines the interval for full snapshots. For example, a config of '4,8,12' + would store a full snapshot every 4096 (2^12) slots, first-level diffs every 256 (2^8) slots, and second- + level diffs every 16 (2^4) slots. Cannot be changed after initialization. [default: + 5,9,11,13,16,18,21] --historic-state-cache-size Specifies how many states from the freezer database should cache in memory [default: 1] @@ -356,6 +375,10 @@ OPTIONS: --network-dir Data directory for network keys. Defaults to network/ inside the beacon node dir. + --parallel-state-cache-size + Set the size of the cache used to de-duplicate requests for the same state. This cache is additional to + other state caches within Lighthouse and should be kept small unless a large number of parallel requests for + different states are anticipated. [default: 2] --port The TCP/UDP ports to listen on. There are two UDP ports. The discovery UDP port will be set to this value and the Quic UDP port will be set to this value + 1. The discovery port can be modified by the --discovery- @@ -440,11 +463,9 @@ OPTIONS: --slasher-validator-chunk-size Number of validators per chunk stored on disk. - --slots-per-restore-point - Specifies how often a freezer DB restore point should be stored. Cannot be changed after initialization. - [default: 8192 (mainnet) or 64 (minimal)] - --state-cache-size - Specifies the size of the snapshot cache [default: 3] + --slots-per-restore-point Deprecated. + --state-cache-size + Specifies how many states the database should cache in memory [default: 128] --suggested-fee-recipient Emergency fallback fee recipient for use in case the validator client does not have one configured. You diff --git a/boot_node/Cargo.toml b/boot_node/Cargo.toml index 6cf62e04308..718a99382f7 100644 --- a/boot_node/Cargo.toml +++ b/boot_node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "boot_node" -version = "5.1.3" +version = "5.1.222-exp" authors = ["Sigma Prime "] edition = { workspace = true } diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index 5f85d777957..3cb757fae4d 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -375,20 +375,20 @@ pub enum ValidatorStatus { impl ValidatorStatus { pub fn from_validator(validator: &Validator, epoch: Epoch, far_future_epoch: Epoch) -> Self { if validator.is_withdrawable_at(epoch) { - if validator.effective_balance == 0 { + if validator.effective_balance() == 0 { ValidatorStatus::WithdrawalDone } else { ValidatorStatus::WithdrawalPossible } - } else if validator.is_exited_at(epoch) && epoch < validator.withdrawable_epoch { - if validator.slashed { + } else if validator.is_exited_at(epoch) && epoch < validator.withdrawable_epoch() { + if validator.slashed() { ValidatorStatus::ExitedSlashed } else { ValidatorStatus::ExitedUnslashed } } else if validator.is_active_at(epoch) { - if validator.exit_epoch < far_future_epoch { - if validator.slashed { + if validator.exit_epoch() < far_future_epoch { + if validator.slashed() { ValidatorStatus::ActiveSlashed } else { ValidatorStatus::ActiveExiting @@ -399,7 +399,7 @@ impl ValidatorStatus { // `pending` statuses are specified as validators where `validator.activation_epoch > current_epoch`. // If this code is reached, this criteria must have been met because `validator.is_active_at(epoch)`, // `validator.is_exited_at(epoch)`, and `validator.is_withdrawable_at(epoch)` all returned false. - } else if validator.activation_eligibility_epoch == far_future_epoch { + } else if validator.activation_eligibility_epoch() == far_future_epoch { ValidatorStatus::PendingInitialized } else { ValidatorStatus::PendingQueued @@ -977,6 +977,7 @@ pub struct SseLateHead { pub proposer_graffiti: String, pub block_delay: Duration, pub observed_delay: Option, + pub attestable_delay: Option, pub imported_delay: Option, pub set_as_head_delay: Option, pub execution_optimistic: bool, diff --git a/common/lighthouse_version/src/lib.rs b/common/lighthouse_version/src/lib.rs index 5387d322e96..a7538e0eae9 100644 --- a/common/lighthouse_version/src/lib.rs +++ b/common/lighthouse_version/src/lib.rs @@ -17,8 +17,8 @@ pub const VERSION: &str = git_version!( // NOTE: using --match instead of --exclude for compatibility with old Git "--match=thiswillnevermatchlol" ], - prefix = "Lighthouse/v5.1.3-", - fallback = "Lighthouse/v5.1.3" + prefix = "Lighthouse/v5.1.222-exp-", + fallback = "Lighthouse/v5.1.222-exp" ); /// Returns `VERSION`, but with platform information appended to the end. @@ -37,9 +37,8 @@ mod test { #[test] fn version_formatting() { - let re = - Regex::new(r"^Lighthouse/v[0-9]+\.[0-9]+\.[0-9]+(-rc.[0-9])?(-[[:xdigit:]]{7})?\+?$") - .unwrap(); + let re = Regex::new(r"^Lighthouse/v[0-9]+\.[0-9]+\.[0-9]+(-exp)?(-[[:xdigit:]]{7})?\+?$") + .unwrap(); assert!( re.is_match(VERSION), "version doesn't match regex: {}", diff --git a/common/promise_cache/Cargo.toml b/common/promise_cache/Cargo.toml new file mode 100644 index 00000000000..b5fa42bd438 --- /dev/null +++ b/common/promise_cache/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "promise_cache" +version = "0.1.0" +edition.workspace = true + +[dependencies] +derivative = { workspace = true } +oneshot_broadcast = { path = "../oneshot_broadcast" } +itertools = { workspace = true } +slog = { workspace = true } diff --git a/common/promise_cache/src/lib.rs b/common/promise_cache/src/lib.rs new file mode 100644 index 00000000000..36b6bd984f5 --- /dev/null +++ b/common/promise_cache/src/lib.rs @@ -0,0 +1,227 @@ +use derivative::Derivative; +use itertools::Itertools; +use oneshot_broadcast::{oneshot, Receiver, Sender}; +use slog::Logger; +use std::collections::HashMap; +use std::hash::Hash; +use std::sync::Arc; + +#[derive(Debug)] +pub struct PromiseCache +where + K: Hash + Eq + Clone, + P: Protect, +{ + cache: HashMap>, + capacity: usize, + protector: P, + max_concurrent_promises: usize, + logger: Logger, +} + +/// A value implementing `Protect` is capable of preventing keys of type `K` from being evicted. +/// +/// It also dictates an ordering on keys which is used to prioritise evictions. +pub trait Protect { + type SortKey: Ord; + + fn sort_key(&self, k: &K) -> Self::SortKey; + + fn protect_from_eviction(&self, k: &K) -> bool; + + fn notify_eviction(&self, _k: &K, _log: &Logger) {} +} + +#[derive(Derivative)] +#[derivative(Clone(bound = ""))] +pub enum CacheItem { + Complete(Arc), + Promise(Receiver>), +} + +impl std::fmt::Debug for CacheItem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + match self { + CacheItem::Complete(value) => value.fmt(f), + CacheItem::Promise(_) => "Promise(..)".fmt(f), + } + } +} + +#[derive(Debug)] +pub enum PromiseCacheError { + Failed(oneshot_broadcast::Error), + MaxConcurrentPromises(usize), +} + +pub trait ToArc { + fn to_arc(&self) -> Arc; +} + +impl CacheItem { + pub fn is_promise(&self) -> bool { + matches!(self, CacheItem::Promise(_)) + } + + pub fn wait(self) -> Result, PromiseCacheError> { + match self { + CacheItem::Complete(value) => Ok(value), + CacheItem::Promise(receiver) => receiver.recv().map_err(PromiseCacheError::Failed), + } + } +} + +impl ToArc for Arc { + fn to_arc(&self) -> Arc { + self.clone() + } +} + +impl ToArc for T +where + T: Clone, +{ + fn to_arc(&self) -> Arc { + Arc::new(self.clone()) + } +} + +impl PromiseCache +where + K: Hash + Eq + Clone, + P: Protect, +{ + pub fn new(capacity: usize, protector: P, logger: Logger) -> Self { + // Making the concurrent promises directly configurable is considered overkill for now, + // so we just derive a vaguely sensible value from the cache size. + let max_concurrent_promises = std::cmp::max(2, capacity / 8); + Self { + cache: HashMap::new(), + capacity, + protector, + max_concurrent_promises, + logger, + } + } + + pub fn get(&mut self, key: &K) -> Option> { + match self.cache.get(key) { + // The cache contained the value, return it. + item @ Some(CacheItem::Complete(_)) => item.cloned(), + // The cache contains a promise for the value. Check to see if the promise has already + // been resolved, without waiting for it. + item @ Some(CacheItem::Promise(receiver)) => match receiver.try_recv() { + // The promise has already been resolved. Replace the entry in the cache with a + // `Complete` entry and then return the value. + Ok(Some(value)) => { + let ready = CacheItem::Complete(value); + self.insert_cache_item(key.clone(), ready.clone()); + Some(ready) + } + // The promise has not yet been resolved. Return the promise so the caller can await + // it. + Ok(None) => item.cloned(), + // The sender has been dropped without sending a value. There was most likely an + // error computing the value. Drop the key from the cache and return + // `None` so the caller can recompute the value. + // + // It's worth noting that this is the only place where we removed unresolved + // promises from the cache. This means unresolved promises will only be removed if + // we try to access them again. This is OK, since the promises don't consume much + // memory. We expect that *all* promises should be resolved, unless there is a + // programming or database error. + Err(oneshot_broadcast::Error::SenderDropped) => { + self.cache.remove(key); + None + } + }, + // The cache does not have this value and it's not already promised to be computed. + None => None, + } + } + + pub fn contains(&self, key: &K) -> bool { + self.cache.contains_key(key) + } + + pub fn insert_value>(&mut self, key: K, value: &C) { + if self + .cache + .get(&key) + // Replace the value if it's not present or if it's a promise. A bird in the hand is + // worth two in the promise-bush! + .map_or(true, CacheItem::is_promise) + { + self.insert_cache_item(key, CacheItem::Complete(value.to_arc())); + } + } + + /// Take care of resolving a promise by ensuring the value is made available: + /// + /// 1. To all waiting thread that are holding a `Receiver`. + /// 2. In the cache itself for future callers. + pub fn resolve_promise>(&mut self, sender: Sender>, key: K, value: &C) { + // Use the sender to notify all actively waiting receivers. + let arc_value = value.to_arc(); + sender.send(arc_value.clone()); + + // Re-insert the value into the cache. The promise may have been evicted in the meantime, + // but we probably want to keep this value (which resolved recently) over other older cache + // entries. + self.insert_value(key, &arc_value); + } + + /// Prunes the cache first before inserting a new item. + fn insert_cache_item(&mut self, key: K, cache_item: CacheItem) { + self.prune_cache(); + self.cache.insert(key, cache_item); + } + + pub fn create_promise(&mut self, key: K) -> Result>, PromiseCacheError> { + let num_active_promises = self.cache.values().filter(|item| item.is_promise()).count(); + if num_active_promises >= self.max_concurrent_promises { + return Err(PromiseCacheError::MaxConcurrentPromises( + num_active_promises, + )); + } + + let (sender, receiver) = oneshot(); + self.insert_cache_item(key, CacheItem::Promise(receiver)); + Ok(sender) + } + + fn prune_cache(&mut self) { + let target_cache_size = self.capacity.saturating_sub(1); + if let Some(prune_count) = self.cache.len().checked_sub(target_cache_size) { + let keys_to_prune = self + .cache + .keys() + .filter(|k| !self.protector.protect_from_eviction(*k)) + .sorted_by_key(|k| self.protector.sort_key(k)) + .take(prune_count) + .cloned() + .collect::>(); + + for key in &keys_to_prune { + self.protector.notify_eviction(key, &self.logger); + self.cache.remove(key); + } + } + } + + pub fn update_protector(&mut self, protector: P) { + self.protector = protector; + } + + pub fn len(&self) -> usize { + self.cache.len() + } + + pub fn is_empty(&self) -> bool { + self.cache.is_empty() + } + + pub fn max_concurrent_promises(&self) -> usize { + self.max_concurrent_promises + } +} diff --git a/common/task_executor/Cargo.toml b/common/task_executor/Cargo.toml index b3d58fa5ea8..cc9a2c5097b 100644 --- a/common/task_executor/Cargo.toml +++ b/common/task_executor/Cargo.toml @@ -12,3 +12,4 @@ futures = { workspace = true } lazy_static = { workspace = true } lighthouse_metrics = { workspace = true } sloggers = { workspace = true } +logging = { workspace = true } diff --git a/common/task_executor/src/test_utils.rs b/common/task_executor/src/test_utils.rs index 6e372d97575..ec8f45d850e 100644 --- a/common/task_executor/src/test_utils.rs +++ b/common/task_executor/src/test_utils.rs @@ -1,4 +1,5 @@ use crate::TaskExecutor; +use logging::test_logger; use slog::Logger; use sloggers::{null::NullLoggerBuilder, Build}; use std::sync::Arc; @@ -26,7 +27,7 @@ impl Default for TestRuntime { fn default() -> Self { let (runtime_shutdown, exit) = async_channel::bounded(1); let (shutdown_tx, _) = futures::channel::mpsc::channel(1); - let log = null_logger().unwrap(); + let log = test_logger(); let (runtime, handle) = if let Ok(handle) = runtime::Handle::try_current() { (None, handle) diff --git a/consensus/fork_choice/tests/tests.rs b/consensus/fork_choice/tests/tests.rs index 3153275fb73..18fe0582d34 100644 --- a/consensus/fork_choice/tests/tests.rs +++ b/consensus/fork_choice/tests/tests.rs @@ -376,7 +376,7 @@ impl ForkChoiceTest { let state_root = harness .chain .store - .get_blinded_block(&fc.fc_store().justified_checkpoint().root) + .get_blinded_block(&fc.fc_store().justified_checkpoint().root, None) .unwrap() .unwrap() .message() @@ -392,7 +392,7 @@ impl ForkChoiceTest { .into_iter() .map(|v| { if v.is_active_at(state.current_epoch()) { - v.effective_balance + v.effective_balance() } else { 0 } diff --git a/consensus/proto_array/src/justified_balances.rs b/consensus/proto_array/src/justified_balances.rs index e08c8443eef..daff362209a 100644 --- a/consensus/proto_array/src/justified_balances.rs +++ b/consensus/proto_array/src/justified_balances.rs @@ -24,11 +24,11 @@ impl JustifiedBalances { .validators() .iter() .map(|validator| { - if !validator.slashed && validator.is_active_at(current_epoch) { - total_effective_balance.safe_add_assign(validator.effective_balance)?; + if !validator.slashed() && validator.is_active_at(current_epoch) { + total_effective_balance.safe_add_assign(validator.effective_balance())?; num_active_validators.safe_add_assign(1)?; - Ok(validator.effective_balance) + Ok(validator.effective_balance()) } else { Ok(0) } diff --git a/consensus/state_processing/Cargo.toml b/consensus/state_processing/Cargo.toml index be5367eb08f..d07763d1825 100644 --- a/consensus/state_processing/Cargo.toml +++ b/consensus/state_processing/Cargo.toml @@ -28,6 +28,7 @@ arbitrary = { workspace = true } lighthouse_metrics = { workspace = true } lazy_static = { workspace = true } derivative = { workspace = true } +vec_map = "0.8.2" [features] default = ["legacy-arith"] diff --git a/consensus/state_processing/src/all_caches.rs b/consensus/state_processing/src/all_caches.rs index 106692c63aa..b915091405b 100644 --- a/consensus/state_processing/src/all_caches.rs +++ b/consensus/state_processing/src/all_caches.rs @@ -9,12 +9,14 @@ use types::{BeaconState, ChainSpec, EpochCacheError, EthSpec, Hash256, RelativeE pub trait AllCaches { /// Build all caches. /// - /// Note that this excludes the tree-hash cache. That needs to be managed separately. + /// Note that this excludes milhouse's intrinsic tree-hash cache. That needs to be managed + /// separately. fn build_all_caches(&mut self, spec: &ChainSpec) -> Result<(), EpochCacheError>; /// Return true if all caches are built. /// - /// Note that this excludes the tree-hash cache. That needs to be managed separately. + /// Note that this excludes milhouse's intrinsic tree-hash cache. That needs to be managed + /// separately. fn all_caches_built(&self) -> bool; } diff --git a/consensus/state_processing/src/block_replayer.rs b/consensus/state_processing/src/block_replayer.rs index f502d7f692c..1749f773f3a 100644 --- a/consensus/state_processing/src/block_replayer.rs +++ b/consensus/state_processing/src/block_replayer.rs @@ -8,17 +8,18 @@ use std::iter::Peekable; use std::marker::PhantomData; use types::{BeaconState, BlindedPayload, ChainSpec, EthSpec, Hash256, SignedBeaconBlock, Slot}; -type PreBlockHook<'a, E, Error> = Box< +pub type PreBlockHook<'a, E, Error> = Box< dyn FnMut(&mut BeaconState, &SignedBeaconBlock>) -> Result<(), Error> + 'a, >; -type PostBlockHook<'a, E, Error> = PreBlockHook<'a, E, Error>; -type PreSlotHook<'a, E, Error> = Box) -> Result<(), Error> + 'a>; -type PostSlotHook<'a, E, Error> = Box< +pub type PostBlockHook<'a, E, Error> = PreBlockHook<'a, E, Error>; +pub type PreSlotHook<'a, E, Error> = + Box, &mut BeaconState) -> Result<(), Error> + 'a>; +pub type PostSlotHook<'a, E, Error> = Box< dyn FnMut(&mut BeaconState, Option>, bool) -> Result<(), Error> + 'a, >; -type StateRootIterDefault = std::iter::Empty>; +pub type StateRootIterDefault = std::iter::Empty>; /// Efficiently apply blocks to a state while configuring various parameters. /// @@ -31,7 +32,6 @@ pub struct BlockReplayer< > { state: BeaconState, spec: &'a ChainSpec, - state_processing_strategy: StateProcessingStrategy, block_sig_strategy: BlockSignatureStrategy, verify_block_root: Option, pre_block_hook: Option>, @@ -89,7 +89,6 @@ where Self { state, spec, - state_processing_strategy: StateProcessingStrategy::Accurate, block_sig_strategy: BlockSignatureStrategy::VerifyBulk, verify_block_root: Some(VerifyBlockRoot::True), pre_block_hook: None, @@ -107,10 +106,10 @@ where mut self, state_processing_strategy: StateProcessingStrategy, ) -> Self { + // FIXME(sproul): no-op if state_processing_strategy == StateProcessingStrategy::Inconsistent { self.verify_block_root = None; } - self.state_processing_strategy = state_processing_strategy; self } @@ -186,11 +185,6 @@ where blocks: &[SignedBeaconBlock>], i: usize, ) -> Result, Error> { - // If we don't care about state roots then return immediately. - if self.state_processing_strategy == StateProcessingStrategy::Inconsistent { - return Ok(Some(Hash256::zero())); - } - // If a state root iterator is configured, use it to find the root. if let Some(ref mut state_root_iter) = self.state_root_iter { let opt_root = state_root_iter @@ -232,11 +226,12 @@ where } while self.state.slot() < block.slot() { + let state_root = self.get_state_root(self.state.slot(), &blocks, i)?; + if let Some(ref mut pre_slot_hook) = self.pre_slot_hook { - pre_slot_hook(&mut self.state)?; + pre_slot_hook(state_root, &mut self.state)?; } - let state_root = self.get_state_root(self.state.slot(), &blocks, i)?; let summary = per_slot_processing(&mut self.state, state_root, self.spec) .map_err(BlockReplayError::from)?; @@ -250,15 +245,11 @@ where pre_block_hook(&mut self.state, block)?; } - let verify_block_root = self.verify_block_root.unwrap_or_else(|| { - // If no explicit policy is set, verify only the first 1 or 2 block roots if using - // accurate state roots. Inaccurate state roots require block root verification to - // be off. - if i <= 1 && self.state_processing_strategy == StateProcessingStrategy::Accurate { - VerifyBlockRoot::True - } else { - VerifyBlockRoot::False - } + // If no explicit policy is set, verify only the first 1 or 2 block roots. + let verify_block_root = self.verify_block_root.unwrap_or(if i <= 1 { + VerifyBlockRoot::True + } else { + VerifyBlockRoot::False }); // Proposer index was already checked when this block was originally processed, we // can omit recomputing it during replay. @@ -268,7 +259,7 @@ where &mut self.state, block, self.block_sig_strategy, - self.state_processing_strategy, + StateProcessingStrategy::Accurate, verify_block_root, &mut ctxt, self.spec, @@ -282,11 +273,12 @@ where if let Some(target_slot) = target_slot { while self.state.slot() < target_slot { + let state_root = self.get_state_root(self.state.slot(), &blocks, blocks.len())?; + if let Some(ref mut pre_slot_hook) = self.pre_slot_hook { - pre_slot_hook(&mut self.state)?; + pre_slot_hook(state_root, &mut self.state)?; } - let state_root = self.get_state_root(self.state.slot(), &blocks, blocks.len())?; let summary = per_slot_processing(&mut self.state, state_root, self.spec) .map_err(BlockReplayError::from)?; diff --git a/consensus/state_processing/src/common/initiate_validator_exit.rs b/consensus/state_processing/src/common/initiate_validator_exit.rs index c527807df89..4abe326cb1c 100644 --- a/consensus/state_processing/src/common/initiate_validator_exit.rs +++ b/consensus/state_processing/src/common/initiate_validator_exit.rs @@ -30,15 +30,16 @@ pub fn initiate_validator_exit( exit_queue_epoch.safe_add_assign(1)?; } - let validator = state.get_validator_mut(index)?; + let validator = state.get_validator_cow(index)?; // Return if the validator already initiated exit - if validator.exit_epoch != spec.far_future_epoch { + if validator.exit_epoch() != spec.far_future_epoch { return Ok(()); } - validator.exit_epoch = exit_queue_epoch; - validator.withdrawable_epoch = + let validator = validator.into_mut()?; + validator.mutable.exit_epoch = exit_queue_epoch; + validator.mutable.withdrawable_epoch = exit_queue_epoch.safe_add(spec.min_validator_withdrawability_delay)?; state diff --git a/consensus/state_processing/src/common/slash_validator.rs b/consensus/state_processing/src/common/slash_validator.rs index 16b4e74ece9..da84b0af135 100644 --- a/consensus/state_processing/src/common/slash_validator.rs +++ b/consensus/state_processing/src/common/slash_validator.rs @@ -25,12 +25,12 @@ pub fn slash_validator( initiate_validator_exit(state, slashed_index, spec)?; let validator = state.get_validator_mut(slashed_index)?; - validator.slashed = true; - validator.withdrawable_epoch = cmp::max( - validator.withdrawable_epoch, + validator.mutable.slashed = true; + validator.mutable.withdrawable_epoch = cmp::max( + validator.withdrawable_epoch(), epoch.safe_add(E::EpochsPerSlashingsVector::to_u64())?, ); - let validator_effective_balance = validator.effective_balance; + let validator_effective_balance = validator.effective_balance(); state.set_slashings( epoch, state diff --git a/consensus/state_processing/src/common/update_progressive_balances_cache.rs b/consensus/state_processing/src/common/update_progressive_balances_cache.rs index af843b3acbc..280b5377ab9 100644 --- a/consensus/state_processing/src/common/update_progressive_balances_cache.rs +++ b/consensus/state_processing/src/common/update_progressive_balances_cache.rs @@ -35,7 +35,7 @@ pub fn initialize_progressive_balances_cache( .zip(state.previous_epoch_participation()?) { // Exclude slashed validators. We are calculating *unslashed* participating totals. - if validator.slashed { + if validator.slashed() { continue; } @@ -78,7 +78,7 @@ fn update_flag_total_balances( ) -> Result<(), BeaconStateError> { for (flag, balance) in total_balances.total_flag_balances.iter_mut().enumerate() { if participation_flags.has_flag(flag)? { - balance.safe_add_assign(validator.effective_balance)?; + balance.safe_add_assign(validator.effective_balance())?; } } Ok(()) diff --git a/consensus/state_processing/src/epoch_cache.rs b/consensus/state_processing/src/epoch_cache.rs index b2f2d85407e..1d7473d7350 100644 --- a/consensus/state_processing/src/epoch_cache.rs +++ b/consensus/state_processing/src/epoch_cache.rs @@ -117,7 +117,7 @@ pub fn initialize_epoch_cache( let mut activation_queue = ActivationQueue::default(); for (index, validator) in state.validators().iter().enumerate() { - effective_balances.push(validator.effective_balance); + effective_balances.push(validator.effective_balance()); // Add to speculative activation queue. activation_queue diff --git a/consensus/state_processing/src/genesis.rs b/consensus/state_processing/src/genesis.rs index b225923b418..88dd94186ae 100644 --- a/consensus/state_processing/src/genesis.rs +++ b/consensus/state_processing/src/genesis.rs @@ -28,7 +28,7 @@ pub fn initialize_beacon_state_from_eth1( let mut state = BeaconState::new(genesis_time, eth1_data, spec); // Seed RANDAO with Eth1 entropy - state.fill_randao_mixes_with(eth1_block_hash); + state.fill_randao_mixes_with(eth1_block_hash)?; let mut deposit_tree = DepositDataTree::create(&[], 0, DEPOSIT_TREE_DEPTH); @@ -152,18 +152,20 @@ pub fn process_activations( spec: &ChainSpec, ) -> Result<(), Error> { let (validators, balances, _) = state.validators_and_balances_and_progressive_balances_mut(); - for (index, validator) in validators.iter_mut().enumerate() { + let mut validators_iter = validators.iter_cow(); + while let Some((index, validator)) = validators_iter.next_cow() { + let validator = validator.into_mut()?; let balance = balances .get(index) .copied() .ok_or(Error::BalancesOutOfBounds(index))?; - validator.effective_balance = std::cmp::min( + validator.mutable.effective_balance = std::cmp::min( balance.safe_sub(balance.safe_rem(spec.effective_balance_increment)?)?, spec.max_effective_balance, ); - if validator.effective_balance == spec.max_effective_balance { - validator.activation_eligibility_epoch = E::genesis_epoch(); - validator.activation_epoch = E::genesis_epoch(); + if validator.effective_balance() == spec.max_effective_balance { + validator.mutable.activation_eligibility_epoch = E::genesis_epoch(); + validator.mutable.activation_epoch = E::genesis_epoch(); } } Ok(()) diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index b370ec6216b..5d26cd22664 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -251,7 +251,7 @@ pub fn process_block_header( // Verify proposer is not slashed verify!( - !state.get_validator(proposer_index as usize)?.slashed, + !state.get_validator(proposer_index as usize)?.slashed(), HeaderInvalid::ProposerSlashed(proposer_index) ); diff --git a/consensus/state_processing/src/per_block_processing/altair/sync_committee.rs b/consensus/state_processing/src/per_block_processing/altair/sync_committee.rs index 210db4c9c15..e35494a96ef 100644 --- a/consensus/state_processing/src/per_block_processing/altair/sync_committee.rs +++ b/consensus/state_processing/src/per_block_processing/altair/sync_committee.rs @@ -59,6 +59,7 @@ pub fn process_sync_aggregate( .into_iter() .zip(aggregate.sync_committee_bits.iter()) { + // FIXME(sproul): double-check this for Capella, proposer shouldn't have 0 effective balance if participation_bit { // Accumulate proposer rewards in a temp var in case the proposer has very low balance, is // part of the sync committee, does not participate and its penalties saturate. diff --git a/consensus/state_processing/src/per_block_processing/errors.rs b/consensus/state_processing/src/per_block_processing/errors.rs index 28d36dbc518..336895514f9 100644 --- a/consensus/state_processing/src/per_block_processing/errors.rs +++ b/consensus/state_processing/src/per_block_processing/errors.rs @@ -82,6 +82,7 @@ pub enum BlockProcessingError { }, ExecutionInvalid, ConsensusContext(ContextError), + MilhouseError(milhouse::Error), EpochCacheError(EpochCacheError), WithdrawalsRootMismatch { expected: Hash256, @@ -138,6 +139,12 @@ impl From for BlockProcessingError { } } +impl From for BlockProcessingError { + fn from(e: milhouse::Error) -> Self { + Self::MilhouseError(e) + } +} + impl From> for BlockProcessingError { fn from(e: BlockOperationError) -> BlockProcessingError { match e { diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index af9b7938132..7e114c71c6e 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -5,6 +5,7 @@ use crate::common::{ }; use crate::per_block_processing::errors::{BlockProcessingError, IntoWithIndex}; use crate::VerifySignatures; +use std::sync::Arc; use types::consts::altair::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}; pub fn process_operations>( @@ -128,7 +129,7 @@ pub mod altair_deneb { let previous_epoch = ctxt.previous_epoch; let current_epoch = ctxt.current_epoch; - let attesting_indices = &verify_attestation_for_block_inclusion( + let attesting_indices = verify_attestation_for_block_inclusion( state, attestation, ctxt, @@ -136,7 +137,8 @@ pub mod altair_deneb { spec, ) .map_err(|e| e.into_with_index(att_index))? - .attesting_indices; + .attesting_indices + .clone(); // Matching roots, participation flag indices let data = &attestation.data; @@ -146,7 +148,7 @@ pub mod altair_deneb { // Update epoch participation flags. let mut proposer_reward_numerator = 0; - for index in attesting_indices { + for index in &attesting_indices { let index = *index as usize; let validator_effective_balance = state.epoch_cache().get_effective_balance(index)?; @@ -411,17 +413,19 @@ pub fn process_deposit( // Create a new validator. let validator = Validator { - pubkey: deposit.data.pubkey, - withdrawal_credentials: deposit.data.withdrawal_credentials, - activation_eligibility_epoch: spec.far_future_epoch, - activation_epoch: spec.far_future_epoch, - exit_epoch: spec.far_future_epoch, - withdrawable_epoch: spec.far_future_epoch, - effective_balance: std::cmp::min( - amount.safe_sub(amount.safe_rem(spec.effective_balance_increment)?)?, - spec.max_effective_balance, - ), - slashed: false, + pubkey: Arc::new(deposit.data.pubkey), + mutable: ValidatorMutable { + withdrawal_credentials: deposit.data.withdrawal_credentials, + activation_eligibility_epoch: spec.far_future_epoch, + activation_epoch: spec.far_future_epoch, + exit_epoch: spec.far_future_epoch, + withdrawable_epoch: spec.far_future_epoch, + effective_balance: std::cmp::min( + amount.safe_sub(amount.safe_rem(spec.effective_balance_increment)?)?, + spec.max_effective_balance, + ), + slashed: false, + }, }; state.validators_mut().push(validator)?; state.balances_mut().push(deposit.data.amount)?; diff --git a/consensus/state_processing/src/per_block_processing/signature_sets.rs b/consensus/state_processing/src/per_block_processing/signature_sets.rs index 163b2cff7a9..d3d3af096db 100644 --- a/consensus/state_processing/src/per_block_processing/signature_sets.rs +++ b/consensus/state_processing/src/per_block_processing/signature_sets.rs @@ -64,7 +64,7 @@ where .validators() .get(validator_index) .and_then(|v| { - let pk: Option = v.pubkey.decompress().ok(); + let pk: Option = v.pubkey().decompress().ok(); pk }) .map(Cow::Owned) diff --git a/consensus/state_processing/src/per_block_processing/verify_bls_to_execution_change.rs b/consensus/state_processing/src/per_block_processing/verify_bls_to_execution_change.rs index 1e8f25ed10b..500355c7543 100644 --- a/consensus/state_processing/src/per_block_processing/verify_bls_to_execution_change.rs +++ b/consensus/state_processing/src/per_block_processing/verify_bls_to_execution_change.rs @@ -29,7 +29,7 @@ pub fn verify_bls_to_execution_change( verify!( validator - .withdrawal_credentials + .withdrawal_credentials() .as_bytes() .first() .map(|byte| *byte == spec.bls_withdrawal_prefix_byte) @@ -41,7 +41,7 @@ pub fn verify_bls_to_execution_change( // future. let pubkey_hash = hash(address_change.from_bls_pubkey.as_serialized()); verify!( - validator.withdrawal_credentials.as_bytes().get(1..) == pubkey_hash.get(1..), + validator.withdrawal_credentials().as_bytes().get(1..) == pubkey_hash.get(1..), Invalid::WithdrawalCredentialsMismatch ); diff --git a/consensus/state_processing/src/per_block_processing/verify_exit.rs b/consensus/state_processing/src/per_block_processing/verify_exit.rs index fc258d38298..3619feaf857 100644 --- a/consensus/state_processing/src/per_block_processing/verify_exit.rs +++ b/consensus/state_processing/src/per_block_processing/verify_exit.rs @@ -41,7 +41,7 @@ pub fn verify_exit( // Verify that the validator has not yet exited. verify!( - validator.exit_epoch == spec.far_future_epoch, + validator.exit_epoch() == spec.far_future_epoch, ExitInvalid::AlreadyExited(exit.validator_index) ); @@ -56,7 +56,7 @@ pub fn verify_exit( // Verify the validator has been active long enough. let earliest_exit_epoch = validator - .activation_epoch + .activation_epoch() .safe_add(spec.shard_committee_period)?; verify!( current_epoch >= earliest_exit_epoch, diff --git a/consensus/state_processing/src/per_epoch_processing/altair/participation_cache.rs b/consensus/state_processing/src/per_epoch_processing/altair/participation_cache.rs deleted file mode 100644 index 4f0ca1142f4..00000000000 --- a/consensus/state_processing/src/per_epoch_processing/altair/participation_cache.rs +++ /dev/null @@ -1,402 +0,0 @@ -//! Provides the `ParticipationCache`, a custom Lighthouse cache which attempts to reduce CPU and -//! memory usage by: -//! -//! - Caching a map of `validator_index -> participation_flags` for all active validators in the -//! previous and current epochs. -//! - Caching the total balances of: -//! - All active validators. -//! - All active validators matching each of the three "timely" flags. -//! - Caching the "eligible" validators. -//! -//! Additionally, this cache is returned from the `altair::process_epoch` function and can be used -//! to get useful summaries about the validator participation in an epoch. - -use types::{ - consts::altair::{ - NUM_FLAG_INDICES, TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX, - TIMELY_TARGET_FLAG_INDEX, - }, - Balance, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, ParticipationFlags, - RelativeEpoch, -}; - -#[derive(Debug, PartialEq, Clone)] -pub enum Error { - InvalidFlagIndex(usize), - InvalidValidatorIndex(usize), -} - -/// Caches the participation values for one epoch (either the previous or current). -#[derive(PartialEq, Debug, Clone)] -struct SingleEpochParticipationCache { - /// Maps an active validator index to their participation flags. - /// - /// To reiterate, only active and unslashed validator indices are stored in this map. - /// - /// ## Note - /// - /// It would be ideal to maintain a reference to the `BeaconState` here rather than copying the - /// `ParticipationFlags`, however that would cause us to run into mutable reference limitations - /// upstream. - unslashed_participating_indices: Vec>, - /// Stores the sum of the balances for all validators in `self.unslashed_participating_indices` - /// for all flags in `NUM_FLAG_INDICES`. - /// - /// A flag balance is only incremented if a validator is in that flag set. - total_flag_balances: [Balance; NUM_FLAG_INDICES], - /// Stores the sum of all balances of all validators in `self.unslashed_participating_indices` - /// (regardless of which flags are set). - total_active_balance: Balance, -} - -impl SingleEpochParticipationCache { - fn new(state: &BeaconState, spec: &ChainSpec) -> Self { - let num_validators = state.validators().len(); - let zero_balance = Balance::zero(spec.effective_balance_increment); - - Self { - unslashed_participating_indices: vec![None; num_validators], - total_flag_balances: [zero_balance; NUM_FLAG_INDICES], - total_active_balance: zero_balance, - } - } - - /// Returns the total balance of attesters who have `flag_index` set. - fn total_flag_balance(&self, flag_index: usize) -> Result { - self.total_flag_balances - .get(flag_index) - .map(Balance::get) - .ok_or(Error::InvalidFlagIndex(flag_index)) - } - - /// Returns the raw total balance of attesters who have `flag_index` set. - fn total_flag_balance_raw(&self, flag_index: usize) -> Result { - self.total_flag_balances - .get(flag_index) - .copied() - .ok_or(Error::InvalidFlagIndex(flag_index)) - } - - /// Returns `true` if `val_index` is active, unslashed and has `flag_index` set. - /// - /// ## Errors - /// - /// May return an error if `flag_index` is out-of-bounds. - fn has_flag(&self, val_index: usize, flag_index: usize) -> Result { - let participation_flags = self - .unslashed_participating_indices - .get(val_index) - .ok_or(Error::InvalidValidatorIndex(val_index))?; - if let Some(participation_flags) = participation_flags { - participation_flags - .has_flag(flag_index) - .map_err(|_| Error::InvalidFlagIndex(flag_index)) - } else { - Ok(false) - } - } - - /// Process an **active** validator, reading from the `state` with respect to the - /// `relative_epoch`. - /// - /// ## Errors - /// - /// - The provided `state` **must** be Altair. An error will be returned otherwise. - /// - An error will be returned if the `val_index` validator is inactive at the given - /// `relative_epoch`. - fn process_active_validator( - &mut self, - val_index: usize, - state: &BeaconState, - current_epoch: Epoch, - relative_epoch: RelativeEpoch, - ) -> Result<(), BeaconStateError> { - let validator = state.get_validator(val_index)?; - let val_balance = validator.effective_balance; - - // Sanity check to ensure the validator is active. - let epoch = relative_epoch.into_epoch(current_epoch); - if !validator.is_active_at(epoch) { - return Err(BeaconStateError::ValidatorIsInactive { val_index }); - } - - let epoch_participation = match relative_epoch { - RelativeEpoch::Current => state.current_epoch_participation(), - RelativeEpoch::Previous => state.previous_epoch_participation(), - _ => Err(BeaconStateError::EpochOutOfBounds), - }? - .get(val_index) - .ok_or(BeaconStateError::ParticipationOutOfBounds(val_index))?; - - // All active validators increase the total active balance. - self.total_active_balance.safe_add_assign(val_balance)?; - - // Only unslashed validators may proceed. - if validator.slashed { - return Ok(()); - } - - // Add their `ParticipationFlags` to the map. - *self - .unslashed_participating_indices - .get_mut(val_index) - .ok_or(BeaconStateError::UnknownValidator(val_index))? = Some(*epoch_participation); - - // Iterate through all the flags and increment the total flag balances for whichever flags - // are set for `val_index`. - for (flag, balance) in self.total_flag_balances.iter_mut().enumerate() { - if epoch_participation.has_flag(flag)? { - balance.safe_add_assign(val_balance)?; - } - } - - Ok(()) - } -} - -/// Maintains a cache to be used during `altair::process_epoch`. -#[derive(PartialEq, Debug, Clone)] -pub struct ParticipationCache { - current_epoch: Epoch, - /// Caches information about active validators pertaining to `self.current_epoch`. - current_epoch_participation: SingleEpochParticipationCache, - previous_epoch: Epoch, - /// Caches information about active validators pertaining to `self.previous_epoch`. - previous_epoch_participation: SingleEpochParticipationCache, - /// Caches the result of the `get_eligible_validator_indices` function. - eligible_indices: Vec, -} - -impl ParticipationCache { - /// Instantiate `Self`, returning a fully initialized cache. - /// - /// ## Errors - /// - /// - The provided `state` **must** be an Altair state. An error will be returned otherwise. - pub fn new( - state: &BeaconState, - spec: &ChainSpec, - ) -> Result { - let current_epoch = state.current_epoch(); - let previous_epoch = state.previous_epoch(); - - // Both the current/previous epoch participations are set to a capacity that is slightly - // larger than required. The difference will be due slashed-but-active validators. - let mut current_epoch_participation = SingleEpochParticipationCache::new(state, spec); - let mut previous_epoch_participation = SingleEpochParticipationCache::new(state, spec); - // Contains the set of validators which are either: - // - // - Active in the previous epoch. - // - Slashed, but not yet withdrawable. - // - // Using the full length of `state.validators` is almost always overkill, but it ensures no - // reallocations. - let mut eligible_indices = Vec::with_capacity(state.validators().len()); - - // Iterate through all validators, updating: - // - // 1. Validator participation for current and previous epochs. - // 2. The "eligible indices". - // - // Care is taken to ensure that the ordering of `eligible_indices` is the same as the - // `get_eligible_validator_indices` function in the spec. - for (val_index, val) in state.validators().iter().enumerate() { - if val.is_active_at(current_epoch) { - current_epoch_participation.process_active_validator( - val_index, - state, - current_epoch, - RelativeEpoch::Current, - )?; - } - - if val.is_active_at(previous_epoch) { - previous_epoch_participation.process_active_validator( - val_index, - state, - current_epoch, - RelativeEpoch::Previous, - )?; - } - - // Note: a validator might still be "eligible" whilst returning `false` to - // `Validator::is_active_at`. - if state.is_eligible_validator(previous_epoch, val_index)? { - eligible_indices.push(val_index) - } - } - - Ok(Self { - current_epoch, - current_epoch_participation, - previous_epoch, - previous_epoch_participation, - eligible_indices, - }) - } - - /// Equivalent to the specification `get_eligible_validator_indices` function. - pub fn eligible_validator_indices(&self) -> &[usize] { - &self.eligible_indices - } - - /// Equivalent to the `get_unslashed_participating_indices` function in the specification. - pub fn get_unslashed_participating_indices( - &self, - flag_index: usize, - epoch: Epoch, - ) -> Result { - let participation = if epoch == self.current_epoch { - &self.current_epoch_participation - } else if epoch == self.previous_epoch { - &self.previous_epoch_participation - } else { - return Err(BeaconStateError::EpochOutOfBounds); - }; - - Ok(UnslashedParticipatingIndices { - participation, - flag_index, - }) - } - - /* - * Balances - */ - - pub fn current_epoch_total_active_balance(&self) -> u64 { - self.current_epoch_participation.total_active_balance.get() - } - - pub fn current_epoch_target_attesting_balance(&self) -> Result { - self.current_epoch_participation - .total_flag_balance(TIMELY_TARGET_FLAG_INDEX) - } - - pub fn current_epoch_target_attesting_balance_raw(&self) -> Result { - self.current_epoch_participation - .total_flag_balance_raw(TIMELY_TARGET_FLAG_INDEX) - } - - pub fn previous_epoch_total_active_balance(&self) -> u64 { - self.previous_epoch_participation.total_active_balance.get() - } - - pub fn previous_epoch_target_attesting_balance(&self) -> Result { - self.previous_epoch_participation - .total_flag_balance(TIMELY_TARGET_FLAG_INDEX) - } - - pub fn previous_epoch_target_attesting_balance_raw(&self) -> Result { - self.previous_epoch_participation - .total_flag_balance_raw(TIMELY_TARGET_FLAG_INDEX) - } - - pub fn previous_epoch_source_attesting_balance(&self) -> Result { - self.previous_epoch_participation - .total_flag_balance(TIMELY_SOURCE_FLAG_INDEX) - } - - pub fn previous_epoch_head_attesting_balance(&self) -> Result { - self.previous_epoch_participation - .total_flag_balance(TIMELY_HEAD_FLAG_INDEX) - } - - /* - * Active/Unslashed - */ - - /// Returns `None` for an unknown `val_index`. - pub fn is_active_unslashed_in_previous_epoch(&self, val_index: usize) -> Option { - self.previous_epoch_participation - .unslashed_participating_indices - .get(val_index) - .map(|flags| flags.is_some()) - } - - /// Returns `None` for an unknown `val_index`. - pub fn is_active_unslashed_in_current_epoch(&self, val_index: usize) -> Option { - self.current_epoch_participation - .unslashed_participating_indices - .get(val_index) - .map(|flags| flags.is_some()) - } - - /* - * Flags - */ - - /// Always returns false for a slashed validator. - pub fn is_previous_epoch_timely_source_attester( - &self, - val_index: usize, - ) -> Result { - self.previous_epoch_participation - .has_flag(val_index, TIMELY_SOURCE_FLAG_INDEX) - } - - /// Always returns false for a slashed validator. - pub fn is_previous_epoch_timely_target_attester( - &self, - val_index: usize, - ) -> Result { - self.previous_epoch_participation - .has_flag(val_index, TIMELY_TARGET_FLAG_INDEX) - } - - /// Always returns false for a slashed validator. - pub fn is_previous_epoch_timely_head_attester(&self, val_index: usize) -> Result { - self.previous_epoch_participation - .has_flag(val_index, TIMELY_HEAD_FLAG_INDEX) - } - - /// Always returns false for a slashed validator. - pub fn is_current_epoch_timely_source_attester(&self, val_index: usize) -> Result { - self.current_epoch_participation - .has_flag(val_index, TIMELY_SOURCE_FLAG_INDEX) - } - - /// Always returns false for a slashed validator. - pub fn is_current_epoch_timely_target_attester(&self, val_index: usize) -> Result { - self.current_epoch_participation - .has_flag(val_index, TIMELY_TARGET_FLAG_INDEX) - } - - /// Always returns false for a slashed validator. - pub fn is_current_epoch_timely_head_attester(&self, val_index: usize) -> Result { - self.current_epoch_participation - .has_flag(val_index, TIMELY_HEAD_FLAG_INDEX) - } -} - -/// Imitates the return value of the `get_unslashed_participating_indices` in the -/// specification. -/// -/// This struct exists to help make the Lighthouse code read more like the specification. -pub struct UnslashedParticipatingIndices<'a> { - participation: &'a SingleEpochParticipationCache, - flag_index: usize, -} - -impl<'a> UnslashedParticipatingIndices<'a> { - /// Returns `Ok(true)` if the given `val_index` is both: - /// - /// - An active validator. - /// - Has `self.flag_index` set. - pub fn contains(&self, val_index: usize) -> Result { - self.participation.has_flag(val_index, self.flag_index) - } - - /// Returns the sum of all balances of validators which have `self.flag_index` set. - /// - /// ## Notes - /// - /// Respects the `EFFECTIVE_BALANCE_INCREMENT` minimum. - pub fn total_balance(&self) -> Result { - self.participation - .total_flag_balances - .get(self.flag_index) - .ok_or(Error::InvalidFlagIndex(self.flag_index)) - .map(Balance::get) - } -} diff --git a/consensus/state_processing/src/per_epoch_processing/altair/participation_flag_updates.rs b/consensus/state_processing/src/per_epoch_processing/altair/participation_flag_updates.rs index dd1b2dfcd86..fc55fb11144 100644 --- a/consensus/state_processing/src/per_epoch_processing/altair/participation_flag_updates.rs +++ b/consensus/state_processing/src/per_epoch_processing/altair/participation_flag_updates.rs @@ -2,17 +2,14 @@ use crate::EpochProcessingError; use types::beacon_state::BeaconState; use types::eth_spec::EthSpec; use types::participation_flags::ParticipationFlags; -use types::VariableList; +use types::List; pub fn process_participation_flag_updates( state: &mut BeaconState, ) -> Result<(), EpochProcessingError> { *state.previous_epoch_participation_mut()? = std::mem::take(state.current_epoch_participation_mut()?); - *state.current_epoch_participation_mut()? = VariableList::new(vec![ - ParticipationFlags::default( - ); - state.validators().len() - ])?; + *state.current_epoch_participation_mut()? = + List::repeat(ParticipationFlags::default(), state.validators().len())?; Ok(()) } diff --git a/consensus/state_processing/src/per_epoch_processing/base/validator_statuses.rs b/consensus/state_processing/src/per_epoch_processing/base/validator_statuses.rs index 7e244058038..fe8db7d2dee 100644 --- a/consensus/state_processing/src/per_epoch_processing/base/validator_statuses.rs +++ b/consensus/state_processing/src/per_epoch_processing/base/validator_statuses.rs @@ -202,9 +202,9 @@ impl ValidatorStatuses { let previous_epoch = state.previous_epoch(); for validator in state.validators().iter() { - let effective_balance = validator.effective_balance; + let effective_balance = validator.effective_balance(); let mut status = ValidatorStatus { - is_slashed: validator.slashed, + is_slashed: validator.slashed(), is_eligible: state.is_eligible_validator(previous_epoch, validator)?, is_withdrawable_in_current_epoch: validator.is_withdrawable_at(current_epoch), current_epoch_effective_balance: effective_balance, diff --git a/consensus/state_processing/src/per_epoch_processing/capella/historical_summaries_update.rs b/consensus/state_processing/src/per_epoch_processing/capella/historical_summaries_update.rs index 7490f276567..00adabdcfe9 100644 --- a/consensus/state_processing/src/per_epoch_processing/capella/historical_summaries_update.rs +++ b/consensus/state_processing/src/per_epoch_processing/capella/historical_summaries_update.rs @@ -13,6 +13,9 @@ pub fn process_historical_summaries_update( .safe_rem((E::slots_per_historical_root() as u64).safe_div(E::slots_per_epoch())?)? == 0 { + // We need to flush any pending mutations before hashing. + state.block_roots_mut().apply_updates()?; + state.state_roots_mut().apply_updates()?; let summary = HistoricalSummary::new(state); return state .historical_summaries_mut()? diff --git a/consensus/state_processing/src/per_epoch_processing/effective_balance_updates.rs b/consensus/state_processing/src/per_epoch_processing/effective_balance_updates.rs index 7bd62c40816..146e4a3a8e3 100644 --- a/consensus/state_processing/src/per_epoch_processing/effective_balance_updates.rs +++ b/consensus/state_processing/src/per_epoch_processing/effective_balance_updates.rs @@ -21,30 +21,32 @@ pub fn process_effective_balance_updates( let downward_threshold = hysteresis_increment.safe_mul(spec.hysteresis_downward_multiplier)?; let upward_threshold = hysteresis_increment.safe_mul(spec.hysteresis_upward_multiplier)?; let (validators, balances, _) = state.validators_and_balances_and_progressive_balances_mut(); - for (index, validator) in validators.iter_mut().enumerate() { + let mut validators_iter = validators.iter_cow(); + + while let Some((index, validator)) = validators_iter.next_cow() { let balance = balances .get(index) .copied() .ok_or(BeaconStateError::BalancesOutOfBounds(index))?; let new_effective_balance = if balance.safe_add(downward_threshold)? - < validator.effective_balance - || validator.effective_balance.safe_add(upward_threshold)? < balance + < validator.effective_balance() + || validator.effective_balance().safe_add(upward_threshold)? < balance { std::cmp::min( balance.safe_sub(balance.safe_rem(spec.effective_balance_increment)?)?, spec.max_effective_balance, ) } else { - validator.effective_balance + validator.effective_balance() }; if validator.is_active_at(next_epoch) { new_total_active_balance.safe_add_assign(new_effective_balance)?; } - if new_effective_balance != validator.effective_balance { - validator.effective_balance = new_effective_balance; + if new_effective_balance != validator.effective_balance() { + validator.into_mut()?.mutable.effective_balance = new_effective_balance; } } diff --git a/consensus/state_processing/src/per_epoch_processing/epoch_processing_summary.rs b/consensus/state_processing/src/per_epoch_processing/epoch_processing_summary.rs index 65a946e7bff..508426af18c 100644 --- a/consensus/state_processing/src/per_epoch_processing/epoch_processing_summary.rs +++ b/consensus/state_processing/src/per_epoch_processing/epoch_processing_summary.rs @@ -3,8 +3,8 @@ use crate::metrics; use std::sync::Arc; use types::{ consts::altair::{TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX}, - BeaconStateError, Epoch, EthSpec, ParticipationFlags, ProgressiveBalancesCache, SyncCommittee, - Validator, VariableList, + BeaconStateError, Epoch, EthSpec, List, ParticipationFlags, ProgressiveBalancesCache, + SyncCommittee, Validator, }; /// Provides a summary of validator participation during the epoch. @@ -25,20 +25,20 @@ pub enum EpochProcessingSummary { #[derive(PartialEq, Debug)] pub struct ParticipationEpochSummary { /// Copy of the validator registry prior to mutation. - validators: VariableList, + validators: List, /// Copy of the participation flags for the previous epoch. - previous_epoch_participation: VariableList, + previous_epoch_participation: List, /// Copy of the participation flags for the current epoch. - current_epoch_participation: VariableList, + current_epoch_participation: List, previous_epoch: Epoch, current_epoch: Epoch, } impl ParticipationEpochSummary { pub fn new( - validators: VariableList, - previous_epoch_participation: VariableList, - current_epoch_participation: VariableList, + validators: List, + previous_epoch_participation: List, + current_epoch_participation: List, previous_epoch: Epoch, current_epoch: Epoch, ) -> Self { @@ -54,7 +54,7 @@ impl ParticipationEpochSummary { pub fn is_active_and_unslashed(&self, val_index: usize, epoch: Epoch) -> bool { self.validators .get(val_index) - .map(|validator| !validator.slashed && validator.is_active_at(epoch)) + .map(|validator| !validator.slashed() && validator.is_active_at(epoch)) .unwrap_or(false) } diff --git a/consensus/state_processing/src/per_epoch_processing/errors.rs b/consensus/state_processing/src/per_epoch_processing/errors.rs index c18e1303b26..de481ec6767 100644 --- a/consensus/state_processing/src/per_epoch_processing/errors.rs +++ b/consensus/state_processing/src/per_epoch_processing/errors.rs @@ -1,4 +1,4 @@ -use types::{BeaconStateError, EpochCacheError, InconsistentFork}; +use types::{milhouse, BeaconStateError, EpochCacheError, InconsistentFork}; #[derive(Debug, PartialEq)] pub enum EpochProcessingError { @@ -23,6 +23,7 @@ pub enum EpochProcessingError { InconsistentStateFork(InconsistentFork), InvalidJustificationBit(ssz_types::Error), InvalidFlagIndex(usize), + MilhouseError(milhouse::Error), EpochCache(EpochCacheError), } @@ -50,6 +51,12 @@ impl From for EpochProcessingError { } } +impl From for EpochProcessingError { + fn from(e: milhouse::Error) -> Self { + Self::MilhouseError(e) + } +} + impl From for EpochProcessingError { fn from(e: EpochCacheError) -> Self { EpochProcessingError::EpochCache(e) diff --git a/consensus/state_processing/src/per_epoch_processing/historical_roots_update.rs b/consensus/state_processing/src/per_epoch_processing/historical_roots_update.rs index 6d06b4d7ca5..7686932192f 100644 --- a/consensus/state_processing/src/per_epoch_processing/historical_roots_update.rs +++ b/consensus/state_processing/src/per_epoch_processing/historical_roots_update.rs @@ -14,7 +14,7 @@ pub fn process_historical_roots_update( .safe_rem(E::SlotsPerHistoricalRoot::to_u64().safe_div(E::slots_per_epoch())?)? == 0 { - let historical_batch = state.historical_batch(); + let historical_batch = state.historical_batch()?; state .historical_roots_mut() .push(historical_batch.tree_hash_root())?; diff --git a/consensus/state_processing/src/per_epoch_processing/registry_updates.rs b/consensus/state_processing/src/per_epoch_processing/registry_updates.rs index 6b86f9c1e76..c978a76d059 100644 --- a/consensus/state_processing/src/per_epoch_processing/registry_updates.rs +++ b/consensus/state_processing/src/per_epoch_processing/registry_updates.rs @@ -17,7 +17,7 @@ pub fn process_registry_updates( let current_epoch = state.current_epoch(); let is_ejectable = |validator: &Validator| { validator.is_active_at(current_epoch) - && validator.effective_balance <= spec.ejection_balance + && validator.effective_balance() <= spec.ejection_balance }; let indices_to_update: Vec<_> = state .validators() @@ -32,7 +32,7 @@ pub fn process_registry_updates( for index in indices_to_update { let validator = state.get_validator_mut(index)?; if validator.is_eligible_for_activation_queue(spec) { - validator.activation_eligibility_epoch = current_epoch.safe_add(1)?; + validator.mutable.activation_eligibility_epoch = current_epoch.safe_add(1)?; } if is_ejectable(validator) { initiate_validator_exit(state, index, spec)?; @@ -50,7 +50,7 @@ pub fn process_registry_updates( let delayed_activation_epoch = state.compute_activation_exit_epoch(current_epoch, spec)?; for index in activation_queue { - state.get_validator_mut(index)?.activation_epoch = delayed_activation_epoch; + state.get_validator_mut(index)?.mutable.activation_epoch = delayed_activation_epoch; } Ok(()) diff --git a/consensus/state_processing/src/per_epoch_processing/resets.rs b/consensus/state_processing/src/per_epoch_processing/resets.rs index d577c52e6a5..c9f69c3c95e 100644 --- a/consensus/state_processing/src/per_epoch_processing/resets.rs +++ b/consensus/state_processing/src/per_epoch_processing/resets.rs @@ -2,7 +2,7 @@ use super::errors::EpochProcessingError; use safe_arith::SafeArith; use types::beacon_state::BeaconState; use types::eth_spec::EthSpec; -use types::{Unsigned, VariableList}; +use types::{List, Unsigned}; pub fn process_eth1_data_reset( state: &mut BeaconState, @@ -13,7 +13,7 @@ pub fn process_eth1_data_reset( .safe_rem(E::SlotsPerEth1VotingPeriod::to_u64())? == 0 { - *state.eth1_data_votes_mut() = VariableList::empty(); + *state.eth1_data_votes_mut() = List::empty(); } Ok(()) } diff --git a/consensus/state_processing/src/per_epoch_processing/single_pass.rs b/consensus/state_processing/src/per_epoch_processing/single_pass.rs index 9319d2941b5..513fc26b6ff 100644 --- a/consensus/state_processing/src/per_epoch_processing/single_pass.rs +++ b/consensus/state_processing/src/per_epoch_processing/single_pass.rs @@ -12,6 +12,7 @@ use types::{ NUM_FLAG_INDICES, PARTICIPATION_FLAG_WEIGHTS, TIMELY_HEAD_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX, WEIGHT_DENOMINATOR, }, + milhouse::Cow, ActivationQueue, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, ExitCache, ForkName, ParticipationFlags, ProgressiveBalancesCache, RelativeEpoch, Unsigned, Validator, }; @@ -173,9 +174,9 @@ pub fn process_epoch_single_pass( let effective_balances_ctxt = &EffectiveBalancesContext::new(spec)?; // Iterate over the validators and related fields in one pass. - let mut validators_iter = validators.iter_mut(); - let mut balances_iter = balances.iter_mut(); - let mut inactivity_scores_iter = inactivity_scores.iter_mut(); + let mut validators_iter = validators.iter_cow(); + let mut balances_iter = balances.iter_cow(); + let mut inactivity_scores_iter = inactivity_scores.iter_cow(); // Values computed for the next epoch transition. let mut next_epoch_total_active_balance = 0; @@ -186,20 +187,21 @@ pub fn process_epoch_single_pass( previous_epoch_participation.iter(), current_epoch_participation.iter(), ) { - let validator = validators_iter - .next() + let (_, mut validator) = validators_iter + .next_cow() .ok_or(BeaconStateError::UnknownValidator(index))?; - let balance = balances_iter - .next() + let (_, mut balance) = balances_iter + .next_cow() .ok_or(BeaconStateError::UnknownValidator(index))?; - let inactivity_score = inactivity_scores_iter - .next() + let (_, mut inactivity_score) = inactivity_scores_iter + .next_cow() .ok_or(BeaconStateError::UnknownValidator(index))?; let is_active_current_epoch = validator.is_active_at(current_epoch); let is_active_previous_epoch = validator.is_active_at(previous_epoch); let is_eligible = is_active_previous_epoch - || (validator.slashed && previous_epoch.safe_add(1)? < validator.withdrawable_epoch); + || (validator.slashed() + && previous_epoch.safe_add(1)? < validator.withdrawable_epoch()); let base_reward = if is_eligible { epoch_cache.get_base_reward(index)? @@ -209,10 +211,10 @@ pub fn process_epoch_single_pass( let validator_info = &ValidatorInfo { index, - effective_balance: validator.effective_balance, + effective_balance: validator.effective_balance(), base_reward, is_eligible, - is_slashed: validator.slashed, + is_slashed: validator.slashed(), is_active_current_epoch, is_active_previous_epoch, previous_epoch_participation, @@ -223,7 +225,7 @@ pub fn process_epoch_single_pass( // `process_inactivity_updates` if conf.inactivity_updates { process_single_inactivity_update( - inactivity_score, + &mut inactivity_score, validator_info, state_ctxt, spec, @@ -233,8 +235,8 @@ pub fn process_epoch_single_pass( // `process_rewards_and_penalties` if conf.rewards_and_penalties { process_single_reward_and_penalty( - balance, - inactivity_score, + &mut balance, + &inactivity_score, validator_info, rewards_ctxt, state_ctxt, @@ -246,7 +248,7 @@ pub fn process_epoch_single_pass( // `process_registry_updates` if conf.registry_updates { process_single_registry_update( - validator, + &mut validator, validator_info, exit_cache, activation_queue, @@ -258,14 +260,14 @@ pub fn process_epoch_single_pass( // `process_slashings` if conf.slashings { - process_single_slashing(balance, validator, slashings_ctxt, state_ctxt, spec)?; + process_single_slashing(&mut balance, &validator, slashings_ctxt, state_ctxt, spec)?; } // `process_effective_balance_updates` if conf.effective_balance_updates { process_single_effective_balance_update( *balance, - validator, + &mut validator, validator_info, &mut next_epoch_total_active_balance, &mut next_epoch_cache, @@ -290,7 +292,7 @@ pub fn process_epoch_single_pass( } fn process_single_inactivity_update( - inactivity_score: &mut u64, + inactivity_score: &mut Cow, validator_info: &ValidatorInfo, state_ctxt: &StateContext, spec: &ChainSpec, @@ -303,25 +305,27 @@ fn process_single_inactivity_update( if validator_info.is_unslashed_participating_index(TIMELY_TARGET_FLAG_INDEX)? { // Avoid mutating when the inactivity score is 0 and can't go any lower -- the common // case. - if *inactivity_score == 0 { + if **inactivity_score == 0 { return Ok(()); } - inactivity_score.safe_sub_assign(1)?; + inactivity_score.make_mut()?.safe_sub_assign(1)?; } else { - inactivity_score.safe_add_assign(spec.inactivity_score_bias)?; + inactivity_score + .make_mut()? + .safe_add_assign(spec.inactivity_score_bias)?; } // Decrease the score of all validators for forgiveness when not during a leak if !state_ctxt.is_in_inactivity_leak { - let deduction = min(spec.inactivity_score_recovery_rate, *inactivity_score); - inactivity_score.safe_sub_assign(deduction)?; + let deduction = min(spec.inactivity_score_recovery_rate, **inactivity_score); + inactivity_score.make_mut()?.safe_sub_assign(deduction)?; } Ok(()) } fn process_single_reward_and_penalty( - balance: &mut u64, + balance: &mut Cow, inactivity_score: &u64, validator_info: &ValidatorInfo, rewards_ctxt: &RewardsAndPenaltiesContext, @@ -351,6 +355,7 @@ fn process_single_reward_and_penalty( )?; if delta.rewards != 0 || delta.penalties != 0 { + let balance = balance.make_mut()?; balance.safe_add_assign(delta.rewards)?; *balance = balance.saturating_sub(delta.penalties); } @@ -452,7 +457,7 @@ impl RewardsAndPenaltiesContext { } fn process_single_registry_update( - validator: &mut Validator, + validator: &mut Cow, validator_info: &ValidatorInfo, exit_cache: &mut ExitCache, activation_queue: &BTreeSet, @@ -463,16 +468,18 @@ fn process_single_registry_update( let current_epoch = state_ctxt.current_epoch; if validator.is_eligible_for_activation_queue(spec) { - validator.activation_eligibility_epoch = current_epoch.safe_add(1)?; + validator.make_mut()?.mutable.activation_eligibility_epoch = current_epoch.safe_add(1)?; } - if validator.is_active_at(current_epoch) && validator.effective_balance <= spec.ejection_balance + if validator.is_active_at(current_epoch) + && validator.effective_balance() <= spec.ejection_balance { initiate_validator_exit(validator, exit_cache, state_ctxt, spec)?; } if activation_queue.contains(&validator_info.index) { - validator.activation_epoch = spec.compute_activation_exit_epoch(current_epoch)?; + validator.make_mut()?.mutable.activation_epoch = + spec.compute_activation_exit_epoch(current_epoch)?; } // Caching: add to speculative activation queue for next epoch. @@ -487,13 +494,13 @@ fn process_single_registry_update( } fn initiate_validator_exit( - validator: &mut Validator, + validator: &mut Cow, exit_cache: &mut ExitCache, state_ctxt: &StateContext, spec: &ChainSpec, ) -> Result<(), Error> { // Return if the validator already initiated exit - if validator.exit_epoch != spec.far_future_epoch { + if validator.exit_epoch() != spec.far_future_epoch { return Ok(()); } @@ -508,8 +515,9 @@ fn initiate_validator_exit( exit_queue_epoch.safe_add_assign(1)?; } - validator.exit_epoch = exit_queue_epoch; - validator.withdrawable_epoch = + let validator = validator.make_mut()?; + validator.mutable.exit_epoch = exit_queue_epoch; + validator.mutable.withdrawable_epoch = exit_queue_epoch.safe_add(spec.min_validator_withdrawability_delay)?; exit_cache.record_validator_exit(exit_queue_epoch)?; @@ -540,24 +548,25 @@ impl SlashingsContext { } fn process_single_slashing( - balance: &mut u64, + balance: &mut Cow, validator: &Validator, slashings_ctxt: &SlashingsContext, state_ctxt: &StateContext, spec: &ChainSpec, ) -> Result<(), Error> { - if validator.slashed && slashings_ctxt.target_withdrawable_epoch == validator.withdrawable_epoch + if validator.slashed() + && slashings_ctxt.target_withdrawable_epoch == validator.withdrawable_epoch() { let increment = spec.effective_balance_increment; let penalty_numerator = validator - .effective_balance + .effective_balance() .safe_div(increment)? .safe_mul(slashings_ctxt.adjusted_total_slashing_balance)?; let penalty = penalty_numerator .safe_div(state_ctxt.total_active_balance)? .safe_mul(increment)?; - *balance = balance.saturating_sub(penalty); + *balance.make_mut()? = balance.saturating_sub(penalty); } Ok(()) } @@ -581,7 +590,7 @@ impl EffectiveBalancesContext { #[allow(clippy::too_many_arguments)] fn process_single_effective_balance_update( balance: u64, - validator: &mut Validator, + validator: &mut Cow, validator_info: &ValidatorInfo, next_epoch_total_active_balance: &mut u64, next_epoch_cache: &mut PreEpochCache, @@ -590,11 +599,11 @@ fn process_single_effective_balance_update( state_ctxt: &StateContext, spec: &ChainSpec, ) -> Result<(), Error> { - let old_effective_balance = validator.effective_balance; + let old_effective_balance = validator.effective_balance(); let new_effective_balance = if balance.safe_add(eb_ctxt.downward_threshold)? - < validator.effective_balance + < validator.effective_balance() || validator - .effective_balance + .effective_balance() .safe_add(eb_ctxt.upward_threshold)? < balance { @@ -603,7 +612,7 @@ fn process_single_effective_balance_update( spec.max_effective_balance, ) } else { - validator.effective_balance + validator.effective_balance() }; if validator.is_active_at(state_ctxt.next_epoch) { @@ -611,12 +620,12 @@ fn process_single_effective_balance_update( } if new_effective_balance != old_effective_balance { - validator.effective_balance = new_effective_balance; + validator.make_mut()?.mutable.effective_balance = new_effective_balance; // Update progressive balances cache for the *current* epoch, which will soon become the // previous epoch once the epoch transition completes. progressive_balances.on_effective_balance_change( - validator.slashed, + validator.slashed(), validator_info.current_epoch_participation, old_effective_balance, new_effective_balance, diff --git a/consensus/state_processing/src/per_epoch_processing/slashings.rs b/consensus/state_processing/src/per_epoch_processing/slashings.rs index a1770478008..7618c9b6367 100644 --- a/consensus/state_processing/src/per_epoch_processing/slashings.rs +++ b/consensus/state_processing/src/per_epoch_processing/slashings.rs @@ -27,9 +27,9 @@ pub fn process_slashings( .iter() .enumerate() .filter(|(_, validator)| { - validator.slashed && target_withdrawable_epoch == validator.withdrawable_epoch + validator.slashed() && target_withdrawable_epoch == validator.withdrawable_epoch() }) - .map(|(index, validator)| (index, validator.effective_balance)) + .map(|(index, validator)| (index, validator.effective_balance())) .collect::>(); for (index, validator_effective_balance) in indices { diff --git a/consensus/state_processing/src/upgrade/altair.rs b/consensus/state_processing/src/upgrade/altair.rs index cfbc6eba9e9..872560db3df 100644 --- a/consensus/state_processing/src/upgrade/altair.rs +++ b/consensus/state_processing/src/upgrade/altair.rs @@ -4,13 +4,13 @@ use std::mem; use std::sync::Arc; use types::{ BeaconState, BeaconStateAltair, BeaconStateError as Error, ChainSpec, EpochCache, EthSpec, - Fork, ParticipationFlags, PendingAttestation, RelativeEpoch, SyncCommittee, VariableList, + Fork, List, ParticipationFlags, PendingAttestation, RelativeEpoch, SyncCommittee, }; /// Translate the participation information from the epoch prior to the fork into Altair's format. pub fn translate_participation( state: &mut BeaconState, - pending_attestations: &VariableList, E::MaxPendingAttestations>, + pending_attestations: &List, E::MaxPendingAttestations>, spec: &ChainSpec, ) -> Result<(), Error> { // Previous epoch committee cache is required for `get_attesting_indices`. @@ -51,8 +51,8 @@ pub fn upgrade_to_altair( let pre = pre_state.as_base_mut()?; let default_epoch_participation = - VariableList::new(vec![ParticipationFlags::default(); pre.validators.len()])?; - let inactivity_scores = VariableList::new(vec![0; pre.validators.len()])?; + List::new(vec![ParticipationFlags::default(); pre.validators.len()])?; + let inactivity_scores = List::new(vec![0; pre.validators.len()])?; let temp_sync_committee = Arc::new(SyncCommittee::temporary()); @@ -108,7 +108,6 @@ pub fn upgrade_to_altair( exit_cache: mem::take(&mut pre.exit_cache), slashings_cache: mem::take(&mut pre.slashings_cache), epoch_cache: EpochCache::default(), - tree_hash_cache: mem::take(&mut pre.tree_hash_cache), }); // Fill in previous epoch participation from the pre state's pending attestations. diff --git a/consensus/state_processing/src/upgrade/capella.rs b/consensus/state_processing/src/upgrade/capella.rs index 87b40abebdd..51e29d10f3c 100644 --- a/consensus/state_processing/src/upgrade/capella.rs +++ b/consensus/state_processing/src/upgrade/capella.rs @@ -1,7 +1,7 @@ use std::mem; use types::{ BeaconState, BeaconStateCapella, BeaconStateError as Error, ChainSpec, EpochCache, EthSpec, - Fork, VariableList, + Fork, List, }; /// Transform a `Merge` state into an `Capella` state. @@ -61,7 +61,7 @@ pub fn upgrade_to_capella( // Capella next_withdrawal_index: 0, next_withdrawal_validator_index: 0, - historical_summaries: VariableList::default(), + historical_summaries: List::default(), // Caches total_active_balance: pre.total_active_balance, progressive_balances_cache: mem::take(&mut pre.progressive_balances_cache), @@ -70,7 +70,6 @@ pub fn upgrade_to_capella( exit_cache: mem::take(&mut pre.exit_cache), slashings_cache: mem::take(&mut pre.slashings_cache), epoch_cache: EpochCache::default(), - tree_hash_cache: mem::take(&mut pre.tree_hash_cache), }); *pre_state = post; diff --git a/consensus/state_processing/src/upgrade/deneb.rs b/consensus/state_processing/src/upgrade/deneb.rs index 43fe5d9dc3d..c21e1361a5a 100644 --- a/consensus/state_processing/src/upgrade/deneb.rs +++ b/consensus/state_processing/src/upgrade/deneb.rs @@ -71,7 +71,6 @@ pub fn upgrade_to_deneb( exit_cache: mem::take(&mut pre.exit_cache), slashings_cache: mem::take(&mut pre.slashings_cache), epoch_cache: EpochCache::default(), - tree_hash_cache: mem::take(&mut pre.tree_hash_cache), }); *pre_state = post; diff --git a/consensus/state_processing/src/upgrade/electra.rs b/consensus/state_processing/src/upgrade/electra.rs index a37d0fc3beb..f64228f050b 100644 --- a/consensus/state_processing/src/upgrade/electra.rs +++ b/consensus/state_processing/src/upgrade/electra.rs @@ -70,7 +70,6 @@ pub fn upgrade_to_electra( exit_cache: mem::take(&mut pre.exit_cache), slashings_cache: mem::take(&mut pre.slashings_cache), epoch_cache: EpochCache::default(), - tree_hash_cache: mem::take(&mut pre.tree_hash_cache), }); *pre_state = post; diff --git a/consensus/state_processing/src/upgrade/merge.rs b/consensus/state_processing/src/upgrade/merge.rs index 147c97ac29e..02705743ceb 100644 --- a/consensus/state_processing/src/upgrade/merge.rs +++ b/consensus/state_processing/src/upgrade/merge.rs @@ -66,7 +66,6 @@ pub fn upgrade_to_bellatrix( exit_cache: mem::take(&mut pre.exit_cache), slashings_cache: mem::take(&mut pre.slashings_cache), epoch_cache: EpochCache::default(), - tree_hash_cache: mem::take(&mut pre.tree_hash_cache), }); *pre_state = post; diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index db15f53537e..4802481ae82 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -52,6 +52,8 @@ serde_json = { workspace = true } smallvec = { workspace = true } maplit = { workspace = true } strum = { workspace = true } +milhouse = { workspace = true } +rpds = "0.11.0" [dev-dependencies] criterion = { workspace = true } diff --git a/consensus/types/benches/benches.rs b/consensus/types/benches/benches.rs index bb2b527109f..17d266a56e5 100644 --- a/consensus/types/benches/benches.rs +++ b/consensus/types/benches/benches.rs @@ -2,12 +2,13 @@ use criterion::Criterion; use criterion::{black_box, criterion_group, criterion_main, Benchmark}; +use milhouse::List; use rayon::prelude::*; use ssz::Encode; use std::sync::Arc; use types::{ test_utils::generate_deterministic_keypair, BeaconState, Epoch, Eth1Data, EthSpec, Hash256, - MainnetEthSpec, Validator, + MainnetEthSpec, Validator, ValidatorMutable, }; fn get_state(validator_count: usize) -> BeaconState { @@ -27,21 +28,25 @@ fn get_state(validator_count: usize) -> BeaconState { .expect("should add balance"); } - *state.validators_mut() = (0..validator_count) - .collect::>() - .par_iter() - .map(|&i| Validator { - pubkey: generate_deterministic_keypair(i).pk.into(), - withdrawal_credentials: Hash256::from_low_u64_le(i as u64), - effective_balance: spec.max_effective_balance, - slashed: false, - activation_eligibility_epoch: Epoch::new(0), - activation_epoch: Epoch::new(0), - exit_epoch: Epoch::from(u64::max_value()), - withdrawable_epoch: Epoch::from(u64::max_value()), - }) - .collect::>() - .into(); + *state.validators_mut() = List::new( + (0..validator_count) + .collect::>() + .par_iter() + .map(|&i| Validator { + pubkey: Arc::new(generate_deterministic_keypair(i).pk.compress()), + mutable: ValidatorMutable { + withdrawal_credentials: Hash256::from_low_u64_le(i as u64), + effective_balance: spec.max_effective_balance, + slashed: false, + activation_eligibility_epoch: Epoch::new(0), + activation_epoch: Epoch::new(0), + exit_epoch: Epoch::from(u64::max_value()), + withdrawable_epoch: Epoch::from(u64::max_value()), + }, + }) + .collect(), + ) + .unwrap(); state } @@ -96,19 +101,6 @@ fn all_benches(c: &mut Criterion) { .sample_size(10), ); - let inner_state = state.clone(); - c.bench( - &format!("{}_validators", validator_count), - Benchmark::new("clone/tree_hash_cache", move |b| { - b.iter_batched_ref( - || inner_state.clone(), - |state| black_box(state.tree_hash_cache().clone()), - criterion::BatchSize::SmallInput, - ) - }) - .sample_size(10), - ); - let inner_state = state.clone(); c.bench( &format!("{}_validators", validator_count), diff --git a/consensus/types/examples/clone_state.rs b/consensus/types/examples/clone_state.rs deleted file mode 100644 index a7e80cf4078..00000000000 --- a/consensus/types/examples/clone_state.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! These examples only really exist so we can use them for flamegraph. If they get annoying to -//! maintain, feel free to delete. - -use types::{ - test_utils::generate_deterministic_keypair, BeaconState, Eth1Data, EthSpec, Hash256, - MinimalEthSpec, Validator, -}; - -type E = MinimalEthSpec; - -fn get_state(validator_count: usize) -> BeaconState { - let spec = &E::default_spec(); - let eth1_data = Eth1Data { - deposit_root: Hash256::zero(), - deposit_count: 0, - block_hash: Hash256::zero(), - }; - - let mut state = BeaconState::new(0, eth1_data, spec); - - for i in 0..validator_count { - state - .balances_mut() - .push(i as u64) - .expect("should add balance"); - state - .validators_mut() - .push(Validator { - pubkey: generate_deterministic_keypair(i).pk.into(), - withdrawal_credentials: Hash256::from_low_u64_le(i as u64), - effective_balance: i as u64, - slashed: i % 2 == 0, - activation_eligibility_epoch: i.into(), - activation_epoch: i.into(), - exit_epoch: i.into(), - withdrawable_epoch: i.into(), - }) - .expect("should add validator"); - } - - state -} - -fn main() { - let validator_count = 1_024; - let state = get_state(validator_count); - - for _ in 0..100_000 { - let _ = state.clone(); - } -} diff --git a/consensus/types/examples/ssz_encode_state.rs b/consensus/types/examples/ssz_encode_state.rs deleted file mode 100644 index 5d0a2db17c7..00000000000 --- a/consensus/types/examples/ssz_encode_state.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! These examples only really exist so we can use them for flamegraph. If they get annoying to -//! maintain, feel free to delete. - -use ssz::Encode; -use types::{ - test_utils::generate_deterministic_keypair, BeaconState, Eth1Data, EthSpec, Hash256, - MinimalEthSpec, Validator, -}; - -type E = MinimalEthSpec; - -fn get_state(validator_count: usize) -> BeaconState { - let spec = &E::default_spec(); - let eth1_data = Eth1Data { - deposit_root: Hash256::zero(), - deposit_count: 0, - block_hash: Hash256::zero(), - }; - - let mut state = BeaconState::new(0, eth1_data, spec); - - for i in 0..validator_count { - state - .balances_mut() - .push(i as u64) - .expect("should add balance"); - state - .validators_mut() - .push(Validator { - pubkey: generate_deterministic_keypair(i).pk.into(), - withdrawal_credentials: Hash256::from_low_u64_le(i as u64), - effective_balance: i as u64, - slashed: i % 2 == 0, - activation_eligibility_epoch: i.into(), - activation_epoch: i.into(), - exit_epoch: i.into(), - withdrawable_epoch: i.into(), - }) - .expect("should add validator"); - } - - state -} - -fn main() { - let validator_count = 1_024; - let state = get_state(validator_count); - - for _ in 0..1_024 { - let state_bytes = state.as_ssz_bytes(); - let _: BeaconState = - BeaconState::from_ssz_bytes(&state_bytes, &E::default_spec()).expect("should decode"); - } -} diff --git a/consensus/types/examples/tree_hash_state.rs b/consensus/types/examples/tree_hash_state.rs deleted file mode 100644 index 26777b25912..00000000000 --- a/consensus/types/examples/tree_hash_state.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! These examples only really exist so we can use them for flamegraph. If they get annoying to -//! maintain, feel free to delete. - -use types::{ - test_utils::generate_deterministic_keypair, BeaconState, Eth1Data, EthSpec, Hash256, - MinimalEthSpec, Validator, -}; - -type E = MinimalEthSpec; - -fn get_state(validator_count: usize) -> BeaconState { - let spec = &E::default_spec(); - let eth1_data = Eth1Data { - deposit_root: Hash256::zero(), - deposit_count: 0, - block_hash: Hash256::zero(), - }; - - let mut state = BeaconState::new(0, eth1_data, spec); - - for i in 0..validator_count { - state - .balances_mut() - .push(i as u64) - .expect("should add balance"); - state - .validators_mut() - .push(Validator { - pubkey: generate_deterministic_keypair(i).pk.into(), - withdrawal_credentials: Hash256::from_low_u64_le(i as u64), - effective_balance: i as u64, - slashed: i % 2 == 0, - activation_eligibility_epoch: i.into(), - activation_epoch: i.into(), - exit_epoch: i.into(), - withdrawable_epoch: i.into(), - }) - .expect("should add validator"); - } - - state -} - -fn main() { - let validator_count = 1_024; - let mut state = get_state(validator_count); - state.update_tree_hash_cache().expect("should update cache"); - - actual_thing::(&mut state); -} - -fn actual_thing(state: &mut BeaconState) { - for _ in 0..200_024 { - let _ = state.update_tree_hash_cache().expect("should update cache"); - } -} diff --git a/consensus/types/src/activation_queue.rs b/consensus/types/src/activation_queue.rs index 09ffa5b85e7..acbb276a61a 100644 --- a/consensus/types/src/activation_queue.rs +++ b/consensus/types/src/activation_queue.rs @@ -23,7 +23,7 @@ impl ActivationQueue { ) { if validator.could_be_eligible_for_activation_at(next_epoch, spec) { self.queue - .insert((validator.activation_eligibility_epoch, index)); + .insert((validator.activation_eligibility_epoch(), index)); } } diff --git a/consensus/types/src/beacon_block.rs b/consensus/types/src/beacon_block.rs index 14874f0204f..94c44abcc90 100644 --- a/consensus/types/src/beacon_block.rs +++ b/consensus/types/src/beacon_block.rs @@ -358,7 +358,7 @@ impl> BeaconBlockBase { }; let deposit = Deposit { - proof: FixedVector::from_elem(Hash256::zero()), + proof: ssz_types::FixedVector::from_elem(Hash256::zero()), data: deposit_data, }; diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index ba11c9c4cce..eafd12b13ca 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -1,12 +1,14 @@ use self::committee_cache::get_active_validator_indices; use crate::historical_summary::HistoricalSummary; use crate::test_utils::TestRandom; +use crate::validator::ValidatorTrait; use crate::*; use compare_fields::CompareFields; use compare_fields_derive::CompareFields; use derivative::Derivative; use ethereum_hashing::hash; use int_to_bytes::{int_to_bytes4, int_to_bytes8}; +use metastruct::{metastruct, NumFields}; pub use pubkey_cache::PubkeyCache; use safe_arith::{ArithError, SafeArith}; use serde::{Deserialize, Serialize}; @@ -28,28 +30,26 @@ pub use crate::beacon_state::balance::Balance; pub use crate::beacon_state::exit_cache::ExitCache; pub use crate::beacon_state::progressive_balances_cache::*; pub use crate::beacon_state::slashings_cache::SlashingsCache; -pub use clone_config::CloneConfig; pub use eth_spec::*; pub use iter::BlockRootsIter; -pub use tree_hash_cache::BeaconTreeHashCache; +pub use milhouse::{interface::Interface, List, Vector}; #[macro_use] mod committee_cache; mod balance; -mod clone_config; +pub mod compact_state; mod exit_cache; mod iter; mod progressive_balances_cache; mod pubkey_cache; mod slashings_cache; mod tests; -mod tree_hash_cache; pub const CACHED_EPOCHS: usize = 3; const MAX_RANDOM_BYTE: u64 = (1 << 8) - 1; -pub type Validators = VariableList::ValidatorRegistryLimit>; -pub type Balances = VariableList::ValidatorRegistryLimit>; +pub type Validators = List::ValidatorRegistryLimit>; +pub type Balances = List::ValidatorRegistryLimit>; #[derive(Debug, PartialEq, Clone)] pub enum Error { @@ -144,6 +144,20 @@ pub enum Error { current_epoch: Epoch, epoch: Epoch, }, + MilhouseError(milhouse::Error), + CommitteeCacheDiffInvalidEpoch { + prev_current_epoch: Epoch, + current_epoch: Epoch, + }, + CommitteeCacheDiffUninitialized { + expected_epoch: Epoch, + }, + DiffAcrossFork { + prev_fork: ForkName, + current_fork: ForkName, + }, + TotalActiveBalanceDiffUninitialized, + MissingImmutableValidator(usize), IndexNotSupported(usize), InvalidFlagIndex(usize), MerkleTreeError(merkle_proof::MerkleTreeError), @@ -207,97 +221,206 @@ impl From for Hash256 { TreeHash, TestRandom, CompareFields, - arbitrary::Arbitrary + arbitrary::Arbitrary, ), serde(bound = "E: EthSpec", deny_unknown_fields), - arbitrary(bound = "E: EthSpec"), + arbitrary(bound = "E: EthSpec, GenericValidator: ValidatorTrait"), derivative(Clone), ), + specific_variant_attributes( + Base(metastruct( + mappings( + map_beacon_state_base_fields(), + map_beacon_state_base_tree_list_fields(mutable, fallible, groups(tree_lists)), + ), + bimappings(bimap_beacon_state_base_tree_list_fields( + other_type = "BeaconStateBase", + self_mutable, + fallible, + groups(tree_lists) + )), + num_fields(all()), + )), + Altair(metastruct( + mappings( + map_beacon_state_altair_fields(), + map_beacon_state_altair_tree_list_fields(mutable, fallible, groups(tree_lists)), + ), + bimappings(bimap_beacon_state_altair_tree_list_fields( + other_type = "BeaconStateAltair", + self_mutable, + fallible, + groups(tree_lists) + )), + num_fields(all()), + )), + Merge(metastruct( + mappings( + map_beacon_state_bellatrix_fields(), + map_beacon_state_bellatrix_tree_list_fields(mutable, fallible, groups(tree_lists)), + ), + bimappings(bimap_beacon_state_merge_tree_list_fields( + other_type = "BeaconStateMerge", + self_mutable, + fallible, + groups(tree_lists) + )), + num_fields(all()), + )), + Capella(metastruct( + mappings( + map_beacon_state_capella_fields(), + map_beacon_state_capella_tree_list_fields(mutable, fallible, groups(tree_lists)), + ), + bimappings(bimap_beacon_state_capella_tree_list_fields( + other_type = "BeaconStateCapella", + self_mutable, + fallible, + groups(tree_lists) + )), + num_fields(all()), + )), + Deneb(metastruct( + mappings( + map_beacon_state_deneb_fields(), + map_beacon_state_deneb_tree_list_fields(mutable, fallible, groups(tree_lists)), + ), + bimappings(bimap_beacon_state_deneb_tree_list_fields( + other_type = "BeaconStateDeneb", + self_mutable, + fallible, + groups(tree_lists) + )), + num_fields(all()), + )), + Electra(metastruct( + mappings( + map_beacon_state_electra_fields(), + map_beacon_state_electra_tree_list_fields(mutable, fallible, groups(tree_lists)), + ), + bimappings(bimap_beacon_state_electra_tree_list_fields( + other_type = "BeaconStateElectra", + self_mutable, + fallible, + groups(tree_lists) + )), + num_fields(all()), + )) + ), cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"), - partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant") + partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant"), + map_ref_mut_into(BeaconStateRef) +)] +#[derive( + Debug, PartialEq, Clone, Serialize, Deserialize, Encode, TreeHash, arbitrary::Arbitrary, )] -#[derive(Debug, PartialEq, Serialize, Deserialize, Encode, TreeHash, arbitrary::Arbitrary)] #[serde(untagged)] #[serde(bound = "E: EthSpec")] -#[arbitrary(bound = "E: EthSpec")] +#[arbitrary(bound = "E: EthSpec, GenericValidator: ValidatorTrait")] #[tree_hash(enum_behaviour = "transparent")] #[ssz(enum_behaviour = "transparent")] -pub struct BeaconState +pub struct BeaconState where E: EthSpec, { // Versioning #[superstruct(getter(copy))] + #[metastruct(exclude_from(tree_lists))] #[serde(with = "serde_utils::quoted_u64")] pub genesis_time: u64, #[superstruct(getter(copy))] + #[metastruct(exclude_from(tree_lists))] pub genesis_validators_root: Hash256, #[superstruct(getter(copy))] + #[metastruct(exclude_from(tree_lists))] pub slot: Slot, #[superstruct(getter(copy))] + #[metastruct(exclude_from(tree_lists))] pub fork: Fork, // History + #[metastruct(exclude_from(tree_lists))] pub latest_block_header: BeaconBlockHeader, - #[compare_fields(as_slice)] - pub block_roots: FixedVector, - #[compare_fields(as_slice)] - pub state_roots: FixedVector, + #[test_random(default)] + #[compare_fields(as_iter)] + pub block_roots: Vector, + #[test_random(default)] + #[compare_fields(as_iter)] + pub state_roots: Vector, // Frozen in Capella, replaced by historical_summaries - pub historical_roots: VariableList, + #[test_random(default)] + #[compare_fields(as_iter)] + pub historical_roots: List, // Ethereum 1.0 chain data + #[metastruct(exclude_from(tree_lists))] pub eth1_data: Eth1Data, - pub eth1_data_votes: VariableList, + #[test_random(default)] + pub eth1_data_votes: List, #[superstruct(getter(copy))] + #[metastruct(exclude_from(tree_lists))] #[serde(with = "serde_utils::quoted_u64")] pub eth1_deposit_index: u64, // Registry - #[compare_fields(as_slice)] - pub validators: VariableList, - #[compare_fields(as_slice)] + #[test_random(default)] + pub validators: List, #[serde(with = "ssz_types::serde_utils::quoted_u64_var_list")] - pub balances: VariableList, + #[compare_fields(as_iter)] + #[test_random(default)] + pub balances: List, // Randomness - pub randao_mixes: FixedVector, + #[test_random(default)] + pub randao_mixes: Vector, // Slashings + #[test_random(default)] #[serde(with = "ssz_types::serde_utils::quoted_u64_fixed_vec")] - pub slashings: FixedVector, + pub slashings: Vector, // Attestations (genesis fork only) #[superstruct(only(Base))] - pub previous_epoch_attestations: VariableList, E::MaxPendingAttestations>, + #[test_random(default)] + pub previous_epoch_attestations: List, E::MaxPendingAttestations>, #[superstruct(only(Base))] - pub current_epoch_attestations: VariableList, E::MaxPendingAttestations>, + #[test_random(default)] + pub current_epoch_attestations: List, E::MaxPendingAttestations>, // Participation (Altair and later) #[superstruct(only(Altair, Merge, Capella, Deneb, Electra))] - pub previous_epoch_participation: VariableList, + #[test_random(default)] + pub previous_epoch_participation: List, #[superstruct(only(Altair, Merge, Capella, Deneb, Electra))] - pub current_epoch_participation: VariableList, + #[test_random(default)] + pub current_epoch_participation: List, // Finality #[test_random(default)] + #[metastruct(exclude_from(tree_lists))] pub justification_bits: BitVector, #[superstruct(getter(copy))] + #[metastruct(exclude_from(tree_lists))] pub previous_justified_checkpoint: Checkpoint, #[superstruct(getter(copy))] + #[metastruct(exclude_from(tree_lists))] pub current_justified_checkpoint: Checkpoint, #[superstruct(getter(copy))] + #[metastruct(exclude_from(tree_lists))] pub finalized_checkpoint: Checkpoint, // Inactivity #[serde(with = "ssz_types::serde_utils::quoted_u64_var_list")] #[superstruct(only(Altair, Merge, Capella, Deneb, Electra))] - pub inactivity_scores: VariableList, + #[test_random(default)] + pub inactivity_scores: List, // Light-client sync committees #[superstruct(only(Altair, Merge, Capella, Deneb, Electra))] + #[metastruct(exclude_from(tree_lists))] pub current_sync_committee: Arc>, #[superstruct(only(Altair, Merge, Capella, Deneb, Electra))] + #[metastruct(exclude_from(tree_lists))] pub next_sync_committee: Arc>, // Execution @@ -305,89 +428,85 @@ where only(Merge), partial_getter(rename = "latest_execution_payload_header_merge") )] + #[metastruct(exclude_from(tree_lists))] pub latest_execution_payload_header: ExecutionPayloadHeaderMerge, #[superstruct( only(Capella), partial_getter(rename = "latest_execution_payload_header_capella") )] + #[metastruct(exclude_from(tree_lists))] pub latest_execution_payload_header: ExecutionPayloadHeaderCapella, #[superstruct( only(Deneb), partial_getter(rename = "latest_execution_payload_header_deneb") )] + #[metastruct(exclude_from(tree_lists))] pub latest_execution_payload_header: ExecutionPayloadHeaderDeneb, #[superstruct( only(Electra), partial_getter(rename = "latest_execution_payload_header_electra") )] + #[metastruct(exclude_from(tree_lists))] pub latest_execution_payload_header: ExecutionPayloadHeaderElectra, // Capella #[superstruct(only(Capella, Deneb, Electra), partial_getter(copy))] #[serde(with = "serde_utils::quoted_u64")] + #[metastruct(exclude_from(tree_lists))] pub next_withdrawal_index: u64, #[superstruct(only(Capella, Deneb, Electra), partial_getter(copy))] #[serde(with = "serde_utils::quoted_u64")] + #[metastruct(exclude_from(tree_lists))] pub next_withdrawal_validator_index: u64, // Deep history valid from Capella onwards. #[superstruct(only(Capella, Deneb, Electra))] - pub historical_summaries: VariableList, + #[test_random(default)] + pub historical_summaries: List, // Caching (not in the spec) #[serde(skip_serializing, skip_deserializing)] #[ssz(skip_serializing, skip_deserializing)] #[tree_hash(skip_hashing)] #[test_random(default)] - #[derivative(Clone(clone_with = "clone_default"))] + #[metastruct(exclude)] pub total_active_balance: Option<(Epoch, u64)>, #[serde(skip_serializing, skip_deserializing)] #[ssz(skip_serializing, skip_deserializing)] #[tree_hash(skip_hashing)] #[test_random(default)] - #[derivative(Clone(clone_with = "clone_default"))] - pub progressive_balances_cache: ProgressiveBalancesCache, + #[metastruct(exclude)] + pub committee_caches: [Arc; CACHED_EPOCHS], #[serde(skip_serializing, skip_deserializing)] #[ssz(skip_serializing, skip_deserializing)] #[tree_hash(skip_hashing)] #[test_random(default)] - #[derivative(Clone(clone_with = "clone_default"))] - pub committee_caches: [CommitteeCache; CACHED_EPOCHS], + #[metastruct(exclude)] + pub progressive_balances_cache: ProgressiveBalancesCache, #[serde(skip_serializing, skip_deserializing)] #[ssz(skip_serializing, skip_deserializing)] #[tree_hash(skip_hashing)] #[test_random(default)] - #[derivative(Clone(clone_with = "clone_default"))] + #[metastruct(exclude)] pub pubkey_cache: PubkeyCache, #[serde(skip_serializing, skip_deserializing)] #[ssz(skip_serializing, skip_deserializing)] #[tree_hash(skip_hashing)] #[test_random(default)] - #[derivative(Clone(clone_with = "clone_default"))] + #[metastruct(exclude)] pub exit_cache: ExitCache, #[serde(skip_serializing, skip_deserializing)] #[ssz(skip_serializing, skip_deserializing)] #[tree_hash(skip_hashing)] #[test_random(default)] - #[derivative(Clone(clone_with = "clone_default"))] + #[metastruct(exclude)] pub slashings_cache: SlashingsCache, /// Epoch cache of values that are useful for block processing that are static over an epoch. #[serde(skip_serializing, skip_deserializing)] #[ssz(skip_serializing, skip_deserializing)] #[tree_hash(skip_hashing)] #[test_random(default)] + #[metastruct(exclude)] pub epoch_cache: EpochCache, - #[serde(skip_serializing, skip_deserializing)] - #[ssz(skip_serializing, skip_deserializing)] - #[tree_hash(skip_hashing)] - #[test_random(default)] - #[derivative(Clone(clone_with = "clone_default"))] - pub tree_hash_cache: BeaconTreeHashCache, -} - -impl Clone for BeaconState { - fn clone(&self) -> Self { - self.clone_with(CloneConfig::all()) - } } impl BeaconState { @@ -395,6 +514,7 @@ impl BeaconState { /// /// Not a complete genesis state, see `initialize_beacon_state_from_eth1`. pub fn new(genesis_time: u64, eth1_data: Eth1Data, spec: &ChainSpec) -> Self { + let default_committee_cache = Arc::new(CommitteeCache::default()); BeaconState::Base(BeaconStateBase { // Versioning genesis_time, @@ -408,28 +528,28 @@ impl BeaconState { // History latest_block_header: BeaconBlock::::empty(spec).temporary_block_header(), - block_roots: FixedVector::from_elem(Hash256::zero()), - state_roots: FixedVector::from_elem(Hash256::zero()), - historical_roots: VariableList::empty(), + block_roots: Vector::default(), + state_roots: Vector::default(), + historical_roots: List::default(), // Eth1 eth1_data, - eth1_data_votes: VariableList::empty(), + eth1_data_votes: List::default(), eth1_deposit_index: 0, // Validator registry - validators: VariableList::empty(), // Set later. - balances: VariableList::empty(), // Set later. + validators: List::default(), // Set later. + balances: List::default(), // Set later. // Randomness - randao_mixes: FixedVector::from_elem(Hash256::zero()), + randao_mixes: Vector::default(), // Slashings - slashings: FixedVector::from_elem(0), + slashings: Vector::default(), // Attestations - previous_epoch_attestations: VariableList::empty(), - current_epoch_attestations: VariableList::empty(), + previous_epoch_attestations: List::default(), + current_epoch_attestations: List::default(), // Finality justification_bits: BitVector::new(), @@ -441,15 +561,14 @@ impl BeaconState { total_active_balance: None, progressive_balances_cache: <_>::default(), committee_caches: [ - CommitteeCache::default(), - CommitteeCache::default(), - CommitteeCache::default(), + default_committee_cache.clone(), + default_committee_cache.clone(), + default_committee_cache, ], pubkey_cache: PubkeyCache::default(), exit_cache: ExitCache::default(), slashings_cache: SlashingsCache::default(), epoch_cache: EpochCache::default(), - tree_hash_cache: <_>::default(), }) } @@ -485,30 +604,6 @@ impl BeaconState { } } - /// Specialised deserialisation method that uses the `ChainSpec` as context. - #[allow(clippy::arithmetic_side_effects)] - pub fn from_ssz_bytes(bytes: &[u8], spec: &ChainSpec) -> Result { - // Slot is after genesis_time (u64) and genesis_validators_root (Hash256). - let slot_start = ::ssz_fixed_len() + ::ssz_fixed_len(); - let slot_end = slot_start + ::ssz_fixed_len(); - - let slot_bytes = bytes - .get(slot_start..slot_end) - .ok_or(DecodeError::InvalidByteLength { - len: bytes.len(), - expected: slot_end, - })?; - - let slot = Slot::from_ssz_bytes(slot_bytes)?; - let fork_at_slot = spec.fork_name_at_slot::(slot); - - Ok(map_fork_name!( - fork_at_slot, - Self, - <_>::from_ssz_bytes(bytes)? - )) - } - /// Returns the `tree_hash_root` of the state. /// /// Spec v0.12.1 @@ -516,11 +611,15 @@ impl BeaconState { Hash256::from_slice(&self.tree_hash_root()[..]) } - pub fn historical_batch(&self) -> HistoricalBatch { - HistoricalBatch { + pub fn historical_batch(&mut self) -> Result, Error> { + // Updating before cloning makes the clone cheap and saves repeated hashing. + self.block_roots_mut().apply_updates()?; + self.state_roots_mut().apply_updates()?; + + Ok(HistoricalBatch { block_roots: self.block_roots().clone(), state_roots: self.state_roots().clone(), - } + }) } /// This method ensures the state's pubkey cache is fully up-to-date before checking if the validator @@ -531,6 +630,21 @@ impl BeaconState { Ok(self.pubkey_cache().get(pubkey)) } + /// Immutable variant of `get_validator_index` which errors if the cache is not up to date. + pub fn get_validator_index_read_only( + &self, + pubkey: &PublicKeyBytes, + ) -> Result, Error> { + let pubkey_cache = self.pubkey_cache(); + if pubkey_cache.len() != self.validators().len() { + return Err(Error::PubkeyCacheIncomplete { + cache_len: pubkey_cache.len(), + registry_len: self.validators().len(), + }); + } + Ok(pubkey_cache.get(pubkey)) + } + /// The epoch corresponding to `self.slot()`. pub fn current_epoch(&self) -> Epoch { self.slot().epoch(E::slots_per_epoch()) @@ -952,7 +1066,7 @@ impl BeaconState { .get(shuffled_index) .ok_or(Error::ShuffleIndexOutOfBounds(shuffled_index))?; let random_byte = Self::shuffling_random_byte(i, seed.as_bytes())?; - let effective_balance = self.get_validator(candidate_index)?.effective_balance; + let effective_balance = self.get_validator(candidate_index)?.effective_balance(); if effective_balance.safe_mul(MAX_RANDOM_BYTE)? >= spec .max_effective_balance @@ -974,7 +1088,7 @@ impl BeaconState { .map(|&index| { self.validators() .get(index) - .map(|v| v.pubkey) + .map(|v| *v.pubkey()) .ok_or(Error::UnknownValidator(index)) }) .collect::, _>>()?; @@ -985,7 +1099,7 @@ impl BeaconState { let aggregate_pubkey = AggregatePublicKey::aggregate(&decompressed_pubkeys)?; Ok(SyncCommittee { - pubkeys: FixedVector::new(pubkeys)?, + pubkeys: ssz_types::FixedVector::new(pubkeys)?, aggregate_pubkey: aggregate_pubkey.to_public_key().compress(), }) } @@ -1005,7 +1119,7 @@ impl BeaconState { Ok(validator_indices .iter() .map(|&validator_index| { - let pubkey = self.get_validator(validator_index as usize)?.pubkey; + let pubkey = *self.get_validator(validator_index as usize)?.pubkey(); Ok(SyncDuty::from_sync_committee( validator_index, @@ -1079,8 +1193,9 @@ impl BeaconState { } /// Fill `randao_mixes` with - pub fn fill_randao_mixes_with(&mut self, index_root: Hash256) { - *self.randao_mixes_mut() = FixedVector::from_elem(index_root); + pub fn fill_randao_mixes_with(&mut self, index_root: Hash256) -> Result<(), Error> { + *self.randao_mixes_mut() = Vector::from_elem(index_root)?; + Ok(()) } /// Safely obtains the index for `randao_mixes` @@ -1213,7 +1328,7 @@ impl BeaconState { } /// Get a reference to the entire `slashings` vector. - pub fn get_all_slashings(&self) -> &[u64] { + pub fn get_all_slashings(&self) -> &Vector { self.slashings() } @@ -1237,45 +1352,25 @@ impl BeaconState { } /// Convenience accessor for validators and balances simultaneously. - pub fn validators_and_balances_and_progressive_balances_mut( - &mut self, + pub fn validators_and_balances_and_progressive_balances_mut<'a>( + &'a mut self, ) -> ( - &mut Validators, - &mut Balances, - &mut ProgressiveBalancesCache, + &'a mut Validators, + &'a mut Balances, + &'a mut ProgressiveBalancesCache, ) { - match self { - BeaconState::Base(state) => ( - &mut state.validators, - &mut state.balances, - &mut state.progressive_balances_cache, - ), - BeaconState::Altair(state) => ( - &mut state.validators, - &mut state.balances, - &mut state.progressive_balances_cache, - ), - BeaconState::Merge(state) => ( - &mut state.validators, - &mut state.balances, - &mut state.progressive_balances_cache, - ), - BeaconState::Capella(state) => ( - &mut state.validators, - &mut state.balances, - &mut state.progressive_balances_cache, - ), - BeaconState::Deneb(state) => ( - &mut state.validators, - &mut state.balances, - &mut state.progressive_balances_cache, - ), - BeaconState::Electra(state) => ( - &mut state.validators, - &mut state.balances, - &mut state.progressive_balances_cache, - ), - } + map_beacon_state_ref_mut_into_beacon_state_ref!(&'a _, self.to_mut(), |inner, cons| { + if false { + cons(&*inner); + unreachable!() + } else { + ( + &mut inner.validators, + &mut inner.balances, + &mut inner.progressive_balances_cache, + ) + } + }) } #[allow(clippy::type_complexity)] @@ -1285,9 +1380,9 @@ impl BeaconState { ( &mut Validators, &mut Balances, - &VariableList, - &VariableList, - &mut VariableList, + &List, + &List, + &mut List, &mut ProgressiveBalancesCache, &mut ExitCache, &mut EpochCache, @@ -1349,6 +1444,13 @@ impl BeaconState { } } + /// Get a mutable reference to the balance of a single validator. + pub fn get_balance_mut(&mut self, validator_index: usize) -> Result<&mut u64, Error> { + self.balances_mut() + .get_mut(validator_index) + .ok_or(Error::BalancesOutOfBounds(validator_index)) + } + /// Generate a seed for the given `epoch`. pub fn get_seed( &self, @@ -1398,10 +1500,20 @@ impl BeaconState { .ok_or(Error::UnknownValidator(validator_index)) } + /// Safe copy-on-write accessor for the `validators` list. + pub fn get_validator_cow( + &mut self, + validator_index: usize, + ) -> Result, Error> { + self.validators_mut() + .get_cow(validator_index) + .ok_or(Error::UnknownValidator(validator_index)) + } + /// Return the effective balance for a validator with the given `validator_index`. pub fn get_effective_balance(&self, validator_index: usize) -> Result { self.get_validator(validator_index) - .map(|v| v.effective_balance) + .map(|v| v.effective_balance()) } /// Get the inactivity score for a single validator. @@ -1423,13 +1535,6 @@ impl BeaconState { .ok_or(Error::InactivityScoresOutOfBounds(validator_index)) } - /// Get a mutable reference to the balance of a single validator. - pub fn get_balance_mut(&mut self, validator_index: usize) -> Result<&mut u64, Error> { - self.balances_mut() - .get_mut(validator_index) - .ok_or(Error::BalancesOutOfBounds(validator_index)) - } - /// Return the epoch at which an activation or exit triggered in ``epoch`` takes effect. /// /// Spec v0.12.1 @@ -1497,7 +1602,7 @@ impl BeaconState { for validator in self.validators() { if validator.is_active_at(current_epoch) { - total_active_balance.safe_add_assign(validator.effective_balance)?; + total_active_balance.safe_add_assign(validator.effective_balance())?; } } Ok(std::cmp::max( @@ -1575,7 +1680,7 @@ impl BeaconState { epoch: Epoch, previous_epoch: Epoch, current_epoch: Epoch, - ) -> Result<&mut VariableList, Error> { + ) -> Result<&mut List, Error> { if epoch == current_epoch { match self { BeaconState::Base(_) => Err(BeaconStateError::IncorrectStateVariant), @@ -1659,7 +1764,6 @@ impl BeaconState { self.drop_committee_cache(RelativeEpoch::Current)?; self.drop_committee_cache(RelativeEpoch::Next)?; self.drop_pubkey_cache(); - self.drop_tree_hash_cache(); self.drop_progressive_balances_cache(); *self.exit_cache_mut() = ExitCache::default(); *self.slashings_cache_mut() = SlashingsCache::default(); @@ -1718,7 +1822,7 @@ impl BeaconState { &self, epoch: Epoch, spec: &ChainSpec, - ) -> Result { + ) -> Result, Error> { CommitteeCache::initialized(self, epoch, spec) } @@ -1732,7 +1836,7 @@ impl BeaconState { self.committee_caches_mut().rotate_left(1); let next = Self::committee_cache_index(RelativeEpoch::Next); - *self.committee_cache_at_index_mut(next)? = CommitteeCache::default(); + *self.committee_cache_at_index_mut(next)? = Arc::new(CommitteeCache::default()); Ok(()) } @@ -1747,21 +1851,24 @@ impl BeaconState { /// Get the committee cache for some `slot`. /// /// Return an error if the cache for the slot's epoch is not initialized. - fn committee_cache_at_slot(&self, slot: Slot) -> Result<&CommitteeCache, Error> { + fn committee_cache_at_slot(&self, slot: Slot) -> Result<&Arc, Error> { let epoch = slot.epoch(E::slots_per_epoch()); let relative_epoch = RelativeEpoch::from_epoch(self.current_epoch(), epoch)?; self.committee_cache(relative_epoch) } /// Get the committee cache at a given index. - fn committee_cache_at_index(&self, index: usize) -> Result<&CommitteeCache, Error> { + fn committee_cache_at_index(&self, index: usize) -> Result<&Arc, Error> { self.committee_caches() .get(index) .ok_or(Error::CommitteeCachesOutOfBounds(index)) } /// Get a mutable reference to the committee cache at a given index. - fn committee_cache_at_index_mut(&mut self, index: usize) -> Result<&mut CommitteeCache, Error> { + fn committee_cache_at_index_mut( + &mut self, + index: usize, + ) -> Result<&mut Arc, Error> { self.committee_caches_mut() .get_mut(index) .ok_or(Error::CommitteeCachesOutOfBounds(index)) @@ -1769,7 +1876,10 @@ impl BeaconState { /// Returns the cache for some `RelativeEpoch`. Returns an error if the cache has not been /// initialized. - pub fn committee_cache(&self, relative_epoch: RelativeEpoch) -> Result<&CommitteeCache, Error> { + pub fn committee_cache( + &self, + relative_epoch: RelativeEpoch, + ) -> Result<&Arc, Error> { let i = Self::committee_cache_index(relative_epoch); let cache = self.committee_cache_at_index(i)?; @@ -1780,30 +1890,10 @@ impl BeaconState { } } - /// Returns the cache for some `RelativeEpoch`, replacing the existing cache with an - /// un-initialized cache. Returns an error if the existing cache has not been initialized. - pub fn take_committee_cache( - &mut self, - relative_epoch: RelativeEpoch, - ) -> Result { - let i = Self::committee_cache_index(relative_epoch); - let current_epoch = self.current_epoch(); - let cache = self - .committee_caches_mut() - .get_mut(i) - .ok_or(Error::CommitteeCachesOutOfBounds(i))?; - - if cache.is_initialized_at(relative_epoch.into_epoch(current_epoch)) { - Ok(mem::take(cache)) - } else { - Err(Error::CommitteeCacheUninitialized(Some(relative_epoch))) - } - } - /// Drops the cache, leaving it in an uninitialized state. pub fn drop_committee_cache(&mut self, relative_epoch: RelativeEpoch) -> Result<(), Error> { *self.committee_cache_at_index_mut(Self::committee_cache_index(relative_epoch))? = - CommitteeCache::default(); + Arc::new(CommitteeCache::default()); Ok(()) } @@ -1813,13 +1903,11 @@ impl BeaconState { /// never re-add a pubkey. pub fn update_pubkey_cache(&mut self) -> Result<(), Error> { let mut pubkey_cache = mem::take(self.pubkey_cache_mut()); - for (i, validator) in self - .validators() - .iter() - .enumerate() - .skip(pubkey_cache.len()) - { - let success = pubkey_cache.insert(validator.pubkey, i); + let start_index = pubkey_cache.len(); + + for (i, validator) in self.validators().iter_from(start_index)?.enumerate() { + let index = start_index.safe_add(i)?; + let success = pubkey_cache.insert(*validator.pubkey(), index); if !success { return Err(Error::PubkeyCacheInconsistent); } @@ -1834,96 +1922,51 @@ impl BeaconState { *self.pubkey_cache_mut() = PubkeyCache::default() } + pub fn has_pending_mutations(&self) -> bool { + self.block_roots().has_pending_updates() + || self.state_roots().has_pending_updates() + || self.historical_roots().has_pending_updates() + || self.eth1_data_votes().has_pending_updates() + || self.validators().has_pending_updates() + || self.balances().has_pending_updates() + || self.randao_mixes().has_pending_updates() + || self.slashings().has_pending_updates() + || self + .previous_epoch_attestations() + .map_or(false, List::has_pending_updates) + || self + .current_epoch_attestations() + .map_or(false, List::has_pending_updates) + || self + .previous_epoch_participation() + .map_or(false, List::has_pending_updates) + || self + .current_epoch_participation() + .map_or(false, List::has_pending_updates) + || self + .inactivity_scores() + .map_or(false, List::has_pending_updates) + } + /// Completely drops the `progressive_balances_cache` cache, replacing it with a new, empty cache. fn drop_progressive_balances_cache(&mut self) { *self.progressive_balances_cache_mut() = ProgressiveBalancesCache::default(); } - /// Initialize but don't fill the tree hash cache, if it isn't already initialized. - pub fn initialize_tree_hash_cache(&mut self) { - if !self.tree_hash_cache().is_initialized() { - *self.tree_hash_cache_mut() = BeaconTreeHashCache::new(self) - } - } - /// Compute the tree hash root of the state using the tree hash cache. /// /// Initialize the tree hash cache if it isn't already initialized. pub fn update_tree_hash_cache(&mut self) -> Result { - self.initialize_tree_hash_cache(); - - let cache = self.tree_hash_cache_mut().take(); - - if let Some(mut cache) = cache { - // Note: we return early if the tree hash fails, leaving `self.tree_hash_cache` as - // None. There's no need to keep a cache that fails. - let root = cache.recalculate_tree_hash_root(self)?; - self.tree_hash_cache_mut().restore(cache); - Ok(root) - } else { - Err(Error::TreeHashCacheNotInitialized) - } + self.apply_pending_mutations()?; + Ok(self.tree_hash_root()) } /// Compute the tree hash root of the validators using the tree hash cache. /// /// Initialize the tree hash cache if it isn't already initialized. pub fn update_validators_tree_hash_cache(&mut self) -> Result { - self.initialize_tree_hash_cache(); - - let cache = self.tree_hash_cache_mut().take(); - - if let Some(mut cache) = cache { - // Note: we return early if the tree hash fails, leaving `self.tree_hash_cache` as - // None. There's no need to keep a cache that fails. - let root = cache.recalculate_validators_tree_hash_root(self.validators())?; - self.tree_hash_cache_mut().restore(cache); - Ok(root) - } else { - Err(Error::TreeHashCacheNotInitialized) - } - } - - /// Completely drops the tree hash cache, replacing it with a new, empty cache. - pub fn drop_tree_hash_cache(&mut self) { - self.tree_hash_cache_mut().uninitialize(); - } - - /// Clone the state whilst preserving only the selected caches. - pub fn clone_with(&self, config: CloneConfig) -> Self { - let mut res = match self { - BeaconState::Base(inner) => BeaconState::Base(inner.clone()), - BeaconState::Altair(inner) => BeaconState::Altair(inner.clone()), - BeaconState::Merge(inner) => BeaconState::Merge(inner.clone()), - BeaconState::Capella(inner) => BeaconState::Capella(inner.clone()), - BeaconState::Deneb(inner) => BeaconState::Deneb(inner.clone()), - BeaconState::Electra(inner) => BeaconState::Electra(inner.clone()), - }; - if config.committee_caches { - res.committee_caches_mut() - .clone_from(self.committee_caches()); - *res.total_active_balance_mut() = *self.total_active_balance(); - } - if config.pubkey_cache { - *res.pubkey_cache_mut() = self.pubkey_cache().clone(); - } - if config.exit_cache { - *res.exit_cache_mut() = self.exit_cache().clone(); - } - if config.slashings_cache { - *res.slashings_cache_mut() = self.slashings_cache().clone(); - } - if config.tree_hash_cache { - *res.tree_hash_cache_mut() = self.tree_hash_cache().clone(); - } - if config.progressive_balances_cache { - *res.progressive_balances_cache_mut() = self.progressive_balances_cache().clone(); - } - res - } - - pub fn clone_with_only_committee_caches(&self) -> Self { - self.clone_with(CloneConfig::committee_caches_only()) + self.validators_mut().apply_updates()?; + Ok(self.validators().tree_hash_root()) } /// Passing `previous_epoch` to this function rather than computing it internally provides @@ -1934,7 +1977,8 @@ impl BeaconState { val: &Validator, ) -> Result { Ok(val.is_active_at(previous_epoch) - || (val.slashed && previous_epoch.safe_add(Epoch::new(1))? < val.withdrawable_epoch)) + || (val.slashed() + && previous_epoch.safe_add(Epoch::new(1))? < val.withdrawable_epoch())) } /// Passing `previous_epoch` to this function rather than computing it internally provides @@ -1979,10 +2023,181 @@ impl BeaconState { self.epoch_cache().get_base_reward(validator_index) } - pub fn compute_merkle_proof( - &mut self, - generalized_index: usize, - ) -> Result, Error> { + #[allow(clippy::arithmetic_side_effects)] + pub fn rebase_on(&mut self, base: &Self, spec: &ChainSpec) -> Result<(), Error> { + // Required for macros (which use type-hints internally). + type GenericValidator = Validator; + + match (&mut *self, base) { + (Self::Base(self_inner), Self::Base(base_inner)) => { + bimap_beacon_state_base_tree_list_fields!( + self_inner, + base_inner, + |_, self_field, base_field| { self_field.rebase_on(base_field) } + ); + } + (Self::Base(_), _) => (), + (Self::Altair(self_inner), Self::Altair(base_inner)) => { + bimap_beacon_state_altair_tree_list_fields!( + self_inner, + base_inner, + |_, self_field, base_field| { self_field.rebase_on(base_field) } + ); + } + (Self::Altair(_), _) => (), + (Self::Merge(self_inner), Self::Merge(base_inner)) => { + bimap_beacon_state_merge_tree_list_fields!( + self_inner, + base_inner, + |_, self_field, base_field| { self_field.rebase_on(base_field) } + ); + } + (Self::Merge(_), _) => (), + (Self::Capella(self_inner), Self::Capella(base_inner)) => { + bimap_beacon_state_capella_tree_list_fields!( + self_inner, + base_inner, + |_, self_field, base_field| { self_field.rebase_on(base_field) } + ); + } + (Self::Capella(_), _) => (), + (Self::Deneb(self_inner), Self::Deneb(base_inner)) => { + bimap_beacon_state_deneb_tree_list_fields!( + self_inner, + base_inner, + |_, self_field, base_field| { self_field.rebase_on(base_field) } + ); + } + (Self::Deneb(_), _) => (), + (Self::Electra(self_inner), Self::Electra(base_inner)) => { + bimap_beacon_state_electra_tree_list_fields!( + self_inner, + base_inner, + |_, self_field, base_field| { self_field.rebase_on(base_field) } + ); + } + (Self::Electra(_), _) => (), + } + + // Use sync committees from `base` if they are equal. + if let Ok(current_sync_committee) = self.current_sync_committee_mut() { + if let Ok(base_sync_committee) = base.current_sync_committee() { + if current_sync_committee == base_sync_committee { + *current_sync_committee = base_sync_committee.clone(); + } + } + } + if let Ok(next_sync_committee) = self.next_sync_committee_mut() { + if let Ok(base_sync_committee) = base.next_sync_committee() { + if next_sync_committee == base_sync_committee { + *next_sync_committee = base_sync_committee.clone(); + } + } + } + + // Rebase caches like the committee caches and the pubkey cache, which are expensive to + // rebuild and likely to be re-usable from the base state. + self.rebase_caches_on(base, spec)?; + + Ok(()) + } + + pub fn rebase_caches_on(&mut self, base: &Self, spec: &ChainSpec) -> Result<(), Error> { + // Use pubkey cache from `base` if it contains superior information (likely if our cache is + // uninitialized). + let num_validators = self.validators().len(); + let pubkey_cache = self.pubkey_cache_mut(); + let base_pubkey_cache = base.pubkey_cache(); + if pubkey_cache.len() < base_pubkey_cache.len() && pubkey_cache.len() < num_validators { + *pubkey_cache = base_pubkey_cache.clone(); + } + + // Use committee caches from `base` if they are relevant. + let epochs = [ + self.previous_epoch(), + self.current_epoch(), + self.next_epoch()?, + ]; + for (index, epoch) in epochs.into_iter().enumerate() { + if let Ok(base_relative_epoch) = RelativeEpoch::from_epoch(base.current_epoch(), epoch) + { + *self.committee_cache_at_index_mut(index)? = + base.committee_cache(base_relative_epoch)?.clone(); + + // Ensure total active balance cache remains built whenever current committee + // cache is built. + if epoch == self.current_epoch() { + self.build_total_active_balance_cache(spec)?; + } + } + } + + Ok(()) + } +} + +impl BeaconState { + /// The number of fields of the `BeaconState` rounded up to the nearest power of two. + /// + /// This is relevant to tree-hashing of the `BeaconState`. + /// + /// We assume this value is stable across forks. This assumption is checked in the + /// `check_num_fields_pow2` test. + pub const NUM_FIELDS_POW2: usize = BeaconStateMerge::::NUM_FIELDS.next_power_of_two(); + + /// Specialised deserialisation method that uses the `ChainSpec` as context. + #[allow(clippy::arithmetic_side_effects)] + pub fn from_ssz_bytes(bytes: &[u8], spec: &ChainSpec) -> Result { + // Slot is after genesis_time (u64) and genesis_validators_root (Hash256). + let slot_start = ::ssz_fixed_len() + ::ssz_fixed_len(); + let slot_end = slot_start + ::ssz_fixed_len(); + + let slot_bytes = bytes + .get(slot_start..slot_end) + .ok_or(DecodeError::InvalidByteLength { + len: bytes.len(), + expected: slot_end, + })?; + + let slot = Slot::from_ssz_bytes(slot_bytes)?; + let fork_at_slot = spec.fork_name_at_slot::(slot); + + Ok(map_fork_name!( + fork_at_slot, + Self, + <_>::from_ssz_bytes(bytes)? + )) + } + + #[allow(clippy::arithmetic_side_effects)] + pub fn apply_pending_mutations(&mut self) -> Result<(), Error> { + match self { + Self::Base(inner) => { + inner.previous_epoch_attestations.apply_updates()?; + inner.current_epoch_attestations.apply_updates()?; + map_beacon_state_base_tree_list_fields!(inner, |_, x| { x.apply_updates() }) + } + Self::Altair(inner) => { + map_beacon_state_altair_tree_list_fields!(inner, |_, x| { x.apply_updates() }) + } + Self::Merge(inner) => { + map_beacon_state_bellatrix_tree_list_fields!(inner, |_, x| { x.apply_updates() }) + } + Self::Capella(inner) => { + map_beacon_state_capella_tree_list_fields!(inner, |_, x| { x.apply_updates() }) + } + Self::Deneb(inner) => { + map_beacon_state_deneb_tree_list_fields!(inner, |_, x| { x.apply_updates() }) + } + Self::Electra(inner) => { + map_beacon_state_electra_tree_list_fields!(inner, |_, x| { x.apply_updates() }) + } + } + self.eth1_data_votes_mut().apply_updates()?; + Ok(()) + } + + pub fn compute_merkle_proof(&self, generalized_index: usize) -> Result, Error> { // 1. Convert generalized index to field index. let field_index = match generalized_index { light_client_update::CURRENT_SYNC_COMMITTEE_INDEX @@ -1992,7 +2207,7 @@ impl BeaconState { // in the `BeaconState`: // https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/beacon-chain.md#beaconstate generalized_index - .checked_sub(tree_hash_cache::NUM_BEACON_STATE_HASH_TREE_ROOT_LEAVES) + .checked_sub(Self::NUM_FIELDS_POW2) .ok_or(Error::IndexNotSupported(generalized_index))? } light_client_update::FINALIZED_ROOT_INDEX => { @@ -2002,20 +2217,47 @@ impl BeaconState { // Subtract off the internal nodes. Result should be 105/2 - 32 = 20 which matches // position of `finalized_checkpoint` in `BeaconState`. finalized_checkpoint_generalized_index - .checked_sub(tree_hash_cache::NUM_BEACON_STATE_HASH_TREE_ROOT_LEAVES) + .checked_sub(Self::NUM_FIELDS_POW2) .ok_or(Error::IndexNotSupported(generalized_index))? } _ => return Err(Error::IndexNotSupported(generalized_index)), }; // 2. Get all `BeaconState` leaves. - self.initialize_tree_hash_cache(); - let mut cache = self - .tree_hash_cache_mut() - .take() - .ok_or(Error::TreeHashCacheNotInitialized)?; - let leaves = cache.recalculate_tree_hash_leaves(self)?; - self.tree_hash_cache_mut().restore(cache); + let mut leaves = vec![]; + #[allow(clippy::arithmetic_side_effects)] + match self { + BeaconState::Base(state) => { + map_beacon_state_base_fields!(state, |_, field| { + leaves.push(field.tree_hash_root()); + }); + } + BeaconState::Altair(state) => { + map_beacon_state_altair_fields!(state, |_, field| { + leaves.push(field.tree_hash_root()); + }); + } + BeaconState::Merge(state) => { + map_beacon_state_bellatrix_fields!(state, |_, field| { + leaves.push(field.tree_hash_root()); + }); + } + BeaconState::Capella(state) => { + map_beacon_state_capella_fields!(state, |_, field| { + leaves.push(field.tree_hash_root()); + }); + } + BeaconState::Deneb(state) => { + map_beacon_state_deneb_fields!(state, |_, field| { + leaves.push(field.tree_hash_root()); + }); + } + BeaconState::Electra(state) => { + map_beacon_state_electra_fields!(state, |_, field| { + leaves.push(field.tree_hash_root()); + }); + } + }; // 3. Make deposit tree. // Use the depth of the `BeaconState` fields (i.e. `log2(32) = 5`). @@ -2074,9 +2316,10 @@ impl From for Error { } } -/// Helper function for "cloning" a field by using its default value. -fn clone_default(_value: &T) -> T { - T::default() +impl From for Error { + fn from(e: milhouse::Error) -> Self { + Self::MilhouseError(e) + } } impl CompareFields for BeaconState { diff --git a/consensus/types/src/beacon_state/clone_config.rs b/consensus/types/src/beacon_state/clone_config.rs deleted file mode 100644 index 27e066d5db6..00000000000 --- a/consensus/types/src/beacon_state/clone_config.rs +++ /dev/null @@ -1,47 +0,0 @@ -/// Configuration struct for controlling which caches of a `BeaconState` should be cloned. -#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] -pub struct CloneConfig { - pub committee_caches: bool, - pub pubkey_cache: bool, - pub exit_cache: bool, - pub slashings_cache: bool, - pub tree_hash_cache: bool, - pub progressive_balances_cache: bool, -} - -impl CloneConfig { - pub fn all() -> Self { - Self { - committee_caches: true, - pubkey_cache: true, - exit_cache: true, - slashings_cache: true, - tree_hash_cache: true, - progressive_balances_cache: true, - } - } - - pub fn none() -> Self { - Self::default() - } - - pub fn committee_caches_only() -> Self { - Self { - committee_caches: true, - ..Self::none() - } - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn sanity() { - assert!(CloneConfig::all().pubkey_cache); - assert!(!CloneConfig::none().tree_hash_cache); - assert!(CloneConfig::committee_caches_only().committee_caches); - assert!(!CloneConfig::committee_caches_only().exit_cache); - } -} diff --git a/consensus/types/src/beacon_state/committee_cache.rs b/consensus/types/src/beacon_state/committee_cache.rs index a6b12cf5af3..7913df8e00e 100644 --- a/consensus/types/src/beacon_state/committee_cache.rs +++ b/consensus/types/src/beacon_state/committee_cache.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; use ssz::{four_byte_option_impl, Decode, DecodeError, Encode}; use ssz_derive::{Decode, Encode}; use std::ops::Range; +use std::sync::Arc; use swap_or_not_shuffle::shuffle_list; mod tests; @@ -65,7 +66,7 @@ impl CommitteeCache { state: &BeaconState, epoch: Epoch, spec: &ChainSpec, - ) -> Result { + ) -> Result, Error> { // Check that the cache is being built for an in-range epoch. // // We allow caches to be constructed for historic epochs, per: @@ -115,13 +116,13 @@ impl CommitteeCache { .ok_or(Error::ShuffleIndexOutOfBounds(v))? = NonZeroUsize::new(i + 1).into(); } - Ok(CommitteeCache { + Ok(Arc::new(CommitteeCache { initialized_epoch: Some(epoch), shuffling, shuffling_positions, committees_per_slot, slots_per_epoch: E::slots_per_epoch(), - }) + })) } /// Returns `true` if the cache has been initialized at the supplied `epoch`. diff --git a/consensus/types/src/beacon_state/committee_cache/tests.rs b/consensus/types/src/beacon_state/committee_cache/tests.rs index a5effb9363b..a2274765691 100644 --- a/consensus/types/src/beacon_state/committee_cache/tests.rs +++ b/consensus/types/src/beacon_state/committee_cache/tests.rs @@ -92,7 +92,7 @@ async fn shuffles_for_the_right_epoch() { .map(|i| Hash256::from_low_u64_be(i as u64)) .collect(); - *state.randao_mixes_mut() = FixedVector::from(distinct_hashes); + *state.randao_mixes_mut() = Vector::try_from_iter(distinct_hashes).unwrap(); let previous_seed = state .get_seed(state.previous_epoch(), Domain::BeaconAttester, spec) diff --git a/consensus/types/src/beacon_state/compact_state.rs b/consensus/types/src/beacon_state/compact_state.rs new file mode 100644 index 00000000000..3f8f47c8541 --- /dev/null +++ b/consensus/types/src/beacon_state/compact_state.rs @@ -0,0 +1,316 @@ +use crate::{ + BeaconState, BeaconStateAltair, BeaconStateBase, BeaconStateCapella, BeaconStateDeneb, + BeaconStateElectra, BeaconStateError as Error, BeaconStateMerge, EthSpec, List, PublicKeyBytes, + Validator, ValidatorMutable, +}; +use itertools::process_results; +use std::sync::Arc; + +pub type CompactBeaconState = BeaconState; + +/// Implement the conversion function from BeaconState -> CompactBeaconState. +macro_rules! full_to_compact { + ($s:ident, $outer:ident, $variant_name:ident, $struct_name:ident, [$($extra_fields:ident),*]) => { + BeaconState::$variant_name($struct_name { + // Versioning + genesis_time: $s.genesis_time, + genesis_validators_root: $s.genesis_validators_root, + slot: $s.slot, + fork: $s.fork, + + // History + latest_block_header: $s.latest_block_header.clone(), + block_roots: $s.block_roots.clone(), + state_roots: $s.state_roots.clone(), + historical_roots: $s.historical_roots.clone(), + + // Eth1 + eth1_data: $s.eth1_data.clone(), + eth1_data_votes: $s.eth1_data_votes.clone(), + eth1_deposit_index: $s.eth1_deposit_index, + + // Validator registry + validators: List::try_from_iter( + $s.validators.into_iter().map(|validator| validator.mutable.clone()) + ).expect("fix this"), + balances: $s.balances.clone(), + + // Shuffling + randao_mixes: $s.randao_mixes.clone(), + + // Slashings + slashings: $s.slashings.clone(), + + // Finality + justification_bits: $s.justification_bits.clone(), + previous_justified_checkpoint: $s.previous_justified_checkpoint, + current_justified_checkpoint: $s.current_justified_checkpoint, + finalized_checkpoint: $s.finalized_checkpoint, + + // Caches. + total_active_balance: $s.total_active_balance.clone(), + committee_caches: $s.committee_caches.clone(), + progressive_balances_cache: $s.progressive_balances_cache.clone(), + pubkey_cache: $s.pubkey_cache.clone(), + exit_cache: $s.exit_cache.clone(), + slashings_cache: $s.slashings_cache.clone(), + epoch_cache: $s.epoch_cache.clone(), + + // Variant-specific fields + $( + $extra_fields: $s.$extra_fields.clone() + ),* + }) + } +} + +/// Implement the conversion from CompactBeaconState -> BeaconState. +macro_rules! compact_to_full { + ($inner:ident, $variant_name:ident, $struct_name:ident, $immutable_validators:ident, [$($extra_fields:ident),*]) => { + BeaconState::$variant_name($struct_name { + // Versioning + genesis_time: $inner.genesis_time, + genesis_validators_root: $inner.genesis_validators_root, + slot: $inner.slot, + fork: $inner.fork, + + // History + latest_block_header: $inner.latest_block_header, + block_roots: $inner.block_roots, + state_roots: $inner.state_roots, + historical_roots: $inner.historical_roots, + + // Eth1 + eth1_data: $inner.eth1_data, + eth1_data_votes: $inner.eth1_data_votes, + eth1_deposit_index: $inner.eth1_deposit_index, + + // Validator registry + validators: process_results($inner.validators.into_iter().enumerate().map(|(i, mutable)| { + $immutable_validators(i) + .ok_or(Error::MissingImmutableValidator(i)) + .map(move |pubkey| { + Validator { + pubkey, + mutable: mutable.clone(), + } + }) + }), |iter| List::try_from_iter(iter))??, + balances: $inner.balances, + + // Shuffling + randao_mixes: $inner.randao_mixes, + + // Slashings + slashings: $inner.slashings, + + // Finality + justification_bits: $inner.justification_bits, + previous_justified_checkpoint: $inner.previous_justified_checkpoint, + current_justified_checkpoint: $inner.current_justified_checkpoint, + finalized_checkpoint: $inner.finalized_checkpoint, + + // Caching + total_active_balance: $inner.total_active_balance, + committee_caches: $inner.committee_caches, + progressive_balances_cache: $inner.progressive_balances_cache, + pubkey_cache: $inner.pubkey_cache, + exit_cache: $inner.exit_cache, + slashings_cache: $inner.slashings_cache, + epoch_cache: $inner.epoch_cache, + + // Variant-specific fields + $( + $extra_fields: $inner.$extra_fields + ),* + }) + } +} + +impl BeaconState { + pub fn into_compact_state(self) -> CompactBeaconState { + match self { + BeaconState::Base(s) => full_to_compact!( + s, + self, + Base, + BeaconStateBase, + [previous_epoch_attestations, current_epoch_attestations] + ), + BeaconState::Altair(s) => full_to_compact!( + s, + self, + Altair, + BeaconStateAltair, + [ + previous_epoch_participation, + current_epoch_participation, + current_sync_committee, + next_sync_committee, + inactivity_scores + ] + ), + BeaconState::Merge(s) => full_to_compact!( + s, + self, + Merge, + BeaconStateMerge, + [ + previous_epoch_participation, + current_epoch_participation, + current_sync_committee, + next_sync_committee, + inactivity_scores, + latest_execution_payload_header + ] + ), + BeaconState::Capella(s) => full_to_compact!( + s, + self, + Capella, + BeaconStateCapella, + [ + previous_epoch_participation, + current_epoch_participation, + current_sync_committee, + next_sync_committee, + inactivity_scores, + latest_execution_payload_header, + historical_summaries, + next_withdrawal_index, + next_withdrawal_validator_index + ] + ), + BeaconState::Deneb(s) => full_to_compact!( + s, + self, + Deneb, + BeaconStateDeneb, + [ + previous_epoch_participation, + current_epoch_participation, + current_sync_committee, + next_sync_committee, + inactivity_scores, + latest_execution_payload_header, + historical_summaries, + next_withdrawal_index, + next_withdrawal_validator_index + ] + ), + BeaconState::Electra(s) => full_to_compact!( + s, + self, + Electra, + BeaconStateElectra, + [ + previous_epoch_participation, + current_epoch_participation, + current_sync_committee, + next_sync_committee, + inactivity_scores, + latest_execution_payload_header, + historical_summaries, + next_withdrawal_index, + next_withdrawal_validator_index + ] + ), + } + } +} + +impl CompactBeaconState { + pub fn try_into_full_state(self, immutable_validators: F) -> Result, Error> + where + F: Fn(usize) -> Option>, + { + let state = match self { + BeaconState::Base(inner) => compact_to_full!( + inner, + Base, + BeaconStateBase, + immutable_validators, + [previous_epoch_attestations, current_epoch_attestations] + ), + BeaconState::Altair(inner) => compact_to_full!( + inner, + Altair, + BeaconStateAltair, + immutable_validators, + [ + previous_epoch_participation, + current_epoch_participation, + current_sync_committee, + next_sync_committee, + inactivity_scores + ] + ), + BeaconState::Merge(inner) => compact_to_full!( + inner, + Merge, + BeaconStateMerge, + immutable_validators, + [ + previous_epoch_participation, + current_epoch_participation, + current_sync_committee, + next_sync_committee, + inactivity_scores, + latest_execution_payload_header + ] + ), + BeaconState::Capella(inner) => compact_to_full!( + inner, + Capella, + BeaconStateCapella, + immutable_validators, + [ + previous_epoch_participation, + current_epoch_participation, + current_sync_committee, + next_sync_committee, + inactivity_scores, + latest_execution_payload_header, + historical_summaries, + next_withdrawal_index, + next_withdrawal_validator_index + ] + ), + BeaconState::Deneb(inner) => compact_to_full!( + inner, + Deneb, + BeaconStateDeneb, + immutable_validators, + [ + previous_epoch_participation, + current_epoch_participation, + current_sync_committee, + next_sync_committee, + inactivity_scores, + latest_execution_payload_header, + historical_summaries, + next_withdrawal_index, + next_withdrawal_validator_index + ] + ), + BeaconState::Electra(inner) => compact_to_full!( + inner, + Electra, + BeaconStateElectra, + immutable_validators, + [ + previous_epoch_participation, + current_epoch_participation, + current_sync_committee, + next_sync_committee, + inactivity_scores, + latest_execution_payload_header, + historical_summaries, + next_withdrawal_index, + next_withdrawal_validator_index + ] + ), + }; + Ok(state) + } +} diff --git a/consensus/types/src/beacon_state/exit_cache.rs b/consensus/types/src/beacon_state/exit_cache.rs index bda788e63b9..1a570549957 100644 --- a/consensus/types/src/beacon_state/exit_cache.rs +++ b/consensus/types/src/beacon_state/exit_cache.rs @@ -1,10 +1,9 @@ use super::{BeaconStateError, ChainSpec, Epoch, Validator}; use safe_arith::SafeArith; -use serde::{Deserialize, Serialize}; use std::cmp::Ordering; /// Map from exit epoch to the number of validators with that exit epoch. -#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, PartialEq)] pub struct ExitCache { /// True if the cache has been initialized. initialized: bool, @@ -16,7 +15,11 @@ pub struct ExitCache { impl ExitCache { /// Initialize a new cache for the given list of validators. - pub fn new(validators: &[Validator], spec: &ChainSpec) -> Result { + pub fn new<'a, V, I>(validators: V, spec: &ChainSpec) -> Result + where + V: IntoIterator, + I: ExactSizeIterator + Iterator, + { let mut exit_cache = ExitCache { initialized: true, max_exit_epoch: Epoch::new(0), @@ -24,9 +27,9 @@ impl ExitCache { }; // Add all validators with a non-default exit epoch to the cache. validators - .iter() - .filter(|validator| validator.exit_epoch != spec.far_future_epoch) - .try_for_each(|validator| exit_cache.record_validator_exit(validator.exit_epoch))?; + .into_iter() + .filter(|validator| validator.exit_epoch() != spec.far_future_epoch) + .try_for_each(|validator| exit_cache.record_validator_exit(validator.exit_epoch()))?; Ok(exit_cache) } diff --git a/consensus/types/src/beacon_state/iter.rs b/consensus/types/src/beacon_state/iter.rs index 2d3ad02c836..2caa0365e01 100644 --- a/consensus/types/src/beacon_state/iter.rs +++ b/consensus/types/src/beacon_state/iter.rs @@ -74,7 +74,7 @@ mod test { let mut state: BeaconState = BeaconState::new(0, <_>::default(), &spec); for i in 0..state.block_roots().len() { - state.block_roots_mut()[i] = root_slot(i).1; + *state.block_roots_mut().get_mut(i).unwrap() = root_slot(i).1; } assert_eq!( @@ -122,7 +122,7 @@ mod test { let mut state: BeaconState = BeaconState::new(0, <_>::default(), &spec); for i in 0..state.block_roots().len() { - state.block_roots_mut()[i] = root_slot(i).1; + *state.block_roots_mut().get_mut(i).unwrap() = root_slot(i).1; } assert_eq!( diff --git a/consensus/types/src/beacon_state/pubkey_cache.rs b/consensus/types/src/beacon_state/pubkey_cache.rs index 0b61ea3c5f8..d58dd7bc1dd 100644 --- a/consensus/types/src/beacon_state/pubkey_cache.rs +++ b/consensus/types/src/beacon_state/pubkey_cache.rs @@ -1,21 +1,21 @@ use crate::*; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use rpds::HashTrieMapSync as HashTrieMap; type ValidatorIndex = usize; #[allow(clippy::len_without_is_empty)] -#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Clone, Default)] pub struct PubkeyCache { - /// Maintain the number of keys added to the map. It is not sufficient to just use the HashMap - /// len, as it does not increase when duplicate keys are added. Duplicate keys are used during - /// testing. + /// Maintain the number of keys added to the map. It is not sufficient to just use the + /// HashTrieMap len, as it does not increase when duplicate keys are added. Duplicate keys are + /// used during testing. len: usize, - map: HashMap, + map: HashTrieMap, } impl PubkeyCache { /// Returns the number of validator indices added to the map so far. + #[allow(clippy::len_without_is_empty)] pub fn len(&self) -> ValidatorIndex { self.len } @@ -26,7 +26,7 @@ impl PubkeyCache { /// that an index is never skipped. pub fn insert(&mut self, pubkey: PublicKeyBytes, index: ValidatorIndex) -> bool { if index == self.len { - self.map.insert(pubkey, index); + self.map.insert_mut(pubkey, index); self.len = self .len .checked_add(1) diff --git a/consensus/types/src/beacon_state/slashings_cache.rs b/consensus/types/src/beacon_state/slashings_cache.rs index cfdc349f86c..19813ebbfe1 100644 --- a/consensus/types/src/beacon_state/slashings_cache.rs +++ b/consensus/types/src/beacon_state/slashings_cache.rs @@ -1,13 +1,13 @@ use crate::{BeaconStateError, Slot, Validator}; use arbitrary::Arbitrary; -use std::collections::HashSet; +use rpds::HashTrieSetSync as HashTrieSet; /// Persistent (cheap to clone) cache of all slashed validator indices. #[derive(Debug, Default, Clone, PartialEq, Arbitrary)] pub struct SlashingsCache { latest_block_slot: Option, #[arbitrary(default)] - slashed_validators: HashSet, + slashed_validators: HashTrieSet, } impl SlashingsCache { @@ -20,7 +20,7 @@ impl SlashingsCache { let slashed_validators = validators .into_iter() .enumerate() - .filter_map(|(i, validator)| validator.slashed.then_some(i)) + .filter_map(|(i, validator)| validator.slashed().then_some(i)) .collect(); Self { latest_block_slot: Some(latest_block_slot), @@ -49,7 +49,7 @@ impl SlashingsCache { validator_index: usize, ) -> Result<(), BeaconStateError> { self.check_initialized(block_slot)?; - self.slashed_validators.insert(validator_index); + self.slashed_validators.insert_mut(validator_index); Ok(()) } diff --git a/consensus/types/src/beacon_state/tests.rs b/consensus/types/src/beacon_state/tests.rs index 00625a1788e..226eb9099a0 100644 --- a/consensus/types/src/beacon_state/tests.rs +++ b/consensus/types/src/beacon_state/tests.rs @@ -1,20 +1,14 @@ #![cfg(test)] -use crate::test_utils::*; -use beacon_chain::test_utils::{ - interop_genesis_state_with_eth1, test_spec, BeaconChainHarness, EphemeralHarnessType, - DEFAULT_ETH1_BLOCK_HASH, -}; +use crate::{test_utils::*, ForkName}; +use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; use beacon_chain::types::{ - test_utils::TestRandom, BeaconState, BeaconStateAltair, BeaconStateBase, BeaconStateError, - ChainSpec, CloneConfig, Domain, Epoch, EthSpec, FixedVector, Hash256, Keypair, MainnetEthSpec, - MinimalEthSpec, RelativeEpoch, Slot, + test_utils::TestRandom, BeaconState, BeaconStateAltair, BeaconStateBase, BeaconStateCapella, + BeaconStateDeneb, BeaconStateElectra, BeaconStateError, BeaconStateMerge, ChainSpec, Domain, + Epoch, EthSpec, Hash256, Keypair, MainnetEthSpec, MinimalEthSpec, RelativeEpoch, Slot, Vector, }; -use safe_arith::SafeArith; use ssz::Encode; -use state_processing::per_slot_processing; use std::ops::Mul; use swap_or_not_shuffle::compute_shuffled_index; -use tree_hash::TreeHash; pub const MAX_VALIDATOR_COUNT: usize = 129; pub const SLOT_OFFSET: Slot = Slot::new(1); @@ -101,7 +95,12 @@ async fn test_beacon_proposer_index() { // Test with two validators per slot, first validator has zero balance. let mut state = build_state::((E::slots_per_epoch() as usize).mul(2)).await; let slot0_candidate0 = ith_candidate(&state, Slot::new(0), 0, &spec); - state.validators_mut()[slot0_candidate0].effective_balance = 0; + state + .validators_mut() + .get_mut(slot0_candidate0) + .unwrap() + .mutable + .effective_balance = 0; test(&state, Slot::new(0), 1); for i in 1..E::slots_per_epoch() { test(&state, Slot::from(i), 0); @@ -159,85 +158,6 @@ async fn cache_initialization() { test_cache_initialization(&mut state, RelativeEpoch::Next, &spec); } -fn test_clone_config(base_state: &BeaconState, clone_config: CloneConfig) { - let state = base_state.clone_with(clone_config); - if clone_config.committee_caches { - state - .committee_cache(RelativeEpoch::Previous) - .expect("committee cache exists"); - state - .committee_cache(RelativeEpoch::Current) - .expect("committee cache exists"); - state - .committee_cache(RelativeEpoch::Next) - .expect("committee cache exists"); - state - .total_active_balance() - .expect("total active balance exists"); - } else { - state - .committee_cache(RelativeEpoch::Previous) - .expect_err("shouldn't exist"); - state - .committee_cache(RelativeEpoch::Current) - .expect_err("shouldn't exist"); - state - .committee_cache(RelativeEpoch::Next) - .expect_err("shouldn't exist"); - } - if clone_config.pubkey_cache { - assert_ne!(state.pubkey_cache().len(), 0); - } else { - assert_eq!(state.pubkey_cache().len(), 0); - } - if clone_config.exit_cache { - state - .exit_cache() - .check_initialized() - .expect("exit cache exists"); - } else { - state - .exit_cache() - .check_initialized() - .expect_err("exit cache doesn't exist"); - } - if clone_config.tree_hash_cache { - assert!(state.tree_hash_cache().is_initialized()); - } else { - assert!( - !state.tree_hash_cache().is_initialized(), - "{:?}", - clone_config - ); - } -} - -#[tokio::test] -async fn clone_config() { - let spec = MinimalEthSpec::default_spec(); - - let mut state = build_state::(16).await; - - state.build_caches(&spec).unwrap(); - state - .update_tree_hash_cache() - .expect("should update tree hash cache"); - - let num_caches = 6; - let all_configs = (0..2u8.pow(num_caches)).map(|i| CloneConfig { - committee_caches: (i & 1) != 0, - pubkey_cache: ((i >> 1) & 1) != 0, - exit_cache: ((i >> 2) & 1) != 0, - slashings_cache: ((i >> 3) & 1) != 0, - tree_hash_cache: ((i >> 4) & 1) != 0, - progressive_balances_cache: ((i >> 5) & 1) != 0, - }); - - for config in all_configs { - test_clone_config(&state, config); - } -} - /// Tests committee-specific components #[cfg(test)] mod committees { @@ -328,10 +248,9 @@ mod committees { let harness = get_harness::(validator_count, slot).await; let mut new_head_state = harness.get_current_state(); - let distinct_hashes: Vec = (0..E::epochs_per_historical_vector()) - .map(|i| Hash256::from_low_u64_be(i as u64)) - .collect(); - *new_head_state.randao_mixes_mut() = FixedVector::from(distinct_hashes); + let distinct_hashes = + (0..E::epochs_per_historical_vector()).map(|i| Hash256::from_low_u64_be(i as u64)); + *new_head_state.randao_mixes_mut() = Vector::try_from_iter(distinct_hashes).unwrap(); new_head_state .force_build_committee_cache(RelativeEpoch::Previous, spec) @@ -487,120 +406,22 @@ fn decode_base_and_altair() { } #[test] -fn tree_hash_cache_linear_history() { - let mut rng = XorShiftRng::from_seed([42; 16]); - - let mut state: BeaconState = - BeaconState::Base(BeaconStateBase::random_for_test(&mut rng)); - - let root = state.update_tree_hash_cache().unwrap(); - - assert_eq!(root.as_bytes(), &state.tree_hash_root()[..]); - - /* - * A cache should hash twice without updating the slot. - */ - - assert_eq!( - state.update_tree_hash_cache().unwrap(), - root, - "tree hash result should be identical on the same slot" - ); - - /* - * A cache should not hash after updating the slot but not updating the state roots. - */ - - // The tree hash cache needs to be rebuilt since it was dropped when it failed. - state - .update_tree_hash_cache() - .expect("should rebuild cache"); - - *state.slot_mut() += 1; - - assert_eq!( - state.update_tree_hash_cache(), - Err(BeaconStateError::NonLinearTreeHashCacheHistory), - "should not build hash without updating the state root" - ); - - /* - * The cache should update if the slot and state root are updated. - */ - - // The tree hash cache needs to be rebuilt since it was dropped when it failed. - let root = state - .update_tree_hash_cache() - .expect("should rebuild cache"); - - *state.slot_mut() += 1; - state - .set_state_root(state.slot() - 1, root) - .expect("should set state root"); - - let root = state.update_tree_hash_cache().unwrap(); - assert_eq!(root.as_bytes(), &state.tree_hash_root()[..]); -} - -// Check how the cache behaves when there's a distance larger than `SLOTS_PER_HISTORICAL_ROOT` -// since its last update. -#[test] -fn tree_hash_cache_linear_history_long_skip() { - let validator_count = 128; - let keypairs = generate_deterministic_keypairs(validator_count); - - let spec = &test_spec::(); - - // This state has a cache that advances normally each slot. - let mut state: BeaconState = interop_genesis_state_with_eth1( - &keypairs, - 0, - Hash256::from_slice(DEFAULT_ETH1_BLOCK_HASH), - None, - spec, - ) - .unwrap(); - - state.update_tree_hash_cache().unwrap(); - - // This state retains its original cache until it is updated after a long skip. - let mut original_cache_state = state.clone(); - assert!(original_cache_state.tree_hash_cache().is_initialized()); - - // Advance the states to a slot beyond the historical state root limit, using the state root - // from the first state to avoid touching the original state's cache. - let start_slot = state.slot(); - let target_slot = start_slot - .safe_add(MinimalEthSpec::slots_per_historical_root() as u64 + 1) - .unwrap(); - - let mut prev_state_root; - while state.slot() < target_slot { - prev_state_root = state.update_tree_hash_cache().unwrap(); - per_slot_processing(&mut state, None, spec).unwrap(); - per_slot_processing(&mut original_cache_state, Some(prev_state_root), spec).unwrap(); +fn check_num_fields_pow2() { + use metastruct::NumFields; + pub type E = MainnetEthSpec; + + for fork_name in ForkName::list_all() { + let num_fields = match fork_name { + ForkName::Base => BeaconStateBase::::NUM_FIELDS, + ForkName::Altair => BeaconStateAltair::::NUM_FIELDS, + ForkName::Merge => BeaconStateMerge::::NUM_FIELDS, + ForkName::Capella => BeaconStateCapella::::NUM_FIELDS, + ForkName::Deneb => BeaconStateDeneb::::NUM_FIELDS, + ForkName::Electra => BeaconStateElectra::::NUM_FIELDS, + }; + assert_eq!( + num_fields.next_power_of_two(), + BeaconState::::NUM_FIELDS_POW2 + ); } - - // The state with the original cache should still be initialized at the starting slot. - assert_eq!( - original_cache_state - .tree_hash_cache() - .initialized_slot() - .unwrap(), - start_slot - ); - - // Updating the tree hash cache should be successful despite the long skip. - assert_eq!( - original_cache_state.update_tree_hash_cache().unwrap(), - state.update_tree_hash_cache().unwrap() - ); - - assert_eq!( - original_cache_state - .tree_hash_cache() - .initialized_slot() - .unwrap(), - target_slot - ); } diff --git a/consensus/types/src/beacon_state/tree_hash_cache.rs b/consensus/types/src/beacon_state/tree_hash_cache.rs deleted file mode 100644 index 290020b1b35..00000000000 --- a/consensus/types/src/beacon_state/tree_hash_cache.rs +++ /dev/null @@ -1,645 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![allow(clippy::disallowed_methods)] -#![allow(clippy::indexing_slicing)] - -use super::Error; -use crate::historical_summary::HistoricalSummaryCache; -use crate::{BeaconState, EthSpec, Hash256, ParticipationList, Slot, Unsigned, Validator}; -use cached_tree_hash::{int_log, CacheArena, CachedTreeHash, TreeHashCache}; -use rayon::prelude::*; -use ssz_derive::{Decode, Encode}; -use ssz_types::VariableList; -use std::cmp::Ordering; -use tree_hash::{mix_in_length, MerkleHasher, TreeHash}; - -/// The number of leaves (including padding) on the `BeaconState` Merkle tree. -/// -/// ## Note -/// -/// This constant is set with the assumption that there are `> 16` and `<= 32` fields on the -/// `BeaconState`. **Tree hashing will fail if this value is set incorrectly.** -pub const NUM_BEACON_STATE_HASH_TREE_ROOT_LEAVES: usize = 32; - -/// The number of nodes in the Merkle tree of a validator record. -const NODES_PER_VALIDATOR: usize = 15; - -/// The number of validator record tree hash caches stored in each arena. -/// -/// This is primarily used for concurrency; if we have 16 validators and set `VALIDATORS_PER_ARENA -/// == 8` then it is possible to do a 2-core concurrent hash. -/// -/// Do not set to 0. -const VALIDATORS_PER_ARENA: usize = 4_096; - -#[derive(Debug, PartialEq, Clone, Encode, Decode)] -pub struct Eth1DataVotesTreeHashCache { - arena: CacheArena, - tree_hash_cache: TreeHashCache, - voting_period: u64, - roots: VariableList, -} - -impl Eth1DataVotesTreeHashCache { - /// Instantiates a new cache. - /// - /// Allocates the necessary memory to store all of the cached Merkle trees. Only the leaves are - /// hashed, leaving the internal nodes as all-zeros. - pub fn new(state: &BeaconState) -> Self { - let mut arena = CacheArena::default(); - let roots: VariableList<_, _> = state - .eth1_data_votes() - .iter() - .map(|eth1_data| eth1_data.tree_hash_root()) - .collect::>() - .into(); - let tree_hash_cache = roots.new_tree_hash_cache(&mut arena); - - Self { - arena, - tree_hash_cache, - voting_period: Self::voting_period(state.slot()), - roots, - } - } - - fn voting_period(slot: Slot) -> u64 { - slot.as_u64() / E::SlotsPerEth1VotingPeriod::to_u64() - } - - pub fn recalculate_tree_hash_root(&mut self, state: &BeaconState) -> Result { - if state.eth1_data_votes().len() < self.roots.len() - || Self::voting_period(state.slot()) != self.voting_period - { - *self = Self::new(state); - } - - state - .eth1_data_votes() - .iter() - .skip(self.roots.len()) - .try_for_each(|eth1_data| self.roots.push(eth1_data.tree_hash_root()))?; - - self.roots - .recalculate_tree_hash_root(&mut self.arena, &mut self.tree_hash_cache) - .map_err(Into::into) - } -} - -/// A cache that performs a caching tree hash of the entire `BeaconState` struct. -/// -/// This type is a wrapper around the inner cache, which does all the work. -#[derive(Debug, Default, PartialEq, Clone)] -pub struct BeaconTreeHashCache { - inner: Option>, -} - -impl BeaconTreeHashCache { - pub fn new(state: &BeaconState) -> Self { - Self { - inner: Some(BeaconTreeHashCacheInner::new(state)), - } - } - - pub fn is_initialized(&self) -> bool { - self.inner.is_some() - } - - /// Move the inner cache out so that the containing `BeaconState` can be borrowed. - pub fn take(&mut self) -> Option> { - self.inner.take() - } - - /// Restore the inner cache after using `take`. - pub fn restore(&mut self, inner: BeaconTreeHashCacheInner) { - self.inner = Some(inner); - } - - /// Make the cache empty. - pub fn uninitialize(&mut self) { - self.inner = None; - } - - /// Return the slot at which the cache was last updated. - /// - /// This should probably only be used during testing. - pub fn initialized_slot(&self) -> Option { - Some(self.inner.as_ref()?.previous_state?.1) - } -} - -#[derive(Debug, PartialEq, Clone)] -pub struct BeaconTreeHashCacheInner { - /// Tracks the previously generated state root to ensure the next state root provided descends - /// directly from this state. - previous_state: Option<(Hash256, Slot)>, - // Validators cache - validators: ValidatorsListTreeHashCache, - // Arenas - fixed_arena: CacheArena, - balances_arena: CacheArena, - slashings_arena: CacheArena, - // Caches - block_roots: TreeHashCache, - state_roots: TreeHashCache, - historical_roots: TreeHashCache, - historical_summaries: OptionalTreeHashCache, - balances: TreeHashCache, - randao_mixes: TreeHashCache, - slashings: TreeHashCache, - eth1_data_votes: Eth1DataVotesTreeHashCache, - inactivity_scores: OptionalTreeHashCache, - // Participation caches - previous_epoch_participation: OptionalTreeHashCache, - current_epoch_participation: OptionalTreeHashCache, -} - -impl BeaconTreeHashCacheInner { - /// Instantiates a new cache. - /// - /// Allocates the necessary memory to store all of the cached Merkle trees. Only the leaves are - /// hashed, leaving the internal nodes as all-zeros. - pub fn new(state: &BeaconState) -> Self { - let mut fixed_arena = CacheArena::default(); - let block_roots = state.block_roots().new_tree_hash_cache(&mut fixed_arena); - let state_roots = state.state_roots().new_tree_hash_cache(&mut fixed_arena); - let historical_roots = state - .historical_roots() - .new_tree_hash_cache(&mut fixed_arena); - let historical_summaries = OptionalTreeHashCache::new( - state - .historical_summaries() - .ok() - .map(HistoricalSummaryCache::new) - .as_ref(), - ); - - let randao_mixes = state.randao_mixes().new_tree_hash_cache(&mut fixed_arena); - - let validators = ValidatorsListTreeHashCache::new::(state.validators()); - - let mut balances_arena = CacheArena::default(); - let balances = state.balances().new_tree_hash_cache(&mut balances_arena); - - let mut slashings_arena = CacheArena::default(); - let slashings = state.slashings().new_tree_hash_cache(&mut slashings_arena); - - let inactivity_scores = OptionalTreeHashCache::new(state.inactivity_scores().ok()); - - let previous_epoch_participation = OptionalTreeHashCache::new( - state - .previous_epoch_participation() - .ok() - .map(ParticipationList::new) - .as_ref(), - ); - let current_epoch_participation = OptionalTreeHashCache::new( - state - .current_epoch_participation() - .ok() - .map(ParticipationList::new) - .as_ref(), - ); - - Self { - previous_state: None, - validators, - fixed_arena, - balances_arena, - slashings_arena, - block_roots, - state_roots, - historical_roots, - historical_summaries, - balances, - randao_mixes, - slashings, - inactivity_scores, - eth1_data_votes: Eth1DataVotesTreeHashCache::new(state), - previous_epoch_participation, - current_epoch_participation, - } - } - - pub fn recalculate_tree_hash_leaves( - &mut self, - state: &BeaconState, - ) -> Result, Error> { - let mut leaves = vec![ - // Genesis data leaves. - state.genesis_time().tree_hash_root(), - state.genesis_validators_root().tree_hash_root(), - // Current fork data leaves. - state.slot().tree_hash_root(), - state.fork().tree_hash_root(), - state.latest_block_header().tree_hash_root(), - // Roots leaves. - state - .block_roots() - .recalculate_tree_hash_root(&mut self.fixed_arena, &mut self.block_roots)?, - state - .state_roots() - .recalculate_tree_hash_root(&mut self.fixed_arena, &mut self.state_roots)?, - state - .historical_roots() - .recalculate_tree_hash_root(&mut self.fixed_arena, &mut self.historical_roots)?, - // Eth1 Data leaves. - state.eth1_data().tree_hash_root(), - self.eth1_data_votes.recalculate_tree_hash_root(state)?, - state.eth1_deposit_index().tree_hash_root(), - // Validator leaves. - self.validators - .recalculate_tree_hash_root(state.validators())?, - state - .balances() - .recalculate_tree_hash_root(&mut self.balances_arena, &mut self.balances)?, - state - .randao_mixes() - .recalculate_tree_hash_root(&mut self.fixed_arena, &mut self.randao_mixes)?, - state - .slashings() - .recalculate_tree_hash_root(&mut self.slashings_arena, &mut self.slashings)?, - ]; - - // Participation - if let BeaconState::Base(state) = state { - leaves.push(state.previous_epoch_attestations.tree_hash_root()); - leaves.push(state.current_epoch_attestations.tree_hash_root()); - } else { - leaves.push( - self.previous_epoch_participation - .recalculate_tree_hash_root(&ParticipationList::new( - state.previous_epoch_participation()?, - ))?, - ); - leaves.push( - self.current_epoch_participation - .recalculate_tree_hash_root(&ParticipationList::new( - state.current_epoch_participation()?, - ))?, - ); - } - // Checkpoint leaves - leaves.push(state.justification_bits().tree_hash_root()); - leaves.push(state.previous_justified_checkpoint().tree_hash_root()); - leaves.push(state.current_justified_checkpoint().tree_hash_root()); - leaves.push(state.finalized_checkpoint().tree_hash_root()); - // Inactivity & light-client sync committees (Altair and later). - if let Ok(inactivity_scores) = state.inactivity_scores() { - leaves.push( - self.inactivity_scores - .recalculate_tree_hash_root(inactivity_scores)?, - ); - } - if let Ok(current_sync_committee) = state.current_sync_committee() { - leaves.push(current_sync_committee.tree_hash_root()); - } - - if let Ok(next_sync_committee) = state.next_sync_committee() { - leaves.push(next_sync_committee.tree_hash_root()); - } - - // Execution payload (merge and later). - if let Ok(payload_header) = state.latest_execution_payload_header() { - leaves.push(payload_header.tree_hash_root()); - } - - // Withdrawal indices (Capella and later). - if let Ok(next_withdrawal_index) = state.next_withdrawal_index() { - leaves.push(next_withdrawal_index.tree_hash_root()); - } - if let Ok(next_withdrawal_validator_index) = state.next_withdrawal_validator_index() { - leaves.push(next_withdrawal_validator_index.tree_hash_root()); - } - - // Historical roots/summaries (Capella and later). - if let Ok(historical_summaries) = state.historical_summaries() { - leaves.push( - self.historical_summaries.recalculate_tree_hash_root( - &HistoricalSummaryCache::new(historical_summaries), - )?, - ); - } - - Ok(leaves) - } - - /// Updates the cache and returns the tree hash root for the given `state`. - /// - /// The provided `state` should be a descendant of the last `state` given to this function, or - /// the `Self::new` function. If the state is more than `SLOTS_PER_HISTORICAL_ROOT` slots - /// after `self.previous_state` then the whole cache will be re-initialized. - pub fn recalculate_tree_hash_root(&mut self, state: &BeaconState) -> Result { - // If this cache has previously produced a root, ensure that it is in the state root - // history of this state. - // - // This ensures that the states applied have a linear history, this - // allows us to make assumptions about how the state changes over times and produce a more - // efficient algorithm. - if let Some((previous_root, previous_slot)) = self.previous_state { - // The previously-hashed state must not be newer than `state`. - if previous_slot > state.slot() { - return Err(Error::TreeHashCacheSkippedSlot { - cache: previous_slot, - state: state.slot(), - }); - } - - // If the state is newer, the previous root must be in the history of the given state. - // If the previous slot is out of range of the `state_roots` array (indicating a long - // gap between the cache's last use and the current state) then we re-initialize. - match state.get_state_root(previous_slot) { - Ok(state_previous_root) if *state_previous_root == previous_root => {} - Ok(_) => return Err(Error::NonLinearTreeHashCacheHistory), - Err(Error::SlotOutOfBounds) => { - *self = Self::new(state); - } - Err(e) => return Err(e), - } - } - - let mut hasher = MerkleHasher::with_leaves(NUM_BEACON_STATE_HASH_TREE_ROOT_LEAVES); - - let leaves = self.recalculate_tree_hash_leaves(state)?; - for leaf in leaves { - hasher.write(leaf.as_bytes())?; - } - - let root = hasher.finish()?; - - self.previous_state = Some((root, state.slot())); - - Ok(root) - } - - /// Updates the cache and provides the root of the given `validators`. - pub fn recalculate_validators_tree_hash_root( - &mut self, - validators: &[Validator], - ) -> Result { - self.validators.recalculate_tree_hash_root(validators) - } -} - -/// A specialized cache for computing the tree hash root of `state.validators`. -#[derive(Debug, PartialEq, Clone, Default, Encode, Decode)] -struct ValidatorsListTreeHashCache { - list_arena: CacheArena, - list_cache: TreeHashCache, - values: ParallelValidatorTreeHash, -} - -impl ValidatorsListTreeHashCache { - /// Instantiates a new cache. - /// - /// Allocates the necessary memory to store all of the cached Merkle trees but does perform any - /// hashing. - fn new(validators: &[Validator]) -> Self { - let mut list_arena = CacheArena::default(); - Self { - list_cache: TreeHashCache::new( - &mut list_arena, - int_log(E::ValidatorRegistryLimit::to_usize()), - validators.len(), - ), - list_arena, - values: ParallelValidatorTreeHash::new(validators), - } - } - - /// Updates the cache and returns the tree hash root for the given `state`. - /// - /// This function makes assumptions that the `validators` list will only change in accordance - /// with valid per-block/per-slot state transitions. - fn recalculate_tree_hash_root(&mut self, validators: &[Validator]) -> Result { - let mut list_arena = std::mem::take(&mut self.list_arena); - - let leaves = self.values.leaves(validators)?; - let num_leaves = leaves.iter().map(|arena| arena.len()).sum(); - - let leaves_iter = ForcedExactSizeIterator { - iter: leaves.into_iter().flatten().map(|h| h.to_fixed_bytes()), - len: num_leaves, - }; - - let list_root = self - .list_cache - .recalculate_merkle_root(&mut list_arena, leaves_iter)?; - - self.list_arena = list_arena; - - Ok(mix_in_length(&list_root, validators.len())) - } -} - -/// Provides a wrapper around some `iter` if the number of items in the iterator is known to the -/// programmer but not the compiler. This allows use of `ExactSizeIterator` in some occasions. -/// -/// Care should be taken to ensure `len` is accurate. -struct ForcedExactSizeIterator { - iter: I, - len: usize, -} - -impl> Iterator for ForcedExactSizeIterator { - type Item = V; - - fn next(&mut self) -> Option { - self.iter.next() - } -} - -impl> ExactSizeIterator for ForcedExactSizeIterator { - fn len(&self) -> usize { - self.len - } -} - -/// Provides a cache for each of the `Validator` objects in `state.validators` and computes the -/// roots of these using Rayon parallelization. -#[derive(Debug, PartialEq, Clone, Default, Encode, Decode)] -pub struct ParallelValidatorTreeHash { - /// Each arena and its associated sub-trees. - arenas: Vec<(CacheArena, Vec)>, -} - -impl ParallelValidatorTreeHash { - /// Instantiates a new cache. - /// - /// Allocates the necessary memory to store all of the cached Merkle trees but does perform any - /// hashing. - fn new(validators: &[Validator]) -> Self { - let num_arenas = std::cmp::max( - 1, - (validators.len() + VALIDATORS_PER_ARENA - 1) / VALIDATORS_PER_ARENA, - ); - - let mut arenas = (1..=num_arenas) - .map(|i| { - let num_validators = if i == num_arenas { - validators.len() % VALIDATORS_PER_ARENA - } else { - VALIDATORS_PER_ARENA - }; - NODES_PER_VALIDATOR * num_validators - }) - .map(|capacity| (CacheArena::with_capacity(capacity), vec![])) - .collect::>(); - - validators.iter().enumerate().for_each(|(i, v)| { - let (arena, caches) = &mut arenas[i / VALIDATORS_PER_ARENA]; - caches.push(v.new_tree_hash_cache(arena)) - }); - - Self { arenas } - } - - /// Returns the number of validators stored in self. - fn len(&self) -> usize { - self.arenas.last().map_or(0, |last| { - // Subtraction cannot underflow because `.last()` ensures the `.len() > 0`. - (self.arenas.len() - 1) * VALIDATORS_PER_ARENA + last.1.len() - }) - } - - /// Updates the caches for each `Validator` in `validators` and returns a list that maps 1:1 - /// with `validators` to the hash of each validator. - /// - /// This function makes assumptions that the `validators` list will only change in accordance - /// with valid per-block/per-slot state transitions. - fn leaves(&mut self, validators: &[Validator]) -> Result>, Error> { - match self.len().cmp(&validators.len()) { - Ordering::Less => validators.iter().skip(self.len()).for_each(|v| { - if self - .arenas - .last() - .map_or(true, |last| last.1.len() >= VALIDATORS_PER_ARENA) - { - let mut arena = CacheArena::default(); - let cache = v.new_tree_hash_cache(&mut arena); - self.arenas.push((arena, vec![cache])) - } else { - let (arena, caches) = &mut self - .arenas - .last_mut() - .expect("Cannot reach this block if arenas is empty."); - caches.push(v.new_tree_hash_cache(arena)) - } - }), - Ordering::Greater => { - return Err(Error::ValidatorRegistryShrunk); - } - Ordering::Equal => (), - } - - self.arenas - .par_iter_mut() - .enumerate() - .map(|(arena_index, (arena, caches))| { - caches - .iter_mut() - .enumerate() - .map(move |(cache_index, cache)| { - let val_index = (arena_index * VALIDATORS_PER_ARENA) + cache_index; - - let validator = validators - .get(val_index) - .ok_or(Error::TreeHashCacheInconsistent)?; - - validator - .recalculate_tree_hash_root(arena, cache) - .map_err(Error::CachedTreeHashError) - }) - .collect() - }) - .collect() - } -} - -#[derive(Debug, PartialEq, Clone)] -pub struct OptionalTreeHashCache { - inner: Option, -} - -#[derive(Debug, PartialEq, Clone)] -pub struct OptionalTreeHashCacheInner { - arena: CacheArena, - tree_hash_cache: TreeHashCache, -} - -impl OptionalTreeHashCache { - /// Initialize a new cache if `item.is_some()`. - fn new>(item: Option<&C>) -> Self { - let inner = item.map(OptionalTreeHashCacheInner::new); - Self { inner } - } - - /// Compute the tree hash root for the given `item`. - /// - /// This function will initialize the inner cache if necessary (e.g. when crossing the fork). - fn recalculate_tree_hash_root>( - &mut self, - item: &C, - ) -> Result { - let cache = self - .inner - .get_or_insert_with(|| OptionalTreeHashCacheInner::new(item)); - item.recalculate_tree_hash_root(&mut cache.arena, &mut cache.tree_hash_cache) - .map_err(Into::into) - } -} - -impl OptionalTreeHashCacheInner { - fn new>(item: &C) -> Self { - let mut arena = CacheArena::default(); - let tree_hash_cache = item.new_tree_hash_cache(&mut arena); - OptionalTreeHashCacheInner { - arena, - tree_hash_cache, - } - } -} - -impl arbitrary::Arbitrary<'_> for BeaconTreeHashCache { - fn arbitrary(_u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { - Ok(Self::default()) - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::{MainnetEthSpec, ParticipationFlags}; - - #[test] - fn validator_node_count() { - let mut arena = CacheArena::default(); - let v = Validator::default(); - let _cache = v.new_tree_hash_cache(&mut arena); - assert_eq!(arena.backing_len(), NODES_PER_VALIDATOR); - } - - #[test] - fn participation_flags() { - type N = ::ValidatorRegistryLimit; - let len = 65; - let mut test_flag = ParticipationFlags::default(); - test_flag.add_flag(0).unwrap(); - let epoch_participation = VariableList::<_, N>::new(vec![test_flag; len]).unwrap(); - - let mut cache = OptionalTreeHashCache { inner: None }; - - let cache_root = cache - .recalculate_tree_hash_root(&ParticipationList::new(&epoch_participation)) - .unwrap(); - let recalc_root = cache - .recalculate_tree_hash_root(&ParticipationList::new(&epoch_participation)) - .unwrap(); - - assert_eq!(cache_root, recalc_root, "recalculated root should match"); - assert_eq!( - cache_root, - epoch_participation.tree_hash_root(), - "cached root should match uncached" - ); - } -} diff --git a/consensus/types/src/blob_sidecar.rs b/consensus/types/src/blob_sidecar.rs index 31b1307aa7f..e54bc2f4f97 100644 --- a/consensus/types/src/blob_sidecar.rs +++ b/consensus/types/src/blob_sidecar.rs @@ -1,7 +1,7 @@ use crate::test_utils::TestRandom; use crate::{ beacon_block_body::BLOB_KZG_COMMITMENTS_INDEX, BeaconBlockHeader, BeaconStateError, Blob, - EthSpec, Hash256, SignedBeaconBlockHeader, Slot, + EthSpec, FixedVector, Hash256, SignedBeaconBlockHeader, Slot, VariableList, }; use crate::{KzgProofs, SignedBeaconBlock}; use bls::Signature; @@ -16,7 +16,6 @@ use safe_arith::{ArithError, SafeArith}; use serde::{Deserialize, Serialize}; use ssz::Encode; use ssz_derive::{Decode, Encode}; -use ssz_types::{FixedVector, VariableList}; use std::fmt::Debug; use std::hash::Hash; use std::sync::Arc; diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index 988bd6755dc..f4e8d4e8b05 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -309,6 +309,13 @@ impl ChainSpec { } } + /// Return the name of the fork activated at `slot`, if any. + pub fn fork_activated_at_slot(&self, slot: Slot) -> Option { + let prev_slot_fork = self.fork_name_at_slot::(slot.saturating_sub(Slot::new(1))); + let slot_fork = self.fork_name_at_slot::(slot); + (slot_fork != prev_slot_fork).then_some(slot_fork) + } + /// Returns the fork version for a named fork. pub fn fork_version_for_name(&self, fork_name: ForkName) -> [u8; 4] { match fork_name { diff --git a/consensus/types/src/execution_payload.rs b/consensus/types/src/execution_payload.rs index 27dc8cab0a4..68e8f6f444f 100644 --- a/consensus/types/src/execution_payload.rs +++ b/consensus/types/src/execution_payload.rs @@ -6,6 +6,8 @@ use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +// FIXME(sproul): try milhouse Vector + pub type Transaction = VariableList; pub type Transactions = VariableList< Transaction<::MaxBytesPerTransaction>, diff --git a/consensus/types/src/execution_payload_header.rs b/consensus/types/src/execution_payload_header.rs index f10f449d6de..4783e2f3bd6 100644 --- a/consensus/types/src/execution_payload_header.rs +++ b/consensus/types/src/execution_payload_header.rs @@ -32,7 +32,8 @@ use tree_hash_derive::TreeHash; tree_hash(enum_behaviour = "transparent") ), cast_error(ty = "Error", expr = "BeaconStateError::IncorrectStateVariant"), - partial_getter_error(ty = "Error", expr = "BeaconStateError::IncorrectStateVariant") + partial_getter_error(ty = "Error", expr = "BeaconStateError::IncorrectStateVariant"), + map_ref_into(ExecutionPayloadHeader) )] #[derive( Debug, Clone, Serialize, Deserialize, Encode, TreeHash, Derivative, arbitrary::Arbitrary, @@ -347,6 +348,27 @@ impl TryFrom> for ExecutionPayloadHeaderDe } } +impl<'a, E: EthSpec> ExecutionPayloadHeaderRefMut<'a, E> { + /// Mutate through + pub fn replace(self, header: ExecutionPayloadHeader) -> Result<(), BeaconStateError> { + match self { + ExecutionPayloadHeaderRefMut::Merge(mut_ref) => { + *mut_ref = header.try_into()?; + } + ExecutionPayloadHeaderRefMut::Capella(mut_ref) => { + *mut_ref = header.try_into()?; + } + ExecutionPayloadHeaderRefMut::Deneb(mut_ref) => { + *mut_ref = header.try_into()?; + } + ExecutionPayloadHeaderRefMut::Electra(mut_ref) => { + *mut_ref = header.try_into()?; + } + } + Ok(()) + } +} + impl TryFrom> for ExecutionPayloadHeaderElectra { type Error = BeaconStateError; fn try_from(header: ExecutionPayloadHeader) -> Result { diff --git a/consensus/types/src/historical_batch.rs b/consensus/types/src/historical_batch.rs index 1c565c0092d..7bac9699eb6 100644 --- a/consensus/types/src/historical_batch.rs +++ b/consensus/types/src/historical_batch.rs @@ -23,8 +23,10 @@ use tree_hash_derive::TreeHash; )] #[arbitrary(bound = "E: EthSpec")] pub struct HistoricalBatch { - pub block_roots: FixedVector, - pub state_roots: FixedVector, + #[test_random(default)] + pub block_roots: Vector, + #[test_random(default)] + pub state_roots: Vector, } #[cfg(test)] diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index 6551ebc1dda..82524e069b1 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -91,7 +91,6 @@ pub mod sync_committee_contribution; pub mod sync_committee_message; pub mod sync_selection_proof; pub mod sync_subnet_id; -mod tree_hash_impls; pub mod validator_registration_data; pub mod withdrawal; @@ -125,7 +124,7 @@ pub use crate::beacon_block_body::{ }; pub use crate::beacon_block_header::BeaconBlockHeader; pub use crate::beacon_committee::{BeaconCommittee, OwnedBeaconCommittee}; -pub use crate::beacon_state::{BeaconTreeHashCache, Error as BeaconStateError, *}; +pub use crate::beacon_state::{compact_state::CompactBeaconState, Error as BeaconStateError, *}; pub use crate::blob_sidecar::{BlobSidecar, BlobSidecarList, BlobsList}; pub use crate::bls_to_execution_change::BlsToExecutionChange; pub use crate::chain_spec::{ChainSpec, Config, Domain}; @@ -221,7 +220,7 @@ pub use crate::sync_committee_subscription::SyncCommitteeSubscription; pub use crate::sync_duty::SyncDuty; pub use crate::sync_selection_proof::SyncSelectionProof; pub use crate::sync_subnet_id::SyncSubnetId; -pub use crate::validator::Validator; +pub use crate::validator::{Validator, ValidatorMutable}; pub use crate::validator_registration_data::*; pub use crate::validator_subscription::ValidatorSubscription; pub use crate::voluntary_exit::VoluntaryExit; @@ -243,8 +242,7 @@ pub use bls::{ AggregatePublicKey, AggregateSignature, Keypair, PublicKey, PublicKeyBytes, SecretKey, Signature, SignatureBytes, }; - pub use kzg::{KzgCommitment, KzgProof, VERSIONED_HASH_VERSION_KZG}; - +pub use milhouse::{self, List, Vector}; pub use ssz_types::{typenum, typenum::Unsigned, BitList, BitVector, FixedVector, VariableList}; pub use superstruct::superstruct; diff --git a/consensus/types/src/light_client_bootstrap.rs b/consensus/types/src/light_client_bootstrap.rs index 43bab325f3c..f76d710e4d6 100644 --- a/consensus/types/src/light_client_bootstrap.rs +++ b/consensus/types/src/light_client_bootstrap.rs @@ -1,8 +1,8 @@ -use super::{BeaconState, EthSpec, FixedVector, Hash256, SyncCommittee}; +use super::{BeaconState, EthSpec, Hash256, SyncCommittee}; use crate::{ - light_client_update::*, test_utils::TestRandom, ChainSpec, ForkName, ForkVersionDeserialize, - LightClientHeaderAltair, LightClientHeaderCapella, LightClientHeaderDeneb, SignedBeaconBlock, - Slot, + light_client_update::*, test_utils::TestRandom, ChainSpec, FixedVector, ForkName, + ForkVersionDeserialize, LightClientHeaderAltair, LightClientHeaderCapella, + LightClientHeaderDeneb, SignedBeaconBlock, Slot, }; use derivative::Derivative; use serde::{Deserialize, Deserializer, Serialize}; diff --git a/consensus/types/src/light_client_finality_update.rs b/consensus/types/src/light_client_finality_update.rs index 288527e91cb..3d0bfd115a7 100644 --- a/consensus/types/src/light_client_finality_update.rs +++ b/consensus/types/src/light_client_finality_update.rs @@ -58,6 +58,7 @@ pub struct LightClientFinalityUpdate { #[superstruct(only(Deneb), partial_getter(rename = "finalized_header_deneb"))] pub finalized_header: LightClientHeaderDeneb, /// Merkle proof attesting finalized header. + #[test_random(default)] pub finality_branch: FixedVector, /// current sync aggreggate pub sync_aggregate: SyncAggregate, diff --git a/consensus/types/src/light_client_update.rs b/consensus/types/src/light_client_update.rs index af9cbc16610..d5e8cd592df 100644 --- a/consensus/types/src/light_client_update.rs +++ b/consensus/types/src/light_client_update.rs @@ -37,6 +37,7 @@ pub const EXECUTION_PAYLOAD_PROOF_LEN: usize = 4; #[derive(Debug, PartialEq, Clone)] pub enum Error { SszTypesError(ssz_types::Error), + MilhouseError(milhouse::Error), BeaconStateError(beacon_state::Error), ArithError(ArithError), AltairForkNotActive, @@ -65,6 +66,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: milhouse::Error) -> Error { + Error::MilhouseError(e) + } +} + /// A LightClientUpdate is the update we request solely to either complete the bootstrapping process, /// or to sync up to the last committee period, we need to have one ready for each ALTAIR period /// we go over, note: there is no need to keep all of the updates from [ALTAIR_PERIOD, CURRENT_PERIOD]. diff --git a/consensus/types/src/test_utils/test_random.rs b/consensus/types/src/test_utils/test_random.rs index 0adaf81bd7d..72a7a036ccc 100644 --- a/consensus/types/src/test_utils/test_random.rs +++ b/consensus/types/src/test_utils/test_random.rs @@ -87,7 +87,7 @@ where } } -impl TestRandom for FixedVector +impl TestRandom for ssz_types::FixedVector where T: TestRandom, { diff --git a/consensus/types/src/tree_hash_impls.rs b/consensus/types/src/tree_hash_impls.rs deleted file mode 100644 index eb3660d4666..00000000000 --- a/consensus/types/src/tree_hash_impls.rs +++ /dev/null @@ -1,165 +0,0 @@ -//! This module contains custom implementations of `CachedTreeHash` for ETH2-specific types. -//! -//! It makes some assumptions about the layouts and update patterns of other structs in this -//! crate, and should be updated carefully whenever those structs are changed. -use crate::{Epoch, Hash256, PublicKeyBytes, Validator}; -use cached_tree_hash::{int_log, CacheArena, CachedTreeHash, Error, TreeHashCache}; -use int_to_bytes::int_to_fixed_bytes32; -use tree_hash::merkle_root; - -/// Number of struct fields on `Validator`. -const NUM_VALIDATOR_FIELDS: usize = 8; - -impl CachedTreeHash for Validator { - fn new_tree_hash_cache(&self, arena: &mut CacheArena) -> TreeHashCache { - TreeHashCache::new(arena, int_log(NUM_VALIDATOR_FIELDS), NUM_VALIDATOR_FIELDS) - } - - /// Efficiently tree hash a `Validator`, assuming it was updated by a valid state transition. - /// - /// Specifically, we assume that the `pubkey` field is constant. - fn recalculate_tree_hash_root( - &self, - arena: &mut CacheArena, - cache: &mut TreeHashCache, - ) -> Result { - // Otherwise just check the fields which might have changed. - let dirty_indices = cache - .leaves() - .iter_mut(arena)? - .enumerate() - .flat_map(|(i, leaf)| { - // Pubkey field (index 0) is constant. - if i == 0 && cache.initialized { - None - } else if process_field_by_index(self, i, leaf, !cache.initialized) { - Some(i) - } else { - None - } - }) - .collect(); - - cache.update_merkle_root(arena, dirty_indices) - } -} - -fn process_field_by_index( - v: &Validator, - field_idx: usize, - leaf: &mut Hash256, - force_update: bool, -) -> bool { - match field_idx { - 0 => process_pubkey_bytes_field(&v.pubkey, leaf, force_update), - 1 => process_slice_field(v.withdrawal_credentials.as_bytes(), leaf, force_update), - 2 => process_u64_field(v.effective_balance, leaf, force_update), - 3 => process_bool_field(v.slashed, leaf, force_update), - 4 => process_epoch_field(v.activation_eligibility_epoch, leaf, force_update), - 5 => process_epoch_field(v.activation_epoch, leaf, force_update), - 6 => process_epoch_field(v.exit_epoch, leaf, force_update), - 7 => process_epoch_field(v.withdrawable_epoch, leaf, force_update), - _ => panic!( - "Validator type only has {} fields, {} out of bounds", - NUM_VALIDATOR_FIELDS, field_idx - ), - } -} - -fn process_pubkey_bytes_field( - val: &PublicKeyBytes, - leaf: &mut Hash256, - force_update: bool, -) -> bool { - let new_tree_hash = merkle_root(val.as_serialized(), 0); - process_slice_field(new_tree_hash.as_bytes(), leaf, force_update) -} - -fn process_slice_field(new_tree_hash: &[u8], leaf: &mut Hash256, force_update: bool) -> bool { - if force_update || leaf.as_bytes() != new_tree_hash { - leaf.assign_from_slice(new_tree_hash); - true - } else { - false - } -} - -fn process_u64_field(val: u64, leaf: &mut Hash256, force_update: bool) -> bool { - let new_tree_hash = int_to_fixed_bytes32(val); - process_slice_field(&new_tree_hash[..], leaf, force_update) -} - -fn process_epoch_field(val: Epoch, leaf: &mut Hash256, force_update: bool) -> bool { - process_u64_field(val.as_u64(), leaf, force_update) -} - -fn process_bool_field(val: bool, leaf: &mut Hash256, force_update: bool) -> bool { - process_u64_field(val as u64, leaf, force_update) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::test_utils::TestRandom; - use rand::SeedableRng; - use rand_xorshift::XorShiftRng; - use tree_hash::TreeHash; - - fn test_validator_tree_hash(v: &Validator) { - let arena = &mut CacheArena::default(); - - let mut cache = v.new_tree_hash_cache(arena); - // With a fresh cache - assert_eq!( - &v.tree_hash_root()[..], - v.recalculate_tree_hash_root(arena, &mut cache) - .unwrap() - .as_bytes(), - "{:?}", - v - ); - // With a completely up-to-date cache - assert_eq!( - &v.tree_hash_root()[..], - v.recalculate_tree_hash_root(arena, &mut cache) - .unwrap() - .as_bytes(), - "{:?}", - v - ); - } - - #[test] - fn default_validator() { - test_validator_tree_hash(&Validator::default()); - } - - #[test] - fn zeroed_validator() { - let v = Validator { - activation_eligibility_epoch: Epoch::from(0u64), - activation_epoch: Epoch::from(0u64), - ..Default::default() - }; - test_validator_tree_hash(&v); - } - - #[test] - fn random_validators() { - let mut rng = XorShiftRng::from_seed([0xf1; 16]); - let num_validators = 1000; - (0..num_validators) - .map(|_| Validator::random_for_test(&mut rng)) - .for_each(|v| test_validator_tree_hash(&v)); - } - - #[test] - #[allow(clippy::assertions_on_constants)] - pub fn smallvec_size_check() { - // If this test fails we need to go and reassess the length of the `SmallVec` in - // `cached_tree_hash::TreeHashCache`. If the size of the `SmallVec` is too slow we're going - // to start doing heap allocations for each validator, this will fragment memory and slow - // us down. - assert!(NUM_VALIDATOR_FIELDS <= 8,); - } -} diff --git a/consensus/types/src/validator.rs b/consensus/types/src/validator.rs index 98567cd1e6c..349f4a9b16f 100644 --- a/consensus/types/src/validator.rs +++ b/consensus/types/src/validator.rs @@ -2,28 +2,34 @@ use crate::{ test_utils::TestRandom, Address, BeaconState, ChainSpec, Epoch, EthSpec, Hash256, PublicKeyBytes, }; +use arbitrary::Arbitrary; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; +use std::sync::Arc; use test_random_derive::TestRandom; +use tree_hash::TreeHash; use tree_hash_derive::TreeHash; +const NUM_FIELDS: usize = 8; + /// Information about a `BeaconChain` validator. /// /// Spec v0.12.1 #[derive( - arbitrary::Arbitrary, - Debug, - Clone, - PartialEq, - Serialize, - Deserialize, - Encode, - Decode, - TestRandom, - TreeHash, + Arbitrary, Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TestRandom, )] +#[serde(deny_unknown_fields)] pub struct Validator { - pub pubkey: PublicKeyBytes, + pub pubkey: Arc, + #[serde(flatten)] + pub mutable: ValidatorMutable, +} + +/// The mutable fields of a validator. +#[derive( + Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom, Arbitrary, +)] +pub struct ValidatorMutable { pub withdrawal_credentials: Hash256, #[serde(with = "serde_utils::quoted_u64")] pub effective_balance: u64, @@ -34,47 +40,148 @@ pub struct Validator { pub withdrawable_epoch: Epoch, } +pub trait ValidatorTrait: + std::fmt::Debug + + PartialEq + + Clone + + serde::Serialize + + Send + + Sync + + serde::de::DeserializeOwned + + ssz::Encode + + ssz::Decode + + TreeHash + + TestRandom + + for<'a> arbitrary::Arbitrary<'a> +{ +} + +impl ValidatorTrait for Validator {} +impl ValidatorTrait for ValidatorMutable {} + impl Validator { + pub fn pubkey(&self) -> &PublicKeyBytes { + &self.pubkey + } + + pub fn pubkey_clone(&self) -> Arc { + self.pubkey.clone() + } + + /// Replace the validator's pubkey (should only be used during testing). + pub fn replace_pubkey(&mut self, pubkey: PublicKeyBytes) { + self.pubkey = Arc::new(pubkey); + } + + #[inline] + pub fn withdrawal_credentials(&self) -> Hash256 { + self.mutable.withdrawal_credentials + } + + #[inline] + pub fn effective_balance(&self) -> u64 { + self.mutable.effective_balance + } + + #[inline] + pub fn slashed(&self) -> bool { + self.mutable.slashed + } + + #[inline] + pub fn activation_eligibility_epoch(&self) -> Epoch { + self.mutable.activation_eligibility_epoch + } + + #[inline] + pub fn activation_epoch(&self) -> Epoch { + self.mutable.activation_epoch + } + + #[inline] + pub fn activation_epoch_mut(&mut self) -> &mut Epoch { + &mut self.mutable.activation_epoch + } + + #[inline] + pub fn exit_epoch(&self) -> Epoch { + self.mutable.exit_epoch + } + + pub fn exit_epoch_mut(&mut self) -> &mut Epoch { + &mut self.mutable.exit_epoch + } + + #[inline] + pub fn withdrawable_epoch(&self) -> Epoch { + self.mutable.withdrawable_epoch + } + /// Returns `true` if the validator is considered active at some epoch. + #[inline] pub fn is_active_at(&self, epoch: Epoch) -> bool { - self.activation_epoch <= epoch && epoch < self.exit_epoch + self.activation_epoch() <= epoch && epoch < self.exit_epoch() } /// Returns `true` if the validator is slashable at some epoch. + #[inline] pub fn is_slashable_at(&self, epoch: Epoch) -> bool { - !self.slashed && self.activation_epoch <= epoch && epoch < self.withdrawable_epoch + !self.slashed() && self.activation_epoch() <= epoch && epoch < self.withdrawable_epoch() } /// Returns `true` if the validator is considered exited at some epoch. + #[inline] pub fn is_exited_at(&self, epoch: Epoch) -> bool { - self.exit_epoch <= epoch + self.exit_epoch() <= epoch } /// Returns `true` if the validator is able to withdraw at some epoch. + #[inline] pub fn is_withdrawable_at(&self, epoch: Epoch) -> bool { - epoch >= self.withdrawable_epoch + epoch >= self.withdrawable_epoch() } /// Returns `true` if the validator is eligible to join the activation queue. /// /// Spec v0.12.1 + #[inline] pub fn is_eligible_for_activation_queue(&self, spec: &ChainSpec) -> bool { - self.activation_eligibility_epoch == spec.far_future_epoch - && self.effective_balance == spec.max_effective_balance + self.activation_eligibility_epoch() == spec.far_future_epoch + && self.effective_balance() == spec.max_effective_balance } /// Returns `true` if the validator is eligible to be activated. /// /// Spec v0.12.1 + #[inline] pub fn is_eligible_for_activation( &self, state: &BeaconState, spec: &ChainSpec, ) -> bool { - // Placement in queue is finalized - self.activation_eligibility_epoch <= state.finalized_checkpoint().epoch // Has not yet been activated - && self.activation_epoch == spec.far_future_epoch + self.activation_epoch() == spec.far_future_epoch && + // Placement in queue is finalized + self.activation_eligibility_epoch() <= state.finalized_checkpoint().epoch + } + + fn tree_hash_root_internal(&self) -> Result { + let mut hasher = tree_hash::MerkleHasher::with_leaves(NUM_FIELDS); + + hasher.write(self.pubkey().tree_hash_root().as_bytes())?; + hasher.write(self.withdrawal_credentials().tree_hash_root().as_bytes())?; + hasher.write(self.effective_balance().tree_hash_root().as_bytes())?; + hasher.write(self.slashed().tree_hash_root().as_bytes())?; + hasher.write( + self.activation_eligibility_epoch() + .tree_hash_root() + .as_bytes(), + )?; + hasher.write(self.activation_epoch().tree_hash_root().as_bytes())?; + hasher.write(self.exit_epoch().tree_hash_root().as_bytes())?; + hasher.write(self.withdrawable_epoch().tree_hash_root().as_bytes())?; + + hasher.finish() } /// Returns `true` if the validator *could* be eligible for activation at `epoch`. @@ -84,18 +191,18 @@ impl Validator { /// the epoch transition at the end of `epoch`. pub fn could_be_eligible_for_activation_at(&self, epoch: Epoch, spec: &ChainSpec) -> bool { // Has not yet been activated - self.activation_epoch == spec.far_future_epoch + self.activation_epoch() == spec.far_future_epoch // Placement in queue could be finalized. // // NOTE: the epoch distance is 1 rather than 2 because we consider the activations that // occur at the *end* of `epoch`, after `process_justification_and_finalization` has already // updated the state's checkpoint. - && self.activation_eligibility_epoch < epoch + && self.activation_eligibility_epoch() < epoch } /// Returns `true` if the validator has eth1 withdrawal credential. pub fn has_eth1_withdrawal_credential(&self, spec: &ChainSpec) -> bool { - self.withdrawal_credentials + self.withdrawal_credentials() .as_bytes() .first() .map(|byte| *byte == spec.eth1_address_withdrawal_prefix_byte) @@ -106,7 +213,7 @@ impl Validator { pub fn get_eth1_withdrawal_address(&self, spec: &ChainSpec) -> Option
{ self.has_eth1_withdrawal_credential(spec) .then(|| { - self.withdrawal_credentials + self.withdrawal_credentials() .as_bytes() .get(12..) .map(Address::from_slice) @@ -121,28 +228,37 @@ impl Validator { let mut bytes = [0u8; 32]; bytes[0] = spec.eth1_address_withdrawal_prefix_byte; bytes[12..].copy_from_slice(execution_address.as_bytes()); - self.withdrawal_credentials = Hash256::from(bytes); + self.mutable.withdrawal_credentials = Hash256::from(bytes); } /// Returns `true` if the validator is fully withdrawable at some epoch. pub fn is_fully_withdrawable_at(&self, balance: u64, epoch: Epoch, spec: &ChainSpec) -> bool { - self.has_eth1_withdrawal_credential(spec) && self.withdrawable_epoch <= epoch && balance > 0 + self.has_eth1_withdrawal_credential(spec) + && self.withdrawable_epoch() <= epoch + && balance > 0 } /// Returns `true` if the validator is partially withdrawable. pub fn is_partially_withdrawable_validator(&self, balance: u64, spec: &ChainSpec) -> bool { self.has_eth1_withdrawal_credential(spec) - && self.effective_balance == spec.max_effective_balance + && self.effective_balance() == spec.max_effective_balance && balance > spec.max_effective_balance } } impl Default for Validator { - /// Yields a "default" `Validator`. Primarily used for testing. fn default() -> Self { - Self { - pubkey: PublicKeyBytes::empty(), - withdrawal_credentials: Hash256::default(), + Validator { + pubkey: Arc::new(PublicKeyBytes::empty()), + mutable: <_>::default(), + } + } +} + +impl Default for ValidatorMutable { + fn default() -> Self { + ValidatorMutable { + withdrawal_credentials: Hash256::zero(), activation_eligibility_epoch: Epoch::from(std::u64::MAX), activation_epoch: Epoch::from(std::u64::MAX), exit_epoch: Epoch::from(std::u64::MAX), @@ -153,6 +269,25 @@ impl Default for Validator { } } +impl TreeHash for Validator { + fn tree_hash_type() -> tree_hash::TreeHashType { + tree_hash::TreeHashType::Container + } + + fn tree_hash_packed_encoding(&self) -> tree_hash::PackedEncoding { + unreachable!("Struct should never be packed.") + } + + fn tree_hash_packing_factor() -> usize { + unreachable!("Struct should never be packed.") + } + + fn tree_hash_root(&self) -> Hash256 { + self.tree_hash_root_internal() + .expect("Validator tree_hash_root should not fail") + } +} + #[cfg(test)] mod tests { use super::*; @@ -166,7 +301,7 @@ mod tests { assert!(!v.is_active_at(epoch)); assert!(!v.is_exited_at(epoch)); assert!(!v.is_withdrawable_at(epoch)); - assert!(!v.slashed); + assert!(!v.slashed()); } #[test] @@ -174,7 +309,10 @@ mod tests { let epoch = Epoch::new(10); let v = Validator { - activation_epoch: epoch, + mutable: ValidatorMutable { + activation_epoch: epoch, + ..Default::default() + }, ..Validator::default() }; @@ -188,7 +326,10 @@ mod tests { let epoch = Epoch::new(10); let v = Validator { - exit_epoch: epoch, + mutable: ValidatorMutable { + exit_epoch: epoch, + ..ValidatorMutable::default() + }, ..Validator::default() }; @@ -202,7 +343,10 @@ mod tests { let epoch = Epoch::new(10); let v = Validator { - withdrawable_epoch: epoch, + mutable: ValidatorMutable { + withdrawable_epoch: epoch, + ..ValidatorMutable::default() + }, ..Validator::default() }; diff --git a/crypto/bls/Cargo.toml b/crypto/bls/Cargo.toml index 7aa8e02dcab..3ae51f27c93 100644 --- a/crypto/bls/Cargo.toml +++ b/crypto/bls/Cargo.toml @@ -24,3 +24,10 @@ fake_crypto = [] supranational = ["blst"] supranational-portable = ["supranational", "blst/portable"] supranational-force-adx = ["supranational", "blst/force-adx"] + +[dev-dependencies] +criterion = "0.3.3" + +[[bench]] +name = "compress_decompress" +harness = false diff --git a/crypto/bls/benches/compress_decompress.rs b/crypto/bls/benches/compress_decompress.rs new file mode 100644 index 00000000000..3053cf1f9a3 --- /dev/null +++ b/crypto/bls/benches/compress_decompress.rs @@ -0,0 +1,64 @@ +use bls::{PublicKey, SecretKey}; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; + +pub fn compress(c: &mut Criterion) { + let private_key = SecretKey::random(); + let public_key = private_key.public_key(); + c.bench_with_input( + BenchmarkId::new("compress", 1), + &public_key, + |b, public_key| { + b.iter(|| public_key.compress()); + }, + ); +} + +pub fn decompress(c: &mut Criterion) { + let private_key = SecretKey::random(); + let public_key_bytes = private_key.public_key().compress(); + c.bench_with_input( + BenchmarkId::new("decompress", 1), + &public_key_bytes, + |b, public_key_bytes| { + b.iter(|| public_key_bytes.decompress().unwrap()); + }, + ); +} + +pub fn deserialize_uncompressed(c: &mut Criterion) { + let private_key = SecretKey::random(); + let public_key_bytes = private_key.public_key().serialize_uncompressed(); + c.bench_with_input( + BenchmarkId::new("deserialize_uncompressed", 1), + &public_key_bytes, + |b, public_key_bytes| { + b.iter(|| PublicKey::deserialize_uncompressed(public_key_bytes).unwrap()); + }, + ); +} + +pub fn compress_all(c: &mut Criterion) { + let n = 500_000; + let keys = (0..n) + .map(|_| { + let private_key = SecretKey::random(); + private_key.public_key() + }) + .collect::>(); + c.bench_with_input(BenchmarkId::new("compress", n), &keys, |b, keys| { + b.iter(|| { + for key in keys { + key.compress(); + } + }); + }); +} + +criterion_group!( + benches, + compress, + decompress, + deserialize_uncompressed, + compress_all +); +criterion_main!(benches); diff --git a/crypto/bls/src/generic_public_key.rs b/crypto/bls/src/generic_public_key.rs index 462e4cb2cb0..80b42dfa714 100644 --- a/crypto/bls/src/generic_public_key.rs +++ b/crypto/bls/src/generic_public_key.rs @@ -11,6 +11,9 @@ use tree_hash::TreeHash; /// The byte-length of a BLS public key when serialized in compressed form. pub const PUBLIC_KEY_BYTES_LEN: usize = 48; +/// The byte-length of a BLS public key when serialized in uncompressed form. +pub const PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN: usize = 96; + /// Represents the public key at infinity. pub const INFINITY_PUBLIC_KEY: [u8; PUBLIC_KEY_BYTES_LEN] = [ 0xc0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -23,8 +26,17 @@ pub trait TPublicKey: Sized + Clone { /// Serialize `self` as compressed bytes. fn serialize(&self) -> [u8; PUBLIC_KEY_BYTES_LEN]; + /// Serialize `self` as uncompressed bytes. + fn serialize_uncompressed(&self) -> [u8; PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN]; + /// Deserialize `self` from compressed bytes. fn deserialize(bytes: &[u8]) -> Result; + + /// Deserialize `self` from uncompressed bytes. + /// + /// This function *does not* perform thorough checks of the input bytes and should only be + /// used with bytes output from `Self::serialize_uncompressed`. + fn deserialize_uncompressed(bytes: &[u8]) -> Result; } /// A BLS public key that is generic across some BLS point (`Pub`). @@ -65,6 +77,11 @@ where self.point.serialize() } + /// Serialize `self` as uncompressed bytes. + pub fn serialize_uncompressed(&self) -> [u8; PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN] { + self.point.serialize_uncompressed() + } + /// Deserialize `self` from compressed bytes. pub fn deserialize(bytes: &[u8]) -> Result { if bytes == &INFINITY_PUBLIC_KEY[..] { @@ -75,6 +92,13 @@ where }) } } + + /// Deserialize `self` from compressed bytes. + pub fn deserialize_uncompressed(bytes: &[u8]) -> Result { + Ok(Self { + point: Pub::deserialize_uncompressed(bytes)?, + }) + } } impl Eq for GenericPublicKey {} diff --git a/crypto/bls/src/impls/blst.rs b/crypto/bls/src/impls/blst.rs index 0049d79cc55..54c7ad2944e 100644 --- a/crypto/bls/src/impls/blst.rs +++ b/crypto/bls/src/impls/blst.rs @@ -1,10 +1,12 @@ use crate::{ generic_aggregate_public_key::TAggregatePublicKey, generic_aggregate_signature::TAggregateSignature, - generic_public_key::{GenericPublicKey, TPublicKey, PUBLIC_KEY_BYTES_LEN}, + generic_public_key::{ + GenericPublicKey, TPublicKey, PUBLIC_KEY_BYTES_LEN, PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN, + }, generic_secret_key::TSecretKey, generic_signature::{TSignature, SIGNATURE_BYTES_LEN}, - Error, Hash256, ZeroizeHash, INFINITY_SIGNATURE, + BlstError, Error, Hash256, ZeroizeHash, INFINITY_SIGNATURE, }; pub use blst::min_pk as blst_core; use blst::{blst_scalar, BLST_ERROR}; @@ -121,6 +123,10 @@ impl TPublicKey for blst_core::PublicKey { self.compress() } + fn serialize_uncompressed(&self) -> [u8; PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN] { + blst_core::PublicKey::serialize(self) + } + fn deserialize(bytes: &[u8]) -> Result { // key_validate accepts uncompressed bytes too so enforce byte length here. // It also does subgroup checks, noting infinity check is done in `generic_public_key.rs`. @@ -132,6 +138,19 @@ impl TPublicKey for blst_core::PublicKey { } Self::key_validate(bytes).map_err(Into::into) } + + fn deserialize_uncompressed(bytes: &[u8]) -> Result { + if bytes.len() != PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN { + return Err(Error::InvalidByteLength { + got: bytes.len(), + expected: PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN, + }); + } + // Ensure we use the `blst` function rather than the one from this trait. + let result: Result = Self::deserialize(bytes); + let key = result?; + Ok(key) + } } /// A wrapper that allows for `PartialEq` and `Clone` impls. diff --git a/crypto/bls/src/impls/fake_crypto.rs b/crypto/bls/src/impls/fake_crypto.rs index f2d8b79b986..a09fb347e6b 100644 --- a/crypto/bls/src/impls/fake_crypto.rs +++ b/crypto/bls/src/impls/fake_crypto.rs @@ -1,7 +1,9 @@ use crate::{ generic_aggregate_public_key::TAggregatePublicKey, generic_aggregate_signature::TAggregateSignature, - generic_public_key::{GenericPublicKey, TPublicKey, PUBLIC_KEY_BYTES_LEN}, + generic_public_key::{ + GenericPublicKey, TPublicKey, PUBLIC_KEY_BYTES_LEN, PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN, + }, generic_secret_key::{TSecretKey, SECRET_KEY_BYTES_LEN}, generic_signature::{TSignature, SIGNATURE_BYTES_LEN}, Error, Hash256, ZeroizeHash, INFINITY_PUBLIC_KEY, INFINITY_SIGNATURE, @@ -46,11 +48,19 @@ impl TPublicKey for PublicKey { self.0 } + fn serialize_uncompressed(&self) -> [u8; PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN] { + panic!("fake_crypto does not support uncompressed keys") + } + fn deserialize(bytes: &[u8]) -> Result { let mut pubkey = Self::infinity(); pubkey.0[..].copy_from_slice(&bytes[0..PUBLIC_KEY_BYTES_LEN]); Ok(pubkey) } + + fn deserialize_uncompressed(_: &[u8]) -> Result { + panic!("fake_crypto does not support uncompressed keys") + } } impl Eq for PublicKey {} diff --git a/crypto/bls/src/lib.rs b/crypto/bls/src/lib.rs index fef9804b784..af269b943d7 100644 --- a/crypto/bls/src/lib.rs +++ b/crypto/bls/src/lib.rs @@ -33,7 +33,9 @@ mod zeroize_hash; pub mod impls; -pub use generic_public_key::{INFINITY_PUBLIC_KEY, PUBLIC_KEY_BYTES_LEN}; +pub use generic_public_key::{ + INFINITY_PUBLIC_KEY, PUBLIC_KEY_BYTES_LEN, PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN, +}; pub use generic_secret_key::SECRET_KEY_BYTES_LEN; pub use generic_signature::{INFINITY_SIGNATURE, SIGNATURE_BYTES_LEN}; pub use get_withdrawal_credentials::get_withdrawal_credentials; diff --git a/crypto/bls/tests/tests.rs b/crypto/bls/tests/tests.rs index 478c1b7dc26..dac2e97f407 100644 --- a/crypto/bls/tests/tests.rs +++ b/crypto/bls/tests/tests.rs @@ -341,6 +341,11 @@ macro_rules! test_suite { .assert_single_message_verify(true) } + #[test] + fn deserialize_infinity_public_key() { + PublicKey::deserialize(&bls::INFINITY_PUBLIC_KEY).unwrap_err(); + } + /// A helper struct to make it easer to deal with `SignatureSet` lifetimes. struct OwnedSignatureSet { signature: AggregateSignature, diff --git a/database_manager/Cargo.toml b/database_manager/Cargo.toml index 07045dd95c2..1aaf054d0b2 100644 --- a/database_manager/Cargo.toml +++ b/database_manager/Cargo.toml @@ -9,6 +9,7 @@ beacon_node = { workspace = true } clap = { workspace = true } clap_utils = { workspace = true } environment = { workspace = true } +ethereum_ssz = { workspace = true } hex = { workspace = true } logging = { workspace = true } sloggers = { workspace = true } diff --git a/database_manager/src/lib.rs b/database_manager/src/lib.rs index 617192abfef..24c3c9058c6 100644 --- a/database_manager/src/lib.rs +++ b/database_manager/src/lib.rs @@ -2,7 +2,7 @@ use beacon_chain::{ builder::Witness, eth1_chain::CachingEth1Backend, schema_change::migrate_schema, slot_clock::SystemTimeSlotClock, }; -use beacon_node::{get_data_dir, get_slots_per_restore_point, ClientConfig}; +use beacon_node::{get_data_dir, ClientConfig}; use clap::{App, Arg, ArgMatches}; use environment::{Environment, RuntimeContext}; use slog::{info, warn, Logger}; @@ -159,17 +159,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .visible_aliases(&["db"]) .setting(clap::AppSettings::ColoredHelp) .about("Manage a beacon node database") - .arg( - Arg::with_name("slots-per-restore-point") - .long("slots-per-restore-point") - .value_name("SLOT_COUNT") - .help( - "Specifies how often a freezer DB restore point should be stored. \ - Cannot be changed after initialization. \ - [default: 2048 (mainnet) or 64 (minimal)]", - ) - .takes_value(true), - ) .arg( Arg::with_name("freezer-dir") .long("freezer-dir") @@ -195,6 +184,21 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .help("Data directory for the blobs database.") .takes_value(true), ) + .arg( + Arg::with_name("hierarchy-exponents") + .long("hierarchy-exponents") + .value_name("EXPONENTS") + .help("Specifies the frequency for storing full state snapshots and hierarchical \ + diffs in the freezer DB. Accepts a comma-separated list of ascending \ + exponents. Each exponent defines an interval for storing diffs to the layer \ + above. The last exponent defines the interval for full snapshots. \ + For example, a config of '4,8,12' would store a full snapshot every \ + 4096 (2^12) slots, first-level diffs every 256 (2^8) slots, and second-level \ + diffs every 16 (2^4) slots. \ + Cannot be changed after initialization. \ + [default: 5,9,11,13,16,18,21]") + .takes_value(true) + ) .subcommand(migrate_cli_app()) .subcommand(version_cli_app()) .subcommand(inspect_cli_app()) @@ -220,16 +224,16 @@ fn parse_client_config( client_config.blobs_db_path = Some(blobs_db_dir); } - let (sprp, sprp_explicit) = get_slots_per_restore_point::(cli_args)?; - client_config.store.slots_per_restore_point = sprp; - client_config.store.slots_per_restore_point_set_explicitly = sprp_explicit; - if let Some(blob_prune_margin_epochs) = clap_utils::parse_optional(cli_args, "blob-prune-margin-epochs")? { client_config.store.blob_prune_margin_epochs = blob_prune_margin_epochs; } + if let Some(hierarchy_config) = clap_utils::parse_optional(cli_args, "hierarchy-exponents")? { + client_config.store.hierarchy_config = hierarchy_config; + } + Ok(client_config) } diff --git a/lcli/Cargo.toml b/lcli/Cargo.toml index 2aba106e506..d3a8f457727 100644 --- a/lcli/Cargo.toml +++ b/lcli/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "lcli" description = "Lighthouse CLI (modeled after zcli)" -version = "5.1.3" +version = "5.1.222-exp" authors = ["Paul Hauner "] edition = { workspace = true } diff --git a/lcli/src/main.rs b/lcli/src/main.rs index c374a8f4b37..e2af274c08a 100644 --- a/lcli/src/main.rs +++ b/lcli/src/main.rs @@ -16,6 +16,7 @@ mod new_testnet; mod parse_ssz; mod replace_state_pubkeys; mod skip_slots; +mod state_diff; mod state_root; mod transition_blocks; @@ -866,6 +867,22 @@ fn main() { .help("Number of repeat runs, useful for benchmarking."), ) ) + .subcommand( + SubCommand::with_name("state-diff") + .about("Compute a state diff for a pair of states") + .arg( + Arg::with_name("state1") + .value_name("STATE1") + .takes_value(true) + .help("Path to first SSZ state"), + ) + .arg( + Arg::with_name("state2") + .value_name("STATE2") + .takes_value(true) + .help("Path to second SSZ state"), + ) + ) .subcommand( SubCommand::with_name("state-root") .about("Computes the state root of some state.") @@ -1095,6 +1112,8 @@ fn run( .map_err(|e| format!("Failed to run mnemonic-validators command: {}", e)), ("indexed-attestations", Some(matches)) => indexed_attestations::run::(matches) .map_err(|e| format!("Failed to run indexed-attestations command: {}", e)), + ("state-diff", Some(matches)) => state_diff::run::(env, matches) + .map_err(|e| format!("Failed to run state-diff command: {}", e)), ("block-root", Some(matches)) => { let network_config = get_network_config()?; block_root::run::(env, network_config, matches) diff --git a/lcli/src/new_testnet.rs b/lcli/src/new_testnet.rs index f9da3d2b3e9..4ea04fd15f4 100644 --- a/lcli/src/new_testnet.rs +++ b/lcli/src/new_testnet.rs @@ -17,13 +17,14 @@ use std::fs::File; use std::io::Read; use std::path::PathBuf; use std::str::FromStr; +use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use types::ExecutionBlockHash; use types::{ test_utils::generate_deterministic_keypairs, Address, BeaconState, ChainSpec, Config, Epoch, Eth1Data, EthSpec, ExecutionPayloadHeader, ExecutionPayloadHeaderCapella, ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderElectra, ExecutionPayloadHeaderMerge, - ForkName, Hash256, Keypair, PublicKey, Validator, + ForkName, Hash256, Keypair, PublicKey, Validator, ValidatorMutable, }; pub fn run(testnet_dir_path: PathBuf, matches: &ArgMatches) -> Result<(), String> { @@ -264,7 +265,7 @@ fn initialize_state_with_validators( let mut state = BeaconState::new(genesis_time, eth1_data, spec); // Seed RANDAO with Eth1 entropy - state.fill_randao_mixes_with(eth1_block_hash); + state.fill_randao_mixes_with(eth1_block_hash).unwrap(); for keypair in keypairs.iter() { let withdrawal_credentials = |pubkey: &PublicKey| { @@ -275,17 +276,19 @@ fn initialize_state_with_validators( let amount = spec.max_effective_balance; // Create a new validator. let validator = Validator { - pubkey: keypair.0.pk.clone().into(), - withdrawal_credentials: withdrawal_credentials(&keypair.1.pk), - activation_eligibility_epoch: spec.far_future_epoch, - activation_epoch: spec.far_future_epoch, - exit_epoch: spec.far_future_epoch, - withdrawable_epoch: spec.far_future_epoch, - effective_balance: std::cmp::min( - amount - amount % (spec.effective_balance_increment), - spec.max_effective_balance, - ), - slashed: false, + pubkey: Arc::new(keypair.0.pk.clone().into()), + mutable: ValidatorMutable { + withdrawal_credentials: withdrawal_credentials(&keypair.1.pk), + activation_eligibility_epoch: spec.far_future_epoch, + activation_epoch: spec.far_future_epoch, + exit_epoch: spec.far_future_epoch, + withdrawable_epoch: spec.far_future_epoch, + effective_balance: std::cmp::min( + amount - amount % (spec.effective_balance_increment), + spec.max_effective_balance, + ), + slashed: false, + }, }; state.validators_mut().push(validator).unwrap(); state.balances_mut().push(amount).unwrap(); diff --git a/lcli/src/replace_state_pubkeys.rs b/lcli/src/replace_state_pubkeys.rs index 0f9fac3aff9..5d8421d6f6e 100644 --- a/lcli/src/replace_state_pubkeys.rs +++ b/lcli/src/replace_state_pubkeys.rs @@ -42,7 +42,8 @@ pub fn run(testnet_dir: PathBuf, matches: &ArgMatches) -> Result<(), let mut deposit_tree = DepositDataTree::create(&[], 0, DEPOSIT_TREE_DEPTH); let mut deposit_root = Hash256::zero(); - for (index, validator) in state.validators_mut().iter_mut().enumerate() { + let validators = state.validators_mut(); + for index in 0..validators.len() { let (secret, _) = recover_validator_secret_from_mnemonic(seed.as_bytes(), index as u32, KeyType::Voting) .map_err(|e| format!("Unable to generate validator key: {:?}", e))?; @@ -52,11 +53,14 @@ pub fn run(testnet_dir: PathBuf, matches: &ArgMatches) -> Result<(), eprintln!("{}: {}", index, keypair.pk); - validator.pubkey = keypair.pk.into(); + validators + .get_mut(index) + .unwrap() + .replace_pubkey(keypair.pk.into()); // Update the deposit tree. let mut deposit_data = DepositData { - pubkey: validator.pubkey, + pubkey: *validators.get(index).unwrap().pubkey(), // Set this to a junk value since it's very time consuming to generate the withdrawal // keys and it's not useful for the time being. withdrawal_credentials: Hash256::zero(), diff --git a/lcli/src/skip_slots.rs b/lcli/src/skip_slots.rs index 9e5da7709f1..d421c077d83 100644 --- a/lcli/src/skip_slots.rs +++ b/lcli/src/skip_slots.rs @@ -57,7 +57,7 @@ use std::fs::File; use std::io::prelude::*; use std::path::PathBuf; use std::time::{Duration, Instant}; -use types::{BeaconState, CloneConfig, EthSpec, Hash256}; +use types::{BeaconState, EthSpec, Hash256}; const HTTP_TIMEOUT: Duration = Duration::from_secs(10); @@ -128,7 +128,7 @@ pub fn run( }; for i in 0..runs { - let mut state = state.clone_with(CloneConfig::all()); + let mut state = state.clone(); let start = Instant::now(); diff --git a/lcli/src/state_diff.rs b/lcli/src/state_diff.rs new file mode 100644 index 00000000000..278b5bf0ee4 --- /dev/null +++ b/lcli/src/state_diff.rs @@ -0,0 +1,42 @@ +use crate::transition_blocks::load_from_ssz_with; +use clap::ArgMatches; +use clap_utils::parse_required; +use environment::Environment; +use std::path::PathBuf; +use store::hdiff::{HDiff, HDiffBuffer}; +use types::{BeaconState, EthSpec}; + +pub fn run(_env: Environment, matches: &ArgMatches) -> Result<(), String> { + let state1_path: PathBuf = parse_required(matches, "state1")?; + let state2_path: PathBuf = parse_required(matches, "state2")?; + let spec = &T::default_spec(); + + let state1 = load_from_ssz_with(&state1_path, spec, BeaconState::::from_ssz_bytes)?; + let state2 = load_from_ssz_with(&state2_path, spec, BeaconState::::from_ssz_bytes)?; + + let buffer1 = HDiffBuffer::from_state(state1.clone()); + let buffer2 = HDiffBuffer::from_state(state2.clone()); + + let t = std::time::Instant::now(); + let diff = HDiff::compute(&buffer1, &buffer2).unwrap(); + let elapsed = t.elapsed(); + + println!("Diff size"); + println!("- state: {} bytes", diff.state_diff_len()); + println!("- balances: {} bytes", diff.balances_diff_len()); + println!("Computation time: {}ms", elapsed.as_millis()); + + // Re-apply. + let mut recon_buffer = HDiffBuffer::from_state(state1); + + let t = std::time::Instant::now(); + diff.apply(&mut recon_buffer).unwrap(); + + println!("Diff application time: {}ms", t.elapsed().as_millis()); + + let recon = recon_buffer.into_state(spec).unwrap(); + + assert_eq!(state2, recon); + + Ok(()) +} diff --git a/lcli/src/transition_blocks.rs b/lcli/src/transition_blocks.rs index c72b41b1d44..bab1649d147 100644 --- a/lcli/src/transition_blocks.rs +++ b/lcli/src/transition_blocks.rs @@ -85,7 +85,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::{Duration, Instant}; use store::HotColdDB; -use types::{BeaconState, ChainSpec, CloneConfig, EthSpec, Hash256, SignedBeaconBlock}; +use types::{BeaconState, ChainSpec, EthSpec, Hash256, SignedBeaconBlock}; const HTTP_TIMEOUT: Duration = Duration::from_secs(10); @@ -201,7 +201,10 @@ pub fn run( let store = Arc::new(store); debug!("Building pubkey cache (might take some time)"); - let validator_pubkey_cache = ValidatorPubkeyCache::new(&pre_state, store) + let validator_pubkey_cache = store.immutable_validators.clone(); + validator_pubkey_cache + .write() + .import_new_pubkeys(&pre_state) .map_err(|e| format!("Failed to create pubkey cache: {:?}", e))?; /* @@ -234,7 +237,7 @@ pub fn run( let mut output_post_state = None; let mut saved_ctxt = None; for i in 0..runs { - let pre_state = pre_state.clone_with(CloneConfig::all()); + let pre_state = pre_state.clone(); let block = block.clone(); let start = Instant::now(); @@ -245,7 +248,7 @@ pub fn run( block, state_root_opt, &config, - &validator_pubkey_cache, + &*validator_pubkey_cache.read(), &mut saved_ctxt, spec, )?; diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 54faa03a31f..e4ba72bc244 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lighthouse" -version = "5.1.3" +version = "5.1.222-exp" authors = ["Sigma Prime "] edition = { workspace = true } autotests = false @@ -51,6 +51,7 @@ task_executor = { workspace = true } malloc_utils = { workspace = true } directory = { workspace = true } unused_port = { workspace = true } +store = { workspace = true } database_manager = { path = "../database_manager" } slasher = { workspace = true } validator_manager = { path = "../validator_manager" } diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 68d8e46eb02..3e56a8dbff6 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -17,6 +17,7 @@ use std::process::Command; use std::str::FromStr; use std::string::ToString; use std::time::Duration; +use store::hdiff::HierarchyConfig; use tempfile::TempDir; use types::non_zero_usize::new_non_zero_usize; use types::{Address, Checkpoint, Epoch, ExecutionBlockHash, ForkName, Hash256, MainnetEthSpec}; @@ -62,6 +63,7 @@ impl CommandLineTest { fn run_with_zero_port_and_no_genesis_sync(&mut self) -> CompletedTest { self.cmd.arg("-z"); + self.cmd.arg("--unsafe-and-dangerous-mode"); self.run() } } @@ -172,26 +174,6 @@ fn shuffling_cache_set() { .with_config(|config| assert_eq!(config.chain.shuffling_cache_size, 500)); } -#[test] -fn snapshot_cache_default() { - CommandLineTest::new() - .run_with_zero_port() - .with_config(|config| { - assert_eq!( - config.chain.snapshot_cache_size, - beacon_node::beacon_chain::snapshot_cache::DEFAULT_SNAPSHOT_CACHE_SIZE - ) - }); -} - -#[test] -fn snapshot_cache_set() { - CommandLineTest::new() - .flag("state-cache-size", Some("500")) - .run_with_zero_port() - .with_config(|config| assert_eq!(config.chain.snapshot_cache_size, 500)); -} - #[test] fn fork_choice_before_proposal_timeout_default() { CommandLineTest::new() @@ -409,6 +391,35 @@ fn eth1_cache_follow_distance_manual() { assert_eq!(config.eth1.cache_follow_distance(), 128); }); } +#[test] +fn hierarchy_exponents_default() { + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| { + assert_eq!(config.store.hierarchy_config, HierarchyConfig::default()); + }); +} +#[test] +fn hierarchy_exponents_valid() { + CommandLineTest::new() + .flag("hierarchy-exponents", Some("3,6,9,12")) + .run_with_zero_port() + .with_config(|config| { + assert_eq!( + config.store.hierarchy_config, + HierarchyConfig { + exponents: vec![3, 6, 9, 12] + } + ); + }); +} +#[test] +#[should_panic] +fn hierarchy_exponents_invalid_order() { + CommandLineTest::new() + .flag("hierarchy-exponents", Some("7,6,9,12")) + .run_with_zero_port(); +} // Tests for Bellatrix flags. fn run_merge_execution_endpoints_flag_test(flag: &str) { @@ -877,6 +888,7 @@ fn network_port_flag_over_ipv4() { let port = 0; CommandLineTest::new() .flag("port", Some(port.to_string().as_str())) + .flag("unsafe-and-dangerous-mode", None) .flag("allow-insecure-genesis-sync", None) .run() .with_config(|config| { @@ -894,6 +906,7 @@ fn network_port_flag_over_ipv4() { let port = unused_tcp4_port().expect("Unable to find unused port."); CommandLineTest::new() .flag("port", Some(port.to_string().as_str())) + .flag("unsafe-and-dangerous-mode", None) .flag("allow-insecure-genesis-sync", None) .run() .with_config(|config| { @@ -914,6 +927,7 @@ fn network_port_flag_over_ipv6() { CommandLineTest::new() .flag("listen-address", Some("::1")) .flag("port", Some(port.to_string().as_str())) + .flag("unsafe-and-dangerous-mode", None) .flag("allow-insecure-genesis-sync", None) .run() .with_config(|config| { @@ -932,6 +946,7 @@ fn network_port_flag_over_ipv6() { CommandLineTest::new() .flag("listen-address", Some("::1")) .flag("port", Some(port.to_string().as_str())) + .flag("unsafe-and-dangerous-mode", None) .flag("allow-insecure-genesis-sync", None) .run() .with_config(|config| { @@ -955,6 +970,7 @@ fn network_port_flag_over_ipv4_and_ipv6() { .flag("listen-address", Some("::1")) .flag("port", Some(port.to_string().as_str())) .flag("port6", Some(port6.to_string().as_str())) + .flag("unsafe-and-dangerous-mode", None) .flag("allow-insecure-genesis-sync", None) .run() .with_config(|config| { @@ -986,6 +1002,7 @@ fn network_port_flag_over_ipv4_and_ipv6() { .flag("port", Some(port.to_string().as_str())) .flag("port6", Some(port6.to_string().as_str())) .flag("allow-insecure-genesis-sync", None) + .flag("unsafe-and-dangerous-mode", None) .run() .with_config(|config| { assert_eq!( @@ -1016,6 +1033,7 @@ fn network_port_and_discovery_port_flags_over_ipv4() { .flag("port", Some(tcp4_port.to_string().as_str())) .flag("discovery-port", Some(disc4_port.to_string().as_str())) .flag("allow-insecure-genesis-sync", None) + .flag("unsafe-and-dangerous-mode", None) .run() .with_config(|config| { assert_eq!( @@ -1037,6 +1055,7 @@ fn network_port_and_discovery_port_flags_over_ipv6() { .flag("port", Some(tcp6_port.to_string().as_str())) .flag("discovery-port", Some(disc6_port.to_string().as_str())) .flag("allow-insecure-genesis-sync", None) + .flag("unsafe-and-dangerous-mode", None) .run() .with_config(|config| { assert_eq!( @@ -1063,6 +1082,7 @@ fn network_port_and_discovery_port_flags_over_ipv4_and_ipv6() { .flag("port6", Some(tcp6_port.to_string().as_str())) .flag("discovery-port6", Some(disc6_port.to_string().as_str())) .flag("allow-insecure-genesis-sync", None) + .flag("unsafe-and-dangerous-mode", None) .run() .with_config(|config| { assert_eq!( @@ -1103,6 +1123,7 @@ fn network_port_discovery_quic_port_flags_over_ipv4_and_ipv6() { .flag("discovery-port6", Some(disc6_port.to_string().as_str())) .flag("quic-port6", Some(quic6_port.to_string().as_str())) .flag("allow-insecure-genesis-sync", None) + .flag("unsafe-and-dangerous-mode", None) .run() .with_config(|config| { assert_eq!( @@ -1329,6 +1350,7 @@ fn enr_match_flag_over_ipv4() { .flag("discovery-port", Some(udp4_port.to_string().as_str())) .flag("port", Some(tcp4_port.to_string().as_str())) .flag("allow-insecure-genesis-sync", None) + .flag("unsafe-and-dangerous-mode", None) .run() .with_config(|config| { assert_eq!( @@ -1360,6 +1382,7 @@ fn enr_match_flag_over_ipv6() { .flag("discovery-port", Some(udp6_port.to_string().as_str())) .flag("port", Some(tcp6_port.to_string().as_str())) .flag("allow-insecure-genesis-sync", None) + .flag("unsafe-and-dangerous-mode", None) .run() .with_config(|config| { assert_eq!( @@ -1399,6 +1422,7 @@ fn enr_match_flag_over_ipv4_and_ipv6() { .flag("discovery-port6", Some(udp6_port.to_string().as_str())) .flag("port6", Some(tcp6_port.to_string().as_str())) .flag("allow-insecure-genesis-sync", None) + .flag("unsafe-and-dangerous-mode", None) .run() .with_config(|config| { assert_eq!( @@ -1526,6 +1550,7 @@ fn http_port_flag() { .flag("http-port", Some(port1.to_string().as_str())) .flag("port", Some(port2.to_string().as_str())) .flag("allow-insecure-genesis-sync", None) + .flag("unsafe-and-dangerous-mode", None) .run() .with_config(|config| assert_eq!(config.http_api.listen_port, port1)); } @@ -1684,6 +1709,7 @@ fn metrics_port_flag() { .flag("metrics-port", Some(port1.to_string().as_str())) .flag("port", Some(port2.to_string().as_str())) .flag("allow-insecure-genesis-sync", None) + .flag("unsafe-and-dangerous-mode", None) .run() .with_config(|config| assert_eq!(config.http_metrics.listen_port, port1)); } @@ -1775,48 +1801,6 @@ fn validator_monitor_metrics_threshold_custom() { }); } -// Tests for Store flags. -#[test] -fn slots_per_restore_point_flag() { - CommandLineTest::new() - .flag("slots-per-restore-point", Some("64")) - .run_with_zero_port() - .with_config(|config| assert_eq!(config.store.slots_per_restore_point, 64)); -} -#[test] -fn slots_per_restore_point_update_prev_default() { - use beacon_node::beacon_chain::store::config::{ - DEFAULT_SLOTS_PER_RESTORE_POINT, PREV_DEFAULT_SLOTS_PER_RESTORE_POINT, - }; - - CommandLineTest::new() - .flag("slots-per-restore-point", Some("2048")) - .run_with_zero_port() - .with_config_and_dir(|config, dir| { - // Check that 2048 is the previous default. - assert_eq!( - config.store.slots_per_restore_point, - PREV_DEFAULT_SLOTS_PER_RESTORE_POINT - ); - - // Restart the BN with the same datadir and the new default SPRP. It should - // allow this. - CommandLineTest::new() - .flag("datadir", Some(&dir.path().display().to_string())) - .flag("zero-ports", None) - .run_with_no_datadir() - .with_config(|config| { - // The dumped config will have the new default 8192 value, but the fact that - // the BN started and ran (with the same datadir) means that the override - // was successful. - assert_eq!( - config.store.slots_per_restore_point, - DEFAULT_SLOTS_PER_RESTORE_POINT - ); - }); - }) -} - #[test] fn block_cache_size_flag() { CommandLineTest::new() @@ -1849,6 +1833,25 @@ fn historic_state_cache_size_default() { }); } #[test] +fn parallel_state_cache_size_flag() { + CommandLineTest::new() + .flag("parallel-state-cache-size", Some("4")) + .run_with_zero_port() + .with_config(|config| assert_eq!(config.chain.parallel_state_cache_size, 4_usize)); +} +#[test] +fn parallel_state_cache_size_default() { + use beacon_node::beacon_chain::chain_config::DEFAULT_PARALLEL_STATE_CACHE_SIZE; + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| { + assert_eq!( + config.chain.parallel_state_cache_size, + DEFAULT_PARALLEL_STATE_CACHE_SIZE + ); + }); +} +#[test] fn auto_compact_db_flag() { CommandLineTest::new() .flag("auto-compact-db", Some("false")) @@ -1947,6 +1950,20 @@ fn epochs_per_migration_override() { .with_config(|config| assert_eq!(config.chain.epochs_per_migration, 128)); } +#[test] +fn epochs_per_state_diff_default() { + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| assert_eq!(config.store.epochs_per_state_diff, 16)); +} +#[test] +fn epochs_per_state_diff_override() { + CommandLineTest::new() + .flag("epochs-per-state-diff", Some("1")) + .run_with_zero_port() + .with_config(|config| assert_eq!(config.store.epochs_per_state_diff, 1)); +} + // Tests for Slasher flags. // Using `--slasher-max-db-size` to work around https://github.com/sigp/lighthouse/issues/2342 #[test] diff --git a/scripts/cross/Dockerfile b/scripts/cross/Dockerfile new file mode 100644 index 00000000000..9456b666ef1 --- /dev/null +++ b/scripts/cross/Dockerfile @@ -0,0 +1,25 @@ +ARG CROSS_BASE_IMAGE +FROM $CROSS_BASE_IMAGE + +RUN apt-get update -y && apt-get upgrade -y + +RUN apt-get install -y unzip && \ + PB_REL="https://github.com/protocolbuffers/protobuf/releases" && \ + curl -L $PB_REL/download/v3.15.8/protoc-3.15.8-linux-x86_64.zip -o protoc.zip && \ + unzip protoc.zip -d /usr && \ + chmod +x /usr/bin/protoc + +RUN apt-get install -y \ + apt-transport-https \ + ca-certificates \ + gnupg-agent \ + software-properties-common + +RUN curl -L https://apt.llvm.org/llvm-snapshot.gpg.key | apt-key add - && \ + apt-add-repository "deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial-5.0 main" && \ + apt-get update && \ + apt-get install -y clang-5.0 + +RUN apt-get install -y cmake + +ENV PROTOC=/usr/bin/protoc diff --git a/scripts/local_testnet/beacon_node.sh b/scripts/local_testnet/beacon_node.sh index 2660dfa3c00..813fb47886b 100755 --- a/scripts/local_testnet/beacon_node.sh +++ b/scripts/local_testnet/beacon_node.sh @@ -68,4 +68,5 @@ exec $lighthouse_binary \ --execution-endpoint $execution_endpoint \ --execution-jwt $execution_jwt \ --http-allow-sync-stalled \ + --unsafe-and-dangerous-mode \ $BN_ARGS diff --git a/testing/ef_tests/Cargo.toml b/testing/ef_tests/Cargo.toml index f3d00fa035c..81f90ba9886 100644 --- a/testing/ef_tests/Cargo.toml +++ b/testing/ef_tests/Cargo.toml @@ -38,5 +38,5 @@ fs2 = { workspace = true } beacon_chain = { workspace = true } store = { workspace = true } fork_choice = { workspace = true } -execution_layer = { workspace = true } logging = { workspace = true } +execution_layer = { workspace = true } diff --git a/testing/ef_tests/src/case_result.rs b/testing/ef_tests/src/case_result.rs index 67ab9c51bbf..a038cd70899 100644 --- a/testing/ef_tests/src/case_result.rs +++ b/testing/ef_tests/src/case_result.rs @@ -2,6 +2,7 @@ use super::*; use compare_fields::{CompareFields, Comparison, FieldComparison}; use std::fmt::Debug; use std::path::{Path, PathBuf}; +use store::hdiff::{HDiff, HDiffBuffer}; use types::BeaconState; pub const MAX_VALUE_STRING_LEN: usize = 500; @@ -39,6 +40,9 @@ pub fn compare_beacon_state_results_without_caches( if let (Ok(ref mut result), Some(ref mut expected)) = (result.as_mut(), expected.as_mut()) { result.drop_all_caches().unwrap(); expected.drop_all_caches().unwrap(); + + result.apply_pending_mutations().unwrap(); + expected.apply_pending_mutations().unwrap(); } compare_result_detailed(result, expected) @@ -115,6 +119,36 @@ where } } +pub fn check_state_diff( + pre_state: &BeaconState, + opt_post_state: &Option>, + spec: &ChainSpec, +) -> Result<(), Error> { + if let Some(post_state) = opt_post_state { + // Produce a diff between the pre- and post-states. + let pre_state_buf = HDiffBuffer::from_state(pre_state.clone()); + let post_state_buf = HDiffBuffer::from_state(post_state.clone()); + let diff = HDiff::compute(&pre_state_buf, &post_state_buf).expect("HDiff should compute"); + + // Apply the diff to the pre-state, ensuring the same post-state is + // regenerated. + let mut reconstructed_buf = HDiffBuffer::from_state(pre_state.clone()); + diff.apply(&mut reconstructed_buf) + .expect("HDiff should apply"); + let diffed_state = reconstructed_buf + .into_state(spec) + .expect("HDiffDiffer should convert to state"); + + // Drop the caches on the post-state to assist with equality checking. + let mut post_state_without_caches = post_state.clone(); + post_state_without_caches.drop_all_caches().unwrap(); + + compare_result_detailed::<_, ()>(&Ok(diffed_state), &Some(post_state_without_caches)) + } else { + Ok(()) + } +} + fn fmt_val(val: T) -> String { let mut string = format!("{:?}", val); string.truncate(MAX_VALUE_STRING_LEN); diff --git a/testing/ef_tests/src/cases/bls_verify_msg.rs b/testing/ef_tests/src/cases/bls_verify_msg.rs index 42ee459a607..31fb16a4df4 100644 --- a/testing/ef_tests/src/cases/bls_verify_msg.rs +++ b/testing/ef_tests/src/cases/bls_verify_msg.rs @@ -1,7 +1,7 @@ use super::*; use crate::case_result::compare_result; use crate::impl_bls_load_case; -use bls::{PublicKeyBytes, Signature, SignatureBytes}; +use bls::{PublicKey, PublicKeyBytes, Signature, SignatureBytes}; use serde::Deserialize; use types::Hash256; @@ -29,6 +29,13 @@ impl Case for BlsVerify { .try_into() .and_then(|signature: Signature| { let pk = self.input.pubkey.decompress()?; + + // Check serialization roundtrip. + let pk_uncompressed = pk.serialize_uncompressed(); + let pk_from_uncompressed = PublicKey::deserialize_uncompressed(&pk_uncompressed) + .expect("uncompressed serialization should round-trip"); + assert_eq!(pk_from_uncompressed, pk); + Ok(signature.verify(&pk, Hash256::from_slice(&message))) }) .unwrap_or(false); diff --git a/testing/ef_tests/src/cases/epoch_processing.rs b/testing/ef_tests/src/cases/epoch_processing.rs index a9c77c53c52..cc60a63a444 100644 --- a/testing/ef_tests/src/cases/epoch_processing.rs +++ b/testing/ef_tests/src/cases/epoch_processing.rs @@ -1,6 +1,6 @@ use super::*; use crate::bls_setting::BlsSetting; -use crate::case_result::compare_beacon_state_results_without_caches; +use crate::case_result::{check_state_diff, compare_beacon_state_results_without_caches}; use crate::decode::{ssz_decode_state, yaml_decode_file}; use crate::type_name; use serde::Deserialize; @@ -342,6 +342,7 @@ impl> Case for EpochProcessing { let mut result = T::run(&mut state, spec).map(|_| state); + check_state_diff(&pre_state, &expected, spec)?; compare_beacon_state_results_without_caches(&mut result, &mut expected) } } diff --git a/testing/ef_tests/src/cases/merkle_proof_validity.rs b/testing/ef_tests/src/cases/merkle_proof_validity.rs index a2e831ade59..cf0b9f77c8f 100644 --- a/testing/ef_tests/src/cases/merkle_proof_validity.rs +++ b/testing/ef_tests/src/cases/merkle_proof_validity.rs @@ -51,7 +51,7 @@ impl LoadCase for MerkleProofValidity { impl Case for MerkleProofValidity { fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { let mut state = self.state.clone(); - state.initialize_tree_hash_cache(); + state.update_tree_hash_cache().unwrap(); let Ok(proof) = state.compute_merkle_proof(self.merkle_proof.leaf_index) else { return Err(Error::FailedToParseTest( "Could not retrieve merkle proof".to_string(), @@ -77,9 +77,6 @@ impl Case for MerkleProofValidity { } } - // Tree hash cache should still be initialized (not dropped). - assert!(state.tree_hash_cache().is_initialized()); - Ok(()) } } diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index a2f50896a57..8cc90058c15 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -1,6 +1,6 @@ use super::*; use crate::bls_setting::BlsSetting; -use crate::case_result::compare_beacon_state_results_without_caches; +use crate::case_result::{check_state_diff, compare_beacon_state_results_without_caches}; use crate::decode::{ssz_decode_file, ssz_decode_file_with, ssz_decode_state, yaml_decode_file}; use serde::Deserialize; use ssz::Decode; @@ -511,6 +511,7 @@ impl> Case for Operations { .apply_to(&mut state, spec, self) .map(|()| state); + check_state_diff(&pre_state, &expected, spec)?; compare_beacon_state_results_without_caches(&mut result, &mut expected) } } diff --git a/testing/ef_tests/src/cases/sanity_blocks.rs b/testing/ef_tests/src/cases/sanity_blocks.rs index b0902cb5b74..11a5632b861 100644 --- a/testing/ef_tests/src/cases/sanity_blocks.rs +++ b/testing/ef_tests/src/cases/sanity_blocks.rs @@ -1,6 +1,6 @@ use super::*; use crate::bls_setting::BlsSetting; -use crate::case_result::compare_beacon_state_results_without_caches; +use crate::case_result::{check_state_diff, compare_beacon_state_results_without_caches}; use crate::decode::{ssz_decode_file_with, ssz_decode_state, yaml_decode_file}; use serde::Deserialize; use state_processing::{ @@ -128,6 +128,16 @@ impl Case for SanityBlocks { Ok(res) => (Ok(res.0), Ok(res.1)), }; compare_beacon_state_results_without_caches(&mut indiv_result, &mut expected)?; - compare_beacon_state_results_without_caches(&mut bulk_result, &mut expected) + compare_beacon_state_results_without_caches(&mut bulk_result, &mut expected)?; + + // Check state diff (requires fully built committee caches). + let mut pre = self.pre.clone(); + pre.build_all_committee_caches(spec).unwrap(); + let post = self.post.clone().map(|mut post| { + post.build_all_committee_caches(spec).unwrap(); + post + }); + check_state_diff(&pre, &post, spec)?; + Ok(()) } } diff --git a/testing/ef_tests/src/cases/sanity_slots.rs b/testing/ef_tests/src/cases/sanity_slots.rs index 71c782c78f4..34963fc6f66 100644 --- a/testing/ef_tests/src/cases/sanity_slots.rs +++ b/testing/ef_tests/src/cases/sanity_slots.rs @@ -1,6 +1,6 @@ use super::*; use crate::bls_setting::BlsSetting; -use crate::case_result::compare_beacon_state_results_without_caches; +use crate::case_result::{check_state_diff, compare_beacon_state_results_without_caches}; use crate::decode::{ssz_decode_state, yaml_decode_file}; use serde::Deserialize; use state_processing::per_slot_processing; @@ -67,6 +67,15 @@ impl Case for SanitySlots { .try_for_each(|_| per_slot_processing(&mut state, None, spec).map(|_| ())) .map(|_| state); - compare_beacon_state_results_without_caches(&mut result, &mut expected) + compare_beacon_state_results_without_caches(&mut result, &mut expected)?; + + // Check state diff (requires fully built committee caches). + let mut pre = self.pre.clone(); + pre.build_all_committee_caches(spec).unwrap(); + let post = self.post.clone().map(|mut post| { + post.build_all_committee_caches(spec).unwrap(); + post + }); + check_state_diff(&pre, &post, spec) } } diff --git a/testing/ef_tests/src/cases/ssz_generic.rs b/testing/ef_tests/src/cases/ssz_generic.rs index bb2465aae10..e620f4509fc 100644 --- a/testing/ef_tests/src/cases/ssz_generic.rs +++ b/testing/ef_tests/src/cases/ssz_generic.rs @@ -1,14 +1,14 @@ #![allow(non_snake_case)] use super::*; -use crate::cases::common::{TestU128, TestU256}; -use crate::decode::{snappy_decode_file, yaml_decode_file}; -use serde::Deserialize; -use serde::{de::Error as SerdeError, Deserializer}; +use crate::cases::common::{SszStaticType, TestU128, TestU256}; +use crate::cases::ssz_static::{check_serialization, check_tree_hash}; +use crate::decode::{log_file_access, snappy_decode_file, yaml_decode_file}; +use serde::{de::Error as SerdeError, Deserialize, Deserializer}; use ssz_derive::{Decode, Encode}; use tree_hash_derive::TreeHash; use types::typenum::*; -use types::{BitList, BitVector, FixedVector, VariableList}; +use types::{BitList, BitVector, ForkName, VariableList, Vector}; #[derive(Debug, Clone, Deserialize)] struct Metadata { @@ -125,10 +125,16 @@ impl Case for SszGeneric { let elem_ty = parts[1]; let length = parts[2]; + // Skip length 0 tests. Milhouse doesn't have any checks against 0-capacity lists. + if length == "0" { + log_file_access(self.path.join("serialized.ssz_snappy")); + return Ok(()); + } + type_dispatch!( ssz_generic_test, (&self.path), - FixedVector, + Vector, <>, [elem_ty => primitive_type] [length => typenum] @@ -263,8 +269,8 @@ struct ComplexTestStruct { #[serde(deserialize_with = "byte_list_from_hex_str")] D: VariableList, E: VarTestStruct, - F: FixedVector, - G: FixedVector, + F: Vector, + G: Vector, } #[derive(Debug, Clone, PartialEq, Decode, Encode, TreeHash, Deserialize)] diff --git a/testing/ef_tests/src/cases/ssz_static.rs b/testing/ef_tests/src/cases/ssz_static.rs index e41c90c6e03..2ad27f3f134 100644 --- a/testing/ef_tests/src/cases/ssz_static.rs +++ b/testing/ef_tests/src/cases/ssz_static.rs @@ -41,7 +41,7 @@ fn load_from_dir(path: &Path) -> Result<(SszStaticRoots, Vec Case for SszStaticTHC> { check_tree_hash(&self.roots.root, self.value.tree_hash_root().as_bytes())?; let mut state = self.value.clone(); - state.initialize_tree_hash_cache(); let cached_tree_hash_root = state.update_tree_hash_cache().unwrap(); check_tree_hash(&self.roots.root, cached_tree_hash_root.as_bytes())?; diff --git a/testing/state_transition_vectors/src/exit.rs b/testing/state_transition_vectors/src/exit.rs index e3cd346da13..4c3b0c4f44a 100644 --- a/testing/state_transition_vectors/src/exit.rs +++ b/testing/state_transition_vectors/src/exit.rs @@ -170,7 +170,7 @@ vectors_and_tests!( invalid_exit_already_initiated, ExitTest { state_modifier: Box::new(|state| { - state.validators_mut().get_mut(0).unwrap().exit_epoch = STATE_EPOCH + 1; + *state.validators_mut().get_mut(0).unwrap().exit_epoch_mut() = STATE_EPOCH + 1; }), expected: Err(BlockProcessingError::ExitInvalid { index: 0, @@ -189,8 +189,11 @@ vectors_and_tests!( invalid_not_active_before_activation_epoch, ExitTest { state_modifier: Box::new(|state| { - state.validators_mut().get_mut(0).unwrap().activation_epoch = - E::default_spec().far_future_epoch; + *state + .validators_mut() + .get_mut(0) + .unwrap() + .activation_epoch_mut() = E::default_spec().far_future_epoch; }), expected: Err(BlockProcessingError::ExitInvalid { index: 0, @@ -209,7 +212,7 @@ vectors_and_tests!( invalid_not_active_after_exit_epoch, ExitTest { state_modifier: Box::new(|state| { - state.validators_mut().get_mut(0).unwrap().exit_epoch = STATE_EPOCH; + *state.validators_mut().get_mut(0).unwrap().exit_epoch_mut() = STATE_EPOCH; }), expected: Err(BlockProcessingError::ExitInvalid { index: 0, @@ -332,17 +335,17 @@ mod custom_tests { fn assert_exited(state: &BeaconState, validator_index: usize) { let spec = E::default_spec(); - let validator = &state.validators()[validator_index]; + let validator = &state.validators().get(validator_index).unwrap(); assert_eq!( - validator.exit_epoch, + validator.exit_epoch(), // This is correct until we exceed the churn limit. If that happens, we // need to introduce more complex logic. state.current_epoch() + 1 + spec.max_seed_lookahead, "exit epoch" ); assert_eq!( - validator.withdrawable_epoch, - validator.exit_epoch + E::default_spec().min_validator_withdrawability_delay, + validator.withdrawable_epoch(), + validator.exit_epoch() + E::default_spec().min_validator_withdrawability_delay, "withdrawable epoch" ); } diff --git a/watch/src/updater/mod.rs b/watch/src/updater/mod.rs index 65e0a90a2b4..c3c8c94cdd7 100644 --- a/watch/src/updater/mod.rs +++ b/watch/src/updater/mod.rs @@ -211,20 +211,20 @@ pub async fn get_validators(bn: &BeaconNodeHttpClient) -> Result